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/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
|
|
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:
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
512
|
-
cur
|
|
988
|
+
}
|
|
989
|
+
if (!cur) {
|
|
990
|
+
cur = norm2(fallbackName);
|
|
991
|
+
main = cur;
|
|
992
|
+
}
|
|
993
|
+
buf.push(line);
|
|
513
994
|
}
|
|
514
|
-
|
|
515
|
-
return
|
|
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
|
-
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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 =
|
|
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
|
|
1328
|
-
|
|
1329
|
-
|
|
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(
|
|
1655
|
+
refs.set(norm2(p), k);
|
|
1335
1656
|
else
|
|
1336
|
-
missing.add(
|
|
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 =
|
|
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
|
-
|
|
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 });
|