5.1 KiB
@suaveplan/control-surface-schema — universal control-surface JSON schema
- Target path:
packages/media/audio/control-surface-schema/ - Tier: universal (zero React/3D/audio coupling — pure data + Zod)
- Date: 2026-05-23
- Status: PROPOSED
Why
Every downstream control-surface renderer — 2D React (@suaveplan/react-control-surface), 3D R3F (@suaveplan/r3f-control-surface), VR/XR runtimes, future native ports — needs to read the same surface definition: nested groups of atomic controls bound to plugin parameters / MIDI CCs / DMX channels, with persistable layout and skin information.
If each renderer owns its own format, they diverge. The same surface saved by a 2D editor can't be opened by the 3D editor. Cross-platform replication (the live-DJ-in-gaming-session scenario) becomes impossible.
This package owns the canonical schema. It is the source of truth for surface persistence, the validation layer at every save/load boundary, and the shared vocabulary every renderer + editor + replication layer agrees on.
What changes
- NEW package
@suaveplan/control-surface-schemaatpackages/media/audio/control-surface-schema/. ControlSurfaceroot document type:id: string(uuid),name,version(semver of the schema),createdAt,updatedAtskin: { id: string; vars?: Record<string, string> }— references a skin package + optional CSS-var overridesroot: ControlNode— nested group tree
ControlNodediscriminated union:Group—{ type: "group"; id; label?; layout: GroupLayout; transform: Transform; children: ControlNode[]; collapsible?: boolean }Control—{ type: "control"; id; kind: ControlKind; label?; transform; appearance?; binding?; range?; behaviour? }
ControlKindenum —knob,slider-v,slider-h,button-momentary,button-latching,switch-2,switch-3,led,digital-display,meter-vu,meter-peak,meter-spectral,xy-pad,jog-wheel,piano-key,drum-pad,text-label,image.Transform—{ position: { x; y; z? }; rotation?: { x?; y?; z? }; scale?: { x?; y?; z? } }.zand rotation/scale fields are optional so 2D-only surfaces stay clean.GroupLayoutdiscriminated union —Grid({ kind: "grid"; columns; rows; gap }),Absolute({ kind: "absolute" }),Stack({ kind: "stack"; direction: "row" | "column"; gap }),Free3D({ kind: "free3d" }).Bindingdiscriminated union:PluginParameter—{ kind: "pluginParam"; pluginId; paramPath: string; curve?: "linear" | "log" | "exp" | { kind: "custom"; points: ReadonlyArray<{ at: number; value: number }> } }TransportControl—{ kind: "transport"; field: "play" | "stop" | "tempo" | "position" | "loop" }MidiOut—{ kind: "midiOut"; portId; channel; cc: number; range?: [number, number] }MidiIn—{ kind: "midiIn"; portId; channel; cc: number; range?: [number, number] }(control reflects external MIDI input)DmxChannel—{ kind: "dmx"; universe; channel; range?: [0, 255] }Compound—{ kind: "compound"; bindings: ReadonlyArray<Binding> }(one control drives many)
Range—{ min: number; max: number; step?: number; default?: number; units?: string }.Behaviour—{ momentaryDelayMs?; latchSticky?; doubleTapAction?: Action; longPressAction?: Action; …}.Actiondiscriminated union —{ kind: "resetToDefault" } | { kind: "setTo"; value: number } | { kind: "toggleGroupCollapse"; targetId } | { kind: "emitEvent"; eventId; payload? }.Appearance—{ skinKey?: string; renderHint?: "auto" | "2d" | "3d"; cssVars?: Record<string, string>; tooltip?: string; icon?: { kind: "url" | "svg" | "skinAsset"; value: string } }.validateSurface(unknown): { surface: ControlSurface } | { error: SchemaError }— Zod-driven safe parse, returns typed error union.upgradeSurface(unknown): ControlSurface— accepts schema version N-1 documents and migrates forward via registered upgraders.walkSurface(surface, visitor): void— DFS traversal helper that yields every node with its parent stack.- Error codes —
CONTROL_SURFACE_SCHEMA.{INVALID_DOCUMENT, UNSUPPORTED_VERSION, UPGRADE_FAILED, DUPLICATE_ID, BINDING_REFERENCE_MISSING}viadefineCodes. - Schema versioning — bump on every breaking change;
upgradeSurfacechains migrations.
Impact
- New universal package. Zero runtime deps except
@suaveplan/error,@suaveplan/types,tslib,zod. - Unblocks renderers (
react-control-surface,r3f-control-surface), editors, persistence layers, network replication. - Schema versioning means
0.1.0 → 0.2.0requires registering an upgrader before consumers can read old documents.
Out of scope
- Renderers — separate packages, no React/R3F imports here.
- Skin packages — separate per-device families (
control-surface-skin-{native-instruments, mackie, piano, …}). - Editor UI — separate (
react-control-surface-editor). - Replication / persistence transports — separate (
network-control-surface).