diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 289b962da5..7a6b9389d0 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -30,7 +30,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w - 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. -- 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). +- 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 52d24b7daf..16f077e0ac 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -321,6 +321,31 @@ (let [new-touched (ctf/get-touched-from-ref-chain-until-target-ref container libraries shape nil)] (assoc shape :touched (disj new-touched :swap-group))))) +(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] + (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 + (- (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) + (boolean (get baseline key)) + (odd? (count (filter identity [(get baseline key) (get parent key) + (get main-parent key)]))))) + changed? (or (not (mth/close? (mod (or (:rotation shape) 0) 360) rotation 0.01)) + (not= (boolean (:flip-x shape)) (flip :flip-x)) + (not= (boolean (:flip-y shape)) (flip :flip-y)))] + (cond-> changes + changed? + (pcb/update-shapes [(:id shape)] + #(update % :touched (fnil conj #{}) :geometry-group) + {:ignore-touched true})))) + (defn generate-keep-touched "This is used as part of the switch process, when you switch from an original-shape to a new-shape. It generate changes to @@ -395,7 +420,12 @@ (keep-swapped-item changes new old ldata page libraries) (clsw/update-attrs-on-switch changes new old new-shape original-shape old-ref container))) changes - pairs)] + pairs) + changes (cond-> changes + (ctk/touched-group? original-shape :geometry-group) + (keep-orientation-override original-shape new-shape page + (or (get-in libraries [(:component-file new-shape) :data]) ldata) + (:component-id new-shape)))] [changes (mapv (comp :parent-id :new) pairs)])) (defn- shape-index @@ -405,31 +435,6 @@ (when-let [parent (get objects parent-id)] (or (d/index-of (:shapes parent) shape-id) 0))) -(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)) - 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 - (- (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) - (boolean (get baseline key)) - (odd? (count (filter identity [(get baseline key) (get parent key) - (get main-parent key)]))))) - changed? (or (not (mth/close? (mod (or (:rotation shape) 0) 360) rotation 0.01)) - (not= (boolean (:flip-x shape)) (flip :flip-x)) - (not= (boolean (:flip-y shape)) (flip :flip-y)))] - (cond-> changes - changed? - (pcb/update-shapes [(:id shape)] - #(update % :touched (fnil conj #{}) :geometry-group) - {:ignore-touched true})))) - (defn generate-component-swap-in-place "Swap the copy head `shape` of `page` for a copy of the component `component-id` of library data `ldata`. The new head takes the old one's diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index f5e4df5d25..9e50d71178 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -642,7 +642,10 @@ copy (ths/get-shape file' :copy01) child (ths/get-shape-by-id file' (first (:shapes copy)))] (t/is (== 30 (mth/precision (:rotation copy) 2))) - (t/is (== 30 (mth/precision (:rotation child) 2))))) + (t/is (== 30 (mth/precision (:rotation child) 2))) + (t/is (ctk/touched-group? copy :geometry-group)) + (t/is (= (orientation file' :copy01) + (orientation (propagate file' :c02) :copy01))))) (t/deftest switch-turns-the-swap-with-the-child-of-the-new-variant (let [file (-> (thf/sample-file :file1)