rig-c 0.0.0-stage → 2.20.4
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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `ConstraintFacts` (issue #1025, step 4c of
|
|
3
|
+
* #380): the document's constraints in update order, the constraint
|
|
4
|
+
* timelines of its animations in the order the runtime builds them, and the
|
|
5
|
+
* slots its skins give a path.
|
|
6
|
+
*
|
|
7
|
+
* Every value is the one the runtime holds after its parse, through a
|
|
8
|
+
* function rigc already runs:
|
|
9
|
+
*
|
|
10
|
+
* - **A constraint's fields** are the core's records (`readModel` →
|
|
11
|
+
* `readConstraintRecord`, `readPathRecord`, `readPhysicsRecord`,
|
|
12
|
+
* `readSliderRecord`), which read a field the record leaves out at the
|
|
13
|
+
* parser's value; a physics record's `massInverse` and `step` are the
|
|
14
|
+
* reader's `1 / mass` and `1 / fps`. A transform constraint reads a mix
|
|
15
|
+
* exactly where one of its `to` entries names that property — the core's
|
|
16
|
+
* `properties`, by `TRANSFORM_MIXES`.
|
|
17
|
+
* - **A slider's animation** is the core's reading of it: its duration is
|
|
18
|
+
* `CoreAnimationTimelines.duration` (the last key time, float32 — the
|
|
19
|
+
* runtime's), and the timelines it carries are the bone and slot timelines
|
|
20
|
+
* plus the count of every later group the reader counts.
|
|
21
|
+
* - **The timelines**: the animations in the file's order
|
|
22
|
+
* (`fileAnimationOrder`, issue #1034: a `rigc-compiled/3` document's
|
|
23
|
+
* `editorOrder`, else the emitter's own rule over its names); in each the
|
|
24
|
+
* groups in the runtime's order (`../facts/constraints.ts`' ⭐), each
|
|
25
|
+
* group's entries in the order the file keys them — the document's, an
|
|
26
|
+
* integer-like constraint name first (`keyedOrder`) — and each entry's
|
|
27
|
+
* timelines in the document's order, which the emitter writes unchanged. A key's time and value are the core's (float32), and a
|
|
28
|
+
* Bézier segment's samples `bezierPolyline` over the numbers the document
|
|
29
|
+
* states — the runtime's curve, as the core reproduces it.
|
|
30
|
+
* - **The reach of a physics timeline naming none** is asked of the core's
|
|
31
|
+
* own `posedPhysics` rather than restated: the timeline is posed once with
|
|
32
|
+
* a key the setup cannot hold (NaN), and the constraints it reached are the
|
|
33
|
+
* ones whose pose field took it; a `reset` timeline's reach is the set
|
|
34
|
+
* `posedPhysics` returns.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ One value is not the document's: `runtimeClass`, the runtime's class
|
|
37
|
+
* name for a kind, which A42's sentence names `update` on. The document has no
|
|
38
|
+
* field for it and rigc has no function returning it, so it is the table
|
|
39
|
+
* below; the selftest compares it with the class spine-core loaded on every
|
|
40
|
+
* call that reaches a constraint (`VF10`).
|
|
41
|
+
*
|
|
42
|
+
* Links nothing from the runtime.
|
|
43
|
+
*/
|
|
44
|
+
import { fileAnimationOrder, keyedOrder, type OrderedDocument } from '../../compile.ts';
|
|
45
|
+
import { bezierPolyline, type CoreAnimationTimelines, type CoreKey } from '../../core/animation.ts';
|
|
46
|
+
import { TRANSFORM_MIXES, TRANSFORM_PROPERTIES, type CoreConstraintRecord } from '../../core/constraints.ts';
|
|
47
|
+
import { EVERY_GLOBAL_PHYSICS, posedPhysics, type CorePhysicsRecord, type PhysicsTimelineKind } from '../../core/constraints_physics.ts';
|
|
48
|
+
import type { CompiledDocument } from '../../core/index.ts';
|
|
49
|
+
import { physicsRuleFor } from '../../timelines.ts';
|
|
50
|
+
import type { ConstraintEntry, ConstraintFacts, ConstraintKind, ConstraintTimeline } from '../facts/constraints.ts';
|
|
51
|
+
import { isObj, type Json } from '../values.ts';
|
|
52
|
+
import { fileOrderedEntries } from './skin_entries.ts';
|
|
53
|
+
import type { ReadDocument } from './parse.ts';
|
|
54
|
+
|
|
55
|
+
/** The runtime's class for each kind — the one value here that is not the document's (the header's ⚠️). */
|
|
56
|
+
export const RUNTIME_CONSTRAINT_CLASS: Readonly<Record<ConstraintKind, string>> = {
|
|
57
|
+
ik: 'IkConstraint',
|
|
58
|
+
transform: 'TransformConstraint',
|
|
59
|
+
path: 'PathConstraint',
|
|
60
|
+
physics: 'PhysicsConstraint',
|
|
61
|
+
slider: 'Slider',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** How many timelines the runtime builds for an animation the core read: every bone and slot timeline, and every later group's count. */
|
|
65
|
+
export function timelineCount(timelines: CoreAnimationTimelines): number {
|
|
66
|
+
let n = 0;
|
|
67
|
+
for (const target of [...timelines.bones, ...timelines.slots]) n += target.timelines.length;
|
|
68
|
+
for (const [, count] of timelines.later) n += count;
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Every value one channel of `keys` poses: each key's own, then the samples of the Bézier segment it opens. */
|
|
73
|
+
export function channelValuesOf(keys: readonly CoreKey[], channel: number): number[] {
|
|
74
|
+
const out: number[] = [];
|
|
75
|
+
keys.forEach((a, i) => {
|
|
76
|
+
out.push(a.values[channel]);
|
|
77
|
+
const b = keys[i + 1];
|
|
78
|
+
if (b === undefined || !Array.isArray(a.curve)) return;
|
|
79
|
+
const c = a.curve.slice(channel * 4, channel * 4 + 4);
|
|
80
|
+
const inner = bezierPolyline(a.stated.time, a.stated.values[channel], c[0], c[1], c[2], c[3], b.stated.time, b.stated.values[channel]);
|
|
81
|
+
for (let p = 1; p < inner.length; p += 2) out.push(inner[p]);
|
|
82
|
+
});
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The physics constraints a timeline naming none reaches, asked of `posedPhysics` (the header). */
|
|
87
|
+
export function unnamedReach(records: readonly CorePhysicsRecord[], kind: PhysicsTimelineKind): Set<string> {
|
|
88
|
+
if (kind === 'reset') {
|
|
89
|
+
const keys: CoreKey[] = [{ time: 0, values: [], stated: { time: 0, values: [] }, curve: 'stepped' }];
|
|
90
|
+
return posedPhysics(records, [{ name: EVERY_GLOBAL_PHYSICS, kind, keys }], 0, -1, () => true).reset;
|
|
91
|
+
}
|
|
92
|
+
const keys: CoreKey[] = [{ time: 0, values: [Number.NaN], stated: { time: 0, values: [Number.NaN] }, curve: 'linear' }];
|
|
93
|
+
const posed = posedPhysics(records, [{ name: EVERY_GLOBAL_PHYSICS, kind, keys }], 0, -1, () => true).records;
|
|
94
|
+
const field = kind === 'mass' ? 'massInverse' : kind;
|
|
95
|
+
return new Set([...posed.values()].filter((r) => Number.isNaN(r[field])).map((r) => r.name));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function entryOf(doc: CompiledDocument, kind: ConstraintKind, name: string, record: CoreConstraintRecord): ConstraintEntry {
|
|
99
|
+
const base = { kind, name, runtimeClass: RUNTIME_CONSTRAINT_CLASS[kind] };
|
|
100
|
+
switch (record.kind) {
|
|
101
|
+
case 'physics':
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
physics: {
|
|
105
|
+
bone: record.bone,
|
|
106
|
+
// The length the core's step is handed (`bone(state, c.bone).length ?? 0` in `src/core/constraints.ts`): the parser's 0 where the document leaves it out.
|
|
107
|
+
boneLength: doc.bones.find((b) => b.name === record.bone)?.length ?? 0,
|
|
108
|
+
components: { x: record.x, y: record.y, rotate: record.rotate, scaleX: record.scaleX, shearX: record.shearX },
|
|
109
|
+
setup: { mix: record.mix, massInverse: record.massInverse, strength: record.strength, damping: record.damping },
|
|
110
|
+
step: record.step,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
case 'path':
|
|
114
|
+
return { ...base, path: { bones: record.bones, slot: record.slot, setup: { mixRotate: record.mixRotate, mixX: record.mixX, mixY: record.mixY } } };
|
|
115
|
+
case 'slider': {
|
|
116
|
+
const anim = doc.animations.find((a) => a.name === record.animation);
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
slider: {
|
|
120
|
+
animation: anim === undefined ? null : { name: anim.name, timelines: timelineCount(anim.timelines), duration: anim.timelines.duration },
|
|
121
|
+
bone: record.bone,
|
|
122
|
+
loop: record.loop,
|
|
123
|
+
scale: record.scale,
|
|
124
|
+
mix: record.mix,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
case 'ik': {
|
|
129
|
+
// The second bone's ancestors, parent first (issue #1205). The core's reader refuses a pair that is not a parent and its child, so on this side the first entry is always the first bone.
|
|
130
|
+
const secondAncestors: string[] = [];
|
|
131
|
+
if (record.bones.length === 2) {
|
|
132
|
+
for (let at = doc.bones.find((b) => b.name === record.bones[1])?.parent; at !== undefined; at = doc.bones.find((b) => b.name === at)?.parent) secondAncestors.push(at);
|
|
133
|
+
}
|
|
134
|
+
return { ...base, ik: { bones: record.bones, target: record.target, mix: record.mix, secondAncestors } };
|
|
135
|
+
}
|
|
136
|
+
case 'transform':
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
transform: {
|
|
140
|
+
bones: record.bones,
|
|
141
|
+
mixes: TRANSFORM_PROPERTIES.map((p) => (record.properties.some((from) => from.to.some((to) => to.property === p)) ? { field: TRANSFORM_MIXES[p], setup: record.mixes[p] } : null)),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One constraint timeline as the model side walks it: where it is, what it keys, and the core's keys for it. */
|
|
148
|
+
export interface ModelConstraintTimelineSource {
|
|
149
|
+
animation: string;
|
|
150
|
+
kind: ConstraintKind;
|
|
151
|
+
word: string;
|
|
152
|
+
/** The constraint it names, or `EVERY_GLOBAL_PHYSICS`. */
|
|
153
|
+
name: string;
|
|
154
|
+
keys: readonly CoreKey[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** How the animations are put in the file's order: `fileAnimationOrder`. A parameter only so the selftest can show what another order would print (`VF11`). */
|
|
158
|
+
export type AnimationFileOrder = (doc: OrderedDocument) => readonly string[];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* `entries` in the order the Spine file keys them by name (issue #1034): each
|
|
162
|
+
* constraint group of an animation is an object the emitter fills in the
|
|
163
|
+
* model's order, so the names are grouped as they first appear and put
|
|
164
|
+
* through `keyedOrder` — an integer-like name first — and every entry of one
|
|
165
|
+
* name keeps its place among them.
|
|
166
|
+
*/
|
|
167
|
+
function inKeyedOrder<T extends { name: string }>(entries: readonly T[]): T[] {
|
|
168
|
+
const names = entries.map((e) => e.name);
|
|
169
|
+
return keyedOrder(names.filter((n, i) => names.indexOf(n) === i)).flatMap((name) => entries.filter((e) => e.name === name));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Every constraint timeline of the document, in the order the runtime builds them (the header) — the walk `modelConstraintFacts` reads, and the selftest's measurement of each derivation. */
|
|
173
|
+
export function modelConstraintTimelineSources(read: ReadDocument, animations: AnimationFileOrder = fileAnimationOrder): ModelConstraintTimelineSource[] {
|
|
174
|
+
const { doc } = read;
|
|
175
|
+
const jsonAnimations = Array.isArray(read.json.animations) ? read.json.animations.filter(isObj) : [];
|
|
176
|
+
const out: ModelConstraintTimelineSource[] = [];
|
|
177
|
+
for (const name of animations(doc)) {
|
|
178
|
+
const anim = doc.animations.find((a) => a.name === name);
|
|
179
|
+
if (anim === undefined) continue;
|
|
180
|
+
const c = anim.constraints;
|
|
181
|
+
for (const tl of inKeyedOrder(c.ik)) out.push({ animation: name, kind: 'ik', word: 'ik', name: tl.name, keys: tl.keys });
|
|
182
|
+
for (const tl of inKeyedOrder(c.transform)) out.push({ animation: name, kind: 'transform', word: 'transform', name: tl.name, keys: tl.keys });
|
|
183
|
+
for (const entry of inKeyedOrder(c.path)) {
|
|
184
|
+
for (const word of Object.keys(entry)) {
|
|
185
|
+
if (word === 'position' || word === 'spacing' || word === 'mix') out.push({ animation: name, kind: 'path', word, name: entry.name, keys: entry[word] ?? [] });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const tl of inKeyedOrder(c.physicsKeyed ?? [])) out.push({ animation: name, kind: 'physics', word: tl.kind, name: tl.name, keys: tl.keys });
|
|
189
|
+
// A slider entry's timelines in the order the document lists them, which `readSliderTimelines` keeps by name only.
|
|
190
|
+
const jsonSliders = ((): Json[] => {
|
|
191
|
+
const json = jsonAnimations.find((a) => a.name === name)?.constraints;
|
|
192
|
+
return isObj(json) && Array.isArray(json.slider) ? json.slider.filter(isObj) : [];
|
|
193
|
+
})();
|
|
194
|
+
// Paired with the document's own entry by index before the walk is put in the file's order.
|
|
195
|
+
const sliders = c.slider.map((entry, i) => ({ name: entry.name, entry, listed: Array.isArray(jsonSliders[i]?.timelines) ? (jsonSliders[i].timelines as unknown[]).filter(isObj).map((t) => t.name) : [] }));
|
|
196
|
+
for (const { entry, listed } of inKeyedOrder(sliders)) {
|
|
197
|
+
for (const word of listed) {
|
|
198
|
+
if (word === 'time' || word === 'mix') out.push({ animation: name, kind: 'slider', word, name: entry.name, keys: entry[word] ?? [] });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function modelConstraintFacts(read: ReadDocument, animations: AnimationFileOrder = fileAnimationOrder): ConstraintFacts {
|
|
206
|
+
const { doc } = read;
|
|
207
|
+
const constraints: ConstraintEntry[] = doc.constraints.map((c) => {
|
|
208
|
+
if (c.record === undefined) throw new Error(`internal: ${c.kind} constraint "${c.name}" is one the reader returned without its record`);
|
|
209
|
+
return entryOf(doc, c.kind, c.name, c.record);
|
|
210
|
+
});
|
|
211
|
+
const indexOf = (kind: ConstraintKind, name: string): number => doc.constraints.findIndex((c) => c.kind === kind && c.name === name);
|
|
212
|
+
const physicsRecords = doc.constraints.flatMap((c) => (c.record?.kind === 'physics' ? [c.record] : []));
|
|
213
|
+
const timelines: ConstraintTimeline[] = modelConstraintTimelineSources(read, animations).map(({ animation, kind, word, name, keys }) => {
|
|
214
|
+
const constraint = name === EVERY_GLOBAL_PHYSICS && kind === 'physics' ? -1 : indexOf(kind, name);
|
|
215
|
+
const reach = constraint >= 0 ? [constraint] : [...unnamedReach(physicsRecords, word as PhysicsTimelineKind)].map((n) => indexOf('physics', n)).sort((a, b) => a - b);
|
|
216
|
+
const rule = kind === 'physics' ? physicsRuleFor(word) : undefined;
|
|
217
|
+
return {
|
|
218
|
+
animation,
|
|
219
|
+
kind,
|
|
220
|
+
word,
|
|
221
|
+
constraint,
|
|
222
|
+
reach,
|
|
223
|
+
frames: word === 'reset' ? [] : keys.map((k) => ({ time: k.time, value: k.values[0] })),
|
|
224
|
+
channelValues: (channel: number) => (word === 'reset' ? [] : channelValuesOf(keys, channel)),
|
|
225
|
+
...(rule === undefined ? {} : { posed: (value: number) => rule.toPose(value) }),
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
const pathSlots: string[] = [];
|
|
229
|
+
for (const entry of fileOrderedEntries(read)) {
|
|
230
|
+
if (entry.record.kind === 'path' && !pathSlots.includes(entry.slot)) pathSlots.push(entry.slot);
|
|
231
|
+
}
|
|
232
|
+
return { animations: doc.animations.length, constraints, timelines, pathSlots };
|
|
233
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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 num: Check = (v, at, p) => (typeof v === 'number' && Number.isFinite(v) ? undefined : say(p, at, v, 'a finite number'));
|
|
49
|
+
const nullable = (inner: Check): Check => (v, at, p) => (v === null ? undefined : inner(v, at, p));
|
|
50
|
+
const list = (inner: Check, length?: number): Check => (v, at, p) => {
|
|
51
|
+
if (!Array.isArray(v) || (length !== undefined && v.length !== length)) return say(p, at, v, length === undefined ? 'a list' : `a list of ${length}`);
|
|
52
|
+
v.forEach((x, i) => inner(x, `${at}[${i}]`, p));
|
|
53
|
+
};
|
|
54
|
+
const record = (inner: Check): Check => (v, at, p) => {
|
|
55
|
+
if (!isObj(v)) return say(p, at, v, 'an object');
|
|
56
|
+
for (const [k, x] of Object.entries(v)) inner(x, `${at}.${k}`, p);
|
|
57
|
+
};
|
|
58
|
+
const oneOf = (words: readonly string[]): Check => (v, at, p) => (typeof v === 'string' && words.includes(v) ? undefined : say(p, at, v, `one of ${words.join(', ')}`));
|
|
59
|
+
const shape = (fields: Readonly<Record<string, Check>>): Check => (v, at, p) => {
|
|
60
|
+
if (!isObj(v)) return say(p, at, v, 'an object');
|
|
61
|
+
for (const [k, check] of Object.entries(fields)) check(v[k], `${at}.${k}`, p);
|
|
62
|
+
for (const k of Object.keys(v)) if (!(k in fields)) p.push(`${at}.${k} is not a field the rig section has`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The rig section's fields, as `RigInfo` declares them and the writer writes every one — each required, none defaulted. */
|
|
66
|
+
const RIG_FIELDS: Readonly<Record<keyof RigInfo, Check>> = {
|
|
67
|
+
archetype: str,
|
|
68
|
+
axisBone: nullable(str),
|
|
69
|
+
axisSubtree: list(str),
|
|
70
|
+
detached: list(list(str, 2)),
|
|
71
|
+
slotOrder: nullable(list(str)),
|
|
72
|
+
meshKinds: record(oneOf(MESH_KINDS)),
|
|
73
|
+
meshDeclaredBones: record(list(str)),
|
|
74
|
+
meshSoftBones: record(str),
|
|
75
|
+
deformMayFold: list(str),
|
|
76
|
+
consumerDrivenMix: list(shape({ type: oneOf(['ik', 'transform']), constraint: str, why: str })),
|
|
77
|
+
idleDrivesMeshes: nullable(str),
|
|
78
|
+
basePlates: list(str),
|
|
79
|
+
meshSlotBudget: nullable(num),
|
|
80
|
+
meshTriangleBudget: nullable(num),
|
|
81
|
+
contactDepth: nullable(num),
|
|
82
|
+
capContainmentCeiling: nullable(num),
|
|
83
|
+
massBone: nullable(str),
|
|
84
|
+
inwardUnit: nullable(list(num, 2)),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Every problem with the document's `rig` section, each by its path — empty when it reads as rig info. */
|
|
88
|
+
export function rigSectionProblems(json: Readonly<Record<string, unknown>>): string[] {
|
|
89
|
+
const problems: string[] = [];
|
|
90
|
+
shape(RIG_FIELDS)(json.rig, 'rig', problems);
|
|
91
|
+
return problems;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The rig info the document declares — its `rig` section, read field by field; a section that does not read is refused by its paths. */
|
|
95
|
+
export function modelRigInfo(read: ReadDocument): RigInfo {
|
|
96
|
+
const problems = rigSectionProblems(read.json);
|
|
97
|
+
if (problems.length > 0) throw new DeclaredInputError(`the document's rig section does not read as rig info: ${problems.length} problem(s): ${problems.join('; ')}`);
|
|
98
|
+
return read.json.rig as RigInfo;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Each animation's declared duration, as the document states it and the core
|
|
103
|
+
* reads it (`CoreAnimationTimelines.declared`), keyed in the document's order —
|
|
104
|
+
* the motion spec's, the order `CompileResult.declaredDurations` is filled in.
|
|
105
|
+
*/
|
|
106
|
+
export function modelDeclaredDurations(read: ReadDocument): Record<string, number> {
|
|
107
|
+
const out: Record<string, number> = {};
|
|
108
|
+
for (const anim of read.doc.animations) out[anim.name] = anim.timelines.declared;
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** What a caller may not give beside a document, because every document states it. */
|
|
113
|
+
export type DeclaredValue = 'rig' | 'declaredDurations';
|
|
114
|
+
|
|
115
|
+
/** The sentence a value given beside the document is refused with. */
|
|
116
|
+
export function declaredBesideRefusal(spec: string, what: DeclaredValue): string {
|
|
117
|
+
const given = what === 'rig' ? 'the rig info' : 'the declared durations';
|
|
118
|
+
const stated = what === 'rig' ? 'states it itself in its rig section' : "states each animation's duration itself";
|
|
119
|
+
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`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A caller's value beside a document that states it, refused by name. */
|
|
123
|
+
export function refuseDeclaredBeside(read: ReadDocument, input: { rig?: unknown; declaredDurations?: unknown }, what: DeclaredValue): void {
|
|
124
|
+
if (input[what] !== undefined) throw new Error(declaredBesideRefusal(read.doc.spec, what));
|
|
125
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `DeformSurveyFacts` (issue #1025, cut 4c-3 of
|
|
3
|
+
* step 4c of #380): the survey off the model document alone —
|
|
4
|
+
* `surveyOfModel` (`src/deformsurvey.ts`), the document's structure read by
|
|
5
|
+
* `modelStructure` (`src/deformstructure.ts`) and every pose taken by the
|
|
6
|
+
* core's poser. It is the survey `explain` takes of a build that carries a
|
|
7
|
+
* model document (`surveyOfBuild`'s model path) and the one
|
|
8
|
+
* `tools/survey_hashes.ts` holds byte for byte to spine-core's on every corpus
|
|
9
|
+
* row; called, not restated.
|
|
10
|
+
*
|
|
11
|
+
* A pose the core refuses — a construct it leaves out, a value it computes as
|
|
12
|
+
* no number — is a `CoreInputError` naming why, and it reaches A39 as the
|
|
13
|
+
* refusal: the line the harness prints for a body that threw, the core's
|
|
14
|
+
* sentence in it. A survey that is not the runtime's is never handed on.
|
|
15
|
+
*
|
|
16
|
+
* Links nothing from the runtime.
|
|
17
|
+
*/
|
|
18
|
+
import { surveyOfModel } from '../../deformsurvey.ts';
|
|
19
|
+
import type { DeformSurveyFacts } from '../facts/deform_survey.ts';
|
|
20
|
+
import type { ReadDocument } from './parse.ts';
|
|
21
|
+
|
|
22
|
+
export function modelDeformSurvey(read: ReadDocument): DeformSurveyFacts {
|
|
23
|
+
return { survey: (exempt) => surveyOfModel(read.doc, exempt) };
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `EventKeyFacts` (issue #1025, cut 4c-4 of #380):
|
|
3
|
+
* every event key of the document, with the declaration it fires.
|
|
4
|
+
*
|
|
5
|
+
* - **The order is the file's.** The animations by `editorAnimationOrder`
|
|
6
|
+
* (`src/compile.ts`, the function the emitter is handed), and an animation's
|
|
7
|
+
* keys in the document's order, which is the order `emitAnimation` writes
|
|
8
|
+
* the model's list in; an animation with no event key writes no `events`
|
|
9
|
+
* group, so it carries no event timeline here either.
|
|
10
|
+
* - **The moved clause's inputs.** Which of `volume` and `balance` the key
|
|
11
|
+
* states — the document's key carries the fields the file's does, and the
|
|
12
|
+
* parser-default pass leaves an event key's audio fields alone (its row is
|
|
13
|
+
* `time` only) — and whether the declaration names an audio path, the
|
|
14
|
+
* `audio` the document's `events` section states for that name.
|
|
15
|
+
* - **No `kept` finding, ever.** The four clauses that stay with the round
|
|
16
|
+
* trip are states `readModel` refuses (`readEventKeys`), so on a document
|
|
17
|
+
* this side has read none of them can hold.
|
|
18
|
+
*
|
|
19
|
+
* Links nothing from the runtime.
|
|
20
|
+
*/
|
|
21
|
+
import { editorAnimationOrder } from '../../compile.ts';
|
|
22
|
+
import type { EventKeyEntry, EventKeyFacts } from '../facts/event_keys.ts';
|
|
23
|
+
import { isObj, type Json } from '../values.ts';
|
|
24
|
+
import type { ReadDocument } from './parse.ts';
|
|
25
|
+
|
|
26
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
27
|
+
|
|
28
|
+
export function modelEventKeys(read: ReadDocument): EventKeyFacts {
|
|
29
|
+
const audio = new Map<string, boolean>();
|
|
30
|
+
for (const def of list(read.json.events)) if (typeof def.name === 'string') audio.set(def.name, typeof def.audio === 'string');
|
|
31
|
+
const byName = new Map<string, Json>();
|
|
32
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
33
|
+
const keys: EventKeyEntry[] = [];
|
|
34
|
+
let timelines = 0;
|
|
35
|
+
for (const animation of editorAnimationOrder([...byName.keys()])) {
|
|
36
|
+
const events = list((byName.get(animation) as Json).events);
|
|
37
|
+
if (events.length === 0) continue;
|
|
38
|
+
timelines++;
|
|
39
|
+
events.forEach((key, index) => {
|
|
40
|
+
const name = String(key.name);
|
|
41
|
+
keys.push({ animation, index, kept: [], stopped: false, name, sets: { volume: key.volume !== undefined, balance: key.balance !== undefined }, audio: audio.get(name) ?? false });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { parsed: true, animations: true, timelines, keys };
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the model side is given beside the document (issue #1025, step 4c of
|
|
3
|
+
* #380): the two values the census found the document does not hold, which a
|
|
4
|
+
* moved assertion reads — the stage (A14, A19) and each atlas page's `pma`
|
|
5
|
+
* (A06). Both entered the document with issue #1026 (`rigc-compiled/3`):
|
|
6
|
+
* a `/3` document is read for them and this input is for a `/2` or `/1`
|
|
7
|
+
* document alone — given beside a `/3` document it is refused by name
|
|
8
|
+
* (`refuseGivenBeside`) rather than ignored, because a value the caller states
|
|
9
|
+
* beside the document's own is a second source for one fact, and a silent
|
|
10
|
+
* winner between two sources is how a verdict comes to pass for the wrong
|
|
11
|
+
* reason.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ **The model side never reads them off a Spine file itself.** A verdict that
|
|
14
|
+
* read `skeleton.json` would be a second reading of the encoding, which is the
|
|
15
|
+
* round trip's subject; so whoever calls the model side states them, from what
|
|
16
|
+
* it holds. The selftest's supplier check and `tools/verdict_gate.ts` hold the
|
|
17
|
+
* build they gate, and read both off it with one function
|
|
18
|
+
* (`modelGivenOfBuild` in that tool): the stage the skeleton header the build
|
|
19
|
+
* wrote states, and each page's `pma` as rigc's own atlas reader
|
|
20
|
+
* (`parseAtlasText`) reads the atlas the build wrote.
|
|
21
|
+
*
|
|
22
|
+
* Links nothing from the runtime.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ReadDocument } from './parse.ts';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A caller's `given` beside a document that states both values itself
|
|
29
|
+
* (`rigc-compiled/3`, issue #1026), refused by name: the caller is to give
|
|
30
|
+
* them for a `/2` or `/1` document only (`modelGivenOf` in
|
|
31
|
+
* `tools/verdict_gate.ts` gives them exactly then).
|
|
32
|
+
*/
|
|
33
|
+
export function refuseGivenBeside(read: ReadDocument, given: ModelGiven | undefined): void {
|
|
34
|
+
if (given !== undefined && read.doc.stated !== null) {
|
|
35
|
+
throw new Error(`the caller gave the stage and the pages' pma beside a ${read.doc.spec} document, which states both itself (issue #1026); a second source for one fact is refused rather than one of them silently read`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The stage and the pages' `pma`, as the caller states them. */
|
|
40
|
+
export interface ModelGiven {
|
|
41
|
+
/** The stage the skeleton states — each extent where it states one — or `null` where it states none. */
|
|
42
|
+
readonly stage: { readonly width?: number; readonly height?: number } | null;
|
|
43
|
+
/** Each page's `pma`, in the atlas's page order: one value per page of the document's `pages`. */
|
|
44
|
+
readonly pma: readonly boolean[];
|
|
45
|
+
}
|