Some checks failed
CI / Windows build (push) Has been cancelled
Replace the ad-hoc tool palette with a shared tool system (EditorToolKind) across the PDF, note and slide editors, and add the core writing tools. - Multiple brushes, each remembering its OWN color (rnote-style): selecting a brush restores its color, changing color updates only that brush, and each brush button shows its current color. - Select tool: tap-select a committed stroke, drag to move it, delete it — persisted and undoable. - Shape tool: line / rectangle / ellipse / arrow, drawn with a live preview and committed as generated PenStrokes (shape_geometry.dart) so they reuse stroke rendering, erase, persistence and undo. - Highlighter + eraser fold into the same tool system. Text/bookmark/search+OCR/backgrounds/Windows-Ink are later batches (TODO). Brush opacity still deferred. analyze clean, 302 tests.
139 lines
5.5 KiB
Dart
139 lines
5.5 KiB
Dart
// lib/editor/engine/shape_geometry.dart
|
|
//
|
|
// Pure geometry for the SHAPE tool. Each shape is generated as a list of
|
|
// NORMALIZED [PenPoint]s (the same model freehand strokes use), so a shape is
|
|
// just a [PenStroke] — it reuses stroke rendering, persistence, erase, and undo
|
|
// with NO new model or storage. Points carry a constant pressure (1.0) so the
|
|
// brush renders them at a steady width (shapes don't taper with pressure).
|
|
//
|
|
// All inputs/outputs are in normalized page coordinates ([0,1] x [0,1]); the
|
|
// caller wraps the points in a PenStroke with the current brush color/width.
|
|
|
|
import 'dart:math' as math;
|
|
|
|
import '../canvas/editor_tool.dart';
|
|
import '../canvas/pen_stroke.dart';
|
|
|
|
/// Number of points sampled around an ellipse. Kept as a const so tests can pin
|
|
/// it (spec: "ellipse = sampled points ~48"). The polyline is closed, so the
|
|
/// last point repeats the first ⇒ [kEllipseSamples] + 1 total points.
|
|
const int kEllipseSamples = 48;
|
|
|
|
/// Constant pressure baked into every shape point so the brush renders a steady
|
|
/// width (no pressure taper for geometric shapes).
|
|
const double _kShapePressure = 1.0;
|
|
|
|
/// Generate the normalized polyline for [kind] spanning [start] → [end].
|
|
///
|
|
/// * [ShapeKind.line] → 2 points.
|
|
/// * [ShapeKind.rectangle] → 5 points (closed: 4 corners + repeat of the
|
|
/// first), an axis-aligned box whose opposite corners are [start]/[end].
|
|
/// * [ShapeKind.ellipse] → [kEllipseSamples] + 1 points (closed), inscribed
|
|
/// in the [start]→[end] bounding box.
|
|
/// * [ShapeKind.arrow] → shaft (start → end) + two arrowhead segments,
|
|
/// emitted as a single polyline so it renders as one stroke.
|
|
List<PenPoint> generateShapePoints(ShapeKind kind, PenPoint start, PenPoint end) {
|
|
switch (kind) {
|
|
case ShapeKind.line:
|
|
return [
|
|
PenPoint(start.x, start.y, _kShapePressure),
|
|
PenPoint(end.x, end.y, _kShapePressure),
|
|
];
|
|
|
|
case ShapeKind.rectangle:
|
|
final l = math.min(start.x, end.x);
|
|
final r = math.max(start.x, end.x);
|
|
final t = math.min(start.y, end.y);
|
|
final b = math.max(start.y, end.y);
|
|
return [
|
|
PenPoint(l, t, _kShapePressure),
|
|
PenPoint(r, t, _kShapePressure),
|
|
PenPoint(r, b, _kShapePressure),
|
|
PenPoint(l, b, _kShapePressure),
|
|
PenPoint(l, t, _kShapePressure), // close
|
|
];
|
|
|
|
case ShapeKind.ellipse:
|
|
final cx = (start.x + end.x) / 2;
|
|
final cy = (start.y + end.y) / 2;
|
|
final rx = (end.x - start.x).abs() / 2;
|
|
final ry = (end.y - start.y).abs() / 2;
|
|
final pts = <PenPoint>[];
|
|
for (var i = 0; i <= kEllipseSamples; i++) {
|
|
final a = (i / kEllipseSamples) * 2 * math.pi;
|
|
pts.add(PenPoint(
|
|
cx + rx * math.cos(a),
|
|
cy + ry * math.sin(a),
|
|
_kShapePressure,
|
|
));
|
|
}
|
|
return pts;
|
|
|
|
case ShapeKind.arrow:
|
|
// Shaft start→end, then back up the shaft to draw the two head barbs so
|
|
// the whole arrow is one continuous polyline (no pen lifts).
|
|
final dx = end.x - start.x;
|
|
final dy = end.y - start.y;
|
|
final len = math.sqrt(dx * dx + dy * dy);
|
|
final pts = <PenPoint>[
|
|
PenPoint(start.x, start.y, _kShapePressure),
|
|
PenPoint(end.x, end.y, _kShapePressure),
|
|
];
|
|
if (len <= 1e-6) return pts; // degenerate: just the (near-zero) shaft
|
|
// Arrowhead: barbs at ±[_kArrowAngle] from the reversed shaft direction,
|
|
// [_kArrowHead] of the shaft length (capped) long.
|
|
final ang = math.atan2(dy, dx);
|
|
final head = math.min(len * _kArrowHeadFraction, _kArrowHeadMax);
|
|
for (final sign in const [1.0, -1.0]) {
|
|
final a = ang + math.pi + sign * _kArrowAngle;
|
|
pts.add(PenPoint(
|
|
end.x + head * math.cos(a),
|
|
end.y + head * math.sin(a),
|
|
_kShapePressure,
|
|
));
|
|
pts.add(PenPoint(end.x, end.y, _kShapePressure)); // back to the tip
|
|
}
|
|
return pts;
|
|
}
|
|
}
|
|
|
|
/// Arrowhead barb length as a fraction of the shaft length.
|
|
const double _kArrowHeadFraction = 0.25;
|
|
|
|
/// Hard cap on the barb length (normalized) so a long arrow's head stays sane.
|
|
const double _kArrowHeadMax = 0.06;
|
|
|
|
/// Half-angle of the arrowhead barbs from the shaft (radians ≈ 28°).
|
|
const double _kArrowAngle = 0.5;
|
|
|
|
/// Return a copy of [points] translated by ([dx],[dy]) in normalized coords,
|
|
/// preserving pressure/tilt. Used by the SELECT tool to drag a stroke.
|
|
List<PenPoint> translatePoints(List<PenPoint> points, double dx, double dy) =>
|
|
[for (final p in points) PenPoint(p.x + dx, p.y + dy, p.pressure, tilt: p.tilt)];
|
|
|
|
/// A translated copy of [stroke] (its points shifted by [dx],[dy]); color,
|
|
/// width, kind, and brush are preserved.
|
|
PenStroke translateStroke(PenStroke stroke, double dx, double dy) => PenStroke(
|
|
points: translatePoints(stroke.points, dx, dy),
|
|
color: stroke.color,
|
|
width: stroke.width,
|
|
kind: stroke.kind,
|
|
brush: stroke.brush,
|
|
);
|
|
|
|
/// Tight normalized bounds of [stroke]'s points, or null when it has no points.
|
|
/// Used by the SELECT tool to draw the selection bounding box.
|
|
({double left, double top, double right, double bottom})? penStrokeBounds(
|
|
PenStroke stroke) {
|
|
if (stroke.points.isEmpty) return null;
|
|
var l = double.infinity, t = double.infinity;
|
|
var r = double.negativeInfinity, b = double.negativeInfinity;
|
|
for (final p in stroke.points) {
|
|
if (p.x < l) l = p.x;
|
|
if (p.y < t) t = p.y;
|
|
if (p.x > r) r = p.x;
|
|
if (p.y > b) b = p.y;
|
|
}
|
|
return (left: l, top: t, right: r, bottom: b);
|
|
}
|