Reverse-engineering LX / Chromatik .lxm (and .lxf)

Reverse-engineered from real Chromatik 1.2.1 example files (~/Chromatik/Models/Examples/*.lxm, Fixtures/Examples/*.lxf). All three LX formats are JSON:

extwhatanalogous to voxeled
.lxpProject — the whole session (model + patterns + mixer + UI)(a show/session)
.lxmModel — a rig of fixtures with placements + output patcha layout (.vxl rig)
.lxfFixture type — geometry template for a JsonFixture (points/strips/params)a fixture definition

.lxm — the model

{
  "version": "1.0.0-SNAPSHOT",   // optional (Cubes.lxm omits version/timestamp)
  "timestamp": 1708605946762,
  "normalization": { ... },      // optional bounds normalization
  "fixtures": [
    {
      "id": 47048,                                   // optional
      "class": "heronarts.lx.structure.GridFixture", // the fixture class (see below)
      "internal": { ... },                           // editor/UI state — ignore
      "parameters": { /* transform + patch + geometry, see below */ },
      "children": {}
    },
    ...
  ]
}

Geometry is NOT stored in the .lxm. Each fixture is generated at load time by resolving its class (or referenced .lxf) with its parameters + transform. Two fixture flavors seen:

parameters common to every fixture

.lxf — the JsonFixture type (e.g. Examples/Cube)

{
  "label": "Cube",
  "tag": "cube",
  "parameters": {                       // typed, user-facing, referenced as "$name"
    "size":          { "type": "float",   "default": 100, "min": 0 },
    "pointsPerEdge": { "type": "int",     "default": 15,  "min": 5, "max": 100 },
    "padding":       { "type": "float",   "default": 3,   "min": 0 },
    "caps":          { "type": "boolean", "default": false }
  },
  "transforms": [                       // conditional transform ops; fields are EXPRESSIONS
    { "enabled": "$center", "x": "-0.5 * ($size + 2 * $padding)", "y": "...", "z": "..." }
  ],
  "components": [                        // geometry primitives
    { "type": "Square", "tag": "front", "x": "$padding", "y": "$padding", "z": 0,
      "yaw": -90, "size": "$size", "pointsPerEdge": "$pointsPerEdge" },
    ...
  ]
}

How it maps to voxeled

Near 1:1 with voxeled's layout + patch:

LX .lxmvoxeled
fixture (class/.lxf + params)a fixture (its generated local points)
x,y,z + yaw,pitch,roll + scalean instance pos + rotDeg (+ scale baked into points)
output patch (protocol/host/universe/offset/byteOrder)the instance's output block
— (LX points are positions only)voxeled normal — estimate on import (outward-from-centroid / PCA)

So a .lxm importer produces a voxeled scene: one voxeled instance per LX fixture, geometry from the class/.lxf, patch mapped to output, normals estimated.

Importer — src/io/lxm-import.mjs

node src/io/lxm-import.mjs <in.lxm> [out.vxl.json] [--fixtures <LX Fixtures dir>]

Tier 1 — built-in fixture classes (done). Parses the .lxm, generates geometry for GridFixture (the only structural built-in that appears in real Chromatik rigs), maps each fixture's transform + patch to a voxeled instance + output, and assigns a normal (local +Z) since LX stores none. Anything else (JsonFixture, …) is reported, never fabricated — the CLI tells you exactly what needs tier 2. Verified against ~/Chromatik/Models/Examples/Grid3D.lxm (10 grids → 4000 points) and covered by test/lxm-import.test.mjs (18 checks); a hand-authored example lives at examples/lx/grid.lxm.

Patch mapping: LX enum ordinals → voxeled (protocol 0 = none → unwired; 1 ≈ Art-Net → universe /channel; 3 ≈ DDP → offset; …). The ordering is best-effort — the raw ordinals are kept under output.raw so nothing is lost until confirmed against the LX source. scale is baked into local geometry; wiring (serpentine) only reorders indices and is a tier-2 refinement.

Tier 2 — JsonFixture (.lxf), done. Resolves the referenced .lxf (pass --fixtures / fixturesDir to point at the LX Fixtures/ root), tolerates JSONC (/* */, //, trailing commas), and generates its geometry:

Verified against ~/Chromatik/Models/Examples/Cubes.lxm (73 JsonFixtures → 17 520 points) and each stock .lxf (Square, Cube, Fan, Winding, SpikySphere). Covered by test/lxf-expr.test.mjs (18) + the tier-2 half of test/lxm-import.test.mjs; self-contained examples live in examples/lx/ (cube.lxm + fixtures/{Square,Cube}.lxf).

Not yet: the arc primitive (absent from the stock fixtures — add when a sample needs it) and LX wiring/tags (index order + grouping, not positions).