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)