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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A06, the body (issue #1025, step 4c of #380): every page the atlas names is
|
|
3
|
+
* the size its PNG is, every region lies inside its page, and — under
|
|
4
|
+
* `spine-html` — no page claims premultiplied alpha and no region is turned.
|
|
5
|
+
* The clause that refused two regions on one page overlapping is
|
|
6
|
+
* `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` since issue #1099 (`./a49.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the pages and
|
|
9
|
+
* regions are a fact (`../facts/atlas_regions.ts`), the directory is the
|
|
10
|
+
* caller's, and the profile's policy switch is passed in. The `check` call —
|
|
11
|
+
* and the argument for the SKIP guards it shares with A17 and A19 (#568, #608)
|
|
12
|
+
* — stays in `validate()`.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import type { Verdicts } from '../harness.ts';
|
|
17
|
+
import type { AtlasRegionFacts } from '../facts/atlas_regions.ts';
|
|
18
|
+
import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_PAGE } from '../reasons.ts';
|
|
19
|
+
import { pageFootprint, pageGridSentence } from '../../atlas.ts';
|
|
20
|
+
import { readPngHeader } from '../../png.ts';
|
|
21
|
+
import { imageDataProblem } from '../../../tools/plate.ts';
|
|
22
|
+
|
|
23
|
+
export function a06AtlasPageSizeMatchesPng({ fail: failed, skip }: Verdicts, { atlas }: AtlasRegionFacts, input: { atlasDir: string }, policy: boolean): void {
|
|
24
|
+
if (!atlas) return skip('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', SKIP_NO_ATLAS);
|
|
25
|
+
if (atlas.pages.length === 0) return skip('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', SKIP_NO_ATLAS_PAGE);
|
|
26
|
+
// Every sentence this body prints goes through here, so the end of the body
|
|
27
|
+
// can tell "nothing failed" from "something failed" (issue #1055, below).
|
|
28
|
+
let failures = 0;
|
|
29
|
+
const fail = (assertion: string, detail: string): void => {
|
|
30
|
+
failures++;
|
|
31
|
+
failed(assertion, detail);
|
|
32
|
+
};
|
|
33
|
+
// 🔒 **A page whose file is not on disk is a page whose size was not
|
|
34
|
+
// measured, and the body says so rather than passing** (issue #1055). Until
|
|
35
|
+
// this the loop below walked past such a page and the assertion came out
|
|
36
|
+
// PASS — on a build with every page file deleted, a PASS over a size clause
|
|
37
|
+
// that had opened nothing, measured on 33 of the 38 recipe-and-profile
|
|
38
|
+
// pairs of the tree's 19 recipes (the other five failed clauses that read
|
|
39
|
+
// only the atlas's numbers: overlap and rotation, under `spine-html`). What the body does instead is decided by what `A17` prints on
|
|
40
|
+
// the same input, so that one missing file is named once, by the rule whose
|
|
41
|
+
// subject it is: A17 FAILs naming every such file and its path, which keeps
|
|
42
|
+
// the report red; this body adds no FAIL of its own about the file (a
|
|
43
|
+
// second naming), and it does not PASS (a certificate over a size nobody
|
|
44
|
+
// read). It SKIPs with a reason that names the pages it did not read and
|
|
45
|
+
// points at A17 — `unreadSizes` below, at both of the body's exits.
|
|
46
|
+
//
|
|
47
|
+
// ⚠️ Only when nothing else here failed. `skip()` is per assertion, and a
|
|
48
|
+
// skip beside failures puts A06 in two of the report's buckets (the #705
|
|
49
|
+
// argument in `a19.ts`); and a FAIL certifies nothing, so the unread page
|
|
50
|
+
// is A17's alone there. ⚠️ Not the tree's other multi-clause rule either
|
|
51
|
+
// (`../reasons.ts`, #580: a multi-clause assertion SKIPs only when EVERY
|
|
52
|
+
// clause had nothing to measure). That rule is about a clause whose subject
|
|
53
|
+
// list is EMPTY — zero is a count, and a count was measured. A page the
|
|
54
|
+
// atlas declares is a member of the size clause's subject that is present
|
|
55
|
+
// and was not read, and the PASS this replaces claimed it was.
|
|
56
|
+
const unread: string[] = [];
|
|
57
|
+
const unreadSizes = (): void => {
|
|
58
|
+
if (failures > 0 || unread.length === 0) return;
|
|
59
|
+
const total = atlas.pages.length;
|
|
60
|
+
skip(
|
|
61
|
+
'A06_ATLAS_PAGE_SIZE_MATCHES_PNG',
|
|
62
|
+
unread.length === total
|
|
63
|
+
? `no page's size was measured: none of the ${total} page file(s) the atlas declares is on disk, which ` +
|
|
64
|
+
"A17_ATLAS_PAGE_FILES_EXIST names file by file. The clauses that read only the atlas's own numbers found " +
|
|
65
|
+
"nothing to fail, and that is not a pass while every page's size is unread"
|
|
66
|
+
: `the size of ${unread.length} of the ${total} page(s) the atlas declares was not measured — ` +
|
|
67
|
+
`${unread.map((name) => JSON.stringify(name)).join(', ')} — because the file is not on disk, which ` +
|
|
68
|
+
`A17_ATLAS_PAGE_FILES_EXIST names. The ${total - unread.length} page(s) that were read, and the clauses ` +
|
|
69
|
+
"that read only the atlas's own numbers, found nothing to fail, and that is not a pass while a page's size is unread",
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
for (const page of atlas.pages) {
|
|
73
|
+
const abs = resolve(input.atlasDir, page.name);
|
|
74
|
+
if (!existsSync(abs)) {
|
|
75
|
+
unread.push(page.name); // A17 names the file; this body says only that it did not read it
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// 🔒 **A page that is not a PNG is a named failure, not a throw** (issue
|
|
79
|
+
// #732). The reader's throw used to reach this rule's catch and print as
|
|
80
|
+
// `threw: not a PNG (bad signature)`: the verdict right, the sentence a
|
|
81
|
+
// stack message that named neither what the file was nor what rigc reads.
|
|
82
|
+
// `pngProblem` ([`src/png.ts`](png.ts)) is the one sentence every reader
|
|
83
|
+
// of a page states; this rule prefixes the size the atlas declares, the
|
|
84
|
+
// value the file would have had to carry.
|
|
85
|
+
//
|
|
86
|
+
// 🔒 **And this is the one line that names it** (issue #1064). A file's
|
|
87
|
+
// readability is this rule's subject — it runs under both profiles and
|
|
88
|
+
// asks `pngProblem` first, so on every input where `A19` would find the
|
|
89
|
+
// same page unreadable, this FAIL is already printed. `A19` therefore
|
|
90
|
+
// SKIPs naming the page and pointing here when it failed nothing else,
|
|
91
|
+
// and says nothing about the page when it did: one unreadable file, one
|
|
92
|
+
// line, the way #1055 made one missing file A17's alone.
|
|
93
|
+
//
|
|
94
|
+
// ⚠️ No size is read off a file rigc cannot decode, deliberately. A WebP
|
|
95
|
+
// header carries its dimensions in a fixed field, and reading them would
|
|
96
|
+
// print a number no oracle in this tree has checked (rigc links no WebP
|
|
97
|
+
// reader to compare a parse against), about a page that is refused here
|
|
98
|
+
// either way — and measured, the variant that PASSED a WebP page whose
|
|
99
|
+
// size agreed built green under the default profile and wrote a
|
|
100
|
+
// directory that `render` then refused.
|
|
101
|
+
const header = readPngHeader(abs);
|
|
102
|
+
if (header.problem !== null) {
|
|
103
|
+
fail(
|
|
104
|
+
'A06_ATLAS_PAGE_SIZE_MATCHES_PNG',
|
|
105
|
+
`page "${page.name}" declares ${page.width}x${page.height} and its file cannot be read as PNG, so the ` +
|
|
106
|
+
`size was not measured: ${header.problem}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
// 🔒 **And a page whose header reads and whose image data does not decode
|
|
110
|
+
// is this rule's too** (issue #1074), for the reason #1064 made the
|
|
111
|
+
// unreadable header its: this is the one reader of every page that runs
|
|
112
|
+
// under both profiles. Measured before this, on the tree's 19 recipes with
|
|
113
|
+
// the first page's compressed stream overwritten past its first half,
|
|
114
|
+
// this rule PASSED on 19 of 19 (it read the header only) and `A19` alone
|
|
115
|
+
// named the file, as `threw: cannot decode PNG …`, on the 12 whose first
|
|
116
|
+
// page carries a part. On the 7 gallery rigs, whose first page is the base
|
|
117
|
+
// plate `A19` does not scan, nothing did: both rules PASSED under both
|
|
118
|
+
// profiles, and the default build wrote a directory `render` then died on.
|
|
119
|
+
//
|
|
120
|
+
// ⚠️ The stream is inflated, not decoded: `imageDataProblem` is the
|
|
121
|
+
// decoder's own refusing half and nothing after it — 68.9 ms over the 19
|
|
122
|
+
// builds' pages against `validate()`'s 286.2 ms under `spine`, where a
|
|
123
|
+
// whole decode would be 459.6 ms. A page whose data does not decode has no
|
|
124
|
+
// size worth comparing, so the grid clause is not asked of it: one file,
|
|
125
|
+
// one line.
|
|
126
|
+
const undecodable = header.info === null ? null : imageDataProblem(readFileSync(abs), abs);
|
|
127
|
+
if (undecodable !== null) {
|
|
128
|
+
fail(
|
|
129
|
+
'A06_ATLAS_PAGE_SIZE_MATCHES_PNG',
|
|
130
|
+
`page "${page.name}" declares ${page.width}x${page.height} and its file cannot be read as PNG, so ` +
|
|
131
|
+
`nothing on it was measured: ${undecodable}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const info = undecodable === null ? header.info : null;
|
|
135
|
+
const onPage = atlas.regions.filter((region) => region.page.name === page.name);
|
|
136
|
+
const gridSaid = info === null ? null : pageGridSentence(page, info, onPage);
|
|
137
|
+
if (gridSaid !== null) {
|
|
138
|
+
// 🔒 **Still validity, and now for a measured reason rather than an
|
|
139
|
+
// inherited one** (issue #715). The card that opened this expected the
|
|
140
|
+
// clause to move behind the profile switch once the runtime's mapping
|
|
141
|
+
// was measured — it is declared-size-relative, so a rescaled page
|
|
142
|
+
// draws. What settles it the other way is that the sentence below can
|
|
143
|
+
// name a repair the FORMAT already provides (`scale:`), and that two of
|
|
144
|
+
// the three readers a wrong grid breaks are rigc's own and run under
|
|
145
|
+
// both profiles. The sentence, and the one derivation of the ratio under
|
|
146
|
+
// it, is `pageGridSentence` in [`src/atlas.ts`](atlas.ts): the compiler's
|
|
147
|
+
// region lift states the same one when it is asked for texels on such a
|
|
148
|
+
// page (issue #750), so the two cannot come to describe one page two ways.
|
|
149
|
+
fail('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', gridSaid);
|
|
150
|
+
}
|
|
151
|
+
// 📐 PROFILE, from here down. `pma: false` and no rotation are rigc's atlas
|
|
152
|
+
// CONVENTION, not the atlas format's rules — a packed page with
|
|
153
|
+
// `rotate: 90` is what the Spine packer produces and every official example
|
|
154
|
+
// ships one. Under `spine` an atlas is judged only on whether its declared
|
|
155
|
+
// size matches the file it names.
|
|
156
|
+
if (policy && page.pma) {
|
|
157
|
+
fail('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', `page "${page.name}" claims premultiplied alpha; parts are straight alpha`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 🔒 **A region's rectangle lies inside the page it names, and that is
|
|
161
|
+
// VALIDITY** (issue #694). The clause is not new — it has read the same four
|
|
162
|
+
// numbers since issue #266 — but it stood inside the policy half below, and
|
|
163
|
+
// behind a guard that skipped any page carrying one region that covered it.
|
|
164
|
+
// Both fences were wrong for it. A rectangle that leaves its page is not a
|
|
165
|
+
// convention somebody else may hold differently: `u2 > 1` samples whatever
|
|
166
|
+
// the wrap mode returns, which is never the drawing the pack was made of, so
|
|
167
|
+
// the part draws garbage or nothing at all. Measured on a pack shaped like
|
|
168
|
+
// the two production atlases that found this — a page declaring 2048x256
|
|
169
|
+
// with regions at `2274,0 980x200` and `0,252 100x258` — every atlas
|
|
170
|
+
// assertion passed under the default profile and the first of the two parts
|
|
171
|
+
// drew 0 of the 10,517 pixels it draws when the same rig is built loose.
|
|
172
|
+
//
|
|
173
|
+
// ⭐ The tool's other half already said so, which is what settles where the
|
|
174
|
+
// clause belongs. `resolveFromAtlas` ([`src/compile.ts`](compile.ts)) refuses
|
|
175
|
+
// exactly this rectangle as a `CompileError` under every profile when a pack
|
|
176
|
+
// arrives through `--atlas-in`, so while it was policy here the compiler and
|
|
177
|
+
// the gate disagreed about the same four numbers — and the gate is the only
|
|
178
|
+
// half that a skeleton and a pack somebody else made ever reach.
|
|
179
|
+
//
|
|
180
|
+
// ⚠️ It is stated over `atlas.regions` rather than per page group, because
|
|
181
|
+
// the question is about one region and its own page and needs no neighbour:
|
|
182
|
+
// a page carrying a single full-page region is measured too, and answers
|
|
183
|
+
// trivially. The rectangle is `pageFootprint`'s and nobody else's here
|
|
184
|
+
// (issue #579): what `TextureAtlas` transposes at 90 and not at 270 is
|
|
185
|
+
// `u2`/`v2`, a UV pair `MeshAttachment.computeUVs` never reads for an atlas
|
|
186
|
+
// region, and the page rectangle is a different quantity — transposed at
|
|
187
|
+
// BOTH quarter turns. A region that fits only *because* it is turned is
|
|
188
|
+
// inside its page, and this clause says so.
|
|
189
|
+
for (const region of atlas.regions) {
|
|
190
|
+
const foot = pageFootprint(region);
|
|
191
|
+
if (
|
|
192
|
+
region.x < 0 ||
|
|
193
|
+
region.y < 0 ||
|
|
194
|
+
region.x + foot.width > region.page.width ||
|
|
195
|
+
region.y + foot.height > region.page.height
|
|
196
|
+
) {
|
|
197
|
+
fail(
|
|
198
|
+
'A06_ATLAS_PAGE_SIZE_MATCHES_PNG',
|
|
199
|
+
`region "${region.name}" occupies ${region.x},${region.y} ${foot.width}x${foot.height} of page ` +
|
|
200
|
+
`"${region.page.name}", which is ${region.page.width}x${region.page.height} — a region that runs off ` +
|
|
201
|
+
'its page samples texels that are not there',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!policy) return unreadSizes();
|
|
206
|
+
// ⭐ **One part per page OR a tiling page** (issue #266, follow-up 2), and
|
|
207
|
+
// since issue #1099 the tiling half is not this assertion's. It held that no
|
|
208
|
+
// two regions on one page overlap — the check that makes shared-page
|
|
209
|
+
// sampling well defined — and it was stated over rectangles, which is the
|
|
210
|
+
// right statement for a region attachment and the wrong one for a mesh: a
|
|
211
|
+
// mesh draws its hull, and `--pack-shape polygon` puts a neighbour inside a
|
|
212
|
+
// mesh's rectangle wherever the hull is not. The clause is now
|
|
213
|
+
// `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` ([`./a49.ts`](a49.ts)), which
|
|
214
|
+
// refuses a pair only where the rectangles overlap AND what the two regions
|
|
215
|
+
// draw does — so every pair this clause refused where neither region is a
|
|
216
|
+
// readable mesh hull (an alias, a sequence frame, a rotated region, two
|
|
217
|
+
// rectangles planted over the same texels) is refused there, by the same two
|
|
218
|
+
// rectangles, under the same profile.
|
|
219
|
+
//
|
|
220
|
+
// ⚠️ Its sibling — every region inside the page it names — moved above and
|
|
221
|
+
// out of this profile in issue #694, and the two are not symmetrical. That
|
|
222
|
+
// one is broken for every consumer; the overlap is a statement about what a
|
|
223
|
+
// pack MEANS, and the corpus is the evidence rather than the taste:
|
|
224
|
+
// measured over the ten atlases in `examples/`, 0 of 132 regions are off
|
|
225
|
+
// their page and 49 pairs on four of those pages overlap, editor-exported
|
|
226
|
+
// and correct. That is why A49 is a renderer rule.
|
|
227
|
+
//
|
|
228
|
+
// ⚠️ Rotation stays refused either way, and that is not the same clause: it
|
|
229
|
+
// is about rigc's own packer never turning a region, which is a statement
|
|
230
|
+
// about what rigc WRITES. It stopped being a statement about what rigc can
|
|
231
|
+
// read in issue #570 — `extractRegion` now lifts a rotated region back off
|
|
232
|
+
// its page, measured against `MeshAttachment.computeUVs` (`PKR02`) — and the two
|
|
233
|
+
// must not be re-merged: an artifact under the renderer's own rulebook that
|
|
234
|
+
// rigc did not pack is still a foreign artifact, whatever rigc can measure.
|
|
235
|
+
for (const region of atlas.regions) {
|
|
236
|
+
if (region.degrees !== 0) {
|
|
237
|
+
fail(
|
|
238
|
+
'A06_ATLAS_PAGE_SIZE_MATCHES_PNG',
|
|
239
|
+
`region "${region.name}" is rotated; rigc's own packer never turns a region (see PACK_NO_ROTATE in ` +
|
|
240
|
+
'src/atlas.ts), so this atlas came from somewhere else',
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
unreadSizes();
|
|
245
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A07, restated over the emitted text (issue #1060): the clause
|
|
3
|
+
* `validate()` runs in `src/validate.ts`, word for word, over the atlas text — 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 { SKIP_NO_ATLAS_PAGE } from '../reasons.ts';
|
|
12
|
+
|
|
13
|
+
export function a07AtlasTextShape({ fail, skip }: Verdicts, atlasText: string): void {
|
|
14
|
+
const atlasLines = atlasText.replace(/\n$/, '').split('\n');
|
|
15
|
+
const atlasPageLines = atlasLines.filter((line) => line.trim().length > 0).length;
|
|
16
|
+
if (atlasPageLines === 0) {
|
|
17
|
+
// One of the exported reason constants, not a sentence of its own: the
|
|
18
|
+
// static-rig suite's arithmetic (S50, #580) counts a rule as skipped
|
|
19
|
+
// for want of a SUBJECT only when its reason is one of those constants.
|
|
20
|
+
return skip('A07_ATLAS_TEXT_SHAPE', SKIP_NO_ATLAS_PAGE);
|
|
21
|
+
}
|
|
22
|
+
// A blank line at either END of the file is named as that end (issues #803,
|
|
23
|
+
// #810), and a run of them is one finding stating its length. They are taken
|
|
24
|
+
// off both ends before the walk below, so the walk sees the page blocks and
|
|
25
|
+
// the blank lines between them and nothing else: a blank line after the last
|
|
26
|
+
// region is not a page block, and "consecutive blank lines" and "the last
|
|
27
|
+
// page block declares no region" are about blocks. `TextureAtlas` reads a
|
|
28
|
+
// blank at either end as nothing (`TextureAtlas.js:98-100` skips the leading
|
|
29
|
+
// run, `:116-118` ends a page block on each blank line), and rigc writes
|
|
30
|
+
// neither (`writeAtlasText`, and `--atlas-in` through `canonicalAtlasShape`),
|
|
31
|
+
// so a file that has one was written by something else and is refused.
|
|
32
|
+
let leading = 0;
|
|
33
|
+
while (leading < atlasLines.length && atlasLines[leading].trim().length === 0) leading++;
|
|
34
|
+
if (leading > 0) {
|
|
35
|
+
fail('A07_ATLAS_TEXT_SHAPE', `line 1: the file begins with ${leading === 1 ? 'a blank line' : `${leading} blank lines`}`);
|
|
36
|
+
}
|
|
37
|
+
let trailing = 0;
|
|
38
|
+
while (trailing < atlasLines.length - leading && atlasLines[atlasLines.length - 1 - trailing].trim().length === 0) trailing++;
|
|
39
|
+
const blockEnd = atlasLines.length - trailing;
|
|
40
|
+
let expectPage = true;
|
|
41
|
+
let sawRegionForPage = false;
|
|
42
|
+
for (let i = leading; i < blockEnd; i++) {
|
|
43
|
+
const line = atlasLines[i];
|
|
44
|
+
if (line.trim().length === 0) {
|
|
45
|
+
if (expectPage) fail('A07_ATLAS_TEXT_SHAPE', `line ${i + 1}: consecutive blank lines`);
|
|
46
|
+
else if (!sawRegionForPage) {
|
|
47
|
+
fail('A07_ATLAS_TEXT_SHAPE', `line ${i + 1}: blank line before this page had any region`);
|
|
48
|
+
}
|
|
49
|
+
expectPage = true;
|
|
50
|
+
sawRegionForPage = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (expectPage) {
|
|
54
|
+
expectPage = false;
|
|
55
|
+
continue; // page name line
|
|
56
|
+
}
|
|
57
|
+
if (line.includes(':')) continue; // key: value line
|
|
58
|
+
// A bare non-key line is a region name, and it is used untrimmed.
|
|
59
|
+
if (line !== line.trim()) {
|
|
60
|
+
fail('A07_ATLAS_TEXT_SHAPE', `line ${i + 1}: region name has stray whitespace: ${JSON.stringify(line)}`);
|
|
61
|
+
}
|
|
62
|
+
sawRegionForPage = true;
|
|
63
|
+
}
|
|
64
|
+
if (!sawRegionForPage) fail('A07_ATLAS_TEXT_SHAPE', 'the last page block declares no region');
|
|
65
|
+
if (trailing > 0) {
|
|
66
|
+
fail('A07_ATLAS_TEXT_SHAPE', `line ${blockEnd + 1}: the file ends with ${trailing === 1 ? 'a blank line' : `${trailing} blank lines`}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A08, the body (issue #1025, step 4c of #380): every region an attachment
|
|
3
|
+
* resolves through is a region the atlas has, spelled exactly.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the atlas's
|
|
6
|
+
* region names and the skin entries' lookups are a fact
|
|
7
|
+
* (`../facts/region_joins.ts`) rather than a `TextureAtlas` built here and the
|
|
8
|
+
* raw JSON's walk. Why the rule runs before the round trip, and what A00 does
|
|
9
|
+
* about a miss it names, is argued above the `check` call in `validate()`,
|
|
10
|
+
* which stays there.
|
|
11
|
+
*
|
|
12
|
+
* `pathsWithNoRegion` is filled, not read: every lookup this body refuses for
|
|
13
|
+
* want of a region, which `validate()`'s A00 reads to defer to this rule rather
|
|
14
|
+
* than restate the miss. The model side hands a set nobody reads.
|
|
15
|
+
*/
|
|
16
|
+
import type { Verdicts } from '../harness.ts';
|
|
17
|
+
import type { RegionJoinFacts } from '../facts/region_joins.ts';
|
|
18
|
+
import { SKIP_NO_ATTACHMENT_REGION_JOIN } from '../reasons.ts';
|
|
19
|
+
|
|
20
|
+
export function a08RegionNamesMatchAttachments({ fail, skip }: Verdicts, facts: RegionJoinFacts, pathsWithNoRegion: Set<string>): void {
|
|
21
|
+
if (facts.regionNames === null) {
|
|
22
|
+
return skip(
|
|
23
|
+
'A08_REGION_NAMES_MATCH_ATTACHMENTS',
|
|
24
|
+
'the atlas text does not parse, so there are no region names to join against (A00 owns that failure)',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const regionNames = new Set(facts.regionNames);
|
|
28
|
+
if (facts.joins === null) {
|
|
29
|
+
return skip('A08_REGION_NAMES_MATCH_ATTACHMENTS', 'the skeleton JSON did not parse (A00 owns that failure)');
|
|
30
|
+
}
|
|
31
|
+
let joined = 0;
|
|
32
|
+
for (const join of facts.joins) {
|
|
33
|
+
// A `sequence` the walk will not guess at: say nothing rather than invent
|
|
34
|
+
// a region name. A00 still has the last word on it.
|
|
35
|
+
if (join.lookups === null) continue;
|
|
36
|
+
for (const lookup of join.lookups) {
|
|
37
|
+
joined++;
|
|
38
|
+
const at = `skin "${join.skin}" slot "${join.slot}" placeholder "${join.placeholder}"`;
|
|
39
|
+
const present = regionNames.has(lookup);
|
|
40
|
+
if (!present) pathsWithNoRegion.add(lookup);
|
|
41
|
+
if (lookup !== lookup.trim()) {
|
|
42
|
+
fail(
|
|
43
|
+
'A08_REGION_NAMES_MATCH_ATTACHMENTS',
|
|
44
|
+
`${at}: attachment "${join.name}" resolves through path ${JSON.stringify(lookup)}, which has stray ` +
|
|
45
|
+
`whitespace — the atlas is matched on the exact string, and ${
|
|
46
|
+
present
|
|
47
|
+
? 'the region it finds carries the same padding'
|
|
48
|
+
: regionNames.has(lookup.trim())
|
|
49
|
+
? `the region this atlas has is ${JSON.stringify(lookup.trim())}, without it`
|
|
50
|
+
: 'no region of this atlas carries it'
|
|
51
|
+
}`,
|
|
52
|
+
);
|
|
53
|
+
} else if (!present) {
|
|
54
|
+
// Not a guess and not a repair — a region the atlas DOES hold that
|
|
55
|
+
// differs from the wanted name only by case or padding. It is
|
|
56
|
+
// reported because it was measured, and where there is none the
|
|
57
|
+
// sentence says nothing at all.
|
|
58
|
+
const near = [...regionNames].find((r) => r.trim().toLowerCase() === lookup.toLowerCase());
|
|
59
|
+
fail(
|
|
60
|
+
'A08_REGION_NAMES_MATCH_ATTACHMENTS',
|
|
61
|
+
`${at}: attachment "${join.name}" wants region "${lookup}", which this atlas does not have${
|
|
62
|
+
near === undefined ? '' : ` — it does have ${JSON.stringify(near)}`
|
|
63
|
+
}. Either the skeleton's "path" or the atlas region name is the one that moved`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const region of regionNames) {
|
|
69
|
+
if (region !== region.trim()) {
|
|
70
|
+
fail('A08_REGION_NAMES_MATCH_ATTACHMENTS', `atlas region ${JSON.stringify(region)} has stray whitespace`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (joined === 0 && regionNames.size === 0) {
|
|
74
|
+
return skip('A08_REGION_NAMES_MATCH_ATTACHMENTS', SKIP_NO_ATTACHMENT_REGION_JOIN);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A09, the body (issue #1025, cut 4c-3 of step 4c of #380): the compiled
|
|
3
|
+
* duration is the declared duration (rule 4).
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the
|
|
6
|
+
* animations, their durations and their timelines' durations as one fact
|
|
7
|
+
* (`../facts/animation_durations.ts`) where it read the loaded skeleton's
|
|
8
|
+
* `animations`, `findAnimation` and each timeline's `getDuration()`. The
|
|
9
|
+
* declared durations are the caller's on both sides, as the rig info is. The
|
|
10
|
+
* `check` call stays in `validate()`.
|
|
11
|
+
*/
|
|
12
|
+
import type { Verdicts } from '../harness.ts';
|
|
13
|
+
import type { AnimationDurationFacts } from '../facts/animation_durations.ts';
|
|
14
|
+
import { SKIP_NO_DECLARED_DURATION } from '../reasons.ts';
|
|
15
|
+
import { float32Step } from '../../timelines.ts';
|
|
16
|
+
|
|
17
|
+
/** One frame at 60 fps: how far a last key may fall short of the declared end before the declared duration reads as wrong. */
|
|
18
|
+
const FRAME = 1 / 60;
|
|
19
|
+
|
|
20
|
+
export function a09AnimationDurationMatchesSpec({ fail, skip }: Verdicts, facts: AnimationDurationFacts, declaredDurations: Record<string, number> | undefined): void {
|
|
21
|
+
// Without the spec there is no declared duration to compare the compiled
|
|
22
|
+
// one against, and returning here used to count as a PASS — a gate saying
|
|
23
|
+
// it checked something it never looked at.
|
|
24
|
+
if (!declaredDurations) {
|
|
25
|
+
return skip('A09_ANIMATION_DURATION_MATCHES_SPEC', 'no motion spec supplied, so no declared duration to compare against');
|
|
26
|
+
}
|
|
27
|
+
// Same trap one level down. A **static rig** — a skeleton that exists to
|
|
28
|
+
// be posed and carries no animation at all, which is what
|
|
29
|
+
// `1-weight-and-mass`'s second export is — declares nothing and loads
|
|
30
|
+
// nothing, so both loops below iterate zero times and the assertion
|
|
31
|
+
// reported PASS. That is the vacuous green this report is built to refuse:
|
|
32
|
+
// there is no duration here, and saying so is the honest answer.
|
|
33
|
+
if (Object.keys(declaredDurations).length === 0 && facts.animations.length === 0) {
|
|
34
|
+
return skip('A09_ANIMATION_DURATION_MATCHES_SPEC', SKIP_NO_DECLARED_DURATION);
|
|
35
|
+
}
|
|
36
|
+
for (const [name, declared] of Object.entries(declaredDurations)) {
|
|
37
|
+
const anim = facts.animations.find((a) => a.name === name);
|
|
38
|
+
if (!anim) {
|
|
39
|
+
fail('A09_ANIMATION_DURATION_MATCHES_SPEC', `spec declares animation "${name}" but the skeleton has none`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Two arms, and the asymmetry is the point.
|
|
43
|
+
//
|
|
44
|
+
// UNDERSHOOT is R7's question — is the declared duration wrong? An
|
|
45
|
+
// animation may hold its final pose, so a last key a little before the
|
|
46
|
+
// end is ordinary and a frame of that is slack.
|
|
47
|
+
//
|
|
48
|
+
// OVERSHOOT is a different question with a different tolerance.
|
|
49
|
+
// `anim.duration` IS the largest key time (`SkeletonJson.ts:1261` takes
|
|
50
|
+
// the max over every timeline's own duration), so a loaded duration past
|
|
51
|
+
// the declared one means a KEY is past it — and nothing that plays the
|
|
52
|
+
// animation for the duration it declares will ever reach that key. Rung
|
|
53
|
+
// 6 lost a one-frame attachment reveal to a key 3.4e-5 s past the end,
|
|
54
|
+
// 1/500 of FRAME, which this comparison read as agreement (issue #54).
|
|
55
|
+
// `compile.ts` refuses that per timeline now; this is the same rule held
|
|
56
|
+
// against a skeleton the compiler never saw.
|
|
57
|
+
// The slack is one float32 step at the declared duration, and it is the
|
|
58
|
+
// same function the compiler's Rule 4 refuses on (`float32Step`, in
|
|
59
|
+
// `timelines.ts`, which says why it is a step of the float and no longer
|
|
60
|
+
// a fixed 1e-6 plus one).
|
|
61
|
+
const slack = float32Step(declared);
|
|
62
|
+
const past = anim.duration - declared;
|
|
63
|
+
if (past > slack) {
|
|
64
|
+
const late = anim.timelineDurations.filter((d) => d - declared > slack).length;
|
|
65
|
+
fail(
|
|
66
|
+
'A09_ANIMATION_DURATION_MATCHES_SPEC',
|
|
67
|
+
`animation "${name}" has ${late} timeline(s) keyed past the declared duration ${declared}s — ` +
|
|
68
|
+
`the last key is at ${anim.duration}s, ${past.toFixed(6)}s late, so nothing ever samples it`,
|
|
69
|
+
);
|
|
70
|
+
} else if (declared - anim.duration > FRAME) {
|
|
71
|
+
fail(
|
|
72
|
+
'A09_ANIMATION_DURATION_MATCHES_SPEC',
|
|
73
|
+
`animation "${name}" loaded duration ${anim.duration}s, spec declares ${declared}s`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const anim of facts.animations) {
|
|
78
|
+
if (!(anim.name in declaredDurations)) {
|
|
79
|
+
fail('A09_ANIMATION_DURATION_MATCHES_SPEC', `skeleton has animation "${anim.name}" with no spec entry`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A10, the body (issue #1025, cut 4c-5a of step 4c of #380): no pose the
|
|
3
|
+
* skeleton is stepped through holds a number that is not finite, or a bone
|
|
4
|
+
* with no inheritance mode.
|
|
5
|
+
*
|
|
6
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads, which is
|
|
7
|
+
* `BoneTimelineFacts` (`../facts/bone_timelines.ts`) for the `inherit` keys
|
|
8
|
+
* where it read the skeleton JSON, and `SteppedPoseFacts`
|
|
9
|
+
* (`../facts/stepped_poses.ts`) where it posed a spine-core skeleton itself:
|
|
10
|
+
* the setup pose, each animation's looping walk, and a bone's mode posed at a
|
|
11
|
+
* key — which `validate()` takes from spine-core and the model side from
|
|
12
|
+
* rigc's core. The scan of a pose is `firstNonFinite` (`../../nonfinite.ts`),
|
|
13
|
+
* the sentence `render` refuses on, called rather than restated. The argument
|
|
14
|
+
* for each clause is above the `check` call, which stays in `validate()`.
|
|
15
|
+
*/
|
|
16
|
+
import type { Verdicts } from '../harness.ts';
|
|
17
|
+
import type { BoneTimelineFacts } from '../facts/bone_timelines.ts';
|
|
18
|
+
import type { SteppedFrame, SteppedPoseFacts } from '../facts/stepped_poses.ts';
|
|
19
|
+
import { SKIP_NO_POSE } from '../reasons.ts';
|
|
20
|
+
import { isObj } from '../values.ts';
|
|
21
|
+
import { firstNonFinite } from '../../nonfinite.ts';
|
|
22
|
+
import { BONE_INHERIT_KNOWN } from '../../rig.ts';
|
|
23
|
+
|
|
24
|
+
/** The steps A10 takes through every animation: the walk is `max(duration, 1) / STEP_FRAMES` long per step. */
|
|
25
|
+
export const STEP_FRAMES = 120;
|
|
26
|
+
|
|
27
|
+
export function a10NoNanAfterStepping({ fail, skip, stats }: Verdicts, boneTimelines: BoneTimelineFacts, facts: SteppedPoseFacts): void {
|
|
28
|
+
if (facts.steppedAnimations.length === 0 && facts.boneCount === 0) {
|
|
29
|
+
skip('A10_NO_NAN_AFTER_STEPPING', SKIP_NO_POSE);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// -- the bone's inheritance mode, posed at every `inherit` key
|
|
34
|
+
let unresolved = 0;
|
|
35
|
+
for (const { animation: animName, bone: boneName, timelines } of boneTimelines.boneTimelines) {
|
|
36
|
+
if (!isObj(timelines) || !Array.isArray(timelines.inherit)) continue;
|
|
37
|
+
if (!facts.hasAnimation(animName) || !facts.hasBone(boneName)) continue;
|
|
38
|
+
for (const rawKey of timelines.inherit as unknown[]) {
|
|
39
|
+
if (!isObj(rawKey)) continue;
|
|
40
|
+
const time = typeof rawKey.time === 'number' ? rawKey.time : 0;
|
|
41
|
+
const posed = facts.posedInherit(animName, boneName, time);
|
|
42
|
+
if (posed === null) continue;
|
|
43
|
+
unresolved++;
|
|
44
|
+
fail(
|
|
45
|
+
'A10_NO_NAN_AFTER_STEPPING',
|
|
46
|
+
`animation "${animName}" bone "${boneName}" inherit (t=${time}): the bone poses inheritance mode ` +
|
|
47
|
+
`${String(posed)} — the key spells ${JSON.stringify(rawKey.inherit)}, which the runtime's mode lookup ` +
|
|
48
|
+
`does not resolve (${BONE_INHERIT_KNOWN}), so no mode applies until the next key and the bone keeps ` +
|
|
49
|
+
'the world rotation, scale and shear it had',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (unresolved > 0) return;
|
|
54
|
+
|
|
55
|
+
// -- the whole world transform, at the setup pose and every stepped frame
|
|
56
|
+
if (facts.steppedAnimations.length === 0) stats.nanStepping = 'skipped';
|
|
57
|
+
const atRest = facts.setup();
|
|
58
|
+
const atSetup = firstNonFinite('the setup pose', atRest.drawn, atRest.bones);
|
|
59
|
+
if (atSetup !== null) {
|
|
60
|
+
fail('A10_NO_NAN_AFTER_STEPPING', atSetup);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What a posed frame shows that the world-transform scan does not read: a
|
|
66
|
+
* bone posing no inheritance mode, and a slot colour that is not finite.
|
|
67
|
+
* `where` leads the sentence — the animation's name at a stepped frame,
|
|
68
|
+
* as it always has, or `the setup pose` on a skeleton with no animation.
|
|
69
|
+
*/
|
|
70
|
+
const poseDefect = (where: string, frame: SteppedFrame): string | null => {
|
|
71
|
+
for (const bone of frame.bones) {
|
|
72
|
+
if (bone.inherit !== null) {
|
|
73
|
+
return (
|
|
74
|
+
`${where}: bone "${bone.name}" poses inheritance mode ${bone.inherit} — its setup ` +
|
|
75
|
+
`spells inherit ${facts.statedInherit(bone.name)}, which the ` +
|
|
76
|
+
`runtime's mode lookup does not resolve (${BONE_INHERIT_KNOWN}), so its world rotation, scale and shear ` +
|
|
77
|
+
'are never computed — they stay 0 and everything the bone carries collapses to a point'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const slot of frame.slots) {
|
|
82
|
+
if (!slot.colour.every(Number.isFinite)) return `${where}: slot "${slot.name}" colour is non-finite`;
|
|
83
|
+
if (slot.dark !== null && !slot.dark.every(Number.isFinite)) {
|
|
84
|
+
return `${where}: slot "${slot.name}" dark colour is non-finite`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (facts.steppedAnimations.length === 0) {
|
|
91
|
+
const atRestDefect = poseDefect('the setup pose', atRest);
|
|
92
|
+
if (atRestDefect !== null) fail('A10_NO_NAN_AFTER_STEPPING', atRestDefect);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const anim of facts.steppedAnimations) {
|
|
96
|
+
const step = Math.max(anim.duration, 1) / STEP_FRAMES;
|
|
97
|
+
const frames = facts.walk(anim.name, step, STEP_FRAMES);
|
|
98
|
+
for (let i = 0; i < STEP_FRAMES; i++) {
|
|
99
|
+
const frame = frames[i];
|
|
100
|
+
const found = firstNonFinite(
|
|
101
|
+
`animation ${JSON.stringify(anim.name)} frame ${i + 1} of ${STEP_FRAMES} (t=${((i + 1) * step).toFixed(4)}s)`,
|
|
102
|
+
frame.drawn,
|
|
103
|
+
frame.bones,
|
|
104
|
+
);
|
|
105
|
+
if (found !== null) {
|
|
106
|
+
fail('A10_NO_NAN_AFTER_STEPPING', found);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const defect = poseDefect(anim.name, frame);
|
|
110
|
+
if (defect !== null) {
|
|
111
|
+
fail('A10_NO_NAN_AFTER_STEPPING', defect);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A11, the body (issue #1025, step 4c of #380): no clipping attachment — the
|
|
3
|
+
* renderer skips them silently. A `renderer` rule, so `spine` never runs it.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the count is
|
|
6
|
+
* a fact (`../facts/skin_entries.ts`).
|
|
7
|
+
*/
|
|
8
|
+
import type { Verdicts } from '../harness.ts';
|
|
9
|
+
import type { SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
10
|
+
|
|
11
|
+
export function a11NoClippingAttachments({ fail }: Verdicts, { clippingCount }: SkinEntryFacts): void {
|
|
12
|
+
if (clippingCount > 0) {
|
|
13
|
+
fail('A11_NO_CLIPPING_ATTACHMENTS', `${clippingCount} clipping attachment(s); the renderer skips them silently`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A12, the body (issue #1025, cut 4c-4 of #380): no dark colour and no
|
|
3
|
+
* two-colour timeline — both parsed, then silently ignored by spine-html.
|
|
4
|
+
*
|
|
5
|
+
* Moved out of `src/validate.ts` unchanged but for what it reads: the slots
|
|
6
|
+
* are a fact (`../facts/skeleton_roster.ts`) and the slot timelines are A45's
|
|
7
|
+
* (`../facts/slot_colour.ts`), rather than the raw JSON and `walkTimelines`
|
|
8
|
+
* over it. The path printed is the one `walkTimelines` built for a slot
|
|
9
|
+
* timeline, `<animation>.slots.<slot>.<timeline>`, and only a timeline whose
|
|
10
|
+
* keys are a list is one it visited.
|
|
11
|
+
*/
|
|
12
|
+
import type { Verdicts } from '../harness.ts';
|
|
13
|
+
import type { SkeletonRosterFacts } from '../facts/skeleton_roster.ts';
|
|
14
|
+
import type { SlotTimelines } from '../facts/slot_colour.ts';
|
|
15
|
+
|
|
16
|
+
export function a12NoDarkColor({ fail }: Verdicts, { slots }: SkeletonRosterFacts, slotTimelines: readonly SlotTimelines[]): void {
|
|
17
|
+
for (const slot of slots) {
|
|
18
|
+
if (slot.dark) {
|
|
19
|
+
fail('A12_NO_DARK_COLOR', `slot "${slot.name}" declares a dark colour; the renderer ignores it`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const { animation, slot, timelines } of slotTimelines) {
|
|
23
|
+
for (const [name, keys] of Object.entries(timelines)) {
|
|
24
|
+
if (!Array.isArray(keys)) continue;
|
|
25
|
+
if (name === 'rgba2' || name === 'rgb2') {
|
|
26
|
+
fail('A12_NO_DARK_COLOR', `${animation}.slots.${slot}.${name}: two-colour timeline "${name}" is silently ignored by the renderer`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|