spine-rigc 1.5.1 → 1.5.3
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 +43 -10
- package/docs/MOTION.md +125 -78
- package/package.json +1 -1
- package/src/compile.ts +36 -1
- package/src/keys.ts +286 -4
- package/src/motion.ts +137 -3
- package/src/pose.ts +53 -0
- package/src/rig.ts +270 -36
- package/src/types.ts +70 -0
- package/src/validate.ts +112 -56
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, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
64
|
+
import type { ShapeVisit, SpecEnumTable, 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,204 @@ 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 `object` or `mixed` row is refused: a nested shape by the
|
|
1870
|
+
* reader that walks it; `RigSegmentsGenerator.bones` (`mixed`: a name, a chain
|
|
1871
|
+
* of names or a span) by the segments generator, by index. Where an `enum` row
|
|
1872
|
+
* is refused is not a list in prose any more: `RIG_ENUMS`, below, has one entry
|
|
1873
|
+
* per `enum` row — a set refused by `refuseValuesOutsideTheirSet`, or the
|
|
1874
|
+
* reader that refuses it — and `satisfies` makes a row without one a type error.
|
|
1875
|
+
*
|
|
1876
|
+
* ⚠️ Until issue #900 this comment carried that list, and ended by naming three
|
|
1877
|
+
* `enum` keys with **no** owner: `boneIndexing`, `from.rotation` and a
|
|
1878
|
+
* generator `kind`. The list was right about the three and wrong by omission
|
|
1879
|
+
* about a fourth — the cut manifest's `mesh.kind`, which `MANIFEST_TYPES` said
|
|
1880
|
+
* "the manifest mesh reader" held and which that reader read as a ribbon in one
|
|
1881
|
+
* place and a ring in another. A list of owners in a comment is a claim nothing
|
|
1882
|
+
* checks; the table is one a control reads.
|
|
1883
|
+
*/
|
|
1884
|
+
type RigTypeTable = {
|
|
1885
|
+
readonly [S in keyof typeof RIG_KEYS]: { readonly [K in (typeof RIG_KEYS)[S][number]]: SpecValueType };
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
export const RIG_TYPES = {
|
|
1889
|
+
RigSpec: {
|
|
1890
|
+
spec: 'enum', name: 'string', note: 'string', skeleton: 'object', images: 'string', bones: 'object[]', slots: 'object[]',
|
|
1891
|
+
skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object',
|
|
1892
|
+
},
|
|
1893
|
+
RigSkeletonHeader: {
|
|
1894
|
+
x: 'number', y: 'number', width: 'number | null', height: 'number | null', fps: 'number', referenceScale: 'number',
|
|
1895
|
+
images: 'string', audio: 'string | null',
|
|
1896
|
+
},
|
|
1897
|
+
RigBone: {
|
|
1898
|
+
name: 'string', parent: 'string', length: 'number', x: 'number', y: 'number', rotation: 'number', scaleX: 'number',
|
|
1899
|
+
scaleY: 'number', shearX: 'number', shearY: 'number', inherit: 'enum', skin: 'boolean', color: 'string', icon: 'string',
|
|
1900
|
+
from: 'object',
|
|
1901
|
+
},
|
|
1902
|
+
RigBoneFrom: { anchor: 'string', slotWindow: 'string', meshCenter: 'string', rotation: 'enum' },
|
|
1903
|
+
RigSlot: { name: 'string', bone: 'string', attachment: 'string | null', color: 'string', dark: 'string', blend: 'enum' },
|
|
1904
|
+
RigEvent: { int: 'number', float: 'number', string: 'string', audio: 'string', volume: 'number', balance: 'number' },
|
|
1905
|
+
RigInvariants: {
|
|
1906
|
+
meshSlots: 'number', meshTriangles: 'number', axisBone: 'string', massBone: 'string', detached: 'object[]',
|
|
1907
|
+
deformMayFold: 'object[]', editorRoundTrip: 'boolean', consumerDrivenMix: 'object[]', idleDrivesMeshes: 'object',
|
|
1908
|
+
},
|
|
1909
|
+
RigIdleDrivesMeshes: { why: 'string' },
|
|
1910
|
+
RigDetachedRule: { bone: 'string', notUnder: 'string', why: 'string' },
|
|
1911
|
+
RigDeformFoldExemption: { slot: 'string', why: 'string' },
|
|
1912
|
+
RigConsumerDrivenMix: { constraint: 'string', type: 'enum', why: 'string' },
|
|
1913
|
+
RigSkinEntry: {
|
|
1914
|
+
attachments: 'map of object', bones: 'string[]', ik: 'string[]', transform: 'string[]', path: 'string[]',
|
|
1915
|
+
physics: 'string[]', slider: 'string[]',
|
|
1916
|
+
},
|
|
1917
|
+
RigIkConstraint: {
|
|
1918
|
+
name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', target: 'string', scaleY: 'enum', mix: 'number',
|
|
1919
|
+
softness: 'number', bendPositive: 'boolean', compress: 'boolean', stretch: 'boolean',
|
|
1920
|
+
},
|
|
1921
|
+
RigTransformConstraint: {
|
|
1922
|
+
name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', source: 'string', localSource: 'boolean',
|
|
1923
|
+
localTarget: 'boolean', additive: 'boolean', clamp: 'boolean', properties: 'map of object',
|
|
1924
|
+
rotation: 'number', x: 'number', y: 'number', scaleX: 'number', scaleY: 'number', shearY: 'number',
|
|
1925
|
+
mixRotate: 'number', mixX: 'number', mixY: 'number', mixScaleX: 'number', mixScaleY: 'number', mixShearY: 'number',
|
|
1926
|
+
},
|
|
1927
|
+
RigPathConstraint: {
|
|
1928
|
+
name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', slot: 'string', positionMode: 'enum',
|
|
1929
|
+
spacingMode: 'enum', rotateMode: 'enum', rotation: 'number', position: 'number', spacing: 'number',
|
|
1930
|
+
mixRotate: 'number', mixX: 'number', mixY: 'number',
|
|
1931
|
+
},
|
|
1932
|
+
RigPhysicsConstraint: {
|
|
1933
|
+
name: 'string', skin: 'boolean', type: 'enum', bone: 'string', x: 'number', y: 'number', rotate: 'number',
|
|
1934
|
+
scaleX: 'number', shearX: 'number', scaleY: 'enum', limit: 'number', fps: 'number', inertia: 'number',
|
|
1935
|
+
strength: 'number', damping: 'number', mass: 'number', wind: 'number', gravity: 'number', mix: 'number',
|
|
1936
|
+
inertiaGlobal: 'boolean', strengthGlobal: 'boolean', dampingGlobal: 'boolean', massGlobal: 'boolean',
|
|
1937
|
+
windGlobal: 'boolean', gravityGlobal: 'boolean', mixGlobal: 'boolean',
|
|
1938
|
+
},
|
|
1939
|
+
RigSliderConstraint: {
|
|
1940
|
+
name: 'string', skin: 'boolean', type: 'enum', animation: 'string', mix: 'number', additive: 'boolean',
|
|
1941
|
+
loop: 'boolean', bone: 'string', property: 'enum', from: 'number', to: 'number', scale: 'number', max: 'number',
|
|
1942
|
+
local: 'boolean', time: 'number',
|
|
1943
|
+
},
|
|
1944
|
+
RigTransformProperty: { offset: 'number', to: 'map of object' },
|
|
1945
|
+
RigTransformTo: { offset: 'number', max: 'number', scale: 'number' },
|
|
1946
|
+
RigRegionAttachment: {
|
|
1947
|
+
type: 'enum', name: 'string', path: 'string', image: 'string', x: 'number', y: 'number', rotation: 'number',
|
|
1948
|
+
scaleX: 'number', scaleY: 'number', width: 'number', height: 'number', color: 'string', sequence: 'object',
|
|
1949
|
+
},
|
|
1950
|
+
RigMeshAttachment: {
|
|
1951
|
+
type: 'enum', name: 'string', path: 'string', image: 'string', uvs: 'number[]', triangles: 'number[]',
|
|
1952
|
+
vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', hull: 'number', edges: 'number[]',
|
|
1953
|
+
width: 'number', height: 'number', color: 'string', generator: 'object', sequence: 'object',
|
|
1954
|
+
},
|
|
1955
|
+
RigLinkedMeshAttachment: {
|
|
1956
|
+
type: 'enum', name: 'string', path: 'string', image: 'string', source: 'string', slot: 'string', skin: 'string',
|
|
1957
|
+
timelines: 'boolean', width: 'number', height: 'number', color: 'string', sequence: 'object',
|
|
1958
|
+
uvs: 'number[]', triangles: 'number[]', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum',
|
|
1959
|
+
hull: 'number', edges: 'number[]', generator: 'object',
|
|
1960
|
+
},
|
|
1961
|
+
RigMeshBinding: { bone: 'string', x: 'number', y: 'number', weight: 'number' },
|
|
1962
|
+
RigSequence: { count: 'number', start: 'number', digits: 'number', setup: 'number' },
|
|
1963
|
+
RigBoundingBoxAttachment: {
|
|
1964
|
+
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
1965
|
+
},
|
|
1966
|
+
RigClippingAttachment: {
|
|
1967
|
+
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
1968
|
+
end: 'string', convex: 'boolean', inverse: 'boolean',
|
|
1969
|
+
},
|
|
1970
|
+
RigPathAttachment: {
|
|
1971
|
+
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
1972
|
+
closed: 'boolean', constantSpeed: 'boolean', lengths: 'number[]',
|
|
1973
|
+
},
|
|
1974
|
+
RigRingGenerator: {
|
|
1975
|
+
kind: 'enum', hull: 'number[][]', center: 'number[]', inner: 'number', size: 'number[]', bias: 'object',
|
|
1976
|
+
controls: 'string[]',
|
|
1977
|
+
},
|
|
1978
|
+
RigRibbonGenerator: { kind: 'enum', size: 'number[]', rows: 'number', chain: 'string[]' },
|
|
1979
|
+
RigContourGenerator: {
|
|
1980
|
+
kind: 'enum', tolerance: 'number', margin: 'number', maxVertices: 'number', alpha: 'number', depth: 'object',
|
|
1981
|
+
soft: 'object',
|
|
1982
|
+
},
|
|
1983
|
+
RigGridGenerator: { kind: 'enum', us: 'number[]', vs: 'number[]', cols: 'number', rows: 'number', depth: 'object', soft: 'object' },
|
|
1984
|
+
RigSegmentsGenerator: { kind: 'enum', cell: 'number', bones: 'mixed', falloff: 'object', alpha: 'number', anchor: 'number[]' },
|
|
1985
|
+
RigSegmentsFalloff: { power: 'number', radius: 'number', maxBones: 'number', minWeight: 'number' },
|
|
1986
|
+
RigSegmentSpan: { bone: 'string', from: 'number[]', to: 'number[]' },
|
|
1987
|
+
RigMeshBias: { axis_deg: 'number', ramp: 'number[]' },
|
|
1988
|
+
RigDepthMap: { image: 'string', near: 'enum', zScale: 'number', gamma: 'number', contrast: 'number', bias: 'number' },
|
|
1989
|
+
RigSoftRegion: { bone: 'string', mask: 'string' },
|
|
1990
|
+
} as const satisfies RigTypeTable;
|
|
1991
|
+
|
|
1992
|
+
/** The two sources a bone's `from.rotation` names (`RigBoneFrom.rotation`). */
|
|
1993
|
+
export const RIG_FROM_ROTATIONS = ['axis', 'anchor'] as const satisfies ReadonlyArray<NonNullable<RigBoneFrom['rotation']>>;
|
|
1994
|
+
|
|
1995
|
+
/** The two ways a weighted `vertices` run names its bones (`RigMeshAttachment.boneIndexing`). */
|
|
1996
|
+
export const RIG_BONE_INDEXING = ['name', 'raw'] as const satisfies ReadonlyArray<NonNullable<RigMeshAttachment['boneIndexing']>>;
|
|
1997
|
+
|
|
1998
|
+
/** What `boneIndexing` outside its set was read as, measured before issue #900. */
|
|
1999
|
+
const BONE_INDEXING_READ_AS =
|
|
2000
|
+
'anything else was read as the default "name" in silence: a weighted "vertices" run was refused as unflagged, ' +
|
|
2001
|
+
'and every other attachment built byte for byte as if the key were absent';
|
|
2002
|
+
|
|
2003
|
+
/**
|
|
2004
|
+
* Who refuses each `enum` row of `RIG_TYPES` outside its set (issue #900) —
|
|
2005
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what the two kinds of entry
|
|
2006
|
+
* mean, and `satisfies` makes an `enum` row without one a type error.
|
|
2007
|
+
*
|
|
2008
|
+
* ⭐ A `set` is stated for exactly the rows nobody held: measured when this
|
|
2009
|
+
* table was written, `boneIndexing` built green at `5`, `"foo"` and `"named"`
|
|
2010
|
+
* (the spelling the issue itself used) as the default, and `from.rotation`
|
|
2011
|
+
* built green at `5` and `"foo"` with the bone's setup rotation dropped. Every
|
|
2012
|
+
* other row already had a reader that refused a planted `5` and `"foo"` by
|
|
2013
|
+
* name, and keeps it; the generator's `kind` is the one of those that is a
|
|
2014
|
+
* dispatch, and its refusal is new — it threw a `TypeError` (*undefined is not
|
|
2015
|
+
* an object (evaluating 'generator.size')*), because the scan skipped a `kind`
|
|
2016
|
+
* it had no row for and the mesh builder fell through to the ring branch.
|
|
2017
|
+
*/
|
|
2018
|
+
export const RIG_ENUMS = {
|
|
2019
|
+
RigSpec: { spec: { owner: 'parseRigSpec' } },
|
|
2020
|
+
RigBone: { inherit: { owner: 'parseRigSpec' } },
|
|
2021
|
+
RigBoneFrom: {
|
|
2022
|
+
rotation: {
|
|
2023
|
+
set: RIG_FROM_ROTATIONS,
|
|
2024
|
+
readAs: 'anything else was read as no rotation source, and the bone was emitted without the setup rotation it asked for',
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
RigSlot: { blend: { owner: 'parseRigSpec' } },
|
|
2028
|
+
RigConsumerDrivenMix: { type: { owner: 'parseRigSpec' } },
|
|
2029
|
+
RigIkConstraint: { type: { owner: 'buildRigConstraint' }, scaleY: { owner: 'buildRigConstraint' } },
|
|
2030
|
+
RigTransformConstraint: { type: { owner: 'buildRigConstraint' } },
|
|
2031
|
+
RigPathConstraint: {
|
|
2032
|
+
type: { owner: 'buildRigConstraint' },
|
|
2033
|
+
positionMode: { owner: 'buildRigConstraint' },
|
|
2034
|
+
spacingMode: { owner: 'buildRigConstraint' },
|
|
2035
|
+
rotateMode: { owner: 'buildRigConstraint' },
|
|
2036
|
+
},
|
|
2037
|
+
RigPhysicsConstraint: { type: { owner: 'buildRigConstraint' }, scaleY: { owner: 'buildRigConstraint' } },
|
|
2038
|
+
RigSliderConstraint: { type: { owner: 'buildRigConstraint' }, property: { owner: 'buildRigConstraint' } },
|
|
2039
|
+
RigRegionAttachment: { type: { owner: 'buildRigAttachment' } },
|
|
2040
|
+
RigMeshAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2041
|
+
RigLinkedMeshAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2042
|
+
RigBoundingBoxAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2043
|
+
RigClippingAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2044
|
+
RigPathAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2045
|
+
// The generator's `kind` chooses the row its other keys are checked against,
|
|
2046
|
+
// so a `kind` outside the five has no row to be visited in: the scan that
|
|
2047
|
+
// dispatches on it is where it is refused.
|
|
2048
|
+
RigRingGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2049
|
+
RigRibbonGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2050
|
+
RigContourGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2051
|
+
RigGridGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2052
|
+
RigSegmentsGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2053
|
+
RigDepthMap: { near: { owner: 'sampleMeshDepth' } },
|
|
2054
|
+
} as const satisfies SpecEnumTable<typeof RIG_TYPES>;
|
|
2055
|
+
|
|
1857
2056
|
/**
|
|
1858
2057
|
* The five constraint `type` names, and the shape each one's keys come from.
|
|
1859
2058
|
*
|
|
@@ -1880,7 +2079,7 @@ const ATTACHMENT_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
|
1880
2079
|
};
|
|
1881
2080
|
|
|
1882
2081
|
/** The generator `kind` names, and the shape each one's keys come from. */
|
|
1883
|
-
const GENERATOR_SHAPE: Record<
|
|
2082
|
+
const GENERATOR_SHAPE: Record<(typeof RIG_GENERATOR_KINDS)[number], keyof typeof RIG_KEYS> = {
|
|
1884
2083
|
ring: 'RigRingGenerator',
|
|
1885
2084
|
ribbon: 'RigRibbonGenerator',
|
|
1886
2085
|
contour: 'RigContourGenerator',
|
|
@@ -1984,22 +2183,25 @@ function checkRigSequence(att: Record<string, unknown>, who: string, where: stri
|
|
|
1984
2183
|
* refusal, and naming its keys would be a second opinion on a fault already
|
|
1985
2184
|
* reported (see the note at the head of `parseMotionSpec`).
|
|
1986
2185
|
*/
|
|
1987
|
-
function checkRigSpecKeys(raw: Record<string, unknown>, where: string):
|
|
1988
|
-
const
|
|
1989
|
-
|
|
2186
|
+
function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{ node: Record<string, unknown>; shape: keyof typeof RIG_KEYS; path: Array<string | number> }> {
|
|
2187
|
+
const visits: Array<{ node: Record<string, unknown>; shape: keyof typeof RIG_KEYS; path: Array<string | number> }> = [];
|
|
2188
|
+
const at = (node: unknown, shape: keyof typeof RIG_KEYS, what: string, path: Array<string | number>): void => {
|
|
2189
|
+
if (!isObj(node)) return;
|
|
2190
|
+
refuseUnknownKeys(node, RIG_KEYS[shape], where, what);
|
|
2191
|
+
visits.push({ node, shape, path });
|
|
1990
2192
|
};
|
|
1991
2193
|
|
|
1992
|
-
at(raw, 'RigSpec', 'this rig spec');
|
|
1993
|
-
at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"');
|
|
2194
|
+
at(raw, 'RigSpec', 'this rig spec', []);
|
|
2195
|
+
at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"', ['skeleton']);
|
|
1994
2196
|
|
|
1995
2197
|
for (const [i, bone] of (Array.isArray(raw.bones) ? raw.bones : []).entries()) {
|
|
1996
2198
|
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"
|
|
2199
|
+
at(bone, 'RigBone', who, ['bones', i]);
|
|
2200
|
+
if (isObj(bone)) at(bone.from, 'RigBoneFrom', `${who}'s "from"`, ['bones', i, 'from']);
|
|
1999
2201
|
}
|
|
2000
2202
|
|
|
2001
2203
|
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}]
|
|
2204
|
+
at(slot, 'RigSlot', isObj(slot) && typeof slot.name === 'string' ? `slot "${slot.name}"` : `slots[${i}]`, ['slots', i]);
|
|
2003
2205
|
}
|
|
2004
2206
|
|
|
2005
2207
|
for (const [i, constraint] of (Array.isArray(raw.constraints) ? raw.constraints : []).entries()) {
|
|
@@ -2010,32 +2212,32 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
2010
2212
|
// that exist; there is no key set to check it against and no honest one to
|
|
2011
2213
|
// guess, so it goes past here to the message that can say something.
|
|
2012
2214
|
if (shape === undefined) continue;
|
|
2013
|
-
at(constraint, shape, `${named} (${String(constraint.type)})
|
|
2215
|
+
at(constraint, shape, `${named} (${String(constraint.type)})`, ['constraints', i]);
|
|
2014
2216
|
for (const [from, entry] of Object.entries(isObj(constraint.properties) ? constraint.properties : {})) {
|
|
2015
|
-
at(entry, 'RigTransformProperty', `${named} properties."${from}"
|
|
2217
|
+
at(entry, 'RigTransformProperty', `${named} properties."${from}"`, ['constraints', i, 'properties', from]);
|
|
2016
2218
|
if (!isObj(entry)) continue;
|
|
2017
2219
|
for (const [to, driven] of Object.entries(isObj(entry.to) ? entry.to : {})) {
|
|
2018
|
-
at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"
|
|
2220
|
+
at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`, ['constraints', i, 'properties', from, 'to', to]);
|
|
2019
2221
|
}
|
|
2020
2222
|
}
|
|
2021
2223
|
}
|
|
2022
2224
|
|
|
2023
2225
|
for (const [name, event] of Object.entries(isObj(raw.events) ? raw.events : {})) {
|
|
2024
|
-
at(event, 'RigEvent', `event "${name}"
|
|
2226
|
+
at(event, 'RigEvent', `event "${name}"`, ['events', name]);
|
|
2025
2227
|
}
|
|
2026
2228
|
|
|
2027
2229
|
if (isObj(raw.invariants)) {
|
|
2028
|
-
at(raw.invariants, 'RigInvariants', '"invariants"');
|
|
2230
|
+
at(raw.invariants, 'RigInvariants', '"invariants"', ['invariants']);
|
|
2029
2231
|
for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
|
|
2030
|
-
at(rule, 'RigDetachedRule', `invariants.detached[${i}]
|
|
2232
|
+
at(rule, 'RigDetachedRule', `invariants.detached[${i}]`, ['invariants', 'detached', i]);
|
|
2031
2233
|
}
|
|
2032
2234
|
for (const [i, rule] of (Array.isArray(raw.invariants.deformMayFold) ? raw.invariants.deformMayFold : []).entries()) {
|
|
2033
|
-
at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]
|
|
2235
|
+
at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`, ['invariants', 'deformMayFold', i]);
|
|
2034
2236
|
}
|
|
2035
2237
|
for (const [i, rule] of (Array.isArray(raw.invariants.consumerDrivenMix) ? raw.invariants.consumerDrivenMix : []).entries()) {
|
|
2036
|
-
at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]
|
|
2238
|
+
at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`, ['invariants', 'consumerDrivenMix', i]);
|
|
2037
2239
|
}
|
|
2038
|
-
at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes');
|
|
2240
|
+
at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes', ['invariants', 'idleDrivesMeshes']);
|
|
2039
2241
|
}
|
|
2040
2242
|
|
|
2041
2243
|
for (const [skinName, skin] of Object.entries(isObj(raw.skins) ? raw.skins : {})) {
|
|
@@ -2044,6 +2246,10 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
2044
2246
|
// likeliest cause (a slot left outside `attachments`), so it is reused
|
|
2045
2247
|
// rather than restated — one refusal per fault.
|
|
2046
2248
|
const parts = splitRigSkin(skin as RigSkin, `${where}: skin "${skinName}"`);
|
|
2249
|
+
// The long form's own lists are typed too; `splitRigSkin` has just refused
|
|
2250
|
+
// a key outside them, so the row's keys are the node's keys.
|
|
2251
|
+
if (parts.explicit) visits.push({ node: skin, shape: 'RigSkinEntry', path: ['skins', skinName] });
|
|
2252
|
+
const base: Array<string | number> = parts.explicit ? ['skins', skinName, 'attachments'] : ['skins', skinName];
|
|
2047
2253
|
for (const [slot, placeholders] of Object.entries(parts.attachments)) {
|
|
2048
2254
|
if (!isObj(placeholders)) continue;
|
|
2049
2255
|
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
@@ -2075,33 +2281,46 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
|
2075
2281
|
'series would be dropped in silence. Remove it, or put it on a region or a mesh.',
|
|
2076
2282
|
);
|
|
2077
2283
|
}
|
|
2078
|
-
|
|
2284
|
+
const here = [...base, slot, placeholder];
|
|
2285
|
+
at(att, shape, `${who} (${type})`, here);
|
|
2079
2286
|
if (att.sequence !== undefined) {
|
|
2080
|
-
at(att.sequence, 'RigSequence', `${who} "sequence"
|
|
2287
|
+
at(att.sequence, 'RigSequence', `${who} "sequence"`, [...here, 'sequence']);
|
|
2081
2288
|
checkRigSequence(att, who, where);
|
|
2082
2289
|
}
|
|
2083
2290
|
for (const [i, vertex] of (Array.isArray(att.weights) ? att.weights : []).entries()) {
|
|
2084
2291
|
for (const [j, binding] of (Array.isArray(vertex) ? vertex : []).entries()) {
|
|
2085
|
-
at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]
|
|
2292
|
+
at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`, [...here, 'weights', i, j]);
|
|
2086
2293
|
}
|
|
2087
2294
|
}
|
|
2088
2295
|
if (!isObj(att.generator)) continue;
|
|
2089
2296
|
const gen = att.generator;
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
//
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2297
|
+
const kind = RIG_GENERATOR_KINDS.find((k) => k === gen.kind);
|
|
2298
|
+
const genShape = kind === undefined ? undefined : GENERATOR_SHAPE[kind];
|
|
2299
|
+
// 🚨 An unknown `kind` used to be skipped here as "the mesh builder's
|
|
2300
|
+
// refusal", and the mesh builder had none: `buildGeneratedMesh` fell
|
|
2301
|
+
// through to the ring branch and threw a TypeError reading
|
|
2302
|
+
// `generator.size` (issue #900). `kind` chooses the row every other key
|
|
2303
|
+
// of the generator is checked against, so this dispatch is the one
|
|
2304
|
+
// place that can refuse it — before any of those keys is judged.
|
|
2305
|
+
if (genShape === undefined) {
|
|
2306
|
+
const stated = gen.kind === undefined ? 'absent' : (JSON.stringify(gen.kind) ?? String(gen.kind));
|
|
2307
|
+
throw new CompileError(
|
|
2308
|
+
`${where}: ${who} generator.kind is ${stated}; one of ${RIG_GENERATOR_KINDS.map((k) => JSON.stringify(k)).join(', ')} — ` +
|
|
2309
|
+
'the kind names the builder, and each builder reads its own keys, so nothing about the mesh can be checked or built without one',
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
at(gen, genShape, `${who} generator (${String(gen.kind)})`, [...here, 'generator']);
|
|
2313
|
+
at(gen.bias, 'RigMeshBias', `${who} generator.bias`, [...here, 'generator', 'bias']);
|
|
2314
|
+
at(gen.depth, 'RigDepthMap', `${who} generator.depth`, [...here, 'generator', 'depth']);
|
|
2315
|
+
at(gen.soft, 'RigSoftRegion', `${who} generator.soft`, [...here, 'generator', 'soft']);
|
|
2316
|
+
at(gen.falloff, 'RigSegmentsFalloff', `${who} generator.falloff`, [...here, 'generator', 'falloff']);
|
|
2099
2317
|
for (const [i, entry] of (Array.isArray(gen.bones) ? gen.bones : []).entries()) {
|
|
2100
|
-
at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]
|
|
2318
|
+
at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`, [...here, 'generator', 'bones', i]);
|
|
2101
2319
|
}
|
|
2102
2320
|
}
|
|
2103
2321
|
}
|
|
2104
2322
|
}
|
|
2323
|
+
return visits;
|
|
2105
2324
|
}
|
|
2106
2325
|
|
|
2107
2326
|
|
|
@@ -2176,11 +2395,26 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
2176
2395
|
// compiler does not read" for a format it has never read at all. The three
|
|
2177
2396
|
// throws directly below are the required-key checks, and each of them is the
|
|
2178
2397
|
// consequence a typo at the root produces.
|
|
2179
|
-
checkRigSpecKeys(raw, where);
|
|
2398
|
+
const visits = checkRigSpecKeys(raw, where);
|
|
2399
|
+
const place = rigPlace(raw);
|
|
2400
|
+
// Every value the scan admitted, against the type its key holds, before any
|
|
2401
|
+
// reader — here or in `compile` — has done arithmetic on it (issue #890).
|
|
2402
|
+
// ⚠️ It runs ahead of this function's own field checks on purpose: those read
|
|
2403
|
+
// values too, and a reader that meets a wrong type names the wrong fault —
|
|
2404
|
+
// `constraint.skin === true` reads `"skin": "true"` as a constraint that asks
|
|
2405
|
+
// for no skin. The price, measured, is three controls whose type half pinned an
|
|
2406
|
+
// older sentence (RF86, PS177, PS189); their other halves are unchanged.
|
|
2407
|
+
const shapeVisits: ShapeVisit[] = visits.map((v) => ({ node: v.node, shape: v.shape, name: (tail) => place([...v.path, ...tail]) }));
|
|
2408
|
+
refuseValuesOfTheWrongType(shapeVisits, RIG_TYPES, where);
|
|
2409
|
+
// A name outside an `enum` row's stated set (issue #900), after the type walk
|
|
2410
|
+
// and over the same visits: `boneIndexing: "foo"` built as the default and
|
|
2411
|
+
// `from.rotation: "foo"` as no rotation, both green. A row whose entry names
|
|
2412
|
+
// an owner is that reader's to refuse, in its own words.
|
|
2413
|
+
refuseValuesOutsideTheirSet(shapeVisits, RIG_TYPES, RIG_ENUMS, where);
|
|
2180
2414
|
// After the key check, so a misspelled key is named as a misspelling before
|
|
2181
2415
|
// its value is judged, and before every reader below, so no range rule or
|
|
2182
2416
|
// derived number ever meets a value the file cannot carry (issue #881).
|
|
2183
|
-
refuseNumbersTheFileCannotCarry(raw, where,
|
|
2417
|
+
refuseNumbersTheFileCannotCarry(raw, where, place);
|
|
2184
2418
|
|
|
2185
2419
|
if (typeof raw.name !== 'string' || raw.name.length === 0) {
|
|
2186
2420
|
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 { SpecEnumTable, 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,75 @@ 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 by this walk: `mesh.kind` and `mesh.hull` (`enum`) — see
|
|
189
|
+
* `MANIFEST_ENUMS`, below; `crop` and the other nested shapes, `parts` and
|
|
190
|
+
* `mesh` (`object`) — the 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
|
+
|
|
214
|
+
/** The two mesh generators a manifest part may name (`FaceManifestMesh.kind`); absent means `ring`. */
|
|
215
|
+
export const MANIFEST_MESH_KINDS = ['ring', 'ribbon'] as const satisfies ReadonlyArray<NonNullable<FaceManifestMesh['kind']>>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Who refuses each `enum` row of `MANIFEST_TYPES` outside its set (issue #900);
|
|
219
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what an entry means.
|
|
220
|
+
*
|
|
221
|
+
* 🚨 `mesh.kind` was listed as held by "the manifest mesh reader", and it was
|
|
222
|
+
* not held at all: that reader has three places that ask the kind, and they
|
|
223
|
+
* disagreed about a value outside the two. The mesh checks read `"foo"` as a
|
|
224
|
+
* ribbon, while the control-bone reader and the geometry builder read it as a
|
|
225
|
+
* ring — so a ring part was refused as *a ribbon mesh needs mesh.rows and a
|
|
226
|
+
* mesh.chain*, and a ribbon part as *a mesh with no control bone deforms
|
|
227
|
+
* nothing*, both naming a fault the part did not have. `hull` is held: the ring
|
|
228
|
+
* check refuses anything but `"polygon"` by name, and keeps that sentence.
|
|
229
|
+
*/
|
|
230
|
+
export const MANIFEST_ENUMS = {
|
|
231
|
+
FaceManifestMesh: {
|
|
232
|
+
kind: {
|
|
233
|
+
set: MANIFEST_MESH_KINDS,
|
|
234
|
+
readAs:
|
|
235
|
+
'anything else was read as a ribbon by the mesh checks and as a ring by the control-bone reader and the ' +
|
|
236
|
+
'geometry builder, so the refusal it got named a fault the part did not have',
|
|
237
|
+
},
|
|
238
|
+
hull: { owner: 'compileInto' },
|
|
239
|
+
},
|
|
240
|
+
} as const satisfies SpecEnumTable<typeof MANIFEST_TYPES>;
|
|
241
|
+
|
|
172
242
|
// ---------------------------------------------------------------------------
|
|
173
243
|
// Motion spec (spec: "rigc-motion/1")
|
|
174
244
|
// ---------------------------------------------------------------------------
|