ldr 0.2.0 → 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 CHANGED
@@ -145,31 +145,67 @@ class Library {
145
145
  }
146
146
  function splitModel(text, fallbackName = "main.ldr") {
147
147
  const files = new Map;
148
+ const data = new Map;
149
+ let dataCur = null;
148
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
+ };
149
168
  for (const line of text.split(/\r?\n/)) {
150
169
  const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
151
170
  if (m) {
152
- if (cur)
153
- files.set(cur, buf.join(`
154
- `));
171
+ firstBlock();
172
+ flush();
173
+ skip = false;
174
+ dataCur = null;
155
175
  cur = norm(m[1]);
156
- buf = [];
157
176
  if (!main)
158
177
  main = cur;
159
178
  continue;
160
179
  }
161
- if (/^\s*0\s+NOFILE/i.test(line))
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;
162
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
+ }
163
201
  if (!cur) {
164
202
  cur = norm(fallbackName);
165
203
  main = cur;
166
204
  }
167
205
  buf.push(line);
168
206
  }
169
- if (cur)
170
- files.set(cur, buf.join(`
171
- `));
172
- return { files, main };
207
+ flush();
208
+ return { files, main, data };
173
209
  }
174
210
  function flatten(model, lib, onInstance) {
175
211
  const missing = new Map;
@@ -223,6 +259,438 @@ function collectDeps(lib, refs) {
223
259
  // src/node/compile.ts
224
260
  import fs2 from "node:fs";
225
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
226
694
  function loadColors(root) {
227
695
  const map = new Map;
228
696
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -270,7 +738,7 @@ class Compiler {
270
738
  const ck = key + (separateStuds ? "" : "#nostudsep");
271
739
  if (this.cache.has(ck))
272
740
  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 };
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 };
274
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];
275
743
  const walk = (fk, M, color, invert, target, depth) => {
276
744
  if (depth > 40)
@@ -325,6 +793,13 @@ class Compiler {
325
793
  }
326
794
  if (c === "5") {
327
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
+ }
328
803
  continue;
329
804
  }
330
805
  if (c === "3" || c === "4") {
@@ -351,6 +826,8 @@ class Compiler {
351
826
  out.uncertified = true;
352
827
  };
353
828
  walk(key, IDENT, "16", false, "body", 0);
829
+ if (this.opts.tex && this.hasTex(key))
830
+ this.texPass(key, out, separateStuds, colorOf);
354
831
  const [x0, y0, z0, x1, y1, z1] = out.bbox;
355
832
  let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
356
833
  for (const b of [out.body, out.under])
@@ -367,6 +844,169 @@ class Compiler {
367
844
  this.cache.set(ck, out);
368
845
  return out;
369
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
+ }
370
1010
  }
371
1011
  function enclosureMask(c) {
372
1012
  const [x0, , z0, x1, y1, z1] = c.bbox;
@@ -449,10 +1089,10 @@ function growBox(b, M, cb) {
449
1089
  for (const z of [cb[2], cb[5]])
450
1090
  grow(b, xf(M, [x, y, z]));
451
1091
  }
452
- function writePack(file, parts, studKinds, weld, extra) {
453
- fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
1092
+ function writePack(file, parts, studKinds, weld, extra, options) {
1093
+ fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
454
1094
  }
455
- function encodePack(parts, studKinds, weld, extra) {
1095
+ function encodePack(parts, studKinds, weld, extra, options = {}) {
456
1096
  const chunks = [];
457
1097
  let off = 0;
458
1098
  const put = (a) => {
@@ -461,13 +1101,15 @@ function encodePack(parts, studKinds, weld, extra) {
461
1101
  chunks.push(new Uint8Array(pad));
462
1102
  off += pad;
463
1103
  }
464
- const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
1104
+ const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
465
1105
  chunks.push(a);
466
1106
  off += a.byteLength;
467
1107
  return r;
468
1108
  };
1109
+ let lastPos = [];
469
1110
  const bucket = (b) => {
470
1111
  const n = b.pos.length / 3;
1112
+ lastPos = [];
471
1113
  if (!n)
472
1114
  return null;
473
1115
  const nrm = new Float32Array(n * 3);
@@ -486,8 +1128,10 @@ function encodePack(parts, studKinds, weld, extra) {
486
1128
  nrm[(i + k) * 3 + 2] = nz;
487
1129
  }
488
1130
  }
489
- if (!weld)
1131
+ if (!weld) {
1132
+ lastPos = b.pos;
490
1133
  return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
1134
+ }
491
1135
  const map = new Map;
492
1136
  const P = [], N = [], C = [], I = [];
493
1137
  for (let i = 0;i < n; i++) {
@@ -503,184 +1147,118 @@ function encodePack(parts, studKinds, weld, extra) {
503
1147
  I.push(j);
504
1148
  }
505
1149
  const vcount = P.length / 3;
1150
+ lastPos = P;
506
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 };
507
1152
  };
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;
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);
552
1168
  }
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);
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]);
580
1179
  }
581
- cur.cmds.push(c);
582
- continue;
1180
+ I[i / 3] = j;
583
1181
  }
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);
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;
646
1190
  }
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;
1191
+ return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
653
1192
  };
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
- }
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;
667
1212
  }
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;
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) };
681
1239
  });
682
- }
683
- return out;
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))]);
684
1262
  }
685
1263
 
686
1264
  // src/model.ts
@@ -739,9 +1317,14 @@ function buildPack(o) {
739
1317
  const lib = new Library(o.ldrawRoot);
740
1318
  const refs = new Map;
741
1319
  const missing = new Set;
1320
+ const data = new Map;
742
1321
  for (const m of o.models ?? []) {
743
1322
  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));
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));
745
1328
  for (const k of r.missing.keys())
746
1329
  missing.add(k);
747
1330
  }
@@ -757,7 +1340,7 @@ function buildPack(o) {
757
1340
  if (lib.isPart(k) && /\.dat$/.test(k))
758
1341
  refs.set(k.slice(6), k);
759
1342
  }
760
- const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
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 });
761
1344
  const parts = new Map, kinds = new Map;
762
1345
  let n = 0;
763
1346
  for (const [ref, key] of refs) {
@@ -778,7 +1361,11 @@ function buildPack(o) {
778
1361
  if (fs3.existsSync(cfg))
779
1362
  extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
780
1363
  }
781
- return { data: encodePack(parts, kinds, comp.opts.weld, extra), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
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] };
782
1369
  }
783
1370
 
784
1371
  // src/cli.ts
@@ -795,6 +1382,9 @@ Options:
795
1382
  --studs low|full Stud detail (default low)
796
1383
  --prims full|low Other primitive detail (default full)
797
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
798
1388
  --no-colors Don't embed the LDConfig colour table
799
1389
  `;
800
1390
  var argv = process.argv.slice(2);
@@ -812,6 +1402,8 @@ var prims = "full";
812
1402
  var weld = true;
813
1403
  var colors = true;
814
1404
  var all = false;
1405
+ var cond = false;
1406
+ var textures = false;
815
1407
  for (let i = 0;i < a.length; i++) {
816
1408
  const k = a[i];
817
1409
  const v = () => {
@@ -836,6 +1428,10 @@ for (let i = 0;i < a.length; i++) {
836
1428
  weld = false;
837
1429
  else if (k === "--no-colors")
838
1430
  colors = false;
1431
+ else if (k === "--cond")
1432
+ cond = true;
1433
+ else if (k === "--textures")
1434
+ textures = true;
839
1435
  else if (k === "--all-parts")
840
1436
  all = true;
841
1437
  else if (k.startsWith("-")) {
@@ -855,7 +1451,7 @@ if (!fs4.existsSync(ldraw + "/LDConfig.ldr")) {
855
1451
  process.exit(1);
856
1452
  }
857
1453
  var t0 = performance.now();
858
- var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors });
1454
+ var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors, cond, textures });
859
1455
  var data = r.data;
860
1456
  if (out.endsWith(".gz"))
861
1457
  data = zlib.gzipSync(data, { level: 9 });