spine-rigc 0.20.3 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/cli.ts +7 -1
- package/docs/AUTHORING.md +275 -43
- package/docs/FACE.md +53 -7
- package/docs/SPEC_COVERAGE.md +9 -0
- package/package.json +2 -1
- package/src/compile.ts +618 -171
- package/src/keys.ts +117 -0
- package/src/motion.ts +143 -15
- package/src/rig.ts +445 -117
- package/src/types.ts +97 -8
- package/src/validate.ts +106 -1
- package/tools/editor_roundtrip.ts +99 -12
package/src/rig.ts
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
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
64
|
|
|
64
65
|
export { CompileError, NotImplementedError };
|
|
65
66
|
|
|
@@ -254,19 +255,6 @@ export interface RigSlot {
|
|
|
254
255
|
// attachments — `readAttachment` (SkeletonJson.ts:535-654)
|
|
255
256
|
// ---------------------------------------------------------------------------
|
|
256
257
|
|
|
257
|
-
/**
|
|
258
|
-
* Which builder in `src/mesh.ts` makes this mesh's geometry, and its parameters.
|
|
259
|
-
*
|
|
260
|
-
* The builders stay **code** and are invoked by **data**: they encode a
|
|
261
|
-
* deformation model (what is pinned, what may move, how authority falls off),
|
|
262
|
-
* and a model is not a table of numbers.
|
|
263
|
-
*
|
|
264
|
-
* ⚠️ A cut with a manifest does not use this. There the generator is invoked
|
|
265
|
-
* through the manifest's `mesh` block, because everything a generator needs —
|
|
266
|
-
* the mask contour, the aperture centre, the part window — is *measured art*,
|
|
267
|
-
* and measured art lives in the manifest. `generator` is for a skeleton with no
|
|
268
|
-
* manifest behind it.
|
|
269
|
-
*/
|
|
270
258
|
/**
|
|
271
259
|
* A greyscale sheet, in a part's own pixel grid, giving each vertex a depth —
|
|
272
260
|
* what `yaw` and `pitch` otherwise derive from one cylinder radius.
|
|
@@ -352,104 +340,143 @@ export interface RigSoftRegion {
|
|
|
352
340
|
mask: string;
|
|
353
341
|
}
|
|
354
342
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Directional authority across an axis — see `sideWeight` in
|
|
345
|
+
* [`mesh.ts`](mesh.ts).
|
|
346
|
+
*
|
|
347
|
+
* ⭐ It was an inline object type until issue #545. The four generator kinds
|
|
348
|
+
* were too: the union is spelled as four **named** interfaces now because
|
|
349
|
+
* `RIG_KEYS` pairs a key set with an interface by name, and a shape with no name
|
|
350
|
+
* is a shape the pairing cannot reach — so an anonymous corner of this file
|
|
351
|
+
* would have been a corner whose key set nothing checked.
|
|
352
|
+
*/
|
|
353
|
+
export interface RigMeshBias {
|
|
354
|
+
/** The axis, in SCREEN degrees, y down — a manifest's own convention. */
|
|
355
|
+
axis_deg: number;
|
|
356
|
+
/** Signed distance across that axis over which authority goes 0 -> 1. */
|
|
357
|
+
ramp: [number, number];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** A ring: a seam contour, an aperture inside it, and the bones that open it. */
|
|
361
|
+
export interface RigRingGenerator {
|
|
362
|
+
kind: 'ring';
|
|
363
|
+
/** The seam contour, in part-local pixels, y down. At least 6 points. */
|
|
364
|
+
hull: Array<[number, number]>;
|
|
365
|
+
/** Aperture centre, part-local pixels, y down. */
|
|
366
|
+
center: [number, number];
|
|
367
|
+
/** Inner ring position between the centre (0) and the hull (1). */
|
|
368
|
+
inner: number;
|
|
369
|
+
/** Part window size, for UVs. */
|
|
370
|
+
size: [number, number];
|
|
371
|
+
/** Directional authority across an axis — see `sideWeight` in mesh.ts. */
|
|
372
|
+
bias?: RigMeshBias;
|
|
373
|
+
/** Control bones, by name. More than one splits the ring by angle. */
|
|
374
|
+
controls: string[];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** A ribbon: a strip of cross rows riding a bone chain. */
|
|
378
|
+
export interface RigRibbonGenerator {
|
|
379
|
+
kind: 'ribbon';
|
|
380
|
+
/** Part window size in pixels. The strip spans it. */
|
|
381
|
+
size: [number, number];
|
|
382
|
+
/** Cross rows, entry first. Triangles = 2 * (rows - 1). */
|
|
383
|
+
rows: number;
|
|
384
|
+
/** The bone chain the strip rides, root first. */
|
|
385
|
+
chain: string[];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* A mesh cut to the part's own alpha silhouette: trace the mask, simplify
|
|
390
|
+
* the outline, push it out by a margin, ear-clip it (`buildContourMesh`).
|
|
391
|
+
*
|
|
392
|
+
* ⭐ It takes no `size` and no geometry. The shape is MEASURED off the
|
|
393
|
+
* attachment's own `image` — the same rule a region attachment's
|
|
394
|
+
* `width`/`height` follow (R5) — so there is no number here that can
|
|
395
|
+
* disagree with the pixels, and no polygon to keep in step with the art.
|
|
396
|
+
*
|
|
397
|
+
* 🚨 It is geometry, not a deformation model: every vertex is pinned to
|
|
398
|
+
* the slot bone at weight 1, so an undeformed contour mesh draws exactly
|
|
399
|
+
* what the region drew and no bone can bend it. See the section header in
|
|
400
|
+
* [`src/mesh.ts`](mesh.ts) for what it buys instead, and reach for `ring`
|
|
401
|
+
* or authored `weights` when a bone has to move the art.
|
|
402
|
+
*/
|
|
403
|
+
export interface RigContourGenerator {
|
|
404
|
+
kind: 'contour';
|
|
405
|
+
/**
|
|
406
|
+
* Douglas-Peucker tolerance in part-local pixels. Bigger spends fewer
|
|
407
|
+
* vertices and cuts more corners; the builder measures how much of the art
|
|
408
|
+
* the result still covers and refuses a mesh that clips it.
|
|
409
|
+
*/
|
|
410
|
+
tolerance: number;
|
|
411
|
+
/**
|
|
412
|
+
* How far the outline is pushed out past the traced silhouette, in pixels.
|
|
413
|
+
* Default 1. Simplification may bite `tolerance` pixels INTO the art, so
|
|
414
|
+
* `margin >= tolerance` is the setting that survives the coverage check.
|
|
415
|
+
*/
|
|
416
|
+
margin?: number;
|
|
417
|
+
/** Refuse rather than emit more outline vertices than this. Default 64. */
|
|
418
|
+
maxVertices?: number;
|
|
419
|
+
/** Alpha at or above which a pixel counts as art, 1..255. Default 1. */
|
|
420
|
+
alpha?: number;
|
|
421
|
+
/** A depth map for this part — see `RigDepthMap`. */
|
|
422
|
+
depth?: RigDepthMap;
|
|
423
|
+
/** A soft region carried by its own bone — see `RigSoftRegion`. */
|
|
424
|
+
soft?: RigSoftRegion;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A lattice over the part window — the topology `docs/FACE.md` §4 turns a
|
|
429
|
+
* plate into so a turn has columns to move.
|
|
430
|
+
*
|
|
431
|
+
* ⭐ It takes no `size`: like a `contour`, the window is the attachment's
|
|
432
|
+
* own `image`, so there is no number here that can disagree with the
|
|
433
|
+
* pixels. Every vertex is pinned to the slot bone at weight 1, which
|
|
434
|
+
* makes the lattice geometry to DEFORM rather than an authority split —
|
|
435
|
+
* reach for `ring` when bones have to move it.
|
|
436
|
+
*/
|
|
437
|
+
export interface RigGridGenerator {
|
|
438
|
+
kind: 'grid';
|
|
439
|
+
/**
|
|
440
|
+
* Column positions across the window, 0..1, ascending. At least 2.
|
|
441
|
+
*
|
|
442
|
+
* ⚠️ Positions, not a count, and that is deliberate: FACE §4.1 places
|
|
443
|
+
* columns where the drawing needs them, and the worked example's are
|
|
444
|
+
* dense at the silhouette and sparse across the middle. They need not
|
|
445
|
+
* reach the window edge — that example's run 0.0235 to 0.9765.
|
|
446
|
+
*/
|
|
447
|
+
us?: number[];
|
|
448
|
+
/** Row positions down the window, 0..1, ascending. At least 2. */
|
|
449
|
+
vs?: number[];
|
|
450
|
+
/**
|
|
451
|
+
* Even division instead: `cols` columns and `rows` rows spanning the
|
|
452
|
+
* whole window. A convenience for a plate with no shape to follow, and
|
|
453
|
+
* refused beside `us`/`vs`, which say the same thing more precisely.
|
|
454
|
+
*/
|
|
455
|
+
cols?: number;
|
|
456
|
+
rows?: number;
|
|
457
|
+
/** A depth map for this part — see `RigDepthMap`. */
|
|
458
|
+
depth?: RigDepthMap;
|
|
459
|
+
/** A soft region carried by its own bone — see `RigSoftRegion`. */
|
|
460
|
+
soft?: RigSoftRegion;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Which builder in `src/mesh.ts` makes this mesh's geometry, and its parameters.
|
|
465
|
+
*
|
|
466
|
+
* The builders stay **code** and are invoked by **data**: they encode a
|
|
467
|
+
* deformation model (what is pinned, what may move, how authority falls off),
|
|
468
|
+
* and a model is not a table of numbers.
|
|
469
|
+
*
|
|
470
|
+
* ⚠️ A cut with a manifest does not use this. There the generator is invoked
|
|
471
|
+
* through the manifest's `mesh` block, because everything a generator needs —
|
|
472
|
+
* the mask contour, the aperture centre, the part window — is *measured art*,
|
|
473
|
+
* and measured art lives in the manifest. `generator` is for a skeleton with no
|
|
474
|
+
* manifest behind it.
|
|
475
|
+
*/
|
|
476
|
+
export type RigMeshGenerator = RigRingGenerator | RigRibbonGenerator | RigContourGenerator | RigGridGenerator;
|
|
477
|
+
|
|
478
|
+
/** The four `kind` names a generator may carry, and the order `RIG_KEYS` takes them in. */
|
|
479
|
+
export const RIG_GENERATOR_KINDS = ['ring', 'ribbon', 'contour', 'grid'] as const;
|
|
453
480
|
|
|
454
481
|
/** `SkeletonJson.ts:540-559`. `type` defaults to `region` (`:539`). */
|
|
455
482
|
export interface RigRegionAttachment {
|
|
@@ -849,6 +876,19 @@ export interface RigConstraintCommon {
|
|
|
849
876
|
skin?: boolean;
|
|
850
877
|
}
|
|
851
878
|
|
|
879
|
+
/**
|
|
880
|
+
* `ScaleYMode` (`ConstraintData.ts:37-45`), which an **ik** and a **physics**
|
|
881
|
+
* constraint both carry under the JSON key `scaleY`.
|
|
882
|
+
*
|
|
883
|
+
* ⚠️ Resolved by `Utils.enumValue`, so only the first letter's case is free and
|
|
884
|
+
* an unresolved name is assigned as `undefined` without a word — the hazard
|
|
885
|
+
* `RIG_PATH_POSITION_MODES` is checked for, at a field that had no check at all.
|
|
886
|
+
*/
|
|
887
|
+
export const RIG_SCALE_Y_MODES = ['None', 'Uniform', 'Volume'] as const;
|
|
888
|
+
|
|
889
|
+
/** The three names, as a rig spec writes them. */
|
|
890
|
+
export type RigScaleYMode = 'none' | 'uniform' | 'volume' | 'None' | 'Uniform' | 'Volume';
|
|
891
|
+
|
|
852
892
|
/** `type: "ik"` (`:149-176`). `scaleY` is 4.3's replacement for 4.2's `uniform`. */
|
|
853
893
|
export interface RigIkConstraint extends RigConstraintCommon {
|
|
854
894
|
type: 'ik';
|
|
@@ -856,7 +896,7 @@ export interface RigIkConstraint extends RigConstraintCommon {
|
|
|
856
896
|
bones: string[];
|
|
857
897
|
target: string;
|
|
858
898
|
/** `ConstraintData.ts:50`. Absent → `None`. */
|
|
859
|
-
scaleY?:
|
|
899
|
+
scaleY?: RigScaleYMode;
|
|
860
900
|
/** Default 1. */
|
|
861
901
|
mix?: number;
|
|
862
902
|
/** Default 0. */
|
|
@@ -877,7 +917,14 @@ export interface RigIkConstraint extends RigConstraintCommon {
|
|
|
877
917
|
*/
|
|
878
918
|
export interface RigTransformProperty {
|
|
879
919
|
offset?: number;
|
|
880
|
-
to: Record<string,
|
|
920
|
+
to: Record<string, RigTransformTo>;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** One driven property of a `RigTransformProperty.to` map. */
|
|
924
|
+
export interface RigTransformTo {
|
|
925
|
+
offset?: number;
|
|
926
|
+
max?: number;
|
|
927
|
+
scale?: number;
|
|
881
928
|
}
|
|
882
929
|
|
|
883
930
|
/** `type: "transform"` (`:177-268`) — rebuilt from scratch in 4.3. */
|
|
@@ -928,8 +975,27 @@ export interface RigPhysicsConstraint extends RigConstraintCommon {
|
|
|
928
975
|
rotate?: number;
|
|
929
976
|
scaleX?: number;
|
|
930
977
|
shearX?: number;
|
|
931
|
-
/**
|
|
932
|
-
|
|
978
|
+
/**
|
|
979
|
+
* 4.3; absent → `ScaleYMode.None`. Same field, same enum and same spelling as
|
|
980
|
+
* `RigIkConstraint.scaleY`.
|
|
981
|
+
*
|
|
982
|
+
* 🚨 It was called `scaleYMode` from this file's first commit (c0e9944,
|
|
983
|
+
* 2026-08-22) until issue #545, and the name was not a synonym — it was the
|
|
984
|
+
* one key in this file that no code anywhere read.
|
|
985
|
+
* `SkeletonJson.js:299` is `getValue(constraintMap, "scaleY", null)`, so the
|
|
986
|
+
* emitter copied `scaleY`, an author writing TypeScript against this interface
|
|
987
|
+
* got a type error on the key that works and silence on the key that does
|
|
988
|
+
* nothing, and `scaleYMode` occurred exactly once in the whole tree: here.
|
|
989
|
+
*
|
|
990
|
+
* ⭐ The rename rather than teaching the emitter to read `scaleYMode`, and the
|
|
991
|
+
* argument is not "the format's name wins" in the abstract — the tree had
|
|
992
|
+
* already answered it four hundred lines above. `RigIkConstraint.scaleY`
|
|
993
|
+
* carries the *same* `ScaleYMode` enum through the *same* `Utils.enumValue`
|
|
994
|
+
* call under the *same* JSON key, and spells it `scaleY`. Teaching the emitter
|
|
995
|
+
* the other name would have left one Spine enum with two rig-spec spellings
|
|
996
|
+
* chosen by constraint type, which is a worse format than either name alone.
|
|
997
|
+
*/
|
|
998
|
+
scaleY?: RigScaleYMode;
|
|
933
999
|
/** Default 5000. */
|
|
934
1000
|
limit?: number;
|
|
935
1001
|
/** Default 60 → `step = 1/fps`. */
|
|
@@ -1153,7 +1219,7 @@ export interface RigInvariants {
|
|
|
1153
1219
|
*/
|
|
1154
1220
|
massBone?: string;
|
|
1155
1221
|
/** Parentage that must never happen, with the reason it is tempting (`A25`). */
|
|
1156
|
-
detached?:
|
|
1222
|
+
detached?: RigDetachedRule[];
|
|
1157
1223
|
/**
|
|
1158
1224
|
* Mesh slots whose `deform` timelines are allowed to turn a triangle inside
|
|
1159
1225
|
* out, exempting them from `A39_DEFORM_KEEPS_TRIANGLE_WINDING`.
|
|
@@ -1172,7 +1238,46 @@ export interface RigInvariants {
|
|
|
1172
1238
|
* in this repository (`gallery/flex`'s leaf) says outright that it is the
|
|
1173
1239
|
* second kind, and names the issue.
|
|
1174
1240
|
*/
|
|
1175
|
-
deformMayFold?:
|
|
1241
|
+
deformMayFold?: RigDeformFoldExemption[];
|
|
1242
|
+
/**
|
|
1243
|
+
* Declare that this rig is authored to come back out of the **Spine editor** —
|
|
1244
|
+
* imported, hand-edited, exported — and gate what that consumer cannot hold
|
|
1245
|
+
* (`A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP`).
|
|
1246
|
+
*
|
|
1247
|
+
* 🔑 **Opt-in, because rigc's output is not wrong.** A physics constraint
|
|
1248
|
+
* driving `rotate` is valid Spine 4.3 and every runtime plays it: a cowlick, a
|
|
1249
|
+
* tail, an ear. What is true is that one consumer discards it — measured, not
|
|
1250
|
+
* inferred (issue #540): the editor's physics model holds `x` and `y` only,
|
|
1251
|
+
* with no cap on how many at once, and a lone `rotate`, `scaleX` or `shearX`
|
|
1252
|
+
* comes back driving nothing at all. Refusing that by default would be rigc
|
|
1253
|
+
* refusing correct data on behalf of a pipeline nobody told it about, which is
|
|
1254
|
+
* the same silence pointed the other way. rigc knows what the object is; only
|
|
1255
|
+
* the rig knows which consumers it is for.
|
|
1256
|
+
*
|
|
1257
|
+
* ⚠️ **Declaring nothing is not the same as being told nothing.** `A41` SKIPs
|
|
1258
|
+
* on a rig that stays quiet — and the SKIP names the constraint and the
|
|
1259
|
+
* component a round trip would drop, because the defect this field exists for
|
|
1260
|
+
* is that nobody finds out. Declaring `true` turns that sentence into a
|
|
1261
|
+
* refusal.
|
|
1262
|
+
*
|
|
1263
|
+
* Only `true` is accepted. A `false` here would be a key nothing reads (issue
|
|
1264
|
+
* #545), and leaving it out says the same thing without the ambiguity.
|
|
1265
|
+
*/
|
|
1266
|
+
editorRoundTrip?: boolean;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** One forbidden parentage — `invariants.detached` (`A25`). */
|
|
1270
|
+
export interface RigDetachedRule {
|
|
1271
|
+
bone: string;
|
|
1272
|
+
notUnder: string;
|
|
1273
|
+
why?: string;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** One slot exempted from `A39` — `invariants.deformMayFold`. */
|
|
1277
|
+
export interface RigDeformFoldExemption {
|
|
1278
|
+
slot: string;
|
|
1279
|
+
/** Required, and empty is refused by name — see `deformMayFold`. */
|
|
1280
|
+
why: string;
|
|
1176
1281
|
}
|
|
1177
1282
|
|
|
1178
1283
|
// ---------------------------------------------------------------------------
|
|
@@ -1226,6 +1331,210 @@ function isObj(v: unknown): v is Record<string, unknown> {
|
|
|
1226
1331
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
1227
1332
|
}
|
|
1228
1333
|
|
|
1334
|
+
/**
|
|
1335
|
+
* Every key each shape of this format owns, keyed by the interface above that
|
|
1336
|
+
* declares it — the runtime shadow of the types, which TypeScript erases.
|
|
1337
|
+
*
|
|
1338
|
+
* 🔒 **Hand-written and mechanically held to the interfaces.** `KEY01` in
|
|
1339
|
+
* `selftest.ts` reads this file's own source, extracts each named interface's
|
|
1340
|
+
* field list and compares it to the entry here, so the pair cannot drift: adding
|
|
1341
|
+
* a field and forgetting this table is a red run, not a key an author cannot
|
|
1342
|
+
* write. `KEY02` closes the other direction — a key declared here and occurring
|
|
1343
|
+
* nowhere else in the tree is refused, which is exactly what `scaleYMode` was.
|
|
1344
|
+
*
|
|
1345
|
+
* ⚠️ `RigUnimplementedAttachment` is deliberately absent. It carries
|
|
1346
|
+
* `[field: string]: unknown` because its whole job is to let a spec *say* a
|
|
1347
|
+
* `point` or a `linkedmesh` and get a named `NotImplementedError` back; checking
|
|
1348
|
+
* the keys of an attachment rigc is about to refuse by type would name the wrong
|
|
1349
|
+
* fault.
|
|
1350
|
+
*/
|
|
1351
|
+
export const RIG_KEYS = {
|
|
1352
|
+
RigSpec: ['spec', 'name', 'note', 'skeleton', 'images', 'bones', 'slots', 'skins', 'constraints', 'events', 'invariants'],
|
|
1353
|
+
RigSkeletonHeader: ['x', 'y', 'width', 'height', 'fps', 'referenceScale', 'images'],
|
|
1354
|
+
RigBone: ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inherit', 'skin', 'color', 'icon', 'from'],
|
|
1355
|
+
RigBoneFrom: ['anchor', 'slotWindow', 'meshCenter', 'rotation'],
|
|
1356
|
+
RigSlot: ['name', 'bone', 'attachment', 'color', 'dark', 'blend'],
|
|
1357
|
+
RigEvent: ['int', 'float', 'string', 'audio', 'volume', 'balance'],
|
|
1358
|
+
RigInvariants: ['meshSlots', 'meshTriangles', 'axisBone', 'massBone', 'detached', 'deformMayFold', 'editorRoundTrip'],
|
|
1359
|
+
RigDetachedRule: ['bone', 'notUnder', 'why'],
|
|
1360
|
+
RigDeformFoldExemption: ['slot', 'why'],
|
|
1361
|
+
// Not retyped: `RIG_SKIN_KEYS` already IS this set, and it is the set
|
|
1362
|
+
// `splitRigSkin` refuses a long-form skin's stray key against. A second
|
|
1363
|
+
// spelling of it here would be two lists that have to agree, which is the
|
|
1364
|
+
// defect this whole table is checked to avoid.
|
|
1365
|
+
RigSkinEntry: RIG_SKIN_KEYS,
|
|
1366
|
+
RigIkConstraint: ['name', 'skin', 'type', 'bones', 'target', 'scaleY', 'mix', 'softness', 'bendPositive', 'compress', 'stretch'],
|
|
1367
|
+
RigTransformConstraint: [
|
|
1368
|
+
'name', 'skin', 'type', 'bones', 'source', 'localSource', 'localTarget', 'additive', 'clamp', 'properties',
|
|
1369
|
+
'rotation', 'x', 'y', 'scaleX', 'scaleY', 'shearY',
|
|
1370
|
+
'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY',
|
|
1371
|
+
],
|
|
1372
|
+
RigPathConstraint: [
|
|
1373
|
+
'name', 'skin', 'type', 'bones', 'slot', 'positionMode', 'spacingMode', 'rotateMode',
|
|
1374
|
+
'rotation', 'position', 'spacing', 'mixRotate', 'mixX', 'mixY',
|
|
1375
|
+
],
|
|
1376
|
+
RigPhysicsConstraint: [
|
|
1377
|
+
'name', 'skin', 'type', 'bone', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'scaleY', 'limit', 'fps',
|
|
1378
|
+
'inertia', 'strength', 'damping', 'mass', 'wind', 'gravity', 'mix',
|
|
1379
|
+
'inertiaGlobal', 'strengthGlobal', 'dampingGlobal', 'massGlobal', 'windGlobal', 'gravityGlobal', 'mixGlobal',
|
|
1380
|
+
],
|
|
1381
|
+
RigSliderConstraint: [
|
|
1382
|
+
'name', 'skin', 'type', 'animation', 'mix', 'additive', 'loop', 'bone', 'property', 'from', 'to', 'scale',
|
|
1383
|
+
'max', 'local', 'time',
|
|
1384
|
+
],
|
|
1385
|
+
RigTransformProperty: ['offset', 'to'],
|
|
1386
|
+
RigTransformTo: ['offset', 'max', 'scale'],
|
|
1387
|
+
RigRegionAttachment: ['type', 'path', 'image', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color'],
|
|
1388
|
+
RigMeshAttachment: [
|
|
1389
|
+
'type', 'path', 'image', 'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges',
|
|
1390
|
+
'width', 'height', 'color', 'generator',
|
|
1391
|
+
],
|
|
1392
|
+
RigMeshBinding: ['bone', 'x', 'y', 'weight'],
|
|
1393
|
+
RigBoundingBoxAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type'],
|
|
1394
|
+
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'end', 'convex', 'inverse'],
|
|
1395
|
+
RigPathAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'closed', 'constantSpeed', 'lengths'],
|
|
1396
|
+
RigRingGenerator: ['kind', 'hull', 'center', 'inner', 'size', 'bias', 'controls'],
|
|
1397
|
+
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
1398
|
+
RigContourGenerator: ['kind', 'tolerance', 'margin', 'maxVertices', 'alpha', 'depth', 'soft'],
|
|
1399
|
+
RigGridGenerator: ['kind', 'us', 'vs', 'cols', 'rows', 'depth', 'soft'],
|
|
1400
|
+
RigMeshBias: ['axis_deg', 'ramp'],
|
|
1401
|
+
RigDepthMap: ['image', 'near', 'zScale', 'gamma', 'contrast', 'bias'],
|
|
1402
|
+
RigSoftRegion: ['bone', 'mask'],
|
|
1403
|
+
} as const satisfies Record<string, readonly string[]>;
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* The five constraint `type` names, and the shape each one's keys come from.
|
|
1407
|
+
*
|
|
1408
|
+
* `satisfies` over `RigSkinConstraintKey` is what makes the five exhaustive: a
|
|
1409
|
+
* sixth constraint type added to that union without an entry here is a type
|
|
1410
|
+
* error rather than a shape whose keys go unchecked.
|
|
1411
|
+
*/
|
|
1412
|
+
const CONSTRAINT_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
1413
|
+
ik: 'RigIkConstraint',
|
|
1414
|
+
transform: 'RigTransformConstraint',
|
|
1415
|
+
path: 'RigPathConstraint',
|
|
1416
|
+
physics: 'RigPhysicsConstraint',
|
|
1417
|
+
slider: 'RigSliderConstraint',
|
|
1418
|
+
} satisfies Record<RigSkinConstraintKey, keyof typeof RIG_KEYS>;
|
|
1419
|
+
|
|
1420
|
+
/** The attachment `type` names, and the shape each one's keys come from. */
|
|
1421
|
+
const ATTACHMENT_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
1422
|
+
region: 'RigRegionAttachment',
|
|
1423
|
+
mesh: 'RigMeshAttachment',
|
|
1424
|
+
boundingbox: 'RigBoundingBoxAttachment',
|
|
1425
|
+
clipping: 'RigClippingAttachment',
|
|
1426
|
+
path: 'RigPathAttachment',
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
/** The generator `kind` names, and the shape each one's keys come from. */
|
|
1430
|
+
const GENERATOR_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
1431
|
+
ring: 'RigRingGenerator',
|
|
1432
|
+
ribbon: 'RigRibbonGenerator',
|
|
1433
|
+
contour: 'RigContourGenerator',
|
|
1434
|
+
grid: 'RigGridGenerator',
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Refuse every key of this rig spec that no shape above declares.
|
|
1439
|
+
*
|
|
1440
|
+
* ⭐ It walks the file rather than the emitter's route, and that is the whole
|
|
1441
|
+
* design. `compile` reaches an attachment only through a slot it is going to
|
|
1442
|
+
* draw and a `setup` entry only through a slot that has attachments, so a check
|
|
1443
|
+
* riding along with the emitter inherits its blind spots — which is how issue
|
|
1444
|
+
* #293's refusal sat green for three weeks on exactly the half-finished rigs it
|
|
1445
|
+
* was written for. Every node of the document is visited here, whether or not
|
|
1446
|
+
* anything downstream would have looked at it.
|
|
1447
|
+
*
|
|
1448
|
+
* A node that is not an object is left alone: its shape is somebody else's
|
|
1449
|
+
* refusal, and naming its keys would be a second opinion on a fault already
|
|
1450
|
+
* reported (see the note at the head of `parseMotionSpec`).
|
|
1451
|
+
*/
|
|
1452
|
+
function checkRigSpecKeys(raw: Record<string, unknown>, where: string): void {
|
|
1453
|
+
const at = (node: unknown, shape: keyof typeof RIG_KEYS, what: string): void => {
|
|
1454
|
+
if (isObj(node)) refuseUnknownKeys(node, RIG_KEYS[shape], where, what);
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
at(raw, 'RigSpec', 'this rig spec');
|
|
1458
|
+
at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"');
|
|
1459
|
+
|
|
1460
|
+
for (const [i, bone] of (Array.isArray(raw.bones) ? raw.bones : []).entries()) {
|
|
1461
|
+
const who = isObj(bone) && typeof bone.name === 'string' ? `bone "${bone.name}"` : `bones[${i}]`;
|
|
1462
|
+
at(bone, 'RigBone', who);
|
|
1463
|
+
if (isObj(bone)) at(bone.from, 'RigBoneFrom', `${who}'s "from"`);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
for (const [i, slot] of (Array.isArray(raw.slots) ? raw.slots : []).entries()) {
|
|
1467
|
+
at(slot, 'RigSlot', isObj(slot) && typeof slot.name === 'string' ? `slot "${slot.name}"` : `slots[${i}]`);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
for (const [i, constraint] of (Array.isArray(raw.constraints) ? raw.constraints : []).entries()) {
|
|
1471
|
+
if (!isObj(constraint)) continue;
|
|
1472
|
+
const named = typeof constraint.name === 'string' ? `constraint "${constraint.name}"` : `constraints[${i}]`;
|
|
1473
|
+
const shape = CONSTRAINT_SHAPE[String(constraint.type)];
|
|
1474
|
+
// An unknown `type` is `buildRigConstraint`'s refusal and names the five
|
|
1475
|
+
// that exist; there is no key set to check it against and no honest one to
|
|
1476
|
+
// guess, so it goes past here to the message that can say something.
|
|
1477
|
+
if (shape === undefined) continue;
|
|
1478
|
+
at(constraint, shape, `${named} (${String(constraint.type)})`);
|
|
1479
|
+
for (const [from, entry] of Object.entries(isObj(constraint.properties) ? constraint.properties : {})) {
|
|
1480
|
+
at(entry, 'RigTransformProperty', `${named} properties."${from}"`);
|
|
1481
|
+
if (!isObj(entry)) continue;
|
|
1482
|
+
for (const [to, driven] of Object.entries(isObj(entry.to) ? entry.to : {})) {
|
|
1483
|
+
at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
for (const [name, event] of Object.entries(isObj(raw.events) ? raw.events : {})) {
|
|
1489
|
+
at(event, 'RigEvent', `event "${name}"`);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (isObj(raw.invariants)) {
|
|
1493
|
+
at(raw.invariants, 'RigInvariants', '"invariants"');
|
|
1494
|
+
for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
|
|
1495
|
+
at(rule, 'RigDetachedRule', `invariants.detached[${i}]`);
|
|
1496
|
+
}
|
|
1497
|
+
for (const [i, rule] of (Array.isArray(raw.invariants.deformMayFold) ? raw.invariants.deformMayFold : []).entries()) {
|
|
1498
|
+
at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
for (const [skinName, skin] of Object.entries(isObj(raw.skins) ? raw.skins : {})) {
|
|
1503
|
+
if (!isObj(skin)) continue;
|
|
1504
|
+
// The long form's own key check is `splitRigSkin`'s and it already names the
|
|
1505
|
+
// likeliest cause (a slot left outside `attachments`), so it is reused
|
|
1506
|
+
// rather than restated — one refusal per fault.
|
|
1507
|
+
const parts = splitRigSkin(skin as RigSkin, `${where}: skin "${skinName}"`);
|
|
1508
|
+
for (const [slot, placeholders] of Object.entries(parts.attachments)) {
|
|
1509
|
+
if (!isObj(placeholders)) continue;
|
|
1510
|
+
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
1511
|
+
if (!isObj(att)) continue;
|
|
1512
|
+
const who = `skin "${skinName}" slot "${slot}" attachment "${placeholder}"`;
|
|
1513
|
+
// `type` absent means `region` — the parser's own default (`:539`).
|
|
1514
|
+
const type = att.type === undefined ? 'region' : String(att.type);
|
|
1515
|
+
const shape = ATTACHMENT_SHAPE[type];
|
|
1516
|
+
if (shape === undefined) continue;
|
|
1517
|
+
at(att, shape, `${who} (${type})`);
|
|
1518
|
+
for (const [i, vertex] of (Array.isArray(att.weights) ? att.weights : []).entries()) {
|
|
1519
|
+
for (const [j, binding] of (Array.isArray(vertex) ? vertex : []).entries()) {
|
|
1520
|
+
at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (!isObj(att.generator)) continue;
|
|
1524
|
+
const gen = att.generator;
|
|
1525
|
+
const genShape = GENERATOR_SHAPE[String(gen.kind)];
|
|
1526
|
+
// Same rule as an unknown attachment type: an unknown `kind` is the mesh
|
|
1527
|
+
// builder's refusal, which can name the four that exist.
|
|
1528
|
+
if (genShape === undefined) continue;
|
|
1529
|
+
at(gen, genShape, `${who} generator (${String(gen.kind)})`);
|
|
1530
|
+
at(gen.bias, 'RigMeshBias', `${who} generator.bias`);
|
|
1531
|
+
at(gen.depth, 'RigDepthMap', `${who} generator.depth`);
|
|
1532
|
+
at(gen.soft, 'RigSoftRegion', `${who} generator.soft`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1229
1538
|
/**
|
|
1230
1539
|
* Parse and check the envelope, then hand back a typed spec.
|
|
1231
1540
|
*
|
|
@@ -1250,6 +1559,12 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
1250
1559
|
if (!Array.isArray(raw.slots)) {
|
|
1251
1560
|
throw new CompileError(`${where}: a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)`);
|
|
1252
1561
|
}
|
|
1562
|
+
// Before anything resolves by name, because a key nothing reads is very often
|
|
1563
|
+
// the CAUSE of the name that does not resolve: `"bones"` typed on a slider is
|
|
1564
|
+
// a slider with no driving bone, and the refusal an author wants names the
|
|
1565
|
+
// typo rather than the consequence.
|
|
1566
|
+
checkRigSpecKeys(raw, where);
|
|
1567
|
+
|
|
1253
1568
|
const spec = raw as unknown as RigSpec;
|
|
1254
1569
|
|
|
1255
1570
|
const seen = new Set<string>();
|
|
@@ -1351,6 +1666,19 @@ export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
|
1351
1666
|
}
|
|
1352
1667
|
}
|
|
1353
1668
|
|
|
1669
|
+
// `invariants.editorRoundTrip` — the field that turns a check ON, so the only
|
|
1670
|
+
// thing it can be wrong about is saying nothing while looking like it said
|
|
1671
|
+
// something. `false` is refused for exactly that: it reads as a decision and
|
|
1672
|
+
// behaves as an absence, which is the shape issue #545 closed elsewhere.
|
|
1673
|
+
const roundTrip = spec.invariants?.editorRoundTrip;
|
|
1674
|
+
if (roundTrip !== undefined && roundTrip !== true) {
|
|
1675
|
+
throw new CompileError(
|
|
1676
|
+
`${where}: invariants.editorRoundTrip is ${JSON.stringify(roundTrip)}; the only accepted value is \`true\`. ` +
|
|
1677
|
+
'A rig that is not authored for the editor leaves the key out — A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP then ' +
|
|
1678
|
+
'SKIPs and still names anything a round trip would drop, so nothing is lost by saying nothing',
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1354
1682
|
const constraintNames = new Set<string>();
|
|
1355
1683
|
/** name -> [type, skinRequired], for the skin lists below. */
|
|
1356
1684
|
const constraintFacts = new Map<string, [string, boolean]>();
|