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/cli.js CHANGED
@@ -11,11 +11,785 @@ import path3 from "node:path";
11
11
  // src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
+
15
+ // src/spec.ts
16
+ var norm = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
17
+ function parseColor(tok) {
18
+ if (/^0x/i.test(tok))
19
+ return parseInt(tok, 16);
20
+ return +tok;
21
+ }
22
+ var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
23
+ var SPECIAL0 = new Set(["FILE", "NOFILE", "!DATA", "!:", "STEP", "BFC", "ROTSTEP", "BUFEXCHG", "!TEXMAP"]);
24
+ var PART_ORG = new Set(["part", "subpart", "primitive", "8_primitive", "48_primitive", "shortcut", "helper"]);
25
+ var MODEL_ORG = new Set(["model", "submodel", "configuration"]);
26
+ var ORG_RE = /^(?:!?LDRAW_ORG|Unofficial|Official\s+LCAD)\s+(\S+)/i;
27
+ function orgType(text) {
28
+ const m = ORG_RE.exec(text);
29
+ if (!m)
30
+ return;
31
+ const t = m[1].toLowerCase().replace(/^unofficial_/, "");
32
+ return PART_ORG.has(t) || MODEL_ORG.has(t) ? t : undefined;
33
+ }
34
+ function texNames(rest) {
35
+ const out = [];
36
+ let i = 0;
37
+ while (i < rest.length) {
38
+ while (rest[i] === " " || rest[i] === "\t")
39
+ i++;
40
+ if (i >= rest.length)
41
+ break;
42
+ if (rest[i] === '"') {
43
+ let v = "";
44
+ i++;
45
+ while (i < rest.length && rest[i] !== '"') {
46
+ if (rest[i] === "\\" && i + 1 < rest.length)
47
+ i++;
48
+ v += rest[i++];
49
+ }
50
+ i++;
51
+ out.push(v);
52
+ } else {
53
+ let j = i;
54
+ while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
55
+ j++;
56
+ out.push(rest.slice(i, j));
57
+ i = j;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ function parseTexMap(tail, next = false) {
63
+ const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
64
+ if (!m)
65
+ return null;
66
+ const method = m[1].toUpperCase();
67
+ const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
68
+ if (!n)
69
+ return null;
70
+ const parts = m[2].trim().split(/\s+/);
71
+ if (parts.length < n + 1)
72
+ return null;
73
+ const v = parts.slice(0, n).map(Number);
74
+ if (!v.every(Number.isFinite))
75
+ return null;
76
+ let rest = m[2].trim();
77
+ for (let k = 0;k < n; k++)
78
+ rest = rest.replace(/^\S+\s*/, "");
79
+ const names = texNames(rest);
80
+ if (!names.length)
81
+ return null;
82
+ const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
83
+ if (n >= 10)
84
+ t.a = v[9];
85
+ if (n === 11)
86
+ t.b = v[10];
87
+ const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
88
+ if (g > 0 && names[g + 1])
89
+ t.glossmap = names[g + 1];
90
+ if (next)
91
+ t.next = true;
92
+ return t;
93
+ }
94
+ var b64 = (s) => {
95
+ const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
96
+ const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
97
+ const bin = atob(pad);
98
+ const out = new Uint8Array(bin.length);
99
+ for (let i = 0;i < bin.length; i++)
100
+ out[i] = bin.charCodeAt(i);
101
+ return out;
102
+ };
103
+ function geomLine(tk) {
104
+ const t = +tk[0];
105
+ if (!(t >= 1 && t <= 5) || tk.length < 2)
106
+ return null;
107
+ const color = parseColor(tk[1]);
108
+ const nums = tk.slice(2).map(Number);
109
+ if (t === 1)
110
+ return { t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) };
111
+ if (t === 2)
112
+ return { t: 2, color, p: nums.slice(0, 6) };
113
+ if (t === 3)
114
+ return { t: 3, color, p: nums.slice(0, 9) };
115
+ if (t === 4)
116
+ return { t: 4, color, p: nums.slice(0, 12) };
117
+ return { t: 5, color, p: nums.slice(0, 12) };
118
+ }
119
+ function parse(src, name = "main.ldr", popts = {}) {
120
+ const gtext = !!popts.geometryText;
121
+ const files = new Map;
122
+ let cur = null;
123
+ let main = "";
124
+ let data;
125
+ let dataCur = null;
126
+ let texStack = [];
127
+ let texNext = null;
128
+ const closeData = () => {
129
+ if (!dataCur)
130
+ return;
131
+ const bytes = b64(dataCur.chunks.join(""));
132
+ const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
133
+ (data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
134
+ dataCur = null;
135
+ };
136
+ let inHeader = true;
137
+ const start = (n) => {
138
+ closeData();
139
+ cur = { name: norm(n), cmds: [] };
140
+ files.set(cur.name, cur);
141
+ if (!main)
142
+ main = cur.name;
143
+ texStack = [];
144
+ texNext = null;
145
+ inHeader = true;
146
+ };
147
+ let skip = false, sawBlock = false;
148
+ const firstBlock = () => {
149
+ if (sawBlock)
150
+ return;
151
+ sawBlock = true;
152
+ files.clear();
153
+ cur = null;
154
+ main = "";
155
+ };
156
+ const annotate = (c, fromColon) => {
157
+ if (texNext) {
158
+ c.tex = texNext;
159
+ texNext = null;
160
+ return;
161
+ }
162
+ const top = texStack[texStack.length - 1];
163
+ if (!top)
164
+ return;
165
+ if (top.fallback) {
166
+ if (!fromColon)
167
+ c.fallback = true;
168
+ } else
169
+ c.tex = top.map;
170
+ };
171
+ for (const raw of src.split(/\r?\n/)) {
172
+ const line = raw.trim();
173
+ if (!line)
174
+ continue;
175
+ if (gtext && line.charCodeAt(0) >= 50 && line.charCodeAt(0) <= 53 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
176
+ if (dataCur || skip)
177
+ continue;
178
+ if (!cur)
179
+ start(name);
180
+ const sp = line.slice(2).trimStart();
181
+ const e = sp.search(/\s/);
182
+ const g = { t: line.charCodeAt(0) - 48, color: parseColor(e < 0 ? sp : sp.slice(0, e)), p: [], src: line };
183
+ annotate(g, false);
184
+ cur.cmds.push(g);
185
+ inHeader = false;
186
+ continue;
187
+ }
188
+ if (line.charCodeAt(0) === 48 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
189
+ let a = 2;
190
+ while (line.charCodeAt(a) === 32 || line.charCodeAt(a) === 9)
191
+ a++;
192
+ let b = a;
193
+ while (b < line.length && line.charCodeAt(b) !== 32 && line.charCodeAt(b) !== 9)
194
+ b++;
195
+ const kw = line.slice(a, b);
196
+ if (!SPECIAL0.has(kw)) {
197
+ if (skip || dataCur)
198
+ continue;
199
+ if (!cur)
200
+ start(name);
201
+ const text = line.slice(a);
202
+ const c = { t: 0, text };
203
+ if (kw.charCodeAt(0) === 33)
204
+ c.meta = kw;
205
+ texNext = null;
206
+ if (inHeader && !cur.org) {
207
+ const o = orgType(text);
208
+ if (o)
209
+ cur.org = o;
210
+ }
211
+ const moved = text.match(MOVED_RE);
212
+ if (moved && !cur.movedTo) {
213
+ let target = moved[1].trim().replace(/\\/g, "/");
214
+ if (!/\.(dat|ldr|mpd)$/i.test(target))
215
+ target += ".dat";
216
+ cur.movedTo = norm(target);
217
+ }
218
+ cur.cmds.push(c);
219
+ continue;
220
+ }
221
+ }
222
+ const tk = line.split(/\s+/);
223
+ const t = +tk[0];
224
+ if (t === 0) {
225
+ const text = line.slice(1).trim();
226
+ if (tk[1] === "FILE") {
227
+ firstBlock();
228
+ skip = false;
229
+ start(text.slice(5));
230
+ continue;
231
+ }
232
+ if (tk[1] === "NOFILE") {
233
+ closeData();
234
+ cur = null;
235
+ skip = true;
236
+ continue;
237
+ }
238
+ if (tk[1] === "!DATA") {
239
+ firstBlock();
240
+ closeData();
241
+ cur = null;
242
+ skip = false;
243
+ dataCur = { name: text.slice(5).trim(), chunks: [] };
244
+ continue;
245
+ }
246
+ if (skip)
247
+ continue;
248
+ if (dataCur) {
249
+ if (tk[1] === "!:")
250
+ dataCur.chunks.push(tk.slice(2).join(""));
251
+ continue;
252
+ }
253
+ if (!cur)
254
+ start(name);
255
+ const c = { t: 0, text };
256
+ if (tk[1] === "STEP") {
257
+ c.step = true;
258
+ texStack = [];
259
+ texNext = null;
260
+ } else if (tk[1] === "BFC")
261
+ c.bfc = tk.slice(2).join(" ");
262
+ else if (tk[1] === "ROTSTEP") {
263
+ c.meta = "ROTSTEP";
264
+ c.step = true;
265
+ texStack = [];
266
+ texNext = null;
267
+ if ((tk[2] ?? "").toUpperCase() === "END")
268
+ c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
269
+ else {
270
+ const mode = (tk[5] ?? "REL").toUpperCase();
271
+ c.rotstep = { x: +tk[2] || 0, y: +tk[3] || 0, z: +tk[4] || 0, mode: ["REL", "ABS", "ADD"].includes(mode) ? mode : "REL" };
272
+ }
273
+ } else if (tk[1] === "BUFEXCHG") {
274
+ c.meta = "BUFEXCHG";
275
+ const op = (tk[2] ?? "").toUpperCase();
276
+ c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
277
+ } else if (tk[1]?.startsWith("!"))
278
+ c.meta = tk[1];
279
+ if (inHeader && !cur.org) {
280
+ const o = orgType(text);
281
+ if (o)
282
+ cur.org = o;
283
+ }
284
+ if (tk[1] === "!TEXMAP") {
285
+ const op = (tk[2] ?? "").toUpperCase();
286
+ texNext = null;
287
+ if (op === "START" || op === "NEXT") {
288
+ const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
289
+ c.texmap = { op, ...map ? { map } : {} };
290
+ if (map) {
291
+ (cur.texmaps ??= []).push(map);
292
+ if (op === "START")
293
+ texStack.push({ map, fallback: false });
294
+ else
295
+ texNext = map;
296
+ }
297
+ } else if (op === "FALLBACK") {
298
+ c.texmap = { op };
299
+ const top = texStack[texStack.length - 1];
300
+ if (top)
301
+ top.fallback = true;
302
+ } else if (op === "END") {
303
+ c.texmap = { op };
304
+ texStack.pop();
305
+ }
306
+ } else if (tk[1] === "!:") {
307
+ texNext = null;
308
+ const g = geomLine(tk.slice(2));
309
+ if (g) {
310
+ annotate(g, true);
311
+ c.geom = g;
312
+ }
313
+ } else
314
+ texNext = null;
315
+ const moved = text.match(MOVED_RE);
316
+ if (moved && !cur.movedTo) {
317
+ let target = moved[1].trim().replace(/\\/g, "/");
318
+ if (!/\.(dat|ldr|mpd)$/i.test(target))
319
+ target += ".dat";
320
+ cur.movedTo = norm(target);
321
+ }
322
+ cur.cmds.push(c);
323
+ continue;
324
+ }
325
+ if (dataCur || skip)
326
+ continue;
327
+ if (!cur)
328
+ start(name);
329
+ const g = geomLine(tk);
330
+ if (!g)
331
+ continue;
332
+ annotate(g, false);
333
+ cur.cmds.push(g);
334
+ inHeader = false;
335
+ }
336
+ closeData();
337
+ return data ? { main, files, data } : { main, files };
338
+ }
339
+ function resolveTexture(doc, name) {
340
+ if (!doc.data)
341
+ return null;
342
+ const n = norm(name);
343
+ return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
344
+ }
345
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
346
+ var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
347
+ 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]];
348
+ var unit3 = (a) => {
349
+ const l = Math.hypot(a[0], a[1], a[2]) || 1;
350
+ return [a[0] / l, a[1] / l, a[2] / l];
351
+ };
352
+ var DEG = Math.PI / 180;
353
+ function texmapUV(map, p) {
354
+ const w = texmapTriangleUV(map, [p, p, p]);
355
+ const u = w[0], v = w[1];
356
+ return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
357
+ }
358
+ var radial = (p, c, n) => {
359
+ const d = sub3(p, c);
360
+ const k = dot3(d, n);
361
+ const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
362
+ return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
363
+ };
364
+ function texmapTriangleUV(map, pts) {
365
+ const { p1, p2, p3 } = map;
366
+ const out = new Array(pts.length * 2).fill(0);
367
+ if (map.method === "PLANAR") {
368
+ const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
369
+ const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
370
+ pts.forEach((p, i) => {
371
+ const d = sub3(p, p1);
372
+ out[i * 2] = dot3(d, e1) / l1;
373
+ out[i * 2 + 1] = dot3(d, e2) / l2;
374
+ });
375
+ return out;
376
+ }
377
+ const cyl = map.method === "CYLINDRICAL";
378
+ const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
379
+ const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
380
+ const A = (map.a ?? 360) * DEG;
381
+ let base = null, baseAng = 0;
382
+ const sAng = (q) => {
383
+ if (!base) {
384
+ base = q;
385
+ baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
386
+ return baseAng;
387
+ }
388
+ return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
389
+ };
390
+ const miss = [];
391
+ let uSum = 0;
392
+ const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
393
+ pts.forEach((p, i) => {
394
+ if (cyl) {
395
+ const q = dir ? radial(p, p1, n) : null;
396
+ if (q) {
397
+ const a = sAng(q);
398
+ out[i * 2] = 0.5 + a / A;
399
+ uSum += out[i * 2];
400
+ } else
401
+ miss.push(i);
402
+ out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
403
+ } else {
404
+ const pd = unit3(sub3(p, p1));
405
+ const c = dot3(pd, n);
406
+ if (Math.abs(c) > 1 - 0.000000001) {
407
+ miss.push(i);
408
+ out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
409
+ return;
410
+ }
411
+ const a = sAng(radial(p, p1, n));
412
+ uSum += a;
413
+ out[i * 2] = 0.5 + a / A;
414
+ const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
415
+ 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]);
416
+ out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
417
+ }
418
+ });
419
+ if (miss.length && miss.length < pts.length) {
420
+ const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
421
+ for (const i of miss)
422
+ out[i * 2] = avg;
423
+ }
424
+ return out;
425
+ }
426
+ function transformTexMap(map, m) {
427
+ 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]];
428
+ return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
429
+ }
430
+ function mul(a, b) {
431
+ 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;
432
+ return [
433
+ a0 * bx + a1 * by + a2 * bz + ax,
434
+ a3 * bx + a4 * by + a5 * bz + ay,
435
+ a6 * bx + a7 * by + a8 * bz + az,
436
+ a0 * b0 + a1 * b3 + a2 * b6,
437
+ a0 * b1 + a1 * b4 + a2 * b7,
438
+ a0 * b2 + a1 * b5 + a2 * b8,
439
+ a3 * b0 + a4 * b3 + a5 * b6,
440
+ a3 * b1 + a4 * b4 + a5 * b7,
441
+ a3 * b2 + a4 * b5 + a5 * b8,
442
+ a6 * b0 + a7 * b3 + a8 * b6,
443
+ a6 * b1 + a7 * b4 + a8 * b7,
444
+ a6 * b2 + a7 * b5 + a8 * b8
445
+ ];
446
+ }
447
+ var I = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
448
+ var isDirectColor = (c) => c > 16777215;
449
+ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
450
+ const q = (i, a) => `${a[i].toFixed(2)},${a[i + 1].toFixed(2)},${a[i + 2].toFixed(2)}`;
451
+ const ek = (a, b) => a < b ? a + "|" + b : b + "|" + a;
452
+ const hard = new Set, soft = new Set;
453
+ for (let i = 0;i < edges.length; i += 6)
454
+ hard.add(ek(q(i, edges), q(i + 3, edges)));
455
+ const stride = cond.length > 0 && cond.length % 12 === 0 ? 12 : 6;
456
+ for (let i = 0;i < cond.length; i += stride)
457
+ soft.add(ek(q(i, cond), q(i + 3, cond)));
458
+ const nT = pos.length / 9, fn = [], vk = [];
459
+ for (let t = 0;t < nT; t++) {
460
+ const a = t * 9;
461
+ const u = [pos[a + 3] - pos[a], pos[a + 4] - pos[a + 1], pos[a + 5] - pos[a + 2]], v = [pos[a + 6] - pos[a], pos[a + 7] - pos[a + 1], pos[a + 8] - pos[a + 2]];
462
+ const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
463
+ const l = Math.hypot(...n) || 1;
464
+ fn.push(n.map((x) => x / l));
465
+ for (let k = 0;k < 3; k++)
466
+ vk.push(q(a + k * 3, pos));
467
+ }
468
+ const byV = new Map;
469
+ vk.forEach((k, i) => (byV.get(k) ?? byV.set(k, []).get(k)).push(i));
470
+ const edgeFaces = new Map;
471
+ for (let t = 0;t < nT; t++)
472
+ for (let k = 0;k < 3; k++) {
473
+ const e = ek(vk[t * 3 + k], vk[t * 3 + (k + 1) % 3]);
474
+ (edgeFaces.get(e) ?? edgeFaces.set(e, []).get(e)).push(t);
475
+ }
476
+ const cosC = Math.cos(creaseDeg * Math.PI / 180), out = new Float32Array(pos.length);
477
+ const joinable = (e, a, b) => {
478
+ if (hard.has(e))
479
+ return false;
480
+ const d = fn[a][0] * fn[b][0] + fn[a][1] * fn[b][1] + fn[a][2] * fn[b][2];
481
+ return soft.has(e) ? d > -0.2 : d > cosC;
482
+ };
483
+ for (const [key, corners] of byV) {
484
+ const faces = corners.map((c) => c / 3 | 0), par = faces.map((_, i) => i);
485
+ const f = (i) => par[i] === i ? i : par[i] = f(par[i]);
486
+ for (let i = 0;i < faces.length; i++)
487
+ for (let j = i + 1;j < faces.length; j++) {
488
+ const A = faces[i], B = faces[j];
489
+ const va = [0, 1, 2].map((k) => vk[A * 3 + k]), vb = new Set([0, 1, 2].map((k) => vk[B * 3 + k]));
490
+ for (const o of va)
491
+ if (o !== key && vb.has(o)) {
492
+ const e = ek(key, o);
493
+ if ((edgeFaces.get(e)?.length ?? 0) >= 2 && joinable(e, A, B))
494
+ par[f(i)] = f(j);
495
+ }
496
+ }
497
+ const acc = new Map;
498
+ faces.forEach((F, i) => {
499
+ const r = f(i), s = acc.get(r) ?? [0, 0, 0];
500
+ s[0] += fn[F][0];
501
+ s[1] += fn[F][1];
502
+ s[2] += fn[F][2];
503
+ acc.set(r, s);
504
+ });
505
+ corners.forEach((c, i) => {
506
+ const s = acc.get(f(i)), l = Math.hypot(...s) || 1;
507
+ out[c * 3] = s[0] / l;
508
+ out[c * 3 + 1] = s[1] / l;
509
+ out[c * 3 + 2] = s[2] / l;
510
+ });
511
+ }
512
+ return out;
513
+ }
514
+ var kindMemo = new WeakMap;
515
+ var SUBREF_RE = /^(s|8|48)\//;
516
+ function fileKind(doc, name) {
517
+ const f = doc.files.get(norm(name));
518
+ if (!f)
519
+ return "part";
520
+ let k = kindMemo.get(f);
521
+ if (k)
522
+ return k;
523
+ if (f.org)
524
+ k = PART_ORG.has(f.org) ? "part" : "model";
525
+ else if (f.name === doc.main)
526
+ k = "model";
527
+ else
528
+ k = f.cmds.some((c) => c.t >= 2 || c.t === 0 && !!c.geom || c.t === 1 && SUBREF_RE.test(c.file)) ? "part" : "model";
529
+ kindMemo.set(f, k);
530
+ return k;
531
+ }
532
+ var looseMemo = new WeakMap;
533
+ function looseSteps(f) {
534
+ let r = looseMemo.get(f);
535
+ if (r)
536
+ return r;
537
+ r = [];
538
+ let s = 0;
539
+ for (const c of f.cmds) {
540
+ if (c.t === 0 && c.step) {
541
+ s++;
542
+ continue;
543
+ }
544
+ if ((c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) && r[r.length - 1] !== s)
545
+ r.push(s);
546
+ }
547
+ looseMemo.set(f, r);
548
+ return r;
549
+ }
550
+ var colorToken = (c) => c > 16777215 ? "0x" + c.toString(16).toUpperCase() : String(c);
551
+ 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(" ")}`;
552
+ var loosePartName = (file, step) => `${norm(file)}#${step}`;
553
+ function looseFile(f, step) {
554
+ const out = [];
555
+ let s = 0;
556
+ const isNext = (c) => !!c && c.t === 0 && c.texmap?.op === "NEXT";
557
+ for (const c of f.cmds) {
558
+ if (c.t === 0) {
559
+ if (c.step) {
560
+ s++;
561
+ continue;
562
+ }
563
+ if (c.bfc !== undefined) {
564
+ out.push(c);
565
+ continue;
566
+ }
567
+ if (s !== step)
568
+ continue;
569
+ if (c.texmap)
570
+ out.push(c);
571
+ else if (c.geom) {
572
+ if (c.geom.t >= 2)
573
+ out.push(c);
574
+ else if (isNext(out[out.length - 1]))
575
+ out.pop();
576
+ }
577
+ continue;
578
+ }
579
+ if (s !== step)
580
+ continue;
581
+ if (c.t === 1) {
582
+ if (isNext(out[out.length - 1]))
583
+ out.pop();
584
+ continue;
585
+ }
586
+ out.push(c);
587
+ }
588
+ return { name: loosePartName(f.name, step), cmds: out };
589
+ }
590
+ function looseSource(f, step) {
591
+ return [`0 ldr: own geometry of ${f.name}, step ${step}`, ...looseFile(f, step).cmds.map(lineOf)].join(`
592
+ `) + `
593
+ `;
594
+ }
595
+ function hash32(s) {
596
+ let h = 2166136261;
597
+ for (let i = 0;i < s.length; i++) {
598
+ h ^= s.charCodeAt(i);
599
+ h = Math.imul(h, 16777619);
600
+ }
601
+ return (h >>> 0).toString(16).padStart(8, "0");
602
+ }
603
+ function invert(m) {
604
+ const [x, y, z, a, b, c, d, e, f, g, h, i] = m;
605
+ const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
606
+ const det = a * A + b * B + c * C;
607
+ if (!Number.isFinite(det) || Math.abs(det) < 0.000000000001)
608
+ return null;
609
+ const k = 1 / det;
610
+ 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];
611
+ 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];
612
+ }
613
+ var num = (x) => String(+x.toFixed(6) || 0);
614
+ var quote = (s) => '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"';
615
+ function texWrapper(part, map) {
616
+ const v = [...map.p1, ...map.p2, ...map.p3, ...map.method !== "PLANAR" ? [map.a ?? 360] : [], ...map.method === "SPHERICAL" ? [map.b ?? 180] : []].map(num).join(" ");
617
+ const source = `0 ldr: ${part} with a model-level !TEXMAP
618
+ 0 BFC CERTIFY CCW
619
+ 0 !TEXMAP START ${map.method} ${v} ${quote(map.texture)}${map.glossmap ? " GLOSSMAP " + quote(map.glossmap) : ""}
620
+ 1 16 0 0 0 1 0 0 0 1 0 0 0 1 ${norm(part)}
621
+ 0 !TEXMAP END
622
+ `;
623
+ return { name: `${norm(part)}#tex-${hash32(source)}`, source };
624
+ }
625
+ var mul3 = (a, b) => {
626
+ const r = new Array(9);
627
+ for (let i = 0;i < 3; i++)
628
+ for (let j = 0;j < 3; j++)
629
+ r[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
630
+ return r;
631
+ };
632
+ function rotStepMatrix(x, y, z) {
633
+ 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);
634
+ 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];
635
+ }
636
+ function stepViewMatrix(view, defaultView = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {
637
+ return !view ? [...defaultView] : view.abs ? [...view.m] : mul3(defaultView, view.m);
638
+ }
639
+ var viewMemo = new WeakMap;
640
+ function fileStepViews(f) {
641
+ const hit = viewMemo.get(f);
642
+ if (hit)
643
+ return hit;
644
+ const out = [];
645
+ let cur = null;
646
+ for (const c of f.cmds) {
647
+ if (c.t !== 0 || !c.step)
648
+ continue;
649
+ const r = c.rotstep;
650
+ if (r) {
651
+ if (r.mode === "END")
652
+ cur = null;
653
+ else {
654
+ const R = rotStepMatrix(r.x, r.y, r.z);
655
+ 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 };
656
+ }
657
+ }
658
+ out.push(cur);
659
+ }
660
+ out.push(cur);
661
+ viewMemo.set(f, out);
662
+ return out;
663
+ }
664
+
665
+ // src/model.ts
666
+ function flatten(doc, opts = {}) {
667
+ const out = [];
668
+ const maxDepth = opts.maxDepth ?? 64;
669
+ const loose = opts.looseGeometry !== false, tx = !!opts.textures;
670
+ const src = new Map;
671
+ const lazy = (f, k) => {
672
+ const key = f.name + "#" + k;
673
+ let fn = src.get(key);
674
+ if (!fn) {
675
+ let s;
676
+ fn = Object.assign(() => s ??= looseSource(f, k), { file: f, step: k });
677
+ src.set(key, fn);
678
+ }
679
+ return fn;
680
+ };
681
+ const walk = (d, fname, m, color, path, stepPath, depth, texIn) => {
682
+ const f = d.files.get(fname);
683
+ if (!f || depth > maxDepth)
684
+ return;
685
+ let s = 0;
686
+ const views = fileStepViews(f);
687
+ const ls = loose ? looseSteps(f) : [];
688
+ let li = 0;
689
+ const emit = (part, col, mm, sp, k, embedded, tex) => {
690
+ const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
691
+ if (embedded)
692
+ i.embedded = true;
693
+ if (tex) {
694
+ const w = texWrapper(part, tex);
695
+ i.texPart = w.name;
696
+ i.texSource = w.source;
697
+ }
698
+ const v = views[k];
699
+ if (v)
700
+ i.view = v;
701
+ out.push(i);
702
+ return i;
703
+ };
704
+ const flushLoose = (upTo) => {
705
+ while (li < ls.length && ls[li] <= upTo) {
706
+ const k = ls[li++];
707
+ emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null).source = lazy(f, k);
708
+ }
709
+ };
710
+ for (const c0 of f.cmds) {
711
+ if (c0.t === 0 && c0.step) {
712
+ s++;
713
+ continue;
714
+ }
715
+ let c = c0;
716
+ if (c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) {
717
+ flushLoose(s);
718
+ continue;
719
+ }
720
+ if (c.t === 0) {
721
+ if (tx && c.geom?.t === 1)
722
+ c = c.geom;
723
+ else
724
+ continue;
725
+ }
726
+ if (c.t !== 1)
727
+ continue;
728
+ if (tx && c.fallback)
729
+ continue;
730
+ const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
731
+ const tex = tx ? c.tex ?? texIn : null;
732
+ const local = tex ? invert(c.m) : null;
733
+ const lt = tex && local ? transformTexMap(tex, local) : null;
734
+ if (d.files.has(c.file)) {
735
+ if (fileKind(d, c.file) === "model") {
736
+ walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1, lt);
737
+ continue;
738
+ }
739
+ emit(c.file, col, mm, sp, s, true, lt);
740
+ continue;
741
+ }
742
+ const ext = opts.resolveModel?.(c.file);
743
+ if (ext) {
744
+ walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1, lt);
745
+ continue;
746
+ }
747
+ emit(c.file, col, mm, sp, s, false, lt);
748
+ }
749
+ flushLoose(Infinity);
750
+ };
751
+ if (fileKind(doc, doc.main) === "part") {
752
+ const f = doc.files.get(doc.main);
753
+ if (f)
754
+ out.push({ part: doc.main, color: 16, m: [...I], step: 0, steps: [0], localStep: 0, path: doc.main, embedded: true });
755
+ return out;
756
+ }
757
+ walk(doc, doc.main, I, 16, doc.main, [], 0, null);
758
+ return out;
759
+ }
760
+ function parseModel(text, name = "main.ldr", opts) {
761
+ return flatten(parse(text, name), opts);
762
+ }
763
+ function sequenceSteps(insts) {
764
+ const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
765
+ const uniq = [...new Set(insts.map(key))].sort();
766
+ const rank = new Map(uniq.map((k, j) => [k, j]));
767
+ const stepOf = new Int32Array(insts.length);
768
+ const views = new Array(uniq.length).fill(null);
769
+ insts.forEach((i, k) => {
770
+ const r = rank.get(key(i));
771
+ stepOf[k] = r;
772
+ if (i.view)
773
+ views[r] = i.view;
774
+ });
775
+ return { count: uniq.length, stepOf, views };
776
+ }
777
+ function parseLDConfig(text) {
778
+ const out = new Map;
779
+ for (const l of text.split(/\r?\n/)) {
780
+ const m = /!COLOUR\s+(\S+)\s+CODE\s+(\d+)\s+VALUE\s+(#[0-9A-Fa-f]{6})\s+EDGE\s+(\S+)(?:.*?ALPHA\s+(\d+))?/.exec(l);
781
+ if (m)
782
+ out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
783
+ }
784
+ return out;
785
+ }
786
+
787
+ // src/node/resolver.ts
14
788
  var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
15
789
  var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
16
- var norm = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
790
+ var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
17
791
  var threeName = (ref) => {
18
- const n = norm(ref);
792
+ const n = norm2(ref);
19
793
  if (n.startsWith("s/"))
20
794
  return "parts/" + n;
21
795
  if (n.startsWith("48/"))
@@ -67,8 +841,22 @@ class Library {
67
841
  this.index.set(r.toLowerCase(), path.join(dir, e.name));
68
842
  }
69
843
  }
844
+ virtual = new Map;
845
+ conflicts = new Set;
846
+ addVirtual(name, text) {
847
+ const n = norm(name), key = "mpd/" + n, prev = this.virtual.get(n);
848
+ if (prev === undefined) {
849
+ this.virtual.set(n, text);
850
+ this.text.set(key, text);
851
+ this.index.set(key, "(mpd) " + n);
852
+ } else if (prev !== text)
853
+ this.conflicts.add(n);
854
+ return key;
855
+ }
70
856
  resolve(ref) {
71
- const n = norm(ref);
857
+ const n = norm2(ref);
858
+ if (this.virtual.size && this.virtual.has(n))
859
+ return "mpd/" + n;
72
860
  for (const pre of ["parts/", "p/", "models/"])
73
861
  if (this.index.has(pre + n))
74
862
  return pre + n;
@@ -112,7 +900,7 @@ class Library {
112
900
  continue;
113
901
  const ck = this.resolve(r.name);
114
902
  if (!ck) {
115
- s.missing.add(norm(r.name));
903
+ s.missing.add(norm2(r.name));
116
904
  continue;
117
905
  }
118
906
  const cs = this.fileStats(ck);
@@ -145,58 +933,104 @@ class Library {
145
933
  }
146
934
  function splitModel(text, fallbackName = "main.ldr") {
147
935
  const files = new Map;
936
+ const data = new Map;
937
+ let dataCur = null;
148
938
  let cur = null, buf = [], main = null;
939
+ let skip = false, sawBlock = false;
940
+ const flush = () => {
941
+ if (cur)
942
+ files.set(cur, buf.join(`
943
+ `));
944
+ cur = null;
945
+ buf = [];
946
+ };
947
+ const firstBlock = () => {
948
+ if (sawBlock)
949
+ return;
950
+ sawBlock = true;
951
+ files.clear();
952
+ cur = null;
953
+ buf = [];
954
+ main = null;
955
+ };
149
956
  for (const line of text.split(/\r?\n/)) {
150
957
  const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
151
958
  if (m) {
152
- if (cur)
153
- files.set(cur, buf.join(`
154
- `));
155
- cur = norm(m[1]);
156
- buf = [];
959
+ firstBlock();
960
+ flush();
961
+ skip = false;
962
+ dataCur = null;
963
+ cur = norm2(m[1]);
157
964
  if (!main)
158
965
  main = cur;
159
966
  continue;
160
967
  }
161
- if (/^\s*0\s+NOFILE/i.test(line))
968
+ if (/^\s*0\s+(NOFILE|!DATA)(\s|$)/i.test(line)) {
969
+ dataCur = null;
970
+ if (/!DATA/i.test(line)) {
971
+ firstBlock();
972
+ const dn = norm2(line.replace(/^\s*0\s+!DATA\s*/i, ""));
973
+ dataCur = dn;
974
+ data.set(dn, "");
975
+ }
976
+ flush();
977
+ skip = true;
978
+ continue;
979
+ }
980
+ if (skip) {
981
+ const dc = dataCur;
982
+ if (dc) {
983
+ const d = /^\s*0\s+!:\s*(.*)$/.exec(line);
984
+ if (d)
985
+ data.set(dc, data.get(dc) + d[1].trim());
986
+ }
162
987
  continue;
988
+ }
163
989
  if (!cur) {
164
- cur = norm(fallbackName);
990
+ cur = norm2(fallbackName);
165
991
  main = cur;
166
992
  }
167
993
  buf.push(line);
168
994
  }
169
- if (cur)
170
- files.set(cur, buf.join(`
171
- `));
172
- return { files, main };
995
+ flush();
996
+ return { files, main, data };
997
+ }
998
+ function modelDoc(model) {
999
+ const files = new Map;
1000
+ for (const [n, t] of model.files) {
1001
+ const f = parse(t, n, { geometryText: true }).files.get(norm(n)) ?? { name: norm(n), cmds: [] };
1002
+ files.set(norm(n), f);
1003
+ }
1004
+ return { main: norm(model.main ?? ""), files };
173
1005
  }
174
- function flatten(model, lib, onInstance) {
1006
+ function flatten2(model, lib, onInstance, opts = {}) {
175
1007
  const missing = new Map;
176
- const walk = (name, parentM, parentColor, depth) => {
177
- if (depth > 32)
178
- return;
179
- for (const line of model.files.get(name).split(`
180
- `)) {
181
- const r = parseType1(line);
182
- if (!r)
183
- continue;
184
- const color = r.color === "16" ? parentColor : r.color;
185
- const m = compose(parentM, r.m);
186
- const n = norm(r.name);
187
- if (model.files.has(n)) {
188
- walk(n, m, color, depth + 1);
189
- continue;
190
- }
191
- const key = lib.resolve(r.name);
192
- if (!key) {
193
- missing.set(n, (missing.get(n) || 0) + 1);
194
- continue;
195
- }
196
- onInstance({ part: key, ref: n, color, m });
1008
+ for (const [n, t] of model.files)
1009
+ lib.addVirtual(n, t);
1010
+ const doc = modelDoc(model);
1011
+ const at = new Map;
1012
+ for (const i of flatten(doc, opts)) {
1013
+ let key;
1014
+ if (i.embedded)
1015
+ key = "mpd/" + i.part;
1016
+ else if (i.source)
1017
+ key = lib.addVirtual(i.part, i.source());
1018
+ else {
1019
+ if (!at.has(i.part))
1020
+ at.set(i.part, lib.resolve(i.part));
1021
+ key = at.get(i.part);
197
1022
  }
198
- };
199
- walk(model.main, IDENT, "16", 0);
1023
+ if (!key) {
1024
+ missing.set(i.part, (missing.get(i.part) || 0) + 1);
1025
+ continue;
1026
+ }
1027
+ const r = { part: key, ref: i.part, color: colorToken(i.color), m: i.m, inst: i };
1028
+ if (i.texPart) {
1029
+ r.texRef = i.texPart;
1030
+ r.texPart = lib.addVirtual(i.texPart, i.texSource);
1031
+ }
1032
+ onInstance(r);
1033
+ }
200
1034
  return { missing };
201
1035
  }
202
1036
  function collectDeps(lib, refs) {
@@ -270,7 +1104,7 @@ class Compiler {
270
1104
  const ck = key + (separateStuds ? "" : "#nostudsep");
271
1105
  if (this.cache.has(ck))
272
1106
  return this.cache.get(ck);
273
- const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
1107
+ const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, cond: this.opts.cond ? [] : undefined, condUnder: this.opts.cond ? [] : undefined, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
274
1108
  const colorOf = (code) => code === "16" ? [1, 1, 1, 0] : [...this.colors.get(code) || (code.startsWith("0x2") ? [parseInt(code.slice(3, 5), 16) / 255, parseInt(code.slice(5, 7), 16) / 255, parseInt(code.slice(7, 9), 16) / 255] : [1, 0, 1]), 1];
275
1109
  const walk = (fk, M, color, invert, target, depth) => {
276
1110
  if (depth > 40)
@@ -325,6 +1159,13 @@ class Compiler {
325
1159
  }
326
1160
  if (c === "5") {
327
1161
  out.condLines++;
1162
+ const dst = target === "under" ? out.condUnder : out.cond;
1163
+ if (dst) {
1164
+ const v = t.split(/\s+/).slice(2, 14).map(Number);
1165
+ if (v.length === 12 && v.every(Number.isFinite))
1166
+ for (let i = 0;i < 12; i += 3)
1167
+ dst.push(...xf(M, v.slice(i, i + 3)));
1168
+ }
328
1169
  continue;
329
1170
  }
330
1171
  if (c === "3" || c === "4") {
@@ -347,25 +1188,190 @@ class Compiler {
347
1188
  }
348
1189
  }
349
1190
  }
350
- if (!certified)
351
- out.uncertified = true;
1191
+ if (!certified)
1192
+ out.uncertified = true;
1193
+ };
1194
+ walk(key, IDENT, "16", false, "body", 0);
1195
+ if (this.opts.tex && this.hasTex(key))
1196
+ this.texPass(key, out, separateStuds, colorOf);
1197
+ const [x0, y0, z0, x1, y1, z1] = out.bbox;
1198
+ let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
1199
+ for (const b of [out.body, out.under])
1200
+ for (let i = 0;i < b.pos.length; i += 3)
1201
+ if (b.pos[i + 1] > y1 - 0.5) {
1202
+ ax0 = Math.min(ax0, b.pos[i]);
1203
+ ax1 = Math.max(ax1, b.pos[i]);
1204
+ az0 = Math.min(az0, b.pos[i + 2]);
1205
+ az1 = Math.max(az1, b.pos[i + 2]);
1206
+ }
1207
+ out.fullBottom = isFinite(x0) && ax0 <= x0 + 0.5 && ax1 >= x1 - 0.5 && az0 <= z0 + 0.5 && az1 >= z1 - 0.5;
1208
+ if (separateStuds && isFinite(x0))
1209
+ out.bottomMask = enclosureMask(out);
1210
+ this.cache.set(ck, out);
1211
+ return out;
1212
+ }
1213
+ texMemo = new Map;
1214
+ hasTex(key) {
1215
+ const m = this.texMemo.get(key);
1216
+ if (m !== undefined)
1217
+ return m;
1218
+ this.texMemo.set(key, false);
1219
+ const txt = this.lib.read(key);
1220
+ let r = /^\s*0\s+!TEXMAP/im.test(txt);
1221
+ if (!r)
1222
+ for (const l of txt.split(/\r?\n/)) {
1223
+ const t = l.trim();
1224
+ const p = t.startsWith("0 !:") ? parseType1(t.slice(4)) : t[0] === "1" ? parseType1(t) : null;
1225
+ if (p) {
1226
+ const k = this.lib.resolve(p.name);
1227
+ if (k && this.hasTex(k)) {
1228
+ r = true;
1229
+ break;
1230
+ }
1231
+ }
1232
+ }
1233
+ this.texMemo.set(key, r);
1234
+ return r;
1235
+ }
1236
+ texture(name) {
1237
+ const n = norm2(name);
1238
+ for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
1239
+ const k = pre + n;
1240
+ if (this.lib.index.has(k))
1241
+ return new Uint8Array(fs2.readFileSync(this.lib.index.get(k)));
1242
+ }
1243
+ return null;
1244
+ }
1245
+ texPass(key, out, separateStuds, colorOf) {
1246
+ const tbody = { pos: [], col: [] }, texb = new Map;
1247
+ const walk = (fk, M, color, invert, target, depth, texIn) => {
1248
+ if (depth > 40)
1249
+ return;
1250
+ let ccw = true, invertNext = false;
1251
+ let stack = [];
1252
+ let next = null;
1253
+ for (const raw of this.lib.read(fk).split(/\r?\n/)) {
1254
+ let t = raw.trim();
1255
+ if (!t)
1256
+ continue;
1257
+ let colon = false;
1258
+ if (t[0] === "0") {
1259
+ const m = /^0\s+(\S+)\s*(.*)$/.exec(t);
1260
+ const kw = (m?.[1] ?? "").toUpperCase(), rest = m?.[2] ?? "";
1261
+ if (kw === "!TEXMAP") {
1262
+ const op = (rest.split(/\s+/)[0] || "").toUpperCase();
1263
+ next = null;
1264
+ if (op === "START" || op === "NEXT") {
1265
+ const map = parseTexMap(rest.replace(/^\S+/, ""), op === "NEXT");
1266
+ if (map) {
1267
+ const pm = transformTexMap(map, M);
1268
+ if (op === "START")
1269
+ stack.push({ map: pm, fallback: false });
1270
+ else
1271
+ next = pm;
1272
+ }
1273
+ } else if (op === "FALLBACK") {
1274
+ const top = stack[stack.length - 1];
1275
+ if (top)
1276
+ top.fallback = true;
1277
+ } else if (op === "END")
1278
+ stack.pop();
1279
+ continue;
1280
+ }
1281
+ if (kw === "!:") {
1282
+ next = null;
1283
+ t = rest.trim();
1284
+ colon = true;
1285
+ if (!t || t[0] === "0")
1286
+ continue;
1287
+ } else {
1288
+ if (kw === "BFC") {
1289
+ if (/\bCW\b/.test(t))
1290
+ ccw = false;
1291
+ if (/\bCCW\b/.test(t))
1292
+ ccw = true;
1293
+ if (/INVERTNEXT/.test(t))
1294
+ invertNext = true;
1295
+ }
1296
+ if (kw === "STEP")
1297
+ stack = [];
1298
+ next = null;
1299
+ continue;
1300
+ }
1301
+ }
1302
+ let tex = texIn;
1303
+ if (next) {
1304
+ tex = next;
1305
+ next = null;
1306
+ } else {
1307
+ const top = stack[stack.length - 1];
1308
+ if (top) {
1309
+ if (top.fallback) {
1310
+ if (!colon)
1311
+ continue;
1312
+ } else
1313
+ tex = top.map;
1314
+ }
1315
+ }
1316
+ const c = t[0];
1317
+ if (c === "1") {
1318
+ const r = parseType1(t);
1319
+ if (!r)
1320
+ continue;
1321
+ const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
1322
+ const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
1323
+ const ck2 = this.resolve(r.name, isTop);
1324
+ if (!ck2) {
1325
+ invertNext = false;
1326
+ continue;
1327
+ }
1328
+ const inv = invert !== invertNext !== det3(r.m) < 0;
1329
+ invertNext = false;
1330
+ if (isTop && separateStuds)
1331
+ continue;
1332
+ walk(ck2, compose(M, r.m), r.color === "16" ? color : r.color, inv, isUnder ? "under" : target, depth + 1, tex);
1333
+ continue;
1334
+ }
1335
+ if (c !== "3" && c !== "4")
1336
+ continue;
1337
+ const tok = t.split(/\s+/);
1338
+ const code = tok[1] === "16" ? color : tok[1];
1339
+ const n = c === "3" ? 3 : 4;
1340
+ const v = tok.slice(2, 2 + n * 3).map(Number);
1341
+ const pts = [];
1342
+ for (let i = 0;i < n; i++)
1343
+ pts.push(xf(M, v.slice(i * 3, i * 3 + 3)));
1344
+ let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
1345
+ if (!ccw !== invert)
1346
+ order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
1347
+ const rgba = colorOf(code);
1348
+ if (tex) {
1349
+ let b = texb.get(tex.texture);
1350
+ if (!b)
1351
+ texb.set(tex.texture, b = { pos: [], col: [], uv: [] });
1352
+ if (Math.abs(tex.a ?? 0) >= 360)
1353
+ b.wrapU = true;
1354
+ for (let k = 0;k < order.length; k += 3) {
1355
+ const tri = [pts[order[k]], pts[order[k + 1]], pts[order[k + 2]]];
1356
+ const w = texmapTriangleUV(tex, tri);
1357
+ for (let j = 0;j < 3; j++) {
1358
+ b.pos.push(...tri[j]);
1359
+ b.col.push(...rgba);
1360
+ b.uv.push(w[j * 2], w[j * 2 + 1]);
1361
+ }
1362
+ }
1363
+ } else if (target === "body")
1364
+ for (const i of order) {
1365
+ tbody.pos.push(...pts[i]);
1366
+ tbody.col.push(...rgba);
1367
+ }
1368
+ }
352
1369
  };
353
- walk(key, IDENT, "16", false, "body", 0);
354
- const [x0, y0, z0, x1, y1, z1] = out.bbox;
355
- let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
356
- for (const b of [out.body, out.under])
357
- for (let i = 0;i < b.pos.length; i += 3)
358
- if (b.pos[i + 1] > y1 - 0.5) {
359
- ax0 = Math.min(ax0, b.pos[i]);
360
- ax1 = Math.max(ax1, b.pos[i]);
361
- az0 = Math.min(az0, b.pos[i + 2]);
362
- az1 = Math.max(az1, b.pos[i + 2]);
363
- }
364
- out.fullBottom = isFinite(x0) && ax0 <= x0 + 0.5 && ax1 >= x1 - 0.5 && az0 <= z0 + 0.5 && az1 >= z1 - 0.5;
365
- if (separateStuds && isFinite(x0))
366
- out.bottomMask = enclosureMask(out);
367
- this.cache.set(ck, out);
368
- return out;
1370
+ walk(key, IDENT, "16", false, "body", 0, null);
1371
+ if (texb.size) {
1372
+ out.tbody = tbody;
1373
+ out.texb = texb;
1374
+ }
369
1375
  }
370
1376
  }
371
1377
  function enclosureMask(c) {
@@ -449,10 +1455,10 @@ function growBox(b, M, cb) {
449
1455
  for (const z of [cb[2], cb[5]])
450
1456
  grow(b, xf(M, [x, y, z]));
451
1457
  }
452
- function writePack(file, parts, studKinds, weld, extra) {
453
- fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
1458
+ function writePack(file, parts, studKinds, weld, extra, options) {
1459
+ fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
454
1460
  }
455
- function encodePack(parts, studKinds, weld, extra) {
1461
+ function encodePack(parts, studKinds, weld, extra, options = {}) {
456
1462
  const chunks = [];
457
1463
  let off = 0;
458
1464
  const put = (a) => {
@@ -461,13 +1467,15 @@ function encodePack(parts, studKinds, weld, extra) {
461
1467
  chunks.push(new Uint8Array(pad));
462
1468
  off += pad;
463
1469
  }
464
- const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
1470
+ const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
465
1471
  chunks.push(a);
466
1472
  off += a.byteLength;
467
1473
  return r;
468
1474
  };
1475
+ let lastPos = [];
469
1476
  const bucket = (b) => {
470
1477
  const n = b.pos.length / 3;
1478
+ lastPos = [];
471
1479
  if (!n)
472
1480
  return null;
473
1481
  const nrm = new Float32Array(n * 3);
@@ -486,8 +1494,10 @@ function encodePack(parts, studKinds, weld, extra) {
486
1494
  nrm[(i + k) * 3 + 2] = nz;
487
1495
  }
488
1496
  }
489
- if (!weld)
1497
+ if (!weld) {
1498
+ lastPos = b.pos;
490
1499
  return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
1500
+ }
491
1501
  const map = new Map;
492
1502
  const P = [], N = [], C = [], I = [];
493
1503
  for (let i = 0;i < n; i++) {
@@ -503,235 +1513,118 @@ function encodePack(parts, studKinds, weld, extra) {
503
1513
  I.push(j);
504
1514
  }
505
1515
  const vcount = P.length / 3;
1516
+ lastPos = P;
506
1517
  return { pos: put(new Float32Array(P)), nrm: put(new Float32Array(N)), col: put(new Float32Array(C)), idx: put(vcount < 65536 ? new Uint16Array(I) : new Uint32Array(I)), tris: n / 3 };
507
1518
  };
508
- const enc = (c) => ({ body: bucket(c.body), under: bucket(c.under), edges: c.edges.length ? put(new Float32Array(c.edges)) : null, cond: c.condLines, studs: c.studs.map((s) => [s.kind, s.color, ...s.m.map((x) => +x.toFixed(4))]), bbox: c.bbox, fullBottom: c.fullBottom, uncert: !!c.uncertified, mask: c.bottomMask ? [c.bottomMask.nx, c.bottomMask.nz, c.bottomMask.bits.join("")] : null });
509
- const header = { version: 1, weld, parts: Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), studKinds: Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)])), ...extra };
510
- const hj = new TextEncoder().encode(JSON.stringify(header));
511
- const hpad = (4 - (4 + hj.length) % 4) % 4;
512
- const head = new Uint8Array(4 + hj.length + hpad);
513
- new DataView(head.buffer).setUint32(0, hj.length + hpad, true);
514
- head.set(hj, 4);
515
- head.fill(32, 4 + hj.length);
516
- return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
517
- }
518
-
519
- // src/spec.ts
520
- var norm2 = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
521
- function parseColor(tok) {
522
- if (/^0x/i.test(tok))
523
- return parseInt(tok, 16);
524
- return +tok;
525
- }
526
- var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
527
- function parse(src, name = "main.ldr") {
528
- const files = new Map;
529
- let cur = null;
530
- let main = "";
531
- const start = (n) => {
532
- cur = { name: norm2(n), cmds: [] };
533
- files.set(cur.name, cur);
534
- if (!main)
535
- main = cur.name;
536
- };
537
- for (const raw of src.split(/\r?\n/)) {
538
- const line = raw.trim();
539
- if (!line)
540
- continue;
541
- const tk = line.split(/\s+/);
542
- const t = +tk[0];
543
- if (t === 0) {
544
- const text = line.slice(1).trim();
545
- if (tk[1] === "FILE") {
546
- start(text.slice(5));
547
- continue;
548
- }
549
- if (tk[1] === "NOFILE") {
550
- cur = null;
551
- continue;
1519
+ let anyCond = false;
1520
+ const condl = (a, pools) => {
1521
+ if (!a || !a.length)
1522
+ return;
1523
+ anyCond = true;
1524
+ const key = (p, i) => Math.round(p[i] * 1000) + "," + Math.round(p[i + 1] * 1000) + "," + Math.round(p[i + 2] * 1000);
1525
+ const need = new Map;
1526
+ for (let i = 0;i < a.length; i += 3)
1527
+ need.set(key(a, i), -1);
1528
+ let base = 0;
1529
+ for (const p of pools) {
1530
+ for (let i = 0;i < p.length; i += 3) {
1531
+ const k = key(p, i);
1532
+ if (need.get(k) === -1)
1533
+ need.set(k, base + i / 3);
552
1534
  }
553
- if (!cur)
554
- start(name);
555
- const c = { t: 0, text };
556
- if (tk[1] === "STEP")
557
- c.step = true;
558
- else if (tk[1] === "BFC")
559
- c.bfc = tk.slice(2).join(" ");
560
- else if (tk[1] === "ROTSTEP") {
561
- c.meta = "ROTSTEP";
562
- if ((tk[2] ?? "").toUpperCase() === "END")
563
- c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
564
- else {
565
- const mode = (tk[5] ?? "REL").toUpperCase();
566
- c.rotstep = { x: +tk[2] || 0, y: +tk[3] || 0, z: +tk[4] || 0, mode: ["REL", "ABS", "ADD"].includes(mode) ? mode : "REL" };
567
- }
568
- } else if (tk[1] === "BUFEXCHG") {
569
- c.meta = "BUFEXCHG";
570
- const op = (tk[2] ?? "").toUpperCase();
571
- c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
572
- } else if (tk[1]?.startsWith("!"))
573
- c.meta = tk[1];
574
- const moved = text.match(MOVED_RE);
575
- if (moved && !cur.movedTo) {
576
- let target = moved[1].trim().replace(/\\/g, "/");
577
- if (!/\.(dat|ldr|mpd)$/i.test(target))
578
- target += ".dat";
579
- cur.movedTo = norm2(target);
1535
+ base += p.length / 3;
1536
+ }
1537
+ const X = [], I = new Array(a.length / 3);
1538
+ for (let i = 0;i < a.length; i += 3) {
1539
+ const k = key(a, i);
1540
+ let j = need.get(k);
1541
+ if (j === -1) {
1542
+ j = base + X.length / 3;
1543
+ need.set(k, j);
1544
+ X.push(a[i], a[i + 1], a[i + 2]);
580
1545
  }
581
- cur.cmds.push(c);
582
- continue;
1546
+ I[i / 3] = j;
583
1547
  }
584
- if (!cur)
585
- start(name);
586
- const color = parseColor(tk[1]);
587
- const nums = tk.slice(2).map(Number);
588
- if (t === 1)
589
- cur.cmds.push({ t: 1, color, m: nums.slice(0, 12), file: norm2(tk.slice(14).join(" ")) });
590
- else if (t === 2)
591
- cur.cmds.push({ t: 2, color, p: nums.slice(0, 6) });
592
- else if (t === 3)
593
- cur.cmds.push({ t: 3, color, p: nums.slice(0, 9) });
594
- else if (t === 4)
595
- cur.cmds.push({ t: 4, color, p: nums.slice(0, 12) });
596
- else if (t === 5)
597
- cur.cmds.push({ t: 5, color, p: nums.slice(0, 12) });
598
- }
599
- return { main, files };
600
- }
601
- function mul(a, b) {
602
- const [ax, ay, az, a0, a1, a2, a3, a4, a5, a6, a7, a8] = a, [bx, by, bz, b0, b1, b2, b3, b4, b5, b6, b7, b8] = b;
603
- return [
604
- a0 * bx + a1 * by + a2 * bz + ax,
605
- a3 * bx + a4 * by + a5 * bz + ay,
606
- a6 * bx + a7 * by + a8 * bz + az,
607
- a0 * b0 + a1 * b3 + a2 * b6,
608
- a0 * b1 + a1 * b4 + a2 * b7,
609
- a0 * b2 + a1 * b5 + a2 * b8,
610
- a3 * b0 + a4 * b3 + a5 * b6,
611
- a3 * b1 + a4 * b4 + a5 * b7,
612
- a3 * b2 + a4 * b5 + a5 * b8,
613
- a6 * b0 + a7 * b3 + a8 * b6,
614
- a6 * b1 + a7 * b4 + a8 * b7,
615
- a6 * b2 + a7 * b5 + a8 * b8
616
- ];
617
- }
618
- var I = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
619
- var isDirectColor = (c) => c > 16777215;
620
- function smoothNormals(pos, edges, cond, creaseDeg = 35) {
621
- const q = (i, a) => `${a[i].toFixed(2)},${a[i + 1].toFixed(2)},${a[i + 2].toFixed(2)}`;
622
- const ek = (a, b) => a < b ? a + "|" + b : b + "|" + a;
623
- const hard = new Set, soft = new Set;
624
- for (let i = 0;i < edges.length; i += 6)
625
- hard.add(ek(q(i, edges), q(i + 3, edges)));
626
- const stride = cond.length > 0 && cond.length % 12 === 0 ? 12 : 6;
627
- for (let i = 0;i < cond.length; i += stride)
628
- soft.add(ek(q(i, cond), q(i + 3, cond)));
629
- const nT = pos.length / 9, fn = [], vk = [];
630
- for (let t = 0;t < nT; t++) {
631
- const a = t * 9;
632
- const u = [pos[a + 3] - pos[a], pos[a + 4] - pos[a + 1], pos[a + 5] - pos[a + 2]], v = [pos[a + 6] - pos[a], pos[a + 7] - pos[a + 1], pos[a + 8] - pos[a + 2]];
633
- const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
634
- const l = Math.hypot(...n) || 1;
635
- fn.push(n.map((x) => x / l));
636
- for (let k = 0;k < 3; k++)
637
- vk.push(q(a + k * 3, pos));
638
- }
639
- const byV = new Map;
640
- vk.forEach((k, i) => (byV.get(k) ?? byV.set(k, []).get(k)).push(i));
641
- const edgeFaces = new Map;
642
- for (let t = 0;t < nT; t++)
643
- for (let k = 0;k < 3; k++) {
644
- const e = ek(vk[t * 3 + k], vk[t * 3 + (k + 1) % 3]);
645
- (edgeFaces.get(e) ?? edgeFaces.set(e, []).get(e)).push(t);
1548
+ let prev = 0, max = 0;
1549
+ for (let i = 0;i < I.length; i++) {
1550
+ const d = I[i] - prev;
1551
+ prev = I[i];
1552
+ const z = d >= 0 ? d * 2 : -d * 2 - 1;
1553
+ I[i] = z;
1554
+ if (z > max)
1555
+ max = z;
646
1556
  }
647
- const cosC = Math.cos(creaseDeg * Math.PI / 180), out = new Float32Array(pos.length);
648
- const joinable = (e, a, b) => {
649
- if (hard.has(e))
650
- return false;
651
- const d = fn[a][0] * fn[b][0] + fn[a][1] * fn[b][1] + fn[a][2] * fn[b][2];
652
- return soft.has(e) ? d > -0.2 : d > cosC;
1557
+ return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
653
1558
  };
654
- for (const [key, corners] of byV) {
655
- const faces = corners.map((c) => c / 3 | 0), par = faces.map((_, i) => i);
656
- const f = (i) => par[i] === i ? i : par[i] = f(par[i]);
657
- for (let i = 0;i < faces.length; i++)
658
- for (let j = i + 1;j < faces.length; j++) {
659
- const A = faces[i], B = faces[j];
660
- const va = [0, 1, 2].map((k) => vk[A * 3 + k]), vb = new Set([0, 1, 2].map((k) => vk[B * 3 + k]));
661
- for (const o of va)
662
- if (o !== key && vb.has(o)) {
663
- const e = ek(key, o);
664
- if ((edgeFaces.get(e)?.length ?? 0) >= 2 && joinable(e, A, B))
665
- par[f(i)] = f(j);
666
- }
667
- }
668
- const acc = new Map;
669
- faces.forEach((F, i) => {
670
- const r = f(i), s = acc.get(r) ?? [0, 0, 0];
671
- s[0] += fn[F][0];
672
- s[1] += fn[F][1];
673
- s[2] += fn[F][2];
674
- acc.set(r, s);
675
- });
676
- corners.forEach((c, i) => {
677
- const s = acc.get(f(i)), l = Math.hypot(...s) || 1;
678
- out[c * 3] = s[0] / l;
679
- out[c * 3 + 1] = s[1] / l;
680
- out[c * 3 + 2] = s[2] / l;
681
- });
682
- }
683
- return out;
684
- }
685
-
686
- // src/model.ts
687
- function flatten2(doc, opts = {}) {
688
- const out = [];
689
- const maxDepth = opts.maxDepth ?? 64;
690
- const walk = (d, fname, m, color, path, stepPath, depth) => {
691
- const f = d.files.get(fname);
692
- if (!f || depth > maxDepth)
693
- return;
694
- let s = 0;
695
- for (const c of f.cmds) {
696
- if (c.t === 0 && c.step)
697
- s++;
698
- if (c.t !== 1)
699
- continue;
700
- const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
701
- if (d.files.has(c.file)) {
702
- walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
703
- continue;
1559
+ const texNames = [];
1560
+ const texBucket = (b) => {
1561
+ const n = b.pos.length / 3;
1562
+ if (!n)
1563
+ return null;
1564
+ const nrm = new Float32Array(n * 3);
1565
+ for (let i = 0;i < n; i += 3) {
1566
+ const p = b.pos;
1567
+ const a = i * 3;
1568
+ 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];
1569
+ let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
1570
+ const l = Math.hypot(nx, ny, nz) || 1;
1571
+ nx /= l;
1572
+ ny /= l;
1573
+ nz /= l;
1574
+ for (let k = 0;k < 3; k++) {
1575
+ nrm[(i + k) * 3] = nx;
1576
+ nrm[(i + k) * 3 + 1] = ny;
1577
+ nrm[(i + k) * 3 + 2] = nz;
704
1578
  }
705
- const ext = opts.resolveModel?.(c.file);
706
- if (ext) {
707
- walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
708
- continue;
1579
+ }
1580
+ const map = new Map;
1581
+ const P = [], N = [], C = [], U = [], I = [];
1582
+ for (let i = 0;i < n; i++) {
1583
+ 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);
1584
+ let j = map.get(k);
1585
+ if (j === undefined) {
1586
+ j = P.length / 3;
1587
+ map.set(k, j);
1588
+ P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
1589
+ N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
1590
+ C.push(...b.col.slice(i * 4, i * 4 + 4));
1591
+ U.push(b.uv[i * 2], b.uv[i * 2 + 1]);
709
1592
  }
710
- out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
1593
+ I.push(j);
711
1594
  }
1595
+ 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 };
712
1596
  };
713
- walk(doc, doc.main, I, 16, doc.main, [], 0);
714
- return out;
715
- }
716
- function parseModel(text, name = "main.ldr", opts) {
717
- return flatten2(parse(text, name), opts);
718
- }
719
- function sequenceSteps(insts) {
720
- const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
721
- const uniq = [...new Set(insts.map(key))].sort();
722
- const rank = new Map(uniq.map((k, j) => [k, j]));
723
- const stepOf = new Int32Array(insts.length);
724
- insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
725
- return { count: uniq.length, stepOf };
726
- }
727
- function parseLDConfig(text) {
728
- const out = new Map;
729
- for (const l of text.split(/\r?\n/)) {
730
- const m = /!COLOUR\s+(\S+)\s+CODE\s+(\d+)\s+VALUE\s+(#[0-9A-Fa-f]{6})\s+EDGE\s+(\S+)(?:.*?ALPHA\s+(\d+))?/.exec(l);
731
- if (m)
732
- out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
733
- }
734
- return out;
1597
+ const encTex = (c) => {
1598
+ if (!c.texb)
1599
+ return {};
1600
+ const tex = [...c.texb].map(([name, b]) => {
1601
+ let t = texNames.indexOf(name);
1602
+ if (t < 0)
1603
+ t = texNames.push(name) - 1;
1604
+ return { t, ...b.wrapU ? { wrapU: true } : {}, ...texBucket(b) };
1605
+ });
1606
+ return { tbody: bucket(c.tbody), tex };
1607
+ };
1608
+ const enc = (c) => {
1609
+ const body = bucket(c.body), bodyP = lastPos, under = bucket(c.under), underP = lastPos;
1610
+ const edges = c.edges.length ? put(new Float32Array(c.edges)) : null;
1611
+ const condlB = condl(c.cond, [bodyP, underP, c.edges]), condlU = condl(c.condUnder, [bodyP, underP, c.edges]);
1612
+ 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) };
1613
+ };
1614
+ const encParts = Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), encKinds = Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)]));
1615
+ const textures = texNames.map((name) => {
1616
+ const b = options.texture?.(name);
1617
+ return b ? { name, mime: "image/png", data: put(b) } : { name, missing: true };
1618
+ });
1619
+ const features = [...anyCond ? ["cond"] : [], ...texNames.length ? ["tex"] : []];
1620
+ const header = { version: features.length ? 2 : 1, ...features.length ? { features } : {}, weld, parts: encParts, studKinds: encKinds, ...texNames.length ? { textures } : {}, ...extra };
1621
+ const hj = new TextEncoder().encode(JSON.stringify(header));
1622
+ const hpad = (4 - (4 + hj.length) % 4) % 4;
1623
+ const head = new Uint8Array(4 + hj.length + hpad);
1624
+ new DataView(head.buffer).setUint32(0, hj.length + hpad, true);
1625
+ head.set(hj, 4);
1626
+ head.fill(32, 4 + hj.length);
1627
+ return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
735
1628
  }
736
1629
 
737
1630
  // src/node/build-pack.ts
@@ -739,25 +1632,36 @@ function buildPack(o) {
739
1632
  const lib = new Library(o.ldrawRoot);
740
1633
  const refs = new Map;
741
1634
  const missing = new Set;
1635
+ const data = new Map;
742
1636
  for (const m of o.models ?? []) {
743
1637
  const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
744
- const r = flatten(splitModel(text, name), lib, (i) => refs.set(i.ref, i.part));
745
- for (const k of r.missing.keys())
746
- missing.add(k);
1638
+ const sm = splitModel(text, name);
1639
+ for (const [k, v] of sm.data)
1640
+ if (!data.has(k))
1641
+ data.set(k, v);
1642
+ for (const textures of o.textures ? [false, true] : [false]) {
1643
+ const r = flatten2(sm, lib, (i) => {
1644
+ refs.set(i.ref, i.part);
1645
+ if (i.texRef)
1646
+ refs.set(i.texRef, i.texPart);
1647
+ }, { textures, looseGeometry: o.looseGeometry });
1648
+ for (const k of r.missing.keys())
1649
+ missing.add(k);
1650
+ }
747
1651
  }
748
1652
  for (const p of o.parts ?? []) {
749
1653
  const k = lib.resolve(p);
750
1654
  if (k)
751
- refs.set(norm(p), k);
1655
+ refs.set(norm2(p), k);
752
1656
  else
753
- missing.add(norm(p));
1657
+ missing.add(norm2(p));
754
1658
  }
755
1659
  if (o.allParts) {
756
1660
  for (const k of lib.index.keys())
757
1661
  if (lib.isPart(k) && /\.dat$/.test(k))
758
1662
  refs.set(k.slice(6), k);
759
1663
  }
760
- const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
1664
+ const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true, cond: !!o.cond, tex: !!o.textures });
761
1665
  const parts = new Map, kinds = new Map;
762
1666
  let n = 0;
763
1667
  for (const [ref, key] of refs) {
@@ -778,7 +1682,12 @@ function buildPack(o) {
778
1682
  if (fs3.existsSync(cfg))
779
1683
  extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
780
1684
  }
781
- return { data: encodePack(parts, kinds, comp.opts.weld, extra), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
1685
+ const texture = (n) => {
1686
+ const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
1687
+ return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
1688
+ };
1689
+ const warnings = [...lib.conflicts].map((n) => `${n}: defined differently by two models; the first definition is used`);
1690
+ return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing], ...warnings.length ? { warnings } : {} };
782
1691
  }
783
1692
 
784
1693
  // src/cli.ts
@@ -795,7 +1704,12 @@ Options:
795
1704
  --studs low|full Stud detail (default low)
796
1705
  --prims full|low Other primitive detail (default full)
797
1706
  --no-weld Don't index/weld vertices
1707
+ --cond Keep conditional (type-5) lines for curved-surface outlines (pack v2, ~+8-14% zstd)
1708
+ --textures Keep printed/sticker parts' !TEXMAP geometry + UVs and embed their PNGs (pack v2; packs without
1709
+ textured parts are unchanged). Textures come from model !DATA blocks, then <ldraw>/parts/textures
798
1710
  --no-colors Don't embed the LDConfig colour table
1711
+ --no-loose-geometry Skip a model file's own type 2-5 lines (drawn in the model, not in a part). By default they become
1712
+ synthetic parts "<file>#<step>" so the model renders as the LDraw spec says
799
1713
  `;
800
1714
  var argv = process.argv.slice(2);
801
1715
  if (argv[0] !== "pack" || argv.includes("-h") || argv.includes("--help")) {
@@ -812,6 +1726,9 @@ var prims = "full";
812
1726
  var weld = true;
813
1727
  var colors = true;
814
1728
  var all = false;
1729
+ var cond = false;
1730
+ var textures = false;
1731
+ var loose = true;
815
1732
  for (let i = 0;i < a.length; i++) {
816
1733
  const k = a[i];
817
1734
  const v = () => {
@@ -836,6 +1753,12 @@ for (let i = 0;i < a.length; i++) {
836
1753
  weld = false;
837
1754
  else if (k === "--no-colors")
838
1755
  colors = false;
1756
+ else if (k === "--cond")
1757
+ cond = true;
1758
+ else if (k === "--textures")
1759
+ textures = true;
1760
+ else if (k === "--no-loose-geometry")
1761
+ loose = false;
839
1762
  else if (k === "--all-parts")
840
1763
  all = true;
841
1764
  else if (k.startsWith("-")) {
@@ -855,7 +1778,7 @@ if (!fs4.existsSync(ldraw + "/LDConfig.ldr")) {
855
1778
  process.exit(1);
856
1779
  }
857
1780
  var t0 = performance.now();
858
- var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors });
1781
+ var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors, cond, textures, looseGeometry: loose });
859
1782
  var data = r.data;
860
1783
  if (out.endsWith(".gz"))
861
1784
  data = zlib.gzipSync(data, { level: 9 });