spine-rigc 1.5.1 → 1.5.2

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/rig.ts CHANGED
@@ -60,7 +60,8 @@
60
60
  * with no manifest at all declares them here.
61
61
  */
62
62
  import { CompileError, NotImplementedError } from './errors.ts';
63
- import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys } from './keys.ts';
63
+ import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType } from './keys.ts';
64
+ import type { ShapeVisit, SpecValueType } from './keys.ts';
64
65
 
65
66
  export { CompileError, NotImplementedError };
66
67
 
@@ -1776,11 +1777,11 @@ function isObj(v: unknown): v is Record<string, unknown> {
1776
1777
  * Every key each shape of this format owns, keyed by the interface above that
1777
1778
  * declares it — the runtime shadow of the types, which TypeScript erases.
1778
1779
  *
1779
- * 🔒 **Hand-written and mechanically held to the interfaces.** `KEY01` in
1780
+ * 🔒 **Hand-written and mechanically held to the interfaces.** `CUR17` in
1780
1781
  * `selftest.ts` reads this file's own source, extracts each named interface's
1781
1782
  * field list and compares it to the entry here, so the pair cannot drift: adding
1782
1783
  * a field and forgetting this table is a red run, not a key an author cannot
1783
- * write. `KEY02` closes the other direction — a key declared here and occurring
1784
+ * write. `CUR18` closes the other direction — a key declared here and occurring
1784
1785
  * nowhere else in the tree is refused, which is exactly what `scaleYMode` was.
1785
1786
  *
1786
1787
  * ⚠️ `RigUnimplementedAttachment` is deliberately absent. It carries
@@ -1854,6 +1855,149 @@ export const RIG_KEYS = {
1854
1855
  RigSoftRegion: ['bone', 'mask'],
1855
1856
  } as const satisfies Record<string, readonly string[]>;
1856
1857
 
1858
+ /**
1859
+ * The type every key of `RIG_KEYS` holds, shape by shape — what
1860
+ * `refuseValuesOfTheWrongType` refuses a value against (issue #890).
1861
+ *
1862
+ * 🔒 **Beside the key table, and held equal to it twice.** `satisfies` over
1863
+ * `RigTypeTable` makes a key typed here and absent there, or admitted there and
1864
+ * untyped here, a type error; the selftest compares the two at runtime as well,
1865
+ * and derives each checked type from the interface the row is named for, so a
1866
+ * field declared `number` and typed `string` here is a red run rather than a
1867
+ * refusal of correct work.
1868
+ *
1869
+ * Where an unchecked type (`object`, `enum`, `mixed` — see `SPEC_VALUE_TYPES`)
1870
+ * is refused, and by what:
1871
+ *
1872
+ * - the version tag `spec` — `parseRigSpec`, before anything else;
1873
+ * - `inherit` — `resolveBoneInherit`; `blend` — the slot loop
1874
+ * (`RIG_SLOT_BLEND`); `consumerDrivenMix[].type` — the invariants block;
1875
+ * - a constraint's `type` — `buildRigConstraint`; `scaleY` on ik, a path
1876
+ * constraint's three modes and a slider's `property` — the builder of that
1877
+ * constraint kind, each listing the names that exist;
1878
+ * - an attachment's `type` — the attachment reader; `depth.near` — the depth
1879
+ * map reader;
1880
+ * - `RigSegmentsGenerator.bones` (`mixed`: a name, a chain of names or a span)
1881
+ * — the segments generator, by index.
1882
+ *
1883
+ * 🚨 Measured when this table was written, three `enum` keys have **no** owner,
1884
+ * and the table does not pretend otherwise: `boneIndexing` built green at `5`
1885
+ * and at `"foo"` (on a weighted mesh too), `from.rotation` built green at `5`
1886
+ * and at `"foo"` on an anchored bone, and a generator `kind` of `5` or `"foo"`
1887
+ * threw a `TypeError` (*undefined is not an object (evaluating
1888
+ * 'generator.size')*) rather than the mesh builder's refusal. Each is a closed
1889
+ * set of names with nobody holding the name, which is a value refusal and not a
1890
+ * type one, so it is not fixed here. `scaleY` on a physics constraint was not
1891
+ * measured.
1892
+ */
1893
+ type RigTypeTable = {
1894
+ readonly [S in keyof typeof RIG_KEYS]: { readonly [K in (typeof RIG_KEYS)[S][number]]: SpecValueType };
1895
+ };
1896
+
1897
+ export const RIG_TYPES = {
1898
+ RigSpec: {
1899
+ spec: 'enum', name: 'string', note: 'string', skeleton: 'object', images: 'string', bones: 'object[]', slots: 'object[]',
1900
+ skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object',
1901
+ },
1902
+ RigSkeletonHeader: {
1903
+ x: 'number', y: 'number', width: 'number | null', height: 'number | null', fps: 'number', referenceScale: 'number',
1904
+ images: 'string', audio: 'string | null',
1905
+ },
1906
+ RigBone: {
1907
+ name: 'string', parent: 'string', length: 'number', x: 'number', y: 'number', rotation: 'number', scaleX: 'number',
1908
+ scaleY: 'number', shearX: 'number', shearY: 'number', inherit: 'enum', skin: 'boolean', color: 'string', icon: 'string',
1909
+ from: 'object',
1910
+ },
1911
+ RigBoneFrom: { anchor: 'string', slotWindow: 'string', meshCenter: 'string', rotation: 'enum' },
1912
+ RigSlot: { name: 'string', bone: 'string', attachment: 'string | null', color: 'string', dark: 'string', blend: 'enum' },
1913
+ RigEvent: { int: 'number', float: 'number', string: 'string', audio: 'string', volume: 'number', balance: 'number' },
1914
+ RigInvariants: {
1915
+ meshSlots: 'number', meshTriangles: 'number', axisBone: 'string', massBone: 'string', detached: 'object[]',
1916
+ deformMayFold: 'object[]', editorRoundTrip: 'boolean', consumerDrivenMix: 'object[]', idleDrivesMeshes: 'object',
1917
+ },
1918
+ RigIdleDrivesMeshes: { why: 'string' },
1919
+ RigDetachedRule: { bone: 'string', notUnder: 'string', why: 'string' },
1920
+ RigDeformFoldExemption: { slot: 'string', why: 'string' },
1921
+ RigConsumerDrivenMix: { constraint: 'string', type: 'enum', why: 'string' },
1922
+ RigSkinEntry: {
1923
+ attachments: 'map of object', bones: 'string[]', ik: 'string[]', transform: 'string[]', path: 'string[]',
1924
+ physics: 'string[]', slider: 'string[]',
1925
+ },
1926
+ RigIkConstraint: {
1927
+ name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', target: 'string', scaleY: 'enum', mix: 'number',
1928
+ softness: 'number', bendPositive: 'boolean', compress: 'boolean', stretch: 'boolean',
1929
+ },
1930
+ RigTransformConstraint: {
1931
+ name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', source: 'string', localSource: 'boolean',
1932
+ localTarget: 'boolean', additive: 'boolean', clamp: 'boolean', properties: 'map of object',
1933
+ rotation: 'number', x: 'number', y: 'number', scaleX: 'number', scaleY: 'number', shearY: 'number',
1934
+ mixRotate: 'number', mixX: 'number', mixY: 'number', mixScaleX: 'number', mixScaleY: 'number', mixShearY: 'number',
1935
+ },
1936
+ RigPathConstraint: {
1937
+ name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', slot: 'string', positionMode: 'enum',
1938
+ spacingMode: 'enum', rotateMode: 'enum', rotation: 'number', position: 'number', spacing: 'number',
1939
+ mixRotate: 'number', mixX: 'number', mixY: 'number',
1940
+ },
1941
+ RigPhysicsConstraint: {
1942
+ name: 'string', skin: 'boolean', type: 'enum', bone: 'string', x: 'number', y: 'number', rotate: 'number',
1943
+ scaleX: 'number', shearX: 'number', scaleY: 'enum', limit: 'number', fps: 'number', inertia: 'number',
1944
+ strength: 'number', damping: 'number', mass: 'number', wind: 'number', gravity: 'number', mix: 'number',
1945
+ inertiaGlobal: 'boolean', strengthGlobal: 'boolean', dampingGlobal: 'boolean', massGlobal: 'boolean',
1946
+ windGlobal: 'boolean', gravityGlobal: 'boolean', mixGlobal: 'boolean',
1947
+ },
1948
+ RigSliderConstraint: {
1949
+ name: 'string', skin: 'boolean', type: 'enum', animation: 'string', mix: 'number', additive: 'boolean',
1950
+ loop: 'boolean', bone: 'string', property: 'enum', from: 'number', to: 'number', scale: 'number', max: 'number',
1951
+ local: 'boolean', time: 'number',
1952
+ },
1953
+ RigTransformProperty: { offset: 'number', to: 'map of object' },
1954
+ RigTransformTo: { offset: 'number', max: 'number', scale: 'number' },
1955
+ RigRegionAttachment: {
1956
+ type: 'enum', name: 'string', path: 'string', image: 'string', x: 'number', y: 'number', rotation: 'number',
1957
+ scaleX: 'number', scaleY: 'number', width: 'number', height: 'number', color: 'string', sequence: 'object',
1958
+ },
1959
+ RigMeshAttachment: {
1960
+ type: 'enum', name: 'string', path: 'string', image: 'string', uvs: 'number[]', triangles: 'number[]',
1961
+ vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', hull: 'number', edges: 'number[]',
1962
+ width: 'number', height: 'number', color: 'string', generator: 'object', sequence: 'object',
1963
+ },
1964
+ RigLinkedMeshAttachment: {
1965
+ type: 'enum', name: 'string', path: 'string', image: 'string', source: 'string', slot: 'string', skin: 'string',
1966
+ timelines: 'boolean', width: 'number', height: 'number', color: 'string', sequence: 'object',
1967
+ uvs: 'number[]', triangles: 'number[]', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum',
1968
+ hull: 'number', edges: 'number[]', generator: 'object',
1969
+ },
1970
+ RigMeshBinding: { bone: 'string', x: 'number', y: 'number', weight: 'number' },
1971
+ RigSequence: { count: 'number', start: 'number', digits: 'number', setup: 'number' },
1972
+ RigBoundingBoxAttachment: {
1973
+ vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
1974
+ },
1975
+ RigClippingAttachment: {
1976
+ vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
1977
+ end: 'string', convex: 'boolean', inverse: 'boolean',
1978
+ },
1979
+ RigPathAttachment: {
1980
+ vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
1981
+ closed: 'boolean', constantSpeed: 'boolean', lengths: 'number[]',
1982
+ },
1983
+ RigRingGenerator: {
1984
+ kind: 'enum', hull: 'number[][]', center: 'number[]', inner: 'number', size: 'number[]', bias: 'object',
1985
+ controls: 'string[]',
1986
+ },
1987
+ RigRibbonGenerator: { kind: 'enum', size: 'number[]', rows: 'number', chain: 'string[]' },
1988
+ RigContourGenerator: {
1989
+ kind: 'enum', tolerance: 'number', margin: 'number', maxVertices: 'number', alpha: 'number', depth: 'object',
1990
+ soft: 'object',
1991
+ },
1992
+ RigGridGenerator: { kind: 'enum', us: 'number[]', vs: 'number[]', cols: 'number', rows: 'number', depth: 'object', soft: 'object' },
1993
+ RigSegmentsGenerator: { kind: 'enum', cell: 'number', bones: 'mixed', falloff: 'object', alpha: 'number', anchor: 'number[]' },
1994
+ RigSegmentsFalloff: { power: 'number', radius: 'number', maxBones: 'number', minWeight: 'number' },
1995
+ RigSegmentSpan: { bone: 'string', from: 'number[]', to: 'number[]' },
1996
+ RigMeshBias: { axis_deg: 'number', ramp: 'number[]' },
1997
+ RigDepthMap: { image: 'string', near: 'enum', zScale: 'number', gamma: 'number', contrast: 'number', bias: 'number' },
1998
+ RigSoftRegion: { bone: 'string', mask: 'string' },
1999
+ } as const satisfies RigTypeTable;
2000
+
1857
2001
  /**
1858
2002
  * The five constraint `type` names, and the shape each one's keys come from.
1859
2003
  *
@@ -1984,22 +2128,25 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
1984
2128
  * refusal, and naming its keys would be a second opinion on a fault already
1985
2129
  * reported (see the note at the head of `parseMotionSpec`).
1986
2130
  */
1987
- function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
1988
- const at = (node: unknown, shape: keyof typeof RIG_KEYS, what: string): void => {
1989
- if (isObj(node)) refuseUnknownKeys(node, RIG_KEYS[shape], where, what);
2131
+ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{ node: Record<string, unknown>; shape: keyof typeof RIG_KEYS; path: Array<string | number> }> {
2132
+ const visits: Array<{ node: Record<string, unknown>; shape: keyof typeof RIG_KEYS; path: Array<string | number> }> = [];
2133
+ const at = (node: unknown, shape: keyof typeof RIG_KEYS, what: string, path: Array<string | number>): void => {
2134
+ if (!isObj(node)) return;
2135
+ refuseUnknownKeys(node, RIG_KEYS[shape], where, what);
2136
+ visits.push({ node, shape, path });
1990
2137
  };
1991
2138
 
1992
- at(raw, 'RigSpec', 'this rig spec');
1993
- at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"');
2139
+ at(raw, 'RigSpec', 'this rig spec', []);
2140
+ at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"', ['skeleton']);
1994
2141
 
1995
2142
  for (const [i, bone] of (Array.isArray(raw.bones) ? raw.bones : []).entries()) {
1996
2143
  const who = isObj(bone) && typeof bone.name === 'string' ? `bone "${bone.name}"` : `bones[${i}]`;
1997
- at(bone, 'RigBone', who);
1998
- if (isObj(bone)) at(bone.from, 'RigBoneFrom', `${who}'s "from"`);
2144
+ at(bone, 'RigBone', who, ['bones', i]);
2145
+ if (isObj(bone)) at(bone.from, 'RigBoneFrom', `${who}'s "from"`, ['bones', i, 'from']);
1999
2146
  }
2000
2147
 
2001
2148
  for (const [i, slot] of (Array.isArray(raw.slots) ? raw.slots : []).entries()) {
2002
- at(slot, 'RigSlot', isObj(slot) && typeof slot.name === 'string' ? `slot "${slot.name}"` : `slots[${i}]`);
2149
+ at(slot, 'RigSlot', isObj(slot) && typeof slot.name === 'string' ? `slot "${slot.name}"` : `slots[${i}]`, ['slots', i]);
2003
2150
  }
2004
2151
 
2005
2152
  for (const [i, constraint] of (Array.isArray(raw.constraints) ? raw.constraints : []).entries()) {
@@ -2010,32 +2157,32 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2010
2157
  // that exist; there is no key set to check it against and no honest one to
2011
2158
  // guess, so it goes past here to the message that can say something.
2012
2159
  if (shape === undefined) continue;
2013
- at(constraint, shape, `${named} (${String(constraint.type)})`);
2160
+ at(constraint, shape, `${named} (${String(constraint.type)})`, ['constraints', i]);
2014
2161
  for (const [from, entry] of Object.entries(isObj(constraint.properties) ? constraint.properties : {})) {
2015
- at(entry, 'RigTransformProperty', `${named} properties."${from}"`);
2162
+ at(entry, 'RigTransformProperty', `${named} properties."${from}"`, ['constraints', i, 'properties', from]);
2016
2163
  if (!isObj(entry)) continue;
2017
2164
  for (const [to, driven] of Object.entries(isObj(entry.to) ? entry.to : {})) {
2018
- at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`);
2165
+ at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`, ['constraints', i, 'properties', from, 'to', to]);
2019
2166
  }
2020
2167
  }
2021
2168
  }
2022
2169
 
2023
2170
  for (const [name, event] of Object.entries(isObj(raw.events) ? raw.events : {})) {
2024
- at(event, 'RigEvent', `event "${name}"`);
2171
+ at(event, 'RigEvent', `event "${name}"`, ['events', name]);
2025
2172
  }
2026
2173
 
2027
2174
  if (isObj(raw.invariants)) {
2028
- at(raw.invariants, 'RigInvariants', '"invariants"');
2175
+ at(raw.invariants, 'RigInvariants', '"invariants"', ['invariants']);
2029
2176
  for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
2030
- at(rule, 'RigDetachedRule', `invariants.detached[${i}]`);
2177
+ at(rule, 'RigDetachedRule', `invariants.detached[${i}]`, ['invariants', 'detached', i]);
2031
2178
  }
2032
2179
  for (const [i, rule] of (Array.isArray(raw.invariants.deformMayFold) ? raw.invariants.deformMayFold : []).entries()) {
2033
- at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`);
2180
+ at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`, ['invariants', 'deformMayFold', i]);
2034
2181
  }
2035
2182
  for (const [i, rule] of (Array.isArray(raw.invariants.consumerDrivenMix) ? raw.invariants.consumerDrivenMix : []).entries()) {
2036
- at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`);
2183
+ at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`, ['invariants', 'consumerDrivenMix', i]);
2037
2184
  }
2038
- at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes');
2185
+ at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes', ['invariants', 'idleDrivesMeshes']);
2039
2186
  }
2040
2187
 
2041
2188
  for (const [skinName, skin] of Object.entries(isObj(raw.skins) ? raw.skins : {})) {
@@ -2044,6 +2191,10 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2044
2191
  // likeliest cause (a slot left outside `attachments`), so it is reused
2045
2192
  // rather than restated — one refusal per fault.
2046
2193
  const parts = splitRigSkin(skin as RigSkin, `${where}: skin "${skinName}"`);
2194
+ // The long form's own lists are typed too; `splitRigSkin` has just refused
2195
+ // a key outside them, so the row's keys are the node's keys.
2196
+ if (parts.explicit) visits.push({ node: skin, shape: 'RigSkinEntry', path: ['skins', skinName] });
2197
+ const base: Array<string | number> = parts.explicit ? ['skins', skinName, 'attachments'] : ['skins', skinName];
2047
2198
  for (const [slot, placeholders] of Object.entries(parts.attachments)) {
2048
2199
  if (!isObj(placeholders)) continue;
2049
2200
  for (const [placeholder, att] of Object.entries(placeholders)) {
@@ -2075,14 +2226,15 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2075
2226
  'series would be dropped in silence. Remove it, or put it on a region or a mesh.',
2076
2227
  );
2077
2228
  }
2078
- at(att, shape, `${who} (${type})`);
2229
+ const here = [...base, slot, placeholder];
2230
+ at(att, shape, `${who} (${type})`, here);
2079
2231
  if (att.sequence !== undefined) {
2080
- at(att.sequence, 'RigSequence', `${who} "sequence"`);
2232
+ at(att.sequence, 'RigSequence', `${who} "sequence"`, [...here, 'sequence']);
2081
2233
  checkRigSequence(att, who, where);
2082
2234
  }
2083
2235
  for (const [i, vertex] of (Array.isArray(att.weights) ? att.weights : []).entries()) {
2084
2236
  for (const [j, binding] of (Array.isArray(vertex) ? vertex : []).entries()) {
2085
- at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`);
2237
+ at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`, [...here, 'weights', i, j]);
2086
2238
  }
2087
2239
  }
2088
2240
  if (!isObj(att.generator)) continue;
@@ -2091,17 +2243,18 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2091
2243
  // Same rule as an unknown attachment type: an unknown `kind` is the mesh
2092
2244
  // builder's refusal, which can name the four that exist.
2093
2245
  if (genShape === undefined) continue;
2094
- at(gen, genShape, `${who} generator (${String(gen.kind)})`);
2095
- at(gen.bias, 'RigMeshBias', `${who} generator.bias`);
2096
- at(gen.depth, 'RigDepthMap', `${who} generator.depth`);
2097
- at(gen.soft, 'RigSoftRegion', `${who} generator.soft`);
2098
- at(gen.falloff, 'RigSegmentsFalloff', `${who} generator.falloff`);
2246
+ at(gen, genShape, `${who} generator (${String(gen.kind)})`, [...here, 'generator']);
2247
+ at(gen.bias, 'RigMeshBias', `${who} generator.bias`, [...here, 'generator', 'bias']);
2248
+ at(gen.depth, 'RigDepthMap', `${who} generator.depth`, [...here, 'generator', 'depth']);
2249
+ at(gen.soft, 'RigSoftRegion', `${who} generator.soft`, [...here, 'generator', 'soft']);
2250
+ at(gen.falloff, 'RigSegmentsFalloff', `${who} generator.falloff`, [...here, 'generator', 'falloff']);
2099
2251
  for (const [i, entry] of (Array.isArray(gen.bones) ? gen.bones : []).entries()) {
2100
- at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`);
2252
+ at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`, [...here, 'generator', 'bones', i]);
2101
2253
  }
2102
2254
  }
2103
2255
  }
2104
2256
  }
2257
+ return visits;
2105
2258
  }
2106
2259
 
2107
2260
 
@@ -2176,11 +2329,24 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
2176
2329
  // compiler does not read" for a format it has never read at all. The three
2177
2330
  // throws directly below are the required-key checks, and each of them is the
2178
2331
  // consequence a typo at the root produces.
2179
- checkRigSpecKeys(raw, where);
2332
+ const visits = checkRigSpecKeys(raw, where);
2333
+ const place = rigPlace(raw);
2334
+ // Every value the scan admitted, against the type its key holds, before any
2335
+ // reader — here or in `compile` — has done arithmetic on it (issue #890).
2336
+ // ⚠️ It runs ahead of this function's own field checks on purpose: those read
2337
+ // values too, and a reader that meets a wrong type names the wrong fault —
2338
+ // `constraint.skin === true` reads `"skin": "true"` as a constraint that asks
2339
+ // for no skin. The price, measured, is three controls whose type half pinned an
2340
+ // older sentence (RF86, PS177, PS189); their other halves are unchanged.
2341
+ refuseValuesOfTheWrongType(
2342
+ visits.map((v) => ({ node: v.node, shape: v.shape, name: (tail) => place([...v.path, ...tail]) })),
2343
+ RIG_TYPES,
2344
+ where,
2345
+ );
2180
2346
  // After the key check, so a misspelled key is named as a misspelling before
2181
2347
  // its value is judged, and before every reader below, so no range rule or
2182
2348
  // derived number ever meets a value the file cannot carry (issue #881).
2183
- refuseNumbersTheFileCannotCarry(raw, where, rigPlace(raw));
2349
+ refuseNumbersTheFileCannotCarry(raw, where, place);
2184
2350
 
2185
2351
  if (typeof raw.name !== 'string' || raw.name.length === 0) {
2186
2352
  throw new CompileError(`${where}: a rig spec needs a "name" — a motion spec names it to pick this rig`);
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  import type { AtlasRegion } from './atlas.ts';
18
18
  import type { DeformTransform, DeformTransformReport } from './deformgen.ts';
19
19
  import type { TurnCeiling } from './depth.ts';
20
+ import type { SpecTypeRow } from './keys.ts';
20
21
  import type { MeshKind } from './mesh.ts';
21
22
  import type { TrackDerive, TrackDeriveReport } from './trackgen.ts';
22
23
 
@@ -169,6 +170,47 @@ export interface FaceManifest {
169
170
  anchors?: Record<string, number[]>;
170
171
  }
171
172
 
173
+ /**
174
+ * The type every field of the cut manifest holds, shape by shape — what
175
+ * `refuseValuesOfTheWrongType` refuses a manifest value against (issue #890).
176
+ * The three interfaces above name three rows; the inline object types they
177
+ * declare (`crop`, `axis`, `stroke`, `roi`, `mesh.bias`) are rows named
178
+ * `<interface>.<field>`, and the selftest derives every row's keys and checked
179
+ * types from the declarations here.
180
+ *
181
+ * ⚠️ A manifest has no key scan, and this table does not add one: a manifest is
182
+ * often also the record of the pipeline that made the art and carries fields
183
+ * rigc does not read. A key outside a row is left alone, exactly as before; a
184
+ * key inside one must hold its type. Measured before the walk existed, 62 of 84
185
+ * wrong-typed plants on the three fixture manifests built green —
186
+ * `crop.h: "256"` among them — and 4 threw a TypeError from `node:path`.
187
+ *
188
+ * Unchecked: `mesh.kind` and `mesh.hull` (`enum`) — the manifest mesh reader;
189
+ * `crop` and the other nested shapes, `parts` and `mesh` (`object`) — the
190
+ * readers that walk them.
191
+ */
192
+ export const MANIFEST_TYPES = {
193
+ FaceManifest: {
194
+ schema: 'string', crop: 'object', base: 'string', state_machine: 'map of string[]', parts: 'object[]',
195
+ insertion: 'number[]', axis: 'object', stroke: 'object', roi: 'object', anchors: 'map of number[]',
196
+ },
197
+ 'FaceManifest.crop': { x: 'number', y: 'number', w: 'number', h: 'number', resample: 'string' },
198
+ 'FaceManifest.axis': { deg: 'number', unit: 'number[]' },
199
+ 'FaceManifest.stroke': {
200
+ amplitude: 'number', extension: 'number', contact_depth: 'number | null', cap_containment_ceiling: 'number | null',
201
+ },
202
+ 'FaceManifest.roi': { x: 'number', y: 'number', w: 'number', h: 'number' },
203
+ FaceManifestPart: {
204
+ slot: 'string', rig_slot: 'string', draw_order: 'number', image: 'string | null', offset: 'number[]', size: 'number[]',
205
+ state_key: 'string', states: 'map of string | null', polygon: 'number[][]', mesh: 'object',
206
+ },
207
+ FaceManifestMesh: {
208
+ kind: 'enum', hull: 'enum', center: 'number[]', inner: 'number', control_bone: 'string', control_bones: 'string[]',
209
+ rows: 'number', chain: 'string[]', bias: 'object',
210
+ },
211
+ 'FaceManifestMesh.bias': { axis_deg: 'number', ramp: 'number[]', note: 'string' },
212
+ } as const satisfies Record<string, SpecTypeRow>;
213
+
172
214
  // ---------------------------------------------------------------------------
173
215
  // Motion spec (spec: "rigc-motion/1")
174
216
  // ---------------------------------------------------------------------------