ConfigurationSerializer
Serialises and restores ComponentMgr block configurations to and from a portable JSON structure. All methods are static. No DOM dependency.
Warning
Meant to be used with ComponentMgr only.
Getting started
import { ConfigurationSerializer } from './configurator';
// Save current scene
const config = ConfigurationSerializer.serializePlacedBlocks(componentMgr);
localStorage.setItem('config', JSON.stringify({ blocks: config }));
// Restore a scene
const saved = JSON.parse(localStorage.getItem('config'));
const { placed, skipped } = ConfigurationSerializer.restorePlacedBlocks(
saved,
componentMgr,
blocks, // Map<blockId, Snappable> from GltfBlockParser.parse()
missingBlockIds, // optional — IDs to skip gracefully
);
console.log(`Restored: ${placed} blocks, skipped: ${skipped}`);
Methods
ConfigurationSerializer.serializePlacedBlocks(componentMgr) → BlockData[]
Snapshots all placed blocks from componentMgr.objects into a serialisable array.
- Parameters:
componentMgr—ComponentMgr - Returns: Array of plain objects (one per placed block)
Each object has the shape:
{
blockId: string, // e.g. "leg-short"
parentBlockId: string | null, // parent block's ID, or null for root blocks
socketLocks: [
{
socketIndex: number, // index in this block's socket array
lockedPartnerBlockId: string, // partner block's ID
lockedPartnerSocketIndex: number, // index in partner block's socket array
}
],
rotationSteps: number, // 0–7 (normalised to 8-step range)
rotationDegrees: number, // rotationSteps × 45
}
ConfigurationSerializer.restorePlacedBlocks(config, componentMgr, blocksMap, missingBlockIds?) → { placed, skipped }
Clears the current scene and restores blocks from a saved configuration.
- Parameters:
config— object with ablocksarray (as returned byserializePlacedBlocks, wrapped in{ blocks: [...] })componentMgr—ComponentMgr— will be cleared and repopulatedblocksMap—Map<string, Snappable>— the template map fromGltfBlockParser.parse()missingBlockIds—string[], optional — block IDs that are known to be absent; they and their dependants are skipped gracefully
- Returns:
{ placed: number, skipped: number }
Restoration order:
- All existing blocks are removed from
componentMgr - Block instances are created from templates (one clone per entry in
config.blocks) - Blocks are sorted topologically — parents before children — so that when a child block is placed, its parent already exists in the scene. The sort works iteratively: each pass places any block whose
parentBlockIdis eithernullor already in the placed set. This handles arbitrary tree depths without recursion. - Socket locks are restored and
snapItemToSocketis called to position each child block at the correct snap location relative to its parent socket - Circular references and missing parents are detected and handled without throwing — see details below
Handling cycles and missing parents:
- Circular reference (
Ais parent ofB,Bis parent ofA): the topological sort detects that neither block can be placed first (both are waiting on the other). When a pass completes without placing any block, the deadlocked remainder is placed as-is with aconsole.warn, andparentBlockis set tonullon any block involved in a cycle. - Missing parent (parent's
blockIdnot found inblocksMapor listed inmissingBlockIds): the child is treated as a root block —parentBlock = null— and placed without snapping. It appears at the world origin.
Notes
rotationStepsis normalised to[0, 7](8 steps × 45° each) on serialise. On restore it is converted back to radians.- If a
blockIdin the config is not found inblocksMap, the block is counted asskipped. Pass known-missing IDs inmissingBlockIdsto suppressconsole.warnoutput for IDs you already know are absent (e.g. a block type that was removed from the GLTF in a newer version). Any block whoseparentBlockIdis in the missing set is also skipped, since it cannot be positioned without its parent. - Primary socket lock: each block in the config records all its locked socket pairs in
socketLocks. The first entry (socketLocks[0]) is the "primary" lock — it is the one used to callsnapItemToSocketand physically position the block. Any additional locks (e.g. a block that straddles two sockets on the same parent) are restored as lock-state-only: thelockedPartnerreferences are set on both sockets, but no position update is applied. This is acceptable because a block can only be positioned relative to one anchor at a time; the extra locks are informational. The primary lock is always the first socket in iteration order that was found to be locked at serialise time. missingBlockIdsis provided as a separate parameter (rather than inferred fromblocksMap) because there are scenarios where a block template exists inblocksMapbut you still want to skip specific instances — for example, when restoring a partial configuration or when a block has been marked as deleted in your application layer.