Files
BadNote/test/stroke_bounds_test.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

98 lines
2.5 KiB
Dart

// Tests for stroke spatial bounds + broad-phase visibility (F7 board culling).
import 'dart:ui' show Rect;
import 'package:flutter_test/flutter_test.dart';
import 'package:badnote/editor/engine/stroke_model.dart';
import 'package:badnote/editor/engine/stroke_bounds.dart';
EditorStroke _stroke(List<List<double>> pts) => EditorStroke.create(
id: 's',
points: [for (final p in pts) EditorPoint(x: p[0], y: p[1])],
);
void main() {
test('strokeBounds is the tight box over points', () {
final b = strokeBounds(_stroke([
[0.1, 0.2],
[0.5, 0.1],
[0.3, 0.6],
]))!;
expect(b.left, closeTo(0.1, 1e-9));
expect(b.top, closeTo(0.1, 1e-9));
expect(b.right, closeTo(0.5, 1e-9));
expect(b.bottom, closeTo(0.6, 1e-9));
});
test('empty stroke → null', () {
expect(strokeBounds(_stroke([])), isNull);
});
test('single point → zero-size rect at that point', () {
final b = strokeBounds(_stroke([
[0.4, 0.7],
]))!;
expect(b.width, 0);
expect(b.height, 0);
expect(b.left, closeTo(0.4, 1e-9));
expect(b.top, closeTo(0.7, 1e-9));
});
test('strokesBounds is the union; skips empty strokes', () {
final b = strokesBounds([
_stroke([
[0.1, 0.1],
[0.2, 0.2],
]),
_stroke([]), // skipped
_stroke([
[0.8, 0.7],
]),
])!;
expect(b.left, closeTo(0.1, 1e-9));
expect(b.top, closeTo(0.1, 1e-9));
expect(b.right, closeTo(0.8, 1e-9));
expect(b.bottom, closeTo(0.7, 1e-9));
});
test('strokesBounds of all-empty → null', () {
expect(strokesBounds([_stroke([]), _stroke([])]), isNull);
});
group('strokeIntersects (broad-phase)', () {
final view = const Rect.fromLTRB(0.0, 0.0, 0.5, 0.5);
test('inside the viewport intersects', () {
expect(strokeIntersects(_stroke([
[0.1, 0.1],
[0.2, 0.2],
]), view), isTrue);
});
test('fully outside does NOT intersect', () {
expect(strokeIntersects(_stroke([
[0.8, 0.8],
[0.9, 0.9],
]), view), isFalse);
});
test('partial overlap intersects', () {
expect(strokeIntersects(_stroke([
[0.4, 0.4],
[0.7, 0.7],
]), view), isTrue);
});
test('touching edge counts as intersecting', () {
expect(strokeIntersects(_stroke([
[0.5, 0.5],
]), view), isTrue);
});
test('empty stroke is never visible', () {
expect(strokeIntersects(_stroke([]), view), isFalse);
});
});
}