diff --git a/.serena/memories/common/component-sync-contract.md b/.serena/memories/common/component-sync-contract.md index 7a6b9389d0..6e248324b7 100644 --- a/.serena/memories/common/component-sync-contract.md +++ b/.serena/memories/common/component-sync-contract.md @@ -22,7 +22,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w - Edit on a copy shape (authored): sets the attr's group (text content: `:content-group` + the text groups it changes), clears `:remote-synced`. Descendants moved/resized only because of it stay untouched. - Rename: an explicit rename touches `:name-group` on root or child. Automatic renames (text name following its content) are derived: the editors write the name with `:ignore-touched #{:name}`, and sync gives an untouched copy text the name of its own letters when the main's name is automatic. Untouched names sync from the near main; a touched root name on update-main renames the component. - Edit on a main: no touched change on the main; triggers sync. -- Sync main→copy: every untouched group gets the near main's value (geometry: expected geometry); touched groups keep their values; never sets or clears `:touched`. Main child added → added clean to copies. Main child deleted → copy child removed, overrides or not. Main reorder → copies follow. A swapped sub-head pairs with the main child it replaces (its `:shape-ref`) and takes only that child's placement (position, rotation, flips, constraints, `layout-item-*`); everything else syncs from its own component's main. A main child deleted → its swapped copies go with it. A swap made in the main reaches copies without `:swap-group` and spares those with it. +- Sync main→copy: every untouched group gets the near main's value (geometry: expected geometry); touched groups keep their values; never sets or clears `:touched` during attribute sync. If a main detaches a nested head, its images become ordinary shapes and lose their now-invalid `:swap-group`. Main child added → added clean to copies. Main child deleted → copy child removed, overrides or not. Main reorder → copies follow. A swapped sub-head pairs with the main child it replaces (its `:shape-ref`) and takes only that child's placement (position, rotation, flips, constraints, `layout-item-*`); everything else syncs from its own component's main. A main child deleted → its swapped copies go with it. A swap made in the main reaches copies without `:swap-group` and spares those with it. - Sync cascade is eager: one sync runs to a fixpoint in memory (copies of A, including the A copy in B's main → copies of B → …) and commits once in the triggering op's undo group. The file is never left partly synced. Syncing only the components reported as changed equals a full-file sync. Code: `cll/generate-sync-file-cascade` (first pass as `generate-sync-file-changes`, then `generate-sync-components-cascade` over `cll/changed-components` of each pass); the frontend `sync-file` commits it to the synced file, and a library file's sync then syncs the current file's copies of the library components its cascade changed. - Reset: the selected head's subtree becomes equal to its near main (expected geometry); all its touched groups cleared, `:swap-group` included: a swapped head turns back into the near main's component in place, keeping its id. Nothing outside that subtree changes. - Update main (from a top copy head): the copy's differing values go to the main; the copy's touched cleared; other copies then sync and keep their own overrides. A swapped child replaces the main child it stands for, and neither keeps `:swap-group`. The main's authored groups are unioned with the copy's even on a repeated update; `:remote-synced` records an earlier update and must never clear those groups (F114). diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index edad15736b..6fbb120183 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -430,7 +430,8 @@ children-main {:pair? ctk/is-main-of? :on-pair (fn [changes child-inst child-main] - (if (and (not reset?) (ctk/swapped? child-inst)) + (if (and (not reset?) (ctk/swapped? child-inst) + (ctk/instance-head? child-main)) (update-swapped-placement changes ctx child-inst child-main) (sync-copy-recursive changes ctx child-inst child-main))) :only-inst (fn [changes child-inst] @@ -1033,7 +1034,9 @@ (if (and (= (:shape-ref dest-shape) (:id origin-shape)) (ctk/subcopy-head? dest-shape) (not (ctk/instance-head? origin-shape))) - (pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true}) + (pcb/update-shapes changes [(:id dest-shape)] + #(-> % ctk/remove-swap-group ctk/unhead-shape) + {:ignore-touched true}) changes)) (defn- check-swapped-main diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index 0072848e98..5414bddf4e 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -785,3 +785,15 @@ (t/is (mth/close? 80 (:height head) 0.001)) (t/is (= (thn/normalize-file (tm/file edited)) (thn/normalize-file (tm/file (r/step updated (n/undo)))))))) + +(t/deftest detaching-a-main-child-removes-the-copys-now-invalid-swap + (let [initial (tm/make-situation (setup)) + detached (-> initial (r/step (n/detach :card-star)) r/settle) + head (tm/resolve-shape detached :copy-star) + main (tm/resolve-shape detached :card-star) + objects (:objects (thf/current-page (tm/file detached)))] + (t/is (not (ctk/instance-head? head))) + (t/is (not (ctk/swapped? head))) + (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))))))))