makeables 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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-CMFI3BIR.js → badge-package-RBQ43DQX.js} +2 -2
- package/dist/{brand-NMENEWIZ.js → brand-UNVFUN4H.js} +4 -6
- package/dist/{brand-preview-W4FTJJM3.js → brand-preview-C6HBXUXH.js} +4 -4
- package/dist/{chunk-6RBOBRL4.js → chunk-7A4TRTVE.js} +2 -2
- package/dist/{chunk-DLTVENEP.js → chunk-KLJRRHMY.js} +70 -13
- package/dist/{chunk-YB2YJCBS.js → chunk-MXT43NAQ.js} +98 -69
- package/dist/{chunk-4WLCMPRH.js → chunk-WEPC5LJ7.js} +9 -2
- package/dist/{chunk-ZZA6CR6X.js → chunk-YOGNQAYJ.js} +117 -105
- package/dist/cli.js +883 -142
- package/dist/{kit-IEWHMBS2.js → kit-SZ4WFGHZ.js} +3 -3
- package/dist/{mcp-YBRUVIWG.js → mcp-2VMGYAUZ.js} +3 -3
- package/dist/print-NGPUZPVN.js +18219 -0
- package/dist/{submission-ATVS5YTK.js → submission-H3RXUH7T.js} +1 -1
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
prepareBrand,
|
|
11
11
|
readBrand,
|
|
12
12
|
writeBrand
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-YOGNQAYJ.js";
|
|
14
14
|
import {
|
|
15
15
|
addKitMember,
|
|
16
16
|
addToSheet,
|
|
@@ -25,12 +25,12 @@ import {
|
|
|
25
25
|
readSheet,
|
|
26
26
|
sheetPreviewPng,
|
|
27
27
|
sheetReport
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-7A4TRTVE.js";
|
|
29
29
|
import {
|
|
30
30
|
getSkill,
|
|
31
31
|
skills,
|
|
32
32
|
version
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-WEPC5LJ7.js";
|
|
34
34
|
import {
|
|
35
35
|
CloudError,
|
|
36
36
|
InputError,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
submitDesign,
|
|
62
62
|
summarizeBadgeLayers,
|
|
63
63
|
writeAtomic
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-MXT43NAQ.js";
|
|
65
65
|
import {
|
|
66
66
|
PACKAGE_LIMIT,
|
|
67
67
|
badgeDesignObjectSchema,
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
stickerMaterialSchema,
|
|
80
80
|
svgNodeSchema,
|
|
81
81
|
svgNodeXml
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-KLJRRHMY.js";
|
|
83
83
|
|
|
84
84
|
// src/cli.ts
|
|
85
85
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
@@ -453,8 +453,10 @@ function worldBody(lib, item) {
|
|
|
453
453
|
return { min: lo, max: hi };
|
|
454
454
|
}
|
|
455
455
|
function pointMates(x, y, adapter) {
|
|
456
|
-
|
|
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;
|
|
457
458
|
}
|
|
459
|
+
var isSpherical = (adapter, kind) => adapter.spherical?.includes(kind) ?? false;
|
|
458
460
|
function axialMates(x, y, adapter) {
|
|
459
461
|
if (length(cross(x.a, y.a)) > 0.01) return false;
|
|
460
462
|
const d = sub(y.p, x.p);
|
|
@@ -464,26 +466,77 @@ function axialMates(x, y, adapter) {
|
|
|
464
466
|
const hy = (y.len ?? 0) / 2;
|
|
465
467
|
return Math.min(hx, along + hy) - Math.max(-hx, along - hy) >= adapter.minEngagement;
|
|
466
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
|
+
}
|
|
467
517
|
function contacts(model, lib, adapter) {
|
|
468
518
|
const world = model.items.map((it) => worldConnectors(lib, it));
|
|
519
|
+
const reach = model.items.map((it, i) => reachBox(lib, it, world[i]));
|
|
469
520
|
const out = [];
|
|
470
|
-
for (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
for (const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
}
|
|
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
|
+
}
|
|
487
540
|
return out;
|
|
488
541
|
}
|
|
489
542
|
function overlapVolume(x, y, epsilon = 0.02) {
|
|
@@ -511,7 +564,7 @@ function partVoxels(part2) {
|
|
|
511
564
|
}
|
|
512
565
|
return part2.voxels;
|
|
513
566
|
}
|
|
514
|
-
function collisionCells(lib, x, y) {
|
|
567
|
+
function collisionCells(lib, x, y, ignore) {
|
|
515
568
|
let hits = 0;
|
|
516
569
|
for (const [a, b] of [
|
|
517
570
|
[x, y],
|
|
@@ -529,6 +582,7 @@ function collisionCells(lib, x, y) {
|
|
|
529
582
|
va.origin[2] + (k + 0.5) * CELL
|
|
530
583
|
];
|
|
531
584
|
const world = add(apply(a.rotation, local), a.position);
|
|
585
|
+
if (ignore && length(sub(world, ignore.centre)) < ignore.radius) continue;
|
|
532
586
|
const r = b.rotation;
|
|
533
587
|
const d = sub(world, b.position);
|
|
534
588
|
const inB = [
|
|
@@ -559,7 +613,7 @@ function readRef(f) {
|
|
|
559
613
|
name: normalizeName(f.slice(14).join(" "))
|
|
560
614
|
};
|
|
561
615
|
}
|
|
562
|
-
function parsePart(name, resolve9,
|
|
616
|
+
function parsePart(name, resolve9, isJoint2) {
|
|
563
617
|
const triangles = [];
|
|
564
618
|
const colors = [];
|
|
565
619
|
const edges = [];
|
|
@@ -577,7 +631,7 @@ function parsePart(name, resolve9, isJoint) {
|
|
|
577
631
|
const ref = readRef(f);
|
|
578
632
|
const rotation = mul(m, ref.rotation);
|
|
579
633
|
const position = add(apply(m, ref.position), t);
|
|
580
|
-
const joint = !inJoint &&
|
|
634
|
+
const joint = !inJoint && isJoint2(ref.name);
|
|
581
635
|
if (joint) joints.push({ name: ref.name, ...toStuds(rotation, position) });
|
|
582
636
|
const inherited = ref.color === 16 ? color : ref.color;
|
|
583
637
|
walk(ref.name, rotation, position, inJoint || joint, depth + 1, inherited);
|
|
@@ -764,6 +818,9 @@ function technicShafts(name, triangles) {
|
|
|
764
818
|
];
|
|
765
819
|
}
|
|
766
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);
|
|
767
824
|
var FAMILY = {
|
|
768
825
|
"3001": "brick",
|
|
769
826
|
"3003": "brick",
|
|
@@ -810,8 +867,11 @@ var lego = {
|
|
|
810
867
|
shoulder: ["shoulderhole"],
|
|
811
868
|
shoulderhole: ["shoulder"],
|
|
812
869
|
wrist: ["hand"],
|
|
813
|
-
hand: ["wrist"]
|
|
870
|
+
hand: ["wrist"],
|
|
871
|
+
ball: ["ballsocket"],
|
|
872
|
+
ballsocket: ["ball"]
|
|
814
873
|
},
|
|
874
|
+
spherical: ["ball", "ballsocket"],
|
|
815
875
|
tolerance: 0.06,
|
|
816
876
|
minEngagement: 0.24,
|
|
817
877
|
family: (part2) => FAMILY[part2] ?? part2
|
|
@@ -834,6 +894,12 @@ var LEGO_CONNECTORS = {
|
|
|
834
894
|
],
|
|
835
895
|
// Tyre 6/ 50 x 8 Offset Tread
|
|
836
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
|
+
],
|
|
837
903
|
// Hinge 1 x 2 Base and Top: both pivot on the cylinder along x at 10 LDU below the origin.
|
|
838
904
|
"3937": [{ kind: "hinge", p: [0, -0.5, 0], a: [1, 0, 0], len: 2 }],
|
|
839
905
|
"3938": [{ kind: "hinge", p: [0, -0.5, 0], a: [1, 0, 0], len: 2 }],
|
|
@@ -918,17 +984,37 @@ var lattice = (lo, hi, phase) => {
|
|
|
918
984
|
if (x - lo > 0.25 && hi - x > 0.25) out.push(x);
|
|
919
985
|
return out;
|
|
920
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
|
+
}
|
|
921
1005
|
function legoConnectors(parsed, voxels) {
|
|
922
1006
|
const out = [];
|
|
923
|
-
for (const j of parsed.joints)
|
|
924
|
-
|
|
925
|
-
|
|
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
|
+
}
|
|
926
1013
|
const { min, max } = parsed.body;
|
|
927
|
-
const up = out.find((c) => dot(c.a, [0, 1, 0]) > 0.99);
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
};
|
|
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);
|
|
932
1018
|
const y = min[1];
|
|
933
1019
|
const rim = [];
|
|
934
1020
|
for (let x = min[0] + CELL / 2; x < max[0]; x += CELL)
|
|
@@ -937,21 +1023,21 @@ function legoConnectors(parsed, voxels) {
|
|
|
937
1023
|
rim.push([min[0] + CELL / 2, y + CELL / 2, z5], [max[0] - CELL / 2, y + CELL / 2, z5]);
|
|
938
1024
|
const walled = rim.filter((p) => cellAt(voxels, p)).length >= WALLED_BOTTOM * rim.length;
|
|
939
1025
|
if (!up && !walled && flatTop(parsed) < FLAT_TOP) return out;
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
for (
|
|
944
|
-
if (
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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);
|
|
951
1037
|
return out;
|
|
952
1038
|
}
|
|
953
1039
|
function loadLegoPart(id, resolve9, opts = {}) {
|
|
954
|
-
const parsed = parsePart(`${id}.dat`, resolve9,
|
|
1040
|
+
const parsed = parsePart(`${id}.dat`, resolve9, isJoint);
|
|
955
1041
|
const bounds = meshBounds(parsed.triangles);
|
|
956
1042
|
const voxels = voxelize(parsed.triangles, bounds.min, bounds.max);
|
|
957
1043
|
return {
|
|
@@ -980,6 +1066,37 @@ function libraryResolver(read, root) {
|
|
|
980
1066
|
return void 0;
|
|
981
1067
|
};
|
|
982
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
|
+
}
|
|
983
1100
|
var DEFAULT_PALETTE = {
|
|
984
1101
|
0: "#1B2A34",
|
|
985
1102
|
1: "#1E5AA8",
|
|
@@ -1024,20 +1141,37 @@ function rasterize(model, lib, opts = {}) {
|
|
|
1024
1141
|
const center = [0, 0, 0];
|
|
1025
1142
|
for (const b of bodies) for (let k = 0; k < 3; k++) center[k] += (b.min[k] + b.max[k]) / 2;
|
|
1026
1143
|
if (bodies.length) for (let k = 0; k < 3; k++) center[k] /= bodies.length;
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
+
};
|
|
1029
1156
|
let minX = Infinity;
|
|
1030
1157
|
let minY = Infinity;
|
|
1031
1158
|
let maxX = -Infinity;
|
|
1032
1159
|
let maxY = -Infinity;
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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;
|
|
1040
1173
|
};
|
|
1174
|
+
const w = new Float64Array(9);
|
|
1041
1175
|
shown.forEach(({ item, i }, n) => {
|
|
1042
1176
|
const part2 = lib[item.part];
|
|
1043
1177
|
if (!part2) throw new Error(`Unknown part: ${item.part}`);
|
|
@@ -1049,27 +1183,43 @@ function rasterize(model, lib, opts = {}) {
|
|
|
1049
1183
|
];
|
|
1050
1184
|
const offset = opts.explode ? scale(sub(mid, center), opts.explode) : [0, 0, 0];
|
|
1051
1185
|
const fade = highlight.size && !highlight.has(i) ? 0.72 : 0;
|
|
1052
|
-
const
|
|
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();
|
|
1053
1195
|
const t = part2.triangles;
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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);
|
|
1059
1205
|
const len = length(normal);
|
|
1060
1206
|
if (len < 1e-9) continue;
|
|
1061
1207
|
const nn = scale(normal, 1 / len);
|
|
1062
1208
|
const facing = dot(nn, view) < 0 ? scale(nn, -1) : nn;
|
|
1063
1209
|
const code = part2.colors[f] === 16 ? item.color : part2.colors[f];
|
|
1064
1210
|
const k = 0.55 + 0.45 * Math.max(0, dot(facing, LIGHT));
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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++;
|
|
1073
1223
|
}
|
|
1074
1224
|
const e = part2.edges ?? [];
|
|
1075
1225
|
const base = rgb(palette[item.color] ?? "#888888");
|
|
@@ -1077,77 +1227,103 @@ function rasterize(model, lib, opts = {}) {
|
|
|
1077
1227
|
const edge = (dark ? [110, 110, 110] : base.map((c) => c * 0.45)).map(
|
|
1078
1228
|
(c) => c + (255 - c) * fade
|
|
1079
1229
|
);
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
}
|
|
1086
1240
|
});
|
|
1087
|
-
const s = tris
|
|
1241
|
+
const s = tris ? 0.9 * Math.min(W2 / (maxX - minX || 1), H2 / (maxY - minY || 1)) : 1;
|
|
1088
1242
|
const ox = W2 / 2 - s * (minX + maxX) / 2;
|
|
1089
1243
|
const oy = H2 / 2 - s * (minY + maxY) / 2;
|
|
1090
|
-
const sx = (v) => [ox + s * v[0], oy + s * v[1], v[2]];
|
|
1091
1244
|
const data = new Uint8ClampedArray(W2 * H2 * 4);
|
|
1092
1245
|
const depth = new Float32Array(W2 * H2).fill(-Infinity);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
for (const tri of tris) {
|
|
1108
|
-
const [a, b, c] = tri.p.map(sx);
|
|
1109
|
-
const area = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
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);
|
|
1110
1260
|
if (Math.abs(area) < 1e-9) continue;
|
|
1111
|
-
const
|
|
1112
|
-
const
|
|
1113
|
-
const
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
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++) {
|
|
1117
1286
|
const px = x + 0.5;
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
const l1 = ((
|
|
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;
|
|
1121
1290
|
const l2 = 1 - l0 - l1;
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
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;
|
|
1124
1299
|
}
|
|
1300
|
+
}
|
|
1125
1301
|
}
|
|
1126
1302
|
const bias = 0.02;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
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))));
|
|
1131
1313
|
for (let k = 0; k <= steps; k++) {
|
|
1132
1314
|
const t = k / steps;
|
|
1133
|
-
const x = Math.round(
|
|
1134
|
-
const y = Math.round(
|
|
1315
|
+
const x = Math.round(ax + (bx - ax) * t);
|
|
1316
|
+
const y = Math.round(ay + (by - ay) * t);
|
|
1135
1317
|
if (x < 0 || y < 0 || x >= W2 || y >= H2) continue;
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const z5 = a[2] + (b[2] - a[2]) * t;
|
|
1146
|
-
if (z5 + bias < depth[p]) continue;
|
|
1147
|
-
data[p * 4] = line.color[0];
|
|
1148
|
-
data[p * 4 + 1] = line.color[1];
|
|
1149
|
-
data[p * 4 + 2] = line.color[2];
|
|
1150
|
-
}
|
|
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
|
+
}
|
|
1151
1327
|
}
|
|
1152
1328
|
}
|
|
1153
1329
|
return { width: W2, height: H2, data };
|
|
@@ -1178,6 +1354,11 @@ var DIRECTIONS = [
|
|
|
1178
1354
|
];
|
|
1179
1355
|
var STEP = 0.15;
|
|
1180
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
|
+
});
|
|
1181
1362
|
var moved = (it, d, s) => ({
|
|
1182
1363
|
...it,
|
|
1183
1364
|
position: add(it.position, scale(d, s))
|
|
@@ -1191,7 +1372,16 @@ function planAssembly(model, lib, adapter) {
|
|
|
1191
1372
|
const bodies = items.map((it) => worldBody(lib, it));
|
|
1192
1373
|
const axes = /* @__PURE__ */ new Map();
|
|
1193
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
|
+
};
|
|
1194
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
|
+
}
|
|
1195
1385
|
if (c.axis) {
|
|
1196
1386
|
axes.set(`${c.a},${c.b}`, c.axis);
|
|
1197
1387
|
axes.set(`${c.b},${c.a}`, c.axis);
|
|
@@ -1199,6 +1389,8 @@ function planAssembly(model, lib, adapter) {
|
|
|
1199
1389
|
if (c.release) {
|
|
1200
1390
|
releases.set(`${c.b},${c.a}`, c.release);
|
|
1201
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));
|
|
1202
1394
|
}
|
|
1203
1395
|
}
|
|
1204
1396
|
const allowed = (i, d, around) => {
|
|
@@ -1218,12 +1410,20 @@ function planAssembly(model, lib, adapter) {
|
|
|
1218
1410
|
return dot(d, sub(centre(bodies[i]), centre(bodies[j]))) >= -1e-6;
|
|
1219
1411
|
};
|
|
1220
1412
|
const left = new Set(items.map((_, i) => i));
|
|
1221
|
-
const freeWay = (i, around) =>
|
|
1413
|
+
const freeWay = (i, around) => ways[i].find((d) => {
|
|
1222
1414
|
if (!allowed(i, d, around)) return false;
|
|
1223
|
-
const others = [...around].filter(
|
|
1224
|
-
|
|
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
|
+
}
|
|
1225
1425
|
const box2 = shifted(bodies[i], d, s);
|
|
1226
|
-
const near =
|
|
1426
|
+
const near = nearby.filter((j) => overlapVolume(box2, bodies[j], -0.4) > 0);
|
|
1227
1427
|
if (!near.length) return true;
|
|
1228
1428
|
const it = moved(items[i], d, s);
|
|
1229
1429
|
if (near.some((j) => collisionCells(lib, it, items[j]) > BLOCKING_CELLS)) return false;
|
|
@@ -1387,6 +1587,134 @@ function remix(exemplar, lib, adapter, opts) {
|
|
|
1387
1587
|
}
|
|
1388
1588
|
return { model: { items }, left: pending.flatMap((u) => u.items.map((it) => it.part)) };
|
|
1389
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
|
+
}
|
|
1390
1718
|
function toStl(model, lib, unit) {
|
|
1391
1719
|
const tris = [];
|
|
1392
1720
|
for (const item of model.items) {
|
|
@@ -1398,6 +1726,9 @@ function toStl(model, lib, unit) {
|
|
|
1398
1726
|
tris.push(p[0] * unit, -p[2] * unit, p[1] * unit);
|
|
1399
1727
|
}
|
|
1400
1728
|
}
|
|
1729
|
+
return encodeStl(tris);
|
|
1730
|
+
}
|
|
1731
|
+
function encodeStl(tris) {
|
|
1401
1732
|
const count = tris.length / 9;
|
|
1402
1733
|
const out = new DataView(new ArrayBuffer(84 + count * 50));
|
|
1403
1734
|
out.setUint32(80, count, true);
|
|
@@ -1413,17 +1744,253 @@ function toStl(model, lib, unit) {
|
|
|
1413
1744
|
}
|
|
1414
1745
|
return new Uint8Array(out.buffer);
|
|
1415
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
|
+
}
|
|
1416
1976
|
var COLLISION_CELLS = 4;
|
|
1977
|
+
var SOCKET_REACH = 0.5;
|
|
1417
1978
|
function validate(model, lib, adapter, opts = {}) {
|
|
1418
1979
|
const issues = [];
|
|
1419
1980
|
const n = model.items.length;
|
|
1420
1981
|
const joints = contacts(model, lib, adapter);
|
|
1421
1982
|
const bodies = model.items.map((it) => worldBody(lib, it));
|
|
1422
1983
|
const through = new Set(joints.filter((c) => c.axis).map((c) => `${c.a},${c.b}`));
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
+
}
|
|
1427
1994
|
const parent = model.items.map((_, i) => i);
|
|
1428
1995
|
const find = (x) => {
|
|
1429
1996
|
while (parent[x] !== x) x = parent[x];
|
|
@@ -1482,6 +2049,16 @@ async function readBrickModel(file, library) {
|
|
|
1482
2049
|
const config = read(join(library, "LDConfig.ldr"));
|
|
1483
2050
|
return { model, lib, names, palette: config ? parsePalette(config) : {} };
|
|
1484
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
|
+
}
|
|
1485
2062
|
function brickCheck({ model, lib, names }) {
|
|
1486
2063
|
const result = validate(model, lib, lego);
|
|
1487
2064
|
const label2 = (i) => `${i}:${model.items[i].part} ${names[model.items[i].part]}`;
|
|
@@ -1492,6 +2069,10 @@ function brickCheck({ model, lib, names }) {
|
|
|
1492
2069
|
issues: result.issues.map((issue) => ({ ...issue, parts: issue.items.map(label2) }))
|
|
1493
2070
|
};
|
|
1494
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
|
+
}
|
|
1495
2076
|
function brickPlan({ model, lib, names }) {
|
|
1496
2077
|
const plan = planAssembly(model, lib, lego);
|
|
1497
2078
|
const axis = (d) => ["+x", "-x", "+y", "-y", "+z", "-z"][[
|
|
@@ -1546,6 +2127,29 @@ function brickRemix(exemplar, seed, mode) {
|
|
|
1546
2127
|
check: brickCheck(grown)
|
|
1547
2128
|
};
|
|
1548
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
|
+
}
|
|
1549
2153
|
|
|
1550
2154
|
// src/card-import.ts
|
|
1551
2155
|
import { createHash } from "crypto";
|
|
@@ -3812,9 +4416,14 @@ var commands = [
|
|
|
3812
4416
|
"sheet pack --file <sheet.json>",
|
|
3813
4417
|
"sheet check --file <sheet.json>",
|
|
3814
4418
|
"sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
|
|
4419
|
+
"brick part --id <ldraw-id> [--library <ldraw-dir>]",
|
|
3815
4420
|
"brick check --file <model.ldr> [--library <ldraw-dir>]",
|
|
3816
4421
|
"brick plan --file <model.ldr> [--library <ldraw-dir>]",
|
|
4422
|
+
"brick stress --file <model.ldr> [--library <ldraw-dir>]",
|
|
3817
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]",
|
|
3818
4427
|
"grid bin --width <cells> --depth <cells> --height <units> --out <file.stl> [--overwrite]",
|
|
3819
4428
|
"grid plate --width <cells> --depth <cells> --out <file.stl> [--overwrite]",
|
|
3820
4429
|
"brick render --file <model.ldr> --out <png> [--step <n>] [--explode <factor>] [--yaw=<deg>] [--pitch=<deg>] [--library <ldraw-dir>] [--overwrite]",
|
|
@@ -3893,7 +4502,10 @@ async function newPath(file) {
|
|
|
3893
4502
|
return path;
|
|
3894
4503
|
}
|
|
3895
4504
|
async function run() {
|
|
3896
|
-
const {
|
|
4505
|
+
const {
|
|
4506
|
+
values: { param: printParams, ...values },
|
|
4507
|
+
positionals
|
|
4508
|
+
} = parseArgs({
|
|
3897
4509
|
allowPositionals: true,
|
|
3898
4510
|
strict: true,
|
|
3899
4511
|
options: {
|
|
@@ -3917,6 +4529,7 @@ async function run() {
|
|
|
3917
4529
|
port: { type: "string" },
|
|
3918
4530
|
url: { type: "string" },
|
|
3919
4531
|
params: { type: "string" },
|
|
4532
|
+
param: { type: "string", multiple: true },
|
|
3920
4533
|
state: { type: "string" },
|
|
3921
4534
|
target: { type: "string" },
|
|
3922
4535
|
yes: { type: "boolean" },
|
|
@@ -3954,6 +4567,7 @@ async function run() {
|
|
|
3954
4567
|
yaw: { type: "string" },
|
|
3955
4568
|
pitch: { type: "string" },
|
|
3956
4569
|
seed: { type: "string" },
|
|
4570
|
+
scale: { type: "string" },
|
|
3957
4571
|
width: { type: "string" },
|
|
3958
4572
|
depth: { type: "string" },
|
|
3959
4573
|
height: { type: "string" },
|
|
@@ -3963,6 +4577,7 @@ async function run() {
|
|
|
3963
4577
|
slug: { type: "string" },
|
|
3964
4578
|
family: { type: "string" },
|
|
3965
4579
|
brand: { type: "string" },
|
|
4580
|
+
colors: { type: "string" },
|
|
3966
4581
|
probe: { type: "boolean" },
|
|
3967
4582
|
blank: { type: "boolean" },
|
|
3968
4583
|
kind: { type: "string" },
|
|
@@ -4001,7 +4616,8 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4001
4616
|
);
|
|
4002
4617
|
return;
|
|
4003
4618
|
}
|
|
4004
|
-
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(" ");
|
|
4005
4621
|
const allowed = {
|
|
4006
4622
|
"kit new": ["name", "out"],
|
|
4007
4623
|
"kit extract": ["file", "out", "params"],
|
|
@@ -4118,10 +4734,15 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4118
4734
|
"sheet pack": ["file"],
|
|
4119
4735
|
"sheet check": ["file"],
|
|
4120
4736
|
"sheet export": ["file", "out", "preview", "overwrite"],
|
|
4737
|
+
"brick part": ["id", "library"],
|
|
4121
4738
|
"brick check": ["file", "library"],
|
|
4122
4739
|
"brick plan": ["file", "library"],
|
|
4740
|
+
"brick stress": ["file", "library"],
|
|
4123
4741
|
"brick render": ["file", "out", "step", "explode", "yaw", "pitch", "library", "overwrite"],
|
|
4124
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"],
|
|
4125
4746
|
"grid bin": ["width", "depth", "height", "out", "overwrite"],
|
|
4126
4747
|
"grid plate": ["width", "depth", "out", "overwrite"],
|
|
4127
4748
|
render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number", "appearance"],
|
|
@@ -4132,6 +4753,8 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4132
4753
|
for (const flag of Object.keys(values))
|
|
4133
4754
|
if (!["json", "help", "version", "compact", "trace", ...allowed[command]].includes(flag))
|
|
4134
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}`);
|
|
4135
4758
|
const progress = (data) => process.stderr.write(`${JSON.stringify(data)}
|
|
4136
4759
|
`);
|
|
4137
4760
|
if (values.product && !["card", "badge", "sticker"].includes(values.product))
|
|
@@ -4144,7 +4767,7 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4144
4767
|
const out = await newPath(need(values.out, "out"));
|
|
4145
4768
|
if (!out.endsWith(".makeables.json"))
|
|
4146
4769
|
throw new InputError("--out must end in .makeables.json.");
|
|
4147
|
-
const { badgePackage } = await import("./badge-package-
|
|
4770
|
+
const { badgePackage } = await import("./badge-package-RBQ43DQX.js");
|
|
4148
4771
|
const { json: body, participant } = await badgePackage(
|
|
4149
4772
|
need(values.file, "file"),
|
|
4150
4773
|
need(values.photo, "photo"),
|
|
@@ -4242,7 +4865,7 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4242
4865
|
} else if (command === "brand export") {
|
|
4243
4866
|
const out = resolve8(need(values.out, "out"));
|
|
4244
4867
|
if (!out.endsWith(".zip")) throw new InputError("--out must end in .zip.");
|
|
4245
|
-
const { packageZip } = await import("./submission-
|
|
4868
|
+
const { packageZip } = await import("./submission-H3RXUH7T.js");
|
|
4246
4869
|
const pkg = await prepareBrand(need(values.file, "file"));
|
|
4247
4870
|
const bytes = await packageZip(pkg);
|
|
4248
4871
|
const handle2 = await open4(
|
|
@@ -4261,7 +4884,7 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4261
4884
|
bytes: bytes.length
|
|
4262
4885
|
};
|
|
4263
4886
|
} else {
|
|
4264
|
-
const { serveBrandPreview } = await import("./brand-preview-
|
|
4887
|
+
const { serveBrandPreview } = await import("./brand-preview-C6HBXUXH.js");
|
|
4265
4888
|
result2 = await serveBrandPreview(need(values.file, "file"), {
|
|
4266
4889
|
site: values.site,
|
|
4267
4890
|
open: !values["no-open"]
|
|
@@ -4377,6 +5000,74 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4377
5000
|
emit(result2, ["makeables skills get cards"], JSON.stringify(result2, null, 2), json);
|
|
4378
5001
|
return;
|
|
4379
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
|
+
}
|
|
4380
5071
|
const card = await cardCommand(command, values, json);
|
|
4381
5072
|
if (card) return;
|
|
4382
5073
|
if (command === "login" || command === "logout") {
|
|
@@ -4415,7 +5106,7 @@ From a built checkout: npm run studio -- <command>`,
|
|
|
4415
5106
|
(text) => text.includes('"makeables-brand"'),
|
|
4416
5107
|
() => false
|
|
4417
5108
|
);
|
|
4418
|
-
const submit = isBrand ? (await import("./brand-
|
|
5109
|
+
const submit = isBrand ? (await import("./brand-UNVFUN4H.js")).submitBrand : submitDesign;
|
|
4419
5110
|
const result2 = await submit({
|
|
4420
5111
|
title: values.title,
|
|
4421
5112
|
credit: values.credit,
|
|
@@ -4522,7 +5213,7 @@ Run makeables skills get core for setup.`,
|
|
|
4522
5213
|
}
|
|
4523
5214
|
if (command === "mcp") {
|
|
4524
5215
|
try {
|
|
4525
|
-
const { runMcp } = await import("./mcp-
|
|
5216
|
+
const { runMcp } = await import("./mcp-2VMGYAUZ.js");
|
|
4526
5217
|
await runMcp(values.root ?? process.cwd());
|
|
4527
5218
|
} catch (error) {
|
|
4528
5219
|
process.stderr.write(
|
|
@@ -4965,6 +5656,23 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
4965
5656
|
);
|
|
4966
5657
|
return true;
|
|
4967
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
|
+
}
|
|
4968
5676
|
if (command.startsWith("brick ")) {
|
|
4969
5677
|
const path = need(file, "file");
|
|
4970
5678
|
const model = await readBrickModel(path, ldrawLibrary(values.library));
|
|
@@ -4982,6 +5690,20 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
4982
5690
|
if (!report.ok) process.exitCode = 2;
|
|
4983
5691
|
return true;
|
|
4984
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
|
+
}
|
|
4985
5707
|
if (command === "brick plan") {
|
|
4986
5708
|
const plan = brickPlan(model);
|
|
4987
5709
|
emit(
|
|
@@ -5004,6 +5726,25 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
|
|
|
5004
5726
|
if (!Number.isFinite(n)) throw new InputError(`--${name} must be a number.`);
|
|
5005
5727
|
return n;
|
|
5006
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
|
+
}
|
|
5007
5748
|
if (command === "brick remix") {
|
|
5008
5749
|
const seed = number("seed");
|
|
5009
5750
|
if (seed === void 0 || !Number.isInteger(seed))
|