← All docs
Ideas

Fixture profile library

> 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