ldr 0.2.0 → 0.4.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/index.js CHANGED
@@ -6,41 +6,250 @@ function parseColor(tok) {
6
6
  return +tok;
7
7
  }
8
8
  var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
9
- function parse(src, name = "main.ldr") {
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
+ }
20
+ function texNames(rest) {
21
+ const out = [];
22
+ let i = 0;
23
+ while (i < rest.length) {
24
+ while (rest[i] === " " || rest[i] === "\t")
25
+ i++;
26
+ if (i >= rest.length)
27
+ break;
28
+ if (rest[i] === '"') {
29
+ let v = "";
30
+ i++;
31
+ while (i < rest.length && rest[i] !== '"') {
32
+ if (rest[i] === "\\" && i + 1 < rest.length)
33
+ i++;
34
+ v += rest[i++];
35
+ }
36
+ i++;
37
+ out.push(v);
38
+ } else {
39
+ let j = i;
40
+ while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
41
+ j++;
42
+ out.push(rest.slice(i, j));
43
+ i = j;
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ function parseTexMap(tail, next = false) {
49
+ const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
50
+ if (!m)
51
+ return null;
52
+ const method = m[1].toUpperCase();
53
+ const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
54
+ if (!n)
55
+ return null;
56
+ const parts = m[2].trim().split(/\s+/);
57
+ if (parts.length < n + 1)
58
+ return null;
59
+ const v = parts.slice(0, n).map(Number);
60
+ if (!v.every(Number.isFinite))
61
+ return null;
62
+ let rest = m[2].trim();
63
+ for (let k = 0;k < n; k++)
64
+ rest = rest.replace(/^\S+\s*/, "");
65
+ const names = texNames(rest);
66
+ if (!names.length)
67
+ return null;
68
+ const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
69
+ if (n >= 10)
70
+ t.a = v[9];
71
+ if (n === 11)
72
+ t.b = v[10];
73
+ const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
74
+ if (g > 0 && names[g + 1])
75
+ t.glossmap = names[g + 1];
76
+ if (next)
77
+ t.next = true;
78
+ return t;
79
+ }
80
+ var b64 = (s) => {
81
+ const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
82
+ const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
83
+ const bin = atob(pad);
84
+ const out = new Uint8Array(bin.length);
85
+ for (let i = 0;i < bin.length; i++)
86
+ out[i] = bin.charCodeAt(i);
87
+ return out;
88
+ };
89
+ function geomLine(tk) {
90
+ const t = +tk[0];
91
+ if (!(t >= 1 && t <= 5) || tk.length < 2)
92
+ return null;
93
+ const color = parseColor(tk[1]);
94
+ const nums = tk.slice(2).map(Number);
95
+ if (t === 1)
96
+ return { t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) };
97
+ if (t === 2)
98
+ return { t: 2, color, p: nums.slice(0, 6) };
99
+ if (t === 3)
100
+ return { t: 3, color, p: nums.slice(0, 9) };
101
+ if (t === 4)
102
+ return { t: 4, color, p: nums.slice(0, 12) };
103
+ return { t: 5, color, p: nums.slice(0, 12) };
104
+ }
105
+ function parse(src, name = "main.ldr", popts = {}) {
106
+ const gtext = !!popts.geometryText;
10
107
  const files = new Map;
11
108
  let cur = null;
12
109
  let main = "";
110
+ let data;
111
+ let dataCur = null;
112
+ let texStack = [];
113
+ let texNext = null;
114
+ const closeData = () => {
115
+ if (!dataCur)
116
+ return;
117
+ const bytes = b64(dataCur.chunks.join(""));
118
+ const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
119
+ (data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
120
+ dataCur = null;
121
+ };
122
+ let inHeader = true;
13
123
  const start = (n) => {
124
+ closeData();
14
125
  cur = { name: norm(n), cmds: [] };
15
126
  files.set(cur.name, cur);
16
127
  if (!main)
17
128
  main = cur.name;
129
+ texStack = [];
130
+ texNext = null;
131
+ inHeader = true;
132
+ };
133
+ let skip = false, sawBlock = false;
134
+ const firstBlock = () => {
135
+ if (sawBlock)
136
+ return;
137
+ sawBlock = true;
138
+ files.clear();
139
+ cur = null;
140
+ main = "";
141
+ };
142
+ const annotate = (c, fromColon) => {
143
+ if (texNext) {
144
+ c.tex = texNext;
145
+ texNext = null;
146
+ return;
147
+ }
148
+ const top = texStack[texStack.length - 1];
149
+ if (!top)
150
+ return;
151
+ if (top.fallback) {
152
+ if (!fromColon)
153
+ c.fallback = true;
154
+ } else
155
+ c.tex = top.map;
18
156
  };
19
157
  for (const raw of src.split(/\r?\n/)) {
20
158
  const line = raw.trim();
21
159
  if (!line)
22
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
+ }
23
208
  const tk = line.split(/\s+/);
24
209
  const t = +tk[0];
25
210
  if (t === 0) {
26
211
  const text = line.slice(1).trim();
27
212
  if (tk[1] === "FILE") {
213
+ firstBlock();
214
+ skip = false;
28
215
  start(text.slice(5));
29
216
  continue;
30
217
  }
31
218
  if (tk[1] === "NOFILE") {
219
+ closeData();
220
+ cur = null;
221
+ skip = true;
222
+ continue;
223
+ }
224
+ if (tk[1] === "!DATA") {
225
+ firstBlock();
226
+ closeData();
32
227
  cur = null;
228
+ skip = false;
229
+ dataCur = { name: text.slice(5).trim(), chunks: [] };
230
+ continue;
231
+ }
232
+ if (skip)
233
+ continue;
234
+ if (dataCur) {
235
+ if (tk[1] === "!:")
236
+ dataCur.chunks.push(tk.slice(2).join(""));
33
237
  continue;
34
238
  }
35
239
  if (!cur)
36
240
  start(name);
37
241
  const c = { t: 0, text };
38
- if (tk[1] === "STEP")
242
+ if (tk[1] === "STEP") {
39
243
  c.step = true;
40
- else if (tk[1] === "BFC")
244
+ texStack = [];
245
+ texNext = null;
246
+ } else if (tk[1] === "BFC")
41
247
  c.bfc = tk.slice(2).join(" ");
42
248
  else if (tk[1] === "ROTSTEP") {
43
249
  c.meta = "ROTSTEP";
250
+ c.step = true;
251
+ texStack = [];
252
+ texNext = null;
44
253
  if ((tk[2] ?? "").toUpperCase() === "END")
45
254
  c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
46
255
  else {
@@ -53,6 +262,42 @@ function parse(src, name = "main.ldr") {
53
262
  c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
54
263
  } else if (tk[1]?.startsWith("!"))
55
264
  c.meta = tk[1];
265
+ if (inHeader && !cur.org) {
266
+ const o = orgType(text);
267
+ if (o)
268
+ cur.org = o;
269
+ }
270
+ if (tk[1] === "!TEXMAP") {
271
+ const op = (tk[2] ?? "").toUpperCase();
272
+ texNext = null;
273
+ if (op === "START" || op === "NEXT") {
274
+ const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
275
+ c.texmap = { op, ...map ? { map } : {} };
276
+ if (map) {
277
+ (cur.texmaps ??= []).push(map);
278
+ if (op === "START")
279
+ texStack.push({ map, fallback: false });
280
+ else
281
+ texNext = map;
282
+ }
283
+ } else if (op === "FALLBACK") {
284
+ c.texmap = { op };
285
+ const top = texStack[texStack.length - 1];
286
+ if (top)
287
+ top.fallback = true;
288
+ } else if (op === "END") {
289
+ c.texmap = { op };
290
+ texStack.pop();
291
+ }
292
+ } else if (tk[1] === "!:") {
293
+ texNext = null;
294
+ const g = geomLine(tk.slice(2));
295
+ if (g) {
296
+ annotate(g, true);
297
+ c.geom = g;
298
+ }
299
+ } else
300
+ texNext = null;
56
301
  const moved = text.match(MOVED_RE);
57
302
  if (moved && !cur.movedTo) {
58
303
  let target = moved[1].trim().replace(/\\/g, "/");
@@ -63,22 +308,110 @@ function parse(src, name = "main.ldr") {
63
308
  cur.cmds.push(c);
64
309
  continue;
65
310
  }
311
+ if (dataCur || skip)
312
+ continue;
66
313
  if (!cur)
67
314
  start(name);
68
- const color = parseColor(tk[1]);
69
- const nums = tk.slice(2).map(Number);
70
- if (t === 1)
71
- cur.cmds.push({ t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) });
72
- else if (t === 2)
73
- cur.cmds.push({ t: 2, color, p: nums.slice(0, 6) });
74
- else if (t === 3)
75
- cur.cmds.push({ t: 3, color, p: nums.slice(0, 9) });
76
- else if (t === 4)
77
- cur.cmds.push({ t: 4, color, p: nums.slice(0, 12) });
78
- else if (t === 5)
79
- cur.cmds.push({ t: 5, color, p: nums.slice(0, 12) });
315
+ const g = geomLine(tk);
316
+ if (!g)
317
+ continue;
318
+ annotate(g, false);
319
+ cur.cmds.push(g);
320
+ inHeader = false;
321
+ }
322
+ closeData();
323
+ return data ? { main, files, data } : { main, files };
324
+ }
325
+ function resolveTexture(doc, name) {
326
+ if (!doc.data)
327
+ return null;
328
+ const n = norm(name);
329
+ return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
330
+ }
331
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
332
+ var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
333
+ 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]];
334
+ var unit3 = (a) => {
335
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
336
+ return [a[0] / l, a[1] / l, a[2] / l];
337
+ };
338
+ var DEG = Math.PI / 180;
339
+ function texmapUV(map, p) {
340
+ const w = texmapTriangleUV(map, [p, p, p]);
341
+ const u = w[0], v = w[1];
342
+ return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
343
+ }
344
+ var radial = (p, c, n) => {
345
+ const d = sub3(p, c);
346
+ const k = dot3(d, n);
347
+ const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
348
+ return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
349
+ };
350
+ function texmapTriangleUV(map, pts) {
351
+ const { p1, p2, p3 } = map;
352
+ const out = new Array(pts.length * 2).fill(0);
353
+ if (map.method === "PLANAR") {
354
+ const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
355
+ const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
356
+ pts.forEach((p, i) => {
357
+ const d = sub3(p, p1);
358
+ out[i * 2] = dot3(d, e1) / l1;
359
+ out[i * 2 + 1] = dot3(d, e2) / l2;
360
+ });
361
+ return out;
80
362
  }
81
- return { main, files };
363
+ const cyl = map.method === "CYLINDRICAL";
364
+ const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
365
+ const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
366
+ const A = (map.a ?? 360) * DEG;
367
+ let base = null, baseAng = 0;
368
+ const sAng = (q) => {
369
+ if (!base) {
370
+ base = q;
371
+ baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
372
+ return baseAng;
373
+ }
374
+ return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
375
+ };
376
+ const miss = [];
377
+ let uSum = 0;
378
+ const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
379
+ pts.forEach((p, i) => {
380
+ if (cyl) {
381
+ const q = dir ? radial(p, p1, n) : null;
382
+ if (q) {
383
+ const a = sAng(q);
384
+ out[i * 2] = 0.5 + a / A;
385
+ uSum += out[i * 2];
386
+ } else
387
+ miss.push(i);
388
+ out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
389
+ } else {
390
+ const pd = unit3(sub3(p, p1));
391
+ const c = dot3(pd, n);
392
+ if (Math.abs(c) > 1 - 0.000000001) {
393
+ miss.push(i);
394
+ out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
395
+ return;
396
+ }
397
+ const a = sAng(radial(p, p1, n));
398
+ uSum += a;
399
+ out[i * 2] = 0.5 + a / A;
400
+ const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
401
+ 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]);
402
+ out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
403
+ }
404
+ });
405
+ if (miss.length && miss.length < pts.length) {
406
+ const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
407
+ for (const i of miss)
408
+ out[i * 2] = avg;
409
+ }
410
+ return out;
411
+ }
412
+ function transformTexMap(map, m) {
413
+ 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]];
414
+ return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
82
415
  }
83
416
  function mul(a, b) {
84
417
  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;
@@ -164,34 +497,249 @@ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
164
497
  }
165
498
  return out;
166
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
+ }
167
650
  // src/model.ts
168
651
  function flatten(doc, opts = {}) {
169
652
  const out = [];
170
653
  const maxDepth = opts.maxDepth ?? 64;
171
- 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) => {
172
667
  const f = d.files.get(fname);
173
668
  if (!f || depth > maxDepth)
174
669
  return;
175
670
  let s = 0;
176
- for (const c of f.cmds) {
177
- 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) {
178
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
+ }
179
711
  if (c.t !== 1)
180
712
  continue;
713
+ if (tx && c.fallback)
714
+ continue;
181
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;
182
719
  if (d.files.has(c.file)) {
183
- 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);
184
725
  continue;
185
726
  }
186
727
  const ext = opts.resolveModel?.(c.file);
187
728
  if (ext) {
188
- 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);
189
730
  continue;
190
731
  }
191
- 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);
192
733
  }
734
+ flushLoose(Infinity);
193
735
  };
194
- 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);
195
743
  return out;
196
744
  }
197
745
  function parseModel(text, name = "main.ldr", opts) {
@@ -202,8 +750,14 @@ function sequenceSteps(insts) {
202
750
  const uniq = [...new Set(insts.map(key))].sort();
203
751
  const rank = new Map(uniq.map((k, j) => [k, j]));
204
752
  const stepOf = new Int32Array(insts.length);
205
- insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
206
- 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 };
207
761
  }
208
762
  function parseLDConfig(text) {
209
763
  const out = new Map;
@@ -241,7 +795,7 @@ function decodePack(buf) {
241
795
  const hl = new DataView(buf).getUint32(0, true);
242
796
  const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, hl)));
243
797
  const base = 4 + hl;
244
- const arr = (r) => r && (r[2] === "f32" ? new Float32Array(buf, base + r[0], r[1]) : r[2] === "u16" ? new Uint16Array(buf, base + r[0], r[1]) : new Uint32Array(buf, base + r[0], r[1]));
798
+ const arr = (r) => r && (r[2] === "f32" ? new Float32Array(buf, base + r[0], r[1]) : r[2] === "u16" ? new Uint16Array(buf, base + r[0], r[1]) : r[2] === "u8" ? new Uint8Array(buf, base + r[0], r[1]) : new Uint32Array(buf, base + r[0], r[1]));
245
799
  return { header, arr, bytes: buf.byteLength };
246
800
  }
247
801
  function geom(b, arr) {
@@ -253,6 +807,8 @@ function geom(b, arr) {
253
807
  g.setAttribute("ldrColor", new THREE.BufferAttribute(arr(b.col), 4));
254
808
  if (b.idx)
255
809
  g.setIndex(new THREE.BufferAttribute(arr(b.idx), 1));
810
+ if (b.uv)
811
+ g.setAttribute("ldrUV", new THREE.BufferAttribute(arr(b.uv), 2));
256
812
  return g;
257
813
  }
258
814
  function makeMaterial(side) {
@@ -268,6 +824,41 @@ function makeMaterial(side) {
268
824
  m.customProgramCacheKey = () => "ldrColor";
269
825
  return m;
270
826
  }
827
+ function makeTexturedMaterial(side, map, wrapU = false) {
828
+ const m = makeMaterial(side);
829
+ const base = m.onBeforeCompile;
830
+ m.onBeforeCompile = (sh, r) => {
831
+ base.call(m, sh, r);
832
+ sh.uniforms.ldrMap = { value: map };
833
+ sh.uniforms.ldrWrapU = { value: wrapU ? 1 : 0 };
834
+ sh.vertexShader = `attribute vec2 ldrUV; varying vec2 vLdrUV;
835
+ ` + sh.vertexShader.replace("#include <uv_vertex>", `#include <uv_vertex>
836
+ vLdrUV = ldrUV;`);
837
+ sh.fragmentShader = `uniform sampler2D ldrMap; uniform int ldrWrapU; varying vec2 vLdrUV;
838
+ ` + sh.fragmentShader.replace("#include <color_fragment>", `#include <color_fragment>
839
+ { vec2 uv = vec2(ldrWrapU == 1 ? fract(vLdrUV.x) : vLdrUV.x, vLdrUV.y); vec4 t = texture2D(ldrMap, uv);
840
+ float inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);
841
+ diffuseColor.rgb = mix(diffuseColor.rgb, t.rgb, t.a * inside); }`);
842
+ };
843
+ m.customProgramCacheKey = () => "ldrColorTex";
844
+ m.userData.ldrMap = map;
845
+ return m;
846
+ }
847
+ function packTexture(pack, i) {
848
+ const t = new THREE.Texture;
849
+ t.flipY = false;
850
+ t.colorSpace = THREE.SRGBColorSpace;
851
+ t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
852
+ const e = pack.header.textures?.[i];
853
+ const bytes = e?.data ? pack.arr(e.data) : null;
854
+ if (!bytes || typeof createImageBitmap === "undefined")
855
+ return { texture: t, ready: Promise.resolve() };
856
+ const ready = createImageBitmap(new Blob([bytes], { type: e.mime || "image/png" }), { premultiplyAlpha: "none", colorSpaceConversion: "none" }).then((img) => {
857
+ t.image = img;
858
+ t.needsUpdate = true;
859
+ }).catch(() => {});
860
+ return { texture: t, ready };
861
+ }
271
862
  var lineMat = () => new THREE.ShaderMaterial({
272
863
  vertexShader: `attribute vec4 im0; attribute vec4 im1; attribute vec4 im2; attribute vec4 im3;
273
864
  void main(){ gl_Position = projectionMatrix * modelViewMatrix * mat4(im0,im1,im2,im3) * vec4(position,1.0); }`,
@@ -296,6 +887,109 @@ var weldLines = (pos) => {
296
887
  function createInstancedEdges(pos, im, material = sharedLineMat()) {
297
888
  return sharedLines(pos, im, material);
298
889
  }
890
+ function conditionalLines(pack, entry, which = "all") {
891
+ if (which === "all") {
892
+ const a = conditionalLines(pack, entry, "body"), b = conditionalLines(pack, entry, "under");
893
+ if (!a || !b)
894
+ return a ?? b;
895
+ const o = new Float32Array(a.length + b.length);
896
+ o.set(a);
897
+ o.set(b, a.length);
898
+ return o;
899
+ }
900
+ const c = which === "under" ? entry?.condlu : entry?.condl;
901
+ if (!c)
902
+ return null;
903
+ const { arr } = pack;
904
+ if (c.raw)
905
+ return arr(c.raw);
906
+ const pools = [entry.body ? arr(entry.body.pos) : [], entry.under ? arr(entry.under.pos) : [], entry.edges ? arr(entry.edges) : [], c.x ? arr(c.x) : []];
907
+ const idx = arr(c.idx);
908
+ const out = new Float32Array(idx.length * 3);
909
+ const starts = [];
910
+ let base = 0;
911
+ for (const p of pools) {
912
+ starts.push(base);
913
+ base += p.length / 3;
914
+ }
915
+ let prev = 0;
916
+ for (let i = 0;i < idx.length; i++) {
917
+ const z = idx[i];
918
+ const j = prev + (z & 1 ? -(z + 1) / 2 : z / 2);
919
+ prev = j;
920
+ let k = 3;
921
+ while (k > 0 && j < starts[k])
922
+ k--;
923
+ const p = pools[k], o = (j - starts[k]) * 3;
924
+ out[i * 3] = p[o];
925
+ out[i * 3 + 1] = p[o + 1];
926
+ out[i * 3 + 2] = p[o + 2];
927
+ }
928
+ return out;
929
+ }
930
+ var condLineMat = () => new THREE.ShaderMaterial({
931
+ uniforms: { ldrViewProj: { value: new THREE.Matrix4 } },
932
+ vertexShader: `uniform mat4 ldrViewProj; attribute mat4 instanceMatrix; attribute vec3 ldrP1; attribute vec3 ldrC0; attribute vec3 ldrC1; attribute float ldrSide;
933
+ void main(){
934
+ mat4 mvp = ldrViewProj * instanceMatrix;
935
+ vec4 a = mvp * vec4(position, 1.0), b = mvp * vec4(ldrP1, 1.0), c0 = mvp * vec4(ldrC0, 1.0), c1 = mvp * vec4(ldrC1, 1.0);
936
+ vec2 A = a.xy / a.w, B = b.xy / b.w, C0 = c0.xy / c0.w, C1 = c1.xy / c1.w, d = B - A;
937
+ float s0 = d.x * (C0.y - A.y) - d.y * (C0.x - A.x), s1 = d.x * (C1.y - A.y) - d.y * (C1.x - A.x);
938
+ gl_Position = s0 * s1 >= 0.0 ? (ldrSide > 0.5 ? b : a) : vec4(2.0, 2.0, 2.0, 1.0);
939
+ }`,
940
+ fragmentShader: `void main(){ gl_FragColor = vec4(0.13,0.13,0.13,1.0); }`
941
+ });
942
+ function conditionalLineVisible(cond, i, mvp) {
943
+ const e = mvp.elements;
944
+ const pr = (o) => {
945
+ const x = cond[o], y = cond[o + 1], z = cond[o + 2];
946
+ const w = e[3] * x + e[7] * y + e[11] * z + e[15];
947
+ return [(e[0] * x + e[4] * y + e[8] * z + e[12]) / w, (e[1] * x + e[5] * y + e[9] * z + e[13]) / w];
948
+ };
949
+ const o = i * 12;
950
+ const A = pr(o), B = pr(o + 3), C0 = pr(o + 6), C1 = pr(o + 9);
951
+ const dx = B[0] - A[0], dy = B[1] - A[1];
952
+ const s0 = dx * (C0[1] - A[1]) - dy * (C0[0] - A[0]), s1 = dx * (C1[1] - A[1]) - dy * (C1[0] - A[0]);
953
+ return s0 * s1 >= 0;
954
+ }
955
+ function createInstancedConditionalEdges(cond, im, material = condLineMat()) {
956
+ return condEdges(condBuffers(cond), im, material);
957
+ }
958
+ function condBuffers(cond) {
959
+ const n = Math.floor(cond.length / 12);
960
+ const buf = new Float32Array(n * 24), side = new Float32Array(n * 2);
961
+ for (let l = 0;l < n; l++) {
962
+ for (let k = 0;k < 12; k++) {
963
+ const v = cond[l * 12 + k];
964
+ buf[l * 24 + k] = v;
965
+ buf[l * 24 + 12 + k] = v;
966
+ }
967
+ side[l * 2 + 1] = 1;
968
+ }
969
+ return { ib: new THREE.InterleavedBuffer(buf, 12), side: new THREE.BufferAttribute(side, 1) };
970
+ }
971
+ function condEdges({ ib, side }, im, material) {
972
+ const g = new THREE.InstancedBufferGeometry;
973
+ g.setAttribute("position", new THREE.InterleavedBufferAttribute(ib, 3, 0));
974
+ g.setAttribute("ldrP1", new THREE.InterleavedBufferAttribute(ib, 3, 3));
975
+ g.setAttribute("ldrC0", new THREE.InterleavedBufferAttribute(ib, 3, 6));
976
+ g.setAttribute("ldrC1", new THREE.InterleavedBufferAttribute(ib, 3, 9));
977
+ g.setAttribute("ldrSide", side);
978
+ g.setAttribute("instanceMatrix", im.instanceMatrix);
979
+ g.instanceCount = im.count;
980
+ const l = new THREE.LineSegments(g, material);
981
+ l.frustumCulled = false;
982
+ l.name = "ldrCondLines";
983
+ l.onBeforeRender = (_r, _s, cam, _g, mat) => {
984
+ const u = mat.uniforms?.ldrViewProj;
985
+ if (!u)
986
+ return;
987
+ l.modelViewMatrix.multiplyMatrices(cam.matrixWorldInverse, l.matrixWorld);
988
+ u.value.multiplyMatrices(cam.projectionMatrix, l.modelViewMatrix);
989
+ mat.uniformsNeedUpdate = true;
990
+ };
991
+ return l;
992
+ }
299
993
  function sharedLines(pos, im, material = sharedLineMat()) {
300
994
  const { P, I } = weldLines(pos);
301
995
  const g = new THREE.InstancedBufferGeometry;
@@ -426,29 +1120,96 @@ function buildLdrScene(insts, pack, P, color, mark) {
426
1120
  const cull = computeCulling(insts, H);
427
1121
  mark("culling");
428
1122
  const edgeObjs = [];
1123
+ const condObjs = [];
1124
+ const wantCond = P.cond !== false && (P.edges === "idx" || P.edges === true);
1125
+ let condMat = null;
1126
+ const cmat = () => condMat ??= condLineMat();
1127
+ const condCache = new Map;
1128
+ const addCond = (entry, im, which = "body") => {
1129
+ if (!wantCond)
1130
+ return null;
1131
+ const ck = which === "under" ? entry.condlu : entry.condl;
1132
+ if (!ck)
1133
+ return null;
1134
+ let cb = condCache.get(ck);
1135
+ if (cb === undefined) {
1136
+ const c = conditionalLines(pack, entry, which);
1137
+ cb = c && c.length ? condBuffers(c) : null;
1138
+ condCache.set(ck, cb);
1139
+ }
1140
+ if (!cb)
1141
+ return null;
1142
+ const l = condEdges(cb, im, cmat());
1143
+ edgeObjs.push(l);
1144
+ condObjs.push(l);
1145
+ root.add(l);
1146
+ return l;
1147
+ };
429
1148
  const stats = { studsTotal: cull.studs.length, studsHidden: cull.studs.filter((s) => s.hidden).length, undersTotal: 0, undersHidden: 0 };
430
1149
  const toggles = [];
431
1150
  const studMeshes = [];
1151
+ const wantTex = P.tex !== false && !!H.textures;
1152
+ const texCache = new Map;
1153
+ const texMats = new Map;
1154
+ const pending = [];
1155
+ const texMat = (t, base, wrapU) => {
1156
+ const role = base === matF ? "F" : base === matB ? "B" : "D";
1157
+ const k = t + ":" + role + ":" + (wrapU ? 1 : 0);
1158
+ let m = texMats.get(k);
1159
+ if (!m) {
1160
+ let tx = texCache.get(t);
1161
+ if (!tx) {
1162
+ const r = packTexture(pack, t);
1163
+ tx = r.texture;
1164
+ pending.push(r.ready);
1165
+ texCache.set(t, tx);
1166
+ }
1167
+ texMats.set(k, m = makeTexturedMaterial(base.side, tx, wrapU));
1168
+ }
1169
+ return m;
1170
+ };
1171
+ let texBatches = 0;
432
1172
  for (const [pk, ids] of byPart) {
433
1173
  const part = pk.replace(/#m$/, "");
434
1174
  const p = H.parts[part];
435
1175
  const mat = P.uncertDouble !== false && p.uncert ? (nUncert++, matD) : pk.endsWith("#m") ? matB : matF;
436
- const body = geom(p.body, arr);
1176
+ const textured = wantTex && !!p.tex;
1177
+ const body = geom(textured ? p.tbody : p.body, arr);
437
1178
  let bodyIm = null;
438
- if (body) {
439
- const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
1179
+ const place = (im) => {
440
1180
  ids.forEach((id, k) => {
441
1181
  im.setMatrixAt(k, toM4(insts[id].m));
442
1182
  im.setColorAt(k, color(insts[id].color));
443
1183
  });
444
1184
  im.computeBoundingSphere();
445
1185
  root.add(im);
1186
+ };
1187
+ if (body) {
1188
+ const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
1189
+ place(im);
446
1190
  }
1191
+ if (textured)
1192
+ for (const tb of p.tex) {
1193
+ const g = geom(tb, arr);
1194
+ const im = new THREE.InstancedMesh(g, texMat(tb.t, mat, !!tb.wrapU), ids.length);
1195
+ texBatches++;
1196
+ if (bodyIm) {
1197
+ im.instanceMatrix = bodyIm.instanceMatrix;
1198
+ im.instanceColor = bodyIm.instanceColor;
1199
+ im.boundingSphere = bodyIm.boundingSphere;
1200
+ root.add(im);
1201
+ } else {
1202
+ place(im);
1203
+ bodyIm = im;
1204
+ }
1205
+ }
447
1206
  if (P.edges === "idx" && p.edges && bodyIm) {
448
1207
  const l = sharedLines(arr(p.edges), bodyIm);
449
1208
  edgeObjs.push(l);
450
1209
  root.add(l);
451
1210
  }
1211
+ if (bodyIm)
1212
+ addCond(p, bodyIm);
452
1213
  const under = geom(p.under, arr);
453
1214
  if (under) {
454
1215
  const ord = [...ids.filter((i) => !cull.underHidden[i]), ...ids.filter((i) => cull.underHidden[i])];
@@ -464,6 +1225,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
464
1225
  im.count = P.cullUnder ? vis : ids.length;
465
1226
  root.add(im);
466
1227
  toggles.push({ mesh: im, all: ids.length, visible: vis, flag: "under" });
1228
+ {
1229
+ const cl = addCond(p, im, "under");
1230
+ if (cl) {
1231
+ toggles.push({ mesh: { set count(n) {
1232
+ cl.geometry.instanceCount = n;
1233
+ } }, all: ids.length, visible: vis, flag: "under" });
1234
+ cl.geometry.instanceCount = im.count;
1235
+ }
1236
+ }
467
1237
  }
468
1238
  if (P.edges === true && p.edges) {
469
1239
  const mats = new Float32Array(ids.length * 16);
@@ -508,6 +1278,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
508
1278
  } }, all: ord.length, visible: vis, flag: "studs" });
509
1279
  l.geometry.instanceCount = im.count;
510
1280
  }
1281
+ {
1282
+ const cl = P.condStuds === false ? null : addCond(k, im);
1283
+ if (cl) {
1284
+ toggles.push({ mesh: { set count(n) {
1285
+ cl.geometry.instanceCount = n;
1286
+ } }, all: ord.length, visible: vis, flag: "studs" });
1287
+ cl.geometry.instanceCount = im.count;
1288
+ }
1289
+ }
511
1290
  if (P.edges === true && k.edges) {
512
1291
  const mats = new Float32Array(ord.length * 16);
513
1292
  ord.forEach((s, i) => toM4(s.w).toArray(mats, i * 16));
@@ -521,7 +1300,19 @@ function buildLdrScene(insts, pack, P, color, mark) {
521
1300
  }
522
1301
  stats.uncertParts = Object.values(H.parts).filter((x) => x.uncert).length;
523
1302
  stats.uncertBatches = nUncert;
524
- const setEdges = (v) => edgeObjs.forEach((o) => o.visible = v);
1303
+ let condOn = true, edgesOn = true;
1304
+ const setEdges = (v) => {
1305
+ edgesOn = v;
1306
+ edgeObjs.forEach((o) => o.visible = v && (condOn || !condObjs.includes(o)));
1307
+ };
1308
+ const setCond = (v) => {
1309
+ condOn = v;
1310
+ condObjs.forEach((o) => o.visible = v && edgesOn);
1311
+ };
1312
+ stats.condBatches = condObjs.length;
1313
+ stats.texBatches = texBatches;
1314
+ stats.textures = texCache.size;
1315
+ const texturesReady = Promise.all(pending).then(() => {});
525
1316
  const setCull = (studs, under) => {
526
1317
  for (const t of toggles)
527
1318
  t.mesh.count = (t.flag === "studs" ? studs : under) ? t.visible : t.all;
@@ -537,6 +1328,11 @@ function buildLdrScene(insts, pack, P, color, mark) {
537
1328
  matF.side = front ? THREE.FrontSide : THREE.DoubleSide;
538
1329
  matB.side = front ? THREE.BackSide : THREE.DoubleSide;
539
1330
  matF.needsUpdate = matB.needsUpdate = true;
1331
+ for (const [k, m] of texMats) {
1332
+ const role = k.split(":")[1];
1333
+ m.side = role === "D" || !front ? THREE.DoubleSide : role === "B" ? THREE.BackSide : THREE.FrontSide;
1334
+ m.needsUpdate = true;
1335
+ }
540
1336
  };
541
1337
  const swapStuds = (alt) => {
542
1338
  const saved = studMeshes.map((s) => s.im.geometry);
@@ -547,7 +1343,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
547
1343
  }
548
1344
  return () => studMeshes.forEach((s, i) => s.im.geometry = saved[i]);
549
1345
  };
550
- return { root, stats, setCull, setSide, setEdges, swapStuds, box };
1346
+ return { root, stats, setCull, setSide, setEdges, setCond, swapStuds, box, texturesReady };
551
1347
  }
552
1348
  // src/scene.ts
553
1349
  import * as THREE2 from "three";
@@ -584,19 +1380,27 @@ function toPlastic(old) {
584
1380
  if (!m) {
585
1381
  const p = new THREE2.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.3, clearcoat: 0.3, clearcoatRoughness: 0.2, specularIntensity: 0.6, side: old.side });
586
1382
  p.onBeforeCompile = old.onBeforeCompile;
587
- p.customProgramCacheKey = () => "ldrColorPhys" + old.side;
1383
+ p.customProgramCacheKey = () => "Phys" + old.customProgramCacheKey() + old.side;
1384
+ p.userData = old.userData;
588
1385
  plasticCache.set(old, m = p);
589
1386
  }
590
1387
  return m;
591
1388
  }
592
1389
  function createLdrObject(instances, pack, options = {}) {
593
- const o = { edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
1390
+ const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
594
1391
  const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
595
- 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 }));
596
1395
  const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
597
1396
  const uniq = [...new Set(all.map(key))].sort();
598
1397
  const rank = new Map(uniq.map((k, j) => [k, j]));
599
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
+ });
600
1404
  const missing = [...new Set(all.filter((i) => !pack.header.parts[i.part]).map((i) => i.part))];
601
1405
  const object = new THREE2.Group;
602
1406
  object.name = "ldr";
@@ -606,7 +1410,7 @@ function createLdrObject(instances, pack, options = {}) {
606
1410
  object.scale.setScalar(o.scale);
607
1411
  const pairs = new Map;
608
1412
  let cur = null;
609
- let { edges, cullStuds: cs, cullUndersides: cu, side } = o;
1413
+ let { edges, conditionalLines: cond, cullStuds: cs, cullUndersides: cu, side } = o;
610
1414
  const stats = { instances: all.length, drawn: 0, missing, studsTotal: 0, studsHidden: 0, undersTotal: 0, undersHidden: 0, buildMs: 0 };
611
1415
  const disposeCur = () => {
612
1416
  if (!cur)
@@ -620,17 +1424,17 @@ function createLdrObject(instances, pack, options = {}) {
620
1424
  if (x.dispose && x.isInstancedMesh)
621
1425
  x.dispose();
622
1426
  });
623
- for (const m of mats)
1427
+ for (const m of [...mats, ...pairs.values()]) {
1428
+ m.userData.ldrMap?.dispose();
624
1429
  m.dispose();
625
- for (const old of pairs.values())
626
- old.dispose();
1430
+ }
627
1431
  cur = null;
628
1432
  };
629
1433
  const build = (step) => {
630
1434
  const t0 = performance.now();
631
1435
  disposeCur();
632
1436
  const insts = step == null ? all : all.filter((_, k) => stepOf[k] <= step);
633
- cur = buildLdrScene(insts, pack, { pack: "", cullStuds: cs, cullUnder: cu, edges: edges ? "idx" : false, side }, color, () => {});
1437
+ cur = buildLdrScene(insts, pack, { pack: "", cullStuds: cs, cullUnder: cu, edges: edges ? "idx" : false, side, cond, condStuds: o.conditionalStudLines !== false, tex: o.textures !== false }, color, () => {});
634
1438
  pairs.clear();
635
1439
  if (o.material === "plastic")
636
1440
  cur.root.traverse((x) => {
@@ -642,19 +1446,28 @@ function createLdrObject(instances, pack, options = {}) {
642
1446
  });
643
1447
  if (!edges)
644
1448
  cur.setEdges(false);
1449
+ if (!cond)
1450
+ cur.setCond(false);
645
1451
  object.add(cur.root);
646
- Object.assign(stats, { drawn: insts.filter((i) => pack.header.parts[i.part]).length, studsTotal: cur.stats.studsTotal, studsHidden: cur.stats.studsHidden, undersTotal: cur.stats.undersTotal, undersHidden: cur.stats.undersHidden, buildMs: performance.now() - t0 });
1452
+ Object.assign(stats, { drawn: insts.filter((i) => pack.header.parts[i.part]).length, studsTotal: cur.stats.studsTotal, studsHidden: cur.stats.studsHidden, undersTotal: cur.stats.undersTotal, undersHidden: cur.stats.undersHidden, condBatches: cur.stats.condBatches ?? 0, texBatches: cur.stats.texBatches ?? 0, textures: cur.stats.textures ?? 0, buildMs: performance.now() - t0 });
1453
+ api.texturesReady = cur.texturesReady;
647
1454
  };
1455
+ const api = { texturesReady: Promise.resolve() };
648
1456
  build(o.step ?? null);
649
- return {
1457
+ return Object.assign(api, {
650
1458
  object,
651
1459
  stats,
652
1460
  stepCount: uniq.length,
1461
+ stepViews,
653
1462
  setStep: (s) => build(s),
654
1463
  setEdges: (v) => {
655
1464
  edges = v;
656
1465
  cur.setEdges(v);
657
1466
  },
1467
+ setConditionalLines: (v) => {
1468
+ cond = v;
1469
+ cur.setCond(v);
1470
+ },
658
1471
  setCulling: (a, b) => {
659
1472
  cs = a;
660
1473
  cu = b;
@@ -681,7 +1494,7 @@ function createLdrObject(instances, pack, options = {}) {
681
1494
  return target;
682
1495
  },
683
1496
  dispose: disposeCur
684
- };
1497
+ });
685
1498
  }
686
1499
  // src/pack-io.ts
687
1500
  import { decompress } from "fzstd";
@@ -697,14 +1510,26 @@ async function decompressPack(data) {
697
1510
  export {
698
1511
  I as IDENTITY,
699
1512
  buildLdrScene,
1513
+ colorToken,
700
1514
  computeCulling,
1515
+ conditionalLineVisible,
1516
+ conditionalLines,
1517
+ createInstancedConditionalEdges,
701
1518
  createInstancedEdges,
702
1519
  createLdrObject,
703
1520
  decodePack,
704
1521
  decompressPack,
1522
+ fileKind,
1523
+ fileStepViews,
705
1524
  flatten,
1525
+ hash32,
1526
+ invert,
706
1527
  isDirectColor,
707
1528
  loadPack,
1529
+ looseFile,
1530
+ loosePartName,
1531
+ looseSource,
1532
+ looseSteps,
708
1533
  makeMaterial,
709
1534
  mul,
710
1535
  norm,
@@ -712,6 +1537,14 @@ export {
712
1537
  parseColor,
713
1538
  parseLDConfig,
714
1539
  parseModel,
1540
+ parseTexMap,
1541
+ resolveTexture,
1542
+ rotStepMatrix,
715
1543
  sequenceSteps,
716
- smoothNormals
1544
+ smoothNormals,
1545
+ stepViewMatrix,
1546
+ texWrapper,
1547
+ texmapTriangleUV,
1548
+ texmapUV,
1549
+ transformTexMap
717
1550
  };