> Track A. Read after 01-hardware-backends.md.
A "fixture" is a named channel layout: "RGB par at universe 1 channel 17 takes 3 channels, R G B in that order." Without fixtures the user has to remember channel numbers; with fixtures they pick by name.
This is not in TagTable. We design it from scratch but keep the schema small enough that a future TagTable port could reuse it.
Schema
src/features/dmx/fixtures/types.ts:
export interface FixtureProfile {
id: string; // stable id: "rgb", "rgbw", "moving-head-12ch"
name: string; // display name: "RGB Par"
totalChannels: number; // how many channels the fixture occupies
channels: FixtureChannel[]; // ordered, length === totalChannels
}
export interface FixtureChannel {
// Index within the fixture: 0..totalChannels-1
// (fixture index 0 maps to startChannel, 1 → startChannel+1, etc.)
offset: number;
// What this channel controls. Drives the inspector UI:
// "rgb-r" / "rgb-g" / "rgb-b" — picked together as a color
// "white" — adds W to RGB → RGBW
// "amber" — A in RGBA, RGBAW
// "uv" — UV channel
// "dimmer" — master intensity (0..255)
// "strobe" — strobe rate (0=off..255=fastest, fixture-specific)
// "pan-coarse" / "pan-fine" — moving heads
// "tilt-coarse" / "tilt-fine"
// "function" — discrete fixture function (gobo, prism, etc.)
// "raw" — anything else, exposed as 0..255 in the inspector
role: ChannelRole;
label: string; // user-visible inside the inspector
defaultValue?: number; // 0..255; what to write when fixture is "off"
}
export type ChannelRole =
| "rgb-r" | "rgb-g" | "rgb-b" | "white" | "amber" | "uv"
| "dimmer" | "strobe"
| "pan-coarse" | "pan-fine" | "tilt-coarse" | "tilt-fine"
| "function" | "raw";
A fixture instance in a graph is just { profileId, universeId, startChannel } — three numbers + a profile reference. The profile lives in the library; the instance lives in the dmx-out node spec.
Built-in profiles
Ship eight as part of the bundle. src/features/dmx/fixtures/builtins.ts:
| ID | Name | Channels | Layout |
|---|---|---|---|
| dimmer | Dimmer (1ch) | 1 | dimmer |
| rgb | RGB Par | 3 | R G B |
| rgbw | RGBW Par | 4 | R G B W |
| rgba | RGBA Par | 4 | R G B A |
| rgbaw | RGBAW Par | 5 | R G B A W |
| rgb-strobe | RGB Par + Strobe | 4 | R G B Strobe |
| rgb-dim-strobe | RGB Par + Dimmer + Strobe | 5 | Dim Strobe R G B |
| moving-head-12 | Moving Head (12ch) | 12 | Pan-c Pan-f Tilt-c Tilt-f Speed Dim Strobe R G B W Function |
Stored as plain TS so they tree-shake out of the renderer bundle if the user never opens a DMX inspector.
User-defined profiles
Persist under settings.dmx.userProfiles: FixtureProfile[]. The store roundtrips them through the existing config sync. No editor in v0.6 — users edit JSON in xosc.config.json if they need a custom profile. A modal editor is a v0.7 polish item.
src/features/dmx/fixtures/index.ts:
import { BUILT_IN_PROFILES } from "./builtins";
export function allProfiles(userProfiles: FixtureProfile[]): FixtureProfile[] {
return [...BUILT_IN_PROFILES, ...userProfiles];
}
export function profileById(id: string, userProfiles: FixtureProfile[]): FixtureProfile | undefined {
return allProfiles(userProfiles).find((p) => p.id === id);
}
How a fixture is applied
When a dmx-out node fires, its inspector has resolved a FixtureProfile + startChannel + per-role values (color, dimmer, etc.) into a list of writes:
function applyFixture(
profile: FixtureProfile,
startChannel: number,
values: { r?: number; g?: number; b?: number; w?: number; a?: number; uv?: number;
dimmer?: number; strobe?: number; raw?: Record<number, number>; },
): Array<[channel: number, value: number]> {
return profile.channels
.map((ch) => {
const c = startChannel + ch.offset;
const v = pickValue(ch.role, values, ch.defaultValue ?? 0);
return [c, v] as [number, number];
});
}
This produces the array the node hands to universeBus.writeMany. Continuous inputs override one channel role per node (configured in the inspector — typically dimmer for an RGB fixture, or raw[N] for arbitrary control).
Where it lives
src/features/dmx/fixtures/
├── types.ts # FixtureProfile, FixtureChannel, ChannelRole
├── builtins.ts # BUILT_IN_PROFILES const
├── apply.ts # applyFixture, pickValue
└── index.ts # allProfiles / profileById exports