✨ Pin that a deleted component keeps passing changes on

When a component nested in others is deleted (inner in mid in outer,
mid deleted), a change to inner still reaches mid's stored shapes,
mid's copies, outer and outer's copies, and restoring mid brings back
an up-to-date main. Keep this behavior: outer is a live component, and
its users cannot see a deleted mid between it and inner.

Extend the deleted-component propagation test with a copy of the
deleted component and its restore, and reword the contract, which said
copies of a deleted component do not sync.

AI-assisted-by: claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alonso.torres 2026-10-13 13:56:29 +02:00
parent c7a40e525c
commit 78ef2dffe6
2 changed files with 32 additions and 6 deletions

View File

@ -31,7 +31,7 @@ Intended semantics of copies, `:touched` and component sync. This is the spec: w
- 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 is clean and keeps `swap-keep-attrs` values; the new head keeps the old shape's id. 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.
- Delete component: copies keep their data and do not sync. Restore brings sync back.
- 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.
## Geometry ownership

View File

@ -89,17 +89,37 @@
(tho/add-frame :frame-comp-6)
(thc/instantiate-component :comp-5 :copy-comp-5 :parent-label :frame-comp-6 :children-labels [:comp-4-comp-5])
(thc/make-component :comp-6 :frame-comp-6)))
(thc/make-component :comp-6 :frame-comp-6)
(thc/instantiate-component :comp-5 :loose-copy-comp-5)))
(propagate-all-component-changes [file]
(-> file
(tho/propagate-component-changes :comp-4)
(tho/propagate-component-changes :comp-5)))
(stored-fill-color [file]
(let [component (thc/get-component file :comp-5 :include-deleted? true)]
(when (:deleted component)
(->> (vals (:objects component))
(some #(when (= :rect (:type %))
(-> % :fills first :fill-color)))))))
(fill-colors [file]
[(tho/bottom-fill-color file :frame-comp-4)
(tho/bottom-fill-color file :frame-comp-5)
(tho/bottom-fill-color file :frame-comp-6)])
(tho/bottom-fill-color file :frame-comp-6)
(tho/bottom-fill-color file :loose-copy-comp-5)])
(restore-comp-5 [file]
(let [page (thf/current-page file)]
(thf/apply-changes file
(cll/generate-restore-component (pcb/empty-changes)
(:data file)
(thi/id :comp-5)
(:id file)
page
(:objects page)))))
(validate [file validator]
(validator file)
@ -108,9 +128,15 @@
(-> (setup)
;; Delete Comp5.
(tho/delete-shape :frame-comp-5)
(validate #(t/is (= (fill-colors %) ["#b1b2b5" nil "#b1b2b5"])))
(validate #(t/is (= (fill-colors %) ["#b1b2b5" nil "#b1b2b5" "#b1b2b5"])))
;; Change the color of Comp4
;; Desired result: Comp6 change color
;; Desired result: the deleted Comp5 keeps passing the change on, to
;; its stored main, its copy and Comp6
(tho/update-bottom-color :frame-comp-4 "#FF0000" :propagate-fn propagate-all-component-changes)
(validate #(t/is (= (fill-colors %) ["#FF0000" nil "#FF0000"]))))))
(validate #(t/is (= (fill-colors %) ["#FF0000" nil "#FF0000" "#FF0000"])))
(validate #(t/is (= (stored-fill-color %) "#FF0000")))
;; Restore Comp5: its main comes back up to date
(restore-comp-5)
(validate #(t/is (= (fill-colors %) ["#FF0000" "#FF0000" "#FF0000" "#FF0000"]))))))