From 705dbd296e0516fea3d7c41cf9515461a7334ee3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 23 Oct 2026 09:40:49 +0200 Subject: [PATCH] :bug: Match component orientation with its relative transform Apply the relative orientation matrix directly. Large rotations also change flip flags, so applying flips before rotation can add 180 degrees. The text regression fails before this fix at 135 and 225 degrees. Validation: geometry suite, 4 tests / 50 assertions; existing golden cases and worlds, 1098 assertions. No existing golden digest changed. Benchmark lower-quantile ratios against Phase 1e: main 10/100/1000 copies 0.51/0.59/0.58; three levels 0.53; chain 0.61; library 0.53; many shapes 0.54; variant switch 0.70. No regression flag. AI-assisted-by: GPT-6 --- common/src/app/common/logic/libraries.cljc | 35 ++++++------- .../logic/comp_expected_geometry_test.cljc | 51 ++++++++++--------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 09683b4445..6468c86bc1 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -1401,26 +1401,23 @@ (dissoc :modifiers)))))))) (defn orient-like - "A function that turns each shape of the subtree of `head` around `center`, - so that `head` takes the flips and then the rotation of `target`; nil when - they already agree." + "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." [head target center] - (let [flip-x? (not= (true? (:flip-x head)) (true? (:flip-x target))) - flip-y? (not= (true? (:flip-y head)) (true? (:flip-y target))) - flip (when (or flip-x? flip-y?) - (ctm/resize-modifiers (gpt/point (if flip-x? -1 1) (if flip-y? -1 1)) center)) - head' (cond-> head (some? flip) (gsh/transform-shape flip)) - angle (- (d/nilv (:rotation target) 0) (d/nilv (:rotation head') 0)) - rotate? (not (mth/close? (mod angle 360) 0 0.01)) - angle (if (> angle 180) (- angle 360) angle)] - (when (or (some? flip) rotate?) - (fn [shape] - (cond-> shape - (some? flip) - (gsh/transform-shape flip) - - rotate? - (as-> shape (gsh/transform-shape shape (ctm/rotation-modifiers shape center angle)))))))) + (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))))))) (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 17bb6dac24..9a6f150a5d 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -11,6 +11,7 @@ through their constraints (decision (f) of the component contract)." (:require [app.common.files.changes-builder :as pcb] + [app.common.geom.matrix :as gmt] [app.common.geom.modifiers :as gm] [app.common.geom.shapes :as gsh] [app.common.logic.libraries :as cll] @@ -147,27 +148,29 @@ (t/deftest rotating-a-copy-with-text-in-a-detached-child-is-already-synced - (let [setup (fn [] - (tm/make-situation - (-> (thf/sample-file :file1) - (thtx/add-text-component-with-copy [[["Hello world"]]]) - (ths/update-shape :main-child :grow-type :auto-height) - (ths/update-shape :copy-child :grow-type :auto-height) - (ths/update-shape :copy-child :touched nil) - (tho/add-simple-component :outer :outer-root :outer-child - :root-params {:width 100 :height 100}) - (thc/instantiate-component :outer :outer-copy)))) - situation (r/run-variant - {:setup setup - :operation (tm/in-sequence [(n/nest-copy :copy-root :outer-root) - (n/detach :copy-root) - (n/rotate :outer-copy 45)])}) - file (tm/file situation) - text (first (filter #(= (thi/id :copy-child) (:shape-ref %)) - (vals (:objects (thf/current-page file))))) - full (:file (r/sync-cascade file {} (keys (get-in file [:data :components])))) - synced (ths/get-shape-by-id full (:id text))] - (t/is (some? text)) - (t/is (gsh/close-attrs? :points (:points text) (:points synced) 0.01)) - (t/is (mth/close? (:x text) (:x synced) 0.01)) - (t/is (mth/close? (:y text) (:y synced) 0.01)))) + (doseq [angle [45 135 225]] + (let [setup (fn [] + (tm/make-situation + (-> (thf/sample-file :file1) + (thtx/add-text-component-with-copy [[["Hello world"]]]) + (ths/update-shape :main-child :grow-type :auto-height) + (ths/update-shape :copy-child :grow-type :auto-height) + (ths/update-shape :copy-child :touched nil) + (tho/add-simple-component :outer :outer-root :outer-child + :root-params {:width 100 :height 100}) + (thc/instantiate-component :outer :outer-copy)))) + situation (r/run-variant + {:setup setup + :operation (tm/in-sequence [(n/nest-copy :copy-root :outer-root) + (n/detach :copy-root) + (n/rotate :outer-copy angle)])}) + file (tm/file situation) + text (first (filter #(= (thi/id :copy-child) (:shape-ref %)) + (vals (:objects (thf/current-page file))))) + full (:file (r/sync-cascade file {} (keys (get-in file [:data :components])))) + synced (ths/get-shape-by-id full (:id text))] + (t/is (some? text)) + (t/is (gmt/close? (:transform text) (:transform synced))) + (t/is (gsh/close-attrs? :points (:points text) (:points synced) 0.01)) + (t/is (mth/close? (:x text) (:x synced) 0.01)) + (t/is (mth/close? (:y text) (:y synced) 0.01)))))