Files
BadNote/lib/editor/engine/stroke_bounds.dart
Akiba So b359000991
Some checks failed
CI / Windows build (push) Has been cancelled
feat: stroke spatial bounds + broad-phase visibility (board culling)
strokeBounds (tight AABB over normalized points, null for empty, zero-size for a
single point), strokesBounds (union), and strokeIntersects (does a stroke's box
overlap a viewport rect — touching edges count). Broad-phase primitive for the
infinite board: skip painting/erasing/hit-testing strokes off-screen (R1 perf),
and a cheap pre-filter before the exact per-point eraser test.

Pure geometry over EditorStroke; fully unit-tested.

flutter analyze lib/editor clean; 191/191 tests (+10).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 03:31:54 +08:00

51 lines
1.7 KiB
Dart

// 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<EditorStroke> 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;
}