133 lines
4.9 KiB
Dart
133 lines
4.9 KiB
Dart
|
|
// test/shape_geometry_test.dart
|
||
|
|
//
|
||
|
|
// Pins the SHAPE-tool geometry (lib/editor/engine/shape_geometry.dart): each
|
||
|
|
// shape is generated as a normalized PenPoint polyline with the documented point
|
||
|
|
// counts (line→2, rect→5 closed, ellipse→kEllipseSamples+1), the shapes span the
|
||
|
|
// requested start→end box, and translateStroke/translatePoints shift every point
|
||
|
|
// without disturbing color/width/kind/brush (the SELECT-tool move primitive).
|
||
|
|
|
||
|
|
import 'package:flutter_test/flutter_test.dart';
|
||
|
|
|
||
|
|
import 'package:badnote/editor/canvas/editor_tool.dart';
|
||
|
|
import 'package:badnote/editor/canvas/pen_stroke.dart';
|
||
|
|
import 'package:badnote/editor/engine/brush.dart';
|
||
|
|
import 'package:badnote/editor/engine/shape_geometry.dart';
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
const a = PenPoint(0.2, 0.3, 1.0);
|
||
|
|
const b = PenPoint(0.8, 0.7, 1.0);
|
||
|
|
|
||
|
|
group('point counts', () {
|
||
|
|
test('line → exactly 2 points (start, end)', () {
|
||
|
|
final pts = generateShapePoints(ShapeKind.line, a, b);
|
||
|
|
expect(pts, hasLength(2));
|
||
|
|
expect(pts.first.x, closeTo(0.2, 1e-9));
|
||
|
|
expect(pts.first.y, closeTo(0.3, 1e-9));
|
||
|
|
expect(pts.last.x, closeTo(0.8, 1e-9));
|
||
|
|
expect(pts.last.y, closeTo(0.7, 1e-9));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rectangle → 5 points, closed (last == first)', () {
|
||
|
|
final pts = generateShapePoints(ShapeKind.rectangle, a, b);
|
||
|
|
expect(pts, hasLength(5));
|
||
|
|
expect(pts.first.x, closeTo(pts.last.x, 1e-9));
|
||
|
|
expect(pts.first.y, closeTo(pts.last.y, 1e-9));
|
||
|
|
// Axis-aligned box corners spanning the start/end bounds.
|
||
|
|
final xs = pts.map((p) => p.x).toSet();
|
||
|
|
final ys = pts.map((p) => p.y).toSet();
|
||
|
|
expect(xs, containsAll(<double>{0.2, 0.8}));
|
||
|
|
expect(ys, containsAll(<double>{0.3, 0.7}));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('ellipse → kEllipseSamples + 1 points, closed', () {
|
||
|
|
final pts = generateShapePoints(ShapeKind.ellipse, a, b);
|
||
|
|
expect(pts, hasLength(kEllipseSamples + 1));
|
||
|
|
expect(pts.first.x, closeTo(pts.last.x, 1e-9));
|
||
|
|
expect(pts.first.y, closeTo(pts.last.y, 1e-9));
|
||
|
|
// ~48 samples by spec.
|
||
|
|
expect(kEllipseSamples, 48);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('arrow → shaft + 2 head barbs (6 points)', () {
|
||
|
|
final pts = generateShapePoints(ShapeKind.arrow, a, b);
|
||
|
|
// start, end, barb1, back-to-tip, barb2, back-to-tip = 6.
|
||
|
|
expect(pts, hasLength(6));
|
||
|
|
expect(pts[0].x, closeTo(0.2, 1e-9));
|
||
|
|
expect(pts[1].x, closeTo(0.8, 1e-9));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('degenerate arrow (zero length) → just the 2 shaft points', () {
|
||
|
|
final pts = generateShapePoints(ShapeKind.arrow, a, a);
|
||
|
|
expect(pts, hasLength(2));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('ellipse spans the start→end box', () {
|
||
|
|
test('points stay within the bounding box (inclusive)', () {
|
||
|
|
final pts = generateShapePoints(ShapeKind.ellipse, a, b);
|
||
|
|
for (final p in pts) {
|
||
|
|
expect(p.x, greaterThanOrEqualTo(0.2 - 1e-9));
|
||
|
|
expect(p.x, lessThanOrEqualTo(0.8 + 1e-9));
|
||
|
|
expect(p.y, greaterThanOrEqualTo(0.3 - 1e-9));
|
||
|
|
expect(p.y, lessThanOrEqualTo(0.7 + 1e-9));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('translate (SELECT-tool move primitive)', () {
|
||
|
|
test('translatePoints shifts every point, preserves pressure', () {
|
||
|
|
const pts = [PenPoint(0.1, 0.2, 0.5), PenPoint(0.3, 0.4, null)];
|
||
|
|
final moved = translatePoints(pts, 0.05, -0.1);
|
||
|
|
expect(moved[0].x, closeTo(0.15, 1e-9));
|
||
|
|
expect(moved[0].y, closeTo(0.1, 1e-9));
|
||
|
|
expect(moved[0].pressure, 0.5);
|
||
|
|
expect(moved[1].x, closeTo(0.35, 1e-9));
|
||
|
|
expect(moved[1].pressure, isNull);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('translateStroke shifts points and preserves metadata', () {
|
||
|
|
const stroke = PenStroke(
|
||
|
|
points: [PenPoint(0.1, 0.1, 1.0), PenPoint(0.2, 0.2, 1.0)],
|
||
|
|
color: 0xFF112233,
|
||
|
|
width: 0.01,
|
||
|
|
kind: PenStrokeKind.highlighter,
|
||
|
|
brush: BrushKind.pencil,
|
||
|
|
);
|
||
|
|
final moved = translateStroke(stroke, 0.1, 0.2);
|
||
|
|
expect(moved.points[0].x, closeTo(0.2, 1e-9));
|
||
|
|
expect(moved.points[0].y, closeTo(0.3, 1e-9));
|
||
|
|
expect(moved.points[1].x, closeTo(0.3, 1e-9));
|
||
|
|
expect(moved.color, 0xFF112233);
|
||
|
|
expect(moved.width, 0.01);
|
||
|
|
expect(moved.kind, PenStrokeKind.highlighter);
|
||
|
|
expect(moved.brush, BrushKind.pencil);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
group('penStrokeBounds', () {
|
||
|
|
test('tight bounds over the points', () {
|
||
|
|
const stroke = PenStroke(
|
||
|
|
points: [PenPoint(0.2, 0.5, 1.0), PenPoint(0.8, 0.1, 1.0)],
|
||
|
|
color: 0xFF000000,
|
||
|
|
width: 0.01,
|
||
|
|
kind: PenStrokeKind.pen,
|
||
|
|
);
|
||
|
|
final bnds = penStrokeBounds(stroke)!;
|
||
|
|
expect(bnds.left, closeTo(0.2, 1e-9));
|
||
|
|
expect(bnds.right, closeTo(0.8, 1e-9));
|
||
|
|
expect(bnds.top, closeTo(0.1, 1e-9));
|
||
|
|
expect(bnds.bottom, closeTo(0.5, 1e-9));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('empty stroke → null bounds', () {
|
||
|
|
const stroke = PenStroke(
|
||
|
|
points: [],
|
||
|
|
color: 0xFF000000,
|
||
|
|
width: 0.01,
|
||
|
|
kind: PenStrokeKind.pen,
|
||
|
|
);
|
||
|
|
expect(penStrokeBounds(stroke), isNull);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|