spine-rigc 1.5.0 → 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 { 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
  *
@@ -1925,6 +2069,10 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
1925
2069
  const whole = (field: string, min: number): void => {
1926
2070
  const value = seq[field];
1927
2071
  if (value === undefined) return;
2072
+ // A number the file cannot carry is `refuseNumbersTheFileCannotCarry`'s,
2073
+ // one sentence for the whole family. This check runs inside the key scan,
2074
+ // before that walk, and printed a stated 1e309 as `null` (issue #881).
2075
+ if (typeof value === 'number' && !Number.isFinite(Math.fround(value))) return;
1928
2076
  if (typeof value !== 'number' || !Number.isInteger(value) || value < min) {
1929
2077
  throw new CompileError(
1930
2078
  `${where}: ${at}.${field} is ${JSON.stringify(value) ?? String(value)}; it is a whole number` +
@@ -1946,7 +2094,7 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
1946
2094
  whole('digits', 0);
1947
2095
  whole('setup', 0);
1948
2096
  const count = seq.count as number;
1949
- if (typeof seq.setup === 'number' && seq.setup >= count) {
2097
+ if (typeof seq.setup === 'number' && Number.isFinite(Math.fround(seq.setup)) && Number.isFinite(Math.fround(count)) && seq.setup >= count) {
1950
2098
  throw new CompileError(
1951
2099
  `${where}: ${at}.setup is ${seq.setup}, and a ${count}-frame series has frames 0 to ${count - 1}. ` +
1952
2100
  '`Sequence.resolveIndex` clamps an index at or past the end to the LAST frame (measured: `setup: 7` on ' +
@@ -1980,22 +2128,25 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
1980
2128
  * refusal, and naming its keys would be a second opinion on a fault already
1981
2129
  * reported (see the note at the head of `parseMotionSpec`).
1982
2130
  */
1983
- function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
1984
- const at = (node: unknown, shape: keyof typeof RIG_KEYS, what: string): void => {
1985
- 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 });
1986
2137
  };
1987
2138
 
1988
- at(raw, 'RigSpec', 'this rig spec');
1989
- at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"');
2139
+ at(raw, 'RigSpec', 'this rig spec', []);
2140
+ at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"', ['skeleton']);
1990
2141
 
1991
2142
  for (const [i, bone] of (Array.isArray(raw.bones) ? raw.bones : []).entries()) {
1992
2143
  const who = isObj(bone) && typeof bone.name === 'string' ? `bone "${bone.name}"` : `bones[${i}]`;
1993
- at(bone, 'RigBone', who);
1994
- 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']);
1995
2146
  }
1996
2147
 
1997
2148
  for (const [i, slot] of (Array.isArray(raw.slots) ? raw.slots : []).entries()) {
1998
- 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]);
1999
2150
  }
2000
2151
 
2001
2152
  for (const [i, constraint] of (Array.isArray(raw.constraints) ? raw.constraints : []).entries()) {
@@ -2006,32 +2157,32 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2006
2157
  // that exist; there is no key set to check it against and no honest one to
2007
2158
  // guess, so it goes past here to the message that can say something.
2008
2159
  if (shape === undefined) continue;
2009
- at(constraint, shape, `${named} (${String(constraint.type)})`);
2160
+ at(constraint, shape, `${named} (${String(constraint.type)})`, ['constraints', i]);
2010
2161
  for (const [from, entry] of Object.entries(isObj(constraint.properties) ? constraint.properties : {})) {
2011
- at(entry, 'RigTransformProperty', `${named} properties."${from}"`);
2162
+ at(entry, 'RigTransformProperty', `${named} properties."${from}"`, ['constraints', i, 'properties', from]);
2012
2163
  if (!isObj(entry)) continue;
2013
2164
  for (const [to, driven] of Object.entries(isObj(entry.to) ? entry.to : {})) {
2014
- at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`);
2165
+ at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`, ['constraints', i, 'properties', from, 'to', to]);
2015
2166
  }
2016
2167
  }
2017
2168
  }
2018
2169
 
2019
2170
  for (const [name, event] of Object.entries(isObj(raw.events) ? raw.events : {})) {
2020
- at(event, 'RigEvent', `event "${name}"`);
2171
+ at(event, 'RigEvent', `event "${name}"`, ['events', name]);
2021
2172
  }
2022
2173
 
2023
2174
  if (isObj(raw.invariants)) {
2024
- at(raw.invariants, 'RigInvariants', '"invariants"');
2175
+ at(raw.invariants, 'RigInvariants', '"invariants"', ['invariants']);
2025
2176
  for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
2026
- at(rule, 'RigDetachedRule', `invariants.detached[${i}]`);
2177
+ at(rule, 'RigDetachedRule', `invariants.detached[${i}]`, ['invariants', 'detached', i]);
2027
2178
  }
2028
2179
  for (const [i, rule] of (Array.isArray(raw.invariants.deformMayFold) ? raw.invariants.deformMayFold : []).entries()) {
2029
- at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`);
2180
+ at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`, ['invariants', 'deformMayFold', i]);
2030
2181
  }
2031
2182
  for (const [i, rule] of (Array.isArray(raw.invariants.consumerDrivenMix) ? raw.invariants.consumerDrivenMix : []).entries()) {
2032
- at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`);
2183
+ at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`, ['invariants', 'consumerDrivenMix', i]);
2033
2184
  }
2034
- at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes');
2185
+ at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes', ['invariants', 'idleDrivesMeshes']);
2035
2186
  }
2036
2187
 
2037
2188
  for (const [skinName, skin] of Object.entries(isObj(raw.skins) ? raw.skins : {})) {
@@ -2040,6 +2191,10 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2040
2191
  // likeliest cause (a slot left outside `attachments`), so it is reused
2041
2192
  // rather than restated — one refusal per fault.
2042
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];
2043
2198
  for (const [slot, placeholders] of Object.entries(parts.attachments)) {
2044
2199
  if (!isObj(placeholders)) continue;
2045
2200
  for (const [placeholder, att] of Object.entries(placeholders)) {
@@ -2071,14 +2226,15 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2071
2226
  'series would be dropped in silence. Remove it, or put it on a region or a mesh.',
2072
2227
  );
2073
2228
  }
2074
- at(att, shape, `${who} (${type})`);
2229
+ const here = [...base, slot, placeholder];
2230
+ at(att, shape, `${who} (${type})`, here);
2075
2231
  if (att.sequence !== undefined) {
2076
- at(att.sequence, 'RigSequence', `${who} "sequence"`);
2232
+ at(att.sequence, 'RigSequence', `${who} "sequence"`, [...here, 'sequence']);
2077
2233
  checkRigSequence(att, who, where);
2078
2234
  }
2079
2235
  for (const [i, vertex] of (Array.isArray(att.weights) ? att.weights : []).entries()) {
2080
2236
  for (const [j, binding] of (Array.isArray(vertex) ? vertex : []).entries()) {
2081
- at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`);
2237
+ at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`, [...here, 'weights', i, j]);
2082
2238
  }
2083
2239
  }
2084
2240
  if (!isObj(att.generator)) continue;
@@ -2087,17 +2243,54 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
2087
2243
  // Same rule as an unknown attachment type: an unknown `kind` is the mesh
2088
2244
  // builder's refusal, which can name the four that exist.
2089
2245
  if (genShape === undefined) continue;
2090
- at(gen, genShape, `${who} generator (${String(gen.kind)})`);
2091
- at(gen.bias, 'RigMeshBias', `${who} generator.bias`);
2092
- at(gen.depth, 'RigDepthMap', `${who} generator.depth`);
2093
- at(gen.soft, 'RigSoftRegion', `${who} generator.soft`);
2094
- 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']);
2095
2251
  for (const [i, entry] of (Array.isArray(gen.bones) ? gen.bones : []).entries()) {
2096
- at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`);
2252
+ at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`, [...here, 'generator', 'bones', i]);
2097
2253
  }
2098
2254
  }
2099
2255
  }
2100
2256
  }
2257
+ return visits;
2258
+ }
2259
+
2260
+
2261
+ /**
2262
+ * A rig-spec path in the words the other refusals in this file use for it:
2263
+ * `bone "hip" x`, `constraint "aim" mixRotate`, `skin "default" slot "tail"
2264
+ * attachment "tail" weights[0][1].x`, `event "step" float`. Anything else is
2265
+ * the dotted path, which names every number exactly.
2266
+ */
2267
+ function rigPlace(raw: Record<string, unknown>): (path: ReadonlyArray<string | number>) => string {
2268
+ const named = (list: unknown, i: number, kind: string, plural: string): string => {
2269
+ const node = Array.isArray(list) ? list[i] : undefined;
2270
+ return isObj(node) && typeof node.name === 'string' ? `${kind} ${JSON.stringify(node.name)}` : `${plural}[${i}]`;
2271
+ };
2272
+ const rest = (path: ReadonlyArray<string | number>): string => {
2273
+ const tail = dottedPath(path);
2274
+ return tail === '' ? '' : ` ${tail}`;
2275
+ };
2276
+ return (path) => {
2277
+ const [head, second] = path;
2278
+ if (typeof second === 'number') {
2279
+ if (head === 'bones') return `${named(raw.bones, second, 'bone', 'bones')}${rest(path.slice(2))}`;
2280
+ if (head === 'slots') return `${named(raw.slots, second, 'slot', 'slots')}${rest(path.slice(2))}`;
2281
+ if (head === 'constraints') return `${named(raw.constraints, second, 'constraint', 'constraints')}${rest(path.slice(2))}`;
2282
+ }
2283
+ if (head === 'events' && typeof second === 'string') return `event ${JSON.stringify(second)}${rest(path.slice(2))}`;
2284
+ if (head === 'skins' && typeof second === 'string') {
2285
+ const inner = path[2] === 'attachments' ? path.slice(3) : path.slice(2);
2286
+ const [slot, attachment] = inner;
2287
+ if (typeof slot === 'string' && typeof attachment === 'string') {
2288
+ return `skin ${JSON.stringify(second)} slot ${JSON.stringify(slot)} attachment ${JSON.stringify(attachment)}${rest(inner.slice(2))}`;
2289
+ }
2290
+ return `skin ${JSON.stringify(second)}${rest(path.slice(2))}`;
2291
+ }
2292
+ return dottedPath(path);
2293
+ };
2101
2294
  }
2102
2295
 
2103
2296
  /**
@@ -2136,7 +2329,24 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
2136
2329
  // compiler does not read" for a format it has never read at all. The three
2137
2330
  // throws directly below are the required-key checks, and each of them is the
2138
2331
  // consequence a typo at the root produces.
2139
- 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
+ );
2346
+ // After the key check, so a misspelled key is named as a misspelling before
2347
+ // its value is judged, and before every reader below, so no range rule or
2348
+ // derived number ever meets a value the file cannot carry (issue #881).
2349
+ refuseNumbersTheFileCannotCarry(raw, where, place);
2140
2350
 
2141
2351
  if (typeof raw.name !== 'string' || raw.name.length === 0) {
2142
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
  // ---------------------------------------------------------------------------
package/src/validate.ts CHANGED
@@ -82,6 +82,7 @@ import {
82
82
  type SpineGeneration,
83
83
  } from './generation.ts';
84
84
  import { colourTypeName, readPngHeader } from './png.ts';
85
+ import { nonFiniteOfPosed } from './render.ts';
85
86
  import { BONE_INHERIT_KNOWN } from './rig.ts';
86
87
  import {
87
88
  CHANNELS_BY_KIND,
@@ -4400,6 +4401,26 @@ export function validate(input: ValidateInput): ValidateReport {
4400
4401
  }
4401
4402
  if (unresolved > 0) return;
4402
4403
 
4404
+ // -- the whole world transform, at the setup pose and every stepped frame
4405
+ //
4406
+ // 🚨 Read through `nonFiniteOfPosed`, the scan `render` refuses on, and
4407
+ // not a second opinion of it (issue #882). This loop read `worldX` and
4408
+ // `worldY` alone until then, and a bone at `rotation: 1e309` — in its setup
4409
+ // pose or as a `rotate` key — poses a finite position over a NaN `a`, `b`,
4410
+ // `c` and `d` whenever it has no child offset from it: measured on every
4411
+ // leaf bone of the three generated probes, 20 of 20 green through the whole
4412
+ // gate, and `render` then refused the same file by the bone. So the gate
4413
+ // said the skeleton was fine and the renderer said it was not, which is two
4414
+ // definitions of one word. The scan reads the six terms of every bone, then
4415
+ // the vertices of every region and mesh shown — because a bone can be
4416
+ // finite over a vertex that is not (a `scaleX` chain whose product stays
4417
+ // under the largest double while every corner of the child's region
4418
+ // passes it) — and names the bone or the vertex, the term and the frame.
4419
+ //
4420
+ // ⚠️ The setup pose is its own surface and is read as such: every frame
4421
+ // below is posed AFTER `state.apply`, so a bone the animation keys from
4422
+ // t=0 never shows its setup value to the loop, and a runtime that shows
4423
+ // the rig at rest does show it.
4403
4424
  for (const anim of data.animations) {
4404
4425
  const skeleton = new Skeleton(data);
4405
4426
  const state = new AnimationState(new AnimationStateData(data));
@@ -4407,18 +4428,27 @@ export function validate(input: ValidateInput): ValidateReport {
4407
4428
  skeleton.setupPose();
4408
4429
  skeleton.update(0);
4409
4430
  skeleton.updateWorldTransform(Physics.reset);
4431
+ const atSetup = nonFiniteOfPosed('the setup pose', skeleton);
4432
+ if (atSetup !== null) {
4433
+ fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
4434
+ return;
4435
+ }
4410
4436
  const step = Math.max(anim.duration, 1) / STEP_FRAMES;
4411
4437
  for (let i = 0; i < STEP_FRAMES; i++) {
4412
4438
  state.update(step);
4413
4439
  state.apply(skeleton);
4414
4440
  skeleton.update(step);
4415
4441
  skeleton.updateWorldTransform(Physics.update);
4442
+ const found = nonFiniteOfPosed(
4443
+ `animation ${JSON.stringify(anim.name)} frame ${i + 1} of ${STEP_FRAMES} (t=${((i + 1) * step).toFixed(4)}s)`,
4444
+ skeleton,
4445
+ );
4446
+ if (found !== null) {
4447
+ fail('A10_NO_NAN_AFTER_STEPPING', found);
4448
+ return;
4449
+ }
4416
4450
  for (const bone of skeleton.bones) {
4417
4451
  const pose = bone.appliedPose;
4418
- if (!Number.isFinite(pose.worldX) || !Number.isFinite(pose.worldY)) {
4419
- fail('A10_NO_NAN_AFTER_STEPPING', `${anim.name}: bone "${bone.data.name}" world is (${pose.worldX}, ${pose.worldY})`);
4420
- return;
4421
- }
4422
4452
  // The setup half of the clause above: a bone's own `inherit` goes
4423
4453
  // through the same lookup, and a miss there loads `undefined` into
4424
4454
  // the setup pose, which every frame copies. Only reachable on a