From 955650c6185bbba58abd30274e84e7acdd4fe048 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Sat, 24 Oct 2026 01:36:47 +0200 Subject: [PATCH] :bug: Place children after carrying variant geometry overrides A switch can retain the copy root's resized geometry while leaving its untouched children in the old frame. After geometry carry, sync the new subtree from the updated builder objects so constraints place its children without adding overrides. Reuse the direct component sync entry point. The portable rotated-root regression fails before the fix with a 30px child offset. Swap and variant suites pass 108 tests / 539 assertions; the pure golden master matches 1149 assertions unchanged. Full frontend: 1517 tests / 8436 assertions. Live ws1 WASM retains the resized root and child placement, with no exception or pending reflow. Benchmark ratios: main 10/100/1000 copies 0.48/0.56/0.57; three levels 0.49; chain 0.48; library 0.50; many shapes 0.45; variant switch 0.61. No slowdown flags. AI-assisted-by: GPT-6 --- .../common/component-swap-pipeline.md | 5 ++++ common/src/app/common/logic/libraries.cljc | 2 +- common/src/app/common/logic/variants.cljc | 9 +++++- .../logic/variants_switch_test.cljc | 28 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.serena/memories/common/component-swap-pipeline.md b/.serena/memories/common/component-swap-pipeline.md index cffcb524be..7494dc905b 100644 --- a/.serena/memories/common/component-swap-pipeline.md +++ b/.serena/memories/common/component-swap-pipeline.md @@ -48,3 +48,8 @@ The clean target already has the copy's rotation, which can turn 100 into 99.99999999999994. Exact dimension equality drops valid resize overrides (F160). Fixed layout sizes and composite geometry use the same tolerance; nongeometry scalar attributes still compare exactly. + +After carrying geometry, the switch syncs its new subtree once from the +updated builder objects. Untouched children must follow a retained root +size through constraints; copying only root attrs leaves them in the old +frame (F162). The direct sync keeps carried groups and child swaps. diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index a60e083bbb..b6732279b7 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -285,7 +285,7 @@ (reduce #(update %1 %2 turn) objects (cfh/get-children-ids-with-self objects (:id shape-main))))))))) -(defn- generate-sync-shape-direct +(defn generate-sync-shape-direct "Generate changes to synchronize one shape that is the root of a component instance, and all its children, from the given component." [changes file libraries container shape-id reset?] diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index c887e3a4fb..6afbcf53cf 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -430,7 +430,14 @@ (ctk/touched-group? % :geometry-group)) orig-touched) (keep-orientation-override original-shape new-shape page (or (get-in libraries [(:component-file new-shape) :data]) ldata) - (:component-id new-shape) nil))] + (:component-id new-shape) nil)) + ;; A carried size changes the frame its untouched children follow. + ;; Place them through the same constraints as an ordinary copy sync. + changes (cond-> changes + (some #(ctk/touched-group? % :geometry-group) orig-touched) + (cll/generate-sync-shape-direct + nil libraries (assoc container :objects (pcb/get-objects changes)) + (:id new-shape) false))] [changes (mapv (comp :parent-id :new) pairs)])) (defn- shape-index diff --git a/common/test/common_tests/logic/variants_switch_test.cljc b/common/test/common_tests/logic/variants_switch_test.cljc index 35acbf5c2b..3ff93e59e5 100644 --- a/common/test/common_tests/logic/variants_switch_test.cljc +++ b/common/test/common_tests/logic/variants_switch_test.cljc @@ -49,6 +49,34 @@ (t/is (mth/close? 80 (get-in copy [:selrect :height]) 1e-10)) (t/is (contains? (:touched copy) :geometry-group))))) +(t/deftest switch-places-children-through-a-carried-root-resize + (doseq [angle [0 45]] + (let [root-params {:width 100 :height 100} + child-params {:x 10 :y 10 :width 20 :height 20 + :constraints-h :left :constraints-v :top} + file (-> (thf/sample-file :file1) + (thv/add-variant-with-child + :v01 :c01 :m01 :c02 :m02 :r01 :r02 + {:variant1-params root-params :variant2-params root-params + :child1-params child-params :child2-params child-params}) + (thc/instantiate-component :c01 :copy01)) + file (reduce (fn [file modifiers] + (let [shape (ths/get-shape file :copy01)] + (thf/apply-changes file + (n/transform-changes (thf/current-page file) shape + (modifiers shape))))) + file [#(ctm/rotation-modifiers % (gsh/shape->center %) angle) + #(ctm/change-size % nil 80)]) + switched (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true}) + relative (fn [file] + (let [root (ths/get-shape file :copy01) + child (get-in (thf/current-page file) [:objects (first (:shapes root))])] + (gpt/subtract (first (:points child)) (first (:points root))))) + child (get-in (thf/current-page switched) + [:objects (first (:shapes (ths/get-shape switched :copy01)))])] + (t/is (< (gpt/distance (relative file) (relative switched)) 0.01)) + (t/is (empty? (:touched child)))))) + ;; ============================================================ ;; BASIC SWITCH TESTS (no overrides) ;; ============================================================