diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 6e248324b7..0beb1bda6d 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -29,7 +29,8 @@ 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 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. +- 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. Orientation inherited from the position source or + the parent stays untouched, including references into deleted mains (F154). 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). The root also keeps its authored rotation or flip as a geometry override when swap placement retains it (F146). 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 7cc039984a..c887e3a4fb 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -324,16 +324,20 @@ (defn- keep-orientation-override "Retained orientation that differs from the new component's expected placement needs a geometry override to survive later sync." - [changes shape baseline page ldata component-id] + [changes shape baseline page ldata component-id position] (let [parent (get-in page [:objects (:parent-id shape)]) main (ctf/get-component-root ldata (ctkl/get-component ldata component-id)) main-parent (get-in (ctf/get-component-container ldata (ctkl/get-component ldata component-id)) [:objects (:parent-id main)]) - parent-delta (if (:shape-ref parent) 0 + baseline (or position baseline) + main-parent (if position + (get-in (meta position) [:container :objects (:parent-id position)]) + main-parent) + parent-delta (if (and (:shape-ref parent) (nil? position)) 0 (- (or (:rotation parent) 0) (or (:rotation main-parent) 0))) rotation (mod (+ (or (:rotation baseline) 0) parent-delta) 360) flip (fn [key] - (if (:shape-ref parent) + (if (and (:shape-ref parent) (nil? position)) (boolean (get baseline key)) (odd? (count (filter identity [(get baseline key) (get parent key) (get main-parent key)]))))) @@ -426,7 +430,7 @@ (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)))] + (:component-id new-shape) nil))] [changes (mapv (comp :parent-id :new) pairs)])) (defn- shape-index @@ -460,7 +464,10 @@ [changes parents-of-swapped] (if keep-touched? (generate-keep-touched changes new-shape shape orig-shapes page libraries ldata) - [(keep-orientation-override changes shape new-shape page ldata component-id) []]) + [(keep-orientation-override changes shape new-shape page ldata component-id + (when (:shape-ref parent) + (ctf/find-ref-shape nil page libraries shape + :include-deleted? true :with-context? true))) []]) ;; 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 5414bddf4e..66b0557dc8 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -797,3 +797,11 @@ (t/is (= (:shapes main) (mapv #(get-in objects [% :shape-ref]) (:shapes head)))) (t/is (= (thn/normalize-file (tm/file initial)) (thn/normalize-file (tm/file (r/step detached (n/undo)))))))) + +(t/deftest plain-swap-does-not-author-the-positions-inherited-orientation + (let [rotated (-> (tm/make-situation (setup-sized)) + (r/step (n/rotate :card-star 45)) r/settle) + swapped (-> rotated (r/step (n/swap-shape :copy-star :heart)) r/settle) + head (tm/resolve-shape swapped :copy-star)] + (t/is (mth/close? 45 (:rotation head) 0.001)) + (t/is (= #{:swap-group} (:touched head)))))