From b21e2775c16955a41ced30fc4586824e11ab75c1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 13 Oct 2026 10:41:11 +0200 Subject: [PATCH] :sparkles: Place a swapped child where the new variant has it A variant switch with keep-touched carries a swapped child into the new variant, but left it where it was in the old one. The next sync then moved it to the place of the child it replaces, at a moment unrelated to the switch. The switch now gives it that placement at once, through the same step sync uses: position, constraints and layout item attributes the user did not change. Benchmark, against the same code without this change under the same load (lower quantile, ms; without / with): deepest-edit-through-three-levels 36.0 / 36.0 edit-through-a-chain-of-dependents 65.2 / 70.2 library-update-into-copies 74.3 / 75.9 main-edit-into-10-copies 1.6 / 1.5 main-edit-into-100-copies 16.3 / 15.7 main-edit-into-1000-copies 162.6 / 165.8 variant-switch-into-copies 27.2 / 27.8 AI-assisted-by: claude-opus-5-5 --- common/src/app/common/logic/libraries.cljc | 39 ++++++++++++------- common/src/app/common/logic/variants.cljc | 7 ++++ .../common_tests/logic/swap_group_test.cljc | 11 ++++++ 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 8c5a7b17ef..8137ec7c2d 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -2422,19 +2422,20 @@ [main-container main-component child-copy] (ctn/get-shape main-container (:shape-ref child-copy))) -(defn- update-swapped-placement - "Synchronizes a swapped copy head with the main shape it replaces, in the - attributes that place it in its parent: position, constraints and layout - item attributes. Its content comes from its own component. A position set - by a layout is left to the layout." - [changes shape-inst shape-main root-inst root-main container] - (let [objects (:objects container) - touched (get shape-inst :touched #{}) +(defn generate-take-placement + "Give the swapped copy head `shape-inst` the placement of `placed`, the + shape whose position it takes, expressed in the same coordinates: the + attributes that place it in its parent (position, constraints and layout + item attributes) that the user did not change. Its content comes from its + own component. With `layout-placed?`, the position is left to the layout. + `objects` holds the subtree of `shape-inst`, which moves with it." + [changes shape-inst placed objects layout-placed?] + (let [touched (get shape-inst :touched #{}) untouched? (fn [attr] (not (touched (ctk/resolve-sync-group (:type shape-inst) attr)))) placement (reduce (fn [attrs attr] - (let [value (get shape-main attr)] + (let [value (get placed attr)] (if (and (untouched? attr) (not= value (get shape-inst attr))) (assoc attrs attr value) @@ -2445,12 +2446,10 @@ position (fn [shape] (gpt/point (dm/get-in shape [:selrect :x]) (dm/get-in shape [:selrect :y]))) - delta (gpt/subtract (position (reposition-shape shape-main root-main root-inst)) - (position shape-inst)) + delta (gpt/subtract (position placed) (position shape-inst)) move? (and (untouched? :x) - (not (gpt/almost-zero? delta)) - (or (not (ctsl/any-layout-immediate-child? objects shape-inst)) - (ctsl/position-absolute? shape-inst)))] + (not layout-placed?) + (not (gpt/almost-zero? delta)))] (cond-> changes (seq placement) (pcb/update-shapes [(:id shape-inst)] @@ -2468,6 +2467,18 @@ #(gsh/move % delta) {:ignore-touched true})))) +(defn- update-swapped-placement + "Synchronizes a swapped copy head with the main shape it replaces, in the + attributes that place it in its parent." + [changes shape-inst shape-main root-inst root-main container] + (let [objects (:objects container)] + (generate-take-placement changes + shape-inst + (reposition-shape shape-main root-main root-inst) + objects + (and (ctsl/any-layout-immediate-child? objects shape-inst) + (not (ctsl/position-absolute? shape-inst)))))) + (defn- update-flex-child-copy-attrs "Synchronizes the attributes inside the flex-child items (main->copy)" [changes shape-main shape-copy main-container main-component copy-container omit-touched?] diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index e9d550388f..612cc8f7a5 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -102,6 +102,13 @@ (= (:component-file %) (:component-file related-shape-in-new))) (ctk/remove-swap-group))) + ;; and its placement, as sync would give it + (cll/generate-take-placement orig-swapped-child + related-shape-in-new + objects + (and (ctsl/any-layout-immediate-child? objects related-shape-in-new) + (not (ctsl/position-absolute? orig-swapped-child)))) + ;; Delete new non-swapped item (cls/generate-delete-shapes ldata page objects (d/ordered-set (:id related-shape-in-new)) {:allow-altering-copies true}) second))) diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index f199c5e23e..61d7b629af 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -475,3 +475,14 @@ (propagate :card) (thf/validate-file!))] (t/is (= margin (:layout-item-margin (ths/get-shape file :copy-star)))))) + +(t/deftest switch-places-the-swap-where-the-new-variant-has-its-child + (let [file (-> (setup-variants) + (move-shape :child2 30 5)) + rel (fn [file child parent] + (let [[x y] (position file child) + [px py] (position file parent)] + [(- x px) (- y py)])) + file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})] + (t/is (ctk/swapped? (ths/get-shape file' :copy-child))) + (t/is (= (rel file' :child2 :m02) (rel file' :copy-child :copy01)))))