From 1554847d40aa1fb160eea3b0c9e6280991c96e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Fri, 2 Oct 2026 10:51:21 +0200 Subject: [PATCH] :zap: 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 --- .../app/main/data/workspace/modifiers.cljs | 123 +++++++++++------- .../logic/ignore_tree_test.cljs | 56 ++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 3 files changed, 131 insertions(+), 50 deletions(-) create mode 100644 frontend/test/frontend_tests/logic/ignore_tree_test.cljs diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 328b366bfb..385dbb216d 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -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)] diff --git a/frontend/test/frontend_tests/logic/ignore_tree_test.cljs b/frontend/test/frontend_tests/logic/ignore_tree_test.cljs new file mode 100644 index 0000000000..f625d0009c --- /dev/null +++ b/frontend/test/frontend_tests/logic/ignore_tree_test.cljs @@ -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))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 07f4cb0e8a..5a0110743e 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -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