🐛 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
This commit is contained in:
alonso.torres 2026-10-23 19:11:34 +02:00
parent a90c6d1d8c
commit 835138749b
2 changed files with 22 additions and 4 deletions

View File

@ -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

View File

@ -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))))))