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.
@@ -6,15 +6,139 @@ function parseColor(tok) {
6
6
  return +tok;
7
7
  }
8
8
  var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
9
+ function texNames(rest) {
10
+ const out = [];
11
+ let i = 0;
12
+ while (i < rest.length) {
13
+ while (rest[i] === " " || rest[i] === "\t")
14
+ i++;
15
+ if (i >= rest.length)
16
+ break;
17
+ if (rest[i] === '"') {
18
+ let v = "";
19
+ i++;
20
+ while (i < rest.length && rest[i] !== '"') {
21
+ if (rest[i] === "\\" && i + 1 < rest.length)
22
+ i++;
23
+ v += rest[i++];
24
+ }
25
+ i++;
26
+ out.push(v);
27
+ } else {
28
+ let j = i;
29
+ while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
30
+ j++;
31
+ out.push(rest.slice(i, j));
32
+ i = j;
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+ function parseTexMap(tail, next = false) {
38
+ const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
39
+ if (!m)
40
+ return null;
41
+ const method = m[1].toUpperCase();
42
+ const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
43
+ if (!n)
44
+ return null;
45
+ const parts = m[2].trim().split(/\s+/);
46
+ if (parts.length < n + 1)
47
+ return null;
48
+ const v = parts.slice(0, n).map(Number);
49
+ if (!v.every(Number.isFinite))
50
+ return null;
51
+ let rest = m[2].trim();
52
+ for (let k = 0;k < n; k++)
53
+ rest = rest.replace(/^\S+\s*/, "");
54
+ const names = texNames(rest);
55
+ if (!names.length)
56
+ return null;
57
+ const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
58
+ if (n >= 10)
59
+ t.a = v[9];
60
+ if (n === 11)
61
+ t.b = v[10];
62
+ const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
63
+ if (g > 0 && names[g + 1])
64
+ t.glossmap = names[g + 1];
65
+ if (next)
66
+ t.next = true;
67
+ return t;
68
+ }
69
+ var b64 = (s) => {
70
+ const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
71
+ const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
72
+ const bin = atob(pad);
73
+ const out = new Uint8Array(bin.length);
74
+ for (let i = 0;i < bin.length; i++)
75
+ out[i] = bin.charCodeAt(i);
76
+ return out;
77
+ };
78
+ function geomLine(tk) {
79
+ const t = +tk[0];
80
+ if (!(t >= 1 && t <= 5) || tk.length < 2)
81
+ return null;
82
+ const color = parseColor(tk[1]);
83
+ const nums = tk.slice(2).map(Number);
84
+ if (t === 1)
85
+ return { t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) };
86
+ if (t === 2)
87
+ return { t: 2, color, p: nums.slice(0, 6) };
88
+ if (t === 3)
89
+ return { t: 3, color, p: nums.slice(0, 9) };
90
+ if (t === 4)
91
+ return { t: 4, color, p: nums.slice(0, 12) };
92
+ return { t: 5, color, p: nums.slice(0, 12) };
93
+ }
9
94
  function parse(src, name = "main.ldr") {
10
95
  const files = new Map;
11
96
  let cur = null;
12
97
  let main = "";
98
+ let data;
99
+ let dataCur = null;
100
+ let texStack = [];
101
+ let texNext = null;
102
+ const closeData = () => {
103
+ if (!dataCur)
104
+ return;
105
+ const bytes = b64(dataCur.chunks.join(""));
106
+ const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
107
+ (data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
108
+ dataCur = null;
109
+ };
13
110
  const start = (n) => {
111
+ closeData();
14
112
  cur = { name: norm(n), cmds: [] };
15
113
  files.set(cur.name, cur);
16
114
  if (!main)
17
115
  main = cur.name;
116
+ texStack = [];
117
+ texNext = null;
118
+ };
119
+ let skip = false, sawBlock = false;
120
+ const firstBlock = () => {
121
+ if (sawBlock)
122
+ return;
123
+ sawBlock = true;
124
+ files.clear();
125
+ cur = null;
126
+ main = "";
127
+ };
128
+ const annotate = (c, fromColon) => {
129
+ if (texNext) {
130
+ c.tex = texNext;
131
+ texNext = null;
132
+ return;
133
+ }
134
+ const top = texStack[texStack.length - 1];
135
+ if (!top)
136
+ return;
137
+ if (top.fallback) {
138
+ if (!fromColon)
139
+ c.fallback = true;
140
+ } else
141
+ c.tex = top.map;
18
142
  };
19
143
  for (const raw of src.split(/\r?\n/)) {
20
144
  const line = raw.trim();
@@ -25,19 +149,40 @@ function parse(src, name = "main.ldr") {
25
149
  if (t === 0) {
26
150
  const text = line.slice(1).trim();
27
151
  if (tk[1] === "FILE") {
152
+ firstBlock();
153
+ skip = false;
28
154
  start(text.slice(5));
29
155
  continue;
30
156
  }
31
157
  if (tk[1] === "NOFILE") {
158
+ closeData();
32
159
  cur = null;
160
+ skip = true;
161
+ continue;
162
+ }
163
+ if (tk[1] === "!DATA") {
164
+ firstBlock();
165
+ closeData();
166
+ cur = null;
167
+ skip = false;
168
+ dataCur = { name: text.slice(5).trim(), chunks: [] };
169
+ continue;
170
+ }
171
+ if (skip)
172
+ continue;
173
+ if (dataCur) {
174
+ if (tk[1] === "!:")
175
+ dataCur.chunks.push(tk.slice(2).join(""));
33
176
  continue;
34
177
  }
35
178
  if (!cur)
36
179
  start(name);
37
180
  const c = { t: 0, text };
38
- if (tk[1] === "STEP")
181
+ if (tk[1] === "STEP") {
39
182
  c.step = true;
40
- else if (tk[1] === "BFC")
183
+ texStack = [];
184
+ texNext = null;
185
+ } else if (tk[1] === "BFC")
41
186
  c.bfc = tk.slice(2).join(" ");
42
187
  else if (tk[1] === "ROTSTEP") {
43
188
  c.meta = "ROTSTEP";
@@ -53,6 +198,37 @@ function parse(src, name = "main.ldr") {
53
198
  c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
54
199
  } else if (tk[1]?.startsWith("!"))
55
200
  c.meta = tk[1];
201
+ if (tk[1] === "!TEXMAP") {
202
+ const op = (tk[2] ?? "").toUpperCase();
203
+ texNext = null;
204
+ if (op === "START" || op === "NEXT") {
205
+ const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
206
+ c.texmap = { op, ...map ? { map } : {} };
207
+ if (map) {
208
+ (cur.texmaps ??= []).push(map);
209
+ if (op === "START")
210
+ texStack.push({ map, fallback: false });
211
+ else
212
+ texNext = map;
213
+ }
214
+ } else if (op === "FALLBACK") {
215
+ c.texmap = { op };
216
+ const top = texStack[texStack.length - 1];
217
+ if (top)
218
+ top.fallback = true;
219
+ } else if (op === "END") {
220
+ c.texmap = { op };
221
+ texStack.pop();
222
+ }
223
+ } else if (tk[1] === "!:") {
224
+ texNext = null;
225
+ const g = geomLine(tk.slice(2));
226
+ if (g) {
227
+ annotate(g, true);
228
+ c.geom = g;
229
+ }
230
+ } else
231
+ texNext = null;
56
232
  const moved = text.match(MOVED_RE);
57
233
  if (moved && !cur.movedTo) {
58
234
  let target = moved[1].trim().replace(/\\/g, "/");
@@ -63,22 +239,109 @@ function parse(src, name = "main.ldr") {
63
239
  cur.cmds.push(c);
64
240
  continue;
65
241
  }
242
+ if (dataCur || skip)
243
+ continue;
66
244
  if (!cur)
67
245
  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) });
246
+ const g = geomLine(tk);
247
+ if (!g)
248
+ continue;
249
+ annotate(g, false);
250
+ cur.cmds.push(g);
251
+ }
252
+ closeData();
253
+ return data ? { main, files, data } : { main, files };
254
+ }
255
+ function resolveTexture(doc, name) {
256
+ if (!doc.data)
257
+ return null;
258
+ const n = norm(name);
259
+ return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
260
+ }
261
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
262
+ var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
263
+ 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]];
264
+ var unit3 = (a) => {
265
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
266
+ return [a[0] / l, a[1] / l, a[2] / l];
267
+ };
268
+ var DEG = Math.PI / 180;
269
+ function texmapUV(map, p) {
270
+ const w = texmapTriangleUV(map, [p, p, p]);
271
+ const u = w[0], v = w[1];
272
+ return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
273
+ }
274
+ var radial = (p, c, n) => {
275
+ const d = sub3(p, c);
276
+ const k = dot3(d, n);
277
+ const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
278
+ return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
279
+ };
280
+ function texmapTriangleUV(map, pts) {
281
+ const { p1, p2, p3 } = map;
282
+ const out = new Array(pts.length * 2).fill(0);
283
+ if (map.method === "PLANAR") {
284
+ const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
285
+ const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
286
+ pts.forEach((p, i) => {
287
+ const d = sub3(p, p1);
288
+ out[i * 2] = dot3(d, e1) / l1;
289
+ out[i * 2 + 1] = dot3(d, e2) / l2;
290
+ });
291
+ return out;
80
292
  }
81
- return { main, files };
293
+ const cyl = map.method === "CYLINDRICAL";
294
+ const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
295
+ const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
296
+ const A = (map.a ?? 360) * DEG;
297
+ let base = null, baseAng = 0;
298
+ const sAng = (q) => {
299
+ if (!base) {
300
+ base = q;
301
+ baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
302
+ return baseAng;
303
+ }
304
+ return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
305
+ };
306
+ const miss = [];
307
+ let uSum = 0;
308
+ const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
309
+ pts.forEach((p, i) => {
310
+ if (cyl) {
311
+ const q = dir ? radial(p, p1, n) : null;
312
+ if (q) {
313
+ const a = sAng(q);
314
+ out[i * 2] = 0.5 + a / A;
315
+ uSum += out[i * 2];
316
+ } else
317
+ miss.push(i);
318
+ out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
319
+ } else {
320
+ const pd = unit3(sub3(p, p1));
321
+ const c = dot3(pd, n);
322
+ if (Math.abs(c) > 1 - 0.000000001) {
323
+ miss.push(i);
324
+ out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
325
+ return;
326
+ }
327
+ const a = sAng(radial(p, p1, n));
328
+ uSum += a;
329
+ out[i * 2] = 0.5 + a / A;
330
+ const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
331
+ 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]);
332
+ out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
333
+ }
334
+ });
335
+ if (miss.length && miss.length < pts.length) {
336
+ const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
337
+ for (const i of miss)
338
+ out[i * 2] = avg;
339
+ }
340
+ return out;
341
+ }
342
+ function transformTexMap(map, m) {
343
+ 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]];
344
+ return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
82
345
  }
83
346
  function mul(a, b) {
84
347
  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;
@@ -241,7 +504,7 @@ function decodePack(buf) {
241
504
  const hl = new DataView(buf).getUint32(0, true);
242
505
  const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, hl)));
243
506
  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]));
507
+ 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
508
  return { header, arr, bytes: buf.byteLength };
246
509
  }
247
510
  function geom(b, arr) {
@@ -253,6 +516,8 @@ function geom(b, arr) {
253
516
  g.setAttribute("ldrColor", new THREE.BufferAttribute(arr(b.col), 4));
254
517
  if (b.idx)
255
518
  g.setIndex(new THREE.BufferAttribute(arr(b.idx), 1));
519
+ if (b.uv)
520
+ g.setAttribute("ldrUV", new THREE.BufferAttribute(arr(b.uv), 2));
256
521
  return g;
257
522
  }
258
523
  function makeMaterial(side) {
@@ -268,6 +533,41 @@ function makeMaterial(side) {
268
533
  m.customProgramCacheKey = () => "ldrColor";
269
534
  return m;
270
535
  }
536
+ function makeTexturedMaterial(side, map, wrapU = false) {
537
+ const m = makeMaterial(side);
538
+ const base = m.onBeforeCompile;
539
+ m.onBeforeCompile = (sh, r) => {
540
+ base.call(m, sh, r);
541
+ sh.uniforms.ldrMap = { value: map };
542
+ sh.uniforms.ldrWrapU = { value: wrapU ? 1 : 0 };
543
+ sh.vertexShader = `attribute vec2 ldrUV; varying vec2 vLdrUV;
544
+ ` + sh.vertexShader.replace("#include <uv_vertex>", `#include <uv_vertex>
545
+ vLdrUV = ldrUV;`);
546
+ sh.fragmentShader = `uniform sampler2D ldrMap; uniform int ldrWrapU; varying vec2 vLdrUV;
547
+ ` + sh.fragmentShader.replace("#include <color_fragment>", `#include <color_fragment>
548
+ { vec2 uv = vec2(ldrWrapU == 1 ? fract(vLdrUV.x) : vLdrUV.x, vLdrUV.y); vec4 t = texture2D(ldrMap, uv);
549
+ float inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);
550
+ diffuseColor.rgb = mix(diffuseColor.rgb, t.rgb, t.a * inside); }`);
551
+ };
552
+ m.customProgramCacheKey = () => "ldrColorTex";
553
+ m.userData.ldrMap = map;
554
+ return m;
555
+ }
556
+ function packTexture(pack, i) {
557
+ const t = new THREE.Texture;
558
+ t.flipY = false;
559
+ t.colorSpace = THREE.SRGBColorSpace;
560
+ t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
561
+ const e = pack.header.textures?.[i];
562
+ const bytes = e?.data ? pack.arr(e.data) : null;
563
+ if (!bytes || typeof createImageBitmap === "undefined")
564
+ return { texture: t, ready: Promise.resolve() };
565
+ const ready = createImageBitmap(new Blob([bytes], { type: e.mime || "image/png" }), { premultiplyAlpha: "none", colorSpaceConversion: "none" }).then((img) => {
566
+ t.image = img;
567
+ t.needsUpdate = true;
568
+ }).catch(() => {});
569
+ return { texture: t, ready };
570
+ }
271
571
  var lineMat = () => new THREE.ShaderMaterial({
272
572
  vertexShader: `attribute vec4 im0; attribute vec4 im1; attribute vec4 im2; attribute vec4 im3;
273
573
  void main(){ gl_Position = projectionMatrix * modelViewMatrix * mat4(im0,im1,im2,im3) * vec4(position,1.0); }`,
@@ -296,6 +596,109 @@ var weldLines = (pos) => {
296
596
  function createInstancedEdges(pos, im, material = sharedLineMat()) {
297
597
  return sharedLines(pos, im, material);
298
598
  }
599
+ function conditionalLines(pack, entry, which = "all") {
600
+ if (which === "all") {
601
+ const a = conditionalLines(pack, entry, "body"), b = conditionalLines(pack, entry, "under");
602
+ if (!a || !b)
603
+ return a ?? b;
604
+ const o = new Float32Array(a.length + b.length);
605
+ o.set(a);
606
+ o.set(b, a.length);
607
+ return o;
608
+ }
609
+ const c = which === "under" ? entry?.condlu : entry?.condl;
610
+ if (!c)
611
+ return null;
612
+ const { arr } = pack;
613
+ if (c.raw)
614
+ return arr(c.raw);
615
+ const pools = [entry.body ? arr(entry.body.pos) : [], entry.under ? arr(entry.under.pos) : [], entry.edges ? arr(entry.edges) : [], c.x ? arr(c.x) : []];
616
+ const idx = arr(c.idx);
617
+ const out = new Float32Array(idx.length * 3);
618
+ const starts = [];
619
+ let base = 0;
620
+ for (const p of pools) {
621
+ starts.push(base);
622
+ base += p.length / 3;
623
+ }
624
+ let prev = 0;
625
+ for (let i = 0;i < idx.length; i++) {
626
+ const z = idx[i];
627
+ const j = prev + (z & 1 ? -(z + 1) / 2 : z / 2);
628
+ prev = j;
629
+ let k = 3;
630
+ while (k > 0 && j < starts[k])
631
+ k--;
632
+ const p = pools[k], o = (j - starts[k]) * 3;
633
+ out[i * 3] = p[o];
634
+ out[i * 3 + 1] = p[o + 1];
635
+ out[i * 3 + 2] = p[o + 2];
636
+ }
637
+ return out;
638
+ }
639
+ var condLineMat = () => new THREE.ShaderMaterial({
640
+ uniforms: { ldrViewProj: { value: new THREE.Matrix4 } },
641
+ vertexShader: `uniform mat4 ldrViewProj; attribute mat4 instanceMatrix; attribute vec3 ldrP1; attribute vec3 ldrC0; attribute vec3 ldrC1; attribute float ldrSide;
642
+ void main(){
643
+ mat4 mvp = ldrViewProj * instanceMatrix;
644
+ 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);
645
+ vec2 A = a.xy / a.w, B = b.xy / b.w, C0 = c0.xy / c0.w, C1 = c1.xy / c1.w, d = B - A;
646
+ 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);
647
+ gl_Position = s0 * s1 >= 0.0 ? (ldrSide > 0.5 ? b : a) : vec4(2.0, 2.0, 2.0, 1.0);
648
+ }`,
649
+ fragmentShader: `void main(){ gl_FragColor = vec4(0.13,0.13,0.13,1.0); }`
650
+ });
651
+ function conditionalLineVisible(cond, i, mvp) {
652
+ const e = mvp.elements;
653
+ const pr = (o) => {
654
+ const x = cond[o], y = cond[o + 1], z = cond[o + 2];
655
+ const w = e[3] * x + e[7] * y + e[11] * z + e[15];
656
+ 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];
657
+ };
658
+ const o = i * 12;
659
+ const A = pr(o), B = pr(o + 3), C0 = pr(o + 6), C1 = pr(o + 9);
660
+ const dx = B[0] - A[0], dy = B[1] - A[1];
661
+ const s0 = dx * (C0[1] - A[1]) - dy * (C0[0] - A[0]), s1 = dx * (C1[1] - A[1]) - dy * (C1[0] - A[0]);
662
+ return s0 * s1 >= 0;
663
+ }
664
+ function createInstancedConditionalEdges(cond, im, material = condLineMat()) {
665
+ return condEdges(condBuffers(cond), im, material);
666
+ }
667
+ function condBuffers(cond) {
668
+ const n = Math.floor(cond.length / 12);
669
+ const buf = new Float32Array(n * 24), side = new Float32Array(n * 2);
670
+ for (let l = 0;l < n; l++) {
671
+ for (let k = 0;k < 12; k++) {
672
+ const v = cond[l * 12 + k];
673
+ buf[l * 24 + k] = v;
674
+ buf[l * 24 + 12 + k] = v;
675
+ }
676
+ side[l * 2 + 1] = 1;
677
+ }
678
+ return { ib: new THREE.InterleavedBuffer(buf, 12), side: new THREE.BufferAttribute(side, 1) };
679
+ }
680
+ function condEdges({ ib, side }, im, material) {
681
+ const g = new THREE.InstancedBufferGeometry;
682
+ g.setAttribute("position", new THREE.InterleavedBufferAttribute(ib, 3, 0));
683
+ g.setAttribute("ldrP1", new THREE.InterleavedBufferAttribute(ib, 3, 3));
684
+ g.setAttribute("ldrC0", new THREE.InterleavedBufferAttribute(ib, 3, 6));
685
+ g.setAttribute("ldrC1", new THREE.InterleavedBufferAttribute(ib, 3, 9));
686
+ g.setAttribute("ldrSide", side);
687
+ g.setAttribute("instanceMatrix", im.instanceMatrix);
688
+ g.instanceCount = im.count;
689
+ const l = new THREE.LineSegments(g, material);
690
+ l.frustumCulled = false;
691
+ l.name = "ldrCondLines";
692
+ l.onBeforeRender = (_r, _s, cam, _g, mat) => {
693
+ const u = mat.uniforms?.ldrViewProj;
694
+ if (!u)
695
+ return;
696
+ l.modelViewMatrix.multiplyMatrices(cam.matrixWorldInverse, l.matrixWorld);
697
+ u.value.multiplyMatrices(cam.projectionMatrix, l.modelViewMatrix);
698
+ mat.uniformsNeedUpdate = true;
699
+ };
700
+ return l;
701
+ }
299
702
  function sharedLines(pos, im, material = sharedLineMat()) {
300
703
  const { P, I } = weldLines(pos);
301
704
  const g = new THREE.InstancedBufferGeometry;
@@ -426,29 +829,96 @@ function buildLdrScene(insts, pack, P, color, mark) {
426
829
  const cull = computeCulling(insts, H);
427
830
  mark("culling");
428
831
  const edgeObjs = [];
832
+ const condObjs = [];
833
+ const wantCond = P.cond !== false && P.edges === "idx";
834
+ let condMat = null;
835
+ const cmat = () => condMat ??= condLineMat();
836
+ const condCache = new Map;
837
+ const addCond = (entry, im, which = "body") => {
838
+ if (!wantCond)
839
+ return null;
840
+ const ck = which === "under" ? entry.condlu : entry.condl;
841
+ if (!ck)
842
+ return null;
843
+ let cb = condCache.get(ck);
844
+ if (cb === undefined) {
845
+ const c = conditionalLines(pack, entry, which);
846
+ cb = c && c.length ? condBuffers(c) : null;
847
+ condCache.set(ck, cb);
848
+ }
849
+ if (!cb)
850
+ return null;
851
+ const l = condEdges(cb, im, cmat());
852
+ edgeObjs.push(l);
853
+ condObjs.push(l);
854
+ root.add(l);
855
+ return l;
856
+ };
429
857
  const stats = { studsTotal: cull.studs.length, studsHidden: cull.studs.filter((s) => s.hidden).length, undersTotal: 0, undersHidden: 0 };
430
858
  const toggles = [];
431
859
  const studMeshes = [];
860
+ const wantTex = P.tex !== false && !!H.textures;
861
+ const texCache = new Map;
862
+ const texMats = new Map;
863
+ const pending = [];
864
+ const texMat = (t, base, wrapU) => {
865
+ const role = base === matF ? "F" : base === matB ? "B" : "D";
866
+ const k = t + ":" + role + ":" + (wrapU ? 1 : 0);
867
+ let m = texMats.get(k);
868
+ if (!m) {
869
+ let tx = texCache.get(t);
870
+ if (!tx) {
871
+ const r = packTexture(pack, t);
872
+ tx = r.texture;
873
+ pending.push(r.ready);
874
+ texCache.set(t, tx);
875
+ }
876
+ texMats.set(k, m = makeTexturedMaterial(base.side, tx, wrapU));
877
+ }
878
+ return m;
879
+ };
880
+ let texBatches = 0;
432
881
  for (const [pk, ids] of byPart) {
433
882
  const part = pk.replace(/#m$/, "");
434
883
  const p = H.parts[part];
435
884
  const mat = P.uncertDouble !== false && p.uncert ? (nUncert++, matD) : pk.endsWith("#m") ? matB : matF;
436
- const body = geom(p.body, arr);
885
+ const textured = wantTex && !!p.tex;
886
+ const body = geom(textured ? p.tbody : p.body, arr);
437
887
  let bodyIm = null;
438
- if (body) {
439
- const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
888
+ const place = (im) => {
440
889
  ids.forEach((id, k) => {
441
890
  im.setMatrixAt(k, toM4(insts[id].m));
442
891
  im.setColorAt(k, color(insts[id].color));
443
892
  });
444
893
  im.computeBoundingSphere();
445
894
  root.add(im);
895
+ };
896
+ if (body) {
897
+ const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
898
+ place(im);
446
899
  }
900
+ if (textured)
901
+ for (const tb of p.tex) {
902
+ const g = geom(tb, arr);
903
+ const im = new THREE.InstancedMesh(g, texMat(tb.t, mat, !!tb.wrapU), ids.length);
904
+ texBatches++;
905
+ if (bodyIm) {
906
+ im.instanceMatrix = bodyIm.instanceMatrix;
907
+ im.instanceColor = bodyIm.instanceColor;
908
+ im.boundingSphere = bodyIm.boundingSphere;
909
+ root.add(im);
910
+ } else {
911
+ place(im);
912
+ bodyIm = im;
913
+ }
914
+ }
447
915
  if (P.edges === "idx" && p.edges && bodyIm) {
448
916
  const l = sharedLines(arr(p.edges), bodyIm);
449
917
  edgeObjs.push(l);
450
918
  root.add(l);
451
919
  }
920
+ if (bodyIm)
921
+ addCond(p, bodyIm);
452
922
  const under = geom(p.under, arr);
453
923
  if (under) {
454
924
  const ord = [...ids.filter((i) => !cull.underHidden[i]), ...ids.filter((i) => cull.underHidden[i])];
@@ -464,6 +934,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
464
934
  im.count = P.cullUnder ? vis : ids.length;
465
935
  root.add(im);
466
936
  toggles.push({ mesh: im, all: ids.length, visible: vis, flag: "under" });
937
+ {
938
+ const cl = addCond(p, im, "under");
939
+ if (cl) {
940
+ toggles.push({ mesh: { set count(n) {
941
+ cl.geometry.instanceCount = n;
942
+ } }, all: ids.length, visible: vis, flag: "under" });
943
+ cl.geometry.instanceCount = im.count;
944
+ }
945
+ }
467
946
  }
468
947
  if (P.edges === true && p.edges) {
469
948
  const mats = new Float32Array(ids.length * 16);
@@ -508,6 +987,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
508
987
  } }, all: ord.length, visible: vis, flag: "studs" });
509
988
  l.geometry.instanceCount = im.count;
510
989
  }
990
+ {
991
+ const cl = P.condStuds === false ? null : addCond(k, im);
992
+ if (cl) {
993
+ toggles.push({ mesh: { set count(n) {
994
+ cl.geometry.instanceCount = n;
995
+ } }, all: ord.length, visible: vis, flag: "studs" });
996
+ cl.geometry.instanceCount = im.count;
997
+ }
998
+ }
511
999
  if (P.edges === true && k.edges) {
512
1000
  const mats = new Float32Array(ord.length * 16);
513
1001
  ord.forEach((s, i) => toM4(s.w).toArray(mats, i * 16));
@@ -521,7 +1009,19 @@ function buildLdrScene(insts, pack, P, color, mark) {
521
1009
  }
522
1010
  stats.uncertParts = Object.values(H.parts).filter((x) => x.uncert).length;
523
1011
  stats.uncertBatches = nUncert;
524
- const setEdges = (v) => edgeObjs.forEach((o) => o.visible = v);
1012
+ let condOn = true, edgesOn = true;
1013
+ const setEdges = (v) => {
1014
+ edgesOn = v;
1015
+ edgeObjs.forEach((o) => o.visible = v && (condOn || !condObjs.includes(o)));
1016
+ };
1017
+ const setCond = (v) => {
1018
+ condOn = v;
1019
+ condObjs.forEach((o) => o.visible = v && edgesOn);
1020
+ };
1021
+ stats.condBatches = condObjs.length;
1022
+ stats.texBatches = texBatches;
1023
+ stats.textures = texCache.size;
1024
+ const texturesReady = Promise.all(pending).then(() => {});
525
1025
  const setCull = (studs, under) => {
526
1026
  for (const t of toggles)
527
1027
  t.mesh.count = (t.flag === "studs" ? studs : under) ? t.visible : t.all;
@@ -537,6 +1037,11 @@ function buildLdrScene(insts, pack, P, color, mark) {
537
1037
  matF.side = front ? THREE.FrontSide : THREE.DoubleSide;
538
1038
  matB.side = front ? THREE.BackSide : THREE.DoubleSide;
539
1039
  matF.needsUpdate = matB.needsUpdate = true;
1040
+ for (const [k, m] of texMats) {
1041
+ const role = k.split(":")[1];
1042
+ m.side = role === "D" || !front ? THREE.DoubleSide : role === "B" ? THREE.BackSide : THREE.FrontSide;
1043
+ m.needsUpdate = true;
1044
+ }
540
1045
  };
541
1046
  const swapStuds = (alt) => {
542
1047
  const saved = studMeshes.map((s) => s.im.geometry);
@@ -547,7 +1052,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
547
1052
  }
548
1053
  return () => studMeshes.forEach((s, i) => s.im.geometry = saved[i]);
549
1054
  };
550
- return { root, stats, setCull, setSide, setEdges, swapStuds, box };
1055
+ return { root, stats, setCull, setSide, setEdges, setCond, swapStuds, box, texturesReady };
551
1056
  }
552
1057
  // src/scene.ts
553
1058
  import * as THREE2 from "three";
@@ -584,13 +1089,14 @@ function toPlastic(old) {
584
1089
  if (!m) {
585
1090
  const p = new THREE2.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.3, clearcoat: 0.3, clearcoatRoughness: 0.2, specularIntensity: 0.6, side: old.side });
586
1091
  p.onBeforeCompile = old.onBeforeCompile;
587
- p.customProgramCacheKey = () => "ldrColorPhys" + old.side;
1092
+ p.customProgramCacheKey = () => "Phys" + old.customProgramCacheKey() + old.side;
1093
+ p.userData = old.userData;
588
1094
  plasticCache.set(old, m = p);
589
1095
  }
590
1096
  return m;
591
1097
  }
592
1098
  function createLdrObject(instances, pack, options = {}) {
593
- const o = { edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
1099
+ const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
594
1100
  const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
595
1101
  const all = instances.map((i) => ({ part: i.part, color: String(i.color), m: i.m, steps: i.steps }));
596
1102
  const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
@@ -606,7 +1112,7 @@ function createLdrObject(instances, pack, options = {}) {
606
1112
  object.scale.setScalar(o.scale);
607
1113
  const pairs = new Map;
608
1114
  let cur = null;
609
- let { edges, cullStuds: cs, cullUndersides: cu, side } = o;
1115
+ let { edges, conditionalLines: cond, cullStuds: cs, cullUndersides: cu, side } = o;
610
1116
  const stats = { instances: all.length, drawn: 0, missing, studsTotal: 0, studsHidden: 0, undersTotal: 0, undersHidden: 0, buildMs: 0 };
611
1117
  const disposeCur = () => {
612
1118
  if (!cur)
@@ -620,17 +1126,17 @@ function createLdrObject(instances, pack, options = {}) {
620
1126
  if (x.dispose && x.isInstancedMesh)
621
1127
  x.dispose();
622
1128
  });
623
- for (const m of mats)
1129
+ for (const m of [...mats, ...pairs.values()]) {
1130
+ m.userData.ldrMap?.dispose();
624
1131
  m.dispose();
625
- for (const old of pairs.values())
626
- old.dispose();
1132
+ }
627
1133
  cur = null;
628
1134
  };
629
1135
  const build = (step) => {
630
1136
  const t0 = performance.now();
631
1137
  disposeCur();
632
1138
  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, () => {});
1139
+ 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
1140
  pairs.clear();
635
1141
  if (o.material === "plastic")
636
1142
  cur.root.traverse((x) => {
@@ -642,11 +1148,15 @@ function createLdrObject(instances, pack, options = {}) {
642
1148
  });
643
1149
  if (!edges)
644
1150
  cur.setEdges(false);
1151
+ if (!cond)
1152
+ cur.setCond(false);
645
1153
  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 });
1154
+ 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 });
1155
+ api.texturesReady = cur.texturesReady;
647
1156
  };
1157
+ const api = { texturesReady: Promise.resolve() };
648
1158
  build(o.step ?? null);
649
- return {
1159
+ return Object.assign(api, {
650
1160
  object,
651
1161
  stats,
652
1162
  stepCount: uniq.length,
@@ -655,6 +1165,10 @@ function createLdrObject(instances, pack, options = {}) {
655
1165
  edges = v;
656
1166
  cur.setEdges(v);
657
1167
  },
1168
+ setConditionalLines: (v) => {
1169
+ cond = v;
1170
+ cur.setCond(v);
1171
+ },
658
1172
  setCulling: (a, b) => {
659
1173
  cs = a;
660
1174
  cu = b;
@@ -681,7 +1195,7 @@ function createLdrObject(instances, pack, options = {}) {
681
1195
  return target;
682
1196
  },
683
1197
  dispose: disposeCur
684
- };
1198
+ });
685
1199
  }
686
1200
  // src/pack-io.ts
687
1201
  import { decompress } from "fzstd";
@@ -766,46 +1280,91 @@ class Library {
766
1280
  }
767
1281
  return out;
768
1282
  }
769
- mesh(name) {
1283
+ mesh(name, opts = {}) {
770
1284
  name = norm(name);
771
- const hit = this.meshCache.get(name);
1285
+ const ck = opts.texmap ? name + "#texmap" : name;
1286
+ const hit = this.meshCache.get(ck);
772
1287
  if (hit)
773
1288
  return hit;
774
1289
  const pos = [], col = [], edges = [], cond = [];
1290
+ const tx = opts.texmap ? { uv: [], tri: [], names: [] } : undefined;
775
1291
  const r = this.where(name);
776
1292
  if (r)
777
- this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond);
1293
+ this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond, tx, null);
778
1294
  const m = { name: r?.doc.main ?? name, pos: new Float32Array(pos), col: new Uint32Array(col), edges: new Float32Array(edges), cond: new Float32Array(cond), nrm: smoothNormals(pos, edges, cond) };
779
- this.meshCache.set(name, m);
1295
+ if (tx) {
1296
+ m.uv = new Float32Array(tx.uv);
1297
+ m.texture = new Int32Array(tx.tri);
1298
+ m.textures = tx.names;
1299
+ }
1300
+ this.meshCache.set(ck, m);
780
1301
  return m;
781
1302
  }
782
- emit(doc, fname, m, color, inv, clip, pos, col, edges, cond) {
1303
+ texture(name, doc) {
1304
+ if (doc) {
1305
+ const d = resolveTexture(doc, name);
1306
+ if (d)
1307
+ return d.bytes;
1308
+ }
1309
+ const n = norm(name);
1310
+ const roots = [this.root];
1311
+ const unoff = this.unofficialRoot ?? (existsSync(join(this.root, "unofficial")) ? join(this.root, "unofficial") : undefined);
1312
+ if (unoff)
1313
+ roots.push(unoff);
1314
+ for (const cand of ["textures/" + n, n])
1315
+ for (const root of roots)
1316
+ for (const d of ["parts", "p", "models"]) {
1317
+ const f = join(root, d, cand);
1318
+ if (existsSync(f))
1319
+ return new Uint8Array(readFileSync(f));
1320
+ }
1321
+ return null;
1322
+ }
1323
+ emit(doc, fname, m, color, inv, clip, pos, col, edges, cond, tx, texIn = null) {
783
1324
  const f = doc.files.get(fname);
784
1325
  if (!f)
785
1326
  return;
786
1327
  let ccw = true, invertNext = false, clipLocal = clip;
787
1328
  const flip = inv !== det(m) < 0;
788
1329
  const cc = (c) => c === 16 ? color : c;
789
- for (const c of f.cmds) {
1330
+ const texIdx = (t) => {
1331
+ let i = tx.names.indexOf(t.texture);
1332
+ if (i < 0) {
1333
+ i = tx.names.length;
1334
+ tx.names.push(t.texture);
1335
+ }
1336
+ return i;
1337
+ };
1338
+ for (const c0 of f.cmds) {
1339
+ let c = c0;
790
1340
  if (c.t === 0) {
791
- if (c.bfc) {
792
- if (c.bfc.includes("CW") && !c.bfc.includes("CCW"))
793
- ccw = false;
794
- if (c.bfc.includes("CCW"))
795
- ccw = true;
796
- if (c.bfc.includes("INVERTNEXT"))
797
- invertNext = true;
798
- if (/\bNOCLIP\b/.test(c.bfc))
799
- clipLocal = false;
800
- else if (/\bCLIP\b/.test(c.bfc))
801
- clipLocal = true;
1341
+ if (tx && c.geom)
1342
+ c = c.geom;
1343
+ else {
1344
+ if (c.bfc) {
1345
+ if (c.bfc.includes("CW") && !c.bfc.includes("CCW"))
1346
+ ccw = false;
1347
+ if (c.bfc.includes("CCW"))
1348
+ ccw = true;
1349
+ if (c.bfc.includes("INVERTNEXT"))
1350
+ invertNext = true;
1351
+ if (/\bNOCLIP\b/.test(c.bfc))
1352
+ clipLocal = false;
1353
+ else if (/\bCLIP\b/.test(c.bfc))
1354
+ clipLocal = true;
1355
+ }
1356
+ continue;
802
1357
  }
803
- continue;
804
1358
  }
1359
+ if (c.t === 0)
1360
+ continue;
1361
+ if (tx && c.fallback)
1362
+ continue;
1363
+ const tex = tx ? c.tex ? transformTexMap(c.tex, m) : texIn : null;
805
1364
  if (c.t === 1) {
806
1365
  const sub = doc.files.has(c.file) ? doc : this.where(c.file)?.doc;
807
1366
  if (sub)
808
- this.emit(sub, sub === doc ? c.file : sub.main, mul(m, c.m), cc(c.color), inv !== invertNext, clipLocal, pos, col, edges, cond);
1367
+ this.emit(sub, sub === doc ? c.file : sub.main, mul(m, c.m), cc(c.color), inv !== invertNext, clipLocal, pos, col, edges, cond, tx, tex);
809
1368
  invertNext = false;
810
1369
  continue;
811
1370
  }
@@ -821,9 +1380,18 @@ class Library {
821
1380
  continue;
822
1381
  }
823
1382
  const ordered = ccw !== flip ? v : [...v].reverse();
1383
+ const ti = tx && tex ? texIdx(tex) : -1;
1384
+ const code = cc(c.color);
824
1385
  const tri = (a, b, d) => {
825
1386
  pos.push(...a, ...b, ...d);
826
- col.push(cc(c.color));
1387
+ col.push(code);
1388
+ if (tx) {
1389
+ tx.tri.push(ti);
1390
+ if (tex)
1391
+ tx.uv.push(...texmapTriangleUV(tex, [a, b, d]));
1392
+ else
1393
+ tx.uv.push(NaN, NaN, NaN, NaN, NaN, NaN);
1394
+ }
827
1395
  };
828
1396
  const emitFace = (w) => {
829
1397
  tri(w[0], w[1], w[2]);
@@ -1006,31 +1574,67 @@ class Library2 {
1006
1574
  }
1007
1575
  function splitModel(text, fallbackName = "main.ldr") {
1008
1576
  const files = new Map;
1577
+ const data = new Map;
1578
+ let dataCur = null;
1009
1579
  let cur = null, buf = [], main = null;
1580
+ let skip = false, sawBlock = false;
1581
+ const flush = () => {
1582
+ if (cur)
1583
+ files.set(cur, buf.join(`
1584
+ `));
1585
+ cur = null;
1586
+ buf = [];
1587
+ };
1588
+ const firstBlock = () => {
1589
+ if (sawBlock)
1590
+ return;
1591
+ sawBlock = true;
1592
+ files.clear();
1593
+ cur = null;
1594
+ buf = [];
1595
+ main = null;
1596
+ };
1010
1597
  for (const line of text.split(/\r?\n/)) {
1011
1598
  const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
1012
1599
  if (m) {
1013
- if (cur)
1014
- files.set(cur, buf.join(`
1015
- `));
1600
+ firstBlock();
1601
+ flush();
1602
+ skip = false;
1603
+ dataCur = null;
1016
1604
  cur = norm2(m[1]);
1017
- buf = [];
1018
1605
  if (!main)
1019
1606
  main = cur;
1020
1607
  continue;
1021
1608
  }
1022
- if (/^\s*0\s+NOFILE/i.test(line))
1609
+ if (/^\s*0\s+(NOFILE|!DATA)(\s|$)/i.test(line)) {
1610
+ dataCur = null;
1611
+ if (/!DATA/i.test(line)) {
1612
+ firstBlock();
1613
+ const dn = norm2(line.replace(/^\s*0\s+!DATA\s*/i, ""));
1614
+ dataCur = dn;
1615
+ data.set(dn, "");
1616
+ }
1617
+ flush();
1618
+ skip = true;
1619
+ continue;
1620
+ }
1621
+ if (skip) {
1622
+ const dc = dataCur;
1623
+ if (dc) {
1624
+ const d = /^\s*0\s+!:\s*(.*)$/.exec(line);
1625
+ if (d)
1626
+ data.set(dc, data.get(dc) + d[1].trim());
1627
+ }
1023
1628
  continue;
1629
+ }
1024
1630
  if (!cur) {
1025
1631
  cur = norm2(fallbackName);
1026
1632
  main = cur;
1027
1633
  }
1028
1634
  buf.push(line);
1029
1635
  }
1030
- if (cur)
1031
- files.set(cur, buf.join(`
1032
- `));
1033
- return { files, main };
1636
+ flush();
1637
+ return { files, main, data };
1034
1638
  }
1035
1639
  function flatten2(model, lib, onInstance) {
1036
1640
  const missing = new Map;
@@ -1131,7 +1735,7 @@ class Compiler {
1131
1735
  const ck = key + (separateStuds ? "" : "#nostudsep");
1132
1736
  if (this.cache.has(ck))
1133
1737
  return this.cache.get(ck);
1134
- const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
1738
+ 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 };
1135
1739
  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];
1136
1740
  const walk = (fk, M, color, invert, target, depth) => {
1137
1741
  if (depth > 40)
@@ -1186,6 +1790,13 @@ class Compiler {
1186
1790
  }
1187
1791
  if (c === "5") {
1188
1792
  out.condLines++;
1793
+ const dst = target === "under" ? out.condUnder : out.cond;
1794
+ if (dst) {
1795
+ const v = t.split(/\s+/).slice(2, 14).map(Number);
1796
+ if (v.length === 12 && v.every(Number.isFinite))
1797
+ for (let i = 0;i < 12; i += 3)
1798
+ dst.push(...xf2(M, v.slice(i, i + 3)));
1799
+ }
1189
1800
  continue;
1190
1801
  }
1191
1802
  if (c === "3" || c === "4") {
@@ -1212,6 +1823,8 @@ class Compiler {
1212
1823
  out.uncertified = true;
1213
1824
  };
1214
1825
  walk(key, IDENT, "16", false, "body", 0);
1826
+ if (this.opts.tex && this.hasTex(key))
1827
+ this.texPass(key, out, separateStuds, colorOf);
1215
1828
  const [x0, y0, z0, x1, y1, z1] = out.bbox;
1216
1829
  let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
1217
1830
  for (const b of [out.body, out.under])
@@ -1228,6 +1841,169 @@ class Compiler {
1228
1841
  this.cache.set(ck, out);
1229
1842
  return out;
1230
1843
  }
1844
+ texMemo = new Map;
1845
+ hasTex(key) {
1846
+ const m = this.texMemo.get(key);
1847
+ if (m !== undefined)
1848
+ return m;
1849
+ this.texMemo.set(key, false);
1850
+ const txt = this.lib.read(key);
1851
+ let r = /^\s*0\s+!TEXMAP/im.test(txt);
1852
+ if (!r)
1853
+ for (const l of txt.split(/\r?\n/)) {
1854
+ const t = l.trim();
1855
+ const p = t.startsWith("0 !:") ? parseType1(t.slice(4)) : t[0] === "1" ? parseType1(t) : null;
1856
+ if (p) {
1857
+ const k = this.lib.resolve(p.name);
1858
+ if (k && this.hasTex(k)) {
1859
+ r = true;
1860
+ break;
1861
+ }
1862
+ }
1863
+ }
1864
+ this.texMemo.set(key, r);
1865
+ return r;
1866
+ }
1867
+ texture(name) {
1868
+ const n = norm2(name);
1869
+ for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
1870
+ const k = pre + n;
1871
+ if (this.lib.index.has(k))
1872
+ return new Uint8Array(fs2.readFileSync(this.lib.index.get(k)));
1873
+ }
1874
+ return null;
1875
+ }
1876
+ texPass(key, out, separateStuds, colorOf) {
1877
+ const tbody = { pos: [], col: [] }, texb = new Map;
1878
+ const walk = (fk, M, color, invert, target, depth, texIn) => {
1879
+ if (depth > 40)
1880
+ return;
1881
+ let ccw = true, invertNext = false;
1882
+ let stack = [];
1883
+ let next = null;
1884
+ for (const raw of this.lib.read(fk).split(/\r?\n/)) {
1885
+ let t = raw.trim();
1886
+ if (!t)
1887
+ continue;
1888
+ let colon = false;
1889
+ if (t[0] === "0") {
1890
+ const m = /^0\s+(\S+)\s*(.*)$/.exec(t);
1891
+ const kw = (m?.[1] ?? "").toUpperCase(), rest = m?.[2] ?? "";
1892
+ if (kw === "!TEXMAP") {
1893
+ const op = (rest.split(/\s+/)[0] || "").toUpperCase();
1894
+ next = null;
1895
+ if (op === "START" || op === "NEXT") {
1896
+ const map = parseTexMap(rest.replace(/^\S+/, ""), op === "NEXT");
1897
+ if (map) {
1898
+ const pm = transformTexMap(map, M);
1899
+ if (op === "START")
1900
+ stack.push({ map: pm, fallback: false });
1901
+ else
1902
+ next = pm;
1903
+ }
1904
+ } else if (op === "FALLBACK") {
1905
+ const top = stack[stack.length - 1];
1906
+ if (top)
1907
+ top.fallback = true;
1908
+ } else if (op === "END")
1909
+ stack.pop();
1910
+ continue;
1911
+ }
1912
+ if (kw === "!:") {
1913
+ next = null;
1914
+ t = rest.trim();
1915
+ colon = true;
1916
+ if (!t || t[0] === "0")
1917
+ continue;
1918
+ } else {
1919
+ if (kw === "BFC") {
1920
+ if (/\bCW\b/.test(t))
1921
+ ccw = false;
1922
+ if (/\bCCW\b/.test(t))
1923
+ ccw = true;
1924
+ if (/INVERTNEXT/.test(t))
1925
+ invertNext = true;
1926
+ }
1927
+ if (kw === "STEP")
1928
+ stack = [];
1929
+ next = null;
1930
+ continue;
1931
+ }
1932
+ }
1933
+ let tex = texIn;
1934
+ if (next) {
1935
+ tex = next;
1936
+ next = null;
1937
+ } else {
1938
+ const top = stack[stack.length - 1];
1939
+ if (top) {
1940
+ if (top.fallback) {
1941
+ if (!colon)
1942
+ continue;
1943
+ } else
1944
+ tex = top.map;
1945
+ }
1946
+ }
1947
+ const c = t[0];
1948
+ if (c === "1") {
1949
+ const r = parseType1(t);
1950
+ if (!r)
1951
+ continue;
1952
+ const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
1953
+ const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
1954
+ const ck2 = this.resolve(r.name, isTop);
1955
+ if (!ck2) {
1956
+ invertNext = false;
1957
+ continue;
1958
+ }
1959
+ const inv = invert !== invertNext !== det3(r.m) < 0;
1960
+ invertNext = false;
1961
+ if (isTop && separateStuds)
1962
+ continue;
1963
+ walk(ck2, compose(M, r.m), r.color === "16" ? color : r.color, inv, isUnder ? "under" : target, depth + 1, tex);
1964
+ continue;
1965
+ }
1966
+ if (c !== "3" && c !== "4")
1967
+ continue;
1968
+ const tok = t.split(/\s+/);
1969
+ const code = tok[1] === "16" ? color : tok[1];
1970
+ const n = c === "3" ? 3 : 4;
1971
+ const v = tok.slice(2, 2 + n * 3).map(Number);
1972
+ const pts = [];
1973
+ for (let i = 0;i < n; i++)
1974
+ pts.push(xf2(M, v.slice(i * 3, i * 3 + 3)));
1975
+ let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
1976
+ if (!ccw !== invert)
1977
+ order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
1978
+ const rgba = colorOf(code);
1979
+ if (tex) {
1980
+ let b = texb.get(tex.texture);
1981
+ if (!b)
1982
+ texb.set(tex.texture, b = { pos: [], col: [], uv: [] });
1983
+ if (Math.abs(tex.a ?? 0) >= 360)
1984
+ b.wrapU = true;
1985
+ for (let k = 0;k < order.length; k += 3) {
1986
+ const tri = [pts[order[k]], pts[order[k + 1]], pts[order[k + 2]]];
1987
+ const w = texmapTriangleUV(tex, tri);
1988
+ for (let j = 0;j < 3; j++) {
1989
+ b.pos.push(...tri[j]);
1990
+ b.col.push(...rgba);
1991
+ b.uv.push(w[j * 2], w[j * 2 + 1]);
1992
+ }
1993
+ }
1994
+ } else if (target === "body")
1995
+ for (const i of order) {
1996
+ tbody.pos.push(...pts[i]);
1997
+ tbody.col.push(...rgba);
1998
+ }
1999
+ }
2000
+ };
2001
+ walk(key, IDENT, "16", false, "body", 0, null);
2002
+ if (texb.size) {
2003
+ out.tbody = tbody;
2004
+ out.texb = texb;
2005
+ }
2006
+ }
1231
2007
  }
1232
2008
  function enclosureMask(c) {
1233
2009
  const [x0, , z0, x1, y1, z1] = c.bbox;
@@ -1310,10 +2086,10 @@ function growBox(b, M, cb) {
1310
2086
  for (const z of [cb[2], cb[5]])
1311
2087
  grow(b, xf2(M, [x, y, z]));
1312
2088
  }
1313
- function writePack(file, parts, studKinds, weld, extra) {
1314
- fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
2089
+ function writePack(file, parts, studKinds, weld, extra, options) {
2090
+ fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
1315
2091
  }
1316
- function encodePack(parts, studKinds, weld, extra) {
2092
+ function encodePack(parts, studKinds, weld, extra, options = {}) {
1317
2093
  const chunks = [];
1318
2094
  let off = 0;
1319
2095
  const put = (a) => {
@@ -1322,13 +2098,15 @@ function encodePack(parts, studKinds, weld, extra) {
1322
2098
  chunks.push(new Uint8Array(pad));
1323
2099
  off += pad;
1324
2100
  }
1325
- const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
2101
+ const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
1326
2102
  chunks.push(a);
1327
2103
  off += a.byteLength;
1328
2104
  return r;
1329
2105
  };
2106
+ let lastPos = [];
1330
2107
  const bucket = (b) => {
1331
2108
  const n = b.pos.length / 3;
2109
+ lastPos = [];
1332
2110
  if (!n)
1333
2111
  return null;
1334
2112
  const nrm = new Float32Array(n * 3);
@@ -1347,8 +2125,10 @@ function encodePack(parts, studKinds, weld, extra) {
1347
2125
  nrm[(i + k) * 3 + 2] = nz;
1348
2126
  }
1349
2127
  }
1350
- if (!weld)
2128
+ if (!weld) {
2129
+ lastPos = b.pos;
1351
2130
  return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
2131
+ }
1352
2132
  const map = new Map;
1353
2133
  const P = [], N = [], C = [], I = [];
1354
2134
  for (let i = 0;i < n; i++) {
@@ -1364,10 +2144,111 @@ function encodePack(parts, studKinds, weld, extra) {
1364
2144
  I.push(j);
1365
2145
  }
1366
2146
  const vcount = P.length / 3;
2147
+ lastPos = P;
1367
2148
  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 };
1368
2149
  };
1369
- 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 });
1370
- 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 };
2150
+ let anyCond = false;
2151
+ const condl = (a, pools) => {
2152
+ if (!a || !a.length)
2153
+ return;
2154
+ anyCond = true;
2155
+ const key = (p, i) => Math.round(p[i] * 1000) + "," + Math.round(p[i + 1] * 1000) + "," + Math.round(p[i + 2] * 1000);
2156
+ const need = new Map;
2157
+ for (let i = 0;i < a.length; i += 3)
2158
+ need.set(key(a, i), -1);
2159
+ let base = 0;
2160
+ for (const p of pools) {
2161
+ for (let i = 0;i < p.length; i += 3) {
2162
+ const k = key(p, i);
2163
+ if (need.get(k) === -1)
2164
+ need.set(k, base + i / 3);
2165
+ }
2166
+ base += p.length / 3;
2167
+ }
2168
+ const X = [], I = new Array(a.length / 3);
2169
+ for (let i = 0;i < a.length; i += 3) {
2170
+ const k = key(a, i);
2171
+ let j = need.get(k);
2172
+ if (j === -1) {
2173
+ j = base + X.length / 3;
2174
+ need.set(k, j);
2175
+ X.push(a[i], a[i + 1], a[i + 2]);
2176
+ }
2177
+ I[i / 3] = j;
2178
+ }
2179
+ let prev = 0, max = 0;
2180
+ for (let i = 0;i < I.length; i++) {
2181
+ const d = I[i] - prev;
2182
+ prev = I[i];
2183
+ const z = d >= 0 ? d * 2 : -d * 2 - 1;
2184
+ I[i] = z;
2185
+ if (z > max)
2186
+ max = z;
2187
+ }
2188
+ return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
2189
+ };
2190
+ const texNames = [];
2191
+ const texBucket = (b) => {
2192
+ const n = b.pos.length / 3;
2193
+ if (!n)
2194
+ return null;
2195
+ const nrm = new Float32Array(n * 3);
2196
+ for (let i = 0;i < n; i += 3) {
2197
+ const p = b.pos;
2198
+ const a = i * 3;
2199
+ 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];
2200
+ let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
2201
+ const l = Math.hypot(nx, ny, nz) || 1;
2202
+ nx /= l;
2203
+ ny /= l;
2204
+ nz /= l;
2205
+ for (let k = 0;k < 3; k++) {
2206
+ nrm[(i + k) * 3] = nx;
2207
+ nrm[(i + k) * 3 + 1] = ny;
2208
+ nrm[(i + k) * 3 + 2] = nz;
2209
+ }
2210
+ }
2211
+ const map = new Map;
2212
+ const P = [], N = [], C = [], U = [], I = [];
2213
+ for (let i = 0;i < n; i++) {
2214
+ 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);
2215
+ let j = map.get(k);
2216
+ if (j === undefined) {
2217
+ j = P.length / 3;
2218
+ map.set(k, j);
2219
+ P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
2220
+ N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
2221
+ C.push(...b.col.slice(i * 4, i * 4 + 4));
2222
+ U.push(b.uv[i * 2], b.uv[i * 2 + 1]);
2223
+ }
2224
+ I.push(j);
2225
+ }
2226
+ 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 };
2227
+ };
2228
+ const encTex = (c) => {
2229
+ if (!c.texb)
2230
+ return {};
2231
+ const tex = [...c.texb].map(([name, b]) => {
2232
+ let t = texNames.indexOf(name);
2233
+ if (t < 0)
2234
+ t = texNames.push(name) - 1;
2235
+ return { t, ...b.wrapU ? { wrapU: true } : {}, ...texBucket(b) };
2236
+ });
2237
+ return { tbody: bucket(c.tbody), tex };
2238
+ };
2239
+ const enc = (c) => {
2240
+ const body = bucket(c.body), bodyP = lastPos, under = bucket(c.under), underP = lastPos;
2241
+ const edges = c.edges.length ? put(new Float32Array(c.edges)) : null;
2242
+ const condlB = condl(c.cond, [bodyP, underP, c.edges]), condlU = condl(c.condUnder, [bodyP, underP, c.edges]);
2243
+ 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) };
2244
+ };
2245
+ const encParts = Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), encKinds = Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)]));
2246
+ const textures = texNames.map((name) => {
2247
+ const b = options.texture?.(name);
2248
+ return b ? { name, mime: "image/png", data: put(b) } : { name, missing: true };
2249
+ });
2250
+ const features = [...anyCond ? ["cond"] : [], ...texNames.length ? ["tex"] : []];
2251
+ const header = { version: features.length ? 2 : 1, ...features.length ? { features } : {}, weld, parts: encParts, studKinds: encKinds, ...texNames.length ? { textures } : {}, ...extra };
1371
2252
  const hj = new TextEncoder().encode(JSON.stringify(header));
1372
2253
  const hpad = (4 - (4 + hj.length) % 4) % 4;
1373
2254
  const head = new Uint8Array(4 + hj.length + hpad);
@@ -1382,9 +2263,14 @@ function buildPack(o) {
1382
2263
  const lib = new Library2(o.ldrawRoot);
1383
2264
  const refs = new Map;
1384
2265
  const missing = new Set;
2266
+ const data = new Map;
1385
2267
  for (const m of o.models ?? []) {
1386
2268
  const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
1387
- const r = flatten2(splitModel(text, name), lib, (i) => refs.set(i.ref, i.part));
2269
+ const sm = splitModel(text, name);
2270
+ for (const [k, v] of sm.data)
2271
+ if (!data.has(k))
2272
+ data.set(k, v);
2273
+ const r = flatten2(sm, lib, (i) => refs.set(i.ref, i.part));
1388
2274
  for (const k of r.missing.keys())
1389
2275
  missing.add(k);
1390
2276
  }
@@ -1400,7 +2286,7 @@ function buildPack(o) {
1400
2286
  if (lib.isPart(k) && /\.dat$/.test(k))
1401
2287
  refs.set(k.slice(6), k);
1402
2288
  }
1403
- const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
2289
+ 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 });
1404
2290
  const parts = new Map, kinds = new Map;
1405
2291
  let n = 0;
1406
2292
  for (const [ref, key] of refs) {
@@ -1421,7 +2307,11 @@ function buildPack(o) {
1421
2307
  if (fs3.existsSync(cfg))
1422
2308
  extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
1423
2309
  }
1424
- return { data: encodePack(parts, kinds, comp.opts.weld, extra), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
2310
+ const texture = (n) => {
2311
+ const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
2312
+ return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
2313
+ };
2314
+ return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
1425
2315
  }
1426
2316
  export {
1427
2317
  Compiler,
@@ -1432,6 +2322,9 @@ export {
1432
2322
  buildPack,
1433
2323
  collectDeps,
1434
2324
  computeCulling,
2325
+ conditionalLineVisible,
2326
+ conditionalLines,
2327
+ createInstancedConditionalEdges,
1435
2328
  createInstancedEdges,
1436
2329
  createLdrObject,
1437
2330
  decodePack,
@@ -1449,8 +2342,13 @@ export {
1449
2342
  parseColor,
1450
2343
  parseLDConfig,
1451
2344
  parseModel,
2345
+ parseTexMap,
2346
+ resolveTexture,
1452
2347
  sequenceSteps,
1453
2348
  smoothNormals,
1454
2349
  splitModel,
2350
+ texmapTriangleUV,
2351
+ texmapUV,
2352
+ transformTexMap,
1455
2353
  writePack
1456
2354
  };