diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 6413ecc871..289b962da5 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -29,7 +29,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w - Detach: the subtree loses component links, `:touched`, `:remote-synced`. First-level nested heads stay copies, move one nesting level up and inherit the near main's touched groups, so values their old near main overrode become overrides. A swapped head left with no enclosing copy becomes a plain copy of its own component (`:shape-ref` = its main root, no `:swap-group`). - Instantiate / nest a copy: the new subtree has no touched groups. - Duplicate a copy: the new subtree keeps the source's values and touched; if its nesting level changes, touched is inherited as in detach. Paste and move out of a copy follow the same rule. -- Swap without keep-touched: the new subtree keeps `swap-keep-attrs` values; the new head keeps the old shape's id. A retained authored rotation or flip stays a `:geometry-group` override so later sync preserves it; size-only overrides clear, and parent-derived orientation stays untouched. Other content overrides clear. Inside a copy it keeps the old `:shape-ref` and gets `:swap-group` (none when the new component is the near main's); inside a main it is a plain nested copy. +- Swap without keep-touched: the new subtree keeps `swap-keep-attrs` values; the new head keeps the old shape's id. A retained rotation or flip that differs from the new component's expected placement becomes a `:geometry-group` override so later sync preserves it, including orientation inherited from the old component. The swap authors that choice. Size-only overrides clear, and parent-derived orientation stays untouched. Other content overrides clear. Inside a copy it keeps the old `:shape-ref` and gets `:swap-group` (none when the new component is the near main's); inside a main it is a plain nested copy. - Variant switch with keep-touched: an attr is carried from the old copy only when its group is touched and the old and new mains hold the same value for it. Resulting touched = groups of carried attrs (+ `:swap-group` when the head is inside a copy). A value equal to the new main is not carried and not touched. Texts: the same per text group (letters carried only when both mains have the same letters; a style group when both mains agree on it once placed on each other's letters). - Delete component: copies keep their data. The deleted main cannot be edited, so it starts no sync, but changes from components nested in it still pass through: sync updates its stored shapes (`:objects` of the deleted component), its copies, and the components that nest it. Restore brings back an up-to-date main. - Undo/redo: restores the exact prior state, `:touched` and `:remote-synced` included, of the op and the sync it caused together (same undo group, one step). Undo never needs a re-sync. diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index 2066f17070..52d24b7daf 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -405,9 +405,9 @@ (when-let [parent (get objects parent-id)] (or (d/index-of (:shapes parent) shape-id) 0))) -(defn- keep-authored-orientation - "A plain swap drops content overrides, but keeps an authored rotation or - flip as geometry so a later sync does not remove its retained placement." +(defn- keep-orientation-override + "A plain swap keeps its old orientation. When that differs from the new + component's expected placement, the swap authors a geometry override." [changes shape baseline page ldata component-id] (let [parent (get-in page [:objects (:parent-id shape)]) main (ctf/get-component-root ldata (ctkl/get-component ldata component-id)) @@ -425,7 +425,7 @@ (not= (boolean (:flip-x shape)) (flip :flip-x)) (not= (boolean (:flip-y shape)) (flip :flip-y)))] (cond-> changes - (and (ctk/touched-group? shape :geometry-group) changed?) + changed? (pcb/update-shapes [(:id shape)] #(update % :touched (fnil conj #{}) :geometry-group) {:ignore-touched true})))) @@ -454,7 +454,7 @@ [changes parents-of-swapped] (if keep-touched? (generate-keep-touched changes new-shape shape orig-shapes page libraries ldata) - [(keep-authored-orientation changes shape new-shape page ldata component-id) []]) + [(keep-orientation-override changes shape new-shape page ldata component-id) []]) ;; Replacement keeps the head id and its cell. Cleanup during the ;; temporary deletion must not change the parent's placement data. diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index 25debb2faf..f5e4df5d25 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -734,3 +734,15 @@ (t/is (mth/close? 0 (:rotation child) 0.001)) (t/is (= #{:swap-group} (:touched head))) (t/is (nil? (:touched child))))) + +(t/deftest plain-swap-keeps-the-orientation-inherited-from-its-old-component + (let [initial (-> (tm/make-situation (setup-sized)) + (r/step (n/rotate :star-main 45)) r/settle) + swapped (-> initial (r/step (n/swap-shape :card-star :heart)) r/settle) + head (tm/resolve-shape swapped :card-star) + file (tm/file swapped) + full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))] + (t/is (nil? (:touched (tm/resolve-shape initial :card-star)))) + (t/is (ctk/touched-group? head :geometry-group)) + (t/is (mth/close? 45 (:rotation head) 0.001)) + (t/is (= (orientation file :card-star) (orientation full :card-star)))))