Context
- The editor scene is renderer-local reactive state (
Scene, signal-backed). Nodes are addresses: GlyphNode carries glyphId + sourceId + a scene position.
- There is one undo authority: the workspace ledger (state-pair replay in the utility workspace). Scene mutations (
moveNodeTo, add/remove node) are not undoable today.
- Node kinds will grow:
ImageNode, TextNode, plugin nodes.
Settled direction
- A node is an address, not an owner. Editing through a node routes to the domain that owns the node's content, and each domain brings its own commit path:
GlyphNode content → GlyphLayer → FontIntent (exists today; Translate resolves ids to one loaded layer).
ImageNode content that is font data (per-layer background image) → FontIntent, even though the interaction feels scene-local.
- Scratch/annotation content → scene payload (below).
- Rust never interprets scene semantics. The boundary: Rust owns data consumed by font semantics (validate / interpolate / compile / export), plus at most dumb ordering and persistence. The frame loop and the live scene stay in the renderer.
- Scene placement becomes undoable via opaque replay entries, not scene-aware intents:
- The renderer produces before/after state pairs (opaque blobs) for scene mutations.
- The ledger stores and orders them; on undo/redo the echo returns the blob and the renderer applies it.
- One generic entry kind + one table in the working store. Rust stores and sequences; it never reads inside.
- The same mechanism is the escape hatch for plugin-node content edits (payload keyed by plugin id).
- The live scene remains a renderer signal projection. The workspace holds replay blobs and (optionally) persisted scene records;
Scene projects them the way Font projects FontStore. No second renderer-local undo stack — interleaving happens in the one ledger.
Product driver: TextNodes as proof blocks
TextNode is an SVG-preview-only text run: a shaped glyph run rendered from compiled/interpolated outlines, movable on canvas like a text shape in Figma.
- No editable geometry, no entries in the glyph object ownership index, no hit-testable points — only bounds (and per-glyph boxes for interaction).
- Proofing sheets: arrange words and spacing contexts around the glyph being edited; the same block pinned at multiple design-space locations gives a live multi-master proofing grid.
- Previews update reactively when referenced glyphs change (ties into incremental compilation and the harfrust shaping plan). Preview lag after an edit is acceptable; stale editable geometry is not.
- Double-click a glyph inside a TextNode → open/focus a
GlyphNode for it.
- This is what makes scene arrangement a work product, which is why placement needs persistence and undo (items 3–4 above).
Constraints
- TextNode stays proofing-oriented: string, size, design location, direction/script/features. No styling/layout feature growth.
- Future kerning-drag inside a TextNode is a font data edit and must lower to a
FontIntent, never a scene payload.
- A node drag coalesces into one replay pair, committed on drag end (mirrors position-patch drafts).
- Preview-only node kinds may recompute asynchronously; editable geometry must remain synchronous with input.
Open questions
- Persistence transport: does scene state ride the ledger or a sidecar (session blob in the
.shift container)? If the ledger persists across sessions, scene replay entries need a defined no-op rule when the referenced scene no longer exists.
- Do placement edits group with content edits inside
editor.transaction(...) as one undo step?
- Are selection changes ever undoable? (Leaning no.)
- Opaque payload versioning and size limits for plugin entries.
- TextNode shaping inputs: where do script/direction/features live — per node, per run, or font defaults?
Context
Scene, signal-backed). Nodes are addresses:GlyphNodecarriesglyphId+sourceId+ a scene position.moveNodeTo, add/remove node) are not undoable today.ImageNode,TextNode, plugin nodes.Settled direction
GlyphNodecontent →GlyphLayer→FontIntent(exists today; Translate resolves ids to one loaded layer).ImageNodecontent that is font data (per-layer background image) →FontIntent, even though the interaction feels scene-local.Sceneprojects them the wayFontprojectsFontStore. No second renderer-local undo stack — interleaving happens in the one ledger.Product driver: TextNodes as proof blocks
TextNodeis an SVG-preview-only text run: a shaped glyph run rendered from compiled/interpolated outlines, movable on canvas like a text shape in Figma.GlyphNodefor it.Constraints
FontIntent, never a scene payload.Open questions
.shiftcontainer)? If the ledger persists across sessions, scene replay entries need a defined no-op rule when the referenced scene no longer exists.editor.transaction(...)as one undo step?