From b86ca2f7fd750ab4723ff2ec3b9396f2b6eb95a3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 23 Oct 2026 09:00:26 +0200 Subject: [PATCH] :bug: Keep intersections accurate for short perpendicular lines An absolute determinant threshold classified short perpendicular lines as parallel. Constraint propagation then displaced text in a rotated nested frame by hundreds of pixels. Compare the determinant relative to both line lengths, retaining the existing parallel-line fallback. The component regression failed on points and x/y before the fix. It now passes, as do direct intersections across sizes and positions, the geometry tests, and existing composable scenarios. Sync benchmark ratios against the 1e baseline (lower quantile): main 10/100/1000 copies 0.47/0.63/0.80; three levels 0.56; chain 0.48; library 0.50; many shapes 0.45; variant switch 0.56. No regression flags. AI-assisted-by: GPT-6 --- .../src/app/common/geom/shapes/intersect.cljc | 5 ++- .../geom_shapes_intersect_test.cljc | 7 +++++ .../logic/comp_expected_geometry_test.cljc | 31 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/common/src/app/common/geom/shapes/intersect.cljc b/common/src/app/common/geom/shapes/intersect.cljc index f63fc7f740..994846051a 100644 --- a/common/src/app/common/geom/shapes/intersect.cljc +++ b/common/src/app/common/geom/shapes/intersect.cljc @@ -387,7 +387,10 @@ ;; Cramer's rule det (- (* a1 b2) (* a2 b1)) - det (if (mth/almost-zero? det) 0.001 det) + ;; The determinant scales with both line lengths. Short perpendicular + ;; lines are not parallel just because their determinant is small. + scale (* (mth/hypot a1 b1) (mth/hypot a2 b2)) + det (if (or (zero? scale) (mth/almost-zero? (/ det scale))) 0.001 det) x (/ (- (* b2 c1) (* b1 c2)) det) y (/ (- (* c2 a1) (* c1 a2)) det)] diff --git a/common/test/common_tests/geom_shapes_intersect_test.cljc b/common/test/common_tests/geom_shapes_intersect_test.cljc index ee1c082534..d3e2ae2905 100644 --- a/common/test/common_tests/geom_shapes_intersect_test.cljc +++ b/common/test/common_tests/geom_shapes_intersect_test.cljc @@ -270,3 +270,10 @@ (t/testing "unrotated shape uses the axis-aligned box" (t/is (true? (gint/has-point? shape (pt 50 40)))) (t/is (false? (gint/has-point? shape (pt 200 40))))))) + +(t/deftest short-perpendicular-lines-still-meet-at-their-crossing + (doseq [size [0.0005 0.005 1 100] + [x y] [[0 0] [200 200] [10000 -10000]]] + (let [actual (gint/line-line-intersect (pt (- x size) y) (pt (+ x size) y) + (pt x (- y size)) (pt x (+ y size)))] + (t/is (gpt/close? (pt x y) actual))))) 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 ac07bdefed..17bb6dac24 100644 --- a/common/test/common_tests/logic/comp_expected_geometry_test.cljc +++ b/common/test/common_tests/logic/comp_expected_geometry_test.cljc @@ -12,16 +12,21 @@ (:require [app.common.files.changes-builder :as pcb] [app.common.geom.modifiers :as gm] + [app.common.geom.shapes :as gsh] [app.common.logic.libraries :as cll] [app.common.logic.placement :as clp] [app.common.logic.shapes :as cls] [app.common.math :as mth] [app.common.test-helpers.components :as thc] + [app.common.test-helpers.composable.comp.nodes :as n] + [app.common.test-helpers.composable.comp.runner :as r] + [app.common.test-helpers.composable.core :as tm] [app.common.test-helpers.compositions :as tho] [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.normalize :as thn] [app.common.test-helpers.shapes :as ths] + [app.common.test-helpers.texts :as thtx] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] [clojure.test :as t])) @@ -140,3 +145,29 @@ (t/is (nil? (:content-scale (ths/get-shape reset :copy2-root)))) (t/is (= (thn/normalize-file changed) (thn/normalize-file undone))))) + +(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))))