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.
- package/cli.ts +28 -4375
- package/docs/AUTHORING.md +2 -2
- package/package.json +1 -1
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a13.ts +3 -3
- package/src/assertions/bodies/a14.ts +3 -3
- package/src/assertions/bodies/a15.ts +5 -5
- package/src/assertions/bodies/a22.ts +3 -3
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/facts/animated_bones.ts +4 -2
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/mesh_attachments.ts +33 -4
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/model/animated_bones.ts +13 -5
- package/src/assertions/model/bone_timelines.ts +5 -3
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/declared.ts +127 -0
- package/src/assertions/model/index.ts +81 -39
- package/src/assertions/model/mesh_attachments.ts +31 -2
- package/src/assertions/model/parse.ts +9 -1
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/stepped_poses.ts +90 -0
- package/src/bonedist.ts +22 -11
- package/src/check.ts +3 -2
- package/src/checkpics.ts +1 -1
- package/src/cli/core_commands.ts +1532 -0
- package/src/cli/shared.ts +1935 -0
- package/src/cli/spine_commands.ts +1141 -0
- package/src/core/additive.ts +458 -0
- package/src/core/constraints.ts +38 -5
- package/src/core/constraints_physics.ts +42 -9
- package/src/core/constraints_slider.ts +136 -6
- package/src/core/index.ts +27 -3
- package/src/core/raw.ts +77 -16
- package/src/core/skins.ts +28 -3
- package/src/core/walk.ts +175 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformmeasure.ts +20 -48
- package/src/deformreport.ts +2 -2
- package/src/framing.ts +1 -1
- package/src/nonfinite.ts +54 -0
- package/src/pose.ts +1 -1
- package/src/preview.ts +1 -1
- package/src/region_joins.ts +54 -0
- package/src/render.ts +243 -2808
- package/src/render_core.ts +19 -31
- package/src/render_shared.ts +2887 -0
- package/src/slots.ts +1 -1
- package/src/spine_side.ts +123 -0
- package/src/validate.ts +443 -548
- package/src/assertions/facts/skin_meshes.ts +0 -49
- package/src/assertions/model/skin_meshes.ts +0 -90
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The mesh attachments, as
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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`,
|
|
10
|
-
* the document's order, which is the order `emitAnimation` writes
|
|
11
|
-
* map in
|
|
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
|
+
}
|