🐛 Keep nested copies in a detached parent's orientation during sync

Express the source subtree in its ordinary parent's orientation before
syncing a surviving nested copy. Detaching a rotated outer instance no
longer makes a later main edit turn the nested copy back. Fix F131.

The focused regression failed on points and rotation before the fix.
Geometry/reset suites: 19 tests, 111 assertions, all green.
Benchmark ratios against Phase 1e: main 10/100/1000 0.46/0.52/0.53;
three levels 0.47; chain 0.51; library 0.51; many shapes 0.49;
variant switch 0.61. No regression flag.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-23 14:52:04 +02:00
parent 1ed6476a53
commit 6831417993
2 changed files with 42 additions and 3 deletions

View File

@ -260,6 +260,27 @@
:include-deleted? true))]
(recur shape-inst' next-data component')))))))
(defn- main-in-parent-orientation
"Place a main in the orientation of an ordinary instance parent. Such a
parent can survive detach, while the nested copy stays untouched."
[main-container shape-main container shape-inst]
(let [parent (ctn/get-shape container (:parent-id shape-inst))
main-parent (get-in main-container [:objects (:parent-id shape-main)])
relative (gmt/multiply (or (:transform parent) (gmt/matrix))
(or (:transform-inverse main-parent) (gmt/matrix)))]
(if (or (nil? parent) (:shape-ref parent) (gmt/unit? relative))
main-container
(let [transform (gmt/transform-in (gsh/shape->center shape-main) relative)
angle (- (or (:rotation parent) 0) (or (:rotation main-parent) 0))
turn #(-> %
(assoc-in [:modifiers :rotation] angle)
(gsh/apply-transform transform)
(dissoc :modifiers))]
(update main-container :objects
(fn [objects]
(reduce #(update %1 %2 turn) objects
(cfh/get-children-ids-with-self objects (:id shape-main)))))))))
(defn- generate-sync-shape-direct
"Generate changes to synchronize one shape that is the root of a component
instance, and all its children, from the given component."
@ -275,11 +296,14 @@
(let [shape-main (if reset?
;; the reset is against the ref-shape, not against the original shape of the component
(ctf/find-ref-shape file container libraries shape-inst :include-deleted? true)
(ctf/get-content-ref-shape library component shape-inst))]
(ctf/get-content-ref-shape library component shape-inst))
main-container (find-main-container container shape-inst shape-main
library file libraries component)
main-container (main-in-parent-orientation main-container shape-main container shape-inst)
shape-main (ctn/get-shape main-container (:id shape-main))]
(sync-copy-recursive changes
{:container container
:main-container (find-main-container container shape-inst shape-main
library file libraries component)
:main-container main-container
:component component
:library library
:root-inst shape-inst

View File

@ -21,6 +21,7 @@
[app.common.test-helpers.components :as thc]
[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]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
@ -34,6 +35,20 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest a-nested-copy-keeps-its-detached-parents-orientation-on-sync
(let [detached (-> (setup/nested-component-with-copy)
(r/step (n/rotate :copy-root 90)) r/settle
(r/step (n/detach :copy-root)) r/settle)
head (tm/role-shape detached :copy-instance)
id (first (:shapes head))
before (tm/shape-by-id detached id)
synced (-> detached (r/step (n/change-property :main1-child :fills "#ff0000")) r/settle)
after (tm/shape-by-id synced id)]
(t/is (= "#ff0000" (-> after :fills first :fill-color)))
(t/is (gsh/close-attrs? :points (:points before) (:points after) 0.001))
(t/is (= (:rotation before) (:rotation after)))
(t/is (nil? (:touched after)))))
(defn- component-with-child
"A 100x100 main board with a 20x20 rect at (70, 10) of it, constrained
`constraints-h`, and a copy of it at (300, 0)."