// lib/editor/notebook/ink_stroke_adapter.dart // // Bridge between the legacy note/ppt storage model (`InkStroke`, ABSOLUTE pixel // coordinates, `PenTool`) and the pen-first canvas model (`PenStroke`, // NORMALIZED [0,1] coordinates, `PenStrokeKind`). The pen-first canvas is the // single performant inking engine, so notes and slides are rebuilt on top of it // and persisted back as `InkStroke` via this adapter. // // Coordinates are normalized against a logical page rectangle: ink absolute // (x,y) -> pen (x/pageW, y/pageH) and back. Stroke width is likewise expressed // as a fraction of the page width on the pen side and as absolute pixels on the // ink side. Only freehand pen/highlighter strokes round-trip; shape/text // `PenTool`s have no pen-canvas representation and are dropped (the pen-first // note is handwriting-first — see the rebuild roadmap). import 'dart:ui' show Size; import '../../models/ink_point.dart'; import '../../models/ink_stroke.dart'; import '../../models/pen_tool.dart'; import '../canvas/pen_stroke.dart'; /// Logical page rectangle a blank note is inked on (portrait, ~A4 √2 ratio). /// Strokes are normalized against this so they stay pinned under zoom/pan. const Size kNoteLogicalPage = Size(1000, 1414); /// True when [tool] is a freehand mark the pen canvas can render /// (pen/marker/highlighter). Shapes and text are not representable. bool isFreehandTool(PenTool tool) => tool == PenTool.pen || tool == PenTool.marker || tool == PenTool.highlighter; /// Maps an ink [PenTool] to the pen-canvas stroke kind. PenStrokeKind penKindFromTool(PenTool tool) => tool == PenTool.highlighter ? PenStrokeKind.highlighter : PenStrokeKind.pen; /// Maps a pen-canvas stroke kind back to a [PenTool]. PenTool toolFromPenKind(PenStrokeKind kind) => kind == PenStrokeKind.highlighter ? PenTool.highlighter : PenTool.pen; /// Convert a stored [InkStroke] (absolute px on [page]) to a [PenStroke] /// (normalized). Returns null for non-freehand strokes (shapes/text), which the /// pen canvas cannot draw. PenStroke? penStrokeFromInk(InkStroke s, Size page) { if (!isFreehandTool(s.tool)) return null; if (s.points.isEmpty) return null; final w = page.width <= 0 ? 1.0 : page.width; final h = page.height <= 0 ? 1.0 : page.height; return PenStroke( points: [ for (final p in s.points) PenPoint(p.x / w, p.y / h, p.pressure, tilt: p.tilt), ], color: s.color, width: s.strokeWidth / w, kind: penKindFromTool(s.tool), ); } /// Convert a freshly drawn [PenStroke] (normalized) back to an [InkStroke] /// (absolute px on [page]) for persistence. [id] and [createdAt] come from the /// caller (uuid + clock) so this stays pure/deterministic. InkStroke inkStrokeFromPen( PenStroke s, Size page, { required String id, required DateTime createdAt, }) { final w = page.width <= 0 ? 1.0 : page.width; final h = page.height <= 0 ? 1.0 : page.height; return InkStroke( id: id, points: [ for (final p in s.points) InkPoint( x: p.x * w, y: p.y * h, pressure: p.pressure ?? 0.5, tilt: p.tilt ?? 0.0, timestamp: 0, ), ], tool: toolFromPenKind(s.kind), color: s.color, strokeWidth: s.width * w, createdAt: createdAt, ); } /// Convert a list of stored ink strokes to pen strokes, dropping the ones the /// canvas cannot represent (shapes/text). Order is preserved. List penStrokesFromInk(Iterable strokes, Size page) => [for (final s in strokes) penStrokeFromInk(s, page)] .whereType() .toList();