From 2099084f0b617c2da82743d5a17331f50988ddef Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Sun, 25 Oct 2026 13:35:45 +0100 Subject: [PATCH] :bug: Settle swapped placement when updating a turned main Updating a rotated copy also turns its main. A swapped descendant whose content and position sizes differ needs its placement settled in that new frame. Append the main-file placement changes to the inverse sync, keeping source-copy changes local and restoring both files in one undo group. Portable local/library regressions at 45 and 90 degrees fail eight assertions before the fix and pass all 20 after it. The original seed712 case4855 and its exact shrink pass, as does the 4855..5000 tail diagnostic. The diagnostic does not replace the full 5000-program swap gate. Common JVM1851/30684 and JS1782/29308, frontend1539/9312, backend1610/8104, exporter14/32 and all 68 plugin cases pass. Focused suites and unchanged goldens pass; lint and formatting pass. Live WASM at 45/90 degrees keeps placement stable through undo/redo and a later main edit. Maximum anchor error is below 0.00001px. Benchmark ratios against the recorded baseline: main 10/100/1000: 0.49/0.56/0.54; three levels: 0.49; chain: 0.49; library: 0.50; many shapes: 0.45; switch: 0.58. AI-assisted-by: GPT-6 --- .../common/component-sync-contract.md | 7 +++ common/src/app/common/logic/libraries.cljc | 21 ++++++++- .../logic/comp_expected_geometry_test.cljc | 45 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index fed4207439..ef35d0998c 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -78,3 +78,10 @@ Reset drops the root's geometry override from the whole sync context only while updating existing shapes can turn a newly added child by the old override after its parent has already reset. The reset and replacement children share one undo group and stay stable through later sync. + +Update-main also settles untouched swapped heads carried through a parent +turn in the updated main (F186). A rigid rotation can leave the wrong +anchor when the replaced position and swap content have different sizes. +Apply the main's nonlocal changes to its own file, settle changed placement, +and append those changes to the same inverse-sync group. Keep the source +copy's local changes routed separately, including for linked libraries. diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 70bab2ae62..ff21dee8bc 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -519,6 +519,24 @@ (pcb/with-library-data library-data) (pcb/update-component id #(assoc % :path path :name name))))) +(defn- settle-updated-main-placement + [changes libraries component-file component] + (let [file (get libraries component-file) + container (ctf/get-component-container (:data file) component) + objects (:objects container) + ids (filter #(ctk/swapped? (get objects %)) + (cfh/get-children-ids objects (:main-instance-id component)))] + (if (seq ids) + (let [redo (into [] (remove :local-change?) (:redo-changes changes)) + file (update file :data cfc/process-changes redo false) + page (ctf/get-component-container (:data file) component) + placement (-> (pcb/empty-changes) + (pcb/with-container page) + (pcb/with-objects (:objects page)) + (generate-sync-transformed-placement file libraries page objects ids))] + (pcb/concat-changes changes placement)) + changes))) + (defn generate-sync-shape-inverse "Generate changes to update the component a shape is linked to, from the values in the shape and all its children." @@ -545,7 +563,8 @@ :synced-ids synced-ids} shape-inst (ctf/find-remote-shape container libraries shape-inst)) - (mark-local-changes @synced-ids))) + (mark-local-changes @synced-ids) + (settle-updated-main-placement libraries (:component-file shape-inst) component))) changes))) (defn split-local-changes diff --git a/common/test/common_tests/logic/comp_expected_geometry_test.cljc b/common/test/common_tests/logic/comp_expected_geometry_test.cljc index 97b4e269f8..2da8cbf014 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -20,6 +20,7 @@ [app.common.logic.shapes :as cls] [app.common.math :as mth] [app.common.test-helpers.components :as thc] + [app.common.test-helpers.composable.comp.files :as cf] [app.common.test-helpers.composable.comp.nodes :as n] [app.common.test-helpers.composable.comp.runner :as r] [app.common.test-helpers.composable.comp.setups :as setup] @@ -36,6 +37,50 @@ (t/use-fixtures :each thi/test-fixture) +(t/deftest updating-a-turned-copy-settles-swapped-placement-in-the-main + (doseq [remote? [false true] + angle [45 90]] + (let [file (-> (thf/sample-file :file) + (tho/add-simple-component :a :main-a :rect-a + :root-params {:width 100 :height 100}) + (tho/add-simple-component :other :main-other :rect-other + :root-params {:width 100 :height 100}) + (tho/add-frame :main-b {:width 200 :height 200}) + (thc/instantiate-component :a :b-a :parent-label :main-b) + (thc/make-component :b :main-b) + (tho/add-frame :main-c {:width 300 :height 300}) + (thc/instantiate-component :b :c-b :parent-label :main-c + :children-labels [:c-a]) + (thc/make-component :c :main-c)) + prepared (-> (tm/make-situation file) + (r/step (n/change-height :b-a 80)) r/settle + (r/step (n/swap-shape :c-a :other)) r/settle) + library (tm/file prepared) + initial (if remote? + (-> (tm/make-situation + (thc/instantiate-component (thf/sample-file :local) :c :copy-c + :library library :children-labels [:copy-b :copy-a])) + (tm/with-aux-files {(:id library) library})) + (tm/with-file prepared + (thc/instantiate-component library :c :copy-c + :children-labels [:copy-b :copy-a]))) + before (-> initial (r/step (n/rotate :copy-c angle)) r/settle) + updated (-> before (r/step (n/update-main :copy-c)) r/settle) + main-file (if remote? (get (tm/aux-files updated) (:id library)) (tm/file updated)) + head (ths/get-shape main-file :c-a) + full (:situation (cf/sync-world updated (keys (get-in main-file [:data :components])))) + full-main (if remote? (get (tm/aux-files full) (:id library)) (tm/file full)) + full-head (ths/get-shape-by-id full-main (:id head))] + (t/is (mth/close? angle (:rotation head) 0.001)) + (t/is (= #{:swap-group} (:touched head))) + (t/is (gsh/close-attrs? :points (:points full-head) (:points head) 0.001)) + (t/is (gsh/close-attrs? :points + (:points (tm/resolve-shape full :copy-a)) + (:points (tm/resolve-shape updated :copy-a)) 0.001)) + (t/is (= (into {} (map (fn [[id f]] [id (thn/normalize-file f)])) (cf/all-files before)) + (into {} (map (fn [[id f]] [id (thn/normalize-file f)])) + (cf/all-files (r/step updated (n/undo))))))))) + (t/deftest resetting-a-turned-copy-places-replacement-children-in-the-main-frame (doseq [angle [45 90]] (let [file (-> (thf/sample-file :file)