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.
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 +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -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
package/tools/plate.ts ADDED
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Minimal RGBA canvas + PNG writer.
3
+ *
4
+ * `src/png.ts` reads 26 bytes of header because that is all the compiler needs;
5
+ * this writes whole files, because a rig has to have something to point at
6
+ * before its art exists — synthetic stand-in plates, reference frames, the
7
+ * selftest's own fixtures. Node's zlib is the only thing it leans on, so the
8
+ * rigc package still has exactly one dependency.
9
+ *
10
+ * Straight (non-premultiplied) alpha, colour type 6. Premultiplied alpha is the
11
+ * single most consequential export setting to get wrong — the renderer does not
12
+ * un-premultiply, so every part gains a dark rim — and a writer that cannot
13
+ * produce it is the cheapest possible guarantee.
14
+ */
15
+ import { deflateSync, inflateSync } from 'node:zlib';
16
+ import { readFileSync, writeFileSync } from 'node:fs';
17
+ import { drawText, textWidth } from './font5x7.ts';
18
+ import { assertPng, NotAPngError } from '../src/png.ts';
19
+
20
+ export type RGBA = [number, number, number, number];
21
+
22
+ export class Plate {
23
+ readonly width: number;
24
+ readonly height: number;
25
+ readonly data: Uint8Array;
26
+
27
+ constructor(width: number, height: number) {
28
+ this.width = width;
29
+ this.height = height;
30
+ this.data = new Uint8Array(width * height * 4);
31
+ }
32
+
33
+ /** Source-over composite of one pixel. Alpha in 0..255. */
34
+ blend(x: number, y: number, [r, g, b, a]: RGBA): void {
35
+ const px = Math.round(x);
36
+ const py = Math.round(y);
37
+ if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
38
+ if (a <= 0) return;
39
+ const i = (py * this.width + px) * 4;
40
+ const sa = a / 255;
41
+ const da = this.data[i + 3] / 255;
42
+ const oa = sa + da * (1 - sa);
43
+ if (oa <= 0) return;
44
+ for (let c = 0; c < 3; c++) {
45
+ const sc = [r, g, b][c];
46
+ this.data[i + c] = Math.round((sc * sa + this.data[i + c] * da * (1 - sa)) / oa);
47
+ }
48
+ this.data[i + 3] = Math.round(oa * 255);
49
+ }
50
+
51
+ /** Overwrite one pixel outright, alpha included. */
52
+ set(x: number, y: number, [r, g, b, a]: RGBA): void {
53
+ const px = Math.round(x);
54
+ const py = Math.round(y);
55
+ if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
56
+ const i = (py * this.width + px) * 4;
57
+ this.data[i] = r;
58
+ this.data[i + 1] = g;
59
+ this.data[i + 2] = b;
60
+ this.data[i + 3] = a;
61
+ }
62
+
63
+ get(x: number, y: number): RGBA {
64
+ const i = (y * this.width + x) * 4;
65
+ return [this.data[i], this.data[i + 1], this.data[i + 2], this.data[i + 3]];
66
+ }
67
+
68
+ /** Multiply the whole plate's alpha by a per-pixel factor in 0..1. */
69
+ maskAlpha(f: (x: number, y: number) => number): void {
70
+ for (let y = 0; y < this.height; y++) {
71
+ for (let x = 0; x < this.width; x++) {
72
+ const i = (y * this.width + x) * 4;
73
+ const k = Math.max(0, Math.min(1, f(x, y)));
74
+ this.data[i + 3] = Math.round(this.data[i + 3] * k);
75
+ }
76
+ }
77
+ }
78
+
79
+ rect(x0: number, y0: number, w: number, h: number, colour: RGBA): void {
80
+ for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) this.blend(x, y, colour);
81
+ }
82
+
83
+ frame(x0: number, y0: number, w: number, h: number, t: number, colour: RGBA): void {
84
+ this.rect(x0, y0, w, t, colour);
85
+ this.rect(x0, y0 + h - t, w, t, colour);
86
+ this.rect(x0, y0, t, h, colour);
87
+ this.rect(x0 + w - t, y0, t, h, colour);
88
+ }
89
+
90
+ line(x0: number, y0: number, x1: number, y1: number, t: number, colour: RGBA): void {
91
+ const steps = Math.ceil(Math.hypot(x1 - x0, y1 - y0)) * 2 + 1;
92
+ for (let s = 0; s <= steps; s++) {
93
+ const u = s / steps;
94
+ const cx = x0 + (x1 - x0) * u;
95
+ const cy = y0 + (y1 - y0) * u;
96
+ for (let dy = -t; dy <= t; dy++) for (let dx = -t; dx <= t; dx++) this.blend(cx + dx, cy + dy, colour);
97
+ }
98
+ }
99
+
100
+ disc(cx: number, cy: number, r: number, colour: RGBA): void {
101
+ for (let y = Math.floor(cy - r); y <= Math.ceil(cy + r); y++) {
102
+ for (let x = Math.floor(cx - r); x <= Math.ceil(cx + r); x++) {
103
+ if (Math.hypot(x - cx, y - cy) <= r) this.blend(x, y, colour);
104
+ }
105
+ }
106
+ }
107
+
108
+ ring(cx: number, cy: number, r: number, t: number, colour: RGBA): void {
109
+ for (let y = Math.floor(cy - r - t); y <= Math.ceil(cy + r + t); y++) {
110
+ for (let x = Math.floor(cx - r - t); x <= Math.ceil(cx + r + t); x++) {
111
+ const d = Math.hypot(x - cx, y - cy);
112
+ if (Math.abs(d - r) <= t) this.blend(x, y, colour);
113
+ }
114
+ }
115
+ }
116
+
117
+ text(str: string, x: number, y: number, scale: number, colour: RGBA): void {
118
+ drawText(str, x, y, scale, (px, py) => this.blend(px, py, colour));
119
+ }
120
+
121
+ textCentred(str: string, cx: number, y: number, scale: number, colour: RGBA): void {
122
+ this.text(str, Math.round(cx - textWidth(str, scale) / 2), y, scale, colour);
123
+ }
124
+
125
+ /** Alpha-weighted pixel count, and how much of it sits outside a predicate. */
126
+ alphaStats(inside: (x: number, y: number) => boolean): {
127
+ total: number;
128
+ outside: number;
129
+ outsideMaxAlpha: number;
130
+ } {
131
+ let total = 0;
132
+ let outside = 0;
133
+ let outsideMaxAlpha = 0;
134
+ for (let y = 0; y < this.height; y++) {
135
+ for (let x = 0; x < this.width; x++) {
136
+ const a = this.data[(y * this.width + x) * 4 + 3];
137
+ if (a === 0) continue;
138
+ total++;
139
+ if (!inside(x, y)) {
140
+ outside++;
141
+ if (a > outsideMaxAlpha) outsideMaxAlpha = a;
142
+ }
143
+ }
144
+ }
145
+ return { total, outside, outsideMaxAlpha };
146
+ }
147
+
148
+ writePng(path: string): void {
149
+ writeFileSync(path, encodePng(this.width, this.height, this.data));
150
+ }
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // PNG encoding
155
+ // ---------------------------------------------------------------------------
156
+
157
+ const CRC_TABLE = (() => {
158
+ const table = new Uint32Array(256);
159
+ for (let n = 0; n < 256; n++) {
160
+ let c = n;
161
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
162
+ table[n] = c >>> 0;
163
+ }
164
+ return table;
165
+ })();
166
+
167
+ function crc32(buf: Uint8Array): number {
168
+ let c = 0xffffffff;
169
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
170
+ return (c ^ 0xffffffff) >>> 0;
171
+ }
172
+
173
+ /** The eight bytes every PNG opens with. */
174
+ export const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
175
+
176
+ /**
177
+ * One PNG chunk: length, type, body, CRC.
178
+ *
179
+ * Exported because this writer only ever emits colour type 6, and the selftest
180
+ * has to produce the colour types it does NOT — indexed and greyscale, with and
181
+ * without a `tRNS` chunk — to have anything for `A19` to judge. Building those by
182
+ * hand needs the CRC table, and a second copy of it in the test would be a second
183
+ * thing that can be wrong.
184
+ */
185
+ export function pngChunk(type: string, body: Uint8Array): Uint8Array {
186
+ const out = new Uint8Array(body.length + 12);
187
+ const view = new DataView(out.buffer);
188
+ view.setUint32(0, body.length);
189
+ for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
190
+ out.set(body, 8);
191
+ view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
192
+ return out;
193
+ }
194
+
195
+ export function encodePng(width: number, height: number, rgba: Uint8Array): Uint8Array {
196
+ // Filter type 0 (none) on every scanline. The plates are flat colour and
197
+ // deterministic output is worth more here than a few kilobytes.
198
+ const raw = new Uint8Array(height * (width * 4 + 1));
199
+ for (let y = 0; y < height; y++) {
200
+ raw[y * (width * 4 + 1)] = 0;
201
+ raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
202
+ }
203
+ const ihdr = new Uint8Array(13);
204
+ const view = new DataView(ihdr.buffer);
205
+ view.setUint32(0, width);
206
+ view.setUint32(4, height);
207
+ ihdr[8] = 8; // bit depth
208
+ ihdr[9] = 6; // colour type: truecolour + alpha
209
+ ihdr[10] = 0;
210
+ ihdr[11] = 0;
211
+ ihdr[12] = 0;
212
+ const parts = [
213
+ PNG_SIGNATURE,
214
+ pngChunk('IHDR', ihdr),
215
+ pngChunk('IDAT', new Uint8Array(deflateSync(raw, { level: 9 }))),
216
+ pngChunk('IEND', new Uint8Array(0)),
217
+ ];
218
+ const total = parts.reduce((n, p) => n + p.length, 0);
219
+ const out = new Uint8Array(total);
220
+ let at = 0;
221
+ for (const p of parts) {
222
+ out.set(p, at);
223
+ at += p.length;
224
+ }
225
+ return out;
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // PNG decoding
230
+ // ---------------------------------------------------------------------------
231
+
232
+ /** Samples per pixel for each PNG colour type. Anything else is not a colour type. */
233
+ const CHANNELS: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
234
+
235
+ /** The bit depths the PNG spec allows for each colour type. */
236
+ const BIT_DEPTHS: Record<number, number[]> = {
237
+ 0: [1, 2, 4, 8, 16],
238
+ 2: [8, 16],
239
+ 3: [1, 2, 4, 8],
240
+ 4: [8, 16],
241
+ 6: [8, 16],
242
+ };
243
+
244
+ /**
245
+ * Read one sample out of an unfiltered scanline, at the file's own bit depth.
246
+ *
247
+ * Returns the RAW value — 0..1 for a 1-bit file, 0..65535 for a 16-bit one — not
248
+ * a scaled byte. Two callers need it that way: a palette index IS the raw value,
249
+ * and a `tRNS` colour is declared at the file's depth and has to be compared
250
+ * against samples at that depth, not against something already rounded to 8 bits.
251
+ */
252
+ function sampleAt(line: Uint8Array, index: number, bitDepth: number): number {
253
+ if (bitDepth === 8) return line[index];
254
+ if (bitDepth === 16) return (line[index * 2] << 8) | line[index * 2 + 1];
255
+ const perByte = 8 / bitDepth;
256
+ const byte = line[Math.floor(index / perByte)];
257
+ // Sub-byte samples are packed most-significant first.
258
+ const shift = 8 - bitDepth * ((index % perByte) + 1);
259
+ return (byte >> shift) & ((1 << bitDepth) - 1);
260
+ }
261
+
262
+ /** A raw sample scaled to 0..255, the range a `Plate` stores. */
263
+ function toByte(sample: number, bitDepth: number): number {
264
+ if (bitDepth === 8) return sample;
265
+ if (bitDepth === 16) return sample >> 8;
266
+ return Math.round((sample * 255) / ((1 << bitDepth) - 1));
267
+ }
268
+
269
+ /** What `decodePng` has once the stream is inflated and before a scanline is unfiltered. */
270
+ interface InflatedPng {
271
+ width: number;
272
+ height: number;
273
+ bitDepth: number;
274
+ colourType: number;
275
+ channels: number;
276
+ pal: Uint8Array;
277
+ alphaTable: Uint8Array | null;
278
+ raw: Uint8Array;
279
+ }
280
+
281
+ /**
282
+ * The half of `decodePng` that can fail: the chunk walk, the header's own
283
+ * checks, the inflate of every `IDAT` concatenated, and whether what came out
284
+ * is as long as the header says the image is (issue #1074). Everything after
285
+ * it — unfiltering, expanding to RGBA — reads bytes that are there and cannot
286
+ * refuse, so `imageDataProblem` runs exactly this and nothing else, and is the
287
+ * decoder's own answer rather than a second opinion beside it.
288
+ */
289
+ function inflatePng(buf: Uint8Array): InflatedPng {
290
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
291
+ let at = 8;
292
+ let width = 0;
293
+ let height = 0;
294
+ let bitDepth = 8;
295
+ let colourType = 6;
296
+ let palette: Uint8Array | null = null;
297
+ let trns: Uint8Array | null = null;
298
+ const idat: Uint8Array[] = [];
299
+ while (at + 8 <= buf.length) {
300
+ const len = view.getUint32(at);
301
+ const type = String.fromCharCode(buf[at + 4], buf[at + 5], buf[at + 6], buf[at + 7]);
302
+ const body = buf.subarray(at + 8, at + 8 + len);
303
+ if (type === 'IHDR') {
304
+ width = view.getUint32(at + 8);
305
+ height = view.getUint32(at + 12);
306
+ bitDepth = body[8];
307
+ colourType = body[9];
308
+ if (body[12] !== 0) throw new Error('interlaced PNG');
309
+ } else if (type === 'PLTE') palette = body.slice();
310
+ else if (type === 'tRNS') trns = body.slice();
311
+ else if (type === 'IDAT') idat.push(body);
312
+ at += 12 + len;
313
+ }
314
+ const channels = CHANNELS[colourType];
315
+ if (channels === undefined) throw new Error(`unsupported colour type ${colourType}`);
316
+ if (!BIT_DEPTHS[colourType].includes(bitDepth)) {
317
+ throw new Error(`bit depth ${bitDepth} is not one PNG allows for colour type ${colourType}`);
318
+ }
319
+ if (colourType === 3 && palette === null) throw new Error('indexed PNG with no PLTE chunk');
320
+ // Const copies so the null checks above narrow inside the per-pixel loop below,
321
+ // where `palette` and `trns` are still the chunk loop's mutable bindings.
322
+ const pal = palette ?? new Uint8Array(0);
323
+ const alphaTable = trns;
324
+
325
+ const raw = new Uint8Array(inflateSync(Buffer.concat(idat.map((c) => Buffer.from(c)))));
326
+ // A stream that inflates clean and short is as undecodable as one that does
327
+ // not inflate: the rows past its end would be read as zeros, a picture the
328
+ // file never held. Longer is accepted — the spec's decoders ignore the excess.
329
+ const needed = height * (Math.ceil((width * channels * bitDepth) / 8) + 1);
330
+ if (raw.length < needed) {
331
+ throw new Error(
332
+ `its image data inflates to ${raw.length} byte(s), where a ${width}x${height} image of colour type ` +
333
+ `${colourType} at bit depth ${bitDepth} needs ${needed}`,
334
+ );
335
+ }
336
+ return { width, height, bitDepth, colourType, channels, pal, alphaTable, raw };
337
+ }
338
+
339
+ /**
340
+ * Read a PNG back into a `Plate`, expanding whatever it is to straight RGBA.
341
+ *
342
+ * `src/png.ts` parses 26 bytes because that is all the COMPILER needs, and it
343
+ * stays that way: the compiler never re-measures art. This
344
+ * decoder is for the measuring tools, which is where art measurement belongs -
345
+ * the same division that puts `mesh.center` in the manifest as a measured number
346
+ * rather than as something the compiler derives at build time.
347
+ *
348
+ * ⭐ **Every colour type the gate accepts is decodable here** (issue #226). It
349
+ * used to read colour types 2 and 6 only, which was the compiler's own old blind
350
+ * spot rebuilt one step later: `A19` learned in #215 that indexed and greyscale
351
+ * art carrying a `tRNS` chunk is ordinary transparent art — ImageMagick, PNG-8
352
+ * export, GIMP indexed, aseprite and pngquant all write it — so such a part
353
+ * builds and validates green, and would then have been refused by the one command
354
+ * whose whole job is to show the author what they built. A wall removed at the
355
+ * gate and rebuilt at the picture is not a wall removed.
356
+ *
357
+ * So the expansion happens here, at decode: a palette index becomes its `PLTE`
358
+ * entry with its `tRNS` alpha, a greyscale sample becomes three equal channels,
359
+ * and a `tRNS` colour on a type 0 or 2 file becomes alpha 0 on the pixels that
360
+ * match it. Every caller above this line keeps seeing exactly one thing, RGBA.
361
+ *
362
+ * Interlaced files are still refused: Adam7 is a second sample layout rather than
363
+ * a second sample format, and no tool in the corpus writes one.
364
+ */
365
+ export function decodePng(buf: Uint8Array): Plate {
366
+ const { width, height, bitDepth, colourType, channels, pal, alphaTable, raw } = inflatePng(buf);
367
+ // The filter operates on BYTES, and its "left neighbour" is one whole pixel
368
+ // back — rounded up to a byte, so sub-byte depths compare against the byte
369
+ // immediately to the left.
370
+ const filterUnit = Math.max(1, Math.ceil((channels * bitDepth) / 8));
371
+ const rowBytes = Math.ceil((width * channels * bitDepth) / 8);
372
+ const stride = rowBytes + 1;
373
+ const plate = new Plate(width, height);
374
+ let prev = new Uint8Array(rowBytes);
375
+ // On a type 0 or 2 file a `tRNS` chunk names ONE invisible colour, one
376
+ // big-endian 16-bit sample per channel whatever the file's own depth. (For an
377
+ // indexed file the same chunk is something else entirely: one alpha byte per
378
+ // palette entry, read directly below.)
379
+ const trnsSample = (i: number): number =>
380
+ alphaTable === null ? -1 : (alphaTable[i * 2] << 8) | alphaTable[i * 2 + 1];
381
+ for (let y = 0; y < height; y++) {
382
+ const filter = raw[y * stride];
383
+ const line = raw.slice(y * stride + 1, (y + 1) * stride);
384
+ for (let x = 0; x < rowBytes; x++) {
385
+ const a = x >= filterUnit ? line[x - filterUnit] : 0;
386
+ const b = prev[x];
387
+ const c = x >= filterUnit ? prev[x - filterUnit] : 0;
388
+ if (filter === 1) line[x] = (line[x] + a) & 255;
389
+ else if (filter === 2) line[x] = (line[x] + b) & 255;
390
+ else if (filter === 3) line[x] = (line[x] + ((a + b) >> 1)) & 255;
391
+ else if (filter === 4) {
392
+ const p = a + b - c;
393
+ const pa = Math.abs(p - a);
394
+ const pb = Math.abs(p - b);
395
+ const pc = Math.abs(p - c);
396
+ line[x] = (line[x] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 255;
397
+ }
398
+ }
399
+ for (let x = 0; x < width; x++) {
400
+ const o = (y * width + x) * 4;
401
+ const first = x * channels;
402
+ if (colourType === 3) {
403
+ const index = sampleAt(line, first, bitDepth);
404
+ plate.data[o] = pal[index * 3];
405
+ plate.data[o + 1] = pal[index * 3 + 1];
406
+ plate.data[o + 2] = pal[index * 3 + 2];
407
+ // A tRNS shorter than the palette leaves the entries past its end opaque,
408
+ // which is what the spec says and what pngquant relies on.
409
+ plate.data[o + 3] = alphaTable !== null && index < alphaTable.length ? alphaTable[index] : 255;
410
+ continue;
411
+ }
412
+ if (colourType === 0 || colourType === 4) {
413
+ const grey = sampleAt(line, first, bitDepth);
414
+ const g = toByte(grey, bitDepth);
415
+ plate.data[o] = g;
416
+ plate.data[o + 1] = g;
417
+ plate.data[o + 2] = g;
418
+ plate.data[o + 3] =
419
+ colourType === 4
420
+ ? toByte(sampleAt(line, first + 1, bitDepth), bitDepth)
421
+ : alphaTable !== null && alphaTable.length >= 2 && trnsSample(0) === grey
422
+ ? 0
423
+ : 255;
424
+ continue;
425
+ }
426
+ const r = sampleAt(line, first, bitDepth);
427
+ const g = sampleAt(line, first + 1, bitDepth);
428
+ const b = sampleAt(line, first + 2, bitDepth);
429
+ plate.data[o] = toByte(r, bitDepth);
430
+ plate.data[o + 1] = toByte(g, bitDepth);
431
+ plate.data[o + 2] = toByte(b, bitDepth);
432
+ plate.data[o + 3] =
433
+ colourType === 6
434
+ ? toByte(sampleAt(line, first + 3, bitDepth), bitDepth)
435
+ : alphaTable !== null &&
436
+ alphaTable.length >= 6 &&
437
+ trnsSample(0) === r &&
438
+ trnsSample(1) === g &&
439
+ trnsSample(2) === b
440
+ ? 0
441
+ : 255;
442
+ }
443
+ prev = line;
444
+ }
445
+ return plate;
446
+ }
447
+
448
+ /**
449
+ * Decode the PNG at `path`, naming the file if it cannot be decoded.
450
+ *
451
+ * The bare message says what is wrong with the bytes and nothing about which
452
+ * bytes; a rig has as many pages as it has parts, and "unsupported colour type 5"
453
+ * with no path is a search rather than a fix.
454
+ */
455
+ export function readPlate(path: string): Plate {
456
+ // Only the DECODE is wrapped. A missing file already says which one, and
457
+ // rewording an ENOENT as a decoding problem would mislabel the commonest
458
+ // failure of all.
459
+ const buf = readFileSync(path);
460
+ // What the file IS comes first, and from the one reader that says so (issue
461
+ // #732): `decodePng` walks chunks from byte 8 whatever the first eight were,
462
+ // so a WebP page reached it and came back as "unexpected end of file" — an
463
+ // inflate error about a file that was never a PNG.
464
+ assertPng(buf, path);
465
+ try {
466
+ return decodePng(buf);
467
+ } catch (err) {
468
+ // The decoder's refusal as the same sentence the gate prints (issue
469
+ // #1074), and as the class every command already refuses by name: it was
470
+ // a bare Error, which `render` and `build` (through a mesh part's trace)
471
+ // let through as a stack.
472
+ throw new NotAPngError(path, undecodableSentence(path, (err as Error).message));
473
+ }
474
+ }
475
+
476
+ /** What a PNG whose header reads and whose image data does not decode is, as one sentence. */
477
+ function undecodableSentence(path: string, decoder: string): string {
478
+ return (
479
+ `${path} is a PNG whose header and chunks read and whose image data cannot be decoded: the decoder stops ` +
480
+ `with "${decoder}", so no texel of it can be read. Re-export the file, or re-copy it whole`
481
+ );
482
+ }
483
+
484
+ /**
485
+ * Why the image data of a PNG that `pngProblem` accepts cannot be decoded, as
486
+ * one sentence — or `null` when it can (issue #1074).
487
+ *
488
+ * 🔒 **It is the decoder, not a check beside it**: it runs `inflatePng`, the
489
+ * half of `decodePng` that can refuse, and reports that refusal in the
490
+ * decoder's own words. `pngProblem` reads chunk headers and never inflates, so
491
+ * a page with an intact chunk walk over a broken compressed stream passed it,
492
+ * passed `A06`, and was found only by whichever reader decoded first — `A19`
493
+ * as `threw: cannot decode PNG …`, `render` and a mesh part's trace as a stack,
494
+ * and on a page `A19` does not scan (the base plate, or any page under the
495
+ * default profile) by nobody: such a build was written green.
496
+ *
497
+ * ⚠️ Inflate only, deliberately: measured over the 19 recipes' pages it is
498
+ * 68.9 ms against `validate()`'s 286.2 ms under `spine`, where a whole decode
499
+ * (`decodePng`, which unfilters and expands every texel) is 459.6 ms.
500
+ */
501
+ export function imageDataProblem(buf: Uint8Array, path: string): string | null {
502
+ try {
503
+ inflatePng(buf);
504
+ return null;
505
+ } catch (err) {
506
+ return undecodableSentence(path, (err as Error).message);
507
+ }
508
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * PNG reading for pixel probes.
3
+ *
4
+ * A browser screenshot comes back as a PNG buffer and a pixel assertion has to
5
+ * look at pixels, so 8-bit RGB/RGBA with the five standard filters is all that
6
+ * can come out of one. This started life inside a single probe; a copied decoder
7
+ * is the shape every drift in this project has had, so it lives here instead of
8
+ * being duplicated into the second one.
9
+ *
10
+ * `src/png.ts` reads 26 bytes (the compiler only needs the header) and
11
+ * `tools/plate.ts` decodes into a writable canvas (the plate tools need to draw).
12
+ * This one decodes into a flat buffer for comparison and nothing else.
13
+ */
14
+ import { inflateSync } from 'node:zlib';
15
+
16
+ export function decodePng(buf) {
17
+ let at = 8;
18
+ let width = 0;
19
+ let height = 0;
20
+ let colourType = 6;
21
+ const idat = [];
22
+ while (at < buf.length) {
23
+ const len = buf.readUInt32BE(at);
24
+ const type = buf.toString('latin1', at + 4, at + 8);
25
+ const body = buf.subarray(at + 8, at + 8 + len);
26
+ if (type === 'IHDR') {
27
+ width = body.readUInt32BE(0);
28
+ height = body.readUInt32BE(4);
29
+ if (body[8] !== 8) throw new Error(`unsupported bit depth ${body[8]}`);
30
+ colourType = body[9];
31
+ if (body[12] !== 0) throw new Error('interlaced PNG');
32
+ } else if (type === 'IDAT') idat.push(body);
33
+ at += 12 + len;
34
+ }
35
+ const bpp = colourType === 6 ? 4 : colourType === 2 ? 3 : 0;
36
+ if (!bpp) throw new Error(`unsupported colour type ${colourType}`);
37
+ const raw = inflateSync(Buffer.concat(idat));
38
+ const stride = width * bpp + 1;
39
+ const out = Buffer.alloc(width * height * bpp);
40
+ let prev = Buffer.alloc(width * bpp);
41
+ for (let y = 0; y < height; y++) {
42
+ const filter = raw[y * stride];
43
+ const line = Buffer.from(raw.subarray(y * stride + 1, (y + 1) * stride));
44
+ for (let x = 0; x < width * bpp; x++) {
45
+ const a = x >= bpp ? line[x - bpp] : 0;
46
+ const b = prev[x];
47
+ const c = x >= bpp ? prev[x - bpp] : 0;
48
+ if (filter === 1) line[x] = (line[x] + a) & 255;
49
+ else if (filter === 2) line[x] = (line[x] + b) & 255;
50
+ else if (filter === 3) line[x] = (line[x] + ((a + b) >> 1)) & 255;
51
+ else if (filter === 4) {
52
+ const p = a + b - c;
53
+ const pa = Math.abs(p - a);
54
+ const pb = Math.abs(p - b);
55
+ const pc = Math.abs(p - c);
56
+ line[x] = (line[x] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 255;
57
+ }
58
+ }
59
+ line.copy(out, y * width * bpp);
60
+ prev = line;
61
+ }
62
+ return { width, height, bpp, data: out };
63
+ }
64
+
65
+ /** Manhattan distance between two images at one pixel. 8 LSB of slack. */
66
+ export function differs(a, b, x, y) {
67
+ if (x < 0 || y < 0 || x >= a.width || y >= a.height) return false;
68
+ const i = (y * a.width + x) * a.bpp;
69
+ let d = 0;
70
+ for (let c = 0; c < a.bpp; c++) d += Math.abs(a.data[i + c] - b.data[i + c]);
71
+ return d > 8;
72
+ }