// lib/editor/engine/stroke_host.dart // // A CoordinateSpaceHost (plan principle #2: ONE host-agnostic ink engine). A // host is anything ink attaches to — a PDF page, an infinite board region, or a // (P5) CAS overlay — identified by [hostId], with a [contentSize] that defines // the normalized↔pixel mapping, and a committed [StrokeStore]. The viewport // mounts one AnnotationLayer per host; nothing in the engine knows whether it's // a page or a board. // // Pure (no widgets/pdfrx); ties together StrokeStore (P0) + stroke_bounds // broad-phase culling. Unit-tested. import 'dart:ui' show Rect, Size; import 'stroke_bounds.dart'; import 'stroke_model.dart'; import 'stroke_store.dart'; /// One ink host: identity + content geometry + its committed strokes. class StrokeHost { StrokeHost({ required this.hostId, required this.contentSize, StrokeStore? store, }) : store = store ?? StrokeStore(); /// Stable id (e.g. `"doc::page:"` or a board-region id) used as the /// ink Picture cache key prefix + the persistence host id. final String hostId; /// Content size in logical px at scale 1; normalized [0,1] coords map onto it. final Size contentSize; /// Committed strokes for this host (revision-tracked). final StrokeStore store; /// Revision of the committed strokes (O(1) repaint gate passthrough). int get revision => store.revision; /// Committed strokes whose bounds overlap [viewportNormalized] — broad-phase /// culling for the infinite board (skip off-screen strokes). For a bounded /// PDF page the whole page is usually in view, so callers can skip this. List strokesIn(Rect viewportNormalized) { return [ for (final s in store.committed) if (strokeIntersects(s, viewportNormalized)) s, ]; } @override String toString() => 'StrokeHost($hostId, $contentSize, rev=$revision)'; }