rig-c 0.0.0-stage → 2.21.0
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 +1191 -0
- package/src/meshquality.ts +2051 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1444 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
package/src/nonfinite.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one sentence for a pose number that is not finite (issue #882, #873) —
|
|
3
|
+
* moved here from `src/render.ts` by issue #1025 (cut 4c-5a of step 4c of
|
|
4
|
+
* #380), unchanged, because it now has a reader that may not link the
|
|
5
|
+
* runtime: `A10_NO_NAN_AFTER_STEPPING`'s body (`./assertions/bodies/a10.ts`),
|
|
6
|
+
* which reads it over the poses spine-core gives and over the poses rigc's
|
|
7
|
+
* core gives. `render` refuses on the same sentence, so the gate and the
|
|
8
|
+
* renderer still hold one definition of "not finite" — the six terms of a
|
|
9
|
+
* bone's world transform, then the vertices computed from them.
|
|
10
|
+
*
|
|
11
|
+
* Pure: no import at all.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The bone fields a world transform is made of — what `firstNonFinite` reads off a bone. */
|
|
15
|
+
export interface WorldTransform {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly a: number;
|
|
18
|
+
readonly b: number;
|
|
19
|
+
readonly c: number;
|
|
20
|
+
readonly d: number;
|
|
21
|
+
readonly worldX: number;
|
|
22
|
+
readonly worldY: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The fields of an attachment entry `firstNonFinite` reads — a frame's posed attachment or a rest entry. */
|
|
26
|
+
export interface PosedVertices {
|
|
27
|
+
readonly slot: string;
|
|
28
|
+
readonly attachment: string;
|
|
29
|
+
readonly vertices: readonly number[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The sentence for the first number at `where` that is not finite — bones before
|
|
34
|
+
* vertices, since a bone that overflowed is the cause and its vertices the
|
|
35
|
+
* symptom — or `null` when every number there is finite.
|
|
36
|
+
*/
|
|
37
|
+
export function firstNonFinite(where: string, entries: readonly PosedVertices[], bones: readonly WorldTransform[]): string | null {
|
|
38
|
+
for (const bone of bones) {
|
|
39
|
+
for (const field of ['a', 'b', 'c', 'd', 'worldX', 'worldY'] as const) {
|
|
40
|
+
if (!Number.isFinite(bone[field])) {
|
|
41
|
+
return `${where}: bone ${JSON.stringify(bone.name)} has ${field} ${String(bone[field])}; a world transform is finite`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const bad = entry.vertices.findIndex((value) => !Number.isFinite(value));
|
|
47
|
+
if (bad === -1) continue;
|
|
48
|
+
return (
|
|
49
|
+
`${where}: slot ${JSON.stringify(entry.slot)} attachment ${JSON.stringify(entry.attachment)} vertex ` +
|
|
50
|
+
`${Math.floor(bad / 2)} has ${bad % 2 === 0 ? 'x' : 'y'} ${String(entry.vertices[bad])}; a posed vertex is finite`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's own metadata — its installed version and repository — read
|
|
3
|
+
* once from the `package.json` beside the code.
|
|
4
|
+
*
|
|
5
|
+
* Moved here unchanged from `src/cli/shared.ts` (issue #1230), which imports
|
|
6
|
+
* these back and re-exports every name it exported before, so no caller
|
|
7
|
+
* changed an import. They moved because the motion comparison
|
|
8
|
+
* (`src/meshcompare.ts`) records the version of the poser that ran
|
|
9
|
+
* (`MeshQualityReport.poser`, P2 of docs/MESH_REDUCTION.md), and a library
|
|
10
|
+
* module that read it through `src/cli/shared.ts` would load the whole CLI —
|
|
11
|
+
* the compiler, `check` and every command body — to read one string. One
|
|
12
|
+
* reader, in the one place both can reach.
|
|
13
|
+
*
|
|
14
|
+
* Imports nothing but `node:fs` and `node:path`: no clock, no runtime.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
interface PackageMeta {
|
|
20
|
+
version?: string;
|
|
21
|
+
repository?: string | { url?: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let packageMeta: PackageMeta | null | undefined;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The directory `cli.ts` sits in: `package.json`, `skills/` and, in a
|
|
28
|
+
* checkout, `examples/` and `scripts/` are beside it, in the repository and
|
|
29
|
+
* once installed. One level above this file (`src/`) — the same directory
|
|
30
|
+
* `src/cli/shared.ts` reached two levels above itself before the reader moved.
|
|
31
|
+
*/
|
|
32
|
+
export const PACKAGE_ROOT = join(import.meta.dir, '..');
|
|
33
|
+
|
|
34
|
+
/** `package.json` sits next to `cli.ts` both in the repo and once installed (`PACKAGE_ROOT`). */
|
|
35
|
+
export function readPackageMeta(): PackageMeta | null {
|
|
36
|
+
if (packageMeta === undefined) {
|
|
37
|
+
try {
|
|
38
|
+
packageMeta = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')) as PackageMeta;
|
|
39
|
+
} catch {
|
|
40
|
+
packageMeta = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return packageMeta;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readVersion(): string {
|
|
47
|
+
return readPackageMeta()?.version ?? 'unknown';
|
|
48
|
+
}
|
package/src/png.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PNG header reader.
|
|
3
|
+
*
|
|
4
|
+
* The only things rigc needs from a part PNG are its true pixel size and whether
|
|
5
|
+
* it can draw a transparent pixel. The size is in the IHDR chunk that every PNG
|
|
6
|
+
* puts first; transparency is in the IHDR's colour type OR in a `tRNS` chunk a
|
|
7
|
+
* little further in, so the reader walks the file's small leading chunks and
|
|
8
|
+
* stops at the pixel data. That keeps the compiler dependency-free — no image
|
|
9
|
+
* library, and nothing on the module path but rigc itself — while still reading
|
|
10
|
+
* every place the answer can be written down.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ It reads a header, not an image: "can this file draw a transparent pixel",
|
|
13
|
+
* never "does it". Whether the art actually has a transparent margin is a
|
|
14
|
+
* question about pixels, and the tools that measure pixels decode the whole file
|
|
15
|
+
* ([`tools/plate.ts`](../tools/plate.ts)).
|
|
16
|
+
*
|
|
17
|
+
* Measuring instead of trusting is the whole point: an atlas `size:` that
|
|
18
|
+
* disagrees with the file loads clean and collapses the UVs silently.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
const SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* PNG colour types that carry a per-pixel alpha channel.
|
|
26
|
+
*
|
|
27
|
+
* ⭐ False here does NOT mean opaque, and reading it that way is what #215 was:
|
|
28
|
+
* types 0, 2 and 3 can all carry a `tRNS` chunk instead — a palette alpha table
|
|
29
|
+
* for indexed art, one invisible colour for the other two — and indexed+tRNS is
|
|
30
|
+
* the ordinary output of ImageMagick, Photoshop's PNG-8 export, GIMP's indexed
|
|
31
|
+
* mode, aseprite and pngquant. `hasTransparency` is the field to judge art by;
|
|
32
|
+
* this one answers the narrower question of where the alpha is stored.
|
|
33
|
+
*/
|
|
34
|
+
const COLOUR_TYPE_HAS_ALPHA: Record<number, boolean> = {
|
|
35
|
+
0: false, // greyscale
|
|
36
|
+
2: false, // truecolour
|
|
37
|
+
3: false, // indexed (transparency, if any, is in tRNS)
|
|
38
|
+
4: true, // greyscale + alpha
|
|
39
|
+
6: true, // truecolour + alpha
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The spec's name for each colour type, for messages that have to name one. */
|
|
43
|
+
const COLOUR_TYPE_NAMES: Record<number, string> = {
|
|
44
|
+
0: 'greyscale',
|
|
45
|
+
2: 'truecolour',
|
|
46
|
+
3: 'indexed',
|
|
47
|
+
4: 'greyscale + alpha',
|
|
48
|
+
6: 'truecolour + alpha',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** How to say a colour type out loud. Unknown types print as themselves. */
|
|
52
|
+
export function colourTypeName(colourType: number): string {
|
|
53
|
+
return COLOUR_TYPE_NAMES[colourType] ?? 'unrecognised';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PngInfo {
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
bitDepth: number;
|
|
60
|
+
colourType: number;
|
|
61
|
+
/** A per-pixel alpha channel in the pixel data: colour types 4 and 6, and only those. */
|
|
62
|
+
hasAlpha: boolean;
|
|
63
|
+
/** A `tRNS` chunk: a palette alpha table (type 3), or one invisible colour (types 0 and 2). */
|
|
64
|
+
hasTrns: boolean;
|
|
65
|
+
/** Either of the above — the file is able to draw a transparent pixel. */
|
|
66
|
+
hasTransparency: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Walk the chunk list looking for `tRNS`, stopping where it can no longer appear.
|
|
71
|
+
*
|
|
72
|
+
* The spec orders `tRNS` after `PLTE` and before the first `IDAT`, so this reads
|
|
73
|
+
* only the file's small leading chunks and never touches the compressed bulk. A
|
|
74
|
+
* length that would run past the end of the file ends the walk rather than
|
|
75
|
+
* throwing: a truncated PNG is A17 and A06's business, and answering "no tRNS"
|
|
76
|
+
* about a file nobody can open is the same answer either way.
|
|
77
|
+
*/
|
|
78
|
+
function scanForTrns(buf: Buffer): boolean {
|
|
79
|
+
let at = 8; // past the signature; the first chunk is IHDR
|
|
80
|
+
while (at + 8 <= buf.length) {
|
|
81
|
+
const length = buf.readUInt32BE(at);
|
|
82
|
+
const type = buf.toString('latin1', at + 4, at + 8);
|
|
83
|
+
if (type === 'tRNS') return true;
|
|
84
|
+
if (type === 'IDAT' || type === 'IEND') return false;
|
|
85
|
+
const next = at + 12 + length; // 4 length + 4 type + body + 4 CRC
|
|
86
|
+
if (next <= at || next > buf.length) return false;
|
|
87
|
+
at = next;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hexBytes(bytes: Uint8Array): string {
|
|
93
|
+
return [...bytes].map((b) => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The signature every PNG begins with, as the messages below print it. */
|
|
97
|
+
const PNG_SIGNATURE_HEX = hexBytes(Uint8Array.from(SIGNATURE));
|
|
98
|
+
|
|
99
|
+
function startsWith(buf: Uint8Array, at: number, bytes: ReadonlyArray<number>): boolean {
|
|
100
|
+
if (buf.length < at + bytes.length) return false;
|
|
101
|
+
for (let i = 0; i < bytes.length; i++) if (buf[at + i] !== bytes[i]) return false;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ascii = (text: string): number[] => [...text].map((c) => c.charCodeAt(0));
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The image formats a page file is found to be in instead, by their own
|
|
109
|
+
* signatures. Each entry is the format's fixed leading bytes and nothing
|
|
110
|
+
* inferred from them — rigc decodes none of these, so the one honest thing to
|
|
111
|
+
* say about such a file is what its first bytes are.
|
|
112
|
+
*
|
|
113
|
+
* ⭐ Named at all because a file's NAME is not evidence (issue #732): a
|
|
114
|
+
* production pack shipped WebP pages called `*.png`, and "bad signature" sent
|
|
115
|
+
* the author to look for corruption in a well-formed image of another format.
|
|
116
|
+
*/
|
|
117
|
+
const OTHER_FORMATS: ReadonlyArray<{ name: string; matches: (buf: Uint8Array) => boolean }> = [
|
|
118
|
+
// A RIFF container with the form type at byte 8. The first chunk's FourCC
|
|
119
|
+
// (`VP8 `, `VP8L`, `VP8X`) is printed beside it by `whatItIs`, never read.
|
|
120
|
+
{ name: 'WebP', matches: (buf) => startsWith(buf, 0, ascii('RIFF')) && startsWith(buf, 8, ascii('WEBP')) },
|
|
121
|
+
{ name: 'JPEG', matches: (buf) => startsWith(buf, 0, [0xff, 0xd8, 0xff]) },
|
|
122
|
+
{ name: 'GIF', matches: (buf) => startsWith(buf, 0, ascii('GIF87a')) || startsWith(buf, 0, ascii('GIF89a')) },
|
|
123
|
+
{ name: 'KTX', matches: (buf) => startsWith(buf, 0, [0xab, ...ascii('KTX 11'), 0xbb, 0x0d, 0x0a, 0x1a, 0x0a]) },
|
|
124
|
+
{ name: 'KTX2', matches: (buf) => startsWith(buf, 0, [0xab, ...ascii('KTX 20'), 0xbb, 0x0d, 0x0a, 0x1a, 0x0a]) },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/** What a file that does not begin with the PNG signature is, as a phrase. */
|
|
128
|
+
function whatItIs(buf: Uint8Array): string {
|
|
129
|
+
const found = OTHER_FORMATS.find((format) => format.matches(buf));
|
|
130
|
+
if (found === undefined) {
|
|
131
|
+
return (
|
|
132
|
+
'in no image format rigc recognises by its signature (it names ' +
|
|
133
|
+
`${OTHER_FORMATS.map((format) => format.name).join(', ')} when it meets them)`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (found.name === 'WebP' && buf.length >= 16) {
|
|
137
|
+
const chunk = String.fromCharCode(buf[12], buf[13], buf[14], buf[15]);
|
|
138
|
+
return `a WebP image (a RIFF/WEBP container whose first chunk is ${JSON.stringify(chunk)})`;
|
|
139
|
+
}
|
|
140
|
+
return `a ${found.name} image`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Why the bytes at `path` are not a PNG rigc can read, as one sentence — or
|
|
145
|
+
* `null` when they are.
|
|
146
|
+
*
|
|
147
|
+
* 🔒 **The one reader of a page file's identity** (issue #732). Every reader of
|
|
148
|
+
* a page goes through it: `readPngInfo` and `readPngHeader` below (the size
|
|
149
|
+
* `A06` judges, the alpha `A19` judges on a one-part page, the sizes the
|
|
150
|
+
* compiler takes from a loose part or a pack's page) and `readPlate` in
|
|
151
|
+
* [`tools/plate.ts`](../tools/plate.ts) (the renderer, `A19`'s scan of a shared
|
|
152
|
+
* page, the region lift and the packer). Before it, one WebP page reached the
|
|
153
|
+
* gate as `A06`'s `threw: not a PNG (bad signature)` and `A19`'s `threw: cannot
|
|
154
|
+
* decode PNG …: unexpected end of file`, and reached `build --atlas-in` as a
|
|
155
|
+
* stack trace — three sentences about one file, and none said what it was.
|
|
156
|
+
*
|
|
157
|
+
* It says four things and decodes nothing: what the file is instead (by the
|
|
158
|
+
* signature it does carry, with its first bytes in hex either way), whether a
|
|
159
|
+
* file that begins as a PNG ends before its chunks do, whether IHDR comes
|
|
160
|
+
* first, and whether IHDR states a size of at least 1x1 (issue #1073). The
|
|
161
|
+
* chunk walk reads only the eight-byte chunk headers, so it costs a skip
|
|
162
|
+
* through the file and no inflate — which is also why it cannot say whether
|
|
163
|
+
* the image data decodes: that is `imageDataProblem` in
|
|
164
|
+
* [`tools/plate.ts`](../tools/plate.ts), beside the decoder (issue #1074).
|
|
165
|
+
*
|
|
166
|
+
* ⚠️ **"Truncated" is judged by the chunk walk, not by a length floor.** The
|
|
167
|
+
* floor that stood here (`buf.length < 26`, "too short") never said truncated,
|
|
168
|
+
* and a PNG cut anywhere after its IHDR passed it: measured on a packed page
|
|
169
|
+
* cut to 60 bytes, the default profile built it green and wrote a skeleton
|
|
170
|
+
* whose page no reader could decode. A PNG ends at its IEND chunk, so a file
|
|
171
|
+
* that runs out first is truncated wherever it runs out.
|
|
172
|
+
*/
|
|
173
|
+
export function pngProblem(buf: Uint8Array, path: string): string | null {
|
|
174
|
+
if (buf.length === 0) return `${path} is empty (0 bytes), and a PNG's first 8 bytes are ${PNG_SIGNATURE_HEX}`;
|
|
175
|
+
if (!startsWith(buf, 0, SIGNATURE.slice(0, Math.min(buf.length, SIGNATURE.length)))) {
|
|
176
|
+
const shown = buf.subarray(0, 12);
|
|
177
|
+
const byName = /\.png$/i.test(path) ? '; its name ends in .png, and it is the bytes that decide what a file is' : '';
|
|
178
|
+
return (
|
|
179
|
+
`${path} is ${whatItIs(buf)}, not a PNG: its first ${shown.length} byte(s) are ${hexBytes(shown)}, where a ` +
|
|
180
|
+
`PNG's first 8 are ${PNG_SIGNATURE_HEX}${byName}. rigc reads PNG and nothing else — its page-size and alpha ` +
|
|
181
|
+
'readers, its renderer and its region lift all decode PNG, and it links no decoder for any other format — ' +
|
|
182
|
+
'so nothing in this file was measured. Re-export it as PNG'
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const truncated = (where: string): string =>
|
|
186
|
+
`${path} is a truncated PNG: it is ${buf.length} byte(s) long and ${where}, so no reader can decode it. ` +
|
|
187
|
+
'Re-export or re-copy the file whole';
|
|
188
|
+
if (buf.length < SIGNATURE.length) return truncated(`ends ${buf.length} byte(s) into the 8-byte PNG signature`);
|
|
189
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
190
|
+
let at = SIGNATURE.length;
|
|
191
|
+
let previous = 'the signature';
|
|
192
|
+
for (;;) {
|
|
193
|
+
if (at + 8 > buf.length) {
|
|
194
|
+
return truncated(
|
|
195
|
+
at === buf.length
|
|
196
|
+
? `ends after ${previous} with no IEND chunk`
|
|
197
|
+
: `ends ${buf.length - at} byte(s) into the header of the chunk after ${previous}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const length = view.getUint32(at);
|
|
201
|
+
const type = String.fromCharCode(buf[at + 4], buf[at + 5], buf[at + 6], buf[at + 7]);
|
|
202
|
+
if (at === SIGNATURE.length && type !== 'IHDR') {
|
|
203
|
+
return `${path} is a PNG whose first chunk is ${JSON.stringify(type)} rather than the IHDR the format requires first`;
|
|
204
|
+
}
|
|
205
|
+
if (type === 'IHDR' && length !== 13) {
|
|
206
|
+
return `${path} is a PNG whose IHDR chunk declares ${length} byte(s) of data, where the format's is 13`;
|
|
207
|
+
}
|
|
208
|
+
const next = at + 12 + length; // 4 length + 4 type + body + 4 CRC
|
|
209
|
+
if (next > buf.length) {
|
|
210
|
+
return truncated(
|
|
211
|
+
`its ${type} chunk at byte ${at} declares ${length} byte(s) of data, which with its CRC runs to byte ${next}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (type === 'IHDR') {
|
|
215
|
+
// 🔒 **A dimension of 0 is a file that describes no texel** (issue
|
|
216
|
+
// #1073), and the format says so: IHDR's width and height are each at
|
|
217
|
+
// least 1. Accepted here, such a page was read as a valid PNG of the
|
|
218
|
+
// wrong size — A06 printed its grid sentence over a ratio of 0.0000, A19
|
|
219
|
+
// a "not measured" line once per region on the page (13 on one export),
|
|
220
|
+
// the compiler took the 0 as a loose part's size and was refused by A03
|
|
221
|
+
// and A10 (`non-positive size`, `x NaN`) or by the mesher (`bad part
|
|
222
|
+
// size 0x150`) without the file named, and `render` drew it in silence.
|
|
223
|
+
// Refused here, it is one fact about one file, said by the one reader
|
|
224
|
+
// every other reader asks first.
|
|
225
|
+
const width = view.getUint32(at + 8);
|
|
226
|
+
const height = view.getUint32(at + 12);
|
|
227
|
+
if (width === 0 || height === 0) {
|
|
228
|
+
const zero = width === 0 && height === 0 ? 'its width and its height are' : width === 0 ? 'its width is' : 'its height is';
|
|
229
|
+
return (
|
|
230
|
+
`${path} is a PNG whose IHDR chunk states its size as ${width}x${height}: ${zero} 0, where the format ` +
|
|
231
|
+
'requires each dimension to be at least 1, so the file describes no texel and nothing in it can be ' +
|
|
232
|
+
'measured or drawn. Re-export it'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (type === 'IEND') return null;
|
|
237
|
+
previous = `its ${type} chunk at byte ${at}`;
|
|
238
|
+
at = next;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A file that is not a PNG rigc can read, thrown by the readers that return a
|
|
244
|
+
* value or nothing (`readPngInfo`, `readPlate`). The message is `pngProblem`'s
|
|
245
|
+
* — or, from `readPlate`, `imageDataProblem`'s for a file whose header reads
|
|
246
|
+
* and whose image data does not decode (issue #1074) — whole; a caller that
|
|
247
|
+
* owns a named failure — the gate, a `CompileError` — reads the sentence
|
|
248
|
+
* instead of catching the throw (`readPngHeader`).
|
|
249
|
+
*/
|
|
250
|
+
export class NotAPngError extends Error {
|
|
251
|
+
readonly path: string;
|
|
252
|
+
constructor(path: string, message: string) {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = 'NotAPngError';
|
|
255
|
+
this.path = path;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** `pngProblem` as a refusal, for the readers that return a value or throw. */
|
|
260
|
+
export function assertPng(buf: Uint8Array, path: string): void {
|
|
261
|
+
const problem = pngProblem(buf, path);
|
|
262
|
+
if (problem !== null) throw new NotAPngError(path, problem);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A page's header, or the sentence that says why it has none — for a caller
|
|
267
|
+
* whose failures are named rather than thrown.
|
|
268
|
+
*/
|
|
269
|
+
export function readPngHeader(path: string): { info: PngInfo; problem: null } | { info: null; problem: string } {
|
|
270
|
+
const buf = readFileSync(path);
|
|
271
|
+
const problem = pngProblem(buf, path);
|
|
272
|
+
return problem === null ? { info: headerOf(buf), problem: null } : { info: null, problem };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function readPngInfo(path: string): PngInfo {
|
|
276
|
+
const buf = readFileSync(path);
|
|
277
|
+
assertPng(buf, path);
|
|
278
|
+
return headerOf(buf);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The IHDR fields of a file `pngProblem` has already accepted. */
|
|
282
|
+
function headerOf(buf: Buffer): PngInfo {
|
|
283
|
+
const colourType = buf.readUInt8(25);
|
|
284
|
+
const hasAlpha = COLOUR_TYPE_HAS_ALPHA[colourType] ?? false;
|
|
285
|
+
// A file with an alpha channel cannot also carry tRNS, so the scan is skipped
|
|
286
|
+
// for the types that already answered — which is every PNG rigc itself writes.
|
|
287
|
+
const hasTrns = hasAlpha ? false : scanForTrns(buf);
|
|
288
|
+
return {
|
|
289
|
+
width: buf.readUInt32BE(16),
|
|
290
|
+
height: buf.readUInt32BE(20),
|
|
291
|
+
bitDepth: buf.readUInt8(24),
|
|
292
|
+
colourType,
|
|
293
|
+
hasAlpha,
|
|
294
|
+
hasTrns,
|
|
295
|
+
hasTransparency: hasAlpha || hasTrns,
|
|
296
|
+
};
|
|
297
|
+
}
|