🐛 Settle swapped placement when updating a turned main

Updating a rotated copy also turns its main. A swapped descendant whose
content and position sizes differ needs its placement settled in that new
frame. Append the main-file placement changes to the inverse sync, keeping
source-copy changes local and restoring both files in one undo group.

Portable local/library regressions at 45 and 90 degrees fail eight
assertions before the fix and pass all 20 after it. The original seed712
case4855 and its exact shrink pass, as does the 4855..5000 tail diagnostic.
The diagnostic does not replace the full 5000-program swap gate.

Common JVM1851/30684 and JS1782/29308, frontend1539/9312,
backend1610/8104, exporter14/32 and all 68 plugin cases pass.
Focused suites and unchanged goldens pass; lint and formatting pass.
Live WASM at 45/90 degrees keeps placement stable through undo/redo and a
later main edit. Maximum anchor error is below 0.00001px.

Benchmark ratios against the recorded baseline:
main 10/100/1000: 0.49/0.56/0.54; three levels: 0.49;
chain: 0.49; library: 0.50; many shapes: 0.45; switch: 0.58.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-25 13:35:45 +01:00
parent a568b1d6cf
commit 2099084f0b
3 changed files with 72 additions and 1 deletions

View File

@ -78,3 +78,10 @@ Reset drops the root's geometry override from the whole sync context
only while updating existing shapes can turn a newly added child by the
old override after its parent has already reset. The reset and replacement
children share one undo group and stay stable through later sync.
Update-main also settles untouched swapped heads carried through a parent
turn in the updated main (F186). A rigid rotation can leave the wrong
anchor when the replaced position and swap content have different sizes.
Apply the main's nonlocal changes to its own file, settle changed placement,
and append those changes to the same inverse-sync group. Keep the source
copy's local changes routed separately, including for linked libraries.

View File

@ -519,6 +519,24 @@
(pcb/with-library-data library-data)
(pcb/update-component id #(assoc % :path path :name name)))))
(defn- settle-updated-main-placement
[changes libraries component-file component]
(let [file (get libraries component-file)
container (ctf/get-component-container (:data file) component)
objects (:objects container)
ids (filter #(ctk/swapped? (get objects %))
(cfh/get-children-ids objects (:main-instance-id component)))]
(if (seq ids)
(let [redo (into [] (remove :local-change?) (:redo-changes changes))
file (update file :data cfc/process-changes redo false)
page (ctf/get-component-container (:data file) component)
placement (-> (pcb/empty-changes)
(pcb/with-container page)
(pcb/with-objects (:objects page))
(generate-sync-transformed-placement file libraries page objects ids))]
(pcb/concat-changes changes placement))
changes)))
(defn generate-sync-shape-inverse
"Generate changes to update the component a shape is linked to, from
the values in the shape and all its children."
@ -545,7 +563,8 @@
:synced-ids synced-ids}
shape-inst
(ctf/find-remote-shape container libraries shape-inst))
(mark-local-changes @synced-ids)))
(mark-local-changes @synced-ids)
(settle-updated-main-placement libraries (:component-file shape-inst) component)))
changes)))
(defn split-local-changes

View File

@ -20,6 +20,7 @@
[app.common.logic.shapes :as cls]
[app.common.math :as mth]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.composable.comp.files :as cf]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
@ -36,6 +37,50 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest updating-a-turned-copy-settles-swapped-placement-in-the-main
(doseq [remote? [false true]
angle [45 90]]
(let [file (-> (thf/sample-file :file)
(tho/add-simple-component :a :main-a :rect-a
:root-params {:width 100 :height 100})
(tho/add-simple-component :other :main-other :rect-other
:root-params {:width 100 :height 100})
(tho/add-frame :main-b {:width 200 :height 200})
(thc/instantiate-component :a :b-a :parent-label :main-b)
(thc/make-component :b :main-b)
(tho/add-frame :main-c {:width 300 :height 300})
(thc/instantiate-component :b :c-b :parent-label :main-c
:children-labels [:c-a])
(thc/make-component :c :main-c))
prepared (-> (tm/make-situation file)
(r/step (n/change-height :b-a 80)) r/settle
(r/step (n/swap-shape :c-a :other)) r/settle)
library (tm/file prepared)
initial (if remote?
(-> (tm/make-situation
(thc/instantiate-component (thf/sample-file :local) :c :copy-c
:library library :children-labels [:copy-b :copy-a]))
(tm/with-aux-files {(:id library) library}))
(tm/with-file prepared
(thc/instantiate-component library :c :copy-c
:children-labels [:copy-b :copy-a])))
before (-> initial (r/step (n/rotate :copy-c angle)) r/settle)
updated (-> before (r/step (n/update-main :copy-c)) r/settle)
main-file (if remote? (get (tm/aux-files updated) (:id library)) (tm/file updated))
head (ths/get-shape main-file :c-a)
full (:situation (cf/sync-world updated (keys (get-in main-file [:data :components]))))
full-main (if remote? (get (tm/aux-files full) (:id library)) (tm/file full))
full-head (ths/get-shape-by-id full-main (:id head))]
(t/is (mth/close? angle (:rotation head) 0.001))
(t/is (= #{:swap-group} (:touched head)))
(t/is (gsh/close-attrs? :points (:points full-head) (:points head) 0.001))
(t/is (gsh/close-attrs? :points
(:points (tm/resolve-shape full :copy-a))
(:points (tm/resolve-shape updated :copy-a)) 0.001))
(t/is (= (into {} (map (fn [[id f]] [id (thn/normalize-file f)])) (cf/all-files before))
(into {} (map (fn [[id f]] [id (thn/normalize-file f)]))
(cf/all-files (r/step updated (n/undo)))))))))
(t/deftest resetting-a-turned-copy-places-replacement-children-in-the-main-frame
(doseq [angle [45 90]]
(let [file (-> (thf/sample-file :file)