feat(engine): P0 stroke engine + persistence

Per the full-refactor plan §9 (input-independent half of P0):
- engine: canonical EditorStroke (lossless InkStroke round-trip) +
  stroke_geometry (single getStroke outline) + revision-gated StrokeStore
- render: static/live ink painters + ink_picture_cache (revision-keyed)
  + annotation_layer (RepaintBoundary)
- persistence: DB v6 (ink, notebook_pages) + editor_repository diff-write
  (UPSERT changed / DELETE removed in one txn; id-set after commit) +
  save_scheduler
- pdf_service export now FILLS the getStroke outline (R7 hairline fix)
Not yet wired into the live editor (input relocation pending pen-pressure
diagnostic). 28 new tests pass.
This commit is contained in:
2026-06-21 23:41:01 +08:00
parent 1e2a83b0b9
commit 914951afb7
16 changed files with 2267 additions and 23 deletions

View File

@@ -41,7 +41,7 @@ class DatabaseService {
_database = await openDatabase(
dbPath,
version: 5,
version: 6,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
@@ -156,12 +156,71 @@ class DatabaseService {
await db.execute(
'CREATE INDEX idx_scratchpads_doc ON scratchpads(document_id)',
);
// Editor ink strokes (v6)
await db.execute('''
CREATE TABLE ink (
id TEXT PRIMARY KEY,
host_kind TEXT NOT NULL,
host_id TEXT NOT NULL,
stroke_json TEXT NOT NULL,
ordinal INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
''');
await db.execute(
'CREATE INDEX idx_ink_host ON ink(host_kind, host_id)',
);
// Notebook pages (v6)
await db.execute('''
CREATE TABLE notebook_pages (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
source_page_index INTEGER NOT NULL,
kind TEXT NOT NULL,
created_at INTEGER NOT NULL
)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 3) await _migrateV2toV3(db);
if (oldVersion < 4) {} // v3->v4: version boundary (no-op schema)
if (oldVersion < 5) await _migrateV4toV5(db);
if (oldVersion < 6) await _migrateV5toV6(db);
}
Future<void> _migrateV5toV6(Database db) async {
await db.transaction((txn) async {
await txn.execute('''
CREATE TABLE ink (
id TEXT PRIMARY KEY,
host_kind TEXT NOT NULL,
host_id TEXT NOT NULL,
stroke_json TEXT NOT NULL,
ordinal INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
''');
await txn.execute(
'CREATE INDEX idx_ink_host ON ink(host_kind, host_id)',
);
await txn.execute('''
CREATE TABLE notebook_pages (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
source_page_index INTEGER NOT NULL,
kind TEXT NOT NULL,
created_at INTEGER NOT NULL
)
''');
});
}
Future<void> _migrateV2toV3(Database db) async {