// lib/editor/engine/stroke_bounds.dart // // Axis-aligned bounds of strokes in normalized content coordinates. Used for // broad-phase culling (don't paint/erase/hit-test strokes whose box is off the // viewport — the infinite board's R1 perf primitive), and as a cheap pre-filter // before the exact per-point eraser test. // // Pure geometry over EditorStroke; no widgets/storage; fully unit-tested. import 'dart:ui' show Rect; import 'stroke_model.dart'; /// Tight axis-aligned bounds of [stroke] in normalized coords, or null when the /// stroke has no points. A single-point stroke yields a zero-size rect at that /// point. Rect? strokeBounds(EditorStroke stroke) { if (stroke.points.isEmpty) return null; var minX = double.infinity, minY = double.infinity; var maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (final p in stroke.points) { if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; } return Rect.fromLTRB(minX, minY, maxX, maxY); } /// Union bounds of [strokes], or null when none have points. Rect? strokesBounds(Iterable strokes) { Rect? acc; for (final stroke in strokes) { final b = strokeBounds(stroke); if (b == null) continue; acc = acc == null ? b : acc.expandToInclude(b); } return acc; } /// Whether [stroke]'s bounds overlap [viewport] (broad-phase visibility test). /// Empty strokes are never visible. Touching edges count as overlapping. bool strokeIntersects(EditorStroke stroke, Rect viewport) { final b = strokeBounds(stroke); if (b == null) return false; return b.left <= viewport.right && b.right >= viewport.left && b.top <= viewport.bottom && b.bottom >= viewport.top; }