⚡ Compute the drop ignore tree in a single pass (#12033)

Walk only from the top-most transformed shapes, carry the component
root down the walk and memoize each shape transform so the update
step reuses it.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Elena Torró 2026-10-02 10:51:21 +02:00 committed by GitHub
parent 09ffcb1fbc
commit 1554847d40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 50 deletions

View File

@ -233,55 +233,78 @@
(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]
([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))
(letfn [(get-ignore-tree
([ignore-tree shape]
(let [shape-id (dm/get-prop shape :id)
transformed-shape (gsh/apply-transform shape (get transforms shape-id))
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))
root
(if (:component-root shape)
shape
(ctn/get-component-shape objects shape {:allow-main? true}))
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)))))
transformed-root
(if (:component-root shape)
transformed-shape
(gsh/apply-transform root (get transforms (:id root))))]
under-transform (volatile! {})
(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/apply-transform shape (get transforms shape-id))]
(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 transforms)
(map #(get objects %))
(reduce get-ignore-tree nil))))
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]
@ -866,8 +889,11 @@
:else
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel? snap-ignore-axis)))
transform-shape
(cached-transform transforms)
ignore-tree
(calculate-ignore-tree-wasm transforms objects)
(calculate-ignore-tree-wasm transforms objects transform-shape)
options
(-> params
@ -898,12 +924,9 @@
update-shape
(fn [shape]
(let [shape-id (dm/get-prop shape :id)
transform (get transforms shape-id)
modifiers (dm/get-in modif-tree [shape-id :modifiers])]
(-> shape
(gsh/apply-transform transform)
(ctm/apply-structure-modifiers modifiers))))
(-> shape
(transform-shape)
(ctm/apply-structure-modifiers (dm/get-in modif-tree [(dm/get-prop shape :id) :modifiers]))))
undo-id (js/Symbol)]

View File

@ -0,0 +1,56 @@
;; 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)))))

View File

@ -46,6 +46,7 @@
[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]
@ -182,6 +183,7 @@
'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