makeables 0.9.0 → 0.11.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/dist/cli.js CHANGED
@@ -1,77 +1,1558 @@
1
1
  #!/usr/bin/env node
2
+ import "./chunk-BYXBJQAS.js";
3
+ import {
4
+ addBrandKit,
5
+ addBrandLogo,
6
+ brandFromUrl,
7
+ brandView,
8
+ checkKitAgainstBrand,
9
+ newBrand,
10
+ prepareBrand,
11
+ readBrand,
12
+ writeBrand
13
+ } from "./chunk-ZZA6CR6X.js";
14
+ import {
15
+ addKitMember,
16
+ addToSheet,
17
+ checkSheet,
18
+ exportKit,
19
+ exportSheetPdf,
20
+ importKit,
21
+ kitCover,
22
+ newSheet,
23
+ packStickerSheet,
24
+ readKit,
25
+ readSheet,
26
+ sheetPreviewPng,
27
+ sheetReport
28
+ } from "./chunk-6RBOBRL4.js";
29
+ import {
30
+ getSkill,
31
+ skills,
32
+ version
33
+ } from "./chunk-4WLCMPRH.js";
2
34
  import {
3
35
  CloudError,
4
36
  InputError,
5
- PACKAGE_LIMIT,
6
37
  addBadgeSticker,
7
- badgeDesignObjectSchema,
8
- badgeDesignSchema,
9
38
  cardStyles,
10
- checkSubmittedSvg,
11
39
  connectAccount,
12
- createStickerSheet,
13
- defaultStickerMaterial,
40
+ createStickerDesign,
14
41
  designCatalog,
15
- designProfile,
16
- draftFromPackage,
17
42
  findCardStyle,
18
43
  findDesign,
19
- getSkill,
20
- hashBytes,
21
- jsonBytes,
22
44
  listCardDesigns,
23
45
  logout,
24
46
  newBadge,
25
47
  newCard,
26
48
  openBrowser,
27
- packSheet,
28
- packageJson,
29
- placeNear,
30
- prepareSheetPrint,
31
49
  readBadge,
32
- readPackageJson,
33
- readPackageZip,
34
50
  readSubmission,
35
51
  removeBadgeLayer,
36
52
  renderBadge,
53
+ renderDraftFace,
37
54
  setBadgeLayer,
38
- sheetPrintPdf,
39
- sheetSizes,
40
- sheetSpec,
41
- sheetTypeIds,
42
- sheetTypes,
43
55
  side,
44
- skills,
45
56
  startStudio,
46
57
  stickerCut,
47
58
  stickerCutSvg,
48
- stickerMaterialSchema,
49
- stickerSheetSchema,
50
59
  stickerStyles,
51
60
  studioRequest,
52
61
  submitDesign,
53
62
  summarizeBadgeLayers,
54
- svgNodeSchema,
55
- svgNodeXml,
56
- version,
57
63
  writeAtomic
58
- } from "./chunk-RFIQNWNF.js";
64
+ } from "./chunk-YB2YJCBS.js";
65
+ import {
66
+ PACKAGE_LIMIT,
67
+ badgeDesignObjectSchema,
68
+ badgeDesignSchema,
69
+ checkSubmittedSvg,
70
+ defaultStickerMaterial,
71
+ designProfile,
72
+ draftFromPackage,
73
+ hashBytes,
74
+ jsonBytes,
75
+ kitDocumentSchema,
76
+ packageJson,
77
+ readPackageJson,
78
+ readPackageZip,
79
+ stickerMaterialSchema,
80
+ svgNodeSchema,
81
+ svgNodeXml
82
+ } from "./chunk-DLTVENEP.js";
59
83
 
60
84
  // src/cli.ts
61
- import { spawnSync as spawnSync2 } from "child_process";
62
- import { constants as constants3 } from "fs";
63
- import { access, lstat as lstat4, open as open3, readFile as readFile8 } from "fs/promises";
64
- import { dirname as dirname4, extname as extname4, resolve as resolve7 } from "path";
85
+ import { spawnSync as spawnSync3 } from "child_process";
86
+ import { randomUUID as randomUUID3 } from "crypto";
87
+ import { constants as constants4 } from "fs";
88
+ import { access, lstat as lstat4, open as open4, readFile as readFile8 } from "fs/promises";
89
+ import { dirname as dirname5, extname as extname4, resolve as resolve8 } from "path";
65
90
  import { parseArgs } from "util";
66
- import sharp8 from "sharp";
91
+ import sharp9 from "sharp";
92
+ import { ZodError as ZodError2 } from "zod";
67
93
  import { zodToJsonSchema } from "zod-to-json-schema";
68
94
 
95
+ // src/brick.ts
96
+ import { existsSync, readFileSync } from "fs";
97
+ import { readFile } from "fs/promises";
98
+ import { join, resolve } from "path";
99
+
100
+ // ../bricks/dist/index.js
101
+ var GRID_MM = 42;
102
+ var GRIDFINITY_UNIT_MM = 10;
103
+ var CELL_UNITS = GRID_MM / GRIDFINITY_UNIT_MM;
104
+ var HEIGHT_UNIT_MM = 7;
105
+ var BASE_PROFILE = [
106
+ [2.95, 0],
107
+ [2.15, 0.8],
108
+ [2.15, 2.6],
109
+ [0, 4.75]
110
+ ];
111
+ var BASE_TOP_MM = 41.5;
112
+ var BASE_TOP_RADIUS_MM = 3.75;
113
+ var GAP_MM = 0.25;
114
+ var WALL_MM = 0.95;
115
+ var FUSE_MM = 0.5;
116
+ var CORNER_SEGMENTS = 6;
117
+ function rounded(cx, cz, w, d, r) {
118
+ const out = [];
119
+ const corners = [
120
+ [cx + w / 2 - r, cz + d / 2 - r, 0],
121
+ [cx - w / 2 + r, cz + d / 2 - r, 90],
122
+ [cx - w / 2 + r, cz - d / 2 + r, 180],
123
+ [cx + w / 2 - r, cz - d / 2 + r, 270]
124
+ ];
125
+ for (const [x, z5, start] of corners)
126
+ for (let k = 0; k <= CORNER_SEGMENTS; k++) {
127
+ const t = (start + 90 * k / CORNER_SEGMENTS) * Math.PI / 180;
128
+ out.push([x + r * Math.cos(t), z5 + r * Math.sin(t)]);
129
+ }
130
+ return out;
131
+ }
132
+ function squareFor(ring, cx, cz, half) {
133
+ return ring.map(([x, z5]) => {
134
+ const dx = x - cx;
135
+ const dz = z5 - cz;
136
+ const s = half / Math.max(Math.abs(dx), Math.abs(dz));
137
+ return [cx + dx * s, cz + dz * s];
138
+ });
139
+ }
140
+ var Mesh = class {
141
+ triangles = [];
142
+ /**
143
+ * Counter-clockwise seen from outside. Zero-area triangles along the knife
144
+ * edge between baseplate cells are kept: they stitch the seam closed.
145
+ */
146
+ tri(a, b, c) {
147
+ this.triangles.push(...a, ...b, ...c);
148
+ }
149
+ /** Side wall between two rings; `outward` faces away from the ring centre. */
150
+ wall(lower, yl, upper, yu, outward, keep = () => true) {
151
+ for (let i = 0; i < lower.length; i++) {
152
+ if (!keep(i)) continue;
153
+ const j = (i + 1) % lower.length;
154
+ const a = [lower[i][0], yl, lower[i][1]];
155
+ const b = [lower[j][0], yl, lower[j][1]];
156
+ const c = [upper[j][0], yu, upper[j][1]];
157
+ const d = [upper[i][0], yu, upper[i][1]];
158
+ if (outward) {
159
+ this.tri(a, c, b);
160
+ this.tri(a, d, c);
161
+ } else {
162
+ this.tri(a, b, c);
163
+ this.tri(a, c, d);
164
+ }
165
+ }
166
+ }
167
+ /** Flat convex cap; `up` faces +Y. */
168
+ cap(ring, y, up) {
169
+ const [cx, cz] = ring.reduce(
170
+ ([sx, sz], [x, z5]) => [sx + x / ring.length, sz + z5 / ring.length],
171
+ [0, 0]
172
+ );
173
+ for (let i = 0; i < ring.length; i++) {
174
+ const j = (i + 1) % ring.length;
175
+ const c = [cx, y, cz];
176
+ const a = [ring[i][0], y, ring[i][1]];
177
+ const b = [ring[j][0], y, ring[j][1]];
178
+ if (up) this.tri(c, b, a);
179
+ else this.tri(c, a, b);
180
+ }
181
+ }
182
+ /** Flat annulus between two stitched rings; `up` faces +Y. */
183
+ annulus(outer, inner, y, up) {
184
+ for (let i = 0; i < outer.length; i++) {
185
+ const j = (i + 1) % outer.length;
186
+ const a = [outer[i][0], y, outer[i][1]];
187
+ const b = [outer[j][0], y, outer[j][1]];
188
+ const c = [inner[j][0], y, inner[j][1]];
189
+ const d = [inner[i][0], y, inner[i][1]];
190
+ if (up) {
191
+ this.tri(a, c, b);
192
+ this.tri(a, d, c);
193
+ } else {
194
+ this.tri(a, b, c);
195
+ this.tri(a, c, d);
196
+ }
197
+ }
198
+ }
199
+ };
200
+ var profileRing = (cx, cz, size, radius, inset) => rounded(cx, cz, size - 2 * inset, size - 2 * inset, Math.max(0.01, radius - inset));
201
+ var toUnits = (mm) => mm.map((v) => v / GRIDFINITY_UNIT_MM);
202
+ function part(id, name, mesh, connectors, height, w, d) {
203
+ return {
204
+ id,
205
+ name,
206
+ connectors,
207
+ triangles: toUnits(mesh.triangles),
208
+ colors: new Array(mesh.triangles.length / 9).fill(16),
209
+ body: {
210
+ min: [-w / 2 * CELL_UNITS, 0, -d / 2 * CELL_UNITS],
211
+ max: [w / 2 * CELL_UNITS, height / GRIDFINITY_UNIT_MM, d / 2 * CELL_UNITS]
212
+ }
213
+ };
214
+ }
215
+ var cellCentres = (w, d) => Array.from({ length: w * d }, (_, n) => [n % w - (w - 1) / 2, Math.floor(n / w) - (d - 1) / 2]);
216
+ function gridfinityBin(w, d, h) {
217
+ if (![w, d, h].every((n) => Number.isInteger(n) && n >= 1))
218
+ throw new Error("Bin size must be whole cells and height units, at least 1.");
219
+ const mesh = new Mesh();
220
+ const top = h * HEIGHT_UNIT_MM;
221
+ for (const [ux, uz] of cellCentres(w, d)) {
222
+ const [cx, cz] = [ux * GRID_MM, uz * GRID_MM];
223
+ const rings = BASE_PROFILE.map(
224
+ ([inset]) => profileRing(cx, cz, BASE_TOP_MM, BASE_TOP_RADIUS_MM, inset)
225
+ );
226
+ mesh.cap(rings[0], 0, false);
227
+ for (let k = 0; k + 1 < rings.length; k++)
228
+ mesh.wall(rings[k], BASE_PROFILE[k][1], rings[k + 1], BASE_PROFILE[k + 1][1], true);
229
+ const crown = BASE_PROFILE[BASE_PROFILE.length - 1][1];
230
+ const last = rings[rings.length - 1];
231
+ mesh.wall(last, crown, last, crown + FUSE_MM, true);
232
+ mesh.cap(last, crown + FUSE_MM, true);
233
+ }
234
+ const outerW = w * GRID_MM - 2 * GAP_MM;
235
+ const outerD = d * GRID_MM - 2 * GAP_MM;
236
+ const outer = rounded(0, 0, outerW, outerD, BASE_TOP_RADIUS_MM);
237
+ const inner = rounded(
238
+ 0,
239
+ 0,
240
+ outerW - 2 * WALL_MM,
241
+ outerD - 2 * WALL_MM,
242
+ BASE_TOP_RADIUS_MM - WALL_MM
243
+ );
244
+ const base = BASE_PROFILE[BASE_PROFILE.length - 1][1];
245
+ mesh.cap(outer, base, false);
246
+ mesh.wall(outer, base, outer, top, true);
247
+ mesh.annulus(outer, inner, top, true);
248
+ mesh.wall(inner, HEIGHT_UNIT_MM, inner, top, false);
249
+ mesh.cap(inner, HEIGHT_UNIT_MM, true);
250
+ const feet = cellCentres(w, d).map(
251
+ ([x, z5]) => ({
252
+ kind: "foot",
253
+ p: [x * CELL_UNITS, 0, z5 * CELL_UNITS],
254
+ a: [0, -1, 0]
255
+ })
256
+ );
257
+ return part(`bin-${w}x${d}x${h}`, `Gridfinity bin ${w} x ${d} x ${h}`, mesh, feet, top, w, d);
258
+ }
259
+ function gridfinityBaseplate(w, d) {
260
+ if (![w, d].every((n) => Number.isInteger(n) && n >= 1))
261
+ throw new Error("Baseplate size must be whole cells, at least 1.");
262
+ const mesh = new Mesh();
263
+ const height = BASE_PROFILE[BASE_PROFILE.length - 1][1];
264
+ for (const [ux, uz] of cellCentres(w, d)) {
265
+ const [cx, cz] = [ux * GRID_MM, uz * GRID_MM];
266
+ const pocket = BASE_PROFILE.map(
267
+ ([inset]) => profileRing(cx, cz, BASE_TOP_MM + 2 * GAP_MM, BASE_TOP_RADIUS_MM + GAP_MM, inset)
268
+ );
269
+ const square = squareFor(pocket[0], cx, cz, GRID_MM / 2);
270
+ const edge = (i) => {
271
+ const j = (i + 1) % square.length;
272
+ const mx = (square[i][0] + square[j][0]) / 2;
273
+ const mz = (square[i][1] + square[j][1]) / 2;
274
+ return Math.abs(Math.abs(mx) - w * GRID_MM / 2) < 1e-6 || Math.abs(Math.abs(mz) - d * GRID_MM / 2) < 1e-6;
275
+ };
276
+ mesh.wall(square, 0, square, height, true, edge);
277
+ mesh.annulus(square, pocket[pocket.length - 1], height, true);
278
+ mesh.annulus(square, pocket[0], 0, false);
279
+ for (let k = 0; k + 1 < pocket.length; k++)
280
+ mesh.wall(pocket[k], BASE_PROFILE[k][1], pocket[k + 1], BASE_PROFILE[k + 1][1], false);
281
+ }
282
+ const pockets = cellCentres(w, d).map(
283
+ ([x, z5]) => ({
284
+ kind: "pocket",
285
+ p: [x * CELL_UNITS, 0, z5 * CELL_UNITS],
286
+ a: [0, 1, 0]
287
+ })
288
+ );
289
+ return part(
290
+ `baseplate-${w}x${d}`,
291
+ `Gridfinity baseplate ${w} x ${d}`,
292
+ mesh,
293
+ pockets,
294
+ height,
295
+ w,
296
+ d
297
+ );
298
+ }
299
+ var IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1];
300
+ var add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
301
+ var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
302
+ var scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
303
+ var dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
304
+ var cross = (a, b) => [
305
+ a[1] * b[2] - a[2] * b[1],
306
+ a[2] * b[0] - a[0] * b[2],
307
+ a[0] * b[1] - a[1] * b[0]
308
+ ];
309
+ var length = (a) => Math.hypot(a[0], a[1], a[2]);
310
+ function apply(m, v) {
311
+ return [
312
+ m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
313
+ m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
314
+ m[6] * v[0] + m[7] * v[1] + m[8] * v[2]
315
+ ];
316
+ }
317
+ function mul(a, b) {
318
+ const r = new Array(9).fill(0);
319
+ for (let i = 0; i < 3; i++)
320
+ for (let j = 0; j < 3; j++)
321
+ for (let k = 0; k < 3; k++) r[i * 3 + j] += a[i * 3 + k] * b[k * 3 + j];
322
+ return r;
323
+ }
324
+ var transpose = (m) => [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]];
325
+ var AXIS_ROTATIONS = (() => {
326
+ const axes = [
327
+ [1, 0, 0],
328
+ [-1, 0, 0],
329
+ [0, 1, 0],
330
+ [0, -1, 0],
331
+ [0, 0, 1],
332
+ [0, 0, -1]
333
+ ];
334
+ const out = [];
335
+ for (const x of axes)
336
+ for (const y of axes) {
337
+ if (dot(x, y) !== 0) continue;
338
+ const z5 = cross(x, y);
339
+ out.push([x[0], y[0], z5[0], x[1], y[1], z5[1], x[2], y[2], z5[2]]);
340
+ }
341
+ return out;
342
+ })();
343
+ var CELL = 0.1;
344
+ var index = (v, i, j, k) => i < 0 || j < 0 || k < 0 || i >= v.dims[0] || j >= v.dims[1] || k >= v.dims[2] ? -1 : (k * v.dims[1] + j) * v.dims[0] + i;
345
+ function cellAt(v, p, field = "solid") {
346
+ const n = index(
347
+ v,
348
+ Math.floor((p[0] - v.origin[0]) / CELL),
349
+ Math.floor((p[1] - v.origin[1]) / CELL),
350
+ Math.floor((p[2] - v.origin[2]) / CELL)
351
+ );
352
+ return n >= 0 && v[field][n] === 1;
353
+ }
354
+ function voxelize(triangles, min, max) {
355
+ const origin = [min[0] - CELL, min[1] - CELL, min[2] - CELL];
356
+ const dims = [0, 1, 2].map(
357
+ (k) => Math.max(1, Math.ceil((max[k] - origin[k]) / CELL) + 1)
358
+ );
359
+ const total = dims[0] * dims[1] * dims[2];
360
+ const votes = new Uint8Array(total);
361
+ const v = { origin, dims, solid: new Uint8Array(total), interior: new Uint8Array(total) };
362
+ for (let axis = 0; axis < 3; axis++) {
363
+ const u = (axis + 1) % 3;
364
+ const w = (axis + 2) % 3;
365
+ const hits = /* @__PURE__ */ new Map();
366
+ for (let t = 0; t < triangles.length; t += 9) {
367
+ const a = [triangles[t], triangles[t + 1], triangles[t + 2]];
368
+ const b = [triangles[t + 3], triangles[t + 4], triangles[t + 5]];
369
+ const c = [triangles[t + 6], triangles[t + 7], triangles[t + 8]];
370
+ const area = (b[u] - a[u]) * (c[w] - a[w]) - (c[u] - a[u]) * (b[w] - a[w]);
371
+ if (Math.abs(area) < 1e-12) continue;
372
+ const lo = (k) => Math.min(a[k], b[k], c[k]);
373
+ const hi = (k) => Math.max(a[k], b[k], c[k]);
374
+ const iu0 = Math.max(0, Math.floor((lo(u) - origin[u]) / CELL - 0.5));
375
+ const iu1 = Math.min(dims[u] - 1, Math.ceil((hi(u) - origin[u]) / CELL - 0.5));
376
+ const iw0 = Math.max(0, Math.floor((lo(w) - origin[w]) / CELL - 0.5));
377
+ const iw1 = Math.min(dims[w] - 1, Math.ceil((hi(w) - origin[w]) / CELL - 0.5));
378
+ for (let iu = iu0; iu <= iu1; iu++)
379
+ for (let iw = iw0; iw <= iw1; iw++) {
380
+ const pu = origin[u] + (iu + 0.5) * CELL + 13e-6;
381
+ const pw = origin[w] + (iw + 0.5) * CELL + 29e-6;
382
+ const l0 = ((b[u] - pu) * (c[w] - pw) - (c[u] - pu) * (b[w] - pw)) / area;
383
+ const l1 = ((c[u] - pu) * (a[w] - pw) - (a[u] - pu) * (c[w] - pw)) / area;
384
+ const l2 = 1 - l0 - l1;
385
+ if (l0 < 0 || l1 < 0 || l2 < 0) continue;
386
+ const key2 = iu * dims[w] + iw;
387
+ const list = hits.get(key2) ?? [];
388
+ list.push(l0 * a[axis] + l1 * b[axis] + l2 * c[axis]);
389
+ hits.set(key2, list);
390
+ }
391
+ }
392
+ for (const [key2, list] of hits) {
393
+ list.sort((x, y) => x - y);
394
+ const iu = Math.floor(key2 / dims[w]);
395
+ const iw = key2 % dims[w];
396
+ const ijk = [0, 0, 0];
397
+ ijk[u] = iu;
398
+ ijk[w] = iw;
399
+ let before = 0;
400
+ for (let ia = 0; ia < dims[axis]; ia++) {
401
+ const c = origin[axis] + (ia + 0.5) * CELL;
402
+ while (before < list.length && list[before] < c) before++;
403
+ ijk[axis] = ia;
404
+ const n = index(v, ijk[0], ijk[1], ijk[2]);
405
+ votes[n] += before % 2 + (list.length - before) % 2;
406
+ }
407
+ }
408
+ }
409
+ for (let n = 0; n < total; n++) v.solid[n] = votes[n] >= 4 ? 1 : 0;
410
+ for (let k = 0; k < dims[2]; k++)
411
+ for (let j = 0; j < dims[1]; j++)
412
+ for (let i = 0; i < dims[0]; i++) {
413
+ const n = index(v, i, j, k);
414
+ if (!v.solid[n]) continue;
415
+ const around = [
416
+ [1, 0, 0],
417
+ [-1, 0, 0],
418
+ [0, 1, 0],
419
+ [0, -1, 0],
420
+ [0, 0, 1],
421
+ [0, 0, -1]
422
+ ].every(([di, dj, dk]) => v.solid[index(v, i + di, j + dj, k + dk)] === 1);
423
+ v.interior[n] = around ? 1 : 0;
424
+ }
425
+ return v;
426
+ }
427
+ function partOf(lib, item) {
428
+ const part2 = lib[item.part];
429
+ if (!part2) throw new Error(`Unknown part: ${item.part}`);
430
+ return part2;
431
+ }
432
+ function worldConnectors(lib, item) {
433
+ return partOf(lib, item).connectors.map((c) => ({
434
+ kind: c.kind,
435
+ p: add(apply(item.rotation, c.p), item.position),
436
+ a: apply(item.rotation, c.a),
437
+ len: c.len
438
+ }));
439
+ }
440
+ function worldBody(lib, item) {
441
+ const { min, max } = partOf(lib, item).body;
442
+ const lo = [Infinity, Infinity, Infinity];
443
+ const hi = [-Infinity, -Infinity, -Infinity];
444
+ for (const x of [min[0], max[0]])
445
+ for (const y of [min[1], max[1]])
446
+ for (const z5 of [min[2], max[2]]) {
447
+ const p = add(apply(item.rotation, [x, y, z5]), item.position);
448
+ for (let k = 0; k < 3; k++) {
449
+ lo[k] = Math.min(lo[k], p[k]);
450
+ hi[k] = Math.max(hi[k], p[k]);
451
+ }
452
+ }
453
+ return { min: lo, max: hi };
454
+ }
455
+ function pointMates(x, y, adapter) {
456
+ return length(sub(x.p, y.p)) <= adapter.tolerance && dot(x.a, y.a) <= -0.99;
457
+ }
458
+ function axialMates(x, y, adapter) {
459
+ if (length(cross(x.a, y.a)) > 0.01) return false;
460
+ const d = sub(y.p, x.p);
461
+ const along = dot(d, x.a);
462
+ if (length(sub(d, scale(x.a, along))) > adapter.tolerance) return false;
463
+ const hx = (x.len ?? 0) / 2;
464
+ const hy = (y.len ?? 0) / 2;
465
+ return Math.min(hx, along + hy) - Math.max(-hx, along - hy) >= adapter.minEngagement;
466
+ }
467
+ function contacts(model, lib, adapter) {
468
+ const world = model.items.map((it) => worldConnectors(lib, it));
469
+ const out = [];
470
+ for (let a = 0; a < world.length; a++)
471
+ for (let b = a + 1; b < world.length; b++) {
472
+ const kinds = /* @__PURE__ */ new Map();
473
+ for (const x of world[a])
474
+ for (const y of world[b]) {
475
+ if (!adapter.fits[x.kind]?.includes(y.kind)) continue;
476
+ const axial = x.len !== void 0 && y.len !== void 0;
477
+ if (axial ? !axialMates(x, y, adapter) : !pointMates(x, y, adapter)) continue;
478
+ const kind = [x.kind, y.kind].sort().join("-");
479
+ kinds.set(kind, {
480
+ count: (kinds.get(kind)?.count ?? 0) + 1,
481
+ axis: axial ? x.a : void 0,
482
+ release: axial ? void 0 : x.a
483
+ });
484
+ }
485
+ for (const [kind, v] of kinds) out.push({ a, b, kind, ...v });
486
+ }
487
+ return out;
488
+ }
489
+ function overlapVolume(x, y, epsilon = 0.02) {
490
+ let v = 1;
491
+ for (let k = 0; k < 3; k++) {
492
+ const d = Math.min(x.max[k], y.max[k]) - Math.max(x.min[k], y.min[k]) - 2 * epsilon;
493
+ if (d <= 0) return 0;
494
+ v *= d;
495
+ }
496
+ return v;
497
+ }
498
+ function meshBounds(triangles) {
499
+ const min = [Infinity, Infinity, Infinity];
500
+ const max = [-Infinity, -Infinity, -Infinity];
501
+ for (let i = 0; i < triangles.length; i++) {
502
+ min[i % 3] = Math.min(min[i % 3], triangles[i]);
503
+ max[i % 3] = Math.max(max[i % 3], triangles[i]);
504
+ }
505
+ return { min, max };
506
+ }
507
+ function partVoxels(part2) {
508
+ if (!part2.voxels) {
509
+ const { min, max } = meshBounds(part2.triangles);
510
+ part2.voxels = voxelize(part2.triangles, min, max);
511
+ }
512
+ return part2.voxels;
513
+ }
514
+ function collisionCells(lib, x, y) {
515
+ let hits = 0;
516
+ for (const [a, b] of [
517
+ [x, y],
518
+ [y, x]
519
+ ]) {
520
+ const va = partVoxels(partOf(lib, a));
521
+ const vb = partVoxels(partOf(lib, b));
522
+ for (let k = 0; k < va.dims[2]; k++)
523
+ for (let j = 0; j < va.dims[1]; j++)
524
+ for (let i = 0; i < va.dims[0]; i++) {
525
+ if (!va.interior[(k * va.dims[1] + j) * va.dims[0] + i]) continue;
526
+ const local = [
527
+ va.origin[0] + (i + 0.5) * CELL,
528
+ va.origin[1] + (j + 0.5) * CELL,
529
+ va.origin[2] + (k + 0.5) * CELL
530
+ ];
531
+ const world = add(apply(a.rotation, local), a.position);
532
+ const r = b.rotation;
533
+ const d = sub(world, b.position);
534
+ const inB = [
535
+ r[0] * d[0] + r[3] * d[1] + r[6] * d[2],
536
+ r[1] * d[0] + r[4] * d[1] + r[7] * d[2],
537
+ r[2] * d[0] + r[5] * d[1] + r[8] * d[2]
538
+ ];
539
+ if (cellAt(vb, inB)) hits++;
540
+ }
541
+ }
542
+ return hits;
543
+ }
544
+ var LDU = 20;
545
+ var FLIP = [1, 0, 0, 0, -1, 0, 0, 0, 1];
546
+ var normalizeName = (name) => name.trim().replace(/\\/g, "/").toLowerCase();
547
+ function toStuds(rotation, position) {
548
+ return {
549
+ rotation: mul(mul(FLIP, rotation), FLIP),
550
+ position: [position[0] / LDU, -position[1] / LDU, position[2] / LDU]
551
+ };
552
+ }
553
+ function readRef(f) {
554
+ const n = f.slice(2, 14).map(Number);
555
+ return {
556
+ color: Number(f[1]),
557
+ position: [n[0], n[1], n[2]],
558
+ rotation: [n[3], n[4], n[5], n[6], n[7], n[8], n[9], n[10], n[11]],
559
+ name: normalizeName(f.slice(14).join(" "))
560
+ };
561
+ }
562
+ function parsePart(name, resolve9, isJoint) {
563
+ const triangles = [];
564
+ const colors = [];
565
+ const edges = [];
566
+ const joints = [];
567
+ const min = [Infinity, Infinity, Infinity];
568
+ const max = [-Infinity, -Infinity, -Infinity];
569
+ const walk = (file, m, t, inJoint, depth, color) => {
570
+ const text = resolve9(file);
571
+ if (text === void 0) throw new Error(`LDraw file not found: ${file}`);
572
+ if (depth > 32) throw new Error(`LDraw nesting too deep at ${file}`);
573
+ for (const line of text.split(/\r?\n/)) {
574
+ const f = line.trim().split(/\s+/);
575
+ const type = f[0];
576
+ if (type === "1" && f.length >= 15) {
577
+ const ref = readRef(f);
578
+ const rotation = mul(m, ref.rotation);
579
+ const position = add(apply(m, ref.position), t);
580
+ const joint = !inJoint && isJoint(ref.name);
581
+ if (joint) joints.push({ name: ref.name, ...toStuds(rotation, position) });
582
+ const inherited = ref.color === 16 ? color : ref.color;
583
+ walk(ref.name, rotation, position, inJoint || joint, depth + 1, inherited);
584
+ } else if (type === "2" && f.length >= 8) {
585
+ const n = f.slice(2, 8).map(Number);
586
+ for (const k of [0, 3]) {
587
+ const w = add(apply(m, [n[k], n[k + 1], n[k + 2]]), t);
588
+ edges.push(w[0] / LDU, -w[1] / LDU, w[2] / LDU);
589
+ }
590
+ } else if (type === "3" || type === "4") {
591
+ const n = f.slice(2).map(Number);
592
+ const pts = [];
593
+ for (let i = 0; i < (type === "3" ? 3 : 4); i++) {
594
+ const w = add(apply(m, [n[i * 3], n[i * 3 + 1], n[i * 3 + 2]]), t);
595
+ pts.push([w[0] / LDU, -w[1] / LDU, w[2] / LDU]);
596
+ }
597
+ const tris = type === "3" ? [pts] : [
598
+ [pts[0], pts[1], pts[2]],
599
+ [pts[0], pts[2], pts[3]]
600
+ ];
601
+ const c = Number(f[1]);
602
+ for (const tri of tris) {
603
+ for (const p of tri) triangles.push(p[0], p[1], p[2]);
604
+ colors.push(c === 16 ? color : c);
605
+ }
606
+ if (!inJoint)
607
+ for (const p of pts)
608
+ for (let k = 0; k < 3; k++) {
609
+ min[k] = Math.min(min[k], p[k]);
610
+ max[k] = Math.max(max[k], p[k]);
611
+ }
612
+ }
613
+ }
614
+ };
615
+ walk(normalizeName(name), IDENTITY, [0, 0, 0], false, 0, 16);
616
+ return { triangles, colors, edges, body: { min, max }, joints };
617
+ }
618
+ function parseModel(text) {
619
+ const items = [];
620
+ let step = 1;
621
+ for (const line of text.split(/\r?\n/)) {
622
+ const f = line.trim().split(/\s+/);
623
+ if (f[0] === "0" && f[1]?.toUpperCase() === "STEP") step++;
624
+ else if (f[0] === "1" && f.length >= 15) {
625
+ const ref = readRef(f);
626
+ items.push({
627
+ part: ref.name.replace(/\.dat$/, ""),
628
+ color: ref.color,
629
+ step,
630
+ ...toStuds(ref.rotation, ref.position)
631
+ });
632
+ }
633
+ }
634
+ return { items };
635
+ }
636
+ function writeModel(model, title) {
637
+ const fmt = (n) => String(Math.round(n * 1e4) / 1e4 || 0);
638
+ const lines = [
639
+ `0 ${title}`,
640
+ `0 Name: ${title.toLowerCase().replace(/\W+/g, "-")}.ldr`,
641
+ "0 Author: Makeables"
642
+ ];
643
+ let step;
644
+ for (const it of model.items) {
645
+ if (step !== void 0 && it.step !== step) lines.push("0 STEP");
646
+ step = it.step;
647
+ const r = mul(mul(FLIP, it.rotation), FLIP);
648
+ const p = [it.position[0] * LDU, -it.position[1] * LDU, it.position[2] * LDU];
649
+ lines.push(`1 ${it.color} ${[...p, ...r].map(fmt).join(" ")} ${it.part}.dat`);
650
+ }
651
+ return `${lines.join("\n")}
652
+ `;
653
+ }
654
+ var REACH = 0.45;
655
+ var DIAGONAL = 0.2;
656
+ var MIN_RUN = 0.4;
657
+ var SAMPLE = 0.05;
658
+ var ring8 = [0, 1, 2, 3, 4, 5, 6, 7].map((k) => [
659
+ Math.cos(k * Math.PI / 4),
660
+ Math.sin(k * Math.PI / 4)
661
+ ]);
662
+ function technicHoles(voxels, body) {
663
+ const found = [];
664
+ for (let axis = 0; axis < 3; axis++) {
665
+ const u = (axis + 1) % 3;
666
+ const w = (axis + 2) % 3;
667
+ const at = (cu, cw, t, du = 0, dw = 0) => {
668
+ const p = [0, 0, 0];
669
+ p[axis] = t;
670
+ p[u] = cu + du;
671
+ p[w] = cw + dw;
672
+ return cellAt(voxels, p);
673
+ };
674
+ const from = (k) => Math.ceil((body.min[k] + 0.3) / SAMPLE - 1e-9) * SAMPLE;
675
+ for (let cu = from(u); cu <= body.max[u] - 0.3 + 1e-9; cu += SAMPLE)
676
+ for (let cw = from(w); cw <= body.max[w] - 0.3 + 1e-9; cw += SAMPLE) {
677
+ let run2 = 0;
678
+ let best = { start: 0, length: 0, cross: 0, open: false };
679
+ let start = 0;
680
+ let cross2 = 0;
681
+ for (let t = body.min[axis] + SAMPLE; t < body.max[axis]; t += 0.1) {
682
+ const hollow = [0, 0.12].every(
683
+ (r) => [
684
+ [1, 0],
685
+ [-1, 0],
686
+ [0, 1],
687
+ [0, -1]
688
+ ].every(([a2, b]) => !at(cu, cw, t, a2 * r, b * r))
689
+ );
690
+ const walled = ring8.filter(([a2, b]) => {
691
+ for (let r = 0.15; r <= REACH + 1e-9; r += 0.05)
692
+ if (at(cu, cw, t, a2 * r, b * r)) return true;
693
+ return false;
694
+ }).length;
695
+ if (hollow && walled === 8) {
696
+ if (!run2) {
697
+ start = t;
698
+ cross2 = 0;
699
+ }
700
+ run2 += 0.1;
701
+ cross2 += ring8.filter((_, k) => k % 2).filter(([a2, b]) => at(cu, cw, t, a2 * DIAGONAL, b * DIAGONAL)).length;
702
+ if (run2 > best.length) best = { start, length: run2, cross: cross2, open: false };
703
+ } else run2 = 0;
704
+ }
705
+ if (best.length + 1e-9 < MIN_RUN) continue;
706
+ const clear = (t0, dt) => {
707
+ for (let t = t0; t > body.min[axis] - 0.1 && t < body.max[axis] + 0.1; t += dt)
708
+ if (at(cu, cw, t)) return false;
709
+ return true;
710
+ };
711
+ if (!clear(best.start - 0.1, -0.1) || !clear(best.start + best.length, 0.1)) continue;
712
+ const p = [0, 0, 0];
713
+ p[axis] = best.start - 0.05 + best.length / 2;
714
+ p[u] = cu;
715
+ p[w] = cw;
716
+ const a = [0, 0, 0];
717
+ a[axis] = 1;
718
+ const steps = best.length / 0.1;
719
+ found.push({
720
+ kind: best.cross / steps >= 3 ? "axlehole" : "pinhole",
721
+ p,
722
+ a,
723
+ len: best.length,
724
+ score: best.length
725
+ });
726
+ }
727
+ }
728
+ const holes = [];
729
+ const left = new Set(found);
730
+ const near = (x, y) => x.a.join() === y.a.join() && x.p.every((v, k) => x.a[k] === 1 || Math.abs(v - y.p[k]) <= SAMPLE + 1e-6);
731
+ for (const seed of found) {
732
+ if (!left.has(seed)) continue;
733
+ const group = [seed];
734
+ left.delete(seed);
735
+ for (let i = 0; i < group.length; i++)
736
+ for (const h of left)
737
+ if (near(group[i], h)) {
738
+ group.push(h);
739
+ left.delete(h);
740
+ }
741
+ const mean = [0, 1, 2].map((k) => group.reduce((s, h) => s + h.p[k], 0) / group.length);
742
+ const longest = group.reduce((x, y) => y.score > x.score ? y : x);
743
+ const crosses = group.filter((h) => h.kind === "axlehole").length;
744
+ holes.push({
745
+ kind: crosses * 2 > group.length ? "axlehole" : "pinhole",
746
+ p: mean.map((v) => Math.round(v / SAMPLE) * SAMPLE || 0),
747
+ a: seed.a,
748
+ len: longest.len
749
+ });
750
+ }
751
+ return holes;
752
+ }
753
+ var SHAFT = /^Technic (Axle\s+\d+(\.\d+)?$|Axle\s+\d+ with Stop|Pin\b|Friction Pin|Axle Pin)/i;
754
+ function technicShafts(name, triangles) {
755
+ if (!SHAFT.test(name) || /connector|joiner|hole|beam/i.test(name)) return [];
756
+ const { min, max } = meshBounds(triangles);
757
+ const size = [0, 1, 2].map((k) => max[k] - min[k]);
758
+ const axis = size.indexOf(Math.max(...size));
759
+ const a = [0, 0, 0];
760
+ a[axis] = 1;
761
+ const p = [0, 1, 2].map((k) => (min[k] + max[k]) / 2);
762
+ return [
763
+ { kind: /pin/i.test(name) && !/axle pin/i.test(name) ? "pin" : "axle", p, a, len: size[axis] }
764
+ ];
765
+ }
766
+ var STUD = /^(stud|stud2|stud2a|studa|stud26|stud6a?|stud9|stud10|stud13|stud15|stud17a?|studel|studh[lr]?|studp01|studxa?|stud2?-logo\d?)\.dat$/;
767
+ var FAMILY = {
768
+ "3001": "brick",
769
+ "3003": "brick",
770
+ "3004": "brick",
771
+ "3005": "brick",
772
+ "3010": "brick",
773
+ "3020": "plate",
774
+ "3021": "plate",
775
+ "3022": "plate",
776
+ "3023": "plate",
777
+ "3024": "plate",
778
+ "3710": "plate",
779
+ "3666": "plate",
780
+ "3460": "plate",
781
+ "3068b": "tile",
782
+ "3069b": "tile",
783
+ "3070b": "tile"
784
+ };
785
+ var lego = {
786
+ name: "lego",
787
+ fits: {
788
+ stud: ["socket"],
789
+ socket: ["stud"],
790
+ axle: ["axlehole", "pinhole"],
791
+ axlehole: ["axle"],
792
+ pin: ["pinhole"],
793
+ pinhole: ["pin", "axle"],
794
+ wheelpin: ["wheelhole"],
795
+ wheelhole: ["wheelpin"],
796
+ rim: ["tyre"],
797
+ tyre: ["rim"],
798
+ hinge: ["hinge"],
799
+ bar: ["clip", "barhole"],
800
+ clip: ["bar"],
801
+ barhole: ["bar"],
802
+ neck: ["neckhole"],
803
+ neckhole: ["neck"],
804
+ hatstud: ["hat"],
805
+ hat: ["hatstud"],
806
+ waist: ["waisthole"],
807
+ waisthole: ["waist"],
808
+ hip: ["leg"],
809
+ leg: ["hip"],
810
+ shoulder: ["shoulderhole"],
811
+ shoulderhole: ["shoulder"],
812
+ wrist: ["hand"],
813
+ hand: ["wrist"]
814
+ },
815
+ tolerance: 0.06,
816
+ minEngagement: 0.24,
817
+ family: (part2) => FAMILY[part2] ?? part2
818
+ };
819
+ var WRIST_RIGHT = [-0.25, -0.875, -0.5];
820
+ var WRIST_LEFT = [0.25, -0.875, -0.5];
821
+ var LEGO_CONNECTORS = {
822
+ // Plate 2 x 2 with 2 Wheel Pins; the pins hang below the plate, so its sockets are listed too.
823
+ "4600": [
824
+ ...[-0.5, 0.5].flatMap(
825
+ (x) => [-0.5, 0.5].map((z5) => ({ kind: "socket", p: [x, -0.4, z5], a: [0, -1, 0] }))
826
+ ),
827
+ { kind: "wheelpin", p: [1.4, -0.25, 0], a: [1, 0, 0], len: 0.6 },
828
+ { kind: "wheelpin", p: [-1.4, -0.25, 0], a: [1, 0, 0], len: 0.6 }
829
+ ],
830
+ // Wheel Rim 6.4 x 8
831
+ "4624": [
832
+ { kind: "wheelhole", p: [0, 0, 0], a: [0, 0, 1], len: 0.8 },
833
+ { kind: "rim", p: [0, 0, 0], a: [0, 0, 1], len: 0.8 }
834
+ ],
835
+ // Tyre 6/ 50 x 8 Offset Tread
836
+ "3641": [{ kind: "tyre", p: [0, 0, 0], a: [0, 0, 1], len: 0.8 }],
837
+ // Hinge 1 x 2 Base and Top: both pivot on the cylinder along x at 10 LDU below the origin.
838
+ "3937": [{ kind: "hinge", p: [0, -0.5, 0], a: [1, 0, 0], len: 2 }],
839
+ "3938": [{ kind: "hinge", p: [0, -0.5, 0], a: [1, 0, 0], len: 2 }],
840
+ // Plate 1 x 3 with 2 Studs Offset: the teeth underneath hold a stud at every half step,
841
+ // so a jumper also sits straight on another jumper's offset studs.
842
+ "34103": [-1, -0.5, 0, 0.5, 1].map(
843
+ (x) => ({ kind: "socket", p: [x, -0.4, 0], a: [0, -1, 0] })
844
+ ),
845
+ // Bar 6L with Thick Stop: the bar runs the full height of the part.
846
+ "63965a": [{ kind: "bar", p: [0, 2.2, 0], a: [0, 1, 0], len: 6 }],
847
+ // Cone 1 x 1: hollow along its axis, so a bar goes in from the top.
848
+ "4589": [
849
+ { kind: "barhole", p: [0, -0.5, 0], a: [0, 1, 0], len: 1.4 },
850
+ { kind: "socket", p: [0, -1.2, 0], a: [0, -1, 0] }
851
+ ],
852
+ // Tile 2 x 3 with Clips Horizontal: both C-clips share an axis along x.
853
+ "30350b": [{ kind: "clip", p: [0, -0.05, 2], a: [1, 0, 0], len: 2 }],
854
+ // Minifig, in the frame of each part as LDraw places a standard figure.
855
+ "973": [
856
+ { kind: "neck", p: [0, 0, 0], a: [0, 1, 0] },
857
+ { kind: "waist", p: [0, -1.6, 0], a: [0, -1, 0] },
858
+ { kind: "shoulder", p: [-0.7776, -0.45, 0], a: [-1, 0, 0] },
859
+ { kind: "shoulder", p: [0.7776, -0.45, 0], a: [1, 0, 0] }
860
+ ],
861
+ "3626c": [
862
+ { kind: "neckhole", p: [0, -1.2, 0], a: [0, -1, 0] },
863
+ { kind: "hatstud", p: [0, 0, 0], a: [0, 1, 0] }
864
+ ],
865
+ "3626cp7g": [
866
+ { kind: "neckhole", p: [0, -1.2, 0], a: [0, -1, 0] },
867
+ { kind: "hatstud", p: [0, 0, 0], a: [0, 1, 0] }
868
+ ],
869
+ "86035": [{ kind: "hat", p: [0, 0, 0], a: [0, -1, 0] }],
870
+ "3815": [
871
+ { kind: "waisthole", p: [0, 0, 0], a: [0, 1, 0] },
872
+ { kind: "hip", p: [0, -0.6, 0], a: [0, -1, 0] },
873
+ { kind: "hip", p: [0, -0.6, 0], a: [0, -1, 0] }
874
+ ],
875
+ "3816": [
876
+ { kind: "leg", p: [0, 0, 0], a: [0, 1, 0] },
877
+ { kind: "socket", p: [-0.5, -1.4, 0], a: [0, -1, 0] }
878
+ ],
879
+ "3817": [
880
+ { kind: "leg", p: [0, 0, 0], a: [0, 1, 0] },
881
+ { kind: "socket", p: [0.5, -1.4, 0], a: [0, -1, 0] }
882
+ ],
883
+ "3818": [
884
+ { kind: "shoulderhole", p: [0, 0, 0], a: [1, 0, 0] },
885
+ { kind: "wrist", p: WRIST_RIGHT, a: [0, 0, -1] }
886
+ ],
887
+ "3819": [
888
+ { kind: "shoulderhole", p: [0, 0, 0], a: [-1, 0, 0] },
889
+ { kind: "wrist", p: WRIST_LEFT, a: [0, 0, -1] }
890
+ ],
891
+ // The hand's peg goes into the sleeve from the front.
892
+ "3820": [{ kind: "hand", p: [0, 0, 0], a: [0, 0, 1] }]
893
+ };
894
+ function technicConnectors(name, parsed, voxels) {
895
+ const shafts = technicShafts(name, parsed.triangles);
896
+ return shafts.length ? shafts : technicHoles(voxels, parsed.body);
897
+ }
898
+ var FLAT_TOP = 0.5;
899
+ function flatTop({ triangles, body }) {
900
+ let area = 0;
901
+ for (let t = 0; t < triangles.length; t += 9) {
902
+ const ys = [triangles[t + 1], triangles[t + 4], triangles[t + 7]];
903
+ if (ys.some((y) => Math.abs(y - body.max[1]) > 1e-3)) continue;
904
+ const ux = triangles[t + 3] - triangles[t];
905
+ const uz = triangles[t + 5] - triangles[t + 2];
906
+ const vx = triangles[t + 6] - triangles[t];
907
+ const vz = triangles[t + 8] - triangles[t + 2];
908
+ area += Math.abs(ux * vz - uz * vx) / 2;
909
+ }
910
+ const footprint = (body.max[0] - body.min[0]) * (body.max[2] - body.min[2]);
911
+ return footprint > 0 ? area / footprint : 0;
912
+ }
913
+ var WALLED_BOTTOM = 0.35;
914
+ var STUD_DEPTH = 0.3;
915
+ var lattice = (lo, hi, phase) => {
916
+ const out = [];
917
+ for (let x = Math.ceil(lo - phase) + phase; x < hi; x++)
918
+ if (x - lo > 0.25 && hi - x > 0.25) out.push(x);
919
+ return out;
920
+ };
921
+ function legoConnectors(parsed, voxels) {
922
+ const out = [];
923
+ for (const j of parsed.joints)
924
+ if (STUD.test(j.name))
925
+ out.push({ kind: "stud", p: j.position, a: apply(j.rotation, [0, 1, 0]) });
926
+ const { min, max } = parsed.body;
927
+ const up = out.find((c) => dot(c.a, [0, 1, 0]) > 0.99);
928
+ const phase = (k) => {
929
+ const v = (min[k] + 0.5) % 1;
930
+ return v < 0 ? v + 1 : v;
931
+ };
932
+ const y = min[1];
933
+ const rim = [];
934
+ for (let x = min[0] + CELL / 2; x < max[0]; x += CELL)
935
+ rim.push([x, y + CELL / 2, min[2] + CELL / 2], [x, y + CELL / 2, max[2] - CELL / 2]);
936
+ for (let z5 = min[2] + CELL / 2; z5 < max[2]; z5 += CELL)
937
+ rim.push([min[0] + CELL / 2, y + CELL / 2, z5], [max[0] - CELL / 2, y + CELL / 2, z5]);
938
+ const walled = rim.filter((p) => cellAt(voxels, p)).length >= WALLED_BOTTOM * rim.length;
939
+ if (!up && !walled && flatTop(parsed) < FLAT_TOP) return out;
940
+ for (const x of lattice(min[0], max[0], phase(0)))
941
+ for (const z5 of lattice(min[2], max[2], phase(2))) {
942
+ let hollow;
943
+ for (let h = y + CELL / 2; h < y + STUD_DEPTH && hollow === void 0; h += CELL)
944
+ if (!cellAt(voxels, [x, h, z5])) hollow = h;
945
+ if (hollow === void 0) continue;
946
+ let ceiling = false;
947
+ for (let h = hollow + CELL; h < max[1] && !ceiling; h += CELL)
948
+ ceiling = cellAt(voxels, [x, h, z5]);
949
+ if (ceiling) out.push({ kind: "socket", p: [x, y, z5], a: [0, -1, 0] });
950
+ }
951
+ return out;
952
+ }
953
+ function loadLegoPart(id, resolve9, opts = {}) {
954
+ const parsed = parsePart(`${id}.dat`, resolve9, (p) => STUD.test(p));
955
+ const bounds = meshBounds(parsed.triangles);
956
+ const voxels = voxelize(parsed.triangles, bounds.min, bounds.max);
957
+ return {
958
+ id,
959
+ name: opts.name ?? id,
960
+ voxels,
961
+ connectors: [
962
+ // Listed sockets replace inferred ones, for parts whose underside the solid misreads.
963
+ ...legoConnectors(parsed, voxels).filter(
964
+ (c) => c.kind !== "socket" || !LEGO_CONNECTORS[id]?.some((l) => l.kind === "socket")
965
+ ),
966
+ ...opts.connectors ?? LEGO_CONNECTORS[id] ?? technicConnectors(opts.name ?? "", parsed, voxels)
967
+ ],
968
+ body: parsed.body,
969
+ triangles: parsed.triangles,
970
+ colors: parsed.colors,
971
+ edges: parsed.edges
972
+ };
973
+ }
974
+ function libraryResolver(read, root) {
975
+ return (name) => {
976
+ for (const dir of ["parts", "p", "parts/s"]) {
977
+ const text = read(`${root}/${dir}/${name}`);
978
+ if (text !== void 0) return text;
979
+ }
980
+ return void 0;
981
+ };
982
+ }
983
+ var DEFAULT_PALETTE = {
984
+ 0: "#1B2A34",
985
+ 1: "#1E5AA8",
986
+ 4: "#B40000",
987
+ 14: "#FAC80A",
988
+ 15: "#F4F4F4",
989
+ 71: "#969696",
990
+ 72: "#646464"
991
+ };
992
+ function parsePalette(ldconfig) {
993
+ const out = {};
994
+ for (const m of ldconfig.matchAll(/^0 !COLOUR \S+\s+CODE\s+(\d+)\s+VALUE\s+(#[0-9A-Fa-f]{6})/gm))
995
+ out[Number(m[1])] = m[2];
996
+ return out;
997
+ }
998
+ var LIGHT = (() => {
999
+ const v = [0.4, 0.85, 0.35];
1000
+ return scale(v, 1 / length(v));
1001
+ })();
1002
+ var rgb = (hex) => {
1003
+ const n = Number.parseInt(hex.slice(1), 16);
1004
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1005
+ };
1006
+ function rasterize(model, lib, opts = {}) {
1007
+ const ss = opts.supersample ?? 2;
1008
+ const W2 = (opts.width ?? 800) * ss;
1009
+ const H2 = (opts.height ?? 600) * ss;
1010
+ const yaw = opts.yaw ?? Math.PI / 4;
1011
+ const pitch = opts.pitch ?? Math.PI / 6;
1012
+ const palette = { ...DEFAULT_PALETTE, ...opts.palette };
1013
+ const highlight = new Set(opts.highlight ?? []);
1014
+ const background = rgb(opts.background ?? "#ffffff");
1015
+ const view = [
1016
+ Math.sin(yaw) * Math.cos(pitch),
1017
+ Math.sin(pitch),
1018
+ Math.cos(yaw) * Math.cos(pitch)
1019
+ ];
1020
+ const right = [Math.cos(yaw), 0, -Math.sin(yaw)];
1021
+ const up = cross(view, right);
1022
+ const shown = model.items.map((item, i) => ({ item, i })).filter(({ item }) => opts.maxStep === void 0 || (item.step ?? 0) <= opts.maxStep);
1023
+ const bodies = shown.map(({ item }) => worldBody(lib, item));
1024
+ const center = [0, 0, 0];
1025
+ for (const b of bodies) for (let k = 0; k < 3; k++) center[k] += (b.min[k] + b.max[k]) / 2;
1026
+ if (bodies.length) for (let k = 0; k < 3; k++) center[k] /= bodies.length;
1027
+ const tris = [];
1028
+ const lines = [];
1029
+ let minX = Infinity;
1030
+ let minY = Infinity;
1031
+ let maxX = -Infinity;
1032
+ let maxY = -Infinity;
1033
+ const toView = (p) => {
1034
+ const v = [dot(p, right), -dot(p, up), dot(p, view)];
1035
+ minX = Math.min(minX, v[0]);
1036
+ maxX = Math.max(maxX, v[0]);
1037
+ minY = Math.min(minY, v[1]);
1038
+ maxY = Math.max(maxY, v[1]);
1039
+ return v;
1040
+ };
1041
+ shown.forEach(({ item, i }, n) => {
1042
+ const part2 = lib[item.part];
1043
+ if (!part2) throw new Error(`Unknown part: ${item.part}`);
1044
+ const b = bodies[n];
1045
+ const mid = [
1046
+ (b.min[0] + b.max[0]) / 2,
1047
+ (b.min[1] + b.max[1]) / 2,
1048
+ (b.min[2] + b.max[2]) / 2
1049
+ ];
1050
+ const offset = opts.explode ? scale(sub(mid, center), opts.explode) : [0, 0, 0];
1051
+ const fade = highlight.size && !highlight.has(i) ? 0.72 : 0;
1052
+ const world = (x, y, z5) => add(add(apply(item.rotation, [x, y, z5]), item.position), offset);
1053
+ const t = part2.triangles;
1054
+ for (let f = 0; f < t.length / 9; f++) {
1055
+ const w = [0, 1, 2].map(
1056
+ (v) => world(t[f * 9 + v * 3], t[f * 9 + v * 3 + 1], t[f * 9 + v * 3 + 2])
1057
+ );
1058
+ const normal = cross(sub(w[1], w[0]), sub(w[2], w[0]));
1059
+ const len = length(normal);
1060
+ if (len < 1e-9) continue;
1061
+ const nn = scale(normal, 1 / len);
1062
+ const facing = dot(nn, view) < 0 ? scale(nn, -1) : nn;
1063
+ const code = part2.colors[f] === 16 ? item.color : part2.colors[f];
1064
+ const k = 0.55 + 0.45 * Math.max(0, dot(facing, LIGHT));
1065
+ const base2 = rgb(palette[code] ?? "#888888");
1066
+ tris.push({
1067
+ p: w.map(toView),
1068
+ color: base2.map((c) => {
1069
+ const v = Math.min(255, c * k);
1070
+ return v + (255 - v) * fade;
1071
+ })
1072
+ });
1073
+ }
1074
+ const e = part2.edges ?? [];
1075
+ const base = rgb(palette[item.color] ?? "#888888");
1076
+ const dark = base[0] + base[1] + base[2] < 115;
1077
+ const edge = (dark ? [110, 110, 110] : base.map((c) => c * 0.45)).map(
1078
+ (c) => c + (255 - c) * fade
1079
+ );
1080
+ for (let s2 = 0; s2 < e.length; s2 += 6)
1081
+ lines.push({
1082
+ a: toView(world(e[s2], e[s2 + 1], e[s2 + 2])),
1083
+ b: toView(world(e[s2 + 3], e[s2 + 4], e[s2 + 5])),
1084
+ color: edge
1085
+ });
1086
+ });
1087
+ const s = tris.length ? 0.9 * Math.min(W2 / (maxX - minX || 1), H2 / (maxY - minY || 1)) : 1;
1088
+ const ox = W2 / 2 - s * (minX + maxX) / 2;
1089
+ const oy = H2 / 2 - s * (minY + maxY) / 2;
1090
+ const sx = (v) => [ox + s * v[0], oy + s * v[1], v[2]];
1091
+ const data = new Uint8ClampedArray(W2 * H2 * 4);
1092
+ const depth = new Float32Array(W2 * H2).fill(-Infinity);
1093
+ for (let p = 0; p < W2 * H2; p++) {
1094
+ data[p * 4] = background[0];
1095
+ data[p * 4 + 1] = background[1];
1096
+ data[p * 4 + 2] = background[2];
1097
+ data[p * 4 + 3] = 255;
1098
+ }
1099
+ const put = (x, y, z5, c) => {
1100
+ const p = y * W2 + x;
1101
+ if (z5 <= depth[p]) return;
1102
+ depth[p] = z5;
1103
+ data[p * 4] = c[0];
1104
+ data[p * 4 + 1] = c[1];
1105
+ data[p * 4 + 2] = c[2];
1106
+ };
1107
+ for (const tri of tris) {
1108
+ const [a, b, c] = tri.p.map(sx);
1109
+ const area = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
1110
+ if (Math.abs(area) < 1e-9) continue;
1111
+ const x0 = Math.max(0, Math.floor(Math.min(a[0], b[0], c[0])));
1112
+ const x1 = Math.min(W2 - 1, Math.ceil(Math.max(a[0], b[0], c[0])));
1113
+ const y0 = Math.max(0, Math.floor(Math.min(a[1], b[1], c[1])));
1114
+ const y1 = Math.min(H2 - 1, Math.ceil(Math.max(a[1], b[1], c[1])));
1115
+ for (let y = y0; y <= y1; y++)
1116
+ for (let x = x0; x <= x1; x++) {
1117
+ const px = x + 0.5;
1118
+ const py = y + 0.5;
1119
+ const l0 = ((b[0] - px) * (c[1] - py) - (c[0] - px) * (b[1] - py)) / area;
1120
+ const l1 = ((c[0] - px) * (a[1] - py) - (a[0] - px) * (c[1] - py)) / area;
1121
+ const l2 = 1 - l0 - l1;
1122
+ if (l0 < 0 || l1 < 0 || l2 < 0) continue;
1123
+ put(x, y, l0 * a[2] + l1 * b[2] + l2 * c[2], tri.color);
1124
+ }
1125
+ }
1126
+ const bias = 0.02;
1127
+ for (const line of lines) {
1128
+ const a = sx(line.a);
1129
+ const b = sx(line.b);
1130
+ const steps = Math.max(1, Math.ceil(Math.max(Math.abs(b[0] - a[0]), Math.abs(b[1] - a[1]))));
1131
+ for (let k = 0; k <= steps; k++) {
1132
+ const t = k / steps;
1133
+ const x = Math.round(a[0] + (b[0] - a[0]) * t);
1134
+ const y = Math.round(a[1] + (b[1] - a[1]) * t);
1135
+ if (x < 0 || y < 0 || x >= W2 || y >= H2) continue;
1136
+ for (const [dx, dy] of ss > 1 ? [
1137
+ [0, 0],
1138
+ [1, 0],
1139
+ [0, 1],
1140
+ [1, 1]
1141
+ ] : [[0, 0]]) {
1142
+ const X = Math.min(W2 - 1, x + dx);
1143
+ const Y = Math.min(H2 - 1, y + dy);
1144
+ const p = Y * W2 + X;
1145
+ const z5 = a[2] + (b[2] - a[2]) * t;
1146
+ if (z5 + bias < depth[p]) continue;
1147
+ data[p * 4] = line.color[0];
1148
+ data[p * 4 + 1] = line.color[1];
1149
+ data[p * 4 + 2] = line.color[2];
1150
+ }
1151
+ }
1152
+ }
1153
+ return { width: W2, height: H2, data };
1154
+ }
1155
+ var key = (a, b, kind) => `${a < b ? `${a}~${b}` : `${b}~${a}`}:${kind}`;
1156
+ function learnGrammar(model, joints, adapter) {
1157
+ const exact = /* @__PURE__ */ new Set();
1158
+ const family = /* @__PURE__ */ new Set();
1159
+ for (const c of joints) {
1160
+ const a = model.items[c.a].part;
1161
+ const b = model.items[c.b].part;
1162
+ exact.add(key(a, b, c.kind));
1163
+ family.add(key(adapter.family(a), adapter.family(b), c.kind));
1164
+ }
1165
+ return { exact, family };
1166
+ }
1167
+ function jointAllowed(grammar, adapter, a, b, kind, mode) {
1168
+ if (mode === "free" || grammar.exact.has(key(a, b, kind))) return true;
1169
+ return mode === "family" && grammar.family.has(key(adapter.family(a), adapter.family(b), kind));
1170
+ }
1171
+ var DIRECTIONS = [
1172
+ [0, 1, 0],
1173
+ [1, 0, 0],
1174
+ [-1, 0, 0],
1175
+ [0, 0, 1],
1176
+ [0, 0, -1],
1177
+ [0, -1, 0]
1178
+ ];
1179
+ var STEP = 0.15;
1180
+ var BLOCKING_CELLS = 4;
1181
+ var moved = (it, d, s) => ({
1182
+ ...it,
1183
+ position: add(it.position, scale(d, s))
1184
+ });
1185
+ var shifted = (b, d, s) => ({
1186
+ min: add(b.min, scale(d, s)),
1187
+ max: add(b.max, scale(d, s))
1188
+ });
1189
+ function planAssembly(model, lib, adapter) {
1190
+ const items = model.items;
1191
+ const bodies = items.map((it) => worldBody(lib, it));
1192
+ const axes = /* @__PURE__ */ new Map();
1193
+ const releases = /* @__PURE__ */ new Map();
1194
+ for (const c of contacts(model, lib, adapter)) {
1195
+ if (c.axis) {
1196
+ axes.set(`${c.a},${c.b}`, c.axis);
1197
+ axes.set(`${c.b},${c.a}`, c.axis);
1198
+ }
1199
+ if (c.release) {
1200
+ releases.set(`${c.b},${c.a}`, c.release);
1201
+ releases.set(`${c.a},${c.b}`, scale(c.release, -1));
1202
+ }
1203
+ }
1204
+ const allowed = (i, d, around) => {
1205
+ for (const j of around) {
1206
+ if (j === i) continue;
1207
+ const axis = axes.get(`${i},${j}`);
1208
+ if (axis && !slides(i, j, d)) return false;
1209
+ const release = releases.get(`${i},${j}`);
1210
+ if (release && dot(release, d) < 0.99) return false;
1211
+ }
1212
+ return true;
1213
+ };
1214
+ const centre = (b) => scale(add(b.min, b.max), 0.5);
1215
+ const slides = (i, j, d) => {
1216
+ const axis = axes.get(`${i},${j}`);
1217
+ if (!axis || Math.abs(dot(axis, d)) <= 0.99) return false;
1218
+ return dot(d, sub(centre(bodies[i]), centre(bodies[j]))) >= -1e-6;
1219
+ };
1220
+ const left = new Set(items.map((_, i) => i));
1221
+ const freeWay = (i, around) => DIRECTIONS.find((d) => {
1222
+ if (!allowed(i, d, around)) return false;
1223
+ const others = [...around].filter((j) => j !== i && !slides(i, j, d));
1224
+ for (let s = STEP; ; s += STEP) {
1225
+ const box2 = shifted(bodies[i], d, s);
1226
+ const near = others.filter((j) => overlapVolume(box2, bodies[j], -0.4) > 0);
1227
+ if (!near.length) return true;
1228
+ const it = moved(items[i], d, s);
1229
+ if (near.some((j) => collisionCells(lib, it, items[j]) > BLOCKING_CELLS)) return false;
1230
+ }
1231
+ });
1232
+ const removed = [];
1233
+ while (left.size) {
1234
+ const order2 = [...left].sort((a, b) => bodies[b].max[1] - bodies[a].max[1] || b - a);
1235
+ let next;
1236
+ for (const i of order2) {
1237
+ const d = freeWay(i, left);
1238
+ if (d) {
1239
+ next = { item: i, insert: [0 - d[0], 0 - d[1], 0 - d[2]] };
1240
+ break;
1241
+ }
1242
+ }
1243
+ if (!next) break;
1244
+ removed.push(next);
1245
+ left.delete(next.item);
1246
+ }
1247
+ if (!items.length || items.some((it) => it.step === void 0))
1248
+ return { steps: removed.reverse(), locked: [...left] };
1249
+ const step = (i) => items[i].step ?? 0;
1250
+ const order = items.map((_, i) => i).sort((a, b) => step(a) - step(b) || a - b);
1251
+ const built = /* @__PURE__ */ new Set();
1252
+ const steps = [];
1253
+ const locked = [];
1254
+ for (const i of order) {
1255
+ const d = freeWay(i, /* @__PURE__ */ new Set([...built, i]));
1256
+ if (d) steps.push({ item: i, insert: [0 - d[0], 0 - d[1], 0 - d[2]] });
1257
+ else locked.push(i);
1258
+ built.add(i);
1259
+ }
1260
+ return { steps, locked };
1261
+ }
1262
+ function seededRandom(seed) {
1263
+ let a = seed >>> 0;
1264
+ return () => {
1265
+ a = a + 1831565813 >>> 0;
1266
+ let t = a;
1267
+ t = Math.imul(t ^ t >>> 15, t | 1);
1268
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1269
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1270
+ };
1271
+ }
1272
+ function mates(adapter, free, placed) {
1273
+ if (!adapter.fits[free.kind]?.includes(placed.kind)) return false;
1274
+ const axial = free.len !== void 0 && placed.len !== void 0;
1275
+ return axial ? length(cross(free.a, placed.a)) < 0.01 : dot(free.a, placed.a) < -0.99;
1276
+ }
1277
+ function remix(exemplar, lib, adapter, opts) {
1278
+ const random = seededRandom(opts.seed);
1279
+ const mode = opts.mode ?? "family";
1280
+ const joints = contacts(exemplar, lib, adapter);
1281
+ const grammar = learnGrammar(exemplar, joints, adapter);
1282
+ const parts = [...new Set(exemplar.items.map((it) => it.part))];
1283
+ const owners = /* @__PURE__ */ new Map();
1284
+ for (const part2 of parts)
1285
+ for (const c of lib[part2].connectors)
1286
+ owners.set(c.kind, (owners.get(c.kind) ?? /* @__PURE__ */ new Set()).add(part2));
1287
+ const specialised = (kind) => kind.split("-").every((k) => owners.get(k)?.size === 1);
1288
+ const group = exemplar.items.map((_, i) => i);
1289
+ const find = (i) => {
1290
+ while (group[i] !== i) i = group[i];
1291
+ return i;
1292
+ };
1293
+ for (const c of joints) if (specialised(c.kind)) group[find(c.a)] = find(c.b);
1294
+ const members = /* @__PURE__ */ new Map();
1295
+ exemplar.items.forEach((_, i) => {
1296
+ members.set(find(i), [...members.get(find(i)) ?? [], i]);
1297
+ });
1298
+ const units = [...members.values()].map((ids) => {
1299
+ const r = exemplar.items[ids[0]];
1300
+ const inverse = transpose(r.rotation);
1301
+ return {
1302
+ first: Math.min(...ids),
1303
+ root: r,
1304
+ items: ids.map((i) => {
1305
+ const it = exemplar.items[i];
1306
+ return {
1307
+ ...it,
1308
+ rotation: mul(inverse, it.rotation),
1309
+ position: apply(inverse, sub(it.position, r.position))
1310
+ };
1311
+ })
1312
+ };
1313
+ });
1314
+ units.sort((x, y) => x.first - y.first);
1315
+ const place = (unit, { rotation, position }) => unit.items.map((it) => ({
1316
+ ...it,
1317
+ rotation: mul(rotation, it.rotation),
1318
+ position: add(apply(rotation, it.position), position)
1319
+ }));
1320
+ const freeConnectors = (items2) => {
1321
+ const world = items2.map((it) => worldConnectors(lib, it));
1322
+ const used = /* @__PURE__ */ new Set();
1323
+ for (const c of contacts({ items: items2 }, lib, adapter))
1324
+ for (const [x, y] of [
1325
+ [c.a, c.b],
1326
+ [c.b, c.a]
1327
+ ])
1328
+ world[x].forEach((cx, n) => {
1329
+ if (world[y].some(
1330
+ (cy) => mates(adapter, cx, cy) && length(sub(cx.p, cy.p)) <= adapter.tolerance
1331
+ ))
1332
+ used.add(`${x}:${n}`);
1333
+ });
1334
+ return world.flatMap((list, x) => list.filter((_, n) => !used.has(`${x}:${n}`)));
1335
+ };
1336
+ const poses = (unit, free) => {
1337
+ const own = freeConnectors(unit.items);
1338
+ const out = [];
1339
+ for (const rotation of AXIS_ROTATIONS)
1340
+ for (const c of own) {
1341
+ const turned = { ...c, p: apply(rotation, c.p), a: apply(rotation, c.a) };
1342
+ for (const f of free)
1343
+ if (mates(adapter, f, turned)) out.push({ rotation, position: sub(f.p, turned.p) });
1344
+ }
1345
+ for (let i = out.length - 1; i > 0; i--) {
1346
+ const j = Math.floor(random() * (i + 1));
1347
+ [out[i], out[j]] = [out[j], out[i]];
1348
+ }
1349
+ return out;
1350
+ };
1351
+ const legal = (build, added) => {
1352
+ const all = [...build, ...added];
1353
+ const fresh = contacts({ items: all }, lib, adapter).filter(
1354
+ (c) => c.a < build.length && c.b >= build.length
1355
+ );
1356
+ if (!fresh.length || !fresh.every(
1357
+ (c) => jointAllowed(grammar, adapter, all[c.a].part, all[c.b].part, c.kind, mode)
1358
+ ))
1359
+ return false;
1360
+ const through = new Set(fresh.filter((c) => c.axis).map((c) => `${c.a},${c.b}`));
1361
+ return added.every((item, k) => {
1362
+ const body = worldBody(lib, item);
1363
+ return build.every(
1364
+ (other, j) => through.has(`${j},${build.length + k}`) || overlapVolume(body, worldBody(lib, other), -0.4) <= 0 || collisionCells(lib, item, other) <= 4
1365
+ );
1366
+ });
1367
+ };
1368
+ const [seed, ...rest] = units;
1369
+ const items = place(seed, seed.root).map((it) => ({ ...it, step: 1 }));
1370
+ const pending = [...rest];
1371
+ for (let step = 2; pending.length; step++) {
1372
+ const free = freeConnectors(items);
1373
+ let chosen;
1374
+ for (const unit of pending) {
1375
+ for (const pose of poses(unit, free)) {
1376
+ const added = place(unit, pose).map((it) => ({ ...it, step }));
1377
+ if (legal(items, added) && !planAssembly({ items: [...items, ...added] }, lib, adapter).locked.length) {
1378
+ chosen = { unit, added };
1379
+ break;
1380
+ }
1381
+ }
1382
+ if (chosen) break;
1383
+ }
1384
+ if (!chosen) break;
1385
+ pending.splice(pending.indexOf(chosen.unit), 1);
1386
+ items.push(...chosen.added);
1387
+ }
1388
+ return { model: { items }, left: pending.flatMap((u) => u.items.map((it) => it.part)) };
1389
+ }
1390
+ function toStl(model, lib, unit) {
1391
+ const tris = [];
1392
+ for (const item of model.items) {
1393
+ const part2 = lib[item.part];
1394
+ if (!part2) throw new Error(`Unknown part: ${item.part}`);
1395
+ const t = part2.triangles;
1396
+ for (let i = 0; i < t.length; i += 3) {
1397
+ const p = add(apply(item.rotation, [t[i], t[i + 1], t[i + 2]]), item.position);
1398
+ tris.push(p[0] * unit, -p[2] * unit, p[1] * unit);
1399
+ }
1400
+ }
1401
+ const count = tris.length / 9;
1402
+ const out = new DataView(new ArrayBuffer(84 + count * 50));
1403
+ out.setUint32(80, count, true);
1404
+ for (let f = 0; f < count; f++) {
1405
+ const o = 84 + f * 50;
1406
+ const v = tris.slice(f * 9, f * 9 + 9);
1407
+ const u = [v[3] - v[0], v[4] - v[1], v[5] - v[2]];
1408
+ const w = [v[6] - v[0], v[7] - v[1], v[8] - v[2]];
1409
+ const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
1410
+ const len = Math.hypot(n[0], n[1], n[2]) || 1;
1411
+ for (let k = 0; k < 3; k++) out.setFloat32(o + k * 4, n[k] / len, true);
1412
+ for (let k = 0; k < 9; k++) out.setFloat32(o + 12 + k * 4, v[k], true);
1413
+ }
1414
+ return new Uint8Array(out.buffer);
1415
+ }
1416
+ var COLLISION_CELLS = 4;
1417
+ function validate(model, lib, adapter, opts = {}) {
1418
+ const issues = [];
1419
+ const n = model.items.length;
1420
+ const joints = contacts(model, lib, adapter);
1421
+ const bodies = model.items.map((it) => worldBody(lib, it));
1422
+ const through = new Set(joints.filter((c) => c.axis).map((c) => `${c.a},${c.b}`));
1423
+ for (let a = 0; a < n; a++)
1424
+ for (let b = a + 1; b < n; b++)
1425
+ if (!through.has(`${a},${b}`) && overlapVolume(bodies[a], bodies[b], -0.4) > 0 && collisionCells(lib, model.items[a], model.items[b]) > COLLISION_CELLS)
1426
+ issues.push({ rule: "overlap", items: [a, b], message: "two parts occupy the same space" });
1427
+ const parent = model.items.map((_, i) => i);
1428
+ const find = (x) => {
1429
+ while (parent[x] !== x) x = parent[x];
1430
+ return x;
1431
+ };
1432
+ for (const c of joints) parent[find(c.a)] = find(c.b);
1433
+ const groups = /* @__PURE__ */ new Map();
1434
+ for (let i = 0; i < n; i++) groups.set(find(i), [...groups.get(find(i)) ?? [], i]);
1435
+ const sorted = [...groups.values()].sort((x, y) => y.length - x.length);
1436
+ for (const g of sorted.slice(1))
1437
+ issues.push({ rule: "disconnected", items: g, message: "parts not joined to the main build" });
1438
+ if (opts.grammar)
1439
+ for (const c of joints) {
1440
+ const [a, b] = [model.items[c.a].part, model.items[c.b].part];
1441
+ if (!jointAllowed(opts.grammar, adapter, a, b, c.kind, opts.mode ?? "strict"))
1442
+ issues.push({
1443
+ rule: "unseen-joint",
1444
+ items: [c.a, c.b],
1445
+ message: "a joint the exemplar never makes"
1446
+ });
1447
+ }
1448
+ return { ok: issues.length === 0, issues, contacts: joints };
1449
+ }
1450
+
1451
+ // src/brick.ts
1452
+ import sharp from "sharp";
1453
+ function ldrawLibrary(flag) {
1454
+ const dir = typeof flag === "string" ? flag : process.env.LDRAW_DIR;
1455
+ if (!dir)
1456
+ throw new InputError(
1457
+ "Pass --library <ldraw-dir> or set LDRAW_DIR to an unpacked LDraw library (https://library.ldraw.org)."
1458
+ );
1459
+ const root = resolve(dir);
1460
+ if (!existsSync(join(root, "parts")) || !existsSync(join(root, "p")))
1461
+ throw new InputError(`${root} is not an LDraw library: expected parts/ and p/ inside.`);
1462
+ return root;
1463
+ }
1464
+ async function readBrickModel(file, library) {
1465
+ const text = await readFile(resolve(file), "utf8").catch((error) => {
1466
+ if (error.code === "ENOENT") throw new InputError(`No model at ${resolve(file)}.`);
1467
+ throw error;
1468
+ });
1469
+ const model = parseModel(text);
1470
+ if (!model.items.length) throw new InputError(`${file} places no parts.`);
1471
+ const read = (path) => existsSync(path) ? readFileSync(path, "utf8") : void 0;
1472
+ const resolver = libraryResolver(read, library);
1473
+ const lib = {};
1474
+ const names = {};
1475
+ for (const item of model.items) {
1476
+ if (lib[item.part]) continue;
1477
+ const text2 = resolver(`${item.part}.dat`);
1478
+ if (text2 === void 0) throw new InputError(`Part ${item.part} is not in the LDraw library.`);
1479
+ names[item.part] = text2.split(/\r?\n/, 1)[0].replace(/^0\s*/, "").trim();
1480
+ lib[item.part] = loadLegoPart(item.part, resolver, { name: names[item.part] });
1481
+ }
1482
+ const config = read(join(library, "LDConfig.ldr"));
1483
+ return { model, lib, names, palette: config ? parsePalette(config) : {} };
1484
+ }
1485
+ function brickCheck({ model, lib, names }) {
1486
+ const result = validate(model, lib, lego);
1487
+ const label2 = (i) => `${i}:${model.items[i].part} ${names[model.items[i].part]}`;
1488
+ return {
1489
+ ok: result.ok,
1490
+ parts: model.items.length,
1491
+ joints: result.contacts.length,
1492
+ issues: result.issues.map((issue) => ({ ...issue, parts: issue.items.map(label2) }))
1493
+ };
1494
+ }
1495
+ function brickPlan({ model, lib, names }) {
1496
+ const plan = planAssembly(model, lib, lego);
1497
+ const axis = (d) => ["+x", "-x", "+y", "-y", "+z", "-z"][[
1498
+ [1, 0, 0],
1499
+ [-1, 0, 0],
1500
+ [0, 1, 0],
1501
+ [0, -1, 0],
1502
+ [0, 0, 1],
1503
+ [0, 0, -1]
1504
+ ].findIndex((v) => v.every((x, k) => Math.abs(x - d[k]) < 1e-6))] ?? d.join(",");
1505
+ return {
1506
+ ...plan,
1507
+ rows: plan.steps.map((s) => ({
1508
+ step: model.items[s.item].step ?? 0,
1509
+ part: model.items[s.item].part,
1510
+ name: names[model.items[s.item].part],
1511
+ insert: axis(s.insert)
1512
+ }))
1513
+ };
1514
+ }
1515
+ async function brickRender({ model, lib, palette }, opts) {
1516
+ const highlight = opts.step === void 0 ? void 0 : model.items.map((it, i) => it.step === opts.step ? i : -1).filter((i) => i >= 0);
1517
+ if (opts.step !== void 0 && !highlight?.length)
1518
+ throw new InputError(`The model has no step ${opts.step}.`);
1519
+ const image = rasterize(model, lib, {
1520
+ width: 1200,
1521
+ height: 900,
1522
+ palette,
1523
+ background: "#ecebe7",
1524
+ maxStep: opts.step,
1525
+ highlight,
1526
+ explode: opts.explode,
1527
+ yaw: opts.yaw === void 0 ? void 0 : opts.yaw * Math.PI / 180,
1528
+ pitch: opts.pitch === void 0 ? void 0 : opts.pitch * Math.PI / 180
1529
+ });
1530
+ return sharp(Buffer.from(image.data.buffer), {
1531
+ raw: { width: image.width, height: image.height, channels: 4 }
1532
+ }).resize(1200, 900).png().toBuffer();
1533
+ }
1534
+ var MODES = ["strict", "family", "free"];
1535
+ function brickRemix(exemplar, seed, mode) {
1536
+ if (mode !== void 0 && !MODES.includes(mode))
1537
+ throw new InputError(`--mode must be one of ${MODES.join(", ")}.`);
1538
+ const result = remix(exemplar.model, exemplar.lib, lego, {
1539
+ seed,
1540
+ mode
1541
+ });
1542
+ const grown = { ...exemplar, model: result.model };
1543
+ return {
1544
+ ldr: writeModel(result.model, `Remix ${seed}`),
1545
+ left: result.left,
1546
+ check: brickCheck(grown)
1547
+ };
1548
+ }
1549
+
69
1550
  // src/card-import.ts
70
1551
  import { createHash } from "crypto";
71
- import { readFile, stat } from "fs/promises";
1552
+ import { readFile as readFile2, stat } from "fs/promises";
72
1553
  import { basename, extname } from "path";
73
1554
  import { DOMParser } from "@xmldom/xmldom";
74
- import sharp from "sharp";
1555
+ import sharp2 from "sharp";
75
1556
  var box = { x: 0, y: 0, w: 1200, h: 756 };
76
1557
  function elements(node) {
77
1558
  return Array.from(node.childNodes).filter((child) => child.nodeType === 1);
@@ -104,7 +1585,7 @@ async function importCard(file, name) {
104
1585
  const info = await stat(file);
105
1586
  if (!info.isFile() || info.size > 2e6)
106
1587
  throw new InputError("Import a local SVG or raster file below 2 MB.");
107
- const bytes = await readFile(file);
1588
+ const bytes = await readFile2(file);
108
1589
  const title = (name ?? basename(file, extname(file))).slice(0, 60);
109
1590
  const design = {
110
1591
  version: 1,
@@ -118,7 +1599,7 @@ async function importCard(file, name) {
118
1599
  back: { background: "#111111", layers: [] }
119
1600
  };
120
1601
  if (extname(file).toLowerCase() !== ".svg") {
121
- const meta = await sharp(bytes, { limitInputPixels: 24e6 }).metadata();
1602
+ const meta = await sharp2(bytes, { limitInputPixels: 24e6 }).metadata();
122
1603
  if (!meta.format || !["png", "jpeg", "webp"].includes(meta.format))
123
1604
  throw new InputError("Import PNG, JPEG, WebP or a self-contained SVG.");
124
1605
  design.assets = {
@@ -181,11 +1662,11 @@ async function importCard(file, name) {
181
1662
  `This SVG uses elements outside the editable format: ${result.error.issues.map((issue) => issue.message).join("; ")}. Keep and edit the original SVG; it was not changed.`
182
1663
  );
183
1664
  }
184
- design.front.layers = groups.map((children, index) => {
1665
+ design.front.layers = groups.map((children, index2) => {
185
1666
  const first = children[0];
186
1667
  return {
187
- id: `source-${String(index + 1).padStart(2, "0")}`,
188
- name: (grouped ? "Original composition" : first?.attrs?.["aria-label"] ?? first?.attrs?.id ?? first?.text?.trim() ?? `Original ${first?.tag} ${index + 1}`).slice(0, 80),
1668
+ id: `source-${String(index2 + 1).padStart(2, "0")}`,
1669
+ name: (grouped ? "Original composition" : first?.attrs?.["aria-label"] ?? first?.attrs?.id ?? first?.text?.trim() ?? `Original ${first?.tag} ${index2 + 1}`).slice(0, 80),
189
1670
  kind: "svg",
190
1671
  nodes: Object.keys(inherited).length ? [{ tag: "g", attrs: inherited, children }] : children,
191
1672
  ...box,
@@ -198,7 +1679,7 @@ async function importCard(file, name) {
198
1679
  for (const layer of design.front.layers) {
199
1680
  if (layer.kind !== "svg") continue;
200
1681
  const markup = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="756" viewBox="0 0 1200 756"><defs>${defs}</defs>${layer.nodes.map((node) => svgNodeXml(node)).join("")}</svg>`;
201
- const { data, info: pixels } = await sharp(Buffer.from(markup)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1682
+ const { data, info: pixels } = await sharp2(Buffer.from(markup)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
202
1683
  let left = 1200, top = 756, right = -1, bottom = -1;
203
1684
  for (let y = 0; y < pixels.height; y++) {
204
1685
  for (let x = 0; x < pixels.width; x++) {
@@ -213,65 +1694,527 @@ async function importCard(file, name) {
213
1694
  const measured = { x: left, y: top, w: right - left + 1, h: bottom - top + 1 };
214
1695
  Object.assign(layer, measured, { sourceBox: { ...measured } });
215
1696
  }
216
- }
217
- return badgeDesignSchema.parse(design);
1697
+ }
1698
+ return badgeDesignSchema.parse(design);
1699
+ }
1700
+
1701
+ // src/grid.ts
1702
+ var whole = (name, raw) => {
1703
+ const n = Number(raw);
1704
+ if (raw === void 0 || !Number.isInteger(n) || n < 1)
1705
+ throw new InputError(`--${name} must be a whole number of at least 1.`);
1706
+ return n;
1707
+ };
1708
+ function gridStl(kind, values) {
1709
+ const part2 = kind === "bin" ? gridfinityBin(
1710
+ whole("width", values.width),
1711
+ whole("depth", values.depth),
1712
+ whole("height", values.height)
1713
+ ) : gridfinityBaseplate(whole("width", values.width), whole("depth", values.depth));
1714
+ const stl = toStl(
1715
+ { items: [{ part: part2.id, rotation: IDENTITY, position: [0, 0, 0], color: 16 }] },
1716
+ { [part2.id]: part2 },
1717
+ GRIDFINITY_UNIT_MM
1718
+ );
1719
+ const { min, max } = meshBounds(part2.triangles);
1720
+ const mm = (k) => +((max[k] - min[k]) * GRIDFINITY_UNIT_MM).toFixed(2);
1721
+ return {
1722
+ stl,
1723
+ part: part2.id,
1724
+ name: part2.name,
1725
+ triangles: part2.triangles.length / 9,
1726
+ sizeMm: { x: mm(0), y: mm(2), z: mm(1) }
1727
+ };
1728
+ }
1729
+
1730
+ // src/images.ts
1731
+ import { readFile as readFile3, stat as stat2, writeFile } from "fs/promises";
1732
+ import { extname as extname2, resolve as resolve2 } from "path";
1733
+ async function readToolResult(path) {
1734
+ const info = await stat2(path);
1735
+ if (!info.isFile() || info.size > 12e6)
1736
+ throw new InputError("Expected a result file below 12 MB.");
1737
+ let value = JSON.parse(await readFile3(path, "utf8"));
1738
+ if (typeof value === "string") value = JSON.parse(value);
1739
+ if (value && typeof value === "object" && "success" in value) {
1740
+ if (!value.success) throw new InputError("The browser command failed.");
1741
+ value = value.data.output;
1742
+ }
1743
+ if (typeof value === "string") value = JSON.parse(value);
1744
+ if (!value?.ok || !value.data || typeof value.data !== "object")
1745
+ throw new InputError(value?.error?.message || "The tool returned an invalid result.");
1746
+ return value.data;
1747
+ }
1748
+ async function imageParams(file, state, target = "portrait") {
1749
+ const types = {
1750
+ ".png": "image/png",
1751
+ ".jpg": "image/jpeg",
1752
+ ".jpeg": "image/jpeg",
1753
+ ".webp": "image/webp"
1754
+ };
1755
+ const mime = types[extname2(file).toLowerCase()];
1756
+ if (!mime || !["portrait", "artwork"].includes(target))
1757
+ throw new InputError("Use a PNG/JPEG/WebP file and portrait or artwork target.");
1758
+ const info = await stat2(file);
1759
+ if (!info.isFile() || info.size > 4e6)
1760
+ throw new InputError("Image must be smaller than 4 MB.");
1761
+ const current = await readToolResult(state);
1762
+ if (typeof current.revision !== "string")
1763
+ throw new InputError("Inspect the editor to obtain its revision.");
1764
+ const bytes = await readFile3(file);
1765
+ if (bytes.length > 4e6) throw new InputError("Image must be smaller than 4 MB.");
1766
+ return {
1767
+ action: "import",
1768
+ target,
1769
+ expectedRevision: current.revision,
1770
+ dataUrl: `data:${mime};base64,${bytes.toString("base64")}`
1771
+ };
1772
+ }
1773
+ async function extractImage(file, out) {
1774
+ const image = await readToolResult(file);
1775
+ const match = /^data:image\/(?:png|jpeg|webp);base64,([A-Za-z0-9+/]+={0,2})$/.exec(
1776
+ image.dataUrl ?? ""
1777
+ );
1778
+ if (!match || image.dataUrl.length > 56e5)
1779
+ throw new InputError("The tool did not return a supported image.");
1780
+ const bytes = Buffer.from(match[1], "base64");
1781
+ if (bytes.length > 4e6) throw new InputError("Image is larger than 4 MB.");
1782
+ const path = resolve2(out);
1783
+ await writeFile(path, bytes, { flag: "wx", mode: 384 });
1784
+ return { path, bytes: bytes.length };
1785
+ }
1786
+
1787
+ // src/kit-extract.ts
1788
+ import { mkdir, mkdtemp, rename, rm, writeFile as writeFile2 } from "fs/promises";
1789
+ import { dirname, join as join2, resolve as resolve3 } from "path";
1790
+ import sharp3 from "sharp";
1791
+ import { z } from "zod";
1792
+ var regionSchema = z.object({
1793
+ x: z.number().int().nonnegative(),
1794
+ y: z.number().int().nonnegative(),
1795
+ w: z.number().int().positive(),
1796
+ h: z.number().int().positive(),
1797
+ name: z.string().trim().min(1).max(60).optional()
1798
+ }).strict();
1799
+ var optionsSchema = z.object({
1800
+ name: z.string().trim().min(1).max(60).default("Artwork kit"),
1801
+ gap: z.number().int().min(0).max(40).default(12),
1802
+ tolerance: z.number().int().min(0).max(150).default(40),
1803
+ minArea: z.number().int().min(1).max(1e5).default(64),
1804
+ regions: z.array(regionSchema).min(1).max(24).optional()
1805
+ }).strict();
1806
+ function dilate(mask, w, h, radius) {
1807
+ const horizontal = new Uint8Array(mask.length), out = new Uint8Array(mask.length);
1808
+ for (let y = 0; y < h; y++) {
1809
+ let count = 0;
1810
+ for (let x = -radius; x < w; x++) {
1811
+ if (x + radius < w) count += mask[y * w + x + radius];
1812
+ if (x - radius - 1 >= 0) count -= mask[y * w + x - radius - 1];
1813
+ if (x >= 0) horizontal[y * w + x] = count > 0 ? 1 : 0;
1814
+ }
1815
+ }
1816
+ for (let x = 0; x < w; x++) {
1817
+ let count = 0;
1818
+ for (let y = -radius; y < h; y++) {
1819
+ if (y + radius < h) count += horizontal[(y + radius) * w + x];
1820
+ if (y - radius - 1 >= 0) count -= horizontal[(y - radius - 1) * w + x];
1821
+ if (y >= 0) out[y * w + x] = count > 0 ? 1 : 0;
1822
+ }
1823
+ }
1824
+ return out;
1825
+ }
1826
+ function artworkRegions(foreground, w, h, gap, minArea) {
1827
+ const mask = dilate(foreground, w, h, gap), queue = new Int32Array(mask.length);
1828
+ const regions = [];
1829
+ for (let start = 0; start < mask.length; start++) {
1830
+ if (!mask[start]) continue;
1831
+ let head = 0, tail = 1, x0 = w, y0 = h, x1 = -1, y1 = -1, area = 0;
1832
+ queue[0] = start;
1833
+ mask[start] = 0;
1834
+ while (head < tail) {
1835
+ const p = queue[head++], x = p % w, y = Math.floor(p / w);
1836
+ if (foreground[p]) {
1837
+ x0 = Math.min(x0, x);
1838
+ x1 = Math.max(x1, x);
1839
+ y0 = Math.min(y0, y);
1840
+ y1 = Math.max(y1, y);
1841
+ area++;
1842
+ }
1843
+ for (const n of [
1844
+ x > 0 ? p - 1 : -1,
1845
+ x < w - 1 ? p + 1 : -1,
1846
+ y > 0 ? p - w : -1,
1847
+ y < h - 1 ? p + w : -1
1848
+ ])
1849
+ if (n >= 0 && mask[n]) {
1850
+ mask[n] = 0;
1851
+ queue[tail++] = n;
1852
+ }
1853
+ }
1854
+ if (area >= minArea)
1855
+ regions.push({
1856
+ x: Math.max(0, x0 - 2),
1857
+ y: Math.max(0, y0 - 2),
1858
+ w: Math.min(w - 1, x1 + 2) - Math.max(0, x0 - 2) + 1,
1859
+ h: Math.min(h - 1, y1 + 2) - Math.max(0, y0 - 2) + 1
1860
+ });
1861
+ }
1862
+ return regions.sort((a, b) => a.y - b.y || a.x - b.x);
1863
+ }
1864
+ function removeOuterBackground(rgba, w, h, background, tolerance) {
1865
+ const result = new Uint8Array(rgba), seen = new Uint8Array(w * h), queue = new Int32Array(w * h);
1866
+ let head = 0, tail = 0;
1867
+ const add2 = (p) => {
1868
+ if (seen[p]) return;
1869
+ seen[p] = 1;
1870
+ if (rgba[p * 4 + 3] < 16 || [0, 1, 2].reduce((n, c) => n + Math.abs(rgba[p * 4 + c] - background[c]), 0) <= tolerance)
1871
+ queue[tail++] = p;
1872
+ };
1873
+ for (let x = 0; x < w; x++) {
1874
+ add2(x);
1875
+ add2((h - 1) * w + x);
1876
+ }
1877
+ for (let y = 0; y < h; y++) {
1878
+ add2(y * w);
1879
+ add2(y * w + w - 1);
1880
+ }
1881
+ while (head < tail) {
1882
+ const p = queue[head++], x = p % w, y = Math.floor(p / w);
1883
+ result[p * 4 + 3] = 0;
1884
+ if (x) add2(p - 1);
1885
+ if (x < w - 1) add2(p + 1);
1886
+ if (y) add2(p - w);
1887
+ if (y < h - 1) add2(p + w);
1888
+ }
1889
+ return result;
1890
+ }
1891
+ async function extractKit(file, out, input) {
1892
+ const options = optionsSchema.parse(input);
1893
+ const { data, info } = await sharp3(resolve3(file), { limitInputPixels: 24e6 }).rotate().ensureAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
1894
+ const w = info.width, h = info.height;
1895
+ const corners = [0, w - 1, (h - 1) * w, w * h - 1];
1896
+ const transparentBackground = corners.every((p) => data[p * 4 + 3] < 16);
1897
+ const background = [0, 1, 2].map(
1898
+ (c) => Math.round(corners.reduce((n, p) => n + data[p * 4 + c], 0) / 4)
1899
+ );
1900
+ const foreground = new Uint8Array(w * h);
1901
+ for (let p = 0; p < foreground.length; p++)
1902
+ foreground[p] = data[p * 4 + 3] >= 16 && (transparentBackground || [0, 1, 2].reduce((n, c) => n + Math.abs(data[p * 4 + c] - background[c]), 0) > options.tolerance) ? 1 : 0;
1903
+ const regions = options.regions ?? artworkRegions(foreground, w, h, options.gap, options.minArea);
1904
+ if (!regions.length || regions.length > 24)
1905
+ throw new InputError("Choose 1\u201324 regions using --params, or adjust gap/minArea.");
1906
+ if (regions.some((r) => r.x + r.w > w || r.y + r.h > h))
1907
+ throw new InputError("Every region must fit inside the image.");
1908
+ const target = resolve3(out), temporary = await mkdtemp(join2(dirname(target), ".makeables-extract-"));
1909
+ try {
1910
+ await mkdir(join2(temporary, "pieces"));
1911
+ const kit = {
1912
+ format: "makeables-kit",
1913
+ version: 1,
1914
+ name: options.name,
1915
+ items: [],
1916
+ sheets: []
1917
+ };
1918
+ const views = [];
1919
+ for (const [index2, region] of regions.entries()) {
1920
+ const id = `piece-${String(index2 + 1).padStart(2, "0")}`, name = region.name ?? `Piece ${index2 + 1}`;
1921
+ const crop = await sharp3(data, { raw: { width: w, height: h, channels: 4 } }).extract({ left: region.x, top: region.y, width: region.w, height: region.h }).raw().toBuffer();
1922
+ const pixels = transparentBackground ? crop : removeOuterBackground(crop, region.w, region.h, background, options.tolerance);
1923
+ const png = await sharp3(pixels, { raw: { width: region.w, height: region.h, channels: 4 } }).png().toBuffer();
1924
+ const design = createStickerDesign();
1925
+ design.sticker = { ...defaultStickerMaterial, finish: "vinyl", sparkle: 0, holography: 0 };
1926
+ design.name = name;
1927
+ design.source = id;
1928
+ design.assets = {
1929
+ artwork: {
1930
+ source: "image",
1931
+ name,
1932
+ data: `data:image/png;base64,${png.toString("base64")}`,
1933
+ sourcePixels: { width: region.w, height: region.h }
1934
+ }
1935
+ };
1936
+ design.front = {
1937
+ background: "#00000000",
1938
+ layers: [
1939
+ {
1940
+ id: "artwork",
1941
+ kind: "image",
1942
+ asset: "artwork",
1943
+ name,
1944
+ x: 64,
1945
+ y: 64,
1946
+ w: 896,
1947
+ h: 896,
1948
+ fit: "contain",
1949
+ opacity: 1,
1950
+ radius: 0
1951
+ }
1952
+ ]
1953
+ };
1954
+ design.back = { background: "#00000000", layers: [] };
1955
+ await writeFile2(
1956
+ join2(temporary, `pieces/${id}.json`),
1957
+ JSON.stringify(badgeDesignSchema.parse(design), null, 2)
1958
+ );
1959
+ kit.items.push({ id, file: `pieces/${id}.json` });
1960
+ views.push({ name, bytes: png });
1961
+ }
1962
+ await writeFile2(join2(temporary, "kit.json"), JSON.stringify(kit, null, 2));
1963
+ await writeFile2(join2(temporary, "contact.png"), await kitCover(options.name, views));
1964
+ await mkdir(target);
1965
+ await rename(temporary, target);
1966
+ return {
1967
+ path: join2(target, "kit.json"),
1968
+ preview: join2(target, "contact.png"),
1969
+ items: kit.items.length,
1970
+ regions,
1971
+ source: { width: w, height: h },
1972
+ notice: "Review the contact proof. Raster lettering remains part of each image; preserve new text as native layers."
1973
+ };
1974
+ } catch (error) {
1975
+ await rm(temporary, { recursive: true, force: true });
1976
+ throw error;
1977
+ }
1978
+ }
1979
+
1980
+ // src/layers.ts
1981
+ import { randomBytes } from "crypto";
1982
+ import { ZodError } from "zod";
1983
+ var layerKinds = ["text", "shape", "graphic", "qr"];
1984
+ function defaults(kind, width, height) {
1985
+ const centered = (w, h) => ({
1986
+ x: Math.round((width - w) / 2),
1987
+ y: Math.round((height - h) / 2),
1988
+ w,
1989
+ h
1990
+ });
1991
+ if (kind === "text")
1992
+ return {
1993
+ ...centered(Math.round(width * 0.8), 160),
1994
+ text: "Text",
1995
+ binding: "none",
1996
+ color: "#141414",
1997
+ font: "sans",
1998
+ size: 72,
1999
+ weight: "700",
2000
+ align: "center",
2001
+ fit: "shrink"
2002
+ };
2003
+ if (kind === "shape")
2004
+ return {
2005
+ ...centered(Math.round(width * 0.6), Math.round(height * 0.3)),
2006
+ shape: "rectangle",
2007
+ color: "#141414",
2008
+ radius: 0,
2009
+ stroke: 0,
2010
+ opacity: 1
2011
+ };
2012
+ if (kind === "qr") return centered(320, 320);
2013
+ return {
2014
+ ...centered(Math.round(width * 0.6), Math.round(width * 0.6)),
2015
+ pattern: "path",
2016
+ color: "#141414",
2017
+ accent: "#141414",
2018
+ density: 1,
2019
+ seed: 1
2020
+ };
2021
+ }
2022
+ function addLayer(design, face, kind, params = {}) {
2023
+ if (!layerKinds.includes(kind))
2024
+ throw new InputError(
2025
+ `--kind must be one of ${layerKinds.join(", ")}. Add images with --sticker <path>.`
2026
+ );
2027
+ const profile = designProfile(design);
2028
+ const id = typeof params.id === "string" ? params.id : `${kind}-${randomBytes(3).toString("hex")}`;
2029
+ if (design[face].layers.some((layer2) => layer2.id === id))
2030
+ throw new InputError(`Layer ${id} already exists on ${face}. Use layer set to change it.`);
2031
+ const layer = {
2032
+ ...defaults(kind, profile.width, profile.height),
2033
+ ...params,
2034
+ id,
2035
+ kind
2036
+ };
2037
+ if (kind === "graphic" && layer.pattern === "path" && !layer.path)
2038
+ throw new InputError('A path graphic needs "path" (SVG path data) and "viewBox" in --params.');
2039
+ try {
2040
+ const next = badgeDesignSchema.parse({
2041
+ ...design,
2042
+ [face]: { ...design[face], layers: [...design[face].layers, layer] }
2043
+ });
2044
+ const text = typeof layer.text === "string" ? layer.text : "";
2045
+ const unsupported = [
2046
+ ...new Set(
2047
+ [...text].filter((c) => {
2048
+ const code = c.codePointAt(0) ?? 0;
2049
+ return code > 591 && !(code >= 8208 && code <= 8231) && code !== 8364;
2050
+ })
2051
+ )
2052
+ ];
2053
+ const warnings = unsupported.length ? [
2054
+ `The bundled fonts may not draw ${unsupported.join(" ")}. Draw symbols as a graphic path or image layer.`
2055
+ ] : [];
2056
+ return { design: next, id, warnings };
2057
+ } catch (error) {
2058
+ if (error instanceof ZodError)
2059
+ throw new InputError(
2060
+ `Invalid ${kind} layer: ${error.issues.map((issue) => `${issue.path.slice(3).join(".") || "layer"}: ${issue.message}`).join("; ")}. Run makeables schema for every field.`
2061
+ );
2062
+ throw error;
2063
+ }
2064
+ }
2065
+ function blankDesign(product, name) {
2066
+ const title = typeof name === "string" && name.trim() ? name.trim().slice(0, 60) : `Untitled ${product}`;
2067
+ const preset = structuredClone(
2068
+ product === "card" ? findCardStyle("pearl") : findDesign("herbario-azul")
2069
+ );
2070
+ if (!preset) throw new Error(`Missing base ${product} style.`);
2071
+ const base = {
2072
+ ...preset,
2073
+ product,
2074
+ name: title,
2075
+ event: title,
2076
+ description: "",
2077
+ artPrompt: "",
2078
+ source: void 0,
2079
+ artwork: void 0,
2080
+ assets: {}
2081
+ };
2082
+ if (product === "card")
2083
+ return badgeDesignSchema.parse({
2084
+ ...base,
2085
+ front: {
2086
+ background: "#f4f1eb",
2087
+ layers: preset.front.layers.filter((layer) => layer.kind === "card-mark")
2088
+ },
2089
+ back: { background: "#f4f1eb", layers: [] }
2090
+ });
2091
+ const portrait = preset.front.layers.find((layer) => layer.kind === "portrait");
2092
+ const text = (id, binding, y, size, weight = "700") => ({
2093
+ id,
2094
+ kind: "text",
2095
+ x: 96,
2096
+ y,
2097
+ w: 832,
2098
+ h: Math.round(size * 1.3),
2099
+ text: "",
2100
+ binding,
2101
+ color: "#141414",
2102
+ font: "sans",
2103
+ size,
2104
+ weight,
2105
+ align: "center",
2106
+ fit: "shrink"
2107
+ });
2108
+ return badgeDesignSchema.parse({
2109
+ ...base,
2110
+ front: {
2111
+ background: "#f4f1eb",
2112
+ layers: [
2113
+ ...portrait ? [{ ...portrait, x: 192, y: 240, w: 640, h: 640, filter: "original", fade: void 0 }] : [],
2114
+ text("participant-name", "name", 960, 96),
2115
+ text("participant-role", "role", 1100, 48, "500")
2116
+ ]
2117
+ },
2118
+ back: {
2119
+ background: "#f4f1eb",
2120
+ // The back repeats the holder: badges need name and role on both faces.
2121
+ layers: [
2122
+ text("back-name", "name", 200, 72),
2123
+ text("back-role", "role", 310, 40, "500"),
2124
+ { id: "access-qr", kind: "qr", x: 312, y: 520, w: 400, h: 400 },
2125
+ text("event-name", "event", 1e3, 56)
2126
+ ]
2127
+ }
2128
+ });
218
2129
  }
219
2130
 
220
- // src/images.ts
221
- import { readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
222
- import { extname as extname2, resolve } from "path";
223
- async function readToolResult(path) {
224
- const info = await stat2(path);
225
- if (!info.isFile() || info.size > 12e6)
226
- throw new InputError("Expected a result file below 12 MB.");
227
- let value = JSON.parse(await readFile2(path, "utf8"));
228
- if (typeof value === "string") value = JSON.parse(value);
229
- if (value && typeof value === "object" && "success" in value) {
230
- if (!value.success) throw new InputError("The browser command failed.");
231
- value = value.data.output;
2131
+ // src/preflight.ts
2132
+ import { spawnSync } from "child_process";
2133
+ import { mkdtemp as mkdtemp2, readdir, rm as rm2 } from "fs/promises";
2134
+ import { tmpdir } from "os";
2135
+ import { join as join3 } from "path";
2136
+ var generationKeys = [
2137
+ "AI_GATEWAY_API_KEY",
2138
+ "VERCEL_OIDC_TOKEN",
2139
+ "OPENAI_API_KEY",
2140
+ "GOOGLE_GENERATIVE_AI_API_KEY"
2141
+ ];
2142
+ var defaultRun = (command, args) => {
2143
+ const result = spawnSync(command, args, { encoding: "utf8", shell: false, timeout: 12e4 });
2144
+ return {
2145
+ status: result.status,
2146
+ stdout: result.stdout ?? "",
2147
+ stderr: result.stderr ?? "",
2148
+ error: result.error
2149
+ };
2150
+ };
2151
+ function redact(text, env) {
2152
+ let out = text;
2153
+ for (const key2 of generationKeys) {
2154
+ const value = env[key2];
2155
+ if (value && value.length > 4) out = out.split(value).join("[redacted]");
232
2156
  }
233
- if (typeof value === "string") value = JSON.parse(value);
234
- if (!value?.ok || !value.data || typeof value.data !== "object")
235
- throw new InputError(value?.error?.message || "The tool returned an invalid result.");
236
- return value.data;
2157
+ return out.trim().slice(0, 400);
237
2158
  }
238
- async function imageParams(file, state, target = "portrait") {
239
- const types = {
240
- ".png": "image/png",
241
- ".jpg": "image/jpeg",
242
- ".jpeg": "image/jpeg",
243
- ".webp": "image/webp"
2159
+ async function preflight(options = {}) {
2160
+ const env = options.env ?? process.env;
2161
+ const run2 = options.run ?? defaultRun;
2162
+ const version2 = run2("ai", ["--version"]);
2163
+ const aiCli = {
2164
+ installed: !version2.error && version2.status === 0,
2165
+ version: version2.status === 0 ? version2.stdout.trim().slice(0, 40) : null,
2166
+ install: "npm install --global ai-cli"
244
2167
  };
245
- const mime = types[extname2(file).toLowerCase()];
246
- if (!mime || !["portrait", "artwork"].includes(target))
247
- throw new InputError("Use a PNG/JPEG/WebP file and portrait or artwork target.");
248
- const info = await stat2(file);
249
- if (!info.isFile() || info.size > 4e6)
250
- throw new InputError("Image must be smaller than 4 MB.");
251
- const current = await readToolResult(state);
252
- if (typeof current.revision !== "string")
253
- throw new InputError("Inspect the editor to obtain its revision.");
254
- const bytes = await readFile2(file);
255
- if (bytes.length > 4e6) throw new InputError("Image must be smaller than 4 MB.");
2168
+ const keys = generationKeys.filter((key2) => Boolean(env[key2]));
2169
+ let probe = null;
2170
+ if (options.probe) {
2171
+ if (!aiCli.installed) probe = { ok: false, error: "ai-cli is not installed." };
2172
+ else if (!keys.length) probe = { ok: false, error: "No generation key in the environment." };
2173
+ else {
2174
+ const dir = await mkdtemp2(join3(tmpdir(), "makeables-probe-"));
2175
+ try {
2176
+ const result = run2("ai", [
2177
+ "image",
2178
+ "--quiet",
2179
+ "--json",
2180
+ "--no-preview",
2181
+ "--output",
2182
+ dir,
2183
+ "a small flat orange circle on a white background"
2184
+ ]);
2185
+ const made = (await readdir(dir)).length > 0;
2186
+ probe = result.status === 0 && made ? { ok: true } : {
2187
+ ok: false,
2188
+ error: redact(result.stderr || result.stdout || "No image was produced.", env)
2189
+ };
2190
+ } finally {
2191
+ await rm2(dir, { recursive: true, force: true });
2192
+ }
2193
+ }
2194
+ }
2195
+ const cliReady = probe ? probe.ok : aiCli.installed && keys.length > 0;
2196
+ const tracing = run2("potrace", ["--version"]);
256
2197
  return {
257
- action: "import",
258
- target,
259
- expectedRevision: current.revision,
260
- dataUrl: `data:${mime};base64,${bytes.toString("base64")}`
2198
+ imageGeneration: {
2199
+ aiCli,
2200
+ keys,
2201
+ probe,
2202
+ ready: cliReady,
2203
+ recommendation: cliReady ? "generate" : "host-or-install"
2204
+ },
2205
+ potrace: { installed: !tracing.error && tracing.status === 0 },
2206
+ ask: {
2207
+ source: ["official", "generated", "mixed"],
2208
+ defaultSource: "mixed",
2209
+ stickers: { min: 6, max: 10 }
2210
+ }
261
2211
  };
262
2212
  }
263
- async function extractImage(file, out) {
264
- const image = await readToolResult(file);
265
- const match = /^data:image\/(?:png|jpeg|webp);base64,([A-Za-z0-9+/]+={0,2})$/.exec(
266
- image.dataUrl ?? ""
267
- );
268
- if (!match || image.dataUrl.length > 56e5)
269
- throw new InputError("The tool did not return a supported image.");
270
- const bytes = Buffer.from(match[1], "base64");
271
- if (bytes.length > 4e6) throw new InputError("Image is larger than 4 MB.");
272
- const path = resolve(out);
273
- await writeFile(path, bytes, { flag: "wx", mode: 384 });
274
- return { path, bytes: bytes.length };
2213
+ function preflightSummary(result) {
2214
+ const generation = result.imageGeneration;
2215
+ const line = generation.ready ? `Image generation: ready (ai-cli${generation.probe?.ok ? ", probe passed" : ""}).` : generation.probe && !generation.probe.ok ? `Image generation: failed (${generation.probe.error}).` : generation.aiCli.installed ? "Image generation: ai-cli installed, no key. Use the host's image tool or set AI_GATEWAY_API_KEY." : "Image generation: not installed. Use the host's image tool or ask to install ai-cli.";
2216
+ return `${line}
2217
+ Ask the user: art source (official, generated or mixed) and how many stickers (6-10).`;
275
2218
  }
276
2219
 
277
2220
  // src/presentation.ts
@@ -298,11 +2241,11 @@ function banner(version2) {
298
2241
  // src/publish.ts
299
2242
  import { createHash as createHash2, randomUUID } from "crypto";
300
2243
  import { constants } from "fs";
301
- import { appendFile, link, mkdir, open, readFile as readFile3, rename, rm } from "fs/promises";
2244
+ import { appendFile, link, mkdir as mkdir2, open, readFile as readFile4, rename as rename2, rm as rm3 } from "fs/promises";
302
2245
  import { homedir } from "os";
303
- import { dirname, join, resolve as resolve2 } from "path";
304
- import sharp2 from "sharp";
305
- import { z } from "zod";
2246
+ import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
2247
+ import sharp4 from "sharp";
2248
+ import { z as z2 } from "zod";
306
2249
  async function saveBundle(url, out) {
307
2250
  const state = await studioRequest(url, "/call", {
308
2251
  name: "badge_inspect",
@@ -323,7 +2266,7 @@ async function saveBundle(url, out) {
323
2266
  result.error?.message ?? "The editor could not export a bundle. Refresh it after saving the current draft."
324
2267
  );
325
2268
  const pkg = await readPackageJson(result.data);
326
- const path = resolve2(out);
2269
+ const path = resolve4(out);
327
2270
  const file = await open(path, "wx", 384);
328
2271
  try {
329
2272
  await file.writeFile(`${JSON.stringify(packageJson(pkg))}
@@ -332,7 +2275,7 @@ async function saveBundle(url, out) {
332
2275
  } finally {
333
2276
  await file.close();
334
2277
  }
335
- const saved = await readFile3(path);
2278
+ const saved = await readFile4(path);
336
2279
  return {
337
2280
  path,
338
2281
  bytes: saved.length,
@@ -344,55 +2287,55 @@ async function saveBundle(url, out) {
344
2287
  published: false
345
2288
  };
346
2289
  }
347
- var receiptSchema = z.object({
348
- id: z.string().uuid(),
349
- version: z.number().int().positive(),
350
- state: z.enum(["published", "withdrawn"]),
351
- url: z.string()
2290
+ var receiptSchema = z2.object({
2291
+ id: z2.string().uuid(),
2292
+ version: z2.number().int().positive(),
2293
+ state: z2.enum(["published", "withdrawn"]),
2294
+ url: z2.string()
352
2295
  });
353
2296
 
354
2297
  // src/reference-image.ts
355
- import { spawnSync } from "child_process";
356
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
357
- import { tmpdir } from "os";
358
- import { join as join2, resolve as resolve3 } from "path";
2298
+ import { spawnSync as spawnSync2 } from "child_process";
2299
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
2300
+ import { tmpdir as tmpdir2 } from "os";
2301
+ import { join as join5, resolve as resolve5 } from "path";
359
2302
  import { DOMParser as DOMParser2, XMLSerializer } from "@xmldom/xmldom";
360
- import sharp3 from "sharp";
361
- import { z as z2 } from "zod";
2303
+ import sharp5 from "sharp";
2304
+ import { z as z3 } from "zod";
362
2305
  var W = 1200;
363
2306
  var H = 756;
364
2307
  var N = W * H;
365
- var optionSchema = z2.object({
366
- crop: z2.string().optional(),
367
- patch: z2.array(z2.string()).default([]),
368
- "edge-band": z2.coerce.number().min(0).max(1e3).optional(),
369
- "bg-tol": z2.coerce.number().min(0).max(765).optional(),
370
- title: z2.string().min(1).max(200).optional(),
371
- desc: z2.string().max(5e3).optional(),
372
- credit: z2.string().max(5e3).optional(),
373
- tones: z2.coerce.number().int().min(2).max(12).optional(),
374
- ink: z2.coerce.number().min(0).max(1).optional(),
375
- raster: z2.boolean().optional(),
376
- points: z2.array(z2.string()).max(100).optional(),
377
- box: z2.string().optional(),
378
- rows: z2.string().optional(),
379
- cols: z2.string().optional(),
380
- dark: z2.coerce.number().min(0).max(765).optional(),
381
- colors: z2.coerce.number().int().min(1).max(12).optional(),
382
- at: z2.string().optional(),
383
- up: z2.coerce.number().int().min(1).max(8).optional(),
384
- k: z2.coerce.number().min(0).max(1).optional(),
385
- fill: z2.string().regex(/^#[a-fA-F0-9]{6}$/).optional(),
386
- top: z2.enum(["left", "right"]).optional(),
387
- upright: z2.enum(["left", "right"]).optional(),
388
- "ref-box": z2.string().optional()
2308
+ var optionSchema = z3.object({
2309
+ crop: z3.string().optional(),
2310
+ patch: z3.array(z3.string()).default([]),
2311
+ "edge-band": z3.coerce.number().min(0).max(1e3).optional(),
2312
+ "bg-tol": z3.coerce.number().min(0).max(765).optional(),
2313
+ title: z3.string().min(1).max(200).optional(),
2314
+ desc: z3.string().max(5e3).optional(),
2315
+ credit: z3.string().max(5e3).optional(),
2316
+ tones: z3.coerce.number().int().min(2).max(12).optional(),
2317
+ ink: z3.coerce.number().min(0).max(1).optional(),
2318
+ raster: z3.boolean().optional(),
2319
+ points: z3.array(z3.string()).max(100).optional(),
2320
+ box: z3.string().optional(),
2321
+ rows: z3.string().optional(),
2322
+ cols: z3.string().optional(),
2323
+ dark: z3.coerce.number().min(0).max(765).optional(),
2324
+ colors: z3.coerce.number().int().min(1).max(12).optional(),
2325
+ at: z3.string().optional(),
2326
+ up: z3.coerce.number().int().min(1).max(8).optional(),
2327
+ k: z3.coerce.number().min(0).max(1).optional(),
2328
+ fill: z3.string().regex(/^#[a-fA-F0-9]{6}$/).optional(),
2329
+ top: z3.enum(["left", "right"]).optional(),
2330
+ upright: z3.enum(["left", "right"]).optional(),
2331
+ "ref-box": z3.string().optional()
389
2332
  }).strict();
390
2333
  function fail(message) {
391
2334
  throw new InputError(message);
392
2335
  }
393
2336
  var nums = (value, count, name) => {
394
2337
  const parts = value.split(",").map(Number);
395
- if (value.split(",").some((part) => !part.trim())) fail(`${name} contains an empty coordinate.`);
2338
+ if (value.split(",").some((part2) => !part2.trim())) fail(`${name} contains an empty coordinate.`);
396
2339
  if (parts.length < count || parts.some((v) => !Number.isFinite(v)))
397
2340
  fail(`${name} needs ${count} comma-separated numbers.`);
398
2341
  return parts;
@@ -404,7 +2347,7 @@ function validBox(values, width, height) {
404
2347
  }
405
2348
  var lumOf = (r, g, b) => 0.299 * r + 0.587 * g + 0.114 * b;
406
2349
  async function detect(file) {
407
- const { data, info } = await sharp3(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
2350
+ const { data, info } = await sharp5(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
408
2351
  const { width: w, height: h } = info;
409
2352
  const px = (x, y) => {
410
2353
  const i = (y * w + x) * 3;
@@ -480,7 +2423,7 @@ async function detect(file) {
480
2423
  async function prepare(file, o) {
481
2424
  if (!o.crop || !o.out) fail('prepare needs --out <dir> and --params with {"crop":"x,y,w,h"}.');
482
2425
  const [cx, cy, cw, ch] = nums(o.crop, 4, "crop");
483
- const meta = await sharp3(file, { limitInputPixels: 24e6 }).metadata();
2426
+ const meta = await sharp5(file, { limitInputPixels: 24e6 }).metadata();
484
2427
  validBox([cx, cy, cw, ch], meta.width ?? 0, meta.height ?? 0);
485
2428
  if (Math.abs(cw / ch / (W / H) - 1) > 0.03)
486
2429
  fail(
@@ -493,12 +2436,12 @@ async function prepare(file, o) {
493
2436
  if (feather < 0 || ![0, 1].includes(flop))
494
2437
  fail("Patch feather must be nonnegative and flop must be 0 or 1.");
495
2438
  }
496
- const out = resolve3(o.out);
2439
+ const out = resolve5(o.out);
497
2440
  mkdirSync(out);
498
2441
  const layers = [];
499
2442
  for (const spec of o.patch) {
500
2443
  const [dx, dy, pw, ph, sx, sy, feather = 6, flop = 0] = nums(spec, 6, "patch");
501
- const patch = await sharp3(file).extract({ left: sx, top: sy, width: pw, height: ph }).flop(!!flop).toColourspace("srgb").removeAlpha().raw().toBuffer();
2444
+ const patch = await sharp5(file).extract({ left: sx, top: sy, width: pw, height: ph }).flop(!!flop).toColourspace("srgb").removeAlpha().raw().toBuffer();
502
2445
  const rgba = Buffer.alloc(pw * ph * 4);
503
2446
  for (let y = 0; y < ph; y++)
504
2447
  for (let x = 0; x < pw; x++) {
@@ -510,10 +2453,10 @@ async function prepare(file, o) {
510
2453
  }
511
2454
  layers.push({ input: rgba, raw: { width: pw, height: ph, channels: 4 }, left: dx, top: dy });
512
2455
  }
513
- const patched = await sharp3(file).removeAlpha().composite(layers).png().toBuffer();
514
- const card = await sharp3(patched).extract({ left: cx, top: cy, width: cw, height: ch }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
2456
+ const patched = await sharp5(file).removeAlpha().composite(layers).png().toBuffer();
2457
+ const card = await sharp5(patched).extract({ left: cx, top: cy, width: cw, height: ch }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
515
2458
  const px = card.data, CW = card.info.width, CH = card.info.height;
516
- const src = await sharp3(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
2459
+ const src = await sharp5(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
517
2460
  const sp = (x, y) => {
518
2461
  const i = (y * src.info.width + x) * 3;
519
2462
  return [src.data[i], src.data[i + 1], src.data[i + 2]];
@@ -553,14 +2496,14 @@ async function prepare(file, o) {
553
2496
  if (y > 0) queue.push(i - CW);
554
2497
  if (y < CH - 1) queue.push(i + CW);
555
2498
  }
556
- const cardPng = await sharp3(px, { raw: card.info }).png().toBuffer();
557
- const resized = sharp3(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
558
- await resized.clone().png().toFile(join2(out, "card.png"));
559
- await resized.clone().jpeg({ quality: 84, mozjpeg: true }).toFile(join2(out, "card.jpg"));
560
- const rgb = await resized.clone().toColourspace("srgb").raw().toBuffer();
2499
+ const cardPng = await sharp5(px, { raw: card.info }).png().toBuffer();
2500
+ const resized = sharp5(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
2501
+ await resized.clone().png().toFile(join5(out, "card.png"));
2502
+ await resized.clone().jpeg({ quality: 84, mozjpeg: true }).toFile(join5(out, "card.jpg"));
2503
+ const rgb2 = await resized.clone().toColourspace("srgb").raw().toBuffer();
561
2504
  const g = await resized.clone().toColourspace("b-w").raw().toBuffer();
562
- writeFileSync(join2(out, "rgb.raw"), rgb);
563
- writeFileSync(join2(out, "lum.raw"), g);
2505
+ writeFileSync(join5(out, "rgb.raw"), rgb2);
2506
+ writeFileSync(join5(out, "lum.raw"), g);
564
2507
  const GW = W * 2, GH = H * 2, big = Buffer.alloc(GW * GH);
565
2508
  for (let y = 0; y < GH; y++) {
566
2509
  const fy = Math.min(H - 1, Math.max(0, (y + 0.5) / 2 - 0.5)), y0 = Math.floor(fy), y1 = Math.min(H - 1, y0 + 1), ty = fy - y0;
@@ -572,14 +2515,14 @@ async function prepare(file, o) {
572
2515
  );
573
2516
  }
574
2517
  }
575
- writeFileSync(join2(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
2518
+ writeFileSync(join5(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
576
2519
  ${GW} ${GH}
577
2520
  255
578
2521
  `), big]));
579
2522
  const hex = paper.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
580
2523
  return {
581
2524
  out,
582
- preview: join2(out, "card.png"),
2525
+ preview: join5(out, "card.png"),
583
2526
  paper: `#${hex}`,
584
2527
  patches: o.patch.length,
585
2528
  filledEdgePixels: filled,
@@ -587,7 +2530,7 @@ ${GW} ${GH}
587
2530
  };
588
2531
  }
589
2532
  function potrace(input, extra) {
590
- const r = spawnSync(
2533
+ const r = spawnSync2(
591
2534
  "potrace",
592
2535
  [input, "-b", "svg", "-o", "-", "-W", `${W}pt`, "-H", `${H}pt`, ...extra],
593
2536
  { maxBuffer: 64 << 20 }
@@ -604,11 +2547,11 @@ function potrace(input, extra) {
604
2547
  }
605
2548
  var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
606
2549
  function vectorize(dir, o) {
607
- dir = resolve3(dir);
2550
+ dir = resolve5(dir);
608
2551
  if (!o.out || !o.title) fail("vectorize needs --out <file.svg> and --title");
609
2552
  const K = Number(o.tones ?? 7);
610
- const lum = readFileSync(join2(dir, "lum.raw")), rgb = readFileSync(join2(dir, "rgb.raw"));
611
- if (lum.length !== N || rgb.length !== N * 3)
2553
+ const lum = readFileSync2(join5(dir, "lum.raw")), rgb2 = readFileSync2(join5(dir, "rgb.raw"));
2554
+ if (lum.length !== N || rgb2.length !== N * 3)
612
2555
  fail("Expected an intact directory created by image prepare.");
613
2556
  let c = [...Array(K)].map((_, i) => 30 + i * 200 / (K - 1));
614
2557
  const band = new Uint8Array(N);
@@ -646,9 +2589,9 @@ function vectorize(dir, o) {
646
2589
  let r = 0, g = 0, b = 0, n = 0;
647
2590
  for (let i = 0; i < N; i++)
648
2591
  if (band[i] === k) {
649
- r += rgb[i * 3];
650
- g += rgb[i * 3 + 1];
651
- b += rgb[i * 3 + 2];
2592
+ r += rgb2[i * 3];
2593
+ g += rgb2[i * 3 + 1];
2594
+ b += rgb2[i * 3 + 2];
652
2595
  n++;
653
2596
  }
654
2597
  const h = (v) => Math.round(v / Math.max(n, 1)).toString(16).padStart(2, "0");
@@ -657,11 +2600,11 @@ function vectorize(dir, o) {
657
2600
  const L = used.length;
658
2601
  const inkK = o.ink !== void 0 ? Number(o.ink) : L > 1 ? (c[used[0]] + c[used[1]]) / 2 / 255 : 0.25;
659
2602
  const groups = [];
660
- const maskFile = join2(dir, "mask.pbm");
2603
+ const maskFile = join5(dir, "mask.pbm");
661
2604
  for (let j = o.raster ? 0 : L - 2; j >= 0; j--) {
662
2605
  let traced;
663
2606
  if (j === 0) {
664
- traced = potrace(join2(dir, "ink.pgm"), [
2607
+ traced = potrace(join5(dir, "ink.pgm"), [
665
2608
  "-k",
666
2609
  inkK.toFixed(4),
667
2610
  "-t",
@@ -684,7 +2627,7 @@ ${W} ${H}
684
2627
  if (traced) groups.push(`<g transform="${traced.tr}" fill="${colors[j]}">${traced.paths}</g>`);
685
2628
  }
686
2629
  const base = o.raster ? `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>
687
- <image width="${W}" height="${H}" preserveAspectRatio="none" href="data:image/jpeg;base64,${readFileSync(join2(dir, "card.jpg")).toString("base64")}"/>` : `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>`;
2630
+ <image width="${W}" height="${H}" preserveAspectRatio="none" href="data:image/jpeg;base64,${readFileSync2(join5(dir, "card.jpg")).toString("base64")}"/>` : `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>`;
688
2631
  const title = esc(o.title), desc = esc(o.desc ?? `${o.title} card artwork.`);
689
2632
  const meta = esc(o.credit ?? "Original artwork traced from a reference image.");
690
2633
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="${desc}">
@@ -697,9 +2640,9 @@ ${groups.join("\n")}
697
2640
  `;
698
2641
  const checked = checkSubmittedSvg(svg);
699
2642
  if (!checked.ok) fail(checked.error);
700
- writeFileSync(resolve3(o.out), svg, { flag: "wx" });
2643
+ writeFileSync(resolve5(o.out), svg, { flag: "wx" });
701
2644
  return {
702
- out: resolve3(o.out),
2645
+ out: resolve5(o.out),
703
2646
  bytes: Buffer.byteLength(svg),
704
2647
  mode: o.raster ? "raster background + vector ink" : "pure vector",
705
2648
  tones: colors,
@@ -707,7 +2650,7 @@ ${groups.join("\n")}
707
2650
  };
708
2651
  }
709
2652
  async function rawImage(file) {
710
- const { data, info } = await sharp3(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
2653
+ const { data, info } = await sharp5(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
711
2654
  const px = (x, y) => {
712
2655
  if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= info.width || y >= info.height)
713
2656
  fail("Pixel coordinates are outside the image.");
@@ -806,19 +2749,19 @@ async function glyphs(file, o) {
806
2749
  if (!o.box || !o.at) fail("glyphs needs --box x,y,w,h and --at X,Y");
807
2750
  const [bx, by, bw, bh] = nums(o.box, 4, "box");
808
2751
  const [ax, ay] = nums(o.at, 2, "at");
809
- const meta = await sharp3(file, { limitInputPixels: 24e6 }).metadata();
2752
+ const meta = await sharp5(file, { limitInputPixels: 24e6 }).metadata();
810
2753
  validBox([bx, by, bw, bh], meta.width ?? 0, meta.height ?? 0);
811
2754
  const up = Number(o.up ?? 4);
812
2755
  if (bw * bh * up * up > 24e6)
813
2756
  fail("Trace region is too large; lower up or choose a smaller box.");
814
- const { data, info } = await sharp3(file).extract({ left: bx, top: by, width: bw, height: bh }).greyscale().removeAlpha().resize({ width: bw * up, kernel: "lanczos3" }).raw().toBuffer({ resolveWithObject: true });
815
- const temp = mkdtempSync(join2(tmpdir(), "makeables-glyphs-"));
816
- const pgm = join2(temp, "glyphs.pgm");
2757
+ const { data, info } = await sharp5(file).extract({ left: bx, top: by, width: bw, height: bh }).greyscale().removeAlpha().resize({ width: bw * up, kernel: "lanczos3" }).raw().toBuffer({ resolveWithObject: true });
2758
+ const temp = mkdtempSync(join5(tmpdir2(), "makeables-glyphs-"));
2759
+ const pgm = join5(temp, "glyphs.pgm");
817
2760
  writeFileSync(pgm, Buffer.concat([Buffer.from(`P5
818
2761
  ${info.width} ${info.height}
819
2762
  255
820
2763
  `), data]));
821
- const r = spawnSync(
2764
+ const r = spawnSync2(
822
2765
  "potrace",
823
2766
  [pgm, "-b", "svg", "-o", "-", "-k", String(o.k ?? 0.5), "-t", "2", "-a", "1", "-O", "0.3"],
824
2767
  { maxBuffer: 64 << 20 }
@@ -832,18 +2775,18 @@ ${info.width} ${info.height}
832
2775
  const paths = [...svg.matchAll(/ d="([^"]+)"/g)].map((m) => m[1].replace(/\s+/g, " ")).join(" ");
833
2776
  if (!tr || !paths) fail("nothing dark enough to trace in that box; lower --k or check the box");
834
2777
  const snippet = `<g transform="translate(${ax} ${ay}) scale(${+(1 / up).toFixed(6)})"><path transform="${tr}" fill="${o.fill ?? "#0a0a0a"}" fill-rule="evenodd" d="${paths}"/></g>`;
835
- if (o.out) writeFileSync(resolve3(o.out), snippet, { flag: "wx" });
2778
+ if (o.out) writeFileSync(resolve5(o.out), snippet, { flag: "wx" });
836
2779
  return {
837
2780
  box: { x: bx, y: by, w: bw, h: bh },
838
2781
  at: { x: ax, y: ay },
839
2782
  bytes: snippet.length,
840
- out: o.out ? resolve3(o.out) : null,
2783
+ out: o.out ? resolve5(o.out) : null,
841
2784
  snippet: o.out ? void 0 : snippet
842
2785
  };
843
2786
  }
844
2787
  function rotate(file, o) {
845
2788
  if (!o.out) fail("rotate needs --out <card.svg>");
846
- const text = readFileSync(file, "utf8");
2789
+ const text = readFileSync2(file, "utf8");
847
2790
  const parser = new DOMParser2({ onError: (_level, message) => fail(`Malformed SVG: ${message}`) });
848
2791
  if (/<!DOCTYPE|<!ENTITY/i.test(text)) fail("SVG declarations are not supported.");
849
2792
  const root = parser.parseFromString(text, "image/svg+xml").documentElement;
@@ -859,28 +2802,28 @@ function rotate(file, o) {
859
2802
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}"><g transform="${transform}"><g${inherited}>${body}</g></g></svg>`;
860
2803
  const checked = checkSubmittedSvg(svg);
861
2804
  if (!checked.ok) fail(checked.error);
862
- writeFileSync(resolve3(o.out), svg, { flag: "wx" });
863
- return { out: resolve3(o.out), transform, bytes: Buffer.byteLength(svg) };
2805
+ writeFileSync(resolve5(o.out), svg, { flag: "wx" });
2806
+ return { out: resolve5(o.out), transform, bytes: Buffer.byteLength(svg) };
864
2807
  }
865
2808
  async function compare(file, ref, o) {
866
2809
  if (!o.out) fail("compare needs --out <file.png>");
867
- let card = sharp3(readFileSync(file));
2810
+ let card = sharp5(readFileSync2(file));
868
2811
  if (o.upright) card = card.rotate(o.upright === "left" ? 90 : -90);
869
2812
  const cardPng = await card.png().toBuffer();
870
- let reference = sharp3(ref);
2813
+ let reference = sharp5(ref);
871
2814
  if (o["ref-box"]) {
872
2815
  const [x, y, w, h] = nums(o["ref-box"], 4, "ref-box");
873
2816
  reference = reference.extract({ left: x, top: y, width: w, height: h });
874
2817
  }
875
2818
  const height = 900;
876
- const a = await sharp3(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
877
- const b = await sharp3(await reference.png().toBuffer()).resize({ height }).png().toBuffer({ resolveWithObject: true });
2819
+ const a = await sharp5(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
2820
+ const b = await sharp5(await reference.png().toBuffer()).resize({ height }).png().toBuffer({ resolveWithObject: true });
878
2821
  const gap = 24, width = a.info.width + b.info.width + gap * 3;
879
- await sharp3({ create: { width, height: height + gap * 2, channels: 3, background: "#ffffff" } }).composite([
2822
+ await sharp5({ create: { width, height: height + gap * 2, channels: 3, background: "#ffffff" } }).composite([
880
2823
  { input: a.data, left: gap, top: gap },
881
2824
  { input: b.data, left: a.info.width + gap * 2, top: gap }
882
- ]).png().toFile(resolve3(o.out));
883
- return { out: resolve3(o.out), left: "card", right: "reference" };
2825
+ ]).png().toFile(resolve5(o.out));
2826
+ return { out: resolve5(o.out), left: "card", right: "reference" };
884
2827
  }
885
2828
  var imageTools = [
886
2829
  "detect",
@@ -900,8 +2843,8 @@ async function referenceImage(command, file, options, out, reference) {
900
2843
  parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
901
2844
  );
902
2845
  const o = { ...parsed.data, out };
903
- if (out && existsSync(resolve3(out))) fail("Output already exists. Choose a new --out path.");
904
- if (!existsSync(file)) fail("Input file does not exist.");
2846
+ if (out && existsSync2(resolve5(out))) fail("Output already exists. Choose a new --out path.");
2847
+ if (!existsSync2(file)) fail("Input file does not exist.");
905
2848
  switch (command) {
906
2849
  case "detect":
907
2850
  return detect(file);
@@ -928,180 +2871,11 @@ async function referenceImage(command, file, options, out, reference) {
928
2871
  }
929
2872
  }
930
2873
 
931
- // src/sheet.ts
932
- import { readFile as readFile4 } from "fs/promises";
933
- import { resolve as resolve4 } from "path";
934
- import { deflateSync } from "zlib";
935
- import { createCanvas } from "@napi-rs/canvas";
936
- import sharp4 from "sharp";
937
- async function readSheet(file) {
938
- const value = JSON.parse(await readFile4(resolve4(file), "utf8"));
939
- const parsed = stickerSheetSchema.safeParse(value);
940
- if (!parsed.success)
941
- throw new InputError(
942
- `Invalid sheet: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`
943
- );
944
- return parsed.data;
945
- }
946
- function newSheet(type, size, name) {
947
- if (typeof type !== "string" || !sheetTypeIds.includes(type))
948
- throw new InputError(`--type must be one of ${sheetTypeIds.join(", ")}.`);
949
- const sheet = createStickerSheet(
950
- type,
951
- typeof name === "string" ? name : void 0
952
- );
953
- if (size !== void 0) {
954
- if (typeof size !== "string" || !(size in sheetSizes))
955
- throw new InputError(`--size must be one of ${Object.keys(sheetSizes).join(", ")}.`);
956
- sheet.size = size;
957
- }
958
- return parse(sheet);
959
- }
960
- function parse(sheet) {
961
- const parsed = stickerSheetSchema.safeParse(sheet);
962
- if (!parsed.success)
963
- throw new InputError(parsed.error.issues.map((issue) => issue.message).join("; "));
964
- return parsed.data;
965
- }
966
- async function readSticker(source) {
967
- const style2 = stickerStyles.find((entry) => entry.source === source);
968
- if (style2) return badgeDesignSchema.parse(structuredClone(style2));
969
- const design = badgeDesignSchema.parse(JSON.parse(await readFile4(resolve4(source), "utf8")));
970
- if (design.product !== "sticker") throw new InputError(`${source} is not a sticker design.`);
971
- return design;
972
- }
973
- var artCache = /* @__PURE__ */ new Map();
974
- function flatArt(design) {
975
- const key = JSON.stringify(design);
976
- let art = artCache.get(key);
977
- if (!art) {
978
- art = renderBadge(design, { face: "front", person: {} }).then(async ({ png }) => {
979
- const { data, info } = await sharp4(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
980
- return {
981
- width: info.width,
982
- height: info.height,
983
- rgba: new Uint8ClampedArray(data.buffer, data.byteOffset, data.length)
984
- };
985
- });
986
- artCache.set(key, art);
987
- }
988
- return art;
989
- }
990
- async function sheetArt(sheet) {
991
- return new Map(
992
- await Promise.all(
993
- sheet.items.map(async (item) => [item.id, await flatArt(item.design)])
994
- )
995
- );
996
- }
997
- function bounds(sheet) {
998
- const spec = sheetSpec(sheet);
999
- return { widthMm: spec.widthMm ?? 0, heightMm: spec.heightMm ?? 0 };
1000
- }
1001
- async function addToSheet(sheet, source, widthMm) {
1002
- const type = sheetTypes[sheet.type];
1003
- if (type.cut === "die-cut" && sheet.items.length)
1004
- throw new InputError("A single sticker file holds one sticker. Start another sheet.");
1005
- const width = widthMm === void 0 ? type.stickerWidthMm : Number(widthMm);
1006
- if (!Number.isFinite(width) || width < 5 || width > 300)
1007
- throw new InputError("--width-mm must be a number from 5 to 300.");
1008
- const design = await readSticker(source);
1009
- const base = (design.source ?? "sticker").replace(/[^a-z0-9-]/g, "").slice(0, 40) || "sticker";
1010
- let id = base;
1011
- for (let n = 2; sheet.items.some((item2) => item2.id === id); n++) id = `${base}-${n}`;
1012
- const spec = sheetSpec(sheet);
1013
- const item = {
1014
- id,
1015
- design,
1016
- x: (spec.widthMm ?? width) / 2,
1017
- y: (spec.heightMm ?? width) / 2,
1018
- rotation: 0,
1019
- widthMm: width
1020
- };
1021
- const next = parse({ ...sheet, items: [...sheet.items, item] });
1022
- if (type.cut === "die-cut") return { sheet: next, item, placed: true };
1023
- const print = prepareSheetPrint(next, await sheetArt(next));
1024
- const moved = placeNear(
1025
- spec,
1026
- bounds(next),
1027
- next.items.map((entry) => ({ item: entry, shape: print.shapes.get(entry.id) })),
1028
- id,
1029
- [item.x, item.y]
1030
- );
1031
- if (moved) next.items[next.items.length - 1] = moved;
1032
- return { sheet: next, item: moved ?? item, placed: Boolean(moved) };
1033
- }
1034
- async function packStickerSheet(sheet) {
1035
- if (sheetTypes[sheet.type].cut === "die-cut") return { sheet, missing: [] };
1036
- const print = prepareSheetPrint(sheet, await sheetArt(sheet));
1037
- const packed = packSheet(
1038
- sheetSpec(sheet),
1039
- bounds(sheet),
1040
- sheet.items.map((item) => ({ item, shape: print.shapes.get(item.id) })),
1041
- sheet.hero
1042
- );
1043
- return { sheet: parse({ ...sheet, items: packed.items }), missing: packed.missing };
1044
- }
1045
- async function checkSheet(sheet) {
1046
- return prepareSheetPrint(sheet, await sheetArt(sheet));
1047
- }
1048
- function sheetReport(print) {
1049
- return {
1050
- printable: print.printable,
1051
- widthMm: Number(print.widthMm.toFixed(2)),
1052
- heightMm: Number(print.heightMm.toFixed(2)),
1053
- stickers: print.items.length,
1054
- violations: print.violations,
1055
- issues: print.issues.map(({ id, issue }) => ({ id, ...issue }))
1056
- };
1057
- }
1058
- async function exportSheetPdf(sheet, print) {
1059
- return sheetPrintPdf(sheet, print, async (bytes) => new Uint8Array(deflateSync(bytes)));
1060
- }
1061
- function sheetPreviewPng(print, dpi = 150) {
1062
- const scale = dpi / 25.4;
1063
- const canvas = createCanvas(
1064
- Math.round(print.widthMm * scale),
1065
- Math.round(print.heightMm * scale)
1066
- );
1067
- const context = canvas.getContext("2d");
1068
- context.fillStyle = "#f4f3ef";
1069
- context.fillRect(0, 0, canvas.width, canvas.height);
1070
- for (const item of print.pdfItems) {
1071
- const art = createCanvas(item.art.width, item.art.height);
1072
- const data = art.getContext("2d").createImageData(item.art.width, item.art.height);
1073
- data.data.set(item.art.rgba);
1074
- art.getContext("2d").putImageData(data, 0, 0);
1075
- context.save();
1076
- context.translate(item.x * scale, item.y * scale);
1077
- context.rotate(item.rotation * Math.PI / 180);
1078
- context.drawImage(
1079
- art,
1080
- -item.widthMm / 2 * scale,
1081
- -item.heightMm / 2 * scale,
1082
- item.widthMm * scale,
1083
- item.heightMm * scale
1084
- );
1085
- context.restore();
1086
- context.strokeStyle = "#ff00ff";
1087
- context.lineWidth = Math.max(1, 0.2 * scale);
1088
- context.beginPath();
1089
- for (const loop of item.cut) {
1090
- for (const [i, [x, y]] of loop.entries())
1091
- if (i) context.lineTo(x * scale, y * scale);
1092
- else context.moveTo(x * scale, y * scale);
1093
- context.closePath();
1094
- }
1095
- context.stroke();
1096
- }
1097
- return canvas.toBuffer("image/png");
1098
- }
1099
-
1100
2874
  // src/sticker-cut.ts
1101
- import sharp5 from "sharp";
2875
+ import sharp6 from "sharp";
1102
2876
  async function stickerPrintCut(design) {
1103
2877
  const { png } = await renderBadge(design, { face: "front", person: {} });
1104
- const { data, info } = await sharp5(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
2878
+ const { data, info } = await sharp6(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1105
2879
  const material = design.sticker ?? defaultStickerMaterial;
1106
2880
  const cut = stickerCut(
1107
2881
  new Uint8ClampedArray(data.buffer, data.byteOffset, data.length),
@@ -1116,7 +2890,7 @@ async function stickerPrintCut(design) {
1116
2890
  // src/sticker-material.ts
1117
2891
  import { readFile as readFile5, stat as stat3 } from "fs/promises";
1118
2892
  import { basename as basename2 } from "path";
1119
- import sharp6 from "sharp";
2893
+ import sharp7 from "sharp";
1120
2894
  async function bindStickerMap(design, layerId, path) {
1121
2895
  if (design.product !== "sticker") throw new InputError("Optical maps require a sticker.");
1122
2896
  const layer = design.front.layers.find((layer2) => layer2.id === layerId);
@@ -1125,7 +2899,7 @@ async function bindStickerMap(design, layerId, path) {
1125
2899
  if (!info.isFile() || info.size > 4e6)
1126
2900
  throw new InputError("Use an optical image below 4 MB.");
1127
2901
  const bytes = await readFile5(path);
1128
- const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
2902
+ const meta = await sharp7(bytes, { limitInputPixels: 16777216 }).metadata();
1129
2903
  if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
1130
2904
  throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
1131
2905
  const asset = design.sticker?.effectMap?.asset ?? "optical-map";
@@ -1150,14 +2924,49 @@ async function bindStickerMap(design, layerId, path) {
1150
2924
  });
1151
2925
  }
1152
2926
 
2927
+ // src/trace.ts
2928
+ import { constants as constants2 } from "fs";
2929
+ import { open as open2 } from "fs/promises";
2930
+ async function appendTrace(path, event) {
2931
+ const file = await open2(
2932
+ path,
2933
+ constants2.O_APPEND | constants2.O_CREAT | constants2.O_WRONLY | constants2.O_NOFOLLOW,
2934
+ 384
2935
+ );
2936
+ try {
2937
+ if (!(await file.stat()).isFile()) throw new Error("Choose a regular trace file.");
2938
+ await file.chmod(384);
2939
+ await file.writeFile(`${JSON.stringify(event)}
2940
+ `);
2941
+ } finally {
2942
+ await file.close();
2943
+ }
2944
+ }
2945
+ function compactReceipt(value) {
2946
+ if (Array.isArray(value)) return value.map(compactReceipt);
2947
+ if (value && typeof value === "object")
2948
+ return Object.fromEntries(
2949
+ Object.entries(value).map(([key2, entry]) => [
2950
+ key2,
2951
+ key2 === "design" && entry && typeof entry === "object" ? {
2952
+ name: "name" in entry ? entry.name : void 0,
2953
+ product: "product" in entry ? entry.product : "badge",
2954
+ omitted: true
2955
+ } : compactReceipt(entry)
2956
+ ])
2957
+ );
2958
+ if (typeof value === "string" && value.length > 2e3) return "[omitted; inspect the saved file]";
2959
+ return value;
2960
+ }
2961
+
1153
2962
  // src/wallet.ts
1154
2963
  import { spawn } from "child_process";
1155
2964
  import { createHash as createHash3 } from "crypto";
1156
- import { existsSync as existsSync2 } from "fs";
1157
- import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
1158
- import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
1159
- import { dirname as dirname3, extname as extname3, join as join6, resolve as resolve6 } from "path";
1160
- import sharp7 from "sharp";
2965
+ import { existsSync as existsSync3 } from "fs";
2966
+ import { chmod as chmod2, lstat as lstat3, mkdir as mkdir4, mkdtemp as mkdtemp4, readFile as readFile7, rename as rename4, rm as rm6, writeFile as writeFile4 } from "fs/promises";
2967
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2968
+ import { dirname as dirname4, extname as extname3, join as join9, resolve as resolve7 } from "path";
2969
+ import sharp8 from "sharp";
1161
2970
 
1162
2971
  // src/wallet-release.json
1163
2972
  var wallet_release_default = {
@@ -1178,18 +2987,18 @@ var wallet_release_default = {
1178
2987
 
1179
2988
  // src/wallet-storage.ts
1180
2989
  import { randomUUID as randomUUID2 } from "crypto";
1181
- import { constants as constants2 } from "fs";
1182
- import { chmod, lstat, mkdir as mkdir2, open as open2, rename as rename2, rm as rm2 } from "fs/promises";
2990
+ import { constants as constants3 } from "fs";
2991
+ import { chmod, lstat, mkdir as mkdir3, open as open3, rename as rename3, rm as rm4 } from "fs/promises";
1183
2992
  import { homedir as homedir2 } from "os";
1184
- import { dirname as dirname2, join as join3, resolve as resolve5 } from "path";
2993
+ import { dirname as dirname3, join as join6, resolve as resolve6 } from "path";
1185
2994
  function walletHome() {
1186
- return resolve5(
1187
- process.env.MAKEABLES_WALLET_HOME ?? join3(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
2995
+ return resolve6(
2996
+ process.env.MAKEABLES_WALLET_HOME ?? join6(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1188
2997
  );
1189
2998
  }
1190
2999
  async function privateDirectory(path) {
1191
- const absolute = resolve5(path);
1192
- const parent = dirname2(absolute);
3000
+ const absolute = resolve6(path);
3001
+ const parent = dirname3(absolute);
1193
3002
  if (parent !== absolute) {
1194
3003
  try {
1195
3004
  const info = await lstat(parent);
@@ -1200,14 +3009,14 @@ async function privateDirectory(path) {
1200
3009
  await privateDirectory(parent);
1201
3010
  }
1202
3011
  }
1203
- await mkdir2(absolute, { recursive: true, mode: 448 });
3012
+ await mkdir3(absolute, { recursive: true, mode: 448 });
1204
3013
  if (!(await lstat(absolute)).isDirectory())
1205
3014
  throw new InputError("Wallet storage must be a directory, not a symbolic link.");
1206
3015
  await chmod(absolute, 448);
1207
3016
  return absolute;
1208
3017
  }
1209
3018
  async function readPrivateJson(path) {
1210
- const handle = await open2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
3019
+ const handle = await open3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
1211
3020
  try {
1212
3021
  const info = await handle.stat();
1213
3022
  if (!info.isFile() || info.size > 1e6)
@@ -1218,9 +3027,9 @@ async function readPrivateJson(path) {
1218
3027
  }
1219
3028
  }
1220
3029
  async function writePrivateJson(path, value) {
1221
- const temporary = join3(dirname2(path), `.${randomUUID2()}.json`);
3030
+ const temporary = join6(dirname3(path), `.${randomUUID2()}.json`);
1222
3031
  try {
1223
- const handle = await open2(temporary, "wx", 384);
3032
+ const handle = await open3(temporary, "wx", 384);
1224
3033
  try {
1225
3034
  await handle.writeFile(`${JSON.stringify(value)}
1226
3035
  `);
@@ -1228,15 +3037,15 @@ async function writePrivateJson(path, value) {
1228
3037
  } finally {
1229
3038
  await handle.close();
1230
3039
  }
1231
- await rename2(temporary, path);
3040
+ await rename3(temporary, path);
1232
3041
  } finally {
1233
- await rm2(temporary, { force: true });
3042
+ await rm4(temporary, { force: true });
1234
3043
  }
1235
3044
  }
1236
3045
 
1237
3046
  // src/wallet-submission.ts
1238
- import { mkdtemp, readFile as readFile6, rm as rm3, writeFile as writeFile2 } from "fs/promises";
1239
- import { join as join4 } from "path";
3047
+ import { mkdtemp as mkdtemp3, readFile as readFile6, rm as rm5, writeFile as writeFile3 } from "fs/promises";
3048
+ import { join as join7 } from "path";
1240
3049
  var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1241
3050
  function submissionId(input) {
1242
3051
  if (uuid.test(input)) return input.toLowerCase();
@@ -1254,7 +3063,7 @@ function submissionId(input) {
1254
3063
  return id.toLowerCase();
1255
3064
  }
1256
3065
  async function downloadWalletSubmission(input) {
1257
- const started = performance.now();
3066
+ const started2 = performance.now();
1258
3067
  const id = submissionId(input);
1259
3068
  const response = await fetch(`https://makeables.dev/api/submissions/${id}/files?download=1`, {
1260
3069
  redirect: "error",
@@ -1284,15 +3093,15 @@ async function downloadWalletSubmission(input) {
1284
3093
  const pkg = await readPackageZip(bytes);
1285
3094
  if (pkg.manifest.product !== "card")
1286
3095
  throw new InputError("Wallet artwork requires a card submission.");
1287
- const directory = await mkdtemp(
1288
- join4(
1289
- await privateDirectory(join4(await privateDirectory(walletHome()), "submissions")),
3096
+ const directory = await mkdtemp3(
3097
+ join7(
3098
+ await privateDirectory(join7(await privateDirectory(walletHome()), "submissions")),
1290
3099
  "download-"
1291
3100
  )
1292
3101
  );
1293
3102
  try {
1294
- const path = join4(directory, `${id}.makeables.zip`);
1295
- await writeFile2(path, bytes, { mode: 384, flag: "wx" });
3103
+ const path = join7(directory, `${id}.makeables.zip`);
3104
+ await writeFile3(path, bytes, { mode: 384, flag: "wx" });
1296
3105
  const saved = await readFile6(path);
1297
3106
  const sha256 = await hashBytes(saved);
1298
3107
  if (saved.length !== bytes.length || sha256 !== await hashBytes(bytes))
@@ -1304,53 +3113,53 @@ async function downloadWalletSubmission(input) {
1304
3113
  submissionId: id,
1305
3114
  title: pkg.manifest.title,
1306
3115
  downloadVerified: true,
1307
- elapsedMs: Math.round(performance.now() - started)
3116
+ elapsedMs: Math.round(performance.now() - started2)
1308
3117
  };
1309
3118
  } catch (error) {
1310
- await rm3(directory, { recursive: true, force: true });
3119
+ await rm5(directory, { recursive: true, force: true });
1311
3120
  throw error;
1312
3121
  }
1313
3122
  }
1314
3123
 
1315
3124
  // src/wallet-targets.ts
1316
3125
  import { lstat as lstat2 } from "fs/promises";
1317
- import { join as join5 } from "path";
3126
+ import { join as join8 } from "path";
1318
3127
  import lockfile from "proper-lockfile";
1319
- import { z as z3 } from "zod";
1320
- var alias = z3.string().regex(/^[a-z][a-z0-9-]{0,47}$/).refine((value) => !/\d{5}/.test(value));
1321
- var label = z3.string().trim().min(1).max(60).refine(
3128
+ import { z as z4 } from "zod";
3129
+ var alias = z4.string().regex(/^[a-z][a-z0-9-]{0,47}$/).refine((value) => !/\d{5}/.test(value));
3130
+ var label = z4.string().trim().min(1).max(60).refine(
1322
3131
  (value) => !/\d{5}/.test(value) && [...value].every(
1323
3132
  (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127
1324
3133
  )
1325
3134
  );
1326
- var targetSchema = z3.object({
3135
+ var targetSchema = z4.object({
1327
3136
  target: alias,
1328
3137
  label,
1329
- last4: z3.string().regex(/^\d{4}$/).optional(),
1330
- identitySource: z3.literal("user-confirmed"),
1331
- backup: z3.string().min(1),
1332
- createdAt: z3.string().datetime()
3138
+ last4: z4.string().regex(/^\d{4}$/).optional(),
3139
+ identitySource: z4.literal("user-confirmed"),
3140
+ backup: z4.string().min(1),
3141
+ createdAt: z4.string().datetime()
1333
3142
  }).strict();
1334
- var registrySchema = z3.object({ version: z3.literal(1), targets: z3.array(targetSchema).max(100) }).strict();
1335
- function targetIdentity(values, key = "remember") {
3143
+ var registrySchema = z4.object({ version: z4.literal(1), targets: z4.array(targetSchema).max(100) }).strict();
3144
+ function targetIdentity(values, key2 = "remember") {
1336
3145
  const result = targetSchema.pick({ target: true, label: true, last4: true }).safeParse({
1337
- target: values[key],
3146
+ target: values[key2],
1338
3147
  label: values.label,
1339
3148
  last4: values.last4
1340
3149
  });
1341
3150
  if (!result.success)
1342
3151
  throw new InputError(
1343
- `Supply --${key} with a lowercase alias and --label with the user-confirmed card name. Optional --last4 must be exactly four digits; never supply a full card number.`
3152
+ `Supply --${key2} with a lowercase alias and --label with the user-confirmed card name. Optional --last4 must be exactly four digits; never supply a full card number.`
1344
3153
  );
1345
3154
  return result.data;
1346
3155
  }
1347
3156
  function registryPath() {
1348
- return join5(walletHome(), "targets", "registry.json");
3157
+ return join8(walletHome(), "targets", "registry.json");
1349
3158
  }
1350
3159
  async function walletTargets() {
1351
3160
  const path = registryPath();
1352
3161
  try {
1353
- for (const parent of [walletHome(), join5(walletHome(), "targets")]) {
3162
+ for (const parent of [walletHome(), join8(walletHome(), "targets")]) {
1354
3163
  if (!(await lstat2(parent)).isDirectory())
1355
3164
  throw new InputError("Invalid Wallet target storage.");
1356
3165
  }
@@ -1373,7 +3182,7 @@ function targetChoices(targets) {
1373
3182
  }));
1374
3183
  }
1375
3184
  async function withWalletTargets(action) {
1376
- const directory = await privateDirectory(join5(await privateDirectory(walletHome()), "targets"));
3185
+ const directory = await privateDirectory(join8(await privateDirectory(walletHome()), "targets"));
1377
3186
  const release = await lockfile.lock(directory, { realpath: false, retries: 0 });
1378
3187
  try {
1379
3188
  return await action(await walletTargets(), async (targets) => {
@@ -1402,9 +3211,9 @@ var WalletError = class extends Error {
1402
3211
  };
1403
3212
  function installation() {
1404
3213
  const platform = `${process.platform}-${process.arch}`;
1405
- const cache = process.env.MAKEABLES_WALLET_CACHE ?? join6(homedir3(), "Library", "Caches", "Makeables", "wallet");
1406
- const root = resolve6(cache, `${wallet_release_default.version}-${platform}`);
1407
- return { platform, root, executable: join6(root, "makeables-wallet", "makeables-wallet") };
3214
+ const cache = process.env.MAKEABLES_WALLET_CACHE ?? join9(homedir3(), "Library", "Caches", "Makeables", "wallet");
3215
+ const root = resolve7(cache, `${wallet_release_default.version}-${platform}`);
3216
+ return { platform, root, executable: join9(root, "makeables-wallet", "makeables-wallet") };
1408
3217
  }
1409
3218
  async function execute(command, args, progress) {
1410
3219
  return new Promise((accept, reject) => {
@@ -1471,7 +3280,7 @@ async function walletDoctor() {
1471
3280
  supported: false,
1472
3281
  reason: "Wallet artwork requires a Mac and an iPhone connected by USB."
1473
3282
  };
1474
- if (!existsSync2(installed))
3283
+ if (!existsSync3(installed))
1475
3284
  return {
1476
3285
  ready: false,
1477
3286
  supported: true,
@@ -1501,7 +3310,7 @@ async function setupWallet(progress) {
1501
3310
  return info;
1502
3311
  }
1503
3312
  const { platform, root, executable } = installation();
1504
- if (existsSync2(executable)) {
3313
+ if (existsSync3(executable)) {
1505
3314
  const info = await walletDoctor();
1506
3315
  if (!info.ready)
1507
3316
  throw new InputError("The installed Wallet companion is incomplete or incompatible.");
@@ -1535,30 +3344,30 @@ async function setupWallet(progress) {
1535
3344
  const bytes = Buffer.concat(chunks);
1536
3345
  if (createHash3("sha256").update(bytes).digest("hex") !== artifact.sha256)
1537
3346
  throw new Error("Wallet companion checksum did not match. Nothing was installed.");
1538
- await mkdir3(dirname3(root), { recursive: true, mode: 448 });
1539
- const stage = await mkdtemp2(join6(dirname3(root), ".install-"));
3347
+ await mkdir4(dirname4(root), { recursive: true, mode: 448 });
3348
+ const stage = await mkdtemp4(join9(dirname4(root), ".install-"));
1540
3349
  try {
1541
- const archive = join6(stage, "companion.tar.gz");
1542
- await writeFile3(archive, bytes, { mode: 384, flag: "wx" });
3350
+ const archive = join9(stage, "companion.tar.gz");
3351
+ await writeFile4(archive, bytes, { mode: 384, flag: "wx" });
1543
3352
  const members = (await execute("tar", ["-tzf", archive])).trim().split("\n");
1544
3353
  if (members.some(
1545
3354
  (path) => !path.startsWith("makeables-wallet/") || path.split("/").includes("..")
1546
3355
  ))
1547
3356
  throw new Error("Unexpected companion archive contents.");
1548
3357
  await execute("tar", ["-xzf", archive, "-C", stage]);
1549
- await rm4(archive);
1550
- await chmod2(join6(stage, "makeables-wallet", "makeables-wallet"), 448);
1551
- const info = await invoke(join6(stage, "makeables-wallet", "makeables-wallet"), ["version"]);
3358
+ await rm6(archive);
3359
+ await chmod2(join9(stage, "makeables-wallet", "makeables-wallet"), 448);
3360
+ const info = await invoke(join9(stage, "makeables-wallet", "makeables-wallet"), ["version"]);
1552
3361
  if (info.version !== wallet_release_default.version || info.protocol !== wallet_release_default.protocol)
1553
3362
  throw new Error("Companion version does not match the installed CLI.");
1554
- await rename3(stage, root);
3363
+ await rename4(stage, root);
1555
3364
  } finally {
1556
- await rm4(stage, { recursive: true, force: true });
3365
+ await rm6(stage, { recursive: true, force: true });
1557
3366
  }
1558
3367
  return walletDoctor();
1559
3368
  }
1560
3369
  async function walletArtwork(file, side2 = "front") {
1561
- const input = resolve6(file);
3370
+ const input = resolve7(file);
1562
3371
  const info = await lstat3(input);
1563
3372
  if (!info.isFile() || info.size > 336e5)
1564
3373
  throw new InputError("Expected artwork or a complete Makeables package below 32 MB.");
@@ -1577,10 +3386,10 @@ async function walletArtwork(file, side2 = "front") {
1577
3386
  if (!result.ok) throw new InputError(result.error);
1578
3387
  }
1579
3388
  }
1580
- return sharp7(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
3389
+ return sharp8(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
1581
3390
  }
1582
3391
  async function preparedArtwork(directory) {
1583
- const path = resolve6(directory);
3392
+ const path = resolve7(directory);
1584
3393
  if (!(await lstat3(path)).isDirectory())
1585
3394
  throw new InputError("--assets must be a prepared artwork directory.");
1586
3395
  for (const name of [
@@ -1588,7 +3397,7 @@ async function preparedArtwork(directory) {
1588
3397
  "cardBackgroundCombined@2x.png",
1589
3398
  "cardBackgroundCombined.pdf"
1590
3399
  ]) {
1591
- const file = join6(path, name);
3400
+ const file = join9(path, name);
1592
3401
  const info = await lstat3(file);
1593
3402
  if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1594
3403
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
@@ -1596,7 +3405,7 @@ async function preparedArtwork(directory) {
1596
3405
  if (!(await readFile7(file)).subarray(0, signature.length).equals(signature))
1597
3406
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1598
3407
  }
1599
- return { path, preview: join6(path, "cardBackgroundCombined@3x.png") };
3408
+ return { path, preview: join9(path, "cardBackgroundCombined@3x.png") };
1600
3409
  }
1601
3410
  async function walletCommand(command, values, progress) {
1602
3411
  if (values.remember) {
@@ -1630,8 +3439,8 @@ async function walletCommand(command, values, progress) {
1630
3439
  return runWalletCommand(command, values, progress);
1631
3440
  }
1632
3441
  async function runWalletCommand(command, values, progress) {
1633
- const started = performance.now();
1634
- const elapsed = () => Math.round(performance.now() - started);
3442
+ const started2 = performance.now();
3443
+ const elapsed = () => Math.round(performance.now() - started2);
1635
3444
  let latestPhase;
1636
3445
  const report = (phase, message) => {
1637
3446
  latestPhase = phase;
@@ -1806,7 +3615,7 @@ async function runWalletCommand(command, values, progress) {
1806
3615
  }
1807
3616
  }
1808
3617
  if (command === "wallet prep" || applying || installing) {
1809
- const work = await mkdtemp2(join6(tmpdir2(), "makeables-wallet-"));
3618
+ const work = await mkdtemp4(join9(tmpdir3(), "makeables-wallet-"));
1810
3619
  let phase = "prepare";
1811
3620
  let deviceWriteAttempted = false;
1812
3621
  let persistent;
@@ -1814,23 +3623,23 @@ async function runWalletCommand(command, values, progress) {
1814
3623
  try {
1815
3624
  let target;
1816
3625
  if (existing) {
1817
- target = resolve6(need("assets"));
3626
+ target = resolve7(need("assets"));
1818
3627
  prepared = existing;
1819
3628
  } else {
1820
3629
  report("prepare", "Preparing the complete card artwork.");
1821
3630
  const png = await walletArtwork(need("file"), values.side === "back" ? "back" : "front");
1822
- const raster = join6(work, "artwork.png");
1823
- await writeFile3(raster, png, { mode: 384 });
3631
+ const raster = join9(work, "artwork.png");
3632
+ await writeFile4(raster, png, { mode: 384 });
1824
3633
  if (command === "wallet prep") {
1825
- target = resolve6(need("out"));
3634
+ target = resolve7(need("out"));
1826
3635
  } else if (installing && !applying) {
1827
3636
  const root = await privateDirectory(
1828
- join6(await privateDirectory(walletHome()), "prepared")
3637
+ join9(await privateDirectory(walletHome()), "prepared")
1829
3638
  );
1830
- persistent = await mkdtemp2(join6(root, "install-"));
1831
- target = join6(persistent, "assets");
3639
+ persistent = await mkdtemp4(join9(root, "install-"));
3640
+ target = join9(persistent, "assets");
1832
3641
  } else {
1833
- target = join6(work, "assets");
3642
+ target = join9(work, "assets");
1834
3643
  }
1835
3644
  prepared = await invoke(
1836
3645
  executable,
@@ -1916,8 +3725,8 @@ async function runWalletCommand(command, values, progress) {
1916
3725
  }
1917
3726
  throw error;
1918
3727
  } finally {
1919
- await rm4(work, { recursive: true, force: true });
1920
- if (persistent && phase === "prepare") await rm4(persistent, { recursive: true, force: true });
3728
+ await rm6(work, { recursive: true, force: true });
3729
+ if (persistent && phase === "prepare") await rm6(persistent, { recursive: true, force: true });
1921
3730
  }
1922
3731
  }
1923
3732
  throw new InputError("Unknown Wallet command.");
@@ -1968,14 +3777,33 @@ function walletSummary(result) {
1968
3777
 
1969
3778
  // src/cli.ts
1970
3779
  var commands = [
3780
+ "preflight [--probe]",
3781
+ "badge package --file <badge.json> --photo <portrait> --name <name> --out <badge.makeables.json> [--role] [--organization] [--number] [--event] [--url <https://...>]",
3782
+ "brand new --name <brand> --out <brand.json> [--slug <slug>]",
3783
+ "brand from-url --url <https://event-or-site> (--out <new-directory> | --file <brand.json>) [--name <brand>] [--slug <slug>]",
3784
+ "brand add-kit --file <brand.json> --ref <kit.json|kit.makeables.zip> --id <kit-id> [--family <family-id>]",
3785
+ "brand add-logo --file <brand.json> --ref <logo.svg|logo.png> --id <logo-id> [--name <label>]",
3786
+ "brand inspect --file <brand.json>",
3787
+ "brand export --file <brand.json> --out <brand.makeables.zip> [--overwrite]",
3788
+ "brand preview --file <brand.json> [--site <origin>] [--no-open]",
3789
+ "kit check --file <kit.json> --brand <brand.json>",
3790
+ "kit new --name <title> --out <kit.json>",
3791
+ "kit extract --file <artwork-sheet.png> --out <new-directory> [--params <json|@file>]",
3792
+ "kit add --file <kit.json> --ref <design|package> --id <piece-id>",
3793
+ "kit sheet --file <kit.json> --ref <sheet.json> --id <sheet-id>",
3794
+ "kit inspect --file <kit.json|kit.makeables.zip>",
3795
+ "kit render --file <kit.json|kit.makeables.zip> --out <mockup.png> [--overwrite]",
3796
+ "kit export --file <kit.json|kit.makeables.zip> --out <kit.makeables.zip> [--overwrite]",
3797
+ "kit import --file <kit.makeables.zip> --out <new-directory>",
1971
3798
  "designs list",
1972
- "new card --design <slug> | --style <id> --out <new-file> [--dry-run]",
3799
+ "new card --design <slug> | --style <id> | --blank [--name <title>] --out <new-file> [--dry-run]",
1973
3800
  "material set --file <design.json> --params <json|@file>",
1974
3801
  "material map --file <sticker.json> --id <image-layer> --ref <optical.png|optical.webp>",
1975
- "new badge --style <id> --out <new-file> [--dry-run]",
1976
- "new sticker --style <id> --out <new-file> [--dry-run]",
3802
+ "new badge (--style <id> | --blank [--name <title>]) --out <new-file> [--dry-run]",
3803
+ "new sticker (--style <id> | --blank [--name <title>]) --out <new-file> [--dry-run]",
1977
3804
  "layer list --file <design.json>",
1978
- "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back]",
3805
+ "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back] [--relative-to document|cwd]",
3806
+ "layer add --file <design.json> --kind text|shape|graphic|qr [--params <json|@file>] [--side front|back]",
1979
3807
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
1980
3808
  "layer remove --file <design.json> --id <layer> [--side front|back]",
1981
3809
  "cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
@@ -1984,7 +3812,13 @@ var commands = [
1984
3812
  "sheet pack --file <sheet.json>",
1985
3813
  "sheet check --file <sheet.json>",
1986
3814
  "sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
1987
- "render --file <design.json|design.makeables.json> --out <png> [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
3815
+ "brick check --file <model.ldr> [--library <ldraw-dir>]",
3816
+ "brick plan --file <model.ldr> [--library <ldraw-dir>]",
3817
+ "brick remix --file <exemplar.ldr> --seed <n> --out <new.ldr> [--mode strict|family|free] [--library <ldraw-dir>]",
3818
+ "grid bin --width <cells> --depth <cells> --height <units> --out <file.stl> [--overwrite]",
3819
+ "grid plate --width <cells> --depth <cells> --out <file.stl> [--overwrite]",
3820
+ "brick render --file <model.ldr> --out <png> [--step <n>] [--explode <factor>] [--yaw=<deg>] [--pitch=<deg>] [--library <ldraw-dir>] [--overwrite]",
3821
+ "render --file <design.json|design.makeables.json> --out <png> [--appearance flat|material] [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
1988
3822
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
1989
3823
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
1990
3824
  ...imageTools.map(
@@ -2027,22 +3861,35 @@ var commands = [
2027
3861
  "schema"
2028
3862
  ];
2029
3863
  var nextSteps = ["makeables skills get core", "makeables doctor"];
3864
+ var compact = false;
3865
+ var stdoutClosed = false;
3866
+ var traceFile;
3867
+ var traceCommand = "invalid";
3868
+ var traceCode;
3869
+ var elapsedStartup = process.uptime() * 1e3;
3870
+ var startedAt = new Date(Date.now() - elapsedStartup).toISOString();
3871
+ var started = performance.now() - elapsedStartup;
3872
+ process.stdout.on("error", (error) => {
3873
+ if (error.code === "EPIPE") stdoutClosed = true;
3874
+ else throw error;
3875
+ });
2030
3876
  function emit(data, next, human, json) {
3877
+ if (stdoutClosed) return;
2031
3878
  process.stdout.write(
2032
- json ? `${JSON.stringify({ ok: true, version, data, nextSteps: next })}
3879
+ json ? `${JSON.stringify({ ok: true, version, data: compact ? compactReceipt(data) : data, nextSteps: next })}
2033
3880
  ` : `${human}
2034
3881
  `
2035
3882
  );
2036
3883
  }
2037
3884
  async function newPath(file) {
2038
- const path = resolve7(file);
3885
+ const path = resolve8(file);
2039
3886
  try {
2040
3887
  await lstat4(path);
2041
3888
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
2042
3889
  } catch (error) {
2043
3890
  if (error.code !== "ENOENT") throw error;
2044
3891
  }
2045
- await access(dirname4(path), constants3.W_OK);
3892
+ await access(dirname5(path), constants4.W_OK);
2046
3893
  return path;
2047
3894
  }
2048
3895
  async function run() {
@@ -2100,10 +3947,34 @@ async function run() {
2100
3947
  type: { type: "string" },
2101
3948
  size: { type: "string" },
2102
3949
  "width-mm": { type: "string" },
2103
- preview: { type: "string" }
3950
+ preview: { type: "string" },
3951
+ library: { type: "string" },
3952
+ step: { type: "string" },
3953
+ explode: { type: "string" },
3954
+ yaw: { type: "string" },
3955
+ pitch: { type: "string" },
3956
+ seed: { type: "string" },
3957
+ width: { type: "string" },
3958
+ depth: { type: "string" },
3959
+ height: { type: "string" },
3960
+ mode: { type: "string" },
3961
+ appearance: { type: "string" },
3962
+ "relative-to": { type: "string" },
3963
+ slug: { type: "string" },
3964
+ family: { type: "string" },
3965
+ brand: { type: "string" },
3966
+ probe: { type: "boolean" },
3967
+ blank: { type: "boolean" },
3968
+ kind: { type: "string" },
3969
+ organization: { type: "string" },
3970
+ event: { type: "string" },
3971
+ compact: { type: "boolean" },
3972
+ trace: { type: "string" }
2104
3973
  }
2105
3974
  });
2106
3975
  const json = machineOutput(Boolean(values.json));
3976
+ compact = Boolean(values.compact);
3977
+ traceFile = values.trace;
2107
3978
  if (values.version) {
2108
3979
  emit({ version }, nextSteps, version, json);
2109
3980
  return;
@@ -2116,7 +3987,7 @@ async function run() {
2116
3987
  emit(
2117
3988
  {
2118
3989
  commands: helpCommands,
2119
- flags: ["--json", "--help", "--version"],
3990
+ flags: ["--json", "--help", "--version", "--compact", "--trace <local.jsonl>"],
2120
3991
  exitCodes: { success: 0, invalidInput: 2, systemFailure: 1 }
2121
3992
  },
2122
3993
  nextSteps,
@@ -2132,6 +4003,34 @@ From a built checkout: npm run studio -- <command>`,
2132
4003
  }
2133
4004
  const command = ["skills get", "studio call"].includes(positionals.slice(0, 2).join(" ")) ? positionals.slice(0, 2).join(" ") : positionals.join(" ");
2134
4005
  const allowed = {
4006
+ "kit new": ["name", "out"],
4007
+ "kit extract": ["file", "out", "params"],
4008
+ "kit add": ["file", "ref", "id"],
4009
+ "kit sheet": ["file", "ref", "id"],
4010
+ "kit check": ["file", "brand"],
4011
+ preflight: ["probe"],
4012
+ "badge package": [
4013
+ "file",
4014
+ "photo",
4015
+ "name",
4016
+ "role",
4017
+ "organization",
4018
+ "number",
4019
+ "event",
4020
+ "url",
4021
+ "out"
4022
+ ],
4023
+ "brand new": ["name", "slug", "out"],
4024
+ "brand from-url": ["url", "out", "file", "name", "slug"],
4025
+ "brand add-kit": ["file", "ref", "id", "family"],
4026
+ "brand add-logo": ["file", "ref", "id", "name"],
4027
+ "brand inspect": ["file"],
4028
+ "brand export": ["file", "out", "overwrite"],
4029
+ "brand preview": ["file", "site", "no-open"],
4030
+ "kit inspect": ["file"],
4031
+ "kit render": ["file", "out", "overwrite"],
4032
+ "kit export": ["file", "out", "overwrite"],
4033
+ "kit import": ["file", "out"],
2135
4034
  ...Object.fromEntries(
2136
4035
  imageTools.map((tool) => [`image ${tool}`, ["file", "out", "params", "ref"]])
2137
4036
  ),
@@ -2204,13 +4103,13 @@ From a built checkout: npm run studio -- <command>`,
2204
4103
  "design create": ["style", "out", "dry-run"],
2205
4104
  "design validate": ["file"],
2206
4105
  "designs list": [],
2207
- "new card": ["design", "style", "file", "name", "out", "dry-run"],
2208
- "new sticker": ["style", "out", "dry-run"],
4106
+ "new card": ["design", "style", "file", "blank", "name", "out", "dry-run"],
4107
+ "new sticker": ["style", "blank", "name", "out", "dry-run"],
2209
4108
  "material set": ["file", "params"],
2210
4109
  "material map": ["file", "id", "ref"],
2211
- "new badge": ["style", "out", "dry-run"],
4110
+ "new badge": ["style", "blank", "name", "out", "dry-run"],
2212
4111
  "layer list": ["file"],
2213
- "layer add": ["file", "sticker", "side"],
4112
+ "layer add": ["file", "sticker", "kind", "params", "side", "relative-to"],
2214
4113
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
2215
4114
  "layer remove": ["file", "id", "side"],
2216
4115
  cut: ["file", "out", "overwrite"],
@@ -2219,17 +4118,240 @@ From a built checkout: npm run studio -- <command>`,
2219
4118
  "sheet pack": ["file"],
2220
4119
  "sheet check": ["file"],
2221
4120
  "sheet export": ["file", "out", "preview", "overwrite"],
2222
- render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number"],
4121
+ "brick check": ["file", "library"],
4122
+ "brick plan": ["file", "library"],
4123
+ "brick render": ["file", "out", "step", "explode", "yaw", "pitch", "library", "overwrite"],
4124
+ "brick remix": ["file", "seed", "out", "mode", "library"],
4125
+ "grid bin": ["width", "depth", "height", "out", "overwrite"],
4126
+ "grid plate": ["width", "depth", "out", "overwrite"],
4127
+ render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number", "appearance"],
2223
4128
  validate: ["file", "out"]
2224
4129
  };
2225
4130
  if (!(command in allowed)) throw new InputError(`Unknown command: ${command}`);
4131
+ traceCommand = command;
2226
4132
  for (const flag of Object.keys(values))
2227
- if (!["json", "help", "version", ...allowed[command]].includes(flag))
4133
+ if (!["json", "help", "version", "compact", "trace", ...allowed[command]].includes(flag))
2228
4134
  throw new InputError(`--${flag} is not supported by ${command}`);
2229
4135
  const progress = (data) => process.stderr.write(`${JSON.stringify(data)}
2230
4136
  `);
2231
4137
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
2232
4138
  throw new InputError("--product must be card, badge or sticker.");
4139
+ if (command === "badge package") {
4140
+ const need = (value, flag) => {
4141
+ if (!value) throw new InputError(`Supply --${flag}.`);
4142
+ return value;
4143
+ };
4144
+ const out = await newPath(need(values.out, "out"));
4145
+ if (!out.endsWith(".makeables.json"))
4146
+ throw new InputError("--out must end in .makeables.json.");
4147
+ const { badgePackage } = await import("./badge-package-CMFI3BIR.js");
4148
+ const { json: body, participant } = await badgePackage(
4149
+ need(values.file, "file"),
4150
+ need(values.photo, "photo"),
4151
+ {
4152
+ name: need(values.name, "name"),
4153
+ role: values.role,
4154
+ organization: values.organization,
4155
+ number: values.number ? Number(values.number) : void 0,
4156
+ event: values.event,
4157
+ url: values.url
4158
+ }
4159
+ );
4160
+ await writeAtomic(out, JSON.stringify(body), true);
4161
+ emit(
4162
+ {
4163
+ path: out,
4164
+ participant: {
4165
+ name: participant.name,
4166
+ role: participant.role,
4167
+ publicUrl: participant.publicUrl
4168
+ }
4169
+ },
4170
+ [`makeables kit add --file <kit.json> --ref ${JSON.stringify(out)} --id badge`],
4171
+ `Badge package ready: ${out}`,
4172
+ json
4173
+ );
4174
+ return;
4175
+ }
4176
+ if (command === "preflight") {
4177
+ const result2 = await preflight({ probe: Boolean(values.probe) });
4178
+ emit(result2, ["makeables skills get kits"], preflightSummary(result2), json);
4179
+ return;
4180
+ }
4181
+ if (command.startsWith("brand ")) {
4182
+ const need = (value, flag) => {
4183
+ if (!value) throw new InputError(`Supply --${flag}.`);
4184
+ return value;
4185
+ };
4186
+ let result2;
4187
+ let human = "Brand ready.";
4188
+ if (command === "brand new") {
4189
+ const path2 = await newPath(need(values.out, "out"));
4190
+ const brand = newBrand(need(values.name, "name"), values.slug);
4191
+ await writeBrand(path2, brand);
4192
+ result2 = { path: path2, brand };
4193
+ } else if (command === "brand from-url") {
4194
+ const out = resolve8(values.file ? dirname5(values.file) : need(values.out, "out"));
4195
+ if (!values.file)
4196
+ try {
4197
+ await lstat4(out);
4198
+ throw new InputError(`Directory already exists: ${out}. Choose a new --out path.`);
4199
+ } catch (error) {
4200
+ if (error.code !== "ENOENT") throw error;
4201
+ }
4202
+ result2 = await brandFromUrl(need(values.url, "url"), out, {
4203
+ name: values.name,
4204
+ slug: values.slug,
4205
+ into: values.file
4206
+ });
4207
+ human = `Brand refs ready. Open ${result2.proof} and confirm the assets with the user.`;
4208
+ } else if (command === "brand add-kit") {
4209
+ const path2 = need(values.file, "file");
4210
+ const brand = await addBrandKit(
4211
+ path2,
4212
+ need(values.ref, "ref"),
4213
+ need(values.id, "id"),
4214
+ values.family
4215
+ );
4216
+ await writeBrand(path2, brand);
4217
+ result2 = { path: resolve8(path2), kits: brand.kits.length };
4218
+ } else if (command === "brand add-logo") {
4219
+ const path2 = need(values.file, "file");
4220
+ const brand = await addBrandLogo(
4221
+ path2,
4222
+ need(values.ref, "ref"),
4223
+ need(values.id, "id"),
4224
+ values.name
4225
+ );
4226
+ await writeBrand(path2, brand);
4227
+ result2 = { path: resolve8(path2), logos: brand.logos.map((logo) => logo.id) };
4228
+ } else if (command === "brand inspect") {
4229
+ const brand = await readBrand(need(values.file, "file"));
4230
+ const view = await brandView(need(values.file, "file"));
4231
+ result2 = {
4232
+ brand,
4233
+ pieces: view.pieces.length,
4234
+ products: Object.fromEntries(
4235
+ ["card", "badge", "sticker"].map((product) => [
4236
+ product,
4237
+ view.pieces.filter((piece) => piece.product === product).length
4238
+ ])
4239
+ ),
4240
+ unsubmitted: brand.kits.filter((kit) => !kit.submission).map((kit) => kit.id)
4241
+ };
4242
+ } else if (command === "brand export") {
4243
+ const out = resolve8(need(values.out, "out"));
4244
+ if (!out.endsWith(".zip")) throw new InputError("--out must end in .zip.");
4245
+ const { packageZip } = await import("./submission-ATVS5YTK.js");
4246
+ const pkg = await prepareBrand(need(values.file, "file"));
4247
+ const bytes = await packageZip(pkg);
4248
+ const handle2 = await open4(
4249
+ values.overwrite ? out : await newPath(out),
4250
+ values.overwrite ? "w" : "wx"
4251
+ );
4252
+ try {
4253
+ await handle2.writeFile(bytes);
4254
+ } finally {
4255
+ await handle2.close();
4256
+ }
4257
+ result2 = {
4258
+ path: out,
4259
+ product: "brand",
4260
+ kits: pkg.manifest.product === "brand" ? pkg.manifest.kits.length : 0,
4261
+ bytes: bytes.length
4262
+ };
4263
+ } else {
4264
+ const { serveBrandPreview } = await import("./brand-preview-W4FTJJM3.js");
4265
+ result2 = await serveBrandPreview(need(values.file, "file"), {
4266
+ site: values.site,
4267
+ open: !values["no-open"]
4268
+ });
4269
+ human = `Brand preview at ${result2.url}. Press Ctrl+C to stop.`;
4270
+ }
4271
+ emit(result2, ["makeables skills get kits"], human, json);
4272
+ return;
4273
+ }
4274
+ if (command.startsWith("kit ")) {
4275
+ const required = (value, flag) => {
4276
+ if (!value) throw new InputError(`Supply --${flag}.`);
4277
+ return value;
4278
+ };
4279
+ let result2;
4280
+ if (command === "kit check") {
4281
+ result2 = await checkKitAgainstBrand(
4282
+ required(values.file, "file"),
4283
+ required(values.brand, "brand")
4284
+ );
4285
+ emit(result2, ["makeables skills get kits"], JSON.stringify(result2, null, 2), json);
4286
+ if (!result2.ok) process.exitCode = 2;
4287
+ return;
4288
+ }
4289
+ if (command === "kit extract") {
4290
+ result2 = await extractKit(
4291
+ required(values.file, "file"),
4292
+ required(values.out, "out"),
4293
+ values.params ? await readParams(values.params) : {}
4294
+ );
4295
+ } else if (command === "kit new") {
4296
+ const path2 = await newPath(required(values.out, "out"));
4297
+ const kit = kitDocumentSchema.parse({
4298
+ format: "makeables-kit",
4299
+ version: 1,
4300
+ name: required(values.name, "name"),
4301
+ items: [],
4302
+ sheets: []
4303
+ });
4304
+ await writeAtomic(path2, JSON.stringify(kit, null, 2), true);
4305
+ result2 = { path: path2, kit };
4306
+ } else if (command === "kit add" || command === "kit sheet") {
4307
+ const path2 = required(values.file, "file");
4308
+ const kit = await addKitMember(
4309
+ path2,
4310
+ required(values.ref, "ref"),
4311
+ required(values.id, "id"),
4312
+ command === "kit add" ? "items" : "sheets"
4313
+ );
4314
+ await writeAtomic(path2, JSON.stringify(kit, null, 2));
4315
+ result2 = { path: resolve8(path2), items: kit.items.length, sheets: kit.sheets.length };
4316
+ } else if (command === "kit import") {
4317
+ result2 = await importKit(required(values.file, "file"), required(values.out, "out"));
4318
+ } else if (command === "kit inspect" && !required(values.file, "file").toLowerCase().endsWith(".zip")) {
4319
+ const value = JSON.parse(await readFile8(required(values.file, "file"), "utf8"));
4320
+ if (value.format === "makeables-kit") result2 = await readKit(required(values.file, "file"));
4321
+ else {
4322
+ const pkg = await readSubmission(required(values.file, "file"));
4323
+ if (pkg.manifest.product !== "kit")
4324
+ throw new InputError("Choose a kit document or package.");
4325
+ result2 = pkg.manifest;
4326
+ }
4327
+ } else {
4328
+ const { pkg, bytes } = await exportKit(required(values.file, "file"));
4329
+ if (command === "kit inspect") result2 = pkg.manifest;
4330
+ else {
4331
+ const out = resolve8(required(values.out, "out"));
4332
+ const rendering = command === "kit render";
4333
+ if (!out.endsWith(rendering ? ".png" : ".zip"))
4334
+ throw new InputError(rendering ? "--out must end in .png." : "--out must end in .zip.");
4335
+ const handle2 = await open4(
4336
+ values.overwrite ? out : await newPath(out),
4337
+ values.overwrite ? "w" : "wx"
4338
+ );
4339
+ try {
4340
+ await handle2.writeFile(rendering ? pkg.files[pkg.manifest.front] : bytes);
4341
+ } finally {
4342
+ await handle2.close();
4343
+ }
4344
+ result2 = {
4345
+ path: out,
4346
+ product: "kit",
4347
+ title: pkg.manifest.title,
4348
+ bytes: rendering ? pkg.files[pkg.manifest.front].length : bytes.length
4349
+ };
4350
+ }
4351
+ }
4352
+ emit(result2, ["makeables skills get kits"], "Kit ready.", json);
4353
+ return;
4354
+ }
2233
4355
  if (command.startsWith("wallet ")) {
2234
4356
  const walletProgress = (data) => {
2235
4357
  if (json) return progress(data);
@@ -2289,7 +4411,12 @@ From a built checkout: npm run studio -- <command>`,
2289
4411
  throw new InputError(
2290
4412
  "Supply --file with editable JSON, an original SVG or a Makeables package."
2291
4413
  );
2292
- const result2 = await submitDesign({
4414
+ const isBrand = values.file.endsWith(".json") && await readFile8(values.file, "utf8").then(
4415
+ (text) => text.includes('"makeables-brand"'),
4416
+ () => false
4417
+ );
4418
+ const submit = isBrand ? (await import("./brand-NMENEWIZ.js")).submitBrand : submitDesign;
4419
+ const result2 = await submit({
2293
4420
  title: values.title,
2294
4421
  credit: values.credit,
2295
4422
  creditUrl: values["credit-url"],
@@ -2339,7 +4466,7 @@ From a built checkout: npm run studio -- <command>`,
2339
4466
  }
2340
4467
  if (command === "doctor") {
2341
4468
  const check = (command2) => {
2342
- const result2 = spawnSync2(command2, ["--version"], {
4469
+ const result2 = spawnSync3(command2, ["--version"], {
2343
4470
  encoding: "utf8",
2344
4471
  timeout: 5e3,
2345
4472
  stdio: ["ignore", "pipe", "pipe"]
@@ -2395,7 +4522,7 @@ Run makeables skills get core for setup.`,
2395
4522
  }
2396
4523
  if (command === "mcp") {
2397
4524
  try {
2398
- const { runMcp } = await import("./mcp-HS2XZXSO.js");
4525
+ const { runMcp } = await import("./mcp-YBRUVIWG.js");
2399
4526
  await runMcp(values.root ?? process.cwd());
2400
4527
  } catch (error) {
2401
4528
  process.stderr.write(
@@ -2537,7 +4664,7 @@ ${styles.map((entry) => `${column(entry.id ?? "", 25)}${entry.name}`).join("\n")
2537
4664
  const path2 = values.out ? await newPath(values.out) : null;
2538
4665
  const dryRun = Boolean(values["dry-run"]);
2539
4666
  if (path2 && !dryRun) {
2540
- const handle2 = await open3(path2, "wx", 384);
4667
+ const handle2 = await open4(path2, "wx", 384);
2541
4668
  try {
2542
4669
  await handle2.writeFile(`${JSON.stringify(design, null, 2)}
2543
4670
  `);
@@ -2560,8 +4687,8 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
2560
4687
  return;
2561
4688
  }
2562
4689
  if (!values.file) throw new InputError("Supply --file to validate a document.");
2563
- const path = resolve7(values.file);
2564
- const handle = await open3(path, "r");
4690
+ const path = resolve8(values.file);
4691
+ const handle = await open4(path, "r");
2565
4692
  let input;
2566
4693
  try {
2567
4694
  const info = await handle.stat();
@@ -2586,7 +4713,7 @@ Schema and layout constraints passed.`,
2586
4713
  }
2587
4714
  async function readParams(value) {
2588
4715
  return JSON.parse(
2589
- value.startsWith("@") ? await readFile8(resolve7(value.slice(1)), "utf8") : value
4716
+ value.startsWith("@") ? await readFile8(resolve8(value.slice(1)), "utf8") : value
2590
4717
  );
2591
4718
  }
2592
4719
  async function cardCommand(command, values, json) {
@@ -2597,12 +4724,26 @@ async function cardCommand(command, values, json) {
2597
4724
  };
2598
4725
  const renderHint = (path, face, design) => `npx makeables render --file ${JSON.stringify(path)} --side ${face} --out ${design.product ?? "badge"}.png --overwrite`;
2599
4726
  if (command === "new sticker") {
4727
+ if (values.blank && values.style) throw new InputError("Choose --blank or --style, not both.");
2600
4728
  const preset = stickerStyles.find(
2601
4729
  (entry) => entry.source === (values.style ?? "typingmind-absolutely-right")
2602
4730
  );
2603
4731
  if (!preset)
2604
4732
  throw new InputError("Unknown sticker style. Run makeables styles list --product sticker.");
2605
- const design = badgeDesignSchema.parse(structuredClone(preset));
4733
+ const design = badgeDesignSchema.parse(
4734
+ values.blank ? {
4735
+ ...structuredClone(preset),
4736
+ name: typeof values.name === "string" ? values.name : "Untitled sticker",
4737
+ source: void 0,
4738
+ event: typeof values.name === "string" ? values.name : "Makeables",
4739
+ description: "",
4740
+ artPrompt: "",
4741
+ assets: {},
4742
+ front: { background: "#00000000", layers: [] },
4743
+ back: { background: "#00000000", layers: [] },
4744
+ sticker: { ...defaultStickerMaterial }
4745
+ } : structuredClone(preset)
4746
+ );
2606
4747
  const path = values.out ? await newPath(String(values.out)) : null;
2607
4748
  const dryRun = Boolean(values["dry-run"]);
2608
4749
  if (path && !dryRun) await writeAtomic(path, `${JSON.stringify(design, null, 2)}
@@ -2619,10 +4760,29 @@ ${path ?? "No file written. Add --out sticker.json."}`,
2619
4760
  );
2620
4761
  return true;
2621
4762
  }
4763
+ if ((command === "new badge" || command === "new card") && values.blank) {
4764
+ if (values.style || values.design || values.file)
4765
+ throw new InputError("Choose --blank or a --style, --design or --file, not both.");
4766
+ const design = blankDesign(command === "new card" ? "card" : "badge", values.name);
4767
+ const path = values.out ? await newPath(String(values.out)) : null;
4768
+ const dryRun = Boolean(values["dry-run"]);
4769
+ if (path && !dryRun) await writeAtomic(path, `${JSON.stringify(design, null, 2)}
4770
+ `, true);
4771
+ emit(
4772
+ { design, path, written: Boolean(path && !dryRun), dryRun },
4773
+ path ? [
4774
+ `makeables layer add --file ${JSON.stringify(path)} --kind text --params '{"text":"..."}'`
4775
+ ] : [],
4776
+ `${design.product === "card" ? "Card" : "Badge"} ready \xB7 ${design.name}
4777
+ ${path ?? "No file written. Add --out."}`,
4778
+ json
4779
+ );
4780
+ return true;
4781
+ }
2622
4782
  if (command === "new badge" || command === "new card") {
2623
4783
  const card = command === "new card";
2624
4784
  if (card && [values.design, values.style, values.file].filter(Boolean).length !== 1)
2625
- throw new InputError("Choose exactly one --design, --style or --file.");
4785
+ throw new InputError("Choose exactly one --design, --style, --file or --blank.");
2626
4786
  const preset = card && values.style ? findCardStyle(need(values.style, "style")) : void 0;
2627
4787
  if (card && values.style && !preset)
2628
4788
  throw new InputError("Unknown card style. Run makeables styles list --product card.");
@@ -2648,7 +4808,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2648
4808
  return true;
2649
4809
  }
2650
4810
  if (command === "material map") {
2651
- const path = resolve7(need(file, "file"));
4811
+ const path = resolve8(need(file, "file"));
2652
4812
  const next = await bindStickerMap(
2653
4813
  await readBadge(path),
2654
4814
  need(values.id, "id"),
@@ -2664,7 +4824,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2664
4824
  return true;
2665
4825
  }
2666
4826
  if (command === "material set") {
2667
- const path = resolve7(need(file, "file"));
4827
+ const path = resolve8(need(file, "file"));
2668
4828
  const design = await readBadge(path);
2669
4829
  const raw = need(values.params, "params");
2670
4830
  const input = JSON.parse(raw.startsWith("@") ? await readFile8(raw.slice(1), "utf8") : raw);
@@ -2726,11 +4886,11 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2726
4886
  const changes = values.params ? await readParams(String(values.params)) : {};
2727
4887
  if (typeof changes !== "object" || changes === null || Array.isArray(changes))
2728
4888
  throw new InputError("--params must be a JSON object of layer fields.");
2729
- for (const key of ["x", "y", "rotation"])
2730
- if (values[key] !== void 0) {
2731
- const value = Number(values[key]);
2732
- if (!Number.isFinite(value)) throw new InputError(`--${key} must be a number.`);
2733
- changes[key] = value;
4889
+ for (const key2 of ["x", "y", "rotation"])
4890
+ if (values[key2] !== void 0) {
4891
+ const value = Number(values[key2]);
4892
+ if (!Number.isFinite(value)) throw new InputError(`--${key2} must be a number.`);
4893
+ changes[key2] = value;
2734
4894
  }
2735
4895
  design = setBadgeLayer(design, face, id, changes);
2736
4896
  }
@@ -2744,14 +4904,37 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2744
4904
  );
2745
4905
  return true;
2746
4906
  }
4907
+ if (command === "layer add" && values.kind) {
4908
+ if (values.sticker) throw new InputError("Choose --kind or --sticker, not both.");
4909
+ const path = need(file, "file");
4910
+ const face = side(values.side);
4911
+ const { design, id, warnings } = addLayer(
4912
+ await readBadge(path),
4913
+ face,
4914
+ String(values.kind),
4915
+ values.params ? await readParams(String(values.params)) : {}
4916
+ );
4917
+ await writeAtomic(path, `${JSON.stringify(design, null, 2)}
4918
+ `);
4919
+ const layer = design[face].layers.find((item) => item.id === id);
4920
+ emit(
4921
+ { layer, warnings },
4922
+ [renderHint(path, face, design)],
4923
+ [`${style("Added", "1", !json)} ${id} on ${face}`, ...warnings].join("\n"),
4924
+ json
4925
+ );
4926
+ return true;
4927
+ }
2747
4928
  if (command === "layer add") {
2748
4929
  const path = need(file, "file");
2749
4930
  const face = side(values.side);
4931
+ if (values["relative-to"] && !["cwd", "document"].includes(String(values["relative-to"])))
4932
+ throw new InputError("--relative-to must be document or cwd.");
2750
4933
  const design = await addBadgeSticker(
2751
4934
  await readBadge(path),
2752
4935
  face,
2753
4936
  need(values.sticker, "sticker"),
2754
- dirname4(resolve7(path))
4937
+ values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve8(path))
2755
4938
  );
2756
4939
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
2757
4940
  `);
@@ -2764,9 +4947,100 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2764
4947
  );
2765
4948
  return true;
2766
4949
  }
4950
+ if (command === "grid bin" || command === "grid plate") {
4951
+ const out = resolve8(need(values.out, "out"));
4952
+ const result = gridStl(command === "grid bin" ? "bin" : "plate", values);
4953
+ const handle = await open4(out, values.overwrite ? "w" : "wx", 420);
4954
+ try {
4955
+ await handle.writeFile(result.stl);
4956
+ } finally {
4957
+ await handle.close();
4958
+ }
4959
+ const { x, y, z: z5 } = result.sizeMm;
4960
+ emit(
4961
+ { out, part: result.part, triangles: result.triangles, sizeMm: result.sizeMm },
4962
+ [`Open ${out} in your slicer.`],
4963
+ `${style("Written", "1", !json)} ${result.name}, ${x} x ${y} x ${z5} mm, ${out}`,
4964
+ json
4965
+ );
4966
+ return true;
4967
+ }
4968
+ if (command.startsWith("brick ")) {
4969
+ const path = need(file, "file");
4970
+ const model = await readBrickModel(path, ldrawLibrary(values.library));
4971
+ if (command === "brick check") {
4972
+ const report = brickCheck(model);
4973
+ emit(
4974
+ report,
4975
+ report.ok ? [`makeables brick plan --file ${JSON.stringify(path)}`] : ["Fix the parts listed, or add connectors for joints the checker cannot see."],
4976
+ [
4977
+ `${style(report.ok ? "Buildable" : "Not buildable", "1", !json)} ${report.parts} parts, ${report.joints} joints`,
4978
+ ...report.issues.map((i) => ` ${i.rule}: ${i.message}: ${i.parts.join(", ")}`)
4979
+ ].join("\n"),
4980
+ json
4981
+ );
4982
+ if (!report.ok) process.exitCode = 2;
4983
+ return true;
4984
+ }
4985
+ if (command === "brick plan") {
4986
+ const plan = brickPlan(model);
4987
+ emit(
4988
+ { steps: plan.rows, locked: plan.locked },
4989
+ [`makeables brick render --file ${JSON.stringify(path)} --out step.png --step 1`],
4990
+ [
4991
+ ...plan.rows.map(
4992
+ (r) => ` ${String(r.step).padStart(3)} ${r.part.padEnd(9)} ${r.insert} ${r.name}`
4993
+ ),
4994
+ ...plan.locked.length ? [` locked: ${plan.locked.join(", ")}`] : []
4995
+ ].join("\n"),
4996
+ json
4997
+ );
4998
+ return true;
4999
+ }
5000
+ const number = (name) => {
5001
+ const raw = values[name];
5002
+ if (raw === void 0) return void 0;
5003
+ const n = Number(raw);
5004
+ if (!Number.isFinite(n)) throw new InputError(`--${name} must be a number.`);
5005
+ return n;
5006
+ };
5007
+ if (command === "brick remix") {
5008
+ const seed = number("seed");
5009
+ if (seed === void 0 || !Number.isInteger(seed))
5010
+ throw new InputError("--seed must be an integer.");
5011
+ const out2 = await newPath(need(values.out, "out"));
5012
+ const result = brickRemix(model, seed, values.mode);
5013
+ await writeAtomic(out2, result.ldr, true);
5014
+ emit(
5015
+ { out: out2, parts: result.check.parts, left: result.left, check: result.check },
5016
+ [
5017
+ `makeables brick plan --file ${JSON.stringify(out2)}`,
5018
+ `makeables brick render --file ${JSON.stringify(out2)} --out remix.png`
5019
+ ],
5020
+ `${style(result.check.ok ? "Remixed" : "Remixed, not buildable", "1", !json)} ${result.check.parts} parts${result.left.length ? `, ${result.left.length} left in the bag` : ""} ${out2}`,
5021
+ json
5022
+ );
5023
+ return true;
5024
+ }
5025
+ const out = resolve8(need(values.out, "out"));
5026
+ const png = await brickRender(model, {
5027
+ step: number("step"),
5028
+ explode: number("explode"),
5029
+ yaw: number("yaw"),
5030
+ pitch: number("pitch")
5031
+ });
5032
+ const handle = await open4(out, values.overwrite ? "w" : "wx", 420);
5033
+ try {
5034
+ await handle.writeFile(png);
5035
+ } finally {
5036
+ await handle.close();
5037
+ }
5038
+ emit({ out }, [`Open ${out} and look at it.`], `${style("Rendered", "1", !json)} ${out}`, json);
5039
+ return true;
5040
+ }
2767
5041
  if (command.startsWith("sheet ")) {
2768
5042
  const write = async (target, bytes) => {
2769
- const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
5043
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2770
5044
  try {
2771
5045
  await handle.writeFile(bytes);
2772
5046
  } finally {
@@ -2845,9 +5119,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2845
5119
  throw new InputError(
2846
5120
  `The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
2847
5121
  );
2848
- const out = resolve7(need(values.out, "out"));
5122
+ const out = resolve8(need(values.out, "out"));
2849
5123
  if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
2850
- const preview = typeof values.preview === "string" ? resolve7(values.preview) : void 0;
5124
+ const preview = typeof values.preview === "string" ? resolve8(values.preview) : void 0;
2851
5125
  if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
2852
5126
  const targets = [out, ...preview ? [preview] : []];
2853
5127
  if (!values.overwrite) for (const target of targets) await newPath(target);
@@ -2865,12 +5139,12 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2865
5139
  if (command === "cut") {
2866
5140
  const design = await readBadge(need(file, "file"));
2867
5141
  if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
2868
- const out = typeof values.out === "string" ? resolve7(values.out) : void 0;
5142
+ const out = typeof values.out === "string" ? resolve8(values.out) : void 0;
2869
5143
  if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
2870
5144
  const target = out && (values.overwrite ? out : await newPath(out));
2871
5145
  const { cut, svg } = await stickerPrintCut(design);
2872
5146
  if (target) {
2873
- const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
5147
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2874
5148
  try {
2875
5149
  await handle.writeFile(svg);
2876
5150
  } finally {
@@ -2902,17 +5176,20 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2902
5176
  return true;
2903
5177
  }
2904
5178
  if (command === "render") {
5179
+ if (values.appearance && !["flat", "material"].includes(String(values.appearance)))
5180
+ throw new InputError("--appearance must be flat or material.");
5181
+ const appearance = values.appearance;
2905
5182
  const path = need(file, "file");
2906
- const out = resolve7(need(values.out, "out"));
5183
+ const out = resolve8(need(values.out, "out"));
2907
5184
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
2908
5185
  const target = values.overwrite ? out : await newPath(out);
2909
5186
  const number = values.number === void 0 ? void 0 : Number(values.number);
2910
5187
  if (number !== void 0 && !Number.isInteger(number))
2911
5188
  throw new InputError("--number must be an integer.");
2912
5189
  const face = side(values.side);
2913
- const value = JSON.parse(await readFile8(resolve7(path), "utf8"));
5190
+ const value = JSON.parse(await readFile8(resolve8(path), "utf8"));
2914
5191
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
2915
- if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
5192
+ if (pkg && ["photo", "name", "role", "number"].some((key2) => values[key2] !== void 0))
2916
5193
  throw new InputError(
2917
5194
  "Portable packages include their participant and images. Edit the package before rendering, or use these flags with a plain design JSON."
2918
5195
  );
@@ -2922,11 +5199,12 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2922
5199
  if (pkg && !previewPath) throw new InputError("This package has no back preview.");
2923
5200
  const card = design.product === "card" || design.product === "sticker";
2924
5201
  const { png, person, samplePortrait } = pkg && previewPath ? {
2925
- png: Buffer.from(pkg.files[previewPath]),
5202
+ png: appearance && draft ? await renderDraftFace(draft, face, appearance) : Buffer.from(pkg.files[previewPath]),
2926
5203
  person: draft?.participant,
2927
5204
  samplePortrait: false
2928
5205
  } : await renderBadge(design, {
2929
5206
  face,
5207
+ appearance,
2930
5208
  photo: typeof values.photo === "string" ? values.photo : void 0,
2931
5209
  person: {
2932
5210
  ...typeof values.name === "string" ? { name: values.name } : {},
@@ -2934,7 +5212,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2934
5212
  ...number === void 0 ? {} : { number }
2935
5213
  }
2936
5214
  });
2937
- const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
5215
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2938
5216
  try {
2939
5217
  await handle.writeFile(png);
2940
5218
  } finally {
@@ -2949,12 +5227,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2949
5227
  side: face,
2950
5228
  width,
2951
5229
  height,
5230
+ appearance: appearance ?? (pkg && design.product === "sticker" ? "material" : "flat"),
2952
5231
  ...card ? {} : { person, samplePortrait }
2953
5232
  },
2954
5233
  [
2955
5234
  `Open ${target} to review.`,
2956
5235
  ...portrait ? ["Pass --photo <image> to use the person's portrait."] : [],
2957
- ...pkg && design.product === "sticker" ? ["The transparent PNG includes the sticker material."] : ["This is flat artwork. Use an available integrated preview for live materials."]
5236
+ ...(appearance === "material" || pkg && !appearance) && design.product === "sticker" ? ["The transparent PNG includes the sticker material."] : ["This is flat artwork. Use an available integrated preview for live materials."]
2958
5237
  ],
2959
5238
  `${style("Rendered", "1", !json)} ${face} ${target}${portrait ? "\n note: sample portrait; pass --photo" : ""}`,
2960
5239
  json
@@ -2972,13 +5251,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2972
5251
  return true;
2973
5252
  }
2974
5253
  if (command === "validate") {
2975
- const path = resolve7(need(file, "file"));
5254
+ const path = resolve8(need(file, "file"));
2976
5255
  if (extname4(path).toLowerCase() === ".svg") {
2977
5256
  const source = await readFile8(path, "utf8");
2978
5257
  const result2 = checkSubmittedSvg(source);
2979
5258
  if (!result2.ok) throw new InputError(result2.error);
2980
5259
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
2981
- if (preview) await sharp8(Buffer.from(result2.svg)).png().toFile(preview);
5260
+ if (preview) await sharp9(Buffer.from(result2.svg)).png().toFile(preview);
2982
5261
  emit(
2983
5262
  {
2984
5263
  valid: true,
@@ -3013,7 +5292,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3013
5292
  return false;
3014
5293
  }
3015
5294
  run().catch((error) => {
3016
- const userError = error instanceof InputError || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
5295
+ const userError = error instanceof InputError || error instanceof ZodError2 || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
3017
5296
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
3018
5297
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
3019
5298
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",
@@ -3032,10 +5311,28 @@ run().catch((error) => {
3032
5311
  },
3033
5312
  nextSteps
3034
5313
  };
3035
- if (machineOutput(process.argv.includes("--json")))
5314
+ traceCode = payload.error.code;
5315
+ if (!stdoutClosed && machineOutput(process.argv.includes("--json")))
3036
5316
  process.stdout.write(`${JSON.stringify(payload)}
3037
5317
  `);
3038
5318
  process.stderr.write(`makeables: ${error.message}
3039
5319
  `);
3040
5320
  process.exitCode = exitCode;
5321
+ }).finally(async () => {
5322
+ if (!traceFile) return;
5323
+ try {
5324
+ const supplied = process.env.MAKEABLES_TRACE_SESSION;
5325
+ await appendTrace(resolve8(traceFile), {
5326
+ sessionId: supplied && /^[a-zA-Z0-9_-]{1,80}$/.test(supplied) ? supplied : randomUUID3(),
5327
+ version,
5328
+ command: traceCommand,
5329
+ startedAt,
5330
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
5331
+ durationMs: Math.round(performance.now() - started),
5332
+ ok: !traceCode,
5333
+ ...traceCode ? { code: traceCode } : {}
5334
+ });
5335
+ } catch {
5336
+ process.stderr.write("makeables: Could not write the optional local trace.\n");
5337
+ }
3041
5338
  });