ldr 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cli.js +784 -188
- package/dist/index.d.ts +3 -3
- package/dist/index.js +554 -32
- package/dist/node/build-pack.d.ts +6 -0
- package/dist/node/compile.d.ts +29 -2
- package/dist/node/index.js +969 -71
- package/dist/node/library.d.ts +9 -2
- package/dist/node/resolver.d.ts +1 -0
- package/dist/render.d.ts +26 -0
- package/dist/scene.d.ts +18 -0
- package/dist/spec.d.ts +83 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -145,31 +145,67 @@ class Library {
|
|
|
145
145
|
}
|
|
146
146
|
function splitModel(text, fallbackName = "main.ldr") {
|
|
147
147
|
const files = new Map;
|
|
148
|
+
const data = new Map;
|
|
149
|
+
let dataCur = null;
|
|
148
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
|
+
};
|
|
149
168
|
for (const line of text.split(/\r?\n/)) {
|
|
150
169
|
const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
|
|
151
170
|
if (m) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
171
|
+
firstBlock();
|
|
172
|
+
flush();
|
|
173
|
+
skip = false;
|
|
174
|
+
dataCur = null;
|
|
155
175
|
cur = norm(m[1]);
|
|
156
|
-
buf = [];
|
|
157
176
|
if (!main)
|
|
158
177
|
main = cur;
|
|
159
178
|
continue;
|
|
160
179
|
}
|
|
161
|
-
if (/^\s*0\s+NOFILE/i.test(line))
|
|
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;
|
|
162
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
|
+
}
|
|
163
201
|
if (!cur) {
|
|
164
202
|
cur = norm(fallbackName);
|
|
165
203
|
main = cur;
|
|
166
204
|
}
|
|
167
205
|
buf.push(line);
|
|
168
206
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`));
|
|
172
|
-
return { files, main };
|
|
207
|
+
flush();
|
|
208
|
+
return { files, main, data };
|
|
173
209
|
}
|
|
174
210
|
function flatten(model, lib, onInstance) {
|
|
175
211
|
const missing = new Map;
|
|
@@ -223,6 +259,438 @@ function collectDeps(lib, refs) {
|
|
|
223
259
|
// src/node/compile.ts
|
|
224
260
|
import fs2 from "node:fs";
|
|
225
261
|
import path2 from "node:path";
|
|
262
|
+
|
|
263
|
+
// src/spec.ts
|
|
264
|
+
var norm2 = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
265
|
+
function parseColor(tok) {
|
|
266
|
+
if (/^0x/i.test(tok))
|
|
267
|
+
return parseInt(tok, 16);
|
|
268
|
+
return +tok;
|
|
269
|
+
}
|
|
270
|
+
var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
|
|
271
|
+
function texNames(rest) {
|
|
272
|
+
const out = [];
|
|
273
|
+
let i = 0;
|
|
274
|
+
while (i < rest.length) {
|
|
275
|
+
while (rest[i] === " " || rest[i] === "\t")
|
|
276
|
+
i++;
|
|
277
|
+
if (i >= rest.length)
|
|
278
|
+
break;
|
|
279
|
+
if (rest[i] === '"') {
|
|
280
|
+
let v = "";
|
|
281
|
+
i++;
|
|
282
|
+
while (i < rest.length && rest[i] !== '"') {
|
|
283
|
+
if (rest[i] === "\\" && i + 1 < rest.length)
|
|
284
|
+
i++;
|
|
285
|
+
v += rest[i++];
|
|
286
|
+
}
|
|
287
|
+
i++;
|
|
288
|
+
out.push(v);
|
|
289
|
+
} else {
|
|
290
|
+
let j = i;
|
|
291
|
+
while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
|
|
292
|
+
j++;
|
|
293
|
+
out.push(rest.slice(i, j));
|
|
294
|
+
i = j;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
function parseTexMap(tail, next = false) {
|
|
300
|
+
const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
|
|
301
|
+
if (!m)
|
|
302
|
+
return null;
|
|
303
|
+
const method = m[1].toUpperCase();
|
|
304
|
+
const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
|
|
305
|
+
if (!n)
|
|
306
|
+
return null;
|
|
307
|
+
const parts = m[2].trim().split(/\s+/);
|
|
308
|
+
if (parts.length < n + 1)
|
|
309
|
+
return null;
|
|
310
|
+
const v = parts.slice(0, n).map(Number);
|
|
311
|
+
if (!v.every(Number.isFinite))
|
|
312
|
+
return null;
|
|
313
|
+
let rest = m[2].trim();
|
|
314
|
+
for (let k = 0;k < n; k++)
|
|
315
|
+
rest = rest.replace(/^\S+\s*/, "");
|
|
316
|
+
const names = texNames(rest);
|
|
317
|
+
if (!names.length)
|
|
318
|
+
return null;
|
|
319
|
+
const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
|
|
320
|
+
if (n >= 10)
|
|
321
|
+
t.a = v[9];
|
|
322
|
+
if (n === 11)
|
|
323
|
+
t.b = v[10];
|
|
324
|
+
const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
|
|
325
|
+
if (g > 0 && names[g + 1])
|
|
326
|
+
t.glossmap = names[g + 1];
|
|
327
|
+
if (next)
|
|
328
|
+
t.next = true;
|
|
329
|
+
return t;
|
|
330
|
+
}
|
|
331
|
+
var b64 = (s) => {
|
|
332
|
+
const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
|
|
333
|
+
const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
|
|
334
|
+
const bin = atob(pad);
|
|
335
|
+
const out = new Uint8Array(bin.length);
|
|
336
|
+
for (let i = 0;i < bin.length; i++)
|
|
337
|
+
out[i] = bin.charCodeAt(i);
|
|
338
|
+
return out;
|
|
339
|
+
};
|
|
340
|
+
function geomLine(tk) {
|
|
341
|
+
const t = +tk[0];
|
|
342
|
+
if (!(t >= 1 && t <= 5) || tk.length < 2)
|
|
343
|
+
return null;
|
|
344
|
+
const color = parseColor(tk[1]);
|
|
345
|
+
const nums = tk.slice(2).map(Number);
|
|
346
|
+
if (t === 1)
|
|
347
|
+
return { t: 1, color, m: nums.slice(0, 12), file: norm2(tk.slice(14).join(" ")) };
|
|
348
|
+
if (t === 2)
|
|
349
|
+
return { t: 2, color, p: nums.slice(0, 6) };
|
|
350
|
+
if (t === 3)
|
|
351
|
+
return { t: 3, color, p: nums.slice(0, 9) };
|
|
352
|
+
if (t === 4)
|
|
353
|
+
return { t: 4, color, p: nums.slice(0, 12) };
|
|
354
|
+
return { t: 5, color, p: nums.slice(0, 12) };
|
|
355
|
+
}
|
|
356
|
+
function parse(src, name = "main.ldr") {
|
|
357
|
+
const files = new Map;
|
|
358
|
+
let cur = null;
|
|
359
|
+
let main = "";
|
|
360
|
+
let data;
|
|
361
|
+
let dataCur = null;
|
|
362
|
+
let texStack = [];
|
|
363
|
+
let texNext = null;
|
|
364
|
+
const closeData = () => {
|
|
365
|
+
if (!dataCur)
|
|
366
|
+
return;
|
|
367
|
+
const bytes = b64(dataCur.chunks.join(""));
|
|
368
|
+
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" } : {} });
|
|
370
|
+
dataCur = null;
|
|
371
|
+
};
|
|
372
|
+
const start = (n) => {
|
|
373
|
+
closeData();
|
|
374
|
+
cur = { name: norm2(n), cmds: [] };
|
|
375
|
+
files.set(cur.name, cur);
|
|
376
|
+
if (!main)
|
|
377
|
+
main = cur.name;
|
|
378
|
+
texStack = [];
|
|
379
|
+
texNext = null;
|
|
380
|
+
};
|
|
381
|
+
let skip = false, sawBlock = false;
|
|
382
|
+
const firstBlock = () => {
|
|
383
|
+
if (sawBlock)
|
|
384
|
+
return;
|
|
385
|
+
sawBlock = true;
|
|
386
|
+
files.clear();
|
|
387
|
+
cur = null;
|
|
388
|
+
main = "";
|
|
389
|
+
};
|
|
390
|
+
const annotate = (c, fromColon) => {
|
|
391
|
+
if (texNext) {
|
|
392
|
+
c.tex = texNext;
|
|
393
|
+
texNext = null;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const top = texStack[texStack.length - 1];
|
|
397
|
+
if (!top)
|
|
398
|
+
return;
|
|
399
|
+
if (top.fallback) {
|
|
400
|
+
if (!fromColon)
|
|
401
|
+
c.fallback = true;
|
|
402
|
+
} else
|
|
403
|
+
c.tex = top.map;
|
|
404
|
+
};
|
|
405
|
+
for (const raw of src.split(/\r?\n/)) {
|
|
406
|
+
const line = raw.trim();
|
|
407
|
+
if (!line)
|
|
408
|
+
continue;
|
|
409
|
+
const tk = line.split(/\s+/);
|
|
410
|
+
const t = +tk[0];
|
|
411
|
+
if (t === 0) {
|
|
412
|
+
const text = line.slice(1).trim();
|
|
413
|
+
if (tk[1] === "FILE") {
|
|
414
|
+
firstBlock();
|
|
415
|
+
skip = false;
|
|
416
|
+
start(text.slice(5));
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (tk[1] === "NOFILE") {
|
|
420
|
+
closeData();
|
|
421
|
+
cur = null;
|
|
422
|
+
skip = true;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (tk[1] === "!DATA") {
|
|
426
|
+
firstBlock();
|
|
427
|
+
closeData();
|
|
428
|
+
cur = null;
|
|
429
|
+
skip = false;
|
|
430
|
+
dataCur = { name: text.slice(5).trim(), chunks: [] };
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (skip)
|
|
434
|
+
continue;
|
|
435
|
+
if (dataCur) {
|
|
436
|
+
if (tk[1] === "!:")
|
|
437
|
+
dataCur.chunks.push(tk.slice(2).join(""));
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (!cur)
|
|
441
|
+
start(name);
|
|
442
|
+
const c = { t: 0, text };
|
|
443
|
+
if (tk[1] === "STEP") {
|
|
444
|
+
c.step = true;
|
|
445
|
+
texStack = [];
|
|
446
|
+
texNext = null;
|
|
447
|
+
} else if (tk[1] === "BFC")
|
|
448
|
+
c.bfc = tk.slice(2).join(" ");
|
|
449
|
+
else if (tk[1] === "ROTSTEP") {
|
|
450
|
+
c.meta = "ROTSTEP";
|
|
451
|
+
if ((tk[2] ?? "").toUpperCase() === "END")
|
|
452
|
+
c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
|
|
453
|
+
else {
|
|
454
|
+
const mode = (tk[5] ?? "REL").toUpperCase();
|
|
455
|
+
c.rotstep = { x: +tk[2] || 0, y: +tk[3] || 0, z: +tk[4] || 0, mode: ["REL", "ABS", "ADD"].includes(mode) ? mode : "REL" };
|
|
456
|
+
}
|
|
457
|
+
} else if (tk[1] === "BUFEXCHG") {
|
|
458
|
+
c.meta = "BUFEXCHG";
|
|
459
|
+
const op = (tk[2] ?? "").toUpperCase();
|
|
460
|
+
c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
|
|
461
|
+
} else if (tk[1]?.startsWith("!"))
|
|
462
|
+
c.meta = tk[1];
|
|
463
|
+
if (tk[1] === "!TEXMAP") {
|
|
464
|
+
const op = (tk[2] ?? "").toUpperCase();
|
|
465
|
+
texNext = null;
|
|
466
|
+
if (op === "START" || op === "NEXT") {
|
|
467
|
+
const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
|
|
468
|
+
c.texmap = { op, ...map ? { map } : {} };
|
|
469
|
+
if (map) {
|
|
470
|
+
(cur.texmaps ??= []).push(map);
|
|
471
|
+
if (op === "START")
|
|
472
|
+
texStack.push({ map, fallback: false });
|
|
473
|
+
else
|
|
474
|
+
texNext = map;
|
|
475
|
+
}
|
|
476
|
+
} else if (op === "FALLBACK") {
|
|
477
|
+
c.texmap = { op };
|
|
478
|
+
const top = texStack[texStack.length - 1];
|
|
479
|
+
if (top)
|
|
480
|
+
top.fallback = true;
|
|
481
|
+
} else if (op === "END") {
|
|
482
|
+
c.texmap = { op };
|
|
483
|
+
texStack.pop();
|
|
484
|
+
}
|
|
485
|
+
} else if (tk[1] === "!:") {
|
|
486
|
+
texNext = null;
|
|
487
|
+
const g = geomLine(tk.slice(2));
|
|
488
|
+
if (g) {
|
|
489
|
+
annotate(g, true);
|
|
490
|
+
c.geom = g;
|
|
491
|
+
}
|
|
492
|
+
} else
|
|
493
|
+
texNext = null;
|
|
494
|
+
const moved = text.match(MOVED_RE);
|
|
495
|
+
if (moved && !cur.movedTo) {
|
|
496
|
+
let target = moved[1].trim().replace(/\\/g, "/");
|
|
497
|
+
if (!/\.(dat|ldr|mpd)$/i.test(target))
|
|
498
|
+
target += ".dat";
|
|
499
|
+
cur.movedTo = norm2(target);
|
|
500
|
+
}
|
|
501
|
+
cur.cmds.push(c);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (dataCur || skip)
|
|
505
|
+
continue;
|
|
506
|
+
if (!cur)
|
|
507
|
+
start(name);
|
|
508
|
+
const g = geomLine(tk);
|
|
509
|
+
if (!g)
|
|
510
|
+
continue;
|
|
511
|
+
annotate(g, false);
|
|
512
|
+
cur.cmds.push(g);
|
|
513
|
+
}
|
|
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 };
|
|
535
|
+
}
|
|
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;
|
|
554
|
+
}
|
|
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;
|
|
565
|
+
}
|
|
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;
|
|
595
|
+
}
|
|
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;
|
|
601
|
+
}
|
|
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
|
+
];
|
|
624
|
+
}
|
|
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);
|
|
653
|
+
}
|
|
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
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/node/compile.ts
|
|
226
694
|
function loadColors(root) {
|
|
227
695
|
const map = new Map;
|
|
228
696
|
for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
|
|
@@ -270,7 +738,7 @@ class Compiler {
|
|
|
270
738
|
const ck = key + (separateStuds ? "" : "#nostudsep");
|
|
271
739
|
if (this.cache.has(ck))
|
|
272
740
|
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 };
|
|
741
|
+
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
742
|
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
743
|
const walk = (fk, M, color, invert, target, depth) => {
|
|
276
744
|
if (depth > 40)
|
|
@@ -325,6 +793,13 @@ class Compiler {
|
|
|
325
793
|
}
|
|
326
794
|
if (c === "5") {
|
|
327
795
|
out.condLines++;
|
|
796
|
+
const dst = target === "under" ? out.condUnder : out.cond;
|
|
797
|
+
if (dst) {
|
|
798
|
+
const v = t.split(/\s+/).slice(2, 14).map(Number);
|
|
799
|
+
if (v.length === 12 && v.every(Number.isFinite))
|
|
800
|
+
for (let i = 0;i < 12; i += 3)
|
|
801
|
+
dst.push(...xf(M, v.slice(i, i + 3)));
|
|
802
|
+
}
|
|
328
803
|
continue;
|
|
329
804
|
}
|
|
330
805
|
if (c === "3" || c === "4") {
|
|
@@ -351,6 +826,8 @@ class Compiler {
|
|
|
351
826
|
out.uncertified = true;
|
|
352
827
|
};
|
|
353
828
|
walk(key, IDENT, "16", false, "body", 0);
|
|
829
|
+
if (this.opts.tex && this.hasTex(key))
|
|
830
|
+
this.texPass(key, out, separateStuds, colorOf);
|
|
354
831
|
const [x0, y0, z0, x1, y1, z1] = out.bbox;
|
|
355
832
|
let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
|
|
356
833
|
for (const b of [out.body, out.under])
|
|
@@ -367,6 +844,169 @@ class Compiler {
|
|
|
367
844
|
this.cache.set(ck, out);
|
|
368
845
|
return out;
|
|
369
846
|
}
|
|
847
|
+
texMemo = new Map;
|
|
848
|
+
hasTex(key) {
|
|
849
|
+
const m = this.texMemo.get(key);
|
|
850
|
+
if (m !== undefined)
|
|
851
|
+
return m;
|
|
852
|
+
this.texMemo.set(key, false);
|
|
853
|
+
const txt = this.lib.read(key);
|
|
854
|
+
let r = /^\s*0\s+!TEXMAP/im.test(txt);
|
|
855
|
+
if (!r)
|
|
856
|
+
for (const l of txt.split(/\r?\n/)) {
|
|
857
|
+
const t = l.trim();
|
|
858
|
+
const p = t.startsWith("0 !:") ? parseType1(t.slice(4)) : t[0] === "1" ? parseType1(t) : null;
|
|
859
|
+
if (p) {
|
|
860
|
+
const k = this.lib.resolve(p.name);
|
|
861
|
+
if (k && this.hasTex(k)) {
|
|
862
|
+
r = true;
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
this.texMemo.set(key, r);
|
|
868
|
+
return r;
|
|
869
|
+
}
|
|
870
|
+
texture(name) {
|
|
871
|
+
const n = norm(name);
|
|
872
|
+
for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
|
|
873
|
+
const k = pre + n;
|
|
874
|
+
if (this.lib.index.has(k))
|
|
875
|
+
return new Uint8Array(fs2.readFileSync(this.lib.index.get(k)));
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
texPass(key, out, separateStuds, colorOf) {
|
|
880
|
+
const tbody = { pos: [], col: [] }, texb = new Map;
|
|
881
|
+
const walk = (fk, M, color, invert, target, depth, texIn) => {
|
|
882
|
+
if (depth > 40)
|
|
883
|
+
return;
|
|
884
|
+
let ccw = true, invertNext = false;
|
|
885
|
+
let stack = [];
|
|
886
|
+
let next = null;
|
|
887
|
+
for (const raw of this.lib.read(fk).split(/\r?\n/)) {
|
|
888
|
+
let t = raw.trim();
|
|
889
|
+
if (!t)
|
|
890
|
+
continue;
|
|
891
|
+
let colon = false;
|
|
892
|
+
if (t[0] === "0") {
|
|
893
|
+
const m = /^0\s+(\S+)\s*(.*)$/.exec(t);
|
|
894
|
+
const kw = (m?.[1] ?? "").toUpperCase(), rest = m?.[2] ?? "";
|
|
895
|
+
if (kw === "!TEXMAP") {
|
|
896
|
+
const op = (rest.split(/\s+/)[0] || "").toUpperCase();
|
|
897
|
+
next = null;
|
|
898
|
+
if (op === "START" || op === "NEXT") {
|
|
899
|
+
const map = parseTexMap(rest.replace(/^\S+/, ""), op === "NEXT");
|
|
900
|
+
if (map) {
|
|
901
|
+
const pm = transformTexMap(map, M);
|
|
902
|
+
if (op === "START")
|
|
903
|
+
stack.push({ map: pm, fallback: false });
|
|
904
|
+
else
|
|
905
|
+
next = pm;
|
|
906
|
+
}
|
|
907
|
+
} else if (op === "FALLBACK") {
|
|
908
|
+
const top = stack[stack.length - 1];
|
|
909
|
+
if (top)
|
|
910
|
+
top.fallback = true;
|
|
911
|
+
} else if (op === "END")
|
|
912
|
+
stack.pop();
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (kw === "!:") {
|
|
916
|
+
next = null;
|
|
917
|
+
t = rest.trim();
|
|
918
|
+
colon = true;
|
|
919
|
+
if (!t || t[0] === "0")
|
|
920
|
+
continue;
|
|
921
|
+
} else {
|
|
922
|
+
if (kw === "BFC") {
|
|
923
|
+
if (/\bCW\b/.test(t))
|
|
924
|
+
ccw = false;
|
|
925
|
+
if (/\bCCW\b/.test(t))
|
|
926
|
+
ccw = true;
|
|
927
|
+
if (/INVERTNEXT/.test(t))
|
|
928
|
+
invertNext = true;
|
|
929
|
+
}
|
|
930
|
+
if (kw === "STEP")
|
|
931
|
+
stack = [];
|
|
932
|
+
next = null;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
let tex = texIn;
|
|
937
|
+
if (next) {
|
|
938
|
+
tex = next;
|
|
939
|
+
next = null;
|
|
940
|
+
} else {
|
|
941
|
+
const top = stack[stack.length - 1];
|
|
942
|
+
if (top) {
|
|
943
|
+
if (top.fallback) {
|
|
944
|
+
if (!colon)
|
|
945
|
+
continue;
|
|
946
|
+
} else
|
|
947
|
+
tex = top.map;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const c = t[0];
|
|
951
|
+
if (c === "1") {
|
|
952
|
+
const r = parseType1(t);
|
|
953
|
+
if (!r)
|
|
954
|
+
continue;
|
|
955
|
+
const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
|
|
956
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
957
|
+
const ck2 = this.resolve(r.name, isTop);
|
|
958
|
+
if (!ck2) {
|
|
959
|
+
invertNext = false;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const inv = invert !== invertNext !== det3(r.m) < 0;
|
|
963
|
+
invertNext = false;
|
|
964
|
+
if (isTop && separateStuds)
|
|
965
|
+
continue;
|
|
966
|
+
walk(ck2, compose(M, r.m), r.color === "16" ? color : r.color, inv, isUnder ? "under" : target, depth + 1, tex);
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (c !== "3" && c !== "4")
|
|
970
|
+
continue;
|
|
971
|
+
const tok = t.split(/\s+/);
|
|
972
|
+
const code = tok[1] === "16" ? color : tok[1];
|
|
973
|
+
const n = c === "3" ? 3 : 4;
|
|
974
|
+
const v = tok.slice(2, 2 + n * 3).map(Number);
|
|
975
|
+
const pts = [];
|
|
976
|
+
for (let i = 0;i < n; i++)
|
|
977
|
+
pts.push(xf(M, v.slice(i * 3, i * 3 + 3)));
|
|
978
|
+
let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
|
|
979
|
+
if (!ccw !== invert)
|
|
980
|
+
order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
|
|
981
|
+
const rgba = colorOf(code);
|
|
982
|
+
if (tex) {
|
|
983
|
+
let b = texb.get(tex.texture);
|
|
984
|
+
if (!b)
|
|
985
|
+
texb.set(tex.texture, b = { pos: [], col: [], uv: [] });
|
|
986
|
+
if (Math.abs(tex.a ?? 0) >= 360)
|
|
987
|
+
b.wrapU = true;
|
|
988
|
+
for (let k = 0;k < order.length; k += 3) {
|
|
989
|
+
const tri = [pts[order[k]], pts[order[k + 1]], pts[order[k + 2]]];
|
|
990
|
+
const w = texmapTriangleUV(tex, tri);
|
|
991
|
+
for (let j = 0;j < 3; j++) {
|
|
992
|
+
b.pos.push(...tri[j]);
|
|
993
|
+
b.col.push(...rgba);
|
|
994
|
+
b.uv.push(w[j * 2], w[j * 2 + 1]);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
} else if (target === "body")
|
|
998
|
+
for (const i of order) {
|
|
999
|
+
tbody.pos.push(...pts[i]);
|
|
1000
|
+
tbody.col.push(...rgba);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
walk(key, IDENT, "16", false, "body", 0, null);
|
|
1005
|
+
if (texb.size) {
|
|
1006
|
+
out.tbody = tbody;
|
|
1007
|
+
out.texb = texb;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
370
1010
|
}
|
|
371
1011
|
function enclosureMask(c) {
|
|
372
1012
|
const [x0, , z0, x1, y1, z1] = c.bbox;
|
|
@@ -449,10 +1089,10 @@ function growBox(b, M, cb) {
|
|
|
449
1089
|
for (const z of [cb[2], cb[5]])
|
|
450
1090
|
grow(b, xf(M, [x, y, z]));
|
|
451
1091
|
}
|
|
452
|
-
function writePack(file, parts, studKinds, weld, extra) {
|
|
453
|
-
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
|
|
1092
|
+
function writePack(file, parts, studKinds, weld, extra, options) {
|
|
1093
|
+
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
|
|
454
1094
|
}
|
|
455
|
-
function encodePack(parts, studKinds, weld, extra) {
|
|
1095
|
+
function encodePack(parts, studKinds, weld, extra, options = {}) {
|
|
456
1096
|
const chunks = [];
|
|
457
1097
|
let off = 0;
|
|
458
1098
|
const put = (a) => {
|
|
@@ -461,13 +1101,15 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
461
1101
|
chunks.push(new Uint8Array(pad));
|
|
462
1102
|
off += pad;
|
|
463
1103
|
}
|
|
464
|
-
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
|
|
1104
|
+
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
|
|
465
1105
|
chunks.push(a);
|
|
466
1106
|
off += a.byteLength;
|
|
467
1107
|
return r;
|
|
468
1108
|
};
|
|
1109
|
+
let lastPos = [];
|
|
469
1110
|
const bucket = (b) => {
|
|
470
1111
|
const n = b.pos.length / 3;
|
|
1112
|
+
lastPos = [];
|
|
471
1113
|
if (!n)
|
|
472
1114
|
return null;
|
|
473
1115
|
const nrm = new Float32Array(n * 3);
|
|
@@ -486,8 +1128,10 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
486
1128
|
nrm[(i + k) * 3 + 2] = nz;
|
|
487
1129
|
}
|
|
488
1130
|
}
|
|
489
|
-
if (!weld)
|
|
1131
|
+
if (!weld) {
|
|
1132
|
+
lastPos = b.pos;
|
|
490
1133
|
return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
|
|
1134
|
+
}
|
|
491
1135
|
const map = new Map;
|
|
492
1136
|
const P = [], N = [], C = [], I = [];
|
|
493
1137
|
for (let i = 0;i < n; i++) {
|
|
@@ -503,184 +1147,118 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
503
1147
|
I.push(j);
|
|
504
1148
|
}
|
|
505
1149
|
const vcount = P.length / 3;
|
|
1150
|
+
lastPos = P;
|
|
506
1151
|
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
1152
|
};
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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;
|
|
1153
|
+
let anyCond = false;
|
|
1154
|
+
const condl = (a, pools) => {
|
|
1155
|
+
if (!a || !a.length)
|
|
1156
|
+
return;
|
|
1157
|
+
anyCond = true;
|
|
1158
|
+
const key = (p, i) => Math.round(p[i] * 1000) + "," + Math.round(p[i + 1] * 1000) + "," + Math.round(p[i + 2] * 1000);
|
|
1159
|
+
const need = new Map;
|
|
1160
|
+
for (let i = 0;i < a.length; i += 3)
|
|
1161
|
+
need.set(key(a, i), -1);
|
|
1162
|
+
let base = 0;
|
|
1163
|
+
for (const p of pools) {
|
|
1164
|
+
for (let i = 0;i < p.length; i += 3) {
|
|
1165
|
+
const k = key(p, i);
|
|
1166
|
+
if (need.get(k) === -1)
|
|
1167
|
+
need.set(k, base + i / 3);
|
|
552
1168
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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);
|
|
1169
|
+
base += p.length / 3;
|
|
1170
|
+
}
|
|
1171
|
+
const X = [], I = new Array(a.length / 3);
|
|
1172
|
+
for (let i = 0;i < a.length; i += 3) {
|
|
1173
|
+
const k = key(a, i);
|
|
1174
|
+
let j = need.get(k);
|
|
1175
|
+
if (j === -1) {
|
|
1176
|
+
j = base + X.length / 3;
|
|
1177
|
+
need.set(k, j);
|
|
1178
|
+
X.push(a[i], a[i + 1], a[i + 2]);
|
|
580
1179
|
}
|
|
581
|
-
|
|
582
|
-
continue;
|
|
1180
|
+
I[i / 3] = j;
|
|
583
1181
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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);
|
|
1182
|
+
let prev = 0, max = 0;
|
|
1183
|
+
for (let i = 0;i < I.length; i++) {
|
|
1184
|
+
const d = I[i] - prev;
|
|
1185
|
+
prev = I[i];
|
|
1186
|
+
const z = d >= 0 ? d * 2 : -d * 2 - 1;
|
|
1187
|
+
I[i] = z;
|
|
1188
|
+
if (z > max)
|
|
1189
|
+
max = z;
|
|
646
1190
|
}
|
|
647
|
-
|
|
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;
|
|
1191
|
+
return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
|
|
653
1192
|
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1193
|
+
const texNames = [];
|
|
1194
|
+
const texBucket = (b) => {
|
|
1195
|
+
const n = b.pos.length / 3;
|
|
1196
|
+
if (!n)
|
|
1197
|
+
return null;
|
|
1198
|
+
const nrm = new Float32Array(n * 3);
|
|
1199
|
+
for (let i = 0;i < n; i += 3) {
|
|
1200
|
+
const p = b.pos;
|
|
1201
|
+
const a = i * 3;
|
|
1202
|
+
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];
|
|
1203
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
1204
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
1205
|
+
nx /= l;
|
|
1206
|
+
ny /= l;
|
|
1207
|
+
nz /= l;
|
|
1208
|
+
for (let k = 0;k < 3; k++) {
|
|
1209
|
+
nrm[(i + k) * 3] = nx;
|
|
1210
|
+
nrm[(i + k) * 3 + 1] = ny;
|
|
1211
|
+
nrm[(i + k) * 3 + 2] = nz;
|
|
667
1212
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1213
|
+
}
|
|
1214
|
+
const map = new Map;
|
|
1215
|
+
const P = [], N = [], C = [], U = [], I = [];
|
|
1216
|
+
for (let i = 0;i < n; i++) {
|
|
1217
|
+
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);
|
|
1218
|
+
let j = map.get(k);
|
|
1219
|
+
if (j === undefined) {
|
|
1220
|
+
j = P.length / 3;
|
|
1221
|
+
map.set(k, j);
|
|
1222
|
+
P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
|
|
1223
|
+
N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
|
|
1224
|
+
C.push(...b.col.slice(i * 4, i * 4 + 4));
|
|
1225
|
+
U.push(b.uv[i * 2], b.uv[i * 2 + 1]);
|
|
1226
|
+
}
|
|
1227
|
+
I.push(j);
|
|
1228
|
+
}
|
|
1229
|
+
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 };
|
|
1230
|
+
};
|
|
1231
|
+
const encTex = (c) => {
|
|
1232
|
+
if (!c.texb)
|
|
1233
|
+
return {};
|
|
1234
|
+
const tex = [...c.texb].map(([name, b]) => {
|
|
1235
|
+
let t = texNames.indexOf(name);
|
|
1236
|
+
if (t < 0)
|
|
1237
|
+
t = texNames.push(name) - 1;
|
|
1238
|
+
return { t, ...b.wrapU ? { wrapU: true } : {}, ...texBucket(b) };
|
|
681
1239
|
});
|
|
682
|
-
|
|
683
|
-
|
|
1240
|
+
return { tbody: bucket(c.tbody), tex };
|
|
1241
|
+
};
|
|
1242
|
+
const enc = (c) => {
|
|
1243
|
+
const body = bucket(c.body), bodyP = lastPos, under = bucket(c.under), underP = lastPos;
|
|
1244
|
+
const edges = c.edges.length ? put(new Float32Array(c.edges)) : null;
|
|
1245
|
+
const condlB = condl(c.cond, [bodyP, underP, c.edges]), condlU = condl(c.condUnder, [bodyP, underP, c.edges]);
|
|
1246
|
+
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) };
|
|
1247
|
+
};
|
|
1248
|
+
const encParts = Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), encKinds = Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)]));
|
|
1249
|
+
const textures = texNames.map((name) => {
|
|
1250
|
+
const b = options.texture?.(name);
|
|
1251
|
+
return b ? { name, mime: "image/png", data: put(b) } : { name, missing: true };
|
|
1252
|
+
});
|
|
1253
|
+
const features = [...anyCond ? ["cond"] : [], ...texNames.length ? ["tex"] : []];
|
|
1254
|
+
const header = { version: features.length ? 2 : 1, ...features.length ? { features } : {}, weld, parts: encParts, studKinds: encKinds, ...texNames.length ? { textures } : {}, ...extra };
|
|
1255
|
+
const hj = new TextEncoder().encode(JSON.stringify(header));
|
|
1256
|
+
const hpad = (4 - (4 + hj.length) % 4) % 4;
|
|
1257
|
+
const head = new Uint8Array(4 + hj.length + hpad);
|
|
1258
|
+
new DataView(head.buffer).setUint32(0, hj.length + hpad, true);
|
|
1259
|
+
head.set(hj, 4);
|
|
1260
|
+
head.fill(32, 4 + hj.length);
|
|
1261
|
+
return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
|
|
684
1262
|
}
|
|
685
1263
|
|
|
686
1264
|
// src/model.ts
|
|
@@ -739,9 +1317,14 @@ function buildPack(o) {
|
|
|
739
1317
|
const lib = new Library(o.ldrawRoot);
|
|
740
1318
|
const refs = new Map;
|
|
741
1319
|
const missing = new Set;
|
|
1320
|
+
const data = new Map;
|
|
742
1321
|
for (const m of o.models ?? []) {
|
|
743
1322
|
const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
|
|
744
|
-
const
|
|
1323
|
+
const sm = splitModel(text, name);
|
|
1324
|
+
for (const [k, v] of sm.data)
|
|
1325
|
+
if (!data.has(k))
|
|
1326
|
+
data.set(k, v);
|
|
1327
|
+
const r = flatten(sm, lib, (i) => refs.set(i.ref, i.part));
|
|
745
1328
|
for (const k of r.missing.keys())
|
|
746
1329
|
missing.add(k);
|
|
747
1330
|
}
|
|
@@ -757,7 +1340,7 @@ function buildPack(o) {
|
|
|
757
1340
|
if (lib.isPart(k) && /\.dat$/.test(k))
|
|
758
1341
|
refs.set(k.slice(6), k);
|
|
759
1342
|
}
|
|
760
|
-
const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
|
|
1343
|
+
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
1344
|
const parts = new Map, kinds = new Map;
|
|
762
1345
|
let n = 0;
|
|
763
1346
|
for (const [ref, key] of refs) {
|
|
@@ -778,7 +1361,11 @@ function buildPack(o) {
|
|
|
778
1361
|
if (fs3.existsSync(cfg))
|
|
779
1362
|
extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
|
|
780
1363
|
}
|
|
781
|
-
|
|
1364
|
+
const texture = (n) => {
|
|
1365
|
+
const k = norm(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
|
|
1366
|
+
return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
|
|
1367
|
+
};
|
|
1368
|
+
return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
|
|
782
1369
|
}
|
|
783
1370
|
|
|
784
1371
|
// src/cli.ts
|
|
@@ -795,6 +1382,9 @@ Options:
|
|
|
795
1382
|
--studs low|full Stud detail (default low)
|
|
796
1383
|
--prims full|low Other primitive detail (default full)
|
|
797
1384
|
--no-weld Don't index/weld vertices
|
|
1385
|
+
--cond Keep conditional (type-5) lines for curved-surface outlines (pack v2, ~+8-14% zstd)
|
|
1386
|
+
--textures Keep printed/sticker parts' !TEXMAP geometry + UVs and embed their PNGs (pack v2; packs without
|
|
1387
|
+
textured parts are unchanged). Textures come from model !DATA blocks, then <ldraw>/parts/textures
|
|
798
1388
|
--no-colors Don't embed the LDConfig colour table
|
|
799
1389
|
`;
|
|
800
1390
|
var argv = process.argv.slice(2);
|
|
@@ -812,6 +1402,8 @@ var prims = "full";
|
|
|
812
1402
|
var weld = true;
|
|
813
1403
|
var colors = true;
|
|
814
1404
|
var all = false;
|
|
1405
|
+
var cond = false;
|
|
1406
|
+
var textures = false;
|
|
815
1407
|
for (let i = 0;i < a.length; i++) {
|
|
816
1408
|
const k = a[i];
|
|
817
1409
|
const v = () => {
|
|
@@ -836,6 +1428,10 @@ for (let i = 0;i < a.length; i++) {
|
|
|
836
1428
|
weld = false;
|
|
837
1429
|
else if (k === "--no-colors")
|
|
838
1430
|
colors = false;
|
|
1431
|
+
else if (k === "--cond")
|
|
1432
|
+
cond = true;
|
|
1433
|
+
else if (k === "--textures")
|
|
1434
|
+
textures = true;
|
|
839
1435
|
else if (k === "--all-parts")
|
|
840
1436
|
all = true;
|
|
841
1437
|
else if (k.startsWith("-")) {
|
|
@@ -855,7 +1451,7 @@ if (!fs4.existsSync(ldraw + "/LDConfig.ldr")) {
|
|
|
855
1451
|
process.exit(1);
|
|
856
1452
|
}
|
|
857
1453
|
var t0 = performance.now();
|
|
858
|
-
var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors });
|
|
1454
|
+
var r = buildPack({ ldrawRoot: ldraw, models, parts, allParts: all, studs, primitives: prims, weld, colors, cond, textures });
|
|
859
1455
|
var data = r.data;
|
|
860
1456
|
if (out.endsWith(".gz"))
|
|
861
1457
|
data = zlib.gzipSync(data, { level: 9 });
|