ldr 0.3.0 → 0.4.1

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.
@@ -6,6 +6,17 @@ function parseColor(tok) {
6
6
  return +tok;
7
7
  }
8
8
  var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
9
+ var SPECIAL0 = new Set(["FILE", "NOFILE", "!DATA", "!:", "STEP", "BFC", "ROTSTEP", "BUFEXCHG", "!TEXMAP"]);
10
+ var PART_ORG = new Set(["part", "subpart", "primitive", "8_primitive", "48_primitive", "shortcut", "helper"]);
11
+ var MODEL_ORG = new Set(["model", "submodel", "configuration"]);
12
+ var ORG_RE = /^(?:!?LDRAW_ORG|Unofficial|Official\s+LCAD)\s+(\S+)/i;
13
+ function orgType(text) {
14
+ const m = ORG_RE.exec(text);
15
+ if (!m)
16
+ return;
17
+ const t = m[1].toLowerCase().replace(/^unofficial_/, "");
18
+ return PART_ORG.has(t) || MODEL_ORG.has(t) ? t : undefined;
19
+ }
9
20
  function texNames(rest) {
10
21
  const out = [];
11
22
  let i = 0;
@@ -91,7 +102,8 @@ function geomLine(tk) {
91
102
  return { t: 4, color, p: nums.slice(0, 12) };
92
103
  return { t: 5, color, p: nums.slice(0, 12) };
93
104
  }
94
- function parse(src, name = "main.ldr") {
105
+ function parse(src, name = "main.ldr", popts = {}) {
106
+ const gtext = !!popts.geometryText;
95
107
  const files = new Map;
96
108
  let cur = null;
97
109
  let main = "";
@@ -107,6 +119,7 @@ function parse(src, name = "main.ldr") {
107
119
  (data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
108
120
  dataCur = null;
109
121
  };
122
+ let inHeader = true;
110
123
  const start = (n) => {
111
124
  closeData();
112
125
  cur = { name: norm(n), cmds: [] };
@@ -115,6 +128,7 @@ function parse(src, name = "main.ldr") {
115
128
  main = cur.name;
116
129
  texStack = [];
117
130
  texNext = null;
131
+ inHeader = true;
118
132
  };
119
133
  let skip = false, sawBlock = false;
120
134
  const firstBlock = () => {
@@ -144,6 +158,53 @@ function parse(src, name = "main.ldr") {
144
158
  const line = raw.trim();
145
159
  if (!line)
146
160
  continue;
161
+ if (gtext && line.charCodeAt(0) >= 50 && line.charCodeAt(0) <= 53 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
162
+ if (dataCur || skip)
163
+ continue;
164
+ if (!cur)
165
+ start(name);
166
+ const sp = line.slice(2).trimStart();
167
+ const e = sp.search(/\s/);
168
+ const g = { t: line.charCodeAt(0) - 48, color: parseColor(e < 0 ? sp : sp.slice(0, e)), p: [], src: line };
169
+ annotate(g, false);
170
+ cur.cmds.push(g);
171
+ inHeader = false;
172
+ continue;
173
+ }
174
+ if (line.charCodeAt(0) === 48 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
175
+ let a = 2;
176
+ while (line.charCodeAt(a) === 32 || line.charCodeAt(a) === 9)
177
+ a++;
178
+ let b = a;
179
+ while (b < line.length && line.charCodeAt(b) !== 32 && line.charCodeAt(b) !== 9)
180
+ b++;
181
+ const kw = line.slice(a, b);
182
+ if (!SPECIAL0.has(kw)) {
183
+ if (skip || dataCur)
184
+ continue;
185
+ if (!cur)
186
+ start(name);
187
+ const text = line.slice(a);
188
+ const c = { t: 0, text };
189
+ if (kw.charCodeAt(0) === 33)
190
+ c.meta = kw;
191
+ texNext = null;
192
+ if (inHeader && !cur.org) {
193
+ const o = orgType(text);
194
+ if (o)
195
+ cur.org = o;
196
+ }
197
+ const moved = text.match(MOVED_RE);
198
+ if (moved && !cur.movedTo) {
199
+ let target = moved[1].trim().replace(/\\/g, "/");
200
+ if (!/\.(dat|ldr|mpd)$/i.test(target))
201
+ target += ".dat";
202
+ cur.movedTo = norm(target);
203
+ }
204
+ cur.cmds.push(c);
205
+ continue;
206
+ }
207
+ }
147
208
  const tk = line.split(/\s+/);
148
209
  const t = +tk[0];
149
210
  if (t === 0) {
@@ -186,6 +247,9 @@ function parse(src, name = "main.ldr") {
186
247
  c.bfc = tk.slice(2).join(" ");
187
248
  else if (tk[1] === "ROTSTEP") {
188
249
  c.meta = "ROTSTEP";
250
+ c.step = true;
251
+ texStack = [];
252
+ texNext = null;
189
253
  if ((tk[2] ?? "").toUpperCase() === "END")
190
254
  c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
191
255
  else {
@@ -198,6 +262,11 @@ function parse(src, name = "main.ldr") {
198
262
  c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
199
263
  } else if (tk[1]?.startsWith("!"))
200
264
  c.meta = tk[1];
265
+ if (inHeader && !cur.org) {
266
+ const o = orgType(text);
267
+ if (o)
268
+ cur.org = o;
269
+ }
201
270
  if (tk[1] === "!TEXMAP") {
202
271
  const op = (tk[2] ?? "").toUpperCase();
203
272
  texNext = null;
@@ -248,6 +317,7 @@ function parse(src, name = "main.ldr") {
248
317
  continue;
249
318
  annotate(g, false);
250
319
  cur.cmds.push(g);
320
+ inHeader = false;
251
321
  }
252
322
  closeData();
253
323
  return data ? { main, files, data } : { main, files };
@@ -427,34 +497,249 @@ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
427
497
  }
428
498
  return out;
429
499
  }
500
+ var kindMemo = new WeakMap;
501
+ var SUBREF_RE = /^(s|8|48)\//;
502
+ function fileKind(doc, name) {
503
+ const f = doc.files.get(norm(name));
504
+ if (!f)
505
+ return "part";
506
+ let k = kindMemo.get(f);
507
+ if (k)
508
+ return k;
509
+ if (f.org)
510
+ k = PART_ORG.has(f.org) ? "part" : "model";
511
+ else if (f.name === doc.main)
512
+ k = "model";
513
+ else
514
+ k = f.cmds.some((c) => c.t >= 2 || c.t === 0 && !!c.geom || c.t === 1 && SUBREF_RE.test(c.file)) ? "part" : "model";
515
+ kindMemo.set(f, k);
516
+ return k;
517
+ }
518
+ var looseMemo = new WeakMap;
519
+ function looseSteps(f) {
520
+ let r = looseMemo.get(f);
521
+ if (r)
522
+ return r;
523
+ r = [];
524
+ let s = 0;
525
+ for (const c of f.cmds) {
526
+ if (c.t === 0 && c.step) {
527
+ s++;
528
+ continue;
529
+ }
530
+ if ((c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) && r[r.length - 1] !== s)
531
+ r.push(s);
532
+ }
533
+ looseMemo.set(f, r);
534
+ return r;
535
+ }
536
+ var colorToken = (c) => c > 16777215 ? "0x" + c.toString(16).toUpperCase() : String(c);
537
+ var lineOf = (c) => c.t === 0 ? "0 " + c.text : c.t === 1 ? `1 ${colorToken(c.color)} ${c.m.join(" ")} ${c.file}` : c.src ? c.src : `${c.t} ${colorToken(c.color)} ${c.p.join(" ")}`;
538
+ var loosePartName = (file, step) => `${norm(file)}#${step}`;
539
+ function looseFile(f, step) {
540
+ const out = [];
541
+ let s = 0;
542
+ const isNext = (c) => !!c && c.t === 0 && c.texmap?.op === "NEXT";
543
+ for (const c of f.cmds) {
544
+ if (c.t === 0) {
545
+ if (c.step) {
546
+ s++;
547
+ continue;
548
+ }
549
+ if (c.bfc !== undefined) {
550
+ out.push(c);
551
+ continue;
552
+ }
553
+ if (s !== step)
554
+ continue;
555
+ if (c.texmap)
556
+ out.push(c);
557
+ else if (c.geom) {
558
+ if (c.geom.t >= 2)
559
+ out.push(c);
560
+ else if (isNext(out[out.length - 1]))
561
+ out.pop();
562
+ }
563
+ continue;
564
+ }
565
+ if (s !== step)
566
+ continue;
567
+ if (c.t === 1) {
568
+ if (isNext(out[out.length - 1]))
569
+ out.pop();
570
+ continue;
571
+ }
572
+ out.push(c);
573
+ }
574
+ return { name: loosePartName(f.name, step), cmds: out };
575
+ }
576
+ function looseSource(f, step) {
577
+ return [`0 ldr: own geometry of ${f.name}, step ${step}`, ...looseFile(f, step).cmds.map(lineOf)].join(`
578
+ `) + `
579
+ `;
580
+ }
581
+ function hash32(s) {
582
+ let h = 2166136261;
583
+ for (let i = 0;i < s.length; i++) {
584
+ h ^= s.charCodeAt(i);
585
+ h = Math.imul(h, 16777619);
586
+ }
587
+ return (h >>> 0).toString(16).padStart(8, "0");
588
+ }
589
+ function invert(m) {
590
+ const [x, y, z, a, b, c, d, e, f, g, h, i] = m;
591
+ const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
592
+ const det = a * A + b * B + c * C;
593
+ if (!Number.isFinite(det) || Math.abs(det) < 0.000000000001)
594
+ return null;
595
+ const k = 1 / det;
596
+ const r = [A * k, -(b * i - c * h) * k, (b * f - c * e) * k, B * k, (a * i - c * g) * k, -(a * f - c * d) * k, C * k, -(a * h - b * g) * k, (a * e - b * d) * k];
597
+ return [-(r[0] * x + r[1] * y + r[2] * z), -(r[3] * x + r[4] * y + r[5] * z), -(r[6] * x + r[7] * y + r[8] * z), ...r];
598
+ }
599
+ var num = (x) => String(+x.toFixed(6) || 0);
600
+ var quote = (s) => '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"';
601
+ function texWrapper(part, map) {
602
+ const v = [...map.p1, ...map.p2, ...map.p3, ...map.method !== "PLANAR" ? [map.a ?? 360] : [], ...map.method === "SPHERICAL" ? [map.b ?? 180] : []].map(num).join(" ");
603
+ const source = `0 ldr: ${part} with a model-level !TEXMAP
604
+ 0 BFC CERTIFY CCW
605
+ 0 !TEXMAP START ${map.method} ${v} ${quote(map.texture)}${map.glossmap ? " GLOSSMAP " + quote(map.glossmap) : ""}
606
+ 1 16 0 0 0 1 0 0 0 1 0 0 0 1 ${norm(part)}
607
+ 0 !TEXMAP END
608
+ `;
609
+ return { name: `${norm(part)}#tex-${hash32(source)}`, source };
610
+ }
611
+ var mul3 = (a, b) => {
612
+ const r = new Array(9);
613
+ for (let i = 0;i < 3; i++)
614
+ for (let j = 0;j < 3; j++)
615
+ r[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
616
+ return r;
617
+ };
618
+ function rotStepMatrix(x, y, z) {
619
+ const D = Math.PI / 180, s1 = Math.sin(x * D), c1 = Math.cos(x * D), s2 = Math.sin(y * D), c2 = Math.cos(y * D), s3 = Math.sin(z * D), c3 = Math.cos(z * D);
620
+ return [c2 * c3, -c2 * s3, s2, c1 * s3 + s1 * s2 * c3, c1 * c3 - s1 * s2 * s3, -s1 * c2, s1 * s3 - c1 * s2 * c3, s1 * c3 + c1 * s2 * s3, c1 * c2];
621
+ }
622
+ function stepViewMatrix(view, defaultView = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {
623
+ return !view ? [...defaultView] : view.abs ? [...view.m] : mul3(defaultView, view.m);
624
+ }
625
+ var viewMemo = new WeakMap;
626
+ function fileStepViews(f) {
627
+ const hit = viewMemo.get(f);
628
+ if (hit)
629
+ return hit;
630
+ const out = [];
631
+ let cur = null;
632
+ for (const c of f.cmds) {
633
+ if (c.t !== 0 || !c.step)
634
+ continue;
635
+ const r = c.rotstep;
636
+ if (r) {
637
+ if (r.mode === "END")
638
+ cur = null;
639
+ else {
640
+ const R = rotStepMatrix(r.x, r.y, r.z);
641
+ cur = r.mode === "ABS" ? { abs: true, m: R, rotstep: r } : r.mode === "ADD" && cur ? { abs: cur.abs, m: mul3(cur.m, R), rotstep: r } : { abs: false, m: R, rotstep: r };
642
+ }
643
+ }
644
+ out.push(cur);
645
+ }
646
+ out.push(cur);
647
+ viewMemo.set(f, out);
648
+ return out;
649
+ }
430
650
  // src/model.ts
431
651
  function flatten(doc, opts = {}) {
432
652
  const out = [];
433
653
  const maxDepth = opts.maxDepth ?? 64;
434
- const walk = (d, fname, m, color, path, stepPath, depth) => {
654
+ const loose = opts.looseGeometry !== false, tx = !!opts.textures;
655
+ const src = new Map;
656
+ const lazy = (f, k) => {
657
+ const key = f.name + "#" + k;
658
+ let fn = src.get(key);
659
+ if (!fn) {
660
+ let s;
661
+ fn = Object.assign(() => s ??= looseSource(f, k), { file: f, step: k });
662
+ src.set(key, fn);
663
+ }
664
+ return fn;
665
+ };
666
+ const walk = (d, fname, m, color, path, stepPath, depth, texIn) => {
435
667
  const f = d.files.get(fname);
436
668
  if (!f || depth > maxDepth)
437
669
  return;
438
670
  let s = 0;
439
- for (const c of f.cmds) {
440
- if (c.t === 0 && c.step)
671
+ const views = fileStepViews(f);
672
+ const ls = loose ? looseSteps(f) : [];
673
+ let li = 0;
674
+ const emit = (part, col, mm, sp, k, embedded, tex) => {
675
+ const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
676
+ if (embedded)
677
+ i.embedded = true;
678
+ if (tex) {
679
+ const w = texWrapper(part, tex);
680
+ i.texPart = w.name;
681
+ i.texSource = w.source;
682
+ }
683
+ const v = views[k];
684
+ if (v)
685
+ i.view = v;
686
+ out.push(i);
687
+ return i;
688
+ };
689
+ const flushLoose = (upTo) => {
690
+ while (li < ls.length && ls[li] <= upTo) {
691
+ const k = ls[li++];
692
+ emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null).source = lazy(f, k);
693
+ }
694
+ };
695
+ for (const c0 of f.cmds) {
696
+ if (c0.t === 0 && c0.step) {
441
697
  s++;
698
+ continue;
699
+ }
700
+ let c = c0;
701
+ if (c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) {
702
+ flushLoose(s);
703
+ continue;
704
+ }
705
+ if (c.t === 0) {
706
+ if (tx && c.geom?.t === 1)
707
+ c = c.geom;
708
+ else
709
+ continue;
710
+ }
442
711
  if (c.t !== 1)
443
712
  continue;
713
+ if (tx && c.fallback)
714
+ continue;
444
715
  const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
716
+ const tex = tx ? c.tex ?? texIn : null;
717
+ const local = tex ? invert(c.m) : null;
718
+ const lt = tex && local ? transformTexMap(tex, local) : null;
445
719
  if (d.files.has(c.file)) {
446
- walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
720
+ if (fileKind(d, c.file) === "model") {
721
+ walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1, lt);
722
+ continue;
723
+ }
724
+ emit(c.file, col, mm, sp, s, true, lt);
447
725
  continue;
448
726
  }
449
727
  const ext = opts.resolveModel?.(c.file);
450
728
  if (ext) {
451
- walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
729
+ walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1, lt);
452
730
  continue;
453
731
  }
454
- out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
732
+ emit(c.file, col, mm, sp, s, false, lt);
455
733
  }
734
+ flushLoose(Infinity);
456
735
  };
457
- walk(doc, doc.main, I, 16, doc.main, [], 0);
736
+ if (fileKind(doc, doc.main) === "part") {
737
+ const f = doc.files.get(doc.main);
738
+ if (f)
739
+ out.push({ part: doc.main, color: 16, m: [...I], step: 0, steps: [0], localStep: 0, path: doc.main, embedded: true });
740
+ return out;
741
+ }
742
+ walk(doc, doc.main, I, 16, doc.main, [], 0, null);
458
743
  return out;
459
744
  }
460
745
  function parseModel(text, name = "main.ldr", opts) {
@@ -465,8 +750,14 @@ function sequenceSteps(insts) {
465
750
  const uniq = [...new Set(insts.map(key))].sort();
466
751
  const rank = new Map(uniq.map((k, j) => [k, j]));
467
752
  const stepOf = new Int32Array(insts.length);
468
- insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
469
- return { count: uniq.length, stepOf };
753
+ const views = new Array(uniq.length).fill(null);
754
+ insts.forEach((i, k) => {
755
+ const r = rank.get(key(i));
756
+ stepOf[k] = r;
757
+ if (i.view)
758
+ views[r] = i.view;
759
+ });
760
+ return { count: uniq.length, stepOf, views };
470
761
  }
471
762
  function parseLDConfig(text) {
472
763
  const out = new Map;
@@ -830,7 +1121,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
830
1121
  mark("culling");
831
1122
  const edgeObjs = [];
832
1123
  const condObjs = [];
833
- const wantCond = P.cond !== false && P.edges === "idx";
1124
+ const wantCond = P.cond !== false && (P.edges === "idx" || P.edges === true);
834
1125
  let condMat = null;
835
1126
  const cmat = () => condMat ??= condLineMat();
836
1127
  const condCache = new Map;
@@ -1098,11 +1389,18 @@ function toPlastic(old) {
1098
1389
  function createLdrObject(instances, pack, options = {}) {
1099
1390
  const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
1100
1391
  const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
1101
- const all = instances.map((i) => ({ part: i.part, color: String(i.color), m: i.m, steps: i.steps }));
1392
+ const useTex = o.textures !== false;
1393
+ const pick = (i) => useTex && i.texPart && pack.header.parts[i.texPart] ? i.texPart : i.part;
1394
+ const all = instances.map((i) => ({ part: pick(i), color: String(i.color), m: i.m, steps: i.steps }));
1102
1395
  const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
1103
1396
  const uniq = [...new Set(all.map(key))].sort();
1104
1397
  const rank = new Map(uniq.map((k, j) => [k, j]));
1105
1398
  const stepOf = all.map((i) => rank.get(key(i)));
1399
+ const stepViews = new Array(uniq.length).fill(null);
1400
+ instances.forEach((i, k) => {
1401
+ if (i.view)
1402
+ stepViews[stepOf[k]] = i.view;
1403
+ });
1106
1404
  const missing = [...new Set(all.filter((i) => !pack.header.parts[i.part]).map((i) => i.part))];
1107
1405
  const object = new THREE2.Group;
1108
1406
  object.name = "ldr";
@@ -1160,6 +1458,7 @@ function createLdrObject(instances, pack, options = {}) {
1160
1458
  object,
1161
1459
  stats,
1162
1460
  stepCount: uniq.length,
1461
+ stepViews,
1163
1462
  setStep: (s) => build(s),
1164
1463
  setEdges: (v) => {
1165
1464
  edges = v;
@@ -1219,6 +1518,16 @@ class Library {
1219
1518
  unofficialRoot;
1220
1519
  cache = new Map;
1221
1520
  meshCache = new Map;
1521
+ embedded = new Map;
1522
+ register(name, doc, file = doc.main) {
1523
+ name = norm(name);
1524
+ const prev = this.embedded.get(name);
1525
+ if (prev && prev.files === doc.files && prev.main === file)
1526
+ return;
1527
+ if (prev || this.lookup(name))
1528
+ this.meshCache.clear();
1529
+ this.embedded.set(name, file === doc.main ? doc : { main: file, files: doc.files, ...doc.data ? { data: doc.data } : {} });
1530
+ }
1222
1531
  constructor(root, unofficialRoot) {
1223
1532
  this.root = root;
1224
1533
  this.unofficialRoot = unofficialRoot;
@@ -1229,6 +1538,9 @@ class Library {
1229
1538
  if (seen.has(name))
1230
1539
  return null;
1231
1540
  seen.add(name);
1541
+ const emb = this.embedded.get(name);
1542
+ if (emb)
1543
+ return { kind: "part", doc: emb };
1232
1544
  const hit = this.lookup(name);
1233
1545
  if (!hit)
1234
1546
  return null;
@@ -1403,34 +1715,48 @@ class Library {
1403
1715
  emitFace([...ordered].reverse());
1404
1716
  }
1405
1717
  }
1406
- flatten(doc) {
1407
- const out = [];
1408
- const walk = (d, fname, m, color, path, stepPath) => {
1409
- const f = d.files.get(fname);
1410
- if (!f)
1411
- return;
1412
- let s = 0;
1413
- for (const c of f.cmds) {
1414
- if (c.t === 0 && c.step)
1415
- s++;
1416
- if (c.t !== 1)
1417
- continue;
1418
- const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
1419
- if (d.files.has(c.file)) {
1420
- walk(d, c.file, mm, col, path + "/" + c.file, sp);
1421
- continue;
1422
- }
1423
- const r = this.where(c.file);
1424
- if (r?.kind === "model")
1425
- walk(r.doc, r.doc.main, mm, col, path + "/" + c.file, sp);
1426
- else {
1427
- const part = r ? r.doc.main : c.file;
1428
- out.push({ part, color: col, m: mm, step: sp[0], localStep: s, steps: sp, path, missing: !r, aliasOf: r?.aliasOf });
1718
+ flatten(doc, opts = {}) {
1719
+ for (const f of doc.files.keys())
1720
+ this.register(f, doc, f);
1721
+ const memo = new Map;
1722
+ const at = (n) => {
1723
+ if (!memo.has(n))
1724
+ memo.set(n, this.where(n));
1725
+ return memo.get(n);
1726
+ };
1727
+ const insts = flatten(doc, { ...opts, resolveModel: (n) => {
1728
+ const r = at(n);
1729
+ return r?.kind === "model" ? r.doc : null;
1730
+ } });
1731
+ const synth = new Map;
1732
+ for (const i of insts) {
1733
+ if (i.texPart && !synth.has(i.texPart)) {
1734
+ synth.set(i.texPart, i.texSource);
1735
+ this.register(i.texPart, parse(i.texSource, i.texPart));
1736
+ }
1737
+ if (i.embedded) {
1738
+ i.missing = false;
1739
+ continue;
1740
+ }
1741
+ if (i.source) {
1742
+ if (!synth.has(i.part)) {
1743
+ synth.set(i.part, "");
1744
+ const s = i.source;
1745
+ const lf = s.file ? looseFile(s.file, s.step) : parse(s(), i.part).files.get(i.part);
1746
+ this.register(i.part, { main: i.part, files: new Map([[i.part, lf]]) });
1429
1747
  }
1748
+ i.missing = false;
1749
+ continue;
1430
1750
  }
1431
- };
1432
- walk(doc, doc.main, I, 16, doc.main, []);
1433
- return out;
1751
+ const r = at(i.part);
1752
+ i.missing = !r;
1753
+ if (r) {
1754
+ i.part = r.doc.main;
1755
+ if (r.aliasOf)
1756
+ i.aliasOf = r.aliasOf;
1757
+ }
1758
+ }
1759
+ return insts;
1434
1760
  }
1435
1761
  }
1436
1762
  // src/node/build-pack.ts
@@ -1496,8 +1822,22 @@ class Library2 {
1496
1822
  this.index.set(r.toLowerCase(), path.join(dir, e.name));
1497
1823
  }
1498
1824
  }
1825
+ virtual = new Map;
1826
+ conflicts = new Set;
1827
+ addVirtual(name, text) {
1828
+ const n = norm(name), key = "mpd/" + n, prev = this.virtual.get(n);
1829
+ if (prev === undefined) {
1830
+ this.virtual.set(n, text);
1831
+ this.text.set(key, text);
1832
+ this.index.set(key, "(mpd) " + n);
1833
+ } else if (prev !== text)
1834
+ this.conflicts.add(n);
1835
+ return key;
1836
+ }
1499
1837
  resolve(ref) {
1500
1838
  const n = norm2(ref);
1839
+ if (this.virtual.size && this.virtual.has(n))
1840
+ return "mpd/" + n;
1501
1841
  for (const pre of ["parts/", "p/", "models/"])
1502
1842
  if (this.index.has(pre + n))
1503
1843
  return pre + n;
@@ -1636,32 +1976,42 @@ function splitModel(text, fallbackName = "main.ldr") {
1636
1976
  flush();
1637
1977
  return { files, main, data };
1638
1978
  }
1639
- function flatten2(model, lib, onInstance) {
1979
+ function modelDoc(model) {
1980
+ const files = new Map;
1981
+ for (const [n, t] of model.files) {
1982
+ const f = parse(t, n, { geometryText: true }).files.get(norm(n)) ?? { name: norm(n), cmds: [] };
1983
+ files.set(norm(n), f);
1984
+ }
1985
+ return { main: norm(model.main ?? ""), files };
1986
+ }
1987
+ function flatten2(model, lib, onInstance, opts = {}) {
1640
1988
  const missing = new Map;
1641
- const walk = (name, parentM, parentColor, depth) => {
1642
- if (depth > 32)
1643
- return;
1644
- for (const line of model.files.get(name).split(`
1645
- `)) {
1646
- const r = parseType1(line);
1647
- if (!r)
1648
- continue;
1649
- const color = r.color === "16" ? parentColor : r.color;
1650
- const m = compose(parentM, r.m);
1651
- const n = norm2(r.name);
1652
- if (model.files.has(n)) {
1653
- walk(n, m, color, depth + 1);
1654
- continue;
1655
- }
1656
- const key = lib.resolve(r.name);
1657
- if (!key) {
1658
- missing.set(n, (missing.get(n) || 0) + 1);
1659
- continue;
1660
- }
1661
- onInstance({ part: key, ref: n, color, m });
1989
+ for (const [n, t] of model.files)
1990
+ lib.addVirtual(n, t);
1991
+ const doc = modelDoc(model);
1992
+ const at = new Map;
1993
+ for (const i of flatten(doc, opts)) {
1994
+ let key;
1995
+ if (i.embedded)
1996
+ key = "mpd/" + i.part;
1997
+ else if (i.source)
1998
+ key = lib.addVirtual(i.part, i.source());
1999
+ else {
2000
+ if (!at.has(i.part))
2001
+ at.set(i.part, lib.resolve(i.part));
2002
+ key = at.get(i.part);
1662
2003
  }
1663
- };
1664
- walk(model.main, IDENT, "16", 0);
2004
+ if (!key) {
2005
+ missing.set(i.part, (missing.get(i.part) || 0) + 1);
2006
+ continue;
2007
+ }
2008
+ const r = { part: key, ref: i.part, color: colorToken(i.color), m: i.m, inst: i };
2009
+ if (i.texPart) {
2010
+ r.texRef = i.texPart;
2011
+ r.texPart = lib.addVirtual(i.texPart, i.texSource);
2012
+ }
2013
+ onInstance(r);
2014
+ }
1665
2015
  return { missing };
1666
2016
  }
1667
2017
  function collectDeps(lib, refs) {
@@ -2270,9 +2620,15 @@ function buildPack(o) {
2270
2620
  for (const [k, v] of sm.data)
2271
2621
  if (!data.has(k))
2272
2622
  data.set(k, v);
2273
- const r = flatten2(sm, lib, (i) => refs.set(i.ref, i.part));
2274
- for (const k of r.missing.keys())
2275
- missing.add(k);
2623
+ for (const textures of o.textures ? [false, true] : [false]) {
2624
+ const r = flatten2(sm, lib, (i) => {
2625
+ refs.set(i.ref, i.part);
2626
+ if (i.texRef)
2627
+ refs.set(i.texRef, i.texPart);
2628
+ }, { textures, looseGeometry: o.looseGeometry });
2629
+ for (const k of r.missing.keys())
2630
+ missing.add(k);
2631
+ }
2276
2632
  }
2277
2633
  for (const p of o.parts ?? []) {
2278
2634
  const k = lib.resolve(p);
@@ -2311,7 +2667,8 @@ function buildPack(o) {
2311
2667
  const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
2312
2668
  return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
2313
2669
  };
2314
- return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
2670
+ const warnings = [...lib.conflicts].map((n) => `${n}: defined differently by two models; the first definition is used`);
2671
+ return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing], ...warnings.length ? { warnings } : {} };
2315
2672
  }
2316
2673
  export {
2317
2674
  Compiler,
@@ -2321,6 +2678,7 @@ export {
2321
2678
  buildLdrScene,
2322
2679
  buildPack,
2323
2680
  collectDeps,
2681
+ colorToken,
2324
2682
  computeCulling,
2325
2683
  conditionalLineVisible,
2326
2684
  conditionalLines,
@@ -2330,11 +2688,19 @@ export {
2330
2688
  decodePack,
2331
2689
  decompressPack,
2332
2690
  encodePack,
2691
+ fileKind,
2692
+ fileStepViews,
2333
2693
  flatten,
2334
2694
  flatten2 as flattenWithLibrary,
2695
+ hash32,
2696
+ invert,
2335
2697
  isDirectColor,
2336
2698
  loadColors,
2337
2699
  loadPack,
2700
+ looseFile,
2701
+ loosePartName,
2702
+ looseSource,
2703
+ looseSteps,
2338
2704
  makeMaterial,
2339
2705
  mul,
2340
2706
  norm,
@@ -2344,9 +2710,12 @@ export {
2344
2710
  parseModel,
2345
2711
  parseTexMap,
2346
2712
  resolveTexture,
2713
+ rotStepMatrix,
2347
2714
  sequenceSteps,
2348
2715
  smoothNormals,
2349
2716
  splitModel,
2717
+ stepViewMatrix,
2718
+ texWrapper,
2350
2719
  texmapTriangleUV,
2351
2720
  texmapUV,
2352
2721
  transformTexMap,