🐛 Compose child flip flags from the roots during geometry sync

Keep the roots' relative flip state when placing the source subtree.
Several small rotations must give the same child flags after a full sync;
recomputing one large turn could toggle both axes. Fix F139.

The direct regression failed nil/nil to true/true before the fix. Focused
geometry and oracle suites pass 20 tests, 88 assertions.
Benchmark ratios against Phase 1e: main 10/100/1000 0.57/0.64/0.60;
three levels 0.67; chain 0.56; library 0.60; many shapes 0.50;
variant switch 0.64. No regression flag.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-23 18:46:20 +02:00
parent 67550ebb5c
commit 90138f14c6
2 changed files with 38 additions and 32 deletions

View File

@ -1407,42 +1407,35 @@
shape (gsh/move shape delta)]
(if-not (ctk/touched-group? dest-root :geometry-group)
shape
(let [origin-transform (d/nilv (:transform origin-root) (gmt/matrix))
dest-transform (d/nilv (:transform dest-root) (gmt/matrix))
rel-transform (gmt/multiply dest-transform (gmt/inverse origin-transform))]
(if ^boolean (gmt/unit? rel-transform)
shape
;; The roots differ in rotation/flips: rotate the whole (already moved)
;; shape around the dest root center by the roots' relative transform.
;; The :rotation attribute delta is fed through the :modifiers path so
;; apply-transform keeps it consistent with the resulting matrix.
(let [center (grc/rect->center (:selrect dest-root))
rel-rotation (mod (- (d/nilv (:rotation dest-root) 0)
(d/nilv (:rotation origin-root) 0))
360)]
(-> shape
(assoc-in [:modifiers :rotation] rel-rotation)
(gsh/apply-transform (gmt/transform-in center rel-transform))
(dissoc :modifiers))))))))
(if-let [turn (orient-like origin-root dest-root
(grc/rect->center (:selrect dest-root)))]
(turn shape)
shape))))
(defn orient-like
"Turn the subtree of `head` around `center` to match `target`'s matrix.
Flip flags can change during a rotation, so they cannot be applied as
separate transforms before that rotation."
Compose flip flags with the roots, independent of rotation step size."
[head target center]
(when-not (and (= (:transform head) (:transform target))
(mth/close? (or (:rotation head) 0) (or (:rotation target) 0) 0.01))
(let [relative (gmt/multiply (or (:transform target) (gmt/matrix))
(or (:transform-inverse head) (gmt/matrix)))
transform (gmt/transform-in center relative)
turned (gsh/apply-transform head transform)
angle (- (or (:rotation target) 0) (or (:rotation turned) 0))]
(when (or (not (gmt/unit? relative)) (not (mth/close? (mod angle 360) 0 0.01)))
(fn [shape]
(-> shape
(assoc-in [:modifiers :rotation] angle)
(gsh/apply-transform transform)
(dissoc :modifiers)))))))
(let [flip-x? (not= (boolean (:flip-x head)) (boolean (:flip-x target)))
flip-y? (not= (boolean (:flip-y head)) (boolean (:flip-y target)))]
(when-not (and (= (:transform head) (:transform target))
(mth/close? (or (:rotation head) 0) (or (:rotation target) 0) 0.01)
(not flip-x?) (not flip-y?))
(let [relative (gmt/multiply (or (:transform target) (gmt/matrix))
(or (:transform-inverse head) (gmt/matrix)))
transform (gmt/transform-in center relative)
turned (gsh/apply-transform head transform)
angle (- (or (:rotation target) 0) (or (:rotation turned) 0))]
(when (or (not (gmt/unit? relative)) (not (mth/close? (mod angle 360) 0 0.01))
flip-x? flip-y?)
(fn [shape]
(let [flip-x (cond-> (:flip-x shape) flip-x? not)
flip-y (cond-> (:flip-y shape) flip-y? not)]
(-> shape
(assoc-in [:modifiers :rotation] angle)
(gsh/apply-transform transform)
(dissoc :modifiers)
(assoc :flip-x flip-x :flip-y flip-y)))))))))
(defn- maybe-sync
[c enabled? done? f]

View File

@ -200,3 +200,16 @@
file (tm/file moved)
full (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))]
(t/is (= (thn/normalize-file file) (thn/normalize-file full)))))
(t/deftest full-sync-keeps-child-flips-after-several-small-root-rotations
(let [situation (-> (setup/nested-component-with-copy)
(r/step (n/rotate :copy-root 45)) r/settle
(r/step (n/rotate :copy-root 45)) r/settle
(r/step (n/rotate :copy-root 45)) r/settle)
file (tm/file situation)
head (tm/role-shape situation :copy-instance)
child (tm/shape-by-id situation (first (:shapes head)))
synced (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))
after (get-in (thf/current-page synced) [:objects (:id child)])]
(t/is (= (select-keys child [:flip-x :flip-y]) (select-keys after [:flip-x :flip-y])))
(t/is (gsh/close-attrs? :points (:points child) (:points after) 0.001))))