From ca97bae4214dd70092f8ef40644a0779c620c85b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 28 Oct 2026 00:06:38 +0100 Subject: [PATCH] :bug: 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 --- common/src/app/common/logic/swap.cljc | 26 +++++++++- common/src/app/common/logic/variants.cljc | 3 +- .../logic/composable_gen_test.cljc | 6 +++ .../common_tests/logic/swap_group_test.cljc | 50 +++++++++++++++++++ .../composable_tests/generated_test.cljs | 6 +++ 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/common/src/app/common/logic/swap.cljc b/common/src/app/common/logic/swap.cljc index c20e45986a..7b70c9aac7 100644 --- a/common/src/app/common/logic/swap.cljc +++ b/common/src/app/common/logic/swap.cljc @@ -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])) diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index ed055571a2..4aedb15c13 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -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 diff --git a/common/test/common_tests/logic/composable_gen_test.cljc b/common/test/common_tests/logic/composable_gen_test.cljc index 8ad1b6867b..e2571701e1 100644 --- a/common/test/common_tests/logic/composable_gen_test.cljc +++ b/common/test/common_tests/logic/composable_gen_test.cljc @@ -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}] diff --git a/common/test/common_tests/logic/swap_group_test.cljc b/common/test/common_tests/logic/swap_group_test.cljc index b6709425f9..9820cd04a0 100644 --- a/common/test/common_tests/logic/swap_group_test.cljc +++ b/common/test/common_tests/logic/swap_group_test.cljc @@ -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)] diff --git a/frontend/test/frontend_tests/composable_tests/generated_test.cljs b/frontend/test/frontend_tests/composable_tests/generated_test.cljs index 6bf3356baa..66c8550fc0 100644 --- a/frontend/test/frontend_tests/composable_tests/generated_test.cljs +++ b/frontend/test/frontend_tests/composable_tests/generated_test.cljs @@ -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]