mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 04:09:38 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf7891479 | ||
|
|
deb0583029 | ||
|
|
a5b19118b9 | ||
|
|
0f66caf542 |
No files matched your search
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.common.files.changes
|
||||
(:require
|
||||
#?(:cljs [app.common.files.validate :as val])
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -428,7 +429,14 @@
|
||||
[:set-base-font-size
|
||||
[:map {:title "ModBaseFontSize"}
|
||||
[:type [:= :set-base-font-size]]
|
||||
[:base-font-size :string]]]])
|
||||
[:base-font-size :string]]]
|
||||
|
||||
[:validate-shapes
|
||||
[:map {:title "ValidateShapesChange"}
|
||||
[:type [:= :validate-shapes]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:shape-ids [:vector ::sm/uuid]]
|
||||
[:context :string]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
@@ -464,7 +472,7 @@
|
||||
to the processor backend."
|
||||
nil)
|
||||
|
||||
(defmulti process-change (fn [_ change] (:type change)))
|
||||
(defmulti process-change (fn [_ change _] (:type change)))
|
||||
(defmulti process-operation (fn [_ op] (:type op)))
|
||||
|
||||
;; Changes Processing Impl
|
||||
@@ -496,22 +504,25 @@
|
||||
|
||||
(defn process-changes
|
||||
([data items]
|
||||
(process-changes data items true))
|
||||
(process-changes data items true {}))
|
||||
|
||||
([data items verify?]
|
||||
(process-changes data items verify? {}))
|
||||
|
||||
([data items verify? libraries]
|
||||
;; When verify? false we spec the schema validation. Currently used
|
||||
;; to make just 1 validation even if the changes are applied twice
|
||||
(when verify?
|
||||
(check-changes items))
|
||||
|
||||
(binding [*touched-changes* (volatile! #{})]
|
||||
(let [result (reduce #(or (process-change %1 %2) %1) data items)]
|
||||
(let [result (reduce #(or (process-change %1 %2 libraries) %1) data items)]
|
||||
(reduce process-touched-change result @*touched-changes*)))))
|
||||
|
||||
;; --- Comment Threads
|
||||
|
||||
(defmethod process-change :set-comment-thread-position
|
||||
[data {:keys [page-id comment-thread-id position frame-id]}]
|
||||
[data {:keys [page-id comment-thread-id position frame-id]} _]
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
(if (and position frame-id)
|
||||
@@ -524,7 +535,7 @@
|
||||
;; --- Guides
|
||||
|
||||
(defmethod process-change :set-guide
|
||||
[data {:keys [page-id id params]}]
|
||||
[data {:keys [page-id id params]} _]
|
||||
(if (nil? params)
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
@@ -540,7 +551,7 @@
|
||||
;; --- Flows
|
||||
|
||||
(defmethod process-change :set-flow
|
||||
[data {:keys [page-id id params]}]
|
||||
[data {:keys [page-id id params]} _]
|
||||
(if (nil? params)
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
@@ -556,7 +567,7 @@
|
||||
;; --- Grids
|
||||
|
||||
(defmethod process-change :set-default-grid
|
||||
[data {:keys [page-id grid-type params]}]
|
||||
[data {:keys [page-id grid-type params]} _]
|
||||
(if (nil? params)
|
||||
(d/update-in-when data [:pages-index page-id]
|
||||
(fn [page]
|
||||
@@ -593,7 +604,7 @@
|
||||
(update state :media-refs into xform media-refs)))
|
||||
|
||||
(defmethod process-change :add-obj
|
||||
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]}]
|
||||
[data {:keys [id obj page-id component-id frame-id parent-id index ignore-touched]} _]
|
||||
;; NOTE: we only perform hard validation on backend
|
||||
#?(:clj (validate-shape obj page-id))
|
||||
|
||||
@@ -628,7 +639,7 @@
|
||||
objects))
|
||||
|
||||
(defmethod process-change :mod-obj
|
||||
[data {:keys [page-id component-id] :as change}]
|
||||
[data {:keys [page-id component-id] :as change} _]
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id :objects] process-operations change)
|
||||
(d/update-in-when data [:components component-id :objects] process-operations change)))
|
||||
@@ -658,19 +669,19 @@
|
||||
objects))
|
||||
|
||||
(defmethod process-change :reorder-children
|
||||
[data {:keys [page-id component-id] :as change}]
|
||||
[data {:keys [page-id component-id] :as change} _]
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id :objects] process-children-reordering change)
|
||||
(d/update-in-when data [:components component-id :objects] process-children-reordering change)))
|
||||
|
||||
(defmethod process-change :del-obj
|
||||
[data {:keys [page-id component-id id ignore-touched]}]
|
||||
[data {:keys [page-id component-id id ignore-touched]} _]
|
||||
(if page-id
|
||||
(d/update-in-when data [:pages-index page-id] ctst/delete-shape id ignore-touched)
|
||||
(d/update-in-when data [:components component-id] ctst/delete-shape id ignore-touched)))
|
||||
|
||||
(defmethod process-change :fix-obj
|
||||
[data {:keys [page-id component-id id] :as params}]
|
||||
[data {:keys [page-id component-id id] :as params} _]
|
||||
(letfn [(fix-container [container]
|
||||
(case (:fix params :broken-children)
|
||||
:broken-children (ctst/fix-broken-children container id)
|
||||
@@ -682,7 +693,7 @@
|
||||
(d/update-in-when data [:components component-id] fix-container))))
|
||||
|
||||
(defmethod process-change :reg-objects
|
||||
[data {:keys [page-id component-id shapes]}]
|
||||
[data {:keys [page-id component-id shapes]} _]
|
||||
;; FIXME: Improve performance
|
||||
(letfn [(reg-objects [objects]
|
||||
(let [lookup (d/getf objects)
|
||||
@@ -734,7 +745,7 @@
|
||||
|
||||
(defmethod process-change :mov-objects
|
||||
;; FIXME: ignore-touched is no longer used, so we can consider it deprecated
|
||||
[data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]}]
|
||||
[data {:keys [parent-id shapes index page-id component-id #_ignore-touched after-shape allow-altering-copies syncing]} _]
|
||||
(letfn [(calculate-invalid-targets [objects shape-id]
|
||||
(let [reduce-fn #(into %1 (calculate-invalid-targets objects %2))]
|
||||
(->> (get-in objects [shape-id :shapes])
|
||||
@@ -849,7 +860,7 @@
|
||||
(d/update-in-when data [:components component-id :objects] move-objects))))
|
||||
|
||||
(defmethod process-change :add-page
|
||||
[data {:keys [id name page]}]
|
||||
[data {:keys [id name page]} _]
|
||||
(when (and id name page)
|
||||
(ex/raise :type :conflict
|
||||
:hint "id+name or page should be provided, never both"))
|
||||
@@ -859,7 +870,7 @@
|
||||
(ctpl/add-page data page)))
|
||||
|
||||
(defmethod process-change :mod-page
|
||||
[data {:keys [id] :as params}]
|
||||
[data {:keys [id] :as params} _]
|
||||
(d/update-in-when data [:pages-index id]
|
||||
(fn [page]
|
||||
(let [name (get params :name)
|
||||
@@ -889,7 +900,7 @@
|
||||
(dissoc :pixel-grid-opacity))))))
|
||||
|
||||
(defmethod process-change :set-plugin-data
|
||||
[data {:keys [object-type object-id page-id namespace key value]}]
|
||||
[data {:keys [object-type object-id page-id namespace key value]} _]
|
||||
(letfn [(update-fn [data]
|
||||
(if (some? value)
|
||||
(assoc-in data [:plugin-data namespace key] value)
|
||||
@@ -915,83 +926,83 @@
|
||||
(d/update-in-when data [:components object-id] update-fn))))
|
||||
|
||||
(defmethod process-change :del-page
|
||||
[data {:keys [id]}]
|
||||
[data {:keys [id]} _]
|
||||
(ctpl/delete-page data id))
|
||||
|
||||
(defmethod process-change :mov-page
|
||||
[data {:keys [id index]}]
|
||||
[data {:keys [id index]} _]
|
||||
(update data :pages d/insert-at-index index [id]))
|
||||
|
||||
(defmethod process-change :add-color
|
||||
[data {:keys [color]}]
|
||||
[data {:keys [color]} _]
|
||||
(ctl/add-color data color))
|
||||
|
||||
(defmethod process-change :mod-color
|
||||
[data {:keys [color]}]
|
||||
[data {:keys [color]} _]
|
||||
(ctl/set-color data color))
|
||||
|
||||
(defmethod process-change :del-color
|
||||
[data {:keys [id]}]
|
||||
[data {:keys [id]} _]
|
||||
(ctl/delete-color data id))
|
||||
|
||||
;; -- Media
|
||||
|
||||
(defmethod process-change :add-media
|
||||
[data {:keys [object]}]
|
||||
[data {:keys [object]} _]
|
||||
(update data :media assoc (:id object) object))
|
||||
|
||||
(defmethod process-change :mod-media
|
||||
[data {:keys [object]}]
|
||||
[data {:keys [object]} _]
|
||||
(d/update-in-when data [:media (:id object)] merge object))
|
||||
|
||||
(defmethod process-change :del-media
|
||||
[data {:keys [id]}]
|
||||
[data {:keys [id]} _]
|
||||
(d/update-when data :media dissoc id))
|
||||
|
||||
;; -- Components
|
||||
|
||||
(defmethod process-change :add-component
|
||||
[data params]
|
||||
[data params _]
|
||||
(ctkl/add-component data params))
|
||||
|
||||
(defmethod process-change :mod-component
|
||||
[data params]
|
||||
[data params _]
|
||||
(ctkl/mod-component data params))
|
||||
|
||||
(defmethod process-change :del-component
|
||||
[data {:keys [id skip-undelete? delta]}]
|
||||
[data {:keys [id skip-undelete? delta]} _]
|
||||
(ctf/delete-component data id skip-undelete? delta))
|
||||
|
||||
(defmethod process-change :restore-component
|
||||
[data {:keys [id page-id]}]
|
||||
[data {:keys [id page-id]} _]
|
||||
(ctf/restore-component data id page-id))
|
||||
|
||||
(defmethod process-change :purge-component
|
||||
[data {:keys [id]}]
|
||||
[data {:keys [id]} _]
|
||||
(ctf/purge-component data id))
|
||||
|
||||
;; -- Typography
|
||||
|
||||
(defmethod process-change :add-typography
|
||||
[data {:keys [typography]}]
|
||||
[data {:keys [typography]} _]
|
||||
(ctyl/add-typography data typography))
|
||||
|
||||
(defmethod process-change :mod-typography
|
||||
[data {:keys [typography]}]
|
||||
[data {:keys [typography]} _]
|
||||
(ctyl/update-typography data (:id typography) merge typography))
|
||||
|
||||
(defmethod process-change :del-typography
|
||||
[data {:keys [id]}]
|
||||
[data {:keys [id]} _]
|
||||
(ctyl/delete-typography data id))
|
||||
|
||||
;; -- Design Tokens
|
||||
|
||||
(defmethod process-change :set-tokens-lib
|
||||
[data {:keys [tokens-lib]}]
|
||||
[data {:keys [tokens-lib]} _]
|
||||
(assoc data :tokens-lib tokens-lib))
|
||||
|
||||
(defmethod process-change :set-token
|
||||
[data {:keys [set-id token-id attrs]}]
|
||||
[data {:keys [set-id token-id attrs]} _]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
@@ -1008,7 +1019,7 @@
|
||||
(ctob/make-token (merge prev-token attrs)))))))))
|
||||
|
||||
(defmethod process-change :set-token-set
|
||||
[data {:keys [id attrs]}]
|
||||
[data {:keys [id attrs]} _]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
@@ -1023,7 +1034,7 @@
|
||||
(ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs))))))))
|
||||
|
||||
(defmethod process-change :set-token-theme
|
||||
[data {:keys [id attrs]}]
|
||||
[data {:keys [id attrs]} _]
|
||||
(update data :tokens-lib
|
||||
(fn [lib]
|
||||
(let [lib' (ctob/ensure-tokens-lib lib)]
|
||||
@@ -1041,35 +1052,67 @@
|
||||
(ctob/make-token-theme (merge prev-token-theme attrs)))))))))
|
||||
|
||||
(defmethod process-change :set-active-token-themes
|
||||
[data {:keys [theme-paths]}]
|
||||
[data {:keys [theme-paths]} _]
|
||||
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
|
||||
(ctob/set-active-themes theme-paths))))
|
||||
|
||||
(defmethod process-change :rename-token-set-group
|
||||
[data {:keys [set-group-path set-group-fname]}]
|
||||
[data {:keys [set-group-path set-group-fname]} _]
|
||||
(update data :tokens-lib (fn [lib]
|
||||
(-> lib
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/rename-set-group set-group-path set-group-fname)))))
|
||||
|
||||
(defmethod process-change :move-token-set
|
||||
[data {:keys [from-path to-path before-path before-group] :as changes}]
|
||||
[data {:keys [from-path to-path before-path before-group] :as changes} _]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set from-path to-path before-path before-group))))
|
||||
|
||||
(defmethod process-change :move-token-set-group
|
||||
[data {:keys [from-path to-path before-path before-group]}]
|
||||
[data {:keys [from-path to-path before-path before-group]} _]
|
||||
(update data :tokens-lib #(-> %
|
||||
(ctob/ensure-tokens-lib)
|
||||
(ctob/move-set-group from-path to-path before-path before-group))))
|
||||
|
||||
;; === Design Tokens configuration
|
||||
;; --- Design Tokens configuration
|
||||
|
||||
(defmethod process-change :set-base-font-size
|
||||
[data {:keys [base-font-size]}]
|
||||
[data {:keys [base-font-size]} _]
|
||||
(ctf/set-base-font-size data base-font-size))
|
||||
|
||||
;; --- Validate Shapes
|
||||
|
||||
#?(:clj
|
||||
(defmethod process-change :validate-shapes
|
||||
[data _ _]
|
||||
data))
|
||||
|
||||
#?(:cljs
|
||||
(defmethod process-change :validate-shapes
|
||||
[data {:keys [page-id shape-ids context]} libraries]
|
||||
(if libraries
|
||||
(println "Validating shapes: \n"
|
||||
" page-id:" (str page-id) "\n"
|
||||
" shape-ids:" (str shape-ids) "\n"
|
||||
" context:" context)
|
||||
(let [file {:data data :id uuid/zero}
|
||||
errors (reduce (fn [acc shape-id]
|
||||
(if-let [page (ctpl/get-page data page-id)]
|
||||
(let [page-errors (val/validate-shape shape-id file page libraries)]
|
||||
(if (seq page-errors)
|
||||
(into acc page-errors)
|
||||
acc))
|
||||
acc))
|
||||
[]
|
||||
shape-ids)]
|
||||
(when (seq errors)
|
||||
(ex/raise :type :validation
|
||||
:code :referential-integrity
|
||||
:hint (str "error on validating shapes: " context)
|
||||
:details errors))
|
||||
data))
|
||||
data))
|
||||
|
||||
;; === Operations
|
||||
|
||||
|
||||
@@ -1203,7 +1203,6 @@
|
||||
[changes]
|
||||
(::page-id (meta changes)))
|
||||
|
||||
|
||||
(defn set-text-content
|
||||
[changes id content prev-content]
|
||||
(assert-page-id! changes)
|
||||
@@ -1224,3 +1223,12 @@
|
||||
(-> changes
|
||||
(update :redo-changes conj redo-change)
|
||||
(update :undo-changes conj undo-change))))
|
||||
|
||||
;; Validate Shapes
|
||||
|
||||
(defn validate-shapes
|
||||
[changes page-id shape-ids context]
|
||||
(update changes :redo-changes conj {:type :validate-shapes
|
||||
:page-id page-id
|
||||
:shape-ids (vec shape-ids)
|
||||
:context context}))
|
||||
@@ -10,7 +10,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.variant :as cfv]
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.component :as ctk]
|
||||
@@ -569,7 +568,17 @@
|
||||
objects (:objects page)
|
||||
file-data (:data file)
|
||||
first-child (get objects (first shapes))
|
||||
prop-names (cfv/extract-properties-names first-child file-data)]
|
||||
|
||||
extract-properties-names
|
||||
(fn [shape]
|
||||
;; Get the names of the properties of the shape's component
|
||||
(->> shape
|
||||
(#(ctkl/get-component file-data (:component-id %) true))
|
||||
:variant-properties
|
||||
(map :name)))
|
||||
|
||||
prop-names (extract-properties-names first-child)]
|
||||
|
||||
(run! (fn [child-id]
|
||||
(when-let [child (get objects child-id)]
|
||||
(if (not (ctk/is-variant? child))
|
||||
@@ -583,7 +592,7 @@
|
||||
(str/ffmt "Main instance in variant % should have the variant-id of the container but has %" (:id child) (:variant-id child))
|
||||
child file page
|
||||
:variant-id shape-id))
|
||||
(when (not= prop-names (cfv/extract-properties-names child file-data))
|
||||
(when (not= prop-names (extract-properties-names child))
|
||||
(report-error :invalid-variant-properties
|
||||
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||
child file page
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
(ns app.common.files.variant
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctcl]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.variant :as ctv]))
|
||||
|
||||
(defn find-variant-components
|
||||
@@ -25,16 +25,9 @@
|
||||
;; We can't simply filter components, because we need to maintain the order
|
||||
(->> (dm/get-in objects [variant-id :shapes])
|
||||
(map #(dm/get-in objects [% :component-id]))
|
||||
(map #(ctcl/get-component data % true))
|
||||
(map #(ctkl/get-component data % true))
|
||||
reverse)))
|
||||
|
||||
(defn extract-properties-names
|
||||
[shape data]
|
||||
(->> shape
|
||||
(#(ctcl/get-component data (:component-id %) true))
|
||||
:variant-properties
|
||||
(map :name)))
|
||||
|
||||
(defn extract-properties-values
|
||||
"Get a map of properties associated to their possible values"
|
||||
[data objects variant-id]
|
||||
@@ -74,10 +67,10 @@
|
||||
|
||||
(defn get-primary-component
|
||||
[data component-id]
|
||||
(when-let [component (ctcl/get-component data component-id)]
|
||||
(if (ctc/is-variant? component)
|
||||
(when-let [component (ctkl/get-component data component-id)]
|
||||
(if (ctk/is-variant? component)
|
||||
(->> component
|
||||
(get-primary-variant data)
|
||||
:component-id
|
||||
(ctcl/get-component data))
|
||||
(ctkl/get-component data))
|
||||
component)))
|
||||
@@ -290,8 +290,11 @@
|
||||
duplicated-parent?
|
||||
(->> ids-map vals (some #(= % (:parent-id first-shape))))
|
||||
|
||||
grid-parent?
|
||||
(and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?))
|
||||
|
||||
changes
|
||||
(if (and (ctsl/grid-layout? objects (:parent-id first-shape)) (not duplicated-parent?))
|
||||
(if grid-parent?
|
||||
(let [target-cell (-> position meta :cell)
|
||||
|
||||
[row column]
|
||||
@@ -313,7 +316,19 @@
|
||||
changes
|
||||
(reduce #(pcb/add-object %1 %2 {:ignore-touched true})
|
||||
changes
|
||||
(rest new-shapes))]
|
||||
(rest new-shapes))
|
||||
|
||||
ids-to-validate (cond-> [(:id first-shape)]
|
||||
grid-parent?
|
||||
(conj (:parent-id first-shape)))
|
||||
|
||||
changes (if (seq ids-to-validate)
|
||||
(pcb/validate-shapes changes
|
||||
(:id page)
|
||||
ids-to-validate
|
||||
(str "generate-instantiate-component: " component-id
|
||||
" under parent-id" (or parent-id " root")))
|
||||
changes)]
|
||||
|
||||
[new-shape changes])))
|
||||
|
||||
@@ -3123,7 +3138,6 @@
|
||||
;; we calculate a new one because the components will have created new shapes.
|
||||
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
|
||||
|
||||
|
||||
;; If there is an alt-duplication we change to root
|
||||
;; For variants so the copy is made as a child of root
|
||||
;; This is because inside a variant-container can't be a copy
|
||||
@@ -3135,7 +3149,6 @@
|
||||
(assoc :parent-id uuid/zero :frame-id uuid/zero)))
|
||||
shapes)
|
||||
|
||||
|
||||
changes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects all-objects)
|
||||
@@ -3165,7 +3178,20 @@
|
||||
(comp
|
||||
(filter #(= :add-obj (:type %)))
|
||||
(map #(vector (:old-id %) (-> % :obj :id))))
|
||||
(:redo-changes changes))]
|
||||
(:redo-changes changes))
|
||||
|
||||
copied-components
|
||||
(ctn/get-all-instance-roots (:objects page) ids)
|
||||
|
||||
ids-to-validate
|
||||
(map #(get ids-map % %) copied-components)
|
||||
|
||||
changes (if (seq ids-to-validate)
|
||||
(pcb/validate-shapes changes
|
||||
(:id page)
|
||||
ids-to-validate
|
||||
(cond-> (str "generate-duplicate-changes: " ids)))
|
||||
changes)]
|
||||
|
||||
(-> changes
|
||||
(generate-duplicate-flows shapes page ids-map)
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
(reduce check-shape changes mod-obj-changes)))
|
||||
|
||||
(defn generate-update-shapes
|
||||
[changes ids update-fn objects {:keys [attrs changed-sub-attr ignore-tree ignore-touched with-objects? translation?]}]
|
||||
[changes ids update-fn objects {:keys [attrs changed-sub-attr ignore-tree ignore-touched with-objects? translation? extra-context]}]
|
||||
(let [changes (reduce
|
||||
(fn [changes id]
|
||||
(let [opts {:attrs attrs
|
||||
@@ -96,7 +96,19 @@
|
||||
(pcb/reorder-grid-children ids))
|
||||
|
||||
(not ignore-touched)
|
||||
(generate-unapply-tokens objects changed-sub-attr))]
|
||||
(generate-unapply-tokens objects changed-sub-attr))
|
||||
|
||||
page-id (pcb/get-page-id changes)
|
||||
modified-components (ctn/get-all-instance-roots objects ids)
|
||||
|
||||
changes (if (and page-id (seq modified-components))
|
||||
(pcb/validate-shapes changes
|
||||
page-id
|
||||
modified-components
|
||||
(cond-> (str "generate-update-shapes: " ids " " attrs)
|
||||
(some? extra-context)
|
||||
(str " \n -> from " extra-context)))
|
||||
changes)]
|
||||
changes))
|
||||
|
||||
(defn- generate-update-shape-flags
|
||||
@@ -248,8 +260,8 @@
|
||||
page-id (pcb/get-page-id changes)
|
||||
page (or (pcb/get-page changes)
|
||||
(ctpl/get-page data page-id))
|
||||
|
||||
ids (cfh/clean-loops objects ids)
|
||||
|
||||
in-component-copy?
|
||||
(fn [shape-id]
|
||||
;; Look for shapes that are inside a component copy, but are
|
||||
@@ -258,7 +270,7 @@
|
||||
;; If we want to specifically allow altering the copies, this is
|
||||
;; a special case, like a component swap, in which case we want
|
||||
;; to delete the old shape
|
||||
(let [shape (get objects shape-id)]
|
||||
(let [shape (get objects shape-id)]
|
||||
(and (ctn/has-any-copy-parent? objects shape)
|
||||
(not allow-altering-copies))))
|
||||
|
||||
@@ -437,7 +449,19 @@
|
||||
(into []
|
||||
(remove #(and (ctsi/has-destination %)
|
||||
(id-to-delete? (:destination %))))
|
||||
interactions))))))]
|
||||
interactions))))))
|
||||
|
||||
modified-components (ctn/get-all-instance-roots objects (disj all-parents uuid/zero))
|
||||
;; There is no need to validate deleted objects. Probably also no need to validate hidden or unmasked objects,
|
||||
;; but we may think of it
|
||||
|
||||
changes (if (seq modified-components)
|
||||
(pcb/validate-shapes changes
|
||||
page-id
|
||||
modified-components
|
||||
(str "generate-delete-shapes: " ids))
|
||||
changes)]
|
||||
|
||||
[all-parents changes])))
|
||||
|
||||
|
||||
|
||||
@@ -172,10 +172,10 @@
|
||||
new-props (- min-props
|
||||
(+ (count props)
|
||||
(if add-name? 1 0)))
|
||||
props (ctv/add-new-props props (repeat new-props ""))]
|
||||
props (ctv/add-new-properties props (repeat new-props ""))]
|
||||
|
||||
(if add-name?
|
||||
(ctv/add-new-prop props (:name component))
|
||||
(ctv/add-new-property props (:name component))
|
||||
props)))
|
||||
|
||||
(defn- create-new-properties-from-non-variant
|
||||
|
||||
@@ -216,6 +216,41 @@
|
||||
:else
|
||||
(get-instance-root objects (get objects (:parent-id shape)))))
|
||||
|
||||
(defn get-all-instance-roots
|
||||
"Given a list of shape ids and an objects tree, returns a set with the ids of
|
||||
all instance roots that are at, above or below any of the shapes identified by
|
||||
the given list. An instance root is a shape that has :component-root set to
|
||||
true (checked by ctk/instance-root?). There is at most one instance root in
|
||||
any subtree rooted at an instance root, so the downward search stops at the
|
||||
first instance root found in each branch. Uses a visited set to avoid
|
||||
reprocessing the same shapes."
|
||||
[objects shape-ids]
|
||||
(let [visited (atom #{})
|
||||
result (atom #{})]
|
||||
(letfn [(search-up [shape-id]
|
||||
(when-not (contains? @visited shape-id)
|
||||
(swap! visited conj shape-id)
|
||||
(let [shape (get objects shape-id)]
|
||||
(when-not (nil? shape)
|
||||
(if (ctk/instance-root? shape)
|
||||
(swap! result conj (:id shape))
|
||||
(when-not (cfh/root? shape)
|
||||
(when-let [parent-id (:parent-id shape)]
|
||||
(search-up parent-id))))))))
|
||||
(search-down [shape-id]
|
||||
(when-not (contains? @visited shape-id)
|
||||
(swap! visited conj shape-id)
|
||||
(let [shape (get objects shape-id)]
|
||||
(when-not (nil? shape)
|
||||
(if (ctk/instance-root? shape)
|
||||
(swap! result conj (:id shape))
|
||||
(doseq [child-id (:shapes shape)]
|
||||
(search-down child-id)))))))]
|
||||
(doseq [shape-id shape-ids]
|
||||
(search-up shape-id)
|
||||
(search-down shape-id))
|
||||
@result)))
|
||||
|
||||
(defn find-component-main
|
||||
"If the shape is a component main instance or is inside one, return that instance.
|
||||
Uses an iterative loop with cycle detection to prevent stack overflow on circular
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
[:variant-id {:optional true} ::sm/uuid]
|
||||
[:variant-properties {:optional true} [:vector schema:variant-property]]])
|
||||
|
||||
(def valid-variant-component?
|
||||
(sm/check-fn schema:variant-component))
|
||||
|
||||
(def schema:variant-shape
|
||||
"The root shape of the main instance of a variant component"
|
||||
[:map
|
||||
@@ -34,14 +37,17 @@
|
||||
[:variant-name {:optional true} :string]
|
||||
[:variant-error {:optional true} :string]])
|
||||
|
||||
(def valid-variant-shape?
|
||||
(sm/check-fn schema:variant-shape))
|
||||
|
||||
(def schema:variant-container
|
||||
"Is a board that contains all variant components of a variant set,
|
||||
for grouping them visually in the workspace"
|
||||
[:map
|
||||
[:is-variant-container {:optional true} :boolean]])
|
||||
|
||||
(def valid-variant-component?
|
||||
(sm/check-fn schema:variant-component))
|
||||
(def valid-variant-container?
|
||||
(sm/check-fn schema:variant-container))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -51,7 +57,12 @@
|
||||
(def value-prefix "Value ")
|
||||
|
||||
(defn properties-to-name
|
||||
"Transform the properties into a name, with the values separated by comma"
|
||||
"Transform the properties into a name, with the values separated by comma.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'Button'}
|
||||
{:name 'Property 2' :value 'Primary'}] -> 'Button, Primary'"
|
||||
[properties]
|
||||
(->> properties
|
||||
(map :value)
|
||||
@@ -59,7 +70,12 @@
|
||||
(str/join ", ")))
|
||||
|
||||
(defn next-property-number
|
||||
"Returns the next property number, to avoid duplicates on the property names"
|
||||
"Returns the next property number, to avoid duplicates on the property names.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 3' :value 'y'}] -> 4"
|
||||
[properties]
|
||||
(let [numbers (keep
|
||||
#(some->> (:name %) (re-find property-regex) second d/parse-integer)
|
||||
@@ -69,37 +85,61 @@
|
||||
0)]
|
||||
(inc (max max-num (count properties)))))
|
||||
|
||||
(defn add-new-prop
|
||||
"Adds a new property with generated name and provided value to the existing props list."
|
||||
[props value]
|
||||
(conj props {:name (str property-prefix (next-property-number props))
|
||||
:value value}))
|
||||
(defn add-new-property
|
||||
"Adds a new property with generated name and provided value to the existing properties list.
|
||||
|
||||
(defn add-new-props
|
||||
"Adds new properties with generated names and provided values to the existing props list."
|
||||
[props values]
|
||||
(let [next-prop-num (next-property-number props)
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}] 'y' -> [{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}]"
|
||||
[properties value]
|
||||
(conj properties {:name (str property-prefix (next-property-number properties))
|
||||
:value value}))
|
||||
|
||||
(defn add-new-properties
|
||||
"Adds new properties with generated names and provided values to the existing properties list.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}] ['a' 'b'] -> [{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'a'}
|
||||
{:name 'Property 3' :value 'b'}]"
|
||||
[properties values]
|
||||
(let [next-prop-num (next-property-number properties)
|
||||
xf (map-indexed (fn [i v]
|
||||
{:name (str property-prefix (+ next-prop-num i))
|
||||
:value v}))]
|
||||
(into props xf values)))
|
||||
(into properties xf values)))
|
||||
|
||||
(defn path-to-properties
|
||||
"From a list of properties and a name with path, assign each token of the
|
||||
path as value of a different property"
|
||||
path as value of a different property. It can add blank properties if
|
||||
necessary, until the min-properties number is reached.
|
||||
|
||||
Example with min-properties=4:
|
||||
|
||||
'Button / Primary / Hover' -> [{:name 'Property 1' :value 'Button'}
|
||||
{:name 'Property 2' :value 'Primary'}
|
||||
{:name 'Property 3' :value 'Hover'}
|
||||
{:name 'Property 4' :value ''}]"
|
||||
([path properties]
|
||||
(path-to-properties path properties 0))
|
||||
([path properties min-props]
|
||||
([path properties min-properties]
|
||||
(let [cpath (cpn/split-path path)
|
||||
total-props (max (count cpath) min-props)
|
||||
total-properties (max (count cpath) min-properties)
|
||||
assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range))
|
||||
;; Add empty strings to the end of cpath to reach the minimum number of properties
|
||||
cpath (take total-props (concat cpath (repeat "")))
|
||||
cpath (take total-properties (concat cpath (repeat "")))
|
||||
remaining (drop (count properties) cpath)]
|
||||
(add-new-props assigned remaining))))
|
||||
(add-new-properties assigned remaining))))
|
||||
|
||||
(defn properties-map->formula
|
||||
"Transforms a map of properties to a formula of properties omitting the empty ones"
|
||||
"Transforms a map of properties to a formula of properties omitting the empty ones.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'Button'}
|
||||
{:name 'Property 2' :value 'Primary'}] -> 'Property 1=Button, Property 2=Primary'"
|
||||
[properties]
|
||||
(->> properties
|
||||
(keep (fn [{:keys [name value]}]
|
||||
@@ -108,7 +148,12 @@
|
||||
(str/join ", ")))
|
||||
|
||||
(defn properties-formula->map
|
||||
"Transforms a formula of properties to a map of properties"
|
||||
"Transforms a formula of properties to a map of properties.
|
||||
|
||||
Example:
|
||||
|
||||
'Property 1=Button, Property 2=Primary' -> [{:name 'Property 1' :value 'Button'}
|
||||
{:name 'Property 2' :value 'Primary'}]"
|
||||
[s]
|
||||
(->> (str/split s ",")
|
||||
(mapv #(str/split % "=" 2))
|
||||
@@ -118,7 +163,12 @@
|
||||
:value (str/trim v)}))))
|
||||
|
||||
(defn valid-properties-formula?
|
||||
"Checks if a formula is valid"
|
||||
"Checks if a formula is valid.
|
||||
|
||||
Example:
|
||||
|
||||
'Property 1=Button, Property 2=Primary' -> true
|
||||
'Property 1=Button, Property 2' -> false"
|
||||
[s]
|
||||
(->> (str/split s ",")
|
||||
(mapv #(str/split % "=" 2))
|
||||
@@ -128,22 +178,41 @@
|
||||
(< (count (second %)) property-max-length)))))
|
||||
|
||||
(defn find-properties-to-remove
|
||||
"Compares two property maps to find which properties should be removed"
|
||||
[prev-props upd-props]
|
||||
(let [upd-names (set (map :name upd-props))]
|
||||
(filterv #(not (contains? upd-names (:name %))) prev-props)))
|
||||
"Compares two property maps to find which properties should be removed.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}]
|
||||
[{:name 'Property 1' :value 'x'}] -> [{:name 'Property 2' :value 'y'}]"
|
||||
[prev-properties upd-properties]
|
||||
(let [upd-names (set (map :name upd-properties))]
|
||||
(filterv #(not (contains? upd-names (:name %))) prev-properties)))
|
||||
|
||||
(defn find-properties-to-update
|
||||
"Compares two property maps to find which properties should be updated"
|
||||
[prev-props upd-props]
|
||||
"Compares two property maps to find which properties should be updated.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}]
|
||||
[{:name 'Property 1' :value 'new-x'}
|
||||
{:name 'Property 2' :value 'y'}] -> [{:name 'Property 1' :value 'new-x'}]"
|
||||
[prev-properties upd-properties]
|
||||
(filterv #(some (fn [prop] (and (= (:name %) (:name prop))
|
||||
(not= (:value %) (:value prop)))) prev-props) upd-props))
|
||||
(not= (:value %) (:value prop)))) prev-properties) upd-properties))
|
||||
|
||||
(defn find-properties-to-add
|
||||
"Compares two property maps to find which properties should be added"
|
||||
[prev-props upd-props]
|
||||
(let [prev-names (set (map :name prev-props))]
|
||||
(filterv #(not (contains? prev-names (:name %))) upd-props)))
|
||||
"Compares two property maps to find which properties should be added.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}]
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}] -> [{:name 'Property 2' :value 'y'}]"
|
||||
[prev-properties upd-properties]
|
||||
(let [prev-names (set (map :name prev-properties))]
|
||||
(filterv #(not (contains? prev-names (:name %))) upd-properties)))
|
||||
|
||||
(defn- split-base-name-and-number
|
||||
"Extract the number in parentheses from an item, if present, and return both the base name and the number"
|
||||
@@ -165,7 +234,12 @@
|
||||
|
||||
(defn update-number-in-repeated-item
|
||||
"Add, keep or update a number in parentheses for a given item, if necessary, depending on the items
|
||||
already present in a list, to avoid repetitions"
|
||||
already present in a list, to avoid repetitions.
|
||||
|
||||
Example:
|
||||
|
||||
['Property'] 'Property' -> 'Property (1)'
|
||||
['Property' 'Property (1)'] 'Property' -> 'Property (2)'"
|
||||
[items item]
|
||||
(let [names (group-numbers-by-base-name items)
|
||||
[base num] (split-base-name-and-number item)
|
||||
@@ -176,24 +250,40 @@
|
||||
(str base (when (pos? n) (str " (" n ")")))))))
|
||||
|
||||
(defn update-number-in-repeated-prop-names
|
||||
"Add, keep or update a number for each prop name depending on the previous ones"
|
||||
[props]
|
||||
(->> props
|
||||
"Add, keep or update a number for each prop name depending on the previous ones.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property' :value 'x'}
|
||||
{:name 'Property' :value 'y'}] -> [{:name 'Property' :value 'x'}
|
||||
{:name 'Property (1)' :value 'y'}]"
|
||||
[properties]
|
||||
(->> properties
|
||||
(reduce (fn [acc prop]
|
||||
(conj acc {:name (update-number-in-repeated-item (mapv :name acc) (:name prop))
|
||||
:value (:value prop)}))
|
||||
[])))
|
||||
|
||||
(defn find-index-for-property-name
|
||||
"Finds the index of a name in a property map"
|
||||
[props name]
|
||||
"Finds the index of a name in a property map.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}] 'Property 2' -> 1"
|
||||
[properties name]
|
||||
(some (fn [[idx prop]]
|
||||
(when (= (:name prop) name)
|
||||
idx))
|
||||
(map-indexed vector props)))
|
||||
(map-indexed vector properties)))
|
||||
|
||||
(defn remove-prefix
|
||||
"Removes the given prefix (with or without a trailing ' / ') from the beginning of the name"
|
||||
"Removes the given prefix (with or without a trailing ' / ') from the beginning of the name.
|
||||
|
||||
Example:
|
||||
|
||||
'Button / Primary' 'Button' -> 'Primary'
|
||||
'Button / Primary' 'Other' -> 'Button / Primary'"
|
||||
[name prefix]
|
||||
(let [long-name (str prefix " / ")]
|
||||
(cond
|
||||
@@ -210,22 +300,22 @@
|
||||
(map :name))
|
||||
|
||||
(defn- matching-indices
|
||||
[props1 props2]
|
||||
(let [names-in-p2 (into #{} xf:map-name props2)
|
||||
[properties1 properties2]
|
||||
(let [names-in-p2 (into #{} xf:map-name properties2)
|
||||
xform (comp
|
||||
(map-indexed (fn [index {:keys [name]}]
|
||||
(when (contains? names-in-p2 name)
|
||||
index)))
|
||||
(filter some?))]
|
||||
(into #{} xform props1)))
|
||||
(into #{} xform properties1)))
|
||||
|
||||
(defn- find-index-by-name
|
||||
"Returns the index of the first item in props with the given name, or nil if not found."
|
||||
[name props]
|
||||
"Returns the index of the first item in properties with the given name, or nil if not found."
|
||||
[name properties]
|
||||
(some (fn [[idx item]]
|
||||
(when (= (:name item) name)
|
||||
idx))
|
||||
(map-indexed vector props)))
|
||||
(map-indexed vector properties)))
|
||||
|
||||
(defn- next-valid-position
|
||||
"Returns the first non-negative integer not present in the used-pos set."
|
||||
@@ -236,42 +326,60 @@
|
||||
p)))
|
||||
|
||||
(defn- find-position
|
||||
"Returns the index of the property with the given name in `props`,
|
||||
"Returns the index of the property with the given name in `properties`,
|
||||
or the next available index not in `used-pos` if not found."
|
||||
[name props used-pos]
|
||||
(or (find-index-by-name name props)
|
||||
[name properties used-pos]
|
||||
(or (find-index-by-name name properties)
|
||||
(next-valid-position used-pos)))
|
||||
|
||||
(defn merge-properties
|
||||
"Merges props2 into props1 with the following rules:
|
||||
- For each property p2 in props2:
|
||||
"Merges properties2 into properties1 with the following rules:
|
||||
- For each property p2 in properties2:
|
||||
- Skip it if its value is empty.
|
||||
- If props1 contains a property with the same name, update its value with that of p2.
|
||||
- Otherwise, assign p2's value to the first unused property in props1. A property is considered used if:
|
||||
- Its name exists in both props1 and props2, or
|
||||
- If properties1 contains a property with the same name, update its value with that of p2.
|
||||
- Otherwise, assign p2's value to the first unused property in properties1. A property is considered used if:
|
||||
- Its name exists in both properties1 and properties2, or
|
||||
- Its value has already been updated during the merge.
|
||||
- If no unused properties are available in props1, append a new property with a default name and p2's value."
|
||||
[props1 props2]
|
||||
(let [props2 (remove #(str/empty? (:value %)) props2)]
|
||||
- If no unused properties are available in properties1, append a new property with a default name and p2's value.
|
||||
|
||||
Example:
|
||||
|
||||
[{:name 'Property 1' :value 'a'}
|
||||
{:name 'Property 2' :value 'b'}]
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}
|
||||
{:name 'Property 3' :value 'z'}] -> [{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}
|
||||
{:name 'Property 3' :value 'z'}]"
|
||||
[properties1 properties2]
|
||||
(let [properties2 (remove #(str/empty? (:value %)) properties2)]
|
||||
(-> (reduce
|
||||
(fn [{:keys [props used-pos]} prop]
|
||||
(let [pos (find-position (:name prop) props used-pos)
|
||||
(fn [{:keys [properties used-pos]} prop]
|
||||
(let [pos (find-position (:name prop) properties used-pos)
|
||||
used-pos (conj used-pos pos)]
|
||||
(if (< pos (count props))
|
||||
{:props (assoc-in (vec props) [pos :value] (:value prop)) :used-pos used-pos}
|
||||
{:props (add-new-prop props (:value prop)) :used-pos used-pos})))
|
||||
{:props (vec props1) :used-pos (matching-indices props1 props2)}
|
||||
props2)
|
||||
:props)))
|
||||
(if (< pos (count properties))
|
||||
{:properties (assoc-in (vec properties) [pos :value] (:value prop)) :used-pos used-pos}
|
||||
{:properties (add-new-property properties (:value prop)) :used-pos used-pos})))
|
||||
{:properties (vec properties1) :used-pos (matching-indices properties1 properties2)}
|
||||
properties2)
|
||||
:properties)))
|
||||
|
||||
(defn compare-properties
|
||||
"Compares vectors of properties keeping the value if it is the same for all
|
||||
or setting a custom value where their values do not coincide"
|
||||
([props-list]
|
||||
(compare-properties props-list nil))
|
||||
or setting a custom value where their values do not coincide.
|
||||
|
||||
([props-list distinct-mark]
|
||||
(let [grouped (group-by :name (apply concat props-list))
|
||||
Example:
|
||||
|
||||
[[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'y'}]
|
||||
[{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value 'z'}]] -> [{:name 'Property 1' :value 'x'}
|
||||
{:name 'Property 2' :value nil}]"
|
||||
([properties-list]
|
||||
(compare-properties properties-list nil))
|
||||
|
||||
([properties-list distinct-mark]
|
||||
(let [grouped (group-by :name (apply concat properties-list))
|
||||
check-values (fn [values]
|
||||
(let [vals (map :value values)]
|
||||
(if (apply = vals)
|
||||
@@ -281,33 +389,35 @@
|
||||
{:name name :value (check-values values)})
|
||||
grouped))))
|
||||
|
||||
(defn same-variant?
|
||||
"Determines if all elements belong to the same variant"
|
||||
[components]
|
||||
(let [variant-ids (distinct (map :variant-id components))
|
||||
not-blank? (complement str/blank?)]
|
||||
(and
|
||||
(= 1 (count variant-ids))
|
||||
(not-blank? (first variant-ids)))))
|
||||
(defn properties-distance
|
||||
"Computes a weighted distance between two property lists `properties1` and `properties2`.
|
||||
Latter properties weight less that previous ones.
|
||||
|
||||
(defn distance
|
||||
"Computes a weighted distance between two property lists `props1` and `props2`.
|
||||
Latter properties weight less that previous ones"
|
||||
[props1 props2]
|
||||
(let [total-num-props (count props1)
|
||||
Example:
|
||||
|
||||
[{:name 'type' :value 'primary'}
|
||||
{:name 'status' :value 'default'}]
|
||||
[{:name 'type' :value 'primary'}
|
||||
{:name 'status' :value 'hover'}] -> 1.0"
|
||||
[properties1 properties2]
|
||||
(let [total-num-properties (count properties1)
|
||||
xform (map-indexed
|
||||
(fn [idx [p1 p2]]
|
||||
(if (not= p1 p2)
|
||||
(math/pow 2 (- total-num-props idx))
|
||||
(math/pow 2 (- total-num-properties idx))
|
||||
0)))]
|
||||
(transduce
|
||||
xform
|
||||
+
|
||||
(map vector props1 props2))))
|
||||
(map vector properties1 properties2))))
|
||||
|
||||
(defn variant-name-to-name
|
||||
"Transforms a variant-name (its properties values) into a standard name:
|
||||
the real name of the shape joined by the properties values separated by '/'"
|
||||
the real name of the shape joined by the properties values separated by '/'.
|
||||
|
||||
Example:
|
||||
|
||||
{:name 'Button' :variant-name 'Primary, Hover'} -> 'Button / Primary / Hover'"
|
||||
[variant]
|
||||
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
|
||||
|
||||
@@ -318,7 +428,12 @@
|
||||
|
||||
(defn find-boolean-pair
|
||||
"Given a vector, return a map that contains the boolean equivalency if the values match
|
||||
with any of the boolean pairs. Returns nil if none match."
|
||||
with any of the boolean pairs. Returns nil if none match.
|
||||
|
||||
Example:
|
||||
|
||||
['on' 'off'] -> {'on' true 'off' false}
|
||||
['foo' 'bar'] -> nil"
|
||||
[[a b :as v]]
|
||||
(let [a' (-> a str/trim str/lower)
|
||||
b' (-> b str/trim str/lower)]
|
||||
@@ -330,3 +445,17 @@
|
||||
(= a' f)) {b true a false}
|
||||
:else nil))
|
||||
boolean-pairs))))
|
||||
|
||||
(defn same-variant?
|
||||
"Determines if all elements belong to the same variant.
|
||||
|
||||
Example:
|
||||
|
||||
[{:variant-id 'abc'} {:variant-id 'abc'}] -> true
|
||||
[{:variant-id 'abc'} {:variant-id 'def'}] -> false"
|
||||
[components]
|
||||
(let [variant-ids (distinct (map :variant-id components))
|
||||
not-blank? (complement str/blank?)]
|
||||
(and
|
||||
(= 1 (count variant-ids))
|
||||
(not-blank? (first variant-ids)))))
|
||||
@@ -87,6 +87,7 @@
|
||||
[common-tests.types.token-test]
|
||||
[common-tests.types.tokens-lib-test]
|
||||
[common-tests.types.tokens-status-test]
|
||||
[common-tests.types.variant-test]
|
||||
[common-tests.undo-stack-test]
|
||||
[common-tests.uuid-test]))
|
||||
|
||||
@@ -142,6 +143,7 @@
|
||||
'common-tests.logic.token-test
|
||||
'common-tests.logic.variants-switch-test
|
||||
'common-tests.math-test
|
||||
'common-tests.types.variant-test
|
||||
'common-tests.media-test
|
||||
'common-tests.path-names-test
|
||||
'common-tests.record-test
|
||||
|
||||
@@ -9,8 +9,220 @@
|
||||
[app.common.types.variant :as ctv]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest properties-to-name-test
|
||||
(t/is (= "" (ctv/properties-to-name [])))
|
||||
(t/is (= "" (ctv/properties-to-name nil)))
|
||||
(t/is (= "Button, Primary" (ctv/properties-to-name [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value "Primary"}])))
|
||||
(t/is (= "Button" (ctv/properties-to-name [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value ""}]))))
|
||||
|
||||
(t/deftest variant-distance01
|
||||
(t/deftest next-property-number-test
|
||||
(t/is (= 1 (ctv/next-property-number [])))
|
||||
(t/is (= 1 (ctv/next-property-number nil)))
|
||||
(t/is (= 2 (ctv/next-property-number [{:name "Property 1" :value "x"}])))
|
||||
(t/is (= 4 (ctv/next-property-number [{:name "Property 3" :value "x"}])))
|
||||
(t/is (= 3 (ctv/next-property-number [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]))))
|
||||
|
||||
(t/deftest add-new-property-test
|
||||
(t/is (= [{:name "Property 1" :value "x"}]
|
||||
(ctv/add-new-property [] "x")))
|
||||
(t/is (= [{:name "Property 1" :value "x"}]
|
||||
(ctv/add-new-property nil "x")))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
|
||||
(ctv/add-new-property [{:name "Property 1" :value "x"}] "y"))))
|
||||
|
||||
(t/deftest add-new-properties-test
|
||||
(t/is (= [{:name "Property 1" :value "a"} {:name "Property 2" :value "b"}]
|
||||
(ctv/add-new-properties [] ["a" "b"])))
|
||||
(t/is (= '({:name "Property 2" :value "b"} {:name "Property 1" :value "a"})
|
||||
(ctv/add-new-properties nil ["a" "b"])))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "a"} {:name "Property 3" :value "b"}]
|
||||
(ctv/add-new-properties [{:name "Property 1" :value "x"}] ["a" "b"]))))
|
||||
|
||||
(t/deftest path-to-properties-test
|
||||
(t/is (= [] (ctv/path-to-properties "" [])))
|
||||
(t/is (= [{:name "Property 1" :value "a"} {:name "Property 2" :value "b"}]
|
||||
(ctv/path-to-properties "a / b" nil)))
|
||||
(t/is (= [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value "Primary"}
|
||||
{:name "Property 3" :value "Hover"}]
|
||||
(ctv/path-to-properties "Button / Primary / Hover" [])))
|
||||
(t/is (= [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value "Primary"}
|
||||
{:name "Property 3" :value "Hover"}
|
||||
{:name "Property 4" :value ""}]
|
||||
(ctv/path-to-properties "Button / Primary / Hover" [] 4)))
|
||||
(t/is (= [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value "Primary"}]
|
||||
(ctv/path-to-properties "Button / Primary" [{:name "Property 1" :value "old"}
|
||||
{:name "Property 2" :value "old2"}]))))
|
||||
|
||||
(t/deftest properties-map->formula-test
|
||||
(t/is (= "" (ctv/properties-map->formula [])))
|
||||
(t/is (= "" (ctv/properties-map->formula nil)))
|
||||
(t/is (= "Property 1=Button, Property 2=Primary"
|
||||
(ctv/properties-map->formula [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value "Primary"}])))
|
||||
(t/is (= "Property 1=Button"
|
||||
(ctv/properties-map->formula [{:name "Property 1" :value "Button"}
|
||||
{:name "Property 2" :value ""}]))))
|
||||
|
||||
(t/deftest properties-formula->map-test
|
||||
(t/is (= [] (ctv/properties-formula->map "")))
|
||||
(t/is (= [] (ctv/properties-formula->map nil)))
|
||||
(t/is (= [{:name "Property 1" :value "Button"} {:name "Property 2" :value "Primary"}]
|
||||
(ctv/properties-formula->map "Property 1=Button, Property 2=Primary")))
|
||||
(t/is (= [{:name "Property 1" :value "Button"}]
|
||||
(ctv/properties-formula->map "Property 1=Button, Property 2="))))
|
||||
|
||||
(t/deftest valid-properties-formula?-test
|
||||
(t/is (= true (ctv/valid-properties-formula? "Property 1=Button, Property 2=Primary")))
|
||||
(t/is (= false (ctv/valid-properties-formula? "")))
|
||||
(t/is (= true (ctv/valid-properties-formula? nil)))
|
||||
(t/is (= false (ctv/valid-properties-formula? "Property 1=Button, Property 2"))))
|
||||
|
||||
(t/deftest find-properties-to-remove-test
|
||||
(t/is (= [] (ctv/find-properties-to-remove [] [])))
|
||||
(t/is (= [] (ctv/find-properties-to-remove nil nil)))
|
||||
(t/is (= [{:name "Property 3" :value "z"}]
|
||||
(ctv/find-properties-to-remove [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}
|
||||
{:name "Property 3" :value "z"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}])))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
|
||||
(ctv/find-properties-to-remove [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]
|
||||
[{:name "Property 3" :value "z"}]))))
|
||||
|
||||
(t/deftest find-properties-to-update-test
|
||||
(t/is (= [] (ctv/find-properties-to-update [] [])))
|
||||
(t/is (= [] (ctv/find-properties-to-update nil nil)))
|
||||
(t/is (= [{:name "Property 1" :value "new-x"}]
|
||||
(ctv/find-properties-to-update [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]
|
||||
[{:name "Property 1" :value "new-x"}
|
||||
{:name "Property 2" :value "y"}])))
|
||||
(t/is (= [{:name "Property 1" :value "new-x"} {:name "Property 2" :value "new-y"}]
|
||||
(ctv/find-properties-to-update [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]
|
||||
[{:name "Property 1" :value "new-x"}
|
||||
{:name "Property 2" :value "new-y"}]))))
|
||||
|
||||
(t/deftest find-properties-to-add-test
|
||||
(t/is (= [] (ctv/find-properties-to-add [] [])))
|
||||
(t/is (= [] (ctv/find-properties-to-add nil nil)))
|
||||
(t/is (= [{:name "Property 3" :value "z"}]
|
||||
(ctv/find-properties-to-add [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}
|
||||
{:name "Property 3" :value "z"}])))
|
||||
(t/is (= [{:name "Property 2" :value "y"}]
|
||||
(ctv/find-properties-to-add [{:name "Property 1" :value "x"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]))))
|
||||
|
||||
(t/deftest update-number-in-repeated-item-test
|
||||
(t/is (= "Property" (ctv/update-number-in-repeated-item [] "Property")))
|
||||
(t/is (= "Property" (ctv/update-number-in-repeated-item nil "Property")))
|
||||
(t/is (= "Property (1)" (ctv/update-number-in-repeated-item ["Property"] "Property")))
|
||||
(t/is (= "Property (2)" (ctv/update-number-in-repeated-item ["Property" "Property (1)"] "Property")))
|
||||
(t/is (= "Property" (ctv/update-number-in-repeated-item ["Other"] "Property"))))
|
||||
|
||||
(t/deftest update-number-in-repeated-prop-names-test
|
||||
(t/is (= [] (ctv/update-number-in-repeated-prop-names [])))
|
||||
(t/is (= [] (ctv/update-number-in-repeated-prop-names nil)))
|
||||
(t/is (= [{:name "Property" :value "x"}]
|
||||
(ctv/update-number-in-repeated-prop-names [{:name "Property" :value "x"}])))
|
||||
(t/is (= [{:name "Property" :value "x"} {:name "Property (1)" :value "y"}]
|
||||
(ctv/update-number-in-repeated-prop-names [{:name "Property" :value "x"}
|
||||
{:name "Property" :value "y"}])))
|
||||
(t/is (= [{:name "Property" :value "x"} {:name "Property (1)" :value "y"} {:name "Property (2)" :value "z"}]
|
||||
(ctv/update-number-in-repeated-prop-names [{:name "Property" :value "x"}
|
||||
{:name "Property" :value "y"}
|
||||
{:name "Property" :value "z"}]))))
|
||||
|
||||
(t/deftest find-index-for-property-name-test
|
||||
(t/is (= nil (ctv/find-index-for-property-name [] "Property 1")))
|
||||
(t/is (= nil (ctv/find-index-for-property-name nil "Property 1")))
|
||||
(t/is (= 0 (ctv/find-index-for-property-name [{:name "Property 1" :value "x"}] "Property 1")))
|
||||
(t/is (= 1 (ctv/find-index-for-property-name [{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}] "Property 2")))
|
||||
(t/is (= nil (ctv/find-index-for-property-name [{:name "Property 1" :value "x"}] "Property 3"))))
|
||||
|
||||
(t/deftest remove-prefix-test
|
||||
(t/is (= "name" (ctv/remove-prefix "name" "")))
|
||||
(t/is (= "name" (ctv/remove-prefix "name" nil)))
|
||||
(t/is (= "Primary" (ctv/remove-prefix "Button / Primary" "Button")))
|
||||
(t/is (= "Primary" (ctv/remove-prefix "Button / Primary" "Button / ")))
|
||||
(t/is (= "Button / Primary" (ctv/remove-prefix "Button / Primary" "Other"))))
|
||||
|
||||
(t/deftest merge-properties-test
|
||||
(t/is (= [] (ctv/merge-properties [] [])))
|
||||
(t/is (= [] (ctv/merge-properties nil nil)))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
|
||||
(ctv/merge-properties [{:name "Property 1" :value "a"}
|
||||
{:name "Property 2" :value "b"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}])))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"} {:name "Property 3" :value "z"}]
|
||||
(ctv/merge-properties [{:name "Property 1" :value "a"}
|
||||
{:name "Property 2" :value "b"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}
|
||||
{:name "Property 3" :value "z"}])))
|
||||
(t/is (= [{:name "Property 1" :value "a"} {:name "Property 2" :value "y"}]
|
||||
(ctv/merge-properties [{:name "Property 1" :value "a"}
|
||||
{:name "Property 2" :value "b"}]
|
||||
[{:name "Property 2" :value "y"}]))))
|
||||
|
||||
(t/deftest compare-properties-test
|
||||
(t/is (= [] (ctv/compare-properties [])))
|
||||
(t/is (= [] (ctv/compare-properties nil)))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "y"}]
|
||||
(ctv/compare-properties [[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]])))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value nil}]
|
||||
(ctv/compare-properties [[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "z"}]])))
|
||||
(t/is (= [{:name "Property 1" :value "x"} {:name "Property 2" :value "*"}]
|
||||
(ctv/compare-properties [[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "y"}]
|
||||
[{:name "Property 1" :value "x"}
|
||||
{:name "Property 2" :value "z"}]]
|
||||
"*"))))
|
||||
|
||||
(t/deftest variant-name-to-name-test
|
||||
(t/is (= "Button / Primary / Hover" (ctv/variant-name-to-name {:name "Button" :variant-name "Primary, Hover"})))
|
||||
(t/is (= "Button" (ctv/variant-name-to-name {:name "Button" :variant-name ""})))
|
||||
(t/is (= "Button" (ctv/variant-name-to-name {:name "Button" :variant-name nil})))
|
||||
(t/is (= "" (ctv/variant-name-to-name {:name "" :variant-name ""})))
|
||||
(t/is (= nil (ctv/variant-name-to-name {:name nil :variant-name nil}))))
|
||||
|
||||
(t/deftest find-boolean-pair-test
|
||||
(t/is (= {"on" true "off" false} (ctv/find-boolean-pair ["on" "off"])))
|
||||
(t/is (= {"yes" true "no" false} (ctv/find-boolean-pair ["yes" "no"])))
|
||||
(t/is (= {"true" true "false" false} (ctv/find-boolean-pair ["true" "false"])))
|
||||
(t/is (= {"on" true "off" false} (ctv/find-boolean-pair ["off" "on"])))
|
||||
(t/is (= {"ON" true "OFF" false} (ctv/find-boolean-pair ["ON" "OFF"])))
|
||||
(t/is (= nil (ctv/find-boolean-pair ["foo" "bar"])))
|
||||
(t/is (= nil (ctv/find-boolean-pair nil)))
|
||||
(t/is (= nil (ctv/find-boolean-pair ["on"]))))
|
||||
|
||||
(t/deftest same-variant?-test
|
||||
(t/is (= false (ctv/same-variant? [])))
|
||||
(t/is (= false (ctv/same-variant? nil)))
|
||||
(t/is (= true (ctv/same-variant? [{:variant-id "abc"}])))
|
||||
(t/is (= true (ctv/same-variant? [{:variant-id "abc"} {:variant-id "abc"}])))
|
||||
(t/is (= false (ctv/same-variant? [{:variant-id "abc"} {:variant-id "def"}])))
|
||||
(t/is (= false (ctv/same-variant? [{:variant-id ""} {:variant-id ""}]))))
|
||||
|
||||
(t/deftest properties-distance01
|
||||
;;c1: primary, default, rounded, blue, dark
|
||||
;;c2: primary, hover, squared, blue, dark
|
||||
;;c3: primary, default, squared, blue, light
|
||||
@@ -35,12 +247,12 @@
|
||||
{:name "borders" :value "rounded"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "theme" :value "light"}]
|
||||
dist2 (ctv/distance target props2)
|
||||
dist3 (ctv/distance target props3)]
|
||||
dist2 (ctv/properties-distance target props2)
|
||||
dist3 (ctv/properties-distance target props3)]
|
||||
(t/is (< dist3 dist2))))
|
||||
|
||||
|
||||
(t/deftest variant-distance02
|
||||
(t/deftest properties-distance02
|
||||
;;c1: primary, default, rounded, blue, dark
|
||||
;;c2: primary, hover, squared, red, dark
|
||||
;;c3: secondary, hover, rounded, blue, dark
|
||||
@@ -65,11 +277,11 @@
|
||||
{:name "borders" :value "rounded"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "theme" :value "dark"}]
|
||||
dist2 (ctv/distance target props2)
|
||||
dist3 (ctv/distance target props3)]
|
||||
dist2 (ctv/properties-distance target props2)
|
||||
dist3 (ctv/properties-distance target props3)]
|
||||
(t/is (< dist2 dist3))))
|
||||
|
||||
(t/deftest variant-distance03
|
||||
(t/deftest properties-distance03
|
||||
;;c1: primary, default, rounded, blue, dark
|
||||
;;c2: secondary, default, rounded, blue, light
|
||||
;;c3: secondary, hover, squared, blue, dark
|
||||
@@ -101,12 +313,18 @@
|
||||
{:name "borders" :value "rounded"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "theme" :value "dark"}]
|
||||
dist2 (ctv/distance target props2)
|
||||
dist3 (ctv/distance target props3)
|
||||
dist4 (ctv/distance target props4)]
|
||||
dist2 (ctv/properties-distance target props2)
|
||||
dist3 (ctv/properties-distance target props3)
|
||||
dist4 (ctv/properties-distance target props4)]
|
||||
(t/is (< dist2 dist4))
|
||||
(t/is (< dist4 dist3))))
|
||||
|
||||
(t/deftest properties-distance04
|
||||
(t/is (= 0 (ctv/properties-distance [] [])))
|
||||
(t/is (= 0 (ctv/properties-distance nil nil)))
|
||||
(t/is (= 0 (ctv/properties-distance [{:name "a" :value "x"}] [{:name "a" :value "x"}])))
|
||||
(t/is (= 2.0 (ctv/properties-distance [{:name "a" :value "x"} {:name "b" :value "y"}] [{:name "a" :value "x"} {:name "b" :value "z"}]))))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -103,12 +103,14 @@
|
||||
(wasm.api/process-objects shapes)
|
||||
(wasm.api/request-render "sync-wasm-structural-changes"))))))
|
||||
|
||||
(defn- apply-changes-localy
|
||||
(defn- apply-changes-locally
|
||||
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
||||
(ptk/reify ::apply-changes-localy
|
||||
(ptk/reify ::apply-changes-locally
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [undo-changes
|
||||
(let [libraries (dsh/lookup-libraries state)
|
||||
|
||||
undo-changes
|
||||
(if pending
|
||||
(->> pending
|
||||
(map :undo-changes)
|
||||
@@ -126,8 +128,8 @@
|
||||
|
||||
apply-changes
|
||||
(fn [fdata]
|
||||
(let [fdata (cpc/process-changes fdata undo-changes false)
|
||||
fdata (cpc/process-changes fdata redo-changes false)
|
||||
(let [fdata (cpc/process-changes fdata undo-changes false libraries)
|
||||
fdata (cpc/process-changes fdata redo-changes false libraries)
|
||||
pids (into #{} xf:map-page-id redo-changes)]
|
||||
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
||||
|
||||
@@ -200,7 +202,7 @@
|
||||
(let [pending (when-not local?
|
||||
(get-pending-commits state))]
|
||||
(rx/concat
|
||||
(rx/of (apply-changes-localy commit pending))
|
||||
(rx/of (apply-changes-locally commit pending))
|
||||
(if pending
|
||||
(rx/concat
|
||||
(->> (rx/from (reverse pending))
|
||||
|
||||
@@ -703,9 +703,9 @@
|
||||
|
||||
#_:clj-kondo/ignore
|
||||
(defn set-wasm-modifiers
|
||||
[modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis
|
||||
[modif-tree & {:keys [ignore-constraints ignore-snap-pixel
|
||||
subtree-ids-by-id selection-rect-cache]
|
||||
:or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil}
|
||||
:or {ignore-constraints false ignore-snap-pixel false}
|
||||
:as params}]
|
||||
(let [modif-tree (without-nil-ids modif-tree)]
|
||||
(ptk/reify ::set-wasm-modifiers
|
||||
@@ -756,7 +756,7 @@
|
||||
root-modifiers
|
||||
|
||||
:else
|
||||
(let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel? snap-ignore-axis)]
|
||||
(let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel?)]
|
||||
(if (seq propagated) propagated root-modifiers)))]
|
||||
(when wasm-ready?
|
||||
(wasm.api/set-modifiers modifiers))
|
||||
@@ -831,8 +831,10 @@
|
||||
;; primaries and descendants would snap back to their
|
||||
;; pre-drag positions on drop.
|
||||
;;
|
||||
;; Only without `snap-pixel?`: the delta that lands
|
||||
;; the shape on the pixel grid is known to WASM alone.
|
||||
;; Skipped when `snap-pixel?` is on: WASM applies
|
||||
;; per-shape pixel correction (different scale/translate
|
||||
;; per descendant) which we can't replicate cheaply on
|
||||
;; the CLJS side.
|
||||
(reduce
|
||||
(fn [acc [id data]]
|
||||
(let [t (:transform data)
|
||||
@@ -862,7 +864,7 @@
|
||||
geometry-entries))
|
||||
|
||||
:else
|
||||
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel? snap-ignore-axis)))
|
||||
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)))
|
||||
|
||||
ignore-tree
|
||||
(calculate-ignore-tree-wasm transforms objects)
|
||||
|
||||
@@ -193,7 +193,8 @@
|
||||
layout-initializer (get-layout-initializer type from-frame? calculate-params?)]
|
||||
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes [id] layout-initializer {:with-objects? true})
|
||||
(dwsh/update-shapes [id] layout-initializer {:with-objects? true
|
||||
:extra-context (str "create-layout-from-id: " id)})
|
||||
(dwsh/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v))
|
||||
(ptk/data-event :layout/update {:ids [id]})
|
||||
(dwu/commit-undo-transaction undo-id))))))
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
{:as props
|
||||
:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
|
||||
ignore-touched undo-group with-objects? changed-sub-attr translation?
|
||||
skip-component-sync?]
|
||||
skip-component-sync? extra-context]
|
||||
:or {reg-objects? false
|
||||
save-undo? true
|
||||
stack-undo? false
|
||||
@@ -222,7 +222,8 @@
|
||||
:ignore-tree ignore-tree
|
||||
:ignore-touched ignore-touched
|
||||
:with-objects? with-objects?
|
||||
:translation? translation?})
|
||||
:translation? translation?
|
||||
:extra-context extra-context})
|
||||
(cond-> undo-group
|
||||
(pcb/set-undo-group undo-group))
|
||||
(pcb/set-translation? translation?)
|
||||
|
||||
@@ -592,14 +592,12 @@
|
||||
(rx/merge
|
||||
(->> angle-stream
|
||||
(rx/sample mconst/rotation-sample-time)
|
||||
(rx/map #(dwm/set-wasm-modifiers (rotation-modifiers % shapes group-center)
|
||||
:ignore-snap-pixel true))
|
||||
(rx/map #(dwm/set-wasm-modifiers (rotation-modifiers % shapes group-center)))
|
||||
(rx/take-until stopper))
|
||||
(->> angle-stream
|
||||
(rx/take-until stopper)
|
||||
(rx/last)
|
||||
(rx/map #(dwm/apply-wasm-modifiers (rotation-modifiers % shapes group-center)
|
||||
:ignore-snap-pixel true))))
|
||||
(rx/map #(dwm/apply-wasm-modifiers (rotation-modifiers % shapes group-center)))))
|
||||
|
||||
(rx/of (finish-transform)))
|
||||
|
||||
@@ -640,9 +638,7 @@
|
||||
modif-tree
|
||||
(dwm/build-modif-tree ids objects get-modifier)]
|
||||
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree
|
||||
:ignore-touched (:ignore-touched options)
|
||||
:ignore-snap-pixel true)))
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree :ignore-touched (:ignore-touched options))))
|
||||
|
||||
(let [page-id (or (:page-id options)
|
||||
(:current-page-id state))
|
||||
|
||||
@@ -764,7 +764,7 @@
|
||||
(remove #(= (:id %) component-id))
|
||||
(filter #(= (dm/get-in % [:variant-properties pos :value]) val))
|
||||
(reverse))
|
||||
nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)
|
||||
nearest-comp (apply min-key #(ctv/properties-distance target-props (:variant-properties %)) valid-comps)
|
||||
shape-parents (cfh/get-parents-with-self current-page-objects (:parent-id shape))
|
||||
nearest-comp-children (cfh/get-children-with-self component-page-objects (:main-instance-id nearest-comp))
|
||||
comps-nesting-loop? (seq? (cfh/components-nesting-loop? nearest-comp-children shape-parents))
|
||||
|
||||
@@ -2098,34 +2098,13 @@
|
||||
|
||||
(h/call wasm/internal-module "_set_structure_modifiers"))))
|
||||
|
||||
;; Axes the pixel grid rounds, as `propagate_modifiers` expects them.
|
||||
(def ^:private pixel-precision
|
||||
{:disabled 0
|
||||
:both 1
|
||||
:only-x 2
|
||||
:only-y 3})
|
||||
|
||||
(defn- pixel-precision-mode
|
||||
"Encodes the pixel grid snapping for the renderer. `snap-ignore-axis`
|
||||
names the axis to leave alone (`:x`, `:y` or nil)."
|
||||
[snap-pixel? snap-ignore-axis]
|
||||
(pixel-precision
|
||||
(cond
|
||||
(not snap-pixel?) :disabled
|
||||
(= :x snap-ignore-axis) :only-y
|
||||
(= :y snap-ignore-axis) :only-x
|
||||
:else :both)))
|
||||
|
||||
(defn propagate-modifiers
|
||||
"Propagates geometry modifiers through the WASM shape tree.
|
||||
|
||||
Rounds the resulting geometry to the pixel grid when `snap-pixel?` is set,
|
||||
skipping the axis named by `snap-ignore-axis` (`:x`, `:y` or nil).
|
||||
|
||||
Always returns a vector. When the context is not ready (lost / mid-reload)
|
||||
or `entries` is empty, returns `[]` so callers never receive `nil` (which
|
||||
would trip `set-modifiers`' vector assert)."
|
||||
[entries snap-pixel? snap-ignore-axis]
|
||||
[entries pixel-precision]
|
||||
(if-not (and (initialized?) (not ^boolean (empty? entries)))
|
||||
[]
|
||||
(let [heapf32 (mem/get-heap-f32)
|
||||
@@ -2143,8 +2122,7 @@
|
||||
offset
|
||||
entries)
|
||||
|
||||
(let [precision (pixel-precision-mode snap-pixel? snap-ignore-axis)
|
||||
offset (-> (h/call wasm/internal-module "_propagate_modifiers" precision)
|
||||
(let [offset (-> (h/call wasm/internal-module "_propagate_modifiers" pixel-precision)
|
||||
(mem/->offset-32))
|
||||
length (aget heapu32 offset)
|
||||
max-offset (+ offset 1 (* length MODIFIER-U32-SIZE))
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
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 _snap-pixel? _snap-ignore-axis]
|
||||
[entries _pixel-precision]
|
||||
(track! :propagate-modifiers)
|
||||
(when (d/not-empty? entries)
|
||||
(into []
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
the real implementations."
|
||||
[]
|
||||
(set! wasm.api/propagate-modifiers
|
||||
(fn [entries _snap-pixel? _snap-ignore-axis]
|
||||
(fn [entries _pixel-precision]
|
||||
(swap! captured-geometry-entries into entries)
|
||||
(into []
|
||||
(map (fn [[id data]] [id (:transform data)]))
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.logic.wasm-pixel-snap-test
|
||||
"Covers which pixel-grid snapping options reach the WASM renderer.
|
||||
|
||||
The rounding happens in Rust, so these tests assert on the arguments
|
||||
crossing the bridge: which axis an axis-locked drag leaves alone, and
|
||||
that rotation does not snap."
|
||||
(:require
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.test-helpers.compositions :as ctho]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]
|
||||
[frontend-tests.helpers.wasm :as thw]))
|
||||
|
||||
(def ^:private captured-snap-options
|
||||
"One entry per `wasm.api/propagate-modifiers` call during a test."
|
||||
(atom []))
|
||||
|
||||
(defn- install-capturing-spy!
|
||||
"Records the snap options of every propagation. Must run after
|
||||
`thw/setup-wasm-mocks!` so teardown restores the real implementation."
|
||||
[]
|
||||
(set! wasm.api/propagate-modifiers
|
||||
(fn [entries snap-pixel? snap-ignore-axis]
|
||||
(swap! captured-snap-options conj
|
||||
{:snap-pixel? snap-pixel? :snap-ignore-axis snap-ignore-axis})
|
||||
(into []
|
||||
(map (fn [[id data]] [id (:transform data)]))
|
||||
entries))))
|
||||
|
||||
(defn- enable-pixel-grid
|
||||
[]
|
||||
(fn [state]
|
||||
(update state :workspace-layout conj :snap-pixel-grid)))
|
||||
|
||||
(t/use-fixtures :each
|
||||
{:before (fn []
|
||||
(cthi/reset-idmap!)
|
||||
(reset! captured-snap-options [])
|
||||
(thw/setup-wasm-mocks!)
|
||||
(install-capturing-spy!))
|
||||
:after (fn []
|
||||
(thw/teardown-wasm-mocks!))})
|
||||
|
||||
(t/deftest axis-locked-move-tells-the-renderer-which-axis-to-ignore
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1 :x 10.4 :y 20.6 :width 100.5 :height 50.3))
|
||||
store (ths/setup-store file)
|
||||
rect (cths/get-shape file :rect1)
|
||||
modif-tree (dwm/create-modif-tree [(:id rect)]
|
||||
(ctm/move-modifiers (gpt/point 5.2 0)))
|
||||
events [(enable-pixel-grid)
|
||||
(dwm/apply-wasm-modifiers modif-tree :snap-ignore-axis :y)]]
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [_new-state]
|
||||
(t/is (= [{:snap-pixel? true :snap-ignore-axis :y}]
|
||||
@captured-snap-options)))))))
|
||||
|
||||
(t/deftest move-without-axis-lock-snaps-both-axes
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1 :x 10.4 :y 20.6 :width 100.5 :height 50.3))
|
||||
store (ths/setup-store file)
|
||||
rect (cths/get-shape file :rect1)
|
||||
modif-tree (dwm/create-modif-tree [(:id rect)]
|
||||
(ctm/move-modifiers (gpt/point 5.2 3.7)))
|
||||
events [(enable-pixel-grid)
|
||||
(dwm/apply-wasm-modifiers modif-tree)]]
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [_new-state]
|
||||
(t/is (= [{:snap-pixel? true :snap-ignore-axis nil}]
|
||||
@captured-snap-options)))))))
|
||||
|
||||
(t/deftest rotation-does-not-snap-to-the-pixel-grid
|
||||
(t/async
|
||||
done
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-rect :rect1 :x 10.4 :y 20.6 :width 100.5 :height 50.3))
|
||||
store (ths/setup-store file)
|
||||
rect (cths/get-shape file :rect1)
|
||||
events [(enable-pixel-grid)
|
||||
(dwt/increase-rotation #{(:id rect)} 15)]]
|
||||
(ths/run-store
|
||||
store done events
|
||||
(fn [_new-state]
|
||||
(t/is (= [{:snap-pixel? false :snap-ignore-axis nil}]
|
||||
@captured-snap-options)))))))
|
||||
@@ -45,7 +45,6 @@
|
||||
[frontend-tests.logic.sidebar-transform-coalescing-test]
|
||||
[frontend-tests.logic.update-position-test]
|
||||
[frontend-tests.logic.wasm-modifiers-nil-id-test]
|
||||
[frontend-tests.logic.wasm-pixel-snap-test]
|
||||
[frontend-tests.main-errors-test]
|
||||
[frontend-tests.plugins.comments-test]
|
||||
[frontend-tests.plugins.context-shapes-test]
|
||||
@@ -153,7 +152,6 @@
|
||||
'frontend-tests.logic.sidebar-transform-coalescing-test
|
||||
'frontend-tests.logic.update-position-test
|
||||
'frontend-tests.logic.wasm-modifiers-nil-id-test
|
||||
'frontend-tests.logic.wasm-pixel-snap-test
|
||||
'frontend-tests.plugins.comments-test
|
||||
'frontend-tests.plugins.context-shapes-test
|
||||
'frontend-tests.plugins.file-test
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
- **plugins-runtime**: `Library.createComponent()` now rejects invalid input (an empty shape list, or a shape inside a component copy) with a validation error instead of returning a component proxy pointing at nothing.
|
||||
- **plugins-runtime**: Setting an individual padding/margin side (`leftPadding`, `topMargin`, …) now re-derives the padding/margin type, switching to `multiple` when the four sides stop being symmetric (so the value is actually painted) and back to `simple` once top/bottom and left/right are mirrored again.
|
||||
- **plugins-runtime**: Removed the premature deep-hardening of the host plugin context, which froze shared host functions (including `Function.prototype`) before SES override taming, causing `TypeError: Cannot assign to read only property 'toString'` on later host-side function extension. Related to #11001.
|
||||
|
||||
## 1.5.0 (2026-07-08)
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { describe, it, vi, expect, beforeEach } from 'vitest';
|
||||
import { loadPlugin, setContextBuilder, getPlugins } from './load-plugin';
|
||||
import { createPlugin } from './create-plugin';
|
||||
import { ses } from './ses.js';
|
||||
import type { Context } from '@penpot/plugin-types';
|
||||
import type { Manifest } from './models/manifest.model.js';
|
||||
|
||||
vi.mock('./create-plugin', () => ({
|
||||
createPlugin: vi.fn(),
|
||||
}));
|
||||
|
||||
// NOTE: `./ses.js` is intentionally NOT mocked here: the test spies on the
|
||||
// real `ses.harden` to assert that `loadPlugin` never hardens the host
|
||||
// context.
|
||||
|
||||
describe('loadPlugin host context boundary (regression for #11001)', () => {
|
||||
let manifest: Manifest;
|
||||
|
||||
beforeEach(() => {
|
||||
manifest = {
|
||||
pluginId: 'test-plugin',
|
||||
name: 'Test Plugin',
|
||||
host: '',
|
||||
code: '',
|
||||
permissions: ['content:read'],
|
||||
};
|
||||
|
||||
vi.mocked(createPlugin).mockResolvedValue({
|
||||
plugin: {
|
||||
close: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
},
|
||||
} as unknown as Awaited<ReturnType<typeof createPlugin>>);
|
||||
});
|
||||
|
||||
it('does not freeze host-owned functions reachable through the context', async () => {
|
||||
const hostListener = function hostListener() {
|
||||
return 'host-value';
|
||||
};
|
||||
const nestedHostObject = {
|
||||
nestedFn() {
|
||||
return 'nested';
|
||||
},
|
||||
};
|
||||
const hostContext = {
|
||||
addListener: hostListener,
|
||||
nested: nestedHostObject,
|
||||
} as unknown as Context;
|
||||
|
||||
setContextBuilder(() => hostContext);
|
||||
|
||||
const hardenSpy = vi.spyOn(ses, 'harden');
|
||||
|
||||
await loadPlugin(manifest);
|
||||
|
||||
// The host context itself must be passed through untouched so the host
|
||||
// can keep modifying its own runtime objects (e.g. on page navigation).
|
||||
expect(createPlugin).toHaveBeenCalledWith(
|
||||
hostContext,
|
||||
manifest,
|
||||
expect.any(Function),
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Host-owned functions must remain extensible: page navigation and
|
||||
// runtime code may patch/augment them (e.g. assigning `toString` on a
|
||||
// wrapped listener). A deep `ses.harden(context)` here would freeze
|
||||
// them and turn such later assignments into
|
||||
// `TypeError: Cannot assign to read only property 'toString'`.
|
||||
expect(Object.isFrozen(hostListener)).toBe(false);
|
||||
expect(Object.isExtensible(hostListener)).toBe(true);
|
||||
expect(Object.isFrozen(nestedHostObject)).toBe(false);
|
||||
expect(() => {
|
||||
hostListener.toString = () => 'patched-by-host';
|
||||
}).not.toThrow();
|
||||
|
||||
expect(hardenSpy).not.toHaveBeenCalled();
|
||||
expect(getPlugins()).toHaveLength(1);
|
||||
|
||||
hardenSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,169 +0,0 @@
|
||||
import { describe, it, vi, expect, beforeAll } from 'vitest';
|
||||
import 'ses';
|
||||
import { loadPlugin, setContextBuilder, getPlugins } from './load-plugin';
|
||||
import type { Context } from '@penpot/plugin-types';
|
||||
import type { Manifest } from './models/manifest.model.js';
|
||||
|
||||
// Real initialization-path regression tests for #11001.
|
||||
//
|
||||
// NOTE: `./create-plugin`, `./plugin-manager` and
|
||||
// `./create-sandbox` are intentionally NOT mocked here. This spec exercises
|
||||
// the real `loadPlugin → createPlugin → createPluginManager → createSandbox`
|
||||
// path with the real SES implementation, mirroring the production
|
||||
// initialization order from `plugins-runtime/src/index.ts`:
|
||||
// repairIntrinsics (module load) → loadPlugin → createSandbox/hardenIntrinsics
|
||||
//
|
||||
// `hardenIntrinsics()` is deliberately NOT called up front: the first test
|
||||
// must run in the production window where only `repairIntrinsics` has run.
|
||||
// Tests run in declaration order; the later tests build on the locked-down
|
||||
// state the first real `loadPlugin` leaves behind (via `createSandbox`).
|
||||
//
|
||||
// Note on SES isolation: this suite depends on Vitest's default
|
||||
// file-level isolation. Each test file runs in a separate worker
|
||||
// process, so SES intrinsics frozen here do not leak into other
|
||||
// spec files.
|
||||
|
||||
const REPAIR_OPTIONS = {
|
||||
evalTaming: 'unsafeEval',
|
||||
stackFiltering: 'verbose',
|
||||
errorTaming: 'unsafe',
|
||||
consoleTaming: 'unsafe',
|
||||
errorTrapping: 'none',
|
||||
unhandledRejectionTrapping: 'none',
|
||||
};
|
||||
|
||||
function makeManifest(
|
||||
code: string,
|
||||
permissions: Manifest['permissions'],
|
||||
): Manifest {
|
||||
return {
|
||||
pluginId: 'test-plugin',
|
||||
name: 'Test Plugin',
|
||||
host: '',
|
||||
code,
|
||||
permissions,
|
||||
};
|
||||
}
|
||||
|
||||
function makeHostFixture() {
|
||||
const listenerTypes: string[] = [];
|
||||
const listeners = new Map<symbol, string>();
|
||||
// Inline code (empty host + non-URL code) resolves without network, so no
|
||||
// fetch mock is needed. UI/modal APIs are never touched by the probe code.
|
||||
const createRectangle = vi.fn(() => ({ type: 'rectangle-marker' }));
|
||||
const selection: object[] = [{ id: 'shape-1' }];
|
||||
const context = {
|
||||
addListener: (type: string, _callback: (...args: unknown[]) => unknown) => {
|
||||
const id = Symbol(type);
|
||||
listeners.set(id, type);
|
||||
listenerTypes.push(type);
|
||||
return id;
|
||||
},
|
||||
removeListener: (id: symbol) => {
|
||||
listeners.delete(id);
|
||||
},
|
||||
theme: 'dark',
|
||||
createRectangle,
|
||||
selection,
|
||||
// Host-only member: present on the raw context but NOT part of the
|
||||
// public penpot API. Plugin code must never see it (see B-2 below).
|
||||
__internalSecret: 'host-internal',
|
||||
} as unknown as Context;
|
||||
return { context, listenerTypes, createRectangle, selection };
|
||||
}
|
||||
|
||||
function lastCompartmentGlobalThis(): Record<string, unknown> {
|
||||
const plugins = getPlugins();
|
||||
const last = plugins[plugins.length - 1] as unknown as {
|
||||
compartment: { compartment: { globalThis: Record<string, unknown> } };
|
||||
};
|
||||
return last.compartment.compartment.globalThis;
|
||||
}
|
||||
|
||||
describe('loadPlugin real initialization path (regression for #11001)', () => {
|
||||
beforeAll(() => {
|
||||
// Production module-load step only: repairs intrinsics WITHOUT
|
||||
// installing override taming, exactly like `index.ts` at import time.
|
||||
(
|
||||
globalThis as unknown as { repairIntrinsics(opts: object): void }
|
||||
).repairIntrinsics({ ...REPAIR_OPTIONS });
|
||||
});
|
||||
|
||||
it('loads through the real path and keeps host function augmentation working', async () => {
|
||||
const fixture = makeHostFixture();
|
||||
setContextBuilder(() => fixture.context);
|
||||
|
||||
await loadPlugin(
|
||||
makeManifest('penpot.on("finish", function () {});', ['content:read']),
|
||||
);
|
||||
|
||||
// The plugin code really ran inside the sandbox: the manager registers
|
||||
// `themechange` + `finish`, and the plugin code adds its own `finish`
|
||||
// listener through the public API.
|
||||
expect(fixture.listenerTypes).toEqual(['themechange', 'finish', 'finish']);
|
||||
expect(getPlugins()).toHaveLength(1);
|
||||
|
||||
// The user-facing behavior from #11001: host-side augmentation of a
|
||||
// fresh function (e.g. assigning `toString` during page navigation)
|
||||
// succeeds after a real plugin load.
|
||||
const freshWrapper = function freshWrapper() {
|
||||
return 'navigation-wrapper';
|
||||
};
|
||||
expect(() => {
|
||||
freshWrapper.toString = () => 'patched-by-runtime';
|
||||
}).not.toThrow();
|
||||
expect(fixture.listenerTypes.length).toBe(3);
|
||||
});
|
||||
|
||||
it('denies the write API without permission and leaves the host untouched', async () => {
|
||||
const fixture = makeHostFixture();
|
||||
setContextBuilder(() => fixture.context);
|
||||
|
||||
await expect(
|
||||
loadPlugin(makeManifest('penpot.createRectangle();', ['content:read'])),
|
||||
).rejects.toThrow(/content:write/);
|
||||
expect(fixture.createRectangle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows the same write API with permission', async () => {
|
||||
const fixture = makeHostFixture();
|
||||
setContextBuilder(() => fixture.context);
|
||||
|
||||
await loadPlugin(
|
||||
makeManifest('penpot.createRectangle();', [
|
||||
'content:read',
|
||||
'content:write',
|
||||
]),
|
||||
);
|
||||
expect(fixture.createRectangle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not expose raw host-only context members to plugin code', async () => {
|
||||
const fixture = makeHostFixture();
|
||||
setContextBuilder(() => fixture.context);
|
||||
|
||||
await loadPlugin(
|
||||
makeManifest('globalThis.__probe = typeof penpot.__internalSecret;', [
|
||||
'content:read',
|
||||
]),
|
||||
);
|
||||
// The public `penpot` object is a boundary proxy over a curated API, not
|
||||
// the raw host context, so host-only members are invisible inside.
|
||||
expect(lastCompartmentGlobalThis()['__probe']).toBe('undefined');
|
||||
});
|
||||
|
||||
it('keeps safeReturn protection on returned values without blocking allowed edits', async () => {
|
||||
const fixture = makeHostFixture();
|
||||
setContextBuilder(() => fixture.context);
|
||||
|
||||
await loadPlugin(
|
||||
makeManifest(
|
||||
'penpot.createRectangle(); ' +
|
||||
'globalThis.__selectionFrozen = Object.isFrozen(penpot.selection);',
|
||||
['content:read', 'content:write'],
|
||||
),
|
||||
);
|
||||
expect(fixture.createRectangle).toHaveBeenCalledTimes(1);
|
||||
expect(lastCompartmentGlobalThis()['__selectionFrozen']).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import type { Context } from '@penpot/plugin-types';
|
||||
import { loadManifest } from './parse-manifest.js';
|
||||
import { Manifest } from './models/manifest.model.js';
|
||||
import { createPlugin } from './create-plugin.js';
|
||||
import { ses } from './ses.js';
|
||||
|
||||
let plugins: Awaited<ReturnType<typeof createPlugin>>[] = [];
|
||||
|
||||
@@ -53,23 +54,8 @@ export const loadPlugin = async function (
|
||||
|
||||
closeAllPlugins();
|
||||
|
||||
// The host context is not deeply frozen at this load stage.
|
||||
//
|
||||
// The context still contains host-internal function objects and shared
|
||||
// prototypes that the host may legitimately extend after plugin load
|
||||
// (for example, by assigning custom properties). Deep-freezing here
|
||||
// would freeze those prototypes before SES override taming completes,
|
||||
// preventing later host-side mutations with a "Cannot assign to read
|
||||
// only property" TypeError.
|
||||
//
|
||||
// Responsibility boundary: this function forwards the context to the
|
||||
// sandbox layer without deep-freezing it. The public API that plugins
|
||||
// consume is constructed by the API module (`api/index.ts`), and
|
||||
// `createSandbox`'s proxy handler applies `ses.safeReturn` to values
|
||||
// crossing into the sandbox. Compartment isolation and intrinsics
|
||||
// hardening are performed by createSandbox, not here.
|
||||
const plugin = await createPlugin(
|
||||
context,
|
||||
ses.harden(context) as Context,
|
||||
manifest,
|
||||
() => {
|
||||
plugins = plugins.filter((api) => api !== plugin);
|
||||
|
||||
@@ -12,8 +12,8 @@ use common::GetBounds;
|
||||
use crate::error::Result;
|
||||
use crate::shapes;
|
||||
use crate::shapes::{
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, PixelPrecision, Shape,
|
||||
TransformEntry, TransformEntrySource, Type,
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
|
||||
TransformEntrySource, Type,
|
||||
};
|
||||
use crate::state::{ShapesPoolRef, State};
|
||||
use crate::uuid::Uuid;
|
||||
@@ -139,84 +139,36 @@ fn calculate_bool_bounds(
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Which parts of the geometry a pixel-grid correction rounds: only the ones
|
||||
/// the transform changes, so a move keeps its dimensions and a resize keeps
|
||||
/// its anchored corner.
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
struct SnapGeometry {
|
||||
x: bool,
|
||||
y: bool,
|
||||
width: bool,
|
||||
height: bool,
|
||||
}
|
||||
fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) {
|
||||
let tr = bounds.transform_matrix().unwrap_or_default();
|
||||
let tr_inv = tr.invert().unwrap_or_default();
|
||||
|
||||
impl SnapGeometry {
|
||||
/// Flags the properties that differ between the two bounds. The axis mask
|
||||
/// in `precision` applies to the position only.
|
||||
fn new(before: &Bounds, after: &Bounds, precision: PixelPrecision) -> Self {
|
||||
SnapGeometry {
|
||||
x: precision.rounds_x() && !is_close_to(before.min_x(), after.min_x()),
|
||||
y: precision.rounds_y() && !is_close_to(before.min_y(), after.min_y()),
|
||||
width: !is_close_to(before.width(), after.width()),
|
||||
height: !is_close_to(before.height(), after.height()),
|
||||
}
|
||||
}
|
||||
let x = bounds.min_x().round();
|
||||
let y = bounds.min_y().round();
|
||||
|
||||
fn resized(&self) -> bool {
|
||||
self.width || self.height
|
||||
}
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
|
||||
fn any(&self) -> bool {
|
||||
self.x || self.y || self.resized()
|
||||
}
|
||||
}
|
||||
let target_width = bounds.width().round();
|
||||
let target_height = bounds.height().round();
|
||||
|
||||
/// Rounds a transform so the parts of the shape the gesture changed land on
|
||||
/// the pixel grid, leaving everything else exactly where it is.
|
||||
fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds, snap: SnapGeometry) {
|
||||
// Target corner, taken before the size correction: that correction scales
|
||||
// about the bounds center, and the translation below undoes the corner
|
||||
// displacement it causes. An unsnapped axis targets its own value.
|
||||
let x = if snap.x {
|
||||
bounds.min_x().round()
|
||||
let scale_width = if width > 0.1 {
|
||||
f32::max(0.01, target_width / width)
|
||||
} else {
|
||||
bounds.min_x()
|
||||
1.0
|
||||
};
|
||||
let y = if snap.y {
|
||||
bounds.min_y().round()
|
||||
let scale_height = if height > 0.1 {
|
||||
f32::max(0.01, target_height / height)
|
||||
} else {
|
||||
bounds.min_y()
|
||||
1.0
|
||||
};
|
||||
|
||||
if snap.resized() {
|
||||
let tr = bounds.transform_matrix().unwrap_or_default();
|
||||
let tr_inv = tr.invert().unwrap_or_default();
|
||||
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
|
||||
// A rounded dimension is never smaller than one pixel.
|
||||
let target_width = f32::max(1.0, width.round());
|
||||
let target_height = f32::max(1.0, height.round());
|
||||
|
||||
let scale_width = if snap.width && width > 0.1 {
|
||||
f32::max(0.01, target_width / width)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let scale_height = if snap.height && height > 0.1 {
|
||||
f32::max(0.01, target_height / height)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
if f32::is_finite(scale_width) && f32::is_finite(scale_height) {
|
||||
let mut round_transform = Matrix::scale((scale_width, scale_height));
|
||||
round_transform.post_concat(&tr);
|
||||
round_transform.pre_concat(&tr_inv);
|
||||
transform.post_concat(&round_transform);
|
||||
bounds.transform_mut(&round_transform);
|
||||
}
|
||||
if f32::is_finite(scale_width) && f32::is_finite(scale_height) {
|
||||
let mut round_transform = Matrix::scale((scale_width, scale_height));
|
||||
round_transform.post_concat(&tr);
|
||||
round_transform.pre_concat(&tr_inv);
|
||||
transform.post_concat(&round_transform);
|
||||
bounds.transform_mut(&round_transform);
|
||||
}
|
||||
|
||||
let dx = x - bounds.min_x();
|
||||
@@ -232,7 +184,7 @@ fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds, snap: SnapGe
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn propagate_transform(
|
||||
entry: TransformEntry,
|
||||
pixel_precision: PixelPrecision,
|
||||
pixel_precision: bool,
|
||||
state: &State,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
@@ -334,11 +286,8 @@ fn propagate_transform(
|
||||
}
|
||||
}
|
||||
|
||||
if pixel_precision.enabled() {
|
||||
let snap = SnapGeometry::new(&shape_bounds_before, &shape_bounds_after, pixel_precision);
|
||||
if snap.any() {
|
||||
set_pixel_precision(&mut transform, &mut shape_bounds_after, snap);
|
||||
}
|
||||
if pixel_precision {
|
||||
set_pixel_precision(&mut transform, &mut shape_bounds_after);
|
||||
}
|
||||
|
||||
if entry.propagate {
|
||||
@@ -468,15 +417,10 @@ fn reflow_shape(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Propagates a set of transforms through the shape tree, returning one
|
||||
/// transform per affected shape.
|
||||
///
|
||||
/// The transforms are relative to the committed geometry, so callers clear
|
||||
/// any transform modifier of their own before propagating.
|
||||
pub fn propagate_modifiers(
|
||||
state: &State,
|
||||
modifiers: &[TransformEntry],
|
||||
pixel_precision: PixelPrecision,
|
||||
pixel_precision: bool,
|
||||
) -> Result<Vec<TransformEntry>> {
|
||||
let mut entries: VecDeque<_> = modifiers
|
||||
.iter()
|
||||
@@ -623,249 +567,6 @@ mod tests {
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_move_keeps_size() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::translate((5.2, 3.7)));
|
||||
let mut transform = Matrix::translate((5.2, 3.7));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::Both);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.width(), 100.5));
|
||||
assert!(is_close_to(bounds_after.height(), 50.3));
|
||||
assert!(is_close_to(bounds_after.min_x(), 16.0));
|
||||
assert!(is_close_to(bounds_after.min_y(), 24.0));
|
||||
assert!(math::is_move_only_matrix(&transform));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_resize_rounds_size() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::scale((1.1, 1.1)));
|
||||
let mut transform = Matrix::scale((1.1, 1.1));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::Both);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(
|
||||
bounds_after.width(),
|
||||
bounds_after.width().round()
|
||||
));
|
||||
assert!(is_close_to(
|
||||
bounds_after.height(),
|
||||
bounds_after.height().round()
|
||||
));
|
||||
assert!(is_close_to(
|
||||
bounds_after.min_x(),
|
||||
bounds_after.min_x().round()
|
||||
));
|
||||
assert!(is_close_to(
|
||||
bounds_after.min_y(),
|
||||
bounds_after.min_y().round()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_move_only_rounds_position() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.9, 70.9);
|
||||
}
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, Matrix::translate((5.2, 3.7)));
|
||||
let result = propagate_modifiers(&state, &[entry], PixelPrecision::Both).unwrap();
|
||||
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let shape = state.shapes.get(&shape_id).unwrap();
|
||||
let bounds = shape.bounds().transform(&transform);
|
||||
|
||||
assert!(is_close_to(bounds.width(), 100.5));
|
||||
assert!(is_close_to(bounds.height(), 50.3));
|
||||
assert!(is_close_to(bounds.min_x(), 16.0));
|
||||
assert!(is_close_to(bounds.min_y(), 24.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_resize_keeps_anchored_corner() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.4, 70.6);
|
||||
}
|
||||
|
||||
// Drag the bottom-right corner in small steps: the top-left corner
|
||||
// stays put on every step.
|
||||
for step in 1..40 {
|
||||
let delta = step as f32 * 0.05;
|
||||
let mut resize = Matrix::scale(((100.0 + delta) / 100.0, (50.0 + delta) / 50.0));
|
||||
resize.post_translate(Point::new(10.4, 20.6));
|
||||
resize.pre_translate(Point::new(-10.4, -20.6));
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, resize);
|
||||
let result = propagate_modifiers(&state, &[entry], PixelPrecision::Both).unwrap();
|
||||
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let shape = state.shapes.get(&shape_id).unwrap();
|
||||
let bounds = shape.bounds().transform(&transform);
|
||||
|
||||
assert!(
|
||||
is_close_to(bounds.min_x(), 10.4) && is_close_to(bounds.min_y(), 20.6),
|
||||
"corner moved to ({}, {}) at delta {}",
|
||||
bounds.min_x(),
|
||||
bounds.min_y(),
|
||||
delta
|
||||
);
|
||||
assert!(is_close_to(bounds.width(), bounds.width().round()));
|
||||
assert!(is_close_to(bounds.height(), bounds.height().round()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_only_x_leaves_y_untouched() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::translate((5.2, 0.0)));
|
||||
let mut transform = Matrix::translate((5.2, 0.0));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::OnlyX);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.min_x(), 16.0));
|
||||
assert!(is_close_to(bounds_after.min_y(), 20.6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_only_y_leaves_x_untouched() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.4, 20.6, 100.5, 50.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::translate((0.0, 3.7)));
|
||||
let mut transform = Matrix::translate((0.0, 3.7));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::OnlyY);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.min_x(), 10.4));
|
||||
assert!(is_close_to(bounds_after.min_y(), 24.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pixel_precision_resize_never_rounds_below_one_pixel() {
|
||||
let bounds = Bounds::from_rect(&math::Rect::from_xywh(10.0, 20.0, 0.4, 0.3));
|
||||
let mut bounds_after = bounds.transform(&Matrix::scale((1.5, 1.5)));
|
||||
let mut transform = Matrix::scale((1.5, 1.5));
|
||||
|
||||
let snap = SnapGeometry::new(&bounds, &bounds_after, PixelPrecision::Both);
|
||||
set_pixel_precision(&mut transform, &mut bounds_after, snap);
|
||||
|
||||
assert!(is_close_to(bounds_after.width(), 1.0));
|
||||
assert!(is_close_to(bounds_after.height(), 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_snaps_every_frame_of_a_gesture() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.9, 70.9);
|
||||
}
|
||||
|
||||
// One frame of a drag, as the entry point runs it: clear the
|
||||
// modifiers, propagate the delta accumulated since the gesture
|
||||
// started, then push the result back as the active modifier, which is
|
||||
// what the renderer draws.
|
||||
let frame = |state: &mut State, delta: f32| {
|
||||
state.shapes.clear_transform_modifiers();
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, Matrix::translate((delta, delta)));
|
||||
let result = propagate_modifiers(state, &[entry], PixelPrecision::Both).unwrap();
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let bounds = state
|
||||
.shapes
|
||||
.get_raw(&shape_id)
|
||||
.unwrap()
|
||||
.bounds()
|
||||
.transform(&transform);
|
||||
|
||||
state.set_modifiers(HashMap::from([(shape_id, transform)]));
|
||||
bounds
|
||||
};
|
||||
|
||||
// Every frame lands on the pixel grid and keeps the size.
|
||||
for step in 1..40 {
|
||||
let bounds = frame(&mut state, step as f32 * 0.35);
|
||||
|
||||
assert!(
|
||||
is_close_to(bounds.min_x(), bounds.min_x().round())
|
||||
&& is_close_to(bounds.min_y(), bounds.min_y().round()),
|
||||
"shape landed off the pixel grid at ({}, {}) on frame {}",
|
||||
bounds.min_x(),
|
||||
bounds.min_y(),
|
||||
step
|
||||
);
|
||||
assert!(is_close_to(bounds.width(), 100.5));
|
||||
assert!(is_close_to(bounds.height(), 50.3));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_propagate_pixel_precision_resize_only_rounds_the_changed_dimension() {
|
||||
let shape_id = Uuid::new_v4();
|
||||
let mut state = State::new();
|
||||
state.shapes.initialize(10);
|
||||
{
|
||||
let shape = state.shapes.add_shape(shape_id);
|
||||
shape.set_selrect(10.4, 20.6, 110.9, 70.9);
|
||||
}
|
||||
|
||||
// Drag the right edge: the width lands on the grid, the height and
|
||||
// the top-left corner stay put.
|
||||
let mut resize = Matrix::scale((103.3 / 100.5, 1.0));
|
||||
resize.post_translate(Point::new(10.4, 20.6));
|
||||
resize.pre_translate(Point::new(-10.4, -20.6));
|
||||
|
||||
let entry = TransformEntry::from_input(shape_id, resize);
|
||||
let result = propagate_modifiers(&state, &[entry], PixelPrecision::Both).unwrap();
|
||||
|
||||
let transform = result
|
||||
.iter()
|
||||
.find(|entry| entry.id == shape_id)
|
||||
.map(|entry| entry.transform)
|
||||
.unwrap();
|
||||
|
||||
let bounds = state
|
||||
.shapes
|
||||
.get_raw(&shape_id)
|
||||
.unwrap()
|
||||
.bounds()
|
||||
.transform(&transform);
|
||||
|
||||
assert!(is_close_to(bounds.width(), 103.0));
|
||||
assert!(is_close_to(bounds.height(), 50.3));
|
||||
assert!(is_close_to(bounds.min_x(), 10.4));
|
||||
assert!(is_close_to(bounds.min_y(), 20.6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_bounds() {
|
||||
let parent_id = Uuid::new_v4();
|
||||
|
||||
@@ -5,59 +5,18 @@ use crate::utils::{uuid_from_u32_quartet, uuid_to_u32_quartet};
|
||||
use crate::uuid::Uuid;
|
||||
use skia::Matrix;
|
||||
|
||||
/// Axes the pixel grid rounds. An axis-locked drag rounds only the axis it
|
||||
/// moves along.
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum PixelPrecision {
|
||||
Disabled,
|
||||
Both,
|
||||
OnlyX,
|
||||
OnlyY,
|
||||
}
|
||||
|
||||
impl PixelPrecision {
|
||||
pub fn enabled(&self) -> bool {
|
||||
*self != PixelPrecision::Disabled
|
||||
}
|
||||
|
||||
pub fn rounds_x(&self) -> bool {
|
||||
matches!(self, PixelPrecision::Both | PixelPrecision::OnlyX)
|
||||
}
|
||||
|
||||
pub fn rounds_y(&self) -> bool {
|
||||
matches!(self, PixelPrecision::Both | PixelPrecision::OnlyY)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PixelPrecision {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => PixelPrecision::Both,
|
||||
2 => PixelPrecision::OnlyX,
|
||||
3 => PixelPrecision::OnlyY,
|
||||
_ => PixelPrecision::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum Modifier {
|
||||
Transform(TransformEntry, PixelPrecision),
|
||||
Transform(TransformEntry, bool),
|
||||
Reflow(Uuid, bool),
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
pub fn transform_propagate(id: Uuid, transform: Matrix) -> Self {
|
||||
Modifier::Transform(
|
||||
TransformEntry::from_propagate(id, transform),
|
||||
PixelPrecision::Disabled,
|
||||
)
|
||||
Modifier::Transform(TransformEntry::from_propagate(id, transform), false)
|
||||
}
|
||||
pub fn parent(id: Uuid, transform: Matrix) -> Self {
|
||||
Modifier::Transform(
|
||||
TransformEntry::parent(id, transform),
|
||||
PixelPrecision::Disabled,
|
||||
)
|
||||
Modifier::Transform(TransformEntry::parent(id, transform), false)
|
||||
}
|
||||
pub fn reflow(id: Uuid, force_reflow: bool) -> Self {
|
||||
Modifier::Reflow(id, force_reflow)
|
||||
|
||||
@@ -403,20 +403,6 @@ impl ShapesPoolImpl {
|
||||
/// gone, but if we don't touch their tiles they keep pointing at the
|
||||
/// previous modified position and the tile texture cache may serve stale
|
||||
/// pixels.
|
||||
/// Drops the transform modifiers, keeping structure and scale-content
|
||||
/// entries, so the pool serves committed geometry again. Called before
|
||||
/// propagating a new set of transforms, which are relative to that
|
||||
/// geometry.
|
||||
pub fn clear_transform_modifiers(&mut self) {
|
||||
if self.modifiers.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.clean_shape_cache();
|
||||
self.modifiers = HashMap::default();
|
||||
self.modifier_uuids.clear();
|
||||
}
|
||||
|
||||
pub fn clean_all(&mut self) -> Vec<Uuid> {
|
||||
self.clean_shape_cache();
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ impl From<RawTransformEntry> for TransformEntry {
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn propagate_modifiers(pixel_precision: u8) -> Result<*mut u8> {
|
||||
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<TransformEntry> = bytes
|
||||
@@ -85,8 +85,7 @@ pub extern "C" fn propagate_modifiers(pixel_precision: u8) -> Result<*mut u8> {
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
with_state!(state, {
|
||||
state.shapes.clear_transform_modifiers();
|
||||
let result = shapes::propagate_modifiers(state, &entries, pixel_precision.into())?;
|
||||
let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?;
|
||||
Ok(mem::write_vec(result))
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user