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
package/src/rig.ts
ADDED
|
@@ -0,0 +1,2941 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rig spec — `"spec": "rigc-rig/1"`. The skeleton as **data**.
|
|
3
|
+
*
|
|
4
|
+
* Until this file existed the bone tree and the slot table were code: three
|
|
5
|
+
* hard-coded formations in `src/archetype.ts`, a slot outside their tables a
|
|
6
|
+
* compile error, and therefore **no skeleton anybody else owns could be stated
|
|
7
|
+
* at all**. That was blocker B1 of [docs/LADDER.md](../docs/LADDER.md), and it
|
|
8
|
+
* gated every rung of the benchmark ladder.
|
|
9
|
+
*
|
|
10
|
+
* ## The vocabulary is Spine's
|
|
11
|
+
*
|
|
12
|
+
* ⭐ Wherever rigc has no better abstraction, this format uses **Spine 4.3's own
|
|
13
|
+
* concept and its own field name, with Spine's own default**, so that an agent
|
|
14
|
+
* that has read Spine's documentation can author a rig here without learning a
|
|
15
|
+
* second vocabulary. `bones[]` is Spine's bone list; `slots[]` is Spine's slot
|
|
16
|
+
* list and its array order is the draw order; `skins` holds Spine's placeholder
|
|
17
|
+
* → attachment maps; `constraints[]` is 4.3's single typed constraint array.
|
|
18
|
+
* Field lists below cite `SkeletonJson.ts` line numbers, which
|
|
19
|
+
* [docs/SPEC_COVERAGE.md](../docs/SPEC_COVERAGE.md) part 1 enumerates in full.
|
|
20
|
+
*
|
|
21
|
+
* Everything rigc adds sits **on top** of that vocabulary and is namespaced so a
|
|
22
|
+
* reader can see where Spine stops:
|
|
23
|
+
*
|
|
24
|
+
* - `from` on a bone — take this bone's setup position from the cut manifest
|
|
25
|
+
* (an anchor, a part window, a mesh centre) instead of writing a literal that
|
|
26
|
+
* would drift away from the measured art.
|
|
27
|
+
* - `generator` on a mesh attachment — build the geometry with one of the
|
|
28
|
+
* builders in `src/mesh.ts` instead of authoring vertex arrays by hand.
|
|
29
|
+
* - `image` on an attachment — name a PNG and let rigc **measure** it, rather
|
|
30
|
+
* than restating a `width`/`height` that can silently disagree with the file
|
|
31
|
+
* (SPEC_COVERAGE part 1-6: a missing `width` loads as `NaN`, with no error).
|
|
32
|
+
* - `invariants` — the structural facts skeleton JSON cannot state about itself,
|
|
33
|
+
* which the validator's archetype assertions read. Nothing in the file says
|
|
34
|
+
* "this bone carries the cut's axis" or "this parentage is forbidden".
|
|
35
|
+
*
|
|
36
|
+
* ## What a field's PRESENCE means
|
|
37
|
+
*
|
|
38
|
+
* 🔑 **A field is emitted exactly when the spec declares it.** Not "when it
|
|
39
|
+
* differs from the default" — Spine's own exporter omits defaults, but rigc
|
|
40
|
+
* cannot, because a rig may need to say `x: 0` out loud (the overlay formation's
|
|
41
|
+
* handle bone does) and because deciding emission from the *value* makes the
|
|
42
|
+
* emitted file depend on arithmetic rather than on what the author wrote. Omit a
|
|
43
|
+
* field and Spine's default stands; write it and it is in the file. A bone whose
|
|
44
|
+
* position comes `from` the manifest counts as declaring `x` and `y`, because
|
|
45
|
+
* the manifest declared them.
|
|
46
|
+
*
|
|
47
|
+
* ## What this format does NOT own
|
|
48
|
+
*
|
|
49
|
+
* rigc joins three files and each owns a domain:
|
|
50
|
+
*
|
|
51
|
+
* - the **cut manifest** owns measured geometry — crop, part offsets and sizes,
|
|
52
|
+
* mask polygons, the state machine, anchors, the axis, the measured ceilings;
|
|
53
|
+
* - the **rig spec** (this file) owns skeleton structure — bones, slots, skins,
|
|
54
|
+
* constraints, and the invariants;
|
|
55
|
+
* - the **motion spec** owns time — named easings, groups, setup pose, the
|
|
56
|
+
* physics tuning table, and the animations.
|
|
57
|
+
*
|
|
58
|
+
* A cut compiled from all three declares its attachments in the manifest (see
|
|
59
|
+
* `slots` below for the join rule) and leaves `skins` empty. A foreign skeleton
|
|
60
|
+
* with no manifest at all declares them here.
|
|
61
|
+
*/
|
|
62
|
+
import { ikShapeFault } from './assertions/bodies/a47.ts';
|
|
63
|
+
import { CompileError, NotImplementedError } from './errors.ts';
|
|
64
|
+
import { dottedPath, refuseNumbersTheFileCannotCarry, refuseUnknownKeys, refuseValuesOfTheWrongType, refuseValuesOutsideTheirSet } from './keys.ts';
|
|
65
|
+
import type { ShapeVisit, SpecEnumTable, SpecValueType } from './keys.ts';
|
|
66
|
+
|
|
67
|
+
export { CompileError, NotImplementedError };
|
|
68
|
+
|
|
69
|
+
/** The only version this compiler reads. */
|
|
70
|
+
export const RIG_SPEC_VERSION = 'rigc-rig/1';
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// skeleton header — `root.skeleton` (SkeletonJson.ts:75-87)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The stage and the runtime hints, all optional.
|
|
78
|
+
*
|
|
79
|
+
* 🔁 The four box fields here are rigc's **stage** — the working area the art
|
|
80
|
+
* was painted in, the frame the coordinate transform and `A14`/`A19` read —
|
|
81
|
+
* and not the Spine header's box of the same names, which the format defines
|
|
82
|
+
* as the setup-pose bounding box. Since issue #907 `build` writes that box
|
|
83
|
+
* into the header, computed from the rig (`headerBoundsOf` in
|
|
84
|
+
* `src/compile.ts`), and states the stage in `skeleton.model.json`.
|
|
85
|
+
*
|
|
86
|
+
* `x`/`y` default to 0 and `width`/`height` fall back to the cut manifest's crop
|
|
87
|
+
* when there is one. With neither a manifest nor a declaration here the compile
|
|
88
|
+
* fails by name: `width`/`height` are what `A14_NO_FULL_FRAME_MESH` and
|
|
89
|
+
* `A19_OVERLAY_PNGS_HAVE_ALPHA` measure against, and a guessed stage is a gate
|
|
90
|
+
* that measures against a number nobody wrote down.
|
|
91
|
+
*
|
|
92
|
+
* ⭐ **`width: null, height: null` is the third state: this skeleton declares no
|
|
93
|
+
* stage** (issue #578). Omitting them is silence and stays a refusal by name;
|
|
94
|
+
* stating them `null` is a claim, and the emitted header then carries none of
|
|
95
|
+
* `x`/`y`/`width`/`height` — which is what an editor export of a skeleton whose
|
|
96
|
+
* bounds were never set looks like, and what a transcriber of one has to be able
|
|
97
|
+
* to write down. `null` is this spec's spelling for a stated absence everywhere
|
|
98
|
+
* else it has one (`RigSlot.attachment` = "show nothing", the cut manifest's
|
|
99
|
+
* `image` = "this cut does not carry the part"), so it is the spelling here too
|
|
100
|
+
* and no new key is introduced: the pair already exists, and only a third value
|
|
101
|
+
* of it is new.
|
|
102
|
+
*
|
|
103
|
+
* Two shapes are refused rather than interpreted, both in `parseRigSpec`:
|
|
104
|
+
* stating one of the pair `null` and the other a number (a stage with one
|
|
105
|
+
* extent is not a stage, and guessing which half was meant is inventing), and
|
|
106
|
+
* stating `x` or `y` alongside the absence (an origin for a box that is not
|
|
107
|
+
* there). ⚠️ A stated absence also beats a cut manifest's `crop`, for the reason
|
|
108
|
+
* a stated `width` already does: the rig spec is where a claim about the
|
|
109
|
+
* skeleton is made, and the manifest is a record of what the art measured.
|
|
110
|
+
*
|
|
111
|
+
* `spine` is not here: rigc emits its own version label and `A16` re-checks it.
|
|
112
|
+
* `hash` is not here either — it is the editor's change-detection token and
|
|
113
|
+
* inventing one would be claiming an export this file did not come from.
|
|
114
|
+
*/
|
|
115
|
+
export interface RigSkeletonHeader {
|
|
116
|
+
x?: number;
|
|
117
|
+
y?: number;
|
|
118
|
+
/** A number, or `null` with `height` for "this skeleton declares no stage". */
|
|
119
|
+
width?: number | null;
|
|
120
|
+
/** A number, or `null` with `width` for "this skeleton declares no stage". */
|
|
121
|
+
height?: number | null;
|
|
122
|
+
/** Nonessential; `SkeletonData.fps` stays 30 when absent. */
|
|
123
|
+
fps?: number;
|
|
124
|
+
/** 4.2+; the runtime's physics/scale reference. Parser default 100. */
|
|
125
|
+
referenceScale?: number;
|
|
126
|
+
/**
|
|
127
|
+
* Nonessential: where the editor's import looks for the part images, as a path
|
|
128
|
+
* from the skeleton file. Declared here it is carried through verbatim; absent,
|
|
129
|
+
* rigc writes the path from `--out` to the one directory the spec names every
|
|
130
|
+
* part PNG in — `--out` itself, spelled `../<its basename>/`, under
|
|
131
|
+
* `--copy-images`, which moved them beside the skeleton and overrides a
|
|
132
|
+
* declaration for the same reason it rewrites the atlas's page names (the
|
|
133
|
+
* editor drops a literal `./` on import; a named directory it keeps). Parts
|
|
134
|
+
* spread over several directories have no single true path, so nothing is
|
|
135
|
+
* written (issue #370).
|
|
136
|
+
*/
|
|
137
|
+
images?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Nonessential: where the editor looks for the skeleton's audio files, as a
|
|
140
|
+
* path from the skeleton file — or `null`, which is what an editor export
|
|
141
|
+
* writes when no audio folder is set. Carried verbatim, `null` included, and
|
|
142
|
+
* written only when stated: rigc has no audio to point at, so this is a value
|
|
143
|
+
* a spec states or does not (issue #716 — every one of the twelve editor
|
|
144
|
+
* exports under `examples/` writes `"audio": null`, and `ingest` carries it).
|
|
145
|
+
*/
|
|
146
|
+
audio?: string | null;
|
|
147
|
+
/**
|
|
148
|
+
* Carry the stage in the Spine files as a bounding-box attachment — opt-in,
|
|
149
|
+
* and absent changes no emitted byte (issue #1168). See `RigStageBox`.
|
|
150
|
+
*/
|
|
151
|
+
stageBox?: RigStageBox;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Where the stage travels in the shipped Spine files: one bounding-box
|
|
156
|
+
* attachment `build` writes from the stage, never from numbers typed here
|
|
157
|
+
* (issue #1168).
|
|
158
|
+
*
|
|
159
|
+
* ⭐ **Why it is needed.** Since issue #907 the header's `x`, `y`, `width`,
|
|
160
|
+
* `height` are the setup-pose bounding box — what the format says they are —
|
|
161
|
+
* and the stage is stated in `skeleton.model.json`. A consumer that ships
|
|
162
|
+
* `skeleton.json`, the atlas and its pages and nothing else has no stage to
|
|
163
|
+
* fit the rig to. A key the format does not define would be read by no
|
|
164
|
+
* runtime and dropped by the editor without a word; a bounding box is returned
|
|
165
|
+
* by every runtime by slot and name, in JSON and in binary.
|
|
166
|
+
*
|
|
167
|
+
* The box's four vertices are the stage's corners in Spine world — `(x, y)`,
|
|
168
|
+
* `(x + width, y)`, `(x + width, y + height)`, `(x, y + height)`, the
|
|
169
|
+
* bottom-left first and counter-clockwise, y up — written unweighted in the
|
|
170
|
+
* slot bone's space. Its numbers come from the stage alone, so they cannot
|
|
171
|
+
* drift from the frame the coordinate transform reads.
|
|
172
|
+
*
|
|
173
|
+
* 🔒 **The rules, each refused by name in `compile`:**
|
|
174
|
+
*
|
|
175
|
+
* - `slot` is a slot `slots` declares, and nothing else fills it — no skin
|
|
176
|
+
* entry and no manifest part. The box is the slot's one attachment, filed
|
|
177
|
+
* in the `default` skin under `attachment`. Its setup pose is the slot's
|
|
178
|
+
* own, stated the way any slot's is (the rig slot's `attachment`, or the
|
|
179
|
+
* motion spec's `setup`) — name the box there for a slot that shows it at
|
|
180
|
+
* setup, which is what a reader of the slot's current attachment sees.
|
|
181
|
+
* - The slot hangs on the **root**, and the root states no setup
|
|
182
|
+
* transform — no `x`, `y`, `rotation`, scale or shear. The vertices are
|
|
183
|
+
* then the stage's numbers themselves, which a reader of the attachment
|
|
184
|
+
* data gets without posing anything. A bone below the root is refused
|
|
185
|
+
* even when it states nothing: the runtime spells an unrotated frame's
|
|
186
|
+
* `b` as cos 90° at its pi (−2.3e-8), once per level, so the posed box
|
|
187
|
+
* lands on the stage through the root's frame — the residue the header's
|
|
188
|
+
* own box already carries — and drifts further at every level below it.
|
|
189
|
+
* A constraint that moves the root at setup is not seen by `compile`,
|
|
190
|
+
* which poses none; `A50_STAGE_BOX_IS_THE_STAGE` names it.
|
|
191
|
+
* - The rig declares a stage. Asking for its box with `"width": null,
|
|
192
|
+
* "height": null` asks for a box around nothing.
|
|
193
|
+
*
|
|
194
|
+
* ⚠️ The claim is the setup pose's. An animation that keys the root moves the
|
|
195
|
+
* box with it, as it moves anything else on the root; the stage is the
|
|
196
|
+
* attachment's vertices, read at setup or straight off the data.
|
|
197
|
+
*/
|
|
198
|
+
export interface RigStageBox {
|
|
199
|
+
/** The slot the box goes in, declared in `slots`; its `bone` is the box's bone. */
|
|
200
|
+
slot: string;
|
|
201
|
+
/** The box's attachment name — its placeholder in the `default` skin and its `Attachment.name`. */
|
|
202
|
+
attachment: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Does this header state that the skeleton has no stage?
|
|
207
|
+
*
|
|
208
|
+
* One reading of the spelling, exported so that the compiler, the emitter and
|
|
209
|
+
* anything that grows a third opinion later read it the same way. `parseRigSpec`
|
|
210
|
+
* has already refused the half-stated shapes by the time this is asked, so the
|
|
211
|
+
* two `null`s travel together.
|
|
212
|
+
*/
|
|
213
|
+
export function declaresNoStage(header: RigSkeletonHeader | undefined): boolean {
|
|
214
|
+
return header !== undefined && header.width === null && header.height === null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// bones — `root.bones[]` (SkeletonJson.ts:90-118)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* `BoneData.ts:80`. Resolved by `Utils.enumValue`, which upper-cases the first
|
|
223
|
+
* letter, so `"noScale"` and `"NoScale"` both load; rigc accepts either and
|
|
224
|
+
* emits the lower-camel spelling the editor writes.
|
|
225
|
+
*
|
|
226
|
+
* ⚠️ 4.0/4.1 called this field `transform`. That name still *loads* in 4.3 and
|
|
227
|
+
* the inheritance silently falls back to Normal — assertion `A02`.
|
|
228
|
+
*/
|
|
229
|
+
export type RigBoneInherit = 'normal' | 'onlyTranslation' | 'noRotationOrReflection' | 'noScale' | 'noScaleOrReflection';
|
|
230
|
+
|
|
231
|
+
export const RIG_BONE_INHERIT: readonly RigBoneInherit[] = [
|
|
232
|
+
'normal',
|
|
233
|
+
'onlyTranslation',
|
|
234
|
+
'noRotationOrReflection',
|
|
235
|
+
'noScale',
|
|
236
|
+
'noScaleOrReflection',
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The mode a spelling of `inherit` resolves to, in the table's own spelling —
|
|
241
|
+
* or `undefined` for one the runtime cannot resolve.
|
|
242
|
+
*
|
|
243
|
+
* ⭐ **One rule for both places the format spells a mode**: a bone's setup
|
|
244
|
+
* `inherit` and an `inherit` timeline key are read by the same call,
|
|
245
|
+
* `Utils.enumValue(Inherit, name)`, which is `Inherit[name[0].toUpperCase() +
|
|
246
|
+
* name.slice(1)]` — the FIRST letter is folded and nothing else. So `noScale`
|
|
247
|
+
* and `NoScale` resolve and `NOSCALE` or `noscale` do not, and a spelling that
|
|
248
|
+
* misses loads as `undefined`: the setup pose holds no mode at all and a
|
|
249
|
+
* timeline frame holds NaN, and in both cases `updateWorldTransform`'s switch
|
|
250
|
+
* matches no case and leaves the world matrix where it was. Nothing throws.
|
|
251
|
+
*
|
|
252
|
+
* 🚨 The setup check was **case-insensitive** until issue #733, which is wider
|
|
253
|
+
* than the runtime's rule by exactly that silence: `"inherit": "NOSCALE"`
|
|
254
|
+
* compiled, gated green on all 45 assertions, and loaded `setupPose.inherit ===
|
|
255
|
+
* undefined`. Measured, not argued — and a key read through the same wide rule
|
|
256
|
+
* would have shipped the same spelling into a timeline.
|
|
257
|
+
*/
|
|
258
|
+
export function resolveBoneInherit(value: unknown): RigBoneInherit | undefined {
|
|
259
|
+
if (typeof value !== 'string' || value.length === 0) return undefined;
|
|
260
|
+
const folded = value[0].toLowerCase() + value.slice(1);
|
|
261
|
+
return RIG_BONE_INHERIT.find((mode) => mode === folded);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The value REQUIRED, as both refusals of an unresolvable mode print it: the
|
|
266
|
+
* five, and the one liberty the runtime's lookup allows.
|
|
267
|
+
*/
|
|
268
|
+
export const BONE_INHERIT_KNOWN =
|
|
269
|
+
`known: ${RIG_BONE_INHERIT.join(', ')} — the runtime folds the case of the first letter and of nothing else`;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Take a bone's setup transform from the cut manifest rather than from a literal.
|
|
273
|
+
*
|
|
274
|
+
* ⭐ This is the one place the rig spec deliberately does not mirror Spine, and
|
|
275
|
+
* the reason is the oldest rule in this project: **the compiler never re-measures
|
|
276
|
+
* art, and a measured number lives in exactly one file.** A rig that wrote
|
|
277
|
+
* `x: 456.5` would be a second copy of a part offset the manifest already holds,
|
|
278
|
+
* and the two would drift the first time the art moved — silently, because both
|
|
279
|
+
* files would still be valid.
|
|
280
|
+
*
|
|
281
|
+
* Exactly one of `anchor` / `slotWindow` / `meshCenter` may be given, and it
|
|
282
|
+
* supplies the bone's `x` and `y`. All three name a point in **crop pixels, y
|
|
283
|
+
* down**; the compiler converts it to Spine world (y up, origin at the crop's
|
|
284
|
+
* bottom-left) and then into the parent bone's local space, so a rotated parent
|
|
285
|
+
* is handled by the same inverse the mesh binder uses.
|
|
286
|
+
*/
|
|
287
|
+
export interface RigBoneFrom {
|
|
288
|
+
/** A key of the manifest's `anchors` block: `[x, y]` or `[x, y, facing_deg]`. */
|
|
289
|
+
anchor?: string;
|
|
290
|
+
/** The centre of a manifest part's window, named by the rig slot it fills. */
|
|
291
|
+
slotWindow?: string;
|
|
292
|
+
/** A manifest part's `mesh.center` — the aperture a ring deforms about. */
|
|
293
|
+
meshCenter?: string;
|
|
294
|
+
/**
|
|
295
|
+
* Where the setup rotation comes from. Omit and no rotation is emitted.
|
|
296
|
+
*
|
|
297
|
+
* `axis` — the manifest's `axis.deg`, negated into Spine's y-up CCW. This
|
|
298
|
+
* is the keystone of an articulated cut: the stroke is a
|
|
299
|
+
* translateX along this bone, so a sibling cut at another camera
|
|
300
|
+
* angle changes one number instead of every key.
|
|
301
|
+
* `anchor` — the third element of the named anchor, a screen-space facing
|
|
302
|
+
* angle. A grip whose local +X points radially outward turns
|
|
303
|
+
* "expand the ring" into one shared translate key.
|
|
304
|
+
*/
|
|
305
|
+
rotation?: 'axis' | 'anchor';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* One bone. Spine's field set, Spine's defaults (`SkeletonJson.ts:90-118`).
|
|
310
|
+
*
|
|
311
|
+
* `parent` is resolved by name and **must be declared earlier in the array** —
|
|
312
|
+
* the parser resolves against the bones it has already read, so a forward
|
|
313
|
+
* reference is not a rigc restriction.
|
|
314
|
+
*/
|
|
315
|
+
export interface RigBone {
|
|
316
|
+
name: string;
|
|
317
|
+
/** Omitted only by the skeleton's single root bone. */
|
|
318
|
+
parent?: string;
|
|
319
|
+
/**
|
|
320
|
+
* Default 0. Drawing reads it nowhere, but a physics constraint driving
|
|
321
|
+
* `rotate`, `shearX` or `scaleX` on this bone steps off its tip — `length`
|
|
322
|
+
* along the bone's x axis is the solver's lever — so a length of 0 there is
|
|
323
|
+
* refused by `A23_PHYSICS_CONSTRAINT_EFFECTIVE` (issue #1195).
|
|
324
|
+
*/
|
|
325
|
+
length?: number;
|
|
326
|
+
/** Local to the parent. Default 0. Supplied by `from` when that is given. */
|
|
327
|
+
x?: number;
|
|
328
|
+
y?: number;
|
|
329
|
+
/** Degrees, CCW, y up. Default 0. Supplied by `from.rotation` when given. */
|
|
330
|
+
rotation?: number;
|
|
331
|
+
/** Default 1. */
|
|
332
|
+
scaleX?: number;
|
|
333
|
+
scaleY?: number;
|
|
334
|
+
/** Default 0. */
|
|
335
|
+
shearX?: number;
|
|
336
|
+
shearY?: number;
|
|
337
|
+
/** Default `normal`. 4.2+ name; 4.0/4.1 called it `transform` — see A02. */
|
|
338
|
+
inherit?: RigBoneInherit;
|
|
339
|
+
/**
|
|
340
|
+
* Default false → `BoneData.skinRequired`: this bone is **inactive** unless the
|
|
341
|
+
* applied skin names it in its `bones` list (see `RigSkinEntry`). Half a switch
|
|
342
|
+
* on its own, so rigc refuses the flag without a skin that activates it.
|
|
343
|
+
*/
|
|
344
|
+
skin?: boolean;
|
|
345
|
+
/** `rrggbbaa`. Editor affordance; no rendering effect. */
|
|
346
|
+
color?: string;
|
|
347
|
+
/**
|
|
348
|
+
* The editor's icon for this bone. Editor affordance; no rendering effect,
|
|
349
|
+
* and no assertion checks the name — the icon vocabulary belongs to the
|
|
350
|
+
* editor, so an unknown one is not rigc's error to raise.
|
|
351
|
+
*/
|
|
352
|
+
icon?: string;
|
|
353
|
+
/** rigc extension — see `RigBoneFrom`. */
|
|
354
|
+
from?: RigBoneFrom;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// slots — `root.slots[]` (SkeletonJson.ts:121-141)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* `SlotData.ts:64`, read by `SkeletonJson.js:124` through `Utils.enumValue`, so
|
|
363
|
+
* only the first letter's case is free: `additive` and `Additive` read
|
|
364
|
+
* `Additive`, while `ADDITIVE`, `mUlTiPlY` and `foo` read as no mode with no
|
|
365
|
+
* error (issue #946, measured with `tools/pose_oracle.ts dump` on spine-core
|
|
366
|
+
* 4.3.13). The parser refuses those by name and the emitter writes the
|
|
367
|
+
* spelling as stated, which is one the runtime resolves.
|
|
368
|
+
*/
|
|
369
|
+
export type RigSlotBlend =
|
|
370
|
+
| 'normal'
|
|
371
|
+
| 'additive'
|
|
372
|
+
| 'multiply'
|
|
373
|
+
| 'screen'
|
|
374
|
+
| 'Normal'
|
|
375
|
+
| 'Additive'
|
|
376
|
+
| 'Multiply'
|
|
377
|
+
| 'Screen';
|
|
378
|
+
|
|
379
|
+
/** The four modes, as a refusal names them; each also reads with its first letter upper-cased. */
|
|
380
|
+
export const RIG_SLOT_BLEND: readonly RigSlotBlend[] = ['normal', 'additive', 'multiply', 'screen'];
|
|
381
|
+
|
|
382
|
+
/** Whether the runtime resolves a stated blend to a mode: its first letter folded, the rest exactly one of the four. */
|
|
383
|
+
export function isRigSlotBlend(value: unknown): value is RigSlotBlend {
|
|
384
|
+
return typeof value === 'string' && (RIG_SLOT_BLEND as readonly string[]).includes(value.charAt(0).toLowerCase() + value.slice(1));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* One slot. **The array order IS the draw order** — there is no separate setup
|
|
389
|
+
* draw-order field anywhere in the format.
|
|
390
|
+
*
|
|
391
|
+
* The rig's slot list is the CANONICAL table and **every slot in it is emitted**,
|
|
392
|
+
* in this order, whether or not anything fills it. A slot no skin and no manifest
|
|
393
|
+
* part fills is emitted with no setup attachment — the shape an editor export
|
|
394
|
+
* carries for a slot that shows nothing (the slot reader above takes `attachment`
|
|
395
|
+
* with a `null` default) — so the emitted array and this one are the same array.
|
|
396
|
+
* `A26_SLOT_DRAW_ORDER` checks both halves of that: nothing out of order, and
|
|
397
|
+
* nothing missing. Declaring a slot no cut fills is therefore legitimate, and it
|
|
398
|
+
* fixes where that slot sits whether or not this cut has art for it.
|
|
399
|
+
*
|
|
400
|
+
* ⚠️ Until issue #575 such a slot was **dropped**, and the gate licensed it: the
|
|
401
|
+
* emitted array was allowed to be any *subsequence* of this one. What that
|
|
402
|
+
* bought was the format's own silence. Nothing said which slot had gone, and
|
|
403
|
+
* every slot below it moved up one index — the index a `drawOrder` key's offsets
|
|
404
|
+
* are counted against, and the one an index-keyed consumer splits on. Two
|
|
405
|
+
* production exports declaring 53 and 61 slots built green at 51 and 57 and read
|
|
406
|
+
* 0.962 and 0.934 under `diff` against the file they were transcribed from.
|
|
407
|
+
*/
|
|
408
|
+
export interface RigSlot {
|
|
409
|
+
name: string;
|
|
410
|
+
/** Required. A miss throws in the parser: `Couldn't find bone … for slot …`. */
|
|
411
|
+
bone: string;
|
|
412
|
+
/**
|
|
413
|
+
* The setup-pose attachment name, or `null` for "show nothing".
|
|
414
|
+
*
|
|
415
|
+
* ⚠️ For a cut compiled with a motion spec this is **not** where the setup pose
|
|
416
|
+
* comes from: `motion.setup` owns it, because which of the two overlay
|
|
417
|
+
* mechanisms a slot uses (attachment + alpha 0, or attachment swapping) is a
|
|
418
|
+
* decision about time. Declaring it in both is a compile error.
|
|
419
|
+
*
|
|
420
|
+
* Required for a slot something fills — the compiler will not guess which of
|
|
421
|
+
* the slot's attachments the setup pose shows — and **optional for a slot
|
|
422
|
+
* nothing fills**, where it can only be `null` and saying so changes no
|
|
423
|
+
* emitted byte. Naming an attachment on a slot nothing fills is refused: the
|
|
424
|
+
* name resolves to nothing, which is the shape of a half-finished wiring-up.
|
|
425
|
+
*/
|
|
426
|
+
attachment?: string | null;
|
|
427
|
+
/** `rrggbbaa`. Default opaque white. */
|
|
428
|
+
color?: string;
|
|
429
|
+
/** Two-colour tint, `rrggbb`. 🚫 `A12_NO_DARK_COLOR` under `spine-html`. */
|
|
430
|
+
dark?: string;
|
|
431
|
+
/** Default `normal`. Only the first letter's case is free (`RigSlotBlend`). */
|
|
432
|
+
blend?: RigSlotBlend;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// attachments — `readAttachment` (SkeletonJson.ts:535-654)
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* A greyscale sheet, in a part's own pixel grid, giving each vertex a depth —
|
|
441
|
+
* what `yaw` and `pitch` otherwise derive from one cylinder radius.
|
|
442
|
+
* [`src/depth.ts`](depth.ts) is the model and the order of operations;
|
|
443
|
+
* `docs/FACE.md` §2.1 is when to reach for it.
|
|
444
|
+
*
|
|
445
|
+
* ⚠️ Naming it changes no emitted byte on its own. It puts a `z` on every
|
|
446
|
+
* vertex, which a `yaw` or `pitch` key then reads by saying `"depth": true`
|
|
447
|
+
* instead of a `radius`. A map that nothing reads is reported and otherwise
|
|
448
|
+
* inert — deliberately, so that adding the input and adopting it are two
|
|
449
|
+
* reviewable steps rather than one.
|
|
450
|
+
*/
|
|
451
|
+
export interface RigDepthMap {
|
|
452
|
+
/**
|
|
453
|
+
* The sheet, relative to the rig's `images` directory, and the same pixel
|
|
454
|
+
* size as this attachment's own `image`.
|
|
455
|
+
*
|
|
456
|
+
* It is NOT packed into the atlas: it is a measurement rigc reads at compile
|
|
457
|
+
* time, not art anything draws. A sheet that reached the atlas would be a
|
|
458
|
+
* page the runtime loads and never samples.
|
|
459
|
+
*/
|
|
460
|
+
image: string;
|
|
461
|
+
/**
|
|
462
|
+
* Which end of the range is closest to the viewer. Stated rather than
|
|
463
|
+
* defaulted, because both conventions are in use and a sheet that means the
|
|
464
|
+
* opposite of what the spec assumes produces a part that turns inside out —
|
|
465
|
+
* with every gate still green, since the arithmetic is correct and only the
|
|
466
|
+
* input was backwards.
|
|
467
|
+
*/
|
|
468
|
+
near: 'white' | 'black';
|
|
469
|
+
/**
|
|
470
|
+
* How many world units the map's full 0..1 range spans, in the attachment's
|
|
471
|
+
* own units — the number `radius` used to carry.
|
|
472
|
+
*
|
|
473
|
+
* Authored, never measured: 8 bits of level say nothing about scale, so a
|
|
474
|
+
* compiler that picked one would be inventing the depth of the art.
|
|
475
|
+
*/
|
|
476
|
+
zScale: number;
|
|
477
|
+
/** Tone curve applied to the nearness. Defaults 1 / 1 / 0, a straight line. */
|
|
478
|
+
gamma?: number;
|
|
479
|
+
contrast?: number;
|
|
480
|
+
bias?: number;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Which part of a mesh is **soft**, and which bone carries it — so a physics
|
|
485
|
+
* constraint on that bone answers an impact over exactly that region.
|
|
486
|
+
*
|
|
487
|
+
* ## Why this is a painted mask and not a depth threshold
|
|
488
|
+
*
|
|
489
|
+
* 🚨 It was a depth threshold for one day (2026-09-05) and that was wrong.
|
|
490
|
+
* Softness and prominence are different properties of the art: on a face the
|
|
491
|
+
* most prominent thing is the **nose**, and a nose does not wobble. A threshold
|
|
492
|
+
* over the depth map produced a region that was plausible, gated green and
|
|
493
|
+
* carried the wrong pixels — the exact shape of failure this compiler exists to
|
|
494
|
+
* refuse, arrived at by reaching for a number that was already in the manifest.
|
|
495
|
+
*
|
|
496
|
+
* It also claimed something untrue. "No mask painted" was the selling line, and
|
|
497
|
+
* a consumer rendering the same effect had a hand-painted spring mask all
|
|
498
|
+
* along. rigc does not get to delete an input by guessing it.
|
|
499
|
+
*
|
|
500
|
+
* ⇒ The mask is authored, like `zScale` and like every other number here that
|
|
501
|
+
* describes a decision about the art rather than a measurement of it.
|
|
502
|
+
*/
|
|
503
|
+
export interface RigSoftRegion {
|
|
504
|
+
/**
|
|
505
|
+
* The bone the soft region is carried by. It must already exist — a bone a
|
|
506
|
+
* physics constraint targets is part of the skeleton, not a side effect of a
|
|
507
|
+
* mesh.
|
|
508
|
+
*/
|
|
509
|
+
bone: string;
|
|
510
|
+
/**
|
|
511
|
+
* A greyscale sheet in the part's own pixel grid: the level IS the weight,
|
|
512
|
+
* black still and white fully carried, sampled at each vertex.
|
|
513
|
+
*
|
|
514
|
+
* ⭐ The ramp is painted rather than parameterised. A `feather` would be this
|
|
515
|
+
* file guessing the shape of a falloff somebody can simply draw, and a hard
|
|
516
|
+
* edge — which a threshold gives you by default — puts the whole difference
|
|
517
|
+
* between carried and still into one triangle.
|
|
518
|
+
*
|
|
519
|
+
* Alpha is not read: a transparent pixel is black, which is weight 0.
|
|
520
|
+
*/
|
|
521
|
+
mask: string;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Directional authority across an axis — see `sideWeight` in
|
|
526
|
+
* [`mesh.ts`](mesh.ts).
|
|
527
|
+
*
|
|
528
|
+
* ⭐ It was an inline object type until issue #545. The four generator kinds
|
|
529
|
+
* were too: the union is spelled as four **named** interfaces now because
|
|
530
|
+
* `RIG_KEYS` pairs a key set with an interface by name, and a shape with no name
|
|
531
|
+
* is a shape the pairing cannot reach — so an anonymous corner of this file
|
|
532
|
+
* would have been a corner whose key set nothing checked.
|
|
533
|
+
*/
|
|
534
|
+
export interface RigMeshBias {
|
|
535
|
+
/** The axis, in SCREEN degrees, y down — a manifest's own convention. */
|
|
536
|
+
axis_deg: number;
|
|
537
|
+
/** Signed distance across that axis over which authority goes 0 -> 1. */
|
|
538
|
+
ramp: [number, number];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** A ring: a seam contour, an aperture inside it, and the bones that open it. */
|
|
542
|
+
export interface RigRingGenerator {
|
|
543
|
+
kind: 'ring';
|
|
544
|
+
/** The seam contour, in part-local pixels, y down. At least 6 points. */
|
|
545
|
+
hull: Array<[number, number]>;
|
|
546
|
+
/** Aperture centre, part-local pixels, y down. */
|
|
547
|
+
center: [number, number];
|
|
548
|
+
/** Inner ring position between the centre (0) and the hull (1). */
|
|
549
|
+
inner: number;
|
|
550
|
+
/** Part window size, for UVs. */
|
|
551
|
+
size: [number, number];
|
|
552
|
+
/** Directional authority across an axis — see `sideWeight` in mesh.ts. */
|
|
553
|
+
bias?: RigMeshBias;
|
|
554
|
+
/**
|
|
555
|
+
* Control bones, by name. More than one splits the ring by angle, and the
|
|
556
|
+
* angle of each is measured from where the rig put that bone relative to
|
|
557
|
+
* `center` — never stated here, so the split cannot drift from the skeleton.
|
|
558
|
+
*/
|
|
559
|
+
controls: string[];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** A ribbon: a strip of cross rows riding a bone chain. */
|
|
563
|
+
export interface RigRibbonGenerator {
|
|
564
|
+
kind: 'ribbon';
|
|
565
|
+
/** Part window size in pixels. The strip spans it. */
|
|
566
|
+
size: [number, number];
|
|
567
|
+
/** Cross rows, entry first. Triangles = 2 * (rows - 1). */
|
|
568
|
+
rows: number;
|
|
569
|
+
/** The bone chain the strip rides, root first. */
|
|
570
|
+
chain: string[];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* A mesh cut to the part's own alpha silhouette: trace the mask, simplify
|
|
575
|
+
* the outline, push it out by a margin, ear-clip it (`buildContourMesh`).
|
|
576
|
+
*
|
|
577
|
+
* ⭐ It takes no `size` and no geometry. The shape is MEASURED off the
|
|
578
|
+
* attachment's own `image` — the same rule a region attachment's
|
|
579
|
+
* `width`/`height` follow (R5) — so there is no number here that can
|
|
580
|
+
* disagree with the pixels, and no polygon to keep in step with the art.
|
|
581
|
+
*
|
|
582
|
+
* 🚨 It is geometry, not a deformation model: every vertex is pinned to
|
|
583
|
+
* the slot bone at weight 1, so an undeformed contour mesh draws exactly
|
|
584
|
+
* what the region drew and no bone can bend it. See the section header in
|
|
585
|
+
* [`src/mesh.ts`](mesh.ts) for what it buys instead, and reach for `ring`
|
|
586
|
+
* or authored `weights` when a bone has to move the art.
|
|
587
|
+
*/
|
|
588
|
+
export interface RigContourGenerator {
|
|
589
|
+
kind: 'contour';
|
|
590
|
+
/**
|
|
591
|
+
* Douglas-Peucker tolerance in the drawing's pixels — the unit every other
|
|
592
|
+
* size here is in, on a packed page that declares a `scale:` as on loose
|
|
593
|
+
* parts: there the trace runs on the page's texels and this is applied as
|
|
594
|
+
* `tolerance × scale` of them (issue #779). Bigger spends fewer vertices
|
|
595
|
+
* and cuts more corners; the builder measures how much of the art the
|
|
596
|
+
* result still covers and refuses a mesh that clips it.
|
|
597
|
+
*/
|
|
598
|
+
tolerance: number;
|
|
599
|
+
/**
|
|
600
|
+
* How far the outline is pushed out past the traced silhouette, in the
|
|
601
|
+
* drawing's pixels (`margin × scale` texels on a `scale:` page). Default 1. Simplification may bite `tolerance` pixels INTO the art, so
|
|
602
|
+
* `margin >= tolerance` is the setting that survives the coverage check.
|
|
603
|
+
*/
|
|
604
|
+
margin?: number;
|
|
605
|
+
/** Refuse rather than emit more outline vertices than this. Default 64. */
|
|
606
|
+
maxVertices?: number;
|
|
607
|
+
/** Alpha at or above which a pixel counts as art, 1..255. Default 1. */
|
|
608
|
+
alpha?: number;
|
|
609
|
+
/** A depth map for this part — see `RigDepthMap`. */
|
|
610
|
+
depth?: RigDepthMap;
|
|
611
|
+
/** A soft region carried by its own bone — see `RigSoftRegion`. */
|
|
612
|
+
soft?: RigSoftRegion;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* A lattice over the part window — the topology `docs/FACE.md` §4 turns a
|
|
617
|
+
* plate into so a turn has columns to move.
|
|
618
|
+
*
|
|
619
|
+
* ⭐ It takes no `size`: like a `contour`, the window is the attachment's
|
|
620
|
+
* own `image`, so there is no number here that can disagree with the
|
|
621
|
+
* pixels. Every vertex is pinned to the slot bone at weight 1, which
|
|
622
|
+
* makes the lattice geometry to DEFORM rather than an authority split —
|
|
623
|
+
* reach for `ring` when bones have to move it.
|
|
624
|
+
*/
|
|
625
|
+
export interface RigGridGenerator {
|
|
626
|
+
kind: 'grid';
|
|
627
|
+
/**
|
|
628
|
+
* Column positions across the window, 0..1, ascending. At least 2.
|
|
629
|
+
*
|
|
630
|
+
* ⚠️ Positions, not a count, and that is deliberate: FACE §4.1 places
|
|
631
|
+
* columns where the drawing needs them, and the worked example's are
|
|
632
|
+
* dense at the silhouette and sparse across the middle. They need not
|
|
633
|
+
* reach the window edge — that example's run 0.0235 to 0.9765.
|
|
634
|
+
*/
|
|
635
|
+
us?: number[];
|
|
636
|
+
/** Row positions down the window, 0..1, ascending. At least 2. */
|
|
637
|
+
vs?: number[];
|
|
638
|
+
/**
|
|
639
|
+
* Even division instead: `cols` columns and `rows` rows spanning the
|
|
640
|
+
* whole window. A convenience for a plate with no shape to follow, and
|
|
641
|
+
* refused beside `us`/`vs`, which say the same thing more precisely.
|
|
642
|
+
*/
|
|
643
|
+
cols?: number;
|
|
644
|
+
rows?: number;
|
|
645
|
+
/** A depth map for this part — see `RigDepthMap`. */
|
|
646
|
+
depth?: RigDepthMap;
|
|
647
|
+
/** A soft region carried by its own bone — see `RigSoftRegion`. */
|
|
648
|
+
soft?: RigSoftRegion;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* One segment stated outright, for a pull that no bone's own span describes.
|
|
653
|
+
*
|
|
654
|
+
* `from` and `to` are in the part's own pixels, y down — the frame every other
|
|
655
|
+
* point a generator takes is in (`ring.center`, `ring.hull`). The bone is
|
|
656
|
+
* resolved by name like everything else; the two points belong to this mesh
|
|
657
|
+
* alone, which is the case a bone's `length` cannot cover: two meshes over one
|
|
658
|
+
* bone that each want it to pull along a different line.
|
|
659
|
+
*/
|
|
660
|
+
export interface RigSegmentSpan {
|
|
661
|
+
bone: string;
|
|
662
|
+
from: [number, number];
|
|
663
|
+
to: [number, number];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* How distance to a segment becomes a weight: `w = 1 / (d + radius)^power`
|
|
668
|
+
* per candidate bone, the strongest `maxBones` kept and normalised, any share
|
|
669
|
+
* under `minWeight` dropped, the rest normalised again.
|
|
670
|
+
*/
|
|
671
|
+
export interface RigSegmentsFalloff {
|
|
672
|
+
/** The exponent. Default 2. */
|
|
673
|
+
power?: number;
|
|
674
|
+
/**
|
|
675
|
+
* **Required.** Added to every distance, in the part's pixels, so a vertex
|
|
676
|
+
* ON a segment has a finite weight and the blend between two segments is as
|
|
677
|
+
* wide as this says. No default: it is a length on this part's art.
|
|
678
|
+
*/
|
|
679
|
+
radius: number;
|
|
680
|
+
/** At most this many bones pull one vertex. Default 4. */
|
|
681
|
+
maxBones?: number;
|
|
682
|
+
/** A normalised share under this is dropped. Default 0.03. */
|
|
683
|
+
minWeight?: number;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* A lattice over the part's alpha, weighted by distance to named bone segments
|
|
688
|
+
* (`buildSegmentsLattice` and `segmentWeights` in [`mesh.ts`](mesh.ts)).
|
|
689
|
+
*
|
|
690
|
+
* ⭐ The one authoring decision is `bones` — which segments may pull this part.
|
|
691
|
+
* The geometry comes off the attachment's own `image`, the segments off the
|
|
692
|
+
* skeleton's setup pose, and the weights off the distance between the two, so
|
|
693
|
+
* nothing else here is a judgement about the art.
|
|
694
|
+
*/
|
|
695
|
+
export interface RigSegmentsGenerator {
|
|
696
|
+
kind: 'segments';
|
|
697
|
+
/** **Required.** The lattice's cell, in the part's pixels, a whole number of at least 1. */
|
|
698
|
+
cell: number;
|
|
699
|
+
/**
|
|
700
|
+
* **Required, at least one entry.** Each is a bone name (origin to its
|
|
701
|
+
* `length` tip), a chain — a list of bone names, root first, each link
|
|
702
|
+
* running from its origin to the next link's (the last to its `length` tip) —
|
|
703
|
+
* or a `RigSegmentSpan` stated outright.
|
|
704
|
+
*/
|
|
705
|
+
bones: Array<string | string[] | RigSegmentSpan>;
|
|
706
|
+
/** The falloff — see `RigSegmentsFalloff`. `radius` is required, so the block is too. */
|
|
707
|
+
falloff: RigSegmentsFalloff;
|
|
708
|
+
/** Alpha at or above which a pixel counts as art, 1..255. Default 1 — `contour`'s own. */
|
|
709
|
+
alpha?: number;
|
|
710
|
+
/**
|
|
711
|
+
* Where the slot bone sits in the part's pixels, y down. Default the window's
|
|
712
|
+
* centre — the placement every generator on this route uses.
|
|
713
|
+
*/
|
|
714
|
+
anchor?: [number, number];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Which builder in `src/mesh.ts` makes this mesh's geometry, and its parameters.
|
|
719
|
+
*
|
|
720
|
+
* The builders stay **code** and are invoked by **data**: they encode a
|
|
721
|
+
* deformation model (what is pinned, what may move, how authority falls off),
|
|
722
|
+
* and a model is not a table of numbers.
|
|
723
|
+
*
|
|
724
|
+
* ⚠️ A cut with a manifest does not use this. There the generator is invoked
|
|
725
|
+
* through the manifest's `mesh` block, because everything a generator needs —
|
|
726
|
+
* the mask contour, the aperture centre, the part window — is *measured art*,
|
|
727
|
+
* and measured art lives in the manifest. `generator` is for a skeleton with no
|
|
728
|
+
* manifest behind it.
|
|
729
|
+
*/
|
|
730
|
+
export type RigMeshGenerator =
|
|
731
|
+
| RigRingGenerator
|
|
732
|
+
| RigRibbonGenerator
|
|
733
|
+
| RigContourGenerator
|
|
734
|
+
| RigGridGenerator
|
|
735
|
+
| RigSegmentsGenerator;
|
|
736
|
+
|
|
737
|
+
/** The five `kind` names a generator may carry, and the order `RIG_KEYS` takes them in. */
|
|
738
|
+
export const RIG_GENERATOR_KINDS = ['ring', 'ribbon', 'contour', 'grid', 'segments'] as const;
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* The attachment's own NAME, as distinct from the placeholder key it is filed
|
|
742
|
+
* under — one field, the same on every attachment type (issue #796).
|
|
743
|
+
*
|
|
744
|
+
* `readAttachment` reads `name = getValue(map, "name", placeholder)`
|
|
745
|
+
* (`SkeletonJson.js:526`) for every type, so this is the runtime's
|
|
746
|
+
* `Attachment.name` — what a consumer reads off `slot.attachment.name` — and,
|
|
747
|
+
* on the three types that draw, the default of `path` (`:529`, `:560`). Absent,
|
|
748
|
+
* the runtime names the attachment by its placeholder.
|
|
749
|
+
*
|
|
750
|
+
* 🔑 Nothing in the format resolves BY it. A skin's table, a slot's setup
|
|
751
|
+
* `attachment`, an attachment or deform timeline and a linked mesh's `source`
|
|
752
|
+
* are all keyed by the placeholder (`:415-418`, `:433`, `:1140`) — measured: a
|
|
753
|
+
* link whose `source` spells its source's name rather than its key throws
|
|
754
|
+
* `Source mesh not found`. So two skins may give one placeholder two
|
|
755
|
+
* attachments of one name, and the runtime keeps both.
|
|
756
|
+
*
|
|
757
|
+
* ⭐ rigc writes this field exactly when the spec states it and never derives
|
|
758
|
+
* one. Until #796 it composed `<skin>/<placeholder>` for a placeholder several
|
|
759
|
+
* skins fill, and a transcribed name had no field to live in — so a rebuild of
|
|
760
|
+
* an editor export answered to other names than its source did, which a
|
|
761
|
+
* consumer reading `attachment.name` can see and no gate could.
|
|
762
|
+
*/
|
|
763
|
+
export type RigAttachmentName = string;
|
|
764
|
+
|
|
765
|
+
/** `SkeletonJson.ts:540-559`. `type` defaults to `region` (`:539`). */
|
|
766
|
+
export interface RigRegionAttachment {
|
|
767
|
+
type?: 'region';
|
|
768
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
769
|
+
name?: RigAttachmentName;
|
|
770
|
+
/** The atlas region to resolve. Defaults to the attachment's own name. */
|
|
771
|
+
path?: string;
|
|
772
|
+
/**
|
|
773
|
+
* rigc extension: a PNG, relative to the rig's `images` directory.
|
|
774
|
+
*
|
|
775
|
+
* ⭐ Naming a file instead of a size is the point. `width`/`height` have **no
|
|
776
|
+
* parser default** — an omission loads as `NaN` and every UV collapses with no
|
|
777
|
+
* error — so a spec that restates them by hand carries a number that can
|
|
778
|
+
* disagree with the pixels. Give an `image` and rigc reads the PNG header and
|
|
779
|
+
* fills both in; the atlas page it emits is that same file, so the size in the
|
|
780
|
+
* skeleton and the size in the atlas cannot drift apart.
|
|
781
|
+
*/
|
|
782
|
+
image?: string;
|
|
783
|
+
x?: number;
|
|
784
|
+
y?: number;
|
|
785
|
+
/** Degrees. Cancels a rotated bone for a plate authored in screen space. */
|
|
786
|
+
rotation?: number;
|
|
787
|
+
scaleX?: number;
|
|
788
|
+
scaleY?: number;
|
|
789
|
+
/** Required by the format; may be omitted here when `image` is given. */
|
|
790
|
+
width?: number;
|
|
791
|
+
height?: number;
|
|
792
|
+
/** `rrggbbaa`. */
|
|
793
|
+
color?: string;
|
|
794
|
+
/** A numbered image series in place of one region — see `RigSequence`. */
|
|
795
|
+
sequence?: RigSequence;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* A numbered image series drawn by ONE attachment — `readSequence`
|
|
800
|
+
* (`SkeletonJson.js:641-649`), on a region, a mesh or a linked mesh.
|
|
801
|
+
*
|
|
802
|
+
* The attachment's `path` (or, with none, its placeholder) is the series'
|
|
803
|
+
* **stem**, and frame `i` is the atlas region `stem + (start + i)` left-padded
|
|
804
|
+
* with zeros to `digits` — `Sequence.getPath` (`Sequence.js:124-132`), which
|
|
805
|
+
* `AtlasAttachmentLoader.findRegions` walks for every `i` below `count`. A
|
|
806
|
+
* `sequence` timeline (the motion spec's `sequence` family) chooses which frame
|
|
807
|
+
* shows; without one, the frame is `setup`.
|
|
808
|
+
*
|
|
809
|
+
* ⭐ **The frames resolve by name and a missing one is refused by name.** On the
|
|
810
|
+
* loose route frame `i` is the PNG `<images>/<region>.png`; under `--atlas-in`
|
|
811
|
+
* it is the pack's region of that name. The compiler looks each one up and names
|
|
812
|
+
* the frame number and the region it looked for when one is absent — it never
|
|
813
|
+
* stands one frame in for another, and the loader's own miss
|
|
814
|
+
* (`Region not found in atlas`) names neither the frame nor the series.
|
|
815
|
+
*
|
|
816
|
+
* ⚠️ An attachment carrying a sequence states no `image`: an image names one
|
|
817
|
+
* region and a sequence names `count` of them, so the pair would be two claims
|
|
818
|
+
* about what the attachment draws. And no `generator`: a generator traces one
|
|
819
|
+
* plate, and which frame it should trace is not something the spec says.
|
|
820
|
+
*/
|
|
821
|
+
export interface RigSequence {
|
|
822
|
+
/**
|
|
823
|
+
* How many frames. **Required** — the parser's default is 0
|
|
824
|
+
* (`new Sequence(getValue(map, "count", 0), true)`), which loads an attachment
|
|
825
|
+
* holding no region at all and draws nothing, with no error.
|
|
826
|
+
*/
|
|
827
|
+
count: number;
|
|
828
|
+
/** The number the first frame's name carries. Parser default 1. */
|
|
829
|
+
start?: number;
|
|
830
|
+
/** Zero-pad the frame number to at least this many digits. Parser default 0 (no padding). */
|
|
831
|
+
digits?: number;
|
|
832
|
+
/**
|
|
833
|
+
* The frame the setup pose shows, 0-based. Parser default 0. Spelled as the
|
|
834
|
+
* FILE spells it (`getValue(map, "setup", 0)`); `setupIndex` is the runtime's
|
|
835
|
+
* field name and is not a key the format has.
|
|
836
|
+
*/
|
|
837
|
+
setup?: number;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* `SkeletonJson.ts:568-605`. Either authored geometry or a `generator`, never
|
|
842
|
+
* both.
|
|
843
|
+
*
|
|
844
|
+
* ⚠️ `vertices` has no encoding flag anywhere in the format. If its length
|
|
845
|
+
* equals `uvs.length` the parser reads unweighted x/y pairs; otherwise it reads
|
|
846
|
+
* the weighted run `boneCount, (boneIndex, bindX, bindY, weight) × n, …`. A
|
|
847
|
+
* coincidental length match reads weight data as coordinates, silently — which
|
|
848
|
+
* is `A04_MESH_TRIANGLES_AND_ENCODING`.
|
|
849
|
+
*/
|
|
850
|
+
/**
|
|
851
|
+
* One bone's pull on one vertex of an authored mesh, **named**.
|
|
852
|
+
*
|
|
853
|
+
* `x`/`y` are the vertex's position in that bone's own setup space — the same
|
|
854
|
+
* pair Spine's weighted run carries after the bone index. `weight` is its share;
|
|
855
|
+
* a vertex's weights sum to 1.
|
|
856
|
+
*/
|
|
857
|
+
export interface RigMeshBinding {
|
|
858
|
+
/** Resolved against the rig's bone list at emit. An unknown name is refused. */
|
|
859
|
+
bone: string;
|
|
860
|
+
x: number;
|
|
861
|
+
y: number;
|
|
862
|
+
weight: number;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export interface RigMeshAttachment {
|
|
866
|
+
type: 'mesh';
|
|
867
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
868
|
+
name?: RigAttachmentName;
|
|
869
|
+
path?: string;
|
|
870
|
+
image?: string;
|
|
871
|
+
/** Its length defines `worldVerticesLength`; required with authored geometry. */
|
|
872
|
+
uvs?: number[];
|
|
873
|
+
triangles?: number[];
|
|
874
|
+
/**
|
|
875
|
+
* Geometry, in one of two forms.
|
|
876
|
+
*
|
|
877
|
+
* **Unweighted** — one `x, y` pair per uv pair, and `vertices.length` equals
|
|
878
|
+
* `uvs.length`. Nothing here names a bone, so nothing here can be rebound.
|
|
879
|
+
*
|
|
880
|
+
* **Weighted, raw** — Spine's own encoding,
|
|
881
|
+
* `boneCount, (boneIndex, bindX, bindY, weight) x n` per vertex, where
|
|
882
|
+
* `boneIndex` is a position in the EMITTED bone array. 🚨 That array is not
|
|
883
|
+
* something a rig spec writes or can see, so those indices shift under any
|
|
884
|
+
* edit to the bone list and every vertex silently rebinds — the mesh still
|
|
885
|
+
* loads, every weight still sums to 1, and nothing in the file objects. rigc
|
|
886
|
+
* therefore refuses this form unless the attachment says `boneIndexing: "raw"`
|
|
887
|
+
* out loud. Use `weights` instead.
|
|
888
|
+
*/
|
|
889
|
+
vertices?: number[];
|
|
890
|
+
/**
|
|
891
|
+
* Weighted geometry that binds **by name**: one entry per vertex, each a list
|
|
892
|
+
* of `{ bone, x, y, weight }`. This is the default form and the one everything
|
|
893
|
+
* else in a rig spec already uses — a bone's `parent`, a slot's `bone`, a
|
|
894
|
+
* constraint's `bones` and `target` all resolve by name and refuse a miss by
|
|
895
|
+
* name. The compiler resolves these to indices on emit, so inserting a bone
|
|
896
|
+
* moves the indices and changes nothing about what the mesh is bound to.
|
|
897
|
+
*
|
|
898
|
+
* Mutually exclusive with `vertices`.
|
|
899
|
+
*/
|
|
900
|
+
weights?: RigMeshBinding[][];
|
|
901
|
+
/**
|
|
902
|
+
* How a weighted `vertices` run names its bones. Default `"name"`, which means
|
|
903
|
+
* "there is no weighted run here — use `weights`". `"raw"` opts into the index
|
|
904
|
+
* encoding above, for a spec transcribed from an export that has not been
|
|
905
|
+
* migrated yet. It is an opt-in because the cost of it is silence.
|
|
906
|
+
*/
|
|
907
|
+
boneIndexing?: 'name' | 'raw';
|
|
908
|
+
/** Hull vertex count. The loader stores it doubled. */
|
|
909
|
+
hull?: number;
|
|
910
|
+
/** Edge index pairs; nonessential, editor-drawn. */
|
|
911
|
+
edges?: number[];
|
|
912
|
+
width?: number;
|
|
913
|
+
height?: number;
|
|
914
|
+
color?: string;
|
|
915
|
+
/** Build the geometry instead of authoring it — see `RigMeshGenerator`. */
|
|
916
|
+
generator?: RigMeshGenerator;
|
|
917
|
+
/** A numbered image series over this one triangulation — see `RigSequence`. */
|
|
918
|
+
sequence?: RigSequence;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* A mesh that borrows another mesh's geometry — Spine's `linkedmesh`, the type
|
|
923
|
+
* a skin variant uses to draw its own art over one triangulation.
|
|
924
|
+
*
|
|
925
|
+
* 🔑 **`source` is the source attachment's PLACEHOLDER — the key it is filed
|
|
926
|
+
* under in its skin — and not its `name`.** The resolution is
|
|
927
|
+
* `skin.getAttachment(sourceSlotIndex, source)` (`SkeletonJson.js:433`), and a
|
|
928
|
+
* skin's table is keyed by the JSON key `readSkin` iterated (`:415-418`), so a
|
|
929
|
+
* source that states a `name` of its own is still found under its placeholder
|
|
930
|
+
* alone — and a `source` spelling that name throws `Source mesh not found`.
|
|
931
|
+
* Two skins each filling the source's placeholder are told apart by `skin`,
|
|
932
|
+
* never by a name (issue #796, which retired #541's reading that a link
|
|
933
|
+
* resolves its source by name).
|
|
934
|
+
*
|
|
935
|
+
* 🚨 **A linked mesh states no geometry of its own, and the parser is silent
|
|
936
|
+
* about one that does.** The `source` branch returns before `readVertices`
|
|
937
|
+
* (`:582-586`), so `uvs`, `triangles`, `vertices`, `weights`, `hull` and `edges`
|
|
938
|
+
* on a link are read by nothing at all. Measured on a forged skeleton: a link
|
|
939
|
+
* declaring 5 uvs, 3 triangles, `hull: 5` and `edges: [0, 2]` beside a 4-vertex
|
|
940
|
+
* source loaded with the SOURCE's 8-long `worldVerticesLength`, 6 triangles,
|
|
941
|
+
* `hullLength` 8 and 10 edges — the numbers the author wrote reached nothing and
|
|
942
|
+
* nothing said so. rigc refuses them by name.
|
|
943
|
+
*
|
|
944
|
+
* ⚠️ `width`/`height` are the link's own art, and the RUNTIME overwrites both
|
|
945
|
+
* with the source's at resolution time (`MeshAttachment.setSourceMesh`,
|
|
946
|
+
* `:102-103`; measured: a link stating 99x77 beside a 32x32 source loads as
|
|
947
|
+
* 32x32). They are emitted because the editor reads them off the file and
|
|
948
|
+
* because the spec stated them, and the gate cannot see them — which is the
|
|
949
|
+
* reason this note exists rather than an assertion.
|
|
950
|
+
*/
|
|
951
|
+
export interface RigLinkedMeshAttachment {
|
|
952
|
+
type: 'linkedmesh';
|
|
953
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
954
|
+
name?: RigAttachmentName;
|
|
955
|
+
/** The art this link draws, exactly as a mesh's: its own region. */
|
|
956
|
+
path?: string;
|
|
957
|
+
image?: string;
|
|
958
|
+
/**
|
|
959
|
+
* The placeholder of the mesh whose geometry this one borrows. Required — and
|
|
960
|
+
* required in the strong sense: `getValue(map, "source", null)` is FALSY-tested
|
|
961
|
+
* (`:582`), so an absent or empty `source` is not a link at all and the parser
|
|
962
|
+
* falls through to `map.uvs`, which a link does not have, and throws.
|
|
963
|
+
*/
|
|
964
|
+
source: string;
|
|
965
|
+
/**
|
|
966
|
+
* The slot the source lives in. Default: **the link's own slot**
|
|
967
|
+
* (`sourceIndex = slotIndex`, `:571-580`). Resolved by name; a slot the rig
|
|
968
|
+
* does not declare is refused.
|
|
969
|
+
*/
|
|
970
|
+
slot?: string;
|
|
971
|
+
/**
|
|
972
|
+
* The skin the source lives in. Default: **the default skin**
|
|
973
|
+
* (`!linkedMesh.skin ? skeletonData.defaultSkin : findSkin(...)`, `:429`).
|
|
974
|
+
* Resolved by name; a skin the rig does not declare is refused.
|
|
975
|
+
*/
|
|
976
|
+
skin?: string;
|
|
977
|
+
/**
|
|
978
|
+
* Whether the link plays the source's deform keys. Default **true**, which
|
|
979
|
+
* also sets `timelineAttachment` to the source and adds this link's slot to
|
|
980
|
+
* the source's `timelineSlots` when the two differ (`:437-448`). `false` makes
|
|
981
|
+
* the link its own `timelineAttachment`, so only keys written against the link
|
|
982
|
+
* itself move it.
|
|
983
|
+
*/
|
|
984
|
+
timelines?: boolean;
|
|
985
|
+
width?: number;
|
|
986
|
+
height?: number;
|
|
987
|
+
color?: string;
|
|
988
|
+
/** The link's OWN numbered series — see `RigSequence`. */
|
|
989
|
+
sequence?: RigSequence;
|
|
990
|
+
/**
|
|
991
|
+
* 🚫 Every geometry field a mesh may state, refused by name on a link. They
|
|
992
|
+
* are declared so the refusal can name them: a key the shape
|
|
993
|
+
* does not hold at all comes back as *keys this compiler does not read … fix
|
|
994
|
+
* the spelling or remove it*, and the remedy sentence is wrong here — the
|
|
995
|
+
* fault is not a typo, it is that the parser reads none of them on a link.
|
|
996
|
+
*/
|
|
997
|
+
uvs?: number[];
|
|
998
|
+
triangles?: number[];
|
|
999
|
+
vertices?: number[];
|
|
1000
|
+
weights?: RigMeshBinding[][];
|
|
1001
|
+
boneIndexing?: 'name' | 'raw';
|
|
1002
|
+
hull?: number;
|
|
1003
|
+
edges?: number[];
|
|
1004
|
+
generator?: RigMeshGenerator;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* The geometry every non-region attachment shares: a polygon, either pinned to
|
|
1009
|
+
* one bone or weighted across several.
|
|
1010
|
+
*
|
|
1011
|
+
* ⭐ `vertexCount` is REQUIRED and cross-checked, and that is the whole design of
|
|
1012
|
+
* these two types. A mesh gets its vertex count from `uvs.length`, so there is
|
|
1013
|
+
* nothing to state; a bounding box and a clipping polygon have no uvs, and the
|
|
1014
|
+
* parser reads `map.vertexCount << 1` — with the field absent that is
|
|
1015
|
+
* `undefined << 1` = **0**, so `readVertices` takes the weighted branch,
|
|
1016
|
+
* decodes coordinates as a weight run, and hands back an attachment with no
|
|
1017
|
+
* vertices at all. Nothing throws. So the count is declared here and checked
|
|
1018
|
+
* against whichever encoding the spec used.
|
|
1019
|
+
*
|
|
1020
|
+
* The two encodings are the mesh's, unchanged, and for the same reason:
|
|
1021
|
+
* `weights` binds by NAME and is the default; `vertices` is either an unweighted
|
|
1022
|
+
* `x, y` run (one pair per vertex) or Spine's index-encoded weighted run, and
|
|
1023
|
+
* the second of those needs `boneIndexing: "raw"` said out loud because a bone
|
|
1024
|
+
* inserted anywhere above shifts every index in silence (issue #45).
|
|
1025
|
+
*/
|
|
1026
|
+
export interface RigVertexGeometry {
|
|
1027
|
+
/** Required. No parser default: absent reads as 0 and the polygon vanishes. */
|
|
1028
|
+
vertexCount: number;
|
|
1029
|
+
/**
|
|
1030
|
+
* Unweighted `x, y` pairs (`vertices.length === vertexCount * 2`), or Spine's
|
|
1031
|
+
* weighted run behind `boneIndexing: "raw"`. Mutually exclusive with `weights`.
|
|
1032
|
+
*/
|
|
1033
|
+
vertices?: number[];
|
|
1034
|
+
/** Weighted geometry bound by name — one entry per vertex. The default form. */
|
|
1035
|
+
weights?: RigMeshBinding[][];
|
|
1036
|
+
/** `"raw"` opts a `vertices` weighted run into the index encoding. */
|
|
1037
|
+
boneIndexing?: 'name' | 'raw';
|
|
1038
|
+
/** `rrggbbaa`. Editor affordance: the colour the box is drawn in. */
|
|
1039
|
+
color?: string;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* `type: "boundingbox"` (`SkeletonJson.ts:560-567`).
|
|
1044
|
+
*
|
|
1045
|
+
* **When you need one:** a polygon the game can hit-test against — a hurt box, a
|
|
1046
|
+
* pick region, a trigger volume — that moves with the skeleton and draws
|
|
1047
|
+
* nothing. It is the only attachment type whose entire purpose is outside the
|
|
1048
|
+
* renderer, which is why it has no `path`, no size and no uvs.
|
|
1049
|
+
*/
|
|
1050
|
+
export interface RigBoundingBoxAttachment extends RigVertexGeometry {
|
|
1051
|
+
type: 'boundingbox';
|
|
1052
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
1053
|
+
name?: RigAttachmentName;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* `type: "clipping"` (`SkeletonJson.ts:635-651`).
|
|
1058
|
+
*
|
|
1059
|
+
* **When you need one:** a mask. The polygon clips every slot drawn from the one
|
|
1060
|
+
* carrying it up to and including `end`, so a window, a portal or a wipe is one
|
|
1061
|
+
* attachment rather than a second set of art.
|
|
1062
|
+
*
|
|
1063
|
+
* ⚠️ `end` is resolved with `skeletonData.findSlot(end)`, which returns **null**
|
|
1064
|
+
* on a miss and assigns that null without complaint (`:626-627`). The clip then
|
|
1065
|
+
* never ends — it runs to the bottom of the draw order and takes every slot
|
|
1066
|
+
* below it with it. rigc refuses a name the rig does not declare.
|
|
1067
|
+
*/
|
|
1068
|
+
export interface RigClippingAttachment extends RigVertexGeometry {
|
|
1069
|
+
type: 'clipping';
|
|
1070
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
1071
|
+
name?: RigAttachmentName;
|
|
1072
|
+
/**
|
|
1073
|
+
* The last slot this clip applies to, by name. Absent leaves `endSlot` null,
|
|
1074
|
+
* which is the parser's own encoding for "clip everything after this one".
|
|
1075
|
+
*/
|
|
1076
|
+
end?: string;
|
|
1077
|
+
/** 4.3. Default false. */
|
|
1078
|
+
convex?: boolean;
|
|
1079
|
+
/** 4.3. Default false. */
|
|
1080
|
+
inverse?: boolean;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* `type: "path"` (`SkeletonJson.ts:606-623`) — a composite cubic Bezier the
|
|
1085
|
+
* skeleton carries as an attachment.
|
|
1086
|
+
*
|
|
1087
|
+
* **When you need one:** a path constraint has nowhere to aim without it. The
|
|
1088
|
+
* polygon here is not drawn (no runtime renders a path); it is the curve
|
|
1089
|
+
* `RigPathConstraint` slides bones along, and it deforms with the slot's bone
|
|
1090
|
+
* like any other vertex attachment.
|
|
1091
|
+
*
|
|
1092
|
+
* 🚨 **`vertexCount` is knots AND handles, and it must be a multiple of 3.**
|
|
1093
|
+
* The parser hands `vertexCount << 1` to `readVertices` and then walks the
|
|
1094
|
+
* result in groups of six (`PathConstraint.computeWorldPositions`): the first
|
|
1095
|
+
* and last points are the outer control handles of the end knots and are
|
|
1096
|
+
* dropped, leaving a `3K + 1` chain — so an OPEN path of K curves has
|
|
1097
|
+
* `vertexCount = 3(K + 1)` (minimum 6) and a CLOSED one has `3K` (minimum 3,
|
|
1098
|
+
* because the chain wraps). A count that is not a multiple of 3 does not throw:
|
|
1099
|
+
* `Utils.newArray(vertexCount / 3, 0)` accepts a fractional size, the groups of
|
|
1100
|
+
* six then straddle the knots, and the constraint slides bones along a curve
|
|
1101
|
+
* nobody drew.
|
|
1102
|
+
*
|
|
1103
|
+
* ⭐ `lengths` is **stated or measured** (issue #804). It is the cumulative
|
|
1104
|
+
* length at the end of each curve, in world units, and `vertexCount / 3` entries
|
|
1105
|
+
* on an open path and a closed one alike — the parser's allocation, one more
|
|
1106
|
+
* than an open path's curves, the last being the wrap-around curve's cumulative,
|
|
1107
|
+
* which nothing reads. Stated, it is emitted as stated: that is what `ingest`
|
|
1108
|
+
* writes from an export, because the editor measured it on a pose rigc does not
|
|
1109
|
+
* reproduce (below). Left out, rigc measures it off the geometry on the
|
|
1110
|
+
* unconstrained setup pose. Only `constantSpeed: false` reads it.
|
|
1111
|
+
*
|
|
1112
|
+
* ⚠️ Why a stated one is not re-measured: the editor's numbers are
|
|
1113
|
+
* `PathConstraint`'s own measurement of the pose the first update gives it —
|
|
1114
|
+
* with every constraint ordered before the path constraint applied. rigc does
|
|
1115
|
+
* not pose, so an authored path whose bones a constraint moves at rest gets the
|
|
1116
|
+
* unconstrained figure, and an export's own array is the only way to carry the
|
|
1117
|
+
* constrained one. Re-deriving it moved 232 of a production rig's 259 bones in
|
|
1118
|
+
* issue #804's pose comparison.
|
|
1119
|
+
*
|
|
1120
|
+
* 🔸 *Which* length, exactly, is `SpinePathAttachment`'s subject in
|
|
1121
|
+
* [`types.ts`](types.ts) and it is not the arc: it is `PathConstraint`'s own
|
|
1122
|
+
* four-sample forward difference, about 0.5 % below the arc, which is what the
|
|
1123
|
+
* Spine editor writes back too (issue #560). This comment said "arc length"
|
|
1124
|
+
* until then.
|
|
1125
|
+
*/
|
|
1126
|
+
export interface RigPathAttachment extends RigVertexGeometry {
|
|
1127
|
+
type: 'path';
|
|
1128
|
+
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
1129
|
+
name?: RigAttachmentName;
|
|
1130
|
+
/** Default false. When true the last knot joins the first. */
|
|
1131
|
+
closed?: boolean;
|
|
1132
|
+
/**
|
|
1133
|
+
* Default **true** (`:610`) — note the direction: leaving it out asks for the
|
|
1134
|
+
* expensive-and-correct traversal, in which the runtime re-measures the path
|
|
1135
|
+
* every frame and `lengths` is never read. `false` makes the runtime trust the
|
|
1136
|
+
* emitted `lengths` instead: cheaper, exact only while the path holds its setup
|
|
1137
|
+
* shape, and the reason a deformed path wants the default.
|
|
1138
|
+
*/
|
|
1139
|
+
constantSpeed?: boolean;
|
|
1140
|
+
/**
|
|
1141
|
+
* Stated: exactly `vertexCount / 3` finite entries, none below the one before
|
|
1142
|
+
* it, emitted as stated. Absent: measured — see the note above.
|
|
1143
|
+
*/
|
|
1144
|
+
lengths?: number[];
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* The two types the format holds and rigc's emitter does not cover. They are
|
|
1149
|
+
* in the type so a spec can *say* them and get a named `NotImplementedError`;
|
|
1150
|
+
* the alternative is the parser's own behaviour, which is to return `null` for
|
|
1151
|
+
* an unknown `type` and drop the attachment without a word
|
|
1152
|
+
* (`SkeletonJson.ts:653`).
|
|
1153
|
+
*
|
|
1154
|
+
* 🚧 It appears nowhere in the benchmark corpus, so it is not on the ladder's
|
|
1155
|
+
* critical path — which is the reason it is deferred rather than an oversight.
|
|
1156
|
+
* `linkedmesh` stood here beside it until issue #691; `RigLinkedMeshAttachment`
|
|
1157
|
+
* is the shape that replaced it.
|
|
1158
|
+
*/
|
|
1159
|
+
export interface RigUnimplementedAttachment {
|
|
1160
|
+
type: 'point';
|
|
1161
|
+
[field: string]: unknown;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export type RigAttachment =
|
|
1165
|
+
| RigRegionAttachment
|
|
1166
|
+
| RigLinkedMeshAttachment
|
|
1167
|
+
| RigMeshAttachment
|
|
1168
|
+
| RigBoundingBoxAttachment
|
|
1169
|
+
| RigClippingAttachment
|
|
1170
|
+
| RigPathAttachment
|
|
1171
|
+
| RigUnimplementedAttachment;
|
|
1172
|
+
|
|
1173
|
+
/** `slotName -> placeholderName -> attachment` (`SkeletonJson.ts:431-439`). */
|
|
1174
|
+
export type RigSkinAttachments = Record<string, Record<string, RigAttachment>>;
|
|
1175
|
+
|
|
1176
|
+
/** The five per-type constraint lists a skin entry can carry (`:386-429`). */
|
|
1177
|
+
export const RIG_SKIN_CONSTRAINT_KEYS = ['ik', 'transform', 'path', 'physics', 'slider'] as const;
|
|
1178
|
+
|
|
1179
|
+
export type RigSkinConstraintKey = (typeof RIG_SKIN_CONSTRAINT_KEYS)[number];
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Every key the long form of a skin entry owns.
|
|
1183
|
+
*
|
|
1184
|
+
* ⚠️ Which is exactly the set of names a SLOT may not have, because these are
|
|
1185
|
+
* the keys that tell the two forms apart — see `splitRigSkin`. `parseRigSpec`
|
|
1186
|
+
* refuses such a slot by name rather than letting one form be read as the other.
|
|
1187
|
+
*/
|
|
1188
|
+
export const RIG_SKIN_KEYS = ['attachments', 'bones', ...RIG_SKIN_CONSTRAINT_KEYS] as const;
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* A skin's full 4.3 shape: attachments, plus the bones and constraints this skin
|
|
1192
|
+
* **activates** (`SkeletonJson.ts:377-429`).
|
|
1193
|
+
*
|
|
1194
|
+
* ⭐ The lists are not a second way to declare a bone or a constraint. They are
|
|
1195
|
+
* the other half of a switch whose first half already existed: a bone's
|
|
1196
|
+
* `skin: true` and a constraint's `skin: true` set `skinRequired`, and
|
|
1197
|
+
* `Skeleton.updateCache` starts every `skinRequired` object **inactive**, turning
|
|
1198
|
+
* it on only for the skin that names it here (`Skeleton.ts:191-217`; a listed
|
|
1199
|
+
* bone activates its whole ancestor chain). So either half alone is dead data,
|
|
1200
|
+
* in opposite directions and both in silence — `skin: true` with no list is a
|
|
1201
|
+
* bone that never poses, a list without `skin: true` is a list that changes
|
|
1202
|
+
* nothing — which is why rigc refuses both halves by name and
|
|
1203
|
+
* `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` checks the artifact for them.
|
|
1204
|
+
*/
|
|
1205
|
+
export interface RigSkinEntry {
|
|
1206
|
+
/** `slotName -> placeholderName -> attachment`. */
|
|
1207
|
+
attachments?: RigSkinAttachments;
|
|
1208
|
+
/** Bone names this skin activates. Each one must declare `skin: true`. */
|
|
1209
|
+
bones?: string[];
|
|
1210
|
+
/** `ik` constraint names this skin activates. Each must declare `skin: true`. */
|
|
1211
|
+
ik?: string[];
|
|
1212
|
+
transform?: string[];
|
|
1213
|
+
path?: string[];
|
|
1214
|
+
physics?: string[];
|
|
1215
|
+
slider?: string[];
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* One skin, in either of two spellings.
|
|
1220
|
+
*
|
|
1221
|
+
* The short one — `slotName -> placeholderName -> attachment` — is the shape
|
|
1222
|
+
* every rig spec in this repository already uses and it stays exactly that. The
|
|
1223
|
+
* long one carries the 4.3 lists beside the attachments and is recognised by its
|
|
1224
|
+
* own keys (`RIG_SKIN_KEYS`); see `splitRigSkin` for the one ambiguity that
|
|
1225
|
+
* creates and how it is refused rather than guessed.
|
|
1226
|
+
*/
|
|
1227
|
+
export type RigSkin = RigSkinAttachments | RigSkinEntry;
|
|
1228
|
+
|
|
1229
|
+
/** The two halves of a skin entry, whichever spelling the spec used. */
|
|
1230
|
+
export interface RigSkinParts {
|
|
1231
|
+
attachments: RigSkinAttachments;
|
|
1232
|
+
bones: string[];
|
|
1233
|
+
constraints: Record<RigSkinConstraintKey, string[]>;
|
|
1234
|
+
/** True when the spec used the long form. Only the messages care. */
|
|
1235
|
+
explicit: boolean;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Normalise one skin entry.
|
|
1240
|
+
*
|
|
1241
|
+
* ⚠️ The two spellings are told apart by the keys in `RIG_SKIN_KEYS`: a skin
|
|
1242
|
+
* that uses any of them is the long form. That is the one thing here that could
|
|
1243
|
+
* ever be ambiguous, and it is ambiguous in exactly one case — a rig with a SLOT
|
|
1244
|
+
* of one of those names — so rigc does not guess: `parseRigSpec` refuses such a
|
|
1245
|
+
* slot by name, because the alternative is a member list read as a slot's
|
|
1246
|
+
* placeholder table or the other way round.
|
|
1247
|
+
*
|
|
1248
|
+
* In the long form EVERY key must be one of them. A key outside the set is
|
|
1249
|
+
* almost always a slot name left behind by a half-finished conversion from the
|
|
1250
|
+
* short form, so it is refused with that as the message rather than ignored — an
|
|
1251
|
+
* ignored slot is an attachment that vanishes.
|
|
1252
|
+
*/
|
|
1253
|
+
export function splitRigSkin(skin: RigSkin, where: string): RigSkinParts {
|
|
1254
|
+
const empty = (): Record<RigSkinConstraintKey, string[]> => ({
|
|
1255
|
+
ik: [],
|
|
1256
|
+
transform: [],
|
|
1257
|
+
path: [],
|
|
1258
|
+
physics: [],
|
|
1259
|
+
slider: [],
|
|
1260
|
+
});
|
|
1261
|
+
if (!isObj(skin)) throw new CompileError(`${where}: a skin must be an object`);
|
|
1262
|
+
const known = new Set<string>(RIG_SKIN_KEYS);
|
|
1263
|
+
const keys = Object.keys(skin);
|
|
1264
|
+
if (!keys.some((key) => known.has(key))) {
|
|
1265
|
+
return { attachments: skin as RigSkinAttachments, bones: [], constraints: empty(), explicit: false };
|
|
1266
|
+
}
|
|
1267
|
+
const entry = skin as RigSkinEntry;
|
|
1268
|
+
if (entry.attachments !== undefined && !isObj(entry.attachments)) {
|
|
1269
|
+
throw new CompileError(`${where}: "attachments" is \`slotName -> placeholderName -> attachment\`, not ${JSON.stringify(entry.attachments)}`);
|
|
1270
|
+
}
|
|
1271
|
+
for (const key of keys) {
|
|
1272
|
+
if (known.has(key)) continue;
|
|
1273
|
+
throw new CompileError(
|
|
1274
|
+
`${where}: uses the long form (it declares ${keys.filter((k) => known.has(k)).map((k) => `"${k}"`).join(', ')}) ` +
|
|
1275
|
+
`and also has a key "${key}". In that form every key is one of ${RIG_SKIN_KEYS.join(', ')} — so "${key}" reads ` +
|
|
1276
|
+
'as neither a member list nor a slot, and a slot left outside the block is an attachment that vanishes. ' +
|
|
1277
|
+
'Move it inside "attachments".',
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
const nameList = (value: unknown, field: string): string[] => {
|
|
1281
|
+
if (value === undefined) return [];
|
|
1282
|
+
if (!Array.isArray(value)) throw new CompileError(`${where}: "${field}" must be an array of names, not ${JSON.stringify(value)}`);
|
|
1283
|
+
return value.map((name, i) => {
|
|
1284
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
1285
|
+
throw new CompileError(`${where}: ${field}[${i}] is ${JSON.stringify(name)}; a skin lists bones and constraints BY NAME`);
|
|
1286
|
+
}
|
|
1287
|
+
return name;
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
const constraints = empty();
|
|
1291
|
+
for (const key of RIG_SKIN_CONSTRAINT_KEYS) constraints[key] = nameList(entry[key], key);
|
|
1292
|
+
return {
|
|
1293
|
+
attachments: entry.attachments ?? {},
|
|
1294
|
+
bones: nameList(entry.bones, 'bones'),
|
|
1295
|
+
constraints,
|
|
1296
|
+
explicit: true,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ---------------------------------------------------------------------------
|
|
1301
|
+
// constraints — `root.constraints[]` (SkeletonJson.ts:144-369), the 4.3 shape
|
|
1302
|
+
// ---------------------------------------------------------------------------
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* A constraint's identity in this format: its KIND and its name, never the name
|
|
1306
|
+
* alone (issue #692).
|
|
1307
|
+
*
|
|
1308
|
+
* `SkeletonData.findConstraint(name, type)` tests `constraint instanceof type`
|
|
1309
|
+
* before it compares the name, so `ik` `leg` and `transform` `leg` are two
|
|
1310
|
+
* objects and every resolution in the format — a timeline group, a skin's member
|
|
1311
|
+
* list, a slider's animation pass — reaches exactly one of them. It doubles as
|
|
1312
|
+
* the phrase a refusal uses, so the key a lookup misses on and the words the
|
|
1313
|
+
* message says it missed on cannot drift apart.
|
|
1314
|
+
*
|
|
1315
|
+
* @internal
|
|
1316
|
+
*/
|
|
1317
|
+
export function constraintAt(type: string, name: string): string {
|
|
1318
|
+
return `${type} constraint "${name}"`;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* 4.3 folds every constraint into ONE array with a `type` discriminator. The
|
|
1323
|
+
* 4.1/4.2 shape — top-level `ik`/`transform`/`path`/`physics` arrays — still
|
|
1324
|
+
* loads clean and the constraints simply vanish, which is `A01`.
|
|
1325
|
+
*
|
|
1326
|
+
* 🚨 An entry whose `type` matches no case is dropped with no error and no
|
|
1327
|
+
* `default:` branch (`:148-367`). rigc therefore refuses an unknown `type` by
|
|
1328
|
+
* name rather than passing it through.
|
|
1329
|
+
*/
|
|
1330
|
+
export interface RigConstraintCommon {
|
|
1331
|
+
name: string;
|
|
1332
|
+
/**
|
|
1333
|
+
* Default false → `skinRequired` (`:147`): the constraint does not run unless
|
|
1334
|
+
* the applied skin lists it under its own type (see `RigSkinEntry`). Half a
|
|
1335
|
+
* switch on its own, so rigc refuses the flag without a skin that activates it.
|
|
1336
|
+
*/
|
|
1337
|
+
skin?: boolean;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* `ScaleYMode` (`ConstraintData.ts:37-45`), which an **ik** and a **physics**
|
|
1342
|
+
* constraint both carry under the JSON key `scaleY`.
|
|
1343
|
+
*
|
|
1344
|
+
* ⚠️ Resolved by `Utils.enumValue`, so only the first letter's case is free and
|
|
1345
|
+
* an unresolved name is assigned as `undefined` without a word — the hazard
|
|
1346
|
+
* `RIG_PATH_POSITION_MODES` is checked for, at a field that had no check at all.
|
|
1347
|
+
*/
|
|
1348
|
+
export const RIG_SCALE_Y_MODES = ['None', 'Uniform', 'Volume'] as const;
|
|
1349
|
+
|
|
1350
|
+
/** The three names, as a rig spec writes them. */
|
|
1351
|
+
export type RigScaleYMode = 'none' | 'uniform' | 'volume' | 'None' | 'Uniform' | 'Volume';
|
|
1352
|
+
|
|
1353
|
+
/** `type: "ik"` (`:149-176`). `scaleY` is 4.3's replacement for 4.2's `uniform`. */
|
|
1354
|
+
export interface RigIkConstraint extends RigConstraintCommon {
|
|
1355
|
+
type: 'ik';
|
|
1356
|
+
/**
|
|
1357
|
+
* One bone, or two where the second's parent is the first; resolved by
|
|
1358
|
+
* name, and a miss throws in the parser. Any other shape is refused by name
|
|
1359
|
+
* (issue #1205): the runtime applies nothing for three or more, and solves
|
|
1360
|
+
* a pair through the first bone's matrix from the second's local offset, so
|
|
1361
|
+
* a bone standing between them is left out of the triangle it solves.
|
|
1362
|
+
*/
|
|
1363
|
+
bones: string[];
|
|
1364
|
+
target: string;
|
|
1365
|
+
/** `ConstraintData.ts:50`. Absent → `None`. */
|
|
1366
|
+
scaleY?: RigScaleYMode;
|
|
1367
|
+
/** Default 1. */
|
|
1368
|
+
mix?: number;
|
|
1369
|
+
/** Default 0. */
|
|
1370
|
+
softness?: number;
|
|
1371
|
+
/** Default true → `bendDirection = ±1`. */
|
|
1372
|
+
bendPositive?: boolean;
|
|
1373
|
+
/** Default false. */
|
|
1374
|
+
compress?: boolean;
|
|
1375
|
+
stretch?: boolean;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* One entry of a transform constraint's `properties` map: which source property
|
|
1380
|
+
* drives which target properties, and by how much (`:241`, `:521`).
|
|
1381
|
+
*
|
|
1382
|
+
* The `from` and `to` names are drawn from a fixed six — `rotate`, `x`, `y`,
|
|
1383
|
+
* `scaleX`, `scaleY`, `shearY` — and **anything else throws in the parser**.
|
|
1384
|
+
*/
|
|
1385
|
+
export interface RigTransformProperty {
|
|
1386
|
+
offset?: number;
|
|
1387
|
+
to: Record<string, RigTransformTo>;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/** One driven property of a `RigTransformProperty.to` map. */
|
|
1391
|
+
export interface RigTransformTo {
|
|
1392
|
+
offset?: number;
|
|
1393
|
+
max?: number;
|
|
1394
|
+
scale?: number;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** `type: "transform"` (`:177-268`) — rebuilt from scratch in 4.3. */
|
|
1398
|
+
export interface RigTransformConstraint extends RigConstraintCommon {
|
|
1399
|
+
type: 'transform';
|
|
1400
|
+
bones: string[];
|
|
1401
|
+
/** 4.2 called this `target`. */
|
|
1402
|
+
source: string;
|
|
1403
|
+
localSource?: boolean;
|
|
1404
|
+
localTarget?: boolean;
|
|
1405
|
+
additive?: boolean;
|
|
1406
|
+
clamp?: boolean;
|
|
1407
|
+
/** `fromName -> { offset, to: { toName -> { offset, max, scale } } }`. */
|
|
1408
|
+
properties?: Record<string, RigTransformProperty>;
|
|
1409
|
+
/** The offsets array. Default 0 each. */
|
|
1410
|
+
rotation?: number;
|
|
1411
|
+
x?: number;
|
|
1412
|
+
y?: number;
|
|
1413
|
+
scaleX?: number;
|
|
1414
|
+
scaleY?: number;
|
|
1415
|
+
shearY?: number;
|
|
1416
|
+
/**
|
|
1417
|
+
* Default 1. ⚠️ Each mix is read **only if the matching `to` property was
|
|
1418
|
+
* declared** (`:259-264`), so a mix without its property is dead data.
|
|
1419
|
+
*/
|
|
1420
|
+
mixRotate?: number;
|
|
1421
|
+
mixX?: number;
|
|
1422
|
+
/** Defaults to `mixX`. */
|
|
1423
|
+
mixY?: number;
|
|
1424
|
+
mixScaleX?: number;
|
|
1425
|
+
/** Defaults to `mixScaleX`. */
|
|
1426
|
+
mixScaleY?: number;
|
|
1427
|
+
mixShearY?: number;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* `type: "physics"` (`:301-339`), 4.2+.
|
|
1432
|
+
*
|
|
1433
|
+
* ⚠️ The five components all default to 0, so a constraint that names none of
|
|
1434
|
+
* them parses cleanly and does absolutely nothing — `A23`.
|
|
1435
|
+
*/
|
|
1436
|
+
export interface RigPhysicsConstraint extends RigConstraintCommon {
|
|
1437
|
+
type: 'physics';
|
|
1438
|
+
bone: string;
|
|
1439
|
+
/** The components. All zero = a constraint that parses and does nothing. */
|
|
1440
|
+
x?: number;
|
|
1441
|
+
y?: number;
|
|
1442
|
+
rotate?: number;
|
|
1443
|
+
scaleX?: number;
|
|
1444
|
+
shearX?: number;
|
|
1445
|
+
/**
|
|
1446
|
+
* 4.3; absent → `ScaleYMode.None`. Same field, same enum and same spelling as
|
|
1447
|
+
* `RigIkConstraint.scaleY`.
|
|
1448
|
+
*
|
|
1449
|
+
* 🚨 It was called `scaleYMode` from this file's first commit (c0e9944,
|
|
1450
|
+
* 2026-08-22) until issue #545, and the name was not a synonym — it was the
|
|
1451
|
+
* one key in this file that no code anywhere read.
|
|
1452
|
+
* `SkeletonJson.js:299` is `getValue(constraintMap, "scaleY", null)`, so the
|
|
1453
|
+
* emitter copied `scaleY`, an author writing TypeScript against this interface
|
|
1454
|
+
* got a type error on the key that works and silence on the key that does
|
|
1455
|
+
* nothing, and `scaleYMode` occurred exactly once in the whole tree: here.
|
|
1456
|
+
*
|
|
1457
|
+
* ⭐ The rename rather than teaching the emitter to read `scaleYMode`, and the
|
|
1458
|
+
* argument is not "the format's name wins" in the abstract — the tree had
|
|
1459
|
+
* already answered it four hundred lines above. `RigIkConstraint.scaleY`
|
|
1460
|
+
* carries the *same* `ScaleYMode` enum through the *same* `Utils.enumValue`
|
|
1461
|
+
* call under the *same* JSON key, and spells it `scaleY`. Teaching the emitter
|
|
1462
|
+
* the other name would have left one Spine enum with two rig-spec spellings
|
|
1463
|
+
* chosen by constraint type, which is a worse format than either name alone.
|
|
1464
|
+
*/
|
|
1465
|
+
scaleY?: RigScaleYMode;
|
|
1466
|
+
/** Default 5000. */
|
|
1467
|
+
limit?: number;
|
|
1468
|
+
/** Default 60 → `step = 1/fps`. */
|
|
1469
|
+
fps?: number;
|
|
1470
|
+
/** Defaults: 0.5 / 100 / 0.85 / 1 / 0 / 0 / 1. */
|
|
1471
|
+
inertia?: number;
|
|
1472
|
+
strength?: number;
|
|
1473
|
+
damping?: number;
|
|
1474
|
+
/** Stored as `massInverse = 1/mass`, so 0 becomes Infinity — `A23`. */
|
|
1475
|
+
mass?: number;
|
|
1476
|
+
wind?: number;
|
|
1477
|
+
gravity?: number;
|
|
1478
|
+
mix?: number;
|
|
1479
|
+
inertiaGlobal?: boolean;
|
|
1480
|
+
strengthGlobal?: boolean;
|
|
1481
|
+
dampingGlobal?: boolean;
|
|
1482
|
+
massGlobal?: boolean;
|
|
1483
|
+
windGlobal?: boolean;
|
|
1484
|
+
gravityGlobal?: boolean;
|
|
1485
|
+
mixGlobal?: boolean;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* The three enums a path constraint chooses its model with, spelled as the
|
|
1490
|
+
* runtime's own enum members (`PathConstraintData.ts:77-87`).
|
|
1491
|
+
*
|
|
1492
|
+
* 🚨 They are checked, and this is one of the places where checking matters most:
|
|
1493
|
+
* `Utils.enumValue` is `type[name[0].toUpperCase() + name.slice(1)]`, so a name
|
|
1494
|
+
* outside the set resolves to **`undefined`** and is assigned without complaint.
|
|
1495
|
+
* The constraint then behaves as some *other* mode — an unknown `spacingMode`
|
|
1496
|
+
* fails the `=== Length` test and spaces bones as though `Fixed` had been asked
|
|
1497
|
+
* for; an unknown `rotateMode` is neither `Tangent` nor `ChainScale`, so bones
|
|
1498
|
+
* follow the path and never turn along it. Both load, both animate, neither is
|
|
1499
|
+
* what was written. Only the first letter's case is free, because that is exactly
|
|
1500
|
+
* what `enumValue` normalises.
|
|
1501
|
+
*/
|
|
1502
|
+
export const RIG_PATH_POSITION_MODES = ['Fixed', 'Percent'] as const;
|
|
1503
|
+
export const RIG_PATH_SPACING_MODES = ['Length', 'Fixed', 'Percent', 'Proportional'] as const;
|
|
1504
|
+
export const RIG_PATH_ROTATE_MODES = ['Tangent', 'Chain', 'ChainScale'] as const;
|
|
1505
|
+
|
|
1506
|
+
/** The six property names a slider or a transform constraint may read (`:241`, `:521`). */
|
|
1507
|
+
export const RIG_FROM_PROPERTIES = ['rotate', 'x', 'y', 'scaleX', 'scaleY', 'shearY'] as const;
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* `type: "path"` (`:269-300`).
|
|
1511
|
+
*
|
|
1512
|
+
* **When you need one:** anything that travels — a cart along a track, a fish
|
|
1513
|
+
* along a current, a chain of links wrapping a pulley. The constraint takes a
|
|
1514
|
+
* `RigPathAttachment` off `slot` and slides `bones` along it, so one `position`
|
|
1515
|
+
* key moves the whole train and the shape of the motion lives in the curve
|
|
1516
|
+
* rather than in the keys.
|
|
1517
|
+
*
|
|
1518
|
+
* ⚠️ `slot` must be a slot that carries a path attachment. `PathConstraint.update`
|
|
1519
|
+
* begins `if (!(attachment instanceof PathAttachment)) return` — so a constraint
|
|
1520
|
+
* aimed at a slot showing a region does nothing at all, with no error anywhere.
|
|
1521
|
+
* rigc refuses a slot that has no path attachment in any skin.
|
|
1522
|
+
*/
|
|
1523
|
+
export interface RigPathConstraint extends RigConstraintCommon {
|
|
1524
|
+
type: 'path';
|
|
1525
|
+
/** At least one, in the order they ride the path. Resolved by name. */
|
|
1526
|
+
bones: string[];
|
|
1527
|
+
/** The slot whose path attachment the bones follow. Required by the parser. */
|
|
1528
|
+
slot: string;
|
|
1529
|
+
/** Default `"Percent"`: `position` 0..1 along the path rather than in world units. */
|
|
1530
|
+
positionMode?: (typeof RIG_PATH_POSITION_MODES)[number] | 'fixed' | 'percent';
|
|
1531
|
+
/** Default `"Length"`: spacing measured in each bone's own length. */
|
|
1532
|
+
spacingMode?: (typeof RIG_PATH_SPACING_MODES)[number] | 'length' | 'fixed' | 'percent' | 'proportional';
|
|
1533
|
+
/** Default `"Tangent"`: each bone turns to the path's tangent where it sits. */
|
|
1534
|
+
rotateMode?: (typeof RIG_PATH_ROTATE_MODES)[number] | 'tangent' | 'chain' | 'chainScale';
|
|
1535
|
+
/** Default 0 → `offsetRotation`, degrees added after the path's own rotation. */
|
|
1536
|
+
rotation?: number;
|
|
1537
|
+
/** Default 0. Where the first bone sits: 0..1 under `Percent`, world units under `Fixed`. */
|
|
1538
|
+
position?: number;
|
|
1539
|
+
/** Default 0. Gap between bones, in the unit `spacingMode` chooses. */
|
|
1540
|
+
spacing?: number;
|
|
1541
|
+
/** Default 1. */
|
|
1542
|
+
mixRotate?: number;
|
|
1543
|
+
mixX?: number;
|
|
1544
|
+
/** Defaults to `mixX` (`:283`). */
|
|
1545
|
+
mixY?: number;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* `type: "slider"` (`:340-366`) — 4.3's own constraint, and the only one that
|
|
1550
|
+
* applies an **animation** rather than a transform.
|
|
1551
|
+
*
|
|
1552
|
+
* **When you need one:** a pose that has to be driven by a value instead of by
|
|
1553
|
+
* time — a dial that opens a door, a blend shape on a face, a suspension that
|
|
1554
|
+
* compresses as the wheel rises. `animation` is applied at a time the slider
|
|
1555
|
+
* chooses, `mix` is its authority, and everything that animation keys is under
|
|
1556
|
+
* its control while it is on.
|
|
1557
|
+
*
|
|
1558
|
+
* The time comes from one of two models and `bone` is the switch (`:350`):
|
|
1559
|
+
*
|
|
1560
|
+
* - **property-driven** — with a `bone`, the slider reads one transform
|
|
1561
|
+
* `property` off it and maps it to a time: `time = to + (value - from) * scale`.
|
|
1562
|
+
* This is the dial.
|
|
1563
|
+
* - **time-driven** — with no `bone`, `time` is the slider's own setup value and
|
|
1564
|
+
* an `animations.<a>.slider.<name>.time` timeline keys it.
|
|
1565
|
+
*
|
|
1566
|
+
* ⚠️ `animation` is resolved in a **second pass** over the constraints array,
|
|
1567
|
+
* after the animations are read (`:495-507`), and a miss **throws**
|
|
1568
|
+
* `Slider animation not found`. It names an animation in the MOTION spec — the
|
|
1569
|
+
* one place a rig spec points across the file boundary, and the mirror of
|
|
1570
|
+
* `events`, where the rig declares a name the motion spec fires.
|
|
1571
|
+
*/
|
|
1572
|
+
export interface RigSliderConstraint extends RigConstraintCommon {
|
|
1573
|
+
type: 'slider';
|
|
1574
|
+
/** An animation the motion spec declares. Required: a miss throws in the parser. */
|
|
1575
|
+
animation: string;
|
|
1576
|
+
/** Default 1. The slider's authority over what its animation keys. */
|
|
1577
|
+
mix?: number;
|
|
1578
|
+
/** Default false. Add the animation to the current pose instead of overwriting it. */
|
|
1579
|
+
additive?: boolean;
|
|
1580
|
+
/**
|
|
1581
|
+
* Default false. Repeat past the animation's duration instead of holding the
|
|
1582
|
+
* last frame. ⚠️ With a `bone`, `loop` divides by the animation's duration
|
|
1583
|
+
* (`Slider.ts:63-64`), so looping a zero-length animation yields a NaN time.
|
|
1584
|
+
*/
|
|
1585
|
+
loop?: boolean;
|
|
1586
|
+
/** The driving bone. Its presence switches the whole model — see above. */
|
|
1587
|
+
bone?: string;
|
|
1588
|
+
/** Which of the six transform properties to read. Required when `bone` is set. */
|
|
1589
|
+
property?: (typeof RIG_FROM_PROPERTIES)[number];
|
|
1590
|
+
/** Default 0. The property value that maps to time `to`. */
|
|
1591
|
+
from?: number;
|
|
1592
|
+
/** Default 0. The time `from` maps to. */
|
|
1593
|
+
to?: number;
|
|
1594
|
+
/** Default 1. Seconds of animation per unit of the property. */
|
|
1595
|
+
scale?: number;
|
|
1596
|
+
/** Default 0. Nonessential: the editor's top of the slider's range. */
|
|
1597
|
+
max?: number;
|
|
1598
|
+
/** Default false. Read the bone's local transform instead of its world one. */
|
|
1599
|
+
local?: boolean;
|
|
1600
|
+
/** The setup time, for the time-driven model. Read only when `bone` is absent. */
|
|
1601
|
+
time?: number;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
export type RigConstraint =
|
|
1605
|
+
| RigIkConstraint
|
|
1606
|
+
| RigTransformConstraint
|
|
1607
|
+
| RigPathConstraint
|
|
1608
|
+
| RigPhysicsConstraint
|
|
1609
|
+
| RigSliderConstraint;
|
|
1610
|
+
|
|
1611
|
+
// ---------------------------------------------------------------------------
|
|
1612
|
+
// events — `root.events` (SkeletonJson.ts:469-484), an OBJECT, not an array
|
|
1613
|
+
// ---------------------------------------------------------------------------
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* One event **definition**: a name the skeleton owns, plus the payload a firing
|
|
1617
|
+
* carries when the animation does not override it.
|
|
1618
|
+
*
|
|
1619
|
+
* ⭐ The declaration lives in the rig spec and the firings live in the motion
|
|
1620
|
+
* spec, for the same reason slots live here and their colour keys live there:
|
|
1621
|
+
* the name is structure — the runtime looks it up, the game listens for it —
|
|
1622
|
+
* and *when* it fires is time. `skeletonData.findEvent` resolves an animation's
|
|
1623
|
+
* key against this table and **throws** on a miss (`:1244`), so an animation
|
|
1624
|
+
* that names an event nobody declared does not load at all. rigc refuses it at
|
|
1625
|
+
* compile instead, where the message can name the file that has to change.
|
|
1626
|
+
*
|
|
1627
|
+
* ⚠️ `volume` and `balance` are read **only when `audio` is set** (`:478-481`).
|
|
1628
|
+
* Declared without one they are dropped in silence, so rigc refuses that pairing
|
|
1629
|
+
* rather than emitting two numbers the runtime will never look at.
|
|
1630
|
+
*/
|
|
1631
|
+
export interface RigEvent {
|
|
1632
|
+
/** Default 0. The `int` payload every firing inherits unless it overrides it. */
|
|
1633
|
+
int?: number;
|
|
1634
|
+
/** Default 0. */
|
|
1635
|
+
float?: number;
|
|
1636
|
+
/** Default `""`. */
|
|
1637
|
+
string?: string;
|
|
1638
|
+
/**
|
|
1639
|
+
* Audio path the editor recorded for this event. Nonessential to playback —
|
|
1640
|
+
* no runtime here loads it — but it is what makes `volume`/`balance` legible.
|
|
1641
|
+
*/
|
|
1642
|
+
audio?: string;
|
|
1643
|
+
/** Only read when `audio` is set. */
|
|
1644
|
+
volume?: number;
|
|
1645
|
+
balance?: number;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// ---------------------------------------------------------------------------
|
|
1649
|
+
// invariants — what skeleton JSON cannot say about itself
|
|
1650
|
+
// ---------------------------------------------------------------------------
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Structural facts the emitted artifact does not record, handed to the validator
|
|
1654
|
+
* so its archetype assertions have something to check instead of a guess.
|
|
1655
|
+
*
|
|
1656
|
+
* These are the fields that used to be properties of a hard-coded formation.
|
|
1657
|
+
* They are optional, and an assertion whose field is absent reports **SKIP** —
|
|
1658
|
+
* never a pass, because an assertion with nothing to look at has not looked.
|
|
1659
|
+
*/
|
|
1660
|
+
export interface RigInvariants {
|
|
1661
|
+
/**
|
|
1662
|
+
* How many slots of this rig may carry a mesh. A budget, not a Spine rule:
|
|
1663
|
+
* every mesh is a canvas that re-rasterises whenever a bone driving it moves.
|
|
1664
|
+
*/
|
|
1665
|
+
meshSlots?: number;
|
|
1666
|
+
/**
|
|
1667
|
+
* How many triangles one of those meshes may carry. Also a budget, and also
|
|
1668
|
+
* not a Spine rule — the editor's own example projects ship meshes several
|
|
1669
|
+
* times this size and they are perfectly valid.
|
|
1670
|
+
*
|
|
1671
|
+
* ⚠️ Declare it or `A13_MESH_BUDGET` has nothing to measure against and SKIPs.
|
|
1672
|
+
* A number baked into the validator would be one project's frame time
|
|
1673
|
+
* masquerading as a property of the format, and would fail every foreign
|
|
1674
|
+
* skeleton that is merely denser than that project can afford.
|
|
1675
|
+
*/
|
|
1676
|
+
meshTriangles?: number;
|
|
1677
|
+
/**
|
|
1678
|
+
* The bone whose setup rotation carries the cut's insertion axis. Its subtree
|
|
1679
|
+
* is authored in **axis space** — translateX only — which is what lets one set
|
|
1680
|
+
* of keys move to a cut at another camera angle (`A24`).
|
|
1681
|
+
*/
|
|
1682
|
+
axisBone?: string;
|
|
1683
|
+
/**
|
|
1684
|
+
* The bone carrying the inserting mass. Its own inward keys spend the same
|
|
1685
|
+
* clearance the stroke does, so `A29`/`A30` add them together.
|
|
1686
|
+
*/
|
|
1687
|
+
massBone?: string;
|
|
1688
|
+
/** Parentage that must never happen, with the reason it is tempting (`A25`). */
|
|
1689
|
+
detached?: RigDetachedRule[];
|
|
1690
|
+
/**
|
|
1691
|
+
* Mesh slots whose `deform` timelines are allowed to turn a triangle inside
|
|
1692
|
+
* out, exempting them from `A39_DEFORM_KEEPS_TRIANGLE_WINDING`.
|
|
1693
|
+
*
|
|
1694
|
+
* 🚨 **This is an opt-OUT, and the default is gated.** A39's whole value is
|
|
1695
|
+
* that a fold is caught without anybody suspecting one, so an author who has
|
|
1696
|
+
* not thought about folding gets the check. What the field exists for is the
|
|
1697
|
+
* art that folds on purpose — a page turning over, a cloth creasing back on
|
|
1698
|
+
* itself — where the reversed winding IS the drawing and refusing it would be
|
|
1699
|
+
* refusing correct work.
|
|
1700
|
+
*
|
|
1701
|
+
* ⚠️ `why` is **required**, and empty is refused by name. An exemption with no
|
|
1702
|
+
* reason is the failure mode this field would otherwise introduce: somebody
|
|
1703
|
+
* declares a slot to get a green build, and the next reader cannot tell a
|
|
1704
|
+
* deliberate page turn from a defect that was waved through. The one exemption
|
|
1705
|
+
* in this repository (`gallery/flex`'s leaf) says outright that it is the
|
|
1706
|
+
* second kind, and names the issue.
|
|
1707
|
+
*/
|
|
1708
|
+
deformMayFold?: RigDeformFoldExemption[];
|
|
1709
|
+
/**
|
|
1710
|
+
* Ik and transform constraints whose mix **the consumer sets**, from code,
|
|
1711
|
+
* rather than any animation in this file — so `A47` / `A48` do not refuse
|
|
1712
|
+
* them for resting muted with nothing keying them up (issue #784).
|
|
1713
|
+
*
|
|
1714
|
+
* 🔑 **The file cannot say this about itself.** A constraint resting at 0
|
|
1715
|
+
* that no animation keys above 0 is either a leftover that moves nothing or a
|
|
1716
|
+
* dial a game turns on at runtime, and the two export as the same bytes. The
|
|
1717
|
+
* gate refuses the shape because nothing *in the file* ever moves it; this is
|
|
1718
|
+
* the statement that something outside it does. It is the rig-side spelling of
|
|
1719
|
+
* `gallery/look`'s rule that a face angle is a value rather than a time: the
|
|
1720
|
+
* object offers the dial and the consumer decides when it turns.
|
|
1721
|
+
*
|
|
1722
|
+
* 🚨 **An opt-OUT, held to `deformMayFold`'s standard.** Each entry names the
|
|
1723
|
+
* constraint AND its `type` — names are unique per kind (issue #692), so a
|
|
1724
|
+
* name alone could mean an ik and a transform at once — and a `why`, required
|
|
1725
|
+
* and non-blank. A name that resolves to nothing, a kind that has no such
|
|
1726
|
+
* rule (a path, physics or slider constraint: `A36`, `A23` and `A37` read no
|
|
1727
|
+
* declaration, so the entry would exempt nothing), a repeat and a blank `why`
|
|
1728
|
+
* are each refused by name. A declared constraint that the file itself
|
|
1729
|
+
* switches on — resting live, or keyed above 0 — is refused at the gate, for
|
|
1730
|
+
* the same reason: the declaration would exempt nothing.
|
|
1731
|
+
*
|
|
1732
|
+
* ⚠️ What it buys is a SKIP, never a pass: a declared constraint is not
|
|
1733
|
+
* measured, and `A47`/`A48` say so by name when nothing else of that kind is
|
|
1734
|
+
* left to measure, and on the build's stats line when something is.
|
|
1735
|
+
*/
|
|
1736
|
+
consumerDrivenMix?: RigConsumerDrivenMix[];
|
|
1737
|
+
/**
|
|
1738
|
+
* The rig's `idle` deforms meshes **on purpose**, so
|
|
1739
|
+
* `A15_IDLE_NO_MESH_BONE_KEYS` reports the renderer cost instead of refusing
|
|
1740
|
+
* each keyed bone (issues #855, #858).
|
|
1741
|
+
*
|
|
1742
|
+
* 🔑 **What A15 assumes, and the genre that breaks it.** The rule exists for a
|
|
1743
|
+
* renderer that skips redrawing a mesh nothing moved, so an `idle` keying a
|
|
1744
|
+
* mesh-driving bone spends that skip on every frame — right for a rig whose
|
|
1745
|
+
* meshes are mostly static, wrong for a painting rig: one illustration
|
|
1746
|
+
* decomposed into layers, most of them weighted meshes over bone chains, with
|
|
1747
|
+
* an `idle` whose whole job is to move them (hair, sleeves, breathing). The
|
|
1748
|
+
* only way through without this field was a same-origin parent above every
|
|
1749
|
+
* keyed bone — 42 extra bones on the first such rig, the same pose, the rule's
|
|
1750
|
+
* wording met and its purpose not.
|
|
1751
|
+
*
|
|
1752
|
+
* 🚨 **An opt-OUT, held to `deformMayFold`'s standard.** The shape is
|
|
1753
|
+
* `{ "why": … }` and a missing, blank or non-string `why` is refused by name.
|
|
1754
|
+
* What it buys is a **SKIP, never a pass**, and the SKIP carries the cost —
|
|
1755
|
+
* the keyed bones, how many mesh attachments they drive and how many vertices
|
|
1756
|
+
* those hold. A declaration on a rig whose `idle` keys no mesh-driving bone is
|
|
1757
|
+
* refused at the gate: it would switch off nothing while reading like it did.
|
|
1758
|
+
*/
|
|
1759
|
+
idleDrivesMeshes?: RigIdleDrivesMeshes;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/** The rig's `idle` is meant to deform meshes — `invariants.idleDrivesMeshes` (`A15`). */
|
|
1763
|
+
export interface RigIdleDrivesMeshes {
|
|
1764
|
+
/** Required, and blank is refused by name — see `idleDrivesMeshes`. */
|
|
1765
|
+
why: string;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/** One constraint whose mix the consumer drives — `invariants.consumerDrivenMix`. */
|
|
1769
|
+
export interface RigConsumerDrivenMix {
|
|
1770
|
+
/** The constraint's `name`. */
|
|
1771
|
+
constraint: string;
|
|
1772
|
+
/** Its `type`: `ik` or `transform`, the two kinds `A47`/`A48` read the declaration for. */
|
|
1773
|
+
type: 'ik' | 'transform';
|
|
1774
|
+
/** Required, and blank is refused by name — see `consumerDrivenMix`. */
|
|
1775
|
+
why: string;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/** One forbidden parentage — `invariants.detached` (`A25`). */
|
|
1779
|
+
export interface RigDetachedRule {
|
|
1780
|
+
bone: string;
|
|
1781
|
+
notUnder: string;
|
|
1782
|
+
why?: string;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/** One slot exempted from `A39` — `invariants.deformMayFold`. */
|
|
1786
|
+
export interface RigDeformFoldExemption {
|
|
1787
|
+
slot: string;
|
|
1788
|
+
/** Required, and empty is refused by name — see `deformMayFold`. */
|
|
1789
|
+
why: string;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// ---------------------------------------------------------------------------
|
|
1793
|
+
// the file
|
|
1794
|
+
// ---------------------------------------------------------------------------
|
|
1795
|
+
|
|
1796
|
+
export interface RigSpec {
|
|
1797
|
+
spec: 'rigc-rig/1';
|
|
1798
|
+
/**
|
|
1799
|
+
* The rig's own name. A motion spec's `archetype` field must equal it: the
|
|
1800
|
+
* spec was authored against one formation, and pairing it with a different rig
|
|
1801
|
+
* silently produces keys aimed at bones that mean something else.
|
|
1802
|
+
*/
|
|
1803
|
+
name: string;
|
|
1804
|
+
note?: string;
|
|
1805
|
+
skeleton?: RigSkeletonHeader;
|
|
1806
|
+
/**
|
|
1807
|
+
* Base directory for every `image` in this file, relative to the rig file
|
|
1808
|
+
* itself. The CLI's `--images <dir>` overrides it (and is then relative to the
|
|
1809
|
+
* working directory), which is how a foreign corpus is compiled without
|
|
1810
|
+
* editing its rig spec.
|
|
1811
|
+
*/
|
|
1812
|
+
images?: string;
|
|
1813
|
+
bones: RigBone[];
|
|
1814
|
+
slots: RigSlot[];
|
|
1815
|
+
/**
|
|
1816
|
+
* `default`, if the rig has one, becomes `skeletonData.defaultSkin` (`:441`).
|
|
1817
|
+
* It is emitted exactly when this map carries the key — an empty `{}` included
|
|
1818
|
+
* — or a manifest part files its states under it; a rig whose art is all in
|
|
1819
|
+
* named skins states no `default` and gets none, which is what the editor's
|
|
1820
|
+
* export of such a rig declares (issue #801).
|
|
1821
|
+
*
|
|
1822
|
+
* Each entry is either the short form — `slotName -> placeholderName ->
|
|
1823
|
+
* attachment` — or the long one, `{ "attachments": {…}, "bones": [...],
|
|
1824
|
+
* "ik": [...] }`, which also says which bones and constraints the skin
|
|
1825
|
+
* activates. `splitRigSkin` normalises the two.
|
|
1826
|
+
*/
|
|
1827
|
+
skins?: Record<string, RigSkin>;
|
|
1828
|
+
constraints?: RigConstraint[];
|
|
1829
|
+
/**
|
|
1830
|
+
* `eventName -> payload defaults`. Emitted as `root.events`, which is an
|
|
1831
|
+
* OBJECT keyed by name and not an array. The motion spec's per-animation
|
|
1832
|
+
* `events` timeline fires them; a firing whose name is not a key here is a
|
|
1833
|
+
* compile error, because the parser throws on it at load.
|
|
1834
|
+
*/
|
|
1835
|
+
events?: Record<string, RigEvent>;
|
|
1836
|
+
invariants?: RigInvariants;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// ---------------------------------------------------------------------------
|
|
1840
|
+
// reading
|
|
1841
|
+
// ---------------------------------------------------------------------------
|
|
1842
|
+
|
|
1843
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
1844
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Every key each shape of this format owns, keyed by the interface above that
|
|
1849
|
+
* declares it — the runtime shadow of the types, which TypeScript erases.
|
|
1850
|
+
*
|
|
1851
|
+
* 🔒 **Hand-written and mechanically held to the interfaces.** `CUR17` in
|
|
1852
|
+
* `selftest.ts` reads this file's own source, extracts each named interface's
|
|
1853
|
+
* field list and compares it to the entry here, so the pair cannot drift: adding
|
|
1854
|
+
* a field and forgetting this table is a red run, not a key an author cannot
|
|
1855
|
+
* write. `CUR18` closes the other direction — a key declared here and occurring
|
|
1856
|
+
* nowhere else in the tree is refused, which is exactly what `scaleYMode` was.
|
|
1857
|
+
*
|
|
1858
|
+
* ⚠️ `RigUnimplementedAttachment` is deliberately absent. It carries
|
|
1859
|
+
* `[field: string]: unknown` because its whole job is to let a spec *say* a
|
|
1860
|
+
* `point` or a `linkedmesh` and get a named `NotImplementedError` back; checking
|
|
1861
|
+
* the keys of an attachment rigc is about to refuse by type would name the wrong
|
|
1862
|
+
* fault.
|
|
1863
|
+
*/
|
|
1864
|
+
export const RIG_KEYS = {
|
|
1865
|
+
RigSpec: ['spec', 'name', 'note', 'skeleton', 'images', 'bones', 'slots', 'skins', 'constraints', 'events', 'invariants'],
|
|
1866
|
+
RigSkeletonHeader: ['x', 'y', 'width', 'height', 'fps', 'referenceScale', 'images', 'audio', 'stageBox'],
|
|
1867
|
+
RigStageBox: ['slot', 'attachment'],
|
|
1868
|
+
RigBone: ['name', 'parent', 'length', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'shearX', 'shearY', 'inherit', 'skin', 'color', 'icon', 'from'],
|
|
1869
|
+
RigBoneFrom: ['anchor', 'slotWindow', 'meshCenter', 'rotation'],
|
|
1870
|
+
RigSlot: ['name', 'bone', 'attachment', 'color', 'dark', 'blend'],
|
|
1871
|
+
RigEvent: ['int', 'float', 'string', 'audio', 'volume', 'balance'],
|
|
1872
|
+
RigInvariants: ['meshSlots', 'meshTriangles', 'axisBone', 'massBone', 'detached', 'deformMayFold', 'consumerDrivenMix', 'idleDrivesMeshes'],
|
|
1873
|
+
RigIdleDrivesMeshes: ['why'],
|
|
1874
|
+
RigDetachedRule: ['bone', 'notUnder', 'why'],
|
|
1875
|
+
RigDeformFoldExemption: ['slot', 'why'],
|
|
1876
|
+
RigConsumerDrivenMix: ['constraint', 'type', 'why'],
|
|
1877
|
+
// Not retyped: `RIG_SKIN_KEYS` already IS this set, and it is the set
|
|
1878
|
+
// `splitRigSkin` refuses a long-form skin's stray key against. A second
|
|
1879
|
+
// spelling of it here would be two lists that have to agree, which is the
|
|
1880
|
+
// defect this whole table is checked to avoid.
|
|
1881
|
+
RigSkinEntry: RIG_SKIN_KEYS,
|
|
1882
|
+
RigIkConstraint: ['name', 'skin', 'type', 'bones', 'target', 'scaleY', 'mix', 'softness', 'bendPositive', 'compress', 'stretch'],
|
|
1883
|
+
RigTransformConstraint: [
|
|
1884
|
+
'name', 'skin', 'type', 'bones', 'source', 'localSource', 'localTarget', 'additive', 'clamp', 'properties',
|
|
1885
|
+
'rotation', 'x', 'y', 'scaleX', 'scaleY', 'shearY',
|
|
1886
|
+
'mixRotate', 'mixX', 'mixY', 'mixScaleX', 'mixScaleY', 'mixShearY',
|
|
1887
|
+
],
|
|
1888
|
+
RigPathConstraint: [
|
|
1889
|
+
'name', 'skin', 'type', 'bones', 'slot', 'positionMode', 'spacingMode', 'rotateMode',
|
|
1890
|
+
'rotation', 'position', 'spacing', 'mixRotate', 'mixX', 'mixY',
|
|
1891
|
+
],
|
|
1892
|
+
RigPhysicsConstraint: [
|
|
1893
|
+
'name', 'skin', 'type', 'bone', 'x', 'y', 'rotate', 'scaleX', 'shearX', 'scaleY', 'limit', 'fps',
|
|
1894
|
+
'inertia', 'strength', 'damping', 'mass', 'wind', 'gravity', 'mix',
|
|
1895
|
+
'inertiaGlobal', 'strengthGlobal', 'dampingGlobal', 'massGlobal', 'windGlobal', 'gravityGlobal', 'mixGlobal',
|
|
1896
|
+
],
|
|
1897
|
+
RigSliderConstraint: [
|
|
1898
|
+
'name', 'skin', 'type', 'animation', 'mix', 'additive', 'loop', 'bone', 'property', 'from', 'to', 'scale',
|
|
1899
|
+
'max', 'local', 'time',
|
|
1900
|
+
],
|
|
1901
|
+
RigTransformProperty: ['offset', 'to'],
|
|
1902
|
+
RigTransformTo: ['offset', 'max', 'scale'],
|
|
1903
|
+
RigRegionAttachment: ['type', 'name', 'path', 'image', 'x', 'y', 'rotation', 'scaleX', 'scaleY', 'width', 'height', 'color', 'sequence'],
|
|
1904
|
+
RigMeshAttachment: [
|
|
1905
|
+
'type', 'name', 'path', 'image', 'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges',
|
|
1906
|
+
'width', 'height', 'color', 'generator', 'sequence',
|
|
1907
|
+
],
|
|
1908
|
+
RigLinkedMeshAttachment: [
|
|
1909
|
+
'type', 'name', 'path', 'image', 'source', 'slot', 'skin', 'timelines', 'width', 'height', 'color', 'sequence',
|
|
1910
|
+
// Declared so the refusal can name them — see `RigLinkedMeshAttachment`.
|
|
1911
|
+
'uvs', 'triangles', 'vertices', 'weights', 'boneIndexing', 'hull', 'edges', 'generator',
|
|
1912
|
+
],
|
|
1913
|
+
RigMeshBinding: ['bone', 'x', 'y', 'weight'],
|
|
1914
|
+
RigSequence: ['count', 'start', 'digits', 'setup'],
|
|
1915
|
+
RigBoundingBoxAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name'],
|
|
1916
|
+
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'end', 'convex', 'inverse'],
|
|
1917
|
+
RigPathAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'closed', 'constantSpeed', 'lengths'],
|
|
1918
|
+
RigRingGenerator: ['kind', 'hull', 'center', 'inner', 'size', 'bias', 'controls'],
|
|
1919
|
+
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
1920
|
+
RigContourGenerator: ['kind', 'tolerance', 'margin', 'maxVertices', 'alpha', 'depth', 'soft'],
|
|
1921
|
+
RigGridGenerator: ['kind', 'us', 'vs', 'cols', 'rows', 'depth', 'soft'],
|
|
1922
|
+
RigSegmentsGenerator: ['kind', 'cell', 'bones', 'falloff', 'alpha', 'anchor'],
|
|
1923
|
+
RigSegmentsFalloff: ['power', 'radius', 'maxBones', 'minWeight'],
|
|
1924
|
+
RigSegmentSpan: ['bone', 'from', 'to'],
|
|
1925
|
+
RigMeshBias: ['axis_deg', 'ramp'],
|
|
1926
|
+
RigDepthMap: ['image', 'near', 'zScale', 'gamma', 'contrast', 'bias'],
|
|
1927
|
+
RigSoftRegion: ['bone', 'mask'],
|
|
1928
|
+
} as const satisfies Record<string, readonly string[]>;
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* The type every key of `RIG_KEYS` holds, shape by shape — what
|
|
1932
|
+
* `refuseValuesOfTheWrongType` refuses a value against (issue #890).
|
|
1933
|
+
*
|
|
1934
|
+
* 🔒 **Beside the key table, and held equal to it twice.** `satisfies` over
|
|
1935
|
+
* `RigTypeTable` makes a key typed here and absent there, or admitted there and
|
|
1936
|
+
* untyped here, a type error; the selftest compares the two at runtime as well,
|
|
1937
|
+
* and derives each checked type from the interface the row is named for, so a
|
|
1938
|
+
* field declared `number` and typed `string` here is a red run rather than a
|
|
1939
|
+
* refusal of correct work.
|
|
1940
|
+
*
|
|
1941
|
+
* Where an unchecked `object` or `mixed` row is refused: a nested shape by the
|
|
1942
|
+
* reader that walks it; `RigSegmentsGenerator.bones` (`mixed`: a name, a chain
|
|
1943
|
+
* of names or a span) by the segments generator, by index. Where an `enum` row
|
|
1944
|
+
* is refused is not a list in prose any more: `RIG_ENUMS`, below, has one entry
|
|
1945
|
+
* per `enum` row — a set refused by `refuseValuesOutsideTheirSet`, or the
|
|
1946
|
+
* reader that refuses it — and `satisfies` makes a row without one a type error.
|
|
1947
|
+
*
|
|
1948
|
+
* ⚠️ Until issue #900 this comment carried that list, and ended by naming three
|
|
1949
|
+
* `enum` keys with **no** owner: `boneIndexing`, `from.rotation` and a
|
|
1950
|
+
* generator `kind`. The list was right about the three and wrong by omission
|
|
1951
|
+
* about a fourth — the cut manifest's `mesh.kind`, which `MANIFEST_TYPES` said
|
|
1952
|
+
* "the manifest mesh reader" held and which that reader read as a ribbon in one
|
|
1953
|
+
* place and a ring in another. A list of owners in a comment is a claim nothing
|
|
1954
|
+
* checks; the table is one a control reads.
|
|
1955
|
+
*/
|
|
1956
|
+
type RigTypeTable = {
|
|
1957
|
+
readonly [S in keyof typeof RIG_KEYS]: { readonly [K in (typeof RIG_KEYS)[S][number]]: SpecValueType };
|
|
1958
|
+
};
|
|
1959
|
+
|
|
1960
|
+
export const RIG_TYPES = {
|
|
1961
|
+
RigSpec: {
|
|
1962
|
+
spec: 'enum', name: 'string', note: 'string', skeleton: 'object', images: 'string', bones: 'object[]', slots: 'object[]',
|
|
1963
|
+
skins: 'map of object', constraints: 'object[]', events: 'map of object', invariants: 'object',
|
|
1964
|
+
},
|
|
1965
|
+
RigSkeletonHeader: {
|
|
1966
|
+
x: 'number', y: 'number', width: 'number | null', height: 'number | null', fps: 'number', referenceScale: 'number',
|
|
1967
|
+
images: 'string', audio: 'string | null', stageBox: 'object',
|
|
1968
|
+
},
|
|
1969
|
+
RigStageBox: { slot: 'string', attachment: 'string' },
|
|
1970
|
+
RigBone: {
|
|
1971
|
+
name: 'string', parent: 'string', length: 'number', x: 'number', y: 'number', rotation: 'number', scaleX: 'number',
|
|
1972
|
+
scaleY: 'number', shearX: 'number', shearY: 'number', inherit: 'enum', skin: 'boolean', color: 'string', icon: 'string',
|
|
1973
|
+
from: 'object',
|
|
1974
|
+
},
|
|
1975
|
+
RigBoneFrom: { anchor: 'string', slotWindow: 'string', meshCenter: 'string', rotation: 'enum' },
|
|
1976
|
+
RigSlot: { name: 'string', bone: 'string', attachment: 'string | null', color: 'string', dark: 'string', blend: 'enum' },
|
|
1977
|
+
RigEvent: { int: 'number', float: 'number', string: 'string', audio: 'string', volume: 'number', balance: 'number' },
|
|
1978
|
+
RigInvariants: {
|
|
1979
|
+
meshSlots: 'number', meshTriangles: 'number', axisBone: 'string', massBone: 'string', detached: 'object[]',
|
|
1980
|
+
deformMayFold: 'object[]', consumerDrivenMix: 'object[]', idleDrivesMeshes: 'object',
|
|
1981
|
+
},
|
|
1982
|
+
RigIdleDrivesMeshes: { why: 'string' },
|
|
1983
|
+
RigDetachedRule: { bone: 'string', notUnder: 'string', why: 'string' },
|
|
1984
|
+
RigDeformFoldExemption: { slot: 'string', why: 'string' },
|
|
1985
|
+
RigConsumerDrivenMix: { constraint: 'string', type: 'enum', why: 'string' },
|
|
1986
|
+
RigSkinEntry: {
|
|
1987
|
+
attachments: 'map of object', bones: 'string[]', ik: 'string[]', transform: 'string[]', path: 'string[]',
|
|
1988
|
+
physics: 'string[]', slider: 'string[]',
|
|
1989
|
+
},
|
|
1990
|
+
RigIkConstraint: {
|
|
1991
|
+
name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', target: 'string', scaleY: 'enum', mix: 'number',
|
|
1992
|
+
softness: 'number', bendPositive: 'boolean', compress: 'boolean', stretch: 'boolean',
|
|
1993
|
+
},
|
|
1994
|
+
RigTransformConstraint: {
|
|
1995
|
+
name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', source: 'string', localSource: 'boolean',
|
|
1996
|
+
localTarget: 'boolean', additive: 'boolean', clamp: 'boolean', properties: 'map of object',
|
|
1997
|
+
rotation: 'number', x: 'number', y: 'number', scaleX: 'number', scaleY: 'number', shearY: 'number',
|
|
1998
|
+
mixRotate: 'number', mixX: 'number', mixY: 'number', mixScaleX: 'number', mixScaleY: 'number', mixShearY: 'number',
|
|
1999
|
+
},
|
|
2000
|
+
RigPathConstraint: {
|
|
2001
|
+
name: 'string', skin: 'boolean', type: 'enum', bones: 'string[]', slot: 'string', positionMode: 'enum',
|
|
2002
|
+
spacingMode: 'enum', rotateMode: 'enum', rotation: 'number', position: 'number', spacing: 'number',
|
|
2003
|
+
mixRotate: 'number', mixX: 'number', mixY: 'number',
|
|
2004
|
+
},
|
|
2005
|
+
RigPhysicsConstraint: {
|
|
2006
|
+
name: 'string', skin: 'boolean', type: 'enum', bone: 'string', x: 'number', y: 'number', rotate: 'number',
|
|
2007
|
+
scaleX: 'number', shearX: 'number', scaleY: 'enum', limit: 'number', fps: 'number', inertia: 'number',
|
|
2008
|
+
strength: 'number', damping: 'number', mass: 'number', wind: 'number', gravity: 'number', mix: 'number',
|
|
2009
|
+
inertiaGlobal: 'boolean', strengthGlobal: 'boolean', dampingGlobal: 'boolean', massGlobal: 'boolean',
|
|
2010
|
+
windGlobal: 'boolean', gravityGlobal: 'boolean', mixGlobal: 'boolean',
|
|
2011
|
+
},
|
|
2012
|
+
RigSliderConstraint: {
|
|
2013
|
+
name: 'string', skin: 'boolean', type: 'enum', animation: 'string', mix: 'number', additive: 'boolean',
|
|
2014
|
+
loop: 'boolean', bone: 'string', property: 'enum', from: 'number', to: 'number', scale: 'number', max: 'number',
|
|
2015
|
+
local: 'boolean', time: 'number',
|
|
2016
|
+
},
|
|
2017
|
+
RigTransformProperty: { offset: 'number', to: 'map of object' },
|
|
2018
|
+
RigTransformTo: { offset: 'number', max: 'number', scale: 'number' },
|
|
2019
|
+
RigRegionAttachment: {
|
|
2020
|
+
type: 'enum', name: 'string', path: 'string', image: 'string', x: 'number', y: 'number', rotation: 'number',
|
|
2021
|
+
scaleX: 'number', scaleY: 'number', width: 'number', height: 'number', color: 'string', sequence: 'object',
|
|
2022
|
+
},
|
|
2023
|
+
RigMeshAttachment: {
|
|
2024
|
+
type: 'enum', name: 'string', path: 'string', image: 'string', uvs: 'number[]', triangles: 'number[]',
|
|
2025
|
+
vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', hull: 'number', edges: 'number[]',
|
|
2026
|
+
width: 'number', height: 'number', color: 'string', generator: 'object', sequence: 'object',
|
|
2027
|
+
},
|
|
2028
|
+
RigLinkedMeshAttachment: {
|
|
2029
|
+
type: 'enum', name: 'string', path: 'string', image: 'string', source: 'string', slot: 'string', skin: 'string',
|
|
2030
|
+
timelines: 'boolean', width: 'number', height: 'number', color: 'string', sequence: 'object',
|
|
2031
|
+
uvs: 'number[]', triangles: 'number[]', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum',
|
|
2032
|
+
hull: 'number', edges: 'number[]', generator: 'object',
|
|
2033
|
+
},
|
|
2034
|
+
RigMeshBinding: { bone: 'string', x: 'number', y: 'number', weight: 'number' },
|
|
2035
|
+
RigSequence: { count: 'number', start: 'number', digits: 'number', setup: 'number' },
|
|
2036
|
+
RigBoundingBoxAttachment: {
|
|
2037
|
+
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2038
|
+
},
|
|
2039
|
+
RigClippingAttachment: {
|
|
2040
|
+
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2041
|
+
end: 'string', convex: 'boolean', inverse: 'boolean',
|
|
2042
|
+
},
|
|
2043
|
+
RigPathAttachment: {
|
|
2044
|
+
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2045
|
+
closed: 'boolean', constantSpeed: 'boolean', lengths: 'number[]',
|
|
2046
|
+
},
|
|
2047
|
+
RigRingGenerator: {
|
|
2048
|
+
kind: 'enum', hull: 'number[][]', center: 'number[]', inner: 'number', size: 'number[]', bias: 'object',
|
|
2049
|
+
controls: 'string[]',
|
|
2050
|
+
},
|
|
2051
|
+
RigRibbonGenerator: { kind: 'enum', size: 'number[]', rows: 'number', chain: 'string[]' },
|
|
2052
|
+
RigContourGenerator: {
|
|
2053
|
+
kind: 'enum', tolerance: 'number', margin: 'number', maxVertices: 'number', alpha: 'number', depth: 'object',
|
|
2054
|
+
soft: 'object',
|
|
2055
|
+
},
|
|
2056
|
+
RigGridGenerator: { kind: 'enum', us: 'number[]', vs: 'number[]', cols: 'number', rows: 'number', depth: 'object', soft: 'object' },
|
|
2057
|
+
RigSegmentsGenerator: { kind: 'enum', cell: 'number', bones: 'mixed', falloff: 'object', alpha: 'number', anchor: 'number[]' },
|
|
2058
|
+
RigSegmentsFalloff: { power: 'number', radius: 'number', maxBones: 'number', minWeight: 'number' },
|
|
2059
|
+
RigSegmentSpan: { bone: 'string', from: 'number[]', to: 'number[]' },
|
|
2060
|
+
RigMeshBias: { axis_deg: 'number', ramp: 'number[]' },
|
|
2061
|
+
RigDepthMap: { image: 'string', near: 'enum', zScale: 'number', gamma: 'number', contrast: 'number', bias: 'number' },
|
|
2062
|
+
RigSoftRegion: { bone: 'string', mask: 'string' },
|
|
2063
|
+
} as const satisfies RigTypeTable;
|
|
2064
|
+
|
|
2065
|
+
/** The two sources a bone's `from.rotation` names (`RigBoneFrom.rotation`). */
|
|
2066
|
+
export const RIG_FROM_ROTATIONS = ['axis', 'anchor'] as const satisfies ReadonlyArray<NonNullable<RigBoneFrom['rotation']>>;
|
|
2067
|
+
|
|
2068
|
+
/** The two ways a weighted `vertices` run names its bones (`RigMeshAttachment.boneIndexing`). */
|
|
2069
|
+
export const RIG_BONE_INDEXING = ['name', 'raw'] as const satisfies ReadonlyArray<NonNullable<RigMeshAttachment['boneIndexing']>>;
|
|
2070
|
+
|
|
2071
|
+
/** What `boneIndexing` outside its set was read as, measured before issue #900. */
|
|
2072
|
+
const BONE_INDEXING_READ_AS =
|
|
2073
|
+
'anything else was read as the default "name" in silence: a weighted "vertices" run was refused as unflagged, ' +
|
|
2074
|
+
'and every other attachment built byte for byte as if the key were absent';
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Who refuses each `enum` row of `RIG_TYPES` outside its set (issue #900) —
|
|
2078
|
+
* `SpecEnumTable` in [`keys.ts`](keys.ts) says what the two kinds of entry
|
|
2079
|
+
* mean, and `satisfies` makes an `enum` row without one a type error.
|
|
2080
|
+
*
|
|
2081
|
+
* ⭐ A `set` is stated for exactly the rows nobody held: measured when this
|
|
2082
|
+
* table was written, `boneIndexing` built green at `5`, `"foo"` and `"named"`
|
|
2083
|
+
* (the spelling the issue itself used) as the default, and `from.rotation`
|
|
2084
|
+
* built green at `5` and `"foo"` with the bone's setup rotation dropped. Every
|
|
2085
|
+
* other row already had a reader that refused a planted `5` and `"foo"` by
|
|
2086
|
+
* name, and keeps it; the generator's `kind` is the one of those that is a
|
|
2087
|
+
* dispatch, and its refusal is new — it threw a `TypeError` (*undefined is not
|
|
2088
|
+
* an object (evaluating 'generator.size')*), because the scan skipped a `kind`
|
|
2089
|
+
* it had no row for and the mesh builder fell through to the ring branch.
|
|
2090
|
+
*/
|
|
2091
|
+
export const RIG_ENUMS = {
|
|
2092
|
+
RigSpec: { spec: { owner: 'parseRigSpec' } },
|
|
2093
|
+
RigBone: { inherit: { owner: 'parseRigSpec' } },
|
|
2094
|
+
RigBoneFrom: {
|
|
2095
|
+
rotation: {
|
|
2096
|
+
set: RIG_FROM_ROTATIONS,
|
|
2097
|
+
readAs: 'anything else was read as no rotation source, and the bone was emitted without the setup rotation it asked for',
|
|
2098
|
+
},
|
|
2099
|
+
},
|
|
2100
|
+
RigSlot: { blend: { owner: 'parseRigSpec' } },
|
|
2101
|
+
RigConsumerDrivenMix: { type: { owner: 'parseRigSpec' } },
|
|
2102
|
+
RigIkConstraint: { type: { owner: 'buildRigConstraint' }, scaleY: { owner: 'buildRigConstraint' } },
|
|
2103
|
+
RigTransformConstraint: { type: { owner: 'buildRigConstraint' } },
|
|
2104
|
+
RigPathConstraint: {
|
|
2105
|
+
type: { owner: 'buildRigConstraint' },
|
|
2106
|
+
positionMode: { owner: 'buildRigConstraint' },
|
|
2107
|
+
spacingMode: { owner: 'buildRigConstraint' },
|
|
2108
|
+
rotateMode: { owner: 'buildRigConstraint' },
|
|
2109
|
+
},
|
|
2110
|
+
RigPhysicsConstraint: { type: { owner: 'buildRigConstraint' }, scaleY: { owner: 'buildRigConstraint' } },
|
|
2111
|
+
RigSliderConstraint: { type: { owner: 'buildRigConstraint' }, property: { owner: 'buildRigConstraint' } },
|
|
2112
|
+
RigRegionAttachment: { type: { owner: 'buildRigAttachment' } },
|
|
2113
|
+
RigMeshAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2114
|
+
RigLinkedMeshAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2115
|
+
RigBoundingBoxAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2116
|
+
RigClippingAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2117
|
+
RigPathAttachment: { type: { owner: 'buildRigAttachment' }, boneIndexing: { set: RIG_BONE_INDEXING, readAs: BONE_INDEXING_READ_AS } },
|
|
2118
|
+
// The generator's `kind` chooses the row its other keys are checked against,
|
|
2119
|
+
// so a `kind` outside the five has no row to be visited in: the scan that
|
|
2120
|
+
// dispatches on it is where it is refused.
|
|
2121
|
+
RigRingGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2122
|
+
RigRibbonGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2123
|
+
RigContourGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2124
|
+
RigGridGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2125
|
+
RigSegmentsGenerator: { kind: { owner: 'checkRigSpecKeys' } },
|
|
2126
|
+
RigDepthMap: { near: { owner: 'sampleMeshDepth' } },
|
|
2127
|
+
} as const satisfies SpecEnumTable<typeof RIG_TYPES>;
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* The five constraint `type` names, and the shape each one's keys come from.
|
|
2131
|
+
*
|
|
2132
|
+
* `satisfies` over `RigSkinConstraintKey` is what makes the five exhaustive: a
|
|
2133
|
+
* sixth constraint type added to that union without an entry here is a type
|
|
2134
|
+
* error rather than a shape whose keys go unchecked.
|
|
2135
|
+
*/
|
|
2136
|
+
const CONSTRAINT_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
2137
|
+
ik: 'RigIkConstraint',
|
|
2138
|
+
transform: 'RigTransformConstraint',
|
|
2139
|
+
path: 'RigPathConstraint',
|
|
2140
|
+
physics: 'RigPhysicsConstraint',
|
|
2141
|
+
slider: 'RigSliderConstraint',
|
|
2142
|
+
} satisfies Record<RigSkinConstraintKey, keyof typeof RIG_KEYS>;
|
|
2143
|
+
|
|
2144
|
+
/** The attachment `type` names, and the shape each one's keys come from. */
|
|
2145
|
+
const ATTACHMENT_SHAPE: Record<string, keyof typeof RIG_KEYS> = {
|
|
2146
|
+
region: 'RigRegionAttachment',
|
|
2147
|
+
mesh: 'RigMeshAttachment',
|
|
2148
|
+
linkedmesh: 'RigLinkedMeshAttachment',
|
|
2149
|
+
boundingbox: 'RigBoundingBoxAttachment',
|
|
2150
|
+
clipping: 'RigClippingAttachment',
|
|
2151
|
+
path: 'RigPathAttachment',
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
/** The generator `kind` names, and the shape each one's keys come from. */
|
|
2155
|
+
const GENERATOR_SHAPE: Record<(typeof RIG_GENERATOR_KINDS)[number], keyof typeof RIG_KEYS> = {
|
|
2156
|
+
ring: 'RigRingGenerator',
|
|
2157
|
+
ribbon: 'RigRibbonGenerator',
|
|
2158
|
+
contour: 'RigContourGenerator',
|
|
2159
|
+
grid: 'RigGridGenerator',
|
|
2160
|
+
segments: 'RigSegmentsGenerator',
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* The attachment kinds that carry a `sequence` — the three `readAttachment`
|
|
2165
|
+
* branches that call `readSequence` (`SkeletonJson.js:530`, `:561`; `mesh` and
|
|
2166
|
+
* `linkedmesh` share the second).
|
|
2167
|
+
*/
|
|
2168
|
+
export const SEQUENCE_ATTACHMENT_TYPES = ['region', 'mesh', 'linkedmesh'] as const;
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* One attachment's `sequence` block, refused by name where the parser would read
|
|
2172
|
+
* it into a series that is not the one the spec states.
|
|
2173
|
+
*
|
|
2174
|
+
* Every refusal here is a silence measured on spine-core 4.3.13 (issue #729):
|
|
2175
|
+
*
|
|
2176
|
+
* - no `count` — the parser's default is 0, and the attachment loads holding
|
|
2177
|
+
* no region at all;
|
|
2178
|
+
* - a `setup` at or past `count` — `Sequence.resolveIndex` clamps it to the
|
|
2179
|
+
* last frame (`setup: 7` on a four-frame series showed frame 4), and a
|
|
2180
|
+
* negative one indexes `regions[-1]`;
|
|
2181
|
+
* - a fractional `count`, `start`, `digits` or `setup` — `start: 1.5` makes
|
|
2182
|
+
* `Sequence.getPath` ask the atlas for `stem1.5`;
|
|
2183
|
+
* - an `image` beside it — one file names one region, and the series names
|
|
2184
|
+
* `count` of them;
|
|
2185
|
+
* - a `generator` beside it — a generator traces one plate, and which frame it
|
|
2186
|
+
* should trace is not something the spec says.
|
|
2187
|
+
*/
|
|
2188
|
+
function checkRigSequence(att: Record<string, unknown>, who: string, where: string): void {
|
|
2189
|
+
const seq = att.sequence;
|
|
2190
|
+
const at = `${who} "sequence"`;
|
|
2191
|
+
if (!isObj(seq)) {
|
|
2192
|
+
throw new CompileError(
|
|
2193
|
+
`${where}: ${at} is ${JSON.stringify(seq) ?? String(seq)}, and a sequence is an object: ` +
|
|
2194
|
+
'`{ "count": <frames>, "start"?: <first number>, "digits"?: <zero padding>, "setup"?: <setup frame> }`',
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
const whole = (field: string, min: number): void => {
|
|
2198
|
+
const value = seq[field];
|
|
2199
|
+
if (value === undefined) return;
|
|
2200
|
+
// A number the file cannot carry is `refuseNumbersTheFileCannotCarry`'s,
|
|
2201
|
+
// one sentence for the whole family. This check runs inside the key scan,
|
|
2202
|
+
// before that walk, and printed a stated 1e309 as `null` (issue #881).
|
|
2203
|
+
if (typeof value === 'number' && !Number.isFinite(Math.fround(value))) return;
|
|
2204
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < min) {
|
|
2205
|
+
throw new CompileError(
|
|
2206
|
+
`${where}: ${at}.${field} is ${JSON.stringify(value) ?? String(value)}; it is a whole number` +
|
|
2207
|
+
(min > 0 ? ` of at least ${min}` : min === 0 ? ' of at least 0' : '') +
|
|
2208
|
+
' — `Sequence.getPath` writes `start + i` into the region name digit for digit, so a fraction names a ' +
|
|
2209
|
+
'region like `stem1.5` and a non-number names none',
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
if (seq.count === undefined) {
|
|
2214
|
+
throw new CompileError(
|
|
2215
|
+
`${where}: ${at} states no "count". The parser reads \`getValue(map, "count", 0)\` ` +
|
|
2216
|
+
'(`SkeletonJson.js:644`), so an omitted count is a series of NO frames: the attachment loads holding no ' +
|
|
2217
|
+
'region and draws nothing, without an error. State how many frames the series has.',
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
whole('count', 1);
|
|
2221
|
+
whole('start', 0);
|
|
2222
|
+
whole('digits', 0);
|
|
2223
|
+
whole('setup', 0);
|
|
2224
|
+
const count = seq.count as number;
|
|
2225
|
+
if (typeof seq.setup === 'number' && Number.isFinite(Math.fround(seq.setup)) && Number.isFinite(Math.fround(count)) && seq.setup >= count) {
|
|
2226
|
+
throw new CompileError(
|
|
2227
|
+
`${where}: ${at}.setup is ${seq.setup}, and a ${count}-frame series has frames 0 to ${count - 1}. ` +
|
|
2228
|
+
'`Sequence.resolveIndex` clamps an index at or past the end to the LAST frame (measured: `setup: 7` on ' +
|
|
2229
|
+
'four frames showed frame 4), so this would show a frame the spec does not name. `setup` is 0-based.',
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
for (const [field, why] of [
|
|
2233
|
+
['image', 'an image names ONE region and a sequence names `count` of them — the frames are the regions ' +
|
|
2234
|
+
'`<path><number>`, and on the loose route each is the PNG of that name in the images directory'],
|
|
2235
|
+
['generator', 'a generator traces one plate, and which frame of the series it should trace is not something ' +
|
|
2236
|
+
'the spec says — author the geometry, which every frame shares'],
|
|
2237
|
+
] as const) {
|
|
2238
|
+
if (att[field] !== undefined) {
|
|
2239
|
+
throw new CompileError(`${where}: ${who} states "${field}" beside "sequence"; ${why}. Remove "${field}".`);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* Refuse every key of this rig spec that no shape above declares.
|
|
2246
|
+
*
|
|
2247
|
+
* ⭐ It walks the file rather than the emitter's route, and that is the whole
|
|
2248
|
+
* design. `compile` reaches an attachment only through a slot it is going to
|
|
2249
|
+
* draw and a `setup` entry only through a slot that has attachments, so a check
|
|
2250
|
+
* riding along with the emitter inherits its blind spots — which is how issue
|
|
2251
|
+
* #293's refusal sat green for three weeks on exactly the half-finished rigs it
|
|
2252
|
+
* was written for. Every node of the document is visited here, whether or not
|
|
2253
|
+
* anything downstream would have looked at it.
|
|
2254
|
+
*
|
|
2255
|
+
* A node that is not an object is left alone: its shape is somebody else's
|
|
2256
|
+
* refusal, and naming its keys would be a second opinion on a fault already
|
|
2257
|
+
* reported (see the note at the head of `parseMotionSpec`).
|
|
2258
|
+
*/
|
|
2259
|
+
function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{ node: Record<string, unknown>; shape: keyof typeof RIG_KEYS; path: Array<string | number> }> {
|
|
2260
|
+
const visits: Array<{ node: Record<string, unknown>; shape: keyof typeof RIG_KEYS; path: Array<string | number> }> = [];
|
|
2261
|
+
const at = (node: unknown, shape: keyof typeof RIG_KEYS, what: string, path: Array<string | number>): void => {
|
|
2262
|
+
if (!isObj(node)) return;
|
|
2263
|
+
refuseUnknownKeys(node, RIG_KEYS[shape], where, what);
|
|
2264
|
+
visits.push({ node, shape, path });
|
|
2265
|
+
};
|
|
2266
|
+
|
|
2267
|
+
at(raw, 'RigSpec', 'this rig spec', []);
|
|
2268
|
+
at(raw.skeleton, 'RigSkeletonHeader', '"skeleton"', ['skeleton']);
|
|
2269
|
+
if (isObj(raw.skeleton)) at(raw.skeleton.stageBox, 'RigStageBox', 'skeleton.stageBox', ['skeleton', 'stageBox']);
|
|
2270
|
+
|
|
2271
|
+
for (const [i, bone] of (Array.isArray(raw.bones) ? raw.bones : []).entries()) {
|
|
2272
|
+
const who = isObj(bone) && typeof bone.name === 'string' ? `bone "${bone.name}"` : `bones[${i}]`;
|
|
2273
|
+
at(bone, 'RigBone', who, ['bones', i]);
|
|
2274
|
+
if (isObj(bone)) at(bone.from, 'RigBoneFrom', `${who}'s "from"`, ['bones', i, 'from']);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
for (const [i, slot] of (Array.isArray(raw.slots) ? raw.slots : []).entries()) {
|
|
2278
|
+
at(slot, 'RigSlot', isObj(slot) && typeof slot.name === 'string' ? `slot "${slot.name}"` : `slots[${i}]`, ['slots', i]);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
for (const [i, constraint] of (Array.isArray(raw.constraints) ? raw.constraints : []).entries()) {
|
|
2282
|
+
if (!isObj(constraint)) continue;
|
|
2283
|
+
const named = typeof constraint.name === 'string' ? `constraint "${constraint.name}"` : `constraints[${i}]`;
|
|
2284
|
+
const shape = CONSTRAINT_SHAPE[String(constraint.type)];
|
|
2285
|
+
// An unknown `type` is `buildRigConstraint`'s refusal and names the five
|
|
2286
|
+
// that exist; there is no key set to check it against and no honest one to
|
|
2287
|
+
// guess, so it goes past here to the message that can say something.
|
|
2288
|
+
if (shape === undefined) continue;
|
|
2289
|
+
at(constraint, shape, `${named} (${String(constraint.type)})`, ['constraints', i]);
|
|
2290
|
+
for (const [from, entry] of Object.entries(isObj(constraint.properties) ? constraint.properties : {})) {
|
|
2291
|
+
at(entry, 'RigTransformProperty', `${named} properties."${from}"`, ['constraints', i, 'properties', from]);
|
|
2292
|
+
if (!isObj(entry)) continue;
|
|
2293
|
+
for (const [to, driven] of Object.entries(isObj(entry.to) ? entry.to : {})) {
|
|
2294
|
+
at(driven, 'RigTransformTo', `${named} properties."${from}".to."${to}"`, ['constraints', i, 'properties', from, 'to', to]);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
for (const [name, event] of Object.entries(isObj(raw.events) ? raw.events : {})) {
|
|
2300
|
+
at(event, 'RigEvent', `event "${name}"`, ['events', name]);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (isObj(raw.invariants)) {
|
|
2304
|
+
at(raw.invariants, 'RigInvariants', '"invariants"', ['invariants']);
|
|
2305
|
+
for (const [i, rule] of (Array.isArray(raw.invariants.detached) ? raw.invariants.detached : []).entries()) {
|
|
2306
|
+
at(rule, 'RigDetachedRule', `invariants.detached[${i}]`, ['invariants', 'detached', i]);
|
|
2307
|
+
}
|
|
2308
|
+
for (const [i, rule] of (Array.isArray(raw.invariants.deformMayFold) ? raw.invariants.deformMayFold : []).entries()) {
|
|
2309
|
+
at(rule, 'RigDeformFoldExemption', `invariants.deformMayFold[${i}]`, ['invariants', 'deformMayFold', i]);
|
|
2310
|
+
}
|
|
2311
|
+
for (const [i, rule] of (Array.isArray(raw.invariants.consumerDrivenMix) ? raw.invariants.consumerDrivenMix : []).entries()) {
|
|
2312
|
+
at(rule, 'RigConsumerDrivenMix', `invariants.consumerDrivenMix[${i}]`, ['invariants', 'consumerDrivenMix', i]);
|
|
2313
|
+
}
|
|
2314
|
+
at(raw.invariants.idleDrivesMeshes, 'RigIdleDrivesMeshes', 'invariants.idleDrivesMeshes', ['invariants', 'idleDrivesMeshes']);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
for (const [skinName, skin] of Object.entries(isObj(raw.skins) ? raw.skins : {})) {
|
|
2318
|
+
if (!isObj(skin)) continue;
|
|
2319
|
+
// The long form's own key check is `splitRigSkin`'s and it already names the
|
|
2320
|
+
// likeliest cause (a slot left outside `attachments`), so it is reused
|
|
2321
|
+
// rather than restated — one refusal per fault.
|
|
2322
|
+
const parts = splitRigSkin(skin as RigSkin, `${where}: skin "${skinName}"`);
|
|
2323
|
+
// The long form's own lists are typed too; `splitRigSkin` has just refused
|
|
2324
|
+
// a key outside them, so the row's keys are the node's keys.
|
|
2325
|
+
if (parts.explicit) visits.push({ node: skin, shape: 'RigSkinEntry', path: ['skins', skinName] });
|
|
2326
|
+
const base: Array<string | number> = parts.explicit ? ['skins', skinName, 'attachments'] : ['skins', skinName];
|
|
2327
|
+
for (const [slot, placeholders] of Object.entries(parts.attachments)) {
|
|
2328
|
+
if (!isObj(placeholders)) continue;
|
|
2329
|
+
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
2330
|
+
if (!isObj(att)) continue;
|
|
2331
|
+
const who = `skin "${skinName}" slot "${slot}" attachment "${placeholder}"`;
|
|
2332
|
+
// `type` absent means `region` — the parser's own default (`:539`).
|
|
2333
|
+
// A mesh carrying `source` is a LINKED mesh — `type: "mesh"` and
|
|
2334
|
+
// `type: "linkedmesh"` share one parser branch and the `source` key is
|
|
2335
|
+
// what decides (`:568-569`, `:582`; SPEC_COVERAGE part 1-6). So its keys
|
|
2336
|
+
// are checked against the LINK's shape whichever of the two spellings it
|
|
2337
|
+
// used: against a mesh's the fault came out as *2 keys this compiler does
|
|
2338
|
+
// not read: "source", "skin" … fix the spelling or remove it*, where
|
|
2339
|
+
// removing `source` is what unmakes the linked mesh. Until issue #691
|
|
2340
|
+
// this branch skipped the check entirely, because the construct had no
|
|
2341
|
+
// key set of its own to check against.
|
|
2342
|
+
const stated = att.type === undefined ? 'region' : String(att.type);
|
|
2343
|
+
const type = stated === 'mesh' && att.source !== undefined ? 'linkedmesh' : stated;
|
|
2344
|
+
const shape = ATTACHMENT_SHAPE[type];
|
|
2345
|
+
if (shape === undefined) continue;
|
|
2346
|
+
// Before the key check, so a `sequence` on a kind that has no texture is
|
|
2347
|
+
// named as that — "a key this compiler does not read … fix the spelling"
|
|
2348
|
+
// would send the author hunting for a typo in a word spelled right.
|
|
2349
|
+
if (att.sequence !== undefined && !(SEQUENCE_ATTACHMENT_TYPES as readonly string[]).includes(type)) {
|
|
2350
|
+
throw new CompileError(
|
|
2351
|
+
`${where}: ${who} is a ${type} and states a "sequence". A sequence is a numbered series of atlas ` +
|
|
2352
|
+
`regions, and only the ${SEQUENCE_ATTACHMENT_TYPES.length} kinds that draw a region carry one — ` +
|
|
2353
|
+
`${SEQUENCE_ATTACHMENT_TYPES.join(', ')} (\`readAttachment\` calls \`readSequence\` in exactly those ` +
|
|
2354
|
+
`branches, \`SkeletonJson.js:530\` and \`:561\`); on a ${type} the parser never reads the key, so the ` +
|
|
2355
|
+
'series would be dropped in silence. Remove it, or put it on a region or a mesh.',
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
const here = [...base, slot, placeholder];
|
|
2359
|
+
at(att, shape, `${who} (${type})`, here);
|
|
2360
|
+
if (att.sequence !== undefined) {
|
|
2361
|
+
at(att.sequence, 'RigSequence', `${who} "sequence"`, [...here, 'sequence']);
|
|
2362
|
+
checkRigSequence(att, who, where);
|
|
2363
|
+
}
|
|
2364
|
+
for (const [i, vertex] of (Array.isArray(att.weights) ? att.weights : []).entries()) {
|
|
2365
|
+
for (const [j, binding] of (Array.isArray(vertex) ? vertex : []).entries()) {
|
|
2366
|
+
at(binding, 'RigMeshBinding', `${who} weights[${i}][${j}]`, [...here, 'weights', i, j]);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
if (!isObj(att.generator)) continue;
|
|
2370
|
+
const gen = att.generator;
|
|
2371
|
+
const kind = RIG_GENERATOR_KINDS.find((k) => k === gen.kind);
|
|
2372
|
+
const genShape = kind === undefined ? undefined : GENERATOR_SHAPE[kind];
|
|
2373
|
+
// 🚨 An unknown `kind` used to be skipped here as "the mesh builder's
|
|
2374
|
+
// refusal", and the mesh builder had none: `buildGeneratedMesh` fell
|
|
2375
|
+
// through to the ring branch and threw a TypeError reading
|
|
2376
|
+
// `generator.size` (issue #900). `kind` chooses the row every other key
|
|
2377
|
+
// of the generator is checked against, so this dispatch is the one
|
|
2378
|
+
// place that can refuse it — before any of those keys is judged.
|
|
2379
|
+
if (genShape === undefined) {
|
|
2380
|
+
const stated = gen.kind === undefined ? 'absent' : (JSON.stringify(gen.kind) ?? String(gen.kind));
|
|
2381
|
+
throw new CompileError(
|
|
2382
|
+
`${where}: ${who} generator.kind is ${stated}; one of ${RIG_GENERATOR_KINDS.map((k) => JSON.stringify(k)).join(', ')} — ` +
|
|
2383
|
+
'the kind names the builder, and each builder reads its own keys, so nothing about the mesh can be checked or built without one',
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
at(gen, genShape, `${who} generator (${String(gen.kind)})`, [...here, 'generator']);
|
|
2387
|
+
at(gen.bias, 'RigMeshBias', `${who} generator.bias`, [...here, 'generator', 'bias']);
|
|
2388
|
+
at(gen.depth, 'RigDepthMap', `${who} generator.depth`, [...here, 'generator', 'depth']);
|
|
2389
|
+
at(gen.soft, 'RigSoftRegion', `${who} generator.soft`, [...here, 'generator', 'soft']);
|
|
2390
|
+
at(gen.falloff, 'RigSegmentsFalloff', `${who} generator.falloff`, [...here, 'generator', 'falloff']);
|
|
2391
|
+
for (const [i, entry] of (Array.isArray(gen.bones) ? gen.bones : []).entries()) {
|
|
2392
|
+
at(entry, 'RigSegmentSpan', `${who} generator.bones[${i}]`, [...here, 'generator', 'bones', i]);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return visits;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* A rig-spec path in the words the other refusals in this file use for it:
|
|
2403
|
+
* `bone "hip" x`, `constraint "aim" mixRotate`, `skin "default" slot "tail"
|
|
2404
|
+
* attachment "tail" weights[0][1].x`, `event "step" float`. Anything else is
|
|
2405
|
+
* the dotted path, which names every number exactly.
|
|
2406
|
+
*/
|
|
2407
|
+
function rigPlace(raw: Record<string, unknown>): (path: ReadonlyArray<string | number>) => string {
|
|
2408
|
+
const named = (list: unknown, i: number, kind: string, plural: string): string => {
|
|
2409
|
+
const node = Array.isArray(list) ? list[i] : undefined;
|
|
2410
|
+
return isObj(node) && typeof node.name === 'string' ? `${kind} ${JSON.stringify(node.name)}` : `${plural}[${i}]`;
|
|
2411
|
+
};
|
|
2412
|
+
const rest = (path: ReadonlyArray<string | number>): string => {
|
|
2413
|
+
const tail = dottedPath(path);
|
|
2414
|
+
return tail === '' ? '' : ` ${tail}`;
|
|
2415
|
+
};
|
|
2416
|
+
return (path) => {
|
|
2417
|
+
const [head, second] = path;
|
|
2418
|
+
if (typeof second === 'number') {
|
|
2419
|
+
if (head === 'bones') return `${named(raw.bones, second, 'bone', 'bones')}${rest(path.slice(2))}`;
|
|
2420
|
+
if (head === 'slots') return `${named(raw.slots, second, 'slot', 'slots')}${rest(path.slice(2))}`;
|
|
2421
|
+
if (head === 'constraints') return `${named(raw.constraints, second, 'constraint', 'constraints')}${rest(path.slice(2))}`;
|
|
2422
|
+
}
|
|
2423
|
+
if (head === 'events' && typeof second === 'string') return `event ${JSON.stringify(second)}${rest(path.slice(2))}`;
|
|
2424
|
+
if (head === 'skins' && typeof second === 'string') {
|
|
2425
|
+
const inner = path[2] === 'attachments' ? path.slice(3) : path.slice(2);
|
|
2426
|
+
const [slot, attachment] = inner;
|
|
2427
|
+
if (typeof slot === 'string' && typeof attachment === 'string') {
|
|
2428
|
+
return `skin ${JSON.stringify(second)} slot ${JSON.stringify(slot)} attachment ${JSON.stringify(attachment)}${rest(inner.slice(2))}`;
|
|
2429
|
+
}
|
|
2430
|
+
return `skin ${JSON.stringify(second)}${rest(path.slice(2))}`;
|
|
2431
|
+
}
|
|
2432
|
+
return dottedPath(path);
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/**
|
|
2437
|
+
* Parse and check the envelope, then hand back a typed spec.
|
|
2438
|
+
*
|
|
2439
|
+
* What is checked here is what makes the REST of the compiler able to assume its
|
|
2440
|
+
* inputs: the version tag, the two required arrays, name uniqueness, and that
|
|
2441
|
+
* every parent and every slot bone resolves against a bone declared earlier.
|
|
2442
|
+
* Deeper checks (does an attachment's image exist, does a constraint's target
|
|
2443
|
+
* bone exist) belong where the data is used, so their message can name the
|
|
2444
|
+
* consumer.
|
|
2445
|
+
*/
|
|
2446
|
+
export function parseRigSpec(raw: unknown, where: string): RigSpec {
|
|
2447
|
+
if (!isObj(raw)) throw new CompileError(`${where}: a rig spec must be a JSON object`);
|
|
2448
|
+
if (raw.spec !== RIG_SPEC_VERSION) {
|
|
2449
|
+
throw new CompileError(`${where}: unknown rig spec version ${JSON.stringify(raw.spec)}, expected "${RIG_SPEC_VERSION}"`);
|
|
2450
|
+
}
|
|
2451
|
+
// Before anything resolves by name AND before the required keys are asked
|
|
2452
|
+
// for, because a key nothing reads is very often the CAUSE of the name — or
|
|
2453
|
+
// the array — that is not there: `"bones"` typed on a slider is a slider with
|
|
2454
|
+
// no driving bone, and `"slot"` typed at the root is a rig with no `slots` at
|
|
2455
|
+
// all. The refusal an author wants names the typo rather than the consequence.
|
|
2456
|
+
//
|
|
2457
|
+
// ⚠️ This comment argued exactly that while sitting three lines BELOW the
|
|
2458
|
+
// throws it was arguing about (issue #672), so misspelling a required key —
|
|
2459
|
+
// the commonest way to lose one — printed `a rig spec needs a "slots" array`
|
|
2460
|
+
// and never named the `"slot"` the file carried. `parseMotionSpec` was
|
|
2461
|
+
// written to this order and cites this function as its precedent; the
|
|
2462
|
+
// precedent was the prose here rather than the code.
|
|
2463
|
+
//
|
|
2464
|
+
// 🔒 Two checks stay above it, and both are the scan's own preconditions
|
|
2465
|
+
// rather than a preference. `isObj` is what makes `raw` an object to read
|
|
2466
|
+
// keys off at all. The version tag decides WHICH key set applies: a file
|
|
2467
|
+
// declaring a spec version this compiler does not know would otherwise be
|
|
2468
|
+
// refused key by key against `rigc-rig/1`'s sets — a list of "keys this
|
|
2469
|
+
// compiler does not read" for a format it has never read at all. The three
|
|
2470
|
+
// throws directly below are the required-key checks, and each of them is the
|
|
2471
|
+
// consequence a typo at the root produces.
|
|
2472
|
+
const visits = checkRigSpecKeys(raw, where);
|
|
2473
|
+
const place = rigPlace(raw);
|
|
2474
|
+
// Every value the scan admitted, against the type its key holds, before any
|
|
2475
|
+
// reader — here or in `compile` — has done arithmetic on it (issue #890).
|
|
2476
|
+
// ⚠️ It runs ahead of this function's own field checks on purpose: those read
|
|
2477
|
+
// values too, and a reader that meets a wrong type names the wrong fault —
|
|
2478
|
+
// `constraint.skin === true` reads `"skin": "true"` as a constraint that asks
|
|
2479
|
+
// for no skin. The price, measured, is three controls whose type half pinned an
|
|
2480
|
+
// older sentence (RF86, PS177, PS189); their other halves are unchanged.
|
|
2481
|
+
const shapeVisits: ShapeVisit[] = visits.map((v) => ({ node: v.node, shape: v.shape, name: (tail) => place([...v.path, ...tail]) }));
|
|
2482
|
+
refuseValuesOfTheWrongType(shapeVisits, RIG_TYPES, where);
|
|
2483
|
+
// A name outside an `enum` row's stated set (issue #900), after the type walk
|
|
2484
|
+
// and over the same visits: `boneIndexing: "foo"` built as the default and
|
|
2485
|
+
// `from.rotation: "foo"` as no rotation, both green. A row whose entry names
|
|
2486
|
+
// an owner is that reader's to refuse, in its own words.
|
|
2487
|
+
refuseValuesOutsideTheirSet(shapeVisits, RIG_TYPES, RIG_ENUMS, where);
|
|
2488
|
+
// After the key check, so a misspelled key is named as a misspelling before
|
|
2489
|
+
// its value is judged, and before every reader below, so no range rule or
|
|
2490
|
+
// derived number ever meets a value the file cannot carry (issue #881).
|
|
2491
|
+
refuseNumbersTheFileCannotCarry(raw, where, place);
|
|
2492
|
+
|
|
2493
|
+
if (typeof raw.name !== 'string' || raw.name.length === 0) {
|
|
2494
|
+
throw new CompileError(`${where}: a rig spec needs a "name" — a motion spec names it to pick this rig`);
|
|
2495
|
+
}
|
|
2496
|
+
if (!Array.isArray(raw.bones) || raw.bones.length === 0) {
|
|
2497
|
+
throw new CompileError(`${where}: a rig spec needs a non-empty "bones" array`);
|
|
2498
|
+
}
|
|
2499
|
+
if (!Array.isArray(raw.slots)) {
|
|
2500
|
+
throw new CompileError(`${where}: a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)`);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
const spec = raw as unknown as RigSpec;
|
|
2504
|
+
|
|
2505
|
+
// The stage, stated or stated absent. Half a statement is refused here rather
|
|
2506
|
+
// than resolved in `compile`, because which half was meant is not derivable
|
|
2507
|
+
// and a compiler that picks one is inventing a number (issue #578).
|
|
2508
|
+
const header = spec.skeleton;
|
|
2509
|
+
if (header !== undefined) {
|
|
2510
|
+
if (header.audio !== undefined && header.audio !== null && typeof header.audio !== 'string') {
|
|
2511
|
+
throw new CompileError(
|
|
2512
|
+
`${where}: "skeleton" states audio ${JSON.stringify(header.audio)}; it is a path from the skeleton file to ` +
|
|
2513
|
+
'its audio folder, or null for none — a string or null',
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
const noWidth = header.width === null;
|
|
2517
|
+
const noHeight = header.height === null;
|
|
2518
|
+
if (noWidth !== noHeight) {
|
|
2519
|
+
const stated = noWidth ? 'height' : 'width';
|
|
2520
|
+
const absent = noWidth ? 'width' : 'height';
|
|
2521
|
+
throw new CompileError(
|
|
2522
|
+
`${where}: "skeleton" states ${absent}: null and a ${stated} of ` +
|
|
2523
|
+
`${JSON.stringify(noWidth ? header.height : header.width)}. A stage has both extents or neither: ` +
|
|
2524
|
+
'write both as null for "this skeleton declares no stage", or give both a number',
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
if (noWidth && noHeight && (header.x !== undefined || header.y !== undefined)) {
|
|
2528
|
+
const origin = [header.x !== undefined ? 'x' : null, header.y !== undefined ? 'y' : null].filter((k) => k !== null);
|
|
2529
|
+
throw new CompileError(
|
|
2530
|
+
`${where}: "skeleton" declares no stage (width: null, height: null) and still states ${origin.join(' and ')}. ` +
|
|
2531
|
+
`${origin.length === 1 ? 'That is an origin' : 'Those are an origin'} for a box that is not there: ` +
|
|
2532
|
+
'drop them, or state a width and a height',
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
// The stage's box (issue #1168): both names stated, and a stage to draw it from.
|
|
2536
|
+
const box = header.stageBox;
|
|
2537
|
+
if (box !== undefined) {
|
|
2538
|
+
const missing = (['slot', 'attachment'] as const).filter((key) => typeof box[key] !== 'string' || box[key].length === 0);
|
|
2539
|
+
if (missing.length > 0) {
|
|
2540
|
+
throw new CompileError(
|
|
2541
|
+
`${where}: skeleton.stageBox states no ${missing.map((key) => `"${key}"`).join(' and ')}. It names the slot the ` +
|
|
2542
|
+
'stage box goes in and the attachment name it carries — `{ "slot": "stage", "attachment": "stage" }` — ' +
|
|
2543
|
+
'and both are required: rigc writes the box\'s numbers from the stage and names nothing on its own',
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
if (noWidth && noHeight) {
|
|
2547
|
+
throw new CompileError(
|
|
2548
|
+
`${where}: skeleton.stageBox asks for the stage as a bounding box in slot "${box.slot}", and "skeleton" ` +
|
|
2549
|
+
'declares no stage (width: null, height: null) — a box around nothing. State a width and a height, or ' +
|
|
2550
|
+
'drop stageBox',
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
const seen = new Set<string>();
|
|
2557
|
+
for (const bone of spec.bones) {
|
|
2558
|
+
if (!isObj(bone) || typeof bone.name !== 'string' || bone.name.length === 0) {
|
|
2559
|
+
throw new CompileError(`${where}: every bone needs a "name"`);
|
|
2560
|
+
}
|
|
2561
|
+
if (seen.has(bone.name)) {
|
|
2562
|
+
throw new CompileError(`${where}: two bones are called "${bone.name}"; bone names are the join key for slots, meshes and timelines`);
|
|
2563
|
+
}
|
|
2564
|
+
seen.add(bone.name);
|
|
2565
|
+
if (bone.parent === undefined) continue;
|
|
2566
|
+
if (typeof bone.parent !== 'string' || !seen.has(bone.parent)) {
|
|
2567
|
+
// The parser resolves `parent` against the bones it has already read, so a
|
|
2568
|
+
// forward reference is not a rigc restriction — it is a bone with no parent
|
|
2569
|
+
// in the loaded skeleton, which loads as a second root.
|
|
2570
|
+
throw new CompileError(
|
|
2571
|
+
`${where}: bone "${bone.name}" names parent ${JSON.stringify(bone.parent)}, which is not declared before it`,
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
if (bone.inherit !== undefined && resolveBoneInherit(bone.inherit) === undefined) {
|
|
2575
|
+
throw new CompileError(
|
|
2576
|
+
`${where}: bone "${bone.name}" has inherit ${JSON.stringify(bone.inherit)}; ${BONE_INHERIT_KNOWN}`,
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
const from = bone.from;
|
|
2580
|
+
if (from !== undefined) {
|
|
2581
|
+
const sources = ['anchor', 'slotWindow', 'meshCenter'].filter((k) => from[k as keyof RigBoneFrom] !== undefined);
|
|
2582
|
+
if (sources.length > 1) {
|
|
2583
|
+
throw new CompileError(
|
|
2584
|
+
`${where}: bone "${bone.name}" takes its position from more than one source (${sources.join(', ')}); name exactly one`,
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
if ((bone.x !== undefined || bone.y !== undefined) && sources.length === 1) {
|
|
2588
|
+
throw new CompileError(
|
|
2589
|
+
`${where}: bone "${bone.name}" declares both a literal x/y and from.${sources[0]}; the two would disagree the first time the art moved`,
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
if (from.rotation !== undefined && bone.rotation !== undefined) {
|
|
2593
|
+
throw new CompileError(`${where}: bone "${bone.name}" declares both a literal rotation and from.rotation`);
|
|
2594
|
+
}
|
|
2595
|
+
if (from.rotation === 'anchor' && from.anchor === undefined) {
|
|
2596
|
+
throw new CompileError(`${where}: bone "${bone.name}" wants its rotation from an anchor but names no from.anchor`);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
const slotNames = new Set<string>();
|
|
2602
|
+
for (const slot of spec.slots) {
|
|
2603
|
+
if (!isObj(slot) || typeof slot.name !== 'string' || slot.name.length === 0) {
|
|
2604
|
+
throw new CompileError(`${where}: every slot needs a "name"`);
|
|
2605
|
+
}
|
|
2606
|
+
if (slotNames.has(slot.name)) throw new CompileError(`${where}: two slots are called "${slot.name}"`);
|
|
2607
|
+
slotNames.add(slot.name);
|
|
2608
|
+
if (typeof slot.bone !== 'string' || !seen.has(slot.bone)) {
|
|
2609
|
+
throw new CompileError(
|
|
2610
|
+
`${where}: slot "${slot.name}" names bone ${JSON.stringify(slot.bone)}, which this rig does not declare`,
|
|
2611
|
+
);
|
|
2612
|
+
}
|
|
2613
|
+
// Issue #946: this used to compare case-insensitively, so `ADDITIVE` built
|
|
2614
|
+
// green and the runtime read the slot as no mode at all. The rule is now the
|
|
2615
|
+
// runtime's own, the one `buildRigConstraint`'s enums state.
|
|
2616
|
+
if (slot.blend !== undefined && !isRigSlotBlend(slot.blend)) {
|
|
2617
|
+
throw new CompileError(
|
|
2618
|
+
`${where}: slot "${slot.name}" has blend ${JSON.stringify(slot.blend)}; known: ${RIG_SLOT_BLEND.join(', ')} ` +
|
|
2619
|
+
"(only the first letter's case is free — the parser's enumValue uppercases that one character and nothing " +
|
|
2620
|
+
'else, and an unresolved name becomes undefined without an error)',
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// `invariants.deformMayFold` — one of the three fields in this file that TURN
|
|
2626
|
+
// A CHECK OFF (`consumerDrivenMix` and `idleDrivesMeshes`, below, are the
|
|
2627
|
+
// others), so its own shape is checked harder than the fields that turn one on. A
|
|
2628
|
+
// typo in a slot name here would silently exempt nothing and gate everything,
|
|
2629
|
+
// which reads exactly like the check working; and an exemption with no reason
|
|
2630
|
+
// is unreviewable six months later. Both are refused by name.
|
|
2631
|
+
const mayFold = spec.invariants?.deformMayFold;
|
|
2632
|
+
if (mayFold !== undefined) {
|
|
2633
|
+
if (!Array.isArray(mayFold)) {
|
|
2634
|
+
throw new CompileError(
|
|
2635
|
+
`${where}: invariants.deformMayFold is ${JSON.stringify(mayFold)}, expected an array of { "slot": …, "why": … }`,
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
const declared = new Set<string>();
|
|
2639
|
+
for (const entry of mayFold) {
|
|
2640
|
+
if (!isObj(entry) || typeof entry.slot !== 'string' || entry.slot.length === 0) {
|
|
2641
|
+
throw new CompileError(`${where}: every invariants.deformMayFold entry needs a "slot"`);
|
|
2642
|
+
}
|
|
2643
|
+
if (!slotNames.has(entry.slot)) {
|
|
2644
|
+
throw new CompileError(
|
|
2645
|
+
`${where}: invariants.deformMayFold exempts slot "${entry.slot}", which this rig does not declare — ` +
|
|
2646
|
+
'a name that resolves to nothing exempts nothing, and reads like the exemption worked',
|
|
2647
|
+
);
|
|
2648
|
+
}
|
|
2649
|
+
if (declared.has(entry.slot)) {
|
|
2650
|
+
throw new CompileError(`${where}: invariants.deformMayFold names slot "${entry.slot}" twice`);
|
|
2651
|
+
}
|
|
2652
|
+
declared.add(entry.slot);
|
|
2653
|
+
if (typeof entry.why !== 'string' || entry.why.trim().length === 0) {
|
|
2654
|
+
throw new CompileError(
|
|
2655
|
+
`${where}: invariants.deformMayFold entry for slot "${entry.slot}" needs a "why" — this field switches ` +
|
|
2656
|
+
'A39_DEFORM_KEEPS_TRIANGLE_WINDING off for that slot, and an exemption nobody can date or justify is ' +
|
|
2657
|
+
'how a defect ships as a decision',
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// A constraint's namespace is its KIND, not the array (issue #692).
|
|
2664
|
+
// `SkeletonData.findConstraint(name, type)` tests `constraint instanceof type`
|
|
2665
|
+
// BEFORE it compares the name, and every resolution in the format goes through
|
|
2666
|
+
// it: a timeline group, a skin's member list, a slider's own second pass. So an
|
|
2667
|
+
// ik constraint and a transform constraint called `leg` are two objects nothing
|
|
2668
|
+
// can confuse, and the rig spec refusing them was stricter than the file it
|
|
2669
|
+
// emits — a shape four skeletons of a production corpus have, where the chain
|
|
2670
|
+
// and the transform constraint that follows it carry the chain's name.
|
|
2671
|
+
/** Every constraint, in declaration order, so the refusals below read in file order. */
|
|
2672
|
+
const constraintsDeclared: Array<{ type: string; name: string; skinRequired: boolean }> = [];
|
|
2673
|
+
/** `<kind> constraint "<name>"` -> that constraint. The key IS the namespace. */
|
|
2674
|
+
const constraintFacts = new Map<string, { type: string; name: string; skinRequired: boolean }>();
|
|
2675
|
+
/** name -> the kinds that declare it, for the message that has to say which. */
|
|
2676
|
+
const constraintKinds = new Map<string, string[]>();
|
|
2677
|
+
for (const constraint of spec.constraints ?? []) {
|
|
2678
|
+
if (!isObj(constraint) || typeof constraint.name !== 'string' || constraint.name.length === 0) {
|
|
2679
|
+
throw new CompileError(`${where}: every constraint needs a "name"`);
|
|
2680
|
+
}
|
|
2681
|
+
const declared = { type: String(constraint.type), name: constraint.name, skinRequired: constraint.skin === true };
|
|
2682
|
+
if (constraintFacts.has(constraintAt(declared.type, declared.name))) {
|
|
2683
|
+
throw new CompileError(
|
|
2684
|
+
`${where}: two ${declared.type} constraints are called "${declared.name}" — a constraint resolves by name ` +
|
|
2685
|
+
'AND type (`SkeletonData.findConstraint`), so names are unique PER KIND: an ik and a transform constraint ' +
|
|
2686
|
+
'may share one, two of a kind may not',
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
constraintsDeclared.push(declared);
|
|
2690
|
+
constraintFacts.set(constraintAt(declared.type, declared.name), declared);
|
|
2691
|
+
constraintKinds.set(declared.name, [...(constraintKinds.get(declared.name) ?? []), declared.type]);
|
|
2692
|
+
// An ik's `bones` as a shape (issue #1205): more than two, or a pair that is
|
|
2693
|
+
// not a parent and its child, both load and build green and the solver then
|
|
2694
|
+
// moves nothing or solves a triangle nobody drew — `ikShapeFault` says which.
|
|
2695
|
+
// Read only once every name resolves to a declared bone, so a misspelling is
|
|
2696
|
+
// still the compiler's refusal by name rather than a shape it does not have.
|
|
2697
|
+
const ikBones = constraint.type === 'ik' ? constraint.bones : undefined;
|
|
2698
|
+
if (Array.isArray(ikBones) && ikBones.every((b): b is string => typeof b === 'string' && seen.has(b))) {
|
|
2699
|
+
const secondAncestors: string[] = [];
|
|
2700
|
+
if (ikBones.length === 2) {
|
|
2701
|
+
for (let at = spec.bones.find((b) => b.name === ikBones[1])?.parent; at !== undefined; at = spec.bones.find((b) => b.name === at)?.parent) secondAncestors.push(at);
|
|
2702
|
+
}
|
|
2703
|
+
const fault = ikShapeFault(constraint.name, ikBones, secondAncestors);
|
|
2704
|
+
if (fault !== null) throw new CompileError(`${where}: ${fault}`);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// `invariants.consumerDrivenMix` — the second field in `invariants` that TURNS
|
|
2709
|
+
// A CHECK OFF, so it is held to `deformMayFold`'s standard above: every way an
|
|
2710
|
+
// entry could exempt nothing while reading like it worked is refused by name
|
|
2711
|
+
// (issue #784). It resolves against `constraintFacts` because a constraint's
|
|
2712
|
+
// namespace is its kind — the entry says which kind, and the lookup is that key.
|
|
2713
|
+
const consumerDriven = spec.invariants?.consumerDrivenMix;
|
|
2714
|
+
if (consumerDriven !== undefined) {
|
|
2715
|
+
const shape = 'an array of { "constraint": …, "type": "ik" | "transform", "why": … }';
|
|
2716
|
+
if (!Array.isArray(consumerDriven)) {
|
|
2717
|
+
throw new CompileError(`${where}: invariants.consumerDrivenMix is ${JSON.stringify(consumerDriven)}, expected ${shape}`);
|
|
2718
|
+
}
|
|
2719
|
+
const named = new Set<string>();
|
|
2720
|
+
for (const entry of consumerDriven) {
|
|
2721
|
+
if (!isObj(entry) || typeof entry.constraint !== 'string' || entry.constraint.length === 0) {
|
|
2722
|
+
throw new CompileError(`${where}: every invariants.consumerDrivenMix entry needs a "constraint" — ${shape}`);
|
|
2723
|
+
}
|
|
2724
|
+
if (entry.type !== 'ik' && entry.type !== 'transform') {
|
|
2725
|
+
const declaredAs = constraintKinds.get(entry.constraint) ?? [];
|
|
2726
|
+
throw new CompileError(
|
|
2727
|
+
`${where}: invariants.consumerDrivenMix entry for "${entry.constraint}" has type ${JSON.stringify(entry.type)}; ` +
|
|
2728
|
+
'only "ik" and "transform" read this declaration (A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT, ' +
|
|
2729
|
+
'A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT). A path, physics or slider constraint resting muted is ' +
|
|
2730
|
+
'A36, A23 or A37, which read no declaration, so the entry would exempt nothing' +
|
|
2731
|
+
(declaredAs.length ? ` — the rig declares "${entry.constraint}" as ${declaredAs.map((t) => `${t === 'ik' ? 'an' : 'a'} ${t}`).join(' and ')} constraint` : ''),
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
const key = constraintAt(entry.type, entry.constraint);
|
|
2735
|
+
if (!constraintFacts.has(key)) {
|
|
2736
|
+
const declaredAs = constraintKinds.get(entry.constraint) ?? [];
|
|
2737
|
+
throw new CompileError(
|
|
2738
|
+
`${where}: invariants.consumerDrivenMix names ${key}, which this rig does not declare` +
|
|
2739
|
+
(declaredAs.length
|
|
2740
|
+
? ` — "${entry.constraint}" is declared as ${declaredAs.map((t) => `${t === 'ik' ? 'an' : 'a'} ${t}`).join(' and ')} constraint, and a constraint resolves by name AND type`
|
|
2741
|
+
: '') +
|
|
2742
|
+
'. A name that resolves to nothing exempts nothing, and reads like the exemption worked',
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2745
|
+
if (named.has(key)) throw new CompileError(`${where}: invariants.consumerDrivenMix names ${key} twice`);
|
|
2746
|
+
named.add(key);
|
|
2747
|
+
if (typeof entry.why !== 'string' || entry.why.trim().length === 0) {
|
|
2748
|
+
throw new CompileError(
|
|
2749
|
+
`${where}: invariants.consumerDrivenMix entry for ${key} needs a "why" — this field switches ` +
|
|
2750
|
+
`${entry.type === 'ik' ? 'A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT' : 'A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT'} ` +
|
|
2751
|
+
'off for that constraint, and an exemption nobody can date or justify is how a defect ships as a decision',
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// `invariants.idleDrivesMeshes` — the third field that TURNS A CHECK OFF
|
|
2758
|
+
// (issues #855, #858), so it is held to the same standard: the one shape it
|
|
2759
|
+
// accepts is `{ "why": … }`, and a `why` that is missing, blank or not a
|
|
2760
|
+
// string is refused by name. `true` is refused too, even though it reads like
|
|
2761
|
+
// the obvious spelling — a switch with no reason attached is exactly the
|
|
2762
|
+
// exemption nobody can review later. Whether the declaration switches off
|
|
2763
|
+
// anything is a question about the emitted `idle`, so it is the gate's (A15
|
|
2764
|
+
// refuses a stale one), not this parser's.
|
|
2765
|
+
const idleDrives = spec.invariants?.idleDrivesMeshes;
|
|
2766
|
+
if (idleDrives !== undefined) {
|
|
2767
|
+
const shape = '{ "why": "<why this idle deforms meshes on purpose>" }';
|
|
2768
|
+
if (!isObj(idleDrives)) {
|
|
2769
|
+
throw new CompileError(`${where}: invariants.idleDrivesMeshes is ${JSON.stringify(idleDrives)}, expected ${shape}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (typeof idleDrives.why !== 'string' || idleDrives.why.trim().length === 0) {
|
|
2772
|
+
throw new CompileError(
|
|
2773
|
+
`${where}: invariants.idleDrivesMeshes needs a "why" (a non-blank string), got ` +
|
|
2774
|
+
`${idleDrives.why === undefined ? 'none' : JSON.stringify(idleDrives.why)} — expected ${shape}. This field switches ` +
|
|
2775
|
+
'A15_IDLE_NO_MESH_BONE_KEYS off, and an exemption nobody can date or justify is how a defect ships as a decision',
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// --- skins: the attachment table, and what the skin ACTIVATES --------------
|
|
2781
|
+
//
|
|
2782
|
+
// The lists resolve by name like everything else in this format, and the
|
|
2783
|
+
// parser is loud about a miss (`Couldn't find bone X for skin Y`) — but in the
|
|
2784
|
+
// consumer's process, so they are refused here where the message can name the
|
|
2785
|
+
// rig spec. What the parser does NOT check is the pairing with `skin: true`,
|
|
2786
|
+
// and that half is silent in both directions (see `RigSkinEntry`).
|
|
2787
|
+
//
|
|
2788
|
+
// ⭐ **Sets rather than "which skin owns this", because a name may be in
|
|
2789
|
+
// several lists.** Until issue #725 these were `name -> the skin that claimed
|
|
2790
|
+
// it first`, and a second skin naming the same bone or constraint was refused
|
|
2791
|
+
// with *"a bone belongs to one skin"*. The rule was never the parser's and the
|
|
2792
|
+
// comment above it said so; what it rested on was that "which skin am I for"
|
|
2793
|
+
// has no answer for a name in two lists. It has one, and the runtime gives it:
|
|
2794
|
+
// `Skeleton.updateCache` activates the bones of the skin being WORN, so a bone
|
|
2795
|
+
// two mutually exclusive variants both list is active under either — measured
|
|
2796
|
+
// on a hand-forged file the refusal used to prevent, `bone.active` true under
|
|
2797
|
+
// each of the two skins, false under a third that lists nothing and false with
|
|
2798
|
+
// no skin set. `Skin.addSkin` deduplicates by object identity, so even a
|
|
2799
|
+
// consumer combining both variants gets the bone once. The format is a
|
|
2800
|
+
// per-skin SET and rigc now says the same thing.
|
|
2801
|
+
//
|
|
2802
|
+
// ⚠️ **The worn skin, and only the worn skin.** `updateCache` reads
|
|
2803
|
+
// `this.skin` and never `SkeletonData.defaultSkin` — the default-skin fallback
|
|
2804
|
+
// is `getAttachment`'s and covers art alone — so a `skin: true` bone that only
|
|
2805
|
+
// the `default` skin lists is measured INACTIVE under every other skin and
|
|
2806
|
+
// with no skin set. That is why nothing here is a union, and it is the one
|
|
2807
|
+
// shape an author is most likely to write expecting "always on".
|
|
2808
|
+
//
|
|
2809
|
+
// What the rule was really guarding — a second list that was meant to name a
|
|
2810
|
+
// different bone — is not derivable from the file, so it is not refused. Every
|
|
2811
|
+
// refusal that IS derivable stays: a name the rig does not declare, a name
|
|
2812
|
+
// declared under another constraint kind, and both halves of the `skin: true`
|
|
2813
|
+
// switch, each of which the rig suite now measures on a shared member.
|
|
2814
|
+
const skinBoneUse = new Set<string>();
|
|
2815
|
+
const skinConstraintUse = new Set<string>();
|
|
2816
|
+
if (spec.skins !== undefined) {
|
|
2817
|
+
if (!isObj(spec.skins)) throw new CompileError(`${where}: "skins" is an object keyed by skin name`);
|
|
2818
|
+
const collision = RIG_SKIN_KEYS.find((key) => slotNames.has(key));
|
|
2819
|
+
if (collision !== undefined) {
|
|
2820
|
+
// The long form is recognised by these keys, so a slot of one of those
|
|
2821
|
+
// names is genuinely ambiguous in the short form. Guessing either way
|
|
2822
|
+
// loses an attachment table or a member list in silence.
|
|
2823
|
+
throw new CompileError(
|
|
2824
|
+
`${where}: a slot is called "${collision}", which is one of the keys that tell a skin's long form ` +
|
|
2825
|
+
'(`{ "attachments": {…}, "bones": [...] }`) from its short one (`slotName -> placeholder -> attachment`): ' +
|
|
2826
|
+
`${RIG_SKIN_KEYS.join(', ')}. Rename the slot.`,
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
for (const [skinName, skin] of Object.entries(spec.skins)) {
|
|
2830
|
+
const at = `${where}: skin "${skinName}"`;
|
|
2831
|
+
const parts = splitRigSkin(skin, at);
|
|
2832
|
+
for (const bone of parts.bones) {
|
|
2833
|
+
if (!seen.has(bone)) {
|
|
2834
|
+
throw new CompileError(`${at} activates bone "${bone}", which this rig does not declare`);
|
|
2835
|
+
}
|
|
2836
|
+
skinBoneUse.add(bone);
|
|
2837
|
+
const declared = spec.bones.find((b) => b.name === bone);
|
|
2838
|
+
if (declared?.skin !== true) {
|
|
2839
|
+
throw new CompileError(
|
|
2840
|
+
`${at} activates bone "${bone}", but that bone does not declare \`"skin": true\`. ` +
|
|
2841
|
+
'Skeleton.updateCache starts a bone active unless it is skinRequired, so this list changes nothing — ' +
|
|
2842
|
+
'the bone poses under every skin.',
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
for (const type of RIG_SKIN_CONSTRAINT_KEYS) {
|
|
2847
|
+
for (const name of parts.constraints[type]) {
|
|
2848
|
+
const facts = constraintFacts.get(constraintAt(type, name));
|
|
2849
|
+
if (facts === undefined) {
|
|
2850
|
+
// The lookup is by name AND type here for the same reason the parser's
|
|
2851
|
+
// is, so "no constraint of this kind" and "no constraint at all" are
|
|
2852
|
+
// two different misses and say so.
|
|
2853
|
+
const kinds = constraintKinds.get(name) ?? [];
|
|
2854
|
+
if (kinds.length === 0) {
|
|
2855
|
+
throw new CompileError(`${at} activates ${type} constraint "${name}", which this rig does not declare`);
|
|
2856
|
+
}
|
|
2857
|
+
// `findConstraint(name, IkConstraintData)` resolves by name AND type,
|
|
2858
|
+
// and the parser throws on the miss.
|
|
2859
|
+
throw new CompileError(
|
|
2860
|
+
`${at} lists "${name}" under "${type}", but the rig declares it as a "${kinds.join('", "')}" constraint — ` +
|
|
2861
|
+
'a skin looks its constraints up by name AND type, so this one is a miss and the loader throws',
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
skinConstraintUse.add(constraintAt(type, name));
|
|
2865
|
+
if (!facts.skinRequired) {
|
|
2866
|
+
throw new CompileError(
|
|
2867
|
+
`${at} activates ${type} constraint "${name}", but that constraint does not declare \`"skin": true\`. ` +
|
|
2868
|
+
'A constraint is active unless it is skinRequired, so this list changes nothing.',
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
// The other direction, and the silent one that costs a pose: an object that
|
|
2876
|
+
// declared `skin: true` and appears in no list is switched off under every skin
|
|
2877
|
+
// there is. Outside the block above on purpose — a rig with no `skins` at all
|
|
2878
|
+
// is the strongest case of it. A listed bone activates its ancestors too
|
|
2879
|
+
// (`Skeleton.ts:198-205`), so a parent reachable only that way is not dead.
|
|
2880
|
+
const activated = new Set(skinBoneUse);
|
|
2881
|
+
const parentOf = new Map(spec.bones.map((b) => [b.name, b.parent]));
|
|
2882
|
+
for (const bone of [...activated]) {
|
|
2883
|
+
for (let cursor = parentOf.get(bone); cursor; cursor = parentOf.get(cursor)) activated.add(cursor);
|
|
2884
|
+
}
|
|
2885
|
+
for (const bone of spec.bones) {
|
|
2886
|
+
if (bone.skin === true && !activated.has(bone.name)) {
|
|
2887
|
+
throw new CompileError(
|
|
2888
|
+
`${where}: bone "${bone.name}" declares \`"skin": true\` but no skin activates it, so it is never active — ` +
|
|
2889
|
+
'list it in the skin it belongs to, or drop the flag',
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
for (const { type, name, skinRequired } of constraintsDeclared) {
|
|
2894
|
+
if (skinRequired && !skinConstraintUse.has(constraintAt(type, name))) {
|
|
2895
|
+
throw new CompileError(
|
|
2896
|
+
`${where}: ${type} constraint "${name}" declares \`"skin": true\` but no skin activates it, so it never runs — ` +
|
|
2897
|
+
`list it in that skin's "${type}" array, or drop the flag`,
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
if (raw.events !== undefined) {
|
|
2903
|
+
if (!isObj(raw.events)) {
|
|
2904
|
+
throw new CompileError(
|
|
2905
|
+
`${where}: "events" is an object keyed by event name (\`{ "footstep": {} }\`), not an array — the format's own shape`,
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
for (const [name, def] of Object.entries(raw.events)) {
|
|
2909
|
+
if (name.length === 0) throw new CompileError(`${where}: an event has an empty name`);
|
|
2910
|
+
if (!isObj(def)) {
|
|
2911
|
+
throw new CompileError(`${where}: event "${name}" must be an object of payload defaults (use {} for none)`);
|
|
2912
|
+
}
|
|
2913
|
+
for (const field of ['int', 'float', 'volume', 'balance'] as const) {
|
|
2914
|
+
const v = def[field];
|
|
2915
|
+
if (v !== undefined && (typeof v !== 'number' || !Number.isFinite(v))) {
|
|
2916
|
+
throw new CompileError(`${where}: event "${name}" has ${field} ${JSON.stringify(v)}, which is not a finite number`);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (def.int !== undefined && !Number.isInteger(def.int)) {
|
|
2920
|
+
throw new CompileError(`${where}: event "${name}" has int ${JSON.stringify(def.int)}; the payload is an integer`);
|
|
2921
|
+
}
|
|
2922
|
+
for (const field of ['string', 'audio'] as const) {
|
|
2923
|
+
if (def[field] !== undefined && typeof def[field] !== 'string') {
|
|
2924
|
+
throw new CompileError(`${where}: event "${name}" has ${field} ${JSON.stringify(def[field])}, which is not a string`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
// SkeletonJson.ts:478-481 reads these two ONLY inside `if (data.audioPath)`.
|
|
2928
|
+
// Without an audio path they are dropped with no error, so a spec that
|
|
2929
|
+
// wrote them down would carry a number no runtime ever reads.
|
|
2930
|
+
for (const field of ['volume', 'balance'] as const) {
|
|
2931
|
+
if (def[field] !== undefined && def.audio === undefined) {
|
|
2932
|
+
throw new CompileError(
|
|
2933
|
+
`${where}: event "${name}" declares ${field} but no "audio"; the parser reads ${field} only when an audio path is set, so it would be dropped in silence`,
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
return spec;
|
|
2941
|
+
}
|