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