🐛 Take nested swap constraints from their position source

Settle a nested swap's untouched constraint group from the replaced
position, including fixed-scroll. Carry compatible switch overrides first
so an inherited position cannot discard an authored constraint override.

Cover F224's reduced history in common and the async frontend, both
constraint axes, absent position fields, compatible carry and exact undo.
The complete original history and browser ShapeProxy replay also pass.

Validation: focused swap/variant/golden tests (123 tests, 1,764 assertions),
full common JVM/JS, frontend, backend and exporter suites; lint and format.
Golden data is unchanged.

Benchmark ratios against the unchanged baseline, with no regression flags:
mains 10/100/1,000: 0.53/0.58/0.63; three levels: 0.56; chain: 0.55;
library: 0.56; many shapes: 0.48; variant switch: 0.65.
The baseline uses another machine/JVM, so ratios are rough comparisons.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-28 00:06:38 +01:00
parent 11d7a630dc
commit ca97bae421
5 changed files with 89 additions and 2 deletions

View File

@ -343,6 +343,26 @@
(ctf/take-position file* libraries objects new-shape (:shape-ref shape) position)
{:ignore-touched true})))
(defn generate-sync-swap-constraints
"Give a nested swap the constraints of its position, after a switch has
carried any compatible overrides. Instantiation supplies content-main
constraints, which do not own the swapped head's placement."
[changes page libraries shape-id]
(let [objects (pcb/get-objects changes)
shape (get objects shape-id)
parent (get objects (:parent-id shape))
position (when (and (ctk/in-component-copy? parent)
(not (ctk/touched-group? shape :constraints-group)))
(ctf/find-ref-shape nil (assoc page :objects objects) libraries shape
:include-deleted? true))]
(cond-> changes
position
(pcb/update-shapes [shape-id]
#(d/patch-object % {:constraints-h (:constraints-h position)
:constraints-v (:constraints-v position)
:fixed-scroll (:fixed-scroll position)})
{:ignore-touched true}))))
(defn generate-new-shape-for-swap
[changes shape file page libraries id-new-component index target-cell keep-props-values]
(let [objects (:objects page)
@ -408,5 +428,9 @@
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true :ignore-flows-for #{(:id shape)}}))
[new-shape changes]
(-> changes
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))
;; A switch carries overrides before taking untouched constraints.
changes (cond-> changes
(not ignore-swapped?)
(generate-sync-swap-constraints page libraries (:id new-shape)))]
[new-shape all-parents changes]))

View File

@ -444,7 +444,8 @@
(some #(ctk/touched-group? % :geometry-group) orig-touched)
(cll/generate-sync-shape-direct
nil libraries (assoc container :objects (pcb/get-objects changes))
(:id new-shape) false))]
(:id new-shape) false))
changes (clsw/generate-sync-swap-constraints changes page libraries (:id new-shape))]
[changes (mapv (comp :parent-id :new) pairs)]))
(defn- shape-index

View File

@ -29,6 +29,12 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest swapping-a-copy-with-authored-position-constraints-stays-settled
(t/is (map? (cg/run {:world [2 1 {:library true :variants true :rich-variants true
:text true :text-styles true :layout :flex :layout-copies true}]
:ops [[:style 12 9 11 6] [:delete 10 14 10 12] [:swap 17 2 14 5]
[:undo] [:redo]]}))))
(t/deftest nesting-a-swapped-copy-in-a-blocked-main-authors-its-lock
(t/is (map? (cg/run {:world [2 0 {:library true :variants true :rich-variants true
:text true :text-styles true :layout nil :layout-copies true}]

View File

@ -652,6 +652,56 @@
(thc/make-component :card :card-main)
(thc/instantiate-component :card :copy :children-labels [:copy-star])))
(t/deftest swapping-a-nested-copy-takes-constraints-from-its-position
(let [initial (-> (tm/make-situation (setup-sized))
(r/step (n/change-property :card-star :constraints-h :right))
(r/step (n/change-property :card-star :constraints-v :bottom))
(r/step (n/change-property :card-star :fixed-scroll true))
r/settle
(r/step (n/change-property :copy-star :constraints-h :scale))
(r/step (n/change-property :copy-star :constraints-v :scale))
(r/step (n/change-property :copy-star :fixed-scroll false))
r/settle)
swapped (-> initial (r/step (n/swap-shape :copy-star :heart)) r/settle)
head (tm/resolve-shape swapped :copy-star)
placement #(select-keys % [:constraints-h :constraints-v :fixed-scroll])
synced (tm/with-file swapped (propagate (tm/file swapped) :card))
undone (r/step swapped (n/undo))
redone (r/step undone (n/redo))]
(t/is (= {:constraints-h :right :constraints-v :bottom :fixed-scroll true}
(placement head)))
(t/is (= #{:swap-group} (:touched head)))
(t/is (= (placement head) (placement (tm/resolve-shape synced :copy-star))))
(t/is (= (thn/normalize-file (tm/file initial))
(thn/normalize-file (tm/file undone))))
(t/is (= (thn/normalize-file (tm/file swapped))
(thn/normalize-file (tm/file redone))))))
(t/deftest swapping-a-nested-copy-clears-content-main-constraints-at-a-default-position
(let [initial (-> (tm/make-situation (setup-sized))
(r/step (n/change-property :heart-main :constraints-h :right))
(r/step (n/change-property :heart-main :constraints-v :bottom))
(r/step (n/change-property :heart-main :fixed-scroll true))
r/settle)
swapped (-> initial (r/step (n/swap-shape :copy-star :heart)) r/settle)
placement #(select-keys % [:constraints-h :constraints-v :fixed-scroll])]
(t/is (= (placement (tm/resolve-shape initial :card-star))
(placement (tm/resolve-shape swapped :copy-star))))
(t/is (= #{:swap-group} (:touched (tm/resolve-shape swapped :copy-star))))))
(t/deftest switching-a-nested-copy-retains-compatible-authored-constraints
(let [initial (-> (tm/make-situation (setup-sized))
(r/step (n/change-property :card-star :constraints-v :bottom))
r/settle
(r/step (n/change-property :copy-star :constraints-v :scale))
r/settle)
switched (-> initial (r/step (n/swap-shape :copy-star :heart :keep-touched? true)) r/settle)
head (tm/resolve-shape switched :copy-star)]
(t/is (= :scale (:constraints-v head)))
(t/is (= #{:swap-group :constraints-group} (:touched head)))
(t/is (= (thn/normalize-file (tm/file initial))
(thn/normalize-file (tm/file (r/step switched (n/undo))))))))
(defn- orientation
[file label]
(let [shape (ths/get-shape file label)]

View File

@ -88,6 +88,12 @@
rx/timer mock/timer-mock}
(await (replay-program program)))))
(t/deftest ^:async swapping-a-copy-with-authored-position-constraints-stays-settled
(await (replay {:world [2 1 {:library true :variants true :rich-variants true
:text true :text-styles true :layout :flex :layout-copies true}]
:ops [[:style 12 9 11 6] [:delete 10 14 10 12] [:swap 17 2 14 5]
[:undo] [:redo]]})))
(t/deftest ^:async accepting-a-library-does-not-record-equal-numeric-geometry
(await (replay {:world [2 1 {:library true :variants true}]
:ops [[:resize 10 2 2 10] [:library-sync 3 1 2 8]