ldr 0.0.0-alpha.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1472 @@
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
+ const data = new Map;
149
+ let dataCur = null;
150
+ let cur = null, buf = [], main = null;
151
+ let skip = false, sawBlock = false;
152
+ const flush = () => {
153
+ if (cur)
154
+ files.set(cur, buf.join(`
155
+ `));
156
+ cur = null;
157
+ buf = [];
158
+ };
159
+ const firstBlock = () => {
160
+ if (sawBlock)
161
+ return;
162
+ sawBlock = true;
163
+ files.clear();
164
+ cur = null;
165
+ buf = [];
166
+ main = null;
167
+ };
168
+ for (const line of text.split(/\r?\n/)) {
169
+ const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
170
+ if (m) {
171
+ firstBlock();
172
+ flush();
173
+ skip = false;
174
+ dataCur = null;
175
+ cur = norm(m[1]);
176
+ if (!main)
177
+ main = cur;
178
+ continue;
179
+ }
180
+ if (/^\s*0\s+(NOFILE|!DATA)(\s|$)/i.test(line)) {
181
+ dataCur = null;
182
+ if (/!DATA/i.test(line)) {
183
+ firstBlock();
184
+ const dn = norm(line.replace(/^\s*0\s+!DATA\s*/i, ""));
185
+ dataCur = dn;
186
+ data.set(dn, "");
187
+ }
188
+ flush();
189
+ skip = true;
190
+ continue;
191
+ }
192
+ if (skip) {
193
+ const dc = dataCur;
194
+ if (dc) {
195
+ const d = /^\s*0\s+!:\s*(.*)$/.exec(line);
196
+ if (d)
197
+ data.set(dc, data.get(dc) + d[1].trim());
198
+ }
199
+ continue;
200
+ }
201
+ if (!cur) {
202
+ cur = norm(fallbackName);
203
+ main = cur;
204
+ }
205
+ buf.push(line);
206
+ }
207
+ flush();
208
+ return { files, main, data };
209
+ }
210
+ function flatten(model, lib, onInstance) {
211
+ const missing = new Map;
212
+ const walk = (name, parentM, parentColor, depth) => {
213
+ if (depth > 32)
214
+ return;
215
+ for (const line of model.files.get(name).split(`
216
+ `)) {
217
+ const r = parseType1(line);
218
+ if (!r)
219
+ continue;
220
+ const color = r.color === "16" ? parentColor : r.color;
221
+ const m = compose(parentM, r.m);
222
+ const n = norm(r.name);
223
+ if (model.files.has(n)) {
224
+ walk(n, m, color, depth + 1);
225
+ continue;
226
+ }
227
+ const key = lib.resolve(r.name);
228
+ if (!key) {
229
+ missing.set(n, (missing.get(n) || 0) + 1);
230
+ continue;
231
+ }
232
+ onInstance({ part: key, ref: n, color, m });
233
+ }
234
+ };
235
+ walk(model.main, IDENT, "16", 0);
236
+ return { missing };
237
+ }
238
+ function collectDeps(lib, refs) {
239
+ const out = new Map;
240
+ const stack = [...refs];
241
+ while (stack.length) {
242
+ const ref = stack.pop();
243
+ const tn = threeName(ref);
244
+ if (out.has(tn))
245
+ continue;
246
+ const key = lib.resolve(ref);
247
+ if (!key)
248
+ continue;
249
+ out.set(tn, key);
250
+ for (const line of lib.read(key).split(/\r?\n/)) {
251
+ const r = parseType1(line);
252
+ if (r)
253
+ stack.push(r.name);
254
+ }
255
+ }
256
+ return out;
257
+ }
258
+
259
+ // src/node/compile.ts
260
+ import fs2 from "node:fs";
261
+ import path2 from "node:path";
262
+
263
+ // src/spec.ts
264
+ var norm2 = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
265
+ function parseColor(tok) {
266
+ if (/^0x/i.test(tok))
267
+ return parseInt(tok, 16);
268
+ return +tok;
269
+ }
270
+ var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
271
+ function texNames(rest) {
272
+ const out = [];
273
+ let i = 0;
274
+ while (i < rest.length) {
275
+ while (rest[i] === " " || rest[i] === "\t")
276
+ i++;
277
+ if (i >= rest.length)
278
+ break;
279
+ if (rest[i] === '"') {
280
+ let v = "";
281
+ i++;
282
+ while (i < rest.length && rest[i] !== '"') {
283
+ if (rest[i] === "\\" && i + 1 < rest.length)
284
+ i++;
285
+ v += rest[i++];
286
+ }
287
+ i++;
288
+ out.push(v);
289
+ } else {
290
+ let j = i;
291
+ while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
292
+ j++;
293
+ out.push(rest.slice(i, j));
294
+ i = j;
295
+ }
296
+ }
297
+ return out;
298
+ }
299
+ function parseTexMap(tail, next = false) {
300
+ const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
301
+ if (!m)
302
+ return null;
303
+ const method = m[1].toUpperCase();
304
+ const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
305
+ if (!n)
306
+ return null;
307
+ const parts = m[2].trim().split(/\s+/);
308
+ if (parts.length < n + 1)
309
+ return null;
310
+ const v = parts.slice(0, n).map(Number);
311
+ if (!v.every(Number.isFinite))
312
+ return null;
313
+ let rest = m[2].trim();
314
+ for (let k = 0;k < n; k++)
315
+ rest = rest.replace(/^\S+\s*/, "");
316
+ const names = texNames(rest);
317
+ if (!names.length)
318
+ return null;
319
+ const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
320
+ if (n >= 10)
321
+ t.a = v[9];
322
+ if (n === 11)
323
+ t.b = v[10];
324
+ const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
325
+ if (g > 0 && names[g + 1])
326
+ t.glossmap = names[g + 1];
327
+ if (next)
328
+ t.next = true;
329
+ return t;
330
+ }
331
+ var b64 = (s) => {
332
+ const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
333
+ const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
334
+ const bin = atob(pad);
335
+ const out = new Uint8Array(bin.length);
336
+ for (let i = 0;i < bin.length; i++)
337
+ out[i] = bin.charCodeAt(i);
338
+ return out;
339
+ };
340
+ function geomLine(tk) {
341
+ const t = +tk[0];
342
+ if (!(t >= 1 && t <= 5) || tk.length < 2)
343
+ return null;
344
+ const color = parseColor(tk[1]);
345
+ const nums = tk.slice(2).map(Number);
346
+ if (t === 1)
347
+ return { t: 1, color, m: nums.slice(0, 12), file: norm2(tk.slice(14).join(" ")) };
348
+ if (t === 2)
349
+ return { t: 2, color, p: nums.slice(0, 6) };
350
+ if (t === 3)
351
+ return { t: 3, color, p: nums.slice(0, 9) };
352
+ if (t === 4)
353
+ return { t: 4, color, p: nums.slice(0, 12) };
354
+ return { t: 5, color, p: nums.slice(0, 12) };
355
+ }
356
+ function parse(src, name = "main.ldr") {
357
+ const files = new Map;
358
+ let cur = null;
359
+ let main = "";
360
+ let data;
361
+ let dataCur = null;
362
+ let texStack = [];
363
+ let texNext = null;
364
+ const closeData = () => {
365
+ if (!dataCur)
366
+ return;
367
+ const bytes = b64(dataCur.chunks.join(""));
368
+ const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
369
+ (data ??= new Map).set(norm2(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
370
+ dataCur = null;
371
+ };
372
+ const start = (n) => {
373
+ closeData();
374
+ cur = { name: norm2(n), cmds: [] };
375
+ files.set(cur.name, cur);
376
+ if (!main)
377
+ main = cur.name;
378
+ texStack = [];
379
+ texNext = null;
380
+ };
381
+ let skip = false, sawBlock = false;
382
+ const firstBlock = () => {
383
+ if (sawBlock)
384
+ return;
385
+ sawBlock = true;
386
+ files.clear();
387
+ cur = null;
388
+ main = "";
389
+ };
390
+ const annotate = (c, fromColon) => {
391
+ if (texNext) {
392
+ c.tex = texNext;
393
+ texNext = null;
394
+ return;
395
+ }
396
+ const top = texStack[texStack.length - 1];
397
+ if (!top)
398
+ return;
399
+ if (top.fallback) {
400
+ if (!fromColon)
401
+ c.fallback = true;
402
+ } else
403
+ c.tex = top.map;
404
+ };
405
+ for (const raw of src.split(/\r?\n/)) {
406
+ const line = raw.trim();
407
+ if (!line)
408
+ continue;
409
+ const tk = line.split(/\s+/);
410
+ const t = +tk[0];
411
+ if (t === 0) {
412
+ const text = line.slice(1).trim();
413
+ if (tk[1] === "FILE") {
414
+ firstBlock();
415
+ skip = false;
416
+ start(text.slice(5));
417
+ continue;
418
+ }
419
+ if (tk[1] === "NOFILE") {
420
+ closeData();
421
+ cur = null;
422
+ skip = true;
423
+ continue;
424
+ }
425
+ if (tk[1] === "!DATA") {
426
+ firstBlock();
427
+ closeData();
428
+ cur = null;
429
+ skip = false;
430
+ dataCur = { name: text.slice(5).trim(), chunks: [] };
431
+ continue;
432
+ }
433
+ if (skip)
434
+ continue;
435
+ if (dataCur) {
436
+ if (tk[1] === "!:")
437
+ dataCur.chunks.push(tk.slice(2).join(""));
438
+ continue;
439
+ }
440
+ if (!cur)
441
+ start(name);
442
+ const c = { t: 0, text };
443
+ if (tk[1] === "STEP") {
444
+ c.step = true;
445
+ texStack = [];
446
+ texNext = null;
447
+ } else if (tk[1] === "BFC")
448
+ c.bfc = tk.slice(2).join(" ");
449
+ else if (tk[1] === "ROTSTEP") {
450
+ c.meta = "ROTSTEP";
451
+ if ((tk[2] ?? "").toUpperCase() === "END")
452
+ c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
453
+ else {
454
+ const mode = (tk[5] ?? "REL").toUpperCase();
455
+ c.rotstep = { x: +tk[2] || 0, y: +tk[3] || 0, z: +tk[4] || 0, mode: ["REL", "ABS", "ADD"].includes(mode) ? mode : "REL" };
456
+ }
457
+ } else if (tk[1] === "BUFEXCHG") {
458
+ c.meta = "BUFEXCHG";
459
+ const op = (tk[2] ?? "").toUpperCase();
460
+ c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
461
+ } else if (tk[1]?.startsWith("!"))
462
+ c.meta = tk[1];
463
+ if (tk[1] === "!TEXMAP") {
464
+ const op = (tk[2] ?? "").toUpperCase();
465
+ texNext = null;
466
+ if (op === "START" || op === "NEXT") {
467
+ const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
468
+ c.texmap = { op, ...map ? { map } : {} };
469
+ if (map) {
470
+ (cur.texmaps ??= []).push(map);
471
+ if (op === "START")
472
+ texStack.push({ map, fallback: false });
473
+ else
474
+ texNext = map;
475
+ }
476
+ } else if (op === "FALLBACK") {
477
+ c.texmap = { op };
478
+ const top = texStack[texStack.length - 1];
479
+ if (top)
480
+ top.fallback = true;
481
+ } else if (op === "END") {
482
+ c.texmap = { op };
483
+ texStack.pop();
484
+ }
485
+ } else if (tk[1] === "!:") {
486
+ texNext = null;
487
+ const g = geomLine(tk.slice(2));
488
+ if (g) {
489
+ annotate(g, true);
490
+ c.geom = g;
491
+ }
492
+ } else
493
+ texNext = null;
494
+ const moved = text.match(MOVED_RE);
495
+ if (moved && !cur.movedTo) {
496
+ let target = moved[1].trim().replace(/\\/g, "/");
497
+ if (!/\.(dat|ldr|mpd)$/i.test(target))
498
+ target += ".dat";
499
+ cur.movedTo = norm2(target);
500
+ }
501
+ cur.cmds.push(c);
502
+ continue;
503
+ }
504
+ if (dataCur || skip)
505
+ continue;
506
+ if (!cur)
507
+ start(name);
508
+ const g = geomLine(tk);
509
+ if (!g)
510
+ continue;
511
+ annotate(g, false);
512
+ cur.cmds.push(g);
513
+ }
514
+ closeData();
515
+ return data ? { main, files, data } : { main, files };
516
+ }
517
+ function resolveTexture(doc, name) {
518
+ if (!doc.data)
519
+ return null;
520
+ const n = norm2(name);
521
+ return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
522
+ }
523
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
524
+ var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
525
+ var cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
526
+ var unit3 = (a) => {
527
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
528
+ return [a[0] / l, a[1] / l, a[2] / l];
529
+ };
530
+ var DEG = Math.PI / 180;
531
+ function texmapUV(map, p) {
532
+ const w = texmapTriangleUV(map, [p, p, p]);
533
+ const u = w[0], v = w[1];
534
+ return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
535
+ }
536
+ var radial = (p, c, n) => {
537
+ const d = sub3(p, c);
538
+ const k = dot3(d, n);
539
+ const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
540
+ return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
541
+ };
542
+ function texmapTriangleUV(map, pts) {
543
+ const { p1, p2, p3 } = map;
544
+ const out = new Array(pts.length * 2).fill(0);
545
+ if (map.method === "PLANAR") {
546
+ const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
547
+ const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
548
+ pts.forEach((p, i) => {
549
+ const d = sub3(p, p1);
550
+ out[i * 2] = dot3(d, e1) / l1;
551
+ out[i * 2 + 1] = dot3(d, e2) / l2;
552
+ });
553
+ return out;
554
+ }
555
+ const cyl = map.method === "CYLINDRICAL";
556
+ const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
557
+ const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
558
+ const A = (map.a ?? 360) * DEG;
559
+ let base = null, baseAng = 0;
560
+ const sAng = (q) => {
561
+ if (!base) {
562
+ base = q;
563
+ baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
564
+ return baseAng;
565
+ }
566
+ return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
567
+ };
568
+ const miss = [];
569
+ let uSum = 0;
570
+ const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
571
+ pts.forEach((p, i) => {
572
+ if (cyl) {
573
+ const q = dir ? radial(p, p1, n) : null;
574
+ if (q) {
575
+ const a = sAng(q);
576
+ out[i * 2] = 0.5 + a / A;
577
+ uSum += out[i * 2];
578
+ } else
579
+ miss.push(i);
580
+ out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
581
+ } else {
582
+ const pd = unit3(sub3(p, p1));
583
+ const c = dot3(pd, n);
584
+ if (Math.abs(c) > 1 - 0.000000001) {
585
+ miss.push(i);
586
+ out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
587
+ return;
588
+ }
589
+ const a = sAng(radial(p, p1, n));
590
+ uSum += a;
591
+ out[i * 2] = 0.5 + a / A;
592
+ const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
593
+ const r = unit3([pd[0] * ct + cr[0] * st + n[0] * k, pd[1] * ct + cr[1] * st + n[1] * k, pd[2] * ct + cr[2] * st + n[2] * k]);
594
+ out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
595
+ }
596
+ });
597
+ if (miss.length && miss.length < pts.length) {
598
+ const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
599
+ for (const i of miss)
600
+ out[i * 2] = avg;
601
+ }
602
+ return out;
603
+ }
604
+ function transformTexMap(map, m) {
605
+ const x = (q) => [m[3] * q[0] + m[4] * q[1] + m[5] * q[2] + m[0], m[6] * q[0] + m[7] * q[1] + m[8] * q[2] + m[1], m[9] * q[0] + m[10] * q[1] + m[11] * q[2] + m[2]];
606
+ return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
607
+ }
608
+ function mul(a, b) {
609
+ 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;
610
+ return [
611
+ a0 * bx + a1 * by + a2 * bz + ax,
612
+ a3 * bx + a4 * by + a5 * bz + ay,
613
+ a6 * bx + a7 * by + a8 * bz + az,
614
+ a0 * b0 + a1 * b3 + a2 * b6,
615
+ a0 * b1 + a1 * b4 + a2 * b7,
616
+ a0 * b2 + a1 * b5 + a2 * b8,
617
+ a3 * b0 + a4 * b3 + a5 * b6,
618
+ a3 * b1 + a4 * b4 + a5 * b7,
619
+ a3 * b2 + a4 * b5 + a5 * b8,
620
+ a6 * b0 + a7 * b3 + a8 * b6,
621
+ a6 * b1 + a7 * b4 + a8 * b7,
622
+ a6 * b2 + a7 * b5 + a8 * b8
623
+ ];
624
+ }
625
+ var I = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
626
+ var isDirectColor = (c) => c > 16777215;
627
+ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
628
+ const q = (i, a) => `${a[i].toFixed(2)},${a[i + 1].toFixed(2)},${a[i + 2].toFixed(2)}`;
629
+ const ek = (a, b) => a < b ? a + "|" + b : b + "|" + a;
630
+ const hard = new Set, soft = new Set;
631
+ for (let i = 0;i < edges.length; i += 6)
632
+ hard.add(ek(q(i, edges), q(i + 3, edges)));
633
+ const stride = cond.length > 0 && cond.length % 12 === 0 ? 12 : 6;
634
+ for (let i = 0;i < cond.length; i += stride)
635
+ soft.add(ek(q(i, cond), q(i + 3, cond)));
636
+ const nT = pos.length / 9, fn = [], vk = [];
637
+ for (let t = 0;t < nT; t++) {
638
+ const a = t * 9;
639
+ 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]];
640
+ 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]];
641
+ const l = Math.hypot(...n) || 1;
642
+ fn.push(n.map((x) => x / l));
643
+ for (let k = 0;k < 3; k++)
644
+ vk.push(q(a + k * 3, pos));
645
+ }
646
+ const byV = new Map;
647
+ vk.forEach((k, i) => (byV.get(k) ?? byV.set(k, []).get(k)).push(i));
648
+ const edgeFaces = new Map;
649
+ for (let t = 0;t < nT; t++)
650
+ for (let k = 0;k < 3; k++) {
651
+ const e = ek(vk[t * 3 + k], vk[t * 3 + (k + 1) % 3]);
652
+ (edgeFaces.get(e) ?? edgeFaces.set(e, []).get(e)).push(t);
653
+ }
654
+ const cosC = Math.cos(creaseDeg * Math.PI / 180), out = new Float32Array(pos.length);
655
+ const joinable = (e, a, b) => {
656
+ if (hard.has(e))
657
+ return false;
658
+ const d = fn[a][0] * fn[b][0] + fn[a][1] * fn[b][1] + fn[a][2] * fn[b][2];
659
+ return soft.has(e) ? d > -0.2 : d > cosC;
660
+ };
661
+ for (const [key, corners] of byV) {
662
+ const faces = corners.map((c) => c / 3 | 0), par = faces.map((_, i) => i);
663
+ const f = (i) => par[i] === i ? i : par[i] = f(par[i]);
664
+ for (let i = 0;i < faces.length; i++)
665
+ for (let j = i + 1;j < faces.length; j++) {
666
+ const A = faces[i], B = faces[j];
667
+ const va = [0, 1, 2].map((k) => vk[A * 3 + k]), vb = new Set([0, 1, 2].map((k) => vk[B * 3 + k]));
668
+ for (const o of va)
669
+ if (o !== key && vb.has(o)) {
670
+ const e = ek(key, o);
671
+ if ((edgeFaces.get(e)?.length ?? 0) >= 2 && joinable(e, A, B))
672
+ par[f(i)] = f(j);
673
+ }
674
+ }
675
+ const acc = new Map;
676
+ faces.forEach((F, i) => {
677
+ const r = f(i), s = acc.get(r) ?? [0, 0, 0];
678
+ s[0] += fn[F][0];
679
+ s[1] += fn[F][1];
680
+ s[2] += fn[F][2];
681
+ acc.set(r, s);
682
+ });
683
+ corners.forEach((c, i) => {
684
+ const s = acc.get(f(i)), l = Math.hypot(...s) || 1;
685
+ out[c * 3] = s[0] / l;
686
+ out[c * 3 + 1] = s[1] / l;
687
+ out[c * 3 + 2] = s[2] / l;
688
+ });
689
+ }
690
+ return out;
691
+ }
692
+
693
+ // src/node/compile.ts
694
+ function loadColors(root) {
695
+ const map = new Map;
696
+ for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
697
+ const m = /!COLOUR\s+\S+\s+CODE\s+(\d+)\s+VALUE\s+#([0-9A-Fa-f]{6})/.exec(l);
698
+ if (m) {
699
+ const h = parseInt(m[2], 16);
700
+ map.set(m[1], [(h >> 16) / 255, (h >> 8 & 255) / 255, (h & 255) / 255]);
701
+ }
702
+ }
703
+ return map;
704
+ }
705
+ 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]);
706
+
707
+ class Compiler {
708
+ lib;
709
+ opts;
710
+ colors;
711
+ cache = new Map;
712
+ constructor(lib, opts) {
713
+ this.lib = lib;
714
+ this.opts = opts;
715
+ this.colors = loadColors(lib.root);
716
+ }
717
+ resolve(ref, isStud) {
718
+ const key = this.lib.resolve(ref);
719
+ if (!key)
720
+ return null;
721
+ const low = isStud ? this.opts.studs === "low" : this.opts.prims === "low";
722
+ if (low && key.startsWith("p/") && !key.startsWith("p/8/") && !key.startsWith("p/48/")) {
723
+ const alt = "p/8/" + key.slice(2);
724
+ if (this.lib.index.has(alt))
725
+ return alt;
726
+ }
727
+ if (low && key.startsWith("p/48/")) {
728
+ const alt = "p/8/" + key.slice(5);
729
+ if (this.lib.index.has(alt))
730
+ return alt;
731
+ const a2 = "p/" + key.slice(5);
732
+ if (this.lib.index.has(a2))
733
+ return a2;
734
+ }
735
+ return key;
736
+ }
737
+ compile(key, separateStuds = this.opts.separateStuds) {
738
+ const ck = key + (separateStuds ? "" : "#nostudsep");
739
+ if (this.cache.has(ck))
740
+ return this.cache.get(ck);
741
+ const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, cond: this.opts.cond ? [] : undefined, condUnder: this.opts.cond ? [] : undefined, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
742
+ 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];
743
+ const walk = (fk, M, color, invert, target, depth) => {
744
+ if (depth > 40)
745
+ return;
746
+ let ccw = true, certified = false, invertNext = false;
747
+ for (const raw of this.lib.read(fk).split(/\r?\n/)) {
748
+ const t = raw.trim();
749
+ if (!t)
750
+ continue;
751
+ const c = t[0];
752
+ if (c === "0") {
753
+ if (/^0\s+BFC/.test(t)) {
754
+ if (/CERTIFY/.test(t) && !/NOCERTIFY/.test(t))
755
+ certified = true;
756
+ if (/\bCW\b/.test(t))
757
+ ccw = false;
758
+ if (/\bCCW\b/.test(t))
759
+ ccw = true;
760
+ if (/INVERTNEXT/.test(t))
761
+ invertNext = true;
762
+ }
763
+ continue;
764
+ }
765
+ if (c === "1") {
766
+ const r = parseType1(t);
767
+ if (!r)
768
+ continue;
769
+ const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
770
+ const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
771
+ const ck2 = this.resolve(r.name, isTop);
772
+ if (!ck2) {
773
+ invertNext = false;
774
+ continue;
775
+ }
776
+ const childColor = r.color === "16" ? color : r.color;
777
+ const Mc = compose(M, r.m);
778
+ const inv = invert !== invertNext !== det3(r.m) < 0;
779
+ invertNext = false;
780
+ if (isTop && separateStuds) {
781
+ out.studs.push({ kind: ck2, color: childColor, m: Mc });
782
+ growBox(out.bbox, Mc, this.compile(ck2, false).bbox);
783
+ continue;
784
+ }
785
+ walk(ck2, Mc, childColor, inv, isUnder ? "under" : target, depth + 1);
786
+ continue;
787
+ }
788
+ const tok = t.split(/\s+/);
789
+ if (c === "2") {
790
+ const v = tok.slice(2, 8).map(Number);
791
+ out.edges.push(...xf(M, v.slice(0, 3)), ...xf(M, v.slice(3, 6)));
792
+ continue;
793
+ }
794
+ if (c === "5") {
795
+ out.condLines++;
796
+ const dst = target === "under" ? out.condUnder : out.cond;
797
+ if (dst) {
798
+ const v = t.split(/\s+/).slice(2, 14).map(Number);
799
+ if (v.length === 12 && v.every(Number.isFinite))
800
+ for (let i = 0;i < 12; i += 3)
801
+ dst.push(...xf(M, v.slice(i, i + 3)));
802
+ }
803
+ continue;
804
+ }
805
+ if (c === "3" || c === "4") {
806
+ const code = tok[1] === "16" ? color : tok[1];
807
+ const n = c === "3" ? 3 : 4;
808
+ const v = tok.slice(2, 2 + n * 3).map(Number);
809
+ const pts = [];
810
+ for (let i = 0;i < n; i++)
811
+ pts.push(xf(M, v.slice(i * 3, i * 3 + 3)));
812
+ for (const p of pts)
813
+ grow(out.bbox, p);
814
+ let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
815
+ if (!ccw !== invert)
816
+ order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
817
+ const rgba = colorOf(code);
818
+ const b = out[target];
819
+ for (const i of order) {
820
+ b.pos.push(...pts[i]);
821
+ b.col.push(...rgba);
822
+ }
823
+ }
824
+ }
825
+ if (!certified)
826
+ out.uncertified = true;
827
+ };
828
+ walk(key, IDENT, "16", false, "body", 0);
829
+ if (this.opts.tex && this.hasTex(key))
830
+ this.texPass(key, out, separateStuds, colorOf);
831
+ const [x0, y0, z0, x1, y1, z1] = out.bbox;
832
+ let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
833
+ for (const b of [out.body, out.under])
834
+ for (let i = 0;i < b.pos.length; i += 3)
835
+ if (b.pos[i + 1] > y1 - 0.5) {
836
+ ax0 = Math.min(ax0, b.pos[i]);
837
+ ax1 = Math.max(ax1, b.pos[i]);
838
+ az0 = Math.min(az0, b.pos[i + 2]);
839
+ az1 = Math.max(az1, b.pos[i + 2]);
840
+ }
841
+ out.fullBottom = isFinite(x0) && ax0 <= x0 + 0.5 && ax1 >= x1 - 0.5 && az0 <= z0 + 0.5 && az1 >= z1 - 0.5;
842
+ if (separateStuds && isFinite(x0))
843
+ out.bottomMask = enclosureMask(out);
844
+ this.cache.set(ck, out);
845
+ return out;
846
+ }
847
+ texMemo = new Map;
848
+ hasTex(key) {
849
+ const m = this.texMemo.get(key);
850
+ if (m !== undefined)
851
+ return m;
852
+ this.texMemo.set(key, false);
853
+ const txt = this.lib.read(key);
854
+ let r = /^\s*0\s+!TEXMAP/im.test(txt);
855
+ if (!r)
856
+ for (const l of txt.split(/\r?\n/)) {
857
+ const t = l.trim();
858
+ const p = t.startsWith("0 !:") ? parseType1(t.slice(4)) : t[0] === "1" ? parseType1(t) : null;
859
+ if (p) {
860
+ const k = this.lib.resolve(p.name);
861
+ if (k && this.hasTex(k)) {
862
+ r = true;
863
+ break;
864
+ }
865
+ }
866
+ }
867
+ this.texMemo.set(key, r);
868
+ return r;
869
+ }
870
+ texture(name) {
871
+ const n = norm(name);
872
+ for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
873
+ const k = pre + n;
874
+ if (this.lib.index.has(k))
875
+ return new Uint8Array(fs2.readFileSync(this.lib.index.get(k)));
876
+ }
877
+ return null;
878
+ }
879
+ texPass(key, out, separateStuds, colorOf) {
880
+ const tbody = { pos: [], col: [] }, texb = new Map;
881
+ const walk = (fk, M, color, invert, target, depth, texIn) => {
882
+ if (depth > 40)
883
+ return;
884
+ let ccw = true, invertNext = false;
885
+ let stack = [];
886
+ let next = null;
887
+ for (const raw of this.lib.read(fk).split(/\r?\n/)) {
888
+ let t = raw.trim();
889
+ if (!t)
890
+ continue;
891
+ let colon = false;
892
+ if (t[0] === "0") {
893
+ const m = /^0\s+(\S+)\s*(.*)$/.exec(t);
894
+ const kw = (m?.[1] ?? "").toUpperCase(), rest = m?.[2] ?? "";
895
+ if (kw === "!TEXMAP") {
896
+ const op = (rest.split(/\s+/)[0] || "").toUpperCase();
897
+ next = null;
898
+ if (op === "START" || op === "NEXT") {
899
+ const map = parseTexMap(rest.replace(/^\S+/, ""), op === "NEXT");
900
+ if (map) {
901
+ const pm = transformTexMap(map, M);
902
+ if (op === "START")
903
+ stack.push({ map: pm, fallback: false });
904
+ else
905
+ next = pm;
906
+ }
907
+ } else if (op === "FALLBACK") {
908
+ const top = stack[stack.length - 1];
909
+ if (top)
910
+ top.fallback = true;
911
+ } else if (op === "END")
912
+ stack.pop();
913
+ continue;
914
+ }
915
+ if (kw === "!:") {
916
+ next = null;
917
+ t = rest.trim();
918
+ colon = true;
919
+ if (!t || t[0] === "0")
920
+ continue;
921
+ } else {
922
+ if (kw === "BFC") {
923
+ if (/\bCW\b/.test(t))
924
+ ccw = false;
925
+ if (/\bCCW\b/.test(t))
926
+ ccw = true;
927
+ if (/INVERTNEXT/.test(t))
928
+ invertNext = true;
929
+ }
930
+ if (kw === "STEP")
931
+ stack = [];
932
+ next = null;
933
+ continue;
934
+ }
935
+ }
936
+ let tex = texIn;
937
+ if (next) {
938
+ tex = next;
939
+ next = null;
940
+ } else {
941
+ const top = stack[stack.length - 1];
942
+ if (top) {
943
+ if (top.fallback) {
944
+ if (!colon)
945
+ continue;
946
+ } else
947
+ tex = top.map;
948
+ }
949
+ }
950
+ const c = t[0];
951
+ if (c === "1") {
952
+ const r = parseType1(t);
953
+ if (!r)
954
+ continue;
955
+ const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
956
+ const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
957
+ const ck2 = this.resolve(r.name, isTop);
958
+ if (!ck2) {
959
+ invertNext = false;
960
+ continue;
961
+ }
962
+ const inv = invert !== invertNext !== det3(r.m) < 0;
963
+ invertNext = false;
964
+ if (isTop && separateStuds)
965
+ continue;
966
+ walk(ck2, compose(M, r.m), r.color === "16" ? color : r.color, inv, isUnder ? "under" : target, depth + 1, tex);
967
+ continue;
968
+ }
969
+ if (c !== "3" && c !== "4")
970
+ continue;
971
+ const tok = t.split(/\s+/);
972
+ const code = tok[1] === "16" ? color : tok[1];
973
+ const n = c === "3" ? 3 : 4;
974
+ const v = tok.slice(2, 2 + n * 3).map(Number);
975
+ const pts = [];
976
+ for (let i = 0;i < n; i++)
977
+ pts.push(xf(M, v.slice(i * 3, i * 3 + 3)));
978
+ let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
979
+ if (!ccw !== invert)
980
+ order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
981
+ const rgba = colorOf(code);
982
+ if (tex) {
983
+ let b = texb.get(tex.texture);
984
+ if (!b)
985
+ texb.set(tex.texture, b = { pos: [], col: [], uv: [] });
986
+ if (Math.abs(tex.a ?? 0) >= 360)
987
+ b.wrapU = true;
988
+ for (let k = 0;k < order.length; k += 3) {
989
+ const tri = [pts[order[k]], pts[order[k + 1]], pts[order[k + 2]]];
990
+ const w = texmapTriangleUV(tex, tri);
991
+ for (let j = 0;j < 3; j++) {
992
+ b.pos.push(...tri[j]);
993
+ b.col.push(...rgba);
994
+ b.uv.push(w[j * 2], w[j * 2 + 1]);
995
+ }
996
+ }
997
+ } else if (target === "body")
998
+ for (const i of order) {
999
+ tbody.pos.push(...pts[i]);
1000
+ tbody.col.push(...rgba);
1001
+ }
1002
+ }
1003
+ };
1004
+ walk(key, IDENT, "16", false, "body", 0, null);
1005
+ if (texb.size) {
1006
+ out.tbody = tbody;
1007
+ out.texb = texb;
1008
+ }
1009
+ }
1010
+ }
1011
+ function enclosureMask(c) {
1012
+ const [x0, , z0, x1, y1, z1] = c.bbox;
1013
+ const nx = Math.round((x1 - x0) / 20), nz = Math.round((z1 - z0) / 20);
1014
+ if (!nx || !nz || nx * nz > 4096)
1015
+ return;
1016
+ const tris = [];
1017
+ for (const b of [c.body, c.under])
1018
+ for (let i = 0;i < b.pos.length; i += 9) {
1019
+ const t = b.pos.slice(i, i + 9);
1020
+ if (Math.max(t[1], t[4], t[7]) < y1 - 6)
1021
+ continue;
1022
+ tris.push(t);
1023
+ }
1024
+ let yRay = y1 - 2;
1025
+ const bits = [];
1026
+ const BIN = 4;
1027
+ const buckets = [new Map, new Map];
1028
+ for (const t of tris) {
1029
+ if (Math.min(t[1], t[4], t[7]) > y1 - 0.5 || Math.max(t[1], t[4], t[7]) < y1 - 4)
1030
+ continue;
1031
+ for (const [ai, o] of [[0, 2], [1, 0]]) {
1032
+ 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);
1033
+ for (let k = lo;k <= hi; k++)
1034
+ (buckets[ai].get(k) || buckets[ai].set(k, []).get(k)).push(t);
1035
+ }
1036
+ }
1037
+ const hit = (px, pz, axis, sign) => {
1038
+ const o = axis === 0 ? 2 : 0;
1039
+ const pv = axis === 0 ? pz : px;
1040
+ for (const t of buckets[axis === 0 ? 0 : 1].get(Math.floor(pv / BIN)) || []) {
1041
+ const a = [t[1], t[o]], b = [t[4], t[3 + o]], d = [t[7], t[6 + o]];
1042
+ const den = (b[1] - d[1]) * (a[0] - d[0]) + (d[0] - b[0]) * (a[1] - d[1]);
1043
+ if (Math.abs(den) < 0.000000001)
1044
+ continue;
1045
+ const y = yRay;
1046
+ 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;
1047
+ if (w1 < -0.000001 || w2 < -0.000001 || w3 < -0.000001)
1048
+ continue;
1049
+ const along = w1 * t[axis] + w2 * t[3 + axis] + w3 * t[6 + axis];
1050
+ const start = axis === 0 ? px : pz;
1051
+ if ((along - start) * sign > 0.01)
1052
+ return true;
1053
+ }
1054
+ return false;
1055
+ };
1056
+ for (let a = 0;a < nx; a++)
1057
+ for (let b = 0;b < nz; b++) {
1058
+ const px = x0 + 10 + 20 * a, pz = z0 + 10 + 20 * b;
1059
+ let ok = true;
1060
+ for (const yy of [y1 - 1, y1 - 3.5]) {
1061
+ yRay = yy;
1062
+ for (const [ox, oz] of [[0, 0], [5, 5], [-5, 5], [5, -5], [-5, -5]]) {
1063
+ const qx = px + ox, qz = pz + oz;
1064
+ if (!(hit(qx, qz, 0, 1) && hit(qx, qz, 0, -1) && hit(qx, qz, 2, 1) && hit(qx, qz, 2, -1))) {
1065
+ ok = false;
1066
+ break;
1067
+ }
1068
+ }
1069
+ if (!ok)
1070
+ break;
1071
+ }
1072
+ bits.push(ok ? 1 : 0);
1073
+ }
1074
+ return { nx, nz, bits };
1075
+ }
1076
+ function grow(b, p) {
1077
+ for (let i = 0;i < 3; i++) {
1078
+ if (p[i] < b[i])
1079
+ b[i] = p[i];
1080
+ if (p[i] > b[i + 3])
1081
+ b[i + 3] = p[i];
1082
+ }
1083
+ }
1084
+ function growBox(b, M, cb) {
1085
+ if (!isFinite(cb[0]))
1086
+ return;
1087
+ for (const x of [cb[0], cb[3]])
1088
+ for (const y of [cb[1], cb[4]])
1089
+ for (const z of [cb[2], cb[5]])
1090
+ grow(b, xf(M, [x, y, z]));
1091
+ }
1092
+ function writePack(file, parts, studKinds, weld, extra, options) {
1093
+ fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
1094
+ }
1095
+ function encodePack(parts, studKinds, weld, extra, options = {}) {
1096
+ const chunks = [];
1097
+ let off = 0;
1098
+ const put = (a) => {
1099
+ const pad = (4 - off % 4) % 4;
1100
+ if (pad) {
1101
+ chunks.push(new Uint8Array(pad));
1102
+ off += pad;
1103
+ }
1104
+ const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
1105
+ chunks.push(a);
1106
+ off += a.byteLength;
1107
+ return r;
1108
+ };
1109
+ let lastPos = [];
1110
+ const bucket = (b) => {
1111
+ const n = b.pos.length / 3;
1112
+ lastPos = [];
1113
+ if (!n)
1114
+ return null;
1115
+ const nrm = new Float32Array(n * 3);
1116
+ for (let i = 0;i < n; i += 3) {
1117
+ const p = b.pos;
1118
+ const a = i * 3;
1119
+ 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];
1120
+ let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
1121
+ const l = Math.hypot(nx, ny, nz) || 1;
1122
+ nx /= l;
1123
+ ny /= l;
1124
+ nz /= l;
1125
+ for (let k = 0;k < 3; k++) {
1126
+ nrm[(i + k) * 3] = nx;
1127
+ nrm[(i + k) * 3 + 1] = ny;
1128
+ nrm[(i + k) * 3 + 2] = nz;
1129
+ }
1130
+ }
1131
+ if (!weld) {
1132
+ lastPos = b.pos;
1133
+ return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
1134
+ }
1135
+ const map = new Map;
1136
+ const P = [], N = [], C = [], I = [];
1137
+ for (let i = 0;i < n; i++) {
1138
+ 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()}`;
1139
+ let j = map.get(k);
1140
+ if (j === undefined) {
1141
+ j = P.length / 3;
1142
+ map.set(k, j);
1143
+ P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
1144
+ N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
1145
+ C.push(...b.col.slice(i * 4, i * 4 + 4));
1146
+ }
1147
+ I.push(j);
1148
+ }
1149
+ const vcount = P.length / 3;
1150
+ lastPos = P;
1151
+ 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 };
1152
+ };
1153
+ let anyCond = false;
1154
+ const condl = (a, pools) => {
1155
+ if (!a || !a.length)
1156
+ return;
1157
+ anyCond = true;
1158
+ const key = (p, i) => Math.round(p[i] * 1000) + "," + Math.round(p[i + 1] * 1000) + "," + Math.round(p[i + 2] * 1000);
1159
+ const need = new Map;
1160
+ for (let i = 0;i < a.length; i += 3)
1161
+ need.set(key(a, i), -1);
1162
+ let base = 0;
1163
+ for (const p of pools) {
1164
+ for (let i = 0;i < p.length; i += 3) {
1165
+ const k = key(p, i);
1166
+ if (need.get(k) === -1)
1167
+ need.set(k, base + i / 3);
1168
+ }
1169
+ base += p.length / 3;
1170
+ }
1171
+ const X = [], I = new Array(a.length / 3);
1172
+ for (let i = 0;i < a.length; i += 3) {
1173
+ const k = key(a, i);
1174
+ let j = need.get(k);
1175
+ if (j === -1) {
1176
+ j = base + X.length / 3;
1177
+ need.set(k, j);
1178
+ X.push(a[i], a[i + 1], a[i + 2]);
1179
+ }
1180
+ I[i / 3] = j;
1181
+ }
1182
+ let prev = 0, max = 0;
1183
+ for (let i = 0;i < I.length; i++) {
1184
+ const d = I[i] - prev;
1185
+ prev = I[i];
1186
+ const z = d >= 0 ? d * 2 : -d * 2 - 1;
1187
+ I[i] = z;
1188
+ if (z > max)
1189
+ max = z;
1190
+ }
1191
+ return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
1192
+ };
1193
+ const texNames = [];
1194
+ const texBucket = (b) => {
1195
+ const n = b.pos.length / 3;
1196
+ if (!n)
1197
+ return null;
1198
+ const nrm = new Float32Array(n * 3);
1199
+ for (let i = 0;i < n; i += 3) {
1200
+ const p = b.pos;
1201
+ const a = i * 3;
1202
+ 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];
1203
+ let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
1204
+ const l = Math.hypot(nx, ny, nz) || 1;
1205
+ nx /= l;
1206
+ ny /= l;
1207
+ nz /= l;
1208
+ for (let k = 0;k < 3; k++) {
1209
+ nrm[(i + k) * 3] = nx;
1210
+ nrm[(i + k) * 3 + 1] = ny;
1211
+ nrm[(i + k) * 3 + 2] = nz;
1212
+ }
1213
+ }
1214
+ const map = new Map;
1215
+ const P = [], N = [], C = [], U = [], I = [];
1216
+ for (let i = 0;i < n; i++) {
1217
+ const k = weld ? `${b.pos.slice(i * 3, i * 3 + 3).map((x) => x.toFixed(3))},${[...nrm.subarray(i * 3, i * 3 + 3)].map((x) => x.toFixed(3))},${b.col.slice(i * 4, i * 4 + 4).join()},${b.uv[i * 2].toFixed(5)},${b.uv[i * 2 + 1].toFixed(5)}` : String(i);
1218
+ let j = map.get(k);
1219
+ if (j === undefined) {
1220
+ j = P.length / 3;
1221
+ map.set(k, j);
1222
+ P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
1223
+ N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
1224
+ C.push(...b.col.slice(i * 4, i * 4 + 4));
1225
+ U.push(b.uv[i * 2], b.uv[i * 2 + 1]);
1226
+ }
1227
+ I.push(j);
1228
+ }
1229
+ return { pos: put(new Float32Array(P)), nrm: put(new Float32Array(N)), col: put(new Float32Array(C)), uv: put(new Float32Array(U)), idx: put(P.length / 3 < 65536 ? new Uint16Array(I) : new Uint32Array(I)), tris: n / 3 };
1230
+ };
1231
+ const encTex = (c) => {
1232
+ if (!c.texb)
1233
+ return {};
1234
+ const tex = [...c.texb].map(([name, b]) => {
1235
+ let t = texNames.indexOf(name);
1236
+ if (t < 0)
1237
+ t = texNames.push(name) - 1;
1238
+ return { t, ...b.wrapU ? { wrapU: true } : {}, ...texBucket(b) };
1239
+ });
1240
+ return { tbody: bucket(c.tbody), tex };
1241
+ };
1242
+ const enc = (c) => {
1243
+ const body = bucket(c.body), bodyP = lastPos, under = bucket(c.under), underP = lastPos;
1244
+ const edges = c.edges.length ? put(new Float32Array(c.edges)) : null;
1245
+ const condlB = condl(c.cond, [bodyP, underP, c.edges]), condlU = condl(c.condUnder, [bodyP, underP, c.edges]);
1246
+ return { body, under, edges, cond: c.condLines, condl: condlB, condlu: condlU, 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, ...encTex(c) };
1247
+ };
1248
+ const encParts = Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), encKinds = Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)]));
1249
+ const textures = texNames.map((name) => {
1250
+ const b = options.texture?.(name);
1251
+ return b ? { name, mime: "image/png", data: put(b) } : { name, missing: true };
1252
+ });
1253
+ const features = [...anyCond ? ["cond"] : [], ...texNames.length ? ["tex"] : []];
1254
+ const header = { version: features.length ? 2 : 1, ...features.length ? { features } : {}, weld, parts: encParts, studKinds: encKinds, ...texNames.length ? { textures } : {}, ...extra };
1255
+ const hj = new TextEncoder().encode(JSON.stringify(header));
1256
+ const hpad = (4 - (4 + hj.length) % 4) % 4;
1257
+ const head = new Uint8Array(4 + hj.length + hpad);
1258
+ new DataView(head.buffer).setUint32(0, hj.length + hpad, true);
1259
+ head.set(hj, 4);
1260
+ head.fill(32, 4 + hj.length);
1261
+ return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
1262
+ }
1263
+
1264
+ // src/model.ts
1265
+ function flatten2(doc, opts = {}) {
1266
+ const out = [];
1267
+ const maxDepth = opts.maxDepth ?? 64;
1268
+ const walk = (d, fname, m, color, path, stepPath, depth) => {
1269
+ const f = d.files.get(fname);
1270
+ if (!f || depth > maxDepth)
1271
+ return;
1272
+ let s = 0;
1273
+ for (const c of f.cmds) {
1274
+ if (c.t === 0 && c.step)
1275
+ s++;
1276
+ if (c.t !== 1)
1277
+ continue;
1278
+ const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
1279
+ if (d.files.has(c.file)) {
1280
+ walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
1281
+ continue;
1282
+ }
1283
+ const ext = opts.resolveModel?.(c.file);
1284
+ if (ext) {
1285
+ walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
1286
+ continue;
1287
+ }
1288
+ out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
1289
+ }
1290
+ };
1291
+ walk(doc, doc.main, I, 16, doc.main, [], 0);
1292
+ return out;
1293
+ }
1294
+ function parseModel(text, name = "main.ldr", opts) {
1295
+ return flatten2(parse(text, name), opts);
1296
+ }
1297
+ function sequenceSteps(insts) {
1298
+ const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
1299
+ const uniq = [...new Set(insts.map(key))].sort();
1300
+ const rank = new Map(uniq.map((k, j) => [k, j]));
1301
+ const stepOf = new Int32Array(insts.length);
1302
+ insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
1303
+ return { count: uniq.length, stepOf };
1304
+ }
1305
+ function parseLDConfig(text) {
1306
+ const out = new Map;
1307
+ for (const l of text.split(/\r?\n/)) {
1308
+ 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);
1309
+ if (m)
1310
+ out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
1311
+ }
1312
+ return out;
1313
+ }
1314
+
1315
+ // src/node/build-pack.ts
1316
+ function buildPack(o) {
1317
+ const lib = new Library(o.ldrawRoot);
1318
+ const refs = new Map;
1319
+ const missing = new Set;
1320
+ const data = new Map;
1321
+ for (const m of o.models ?? []) {
1322
+ const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
1323
+ const sm = splitModel(text, name);
1324
+ for (const [k, v] of sm.data)
1325
+ if (!data.has(k))
1326
+ data.set(k, v);
1327
+ const r = flatten(sm, lib, (i) => refs.set(i.ref, i.part));
1328
+ for (const k of r.missing.keys())
1329
+ missing.add(k);
1330
+ }
1331
+ for (const p of o.parts ?? []) {
1332
+ const k = lib.resolve(p);
1333
+ if (k)
1334
+ refs.set(norm(p), k);
1335
+ else
1336
+ missing.add(norm(p));
1337
+ }
1338
+ if (o.allParts) {
1339
+ for (const k of lib.index.keys())
1340
+ if (lib.isPart(k) && /\.dat$/.test(k))
1341
+ refs.set(k.slice(6), k);
1342
+ }
1343
+ const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true, cond: !!o.cond, tex: !!o.textures });
1344
+ const parts = new Map, kinds = new Map;
1345
+ let n = 0;
1346
+ for (const [ref, key] of refs) {
1347
+ try {
1348
+ const c = comp.compile(key);
1349
+ parts.set(ref, c);
1350
+ for (const s of c.studs)
1351
+ if (!kinds.has(s.kind))
1352
+ kinds.set(s.kind, comp.compile(s.kind, false));
1353
+ } catch {
1354
+ missing.add(ref);
1355
+ }
1356
+ o.onProgress?.(++n, refs.size);
1357
+ }
1358
+ let extra;
1359
+ if (o.colors !== false) {
1360
+ const cfg = path3.join(o.ldrawRoot, "LDConfig.ldr");
1361
+ if (fs3.existsSync(cfg))
1362
+ extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
1363
+ }
1364
+ const texture = (n) => {
1365
+ const k = norm(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
1366
+ return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
1367
+ };
1368
+ return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
1369
+ }
1370
+
1371
+ // src/cli.ts
1372
+ var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
1373
+
1374
+ Usage:
1375
+ ldr pack --ldraw <dir> -o <out.bin[.zst|.gz]> [model.ldr ...] [options]
1376
+
1377
+ Options:
1378
+ --ldraw <dir> LDraw library root (contains LDConfig.ldr, parts/, p/). Download complete.zip from https://library.ldraw.org
1379
+ -o, --out <file> Output file. Extension .zst / .gz compresses (recommended for serving).
1380
+ --part <ref> Include a part (repeatable), e.g. --part 3001.dat
1381
+ --all-parts Include every part in the library (large)
1382
+ --studs low|full Stud detail (default low)
1383
+ --prims full|low Other primitive detail (default full)
1384
+ --no-weld Don't index/weld vertices
1385
+ --cond Keep conditional (type-5) lines for curved-surface outlines (pack v2, ~+8-14% zstd)
1386
+ --textures Keep printed/sticker parts' !TEXMAP geometry + UVs and embed their PNGs (pack v2; packs without
1387
+ textured parts are unchanged). Textures come from model !DATA blocks, then <ldraw>/parts/textures
1388
+ --no-colors Don't embed the LDConfig colour table
1389
+ `;
1390
+ var argv = process.argv.slice(2);
1391
+ if (argv[0] !== "pack" || argv.includes("-h") || argv.includes("--help")) {
1392
+ console.log(HELP);
1393
+ process.exit(argv[0] === "pack" || argv.includes("--help") || argv.includes("-h") ? 0 : 1);
1394
+ }
1395
+ var a = argv.slice(1);
1396
+ var models = [];
1397
+ var parts = [];
1398
+ var ldraw = "";
1399
+ var out = "";
1400
+ var studs = "low";
1401
+ var prims = "full";
1402
+ var weld = true;
1403
+ var colors = true;
1404
+ var all = false;
1405
+ var cond = false;
1406
+ var textures = false;
1407
+ for (let i = 0;i < a.length; i++) {
1408
+ const k = a[i];
1409
+ const v = () => {
1410
+ const x = a[++i];
1411
+ if (x === undefined) {
1412
+ console.error(`missing value for ${k}`);
1413
+ process.exit(1);
1414
+ }
1415
+ return x;
1416
+ };
1417
+ if (k === "--ldraw")
1418
+ ldraw = v();
1419
+ else if (k === "-o" || k === "--out")
1420
+ out = v();
1421
+ else if (k === "--part")
1422
+ parts.push(v());
1423
+ else if (k === "--studs")
1424
+ studs = v();
1425
+ else if (k === "--prims")
1426
+ prims = v();
1427
+ else if (k === "--no-weld")
1428
+ weld = false;
1429
+ else if (k === "--no-colors")
1430
+ colors = false;
1431
+ else if (k === "--cond")
1432
+ cond = true;
1433
+ else if (k === "--textures")
1434
+ textures = true;
1435
+ else if (k === "--all-parts")
1436
+ all = true;
1437
+ else if (k.startsWith("-")) {
1438
+ console.error(`unknown option ${k}
1439
+
1440
+ ${HELP}`);
1441
+ process.exit(1);
1442
+ } else
1443
+ models.push(k);
1444
+ }
1445
+ if (!ldraw || !out) {
1446
+ console.error(HELP);
1447
+ process.exit(1);
1448
+ }
1449
+ if (!fs4.existsSync(ldraw + "/LDConfig.ldr")) {
1450
+ console.error(`${ldraw} does not look like an LDraw library (no LDConfig.ldr)`);
1451
+ process.exit(1);
1452
+ }
1453
+ var t0 = performance.now();
1454
+ var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors, cond, textures });
1455
+ var data = r.data;
1456
+ if (out.endsWith(".gz"))
1457
+ data = zlib.gzipSync(data, { level: 9 });
1458
+ else if (out.endsWith(".zst")) {
1459
+ const B = globalThis.Bun, z = zlib;
1460
+ if (B?.zstdCompressSync)
1461
+ data = B.zstdCompressSync(data, { level: 19 });
1462
+ else if (z.zstdCompressSync)
1463
+ data = z.zstdCompressSync(data, { params: { [z.constants.ZSTD_c_compressionLevel]: 19 } });
1464
+ else {
1465
+ console.error("zstd output needs Bun or Node >= 22.15 (try `bunx ldr pack ...` or use a .gz output)");
1466
+ process.exit(1);
1467
+ }
1468
+ }
1469
+ fs4.writeFileSync(out, data);
1470
+ 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`);
1471
+ if (r.missing.length)
1472
+ console.warn(`missing ${r.missing.length} refs: ${r.missing.slice(0, 20).join(", ")}${r.missing.length > 20 ? " …" : ""}`);