spine-rigc 2.57.0 → 2.58.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/compile.ts CHANGED
@@ -889,7 +889,7 @@ function refuseNamesTheEditorCouldKeyDifferently(
889
889
  throw new CompileError(
890
890
  `${found.length} pair(s) of ${what.noun} have no one order: ${what.why}` +
891
891
  `${spelled.join('. ')}` +
892
- (found.length > spelled.length ? `. …and ${found.length - spelled.length} more pair(s)` : ''),
892
+ (found.length > spelled.length ? `. …and ${found.length - spelled.length} more pair(s)` : ''), 'CE_COMPILE_EDITOR_NAME_ORDER_AMBIGUOUS',
893
893
  );
894
894
  }
895
895
 
@@ -969,7 +969,7 @@ export function f32(n: number): number {
969
969
  }
970
970
  // Unreachable — nine significant digits name every float32. Stated rather than
971
971
  // assumed, because a fallback here would be a number nobody chose.
972
- throw new CompileError(`no decimal of at most 9 significant digits names the float32 of ${n}`);
972
+ throw new CompileError(`no decimal of at most 9 significant digits names the float32 of ${n}`, 'CE_COMPILE_FLOAT32_DECIMAL_UNREPRESENTABLE');
973
973
  }
974
974
 
975
975
  /**
@@ -1144,7 +1144,7 @@ function checkKeyTime(where: string, time: number, authored: number, duration: n
1144
1144
  throw new CompileError(
1145
1145
  `${where}: key at ${at} is ${f32(past)}s past the declared duration ${duration}s — nothing that plays this ` +
1146
1146
  `animation for the duration it declares ever reaches it. Move the key to ${duration}, or declare the ` +
1147
- `duration you meant.`,
1147
+ `duration you meant.`, 'CE_COMPILE_KEY_AFTER_DURATION',
1148
1148
  );
1149
1149
  }
1150
1150
 
@@ -1156,7 +1156,7 @@ function channelHex(v: number): string {
1156
1156
  }
1157
1157
 
1158
1158
  function rgbaHex(v: number[]): string {
1159
- if (v.length !== 4) throw new CompileError(`rgba value needs 4 channels, got ${v.length}`);
1159
+ if (v.length !== 4) throw new CompileError(`rgba value needs 4 channels, got ${v.length}`, 'CE_COMPILE_RGBA_CHANNEL_COUNT');
1160
1160
  return v.map(channelHex).join('');
1161
1161
  }
1162
1162
 
@@ -1176,7 +1176,7 @@ function rgbaHex(v: number[]): string {
1176
1176
  * by: `readCurve(…, 0..3, light r/g/b/a)` then `readCurve(…, 4..6, dark r/g/b)`.
1177
1177
  */
1178
1178
  function rgba2Hex(v: number[]): { light: string; dark: string } {
1179
- if (v.length !== 7) throw new CompileError(`rgba2 value needs 7 channels, got ${v.length}`);
1179
+ if (v.length !== 7) throw new CompileError(`rgba2 value needs 7 channels, got ${v.length}`, 'CE_COMPILE_RGBA2_CHANNEL_COUNT');
1180
1180
  return { light: v.slice(0, 4).map(channelHex).join(''), dark: v.slice(4, 7).map(channelHex).join('') };
1181
1181
  }
1182
1182
 
@@ -1226,7 +1226,7 @@ const COLOUR_KEYS: Record<SlotColourShape, { channels: number; spelling: string;
1226
1226
  function colourKey(shape: SlotColourShape, v: number[]): Record<string, string | number> {
1227
1227
  const { channels, write } = COLOUR_KEYS[shape];
1228
1228
  if (v.length !== channels) {
1229
- throw new CompileError(`${shape} value needs ${channels} channel${channels === 1 ? '' : 's'}, got ${v.length}`);
1229
+ throw new CompileError(`${shape} value needs ${channels} channel${channels === 1 ? '' : 's'}, got ${v.length}`, 'CE_COMPILE_COLOUR_CHANNEL_COUNT');
1230
1230
  }
1231
1231
  return write(v);
1232
1232
  }
@@ -1654,7 +1654,7 @@ function readJson<T>(path: string): T {
1654
1654
  try {
1655
1655
  text = readFileSync(path, 'utf8');
1656
1656
  } catch (err) {
1657
- throw new CompileError(`cannot read ${path}: ${(err as Error).message}`);
1657
+ throw new CompileError(`cannot read ${path}: ${(err as Error).message}`, 'CE_COMPILE_JSON_READ_FAILED');
1658
1658
  }
1659
1659
  try {
1660
1660
  // A parse failure gets a line/column appended to the runtime's own
@@ -1662,7 +1662,7 @@ function readJson<T>(path: string): T {
1662
1662
  // cannot say where.
1663
1663
  return parseJsonWithPosition(text) as T;
1664
1664
  } catch (err) {
1665
- throw new CompileError(`cannot read ${path}: ${(err as Error).message}`);
1665
+ throw new CompileError(`cannot read ${path}: ${(err as Error).message}`, 'CE_COMPILE_JSON_READ_FAILED');
1666
1666
  }
1667
1667
  }
1668
1668
 
@@ -1882,13 +1882,13 @@ function fromLoosePng(
1882
1882
  // Left to `readFileSync` this arrives as a raw ENOENT with a stack, which
1883
1883
  // is the tool telling an agent about its own internals instead of about
1884
1884
  // the rig. The validator's messages are the UI, and so are these.
1885
- throw new CompileError(`image "${relPath}" is not on disk at ${absPath}`);
1885
+ throw new CompileError(`image "${relPath}" is not on disk at ${absPath}`, 'CE_COMPILE_IMAGE_MISSING');
1886
1886
  }
1887
1887
  // A loose part IS a page of the atlas this route emits, so it is refused the
1888
1888
  // way a pack's page is (issue #732): by the one reader's sentence, as a
1889
1889
  // CompileError, rather than as that reader's throw with a stack under it.
1890
1890
  const header = readPngHeader(absPath);
1891
- if (header.problem !== null) throw new CompileError(`image "${relPath}": ${header.problem}`);
1891
+ if (header.problem !== null) throw new CompileError(`image "${relPath}": ${header.problem}`, 'CE_COMPILE_IMAGE_PNG_INVALID');
1892
1892
  const info = header.info;
1893
1893
  // Page name is the PNG path *relative to the atlas file*, so the viewer
1894
1894
  // resolves it the way every Spine consumer does: against the atlas URL.
@@ -1913,7 +1913,7 @@ interface AtlasSource {
1913
1913
  }
1914
1914
 
1915
1915
  function readAtlasIn(path: string): AtlasSource {
1916
- if (!existsSync(path)) throw new CompileError(`--atlas-in names ${path}, which is not on disk`);
1916
+ if (!existsSync(path)) throw new CompileError(`--atlas-in names ${path}, which is not on disk`, 'CE_COMPILE_ATLAS_INPUT_MISSING');
1917
1917
  const parsed = parseAtlasText(readFileSync(path, 'utf8'));
1918
1918
  const byName = new Map<string, { region: AtlasRegion; page: ParsedAtlas['pages'][number] }>();
1919
1919
  for (const page of parsed.pages) {
@@ -1925,7 +1925,7 @@ function readAtlasIn(path: string): AtlasSource {
1925
1925
  if (!byName.has(key)) byName.set(key, { region, page });
1926
1926
  }
1927
1927
  }
1928
- if (byName.size === 0) throw new CompileError(`--atlas-in ${path} declares no regions`);
1928
+ if (byName.size === 0) throw new CompileError(`--atlas-in ${path} declares no regions`, 'CE_COMPILE_ATLAS_INPUT_EMPTY');
1929
1929
  // 🔒 **What each page IS, where the pack is opened** (issue #732). Every page
1930
1930
  // the pack names reaches the gate — the atlas this build emits is the pack's
1931
1931
  // own text — so a page no reader can open is refused here, all of them in
@@ -1958,7 +1958,7 @@ function readAtlasIn(path: string): AtlasSource {
1958
1958
  if (unread.length > 0) {
1959
1959
  throw new CompileError(
1960
1960
  `--atlas-in ${path}: ${unread.length} of its ${parsed.pages.length} page(s) cannot be read as PNG, and ` +
1961
- `nothing was compiled against the pack — ${unread.join('; ')}`,
1961
+ `nothing was compiled against the pack — ${unread.join('; ')}`, 'CE_COMPILE_ATLAS_PAGES_UNREADABLE',
1962
1962
  );
1963
1963
  }
1964
1964
  return { path, dir: dirname(path), parsed, byName, grids };
@@ -2028,13 +2028,13 @@ function resolveFromAtlas(
2028
2028
  throw new CompileError(
2029
2029
  `image "${relPath}" resolves to region "${region}", which the atlas at ${atlas.path} does not have. ` +
2030
2030
  (near.length ? `Did you mean ${near.map((n) => JSON.stringify(n)).join(', ')}? ` : '') +
2031
- `The atlas declares ${known.length} region(s): ${known.join(', ')}`,
2031
+ `The atlas declares ${known.length} region(s): ${known.join(', ')}`, 'CE_COMPILE_ATLAS_REGION_UNKNOWN',
2032
2032
  );
2033
2033
  }
2034
2034
  const absPath = resolve(atlas.dir, found.page.name);
2035
2035
  if (!existsSync(absPath)) {
2036
2036
  throw new CompileError(
2037
- `region "${region}" sits on page "${found.page.name}" of ${atlas.path}, which is not on disk at ${absPath}`,
2037
+ `region "${region}" sits on page "${found.page.name}" of ${atlas.path}, which is not on disk at ${absPath}`, 'CE_COMPILE_ATLAS_REGION_PAGE_MISSING',
2038
2038
  );
2039
2039
  }
2040
2040
  // The rectangle ON THE PAGE, which at a quarter turn is not the one `bounds:`
@@ -2049,7 +2049,7 @@ function resolveFromAtlas(
2049
2049
  throw new CompileError(
2050
2050
  `region "${region}" is at ${found.region.x},${found.region.y} sized ${rectW}x${rectH} on a ` +
2051
2051
  `${found.page.width}x${found.page.height} page in ${atlas.path}; the rectangle runs off the page, so its ` +
2052
- 'UVs would leave the texture',
2052
+ 'UVs would leave the texture', 'CE_COMPILE_ATLAS_REGION_OUTSIDE_PAGE',
2053
2053
  );
2054
2054
  }
2055
2055
  const info = readPngInfo(absPath);
@@ -2342,7 +2342,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2342
2342
  return fn();
2343
2343
  } catch (err) {
2344
2344
  if (err instanceof CompileError && !err.message.includes(motionPath)) {
2345
- throw new CompileError(`${motionPath}: ${err.message}`);
2345
+ throw new CompileError(`${motionPath}: ${err.message}`, err.code);
2346
2346
  }
2347
2347
  throw err;
2348
2348
  }
@@ -2357,7 +2357,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2357
2357
  // does not — the class of wrongness that loads, plays and lies.
2358
2358
  if (motion.archetype !== rig.name) {
2359
2359
  throw new CompileError(
2360
- `motion spec names archetype "${motion.archetype}" but the rig spec at ${rigPath} is called "${rig.name}"`,
2360
+ `motion spec names archetype "${motion.archetype}" but the rig spec at ${rigPath} is called "${rig.name}"`, 'CE_COMPILE_MOTION_ARCHETYPE_MISMATCH',
2361
2361
  );
2362
2362
  }
2363
2363
  });
@@ -2378,7 +2378,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2378
2378
  if (!noStage && (stageWidth === undefined || stageHeight === undefined)) {
2379
2379
  throw new CompileError(
2380
2380
  'no stage size: give the rig spec a `skeleton.width`/`skeleton.height`, or compile against a cut manifest whose `crop` states them, ' +
2381
- 'or state `"width": null, "height": null` for a skeleton that declares no stage',
2381
+ 'or state `"width": null, "height": null` for a skeleton that declares no stage', 'CE_COMPILE_STAGE_SIZE_MISSING',
2382
2382
  );
2383
2383
  }
2384
2384
  /**
@@ -2434,7 +2434,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2434
2434
  partDir: string | null = dirname(resolve(baseDir, relPath)),
2435
2435
  ): CompiledImage => {
2436
2436
  if (seenRegions.has(region)) {
2437
- throw new CompileError(`duplicate region name "${region}" (${relPath})`);
2437
+ throw new CompileError(`duplicate region name "${region}" (${relPath})`, 'CE_COMPILE_REGION_NAME_DUPLICATE');
2438
2438
  }
2439
2439
  const loosePath = resolve(baseDir, relPath);
2440
2440
  const img =
@@ -2485,7 +2485,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2485
2485
  throw new CompileError(
2486
2486
  `${where}: "${relPath}" and the art already atlased as region "${region}" are two different files — ` +
2487
2487
  `${wanted} and ${already}. An atlas region is named by its PNG's basename, so only one of the two can ` +
2488
- 'hold that name and this attachment would draw the other file\'s pixels. Rename one of the PNGs.',
2488
+ 'hold that name and this attachment would draw the other file\'s pixels. Rename one of the PNGs.', 'CE_COMPILE_REGION_FILE_CONFLICT',
2489
2489
  );
2490
2490
  };
2491
2491
 
@@ -2516,7 +2516,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2516
2516
  `${where}: sequence ${frame} is the region "${region}", and there is no PNG for it at ${wanted}. A ` +
2517
2517
  `sequence's frames are the regions "<stem><start + i>", zero-padded to "digits" — here stem ` +
2518
2518
  `${JSON.stringify(stem)}, start ${seq.start ?? 1}, digits ${seq.digits ?? 0} — and the compiler draws ` +
2519
- 'no frame in place of another. Add the file, or state the "count" the series really has.',
2519
+ 'no frame in place of another. Add the file, or state the "count" the series really has.', 'CE_COMPILE_SEQUENCE_FRAME_PNG_MISSING',
2520
2520
  );
2521
2521
  }
2522
2522
  if (atlasIn !== null && !atlasIn.byName.has(region)) {
@@ -2526,7 +2526,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2526
2526
  (near.length ? `Did you mean ${near.map((n) => JSON.stringify(n)).join(', ')}? ` : '') +
2527
2527
  `A sequence's frames are the regions "<stem><start + i>", zero-padded to "digits" — here stem ` +
2528
2528
  `${JSON.stringify(stem)}, start ${seq.start ?? 1}, digits ${seq.digits ?? 0} — and the compiler draws ` +
2529
- 'no frame in place of another.',
2529
+ 'no frame in place of another.', 'CE_COMPILE_SEQUENCE_FRAME_REGION_MISSING',
2530
2530
  );
2531
2531
  }
2532
2532
  const already = regionSource.get(region);
@@ -2535,7 +2535,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2535
2535
  throw new CompileError(
2536
2536
  `${where}: sequence ${frame} is the region "${region}", and the art already atlased under that name is ` +
2537
2537
  `${already}, not ${wanted}. One region name is one set of pixels, so this frame would draw the other ` +
2538
- "file's. Rename one of them.",
2538
+ "file's. Rename one of them.", 'CE_COMPILE_SEQUENCE_FRAME_FILE_CONFLICT',
2539
2539
  );
2540
2540
  }
2541
2541
  // The directory the frame NAMES are relative to — the images root, not
@@ -2575,7 +2575,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2575
2575
  for (const part of parts) {
2576
2576
  if (!rigSlotIndex.has(part.slot)) {
2577
2577
  throw new CompileError(
2578
- `the manifest binds a part to slot "${part.slot}" (via rig_slot), which the rig "${rig.name}" does not declare — add the slot to the rig rather than inventing one here`,
2578
+ `the manifest binds a part to slot "${part.slot}" (via rig_slot), which the rig "${rig.name}" does not declare — add the slot to the rig rather than inventing one here`, 'CE_COMPILE_MANIFEST_SLOT_UNKNOWN',
2579
2579
  );
2580
2580
  }
2581
2581
  }
@@ -2589,7 +2589,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2589
2589
  const at = rigSlotIndex.get(part.slot)!;
2590
2590
  if (at < orderCursor) {
2591
2591
  throw new CompileError(
2592
- `the manifest draws "${part.slot}" (draw_order ${part.draw_order}) out of the rig's slot order; the rig's slots array IS the draw order`,
2592
+ `the manifest draws "${part.slot}" (draw_order ${part.draw_order}) out of the rig's slot order; the rig's slots array IS the draw order`, 'CE_COMPILE_MANIFEST_DRAW_ORDER_MISMATCH',
2593
2593
  );
2594
2594
  }
2595
2595
  orderCursor = at;
@@ -2604,33 +2604,33 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2604
2604
  if (meshParts.length > meshBudget) {
2605
2605
  throw new CompileError(
2606
2606
  `${meshParts.length} mesh slot(s) declared but the rig "${rig.name}" allows ${meshBudget}` +
2607
- ' — raise `invariants.meshSlots` in the rig spec if that budget is the thing being changed',
2607
+ ' — raise `invariants.meshSlots` in the rig spec if that budget is the thing being changed', 'CE_COMPILE_MANIFEST_MESH_SLOT_BUDGET',
2608
2608
  );
2609
2609
  }
2610
2610
  for (const part of meshParts) {
2611
2611
  if (isBasePlate(part, manifest!)) {
2612
2612
  // A base plate mesh is a full-frame canvas every frame.
2613
- throw new CompileError(`slot "${part.slot}" is the base plate; it must never be a mesh`);
2613
+ throw new CompileError(`slot "${part.slot}" is the base plate; it must never be a mesh`, 'CE_COMPILE_BASE_PLATE_MESH');
2614
2614
  }
2615
2615
  const spec = part.mesh!;
2616
2616
  const kind = spec.kind ?? 'ring';
2617
2617
  if (kind === 'ring') {
2618
2618
  if (!part.polygon?.length) {
2619
- throw new CompileError(`slot "${part.slot}" declares a ring mesh but has no polygon to use as its rim`);
2619
+ throw new CompileError(`slot "${part.slot}" declares a ring mesh but has no polygon to use as its rim`, 'CE_COMPILE_RING_POLYGON_MISSING');
2620
2620
  }
2621
2621
  if (spec.hull !== 'polygon') {
2622
- throw new CompileError(`slot "${part.slot}": mesh.hull must be "polygon", got ${JSON.stringify(spec.hull)}`);
2622
+ throw new CompileError(`slot "${part.slot}": mesh.hull must be "polygon", got ${JSON.stringify(spec.hull)}`, 'CE_COMPILE_RING_HULL_INVALID');
2623
2623
  }
2624
2624
  if (!spec.center || spec.inner === undefined) {
2625
- throw new CompileError(`slot "${part.slot}": a ring mesh needs mesh.center and mesh.inner`);
2625
+ throw new CompileError(`slot "${part.slot}": a ring mesh needs mesh.center and mesh.inner`, 'CE_COMPILE_RING_PARAMETERS_MISSING');
2626
2626
  }
2627
2627
  } else {
2628
2628
  if (!spec.rows || !spec.chain?.length) {
2629
- throw new CompileError(`slot "${part.slot}": a ribbon mesh needs mesh.rows and a mesh.chain`);
2629
+ throw new CompileError(`slot "${part.slot}": a ribbon mesh needs mesh.rows and a mesh.chain`, 'CE_COMPILE_RIBBON_PARAMETERS_MISSING');
2630
2630
  }
2631
2631
  }
2632
2632
  if (!meshControlBones(part).length) {
2633
- throw new CompileError(`slot "${part.slot}": a mesh with no control bone deforms nothing`);
2633
+ throw new CompileError(`slot "${part.slot}": a mesh with no control bone deforms nothing`, 'CE_COMPILE_MESH_CONTROL_BONE_MISSING');
2634
2634
  }
2635
2635
  }
2636
2636
 
@@ -2656,7 +2656,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2656
2656
  const img = addImage(part.image, manifestDir!, isBasePlate(part, manifest!));
2657
2657
  if (img.width !== win.w || img.height !== win.h) {
2658
2658
  throw new CompileError(
2659
- `${measuredFrom(img, part.image)} is ${img.width}x${img.height} but the manifest window for "${part.slot}" is ${win.w}x${win.h}`,
2659
+ `${measuredFrom(img, part.image)} is ${img.width}x${img.height} but the manifest window for "${part.slot}" is ${win.w}x${win.h}`, 'CE_COMPILE_MANIFEST_WINDOW_SIZE_MISMATCH',
2660
2660
  );
2661
2661
  }
2662
2662
  slotAttachments.set(part.slot, [img.region]);
@@ -2691,7 +2691,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2691
2691
  const img = addImage(relPath, manifestDir!, false);
2692
2692
  if (img.width !== win.w || img.height !== win.h) {
2693
2693
  throw new CompileError(
2694
- `${measuredFrom(img, relPath)} is ${img.width}x${img.height} but slot "${part.slot}" declares ${win.w}x${win.h}`,
2694
+ `${measuredFrom(img, relPath)} is ${img.width}x${img.height} but slot "${part.slot}" declares ${win.w}x${win.h}`, 'CE_COMPILE_SLOT_IMAGE_SIZE_MISMATCH',
2695
2695
  );
2696
2696
  }
2697
2697
  names.push(img.region);
@@ -2714,11 +2714,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2714
2714
  for (const skinName of skinNames) {
2715
2715
  for (const [slotName, placeholders] of Object.entries(skinParts.get(skinName)!.attachments)) {
2716
2716
  if (!rigSlotIndex.has(slotName)) {
2717
- throw new CompileError(`rig skin "${skinName}" gives attachments to slot "${slotName}", which the rig does not declare`);
2717
+ throw new CompileError(`rig skin "${skinName}" gives attachments to slot "${slotName}", which the rig does not declare`, 'CE_COMPILE_SKIN_SLOT_UNKNOWN');
2718
2718
  }
2719
2719
  if (slotAttachments.has(slotName)) {
2720
2720
  throw new CompileError(
2721
- `slot "${slotName}" is filled by a manifest part AND by rig skin "${skinName}"; one slot, one source of attachments`,
2721
+ `slot "${slotName}" is filled by a manifest part AND by rig skin "${skinName}"; one slot, one source of attachments`, 'CE_COMPILE_SLOT_ATTACHMENT_SOURCES_CONFLICT',
2722
2722
  );
2723
2723
  }
2724
2724
  const names = rigAttachmentNames.get(slotName) ?? [];
@@ -2809,7 +2809,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2809
2809
  transforms = computeWorldTransforms(bones);
2810
2810
  exactFrames = computeExactFrameTransforms(bones);
2811
2811
  } catch (err) {
2812
- if (err instanceof TransformError) throw new CompileError(err.message);
2812
+ if (err instanceof TransformError) throw new CompileError(err.message, 'CE_COMPILE_SETUP_TRANSFORM_INVALID');
2813
2813
  throw err;
2814
2814
  }
2815
2815
 
@@ -2817,7 +2817,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2817
2817
  for (const name of meshControlBones(part)) {
2818
2818
  if (!boneNames.has(name)) {
2819
2819
  throw new CompileError(
2820
- `slot "${part.slot}" drives control bone "${name}", which the rig "${rig.name}" does not declare`,
2820
+ `slot "${part.slot}" drives control bone "${name}", which the rig "${rig.name}" does not declare`, 'CE_COMPILE_CONTROL_BONE_UNKNOWN',
2821
2821
  );
2822
2822
  }
2823
2823
  }
@@ -2909,7 +2909,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2909
2909
  // here down is the half that needs the rig in front of it.
2910
2910
  if (setup !== undefined && rigSlot.attachment !== undefined) {
2911
2911
  throw new CompileError(
2912
- `slot "${rigSlot.name}" has a setup attachment in the rig spec AND in the motion spec; the setup pose has one author`,
2912
+ `slot "${rigSlot.name}" has a setup attachment in the rig spec AND in the motion spec; the setup pose has one author`, 'CE_COMPILE_SETUP_ATTACHMENT_AUTHORS_CONFLICT',
2913
2913
  );
2914
2914
  }
2915
2915
  let setupAttachment: string | null;
@@ -2922,7 +2922,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2922
2922
  else if (empty) setupAttachment = null;
2923
2923
  else {
2924
2924
  throw new CompileError(
2925
- `no setup pose for slot "${rigSlot.name}": give the motion spec a \`setup\` entry or the rig slot an \`attachment\` — the compiler will not guess one`,
2925
+ `no setup pose for slot "${rigSlot.name}": give the motion spec a \`setup\` entry or the rig slot an \`attachment\` — the compiler will not guess one`, 'CE_COMPILE_SETUP_ATTACHMENT_MISSING',
2926
2926
  );
2927
2927
  }
2928
2928
  if (setupAttachment !== null && !names.includes(setupAttachment)) {
@@ -2971,10 +2971,10 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2971
2971
  `no art: ${found} — ${supply}, ${many ? 'fix the paths' : 'fix the path'} in the manifest, or state the ` +
2972
2972
  'setup pose as null';
2973
2973
  }
2974
- throw new CompileError(message);
2974
+ throw new CompileError(message, 'CE_COMPILE_SETUP_ATTACHMENT_UNAVAILABLE');
2975
2975
  }
2976
2976
  if (setup?.color && rigSlot.color !== undefined) {
2977
- throw new CompileError(`slot "${rigSlot.name}" has a setup colour in the rig spec AND in the motion spec`);
2977
+ throw new CompileError(`slot "${rigSlot.name}" has a setup colour in the rig spec AND in the motion spec`, 'CE_COMPILE_SETUP_COLOUR_AUTHORS_CONFLICT');
2978
2978
  }
2979
2979
  // The setup attachment is held as the value it is, `null` included; leaving
2980
2980
  // the key out for `null` is the emitter's (`emitSlots`).
@@ -3001,7 +3001,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3001
3001
  const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, exactFrames, rigSlot.bone) : null;
3002
3002
  for (const name of names) {
3003
3003
  const img = images.find((im) => im.region === name);
3004
- if (!img) throw new CompileError(`internal: no image for attachment ${name}`);
3004
+ if (!img) throw new CompileError(`internal: no image for attachment ${name}`, 'CE_COMPILE_INTERNAL_ATTACHMENT_IMAGE_MISSING');
3005
3005
  if (mesh) {
3006
3006
  // Every state of a mesh slot gets the SAME geometry. That is what makes
3007
3007
  // an attachment swap mid-deform safe: the control bone's pose means the
@@ -3089,7 +3089,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3089
3089
  `${budgeted.length}, "meshTriangles": <triangles one mesh may carry> }\` to the rig spec: geometry rigc ` +
3090
3090
  'built is geometry it will not ship unmeasured, and `A13_MESH_BUDGET` has nothing to measure against ' +
3091
3091
  'until that budget is stated. (Authored geometry is exempt — rigc did not draw it.)'
3092
- : ' — raise `invariants.meshSlots` in the rig spec if that budget is the thing being changed'),
3092
+ : ' — raise `invariants.meshSlots` in the rig spec if that budget is the thing being changed'), 'CE_COMPILE_GENERATED_MESH_SLOT_BUDGET',
3093
3093
  );
3094
3094
  }
3095
3095
 
@@ -3126,7 +3126,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3126
3126
  throw new CompileError(
3127
3127
  `physics constraint "${EVERY_GLOBAL_PHYSICS}": the name is reserved — a motion spec's \`"physics": ` +
3128
3128
  `"${EVERY_GLOBAL_PHYSICS}"\` is the timeline that names no constraint and drives every physics constraint ` +
3129
- 'declaring the keyed property global, so a constraint called that could not be keyed by name. Rename it',
3129
+ 'declaring the keyed property global, so a constraint called that could not be keyed by name. Rename it', 'CE_COMPILE_PHYSICS_NAME_RESERVED',
3130
3130
  );
3131
3131
  }
3132
3132
  constraintDeclared.add(constraintAt(type, name));
@@ -3199,12 +3199,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3199
3199
  // apart.
3200
3200
  if (constraintDeclared.has(constraintAt('physics', name))) {
3201
3201
  throw new CompileError(
3202
- `physics constraint "${name}" is declared in both the rig spec and the motion spec's physics table`,
3202
+ `physics constraint "${name}" is declared in both the rig spec and the motion spec's physics table`, 'CE_COMPILE_PHYSICS_DECLARATION_DUPLICATE',
3203
3203
  );
3204
3204
  }
3205
3205
  declareConstraint(name, 'physics');
3206
3206
  if (!boneNames.has(spec.bone)) {
3207
- throw new CompileError(`physics constraint "${name}" targets unknown bone "${spec.bone}"`);
3207
+ throw new CompileError(`physics constraint "${name}" targets unknown bone "${spec.bone}"`, 'CE_COMPILE_PHYSICS_BONE_UNKNOWN');
3208
3208
  }
3209
3209
  // A stop's copy and clamps sit right after the rig constraint that swings
3210
3210
  // each link (`src/stop.ts`); this table is emitted after every rig
@@ -3214,7 +3214,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3214
3214
  throw new CompileError(
3215
3215
  `physics constraint "${name}" drives "${spec.bone}", a link stop "${stop.chain}" holds, from the physics table — ` +
3216
3216
  "the table is emitted after every rig constraint, past the stop's copy and clamp, which would read the link " +
3217
- 'before this swings it; declare the constraint in the rig spec\'s "constraints"',
3217
+ 'before this swings it; declare the constraint in the rig spec\'s "constraints"', 'CE_COMPILE_PHYSICS_STOP_ORDER_INVALID',
3218
3218
  );
3219
3219
  }
3220
3220
  // Every component and parameter the table states is held, 0 and the
@@ -3233,7 +3233,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3233
3233
  // The parser is happy with this and the constraint does nothing at all.
3234
3234
  // A23 catches it too; refusing here means it never reaches the gate.
3235
3235
  throw new CompileError(
3236
- `physics constraint "${name}" drives no component — set at least one of ${PHYSICS_COMPONENTS.join('/')}`,
3236
+ `physics constraint "${name}" drives no component — set at least one of ${PHYSICS_COMPONENTS.join('/')}`, 'CE_COMPILE_PHYSICS_COMPONENTS_EMPTY',
3237
3237
  );
3238
3238
  }
3239
3239
  for (const [param] of PHYSICS_PARAMS) {
@@ -3342,7 +3342,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3342
3342
  if (everyGlobal && track.physics === undefined) {
3343
3343
  throw new CompileError(
3344
3344
  `animation "${animName}": group "${String(track.group)}" lists "${EVERY_GLOBAL_PHYSICS}", which is not a ` +
3345
- `constraint but the target that names none — write it as the track's \`"physics": "${EVERY_GLOBAL_PHYSICS}"\``,
3345
+ `constraint but the target that names none — write it as the track's \`"physics": "${EVERY_GLOBAL_PHYSICS}"\``, 'CE_COMPILE_PHYSICS_GLOBAL_GROUP_MEMBER',
3346
3346
  );
3347
3347
  }
3348
3348
  if (everyGlobal) {
@@ -3364,7 +3364,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3364
3364
  ' — it would parse and move nothing. ' +
3365
3365
  (track.property === 'reset'
3366
3366
  ? 'Declare a physics constraint, or remove the track'
3367
- : `Set "${flag}": true on the constraints it is for, or key one by name`),
3367
+ : `Set "${flag}": true on the constraints it is for, or key one by name`), 'CE_COMPILE_PHYSICS_GLOBAL_TRACK_INERT',
3368
3368
  );
3369
3369
  }
3370
3370
  } else if (family !== null) {
@@ -3380,21 +3380,21 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3380
3380
  if (kinds.length > 0) {
3381
3381
  throw new CompileError(
3382
3382
  `animation "${animName}" keys "${target}" as a ${label}, but the rig declares it as a "${kinds.join('"/"')}" ` +
3383
- 'constraint — a timeline group resolves its target by name AND type, misses, and the loader throws',
3383
+ 'constraint — a timeline group resolves its target by name AND type, misses, and the loader throws', 'CE_COMPILE_CONSTRAINT_TRACK_TYPE_MISMATCH',
3384
3384
  );
3385
3385
  }
3386
3386
  const known = constraintNamesOfKind.get(family) ?? [];
3387
3387
  throw new CompileError(
3388
3388
  `animation "${animName}" keys unknown ${label} "${target}"` +
3389
- (known.length ? ` (the rig declares: ${known.join(', ')})` : `, and the rig declares no ${family} constraint at all`),
3389
+ (known.length ? ` (the rig declares: ${known.join(', ')})` : `, and the rig declares no ${family} constraint at all`), 'CE_COMPILE_CONSTRAINT_TRACK_TARGET_UNKNOWN',
3390
3390
  );
3391
3391
  }
3392
3392
  } else if (isBoneTrack) {
3393
3393
  if (!boneNames.has(target)) {
3394
- throw new CompileError(`animation "${animName}" keys unknown bone "${target}"`);
3394
+ throw new CompileError(`animation "${animName}" keys unknown bone "${target}"`, 'CE_COMPILE_ANIMATION_BONE_UNKNOWN');
3395
3395
  }
3396
3396
  } else if (!slotNames.has(target)) {
3397
- throw new CompileError(`animation "${animName}" targets unknown slot "${target}"`);
3397
+ throw new CompileError(`animation "${animName}" targets unknown slot "${target}"`, 'CE_COMPILE_ANIMATION_SLOT_UNKNOWN');
3398
3398
  }
3399
3399
  // A timeline is a kind, a name and a property — `physics.leg.mix` and
3400
3400
  // `path.leg.mix` are two timelines on two constraints, so the claim
@@ -3403,7 +3403,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3403
3403
  const claim = `${family ?? (isBoneTrack ? 'bone' : 'slot')} ${target}.${track.property}`;
3404
3404
  if (claimed.has(claim)) {
3405
3405
  throw new CompileError(
3406
- `animation "${animName}" has two tracks on ${target}.${track.property}; merge them into one track`,
3406
+ `animation "${animName}" has two tracks on ${target}.${track.property}; merge them into one track`, 'CE_COMPILE_ANIMATION_TRACK_DUPLICATE',
3407
3407
  );
3408
3408
  }
3409
3409
  claimed.add(claim);
@@ -3427,7 +3427,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3427
3427
  'channels at every time, its setup value before its first key included, so the one the file states ' +
3428
3428
  'later overwrites the other everywhere and the first one\'s keys there are read by nothing. Key each ' +
3429
3429
  'channel once: "rgb" and "alpha" move the light colour\'s two halves on their own key times, "rgba" ' +
3430
- 'moves them together, and "rgb2" / "rgba2" add the dark colour',
3430
+ 'moves them together, and "rgb2" / "rgba2" add the dark colour', 'CE_COMPILE_COLOUR_TRACK_CHANNEL_OVERLAP',
3431
3431
  );
3432
3432
  }
3433
3433
  }
@@ -3480,7 +3480,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3480
3480
  if (kinds.length > 0) {
3481
3481
  throw new CompileError(
3482
3482
  `animation "${animName}" keys "${name}" as ${group === 'ik' ? 'an' : 'a'} ${group} constraint, but the rig declares it as a ` +
3483
- `"${kinds.join('"/"')}" constraint — the parser looks a timeline's target up by name AND type, misses, and throws`,
3483
+ `"${kinds.join('"/"')}" constraint — the parser looks a timeline's target up by name AND type, misses, and throws`, 'CE_COMPILE_CONSTRAINT_TIMELINE_TYPE_MISMATCH',
3484
3484
  );
3485
3485
  }
3486
3486
  const known = constraintNamesOfKind.get(group) ?? [];
@@ -3488,13 +3488,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3488
3488
  `animation "${animName}" keys unknown ${group} constraint "${name}"; ` +
3489
3489
  (known.length
3490
3490
  ? `the rig declares ${group} constraint(s): ${known.join(', ')}`
3491
- : `the rig declares no ${group} constraint at all`),
3491
+ : `the rig declares no ${group} constraint at all`), 'CE_COMPILE_CONSTRAINT_TIMELINE_TARGET_UNKNOWN',
3492
3492
  );
3493
3493
  }
3494
3494
  if (constraintTimelines[group].has(name)) {
3495
3495
  throw new CompileError(
3496
3496
  `animation "${animName}" has two ${group} timelines on constraint "${name}"; ` +
3497
- 'the group holds one timeline per constraint, so merge them into one',
3497
+ 'the group holds one timeline per constraint, so merge them into one', 'CE_COMPILE_CONSTRAINT_TIMELINE_DUPLICATE',
3498
3498
  );
3499
3499
  }
3500
3500
  const keys = compileConstraintTrack(
@@ -3532,25 +3532,25 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3532
3532
  const table = skinTables.get(skinName);
3533
3533
  if (!table) {
3534
3534
  throw new CompileError(
3535
- `${at}: this rig emits no skin called "${skinName}" (it emits: ${[...skinTables.keys()].join(', ')})`,
3535
+ `${at}: this rig emits no skin called "${skinName}" (it emits: ${[...skinTables.keys()].join(', ')})`, 'CE_COMPILE_ATTACHMENT_TIMELINE_SKIN_UNKNOWN',
3536
3536
  );
3537
3537
  }
3538
3538
  const perSlot = table[track.slot];
3539
3539
  if (!perSlot) {
3540
3540
  throw new CompileError(
3541
3541
  `${at}: skin "${skinName}" gives slot "${String(track.slot)}" no attachments` +
3542
- (slotNames.has(track.slot) ? '' : ', and this rig does not declare that slot at all'),
3542
+ (slotNames.has(track.slot) ? '' : ', and this rig does not declare that slot at all'), 'CE_COMPILE_ATTACHMENT_TIMELINE_SLOT_EMPTY',
3543
3543
  );
3544
3544
  }
3545
3545
  const attachment = perSlot[track.attachment];
3546
3546
  if (!attachment) {
3547
3547
  throw new CompileError(
3548
3548
  `${at}: slot "${track.slot}" in skin "${skinName}" has no attachment "${String(track.attachment)}" ` +
3549
- `(it has: ${Object.keys(perSlot).join(', ')})`,
3549
+ `(it has: ${Object.keys(perSlot).join(', ')})`, 'CE_COMPILE_ATTACHMENT_TIMELINE_ATTACHMENT_UNKNOWN',
3550
3550
  );
3551
3551
  }
3552
3552
  if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
3553
- throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`);
3553
+ throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`, 'CE_COMPILE_DEFORM_TIMELINE_DUPLICATE');
3554
3554
  }
3555
3555
  const keys = compileDeformTrack(
3556
3556
  track,
@@ -3579,28 +3579,28 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3579
3579
  const table = skinTables.get(skinName);
3580
3580
  if (!table) {
3581
3581
  throw new CompileError(
3582
- `${at}: this rig emits no skin called "${skinName}" (it emits: ${[...skinTables.keys()].join(', ')})`,
3582
+ `${at}: this rig emits no skin called "${skinName}" (it emits: ${[...skinTables.keys()].join(', ')})`, 'CE_COMPILE_ATTACHMENT_TIMELINE_SKIN_UNKNOWN',
3583
3583
  );
3584
3584
  }
3585
3585
  const perSlot = table[track.slot];
3586
3586
  if (!perSlot) {
3587
3587
  throw new CompileError(
3588
3588
  `${at}: skin "${skinName}" gives slot "${String(track.slot)}" no attachments` +
3589
- (slotNames.has(track.slot) ? '' : ', and this rig does not declare that slot at all'),
3589
+ (slotNames.has(track.slot) ? '' : ', and this rig does not declare that slot at all'), 'CE_COMPILE_ATTACHMENT_TIMELINE_SLOT_EMPTY',
3590
3590
  );
3591
3591
  }
3592
3592
  const attachment = perSlot[track.attachment];
3593
3593
  if (!attachment) {
3594
3594
  throw new CompileError(
3595
3595
  `${at}: slot "${track.slot}" in skin "${skinName}" has no attachment "${String(track.attachment)}" ` +
3596
- `(it has: ${Object.keys(perSlot).join(', ')})`,
3596
+ `(it has: ${Object.keys(perSlot).join(', ')})`, 'CE_COMPILE_ATTACHMENT_TIMELINE_ATTACHMENT_UNKNOWN',
3597
3597
  );
3598
3598
  }
3599
3599
  const keys = compileSequenceTrack(track, anim.duration, attachment, at);
3600
3600
  for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
3601
3601
  const slot = attachmentTimelinesOf(skinName, track.slot);
3602
3602
  if (slot.get(track.attachment)?.sequence) {
3603
- throw new CompileError(`${at}: two sequence timelines on one attachment; merge them into one`);
3603
+ throw new CompileError(`${at}: two sequence timelines on one attachment; merge them into one`, 'CE_COMPILE_SEQUENCE_TIMELINE_DUPLICATE');
3604
3604
  }
3605
3605
  slot.set(track.attachment, { ...(slot.get(track.attachment) ?? {}), sequence: keys });
3606
3606
  }
@@ -3625,7 +3625,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3625
3625
  // a key on a track whose neighbour already sits on the declared duration.
3626
3626
  if (Math.abs(compiledDuration - anim.duration) > FRAME) {
3627
3627
  throw new CompileError(
3628
- `animation "${animName}" declares duration ${anim.duration}s but its last key is at ${compiledDuration}s`,
3628
+ `animation "${animName}" declares duration ${anim.duration}s but its last key is at ${compiledDuration}s`, 'CE_COMPILE_ANIMATION_DURATION_MISMATCH',
3629
3629
  );
3630
3630
  }
3631
3631
  // The model's record of the animation. Which groups the file carries, in
@@ -3715,7 +3715,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3715
3715
  const editorOrder: ModelEditorOrder = editorOrderOver(skins, [...animations.keys()]);
3716
3716
 
3717
3717
  for (const slot of slots) {
3718
- if (!boneNames.has(slot.bone)) throw new CompileError(`slot "${slot.name}" has no bone`);
3718
+ if (!boneNames.has(slot.bone)) throw new CompileError(`slot "${slot.name}" has no bone`, 'CE_COMPILE_SLOT_BONE_MISSING');
3719
3719
  }
3720
3720
 
3721
3721
  // One bone order per weighted attachment (issue #1407), on the finished
@@ -3904,7 +3904,7 @@ function buildBone(spec: RigBone, soFar: ModelBone[], ctx: BoneContext): ModelBo
3904
3904
  bone.y = f32(world[1]);
3905
3905
  } else {
3906
3906
  const parent = computeWorldTransforms(soFar).get(spec.parent);
3907
- if (!parent) throw new CompileError(`bone "${spec.name}" names parent "${spec.parent}", which is declared after it`);
3907
+ if (!parent) throw new CompileError(`bone "${spec.name}" names parent "${spec.parent}", which is declared after it`, 'CE_COMPILE_BONE_PARENT_ORDER');
3908
3908
  const [lx, ly] = toBoneLocal(parent, world[0], world[1]);
3909
3909
  bone.x = f32(lx);
3910
3910
  bone.y = f32(ly);
@@ -3936,7 +3936,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3936
3936
  if (!from) return null;
3937
3937
  const needManifest = (what: string): FaceManifest => {
3938
3938
  if (!ctx.manifest) {
3939
- throw new CompileError(`bone "${spec.name}" takes its position from ${what}, which needs a cut manifest`);
3939
+ throw new CompileError(`bone "${spec.name}" takes its position from ${what}, which needs a cut manifest`, 'CE_COMPILE_BONE_POSITION_MANIFEST_REQUIRED');
3940
3940
  }
3941
3941
  return ctx.manifest;
3942
3942
  };
@@ -3945,7 +3945,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3945
3945
  const anchor = manifest.anchors?.[from.anchor];
3946
3946
  if (!anchor || anchor.length < 2) {
3947
3947
  throw new CompileError(
3948
- `manifest anchors has no [x, y] for "${from.anchor}" (bone "${spec.name}" of rig "${ctx.rig.name}")`,
3948
+ `manifest anchors has no [x, y] for "${from.anchor}" (bone "${spec.name}" of rig "${ctx.rig.name}")`, 'CE_COMPILE_BONE_ANCHOR_MISSING',
3949
3949
  );
3950
3950
  }
3951
3951
  return [anchor[0], anchor[1]];
@@ -3955,7 +3955,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3955
3955
  const part = ctx.partBySlot.get(from.slotWindow);
3956
3956
  if (!part) {
3957
3957
  throw new CompileError(
3958
- `bone "${spec.name}" sits at the centre of slot "${from.slotWindow}", which this cut's manifest carries no part for`,
3958
+ `bone "${spec.name}" sits at the centre of slot "${from.slotWindow}", which this cut's manifest carries no part for`, 'CE_COMPILE_BONE_SLOT_WINDOW_MISSING',
3959
3959
  );
3960
3960
  }
3961
3961
  const win = partWindow(part, manifest);
@@ -3966,7 +3966,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3966
3966
  const centre = ctx.partBySlot.get(from.meshCenter)?.mesh?.center;
3967
3967
  if (!centre) {
3968
3968
  throw new CompileError(
3969
- `bone "${spec.name}" sits on the mesh centre of slot "${from.meshCenter}", which declares no mesh.center`,
3969
+ `bone "${spec.name}" sits on the mesh centre of slot "${from.meshCenter}", which declares no mesh.center`, 'CE_COMPILE_BONE_MESH_CENTER_MISSING',
3970
3970
  );
3971
3971
  }
3972
3972
  return [centre[0], centre[1]];
@@ -3979,7 +3979,7 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
3979
3979
  const source = spec.from?.rotation;
3980
3980
  if (source === 'axis') {
3981
3981
  if (ctx.axisSpineDeg === null) {
3982
- throw new CompileError(`bone "${spec.name}" takes its rotation from the cut axis, which the manifest does not declare`);
3982
+ throw new CompileError(`bone "${spec.name}" takes its rotation from the cut axis, which the manifest does not declare`, 'CE_COMPILE_BONE_ROTATION_AXIS_MISSING');
3983
3983
  }
3984
3984
  return ctx.axisSpineDeg;
3985
3985
  }
@@ -3988,7 +3988,7 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
3988
3988
  const anchor = ctx.manifest?.anchors?.[key];
3989
3989
  if (!anchor || anchor.length < 3) {
3990
3990
  throw new CompileError(
3991
- `bone "${spec.name}" takes its rotation from anchor "${key}", which has no third element (a screen-space facing angle)`,
3991
+ `bone "${spec.name}" takes its rotation from anchor "${key}", which has no third element (a screen-space facing angle)`, 'CE_COMPILE_BONE_ANCHOR_ROTATION_MISSING',
3992
3992
  );
3993
3993
  }
3994
3994
  return screenToSpineDegrees(anchor[2]);
@@ -4124,7 +4124,7 @@ function refuseDefaultSkinContest(slotName: string, placeholder: string, skins:
4124
4124
  `"Multiple attachments have the same name: ${placeholder} ${placeholder}", because a default-skin attachment ` +
4125
4125
  "hangs on the slot beside the named skins' placeholder of that name. Move the default skin's entry for this " +
4126
4126
  `slot into a named skin — call it "base" — so every skin filling "${placeholder}" is a named one. Two or more ` +
4127
- 'named skins sharing a placeholder is the shape the editor exports itself, and it needs no `name` (#796).',
4127
+ 'named skins sharing a placeholder is the shape the editor exports itself, and it needs no `name` (#796).', 'CE_COMPILE_DEFAULT_SKIN_PLACEHOLDER_CONFLICT',
4128
4128
  );
4129
4129
  }
4130
4130
 
@@ -4304,7 +4304,7 @@ function buildRigAttachment(
4304
4304
  `is one of ${SPINE_ATTACHMENT_TYPES.join(', ')}, or the key is absent and reads as "region". ` +
4305
4305
  'PRESENT-and-null is not absent: `getValue(map, "type", "region")` takes the default only when the key is ' +
4306
4306
  'missing (`SkeletonJson.ts:527`), so this map matches no case, `readAttachment` returns null (`:653`), and ' +
4307
- 'the attachment is dropped from the skeleton without a word. Remove the key, or name a type.',
4307
+ 'the attachment is dropped from the skeleton without a word. Remove the key, or name a type.', 'CE_COMPILE_ATTACHMENT_TYPE_NOT_NAME',
4308
4308
  );
4309
4309
  }
4310
4310
  const type = stated ?? 'region';
@@ -4319,7 +4319,7 @@ function buildRigAttachment(
4319
4319
  `${where}: "name" is ${JSON.stringify(statedName) ?? String(statedName)}, which is not a string. An ` +
4320
4320
  "attachment's name is the runtime's `Attachment.name` — `getValue(map, \"name\", placeholder)`, " +
4321
4321
  '`SkeletonJson.js:526` — and on a region, mesh or linked mesh it is also what `path` defaults to. Write it ' +
4322
- 'as a string, or leave the key out and the attachment is named by its placeholder.',
4322
+ 'as a string, or leave the key out and the attachment is named by its placeholder.', 'CE_COMPILE_ATTACHMENT_NAME_NOT_STRING',
4323
4323
  );
4324
4324
  }
4325
4325
  // A linked mesh in the format's OTHER spelling. `type: "mesh"` and `type:
@@ -4337,7 +4337,7 @@ function buildRigAttachment(
4337
4337
  if (type === 'boundingbox') return buildRigBoundingBox(att as RigBoundingBoxAttachment, where, ctx);
4338
4338
  if (type === 'clipping') return buildRigClipping(att as RigClippingAttachment, where, ctx);
4339
4339
  if (type === 'path') return buildRigPath(att as RigPathAttachment, where, ctx);
4340
- if (DEFERRED_ATTACHMENTS[type] !== undefined) throw new NotImplementedError(deferredAttachmentRefusal(type, where, ''));
4340
+ if (DEFERRED_ATTACHMENTS[type] !== undefined) throw new NotImplementedError(deferredAttachmentRefusal(type, where, ''), 'CE_COMPILE_ATTACHMENT_TYPE_DEFERRED');
4341
4341
  const near = nearMisses(type, SPINE_ATTACHMENT_TYPES);
4342
4342
  throw new CompileError(
4343
4343
  `${where}: attachment type "${type}" is not one of the ${SPINE_ATTACHMENT_TYPES.length} the Spine 4.3 format ` +
@@ -4349,7 +4349,7 @@ function buildRigAttachment(
4349
4349
  // this format standing one level up from where it was written.
4350
4350
  (type === 'sequence'
4351
4351
  ? ' "sequence" is a KEY on a region or a mesh rather than a type of its own (docs/SPEC_COVERAGE.md part 1-6).'
4352
- : ''),
4352
+ : ''), 'CE_COMPILE_ATTACHMENT_TYPE_UNKNOWN',
4353
4353
  );
4354
4354
  }
4355
4355
 
@@ -4386,26 +4386,26 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
4386
4386
  if (typeof count !== 'number' || !Number.isInteger(count) || count < 3) {
4387
4387
  throw new CompileError(
4388
4388
  `${where}: vertexCount is ${JSON.stringify(count)}; a polygon needs at least 3 vertices, stated outright — ` +
4389
- 'the field has no parser default, and an absent one reads as 0 and takes the polygon with it',
4389
+ 'the field has no parser default, and an absent one reads as 0 and takes the polygon with it', 'CE_COMPILE_POLYGON_VERTEX_COUNT_INVALID',
4390
4390
  );
4391
4391
  }
4392
4392
  if (att.vertices && att.weights) {
4393
4393
  throw new CompileError(
4394
- `${where}: geometry comes as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`,
4394
+ `${where}: geometry comes as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`, 'CE_COMPILE_VERTEX_GEOMETRY_SOURCES_CONFLICT',
4395
4395
  );
4396
4396
  }
4397
4397
  if (att.weights) {
4398
4398
  if (att.boneIndexing === 'raw') {
4399
- throw new CompileError(`${where}: "boneIndexing": "raw" describes a "vertices" run; "weights" always binds by name`);
4399
+ throw new CompileError(`${where}: "boneIndexing": "raw" describes a "vertices" run; "weights" always binds by name`, 'CE_COMPILE_NAMED_WEIGHTS_RAW_INDEXING');
4400
4400
  }
4401
4401
  if (att.weights.length !== count) {
4402
- throw new CompileError(`${where}: weights cover ${att.weights.length} vertices but vertexCount is ${count}`);
4402
+ throw new CompileError(`${where}: weights cover ${att.weights.length} vertices but vertexCount is ${count}`, 'CE_COMPILE_POLYGON_WEIGHT_VERTEX_COUNT');
4403
4403
  }
4404
4404
  return { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
4405
4405
  }
4406
4406
  const raw = att.vertices;
4407
4407
  if (!raw || raw.length === 0) {
4408
- throw new CompileError(`${where}: needs geometry — "vertices" (x, y per vertex) or "weights" (bound by name)`);
4408
+ throw new CompileError(`${where}: needs geometry — "vertices" (x, y per vertex) or "weights" (bound by name)`, 'CE_COMPILE_POLYGON_GEOMETRY_MISSING');
4409
4409
  }
4410
4410
  const unweighted = raw.length === count * 2;
4411
4411
  if (!unweighted) {
@@ -4415,7 +4415,7 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
4415
4415
  'The parser reads that as a WEIGHTED run, whose bone INDEXES point into the emitted bone array — a list ' +
4416
4416
  'the spec never writes, so inserting a bone rebinds every vertex in silence. ' +
4417
4417
  'Give the bindings by name as "weights": [[{ "bone": …, "x": …, "y": …, "weight": … }, …], …], ' +
4418
- 'fix vertexCount, or say "boneIndexing": "raw" on this attachment to keep the index form deliberately.',
4418
+ 'fix vertexCount, or say "boneIndexing": "raw" on this attachment to keep the index form deliberately.', 'CE_COMPILE_POLYGON_WEIGHTED_RUN_IMPLICIT',
4419
4419
  );
4420
4420
  }
4421
4421
  // A raw run still has to decode to exactly `vertexCount` vertices, or the
@@ -4423,7 +4423,7 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
4423
4423
  checkRawRunShape(raw, count, `vertexCount is ${count}`, where);
4424
4424
  }
4425
4425
  for (const n of raw) {
4426
- if (!Number.isFinite(n)) throw new CompileError(`${where}: the vertex array holds a non-finite value ${String(n)}`);
4426
+ if (!Number.isFinite(n)) throw new CompileError(`${where}: the vertex array holds a non-finite value ${String(n)}`, 'CE_COMPILE_VERTEX_VALUE_NONFINITE');
4427
4427
  }
4428
4428
  if (unweighted) return { weighted: false, xy: raw.map(f32) };
4429
4429
  const bindings = bindRawRun(raw.map(f32), ctx.bones, where);
@@ -4442,17 +4442,17 @@ function checkRawRunShape(raw: readonly number[], vertices: number, countSays: s
4442
4442
  for (let i = 0; i < raw.length; decoded++) {
4443
4443
  const bones = raw[i++];
4444
4444
  if (!Number.isInteger(bones) || bones < 1) {
4445
- throw new CompileError(`${where}: the raw weighted run has a bone count of ${String(bones)} at index ${i - 1}`);
4445
+ throw new CompileError(`${where}: the raw weighted run has a bone count of ${String(bones)} at index ${i - 1}`, 'CE_COMPILE_RAW_WEIGHT_BONE_COUNT_INVALID');
4446
4446
  }
4447
4447
  i += bones * 4;
4448
4448
  if (i > raw.length) {
4449
4449
  throw new CompileError(
4450
- `${where}: the raw weighted run is truncated — vertex ${decoded} claims ${bones} bone(s) and the array ends first`,
4450
+ `${where}: the raw weighted run is truncated — vertex ${decoded} claims ${bones} bone(s) and the array ends first`, 'CE_COMPILE_RAW_WEIGHT_RUN_TRUNCATED',
4451
4451
  );
4452
4452
  }
4453
4453
  }
4454
4454
  if (decoded !== vertices) {
4455
- throw new CompileError(`${where}: the raw weighted run decodes to ${decoded} vertices but ${countSays}`);
4455
+ throw new CompileError(`${where}: the raw weighted run decodes to ${decoded} vertices but ${countSays}`, 'CE_COMPILE_RAW_WEIGHT_VERTEX_COUNT_MISMATCH');
4456
4456
  }
4457
4457
  }
4458
4458
 
@@ -4475,7 +4475,7 @@ function bindRawRun(run: readonly number[], bones: readonly ModelBone[], where:
4475
4475
  for (let k = 0; k < count; k++, i += 4) {
4476
4476
  const bone = bones[run[i]];
4477
4477
  if (bone === undefined) {
4478
- throw new CompileError(`${where}: vertex ${out.length} binds bone index ${run[i]}, which is not in the bone list`);
4478
+ throw new CompileError(`${where}: vertex ${out.length} binds bone index ${run[i]}, which is not in the bone list`, 'CE_COMPILE_RAW_WEIGHT_BONE_INDEX_UNKNOWN');
4479
4479
  }
4480
4480
  vertex.push({ bone: bone.name, x: run[i + 1], y: run[i + 2], weight: run[i + 3] });
4481
4481
  }
@@ -4676,13 +4676,13 @@ function resolveStageBox(
4676
4676
  if (asked === undefined) return null;
4677
4677
  const field = `skeleton.stageBox (slot "${asked.slot}", attachment "${asked.attachment}")`;
4678
4678
  if (ctx.stageWidth === undefined || ctx.stageHeight === undefined) {
4679
- throw new CompileError(`${field}: the rig declares no stage, so there is no box to write — state skeleton.width and skeleton.height`);
4679
+ throw new CompileError(`${field}: the rig declares no stage, so there is no box to write — state skeleton.width and skeleton.height`, 'CE_COMPILE_STAGE_BOX_STAGE_MISSING');
4680
4680
  }
4681
4681
  const rigSlot = rig.slots.find((s) => s.name === asked.slot);
4682
4682
  if (rigSlot === undefined) {
4683
4683
  throw new CompileError(
4684
4684
  `${field}: slot "${asked.slot}" is not one the rig declares. Declare it in "slots" — where it stands in the ` +
4685
- `array is its place in the draw order, and its "bone" is the box's bone — e.g. { "name": "${asked.slot}", "bone": "root", "attachment": "${asked.attachment}" }`,
4685
+ `array is its place in the draw order, and its "bone" is the box's bone — e.g. { "name": "${asked.slot}", "bone": "root", "attachment": "${asked.attachment}" }`, 'CE_COMPILE_STAGE_BOX_SLOT_UNKNOWN',
4686
4686
  );
4687
4687
  }
4688
4688
  const filledBy = [
@@ -4694,13 +4694,13 @@ function resolveStageBox(
4694
4694
  if (filledBy.length > 0) {
4695
4695
  throw new CompileError(
4696
4696
  `${field}: slot "${asked.slot}" is also filled by ${filledBy.join(' and ')}. The stage box is its slot's one ` +
4697
- 'attachment, so a reader of that slot finds the stage and nothing else: give the box a slot of its own',
4697
+ 'attachment, so a reader of that slot finds the stage and nothing else: give the box a slot of its own', 'CE_COMPILE_STAGE_BOX_SLOT_CONFLICT',
4698
4698
  );
4699
4699
  }
4700
4700
  // The box's bone: the root, stating no setup transform — as built, so a root placed `from` the manifest is read as placed.
4701
4701
  const bone = ctx.bones.find((b) => b.name === rigSlot.bone);
4702
4702
  if (bone === undefined) {
4703
- throw new CompileError(`${field}: slot "${asked.slot}" names bone "${rigSlot.bone}", which the rig does not declare`);
4703
+ throw new CompileError(`${field}: slot "${asked.slot}" names bone "${rigSlot.bone}", which the rig does not declare`, 'CE_COMPILE_STAGE_BOX_BONE_UNKNOWN');
4704
4704
  }
4705
4705
  if (bone.parent !== undefined) {
4706
4706
  throw new CompileError(
@@ -4708,7 +4708,7 @@ function resolveStageBox(
4708
4708
  "the stage's corners in world space, written as they are, and the gate holds them on the header's grid " +
4709
4709
  "through the runtime's own frame of an unrotated root; every bone below it adds that frame's residue once " +
4710
4710
  "more (the runtime spells an unrotated b as cos 90° at its pi, -2.3e-8 per level). Put the slot on the root " +
4711
- `("${ctx.bones[0]?.name ?? 'root'}")`,
4711
+ `("${ctx.bones[0]?.name ?? 'root'}")`, 'CE_COMPILE_STAGE_BOX_BONE_NOT_ROOT',
4712
4712
  );
4713
4713
  }
4714
4714
  const stated = [
@@ -4719,7 +4719,7 @@ function resolveStageBox(
4719
4719
  throw new CompileError(
4720
4720
  `${field}: slot "${asked.slot}" hangs on the root "${bone.name}", which states a setup transform (${stated.join(', ')}). ` +
4721
4721
  "The box's vertices are the stage's corners in world space, written as they are, so the root must not move, " +
4722
- 'turn, scale or shear at setup — move what the root carries onto a bone below it, or drop stageBox',
4722
+ 'turn, scale or shear at setup — move what the root carries onto a bone below it, or drop stageBox', 'CE_COMPILE_STAGE_BOX_ROOT_TRANSFORMED',
4723
4723
  );
4724
4724
  }
4725
4725
  const x = rig.skeleton?.x ?? 0;
@@ -4760,7 +4760,7 @@ function buildRigClipping(
4760
4760
  if (typeof att.end !== 'string' || !ctx.slotNames.has(att.end)) {
4761
4761
  throw new CompileError(
4762
4762
  `${where}: end names slot ${JSON.stringify(att.end)}, which this rig does not declare; ` +
4763
- 'a miss loads as null and the clip then runs to the bottom of the draw order',
4763
+ 'a miss loads as null and the clip then runs to the bottom of the draw order', 'CE_COMPILE_CLIPPING_END_SLOT_UNKNOWN',
4764
4764
  );
4765
4765
  }
4766
4766
  }
@@ -4804,7 +4804,7 @@ function setupWorldVertices(
4804
4804
  let y = 0;
4805
4805
  for (const binding of vertex) {
4806
4806
  const m = transforms.get(binding.bone);
4807
- if (!m) throw new CompileError(`${where}: vertex ${out.length} binds bone "${binding.bone}", which has no setup transform`);
4807
+ if (!m) throw new CompileError(`${where}: vertex ${out.length} binds bone "${binding.bone}", which has no setup transform`, 'CE_COMPILE_VERTEX_BIND_TRANSFORM_MISSING');
4808
4808
  const [wx, wy] = toWorld(m, binding.x, binding.y);
4809
4809
  x += wx * binding.weight;
4810
4810
  y += wy * binding.weight;
@@ -4947,14 +4947,14 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4947
4947
  throw new CompileError(
4948
4948
  `${where}: vertexCount is ${count}, which is not a multiple of 3. A path's vertices are knots AND their ` +
4949
4949
  'Bezier handles, read in groups of three; the parser sizes its lengths array with `vertexCount / 3` and ' +
4950
- 'accepts a fractional size without a word, so the curves would straddle the knots.',
4950
+ 'accepts a fractional size without a word, so the curves would straddle the knots.', 'CE_COMPILE_PATH_VERTEX_COUNT_MULTIPLE',
4951
4951
  );
4952
4952
  }
4953
4953
  const minimum = closed ? 3 : 6;
4954
4954
  if (count < minimum) {
4955
4955
  throw new CompileError(
4956
4956
  `${where}: vertexCount is ${count} and ${closed ? 'a closed' : 'an open'} path needs at least ${minimum} — ` +
4957
- `${closed ? 'a closed path of K curves carries 3K points' : 'an open one carries 3(K + 1), the first and last being the end knots\' outer handles'}`,
4957
+ `${closed ? 'a closed path of K curves carries 3K points' : 'an open one carries 3(K + 1), the first and last being the end knots\' outer handles'}`, 'CE_COMPILE_PATH_VERTEX_COUNT_MINIMUM',
4958
4958
  );
4959
4959
  }
4960
4960
  const entries = count / 3;
@@ -4963,7 +4963,7 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4963
4963
  lengths = statedPathLengths(att.lengths, entries, closed, where);
4964
4964
  } else {
4965
4965
  const anchor = ctx.transforms.get(ctx.anchorBone);
4966
- if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
4966
+ if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
4967
4967
  const points = setupWorldVertices(vertices, anchor, ctx.transforms, where);
4968
4968
  lengths = pathCurveLengths(pathChain(points, true));
4969
4969
  // The total the runtime reads is the last CURVE's entry, `lengths[vertexCount
@@ -4973,7 +4973,7 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4973
4973
  if (lengths.length !== entries || !lengths.every((n) => Number.isFinite(n)) || !(total > 0)) {
4974
4974
  throw new CompileError(
4975
4975
  `${where}: the geometry measures ${lengths.length} entry(ies) where the parser sizes ${entries}, of total ` +
4976
- `length ${String(total)}; a path of zero length divides by zero the first time a bone is placed on it`,
4976
+ `length ${String(total)}; a path of zero length divides by zero the first time a bone is placed on it`, 'CE_COMPILE_PATH_GEOMETRY_LENGTH_INVALID',
4977
4977
  );
4978
4978
  }
4979
4979
  }
@@ -5009,25 +5009,25 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
5009
5009
  */
5010
5010
  function statedPathLengths(stated: unknown, entries: number, closed: boolean, where: string): number[] {
5011
5011
  if (!Array.isArray(stated)) {
5012
- throw new CompileError(`${where}: "lengths" is ${JSON.stringify(stated)}; it is an array of ${entries} number(s)`);
5012
+ throw new CompileError(`${where}: "lengths" is ${JSON.stringify(stated)}; it is an array of ${entries} number(s)`, 'CE_COMPILE_PATH_LENGTHS_NOT_ARRAY');
5013
5013
  }
5014
5014
  if (stated.length !== entries) {
5015
5015
  throw new CompileError(
5016
5016
  `${where}: "lengths" has ${stated.length} entry(ies) where the parser sizes ${entries} — vertexCount / 3 on ` +
5017
5017
  `${closed ? 'a closed' : 'an open'} path${closed ? '' : ', one more than its curves: the editor writes the wrap-around curve\'s cumulative last and nothing reads it'}. ` +
5018
- 'Give the array the source states, or leave the field out and rigc measures it',
5018
+ 'Give the array the source states, or leave the field out and rigc measures it', 'CE_COMPILE_PATH_LENGTHS_COUNT',
5019
5019
  );
5020
5020
  }
5021
5021
  let previous = 0;
5022
5022
  for (let i = 0; i < stated.length; i++) {
5023
5023
  const value: unknown = stated[i];
5024
5024
  if (typeof value !== 'number' || !Number.isFinite(value)) {
5025
- throw new CompileError(`${where}: "lengths"[${i}] is ${JSON.stringify(value)}; every entry is a finite cumulative length`);
5025
+ throw new CompileError(`${where}: "lengths"[${i}] is ${JSON.stringify(value)}; every entry is a finite cumulative length`, 'CE_COMPILE_PATH_LENGTH_NONFINITE');
5026
5026
  }
5027
5027
  if (value < previous) {
5028
5028
  throw new CompileError(
5029
5029
  `${where}: "lengths"[${i}] is ${value}, below ${i === 0 ? '0' : `the ${previous} before it`}. The array is CUMULATIVE — the length at ` +
5030
- 'the end of each curve — so no entry is below its predecessor',
5030
+ 'the end of each curve — so no entry is below its predecessor', 'CE_COMPILE_PATH_LENGTHS_DESCENDING',
5031
5031
  );
5032
5032
  }
5033
5033
  previous = value;
@@ -5068,7 +5068,7 @@ function atlasedImage(image: string, where: string, ctx: AttachmentContext): Com
5068
5068
  'measured and atlased — one per file, whichever skin names it (issue #555) — so reaching this means rigc ' +
5069
5069
  'skipped one, not that the spec left anything out. ' +
5070
5070
  (near.length ? `The nearest region(s) built are ${near.map((n) => JSON.stringify(n)).join(', ')}. ` : '') +
5071
- `The atlas holds ${built.length} region(s): ${built.join(', ')}`,
5071
+ `The atlas holds ${built.length} region(s): ${built.join(', ')}`, 'CE_COMPILE_INTERNAL_IMAGE_NOT_ATLASED',
5072
5072
  );
5073
5073
  }
5074
5074
 
@@ -5215,7 +5215,7 @@ function sequenceFrameSize(
5215
5215
  throw new CompileError(
5216
5216
  `${where}: the ${frames.length} frames of this sequence measure ${sizes.join(', ')} in ${field} ` +
5217
5217
  `(${frames.map((img) => `"${img.region}" ${img[field]}`).join(', ')}), and an attachment has one ` +
5218
- `${field} every frame is drawn into. State "${field}" — the frames do not agree on one`,
5218
+ `${field} every frame is drawn into. State "${field}" — the frames do not agree on one`, 'CE_COMPILE_SEQUENCE_FRAME_SIZE_AMBIGUOUS',
5219
5219
  );
5220
5220
  }
5221
5221
  }
@@ -5240,7 +5240,7 @@ function sequenceFrames(att: { path?: string; sequence?: RigSequence }, placehol
5240
5240
  throw new CompileError(
5241
5241
  `${where}: sequence frame ${i} is the region "${region}", which was never added to the atlas — every frame ` +
5242
5242
  'of a sequence is atlased or refused by name before any attachment is built, so reaching this means rigc ' +
5243
- 'skipped one',
5243
+ 'skipped one', 'CE_COMPILE_INTERNAL_SEQUENCE_FRAME_NOT_ATLASED',
5244
5244
  );
5245
5245
  }
5246
5246
  frames.push(img);
@@ -5295,7 +5295,7 @@ function buildRigRegion(
5295
5295
  ? ''
5296
5296
  : ` in the page's texels, which its scale: ${img.atlasScale} makes a ` +
5297
5297
  `${img.width}x${img.height} drawing (+/- ${f32(0.5 / img.atlasScale)}px, the pack's rounding)`) +
5298
- ')',
5298
+ ')', 'CE_COMPILE_REGION_ATLAS_SIZE_MISMATCH',
5299
5299
  );
5300
5300
  }
5301
5301
  }
@@ -5306,7 +5306,7 @@ function buildRigRegion(
5306
5306
  // No parser default: an omission loads as NaN and every UV collapses, with
5307
5307
  // no error at all. So it is this or nothing.
5308
5308
  throw new CompileError(
5309
- `${where}: a region needs width and height — give them, or give an "image" and rigc will measure the PNG`,
5309
+ `${where}: a region needs width and height — give them, or give an "image" and rigc will measure the PNG`, 'CE_COMPILE_REGION_SIZE_MISSING',
5310
5310
  );
5311
5311
  }
5312
5312
  const out: ModelRegionAttachment = { kind: 'region', width: f32(width), height: f32(height) };
@@ -5390,12 +5390,12 @@ function regionAtlasRect(
5390
5390
  function bindNamedWeights(weights: RigMeshBinding[][], where: string, ctx: AttachmentContext): ModelBinding[][] {
5391
5391
  return weights.map((vertex, i) => {
5392
5392
  if (!Array.isArray(vertex) || vertex.length === 0) {
5393
- throw new CompileError(`${where}: vertex ${i} has no bone bindings; a weighted vertex names at least one bone`);
5393
+ throw new CompileError(`${where}: vertex ${i} has no bone bindings; a weighted vertex names at least one bone`, 'CE_COMPILE_VERTEX_BINDINGS_EMPTY');
5394
5394
  }
5395
5395
  return vertex.map((binding) => {
5396
5396
  if (!ctx.bones.some((b) => b.name === binding.bone)) {
5397
5397
  throw new CompileError(
5398
- `${where}: vertex ${i} binds bone ${JSON.stringify(binding.bone)}, which the rig does not declare as a bone`,
5398
+ `${where}: vertex ${i} binds bone ${JSON.stringify(binding.bone)}, which the rig does not declare as a bone`, 'CE_COMPILE_VERTEX_BIND_BONE_UNKNOWN',
5399
5399
  );
5400
5400
  }
5401
5401
  return { bone: binding.bone, x: f32(binding.x), y: f32(binding.y), weight: f32(binding.weight) };
@@ -5513,7 +5513,7 @@ function authoredHullAndEdges(
5513
5513
  checkHullOrder(outline, vertexCount);
5514
5514
  return { hull: outline.hull, edges: att.edges ?? meshEdges(vertexCount, triangles, outline.hull) };
5515
5515
  } catch (err) {
5516
- if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}${splitPointer(vertexCount, triangles, named)}`);
5516
+ if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}${splitPointer(vertexCount, triangles, named)}`, 'CE_COMPILE_AUTHORED_MESH_OUTLINE_INVALID');
5517
5517
  throw err;
5518
5518
  }
5519
5519
  }
@@ -5554,7 +5554,7 @@ function generatedHullAndEdges(geometry: MeshGeometry, where: string): { hull: n
5554
5554
  checkHullOrder(outline, vertexCount);
5555
5555
  } catch (err) {
5556
5556
  if (err instanceof MeshError) {
5557
- throw new CompileError(`${where}: internal: the ${geometry.kind} generator built an inconsistent mesh — ${err.message}`);
5557
+ throw new CompileError(`${where}: internal: the ${geometry.kind} generator built an inconsistent mesh — ${err.message}`, 'CE_COMPILE_INTERNAL_GENERATED_MESH_OUTLINE_INVALID');
5558
5558
  }
5559
5559
  throw err;
5560
5560
  }
@@ -5570,27 +5570,27 @@ function buildRigMesh(
5570
5570
  const authored =
5571
5571
  att.uvs !== undefined || att.triangles !== undefined || att.vertices !== undefined || att.weights !== undefined;
5572
5572
  if (authored && att.generator) {
5573
- throw new CompileError(`${where}: a mesh is either authored geometry or a generator, never both`);
5573
+ throw new CompileError(`${where}: a mesh is either authored geometry or a generator, never both`, 'CE_COMPILE_MESH_AUTHORED_AND_GENERATED');
5574
5574
  }
5575
5575
  if (att.generator) return buildGeneratedMesh(att, att.generator, placeholder, where, ctx);
5576
5576
  if (att.vertices && att.weights) {
5577
5577
  throw new CompileError(
5578
- `${where}: a mesh gives geometry as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`,
5578
+ `${where}: a mesh gives geometry as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`, 'CE_COMPILE_MESH_GEOMETRY_SOURCES_CONFLICT',
5579
5579
  );
5580
5580
  }
5581
5581
  if (!att.uvs || !att.triangles || !(att.vertices || att.weights)) {
5582
- throw new CompileError(`${where}: an authored mesh needs uvs, triangles and vertices or weights (or a "generator")`);
5582
+ throw new CompileError(`${where}: an authored mesh needs uvs, triangles and vertices or weights (or a "generator")`, 'CE_COMPILE_MESH_GEOMETRY_MISSING');
5583
5583
  }
5584
5584
  const uvCount = att.uvs.length;
5585
5585
  let vertices: ModelVertices;
5586
5586
  let boundBones: string[] = [];
5587
5587
  if (att.weights) {
5588
5588
  if (att.boneIndexing === 'raw') {
5589
- throw new CompileError(`${where}: "boneIndexing": "raw" describes a "vertices" run; "weights" always binds by name`);
5589
+ throw new CompileError(`${where}: "boneIndexing": "raw" describes a "vertices" run; "weights" always binds by name`, 'CE_COMPILE_NAMED_WEIGHTS_RAW_INDEXING');
5590
5590
  }
5591
5591
  if (att.weights.length !== uvCount / 2) {
5592
5592
  throw new CompileError(
5593
- `${where}: weights cover ${att.weights.length} vertices but there are ${uvCount / 2} uv pairs`,
5593
+ `${where}: weights cover ${att.weights.length} vertices but there are ${uvCount / 2} uv pairs`, 'CE_COMPILE_MESH_WEIGHT_UV_COUNT',
5594
5594
  );
5595
5595
  }
5596
5596
  vertices = { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
@@ -5605,7 +5605,7 @@ function buildRigMesh(
5605
5605
  `${where}: this mesh's "vertices" is a weighted run, whose bone INDEXES point into the emitted bone array — ` +
5606
5606
  'a list the spec never writes, so inserting a bone rebinds every vertex in silence. ' +
5607
5607
  'Give the bindings by name as "weights": [[{ "bone": …, "x": …, "y": …, "weight": … }, …], …], ' +
5608
- 'or say "boneIndexing": "raw" on this attachment to keep the index form deliberately.',
5608
+ 'or say "boneIndexing": "raw" on this attachment to keep the index form deliberately.', 'CE_COMPILE_MESH_WEIGHTED_RUN_IMPLICIT',
5609
5609
  );
5610
5610
  }
5611
5611
  if (weighted) {
@@ -5635,7 +5635,7 @@ function buildRigMesh(
5635
5635
  const height = seqSize !== null ? seqSize.height : (att.height ?? img?.height);
5636
5636
  if (width === undefined || height === undefined) {
5637
5637
  throw new CompileError(
5638
- `${where}: a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
5638
+ `${where}: a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`, 'CE_COMPILE_MESH_SIZE_MISSING',
5639
5639
  );
5640
5640
  }
5641
5641
  const out: ModelMeshAttachment = {
@@ -5714,7 +5714,7 @@ function buildRigLinkedMesh(
5714
5714
  'no geometry of its own — it draws the geometry of the attachment "source" names. The parser returns from ' +
5715
5715
  'the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so these keys are read by nothing ' +
5716
5716
  "at all: a link declaring 5 uvs beside a 4-vertex source loads the SOURCE's 4 vertices and says nothing. " +
5717
- `Remove ${geometry.length === 1 ? 'it' : 'them'}, or remove "source" and author this as a mesh of its own.`,
5717
+ `Remove ${geometry.length === 1 ? 'it' : 'them'}, or remove "source" and author this as a mesh of its own.`, 'CE_COMPILE_LINKED_MESH_GEOMETRY_IGNORED',
5718
5718
  );
5719
5719
  }
5720
5720
  const source = att.source;
@@ -5724,7 +5724,7 @@ function buildRigLinkedMesh(
5724
5724
  `attachment is filed under in its skin, not its "name"), and this one states ${JSON.stringify(source) ?? String(source)}. ` +
5725
5725
  '"source" is what MAKES a mesh linked: `getValue(map, "source", null)` is falsy-tested ' +
5726
5726
  '(`SkeletonJson.ts:582`), so an absent or empty one is read as an ordinary mesh, whose `uvs` this ' +
5727
- 'attachment does not have — the parser dereferences `map.uvs.length` and throws.',
5727
+ 'attachment does not have — the parser dereferences `map.uvs.length` and throws.', 'CE_COMPILE_LINKED_MESH_SOURCE_MISSING',
5728
5728
  );
5729
5729
  }
5730
5730
  if (att.slot !== undefined && !ctx.slotNames.has(att.slot)) {
@@ -5732,7 +5732,7 @@ function buildRigLinkedMesh(
5732
5732
  `${where}: "slot" is ${JSON.stringify(att.slot)}, which the rig does not declare as a slot. It names where ` +
5733
5733
  'the source lives and is resolved through `skeletonData.findSlot` (`SkeletonJson.ts:575`), which throws ' +
5734
5734
  `\`Source mesh slot not found\` on a miss. The rig's slots are ${[...ctx.slotNames].sort().map((s) => JSON.stringify(s)).join(', ')}. ` +
5735
- "Leave it out and the source is looked for in this attachment's own slot, which is the parser's default.",
5735
+ "Leave it out and the source is looked for in this attachment's own slot, which is the parser's default.", 'CE_COMPILE_LINKED_MESH_SLOT_UNKNOWN',
5736
5736
  );
5737
5737
  }
5738
5738
  ctx.links.push({
@@ -5751,7 +5751,7 @@ function buildRigLinkedMesh(
5751
5751
  const height = seqSize !== null ? seqSize.height : (att.height ?? img?.height);
5752
5752
  if (width === undefined || height === undefined) {
5753
5753
  throw new CompileError(
5754
- `${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`,
5754
+ `${where}: a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG`, 'CE_COMPILE_LINKED_MESH_SIZE_MISSING',
5755
5755
  );
5756
5756
  }
5757
5757
  // The link in full — skin, slot and `timelines` as the parser resolves them.
@@ -5813,7 +5813,7 @@ function resolveLinkedMeshes(
5813
5813
  `${link.where}: no "skin" is stated, so the parser looks for the source "${link.source}" in the default ` +
5814
5814
  `skin — and this rig declares no default skin. The rig's skins are ${skinNames.map((s) => JSON.stringify(s)).join(', ')}. ` +
5815
5815
  'State the skin the source is filed under. Left to the round trip this is a `TypeError` off the null ' +
5816
- '`SkeletonData.defaultSkin`, which names neither this attachment nor where it was looking.',
5816
+ '`SkeletonData.defaultSkin`, which names neither this attachment nor where it was looking.', 'CE_COMPILE_LINKED_MESH_DEFAULT_SKIN_MISSING',
5817
5817
  );
5818
5818
  }
5819
5819
  if (table === undefined) {
@@ -5821,7 +5821,7 @@ function resolveLinkedMeshes(
5821
5821
  `${link.where}: "skin" is ${JSON.stringify(link.skin)}, and the rig declares no such skin. The rig's skins ` +
5822
5822
  `are ${skinNames.map((s) => JSON.stringify(s)).join(', ')}. ` +
5823
5823
  "Left to the round trip this is the runtime's `Skin not found`, which names neither this attachment nor " +
5824
- 'where it was looking.',
5824
+ 'where it was looking.', 'CE_COMPILE_LINKED_MESH_SKIN_UNKNOWN',
5825
5825
  );
5826
5826
  }
5827
5827
  const slot = table[link.slot];
@@ -5835,7 +5835,7 @@ function resolveLinkedMeshes(
5835
5835
  `holds ${held.length === 0 ? 'no attachment at all' : `${held.length}: ${held.map((k) => JSON.stringify(k)).join(', ')}`}. ` +
5836
5836
  '"source" is the PLACEHOLDER the source is filed under — `skin.getAttachment(slotIndex, source)` ' +
5837
5837
  "(`SkeletonJson.ts:433`), whose table is keyed by the JSON key, not by the attachment's `name`. " +
5838
- "Left to the round trip this is the runtime's `Source mesh not found`.",
5838
+ "Left to the round trip this is the runtime's `Source mesh not found`.", 'CE_COMPILE_LINKED_MESH_SOURCE_UNKNOWN',
5839
5839
  );
5840
5840
  }
5841
5841
  const type = found.kind;
@@ -5845,7 +5845,7 @@ function resolveLinkedMeshes(
5845
5845
  'them is refused. Measured through spine-core: the runtime resolves links in the order they were read, ' +
5846
5846
  "so a link whose source is a link loads the source's geometry when the source comes first in the file " +
5847
5847
  'and loads NOTHING — `worldVerticesLength` 0, 0 triangles, a 0x0 size — when the two are swapped, in ' +
5848
- 'silence either way. Point "source" at the mesh itself.',
5848
+ 'silence either way. Point "source" at the mesh itself.', 'CE_COMPILE_LINKED_MESH_SOURCE_IS_LINK',
5849
5849
  );
5850
5850
  }
5851
5851
  if (type !== 'mesh') {
@@ -5853,7 +5853,7 @@ function resolveLinkedMeshes(
5853
5853
  `${link.where}: "source" is ${JSON.stringify(link.source)}, which is a ${JSON.stringify(type)} attachment ` +
5854
5854
  "and not a mesh. A linked mesh takes another MESH's `uvs`, `triangles`, `vertices`, `hull` and `edges` " +
5855
5855
  '(`MeshAttachment.setSourceMesh`); the runtime casts whatever it finds and reads those fields off it, ' +
5856
- 'which on any other type is `undefined` and no error.',
5856
+ 'which on any other type is `undefined` and no error.', 'CE_COMPILE_LINKED_MESH_SOURCE_NOT_MESH',
5857
5857
  );
5858
5858
  }
5859
5859
  }
@@ -5885,9 +5885,9 @@ function buildGeneratedMesh(
5885
5885
  // Two readings of one bone (issue #1021): the runtime's matrices to bind
5886
5886
  // through, and the exact frame to measure the control angles in.
5887
5887
  const resolve = (name: string, frames: Map<string, BoneTransform> = ctx.transforms): BoneTransform => {
5888
- if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: mesh bone "${name}" is not in the rig's bone list`);
5888
+ if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: mesh bone "${name}" is not in the rig's bone list`, 'CE_COMPILE_MESH_BONE_UNKNOWN');
5889
5889
  const transform = frames.get(name);
5890
- if (!transform) throw new CompileError(`${where}: no setup transform for mesh bone "${name}"`);
5890
+ if (!transform) throw new CompileError(`${where}: no setup transform for mesh bone "${name}"`, 'CE_COMPILE_MESH_BONE_TRANSFORM_MISSING');
5891
5891
  return transform;
5892
5892
  };
5893
5893
  const refFor = (name: string): MeshBoneRef => {
@@ -5901,7 +5901,7 @@ function buildGeneratedMesh(
5901
5901
  // angle about the aperture means anything (issue #684).
5902
5902
  const [w, h] = generator.size;
5903
5903
  const anchor = ctx.transforms.get(ctx.anchorBone);
5904
- if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
5904
+ if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
5905
5905
  const place = (px: number, py: number): [number, number] => [anchor.worldX + px - w / 2, anchor.worldY + h / 2 - py];
5906
5906
  // The inverse a control angle is measured through, in the exact frame: the
5907
5907
  // anchor's and the control's positions as the spec put them, not as the
@@ -5925,7 +5925,7 @@ function buildGeneratedMesh(
5925
5925
  }),
5926
5926
  });
5927
5927
  } catch (err) {
5928
- if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
5928
+ if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`, 'CE_COMPILE_RING_RIBBON_GENERATOR_INVALID');
5929
5929
  throw err;
5930
5930
  }
5931
5931
  const vertices = bindWeightedVertices(
@@ -6068,12 +6068,12 @@ function sampleMeshDepth(
6068
6068
  summary: NonNullable<CompileResult['meshes'][number]['depth']>;
6069
6069
  } {
6070
6070
  if (typeof spec.image !== 'string' || spec.image.length === 0) {
6071
- throw new CompileError(`${where}: the depth block has no "image"; it is the sheet to sample, relative to the rig's images directory`);
6071
+ throw new CompileError(`${where}: the depth block has no "image"; it is the sheet to sample, relative to the rig's images directory`, 'CE_COMPILE_DEPTH_IMAGE_MISSING');
6072
6072
  }
6073
6073
  if (spec.near !== 'white' && spec.near !== 'black') {
6074
6074
  throw new CompileError(
6075
6075
  `${where}: the depth block says "near": ${JSON.stringify(spec.near)}; it is "white" or "black". Both ` +
6076
- 'conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate green.',
6076
+ 'conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate green.', 'CE_COMPILE_DEPTH_NEAR_INVALID',
6077
6077
  );
6078
6078
  }
6079
6079
  const tone: DepthTone = {
@@ -6085,7 +6085,7 @@ function sampleMeshDepth(
6085
6085
  checkTone(tone, where);
6086
6086
  checkZScale(spec.zScale, where);
6087
6087
  } catch (err) {
6088
- if (err instanceof DepthError) throw new CompileError(err.message);
6088
+ if (err instanceof DepthError) throw new CompileError(err.message, 'CE_COMPILE_DEPTH_TONE_SCALE_INVALID');
6089
6089
  throw err;
6090
6090
  }
6091
6091
 
@@ -6157,7 +6157,7 @@ function sampleMeshDepth(
6157
6157
  : ` in the page's texels, pixel (${f32(points[first][0] * toSheetX)}, ${f32(points[first][1] * toSheetY)}) of ` +
6158
6158
  'the drawing-sized sheet') +
6159
6159
  `. ${why} ` +
6160
- 'Sampling anyway would give those vertices the sheet\'s background depth and fold them away from the turn.',
6160
+ 'Sampling anyway would give those vertices the sheet\'s background depth and fold them away from the turn.', 'CE_COMPILE_DEPTH_COVERAGE_INCOMPLETE',
6161
6161
  );
6162
6162
  }
6163
6163
 
@@ -6196,8 +6196,8 @@ function sampleMeshDepth(
6196
6196
  /** One bone of the rig as `bindWeightedVertices` wants it: by name, with its setup inverse. */
6197
6197
  function meshBoneRef(name: string, where: string, ctx: AttachmentContext): MeshBoneRef {
6198
6198
  const transform = ctx.transforms.get(name);
6199
- if (!transform) throw new CompileError(`${where}: bone "${name}" has no setup transform`);
6200
- if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: bone "${name}" is not in the rig's bone list`);
6199
+ if (!transform) throw new CompileError(`${where}: bone "${name}" has no setup transform`, 'CE_COMPILE_MESH_BONE_SETUP_TRANSFORM_MISSING');
6200
+ if (!ctx.bones.some((b) => b.name === name)) throw new CompileError(`${where}: bone "${name}" is not in the rig's bone list`, 'CE_COMPILE_MESH_BONE_NOT_DECLARED');
6201
6201
  return { name, toBind: (wx, wy) => toBoneLocal(transform, wx, wy) };
6202
6202
  }
6203
6203
 
@@ -6272,11 +6272,11 @@ function readGreySheet(
6272
6272
  const sheetHeight = scaled === undefined ? grid.partHeight : scaled.height;
6273
6273
  const noun = purpose === 'depth' ? 'depth map' : 'soft mask';
6274
6274
  if (typeof image !== 'string' || image.length === 0) {
6275
- throw new CompileError(`${where}: the "${purpose}" block has no image to read`);
6275
+ throw new CompileError(`${where}: the "${purpose}" block has no image to read`, 'CE_COMPILE_GREY_SHEET_IMAGE_MISSING');
6276
6276
  }
6277
6277
  const path = join(ctx.imagesDir, image);
6278
6278
  if (!existsSync(path)) {
6279
- throw new CompileError(`${where}: the ${noun} "${image}" is not at ${relative(process.cwd(), path)}`);
6279
+ throw new CompileError(`${where}: the ${noun} "${image}" is not at ${relative(process.cwd(), path)}`, 'CE_COMPILE_GREY_SHEET_IMAGE_NOT_FOUND');
6280
6280
  }
6281
6281
  const sheet = readPlate(path);
6282
6282
  if (sheet.width !== sheetWidth || sheet.height !== sheetHeight) {
@@ -6290,7 +6290,7 @@ function readGreySheet(
6290
6290
  `declares scale: ${scaled.scale}, which makes a texel ${f32(1 / scaled.scale)}px of the drawing. It is ` +
6291
6291
  "sampled in the part's own pixel grid, which is the drawing's, at each vertex's texel position over " +
6292
6292
  `that scale — so the sheet is ${scaled.width}x${scaled.height}, the size of the loose art, whatever the ` +
6293
- 'page holds. Resample the sheet, or point at the one that was made for this part.',
6293
+ 'page holds. Resample the sheet, or point at the one that was made for this part.', 'CE_COMPILE_GREY_SHEET_SIZE_MISMATCH',
6294
6294
  );
6295
6295
  }
6296
6296
  // One channel, proved rather than assumed.
@@ -6305,7 +6305,7 @@ function readGreySheet(
6305
6305
  throw new CompileError(
6306
6306
  `${where}: the ${noun} "${image}" is not greyscale — pixel (${i % sheet.width}, ${Math.floor(i / sheet.width)}) ` +
6307
6307
  `is rgb(${r}, ${g}, ${b}). It is one channel; reading red out of a colour file would use whatever that ` +
6308
- 'channel happened to hold.',
6308
+ 'channel happened to hold.', 'CE_COMPILE_GREY_SHEET_NOT_GREYSCALE',
6309
6309
  );
6310
6310
  }
6311
6311
  level[i] = r;
@@ -6339,20 +6339,20 @@ function softRegionWeights(
6339
6339
  ): { weights: MeshVertexWeight[][]; levels: number[]; bone: string; mask: string; digest: string; carried: number; ramped: number } {
6340
6340
  const { bone } = spec;
6341
6341
  if (typeof bone !== 'string' || bone.length === 0) {
6342
- throw new CompileError(`${where}: the "soft" block has no "bone"; it is the bone the soft region is carried by`);
6342
+ throw new CompileError(`${where}: the "soft" block has no "bone"; it is the bone the soft region is carried by`, 'CE_COMPILE_SOFT_BONE_MISSING');
6343
6343
  }
6344
6344
  if (!ctx.bones.some((b) => b.name === bone)) {
6345
6345
  throw new CompileError(
6346
6346
  `${where}: "soft" names bone "${bone}", which this rig does not declare. rigc binds a region to a bone that ` +
6347
6347
  'already exists rather than creating one — a bone a physics constraint has to target is part of the ' +
6348
- 'skeleton, not a side effect of a mesh.',
6348
+ 'skeleton, not a side effect of a mesh.', 'CE_COMPILE_SOFT_BONE_UNKNOWN',
6349
6349
  );
6350
6350
  }
6351
6351
  if (bone === ctx.anchorBone) {
6352
6352
  throw new CompileError(
6353
6353
  `${where}: "soft" names "${bone}", which is this slot's own bone. Carrying the region with the bone the rest ` +
6354
6354
  'of the mesh is already pinned to moves nothing — a soft region needs a bone that can move independently, ' +
6355
- 'which is what a physics constraint is put on.',
6355
+ 'which is what a physics constraint is put on.', 'CE_COMPILE_SOFT_BONE_IS_ANCHOR',
6356
6356
  );
6357
6357
  }
6358
6358
  const { map: sheet } = readGreySheet(spec.mask, 'soft', grid, where, ctx);
@@ -6377,7 +6377,7 @@ function softRegionWeights(
6377
6377
  throw new CompileError(
6378
6378
  `${where}: the soft mask "${spec.mask}" carries no vertex of this mesh — every one of its ${points.length} ` +
6379
6379
  'vertices samples black. A region that moves nothing is a physics constraint with nothing on the end of it; ' +
6380
- 'check that the mask is painted where the mesh actually is, and that it is white where the art is soft.',
6380
+ 'check that the mask is painted where the mesh actually is, and that it is white where the art is soft.', 'CE_COMPILE_SOFT_MASK_INERT',
6381
6381
  );
6382
6382
  }
6383
6383
  return { weights, levels, bone, mask: spec.mask, digest: depthDigest(sheet), carried, ramped };
@@ -6409,7 +6409,7 @@ function buildGridAttachment(
6409
6409
  if (att.image === undefined) {
6410
6410
  throw new CompileError(
6411
6411
  `${where}: a "grid" generator lays its lattice over the part's own window, so the attachment needs an ` +
6412
- '"image" — there is nothing else here that says how big that window is',
6412
+ '"image" — there is nothing else here that says how big that window is', 'CE_COMPILE_GRID_IMAGE_MISSING',
6413
6413
  );
6414
6414
  }
6415
6415
  const stated = generator.us !== undefined || generator.vs !== undefined;
@@ -6418,18 +6418,18 @@ function buildGridAttachment(
6418
6418
  throw new CompileError(
6419
6419
  `${where}: the "grid" generator states both positions ("us"/"vs") and a count ("cols"/"rows"). They are two ` +
6420
6420
  'answers to where the columns go, and a spec carrying both leaves a reader unable to say which one built ' +
6421
- 'the mesh. Drop one — positions are the more precise of the two.',
6421
+ 'the mesh. Drop one — positions are the more precise of the two.', 'CE_COMPILE_GRID_POSITIONS_AND_COUNT',
6422
6422
  );
6423
6423
  }
6424
6424
  if (!stated && !even) {
6425
6425
  throw new CompileError(
6426
6426
  `${where}: the "grid" generator says neither where its columns go ("us"/"vs", fractions of the window) nor ` +
6427
- 'how many there are ("cols"/"rows"). A lattice is not a default.',
6427
+ 'how many there are ("cols"/"rows"). A lattice is not a default.', 'CE_COMPILE_GRID_POSITION_AND_COUNT_MISSING',
6428
6428
  );
6429
6429
  }
6430
6430
  const spread = (n: unknown, name: string): number[] => {
6431
6431
  if (typeof n !== 'number' || !Number.isInteger(n) || n < 2) {
6432
- throw new CompileError(`${where}: "${name}" is ${JSON.stringify(n)}; it is a whole number of at least 2`);
6432
+ throw new CompileError(`${where}: "${name}" is ${JSON.stringify(n)}; it is a whole number of at least 2`, 'CE_COMPILE_GRID_COUNT_INVALID');
6433
6433
  }
6434
6434
  // An even division spans the whole window. A lattice that should sit inside
6435
6435
  // it states its own positions — which is what `us`/`vs` are for.
@@ -6440,7 +6440,7 @@ function buildGridAttachment(
6440
6440
  if (us === undefined || vs === undefined) {
6441
6441
  throw new CompileError(
6442
6442
  `${where}: the "grid" generator states ${us === undefined ? '"vs"' : '"us"'} and not ` +
6443
- `${us === undefined ? '"us"' : '"vs"'}; a lattice needs both axes`,
6443
+ `${us === undefined ? '"us"' : '"vs"'}; a lattice needs both axes`, 'CE_COMPILE_GRID_AXIS_MISSING',
6444
6444
  );
6445
6445
  }
6446
6446
 
@@ -6451,7 +6451,7 @@ function buildGridAttachment(
6451
6451
  try {
6452
6452
  geometry = buildGridMesh({ size: [plate.width, plate.height], us, vs });
6453
6453
  } catch (err) {
6454
- if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
6454
+ if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`, 'CE_COMPILE_GRID_GENERATOR_INVALID');
6455
6455
  throw err;
6456
6456
  }
6457
6457
 
@@ -6459,16 +6459,16 @@ function buildGridAttachment(
6459
6459
  const h = img.height;
6460
6460
  const toArt = w / plate.width;
6461
6461
  if (att.width !== undefined && att.width !== w) {
6462
- throw new CompileError(`${where}: the spec says width ${att.width} and "${att.image}" measures ${w}`);
6462
+ throw new CompileError(`${where}: the spec says width ${att.width} and "${att.image}" measures ${w}`, 'CE_COMPILE_GENERATED_IMAGE_WIDTH_MISMATCH');
6463
6463
  }
6464
6464
  if (att.height !== undefined && att.height !== h) {
6465
- throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
6465
+ throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`, 'CE_COMPILE_GENERATED_IMAGE_HEIGHT_MISMATCH');
6466
6466
  }
6467
6467
  const anchor = ctx.transforms.get(ctx.anchorBone);
6468
6468
  const frame = ctx.exactFrames.get(ctx.anchorBone);
6469
- if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
6469
+ if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
6470
6470
  if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
6471
- throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
6471
+ throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`, 'CE_COMPILE_SLOT_BONE_NOT_DECLARED');
6472
6472
  }
6473
6473
  // Sampled BEFORE the encode, because `bind` rewrites the weights the encode
6474
6474
  // then writes out.
@@ -6559,21 +6559,21 @@ function segmentsFalloffOf(f: RigSegmentsFalloff, where: string): SegmentsFallof
6559
6559
  if (typeof f.radius !== 'number' || !Number.isFinite(f.radius) || !(f.radius > 0)) {
6560
6560
  throw new CompileError(
6561
6561
  `${where}: "falloff.radius" is ${JSON.stringify(f.radius)}; it is added to every distance before the power is ` +
6562
- "taken, a positive number of the part's pixels — at 0 a vertex on a segment would carry an infinite pull",
6562
+ "taken, a positive number of the part's pixels — at 0 a vertex on a segment would carry an infinite pull", 'CE_COMPILE_SEGMENTS_FALLOFF_RADIUS_INVALID',
6563
6563
  );
6564
6564
  }
6565
6565
  const power = f.power ?? SEGMENTS_DEFAULTS.power;
6566
6566
  if (typeof power !== 'number' || !Number.isFinite(power) || !(power > 0)) {
6567
6567
  throw new CompileError(
6568
6568
  `${where}: "falloff.power" is ${JSON.stringify(power)}; it is the exponent a pull falls off with distance by, ` +
6569
- 'a positive number — at 0 or below the far segment pulls as hard as the near one, or harder',
6569
+ 'a positive number — at 0 or below the far segment pulls as hard as the near one, or harder', 'CE_COMPILE_SEGMENTS_FALLOFF_POWER_INVALID',
6570
6570
  );
6571
6571
  }
6572
6572
  const maxBones = f.maxBones ?? SEGMENTS_DEFAULTS.maxBones;
6573
6573
  if (typeof maxBones !== 'number' || !Number.isInteger(maxBones) || maxBones < 1) {
6574
6574
  throw new CompileError(
6575
6575
  `${where}: "falloff.maxBones" is ${JSON.stringify(maxBones)}; it is how many bones may pull one vertex, a ` +
6576
- 'whole number of at least 1',
6576
+ 'whole number of at least 1', 'CE_COMPILE_SEGMENTS_FALLOFF_MAX_BONES_INVALID',
6577
6577
  );
6578
6578
  }
6579
6579
  const minWeight = f.minWeight ?? SEGMENTS_DEFAULTS.minWeight;
@@ -6582,7 +6582,7 @@ function segmentsFalloffOf(f: RigSegmentsFalloff, where: string): SegmentsFallof
6582
6582
  `${where}: "falloff.minWeight" is ${JSON.stringify(minWeight)}; it is the share under which a bone's pull ` +
6583
6583
  `is dropped, so it lies in ${SEGMENTS_WEIGHT_STEP}..1 with 1 excluded — at 1 or above every share is dropped, ` +
6584
6584
  `and under ${SEGMENTS_WEIGHT_STEP}, one step of the 6-decimal grid weights are written on, a kept binding ` +
6585
- 'could be written as 0',
6585
+ 'could be written as 0', 'CE_COMPILE_SEGMENTS_FALLOFF_MIN_WEIGHT_INVALID',
6586
6586
  );
6587
6587
  }
6588
6588
  return { power, radius: f.radius, maxBones, minWeight };
@@ -6603,12 +6603,12 @@ function segmentsInfluenceOf(
6603
6603
  throw new CompileError(
6604
6604
  `${where}: the "segments" generator states both "falloff" and "influence". Both turn a distance into a ` +
6605
6605
  'share — "falloff" as 1 / (d + radius)^power over every segment, "influence" as a pull that reaches zero at ' +
6606
- 'its "radius" — and a mesh is weighted by one of them: remove the other',
6606
+ 'its "radius" — and a mesh is weighted by one of them: remove the other', 'CE_COMPILE_SEGMENTS_WEIGHT_MODE_CONFLICT',
6607
6607
  );
6608
6608
  }
6609
6609
  if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
6610
6610
  throw new CompileError(
6611
- `${where}: "influence" is ${JSON.stringify(spec)}; it is { "radius", "across", "max", "shape" }, every one required`,
6611
+ `${where}: "influence" is ${JSON.stringify(spec)}; it is { "radius", "across", "max", "shape" }, every one required`, 'CE_COMPILE_SEGMENTS_INFLUENCE_INVALID',
6612
6612
  );
6613
6613
  }
6614
6614
  const i = spec as Partial<Record<keyof SegmentsInfluence, unknown>>;
@@ -6616,20 +6616,20 @@ function segmentsInfluenceOf(
6616
6616
  throw new CompileError(
6617
6617
  `${where}: "influence.radius" is ${JSON.stringify(i.radius) ?? 'absent'}; it is how far a link reaches, in ` +
6618
6618
  "the part's pixels, a positive number with no default — a length on this art: a link whose segment is that " +
6619
- 'far from a vertex or farther never pulls it',
6619
+ 'far from a vertex or farther never pulls it', 'CE_COMPILE_SEGMENTS_INFLUENCE_RADIUS_INVALID',
6620
6620
  );
6621
6621
  }
6622
6622
  if (typeof i.across !== 'boolean') {
6623
6623
  throw new CompileError(
6624
6624
  `${where}: "influence.across" is ${JSON.stringify(i.across) ?? 'absent'}; it is true (every link within ` +
6625
6625
  '"radius" pulls, across the entries of "bones") or false (only the links of the entry nearest the vertex), ' +
6626
- "with no default — whether a chain may pull its neighbour's cloth is the decision this field is",
6626
+ "with no default — whether a chain may pull its neighbour's cloth is the decision this field is", 'CE_COMPILE_SEGMENTS_INFLUENCE_ACROSS_INVALID',
6627
6627
  );
6628
6628
  }
6629
6629
  if (typeof i.max !== 'number' || !Number.isInteger(i.max) || i.max < 1) {
6630
6630
  throw new CompileError(
6631
6631
  `${where}: "influence.max" is ${JSON.stringify(i.max) ?? 'absent'}; it is how many links may pull one vertex, ` +
6632
- 'the strongest kept, a whole number of at least 1 with no default',
6632
+ 'the strongest kept, a whole number of at least 1 with no default', 'CE_COMPILE_SEGMENTS_INFLUENCE_MAX_INVALID',
6633
6633
  );
6634
6634
  }
6635
6635
  const shape = RIG_INFLUENCE_SHAPES.find((name) => name === i.shape);
@@ -6637,7 +6637,7 @@ function segmentsInfluenceOf(
6637
6637
  throw new CompileError(
6638
6638
  `${where}: "influence.shape" is ${JSON.stringify(i.shape) ?? 'absent'}; one of ` +
6639
6639
  `${RIG_INFLUENCE_SHAPES.map((name) => JSON.stringify(name)).join(', ')} — how a pull falls from 1 on a link ` +
6640
- 'to 0 at "radius", with no default: "linear" is 1 - d/radius, "smooth" arrives at both ends with zero slope',
6640
+ 'to 0 at "radius", with no default: "linear" is 1 - d/radius, "smooth" arrives at both ends with zero slope', 'CE_COMPILE_SEGMENTS_INFLUENCE_SHAPE_INVALID',
6641
6641
  );
6642
6642
  }
6643
6643
  return { radius: i.radius, across: i.across, max: i.max, shape };
@@ -6671,7 +6671,7 @@ function buildSegmentsAttachment(
6671
6671
  if (att.image === undefined) {
6672
6672
  throw new CompileError(
6673
6673
  `${where}: a "segments" generator lays its lattice over the part's own alpha, so the attachment needs an ` +
6674
- '"image" — there is nothing else here that says which pixels are art',
6674
+ '"image" — there is nothing else here that says which pixels are art', 'CE_COMPILE_SEGMENTS_IMAGE_MISSING',
6675
6675
  );
6676
6676
  }
6677
6677
  const img = atlasedImage(att.image, where, ctx);
@@ -6679,7 +6679,7 @@ function buildSegmentsAttachment(
6679
6679
  throw new CompileError(
6680
6680
  `${where}: a "segments" generator lays its lattice over the part's own alpha, and "${att.image}" is lifted ` +
6681
6681
  'off a packed page at the coordinates the atlas states, which on this file are not where its texels are — ' +
6682
- `so there is no art here to cover, only another part of the page. ${img.pageGrid.sentence}`,
6682
+ `so there is no art here to cover, only another part of the page. ${img.pageGrid.sentence}`, 'CE_COMPILE_SEGMENTS_ATLAS_GRID_UNSUPPORTED',
6683
6683
  );
6684
6684
  }
6685
6685
  if (img.atlasScale !== undefined) {
@@ -6687,7 +6687,7 @@ function buildSegmentsAttachment(
6687
6687
  `${where}: a "segments" generator measures its "cell" and its falloff "radius" in the part's pixels, and ` +
6688
6688
  `"${att.image}" sits on a page that declares scale: ${img.atlasScale}, where a pixel of the drawing is not a ` +
6689
6689
  'texel of the page. This generator does not convert between the two; build it from the loose part, or ' +
6690
- 'from a pack at scale 1',
6690
+ 'from a pack at scale 1', 'CE_COMPILE_SEGMENTS_ATLAS_SCALE_UNSUPPORTED',
6691
6691
  );
6692
6692
  }
6693
6693
 
@@ -6699,7 +6699,7 @@ function buildSegmentsAttachment(
6699
6699
  `${where}: the "segments" generator states no "falloff" block. Its "radius" — added to every distance, in ` +
6700
6700
  "the part's pixels — has no default, because it is a length on this art: " +
6701
6701
  '"falloff": { "radius": <pixels> } is the least it takes (or an "influence" block, whose pull reaches zero ' +
6702
- 'at its own "radius")',
6702
+ 'at its own "radius")', 'CE_COMPILE_SEGMENTS_WEIGHT_MODE_MISSING',
6703
6703
  );
6704
6704
  }
6705
6705
  const falloff = influence === null ? segmentsFalloffOf(generator.falloff!, where) : null;
@@ -6715,16 +6715,16 @@ function buildSegmentsAttachment(
6715
6715
  cell: generator.cell,
6716
6716
  });
6717
6717
  } catch (err) {
6718
- if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
6718
+ if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`, 'CE_COMPILE_SEGMENTS_LATTICE_INVALID');
6719
6719
  throw err;
6720
6720
  }
6721
6721
  const w = img.width;
6722
6722
  const h = img.height;
6723
6723
  if (att.width !== undefined && att.width !== w) {
6724
- throw new CompileError(`${where}: the spec says width ${att.width} and "${att.image}" measures ${w}`);
6724
+ throw new CompileError(`${where}: the spec says width ${att.width} and "${att.image}" measures ${w}`, 'CE_COMPILE_GENERATED_IMAGE_WIDTH_MISMATCH');
6725
6725
  }
6726
6726
  if (att.height !== undefined && att.height !== h) {
6727
- throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
6727
+ throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`, 'CE_COMPILE_GENERATED_IMAGE_HEIGHT_MISMATCH');
6728
6728
  }
6729
6729
 
6730
6730
  // -- the placement ---------------------------------------------------------
@@ -6733,14 +6733,14 @@ function buildSegmentsAttachment(
6733
6733
  // MEASURED in — distances, falloff, `minWeight`, the shared-origin refusal —
6734
6734
  // and the runtime's is what the vertices are BOUND through (`place`).
6735
6735
  const exactSlotBone = ctx.exactFrames.get(ctx.anchorBone);
6736
- if (!slotBone || !exactSlotBone) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
6736
+ if (!slotBone || !exactSlotBone) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
6737
6737
  let anchorAt: [number, number] = [w / 2, h / 2];
6738
6738
  if (generator.anchor !== undefined) {
6739
6739
  const stated = finitePair(generator.anchor);
6740
6740
  if (stated === null) {
6741
6741
  throw new CompileError(
6742
6742
  `${where}: "anchor" is ${JSON.stringify(generator.anchor)}; it is where the slot bone sits in the part's ` +
6743
- 'pixels, y down, as [x, y] — two finite numbers',
6743
+ 'pixels, y down, as [x, y] — two finite numbers', 'CE_COMPILE_SEGMENTS_ANCHOR_INVALID',
6744
6744
  );
6745
6745
  }
6746
6746
  anchorAt = stated;
@@ -6758,7 +6758,7 @@ function buildSegmentsAttachment(
6758
6758
  throw new CompileError(
6759
6759
  `${where}: the "segments" generator names no bones. "bones" is the one decision this generator takes — which ` +
6760
6760
  'segments may pull the part — so it lists at least one: a bone name, a chain as a list of bone names, or ' +
6761
- '{ "bone", "from", "to" }',
6761
+ '{ "bone", "from", "to" }', 'CE_COMPILE_SEGMENTS_BONES_EMPTY',
6762
6762
  );
6763
6763
  }
6764
6764
  const parentOf = new Map(ctx.bones.map((b) => [b.name, b.parent ?? null]));
@@ -6767,7 +6767,7 @@ function buildSegmentsAttachment(
6767
6767
  const segments: EntrySegment[] = [];
6768
6768
  const boneOf = (name: unknown, at: string): { index: number; transform: BoneTransform } => {
6769
6769
  if (typeof name !== 'string' || !parentOf.has(name)) {
6770
- throw new CompileError(`${where}: ${at} names mesh bone ${JSON.stringify(name)}, which is not in the rig's bone list`);
6770
+ throw new CompileError(`${where}: ${at} names mesh bone ${JSON.stringify(name)}, which is not in the rig's bone list`, 'CE_COMPILE_SEGMENTS_BONE_UNKNOWN');
6771
6771
  }
6772
6772
  let index = named.indexOf(name);
6773
6773
  if (index < 0) {
@@ -6785,7 +6785,7 @@ function buildSegmentsAttachment(
6785
6785
  'so it has an origin and no segment. Give the bone a "length" (its segment runs from its origin to that ' +
6786
6786
  'tip, along its setup rotation), ' +
6787
6787
  (chained ? 'end the chain on a bone that has one, ' : 'name it inside a chain whose next link says where it ends, ') +
6788
- 'or state the segment outright as { "bone", "from", "to" }',
6788
+ 'or state the segment outright as { "bone", "from", "to" }', 'CE_COMPILE_SEGMENTS_BONE_LENGTH_MISSING',
6789
6789
  );
6790
6790
  }
6791
6791
  return toWorld(transform, length, 0);
@@ -6799,7 +6799,7 @@ function buildSegmentsAttachment(
6799
6799
  }
6800
6800
  if (Array.isArray(entry)) {
6801
6801
  if (entry.length === 0) {
6802
- throw new CompileError(`${where}: ${at} is an empty chain; a chain lists its bones root first, at least one`);
6802
+ throw new CompileError(`${where}: ${at} is an empty chain; a chain lists its bones root first, at least one`, 'CE_COMPILE_SEGMENTS_CHAIN_EMPTY');
6803
6803
  }
6804
6804
  const links = entry.map((name: unknown, k: number) => ({ name, ...boneOf(name, `${at}[${k}]`) }));
6805
6805
  links.forEach((link, k) => {
@@ -6817,7 +6817,7 @@ function buildSegmentsAttachment(
6817
6817
  throw new CompileError(
6818
6818
  `${where}: ${at} chains "${name}" to "${nextName}", and "${nextName}" is not under "${name}" in the bone ` +
6819
6819
  `tree — a chain runs down the skeleton, each link a descendant of the one before it. List the chain ` +
6820
- 'root first, or name the two bones as separate entries',
6820
+ 'root first, or name the two bones as separate entries', 'CE_COMPILE_SEGMENTS_CHAIN_ORDER_INVALID',
6821
6821
  );
6822
6822
  }
6823
6823
  const b: [number, number] = [next.transform.worldX, next.transform.worldY];
@@ -6825,7 +6825,7 @@ function buildSegmentsAttachment(
6825
6825
  throw new CompileError(
6826
6826
  `${where}: ${at} chains "${name}" to "${nextName}", and the two bones share their setup origin ` +
6827
6827
  `(${origin[0]}, ${origin[1]}), so the link has no length and no direction to weight along. Give ` +
6828
- `"${nextName}" its own position, or name "${name}" alone with a "length"`,
6828
+ `"${nextName}" its own position, or name "${name}" alone with a "length"`, 'CE_COMPILE_SEGMENTS_CHAIN_ZERO_LENGTH',
6829
6829
  );
6830
6830
  }
6831
6831
  segments.push({ bone: link.index, entry: i, a: origin, b });
@@ -6840,7 +6840,7 @@ function buildSegmentsAttachment(
6840
6840
  if (from === null || to === null) {
6841
6841
  throw new CompileError(
6842
6842
  `${where}: ${at} states a segment whose ${from === null ? '"from"' : '"to"'} is ` +
6843
- `${JSON.stringify(from === null ? span.from : span.to)}; both ends are [x, y] in the part's pixels, y down`,
6843
+ `${JSON.stringify(from === null ? span.from : span.to)}; both ends are [x, y] in the part's pixels, y down`, 'CE_COMPILE_SEGMENTS_ENDPOINT_INVALID',
6844
6844
  );
6845
6845
  }
6846
6846
  segments.push({ bone: index, entry: i, a: measuredAt(from[0], from[1]), b: measuredAt(to[0], to[1]) });
@@ -6848,7 +6848,7 @@ function buildSegmentsAttachment(
6848
6848
  }
6849
6849
  throw new CompileError(
6850
6850
  `${where}: ${at} is ${JSON.stringify(entry)}; an entry is a bone name, a chain as a list of bone names, or ` +
6851
- '{ "bone", "from", "to" }',
6851
+ '{ "bone", "from", "to" }', 'CE_COMPILE_SEGMENTS_ENTRY_INVALID',
6852
6852
  );
6853
6853
  });
6854
6854
 
@@ -6868,7 +6868,7 @@ function buildSegmentsAttachment(
6868
6868
  `"${named[reached.nearest.bone]}" at ${reached.nearest.distance.toFixed(4)}, and "influence.radius" is ` +
6869
6869
  `${influence.radius}${influence.across ? '' : ' (with "across": false, the nearest entry\'s links only)'}. ` +
6870
6870
  'A vertex needs a link, and rigc will not hand it one the reach does not: raise "radius" over that distance, ' +
6871
- 'or name a segment nearer this part of the art',
6871
+ 'or name a segment nearer this part of the art', 'CE_COMPILE_SEGMENTS_VERTEX_UNREACHED',
6872
6872
  );
6873
6873
  }
6874
6874
  const got = segmentShares(measuredAt(px, py), segments, falloff!);
@@ -6879,7 +6879,7 @@ function buildSegmentsAttachment(
6879
6879
  `strongest pull(s) share it as ${got.dropped.map((d) => `"${named[d.bone]}" ${d.share.toFixed(4)}`).join(', ')}, ` +
6880
6880
  `and every one is under "minWeight" ${minWeight}. A vertex needs a bone, and rigc will not pick one the ` +
6881
6881
  `falloff dropped: lower "minWeight" to ${(1 / maxBones).toFixed(4)} or under (1 / "maxBones", which the ` +
6882
- 'strongest share can never fall below), or name a segment nearer this part of the art',
6882
+ 'strongest share can never fall below), or name a segment nearer this part of the art', 'CE_COMPILE_SEGMENTS_VERTEX_WEIGHTS_DROPPED',
6883
6883
  );
6884
6884
  });
6885
6885
  const geometry: MeshGeometry = {
@@ -6984,7 +6984,7 @@ function buildContourAttachment(
6984
6984
  if (att.image === undefined) {
6985
6985
  throw new CompileError(
6986
6986
  `${where}: a "contour" generator traces the part's own alpha, so the attachment needs an "image" — ` +
6987
- 'there is nothing else here that says which pixels to measure',
6987
+ 'there is nothing else here that says which pixels to measure', 'CE_COMPILE_CONTOUR_IMAGE_MISSING',
6988
6988
  );
6989
6989
  }
6990
6990
  const img = atlasedImage(att.image, where, ctx);
@@ -7003,7 +7003,7 @@ function buildContourAttachment(
7003
7003
  throw new CompileError(
7004
7004
  `${where}: a "contour" generator traces the part's own alpha, and "${att.image}" is lifted off a packed ` +
7005
7005
  'page at the coordinates the atlas states, which on this file are not where its texels are — so there is ' +
7006
- `no silhouette here to trace, only another part of the page. ${img.pageGrid.sentence}`,
7006
+ `no silhouette here to trace, only another part of the page. ${img.pageGrid.sentence}`, 'CE_COMPILE_CONTOUR_ATLAS_GRID_UNSUPPORTED',
7007
7007
  );
7008
7008
  }
7009
7009
  // ⚠️ Nothing here reads the PNG's colour type. `hasAlpha` answers "where does
@@ -7031,7 +7031,7 @@ function buildContourAttachment(
7031
7031
  ...(img.atlasScale === undefined ? {} : { pageScale: img.atlasScale }),
7032
7032
  });
7033
7033
  } catch (err) {
7034
- if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
7034
+ if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`, 'CE_COMPILE_CONTOUR_GENERATOR_INVALID');
7035
7035
  throw err;
7036
7036
  }
7037
7037
 
@@ -7046,16 +7046,16 @@ function buildContourAttachment(
7046
7046
  const h = img.height;
7047
7047
  const toArt = w / plate.width;
7048
7048
  if (att.width !== undefined && att.width !== w) {
7049
- throw new CompileError(`${where}: the spec says width ${att.width} and "${att.image}" measures ${w}`);
7049
+ throw new CompileError(`${where}: the spec says width ${att.width} and "${att.image}" measures ${w}`, 'CE_COMPILE_GENERATED_IMAGE_WIDTH_MISMATCH');
7050
7050
  }
7051
7051
  if (att.height !== undefined && att.height !== h) {
7052
- throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`);
7052
+ throw new CompileError(`${where}: the spec says height ${att.height} and "${att.image}" measures ${h}`, 'CE_COMPILE_GENERATED_IMAGE_HEIGHT_MISMATCH');
7053
7053
  }
7054
7054
  const anchor = ctx.transforms.get(ctx.anchorBone);
7055
7055
  const frame = ctx.exactFrames.get(ctx.anchorBone);
7056
- if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`);
7056
+ if (!anchor || !frame) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
7057
7057
  if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
7058
- throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`);
7058
+ throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" is not in the rig's bone list`, 'CE_COMPILE_SLOT_BONE_NOT_DECLARED');
7059
7059
  }
7060
7060
  const vertices = bindWeightedVertices(
7061
7061
  geometry,
@@ -7169,7 +7169,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7169
7169
  const boneNames = ctx.boneNames;
7170
7170
  const needBone = (name: unknown, field: string): string => {
7171
7171
  if (typeof name !== 'string' || !boneNames.has(name)) {
7172
- throw new CompileError(`${where}: ${field} names ${JSON.stringify(name)}, which the rig does not declare as a bone`);
7172
+ throw new CompileError(`${where}: ${field} names ${JSON.stringify(name)}, which the rig does not declare as a bone`, 'CE_COMPILE_CONSTRAINT_BONE_UNKNOWN');
7173
7173
  }
7174
7174
  return name;
7175
7175
  };
@@ -7186,14 +7186,14 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7186
7186
  throw new CompileError(
7187
7187
  `${where}: ${field} is ${JSON.stringify(value)}; known: ${allowed.join(', ')} (only the first letter's case is ` +
7188
7188
  "free — the parser's enumValue uppercases that one character and nothing else, and an unresolved name " +
7189
- 'becomes undefined without an error)',
7189
+ 'becomes undefined without an error)', 'CE_COMPILE_CONSTRAINT_ENUM_UNKNOWN',
7190
7190
  );
7191
7191
  }
7192
7192
  return value;
7193
7193
  };
7194
7194
  const needNumber = (value: unknown, field: string): number => {
7195
7195
  if (typeof value !== 'number' || !Number.isFinite(value)) {
7196
- throw new CompileError(`${where}: ${field} is ${JSON.stringify(value)}, which is not a finite number`);
7196
+ throw new CompileError(`${where}: ${field} is ${JSON.stringify(value)}, which is not a finite number`, 'CE_COMPILE_CONSTRAINT_NUMBER_NONFINITE');
7197
7197
  }
7198
7198
  return value;
7199
7199
  };
@@ -7211,7 +7211,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7211
7211
  const boneList = (): string[] => {
7212
7212
  const list = spec.bones;
7213
7213
  if (!Array.isArray(list) || list.length === 0) {
7214
- throw new CompileError(`${where}: a ${spec.type} constraint needs a non-empty "bones" array`);
7214
+ throw new CompileError(`${where}: a ${spec.type} constraint needs a non-empty "bones" array`, 'CE_COMPILE_CONSTRAINT_BONES_EMPTY');
7215
7215
  }
7216
7216
  return list.map((name, i) => needBone(name, `bones[${i}]`));
7217
7217
  };
@@ -7236,11 +7236,11 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7236
7236
  // places in this format that does not fail silently — but it throws at load
7237
7237
  // time, in the consumer's process, and that is late.
7238
7238
  if (!TRANSFORM_PROPERTIES.includes(from)) {
7239
- throw new CompileError(`${where}: properties has "${from}"; known: ${TRANSFORM_PROPERTIES.join(', ')}`);
7239
+ throw new CompileError(`${where}: properties has "${from}"; known: ${TRANSFORM_PROPERTIES.join(', ')}`, 'CE_COMPILE_TRANSFORM_PROPERTY_UNKNOWN');
7240
7240
  }
7241
7241
  for (const to of Object.keys(entry?.to ?? {})) {
7242
7242
  if (!TRANSFORM_PROPERTIES.includes(to)) {
7243
- throw new CompileError(`${where}: properties.${from}.to has "${to}"; known: ${TRANSFORM_PROPERTIES.join(', ')}`);
7243
+ throw new CompileError(`${where}: properties.${from}.to has "${to}"; known: ${TRANSFORM_PROPERTIES.join(', ')}`, 'CE_COMPILE_TRANSFORM_TO_PROPERTY_UNKNOWN');
7244
7244
  }
7245
7245
  }
7246
7246
  }
@@ -7272,7 +7272,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7272
7272
  // loud, but in the consumer's process. The silent half is the one below it.
7273
7273
  const slot = spec.slot;
7274
7274
  if (typeof slot !== 'string' || !ctx.slotNames.has(slot)) {
7275
- throw new CompileError(`${where}: slot names ${JSON.stringify(slot)}, which the rig does not declare as a slot`);
7275
+ throw new CompileError(`${where}: slot names ${JSON.stringify(slot)}, which the rig does not declare as a slot`, 'CE_COMPILE_CONSTRAINT_SLOT_UNKNOWN');
7276
7276
  }
7277
7277
  if (!ctx.pathSlots.has(slot)) {
7278
7278
  // `PathConstraint.update` opens with
@@ -7283,7 +7283,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7283
7283
  `${where}: slot "${slot}" has no path attachment in any skin, so the constraint has no curve to follow — ` +
7284
7284
  'PathConstraint.update returns immediately unless the slot\'s attachment is a path, which means this ' +
7285
7285
  'constraint would load, report every mix it was given, and move nothing. ' +
7286
- 'Give that slot an attachment with "type": "path".',
7286
+ 'Give that slot an attachment with "type": "path".', 'CE_COMPILE_PATH_CONSTRAINT_SLOT_NO_PATH',
7287
7287
  );
7288
7288
  }
7289
7289
  out.slot = slot;
@@ -7309,14 +7309,14 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7309
7309
  if (typeof animation !== 'string' || animation.length === 0) {
7310
7310
  throw new CompileError(
7311
7311
  `${where}: a slider needs an "animation" — the animation it applies. Without one the parser's second pass ` +
7312
- 'over the constraints array throws `Slider animation not found`.',
7312
+ 'over the constraints array throws `Slider animation not found`.', 'CE_COMPILE_SLIDER_ANIMATION_MISSING',
7313
7313
  );
7314
7314
  }
7315
7315
  if (!ctx.animationNames.has(animation)) {
7316
7316
  const known = [...ctx.animationNames];
7317
7317
  throw new CompileError(
7318
7318
  `${where}: applies animation "${animation}", which the motion spec does not declare` +
7319
- (known.length ? ` (it declares: ${known.join(', ')})` : ' (it declares none at all)'),
7319
+ (known.length ? ` (it declares: ${known.join(', ')})` : ' (it declares none at all)'), 'CE_COMPILE_SLIDER_ANIMATION_UNKNOWN',
7320
7320
  );
7321
7321
  }
7322
7322
  // 🔁 The editor keeps a slider's animation by index and re-sorts the object
@@ -7331,7 +7331,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7331
7331
  `lists every array-index key (${spelled} here) before the rest, the editor re-sorts the animations into its ` +
7332
7332
  'own order on import, and a slider holds its animation by index (measured on Spine 4.3.26; AUTHORING R10). ' +
7333
7333
  `Rename ${spelled} so no animation name is a plain non-negative integer — a prefix or a leading zero ` +
7334
- `("a${repointed.indexNames[0]}", "0${repointed.indexNames[0]}") puts the file's order and the editor's in step.`,
7334
+ `("a${repointed.indexNames[0]}", "0${repointed.indexNames[0]}") puts the file's order and the editor's in step.`, 'CE_COMPILE_SLIDER_ANIMATION_EDITOR_ORDER',
7335
7335
  );
7336
7336
  }
7337
7337
  out.animation = animation;
@@ -7351,7 +7351,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7351
7351
  if (typeof property !== 'string' || !RIG_FROM_PROPERTIES.includes(property as (typeof RIG_FROM_PROPERTIES)[number])) {
7352
7352
  throw new CompileError(
7353
7353
  `${where}: drives off bone "${String(spec.bone)}" but its property is ${JSON.stringify(property)}; ` +
7354
- `known: ${RIG_FROM_PROPERTIES.join(', ')} (the parser throws on anything else)`,
7354
+ `known: ${RIG_FROM_PROPERTIES.join(', ')} (the parser throws on anything else)`, 'CE_COMPILE_SLIDER_BONE_PROPERTY_UNKNOWN',
7355
7355
  );
7356
7356
  }
7357
7357
  out.property = property;
@@ -7361,7 +7361,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7361
7361
  if (spec.local !== undefined) out.local = spec.local;
7362
7362
  if (spec.scale !== undefined && spec.scale === 0) {
7363
7363
  // time = to + (value - from) * 0, so the slider holds one frame forever.
7364
- throw new CompileError(`${where}: scale is 0, so the bone's property cannot move the slider's time at all`);
7364
+ throw new CompileError(`${where}: scale is 0, so the bone's property cannot move the slider's time at all`, 'CE_COMPILE_SLIDER_SCALE_ZERO');
7365
7365
  }
7366
7366
  // The window of driving values that can reach a frame at all, and the two
7367
7367
  // functions every figure in the two reader clauses below comes off.
@@ -7608,7 +7608,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7608
7608
  `at ${dead.end.toFixed(3)}° is read as ${readAs.toFixed(3)}° and maps to time ${lands.toFixed(3)}s. ` +
7609
7609
  `${consequence} The whole part of the range ${dead.side} is dead and ` +
7610
7610
  'nothing at runtime reports it. Add `"local": true` to read the bone\'s own rotation signed and unwrapped — ' +
7611
- `that is the form a face axis wants — or ${dead.repair}.`,
7611
+ `that is the form a face axis wants — or ${dead.repair}.`, 'CE_COMPILE_SLIDER_WORLD_ROTATION_DEAD_RANGE',
7612
7612
  );
7613
7613
  }
7614
7614
  }
@@ -7681,14 +7681,14 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7681
7681
  `${lowest.toFixed(3)} is read as ${readAs.toFixed(3)} and maps to time ${lands.toFixed(3)}s — the time the ` +
7682
7682
  `bone at ${readAs.toFixed(3)} maps to. Positions below 0 read as the mirror of positions above it: ` +
7683
7683
  `${consequence} Nothing at runtime reports it. Add \`"local": true\` to read the bone's own scale signed and ` +
7684
- 'unfolded — that is the form a squash axis wants — or move the range so it does not dip below 0.',
7684
+ 'unfolded — that is the form a squash axis wants — or move the range so it does not dip below 0.', 'CE_COMPILE_SLIDER_WORLD_SCALE_FOLDED',
7685
7685
  );
7686
7686
  }
7687
7687
  for (const field of timeSide) {
7688
7688
  if (spec[field] !== undefined) {
7689
7689
  throw new CompileError(
7690
7690
  `${where}: declares both a "bone" and "${field}". A slider with a bone takes its time from that bone's ` +
7691
- `property; "${field}" is read only by the bone-less form (\`:361\`), so it would be dropped in silence.`,
7691
+ `property; "${field}" is read only by the bone-less form (\`:361\`), so it would be dropped in silence.`, 'CE_COMPILE_SLIDER_BONELESS_FIELD_WITH_BONE',
7692
7692
  );
7693
7693
  }
7694
7694
  }
@@ -7699,7 +7699,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7699
7699
  throw new CompileError(
7700
7700
  `${where}: declares "${field}" but no "bone". Every one of ${boneSide.join('/')} is read only inside the ` +
7701
7701
  "parser's `if (boneName)` branch (`:350-360`), so it would be dropped in silence. Name the driving bone, " +
7702
- 'or key `slider.<name>.time` in the motion spec instead.',
7702
+ 'or key `slider.<name>.time` in the motion spec instead.', 'CE_COMPILE_SLIDER_BONE_FIELD_WITHOUT_BONE',
7703
7703
  );
7704
7704
  }
7705
7705
  }
@@ -7744,7 +7744,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7744
7744
  throw new NotImplementedError(
7745
7745
  `${where}: constraint type ${JSON.stringify(spec.type)} is not one Spine 4.3 knows. ` +
7746
7746
  'The five are: ik, transform, path, physics, slider. An unrecognised type matches no case in the parser and ' +
7747
- 'the constraint is dropped without a word.',
7747
+ 'the constraint is dropped without a word.', 'CE_COMPILE_CONSTRAINT_TYPE_UNKNOWN',
7748
7748
  );
7749
7749
  }
7750
7750
 
@@ -7766,7 +7766,7 @@ function buildRigInfo(
7766
7766
  ): RigInfo {
7767
7767
  const axisBone = rig.invariants?.axisBone ?? null;
7768
7768
  if (axisBone !== null && !bones.some((b) => b.name === axisBone)) {
7769
- throw new CompileError(`rig "${rig.name}" names "${axisBone}" as its axis bone, which it does not declare`);
7769
+ throw new CompileError(`rig "${rig.name}" names "${axisBone}" as its axis bone, which it does not declare`, 'CE_COMPILE_AXIS_BONE_UNKNOWN');
7770
7770
  }
7771
7771
  const axisSubtree: string[] = [];
7772
7772
  if (axisBone) {
@@ -7794,7 +7794,7 @@ function buildRigInfo(
7794
7794
  if (meshKinds[slot] === undefined) {
7795
7795
  throw new CompileError(
7796
7796
  `rig "${rig.name}" exempts slot "${slot}" from A39_DEFORM_KEEPS_TRIANGLE_WINDING, but that slot carries no ` +
7797
- 'mesh — winding is a property of triangles, so there is nothing there to exempt',
7797
+ 'mesh — winding is a property of triangles, so there is nothing there to exempt', 'CE_COMPILE_WINDING_EXEMPTION_NOT_MESH',
7798
7798
  );
7799
7799
  }
7800
7800
  }
@@ -7807,12 +7807,12 @@ function buildRigInfo(
7807
7807
  spineDeg === null ? null : [Math.cos((spineDeg * Math.PI) / 180), Math.sin((spineDeg * Math.PI) / 180)];
7808
7808
  const contactDepth = manifest?.stroke?.contact_depth ?? null;
7809
7809
  if (contactDepth !== null && !(contactDepth > 0)) {
7810
- throw new CompileError(`manifest stroke.contact_depth is ${contactDepth}; it must be a positive number of axis pixels`);
7810
+ throw new CompileError(`manifest stroke.contact_depth is ${contactDepth}; it must be a positive number of axis pixels`, 'CE_COMPILE_CONTACT_DEPTH_INVALID');
7811
7811
  }
7812
7812
  const capCeiling = manifest?.stroke?.cap_containment_ceiling ?? null;
7813
7813
  if (capCeiling !== null && !(capCeiling > 0)) {
7814
7814
  throw new CompileError(
7815
- `manifest stroke.cap_containment_ceiling is ${capCeiling}; it must be a positive number of axis pixels (use null for "not measurable on this cut")`,
7815
+ `manifest stroke.cap_containment_ceiling is ${capCeiling}; it must be a positive number of axis pixels (use null for "not measurable on this cut")`, 'CE_COMPILE_CONTAINMENT_CEILING_INVALID',
7816
7816
  );
7817
7817
  }
7818
7818
  return {
@@ -7851,13 +7851,13 @@ function checkAxisSelfConsistency(manifest: FaceManifest): void {
7851
7851
  if (!manifest.axis) return;
7852
7852
  const { deg, unit } = manifest.axis;
7853
7853
  if (!Array.isArray(unit) || unit.length !== 2) {
7854
- throw new CompileError(`manifest axis.unit must be [x, y], got ${JSON.stringify(unit)}`);
7854
+ throw new CompileError(`manifest axis.unit must be [x, y], got ${JSON.stringify(unit)}`, 'CE_COMPILE_AXIS_UNIT_INVALID');
7855
7855
  }
7856
7856
  const ex = Math.cos((deg * Math.PI) / 180);
7857
7857
  const ey = Math.sin((deg * Math.PI) / 180);
7858
7858
  if (hypot2(unit[0] - ex, unit[1] - ey) > 1e-3) {
7859
7859
  throw new CompileError(
7860
- `manifest axis.unit [${unit[0]}, ${unit[1]}] does not match axis.deg ${deg} (expected [${f32(ex)}, ${f32(ey)}])`,
7860
+ `manifest axis.unit [${unit[0]}, ${unit[1]}] does not match axis.deg ${deg} (expected [${f32(ex)}, ${f32(ey)}])`, 'CE_COMPILE_AXIS_UNIT_DEG_MISMATCH',
7861
7861
  );
7862
7862
  }
7863
7863
  }
@@ -7916,9 +7916,9 @@ function buildMesh(
7916
7916
  const controls = meshControlBones(part);
7917
7917
 
7918
7918
  const refFor = (name: string): MeshBoneRef => {
7919
- if (!bones.some((b) => b.name === name)) throw new CompileError(`internal: mesh bone "${name}" is not in the bone list`);
7919
+ if (!bones.some((b) => b.name === name)) throw new CompileError(`internal: mesh bone "${name}" is not in the bone list`, 'CE_COMPILE_INTERNAL_MESH_BONE_UNKNOWN');
7920
7920
  const m = transforms.get(name);
7921
- if (!m) throw new CompileError(`internal: no setup transform for mesh bone "${name}"`);
7921
+ if (!m) throw new CompileError(`internal: no setup transform for mesh bone "${name}"`, 'CE_COMPILE_INTERNAL_MESH_BONE_TRANSFORM_MISSING');
7922
7922
  return { name, toBind: (wx, wy) => toBoneLocal(m, wx, wy) };
7923
7923
  };
7924
7924
 
@@ -7945,7 +7945,7 @@ function buildMesh(
7945
7945
  // bits turned an authored 90 into 89.99999734089101 in that refusal.
7946
7946
  const controlAngles = ringControlAngles(controls, spec.center!, (name) => {
7947
7947
  const m = exactFrames.get(name);
7948
- if (!m) throw new CompileError(`internal: no setup transform for control bone "${name}"`);
7948
+ if (!m) throw new CompileError(`internal: no setup transform for control bone "${name}"`, 'CE_COMPILE_INTERNAL_CONTROL_BONE_TRANSFORM_MISSING');
7949
7949
  return [m.worldX, cropH - m.worldY];
7950
7950
  });
7951
7951
  geometry = buildRingMesh({
@@ -7958,7 +7958,7 @@ function buildMesh(
7958
7958
  });
7959
7959
  }
7960
7960
  } catch (err) {
7961
- if (err instanceof MeshError) throw new CompileError(`slot "${part.slot}" mesh: ${err.message}`);
7961
+ if (err instanceof MeshError) throw new CompileError(`slot "${part.slot}" mesh: ${err.message}`, 'CE_COMPILE_MANIFEST_RING_RIBBON_INVALID');
7962
7962
  throw err;
7963
7963
  }
7964
7964
 
@@ -8009,17 +8009,17 @@ function buildMesh(
8009
8009
  */
8010
8010
  function rawCurve(curve: number[] | 'stepped', channels: number, where: string, at: string): number[] | 'stepped' {
8011
8011
  if (curve === 'stepped') return 'stepped';
8012
- if (!Array.isArray(curve)) throw new CompileError(`${where} (t=${at}): curve must be an array or "stepped"`);
8012
+ if (!Array.isArray(curve)) throw new CompileError(`${where} (t=${at}): curve must be an array or "stepped"`, 'CE_COMPILE_CURVE_NOT_ARRAY');
8013
8013
  if (curve.length !== channels * 4) {
8014
8014
  // A short array multiplies `undefined` into the cubic and yields NaN with no
8015
8015
  // error at all — case 6g, and the reason A05 exists.
8016
8016
  throw new CompileError(
8017
- `${where} (t=${at}): raw curve has ${curve.length} numbers, this timeline needs ${channels} channel(s) x 4 = ${channels * 4}`,
8017
+ `${where} (t=${at}): raw curve has ${curve.length} numbers, this timeline needs ${channels} channel(s) x 4 = ${channels * 4}`, 'CE_COMPILE_CURVE_CHANNEL_COUNT',
8018
8018
  );
8019
8019
  }
8020
8020
  for (const n of curve) {
8021
8021
  if (typeof n !== 'number' || !Number.isFinite(n)) {
8022
- throw new CompileError(`${where} (t=${at}): raw curve holds a non-finite value ${JSON.stringify(n)}`);
8022
+ throw new CompileError(`${where} (t=${at}): raw curve holds a non-finite value ${JSON.stringify(n)}`, 'CE_COMPILE_CURVE_VALUE_NONFINITE');
8023
8023
  }
8024
8024
  }
8025
8025
  return curve.map(f32);
@@ -8043,9 +8043,9 @@ function compileValueTrack(
8043
8043
  kind: string,
8044
8044
  ): ModelKey[] {
8045
8045
  const shape = shapes[track.property];
8046
- if (!shape) throw new CompileError(`animation "${animName}": ${kind} "${target}" has no property "${track.property}"`);
8046
+ if (!shape) throw new CompileError(`animation "${animName}": ${kind} "${target}" has no property "${track.property}"`, 'CE_COMPILE_VALUE_TIMELINE_PROPERTY_UNKNOWN');
8047
8047
  const where = `animation "${animName}" ${kind} "${target}" ${track.property}`;
8048
- if (!track.keys.length) throw new CompileError(`${where}: no keys`);
8048
+ if (!track.keys.length) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8049
8049
 
8050
8050
  const out: ModelKey[] = [];
8051
8051
  for (let i = 0; i < track.keys.length; i++) {
@@ -8053,7 +8053,7 @@ function compileValueTrack(
8053
8053
  const next = track.keys[i + 1];
8054
8054
  const time = keyTime(key.t + shift);
8055
8055
  if (i > 0 && time <= (out[i - 1].time as number)) {
8056
- throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
8056
+ throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`, 'CE_COMPILE_KEY_TIMES_NOT_INCREASING');
8057
8057
  }
8058
8058
  checkKeyTime(where, time, key.t, duration);
8059
8059
  // A named-value timeline (`inherit`): the key's `v` is one name, stepped by
@@ -8066,12 +8066,12 @@ function compileValueTrack(
8066
8066
  throw new CompileError(
8067
8067
  `${where} key at t=${key.t} carries ${key.ease !== undefined ? `an easing ("${key.ease}")` : 'a curve'}, and ` +
8068
8068
  `this timeline is stepped by the format — its reader builds no curve, so the mode changes AT the key and ` +
8069
- 'holds until the next one. Remove it',
8069
+ 'holds until the next one. Remove it', 'CE_COMPILE_STEPPED_TIMELINE_EASING',
8070
8070
  );
8071
8071
  }
8072
8072
  const mode = shape.named.resolve(key.v);
8073
8073
  if (mode === undefined) {
8074
- throw new CompileError(`${where} key at t=${key.t} names mode ${JSON.stringify(key.v)}; ${shape.named.known}`);
8074
+ throw new CompileError(`${where} key at t=${key.t} names mode ${JSON.stringify(key.v)}; ${shape.named.known}`, 'CE_COMPILE_TIMELINE_MODE_UNKNOWN');
8075
8075
  }
8076
8076
  out.push({ time, [shape.named.field]: mode });
8077
8077
  continue;
@@ -8079,18 +8079,18 @@ function compileValueTrack(
8079
8079
  // A no-field timeline (`reset`) is an event: the key IS the value, so it
8080
8080
  // carries none. Anything else must match the field count exactly.
8081
8081
  if (shape.fields.length === 0) {
8082
- if (key.v !== null) throw new CompileError(`${where}: this timeline takes no value; use null`);
8083
- if (key.ease) throw new CompileError(`${where}: an event timeline cannot carry an easing`);
8082
+ if (key.v !== null) throw new CompileError(`${where}: this timeline takes no value; use null`, 'CE_COMPILE_VALUELESS_TIMELINE_VALUE');
8083
+ if (key.ease) throw new CompileError(`${where}: an event timeline cannot carry an easing`, 'CE_COMPILE_EVENT_TIMELINE_EASING');
8084
8084
  out.push({ time });
8085
8085
  continue;
8086
8086
  }
8087
8087
  if (!Array.isArray(key.v) || key.v.length !== shape.fields.length) {
8088
- throw new CompileError(`${where}: key value must be an array of ${shape.fields.length} number(s)`);
8088
+ throw new CompileError(`${where}: key value must be an array of ${shape.fields.length} number(s)`, 'CE_COMPILE_TIMELINE_VALUE_CHANNEL_COUNT');
8089
8089
  }
8090
8090
  const entry: ModelKey = { time };
8091
8091
  shape.fields.forEach((field, c) => {
8092
8092
  const v = key.v as number[];
8093
- if (!Number.isFinite(v[c])) throw new CompileError(`${where}: non-finite value ${String(v[c])}`);
8093
+ if (!Number.isFinite(v[c])) throw new CompileError(`${where}: non-finite value ${String(v[c])}`, 'CE_COMPILE_TIMELINE_VALUE_NONFINITE');
8094
8094
  // The bound, where the runtime has one. Refused here rather than left to
8095
8095
  // `A23` because this is a spec somebody wrote and the key is the thing to
8096
8096
  // change — the same division `compileConstraintTrack`'s `range` makes for
@@ -8106,28 +8106,28 @@ function compileValueTrack(
8106
8106
  // reads "… mass: value is 0", which says the property twice and the
8107
8107
  // second time under the wrong name.
8108
8108
  const at = shape.fields.length > 1 ? `${field} at t=${key.t}` : `key at t=${key.t}`;
8109
- if (refusal !== null) throw new CompileError(`${where} ${at} is ${refusal}`);
8109
+ if (refusal !== null) throw new CompileError(`${where} ${at} is ${refusal}`, 'CE_COMPILE_PHYSICS_KEY_RANGE_INVALID');
8110
8110
  }
8111
8111
  entry[field] = f32(v[c]);
8112
8112
  });
8113
8113
  if (key.ease !== undefined && key.curve !== undefined) {
8114
- throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
8114
+ throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`, 'CE_COMPILE_KEY_EASING_AND_CURVE');
8115
8115
  }
8116
8116
  if (key.curve !== undefined) {
8117
- if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`);
8117
+ if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_CURVE');
8118
8118
  entry.curve = rawCurve(key.curve, shape.fields.length, where, String(key.t));
8119
8119
  } else if (key.ease && next) {
8120
8120
  if (key.ease === 'stepped') {
8121
8121
  entry.curve = 'stepped';
8122
8122
  } else {
8123
8123
  const handles = motion.easings?.[key.ease];
8124
- if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`);
8125
- if (!Array.isArray(next.v)) throw new CompileError(`${where}: next key value must be an array`);
8124
+ if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`, 'CE_COMPILE_KEY_EASING_UNKNOWN');
8125
+ if (!Array.isArray(next.v)) throw new CompileError(`${where}: next key value must be an array`, 'CE_COMPILE_NEXT_KEY_VALUE_NOT_ARRAY');
8126
8126
  const t2 = keyTime(next.t + shift);
8127
8127
  entry.curve = easingCurve(handles, time, t2, key.v as number[], next.v as number[]);
8128
8128
  }
8129
8129
  } else if (key.ease && !next) {
8130
- throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`);
8130
+ throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_EASING');
8131
8131
  }
8132
8132
  out.push(entry);
8133
8133
  }
@@ -8176,7 +8176,7 @@ function compileDrawOrder(
8176
8176
  slots: readonly ModelSlot[],
8177
8177
  ): ModelKey[] {
8178
8178
  const where = `animation "${animName}" drawOrder`;
8179
- if (!keys.length) throw new CompileError(`${where}: no keys`);
8179
+ if (!keys.length) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8180
8180
  const indexOf = new Map(slots.map((s, i) => [s.name, i]));
8181
8181
 
8182
8182
  const out: ModelKey[] = [];
@@ -8184,7 +8184,7 @@ function compileDrawOrder(
8184
8184
  const key = keys[i];
8185
8185
  const time = keyTime(key.t);
8186
8186
  if (i > 0 && time <= (out[i - 1].time as number)) {
8187
- throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
8187
+ throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`, 'CE_COMPILE_KEY_TIMES_NOT_INCREASING');
8188
8188
  }
8189
8189
  checkKeyTime(where, time, key.t, duration);
8190
8190
  // No offsets = "back to the setup draw order", which is the parser's own
@@ -8199,19 +8199,19 @@ function compileDrawOrder(
8199
8199
  for (const off of key.offsets) {
8200
8200
  const index = indexOf.get(off.slot);
8201
8201
  if (index === undefined) {
8202
- throw new CompileError(`${where} at t=${key.t}: slot "${off.slot}" is not one this rig emits`);
8202
+ throw new CompileError(`${where} at t=${key.t}: slot "${off.slot}" is not one this rig emits`, 'CE_COMPILE_DRAW_ORDER_SLOT_UNKNOWN');
8203
8203
  }
8204
8204
  if (seen.has(off.slot)) {
8205
- throw new CompileError(`${where} at t=${key.t}: slot "${off.slot}" is offset twice in one key`);
8205
+ throw new CompileError(`${where} at t=${key.t}: slot "${off.slot}" is offset twice in one key`, 'CE_COMPILE_DRAW_ORDER_SLOT_DUPLICATE');
8206
8206
  }
8207
8207
  if (!Number.isInteger(off.offset)) {
8208
- throw new CompileError(`${where} at t=${key.t}: slot "${off.slot}" offset ${off.offset} is not a whole number`);
8208
+ throw new CompileError(`${where} at t=${key.t}: slot "${off.slot}" offset ${off.offset} is not a whole number`, 'CE_COMPILE_DRAW_ORDER_OFFSET_NONINTEGER');
8209
8209
  }
8210
8210
  const landing = index + off.offset;
8211
8211
  if (landing < 0 || landing >= slots.length) {
8212
8212
  throw new CompileError(
8213
8213
  `${where} at t=${key.t}: slot "${off.slot}" is at index ${index} and offset ${off.offset} puts it at ` +
8214
- `${landing}, outside the ${slots.length} emitted slots`,
8214
+ `${landing}, outside the ${slots.length} emitted slots`, 'CE_COMPILE_DRAW_ORDER_OFFSET_OUTSIDE_SLOTS',
8215
8215
  );
8216
8216
  }
8217
8217
  seen.add(off.slot);
@@ -8256,27 +8256,27 @@ function compileEvents(
8256
8256
  events: Record<string, RigEvent>,
8257
8257
  ): ModelKey[] {
8258
8258
  const where = `animation "${animName}" events`;
8259
- if (!keys.length) throw new CompileError(`${where}: no keys`);
8259
+ if (!keys.length) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8260
8260
 
8261
8261
  const out: ModelKey[] = [];
8262
8262
  for (let i = 0; i < keys.length; i++) {
8263
8263
  const key = keys[i];
8264
8264
  if (typeof key.name !== 'string' || key.name.length === 0) {
8265
- throw new CompileError(`${where}: key ${i} has no "name"; an event key fires an event by name`);
8265
+ throw new CompileError(`${where}: key ${i} has no "name"; an event key fires an event by name`, 'CE_COMPILE_EVENT_KEY_NAME_MISSING');
8266
8266
  }
8267
8267
  const declared = events[key.name];
8268
8268
  if (declared === undefined) {
8269
8269
  const known = Object.keys(events);
8270
8270
  throw new CompileError(
8271
8271
  `${where} at t=${key.t}: event "${key.name}" is not declared in the rig spec's "events" block; ` +
8272
- (known.length ? `declared: ${known.join(', ')}` : 'that block is empty or absent'),
8272
+ (known.length ? `declared: ${known.join(', ')}` : 'that block is empty or absent'), 'CE_COMPILE_EVENT_KEY_EVENT_UNKNOWN',
8273
8273
  );
8274
8274
  }
8275
8275
  const time = keyTime(key.t);
8276
8276
  if (i > 0 && time < (out[i - 1].time as number)) {
8277
8277
  throw new CompileError(
8278
8278
  `${where}: key times must not go backwards (at t=${key.t}, after t=${String(out[i - 1].time)}) — ` +
8279
- 'the parser writes frames in array order and never sorts them',
8279
+ 'the parser writes frames in array order and never sorts them', 'CE_COMPILE_EVENT_KEY_TIMES_BACKWARDS',
8280
8280
  );
8281
8281
  }
8282
8282
  checkKeyTime(where, time, key.t, duration);
@@ -8284,19 +8284,19 @@ function compileEvents(
8284
8284
  const entry: ModelKey = { time, name: key.name };
8285
8285
  if (key.int !== undefined) {
8286
8286
  if (!Number.isInteger(key.int)) {
8287
- throw new CompileError(`${where} at t=${key.t}: int ${String(key.int)} is not an integer`);
8287
+ throw new CompileError(`${where} at t=${key.t}: int ${String(key.int)} is not an integer`, 'CE_COMPILE_EVENT_KEY_INT_INVALID');
8288
8288
  }
8289
8289
  entry.int = key.int;
8290
8290
  }
8291
8291
  if (key.float !== undefined) {
8292
8292
  if (!Number.isFinite(key.float)) {
8293
- throw new CompileError(`${where} at t=${key.t}: float ${String(key.float)} is not finite`);
8293
+ throw new CompileError(`${where} at t=${key.t}: float ${String(key.float)} is not finite`, 'CE_COMPILE_EVENT_KEY_FLOAT_NONFINITE');
8294
8294
  }
8295
8295
  entry.float = f32(key.float);
8296
8296
  }
8297
8297
  if (key.string !== undefined) {
8298
8298
  if (typeof key.string !== 'string') {
8299
- throw new CompileError(`${where} at t=${key.t}: string ${JSON.stringify(key.string)} is not a string`);
8299
+ throw new CompileError(`${where} at t=${key.t}: string ${JSON.stringify(key.string)} is not a string`, 'CE_COMPILE_EVENT_KEY_STRING_INVALID');
8300
8300
  }
8301
8301
  entry.string = key.string;
8302
8302
  }
@@ -8306,10 +8306,10 @@ function compileEvents(
8306
8306
  if (declared.audio === undefined) {
8307
8307
  throw new CompileError(
8308
8308
  `${where} at t=${key.t}: ${field} is set but event "${key.name}" declares no "audio"; ` +
8309
- `the parser reads ${field} only for an event with an audio path, so it would be dropped in silence`,
8309
+ `the parser reads ${field} only for an event with an audio path, so it would be dropped in silence`, 'CE_COMPILE_EVENT_AUDIO_OVERRIDE_NO_AUDIO',
8310
8310
  );
8311
8311
  }
8312
- if (!Number.isFinite(v)) throw new CompileError(`${where} at t=${key.t}: ${field} ${String(v)} is not finite`);
8312
+ if (!Number.isFinite(v)) throw new CompileError(`${where} at t=${key.t}: ${field} ${String(v)} is not finite`, 'CE_COMPILE_EVENT_AUDIO_NUMBER_NONFINITE');
8313
8313
  entry[field] = f32(v);
8314
8314
  }
8315
8315
  out.push(entry);
@@ -8378,7 +8378,7 @@ function compileConstraintTrack(
8378
8378
  // An array by the time this runs (`parseMotionSpec`); an EMPTY one is a
8379
8379
  // format rule rather than a shape — the parser reads key 0, finds nothing and
8380
8380
  // skips the timeline, which is assertion A34's silent case.
8381
- if (keys.length === 0) throw new CompileError(`${where}: no keys`);
8381
+ if (keys.length === 0) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8382
8382
 
8383
8383
  const read = (key: MotionIkTrack['keys'][number] | MotionTransformTrack['keys'][number], field: string): unknown =>
8384
8384
  (key as unknown as Record<string, unknown>)[field];
@@ -8397,7 +8397,7 @@ function compileConstraintTrack(
8397
8397
  throw new CompileError(
8398
8398
  `${where}: key ${i} (t=${key.t}) names "${field}" and key 0 does not. Every key of ${article} ${group} timeline ` +
8399
8399
  'is read with its own default, so a field stated on some keys and not others snaps to the default on ' +
8400
- 'the rest — state it on every key or on none.',
8400
+ 'the rest — state it on every key or on none.', 'CE_COMPILE_CONSTRAINT_KEY_FIELD_ADDED',
8401
8401
  );
8402
8402
  }
8403
8403
  for (const field of first) {
@@ -8405,7 +8405,7 @@ function compileConstraintTrack(
8405
8405
  throw new CompileError(
8406
8406
  `${where}: key 0 names "${field}" and key ${i} (t=${key.t}) does not. Every key of ${article} ${group} timeline ` +
8407
8407
  `is read with its own default, so "${field}" would snap to ` +
8408
- `${JSON.stringify(defaultOf(shape, field))} at t=${key.t} — state it on every key or on none.`,
8408
+ `${JSON.stringify(defaultOf(shape, field))} at t=${key.t} — state it on every key or on none.`, 'CE_COMPILE_CONSTRAINT_KEY_FIELD_OMITTED',
8409
8409
  );
8410
8410
  }
8411
8411
  });
@@ -8425,7 +8425,7 @@ function compileConstraintTrack(
8425
8425
  const next = keys[i + 1];
8426
8426
  const time = keyTime(key.t);
8427
8427
  if (i > 0 && time <= (out[i - 1].time as number)) {
8428
- throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
8428
+ throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`, 'CE_COMPILE_KEY_TIMES_NOT_INCREASING');
8429
8429
  }
8430
8430
  checkKeyTime(where, time, key.t, duration);
8431
8431
 
@@ -8434,14 +8434,14 @@ function compileConstraintTrack(
8434
8434
  const v = read(key, channel.field);
8435
8435
  if (v === undefined) continue;
8436
8436
  if (typeof v !== 'number' || !Number.isFinite(v)) {
8437
- throw new CompileError(`${where} (t=${key.t}): ${channel.field} is ${JSON.stringify(v)}, not a finite number`);
8437
+ throw new CompileError(`${where} (t=${key.t}): ${channel.field} is ${JSON.stringify(v)}, not a finite number`, 'CE_COMPILE_CONSTRAINT_KEY_NUMBER_NONFINITE');
8438
8438
  }
8439
8439
  const bounds = shape.range[channel.field];
8440
8440
  if (bounds && (v < bounds[0] || v > bounds[1])) {
8441
8441
  throw new CompileError(
8442
8442
  `${where} (t=${key.t}): ${channel.field} is ${v}, outside ${bounds[0]}..${
8443
8443
  bounds[1] === Infinity ? '∞' : bounds[1]
8444
- } — the runtime documents it as ${channel.field === 'mix' ? 'a percentage 0-1' : 'a distance'}`,
8444
+ } — the runtime documents it as ${channel.field === 'mix' ? 'a percentage 0-1' : 'a distance'}`, 'CE_COMPILE_CONSTRAINT_KEY_RANGE_INVALID',
8445
8445
  );
8446
8446
  }
8447
8447
  entry[channel.field] = f32(v);
@@ -8458,23 +8458,23 @@ function compileConstraintTrack(
8458
8458
  continue;
8459
8459
  }
8460
8460
  if (typeof v !== 'boolean') {
8461
- throw new CompileError(`${where} (t=${key.t}): ${flag.field} is ${JSON.stringify(v)}, not true or false`);
8461
+ throw new CompileError(`${where} (t=${key.t}): ${flag.field} is ${JSON.stringify(v)}, not true or false`, 'CE_COMPILE_CONSTRAINT_KEY_FLAG_NOT_BOOLEAN');
8462
8462
  }
8463
8463
  entry[flag.field] = v;
8464
8464
  }
8465
8465
 
8466
8466
  if (key.ease !== undefined && key.curve !== undefined) {
8467
- throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
8467
+ throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`, 'CE_COMPILE_KEY_EASING_AND_CURVE');
8468
8468
  }
8469
8469
  if (key.curve !== undefined) {
8470
- if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`);
8470
+ if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_CURVE');
8471
8471
  entry.curve = rawCurve(key.curve, shape.channels.length, where, String(key.t));
8472
8472
  } else if (key.ease !== undefined && next) {
8473
8473
  if (key.ease === 'stepped') {
8474
8474
  entry.curve = 'stepped';
8475
8475
  } else {
8476
8476
  const handles = motion.easings?.[key.ease];
8477
- if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`);
8477
+ if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`, 'CE_COMPILE_KEY_EASING_UNKNOWN');
8478
8478
  const t2 = keyTime(next.t);
8479
8479
  entry.curve = easingCurve(
8480
8480
  handles,
@@ -8485,7 +8485,7 @@ function compileConstraintTrack(
8485
8485
  );
8486
8486
  }
8487
8487
  } else if (key.ease !== undefined && !next) {
8488
- throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`);
8488
+ throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_EASING');
8489
8489
  }
8490
8490
  out.push(entry);
8491
8491
  }
@@ -8599,7 +8599,7 @@ export function deformGeometryOf(
8599
8599
  if (!isModelVertexAttachment(att)) {
8600
8600
  throw new CompileError(
8601
8601
  `${where}: a deform timeline keys the vertices of an attachment, and this one is a "${type}" — ` +
8602
- 'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping, path.',
8602
+ 'it has no vertex array to deform. Deformable types: mesh, boundingbox, clipping, path.', 'CE_COMPILE_DEFORM_ATTACHMENT_TYPE_INVALID',
8603
8603
  );
8604
8604
  }
8605
8605
  if (att.kind === 'mesh') {
@@ -8666,7 +8666,7 @@ export function deformGeometryOf(
8666
8666
  const vertex = vertices.bindings[v];
8667
8667
  const n = vertex.length;
8668
8668
  if (n < 1) {
8669
- throw new CompileError(`${where}: the attachment's weighted vertex run has a bone count of ${String(n)} at index ${at}`);
8669
+ throw new CompileError(`${where}: the attachment's weighted vertex run has a bone count of ${String(n)} at index ${at}`, 'CE_COMPILE_DEFORM_WEIGHT_BONE_COUNT_INVALID');
8670
8670
  }
8671
8671
  if (n === 1) {
8672
8672
  bones.add(vertex[0].bone);
@@ -8727,7 +8727,7 @@ export function deformGeometryOf(
8727
8727
  let sum = 0;
8728
8728
  for (const binding of vertices.bindings[v]) {
8729
8729
  const m = transforms.get(binding.bone);
8730
- if (!m) throw new CompileError(`${where}: vertex ${v} binds bone "${binding.bone}", which has no setup transform`);
8730
+ if (!m) throw new CompileError(`${where}: vertex ${v} binds bone "${binding.bone}", which has no setup transform`, 'CE_COMPILE_VERTEX_BIND_TRANSFORM_MISSING');
8731
8731
  const weight = binding.weight;
8732
8732
  const [x, y] = toWorld(m, binding.x, binding.y);
8733
8733
  wx += x * weight;
@@ -8834,7 +8834,7 @@ function compileSequenceTrack(
8834
8834
  `${where}: attachment "${track.attachment}" carries no "sequence" block, so there is no series to step. The ` +
8835
8835
  'parser gives every region and mesh a series of ONE region (`readSequence(null)` is `new Sequence(1, ' +
8836
8836
  'false)`), so this timeline would load and show that region under every mode at every time. Give the ' +
8837
- 'attachment a "sequence" in the rig spec, or remove the track.',
8837
+ 'attachment a "sequence" in the rig spec, or remove the track.', 'CE_COMPILE_SEQUENCE_ATTACHMENT_NO_SERIES',
8838
8838
  );
8839
8839
  }
8840
8840
  if (attachment.kind === 'linkedmesh' && attachment.timelines) {
@@ -8844,23 +8844,23 @@ function compileSequenceTrack(
8844
8844
  `is not false), so its \`timelineAttachment\` is "${source}" (\`SkeletonJson.js:437-448\`) and ` +
8845
8845
  '`SequenceTimeline` applies only where the slot\'s attachment\'s `timelineAttachment` is the one it was ' +
8846
8846
  `built for — a key aimed at the link is applied to nothing. Key "${source}" instead (the link steps its own ` +
8847
- 'series by it), or set "timelines": false on the link.',
8847
+ 'series by it), or set "timelines": false on the link.', 'CE_COMPILE_SEQUENCE_LINK_TIMELINE_INERT',
8848
8848
  );
8849
8849
  }
8850
- if (track.keys.length === 0) throw new CompileError(`${where}: no keys`);
8850
+ if (track.keys.length === 0) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8851
8851
  const out: ModelKey[] = [];
8852
8852
  for (let i = 0; i < track.keys.length; i++) {
8853
8853
  const key = track.keys[i];
8854
8854
  const time = keyTime(key.t);
8855
8855
  if (i > 0 && time <= (out[i - 1].time as number)) {
8856
- throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
8856
+ throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`, 'CE_COMPILE_KEY_TIMES_NOT_INCREASING');
8857
8857
  }
8858
8858
  checkKeyTime(where, time, key.t, duration);
8859
8859
  if (key.index !== undefined && key.index >= series.count) {
8860
8860
  throw new CompileError(
8861
8861
  `${where} (t=${key.t}): index ${key.index} is past the end of a ${series.count}-frame series (frames 0 to ` +
8862
8862
  `${series.count - 1}). \`Sequence.resolveIndex\` clamps it to the last frame, so the key would start on a ` +
8863
- 'frame it does not name. `index` is 0-based.',
8863
+ 'frame it does not name. `index` is 0-based.', 'CE_COMPILE_SEQUENCE_KEY_INDEX_OUTSIDE_SERIES',
8864
8864
  );
8865
8865
  }
8866
8866
  const entry: ModelKey = { time };
@@ -8886,14 +8886,14 @@ function compileDeformTrack(
8886
8886
  // An array by the time this runs (`parseMotionSpec`); an EMPTY one is a
8887
8887
  // format rule rather than a shape — the parser reads key 0, finds nothing and
8888
8888
  // skips the timeline, which is assertion A34's silent case.
8889
- if (keys.length === 0) throw new CompileError(`${where}: no keys`);
8889
+ if (keys.length === 0) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8890
8890
 
8891
8891
  const out: ModelKey[] = [];
8892
8892
  for (let i = 0; i < keys.length; i++) {
8893
8893
  const key = keys[i];
8894
8894
  const time = keyTime(key.t);
8895
8895
  if (i > 0 && time <= (out[i - 1].time as number)) {
8896
- throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
8896
+ throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`, 'CE_COMPILE_KEY_TIMES_NOT_INCREASING');
8897
8897
  }
8898
8898
  checkKeyTime(where, time, key.t, duration);
8899
8899
  // -- a key that states a MODEL rather than a run (issue #294) ------------
@@ -8910,7 +8910,7 @@ function compileDeformTrack(
8910
8910
  throw new CompileError(
8911
8911
  `${where} (t=${key.t}): the key carries both a "transform" and a "vertices" run, and they are two answers to ` +
8912
8912
  'one question. Authored offsets and a stated model do not combine, for the same reason a mesh cannot carry ' +
8913
- 'both a "generator" and authored geometry — drop one.',
8913
+ 'both a "generator" and authored geometry — drop one.', 'CE_COMPILE_DEFORM_TRANSFORM_AND_VERTICES',
8914
8914
  );
8915
8915
  }
8916
8916
  if (key.offset !== undefined || key.fromVertex !== undefined) {
@@ -8918,14 +8918,14 @@ function compileDeformTrack(
8918
8918
  `${where} (t=${key.t}): the key states a "transform" and a start index. A transform is a model of the whole ` +
8919
8919
  `attachment and is evaluated over all ${geometry.vertexCount} of its vertices, so it always starts at deform ` +
8920
8920
  'index 0. A model applied to part of a run leaves a step at the run\'s edge — write the partial run by hand ' +
8921
- 'if that is what you mean.',
8921
+ 'if that is what you mean.', 'CE_COMPILE_DEFORM_TRANSFORM_START_INDEX',
8922
8922
  );
8923
8923
  }
8924
8924
  if (geometry.setup === null) {
8925
8925
  throw new CompileError(
8926
8926
  `${where} (t=${key.t}): a "transform" states where every vertex of this attachment goes, and this one has a ` +
8927
8927
  `vertex the arithmetic cannot place — ${geometry.setupWhy}. Fix the weights, or write the bind-space pairs ` +
8928
- 'yourself and start the run with "offset".',
8928
+ 'yourself and start the run with "offset".', 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED',
8929
8929
  );
8930
8930
  }
8931
8931
  const report = evaluateDeformTransform(
@@ -8942,7 +8942,7 @@ function compileDeformTrack(
8942
8942
  // assumed: a silent mismatch here is the overrun A35 exists for.
8943
8943
  throw new CompileError(
8944
8944
  `${where} (t=${key.t}): the transform produced ${report.offsets.length} numbers for ` +
8945
- `${geometry.vertexCount} vertices`,
8945
+ `${geometry.vertexCount} vertices`, 'CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT',
8946
8946
  );
8947
8947
  }
8948
8948
  // -- one displacement per vertex -> one pair per influence (issue #389) -
@@ -8958,7 +8958,7 @@ function compileDeformTrack(
8958
8958
  if (run.length !== geometry.deformLength) {
8959
8959
  throw new CompileError(
8960
8960
  `${where} (t=${key.t}): the transform expanded to ${run.length} numbers and this attachment's deform ` +
8961
- `array is ${geometry.deformLength} long`,
8961
+ `array is ${geometry.deformLength} long`, 'CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH',
8962
8962
  );
8963
8963
  }
8964
8964
  generated.push({
@@ -8992,7 +8992,7 @@ function compileDeformTrack(
8992
8992
  if (key.offset !== undefined && key.fromVertex !== undefined) {
8993
8993
  throw new CompileError(
8994
8994
  `${where} (t=${key.t}): a key gives its start as "offset" (an index into the deform array) or as ` +
8995
- '"fromVertex" (a vertex index rigc translates), never both',
8995
+ '"fromVertex" (a vertex index rigc translates), never both', 'CE_COMPILE_DEFORM_START_SOURCES_CONFLICT',
8996
8996
  );
8997
8997
  }
8998
8998
  const run = key.vertices ?? null;
@@ -9003,7 +9003,7 @@ function compileDeformTrack(
9003
9003
  if (key.offset !== undefined || key.fromVertex !== undefined) {
9004
9004
  throw new CompileError(
9005
9005
  `${where} (t=${key.t}): the key has no "vertices", which is the format's way of saying "back to the ` +
9006
- 'setup pose" — so there is nothing for a start index to point at. Drop the offset, or give it a run.',
9006
+ 'setup pose" — so there is nothing for a start index to point at. Drop the offset, or give it a run.', 'CE_COMPILE_DEFORM_SETUP_KEY_START_INDEX',
9007
9007
  );
9008
9008
  }
9009
9009
  out.push({ time });
@@ -9012,7 +9012,7 @@ function compileDeformTrack(
9012
9012
  if (!Array.isArray(run) || run.length === 0) {
9013
9013
  throw new CompileError(
9014
9014
  `${where} (t=${key.t}): "vertices" is ${JSON.stringify(key.vertices)}; give an array of x, y offsets, ` +
9015
- 'or omit it entirely for "back to the setup pose"',
9015
+ 'or omit it entirely for "back to the setup pose"', 'CE_COMPILE_DEFORM_VERTICES_NOT_ARRAY',
9016
9016
  );
9017
9017
  }
9018
9018
  // ⛔ No parity clause here, and its absence is the rule (issue #576).
@@ -9042,7 +9042,7 @@ function compileDeformTrack(
9042
9042
  // format holds, and the half that refused was the authoring half.
9043
9043
  for (const n of run) {
9044
9044
  if (typeof n !== 'number' || !Number.isFinite(n)) {
9045
- throw new CompileError(`${where} (t=${key.t}): "vertices" holds a non-finite value ${JSON.stringify(n)}`);
9045
+ throw new CompileError(`${where} (t=${key.t}): "vertices" holds a non-finite value ${JSON.stringify(n)}`, 'CE_COMPILE_DEFORM_VERTEX_VALUE_NONFINITE');
9046
9046
  }
9047
9047
  }
9048
9048
  const start = deformStart(key, run.length, geometry, where);
@@ -9051,7 +9051,7 @@ function compileDeformTrack(
9051
9051
  `${where} (t=${key.t}): the run starts at deform index ${start} and is ${run.length} long, which ends at ` +
9052
9052
  `${start + run.length}; this attachment's deform array is ${geometry.deformLength} long ` +
9053
9053
  `(${geometry.weighted ? `${geometry.deformLength / 2} bone influences` : `${geometry.vertexCount} vertices`}). ` +
9054
- 'The parser copies into a Float32Array, so everything past the end is dropped without a word.',
9054
+ 'The parser copies into a Float32Array, so everything past the end is dropped without a word.', 'CE_COMPILE_DEFORM_RUN_OUTSIDE_ATTACHMENT',
9055
9055
  );
9056
9056
  }
9057
9057
  const entry: ModelKey = { time };
@@ -9122,7 +9122,7 @@ function deformStart(
9122
9122
  if (key.offset !== undefined) {
9123
9123
  if (!Number.isInteger(key.offset) || key.offset < 0) {
9124
9124
  throw new CompileError(
9125
- `${where} (t=${key.t}): offset is ${JSON.stringify(key.offset)}; it is an index into the deform array, so a whole number ≥ 0`,
9125
+ `${where} (t=${key.t}): offset is ${JSON.stringify(key.offset)}; it is an index into the deform array, so a whole number ≥ 0`, 'CE_COMPILE_DEFORM_OFFSET_INVALID',
9126
9126
  );
9127
9127
  }
9128
9128
  // ⛔ An odd `offset` is not refused either, and it went the same way as the
@@ -9140,7 +9140,7 @@ function deformStart(
9140
9140
  if (key.fromVertex === undefined) return 0;
9141
9141
  const from = key.fromVertex;
9142
9142
  if (!Number.isInteger(from) || from < 0) {
9143
- throw new CompileError(`${where} (t=${key.t}): fromVertex is ${JSON.stringify(from)}; it is a vertex index, so a whole number ≥ 0`);
9143
+ throw new CompileError(`${where} (t=${key.t}): fromVertex is ${JSON.stringify(from)}; it is a vertex index, so a whole number ≥ 0`, 'CE_COMPILE_DEFORM_FROM_VERTEX_INVALID');
9144
9144
  }
9145
9145
  // ⌈⌉ rather than ÷, because an odd run REACHES a last vertex without covering
9146
9146
  // it: its final number is that vertex's x and the y beside it stays at setup
@@ -9151,7 +9151,7 @@ function deformStart(
9151
9151
  if (from + covered > geometry.vertexCount) {
9152
9152
  throw new CompileError(
9153
9153
  `${where} (t=${key.t}): fromVertex ${from} plus ${covered} vertex offset(s) runs to vertex ${from + covered}, ` +
9154
- `and the attachment has ${geometry.vertexCount}`,
9154
+ `and the attachment has ${geometry.vertexCount}`, 'CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT',
9155
9155
  );
9156
9156
  }
9157
9157
  if (!geometry.weighted) return from * 2;
@@ -9164,7 +9164,7 @@ function deformStart(
9164
9164
  'is not a thing that array can hold: the world offset of a multi-bone vertex is the weighted sum of a ' +
9165
9165
  'per-bone offset in each bone\'s own bind space, so rigc will not guess one for you. Either key the ' +
9166
9166
  'control bone instead, or write the bind-space pairs yourself and start the run with "offset" ' +
9167
- `(vertex ${from} starts at deform index ${2 * counts.slice(0, from).reduce((a, b) => a + b, 0)}).`,
9167
+ `(vertex ${from} starts at deform index ${2 * counts.slice(0, from).reduce((a, b) => a + b, 0)}).`, 'CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS',
9168
9168
  );
9169
9169
  }
9170
9170
  let start = 0;
@@ -9192,21 +9192,21 @@ function deformKeyCurve(
9192
9192
  next: ModelKey | undefined,
9193
9193
  ): ModelKey {
9194
9194
  if (key.ease !== undefined && key.curve !== undefined) {
9195
- throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
9195
+ throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`, 'CE_COMPILE_KEY_EASING_AND_CURVE');
9196
9196
  }
9197
9197
  if (key.curve !== undefined) {
9198
- if (next === undefined) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`);
9198
+ if (next === undefined) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_CURVE');
9199
9199
  entry.curve = rawCurve(key.curve, 1, where, String(key.t));
9200
9200
  return entry;
9201
9201
  }
9202
9202
  if (key.ease === undefined) return entry;
9203
- if (next === undefined) throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`);
9203
+ if (next === undefined) throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_EASING');
9204
9204
  if (key.ease === 'stepped') {
9205
9205
  entry.curve = 'stepped';
9206
9206
  return entry;
9207
9207
  }
9208
9208
  const handles = motion.easings?.[key.ease];
9209
- if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`);
9209
+ if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`, 'CE_COMPILE_KEY_EASING_UNKNOWN');
9210
9210
  // The one channel runs 0 -> 1; the VALUE being held is the geometry, so the
9211
9211
  // hold test compares the two keys' runs rather than those two constants.
9212
9212
  entry.curve = easingCurve(handles, entry.time as number, keyTime(keys[index + 1].t), [0], [1], sameDeform(entry, next));
@@ -9242,7 +9242,7 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9242
9242
  if (named.length > 1) {
9243
9243
  throw new CompileError(
9244
9244
  `animation "${animName}": a track names more than one target (${named.join(', ')}); the list is ` +
9245
- `${targetFields.join('/')} and a track names exactly one`,
9245
+ `${targetFields.join('/')} and a track names exactly one`, 'CE_COMPILE_TRACK_TARGETS_MULTIPLE',
9246
9246
  );
9247
9247
  }
9248
9248
  const family = constraintFamilyOf(track);
@@ -9252,12 +9252,12 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9252
9252
  const who = direct === undefined ? `group "${String(track.group)}"` : `${label} "${direct}"`;
9253
9253
  if (!(track.property in tracks)) {
9254
9254
  throw new CompileError(
9255
- `animation "${animName}": ${who} has no timeline "${track.property}" (it has: ${Object.keys(tracks).join(', ')})`,
9255
+ `animation "${animName}": ${who} has no timeline "${track.property}" (it has: ${Object.keys(tracks).join(', ')})`, 'CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN',
9256
9256
  );
9257
9257
  }
9258
9258
  if (direct !== undefined) return [direct];
9259
9259
  const members = motion.groups?.[String(track.group)];
9260
- if (!members) throw new CompileError(`animation "${animName}": unknown group "${String(track.group)}"`);
9260
+ if (!members) throw new CompileError(`animation "${animName}": unknown group "${String(track.group)}"`, 'CE_COMPILE_ANIMATION_GROUP_UNKNOWN');
9261
9261
  return members;
9262
9262
  }
9263
9263
  // A constraint timeline with no constraint named. Worth its own refusal: the
@@ -9268,12 +9268,12 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9268
9268
  if (owning.length) {
9269
9269
  throw new CompileError(
9270
9270
  `animation "${animName}": "${track.property}" is a ${owning.map((f) => CONSTRAINT_TRACK_FAMILIES[f].label).join(' / ')} ` +
9271
- `timeline, and this track names no constraint — put the name in "${owning.join('" or "')}"`,
9271
+ `timeline, and this track names no constraint — put the name in "${owning.join('" or "')}"`, 'CE_COMPILE_CONSTRAINT_TRACK_TARGET_MISSING',
9272
9272
  );
9273
9273
  }
9274
9274
  const isBoneTrack = track.property in BONE_TRACKS;
9275
9275
  if (isBoneTrack && !track.bone && !track.group) {
9276
- throw new CompileError(`animation "${animName}": "${track.property}" is a bone track but no bone is named`);
9276
+ throw new CompileError(`animation "${animName}": "${track.property}" is a bone track but no bone is named`, 'CE_COMPILE_BONE_TRACK_TARGET_MISSING');
9277
9277
  }
9278
9278
  if (!isBoneTrack && track.bone) {
9279
9279
  // The bone family's sibling of `compileTrack`'s slot refusal, raised here for
@@ -9288,7 +9288,7 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9288
9288
  throw new CompileError(
9289
9289
  `animation "${animName}" bone "${track.bone}" has no timeline "${track.property}" ` +
9290
9290
  `(it has: ${Object.keys(BONE_TRACKS).join(', ')})` +
9291
- (track.property in SLOT_TRACKS ? `. "${track.property}" is a slot timeline — put the name in "slot"` : ''),
9291
+ (track.property in SLOT_TRACKS ? `. "${track.property}" is a slot timeline — put the name in "slot"` : ''), 'CE_COMPILE_BONE_TRACK_PROPERTY_UNKNOWN',
9292
9292
  );
9293
9293
  }
9294
9294
  if (track.bone) return [track.bone];
@@ -9300,7 +9300,7 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9300
9300
  // following the part and two objects moving together, which reads as a
9301
9301
  // composite.
9302
9302
  const members = motion.groups?.[track.group];
9303
- if (!members) throw new CompileError(`animation "${animName}": unknown group "${track.group}"`);
9303
+ if (!members) throw new CompileError(`animation "${animName}": unknown group "${track.group}"`, 'CE_COMPILE_ANIMATION_GROUP_UNKNOWN');
9304
9304
  // 🚨 The group is the one target shape whose FAMILY is decided by the
9305
9305
  // property, and three tables decide it: `constraintFamilyOf` asks
9306
9306
  // `property in PHYSICS_TRACKS`, this function asks `property in BONE_TRACKS`,
@@ -9326,12 +9326,12 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9326
9326
  `animation "${animName}" group "${track.group}" has no timeline "${track.property}" ` +
9327
9327
  `(a bone group has: ${Object.keys(BONE_TRACKS).join(', ')}; ` +
9328
9328
  `a slot group has: ${Object.keys(SLOT_TRACKS).join(', ')}; ` +
9329
- `a ${CONSTRAINT_TRACK_FAMILIES.physics.label} group has: ${Object.keys(PHYSICS_TRACKS).join(', ')})`,
9329
+ `a ${CONSTRAINT_TRACK_FAMILIES.physics.label} group has: ${Object.keys(PHYSICS_TRACKS).join(', ')})`, 'CE_COMPILE_GROUP_TRACK_PROPERTY_UNKNOWN',
9330
9330
  );
9331
9331
  }
9332
9332
  return members;
9333
9333
  }
9334
- throw new CompileError(`animation "${animName}": a track targets neither slot nor group`);
9334
+ throw new CompileError(`animation "${animName}": a track targets neither slot nor group`, 'CE_COMPILE_TRACK_TARGET_MISSING');
9335
9335
  }
9336
9336
 
9337
9337
  /**
@@ -9352,23 +9352,23 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9352
9352
  function checkMotionGroups(motion: MotionSpec): void {
9353
9353
  for (const [name, members] of Object.entries(motion.groups ?? {})) {
9354
9354
  if (!Array.isArray(members)) {
9355
- throw new CompileError(`group "${name}" is ${JSON.stringify(members)}; it is an array of member names`);
9355
+ throw new CompileError(`group "${name}" is ${JSON.stringify(members)}; it is an array of member names`, 'CE_COMPILE_GROUP_MEMBERS_NOT_ARRAY');
9356
9356
  }
9357
9357
  if (members.length === 0) {
9358
9358
  throw new CompileError(
9359
- `group "${name}" declares no members, so every track naming it would compile no timeline and gate green`,
9359
+ `group "${name}" declares no members, so every track naming it would compile no timeline and gate green`, 'CE_COMPILE_GROUP_MEMBERS_EMPTY',
9360
9360
  );
9361
9361
  }
9362
9362
  const seen = new Set<string>();
9363
9363
  members.forEach((member, i) => {
9364
9364
  if (typeof member !== 'string' || member.length === 0) {
9365
- throw new CompileError(`group "${name}" member ${i} is ${JSON.stringify(member)}; it is a bone or slot name`);
9365
+ throw new CompileError(`group "${name}" member ${i} is ${JSON.stringify(member)}; it is a bone or slot name`, 'CE_COMPILE_GROUP_MEMBER_NOT_NAME');
9366
9366
  }
9367
9367
  if (seen.has(member)) {
9368
9368
  throw new CompileError(
9369
9369
  `group "${name}" names member "${member}" twice (at index ${members.indexOf(member)} and ${i}). ` +
9370
9370
  'Member order is what `stagger` counts and what a per-member value map is read against, so a repeat is ' +
9371
- 'two different delays and two different values for one bone.',
9371
+ 'two different delays and two different values for one bone.', 'CE_COMPILE_GROUP_MEMBER_DUPLICATE',
9372
9372
  );
9373
9373
  }
9374
9374
  seen.add(member);
@@ -9414,13 +9414,13 @@ function resolveMemberTrack(
9414
9414
  if ((carriesMap || carriesModel) && track.group === undefined && track.bone === undefined) {
9415
9415
  throw new CompileError(
9416
9416
  `${at}: per-member values need a target whose members are named — put them on a track that names a "group", or ` +
9417
- 'a "bone" for the one-member case',
9417
+ 'a "bone" for the one-member case', 'CE_COMPILE_MEMBER_VALUES_TARGET_UNNAMED',
9418
9418
  );
9419
9419
  }
9420
9420
  if (carriesMap && targetKind === 'bone') {
9421
9421
  throw new CompileError(
9422
9422
  `${at}: a per-member "v" map names members, and a bone track has one target rather than members. Write the ` +
9423
- 'value directly, or move the track onto a group.',
9423
+ 'value directly, or move the track onto a group.', 'CE_COMPILE_BONE_MEMBER_VALUE_MAP',
9424
9424
  );
9425
9425
  }
9426
9426
 
@@ -9437,7 +9437,7 @@ function resolveMemberTrack(
9437
9437
  throw new CompileError(
9438
9438
  `${where}: this key carries both a "derive" model and a per-member "v" map — two answers to one question. ` +
9439
9439
  'The model states the arithmetic and the depths that produced the numbers; the map states the numbers. ' +
9440
- 'Pick one.',
9440
+ 'Pick one.', 'CE_COMPILE_MEMBER_DERIVE_AND_VALUE_MAP',
9441
9441
  );
9442
9442
  }
9443
9443
  if (key.derive === undefined) {
@@ -9452,7 +9452,7 @@ function resolveMemberTrack(
9452
9452
  if (!known.has(named)) {
9453
9453
  throw new CompileError(
9454
9454
  `${where}: the value map names "${named}", which group "${target}" does not declare ` +
9455
- `(its members are: ${targets.join(', ')})`,
9455
+ `(its members are: ${targets.join(', ')})`, 'CE_COMPILE_MEMBER_VALUE_NAME_UNKNOWN',
9456
9456
  );
9457
9457
  }
9458
9458
  }
@@ -9464,7 +9464,7 @@ function resolveMemberTrack(
9464
9464
  `(it states: ${Object.keys(table).join(', ') || 'nothing'}). ` +
9465
9465
  'A member is refused rather than defaulted: an absent value is exactly the thing a map of six is ' +
9466
9466
  'written to make visible, and defaulting it to the identity would key that one bone with a different ' +
9467
- 'motion in silence.',
9467
+ 'motion in silence.', 'CE_COMPILE_MEMBER_VALUE_MISSING',
9468
9468
  );
9469
9469
  }
9470
9470
  const value = table[name];
@@ -9500,7 +9500,7 @@ function resolveMemberTrack(
9500
9500
  throw new CompileError(
9501
9501
  `${where}: derive reads member "${name}"'s setup position, and this rig declares no such bone. ` +
9502
9502
  'A model over a group of slots or constraints has no coordinate to be evaluated at — state the values ' +
9503
- 'as a "v" map.',
9503
+ 'as a "v" map.', 'CE_COMPILE_MEMBER_DERIVE_TARGET_NOT_BONE',
9504
9504
  );
9505
9505
  }
9506
9506
  parents.add(bone.parent ?? '(root)');
@@ -9511,7 +9511,7 @@ function resolveMemberTrack(
9511
9511
  `${where}: derive measures every member's "${coordinate}" from its parent's origin and "about" says where the ` +
9512
9512
  `axis crosses it, so one space is what makes the members comparable. These members sit under ` +
9513
9513
  `${parents.size} different parents (${[...parents].join(', ')}), and their coordinates are therefore ` +
9514
- 'measured from different origins. Split the track by parent, or state the values as a "v" map.',
9514
+ 'measured from different origins. Split the track by parent, or state the values as a "v" map.', 'CE_COMPILE_MEMBER_DERIVE_PARENTS_DIFFER',
9515
9515
  );
9516
9516
  }
9517
9517
  const report = evaluateTrackDerive(key.derive, track.property, members, onModelGrid, where);
@@ -9571,7 +9571,7 @@ function compileTrack(
9571
9571
  if (shape === undefined) {
9572
9572
  throw new CompileError(
9573
9573
  `animation "${animName}" slot "${target}" has no timeline "${track.property}" ` +
9574
- `(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`,
9574
+ `(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`, 'CE_COMPILE_SLOT_TRACK_PROPERTY_UNKNOWN',
9575
9575
  );
9576
9576
  }
9577
9577
  // 🚨 Raised here, with the target and before the keys, for the same reason as
@@ -9596,10 +9596,10 @@ function compileTrack(
9596
9596
  `${where}: slot "${target}" declares no setup "dark", and an "${shape}" timeline poses a slot's dark colour — ` +
9597
9597
  'the runtime allocates one only for a slot whose setup pose has it, so applying this animation throws ' +
9598
9598
  `instead of tinting. Give slot "${target}" a \`dark\` in the rig spec (the colour it holds at rest), or ` +
9599
- `key "${shape === 'rgba2' ? 'rgba' : 'rgb'}" if only the light colour moves`,
9599
+ `key "${shape === 'rgba2' ? 'rgba' : 'rgb'}" if only the light colour moves`, 'CE_COMPILE_DARK_TIMELINE_SETUP_DARK_MISSING',
9600
9600
  );
9601
9601
  }
9602
- if (!track.keys.length) throw new CompileError(`${where}: no keys`);
9602
+ if (!track.keys.length) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
9603
9603
 
9604
9604
  const out: ModelKey[] = [];
9605
9605
  for (let i = 0; i < track.keys.length; i++) {
@@ -9607,13 +9607,13 @@ function compileTrack(
9607
9607
  const next = track.keys[i + 1];
9608
9608
  const time = keyTime(key.t + shift);
9609
9609
  if (i > 0 && time <= (out[i - 1].time as number)) {
9610
- throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`);
9610
+ throw new CompileError(`${where}: key times must strictly increase (at t=${key.t})`, 'CE_COMPILE_KEY_TIMES_NOT_INCREASING');
9611
9611
  }
9612
9612
  checkKeyTime(where, time, key.t, duration);
9613
9613
 
9614
9614
  if (shape === 'attachment') {
9615
9615
  if (key.v !== null && typeof key.v !== 'string') {
9616
- throw new CompileError(`${where}: attachment key value must be a string or null`);
9616
+ throw new CompileError(`${where}: attachment key value must be a string or null`, 'CE_COMPILE_ATTACHMENT_KEY_VALUE_INVALID');
9617
9617
  }
9618
9618
  // Resolved against every skin, not just `default` — see `SlotAttachmentIndex`.
9619
9619
  const known = attachments.bySlot.get(target);
@@ -9628,10 +9628,10 @@ function compileTrack(
9628
9628
  `(searched: ${attachments.searched.join(', ')}) — ` +
9629
9629
  (known === undefined || known.size === 0
9630
9630
  ? 'the slot has no attachments at all'
9631
- : `the slot has: ${[...known].join(', ')}`),
9631
+ : `the slot has: ${[...known].join(', ')}`), 'CE_COMPILE_ATTACHMENT_KEY_UNKNOWN',
9632
9632
  );
9633
9633
  }
9634
- if (key.ease) throw new CompileError(`${where}: attachment keys cannot carry an easing`);
9634
+ if (key.ease) throw new CompileError(`${where}: attachment keys cannot carry an easing`, 'CE_COMPILE_ATTACHMENT_KEY_EASING');
9635
9635
  // Attachment timelines are inherently stepped — exactly what lip-sync wants.
9636
9636
  out.push({ time, name: key.v });
9637
9637
  continue;
@@ -9650,7 +9650,7 @@ function compileTrack(
9650
9650
  // a second thing to keep in step.
9651
9651
  const { spelling, channels } = COLOUR_KEYS[shape];
9652
9652
  const colourOf = (v: number[]): Record<string, string | number> => colourKey(shape, v);
9653
- if (!Array.isArray(key.v)) throw new CompileError(`${where}: ${shape} key value must be ${spelling}`);
9653
+ if (!Array.isArray(key.v)) throw new CompileError(`${where}: ${shape} key value must be ${spelling}`, 'CE_COMPILE_SLOT_KEY_VALUE_SHAPE');
9654
9654
  if (shape === 'alpha' && key.v.length === 1 && !(Number.isFinite(key.v[0]) && key.v[0] >= 0 && key.v[0] <= 1)) {
9655
9655
  // The one colour shape written as a number rather than a byte, so the one
9656
9656
  // that is not clamped onto 0..1 by `channelHex` on the way out. The
@@ -9661,15 +9661,15 @@ function compileTrack(
9661
9661
  // clamping is choosing the value.
9662
9662
  throw new CompileError(
9663
9663
  `${where}: key at t=${key.t} is ${String(key.v[0])}; an alpha is a number from 0 to 1 — the runtime clamps ` +
9664
- 'the posed alpha to that range after interpolating, so a value outside it is one no pose ever holds',
9664
+ 'the posed alpha to that range after interpolating, so a value outside it is one no pose ever holds', 'CE_COMPILE_ALPHA_KEY_RANGE_INVALID',
9665
9665
  );
9666
9666
  }
9667
9667
  const entry: ModelKey = { time, ...colourOf(key.v) };
9668
9668
  if (key.ease !== undefined && key.curve !== undefined) {
9669
- throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`);
9669
+ throw new CompileError(`${where}: a key carries both a named easing and a raw curve; pick one`, 'CE_COMPILE_KEY_EASING_AND_CURVE');
9670
9670
  }
9671
9671
  if (key.curve !== undefined) {
9672
- if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`);
9672
+ if (!next) throw new CompileError(`${where}: last key carries a curve but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_CURVE');
9673
9673
  entry.curve = rawCurve(key.curve, channels, where, String(key.t));
9674
9674
  out.push(entry);
9675
9675
  continue;
@@ -9679,9 +9679,9 @@ function compileTrack(
9679
9679
  entry.curve = 'stepped';
9680
9680
  } else {
9681
9681
  const handles = motion.easings?.[key.ease];
9682
- if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`);
9682
+ if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`, 'CE_COMPILE_KEY_EASING_UNKNOWN');
9683
9683
  if (!Array.isArray(next.v)) {
9684
- throw new CompileError(`${where}: ${shape} key value must be ${spelling}`);
9684
+ throw new CompileError(`${where}: ${shape} key value must be ${spelling}`, 'CE_COMPILE_SLOT_KEY_VALUE_SHAPE');
9685
9685
  }
9686
9686
  const t2 = keyTime(next.t + shift);
9687
9687
  // 4 numbers per channel, in the format's own channel order — 16 for
@@ -9696,7 +9696,7 @@ function compileTrack(
9696
9696
  entry.curve = easingCurve(handles, time, t2, key.v, next.v, held);
9697
9697
  }
9698
9698
  } else if (key.ease && !next) {
9699
- throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`);
9699
+ throw new CompileError(`${where}: last key carries an easing but has nothing to ease to`, 'CE_COMPILE_LAST_KEY_EASING');
9700
9700
  }
9701
9701
  out.push(entry);
9702
9702
  }