makeables 0.10.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,4 +1,16 @@
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";
2
14
  import {
3
15
  addKitMember,
4
16
  addToSheet,
@@ -13,41 +25,28 @@ import {
13
25
  readSheet,
14
26
  sheetPreviewPng,
15
27
  sheetReport
16
- } from "./chunk-TRIRNJSG.js";
28
+ } from "./chunk-6RBOBRL4.js";
17
29
  import {
18
30
  getSkill,
19
31
  skills,
20
32
  version
21
- } from "./chunk-SZTWKHNA.js";
33
+ } from "./chunk-4WLCMPRH.js";
22
34
  import {
23
35
  CloudError,
24
36
  InputError,
25
- PACKAGE_LIMIT,
26
37
  addBadgeSticker,
27
- badgeDesignObjectSchema,
28
- badgeDesignSchema,
29
38
  cardStyles,
30
- checkSubmittedSvg,
31
39
  connectAccount,
32
40
  createStickerDesign,
33
- defaultStickerMaterial,
34
41
  designCatalog,
35
- designProfile,
36
- draftFromPackage,
37
42
  findCardStyle,
38
43
  findDesign,
39
- hashBytes,
40
- jsonBytes,
41
- kitDocumentSchema,
42
44
  listCardDesigns,
43
45
  logout,
44
46
  newBadge,
45
47
  newCard,
46
48
  openBrowser,
47
- packageJson,
48
49
  readBadge,
49
- readPackageJson,
50
- readPackageZip,
51
50
  readSubmission,
52
51
  removeBadgeLayer,
53
52
  renderBadge,
@@ -57,33 +56,1503 @@ import {
57
56
  startStudio,
58
57
  stickerCut,
59
58
  stickerCutSvg,
60
- stickerMaterialSchema,
61
59
  stickerStyles,
62
60
  studioRequest,
63
61
  submitDesign,
64
62
  summarizeBadgeLayers,
65
- svgNodeSchema,
66
- svgNodeXml,
67
63
  writeAtomic
68
- } from "./chunk-G6OTYITI.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";
69
83
 
70
84
  // src/cli.ts
71
- import { spawnSync as spawnSync2 } from "child_process";
85
+ import { spawnSync as spawnSync3 } from "child_process";
72
86
  import { randomUUID as randomUUID3 } from "crypto";
73
87
  import { constants as constants4 } from "fs";
74
- import { access, lstat as lstat4, open as open4, readFile as readFile7 } from "fs/promises";
75
- import { dirname as dirname5, extname as extname4, resolve as resolve7 } from "path";
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";
76
90
  import { parseArgs } from "util";
77
- import sharp8 from "sharp";
78
- import { ZodError } from "zod";
91
+ import sharp9 from "sharp";
92
+ import { ZodError as ZodError2 } from "zod";
79
93
  import { zodToJsonSchema } from "zod-to-json-schema";
80
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
+
81
1550
  // src/card-import.ts
82
1551
  import { createHash } from "crypto";
83
- import { readFile, stat } from "fs/promises";
1552
+ import { readFile as readFile2, stat } from "fs/promises";
84
1553
  import { basename, extname } from "path";
85
1554
  import { DOMParser } from "@xmldom/xmldom";
86
- import sharp from "sharp";
1555
+ import sharp2 from "sharp";
87
1556
  var box = { x: 0, y: 0, w: 1200, h: 756 };
88
1557
  function elements(node) {
89
1558
  return Array.from(node.childNodes).filter((child) => child.nodeType === 1);
@@ -116,7 +1585,7 @@ async function importCard(file, name) {
116
1585
  const info = await stat(file);
117
1586
  if (!info.isFile() || info.size > 2e6)
118
1587
  throw new InputError("Import a local SVG or raster file below 2 MB.");
119
- const bytes = await readFile(file);
1588
+ const bytes = await readFile2(file);
120
1589
  const title = (name ?? basename(file, extname(file))).slice(0, 60);
121
1590
  const design = {
122
1591
  version: 1,
@@ -130,7 +1599,7 @@ async function importCard(file, name) {
130
1599
  back: { background: "#111111", layers: [] }
131
1600
  };
132
1601
  if (extname(file).toLowerCase() !== ".svg") {
133
- const meta = await sharp(bytes, { limitInputPixels: 24e6 }).metadata();
1602
+ const meta = await sharp2(bytes, { limitInputPixels: 24e6 }).metadata();
134
1603
  if (!meta.format || !["png", "jpeg", "webp"].includes(meta.format))
135
1604
  throw new InputError("Import PNG, JPEG, WebP or a self-contained SVG.");
136
1605
  design.assets = {
@@ -193,11 +1662,11 @@ async function importCard(file, name) {
193
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.`
194
1663
  );
195
1664
  }
196
- design.front.layers = groups.map((children, index) => {
1665
+ design.front.layers = groups.map((children, index2) => {
197
1666
  const first = children[0];
198
1667
  return {
199
- id: `source-${String(index + 1).padStart(2, "0")}`,
200
- 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),
201
1670
  kind: "svg",
202
1671
  nodes: Object.keys(inherited).length ? [{ tag: "g", attrs: inherited, children }] : children,
203
1672
  ...box,
@@ -210,7 +1679,7 @@ async function importCard(file, name) {
210
1679
  for (const layer of design.front.layers) {
211
1680
  if (layer.kind !== "svg") continue;
212
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>`;
213
- 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 });
214
1683
  let left = 1200, top = 756, right = -1, bottom = -1;
215
1684
  for (let y = 0; y < pixels.height; y++) {
216
1685
  for (let x = 0; x < pixels.width; x++) {
@@ -229,14 +1698,43 @@ async function importCard(file, name) {
229
1698
  return badgeDesignSchema.parse(design);
230
1699
  }
231
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
+
232
1730
  // src/images.ts
233
- import { readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
234
- import { extname as extname2, resolve } from "path";
1731
+ import { readFile as readFile3, stat as stat2, writeFile } from "fs/promises";
1732
+ import { extname as extname2, resolve as resolve2 } from "path";
235
1733
  async function readToolResult(path) {
236
1734
  const info = await stat2(path);
237
1735
  if (!info.isFile() || info.size > 12e6)
238
1736
  throw new InputError("Expected a result file below 12 MB.");
239
- let value = JSON.parse(await readFile2(path, "utf8"));
1737
+ let value = JSON.parse(await readFile3(path, "utf8"));
240
1738
  if (typeof value === "string") value = JSON.parse(value);
241
1739
  if (value && typeof value === "object" && "success" in value) {
242
1740
  if (!value.success) throw new InputError("The browser command failed.");
@@ -263,7 +1761,7 @@ async function imageParams(file, state, target = "portrait") {
263
1761
  const current = await readToolResult(state);
264
1762
  if (typeof current.revision !== "string")
265
1763
  throw new InputError("Inspect the editor to obtain its revision.");
266
- const bytes = await readFile2(file);
1764
+ const bytes = await readFile3(file);
267
1765
  if (bytes.length > 4e6) throw new InputError("Image must be smaller than 4 MB.");
268
1766
  return {
269
1767
  action: "import",
@@ -281,15 +1779,15 @@ async function extractImage(file, out) {
281
1779
  throw new InputError("The tool did not return a supported image.");
282
1780
  const bytes = Buffer.from(match[1], "base64");
283
1781
  if (bytes.length > 4e6) throw new InputError("Image is larger than 4 MB.");
284
- const path = resolve(out);
1782
+ const path = resolve2(out);
285
1783
  await writeFile(path, bytes, { flag: "wx", mode: 384 });
286
1784
  return { path, bytes: bytes.length };
287
1785
  }
288
1786
 
289
1787
  // src/kit-extract.ts
290
1788
  import { mkdir, mkdtemp, rename, rm, writeFile as writeFile2 } from "fs/promises";
291
- import { dirname, join, resolve as resolve2 } from "path";
292
- import sharp2 from "sharp";
1789
+ import { dirname, join as join2, resolve as resolve3 } from "path";
1790
+ import sharp3 from "sharp";
293
1791
  import { z } from "zod";
294
1792
  var regionSchema = z.object({
295
1793
  x: z.number().int().nonnegative(),
@@ -366,33 +1864,33 @@ function artworkRegions(foreground, w, h, gap, minArea) {
366
1864
  function removeOuterBackground(rgba, w, h, background, tolerance) {
367
1865
  const result = new Uint8Array(rgba), seen = new Uint8Array(w * h), queue = new Int32Array(w * h);
368
1866
  let head = 0, tail = 0;
369
- const add = (p) => {
1867
+ const add2 = (p) => {
370
1868
  if (seen[p]) return;
371
1869
  seen[p] = 1;
372
1870
  if (rgba[p * 4 + 3] < 16 || [0, 1, 2].reduce((n, c) => n + Math.abs(rgba[p * 4 + c] - background[c]), 0) <= tolerance)
373
1871
  queue[tail++] = p;
374
1872
  };
375
1873
  for (let x = 0; x < w; x++) {
376
- add(x);
377
- add((h - 1) * w + x);
1874
+ add2(x);
1875
+ add2((h - 1) * w + x);
378
1876
  }
379
1877
  for (let y = 0; y < h; y++) {
380
- add(y * w);
381
- add(y * w + w - 1);
1878
+ add2(y * w);
1879
+ add2(y * w + w - 1);
382
1880
  }
383
1881
  while (head < tail) {
384
1882
  const p = queue[head++], x = p % w, y = Math.floor(p / w);
385
1883
  result[p * 4 + 3] = 0;
386
- if (x) add(p - 1);
387
- if (x < w - 1) add(p + 1);
388
- if (y) add(p - w);
389
- if (y < h - 1) add(p + w);
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);
390
1888
  }
391
1889
  return result;
392
1890
  }
393
1891
  async function extractKit(file, out, input) {
394
1892
  const options = optionsSchema.parse(input);
395
- const { data, info } = await sharp2(resolve2(file), { limitInputPixels: 24e6 }).rotate().ensureAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
1893
+ const { data, info } = await sharp3(resolve3(file), { limitInputPixels: 24e6 }).rotate().ensureAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
396
1894
  const w = info.width, h = info.height;
397
1895
  const corners = [0, w - 1, (h - 1) * w, w * h - 1];
398
1896
  const transparentBackground = corners.every((p) => data[p * 4 + 3] < 16);
@@ -407,9 +1905,9 @@ async function extractKit(file, out, input) {
407
1905
  throw new InputError("Choose 1\u201324 regions using --params, or adjust gap/minArea.");
408
1906
  if (regions.some((r) => r.x + r.w > w || r.y + r.h > h))
409
1907
  throw new InputError("Every region must fit inside the image.");
410
- const target = resolve2(out), temporary = await mkdtemp(join(dirname(target), ".makeables-extract-"));
1908
+ const target = resolve3(out), temporary = await mkdtemp(join2(dirname(target), ".makeables-extract-"));
411
1909
  try {
412
- await mkdir(join(temporary, "pieces"));
1910
+ await mkdir(join2(temporary, "pieces"));
413
1911
  const kit = {
414
1912
  format: "makeables-kit",
415
1913
  version: 1,
@@ -418,11 +1916,11 @@ async function extractKit(file, out, input) {
418
1916
  sheets: []
419
1917
  };
420
1918
  const views = [];
421
- for (const [index, region] of regions.entries()) {
422
- const id = `piece-${String(index + 1).padStart(2, "0")}`, name = region.name ?? `Piece ${index + 1}`;
423
- const crop = await sharp2(data, { raw: { width: w, height: h, channels: 4 } }).extract({ left: region.x, top: region.y, width: region.w, height: region.h }).raw().toBuffer();
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();
424
1922
  const pixels = transparentBackground ? crop : removeOuterBackground(crop, region.w, region.h, background, options.tolerance);
425
- const png = await sharp2(pixels, { raw: { width: region.w, height: region.h, channels: 4 } }).png().toBuffer();
1923
+ const png = await sharp3(pixels, { raw: { width: region.w, height: region.h, channels: 4 } }).png().toBuffer();
426
1924
  const design = createStickerDesign();
427
1925
  design.sticker = { ...defaultStickerMaterial, finish: "vinyl", sparkle: 0, holography: 0 };
428
1926
  design.name = name;
@@ -455,19 +1953,19 @@ async function extractKit(file, out, input) {
455
1953
  };
456
1954
  design.back = { background: "#00000000", layers: [] };
457
1955
  await writeFile2(
458
- join(temporary, `pieces/${id}.json`),
1956
+ join2(temporary, `pieces/${id}.json`),
459
1957
  JSON.stringify(badgeDesignSchema.parse(design), null, 2)
460
1958
  );
461
1959
  kit.items.push({ id, file: `pieces/${id}.json` });
462
1960
  views.push({ name, bytes: png });
463
1961
  }
464
- await writeFile2(join(temporary, "kit.json"), JSON.stringify(kit, null, 2));
465
- await writeFile2(join(temporary, "contact.png"), await kitCover(options.name, views));
1962
+ await writeFile2(join2(temporary, "kit.json"), JSON.stringify(kit, null, 2));
1963
+ await writeFile2(join2(temporary, "contact.png"), await kitCover(options.name, views));
466
1964
  await mkdir(target);
467
1965
  await rename(temporary, target);
468
1966
  return {
469
- path: join(target, "kit.json"),
470
- preview: join(target, "contact.png"),
1967
+ path: join2(target, "kit.json"),
1968
+ preview: join2(target, "contact.png"),
471
1969
  items: kit.items.length,
472
1970
  regions,
473
1971
  source: { width: w, height: h },
@@ -479,6 +1977,246 @@ async function extractKit(file, out, input) {
479
1977
  }
480
1978
  }
481
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
+ });
2129
+ }
2130
+
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]");
2156
+ }
2157
+ return out.trim().slice(0, 400);
2158
+ }
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"
2167
+ };
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"]);
2197
+ return {
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
+ }
2211
+ };
2212
+ }
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).`;
2218
+ }
2219
+
482
2220
  // src/presentation.ts
483
2221
  import { stripVTControlCharacters } from "util";
484
2222
  function machineOutput(json) {
@@ -503,10 +2241,10 @@ function banner(version2) {
503
2241
  // src/publish.ts
504
2242
  import { createHash as createHash2, randomUUID } from "crypto";
505
2243
  import { constants } from "fs";
506
- import { appendFile, link, mkdir as mkdir2, open, readFile as readFile3, rename as rename2, rm as rm2 } from "fs/promises";
2244
+ import { appendFile, link, mkdir as mkdir2, open, readFile as readFile4, rename as rename2, rm as rm3 } from "fs/promises";
507
2245
  import { homedir } from "os";
508
- import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
509
- import sharp3 from "sharp";
2246
+ import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
2247
+ import sharp4 from "sharp";
510
2248
  import { z as z2 } from "zod";
511
2249
  async function saveBundle(url, out) {
512
2250
  const state = await studioRequest(url, "/call", {
@@ -528,7 +2266,7 @@ async function saveBundle(url, out) {
528
2266
  result.error?.message ?? "The editor could not export a bundle. Refresh it after saving the current draft."
529
2267
  );
530
2268
  const pkg = await readPackageJson(result.data);
531
- const path = resolve3(out);
2269
+ const path = resolve4(out);
532
2270
  const file = await open(path, "wx", 384);
533
2271
  try {
534
2272
  await file.writeFile(`${JSON.stringify(packageJson(pkg))}
@@ -537,7 +2275,7 @@ async function saveBundle(url, out) {
537
2275
  } finally {
538
2276
  await file.close();
539
2277
  }
540
- const saved = await readFile3(path);
2278
+ const saved = await readFile4(path);
541
2279
  return {
542
2280
  path,
543
2281
  bytes: saved.length,
@@ -557,12 +2295,12 @@ var receiptSchema = z2.object({
557
2295
  });
558
2296
 
559
2297
  // src/reference-image.ts
560
- import { spawnSync } from "child_process";
561
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
562
- import { tmpdir } from "os";
563
- import { join as join3, resolve as resolve4 } 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";
564
2302
  import { DOMParser as DOMParser2, XMLSerializer } from "@xmldom/xmldom";
565
- import sharp4 from "sharp";
2303
+ import sharp5 from "sharp";
566
2304
  import { z as z3 } from "zod";
567
2305
  var W = 1200;
568
2306
  var H = 756;
@@ -597,7 +2335,7 @@ function fail(message) {
597
2335
  }
598
2336
  var nums = (value, count, name) => {
599
2337
  const parts = value.split(",").map(Number);
600
- 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.`);
601
2339
  if (parts.length < count || parts.some((v) => !Number.isFinite(v)))
602
2340
  fail(`${name} needs ${count} comma-separated numbers.`);
603
2341
  return parts;
@@ -609,7 +2347,7 @@ function validBox(values, width, height) {
609
2347
  }
610
2348
  var lumOf = (r, g, b) => 0.299 * r + 0.587 * g + 0.114 * b;
611
2349
  async function detect(file) {
612
- const { data, info } = await sharp4(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 });
613
2351
  const { width: w, height: h } = info;
614
2352
  const px = (x, y) => {
615
2353
  const i = (y * w + x) * 3;
@@ -685,7 +2423,7 @@ async function detect(file) {
685
2423
  async function prepare(file, o) {
686
2424
  if (!o.crop || !o.out) fail('prepare needs --out <dir> and --params with {"crop":"x,y,w,h"}.');
687
2425
  const [cx, cy, cw, ch] = nums(o.crop, 4, "crop");
688
- const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
2426
+ const meta = await sharp5(file, { limitInputPixels: 24e6 }).metadata();
689
2427
  validBox([cx, cy, cw, ch], meta.width ?? 0, meta.height ?? 0);
690
2428
  if (Math.abs(cw / ch / (W / H) - 1) > 0.03)
691
2429
  fail(
@@ -698,12 +2436,12 @@ async function prepare(file, o) {
698
2436
  if (feather < 0 || ![0, 1].includes(flop))
699
2437
  fail("Patch feather must be nonnegative and flop must be 0 or 1.");
700
2438
  }
701
- const out = resolve4(o.out);
2439
+ const out = resolve5(o.out);
702
2440
  mkdirSync(out);
703
2441
  const layers = [];
704
2442
  for (const spec of o.patch) {
705
2443
  const [dx, dy, pw, ph, sx, sy, feather = 6, flop = 0] = nums(spec, 6, "patch");
706
- const patch = await sharp4(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();
707
2445
  const rgba = Buffer.alloc(pw * ph * 4);
708
2446
  for (let y = 0; y < ph; y++)
709
2447
  for (let x = 0; x < pw; x++) {
@@ -715,10 +2453,10 @@ async function prepare(file, o) {
715
2453
  }
716
2454
  layers.push({ input: rgba, raw: { width: pw, height: ph, channels: 4 }, left: dx, top: dy });
717
2455
  }
718
- const patched = await sharp4(file).removeAlpha().composite(layers).png().toBuffer();
719
- const card = await sharp4(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 });
720
2458
  const px = card.data, CW = card.info.width, CH = card.info.height;
721
- const src = await sharp4(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 });
722
2460
  const sp = (x, y) => {
723
2461
  const i = (y * src.info.width + x) * 3;
724
2462
  return [src.data[i], src.data[i + 1], src.data[i + 2]];
@@ -758,14 +2496,14 @@ async function prepare(file, o) {
758
2496
  if (y > 0) queue.push(i - CW);
759
2497
  if (y < CH - 1) queue.push(i + CW);
760
2498
  }
761
- const cardPng = await sharp4(px, { raw: card.info }).png().toBuffer();
762
- const resized = sharp4(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
763
- await resized.clone().png().toFile(join3(out, "card.png"));
764
- await resized.clone().jpeg({ quality: 84, mozjpeg: true }).toFile(join3(out, "card.jpg"));
765
- 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();
766
2504
  const g = await resized.clone().toColourspace("b-w").raw().toBuffer();
767
- writeFileSync(join3(out, "rgb.raw"), rgb);
768
- writeFileSync(join3(out, "lum.raw"), g);
2505
+ writeFileSync(join5(out, "rgb.raw"), rgb2);
2506
+ writeFileSync(join5(out, "lum.raw"), g);
769
2507
  const GW = W * 2, GH = H * 2, big = Buffer.alloc(GW * GH);
770
2508
  for (let y = 0; y < GH; y++) {
771
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;
@@ -777,14 +2515,14 @@ async function prepare(file, o) {
777
2515
  );
778
2516
  }
779
2517
  }
780
- writeFileSync(join3(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
2518
+ writeFileSync(join5(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
781
2519
  ${GW} ${GH}
782
2520
  255
783
2521
  `), big]));
784
2522
  const hex = paper.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
785
2523
  return {
786
2524
  out,
787
- preview: join3(out, "card.png"),
2525
+ preview: join5(out, "card.png"),
788
2526
  paper: `#${hex}`,
789
2527
  patches: o.patch.length,
790
2528
  filledEdgePixels: filled,
@@ -792,7 +2530,7 @@ ${GW} ${GH}
792
2530
  };
793
2531
  }
794
2532
  function potrace(input, extra) {
795
- const r = spawnSync(
2533
+ const r = spawnSync2(
796
2534
  "potrace",
797
2535
  [input, "-b", "svg", "-o", "-", "-W", `${W}pt`, "-H", `${H}pt`, ...extra],
798
2536
  { maxBuffer: 64 << 20 }
@@ -809,11 +2547,11 @@ function potrace(input, extra) {
809
2547
  }
810
2548
  var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
811
2549
  function vectorize(dir, o) {
812
- dir = resolve4(dir);
2550
+ dir = resolve5(dir);
813
2551
  if (!o.out || !o.title) fail("vectorize needs --out <file.svg> and --title");
814
2552
  const K = Number(o.tones ?? 7);
815
- const lum = readFileSync(join3(dir, "lum.raw")), rgb = readFileSync(join3(dir, "rgb.raw"));
816
- 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)
817
2555
  fail("Expected an intact directory created by image prepare.");
818
2556
  let c = [...Array(K)].map((_, i) => 30 + i * 200 / (K - 1));
819
2557
  const band = new Uint8Array(N);
@@ -851,9 +2589,9 @@ function vectorize(dir, o) {
851
2589
  let r = 0, g = 0, b = 0, n = 0;
852
2590
  for (let i = 0; i < N; i++)
853
2591
  if (band[i] === k) {
854
- r += rgb[i * 3];
855
- g += rgb[i * 3 + 1];
856
- b += rgb[i * 3 + 2];
2592
+ r += rgb2[i * 3];
2593
+ g += rgb2[i * 3 + 1];
2594
+ b += rgb2[i * 3 + 2];
857
2595
  n++;
858
2596
  }
859
2597
  const h = (v) => Math.round(v / Math.max(n, 1)).toString(16).padStart(2, "0");
@@ -862,11 +2600,11 @@ function vectorize(dir, o) {
862
2600
  const L = used.length;
863
2601
  const inkK = o.ink !== void 0 ? Number(o.ink) : L > 1 ? (c[used[0]] + c[used[1]]) / 2 / 255 : 0.25;
864
2602
  const groups = [];
865
- const maskFile = join3(dir, "mask.pbm");
2603
+ const maskFile = join5(dir, "mask.pbm");
866
2604
  for (let j = o.raster ? 0 : L - 2; j >= 0; j--) {
867
2605
  let traced;
868
2606
  if (j === 0) {
869
- traced = potrace(join3(dir, "ink.pgm"), [
2607
+ traced = potrace(join5(dir, "ink.pgm"), [
870
2608
  "-k",
871
2609
  inkK.toFixed(4),
872
2610
  "-t",
@@ -889,7 +2627,7 @@ ${W} ${H}
889
2627
  if (traced) groups.push(`<g transform="${traced.tr}" fill="${colors[j]}">${traced.paths}</g>`);
890
2628
  }
891
2629
  const base = o.raster ? `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>
892
- <image width="${W}" height="${H}" preserveAspectRatio="none" href="data:image/jpeg;base64,${readFileSync(join3(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]}"/>`;
893
2631
  const title = esc(o.title), desc = esc(o.desc ?? `${o.title} card artwork.`);
894
2632
  const meta = esc(o.credit ?? "Original artwork traced from a reference image.");
895
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}">
@@ -902,9 +2640,9 @@ ${groups.join("\n")}
902
2640
  `;
903
2641
  const checked = checkSubmittedSvg(svg);
904
2642
  if (!checked.ok) fail(checked.error);
905
- writeFileSync(resolve4(o.out), svg, { flag: "wx" });
2643
+ writeFileSync(resolve5(o.out), svg, { flag: "wx" });
906
2644
  return {
907
- out: resolve4(o.out),
2645
+ out: resolve5(o.out),
908
2646
  bytes: Buffer.byteLength(svg),
909
2647
  mode: o.raster ? "raster background + vector ink" : "pure vector",
910
2648
  tones: colors,
@@ -912,7 +2650,7 @@ ${groups.join("\n")}
912
2650
  };
913
2651
  }
914
2652
  async function rawImage(file) {
915
- const { data, info } = await sharp4(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 });
916
2654
  const px = (x, y) => {
917
2655
  if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= info.width || y >= info.height)
918
2656
  fail("Pixel coordinates are outside the image.");
@@ -1011,19 +2749,19 @@ async function glyphs(file, o) {
1011
2749
  if (!o.box || !o.at) fail("glyphs needs --box x,y,w,h and --at X,Y");
1012
2750
  const [bx, by, bw, bh] = nums(o.box, 4, "box");
1013
2751
  const [ax, ay] = nums(o.at, 2, "at");
1014
- const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
2752
+ const meta = await sharp5(file, { limitInputPixels: 24e6 }).metadata();
1015
2753
  validBox([bx, by, bw, bh], meta.width ?? 0, meta.height ?? 0);
1016
2754
  const up = Number(o.up ?? 4);
1017
2755
  if (bw * bh * up * up > 24e6)
1018
2756
  fail("Trace region is too large; lower up or choose a smaller box.");
1019
- const { data, info } = await sharp4(file).extract({ left: bx, top: by, width: bw, height: bh }).greyscale().removeAlpha().resize({ width: bw * up, kernel: "lanczos3" }).raw().toBuffer({ resolveWithObject: true });
1020
- const temp = mkdtempSync(join3(tmpdir(), "makeables-glyphs-"));
1021
- const pgm = join3(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");
1022
2760
  writeFileSync(pgm, Buffer.concat([Buffer.from(`P5
1023
2761
  ${info.width} ${info.height}
1024
2762
  255
1025
2763
  `), data]));
1026
- const r = spawnSync(
2764
+ const r = spawnSync2(
1027
2765
  "potrace",
1028
2766
  [pgm, "-b", "svg", "-o", "-", "-k", String(o.k ?? 0.5), "-t", "2", "-a", "1", "-O", "0.3"],
1029
2767
  { maxBuffer: 64 << 20 }
@@ -1037,18 +2775,18 @@ ${info.width} ${info.height}
1037
2775
  const paths = [...svg.matchAll(/ d="([^"]+)"/g)].map((m) => m[1].replace(/\s+/g, " ")).join(" ");
1038
2776
  if (!tr || !paths) fail("nothing dark enough to trace in that box; lower --k or check the box");
1039
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>`;
1040
- if (o.out) writeFileSync(resolve4(o.out), snippet, { flag: "wx" });
2778
+ if (o.out) writeFileSync(resolve5(o.out), snippet, { flag: "wx" });
1041
2779
  return {
1042
2780
  box: { x: bx, y: by, w: bw, h: bh },
1043
2781
  at: { x: ax, y: ay },
1044
2782
  bytes: snippet.length,
1045
- out: o.out ? resolve4(o.out) : null,
2783
+ out: o.out ? resolve5(o.out) : null,
1046
2784
  snippet: o.out ? void 0 : snippet
1047
2785
  };
1048
2786
  }
1049
2787
  function rotate(file, o) {
1050
2788
  if (!o.out) fail("rotate needs --out <card.svg>");
1051
- const text = readFileSync(file, "utf8");
2789
+ const text = readFileSync2(file, "utf8");
1052
2790
  const parser = new DOMParser2({ onError: (_level, message) => fail(`Malformed SVG: ${message}`) });
1053
2791
  if (/<!DOCTYPE|<!ENTITY/i.test(text)) fail("SVG declarations are not supported.");
1054
2792
  const root = parser.parseFromString(text, "image/svg+xml").documentElement;
@@ -1064,28 +2802,28 @@ function rotate(file, o) {
1064
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>`;
1065
2803
  const checked = checkSubmittedSvg(svg);
1066
2804
  if (!checked.ok) fail(checked.error);
1067
- writeFileSync(resolve4(o.out), svg, { flag: "wx" });
1068
- return { out: resolve4(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) };
1069
2807
  }
1070
2808
  async function compare(file, ref, o) {
1071
2809
  if (!o.out) fail("compare needs --out <file.png>");
1072
- let card = sharp4(readFileSync(file));
2810
+ let card = sharp5(readFileSync2(file));
1073
2811
  if (o.upright) card = card.rotate(o.upright === "left" ? 90 : -90);
1074
2812
  const cardPng = await card.png().toBuffer();
1075
- let reference = sharp4(ref);
2813
+ let reference = sharp5(ref);
1076
2814
  if (o["ref-box"]) {
1077
2815
  const [x, y, w, h] = nums(o["ref-box"], 4, "ref-box");
1078
2816
  reference = reference.extract({ left: x, top: y, width: w, height: h });
1079
2817
  }
1080
2818
  const height = 900;
1081
- const a = await sharp4(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
1082
- const b = await sharp4(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 });
1083
2821
  const gap = 24, width = a.info.width + b.info.width + gap * 3;
1084
- await sharp4({ 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([
1085
2823
  { input: a.data, left: gap, top: gap },
1086
2824
  { input: b.data, left: a.info.width + gap * 2, top: gap }
1087
- ]).png().toFile(resolve4(o.out));
1088
- return { out: resolve4(o.out), left: "card", right: "reference" };
2825
+ ]).png().toFile(resolve5(o.out));
2826
+ return { out: resolve5(o.out), left: "card", right: "reference" };
1089
2827
  }
1090
2828
  var imageTools = [
1091
2829
  "detect",
@@ -1105,8 +2843,8 @@ async function referenceImage(command, file, options, out, reference) {
1105
2843
  parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
1106
2844
  );
1107
2845
  const o = { ...parsed.data, out };
1108
- if (out && existsSync(resolve4(out))) fail("Output already exists. Choose a new --out path.");
1109
- 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.");
1110
2848
  switch (command) {
1111
2849
  case "detect":
1112
2850
  return detect(file);
@@ -1134,10 +2872,10 @@ async function referenceImage(command, file, options, out, reference) {
1134
2872
  }
1135
2873
 
1136
2874
  // src/sticker-cut.ts
1137
- import sharp5 from "sharp";
2875
+ import sharp6 from "sharp";
1138
2876
  async function stickerPrintCut(design) {
1139
2877
  const { png } = await renderBadge(design, { face: "front", person: {} });
1140
- const { data, info } = await sharp5(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
2878
+ const { data, info } = await sharp6(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1141
2879
  const material = design.sticker ?? defaultStickerMaterial;
1142
2880
  const cut = stickerCut(
1143
2881
  new Uint8ClampedArray(data.buffer, data.byteOffset, data.length),
@@ -1150,9 +2888,9 @@ async function stickerPrintCut(design) {
1150
2888
  }
1151
2889
 
1152
2890
  // src/sticker-material.ts
1153
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
2891
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1154
2892
  import { basename as basename2 } from "path";
1155
- import sharp6 from "sharp";
2893
+ import sharp7 from "sharp";
1156
2894
  async function bindStickerMap(design, layerId, path) {
1157
2895
  if (design.product !== "sticker") throw new InputError("Optical maps require a sticker.");
1158
2896
  const layer = design.front.layers.find((layer2) => layer2.id === layerId);
@@ -1160,8 +2898,8 @@ async function bindStickerMap(design, layerId, path) {
1160
2898
  const info = await stat3(path);
1161
2899
  if (!info.isFile() || info.size > 4e6)
1162
2900
  throw new InputError("Use an optical image below 4 MB.");
1163
- const bytes = await readFile4(path);
1164
- const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
2901
+ const bytes = await readFile5(path);
2902
+ const meta = await sharp7(bytes, { limitInputPixels: 16777216 }).metadata();
1165
2903
  if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
1166
2904
  throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
1167
2905
  const asset = design.sticker?.effectMap?.asset ?? "optical-map";
@@ -1208,9 +2946,9 @@ function compactReceipt(value) {
1208
2946
  if (Array.isArray(value)) return value.map(compactReceipt);
1209
2947
  if (value && typeof value === "object")
1210
2948
  return Object.fromEntries(
1211
- Object.entries(value).map(([key, entry]) => [
1212
- key,
1213
- key === "design" && entry && typeof entry === "object" ? {
2949
+ Object.entries(value).map(([key2, entry]) => [
2950
+ key2,
2951
+ key2 === "design" && entry && typeof entry === "object" ? {
1214
2952
  name: "name" in entry ? entry.name : void 0,
1215
2953
  product: "product" in entry ? entry.product : "badge",
1216
2954
  omitted: true
@@ -1224,11 +2962,11 @@ function compactReceipt(value) {
1224
2962
  // src/wallet.ts
1225
2963
  import { spawn } from "child_process";
1226
2964
  import { createHash as createHash3 } from "crypto";
1227
- import { existsSync as existsSync2 } from "fs";
1228
- import { chmod as chmod2, lstat as lstat3, mkdir as mkdir4, mkdtemp as mkdtemp3, readFile as readFile6, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
1229
- import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
1230
- import { dirname as dirname4, extname as extname3, join as join7, resolve as resolve6 } from "path";
1231
- 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";
1232
2970
 
1233
2971
  // src/wallet-release.json
1234
2972
  var wallet_release_default = {
@@ -1250,16 +2988,16 @@ var wallet_release_default = {
1250
2988
  // src/wallet-storage.ts
1251
2989
  import { randomUUID as randomUUID2 } from "crypto";
1252
2990
  import { constants as constants3 } from "fs";
1253
- import { chmod, lstat, mkdir as mkdir3, open as open3, rename as rename3, rm as rm3 } from "fs/promises";
2991
+ import { chmod, lstat, mkdir as mkdir3, open as open3, rename as rename3, rm as rm4 } from "fs/promises";
1254
2992
  import { homedir as homedir2 } from "os";
1255
- import { dirname as dirname3, join as join4, resolve as resolve5 } from "path";
2993
+ import { dirname as dirname3, join as join6, resolve as resolve6 } from "path";
1256
2994
  function walletHome() {
1257
- return resolve5(
1258
- process.env.MAKEABLES_WALLET_HOME ?? join4(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
2995
+ return resolve6(
2996
+ process.env.MAKEABLES_WALLET_HOME ?? join6(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1259
2997
  );
1260
2998
  }
1261
2999
  async function privateDirectory(path) {
1262
- const absolute = resolve5(path);
3000
+ const absolute = resolve6(path);
1263
3001
  const parent = dirname3(absolute);
1264
3002
  if (parent !== absolute) {
1265
3003
  try {
@@ -1289,7 +3027,7 @@ async function readPrivateJson(path) {
1289
3027
  }
1290
3028
  }
1291
3029
  async function writePrivateJson(path, value) {
1292
- const temporary = join4(dirname3(path), `.${randomUUID2()}.json`);
3030
+ const temporary = join6(dirname3(path), `.${randomUUID2()}.json`);
1293
3031
  try {
1294
3032
  const handle = await open3(temporary, "wx", 384);
1295
3033
  try {
@@ -1301,13 +3039,13 @@ async function writePrivateJson(path, value) {
1301
3039
  }
1302
3040
  await rename3(temporary, path);
1303
3041
  } finally {
1304
- await rm3(temporary, { force: true });
3042
+ await rm4(temporary, { force: true });
1305
3043
  }
1306
3044
  }
1307
3045
 
1308
3046
  // src/wallet-submission.ts
1309
- import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile3 } from "fs/promises";
1310
- import { join as join5 } 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";
1311
3049
  var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1312
3050
  function submissionId(input) {
1313
3051
  if (uuid.test(input)) return input.toLowerCase();
@@ -1355,16 +3093,16 @@ async function downloadWalletSubmission(input) {
1355
3093
  const pkg = await readPackageZip(bytes);
1356
3094
  if (pkg.manifest.product !== "card")
1357
3095
  throw new InputError("Wallet artwork requires a card submission.");
1358
- const directory = await mkdtemp2(
1359
- join5(
1360
- await privateDirectory(join5(await privateDirectory(walletHome()), "submissions")),
3096
+ const directory = await mkdtemp3(
3097
+ join7(
3098
+ await privateDirectory(join7(await privateDirectory(walletHome()), "submissions")),
1361
3099
  "download-"
1362
3100
  )
1363
3101
  );
1364
3102
  try {
1365
- const path = join5(directory, `${id}.makeables.zip`);
3103
+ const path = join7(directory, `${id}.makeables.zip`);
1366
3104
  await writeFile3(path, bytes, { mode: 384, flag: "wx" });
1367
- const saved = await readFile5(path);
3105
+ const saved = await readFile6(path);
1368
3106
  const sha256 = await hashBytes(saved);
1369
3107
  if (saved.length !== bytes.length || sha256 !== await hashBytes(bytes))
1370
3108
  throw new Error("Saved submission checksum does not match.");
@@ -1378,14 +3116,14 @@ async function downloadWalletSubmission(input) {
1378
3116
  elapsedMs: Math.round(performance.now() - started2)
1379
3117
  };
1380
3118
  } catch (error) {
1381
- await rm4(directory, { recursive: true, force: true });
3119
+ await rm5(directory, { recursive: true, force: true });
1382
3120
  throw error;
1383
3121
  }
1384
3122
  }
1385
3123
 
1386
3124
  // src/wallet-targets.ts
1387
3125
  import { lstat as lstat2 } from "fs/promises";
1388
- import { join as join6 } from "path";
3126
+ import { join as join8 } from "path";
1389
3127
  import lockfile from "proper-lockfile";
1390
3128
  import { z as z4 } from "zod";
1391
3129
  var alias = z4.string().regex(/^[a-z][a-z0-9-]{0,47}$/).refine((value) => !/\d{5}/.test(value));
@@ -1403,25 +3141,25 @@ var targetSchema = z4.object({
1403
3141
  createdAt: z4.string().datetime()
1404
3142
  }).strict();
1405
3143
  var registrySchema = z4.object({ version: z4.literal(1), targets: z4.array(targetSchema).max(100) }).strict();
1406
- function targetIdentity(values, key = "remember") {
3144
+ function targetIdentity(values, key2 = "remember") {
1407
3145
  const result = targetSchema.pick({ target: true, label: true, last4: true }).safeParse({
1408
- target: values[key],
3146
+ target: values[key2],
1409
3147
  label: values.label,
1410
3148
  last4: values.last4
1411
3149
  });
1412
3150
  if (!result.success)
1413
3151
  throw new InputError(
1414
- `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.`
1415
3153
  );
1416
3154
  return result.data;
1417
3155
  }
1418
3156
  function registryPath() {
1419
- return join6(walletHome(), "targets", "registry.json");
3157
+ return join8(walletHome(), "targets", "registry.json");
1420
3158
  }
1421
3159
  async function walletTargets() {
1422
3160
  const path = registryPath();
1423
3161
  try {
1424
- for (const parent of [walletHome(), join6(walletHome(), "targets")]) {
3162
+ for (const parent of [walletHome(), join8(walletHome(), "targets")]) {
1425
3163
  if (!(await lstat2(parent)).isDirectory())
1426
3164
  throw new InputError("Invalid Wallet target storage.");
1427
3165
  }
@@ -1444,7 +3182,7 @@ function targetChoices(targets) {
1444
3182
  }));
1445
3183
  }
1446
3184
  async function withWalletTargets(action) {
1447
- const directory = await privateDirectory(join6(await privateDirectory(walletHome()), "targets"));
3185
+ const directory = await privateDirectory(join8(await privateDirectory(walletHome()), "targets"));
1448
3186
  const release = await lockfile.lock(directory, { realpath: false, retries: 0 });
1449
3187
  try {
1450
3188
  return await action(await walletTargets(), async (targets) => {
@@ -1473,9 +3211,9 @@ var WalletError = class extends Error {
1473
3211
  };
1474
3212
  function installation() {
1475
3213
  const platform = `${process.platform}-${process.arch}`;
1476
- const cache = process.env.MAKEABLES_WALLET_CACHE ?? join7(homedir3(), "Library", "Caches", "Makeables", "wallet");
1477
- const root = resolve6(cache, `${wallet_release_default.version}-${platform}`);
1478
- return { platform, root, executable: join7(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") };
1479
3217
  }
1480
3218
  async function execute(command, args, progress) {
1481
3219
  return new Promise((accept, reject) => {
@@ -1542,7 +3280,7 @@ async function walletDoctor() {
1542
3280
  supported: false,
1543
3281
  reason: "Wallet artwork requires a Mac and an iPhone connected by USB."
1544
3282
  };
1545
- if (!existsSync2(installed))
3283
+ if (!existsSync3(installed))
1546
3284
  return {
1547
3285
  ready: false,
1548
3286
  supported: true,
@@ -1572,7 +3310,7 @@ async function setupWallet(progress) {
1572
3310
  return info;
1573
3311
  }
1574
3312
  const { platform, root, executable } = installation();
1575
- if (existsSync2(executable)) {
3313
+ if (existsSync3(executable)) {
1576
3314
  const info = await walletDoctor();
1577
3315
  if (!info.ready)
1578
3316
  throw new InputError("The installed Wallet companion is incomplete or incompatible.");
@@ -1607,9 +3345,9 @@ async function setupWallet(progress) {
1607
3345
  if (createHash3("sha256").update(bytes).digest("hex") !== artifact.sha256)
1608
3346
  throw new Error("Wallet companion checksum did not match. Nothing was installed.");
1609
3347
  await mkdir4(dirname4(root), { recursive: true, mode: 448 });
1610
- const stage = await mkdtemp3(join7(dirname4(root), ".install-"));
3348
+ const stage = await mkdtemp4(join9(dirname4(root), ".install-"));
1611
3349
  try {
1612
- const archive = join7(stage, "companion.tar.gz");
3350
+ const archive = join9(stage, "companion.tar.gz");
1613
3351
  await writeFile4(archive, bytes, { mode: 384, flag: "wx" });
1614
3352
  const members = (await execute("tar", ["-tzf", archive])).trim().split("\n");
1615
3353
  if (members.some(
@@ -1617,19 +3355,19 @@ async function setupWallet(progress) {
1617
3355
  ))
1618
3356
  throw new Error("Unexpected companion archive contents.");
1619
3357
  await execute("tar", ["-xzf", archive, "-C", stage]);
1620
- await rm5(archive);
1621
- await chmod2(join7(stage, "makeables-wallet", "makeables-wallet"), 448);
1622
- const info = await invoke(join7(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"]);
1623
3361
  if (info.version !== wallet_release_default.version || info.protocol !== wallet_release_default.protocol)
1624
3362
  throw new Error("Companion version does not match the installed CLI.");
1625
3363
  await rename4(stage, root);
1626
3364
  } finally {
1627
- await rm5(stage, { recursive: true, force: true });
3365
+ await rm6(stage, { recursive: true, force: true });
1628
3366
  }
1629
3367
  return walletDoctor();
1630
3368
  }
1631
3369
  async function walletArtwork(file, side2 = "front") {
1632
- const input = resolve6(file);
3370
+ const input = resolve7(file);
1633
3371
  const info = await lstat3(input);
1634
3372
  if (!info.isFile() || info.size > 336e5)
1635
3373
  throw new InputError("Expected artwork or a complete Makeables package below 32 MB.");
@@ -1642,16 +3380,16 @@ async function walletArtwork(file, side2 = "front") {
1642
3380
  if (!path) throw new InputError("This package has no back artwork.");
1643
3381
  source = Buffer.from(pkg.files[path]);
1644
3382
  } else {
1645
- source = await readFile6(input);
3383
+ source = await readFile7(input);
1646
3384
  if (extname3(input).toLowerCase() === ".svg") {
1647
3385
  const result = checkSubmittedSvg(source.toString("utf8"));
1648
3386
  if (!result.ok) throw new InputError(result.error);
1649
3387
  }
1650
3388
  }
1651
- 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();
1652
3390
  }
1653
3391
  async function preparedArtwork(directory) {
1654
- const path = resolve6(directory);
3392
+ const path = resolve7(directory);
1655
3393
  if (!(await lstat3(path)).isDirectory())
1656
3394
  throw new InputError("--assets must be a prepared artwork directory.");
1657
3395
  for (const name of [
@@ -1659,15 +3397,15 @@ async function preparedArtwork(directory) {
1659
3397
  "cardBackgroundCombined@2x.png",
1660
3398
  "cardBackgroundCombined.pdf"
1661
3399
  ]) {
1662
- const file = join7(path, name);
3400
+ const file = join9(path, name);
1663
3401
  const info = await lstat3(file);
1664
3402
  if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1665
3403
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1666
3404
  const signature = Buffer.from(name.endsWith(".pdf") ? "%PDF-" : "\x89PNG\r\n\n", "latin1");
1667
- if (!(await readFile6(file)).subarray(0, signature.length).equals(signature))
3405
+ if (!(await readFile7(file)).subarray(0, signature.length).equals(signature))
1668
3406
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1669
3407
  }
1670
- return { path, preview: join7(path, "cardBackgroundCombined@3x.png") };
3408
+ return { path, preview: join9(path, "cardBackgroundCombined@3x.png") };
1671
3409
  }
1672
3410
  async function walletCommand(command, values, progress) {
1673
3411
  if (values.remember) {
@@ -1877,7 +3615,7 @@ async function runWalletCommand(command, values, progress) {
1877
3615
  }
1878
3616
  }
1879
3617
  if (command === "wallet prep" || applying || installing) {
1880
- const work = await mkdtemp3(join7(tmpdir2(), "makeables-wallet-"));
3618
+ const work = await mkdtemp4(join9(tmpdir3(), "makeables-wallet-"));
1881
3619
  let phase = "prepare";
1882
3620
  let deviceWriteAttempted = false;
1883
3621
  let persistent;
@@ -1885,23 +3623,23 @@ async function runWalletCommand(command, values, progress) {
1885
3623
  try {
1886
3624
  let target;
1887
3625
  if (existing) {
1888
- target = resolve6(need("assets"));
3626
+ target = resolve7(need("assets"));
1889
3627
  prepared = existing;
1890
3628
  } else {
1891
3629
  report("prepare", "Preparing the complete card artwork.");
1892
3630
  const png = await walletArtwork(need("file"), values.side === "back" ? "back" : "front");
1893
- const raster = join7(work, "artwork.png");
3631
+ const raster = join9(work, "artwork.png");
1894
3632
  await writeFile4(raster, png, { mode: 384 });
1895
3633
  if (command === "wallet prep") {
1896
- target = resolve6(need("out"));
3634
+ target = resolve7(need("out"));
1897
3635
  } else if (installing && !applying) {
1898
3636
  const root = await privateDirectory(
1899
- join7(await privateDirectory(walletHome()), "prepared")
3637
+ join9(await privateDirectory(walletHome()), "prepared")
1900
3638
  );
1901
- persistent = await mkdtemp3(join7(root, "install-"));
1902
- target = join7(persistent, "assets");
3639
+ persistent = await mkdtemp4(join9(root, "install-"));
3640
+ target = join9(persistent, "assets");
1903
3641
  } else {
1904
- target = join7(work, "assets");
3642
+ target = join9(work, "assets");
1905
3643
  }
1906
3644
  prepared = await invoke(
1907
3645
  executable,
@@ -1987,8 +3725,8 @@ async function runWalletCommand(command, values, progress) {
1987
3725
  }
1988
3726
  throw error;
1989
3727
  } finally {
1990
- await rm5(work, { recursive: true, force: true });
1991
- if (persistent && phase === "prepare") await rm5(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 });
1992
3730
  }
1993
3731
  }
1994
3732
  throw new InputError("Unknown Wallet command.");
@@ -2039,6 +3777,16 @@ function walletSummary(result) {
2039
3777
 
2040
3778
  // src/cli.ts
2041
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>",
2042
3790
  "kit new --name <title> --out <kit.json>",
2043
3791
  "kit extract --file <artwork-sheet.png> --out <new-directory> [--params <json|@file>]",
2044
3792
  "kit add --file <kit.json> --ref <design|package> --id <piece-id>",
@@ -2048,13 +3796,14 @@ var commands = [
2048
3796
  "kit export --file <kit.json|kit.makeables.zip> --out <kit.makeables.zip> [--overwrite]",
2049
3797
  "kit import --file <kit.makeables.zip> --out <new-directory>",
2050
3798
  "designs list",
2051
- "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]",
2052
3800
  "material set --file <design.json> --params <json|@file>",
2053
3801
  "material map --file <sticker.json> --id <image-layer> --ref <optical.png|optical.webp>",
2054
- "new badge --style <id> --out <new-file> [--dry-run]",
2055
- "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]",
2056
3804
  "layer list --file <design.json>",
2057
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]",
2058
3807
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
2059
3808
  "layer remove --file <design.json> --id <layer> [--side front|back]",
2060
3809
  "cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
@@ -2063,6 +3812,12 @@ var commands = [
2063
3812
  "sheet pack --file <sheet.json>",
2064
3813
  "sheet check --file <sheet.json>",
2065
3814
  "sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--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]",
2066
3821
  "render --file <design.json|design.makeables.json> --out <png> [--appearance flat|material] [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
2067
3822
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
2068
3823
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
@@ -2127,7 +3882,7 @@ function emit(data, next, human, json) {
2127
3882
  );
2128
3883
  }
2129
3884
  async function newPath(file) {
2130
- const path = resolve7(file);
3885
+ const path = resolve8(file);
2131
3886
  try {
2132
3887
  await lstat4(path);
2133
3888
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
@@ -2193,8 +3948,26 @@ async function run() {
2193
3948
  size: { type: "string" },
2194
3949
  "width-mm": { type: "string" },
2195
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" },
2196
3961
  appearance: { type: "string" },
2197
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" },
2198
3971
  compact: { type: "boolean" },
2199
3972
  trace: { type: "string" }
2200
3973
  }
@@ -2234,6 +4007,26 @@ From a built checkout: npm run studio -- <command>`,
2234
4007
  "kit extract": ["file", "out", "params"],
2235
4008
  "kit add": ["file", "ref", "id"],
2236
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"],
2237
4030
  "kit inspect": ["file"],
2238
4031
  "kit render": ["file", "out", "overwrite"],
2239
4032
  "kit export": ["file", "out", "overwrite"],
@@ -2310,13 +4103,13 @@ From a built checkout: npm run studio -- <command>`,
2310
4103
  "design create": ["style", "out", "dry-run"],
2311
4104
  "design validate": ["file"],
2312
4105
  "designs list": [],
2313
- "new card": ["design", "style", "file", "name", "out", "dry-run"],
2314
- "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"],
2315
4108
  "material set": ["file", "params"],
2316
4109
  "material map": ["file", "id", "ref"],
2317
- "new badge": ["style", "out", "dry-run"],
4110
+ "new badge": ["style", "blank", "name", "out", "dry-run"],
2318
4111
  "layer list": ["file"],
2319
- "layer add": ["file", "sticker", "side", "relative-to"],
4112
+ "layer add": ["file", "sticker", "kind", "params", "side", "relative-to"],
2320
4113
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
2321
4114
  "layer remove": ["file", "id", "side"],
2322
4115
  cut: ["file", "out", "overwrite"],
@@ -2325,6 +4118,12 @@ From a built checkout: npm run studio -- <command>`,
2325
4118
  "sheet pack": ["file"],
2326
4119
  "sheet check": ["file"],
2327
4120
  "sheet export": ["file", "out", "preview", "overwrite"],
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"],
2328
4127
  render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number", "appearance"],
2329
4128
  validate: ["file", "out"]
2330
4129
  };
@@ -2337,12 +4136,156 @@ From a built checkout: npm run studio -- <command>`,
2337
4136
  `);
2338
4137
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
2339
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
+ }
2340
4274
  if (command.startsWith("kit ")) {
2341
4275
  const required = (value, flag) => {
2342
4276
  if (!value) throw new InputError(`Supply --${flag}.`);
2343
4277
  return value;
2344
4278
  };
2345
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
+ }
2346
4289
  if (command === "kit extract") {
2347
4290
  result2 = await extractKit(
2348
4291
  required(values.file, "file"),
@@ -2369,11 +4312,11 @@ From a built checkout: npm run studio -- <command>`,
2369
4312
  command === "kit add" ? "items" : "sheets"
2370
4313
  );
2371
4314
  await writeAtomic(path2, JSON.stringify(kit, null, 2));
2372
- result2 = { path: resolve7(path2), items: kit.items.length, sheets: kit.sheets.length };
4315
+ result2 = { path: resolve8(path2), items: kit.items.length, sheets: kit.sheets.length };
2373
4316
  } else if (command === "kit import") {
2374
4317
  result2 = await importKit(required(values.file, "file"), required(values.out, "out"));
2375
4318
  } else if (command === "kit inspect" && !required(values.file, "file").toLowerCase().endsWith(".zip")) {
2376
- const value = JSON.parse(await readFile7(required(values.file, "file"), "utf8"));
4319
+ const value = JSON.parse(await readFile8(required(values.file, "file"), "utf8"));
2377
4320
  if (value.format === "makeables-kit") result2 = await readKit(required(values.file, "file"));
2378
4321
  else {
2379
4322
  const pkg = await readSubmission(required(values.file, "file"));
@@ -2385,7 +4328,7 @@ From a built checkout: npm run studio -- <command>`,
2385
4328
  const { pkg, bytes } = await exportKit(required(values.file, "file"));
2386
4329
  if (command === "kit inspect") result2 = pkg.manifest;
2387
4330
  else {
2388
- const out = resolve7(required(values.out, "out"));
4331
+ const out = resolve8(required(values.out, "out"));
2389
4332
  const rendering = command === "kit render";
2390
4333
  if (!out.endsWith(rendering ? ".png" : ".zip"))
2391
4334
  throw new InputError(rendering ? "--out must end in .png." : "--out must end in .zip.");
@@ -2468,7 +4411,12 @@ From a built checkout: npm run studio -- <command>`,
2468
4411
  throw new InputError(
2469
4412
  "Supply --file with editable JSON, an original SVG or a Makeables package."
2470
4413
  );
2471
- 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({
2472
4420
  title: values.title,
2473
4421
  credit: values.credit,
2474
4422
  creditUrl: values["credit-url"],
@@ -2518,7 +4466,7 @@ From a built checkout: npm run studio -- <command>`,
2518
4466
  }
2519
4467
  if (command === "doctor") {
2520
4468
  const check = (command2) => {
2521
- const result2 = spawnSync2(command2, ["--version"], {
4469
+ const result2 = spawnSync3(command2, ["--version"], {
2522
4470
  encoding: "utf8",
2523
4471
  timeout: 5e3,
2524
4472
  stdio: ["ignore", "pipe", "pipe"]
@@ -2574,7 +4522,7 @@ Run makeables skills get core for setup.`,
2574
4522
  }
2575
4523
  if (command === "mcp") {
2576
4524
  try {
2577
- const { runMcp } = await import("./mcp-VS5XYXVM.js");
4525
+ const { runMcp } = await import("./mcp-YBRUVIWG.js");
2578
4526
  await runMcp(values.root ?? process.cwd());
2579
4527
  } catch (error) {
2580
4528
  process.stderr.write(
@@ -2633,7 +4581,7 @@ Keep this process running while you design.`,
2633
4581
  const info = await lstat4(path2);
2634
4582
  if (!info.isFile() || info.size > 12e6)
2635
4583
  throw new InputError("Expected a params file below 12 MB.");
2636
- params = JSON.parse(await readFile7(path2, "utf8"));
4584
+ params = JSON.parse(await readFile8(path2, "utf8"));
2637
4585
  } else if (values.params) params = JSON.parse(values.params);
2638
4586
  }
2639
4587
  const result2 = await studioRequest(
@@ -2739,14 +4687,14 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
2739
4687
  return;
2740
4688
  }
2741
4689
  if (!values.file) throw new InputError("Supply --file to validate a document.");
2742
- const path = resolve7(values.file);
4690
+ const path = resolve8(values.file);
2743
4691
  const handle = await open4(path, "r");
2744
4692
  let input;
2745
4693
  try {
2746
4694
  const info = await handle.stat();
2747
4695
  if (!info.isFile() || info.size > 2 * 1024 * 1024)
2748
4696
  throw new InputError("Expected a JSON file smaller than 2 MB.");
2749
- input = JSON.parse(await readFile7(handle, "utf8"));
4697
+ input = JSON.parse(await readFile8(handle, "utf8"));
2750
4698
  } finally {
2751
4699
  await handle.close();
2752
4700
  }
@@ -2765,7 +4713,7 @@ Schema and layout constraints passed.`,
2765
4713
  }
2766
4714
  async function readParams(value) {
2767
4715
  return JSON.parse(
2768
- value.startsWith("@") ? await readFile7(resolve7(value.slice(1)), "utf8") : value
4716
+ value.startsWith("@") ? await readFile8(resolve8(value.slice(1)), "utf8") : value
2769
4717
  );
2770
4718
  }
2771
4719
  async function cardCommand(command, values, json) {
@@ -2776,12 +4724,26 @@ async function cardCommand(command, values, json) {
2776
4724
  };
2777
4725
  const renderHint = (path, face, design) => `npx makeables render --file ${JSON.stringify(path)} --side ${face} --out ${design.product ?? "badge"}.png --overwrite`;
2778
4726
  if (command === "new sticker") {
4727
+ if (values.blank && values.style) throw new InputError("Choose --blank or --style, not both.");
2779
4728
  const preset = stickerStyles.find(
2780
4729
  (entry) => entry.source === (values.style ?? "typingmind-absolutely-right")
2781
4730
  );
2782
4731
  if (!preset)
2783
4732
  throw new InputError("Unknown sticker style. Run makeables styles list --product sticker.");
2784
- 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
+ );
2785
4747
  const path = values.out ? await newPath(String(values.out)) : null;
2786
4748
  const dryRun = Boolean(values["dry-run"]);
2787
4749
  if (path && !dryRun) await writeAtomic(path, `${JSON.stringify(design, null, 2)}
@@ -2798,10 +4760,29 @@ ${path ?? "No file written. Add --out sticker.json."}`,
2798
4760
  );
2799
4761
  return true;
2800
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
+ }
2801
4782
  if (command === "new badge" || command === "new card") {
2802
4783
  const card = command === "new card";
2803
4784
  if (card && [values.design, values.style, values.file].filter(Boolean).length !== 1)
2804
- throw new InputError("Choose exactly one --design, --style or --file.");
4785
+ throw new InputError("Choose exactly one --design, --style, --file or --blank.");
2805
4786
  const preset = card && values.style ? findCardStyle(need(values.style, "style")) : void 0;
2806
4787
  if (card && values.style && !preset)
2807
4788
  throw new InputError("Unknown card style. Run makeables styles list --product card.");
@@ -2827,7 +4808,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2827
4808
  return true;
2828
4809
  }
2829
4810
  if (command === "material map") {
2830
- const path = resolve7(need(file, "file"));
4811
+ const path = resolve8(need(file, "file"));
2831
4812
  const next = await bindStickerMap(
2832
4813
  await readBadge(path),
2833
4814
  need(values.id, "id"),
@@ -2843,10 +4824,10 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2843
4824
  return true;
2844
4825
  }
2845
4826
  if (command === "material set") {
2846
- const path = resolve7(need(file, "file"));
4827
+ const path = resolve8(need(file, "file"));
2847
4828
  const design = await readBadge(path);
2848
4829
  const raw = need(values.params, "params");
2849
- const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
4830
+ const input = JSON.parse(raw.startsWith("@") ? await readFile8(raw.slice(1), "utf8") : raw);
2850
4831
  if (design.product === "sticker") {
2851
4832
  const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
2852
4833
  if (!patch2.success)
@@ -2905,11 +4886,11 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2905
4886
  const changes = values.params ? await readParams(String(values.params)) : {};
2906
4887
  if (typeof changes !== "object" || changes === null || Array.isArray(changes))
2907
4888
  throw new InputError("--params must be a JSON object of layer fields.");
2908
- for (const key of ["x", "y", "rotation"])
2909
- if (values[key] !== void 0) {
2910
- const value = Number(values[key]);
2911
- if (!Number.isFinite(value)) throw new InputError(`--${key} must be a number.`);
2912
- 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;
2913
4894
  }
2914
4895
  design = setBadgeLayer(design, face, id, changes);
2915
4896
  }
@@ -2923,6 +4904,27 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2923
4904
  );
2924
4905
  return true;
2925
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
+ }
2926
4928
  if (command === "layer add") {
2927
4929
  const path = need(file, "file");
2928
4930
  const face = side(values.side);
@@ -2932,7 +4934,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2932
4934
  await readBadge(path),
2933
4935
  face,
2934
4936
  need(values.sticker, "sticker"),
2935
- values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve7(path))
4937
+ values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve8(path))
2936
4938
  );
2937
4939
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
2938
4940
  `);
@@ -2945,6 +4947,97 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2945
4947
  );
2946
4948
  return true;
2947
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
+ }
2948
5041
  if (command.startsWith("sheet ")) {
2949
5042
  const write = async (target, bytes) => {
2950
5043
  const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
@@ -3026,9 +5119,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3026
5119
  throw new InputError(
3027
5120
  `The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
3028
5121
  );
3029
- const out = resolve7(need(values.out, "out"));
5122
+ const out = resolve8(need(values.out, "out"));
3030
5123
  if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
3031
- const preview = typeof values.preview === "string" ? resolve7(values.preview) : void 0;
5124
+ const preview = typeof values.preview === "string" ? resolve8(values.preview) : void 0;
3032
5125
  if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
3033
5126
  const targets = [out, ...preview ? [preview] : []];
3034
5127
  if (!values.overwrite) for (const target of targets) await newPath(target);
@@ -3046,7 +5139,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3046
5139
  if (command === "cut") {
3047
5140
  const design = await readBadge(need(file, "file"));
3048
5141
  if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
3049
- const out = typeof values.out === "string" ? resolve7(values.out) : void 0;
5142
+ const out = typeof values.out === "string" ? resolve8(values.out) : void 0;
3050
5143
  if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
3051
5144
  const target = out && (values.overwrite ? out : await newPath(out));
3052
5145
  const { cut, svg } = await stickerPrintCut(design);
@@ -3087,16 +5180,16 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3087
5180
  throw new InputError("--appearance must be flat or material.");
3088
5181
  const appearance = values.appearance;
3089
5182
  const path = need(file, "file");
3090
- const out = resolve7(need(values.out, "out"));
5183
+ const out = resolve8(need(values.out, "out"));
3091
5184
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
3092
5185
  const target = values.overwrite ? out : await newPath(out);
3093
5186
  const number = values.number === void 0 ? void 0 : Number(values.number);
3094
5187
  if (number !== void 0 && !Number.isInteger(number))
3095
5188
  throw new InputError("--number must be an integer.");
3096
5189
  const face = side(values.side);
3097
- const value = JSON.parse(await readFile7(resolve7(path), "utf8"));
5190
+ const value = JSON.parse(await readFile8(resolve8(path), "utf8"));
3098
5191
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
3099
- 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))
3100
5193
  throw new InputError(
3101
5194
  "Portable packages include their participant and images. Edit the package before rendering, or use these flags with a plain design JSON."
3102
5195
  );
@@ -3158,13 +5251,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3158
5251
  return true;
3159
5252
  }
3160
5253
  if (command === "validate") {
3161
- const path = resolve7(need(file, "file"));
5254
+ const path = resolve8(need(file, "file"));
3162
5255
  if (extname4(path).toLowerCase() === ".svg") {
3163
- const source = await readFile7(path, "utf8");
5256
+ const source = await readFile8(path, "utf8");
3164
5257
  const result2 = checkSubmittedSvg(source);
3165
5258
  if (!result2.ok) throw new InputError(result2.error);
3166
5259
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
3167
- if (preview) await sharp8(Buffer.from(result2.svg)).png().toFile(preview);
5260
+ if (preview) await sharp9(Buffer.from(result2.svg)).png().toFile(preview);
3168
5261
  emit(
3169
5262
  {
3170
5263
  valid: true,
@@ -3181,7 +5274,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3181
5274
  return true;
3182
5275
  }
3183
5276
  if (values.out) throw new InputError("--out is only used to preview SVG validation.");
3184
- const input = JSON.parse(await readFile7(path, "utf8"));
5277
+ const input = JSON.parse(await readFile8(path, "utf8"));
3185
5278
  const result = badgeDesignSchema.safeParse(input);
3186
5279
  if (!result.success)
3187
5280
  throw new InputError(
@@ -3199,7 +5292,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3199
5292
  return false;
3200
5293
  }
3201
5294
  run().catch((error) => {
3202
- const userError = error instanceof InputError || error instanceof ZodError || 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 || [
3203
5296
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
3204
5297
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
3205
5298
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",
@@ -3229,7 +5322,7 @@ run().catch((error) => {
3229
5322
  if (!traceFile) return;
3230
5323
  try {
3231
5324
  const supplied = process.env.MAKEABLES_TRACE_SESSION;
3232
- await appendTrace(resolve7(traceFile), {
5325
+ await appendTrace(resolve8(traceFile), {
3233
5326
  sessionId: supplied && /^[a-zA-Z0-9_-]{1,80}$/.test(supplied) ? supplied : randomUUID3(),
3234
5327
  version,
3235
5328
  command: traceCommand,