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/README.md +157 -73
- package/dist/cli.js +817 -490
- package/dist/index.d.ts +2 -2
- package/dist/index.js +323 -12
- package/dist/model.d.ts +30 -3
- package/dist/node/build-pack.d.ts +5 -0
- package/dist/node/index.js +435 -66
- package/dist/node/library.d.ts +10 -2
- package/dist/node/resolver.d.ts +28 -1
- package/dist/render.d.ts +1 -1
- package/dist/scene.d.ts +4 -0
- package/dist/spec.d.ts +83 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { parse, parseColor, isDirectColor, mul, I as IDENTITY, norm, smoothNormals, parseTexMap, texmapUV, texmapTriangleUV, transformTexMap, resolveTexture } from "./spec.js";
|
|
2
|
-
export type { M as Matrix12, Doc, LFile, Cmd, RotStep, BufExchg, TexMap, TexMapCmd, TexMapMethod, TexAnno, DataBlock, PartMesh } from "./spec.js";
|
|
1
|
+
export { parse, parseColor, isDirectColor, mul, I as IDENTITY, norm, smoothNormals, parseTexMap, texmapUV, texmapTriangleUV, transformTexMap, resolveTexture, fileKind, looseSteps, looseSource, looseFile, loosePartName, texWrapper, invert, hash32, colorToken, rotStepMatrix, stepViewMatrix, fileStepViews } from "./spec.js";
|
|
2
|
+
export type { M as Matrix12, Doc, LFile, Cmd, RotStep, BufExchg, TexMap, TexMapCmd, TexMapMethod, TexAnno, DataBlock, PartMesh, FileKind, StepView } from "./spec.js";
|
|
3
3
|
export { flatten, parseModel, sequenceSteps, parseLDConfig } from "./model.js";
|
|
4
4
|
export type { PartInstance, FlattenOptions, LDrawColor } from "./model.js";
|
|
5
5
|
export { loadPack, decodePack, buildLdrScene, makeMaterial, createInstancedEdges, computeCulling, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible } from "./render.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,17 @@ function parseColor(tok) {
|
|
|
6
6
|
return +tok;
|
|
7
7
|
}
|
|
8
8
|
var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
|
|
9
|
+
var SPECIAL0 = new Set(["FILE", "NOFILE", "!DATA", "!:", "STEP", "BFC", "ROTSTEP", "BUFEXCHG", "!TEXMAP"]);
|
|
10
|
+
var PART_ORG = new Set(["part", "subpart", "primitive", "8_primitive", "48_primitive", "shortcut", "helper"]);
|
|
11
|
+
var MODEL_ORG = new Set(["model", "submodel", "configuration"]);
|
|
12
|
+
var ORG_RE = /^(?:!?LDRAW_ORG|Unofficial|Official\s+LCAD)\s+(\S+)/i;
|
|
13
|
+
function orgType(text) {
|
|
14
|
+
const m = ORG_RE.exec(text);
|
|
15
|
+
if (!m)
|
|
16
|
+
return;
|
|
17
|
+
const t = m[1].toLowerCase().replace(/^unofficial_/, "");
|
|
18
|
+
return PART_ORG.has(t) || MODEL_ORG.has(t) ? t : undefined;
|
|
19
|
+
}
|
|
9
20
|
function texNames(rest) {
|
|
10
21
|
const out = [];
|
|
11
22
|
let i = 0;
|
|
@@ -91,7 +102,8 @@ function geomLine(tk) {
|
|
|
91
102
|
return { t: 4, color, p: nums.slice(0, 12) };
|
|
92
103
|
return { t: 5, color, p: nums.slice(0, 12) };
|
|
93
104
|
}
|
|
94
|
-
function parse(src, name = "main.ldr") {
|
|
105
|
+
function parse(src, name = "main.ldr", popts = {}) {
|
|
106
|
+
const gtext = !!popts.geometryText;
|
|
95
107
|
const files = new Map;
|
|
96
108
|
let cur = null;
|
|
97
109
|
let main = "";
|
|
@@ -107,6 +119,7 @@ function parse(src, name = "main.ldr") {
|
|
|
107
119
|
(data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
|
|
108
120
|
dataCur = null;
|
|
109
121
|
};
|
|
122
|
+
let inHeader = true;
|
|
110
123
|
const start = (n) => {
|
|
111
124
|
closeData();
|
|
112
125
|
cur = { name: norm(n), cmds: [] };
|
|
@@ -115,6 +128,7 @@ function parse(src, name = "main.ldr") {
|
|
|
115
128
|
main = cur.name;
|
|
116
129
|
texStack = [];
|
|
117
130
|
texNext = null;
|
|
131
|
+
inHeader = true;
|
|
118
132
|
};
|
|
119
133
|
let skip = false, sawBlock = false;
|
|
120
134
|
const firstBlock = () => {
|
|
@@ -144,6 +158,53 @@ function parse(src, name = "main.ldr") {
|
|
|
144
158
|
const line = raw.trim();
|
|
145
159
|
if (!line)
|
|
146
160
|
continue;
|
|
161
|
+
if (gtext && line.charCodeAt(0) >= 50 && line.charCodeAt(0) <= 53 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
|
|
162
|
+
if (dataCur || skip)
|
|
163
|
+
continue;
|
|
164
|
+
if (!cur)
|
|
165
|
+
start(name);
|
|
166
|
+
const sp = line.slice(2).trimStart();
|
|
167
|
+
const e = sp.search(/\s/);
|
|
168
|
+
const g = { t: line.charCodeAt(0) - 48, color: parseColor(e < 0 ? sp : sp.slice(0, e)), p: [], src: line };
|
|
169
|
+
annotate(g, false);
|
|
170
|
+
cur.cmds.push(g);
|
|
171
|
+
inHeader = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (line.charCodeAt(0) === 48 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
|
|
175
|
+
let a = 2;
|
|
176
|
+
while (line.charCodeAt(a) === 32 || line.charCodeAt(a) === 9)
|
|
177
|
+
a++;
|
|
178
|
+
let b = a;
|
|
179
|
+
while (b < line.length && line.charCodeAt(b) !== 32 && line.charCodeAt(b) !== 9)
|
|
180
|
+
b++;
|
|
181
|
+
const kw = line.slice(a, b);
|
|
182
|
+
if (!SPECIAL0.has(kw)) {
|
|
183
|
+
if (skip || dataCur)
|
|
184
|
+
continue;
|
|
185
|
+
if (!cur)
|
|
186
|
+
start(name);
|
|
187
|
+
const text = line.slice(a);
|
|
188
|
+
const c = { t: 0, text };
|
|
189
|
+
if (kw.charCodeAt(0) === 33)
|
|
190
|
+
c.meta = kw;
|
|
191
|
+
texNext = null;
|
|
192
|
+
if (inHeader && !cur.org) {
|
|
193
|
+
const o = orgType(text);
|
|
194
|
+
if (o)
|
|
195
|
+
cur.org = o;
|
|
196
|
+
}
|
|
197
|
+
const moved = text.match(MOVED_RE);
|
|
198
|
+
if (moved && !cur.movedTo) {
|
|
199
|
+
let target = moved[1].trim().replace(/\\/g, "/");
|
|
200
|
+
if (!/\.(dat|ldr|mpd)$/i.test(target))
|
|
201
|
+
target += ".dat";
|
|
202
|
+
cur.movedTo = norm(target);
|
|
203
|
+
}
|
|
204
|
+
cur.cmds.push(c);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
147
208
|
const tk = line.split(/\s+/);
|
|
148
209
|
const t = +tk[0];
|
|
149
210
|
if (t === 0) {
|
|
@@ -186,6 +247,9 @@ function parse(src, name = "main.ldr") {
|
|
|
186
247
|
c.bfc = tk.slice(2).join(" ");
|
|
187
248
|
else if (tk[1] === "ROTSTEP") {
|
|
188
249
|
c.meta = "ROTSTEP";
|
|
250
|
+
c.step = true;
|
|
251
|
+
texStack = [];
|
|
252
|
+
texNext = null;
|
|
189
253
|
if ((tk[2] ?? "").toUpperCase() === "END")
|
|
190
254
|
c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
|
|
191
255
|
else {
|
|
@@ -198,6 +262,11 @@ function parse(src, name = "main.ldr") {
|
|
|
198
262
|
c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
|
|
199
263
|
} else if (tk[1]?.startsWith("!"))
|
|
200
264
|
c.meta = tk[1];
|
|
265
|
+
if (inHeader && !cur.org) {
|
|
266
|
+
const o = orgType(text);
|
|
267
|
+
if (o)
|
|
268
|
+
cur.org = o;
|
|
269
|
+
}
|
|
201
270
|
if (tk[1] === "!TEXMAP") {
|
|
202
271
|
const op = (tk[2] ?? "").toUpperCase();
|
|
203
272
|
texNext = null;
|
|
@@ -248,6 +317,7 @@ function parse(src, name = "main.ldr") {
|
|
|
248
317
|
continue;
|
|
249
318
|
annotate(g, false);
|
|
250
319
|
cur.cmds.push(g);
|
|
320
|
+
inHeader = false;
|
|
251
321
|
}
|
|
252
322
|
closeData();
|
|
253
323
|
return data ? { main, files, data } : { main, files };
|
|
@@ -427,34 +497,249 @@ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
|
|
|
427
497
|
}
|
|
428
498
|
return out;
|
|
429
499
|
}
|
|
500
|
+
var kindMemo = new WeakMap;
|
|
501
|
+
var SUBREF_RE = /^(s|8|48)\//;
|
|
502
|
+
function fileKind(doc, name) {
|
|
503
|
+
const f = doc.files.get(norm(name));
|
|
504
|
+
if (!f)
|
|
505
|
+
return "part";
|
|
506
|
+
let k = kindMemo.get(f);
|
|
507
|
+
if (k)
|
|
508
|
+
return k;
|
|
509
|
+
if (f.org)
|
|
510
|
+
k = PART_ORG.has(f.org) ? "part" : "model";
|
|
511
|
+
else if (f.name === doc.main)
|
|
512
|
+
k = "model";
|
|
513
|
+
else
|
|
514
|
+
k = f.cmds.some((c) => c.t >= 2 || c.t === 0 && !!c.geom || c.t === 1 && SUBREF_RE.test(c.file)) ? "part" : "model";
|
|
515
|
+
kindMemo.set(f, k);
|
|
516
|
+
return k;
|
|
517
|
+
}
|
|
518
|
+
var looseMemo = new WeakMap;
|
|
519
|
+
function looseSteps(f) {
|
|
520
|
+
let r = looseMemo.get(f);
|
|
521
|
+
if (r)
|
|
522
|
+
return r;
|
|
523
|
+
r = [];
|
|
524
|
+
let s = 0;
|
|
525
|
+
for (const c of f.cmds) {
|
|
526
|
+
if (c.t === 0 && c.step) {
|
|
527
|
+
s++;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if ((c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) && r[r.length - 1] !== s)
|
|
531
|
+
r.push(s);
|
|
532
|
+
}
|
|
533
|
+
looseMemo.set(f, r);
|
|
534
|
+
return r;
|
|
535
|
+
}
|
|
536
|
+
var colorToken = (c) => c > 16777215 ? "0x" + c.toString(16).toUpperCase() : String(c);
|
|
537
|
+
var lineOf = (c) => c.t === 0 ? "0 " + c.text : c.t === 1 ? `1 ${colorToken(c.color)} ${c.m.join(" ")} ${c.file}` : c.src ? c.src : `${c.t} ${colorToken(c.color)} ${c.p.join(" ")}`;
|
|
538
|
+
var loosePartName = (file, step) => `${norm(file)}#${step}`;
|
|
539
|
+
function looseFile(f, step) {
|
|
540
|
+
const out = [];
|
|
541
|
+
let s = 0;
|
|
542
|
+
const isNext = (c) => !!c && c.t === 0 && c.texmap?.op === "NEXT";
|
|
543
|
+
for (const c of f.cmds) {
|
|
544
|
+
if (c.t === 0) {
|
|
545
|
+
if (c.step) {
|
|
546
|
+
s++;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (c.bfc !== undefined) {
|
|
550
|
+
out.push(c);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (s !== step)
|
|
554
|
+
continue;
|
|
555
|
+
if (c.texmap)
|
|
556
|
+
out.push(c);
|
|
557
|
+
else if (c.geom) {
|
|
558
|
+
if (c.geom.t >= 2)
|
|
559
|
+
out.push(c);
|
|
560
|
+
else if (isNext(out[out.length - 1]))
|
|
561
|
+
out.pop();
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (s !== step)
|
|
566
|
+
continue;
|
|
567
|
+
if (c.t === 1) {
|
|
568
|
+
if (isNext(out[out.length - 1]))
|
|
569
|
+
out.pop();
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
out.push(c);
|
|
573
|
+
}
|
|
574
|
+
return { name: loosePartName(f.name, step), cmds: out };
|
|
575
|
+
}
|
|
576
|
+
function looseSource(f, step) {
|
|
577
|
+
return [`0 ldr: own geometry of ${f.name}, step ${step}`, ...looseFile(f, step).cmds.map(lineOf)].join(`
|
|
578
|
+
`) + `
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
581
|
+
function hash32(s) {
|
|
582
|
+
let h = 2166136261;
|
|
583
|
+
for (let i = 0;i < s.length; i++) {
|
|
584
|
+
h ^= s.charCodeAt(i);
|
|
585
|
+
h = Math.imul(h, 16777619);
|
|
586
|
+
}
|
|
587
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
588
|
+
}
|
|
589
|
+
function invert(m) {
|
|
590
|
+
const [x, y, z, a, b, c, d, e, f, g, h, i] = m;
|
|
591
|
+
const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
|
|
592
|
+
const det = a * A + b * B + c * C;
|
|
593
|
+
if (!Number.isFinite(det) || Math.abs(det) < 0.000000000001)
|
|
594
|
+
return null;
|
|
595
|
+
const k = 1 / det;
|
|
596
|
+
const r = [A * k, -(b * i - c * h) * k, (b * f - c * e) * k, B * k, (a * i - c * g) * k, -(a * f - c * d) * k, C * k, -(a * h - b * g) * k, (a * e - b * d) * k];
|
|
597
|
+
return [-(r[0] * x + r[1] * y + r[2] * z), -(r[3] * x + r[4] * y + r[5] * z), -(r[6] * x + r[7] * y + r[8] * z), ...r];
|
|
598
|
+
}
|
|
599
|
+
var num = (x) => String(+x.toFixed(6) || 0);
|
|
600
|
+
var quote = (s) => '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"';
|
|
601
|
+
function texWrapper(part, map) {
|
|
602
|
+
const v = [...map.p1, ...map.p2, ...map.p3, ...map.method !== "PLANAR" ? [map.a ?? 360] : [], ...map.method === "SPHERICAL" ? [map.b ?? 180] : []].map(num).join(" ");
|
|
603
|
+
const source = `0 ldr: ${part} with a model-level !TEXMAP
|
|
604
|
+
0 BFC CERTIFY CCW
|
|
605
|
+
0 !TEXMAP START ${map.method} ${v} ${quote(map.texture)}${map.glossmap ? " GLOSSMAP " + quote(map.glossmap) : ""}
|
|
606
|
+
1 16 0 0 0 1 0 0 0 1 0 0 0 1 ${norm(part)}
|
|
607
|
+
0 !TEXMAP END
|
|
608
|
+
`;
|
|
609
|
+
return { name: `${norm(part)}#tex-${hash32(source)}`, source };
|
|
610
|
+
}
|
|
611
|
+
var mul3 = (a, b) => {
|
|
612
|
+
const r = new Array(9);
|
|
613
|
+
for (let i = 0;i < 3; i++)
|
|
614
|
+
for (let j = 0;j < 3; j++)
|
|
615
|
+
r[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
|
|
616
|
+
return r;
|
|
617
|
+
};
|
|
618
|
+
function rotStepMatrix(x, y, z) {
|
|
619
|
+
const D = Math.PI / 180, s1 = Math.sin(x * D), c1 = Math.cos(x * D), s2 = Math.sin(y * D), c2 = Math.cos(y * D), s3 = Math.sin(z * D), c3 = Math.cos(z * D);
|
|
620
|
+
return [c2 * c3, -c2 * s3, s2, c1 * s3 + s1 * s2 * c3, c1 * c3 - s1 * s2 * s3, -s1 * c2, s1 * s3 - c1 * s2 * c3, s1 * c3 + c1 * s2 * s3, c1 * c2];
|
|
621
|
+
}
|
|
622
|
+
function stepViewMatrix(view, defaultView = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {
|
|
623
|
+
return !view ? [...defaultView] : view.abs ? [...view.m] : mul3(defaultView, view.m);
|
|
624
|
+
}
|
|
625
|
+
var viewMemo = new WeakMap;
|
|
626
|
+
function fileStepViews(f) {
|
|
627
|
+
const hit = viewMemo.get(f);
|
|
628
|
+
if (hit)
|
|
629
|
+
return hit;
|
|
630
|
+
const out = [];
|
|
631
|
+
let cur = null;
|
|
632
|
+
for (const c of f.cmds) {
|
|
633
|
+
if (c.t !== 0 || !c.step)
|
|
634
|
+
continue;
|
|
635
|
+
const r = c.rotstep;
|
|
636
|
+
if (r) {
|
|
637
|
+
if (r.mode === "END")
|
|
638
|
+
cur = null;
|
|
639
|
+
else {
|
|
640
|
+
const R = rotStepMatrix(r.x, r.y, r.z);
|
|
641
|
+
cur = r.mode === "ABS" ? { abs: true, m: R, rotstep: r } : r.mode === "ADD" && cur ? { abs: cur.abs, m: mul3(cur.m, R), rotstep: r } : { abs: false, m: R, rotstep: r };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
out.push(cur);
|
|
645
|
+
}
|
|
646
|
+
out.push(cur);
|
|
647
|
+
viewMemo.set(f, out);
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
430
650
|
// src/model.ts
|
|
431
651
|
function flatten(doc, opts = {}) {
|
|
432
652
|
const out = [];
|
|
433
653
|
const maxDepth = opts.maxDepth ?? 64;
|
|
434
|
-
const
|
|
654
|
+
const loose = opts.looseGeometry !== false, tx = !!opts.textures;
|
|
655
|
+
const src = new Map;
|
|
656
|
+
const lazy = (f, k) => {
|
|
657
|
+
const key = f.name + "#" + k;
|
|
658
|
+
let fn = src.get(key);
|
|
659
|
+
if (!fn) {
|
|
660
|
+
let s;
|
|
661
|
+
fn = Object.assign(() => s ??= looseSource(f, k), { file: f, step: k });
|
|
662
|
+
src.set(key, fn);
|
|
663
|
+
}
|
|
664
|
+
return fn;
|
|
665
|
+
};
|
|
666
|
+
const walk = (d, fname, m, color, path, stepPath, depth, texIn) => {
|
|
435
667
|
const f = d.files.get(fname);
|
|
436
668
|
if (!f || depth > maxDepth)
|
|
437
669
|
return;
|
|
438
670
|
let s = 0;
|
|
439
|
-
|
|
440
|
-
|
|
671
|
+
const views = fileStepViews(f);
|
|
672
|
+
const ls = loose ? looseSteps(f) : [];
|
|
673
|
+
let li = 0;
|
|
674
|
+
const emit = (part, col, mm, sp, k, embedded, tex) => {
|
|
675
|
+
const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
|
|
676
|
+
if (embedded)
|
|
677
|
+
i.embedded = true;
|
|
678
|
+
if (tex) {
|
|
679
|
+
const w = texWrapper(part, tex);
|
|
680
|
+
i.texPart = w.name;
|
|
681
|
+
i.texSource = w.source;
|
|
682
|
+
}
|
|
683
|
+
const v = views[k];
|
|
684
|
+
if (v)
|
|
685
|
+
i.view = v;
|
|
686
|
+
out.push(i);
|
|
687
|
+
return i;
|
|
688
|
+
};
|
|
689
|
+
const flushLoose = (upTo) => {
|
|
690
|
+
while (li < ls.length && ls[li] <= upTo) {
|
|
691
|
+
const k = ls[li++];
|
|
692
|
+
emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null).source = lazy(f, k);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
for (const c0 of f.cmds) {
|
|
696
|
+
if (c0.t === 0 && c0.step) {
|
|
441
697
|
s++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
let c = c0;
|
|
701
|
+
if (c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) {
|
|
702
|
+
flushLoose(s);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (c.t === 0) {
|
|
706
|
+
if (tx && c.geom?.t === 1)
|
|
707
|
+
c = c.geom;
|
|
708
|
+
else
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
442
711
|
if (c.t !== 1)
|
|
443
712
|
continue;
|
|
713
|
+
if (tx && c.fallback)
|
|
714
|
+
continue;
|
|
444
715
|
const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
|
|
716
|
+
const tex = tx ? c.tex ?? texIn : null;
|
|
717
|
+
const local = tex ? invert(c.m) : null;
|
|
718
|
+
const lt = tex && local ? transformTexMap(tex, local) : null;
|
|
445
719
|
if (d.files.has(c.file)) {
|
|
446
|
-
|
|
720
|
+
if (fileKind(d, c.file) === "model") {
|
|
721
|
+
walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1, lt);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
emit(c.file, col, mm, sp, s, true, lt);
|
|
447
725
|
continue;
|
|
448
726
|
}
|
|
449
727
|
const ext = opts.resolveModel?.(c.file);
|
|
450
728
|
if (ext) {
|
|
451
|
-
walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
|
|
729
|
+
walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1, lt);
|
|
452
730
|
continue;
|
|
453
731
|
}
|
|
454
|
-
|
|
732
|
+
emit(c.file, col, mm, sp, s, false, lt);
|
|
455
733
|
}
|
|
734
|
+
flushLoose(Infinity);
|
|
456
735
|
};
|
|
457
|
-
|
|
736
|
+
if (fileKind(doc, doc.main) === "part") {
|
|
737
|
+
const f = doc.files.get(doc.main);
|
|
738
|
+
if (f)
|
|
739
|
+
out.push({ part: doc.main, color: 16, m: [...I], step: 0, steps: [0], localStep: 0, path: doc.main, embedded: true });
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
walk(doc, doc.main, I, 16, doc.main, [], 0, null);
|
|
458
743
|
return out;
|
|
459
744
|
}
|
|
460
745
|
function parseModel(text, name = "main.ldr", opts) {
|
|
@@ -465,8 +750,14 @@ function sequenceSteps(insts) {
|
|
|
465
750
|
const uniq = [...new Set(insts.map(key))].sort();
|
|
466
751
|
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
467
752
|
const stepOf = new Int32Array(insts.length);
|
|
468
|
-
|
|
469
|
-
|
|
753
|
+
const views = new Array(uniq.length).fill(null);
|
|
754
|
+
insts.forEach((i, k) => {
|
|
755
|
+
const r = rank.get(key(i));
|
|
756
|
+
stepOf[k] = r;
|
|
757
|
+
if (i.view)
|
|
758
|
+
views[r] = i.view;
|
|
759
|
+
});
|
|
760
|
+
return { count: uniq.length, stepOf, views };
|
|
470
761
|
}
|
|
471
762
|
function parseLDConfig(text) {
|
|
472
763
|
const out = new Map;
|
|
@@ -830,7 +1121,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
830
1121
|
mark("culling");
|
|
831
1122
|
const edgeObjs = [];
|
|
832
1123
|
const condObjs = [];
|
|
833
|
-
const wantCond = P.cond !== false && P.edges === "idx";
|
|
1124
|
+
const wantCond = P.cond !== false && (P.edges === "idx" || P.edges === true);
|
|
834
1125
|
let condMat = null;
|
|
835
1126
|
const cmat = () => condMat ??= condLineMat();
|
|
836
1127
|
const condCache = new Map;
|
|
@@ -1098,11 +1389,18 @@ function toPlastic(old) {
|
|
|
1098
1389
|
function createLdrObject(instances, pack, options = {}) {
|
|
1099
1390
|
const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
|
|
1100
1391
|
const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
|
|
1101
|
-
const
|
|
1392
|
+
const useTex = o.textures !== false;
|
|
1393
|
+
const pick = (i) => useTex && i.texPart && pack.header.parts[i.texPart] ? i.texPart : i.part;
|
|
1394
|
+
const all = instances.map((i) => ({ part: pick(i), color: String(i.color), m: i.m, steps: i.steps }));
|
|
1102
1395
|
const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
|
|
1103
1396
|
const uniq = [...new Set(all.map(key))].sort();
|
|
1104
1397
|
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
1105
1398
|
const stepOf = all.map((i) => rank.get(key(i)));
|
|
1399
|
+
const stepViews = new Array(uniq.length).fill(null);
|
|
1400
|
+
instances.forEach((i, k) => {
|
|
1401
|
+
if (i.view)
|
|
1402
|
+
stepViews[stepOf[k]] = i.view;
|
|
1403
|
+
});
|
|
1106
1404
|
const missing = [...new Set(all.filter((i) => !pack.header.parts[i.part]).map((i) => i.part))];
|
|
1107
1405
|
const object = new THREE2.Group;
|
|
1108
1406
|
object.name = "ldr";
|
|
@@ -1160,6 +1458,7 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
1160
1458
|
object,
|
|
1161
1459
|
stats,
|
|
1162
1460
|
stepCount: uniq.length,
|
|
1461
|
+
stepViews,
|
|
1163
1462
|
setStep: (s) => build(s),
|
|
1164
1463
|
setEdges: (v) => {
|
|
1165
1464
|
edges = v;
|
|
@@ -1211,6 +1510,7 @@ async function decompressPack(data) {
|
|
|
1211
1510
|
export {
|
|
1212
1511
|
I as IDENTITY,
|
|
1213
1512
|
buildLdrScene,
|
|
1513
|
+
colorToken,
|
|
1214
1514
|
computeCulling,
|
|
1215
1515
|
conditionalLineVisible,
|
|
1216
1516
|
conditionalLines,
|
|
@@ -1219,9 +1519,17 @@ export {
|
|
|
1219
1519
|
createLdrObject,
|
|
1220
1520
|
decodePack,
|
|
1221
1521
|
decompressPack,
|
|
1522
|
+
fileKind,
|
|
1523
|
+
fileStepViews,
|
|
1222
1524
|
flatten,
|
|
1525
|
+
hash32,
|
|
1526
|
+
invert,
|
|
1223
1527
|
isDirectColor,
|
|
1224
1528
|
loadPack,
|
|
1529
|
+
looseFile,
|
|
1530
|
+
loosePartName,
|
|
1531
|
+
looseSource,
|
|
1532
|
+
looseSteps,
|
|
1225
1533
|
makeMaterial,
|
|
1226
1534
|
mul,
|
|
1227
1535
|
norm,
|
|
@@ -1231,8 +1539,11 @@ export {
|
|
|
1231
1539
|
parseModel,
|
|
1232
1540
|
parseTexMap,
|
|
1233
1541
|
resolveTexture,
|
|
1542
|
+
rotStepMatrix,
|
|
1234
1543
|
sequenceSteps,
|
|
1235
1544
|
smoothNormals,
|
|
1545
|
+
stepViewMatrix,
|
|
1546
|
+
texWrapper,
|
|
1236
1547
|
texmapTriangleUV,
|
|
1237
1548
|
texmapUV,
|
|
1238
1549
|
transformTexMap
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse, norm, type Doc, type M } from "./spec.js";
|
|
1
|
+
import { parse, norm, type Doc, type M, type LFile, type StepView } from "./spec.js";
|
|
2
2
|
/** A placed part. `color` is an LDraw colour code (or direct colour 0x2RRGGBB); `m` is the 12-number LDraw matrix `x y z a b c d e f g h i`. */
|
|
3
3
|
export interface PartInstance {
|
|
4
4
|
part: string;
|
|
@@ -8,28 +8,55 @@ export interface PartInstance {
|
|
|
8
8
|
step: number;
|
|
9
9
|
/** Step path through nested submodels, e.g. [3, 1] = top step 3, submodel step 1. */
|
|
10
10
|
steps: number[];
|
|
11
|
+
/** Step within the file that places this instance (the last entry of `steps`). */
|
|
12
|
+
localStep: number;
|
|
11
13
|
/** Submodel path, e.g. "main.ldr/wheel.ldr". */
|
|
12
14
|
path: string;
|
|
15
|
+
/** The part is defined inside the document (an MPD-embedded part, see `fileKind`), not taken from the library. */
|
|
16
|
+
embedded?: boolean;
|
|
17
|
+
/** Set for synthetic parts (a model file's own type 2-5 lines, named `<file>#<step>`): returns the part's LDraw source
|
|
18
|
+
* (`source.file` / `source.step` give the model file and step, see `looseFile`). */
|
|
19
|
+
source?: (() => string) & {
|
|
20
|
+
file?: LFile;
|
|
21
|
+
step?: number;
|
|
22
|
+
};
|
|
23
|
+
/** Only with `flatten(doc, { textures: true })`: the part is wrapped by a model-level `!TEXMAP`. `texPart` names the synthetic
|
|
24
|
+
* textured part (`<part>#tex-<hash>`, in packs built with `textures: true`) and `texSource` is its LDraw source; `part` stays the plain part. */
|
|
25
|
+
texPart?: string;
|
|
26
|
+
texSource?: string;
|
|
27
|
+
/** View rotation of this instance's step from `0 ROTSTEP` (absent = default view). See `fileStepViews`. */
|
|
28
|
+
view?: StepView;
|
|
13
29
|
}
|
|
14
30
|
export interface FlattenOptions {
|
|
15
31
|
/** Resolve an external reference that is not a submodel of this document (e.g. a model file you loaded separately). Return null to treat it as a part. */
|
|
16
32
|
resolveModel?: (name: string) => Doc | null;
|
|
17
33
|
/** Max submodel nesting (cycle guard). Default 64. */
|
|
18
34
|
maxDepth?: number;
|
|
35
|
+
/** Emit a model file's own type 2-5 lines as synthetic parts `<file>#<step>` (LDraw allows geometry in any file). Default true. */
|
|
36
|
+
looseGeometry?: boolean;
|
|
37
|
+
/** TEXMAP-aware view of the model: references wrapped by a model-level `!TEXMAP` get `texPart`, `0 !:` type-1 lines are
|
|
38
|
+
* included and `!TEXMAP FALLBACK` references are skipped. Use with packs built with `textures: true`. Default false. */
|
|
39
|
+
textures?: boolean;
|
|
19
40
|
}
|
|
20
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Flatten a parsed LDraw document into part instances. Model files (`0 FILE` blocks in an MPD that `fileKind` calls models) are
|
|
43
|
+
* expanded; MPD-embedded parts and library references become instances. A model file's own geometry becomes synthetic parts.
|
|
44
|
+
*/
|
|
21
45
|
export declare function flatten(doc: Doc, opts?: FlattenOptions): PartInstance[];
|
|
22
46
|
/** Parse LDraw text (.ldr/.mpd/.dat) and flatten it in one call. */
|
|
23
47
|
export declare function parseModel(text: string, name?: string, opts?: FlattenOptions): PartInstance[];
|
|
24
48
|
/**
|
|
25
49
|
* Assign each instance a global sequential build-step index, ordering by the full nested step path
|
|
26
|
-
* (so submodel steps become their own steps). Returns the number of steps
|
|
50
|
+
* (so submodel steps become their own steps). Returns the number of steps and, per global step, its ROTSTEP view
|
|
51
|
+
* (null = default view; the view of the innermost file's step, see `fileStepViews`).
|
|
27
52
|
*/
|
|
28
53
|
export declare function sequenceSteps<T extends {
|
|
29
54
|
steps: number[];
|
|
55
|
+
view?: StepView;
|
|
30
56
|
}>(insts: T[]): {
|
|
31
57
|
count: number;
|
|
32
58
|
stepOf: Int32Array;
|
|
59
|
+
views: (StepView | null)[];
|
|
33
60
|
};
|
|
34
61
|
/** Colour table entry. */
|
|
35
62
|
export interface LDrawColor {
|
|
@@ -22,6 +22,9 @@ export interface BuildPackOptions {
|
|
|
22
22
|
/** Keep textured (`!TEXMAP`) parts' TEXMAP-aware geometry + UVs and embed their PNGs (pack v2 "tex"). Off by default;
|
|
23
23
|
* packs without textured parts are byte-identical either way. Textures resolve from model `!DATA` blocks, then `textures/` in the library. */
|
|
24
24
|
textures?: boolean;
|
|
25
|
+
/** Render a model file's own type 2-5 lines (as synthetic parts `<file>#<step>`). Default true (LDraw allows geometry in any file);
|
|
26
|
+
* false reproduces packs from ldr <= 0.2.0, which dropped it. */
|
|
27
|
+
looseGeometry?: boolean;
|
|
25
28
|
/** Embed the LDConfig colour table (code → hex) in the pack header. Default true. */
|
|
26
29
|
colors?: boolean;
|
|
27
30
|
onProgress?: (done: number, total: number) => void;
|
|
@@ -31,5 +34,7 @@ export interface BuildPackResult {
|
|
|
31
34
|
parts: number;
|
|
32
35
|
studKinds: number;
|
|
33
36
|
missing: string[];
|
|
37
|
+
/** e.g. two models embedding different files under the same name (the first one is used). */
|
|
38
|
+
warnings?: string[];
|
|
34
39
|
}
|
|
35
40
|
export declare function buildPack(o: BuildPackOptions): BuildPackResult;
|