🐛 Remove the empty override field after clearing a swap

When removing the last swap group, drop touched instead of retaining an
empty set. A main clone then uses the same representation as set-touched;
a later move and undo can restore its exact prior state (F156).

The unit and move regressions fail before the change. The swap suite and
original shrunk program pass 48 tests and 184 assertions. Common lint and
format pass. Benchmark ratios: mains 0.49/0.56/0.79; levels 0.66; chain
0.82; library 0.78; many shapes 0.92; switch 1.02, no regression flags.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-23 20:59:56 +02:00
parent 980c71b645
commit bc4edefd81
3 changed files with 27 additions and 3 deletions

View File

@ -61,3 +61,8 @@ A variant container is a frame with `:is-variant-container true`. Its children a
Predicates are broad: `ctk/is-variant?` checks `:variant-id` and applies to both variant master shapes and component rows; `ctk/is-variant-container?` checks the container shape flag.
Moving/dropping a shape into a variant container through the move-to-frame path can auto-convert it into a variant via `generate-make-shapes-variant`, which may duplicate the underlying component. Treat drag/drop into variant containers as a component/variant operation, not a plain reparent.
- `ctk/remove-swap-group` removes `:touched` when it removes the final group.
New main clones must use the same empty-state representation as
`process-operation :set-touched`; otherwise a later move+undo changes
`:touched #{}` to a missing field and cannot restore the exact state (F156).

View File

@ -274,9 +274,12 @@
(defn remove-swap-group
"Make the shape not swapped."
[shape]
(cond-> shape
(swapped? shape)
(update :touched disj :swap-group)))
(if (swapped? shape)
(let [touched (disj (:touched shape) :swap-group)]
(if (seq touched)
(assoc shape :touched touched)
(dissoc shape :touched)))
shape))
(defn get-deleted-component-root
[component]

View File

@ -805,3 +805,19 @@
head (tm/resolve-shape swapped :copy-star)]
(t/is (mth/close? 45 (:rotation head) 0.001))
(t/is (= #{:swap-group} (:touched head)))))
(t/deftest removing-the-last-swap-group-leaves-no-empty-override-field
(let [shape {:touched #{:swap-group}}]
(t/is (not (contains? (ctk/remove-swap-group shape) :touched)))
(t/is (= #{:geometry-group}
(:touched (ctk/remove-swap-group (assoc shape :touched #{:swap-group :geometry-group})))))))
(t/deftest moving-a-swapped-subtree-pushed-into-a-main-undoes-exactly
(let [updated (-> (tm/make-situation (setup))
(r/step (n/update-main :copy)) r/settle)
file (tm/file updated)
head (tm/shape-by-id updated (:shape-ref (tm/resolve-shape updated :copy-star)))
moved (-> updated (r/step (n/nest-copy (constantly (:id head)) :star-main)) r/settle)
undone (r/step moved (n/undo))]
(t/is (nil? (:touched head)))
(t/is (= (thn/normalize-file file) (thn/normalize-file (tm/file undone))))))