spine-rigc 1.9.1 → 1.9.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.
Files changed (56) hide show
  1. package/cli.ts +28 -4375
  2. package/docs/AUTHORING.md +2 -2
  3. package/package.json +1 -1
  4. package/src/assertions/bodies/a10.ts +116 -0
  5. package/src/assertions/bodies/a13.ts +3 -3
  6. package/src/assertions/bodies/a14.ts +3 -3
  7. package/src/assertions/bodies/a15.ts +5 -5
  8. package/src/assertions/bodies/a22.ts +3 -3
  9. package/src/assertions/bodies/a34.ts +133 -0
  10. package/src/assertions/bodies/a40.ts +128 -0
  11. package/src/assertions/facts/animated_bones.ts +4 -2
  12. package/src/assertions/facts/constraint_targets.ts +56 -0
  13. package/src/assertions/facts/mesh_attachments.ts +33 -4
  14. package/src/assertions/facts/slider_composition.ts +78 -0
  15. package/src/assertions/facts/stepped_poses.ts +74 -0
  16. package/src/assertions/model/animated_bones.ts +13 -5
  17. package/src/assertions/model/bone_timelines.ts +5 -3
  18. package/src/assertions/model/constraint_targets.ts +82 -0
  19. package/src/assertions/model/declared.ts +127 -0
  20. package/src/assertions/model/index.ts +81 -39
  21. package/src/assertions/model/mesh_attachments.ts +31 -2
  22. package/src/assertions/model/parse.ts +9 -1
  23. package/src/assertions/model/runtime_timelines.ts +78 -0
  24. package/src/assertions/model/slider_composition.ts +193 -0
  25. package/src/assertions/model/stepped_poses.ts +90 -0
  26. package/src/bonedist.ts +22 -11
  27. package/src/check.ts +3 -2
  28. package/src/checkpics.ts +1 -1
  29. package/src/cli/core_commands.ts +1532 -0
  30. package/src/cli/shared.ts +1935 -0
  31. package/src/cli/spine_commands.ts +1141 -0
  32. package/src/core/additive.ts +458 -0
  33. package/src/core/constraints.ts +38 -5
  34. package/src/core/constraints_physics.ts +42 -9
  35. package/src/core/constraints_slider.ts +136 -6
  36. package/src/core/index.ts +27 -3
  37. package/src/core/raw.ts +77 -16
  38. package/src/core/skins.ts +28 -3
  39. package/src/core/walk.ts +175 -0
  40. package/src/correspondence.ts +15 -0
  41. package/src/deformbuild.ts +60 -0
  42. package/src/deformmeasure.ts +20 -48
  43. package/src/deformreport.ts +2 -2
  44. package/src/framing.ts +1 -1
  45. package/src/nonfinite.ts +54 -0
  46. package/src/pose.ts +1 -1
  47. package/src/preview.ts +1 -1
  48. package/src/region_joins.ts +54 -0
  49. package/src/render.ts +243 -2808
  50. package/src/render_core.ts +19 -31
  51. package/src/render_shared.ts +2887 -0
  52. package/src/slots.ts +1 -1
  53. package/src/spine_side.ts +123 -0
  54. package/src/validate.ts +443 -548
  55. package/src/assertions/facts/skin_meshes.ts +0 -49
  56. package/src/assertions/model/skin_meshes.ts +0 -90
@@ -1,8 +1,16 @@
1
1
  /**
2
- * The mesh attachments, as the bodies that read their geometry and their
3
- * weights see them (issue #1025, step 4c of #380) — the census's F07 (vertex
4
- * data), F08 (mesh geometry) and F10 (the linked-mesh join), for A04, A20,
5
- * A21, A28 and the mesh half of A23.
2
+ * The mesh attachments, as every body that reads a mesh sees them (issue
3
+ * #1025, step 4c of #380) — the census's F03, F05, F07 (vertex data), F08
4
+ * (mesh geometry) and F10 (the linked-mesh join), for A04, A13, A14, A15, A20,
5
+ * A21, A22, A28 and the mesh half of A23.
6
+ *
7
+ * ⭐ **One family** (issue #1054). Cut 4c-1 stated the meshes for A13, A14,
8
+ * A15 and A22 as a family of their own (`SkinMeshFacts`) and cut 4c-2 stated
9
+ * them again here; the two walked the same entries in the same order and
10
+ * agreed on every field they shared, and two families of one fact are two
11
+ * places for a supplier to disagree with itself. The size the budget and
12
+ * canvas rules read joined this one, and the bones a mesh's weights name are
13
+ * derived from `weights` (`weightBonesOf`) rather than stated beside them.
6
14
  *
7
15
  * ⭐ **The order is the file's**, as `./skin_entries.ts` states it: every
8
16
  * skin in the file's order, and within a skin the entries slot by slot in the
@@ -56,6 +64,9 @@ export interface MeshEntry {
56
64
  readonly regionUVs: readonly number[];
57
65
  /** `hullLength`: two numbers per hull vertex. */
58
66
  readonly hullLength: number;
67
+ /** Its `width` and `height` — a link's are its source's, which the runtime's `setSourceMesh` writes over the link's own. */
68
+ readonly width: number;
69
+ readonly height: number;
59
70
  /** Per vertex its influences, in order — or `null` for an unweighted mesh. */
60
71
  readonly weights: ReadonlyArray<readonly MeshBinding[]> | null;
61
72
  /** The kept clauses' findings about this mesh's vertex run (`A04`'s encoding rules), in order; never on the model side. */
@@ -69,3 +80,21 @@ export interface MeshFacts {
69
80
  /** Every mesh attachment of every skin, in the file's walk order. */
70
81
  readonly meshes: readonly MeshEntry[];
71
82
  }
83
+
84
+ /**
85
+ * Every bone a mesh's weights name, in the weight run's order (repeats kept),
86
+ * by name — empty for an unweighted mesh. A binding whose index the roster
87
+ * does not hold names nothing here: that index is the encoding's fault, which
88
+ * `A20`'s kept clause names (`MeshBinding.encoding`), and no bone drives the
89
+ * mesh through it.
90
+ */
91
+ export function weightBonesOf(facts: Pick<MeshFacts, 'bones'>, mesh: Pick<MeshEntry, 'weights'>): string[] {
92
+ const names: string[] = [];
93
+ for (const vertex of mesh.weights ?? []) {
94
+ for (const binding of vertex) {
95
+ const name = facts.bones[binding.bone];
96
+ if (name !== undefined) names.push(name);
97
+ }
98
+ }
99
+ return names;
100
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The sliders and the timelines their animations key, as A40 reads them
3
+ * (issue #1025, cut 4c-5 of step 4c of #380) — the census's F01–F04, F12,
4
+ * F15, F18, F19 and R3.
5
+ *
6
+ * ⭐ **The order is the runtime's.** The sliders in the `constraints` array's
7
+ * order, which is the update order and the order A40 calls "earlier" and
8
+ * "later" in; each animation's timelines in the order the runtime builds
9
+ * them — the slot timelines, the bone timelines, then ik, transform, path,
10
+ * physics, slider, the attachments' deform and sequence, the draw order and
11
+ * the events, each group's targets in the order the file keys them [measured:
12
+ * an animation keying `events`, `drawOrder`, `attachments`, `slider`,
13
+ * `physics`, `path`, `transform`, `ik`, `slots`, `bones` in that key order
14
+ * loads them slots, bones, ik, transform, path, physics, slider, deform,
15
+ * sequence, draw order, events]; each timeline's properties in the order it
16
+ * registers them. A40 groups the timelines by property in that order and
17
+ * prints one line per property shared, so the order is a fact both suppliers
18
+ * state, not a sort either applies.
19
+ *
20
+ * 🔑 **A property is named by an id both suppliers spell the same way**: the
21
+ * property's name, then the index of the bone, slot or constraint it
22
+ * addresses in the skeleton's order (`-1` for a physics timeline naming no
23
+ * constraint), then — for a deform or sequence timeline — the attachment it
24
+ * is keyed on as `skin/slot/placeholder`. The runtime spells the last as an
25
+ * object's serial number, which is the runtime's and not the rig's, so its
26
+ * supplier maps each attachment object to the address it is filed under; a
27
+ * physics `reset` and the draw-order and event timelines carry no index at
28
+ * all (measured: a named `reset` reads `physicsConstraintReset` alone).
29
+ *
30
+ * 🔸 **What applying a timeline additively does is a question, not a value.**
31
+ * It is posed — twice, with `add`, from two start states, under every skin —
32
+ * so a body asks for it (`behaviour`), and only for the timelines it needs:
33
+ * the validator answers with the runtime's probe, the model side with the
34
+ * core's (`src/core/additive.ts`), and the selftest puts every question a
35
+ * body asks to both.
36
+ *
37
+ * Links nothing from the runtime.
38
+ */
39
+
40
+ /** What applying a timeline twice with `add` does (`src/core/additive.ts`' header). */
41
+ export type AddBehaviour = 'accumulates' | 'overwrites' | 'inert';
42
+
43
+ /** One property a timeline registers: the id it is shared by, the property's name, and the target and property as A40's sentence names them. */
44
+ export interface SliderTimelineProperty {
45
+ readonly id: string;
46
+ readonly property: string;
47
+ readonly names: string;
48
+ }
49
+
50
+ /** One timeline of a slider's animation. */
51
+ export interface SliderTimelineFact {
52
+ /** The runtime class it loads as — the word A40's sentence names `apply` on. */
53
+ readonly runtimeClass: string;
54
+ readonly properties: readonly SliderTimelineProperty[];
55
+ }
56
+
57
+ /** One slider constraint. */
58
+ export interface SliderFact {
59
+ readonly name: string;
60
+ /** Its position in the skeleton's `constraints` array. */
61
+ readonly index: number;
62
+ /** Its setup `mix`. */
63
+ readonly mix: number;
64
+ readonly additive: boolean;
65
+ readonly skinRequired: boolean;
66
+ /** The skins whose `slider` list names it, in the skeleton's skin order. */
67
+ readonly skins: readonly string[];
68
+ /** The animation it applies, with its timelines in the runtime's order — or `null` where it applies none. */
69
+ readonly animation: { readonly name: string; readonly timelines: readonly SliderTimelineFact[] } | null;
70
+ }
71
+
72
+ /** What A40 reads beside the constraint facts. */
73
+ export interface SliderCompositionFacts {
74
+ /** Every slider, in the `constraints` array's order. */
75
+ readonly sliders: readonly SliderFact[];
76
+ /** What the `timeline`-th timeline of `animation` does when it is applied twice with `add` — posed. */
77
+ behaviour(animation: string, timeline: number): AddBehaviour;
78
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The stepped poses (issue #1025, cut 4c-5a of step 4c of #380) — the census's
3
+ * F01, F18, R4 (the posed `inherit`) and R5 as A10 reads them: the skeleton's
4
+ * setup pose with every physics state reset, every animation walked on a
5
+ * LOOPING track from that pose in equal steps, and a bone's inheritance mode
6
+ * posed at one time on a fresh, non-looping track.
7
+ *
8
+ * ⭐ **A pose is handed over whole, non-finite values in place.** A10's
9
+ * subject is the first number of a pose that is not finite, so a supplier
10
+ * that refused such a pose, or wrote it as something else, would answer a
11
+ * different question. The runtime's supplier reads what spine-core posed; the
12
+ * model side's is the core's looping walk (`src/core/walk.ts`), which returns
13
+ * the value rather than refusing it.
14
+ *
15
+ * 🔸 **The inheritance mode is `null` where the pose holds a mode**, and the
16
+ * value it holds, as `String` spells it, where it holds none — the one bone
17
+ * reading that can be wrong with every number finite (#733): the runtime's
18
+ * lookup leaves a spelling it does not resolve as no mode at all.
19
+ *
20
+ * The bone timelines A10 walks for its `inherit` keys are `BoneTimelineFacts`
21
+ * (`./bone_timelines.ts`), the family A24, A29 and A30 read.
22
+ *
23
+ * Links nothing from the runtime: `validate()` supplies it from spine-core
24
+ * (`spineSteppedPoses`), the model side from the document and the core
25
+ * (`../model/stepped_poses.ts`).
26
+ */
27
+ import type { PosedVertices, WorldTransform } from '../../nonfinite.ts';
28
+
29
+ /** One bone of a pose: its world transform as computed, and its inheritance mode — `null` for a mode, else the value held. */
30
+ export interface SteppedBone extends WorldTransform {
31
+ readonly inherit: string | null;
32
+ }
33
+
34
+ /** One slot of a pose: its light colour and its dark colour (`null` for a slot holding none), as computed. */
35
+ export interface SteppedSlot {
36
+ readonly name: string;
37
+ readonly colour: readonly [number, number, number, number];
38
+ readonly dark: readonly [number, number, number] | null;
39
+ }
40
+
41
+ /** One pose: every bone in skeleton order, every region and mesh shown in draw order, every slot in setup order. */
42
+ export interface SteppedFrame {
43
+ readonly bones: readonly SteppedBone[];
44
+ readonly drawn: readonly PosedVertices[];
45
+ readonly slots: readonly SteppedSlot[];
46
+ }
47
+
48
+ /** What A10 reads. */
49
+ export interface SteppedPoseFacts {
50
+ /** The skeleton's bone count. */
51
+ readonly boneCount: number;
52
+ /** The animations, in the file's order, each with the runtime's duration (the last key of every timeline, as float32). */
53
+ readonly steppedAnimations: ReadonlyArray<{ readonly name: string; readonly duration: number }>;
54
+ /** Whether the skeleton holds an animation of this name. */
55
+ hasAnimation(name: string): boolean;
56
+ /** Whether the skeleton holds a bone of this name. */
57
+ hasBone(name: string): boolean;
58
+ /**
59
+ * The bone's inheritance mode with the animation applied at `time` on a
60
+ * fresh, non-looping track from the setup pose, every physics state reset
61
+ * before it — `null` where the pose holds a mode, else that value spelled by
62
+ * `String`, and `undefined` for a bone the skeleton does not have.
63
+ */
64
+ posedInherit(animation: string, bone: string, time: number): string | null | undefined;
65
+ /** How the file spells a bone's setup `inherit`: `JSON.stringify` of the stated value (`"undefined"` for none). */
66
+ statedInherit(bone: string): string;
67
+ /** The setup pose, every physics state reset, before any animation is set. */
68
+ setup(): SteppedFrame;
69
+ /**
70
+ * The animation set on a LOOPING track over that setup pose, then `frames`
71
+ * steps of `step` each — the poses after steps 1 to `frames`, in order.
72
+ */
73
+ walk(animation: string, step: number, frames: number): SteppedFrame[];
74
+ }
@@ -5,13 +5,21 @@
5
5
  * The document writes each animation's `bones` as a list in the model's map
6
6
  * order, and the emitter writes the Spine file's `bones` group from that same
7
7
  * map, entry for entry, leaving the group out when it is empty
8
- * (`emitAnimation`'s `if (animation.bones.size)`) — so the list's order is the
9
- * file's, an empty list is a file with no `bones` group, and an animation the
10
- * document does not hold is one the file does not key. The first animation of
11
- * a name answers, as the file's object would hold only one.
8
+ * (`emitAnimation`'s `if (animation.bones.size)`) — so an empty list is a file
9
+ * with no `bones` group, and an animation the document does not hold is one the
10
+ * file does not key. The first animation of a name answers, as the file's
11
+ * object would hold only one.
12
+ *
13
+ * 🔢 **The order is `keyedOrder` over the list's, not the list's** (issue
14
+ * #1039). The group is an object keyed by bone name, so the file lists an
15
+ * array-index name (`5`, `10`) first whatever order the emitter filled it in —
16
+ * measured on a walk whose tracks key `-a, 5, 2b, 10, …`: the document lists
17
+ * that, the file and spine-core `5, 10, -a, 2b, …`, and A15 prints one line per
18
+ * keyed bone in the file's order.
12
19
  *
13
20
  * Links nothing from the runtime.
14
21
  */
22
+ import { keyedOrder } from '../../compile.ts';
15
23
  import type { AnimatedBoneFacts } from '../facts/animated_bones.ts';
16
24
  import { isObj } from '../values.ts';
17
25
  import type { ReadDocument } from './parse.ts';
@@ -24,7 +32,7 @@ export function modelAnimatedBones(read: ReadDocument): AnimatedBoneFacts {
24
32
  if (animation === undefined) return undefined;
25
33
  const bones = Array.isArray(animation.bones) ? animation.bones.filter(isObj) : [];
26
34
  if (bones.length === 0) return null;
27
- return bones.map((bone) => String(bone.name));
35
+ return keyedOrder(bones.map((bone) => String(bone.name)));
28
36
  },
29
37
  };
30
38
  }
@@ -6,9 +6,11 @@
6
6
  * `./slot_colour.ts`), over the `bones` group: the animations put in the
7
7
  * file's order by `editorAnimationOrder` (`src/compile.ts`, the function the
8
8
  * emitter is handed), each animation's bones and their timelines laid out as
9
- * the file lays them (`animations.<name>.bones.<bone>.<timeline> = keys`, in
10
- * the document's order, which is the order `emitAnimation` writes the model's
11
- * map in), and the whole handed to the emitter's own `withoutParserDefaults`
9
+ * the file lays them (`animations.<name>.bones.<bone>.<timeline> = keys`,
10
+ * filled in the document's order, which is the order `emitAnimation` writes
11
+ * the model's map in — and read back out of that object, so an array-index
12
+ * bone name comes first as it does in the file: issue #1039 measured this
13
+ * reader already walking the file's order), and the whole handed to the emitter's own `withoutParserDefaults`
12
14
  * and `inEditorKeyOrder` (`src/keyorder.ts`) — so a key field the emitter
13
15
  * leaves out at the parser's default (`time: 0`, a translate key's `y: 0`) is
14
16
  * left out here too, and a body printing a key's `time` prints what it prints
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The model side's supply of `ConstraintTargetFacts` (issue #1025, cut 4c-5
3
+ * of step 4c of #380): the document's constraints and every animation's
4
+ * constraint groups, laid out as the Spine file lays them.
5
+ *
6
+ * - **The constraints**: the document's, in its order — which the emitter
7
+ * writes the `constraints` array in — each its name and its kind, the word
8
+ * the file spells its `type` with.
9
+ * - **The animations**: in the file's order (`fileAnimationOrder`, issue
10
+ * #1034); in each the five groups in A34's fixed order, a group only where
11
+ * the document holds an entry for it (`emitAnimation` writes a group only
12
+ * when the model holds something for it); each group's entries in the order
13
+ * the file keys them — an object the emitter fills in the document's order,
14
+ * so its names land as `keyedOrder` lands them, built here as that object;
15
+ * the timeline naming no physics constraint under the empty name the
16
+ * emitter spells it with, where the document spells it `*`
17
+ * (`EVERY_GLOBAL_PHYSICS`); each entry's timelines in the document's order,
18
+ * which the emitter writes unchanged (a name-keyed map no key-order pass
19
+ * touches, `src/keyorder.ts`). A document the reader accepted states every
20
+ * key array as a non-empty list, a named group's entry as named timelines,
21
+ * and only constraints of the kind its group names, so the clauses that
22
+ * refuse those shapes have nothing to fire on here.
23
+ * - **The reach** of a physics timeline naming none: the core's own reading,
24
+ * `unnamedReach` (`./constraints.ts`, which poses the timeline through
25
+ * `posedPhysics` and reads which records took the key), over the document's
26
+ * physics records in its order. The reader refuses a physics timeline name
27
+ * it does not know, so every name asked of it is one the core poses.
28
+ *
29
+ * Links nothing from the runtime.
30
+ */
31
+ import { fileAnimationOrder } from '../../compile.ts';
32
+ import { EVERY_GLOBAL_PHYSICS, PHYSICS_TIMELINE_KINDS, type CorePhysicsRecord } from '../../core/constraints_physics.ts';
33
+ import type { ConstraintTargetFacts, TargetAnimation, TargetEntry, TargetKeyArray } from '../facts/constraint_targets.ts';
34
+ import { isObj, type Json } from '../values.ts';
35
+ import { unnamedReach } from './constraints.ts';
36
+ import type { ReadDocument } from './parse.ts';
37
+
38
+ const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
39
+ const keyArray = (timeline: string, keys: unknown): TargetKeyArray => ({ timeline, keys: Array.isArray(keys) ? { count: keys.length } : { spelled: `${JSON.stringify(keys)}` } });
40
+
41
+ /** One group's entries as the file keys them: an object filled in the document's order, read back in its key order. */
42
+ function fileEntries(group: 'ik' | 'transform' | 'path' | 'physics' | 'slider', entries: readonly Json[]): TargetEntry[] {
43
+ const keyed: Record<string, TargetEntry> = {};
44
+ for (const entry of entries) {
45
+ if (typeof entry.name !== 'string') continue;
46
+ const name = group === 'physics' && entry.name === EVERY_GLOBAL_PHYSICS ? '' : entry.name;
47
+ keyed[name] =
48
+ group === 'ik' || group === 'transform'
49
+ ? { name, bare: null, keyArrays: [keyArray('', entry.keys)] }
50
+ : { name, bare: null, keyArrays: list(entry.timelines).flatMap((t) => (typeof t.name === 'string' ? [keyArray(t.name, t.keys)] : [])) };
51
+ }
52
+ return Object.values(keyed);
53
+ }
54
+
55
+ export function modelConstraintTargets(read: ReadDocument): ConstraintTargetFacts {
56
+ const { doc } = read;
57
+ const byName = new Map<string, Json>();
58
+ for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
59
+ const animations: TargetAnimation[] = [];
60
+ for (const name of fileAnimationOrder(doc)) {
61
+ const anim = byName.get(name);
62
+ if (anim === undefined) continue;
63
+ const groups: TargetAnimation['groups'][number][] = [];
64
+ const constraints = isObj(anim.constraints) ? anim.constraints : {};
65
+ for (const group of ['ik', 'transform', 'path', 'physics', 'slider'] as const) {
66
+ const entries = list(constraints[group]);
67
+ if (entries.length > 0) groups.push({ group, entries: fileEntries(group, entries) });
68
+ }
69
+ animations.push({ name, groups });
70
+ }
71
+ const records = doc.constraints.flatMap((c): CorePhysicsRecord[] => (c.record?.kind === 'physics' ? [c.record] : []));
72
+ return {
73
+ constraints: doc.constraints.map((c) => ({ name: c.name, type: c.kind, spelled: c.name })),
74
+ groupsByAnimation: animations,
75
+ reach: (timeline) => {
76
+ const kind = PHYSICS_TIMELINE_KINDS.find((k) => k === timeline);
77
+ if (kind === undefined) return null;
78
+ const reached = unnamedReach(records, kind);
79
+ return { resets: kind === 'reset', reached: records.filter((r) => reached.has(r.name)).map((r) => r.name) };
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * What the build declared, read off the document (issue #1054): the rig info
3
+ * the archetype and declaration rules read, and each animation's declared
4
+ * duration, which A09 holds the keys against.
5
+ *
6
+ * ⭐ **The document is the one source, on every spec it is read at.** A
7
+ * `rigc-compiled/3`, `/2` and `/1` document all carry the `rig` section
8
+ * (`CompileResult.rig` as `modelDocument` writes it) and every animation's
9
+ * `duration` (the motion spec's, which `readModel` reads as
10
+ * `CoreAnimationTimelines.declared` and refuses when it is not a finite
11
+ * number). Until this module the model side took both from its caller, as
12
+ * `validate()` does, and its verdict moved with what the caller handed it:
13
+ * measured over the 19 recipes with the document's own values left out, A09
14
+ * read SKIP on 38 of 38 cells, ten rules read their bare-directory SKIP, and
15
+ * A20 read FAIL on 7 cells where it reads PASS — without the rig every mesh is
16
+ * judged a generated ring (`meshKindOf`), so an authored mesh fails the
17
+ * generator policy. A value the document states is read from the document,
18
+ * and the same value given beside it is refused by name
19
+ * (`refuseDeclaredBeside`), as issue #1026 made the stage and `pma`
20
+ * (`./given.ts`).
21
+ *
22
+ * 🔒 **The rig section is read field by field, never cast.** A body reads a
23
+ * declaration only through `modelRigInfo`, which refuses by its path a field
24
+ * that is missing, of the wrong type or not one the writer writes — the
25
+ * model side's parse runs it (`./parse.ts`, `A00_MODEL_READ`), so a document
26
+ * whose declarations are malformed is refused where the reader refuses, and
27
+ * never judged against a guess.
28
+ *
29
+ * Links nothing from the runtime.
30
+ */
31
+ import type { MeshKind } from '../../mesh.ts';
32
+ import type { RigInfo } from '../../types.ts';
33
+ import { isObj } from '../values.ts';
34
+ import type { ReadDocument } from './parse.ts';
35
+
36
+ /** A rig section the model side cannot read as rig info: every problem, each by its path, in one sentence. */
37
+ export class DeclaredInputError extends Error {}
38
+
39
+ /** The mesh kinds a rig's `meshKinds` may name: the generators' and `authored`. */
40
+ const MESH_KINDS = ['ring', 'ribbon', 'contour', 'grid', 'segments', 'authored'] as const satisfies ReadonlyArray<MeshKind | 'authored'>;
41
+ // Every kind the type names is in the list, or this line does not type-check.
42
+ const MESH_KINDS_COMPLETE: Exclude<MeshKind | 'authored', (typeof MESH_KINDS)[number]> extends never ? true : false = true;
43
+ void MESH_KINDS_COMPLETE;
44
+
45
+ type Check = (value: unknown, at: string, problems: string[]) => void;
46
+ const say = (problems: string[], at: string, value: unknown, what: string): void => void problems.push(`${at} is ${JSON.stringify(value) ?? 'absent'}, not ${what}`);
47
+ const str: Check = (v, at, p) => (typeof v === 'string' ? undefined : say(p, at, v, 'a string'));
48
+ const bool: Check = (v, at, p) => (typeof v === 'boolean' ? undefined : say(p, at, v, 'true or false'));
49
+ const num: Check = (v, at, p) => (typeof v === 'number' && Number.isFinite(v) ? undefined : say(p, at, v, 'a finite number'));
50
+ const nullable = (inner: Check): Check => (v, at, p) => (v === null ? undefined : inner(v, at, p));
51
+ const list = (inner: Check, length?: number): Check => (v, at, p) => {
52
+ if (!Array.isArray(v) || (length !== undefined && v.length !== length)) return say(p, at, v, length === undefined ? 'a list' : `a list of ${length}`);
53
+ v.forEach((x, i) => inner(x, `${at}[${i}]`, p));
54
+ };
55
+ const record = (inner: Check): Check => (v, at, p) => {
56
+ if (!isObj(v)) return say(p, at, v, 'an object');
57
+ for (const [k, x] of Object.entries(v)) inner(x, `${at}.${k}`, p);
58
+ };
59
+ const oneOf = (words: readonly string[]): Check => (v, at, p) => (typeof v === 'string' && words.includes(v) ? undefined : say(p, at, v, `one of ${words.join(', ')}`));
60
+ const shape = (fields: Readonly<Record<string, Check>>): Check => (v, at, p) => {
61
+ if (!isObj(v)) return say(p, at, v, 'an object');
62
+ for (const [k, check] of Object.entries(fields)) check(v[k], `${at}.${k}`, p);
63
+ for (const k of Object.keys(v)) if (!(k in fields)) p.push(`${at}.${k} is not a field the rig section has`);
64
+ };
65
+
66
+ /** The rig section's fields, as `RigInfo` declares them and the writer writes every one — each required, none defaulted. */
67
+ const RIG_FIELDS: Readonly<Record<keyof RigInfo, Check>> = {
68
+ archetype: str,
69
+ axisBone: nullable(str),
70
+ axisSubtree: list(str),
71
+ detached: list(list(str, 2)),
72
+ slotOrder: nullable(list(str)),
73
+ meshKinds: record(oneOf(MESH_KINDS)),
74
+ meshDeclaredBones: record(list(str)),
75
+ meshSoftBones: record(str),
76
+ deformMayFold: list(str),
77
+ editorRoundTrip: bool,
78
+ consumerDrivenMix: list(shape({ type: oneOf(['ik', 'transform']), constraint: str, why: str })),
79
+ idleDrivesMeshes: nullable(str),
80
+ basePlates: list(str),
81
+ meshSlotBudget: nullable(num),
82
+ meshTriangleBudget: nullable(num),
83
+ contactDepth: nullable(num),
84
+ capContainmentCeiling: nullable(num),
85
+ massBone: nullable(str),
86
+ inwardUnit: nullable(list(num, 2)),
87
+ };
88
+
89
+ /** Every problem with the document's `rig` section, each by its path — empty when it reads as rig info. */
90
+ export function rigSectionProblems(json: Readonly<Record<string, unknown>>): string[] {
91
+ const problems: string[] = [];
92
+ shape(RIG_FIELDS)(json.rig, 'rig', problems);
93
+ return problems;
94
+ }
95
+
96
+ /** The rig info the document declares — its `rig` section, read field by field; a section that does not read is refused by its paths. */
97
+ export function modelRigInfo(read: ReadDocument): RigInfo {
98
+ const problems = rigSectionProblems(read.json);
99
+ if (problems.length > 0) throw new DeclaredInputError(`the document's rig section does not read as rig info: ${problems.length} problem(s): ${problems.join('; ')}`);
100
+ return read.json.rig as RigInfo;
101
+ }
102
+
103
+ /**
104
+ * Each animation's declared duration, as the document states it and the core
105
+ * reads it (`CoreAnimationTimelines.declared`), keyed in the document's order —
106
+ * the motion spec's, the order `CompileResult.declaredDurations` is filled in.
107
+ */
108
+ export function modelDeclaredDurations(read: ReadDocument): Record<string, number> {
109
+ const out: Record<string, number> = {};
110
+ for (const anim of read.doc.animations) out[anim.name] = anim.timelines.declared;
111
+ return out;
112
+ }
113
+
114
+ /** What a caller may not give beside a document, because every document states it. */
115
+ export type DeclaredValue = 'rig' | 'declaredDurations';
116
+
117
+ /** The sentence a value given beside the document is refused with. */
118
+ export function declaredBesideRefusal(spec: string, what: DeclaredValue): string {
119
+ const given = what === 'rig' ? 'the rig info' : 'the declared durations';
120
+ const stated = what === 'rig' ? 'states it itself in its rig section' : "states each animation's duration itself";
121
+ return `the caller gave ${given} beside a ${spec} document, which ${stated} (issue #1054); a second source for one fact is refused rather than one of them silently read`;
122
+ }
123
+
124
+ /** A caller's value beside a document that states it, refused by name. */
125
+ export function refuseDeclaredBeside(read: ReadDocument, input: { rig?: unknown; declaredDurations?: unknown }, what: DeclaredValue): void {
126
+ if (input[what] !== undefined) throw new Error(declaredBesideRefusal(read.doc.spec, what));
127
+ }