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