From 835138749b30c906c53eaf118b5da561fef555fd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 23 Oct 2026 19:11:34 +0200 Subject: [PATCH] :bug: Ignore dot product roundoff when detecting shape flips A right-angle turn can produce a tiny negative dot product. Compare it relative to the axis lengths before treating the turn as a reflection. Red regression reproduced at small, normal and large sizes. Focused geometry, reset, swap and runner suites pass 121 tests / 540 assertions. Benchmark vs Phase 1e: main 10/100/1000 0.51/0.64/0.65; levels 0.50; chain 0.57; library 0.53; many shapes 0.47; switch 0.59. No flags. AI-assisted-by: GPT-6 --- common/src/app/common/geom/shapes/transforms.cljc | 15 +++++++++++---- common/test/common_tests/geom_shapes_test.cljc | 11 +++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index 7ba56f2197..d0cd0614aa 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -297,19 +297,26 @@ yv1 (gpt/to-vec p0' (nth points' 3)) yv2 (gpt/to-vec p0 (nth points 3)) - dot-y (gpt/dot yv1 yv2)] + dot-y (gpt/dot yv1 yv2) + + ;; A right-angle turn has a zero dot product. Coordinate roundoff + ;; must not turn that into a reflection, regardless of shape size. + flip-x? (and (neg? dot-x) + (not (mth/close? dot-x 0 (* 1e-10 (gpt/length xv1) (gpt/length xv2))))) + flip-y? (and (neg? dot-y) + (not (mth/close? dot-y 0 (* 1e-10 (gpt/length yv1) (gpt/length yv2)))))] (cond-> shape - (neg? dot-x) + flip-x? (update :flip-x not) - (neg? dot-y) + flip-y? (update :flip-y not) ;; Negate rotation only when an odd number of axes are flipped, ;; since flipping both axes is equivalent to a 180° rotation and ;; two negations would cancel each other out. - (not= (neg? dot-x) (neg? dot-y)) + (not= flip-x? flip-y?) (update :rotation -)))) (defn- apply-transform-move diff --git a/common/test/common_tests/geom_shapes_test.cljc b/common/test/common_tests/geom_shapes_test.cljc index e60ded0fe7..7a3023522b 100644 --- a/common/test/common_tests/geom_shapes_test.cljc +++ b/common/test/common_tests/geom_shapes_test.cljc @@ -271,3 +271,14 @@ (t/is (true? (:flip-y result))) ;; rotation must not be negated when both axes are flipped (t/is (mth/close? 30 (:rotation result)))))) + +(t/deftest a-right-angle-turn-does-not-invent-a-flip-from-roundoff + (doseq [size [0.01 100 1000000]] + (let [shape (create-test-shape :rect {:width size :height size :rotation 90}) + shape (assoc shape :points [(gpt/point size 0) (gpt/point size size) + (gpt/point 0 size) (gpt/point 0 (* size 1e-14))]) + turn (gmt/transform-in (gpt/point (/ size 2) (/ size 2)) (gmt/rotate-matrix 90)) + after (gsh/apply-transform (assoc-in shape [:modifiers :rotation] 90) turn)] + (t/is (not (:flip-x after))) + (t/is (not (:flip-y after))) + (t/is (mth/close? 180 (:rotation after))))))