feat: host-agnostic StrokeHost (CoordinateSpaceHost, plan principle #2)
Some checks failed
CI / Windows build (push) Has been cancelled

Ties the engine together: a StrokeHost is anything ink attaches to — a PDF page,
an infinite-board region, or a (P5) CAS overlay — with a stable hostId (cache +
persistence key), a contentSize (normalized↔px mapping), and a revision-tracked
StrokeStore. strokesIn(viewport) broad-phase-culls via stroke_bounds for the
board. The viewport mounts one AnnotationLayer per host; nothing in the engine
knows page vs board (the one host-agnostic ink engine).

Makes StrokeStore (P0) + stroke_bounds load-bearing together. Pure; unit-tested.

flutter analyze lib/editor clean; 208/208 tests (+4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 03:36:44 +08:00
parent b1f131814e
commit 77b6ee415d
2 changed files with 97 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
// 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:<id>:page:<n>"` 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<EditorStroke> strokesIn(Rect viewportNormalized) {
return [
for (final s in store.committed)
if (strokeIntersects(s, viewportNormalized)) s,
];
}
@override
String toString() => 'StrokeHost($hostId, $contentSize, rev=$revision)';
}