69 lines
2.2 KiB
Dart
69 lines
2.2 KiB
Dart
|
|
// lib/editor/engine/stroke_geometry.dart
|
||
|
|
//
|
||
|
|
// Single source of stroke outline geometry for both screen render and export.
|
||
|
|
// The recipe is lifted verbatim from the proven live
|
||
|
|
// `ink_painters.buildStrokePath` (lib/editor/canvas/ink_painters.dart): points
|
||
|
|
// are scaled from normalized page coords to pixels, perfect_freehand produces
|
||
|
|
// the outline, and a closed fill Path is built. Keeping ONE implementation here
|
||
|
|
// kills the hairline-export divergence (R7).
|
||
|
|
|
||
|
|
import 'dart:ui';
|
||
|
|
|
||
|
|
import 'package:perfect_freehand/perfect_freehand.dart' as pf;
|
||
|
|
|
||
|
|
import 'stroke_model.dart';
|
||
|
|
|
||
|
|
/// Builds a closed, fillable outline [Path] for one [stroke], scaled into the
|
||
|
|
/// pixel space of [pageSize] (which maps normalized [0,1] coords to pixels).
|
||
|
|
///
|
||
|
|
/// [isComplete] should be false for the in-progress live stroke so freehand
|
||
|
|
/// tapers the trailing end correctly, and true for committed strokes.
|
||
|
|
///
|
||
|
|
/// Returns an empty [Path] when the stroke has no points (or freehand produces
|
||
|
|
/// no outline).
|
||
|
|
Path buildStrokeOutline(
|
||
|
|
EditorStroke stroke,
|
||
|
|
Size pageSize, {
|
||
|
|
required bool isComplete,
|
||
|
|
}) {
|
||
|
|
final path = Path();
|
||
|
|
if (stroke.points.isEmpty) return path;
|
||
|
|
|
||
|
|
final pixelWidth = stroke.width * pageSize.width;
|
||
|
|
|
||
|
|
final hasRealPressure = stroke.points.any((p) => p.pressure != null);
|
||
|
|
final isHighlighter = stroke.tool == EditorTool.highlighter;
|
||
|
|
|
||
|
|
final pfPoints = stroke.points
|
||
|
|
.map(
|
||
|
|
(p) => pf.Point(
|
||
|
|
p.x * pageSize.width,
|
||
|
|
p.y * pageSize.height,
|
||
|
|
p.pressure ?? 0.5,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
.toList();
|
||
|
|
|
||
|
|
final outline = pf.getStroke(
|
||
|
|
pfPoints,
|
||
|
|
size: pixelWidth,
|
||
|
|
// Highlighter keeps a constant width (no thinning); pen thins (0.7),
|
||
|
|
// matching the live recipe.
|
||
|
|
thinning: isHighlighter ? 0.0 : 0.7,
|
||
|
|
smoothing: 0.5,
|
||
|
|
streamline: 0.5,
|
||
|
|
// Real stylus pressure -> don't simulate; no pressure -> let freehand fake
|
||
|
|
// it based on velocity (highlighter never simulates).
|
||
|
|
simulatePressure: !hasRealPressure && !isHighlighter,
|
||
|
|
isComplete: isComplete,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (outline.isEmpty) return path;
|
||
|
|
path.moveTo(outline.first.x, outline.first.y);
|
||
|
|
for (var i = 1; i < outline.length; i++) {
|
||
|
|
path.lineTo(outline[i].x, outline[i].y);
|
||
|
|
}
|
||
|
|
path.close();
|
||
|
|
return path;
|
||
|
|
}
|