🐛 Fix crash with degenerate selrect in text pipeline (#10618)

Guard remaining text pipeline locations that accessed :selrect directly
against nil/zero-dimension selrects by using safe-size-rect, which
provides a 4-level fallback chain (selrect -> points -> shape fields ->
empty 0.01x0.01 rect).

Fixes:
- fix-position in viewport_texts_html.cljs: replaced dm/get-prop
  :selrect with ctm/safe-size-rect for both old and new shape
- assoc-position-data in modifiers.cljs: replaced (:selrect ...)
  with ctm/safe-size-rect for delta computation
- change-orientation-modifiers in modifiers.cljc: replaced raw
  :selrect access with safe-size-rect for scale and origin computation

Closes #10617

AI-assisted-by: mimo-v2.5-pro
This commit is contained in:
Andrey Antukh 2026-07-09 11:03:06 +02:00 committed by GitHub
parent 47a3158602
commit 28fd798c52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 71 additions and 6 deletions

View File

@ -500,9 +500,9 @@
shape-transform (:transform shape)
shape-transform-inv (:transform-inverse shape)
shape-center (gco/shape->center shape)
{sr-width :width sr-height :height} (:selrect shape)
{sr-width :width sr-height :height} (safe-size-rect shape)
origin (cond-> (gpt/point (:selrect shape))
origin (cond-> (gpt/point (safe-size-rect shape))
(some? shape-transform)
(gmt/transform-point-center shape-center shape-transform))

View File

@ -657,3 +657,35 @@
mods (ctm/rotation (ctm/empty) (gpt/point 50 25) 45)
result (ctm/apply-structure-modifiers shape mods)]
(t/is (mth/close? 45.0 (:rotation result))))))
;; ─── change-orientation-modifiers — degenerate selrect ────────────────────────
(defn- make-degenerate-shape
"Build a shape whose selrect has zero width/height, simulating a shape
decoded from the server via map->Rect (bypasses make-rect's 0.01 floor)."
[x y selrect-width selrect-height]
(let [shape (make-shape x y 100 50)]
(assoc shape :selrect (grc/map->Rect {:x x :y y
:width selrect-width
:height selrect-height
:x1 x :y1 y
:x2 (+ x selrect-width)
:y2 (+ y selrect-height)}))))
(t/deftest change-orientation-zero-width-selrect-does-not-throw
(t/testing "orientation change on a shape with zero selrect width does not throw"
(let [shape (make-degenerate-shape 0 0 0 50)
mods (ctm/change-orientation-modifiers shape :horiz)]
(t/is (some? mods)))))
(t/deftest change-orientation-zero-height-selrect-does-not-throw
(t/testing "orientation change on a shape with zero selrect height does not throw"
(let [shape (make-degenerate-shape 0 0 100 0)
mods (ctm/change-orientation-modifiers shape :vert)]
(t/is (some? mods)))))
(t/deftest change-orientation-zero-width-and-height-selrect-does-not-throw
(t/testing "orientation change on a fully degenerate selrect does not throw"
(let [shape (make-degenerate-shape 0 0 0 0)
mods (ctm/change-orientation-modifiers shape :horiz)]
(t/is (some? mods)))))

View File

@ -256,8 +256,8 @@
(defn assoc-position-data
[shape position-data old-shape]
(let [deltav (gpt/to-vec (gpt/point (:selrect old-shape))
(gpt/point (:selrect shape)))
(let [deltav (gpt/to-vec (gpt/point (ctm/safe-size-rect old-shape))
(gpt/point (ctm/safe-size-rect shape)))
position-data
(-> position-data
(gsh/move-position-data deltav))]

View File

@ -35,8 +35,8 @@
(if-let [modifiers (:modifiers shape)]
(let [shape' (gsh/transform-shape shape modifiers)
old-sr (dm/get-prop shape :selrect)
new-sr (dm/get-prop shape' :selrect)
old-sr (ctm/safe-size-rect shape)
new-sr (ctm/safe-size-rect shape')
;; We need to remove the movement because the dynamic modifiers will have move it
deltav (gpt/to-vec (gpt/point new-sr)

View File

@ -9,9 +9,11 @@
[app.common.geom.rect :as grc]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.shapes :as cths]
[app.common.types.modifiers :as ctm]
[app.common.types.shape :as cts]
[app.common.types.text :as txt]
[app.main.data.workspace.texts :as dwt]
[app.main.ui.workspace.shapes.text.viewport-texts-html :as vth]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]))
@ -374,3 +376,34 @@
"exactly one typography was added")
(t/is (= "0.1" (:letter-spacing (first typographies)))
"float letter-spacing is normalised to 2-decimal string")))))))
;; ---------------------------------------------------------------------------
;; Tests: fix-position with degenerate selrect
;; ---------------------------------------------------------------------------
(t/deftest fix-position-zero-width-selrect-does-not-throw
(t/testing "fix-position on a shape with zero selrect width does not throw"
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
modifiers (ctm/change-dimensions-modifiers shape :width 200 {:ignore-lock? true})
shape' (assoc shape :modifiers modifiers)
result (vth/fix-position shape')]
(t/is (some? result))
(t/is (some? (:selrect result))))))
(t/deftest fix-position-zero-height-selrect-does-not-throw
(t/testing "fix-position on a shape with zero selrect height does not throw"
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
modifiers (ctm/change-dimensions-modifiers shape :height 80 {:ignore-lock? true})
shape' (assoc shape :modifiers modifiers)
result (vth/fix-position shape')]
(t/is (some? result))
(t/is (some? (:selrect result))))))
(t/deftest fix-position-zero-width-and-height-selrect-does-not-throw
(t/testing "fix-position on a fully degenerate selrect does not throw"
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
modifiers (ctm/change-dimensions-modifiers shape :width 150 {:ignore-lock? true})
shape' (assoc shape :modifiers modifiers)
result (vth/fix-position shape')]
(t/is (some? result))
(t/is (some? (:selrect result))))))