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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A26, the body (issue #1025, cut 4c-4 of #380): the slots array IS the rig's
|
|
3
|
+
* slot table — nothing out of order and nothing missing.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the slots'
|
|
6
|
+
* names are a fact (`../facts/skeleton_roster.ts`) rather than the raw JSON's
|
|
7
|
+
* `slots`, and the rig info is the caller's. Why the second half exists (issue
|
|
8
|
+
* #575) 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 a26SlotDrawOrder({ fail, skip }: Verdicts, { slots }: SkeletonRosterFacts, input: { rig?: RigInfo }): void {
|
|
15
|
+
if (!input.rig) return skip('A26_SLOT_DRAW_ORDER', 'no rig info (validating a bare directory)');
|
|
16
|
+
const order = input.rig.slotOrder;
|
|
17
|
+
if (!order) {
|
|
18
|
+
return skip(
|
|
19
|
+
'A26_SLOT_DRAW_ORDER',
|
|
20
|
+
`the rig "${input.rig.archetype}" declares no canonical slot order, so the emitted order has nothing to disagree with`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const names = slots.map((s) => s.name);
|
|
24
|
+
// ⛔ **No empty-subject SKIP here, and #575 is the whole reason** (#580). The
|
|
25
|
+
// empty sequence is a subsequence of every table, so before #575 a skeleton
|
|
26
|
+
// with no slot reported its draw order held and this rule wanted the same
|
|
27
|
+
// guard its neighbours got. It no longer does: the completeness clause below
|
|
28
|
+
// reads zero emitted slots against a declared table as EVERY slot lost,
|
|
29
|
+
// which is the maximal case of exactly the defect #575 filed — so the honest
|
|
30
|
+
// verdict is a FAIL naming them, and a SKIP here would suppress it. Measured:
|
|
31
|
+
// with the guard in place a rig declaring one slot beside an artifact
|
|
32
|
+
// carrying none reported SKIP; with it gone, `"block" is declared and not
|
|
33
|
+
// emitted`. A26 therefore has no vacuous pass left to convert, the way
|
|
34
|
+
// `A07_ATLAS_TEXT_SHAPE` has none.
|
|
35
|
+
let at = 0;
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const found = order.indexOf(name, at);
|
|
38
|
+
if (found < 0) {
|
|
39
|
+
const known = order.indexOf(name);
|
|
40
|
+
fail(
|
|
41
|
+
'A26_SLOT_DRAW_ORDER',
|
|
42
|
+
known < 0
|
|
43
|
+
? `slot "${name}" is not in the archetype's slot table`
|
|
44
|
+
: `slot "${name}" is drawn out of order (table position ${known}, after a slot at ${at})`,
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
at = found + 1;
|
|
49
|
+
}
|
|
50
|
+
const emitted = new Set(names);
|
|
51
|
+
const missing = order.filter((name) => !emitted.has(name));
|
|
52
|
+
if (missing.length > 0) {
|
|
53
|
+
fail(
|
|
54
|
+
'A26_SLOT_DRAW_ORDER',
|
|
55
|
+
`the rig "${input.rig.archetype}" declares ${order.length} slot(s) and the skeleton has ${names.length}: ` +
|
|
56
|
+
`${missing.map((name) => `"${name}"`).join(', ')} ${missing.length === 1 ? 'is' : 'are'} declared and ` +
|
|
57
|
+
'not emitted. A slot nothing fills is emitted with no setup attachment, not dropped — dropping one ' +
|
|
58
|
+
'moves every slot below it up one index',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A27, the body (issue #1025, step 4c of #380): the only region on a page is
|
|
3
|
+
* named for the page's file.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the regions
|
|
6
|
+
* are a fact (`../facts/atlas_regions.ts`). Why the second link of the
|
|
7
|
+
* attachment -> region -> file chain needs holding stays above the `check`
|
|
8
|
+
* call in `validate()`.
|
|
9
|
+
*/
|
|
10
|
+
import { basename } from 'node:path';
|
|
11
|
+
import type { Verdicts } from '../harness.ts';
|
|
12
|
+
import type { AtlasRegionFacts } from '../facts/atlas_regions.ts';
|
|
13
|
+
import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_REGION } from '../reasons.ts';
|
|
14
|
+
|
|
15
|
+
export function a27RegionNameMatchesPageFilename({ fail, skip }: Verdicts, { atlas }: AtlasRegionFacts): void {
|
|
16
|
+
if (!atlas) return skip('A27_REGION_NAME_MATCHES_PAGE_FILENAME', SKIP_NO_ATLAS);
|
|
17
|
+
if (atlas.regions.length === 0) return skip('A27_REGION_NAME_MATCHES_PAGE_FILENAME', SKIP_NO_ATLAS_REGION);
|
|
18
|
+
const perPage = new Map<string, number>();
|
|
19
|
+
for (const region of atlas.regions) perPage.set(region.page.name, (perPage.get(region.page.name) ?? 0) + 1);
|
|
20
|
+
for (const region of atlas.regions) {
|
|
21
|
+
// A real packer puts many regions on one page and the names stop matching
|
|
22
|
+
// filenames by design. Then this check has nothing to say, so it says
|
|
23
|
+
// nothing rather than something wrong.
|
|
24
|
+
if ((perPage.get(region.page.name) ?? 0) !== 1) continue;
|
|
25
|
+
const expected = basename(region.page.name).replace(/\.png$/i, '');
|
|
26
|
+
if (region.name !== expected) {
|
|
27
|
+
fail(
|
|
28
|
+
'A27_REGION_NAME_MATCHES_PAGE_FILENAME',
|
|
29
|
+
`region "${region.name}" is the only region on page "${region.page.name}", whose basename is "${expected}"`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A28, the body (issue #1025, step 4c of #380): a ribbon's rows share their
|
|
3
|
+
* weights. An `archetype` rule.
|
|
4
|
+
*
|
|
5
|
+
* This is what makes "length without width" a property of the file rather than
|
|
6
|
+
* a hope. Both vertices of a row carry the same bones at the same weights, so
|
|
7
|
+
* whatever the chain does to one it does to the other and their separation can
|
|
8
|
+
* only rotate — the strip curves and stretches, and never gets fatter. Give one
|
|
9
|
+
* side a different weight and the strip develops a taper that grows with its
|
|
10
|
+
* travel, which is the sort of thing that reads as bad art rather than as a bug.
|
|
11
|
+
*
|
|
12
|
+
* Moved out of `src/validate.ts` whole: both clauses are about the rig — a
|
|
13
|
+
* document's ribbon states every vertex's bindings — and the row's shape is
|
|
14
|
+
* printed as the file holds it, each bone by its index in the roster and each
|
|
15
|
+
* weight as the runtime's float32. The guard on a skeleton the round trip did
|
|
16
|
+
* not load stays in `validate()`, which has no facts to hand over then.
|
|
17
|
+
*/
|
|
18
|
+
import type { Verdicts } from '../harness.ts';
|
|
19
|
+
import type { MeshFacts } from '../facts/mesh_attachments.ts';
|
|
20
|
+
import type { RigInfo } from '../../types.ts';
|
|
21
|
+
|
|
22
|
+
export function a28RibbonRowsShareWeights({ fail, skip }: Verdicts, facts: MeshFacts | null, rig: RigInfo | undefined): void {
|
|
23
|
+
if (facts === null) return skip('A28_RIBBON_ROWS_SHARE_WEIGHTS', 'the skeleton did not load (A00 owns that failure)');
|
|
24
|
+
if (!rig) return skip('A28_RIBBON_ROWS_SHARE_WEIGHTS', 'no rig info (validating a bare directory), so no mesh is known to be a ribbon');
|
|
25
|
+
const kinds = rig.meshKinds;
|
|
26
|
+
if (!Object.values(kinds).includes('ribbon')) {
|
|
27
|
+
// Say WHICH kind of "no ribbon" this is. "declares no ribbon" is true of a
|
|
28
|
+
// cut with no mesh at all, of one whose meshes are authored geometry, and
|
|
29
|
+
// of one whose meshes are contours — and the last two are cases where a
|
|
30
|
+
// reader should know that a mesh went unmeasured on purpose.
|
|
31
|
+
const unpaired = Object.entries(kinds)
|
|
32
|
+
.filter(([, kind]) => kind === 'authored' || kind === 'contour' || kind === 'segments')
|
|
33
|
+
.map(([slot, kind]) => `"${slot}" (${kind})`);
|
|
34
|
+
return skip(
|
|
35
|
+
'A28_RIBBON_ROWS_SHARE_WEIGHTS',
|
|
36
|
+
unpaired.length
|
|
37
|
+
? `the rig "${rig.archetype}" declares no ribbon mesh on this cut — its mesh slot(s) ${unpaired.join(', ')} have no rows to pair: authored geometry is somebody else's topology, a contour is one silhouette loop, and a segments lattice has cells rather than cross rows`
|
|
38
|
+
: `the rig "${rig.archetype}" declares no ribbon mesh on this cut`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
for (const mesh of facts.meshes) {
|
|
42
|
+
if (!mesh.weights) continue;
|
|
43
|
+
// 🔗 A LINKED mesh's rows are its source's and are paired there. Skipped
|
|
44
|
+
// for A21's reason and with A21's failure mode behind it: this reads
|
|
45
|
+
// `meshKinds` at the LINK's slot, which says nothing about the geometry
|
|
46
|
+
// the link borrowed, so a link sitting in a ribbon's slot from another
|
|
47
|
+
// skin would be measured twice and a link to a NON-ribbon in a ribbon's
|
|
48
|
+
// slot would be measured as a strip it was never built as (issue #691).
|
|
49
|
+
if (mesh.link !== null) continue;
|
|
50
|
+
if (rig.meshKinds[mesh.slot] !== 'ribbon') continue;
|
|
51
|
+
const perVertex = mesh.weights;
|
|
52
|
+
if (perVertex.length % 2 !== 0) {
|
|
53
|
+
fail('A28_RIBBON_ROWS_SHARE_WEIGHTS', `ribbon "${mesh.name}" has ${perVertex.length} vertices; a strip has an even count`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Perimeter order: left row i is index i, right row i is its mirror.
|
|
57
|
+
const rows = perVertex.length / 2;
|
|
58
|
+
for (let i = 0; i < rows; i++) {
|
|
59
|
+
const left = perVertex[i];
|
|
60
|
+
const right = perVertex[perVertex.length - 1 - i];
|
|
61
|
+
const shape = (v: typeof left) => v.map((w) => `${w.bone}:${w.weight.toFixed(6)}`).join(',');
|
|
62
|
+
if (shape(left) !== shape(right)) {
|
|
63
|
+
fail(
|
|
64
|
+
'A28_RIBBON_ROWS_SHARE_WEIGHTS',
|
|
65
|
+
`ribbon "${mesh.name}" row ${i} has [${shape(left)}] on one side and [${shape(right)}] on the other; its width would change with the chain`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A29, the body (issue #1025, cut 4c-4 of #380): inward travel stops where the
|
|
3
|
+
* two masses meet — the measured `stroke.contact_depth`.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the travel
|
|
6
|
+
* is measured over the bone timelines as a fact (`../inward_advance.ts` over
|
|
7
|
+
* `../facts/bone_timelines.ts`) rather than over the raw JSON, and the rig
|
|
8
|
+
* info is the caller's. Why the ceiling is a measured fact about the art stays
|
|
9
|
+
* 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 { deepestInwardAdvance } from '../inward_advance.ts';
|
|
15
|
+
|
|
16
|
+
export function a29StrokeWithinContactDepth({ fail, skip, stats }: Verdicts, facts: BoneTimelineFacts, input: { rig?: RigInfo }): void {
|
|
17
|
+
const rig = input.rig;
|
|
18
|
+
if (!rig) return skip('A29_STROKE_WITHIN_CONTACT_DEPTH', 'no rig info (validating a bare directory)');
|
|
19
|
+
if (!rig.contactDepth) {
|
|
20
|
+
return skip(
|
|
21
|
+
'A29_STROKE_WITHIN_CONTACT_DEPTH',
|
|
22
|
+
'the manifest declares no `stroke.contact_depth`, so this cut has no measured contact ceiling to hold the stroke to',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const deep = deepestInwardAdvance(facts, rig);
|
|
26
|
+
stats.contactDepth = rig.contactDepth;
|
|
27
|
+
stats.deepestAdvance = Math.round(deep.total * 1000) / 1000;
|
|
28
|
+
if (deep.total > rig.contactDepth + 1e-6) {
|
|
29
|
+
fail(
|
|
30
|
+
'A29_STROKE_WITHIN_CONTACT_DEPTH',
|
|
31
|
+
`${deep.describe()} but the masses meet at ${rig.contactDepth}px — the two plates would interpenetrate`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A30, the body (issue #1025, cut 4c-4 of #380): inward travel stops where the
|
|
3
|
+
* drawn cover runs out — the measured `stroke.cap_containment_ceiling` — and
|
|
4
|
+
* no bone under the axis scales while that ceiling is declared.
|
|
5
|
+
*
|
|
6
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the travel
|
|
7
|
+
* and the scale timelines are read off the bone timelines as a fact
|
|
8
|
+
* (`../facts/bone_timelines.ts`, through `../inward_advance.ts`) rather than
|
|
9
|
+
* off the raw JSON, and the rig info is the caller's. Why this is not a
|
|
10
|
+
* restatement of A29, and why a scale key is refused rather than measured,
|
|
11
|
+
* stay above the `check` call in `validate()`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Verdicts } from '../harness.ts';
|
|
14
|
+
import type { BoneTimelineFacts } from '../facts/bone_timelines.ts';
|
|
15
|
+
import type { RigInfo } from '../../types.ts';
|
|
16
|
+
import { deepestInwardAdvance } from '../inward_advance.ts';
|
|
17
|
+
import { isObj } from '../values.ts';
|
|
18
|
+
|
|
19
|
+
export function a30StrokeWithinCapContainment({ fail, skip, stats }: Verdicts, facts: BoneTimelineFacts, input: { rig?: RigInfo }): void {
|
|
20
|
+
const rig = input.rig;
|
|
21
|
+
if (!rig) return skip('A30_STROKE_WITHIN_CAP_CONTAINMENT', 'no rig info (validating a bare directory)');
|
|
22
|
+
if (!rig.capContainmentCeiling) {
|
|
23
|
+
return skip(
|
|
24
|
+
'A30_STROKE_WITHIN_CAP_CONTAINMENT',
|
|
25
|
+
'the manifest declares no `stroke.cap_containment_ceiling`, so this cut has no measured containment ceiling',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const deep = deepestInwardAdvance(facts, rig);
|
|
29
|
+
stats.capCeiling = rig.capContainmentCeiling;
|
|
30
|
+
stats.deepestAdvance = Math.round(deep.total * 1000) / 1000;
|
|
31
|
+
if (deep.total > rig.capContainmentCeiling + 1e-6) {
|
|
32
|
+
fail(
|
|
33
|
+
'A30_STROKE_WITHIN_CAP_CONTAINMENT',
|
|
34
|
+
`${deep.describe()} but the leading contour leaves the occluder's opaque footprint at ` +
|
|
35
|
+
`${rig.capContainmentCeiling}px — the part would be drawn where it should be covered`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const subtree = new Set(rig.axisSubtree);
|
|
39
|
+
for (const { animation: animName, bone: boneName, timelines } of facts.boneTimelines) {
|
|
40
|
+
if (!subtree.has(boneName) || !isObj(timelines)) continue;
|
|
41
|
+
for (const name of Object.keys(timelines)) {
|
|
42
|
+
if (name !== 'scale' && name !== 'scalex' && name !== 'scaley') continue;
|
|
43
|
+
fail(
|
|
44
|
+
'A30_STROKE_WITHIN_CAP_CONTAINMENT',
|
|
45
|
+
`"${animName}" gives "${boneName}" a ${name} timeline while a cap-containment ceiling is declared; ` +
|
|
46
|
+
'the ceiling was measured on the undeformed contour, so a scaled plate is outside its evidence',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A31, restated over the emitted text (issue #1060): the clause
|
|
3
|
+
* `validate()` runs in `src/validate.ts`, word for word, over the skeleton JSON — the
|
|
4
|
+
* text rigc wrote, read by rigc's own reader rather than by spine-core's. The
|
|
5
|
+
* round trip's copy is unchanged and runs in `cli.ts build`; this one runs in
|
|
6
|
+
* the gate of the entry that links none of the runtime
|
|
7
|
+
* (`../emitted/index.ts`), and the selftest's `RC28` holds the two to the
|
|
8
|
+
* same lines on every recipe and on this assertion's mutants.
|
|
9
|
+
*/
|
|
10
|
+
import type { Verdicts } from '../harness.ts';
|
|
11
|
+
import { isObj, type Json } from '../values.ts';
|
|
12
|
+
|
|
13
|
+
export function a31DrawOrderOffsetsResolve({ fail, skip }: Verdicts, raw: Json | null): void {
|
|
14
|
+
if (!raw) return skip('A31_DRAW_ORDER_OFFSETS_RESOLVE', 'the skeleton JSON did not parse (A00 owns that failure)');
|
|
15
|
+
if (!Array.isArray(raw.slots) || !isObj(raw.animations)) {
|
|
16
|
+
return skip('A31_DRAW_ORDER_OFFSETS_RESOLVE', 'the skeleton declares no slots or no animations');
|
|
17
|
+
}
|
|
18
|
+
const slotIndex = new Map<string, number>();
|
|
19
|
+
(raw.slots as unknown[]).forEach((slot, i) => {
|
|
20
|
+
if (isObj(slot) && typeof slot.name === 'string') slotIndex.set(slot.name, i);
|
|
21
|
+
});
|
|
22
|
+
const slotCount = (raw.slots as unknown[]).length;
|
|
23
|
+
let sawATimeline = false;
|
|
24
|
+
for (const [animName, anim] of Object.entries(raw.animations as Json)) {
|
|
25
|
+
if (!isObj(anim) || !Array.isArray(anim.drawOrder)) continue;
|
|
26
|
+
sawATimeline = true;
|
|
27
|
+
(anim.drawOrder as unknown[]).forEach((key, k) => {
|
|
28
|
+
const at = `animation "${animName}" drawOrder key ${k}`;
|
|
29
|
+
if (!isObj(key) || !Array.isArray(key.offsets)) return; // no offsets = setup order
|
|
30
|
+
let previous = -1;
|
|
31
|
+
for (const entry of key.offsets as unknown[]) {
|
|
32
|
+
if (!isObj(entry) || typeof entry.slot !== 'string' || typeof entry.offset !== 'number') {
|
|
33
|
+
fail('A31_DRAW_ORDER_OFFSETS_RESOLVE', `${at}: an offset is not { slot: string, offset: number }`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const index = slotIndex.get(entry.slot);
|
|
37
|
+
if (index === undefined) {
|
|
38
|
+
fail('A31_DRAW_ORDER_OFFSETS_RESOLVE', `${at}: slot "${entry.slot}" is not in the skeleton`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (index <= previous) {
|
|
42
|
+
const detail =
|
|
43
|
+
`${at}: slot "${entry.slot}" is at index ${index}, after an entry at index ${previous} — ` +
|
|
44
|
+
'offsets must be in ascending slot order or the loader never finishes reading them';
|
|
45
|
+
fail('A31_DRAW_ORDER_OFFSETS_RESOLVE', detail);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
previous = index;
|
|
49
|
+
const landing = index + entry.offset;
|
|
50
|
+
if (!Number.isInteger(entry.offset) || landing < 0 || landing >= slotCount) {
|
|
51
|
+
fail(
|
|
52
|
+
'A31_DRAW_ORDER_OFFSETS_RESOLVE',
|
|
53
|
+
`${at}: slot "${entry.slot}" is at index ${index} and offset ${entry.offset} puts it at ${landing}, ` +
|
|
54
|
+
`outside the ${slotCount} slots`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!sawATimeline) return skip('A31_DRAW_ORDER_OFFSETS_RESOLVE', 'no animation carries a drawOrder timeline');
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A32, the body (issue #1025, cut 4c-4 of #380): an event key that sets
|
|
3
|
+
* `volume` or `balance` fires an event that declares an audio path — and the
|
|
4
|
+
* clauses that stay with the round trip, printed where they always printed.
|
|
5
|
+
*
|
|
6
|
+
* Split per clause (`../facts/event_keys.ts`). The rig's clause moved here: a
|
|
7
|
+
* readable document can state a key's `volume` on an event with no `audio`,
|
|
8
|
+
* and the file the emitter writes from it is the file this clause refuses. The
|
|
9
|
+
* other four — no string `name`, an undeclared event, a time that is not a
|
|
10
|
+
* finite number, a time before the key before it — are states the model
|
|
11
|
+
* document's reader refuses by name, so no readable document reaches them;
|
|
12
|
+
* `validate()` runs them over the skeleton JSON and hands their findings in
|
|
13
|
+
* (`kept`), and this body prints them at their place, in their order, before
|
|
14
|
+
* the moved clause and instead of it where one ended the key's reading. The
|
|
15
|
+
* three failure modes and why the rule reads the raw JSON stay above the
|
|
16
|
+
* `check` call in `validate()`.
|
|
17
|
+
*/
|
|
18
|
+
import type { Verdicts } from '../harness.ts';
|
|
19
|
+
import type { EventKeyFacts } from '../facts/event_keys.ts';
|
|
20
|
+
|
|
21
|
+
/** Where a key is, as every A32 line names it — the kept clauses in `src/validate.ts` name it this way too. */
|
|
22
|
+
export function eventKeyAt(animation: string, index: number): string {
|
|
23
|
+
return `animation "${animation}" event key ${index}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function a32EventKeysResolve({ fail, skip }: Verdicts, facts: EventKeyFacts): void {
|
|
27
|
+
if (!facts.parsed) return skip('A32_EVENT_KEYS_RESOLVE', 'the skeleton JSON did not parse (A00 owns that failure)');
|
|
28
|
+
if (!facts.animations) return skip('A32_EVENT_KEYS_RESOLVE', 'the skeleton declares no animations');
|
|
29
|
+
for (const key of facts.keys) {
|
|
30
|
+
for (const finding of key.kept) fail('A32_EVENT_KEYS_RESOLVE', finding);
|
|
31
|
+
if (key.stopped) continue;
|
|
32
|
+
const at = eventKeyAt(key.animation, key.index);
|
|
33
|
+
for (const field of ['volume', 'balance'] as const) {
|
|
34
|
+
if (key.sets[field] && !key.audio) {
|
|
35
|
+
fail(
|
|
36
|
+
'A32_EVENT_KEYS_RESOLVE',
|
|
37
|
+
`${at}: "${key.name}" sets ${field}, but the event declares no audio path — the parser reads ` +
|
|
38
|
+
`${field} only for an event that has one, so it is dropped in silence`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (facts.timelines === 0) return skip('A32_EVENT_KEYS_RESOLVE', 'no animation carries an event timeline');
|
|
44
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A33, the body (issue #1025, step 4c of #380): bounding boxes, clipping
|
|
3
|
+
* polygons and paths hold a real polygon, a path's curve lengths strictly
|
|
4
|
+
* increase, and a clip ends at a slot of the skeleton.
|
|
5
|
+
*
|
|
6
|
+
* These types are the same shape — a polygon and nothing else — and they
|
|
7
|
+
* fail the same ways, all silent:
|
|
8
|
+
*
|
|
9
|
+
* 1. **A missing or wrong `vertexCount`.** The parser reads
|
|
10
|
+
* `map.vertexCount << 1` and hands it to `readVertices` as the length to
|
|
11
|
+
* expect (`:552`, `:632`). `undefined << 1` is 0, so an omission makes
|
|
12
|
+
* the coordinate array read as a WEIGHTED run: it decodes numbers as
|
|
13
|
+
* bone counts and weights, and the attachment ends up with no vertices
|
|
14
|
+
* at all. Nothing throws, and neither type draws a pixel, so nothing
|
|
15
|
+
* downstream notices either.
|
|
16
|
+
* 2. **A weighted run that does not decode to that many vertices.** Same
|
|
17
|
+
* trap as a mesh's (A04), minus the uvs that would have caught it.
|
|
18
|
+
* 3. **A clipping `end` naming a slot that is not there.**
|
|
19
|
+
* `skeletonData.findSlot` returns null on a miss and `:626-627` assigns
|
|
20
|
+
* the null, so the clip does not end where it was told to — it runs to
|
|
21
|
+
* the bottom of the draw order and takes every slot below it with it.
|
|
22
|
+
* Checked on what the file states, because a null `endSlot` and an `end`
|
|
23
|
+
* that was never written are the same loaded object.
|
|
24
|
+
*
|
|
25
|
+
* Moved out of `src/validate.ts` clause by clause. The path clauses, the
|
|
26
|
+
* clip's end and a polygon's vertex count are about the rig — a document's
|
|
27
|
+
* polygon states its count and its vertices, a path its `closed` and
|
|
28
|
+
* `lengths`, a clip its `end` — and moved. The decode of a weighted run and
|
|
29
|
+
* the length of an unweighted array (the second failure above) are about the
|
|
30
|
+
* encoding: the document states the weighted form outright and names bones.
|
|
31
|
+
* They stay with the round trip, which hands their findings in as each
|
|
32
|
+
* polygon's `encoding`, printed where the body always printed them.
|
|
33
|
+
*/
|
|
34
|
+
import type { Verdicts } from '../harness.ts';
|
|
35
|
+
import type { PolygonFacts } from '../facts/vertex_polygons.ts';
|
|
36
|
+
|
|
37
|
+
export function a33VertexAttachmentGeometry({ fail, skip }: Verdicts, { polygons, clipEnds, slots }: PolygonFacts): void {
|
|
38
|
+
const paths = polygons.filter((p) => p.path !== null);
|
|
39
|
+
for (const path of paths) {
|
|
40
|
+
const what = path.what;
|
|
41
|
+
const vertexCount = path.worldVerticesLength / 2;
|
|
42
|
+
if (vertexCount % 3 !== 0) {
|
|
43
|
+
fail(
|
|
44
|
+
'A33_VERTEX_ATTACHMENT_GEOMETRY',
|
|
45
|
+
`${what} has ${vertexCount} vertices, which is not a multiple of 3 — a path is knots and Bezier ` +
|
|
46
|
+
'handles read in groups of three, and `Utils.newArray(vertexCount / 3, 0)` accepts the fractional ' +
|
|
47
|
+
'size without a word, so the curves straddle the knots',
|
|
48
|
+
);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Curves: 3K + 1 chain points. An open path drops the first and last
|
|
52
|
+
// vertex (the end knots' outer handles), a closed one repeats the first.
|
|
53
|
+
const curves = path.path?.closed ? vertexCount / 3 : vertexCount / 3 - 1;
|
|
54
|
+
if (curves < 1) {
|
|
55
|
+
fail('A33_VERTEX_ATTACHMENT_GEOMETRY', `${what} has ${vertexCount} vertices, which is not one whole curve`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// `lengths` is what a `constantSpeed: false` traversal measures with:
|
|
59
|
+
// `lengths[curve]` bounds each curve and the last entry IS the path
|
|
60
|
+
// length. The parser sizes the array from `vertexCount / 3` and copies
|
|
61
|
+
// whatever the file gave, so a short array leaves trailing zeros — and a
|
|
62
|
+
// zero-length curve makes the parser divide the position by it.
|
|
63
|
+
const lengths = path.path?.lengths ?? [];
|
|
64
|
+
let previous = 0;
|
|
65
|
+
for (let c = 0; c < curves; c++) {
|
|
66
|
+
const value = lengths[c];
|
|
67
|
+
if (!Number.isFinite(value) || value <= previous) {
|
|
68
|
+
fail(
|
|
69
|
+
'A33_VERTEX_ATTACHMENT_GEOMETRY',
|
|
70
|
+
`${what} lengths[${c}] is ${String(value)}, and the entry before it was ${previous}. The array is the ` +
|
|
71
|
+
'CUMULATIVE arc length at the end of each of the ' +
|
|
72
|
+
`${curves} curve(s), so it strictly increases; a value that does not means either a zero-length ` +
|
|
73
|
+
'curve (the position is divided by it) or an array shorter than the geometry (the tail reads as 0)',
|
|
74
|
+
);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
previous = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const slotNames = new Set(slots);
|
|
81
|
+
let endsChecked = 0;
|
|
82
|
+
for (const { placeholder, slot: slotName, end } of clipEnds) {
|
|
83
|
+
endsChecked++;
|
|
84
|
+
if (typeof end !== 'string' || !slotNames.has(end)) {
|
|
85
|
+
fail(
|
|
86
|
+
'A33_VERTEX_ATTACHMENT_GEOMETRY',
|
|
87
|
+
`clipping attachment "${placeholder}" on slot "${slotName}" ends at ${JSON.stringify(end)}, ` +
|
|
88
|
+
'which is not a slot of this skeleton — the clip would run to the bottom of the draw order',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (polygons.length === 0 && endsChecked === 0) {
|
|
93
|
+
return skip(
|
|
94
|
+
'A33_VERTEX_ATTACHMENT_GEOMETRY',
|
|
95
|
+
'the skeleton carries no bounding box, clipping attachment or path',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
for (const { what, worldVerticesLength: length, encoding } of polygons) {
|
|
99
|
+
if (!Number.isInteger(length) || length < 6 || length % 2 !== 0) {
|
|
100
|
+
fail(
|
|
101
|
+
'A33_VERTEX_ATTACHMENT_GEOMETRY',
|
|
102
|
+
`${what} loaded worldVerticesLength ${length}; a polygon is an even count of at least 6 (3 vertices). ` +
|
|
103
|
+
'A missing "vertexCount" reads as 0 and takes the polygon with it',
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// The vertex run's own coherence is the encoding's, kept with the round trip (the header).
|
|
108
|
+
for (const detail of encoding) fail('A33_VERTEX_ATTACHMENT_GEOMETRY', detail);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A34, the body (issue #1025, cut 4c-5 of step 4c of #380): a constraint
|
|
3
|
+
* timeline aims at a constraint of that type.
|
|
4
|
+
*
|
|
5
|
+
* The argument — the two shapes, the lookup by name AND type, the empty key
|
|
6
|
+
* array the parser skips in silence, the physics group that names no
|
|
7
|
+
* constraint — stands above the `check` call in `validate()`.
|
|
8
|
+
*
|
|
9
|
+
* Moved out of `src/validate.ts` whole, every clause with it (cut 4c-4
|
|
10
|
+
* deferred it whole because its one rig clause shares this walk with the
|
|
11
|
+
* physics branch): which constraint an entry names, what it holds and whom a
|
|
12
|
+
* physics timeline naming none reaches are the rig's, and a document's records
|
|
13
|
+
* state each — except what no readable document can carry, which only the
|
|
14
|
+
* Spine text holds (a name the skeleton lacks, a name of another type, an
|
|
15
|
+
* empty key array, a bare key array where named timelines go): the model
|
|
16
|
+
* side's walk never meets those, and the reader refuses the document that
|
|
17
|
+
* would state them.
|
|
18
|
+
*/
|
|
19
|
+
import type { Verdicts } from '../harness.ts';
|
|
20
|
+
import type { ConstraintTargetFacts, TargetKeyArray } from '../facts/constraint_targets.ts';
|
|
21
|
+
import { CHANNELS_BY_KIND } from '../../timelines.ts';
|
|
22
|
+
|
|
23
|
+
export function a34ConstraintTimelineTargets({ fail, skip }: Verdicts, facts: ConstraintTargetFacts): void {
|
|
24
|
+
if (facts.groupsByAnimation === null) return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'the skeleton declares no animations');
|
|
25
|
+
// name -> the KINDS declared under it, because that is the namespace the
|
|
26
|
+
// lookup this assertion is about resolves in: `findConstraint(name, type)`
|
|
27
|
+
// tests the type first, so a skeleton may carry `leg` as an ik constraint
|
|
28
|
+
// AND as a transform one and each group finds its own (issue #692). A map
|
|
29
|
+
// keyed by the name alone let the second of a pair overwrite the first, and
|
|
30
|
+
// this assertion then reported a correct file as a type mismatch.
|
|
31
|
+
const kindsOf = new Map<string, string[]>();
|
|
32
|
+
for (const entry of facts.constraints) {
|
|
33
|
+
if (entry.name !== null) kindsOf.set(entry.name, [...(kindsOf.get(entry.name) ?? []), entry.type]);
|
|
34
|
+
}
|
|
35
|
+
/** The file's physics constraints, which the unnamed physics group is read against. */
|
|
36
|
+
const rawPhysics = facts.constraints.filter((entry) => entry.type === 'physics');
|
|
37
|
+
let sawATimeline = false;
|
|
38
|
+
/** One target of one group: the name resolves, the type matches, keys exist. */
|
|
39
|
+
const checkTarget = (at: string, group: string, name: string, keyArrays: readonly TargetKeyArray[]): void => {
|
|
40
|
+
sawATimeline = true;
|
|
41
|
+
const declared = kindsOf.get(name) ?? [];
|
|
42
|
+
if (declared.length === 0) {
|
|
43
|
+
const known = [...kindsOf.entries()].filter(([, kinds]) => kinds.includes(group)).map(([n]) => n);
|
|
44
|
+
fail(
|
|
45
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
46
|
+
`${at}: the skeleton's constraints array has no "${name}"` +
|
|
47
|
+
(known.length ? ` (${group} constraints: ${known.join(', ')})` : `, and no ${group} constraint at all`),
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!declared.includes(group)) {
|
|
52
|
+
fail(
|
|
53
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
54
|
+
`${at}: "${name}" is declared as a "${declared.join('"/"')}" constraint, so the ${group} lookup misses it and the loader throws`,
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
checkKeys(at, keyArrays);
|
|
59
|
+
};
|
|
60
|
+
/** The key arrays of one group entry that resolved: each one is walked, so each has to hold a key. */
|
|
61
|
+
const checkKeys = (at: string, keyArrays: readonly TargetKeyArray[]): void => {
|
|
62
|
+
if (keyArrays.length === 0) {
|
|
63
|
+
fail(
|
|
64
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
65
|
+
`${at}: the constraint is named and carries no timeline at all; the group is walked and nothing happens`,
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const { timeline, keys } of keyArrays) {
|
|
70
|
+
if ('count' in keys && keys.count > 0) continue;
|
|
71
|
+
fail(
|
|
72
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
73
|
+
`${at}${timeline ? ` timeline "${timeline}"` : ''}: the key array is ` +
|
|
74
|
+
`${'count' in keys ? 'empty' : keys.spelled}; the parser reads key 0, finds nothing and ` +
|
|
75
|
+
'skips the whole timeline without a word',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
for (const { name: animName, groups } of facts.groupsByAnimation) {
|
|
80
|
+
for (const { group, entries } of groups) {
|
|
81
|
+
for (const { name, bare, keyArrays } of entries) {
|
|
82
|
+
// group.<constraint> = keys[]
|
|
83
|
+
if (group === 'ik' || group === 'transform') {
|
|
84
|
+
checkTarget(`animation "${animName}" ${group} timeline "${name}"`, group, name, keyArrays);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// group.<constraint>.<timeline> = keys[]
|
|
88
|
+
const at = `animation "${animName}" ${group} constraint "${name}"`;
|
|
89
|
+
if (bare !== null) {
|
|
90
|
+
sawATimeline = true;
|
|
91
|
+
fail(
|
|
92
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
93
|
+
`${at}: this group maps a constraint to NAMED timelines ` +
|
|
94
|
+
`(${Object.keys(CHANNELS_BY_KIND[group]).join('/')}), and this one holds ${bare} — ` +
|
|
95
|
+
'a bare key array here is the ik/transform shape and is walked as an object',
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// The empty name is not a miss: it is the physics group's global form,
|
|
100
|
+
// which `SkeletonJson` loads as `constraintIndex -1` rather than looking
|
|
101
|
+
// anything up, and which writes every physics constraint that declares
|
|
102
|
+
// the timeline's property global (issue #726). Refusing it as "no
|
|
103
|
+
// constraint called ''" was a sentence that is false about the file.
|
|
104
|
+
// What CAN be wrong with it is that it reaches nobody: the parser
|
|
105
|
+
// accepts it, the runtime walks every constraint, and none of them
|
|
106
|
+
// takes the key.
|
|
107
|
+
if (group === 'physics' && name === '') {
|
|
108
|
+
sawATimeline = true;
|
|
109
|
+
for (const { timeline: timelineName } of keyArrays) {
|
|
110
|
+
const reach = facts.reach(timelineName);
|
|
111
|
+
// A name the parser skips is skipped here too: no timeline exists
|
|
112
|
+
// to reach anybody, and whether the NAME is right is not a target
|
|
113
|
+
// question.
|
|
114
|
+
if (reach === null || reach.reached.length > 0) continue;
|
|
115
|
+
fail(
|
|
116
|
+
'A34_CONSTRAINT_TIMELINE_TARGETS',
|
|
117
|
+
`${at} timeline "${timelineName}": a physics group that names no constraint writes every physics ` +
|
|
118
|
+
`constraint ${reach.resets ? 'the skeleton has' : `declaring "${timelineName}Global": true`}, and ` +
|
|
119
|
+
(rawPhysics.length === 0 ? 'the skeleton has no physics constraint' : `none of ${rawPhysics.map((one) => `"${one.spelled}"`).join(', ')} does`) +
|
|
120
|
+
' — the parser loads it, the runtime walks every constraint, and no constraint takes the key',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
checkKeys(at, keyArrays);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
checkTarget(at, group, name, keyArrays);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!sawATimeline) {
|
|
131
|
+
return skip('A34_CONSTRAINT_TIMELINE_TARGETS', 'no animation carries a constraint timeline');
|
|
132
|
+
}
|
|
133
|
+
}
|