spine-rigc 2.56.0 → 2.58.0

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
@@ -99,6 +99,7 @@ import {
99
99
  type MeshGeometry,
100
100
  type MeshVertexWeight,
101
101
  } from './mesh.ts';
102
+ import { meshPieces, shellWord } from './meshsplit.ts';
102
103
  import { Plate, readPlate } from '../tools/plate.ts';
103
104
  import {
104
105
  extractRegion,
@@ -888,7 +889,7 @@ function refuseNamesTheEditorCouldKeyDifferently(
888
889
  throw new CompileError(
889
890
  `${found.length} pair(s) of ${what.noun} have no one order: ${what.why}` +
890
891
  `${spelled.join('. ')}` +
891
- (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',
892
893
  );
893
894
  }
894
895
 
@@ -968,7 +969,7 @@ export function f32(n: number): number {
968
969
  }
969
970
  // Unreachable — nine significant digits name every float32. Stated rather than
970
971
  // assumed, because a fallback here would be a number nobody chose.
971
- 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');
972
973
  }
973
974
 
974
975
  /**
@@ -1143,7 +1144,7 @@ function checkKeyTime(where: string, time: number, authored: number, duration: n
1143
1144
  throw new CompileError(
1144
1145
  `${where}: key at ${at} is ${f32(past)}s past the declared duration ${duration}s — nothing that plays this ` +
1145
1146
  `animation for the duration it declares ever reaches it. Move the key to ${duration}, or declare the ` +
1146
- `duration you meant.`,
1147
+ `duration you meant.`, 'CE_COMPILE_KEY_AFTER_DURATION',
1147
1148
  );
1148
1149
  }
1149
1150
 
@@ -1155,7 +1156,7 @@ function channelHex(v: number): string {
1155
1156
  }
1156
1157
 
1157
1158
  function rgbaHex(v: number[]): string {
1158
- 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');
1159
1160
  return v.map(channelHex).join('');
1160
1161
  }
1161
1162
 
@@ -1175,7 +1176,7 @@ function rgbaHex(v: number[]): string {
1175
1176
  * by: `readCurve(…, 0..3, light r/g/b/a)` then `readCurve(…, 4..6, dark r/g/b)`.
1176
1177
  */
1177
1178
  function rgba2Hex(v: number[]): { light: string; dark: string } {
1178
- 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');
1179
1180
  return { light: v.slice(0, 4).map(channelHex).join(''), dark: v.slice(4, 7).map(channelHex).join('') };
1180
1181
  }
1181
1182
 
@@ -1225,7 +1226,7 @@ const COLOUR_KEYS: Record<SlotColourShape, { channels: number; spelling: string;
1225
1226
  function colourKey(shape: SlotColourShape, v: number[]): Record<string, string | number> {
1226
1227
  const { channels, write } = COLOUR_KEYS[shape];
1227
1228
  if (v.length !== channels) {
1228
- 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');
1229
1230
  }
1230
1231
  return write(v);
1231
1232
  }
@@ -1653,7 +1654,7 @@ function readJson<T>(path: string): T {
1653
1654
  try {
1654
1655
  text = readFileSync(path, 'utf8');
1655
1656
  } catch (err) {
1656
- 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');
1657
1658
  }
1658
1659
  try {
1659
1660
  // A parse failure gets a line/column appended to the runtime's own
@@ -1661,7 +1662,7 @@ function readJson<T>(path: string): T {
1661
1662
  // cannot say where.
1662
1663
  return parseJsonWithPosition(text) as T;
1663
1664
  } catch (err) {
1664
- 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');
1665
1666
  }
1666
1667
  }
1667
1668
 
@@ -1881,13 +1882,13 @@ function fromLoosePng(
1881
1882
  // Left to `readFileSync` this arrives as a raw ENOENT with a stack, which
1882
1883
  // is the tool telling an agent about its own internals instead of about
1883
1884
  // the rig. The validator's messages are the UI, and so are these.
1884
- 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');
1885
1886
  }
1886
1887
  // A loose part IS a page of the atlas this route emits, so it is refused the
1887
1888
  // way a pack's page is (issue #732): by the one reader's sentence, as a
1888
1889
  // CompileError, rather than as that reader's throw with a stack under it.
1889
1890
  const header = readPngHeader(absPath);
1890
- 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');
1891
1892
  const info = header.info;
1892
1893
  // Page name is the PNG path *relative to the atlas file*, so the viewer
1893
1894
  // resolves it the way every Spine consumer does: against the atlas URL.
@@ -1912,7 +1913,7 @@ interface AtlasSource {
1912
1913
  }
1913
1914
 
1914
1915
  function readAtlasIn(path: string): AtlasSource {
1915
- 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');
1916
1917
  const parsed = parseAtlasText(readFileSync(path, 'utf8'));
1917
1918
  const byName = new Map<string, { region: AtlasRegion; page: ParsedAtlas['pages'][number] }>();
1918
1919
  for (const page of parsed.pages) {
@@ -1924,7 +1925,7 @@ function readAtlasIn(path: string): AtlasSource {
1924
1925
  if (!byName.has(key)) byName.set(key, { region, page });
1925
1926
  }
1926
1927
  }
1927
- 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');
1928
1929
  // 🔒 **What each page IS, where the pack is opened** (issue #732). Every page
1929
1930
  // the pack names reaches the gate — the atlas this build emits is the pack's
1930
1931
  // own text — so a page no reader can open is refused here, all of them in
@@ -1957,7 +1958,7 @@ function readAtlasIn(path: string): AtlasSource {
1957
1958
  if (unread.length > 0) {
1958
1959
  throw new CompileError(
1959
1960
  `--atlas-in ${path}: ${unread.length} of its ${parsed.pages.length} page(s) cannot be read as PNG, and ` +
1960
- `nothing was compiled against the pack — ${unread.join('; ')}`,
1961
+ `nothing was compiled against the pack — ${unread.join('; ')}`, 'CE_COMPILE_ATLAS_PAGES_UNREADABLE',
1961
1962
  );
1962
1963
  }
1963
1964
  return { path, dir: dirname(path), parsed, byName, grids };
@@ -2027,13 +2028,13 @@ function resolveFromAtlas(
2027
2028
  throw new CompileError(
2028
2029
  `image "${relPath}" resolves to region "${region}", which the atlas at ${atlas.path} does not have. ` +
2029
2030
  (near.length ? `Did you mean ${near.map((n) => JSON.stringify(n)).join(', ')}? ` : '') +
2030
- `The atlas declares ${known.length} region(s): ${known.join(', ')}`,
2031
+ `The atlas declares ${known.length} region(s): ${known.join(', ')}`, 'CE_COMPILE_ATLAS_REGION_UNKNOWN',
2031
2032
  );
2032
2033
  }
2033
2034
  const absPath = resolve(atlas.dir, found.page.name);
2034
2035
  if (!existsSync(absPath)) {
2035
2036
  throw new CompileError(
2036
- `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',
2037
2038
  );
2038
2039
  }
2039
2040
  // The rectangle ON THE PAGE, which at a quarter turn is not the one `bounds:`
@@ -2048,7 +2049,7 @@ function resolveFromAtlas(
2048
2049
  throw new CompileError(
2049
2050
  `region "${region}" is at ${found.region.x},${found.region.y} sized ${rectW}x${rectH} on a ` +
2050
2051
  `${found.page.width}x${found.page.height} page in ${atlas.path}; the rectangle runs off the page, so its ` +
2051
- 'UVs would leave the texture',
2052
+ 'UVs would leave the texture', 'CE_COMPILE_ATLAS_REGION_OUTSIDE_PAGE',
2052
2053
  );
2053
2054
  }
2054
2055
  const info = readPngInfo(absPath);
@@ -2341,7 +2342,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2341
2342
  return fn();
2342
2343
  } catch (err) {
2343
2344
  if (err instanceof CompileError && !err.message.includes(motionPath)) {
2344
- throw new CompileError(`${motionPath}: ${err.message}`);
2345
+ throw new CompileError(`${motionPath}: ${err.message}`, err.code);
2345
2346
  }
2346
2347
  throw err;
2347
2348
  }
@@ -2356,7 +2357,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2356
2357
  // does not — the class of wrongness that loads, plays and lies.
2357
2358
  if (motion.archetype !== rig.name) {
2358
2359
  throw new CompileError(
2359
- `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',
2360
2361
  );
2361
2362
  }
2362
2363
  });
@@ -2377,7 +2378,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2377
2378
  if (!noStage && (stageWidth === undefined || stageHeight === undefined)) {
2378
2379
  throw new CompileError(
2379
2380
  'no stage size: give the rig spec a `skeleton.width`/`skeleton.height`, or compile against a cut manifest whose `crop` states them, ' +
2380
- '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',
2381
2382
  );
2382
2383
  }
2383
2384
  /**
@@ -2433,7 +2434,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2433
2434
  partDir: string | null = dirname(resolve(baseDir, relPath)),
2434
2435
  ): CompiledImage => {
2435
2436
  if (seenRegions.has(region)) {
2436
- throw new CompileError(`duplicate region name "${region}" (${relPath})`);
2437
+ throw new CompileError(`duplicate region name "${region}" (${relPath})`, 'CE_COMPILE_REGION_NAME_DUPLICATE');
2437
2438
  }
2438
2439
  const loosePath = resolve(baseDir, relPath);
2439
2440
  const img =
@@ -2484,7 +2485,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2484
2485
  throw new CompileError(
2485
2486
  `${where}: "${relPath}" and the art already atlased as region "${region}" are two different files — ` +
2486
2487
  `${wanted} and ${already}. An atlas region is named by its PNG's basename, so only one of the two can ` +
2487
- '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',
2488
2489
  );
2489
2490
  };
2490
2491
 
@@ -2515,7 +2516,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2515
2516
  `${where}: sequence ${frame} is the region "${region}", and there is no PNG for it at ${wanted}. A ` +
2516
2517
  `sequence's frames are the regions "<stem><start + i>", zero-padded to "digits" — here stem ` +
2517
2518
  `${JSON.stringify(stem)}, start ${seq.start ?? 1}, digits ${seq.digits ?? 0} — and the compiler draws ` +
2518
- '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',
2519
2520
  );
2520
2521
  }
2521
2522
  if (atlasIn !== null && !atlasIn.byName.has(region)) {
@@ -2525,7 +2526,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2525
2526
  (near.length ? `Did you mean ${near.map((n) => JSON.stringify(n)).join(', ')}? ` : '') +
2526
2527
  `A sequence's frames are the regions "<stem><start + i>", zero-padded to "digits" — here stem ` +
2527
2528
  `${JSON.stringify(stem)}, start ${seq.start ?? 1}, digits ${seq.digits ?? 0} — and the compiler draws ` +
2528
- 'no frame in place of another.',
2529
+ 'no frame in place of another.', 'CE_COMPILE_SEQUENCE_FRAME_REGION_MISSING',
2529
2530
  );
2530
2531
  }
2531
2532
  const already = regionSource.get(region);
@@ -2534,7 +2535,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2534
2535
  throw new CompileError(
2535
2536
  `${where}: sequence ${frame} is the region "${region}", and the art already atlased under that name is ` +
2536
2537
  `${already}, not ${wanted}. One region name is one set of pixels, so this frame would draw the other ` +
2537
- "file's. Rename one of them.",
2538
+ "file's. Rename one of them.", 'CE_COMPILE_SEQUENCE_FRAME_FILE_CONFLICT',
2538
2539
  );
2539
2540
  }
2540
2541
  // The directory the frame NAMES are relative to — the images root, not
@@ -2574,7 +2575,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2574
2575
  for (const part of parts) {
2575
2576
  if (!rigSlotIndex.has(part.slot)) {
2576
2577
  throw new CompileError(
2577
- `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',
2578
2579
  );
2579
2580
  }
2580
2581
  }
@@ -2588,7 +2589,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2588
2589
  const at = rigSlotIndex.get(part.slot)!;
2589
2590
  if (at < orderCursor) {
2590
2591
  throw new CompileError(
2591
- `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',
2592
2593
  );
2593
2594
  }
2594
2595
  orderCursor = at;
@@ -2603,33 +2604,33 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2603
2604
  if (meshParts.length > meshBudget) {
2604
2605
  throw new CompileError(
2605
2606
  `${meshParts.length} mesh slot(s) declared but the rig "${rig.name}" allows ${meshBudget}` +
2606
- ' — 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',
2607
2608
  );
2608
2609
  }
2609
2610
  for (const part of meshParts) {
2610
2611
  if (isBasePlate(part, manifest!)) {
2611
2612
  // A base plate mesh is a full-frame canvas every frame.
2612
- 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');
2613
2614
  }
2614
2615
  const spec = part.mesh!;
2615
2616
  const kind = spec.kind ?? 'ring';
2616
2617
  if (kind === 'ring') {
2617
2618
  if (!part.polygon?.length) {
2618
- 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');
2619
2620
  }
2620
2621
  if (spec.hull !== 'polygon') {
2621
- 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');
2622
2623
  }
2623
2624
  if (!spec.center || spec.inner === undefined) {
2624
- 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');
2625
2626
  }
2626
2627
  } else {
2627
2628
  if (!spec.rows || !spec.chain?.length) {
2628
- 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');
2629
2630
  }
2630
2631
  }
2631
2632
  if (!meshControlBones(part).length) {
2632
- 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');
2633
2634
  }
2634
2635
  }
2635
2636
 
@@ -2655,7 +2656,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2655
2656
  const img = addImage(part.image, manifestDir!, isBasePlate(part, manifest!));
2656
2657
  if (img.width !== win.w || img.height !== win.h) {
2657
2658
  throw new CompileError(
2658
- `${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',
2659
2660
  );
2660
2661
  }
2661
2662
  slotAttachments.set(part.slot, [img.region]);
@@ -2690,7 +2691,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2690
2691
  const img = addImage(relPath, manifestDir!, false);
2691
2692
  if (img.width !== win.w || img.height !== win.h) {
2692
2693
  throw new CompileError(
2693
- `${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',
2694
2695
  );
2695
2696
  }
2696
2697
  names.push(img.region);
@@ -2713,11 +2714,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2713
2714
  for (const skinName of skinNames) {
2714
2715
  for (const [slotName, placeholders] of Object.entries(skinParts.get(skinName)!.attachments)) {
2715
2716
  if (!rigSlotIndex.has(slotName)) {
2716
- 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');
2717
2718
  }
2718
2719
  if (slotAttachments.has(slotName)) {
2719
2720
  throw new CompileError(
2720
- `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',
2721
2722
  );
2722
2723
  }
2723
2724
  const names = rigAttachmentNames.get(slotName) ?? [];
@@ -2808,7 +2809,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2808
2809
  transforms = computeWorldTransforms(bones);
2809
2810
  exactFrames = computeExactFrameTransforms(bones);
2810
2811
  } catch (err) {
2811
- if (err instanceof TransformError) throw new CompileError(err.message);
2812
+ if (err instanceof TransformError) throw new CompileError(err.message, 'CE_COMPILE_SETUP_TRANSFORM_INVALID');
2812
2813
  throw err;
2813
2814
  }
2814
2815
 
@@ -2816,7 +2817,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2816
2817
  for (const name of meshControlBones(part)) {
2817
2818
  if (!boneNames.has(name)) {
2818
2819
  throw new CompileError(
2819
- `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',
2820
2821
  );
2821
2822
  }
2822
2823
  }
@@ -2908,7 +2909,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2908
2909
  // here down is the half that needs the rig in front of it.
2909
2910
  if (setup !== undefined && rigSlot.attachment !== undefined) {
2910
2911
  throw new CompileError(
2911
- `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',
2912
2913
  );
2913
2914
  }
2914
2915
  let setupAttachment: string | null;
@@ -2921,7 +2922,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2921
2922
  else if (empty) setupAttachment = null;
2922
2923
  else {
2923
2924
  throw new CompileError(
2924
- `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',
2925
2926
  );
2926
2927
  }
2927
2928
  if (setupAttachment !== null && !names.includes(setupAttachment)) {
@@ -2970,10 +2971,10 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
2970
2971
  `no art: ${found} — ${supply}, ${many ? 'fix the paths' : 'fix the path'} in the manifest, or state the ` +
2971
2972
  'setup pose as null';
2972
2973
  }
2973
- throw new CompileError(message);
2974
+ throw new CompileError(message, 'CE_COMPILE_SETUP_ATTACHMENT_UNAVAILABLE');
2974
2975
  }
2975
2976
  if (setup?.color && rigSlot.color !== undefined) {
2976
- 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');
2977
2978
  }
2978
2979
  // The setup attachment is held as the value it is, `null` included; leaving
2979
2980
  // the key out for `null` is the emitter's (`emitSlots`).
@@ -3000,7 +3001,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3000
3001
  const mesh = part.mesh ? buildMesh(part, manifest!, bones, transforms, exactFrames, rigSlot.bone) : null;
3001
3002
  for (const name of names) {
3002
3003
  const img = images.find((im) => im.region === name);
3003
- 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');
3004
3005
  if (mesh) {
3005
3006
  // Every state of a mesh slot gets the SAME geometry. That is what makes
3006
3007
  // an attachment swap mid-deform safe: the control bone's pose means the
@@ -3088,7 +3089,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3088
3089
  `${budgeted.length}, "meshTriangles": <triangles one mesh may carry> }\` to the rig spec: geometry rigc ` +
3089
3090
  'built is geometry it will not ship unmeasured, and `A13_MESH_BUDGET` has nothing to measure against ' +
3090
3091
  'until that budget is stated. (Authored geometry is exempt — rigc did not draw it.)'
3091
- : ' — 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',
3092
3093
  );
3093
3094
  }
3094
3095
 
@@ -3125,7 +3126,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3125
3126
  throw new CompileError(
3126
3127
  `physics constraint "${EVERY_GLOBAL_PHYSICS}": the name is reserved — a motion spec's \`"physics": ` +
3127
3128
  `"${EVERY_GLOBAL_PHYSICS}"\` is the timeline that names no constraint and drives every physics constraint ` +
3128
- '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',
3129
3130
  );
3130
3131
  }
3131
3132
  constraintDeclared.add(constraintAt(type, name));
@@ -3198,12 +3199,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3198
3199
  // apart.
3199
3200
  if (constraintDeclared.has(constraintAt('physics', name))) {
3200
3201
  throw new CompileError(
3201
- `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',
3202
3203
  );
3203
3204
  }
3204
3205
  declareConstraint(name, 'physics');
3205
3206
  if (!boneNames.has(spec.bone)) {
3206
- 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');
3207
3208
  }
3208
3209
  // A stop's copy and clamps sit right after the rig constraint that swings
3209
3210
  // each link (`src/stop.ts`); this table is emitted after every rig
@@ -3213,7 +3214,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3213
3214
  throw new CompileError(
3214
3215
  `physics constraint "${name}" drives "${spec.bone}", a link stop "${stop.chain}" holds, from the physics table — ` +
3215
3216
  "the table is emitted after every rig constraint, past the stop's copy and clamp, which would read the link " +
3216
- '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',
3217
3218
  );
3218
3219
  }
3219
3220
  // Every component and parameter the table states is held, 0 and the
@@ -3232,7 +3233,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3232
3233
  // The parser is happy with this and the constraint does nothing at all.
3233
3234
  // A23 catches it too; refusing here means it never reaches the gate.
3234
3235
  throw new CompileError(
3235
- `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',
3236
3237
  );
3237
3238
  }
3238
3239
  for (const [param] of PHYSICS_PARAMS) {
@@ -3341,7 +3342,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3341
3342
  if (everyGlobal && track.physics === undefined) {
3342
3343
  throw new CompileError(
3343
3344
  `animation "${animName}": group "${String(track.group)}" lists "${EVERY_GLOBAL_PHYSICS}", which is not a ` +
3344
- `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',
3345
3346
  );
3346
3347
  }
3347
3348
  if (everyGlobal) {
@@ -3363,7 +3364,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3363
3364
  ' — it would parse and move nothing. ' +
3364
3365
  (track.property === 'reset'
3365
3366
  ? 'Declare a physics constraint, or remove the track'
3366
- : `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',
3367
3368
  );
3368
3369
  }
3369
3370
  } else if (family !== null) {
@@ -3379,21 +3380,21 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3379
3380
  if (kinds.length > 0) {
3380
3381
  throw new CompileError(
3381
3382
  `animation "${animName}" keys "${target}" as a ${label}, but the rig declares it as a "${kinds.join('"/"')}" ` +
3382
- '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',
3383
3384
  );
3384
3385
  }
3385
3386
  const known = constraintNamesOfKind.get(family) ?? [];
3386
3387
  throw new CompileError(
3387
3388
  `animation "${animName}" keys unknown ${label} "${target}"` +
3388
- (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',
3389
3390
  );
3390
3391
  }
3391
3392
  } else if (isBoneTrack) {
3392
3393
  if (!boneNames.has(target)) {
3393
- throw new CompileError(`animation "${animName}" keys unknown bone "${target}"`);
3394
+ throw new CompileError(`animation "${animName}" keys unknown bone "${target}"`, 'CE_COMPILE_ANIMATION_BONE_UNKNOWN');
3394
3395
  }
3395
3396
  } else if (!slotNames.has(target)) {
3396
- throw new CompileError(`animation "${animName}" targets unknown slot "${target}"`);
3397
+ throw new CompileError(`animation "${animName}" targets unknown slot "${target}"`, 'CE_COMPILE_ANIMATION_SLOT_UNKNOWN');
3397
3398
  }
3398
3399
  // A timeline is a kind, a name and a property — `physics.leg.mix` and
3399
3400
  // `path.leg.mix` are two timelines on two constraints, so the claim
@@ -3402,7 +3403,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3402
3403
  const claim = `${family ?? (isBoneTrack ? 'bone' : 'slot')} ${target}.${track.property}`;
3403
3404
  if (claimed.has(claim)) {
3404
3405
  throw new CompileError(
3405
- `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',
3406
3407
  );
3407
3408
  }
3408
3409
  claimed.add(claim);
@@ -3426,7 +3427,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3426
3427
  'channels at every time, its setup value before its first key included, so the one the file states ' +
3427
3428
  'later overwrites the other everywhere and the first one\'s keys there are read by nothing. Key each ' +
3428
3429
  'channel once: "rgb" and "alpha" move the light colour\'s two halves on their own key times, "rgba" ' +
3429
- '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',
3430
3431
  );
3431
3432
  }
3432
3433
  }
@@ -3479,7 +3480,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3479
3480
  if (kinds.length > 0) {
3480
3481
  throw new CompileError(
3481
3482
  `animation "${animName}" keys "${name}" as ${group === 'ik' ? 'an' : 'a'} ${group} constraint, but the rig declares it as a ` +
3482
- `"${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',
3483
3484
  );
3484
3485
  }
3485
3486
  const known = constraintNamesOfKind.get(group) ?? [];
@@ -3487,13 +3488,13 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3487
3488
  `animation "${animName}" keys unknown ${group} constraint "${name}"; ` +
3488
3489
  (known.length
3489
3490
  ? `the rig declares ${group} constraint(s): ${known.join(', ')}`
3490
- : `the rig declares no ${group} constraint at all`),
3491
+ : `the rig declares no ${group} constraint at all`), 'CE_COMPILE_CONSTRAINT_TIMELINE_TARGET_UNKNOWN',
3491
3492
  );
3492
3493
  }
3493
3494
  if (constraintTimelines[group].has(name)) {
3494
3495
  throw new CompileError(
3495
3496
  `animation "${animName}" has two ${group} timelines on constraint "${name}"; ` +
3496
- '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',
3497
3498
  );
3498
3499
  }
3499
3500
  const keys = compileConstraintTrack(
@@ -3531,25 +3532,25 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3531
3532
  const table = skinTables.get(skinName);
3532
3533
  if (!table) {
3533
3534
  throw new CompileError(
3534
- `${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',
3535
3536
  );
3536
3537
  }
3537
3538
  const perSlot = table[track.slot];
3538
3539
  if (!perSlot) {
3539
3540
  throw new CompileError(
3540
3541
  `${at}: skin "${skinName}" gives slot "${String(track.slot)}" no attachments` +
3541
- (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',
3542
3543
  );
3543
3544
  }
3544
3545
  const attachment = perSlot[track.attachment];
3545
3546
  if (!attachment) {
3546
3547
  throw new CompileError(
3547
3548
  `${at}: slot "${track.slot}" in skin "${skinName}" has no attachment "${String(track.attachment)}" ` +
3548
- `(it has: ${Object.keys(perSlot).join(', ')})`,
3549
+ `(it has: ${Object.keys(perSlot).join(', ')})`, 'CE_COMPILE_ATTACHMENT_TIMELINE_ATTACHMENT_UNKNOWN',
3549
3550
  );
3550
3551
  }
3551
3552
  if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
3552
- 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');
3553
3554
  }
3554
3555
  const keys = compileDeformTrack(
3555
3556
  track,
@@ -3578,28 +3579,28 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3578
3579
  const table = skinTables.get(skinName);
3579
3580
  if (!table) {
3580
3581
  throw new CompileError(
3581
- `${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',
3582
3583
  );
3583
3584
  }
3584
3585
  const perSlot = table[track.slot];
3585
3586
  if (!perSlot) {
3586
3587
  throw new CompileError(
3587
3588
  `${at}: skin "${skinName}" gives slot "${String(track.slot)}" no attachments` +
3588
- (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',
3589
3590
  );
3590
3591
  }
3591
3592
  const attachment = perSlot[track.attachment];
3592
3593
  if (!attachment) {
3593
3594
  throw new CompileError(
3594
3595
  `${at}: slot "${track.slot}" in skin "${skinName}" has no attachment "${String(track.attachment)}" ` +
3595
- `(it has: ${Object.keys(perSlot).join(', ')})`,
3596
+ `(it has: ${Object.keys(perSlot).join(', ')})`, 'CE_COMPILE_ATTACHMENT_TIMELINE_ATTACHMENT_UNKNOWN',
3596
3597
  );
3597
3598
  }
3598
3599
  const keys = compileSequenceTrack(track, anim.duration, attachment, at);
3599
3600
  for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
3600
3601
  const slot = attachmentTimelinesOf(skinName, track.slot);
3601
3602
  if (slot.get(track.attachment)?.sequence) {
3602
- 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');
3603
3604
  }
3604
3605
  slot.set(track.attachment, { ...(slot.get(track.attachment) ?? {}), sequence: keys });
3605
3606
  }
@@ -3624,7 +3625,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3624
3625
  // a key on a track whose neighbour already sits on the declared duration.
3625
3626
  if (Math.abs(compiledDuration - anim.duration) > FRAME) {
3626
3627
  throw new CompileError(
3627
- `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',
3628
3629
  );
3629
3630
  }
3630
3631
  // The model's record of the animation. Which groups the file carries, in
@@ -3714,7 +3715,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
3714
3715
  const editorOrder: ModelEditorOrder = editorOrderOver(skins, [...animations.keys()]);
3715
3716
 
3716
3717
  for (const slot of slots) {
3717
- 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');
3718
3719
  }
3719
3720
 
3720
3721
  // One bone order per weighted attachment (issue #1407), on the finished
@@ -3903,7 +3904,7 @@ function buildBone(spec: RigBone, soFar: ModelBone[], ctx: BoneContext): ModelBo
3903
3904
  bone.y = f32(world[1]);
3904
3905
  } else {
3905
3906
  const parent = computeWorldTransforms(soFar).get(spec.parent);
3906
- 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');
3907
3908
  const [lx, ly] = toBoneLocal(parent, world[0], world[1]);
3908
3909
  bone.x = f32(lx);
3909
3910
  bone.y = f32(ly);
@@ -3935,7 +3936,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3935
3936
  if (!from) return null;
3936
3937
  const needManifest = (what: string): FaceManifest => {
3937
3938
  if (!ctx.manifest) {
3938
- 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');
3939
3940
  }
3940
3941
  return ctx.manifest;
3941
3942
  };
@@ -3944,7 +3945,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3944
3945
  const anchor = manifest.anchors?.[from.anchor];
3945
3946
  if (!anchor || anchor.length < 2) {
3946
3947
  throw new CompileError(
3947
- `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',
3948
3949
  );
3949
3950
  }
3950
3951
  return [anchor[0], anchor[1]];
@@ -3954,7 +3955,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3954
3955
  const part = ctx.partBySlot.get(from.slotWindow);
3955
3956
  if (!part) {
3956
3957
  throw new CompileError(
3957
- `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',
3958
3959
  );
3959
3960
  }
3960
3961
  const win = partWindow(part, manifest);
@@ -3965,7 +3966,7 @@ function cropPointOf(spec: RigBone, ctx: BoneContext): [number, number] | null {
3965
3966
  const centre = ctx.partBySlot.get(from.meshCenter)?.mesh?.center;
3966
3967
  if (!centre) {
3967
3968
  throw new CompileError(
3968
- `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',
3969
3970
  );
3970
3971
  }
3971
3972
  return [centre[0], centre[1]];
@@ -3978,7 +3979,7 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
3978
3979
  const source = spec.from?.rotation;
3979
3980
  if (source === 'axis') {
3980
3981
  if (ctx.axisSpineDeg === null) {
3981
- 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');
3982
3983
  }
3983
3984
  return ctx.axisSpineDeg;
3984
3985
  }
@@ -3987,7 +3988,7 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
3987
3988
  const anchor = ctx.manifest?.anchors?.[key];
3988
3989
  if (!anchor || anchor.length < 3) {
3989
3990
  throw new CompileError(
3990
- `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',
3991
3992
  );
3992
3993
  }
3993
3994
  return screenToSpineDegrees(anchor[2]);
@@ -4123,7 +4124,7 @@ function refuseDefaultSkinContest(slotName: string, placeholder: string, skins:
4123
4124
  `"Multiple attachments have the same name: ${placeholder} ${placeholder}", because a default-skin attachment ` +
4124
4125
  "hangs on the slot beside the named skins' placeholder of that name. Move the default skin's entry for this " +
4125
4126
  `slot into a named skin — call it "base" — so every skin filling "${placeholder}" is a named one. Two or more ` +
4126
- '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',
4127
4128
  );
4128
4129
  }
4129
4130
 
@@ -4303,7 +4304,7 @@ function buildRigAttachment(
4303
4304
  `is one of ${SPINE_ATTACHMENT_TYPES.join(', ')}, or the key is absent and reads as "region". ` +
4304
4305
  'PRESENT-and-null is not absent: `getValue(map, "type", "region")` takes the default only when the key is ' +
4305
4306
  'missing (`SkeletonJson.ts:527`), so this map matches no case, `readAttachment` returns null (`:653`), and ' +
4306
- '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',
4307
4308
  );
4308
4309
  }
4309
4310
  const type = stated ?? 'region';
@@ -4318,7 +4319,7 @@ function buildRigAttachment(
4318
4319
  `${where}: "name" is ${JSON.stringify(statedName) ?? String(statedName)}, which is not a string. An ` +
4319
4320
  "attachment's name is the runtime's `Attachment.name` — `getValue(map, \"name\", placeholder)`, " +
4320
4321
  '`SkeletonJson.js:526` — and on a region, mesh or linked mesh it is also what `path` defaults to. Write it ' +
4321
- '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',
4322
4323
  );
4323
4324
  }
4324
4325
  // A linked mesh in the format's OTHER spelling. `type: "mesh"` and `type:
@@ -4336,7 +4337,7 @@ function buildRigAttachment(
4336
4337
  if (type === 'boundingbox') return buildRigBoundingBox(att as RigBoundingBoxAttachment, where, ctx);
4337
4338
  if (type === 'clipping') return buildRigClipping(att as RigClippingAttachment, where, ctx);
4338
4339
  if (type === 'path') return buildRigPath(att as RigPathAttachment, where, ctx);
4339
- 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');
4340
4341
  const near = nearMisses(type, SPINE_ATTACHMENT_TYPES);
4341
4342
  throw new CompileError(
4342
4343
  `${where}: attachment type "${type}" is not one of the ${SPINE_ATTACHMENT_TYPES.length} the Spine 4.3 format ` +
@@ -4348,7 +4349,7 @@ function buildRigAttachment(
4348
4349
  // this format standing one level up from where it was written.
4349
4350
  (type === 'sequence'
4350
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).'
4351
- : ''),
4352
+ : ''), 'CE_COMPILE_ATTACHMENT_TYPE_UNKNOWN',
4352
4353
  );
4353
4354
  }
4354
4355
 
@@ -4385,26 +4386,26 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
4385
4386
  if (typeof count !== 'number' || !Number.isInteger(count) || count < 3) {
4386
4387
  throw new CompileError(
4387
4388
  `${where}: vertexCount is ${JSON.stringify(count)}; a polygon needs at least 3 vertices, stated outright — ` +
4388
- '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',
4389
4390
  );
4390
4391
  }
4391
4392
  if (att.vertices && att.weights) {
4392
4393
  throw new CompileError(
4393
- `${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',
4394
4395
  );
4395
4396
  }
4396
4397
  if (att.weights) {
4397
4398
  if (att.boneIndexing === 'raw') {
4398
- 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');
4399
4400
  }
4400
4401
  if (att.weights.length !== count) {
4401
- 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');
4402
4403
  }
4403
4404
  return { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
4404
4405
  }
4405
4406
  const raw = att.vertices;
4406
4407
  if (!raw || raw.length === 0) {
4407
- 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');
4408
4409
  }
4409
4410
  const unweighted = raw.length === count * 2;
4410
4411
  if (!unweighted) {
@@ -4414,7 +4415,7 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
4414
4415
  'The parser reads that as a WEIGHTED run, whose bone INDEXES point into the emitted bone array — a list ' +
4415
4416
  'the spec never writes, so inserting a bone rebinds every vertex in silence. ' +
4416
4417
  'Give the bindings by name as "weights": [[{ "bone": …, "x": …, "y": …, "weight": … }, …], …], ' +
4417
- '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',
4418
4419
  );
4419
4420
  }
4420
4421
  // A raw run still has to decode to exactly `vertexCount` vertices, or the
@@ -4422,7 +4423,7 @@ function buildVertexGeometry(att: RigVertexGeometry, where: string, ctx: Attachm
4422
4423
  checkRawRunShape(raw, count, `vertexCount is ${count}`, where);
4423
4424
  }
4424
4425
  for (const n of raw) {
4425
- 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');
4426
4427
  }
4427
4428
  if (unweighted) return { weighted: false, xy: raw.map(f32) };
4428
4429
  const bindings = bindRawRun(raw.map(f32), ctx.bones, where);
@@ -4441,17 +4442,17 @@ function checkRawRunShape(raw: readonly number[], vertices: number, countSays: s
4441
4442
  for (let i = 0; i < raw.length; decoded++) {
4442
4443
  const bones = raw[i++];
4443
4444
  if (!Number.isInteger(bones) || bones < 1) {
4444
- 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');
4445
4446
  }
4446
4447
  i += bones * 4;
4447
4448
  if (i > raw.length) {
4448
4449
  throw new CompileError(
4449
- `${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',
4450
4451
  );
4451
4452
  }
4452
4453
  }
4453
4454
  if (decoded !== vertices) {
4454
- 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');
4455
4456
  }
4456
4457
  }
4457
4458
 
@@ -4474,7 +4475,7 @@ function bindRawRun(run: readonly number[], bones: readonly ModelBone[], where:
4474
4475
  for (let k = 0; k < count; k++, i += 4) {
4475
4476
  const bone = bones[run[i]];
4476
4477
  if (bone === undefined) {
4477
- 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');
4478
4479
  }
4479
4480
  vertex.push({ bone: bone.name, x: run[i + 1], y: run[i + 2], weight: run[i + 3] });
4480
4481
  }
@@ -4675,13 +4676,13 @@ function resolveStageBox(
4675
4676
  if (asked === undefined) return null;
4676
4677
  const field = `skeleton.stageBox (slot "${asked.slot}", attachment "${asked.attachment}")`;
4677
4678
  if (ctx.stageWidth === undefined || ctx.stageHeight === undefined) {
4678
- 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');
4679
4680
  }
4680
4681
  const rigSlot = rig.slots.find((s) => s.name === asked.slot);
4681
4682
  if (rigSlot === undefined) {
4682
4683
  throw new CompileError(
4683
4684
  `${field}: slot "${asked.slot}" is not one the rig declares. Declare it in "slots" — where it stands in the ` +
4684
- `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',
4685
4686
  );
4686
4687
  }
4687
4688
  const filledBy = [
@@ -4693,13 +4694,13 @@ function resolveStageBox(
4693
4694
  if (filledBy.length > 0) {
4694
4695
  throw new CompileError(
4695
4696
  `${field}: slot "${asked.slot}" is also filled by ${filledBy.join(' and ')}. The stage box is its slot's one ` +
4696
- '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',
4697
4698
  );
4698
4699
  }
4699
4700
  // The box's bone: the root, stating no setup transform — as built, so a root placed `from` the manifest is read as placed.
4700
4701
  const bone = ctx.bones.find((b) => b.name === rigSlot.bone);
4701
4702
  if (bone === undefined) {
4702
- 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');
4703
4704
  }
4704
4705
  if (bone.parent !== undefined) {
4705
4706
  throw new CompileError(
@@ -4707,7 +4708,7 @@ function resolveStageBox(
4707
4708
  "the stage's corners in world space, written as they are, and the gate holds them on the header's grid " +
4708
4709
  "through the runtime's own frame of an unrotated root; every bone below it adds that frame's residue once " +
4709
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 " +
4710
- `("${ctx.bones[0]?.name ?? 'root'}")`,
4711
+ `("${ctx.bones[0]?.name ?? 'root'}")`, 'CE_COMPILE_STAGE_BOX_BONE_NOT_ROOT',
4711
4712
  );
4712
4713
  }
4713
4714
  const stated = [
@@ -4718,7 +4719,7 @@ function resolveStageBox(
4718
4719
  throw new CompileError(
4719
4720
  `${field}: slot "${asked.slot}" hangs on the root "${bone.name}", which states a setup transform (${stated.join(', ')}). ` +
4720
4721
  "The box's vertices are the stage's corners in world space, written as they are, so the root must not move, " +
4721
- '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',
4722
4723
  );
4723
4724
  }
4724
4725
  const x = rig.skeleton?.x ?? 0;
@@ -4759,7 +4760,7 @@ function buildRigClipping(
4759
4760
  if (typeof att.end !== 'string' || !ctx.slotNames.has(att.end)) {
4760
4761
  throw new CompileError(
4761
4762
  `${where}: end names slot ${JSON.stringify(att.end)}, which this rig does not declare; ` +
4762
- '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',
4763
4764
  );
4764
4765
  }
4765
4766
  }
@@ -4803,7 +4804,7 @@ function setupWorldVertices(
4803
4804
  let y = 0;
4804
4805
  for (const binding of vertex) {
4805
4806
  const m = transforms.get(binding.bone);
4806
- 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');
4807
4808
  const [wx, wy] = toWorld(m, binding.x, binding.y);
4808
4809
  x += wx * binding.weight;
4809
4810
  y += wy * binding.weight;
@@ -4946,14 +4947,14 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4946
4947
  throw new CompileError(
4947
4948
  `${where}: vertexCount is ${count}, which is not a multiple of 3. A path's vertices are knots AND their ` +
4948
4949
  'Bezier handles, read in groups of three; the parser sizes its lengths array with `vertexCount / 3` and ' +
4949
- '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',
4950
4951
  );
4951
4952
  }
4952
4953
  const minimum = closed ? 3 : 6;
4953
4954
  if (count < minimum) {
4954
4955
  throw new CompileError(
4955
4956
  `${where}: vertexCount is ${count} and ${closed ? 'a closed' : 'an open'} path needs at least ${minimum} — ` +
4956
- `${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',
4957
4958
  );
4958
4959
  }
4959
4960
  const entries = count / 3;
@@ -4962,7 +4963,7 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4962
4963
  lengths = statedPathLengths(att.lengths, entries, closed, where);
4963
4964
  } else {
4964
4965
  const anchor = ctx.transforms.get(ctx.anchorBone);
4965
- 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');
4966
4967
  const points = setupWorldVertices(vertices, anchor, ctx.transforms, where);
4967
4968
  lengths = pathCurveLengths(pathChain(points, true));
4968
4969
  // The total the runtime reads is the last CURVE's entry, `lengths[vertexCount
@@ -4972,7 +4973,7 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
4972
4973
  if (lengths.length !== entries || !lengths.every((n) => Number.isFinite(n)) || !(total > 0)) {
4973
4974
  throw new CompileError(
4974
4975
  `${where}: the geometry measures ${lengths.length} entry(ies) where the parser sizes ${entries}, of total ` +
4975
- `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',
4976
4977
  );
4977
4978
  }
4978
4979
  }
@@ -5008,25 +5009,25 @@ function buildRigPath(att: RigPathAttachment, where: string, ctx: AttachmentCont
5008
5009
  */
5009
5010
  function statedPathLengths(stated: unknown, entries: number, closed: boolean, where: string): number[] {
5010
5011
  if (!Array.isArray(stated)) {
5011
- 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');
5012
5013
  }
5013
5014
  if (stated.length !== entries) {
5014
5015
  throw new CompileError(
5015
5016
  `${where}: "lengths" has ${stated.length} entry(ies) where the parser sizes ${entries} — vertexCount / 3 on ` +
5016
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'}. ` +
5017
- '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',
5018
5019
  );
5019
5020
  }
5020
5021
  let previous = 0;
5021
5022
  for (let i = 0; i < stated.length; i++) {
5022
5023
  const value: unknown = stated[i];
5023
5024
  if (typeof value !== 'number' || !Number.isFinite(value)) {
5024
- 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');
5025
5026
  }
5026
5027
  if (value < previous) {
5027
5028
  throw new CompileError(
5028
5029
  `${where}: "lengths"[${i}] is ${value}, below ${i === 0 ? '0' : `the ${previous} before it`}. The array is CUMULATIVE — the length at ` +
5029
- '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',
5030
5031
  );
5031
5032
  }
5032
5033
  previous = value;
@@ -5067,7 +5068,7 @@ function atlasedImage(image: string, where: string, ctx: AttachmentContext): Com
5067
5068
  'measured and atlased — one per file, whichever skin names it (issue #555) — so reaching this means rigc ' +
5068
5069
  'skipped one, not that the spec left anything out. ' +
5069
5070
  (near.length ? `The nearest region(s) built are ${near.map((n) => JSON.stringify(n)).join(', ')}. ` : '') +
5070
- `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',
5071
5072
  );
5072
5073
  }
5073
5074
 
@@ -5214,7 +5215,7 @@ function sequenceFrameSize(
5214
5215
  throw new CompileError(
5215
5216
  `${where}: the ${frames.length} frames of this sequence measure ${sizes.join(', ')} in ${field} ` +
5216
5217
  `(${frames.map((img) => `"${img.region}" ${img[field]}`).join(', ')}), and an attachment has one ` +
5217
- `${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',
5218
5219
  );
5219
5220
  }
5220
5221
  }
@@ -5239,7 +5240,7 @@ function sequenceFrames(att: { path?: string; sequence?: RigSequence }, placehol
5239
5240
  throw new CompileError(
5240
5241
  `${where}: sequence frame ${i} is the region "${region}", which was never added to the atlas — every frame ` +
5241
5242
  'of a sequence is atlased or refused by name before any attachment is built, so reaching this means rigc ' +
5242
- 'skipped one',
5243
+ 'skipped one', 'CE_COMPILE_INTERNAL_SEQUENCE_FRAME_NOT_ATLASED',
5243
5244
  );
5244
5245
  }
5245
5246
  frames.push(img);
@@ -5294,7 +5295,7 @@ function buildRigRegion(
5294
5295
  ? ''
5295
5296
  : ` in the page's texels, which its scale: ${img.atlasScale} makes a ` +
5296
5297
  `${img.width}x${img.height} drawing (+/- ${f32(0.5 / img.atlasScale)}px, the pack's rounding)`) +
5297
- ')',
5298
+ ')', 'CE_COMPILE_REGION_ATLAS_SIZE_MISMATCH',
5298
5299
  );
5299
5300
  }
5300
5301
  }
@@ -5305,7 +5306,7 @@ function buildRigRegion(
5305
5306
  // No parser default: an omission loads as NaN and every UV collapses, with
5306
5307
  // no error at all. So it is this or nothing.
5307
5308
  throw new CompileError(
5308
- `${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',
5309
5310
  );
5310
5311
  }
5311
5312
  const out: ModelRegionAttachment = { kind: 'region', width: f32(width), height: f32(height) };
@@ -5389,12 +5390,12 @@ function regionAtlasRect(
5389
5390
  function bindNamedWeights(weights: RigMeshBinding[][], where: string, ctx: AttachmentContext): ModelBinding[][] {
5390
5391
  return weights.map((vertex, i) => {
5391
5392
  if (!Array.isArray(vertex) || vertex.length === 0) {
5392
- 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');
5393
5394
  }
5394
5395
  return vertex.map((binding) => {
5395
5396
  if (!ctx.bones.some((b) => b.name === binding.bone)) {
5396
5397
  throw new CompileError(
5397
- `${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',
5398
5399
  );
5399
5400
  }
5400
5401
  return { bone: binding.bone, x: f32(binding.x), y: f32(binding.y), weight: f32(binding.weight) };
@@ -5499,6 +5500,7 @@ function authoredHullAndEdges(
5499
5500
  vertexCount: number,
5500
5501
  triangles: number[],
5501
5502
  where: string,
5503
+ named: { skin: string; slot: string; placeholder: string },
5502
5504
  ): { hull: number; edges: number[] } {
5503
5505
  try {
5504
5506
  const outline = traceOutline(vertexCount, triangles);
@@ -5511,11 +5513,31 @@ function authoredHullAndEdges(
5511
5513
  checkHullOrder(outline, vertexCount);
5512
5514
  return { hull: outline.hull, edges: att.edges ?? meshEdges(vertexCount, triangles, outline.hull) };
5513
5515
  } catch (err) {
5514
- if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}`);
5516
+ if (err instanceof MeshError) throw new CompileError(`${where}: ${err.message}${splitPointer(vertexCount, triangles, named)}`, 'CE_COMPILE_AUTHORED_MESH_OUTLINE_INVALID');
5515
5517
  throw err;
5516
5518
  }
5517
5519
  }
5518
5520
 
5521
+ /**
5522
+ * The command that answers an outline refusal whose cause is pieces that share
5523
+ * no vertex (issue #1414): one mesh is one outline, so the fix is one mesh per
5524
+ * piece, and `rigc split` writes that proposal. Empty for every other cause —
5525
+ * a pinch, a hole, a doubled triangle — whose fix is the triangles, and for a
5526
+ * list the refusal is about because its indices are not vertices at all.
5527
+ */
5528
+ function splitPointer(vertexCount: number, triangles: readonly number[], named: { skin: string; slot: string; placeholder: string }): string {
5529
+ if (triangles.length % 3 !== 0 || triangles.some((v) => !Number.isInteger(v) || v < 0 || v >= vertexCount)) return '';
5530
+ const count = meshPieces(vertexCount, triangles).pieces.length;
5531
+ if (count < 2) return '';
5532
+ // Each name one shell word (issue #1442): the command is copied into a shell, and a bare `left legs` is two arguments.
5533
+ const skin = named.skin === 'default' ? '' : ` --skin ${shellWord(named.skin)}`;
5534
+ return (
5535
+ ` — the triangles are ${count} pieces that share no vertex, and one mesh is one outline: ` +
5536
+ `\`rigc split --rig <rig> --motion <motion> --slot ${shellWord(named.slot)} --attachment ${shellWord(named.placeholder)}${skin} --out <dir>\` ` +
5537
+ 'writes one mesh per piece, its deform keys renumbered (AUTHORING §3.4.11)'
5538
+ );
5539
+ }
5540
+
5519
5541
  /**
5520
5542
  * The same two fields for geometry rigc built. The generators already put the
5521
5543
  * outline first and in order — a contour is all outline, a ring's outer ring
@@ -5532,7 +5554,7 @@ function generatedHullAndEdges(geometry: MeshGeometry, where: string): { hull: n
5532
5554
  checkHullOrder(outline, vertexCount);
5533
5555
  } catch (err) {
5534
5556
  if (err instanceof MeshError) {
5535
- 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');
5536
5558
  }
5537
5559
  throw err;
5538
5560
  }
@@ -5548,27 +5570,27 @@ function buildRigMesh(
5548
5570
  const authored =
5549
5571
  att.uvs !== undefined || att.triangles !== undefined || att.vertices !== undefined || att.weights !== undefined;
5550
5572
  if (authored && att.generator) {
5551
- 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');
5552
5574
  }
5553
5575
  if (att.generator) return buildGeneratedMesh(att, att.generator, placeholder, where, ctx);
5554
5576
  if (att.vertices && att.weights) {
5555
5577
  throw new CompileError(
5556
- `${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',
5557
5579
  );
5558
5580
  }
5559
5581
  if (!att.uvs || !att.triangles || !(att.vertices || att.weights)) {
5560
- 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');
5561
5583
  }
5562
5584
  const uvCount = att.uvs.length;
5563
5585
  let vertices: ModelVertices;
5564
5586
  let boundBones: string[] = [];
5565
5587
  if (att.weights) {
5566
5588
  if (att.boneIndexing === 'raw') {
5567
- 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');
5568
5590
  }
5569
5591
  if (att.weights.length !== uvCount / 2) {
5570
5592
  throw new CompileError(
5571
- `${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',
5572
5594
  );
5573
5595
  }
5574
5596
  vertices = { weighted: true, bindings: bindNamedWeights(att.weights, where, ctx) };
@@ -5583,7 +5605,7 @@ function buildRigMesh(
5583
5605
  `${where}: this mesh's "vertices" is a weighted run, whose bone INDEXES point into the emitted bone array — ` +
5584
5606
  'a list the spec never writes, so inserting a bone rebinds every vertex in silence. ' +
5585
5607
  'Give the bindings by name as "weights": [[{ "bone": …, "x": …, "y": …, "weight": … }, …], …], ' +
5586
- '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',
5587
5609
  );
5588
5610
  }
5589
5611
  if (weighted) {
@@ -5599,7 +5621,7 @@ function buildRigMesh(
5599
5621
  vertices = { weighted: false, xy: raw.map(f32) };
5600
5622
  }
5601
5623
  }
5602
- const { hull, edges } = authoredHullAndEdges(att, uvCount / 2, att.triangles, where);
5624
+ const { hull, edges } = authoredHullAndEdges(att, uvCount / 2, att.triangles, where, { skin: ctx.skinName, slot: ctx.slotName, placeholder });
5603
5625
  // `width`/`height` are the size of the image the mesh is drawn on. Stated
5604
5626
  // wins; otherwise the PNG the attachment names, already measured for the
5605
5627
  // atlas — the same `CompiledImage.width` a region reads, so a page with a
@@ -5613,7 +5635,7 @@ function buildRigMesh(
5613
5635
  const height = seqSize !== null ? seqSize.height : (att.height ?? img?.height);
5614
5636
  if (width === undefined || height === undefined) {
5615
5637
  throw new CompileError(
5616
- `${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',
5617
5639
  );
5618
5640
  }
5619
5641
  const out: ModelMeshAttachment = {
@@ -5692,7 +5714,7 @@ function buildRigLinkedMesh(
5692
5714
  'no geometry of its own — it draws the geometry of the attachment "source" names. The parser returns from ' +
5693
5715
  'the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so these keys are read by nothing ' +
5694
5716
  "at all: a link declaring 5 uvs beside a 4-vertex source loads the SOURCE's 4 vertices and says nothing. " +
5695
- `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',
5696
5718
  );
5697
5719
  }
5698
5720
  const source = att.source;
@@ -5702,7 +5724,7 @@ function buildRigLinkedMesh(
5702
5724
  `attachment is filed under in its skin, not its "name"), and this one states ${JSON.stringify(source) ?? String(source)}. ` +
5703
5725
  '"source" is what MAKES a mesh linked: `getValue(map, "source", null)` is falsy-tested ' +
5704
5726
  '(`SkeletonJson.ts:582`), so an absent or empty one is read as an ordinary mesh, whose `uvs` this ' +
5705
- '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',
5706
5728
  );
5707
5729
  }
5708
5730
  if (att.slot !== undefined && !ctx.slotNames.has(att.slot)) {
@@ -5710,7 +5732,7 @@ function buildRigLinkedMesh(
5710
5732
  `${where}: "slot" is ${JSON.stringify(att.slot)}, which the rig does not declare as a slot. It names where ` +
5711
5733
  'the source lives and is resolved through `skeletonData.findSlot` (`SkeletonJson.ts:575`), which throws ' +
5712
5734
  `\`Source mesh slot not found\` on a miss. The rig's slots are ${[...ctx.slotNames].sort().map((s) => JSON.stringify(s)).join(', ')}. ` +
5713
- "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',
5714
5736
  );
5715
5737
  }
5716
5738
  ctx.links.push({
@@ -5729,7 +5751,7 @@ function buildRigLinkedMesh(
5729
5751
  const height = seqSize !== null ? seqSize.height : (att.height ?? img?.height);
5730
5752
  if (width === undefined || height === undefined) {
5731
5753
  throw new CompileError(
5732
- `${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',
5733
5755
  );
5734
5756
  }
5735
5757
  // The link in full — skin, slot and `timelines` as the parser resolves them.
@@ -5791,7 +5813,7 @@ function resolveLinkedMeshes(
5791
5813
  `${link.where}: no "skin" is stated, so the parser looks for the source "${link.source}" in the default ` +
5792
5814
  `skin — and this rig declares no default skin. The rig's skins are ${skinNames.map((s) => JSON.stringify(s)).join(', ')}. ` +
5793
5815
  'State the skin the source is filed under. Left to the round trip this is a `TypeError` off the null ' +
5794
- '`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',
5795
5817
  );
5796
5818
  }
5797
5819
  if (table === undefined) {
@@ -5799,7 +5821,7 @@ function resolveLinkedMeshes(
5799
5821
  `${link.where}: "skin" is ${JSON.stringify(link.skin)}, and the rig declares no such skin. The rig's skins ` +
5800
5822
  `are ${skinNames.map((s) => JSON.stringify(s)).join(', ')}. ` +
5801
5823
  "Left to the round trip this is the runtime's `Skin not found`, which names neither this attachment nor " +
5802
- 'where it was looking.',
5824
+ 'where it was looking.', 'CE_COMPILE_LINKED_MESH_SKIN_UNKNOWN',
5803
5825
  );
5804
5826
  }
5805
5827
  const slot = table[link.slot];
@@ -5813,7 +5835,7 @@ function resolveLinkedMeshes(
5813
5835
  `holds ${held.length === 0 ? 'no attachment at all' : `${held.length}: ${held.map((k) => JSON.stringify(k)).join(', ')}`}. ` +
5814
5836
  '"source" is the PLACEHOLDER the source is filed under — `skin.getAttachment(slotIndex, source)` ' +
5815
5837
  "(`SkeletonJson.ts:433`), whose table is keyed by the JSON key, not by the attachment's `name`. " +
5816
- "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',
5817
5839
  );
5818
5840
  }
5819
5841
  const type = found.kind;
@@ -5823,7 +5845,7 @@ function resolveLinkedMeshes(
5823
5845
  'them is refused. Measured through spine-core: the runtime resolves links in the order they were read, ' +
5824
5846
  "so a link whose source is a link loads the source's geometry when the source comes first in the file " +
5825
5847
  'and loads NOTHING — `worldVerticesLength` 0, 0 triangles, a 0x0 size — when the two are swapped, in ' +
5826
- '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',
5827
5849
  );
5828
5850
  }
5829
5851
  if (type !== 'mesh') {
@@ -5831,7 +5853,7 @@ function resolveLinkedMeshes(
5831
5853
  `${link.where}: "source" is ${JSON.stringify(link.source)}, which is a ${JSON.stringify(type)} attachment ` +
5832
5854
  "and not a mesh. A linked mesh takes another MESH's `uvs`, `triangles`, `vertices`, `hull` and `edges` " +
5833
5855
  '(`MeshAttachment.setSourceMesh`); the runtime casts whatever it finds and reads those fields off it, ' +
5834
- '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',
5835
5857
  );
5836
5858
  }
5837
5859
  }
@@ -5863,9 +5885,9 @@ function buildGeneratedMesh(
5863
5885
  // Two readings of one bone (issue #1021): the runtime's matrices to bind
5864
5886
  // through, and the exact frame to measure the control angles in.
5865
5887
  const resolve = (name: string, frames: Map<string, BoneTransform> = ctx.transforms): BoneTransform => {
5866
- 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');
5867
5889
  const transform = frames.get(name);
5868
- 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');
5869
5891
  return transform;
5870
5892
  };
5871
5893
  const refFor = (name: string): MeshBoneRef => {
@@ -5879,7 +5901,7 @@ function buildGeneratedMesh(
5879
5901
  // angle about the aperture means anything (issue #684).
5880
5902
  const [w, h] = generator.size;
5881
5903
  const anchor = ctx.transforms.get(ctx.anchorBone);
5882
- 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');
5883
5905
  const place = (px: number, py: number): [number, number] => [anchor.worldX + px - w / 2, anchor.worldY + h / 2 - py];
5884
5906
  // The inverse a control angle is measured through, in the exact frame: the
5885
5907
  // anchor's and the control's positions as the spec put them, not as the
@@ -5903,7 +5925,7 @@ function buildGeneratedMesh(
5903
5925
  }),
5904
5926
  });
5905
5927
  } catch (err) {
5906
- 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');
5907
5929
  throw err;
5908
5930
  }
5909
5931
  const vertices = bindWeightedVertices(
@@ -6046,12 +6068,12 @@ function sampleMeshDepth(
6046
6068
  summary: NonNullable<CompileResult['meshes'][number]['depth']>;
6047
6069
  } {
6048
6070
  if (typeof spec.image !== 'string' || spec.image.length === 0) {
6049
- 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');
6050
6072
  }
6051
6073
  if (spec.near !== 'white' && spec.near !== 'black') {
6052
6074
  throw new CompileError(
6053
6075
  `${where}: the depth block says "near": ${JSON.stringify(spec.near)}; it is "white" or "black". Both ` +
6054
- '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',
6055
6077
  );
6056
6078
  }
6057
6079
  const tone: DepthTone = {
@@ -6063,7 +6085,7 @@ function sampleMeshDepth(
6063
6085
  checkTone(tone, where);
6064
6086
  checkZScale(spec.zScale, where);
6065
6087
  } catch (err) {
6066
- 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');
6067
6089
  throw err;
6068
6090
  }
6069
6091
 
@@ -6135,7 +6157,7 @@ function sampleMeshDepth(
6135
6157
  : ` in the page's texels, pixel (${f32(points[first][0] * toSheetX)}, ${f32(points[first][1] * toSheetY)}) of ` +
6136
6158
  'the drawing-sized sheet') +
6137
6159
  `. ${why} ` +
6138
- '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',
6139
6161
  );
6140
6162
  }
6141
6163
 
@@ -6174,8 +6196,8 @@ function sampleMeshDepth(
6174
6196
  /** One bone of the rig as `bindWeightedVertices` wants it: by name, with its setup inverse. */
6175
6197
  function meshBoneRef(name: string, where: string, ctx: AttachmentContext): MeshBoneRef {
6176
6198
  const transform = ctx.transforms.get(name);
6177
- if (!transform) throw new CompileError(`${where}: bone "${name}" has no setup transform`);
6178
- 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');
6179
6201
  return { name, toBind: (wx, wy) => toBoneLocal(transform, wx, wy) };
6180
6202
  }
6181
6203
 
@@ -6250,11 +6272,11 @@ function readGreySheet(
6250
6272
  const sheetHeight = scaled === undefined ? grid.partHeight : scaled.height;
6251
6273
  const noun = purpose === 'depth' ? 'depth map' : 'soft mask';
6252
6274
  if (typeof image !== 'string' || image.length === 0) {
6253
- 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');
6254
6276
  }
6255
6277
  const path = join(ctx.imagesDir, image);
6256
6278
  if (!existsSync(path)) {
6257
- 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');
6258
6280
  }
6259
6281
  const sheet = readPlate(path);
6260
6282
  if (sheet.width !== sheetWidth || sheet.height !== sheetHeight) {
@@ -6268,7 +6290,7 @@ function readGreySheet(
6268
6290
  `declares scale: ${scaled.scale}, which makes a texel ${f32(1 / scaled.scale)}px of the drawing. It is ` +
6269
6291
  "sampled in the part's own pixel grid, which is the drawing's, at each vertex's texel position over " +
6270
6292
  `that scale — so the sheet is ${scaled.width}x${scaled.height}, the size of the loose art, whatever the ` +
6271
- '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',
6272
6294
  );
6273
6295
  }
6274
6296
  // One channel, proved rather than assumed.
@@ -6283,7 +6305,7 @@ function readGreySheet(
6283
6305
  throw new CompileError(
6284
6306
  `${where}: the ${noun} "${image}" is not greyscale — pixel (${i % sheet.width}, ${Math.floor(i / sheet.width)}) ` +
6285
6307
  `is rgb(${r}, ${g}, ${b}). It is one channel; reading red out of a colour file would use whatever that ` +
6286
- 'channel happened to hold.',
6308
+ 'channel happened to hold.', 'CE_COMPILE_GREY_SHEET_NOT_GREYSCALE',
6287
6309
  );
6288
6310
  }
6289
6311
  level[i] = r;
@@ -6317,20 +6339,20 @@ function softRegionWeights(
6317
6339
  ): { weights: MeshVertexWeight[][]; levels: number[]; bone: string; mask: string; digest: string; carried: number; ramped: number } {
6318
6340
  const { bone } = spec;
6319
6341
  if (typeof bone !== 'string' || bone.length === 0) {
6320
- 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');
6321
6343
  }
6322
6344
  if (!ctx.bones.some((b) => b.name === bone)) {
6323
6345
  throw new CompileError(
6324
6346
  `${where}: "soft" names bone "${bone}", which this rig does not declare. rigc binds a region to a bone that ` +
6325
6347
  'already exists rather than creating one — a bone a physics constraint has to target is part of the ' +
6326
- 'skeleton, not a side effect of a mesh.',
6348
+ 'skeleton, not a side effect of a mesh.', 'CE_COMPILE_SOFT_BONE_UNKNOWN',
6327
6349
  );
6328
6350
  }
6329
6351
  if (bone === ctx.anchorBone) {
6330
6352
  throw new CompileError(
6331
6353
  `${where}: "soft" names "${bone}", which is this slot's own bone. Carrying the region with the bone the rest ` +
6332
6354
  'of the mesh is already pinned to moves nothing — a soft region needs a bone that can move independently, ' +
6333
- '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',
6334
6356
  );
6335
6357
  }
6336
6358
  const { map: sheet } = readGreySheet(spec.mask, 'soft', grid, where, ctx);
@@ -6355,7 +6377,7 @@ function softRegionWeights(
6355
6377
  throw new CompileError(
6356
6378
  `${where}: the soft mask "${spec.mask}" carries no vertex of this mesh — every one of its ${points.length} ` +
6357
6379
  'vertices samples black. A region that moves nothing is a physics constraint with nothing on the end of it; ' +
6358
- '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',
6359
6381
  );
6360
6382
  }
6361
6383
  return { weights, levels, bone, mask: spec.mask, digest: depthDigest(sheet), carried, ramped };
@@ -6387,7 +6409,7 @@ function buildGridAttachment(
6387
6409
  if (att.image === undefined) {
6388
6410
  throw new CompileError(
6389
6411
  `${where}: a "grid" generator lays its lattice over the part's own window, so the attachment needs an ` +
6390
- '"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',
6391
6413
  );
6392
6414
  }
6393
6415
  const stated = generator.us !== undefined || generator.vs !== undefined;
@@ -6396,18 +6418,18 @@ function buildGridAttachment(
6396
6418
  throw new CompileError(
6397
6419
  `${where}: the "grid" generator states both positions ("us"/"vs") and a count ("cols"/"rows"). They are two ` +
6398
6420
  'answers to where the columns go, and a spec carrying both leaves a reader unable to say which one built ' +
6399
- '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',
6400
6422
  );
6401
6423
  }
6402
6424
  if (!stated && !even) {
6403
6425
  throw new CompileError(
6404
6426
  `${where}: the "grid" generator says neither where its columns go ("us"/"vs", fractions of the window) nor ` +
6405
- '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',
6406
6428
  );
6407
6429
  }
6408
6430
  const spread = (n: unknown, name: string): number[] => {
6409
6431
  if (typeof n !== 'number' || !Number.isInteger(n) || n < 2) {
6410
- 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');
6411
6433
  }
6412
6434
  // An even division spans the whole window. A lattice that should sit inside
6413
6435
  // it states its own positions — which is what `us`/`vs` are for.
@@ -6418,7 +6440,7 @@ function buildGridAttachment(
6418
6440
  if (us === undefined || vs === undefined) {
6419
6441
  throw new CompileError(
6420
6442
  `${where}: the "grid" generator states ${us === undefined ? '"vs"' : '"us"'} and not ` +
6421
- `${us === undefined ? '"us"' : '"vs"'}; a lattice needs both axes`,
6443
+ `${us === undefined ? '"us"' : '"vs"'}; a lattice needs both axes`, 'CE_COMPILE_GRID_AXIS_MISSING',
6422
6444
  );
6423
6445
  }
6424
6446
 
@@ -6429,7 +6451,7 @@ function buildGridAttachment(
6429
6451
  try {
6430
6452
  geometry = buildGridMesh({ size: [plate.width, plate.height], us, vs });
6431
6453
  } catch (err) {
6432
- 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');
6433
6455
  throw err;
6434
6456
  }
6435
6457
 
@@ -6437,16 +6459,16 @@ function buildGridAttachment(
6437
6459
  const h = img.height;
6438
6460
  const toArt = w / plate.width;
6439
6461
  if (att.width !== undefined && att.width !== w) {
6440
- 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');
6441
6463
  }
6442
6464
  if (att.height !== undefined && att.height !== h) {
6443
- 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');
6444
6466
  }
6445
6467
  const anchor = ctx.transforms.get(ctx.anchorBone);
6446
6468
  const frame = ctx.exactFrames.get(ctx.anchorBone);
6447
- 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');
6448
6470
  if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
6449
- 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');
6450
6472
  }
6451
6473
  // Sampled BEFORE the encode, because `bind` rewrites the weights the encode
6452
6474
  // then writes out.
@@ -6537,21 +6559,21 @@ function segmentsFalloffOf(f: RigSegmentsFalloff, where: string): SegmentsFallof
6537
6559
  if (typeof f.radius !== 'number' || !Number.isFinite(f.radius) || !(f.radius > 0)) {
6538
6560
  throw new CompileError(
6539
6561
  `${where}: "falloff.radius" is ${JSON.stringify(f.radius)}; it is added to every distance before the power is ` +
6540
- "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',
6541
6563
  );
6542
6564
  }
6543
6565
  const power = f.power ?? SEGMENTS_DEFAULTS.power;
6544
6566
  if (typeof power !== 'number' || !Number.isFinite(power) || !(power > 0)) {
6545
6567
  throw new CompileError(
6546
6568
  `${where}: "falloff.power" is ${JSON.stringify(power)}; it is the exponent a pull falls off with distance by, ` +
6547
- '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',
6548
6570
  );
6549
6571
  }
6550
6572
  const maxBones = f.maxBones ?? SEGMENTS_DEFAULTS.maxBones;
6551
6573
  if (typeof maxBones !== 'number' || !Number.isInteger(maxBones) || maxBones < 1) {
6552
6574
  throw new CompileError(
6553
6575
  `${where}: "falloff.maxBones" is ${JSON.stringify(maxBones)}; it is how many bones may pull one vertex, a ` +
6554
- 'whole number of at least 1',
6576
+ 'whole number of at least 1', 'CE_COMPILE_SEGMENTS_FALLOFF_MAX_BONES_INVALID',
6555
6577
  );
6556
6578
  }
6557
6579
  const minWeight = f.minWeight ?? SEGMENTS_DEFAULTS.minWeight;
@@ -6560,7 +6582,7 @@ function segmentsFalloffOf(f: RigSegmentsFalloff, where: string): SegmentsFallof
6560
6582
  `${where}: "falloff.minWeight" is ${JSON.stringify(minWeight)}; it is the share under which a bone's pull ` +
6561
6583
  `is dropped, so it lies in ${SEGMENTS_WEIGHT_STEP}..1 with 1 excluded — at 1 or above every share is dropped, ` +
6562
6584
  `and under ${SEGMENTS_WEIGHT_STEP}, one step of the 6-decimal grid weights are written on, a kept binding ` +
6563
- 'could be written as 0',
6585
+ 'could be written as 0', 'CE_COMPILE_SEGMENTS_FALLOFF_MIN_WEIGHT_INVALID',
6564
6586
  );
6565
6587
  }
6566
6588
  return { power, radius: f.radius, maxBones, minWeight };
@@ -6581,12 +6603,12 @@ function segmentsInfluenceOf(
6581
6603
  throw new CompileError(
6582
6604
  `${where}: the "segments" generator states both "falloff" and "influence". Both turn a distance into a ` +
6583
6605
  'share — "falloff" as 1 / (d + radius)^power over every segment, "influence" as a pull that reaches zero at ' +
6584
- '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',
6585
6607
  );
6586
6608
  }
6587
6609
  if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
6588
6610
  throw new CompileError(
6589
- `${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',
6590
6612
  );
6591
6613
  }
6592
6614
  const i = spec as Partial<Record<keyof SegmentsInfluence, unknown>>;
@@ -6594,20 +6616,20 @@ function segmentsInfluenceOf(
6594
6616
  throw new CompileError(
6595
6617
  `${where}: "influence.radius" is ${JSON.stringify(i.radius) ?? 'absent'}; it is how far a link reaches, in ` +
6596
6618
  "the part's pixels, a positive number with no default — a length on this art: a link whose segment is that " +
6597
- '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',
6598
6620
  );
6599
6621
  }
6600
6622
  if (typeof i.across !== 'boolean') {
6601
6623
  throw new CompileError(
6602
6624
  `${where}: "influence.across" is ${JSON.stringify(i.across) ?? 'absent'}; it is true (every link within ` +
6603
6625
  '"radius" pulls, across the entries of "bones") or false (only the links of the entry nearest the vertex), ' +
6604
- "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',
6605
6627
  );
6606
6628
  }
6607
6629
  if (typeof i.max !== 'number' || !Number.isInteger(i.max) || i.max < 1) {
6608
6630
  throw new CompileError(
6609
6631
  `${where}: "influence.max" is ${JSON.stringify(i.max) ?? 'absent'}; it is how many links may pull one vertex, ` +
6610
- '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',
6611
6633
  );
6612
6634
  }
6613
6635
  const shape = RIG_INFLUENCE_SHAPES.find((name) => name === i.shape);
@@ -6615,7 +6637,7 @@ function segmentsInfluenceOf(
6615
6637
  throw new CompileError(
6616
6638
  `${where}: "influence.shape" is ${JSON.stringify(i.shape) ?? 'absent'}; one of ` +
6617
6639
  `${RIG_INFLUENCE_SHAPES.map((name) => JSON.stringify(name)).join(', ')} — how a pull falls from 1 on a link ` +
6618
- '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',
6619
6641
  );
6620
6642
  }
6621
6643
  return { radius: i.radius, across: i.across, max: i.max, shape };
@@ -6649,7 +6671,7 @@ function buildSegmentsAttachment(
6649
6671
  if (att.image === undefined) {
6650
6672
  throw new CompileError(
6651
6673
  `${where}: a "segments" generator lays its lattice over the part's own alpha, so the attachment needs an ` +
6652
- '"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',
6653
6675
  );
6654
6676
  }
6655
6677
  const img = atlasedImage(att.image, where, ctx);
@@ -6657,7 +6679,7 @@ function buildSegmentsAttachment(
6657
6679
  throw new CompileError(
6658
6680
  `${where}: a "segments" generator lays its lattice over the part's own alpha, and "${att.image}" is lifted ` +
6659
6681
  'off a packed page at the coordinates the atlas states, which on this file are not where its texels are — ' +
6660
- `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',
6661
6683
  );
6662
6684
  }
6663
6685
  if (img.atlasScale !== undefined) {
@@ -6665,7 +6687,7 @@ function buildSegmentsAttachment(
6665
6687
  `${where}: a "segments" generator measures its "cell" and its falloff "radius" in the part's pixels, and ` +
6666
6688
  `"${att.image}" sits on a page that declares scale: ${img.atlasScale}, where a pixel of the drawing is not a ` +
6667
6689
  'texel of the page. This generator does not convert between the two; build it from the loose part, or ' +
6668
- 'from a pack at scale 1',
6690
+ 'from a pack at scale 1', 'CE_COMPILE_SEGMENTS_ATLAS_SCALE_UNSUPPORTED',
6669
6691
  );
6670
6692
  }
6671
6693
 
@@ -6677,7 +6699,7 @@ function buildSegmentsAttachment(
6677
6699
  `${where}: the "segments" generator states no "falloff" block. Its "radius" — added to every distance, in ` +
6678
6700
  "the part's pixels — has no default, because it is a length on this art: " +
6679
6701
  '"falloff": { "radius": <pixels> } is the least it takes (or an "influence" block, whose pull reaches zero ' +
6680
- 'at its own "radius")',
6702
+ 'at its own "radius")', 'CE_COMPILE_SEGMENTS_WEIGHT_MODE_MISSING',
6681
6703
  );
6682
6704
  }
6683
6705
  const falloff = influence === null ? segmentsFalloffOf(generator.falloff!, where) : null;
@@ -6693,16 +6715,16 @@ function buildSegmentsAttachment(
6693
6715
  cell: generator.cell,
6694
6716
  });
6695
6717
  } catch (err) {
6696
- 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');
6697
6719
  throw err;
6698
6720
  }
6699
6721
  const w = img.width;
6700
6722
  const h = img.height;
6701
6723
  if (att.width !== undefined && att.width !== w) {
6702
- 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');
6703
6725
  }
6704
6726
  if (att.height !== undefined && att.height !== h) {
6705
- 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');
6706
6728
  }
6707
6729
 
6708
6730
  // -- the placement ---------------------------------------------------------
@@ -6711,14 +6733,14 @@ function buildSegmentsAttachment(
6711
6733
  // MEASURED in — distances, falloff, `minWeight`, the shared-origin refusal —
6712
6734
  // and the runtime's is what the vertices are BOUND through (`place`).
6713
6735
  const exactSlotBone = ctx.exactFrames.get(ctx.anchorBone);
6714
- 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');
6715
6737
  let anchorAt: [number, number] = [w / 2, h / 2];
6716
6738
  if (generator.anchor !== undefined) {
6717
6739
  const stated = finitePair(generator.anchor);
6718
6740
  if (stated === null) {
6719
6741
  throw new CompileError(
6720
6742
  `${where}: "anchor" is ${JSON.stringify(generator.anchor)}; it is where the slot bone sits in the part's ` +
6721
- 'pixels, y down, as [x, y] — two finite numbers',
6743
+ 'pixels, y down, as [x, y] — two finite numbers', 'CE_COMPILE_SEGMENTS_ANCHOR_INVALID',
6722
6744
  );
6723
6745
  }
6724
6746
  anchorAt = stated;
@@ -6736,7 +6758,7 @@ function buildSegmentsAttachment(
6736
6758
  throw new CompileError(
6737
6759
  `${where}: the "segments" generator names no bones. "bones" is the one decision this generator takes — which ` +
6738
6760
  'segments may pull the part — so it lists at least one: a bone name, a chain as a list of bone names, or ' +
6739
- '{ "bone", "from", "to" }',
6761
+ '{ "bone", "from", "to" }', 'CE_COMPILE_SEGMENTS_BONES_EMPTY',
6740
6762
  );
6741
6763
  }
6742
6764
  const parentOf = new Map(ctx.bones.map((b) => [b.name, b.parent ?? null]));
@@ -6745,7 +6767,7 @@ function buildSegmentsAttachment(
6745
6767
  const segments: EntrySegment[] = [];
6746
6768
  const boneOf = (name: unknown, at: string): { index: number; transform: BoneTransform } => {
6747
6769
  if (typeof name !== 'string' || !parentOf.has(name)) {
6748
- 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');
6749
6771
  }
6750
6772
  let index = named.indexOf(name);
6751
6773
  if (index < 0) {
@@ -6763,7 +6785,7 @@ function buildSegmentsAttachment(
6763
6785
  'so it has an origin and no segment. Give the bone a "length" (its segment runs from its origin to that ' +
6764
6786
  'tip, along its setup rotation), ' +
6765
6787
  (chained ? 'end the chain on a bone that has one, ' : 'name it inside a chain whose next link says where it ends, ') +
6766
- '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',
6767
6789
  );
6768
6790
  }
6769
6791
  return toWorld(transform, length, 0);
@@ -6777,7 +6799,7 @@ function buildSegmentsAttachment(
6777
6799
  }
6778
6800
  if (Array.isArray(entry)) {
6779
6801
  if (entry.length === 0) {
6780
- 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');
6781
6803
  }
6782
6804
  const links = entry.map((name: unknown, k: number) => ({ name, ...boneOf(name, `${at}[${k}]`) }));
6783
6805
  links.forEach((link, k) => {
@@ -6795,7 +6817,7 @@ function buildSegmentsAttachment(
6795
6817
  throw new CompileError(
6796
6818
  `${where}: ${at} chains "${name}" to "${nextName}", and "${nextName}" is not under "${name}" in the bone ` +
6797
6819
  `tree — a chain runs down the skeleton, each link a descendant of the one before it. List the chain ` +
6798
- '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',
6799
6821
  );
6800
6822
  }
6801
6823
  const b: [number, number] = [next.transform.worldX, next.transform.worldY];
@@ -6803,7 +6825,7 @@ function buildSegmentsAttachment(
6803
6825
  throw new CompileError(
6804
6826
  `${where}: ${at} chains "${name}" to "${nextName}", and the two bones share their setup origin ` +
6805
6827
  `(${origin[0]}, ${origin[1]}), so the link has no length and no direction to weight along. Give ` +
6806
- `"${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',
6807
6829
  );
6808
6830
  }
6809
6831
  segments.push({ bone: link.index, entry: i, a: origin, b });
@@ -6818,7 +6840,7 @@ function buildSegmentsAttachment(
6818
6840
  if (from === null || to === null) {
6819
6841
  throw new CompileError(
6820
6842
  `${where}: ${at} states a segment whose ${from === null ? '"from"' : '"to"'} is ` +
6821
- `${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',
6822
6844
  );
6823
6845
  }
6824
6846
  segments.push({ bone: index, entry: i, a: measuredAt(from[0], from[1]), b: measuredAt(to[0], to[1]) });
@@ -6826,7 +6848,7 @@ function buildSegmentsAttachment(
6826
6848
  }
6827
6849
  throw new CompileError(
6828
6850
  `${where}: ${at} is ${JSON.stringify(entry)}; an entry is a bone name, a chain as a list of bone names, or ` +
6829
- '{ "bone", "from", "to" }',
6851
+ '{ "bone", "from", "to" }', 'CE_COMPILE_SEGMENTS_ENTRY_INVALID',
6830
6852
  );
6831
6853
  });
6832
6854
 
@@ -6846,7 +6868,7 @@ function buildSegmentsAttachment(
6846
6868
  `"${named[reached.nearest.bone]}" at ${reached.nearest.distance.toFixed(4)}, and "influence.radius" is ` +
6847
6869
  `${influence.radius}${influence.across ? '' : ' (with "across": false, the nearest entry\'s links only)'}. ` +
6848
6870
  'A vertex needs a link, and rigc will not hand it one the reach does not: raise "radius" over that distance, ' +
6849
- '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',
6850
6872
  );
6851
6873
  }
6852
6874
  const got = segmentShares(measuredAt(px, py), segments, falloff!);
@@ -6857,7 +6879,7 @@ function buildSegmentsAttachment(
6857
6879
  `strongest pull(s) share it as ${got.dropped.map((d) => `"${named[d.bone]}" ${d.share.toFixed(4)}`).join(', ')}, ` +
6858
6880
  `and every one is under "minWeight" ${minWeight}. A vertex needs a bone, and rigc will not pick one the ` +
6859
6881
  `falloff dropped: lower "minWeight" to ${(1 / maxBones).toFixed(4)} or under (1 / "maxBones", which the ` +
6860
- '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',
6861
6883
  );
6862
6884
  });
6863
6885
  const geometry: MeshGeometry = {
@@ -6962,7 +6984,7 @@ function buildContourAttachment(
6962
6984
  if (att.image === undefined) {
6963
6985
  throw new CompileError(
6964
6986
  `${where}: a "contour" generator traces the part's own alpha, so the attachment needs an "image" — ` +
6965
- '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',
6966
6988
  );
6967
6989
  }
6968
6990
  const img = atlasedImage(att.image, where, ctx);
@@ -6981,7 +7003,7 @@ function buildContourAttachment(
6981
7003
  throw new CompileError(
6982
7004
  `${where}: a "contour" generator traces the part's own alpha, and "${att.image}" is lifted off a packed ` +
6983
7005
  'page at the coordinates the atlas states, which on this file are not where its texels are — so there is ' +
6984
- `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',
6985
7007
  );
6986
7008
  }
6987
7009
  // ⚠️ Nothing here reads the PNG's colour type. `hasAlpha` answers "where does
@@ -7009,7 +7031,7 @@ function buildContourAttachment(
7009
7031
  ...(img.atlasScale === undefined ? {} : { pageScale: img.atlasScale }),
7010
7032
  });
7011
7033
  } catch (err) {
7012
- 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');
7013
7035
  throw err;
7014
7036
  }
7015
7037
 
@@ -7024,16 +7046,16 @@ function buildContourAttachment(
7024
7046
  const h = img.height;
7025
7047
  const toArt = w / plate.width;
7026
7048
  if (att.width !== undefined && att.width !== w) {
7027
- 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');
7028
7050
  }
7029
7051
  if (att.height !== undefined && att.height !== h) {
7030
- 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');
7031
7053
  }
7032
7054
  const anchor = ctx.transforms.get(ctx.anchorBone);
7033
7055
  const frame = ctx.exactFrames.get(ctx.anchorBone);
7034
- 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');
7035
7057
  if (!ctx.bones.some((b) => b.name === ctx.anchorBone)) {
7036
- 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');
7037
7059
  }
7038
7060
  const vertices = bindWeightedVertices(
7039
7061
  geometry,
@@ -7147,7 +7169,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7147
7169
  const boneNames = ctx.boneNames;
7148
7170
  const needBone = (name: unknown, field: string): string => {
7149
7171
  if (typeof name !== 'string' || !boneNames.has(name)) {
7150
- 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');
7151
7173
  }
7152
7174
  return name;
7153
7175
  };
@@ -7164,14 +7186,14 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7164
7186
  throw new CompileError(
7165
7187
  `${where}: ${field} is ${JSON.stringify(value)}; known: ${allowed.join(', ')} (only the first letter's case is ` +
7166
7188
  "free — the parser's enumValue uppercases that one character and nothing else, and an unresolved name " +
7167
- 'becomes undefined without an error)',
7189
+ 'becomes undefined without an error)', 'CE_COMPILE_CONSTRAINT_ENUM_UNKNOWN',
7168
7190
  );
7169
7191
  }
7170
7192
  return value;
7171
7193
  };
7172
7194
  const needNumber = (value: unknown, field: string): number => {
7173
7195
  if (typeof value !== 'number' || !Number.isFinite(value)) {
7174
- 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');
7175
7197
  }
7176
7198
  return value;
7177
7199
  };
@@ -7189,7 +7211,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7189
7211
  const boneList = (): string[] => {
7190
7212
  const list = spec.bones;
7191
7213
  if (!Array.isArray(list) || list.length === 0) {
7192
- 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');
7193
7215
  }
7194
7216
  return list.map((name, i) => needBone(name, `bones[${i}]`));
7195
7217
  };
@@ -7214,11 +7236,11 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7214
7236
  // places in this format that does not fail silently — but it throws at load
7215
7237
  // time, in the consumer's process, and that is late.
7216
7238
  if (!TRANSFORM_PROPERTIES.includes(from)) {
7217
- 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');
7218
7240
  }
7219
7241
  for (const to of Object.keys(entry?.to ?? {})) {
7220
7242
  if (!TRANSFORM_PROPERTIES.includes(to)) {
7221
- 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');
7222
7244
  }
7223
7245
  }
7224
7246
  }
@@ -7250,7 +7272,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7250
7272
  // loud, but in the consumer's process. The silent half is the one below it.
7251
7273
  const slot = spec.slot;
7252
7274
  if (typeof slot !== 'string' || !ctx.slotNames.has(slot)) {
7253
- 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');
7254
7276
  }
7255
7277
  if (!ctx.pathSlots.has(slot)) {
7256
7278
  // `PathConstraint.update` opens with
@@ -7261,7 +7283,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7261
7283
  `${where}: slot "${slot}" has no path attachment in any skin, so the constraint has no curve to follow — ` +
7262
7284
  'PathConstraint.update returns immediately unless the slot\'s attachment is a path, which means this ' +
7263
7285
  'constraint would load, report every mix it was given, and move nothing. ' +
7264
- 'Give that slot an attachment with "type": "path".',
7286
+ 'Give that slot an attachment with "type": "path".', 'CE_COMPILE_PATH_CONSTRAINT_SLOT_NO_PATH',
7265
7287
  );
7266
7288
  }
7267
7289
  out.slot = slot;
@@ -7287,14 +7309,14 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7287
7309
  if (typeof animation !== 'string' || animation.length === 0) {
7288
7310
  throw new CompileError(
7289
7311
  `${where}: a slider needs an "animation" — the animation it applies. Without one the parser's second pass ` +
7290
- 'over the constraints array throws `Slider animation not found`.',
7312
+ 'over the constraints array throws `Slider animation not found`.', 'CE_COMPILE_SLIDER_ANIMATION_MISSING',
7291
7313
  );
7292
7314
  }
7293
7315
  if (!ctx.animationNames.has(animation)) {
7294
7316
  const known = [...ctx.animationNames];
7295
7317
  throw new CompileError(
7296
7318
  `${where}: applies animation "${animation}", which the motion spec does not declare` +
7297
- (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',
7298
7320
  );
7299
7321
  }
7300
7322
  // 🔁 The editor keeps a slider's animation by index and re-sorts the object
@@ -7309,7 +7331,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7309
7331
  `lists every array-index key (${spelled} here) before the rest, the editor re-sorts the animations into its ` +
7310
7332
  'own order on import, and a slider holds its animation by index (measured on Spine 4.3.26; AUTHORING R10). ' +
7311
7333
  `Rename ${spelled} so no animation name is a plain non-negative integer — a prefix or a leading zero ` +
7312
- `("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',
7313
7335
  );
7314
7336
  }
7315
7337
  out.animation = animation;
@@ -7329,7 +7351,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7329
7351
  if (typeof property !== 'string' || !RIG_FROM_PROPERTIES.includes(property as (typeof RIG_FROM_PROPERTIES)[number])) {
7330
7352
  throw new CompileError(
7331
7353
  `${where}: drives off bone "${String(spec.bone)}" but its property is ${JSON.stringify(property)}; ` +
7332
- `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',
7333
7355
  );
7334
7356
  }
7335
7357
  out.property = property;
@@ -7339,7 +7361,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7339
7361
  if (spec.local !== undefined) out.local = spec.local;
7340
7362
  if (spec.scale !== undefined && spec.scale === 0) {
7341
7363
  // time = to + (value - from) * 0, so the slider holds one frame forever.
7342
- 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');
7343
7365
  }
7344
7366
  // The window of driving values that can reach a frame at all, and the two
7345
7367
  // functions every figure in the two reader clauses below comes off.
@@ -7586,7 +7608,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7586
7608
  `at ${dead.end.toFixed(3)}° is read as ${readAs.toFixed(3)}° and maps to time ${lands.toFixed(3)}s. ` +
7587
7609
  `${consequence} The whole part of the range ${dead.side} is dead and ` +
7588
7610
  'nothing at runtime reports it. Add `"local": true` to read the bone\'s own rotation signed and unwrapped — ' +
7589
- `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',
7590
7612
  );
7591
7613
  }
7592
7614
  }
@@ -7659,14 +7681,14 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7659
7681
  `${lowest.toFixed(3)} is read as ${readAs.toFixed(3)} and maps to time ${lands.toFixed(3)}s — the time the ` +
7660
7682
  `bone at ${readAs.toFixed(3)} maps to. Positions below 0 read as the mirror of positions above it: ` +
7661
7683
  `${consequence} Nothing at runtime reports it. Add \`"local": true\` to read the bone's own scale signed and ` +
7662
- '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',
7663
7685
  );
7664
7686
  }
7665
7687
  for (const field of timeSide) {
7666
7688
  if (spec[field] !== undefined) {
7667
7689
  throw new CompileError(
7668
7690
  `${where}: declares both a "bone" and "${field}". A slider with a bone takes its time from that bone's ` +
7669
- `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',
7670
7692
  );
7671
7693
  }
7672
7694
  }
@@ -7677,7 +7699,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7677
7699
  throw new CompileError(
7678
7700
  `${where}: declares "${field}" but no "bone". Every one of ${boneSide.join('/')} is read only inside the ` +
7679
7701
  "parser's `if (boneName)` branch (`:350-360`), so it would be dropped in silence. Name the driving bone, " +
7680
- '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',
7681
7703
  );
7682
7704
  }
7683
7705
  }
@@ -7722,7 +7744,7 @@ function buildRigConstraint(spec: RigConstraintInput, ctx: ConstraintContext): M
7722
7744
  throw new NotImplementedError(
7723
7745
  `${where}: constraint type ${JSON.stringify(spec.type)} is not one Spine 4.3 knows. ` +
7724
7746
  'The five are: ik, transform, path, physics, slider. An unrecognised type matches no case in the parser and ' +
7725
- 'the constraint is dropped without a word.',
7747
+ 'the constraint is dropped without a word.', 'CE_COMPILE_CONSTRAINT_TYPE_UNKNOWN',
7726
7748
  );
7727
7749
  }
7728
7750
 
@@ -7744,7 +7766,7 @@ function buildRigInfo(
7744
7766
  ): RigInfo {
7745
7767
  const axisBone = rig.invariants?.axisBone ?? null;
7746
7768
  if (axisBone !== null && !bones.some((b) => b.name === axisBone)) {
7747
- 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');
7748
7770
  }
7749
7771
  const axisSubtree: string[] = [];
7750
7772
  if (axisBone) {
@@ -7772,7 +7794,7 @@ function buildRigInfo(
7772
7794
  if (meshKinds[slot] === undefined) {
7773
7795
  throw new CompileError(
7774
7796
  `rig "${rig.name}" exempts slot "${slot}" from A39_DEFORM_KEEPS_TRIANGLE_WINDING, but that slot carries no ` +
7775
- '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',
7776
7798
  );
7777
7799
  }
7778
7800
  }
@@ -7785,12 +7807,12 @@ function buildRigInfo(
7785
7807
  spineDeg === null ? null : [Math.cos((spineDeg * Math.PI) / 180), Math.sin((spineDeg * Math.PI) / 180)];
7786
7808
  const contactDepth = manifest?.stroke?.contact_depth ?? null;
7787
7809
  if (contactDepth !== null && !(contactDepth > 0)) {
7788
- 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');
7789
7811
  }
7790
7812
  const capCeiling = manifest?.stroke?.cap_containment_ceiling ?? null;
7791
7813
  if (capCeiling !== null && !(capCeiling > 0)) {
7792
7814
  throw new CompileError(
7793
- `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',
7794
7816
  );
7795
7817
  }
7796
7818
  return {
@@ -7829,13 +7851,13 @@ function checkAxisSelfConsistency(manifest: FaceManifest): void {
7829
7851
  if (!manifest.axis) return;
7830
7852
  const { deg, unit } = manifest.axis;
7831
7853
  if (!Array.isArray(unit) || unit.length !== 2) {
7832
- 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');
7833
7855
  }
7834
7856
  const ex = Math.cos((deg * Math.PI) / 180);
7835
7857
  const ey = Math.sin((deg * Math.PI) / 180);
7836
7858
  if (hypot2(unit[0] - ex, unit[1] - ey) > 1e-3) {
7837
7859
  throw new CompileError(
7838
- `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',
7839
7861
  );
7840
7862
  }
7841
7863
  }
@@ -7894,9 +7916,9 @@ function buildMesh(
7894
7916
  const controls = meshControlBones(part);
7895
7917
 
7896
7918
  const refFor = (name: string): MeshBoneRef => {
7897
- 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');
7898
7920
  const m = transforms.get(name);
7899
- 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');
7900
7922
  return { name, toBind: (wx, wy) => toBoneLocal(m, wx, wy) };
7901
7923
  };
7902
7924
 
@@ -7923,7 +7945,7 @@ function buildMesh(
7923
7945
  // bits turned an authored 90 into 89.99999734089101 in that refusal.
7924
7946
  const controlAngles = ringControlAngles(controls, spec.center!, (name) => {
7925
7947
  const m = exactFrames.get(name);
7926
- 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');
7927
7949
  return [m.worldX, cropH - m.worldY];
7928
7950
  });
7929
7951
  geometry = buildRingMesh({
@@ -7936,7 +7958,7 @@ function buildMesh(
7936
7958
  });
7937
7959
  }
7938
7960
  } catch (err) {
7939
- 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');
7940
7962
  throw err;
7941
7963
  }
7942
7964
 
@@ -7987,17 +8009,17 @@ function buildMesh(
7987
8009
  */
7988
8010
  function rawCurve(curve: number[] | 'stepped', channels: number, where: string, at: string): number[] | 'stepped' {
7989
8011
  if (curve === 'stepped') return 'stepped';
7990
- 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');
7991
8013
  if (curve.length !== channels * 4) {
7992
8014
  // A short array multiplies `undefined` into the cubic and yields NaN with no
7993
8015
  // error at all — case 6g, and the reason A05 exists.
7994
8016
  throw new CompileError(
7995
- `${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',
7996
8018
  );
7997
8019
  }
7998
8020
  for (const n of curve) {
7999
8021
  if (typeof n !== 'number' || !Number.isFinite(n)) {
8000
- 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');
8001
8023
  }
8002
8024
  }
8003
8025
  return curve.map(f32);
@@ -8021,9 +8043,9 @@ function compileValueTrack(
8021
8043
  kind: string,
8022
8044
  ): ModelKey[] {
8023
8045
  const shape = shapes[track.property];
8024
- 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');
8025
8047
  const where = `animation "${animName}" ${kind} "${target}" ${track.property}`;
8026
- 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');
8027
8049
 
8028
8050
  const out: ModelKey[] = [];
8029
8051
  for (let i = 0; i < track.keys.length; i++) {
@@ -8031,7 +8053,7 @@ function compileValueTrack(
8031
8053
  const next = track.keys[i + 1];
8032
8054
  const time = keyTime(key.t + shift);
8033
8055
  if (i > 0 && time <= (out[i - 1].time as number)) {
8034
- 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');
8035
8057
  }
8036
8058
  checkKeyTime(where, time, key.t, duration);
8037
8059
  // A named-value timeline (`inherit`): the key's `v` is one name, stepped by
@@ -8044,12 +8066,12 @@ function compileValueTrack(
8044
8066
  throw new CompileError(
8045
8067
  `${where} key at t=${key.t} carries ${key.ease !== undefined ? `an easing ("${key.ease}")` : 'a curve'}, and ` +
8046
8068
  `this timeline is stepped by the format — its reader builds no curve, so the mode changes AT the key and ` +
8047
- 'holds until the next one. Remove it',
8069
+ 'holds until the next one. Remove it', 'CE_COMPILE_STEPPED_TIMELINE_EASING',
8048
8070
  );
8049
8071
  }
8050
8072
  const mode = shape.named.resolve(key.v);
8051
8073
  if (mode === undefined) {
8052
- 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');
8053
8075
  }
8054
8076
  out.push({ time, [shape.named.field]: mode });
8055
8077
  continue;
@@ -8057,18 +8079,18 @@ function compileValueTrack(
8057
8079
  // A no-field timeline (`reset`) is an event: the key IS the value, so it
8058
8080
  // carries none. Anything else must match the field count exactly.
8059
8081
  if (shape.fields.length === 0) {
8060
- if (key.v !== null) throw new CompileError(`${where}: this timeline takes no value; use null`);
8061
- 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');
8062
8084
  out.push({ time });
8063
8085
  continue;
8064
8086
  }
8065
8087
  if (!Array.isArray(key.v) || key.v.length !== shape.fields.length) {
8066
- 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');
8067
8089
  }
8068
8090
  const entry: ModelKey = { time };
8069
8091
  shape.fields.forEach((field, c) => {
8070
8092
  const v = key.v as number[];
8071
- 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');
8072
8094
  // The bound, where the runtime has one. Refused here rather than left to
8073
8095
  // `A23` because this is a spec somebody wrote and the key is the thing to
8074
8096
  // change — the same division `compileConstraintTrack`'s `range` makes for
@@ -8084,28 +8106,28 @@ function compileValueTrack(
8084
8106
  // reads "… mass: value is 0", which says the property twice and the
8085
8107
  // second time under the wrong name.
8086
8108
  const at = shape.fields.length > 1 ? `${field} at t=${key.t}` : `key at t=${key.t}`;
8087
- 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');
8088
8110
  }
8089
8111
  entry[field] = f32(v[c]);
8090
8112
  });
8091
8113
  if (key.ease !== undefined && key.curve !== undefined) {
8092
- 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');
8093
8115
  }
8094
8116
  if (key.curve !== undefined) {
8095
- 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');
8096
8118
  entry.curve = rawCurve(key.curve, shape.fields.length, where, String(key.t));
8097
8119
  } else if (key.ease && next) {
8098
8120
  if (key.ease === 'stepped') {
8099
8121
  entry.curve = 'stepped';
8100
8122
  } else {
8101
8123
  const handles = motion.easings?.[key.ease];
8102
- if (!handles) throw new CompileError(`${where}: unknown easing "${key.ease}"`);
8103
- 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');
8104
8126
  const t2 = keyTime(next.t + shift);
8105
8127
  entry.curve = easingCurve(handles, time, t2, key.v as number[], next.v as number[]);
8106
8128
  }
8107
8129
  } else if (key.ease && !next) {
8108
- 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');
8109
8131
  }
8110
8132
  out.push(entry);
8111
8133
  }
@@ -8154,7 +8176,7 @@ function compileDrawOrder(
8154
8176
  slots: readonly ModelSlot[],
8155
8177
  ): ModelKey[] {
8156
8178
  const where = `animation "${animName}" drawOrder`;
8157
- if (!keys.length) throw new CompileError(`${where}: no keys`);
8179
+ if (!keys.length) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8158
8180
  const indexOf = new Map(slots.map((s, i) => [s.name, i]));
8159
8181
 
8160
8182
  const out: ModelKey[] = [];
@@ -8162,7 +8184,7 @@ function compileDrawOrder(
8162
8184
  const key = keys[i];
8163
8185
  const time = keyTime(key.t);
8164
8186
  if (i > 0 && time <= (out[i - 1].time as number)) {
8165
- 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');
8166
8188
  }
8167
8189
  checkKeyTime(where, time, key.t, duration);
8168
8190
  // No offsets = "back to the setup draw order", which is the parser's own
@@ -8177,19 +8199,19 @@ function compileDrawOrder(
8177
8199
  for (const off of key.offsets) {
8178
8200
  const index = indexOf.get(off.slot);
8179
8201
  if (index === undefined) {
8180
- 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');
8181
8203
  }
8182
8204
  if (seen.has(off.slot)) {
8183
- 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');
8184
8206
  }
8185
8207
  if (!Number.isInteger(off.offset)) {
8186
- 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');
8187
8209
  }
8188
8210
  const landing = index + off.offset;
8189
8211
  if (landing < 0 || landing >= slots.length) {
8190
8212
  throw new CompileError(
8191
8213
  `${where} at t=${key.t}: slot "${off.slot}" is at index ${index} and offset ${off.offset} puts it at ` +
8192
- `${landing}, outside the ${slots.length} emitted slots`,
8214
+ `${landing}, outside the ${slots.length} emitted slots`, 'CE_COMPILE_DRAW_ORDER_OFFSET_OUTSIDE_SLOTS',
8193
8215
  );
8194
8216
  }
8195
8217
  seen.add(off.slot);
@@ -8234,27 +8256,27 @@ function compileEvents(
8234
8256
  events: Record<string, RigEvent>,
8235
8257
  ): ModelKey[] {
8236
8258
  const where = `animation "${animName}" events`;
8237
- if (!keys.length) throw new CompileError(`${where}: no keys`);
8259
+ if (!keys.length) throw new CompileError(`${where}: no keys`, 'CE_COMPILE_TIMELINE_KEYS_EMPTY');
8238
8260
 
8239
8261
  const out: ModelKey[] = [];
8240
8262
  for (let i = 0; i < keys.length; i++) {
8241
8263
  const key = keys[i];
8242
8264
  if (typeof key.name !== 'string' || key.name.length === 0) {
8243
- 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');
8244
8266
  }
8245
8267
  const declared = events[key.name];
8246
8268
  if (declared === undefined) {
8247
8269
  const known = Object.keys(events);
8248
8270
  throw new CompileError(
8249
8271
  `${where} at t=${key.t}: event "${key.name}" is not declared in the rig spec's "events" block; ` +
8250
- (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',
8251
8273
  );
8252
8274
  }
8253
8275
  const time = keyTime(key.t);
8254
8276
  if (i > 0 && time < (out[i - 1].time as number)) {
8255
8277
  throw new CompileError(
8256
8278
  `${where}: key times must not go backwards (at t=${key.t}, after t=${String(out[i - 1].time)}) — ` +
8257
- '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',
8258
8280
  );
8259
8281
  }
8260
8282
  checkKeyTime(where, time, key.t, duration);
@@ -8262,19 +8284,19 @@ function compileEvents(
8262
8284
  const entry: ModelKey = { time, name: key.name };
8263
8285
  if (key.int !== undefined) {
8264
8286
  if (!Number.isInteger(key.int)) {
8265
- 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');
8266
8288
  }
8267
8289
  entry.int = key.int;
8268
8290
  }
8269
8291
  if (key.float !== undefined) {
8270
8292
  if (!Number.isFinite(key.float)) {
8271
- 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');
8272
8294
  }
8273
8295
  entry.float = f32(key.float);
8274
8296
  }
8275
8297
  if (key.string !== undefined) {
8276
8298
  if (typeof key.string !== 'string') {
8277
- 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');
8278
8300
  }
8279
8301
  entry.string = key.string;
8280
8302
  }
@@ -8284,10 +8306,10 @@ function compileEvents(
8284
8306
  if (declared.audio === undefined) {
8285
8307
  throw new CompileError(
8286
8308
  `${where} at t=${key.t}: ${field} is set but event "${key.name}" declares no "audio"; ` +
8287
- `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',
8288
8310
  );
8289
8311
  }
8290
- 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');
8291
8313
  entry[field] = f32(v);
8292
8314
  }
8293
8315
  out.push(entry);
@@ -8356,7 +8378,7 @@ function compileConstraintTrack(
8356
8378
  // An array by the time this runs (`parseMotionSpec`); an EMPTY one is a
8357
8379
  // format rule rather than a shape — the parser reads key 0, finds nothing and
8358
8380
  // skips the timeline, which is assertion A34's silent case.
8359
- 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');
8360
8382
 
8361
8383
  const read = (key: MotionIkTrack['keys'][number] | MotionTransformTrack['keys'][number], field: string): unknown =>
8362
8384
  (key as unknown as Record<string, unknown>)[field];
@@ -8375,7 +8397,7 @@ function compileConstraintTrack(
8375
8397
  throw new CompileError(
8376
8398
  `${where}: key ${i} (t=${key.t}) names "${field}" and key 0 does not. Every key of ${article} ${group} timeline ` +
8377
8399
  'is read with its own default, so a field stated on some keys and not others snaps to the default on ' +
8378
- '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',
8379
8401
  );
8380
8402
  }
8381
8403
  for (const field of first) {
@@ -8383,7 +8405,7 @@ function compileConstraintTrack(
8383
8405
  throw new CompileError(
8384
8406
  `${where}: key 0 names "${field}" and key ${i} (t=${key.t}) does not. Every key of ${article} ${group} timeline ` +
8385
8407
  `is read with its own default, so "${field}" would snap to ` +
8386
- `${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',
8387
8409
  );
8388
8410
  }
8389
8411
  });
@@ -8403,7 +8425,7 @@ function compileConstraintTrack(
8403
8425
  const next = keys[i + 1];
8404
8426
  const time = keyTime(key.t);
8405
8427
  if (i > 0 && time <= (out[i - 1].time as number)) {
8406
- 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');
8407
8429
  }
8408
8430
  checkKeyTime(where, time, key.t, duration);
8409
8431
 
@@ -8412,14 +8434,14 @@ function compileConstraintTrack(
8412
8434
  const v = read(key, channel.field);
8413
8435
  if (v === undefined) continue;
8414
8436
  if (typeof v !== 'number' || !Number.isFinite(v)) {
8415
- 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');
8416
8438
  }
8417
8439
  const bounds = shape.range[channel.field];
8418
8440
  if (bounds && (v < bounds[0] || v > bounds[1])) {
8419
8441
  throw new CompileError(
8420
8442
  `${where} (t=${key.t}): ${channel.field} is ${v}, outside ${bounds[0]}..${
8421
8443
  bounds[1] === Infinity ? '∞' : bounds[1]
8422
- } — 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',
8423
8445
  );
8424
8446
  }
8425
8447
  entry[channel.field] = f32(v);
@@ -8436,23 +8458,23 @@ function compileConstraintTrack(
8436
8458
  continue;
8437
8459
  }
8438
8460
  if (typeof v !== 'boolean') {
8439
- 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');
8440
8462
  }
8441
8463
  entry[flag.field] = v;
8442
8464
  }
8443
8465
 
8444
8466
  if (key.ease !== undefined && key.curve !== undefined) {
8445
- 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');
8446
8468
  }
8447
8469
  if (key.curve !== undefined) {
8448
- 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');
8449
8471
  entry.curve = rawCurve(key.curve, shape.channels.length, where, String(key.t));
8450
8472
  } else if (key.ease !== undefined && next) {
8451
8473
  if (key.ease === 'stepped') {
8452
8474
  entry.curve = 'stepped';
8453
8475
  } else {
8454
8476
  const handles = motion.easings?.[key.ease];
8455
- 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');
8456
8478
  const t2 = keyTime(next.t);
8457
8479
  entry.curve = easingCurve(
8458
8480
  handles,
@@ -8463,7 +8485,7 @@ function compileConstraintTrack(
8463
8485
  );
8464
8486
  }
8465
8487
  } else if (key.ease !== undefined && !next) {
8466
- 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');
8467
8489
  }
8468
8490
  out.push(entry);
8469
8491
  }
@@ -8577,7 +8599,7 @@ export function deformGeometryOf(
8577
8599
  if (!isModelVertexAttachment(att)) {
8578
8600
  throw new CompileError(
8579
8601
  `${where}: a deform timeline keys the vertices of an attachment, and this one is a "${type}" — ` +
8580
- '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',
8581
8603
  );
8582
8604
  }
8583
8605
  if (att.kind === 'mesh') {
@@ -8644,7 +8666,7 @@ export function deformGeometryOf(
8644
8666
  const vertex = vertices.bindings[v];
8645
8667
  const n = vertex.length;
8646
8668
  if (n < 1) {
8647
- 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');
8648
8670
  }
8649
8671
  if (n === 1) {
8650
8672
  bones.add(vertex[0].bone);
@@ -8705,7 +8727,7 @@ export function deformGeometryOf(
8705
8727
  let sum = 0;
8706
8728
  for (const binding of vertices.bindings[v]) {
8707
8729
  const m = transforms.get(binding.bone);
8708
- 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');
8709
8731
  const weight = binding.weight;
8710
8732
  const [x, y] = toWorld(m, binding.x, binding.y);
8711
8733
  wx += x * weight;
@@ -8812,7 +8834,7 @@ function compileSequenceTrack(
8812
8834
  `${where}: attachment "${track.attachment}" carries no "sequence" block, so there is no series to step. The ` +
8813
8835
  'parser gives every region and mesh a series of ONE region (`readSequence(null)` is `new Sequence(1, ' +
8814
8836
  'false)`), so this timeline would load and show that region under every mode at every time. Give the ' +
8815
- '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',
8816
8838
  );
8817
8839
  }
8818
8840
  if (attachment.kind === 'linkedmesh' && attachment.timelines) {
@@ -8822,23 +8844,23 @@ function compileSequenceTrack(
8822
8844
  `is not false), so its \`timelineAttachment\` is "${source}" (\`SkeletonJson.js:437-448\`) and ` +
8823
8845
  '`SequenceTimeline` applies only where the slot\'s attachment\'s `timelineAttachment` is the one it was ' +
8824
8846
  `built for — a key aimed at the link is applied to nothing. Key "${source}" instead (the link steps its own ` +
8825
- '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',
8826
8848
  );
8827
8849
  }
8828
- 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');
8829
8851
  const out: ModelKey[] = [];
8830
8852
  for (let i = 0; i < track.keys.length; i++) {
8831
8853
  const key = track.keys[i];
8832
8854
  const time = keyTime(key.t);
8833
8855
  if (i > 0 && time <= (out[i - 1].time as number)) {
8834
- 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');
8835
8857
  }
8836
8858
  checkKeyTime(where, time, key.t, duration);
8837
8859
  if (key.index !== undefined && key.index >= series.count) {
8838
8860
  throw new CompileError(
8839
8861
  `${where} (t=${key.t}): index ${key.index} is past the end of a ${series.count}-frame series (frames 0 to ` +
8840
8862
  `${series.count - 1}). \`Sequence.resolveIndex\` clamps it to the last frame, so the key would start on a ` +
8841
- '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',
8842
8864
  );
8843
8865
  }
8844
8866
  const entry: ModelKey = { time };
@@ -8864,14 +8886,14 @@ function compileDeformTrack(
8864
8886
  // An array by the time this runs (`parseMotionSpec`); an EMPTY one is a
8865
8887
  // format rule rather than a shape — the parser reads key 0, finds nothing and
8866
8888
  // skips the timeline, which is assertion A34's silent case.
8867
- 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');
8868
8890
 
8869
8891
  const out: ModelKey[] = [];
8870
8892
  for (let i = 0; i < keys.length; i++) {
8871
8893
  const key = keys[i];
8872
8894
  const time = keyTime(key.t);
8873
8895
  if (i > 0 && time <= (out[i - 1].time as number)) {
8874
- 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');
8875
8897
  }
8876
8898
  checkKeyTime(where, time, key.t, duration);
8877
8899
  // -- a key that states a MODEL rather than a run (issue #294) ------------
@@ -8888,7 +8910,7 @@ function compileDeformTrack(
8888
8910
  throw new CompileError(
8889
8911
  `${where} (t=${key.t}): the key carries both a "transform" and a "vertices" run, and they are two answers to ` +
8890
8912
  'one question. Authored offsets and a stated model do not combine, for the same reason a mesh cannot carry ' +
8891
- 'both a "generator" and authored geometry — drop one.',
8913
+ 'both a "generator" and authored geometry — drop one.', 'CE_COMPILE_DEFORM_TRANSFORM_AND_VERTICES',
8892
8914
  );
8893
8915
  }
8894
8916
  if (key.offset !== undefined || key.fromVertex !== undefined) {
@@ -8896,14 +8918,14 @@ function compileDeformTrack(
8896
8918
  `${where} (t=${key.t}): the key states a "transform" and a start index. A transform is a model of the whole ` +
8897
8919
  `attachment and is evaluated over all ${geometry.vertexCount} of its vertices, so it always starts at deform ` +
8898
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 ' +
8899
- 'if that is what you mean.',
8921
+ 'if that is what you mean.', 'CE_COMPILE_DEFORM_TRANSFORM_START_INDEX',
8900
8922
  );
8901
8923
  }
8902
8924
  if (geometry.setup === null) {
8903
8925
  throw new CompileError(
8904
8926
  `${where} (t=${key.t}): a "transform" states where every vertex of this attachment goes, and this one has a ` +
8905
8927
  `vertex the arithmetic cannot place — ${geometry.setupWhy}. Fix the weights, or write the bind-space pairs ` +
8906
- 'yourself and start the run with "offset".',
8928
+ 'yourself and start the run with "offset".', 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED',
8907
8929
  );
8908
8930
  }
8909
8931
  const report = evaluateDeformTransform(
@@ -8920,7 +8942,7 @@ function compileDeformTrack(
8920
8942
  // assumed: a silent mismatch here is the overrun A35 exists for.
8921
8943
  throw new CompileError(
8922
8944
  `${where} (t=${key.t}): the transform produced ${report.offsets.length} numbers for ` +
8923
- `${geometry.vertexCount} vertices`,
8945
+ `${geometry.vertexCount} vertices`, 'CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT',
8924
8946
  );
8925
8947
  }
8926
8948
  // -- one displacement per vertex -> one pair per influence (issue #389) -
@@ -8936,7 +8958,7 @@ function compileDeformTrack(
8936
8958
  if (run.length !== geometry.deformLength) {
8937
8959
  throw new CompileError(
8938
8960
  `${where} (t=${key.t}): the transform expanded to ${run.length} numbers and this attachment's deform ` +
8939
- `array is ${geometry.deformLength} long`,
8961
+ `array is ${geometry.deformLength} long`, 'CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH',
8940
8962
  );
8941
8963
  }
8942
8964
  generated.push({
@@ -8970,7 +8992,7 @@ function compileDeformTrack(
8970
8992
  if (key.offset !== undefined && key.fromVertex !== undefined) {
8971
8993
  throw new CompileError(
8972
8994
  `${where} (t=${key.t}): a key gives its start as "offset" (an index into the deform array) or as ` +
8973
- '"fromVertex" (a vertex index rigc translates), never both',
8995
+ '"fromVertex" (a vertex index rigc translates), never both', 'CE_COMPILE_DEFORM_START_SOURCES_CONFLICT',
8974
8996
  );
8975
8997
  }
8976
8998
  const run = key.vertices ?? null;
@@ -8981,7 +9003,7 @@ function compileDeformTrack(
8981
9003
  if (key.offset !== undefined || key.fromVertex !== undefined) {
8982
9004
  throw new CompileError(
8983
9005
  `${where} (t=${key.t}): the key has no "vertices", which is the format's way of saying "back to the ` +
8984
- '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',
8985
9007
  );
8986
9008
  }
8987
9009
  out.push({ time });
@@ -8990,7 +9012,7 @@ function compileDeformTrack(
8990
9012
  if (!Array.isArray(run) || run.length === 0) {
8991
9013
  throw new CompileError(
8992
9014
  `${where} (t=${key.t}): "vertices" is ${JSON.stringify(key.vertices)}; give an array of x, y offsets, ` +
8993
- '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',
8994
9016
  );
8995
9017
  }
8996
9018
  // ⛔ No parity clause here, and its absence is the rule (issue #576).
@@ -9020,7 +9042,7 @@ function compileDeformTrack(
9020
9042
  // format holds, and the half that refused was the authoring half.
9021
9043
  for (const n of run) {
9022
9044
  if (typeof n !== 'number' || !Number.isFinite(n)) {
9023
- 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');
9024
9046
  }
9025
9047
  }
9026
9048
  const start = deformStart(key, run.length, geometry, where);
@@ -9029,7 +9051,7 @@ function compileDeformTrack(
9029
9051
  `${where} (t=${key.t}): the run starts at deform index ${start} and is ${run.length} long, which ends at ` +
9030
9052
  `${start + run.length}; this attachment's deform array is ${geometry.deformLength} long ` +
9031
9053
  `(${geometry.weighted ? `${geometry.deformLength / 2} bone influences` : `${geometry.vertexCount} vertices`}). ` +
9032
- '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',
9033
9055
  );
9034
9056
  }
9035
9057
  const entry: ModelKey = { time };
@@ -9100,7 +9122,7 @@ function deformStart(
9100
9122
  if (key.offset !== undefined) {
9101
9123
  if (!Number.isInteger(key.offset) || key.offset < 0) {
9102
9124
  throw new CompileError(
9103
- `${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',
9104
9126
  );
9105
9127
  }
9106
9128
  // ⛔ An odd `offset` is not refused either, and it went the same way as the
@@ -9118,7 +9140,7 @@ function deformStart(
9118
9140
  if (key.fromVertex === undefined) return 0;
9119
9141
  const from = key.fromVertex;
9120
9142
  if (!Number.isInteger(from) || from < 0) {
9121
- 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');
9122
9144
  }
9123
9145
  // ⌈⌉ rather than ÷, because an odd run REACHES a last vertex without covering
9124
9146
  // it: its final number is that vertex's x and the y beside it stays at setup
@@ -9129,7 +9151,7 @@ function deformStart(
9129
9151
  if (from + covered > geometry.vertexCount) {
9130
9152
  throw new CompileError(
9131
9153
  `${where} (t=${key.t}): fromVertex ${from} plus ${covered} vertex offset(s) runs to vertex ${from + covered}, ` +
9132
- `and the attachment has ${geometry.vertexCount}`,
9154
+ `and the attachment has ${geometry.vertexCount}`, 'CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT',
9133
9155
  );
9134
9156
  }
9135
9157
  if (!geometry.weighted) return from * 2;
@@ -9142,7 +9164,7 @@ function deformStart(
9142
9164
  'is not a thing that array can hold: the world offset of a multi-bone vertex is the weighted sum of a ' +
9143
9165
  'per-bone offset in each bone\'s own bind space, so rigc will not guess one for you. Either key the ' +
9144
9166
  'control bone instead, or write the bind-space pairs yourself and start the run with "offset" ' +
9145
- `(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',
9146
9168
  );
9147
9169
  }
9148
9170
  let start = 0;
@@ -9170,21 +9192,21 @@ function deformKeyCurve(
9170
9192
  next: ModelKey | undefined,
9171
9193
  ): ModelKey {
9172
9194
  if (key.ease !== undefined && key.curve !== undefined) {
9173
- 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');
9174
9196
  }
9175
9197
  if (key.curve !== undefined) {
9176
- 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');
9177
9199
  entry.curve = rawCurve(key.curve, 1, where, String(key.t));
9178
9200
  return entry;
9179
9201
  }
9180
9202
  if (key.ease === undefined) return entry;
9181
- 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');
9182
9204
  if (key.ease === 'stepped') {
9183
9205
  entry.curve = 'stepped';
9184
9206
  return entry;
9185
9207
  }
9186
9208
  const handles = motion.easings?.[key.ease];
9187
- 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');
9188
9210
  // The one channel runs 0 -> 1; the VALUE being held is the geometry, so the
9189
9211
  // hold test compares the two keys' runs rather than those two constants.
9190
9212
  entry.curve = easingCurve(handles, entry.time as number, keyTime(keys[index + 1].t), [0], [1], sameDeform(entry, next));
@@ -9220,7 +9242,7 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9220
9242
  if (named.length > 1) {
9221
9243
  throw new CompileError(
9222
9244
  `animation "${animName}": a track names more than one target (${named.join(', ')}); the list is ` +
9223
- `${targetFields.join('/')} and a track names exactly one`,
9245
+ `${targetFields.join('/')} and a track names exactly one`, 'CE_COMPILE_TRACK_TARGETS_MULTIPLE',
9224
9246
  );
9225
9247
  }
9226
9248
  const family = constraintFamilyOf(track);
@@ -9230,12 +9252,12 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9230
9252
  const who = direct === undefined ? `group "${String(track.group)}"` : `${label} "${direct}"`;
9231
9253
  if (!(track.property in tracks)) {
9232
9254
  throw new CompileError(
9233
- `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',
9234
9256
  );
9235
9257
  }
9236
9258
  if (direct !== undefined) return [direct];
9237
9259
  const members = motion.groups?.[String(track.group)];
9238
- 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');
9239
9261
  return members;
9240
9262
  }
9241
9263
  // A constraint timeline with no constraint named. Worth its own refusal: the
@@ -9246,12 +9268,12 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9246
9268
  if (owning.length) {
9247
9269
  throw new CompileError(
9248
9270
  `animation "${animName}": "${track.property}" is a ${owning.map((f) => CONSTRAINT_TRACK_FAMILIES[f].label).join(' / ')} ` +
9249
- `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',
9250
9272
  );
9251
9273
  }
9252
9274
  const isBoneTrack = track.property in BONE_TRACKS;
9253
9275
  if (isBoneTrack && !track.bone && !track.group) {
9254
- 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');
9255
9277
  }
9256
9278
  if (!isBoneTrack && track.bone) {
9257
9279
  // The bone family's sibling of `compileTrack`'s slot refusal, raised here for
@@ -9266,7 +9288,7 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9266
9288
  throw new CompileError(
9267
9289
  `animation "${animName}" bone "${track.bone}" has no timeline "${track.property}" ` +
9268
9290
  `(it has: ${Object.keys(BONE_TRACKS).join(', ')})` +
9269
- (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',
9270
9292
  );
9271
9293
  }
9272
9294
  if (track.bone) return [track.bone];
@@ -9278,7 +9300,7 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9278
9300
  // following the part and two objects moving together, which reads as a
9279
9301
  // composite.
9280
9302
  const members = motion.groups?.[track.group];
9281
- 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');
9282
9304
  // 🚨 The group is the one target shape whose FAMILY is decided by the
9283
9305
  // property, and three tables decide it: `constraintFamilyOf` asks
9284
9306
  // `property in PHYSICS_TRACKS`, this function asks `property in BONE_TRACKS`,
@@ -9304,12 +9326,12 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9304
9326
  `animation "${animName}" group "${track.group}" has no timeline "${track.property}" ` +
9305
9327
  `(a bone group has: ${Object.keys(BONE_TRACKS).join(', ')}; ` +
9306
9328
  `a slot group has: ${Object.keys(SLOT_TRACKS).join(', ')}; ` +
9307
- `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',
9308
9330
  );
9309
9331
  }
9310
9332
  return members;
9311
9333
  }
9312
- 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');
9313
9335
  }
9314
9336
 
9315
9337
  /**
@@ -9330,23 +9352,23 @@ function resolveTargets(track: MotionTrack, motion: MotionSpec, animName: string
9330
9352
  function checkMotionGroups(motion: MotionSpec): void {
9331
9353
  for (const [name, members] of Object.entries(motion.groups ?? {})) {
9332
9354
  if (!Array.isArray(members)) {
9333
- 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');
9334
9356
  }
9335
9357
  if (members.length === 0) {
9336
9358
  throw new CompileError(
9337
- `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',
9338
9360
  );
9339
9361
  }
9340
9362
  const seen = new Set<string>();
9341
9363
  members.forEach((member, i) => {
9342
9364
  if (typeof member !== 'string' || member.length === 0) {
9343
- 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');
9344
9366
  }
9345
9367
  if (seen.has(member)) {
9346
9368
  throw new CompileError(
9347
9369
  `group "${name}" names member "${member}" twice (at index ${members.indexOf(member)} and ${i}). ` +
9348
9370
  'Member order is what `stagger` counts and what a per-member value map is read against, so a repeat is ' +
9349
- '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',
9350
9372
  );
9351
9373
  }
9352
9374
  seen.add(member);
@@ -9392,13 +9414,13 @@ function resolveMemberTrack(
9392
9414
  if ((carriesMap || carriesModel) && track.group === undefined && track.bone === undefined) {
9393
9415
  throw new CompileError(
9394
9416
  `${at}: per-member values need a target whose members are named — put them on a track that names a "group", or ` +
9395
- 'a "bone" for the one-member case',
9417
+ 'a "bone" for the one-member case', 'CE_COMPILE_MEMBER_VALUES_TARGET_UNNAMED',
9396
9418
  );
9397
9419
  }
9398
9420
  if (carriesMap && targetKind === 'bone') {
9399
9421
  throw new CompileError(
9400
9422
  `${at}: a per-member "v" map names members, and a bone track has one target rather than members. Write the ` +
9401
- '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',
9402
9424
  );
9403
9425
  }
9404
9426
 
@@ -9415,7 +9437,7 @@ function resolveMemberTrack(
9415
9437
  throw new CompileError(
9416
9438
  `${where}: this key carries both a "derive" model and a per-member "v" map — two answers to one question. ` +
9417
9439
  'The model states the arithmetic and the depths that produced the numbers; the map states the numbers. ' +
9418
- 'Pick one.',
9440
+ 'Pick one.', 'CE_COMPILE_MEMBER_DERIVE_AND_VALUE_MAP',
9419
9441
  );
9420
9442
  }
9421
9443
  if (key.derive === undefined) {
@@ -9430,7 +9452,7 @@ function resolveMemberTrack(
9430
9452
  if (!known.has(named)) {
9431
9453
  throw new CompileError(
9432
9454
  `${where}: the value map names "${named}", which group "${target}" does not declare ` +
9433
- `(its members are: ${targets.join(', ')})`,
9455
+ `(its members are: ${targets.join(', ')})`, 'CE_COMPILE_MEMBER_VALUE_NAME_UNKNOWN',
9434
9456
  );
9435
9457
  }
9436
9458
  }
@@ -9442,7 +9464,7 @@ function resolveMemberTrack(
9442
9464
  `(it states: ${Object.keys(table).join(', ') || 'nothing'}). ` +
9443
9465
  'A member is refused rather than defaulted: an absent value is exactly the thing a map of six is ' +
9444
9466
  'written to make visible, and defaulting it to the identity would key that one bone with a different ' +
9445
- 'motion in silence.',
9467
+ 'motion in silence.', 'CE_COMPILE_MEMBER_VALUE_MISSING',
9446
9468
  );
9447
9469
  }
9448
9470
  const value = table[name];
@@ -9478,7 +9500,7 @@ function resolveMemberTrack(
9478
9500
  throw new CompileError(
9479
9501
  `${where}: derive reads member "${name}"'s setup position, and this rig declares no such bone. ` +
9480
9502
  'A model over a group of slots or constraints has no coordinate to be evaluated at — state the values ' +
9481
- 'as a "v" map.',
9503
+ 'as a "v" map.', 'CE_COMPILE_MEMBER_DERIVE_TARGET_NOT_BONE',
9482
9504
  );
9483
9505
  }
9484
9506
  parents.add(bone.parent ?? '(root)');
@@ -9489,7 +9511,7 @@ function resolveMemberTrack(
9489
9511
  `${where}: derive measures every member's "${coordinate}" from its parent's origin and "about" says where the ` +
9490
9512
  `axis crosses it, so one space is what makes the members comparable. These members sit under ` +
9491
9513
  `${parents.size} different parents (${[...parents].join(', ')}), and their coordinates are therefore ` +
9492
- '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',
9493
9515
  );
9494
9516
  }
9495
9517
  const report = evaluateTrackDerive(key.derive, track.property, members, onModelGrid, where);
@@ -9549,7 +9571,7 @@ function compileTrack(
9549
9571
  if (shape === undefined) {
9550
9572
  throw new CompileError(
9551
9573
  `animation "${animName}" slot "${target}" has no timeline "${track.property}" ` +
9552
- `(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`,
9574
+ `(it has: ${Object.keys(SLOT_TRACKS).join(', ')})`, 'CE_COMPILE_SLOT_TRACK_PROPERTY_UNKNOWN',
9553
9575
  );
9554
9576
  }
9555
9577
  // 🚨 Raised here, with the target and before the keys, for the same reason as
@@ -9574,10 +9596,10 @@ function compileTrack(
9574
9596
  `${where}: slot "${target}" declares no setup "dark", and an "${shape}" timeline poses a slot's dark colour — ` +
9575
9597
  'the runtime allocates one only for a slot whose setup pose has it, so applying this animation throws ' +
9576
9598
  `instead of tinting. Give slot "${target}" a \`dark\` in the rig spec (the colour it holds at rest), or ` +
9577
- `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',
9578
9600
  );
9579
9601
  }
9580
- 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');
9581
9603
 
9582
9604
  const out: ModelKey[] = [];
9583
9605
  for (let i = 0; i < track.keys.length; i++) {
@@ -9585,13 +9607,13 @@ function compileTrack(
9585
9607
  const next = track.keys[i + 1];
9586
9608
  const time = keyTime(key.t + shift);
9587
9609
  if (i > 0 && time <= (out[i - 1].time as number)) {
9588
- 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');
9589
9611
  }
9590
9612
  checkKeyTime(where, time, key.t, duration);
9591
9613
 
9592
9614
  if (shape === 'attachment') {
9593
9615
  if (key.v !== null && typeof key.v !== 'string') {
9594
- 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');
9595
9617
  }
9596
9618
  // Resolved against every skin, not just `default` — see `SlotAttachmentIndex`.
9597
9619
  const known = attachments.bySlot.get(target);
@@ -9606,10 +9628,10 @@ function compileTrack(
9606
9628
  `(searched: ${attachments.searched.join(', ')}) — ` +
9607
9629
  (known === undefined || known.size === 0
9608
9630
  ? 'the slot has no attachments at all'
9609
- : `the slot has: ${[...known].join(', ')}`),
9631
+ : `the slot has: ${[...known].join(', ')}`), 'CE_COMPILE_ATTACHMENT_KEY_UNKNOWN',
9610
9632
  );
9611
9633
  }
9612
- 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');
9613
9635
  // Attachment timelines are inherently stepped — exactly what lip-sync wants.
9614
9636
  out.push({ time, name: key.v });
9615
9637
  continue;
@@ -9628,7 +9650,7 @@ function compileTrack(
9628
9650
  // a second thing to keep in step.
9629
9651
  const { spelling, channels } = COLOUR_KEYS[shape];
9630
9652
  const colourOf = (v: number[]): Record<string, string | number> => colourKey(shape, v);
9631
- 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');
9632
9654
  if (shape === 'alpha' && key.v.length === 1 && !(Number.isFinite(key.v[0]) && key.v[0] >= 0 && key.v[0] <= 1)) {
9633
9655
  // The one colour shape written as a number rather than a byte, so the one
9634
9656
  // that is not clamped onto 0..1 by `channelHex` on the way out. The
@@ -9639,15 +9661,15 @@ function compileTrack(
9639
9661
  // clamping is choosing the value.
9640
9662
  throw new CompileError(
9641
9663
  `${where}: key at t=${key.t} is ${String(key.v[0])}; an alpha is a number from 0 to 1 — the runtime clamps ` +
9642
- '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',
9643
9665
  );
9644
9666
  }
9645
9667
  const entry: ModelKey = { time, ...colourOf(key.v) };
9646
9668
  if (key.ease !== undefined && key.curve !== undefined) {
9647
- 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');
9648
9670
  }
9649
9671
  if (key.curve !== undefined) {
9650
- 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');
9651
9673
  entry.curve = rawCurve(key.curve, channels, where, String(key.t));
9652
9674
  out.push(entry);
9653
9675
  continue;
@@ -9657,9 +9679,9 @@ function compileTrack(
9657
9679
  entry.curve = 'stepped';
9658
9680
  } else {
9659
9681
  const handles = motion.easings?.[key.ease];
9660
- 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');
9661
9683
  if (!Array.isArray(next.v)) {
9662
- 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');
9663
9685
  }
9664
9686
  const t2 = keyTime(next.t + shift);
9665
9687
  // 4 numbers per channel, in the format's own channel order — 16 for
@@ -9674,7 +9696,7 @@ function compileTrack(
9674
9696
  entry.curve = easingCurve(handles, time, t2, key.v, next.v, held);
9675
9697
  }
9676
9698
  } else if (key.ease && !next) {
9677
- 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');
9678
9700
  }
9679
9701
  out.push(entry);
9680
9702
  }