ldr 0.0.0-alpha.1 → 0.2.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/LICENSE +21 -0
- package/README.md +119 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +876 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +717 -0
- package/dist/model.d.ts +44 -0
- package/dist/node/build-pack.d.ts +29 -0
- package/dist/node/compile.d.ts +43 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +1456 -0
- package/dist/node/library.d.ts +35 -0
- package/dist/node/resolver.d.ts +32 -0
- package/dist/pack-io.d.ts +2 -0
- package/dist/render.d.ts +43 -0
- package/dist/scene.d.ts +56 -0
- package/dist/spec.d.ts +73 -0
- package/package.json +56 -36
- package/lib/.DS_Store +0 -0
- package/lib/index.js +0 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import fs4 from "node:fs";
|
|
5
|
+
import zlib from "node:zlib";
|
|
6
|
+
|
|
7
|
+
// src/node/build-pack.ts
|
|
8
|
+
import fs3 from "node:fs";
|
|
9
|
+
import path3 from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/node/resolver.ts
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
|
|
15
|
+
var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
|
|
16
|
+
var norm = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
|
|
17
|
+
var threeName = (ref) => {
|
|
18
|
+
const n = norm(ref);
|
|
19
|
+
if (n.startsWith("s/"))
|
|
20
|
+
return "parts/" + n;
|
|
21
|
+
if (n.startsWith("48/"))
|
|
22
|
+
return "p/" + n;
|
|
23
|
+
return n;
|
|
24
|
+
};
|
|
25
|
+
function parseType1(line) {
|
|
26
|
+
const t = line.trim().split(/\s+/);
|
|
27
|
+
if (t[0] !== "1" || t.length < 15)
|
|
28
|
+
return null;
|
|
29
|
+
const v = t.slice(2, 14).map(Number);
|
|
30
|
+
return { color: t[1], m: v, name: t.slice(14).join(" ") };
|
|
31
|
+
}
|
|
32
|
+
function xf(m, p) {
|
|
33
|
+
return [
|
|
34
|
+
m[0] + m[3] * p[0] + m[4] * p[1] + m[5] * p[2],
|
|
35
|
+
m[1] + m[6] * p[0] + m[7] * p[1] + m[8] * p[2],
|
|
36
|
+
m[2] + m[9] * p[0] + m[10] * p[1] + m[11] * p[2]
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
function compose(a, b) {
|
|
40
|
+
const r = (i, j) => a[3 + i * 3] * b[3 + j] + a[3 + i * 3 + 1] * b[6 + j] + a[3 + i * 3 + 2] * b[9 + j];
|
|
41
|
+
const t = xf(a, [b[0], b[1], b[2]]);
|
|
42
|
+
return [t[0], t[1], t[2], r(0, 0), r(0, 1), r(0, 2), r(1, 0), r(1, 1), r(1, 2), r(2, 0), r(2, 1), r(2, 2)];
|
|
43
|
+
}
|
|
44
|
+
var IDENT = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
45
|
+
|
|
46
|
+
class Library {
|
|
47
|
+
root;
|
|
48
|
+
index;
|
|
49
|
+
text;
|
|
50
|
+
stats;
|
|
51
|
+
constructor(root) {
|
|
52
|
+
this.root = root;
|
|
53
|
+
this.index = new Map;
|
|
54
|
+
this.text = new Map;
|
|
55
|
+
this.stats = new Map;
|
|
56
|
+
for (const d of ["parts", "p", "models"])
|
|
57
|
+
this.#walk(path.join(root, d), d);
|
|
58
|
+
}
|
|
59
|
+
#walk(dir, rel) {
|
|
60
|
+
if (!fs.existsSync(dir))
|
|
61
|
+
return;
|
|
62
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
63
|
+
const r = rel + "/" + e.name;
|
|
64
|
+
if (e.isDirectory())
|
|
65
|
+
this.#walk(path.join(dir, e.name), r);
|
|
66
|
+
else
|
|
67
|
+
this.index.set(r.toLowerCase(), path.join(dir, e.name));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
resolve(ref) {
|
|
71
|
+
const n = norm(ref);
|
|
72
|
+
for (const pre of ["parts/", "p/", "models/"])
|
|
73
|
+
if (this.index.has(pre + n))
|
|
74
|
+
return pre + n;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
read(key) {
|
|
78
|
+
if (!this.text.has(key))
|
|
79
|
+
this.text.set(key, fs.readFileSync(this.index.get(key), "latin1"));
|
|
80
|
+
return this.text.get(key);
|
|
81
|
+
}
|
|
82
|
+
isPart(key) {
|
|
83
|
+
return key.startsWith("parts/") && !key.startsWith("parts/s/");
|
|
84
|
+
}
|
|
85
|
+
fileStats(key) {
|
|
86
|
+
if (this.stats.has(key))
|
|
87
|
+
return this.stats.get(key);
|
|
88
|
+
const s = { tris: 0, studTris: 0, underTris: 0, studs: [], min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity], missing: new Set };
|
|
89
|
+
this.stats.set(key, s);
|
|
90
|
+
const base = path.basename(key);
|
|
91
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
92
|
+
const grow = (p) => {
|
|
93
|
+
for (let i = 0;i < 3; i++) {
|
|
94
|
+
if (p[i] < s.min[i])
|
|
95
|
+
s.min[i] = p[i];
|
|
96
|
+
if (p[i] > s.max[i])
|
|
97
|
+
s.max[i] = p[i];
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
for (const line of this.read(key).split(/\r?\n/)) {
|
|
101
|
+
const t = line.trim();
|
|
102
|
+
const c = t[0];
|
|
103
|
+
if (c === "3" || c === "4") {
|
|
104
|
+
const v = t.split(/\s+/).slice(2).map(Number);
|
|
105
|
+
const nt = c === "3" ? 1 : 2;
|
|
106
|
+
s.tris += nt;
|
|
107
|
+
for (let i = 0;i + 2 < v.length; i += 3)
|
|
108
|
+
grow([v[i], v[i + 1], v[i + 2]]);
|
|
109
|
+
} else if (c === "1") {
|
|
110
|
+
const r = parseType1(t);
|
|
111
|
+
if (!r)
|
|
112
|
+
continue;
|
|
113
|
+
const ck = this.resolve(r.name);
|
|
114
|
+
if (!ck) {
|
|
115
|
+
s.missing.add(norm(r.name));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const cs = this.fileStats(ck);
|
|
119
|
+
s.tris += cs.tris;
|
|
120
|
+
s.studTris += cs.studTris;
|
|
121
|
+
s.underTris += cs.underTris;
|
|
122
|
+
for (const m of cs.missing)
|
|
123
|
+
s.missing.add(m);
|
|
124
|
+
if (TOP_STUD_RE.test(path.basename(ck)))
|
|
125
|
+
s.studs.push([r.m[0], r.m[1], r.m[2]]);
|
|
126
|
+
else
|
|
127
|
+
for (const p of cs.studs)
|
|
128
|
+
s.studs.push(xf(r.m, p));
|
|
129
|
+
if (isFinite(cs.min[0])) {
|
|
130
|
+
for (const x of [cs.min[0], cs.max[0]])
|
|
131
|
+
for (const y of [cs.min[1], cs.max[1]])
|
|
132
|
+
for (const z of [cs.min[2], cs.max[2]])
|
|
133
|
+
grow(xf(r.m, [x, y, z]));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (isTop) {
|
|
138
|
+
s.studTris = s.tris;
|
|
139
|
+
}
|
|
140
|
+
if (isUnder) {
|
|
141
|
+
s.underTris = s.tris;
|
|
142
|
+
}
|
|
143
|
+
return s;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function splitModel(text, fallbackName = "main.ldr") {
|
|
147
|
+
const files = new Map;
|
|
148
|
+
let cur = null, buf = [], main = null;
|
|
149
|
+
for (const line of text.split(/\r?\n/)) {
|
|
150
|
+
const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
|
|
151
|
+
if (m) {
|
|
152
|
+
if (cur)
|
|
153
|
+
files.set(cur, buf.join(`
|
|
154
|
+
`));
|
|
155
|
+
cur = norm(m[1]);
|
|
156
|
+
buf = [];
|
|
157
|
+
if (!main)
|
|
158
|
+
main = cur;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (/^\s*0\s+NOFILE/i.test(line))
|
|
162
|
+
continue;
|
|
163
|
+
if (!cur) {
|
|
164
|
+
cur = norm(fallbackName);
|
|
165
|
+
main = cur;
|
|
166
|
+
}
|
|
167
|
+
buf.push(line);
|
|
168
|
+
}
|
|
169
|
+
if (cur)
|
|
170
|
+
files.set(cur, buf.join(`
|
|
171
|
+
`));
|
|
172
|
+
return { files, main };
|
|
173
|
+
}
|
|
174
|
+
function flatten(model, lib, onInstance) {
|
|
175
|
+
const missing = new Map;
|
|
176
|
+
const walk = (name, parentM, parentColor, depth) => {
|
|
177
|
+
if (depth > 32)
|
|
178
|
+
return;
|
|
179
|
+
for (const line of model.files.get(name).split(`
|
|
180
|
+
`)) {
|
|
181
|
+
const r = parseType1(line);
|
|
182
|
+
if (!r)
|
|
183
|
+
continue;
|
|
184
|
+
const color = r.color === "16" ? parentColor : r.color;
|
|
185
|
+
const m = compose(parentM, r.m);
|
|
186
|
+
const n = norm(r.name);
|
|
187
|
+
if (model.files.has(n)) {
|
|
188
|
+
walk(n, m, color, depth + 1);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const key = lib.resolve(r.name);
|
|
192
|
+
if (!key) {
|
|
193
|
+
missing.set(n, (missing.get(n) || 0) + 1);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
onInstance({ part: key, ref: n, color, m });
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
walk(model.main, IDENT, "16", 0);
|
|
200
|
+
return { missing };
|
|
201
|
+
}
|
|
202
|
+
function collectDeps(lib, refs) {
|
|
203
|
+
const out = new Map;
|
|
204
|
+
const stack = [...refs];
|
|
205
|
+
while (stack.length) {
|
|
206
|
+
const ref = stack.pop();
|
|
207
|
+
const tn = threeName(ref);
|
|
208
|
+
if (out.has(tn))
|
|
209
|
+
continue;
|
|
210
|
+
const key = lib.resolve(ref);
|
|
211
|
+
if (!key)
|
|
212
|
+
continue;
|
|
213
|
+
out.set(tn, key);
|
|
214
|
+
for (const line of lib.read(key).split(/\r?\n/)) {
|
|
215
|
+
const r = parseType1(line);
|
|
216
|
+
if (r)
|
|
217
|
+
stack.push(r.name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/node/compile.ts
|
|
224
|
+
import fs2 from "node:fs";
|
|
225
|
+
import path2 from "node:path";
|
|
226
|
+
function loadColors(root) {
|
|
227
|
+
const map = new Map;
|
|
228
|
+
for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
|
|
229
|
+
const m = /!COLOUR\s+\S+\s+CODE\s+(\d+)\s+VALUE\s+#([0-9A-Fa-f]{6})/.exec(l);
|
|
230
|
+
if (m) {
|
|
231
|
+
const h = parseInt(m[2], 16);
|
|
232
|
+
map.set(m[1], [(h >> 16) / 255, (h >> 8 & 255) / 255, (h & 255) / 255]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return map;
|
|
236
|
+
}
|
|
237
|
+
var det3 = (m) => m[3] * (m[7] * m[11] - m[8] * m[10]) - m[4] * (m[6] * m[11] - m[8] * m[9]) + m[5] * (m[6] * m[10] - m[7] * m[9]);
|
|
238
|
+
|
|
239
|
+
class Compiler {
|
|
240
|
+
lib;
|
|
241
|
+
opts;
|
|
242
|
+
colors;
|
|
243
|
+
cache = new Map;
|
|
244
|
+
constructor(lib, opts) {
|
|
245
|
+
this.lib = lib;
|
|
246
|
+
this.opts = opts;
|
|
247
|
+
this.colors = loadColors(lib.root);
|
|
248
|
+
}
|
|
249
|
+
resolve(ref, isStud) {
|
|
250
|
+
const key = this.lib.resolve(ref);
|
|
251
|
+
if (!key)
|
|
252
|
+
return null;
|
|
253
|
+
const low = isStud ? this.opts.studs === "low" : this.opts.prims === "low";
|
|
254
|
+
if (low && key.startsWith("p/") && !key.startsWith("p/8/") && !key.startsWith("p/48/")) {
|
|
255
|
+
const alt = "p/8/" + key.slice(2);
|
|
256
|
+
if (this.lib.index.has(alt))
|
|
257
|
+
return alt;
|
|
258
|
+
}
|
|
259
|
+
if (low && key.startsWith("p/48/")) {
|
|
260
|
+
const alt = "p/8/" + key.slice(5);
|
|
261
|
+
if (this.lib.index.has(alt))
|
|
262
|
+
return alt;
|
|
263
|
+
const a2 = "p/" + key.slice(5);
|
|
264
|
+
if (this.lib.index.has(a2))
|
|
265
|
+
return a2;
|
|
266
|
+
}
|
|
267
|
+
return key;
|
|
268
|
+
}
|
|
269
|
+
compile(key, separateStuds = this.opts.separateStuds) {
|
|
270
|
+
const ck = key + (separateStuds ? "" : "#nostudsep");
|
|
271
|
+
if (this.cache.has(ck))
|
|
272
|
+
return this.cache.get(ck);
|
|
273
|
+
const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
|
|
274
|
+
const colorOf = (code) => code === "16" ? [1, 1, 1, 0] : [...this.colors.get(code) || (code.startsWith("0x2") ? [parseInt(code.slice(3, 5), 16) / 255, parseInt(code.slice(5, 7), 16) / 255, parseInt(code.slice(7, 9), 16) / 255] : [1, 0, 1]), 1];
|
|
275
|
+
const walk = (fk, M, color, invert, target, depth) => {
|
|
276
|
+
if (depth > 40)
|
|
277
|
+
return;
|
|
278
|
+
let ccw = true, certified = false, invertNext = false;
|
|
279
|
+
for (const raw of this.lib.read(fk).split(/\r?\n/)) {
|
|
280
|
+
const t = raw.trim();
|
|
281
|
+
if (!t)
|
|
282
|
+
continue;
|
|
283
|
+
const c = t[0];
|
|
284
|
+
if (c === "0") {
|
|
285
|
+
if (/^0\s+BFC/.test(t)) {
|
|
286
|
+
if (/CERTIFY/.test(t) && !/NOCERTIFY/.test(t))
|
|
287
|
+
certified = true;
|
|
288
|
+
if (/\bCW\b/.test(t))
|
|
289
|
+
ccw = false;
|
|
290
|
+
if (/\bCCW\b/.test(t))
|
|
291
|
+
ccw = true;
|
|
292
|
+
if (/INVERTNEXT/.test(t))
|
|
293
|
+
invertNext = true;
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (c === "1") {
|
|
298
|
+
const r = parseType1(t);
|
|
299
|
+
if (!r)
|
|
300
|
+
continue;
|
|
301
|
+
const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
|
|
302
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
303
|
+
const ck2 = this.resolve(r.name, isTop);
|
|
304
|
+
if (!ck2) {
|
|
305
|
+
invertNext = false;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const childColor = r.color === "16" ? color : r.color;
|
|
309
|
+
const Mc = compose(M, r.m);
|
|
310
|
+
const inv = invert !== invertNext !== det3(r.m) < 0;
|
|
311
|
+
invertNext = false;
|
|
312
|
+
if (isTop && separateStuds) {
|
|
313
|
+
out.studs.push({ kind: ck2, color: childColor, m: Mc });
|
|
314
|
+
growBox(out.bbox, Mc, this.compile(ck2, false).bbox);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
walk(ck2, Mc, childColor, inv, isUnder ? "under" : target, depth + 1);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const tok = t.split(/\s+/);
|
|
321
|
+
if (c === "2") {
|
|
322
|
+
const v = tok.slice(2, 8).map(Number);
|
|
323
|
+
out.edges.push(...xf(M, v.slice(0, 3)), ...xf(M, v.slice(3, 6)));
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (c === "5") {
|
|
327
|
+
out.condLines++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (c === "3" || c === "4") {
|
|
331
|
+
const code = tok[1] === "16" ? color : tok[1];
|
|
332
|
+
const n = c === "3" ? 3 : 4;
|
|
333
|
+
const v = tok.slice(2, 2 + n * 3).map(Number);
|
|
334
|
+
const pts = [];
|
|
335
|
+
for (let i = 0;i < n; i++)
|
|
336
|
+
pts.push(xf(M, v.slice(i * 3, i * 3 + 3)));
|
|
337
|
+
for (const p of pts)
|
|
338
|
+
grow(out.bbox, p);
|
|
339
|
+
let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
|
|
340
|
+
if (!ccw !== invert)
|
|
341
|
+
order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
|
|
342
|
+
const rgba = colorOf(code);
|
|
343
|
+
const b = out[target];
|
|
344
|
+
for (const i of order) {
|
|
345
|
+
b.pos.push(...pts[i]);
|
|
346
|
+
b.col.push(...rgba);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!certified)
|
|
351
|
+
out.uncertified = true;
|
|
352
|
+
};
|
|
353
|
+
walk(key, IDENT, "16", false, "body", 0);
|
|
354
|
+
const [x0, y0, z0, x1, y1, z1] = out.bbox;
|
|
355
|
+
let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
|
|
356
|
+
for (const b of [out.body, out.under])
|
|
357
|
+
for (let i = 0;i < b.pos.length; i += 3)
|
|
358
|
+
if (b.pos[i + 1] > y1 - 0.5) {
|
|
359
|
+
ax0 = Math.min(ax0, b.pos[i]);
|
|
360
|
+
ax1 = Math.max(ax1, b.pos[i]);
|
|
361
|
+
az0 = Math.min(az0, b.pos[i + 2]);
|
|
362
|
+
az1 = Math.max(az1, b.pos[i + 2]);
|
|
363
|
+
}
|
|
364
|
+
out.fullBottom = isFinite(x0) && ax0 <= x0 + 0.5 && ax1 >= x1 - 0.5 && az0 <= z0 + 0.5 && az1 >= z1 - 0.5;
|
|
365
|
+
if (separateStuds && isFinite(x0))
|
|
366
|
+
out.bottomMask = enclosureMask(out);
|
|
367
|
+
this.cache.set(ck, out);
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function enclosureMask(c) {
|
|
372
|
+
const [x0, , z0, x1, y1, z1] = c.bbox;
|
|
373
|
+
const nx = Math.round((x1 - x0) / 20), nz = Math.round((z1 - z0) / 20);
|
|
374
|
+
if (!nx || !nz || nx * nz > 4096)
|
|
375
|
+
return;
|
|
376
|
+
const tris = [];
|
|
377
|
+
for (const b of [c.body, c.under])
|
|
378
|
+
for (let i = 0;i < b.pos.length; i += 9) {
|
|
379
|
+
const t = b.pos.slice(i, i + 9);
|
|
380
|
+
if (Math.max(t[1], t[4], t[7]) < y1 - 6)
|
|
381
|
+
continue;
|
|
382
|
+
tris.push(t);
|
|
383
|
+
}
|
|
384
|
+
let yRay = y1 - 2;
|
|
385
|
+
const bits = [];
|
|
386
|
+
const BIN = 4;
|
|
387
|
+
const buckets = [new Map, new Map];
|
|
388
|
+
for (const t of tris) {
|
|
389
|
+
if (Math.min(t[1], t[4], t[7]) > y1 - 0.5 || Math.max(t[1], t[4], t[7]) < y1 - 4)
|
|
390
|
+
continue;
|
|
391
|
+
for (const [ai, o] of [[0, 2], [1, 0]]) {
|
|
392
|
+
const lo = Math.floor(Math.min(t[o], t[3 + o], t[6 + o]) / BIN), hi = Math.floor(Math.max(t[o], t[3 + o], t[6 + o]) / BIN);
|
|
393
|
+
for (let k = lo;k <= hi; k++)
|
|
394
|
+
(buckets[ai].get(k) || buckets[ai].set(k, []).get(k)).push(t);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const hit = (px, pz, axis, sign) => {
|
|
398
|
+
const o = axis === 0 ? 2 : 0;
|
|
399
|
+
const pv = axis === 0 ? pz : px;
|
|
400
|
+
for (const t of buckets[axis === 0 ? 0 : 1].get(Math.floor(pv / BIN)) || []) {
|
|
401
|
+
const a = [t[1], t[o]], b = [t[4], t[3 + o]], d = [t[7], t[6 + o]];
|
|
402
|
+
const den = (b[1] - d[1]) * (a[0] - d[0]) + (d[0] - b[0]) * (a[1] - d[1]);
|
|
403
|
+
if (Math.abs(den) < 0.000000001)
|
|
404
|
+
continue;
|
|
405
|
+
const y = yRay;
|
|
406
|
+
const w1 = ((b[1] - d[1]) * (y - d[0]) + (d[0] - b[0]) * (pv - d[1])) / den, w2 = ((d[1] - a[1]) * (y - d[0]) + (a[0] - d[0]) * (pv - d[1])) / den, w3 = 1 - w1 - w2;
|
|
407
|
+
if (w1 < -0.000001 || w2 < -0.000001 || w3 < -0.000001)
|
|
408
|
+
continue;
|
|
409
|
+
const along = w1 * t[axis] + w2 * t[3 + axis] + w3 * t[6 + axis];
|
|
410
|
+
const start = axis === 0 ? px : pz;
|
|
411
|
+
if ((along - start) * sign > 0.01)
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
};
|
|
416
|
+
for (let a = 0;a < nx; a++)
|
|
417
|
+
for (let b = 0;b < nz; b++) {
|
|
418
|
+
const px = x0 + 10 + 20 * a, pz = z0 + 10 + 20 * b;
|
|
419
|
+
let ok = true;
|
|
420
|
+
for (const yy of [y1 - 1, y1 - 3.5]) {
|
|
421
|
+
yRay = yy;
|
|
422
|
+
for (const [ox, oz] of [[0, 0], [5, 5], [-5, 5], [5, -5], [-5, -5]]) {
|
|
423
|
+
const qx = px + ox, qz = pz + oz;
|
|
424
|
+
if (!(hit(qx, qz, 0, 1) && hit(qx, qz, 0, -1) && hit(qx, qz, 2, 1) && hit(qx, qz, 2, -1))) {
|
|
425
|
+
ok = false;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!ok)
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
bits.push(ok ? 1 : 0);
|
|
433
|
+
}
|
|
434
|
+
return { nx, nz, bits };
|
|
435
|
+
}
|
|
436
|
+
function grow(b, p) {
|
|
437
|
+
for (let i = 0;i < 3; i++) {
|
|
438
|
+
if (p[i] < b[i])
|
|
439
|
+
b[i] = p[i];
|
|
440
|
+
if (p[i] > b[i + 3])
|
|
441
|
+
b[i + 3] = p[i];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function growBox(b, M, cb) {
|
|
445
|
+
if (!isFinite(cb[0]))
|
|
446
|
+
return;
|
|
447
|
+
for (const x of [cb[0], cb[3]])
|
|
448
|
+
for (const y of [cb[1], cb[4]])
|
|
449
|
+
for (const z of [cb[2], cb[5]])
|
|
450
|
+
grow(b, xf(M, [x, y, z]));
|
|
451
|
+
}
|
|
452
|
+
function writePack(file, parts, studKinds, weld, extra) {
|
|
453
|
+
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
|
|
454
|
+
}
|
|
455
|
+
function encodePack(parts, studKinds, weld, extra) {
|
|
456
|
+
const chunks = [];
|
|
457
|
+
let off = 0;
|
|
458
|
+
const put = (a) => {
|
|
459
|
+
const pad = (4 - off % 4) % 4;
|
|
460
|
+
if (pad) {
|
|
461
|
+
chunks.push(new Uint8Array(pad));
|
|
462
|
+
off += pad;
|
|
463
|
+
}
|
|
464
|
+
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
|
|
465
|
+
chunks.push(a);
|
|
466
|
+
off += a.byteLength;
|
|
467
|
+
return r;
|
|
468
|
+
};
|
|
469
|
+
const bucket = (b) => {
|
|
470
|
+
const n = b.pos.length / 3;
|
|
471
|
+
if (!n)
|
|
472
|
+
return null;
|
|
473
|
+
const nrm = new Float32Array(n * 3);
|
|
474
|
+
for (let i = 0;i < n; i += 3) {
|
|
475
|
+
const p = b.pos;
|
|
476
|
+
const a = i * 3;
|
|
477
|
+
const ux = p[a + 3] - p[a], uy = p[a + 4] - p[a + 1], uz = p[a + 5] - p[a + 2], vx = p[a + 6] - p[a], vy = p[a + 7] - p[a + 1], vz = p[a + 8] - p[a + 2];
|
|
478
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
479
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
480
|
+
nx /= l;
|
|
481
|
+
ny /= l;
|
|
482
|
+
nz /= l;
|
|
483
|
+
for (let k = 0;k < 3; k++) {
|
|
484
|
+
nrm[(i + k) * 3] = nx;
|
|
485
|
+
nrm[(i + k) * 3 + 1] = ny;
|
|
486
|
+
nrm[(i + k) * 3 + 2] = nz;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (!weld)
|
|
490
|
+
return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
|
|
491
|
+
const map = new Map;
|
|
492
|
+
const P = [], N = [], C = [], I = [];
|
|
493
|
+
for (let i = 0;i < n; i++) {
|
|
494
|
+
const k = `${b.pos[i * 3].toFixed(3)},${b.pos[i * 3 + 1].toFixed(3)},${b.pos[i * 3 + 2].toFixed(3)},${nrm[i * 3].toFixed(3)},${nrm[i * 3 + 1].toFixed(3)},${nrm[i * 3 + 2].toFixed(3)},${b.col.slice(i * 4, i * 4 + 4).join()}`;
|
|
495
|
+
let j = map.get(k);
|
|
496
|
+
if (j === undefined) {
|
|
497
|
+
j = P.length / 3;
|
|
498
|
+
map.set(k, j);
|
|
499
|
+
P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
|
|
500
|
+
N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
|
|
501
|
+
C.push(...b.col.slice(i * 4, i * 4 + 4));
|
|
502
|
+
}
|
|
503
|
+
I.push(j);
|
|
504
|
+
}
|
|
505
|
+
const vcount = P.length / 3;
|
|
506
|
+
return { pos: put(new Float32Array(P)), nrm: put(new Float32Array(N)), col: put(new Float32Array(C)), idx: put(vcount < 65536 ? new Uint16Array(I) : new Uint32Array(I)), tris: n / 3 };
|
|
507
|
+
};
|
|
508
|
+
const enc = (c) => ({ body: bucket(c.body), under: bucket(c.under), edges: c.edges.length ? put(new Float32Array(c.edges)) : null, cond: c.condLines, studs: c.studs.map((s) => [s.kind, s.color, ...s.m.map((x) => +x.toFixed(4))]), bbox: c.bbox, fullBottom: c.fullBottom, uncert: !!c.uncertified, mask: c.bottomMask ? [c.bottomMask.nx, c.bottomMask.nz, c.bottomMask.bits.join("")] : null });
|
|
509
|
+
const header = { version: 1, weld, parts: Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), studKinds: Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)])), ...extra };
|
|
510
|
+
const hj = new TextEncoder().encode(JSON.stringify(header));
|
|
511
|
+
const hpad = (4 - (4 + hj.length) % 4) % 4;
|
|
512
|
+
const head = new Uint8Array(4 + hj.length + hpad);
|
|
513
|
+
new DataView(head.buffer).setUint32(0, hj.length + hpad, true);
|
|
514
|
+
head.set(hj, 4);
|
|
515
|
+
head.fill(32, 4 + hj.length);
|
|
516
|
+
return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/spec.ts
|
|
520
|
+
var norm2 = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
521
|
+
function parseColor(tok) {
|
|
522
|
+
if (/^0x/i.test(tok))
|
|
523
|
+
return parseInt(tok, 16);
|
|
524
|
+
return +tok;
|
|
525
|
+
}
|
|
526
|
+
var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
|
|
527
|
+
function parse(src, name = "main.ldr") {
|
|
528
|
+
const files = new Map;
|
|
529
|
+
let cur = null;
|
|
530
|
+
let main = "";
|
|
531
|
+
const start = (n) => {
|
|
532
|
+
cur = { name: norm2(n), cmds: [] };
|
|
533
|
+
files.set(cur.name, cur);
|
|
534
|
+
if (!main)
|
|
535
|
+
main = cur.name;
|
|
536
|
+
};
|
|
537
|
+
for (const raw of src.split(/\r?\n/)) {
|
|
538
|
+
const line = raw.trim();
|
|
539
|
+
if (!line)
|
|
540
|
+
continue;
|
|
541
|
+
const tk = line.split(/\s+/);
|
|
542
|
+
const t = +tk[0];
|
|
543
|
+
if (t === 0) {
|
|
544
|
+
const text = line.slice(1).trim();
|
|
545
|
+
if (tk[1] === "FILE") {
|
|
546
|
+
start(text.slice(5));
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (tk[1] === "NOFILE") {
|
|
550
|
+
cur = null;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (!cur)
|
|
554
|
+
start(name);
|
|
555
|
+
const c = { t: 0, text };
|
|
556
|
+
if (tk[1] === "STEP")
|
|
557
|
+
c.step = true;
|
|
558
|
+
else if (tk[1] === "BFC")
|
|
559
|
+
c.bfc = tk.slice(2).join(" ");
|
|
560
|
+
else if (tk[1] === "ROTSTEP") {
|
|
561
|
+
c.meta = "ROTSTEP";
|
|
562
|
+
if ((tk[2] ?? "").toUpperCase() === "END")
|
|
563
|
+
c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
|
|
564
|
+
else {
|
|
565
|
+
const mode = (tk[5] ?? "REL").toUpperCase();
|
|
566
|
+
c.rotstep = { x: +tk[2] || 0, y: +tk[3] || 0, z: +tk[4] || 0, mode: ["REL", "ABS", "ADD"].includes(mode) ? mode : "REL" };
|
|
567
|
+
}
|
|
568
|
+
} else if (tk[1] === "BUFEXCHG") {
|
|
569
|
+
c.meta = "BUFEXCHG";
|
|
570
|
+
const op = (tk[2] ?? "").toUpperCase();
|
|
571
|
+
c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
|
|
572
|
+
} else if (tk[1]?.startsWith("!"))
|
|
573
|
+
c.meta = tk[1];
|
|
574
|
+
const moved = text.match(MOVED_RE);
|
|
575
|
+
if (moved && !cur.movedTo) {
|
|
576
|
+
let target = moved[1].trim().replace(/\\/g, "/");
|
|
577
|
+
if (!/\.(dat|ldr|mpd)$/i.test(target))
|
|
578
|
+
target += ".dat";
|
|
579
|
+
cur.movedTo = norm2(target);
|
|
580
|
+
}
|
|
581
|
+
cur.cmds.push(c);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (!cur)
|
|
585
|
+
start(name);
|
|
586
|
+
const color = parseColor(tk[1]);
|
|
587
|
+
const nums = tk.slice(2).map(Number);
|
|
588
|
+
if (t === 1)
|
|
589
|
+
cur.cmds.push({ t: 1, color, m: nums.slice(0, 12), file: norm2(tk.slice(14).join(" ")) });
|
|
590
|
+
else if (t === 2)
|
|
591
|
+
cur.cmds.push({ t: 2, color, p: nums.slice(0, 6) });
|
|
592
|
+
else if (t === 3)
|
|
593
|
+
cur.cmds.push({ t: 3, color, p: nums.slice(0, 9) });
|
|
594
|
+
else if (t === 4)
|
|
595
|
+
cur.cmds.push({ t: 4, color, p: nums.slice(0, 12) });
|
|
596
|
+
else if (t === 5)
|
|
597
|
+
cur.cmds.push({ t: 5, color, p: nums.slice(0, 12) });
|
|
598
|
+
}
|
|
599
|
+
return { main, files };
|
|
600
|
+
}
|
|
601
|
+
function mul(a, b) {
|
|
602
|
+
const [ax, ay, az, a0, a1, a2, a3, a4, a5, a6, a7, a8] = a, [bx, by, bz, b0, b1, b2, b3, b4, b5, b6, b7, b8] = b;
|
|
603
|
+
return [
|
|
604
|
+
a0 * bx + a1 * by + a2 * bz + ax,
|
|
605
|
+
a3 * bx + a4 * by + a5 * bz + ay,
|
|
606
|
+
a6 * bx + a7 * by + a8 * bz + az,
|
|
607
|
+
a0 * b0 + a1 * b3 + a2 * b6,
|
|
608
|
+
a0 * b1 + a1 * b4 + a2 * b7,
|
|
609
|
+
a0 * b2 + a1 * b5 + a2 * b8,
|
|
610
|
+
a3 * b0 + a4 * b3 + a5 * b6,
|
|
611
|
+
a3 * b1 + a4 * b4 + a5 * b7,
|
|
612
|
+
a3 * b2 + a4 * b5 + a5 * b8,
|
|
613
|
+
a6 * b0 + a7 * b3 + a8 * b6,
|
|
614
|
+
a6 * b1 + a7 * b4 + a8 * b7,
|
|
615
|
+
a6 * b2 + a7 * b5 + a8 * b8
|
|
616
|
+
];
|
|
617
|
+
}
|
|
618
|
+
var I = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
619
|
+
var isDirectColor = (c) => c > 16777215;
|
|
620
|
+
function smoothNormals(pos, edges, cond, creaseDeg = 35) {
|
|
621
|
+
const q = (i, a) => `${a[i].toFixed(2)},${a[i + 1].toFixed(2)},${a[i + 2].toFixed(2)}`;
|
|
622
|
+
const ek = (a, b) => a < b ? a + "|" + b : b + "|" + a;
|
|
623
|
+
const hard = new Set, soft = new Set;
|
|
624
|
+
for (let i = 0;i < edges.length; i += 6)
|
|
625
|
+
hard.add(ek(q(i, edges), q(i + 3, edges)));
|
|
626
|
+
const stride = cond.length > 0 && cond.length % 12 === 0 ? 12 : 6;
|
|
627
|
+
for (let i = 0;i < cond.length; i += stride)
|
|
628
|
+
soft.add(ek(q(i, cond), q(i + 3, cond)));
|
|
629
|
+
const nT = pos.length / 9, fn = [], vk = [];
|
|
630
|
+
for (let t = 0;t < nT; t++) {
|
|
631
|
+
const a = t * 9;
|
|
632
|
+
const u = [pos[a + 3] - pos[a], pos[a + 4] - pos[a + 1], pos[a + 5] - pos[a + 2]], v = [pos[a + 6] - pos[a], pos[a + 7] - pos[a + 1], pos[a + 8] - pos[a + 2]];
|
|
633
|
+
const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
|
|
634
|
+
const l = Math.hypot(...n) || 1;
|
|
635
|
+
fn.push(n.map((x) => x / l));
|
|
636
|
+
for (let k = 0;k < 3; k++)
|
|
637
|
+
vk.push(q(a + k * 3, pos));
|
|
638
|
+
}
|
|
639
|
+
const byV = new Map;
|
|
640
|
+
vk.forEach((k, i) => (byV.get(k) ?? byV.set(k, []).get(k)).push(i));
|
|
641
|
+
const edgeFaces = new Map;
|
|
642
|
+
for (let t = 0;t < nT; t++)
|
|
643
|
+
for (let k = 0;k < 3; k++) {
|
|
644
|
+
const e = ek(vk[t * 3 + k], vk[t * 3 + (k + 1) % 3]);
|
|
645
|
+
(edgeFaces.get(e) ?? edgeFaces.set(e, []).get(e)).push(t);
|
|
646
|
+
}
|
|
647
|
+
const cosC = Math.cos(creaseDeg * Math.PI / 180), out = new Float32Array(pos.length);
|
|
648
|
+
const joinable = (e, a, b) => {
|
|
649
|
+
if (hard.has(e))
|
|
650
|
+
return false;
|
|
651
|
+
const d = fn[a][0] * fn[b][0] + fn[a][1] * fn[b][1] + fn[a][2] * fn[b][2];
|
|
652
|
+
return soft.has(e) ? d > -0.2 : d > cosC;
|
|
653
|
+
};
|
|
654
|
+
for (const [key, corners] of byV) {
|
|
655
|
+
const faces = corners.map((c) => c / 3 | 0), par = faces.map((_, i) => i);
|
|
656
|
+
const f = (i) => par[i] === i ? i : par[i] = f(par[i]);
|
|
657
|
+
for (let i = 0;i < faces.length; i++)
|
|
658
|
+
for (let j = i + 1;j < faces.length; j++) {
|
|
659
|
+
const A = faces[i], B = faces[j];
|
|
660
|
+
const va = [0, 1, 2].map((k) => vk[A * 3 + k]), vb = new Set([0, 1, 2].map((k) => vk[B * 3 + k]));
|
|
661
|
+
for (const o of va)
|
|
662
|
+
if (o !== key && vb.has(o)) {
|
|
663
|
+
const e = ek(key, o);
|
|
664
|
+
if ((edgeFaces.get(e)?.length ?? 0) >= 2 && joinable(e, A, B))
|
|
665
|
+
par[f(i)] = f(j);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const acc = new Map;
|
|
669
|
+
faces.forEach((F, i) => {
|
|
670
|
+
const r = f(i), s = acc.get(r) ?? [0, 0, 0];
|
|
671
|
+
s[0] += fn[F][0];
|
|
672
|
+
s[1] += fn[F][1];
|
|
673
|
+
s[2] += fn[F][2];
|
|
674
|
+
acc.set(r, s);
|
|
675
|
+
});
|
|
676
|
+
corners.forEach((c, i) => {
|
|
677
|
+
const s = acc.get(f(i)), l = Math.hypot(...s) || 1;
|
|
678
|
+
out[c * 3] = s[0] / l;
|
|
679
|
+
out[c * 3 + 1] = s[1] / l;
|
|
680
|
+
out[c * 3 + 2] = s[2] / l;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/model.ts
|
|
687
|
+
function flatten2(doc, opts = {}) {
|
|
688
|
+
const out = [];
|
|
689
|
+
const maxDepth = opts.maxDepth ?? 64;
|
|
690
|
+
const walk = (d, fname, m, color, path, stepPath, depth) => {
|
|
691
|
+
const f = d.files.get(fname);
|
|
692
|
+
if (!f || depth > maxDepth)
|
|
693
|
+
return;
|
|
694
|
+
let s = 0;
|
|
695
|
+
for (const c of f.cmds) {
|
|
696
|
+
if (c.t === 0 && c.step)
|
|
697
|
+
s++;
|
|
698
|
+
if (c.t !== 1)
|
|
699
|
+
continue;
|
|
700
|
+
const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
|
|
701
|
+
if (d.files.has(c.file)) {
|
|
702
|
+
walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const ext = opts.resolveModel?.(c.file);
|
|
706
|
+
if (ext) {
|
|
707
|
+
walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
walk(doc, doc.main, I, 16, doc.main, [], 0);
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
function parseModel(text, name = "main.ldr", opts) {
|
|
717
|
+
return flatten2(parse(text, name), opts);
|
|
718
|
+
}
|
|
719
|
+
function sequenceSteps(insts) {
|
|
720
|
+
const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
|
|
721
|
+
const uniq = [...new Set(insts.map(key))].sort();
|
|
722
|
+
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
723
|
+
const stepOf = new Int32Array(insts.length);
|
|
724
|
+
insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
|
|
725
|
+
return { count: uniq.length, stepOf };
|
|
726
|
+
}
|
|
727
|
+
function parseLDConfig(text) {
|
|
728
|
+
const out = new Map;
|
|
729
|
+
for (const l of text.split(/\r?\n/)) {
|
|
730
|
+
const m = /!COLOUR\s+(\S+)\s+CODE\s+(\d+)\s+VALUE\s+(#[0-9A-Fa-f]{6})\s+EDGE\s+(\S+)(?:.*?ALPHA\s+(\d+))?/.exec(l);
|
|
731
|
+
if (m)
|
|
732
|
+
out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/node/build-pack.ts
|
|
738
|
+
function buildPack(o) {
|
|
739
|
+
const lib = new Library(o.ldrawRoot);
|
|
740
|
+
const refs = new Map;
|
|
741
|
+
const missing = new Set;
|
|
742
|
+
for (const m of o.models ?? []) {
|
|
743
|
+
const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
|
|
744
|
+
const r = flatten(splitModel(text, name), lib, (i) => refs.set(i.ref, i.part));
|
|
745
|
+
for (const k of r.missing.keys())
|
|
746
|
+
missing.add(k);
|
|
747
|
+
}
|
|
748
|
+
for (const p of o.parts ?? []) {
|
|
749
|
+
const k = lib.resolve(p);
|
|
750
|
+
if (k)
|
|
751
|
+
refs.set(norm(p), k);
|
|
752
|
+
else
|
|
753
|
+
missing.add(norm(p));
|
|
754
|
+
}
|
|
755
|
+
if (o.allParts) {
|
|
756
|
+
for (const k of lib.index.keys())
|
|
757
|
+
if (lib.isPart(k) && /\.dat$/.test(k))
|
|
758
|
+
refs.set(k.slice(6), k);
|
|
759
|
+
}
|
|
760
|
+
const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
|
|
761
|
+
const parts = new Map, kinds = new Map;
|
|
762
|
+
let n = 0;
|
|
763
|
+
for (const [ref, key] of refs) {
|
|
764
|
+
try {
|
|
765
|
+
const c = comp.compile(key);
|
|
766
|
+
parts.set(ref, c);
|
|
767
|
+
for (const s of c.studs)
|
|
768
|
+
if (!kinds.has(s.kind))
|
|
769
|
+
kinds.set(s.kind, comp.compile(s.kind, false));
|
|
770
|
+
} catch {
|
|
771
|
+
missing.add(ref);
|
|
772
|
+
}
|
|
773
|
+
o.onProgress?.(++n, refs.size);
|
|
774
|
+
}
|
|
775
|
+
let extra;
|
|
776
|
+
if (o.colors !== false) {
|
|
777
|
+
const cfg = path3.join(o.ldrawRoot, "LDConfig.ldr");
|
|
778
|
+
if (fs3.existsSync(cfg))
|
|
779
|
+
extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
|
|
780
|
+
}
|
|
781
|
+
return { data: encodePack(parts, kinds, comp.opts.weld, extra), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/cli.ts
|
|
785
|
+
var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
|
|
786
|
+
|
|
787
|
+
Usage:
|
|
788
|
+
ldr pack --ldraw <dir> -o <out.bin[.zst|.gz]> [model.ldr ...] [options]
|
|
789
|
+
|
|
790
|
+
Options:
|
|
791
|
+
--ldraw <dir> LDraw library root (contains LDConfig.ldr, parts/, p/). Download complete.zip from https://library.ldraw.org
|
|
792
|
+
-o, --out <file> Output file. Extension .zst / .gz compresses (recommended for serving).
|
|
793
|
+
--part <ref> Include a part (repeatable), e.g. --part 3001.dat
|
|
794
|
+
--all-parts Include every part in the library (large)
|
|
795
|
+
--studs low|full Stud detail (default low)
|
|
796
|
+
--prims full|low Other primitive detail (default full)
|
|
797
|
+
--no-weld Don't index/weld vertices
|
|
798
|
+
--no-colors Don't embed the LDConfig colour table
|
|
799
|
+
`;
|
|
800
|
+
var argv = process.argv.slice(2);
|
|
801
|
+
if (argv[0] !== "pack" || argv.includes("-h") || argv.includes("--help")) {
|
|
802
|
+
console.log(HELP);
|
|
803
|
+
process.exit(argv[0] === "pack" || argv.includes("--help") || argv.includes("-h") ? 0 : 1);
|
|
804
|
+
}
|
|
805
|
+
var a = argv.slice(1);
|
|
806
|
+
var models = [];
|
|
807
|
+
var parts = [];
|
|
808
|
+
var ldraw = "";
|
|
809
|
+
var out = "";
|
|
810
|
+
var studs = "low";
|
|
811
|
+
var prims = "full";
|
|
812
|
+
var weld = true;
|
|
813
|
+
var colors = true;
|
|
814
|
+
var all = false;
|
|
815
|
+
for (let i = 0;i < a.length; i++) {
|
|
816
|
+
const k = a[i];
|
|
817
|
+
const v = () => {
|
|
818
|
+
const x = a[++i];
|
|
819
|
+
if (x === undefined) {
|
|
820
|
+
console.error(`missing value for ${k}`);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
return x;
|
|
824
|
+
};
|
|
825
|
+
if (k === "--ldraw")
|
|
826
|
+
ldraw = v();
|
|
827
|
+
else if (k === "-o" || k === "--out")
|
|
828
|
+
out = v();
|
|
829
|
+
else if (k === "--part")
|
|
830
|
+
parts.push(v());
|
|
831
|
+
else if (k === "--studs")
|
|
832
|
+
studs = v();
|
|
833
|
+
else if (k === "--prims")
|
|
834
|
+
prims = v();
|
|
835
|
+
else if (k === "--no-weld")
|
|
836
|
+
weld = false;
|
|
837
|
+
else if (k === "--no-colors")
|
|
838
|
+
colors = false;
|
|
839
|
+
else if (k === "--all-parts")
|
|
840
|
+
all = true;
|
|
841
|
+
else if (k.startsWith("-")) {
|
|
842
|
+
console.error(`unknown option ${k}
|
|
843
|
+
|
|
844
|
+
${HELP}`);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
} else
|
|
847
|
+
models.push(k);
|
|
848
|
+
}
|
|
849
|
+
if (!ldraw || !out) {
|
|
850
|
+
console.error(HELP);
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
if (!fs4.existsSync(ldraw + "/LDConfig.ldr")) {
|
|
854
|
+
console.error(`${ldraw} does not look like an LDraw library (no LDConfig.ldr)`);
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
var t0 = performance.now();
|
|
858
|
+
var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors });
|
|
859
|
+
var data = r.data;
|
|
860
|
+
if (out.endsWith(".gz"))
|
|
861
|
+
data = zlib.gzipSync(data, { level: 9 });
|
|
862
|
+
else if (out.endsWith(".zst")) {
|
|
863
|
+
const B = globalThis.Bun, z = zlib;
|
|
864
|
+
if (B?.zstdCompressSync)
|
|
865
|
+
data = B.zstdCompressSync(data, { level: 19 });
|
|
866
|
+
else if (z.zstdCompressSync)
|
|
867
|
+
data = z.zstdCompressSync(data, { params: { [z.constants.ZSTD_c_compressionLevel]: 19 } });
|
|
868
|
+
else {
|
|
869
|
+
console.error("zstd output needs Bun or Node >= 22.15 (try `bunx ldr pack ...` or use a .gz output)");
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
fs4.writeFileSync(out, data);
|
|
874
|
+
console.log(`${out}: ${r.parts} parts, ${r.studKinds} stud kinds, ${(data.length / 1048576).toFixed(2)} MB (raw ${(r.data.length / 1048576).toFixed(2)} MB), ${Math.round(performance.now() - t0)} ms`);
|
|
875
|
+
if (r.missing.length)
|
|
876
|
+
console.warn(`missing ${r.missing.length} refs: ${r.missing.slice(0, 20).join(", ")}${r.missing.length > 20 ? " …" : ""}`);
|