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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1191 -0
  185. package/src/meshquality.ts +2051 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1444 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -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
+ }