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