From 90138f14c64382a85eec9dd5ea2c5e26c24cb023 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 23 Oct 2026 18:46:20 +0200 Subject: [PATCH] :bug: 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 --- common/src/app/common/logic/libraries.cljc | 57 ++++++++----------- .../logic/comp_expected_geometry_test.cljc | 13 +++++ 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index f7ac1c0561..5d56afd842 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -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] diff --git a/common/test/common_tests/logic/comp_expected_geometry_test.cljc b/common/test/common_tests/logic/comp_expected_geometry_test.cljc index 0fff701619..559a35b550 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -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))))