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/docs/AUTHORING.md +75 -19
- package/docs/INGEST.md +20 -13
- package/docs/MOTION.md +138 -85
- package/package.json +1 -1
- package/src/compile.ts +35 -1
- package/src/keys.ts +297 -2
- package/src/motion.ts +123 -3
- package/src/pose.ts +84 -6
- package/src/render.ts +32 -0
- package/src/rig.ts +241 -31
- package/src/types.ts +42 -0
- package/src/validate.ts +34 -4
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.** `
|
|
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. `
|
|
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):
|
|
1984
|
-
const
|
|
1985
|
-
|
|
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
|
-
|
|
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
|