Wire finger drawing on PDF without breaking pinch; auto-hide page scrubber and fix bounce; share sticky tools with resize and per-page remember; side-button select; separate pen slots with colors; rnote pressure shapes plus tip-velocity width and lower stroke latency.
Co-authored-by: Cursor <cursoragent@cursor.com>
Surface Aug6 diag showed sDrop=0 but ~220 same-ms dual ZOOM frames and √2 cur ping-pong from reading currentZoom back into pinch state. Flush once per microtask and embed gitSha in diagnostic meta.
Co-authored-by: Cursor <cursoragent@cursor.com>
Hard SDROP avalanches froze lastRaw while zoom still crawled; soft-clamp
and re-anchor instead. Ballpoint is near-solid, pencil uses soft √p without
multiply stacking; PDF ink falls back to nearest page during zoom settle.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add notebook.json containers with multi-member pages, fix PDF text
editing (size/bold/drag/double-tap), index SidecarText in search, and
share keyboard page shortcuts plus a PDF scrubber.
Co-authored-by: Cursor <cursoragent@cursor.com>
Wire Win32 pressure into Dart, tighten pinch guards, use geometric shape
strokes, expand the ink palette, and replace scratch-link split view with
an on-page sticky that shares the sidecar repo.
Co-authored-by: Cursor <cursoragent@cursor.com>
Make Surface remote debugging and classroom workflows viable: always-on
structured logs with one-click zip export, a single AppShell chrome,
OOXML PPTX/DOCX annotation without LibreOffice, and a first-class sticky
board. Also drop spike/legacy ink widgets and tighten pen feel
(predictor, PenInfoHistory, page-tile layer).
Co-authored-by: Cursor <cursoragent@cursor.com>
Search now covers handwriting, the PDF text layer, AND scanned
(rasterized) PDFs.
- PdfTextIndexer runs at import: sums the embedded text layer across
pages; if present it stores that as the document body, otherwise the
PDF is rasterized and its rendered pages are OCR'd in the background.
The result lands in the sidecar `pageText` field (distinct from
`ocrText`, the handwriting OCR). Idempotent (skips a sidecar that
already has pageText); degrades gracefully with no OCR engine.
- pdfrx_page_text_source abstracts text/render so it's testable.
- VaultSearchIndex now harvests title + typed text + handwriting OCR +
PDF pageText, so search finds notes, typed PDFs and scanned PDFs.
analyze clean, 409 tests green.
Add a text-annotation tool to the PDF editor. With the text tool a
pen-tap, or a mouse double-click, drops a text box at that normalized
page point and focuses a real Flutter TextField — so the OS IME and the
Windows-Ink handwriting panel feed it (device-validated). Tapping an
existing box re-opens it; clearing it deletes it.
- SidecarText {nx, ny, text, fontSize (page-relative), color} per page,
glued under zoom; stored in the sidecar `texts` field (back-compat
missing -> none), saved via scheduleTextsSave and loaded on open.
- Rendered in pageOverlaysBuilder at the scaled position.
PDF editor only for now (note text later). analyze clean, 397 tests.
Add a bookmark tool to the PDF editor. A bookmark anchors to a precise
location: when text is selected it captures the selection's normalized
rect + the start char index in the page text (the true paragraph
anchor); with no selection it falls back to the tapped page + point.
- Bookmark model gains optional normalized anchor rect + charIndex +
label (all absent from JSON when null, so old sidecars still load).
- Bookmarks persist in the sidecar (scheduleBookmarkUpsert) and load on
open; a bookmarks panel lists them and tapping one jumps to its page.
Delete is persisted.
Scoped to the PDF editor (note bookmarks later); scroll-to-anchor is
page-level for now. analyze clean, 391 tests green.
A blank note can show a page-background template painted behind the
ink, picked from the toolbar and persisted per notebook.
- NoteBackground: blank / dots / ruled / grid / cornell, drawn in
page space (scales with zoom), subtle grey. Cornell = left margin +
bottom summary rule over a ruled body.
- Stored as the enum name in the notebook sidecar (back-compat:
missing/unknown -> blank), saved/loaded via SidecarRepository so it
restores on reopen.
- Picker added to the note tool palette.
PDF backgrounds skipped (PDFs have their own page content). analyze
clean, 386 tests green.
Closes TODO(brush-persist). EditorStroke now serializes its brush as
the stable BrushKind name; sidecars written before this field, and any
unknown name, load as fountainPen (back-compat). PenStroke<->EditorStroke
carry brush both ways, so a ballpoint/highlighter/pencil stroke keeps
its opacity/blend after a document is closed and reopened.
Note: InkStroke (the note/scratchpad world-coord format) has no brush
field, so notes derive brush from the tool — highlighter is preserved,
ballpoint/pencil collapse to fountainPen on reload (TODO: extend
InkStroke). PDF documents persist brush fully. analyze clean,
379 tests green.
Honor each brush's opacity/blend so the brushes feel distinct
(closes TODO(brush-opacity)).
- Shared paint resolver: a stroke's color alpha is multiplied by its
brush opacity; ballpoint/pencil opacity is tied to pressure
(per-stroke average this increment) so a ballpoint reads lighter
than a solid fountain pen.
- Highlighter paints with BlendMode.multiply and draws once, so
cross-stroke overlap darkens like a real marker while self-overlap
doesn't.
- Applied across BOTH render paths (PenCanvas static/live painters and
the PDF _PageOverlayPainter).
Pencil paper-grain texture still deferred (TODO brush-texture); brush
kind is not yet serialized (TODO brush-persist — next). analyze clean,
tests green.
Phase 6 (final storage phase).
- SidecarRepositoryRegistry tracks every open repo; SidecarFlushObserver
(a WidgetsBindingObserver in main) flushes them all on
inactive/hidden/paused/detached, awaiting each flush — the last
strokes can't be lost on app close, not just on the 800ms timer.
- VaultSearchIndex rebuilds by scanning vault sidecars (the source of
truth) — note titles, OCR text and document names — and search_provider
queries it, so search spans notes + PDFs. Rebuilt on launch / after
import.
The vault file-based storage migration (Phases 0-6) is complete:
annotations travel with the file, picked vault folder, atomic autosave,
one Import-file entry, SQLite migrated to sidecars. analyze clean,
tests green.
Phase 4. Standalone notes move off SQLite into the vault, like the
PDF annotations.
- "Create notebook" makes a vault folder with a notebook.badnote.json
(BadnoteSidecar docType 'notebook' + a title field), opened via
SidecarRepository.
- PenNoteScreen loads/saves its strokes (page 0) + title to that
sidecar instead of the SQLite Note model.
- note_provider lists notes from a vault scan (VaultService.scanNotes
= folders with notebook.badnote.json and no source file); the doc
scan still excludes them. Delete removes the folder.
PDF/slide editors unchanged; pre-existing SQLite notes migrate in
Phase 5. analyze clean, tests green.
Phase 2 (core swap). The PDF editor and split-view scratchpad stop
writing SQLite and persist to a per-file sidecar
`<pdfPath>.badnote.json` (debounced, atomic temp+rename+.bak) — so
annotations travel with the file. The source path is the identity
(no more djb2 doc-id).
- SidecarRepository wraps the Phase-1 store with debounced autosave.
- pen_editor: per-page ink, scratch-links AND highlights now persist
to the sidecar and restore on reopen (closes persist-highlights).
- New "un-highlight" tool: tap a stored highlight to remove it — the
highlight could not be removed before.
- split_view: each anchor's scratchpad lives in the sidecar's
scratchLinks[id].scratchpad, keyed by anchor id.
Note: pre-existing SQLite annotations are migrated later (Phase 5);
note/slide editors swap in Phase 4. analyze clean, tests green.
Replace the ad-hoc tool palette with a shared tool system
(EditorToolKind) across the PDF, note and slide editors, and add
the core writing tools.
- Multiple brushes, each remembering its OWN color (rnote-style):
selecting a brush restores its color, changing color updates only
that brush, and each brush button shows its current color.
- Select tool: tap-select a committed stroke, drag to move it,
delete it — persisted and undoable.
- Shape tool: line / rectangle / ellipse / arrow, drawn with a live
preview and committed as generated PenStrokes (shape_geometry.dart)
so they reuse stroke rendering, erase, persistence and undo.
- Highlighter + eraser fold into the same tool system.
Text/bookmark/search+OCR/backgrounds/Windows-Ink are later batches
(TODO). Brush opacity still deferred. analyze clean, 302 tests.
Two critical PDF-editor bugs.
1. Live ink only appeared after lifting the pen. The page overlay
painter captured the live stroke as a build-time snapshot, so
per-move repaints redrew stale (null) data until commit. Route
the live stroke through a ValueNotifier the painter reads at
paint time (repaint: merge(overlayRepaint, liveStrokeVN)).
2. Pinch-zoom jumped on Windows touch. pdfrx's internal forked
InteractiveViewer scales with an unguarded scaleStart*details.scale
that pops on a touch-count blip or one-frame spike. Take over the
pinch: scaleEnabled:false (pdfrx keeps 1-finger scroll + wheel),
a glitch-guarded ScaleGestureRecognizer drives focal zoom via the
pdfrx controller, reusing absolutePinchScale + the re-baseline /
per-frame-clamp / focal-jump guards already proven on the note
canvas.
Zoom + pen feel are device-validated. analyze clean, tests green.
Replace the rejected standalone sticky-card board with the real
feature: place a link anchor anywhere on a PDF page, tap it to open
split view whose right pane is THAT anchor's own infinite scratchpad
(keyed by anchor id) — like a paper sticky-note tab.
- ScratchLink model + scratch_links table (id, doc, page, nx, ny).
- PDF editor: "place link" tool drops/loads/shows tappable markers;
tap opens SplitViewScreen for that anchor; long-press deletes.
- SplitViewScreen rebuilt on pdfrx (was syncfusion), right scratchpad
keyed by scratchLinkId, new brush palette (was AnnotationToolbar).
- Remove board_screen + its test + the home board entry.
analyze clean, tests green.
Replace the 2-tool ink system with a data-driven, Krita-style
BrushProfile (lib/editor/engine/brush.dart). Adding a brush is a
const map entry, not render-path branching.
Four presets from the rnote/krita spec:
- fountain pen: quadratic (p^2) pressure, wide dynamic width
- ballpoint: near-constant width (thinning 0.15)
- highlighter: flat width, square caps
- pencil: sqrt(p) pressure, moderate width
Pressure is pre-warped per brush via PressureCurve(gamma) before
perfect_freehand; geometry fields (thinning/streamline/smoothing/
caps) flow through the shared stroke recipe so the PDF overlay and
the note/slide PenCanvas both honor the brush. Brush kind is now
persisted on the stroke model. Picker added to all three toolbars.
Opacity/multiply and pencil grain are carried as data but not yet
composited (TODO brush-opacity / brush-texture); this increment is
width + pressure-curve differentiation. analyze clean, 283 tests.
Replace the single-page PdfPageView bitmap with a pdfrx
PdfViewer: real vector text, continuous scroll, native
pinch-zoom (no custom zoom solver, so no zoom-jump here).
Ink is glued per-page via pageOverlaysBuilder; the pen is
captured at the viewer level by PenCaptureRegion while touch
falls through to scroll / pinch / text-select.
Add select-text -> highlight via PdfTextSelectionParams: the
selection's fragment rects are stored as normalized page rects
and drawn under the ink. In-memory only for now.
Per-page persistence, undo/redo, tools, colors, thumbnails and
pen settings are reused verbatim. analyze clean, 270 tests green.
The note and slide palettes lacked the settings gear the PDF editor has,
so width / pressure / eraser-size+mode / palm-rejection were unreachable
there (the user's 'toolbar 少了很多东西'). Add the gear to both; it opens
the existing rich pen-settings sheet.
flutter analyze: 0 issues.
Device log showed the applied scale oscillating ~1.4x every frame while
the raw pinch was smooth (cur 1.116->0.797->1.074, raw ~0.46). Root cause:
on a pointer-count re-baseline (Windows touch flickers 2<->1<->2 mid-pinch)
the code set _scaleStart = matrix.getMaxScaleOnAxis() — a read-back captured
at a glitchy instant — so the absolute map K = scaleStart/rawScaleAtBaseline
jumped frame to frame.
Fix: anchor the re-baseline to the CLEAN tracked _lastAppliedScale instead
of the live matrix read-back, so the displayed scale is continuous across
the re-baseline regardless of any matrix transient. The math is already
covered by the pinch_scale_solver "same-instant re-baseline" test; this just
feeds it the right value.
flutter analyze: 0. pinch_scale_solver + pen_zoom: pass.
PPT slides now annotate with the single performant inking engine
(PenCanvas) instead of the old ink_canvas, per "all note features on the
pen-first canvas".
- PenSlideScreen: per-slide normalized strokes over each slide image,
prev/next + slider nav, undo/redo, shared M3 palette, and the pressure
curve / eraser size+mode / palm rejection from the shared canvas.
- slide_export: pure, tested export geometry. Because strokes are now
normalized to the page rect, the PDF exporter maps them straight into
each slide's draw rect — fixing the old exporter's known ink
misalignment (it guessed live-widget size).
- Route PPT import + open -> PenSlideScreen; delete the dead old
ppt_annotator_screen. (ink_canvas/annotation_toolbar remain for
split_view, the last old-canvas screen.)
Tests: slide_export geometry (4). flutter analyze: 0 issues. Suite: 269/269.
Notes now use the single performant inking engine (PenCanvas) instead of
the old ink_canvas, per "all note features on the pen-first canvas".
- ink_stroke_adapter: pure InkStroke<->PenStroke bridge (normalize against
a logical note page; drop non-freehand shapes/text). Round-trip tested.
- pen_palette_widgets: shared M3 ToolButton/PaletteDivider/RoundIconButton
so PDF + note editors use identical chrome (PenEditorScreen migrated to
them; its private copies deleted).
- PenNoteScreen: PenCanvas over a white logical page, undo/redo, title,
save -> Note.strokes (+ local OCR for search). Pressure curve, eraser
size/mode and palm rejection all inherited from the shared canvas.
- Route home (new/open) + search note hits -> PenNoteScreen; remove the
now-redundant "Pen Canvas (beta)" spike button; delete the dead old
note_editor_screen.
Tests: ink_stroke_adapter (5) + pen_note_screen widget (load + commit, 2).
flutter analyze: 0 issues. Full suite: 265/265.
The search-result document jump still opened the OLD PdfAnnotatorScreen,
the last live entry to it. Route it to PenEditorScreen instead, and add
an initialPage param to the editor so the jump lands on the hit's page
(clamped to the document range once it loads).
With this, PenEditorScreen is the ONLY reachable PDF surface; the old
annotator is now dead code (no remaining references).
flutter analyze: 0 issues. Full suite: 258/258.
"优化橡皮擦工具,你优化在哪" — the eraser already did segment erase, but
the radius was a hardcoded const with no size control and no whole-stroke
mode. Add both, OneNote/Notability-style:
- PenConfig: eraserRadius (0.005-0.1, default 0.02) + eraserWholeStroke
bool, with copyWith / JSON / setters.
- PenCanvas: uses widget.eraserRadius for the live hit area AND the cursor
preview (they stay in sync); eraserWholeStroke=true removes the whole
stroke on contact, false keeps the segment-split behavior.
- pen_editor threads both from PenConfig.
- pen-settings: new Eraser section — size slider + "Stroke Eraser" switch.
Tests: pen_eraser_mode_widget proves point-eraser keeps the untouched ends
while stroke-eraser deletes the whole stroke from the same pass.
flutter analyze: 0 issues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"手写笔就是一个带压感的手指,没有特殊适配" — correct. The new editor fed
RAW LINEAR stylus pressure into perfect_freehand, and the pressureGamma
config (a slider in pen-settings) was read ONLY by that slider's UI and
NEVER applied to a stroke. Dead wiring, like the rest.
Wire it for real:
- PenCanvas applies PressureCurve(floor, gamma) at capture, so stored
pressure carries the feel and live + PDF export replay identically.
- Natural defaults: gamma 0.7 (light touches register more width, rnote/
OneNote-like) + floor 0.12 (thin strokes keep body, not scratchy).
- pen_editor threads PenConfig.pressureGamma into the canvas — the slider
now actually changes stroke width.
- pen_config: natural default + one-time migration of the legacy inert
gamma 1.0, guarded by a marker so a deliberate 1.0 still sticks.
Tests: pen_config_gamma_migration (4) + pressure_curve (6) pass; pen
widget regressions green. flutter analyze: 0 issues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app had zero localization ("软件多语言做了吗" — no). Add Flutter's
official gen-l10n pipeline and localize the core flow the user sees.
- pubspec: flutter_localizations + intl + generate: true
- l10n.yaml + lib/l10n/app_en.arb + app_zh.arb (37 strings)
- main.dart: localizationsDelegates + supportedLocales (follows OS locale)
- pen editor: all tool tooltips, page pill, error states localized
- home: app bar actions + empty-state buttons localized
Proven end-to-end: l10n_test pumps the same widget under Locale('en')
and Locale('zh') and asserts English vs Chinese strings resolve.
flutter analyze: 0 issues. l10n_test: 3/3 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Device log showed a single-frame scale pop (cur 0.504->0.694, a
+38% jump UP while the pinch was still shrinking).
Root cause: the absolute mapping targetScale = scaleStart *
details.scale is only valid when details.scale is 1.0 at the
moment scaleStart is captured. That holds at gesture start, but
on a mid-gesture re-baseline (a finger blips 2->1->2, routine on
Windows touch) a fresh scaleStart got multiplied by the
recognizer's still-cumulative details.scale, popping the zoom
then snapping back.
Fix: track rawScaleAtBaseline and normalize details.scale against
it so the cumulative reads 1.0 at every baseline. Extracted
absolutePinchScale() pure solver + 5 unit tests covering the
exact re-baseline scenario.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires simplifyStroke into the save conversion: a fast Surface-Pen stroke's
hundreds of near-collinear samples are thinned before hitting the DB, shrinking
the row + speeding reload re-rasterization (R10) with no perceptible change. The
live in-memory strokes are untouched — only what we PERSIST is simplified.
Makes the (unit-tested) RDP core load-bearing.
flutter analyze lib/editor clean; 238/238 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the now-tested viewport_fit.centerOffset into the live editor, replacing
the inline ad-hoc arithmetic. Behavior-identical (same centering at scale 1) —
makes the pure core load-bearing and kills the duplicate math.
flutter analyze lib/editor clean; 228/228 tests pass (no behavior change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extracts the untested ad-hoc arithmetic from pen_editor_screen._centerPage into
pure, shared, tested functions: fitWidthScale (fill viewport width — the
continuous-single default), fitPageScale (letterboxed min-axis fit), and
centerOffset (top-left translation to center scaled content; negative when it
overflows/scrolls). Powers the viewport's initial transform + the reader
fit-width/fit-page actions.
flutter analyze lib/editor clean; 228/228 tests (+5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scoreText (more normalized occurrences rank higher; earlier first match breaks
ties) and rankHits (score every source, drop non-matches, attach a display
snippet of the original text, sort best-first with an explicit input-order
tiebreak since Dart's sort isn't stable). The search_indexer's pure ranking
core, making search_text + search_snippet load-bearing.
flutter analyze lib/editor clean; 223/223 tests (+9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Board + BoardCard: positioned sticky-note cards in board coordinates, immutable
copy-on-write edits (add/removeById/moveCard/setText, unique ids), cardsIn()
broad-phase culling, and linkGraph()/backlinksOf() that derive the 双链 graph
from the cards' [[links]] — making link_graph load-bearing. Cards also host ink
via a StrokeHost keyed by card id (same host-agnostic engine as PDF pages).
Pure model; fully unit-tested (CRUD, immutability, culling, backlinks).
flutter analyze lib/editor clean; 215/215 tests (+7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
ToolSettings: the active tool + each tool's OWN remembered ToolConfig
(color/width), so pen→highlighter→pen restores the pen's last color/width
instead of bleeding the highlighter's. Immutable copy-on-write (withActive/
withColor/withWidth only touch the active tool); sensible defaults (black thin
pen, yellow fat highlighter, medium eraser). The toolbar holds + persists one.
Pure model; fully unit-tested (per-tool isolation, immutability, equality).
flutter analyze lib/editor clean; 204/204 tests (+6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
simplifyStroke reduces a stroke's points via Ramer–Douglas–Peucker at a
normalized perpendicular-distance tolerance: a fast Surface-Pen stroke drops
hundreds of near-collinear samples with no visible change, shrinking the DB row
and speeding re-rasterization (R1/R10). Endpoints + significant vertices kept;
pressure/tilt + color/width/tool/id preserved; <=2 points or tol<=0 are no-ops
(returns the same instance). The commit path can call it before saveHost; the
live in-progress stroke stays untouched.
Pure geometry over EditorStroke; fully unit-tested (collinear collapse, peak
retention, within-tolerance drop, metadata preservation).
flutter analyze lib/editor clean; 198/198 tests (+7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
strokeBounds (tight AABB over normalized points, null for empty, zero-size for a
single point), strokesBounds (union), and strokeIntersects (does a stroke's box
overlap a viewport rect — touching edges count). Broad-phase primitive for the
infinite board: skip painting/erasing/hit-testing strokes off-screen (R1 perf),
and a cheap pre-filter before the exact per-point eraser test.
Pure geometry over EditorStroke; fully unit-tested.
flutter analyze lib/editor clean; 191/191 tests (+10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
normalizeForIndex (lowercase + collapse whitespace runs incl. hard PDF/OCR
newlines + trim) and matchesNormalized so a query matches across the line breaks
in raw extracted text. Deliberately NO word-tokenization: Chinese has no
inter-word spaces, so substring match over normalized text is correct for both
Latin and CJK (段/word segmentation belongs in the DB FTS tokenizer). Verified
on CJK inputs (你好/笔记应用).
flutter analyze lib/editor clean; 181/181 tests (+9).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Book-like reading the user explicitly wanted: pairIntoRows groups pages into
two-up spread rows (optional coverAlone for a title page; trailing odd page sits
alone), spreadRowHeights fits each page to half the column and takes the tallest
per row (common baseline), and spreadStackMetrics stacks the rows so the
existing PageStackMetrics.visibleRange windows continuous-DOUBLE by ROW.
Pure geometry reusing the continuous-single layer; the row-mounting widget is
device-gated. Zero-rework-risk (not rendering).
flutter analyze lib/editor clean; 172/172 tests (+8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For library-wide full-text search (the user's #1 named differentiator):
snippetFor() finds the first case-insensitive match of a query in a source
string (PDF text page / typed box / OCR'd handwriting) and returns a windowed
excerpt centered on it, preserving the match offset + length and
truncatedStart/End flags so the results list can render "…ctx **match** ctx…"
and jump to the hit. The full match is always included; near-edge matches don't
over-truncate. Returns null for empty/absent query.
The FTS index + ranking live in the DB (search_indexer, later); this is the
pure, storage-free excerpt math, fully unit-tested.
flutter analyze lib/editor clean; 164/164 tests (+8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SpeedyNote-style page binding the user asked for: a notebook is an ordered
list of logical pages, each a SOURCE page (renders PDF page N, vector preserved)
or a BLANK inserted page. Crucially, inserting/reordering logical pages does NOT
renumber the PDF underlay — each page carries its source index. Copy-on-write
edits (insertBlankAt/After, removeAt, move) return a new immutable PageMap;
out-of-range edits throw RangeError; value equality + unmodifiable page list.
Pure model (no DB/widget) so it's fully unit-tested; the notebook_pages table +
viewport wire it later.
flutter analyze lib/editor clean; 156/156 tests (+11).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The user's repeated "可配置笔" ask, as a pure value type: raw normalized
pressure is pre-shaped into [floor, 1] via a min-width floor (the plan's
marker fixed-pressure floor) and a gamma response (γ<1 = more sensitive at light
touch, γ>1 = firmer). Clamps out-of-range + NaN inputs; endpoints anchored at
floor and 1. Widget-free/storage-free; PenConfig + the canvas wire it later
(live-path, on-device validated).
flutter analyze lib/editor clean; 145/145 tests (+6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure bidirectional-link engine for the sticky-note/board system (a user-named
differentiator). parseLinkTargets extracts trimmed, de-duped [[targets]];
LinkGraph builds forward + backlink indices (fromTexts parses, fromLinks takes
explicit targets), ignores self-links, and danglingTargets() surfaces links to
unknown nodes. Widget-free + storage-free so it is fully unit-tested; the board
UI + persistence wrap it later.
Built ahead of its phase deliberately as a zero-rework-risk pure data structure
(not rendering/perf — the P0.5 device gate can't invalidate it).
flutter analyze lib/editor clean; 139/139 tests (+10: parsing, backlinks,
self-link, dangling, immutability).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends PageStackMetrics with the navigation geometry continuous-single needs:
maxScrollExtent (last page bottom rests at viewport bottom, never negative),
clampScroll, and dominantPageAt — the page covering most of the viewport, which
drives the page-number indicator + thumbnail-grid highlight + jump-to-page (F4).
Pure; clamps past both ends; 0 for empty documents.
flutter analyze lib/editor clean; 129/129 tests (+6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Production adapter wrapping a pdfrx PdfDocument: snapshots page sizes via the
real pdfrx 2.4.4 geometry API (PdfDocument.pages, PdfPage.width/height) so the
pure layout math runs on the real document. Because it lives under lib/editor/,
`flutter analyze lib/editor` (the Oracle) type-checks it against the installed
pdfrx every run — a version bump that renames/retypes these members now FAILS
analysis instead of silently drifting (SF4 source-pin, statically).
Runtime contract + layout composition tested via the pdfium-free .fromSizes
ctor; the .fromDocument pin is the static guarantee (exercising it needs pdfium
= device path).
flutter analyze lib/editor clean; 123/123 tests (+3).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Connects the two pure P0.5 pieces: a minimal PageDocumentSource abstraction
(pageCount + pageSize, a pdfrx PdfDocument in production) and
pageStackMetricsForWidth() which fits every page to a single column width
(continuous-single) — height = columnWidth × aspect — feeding
PageStackMetrics.visibleRange. Defensive against non-positive page width.
This makes the windowing math consumable + unit-testable against a fake source
(no pdfium/GPU); the production pdfrx adapter is the thin device-side wrapper
added with the page-mounting widget.
flutter analyze lib/editor clean; 120/120 tests (+5: fit-to-width, gap, empty,
zero-width guard, windowing composition).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PageStackMetrics + PageWindow: the pure geometry that decides which pages are
mounted for a scroll position (windowed lazy hosting → 60fps on a 300-page doc,
R1). Pages stack vertically with cumulative tops (O(log n) binary-search
visibleRange); a viewport [scroll, scroll+extent) grown by cacheExtent on each
side selects the inclusive intersecting page band, half-open at page boundaries,
clamped to valid indices, empty for empty/over-scrolled-past documents, and
gap-aware (a scroll resting inside an inter-page gap shows no page).
Widget-free + pdfrx-free by design: the page-mounting widget and zoom-settle DPI
refresh are device-gated; only the windowing math is automatable, and it is here
with exhaustive unit coverage (boundaries, cache band, clamping, gaps, empty).
flutter analyze lib/editor clean; 115/115 tests (+13).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>