Serialization
Serialize a Stage to a plain-data document (or JSON string) and load it
back. Round-trips are exact for built-in shapes (ids, transforms, style, geometry).
import { SceneSerializer, ClassRegistry, createDefaultRegistry, MigrationRunner, CURRENT_SCHEMA_VERSION,} from '@veyrajs/core'import type { SceneSerializerOptions, NodeFactory, Migration, SceneDocument, SerializedNode,} from '@veyrajs/core'SceneSerializer
Section titled “SceneSerializer”The serialization entry point. toDocument/stringify save; load/parse restore.
interface SceneSerializerOptions { registry?: ClassRegistry // custom type factories (default: createDefaultRegistry()) migrations?: MigrationRunner // upgrades older documents on load}
class SceneSerializer { constructor(options?: SceneSerializerOptions)
toDocument(stage: Stage): SceneDocument // serialize the stage's layers stringify(stage: Stage, space?: number): string // toDocument + JSON.stringify fromObject(data: SerializedNode): Node // build a node + its descendants load(stage: Stage, document: SceneDocument): void // replace stage content parse(stage: Stage, json: string): void // load from a JSON string}- Save serializes the stage’s layers (its children) via each node’s
toObject(). The stage itself (size, camera) is viewport state and is not serialized. - Load runs migrations, clears the stage, then rebuilds each top-level node with
fromObject(which recurses intochildren), and requests a render.
const s = new SceneSerializer()localStorage.setItem('scene', s.stringify(stage))s.parse(stage, localStorage.getItem('scene') ?? '{"version":1,"nodes":[]}')ClassRegistry
Section titled “ClassRegistry”Maps a serialized type string to a factory that constructs the node — the extension point for
custom node types.
type NodeFactory = (data: SerializedNode) => Node
class ClassRegistry { register(type: string, factory: NodeFactory): this // chainable has(type: string): boolean create(data: SerializedNode): Node // throws on unknown type}
function createDefaultRegistry(): ClassRegistry // every built-in type registered- Factories build the bare node; the serializer recurses into
data.children. - Unknown types throw rather than silently dropping nodes, so a missing plugin is loud.
const registry = createDefaultRegistry().register('BBox', (d) => new BBox(d as never))new SceneSerializer({ registry }).load(stage, doc)MigrationRunner
Section titled “MigrationRunner”Upgrades older documents to the current schema version on load.
interface Migration { from: number migrate(doc: SceneDocument): SceneDocument // must advance `version`}
class MigrationRunner { register(migration: Migration): this // chainable run(doc: SceneDocument): SceneDocument}- One step at a time. Register
0→1,1→2, … and the runner chains them untilversion === CURRENT_SCHEMA_VERSION. - Pure transforms — return a new document with the new
version; no side effects. - Missing migrations are loud — an intermediate version with no registered path throws (also guards against a migration that fails to advance the version).
const migrations = new MigrationRunner() .register({ from: 0, migrate: (d) => ({ ...d, version: 1, nodes: renameFills(d.nodes) }) })new SceneSerializer({ migrations }).load(stage, oldDoc)Document types
Section titled “Document types”const CURRENT_SCHEMA_VERSION: number // current on-disk format version (bumped on changes)
interface SceneDocument { version: number // schema version the document was written with nodes: SerializedNode[] // the top-level layers}
interface SerializedNode { type: string // registered type name id: string children?: SerializedNode[] // serialized children (containers) [key: string]: unknown // type-specific props}SerializedNodeis intentionally loose ([key: string]: unknown); strong typing lives at the boundaries (toObjectproduces it, registry factories consume it back into typed configs).versionis per document (the on-disk format, drives migrations) — distinct from the package’s runtimeVERSION.
- Replaces, not merges.
load/parseclear the stage first; clear your selection and history around a load (the old node instances are gone). - Images need re-attaching. Only an image’s size round-trips; reassign its
imageafter load (assets are referenced, not embedded). - Custom types require their factories registered on the
registryyou pass in.