mirror of
https://github.com/penpot/penpot.git
synced 2026-03-30 00:00:45 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
@@ -1230,6 +1230,7 @@
|
||||
{:cmd :export-shapes
|
||||
:profile-id (:profile-id @st/state)
|
||||
:wait true
|
||||
:is-wasm false
|
||||
:exports [{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id id
|
||||
|
||||
192
frontend/test/frontend_tests/helpers/wasm.cljs
Normal file
192
frontend/test/frontend_tests/helpers/wasm.cljs
Normal file
@@ -0,0 +1,192 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns frontend-tests.helpers.wasm
|
||||
"Test helpers for mocking WASM API boundary functions.
|
||||
|
||||
In the Node.js test environment the WASM binary is not available,
|
||||
but the `render-wasm/v1` feature flag is enabled by default, so
|
||||
every geometry-modifying event takes the WASM code path.
|
||||
This namespace provides lightweight mock implementations that let
|
||||
the Clojure-side logic execute normally while stubbing out every
|
||||
call that would touch the WASM heap.
|
||||
|
||||
Each mock tracks how many times it was called via `call-counts`.
|
||||
Use `(call-count :propagate-modifiers)` in test assertions to
|
||||
verify the WASM code path was exercised."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.api.fonts :as wasm.fonts]))
|
||||
|
||||
;; --- Call tracking ---------------------------------------------------
|
||||
|
||||
(def ^:private call-counts
|
||||
"Atom holding a map of mock-name → number of calls since last reset."
|
||||
(atom {}))
|
||||
|
||||
(defn- track!
|
||||
"Increment the call count for `mock-name`."
|
||||
[mock-name]
|
||||
(swap! call-counts update mock-name (fnil inc 0)))
|
||||
|
||||
(defn call-count
|
||||
"Return how many times mock `mock-name` was called since setup.
|
||||
`mock-name` is a keyword, e.g. `:propagate-modifiers`."
|
||||
[mock-name]
|
||||
(get @call-counts mock-name 0))
|
||||
|
||||
(defn reset-call-counts!
|
||||
"Reset all call counts to zero."
|
||||
[]
|
||||
(reset! call-counts {}))
|
||||
|
||||
;; --- Mock implementations --------------------------------------------
|
||||
|
||||
(defn- mock-propagate-modifiers
|
||||
"Passthrough mock for `wasm.api/propagate-modifiers`.
|
||||
|
||||
Receives `entries` — a vector of `[uuid {:transform matrix, :kind kw}]`
|
||||
pairs produced by `parse-geometry-modifiers` — and returns a vector
|
||||
of `[uuid matrix]` pairs that `apply-wasm-modifiers` converts to a
|
||||
transforms map via `(into {} result)`.
|
||||
|
||||
This effectively tells the caller \"apply exactly the transform that
|
||||
was requested\", which is what the real WASM engine does for simple
|
||||
moves / resizes without constraints."
|
||||
[entries _pixel-precision]
|
||||
(track! :propagate-modifiers)
|
||||
(when (d/not-empty? entries)
|
||||
(into []
|
||||
(map (fn [[id data]]
|
||||
(d/vec2 id (:transform data))))
|
||||
entries)))
|
||||
|
||||
(defn- mock-clean-modifiers
|
||||
[]
|
||||
(track! :clean-modifiers)
|
||||
nil)
|
||||
|
||||
(defn- mock-set-structure-modifiers
|
||||
[_entries]
|
||||
(track! :set-structure-modifiers)
|
||||
nil)
|
||||
|
||||
(defn- mock-set-shape-grow-type
|
||||
[_grow-type]
|
||||
(track! :set-shape-grow-type)
|
||||
nil)
|
||||
|
||||
(defn- mock-set-shape-text-content
|
||||
[_shape-id _content]
|
||||
(track! :set-shape-text-content)
|
||||
nil)
|
||||
|
||||
(defn- mock-set-shape-text-images
|
||||
([_shape-id _content]
|
||||
(track! :set-shape-text-images)
|
||||
nil)
|
||||
([_shape-id _content _thumbnail?]
|
||||
(track! :set-shape-text-images)
|
||||
nil))
|
||||
|
||||
(defn- mock-get-text-dimensions
|
||||
([]
|
||||
(track! :get-text-dimensions)
|
||||
{:x 0 :y 0 :width 100 :height 20 :max-width 100})
|
||||
([_id]
|
||||
(track! :get-text-dimensions)
|
||||
{:x 0 :y 0 :width 100 :height 20 :max-width 100}))
|
||||
|
||||
(defn- mock-font-stored?
|
||||
[_font-data _emoji?]
|
||||
(track! :font-stored?)
|
||||
true)
|
||||
|
||||
(defn- mock-make-font-data
|
||||
[font]
|
||||
(track! :make-font-data)
|
||||
{:wasm-id 0
|
||||
:weight (or (:font-weight font) "400")
|
||||
:style (or (:font-style font) "normal")
|
||||
:emoji? false})
|
||||
|
||||
(defn- mock-get-content-fonts
|
||||
[_content]
|
||||
(track! :get-content-fonts)
|
||||
[])
|
||||
|
||||
;; --- Persistent mock installation via `set!` --------------------------
|
||||
;;
|
||||
;; These use `set!` to directly mutate the module-level JS vars, making
|
||||
;; the mocks persist across async boundaries. They are intended to be
|
||||
;; used with `t/use-fixtures :each` which correctly sequences `:after`
|
||||
;; to run only after the async test's `done` callback fires.
|
||||
|
||||
(def ^:private originals
|
||||
"Stores the original WASM function values so they can be restored."
|
||||
(atom {}))
|
||||
|
||||
(defn setup-wasm-mocks!
|
||||
"Install WASM mocks via `set!` that persist across async boundaries.
|
||||
Also resets call counts. Call `teardown-wasm-mocks!` to restore."
|
||||
[]
|
||||
;; Reset call tracking
|
||||
(reset-call-counts!)
|
||||
;; Save originals
|
||||
(reset! originals
|
||||
{:clean-modifiers wasm.api/clean-modifiers
|
||||
:set-structure-modifiers wasm.api/set-structure-modifiers
|
||||
:propagate-modifiers wasm.api/propagate-modifiers
|
||||
:set-shape-grow-type wasm.api/set-shape-grow-type
|
||||
:set-shape-text-content wasm.api/set-shape-text-content
|
||||
:set-shape-text-images wasm.api/set-shape-text-images
|
||||
:get-text-dimensions wasm.api/get-text-dimensions
|
||||
:font-stored? wasm.fonts/font-stored?
|
||||
:make-font-data wasm.fonts/make-font-data
|
||||
:get-content-fonts wasm.fonts/get-content-fonts})
|
||||
;; Install mocks
|
||||
(set! wasm.api/clean-modifiers mock-clean-modifiers)
|
||||
(set! wasm.api/set-structure-modifiers mock-set-structure-modifiers)
|
||||
(set! wasm.api/propagate-modifiers mock-propagate-modifiers)
|
||||
(set! wasm.api/set-shape-grow-type mock-set-shape-grow-type)
|
||||
(set! wasm.api/set-shape-text-content mock-set-shape-text-content)
|
||||
(set! wasm.api/set-shape-text-images mock-set-shape-text-images)
|
||||
(set! wasm.api/get-text-dimensions mock-get-text-dimensions)
|
||||
(set! wasm.fonts/font-stored? mock-font-stored?)
|
||||
(set! wasm.fonts/make-font-data mock-make-font-data)
|
||||
(set! wasm.fonts/get-content-fonts mock-get-content-fonts))
|
||||
|
||||
(defn teardown-wasm-mocks!
|
||||
"Restore the original WASM functions saved by `setup-wasm-mocks!`."
|
||||
[]
|
||||
(let [orig @originals]
|
||||
(set! wasm.api/clean-modifiers (:clean-modifiers orig))
|
||||
(set! wasm.api/set-structure-modifiers (:set-structure-modifiers orig))
|
||||
(set! wasm.api/propagate-modifiers (:propagate-modifiers orig))
|
||||
(set! wasm.api/set-shape-grow-type (:set-shape-grow-type orig))
|
||||
(set! wasm.api/set-shape-text-content (:set-shape-text-content orig))
|
||||
(set! wasm.api/set-shape-text-images (:set-shape-text-images orig))
|
||||
(set! wasm.api/get-text-dimensions (:get-text-dimensions orig))
|
||||
(set! wasm.fonts/font-stored? (:font-stored? orig))
|
||||
(set! wasm.fonts/make-font-data (:make-font-data orig))
|
||||
(set! wasm.fonts/get-content-fonts (:get-content-fonts orig)))
|
||||
(reset! originals {}))
|
||||
|
||||
(defn with-wasm-mocks*
|
||||
"Calls `(thunk)` with all WASM API boundary functions replaced by
|
||||
safe mocks, restoring the originals when the thunk returns.
|
||||
|
||||
NOTE: Teardown happens synchronously when `thunk` returns. For
|
||||
async tests (e.g. those using `tohs/run-store-async`), use
|
||||
`setup-wasm-mocks!` / `teardown-wasm-mocks!` via
|
||||
`t/use-fixtures :each` instead."
|
||||
[thunk]
|
||||
(setup-wasm-mocks!)
|
||||
(try
|
||||
(thunk)
|
||||
(finally
|
||||
(teardown-wasm-mocks!))))
|
||||
@@ -23,11 +23,15 @@
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.pages :as thp]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
[frontend-tests.tokens.helpers.state :as tohs]
|
||||
[frontend-tests.tokens.helpers.tokens :as toht]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
{:before (fn []
|
||||
(thp/reset-idmap!)
|
||||
(thw/setup-wasm-mocks!))
|
||||
:after thw/teardown-wasm-mocks!})
|
||||
|
||||
(defn- setup-base-file
|
||||
[]
|
||||
@@ -426,7 +430,10 @@
|
||||
(t/is (mth/close? (get c-frame1' :width) 200))
|
||||
(t/is (mth/close? (get c-frame1' :height) 200))
|
||||
|
||||
(t/is (empty? (:touched c-frame1'))))))))]
|
||||
(t/is (empty? (:touched c-frame1')))
|
||||
|
||||
(t/testing "WASM mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :propagate-modifiers)))))))))]
|
||||
|
||||
(tohs/run-store-async
|
||||
store step2 events identity))))
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
[app.main.data.workspace.guides :as-alias dwg]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.pages :as thp]
|
||||
[frontend-tests.helpers.state :as ths]))
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
@@ -22,35 +23,43 @@
|
||||
(t/deftest test-remove-swap-slot-copy-paste-blue1-to-root
|
||||
(t/async
|
||||
done
|
||||
(let [;; ==== Setup
|
||||
file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-frame :frame1))
|
||||
store (ths/setup-store file)
|
||||
frame1 (cths/get-shape file :frame1)
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [;; ==== Setup
|
||||
file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-frame :frame1))
|
||||
store (ths/setup-store file)
|
||||
frame1 (cths/get-shape file :frame1)
|
||||
|
||||
guide {:axis :x
|
||||
:frame-id (:id frame1)
|
||||
:id (uuid/next)
|
||||
:position 0}
|
||||
guide {:axis :x
|
||||
:frame-id (:id frame1)
|
||||
:id (uuid/next)
|
||||
:position 0}
|
||||
|
||||
;; ==== Action
|
||||
events
|
||||
[(dw/update-guides guide)
|
||||
(dw/update-position (:id frame1) {:x 100})]]
|
||||
;; ==== Action
|
||||
events
|
||||
[(dw/update-guides guide)
|
||||
(dw/update-position (:id frame1) {:x 100})]]
|
||||
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [;; ==== Get
|
||||
file' (ths/get-file-from-state new-state)
|
||||
page' (cthf/current-page file')
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [new-state]
|
||||
(let [;; ==== Get
|
||||
file' (ths/get-file-from-state new-state)
|
||||
page' (cthf/current-page file')
|
||||
|
||||
guide' (-> page'
|
||||
:guides
|
||||
(vals)
|
||||
(first))]
|
||||
;; ==== Check
|
||||
;; guide has moved
|
||||
(t/is (= (:position guide') 100))
|
||||
|
||||
;; WASM mocks were exercised
|
||||
(t/is (pos? (thw/call-count :clean-modifiers)))
|
||||
(t/is (pos? (thw/call-count :set-structure-modifiers)))
|
||||
(t/is (pos? (thw/call-count :propagate-modifiers)))))))))))
|
||||
|
||||
guide' (-> page'
|
||||
:guides
|
||||
(vals)
|
||||
(first))]
|
||||
;; ==== Check
|
||||
;; guide has moved
|
||||
(t/is (= (:position guide') 100))))))))
|
||||
|
||||
|
||||
|
||||
@@ -12,242 +12,250 @@
|
||||
[app.main.store :as st]
|
||||
[app.plugins.api :as api]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]))
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(t/deftest test-common-shape-properties
|
||||
(let [;; ==== Setup
|
||||
store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
(thw/with-wasm-mocks*
|
||||
(fn []
|
||||
(let [;; ==== Setup
|
||||
store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
||||
|
||||
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
^js context (api/create-context "00000000-0000-0000-0000-000000000000")
|
||||
|
||||
_ (set! st/state store)
|
||||
_ (set! st/state store)
|
||||
|
||||
^js file (. context -currentFile)
|
||||
^js page (. context -currentPage)
|
||||
^js shape (.createRectangle context)
|
||||
^js file (. context -currentFile)
|
||||
^js page (. context -currentPage)
|
||||
^js shape (.createRectangle context)
|
||||
|
||||
get-shape-path
|
||||
#(vector :files (aget file "$id") :data :pages-index (aget page "$id") :objects (aget shape "$id") %)]
|
||||
get-shape-path
|
||||
#(vector :files (aget file "$id") :data :pages-index (aget page "$id") :objects (aget shape "$id") %)]
|
||||
|
||||
(t/testing "Basic shape properites"
|
||||
(t/testing " - name"
|
||||
(set! (.-name shape) "TEST")
|
||||
(t/is (= (.-name shape) "TEST"))
|
||||
(t/is (= (get-in @store (get-shape-path :name)) "TEST")))
|
||||
(t/testing "Basic shape properites"
|
||||
(t/testing " - name"
|
||||
(set! (.-name shape) "TEST")
|
||||
(t/is (= (.-name shape) "TEST"))
|
||||
(t/is (= (get-in @store (get-shape-path :name)) "TEST")))
|
||||
|
||||
(t/testing " - x"
|
||||
(set! (.-x shape) 10)
|
||||
(t/is (= (.-x shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :x)) 10))
|
||||
(t/testing " - x"
|
||||
(set! (.-x shape) 10)
|
||||
(t/is (= (.-x shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :x)) 10))
|
||||
|
||||
(set! (.-x shape) "fail")
|
||||
(t/is (= (.-x shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :x)) 10)))
|
||||
(set! (.-x shape) "fail")
|
||||
(t/is (= (.-x shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :x)) 10)))
|
||||
|
||||
(t/testing " - y"
|
||||
(set! (.-y shape) 50)
|
||||
(t/is (= (.-y shape) 50))
|
||||
(t/is (= (get-in @store (get-shape-path :y)) 50))
|
||||
(t/testing " - y"
|
||||
(set! (.-y shape) 50)
|
||||
(t/is (= (.-y shape) 50))
|
||||
(t/is (= (get-in @store (get-shape-path :y)) 50))
|
||||
|
||||
(set! (.-y shape) "fail")
|
||||
(t/is (= (.-y shape) 50))
|
||||
(t/is (= (get-in @store (get-shape-path :y)) 50)))
|
||||
(set! (.-y shape) "fail")
|
||||
(t/is (= (.-y shape) 50))
|
||||
(t/is (= (get-in @store (get-shape-path :y)) 50)))
|
||||
|
||||
(t/testing " - resize"
|
||||
(.resize shape 250 300)
|
||||
(t/is (= (.-width shape) 250))
|
||||
(t/is (= (.-height shape) 300))
|
||||
(t/is (= (get-in @store (get-shape-path :width)) 250))
|
||||
(t/is (= (get-in @store (get-shape-path :height)) 300))
|
||||
(t/testing " - resize"
|
||||
(.resize shape 250 300)
|
||||
(t/is (= (.-width shape) 250))
|
||||
(t/is (= (.-height shape) 300))
|
||||
(t/is (= (get-in @store (get-shape-path :width)) 250))
|
||||
(t/is (= (get-in @store (get-shape-path :height)) 300))
|
||||
|
||||
(.resize shape 0 0)
|
||||
(t/is (= (.-width shape) 250))
|
||||
(t/is (= (.-height shape) 300))
|
||||
(t/is (= (get-in @store (get-shape-path :width)) 250))
|
||||
(t/is (= (get-in @store (get-shape-path :height)) 300)))
|
||||
(.resize shape 0 0)
|
||||
(t/is (= (.-width shape) 250))
|
||||
(t/is (= (.-height shape) 300))
|
||||
(t/is (= (get-in @store (get-shape-path :width)) 250))
|
||||
(t/is (= (get-in @store (get-shape-path :height)) 300)))
|
||||
|
||||
(t/testing " - blocked"
|
||||
(set! (.-blocked shape) true)
|
||||
(t/is (= (.-blocked shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :blocked)) true))
|
||||
(t/testing " - blocked"
|
||||
(set! (.-blocked shape) true)
|
||||
(t/is (= (.-blocked shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :blocked)) true))
|
||||
|
||||
(set! (.-blocked shape) false)
|
||||
(t/is (= (.-blocked shape) false))
|
||||
(t/is (= (get-in @store (get-shape-path :blocked)) false)))
|
||||
(set! (.-blocked shape) false)
|
||||
(t/is (= (.-blocked shape) false))
|
||||
(t/is (= (get-in @store (get-shape-path :blocked)) false)))
|
||||
|
||||
(t/testing " - hidden"
|
||||
(set! (.-hidden shape) true)
|
||||
(t/is (= (.-hidden shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :hidden)) true))
|
||||
(t/testing " - hidden"
|
||||
(set! (.-hidden shape) true)
|
||||
(t/is (= (.-hidden shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :hidden)) true))
|
||||
|
||||
(set! (.-hidden shape) false)
|
||||
(t/is (= (.-hidden shape) false))
|
||||
(t/is (= (get-in @store (get-shape-path :hidden)) false)))
|
||||
(set! (.-hidden shape) false)
|
||||
(t/is (= (.-hidden shape) false))
|
||||
(t/is (= (get-in @store (get-shape-path :hidden)) false)))
|
||||
|
||||
(t/testing " - proportionLock"
|
||||
(set! (.-proportionLock shape) true)
|
||||
(t/is (= (.-proportionLock shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :proportion-lock)) true)))
|
||||
(t/testing " - proportionLock"
|
||||
(set! (.-proportionLock shape) true)
|
||||
(t/is (= (.-proportionLock shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :proportion-lock)) true)))
|
||||
|
||||
(t/testing " - constraintsHorizontal"
|
||||
(set! (.-constraintsHorizontal shape) "fail")
|
||||
(t/is (not= (.-constraintsHorizontal shape) "fail"))
|
||||
(t/is (not= (get-in @store (get-shape-path :constraints-h)) "fail"))
|
||||
(t/testing " - constraintsHorizontal"
|
||||
(set! (.-constraintsHorizontal shape) "fail")
|
||||
(t/is (not= (.-constraintsHorizontal shape) "fail"))
|
||||
(t/is (not= (get-in @store (get-shape-path :constraints-h)) "fail"))
|
||||
|
||||
(set! (.-constraintsHorizontal shape) "right")
|
||||
(t/is (= (.-constraintsHorizontal shape) "right"))
|
||||
(t/is (= (get-in @store (get-shape-path :constraints-h)) :right)))
|
||||
(set! (.-constraintsHorizontal shape) "right")
|
||||
(t/is (= (.-constraintsHorizontal shape) "right"))
|
||||
(t/is (= (get-in @store (get-shape-path :constraints-h)) :right)))
|
||||
|
||||
(t/testing " - constraintsVertical"
|
||||
(set! (.-constraintsVertical shape) "fail")
|
||||
(t/is (not= (.-constraintsVertical shape) "fail"))
|
||||
(t/is (not= (get-in @store (get-shape-path :constraints-v)) "fail"))
|
||||
(t/testing " - constraintsVertical"
|
||||
(set! (.-constraintsVertical shape) "fail")
|
||||
(t/is (not= (.-constraintsVertical shape) "fail"))
|
||||
(t/is (not= (get-in @store (get-shape-path :constraints-v)) "fail"))
|
||||
|
||||
(set! (.-constraintsVertical shape) "bottom")
|
||||
(t/is (= (.-constraintsVertical shape) "bottom"))
|
||||
(t/is (= (get-in @store (get-shape-path :constraints-v)) :bottom)))
|
||||
(set! (.-constraintsVertical shape) "bottom")
|
||||
(t/is (= (.-constraintsVertical shape) "bottom"))
|
||||
(t/is (= (get-in @store (get-shape-path :constraints-v)) :bottom)))
|
||||
|
||||
(t/testing " - borderRadius"
|
||||
(set! (.-borderRadius shape) 10)
|
||||
(t/is (= (.-borderRadius shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :r1)) 10))
|
||||
(t/testing " - borderRadius"
|
||||
(set! (.-borderRadius shape) 10)
|
||||
(t/is (= (.-borderRadius shape) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :r1)) 10))
|
||||
|
||||
(set! (.-borderRadiusTopLeft shape) 20)
|
||||
(t/is (= (.-borderRadiusTopLeft shape) 20))
|
||||
(t/is (= (get-in @store (get-shape-path :r1)) 20))
|
||||
(t/is (= (get-in @store (get-shape-path :r2)) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :r3)) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :r4)) 10))
|
||||
(set! (.-borderRadiusTopLeft shape) 20)
|
||||
(t/is (= (.-borderRadiusTopLeft shape) 20))
|
||||
(t/is (= (get-in @store (get-shape-path :r1)) 20))
|
||||
(t/is (= (get-in @store (get-shape-path :r2)) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :r3)) 10))
|
||||
(t/is (= (get-in @store (get-shape-path :r4)) 10))
|
||||
|
||||
(set! (.-borderRadiusTopRight shape) 30)
|
||||
(set! (.-borderRadiusBottomRight shape) 40)
|
||||
(set! (.-borderRadiusBottomLeft shape) 50)
|
||||
(t/is (= (.-borderRadiusTopRight shape) 30))
|
||||
(t/is (= (.-borderRadiusBottomRight shape) 40))
|
||||
(t/is (= (.-borderRadiusBottomLeft shape) 50))
|
||||
(set! (.-borderRadiusTopRight shape) 30)
|
||||
(set! (.-borderRadiusBottomRight shape) 40)
|
||||
(set! (.-borderRadiusBottomLeft shape) 50)
|
||||
(t/is (= (.-borderRadiusTopRight shape) 30))
|
||||
(t/is (= (.-borderRadiusBottomRight shape) 40))
|
||||
(t/is (= (.-borderRadiusBottomLeft shape) 50))
|
||||
|
||||
(t/is (= (get-in @store (get-shape-path :r1)) 20))
|
||||
(t/is (= (get-in @store (get-shape-path :r2)) 30))
|
||||
(t/is (= (get-in @store (get-shape-path :r3)) 40))
|
||||
(t/is (= (get-in @store (get-shape-path :r4)) 50)))
|
||||
(t/is (= (get-in @store (get-shape-path :r1)) 20))
|
||||
(t/is (= (get-in @store (get-shape-path :r2)) 30))
|
||||
(t/is (= (get-in @store (get-shape-path :r3)) 40))
|
||||
(t/is (= (get-in @store (get-shape-path :r4)) 50)))
|
||||
|
||||
(t/testing " - opacity"
|
||||
(set! (.-opacity shape) 0.5)
|
||||
(t/is (= (.-opacity shape) 0.5))
|
||||
(t/is (= (get-in @store (get-shape-path :opacity)) 0.5)))
|
||||
(t/testing " - opacity"
|
||||
(set! (.-opacity shape) 0.5)
|
||||
(t/is (= (.-opacity shape) 0.5))
|
||||
(t/is (= (get-in @store (get-shape-path :opacity)) 0.5)))
|
||||
|
||||
(t/testing " - blendMode"
|
||||
(set! (.-blendMode shape) "multiply")
|
||||
(t/is (= (.-blendMode shape) "multiply"))
|
||||
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply))
|
||||
(t/testing " - blendMode"
|
||||
(set! (.-blendMode shape) "multiply")
|
||||
(t/is (= (.-blendMode shape) "multiply"))
|
||||
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply))
|
||||
|
||||
(set! (.-blendMode shape) "fail")
|
||||
(t/is (= (.-blendMode shape) "multiply"))
|
||||
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply)))
|
||||
(set! (.-blendMode shape) "fail")
|
||||
(t/is (= (.-blendMode shape) "multiply"))
|
||||
(t/is (= (get-in @store (get-shape-path :blend-mode)) :multiply)))
|
||||
|
||||
(t/testing " - shadows"
|
||||
(let [shadow #js {:style "drop-shadow"
|
||||
:color #js {:color "#FABADA" :opacity 1}}]
|
||||
(set! (.-shadows shape) #js [shadow])
|
||||
(let [shadow-id (uuid/uuid (aget (aget (aget shape "shadows") 0) "id"))]
|
||||
(t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow"))
|
||||
(t/is (= (get-in @store (get-shape-path :shadow)) [{:id shadow-id
|
||||
:style :drop-shadow
|
||||
:offset-x 4
|
||||
:offset-y 4
|
||||
:blur 4
|
||||
:spread 0
|
||||
:color {:color "#fabada" :opacity 1}
|
||||
:hidden false}]))))
|
||||
(let [shadow #js {:style "fail"}]
|
||||
(set! (.-shadows shape) #js [shadow])
|
||||
(t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow"))))
|
||||
(t/testing " - shadows"
|
||||
(let [shadow #js {:style "drop-shadow"
|
||||
:color #js {:color "#FABADA" :opacity 1}}]
|
||||
(set! (.-shadows shape) #js [shadow])
|
||||
(let [shadow-id (uuid/uuid (aget (aget (aget shape "shadows") 0) "id"))]
|
||||
(t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow"))
|
||||
(t/is (= (get-in @store (get-shape-path :shadow)) [{:id shadow-id
|
||||
:style :drop-shadow
|
||||
:offset-x 4
|
||||
:offset-y 4
|
||||
:blur 4
|
||||
:spread 0
|
||||
:color {:color "#fabada" :opacity 1}
|
||||
:hidden false}]))))
|
||||
(let [shadow #js {:style "fail"}]
|
||||
(set! (.-shadows shape) #js [shadow])
|
||||
(t/is (= (-> (. shape -shadows) (aget 0) (aget "style")) "drop-shadow"))))
|
||||
|
||||
(t/testing " - blur"
|
||||
(set! (.-blur shape) #js {:value 10})
|
||||
(t/is (= (-> (. shape -blur) (aget "type")) "layer-blur"))
|
||||
(t/is (= (-> (. shape -blur) (aget "value")) 10))
|
||||
(t/is (= (-> (. shape -blur) (aget "hidden")) false))
|
||||
(let [id (-> (. shape -blur) (aget "id") uuid/uuid)]
|
||||
(t/is (= (get-in @store (get-shape-path :blur)) {:id id :type :layer-blur :value 10 :hidden false}))))
|
||||
(t/testing " - blur"
|
||||
(set! (.-blur shape) #js {:value 10})
|
||||
(t/is (= (-> (. shape -blur) (aget "type")) "layer-blur"))
|
||||
(t/is (= (-> (. shape -blur) (aget "value")) 10))
|
||||
(t/is (= (-> (. shape -blur) (aget "hidden")) false))
|
||||
(let [id (-> (. shape -blur) (aget "id") uuid/uuid)]
|
||||
(t/is (= (get-in @store (get-shape-path :blur)) {:id id :type :layer-blur :value 10 :hidden false}))))
|
||||
|
||||
(t/testing " - exports"
|
||||
(set! (.-exports shape) #js [#js {:type "pdf" :scale 2 :suffix "test"}])
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "type")) "pdf"))
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "scale")) 2))
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test"))
|
||||
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}]))
|
||||
(t/testing " - exports"
|
||||
(set! (.-exports shape) #js [#js {:type "pdf" :scale 2 :suffix "test"}])
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "type")) "pdf"))
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "scale")) 2))
|
||||
(t/is (= (-> (. shape -exports) (aget 0) (aget "suffix")) "test"))
|
||||
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}]))
|
||||
|
||||
(set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])
|
||||
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}])))
|
||||
(set! (.-exports shape) #js [#js {:type 10 :scale 2 :suffix "test"}])
|
||||
(t/is (= (get-in @store (get-shape-path :exports)) [{:type :pdf :scale 2 :suffix "test" :skip-children false}])))
|
||||
|
||||
(t/testing " - flipX"
|
||||
(set! (.-flipX shape) true)
|
||||
(t/is (= (.-flipX shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :flip-x)) true)))
|
||||
(t/testing " - flipX"
|
||||
(set! (.-flipX shape) true)
|
||||
(t/is (= (.-flipX shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :flip-x)) true)))
|
||||
|
||||
(t/testing " - flipY"
|
||||
(set! (.-flipY shape) true)
|
||||
(t/is (= (.-flipY shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :flip-y)) true)))
|
||||
(t/testing " - flipY"
|
||||
(set! (.-flipY shape) true)
|
||||
(t/is (= (.-flipY shape) true))
|
||||
(t/is (= (get-in @store (get-shape-path :flip-y)) true)))
|
||||
|
||||
(t/testing " - rotation"
|
||||
(set! (.-rotation shape) 45)
|
||||
(t/is (= (.-rotation shape) 45))
|
||||
(t/is (= (get-in @store (get-shape-path :rotation)) 45))
|
||||
(t/testing " - rotation"
|
||||
(set! (.-rotation shape) 45)
|
||||
(t/is (= (.-rotation shape) 45))
|
||||
(t/is (= (get-in @store (get-shape-path :rotation)) 45))
|
||||
|
||||
(set! (.-rotation shape) 0)
|
||||
(t/is (= (.-rotation shape) 0))
|
||||
(t/is (= (get-in @store (get-shape-path :rotation)) 0)))
|
||||
(set! (.-rotation shape) 0)
|
||||
(t/is (= (.-rotation shape) 0))
|
||||
(t/is (= (get-in @store (get-shape-path :rotation)) 0)))
|
||||
|
||||
(t/testing " - fills"
|
||||
(set! (.-fills shape) #js [#js {:fillColor 100}])
|
||||
(t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#B1B2B5" :fill-opacity 1}]))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5"))
|
||||
(t/testing " - fills"
|
||||
(set! (.-fills shape) #js [#js {:fillColor 100}])
|
||||
(t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#B1B2B5" :fill-opacity 1}]))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#B1B2B5"))
|
||||
|
||||
(set! (.-fills shape) #js [#js {:fillColor "#fabada" :fillOpacity 1}])
|
||||
(t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#fabada" :fill-opacity 1}]))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#fabada"))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillOpacity")) 1)))
|
||||
(set! (.-fills shape) #js [#js {:fillColor "#fabada" :fillOpacity 1}])
|
||||
(t/is (= (get-in @store (get-shape-path :fills)) [{:fill-color "#fabada" :fill-opacity 1}]))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillColor")) "#fabada"))
|
||||
(t/is (= (-> (. shape -fills) (aget 0) (aget "fillOpacity")) 1)))
|
||||
|
||||
(t/testing " - strokes"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
|
||||
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5}]))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeColor")) "#fabada"))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeOpacity")) 1))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidth")) 5))))
|
||||
(t/testing " - strokes"
|
||||
(set! (.-strokes shape) #js [#js {:strokeColor "#fabada" :strokeOpacity 1 :strokeWidth 5}])
|
||||
(t/is (= (get-in @store (get-shape-path :strokes)) [{:stroke-color "#fabada" :stroke-opacity 1 :stroke-width 5}]))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeColor")) "#fabada"))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeOpacity")) 1))
|
||||
(t/is (= (-> (. ^js shape -strokes) (aget 0) (aget "strokeWidth")) 5))))
|
||||
|
||||
(t/testing "Relative properties"
|
||||
(let [board (.createBoard context)]
|
||||
(set! (.-x board) 100)
|
||||
(set! (.-y board) 200)
|
||||
(t/is (= (.-x board) 100))
|
||||
(t/is (= (.-y board) 200))
|
||||
(.appendChild board shape)
|
||||
(t/testing "Relative properties"
|
||||
(let [board (.createBoard context)]
|
||||
(set! (.-x board) 100)
|
||||
(set! (.-y board) 200)
|
||||
(t/is (= (.-x board) 100))
|
||||
(t/is (= (.-y board) 200))
|
||||
(.appendChild board shape)
|
||||
|
||||
(t/testing " - boardX"
|
||||
(set! (.-boardX ^js shape) 10)
|
||||
(t/is (m/close? (.-boardX ^js shape) 10))
|
||||
(t/is (m/close? (.-x shape) 110))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :x)) 110)))
|
||||
(t/testing " - boardX"
|
||||
(set! (.-boardX ^js shape) 10)
|
||||
(t/is (m/close? (.-boardX ^js shape) 10))
|
||||
(t/is (m/close? (.-x shape) 110))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :x)) 110)))
|
||||
|
||||
(t/testing " - boardY"
|
||||
(set! (.-boardY ^js shape) 20)
|
||||
(t/is (m/close? (.-boardY ^js shape) 20))
|
||||
(t/is (m/close? (.-y shape) 220))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :y)) 220)))
|
||||
(t/testing " - boardY"
|
||||
(set! (.-boardY ^js shape) 20)
|
||||
(t/is (m/close? (.-boardY ^js shape) 20))
|
||||
(t/is (m/close? (.-y shape) 220))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :y)) 220)))
|
||||
|
||||
(t/testing " - parentX"
|
||||
(set! (.-parentX ^js shape) 30)
|
||||
(t/is (m/close? (.-parentX ^js shape) 30))
|
||||
(t/is (m/close? (.-x shape) 130))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :x)) 130)))
|
||||
(t/testing " - parentX"
|
||||
(set! (.-parentX ^js shape) 30)
|
||||
(t/is (m/close? (.-parentX ^js shape) 30))
|
||||
(t/is (m/close? (.-x shape) 130))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :x)) 130)))
|
||||
|
||||
(t/testing " - parentY"
|
||||
(set! (.-parentY ^js shape) 40)
|
||||
(t/is (m/close? (.-parentY ^js shape) 40))
|
||||
(t/is (m/close? (.-y shape) 240))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :y)) 240)))))
|
||||
(t/testing " - parentY"
|
||||
(set! (.-parentY ^js shape) 40)
|
||||
(t/is (m/close? (.-parentY ^js shape) 40))
|
||||
(t/is (m/close? (.-y shape) 240))
|
||||
(t/is (m/close? (get-in @store (get-shape-path :y)) 240)))))
|
||||
|
||||
(t/testing "Clone")
|
||||
(t/testing "Remove")))
|
||||
(t/testing "Clone")
|
||||
(t/testing "Remove")
|
||||
|
||||
(t/testing "WASM mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :clean-modifiers)))
|
||||
(t/is (pos? (thw/call-count :set-structure-modifiers)))
|
||||
(t/is (pos? (thw/call-count :propagate-modifiers))))))))
|
||||
|
||||
@@ -30,10 +30,6 @@
|
||||
(.exit js/process 0)
|
||||
(.exit js/process 1)))
|
||||
|
||||
;; FIXME: workaround, wasn is temporarily disabled for unit tests
|
||||
(set! app.main.features/global-enabled-features
|
||||
(disj app.main.features/global-enabled-features "render-wasm/v1"))
|
||||
|
||||
(defn init
|
||||
[]
|
||||
(t/run-tests
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
[cuerdas.core :as str]
|
||||
[frontend-tests.helpers.pages :as thp]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]
|
||||
[frontend-tests.tokens.helpers.state :as tohs]
|
||||
[frontend-tests.tokens.helpers.tokens :as toht]))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before thp/reset-idmap!})
|
||||
{:before (fn []
|
||||
(thp/reset-idmap!)
|
||||
(thw/setup-wasm-mocks!))
|
||||
:after thw/teardown-wasm-mocks!})
|
||||
|
||||
(defn setup-file []
|
||||
(cthf/sample-file :file-1 :page-label :page-1))
|
||||
@@ -273,7 +277,9 @@
|
||||
(t/is (= (:height (:applied-tokens rect-1')) (:name token-target'))))
|
||||
(t/testing "shapes width and height got updated"
|
||||
(t/is (= (:width rect-1') 100))
|
||||
(t/is (= (:height rect-1') 100))))))))))
|
||||
(t/is (= (:height rect-1') 100)))
|
||||
(t/testing "WASM mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :propagate-modifiers)))))))))))
|
||||
|
||||
(t/deftest test-apply-padding
|
||||
(t/testing "applies padding token to shapes with layout"
|
||||
@@ -346,7 +352,9 @@
|
||||
(t/is (= (:height (:applied-tokens rect-1')) (:name token-target'))))
|
||||
(t/testing "shapes width and height got updated"
|
||||
(t/is (= (:width rect-1') 100))
|
||||
(t/is (= (:height rect-1') 100))))))))))
|
||||
(t/is (= (:height rect-1') 100)))
|
||||
(t/testing "WASM mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :propagate-modifiers)))))))))))
|
||||
|
||||
(t/deftest test-apply-opacity
|
||||
(t/testing "applies opacity token and updates the shapes opacity"
|
||||
@@ -431,7 +439,9 @@
|
||||
rect-1' (cths/get-shape file' :rect-1)]
|
||||
(t/is (some? (:applied-tokens rect-1')))
|
||||
(t/is (= (:rotation (:applied-tokens rect-1')) (:name token-target')))
|
||||
(t/is (= (:rotation rect-1') 120)))))))))
|
||||
(t/is (= (:rotation rect-1') 120))
|
||||
(t/testing "WASM mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :propagate-modifiers)))))))))))
|
||||
|
||||
(t/deftest test-apply-stroke-width
|
||||
(t/testing "applies stroke-width token and updates the shapes with stroke"
|
||||
@@ -540,7 +550,10 @@
|
||||
(:styles))]
|
||||
(t/is (some? (:applied-tokens text-1')))
|
||||
(t/is (= (:font-size (:applied-tokens text-1')) (:name token-target')))
|
||||
(t/is (= (:font-size style-text-blocks) "24")))))))))
|
||||
(t/is (= (:font-size style-text-blocks) "24"))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-apply-line-height
|
||||
(t/testing "applies line-height token and updates the text line-height"
|
||||
@@ -575,7 +588,10 @@
|
||||
(:styles))]
|
||||
(t/is (some? (:applied-tokens text-1')))
|
||||
(t/is (= (:line-height (:applied-tokens text-1')) (:name token-target')))
|
||||
(t/is (= (:line-height style-text-blocks) 1.5)))))))))
|
||||
(t/is (= (:line-height style-text-blocks) 1.5))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-apply-letter-spacing
|
||||
(t/testing "applies letter-spacing token and updates the text letter-spacing"
|
||||
@@ -610,7 +626,10 @@
|
||||
(:styles))]
|
||||
(t/is (some? (:applied-tokens text-1')))
|
||||
(t/is (= (:letter-spacing (:applied-tokens text-1')) (:name token-target')))
|
||||
(t/is (= (:letter-spacing style-text-blocks) "2")))))))))
|
||||
(t/is (= (:letter-spacing style-text-blocks) "2"))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-apply-font-family
|
||||
(t/testing "applies font-family token and updates the text font-family"
|
||||
@@ -645,7 +664,10 @@
|
||||
(:styles))]
|
||||
(t/is (some? (:applied-tokens text-1')))
|
||||
(t/is (= (:font-family (:applied-tokens text-1')) (:name token-target')))
|
||||
(t/is (= (:font-family style-text-blocks) (:font-id txt/default-text-attrs))))))))))
|
||||
(t/is (= (:font-family style-text-blocks) (:font-id txt/default-text-attrs)))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-apply-text-case
|
||||
(t/testing "applies text-case token and updates the text transform"
|
||||
@@ -750,7 +772,10 @@
|
||||
(:styles))]
|
||||
(t/is (some? (:applied-tokens text-1')))
|
||||
(t/is (= (:font-weight (:applied-tokens text-1')) (:name token-target')))
|
||||
(t/is (= (:font-weight style-text-blocks) "400")))))))))
|
||||
(t/is (= (:font-weight style-text-blocks) "400"))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-toggle-token-none
|
||||
(t/testing "should apply token to all selected items, where no item has the token applied"
|
||||
@@ -973,7 +998,10 @@
|
||||
(t/is (= (:font-family style-text-blocks) "sourcesanspro"))
|
||||
(t/is (= (:letter-spacing style-text-blocks) "2"))
|
||||
(t/is (= (:text-transform style-text-blocks) "uppercase"))
|
||||
(t/is (= (:text-decoration style-text-blocks) "underline")))))))))
|
||||
(t/is (= (:text-decoration style-text-blocks) "underline"))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-apply-reference-typography-token
|
||||
(t/testing "applies typography (composite) tokens with references"
|
||||
@@ -1018,7 +1046,10 @@
|
||||
(t/is (= (:typography (:applied-tokens text-1')) "typography"))
|
||||
|
||||
(t/is (= (:font-size style-text-blocks) "100"))
|
||||
(t/is (= (:font-family style-text-blocks) "Arial")))))))))
|
||||
(t/is (= (:font-family style-text-blocks) "Arial"))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
(t/deftest test-unapply-atomic-tokens-on-composite-apply
|
||||
(t/testing "unapplies atomic typography tokens when applying composite token"
|
||||
@@ -1172,4 +1203,7 @@
|
||||
(t/is (nil? (:typography-ref-id paragraph-3)))
|
||||
(t/is (nil? (:typography-ref-file paragraph-3)))
|
||||
(t/is (nil? (:typography-ref-id text-node-3)))
|
||||
(t/is (nil? (:typography-ref-file text-node-3))))))))))
|
||||
(t/is (nil? (:typography-ref-file text-node-3)))
|
||||
(t/testing "WASM text mocks were exercised"
|
||||
(t/is (pos? (thw/call-count :set-shape-text-content)))
|
||||
(t/is (pos? (thw/call-count :get-text-dimensions)))))))))))
|
||||
|
||||
@@ -3,12 +3,83 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Penpot plugin example</title>
|
||||
<title>Penpot MCP Plugin</title>
|
||||
</head>
|
||||
<body>
|
||||
<button type="button" data-appearance="secondary" data-handler="connect-mcp">Connect to MCP server</button>
|
||||
<div class="plugin-container">
|
||||
<div id="connection-status" class="status-pill" data-status="idle">
|
||||
<span class="status-dot"></span>
|
||||
<span id="status-text" class="body-s">Not connected</span>
|
||||
</div>
|
||||
|
||||
<div id="connection-status" style="margin-top: 10px; font-size: 12px; color: #666">Not connected</div>
|
||||
<button type="button" id="connect-btn" data-appearance="primary" data-handler="connect-mcp">
|
||||
Connect MCP Server
|
||||
</button>
|
||||
<button type="button" id="disconnect-btn" data-appearance="secondary" data-handler="disconnect-mcp" hidden>
|
||||
Disconnect MCP Server
|
||||
</button>
|
||||
|
||||
<details class="collapsible-section" id="execution-status">
|
||||
<summary class="collapsible-header">
|
||||
<svg
|
||||
class="collapsible-arrow"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="12"
|
||||
height="12"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
|
||||
</svg>
|
||||
<span class="body-s">Execution status</span>
|
||||
</summary>
|
||||
|
||||
<div class="collapsible-body">
|
||||
<span class="body-s tool-label">Current task</span>
|
||||
<div class="tool-display">
|
||||
<svg
|
||||
class="tool-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"
|
||||
/>
|
||||
</svg>
|
||||
<span id="current-task" class="body-s">---</span>
|
||||
</div>
|
||||
|
||||
<div class="code-section-header">
|
||||
<span class="body-s tool-label">Executed code</span>
|
||||
<button type="button" id="copy-code-btn" class="copy-btn" title="Copy code" disabled>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="executed-code"
|
||||
class="code-textarea"
|
||||
readonly
|
||||
placeholder="No code executed yet..."
|
||||
></textarea>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
BIN
mcp/packages/plugin/public/icon.jpg
Normal file
BIN
mcp/packages/plugin/public/icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "Penpot MCP Plugin",
|
||||
"code": "plugin.js",
|
||||
"icon": "icon.jpg",
|
||||
"version": 2,
|
||||
"description": "This plugin enables interaction with the Penpot MCP server",
|
||||
"permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"]
|
||||
|
||||
@@ -6,21 +6,33 @@ document.body.dataset.theme = searchParams.get("theme") ?? "light";
|
||||
|
||||
// WebSocket connection management
|
||||
let ws: WebSocket | null = null;
|
||||
const statusElement = document.getElementById("connection-status");
|
||||
|
||||
const statusPill = document.getElementById("connection-status") as HTMLElement;
|
||||
const statusText = document.getElementById("status-text") as HTMLElement;
|
||||
const currentTaskEl = document.getElementById("current-task") as HTMLElement;
|
||||
const executedCodeEl = document.getElementById("executed-code") as HTMLTextAreaElement;
|
||||
const copyCodeBtn = document.getElementById("copy-code-btn") as HTMLButtonElement;
|
||||
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;
|
||||
const disconnectBtn = document.getElementById("disconnect-btn") as HTMLButtonElement;
|
||||
|
||||
/**
|
||||
* Updates the connection status display element.
|
||||
* Updates the status pill and button visibility based on connection state.
|
||||
*
|
||||
* @param status - the base status text to display
|
||||
* @param isConnectedState - whether the connection is in a connected state (affects color)
|
||||
* @param message - optional additional message to append to the status
|
||||
* @param code - the connection state code ("idle" | "connecting" | "connected" | "disconnected" | "error")
|
||||
* @param label - human-readable label to display inside the pill
|
||||
*/
|
||||
function updateConnectionStatus(code: string, status: string, isConnectedState: boolean, message?: string): void {
|
||||
if (statusElement) {
|
||||
const displayText = message ? `${status}: ${message}` : status;
|
||||
statusElement.textContent = displayText;
|
||||
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
|
||||
function updateConnectionStatus(code: string, label: string): void {
|
||||
if (statusPill) {
|
||||
statusPill.dataset.status = code;
|
||||
}
|
||||
if (statusText) {
|
||||
statusText.textContent = label;
|
||||
}
|
||||
|
||||
const isConnected = code === "connected";
|
||||
if (connectBtn) connectBtn.hidden = isConnected;
|
||||
if (disconnectBtn) disconnectBtn.hidden = !isConnected;
|
||||
|
||||
parent.postMessage(
|
||||
{
|
||||
type: "update-connection-status",
|
||||
@@ -30,6 +42,34 @@ function updateConnectionStatus(code: string, status: string, isConnectedState:
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the "Current task" display with the currently executing task name.
|
||||
*
|
||||
* @param taskName - the task name to display, or null to reset to "---"
|
||||
*/
|
||||
function updateCurrentTask(taskName: string | null): void {
|
||||
if (currentTaskEl) {
|
||||
currentTaskEl.textContent = taskName ?? "---";
|
||||
}
|
||||
if (taskName === null) {
|
||||
updateExecutedCode(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the executed code textarea with the last code run during task execution.
|
||||
*
|
||||
* @param code - the code string to display, or null to clear
|
||||
*/
|
||||
function updateExecutedCode(code: string | null): void {
|
||||
if (executedCodeEl) {
|
||||
executedCodeEl.value = code ?? "";
|
||||
}
|
||||
if (copyCodeBtn) {
|
||||
copyCodeBtn.disabled = !code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a task response back to the MCP server via WebSocket.
|
||||
*
|
||||
@@ -49,7 +89,7 @@ function sendTaskResponse(response: any): void {
|
||||
*/
|
||||
function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
updateConnectionStatus("connected", "Already connected", true);
|
||||
updateConnectionStatus("connected", "Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,17 +102,22 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
updateConnectionStatus("connecting", "Connecting...", false);
|
||||
updateConnectionStatus("connecting", "Connecting...");
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("Connected to MCP server");
|
||||
updateConnectionStatus("connected", "Connected to MCP server", true);
|
||||
updateConnectionStatus("connected", "Connected");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
console.log("Received from MCP server:", event.data);
|
||||
const request = JSON.parse(event.data);
|
||||
// Track the current task received from the MCP server
|
||||
if (request.task) {
|
||||
updateCurrentTask(request.task);
|
||||
updateExecutedCode(request.params?.code ?? null);
|
||||
}
|
||||
// Forward the task request to the plugin for execution
|
||||
parent.postMessage(request, "*");
|
||||
} catch (error) {
|
||||
@@ -84,8 +129,9 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||
// If we've send the error update we don't send the disconnect as well
|
||||
if (!wsError) {
|
||||
console.log("Disconnected from MCP server");
|
||||
const message = event.reason || undefined;
|
||||
updateConnectionStatus("disconnected", "Disconnected", false, message);
|
||||
const label = event.reason ? `Disconnected: ${event.reason}` : "Disconnected";
|
||||
updateConnectionStatus("disconnected", label);
|
||||
updateCurrentTask(null);
|
||||
}
|
||||
ws = null;
|
||||
};
|
||||
@@ -94,19 +140,34 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
|
||||
console.error("WebSocket error:", error);
|
||||
wsError = error;
|
||||
// note: WebSocket error events typically don't contain detailed error messages
|
||||
updateConnectionStatus("error", "Connection error", false);
|
||||
updateConnectionStatus("error", "Connection error");
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MCP server:", error);
|
||||
const message = error instanceof Error ? error.message : undefined;
|
||||
updateConnectionStatus("error", "Connection failed", false, message);
|
||||
const reason = error instanceof Error ? error.message : undefined;
|
||||
const label = reason ? `Connection failed: ${reason}` : "Connection failed";
|
||||
updateConnectionStatus("error", label);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => {
|
||||
copyCodeBtn?.addEventListener("click", () => {
|
||||
const code = executedCodeEl?.value;
|
||||
if (!code) return;
|
||||
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
copyCodeBtn.classList.add("copied");
|
||||
setTimeout(() => copyCodeBtn.classList.remove("copied"), 1500);
|
||||
});
|
||||
});
|
||||
|
||||
connectBtn?.addEventListener("click", () => {
|
||||
connectToMcpServer();
|
||||
});
|
||||
|
||||
disconnectBtn?.addEventListener("click", () => {
|
||||
ws?.close();
|
||||
});
|
||||
|
||||
// Listen plugin.ts messages
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.data.type === "start-server") {
|
||||
|
||||
@@ -10,8 +10,8 @@ const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()];
|
||||
|
||||
// Open the plugin UI (main.ts)
|
||||
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, {
|
||||
width: 158,
|
||||
height: 200,
|
||||
width: 236,
|
||||
height: 210,
|
||||
hidden: !!mcp,
|
||||
} as any);
|
||||
|
||||
|
||||
@@ -1,10 +1,178 @@
|
||||
@import "@penpot/plugin-styles/styles.css";
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-end: 0.75rem;
|
||||
.plugin-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-16) var(--spacing-8);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Status pill ─────────────────────────────────────────────────── */
|
||||
|
||||
.status-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
border-radius: var(--spacing-8);
|
||||
border: 1px solid var(--background-quaternary);
|
||||
color: var(--foreground-secondary);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-pill[data-status="connected"] {
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.status-pill[data-status="disconnected"],
|
||||
.status-pill[data-status="error"] {
|
||||
border-color: var(--error-500);
|
||||
color: var(--error-500);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Collapsible section ─────────────────────────────────────────── */
|
||||
|
||||
.collapsible-section {
|
||||
border: 1px solid var(--background-quaternary);
|
||||
border-radius: var(--spacing-8);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
cursor: pointer;
|
||||
color: var(--foreground-secondary);
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible-header::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible-arrow {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
details[open] > .collapsible-header .collapsible-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.collapsible-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-4) var(--spacing-12) var(--spacing-12);
|
||||
border-top: 1px solid var(--background-quaternary);
|
||||
}
|
||||
|
||||
/* ── Tool section ────────────────────────────────────────────────── */
|
||||
|
||||
.tool-label {
|
||||
color: var(--foreground-secondary);
|
||||
}
|
||||
|
||||
.tool-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-radius: var(--spacing-8);
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--foreground-secondary);
|
||||
min-height: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ── Code section ────────────────────────────────────────────────── */
|
||||
|
||||
.code-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
.code-textarea {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
resize: vertical;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-radius: var(--spacing-8);
|
||||
border: 1px solid var(--background-quaternary);
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--foreground-secondary);
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--background-quaternary);
|
||||
border-radius: var(--spacing-4);
|
||||
background-color: transparent;
|
||||
color: var(--foreground-secondary);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.copy-btn:hover:not(:disabled) {
|
||||
background-color: var(--background-tertiary);
|
||||
color: var(--foreground-primary);
|
||||
}
|
||||
|
||||
.copy-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.copy-btn.copied {
|
||||
color: var(--accent-primary);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* ── Action buttons ──────────────────────────────────────────────── */
|
||||
|
||||
#connect-btn,
|
||||
#disconnect-btn {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -435,10 +435,10 @@ impl RenderState {
|
||||
shape.frame_clip_layer_blur()
|
||||
}
|
||||
|
||||
/// Renders background blur effect directly to the Current surface.
|
||||
/// Renders background blur effect directly to the given target surface.
|
||||
/// Must be called BEFORE any save_layer for the shape's own opacity/blend,
|
||||
/// so that the backdrop blur is independent of the shape's visual properties.
|
||||
fn render_background_blur(&mut self, shape: &Shape) {
|
||||
fn render_background_blur(&mut self, shape: &Shape, target_surface: SurfaceId) {
|
||||
if self.options.is_fast_mode() {
|
||||
return;
|
||||
}
|
||||
@@ -458,21 +458,22 @@ impl RenderState {
|
||||
let scaled_sigma = radius_to_sigma(blur.value * scale);
|
||||
// Cap sigma so the blur kernel (≈3σ) stays within the tile margin.
|
||||
// This prevents visible seams at tile boundaries when zoomed in.
|
||||
let margin = self.surfaces.margins().width as f32;
|
||||
let max_sigma = margin / 3.0;
|
||||
let capped_sigma = scaled_sigma.min(max_sigma);
|
||||
|
||||
let blur_filter = match skia::image_filters::blur(
|
||||
(capped_sigma, capped_sigma),
|
||||
skia::TileMode::Clamp,
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Some(filter) => filter,
|
||||
None => return,
|
||||
// During export there's no tiling, so skip the cap.
|
||||
let sigma = if self.export_context.is_some() {
|
||||
scaled_sigma
|
||||
} else {
|
||||
let margin = self.surfaces.margins().width as f32;
|
||||
let max_sigma = margin / 3.0;
|
||||
scaled_sigma.min(max_sigma)
|
||||
};
|
||||
|
||||
let snapshot = self.surfaces.snapshot(SurfaceId::Current);
|
||||
let blur_filter =
|
||||
match skia::image_filters::blur((sigma, sigma), skia::TileMode::Clamp, None, None) {
|
||||
Some(filter) => filter,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let target_surface_snapshot = self.surfaces.snapshot(target_surface);
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
@@ -482,10 +483,10 @@ impl RenderState {
|
||||
matrix.post_translate(center);
|
||||
matrix.pre_translate(-center);
|
||||
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Current);
|
||||
let canvas = self.surfaces.canvas(target_surface);
|
||||
canvas.save();
|
||||
|
||||
// Current has no render context transform (identity canvas).
|
||||
// Current/Export have no render context transform (identity canvas).
|
||||
// Apply scale + translate + shape transform so the clip maps
|
||||
// from shape-local coords to device pixels correctly.
|
||||
canvas.scale((scale, scale));
|
||||
@@ -528,7 +529,7 @@ impl RenderState {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_image_filter(blur_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::Src);
|
||||
canvas.draw_image(&snapshot, (0, 0), Some(&paint));
|
||||
canvas.draw_image(&target_surface_snapshot, (0, 0), Some(&paint));
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
@@ -1521,6 +1522,11 @@ impl RenderState {
|
||||
) -> Result<(Vec<u8>, i32, i32)> {
|
||||
let target_surface = SurfaceId::Export;
|
||||
|
||||
// Reset focus mode so all shapes in the export tree are rendered.
|
||||
// Without this, leftover focus_mode state from the workspace could
|
||||
// cause shapes (and their background blur) to be skipped.
|
||||
self.focus_mode.clear();
|
||||
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
@@ -1533,6 +1539,8 @@ impl RenderState {
|
||||
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
|
||||
|
||||
self.surfaces.resize_export_surface(scale, extrect);
|
||||
self.render_area = extrect;
|
||||
self.render_area_with_margins = extrect;
|
||||
self.surfaces.update_render_context(extrect, scale);
|
||||
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
@@ -1546,6 +1554,9 @@ impl RenderState {
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||
}
|
||||
|
||||
// Clear export context so get_scale() returns to workspace zoom.
|
||||
self.export_context = None;
|
||||
|
||||
self.surfaces
|
||||
.flush_and_submit(&mut self.gpu_state, target_surface);
|
||||
|
||||
@@ -2334,13 +2345,7 @@ impl RenderState {
|
||||
// Render background blur BEFORE save_layer so it modifies
|
||||
// the backdrop independently of the shape's opacity.
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
self.render_background_blur(element);
|
||||
}
|
||||
|
||||
// Render background blur BEFORE save_layer so it modifies
|
||||
// the backdrop independently of the shape's opacity.
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
self.render_background_blur(element);
|
||||
self.render_background_blur(element, target_surface);
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask, target_surface);
|
||||
@@ -2910,6 +2915,10 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn get_scale(&self) -> f32 {
|
||||
// During export, use the export scale instead of the workspace zoom.
|
||||
if let Some((_, export_scale)) = self.export_context {
|
||||
return export_scale;
|
||||
}
|
||||
self.viewbox.zoom() * self.options.dpr()
|
||||
}
|
||||
|
||||
|
||||
@@ -168,8 +168,16 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(shape_to_delete) = self.shapes.get_mut(&id) {
|
||||
shape_to_delete.set_deleted(true);
|
||||
if let Some(shape_to_delete) = self.shapes.get(&id) {
|
||||
let to_delete = shape_to_delete.all_children(&self.shapes, true, true);
|
||||
for shape_id in to_delete {
|
||||
if let Some(shape_to_delete) = self.shapes.get_mut(&shape_id) {
|
||||
shape_to_delete.set_deleted(true);
|
||||
}
|
||||
if self.render_state.show_grid == Some(shape_id) {
|
||||
self.render_state.show_grid = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub enum RawBoolType {
|
||||
Union = 0,
|
||||
Difference = 1,
|
||||
Intersection = 2,
|
||||
Exclusion = 3,
|
||||
Exclude = 3,
|
||||
}
|
||||
|
||||
impl From<u8> for RawBoolType {
|
||||
@@ -33,7 +33,7 @@ impl From<RawBoolType> for BoolType {
|
||||
RawBoolType::Union => BoolType::Union,
|
||||
RawBoolType::Difference => BoolType::Difference,
|
||||
RawBoolType::Intersection => BoolType::Intersection,
|
||||
RawBoolType::Exclusion => BoolType::Exclusion,
|
||||
RawBoolType::Exclude => BoolType::Exclusion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user