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