From 5ad0744d0e62c1971119a5e3bfe36185251cb134 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Sat, 24 Oct 2026 09:38:55 +0200 Subject: [PATCH] :bug: Keep inherited orientation when pushing a swapped child When update-main turns a swap into a plain nested copy, retain its orientation as a geometry override if it differs from its own component's expected parent placement. Otherwise a later full sync discards the rotation inherited from the replaced position (F168). The portable regression fails three assertions before the fix. Both 0-degree and 45-degree cases now pass, including exact undo. Fresh swap, variant and flex suites pass 125 tests and 596 assertions. The pure golden master matches all 1149 assertions without recording. Benchmark ratios against Phase 1e: main 10/100/1000 copies 0.52/0.62/0.56, three levels 0.48, chain 0.56, library 0.51, many shapes 0.43, variant switch 0.60. No regression flags. AI-assisted-by: GPT-6 --- .../memories/common/component-data-model.md | 6 +++++ common/src/app/common/logic/libraries.cljc | 22 +++++++++++++++++-- .../common_tests/logic/swap_group_test.cljc | 18 +++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/.serena/memories/common/component-data-model.md b/.serena/memories/common/component-data-model.md index b8baf24fbd..326965701d 100644 --- a/.serena/memories/common/component-data-model.md +++ b/.serena/memories/common/component-data-model.md @@ -72,3 +72,9 @@ take find-content-ref-shape at every link. A swapped intermediate head follows its own main; collecting its replaced position's content overrides creates phantom groups in copies of that swap (F165). Position advancement still uses advance-shape-ref and find-ref-shape. + +A swapped head pushed into a main can become a plain nested copy of its own +component. If the old position supplied rotation or flips that differ from +that component's expected parent placement, the new nested copy needs a +geometry-group override. Otherwise a later sync discards the orientation +that update-main just pushed (F168). The source copy clears its overrides. diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index b6732279b7..870d71730b 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -734,6 +734,21 @@ {:type :reg-objects :shapes all-parents}) (map (comp del-obj-change :id) new-shapes)))) +(defn- rehead-pushed-swap + "A pushed swap becomes a plain nested copy. Its old position's + orientation becomes authored when it differs from its own main." + [shape main container] + (let [shape (-> shape + (assoc :shape-ref (:id main)) + (ctk/remove-swap-group)) + main-container (when main + (main-in-parent-orientation (:container (meta main)) main + container shape false)) + expected (get-in main-container [:objects (:id main)])] + (cond-> shape + (and expected (orient-like shape expected (gsh/shape->center shape))) + (update :touched ctk/set-touched-group :geometry-group)))) + (defn- add-shape-to-main "Add to the main shape `parent-main`, at `index`, a copy of `child-inst`, a child of the copy shape that pairs with it; `child-inst` then refers to @@ -752,14 +767,17 @@ (seq (ctn/get-parent-copy-heads (:objects main-container) parent-main))) replaced (when swap-in-copy? (ctn/get-shape main-container (:shape-ref child-inst))) + content-main (when (and (ctk/swapped? child-inst) (not swap-in-copy?)) + (ctf/find-content-ref-shape nil container libraries child-inst + :include-deleted? true + :with-context? true)) update-new-shape (fn [new-shape original-shape] (cond-> (reposition-shape new-shape root-inst root-main) (and (= (:id original-shape) (:id child-inst)) (ctk/swapped? child-inst) (not swap-in-copy?)) - (-> (assoc :shape-ref (swap-content-root-id libraries child-inst)) - (ctk/remove-swap-group)))) + (rehead-pushed-swap content-main main-container))) ;; the copy now follows the main, swap included update-original-shape (fn [original-shape new-shape] diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index 45cc06ed89..8682181ae0 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -899,3 +899,21 @@ (mapv :id chain))) (t/is (= #{:swap-group} (ctf/get-touched-from-ref-chain-until-target-ref page libraries child nil))))) + +(t/deftest update-main-authors-a-swaps-inherited-orientation-in-its-new-main + (doseq [angle [0 45]] + (let [initial (-> (tm/make-situation (setup-sized)) + (r/step (n/rotate :card-star angle)) r/settle) + swapped (-> initial (r/step (n/swap-shape :copy-star :heart)) r/settle) + updated (-> swapped (r/step (n/update-main :copy)) r/settle) + file (tm/file updated) + head (tm/resolve-shape updated :copy-star) + main (tm/shape-by-id updated (:shape-ref head)) + full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))] + (t/is (= (if (zero? angle) #{} #{:geometry-group}) (set (:touched main)))) + (t/is (mth/close? angle (:rotation main) 0.001)) + (t/is (mth/close? angle (:rotation (ths/get-shape-by-id full (:id main))) 0.001)) + (t/is (gsh/close-attrs? :points (:points head) + (:points (ths/get-shape-by-id full (:id head))) 0.001)) + (t/is (= (thn/normalize-file (tm/file swapped)) + (thn/normalize-file (tm/file (r/step updated (n/undo)))))))))