mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 22:10:34 +00:00
♻️ Move the placement step of a transform to common
Which shapes of a copy a transform leaves untouched (the ignore tree) was frontend code, written twice: calculate-ignore-tree for the modifiers of the SVG renderer and calculate-ignore-tree-wasm for the matrices of the WASM renderer. The second is the single-pass version of the first: each shape still compares itself with the copy root of the latest transformed shape above it, as the repeated walks did when the last one won. app.common.logic.placement now holds it once: calculate-ignore-tree takes the transformed ids in order and a function that transforms a shape, built with modifiers-transform (SVG) or matrices-transform (WASM), both cached per shape. check-delta and transform-attrs moved with it. The frontend calls the common version, and its tests moved to common-tests.logic.placement-test, which runs on the JVM and in JS, with a rotation through modifiers added. No behavior change: the golden masters and the suites are unchanged. The benchmark does not run this code. AI-assisted-by: claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6cdb3bb922
commit
536949dd9d
@ -110,7 +110,7 @@ Two pure runners (no store, JVM + CLJS):
|
||||
(`:component-id-mismatch`), nor with `{:sync? false}`. Assembly ops (create/instantiate/nest/variant
|
||||
container): no sync, no undo group. `n/reset` (`reset-copy-instance` resets a lineage's latest copy) is a user op (sync +
|
||||
undo); the interpreter sends `dwl/reset-component`. `{:sync? false}` disables sync. `Rotate`/`ChangeHeight`/`MoveShape`
|
||||
are `:frontend-only` and throw `::runner/frontend-only` (the placement step is frontend code). Swap and switch nodes use the production
|
||||
are `:frontend-only` and throw `::runner/frontend-only` (their modifiers come from frontend events; the placement step itself is common, `app.common.logic.placement`). Swap and switch nodes use the production
|
||||
`clv/generate-component-swap-in-place` (+ `clv/find-switch-target` for switch), the same code the
|
||||
frontend events call. Not modelled: the `:modified-at` bump, and the swap/switch event follow-ups
|
||||
(token propagation, WASM text resize, layout update). Tests:
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
## Component-copy touched suppression
|
||||
|
||||
- `calculate-ignore-tree` walks modified shapes and descendants to decide per copy-shape `ignore-geometry?`.
|
||||
- `clp/calculate-ignore-tree` (common, `app.common.logic.placement`) walks modified shapes and descendants to decide per copy-shape `ignore-geometry?`; both renderers call it, with `clp/modifiers-transform` (SVG) or `clp/matrices-transform` (WASM).
|
||||
- `check-delta` compares a copy's relative position/rotation to its component root before and after transform. If relative movement is under about 1px and size/rotation are effectively unchanged, geometry touching is suppressed.
|
||||
- This logic is why pure translations of component copies can avoid marking every descendant as geometry-touched, while resizes/rotations still propagate touched state.
|
||||
|
||||
|
||||
219
common/src/app/common/logic/placement.cljc
Normal file
219
common/src/app/common/logic/placement.cljc
Normal file
@ -0,0 +1,219 @@
|
||||
;; 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 app.common.logic.placement
|
||||
"The placement step of a transform: which shapes of a copy a transform
|
||||
leaves untouched. Moving, resizing or rotating a copy as a whole is free
|
||||
placement and touches none of its descendants; editing one shape of a copy
|
||||
touches it (see `mem:common/component-sync-contract`). The result, the
|
||||
ignore tree, goes to `cls/generate-update-shapes` (`:ignore-tree`), which
|
||||
leaves the geometry of the shapes it marks untouched."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]))
|
||||
|
||||
(def transform-attrs
|
||||
"The attributes a transform can change, so an update after it compares
|
||||
only these."
|
||||
#{:selrect
|
||||
:points
|
||||
:x
|
||||
:y
|
||||
:r1
|
||||
:r2
|
||||
:r3
|
||||
:r4
|
||||
:shadow
|
||||
:blur
|
||||
:background-blur
|
||||
:strokes
|
||||
:width
|
||||
:height
|
||||
:content
|
||||
:transform
|
||||
:transform-inverse
|
||||
:rotation
|
||||
:flip-x
|
||||
:flip-y
|
||||
:grow-type
|
||||
:position-data
|
||||
:layout-gap
|
||||
:layout-padding
|
||||
:layout-item-h-sizing
|
||||
:layout-item-max-h
|
||||
:layout-item-max-w
|
||||
:layout-item-min-h
|
||||
:layout-item-min-w
|
||||
:layout-item-v-sizing
|
||||
:layout-padding-type
|
||||
:layout-item-margin
|
||||
:layout-item-margin-type
|
||||
:layout-grid-cells
|
||||
:layout-grid-columns
|
||||
:layout-grid-rows
|
||||
:fills
|
||||
:fill-image})
|
||||
|
||||
(defn- check-delta
|
||||
"If the shape is a component instance, check whether the transformation is a
|
||||
free change that must not mark the shape as touched.
|
||||
|
||||
For the instance ROOT, position is free placement, but its rotation and flips
|
||||
are inherited content: transforming the copy as a whole overrides them, so a
|
||||
change there marks the root as touched.
|
||||
|
||||
For DESCENDANTS, position, size, rotation and flips are compared RELATIVE TO
|
||||
THE ROOT: a transformation of the whole instance preserves them all and does
|
||||
not mark the descendants as touched; editing an individual shape does."
|
||||
[shape root transformed-shape transformed-root]
|
||||
(let [is-root? (and (some? root)
|
||||
(= (dm/get-prop shape :id) (dm/get-prop root :id)))
|
||||
|
||||
center (fn [shape]
|
||||
(grc/rect->center (:selrect shape)))
|
||||
|
||||
rel-pos (fn [shape root]
|
||||
(when root
|
||||
;; vector from the root center to the shape center,
|
||||
;; expressed in the root's local (untransformed) axes
|
||||
(-> (gpt/subtract (center shape) (center root))
|
||||
(gpt/transform (:transform-inverse root (gmt/matrix))))))
|
||||
|
||||
orientation (fn [shape root]
|
||||
;; the shape's rotation/flip orientation taken from its
|
||||
;; transform matrix, so it is reliable regardless of how the
|
||||
;; transform was applied (the WASM apply-transform path does
|
||||
;; not refresh the :rotation attribute). For the ROOT the
|
||||
;; absolute orientation; for a DESCENDANT the orientation
|
||||
;; relative to the root — invariant when the whole instance
|
||||
;; is rotated or flipped as a unit.
|
||||
(let [t (:transform shape (gmt/matrix))]
|
||||
(if is-root?
|
||||
t
|
||||
(gmt/multiply (:transform-inverse root (gmt/matrix)) t))))
|
||||
|
||||
pos-before (rel-pos shape root)
|
||||
pos-after (rel-pos transformed-shape transformed-root)
|
||||
|
||||
distance
|
||||
(if (and pos-before pos-after)
|
||||
(gpt/distance-vector pos-before pos-after)
|
||||
(gpt/point 0 0))
|
||||
|
||||
orientation-unchanged?
|
||||
(gmt/close? (orientation shape root)
|
||||
(orientation transformed-shape transformed-root))
|
||||
|
||||
selrect (:selrect shape)
|
||||
transformed-selrect (:selrect transformed-shape)]
|
||||
|
||||
;; There are cases in that the coordinates change slightly (e.g. when rounding
|
||||
;; to pixel, or when recalculating text positions in different zoom levels).
|
||||
;; To take this into account, we ignore movements smaller than 1 pixel.
|
||||
;;
|
||||
;; When the change is a resize, also has a transformation that may have the
|
||||
;; shape position unchanged. But in this case we do not want to ignore it.
|
||||
(and (and (< (:x distance) 1) (< (:y distance) 1))
|
||||
(mth/close? (:width selrect) (:width transformed-selrect))
|
||||
(mth/close? (:height selrect) (:height transformed-selrect))
|
||||
orientation-unchanged?)))
|
||||
|
||||
(defn cached-transform
|
||||
"`transform` (shape -> transformed shape), computed once per shape value."
|
||||
[transform]
|
||||
(let [cache (volatile! {})]
|
||||
(fn [shape]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
[cached result] (get @cache id)]
|
||||
(if (and (some? cached) (identical? shape cached))
|
||||
result
|
||||
(let [result (transform shape)]
|
||||
(vswap! cache assoc id [shape result])
|
||||
result))))))
|
||||
|
||||
(defn calculate-ignore-tree
|
||||
"A map of each shape of a copy under the transformed shapes `ids` to
|
||||
whether the transform leaves it untouched: true when its geometry
|
||||
relative to its copy root (or, for the root, its orientation) is the
|
||||
same after `transform` (shape -> transformed shape). Each shape is
|
||||
compared with the root of the copy of the transformed shape latest in
|
||||
`ids` among itself and its ancestors."
|
||||
[objects ids transform]
|
||||
(let [order (into {} (map-indexed (fn [i id] [id i])) ids)
|
||||
|
||||
component-shape
|
||||
(fn [shape parent parent-component]
|
||||
(cond
|
||||
(cfh/root? shape) nil
|
||||
(ctk/instance-root? shape) shape
|
||||
(and (ctk/instance-head? shape) (not (ctk/in-component-copy? parent))) shape
|
||||
:else parent-component))
|
||||
|
||||
walk
|
||||
(fn walk [ignore-tree shape component latest]
|
||||
(let [index (get order (dm/get-prop shape :id))
|
||||
latest (if (and (some? index) (or (nil? latest) (> index (nth latest 0))))
|
||||
(if (some? component)
|
||||
[index component (transform component)]
|
||||
latest)
|
||||
latest)
|
||||
ignore-tree
|
||||
(if (and (some? latest) (ctk/in-component-copy? shape))
|
||||
(let [[_ root transformed-root] latest]
|
||||
(assoc ignore-tree
|
||||
(dm/get-prop shape :id)
|
||||
(check-delta shape root (transform shape) transformed-root)))
|
||||
ignore-tree)]
|
||||
(reduce (fn [ignore-tree child]
|
||||
(walk ignore-tree child (component-shape child shape component) latest))
|
||||
ignore-tree
|
||||
(map (d/getf objects) (:shapes shape)))))
|
||||
|
||||
under-transform (volatile! {})
|
||||
|
||||
under-transform?
|
||||
(fn under-transform? [id]
|
||||
(let [parent-id (dm/get-in objects [id :parent-id])]
|
||||
(cond
|
||||
(or (nil? parent-id) (= parent-id id)) false
|
||||
(contains? order parent-id) true
|
||||
:else
|
||||
(let [cached (get @under-transform parent-id ::none)]
|
||||
(if (= cached ::none)
|
||||
(let [result (under-transform? parent-id)]
|
||||
(vswap! under-transform assoc parent-id result)
|
||||
result)
|
||||
cached)))))]
|
||||
(->> ids
|
||||
(remove under-transform?)
|
||||
(keep #(get objects %))
|
||||
(reduce (fn [ignore-tree shape]
|
||||
(let [parent (get objects (:parent-id shape))]
|
||||
(walk ignore-tree
|
||||
shape
|
||||
(component-shape shape parent (ctn/get-component-shape objects parent {:allow-main? true}))
|
||||
nil)))
|
||||
nil))))
|
||||
|
||||
(defn modifiers-transform
|
||||
"A cached transform (see `cached-transform`) that applies to each shape its
|
||||
modifiers in `modif-tree`."
|
||||
[modif-tree]
|
||||
(cached-transform #(gsh/transform-shape % (dm/get-in modif-tree [(dm/get-prop % :id) :modifiers]))))
|
||||
|
||||
(defn matrices-transform
|
||||
"A cached transform (see `cached-transform`) that applies to each shape its
|
||||
matrix in `transforms`, a map of ids to matrices."
|
||||
[transforms]
|
||||
(cached-transform #(gsh/apply-transform % (get transforms (dm/get-prop % :id)))))
|
||||
@ -274,10 +274,11 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Geometry operations
|
||||
;;
|
||||
;; The frontend interpreter dispatches the real sidebar events, whose
|
||||
;; transform step decides which copy shapes the change touches
|
||||
;; (`check-delta`), frontend code. Their `apply-to` makes the same transform
|
||||
;; without that step, so the pure runner rejects them.
|
||||
;; The frontend interpreter dispatches the real sidebar events: they build
|
||||
;; the modifiers (frontend code: constraints, pixel snap) and the placement
|
||||
;; step (`clp/calculate-ignore-tree`) decides which copy shapes the change
|
||||
;; touches. Their `apply-to` makes the same transform without either, so the
|
||||
;; pure runner rejects them.
|
||||
|
||||
(defrecord Rotate [target angle]
|
||||
IComponentOperation
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
(ns common-tests.logic.composable-sync-test
|
||||
"Runs the shared composable component cases through the pure runner, on the
|
||||
JVM and in JS. Only the frontend interpreter runs the geometry cases (N, Z,
|
||||
AI and the rotation case), which need the frontend placement step, case X,
|
||||
AI and the rotation case), whose modifiers come from frontend events, case X,
|
||||
which needs a pending frontend sync, case AJ, which needs the frontend
|
||||
reflow, and cases AT..AX, whose variant actions are frontend events."
|
||||
(:require
|
||||
|
||||
77
common/test/common_tests/logic/placement_test.cljc
Normal file
77
common/test/common_tests/logic/placement_test.cljc
Normal file
@ -0,0 +1,77 @@
|
||||
;; 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 common-tests.logic.placement-test
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.modifiers :as gm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logic.placement :as clp]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(defn- setup
|
||||
[]
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-frame :board)
|
||||
(tho/add-nested-component-with-copy :component1 :main1-root :main1-child
|
||||
:component2 :main2-root :nested-head
|
||||
:copy2-root
|
||||
:copy2-root-params {:parent-label :board}))]
|
||||
(:objects (thf/current-page file))))
|
||||
|
||||
(defn- moved-ignore-tree
|
||||
"The ignore tree of moving the shapes `moved-ids` by (10, 20), each with
|
||||
the same matrix, as the WASM renderer gives it."
|
||||
[objects moved-ids]
|
||||
(let [transforms (zipmap moved-ids (repeat (gmt/translate-matrix (gpt/point 10 20))))]
|
||||
(clp/calculate-ignore-tree objects moved-ids (clp/matrices-transform transforms))))
|
||||
|
||||
(defn- copy-ids
|
||||
[objects]
|
||||
(->> (cfh/get-children-ids-with-self objects (thi/id :copy2-root))
|
||||
(filter #(ctk/in-component-copy? (get objects %)))))
|
||||
|
||||
(t/deftest moving-a-board-keeps-its-copies-untouched
|
||||
(let [objects (setup)
|
||||
tree (moved-ignore-tree objects (cfh/get-children-ids-with-self objects (thi/id :board)))]
|
||||
(t/is (seq (copy-ids objects)))
|
||||
(t/is (every? #(true? (get tree %)) (copy-ids objects)))))
|
||||
|
||||
(t/deftest moving-a-copy-keeps-its-children-untouched
|
||||
(let [objects (setup)
|
||||
tree (moved-ignore-tree objects (cfh/get-children-ids-with-self objects (thi/id :copy2-root)))]
|
||||
(t/is (every? #(true? (get tree %)) (copy-ids objects)))))
|
||||
|
||||
(t/deftest moving-a-shape-inside-a-copy-touches-it
|
||||
(let [objects (setup)
|
||||
rect-id (last (copy-ids objects))
|
||||
tree (moved-ignore-tree objects [rect-id])]
|
||||
(t/is (false? (get tree rect-id)))))
|
||||
|
||||
(t/deftest rotating-a-copy-with-modifiers-keeps-its-children-untouched
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root
|
||||
:main-root-params {:width 100 :height 100}
|
||||
:main-child-params {:x 10 :y 10 :width 20 :height 20}))
|
||||
objects (:objects (thf/current-page file))
|
||||
root (get objects (thi/id :copy-root))
|
||||
child-id (first (:shapes root))
|
||||
modif-tree (gm/set-objects-modifiers
|
||||
{(:id root) {:modifiers (ctm/rotation-modifiers root (gpt/point 50 50) 30)}}
|
||||
objects)
|
||||
tree (clp/calculate-ignore-tree objects (keys modif-tree) (clp/modifiers-transform modif-tree))]
|
||||
(t/testing "the root's rotation is an override"
|
||||
(t/is (false? (get tree (:id root)))))
|
||||
(t/testing "its child keeps its geometry relative to it"
|
||||
(t/is (true? (get tree child-id))))))
|
||||
@ -73,6 +73,7 @@
|
||||
[common-tests.logic.library-asset-sync-test]
|
||||
[common-tests.logic.move-shapes-test]
|
||||
[common-tests.logic.multiple-nesting-levels-test]
|
||||
[common-tests.logic.placement-test]
|
||||
[common-tests.logic.swap-and-reset-test]
|
||||
[common-tests.logic.swap-as-override-test]
|
||||
[common-tests.logic.swap-group-test]
|
||||
@ -192,6 +193,7 @@
|
||||
'common-tests.logic.library-asset-sync-test
|
||||
'common-tests.logic.move-shapes-test
|
||||
'common-tests.logic.multiple-nesting-levels-test
|
||||
'common-tests.logic.placement-test
|
||||
'common-tests.logic.swap-and-reset-test
|
||||
'common-tests.logic.swap-as-override-test
|
||||
'common-tests.logic.swap-group-test
|
||||
|
||||
@ -16,10 +16,9 @@
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.logic.placement :as clp]
|
||||
[app.common.math :as mth]
|
||||
[app.common.render-wasm.api.select :as wselect]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.types.shape.attrs :refer [editable-attrs]]
|
||||
@ -63,46 +62,6 @@
|
||||
(when (wasm-gesture/try-end-interactive-transform!)
|
||||
(wasm.api/set-modifiers-end)))
|
||||
|
||||
(def ^:private transform-attrs
|
||||
#{:selrect
|
||||
:points
|
||||
:x
|
||||
:y
|
||||
:r1
|
||||
:r2
|
||||
:r3
|
||||
:r4
|
||||
:shadow
|
||||
:blur
|
||||
:background-blur
|
||||
:strokes
|
||||
:width
|
||||
:height
|
||||
:content
|
||||
:transform
|
||||
:transform-inverse
|
||||
:rotation
|
||||
:flip-x
|
||||
:flip-y
|
||||
:grow-type
|
||||
:position-data
|
||||
:layout-gap
|
||||
:layout-padding
|
||||
:layout-item-h-sizing
|
||||
:layout-item-max-h
|
||||
:layout-item-max-w
|
||||
:layout-item-min-h
|
||||
:layout-item-min-w
|
||||
:layout-item-v-sizing
|
||||
:layout-padding-type
|
||||
:layout-item-margin
|
||||
:layout-item-margin-type
|
||||
:layout-grid-cells
|
||||
:layout-grid-columns
|
||||
:layout-grid-rows
|
||||
:fills
|
||||
:fill-image})
|
||||
|
||||
;; -- temporary modifiers -------------------------------------------
|
||||
|
||||
;; During an interactive transformation of shapes (e.g. when resizing or rotating
|
||||
@ -119,193 +78,6 @@
|
||||
;; apply-modifiers event is done, that consolidates all modifiers into the base
|
||||
;; geometric attributes of the shapes.
|
||||
|
||||
(defn- check-delta
|
||||
"If the shape is a component instance, check whether the transformation is a
|
||||
free change that must not mark the shape as touched.
|
||||
|
||||
For the instance ROOT, position is free placement, but its rotation and flips
|
||||
are inherited content: transforming the copy as a whole overrides them, so a
|
||||
change there marks the root as touched.
|
||||
|
||||
For DESCENDANTS, position, size, rotation and flips are compared RELATIVE TO
|
||||
THE ROOT: a transformation of the whole instance preserves them all and does
|
||||
not mark the descendants as touched; editing an individual shape does."
|
||||
[shape root transformed-shape transformed-root]
|
||||
(let [is-root? (and (some? root)
|
||||
(= (dm/get-prop shape :id) (dm/get-prop root :id)))
|
||||
|
||||
center (fn [shape]
|
||||
(grc/rect->center (:selrect shape)))
|
||||
|
||||
rel-pos (fn [shape root]
|
||||
(when root
|
||||
;; vector from the root center to the shape center,
|
||||
;; expressed in the root's local (untransformed) axes
|
||||
(-> (gpt/subtract (center shape) (center root))
|
||||
(gpt/transform (:transform-inverse root (gmt/matrix))))))
|
||||
|
||||
orientation (fn [shape root]
|
||||
;; the shape's rotation/flip orientation taken from its
|
||||
;; transform matrix, so it is reliable regardless of how the
|
||||
;; transform was applied (the WASM apply-transform path does
|
||||
;; not refresh the :rotation attribute). For the ROOT the
|
||||
;; absolute orientation; for a DESCENDANT the orientation
|
||||
;; relative to the root — invariant when the whole instance
|
||||
;; is rotated or flipped as a unit.
|
||||
(let [t (:transform shape (gmt/matrix))]
|
||||
(if is-root?
|
||||
t
|
||||
(gmt/multiply (:transform-inverse root (gmt/matrix)) t))))
|
||||
|
||||
pos-before (rel-pos shape root)
|
||||
pos-after (rel-pos transformed-shape transformed-root)
|
||||
|
||||
distance
|
||||
(if (and pos-before pos-after)
|
||||
(gpt/distance-vector pos-before pos-after)
|
||||
(gpt/point 0 0))
|
||||
|
||||
orientation-unchanged?
|
||||
(gmt/close? (orientation shape root)
|
||||
(orientation transformed-shape transformed-root))
|
||||
|
||||
selrect (:selrect shape)
|
||||
transformed-selrect (:selrect transformed-shape)]
|
||||
|
||||
;; There are cases in that the coordinates change slightly (e.g. when rounding
|
||||
;; to pixel, or when recalculating text positions in different zoom levels).
|
||||
;; To take this into account, we ignore movements smaller than 1 pixel.
|
||||
;;
|
||||
;; When the change is a resize, also has a transformation that may have the
|
||||
;; shape position unchanged. But in this case we do not want to ignore it.
|
||||
(and (and (< (:x distance) 1) (< (:y distance) 1))
|
||||
(mth/close? (:width selrect) (:width transformed-selrect))
|
||||
(mth/close? (:height selrect) (:height transformed-selrect))
|
||||
orientation-unchanged?)))
|
||||
|
||||
(defn calculate-ignore-tree
|
||||
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
|
||||
[modif-tree objects]
|
||||
|
||||
(letfn [(get-ignore-tree
|
||||
([ignore-tree shape]
|
||||
(let [shape-id (dm/get-prop shape :id)
|
||||
transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))
|
||||
|
||||
root
|
||||
(if (:component-root shape)
|
||||
shape
|
||||
(ctn/get-component-shape objects shape {:allow-main? true}))
|
||||
|
||||
transformed-root
|
||||
(if (:component-root shape)
|
||||
transformed-shape
|
||||
(gsh/transform-shape root (dm/get-in modif-tree [(:id root) :modifiers])))]
|
||||
|
||||
(get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))
|
||||
|
||||
([ignore-tree shape root transformed-root]
|
||||
(let [shape-id (dm/get-prop shape :id)
|
||||
transformed-shape (gsh/transform-shape shape (dm/get-in modif-tree [shape-id :modifiers]))]
|
||||
(get-ignore-tree ignore-tree shape transformed-shape root transformed-root)))
|
||||
|
||||
([ignore-tree shape transformed-shape root transformed-root]
|
||||
(let [shape-id (dm/get-prop shape :id)
|
||||
|
||||
ignore-tree
|
||||
(cond-> ignore-tree
|
||||
(and (some? root) (ctk/in-component-copy? shape))
|
||||
(assoc
|
||||
shape-id
|
||||
(check-delta shape root transformed-shape transformed-root)))
|
||||
|
||||
set-child
|
||||
(fn [ignore-tree child]
|
||||
(get-ignore-tree ignore-tree child root transformed-root))]
|
||||
|
||||
(->> (:shapes shape)
|
||||
(map (d/getf objects))
|
||||
(reduce set-child ignore-tree)))))]
|
||||
|
||||
;; we check twice because we want only to search parents of components but once the
|
||||
;; tree is traversed we only want to process the objects in components
|
||||
(->> (keys modif-tree)
|
||||
(map #(get objects %))
|
||||
(reduce get-ignore-tree nil))))
|
||||
|
||||
(defn- cached-transform
|
||||
[transforms]
|
||||
(let [cache (js/Map.)]
|
||||
(fn [shape]
|
||||
(let [id (dm/get-prop shape :id)
|
||||
cached (.get cache id)]
|
||||
(if (and (some? cached) (identical? shape (aget cached 0)))
|
||||
(aget cached 1)
|
||||
(let [result (gsh/apply-transform shape (get transforms id))]
|
||||
(.set cache id #js [shape result])
|
||||
result))))))
|
||||
|
||||
(defn calculate-ignore-tree-wasm
|
||||
"Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers"
|
||||
([transforms objects]
|
||||
(calculate-ignore-tree-wasm transforms objects (cached-transform transforms)))
|
||||
([transforms objects transform]
|
||||
(let [order (into {} (map-indexed (fn [i id] [id i])) (keys transforms))
|
||||
|
||||
component-shape
|
||||
(fn [shape parent parent-component]
|
||||
(cond
|
||||
(cfh/root? shape) nil
|
||||
(ctk/instance-root? shape) shape
|
||||
(and (ctk/instance-head? shape) (not (ctk/in-component-copy? parent))) shape
|
||||
:else parent-component))
|
||||
|
||||
walk
|
||||
(fn walk [ignore-tree shape component latest]
|
||||
(let [index (get order (dm/get-prop shape :id))
|
||||
latest (if (and (some? index) (or (nil? latest) (> index (nth latest 0))))
|
||||
(if (some? component)
|
||||
[index component (transform component)]
|
||||
latest)
|
||||
latest)
|
||||
ignore-tree
|
||||
(if (and (some? latest) (ctk/in-component-copy? shape))
|
||||
(let [[_ root transformed-root] latest]
|
||||
(assoc ignore-tree
|
||||
(dm/get-prop shape :id)
|
||||
(check-delta shape root (transform shape) transformed-root)))
|
||||
ignore-tree)]
|
||||
(reduce (fn [ignore-tree child]
|
||||
(walk ignore-tree child (component-shape child shape component) latest))
|
||||
ignore-tree
|
||||
(map (d/getf objects) (:shapes shape)))))
|
||||
|
||||
under-transform (volatile! {})
|
||||
|
||||
under-transform?
|
||||
(fn under-transform? [id]
|
||||
(let [parent-id (dm/get-in objects [id :parent-id])]
|
||||
(cond
|
||||
(or (nil? parent-id) (= parent-id id)) false
|
||||
(contains? order parent-id) true
|
||||
:else
|
||||
(let [cached (get @under-transform parent-id ::none)]
|
||||
(if (= cached ::none)
|
||||
(let [result (under-transform? parent-id)]
|
||||
(vswap! under-transform assoc parent-id result)
|
||||
result)
|
||||
cached)))))]
|
||||
(->> (keys transforms)
|
||||
(remove under-transform?)
|
||||
(keep #(get objects %))
|
||||
(reduce (fn [ignore-tree shape]
|
||||
(let [parent (get objects (:parent-id shape))]
|
||||
(walk ignore-tree
|
||||
shape
|
||||
(component-shape shape parent (ctn/get-component-shape objects parent {:allow-main? true}))
|
||||
nil)))
|
||||
nil)))))
|
||||
|
||||
(defn assoc-position-data
|
||||
[shape position-data old-shape]
|
||||
(let [deltav (gpt/to-vec (gpt/point (ctm/safe-size-rect old-shape))
|
||||
@ -907,10 +679,10 @@
|
||||
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel? snap-ignore-axis)))
|
||||
|
||||
transform-shape
|
||||
(cached-transform transforms)
|
||||
(clp/matrices-transform transforms)
|
||||
|
||||
ignore-tree
|
||||
(calculate-ignore-tree-wasm transforms objects transform-shape)
|
||||
(clp/calculate-ignore-tree objects (keys transforms) transform-shape)
|
||||
|
||||
options
|
||||
(-> params
|
||||
@ -919,7 +691,7 @@
|
||||
(assoc :translation? translation?)
|
||||
;; Attributes that can change in the transform. This
|
||||
;; way we don't have to check all the attributes
|
||||
(assoc :attrs transform-attrs))
|
||||
(assoc :attrs clp/transform-attrs))
|
||||
|
||||
modif-tree
|
||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||
@ -1061,7 +833,8 @@
|
||||
ids)
|
||||
|
||||
ignore-tree
|
||||
(calculate-ignore-tree object-modifiers objects)
|
||||
(clp/calculate-ignore-tree objects (keys object-modifiers)
|
||||
(clp/modifiers-transform object-modifiers))
|
||||
|
||||
options
|
||||
(-> options
|
||||
@ -1069,7 +842,7 @@
|
||||
(assoc :ignore-tree ignore-tree)
|
||||
;; Attributes that can change in the transform. This
|
||||
;; way we don't have to check all the attributes
|
||||
(assoc :attrs transform-attrs))
|
||||
(assoc :attrs clp/transform-attrs))
|
||||
|
||||
update-shape
|
||||
(fn [shape]
|
||||
|
||||
@ -1,56 +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.ignore-tree-test
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[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.types.component :as ctk]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(t/use-fixtures :each {:before cthi/reset-idmap!})
|
||||
|
||||
(defn- setup
|
||||
[]
|
||||
(let [file (-> (cthf/sample-file :file1)
|
||||
(ctho/add-frame :board)
|
||||
(ctho/add-nested-component-with-copy :component1 :main1-root :main1-child
|
||||
:component2 :main2-root :nested-head
|
||||
:copy2-root
|
||||
:copy2-root-params {:parent-label :board}))]
|
||||
(:objects (cthf/current-page file))))
|
||||
|
||||
(defn- ignore-tree
|
||||
[objects moved-ids]
|
||||
(let [move (gmt/translate-matrix (gpt/point 10 20))]
|
||||
(dwm/calculate-ignore-tree-wasm (zipmap moved-ids (repeat move)) objects)))
|
||||
|
||||
(defn- copy-ids
|
||||
[objects]
|
||||
(->> (cfh/get-children-ids-with-self objects (cthi/id :copy2-root))
|
||||
(filter #(ctk/in-component-copy? (get objects %)))))
|
||||
|
||||
(t/deftest moving-a-board-keeps-its-copies-untouched
|
||||
(let [objects (setup)
|
||||
tree (ignore-tree objects (cfh/get-children-ids-with-self objects (cthi/id :board)))]
|
||||
(t/is (seq (copy-ids objects)))
|
||||
(t/is (every? #(true? (get tree %)) (copy-ids objects)))))
|
||||
|
||||
(t/deftest moving-a-copy-keeps-its-children-untouched
|
||||
(let [objects (setup)
|
||||
tree (ignore-tree objects (cfh/get-children-ids-with-self objects (cthi/id :copy2-root)))]
|
||||
(t/is (every? #(true? (get tree %)) (copy-ids objects)))))
|
||||
|
||||
(t/deftest moving-a-shape-inside-a-copy-touches-it
|
||||
(let [objects (setup)
|
||||
rect-id (last (copy-ids objects))
|
||||
tree (ignore-tree objects [rect-id])]
|
||||
(t/is (false? (get tree rect-id)))))
|
||||
@ -31,8 +31,8 @@
|
||||
[frontend-tests.data.workspace-comments-test]
|
||||
[frontend-tests.data.workspace-context-menu-test]
|
||||
[frontend-tests.data.workspace-edit-watchers-test]
|
||||
[frontend-tests.data.workspace-interactions-test]
|
||||
[frontend-tests.data.workspace-font-variants-test]
|
||||
[frontend-tests.data.workspace-interactions-test]
|
||||
[frontend-tests.data.workspace-libraries-test]
|
||||
[frontend-tests.data.workspace-library-links-test]
|
||||
[frontend-tests.data.workspace-library-updates-test]
|
||||
@ -61,7 +61,6 @@
|
||||
[frontend-tests.logic.copying-and-duplicating-test]
|
||||
[frontend-tests.logic.frame-guides-test]
|
||||
[frontend-tests.logic.groups-test]
|
||||
[frontend-tests.logic.ignore-tree-test]
|
||||
[frontend-tests.logic.nudge-selected-shapes-test]
|
||||
[frontend-tests.logic.pasting-in-containers-test]
|
||||
[frontend-tests.logic.path-actions-test]
|
||||
@ -227,7 +226,6 @@
|
||||
'frontend-tests.logic.copying-and-duplicating-test
|
||||
'frontend-tests.logic.frame-guides-test
|
||||
'frontend-tests.logic.groups-test
|
||||
'frontend-tests.logic.ignore-tree-test
|
||||
'frontend-tests.logic.nudge-selected-shapes-test
|
||||
'frontend-tests.logic.path-actions-test
|
||||
'frontend-tests.logic.path-clipboard-test
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user