fix(dyn): clamp guide length to the viewport so a huge distance can't OOM

The dynamic-input guides are dotted, and a dash pattern is measured in pixels:
the tessellator emits a quad every few pixels, so a guide costs in proportion
to its SCREEN length, not to what it represents. Entering a large distance
(5000000 in the value box) projects the cursor millions of pixels from the
anchor, so the guide asks for millions of dash quads — gigabytes — and the
process fills RAM and dies while building a frame whose content is off-screen
anyway (#406).

Clamp the drawn length to just past the viewport diagonal. `dx`/`dy` still come
from the true cursor, and the angle and read-outs are computed from the raw
length, so direction and every displayed value are unchanged — only the tail
past the viewport edge, which nobody can see, is dropped. Clamping `cursor`
bounds every guide below it: they all derive from it, including the polar arc
and the perpendicular endpoint.

The reporter's own bisect points here: with DYN off the crash does not happen,
and drawing the same line by command (which builds no interactive guide) never
reproduced it.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-17 13:44:20 +03:00
1 parent f720e40ba1
commit 48a43aae11
1 file changed
+23 -4
+23 -4
View File
@@ -1757,10 +1757,29 @@ impl DynInputCanvas {
/// Guided layout: draw the guide geometry anchored at `base`, then place
/// each box according to its role.
fn draw_guided(&self, frame: &mut canvas::Frame, bounds: iced::Rectangle, base: Point) {
let cursor = self.cursor_screen;
let (vx, vy) = (cursor.x - base.x, cursor.y - base.y);
let len = (vx * vx + vy * vy).sqrt().max(1.0);
let (dx, dy) = (vx / len, vy / len);
let cursor_raw = self.cursor_screen;
let (vx, vy) = (cursor_raw.x - base.x, cursor_raw.y - base.y);
let raw_len = (vx * vx + vy * vy).sqrt().max(1.0);
let (dx, dy) = (vx / raw_len, vy / raw_len);
// Clamp the drawn length to just past the viewport.
//
// These guides are dotted, and a dash pattern is measured in PIXELS: the
// tessellator emits a quad every few pixels, so the cost scales with a
// guide's screen length, not with what it represents. Typing a large
// distance (5000000 into the value box) projects the cursor millions of
// pixels away — millions of quads, gigabytes of them — and the process
// dies building a frame whose content is off-screen anyway (#406).
//
// Everything past the viewport edge is invisible, so dropping it costs
// nothing: `dx`/`dy` keep the true direction, the angle and every
// read-out are computed from `raw_len` above, and only the tail nobody
// can see is cut. Clamping `cursor` here bounds every guide below —
// they all derive from it.
let len = raw_len.min(bounds.width.hypot(bounds.height) * 1.5);
let cursor = Point {
x: base.x + dx * len,
y: base.y + dy * len,
};
// Perpendicular pointing to the lower half so labels sit under the line.
let (mut nx, mut ny) = (-dy, dx);
if ny < 0.0 {