makeables 0.10.0 → 0.12.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-YOGNQAYJ.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-7A4TRTVE.js";
17
29
  import {
18
30
  getSkill,
19
31
  skills,
20
32
  version
21
- } from "./chunk-SZTWKHNA.js";
33
+ } from "./chunk-WEPC5LJ7.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,2107 @@ 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-MXT43NAQ.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-KLJRRHMY.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
+ if (length(sub(x.p, y.p)) > adapter.tolerance) return false;
457
+ return isSpherical(adapter, x.kind) || dot(x.a, y.a) <= -0.99;
458
+ }
459
+ var isSpherical = (adapter, kind) => adapter.spherical?.includes(kind) ?? false;
460
+ function axialMates(x, y, adapter) {
461
+ if (length(cross(x.a, y.a)) > 0.01) return false;
462
+ const d = sub(y.p, x.p);
463
+ const along = dot(d, x.a);
464
+ if (length(sub(d, scale(x.a, along))) > adapter.tolerance) return false;
465
+ const hx = (x.len ?? 0) / 2;
466
+ const hy = (y.len ?? 0) / 2;
467
+ return Math.min(hx, along + hy) - Math.max(-hx, along - hy) >= adapter.minEngagement;
468
+ }
469
+ var GRID = 2;
470
+ function nearPairs(boxes, margin) {
471
+ const cells = /* @__PURE__ */ new Map();
472
+ const span = (lo, hi) => [
473
+ Math.floor((lo - margin) / GRID),
474
+ Math.floor((hi + margin) / GRID)
475
+ ];
476
+ boxes.forEach((b, i) => {
477
+ const [x0, x1] = span(b.min[0], b.max[0]);
478
+ const [y0, y1] = span(b.min[1], b.max[1]);
479
+ const [z0, z1] = span(b.min[2], b.max[2]);
480
+ for (let x = x0; x <= x1; x++)
481
+ for (let y = y0; y <= y1; y++)
482
+ for (let z5 = z0; z5 <= z1; z5++) {
483
+ const key2 = `${x},${y},${z5}`;
484
+ const list = cells.get(key2);
485
+ if (list) list.push(i);
486
+ else cells.set(key2, [i]);
487
+ }
488
+ });
489
+ const seen = /* @__PURE__ */ new Set();
490
+ const out = [];
491
+ const n = boxes.length;
492
+ for (const list of cells.values())
493
+ for (let p = 0; p < list.length; p++)
494
+ for (let q = p + 1; q < list.length; q++) {
495
+ const [a, b] = list[p] < list[q] ? [list[p], list[q]] : [list[q], list[p]];
496
+ if (seen.has(a * n + b)) continue;
497
+ seen.add(a * n + b);
498
+ if (touches(boxes[a], boxes[b], margin)) out.push([a, b]);
499
+ }
500
+ return out.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
501
+ }
502
+ var touches = (x, y, margin) => [0, 1, 2].every((k) => x.min[k] - margin <= y.max[k] && y.min[k] - margin <= x.max[k]);
503
+ function reachBox(lib, item, connectors) {
504
+ const { min, max } = worldBody(lib, item);
505
+ const lo = [...min];
506
+ const hi = [...max];
507
+ for (const c of connectors) {
508
+ const h = (c.len ?? 0) / 2;
509
+ for (const p of [add(c.p, scale(c.a, h)), add(c.p, scale(c.a, -h))])
510
+ for (let k = 0; k < 3; k++) {
511
+ lo[k] = Math.min(lo[k], p[k]);
512
+ hi[k] = Math.max(hi[k], p[k]);
513
+ }
514
+ }
515
+ return { min: lo, max: hi };
516
+ }
517
+ function contacts(model, lib, adapter) {
518
+ const world = model.items.map((it) => worldConnectors(lib, it));
519
+ const reach = model.items.map((it, i) => reachBox(lib, it, world[i]));
520
+ const out = [];
521
+ for (const [a, b] of nearPairs(reach, adapter.tolerance)) {
522
+ const kinds = /* @__PURE__ */ new Map();
523
+ for (const x of world[a])
524
+ for (const y of world[b]) {
525
+ if (!adapter.fits[x.kind]?.includes(y.kind)) continue;
526
+ const axial = x.len !== void 0 && y.len !== void 0;
527
+ if (axial ? !axialMates(x, y, adapter) : !pointMates(x, y, adapter)) continue;
528
+ const kind = [x.kind, y.kind].sort().join("-");
529
+ const seen = kinds.get(kind);
530
+ kinds.set(kind, {
531
+ count: (seen?.count ?? 0) + 1,
532
+ axis: axial ? x.a : void 0,
533
+ release: axial || isSpherical(adapter, x.kind) ? void 0 : x.a,
534
+ sum: add(seen?.sum ?? [0, 0, 0], x.p)
535
+ });
536
+ }
537
+ for (const [kind, { sum, ...v }] of kinds)
538
+ out.push({ a, b, kind, ...v, at: scale(sum, 1 / v.count) });
539
+ }
540
+ return out;
541
+ }
542
+ function overlapVolume(x, y, epsilon = 0.02) {
543
+ let v = 1;
544
+ for (let k = 0; k < 3; k++) {
545
+ const d = Math.min(x.max[k], y.max[k]) - Math.max(x.min[k], y.min[k]) - 2 * epsilon;
546
+ if (d <= 0) return 0;
547
+ v *= d;
548
+ }
549
+ return v;
550
+ }
551
+ function meshBounds(triangles) {
552
+ const min = [Infinity, Infinity, Infinity];
553
+ const max = [-Infinity, -Infinity, -Infinity];
554
+ for (let i = 0; i < triangles.length; i++) {
555
+ min[i % 3] = Math.min(min[i % 3], triangles[i]);
556
+ max[i % 3] = Math.max(max[i % 3], triangles[i]);
557
+ }
558
+ return { min, max };
559
+ }
560
+ function partVoxels(part2) {
561
+ if (!part2.voxels) {
562
+ const { min, max } = meshBounds(part2.triangles);
563
+ part2.voxels = voxelize(part2.triangles, min, max);
564
+ }
565
+ return part2.voxels;
566
+ }
567
+ function collisionCells(lib, x, y, ignore) {
568
+ let hits = 0;
569
+ for (const [a, b] of [
570
+ [x, y],
571
+ [y, x]
572
+ ]) {
573
+ const va = partVoxels(partOf(lib, a));
574
+ const vb = partVoxels(partOf(lib, b));
575
+ for (let k = 0; k < va.dims[2]; k++)
576
+ for (let j = 0; j < va.dims[1]; j++)
577
+ for (let i = 0; i < va.dims[0]; i++) {
578
+ if (!va.interior[(k * va.dims[1] + j) * va.dims[0] + i]) continue;
579
+ const local = [
580
+ va.origin[0] + (i + 0.5) * CELL,
581
+ va.origin[1] + (j + 0.5) * CELL,
582
+ va.origin[2] + (k + 0.5) * CELL
583
+ ];
584
+ const world = add(apply(a.rotation, local), a.position);
585
+ if (ignore && length(sub(world, ignore.centre)) < ignore.radius) continue;
586
+ const r = b.rotation;
587
+ const d = sub(world, b.position);
588
+ const inB = [
589
+ r[0] * d[0] + r[3] * d[1] + r[6] * d[2],
590
+ r[1] * d[0] + r[4] * d[1] + r[7] * d[2],
591
+ r[2] * d[0] + r[5] * d[1] + r[8] * d[2]
592
+ ];
593
+ if (cellAt(vb, inB)) hits++;
594
+ }
595
+ }
596
+ return hits;
597
+ }
598
+ var LDU = 20;
599
+ var FLIP = [1, 0, 0, 0, -1, 0, 0, 0, 1];
600
+ var normalizeName = (name) => name.trim().replace(/\\/g, "/").toLowerCase();
601
+ function toStuds(rotation, position) {
602
+ return {
603
+ rotation: mul(mul(FLIP, rotation), FLIP),
604
+ position: [position[0] / LDU, -position[1] / LDU, position[2] / LDU]
605
+ };
606
+ }
607
+ function readRef(f) {
608
+ const n = f.slice(2, 14).map(Number);
609
+ return {
610
+ color: Number(f[1]),
611
+ position: [n[0], n[1], n[2]],
612
+ rotation: [n[3], n[4], n[5], n[6], n[7], n[8], n[9], n[10], n[11]],
613
+ name: normalizeName(f.slice(14).join(" "))
614
+ };
615
+ }
616
+ function parsePart(name, resolve9, isJoint2) {
617
+ const triangles = [];
618
+ const colors = [];
619
+ const edges = [];
620
+ const joints = [];
621
+ const min = [Infinity, Infinity, Infinity];
622
+ const max = [-Infinity, -Infinity, -Infinity];
623
+ const walk = (file, m, t, inJoint, depth, color) => {
624
+ const text = resolve9(file);
625
+ if (text === void 0) throw new Error(`LDraw file not found: ${file}`);
626
+ if (depth > 32) throw new Error(`LDraw nesting too deep at ${file}`);
627
+ for (const line of text.split(/\r?\n/)) {
628
+ const f = line.trim().split(/\s+/);
629
+ const type = f[0];
630
+ if (type === "1" && f.length >= 15) {
631
+ const ref = readRef(f);
632
+ const rotation = mul(m, ref.rotation);
633
+ const position = add(apply(m, ref.position), t);
634
+ const joint = !inJoint && isJoint2(ref.name);
635
+ if (joint) joints.push({ name: ref.name, ...toStuds(rotation, position) });
636
+ const inherited = ref.color === 16 ? color : ref.color;
637
+ walk(ref.name, rotation, position, inJoint || joint, depth + 1, inherited);
638
+ } else if (type === "2" && f.length >= 8) {
639
+ const n = f.slice(2, 8).map(Number);
640
+ for (const k of [0, 3]) {
641
+ const w = add(apply(m, [n[k], n[k + 1], n[k + 2]]), t);
642
+ edges.push(w[0] / LDU, -w[1] / LDU, w[2] / LDU);
643
+ }
644
+ } else if (type === "3" || type === "4") {
645
+ const n = f.slice(2).map(Number);
646
+ const pts = [];
647
+ for (let i = 0; i < (type === "3" ? 3 : 4); i++) {
648
+ const w = add(apply(m, [n[i * 3], n[i * 3 + 1], n[i * 3 + 2]]), t);
649
+ pts.push([w[0] / LDU, -w[1] / LDU, w[2] / LDU]);
650
+ }
651
+ const tris = type === "3" ? [pts] : [
652
+ [pts[0], pts[1], pts[2]],
653
+ [pts[0], pts[2], pts[3]]
654
+ ];
655
+ const c = Number(f[1]);
656
+ for (const tri of tris) {
657
+ for (const p of tri) triangles.push(p[0], p[1], p[2]);
658
+ colors.push(c === 16 ? color : c);
659
+ }
660
+ if (!inJoint)
661
+ for (const p of pts)
662
+ for (let k = 0; k < 3; k++) {
663
+ min[k] = Math.min(min[k], p[k]);
664
+ max[k] = Math.max(max[k], p[k]);
665
+ }
666
+ }
667
+ }
668
+ };
669
+ walk(normalizeName(name), IDENTITY, [0, 0, 0], false, 0, 16);
670
+ return { triangles, colors, edges, body: { min, max }, joints };
671
+ }
672
+ function parseModel(text) {
673
+ const items = [];
674
+ let step = 1;
675
+ for (const line of text.split(/\r?\n/)) {
676
+ const f = line.trim().split(/\s+/);
677
+ if (f[0] === "0" && f[1]?.toUpperCase() === "STEP") step++;
678
+ else if (f[0] === "1" && f.length >= 15) {
679
+ const ref = readRef(f);
680
+ items.push({
681
+ part: ref.name.replace(/\.dat$/, ""),
682
+ color: ref.color,
683
+ step,
684
+ ...toStuds(ref.rotation, ref.position)
685
+ });
686
+ }
687
+ }
688
+ return { items };
689
+ }
690
+ function writeModel(model, title) {
691
+ const fmt = (n) => String(Math.round(n * 1e4) / 1e4 || 0);
692
+ const lines = [
693
+ `0 ${title}`,
694
+ `0 Name: ${title.toLowerCase().replace(/\W+/g, "-")}.ldr`,
695
+ "0 Author: Makeables"
696
+ ];
697
+ let step;
698
+ for (const it of model.items) {
699
+ if (step !== void 0 && it.step !== step) lines.push("0 STEP");
700
+ step = it.step;
701
+ const r = mul(mul(FLIP, it.rotation), FLIP);
702
+ const p = [it.position[0] * LDU, -it.position[1] * LDU, it.position[2] * LDU];
703
+ lines.push(`1 ${it.color} ${[...p, ...r].map(fmt).join(" ")} ${it.part}.dat`);
704
+ }
705
+ return `${lines.join("\n")}
706
+ `;
707
+ }
708
+ var REACH = 0.45;
709
+ var DIAGONAL = 0.2;
710
+ var MIN_RUN = 0.4;
711
+ var SAMPLE = 0.05;
712
+ var ring8 = [0, 1, 2, 3, 4, 5, 6, 7].map((k) => [
713
+ Math.cos(k * Math.PI / 4),
714
+ Math.sin(k * Math.PI / 4)
715
+ ]);
716
+ function technicHoles(voxels, body) {
717
+ const found = [];
718
+ for (let axis = 0; axis < 3; axis++) {
719
+ const u = (axis + 1) % 3;
720
+ const w = (axis + 2) % 3;
721
+ const at = (cu, cw, t, du = 0, dw = 0) => {
722
+ const p = [0, 0, 0];
723
+ p[axis] = t;
724
+ p[u] = cu + du;
725
+ p[w] = cw + dw;
726
+ return cellAt(voxels, p);
727
+ };
728
+ const from = (k) => Math.ceil((body.min[k] + 0.3) / SAMPLE - 1e-9) * SAMPLE;
729
+ for (let cu = from(u); cu <= body.max[u] - 0.3 + 1e-9; cu += SAMPLE)
730
+ for (let cw = from(w); cw <= body.max[w] - 0.3 + 1e-9; cw += SAMPLE) {
731
+ let run2 = 0;
732
+ let best = { start: 0, length: 0, cross: 0, open: false };
733
+ let start = 0;
734
+ let cross2 = 0;
735
+ for (let t = body.min[axis] + SAMPLE; t < body.max[axis]; t += 0.1) {
736
+ const hollow = [0, 0.12].every(
737
+ (r) => [
738
+ [1, 0],
739
+ [-1, 0],
740
+ [0, 1],
741
+ [0, -1]
742
+ ].every(([a2, b]) => !at(cu, cw, t, a2 * r, b * r))
743
+ );
744
+ const walled = ring8.filter(([a2, b]) => {
745
+ for (let r = 0.15; r <= REACH + 1e-9; r += 0.05)
746
+ if (at(cu, cw, t, a2 * r, b * r)) return true;
747
+ return false;
748
+ }).length;
749
+ if (hollow && walled === 8) {
750
+ if (!run2) {
751
+ start = t;
752
+ cross2 = 0;
753
+ }
754
+ run2 += 0.1;
755
+ cross2 += ring8.filter((_, k) => k % 2).filter(([a2, b]) => at(cu, cw, t, a2 * DIAGONAL, b * DIAGONAL)).length;
756
+ if (run2 > best.length) best = { start, length: run2, cross: cross2, open: false };
757
+ } else run2 = 0;
758
+ }
759
+ if (best.length + 1e-9 < MIN_RUN) continue;
760
+ const clear = (t0, dt) => {
761
+ for (let t = t0; t > body.min[axis] - 0.1 && t < body.max[axis] + 0.1; t += dt)
762
+ if (at(cu, cw, t)) return false;
763
+ return true;
764
+ };
765
+ if (!clear(best.start - 0.1, -0.1) || !clear(best.start + best.length, 0.1)) continue;
766
+ const p = [0, 0, 0];
767
+ p[axis] = best.start - 0.05 + best.length / 2;
768
+ p[u] = cu;
769
+ p[w] = cw;
770
+ const a = [0, 0, 0];
771
+ a[axis] = 1;
772
+ const steps = best.length / 0.1;
773
+ found.push({
774
+ kind: best.cross / steps >= 3 ? "axlehole" : "pinhole",
775
+ p,
776
+ a,
777
+ len: best.length,
778
+ score: best.length
779
+ });
780
+ }
781
+ }
782
+ const holes = [];
783
+ const left = new Set(found);
784
+ 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);
785
+ for (const seed of found) {
786
+ if (!left.has(seed)) continue;
787
+ const group = [seed];
788
+ left.delete(seed);
789
+ for (let i = 0; i < group.length; i++)
790
+ for (const h of left)
791
+ if (near(group[i], h)) {
792
+ group.push(h);
793
+ left.delete(h);
794
+ }
795
+ const mean = [0, 1, 2].map((k) => group.reduce((s, h) => s + h.p[k], 0) / group.length);
796
+ const longest = group.reduce((x, y) => y.score > x.score ? y : x);
797
+ const crosses = group.filter((h) => h.kind === "axlehole").length;
798
+ holes.push({
799
+ kind: crosses * 2 > group.length ? "axlehole" : "pinhole",
800
+ p: mean.map((v) => Math.round(v / SAMPLE) * SAMPLE || 0),
801
+ a: seed.a,
802
+ len: longest.len
803
+ });
804
+ }
805
+ return holes;
806
+ }
807
+ var SHAFT = /^Technic (Axle\s+\d+(\.\d+)?$|Axle\s+\d+ with Stop|Pin\b|Friction Pin|Axle Pin)/i;
808
+ function technicShafts(name, triangles) {
809
+ if (!SHAFT.test(name) || /connector|joiner|hole|beam/i.test(name)) return [];
810
+ const { min, max } = meshBounds(triangles);
811
+ const size = [0, 1, 2].map((k) => max[k] - min[k]);
812
+ const axis = size.indexOf(Math.max(...size));
813
+ const a = [0, 0, 0];
814
+ a[axis] = 1;
815
+ const p = [0, 1, 2].map((k) => (min[k] + max[k]) / 2);
816
+ return [
817
+ { kind: /pin/i.test(name) && !/axle pin/i.test(name) ? "pin" : "axle", p, a, len: size[axis] }
818
+ ];
819
+ }
820
+ var STUD = /^(stud|stud2|stud2a|studa|stud26|stud6a?|stud9|stud10|stud13|stud15|stud17a?|studel|studh[lr]?|studp01|studxa?|stud2?-logo\d?)\.dat$/;
821
+ var BALL = /^joint8ball\.dat$/;
822
+ var BALL_SOCKET = /^joint8socket\d\.dat$/;
823
+ var isJoint = (p) => STUD.test(p) || BALL.test(p) || BALL_SOCKET.test(p);
824
+ var FAMILY = {
825
+ "3001": "brick",
826
+ "3003": "brick",
827
+ "3004": "brick",
828
+ "3005": "brick",
829
+ "3010": "brick",
830
+ "3020": "plate",
831
+ "3021": "plate",
832
+ "3022": "plate",
833
+ "3023": "plate",
834
+ "3024": "plate",
835
+ "3710": "plate",
836
+ "3666": "plate",
837
+ "3460": "plate",
838
+ "3068b": "tile",
839
+ "3069b": "tile",
840
+ "3070b": "tile"
841
+ };
842
+ var lego = {
843
+ name: "lego",
844
+ fits: {
845
+ stud: ["socket"],
846
+ socket: ["stud"],
847
+ axle: ["axlehole", "pinhole"],
848
+ axlehole: ["axle"],
849
+ pin: ["pinhole"],
850
+ pinhole: ["pin", "axle"],
851
+ wheelpin: ["wheelhole"],
852
+ wheelhole: ["wheelpin"],
853
+ rim: ["tyre"],
854
+ tyre: ["rim"],
855
+ hinge: ["hinge"],
856
+ bar: ["clip", "barhole"],
857
+ clip: ["bar"],
858
+ barhole: ["bar"],
859
+ neck: ["neckhole"],
860
+ neckhole: ["neck"],
861
+ hatstud: ["hat"],
862
+ hat: ["hatstud"],
863
+ waist: ["waisthole"],
864
+ waisthole: ["waist"],
865
+ hip: ["leg"],
866
+ leg: ["hip"],
867
+ shoulder: ["shoulderhole"],
868
+ shoulderhole: ["shoulder"],
869
+ wrist: ["hand"],
870
+ hand: ["wrist"],
871
+ ball: ["ballsocket"],
872
+ ballsocket: ["ball"]
873
+ },
874
+ spherical: ["ball", "ballsocket"],
875
+ tolerance: 0.06,
876
+ minEngagement: 0.24,
877
+ family: (part2) => FAMILY[part2] ?? part2
878
+ };
879
+ var WRIST_RIGHT = [-0.25, -0.875, -0.5];
880
+ var WRIST_LEFT = [0.25, -0.875, -0.5];
881
+ var LEGO_CONNECTORS = {
882
+ // Plate 2 x 2 with 2 Wheel Pins; the pins hang below the plate, so its sockets are listed too.
883
+ "4600": [
884
+ ...[-0.5, 0.5].flatMap(
885
+ (x) => [-0.5, 0.5].map((z5) => ({ kind: "socket", p: [x, -0.4, z5], a: [0, -1, 0] }))
886
+ ),
887
+ { kind: "wheelpin", p: [1.4, -0.25, 0], a: [1, 0, 0], len: 0.6 },
888
+ { kind: "wheelpin", p: [-1.4, -0.25, 0], a: [1, 0, 0], len: 0.6 }
889
+ ],
890
+ // Wheel Rim 6.4 x 8
891
+ "4624": [
892
+ { kind: "wheelhole", p: [0, 0, 0], a: [0, 0, 1], len: 0.8 },
893
+ { kind: "rim", p: [0, 0, 0], a: [0, 0, 1], len: 0.8 }
894
+ ],
895
+ // Tyre 6/ 50 x 8 Offset Tread
896
+ "3641": [{ kind: "tyre", p: [0, 0, 0], a: [0, 0, 1], len: 0.8 }],
897
+ // Plate 1 x 2 with Socket Joint-8 and Ball Joint-8: its ball is drawn as a plain sphere,
898
+ // which also hangs below the plate and pulls the inferred sockets down with it.
899
+ "14419": [
900
+ { kind: "ball", p: [-1.5, -0.2, 0], a: [-1, 0, 0] },
901
+ ...[-0.5, 0.5].map((x) => ({ kind: "socket", p: [x, -0.4, 0], a: [0, -1, 0] }))
902
+ ],
903
+ // Hinge 1 x 2 Base and Top: both pivot on the cylinder along x at 10 LDU below the origin.
904
+ "3937": [{ kind: "hinge", p: [0, -0.5, 0], a: [1, 0, 0], len: 2 }],
905
+ "3938": [{ kind: "hinge", p: [0, -0.5, 0], a: [1, 0, 0], len: 2 }],
906
+ // Plate 1 x 3 with 2 Studs Offset: the teeth underneath hold a stud at every half step,
907
+ // so a jumper also sits straight on another jumper's offset studs.
908
+ "34103": [-1, -0.5, 0, 0.5, 1].map(
909
+ (x) => ({ kind: "socket", p: [x, -0.4, 0], a: [0, -1, 0] })
910
+ ),
911
+ // Bar 6L with Thick Stop: the bar runs the full height of the part.
912
+ "63965a": [{ kind: "bar", p: [0, 2.2, 0], a: [0, 1, 0], len: 6 }],
913
+ // Cone 1 x 1: hollow along its axis, so a bar goes in from the top.
914
+ "4589": [
915
+ { kind: "barhole", p: [0, -0.5, 0], a: [0, 1, 0], len: 1.4 },
916
+ { kind: "socket", p: [0, -1.2, 0], a: [0, -1, 0] }
917
+ ],
918
+ // Tile 2 x 3 with Clips Horizontal: both C-clips share an axis along x.
919
+ "30350b": [{ kind: "clip", p: [0, -0.05, 2], a: [1, 0, 0], len: 2 }],
920
+ // Minifig, in the frame of each part as LDraw places a standard figure.
921
+ "973": [
922
+ { kind: "neck", p: [0, 0, 0], a: [0, 1, 0] },
923
+ { kind: "waist", p: [0, -1.6, 0], a: [0, -1, 0] },
924
+ { kind: "shoulder", p: [-0.7776, -0.45, 0], a: [-1, 0, 0] },
925
+ { kind: "shoulder", p: [0.7776, -0.45, 0], a: [1, 0, 0] }
926
+ ],
927
+ "3626c": [
928
+ { kind: "neckhole", p: [0, -1.2, 0], a: [0, -1, 0] },
929
+ { kind: "hatstud", p: [0, 0, 0], a: [0, 1, 0] }
930
+ ],
931
+ "3626cp7g": [
932
+ { kind: "neckhole", p: [0, -1.2, 0], a: [0, -1, 0] },
933
+ { kind: "hatstud", p: [0, 0, 0], a: [0, 1, 0] }
934
+ ],
935
+ "86035": [{ kind: "hat", p: [0, 0, 0], a: [0, -1, 0] }],
936
+ "3815": [
937
+ { kind: "waisthole", p: [0, 0, 0], a: [0, 1, 0] },
938
+ { kind: "hip", p: [0, -0.6, 0], a: [0, -1, 0] },
939
+ { kind: "hip", p: [0, -0.6, 0], a: [0, -1, 0] }
940
+ ],
941
+ "3816": [
942
+ { kind: "leg", p: [0, 0, 0], a: [0, 1, 0] },
943
+ { kind: "socket", p: [-0.5, -1.4, 0], a: [0, -1, 0] }
944
+ ],
945
+ "3817": [
946
+ { kind: "leg", p: [0, 0, 0], a: [0, 1, 0] },
947
+ { kind: "socket", p: [0.5, -1.4, 0], a: [0, -1, 0] }
948
+ ],
949
+ "3818": [
950
+ { kind: "shoulderhole", p: [0, 0, 0], a: [1, 0, 0] },
951
+ { kind: "wrist", p: WRIST_RIGHT, a: [0, 0, -1] }
952
+ ],
953
+ "3819": [
954
+ { kind: "shoulderhole", p: [0, 0, 0], a: [-1, 0, 0] },
955
+ { kind: "wrist", p: WRIST_LEFT, a: [0, 0, -1] }
956
+ ],
957
+ // The hand's peg goes into the sleeve from the front.
958
+ "3820": [{ kind: "hand", p: [0, 0, 0], a: [0, 0, 1] }]
959
+ };
960
+ function technicConnectors(name, parsed, voxels) {
961
+ const shafts = technicShafts(name, parsed.triangles);
962
+ return shafts.length ? shafts : technicHoles(voxels, parsed.body);
963
+ }
964
+ var FLAT_TOP = 0.5;
965
+ function flatTop({ triangles, body }) {
966
+ let area = 0;
967
+ for (let t = 0; t < triangles.length; t += 9) {
968
+ const ys = [triangles[t + 1], triangles[t + 4], triangles[t + 7]];
969
+ if (ys.some((y) => Math.abs(y - body.max[1]) > 1e-3)) continue;
970
+ const ux = triangles[t + 3] - triangles[t];
971
+ const uz = triangles[t + 5] - triangles[t + 2];
972
+ const vx = triangles[t + 6] - triangles[t];
973
+ const vz = triangles[t + 8] - triangles[t + 2];
974
+ area += Math.abs(ux * vz - uz * vx) / 2;
975
+ }
976
+ const footprint = (body.max[0] - body.min[0]) * (body.max[2] - body.min[2]);
977
+ return footprint > 0 ? area / footprint : 0;
978
+ }
979
+ var WALLED_BOTTOM = 0.35;
980
+ var STUD_DEPTH = 0.3;
981
+ var lattice = (lo, hi, phase) => {
982
+ const out = [];
983
+ for (let x = Math.ceil(lo - phase) + phase; x < hi; x++)
984
+ if (x - lo > 0.25 && hi - x > 0.25) out.push(x);
985
+ return out;
986
+ };
987
+ var BORE = 0.25;
988
+ function takesStud(voxels, x, y, z5, top) {
989
+ let hollow;
990
+ for (let h = y + CELL / 2; h < y + STUD_DEPTH && hollow === void 0; h += CELL)
991
+ if (!cellAt(voxels, [x, h, z5])) hollow = h;
992
+ if (hollow === void 0) return false;
993
+ const capped = (dx, dz) => {
994
+ for (let h = hollow + CELL; h < top; h += CELL)
995
+ if (cellAt(voxels, [x + dx, h, z5 + dz])) return true;
996
+ return false;
997
+ };
998
+ return capped(0, 0) || [
999
+ [BORE, 0],
1000
+ [-BORE, 0],
1001
+ [0, BORE],
1002
+ [0, -BORE]
1003
+ ].every(([dx, dz]) => capped(dx, dz));
1004
+ }
1005
+ function legoConnectors(parsed, voxels) {
1006
+ const out = [];
1007
+ for (const j of parsed.joints) {
1008
+ const a = apply(j.rotation, [0, 1, 0]);
1009
+ if (STUD.test(j.name)) out.push({ kind: "stud", p: j.position, a });
1010
+ else if (BALL.test(j.name)) out.push({ kind: "ball", p: j.position, a });
1011
+ else if (BALL_SOCKET.test(j.name)) out.push({ kind: "ballsocket", p: j.position, a });
1012
+ }
1013
+ const { min, max } = parsed.body;
1014
+ const up = out.find((c) => c.kind === "stud" && dot(c.a, [0, 1, 0]) > 0.99);
1015
+ const wrap = (v) => (v % 1 + 1) % 1;
1016
+ const edgePhase = (k) => wrap(min[k] + 0.5);
1017
+ const studPhase = (k) => up ? wrap(up.p[k]) : edgePhase(k);
1018
+ const y = min[1];
1019
+ const rim = [];
1020
+ for (let x = min[0] + CELL / 2; x < max[0]; x += CELL)
1021
+ rim.push([x, y + CELL / 2, min[2] + CELL / 2], [x, y + CELL / 2, max[2] - CELL / 2]);
1022
+ for (let z5 = min[2] + CELL / 2; z5 < max[2]; z5 += CELL)
1023
+ rim.push([min[0] + CELL / 2, y + CELL / 2, z5], [max[0] - CELL / 2, y + CELL / 2, z5]);
1024
+ const walled = rim.filter((p) => cellAt(voxels, p)).length >= WALLED_BOTTOM * rim.length;
1025
+ if (!up && !walled && flatTop(parsed) < FLAT_TOP) return out;
1026
+ const sockets = (px, pz) => {
1027
+ const found = [];
1028
+ for (const x of lattice(min[0], max[0], px))
1029
+ for (const z5 of lattice(min[2], max[2], pz))
1030
+ if (takesStud(voxels, x, y, z5, max[1]))
1031
+ found.push({ kind: "socket", p: [x, y, z5], a: [0, -1, 0] });
1032
+ return found;
1033
+ };
1034
+ const byStuds = sockets(studPhase(0), studPhase(2));
1035
+ const byEdges = sockets(edgePhase(0), edgePhase(2));
1036
+ out.push(...byEdges.length > byStuds.length ? byEdges : byStuds);
1037
+ return out;
1038
+ }
1039
+ function loadLegoPart(id, resolve9, opts = {}) {
1040
+ const parsed = parsePart(`${id}.dat`, resolve9, isJoint);
1041
+ const bounds = meshBounds(parsed.triangles);
1042
+ const voxels = voxelize(parsed.triangles, bounds.min, bounds.max);
1043
+ return {
1044
+ id,
1045
+ name: opts.name ?? id,
1046
+ voxels,
1047
+ connectors: [
1048
+ // Listed sockets replace inferred ones, for parts whose underside the solid misreads.
1049
+ ...legoConnectors(parsed, voxels).filter(
1050
+ (c) => c.kind !== "socket" || !LEGO_CONNECTORS[id]?.some((l) => l.kind === "socket")
1051
+ ),
1052
+ ...opts.connectors ?? LEGO_CONNECTORS[id] ?? technicConnectors(opts.name ?? "", parsed, voxels)
1053
+ ],
1054
+ body: parsed.body,
1055
+ triangles: parsed.triangles,
1056
+ colors: parsed.colors,
1057
+ edges: parsed.edges
1058
+ };
1059
+ }
1060
+ function libraryResolver(read, root) {
1061
+ return (name) => {
1062
+ for (const dir of ["parts", "p", "parts/s"]) {
1063
+ const text = read(`${root}/${dir}/${name}`);
1064
+ if (text !== void 0) return text;
1065
+ }
1066
+ return void 0;
1067
+ };
1068
+ }
1069
+ function turn(x, y) {
1070
+ const r = AXIS_ROTATIONS.find(
1071
+ (m) => [0, 1, 2].every((k) => m[k * 3] === x[k] && m[k * 3 + 1] === y[k])
1072
+ );
1073
+ if (!r) throw new Error(`No axis rotation sends x to ${x} and y to ${y}.`);
1074
+ return r;
1075
+ }
1076
+ var round = (v) => v.map((x) => Math.round(x * 1e3) / 1e3 + 0);
1077
+ function describePart(part2) {
1078
+ const { min, max } = part2.body;
1079
+ const near = (a, b) => Math.abs(a - b) < 0.05;
1080
+ const connectors = {};
1081
+ for (const c of part2.connectors) {
1082
+ connectors[c.kind] ??= [];
1083
+ const list = connectors[c.kind];
1084
+ list.push({
1085
+ index: list.length,
1086
+ p: round(c.p),
1087
+ a: round(c.a),
1088
+ ...c.len === void 0 ? {} : { len: c.len }
1089
+ });
1090
+ }
1091
+ return {
1092
+ id: part2.id,
1093
+ name: part2.name,
1094
+ size: round([max[0] - min[0], max[1] - min[1], max[2] - min[2]]),
1095
+ body: { min: round(min), max: round(max) },
1096
+ origin: near(max[1], 0) ? "top" : near(min[1], 0) ? "bottom" : "inside",
1097
+ connectors
1098
+ };
1099
+ }
1100
+ var DEFAULT_PALETTE = {
1101
+ 0: "#1B2A34",
1102
+ 1: "#1E5AA8",
1103
+ 4: "#B40000",
1104
+ 14: "#FAC80A",
1105
+ 15: "#F4F4F4",
1106
+ 71: "#969696",
1107
+ 72: "#646464"
1108
+ };
1109
+ function parsePalette(ldconfig) {
1110
+ const out = {};
1111
+ for (const m of ldconfig.matchAll(/^0 !COLOUR \S+\s+CODE\s+(\d+)\s+VALUE\s+(#[0-9A-Fa-f]{6})/gm))
1112
+ out[Number(m[1])] = m[2];
1113
+ return out;
1114
+ }
1115
+ var LIGHT = (() => {
1116
+ const v = [0.4, 0.85, 0.35];
1117
+ return scale(v, 1 / length(v));
1118
+ })();
1119
+ var rgb = (hex) => {
1120
+ const n = Number.parseInt(hex.slice(1), 16);
1121
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1122
+ };
1123
+ function rasterize(model, lib, opts = {}) {
1124
+ const ss = opts.supersample ?? 2;
1125
+ const W2 = (opts.width ?? 800) * ss;
1126
+ const H2 = (opts.height ?? 600) * ss;
1127
+ const yaw = opts.yaw ?? Math.PI / 4;
1128
+ const pitch = opts.pitch ?? Math.PI / 6;
1129
+ const palette = { ...DEFAULT_PALETTE, ...opts.palette };
1130
+ const highlight = new Set(opts.highlight ?? []);
1131
+ const background = rgb(opts.background ?? "#ffffff");
1132
+ const view = [
1133
+ Math.sin(yaw) * Math.cos(pitch),
1134
+ Math.sin(pitch),
1135
+ Math.cos(yaw) * Math.cos(pitch)
1136
+ ];
1137
+ const right = [Math.cos(yaw), 0, -Math.sin(yaw)];
1138
+ const up = cross(view, right);
1139
+ const shown = model.items.map((item, i) => ({ item, i })).filter(({ item }) => opts.maxStep === void 0 || (item.step ?? 0) <= opts.maxStep);
1140
+ const bodies = shown.map(({ item }) => worldBody(lib, item));
1141
+ const center = [0, 0, 0];
1142
+ for (const b of bodies) for (let k = 0; k < 3; k++) center[k] += (b.min[k] + b.max[k]) / 2;
1143
+ if (bodies.length) for (let k = 0; k < 3; k++) center[k] /= bodies.length;
1144
+ let tri = new Float64Array(9 * 4096);
1145
+ let triColor = new Float64Array(3 * 4096);
1146
+ let tris = 0;
1147
+ let seg = new Float64Array(6 * 4096);
1148
+ let segColor = new Float64Array(3 * 4096);
1149
+ let segs = 0;
1150
+ const grow = (buf, need) => {
1151
+ if (need <= buf.length) return buf;
1152
+ const next = new Float64Array(Math.max(need, buf.length * 2));
1153
+ next.set(buf);
1154
+ return next;
1155
+ };
1156
+ let minX = Infinity;
1157
+ let minY = Infinity;
1158
+ let maxX = -Infinity;
1159
+ let maxY = -Infinity;
1160
+ const [r0, r1, r2] = right;
1161
+ const [u0, u1, u2] = up;
1162
+ const [v0, v1, v2] = view;
1163
+ const project = (out, o, x, y, z5) => {
1164
+ const vx = x * r0 + y * r1 + z5 * r2;
1165
+ const vy = -(x * u0 + y * u1 + z5 * u2);
1166
+ out[o] = vx;
1167
+ out[o + 1] = vy;
1168
+ out[o + 2] = x * v0 + y * v1 + z5 * v2;
1169
+ if (vx < minX) minX = vx;
1170
+ if (vx > maxX) maxX = vx;
1171
+ if (vy < minY) minY = vy;
1172
+ if (vy > maxY) maxY = vy;
1173
+ };
1174
+ const w = new Float64Array(9);
1175
+ shown.forEach(({ item, i }, n) => {
1176
+ const part2 = lib[item.part];
1177
+ if (!part2) throw new Error(`Unknown part: ${item.part}`);
1178
+ const b = bodies[n];
1179
+ const mid = [
1180
+ (b.min[0] + b.max[0]) / 2,
1181
+ (b.min[1] + b.max[1]) / 2,
1182
+ (b.min[2] + b.max[2]) / 2
1183
+ ];
1184
+ const offset = opts.explode ? scale(sub(mid, center), opts.explode) : [0, 0, 0];
1185
+ const fade = highlight.size && !highlight.has(i) ? 0.72 : 0;
1186
+ const m = item.rotation;
1187
+ const [p0, p1, p2] = item.position;
1188
+ const [o0, o1, o2] = offset;
1189
+ const world = (k, x, y, z5) => {
1190
+ w[k] = m[0] * x + m[1] * y + m[2] * z5 + p0 + o0;
1191
+ w[k + 1] = m[3] * x + m[4] * y + m[5] * z5 + p1 + o1;
1192
+ w[k + 2] = m[6] * x + m[7] * y + m[8] * z5 + p2 + o2;
1193
+ };
1194
+ const shade = /* @__PURE__ */ new Map();
1195
+ const t = part2.triangles;
1196
+ const faces = t.length / 9;
1197
+ tri = grow(tri, (tris + faces) * 9);
1198
+ triColor = grow(triColor, (tris + faces) * 3);
1199
+ for (let f = 0; f < faces; f++) {
1200
+ for (let v = 0; v < 3; v++)
1201
+ world(v * 3, t[f * 9 + v * 3], t[f * 9 + v * 3 + 1], t[f * 9 + v * 3 + 2]);
1202
+ const e1 = [w[3] - w[0], w[4] - w[1], w[5] - w[2]];
1203
+ const e2 = [w[6] - w[0], w[7] - w[1], w[8] - w[2]];
1204
+ const normal = cross(e1, e2);
1205
+ const len = length(normal);
1206
+ if (len < 1e-9) continue;
1207
+ const nn = scale(normal, 1 / len);
1208
+ const facing = dot(nn, view) < 0 ? scale(nn, -1) : nn;
1209
+ const code = part2.colors[f] === 16 ? item.color : part2.colors[f];
1210
+ const k = 0.55 + 0.45 * Math.max(0, dot(facing, LIGHT));
1211
+ let base2 = shade.get(code);
1212
+ if (!base2) {
1213
+ base2 = rgb(palette[code] ?? "#888888");
1214
+ shade.set(code, base2);
1215
+ }
1216
+ for (let v = 0; v < 3; v++)
1217
+ project(tri, tris * 9 + v * 3, w[v * 3], w[v * 3 + 1], w[v * 3 + 2]);
1218
+ for (let c = 0; c < 3; c++) {
1219
+ const value = Math.min(255, base2[c] * k);
1220
+ triColor[tris * 3 + c] = value + (255 - value) * fade;
1221
+ }
1222
+ tris++;
1223
+ }
1224
+ const e = part2.edges ?? [];
1225
+ const base = rgb(palette[item.color] ?? "#888888");
1226
+ const dark = base[0] + base[1] + base[2] < 115;
1227
+ const edge = (dark ? [110, 110, 110] : base.map((c) => c * 0.45)).map(
1228
+ (c) => c + (255 - c) * fade
1229
+ );
1230
+ seg = grow(seg, (segs + e.length / 6) * 6);
1231
+ segColor = grow(segColor, (segs + e.length / 6) * 3);
1232
+ for (let q = 0; q < e.length; q += 6) {
1233
+ world(0, e[q], e[q + 1], e[q + 2]);
1234
+ world(3, e[q + 3], e[q + 4], e[q + 5]);
1235
+ project(seg, segs * 6, w[0], w[1], w[2]);
1236
+ project(seg, segs * 6 + 3, w[3], w[4], w[5]);
1237
+ segColor.set(edge, segs * 3);
1238
+ segs++;
1239
+ }
1240
+ });
1241
+ const s = tris ? 0.9 * Math.min(W2 / (maxX - minX || 1), H2 / (maxY - minY || 1)) : 1;
1242
+ const ox = W2 / 2 - s * (minX + maxX) / 2;
1243
+ const oy = H2 / 2 - s * (minY + maxY) / 2;
1244
+ const data = new Uint8ClampedArray(W2 * H2 * 4);
1245
+ const depth = new Float32Array(W2 * H2).fill(-Infinity);
1246
+ const pixels = new Uint32Array(data.buffer);
1247
+ pixels.fill(255 << 24 | background[2] << 16 | background[1] << 8 | background[0]);
1248
+ for (let f = 0; f < tris; f++) {
1249
+ const q = f * 9;
1250
+ const ax = ox + s * tri[q];
1251
+ const ay = oy + s * tri[q + 1];
1252
+ const az = tri[q + 2];
1253
+ const bx = ox + s * tri[q + 3];
1254
+ const by = oy + s * tri[q + 4];
1255
+ const bz = tri[q + 5];
1256
+ const cx = ox + s * tri[q + 6];
1257
+ const cy = oy + s * tri[q + 7];
1258
+ const cz = tri[q + 8];
1259
+ const area = (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);
1260
+ if (Math.abs(area) < 1e-9) continue;
1261
+ const red = triColor[f * 3];
1262
+ const green = triColor[f * 3 + 1];
1263
+ const blue = triColor[f * 3 + 2];
1264
+ const x0 = Math.max(0, Math.floor(Math.min(ax, bx, cx)));
1265
+ const x1 = Math.min(W2 - 1, Math.ceil(Math.max(ax, bx, cx)));
1266
+ const y0 = Math.max(0, Math.floor(Math.min(ay, by, cy)));
1267
+ const y1 = Math.min(H2 - 1, Math.ceil(Math.max(ay, by, cy)));
1268
+ for (let y = y0; y <= y1; y++) {
1269
+ const py = y + 0.5;
1270
+ let lo = Infinity;
1271
+ let hi = -Infinity;
1272
+ for (const [sx, sy, ex, ey] of [
1273
+ [ax, ay, bx, by],
1274
+ [bx, by, cx, cy],
1275
+ [cx, cy, ax, ay]
1276
+ ]) {
1277
+ if ((sy - py) * (ey - py) > 0 || sy === ey) continue;
1278
+ const cross2 = sx + (py - sy) * (ex - sx) / (ey - sy);
1279
+ if (cross2 < lo) lo = cross2;
1280
+ if (cross2 > hi) hi = cross2;
1281
+ }
1282
+ if (lo > hi) continue;
1283
+ const from = Math.max(x0, Math.floor(lo) - 1);
1284
+ const to = Math.min(x1, Math.ceil(hi) + 1);
1285
+ for (let x = from; x <= to; x++) {
1286
+ const px = x + 0.5;
1287
+ const l0 = ((bx - px) * (cy - py) - (cx - px) * (by - py)) / area;
1288
+ if (l0 < 0) continue;
1289
+ const l1 = ((cx - px) * (ay - py) - (ax - px) * (cy - py)) / area;
1290
+ const l2 = 1 - l0 - l1;
1291
+ if (l1 < 0 || l2 < 0) continue;
1292
+ const z5 = l0 * az + l1 * bz + l2 * cz;
1293
+ const p = y * W2 + x;
1294
+ if (z5 <= depth[p]) continue;
1295
+ depth[p] = z5;
1296
+ data[p * 4] = red;
1297
+ data[p * 4 + 1] = green;
1298
+ data[p * 4 + 2] = blue;
1299
+ }
1300
+ }
1301
+ }
1302
+ const bias = 0.02;
1303
+ const thick = ss > 1 ? 2 : 1;
1304
+ for (let l = 0; l < segs; l++) {
1305
+ const q = l * 6;
1306
+ const ax = ox + s * seg[q];
1307
+ const ay = oy + s * seg[q + 1];
1308
+ const az = seg[q + 2];
1309
+ const bx = ox + s * seg[q + 3];
1310
+ const by = oy + s * seg[q + 4];
1311
+ const bz = seg[q + 5];
1312
+ const steps = Math.max(1, Math.ceil(Math.max(Math.abs(bx - ax), Math.abs(by - ay))));
1313
+ for (let k = 0; k <= steps; k++) {
1314
+ const t = k / steps;
1315
+ const x = Math.round(ax + (bx - ax) * t);
1316
+ const y = Math.round(ay + (by - ay) * t);
1317
+ if (x < 0 || y < 0 || x >= W2 || y >= H2) continue;
1318
+ const z5 = az + (bz - az) * t;
1319
+ for (let dy = 0; dy < thick; dy++)
1320
+ for (let dx = 0; dx < thick; dx++) {
1321
+ const p = Math.min(H2 - 1, y + dy) * W2 + Math.min(W2 - 1, x + dx);
1322
+ if (z5 + bias < depth[p]) continue;
1323
+ data[p * 4] = segColor[l * 3];
1324
+ data[p * 4 + 1] = segColor[l * 3 + 1];
1325
+ data[p * 4 + 2] = segColor[l * 3 + 2];
1326
+ }
1327
+ }
1328
+ }
1329
+ return { width: W2, height: H2, data };
1330
+ }
1331
+ var key = (a, b, kind) => `${a < b ? `${a}~${b}` : `${b}~${a}`}:${kind}`;
1332
+ function learnGrammar(model, joints, adapter) {
1333
+ const exact = /* @__PURE__ */ new Set();
1334
+ const family = /* @__PURE__ */ new Set();
1335
+ for (const c of joints) {
1336
+ const a = model.items[c.a].part;
1337
+ const b = model.items[c.b].part;
1338
+ exact.add(key(a, b, c.kind));
1339
+ family.add(key(adapter.family(a), adapter.family(b), c.kind));
1340
+ }
1341
+ return { exact, family };
1342
+ }
1343
+ function jointAllowed(grammar, adapter, a, b, kind, mode) {
1344
+ if (mode === "free" || grammar.exact.has(key(a, b, kind))) return true;
1345
+ return mode === "family" && grammar.family.has(key(adapter.family(a), adapter.family(b), kind));
1346
+ }
1347
+ var DIRECTIONS = [
1348
+ [0, 1, 0],
1349
+ [1, 0, 0],
1350
+ [-1, 0, 0],
1351
+ [0, 0, 1],
1352
+ [0, 0, -1],
1353
+ [0, -1, 0]
1354
+ ];
1355
+ var STEP = 0.15;
1356
+ var BLOCKING_CELLS = 4;
1357
+ var SWEEP = 4;
1358
+ var union = (x, y) => ({
1359
+ min: [0, 1, 2].map((k) => Math.min(x.min[k], y.min[k])),
1360
+ max: [0, 1, 2].map((k) => Math.max(x.max[k], y.max[k]))
1361
+ });
1362
+ var moved = (it, d, s) => ({
1363
+ ...it,
1364
+ position: add(it.position, scale(d, s))
1365
+ });
1366
+ var shifted = (b, d, s) => ({
1367
+ min: add(b.min, scale(d, s)),
1368
+ max: add(b.max, scale(d, s))
1369
+ });
1370
+ function planAssembly(model, lib, adapter) {
1371
+ const items = model.items;
1372
+ const bodies = items.map((it) => worldBody(lib, it));
1373
+ const axes = /* @__PURE__ */ new Map();
1374
+ const releases = /* @__PURE__ */ new Map();
1375
+ const snaps = /* @__PURE__ */ new Set();
1376
+ const ways = items.map(() => [...DIRECTIONS]);
1377
+ const tryWay = (i, d) => {
1378
+ if (!ways[i].some((w) => dot(w, d) > 0.999)) ways[i].push(d);
1379
+ };
1380
+ for (const c of contacts(model, lib, adapter)) {
1381
+ if (c.kind.split("-").every((k) => isSpherical(adapter, k))) {
1382
+ snaps.add(`${c.a},${c.b}`);
1383
+ snaps.add(`${c.b},${c.a}`);
1384
+ }
1385
+ if (c.axis) {
1386
+ axes.set(`${c.a},${c.b}`, c.axis);
1387
+ axes.set(`${c.b},${c.a}`, c.axis);
1388
+ }
1389
+ if (c.release) {
1390
+ releases.set(`${c.b},${c.a}`, c.release);
1391
+ releases.set(`${c.a},${c.b}`, scale(c.release, -1));
1392
+ tryWay(c.b, c.release);
1393
+ tryWay(c.a, scale(c.release, -1));
1394
+ }
1395
+ }
1396
+ const allowed = (i, d, around) => {
1397
+ for (const j of around) {
1398
+ if (j === i) continue;
1399
+ const axis = axes.get(`${i},${j}`);
1400
+ if (axis && !slides(i, j, d)) return false;
1401
+ const release = releases.get(`${i},${j}`);
1402
+ if (release && dot(release, d) < 0.99) return false;
1403
+ }
1404
+ return true;
1405
+ };
1406
+ const centre = (b) => scale(add(b.min, b.max), 0.5);
1407
+ const slides = (i, j, d) => {
1408
+ const axis = axes.get(`${i},${j}`);
1409
+ if (!axis || Math.abs(dot(axis, d)) <= 0.99) return false;
1410
+ return dot(d, sub(centre(bodies[i]), centre(bodies[j]))) >= -1e-6;
1411
+ };
1412
+ const left = new Set(items.map((_, i) => i));
1413
+ const freeWay = (i, around) => ways[i].find((d) => {
1414
+ if (!allowed(i, d, around)) return false;
1415
+ const others = [...around].filter(
1416
+ (j) => j !== i && !slides(i, j, d) && !snaps.has(`${i},${j}`)
1417
+ );
1418
+ let nearby = [];
1419
+ for (let s = STEP, until = 0; ; s += STEP) {
1420
+ if (s > until) {
1421
+ const stretch = union(shifted(bodies[i], d, s), shifted(bodies[i], d, s + SWEEP));
1422
+ nearby = others.filter((j) => overlapVolume(stretch, bodies[j], -0.4) > 0);
1423
+ until = s + SWEEP;
1424
+ }
1425
+ const box2 = shifted(bodies[i], d, s);
1426
+ const near = nearby.filter((j) => overlapVolume(box2, bodies[j], -0.4) > 0);
1427
+ if (!near.length) return true;
1428
+ const it = moved(items[i], d, s);
1429
+ if (near.some((j) => collisionCells(lib, it, items[j]) > BLOCKING_CELLS)) return false;
1430
+ }
1431
+ });
1432
+ const removed = [];
1433
+ while (left.size) {
1434
+ const order2 = [...left].sort((a, b) => bodies[b].max[1] - bodies[a].max[1] || b - a);
1435
+ let next;
1436
+ for (const i of order2) {
1437
+ const d = freeWay(i, left);
1438
+ if (d) {
1439
+ next = { item: i, insert: [0 - d[0], 0 - d[1], 0 - d[2]] };
1440
+ break;
1441
+ }
1442
+ }
1443
+ if (!next) break;
1444
+ removed.push(next);
1445
+ left.delete(next.item);
1446
+ }
1447
+ if (!items.length || items.some((it) => it.step === void 0))
1448
+ return { steps: removed.reverse(), locked: [...left] };
1449
+ const step = (i) => items[i].step ?? 0;
1450
+ const order = items.map((_, i) => i).sort((a, b) => step(a) - step(b) || a - b);
1451
+ const built = /* @__PURE__ */ new Set();
1452
+ const steps = [];
1453
+ const locked = [];
1454
+ for (const i of order) {
1455
+ const d = freeWay(i, /* @__PURE__ */ new Set([...built, i]));
1456
+ if (d) steps.push({ item: i, insert: [0 - d[0], 0 - d[1], 0 - d[2]] });
1457
+ else locked.push(i);
1458
+ built.add(i);
1459
+ }
1460
+ return { steps, locked };
1461
+ }
1462
+ function seededRandom(seed) {
1463
+ let a = seed >>> 0;
1464
+ return () => {
1465
+ a = a + 1831565813 >>> 0;
1466
+ let t = a;
1467
+ t = Math.imul(t ^ t >>> 15, t | 1);
1468
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
1469
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
1470
+ };
1471
+ }
1472
+ function mates(adapter, free, placed) {
1473
+ if (!adapter.fits[free.kind]?.includes(placed.kind)) return false;
1474
+ const axial = free.len !== void 0 && placed.len !== void 0;
1475
+ return axial ? length(cross(free.a, placed.a)) < 0.01 : dot(free.a, placed.a) < -0.99;
1476
+ }
1477
+ function remix(exemplar, lib, adapter, opts) {
1478
+ const random = seededRandom(opts.seed);
1479
+ const mode = opts.mode ?? "family";
1480
+ const joints = contacts(exemplar, lib, adapter);
1481
+ const grammar = learnGrammar(exemplar, joints, adapter);
1482
+ const parts = [...new Set(exemplar.items.map((it) => it.part))];
1483
+ const owners = /* @__PURE__ */ new Map();
1484
+ for (const part2 of parts)
1485
+ for (const c of lib[part2].connectors)
1486
+ owners.set(c.kind, (owners.get(c.kind) ?? /* @__PURE__ */ new Set()).add(part2));
1487
+ const specialised = (kind) => kind.split("-").every((k) => owners.get(k)?.size === 1);
1488
+ const group = exemplar.items.map((_, i) => i);
1489
+ const find = (i) => {
1490
+ while (group[i] !== i) i = group[i];
1491
+ return i;
1492
+ };
1493
+ for (const c of joints) if (specialised(c.kind)) group[find(c.a)] = find(c.b);
1494
+ const members = /* @__PURE__ */ new Map();
1495
+ exemplar.items.forEach((_, i) => {
1496
+ members.set(find(i), [...members.get(find(i)) ?? [], i]);
1497
+ });
1498
+ const units = [...members.values()].map((ids) => {
1499
+ const r = exemplar.items[ids[0]];
1500
+ const inverse = transpose(r.rotation);
1501
+ return {
1502
+ first: Math.min(...ids),
1503
+ root: r,
1504
+ items: ids.map((i) => {
1505
+ const it = exemplar.items[i];
1506
+ return {
1507
+ ...it,
1508
+ rotation: mul(inverse, it.rotation),
1509
+ position: apply(inverse, sub(it.position, r.position))
1510
+ };
1511
+ })
1512
+ };
1513
+ });
1514
+ units.sort((x, y) => x.first - y.first);
1515
+ const place = (unit, { rotation, position }) => unit.items.map((it) => ({
1516
+ ...it,
1517
+ rotation: mul(rotation, it.rotation),
1518
+ position: add(apply(rotation, it.position), position)
1519
+ }));
1520
+ const freeConnectors = (items2) => {
1521
+ const world = items2.map((it) => worldConnectors(lib, it));
1522
+ const used = /* @__PURE__ */ new Set();
1523
+ for (const c of contacts({ items: items2 }, lib, adapter))
1524
+ for (const [x, y] of [
1525
+ [c.a, c.b],
1526
+ [c.b, c.a]
1527
+ ])
1528
+ world[x].forEach((cx, n) => {
1529
+ if (world[y].some(
1530
+ (cy) => mates(adapter, cx, cy) && length(sub(cx.p, cy.p)) <= adapter.tolerance
1531
+ ))
1532
+ used.add(`${x}:${n}`);
1533
+ });
1534
+ return world.flatMap((list, x) => list.filter((_, n) => !used.has(`${x}:${n}`)));
1535
+ };
1536
+ const poses = (unit, free) => {
1537
+ const own = freeConnectors(unit.items);
1538
+ const out = [];
1539
+ for (const rotation of AXIS_ROTATIONS)
1540
+ for (const c of own) {
1541
+ const turned = { ...c, p: apply(rotation, c.p), a: apply(rotation, c.a) };
1542
+ for (const f of free)
1543
+ if (mates(adapter, f, turned)) out.push({ rotation, position: sub(f.p, turned.p) });
1544
+ }
1545
+ for (let i = out.length - 1; i > 0; i--) {
1546
+ const j = Math.floor(random() * (i + 1));
1547
+ [out[i], out[j]] = [out[j], out[i]];
1548
+ }
1549
+ return out;
1550
+ };
1551
+ const legal = (build, added) => {
1552
+ const all = [...build, ...added];
1553
+ const fresh = contacts({ items: all }, lib, adapter).filter(
1554
+ (c) => c.a < build.length && c.b >= build.length
1555
+ );
1556
+ if (!fresh.length || !fresh.every(
1557
+ (c) => jointAllowed(grammar, adapter, all[c.a].part, all[c.b].part, c.kind, mode)
1558
+ ))
1559
+ return false;
1560
+ const through = new Set(fresh.filter((c) => c.axis).map((c) => `${c.a},${c.b}`));
1561
+ return added.every((item, k) => {
1562
+ const body = worldBody(lib, item);
1563
+ return build.every(
1564
+ (other, j) => through.has(`${j},${build.length + k}`) || overlapVolume(body, worldBody(lib, other), -0.4) <= 0 || collisionCells(lib, item, other) <= 4
1565
+ );
1566
+ });
1567
+ };
1568
+ const [seed, ...rest] = units;
1569
+ const items = place(seed, seed.root).map((it) => ({ ...it, step: 1 }));
1570
+ const pending = [...rest];
1571
+ for (let step = 2; pending.length; step++) {
1572
+ const free = freeConnectors(items);
1573
+ let chosen;
1574
+ for (const unit of pending) {
1575
+ for (const pose of poses(unit, free)) {
1576
+ const added = place(unit, pose).map((it) => ({ ...it, step }));
1577
+ if (legal(items, added) && !planAssembly({ items: [...items, ...added] }, lib, adapter).locked.length) {
1578
+ chosen = { unit, added };
1579
+ break;
1580
+ }
1581
+ }
1582
+ if (chosen) break;
1583
+ }
1584
+ if (!chosen) break;
1585
+ pending.splice(pending.indexOf(chosen.unit), 1);
1586
+ items.push(...chosen.added);
1587
+ }
1588
+ return { model: { items }, left: pending.flatMap((u) => u.items.map((it) => it.part)) };
1589
+ }
1590
+ var BRICKS = [
1591
+ ["3007", 8, 2],
1592
+ ["2456", 6, 2],
1593
+ ["3001", 4, 2],
1594
+ ["3002", 3, 2],
1595
+ ["3008", 8, 1],
1596
+ ["3009", 6, 1],
1597
+ ["3003", 2, 2],
1598
+ ["3010", 4, 1],
1599
+ ["3622", 3, 1],
1600
+ ["3004", 2, 1],
1601
+ ["3005", 1, 1]
1602
+ ].sort((a, b) => b[1] * b[2] - a[1] * a[2] || b[1] - a[1]);
1603
+ var PLATES = [
1604
+ ["3034", 8, 2],
1605
+ ["3795", 6, 2],
1606
+ ["3020", 4, 2],
1607
+ ["3021", 3, 2],
1608
+ ["3460", 8, 1],
1609
+ ["3666", 6, 1],
1610
+ ["3022", 2, 2],
1611
+ ["3710", 4, 1],
1612
+ ["3623", 3, 1],
1613
+ ["3023b", 2, 1],
1614
+ ["3024", 1, 1]
1615
+ ].sort((a, b) => b[1] * b[2] - a[1] * a[2] || b[1] - a[1]);
1616
+ var TILES = [
1617
+ ["87079", 4, 2],
1618
+ ["26603", 3, 2],
1619
+ ["4162", 8, 1],
1620
+ ["6636", 6, 1],
1621
+ ["3068b", 2, 2],
1622
+ ["2431", 4, 1],
1623
+ ["63864", 3, 1],
1624
+ ["3069b", 2, 1],
1625
+ ["3070b", 1, 1]
1626
+ ].sort((a, b) => b[1] * b[2] - a[1] * a[2] || b[1] - a[1]);
1627
+ var ALONG_Z = turn([0, 0, 1], [0, 1, 0]);
1628
+ function stress(model, lib, adapter) {
1629
+ const n = model.items.length;
1630
+ const joints = contacts(model, lib, adapter);
1631
+ const mass = model.items.map((it) => {
1632
+ const v = partVoxels(lib[it.part]);
1633
+ let cells = 0;
1634
+ for (const c of v.interior) if (c) cells++;
1635
+ return cells * CELL ** 3;
1636
+ });
1637
+ const centre = model.items.map((it) => {
1638
+ const { min, max } = worldBody(lib, it);
1639
+ return scale(add(min, max), 0.5);
1640
+ });
1641
+ const floor = Math.min(...model.items.map((it) => worldBody(lib, it).min[1]));
1642
+ const grounded = model.items.map((it, i) => worldBody(lib, it).min[1] - floor < 0.05 ? i : -1).filter((i) => i >= 0);
1643
+ const edges = model.items.map(() => []);
1644
+ joints.forEach((c, k) => {
1645
+ edges[c.a].push([c.b, k]);
1646
+ edges[c.b].push([c.a, k]);
1647
+ });
1648
+ const reach = (start, skip) => {
1649
+ const seen = /* @__PURE__ */ new Set([start]);
1650
+ const queue = [start];
1651
+ while (queue.length) {
1652
+ const x = queue.pop();
1653
+ for (const [y, k] of edges[x])
1654
+ if (k !== skip && !seen.has(y)) {
1655
+ seen.add(y);
1656
+ queue.push(y);
1657
+ }
1658
+ }
1659
+ return seen;
1660
+ };
1661
+ const cut = bridges(n, edges);
1662
+ const out = [];
1663
+ joints.forEach((c, k) => {
1664
+ if (!cut.has(k)) return;
1665
+ const side2 = reach(c.a, k);
1666
+ const held = grounded.some((g) => side2.has(g)) ? [...reach(c.b, k)] : [...side2];
1667
+ if (held.some((i) => grounded.includes(i))) return;
1668
+ const load = held.reduce((s, i) => s + mass[i], 0);
1669
+ const com = scale(
1670
+ held.reduce((s, i) => add(s, scale(centre[i], mass[i])), [0, 0, 0]),
1671
+ 1 / load
1672
+ );
1673
+ const lever = Math.hypot(com[0] - c.at[0], com[2] - c.at[2]);
1674
+ out.push({
1675
+ a: c.a,
1676
+ b: c.b,
1677
+ kind: c.kind,
1678
+ count: c.count,
1679
+ holds: held.sort((x, y) => x - y),
1680
+ load: round3(load),
1681
+ lever: round3(lever),
1682
+ moment: round3(load * lever / c.count)
1683
+ });
1684
+ });
1685
+ return out.sort((x, y) => y.moment - x.moment);
1686
+ }
1687
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
1688
+ function bridges(n, edges) {
1689
+ const order = new Array(n).fill(-1);
1690
+ const low = new Array(n).fill(0);
1691
+ const out = /* @__PURE__ */ new Set();
1692
+ let time = 0;
1693
+ for (let root = 0; root < n; root++) {
1694
+ if (order[root] >= 0) continue;
1695
+ const stack = [{ v: root, via: -1, next: 0 }];
1696
+ order[root] = low[root] = time++;
1697
+ while (stack.length) {
1698
+ const top = stack[stack.length - 1];
1699
+ if (top.next < edges[top.v].length) {
1700
+ const [w, k] = edges[top.v][top.next++];
1701
+ if (k === top.via) continue;
1702
+ if (order[w] < 0) {
1703
+ order[w] = low[w] = time++;
1704
+ stack.push({ v: w, via: k, next: 0 });
1705
+ } else low[top.v] = Math.min(low[top.v], order[w]);
1706
+ } else {
1707
+ stack.pop();
1708
+ const parent = stack[stack.length - 1];
1709
+ if (parent) {
1710
+ low[parent.v] = Math.min(low[parent.v], low[top.v]);
1711
+ if (low[top.v] > order[parent.v]) out.add(top.via);
1712
+ }
1713
+ }
1714
+ }
1715
+ }
1716
+ return out;
1717
+ }
1718
+ function toStl(model, lib, unit) {
1719
+ const tris = [];
1720
+ for (const item of model.items) {
1721
+ const part2 = lib[item.part];
1722
+ if (!part2) throw new Error(`Unknown part: ${item.part}`);
1723
+ const t = part2.triangles;
1724
+ for (let i = 0; i < t.length; i += 3) {
1725
+ const p = add(apply(item.rotation, [t[i], t[i + 1], t[i + 2]]), item.position);
1726
+ tris.push(p[0] * unit, -p[2] * unit, p[1] * unit);
1727
+ }
1728
+ }
1729
+ return encodeStl(tris);
1730
+ }
1731
+ function encodeStl(tris) {
1732
+ const count = tris.length / 9;
1733
+ const out = new DataView(new ArrayBuffer(84 + count * 50));
1734
+ out.setUint32(80, count, true);
1735
+ for (let f = 0; f < count; f++) {
1736
+ const o = 84 + f * 50;
1737
+ const v = tris.slice(f * 9, f * 9 + 9);
1738
+ const u = [v[3] - v[0], v[4] - v[1], v[5] - v[2]];
1739
+ const w = [v[6] - v[0], v[7] - v[1], v[8] - v[2]];
1740
+ 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]];
1741
+ const len = Math.hypot(n[0], n[1], n[2]) || 1;
1742
+ for (let k = 0; k < 3; k++) out.setFloat32(o + k * 4, n[k] / len, true);
1743
+ for (let k = 0; k < 9; k++) out.setFloat32(o + 12 + k * 4, v[k], true);
1744
+ }
1745
+ return new Uint8Array(out.buffer);
1746
+ }
1747
+ function toSolidStl(model, lib, unit, contacts2 = []) {
1748
+ const placed = model.items.map((item) => {
1749
+ const part2 = lib[item.part];
1750
+ if (!part2) throw new Error(`Unknown part: ${item.part}`);
1751
+ const { min: min2, max: max2 } = meshBounds(part2.triangles);
1752
+ const lo = [Infinity, Infinity, Infinity];
1753
+ const hi = [-Infinity, -Infinity, -Infinity];
1754
+ for (let c = 0; c < 8; c++) {
1755
+ const p = add(
1756
+ apply(item.rotation, [
1757
+ c & 1 ? max2[0] : min2[0],
1758
+ c & 2 ? max2[1] : min2[1],
1759
+ c & 4 ? max2[2] : min2[2]
1760
+ ]),
1761
+ item.position
1762
+ );
1763
+ for (let k = 0; k < 3; k++) {
1764
+ lo[k] = Math.min(lo[k], p[k]);
1765
+ hi[k] = Math.max(hi[k], p[k]);
1766
+ }
1767
+ }
1768
+ return { item, voxels: partVoxels(part2), lo, hi };
1769
+ });
1770
+ const min = [0, 1, 2].map((k) => Math.min(...placed.map((p) => p.lo[k])) - CELL);
1771
+ const max = [0, 1, 2].map((k) => Math.max(...placed.map((p) => p.hi[k])) + CELL);
1772
+ const dims = [0, 1, 2].map((k) => Math.ceil((max[k] - min[k]) / CELL));
1773
+ const solid = new Uint8Array(dims[0] * dims[1] * dims[2]);
1774
+ const owner = new Int32Array(solid.length).fill(-1);
1775
+ const at = (i, j, k) => (k * dims[1] + j) * dims[0] + i;
1776
+ placed.forEach(({ item, voxels, lo, hi }, index2) => {
1777
+ const r = item.rotation;
1778
+ const i0 = [0, 1, 2].map((k) => Math.max(0, Math.floor((lo[k] - min[k]) / CELL)));
1779
+ const i1 = [0, 1, 2].map((k) => Math.min(dims[k] - 1, Math.ceil((hi[k] - min[k]) / CELL)));
1780
+ for (let k = i0[2]; k <= i1[2]; k++)
1781
+ for (let j = i0[1]; j <= i1[1]; j++)
1782
+ for (let i = i0[0]; i <= i1[0]; i++) {
1783
+ const n = at(i, j, k);
1784
+ if (solid[n]) continue;
1785
+ const d = [
1786
+ min[0] + (i + 0.5) * CELL - item.position[0],
1787
+ min[1] + (j + 0.5) * CELL - item.position[1],
1788
+ min[2] + (k + 0.5) * CELL - item.position[2]
1789
+ ];
1790
+ const local = [
1791
+ r[0] * d[0] + r[3] * d[1] + r[6] * d[2],
1792
+ r[1] * d[0] + r[4] * d[1] + r[7] * d[2],
1793
+ r[2] * d[0] + r[5] * d[1] + r[8] * d[2]
1794
+ ];
1795
+ if (cellAt(voxels, local)) {
1796
+ solid[n] = 1;
1797
+ owner[n] = index2;
1798
+ }
1799
+ }
1800
+ });
1801
+ weld(contacts2, placed, solid, owner, dims, min, at);
1802
+ unpinch(solid, dims);
1803
+ const tris = [];
1804
+ const corner = (i, j, k) => {
1805
+ const x = min[0] + i * CELL;
1806
+ const y = min[1] + j * CELL;
1807
+ const z5 = min[2] + k * CELL;
1808
+ tris.push(x * unit, -z5 * unit, y * unit);
1809
+ };
1810
+ const filled = (i, j, k) => i >= 0 && j >= 0 && k >= 0 && i < dims[0] && j < dims[1] && k < dims[2] && solid[at(i, j, k)] === 1;
1811
+ const faces = [
1812
+ [
1813
+ [1, 0, 0],
1814
+ [
1815
+ [1, 0, 0],
1816
+ [1, 1, 0],
1817
+ [1, 1, 1],
1818
+ [1, 0, 1]
1819
+ ]
1820
+ ],
1821
+ [
1822
+ [-1, 0, 0],
1823
+ [
1824
+ [0, 0, 0],
1825
+ [0, 0, 1],
1826
+ [0, 1, 1],
1827
+ [0, 1, 0]
1828
+ ]
1829
+ ],
1830
+ [
1831
+ [0, 1, 0],
1832
+ [
1833
+ [0, 1, 0],
1834
+ [0, 1, 1],
1835
+ [1, 1, 1],
1836
+ [1, 1, 0]
1837
+ ]
1838
+ ],
1839
+ [
1840
+ [0, -1, 0],
1841
+ [
1842
+ [0, 0, 0],
1843
+ [1, 0, 0],
1844
+ [1, 0, 1],
1845
+ [0, 0, 1]
1846
+ ]
1847
+ ],
1848
+ [
1849
+ [0, 0, 1],
1850
+ [
1851
+ [0, 0, 1],
1852
+ [1, 0, 1],
1853
+ [1, 1, 1],
1854
+ [0, 1, 1]
1855
+ ]
1856
+ ],
1857
+ [
1858
+ [0, 0, -1],
1859
+ [
1860
+ [0, 0, 0],
1861
+ [0, 1, 0],
1862
+ [1, 1, 0],
1863
+ [1, 0, 0]
1864
+ ]
1865
+ ]
1866
+ ];
1867
+ for (let k = 0; k < dims[2]; k++)
1868
+ for (let j = 0; j < dims[1]; j++)
1869
+ for (let i = 0; i < dims[0]; i++) {
1870
+ if (!solid[at(i, j, k)]) continue;
1871
+ for (const [n, q] of faces) {
1872
+ if (filled(i + n[0], j + n[1], k + n[2])) continue;
1873
+ for (const v of [q[0], q[1], q[2], q[0], q[2], q[3]])
1874
+ corner(i + v[0], j + v[1], k + v[2]);
1875
+ }
1876
+ }
1877
+ return encodeStl(tris);
1878
+ }
1879
+ function components(solid, dims) {
1880
+ const label2 = new Int32Array(solid.length);
1881
+ const stack = [];
1882
+ const plane = dims[0] * dims[1];
1883
+ let next = 0;
1884
+ for (let start = 0; start < solid.length; start++) {
1885
+ if (!solid[start] || label2[start]) continue;
1886
+ label2[start] = ++next;
1887
+ stack.push(start);
1888
+ while (stack.length) {
1889
+ const n = stack.pop();
1890
+ const i = n % dims[0];
1891
+ const j = Math.floor(n / dims[0]) % dims[1];
1892
+ const k = Math.floor(n / plane);
1893
+ const near = [
1894
+ i > 0 ? n - 1 : -1,
1895
+ i < dims[0] - 1 ? n + 1 : -1,
1896
+ j > 0 ? n - dims[0] : -1,
1897
+ j < dims[1] - 1 ? n + dims[0] : -1,
1898
+ k > 0 ? n - plane : -1,
1899
+ k < dims[2] - 1 ? n + plane : -1
1900
+ ];
1901
+ for (const m of near)
1902
+ if (m >= 0 && solid[m] && !label2[m]) {
1903
+ label2[m] = next;
1904
+ stack.push(m);
1905
+ }
1906
+ }
1907
+ }
1908
+ return label2;
1909
+ }
1910
+ function weld(contacts2, placed, solid, owner, dims, min, at) {
1911
+ const cellOf = (item) => owner.indexOf(item);
1912
+ let label2 = components(solid, dims);
1913
+ for (const c of contacts2) {
1914
+ const a = cellOf(c.a);
1915
+ const b = cellOf(c.b);
1916
+ if (a < 0 || b < 0) continue;
1917
+ const boxes = [placed[c.a], placed[c.b]];
1918
+ const reach = Math.max(
1919
+ ...boxes.flatMap(
1920
+ (box2) => [0, 1, 2].map((k) => Math.max(c.at[k] - box2.lo[k], box2.hi[k] - c.at[k]))
1921
+ )
1922
+ );
1923
+ for (let r = CELL; label2[a] !== label2[b] && r <= reach + CELL; r += CELL) {
1924
+ const i0 = [0, 1, 2].map((k) => Math.max(0, Math.floor((c.at[k] - r - min[k]) / CELL)));
1925
+ const i1 = [0, 1, 2].map(
1926
+ (k) => Math.min(dims[k] - 1, Math.ceil((c.at[k] + r - min[k]) / CELL))
1927
+ );
1928
+ for (let k = i0[2]; k <= i1[2]; k++)
1929
+ for (let j = i0[1]; j <= i1[1]; j++)
1930
+ for (let i = i0[0]; i <= i1[0]; i++) {
1931
+ const p = [
1932
+ min[0] + (i + 0.5) * CELL,
1933
+ min[1] + (j + 0.5) * CELL,
1934
+ min[2] + (k + 0.5) * CELL
1935
+ ];
1936
+ if (Math.hypot(p[0] - c.at[0], p[1] - c.at[1], p[2] - c.at[2]) > r) continue;
1937
+ if (!boxes.some((box2) => p.every((v, q) => v >= box2.lo[q] && v <= box2.hi[q]))) continue;
1938
+ solid[at(i, j, k)] = 1;
1939
+ }
1940
+ label2 = components(solid, dims);
1941
+ }
1942
+ }
1943
+ }
1944
+ function unpinch(solid, dims) {
1945
+ const step = [1, dims[0], dims[0] * dims[1]];
1946
+ const coord = (n) => [
1947
+ n % dims[0],
1948
+ Math.floor(n / dims[0]) % dims[1],
1949
+ Math.floor(n / step[2])
1950
+ ];
1951
+ for (let changed = true; changed; ) {
1952
+ changed = false;
1953
+ for (let n = 0; n < solid.length; n++) {
1954
+ const c = coord(n);
1955
+ for (const [u, w] of [
1956
+ [0, 1],
1957
+ [1, 2],
1958
+ [0, 2]
1959
+ ]) {
1960
+ if (c[u] + 1 >= dims[u] || c[w] + 1 >= dims[w]) continue;
1961
+ const a = n;
1962
+ const b = n + step[u];
1963
+ const d = n + step[w];
1964
+ const e = n + step[u] + step[w];
1965
+ if (solid[a] && solid[e] && !solid[b] && !solid[d]) {
1966
+ solid[b] = 1;
1967
+ changed = true;
1968
+ } else if (solid[b] && solid[d] && !solid[a] && !solid[e]) {
1969
+ solid[a] = 1;
1970
+ changed = true;
1971
+ }
1972
+ }
1973
+ }
1974
+ }
1975
+ }
1976
+ var COLLISION_CELLS = 4;
1977
+ var SOCKET_REACH = 0.5;
1978
+ function validate(model, lib, adapter, opts = {}) {
1979
+ const issues = [];
1980
+ const n = model.items.length;
1981
+ const joints = contacts(model, lib, adapter);
1982
+ const bodies = model.items.map((it) => worldBody(lib, it));
1983
+ const through = new Set(joints.filter((c) => c.axis).map((c) => `${c.a},${c.b}`));
1984
+ const balls = new Map(
1985
+ joints.filter((c) => c.kind.split("-").every((k) => isSpherical(adapter, k))).map((c) => [`${c.a},${c.b}`, c.at])
1986
+ );
1987
+ for (const [a, b] of nearPairs(bodies, 0.8)) {
1988
+ if (through.has(`${a},${b}`) || overlapVolume(bodies[a], bodies[b], -0.4) <= 0) continue;
1989
+ const centre = balls.get(`${a},${b}`);
1990
+ const ignore = centre && { centre, radius: SOCKET_REACH };
1991
+ if (collisionCells(lib, model.items[a], model.items[b], ignore) > COLLISION_CELLS)
1992
+ issues.push({ rule: "overlap", items: [a, b], message: "two parts occupy the same space" });
1993
+ }
1994
+ const parent = model.items.map((_, i) => i);
1995
+ const find = (x) => {
1996
+ while (parent[x] !== x) x = parent[x];
1997
+ return x;
1998
+ };
1999
+ for (const c of joints) parent[find(c.a)] = find(c.b);
2000
+ const groups = /* @__PURE__ */ new Map();
2001
+ for (let i = 0; i < n; i++) groups.set(find(i), [...groups.get(find(i)) ?? [], i]);
2002
+ const sorted = [...groups.values()].sort((x, y) => y.length - x.length);
2003
+ for (const g of sorted.slice(1))
2004
+ issues.push({ rule: "disconnected", items: g, message: "parts not joined to the main build" });
2005
+ if (opts.grammar)
2006
+ for (const c of joints) {
2007
+ const [a, b] = [model.items[c.a].part, model.items[c.b].part];
2008
+ if (!jointAllowed(opts.grammar, adapter, a, b, c.kind, opts.mode ?? "strict"))
2009
+ issues.push({
2010
+ rule: "unseen-joint",
2011
+ items: [c.a, c.b],
2012
+ message: "a joint the exemplar never makes"
2013
+ });
2014
+ }
2015
+ return { ok: issues.length === 0, issues, contacts: joints };
2016
+ }
2017
+
2018
+ // src/brick.ts
2019
+ import sharp from "sharp";
2020
+ function ldrawLibrary(flag) {
2021
+ const dir = typeof flag === "string" ? flag : process.env.LDRAW_DIR;
2022
+ if (!dir)
2023
+ throw new InputError(
2024
+ "Pass --library <ldraw-dir> or set LDRAW_DIR to an unpacked LDraw library (https://library.ldraw.org)."
2025
+ );
2026
+ const root = resolve(dir);
2027
+ if (!existsSync(join(root, "parts")) || !existsSync(join(root, "p")))
2028
+ throw new InputError(`${root} is not an LDraw library: expected parts/ and p/ inside.`);
2029
+ return root;
2030
+ }
2031
+ async function readBrickModel(file, library) {
2032
+ const text = await readFile(resolve(file), "utf8").catch((error) => {
2033
+ if (error.code === "ENOENT") throw new InputError(`No model at ${resolve(file)}.`);
2034
+ throw error;
2035
+ });
2036
+ const model = parseModel(text);
2037
+ if (!model.items.length) throw new InputError(`${file} places no parts.`);
2038
+ const read = (path) => existsSync(path) ? readFileSync(path, "utf8") : void 0;
2039
+ const resolver = libraryResolver(read, library);
2040
+ const lib = {};
2041
+ const names = {};
2042
+ for (const item of model.items) {
2043
+ if (lib[item.part]) continue;
2044
+ const text2 = resolver(`${item.part}.dat`);
2045
+ if (text2 === void 0) throw new InputError(`Part ${item.part} is not in the LDraw library.`);
2046
+ names[item.part] = text2.split(/\r?\n/, 1)[0].replace(/^0\s*/, "").trim();
2047
+ lib[item.part] = loadLegoPart(item.part, resolver, { name: names[item.part] });
2048
+ }
2049
+ const config = read(join(library, "LDConfig.ldr"));
2050
+ return { model, lib, names, palette: config ? parsePalette(config) : {} };
2051
+ }
2052
+ function brickPart(id, library) {
2053
+ const read = (path) => existsSync(path) ? readFileSync(path, "utf8") : void 0;
2054
+ const resolver = libraryResolver(read, library);
2055
+ const text = resolver(`${id}.dat`);
2056
+ if (text === void 0) throw new InputError(`Part ${id} is not in the LDraw library.`);
2057
+ const name = text.split(/\r?\n/, 1)[0].replace(/^0\s*/, "").trim();
2058
+ const moved2 = name.match(/^~Moved to (\S+)/);
2059
+ if (moved2) throw new InputError(`Part ${id} moved to ${moved2[1]}; use that id.`);
2060
+ return describePart(loadLegoPart(id, resolver, { name }));
2061
+ }
2062
+ function brickCheck({ model, lib, names }) {
2063
+ const result = validate(model, lib, lego);
2064
+ const label2 = (i) => `${i}:${model.items[i].part} ${names[model.items[i].part]}`;
2065
+ return {
2066
+ ok: result.ok,
2067
+ parts: model.items.length,
2068
+ joints: result.contacts.length,
2069
+ issues: result.issues.map((issue) => ({ ...issue, parts: issue.items.map(label2) }))
2070
+ };
2071
+ }
2072
+ function brickStress({ model, lib, names }) {
2073
+ const label2 = (i) => `${i}:${model.items[i].part} ${names[model.items[i].part]}`;
2074
+ return stress(model, lib, lego).map((j) => ({ ...j, between: [label2(j.a), label2(j.b)] }));
2075
+ }
2076
+ function brickPlan({ model, lib, names }) {
2077
+ const plan = planAssembly(model, lib, lego);
2078
+ const axis = (d) => ["+x", "-x", "+y", "-y", "+z", "-z"][[
2079
+ [1, 0, 0],
2080
+ [-1, 0, 0],
2081
+ [0, 1, 0],
2082
+ [0, -1, 0],
2083
+ [0, 0, 1],
2084
+ [0, 0, -1]
2085
+ ].findIndex((v) => v.every((x, k) => Math.abs(x - d[k]) < 1e-6))] ?? d.join(",");
2086
+ return {
2087
+ ...plan,
2088
+ rows: plan.steps.map((s) => ({
2089
+ step: model.items[s.item].step ?? 0,
2090
+ part: model.items[s.item].part,
2091
+ name: names[model.items[s.item].part],
2092
+ insert: axis(s.insert)
2093
+ }))
2094
+ };
2095
+ }
2096
+ async function brickRender({ model, lib, palette }, opts) {
2097
+ const highlight = opts.step === void 0 ? void 0 : model.items.map((it, i) => it.step === opts.step ? i : -1).filter((i) => i >= 0);
2098
+ if (opts.step !== void 0 && !highlight?.length)
2099
+ throw new InputError(`The model has no step ${opts.step}.`);
2100
+ const image = rasterize(model, lib, {
2101
+ width: 1200,
2102
+ height: 900,
2103
+ palette,
2104
+ background: "#ecebe7",
2105
+ maxStep: opts.step,
2106
+ highlight,
2107
+ explode: opts.explode,
2108
+ yaw: opts.yaw === void 0 ? void 0 : opts.yaw * Math.PI / 180,
2109
+ pitch: opts.pitch === void 0 ? void 0 : opts.pitch * Math.PI / 180
2110
+ });
2111
+ return sharp(Buffer.from(image.data.buffer), {
2112
+ raw: { width: image.width, height: image.height, channels: 4 }
2113
+ }).resize(1200, 900).png().toBuffer();
2114
+ }
2115
+ var MODES = ["strict", "family", "free"];
2116
+ function brickRemix(exemplar, seed, mode) {
2117
+ if (mode !== void 0 && !MODES.includes(mode))
2118
+ throw new InputError(`--mode must be one of ${MODES.join(", ")}.`);
2119
+ const result = remix(exemplar.model, exemplar.lib, lego, {
2120
+ seed,
2121
+ mode
2122
+ });
2123
+ const grown = { ...exemplar, model: result.model };
2124
+ return {
2125
+ ldr: writeModel(result.model, `Remix ${seed}`),
2126
+ left: result.left,
2127
+ check: brickCheck(grown)
2128
+ };
2129
+ }
2130
+ var STUD_MM = 8;
2131
+ function brickStl({ model, lib }, scale2 = 1) {
2132
+ if (!Number.isFinite(scale2) || scale2 <= 0)
2133
+ throw new InputError("--scale must be a positive number.");
2134
+ const stl = toSolidStl(model, lib, STUD_MM * scale2, validate(model, lib, lego).contacts);
2135
+ const view = new DataView(stl.buffer);
2136
+ const count = view.getUint32(80, true);
2137
+ const min = [Infinity, Infinity, Infinity];
2138
+ const max = [-Infinity, -Infinity, -Infinity];
2139
+ for (let f = 0; f < count; f++)
2140
+ for (let k = 0; k < 9; k++) {
2141
+ const v = view.getFloat32(84 + f * 50 + 12 + k * 4, true);
2142
+ min[k % 3] = Math.min(min[k % 3], v);
2143
+ max[k % 3] = Math.max(max[k % 3], v);
2144
+ }
2145
+ const mm = (k) => +(max[k] - min[k]).toFixed(2);
2146
+ return {
2147
+ stl,
2148
+ parts: model.items.length,
2149
+ triangles: count,
2150
+ sizeMm: { x: mm(0), y: mm(1), z: mm(2) }
2151
+ };
2152
+ }
2153
+
81
2154
  // src/card-import.ts
82
2155
  import { createHash } from "crypto";
83
- import { readFile, stat } from "fs/promises";
2156
+ import { readFile as readFile2, stat } from "fs/promises";
84
2157
  import { basename, extname } from "path";
85
2158
  import { DOMParser } from "@xmldom/xmldom";
86
- import sharp from "sharp";
2159
+ import sharp2 from "sharp";
87
2160
  var box = { x: 0, y: 0, w: 1200, h: 756 };
88
2161
  function elements(node) {
89
2162
  return Array.from(node.childNodes).filter((child) => child.nodeType === 1);
@@ -116,7 +2189,7 @@ async function importCard(file, name) {
116
2189
  const info = await stat(file);
117
2190
  if (!info.isFile() || info.size > 2e6)
118
2191
  throw new InputError("Import a local SVG or raster file below 2 MB.");
119
- const bytes = await readFile(file);
2192
+ const bytes = await readFile2(file);
120
2193
  const title = (name ?? basename(file, extname(file))).slice(0, 60);
121
2194
  const design = {
122
2195
  version: 1,
@@ -130,7 +2203,7 @@ async function importCard(file, name) {
130
2203
  back: { background: "#111111", layers: [] }
131
2204
  };
132
2205
  if (extname(file).toLowerCase() !== ".svg") {
133
- const meta = await sharp(bytes, { limitInputPixels: 24e6 }).metadata();
2206
+ const meta = await sharp2(bytes, { limitInputPixels: 24e6 }).metadata();
134
2207
  if (!meta.format || !["png", "jpeg", "webp"].includes(meta.format))
135
2208
  throw new InputError("Import PNG, JPEG, WebP or a self-contained SVG.");
136
2209
  design.assets = {
@@ -193,11 +2266,11 @@ async function importCard(file, name) {
193
2266
  `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
2267
  );
195
2268
  }
196
- design.front.layers = groups.map((children, index) => {
2269
+ design.front.layers = groups.map((children, index2) => {
197
2270
  const first = children[0];
198
2271
  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),
2272
+ id: `source-${String(index2 + 1).padStart(2, "0")}`,
2273
+ name: (grouped ? "Original composition" : first?.attrs?.["aria-label"] ?? first?.attrs?.id ?? first?.text?.trim() ?? `Original ${first?.tag} ${index2 + 1}`).slice(0, 80),
201
2274
  kind: "svg",
202
2275
  nodes: Object.keys(inherited).length ? [{ tag: "g", attrs: inherited, children }] : children,
203
2276
  ...box,
@@ -210,7 +2283,7 @@ async function importCard(file, name) {
210
2283
  for (const layer of design.front.layers) {
211
2284
  if (layer.kind !== "svg") continue;
212
2285
  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 });
2286
+ const { data, info: pixels } = await sharp2(Buffer.from(markup)).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
214
2287
  let left = 1200, top = 756, right = -1, bottom = -1;
215
2288
  for (let y = 0; y < pixels.height; y++) {
216
2289
  for (let x = 0; x < pixels.width; x++) {
@@ -229,14 +2302,43 @@ async function importCard(file, name) {
229
2302
  return badgeDesignSchema.parse(design);
230
2303
  }
231
2304
 
2305
+ // src/grid.ts
2306
+ var whole = (name, raw) => {
2307
+ const n = Number(raw);
2308
+ if (raw === void 0 || !Number.isInteger(n) || n < 1)
2309
+ throw new InputError(`--${name} must be a whole number of at least 1.`);
2310
+ return n;
2311
+ };
2312
+ function gridStl(kind, values) {
2313
+ const part2 = kind === "bin" ? gridfinityBin(
2314
+ whole("width", values.width),
2315
+ whole("depth", values.depth),
2316
+ whole("height", values.height)
2317
+ ) : gridfinityBaseplate(whole("width", values.width), whole("depth", values.depth));
2318
+ const stl = toStl(
2319
+ { items: [{ part: part2.id, rotation: IDENTITY, position: [0, 0, 0], color: 16 }] },
2320
+ { [part2.id]: part2 },
2321
+ GRIDFINITY_UNIT_MM
2322
+ );
2323
+ const { min, max } = meshBounds(part2.triangles);
2324
+ const mm = (k) => +((max[k] - min[k]) * GRIDFINITY_UNIT_MM).toFixed(2);
2325
+ return {
2326
+ stl,
2327
+ part: part2.id,
2328
+ name: part2.name,
2329
+ triangles: part2.triangles.length / 9,
2330
+ sizeMm: { x: mm(0), y: mm(2), z: mm(1) }
2331
+ };
2332
+ }
2333
+
232
2334
  // src/images.ts
233
- import { readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
234
- import { extname as extname2, resolve } from "path";
2335
+ import { readFile as readFile3, stat as stat2, writeFile } from "fs/promises";
2336
+ import { extname as extname2, resolve as resolve2 } from "path";
235
2337
  async function readToolResult(path) {
236
2338
  const info = await stat2(path);
237
2339
  if (!info.isFile() || info.size > 12e6)
238
2340
  throw new InputError("Expected a result file below 12 MB.");
239
- let value = JSON.parse(await readFile2(path, "utf8"));
2341
+ let value = JSON.parse(await readFile3(path, "utf8"));
240
2342
  if (typeof value === "string") value = JSON.parse(value);
241
2343
  if (value && typeof value === "object" && "success" in value) {
242
2344
  if (!value.success) throw new InputError("The browser command failed.");
@@ -263,7 +2365,7 @@ async function imageParams(file, state, target = "portrait") {
263
2365
  const current = await readToolResult(state);
264
2366
  if (typeof current.revision !== "string")
265
2367
  throw new InputError("Inspect the editor to obtain its revision.");
266
- const bytes = await readFile2(file);
2368
+ const bytes = await readFile3(file);
267
2369
  if (bytes.length > 4e6) throw new InputError("Image must be smaller than 4 MB.");
268
2370
  return {
269
2371
  action: "import",
@@ -281,15 +2383,15 @@ async function extractImage(file, out) {
281
2383
  throw new InputError("The tool did not return a supported image.");
282
2384
  const bytes = Buffer.from(match[1], "base64");
283
2385
  if (bytes.length > 4e6) throw new InputError("Image is larger than 4 MB.");
284
- const path = resolve(out);
2386
+ const path = resolve2(out);
285
2387
  await writeFile(path, bytes, { flag: "wx", mode: 384 });
286
2388
  return { path, bytes: bytes.length };
287
2389
  }
288
2390
 
289
2391
  // src/kit-extract.ts
290
2392
  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";
2393
+ import { dirname, join as join2, resolve as resolve3 } from "path";
2394
+ import sharp3 from "sharp";
293
2395
  import { z } from "zod";
294
2396
  var regionSchema = z.object({
295
2397
  x: z.number().int().nonnegative(),
@@ -366,33 +2468,33 @@ function artworkRegions(foreground, w, h, gap, minArea) {
366
2468
  function removeOuterBackground(rgba, w, h, background, tolerance) {
367
2469
  const result = new Uint8Array(rgba), seen = new Uint8Array(w * h), queue = new Int32Array(w * h);
368
2470
  let head = 0, tail = 0;
369
- const add = (p) => {
2471
+ const add2 = (p) => {
370
2472
  if (seen[p]) return;
371
2473
  seen[p] = 1;
372
2474
  if (rgba[p * 4 + 3] < 16 || [0, 1, 2].reduce((n, c) => n + Math.abs(rgba[p * 4 + c] - background[c]), 0) <= tolerance)
373
2475
  queue[tail++] = p;
374
2476
  };
375
2477
  for (let x = 0; x < w; x++) {
376
- add(x);
377
- add((h - 1) * w + x);
2478
+ add2(x);
2479
+ add2((h - 1) * w + x);
378
2480
  }
379
2481
  for (let y = 0; y < h; y++) {
380
- add(y * w);
381
- add(y * w + w - 1);
2482
+ add2(y * w);
2483
+ add2(y * w + w - 1);
382
2484
  }
383
2485
  while (head < tail) {
384
2486
  const p = queue[head++], x = p % w, y = Math.floor(p / w);
385
2487
  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);
2488
+ if (x) add2(p - 1);
2489
+ if (x < w - 1) add2(p + 1);
2490
+ if (y) add2(p - w);
2491
+ if (y < h - 1) add2(p + w);
390
2492
  }
391
2493
  return result;
392
2494
  }
393
2495
  async function extractKit(file, out, input) {
394
2496
  const options = optionsSchema.parse(input);
395
- const { data, info } = await sharp2(resolve2(file), { limitInputPixels: 24e6 }).rotate().ensureAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
2497
+ const { data, info } = await sharp3(resolve3(file), { limitInputPixels: 24e6 }).rotate().ensureAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
396
2498
  const w = info.width, h = info.height;
397
2499
  const corners = [0, w - 1, (h - 1) * w, w * h - 1];
398
2500
  const transparentBackground = corners.every((p) => data[p * 4 + 3] < 16);
@@ -407,9 +2509,9 @@ async function extractKit(file, out, input) {
407
2509
  throw new InputError("Choose 1\u201324 regions using --params, or adjust gap/minArea.");
408
2510
  if (regions.some((r) => r.x + r.w > w || r.y + r.h > h))
409
2511
  throw new InputError("Every region must fit inside the image.");
410
- const target = resolve2(out), temporary = await mkdtemp(join(dirname(target), ".makeables-extract-"));
2512
+ const target = resolve3(out), temporary = await mkdtemp(join2(dirname(target), ".makeables-extract-"));
411
2513
  try {
412
- await mkdir(join(temporary, "pieces"));
2514
+ await mkdir(join2(temporary, "pieces"));
413
2515
  const kit = {
414
2516
  format: "makeables-kit",
415
2517
  version: 1,
@@ -418,11 +2520,11 @@ async function extractKit(file, out, input) {
418
2520
  sheets: []
419
2521
  };
420
2522
  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();
2523
+ for (const [index2, region] of regions.entries()) {
2524
+ const id = `piece-${String(index2 + 1).padStart(2, "0")}`, name = region.name ?? `Piece ${index2 + 1}`;
2525
+ 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
2526
  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();
2527
+ const png = await sharp3(pixels, { raw: { width: region.w, height: region.h, channels: 4 } }).png().toBuffer();
426
2528
  const design = createStickerDesign();
427
2529
  design.sticker = { ...defaultStickerMaterial, finish: "vinyl", sparkle: 0, holography: 0 };
428
2530
  design.name = name;
@@ -455,19 +2557,19 @@ async function extractKit(file, out, input) {
455
2557
  };
456
2558
  design.back = { background: "#00000000", layers: [] };
457
2559
  await writeFile2(
458
- join(temporary, `pieces/${id}.json`),
2560
+ join2(temporary, `pieces/${id}.json`),
459
2561
  JSON.stringify(badgeDesignSchema.parse(design), null, 2)
460
2562
  );
461
2563
  kit.items.push({ id, file: `pieces/${id}.json` });
462
2564
  views.push({ name, bytes: png });
463
2565
  }
464
- await writeFile2(join(temporary, "kit.json"), JSON.stringify(kit, null, 2));
465
- await writeFile2(join(temporary, "contact.png"), await kitCover(options.name, views));
2566
+ await writeFile2(join2(temporary, "kit.json"), JSON.stringify(kit, null, 2));
2567
+ await writeFile2(join2(temporary, "contact.png"), await kitCover(options.name, views));
466
2568
  await mkdir(target);
467
2569
  await rename(temporary, target);
468
2570
  return {
469
- path: join(target, "kit.json"),
470
- preview: join(target, "contact.png"),
2571
+ path: join2(target, "kit.json"),
2572
+ preview: join2(target, "contact.png"),
471
2573
  items: kit.items.length,
472
2574
  regions,
473
2575
  source: { width: w, height: h },
@@ -479,6 +2581,246 @@ async function extractKit(file, out, input) {
479
2581
  }
480
2582
  }
481
2583
 
2584
+ // src/layers.ts
2585
+ import { randomBytes } from "crypto";
2586
+ import { ZodError } from "zod";
2587
+ var layerKinds = ["text", "shape", "graphic", "qr"];
2588
+ function defaults(kind, width, height) {
2589
+ const centered = (w, h) => ({
2590
+ x: Math.round((width - w) / 2),
2591
+ y: Math.round((height - h) / 2),
2592
+ w,
2593
+ h
2594
+ });
2595
+ if (kind === "text")
2596
+ return {
2597
+ ...centered(Math.round(width * 0.8), 160),
2598
+ text: "Text",
2599
+ binding: "none",
2600
+ color: "#141414",
2601
+ font: "sans",
2602
+ size: 72,
2603
+ weight: "700",
2604
+ align: "center",
2605
+ fit: "shrink"
2606
+ };
2607
+ if (kind === "shape")
2608
+ return {
2609
+ ...centered(Math.round(width * 0.6), Math.round(height * 0.3)),
2610
+ shape: "rectangle",
2611
+ color: "#141414",
2612
+ radius: 0,
2613
+ stroke: 0,
2614
+ opacity: 1
2615
+ };
2616
+ if (kind === "qr") return centered(320, 320);
2617
+ return {
2618
+ ...centered(Math.round(width * 0.6), Math.round(width * 0.6)),
2619
+ pattern: "path",
2620
+ color: "#141414",
2621
+ accent: "#141414",
2622
+ density: 1,
2623
+ seed: 1
2624
+ };
2625
+ }
2626
+ function addLayer(design, face, kind, params = {}) {
2627
+ if (!layerKinds.includes(kind))
2628
+ throw new InputError(
2629
+ `--kind must be one of ${layerKinds.join(", ")}. Add images with --sticker <path>.`
2630
+ );
2631
+ const profile = designProfile(design);
2632
+ const id = typeof params.id === "string" ? params.id : `${kind}-${randomBytes(3).toString("hex")}`;
2633
+ if (design[face].layers.some((layer2) => layer2.id === id))
2634
+ throw new InputError(`Layer ${id} already exists on ${face}. Use layer set to change it.`);
2635
+ const layer = {
2636
+ ...defaults(kind, profile.width, profile.height),
2637
+ ...params,
2638
+ id,
2639
+ kind
2640
+ };
2641
+ if (kind === "graphic" && layer.pattern === "path" && !layer.path)
2642
+ throw new InputError('A path graphic needs "path" (SVG path data) and "viewBox" in --params.');
2643
+ try {
2644
+ const next = badgeDesignSchema.parse({
2645
+ ...design,
2646
+ [face]: { ...design[face], layers: [...design[face].layers, layer] }
2647
+ });
2648
+ const text = typeof layer.text === "string" ? layer.text : "";
2649
+ const unsupported = [
2650
+ ...new Set(
2651
+ [...text].filter((c) => {
2652
+ const code = c.codePointAt(0) ?? 0;
2653
+ return code > 591 && !(code >= 8208 && code <= 8231) && code !== 8364;
2654
+ })
2655
+ )
2656
+ ];
2657
+ const warnings = unsupported.length ? [
2658
+ `The bundled fonts may not draw ${unsupported.join(" ")}. Draw symbols as a graphic path or image layer.`
2659
+ ] : [];
2660
+ return { design: next, id, warnings };
2661
+ } catch (error) {
2662
+ if (error instanceof ZodError)
2663
+ throw new InputError(
2664
+ `Invalid ${kind} layer: ${error.issues.map((issue) => `${issue.path.slice(3).join(".") || "layer"}: ${issue.message}`).join("; ")}. Run makeables schema for every field.`
2665
+ );
2666
+ throw error;
2667
+ }
2668
+ }
2669
+ function blankDesign(product, name) {
2670
+ const title = typeof name === "string" && name.trim() ? name.trim().slice(0, 60) : `Untitled ${product}`;
2671
+ const preset = structuredClone(
2672
+ product === "card" ? findCardStyle("pearl") : findDesign("herbario-azul")
2673
+ );
2674
+ if (!preset) throw new Error(`Missing base ${product} style.`);
2675
+ const base = {
2676
+ ...preset,
2677
+ product,
2678
+ name: title,
2679
+ event: title,
2680
+ description: "",
2681
+ artPrompt: "",
2682
+ source: void 0,
2683
+ artwork: void 0,
2684
+ assets: {}
2685
+ };
2686
+ if (product === "card")
2687
+ return badgeDesignSchema.parse({
2688
+ ...base,
2689
+ front: {
2690
+ background: "#f4f1eb",
2691
+ layers: preset.front.layers.filter((layer) => layer.kind === "card-mark")
2692
+ },
2693
+ back: { background: "#f4f1eb", layers: [] }
2694
+ });
2695
+ const portrait = preset.front.layers.find((layer) => layer.kind === "portrait");
2696
+ const text = (id, binding, y, size, weight = "700") => ({
2697
+ id,
2698
+ kind: "text",
2699
+ x: 96,
2700
+ y,
2701
+ w: 832,
2702
+ h: Math.round(size * 1.3),
2703
+ text: "",
2704
+ binding,
2705
+ color: "#141414",
2706
+ font: "sans",
2707
+ size,
2708
+ weight,
2709
+ align: "center",
2710
+ fit: "shrink"
2711
+ });
2712
+ return badgeDesignSchema.parse({
2713
+ ...base,
2714
+ front: {
2715
+ background: "#f4f1eb",
2716
+ layers: [
2717
+ ...portrait ? [{ ...portrait, x: 192, y: 240, w: 640, h: 640, filter: "original", fade: void 0 }] : [],
2718
+ text("participant-name", "name", 960, 96),
2719
+ text("participant-role", "role", 1100, 48, "500")
2720
+ ]
2721
+ },
2722
+ back: {
2723
+ background: "#f4f1eb",
2724
+ // The back repeats the holder: badges need name and role on both faces.
2725
+ layers: [
2726
+ text("back-name", "name", 200, 72),
2727
+ text("back-role", "role", 310, 40, "500"),
2728
+ { id: "access-qr", kind: "qr", x: 312, y: 520, w: 400, h: 400 },
2729
+ text("event-name", "event", 1e3, 56)
2730
+ ]
2731
+ }
2732
+ });
2733
+ }
2734
+
2735
+ // src/preflight.ts
2736
+ import { spawnSync } from "child_process";
2737
+ import { mkdtemp as mkdtemp2, readdir, rm as rm2 } from "fs/promises";
2738
+ import { tmpdir } from "os";
2739
+ import { join as join3 } from "path";
2740
+ var generationKeys = [
2741
+ "AI_GATEWAY_API_KEY",
2742
+ "VERCEL_OIDC_TOKEN",
2743
+ "OPENAI_API_KEY",
2744
+ "GOOGLE_GENERATIVE_AI_API_KEY"
2745
+ ];
2746
+ var defaultRun = (command, args) => {
2747
+ const result = spawnSync(command, args, { encoding: "utf8", shell: false, timeout: 12e4 });
2748
+ return {
2749
+ status: result.status,
2750
+ stdout: result.stdout ?? "",
2751
+ stderr: result.stderr ?? "",
2752
+ error: result.error
2753
+ };
2754
+ };
2755
+ function redact(text, env) {
2756
+ let out = text;
2757
+ for (const key2 of generationKeys) {
2758
+ const value = env[key2];
2759
+ if (value && value.length > 4) out = out.split(value).join("[redacted]");
2760
+ }
2761
+ return out.trim().slice(0, 400);
2762
+ }
2763
+ async function preflight(options = {}) {
2764
+ const env = options.env ?? process.env;
2765
+ const run2 = options.run ?? defaultRun;
2766
+ const version2 = run2("ai", ["--version"]);
2767
+ const aiCli = {
2768
+ installed: !version2.error && version2.status === 0,
2769
+ version: version2.status === 0 ? version2.stdout.trim().slice(0, 40) : null,
2770
+ install: "npm install --global ai-cli"
2771
+ };
2772
+ const keys = generationKeys.filter((key2) => Boolean(env[key2]));
2773
+ let probe = null;
2774
+ if (options.probe) {
2775
+ if (!aiCli.installed) probe = { ok: false, error: "ai-cli is not installed." };
2776
+ else if (!keys.length) probe = { ok: false, error: "No generation key in the environment." };
2777
+ else {
2778
+ const dir = await mkdtemp2(join3(tmpdir(), "makeables-probe-"));
2779
+ try {
2780
+ const result = run2("ai", [
2781
+ "image",
2782
+ "--quiet",
2783
+ "--json",
2784
+ "--no-preview",
2785
+ "--output",
2786
+ dir,
2787
+ "a small flat orange circle on a white background"
2788
+ ]);
2789
+ const made = (await readdir(dir)).length > 0;
2790
+ probe = result.status === 0 && made ? { ok: true } : {
2791
+ ok: false,
2792
+ error: redact(result.stderr || result.stdout || "No image was produced.", env)
2793
+ };
2794
+ } finally {
2795
+ await rm2(dir, { recursive: true, force: true });
2796
+ }
2797
+ }
2798
+ }
2799
+ const cliReady = probe ? probe.ok : aiCli.installed && keys.length > 0;
2800
+ const tracing = run2("potrace", ["--version"]);
2801
+ return {
2802
+ imageGeneration: {
2803
+ aiCli,
2804
+ keys,
2805
+ probe,
2806
+ ready: cliReady,
2807
+ recommendation: cliReady ? "generate" : "host-or-install"
2808
+ },
2809
+ potrace: { installed: !tracing.error && tracing.status === 0 },
2810
+ ask: {
2811
+ source: ["official", "generated", "mixed"],
2812
+ defaultSource: "mixed",
2813
+ stickers: { min: 6, max: 10 }
2814
+ }
2815
+ };
2816
+ }
2817
+ function preflightSummary(result) {
2818
+ const generation = result.imageGeneration;
2819
+ 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.";
2820
+ return `${line}
2821
+ Ask the user: art source (official, generated or mixed) and how many stickers (6-10).`;
2822
+ }
2823
+
482
2824
  // src/presentation.ts
483
2825
  import { stripVTControlCharacters } from "util";
484
2826
  function machineOutput(json) {
@@ -503,10 +2845,10 @@ function banner(version2) {
503
2845
  // src/publish.ts
504
2846
  import { createHash as createHash2, randomUUID } from "crypto";
505
2847
  import { constants } from "fs";
506
- import { appendFile, link, mkdir as mkdir2, open, readFile as readFile3, rename as rename2, rm as rm2 } from "fs/promises";
2848
+ import { appendFile, link, mkdir as mkdir2, open, readFile as readFile4, rename as rename2, rm as rm3 } from "fs/promises";
507
2849
  import { homedir } from "os";
508
- import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
509
- import sharp3 from "sharp";
2850
+ import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
2851
+ import sharp4 from "sharp";
510
2852
  import { z as z2 } from "zod";
511
2853
  async function saveBundle(url, out) {
512
2854
  const state = await studioRequest(url, "/call", {
@@ -528,7 +2870,7 @@ async function saveBundle(url, out) {
528
2870
  result.error?.message ?? "The editor could not export a bundle. Refresh it after saving the current draft."
529
2871
  );
530
2872
  const pkg = await readPackageJson(result.data);
531
- const path = resolve3(out);
2873
+ const path = resolve4(out);
532
2874
  const file = await open(path, "wx", 384);
533
2875
  try {
534
2876
  await file.writeFile(`${JSON.stringify(packageJson(pkg))}
@@ -537,7 +2879,7 @@ async function saveBundle(url, out) {
537
2879
  } finally {
538
2880
  await file.close();
539
2881
  }
540
- const saved = await readFile3(path);
2882
+ const saved = await readFile4(path);
541
2883
  return {
542
2884
  path,
543
2885
  bytes: saved.length,
@@ -557,12 +2899,12 @@ var receiptSchema = z2.object({
557
2899
  });
558
2900
 
559
2901
  // 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";
2902
+ import { spawnSync as spawnSync2 } from "child_process";
2903
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
2904
+ import { tmpdir as tmpdir2 } from "os";
2905
+ import { join as join5, resolve as resolve5 } from "path";
564
2906
  import { DOMParser as DOMParser2, XMLSerializer } from "@xmldom/xmldom";
565
- import sharp4 from "sharp";
2907
+ import sharp5 from "sharp";
566
2908
  import { z as z3 } from "zod";
567
2909
  var W = 1200;
568
2910
  var H = 756;
@@ -597,7 +2939,7 @@ function fail(message) {
597
2939
  }
598
2940
  var nums = (value, count, name) => {
599
2941
  const parts = value.split(",").map(Number);
600
- if (value.split(",").some((part) => !part.trim())) fail(`${name} contains an empty coordinate.`);
2942
+ if (value.split(",").some((part2) => !part2.trim())) fail(`${name} contains an empty coordinate.`);
601
2943
  if (parts.length < count || parts.some((v) => !Number.isFinite(v)))
602
2944
  fail(`${name} needs ${count} comma-separated numbers.`);
603
2945
  return parts;
@@ -609,7 +2951,7 @@ function validBox(values, width, height) {
609
2951
  }
610
2952
  var lumOf = (r, g, b) => 0.299 * r + 0.587 * g + 0.114 * b;
611
2953
  async function detect(file) {
612
- const { data, info } = await sharp4(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
2954
+ const { data, info } = await sharp5(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
613
2955
  const { width: w, height: h } = info;
614
2956
  const px = (x, y) => {
615
2957
  const i = (y * w + x) * 3;
@@ -685,7 +3027,7 @@ async function detect(file) {
685
3027
  async function prepare(file, o) {
686
3028
  if (!o.crop || !o.out) fail('prepare needs --out <dir> and --params with {"crop":"x,y,w,h"}.');
687
3029
  const [cx, cy, cw, ch] = nums(o.crop, 4, "crop");
688
- const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
3030
+ const meta = await sharp5(file, { limitInputPixels: 24e6 }).metadata();
689
3031
  validBox([cx, cy, cw, ch], meta.width ?? 0, meta.height ?? 0);
690
3032
  if (Math.abs(cw / ch / (W / H) - 1) > 0.03)
691
3033
  fail(
@@ -698,12 +3040,12 @@ async function prepare(file, o) {
698
3040
  if (feather < 0 || ![0, 1].includes(flop))
699
3041
  fail("Patch feather must be nonnegative and flop must be 0 or 1.");
700
3042
  }
701
- const out = resolve4(o.out);
3043
+ const out = resolve5(o.out);
702
3044
  mkdirSync(out);
703
3045
  const layers = [];
704
3046
  for (const spec of o.patch) {
705
3047
  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();
3048
+ const patch = await sharp5(file).extract({ left: sx, top: sy, width: pw, height: ph }).flop(!!flop).toColourspace("srgb").removeAlpha().raw().toBuffer();
707
3049
  const rgba = Buffer.alloc(pw * ph * 4);
708
3050
  for (let y = 0; y < ph; y++)
709
3051
  for (let x = 0; x < pw; x++) {
@@ -715,10 +3057,10 @@ async function prepare(file, o) {
715
3057
  }
716
3058
  layers.push({ input: rgba, raw: { width: pw, height: ph, channels: 4 }, left: dx, top: dy });
717
3059
  }
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 });
3060
+ const patched = await sharp5(file).removeAlpha().composite(layers).png().toBuffer();
3061
+ const card = await sharp5(patched).extract({ left: cx, top: cy, width: cw, height: ch }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
720
3062
  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 });
3063
+ const src = await sharp5(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
722
3064
  const sp = (x, y) => {
723
3065
  const i = (y * src.info.width + x) * 3;
724
3066
  return [src.data[i], src.data[i + 1], src.data[i + 2]];
@@ -758,14 +3100,14 @@ async function prepare(file, o) {
758
3100
  if (y > 0) queue.push(i - CW);
759
3101
  if (y < CH - 1) queue.push(i + CW);
760
3102
  }
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();
3103
+ const cardPng = await sharp5(px, { raw: card.info }).png().toBuffer();
3104
+ const resized = sharp5(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
3105
+ await resized.clone().png().toFile(join5(out, "card.png"));
3106
+ await resized.clone().jpeg({ quality: 84, mozjpeg: true }).toFile(join5(out, "card.jpg"));
3107
+ const rgb2 = await resized.clone().toColourspace("srgb").raw().toBuffer();
766
3108
  const g = await resized.clone().toColourspace("b-w").raw().toBuffer();
767
- writeFileSync(join3(out, "rgb.raw"), rgb);
768
- writeFileSync(join3(out, "lum.raw"), g);
3109
+ writeFileSync(join5(out, "rgb.raw"), rgb2);
3110
+ writeFileSync(join5(out, "lum.raw"), g);
769
3111
  const GW = W * 2, GH = H * 2, big = Buffer.alloc(GW * GH);
770
3112
  for (let y = 0; y < GH; y++) {
771
3113
  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 +3119,14 @@ async function prepare(file, o) {
777
3119
  );
778
3120
  }
779
3121
  }
780
- writeFileSync(join3(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
3122
+ writeFileSync(join5(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
781
3123
  ${GW} ${GH}
782
3124
  255
783
3125
  `), big]));
784
3126
  const hex = paper.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
785
3127
  return {
786
3128
  out,
787
- preview: join3(out, "card.png"),
3129
+ preview: join5(out, "card.png"),
788
3130
  paper: `#${hex}`,
789
3131
  patches: o.patch.length,
790
3132
  filledEdgePixels: filled,
@@ -792,7 +3134,7 @@ ${GW} ${GH}
792
3134
  };
793
3135
  }
794
3136
  function potrace(input, extra) {
795
- const r = spawnSync(
3137
+ const r = spawnSync2(
796
3138
  "potrace",
797
3139
  [input, "-b", "svg", "-o", "-", "-W", `${W}pt`, "-H", `${H}pt`, ...extra],
798
3140
  { maxBuffer: 64 << 20 }
@@ -809,11 +3151,11 @@ function potrace(input, extra) {
809
3151
  }
810
3152
  var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
811
3153
  function vectorize(dir, o) {
812
- dir = resolve4(dir);
3154
+ dir = resolve5(dir);
813
3155
  if (!o.out || !o.title) fail("vectorize needs --out <file.svg> and --title");
814
3156
  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)
3157
+ const lum = readFileSync2(join5(dir, "lum.raw")), rgb2 = readFileSync2(join5(dir, "rgb.raw"));
3158
+ if (lum.length !== N || rgb2.length !== N * 3)
817
3159
  fail("Expected an intact directory created by image prepare.");
818
3160
  let c = [...Array(K)].map((_, i) => 30 + i * 200 / (K - 1));
819
3161
  const band = new Uint8Array(N);
@@ -851,9 +3193,9 @@ function vectorize(dir, o) {
851
3193
  let r = 0, g = 0, b = 0, n = 0;
852
3194
  for (let i = 0; i < N; i++)
853
3195
  if (band[i] === k) {
854
- r += rgb[i * 3];
855
- g += rgb[i * 3 + 1];
856
- b += rgb[i * 3 + 2];
3196
+ r += rgb2[i * 3];
3197
+ g += rgb2[i * 3 + 1];
3198
+ b += rgb2[i * 3 + 2];
857
3199
  n++;
858
3200
  }
859
3201
  const h = (v) => Math.round(v / Math.max(n, 1)).toString(16).padStart(2, "0");
@@ -862,11 +3204,11 @@ function vectorize(dir, o) {
862
3204
  const L = used.length;
863
3205
  const inkK = o.ink !== void 0 ? Number(o.ink) : L > 1 ? (c[used[0]] + c[used[1]]) / 2 / 255 : 0.25;
864
3206
  const groups = [];
865
- const maskFile = join3(dir, "mask.pbm");
3207
+ const maskFile = join5(dir, "mask.pbm");
866
3208
  for (let j = o.raster ? 0 : L - 2; j >= 0; j--) {
867
3209
  let traced;
868
3210
  if (j === 0) {
869
- traced = potrace(join3(dir, "ink.pgm"), [
3211
+ traced = potrace(join5(dir, "ink.pgm"), [
870
3212
  "-k",
871
3213
  inkK.toFixed(4),
872
3214
  "-t",
@@ -889,7 +3231,7 @@ ${W} ${H}
889
3231
  if (traced) groups.push(`<g transform="${traced.tr}" fill="${colors[j]}">${traced.paths}</g>`);
890
3232
  }
891
3233
  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]}"/>`;
3234
+ <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
3235
  const title = esc(o.title), desc = esc(o.desc ?? `${o.title} card artwork.`);
894
3236
  const meta = esc(o.credit ?? "Original artwork traced from a reference image.");
895
3237
  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 +3244,9 @@ ${groups.join("\n")}
902
3244
  `;
903
3245
  const checked = checkSubmittedSvg(svg);
904
3246
  if (!checked.ok) fail(checked.error);
905
- writeFileSync(resolve4(o.out), svg, { flag: "wx" });
3247
+ writeFileSync(resolve5(o.out), svg, { flag: "wx" });
906
3248
  return {
907
- out: resolve4(o.out),
3249
+ out: resolve5(o.out),
908
3250
  bytes: Buffer.byteLength(svg),
909
3251
  mode: o.raster ? "raster background + vector ink" : "pure vector",
910
3252
  tones: colors,
@@ -912,7 +3254,7 @@ ${groups.join("\n")}
912
3254
  };
913
3255
  }
914
3256
  async function rawImage(file) {
915
- const { data, info } = await sharp4(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
3257
+ const { data, info } = await sharp5(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
916
3258
  const px = (x, y) => {
917
3259
  if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= info.width || y >= info.height)
918
3260
  fail("Pixel coordinates are outside the image.");
@@ -1011,19 +3353,19 @@ async function glyphs(file, o) {
1011
3353
  if (!o.box || !o.at) fail("glyphs needs --box x,y,w,h and --at X,Y");
1012
3354
  const [bx, by, bw, bh] = nums(o.box, 4, "box");
1013
3355
  const [ax, ay] = nums(o.at, 2, "at");
1014
- const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
3356
+ const meta = await sharp5(file, { limitInputPixels: 24e6 }).metadata();
1015
3357
  validBox([bx, by, bw, bh], meta.width ?? 0, meta.height ?? 0);
1016
3358
  const up = Number(o.up ?? 4);
1017
3359
  if (bw * bh * up * up > 24e6)
1018
3360
  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");
3361
+ 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 });
3362
+ const temp = mkdtempSync(join5(tmpdir2(), "makeables-glyphs-"));
3363
+ const pgm = join5(temp, "glyphs.pgm");
1022
3364
  writeFileSync(pgm, Buffer.concat([Buffer.from(`P5
1023
3365
  ${info.width} ${info.height}
1024
3366
  255
1025
3367
  `), data]));
1026
- const r = spawnSync(
3368
+ const r = spawnSync2(
1027
3369
  "potrace",
1028
3370
  [pgm, "-b", "svg", "-o", "-", "-k", String(o.k ?? 0.5), "-t", "2", "-a", "1", "-O", "0.3"],
1029
3371
  { maxBuffer: 64 << 20 }
@@ -1037,18 +3379,18 @@ ${info.width} ${info.height}
1037
3379
  const paths = [...svg.matchAll(/ d="([^"]+)"/g)].map((m) => m[1].replace(/\s+/g, " ")).join(" ");
1038
3380
  if (!tr || !paths) fail("nothing dark enough to trace in that box; lower --k or check the box");
1039
3381
  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" });
3382
+ if (o.out) writeFileSync(resolve5(o.out), snippet, { flag: "wx" });
1041
3383
  return {
1042
3384
  box: { x: bx, y: by, w: bw, h: bh },
1043
3385
  at: { x: ax, y: ay },
1044
3386
  bytes: snippet.length,
1045
- out: o.out ? resolve4(o.out) : null,
3387
+ out: o.out ? resolve5(o.out) : null,
1046
3388
  snippet: o.out ? void 0 : snippet
1047
3389
  };
1048
3390
  }
1049
3391
  function rotate(file, o) {
1050
3392
  if (!o.out) fail("rotate needs --out <card.svg>");
1051
- const text = readFileSync(file, "utf8");
3393
+ const text = readFileSync2(file, "utf8");
1052
3394
  const parser = new DOMParser2({ onError: (_level, message) => fail(`Malformed SVG: ${message}`) });
1053
3395
  if (/<!DOCTYPE|<!ENTITY/i.test(text)) fail("SVG declarations are not supported.");
1054
3396
  const root = parser.parseFromString(text, "image/svg+xml").documentElement;
@@ -1064,28 +3406,28 @@ function rotate(file, o) {
1064
3406
  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
3407
  const checked = checkSubmittedSvg(svg);
1066
3408
  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) };
3409
+ writeFileSync(resolve5(o.out), svg, { flag: "wx" });
3410
+ return { out: resolve5(o.out), transform, bytes: Buffer.byteLength(svg) };
1069
3411
  }
1070
3412
  async function compare(file, ref, o) {
1071
3413
  if (!o.out) fail("compare needs --out <file.png>");
1072
- let card = sharp4(readFileSync(file));
3414
+ let card = sharp5(readFileSync2(file));
1073
3415
  if (o.upright) card = card.rotate(o.upright === "left" ? 90 : -90);
1074
3416
  const cardPng = await card.png().toBuffer();
1075
- let reference = sharp4(ref);
3417
+ let reference = sharp5(ref);
1076
3418
  if (o["ref-box"]) {
1077
3419
  const [x, y, w, h] = nums(o["ref-box"], 4, "ref-box");
1078
3420
  reference = reference.extract({ left: x, top: y, width: w, height: h });
1079
3421
  }
1080
3422
  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 });
3423
+ const a = await sharp5(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
3424
+ const b = await sharp5(await reference.png().toBuffer()).resize({ height }).png().toBuffer({ resolveWithObject: true });
1083
3425
  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([
3426
+ await sharp5({ create: { width, height: height + gap * 2, channels: 3, background: "#ffffff" } }).composite([
1085
3427
  { input: a.data, left: gap, top: gap },
1086
3428
  { 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" };
3429
+ ]).png().toFile(resolve5(o.out));
3430
+ return { out: resolve5(o.out), left: "card", right: "reference" };
1089
3431
  }
1090
3432
  var imageTools = [
1091
3433
  "detect",
@@ -1105,8 +3447,8 @@ async function referenceImage(command, file, options, out, reference) {
1105
3447
  parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
1106
3448
  );
1107
3449
  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.");
3450
+ if (out && existsSync2(resolve5(out))) fail("Output already exists. Choose a new --out path.");
3451
+ if (!existsSync2(file)) fail("Input file does not exist.");
1110
3452
  switch (command) {
1111
3453
  case "detect":
1112
3454
  return detect(file);
@@ -1134,10 +3476,10 @@ async function referenceImage(command, file, options, out, reference) {
1134
3476
  }
1135
3477
 
1136
3478
  // src/sticker-cut.ts
1137
- import sharp5 from "sharp";
3479
+ import sharp6 from "sharp";
1138
3480
  async function stickerPrintCut(design) {
1139
3481
  const { png } = await renderBadge(design, { face: "front", person: {} });
1140
- const { data, info } = await sharp5(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
3482
+ const { data, info } = await sharp6(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1141
3483
  const material = design.sticker ?? defaultStickerMaterial;
1142
3484
  const cut = stickerCut(
1143
3485
  new Uint8ClampedArray(data.buffer, data.byteOffset, data.length),
@@ -1150,9 +3492,9 @@ async function stickerPrintCut(design) {
1150
3492
  }
1151
3493
 
1152
3494
  // src/sticker-material.ts
1153
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
3495
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1154
3496
  import { basename as basename2 } from "path";
1155
- import sharp6 from "sharp";
3497
+ import sharp7 from "sharp";
1156
3498
  async function bindStickerMap(design, layerId, path) {
1157
3499
  if (design.product !== "sticker") throw new InputError("Optical maps require a sticker.");
1158
3500
  const layer = design.front.layers.find((layer2) => layer2.id === layerId);
@@ -1160,8 +3502,8 @@ async function bindStickerMap(design, layerId, path) {
1160
3502
  const info = await stat3(path);
1161
3503
  if (!info.isFile() || info.size > 4e6)
1162
3504
  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();
3505
+ const bytes = await readFile5(path);
3506
+ const meta = await sharp7(bytes, { limitInputPixels: 16777216 }).metadata();
1165
3507
  if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
1166
3508
  throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
1167
3509
  const asset = design.sticker?.effectMap?.asset ?? "optical-map";
@@ -1208,9 +3550,9 @@ function compactReceipt(value) {
1208
3550
  if (Array.isArray(value)) return value.map(compactReceipt);
1209
3551
  if (value && typeof value === "object")
1210
3552
  return Object.fromEntries(
1211
- Object.entries(value).map(([key, entry]) => [
1212
- key,
1213
- key === "design" && entry && typeof entry === "object" ? {
3553
+ Object.entries(value).map(([key2, entry]) => [
3554
+ key2,
3555
+ key2 === "design" && entry && typeof entry === "object" ? {
1214
3556
  name: "name" in entry ? entry.name : void 0,
1215
3557
  product: "product" in entry ? entry.product : "badge",
1216
3558
  omitted: true
@@ -1224,11 +3566,11 @@ function compactReceipt(value) {
1224
3566
  // src/wallet.ts
1225
3567
  import { spawn } from "child_process";
1226
3568
  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";
3569
+ import { existsSync as existsSync3 } from "fs";
3570
+ 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";
3571
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
3572
+ import { dirname as dirname4, extname as extname3, join as join9, resolve as resolve7 } from "path";
3573
+ import sharp8 from "sharp";
1232
3574
 
1233
3575
  // src/wallet-release.json
1234
3576
  var wallet_release_default = {
@@ -1250,16 +3592,16 @@ var wallet_release_default = {
1250
3592
  // src/wallet-storage.ts
1251
3593
  import { randomUUID as randomUUID2 } from "crypto";
1252
3594
  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";
3595
+ import { chmod, lstat, mkdir as mkdir3, open as open3, rename as rename3, rm as rm4 } from "fs/promises";
1254
3596
  import { homedir as homedir2 } from "os";
1255
- import { dirname as dirname3, join as join4, resolve as resolve5 } from "path";
3597
+ import { dirname as dirname3, join as join6, resolve as resolve6 } from "path";
1256
3598
  function walletHome() {
1257
- return resolve5(
1258
- process.env.MAKEABLES_WALLET_HOME ?? join4(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
3599
+ return resolve6(
3600
+ process.env.MAKEABLES_WALLET_HOME ?? join6(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1259
3601
  );
1260
3602
  }
1261
3603
  async function privateDirectory(path) {
1262
- const absolute = resolve5(path);
3604
+ const absolute = resolve6(path);
1263
3605
  const parent = dirname3(absolute);
1264
3606
  if (parent !== absolute) {
1265
3607
  try {
@@ -1289,7 +3631,7 @@ async function readPrivateJson(path) {
1289
3631
  }
1290
3632
  }
1291
3633
  async function writePrivateJson(path, value) {
1292
- const temporary = join4(dirname3(path), `.${randomUUID2()}.json`);
3634
+ const temporary = join6(dirname3(path), `.${randomUUID2()}.json`);
1293
3635
  try {
1294
3636
  const handle = await open3(temporary, "wx", 384);
1295
3637
  try {
@@ -1301,13 +3643,13 @@ async function writePrivateJson(path, value) {
1301
3643
  }
1302
3644
  await rename3(temporary, path);
1303
3645
  } finally {
1304
- await rm3(temporary, { force: true });
3646
+ await rm4(temporary, { force: true });
1305
3647
  }
1306
3648
  }
1307
3649
 
1308
3650
  // 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";
3651
+ import { mkdtemp as mkdtemp3, readFile as readFile6, rm as rm5, writeFile as writeFile3 } from "fs/promises";
3652
+ import { join as join7 } from "path";
1311
3653
  var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1312
3654
  function submissionId(input) {
1313
3655
  if (uuid.test(input)) return input.toLowerCase();
@@ -1355,16 +3697,16 @@ async function downloadWalletSubmission(input) {
1355
3697
  const pkg = await readPackageZip(bytes);
1356
3698
  if (pkg.manifest.product !== "card")
1357
3699
  throw new InputError("Wallet artwork requires a card submission.");
1358
- const directory = await mkdtemp2(
1359
- join5(
1360
- await privateDirectory(join5(await privateDirectory(walletHome()), "submissions")),
3700
+ const directory = await mkdtemp3(
3701
+ join7(
3702
+ await privateDirectory(join7(await privateDirectory(walletHome()), "submissions")),
1361
3703
  "download-"
1362
3704
  )
1363
3705
  );
1364
3706
  try {
1365
- const path = join5(directory, `${id}.makeables.zip`);
3707
+ const path = join7(directory, `${id}.makeables.zip`);
1366
3708
  await writeFile3(path, bytes, { mode: 384, flag: "wx" });
1367
- const saved = await readFile5(path);
3709
+ const saved = await readFile6(path);
1368
3710
  const sha256 = await hashBytes(saved);
1369
3711
  if (saved.length !== bytes.length || sha256 !== await hashBytes(bytes))
1370
3712
  throw new Error("Saved submission checksum does not match.");
@@ -1378,14 +3720,14 @@ async function downloadWalletSubmission(input) {
1378
3720
  elapsedMs: Math.round(performance.now() - started2)
1379
3721
  };
1380
3722
  } catch (error) {
1381
- await rm4(directory, { recursive: true, force: true });
3723
+ await rm5(directory, { recursive: true, force: true });
1382
3724
  throw error;
1383
3725
  }
1384
3726
  }
1385
3727
 
1386
3728
  // src/wallet-targets.ts
1387
3729
  import { lstat as lstat2 } from "fs/promises";
1388
- import { join as join6 } from "path";
3730
+ import { join as join8 } from "path";
1389
3731
  import lockfile from "proper-lockfile";
1390
3732
  import { z as z4 } from "zod";
1391
3733
  var alias = z4.string().regex(/^[a-z][a-z0-9-]{0,47}$/).refine((value) => !/\d{5}/.test(value));
@@ -1403,25 +3745,25 @@ var targetSchema = z4.object({
1403
3745
  createdAt: z4.string().datetime()
1404
3746
  }).strict();
1405
3747
  var registrySchema = z4.object({ version: z4.literal(1), targets: z4.array(targetSchema).max(100) }).strict();
1406
- function targetIdentity(values, key = "remember") {
3748
+ function targetIdentity(values, key2 = "remember") {
1407
3749
  const result = targetSchema.pick({ target: true, label: true, last4: true }).safeParse({
1408
- target: values[key],
3750
+ target: values[key2],
1409
3751
  label: values.label,
1410
3752
  last4: values.last4
1411
3753
  });
1412
3754
  if (!result.success)
1413
3755
  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.`
3756
+ `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
3757
  );
1416
3758
  return result.data;
1417
3759
  }
1418
3760
  function registryPath() {
1419
- return join6(walletHome(), "targets", "registry.json");
3761
+ return join8(walletHome(), "targets", "registry.json");
1420
3762
  }
1421
3763
  async function walletTargets() {
1422
3764
  const path = registryPath();
1423
3765
  try {
1424
- for (const parent of [walletHome(), join6(walletHome(), "targets")]) {
3766
+ for (const parent of [walletHome(), join8(walletHome(), "targets")]) {
1425
3767
  if (!(await lstat2(parent)).isDirectory())
1426
3768
  throw new InputError("Invalid Wallet target storage.");
1427
3769
  }
@@ -1444,7 +3786,7 @@ function targetChoices(targets) {
1444
3786
  }));
1445
3787
  }
1446
3788
  async function withWalletTargets(action) {
1447
- const directory = await privateDirectory(join6(await privateDirectory(walletHome()), "targets"));
3789
+ const directory = await privateDirectory(join8(await privateDirectory(walletHome()), "targets"));
1448
3790
  const release = await lockfile.lock(directory, { realpath: false, retries: 0 });
1449
3791
  try {
1450
3792
  return await action(await walletTargets(), async (targets) => {
@@ -1473,9 +3815,9 @@ var WalletError = class extends Error {
1473
3815
  };
1474
3816
  function installation() {
1475
3817
  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") };
3818
+ const cache = process.env.MAKEABLES_WALLET_CACHE ?? join9(homedir3(), "Library", "Caches", "Makeables", "wallet");
3819
+ const root = resolve7(cache, `${wallet_release_default.version}-${platform}`);
3820
+ return { platform, root, executable: join9(root, "makeables-wallet", "makeables-wallet") };
1479
3821
  }
1480
3822
  async function execute(command, args, progress) {
1481
3823
  return new Promise((accept, reject) => {
@@ -1542,7 +3884,7 @@ async function walletDoctor() {
1542
3884
  supported: false,
1543
3885
  reason: "Wallet artwork requires a Mac and an iPhone connected by USB."
1544
3886
  };
1545
- if (!existsSync2(installed))
3887
+ if (!existsSync3(installed))
1546
3888
  return {
1547
3889
  ready: false,
1548
3890
  supported: true,
@@ -1572,7 +3914,7 @@ async function setupWallet(progress) {
1572
3914
  return info;
1573
3915
  }
1574
3916
  const { platform, root, executable } = installation();
1575
- if (existsSync2(executable)) {
3917
+ if (existsSync3(executable)) {
1576
3918
  const info = await walletDoctor();
1577
3919
  if (!info.ready)
1578
3920
  throw new InputError("The installed Wallet companion is incomplete or incompatible.");
@@ -1607,9 +3949,9 @@ async function setupWallet(progress) {
1607
3949
  if (createHash3("sha256").update(bytes).digest("hex") !== artifact.sha256)
1608
3950
  throw new Error("Wallet companion checksum did not match. Nothing was installed.");
1609
3951
  await mkdir4(dirname4(root), { recursive: true, mode: 448 });
1610
- const stage = await mkdtemp3(join7(dirname4(root), ".install-"));
3952
+ const stage = await mkdtemp4(join9(dirname4(root), ".install-"));
1611
3953
  try {
1612
- const archive = join7(stage, "companion.tar.gz");
3954
+ const archive = join9(stage, "companion.tar.gz");
1613
3955
  await writeFile4(archive, bytes, { mode: 384, flag: "wx" });
1614
3956
  const members = (await execute("tar", ["-tzf", archive])).trim().split("\n");
1615
3957
  if (members.some(
@@ -1617,19 +3959,19 @@ async function setupWallet(progress) {
1617
3959
  ))
1618
3960
  throw new Error("Unexpected companion archive contents.");
1619
3961
  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"]);
3962
+ await rm6(archive);
3963
+ await chmod2(join9(stage, "makeables-wallet", "makeables-wallet"), 448);
3964
+ const info = await invoke(join9(stage, "makeables-wallet", "makeables-wallet"), ["version"]);
1623
3965
  if (info.version !== wallet_release_default.version || info.protocol !== wallet_release_default.protocol)
1624
3966
  throw new Error("Companion version does not match the installed CLI.");
1625
3967
  await rename4(stage, root);
1626
3968
  } finally {
1627
- await rm5(stage, { recursive: true, force: true });
3969
+ await rm6(stage, { recursive: true, force: true });
1628
3970
  }
1629
3971
  return walletDoctor();
1630
3972
  }
1631
3973
  async function walletArtwork(file, side2 = "front") {
1632
- const input = resolve6(file);
3974
+ const input = resolve7(file);
1633
3975
  const info = await lstat3(input);
1634
3976
  if (!info.isFile() || info.size > 336e5)
1635
3977
  throw new InputError("Expected artwork or a complete Makeables package below 32 MB.");
@@ -1642,16 +3984,16 @@ async function walletArtwork(file, side2 = "front") {
1642
3984
  if (!path) throw new InputError("This package has no back artwork.");
1643
3985
  source = Buffer.from(pkg.files[path]);
1644
3986
  } else {
1645
- source = await readFile6(input);
3987
+ source = await readFile7(input);
1646
3988
  if (extname3(input).toLowerCase() === ".svg") {
1647
3989
  const result = checkSubmittedSvg(source.toString("utf8"));
1648
3990
  if (!result.ok) throw new InputError(result.error);
1649
3991
  }
1650
3992
  }
1651
- return sharp7(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
3993
+ return sharp8(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
1652
3994
  }
1653
3995
  async function preparedArtwork(directory) {
1654
- const path = resolve6(directory);
3996
+ const path = resolve7(directory);
1655
3997
  if (!(await lstat3(path)).isDirectory())
1656
3998
  throw new InputError("--assets must be a prepared artwork directory.");
1657
3999
  for (const name of [
@@ -1659,15 +4001,15 @@ async function preparedArtwork(directory) {
1659
4001
  "cardBackgroundCombined@2x.png",
1660
4002
  "cardBackgroundCombined.pdf"
1661
4003
  ]) {
1662
- const file = join7(path, name);
4004
+ const file = join9(path, name);
1663
4005
  const info = await lstat3(file);
1664
4006
  if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1665
4007
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1666
4008
  const signature = Buffer.from(name.endsWith(".pdf") ? "%PDF-" : "\x89PNG\r\n\n", "latin1");
1667
- if (!(await readFile6(file)).subarray(0, signature.length).equals(signature))
4009
+ if (!(await readFile7(file)).subarray(0, signature.length).equals(signature))
1668
4010
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1669
4011
  }
1670
- return { path, preview: join7(path, "cardBackgroundCombined@3x.png") };
4012
+ return { path, preview: join9(path, "cardBackgroundCombined@3x.png") };
1671
4013
  }
1672
4014
  async function walletCommand(command, values, progress) {
1673
4015
  if (values.remember) {
@@ -1877,7 +4219,7 @@ async function runWalletCommand(command, values, progress) {
1877
4219
  }
1878
4220
  }
1879
4221
  if (command === "wallet prep" || applying || installing) {
1880
- const work = await mkdtemp3(join7(tmpdir2(), "makeables-wallet-"));
4222
+ const work = await mkdtemp4(join9(tmpdir3(), "makeables-wallet-"));
1881
4223
  let phase = "prepare";
1882
4224
  let deviceWriteAttempted = false;
1883
4225
  let persistent;
@@ -1885,23 +4227,23 @@ async function runWalletCommand(command, values, progress) {
1885
4227
  try {
1886
4228
  let target;
1887
4229
  if (existing) {
1888
- target = resolve6(need("assets"));
4230
+ target = resolve7(need("assets"));
1889
4231
  prepared = existing;
1890
4232
  } else {
1891
4233
  report("prepare", "Preparing the complete card artwork.");
1892
4234
  const png = await walletArtwork(need("file"), values.side === "back" ? "back" : "front");
1893
- const raster = join7(work, "artwork.png");
4235
+ const raster = join9(work, "artwork.png");
1894
4236
  await writeFile4(raster, png, { mode: 384 });
1895
4237
  if (command === "wallet prep") {
1896
- target = resolve6(need("out"));
4238
+ target = resolve7(need("out"));
1897
4239
  } else if (installing && !applying) {
1898
4240
  const root = await privateDirectory(
1899
- join7(await privateDirectory(walletHome()), "prepared")
4241
+ join9(await privateDirectory(walletHome()), "prepared")
1900
4242
  );
1901
- persistent = await mkdtemp3(join7(root, "install-"));
1902
- target = join7(persistent, "assets");
4243
+ persistent = await mkdtemp4(join9(root, "install-"));
4244
+ target = join9(persistent, "assets");
1903
4245
  } else {
1904
- target = join7(work, "assets");
4246
+ target = join9(work, "assets");
1905
4247
  }
1906
4248
  prepared = await invoke(
1907
4249
  executable,
@@ -1987,8 +4329,8 @@ async function runWalletCommand(command, values, progress) {
1987
4329
  }
1988
4330
  throw error;
1989
4331
  } finally {
1990
- await rm5(work, { recursive: true, force: true });
1991
- if (persistent && phase === "prepare") await rm5(persistent, { recursive: true, force: true });
4332
+ await rm6(work, { recursive: true, force: true });
4333
+ if (persistent && phase === "prepare") await rm6(persistent, { recursive: true, force: true });
1992
4334
  }
1993
4335
  }
1994
4336
  throw new InputError("Unknown Wallet command.");
@@ -2039,6 +4381,16 @@ function walletSummary(result) {
2039
4381
 
2040
4382
  // src/cli.ts
2041
4383
  var commands = [
4384
+ "preflight [--probe]",
4385
+ "badge package --file <badge.json> --photo <portrait> --name <name> --out <badge.makeables.json> [--role] [--organization] [--number] [--event] [--url <https://...>]",
4386
+ "brand new --name <brand> --out <brand.json> [--slug <slug>]",
4387
+ "brand from-url --url <https://event-or-site> (--out <new-directory> | --file <brand.json>) [--name <brand>] [--slug <slug>]",
4388
+ "brand add-kit --file <brand.json> --ref <kit.json|kit.makeables.zip> --id <kit-id> [--family <family-id>]",
4389
+ "brand add-logo --file <brand.json> --ref <logo.svg|logo.png> --id <logo-id> [--name <label>]",
4390
+ "brand inspect --file <brand.json>",
4391
+ "brand export --file <brand.json> --out <brand.makeables.zip> [--overwrite]",
4392
+ "brand preview --file <brand.json> [--site <origin>] [--no-open]",
4393
+ "kit check --file <kit.json> --brand <brand.json>",
2042
4394
  "kit new --name <title> --out <kit.json>",
2043
4395
  "kit extract --file <artwork-sheet.png> --out <new-directory> [--params <json|@file>]",
2044
4396
  "kit add --file <kit.json> --ref <design|package> --id <piece-id>",
@@ -2048,13 +4400,14 @@ var commands = [
2048
4400
  "kit export --file <kit.json|kit.makeables.zip> --out <kit.makeables.zip> [--overwrite]",
2049
4401
  "kit import --file <kit.makeables.zip> --out <new-directory>",
2050
4402
  "designs list",
2051
- "new card --design <slug> | --style <id> --out <new-file> [--dry-run]",
4403
+ "new card --design <slug> | --style <id> | --blank [--name <title>] --out <new-file> [--dry-run]",
2052
4404
  "material set --file <design.json> --params <json|@file>",
2053
4405
  "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]",
4406
+ "new badge (--style <id> | --blank [--name <title>]) --out <new-file> [--dry-run]",
4407
+ "new sticker (--style <id> | --blank [--name <title>]) --out <new-file> [--dry-run]",
2056
4408
  "layer list --file <design.json>",
2057
4409
  "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back] [--relative-to document|cwd]",
4410
+ "layer add --file <design.json> --kind text|shape|graphic|qr [--params <json|@file>] [--side front|back]",
2058
4411
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
2059
4412
  "layer remove --file <design.json> --id <layer> [--side front|back]",
2060
4413
  "cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
@@ -2063,6 +4416,17 @@ var commands = [
2063
4416
  "sheet pack --file <sheet.json>",
2064
4417
  "sheet check --file <sheet.json>",
2065
4418
  "sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
4419
+ "brick part --id <ldraw-id> [--library <ldraw-dir>]",
4420
+ "brick check --file <model.ldr> [--library <ldraw-dir>]",
4421
+ "brick plan --file <model.ldr> [--library <ldraw-dir>]",
4422
+ "brick stress --file <model.ldr> [--library <ldraw-dir>]",
4423
+ "brick remix --file <exemplar.ldr> --seed <n> --out <new.ldr> [--mode strict|family|free] [--library <ldraw-dir>]",
4424
+ "brick stl --file <model.ldr> --out <file.stl> [--scale <factor>] [--library <ldraw-dir>] [--overwrite]",
4425
+ "print list",
4426
+ "print <object> [--param <name=value>]... [--brand <slug|brand.json>] --out <file.stl|file.3mf> [--colors <body,label>] [--preview <views.png>] [--overwrite]",
4427
+ "grid bin --width <cells> --depth <cells> --height <units> --out <file.stl> [--overwrite]",
4428
+ "grid plate --width <cells> --depth <cells> --out <file.stl> [--overwrite]",
4429
+ "brick render --file <model.ldr> --out <png> [--step <n>] [--explode <factor>] [--yaw=<deg>] [--pitch=<deg>] [--library <ldraw-dir>] [--overwrite]",
2066
4430
  "render --file <design.json|design.makeables.json> --out <png> [--appearance flat|material] [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
2067
4431
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
2068
4432
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
@@ -2127,7 +4491,7 @@ function emit(data, next, human, json) {
2127
4491
  );
2128
4492
  }
2129
4493
  async function newPath(file) {
2130
- const path = resolve7(file);
4494
+ const path = resolve8(file);
2131
4495
  try {
2132
4496
  await lstat4(path);
2133
4497
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
@@ -2138,7 +4502,10 @@ async function newPath(file) {
2138
4502
  return path;
2139
4503
  }
2140
4504
  async function run() {
2141
- const { values, positionals } = parseArgs({
4505
+ const {
4506
+ values: { param: printParams, ...values },
4507
+ positionals
4508
+ } = parseArgs({
2142
4509
  allowPositionals: true,
2143
4510
  strict: true,
2144
4511
  options: {
@@ -2162,6 +4529,7 @@ async function run() {
2162
4529
  port: { type: "string" },
2163
4530
  url: { type: "string" },
2164
4531
  params: { type: "string" },
4532
+ param: { type: "string", multiple: true },
2165
4533
  state: { type: "string" },
2166
4534
  target: { type: "string" },
2167
4535
  yes: { type: "boolean" },
@@ -2193,8 +4561,28 @@ async function run() {
2193
4561
  size: { type: "string" },
2194
4562
  "width-mm": { type: "string" },
2195
4563
  preview: { type: "string" },
4564
+ library: { type: "string" },
4565
+ step: { type: "string" },
4566
+ explode: { type: "string" },
4567
+ yaw: { type: "string" },
4568
+ pitch: { type: "string" },
4569
+ seed: { type: "string" },
4570
+ scale: { type: "string" },
4571
+ width: { type: "string" },
4572
+ depth: { type: "string" },
4573
+ height: { type: "string" },
4574
+ mode: { type: "string" },
2196
4575
  appearance: { type: "string" },
2197
4576
  "relative-to": { type: "string" },
4577
+ slug: { type: "string" },
4578
+ family: { type: "string" },
4579
+ brand: { type: "string" },
4580
+ colors: { type: "string" },
4581
+ probe: { type: "boolean" },
4582
+ blank: { type: "boolean" },
4583
+ kind: { type: "string" },
4584
+ organization: { type: "string" },
4585
+ event: { type: "string" },
2198
4586
  compact: { type: "boolean" },
2199
4587
  trace: { type: "string" }
2200
4588
  }
@@ -2228,12 +4616,33 @@ From a built checkout: npm run studio -- <command>`,
2228
4616
  );
2229
4617
  return;
2230
4618
  }
2231
- const command = ["skills get", "studio call"].includes(positionals.slice(0, 2).join(" ")) ? positionals.slice(0, 2).join(" ") : positionals.join(" ");
4619
+ const printTarget = positionals[0] === "print" && positionals.length === 2 && positionals[1] !== "list" ? positionals[1] : void 0;
4620
+ const command = printTarget ? "print <object>" : ["skills get", "studio call"].includes(positionals.slice(0, 2).join(" ")) ? positionals.slice(0, 2).join(" ") : positionals.join(" ");
2232
4621
  const allowed = {
2233
4622
  "kit new": ["name", "out"],
2234
4623
  "kit extract": ["file", "out", "params"],
2235
4624
  "kit add": ["file", "ref", "id"],
2236
4625
  "kit sheet": ["file", "ref", "id"],
4626
+ "kit check": ["file", "brand"],
4627
+ preflight: ["probe"],
4628
+ "badge package": [
4629
+ "file",
4630
+ "photo",
4631
+ "name",
4632
+ "role",
4633
+ "organization",
4634
+ "number",
4635
+ "event",
4636
+ "url",
4637
+ "out"
4638
+ ],
4639
+ "brand new": ["name", "slug", "out"],
4640
+ "brand from-url": ["url", "out", "file", "name", "slug"],
4641
+ "brand add-kit": ["file", "ref", "id", "family"],
4642
+ "brand add-logo": ["file", "ref", "id", "name"],
4643
+ "brand inspect": ["file"],
4644
+ "brand export": ["file", "out", "overwrite"],
4645
+ "brand preview": ["file", "site", "no-open"],
2237
4646
  "kit inspect": ["file"],
2238
4647
  "kit render": ["file", "out", "overwrite"],
2239
4648
  "kit export": ["file", "out", "overwrite"],
@@ -2310,13 +4719,13 @@ From a built checkout: npm run studio -- <command>`,
2310
4719
  "design create": ["style", "out", "dry-run"],
2311
4720
  "design validate": ["file"],
2312
4721
  "designs list": [],
2313
- "new card": ["design", "style", "file", "name", "out", "dry-run"],
2314
- "new sticker": ["style", "out", "dry-run"],
4722
+ "new card": ["design", "style", "file", "blank", "name", "out", "dry-run"],
4723
+ "new sticker": ["style", "blank", "name", "out", "dry-run"],
2315
4724
  "material set": ["file", "params"],
2316
4725
  "material map": ["file", "id", "ref"],
2317
- "new badge": ["style", "out", "dry-run"],
4726
+ "new badge": ["style", "blank", "name", "out", "dry-run"],
2318
4727
  "layer list": ["file"],
2319
- "layer add": ["file", "sticker", "side", "relative-to"],
4728
+ "layer add": ["file", "sticker", "kind", "params", "side", "relative-to"],
2320
4729
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
2321
4730
  "layer remove": ["file", "id", "side"],
2322
4731
  cut: ["file", "out", "overwrite"],
@@ -2325,6 +4734,17 @@ From a built checkout: npm run studio -- <command>`,
2325
4734
  "sheet pack": ["file"],
2326
4735
  "sheet check": ["file"],
2327
4736
  "sheet export": ["file", "out", "preview", "overwrite"],
4737
+ "brick part": ["id", "library"],
4738
+ "brick check": ["file", "library"],
4739
+ "brick plan": ["file", "library"],
4740
+ "brick stress": ["file", "library"],
4741
+ "brick render": ["file", "out", "step", "explode", "yaw", "pitch", "library", "overwrite"],
4742
+ "brick remix": ["file", "seed", "out", "mode", "library"],
4743
+ "brick stl": ["file", "out", "scale", "library", "overwrite"],
4744
+ "print list": [],
4745
+ "print <object>": ["out", "brand", "colors", "preview", "overwrite"],
4746
+ "grid bin": ["width", "depth", "height", "out", "overwrite"],
4747
+ "grid plate": ["width", "depth", "out", "overwrite"],
2328
4748
  render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number", "appearance"],
2329
4749
  validate: ["file", "out"]
2330
4750
  };
@@ -2333,16 +4753,162 @@ From a built checkout: npm run studio -- <command>`,
2333
4753
  for (const flag of Object.keys(values))
2334
4754
  if (!["json", "help", "version", "compact", "trace", ...allowed[command]].includes(flag))
2335
4755
  throw new InputError(`--${flag} is not supported by ${command}`);
4756
+ if (printParams && command !== "print <object>")
4757
+ throw new InputError(`--param is not supported by ${command}`);
2336
4758
  const progress = (data) => process.stderr.write(`${JSON.stringify(data)}
2337
4759
  `);
2338
4760
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
2339
4761
  throw new InputError("--product must be card, badge or sticker.");
4762
+ if (command === "badge package") {
4763
+ const need = (value, flag) => {
4764
+ if (!value) throw new InputError(`Supply --${flag}.`);
4765
+ return value;
4766
+ };
4767
+ const out = await newPath(need(values.out, "out"));
4768
+ if (!out.endsWith(".makeables.json"))
4769
+ throw new InputError("--out must end in .makeables.json.");
4770
+ const { badgePackage } = await import("./badge-package-RBQ43DQX.js");
4771
+ const { json: body, participant } = await badgePackage(
4772
+ need(values.file, "file"),
4773
+ need(values.photo, "photo"),
4774
+ {
4775
+ name: need(values.name, "name"),
4776
+ role: values.role,
4777
+ organization: values.organization,
4778
+ number: values.number ? Number(values.number) : void 0,
4779
+ event: values.event,
4780
+ url: values.url
4781
+ }
4782
+ );
4783
+ await writeAtomic(out, JSON.stringify(body), true);
4784
+ emit(
4785
+ {
4786
+ path: out,
4787
+ participant: {
4788
+ name: participant.name,
4789
+ role: participant.role,
4790
+ publicUrl: participant.publicUrl
4791
+ }
4792
+ },
4793
+ [`makeables kit add --file <kit.json> --ref ${JSON.stringify(out)} --id badge`],
4794
+ `Badge package ready: ${out}`,
4795
+ json
4796
+ );
4797
+ return;
4798
+ }
4799
+ if (command === "preflight") {
4800
+ const result2 = await preflight({ probe: Boolean(values.probe) });
4801
+ emit(result2, ["makeables skills get kits"], preflightSummary(result2), json);
4802
+ return;
4803
+ }
4804
+ if (command.startsWith("brand ")) {
4805
+ const need = (value, flag) => {
4806
+ if (!value) throw new InputError(`Supply --${flag}.`);
4807
+ return value;
4808
+ };
4809
+ let result2;
4810
+ let human = "Brand ready.";
4811
+ if (command === "brand new") {
4812
+ const path2 = await newPath(need(values.out, "out"));
4813
+ const brand = newBrand(need(values.name, "name"), values.slug);
4814
+ await writeBrand(path2, brand);
4815
+ result2 = { path: path2, brand };
4816
+ } else if (command === "brand from-url") {
4817
+ const out = resolve8(values.file ? dirname5(values.file) : need(values.out, "out"));
4818
+ if (!values.file)
4819
+ try {
4820
+ await lstat4(out);
4821
+ throw new InputError(`Directory already exists: ${out}. Choose a new --out path.`);
4822
+ } catch (error) {
4823
+ if (error.code !== "ENOENT") throw error;
4824
+ }
4825
+ result2 = await brandFromUrl(need(values.url, "url"), out, {
4826
+ name: values.name,
4827
+ slug: values.slug,
4828
+ into: values.file
4829
+ });
4830
+ human = `Brand refs ready. Open ${result2.proof} and confirm the assets with the user.`;
4831
+ } else if (command === "brand add-kit") {
4832
+ const path2 = need(values.file, "file");
4833
+ const brand = await addBrandKit(
4834
+ path2,
4835
+ need(values.ref, "ref"),
4836
+ need(values.id, "id"),
4837
+ values.family
4838
+ );
4839
+ await writeBrand(path2, brand);
4840
+ result2 = { path: resolve8(path2), kits: brand.kits.length };
4841
+ } else if (command === "brand add-logo") {
4842
+ const path2 = need(values.file, "file");
4843
+ const brand = await addBrandLogo(
4844
+ path2,
4845
+ need(values.ref, "ref"),
4846
+ need(values.id, "id"),
4847
+ values.name
4848
+ );
4849
+ await writeBrand(path2, brand);
4850
+ result2 = { path: resolve8(path2), logos: brand.logos.map((logo) => logo.id) };
4851
+ } else if (command === "brand inspect") {
4852
+ const brand = await readBrand(need(values.file, "file"));
4853
+ const view = await brandView(need(values.file, "file"));
4854
+ result2 = {
4855
+ brand,
4856
+ pieces: view.pieces.length,
4857
+ products: Object.fromEntries(
4858
+ ["card", "badge", "sticker"].map((product) => [
4859
+ product,
4860
+ view.pieces.filter((piece) => piece.product === product).length
4861
+ ])
4862
+ ),
4863
+ unsubmitted: brand.kits.filter((kit) => !kit.submission).map((kit) => kit.id)
4864
+ };
4865
+ } else if (command === "brand export") {
4866
+ const out = resolve8(need(values.out, "out"));
4867
+ if (!out.endsWith(".zip")) throw new InputError("--out must end in .zip.");
4868
+ const { packageZip } = await import("./submission-H3RXUH7T.js");
4869
+ const pkg = await prepareBrand(need(values.file, "file"));
4870
+ const bytes = await packageZip(pkg);
4871
+ const handle2 = await open4(
4872
+ values.overwrite ? out : await newPath(out),
4873
+ values.overwrite ? "w" : "wx"
4874
+ );
4875
+ try {
4876
+ await handle2.writeFile(bytes);
4877
+ } finally {
4878
+ await handle2.close();
4879
+ }
4880
+ result2 = {
4881
+ path: out,
4882
+ product: "brand",
4883
+ kits: pkg.manifest.product === "brand" ? pkg.manifest.kits.length : 0,
4884
+ bytes: bytes.length
4885
+ };
4886
+ } else {
4887
+ const { serveBrandPreview } = await import("./brand-preview-C6HBXUXH.js");
4888
+ result2 = await serveBrandPreview(need(values.file, "file"), {
4889
+ site: values.site,
4890
+ open: !values["no-open"]
4891
+ });
4892
+ human = `Brand preview at ${result2.url}. Press Ctrl+C to stop.`;
4893
+ }
4894
+ emit(result2, ["makeables skills get kits"], human, json);
4895
+ return;
4896
+ }
2340
4897
  if (command.startsWith("kit ")) {
2341
4898
  const required = (value, flag) => {
2342
4899
  if (!value) throw new InputError(`Supply --${flag}.`);
2343
4900
  return value;
2344
4901
  };
2345
4902
  let result2;
4903
+ if (command === "kit check") {
4904
+ result2 = await checkKitAgainstBrand(
4905
+ required(values.file, "file"),
4906
+ required(values.brand, "brand")
4907
+ );
4908
+ emit(result2, ["makeables skills get kits"], JSON.stringify(result2, null, 2), json);
4909
+ if (!result2.ok) process.exitCode = 2;
4910
+ return;
4911
+ }
2346
4912
  if (command === "kit extract") {
2347
4913
  result2 = await extractKit(
2348
4914
  required(values.file, "file"),
@@ -2369,11 +4935,11 @@ From a built checkout: npm run studio -- <command>`,
2369
4935
  command === "kit add" ? "items" : "sheets"
2370
4936
  );
2371
4937
  await writeAtomic(path2, JSON.stringify(kit, null, 2));
2372
- result2 = { path: resolve7(path2), items: kit.items.length, sheets: kit.sheets.length };
4938
+ result2 = { path: resolve8(path2), items: kit.items.length, sheets: kit.sheets.length };
2373
4939
  } else if (command === "kit import") {
2374
4940
  result2 = await importKit(required(values.file, "file"), required(values.out, "out"));
2375
4941
  } else if (command === "kit inspect" && !required(values.file, "file").toLowerCase().endsWith(".zip")) {
2376
- const value = JSON.parse(await readFile7(required(values.file, "file"), "utf8"));
4942
+ const value = JSON.parse(await readFile8(required(values.file, "file"), "utf8"));
2377
4943
  if (value.format === "makeables-kit") result2 = await readKit(required(values.file, "file"));
2378
4944
  else {
2379
4945
  const pkg = await readSubmission(required(values.file, "file"));
@@ -2385,7 +4951,7 @@ From a built checkout: npm run studio -- <command>`,
2385
4951
  const { pkg, bytes } = await exportKit(required(values.file, "file"));
2386
4952
  if (command === "kit inspect") result2 = pkg.manifest;
2387
4953
  else {
2388
- const out = resolve7(required(values.out, "out"));
4954
+ const out = resolve8(required(values.out, "out"));
2389
4955
  const rendering = command === "kit render";
2390
4956
  if (!out.endsWith(rendering ? ".png" : ".zip"))
2391
4957
  throw new InputError(rendering ? "--out must end in .png." : "--out must end in .zip.");
@@ -2434,6 +5000,74 @@ From a built checkout: npm run studio -- <command>`,
2434
5000
  emit(result2, ["makeables skills get cards"], JSON.stringify(result2, null, 2), json);
2435
5001
  return;
2436
5002
  }
5003
+ if (command === "print list") {
5004
+ const { printList } = await import("./print-NGPUZPVN.js");
5005
+ const objects = printList();
5006
+ emit(
5007
+ { objects },
5008
+ ["Build one with makeables print <object> --param name=value --out <file.stl>."],
5009
+ objects.map(
5010
+ (o) => `${style(o.id, "1", !json)} ${o.description}
5011
+ ${[
5012
+ ...o.params.map(
5013
+ (p) => ` --param ${p.name}=${p.options ? p.options.join("|") : p.type === "number" ? `${p.min}..${p.max}` : "<text>"} (default ${JSON.stringify(p.default)})`
5014
+ ),
5015
+ ...Object.entries(o.assets).map(
5016
+ ([slot, a]) => ` --param ${slot}=<file.${a.kind}> ${a.description}`
5017
+ )
5018
+ ].join("\n")}`
5019
+ ).join("\n"),
5020
+ json
5021
+ );
5022
+ return;
5023
+ }
5024
+ if (command === "print <object>" && printTarget) {
5025
+ if (!values.out) throw new InputError("Supply --out.");
5026
+ const out = resolve8(values.out);
5027
+ const preview = values.preview ? resolve8(values.preview) : void 0;
5028
+ const { printObject, printPreview } = await import("./print-NGPUZPVN.js");
5029
+ const multicolor = out.toLowerCase().endsWith(".3mf");
5030
+ if (!multicolor && !out.toLowerCase().endsWith(".stl"))
5031
+ throw new InputError("--out must end in .stl, or .3mf for two colours.");
5032
+ if (values.colors && !multicolor)
5033
+ throw new InputError("--colors needs a .3mf --out; an STL has one colour.");
5034
+ const result2 = await printObject(printTarget, printParams ?? [], values.brand, {
5035
+ multicolor,
5036
+ colors: values.colors
5037
+ });
5038
+ const files = [[out, result2.threeMf ?? result2.stl]];
5039
+ if (preview) files.push([preview, await printPreview(result2)]);
5040
+ for (const [path2, bytes] of files) {
5041
+ const handle2 = await open4(path2, values.overwrite ? "w" : "wx", 420);
5042
+ try {
5043
+ await handle2.writeFile(bytes);
5044
+ } finally {
5045
+ await handle2.close();
5046
+ }
5047
+ }
5048
+ const { x, y, z: z5 } = result2.sizeMm;
5049
+ emit(
5050
+ {
5051
+ out,
5052
+ object: result2.object,
5053
+ brand: result2.brand,
5054
+ params: result2.params,
5055
+ triangles: result2.triangles,
5056
+ sizeMm: result2.sizeMm,
5057
+ check: result2.check,
5058
+ printability: result2.printability,
5059
+ ...result2.colors && { colors: result2.colors },
5060
+ ...preview && { preview }
5061
+ },
5062
+ [
5063
+ preview ? `Look at ${preview} (top, three-quarter, front) before printing.` : `Add --preview <views.png> to look at it before printing.`,
5064
+ `Open ${out} in your slicer; it is one closed solid with no edges to repair.`
5065
+ ],
5066
+ `${style("Written", "1", !json)} ${result2.name}, ${x} x ${y} x ${z5} mm, ${out}`,
5067
+ json
5068
+ );
5069
+ return;
5070
+ }
2437
5071
  const card = await cardCommand(command, values, json);
2438
5072
  if (card) return;
2439
5073
  if (command === "login" || command === "logout") {
@@ -2468,7 +5102,12 @@ From a built checkout: npm run studio -- <command>`,
2468
5102
  throw new InputError(
2469
5103
  "Supply --file with editable JSON, an original SVG or a Makeables package."
2470
5104
  );
2471
- const result2 = await submitDesign({
5105
+ const isBrand = values.file.endsWith(".json") && await readFile8(values.file, "utf8").then(
5106
+ (text) => text.includes('"makeables-brand"'),
5107
+ () => false
5108
+ );
5109
+ const submit = isBrand ? (await import("./brand-UNVFUN4H.js")).submitBrand : submitDesign;
5110
+ const result2 = await submit({
2472
5111
  title: values.title,
2473
5112
  credit: values.credit,
2474
5113
  creditUrl: values["credit-url"],
@@ -2518,7 +5157,7 @@ From a built checkout: npm run studio -- <command>`,
2518
5157
  }
2519
5158
  if (command === "doctor") {
2520
5159
  const check = (command2) => {
2521
- const result2 = spawnSync2(command2, ["--version"], {
5160
+ const result2 = spawnSync3(command2, ["--version"], {
2522
5161
  encoding: "utf8",
2523
5162
  timeout: 5e3,
2524
5163
  stdio: ["ignore", "pipe", "pipe"]
@@ -2574,7 +5213,7 @@ Run makeables skills get core for setup.`,
2574
5213
  }
2575
5214
  if (command === "mcp") {
2576
5215
  try {
2577
- const { runMcp } = await import("./mcp-VS5XYXVM.js");
5216
+ const { runMcp } = await import("./mcp-2VMGYAUZ.js");
2578
5217
  await runMcp(values.root ?? process.cwd());
2579
5218
  } catch (error) {
2580
5219
  process.stderr.write(
@@ -2633,7 +5272,7 @@ Keep this process running while you design.`,
2633
5272
  const info = await lstat4(path2);
2634
5273
  if (!info.isFile() || info.size > 12e6)
2635
5274
  throw new InputError("Expected a params file below 12 MB.");
2636
- params = JSON.parse(await readFile7(path2, "utf8"));
5275
+ params = JSON.parse(await readFile8(path2, "utf8"));
2637
5276
  } else if (values.params) params = JSON.parse(values.params);
2638
5277
  }
2639
5278
  const result2 = await studioRequest(
@@ -2739,14 +5378,14 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
2739
5378
  return;
2740
5379
  }
2741
5380
  if (!values.file) throw new InputError("Supply --file to validate a document.");
2742
- const path = resolve7(values.file);
5381
+ const path = resolve8(values.file);
2743
5382
  const handle = await open4(path, "r");
2744
5383
  let input;
2745
5384
  try {
2746
5385
  const info = await handle.stat();
2747
5386
  if (!info.isFile() || info.size > 2 * 1024 * 1024)
2748
5387
  throw new InputError("Expected a JSON file smaller than 2 MB.");
2749
- input = JSON.parse(await readFile7(handle, "utf8"));
5388
+ input = JSON.parse(await readFile8(handle, "utf8"));
2750
5389
  } finally {
2751
5390
  await handle.close();
2752
5391
  }
@@ -2765,7 +5404,7 @@ Schema and layout constraints passed.`,
2765
5404
  }
2766
5405
  async function readParams(value) {
2767
5406
  return JSON.parse(
2768
- value.startsWith("@") ? await readFile7(resolve7(value.slice(1)), "utf8") : value
5407
+ value.startsWith("@") ? await readFile8(resolve8(value.slice(1)), "utf8") : value
2769
5408
  );
2770
5409
  }
2771
5410
  async function cardCommand(command, values, json) {
@@ -2776,12 +5415,26 @@ async function cardCommand(command, values, json) {
2776
5415
  };
2777
5416
  const renderHint = (path, face, design) => `npx makeables render --file ${JSON.stringify(path)} --side ${face} --out ${design.product ?? "badge"}.png --overwrite`;
2778
5417
  if (command === "new sticker") {
5418
+ if (values.blank && values.style) throw new InputError("Choose --blank or --style, not both.");
2779
5419
  const preset = stickerStyles.find(
2780
5420
  (entry) => entry.source === (values.style ?? "typingmind-absolutely-right")
2781
5421
  );
2782
5422
  if (!preset)
2783
5423
  throw new InputError("Unknown sticker style. Run makeables styles list --product sticker.");
2784
- const design = badgeDesignSchema.parse(structuredClone(preset));
5424
+ const design = badgeDesignSchema.parse(
5425
+ values.blank ? {
5426
+ ...structuredClone(preset),
5427
+ name: typeof values.name === "string" ? values.name : "Untitled sticker",
5428
+ source: void 0,
5429
+ event: typeof values.name === "string" ? values.name : "Makeables",
5430
+ description: "",
5431
+ artPrompt: "",
5432
+ assets: {},
5433
+ front: { background: "#00000000", layers: [] },
5434
+ back: { background: "#00000000", layers: [] },
5435
+ sticker: { ...defaultStickerMaterial }
5436
+ } : structuredClone(preset)
5437
+ );
2785
5438
  const path = values.out ? await newPath(String(values.out)) : null;
2786
5439
  const dryRun = Boolean(values["dry-run"]);
2787
5440
  if (path && !dryRun) await writeAtomic(path, `${JSON.stringify(design, null, 2)}
@@ -2798,10 +5451,29 @@ ${path ?? "No file written. Add --out sticker.json."}`,
2798
5451
  );
2799
5452
  return true;
2800
5453
  }
5454
+ if ((command === "new badge" || command === "new card") && values.blank) {
5455
+ if (values.style || values.design || values.file)
5456
+ throw new InputError("Choose --blank or a --style, --design or --file, not both.");
5457
+ const design = blankDesign(command === "new card" ? "card" : "badge", values.name);
5458
+ const path = values.out ? await newPath(String(values.out)) : null;
5459
+ const dryRun = Boolean(values["dry-run"]);
5460
+ if (path && !dryRun) await writeAtomic(path, `${JSON.stringify(design, null, 2)}
5461
+ `, true);
5462
+ emit(
5463
+ { design, path, written: Boolean(path && !dryRun), dryRun },
5464
+ path ? [
5465
+ `makeables layer add --file ${JSON.stringify(path)} --kind text --params '{"text":"..."}'`
5466
+ ] : [],
5467
+ `${design.product === "card" ? "Card" : "Badge"} ready \xB7 ${design.name}
5468
+ ${path ?? "No file written. Add --out."}`,
5469
+ json
5470
+ );
5471
+ return true;
5472
+ }
2801
5473
  if (command === "new badge" || command === "new card") {
2802
5474
  const card = command === "new card";
2803
5475
  if (card && [values.design, values.style, values.file].filter(Boolean).length !== 1)
2804
- throw new InputError("Choose exactly one --design, --style or --file.");
5476
+ throw new InputError("Choose exactly one --design, --style, --file or --blank.");
2805
5477
  const preset = card && values.style ? findCardStyle(need(values.style, "style")) : void 0;
2806
5478
  if (card && values.style && !preset)
2807
5479
  throw new InputError("Unknown card style. Run makeables styles list --product card.");
@@ -2827,7 +5499,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2827
5499
  return true;
2828
5500
  }
2829
5501
  if (command === "material map") {
2830
- const path = resolve7(need(file, "file"));
5502
+ const path = resolve8(need(file, "file"));
2831
5503
  const next = await bindStickerMap(
2832
5504
  await readBadge(path),
2833
5505
  need(values.id, "id"),
@@ -2843,10 +5515,10 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2843
5515
  return true;
2844
5516
  }
2845
5517
  if (command === "material set") {
2846
- const path = resolve7(need(file, "file"));
5518
+ const path = resolve8(need(file, "file"));
2847
5519
  const design = await readBadge(path);
2848
5520
  const raw = need(values.params, "params");
2849
- const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
5521
+ const input = JSON.parse(raw.startsWith("@") ? await readFile8(raw.slice(1), "utf8") : raw);
2850
5522
  if (design.product === "sticker") {
2851
5523
  const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
2852
5524
  if (!patch2.success)
@@ -2905,11 +5577,11 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2905
5577
  const changes = values.params ? await readParams(String(values.params)) : {};
2906
5578
  if (typeof changes !== "object" || changes === null || Array.isArray(changes))
2907
5579
  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;
5580
+ for (const key2 of ["x", "y", "rotation"])
5581
+ if (values[key2] !== void 0) {
5582
+ const value = Number(values[key2]);
5583
+ if (!Number.isFinite(value)) throw new InputError(`--${key2} must be a number.`);
5584
+ changes[key2] = value;
2913
5585
  }
2914
5586
  design = setBadgeLayer(design, face, id, changes);
2915
5587
  }
@@ -2923,6 +5595,27 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2923
5595
  );
2924
5596
  return true;
2925
5597
  }
5598
+ if (command === "layer add" && values.kind) {
5599
+ if (values.sticker) throw new InputError("Choose --kind or --sticker, not both.");
5600
+ const path = need(file, "file");
5601
+ const face = side(values.side);
5602
+ const { design, id, warnings } = addLayer(
5603
+ await readBadge(path),
5604
+ face,
5605
+ String(values.kind),
5606
+ values.params ? await readParams(String(values.params)) : {}
5607
+ );
5608
+ await writeAtomic(path, `${JSON.stringify(design, null, 2)}
5609
+ `);
5610
+ const layer = design[face].layers.find((item) => item.id === id);
5611
+ emit(
5612
+ { layer, warnings },
5613
+ [renderHint(path, face, design)],
5614
+ [`${style("Added", "1", !json)} ${id} on ${face}`, ...warnings].join("\n"),
5615
+ json
5616
+ );
5617
+ return true;
5618
+ }
2926
5619
  if (command === "layer add") {
2927
5620
  const path = need(file, "file");
2928
5621
  const face = side(values.side);
@@ -2932,7 +5625,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2932
5625
  await readBadge(path),
2933
5626
  face,
2934
5627
  need(values.sticker, "sticker"),
2935
- values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve7(path))
5628
+ values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve8(path))
2936
5629
  );
2937
5630
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
2938
5631
  `);
@@ -2945,6 +5638,147 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2945
5638
  );
2946
5639
  return true;
2947
5640
  }
5641
+ if (command === "grid bin" || command === "grid plate") {
5642
+ const out = resolve8(need(values.out, "out"));
5643
+ const result = gridStl(command === "grid bin" ? "bin" : "plate", values);
5644
+ const handle = await open4(out, values.overwrite ? "w" : "wx", 420);
5645
+ try {
5646
+ await handle.writeFile(result.stl);
5647
+ } finally {
5648
+ await handle.close();
5649
+ }
5650
+ const { x, y, z: z5 } = result.sizeMm;
5651
+ emit(
5652
+ { out, part: result.part, triangles: result.triangles, sizeMm: result.sizeMm },
5653
+ [`Open ${out} in your slicer.`],
5654
+ `${style("Written", "1", !json)} ${result.name}, ${x} x ${y} x ${z5} mm, ${out}`,
5655
+ json
5656
+ );
5657
+ return true;
5658
+ }
5659
+ if (command === "brick part") {
5660
+ const id = need(values.id, "id");
5661
+ const sheet = brickPart(id, ldrawLibrary(values.library));
5662
+ emit(
5663
+ sheet,
5664
+ [`Place ${id} with Assembly.seat, which counts sockets as listed here.`],
5665
+ [
5666
+ `${style(sheet.id, "1", !json)} ${sheet.name}`,
5667
+ ` size ${sheet.size.join(" x ")} studs, origin at ${sheet.origin}`,
5668
+ ...Object.entries(sheet.connectors).map(
5669
+ ([kind, list]) => ` ${kind}: ${list.map((c) => `#${c.index} (${c.p.join(", ")})`).join(" ")}`
5670
+ )
5671
+ ].join("\n"),
5672
+ json
5673
+ );
5674
+ return true;
5675
+ }
5676
+ if (command.startsWith("brick ")) {
5677
+ const path = need(file, "file");
5678
+ const model = await readBrickModel(path, ldrawLibrary(values.library));
5679
+ if (command === "brick check") {
5680
+ const report = brickCheck(model);
5681
+ emit(
5682
+ report,
5683
+ report.ok ? [`makeables brick plan --file ${JSON.stringify(path)}`] : ["Fix the parts listed, or add connectors for joints the checker cannot see."],
5684
+ [
5685
+ `${style(report.ok ? "Buildable" : "Not buildable", "1", !json)} ${report.parts} parts, ${report.joints} joints`,
5686
+ ...report.issues.map((i) => ` ${i.rule}: ${i.message}: ${i.parts.join(", ")}`)
5687
+ ].join("\n"),
5688
+ json
5689
+ );
5690
+ if (!report.ok) process.exitCode = 2;
5691
+ return true;
5692
+ }
5693
+ if (command === "brick stress") {
5694
+ const joints = brickStress(model);
5695
+ emit(
5696
+ { joints },
5697
+ joints.length ? [
5698
+ "Brace the joints at the top: add a second connection, shorten the lever, or lighten the load."
5699
+ ] : ["Every joint sits in a loop; nothing hangs on a single joint."],
5700
+ joints.length ? joints.map(
5701
+ (j) => ` ${j.moment.toFixed(2).padStart(8)} ${j.kind.padEnd(16)} x${j.count} holds ${j.holds.length} parts, lever ${j.lever} studs ${j.between.join(" -> ")}`
5702
+ ).join("\n") : "No joint holds part of the build alone.",
5703
+ json
5704
+ );
5705
+ return true;
5706
+ }
5707
+ if (command === "brick plan") {
5708
+ const plan = brickPlan(model);
5709
+ emit(
5710
+ { steps: plan.rows, locked: plan.locked },
5711
+ [`makeables brick render --file ${JSON.stringify(path)} --out step.png --step 1`],
5712
+ [
5713
+ ...plan.rows.map(
5714
+ (r) => ` ${String(r.step).padStart(3)} ${r.part.padEnd(9)} ${r.insert} ${r.name}`
5715
+ ),
5716
+ ...plan.locked.length ? [` locked: ${plan.locked.join(", ")}`] : []
5717
+ ].join("\n"),
5718
+ json
5719
+ );
5720
+ return true;
5721
+ }
5722
+ const number = (name) => {
5723
+ const raw = values[name];
5724
+ if (raw === void 0) return void 0;
5725
+ const n = Number(raw);
5726
+ if (!Number.isFinite(n)) throw new InputError(`--${name} must be a number.`);
5727
+ return n;
5728
+ };
5729
+ if (command === "brick stl") {
5730
+ const out2 = resolve8(need(values.out, "out"));
5731
+ const scale2 = number("scale");
5732
+ const result = brickStl(model, scale2);
5733
+ const handle2 = await open4(out2, values.overwrite ? "w" : "wx", 420);
5734
+ try {
5735
+ await handle2.writeFile(result.stl);
5736
+ } finally {
5737
+ await handle2.close();
5738
+ }
5739
+ const { x, y, z: z5 } = result.sizeMm;
5740
+ emit(
5741
+ { out: out2, parts: result.parts, triangles: result.triangles, sizeMm: result.sizeMm },
5742
+ [`Open ${out2} in your slicer; the parts are already fused into one closed solid.`],
5743
+ `${style("Written", "1", !json)} ${result.parts} parts, ${x} x ${y} x ${z5} mm, ${out2}`,
5744
+ json
5745
+ );
5746
+ return true;
5747
+ }
5748
+ if (command === "brick remix") {
5749
+ const seed = number("seed");
5750
+ if (seed === void 0 || !Number.isInteger(seed))
5751
+ throw new InputError("--seed must be an integer.");
5752
+ const out2 = await newPath(need(values.out, "out"));
5753
+ const result = brickRemix(model, seed, values.mode);
5754
+ await writeAtomic(out2, result.ldr, true);
5755
+ emit(
5756
+ { out: out2, parts: result.check.parts, left: result.left, check: result.check },
5757
+ [
5758
+ `makeables brick plan --file ${JSON.stringify(out2)}`,
5759
+ `makeables brick render --file ${JSON.stringify(out2)} --out remix.png`
5760
+ ],
5761
+ `${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}`,
5762
+ json
5763
+ );
5764
+ return true;
5765
+ }
5766
+ const out = resolve8(need(values.out, "out"));
5767
+ const png = await brickRender(model, {
5768
+ step: number("step"),
5769
+ explode: number("explode"),
5770
+ yaw: number("yaw"),
5771
+ pitch: number("pitch")
5772
+ });
5773
+ const handle = await open4(out, values.overwrite ? "w" : "wx", 420);
5774
+ try {
5775
+ await handle.writeFile(png);
5776
+ } finally {
5777
+ await handle.close();
5778
+ }
5779
+ emit({ out }, [`Open ${out} and look at it.`], `${style("Rendered", "1", !json)} ${out}`, json);
5780
+ return true;
5781
+ }
2948
5782
  if (command.startsWith("sheet ")) {
2949
5783
  const write = async (target, bytes) => {
2950
5784
  const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
@@ -3026,9 +5860,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3026
5860
  throw new InputError(
3027
5861
  `The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
3028
5862
  );
3029
- const out = resolve7(need(values.out, "out"));
5863
+ const out = resolve8(need(values.out, "out"));
3030
5864
  if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
3031
- const preview = typeof values.preview === "string" ? resolve7(values.preview) : void 0;
5865
+ const preview = typeof values.preview === "string" ? resolve8(values.preview) : void 0;
3032
5866
  if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
3033
5867
  const targets = [out, ...preview ? [preview] : []];
3034
5868
  if (!values.overwrite) for (const target of targets) await newPath(target);
@@ -3046,7 +5880,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3046
5880
  if (command === "cut") {
3047
5881
  const design = await readBadge(need(file, "file"));
3048
5882
  if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
3049
- const out = typeof values.out === "string" ? resolve7(values.out) : void 0;
5883
+ const out = typeof values.out === "string" ? resolve8(values.out) : void 0;
3050
5884
  if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
3051
5885
  const target = out && (values.overwrite ? out : await newPath(out));
3052
5886
  const { cut, svg } = await stickerPrintCut(design);
@@ -3087,16 +5921,16 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3087
5921
  throw new InputError("--appearance must be flat or material.");
3088
5922
  const appearance = values.appearance;
3089
5923
  const path = need(file, "file");
3090
- const out = resolve7(need(values.out, "out"));
5924
+ const out = resolve8(need(values.out, "out"));
3091
5925
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
3092
5926
  const target = values.overwrite ? out : await newPath(out);
3093
5927
  const number = values.number === void 0 ? void 0 : Number(values.number);
3094
5928
  if (number !== void 0 && !Number.isInteger(number))
3095
5929
  throw new InputError("--number must be an integer.");
3096
5930
  const face = side(values.side);
3097
- const value = JSON.parse(await readFile7(resolve7(path), "utf8"));
5931
+ const value = JSON.parse(await readFile8(resolve8(path), "utf8"));
3098
5932
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
3099
- if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
5933
+ if (pkg && ["photo", "name", "role", "number"].some((key2) => values[key2] !== void 0))
3100
5934
  throw new InputError(
3101
5935
  "Portable packages include their participant and images. Edit the package before rendering, or use these flags with a plain design JSON."
3102
5936
  );
@@ -3158,13 +5992,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3158
5992
  return true;
3159
5993
  }
3160
5994
  if (command === "validate") {
3161
- const path = resolve7(need(file, "file"));
5995
+ const path = resolve8(need(file, "file"));
3162
5996
  if (extname4(path).toLowerCase() === ".svg") {
3163
- const source = await readFile7(path, "utf8");
5997
+ const source = await readFile8(path, "utf8");
3164
5998
  const result2 = checkSubmittedSvg(source);
3165
5999
  if (!result2.ok) throw new InputError(result2.error);
3166
6000
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
3167
- if (preview) await sharp8(Buffer.from(result2.svg)).png().toFile(preview);
6001
+ if (preview) await sharp9(Buffer.from(result2.svg)).png().toFile(preview);
3168
6002
  emit(
3169
6003
  {
3170
6004
  valid: true,
@@ -3181,7 +6015,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3181
6015
  return true;
3182
6016
  }
3183
6017
  if (values.out) throw new InputError("--out is only used to preview SVG validation.");
3184
- const input = JSON.parse(await readFile7(path, "utf8"));
6018
+ const input = JSON.parse(await readFile8(path, "utf8"));
3185
6019
  const result = badgeDesignSchema.safeParse(input);
3186
6020
  if (!result.success)
3187
6021
  throw new InputError(
@@ -3199,7 +6033,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3199
6033
  return false;
3200
6034
  }
3201
6035
  run().catch((error) => {
3202
- const userError = error instanceof InputError || error instanceof ZodError || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
6036
+ const userError = error instanceof InputError || error instanceof ZodError2 || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
3203
6037
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
3204
6038
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
3205
6039
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",
@@ -3229,7 +6063,7 @@ run().catch((error) => {
3229
6063
  if (!traceFile) return;
3230
6064
  try {
3231
6065
  const supplied = process.env.MAKEABLES_TRACE_SESSION;
3232
- await appendTrace(resolve7(traceFile), {
6066
+ await appendTrace(resolve8(traceFile), {
3233
6067
  sessionId: supplied && /^[a-zA-Z0-9_-]{1,80}$/.test(supplied) ? supplied : randomUUID3(),
3234
6068
  version,
3235
6069
  command: traceCommand,