Skip to content

Design scene node editing: content vs placement, opaque ledger replay, TextNode proof blocks #113

Description

@kostyafarber

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

  1. 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 → GlyphLayerFontIntent (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).
  2. 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.
  3. 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).
  4. 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions