ldr 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,263 +11,26 @@ 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
- var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
15
- var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
16
- var norm = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
17
- var threeName = (ref) => {
18
- const n = norm(ref);
19
- if (n.startsWith("s/"))
20
- return "parts/" + n;
21
- if (n.startsWith("48/"))
22
- return "p/" + n;
23
- return n;
24
- };
25
- function parseType1(line) {
26
- const t = line.trim().split(/\s+/);
27
- if (t[0] !== "1" || t.length < 15)
28
- return null;
29
- const v = t.slice(2, 14).map(Number);
30
- return { color: t[1], m: v, name: t.slice(14).join(" ") };
31
- }
32
- function xf(m, p) {
33
- return [
34
- m[0] + m[3] * p[0] + m[4] * p[1] + m[5] * p[2],
35
- m[1] + m[6] * p[0] + m[7] * p[1] + m[8] * p[2],
36
- m[2] + m[9] * p[0] + m[10] * p[1] + m[11] * p[2]
37
- ];
38
- }
39
- function compose(a, b) {
40
- const r = (i, j) => a[3 + i * 3] * b[3 + j] + a[3 + i * 3 + 1] * b[6 + j] + a[3 + i * 3 + 2] * b[9 + j];
41
- const t = xf(a, [b[0], b[1], b[2]]);
42
- return [t[0], t[1], t[2], r(0, 0), r(0, 1), r(0, 2), r(1, 0), r(1, 1), r(1, 2), r(2, 0), r(2, 1), r(2, 2)];
43
- }
44
- var IDENT = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
45
-
46
- class Library {
47
- root;
48
- index;
49
- text;
50
- stats;
51
- constructor(root) {
52
- this.root = root;
53
- this.index = new Map;
54
- this.text = new Map;
55
- this.stats = new Map;
56
- for (const d of ["parts", "p", "models"])
57
- this.#walk(path.join(root, d), d);
58
- }
59
- #walk(dir, rel) {
60
- if (!fs.existsSync(dir))
61
- return;
62
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
63
- const r = rel + "/" + e.name;
64
- if (e.isDirectory())
65
- this.#walk(path.join(dir, e.name), r);
66
- else
67
- this.index.set(r.toLowerCase(), path.join(dir, e.name));
68
- }
69
- }
70
- resolve(ref) {
71
- const n = norm(ref);
72
- for (const pre of ["parts/", "p/", "models/"])
73
- if (this.index.has(pre + n))
74
- return pre + n;
75
- return null;
76
- }
77
- read(key) {
78
- if (!this.text.has(key))
79
- this.text.set(key, fs.readFileSync(this.index.get(key), "latin1"));
80
- return this.text.get(key);
81
- }
82
- isPart(key) {
83
- return key.startsWith("parts/") && !key.startsWith("parts/s/");
84
- }
85
- fileStats(key) {
86
- if (this.stats.has(key))
87
- return this.stats.get(key);
88
- const s = { tris: 0, studTris: 0, underTris: 0, studs: [], min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity], missing: new Set };
89
- this.stats.set(key, s);
90
- const base = path.basename(key);
91
- const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
92
- const grow = (p) => {
93
- for (let i = 0;i < 3; i++) {
94
- if (p[i] < s.min[i])
95
- s.min[i] = p[i];
96
- if (p[i] > s.max[i])
97
- s.max[i] = p[i];
98
- }
99
- };
100
- for (const line of this.read(key).split(/\r?\n/)) {
101
- const t = line.trim();
102
- const c = t[0];
103
- if (c === "3" || c === "4") {
104
- const v = t.split(/\s+/).slice(2).map(Number);
105
- const nt = c === "3" ? 1 : 2;
106
- s.tris += nt;
107
- for (let i = 0;i + 2 < v.length; i += 3)
108
- grow([v[i], v[i + 1], v[i + 2]]);
109
- } else if (c === "1") {
110
- const r = parseType1(t);
111
- if (!r)
112
- continue;
113
- const ck = this.resolve(r.name);
114
- if (!ck) {
115
- s.missing.add(norm(r.name));
116
- continue;
117
- }
118
- const cs = this.fileStats(ck);
119
- s.tris += cs.tris;
120
- s.studTris += cs.studTris;
121
- s.underTris += cs.underTris;
122
- for (const m of cs.missing)
123
- s.missing.add(m);
124
- if (TOP_STUD_RE.test(path.basename(ck)))
125
- s.studs.push([r.m[0], r.m[1], r.m[2]]);
126
- else
127
- for (const p of cs.studs)
128
- s.studs.push(xf(r.m, p));
129
- if (isFinite(cs.min[0])) {
130
- for (const x of [cs.min[0], cs.max[0]])
131
- for (const y of [cs.min[1], cs.max[1]])
132
- for (const z of [cs.min[2], cs.max[2]])
133
- grow(xf(r.m, [x, y, z]));
134
- }
135
- }
136
- }
137
- if (isTop) {
138
- s.studTris = s.tris;
139
- }
140
- if (isUnder) {
141
- s.underTris = s.tris;
142
- }
143
- return s;
144
- }
145
- }
146
- function splitModel(text, fallbackName = "main.ldr") {
147
- const files = new Map;
148
- const data = new Map;
149
- let dataCur = null;
150
- let cur = null, buf = [], main = null;
151
- let skip = false, sawBlock = false;
152
- const flush = () => {
153
- if (cur)
154
- files.set(cur, buf.join(`
155
- `));
156
- cur = null;
157
- buf = [];
158
- };
159
- const firstBlock = () => {
160
- if (sawBlock)
161
- return;
162
- sawBlock = true;
163
- files.clear();
164
- cur = null;
165
- buf = [];
166
- main = null;
167
- };
168
- for (const line of text.split(/\r?\n/)) {
169
- const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
170
- if (m) {
171
- firstBlock();
172
- flush();
173
- skip = false;
174
- dataCur = null;
175
- cur = norm(m[1]);
176
- if (!main)
177
- main = cur;
178
- continue;
179
- }
180
- if (/^\s*0\s+(NOFILE|!DATA)(\s|$)/i.test(line)) {
181
- dataCur = null;
182
- if (/!DATA/i.test(line)) {
183
- firstBlock();
184
- const dn = norm(line.replace(/^\s*0\s+!DATA\s*/i, ""));
185
- dataCur = dn;
186
- data.set(dn, "");
187
- }
188
- flush();
189
- skip = true;
190
- continue;
191
- }
192
- if (skip) {
193
- const dc = dataCur;
194
- if (dc) {
195
- const d = /^\s*0\s+!:\s*(.*)$/.exec(line);
196
- if (d)
197
- data.set(dc, data.get(dc) + d[1].trim());
198
- }
199
- continue;
200
- }
201
- if (!cur) {
202
- cur = norm(fallbackName);
203
- main = cur;
204
- }
205
- buf.push(line);
206
- }
207
- flush();
208
- return { files, main, data };
209
- }
210
- function flatten(model, lib, onInstance) {
211
- const missing = new Map;
212
- const walk = (name, parentM, parentColor, depth) => {
213
- if (depth > 32)
214
- return;
215
- for (const line of model.files.get(name).split(`
216
- `)) {
217
- const r = parseType1(line);
218
- if (!r)
219
- continue;
220
- const color = r.color === "16" ? parentColor : r.color;
221
- const m = compose(parentM, r.m);
222
- const n = norm(r.name);
223
- if (model.files.has(n)) {
224
- walk(n, m, color, depth + 1);
225
- continue;
226
- }
227
- const key = lib.resolve(r.name);
228
- if (!key) {
229
- missing.set(n, (missing.get(n) || 0) + 1);
230
- continue;
231
- }
232
- onInstance({ part: key, ref: n, color, m });
233
- }
234
- };
235
- walk(model.main, IDENT, "16", 0);
236
- return { missing };
237
- }
238
- function collectDeps(lib, refs) {
239
- const out = new Map;
240
- const stack = [...refs];
241
- while (stack.length) {
242
- const ref = stack.pop();
243
- const tn = threeName(ref);
244
- if (out.has(tn))
245
- continue;
246
- const key = lib.resolve(ref);
247
- if (!key)
248
- continue;
249
- out.set(tn, key);
250
- for (const line of lib.read(key).split(/\r?\n/)) {
251
- const r = parseType1(line);
252
- if (r)
253
- stack.push(r.name);
254
- }
255
- }
256
- return out;
257
- }
258
-
259
- // src/node/compile.ts
260
- import fs2 from "node:fs";
261
- import path2 from "node:path";
262
14
 
263
15
  // src/spec.ts
264
- var norm2 = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
16
+ var norm = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
265
17
  function parseColor(tok) {
266
18
  if (/^0x/i.test(tok))
267
19
  return parseInt(tok, 16);
268
20
  return +tok;
269
21
  }
270
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
+ }
271
34
  function texNames(rest) {
272
35
  const out = [];
273
36
  let i = 0;
@@ -344,7 +107,7 @@ function geomLine(tk) {
344
107
  const color = parseColor(tk[1]);
345
108
  const nums = tk.slice(2).map(Number);
346
109
  if (t === 1)
347
- return { t: 1, color, m: nums.slice(0, 12), file: norm2(tk.slice(14).join(" ")) };
110
+ return { t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) };
348
111
  if (t === 2)
349
112
  return { t: 2, color, p: nums.slice(0, 6) };
350
113
  if (t === 3)
@@ -353,7 +116,8 @@ function geomLine(tk) {
353
116
  return { t: 4, color, p: nums.slice(0, 12) };
354
117
  return { t: 5, color, p: nums.slice(0, 12) };
355
118
  }
356
- function parse(src, name = "main.ldr") {
119
+ function parse(src, name = "main.ldr", popts = {}) {
120
+ const gtext = !!popts.geometryText;
357
121
  const files = new Map;
358
122
  let cur = null;
359
123
  let main = "";
@@ -366,17 +130,19 @@ function parse(src, name = "main.ldr") {
366
130
  return;
367
131
  const bytes = b64(dataCur.chunks.join(""));
368
132
  const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
369
- (data ??= new Map).set(norm2(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
133
+ (data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
370
134
  dataCur = null;
371
135
  };
136
+ let inHeader = true;
372
137
  const start = (n) => {
373
138
  closeData();
374
- cur = { name: norm2(n), cmds: [] };
139
+ cur = { name: norm(n), cmds: [] };
375
140
  files.set(cur.name, cur);
376
141
  if (!main)
377
142
  main = cur.name;
378
143
  texStack = [];
379
144
  texNext = null;
145
+ inHeader = true;
380
146
  };
381
147
  let skip = false, sawBlock = false;
382
148
  const firstBlock = () => {
@@ -406,6 +172,53 @@ function parse(src, name = "main.ldr") {
406
172
  const line = raw.trim();
407
173
  if (!line)
408
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
+ }
409
222
  const tk = line.split(/\s+/);
410
223
  const t = +tk[0];
411
224
  if (t === 0) {
@@ -448,6 +261,9 @@ function parse(src, name = "main.ldr") {
448
261
  c.bfc = tk.slice(2).join(" ");
449
262
  else if (tk[1] === "ROTSTEP") {
450
263
  c.meta = "ROTSTEP";
264
+ c.step = true;
265
+ texStack = [];
266
+ texNext = null;
451
267
  if ((tk[2] ?? "").toUpperCase() === "END")
452
268
  c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
453
269
  else {
@@ -460,6 +276,11 @@ function parse(src, name = "main.ldr") {
460
276
  c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
461
277
  } else if (tk[1]?.startsWith("!"))
462
278
  c.meta = tk[1];
279
+ if (inHeader && !cur.org) {
280
+ const o = orgType(text);
281
+ if (o)
282
+ cur.org = o;
283
+ }
463
284
  if (tk[1] === "!TEXMAP") {
464
285
  const op = (tk[2] ?? "").toUpperCase();
465
286
  texNext = null;
@@ -496,201 +317,746 @@ function parse(src, name = "main.ldr") {
496
317
  let target = moved[1].trim().replace(/\\/g, "/");
497
318
  if (!/\.(dat|ldr|mpd)$/i.test(target))
498
319
  target += ".dat";
499
- cur.movedTo = norm2(target);
320
+ cur.movedTo = norm(target);
500
321
  }
501
322
  cur.cmds.push(c);
502
323
  continue;
503
324
  }
504
- if (dataCur || skip)
505
- continue;
506
- if (!cur)
507
- start(name);
508
- const g = geomLine(tk);
509
- if (!g)
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
788
+ var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
789
+ var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
790
+ var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
791
+ var threeName = (ref) => {
792
+ const n = norm2(ref);
793
+ if (n.startsWith("s/"))
794
+ return "parts/" + n;
795
+ if (n.startsWith("48/"))
796
+ return "p/" + n;
797
+ return n;
798
+ };
799
+ function parseType1(line) {
800
+ const t = line.trim().split(/\s+/);
801
+ if (t[0] !== "1" || t.length < 15)
802
+ return null;
803
+ const v = t.slice(2, 14).map(Number);
804
+ return { color: t[1], m: v, name: t.slice(14).join(" ") };
805
+ }
806
+ function xf(m, p) {
807
+ return [
808
+ m[0] + m[3] * p[0] + m[4] * p[1] + m[5] * p[2],
809
+ m[1] + m[6] * p[0] + m[7] * p[1] + m[8] * p[2],
810
+ m[2] + m[9] * p[0] + m[10] * p[1] + m[11] * p[2]
811
+ ];
812
+ }
813
+ function compose(a, b) {
814
+ const r = (i, j) => a[3 + i * 3] * b[3 + j] + a[3 + i * 3 + 1] * b[6 + j] + a[3 + i * 3 + 2] * b[9 + j];
815
+ const t = xf(a, [b[0], b[1], b[2]]);
816
+ return [t[0], t[1], t[2], r(0, 0), r(0, 1), r(0, 2), r(1, 0), r(1, 1), r(1, 2), r(2, 0), r(2, 1), r(2, 2)];
817
+ }
818
+ var IDENT = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
819
+
820
+ class Library {
821
+ root;
822
+ index;
823
+ text;
824
+ stats;
825
+ constructor(root) {
826
+ this.root = root;
827
+ this.index = new Map;
828
+ this.text = new Map;
829
+ this.stats = new Map;
830
+ for (const d of ["parts", "p", "models"])
831
+ this.#walk(path.join(root, d), d);
832
+ }
833
+ #walk(dir, rel) {
834
+ if (!fs.existsSync(dir))
835
+ return;
836
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
837
+ const r = rel + "/" + e.name;
838
+ if (e.isDirectory())
839
+ this.#walk(path.join(dir, e.name), r);
840
+ else
841
+ this.index.set(r.toLowerCase(), path.join(dir, e.name));
842
+ }
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
+ }
856
+ resolve(ref) {
857
+ const n = norm2(ref);
858
+ if (this.virtual.size && this.virtual.has(n))
859
+ return "mpd/" + n;
860
+ for (const pre of ["parts/", "p/", "models/"])
861
+ if (this.index.has(pre + n))
862
+ return pre + n;
863
+ return null;
864
+ }
865
+ read(key) {
866
+ if (!this.text.has(key))
867
+ this.text.set(key, fs.readFileSync(this.index.get(key), "latin1"));
868
+ return this.text.get(key);
869
+ }
870
+ isPart(key) {
871
+ return key.startsWith("parts/") && !key.startsWith("parts/s/");
872
+ }
873
+ fileStats(key) {
874
+ if (this.stats.has(key))
875
+ return this.stats.get(key);
876
+ const s = { tris: 0, studTris: 0, underTris: 0, studs: [], min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity], missing: new Set };
877
+ this.stats.set(key, s);
878
+ const base = path.basename(key);
879
+ const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
880
+ const grow = (p) => {
881
+ for (let i = 0;i < 3; i++) {
882
+ if (p[i] < s.min[i])
883
+ s.min[i] = p[i];
884
+ if (p[i] > s.max[i])
885
+ s.max[i] = p[i];
886
+ }
887
+ };
888
+ for (const line of this.read(key).split(/\r?\n/)) {
889
+ const t = line.trim();
890
+ const c = t[0];
891
+ if (c === "3" || c === "4") {
892
+ const v = t.split(/\s+/).slice(2).map(Number);
893
+ const nt = c === "3" ? 1 : 2;
894
+ s.tris += nt;
895
+ for (let i = 0;i + 2 < v.length; i += 3)
896
+ grow([v[i], v[i + 1], v[i + 2]]);
897
+ } else if (c === "1") {
898
+ const r = parseType1(t);
899
+ if (!r)
900
+ continue;
901
+ const ck = this.resolve(r.name);
902
+ if (!ck) {
903
+ s.missing.add(norm2(r.name));
904
+ continue;
905
+ }
906
+ const cs = this.fileStats(ck);
907
+ s.tris += cs.tris;
908
+ s.studTris += cs.studTris;
909
+ s.underTris += cs.underTris;
910
+ for (const m of cs.missing)
911
+ s.missing.add(m);
912
+ if (TOP_STUD_RE.test(path.basename(ck)))
913
+ s.studs.push([r.m[0], r.m[1], r.m[2]]);
914
+ else
915
+ for (const p of cs.studs)
916
+ s.studs.push(xf(r.m, p));
917
+ if (isFinite(cs.min[0])) {
918
+ for (const x of [cs.min[0], cs.max[0]])
919
+ for (const y of [cs.min[1], cs.max[1]])
920
+ for (const z of [cs.min[2], cs.max[2]])
921
+ grow(xf(r.m, [x, y, z]));
922
+ }
923
+ }
924
+ }
925
+ if (isTop) {
926
+ s.studTris = s.tris;
927
+ }
928
+ if (isUnder) {
929
+ s.underTris = s.tris;
930
+ }
931
+ return s;
932
+ }
933
+ }
934
+ function splitModel(text, fallbackName = "main.ldr") {
935
+ const files = new Map;
936
+ const data = new Map;
937
+ let dataCur = null;
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
+ };
956
+ for (const line of text.split(/\r?\n/)) {
957
+ const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
958
+ if (m) {
959
+ firstBlock();
960
+ flush();
961
+ skip = false;
962
+ dataCur = null;
963
+ cur = norm2(m[1]);
964
+ if (!main)
965
+ main = cur;
966
+ continue;
967
+ }
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
+ }
510
987
  continue;
511
- annotate(g, false);
512
- cur.cmds.push(g);
988
+ }
989
+ if (!cur) {
990
+ cur = norm2(fallbackName);
991
+ main = cur;
992
+ }
993
+ buf.push(line);
513
994
  }
514
- closeData();
515
- return data ? { main, files, data } : { main, files };
516
- }
517
- function resolveTexture(doc, name) {
518
- if (!doc.data)
519
- return null;
520
- const n = norm2(name);
521
- return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
522
- }
523
- var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
524
- var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
525
- var cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
526
- var unit3 = (a) => {
527
- const l = Math.hypot(a[0], a[1], a[2]) || 1;
528
- return [a[0] / l, a[1] / l, a[2] / l];
529
- };
530
- var DEG = Math.PI / 180;
531
- function texmapUV(map, p) {
532
- const w = texmapTriangleUV(map, [p, p, p]);
533
- const u = w[0], v = w[1];
534
- return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
995
+ flush();
996
+ return { files, main, data };
535
997
  }
536
- var radial = (p, c, n) => {
537
- const d = sub3(p, c);
538
- const k = dot3(d, n);
539
- const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
540
- return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
541
- };
542
- function texmapTriangleUV(map, pts) {
543
- const { p1, p2, p3 } = map;
544
- const out = new Array(pts.length * 2).fill(0);
545
- if (map.method === "PLANAR") {
546
- const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
547
- const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
548
- pts.forEach((p, i) => {
549
- const d = sub3(p, p1);
550
- out[i * 2] = dot3(d, e1) / l1;
551
- out[i * 2 + 1] = dot3(d, e2) / l2;
552
- });
553
- return out;
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);
554
1003
  }
555
- const cyl = map.method === "CYLINDRICAL";
556
- const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
557
- const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
558
- const A = (map.a ?? 360) * DEG;
559
- let base = null, baseAng = 0;
560
- const sAng = (q) => {
561
- if (!base) {
562
- base = q;
563
- baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
564
- return baseAng;
1004
+ return { main: norm(model.main ?? ""), files };
1005
+ }
1006
+ function flatten2(model, lib, onInstance, opts = {}) {
1007
+ const missing = new Map;
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);
565
1022
  }
566
- return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
567
- };
568
- const miss = [];
569
- let uSum = 0;
570
- const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
571
- pts.forEach((p, i) => {
572
- if (cyl) {
573
- const q = dir ? radial(p, p1, n) : null;
574
- if (q) {
575
- const a = sAng(q);
576
- out[i * 2] = 0.5 + a / A;
577
- uSum += out[i * 2];
578
- } else
579
- miss.push(i);
580
- out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
581
- } else {
582
- const pd = unit3(sub3(p, p1));
583
- const c = dot3(pd, n);
584
- if (Math.abs(c) > 1 - 0.000000001) {
585
- miss.push(i);
586
- out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
587
- return;
588
- }
589
- const a = sAng(radial(p, p1, n));
590
- uSum += a;
591
- out[i * 2] = 0.5 + a / A;
592
- const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
593
- const r = unit3([pd[0] * ct + cr[0] * st + n[0] * k, pd[1] * ct + cr[1] * st + n[1] * k, pd[2] * ct + cr[2] * st + n[2] * k]);
594
- out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
1023
+ if (!key) {
1024
+ missing.set(i.part, (missing.get(i.part) || 0) + 1);
1025
+ continue;
595
1026
  }
596
- });
597
- if (miss.length && miss.length < pts.length) {
598
- const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
599
- for (const i of miss)
600
- out[i * 2] = avg;
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);
601
1033
  }
602
- return out;
603
- }
604
- function transformTexMap(map, m) {
605
- const x = (q) => [m[3] * q[0] + m[4] * q[1] + m[5] * q[2] + m[0], m[6] * q[0] + m[7] * q[1] + m[8] * q[2] + m[1], m[9] * q[0] + m[10] * q[1] + m[11] * q[2] + m[2]];
606
- return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
607
- }
608
- function mul(a, b) {
609
- const [ax, ay, az, a0, a1, a2, a3, a4, a5, a6, a7, a8] = a, [bx, by, bz, b0, b1, b2, b3, b4, b5, b6, b7, b8] = b;
610
- return [
611
- a0 * bx + a1 * by + a2 * bz + ax,
612
- a3 * bx + a4 * by + a5 * bz + ay,
613
- a6 * bx + a7 * by + a8 * bz + az,
614
- a0 * b0 + a1 * b3 + a2 * b6,
615
- a0 * b1 + a1 * b4 + a2 * b7,
616
- a0 * b2 + a1 * b5 + a2 * b8,
617
- a3 * b0 + a4 * b3 + a5 * b6,
618
- a3 * b1 + a4 * b4 + a5 * b7,
619
- a3 * b2 + a4 * b5 + a5 * b8,
620
- a6 * b0 + a7 * b3 + a8 * b6,
621
- a6 * b1 + a7 * b4 + a8 * b7,
622
- a6 * b2 + a7 * b5 + a8 * b8
623
- ];
1034
+ return { missing };
624
1035
  }
625
- var I = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
626
- var isDirectColor = (c) => c > 16777215;
627
- function smoothNormals(pos, edges, cond, creaseDeg = 35) {
628
- const q = (i, a) => `${a[i].toFixed(2)},${a[i + 1].toFixed(2)},${a[i + 2].toFixed(2)}`;
629
- const ek = (a, b) => a < b ? a + "|" + b : b + "|" + a;
630
- const hard = new Set, soft = new Set;
631
- for (let i = 0;i < edges.length; i += 6)
632
- hard.add(ek(q(i, edges), q(i + 3, edges)));
633
- const stride = cond.length > 0 && cond.length % 12 === 0 ? 12 : 6;
634
- for (let i = 0;i < cond.length; i += stride)
635
- soft.add(ek(q(i, cond), q(i + 3, cond)));
636
- const nT = pos.length / 9, fn = [], vk = [];
637
- for (let t = 0;t < nT; t++) {
638
- const a = t * 9;
639
- const u = [pos[a + 3] - pos[a], pos[a + 4] - pos[a + 1], pos[a + 5] - pos[a + 2]], v = [pos[a + 6] - pos[a], pos[a + 7] - pos[a + 1], pos[a + 8] - pos[a + 2]];
640
- const n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
641
- const l = Math.hypot(...n) || 1;
642
- fn.push(n.map((x) => x / l));
643
- for (let k = 0;k < 3; k++)
644
- vk.push(q(a + k * 3, pos));
645
- }
646
- const byV = new Map;
647
- vk.forEach((k, i) => (byV.get(k) ?? byV.set(k, []).get(k)).push(i));
648
- const edgeFaces = new Map;
649
- for (let t = 0;t < nT; t++)
650
- for (let k = 0;k < 3; k++) {
651
- const e = ek(vk[t * 3 + k], vk[t * 3 + (k + 1) % 3]);
652
- (edgeFaces.get(e) ?? edgeFaces.set(e, []).get(e)).push(t);
1036
+ function collectDeps(lib, refs) {
1037
+ const out = new Map;
1038
+ const stack = [...refs];
1039
+ while (stack.length) {
1040
+ const ref = stack.pop();
1041
+ const tn = threeName(ref);
1042
+ if (out.has(tn))
1043
+ continue;
1044
+ const key = lib.resolve(ref);
1045
+ if (!key)
1046
+ continue;
1047
+ out.set(tn, key);
1048
+ for (const line of lib.read(key).split(/\r?\n/)) {
1049
+ const r = parseType1(line);
1050
+ if (r)
1051
+ stack.push(r.name);
653
1052
  }
654
- const cosC = Math.cos(creaseDeg * Math.PI / 180), out = new Float32Array(pos.length);
655
- const joinable = (e, a, b) => {
656
- if (hard.has(e))
657
- return false;
658
- const d = fn[a][0] * fn[b][0] + fn[a][1] * fn[b][1] + fn[a][2] * fn[b][2];
659
- return soft.has(e) ? d > -0.2 : d > cosC;
660
- };
661
- for (const [key, corners] of byV) {
662
- const faces = corners.map((c) => c / 3 | 0), par = faces.map((_, i) => i);
663
- const f = (i) => par[i] === i ? i : par[i] = f(par[i]);
664
- for (let i = 0;i < faces.length; i++)
665
- for (let j = i + 1;j < faces.length; j++) {
666
- const A = faces[i], B = faces[j];
667
- const va = [0, 1, 2].map((k) => vk[A * 3 + k]), vb = new Set([0, 1, 2].map((k) => vk[B * 3 + k]));
668
- for (const o of va)
669
- if (o !== key && vb.has(o)) {
670
- const e = ek(key, o);
671
- if ((edgeFaces.get(e)?.length ?? 0) >= 2 && joinable(e, A, B))
672
- par[f(i)] = f(j);
673
- }
674
- }
675
- const acc = new Map;
676
- faces.forEach((F, i) => {
677
- const r = f(i), s = acc.get(r) ?? [0, 0, 0];
678
- s[0] += fn[F][0];
679
- s[1] += fn[F][1];
680
- s[2] += fn[F][2];
681
- acc.set(r, s);
682
- });
683
- corners.forEach((c, i) => {
684
- const s = acc.get(f(i)), l = Math.hypot(...s) || 1;
685
- out[c * 3] = s[0] / l;
686
- out[c * 3 + 1] = s[1] / l;
687
- out[c * 3 + 2] = s[2] / l;
688
- });
689
1053
  }
690
1054
  return out;
691
1055
  }
692
1056
 
693
1057
  // src/node/compile.ts
1058
+ import fs2 from "node:fs";
1059
+ import path2 from "node:path";
694
1060
  function loadColors(root) {
695
1061
  const map = new Map;
696
1062
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -868,7 +1234,7 @@ class Compiler {
868
1234
  return r;
869
1235
  }
870
1236
  texture(name) {
871
- const n = norm(name);
1237
+ const n = norm2(name);
872
1238
  for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
873
1239
  const k = pre + n;
874
1240
  if (this.lib.index.has(k))
@@ -1261,57 +1627,6 @@ function encodePack(parts, studKinds, weld, extra, options = {}) {
1261
1627
  return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
1262
1628
  }
1263
1629
 
1264
- // src/model.ts
1265
- function flatten2(doc, opts = {}) {
1266
- const out = [];
1267
- const maxDepth = opts.maxDepth ?? 64;
1268
- const walk = (d, fname, m, color, path, stepPath, depth) => {
1269
- const f = d.files.get(fname);
1270
- if (!f || depth > maxDepth)
1271
- return;
1272
- let s = 0;
1273
- for (const c of f.cmds) {
1274
- if (c.t === 0 && c.step)
1275
- s++;
1276
- if (c.t !== 1)
1277
- continue;
1278
- const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
1279
- if (d.files.has(c.file)) {
1280
- walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
1281
- continue;
1282
- }
1283
- const ext = opts.resolveModel?.(c.file);
1284
- if (ext) {
1285
- walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
1286
- continue;
1287
- }
1288
- out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
1289
- }
1290
- };
1291
- walk(doc, doc.main, I, 16, doc.main, [], 0);
1292
- return out;
1293
- }
1294
- function parseModel(text, name = "main.ldr", opts) {
1295
- return flatten2(parse(text, name), opts);
1296
- }
1297
- function sequenceSteps(insts) {
1298
- const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
1299
- const uniq = [...new Set(insts.map(key))].sort();
1300
- const rank = new Map(uniq.map((k, j) => [k, j]));
1301
- const stepOf = new Int32Array(insts.length);
1302
- insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
1303
- return { count: uniq.length, stepOf };
1304
- }
1305
- function parseLDConfig(text) {
1306
- const out = new Map;
1307
- for (const l of text.split(/\r?\n/)) {
1308
- 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);
1309
- if (m)
1310
- out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
1311
- }
1312
- return out;
1313
- }
1314
-
1315
1630
  // src/node/build-pack.ts
1316
1631
  function buildPack(o) {
1317
1632
  const lib = new Library(o.ldrawRoot);
@@ -1324,16 +1639,22 @@ function buildPack(o) {
1324
1639
  for (const [k, v] of sm.data)
1325
1640
  if (!data.has(k))
1326
1641
  data.set(k, v);
1327
- const r = flatten(sm, lib, (i) => refs.set(i.ref, i.part));
1328
- for (const k of r.missing.keys())
1329
- missing.add(k);
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
+ }
1330
1651
  }
1331
1652
  for (const p of o.parts ?? []) {
1332
1653
  const k = lib.resolve(p);
1333
1654
  if (k)
1334
- refs.set(norm(p), k);
1655
+ refs.set(norm2(p), k);
1335
1656
  else
1336
- missing.add(norm(p));
1657
+ missing.add(norm2(p));
1337
1658
  }
1338
1659
  if (o.allParts) {
1339
1660
  for (const k of lib.index.keys())
@@ -1362,10 +1683,11 @@ function buildPack(o) {
1362
1683
  extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
1363
1684
  }
1364
1685
  const texture = (n) => {
1365
- const k = norm(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
1686
+ const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
1366
1687
  return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
1367
1688
  };
1368
- return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
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 } : {} };
1369
1691
  }
1370
1692
 
1371
1693
  // src/cli.ts
@@ -1386,6 +1708,8 @@ Options:
1386
1708
  --textures Keep printed/sticker parts' !TEXMAP geometry + UVs and embed their PNGs (pack v2; packs without
1387
1709
  textured parts are unchanged). Textures come from model !DATA blocks, then <ldraw>/parts/textures
1388
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
1389
1713
  `;
1390
1714
  var argv = process.argv.slice(2);
1391
1715
  if (argv[0] !== "pack" || argv.includes("-h") || argv.includes("--help")) {
@@ -1404,6 +1728,7 @@ var colors = true;
1404
1728
  var all = false;
1405
1729
  var cond = false;
1406
1730
  var textures = false;
1731
+ var loose = true;
1407
1732
  for (let i = 0;i < a.length; i++) {
1408
1733
  const k = a[i];
1409
1734
  const v = () => {
@@ -1432,6 +1757,8 @@ for (let i = 0;i < a.length; i++) {
1432
1757
  cond = true;
1433
1758
  else if (k === "--textures")
1434
1759
  textures = true;
1760
+ else if (k === "--no-loose-geometry")
1761
+ loose = false;
1435
1762
  else if (k === "--all-parts")
1436
1763
  all = true;
1437
1764
  else if (k.startsWith("-")) {
@@ -1451,7 +1778,7 @@ if (!fs4.existsSync(ldraw + "/LDConfig.ldr")) {
1451
1778
  process.exit(1);
1452
1779
  }
1453
1780
  var t0 = performance.now();
1454
- var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors, cond, textures });
1781
+ var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors, cond, textures, looseGeometry: loose });
1455
1782
  var data = r.data;
1456
1783
  if (out.endsWith(".gz"))
1457
1784
  data = zlib.gzipSync(data, { level: 9 });