🐛 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
This commit is contained in:
alonso.torres 2026-10-23 09:00:26 +02:00
parent 2b31f88797
commit b86ca2f7fd
3 changed files with 42 additions and 1 deletions

View File

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

View File

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

View File

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