Snapshot after the 2026-06-24 session. Supersedes the 2026-06-23 notes below the line. No IIIF / no W3C Web Annotation — Apache Arrow is the native model.
- Phase 1 carve (
c89429a): framework-agnosticsrc/lib/engine/(zerosvelte/$app), barrelindex.ts;PixiCanvas.svelte→src/lib/svelte/. - Phase 2 store decoupling:
src/lib/engine/store/{AnnotationStore,LayerStore}.tsare now plain-TS + an observer (on/emit"structural"/"field");src/lib/stores/*.svelte.tsare thin runes adapters (tick-bump). Oldundo.svelte.tsdeleted. - Transport injection (THE packaging blocker, fixed):
AnnotationStoreno longer hardcodesfetch('/api/annotations/…'). It takes an injectedAnnotationTransport(src/lib/engine/store/transport.ts); the HTTP impl issrc/lib/stores/httpAnnotationTransport.tsin the binding. Engine is now genuinely host-agnostic (in-memory / gRPC / streaming-inference transports drop in).
appendManyLocalconcats ONE ArrowRecordBatch(built from the base schema's exact types — Float32/Dictionary/List preserved) instead of rebuilding the whole table. Measured: ~1294 ms → ~2 ms to append into a 100k table; correctness verified (shape_type + base64 mask round-trip).appendLocaldelegates. Streamed model inference uses the same path.
- Brush produces a real raster mask (was a polygon "box" — caused by Toolbar↔engine brush-option desync; engine is now the single source of truth, Toolbar reflects it via
brushOptions). - Masks are editable (bbox editor + sprite follows geometry overrides).
- Alt+click deletes a polygon vertex; modifier state synced from the pointer event.
- Mask GPU-texture leak +
img.onload-on-destroyed-sprite race fixed;hover/highlightrender-on-clear (the stale selection "box").
- P0 path-traversal closed on all
[pageId]API routes (src/lib/server/ids.tssafePageId). - P0 empty-page schema drop fixed: empty table now carries all 23
ANNOTATION_COLUMNS(was silently droppingshape_type/mask/rotation/… from the first shape on an empty page).
ImagePlugin: texture/source destroy on swap+teardown; cachedColorMatrixFilter; pointer-capture pan.Magnetic/Scissorstools:requestRender(render-on-demand), removedconsole.log, Douglas-Peucker → sharedsimplifyPath.
Done: both P0s (above) + transport injection + mask leaks/race + render-on-clear + Magnetic/Scissors render + ImagePlugin leaks + ScissorsTool DP-dedup + encodeURIComponent + streaming _loading finally.
Remaining (P1):
noUncheckedIndexedAccessOFF → 124 masked OOB/undefined accesses across 11 engine files. Enable + fix.canUndo/canRedoare global, but undo/redo are per-page → make page-scoped (threadpageIdthrough adapter + Toolbar).selectedSetnot cleared on undo/redo/Escape → fold intocancel().- per-
pageIdcaches/undo stacks never evicted → adddispose(pageId)on page unmount. tableFromArrayswidens Float32→Float64 on rematerialize/save (incremental append already preserves it; pin schema on the rematerialize/save path too).- ESLint: no config exists → add flat
eslint.config.js+ typescript-eslint + svelte plugin;tsc --noEmitin CI.
We re-rasterize 5 monolithic Graphics on every transform with no culling, bake size-into-geometry (zoom ⇒ full rebuild), run two raw-DOM event stacks, and hand-roll math Pixi shapes already do. Top 5 (impact ÷ effort):
- Register
CullerPlugin(PixiCanvas.svelte) — ourcullable=trueis inert (plugin never added); pan never culls. S. isRenderGroupon the annotation container — GPU pan/zoom transform. S.Assets.load({src, parser:'texture'})for image + base64 masks (off-main-thread decode + dedup;parserhandles our extension-less/api/pagesURLs). S.ParticleContainerfor point annotations (~1/3 of data) — one draw call; constant radius viascale=1/zoom. L.- FederatedEvent system —
stage.eventMode='static'+globalpointermove/pointerupoutside; delete the dual DOM stack,setPointerCapturebookkeeping, andgetBoundingClientRect-per-event. L (incremental).
Real bug found: line/open-polyline annotations have no fill area → only selectable by bbox. Fix with Polygon.strokeContains.
Perf wall (100k): cacheAsTexture(true) on the static layer · spatial tile grid of isRenderGroup sub-Containers (a 1-row edit currently rebuilds the whole monolith → the 1.1s freeze) · split sync() into geometry-build vs per-frame cull · onRender counter-scaling (kill zoom re-sync) · boundsArea on the container · OffscreenCanvas + WebWorkerAdapter (materialize+draw off-thread — the eventual ceiling for streamed inference) · mask atlas/spritesheet.
Viz moat (the FiftyOne differentiator): today's "heatmap" is a fake per-row tint. Real: GPU density field = one viewport quad Mesh + Shader.from({gl,gpu}) with region centers in a UniformGroup/data-Texture → constant cost regardless of N; FillGradient saliency; blendMode:'add' masks; single-channel mask data-Texture + colormap; native graphicsContextToSvg export. Same shader layer is modality-agnostic (runs on a video frame texture).
Simpler math/events: stage.toLocal(event.global) / toGlobal (handles rotation — our scalar transform breaks under oriented boxes) · Polygon.contains/strokeContains · Rectangle.contains/intersects/pad · interactive eventMode='static' handles (Pixi does hit-test+hover+cursor) · modifiers off the event · renderer.events.cursorStyles · math-extras.
Leave custom: the Arrow columnar store + transport + RecordBatch append; render-on-demand policy; tool state machines; the overlay/WAL.
@ra-anno/engine (all framework-agnostic: arrow + pixi + interaction) + @ra-anno/svelte (binding). One npm install; split arrow/pixi later as it grows. Stay on TS7 (consumed as source via exports→./src/index.ts, no composite/project references). Keep core/canvas as separate internal modules so audio/video later is a file-move, not a rewrite. Fold the god-file decomposition (InteractionManager, ArrowDataPlugin) into the move.
- Surface new schema fields in UI: oriented-box (rotation handle), point/line/baseline editors, relations (
links), reading-order, region-type dropdown,difficult. - HTR: region→line→word hierarchy + reading order; baseline + auto-mask + transcription.
- Multimodal (one Arrow store, modality-discriminated columns +
ModalityViewerinterface): image=Pixi (have) · video=PixiVideoSourcesprite + overlay + DOM timeline · audio=peaks.js · text=DOM spans. Sequence: image → text+embeddings → audio → video. - Inference streaming at scale (~1M images): attach ML backends (e.g. NVIDIA LocateAnything VLM grounding → boxes/points/masks). Predictions stream back as Arrow RecordBatches →
appendManyLocal(O(appended)) → progressive render. Next: anInferenceProviderabstraction parallel to the transport; render-side culling/ParticleContainer for the volume. - Persistence/data-flow gaps (from the read/write trace):
X-Deleted-Idsheader overflows on bulk delete (>~500 ids) → move to body; field overlays not materialized into the cached table until save; server ETag versioning is in-memory only; no autosave/WAL checkpoint. Real columnar backend (Lance) replacingstatic/mock/*.arrow(the PATCH rebuilds the whole table).
- Magnetic + Scissors wiring (user chose "wire up"): lazy
initWithImageon first select (8 MB OpenCV) + loading state — paused pending theassetsfinding (useAssets.loadfor the image element). - Then: apply the pixi go-native Top 5 + the remaining TS P1s, then the package restructure.
- No IIIF, no W3C Web Annotation model (Arrow native).
- Skip: cuboid/3D boxes, pose/keypoints, depth, VQA chatbot, in-app training, the COCO model zoo.