rig-c 0.0.0-stage → 2.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1188 -0
- package/src/meshquality.ts +2042 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1425 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A21, the body (issue #1025, step 4c of #380): a generated mesh's rim — or
|
|
3
|
+
* a ribbon's entry row — is pinned to the slot's own bone, and a soft region
|
|
4
|
+
* moves only the vertices it declares. An `archetype` rule.
|
|
5
|
+
*
|
|
6
|
+
* Moved out of `src/validate.ts` whole: every clause is about the rig — which
|
|
7
|
+
* vertex sits on the region border or the hull, which bone carries it and at
|
|
8
|
+
* what weight — and a document's mesh states each of those (its `uvs`, its
|
|
9
|
+
* `hull`, its bindings by bone). Nothing it reads is the encoding's. The
|
|
10
|
+
* weights are the runtime's float32, a binding's bone its index in the roster
|
|
11
|
+
* (named back through the roster where the sentence names it), and a link's
|
|
12
|
+
* geometry its source's.
|
|
13
|
+
*/
|
|
14
|
+
import type { Verdicts } from '../harness.ts';
|
|
15
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
16
|
+
import { authoredMeshNames, meshKindOf } from '../mesh_kinds.ts';
|
|
17
|
+
import type { RigInfo } from '../../types.ts';
|
|
18
|
+
|
|
19
|
+
export function a21MeshRimPinned({ fail, skip }: Verdicts, { meshes, bones }: MeshFacts, rig: RigInfo | undefined): void {
|
|
20
|
+
// Without the rig, ring and ribbon cannot be told apart — and the two kinds
|
|
21
|
+
// pin OPPOSITE edges, so guessing one would either check the wrong edge or
|
|
22
|
+
// check nothing while reporting a pass.
|
|
23
|
+
if (!rig) {
|
|
24
|
+
return skip('A21_MESH_RIM_PINNED', 'no rig info (validating a bare directory), so ring and ribbon cannot be told apart');
|
|
25
|
+
}
|
|
26
|
+
if (!meshes.some((m) => m.weights)) {
|
|
27
|
+
return skip('A21_MESH_RIM_PINNED', 'the skeleton has no weighted mesh attachment, so there is no rim to find unpinned');
|
|
28
|
+
}
|
|
29
|
+
// An authored mesh has no rim rigc drew and no entry row rigc placed, and
|
|
30
|
+
// a linked mesh has no rim of its OWN at all — the one it draws belongs to
|
|
31
|
+
// its source, and is measured there. Nothing to measure is a SKIP — never
|
|
32
|
+
// a pass, and never a failure on somebody else's correct geometry.
|
|
33
|
+
//
|
|
34
|
+
// 🔸 A `segments` mesh is rigc's own geometry and still has no rim to pin:
|
|
35
|
+
// its outline is traced off the art and weighted by distance like every
|
|
36
|
+
// other vertex, because a layer pulled by named bones is SUPPOSED to move
|
|
37
|
+
// at its edge. "Pinned to the slot bone" is not a claim that generator
|
|
38
|
+
// makes, so there is nothing here to hold it to — and `A20`'s coherence
|
|
39
|
+
// rules, which it does make, still apply to it in full.
|
|
40
|
+
const rimless = (m: (typeof meshes)[number]): boolean => meshKindOf(m, rig) === 'authored' || meshKindOf(m, rig) === 'segments';
|
|
41
|
+
const measurable = meshes.filter((m) => m.weights && !rimless(m));
|
|
42
|
+
if (measurable.length === 0) {
|
|
43
|
+
const authored = authoredMeshNames(meshes.filter((m) => m.weights), rig);
|
|
44
|
+
const segmented = meshes.filter((m) => m.weights && meshKindOf(m, rig) === 'segments').map((m) => `"${m.name}"`);
|
|
45
|
+
return skip(
|
|
46
|
+
'A21_MESH_RIM_PINNED',
|
|
47
|
+
segmented.length === 0
|
|
48
|
+
? `every weighted mesh here is authored or linked geometry (${authored.join(', ')}), not a rigc ring or ` +
|
|
49
|
+
'ribbon — rigc did not place its rim, so it has no rim of its own to find unpinned'
|
|
50
|
+
: `every weighted mesh here is ${authored.length ? `authored or linked geometry (${authored.join(', ')}) or ` : ''}` +
|
|
51
|
+
`a "segments" lattice (${segmented.join(', ')}), not a rigc ring or ribbon — a segments mesh weights its ` +
|
|
52
|
+
'outline by distance to the bones it names, so it has no rim pinned to the slot bone to find unpinned',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
for (const mesh of measurable) {
|
|
56
|
+
if (!mesh.weights) continue;
|
|
57
|
+
const perVertexAll = mesh.weights;
|
|
58
|
+
const slotBoneOf = mesh.slotBone;
|
|
59
|
+
// A ribbon's outer boundary is SUPPOSED to move — that is the whole point
|
|
60
|
+
// of a strip that changes length. So the rule splits by mesh kind rather
|
|
61
|
+
// than being relaxed: for a ribbon the invariant is that the ENTRY row
|
|
62
|
+
// cannot move, because that row is where the strip joins the part it
|
|
63
|
+
// comes out of. Both rules protect the same thing (the mesh's join to the
|
|
64
|
+
// plate underneath); they just live at different edges of the mesh.
|
|
65
|
+
//
|
|
66
|
+
// A `contour` takes the hull path below, and that is the check it wants
|
|
67
|
+
// rather than a third branch: a contour mesh's hull IS every vertex it
|
|
68
|
+
// has, and every one is pinned to the slot bone at weight 1, so "the rim
|
|
69
|
+
// is pinned" reads over the whole mesh. If a later contour tier ever
|
|
70
|
+
// moves interior vertices, this is the assertion that has to grow a
|
|
71
|
+
// branch — it will fail rather than pass quietly, which is the right way
|
|
72
|
+
// round.
|
|
73
|
+
if (meshKindOf(mesh, rig) === 'ribbon') {
|
|
74
|
+
const uvs = mesh.regionUVs ?? [];
|
|
75
|
+
let entryRow = 0;
|
|
76
|
+
for (let v = 0; v < perVertexAll.length; v++) {
|
|
77
|
+
if (Math.abs(uvs[v * 2 + 1]) > 1e-6) continue; // not on the entry edge
|
|
78
|
+
entryRow++;
|
|
79
|
+
const vertex = perVertexAll[v];
|
|
80
|
+
if (vertex.length !== 1 || Math.abs(vertex[0].weight - 1) > 1e-6) {
|
|
81
|
+
fail(
|
|
82
|
+
'A21_MESH_RIM_PINNED',
|
|
83
|
+
`ribbon "${mesh.name}" entry vertex ${v} is not pinned (${vertex.map((w) => w.weight.toFixed(3)).join('+')})`,
|
|
84
|
+
);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (slotBoneOf && bones[vertex[0].bone] !== slotBoneOf) {
|
|
88
|
+
fail(
|
|
89
|
+
'A21_MESH_RIM_PINNED',
|
|
90
|
+
`ribbon "${mesh.name}" entry vertex ${v} is pinned to "${bones[vertex[0].bone]}", not the anchor bone "${slotBoneOf}"`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (entryRow < 2) {
|
|
95
|
+
fail('A21_MESH_RIM_PINNED', `ribbon "${mesh.name}" has ${entryRow} vertices on its entry edge; a strip needs two`);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// A rim a soft mask deliberately CARRIED (issue #382). The rule splits
|
|
100
|
+
// by declaration rather than being relaxed — the same move the ribbon
|
|
101
|
+
// branch above makes, and for the same reason: a wobbling silhouette is
|
|
102
|
+
// supposed to move, and the invariant is that nothing ELSE does. So on
|
|
103
|
+
// such a mesh a vertex is either pinned to the slot bone at 1 or shared
|
|
104
|
+
// between it and the ONE bone the rig declared, and a third bone, a
|
|
105
|
+
// wrong bone or a weight that does not close is still a failure.
|
|
106
|
+
const boundBone = rig.meshSoftBones[mesh.slot] ?? null;
|
|
107
|
+
if (boundBone !== null) {
|
|
108
|
+
const allowed = new Set([slotBoneOf, boundBone]);
|
|
109
|
+
let carried = 0;
|
|
110
|
+
for (let v = 0; v < perVertexAll.length; v++) {
|
|
111
|
+
const vertex = perVertexAll[v];
|
|
112
|
+
let sum = 0;
|
|
113
|
+
for (const { bone, weight } of vertex) {
|
|
114
|
+
const name = bones[bone];
|
|
115
|
+
if (!allowed.has(name)) {
|
|
116
|
+
fail(
|
|
117
|
+
'A21_MESH_RIM_PINNED',
|
|
118
|
+
`mesh "${mesh.name}" vertex ${v} is carried by "${name}", and this mesh declares only ` +
|
|
119
|
+
`"${slotBoneOf}" and the soft region's "${boundBone}"`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (name === boundBone && weight > 0) carried = carried + (weight >= 1 ? 1 : 0);
|
|
123
|
+
sum += weight;
|
|
124
|
+
}
|
|
125
|
+
if (Math.abs(sum - 1) > 1e-4) {
|
|
126
|
+
fail(
|
|
127
|
+
'A21_MESH_RIM_PINNED',
|
|
128
|
+
`mesh "${mesh.name}" vertex ${v} weights sum to ${sum.toFixed(4)}; a depth-bound mesh splits each ` +
|
|
129
|
+
'vertex between the slot bone and the bound bone, so it closes at 1',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// A mask that carried nothing reached here as a mesh pinned exactly
|
|
134
|
+
// as before, which is not the thing that was declared.
|
|
135
|
+
if (carried === 0) {
|
|
136
|
+
fail(
|
|
137
|
+
'A21_MESH_RIM_PINNED',
|
|
138
|
+
`mesh "${mesh.name}" declares a soft region on "${boundBone}" and no vertex is fully carried by it`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const hullVertices = mesh.hullLength / 2;
|
|
144
|
+
if (!Number.isInteger(hullVertices) || hullVertices < 3) {
|
|
145
|
+
fail('A21_MESH_RIM_PINNED', `mesh "${mesh.name}" declares hull ${mesh.hullLength / 2}; the rim must be a real ring`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const perVertex = mesh.weights;
|
|
149
|
+
if (hullVertices > perVertex.length) {
|
|
150
|
+
fail('A21_MESH_RIM_PINNED', `mesh "${mesh.name}" hull is ${hullVertices} of ${perVertex.length} vertices`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// The rim is the alpha contour where generated pixels meet untouched
|
|
154
|
+
// base. One bone at weight 1, and that bone must be the slot's own —
|
|
155
|
+
// anything else and the seam can move.
|
|
156
|
+
const slotBone = mesh.slotBone;
|
|
157
|
+
for (let i = 0; i < hullVertices; i++) {
|
|
158
|
+
const vertex = perVertex[i];
|
|
159
|
+
if (vertex.length !== 1 || Math.abs(vertex[0].weight - 1) > 1e-6) {
|
|
160
|
+
fail(
|
|
161
|
+
'A21_MESH_RIM_PINNED',
|
|
162
|
+
`mesh "${mesh.name}" rim vertex ${i} is not pinned (${vertex.map((v) => v.weight.toFixed(3)).join('+')})`,
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (slotBone && bones[vertex[0].bone] !== slotBone) {
|
|
167
|
+
fail(
|
|
168
|
+
'A21_MESH_RIM_PINNED',
|
|
169
|
+
`mesh "${mesh.name}" rim vertex ${i} is pinned to "${bones[vertex[0].bone]}", not the slot bone "${slotBone}"`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Independent of the ring ORDER: whatever sits on the region border is
|
|
174
|
+
// the outline, and the outline moving means the part's own edge moving.
|
|
175
|
+
// Without this, reordering the rings would move the pinned prefix off
|
|
176
|
+
// the outline and A21 would still pass on the count alone.
|
|
177
|
+
const uvs = mesh.regionUVs ?? [];
|
|
178
|
+
for (let v = 0; v < perVertex.length; v++) {
|
|
179
|
+
const u = uvs[v * 2];
|
|
180
|
+
const t = uvs[v * 2 + 1];
|
|
181
|
+
const onBorder = [u, t].some((c) => Math.abs(c) < 1e-6 || Math.abs(c - 1) < 1e-6);
|
|
182
|
+
if (!onBorder) continue;
|
|
183
|
+
const vertex = perVertex[v];
|
|
184
|
+
if (vertex.length !== 1 || Math.abs(vertex[0].weight - 1) > 1e-6) {
|
|
185
|
+
fail('A21_MESH_RIM_PINNED', `mesh "${mesh.name}" vertex ${v} is on the region border but not pinned`);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A22, the body (issue #1025, step 4c of #380): every mesh's authored UVs lie
|
|
3
|
+
* in the unit square, one pair per vertex.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the meshes
|
|
6
|
+
* are a fact (`../facts/mesh_attachments.ts`). The `check` call stays in
|
|
7
|
+
* `validate()`.
|
|
8
|
+
*/
|
|
9
|
+
import type { Verdicts } from '../harness.ts';
|
|
10
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
11
|
+
import { SKIP_NO_MESH_ATTACHMENT } from '../reasons.ts';
|
|
12
|
+
|
|
13
|
+
export function a22MeshUvsInUnitRange({ fail, skip }: Verdicts, { meshes }: Pick<MeshFacts, 'meshes'>): void {
|
|
14
|
+
const meshAttachments = meshes;
|
|
15
|
+
if (meshAttachments.length === 0) return skip('A22_MESH_UVS_IN_UNIT_RANGE', SKIP_NO_MESH_ATTACHMENT);
|
|
16
|
+
for (const mesh of meshAttachments) {
|
|
17
|
+
// `regionUVs` is what the JSON authored; `uvs` is the page-space result
|
|
18
|
+
// and stays EMPTY until a renderer calls computeUVs, so asserting on it
|
|
19
|
+
// here would be asserting on the wrong array (measured: length 0 after
|
|
20
|
+
// a clean load). With one part per page the two are equal anyway —
|
|
21
|
+
// computeUVs reduces to `u + regionUV * width` with u=0, width=1
|
|
22
|
+
// (MeshAttachment.js:173-174), which is the claim this assertion rests
|
|
23
|
+
// on and this is where it is checked.
|
|
24
|
+
const uvs = mesh.regionUVs;
|
|
25
|
+
if (!uvs || uvs.length !== mesh.worldVerticesLength) {
|
|
26
|
+
fail(
|
|
27
|
+
'A22_MESH_UVS_IN_UNIT_RANGE',
|
|
28
|
+
`mesh "${mesh.name}" has ${uvs?.length ?? 0} authored uv values for ${mesh.worldVerticesLength}`,
|
|
29
|
+
);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (let i = 0; i < uvs.length; i++) {
|
|
33
|
+
if (!Number.isFinite(uvs[i]) || uvs[i] < -1e-6 || uvs[i] > 1 + 1e-6) {
|
|
34
|
+
fail('A22_MESH_UVS_IN_UNIT_RANGE', `mesh "${mesh.name}" uv[${i}] is ${uvs[i]}`);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A23, the body (issue #1025, step 4c of #380): a physics constraint that
|
|
3
|
+
* does nothing, quietly — at its setup pose, and on every physics key.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` whole: every clause is about the rig — the
|
|
6
|
+
* components a constraint drives, its four bounded setup values, its `fps`,
|
|
7
|
+
* every value a physics timeline keys and what switches a muted constraint on
|
|
8
|
+
* — and a document's constraint and timeline records state each. Nothing it
|
|
9
|
+
* reads is the encoding's. The values are the ones the runtime holds after
|
|
10
|
+
* its parse, through the facts (`../facts/constraints.ts`): a field left out
|
|
11
|
+
* at the parser's value, `massInverse` = `1 / mass`, `step` = `1 / fps`, a
|
|
12
|
+
* key's time and value as float32, a key's value as the pose field it lands
|
|
13
|
+
* in (`posed`), and the constraints a timeline naming none reaches.
|
|
14
|
+
*/
|
|
15
|
+
import type { Verdicts } from '../harness.ts';
|
|
16
|
+
import type { ConstraintFacts, PhysicsSetup } from '../facts/constraints.ts';
|
|
17
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
18
|
+
import { noneKeysItsMixAbove0, PHYSICS_COMPONENTS, REST_OR_KEY_ITS_MIX, switchedOn } from '../constraint_words.ts';
|
|
19
|
+
import { SKIP_NO_PHYSICS_CONSTRAINT } from '../reasons.ts';
|
|
20
|
+
import {
|
|
21
|
+
PHYSICS_POSE_RULES,
|
|
22
|
+
physicsBasisFor,
|
|
23
|
+
physicsBasisSays,
|
|
24
|
+
physicsKeyRefusal,
|
|
25
|
+
physicsOutsideSays,
|
|
26
|
+
physicsRuleFor,
|
|
27
|
+
type PhysicsPoseRule,
|
|
28
|
+
} from '../../timelines.ts';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What A23 says about a SETUP pose outside its bound, per property.
|
|
32
|
+
*
|
|
33
|
+
* The predicate lives in `PHYSICS_POSE_RULES` and the sentence lives here, and
|
|
34
|
+
* the split is deliberate: the predicate is the thing the compiler and this file
|
|
35
|
+
* must not disagree about, while the sentence names what happens to THIS rig —
|
|
36
|
+
* "it is muted", "nothing pulls it back" — which is what the author acts on and
|
|
37
|
+
* is worth nothing to a compiler refusing a key. `PHYSICS_POSE_RULES` is the
|
|
38
|
+
* index, so a rule added there with no sentence here fails to type-check rather
|
|
39
|
+
* than printing `undefined`.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ These are the SETUP pose's sentences and nothing else, which matters on the
|
|
42
|
+
* two rows whose keyed bound is wider than their resting one — `mix` since issue
|
|
43
|
+
* #610 and `strength` since #727. A key of 0 on either is accepted, so "it is
|
|
44
|
+
* muted" and "nothing pulls it back" are read here by a rig that states the
|
|
45
|
+
* number at rest, and what a key is refused with is the row's own `why`.
|
|
46
|
+
*
|
|
47
|
+
* `animations` is how many animations the skeleton declares, and only the `mix`
|
|
48
|
+
* sentence reads it: that bound is the one a key can satisfy instead of the
|
|
49
|
+
* setup pose (`inertAtSetup`, issue #743), so the refusal has to say that the
|
|
50
|
+
* other half was looked for and how wide the search was. A rig with no animation
|
|
51
|
+
* at all then says "none of the 0 animations" rather than implying somebody
|
|
52
|
+
* keyed something.
|
|
53
|
+
*
|
|
54
|
+
* `rule` is the row the value was judged by, and every sentence reads it: the
|
|
55
|
+
* reason each prints is the row's `basis` arm for the value (issue #798), the
|
|
56
|
+
* same object the key's sentence quotes, so what the setup pose and a key say
|
|
57
|
+
* about one number is one text — and whether that reason is the runtime's
|
|
58
|
+
* arithmetic or rigc's call is stated rather than implied.
|
|
59
|
+
*/
|
|
60
|
+
const SETUP_POSE_SAYS: Record<string, (pose: PhysicsSetup, animations: number, rule: PhysicsPoseRule) => string> = {
|
|
61
|
+
// Every reason below is the row's `basis` arm for the value (issue #798): an
|
|
62
|
+
// arithmetic arm names its expression, a behavioural one says refusing it is
|
|
63
|
+
// rigc's call and what the value does. ⚠️ Until then a setup mix BELOW 0 was
|
|
64
|
+
// told "it is muted", which it is not — [measured] it moves the bone by
|
|
65
|
+
// exactly −1× what +0.5 does — so the two ways out print two sentences.
|
|
66
|
+
mix: (pose, animations, rule) =>
|
|
67
|
+
`has mix ${pose.mix} and ${noneKeysItsMixAbove0(animations)}; ${setupBasisSays(rule, pose.mix)} — ${REST_OR_KEY_ITS_MIX}`,
|
|
68
|
+
mass: (pose, _animations, rule) => `has massInverse ${pose.massInverse}; mass must be ${rule.states} — ${setupBasisSays(rule, pose.massInverse)}`,
|
|
69
|
+
// Two arms, read off the row (issue #748): 0 is a constraint nothing pulls
|
|
70
|
+
// back and below 0 one that is pushed away, and the row's `why` — the key's
|
|
71
|
+
// sentence — quotes the second from the same object. The basis sentence ends
|
|
72
|
+
// on the arm's own words, which `T100` holds.
|
|
73
|
+
strength: (pose, _animations, rule) => `has strength ${pose.strength}; ${setupBasisSays(rule, pose.strength)}`,
|
|
74
|
+
// The bound is read off the row, so the setup pose and a key cannot state two
|
|
75
|
+
// intervals; the reason is the arm the value took, since the ends are inside (#794).
|
|
76
|
+
damping: (pose, _animations, rule) =>
|
|
77
|
+
`has damping ${pose.damping}; must be ${rule.states} — the per-step decay is \`damping ** (60 * step)\`, and ` +
|
|
78
|
+
setupBasisSays(rule, pose.damping),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The basis sentence for a setup value, or the bound itself where no arm holds
|
|
83
|
+
* — a pose value the parser handed over that none of the row's arms is about
|
|
84
|
+
* (a NaN, say), which has no reason to give and must not borrow another's.
|
|
85
|
+
*/
|
|
86
|
+
function setupBasisSays(rule: PhysicsPoseRule, poseValue: number): string {
|
|
87
|
+
const arm = physicsBasisFor(rule, poseValue);
|
|
88
|
+
return arm === undefined ? physicsOutsideSays(rule, poseValue) : physicsBasisSays(arm);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Every failure mode here is silent. The five component fields default to
|
|
92
|
+
// 0, so a constraint can drive nothing at all; `mix` 0 mutes it; `mass` 0
|
|
93
|
+
// becomes an infinite massInverse; and `damping` above 1 never settles, which
|
|
94
|
+
// on a mesh-driving bone means the canvas re-rasterises forever. 1 itself is
|
|
95
|
+
// inside the bound since issue #794: it never decays, which is finite and a
|
|
96
|
+
// choice, and what it costs a consumer's frame is the consumer's to judge.
|
|
97
|
+
//
|
|
98
|
+
// 🔑 **Two arms, one criterion.** The second arm below reads every physics
|
|
99
|
+
// TIMELINE key, and it is written against `PHYSICS_POSE_RULES` — the table
|
|
100
|
+
// this one is also written against, and the table `compileValueTrack`
|
|
101
|
+
// refuses a spec's own out-of-range number with. Three readings of one rule
|
|
102
|
+
// is how two of them come to disagree, so there is one (issue #610).
|
|
103
|
+
export function a23PhysicsConstraintEffective({ fail, skip, stats }: Verdicts, facts: ConstraintFacts, meshFacts: MeshFacts): void {
|
|
104
|
+
// ⟨subject⟩_⟨property⟩, and its two siblings already read this way: A36
|
|
105
|
+
// skips on "the skeleton declares no path constraint" and A37 on "no
|
|
106
|
+
// slider constraint", while this one passed over an empty filter (#580).
|
|
107
|
+
// The stat is written before the guard so a reader of a SKIP still sees
|
|
108
|
+
// the count that produced it.
|
|
109
|
+
stats.physicsConstraints = facts.constraints.filter((c) => c.physics !== undefined).length;
|
|
110
|
+
if (stats.physicsConstraints === 0) return skip('A23_PHYSICS_CONSTRAINT_EFFECTIVE', SKIP_NO_PHYSICS_CONSTRAINT);
|
|
111
|
+
const meshBoneNames = new Set<string>();
|
|
112
|
+
for (const mesh of meshFacts.meshes) meshBoneNames.add(mesh.slotBone);
|
|
113
|
+
for (const mesh of meshFacts.meshes) {
|
|
114
|
+
if (!mesh.weights) continue;
|
|
115
|
+
for (const vertex of mesh.weights) {
|
|
116
|
+
for (const { bone: index } of vertex) {
|
|
117
|
+
const bone = meshFacts.bones[index];
|
|
118
|
+
if (bone !== undefined) meshBoneNames.add(bone);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// --- which constraints an animation switches ON (issue #743) ----------
|
|
123
|
+
//
|
|
124
|
+
// 🔑 The setup pose is the rig AT REST, and one of the four bounds is a
|
|
125
|
+
// state rather than a break there: `PHYSICS_POSE_RULES` marks `mix`
|
|
126
|
+
// `inertAtSetup`, because `update` opens with `if (mix === 0) return;`
|
|
127
|
+
// (`PhysicsConstraint.js:109-111`) and nothing else in the pose has such a
|
|
128
|
+
// branch. So a constraint that rests muted and is keyed above 0 by an
|
|
129
|
+
// animation is a rig the runtime plays as authored, and refusing it would
|
|
130
|
+
// refuse a design: physics off at rest, switched on by the animation that
|
|
131
|
+
// needs it.
|
|
132
|
+
//
|
|
133
|
+
// 📏 Measured on a generated physics fixture, 36 steps at 60 fps with the
|
|
134
|
+
// constraint's own bone swung by its parent: resting at `mix` 0 with an
|
|
135
|
+
// animation keying `mix` to 1 poses the bone IDENTICALLY to the same rig
|
|
136
|
+
// resting at 1 (max |dx| 0.000000) and up to 7.771177 away from the twin
|
|
137
|
+
// that keys nothing — which poses identically to one keyed to 0 only
|
|
138
|
+
// (max |dx| 0.000000). Two states, and the file says which.
|
|
139
|
+
//
|
|
140
|
+
// ⚠️ The escape is the rule's own field rather than the word "mix": a
|
|
141
|
+
// setup `mass` of 0 is `massInverse` Infinity BEFORE anything plays, and
|
|
142
|
+
// [measured] at rest it reads NaN on every frame although an animation
|
|
143
|
+
// keys `mass` to 1. A key cannot rescue a value that has already broken
|
|
144
|
+
// the rig it is resting in.
|
|
145
|
+
//
|
|
146
|
+
// The keys are read by `switchedOn`, the one reading A36 and A37 share
|
|
147
|
+
// (issue #752), through the runtime's own accessor: the pose field is
|
|
148
|
+
// where the integrator reads the number, and for `mass` that is not the
|
|
149
|
+
// number the key states. It counts the unnamed global form through the
|
|
150
|
+
// timeline's reach and a Bezier segment's samples as well as its keys.
|
|
151
|
+
const unmuted = new Map<number, Set<string>>();
|
|
152
|
+
for (const rule of PHYSICS_POSE_RULES) {
|
|
153
|
+
if (!rule.inertAtSetup) continue;
|
|
154
|
+
const reached = switchedOn(
|
|
155
|
+
facts,
|
|
156
|
+
(timeline) => timeline.kind === 'physics' && timeline.word === rule.timeline,
|
|
157
|
+
1,
|
|
158
|
+
(timeline, value) => rule.poseOk(timeline.posed === undefined ? value : timeline.posed(value)),
|
|
159
|
+
);
|
|
160
|
+
facts.constraints.forEach((one, index) => {
|
|
161
|
+
if (one.physics === undefined || !reached.has(index)) return;
|
|
162
|
+
const by = unmuted.get(index) ?? new Set<string>();
|
|
163
|
+
by.add(rule.timeline);
|
|
164
|
+
unmuted.set(index, by);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
let mutedUntilKeyed = 0;
|
|
168
|
+
facts.constraints.forEach((constraint, index) => {
|
|
169
|
+
const physics = constraint.physics;
|
|
170
|
+
if (physics === undefined) return;
|
|
171
|
+
const where = `physics "${constraint.name}"`;
|
|
172
|
+
const components = PHYSICS_COMPONENTS.filter((k) => physics.components[k] > 0);
|
|
173
|
+
if (!components.length) {
|
|
174
|
+
fail('A23_PHYSICS_CONSTRAINT_EFFECTIVE', `${where} drives no component; it parses and does nothing`);
|
|
175
|
+
}
|
|
176
|
+
// --- a lever drive on a bone with no lever (issue #1195) ---------------
|
|
177
|
+
//
|
|
178
|
+
// `rotate`, `shearX` and `scaleX` are stepped off the bone's tip,
|
|
179
|
+
// `length·(a, c)` (`stepPhysics` in `src/core/constraints_physics.ts`,
|
|
180
|
+
// held to spine-core at tolerance 0); `x` and `y` read only the origin, so
|
|
181
|
+
// they are not refused here — the editor's own example exports and
|
|
182
|
+
// rigc's public builds carry them on bones of length 0 and they step as
|
|
183
|
+
// authored. The two lever readings fail in opposite directions, so they
|
|
184
|
+
// print two sentences:
|
|
185
|
+
//
|
|
186
|
+
// 📏 rotate / shearX: the offset chases `atan2(dy + ty, dx + tx)`, and with
|
|
187
|
+
// no tip and no motion that is `atan2(0, 0)` = 0 — a WORLD direction —
|
|
188
|
+
// where any tip at all makes it the bone's own previous direction.
|
|
189
|
+
// Measured through the core on a four-bone chain held still for 3 s
|
|
190
|
+
// (inertia 0.5, strength 100, damping 0.85, weight 0.2794): at length 0
|
|
191
|
+
// the bone turns 26.602° from a 45° rest, 53.204° from 90°, 106.408° from
|
|
192
|
+
// 180°, and holds there; at length 0.01 it moves 0.0000. The swing under
|
|
193
|
+
// motion, by contrast, is continuous in the length (0.914° at 36.5 up to
|
|
194
|
+
// 64.934° at 0 on one bounce) — that is the author's tuning, not a defect,
|
|
195
|
+
// and this clause does not judge it.
|
|
196
|
+
//
|
|
197
|
+
// ⚠️ The rest ANGLE is not read. The same probe resting at world 0° moves
|
|
198
|
+
// 0.000° at length 0 — but only because the bone already points where the
|
|
199
|
+
// solver aims it. A key that turns the bone, or a parent that turns it,
|
|
200
|
+
// breaks that, so a length-0 bone resting at 0° is the same defect waiting
|
|
201
|
+
// for its first rotation, and refusing it by the angle would refuse a
|
|
202
|
+
// pose rather than a rig.
|
|
203
|
+
//
|
|
204
|
+
// 📏 scaleX: the along-bone motion is divided by `length·|x column|` and
|
|
205
|
+
// nothing is added when that is 0, so the drive does nothing — every
|
|
206
|
+
// length-0 rung of the same probe read 0.0000, where 0.01 threw the tip
|
|
207
|
+
// 806 units. It is this assertion's first clause reached through the bone
|
|
208
|
+
// rather than the component fields.
|
|
209
|
+
if (physics.boneLength === 0) {
|
|
210
|
+
const turns = components.filter((k) => k === 'rotate' || k === 'shearX');
|
|
211
|
+
if (turns.length) {
|
|
212
|
+
fail(
|
|
213
|
+
'A23_PHYSICS_CONSTRAINT_EFFECTIVE',
|
|
214
|
+
`${where} drives ${turns.join(' and ')} on bone "${physics.bone}" whose length is 0 — with no tip the solver aims the bone ` +
|
|
215
|
+
`at world angle 0 and holds it there; give "${physics.bone}" a length or drive x/y instead`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (components.includes('scaleX')) {
|
|
219
|
+
fail(
|
|
220
|
+
'A23_PHYSICS_CONSTRAINT_EFFECTIVE',
|
|
221
|
+
`${where} drives scaleX on bone "${physics.bone}" whose length is 0, which the solver divides the motion by, so it drives ` +
|
|
222
|
+
`nothing; give "${physics.bone}" a length`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const pose = physics.setup;
|
|
227
|
+
// The four bounded fields, each judged by its `PHYSICS_POSE_RULES` row.
|
|
228
|
+
// The wording is per-field and stays so: "it is muted" and "nothing
|
|
229
|
+
// pulls it back" say what happens to THIS rig, which is what the author
|
|
230
|
+
// needs, and the shared table supplies the predicate rather than the
|
|
231
|
+
// sentence.
|
|
232
|
+
for (const rule of PHYSICS_POSE_RULES) {
|
|
233
|
+
if (rule.poseOk(pose[rule.field])) continue;
|
|
234
|
+
if (rule.inertAtSetup && unmuted.get(index)?.has(rule.timeline)) {
|
|
235
|
+
mutedUntilKeyed++;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const drivesAMesh = rule.timeline === 'damping' && meshBoneNames.has(physics.bone);
|
|
239
|
+
fail(
|
|
240
|
+
'A23_PHYSICS_CONSTRAINT_EFFECTIVE',
|
|
241
|
+
`${where} ${SETUP_POSE_SAYS[rule.timeline](pose, facts.animations, rule)}` +
|
|
242
|
+
(drivesAMesh ? ' — and this bone drives a mesh, so the canvas never rests' : ''),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (physics.step <= 0 || !Number.isFinite(physics.step)) {
|
|
246
|
+
fail('A23_PHYSICS_CONSTRAINT_EFFECTIVE', `${where} has step ${physics.step} (fps must be > 0)`);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// What the PASS would otherwise not say: this rig rests with physics off
|
|
250
|
+
// on that many constraints and an animation is what switches them on. A
|
|
251
|
+
// pass carries no detail — `passed` is a list of names — so `stats` is the
|
|
252
|
+
// channel that exists, and a number is what belongs in a line printed as
|
|
253
|
+
// `k=v`: naming every such constraint and the animations that reach it
|
|
254
|
+
// would be a paragraph on one line, and the rig where nobody meant it is
|
|
255
|
+
// the rig where the NUMBER is the surprise. Absent rather than 0 when
|
|
256
|
+
// nothing rests muted, so it appears only where it says something.
|
|
257
|
+
if (mutedUntilKeyed) stats.physicsMutedUntilKeyed = mutedUntilKeyed;
|
|
258
|
+
|
|
259
|
+
// --- the same criterion, on every physics timeline key (issue #610) ----
|
|
260
|
+
//
|
|
261
|
+
// The arm above reads the setup pose and, until this one existed, nothing
|
|
262
|
+
// else — complete while a motion spec could key `mix` and `reset` only,
|
|
263
|
+
// and incomplete from #593 on, when it became able to key all seven.
|
|
264
|
+
//
|
|
265
|
+
// 📏 Measured on the tree before this landed, one keyed value at a time on
|
|
266
|
+
// a generated physics rig, 24 steps at 60 fps: `mass: 0` reached
|
|
267
|
+
// `massInverse` Infinity and every offset NaN — named by
|
|
268
|
+
// `A10_NO_NAN_AFTER_STEPPING`, from the BONE, with no word about the
|
|
269
|
+
// animation, the constraint, the timeline or the key; `damping: 2` ran the
|
|
270
|
+
// x offset to −26,634 and the velocity to −1.55e6 and still climbing, with
|
|
271
|
+
// **zero** gate failures; `strength: 0` drifted monotonically with zero gate
|
|
272
|
+
// failures; `mix: 1.5` produced zero gate failures and an integration
|
|
273
|
+
// identical to `mix: 1`, because mix multiplies the finished offset onto
|
|
274
|
+
// the bone and never enters the solve.
|
|
275
|
+
//
|
|
276
|
+
// ⭐ The value is judged where the runtime keeps it, not where the file
|
|
277
|
+
// writes it: `posed` is the number the runtime's own accessor writes, so a
|
|
278
|
+
// `mass` key lands as its reciprocal and is then held to exactly the
|
|
279
|
+
// predicate the setup arm holds `massInverse` to.
|
|
280
|
+
let keysRead = 0;
|
|
281
|
+
for (const timeline of facts.timelines) {
|
|
282
|
+
// A `reset` timeline keys no value, and was never one of the seven the pose holds.
|
|
283
|
+
if (timeline.kind !== 'physics' || timeline.word === 'reset') continue;
|
|
284
|
+
const name = timeline.word;
|
|
285
|
+
const rule = physicsRuleFor(name);
|
|
286
|
+
// -1 is the global form: the timeline drives every physics constraint
|
|
287
|
+
// whose matching `…Global` flag is set, so there is no one name to give.
|
|
288
|
+
const target =
|
|
289
|
+
timeline.constraint === -1
|
|
290
|
+
? 'every physics constraint'
|
|
291
|
+
: `physics "${facts.constraints[timeline.constraint]?.name ?? `#${timeline.constraint}`}"`;
|
|
292
|
+
for (const frame of timeline.frames) {
|
|
293
|
+
keysRead++;
|
|
294
|
+
if (rule === undefined) continue;
|
|
295
|
+
const value = frame.value;
|
|
296
|
+
const refusal = physicsKeyRefusal(rule, value, timeline.posed === undefined ? undefined : timeline.posed(value));
|
|
297
|
+
if (refusal === null) continue;
|
|
298
|
+
fail(
|
|
299
|
+
'A23_PHYSICS_CONSTRAINT_EFFECTIVE',
|
|
300
|
+
`animation "${timeline.animation}" ${target} ${name} key at t=${frame.time.toFixed(6)}s is ${refusal}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
stats.physicsTimelineKeys = keysRead;
|
|
305
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A24, the body (issue #1025, cut 4c-4 of #380): motion under the axis bone
|
|
3
|
+
* stays in AXIS space, and the axis bone itself carries no keys.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the bone
|
|
6
|
+
* timelines are a fact (`../facts/bone_timelines.ts`) rather than the raw
|
|
7
|
+
* JSON's `animations`, walked in the same order, and the rig info is the
|
|
8
|
+
* caller's. Why the rule exists — the generator that wrote screen-space travel
|
|
9
|
+
* — stays above the `check` call in `validate()`.
|
|
10
|
+
*/
|
|
11
|
+
import type { Verdicts } from '../harness.ts';
|
|
12
|
+
import type { BoneTimelineFacts } from '../facts/bone_timelines.ts';
|
|
13
|
+
import type { RigInfo } from '../../types.ts';
|
|
14
|
+
import { isObj, type Json } from '../values.ts';
|
|
15
|
+
|
|
16
|
+
export function a24AxisSpaceStroke({ fail, skip }: Verdicts, { boneTimelines }: BoneTimelineFacts, input: { rig?: RigInfo }): void {
|
|
17
|
+
const rig = input.rig;
|
|
18
|
+
if (!rig) return skip('A24_AXIS_SPACE_STROKE', 'no rig info (validating a bare directory)');
|
|
19
|
+
if (!rig.axisBone) {
|
|
20
|
+
return skip(
|
|
21
|
+
'A24_AXIS_SPACE_STROKE',
|
|
22
|
+
`the rig "${rig.archetype}" declares no axis bone, so there is no axis space for a stroke to leave`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const subtree = new Set(rig.axisSubtree);
|
|
26
|
+
// The stroke is the subject and its keys are where it lives (#580). A rig
|
|
27
|
+
// that names an axis bone and then keys neither it nor anything under it has
|
|
28
|
+
// no stroke for this rule to find out of axis space, and a loop over nothing
|
|
29
|
+
// used to report that as held.
|
|
30
|
+
let keyed = 0;
|
|
31
|
+
for (const { animation: animName, bone: boneName, timelines } of boneTimelines) {
|
|
32
|
+
if (boneName === rig.axisBone) {
|
|
33
|
+
keyed++;
|
|
34
|
+
fail(
|
|
35
|
+
'A24_AXIS_SPACE_STROKE',
|
|
36
|
+
`"${animName}" keys the axis bone "${boneName}"; the axis angle is a per-cut SETUP value, not animation`,
|
|
37
|
+
);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (subtree.has(boneName)) keyed++;
|
|
41
|
+
if (!subtree.has(boneName) || !isObj(timelines)) continue;
|
|
42
|
+
if ('translatey' in timelines) {
|
|
43
|
+
fail(
|
|
44
|
+
'A24_AXIS_SPACE_STROKE',
|
|
45
|
+
`"${animName}" gives "${boneName}" a translatey timeline; a bone under "${rig.axisBone}" moves along the axis only`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const keys = (timelines as Json).translate;
|
|
49
|
+
if (!Array.isArray(keys)) continue;
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
if (!isObj(key)) continue;
|
|
52
|
+
const y = Number(key.y ?? 0);
|
|
53
|
+
if (Number.isFinite(y) && Math.abs(y) > 1e-6) {
|
|
54
|
+
fail(
|
|
55
|
+
'A24_AXIS_SPACE_STROKE',
|
|
56
|
+
`"${animName}" keys "${boneName}" translate y=${y} at t=${String(key.time ?? 0)}; the axis bone carries the direction, so keys are translateX only`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (keyed === 0) {
|
|
62
|
+
return skip(
|
|
63
|
+
'A24_AXIS_SPACE_STROKE',
|
|
64
|
+
`no animation keys the axis bone "${rig.axisBone}" or any of the ${rig.axisSubtree.length} bone(s) under ` +
|
|
65
|
+
'it, so this rig has no stroke to hold in axis space',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A25, the body (issue #1025, cut 4c-4 of #380): a bone the rig declares
|
|
3
|
+
* detached from another is not its descendant.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the bones
|
|
6
|
+
* and their parents are a fact (`../facts/skeleton_roster.ts`) rather than the
|
|
7
|
+
* raw JSON's `bones`, and the rig info is the caller's. Why some bones are
|
|
8
|
+
* detached on purpose stays above the `check` call in `validate()`.
|
|
9
|
+
*/
|
|
10
|
+
import type { Verdicts } from '../harness.ts';
|
|
11
|
+
import type { SkeletonRosterFacts } from '../facts/skeleton_roster.ts';
|
|
12
|
+
import type { RigInfo } from '../../types.ts';
|
|
13
|
+
|
|
14
|
+
export function a25DetachedBoneParentage({ fail, skip }: Verdicts, { bones }: SkeletonRosterFacts, input: { rig?: RigInfo }): void {
|
|
15
|
+
const rig = input.rig;
|
|
16
|
+
if (!rig) return skip('A25_DETACHED_BONE_PARENTAGE', 'no rig info (validating a bare directory)');
|
|
17
|
+
if (!rig.detached.length) {
|
|
18
|
+
return skip('A25_DETACHED_BONE_PARENTAGE', `the rig "${rig.archetype}" declares no forbidden parentage`);
|
|
19
|
+
}
|
|
20
|
+
const parentOf = new Map<string, string | null>();
|
|
21
|
+
for (const bone of bones) parentOf.set(bone.name, bone.parent);
|
|
22
|
+
for (const [child, forbidden] of rig.detached) {
|
|
23
|
+
if (!parentOf.has(child)) {
|
|
24
|
+
fail('A25_DETACHED_BONE_PARENTAGE', `the rig declares "${child}" detached from "${forbidden}" but has no such bone`);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (let cursor = parentOf.get(child) ?? null; cursor; cursor = parentOf.get(cursor) ?? null) {
|
|
29
|
+
if (seen.has(cursor)) break; // a cycle; the loader would have thrown first
|
|
30
|
+
seen.add(cursor);
|
|
31
|
+
if (cursor !== forbidden) continue;
|
|
32
|
+
fail(
|
|
33
|
+
'A25_DETACHED_BONE_PARENTAGE',
|
|
34
|
+
`"${child}" is a descendant of "${forbidden}"; it must not be dragged by that bone's motion`,
|
|
35
|
+
);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|