🐛 Author pushed content against its new nested reference

When a pushed swap takes a position with the same component, preserve
content differences as overrides on the promoted head and descendants.
The source copy clears its overrides and follows the promoted subtree.

Portable regression: three failures before the fix; now passes, including
full sync and exact undo. Focused JVM suites: 121 tests, 638 assertions.

Component benchmark ratios: mains 0.61/0.78/0.69; levels 0.53;
chain 0.50; library 0.56; many shapes 0.75; variant switch 0.61.
All worlds pass the baseline performance gate.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-24 14:22:09 +02:00
parent c36d15db20
commit 337b1230b6
2 changed files with 66 additions and 5 deletions

View File

@ -755,6 +755,30 @@
(and expected (orient-like shape expected (gsh/shape->center shape)))
(update :touched ctk/set-touched-group :geometry-group))))
(defn- author-pushed-content
"Taking a new content reference makes the pushed differences authored.
Placement still follows the position, and existing overrides survive."
[shape reference]
(if (or (nil? reference) (ctk/swapped? shape))
shape
(let [text? (cfh/text-shape? shape)
groups (into #{}
(keep (fn [attr]
(let [group (ctk/resolve-sync-group (:type shape) attr)]
(when (and group
(not (contains? placement-groups group))
(not= attr :position-data)
(not (and text? (= attr :content)))
(not= (get shape attr) (get reference attr)))
group))))
updatable-attrs)
text-groups (when text?
(txt/override-groups (:content reference) (:content shape)))
groups (cond-> (into groups text-groups)
(seq text-groups) (conj :content-group))]
(cond-> shape
(seq groups) (update :touched (fnil into #{}) groups)))))
(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
@ -817,11 +841,15 @@
new-shapes
(if swap-in-copy?
(let [objects (d/index-by :id new-shapes)
position (ctf/find-ref-shape nil main-container libraries replaced
:include-deleted? true
:with-context? true)]
(mapv (ctf/take-position nil libraries objects new-head (:shape-ref replaced) position)
(let [objects (d/index-by :id new-shapes)
position (ctf/find-ref-shape nil main-container libraries replaced
:include-deleted? true
:with-context? true)
take-position (ctf/take-position nil libraries objects new-head (:shape-ref replaced) position)
position-objects (get-in (meta position) [:container :objects])]
(mapv (fn [shape]
(let [shape (take-position shape)]
(author-pushed-content shape (get position-objects (:shape-ref shape)))))
new-shapes))
new-shapes)

View File

@ -933,3 +933,36 @@
(: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))))))))
(t/deftest pushing-a-swap-authors-content-that-differs-from-its-new-position
(let [file (-> (setup)
(tho/add-frame :page-main)
(thc/instantiate-component :card :inner-copy
:parent-label :page-main
:children-labels [:inner-star])
(thc/make-component :page :page-main)
(thc/instantiate-component :page :outer-copy
:children-labels [:outer-inner :outer-star]))
swapped (-> (tm/make-situation file)
(r/step (n/swap-shape :outer-star :heart)) r/settle
(r/step (n/swap-shape :card-star :heart)) r/settle)
child-id (first (:shapes (tm/resolve-shape swapped :card-star)))
edited (-> swapped
(r/step (n/change-property :card-star :fills "#333333")) r/settle
(r/step (n/change-property (constantly child-id) :fills "#444444")) r/settle)
updated (-> edited (r/step (n/update-main :outer-copy)) r/settle)
head (tm/resolve-shape updated :outer-star)
main (tm/shape-by-id updated (:shape-ref head))
main-child (tm/shape-by-id updated (first (:shapes main)))
file (tm/file updated)
full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))]
(t/is (ctk/swapped? (tm/resolve-shape edited :outer-star)))
(t/is (= (thi/id :card-star) (:shape-ref main)))
(t/is (= #{:fill-group} (:touched main) (:touched main-child)))
(t/is (= (:fills (tm/resolve-shape edited :outer-star)) (:fills main)))
(t/is (= "#222222" (get-in main-child [:fills 0 :fill-color])))
(t/is (empty? (:touched head)))
(t/is (= (:fills main) (:fills (ths/get-shape-by-id full (:id main)))))
(t/is (= (:fills main-child) (:fills (ths/get-shape-by-id full (:id main-child)))))
(t/is (= (thn/normalize-file (tm/file edited))
(thn/normalize-file (tm/file (r/step updated (n/undo))))))))