🐛 Keep inherited orientation when pushing a swapped child

When update-main turns a swap into a plain nested copy, retain its
orientation as a geometry override if it differs from its own component's
expected parent placement. Otherwise a later full sync discards the
rotation inherited from the replaced position (F168).

The portable regression fails three assertions before the fix. Both
0-degree and 45-degree cases now pass, including exact undo. Fresh swap,
variant and flex suites pass 125 tests and 596 assertions. The pure golden
master matches all 1149 assertions without recording.

Benchmark ratios against Phase 1e: main 10/100/1000 copies 0.52/0.62/0.56,
three levels 0.48, chain 0.56, library 0.51, many shapes 0.43, variant
switch 0.60. No regression flags.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-24 09:38:55 +02:00
parent 357e8a7ff7
commit 5ad0744d0e
3 changed files with 44 additions and 2 deletions

View File

@ -72,3 +72,9 @@ take find-content-ref-shape at every link. A swapped intermediate head
follows its own main; collecting its replaced position's content overrides
creates phantom groups in copies of that swap (F165). Position advancement
still uses advance-shape-ref and find-ref-shape.
A swapped head pushed into a main can become a plain nested copy of its own
component. If the old position supplied rotation or flips that differ from
that component's expected parent placement, the new nested copy needs a
geometry-group override. Otherwise a later sync discards the orientation
that update-main just pushed (F168). The source copy clears its overrides.

View File

@ -734,6 +734,21 @@
{:type :reg-objects :shapes all-parents})
(map (comp del-obj-change :id) new-shapes))))
(defn- rehead-pushed-swap
"A pushed swap becomes a plain nested copy. Its old position's
orientation becomes authored when it differs from its own main."
[shape main container]
(let [shape (-> shape
(assoc :shape-ref (:id main))
(ctk/remove-swap-group))
main-container (when main
(main-in-parent-orientation (:container (meta main)) main
container shape false))
expected (get-in main-container [:objects (:id main)])]
(cond-> shape
(and expected (orient-like shape expected (gsh/shape->center shape)))
(update :touched ctk/set-touched-group :geometry-group))))
(defn- add-shape-to-main
"Add to the main shape `parent-main`, at `index`, a copy of `child-inst`,
a child of the copy shape that pairs with it; `child-inst` then refers to
@ -752,14 +767,17 @@
(seq (ctn/get-parent-copy-heads (:objects main-container) parent-main)))
replaced (when swap-in-copy?
(ctn/get-shape main-container (:shape-ref child-inst)))
content-main (when (and (ctk/swapped? child-inst) (not swap-in-copy?))
(ctf/find-content-ref-shape nil container libraries child-inst
:include-deleted? true
:with-context? true))
update-new-shape (fn [new-shape original-shape]
(cond-> (reposition-shape new-shape root-inst root-main)
(and (= (:id original-shape) (:id child-inst))
(ctk/swapped? child-inst)
(not swap-in-copy?))
(-> (assoc :shape-ref (swap-content-root-id libraries child-inst))
(ctk/remove-swap-group))))
(rehead-pushed-swap content-main main-container)))
;; the copy now follows the main, swap included
update-original-shape (fn [original-shape new-shape]

View File

@ -899,3 +899,21 @@
(mapv :id chain)))
(t/is (= #{:swap-group}
(ctf/get-touched-from-ref-chain-until-target-ref page libraries child nil)))))
(t/deftest update-main-authors-a-swaps-inherited-orientation-in-its-new-main
(doseq [angle [0 45]]
(let [initial (-> (tm/make-situation (setup-sized))
(r/step (n/rotate :card-star angle)) r/settle)
swapped (-> initial (r/step (n/swap-shape :copy-star :heart)) r/settle)
updated (-> swapped (r/step (n/update-main :copy)) r/settle)
file (tm/file updated)
head (tm/resolve-shape updated :copy-star)
main (tm/shape-by-id updated (:shape-ref head))
full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))]
(t/is (= (if (zero? angle) #{} #{:geometry-group}) (set (:touched main))))
(t/is (mth/close? angle (:rotation main) 0.001))
(t/is (mth/close? angle (:rotation (ths/get-shape-by-id full (:id main))) 0.001))
(t/is (gsh/close-attrs? :points (:points head)
(:points (ths/get-shape-by-id full (:id head))) 0.001))
(t/is (= (thn/normalize-file (tm/file swapped))
(thn/normalize-file (tm/file (r/step updated (n/undo)))))))))