/** * Scene-graph data model v1 for Moraya Board (teaching whiteboard). * * Design principle (spec §3, moraya docs/specs/teaching-whiteboard.md): * structured elements ALWAYS keep their source of truth — `formula` stores * LaTeX source, `text` stores plain text. Rendering is a derived view, which * is what makes lossless board ↔ Markdown extraction possible. Strokes and * shapes live only in the board file; they have no Markdown representation * by design. */ declare const SCENE_TYPE: "moraya-board"; declare const SCENE_VERSION: 1; /** JSON-serializable value; the payload a `CustomElement` may carry. */ type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue; }; /** Properties shared by every element on the canvas. */ interface BoardElementBase { /** Stable unique id (crypto.randomUUID() at creation time). */ id: string; /** Top-left of the element's bounding box, scene coordinates. */ x: number; y: number; /** Rotation around the bounding-box center, radians. */ rotation?: number; /** Paint order; higher renders on top. Ties break by array order. */ z?: number; /** Stroke/text color, CSS color string. */ color?: string; /** 0..1; omitted = 1. */ opacity?: number; /** * Optional hyperlink target (http/https/mailto). Rendered as an `` wrapper. * Hosts MUST sanitize before setting; the renderer also drops unsafe schemes. */ link?: string; /** * Group membership, innermost first (nested groups append outward) — the * Excalidraw grouping model. Selecting any member selects the whole group. */ groupIds?: string[]; /** * Locked: still rendered, but excluded from marquee/erase and not * movable/resizable/deletable until unlocked. Omitted = unlocked. */ locked?: boolean; /** Mirror horizontally around the bbox center (visual only). Omitted = no. */ flipX?: boolean; /** Mirror vertically around the bbox center (visual only). Omitted = no. */ flipY?: boolean; } /** * An arrow endpoint's attachment to a shape. `focus` is reserved for * off-center attachment fidelity (0 = aim at the target's center); `gap` is * the visual distance kept between the arrowhead and the shape's edge. */ interface ArrowBinding { elementId: string; focus: number; gap: number; } /** Freehand ink. Point runs are fed to perfect-freehand at render time. */ interface StrokeElement extends BoardElementBase { type: 'stroke'; /** [x, y, pressure] triples relative to (x, y). Pressure 0..1, 0.5 = unknown. */ points: Array<[number, number, number]>; /** Base stroke size in scene units. */ size: number; } /** * Natural rendered size of a text/formula element, in scene units. * * The core package has no DOM and cannot measure KaTeX HTML or wrapped text, * so a host supplies these via a MeasureFn (see MEASURE contract). Persisting * the result means re-opening a board — or exporting it from a pure-Node * toolchain with no host — needs no re-measurement. */ interface Measured { width: number; height: number; } type ShapeKind = 'line' | 'arrow' | 'rect' | 'ellipse' | 'diamond'; /** Sketchy fill pattern (Excalidraw), used when `sloppiness` > 0. */ type FillStyle = 'hachure' | 'crossHatch' | 'solid'; /** Stroke dash pattern. */ type StrokeStyle = 'solid' | 'dashed' | 'dotted'; /** Hand-drawn roughness: 0 = clean (architect), 1 = artist, 2 = cartoonist. */ type Sloppiness = 0 | 1 | 2; /** Corner treatment for rect/diamond. */ type Edges = 'sharp' | 'round'; /** Arrow/line endpoint decoration. */ type Arrowhead = 'none' | 'arrow' | 'triangle' | 'bar' | 'dot'; interface ShapeElement extends BoardElementBase { type: 'shape'; kind: ShapeKind; /** Bounding-box size; line/arrow use the box diagonal (x,y)→(x+w,y+h). */ width: number; height: number; strokeWidth: number; /** CSS color; omitted = transparent (no fill). */ fill?: string; /** Arrow/line endpoint attachments (arrows follow the bound shapes). */ startBinding?: ArrowBinding; endBinding?: ArrowBinding; /** Sketchy fill pattern (applies when sloppiness > 0). */ fillStyle?: FillStyle; /** Stroke dash pattern. */ strokeStyle?: StrokeStyle; /** Hand-drawn roughness; undefined/0 = clean primitives. */ sloppiness?: Sloppiness; /** Corner treatment (rect/diamond). */ edges?: Edges; /** Endpoint decorations (line/arrow). Undefined arrow keeps the default marker. */ startArrowhead?: Arrowhead; endArrowhead?: Arrowhead; /** * Multi-point polyline vertices (line/arrow only), relative to (x,y) with * `points[0] === [0,0]`. Absent = the straight segment (x,y)→(x+width,y+height). * By convention `points[last]` equals `[width, height]`, so the two endpoints * are always the first/last vertex. */ points?: Array<[number, number]>; /** * Elbow (orthogonal) arrow: `points` is an auto-generated right-angle route * between the two endpoints, recomputed on move/bind. Interior vertices are * not user-editable while this is set. */ elbow?: boolean; } /** Horizontal text alignment (multi-line). Omitted = left. */ type TextAlign = 'left' | 'center' | 'right'; /** Vertical placement of a container-bound label within its shape. */ type VerticalAlign = 'top' | 'middle' | 'bottom'; interface TextElement extends BoardElementBase { type: 'text'; /** Plain-text source of truth (extractable to a Markdown paragraph). */ text: string; fontSize: number; /** CSS font stack; omitted = the renderer's default sans stack. */ fontFamily?: string; /** Horizontal alignment for multi-line text; omitted = left. */ textAlign?: TextAlign; /** Vertical placement inside the bound container; omitted = middle. */ verticalAlign?: VerticalAlign; /** * Bound container: this text is a label centered inside the shape with this * id, following it on move/resize and deleted with it. Omitted = free text. */ containerId?: string; /** Host-measured natural size; absent until first measured. */ measured?: Measured; } interface FormulaElement extends BoardElementBase { type: 'formula'; /** * LaTeX source of truth, including mhchem chemistry (`\ce{...}`, `\pu{...}`). * Extractable to Markdown as `$$...$$` (display) or `$...$` (inline). */ latex: string; /** display = block math ($$…$$); inline = $…$. */ display: boolean; fontSize: number; /** Host-measured natural size; absent until first measured. */ measured?: Measured; } /** Visible sub-region of an image's source, as fractions [0,1] of the natural image. */ interface ImageCrop { x: number; y: number; width: number; height: number; } interface ImageElement extends BoardElementBase { type: 'image'; /** data: URI (self-contained) or a path relative to the .mboard.svg file. */ src: string; width: number; height: number; alt?: string; /** Source crop; absent = the whole image is shown. */ crop?: ImageCrop; } /** * A video placed on the board. * * Unlike `image`, `src` is ALWAYS a URL to a hosted asset (a Picora playback * URL — HLS `.m3u8` or a direct file), never a `data:` URI: inlining video * bytes would bloat the scene JSON past document-size limits. Playback is a * live-host concern; in exported/rasterized/sanitized SVG the element degrades * to its `poster` (see renderVideo). HLS sources require the host to attach an * HLS loader (e.g. hls.js) to the