ldr 0.0.0-alpha.1 → 0.2.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/LICENSE +21 -0
- package/README.md +119 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +876 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +717 -0
- package/dist/model.d.ts +44 -0
- package/dist/node/build-pack.d.ts +29 -0
- package/dist/node/compile.d.ts +43 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +1456 -0
- package/dist/node/library.d.ts +35 -0
- package/dist/node/resolver.d.ts +32 -0
- package/dist/pack-io.d.ts +2 -0
- package/dist/render.d.ts +43 -0
- package/dist/scene.d.ts +56 -0
- package/dist/spec.d.ts +73 -0
- package/package.json +56 -36
- package/lib/.DS_Store +0 -0
- package/lib/index.js +0 -1
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
// src/spec.ts
|
|
2
|
+
var norm = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
3
|
+
function parseColor(tok) {
|
|
4
|
+
if (/^0x/i.test(tok))
|
|
5
|
+
return parseInt(tok, 16);
|
|
6
|
+
return +tok;
|
|
7
|
+
}
|
|
8
|
+
var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
|
|
9
|
+
function parse(src, name = "main.ldr") {
|
|
10
|
+
const files = new Map;
|
|
11
|
+
let cur = null;
|
|
12
|
+
let main = "";
|
|
13
|
+
const start = (n) => {
|
|
14
|
+
cur = { name: norm(n), cmds: [] };
|
|
15
|
+
files.set(cur.name, cur);
|
|
16
|
+
if (!main)
|
|
17
|
+
main = cur.name;
|
|
18
|
+
};
|
|
19
|
+
for (const raw of src.split(/\r?\n/)) {
|
|
20
|
+
const line = raw.trim();
|
|
21
|
+
if (!line)
|
|
22
|
+
continue;
|
|
23
|
+
const tk = line.split(/\s+/);
|
|
24
|
+
const t = +tk[0];
|
|
25
|
+
if (t === 0) {
|
|
26
|
+
const text = line.slice(1).trim();
|
|
27
|
+
if (tk[1] === "FILE") {
|
|
28
|
+
start(text.slice(5));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (tk[1] === "NOFILE") {
|
|
32
|
+
cur = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!cur)
|
|
36
|
+
start(name);
|
|
37
|
+
const c = { t: 0, text };
|
|
38
|
+
if (tk[1] === "STEP")
|
|
39
|
+
c.step = true;
|
|
40
|
+
else if (tk[1] === "BFC")
|
|
41
|
+
c.bfc = tk.slice(2).join(" ");
|
|
42
|
+
else if (tk[1] === "ROTSTEP") {
|
|
43
|
+
c.meta = "ROTSTEP";
|
|
44
|
+
if ((tk[2] ?? "").toUpperCase() === "END")
|
|
45
|
+
c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
|
|
46
|
+
else {
|
|
47
|
+
const mode = (tk[5] ?? "REL").toUpperCase();
|
|
48
|
+
c.rotstep = { x: +tk[2] || 0, y: +tk[3] || 0, z: +tk[4] || 0, mode: ["REL", "ABS", "ADD"].includes(mode) ? mode : "REL" };
|
|
49
|
+
}
|
|
50
|
+
} else if (tk[1] === "BUFEXCHG") {
|
|
51
|
+
c.meta = "BUFEXCHG";
|
|
52
|
+
const op = (tk[2] ?? "").toUpperCase();
|
|
53
|
+
c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
|
|
54
|
+
} else if (tk[1]?.startsWith("!"))
|
|
55
|
+
c.meta = tk[1];
|
|
56
|
+
const moved = text.match(MOVED_RE);
|
|
57
|
+
if (moved && !cur.movedTo) {
|
|
58
|
+
let target = moved[1].trim().replace(/\\/g, "/");
|
|
59
|
+
if (!/\.(dat|ldr|mpd)$/i.test(target))
|
|
60
|
+
target += ".dat";
|
|
61
|
+
cur.movedTo = norm(target);
|
|
62
|
+
}
|
|
63
|
+
cur.cmds.push(c);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!cur)
|
|
67
|
+
start(name);
|
|
68
|
+
const color = parseColor(tk[1]);
|
|
69
|
+
const nums = tk.slice(2).map(Number);
|
|
70
|
+
if (t === 1)
|
|
71
|
+
cur.cmds.push({ t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) });
|
|
72
|
+
else if (t === 2)
|
|
73
|
+
cur.cmds.push({ t: 2, color, p: nums.slice(0, 6) });
|
|
74
|
+
else if (t === 3)
|
|
75
|
+
cur.cmds.push({ t: 3, color, p: nums.slice(0, 9) });
|
|
76
|
+
else if (t === 4)
|
|
77
|
+
cur.cmds.push({ t: 4, color, p: nums.slice(0, 12) });
|
|
78
|
+
else if (t === 5)
|
|
79
|
+
cur.cmds.push({ t: 5, color, p: nums.slice(0, 12) });
|
|
80
|
+
}
|
|
81
|
+
return { main, files };
|
|
82
|
+
}
|
|
83
|
+
function mul(a, b) {
|
|
84
|
+
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;
|
|
85
|
+
return [
|
|
86
|
+
a0 * bx + a1 * by + a2 * bz + ax,
|
|
87
|
+
a3 * bx + a4 * by + a5 * bz + ay,
|
|
88
|
+
a6 * bx + a7 * by + a8 * bz + az,
|
|
89
|
+
a0 * b0 + a1 * b3 + a2 * b6,
|
|
90
|
+
a0 * b1 + a1 * b4 + a2 * b7,
|
|
91
|
+
a0 * b2 + a1 * b5 + a2 * b8,
|
|
92
|
+
a3 * b0 + a4 * b3 + a5 * b6,
|
|
93
|
+
a3 * b1 + a4 * b4 + a5 * b7,
|
|
94
|
+
a3 * b2 + a4 * b5 + a5 * b8,
|
|
95
|
+
a6 * b0 + a7 * b3 + a8 * b6,
|
|
96
|
+
a6 * b1 + a7 * b4 + a8 * b7,
|
|
97
|
+
a6 * b2 + a7 * b5 + a8 * b8
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
var I = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
101
|
+
var isDirectColor = (c) => c > 16777215;
|
|
102
|
+
function smoothNormals(pos, edges, cond, creaseDeg = 35) {
|
|
103
|
+
const q = (i, a) => `${a[i].toFixed(2)},${a[i + 1].toFixed(2)},${a[i + 2].toFixed(2)}`;
|
|
104
|
+
const ek = (a, b) => a < b ? a + "|" + b : b + "|" + a;
|
|
105
|
+
const hard = new Set, soft = new Set;
|
|
106
|
+
for (let i = 0;i < edges.length; i += 6)
|
|
107
|
+
hard.add(ek(q(i, edges), q(i + 3, edges)));
|
|
108
|
+
const stride = cond.length > 0 && cond.length % 12 === 0 ? 12 : 6;
|
|
109
|
+
for (let i = 0;i < cond.length; i += stride)
|
|
110
|
+
soft.add(ek(q(i, cond), q(i + 3, cond)));
|
|
111
|
+
const nT = pos.length / 9, fn = [], vk = [];
|
|
112
|
+
for (let t = 0;t < nT; t++) {
|
|
113
|
+
const a = t * 9;
|
|
114
|
+
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]];
|
|
115
|
+
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]];
|
|
116
|
+
const l = Math.hypot(...n) || 1;
|
|
117
|
+
fn.push(n.map((x) => x / l));
|
|
118
|
+
for (let k = 0;k < 3; k++)
|
|
119
|
+
vk.push(q(a + k * 3, pos));
|
|
120
|
+
}
|
|
121
|
+
const byV = new Map;
|
|
122
|
+
vk.forEach((k, i) => (byV.get(k) ?? byV.set(k, []).get(k)).push(i));
|
|
123
|
+
const edgeFaces = new Map;
|
|
124
|
+
for (let t = 0;t < nT; t++)
|
|
125
|
+
for (let k = 0;k < 3; k++) {
|
|
126
|
+
const e = ek(vk[t * 3 + k], vk[t * 3 + (k + 1) % 3]);
|
|
127
|
+
(edgeFaces.get(e) ?? edgeFaces.set(e, []).get(e)).push(t);
|
|
128
|
+
}
|
|
129
|
+
const cosC = Math.cos(creaseDeg * Math.PI / 180), out = new Float32Array(pos.length);
|
|
130
|
+
const joinable = (e, a, b) => {
|
|
131
|
+
if (hard.has(e))
|
|
132
|
+
return false;
|
|
133
|
+
const d = fn[a][0] * fn[b][0] + fn[a][1] * fn[b][1] + fn[a][2] * fn[b][2];
|
|
134
|
+
return soft.has(e) ? d > -0.2 : d > cosC;
|
|
135
|
+
};
|
|
136
|
+
for (const [key, corners] of byV) {
|
|
137
|
+
const faces = corners.map((c) => c / 3 | 0), par = faces.map((_, i) => i);
|
|
138
|
+
const f = (i) => par[i] === i ? i : par[i] = f(par[i]);
|
|
139
|
+
for (let i = 0;i < faces.length; i++)
|
|
140
|
+
for (let j = i + 1;j < faces.length; j++) {
|
|
141
|
+
const A = faces[i], B = faces[j];
|
|
142
|
+
const va = [0, 1, 2].map((k) => vk[A * 3 + k]), vb = new Set([0, 1, 2].map((k) => vk[B * 3 + k]));
|
|
143
|
+
for (const o of va)
|
|
144
|
+
if (o !== key && vb.has(o)) {
|
|
145
|
+
const e = ek(key, o);
|
|
146
|
+
if ((edgeFaces.get(e)?.length ?? 0) >= 2 && joinable(e, A, B))
|
|
147
|
+
par[f(i)] = f(j);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const acc = new Map;
|
|
151
|
+
faces.forEach((F, i) => {
|
|
152
|
+
const r = f(i), s = acc.get(r) ?? [0, 0, 0];
|
|
153
|
+
s[0] += fn[F][0];
|
|
154
|
+
s[1] += fn[F][1];
|
|
155
|
+
s[2] += fn[F][2];
|
|
156
|
+
acc.set(r, s);
|
|
157
|
+
});
|
|
158
|
+
corners.forEach((c, i) => {
|
|
159
|
+
const s = acc.get(f(i)), l = Math.hypot(...s) || 1;
|
|
160
|
+
out[c * 3] = s[0] / l;
|
|
161
|
+
out[c * 3 + 1] = s[1] / l;
|
|
162
|
+
out[c * 3 + 2] = s[2] / l;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
// src/model.ts
|
|
168
|
+
function flatten(doc, opts = {}) {
|
|
169
|
+
const out = [];
|
|
170
|
+
const maxDepth = opts.maxDepth ?? 64;
|
|
171
|
+
const walk = (d, fname, m, color, path, stepPath, depth) => {
|
|
172
|
+
const f = d.files.get(fname);
|
|
173
|
+
if (!f || depth > maxDepth)
|
|
174
|
+
return;
|
|
175
|
+
let s = 0;
|
|
176
|
+
for (const c of f.cmds) {
|
|
177
|
+
if (c.t === 0 && c.step)
|
|
178
|
+
s++;
|
|
179
|
+
if (c.t !== 1)
|
|
180
|
+
continue;
|
|
181
|
+
const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
|
|
182
|
+
if (d.files.has(c.file)) {
|
|
183
|
+
walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const ext = opts.resolveModel?.(c.file);
|
|
187
|
+
if (ext) {
|
|
188
|
+
walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
walk(doc, doc.main, I, 16, doc.main, [], 0);
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function parseModel(text, name = "main.ldr", opts) {
|
|
198
|
+
return flatten(parse(text, name), opts);
|
|
199
|
+
}
|
|
200
|
+
function sequenceSteps(insts) {
|
|
201
|
+
const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
|
|
202
|
+
const uniq = [...new Set(insts.map(key))].sort();
|
|
203
|
+
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
204
|
+
const stepOf = new Int32Array(insts.length);
|
|
205
|
+
insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
|
|
206
|
+
return { count: uniq.length, stepOf };
|
|
207
|
+
}
|
|
208
|
+
function parseLDConfig(text) {
|
|
209
|
+
const out = new Map;
|
|
210
|
+
for (const l of text.split(/\r?\n/)) {
|
|
211
|
+
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);
|
|
212
|
+
if (m)
|
|
213
|
+
out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
// src/render.ts
|
|
218
|
+
import * as THREE from "three";
|
|
219
|
+
async function loadPack(url) {
|
|
220
|
+
const res = await fetch(url);
|
|
221
|
+
const t0 = performance.now();
|
|
222
|
+
let buf;
|
|
223
|
+
if (url.endsWith(".gz"))
|
|
224
|
+
buf = await new Response(res.body.pipeThrough(new DecompressionStream("gzip"))).arrayBuffer();
|
|
225
|
+
else if (url.endsWith(".br"))
|
|
226
|
+
buf = await new Response(res.body.pipeThrough(new DecompressionStream("brotli"))).arrayBuffer();
|
|
227
|
+
else if (url.endsWith(".zst")) {
|
|
228
|
+
const raw = new Uint8Array(await res.arrayBuffer());
|
|
229
|
+
const t1 = performance.now();
|
|
230
|
+
const { decompress } = await import("fzstd");
|
|
231
|
+
const o = decompress(raw);
|
|
232
|
+
buf = o.buffer.slice(o.byteOffset, o.byteOffset + o.byteLength);
|
|
233
|
+
globalThis.__decodeMs = performance.now() - t1;
|
|
234
|
+
} else
|
|
235
|
+
buf = await res.arrayBuffer();
|
|
236
|
+
if (!url.endsWith(".zst"))
|
|
237
|
+
globalThis.__decodeMs = performance.now() - t0;
|
|
238
|
+
return decodePack(buf);
|
|
239
|
+
}
|
|
240
|
+
function decodePack(buf) {
|
|
241
|
+
const hl = new DataView(buf).getUint32(0, true);
|
|
242
|
+
const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, hl)));
|
|
243
|
+
const base = 4 + hl;
|
|
244
|
+
const arr = (r) => r && (r[2] === "f32" ? new Float32Array(buf, base + r[0], r[1]) : r[2] === "u16" ? new Uint16Array(buf, base + r[0], r[1]) : new Uint32Array(buf, base + r[0], r[1]));
|
|
245
|
+
return { header, arr, bytes: buf.byteLength };
|
|
246
|
+
}
|
|
247
|
+
function geom(b, arr) {
|
|
248
|
+
if (!b)
|
|
249
|
+
return null;
|
|
250
|
+
const g = new THREE.BufferGeometry;
|
|
251
|
+
g.setAttribute("position", new THREE.BufferAttribute(arr(b.pos), 3));
|
|
252
|
+
g.setAttribute("normal", new THREE.BufferAttribute(arr(b.nrm), 3));
|
|
253
|
+
g.setAttribute("ldrColor", new THREE.BufferAttribute(arr(b.col), 4));
|
|
254
|
+
if (b.idx)
|
|
255
|
+
g.setIndex(new THREE.BufferAttribute(arr(b.idx), 1));
|
|
256
|
+
return g;
|
|
257
|
+
}
|
|
258
|
+
function makeMaterial(side) {
|
|
259
|
+
const m = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.45, side });
|
|
260
|
+
m.onBeforeCompile = (s) => {
|
|
261
|
+
s.vertexShader = `attribute vec4 ldrColor;
|
|
262
|
+
` + s.vertexShader.replace("#include <color_vertex>", `#ifdef USE_INSTANCING_COLOR
|
|
263
|
+
vColor = vec4(ldrColor.a > 0.5 ? pow(ldrColor.rgb, vec3(2.2)) : instanceColor.rgb, 1.0); // baked LDConfig colours are sRGB
|
|
264
|
+
#else
|
|
265
|
+
vColor = vec4(pow(ldrColor.rgb, vec3(2.2)), 1.0);
|
|
266
|
+
#endif`);
|
|
267
|
+
};
|
|
268
|
+
m.customProgramCacheKey = () => "ldrColor";
|
|
269
|
+
return m;
|
|
270
|
+
}
|
|
271
|
+
var lineMat = () => new THREE.ShaderMaterial({
|
|
272
|
+
vertexShader: `attribute vec4 im0; attribute vec4 im1; attribute vec4 im2; attribute vec4 im3;
|
|
273
|
+
void main(){ gl_Position = projectionMatrix * modelViewMatrix * mat4(im0,im1,im2,im3) * vec4(position,1.0); }`,
|
|
274
|
+
fragmentShader: `void main(){ gl_FragColor = vec4(0.13,0.13,0.13,1.0); }`
|
|
275
|
+
});
|
|
276
|
+
var sharedLineMat = () => new THREE.ShaderMaterial({
|
|
277
|
+
vertexShader: `attribute mat4 instanceMatrix; void main(){ gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position,1.0); }`,
|
|
278
|
+
fragmentShader: `void main(){ gl_FragColor = vec4(0.13,0.13,0.13,1.0); }`
|
|
279
|
+
});
|
|
280
|
+
var weldLines = (pos) => {
|
|
281
|
+
const map = new Map;
|
|
282
|
+
const P = [];
|
|
283
|
+
const I = [];
|
|
284
|
+
for (let i = 0;i < pos.length; i += 3) {
|
|
285
|
+
const k = pos[i].toFixed(2) + "," + pos[i + 1].toFixed(2) + "," + pos[i + 2].toFixed(2);
|
|
286
|
+
let j = map.get(k);
|
|
287
|
+
if (j === undefined) {
|
|
288
|
+
j = P.length / 3;
|
|
289
|
+
map.set(k, j);
|
|
290
|
+
P.push(pos[i], pos[i + 1], pos[i + 2]);
|
|
291
|
+
}
|
|
292
|
+
I.push(j);
|
|
293
|
+
}
|
|
294
|
+
return { P: new Float32Array(P), I: P.length / 3 < 65536 ? new Uint16Array(I) : new Uint32Array(I) };
|
|
295
|
+
};
|
|
296
|
+
function createInstancedEdges(pos, im, material = sharedLineMat()) {
|
|
297
|
+
return sharedLines(pos, im, material);
|
|
298
|
+
}
|
|
299
|
+
function sharedLines(pos, im, material = sharedLineMat()) {
|
|
300
|
+
const { P, I } = weldLines(pos);
|
|
301
|
+
const g = new THREE.InstancedBufferGeometry;
|
|
302
|
+
g.setAttribute("position", new THREE.BufferAttribute(P, 3));
|
|
303
|
+
g.setIndex(new THREE.BufferAttribute(I, 1));
|
|
304
|
+
g.setAttribute("instanceMatrix", im.instanceMatrix);
|
|
305
|
+
g.instanceCount = im.count;
|
|
306
|
+
const l = new THREE.LineSegments(g, material);
|
|
307
|
+
l.frustumCulled = false;
|
|
308
|
+
return l;
|
|
309
|
+
}
|
|
310
|
+
function instancedLines(pos, mats, count) {
|
|
311
|
+
const g = new THREE.InstancedBufferGeometry;
|
|
312
|
+
g.setAttribute("position", new THREE.BufferAttribute(pos, 3));
|
|
313
|
+
const ib = new THREE.InstancedInterleavedBuffer(mats, 16);
|
|
314
|
+
for (let i = 0;i < 4; i++)
|
|
315
|
+
g.setAttribute("im" + i, new THREE.InterleavedBufferAttribute(ib, 4, i * 4));
|
|
316
|
+
g.instanceCount = count;
|
|
317
|
+
const l = new THREE.LineSegments(g, lineMat());
|
|
318
|
+
l.frustumCulled = false;
|
|
319
|
+
return l;
|
|
320
|
+
}
|
|
321
|
+
var toM4 = (m, o = 0) => new THREE.Matrix4().set(m[o + 3], m[o + 4], m[o + 5], m[o], m[o + 6], m[o + 7], m[o + 8], m[o + 1], m[o + 9], m[o + 10], m[o + 11], m[o + 2], 0, 0, 0, 1);
|
|
322
|
+
var mul12 = (a, b) => {
|
|
323
|
+
const r = (i, j) => a[3 + i * 3] * b[3 + j] + a[4 + i * 3] * b[6 + j] + a[5 + i * 3] * b[9 + j];
|
|
324
|
+
return [a[0] + a[3] * b[0] + a[4] * b[1] + a[5] * b[2], a[1] + a[6] * b[0] + a[7] * b[1] + a[8] * b[2], a[2] + a[9] * b[0] + a[10] * b[1] + a[11] * b[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)];
|
|
325
|
+
};
|
|
326
|
+
function computeCulling(insts, H) {
|
|
327
|
+
const upright = (m) => Math.abs(m[7] - 1) < 0.001 && Math.abs(m[4]) < 0.001 && Math.abs(m[10]) < 0.001;
|
|
328
|
+
const CELL = 40;
|
|
329
|
+
const rects = new Map;
|
|
330
|
+
const lk = (y) => Math.round(y * 2) / 2;
|
|
331
|
+
insts.forEach((it, idx) => {
|
|
332
|
+
const p = H.parts[it.part];
|
|
333
|
+
if (!p || !p.mask || !upright(it.m))
|
|
334
|
+
return;
|
|
335
|
+
const b = p.bbox;
|
|
336
|
+
const xs = [], zs = [];
|
|
337
|
+
for (const x of [b[0], b[3]])
|
|
338
|
+
for (const z of [b[2], b[5]]) {
|
|
339
|
+
const w = [it.m[0] + it.m[3] * x + it.m[5] * z, it.m[2] + it.m[9] * x + it.m[11] * z];
|
|
340
|
+
xs.push(w[0]);
|
|
341
|
+
zs.push(w[1]);
|
|
342
|
+
}
|
|
343
|
+
const r = [Math.min(...xs), Math.max(...xs), Math.min(...zs), Math.max(...zs), idx];
|
|
344
|
+
const lv = lk(it.m[1] + b[4]);
|
|
345
|
+
for (let cx = Math.floor(r[0] / CELL);cx <= Math.floor(r[1] / CELL); cx++)
|
|
346
|
+
for (let cz = Math.floor(r[2] / CELL);cz <= Math.floor(r[3] / CELL); cz++) {
|
|
347
|
+
const k = lv + "|" + cx + "|" + cz;
|
|
348
|
+
(rects.get(k) || rects.set(k, []).get(k)).push(r);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
const covered = (x, y, z, self) => {
|
|
352
|
+
const list = rects.get(lk(y) + "|" + Math.floor(x / CELL) + "|" + Math.floor(z / CELL));
|
|
353
|
+
if (!list)
|
|
354
|
+
return false;
|
|
355
|
+
for (const r of list) {
|
|
356
|
+
if (r[4] === self || !(x - 6 >= r[0] - 0.5 && x + 6 <= r[1] + 0.5 && z - 6 >= r[2] - 0.5 && z + 6 <= r[3] + 0.5))
|
|
357
|
+
continue;
|
|
358
|
+
const it = insts[r[4]];
|
|
359
|
+
const p = H.parts[it.part];
|
|
360
|
+
const m = it.m;
|
|
361
|
+
const dx = x - m[0], dy = y - m[1], dz = z - m[2];
|
|
362
|
+
const lx = m[3] * dx + m[6] * dy + m[9] * dz, lz = m[5] * dx + m[8] * dy + m[11] * dz;
|
|
363
|
+
const [nx, nz, bits] = p.mask;
|
|
364
|
+
let ok = true;
|
|
365
|
+
for (const [ox, oz] of [[0, 0], [5, 5], [-5, 5], [5, -5], [-5, -5]]) {
|
|
366
|
+
const ca = Math.floor((lx + ox - p.bbox[0]) / 20), cb = Math.floor((lz + oz - p.bbox[2]) / 20);
|
|
367
|
+
if (ca < 0 || cb < 0 || ca >= nx || cb >= nz || bits[ca * nz + cb] !== "1") {
|
|
368
|
+
ok = false;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (ok)
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
return false;
|
|
376
|
+
};
|
|
377
|
+
const studs = [];
|
|
378
|
+
const studTops = new Set;
|
|
379
|
+
const key3 = (x, y, z) => `${Math.round(x)},${Math.round(y)},${Math.round(z)}`;
|
|
380
|
+
insts.forEach((it, idx) => {
|
|
381
|
+
const p = H.parts[it.part];
|
|
382
|
+
if (!p)
|
|
383
|
+
return;
|
|
384
|
+
for (const s of p.studs) {
|
|
385
|
+
const w = mul12(it.m, s.slice(2));
|
|
386
|
+
const color = s[1] === "16" ? it.color : s[1];
|
|
387
|
+
const up = upright(w);
|
|
388
|
+
studs.push({ kind: s[0], color, w, hidden: up && covered(w[0], w[1], w[2], idx) });
|
|
389
|
+
if (up)
|
|
390
|
+
studTops.add(key3(w[0], w[1], w[2]));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
const underHidden = insts.map((it) => {
|
|
394
|
+
const p = H.parts[it.part];
|
|
395
|
+
if (!p || !p.under || !upright(it.m))
|
|
396
|
+
return false;
|
|
397
|
+
const b = p.bbox;
|
|
398
|
+
const nx = Math.round((b[3] - b[0]) / 20), nz = Math.round((b[5] - b[2]) / 20);
|
|
399
|
+
if (!nx || !nz || nx * nz > 4096)
|
|
400
|
+
return false;
|
|
401
|
+
for (let a = 0;a < nx; a++)
|
|
402
|
+
for (let c = 0;c < nz; c++) {
|
|
403
|
+
const lx = b[0] + 10 + 20 * a, lz = b[2] + 10 + 20 * c;
|
|
404
|
+
const x = it.m[0] + it.m[3] * lx + it.m[5] * lz, z = it.m[2] + it.m[9] * lx + it.m[11] * lz;
|
|
405
|
+
if (!studTops.has(key3(x, it.m[1] + b[4], z)))
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
});
|
|
410
|
+
return { studs, underHidden };
|
|
411
|
+
}
|
|
412
|
+
function buildLdrScene(insts, pack, P, color, mark) {
|
|
413
|
+
const { header: H, arr } = pack;
|
|
414
|
+
const root = new THREE.Group;
|
|
415
|
+
const matF = makeMaterial(P.side === "front" ? THREE.FrontSide : THREE.DoubleSide), matB = makeMaterial(P.side === "front" ? THREE.BackSide : THREE.DoubleSide);
|
|
416
|
+
const matD = makeMaterial(THREE.DoubleSide);
|
|
417
|
+
let nUncert = 0;
|
|
418
|
+
const det = (m) => m[3] * (m[7] * m[11] - m[8] * m[10]) - m[4] * (m[6] * m[11] - m[8] * m[9]) + m[5] * (m[6] * m[10] - m[7] * m[9]);
|
|
419
|
+
const byPart = new Map;
|
|
420
|
+
insts.forEach((it, i) => {
|
|
421
|
+
if (H.parts[it.part]) {
|
|
422
|
+
const k = it.part + (det(it.m) < 0 ? "#m" : "");
|
|
423
|
+
(byPart.get(k) || byPart.set(k, []).get(k)).push(i);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const cull = computeCulling(insts, H);
|
|
427
|
+
mark("culling");
|
|
428
|
+
const edgeObjs = [];
|
|
429
|
+
const stats = { studsTotal: cull.studs.length, studsHidden: cull.studs.filter((s) => s.hidden).length, undersTotal: 0, undersHidden: 0 };
|
|
430
|
+
const toggles = [];
|
|
431
|
+
const studMeshes = [];
|
|
432
|
+
for (const [pk, ids] of byPart) {
|
|
433
|
+
const part = pk.replace(/#m$/, "");
|
|
434
|
+
const p = H.parts[part];
|
|
435
|
+
const mat = P.uncertDouble !== false && p.uncert ? (nUncert++, matD) : pk.endsWith("#m") ? matB : matF;
|
|
436
|
+
const body = geom(p.body, arr);
|
|
437
|
+
let bodyIm = null;
|
|
438
|
+
if (body) {
|
|
439
|
+
const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
|
|
440
|
+
ids.forEach((id, k) => {
|
|
441
|
+
im.setMatrixAt(k, toM4(insts[id].m));
|
|
442
|
+
im.setColorAt(k, color(insts[id].color));
|
|
443
|
+
});
|
|
444
|
+
im.computeBoundingSphere();
|
|
445
|
+
root.add(im);
|
|
446
|
+
}
|
|
447
|
+
if (P.edges === "idx" && p.edges && bodyIm) {
|
|
448
|
+
const l = sharedLines(arr(p.edges), bodyIm);
|
|
449
|
+
edgeObjs.push(l);
|
|
450
|
+
root.add(l);
|
|
451
|
+
}
|
|
452
|
+
const under = geom(p.under, arr);
|
|
453
|
+
if (under) {
|
|
454
|
+
const ord = [...ids.filter((i) => !cull.underHidden[i]), ...ids.filter((i) => cull.underHidden[i])];
|
|
455
|
+
const vis = ids.filter((i) => !cull.underHidden[i]).length;
|
|
456
|
+
stats.undersTotal += ids.length;
|
|
457
|
+
stats.undersHidden += ids.length - vis;
|
|
458
|
+
const im = new THREE.InstancedMesh(under, mat, ids.length);
|
|
459
|
+
ord.forEach((id, k) => {
|
|
460
|
+
im.setMatrixAt(k, toM4(insts[id].m));
|
|
461
|
+
im.setColorAt(k, color(insts[id].color));
|
|
462
|
+
});
|
|
463
|
+
im.computeBoundingSphere();
|
|
464
|
+
im.count = P.cullUnder ? vis : ids.length;
|
|
465
|
+
root.add(im);
|
|
466
|
+
toggles.push({ mesh: im, all: ids.length, visible: vis, flag: "under" });
|
|
467
|
+
}
|
|
468
|
+
if (P.edges === true && p.edges) {
|
|
469
|
+
const mats = new Float32Array(ids.length * 16);
|
|
470
|
+
ids.forEach((id, k) => toM4(insts[id].m).toArray(mats, k * 16));
|
|
471
|
+
{
|
|
472
|
+
const l = instancedLines(arr(p.edges), mats, ids.length);
|
|
473
|
+
edgeObjs.push(l);
|
|
474
|
+
root.add(l);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const byKind = new Map;
|
|
479
|
+
for (const s of cull.studs) {
|
|
480
|
+
const k = s.kind + (det(s.w) < 0 ? "#m" : "");
|
|
481
|
+
(byKind.get(k) || byKind.set(k, []).get(k)).push(s);
|
|
482
|
+
}
|
|
483
|
+
for (const [kk, list] of byKind) {
|
|
484
|
+
const kind = kk.replace(/#m$/, "");
|
|
485
|
+
const mat = kk.endsWith("#m") ? matB : matF;
|
|
486
|
+
const k = H.studKinds[kind];
|
|
487
|
+
const g = geom(k.body, arr);
|
|
488
|
+
if (!g)
|
|
489
|
+
continue;
|
|
490
|
+
const ord = [...list.filter((s) => !s.hidden), ...list.filter((s) => s.hidden)];
|
|
491
|
+
const vis = list.filter((s) => !s.hidden).length;
|
|
492
|
+
const im = new THREE.InstancedMesh(g, mat, ord.length);
|
|
493
|
+
ord.forEach((s, i) => {
|
|
494
|
+
im.setMatrixAt(i, toM4(s.w));
|
|
495
|
+
im.setColorAt(i, color(s.color));
|
|
496
|
+
});
|
|
497
|
+
im.computeBoundingSphere();
|
|
498
|
+
im.count = P.cullStuds ? vis : ord.length;
|
|
499
|
+
root.add(im);
|
|
500
|
+
toggles.push({ mesh: im, all: ord.length, visible: vis, flag: "studs" });
|
|
501
|
+
studMeshes.push({ im, kind });
|
|
502
|
+
if (P.edges === "idx" && k.edges) {
|
|
503
|
+
const l = sharedLines(arr(k.edges), im);
|
|
504
|
+
edgeObjs.push(l);
|
|
505
|
+
root.add(l);
|
|
506
|
+
toggles.push({ mesh: { set count(n) {
|
|
507
|
+
l.geometry.instanceCount = n;
|
|
508
|
+
} }, all: ord.length, visible: vis, flag: "studs" });
|
|
509
|
+
l.geometry.instanceCount = im.count;
|
|
510
|
+
}
|
|
511
|
+
if (P.edges === true && k.edges) {
|
|
512
|
+
const mats = new Float32Array(ord.length * 16);
|
|
513
|
+
ord.forEach((s, i) => toM4(s.w).toArray(mats, i * 16));
|
|
514
|
+
const l = instancedLines(arr(k.edges), mats, P.cullStuds ? vis : ord.length);
|
|
515
|
+
edgeObjs.push(l);
|
|
516
|
+
root.add(l);
|
|
517
|
+
toggles.push({ mesh: { set count(n) {
|
|
518
|
+
l.geometry.instanceCount = n;
|
|
519
|
+
} }, all: ord.length, visible: vis, flag: "studs" });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
stats.uncertParts = Object.values(H.parts).filter((x) => x.uncert).length;
|
|
523
|
+
stats.uncertBatches = nUncert;
|
|
524
|
+
const setEdges = (v) => edgeObjs.forEach((o) => o.visible = v);
|
|
525
|
+
const setCull = (studs, under) => {
|
|
526
|
+
for (const t of toggles)
|
|
527
|
+
t.mesh.count = (t.flag === "studs" ? studs : under) ? t.visible : t.all;
|
|
528
|
+
};
|
|
529
|
+
const box = new THREE.Box3;
|
|
530
|
+
for (const it of insts) {
|
|
531
|
+
const p = H.parts[it.part];
|
|
532
|
+
if (!p)
|
|
533
|
+
continue;
|
|
534
|
+
box.expandByPoint(new THREE.Vector3(it.m[0], it.m[1], it.m[2]));
|
|
535
|
+
}
|
|
536
|
+
const setSide = (front) => {
|
|
537
|
+
matF.side = front ? THREE.FrontSide : THREE.DoubleSide;
|
|
538
|
+
matB.side = front ? THREE.BackSide : THREE.DoubleSide;
|
|
539
|
+
matF.needsUpdate = matB.needsUpdate = true;
|
|
540
|
+
};
|
|
541
|
+
const swapStuds = (alt) => {
|
|
542
|
+
const saved = studMeshes.map((s) => s.im.geometry);
|
|
543
|
+
for (const s of studMeshes) {
|
|
544
|
+
const k2 = alt.header.studKinds[s.kind] ? s.kind : Object.keys(alt.header.studKinds).find((k) => k.replace("p/8/", "p/") === s.kind.replace("p/8/", "p/"));
|
|
545
|
+
if (k2)
|
|
546
|
+
s.im.geometry = geom(alt.header.studKinds[k2].body, alt.arr);
|
|
547
|
+
}
|
|
548
|
+
return () => studMeshes.forEach((s, i) => s.im.geometry = saved[i]);
|
|
549
|
+
};
|
|
550
|
+
return { root, stats, setCull, setSide, setEdges, swapStuds, box };
|
|
551
|
+
}
|
|
552
|
+
// src/scene.ts
|
|
553
|
+
import * as THREE2 from "three";
|
|
554
|
+
function colorFn(src, pack, fallback) {
|
|
555
|
+
const table = new Map;
|
|
556
|
+
const put = (k, v) => table.set(String(k), new THREE2.Color(typeof v === "string" ? v : v.hex));
|
|
557
|
+
if (typeof src === "function") {
|
|
558
|
+
return (c) => {
|
|
559
|
+
let col = table.get(c);
|
|
560
|
+
if (!col) {
|
|
561
|
+
const v = src(+c);
|
|
562
|
+
col = v === undefined ? direct(c) ?? fallback : new THREE2.Color(v);
|
|
563
|
+
table.set(c, col);
|
|
564
|
+
}
|
|
565
|
+
return col;
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const base = src ?? pack.header.colors ?? {};
|
|
569
|
+
if (base instanceof Map)
|
|
570
|
+
for (const [k, v] of base)
|
|
571
|
+
put(k, v);
|
|
572
|
+
else
|
|
573
|
+
for (const k in base)
|
|
574
|
+
put(k, base[k]);
|
|
575
|
+
return (c) => table.get(c) ?? direct(c) ?? fallback;
|
|
576
|
+
}
|
|
577
|
+
var direct = (c) => {
|
|
578
|
+
const n = +c;
|
|
579
|
+
return n > 16777215 && n >>> 24 === 2 ? new THREE2.Color(n & 16777215) : null;
|
|
580
|
+
};
|
|
581
|
+
var plasticCache = new WeakMap;
|
|
582
|
+
function toPlastic(old) {
|
|
583
|
+
let m = plasticCache.get(old);
|
|
584
|
+
if (!m) {
|
|
585
|
+
const p = new THREE2.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.3, clearcoat: 0.3, clearcoatRoughness: 0.2, specularIntensity: 0.6, side: old.side });
|
|
586
|
+
p.onBeforeCompile = old.onBeforeCompile;
|
|
587
|
+
p.customProgramCacheKey = () => "ldrColorPhys" + old.side;
|
|
588
|
+
plasticCache.set(old, m = p);
|
|
589
|
+
}
|
|
590
|
+
return m;
|
|
591
|
+
}
|
|
592
|
+
function createLdrObject(instances, pack, options = {}) {
|
|
593
|
+
const o = { edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
|
|
594
|
+
const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
|
|
595
|
+
const all = instances.map((i) => ({ part: i.part, color: String(i.color), m: i.m, steps: i.steps }));
|
|
596
|
+
const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
|
|
597
|
+
const uniq = [...new Set(all.map(key))].sort();
|
|
598
|
+
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
599
|
+
const stepOf = all.map((i) => rank.get(key(i)));
|
|
600
|
+
const missing = [...new Set(all.filter((i) => !pack.header.parts[i.part]).map((i) => i.part))];
|
|
601
|
+
const object = new THREE2.Group;
|
|
602
|
+
object.name = "ldr";
|
|
603
|
+
if (!o.ldrawSpace)
|
|
604
|
+
object.rotation.x = Math.PI;
|
|
605
|
+
if (o.scale && o.scale !== 1)
|
|
606
|
+
object.scale.setScalar(o.scale);
|
|
607
|
+
const pairs = new Map;
|
|
608
|
+
let cur = null;
|
|
609
|
+
let { edges, cullStuds: cs, cullUndersides: cu, side } = o;
|
|
610
|
+
const stats = { instances: all.length, drawn: 0, missing, studsTotal: 0, studsHidden: 0, undersTotal: 0, undersHidden: 0, buildMs: 0 };
|
|
611
|
+
const disposeCur = () => {
|
|
612
|
+
if (!cur)
|
|
613
|
+
return;
|
|
614
|
+
object.remove(cur.root);
|
|
615
|
+
const mats = new Set;
|
|
616
|
+
cur.root.traverse((x) => {
|
|
617
|
+
x.geometry?.dispose?.();
|
|
618
|
+
if (x.material)
|
|
619
|
+
mats.add(x.material);
|
|
620
|
+
if (x.dispose && x.isInstancedMesh)
|
|
621
|
+
x.dispose();
|
|
622
|
+
});
|
|
623
|
+
for (const m of mats)
|
|
624
|
+
m.dispose();
|
|
625
|
+
for (const old of pairs.values())
|
|
626
|
+
old.dispose();
|
|
627
|
+
cur = null;
|
|
628
|
+
};
|
|
629
|
+
const build = (step) => {
|
|
630
|
+
const t0 = performance.now();
|
|
631
|
+
disposeCur();
|
|
632
|
+
const insts = step == null ? all : all.filter((_, k) => stepOf[k] <= step);
|
|
633
|
+
cur = buildLdrScene(insts, pack, { pack: "", cullStuds: cs, cullUnder: cu, edges: edges ? "idx" : false, side }, color, () => {});
|
|
634
|
+
pairs.clear();
|
|
635
|
+
if (o.material === "plastic")
|
|
636
|
+
cur.root.traverse((x) => {
|
|
637
|
+
if (x.isMesh && x.material?.isMeshStandardMaterial) {
|
|
638
|
+
const old = x.material;
|
|
639
|
+
x.material = toPlastic(old);
|
|
640
|
+
pairs.set(x.material, old);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
if (!edges)
|
|
644
|
+
cur.setEdges(false);
|
|
645
|
+
object.add(cur.root);
|
|
646
|
+
Object.assign(stats, { drawn: insts.filter((i) => pack.header.parts[i.part]).length, studsTotal: cur.stats.studsTotal, studsHidden: cur.stats.studsHidden, undersTotal: cur.stats.undersTotal, undersHidden: cur.stats.undersHidden, buildMs: performance.now() - t0 });
|
|
647
|
+
};
|
|
648
|
+
build(o.step ?? null);
|
|
649
|
+
return {
|
|
650
|
+
object,
|
|
651
|
+
stats,
|
|
652
|
+
stepCount: uniq.length,
|
|
653
|
+
setStep: (s) => build(s),
|
|
654
|
+
setEdges: (v) => {
|
|
655
|
+
edges = v;
|
|
656
|
+
cur.setEdges(v);
|
|
657
|
+
},
|
|
658
|
+
setCulling: (a, b) => {
|
|
659
|
+
cs = a;
|
|
660
|
+
cu = b;
|
|
661
|
+
cur.setCull(a, b);
|
|
662
|
+
},
|
|
663
|
+
setSide: (s) => {
|
|
664
|
+
side = s;
|
|
665
|
+
cur.setSide(s === "front");
|
|
666
|
+
for (const [p, old] of pairs) {
|
|
667
|
+
p.side = old.side;
|
|
668
|
+
p.needsUpdate = true;
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
getBox: (target = new THREE2.Box3) => {
|
|
672
|
+
object.updateMatrixWorld(true);
|
|
673
|
+
target.makeEmpty();
|
|
674
|
+
object.traverse((x) => {
|
|
675
|
+
if (x.isInstancedMesh && x.count) {
|
|
676
|
+
x.computeBoundingBox();
|
|
677
|
+
const b = x.boundingBox.clone().applyMatrix4(x.matrixWorld);
|
|
678
|
+
target.union(b);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
return target;
|
|
682
|
+
},
|
|
683
|
+
dispose: disposeCur
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
// src/pack-io.ts
|
|
687
|
+
import { decompress } from "fzstd";
|
|
688
|
+
async function decompressPack(data) {
|
|
689
|
+
const u = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
690
|
+
const own = (o) => o.buffer.slice(o.byteOffset, o.byteOffset + o.byteLength);
|
|
691
|
+
if (u[0] === 40 && u[1] === 181 && u[2] === 47 && u[3] === 253)
|
|
692
|
+
return own(decompress(u));
|
|
693
|
+
if (u[0] === 31 && u[1] === 139)
|
|
694
|
+
return new Response(new Blob([u]).stream().pipeThrough(new DecompressionStream("gzip"))).arrayBuffer();
|
|
695
|
+
return own(u);
|
|
696
|
+
}
|
|
697
|
+
// src/node/library.ts
|
|
698
|
+
import { existsSync, readFileSync } from "fs";
|
|
699
|
+
import { join } from "path";
|
|
700
|
+
var xf = (m, x, y, z) => [m[3] * x + m[4] * y + m[5] * z + m[0], m[6] * x + m[7] * y + m[8] * z + m[1], m[9] * x + m[10] * y + m[11] * z + m[2]];
|
|
701
|
+
var det = (m) => m[3] * (m[7] * m[11] - m[8] * m[10]) - m[4] * (m[6] * m[11] - m[8] * m[9]) + m[5] * (m[6] * m[10] - m[7] * m[9]);
|
|
702
|
+
|
|
703
|
+
class Library {
|
|
704
|
+
root;
|
|
705
|
+
unofficialRoot;
|
|
706
|
+
cache = new Map;
|
|
707
|
+
meshCache = new Map;
|
|
708
|
+
constructor(root, unofficialRoot) {
|
|
709
|
+
this.root = root;
|
|
710
|
+
this.unofficialRoot = unofficialRoot;
|
|
711
|
+
}
|
|
712
|
+
where(name, _seen) {
|
|
713
|
+
name = norm(name);
|
|
714
|
+
const seen = _seen ?? new Set;
|
|
715
|
+
if (seen.has(name))
|
|
716
|
+
return null;
|
|
717
|
+
seen.add(name);
|
|
718
|
+
const hit = this.lookup(name);
|
|
719
|
+
if (!hit)
|
|
720
|
+
return null;
|
|
721
|
+
const file = hit.doc.files.get(hit.doc.main);
|
|
722
|
+
if (file?.movedTo && file.movedTo !== name) {
|
|
723
|
+
const target = this.where(file.movedTo, seen);
|
|
724
|
+
if (target)
|
|
725
|
+
return { ...target, aliasOf: name };
|
|
726
|
+
}
|
|
727
|
+
return hit;
|
|
728
|
+
}
|
|
729
|
+
lookup(name) {
|
|
730
|
+
const roots = [
|
|
731
|
+
{ root: this.root, dirs: [["parts", "part"], ["p", "prim"], ["models", "model"], ["parts/s", "part"]] }
|
|
732
|
+
];
|
|
733
|
+
const unoff = this.unofficialRoot ?? (existsSync(join(this.root, "unofficial")) ? join(this.root, "unofficial") : undefined);
|
|
734
|
+
if (unoff)
|
|
735
|
+
roots.push({ root: unoff, dirs: [["parts", "part"], ["p", "prim"], ["models", "model"], ["parts/s", "part"]] });
|
|
736
|
+
for (const { root, dirs } of roots) {
|
|
737
|
+
for (const [d, kind] of dirs) {
|
|
738
|
+
if (d === "parts/s" && name.startsWith("s/"))
|
|
739
|
+
continue;
|
|
740
|
+
const key = root + "|" + d + "/" + name;
|
|
741
|
+
if (!this.cache.has(key)) {
|
|
742
|
+
const f = this.resolveFile(root, d, name);
|
|
743
|
+
this.cache.set(key, f ? parse(readFileSync(f, "latin1"), name) : null);
|
|
744
|
+
}
|
|
745
|
+
const doc = this.cache.get(key);
|
|
746
|
+
if (doc)
|
|
747
|
+
return { kind, doc };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
resolveFile(root, dir, name) {
|
|
753
|
+
const exact = join(root, dir, name);
|
|
754
|
+
if (existsSync(exact))
|
|
755
|
+
return exact;
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
colors() {
|
|
759
|
+
const out = new Map;
|
|
760
|
+
const d = parse(readFileSync(join(this.root, "LDConfig.ldr"), "latin1"));
|
|
761
|
+
for (const c of d.files.get(d.main).cmds)
|
|
762
|
+
if (c.t === 0 && c.meta === "!COLOUR") {
|
|
763
|
+
const t = c.text.split(/\s+/);
|
|
764
|
+
const g = (k) => t[t.indexOf(k) + 1];
|
|
765
|
+
out.set(+g("CODE"), { name: t[1], hex: g("VALUE"), edge: g("EDGE"), alpha: t.includes("ALPHA") ? +g("ALPHA") : 255 });
|
|
766
|
+
}
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
mesh(name) {
|
|
770
|
+
name = norm(name);
|
|
771
|
+
const hit = this.meshCache.get(name);
|
|
772
|
+
if (hit)
|
|
773
|
+
return hit;
|
|
774
|
+
const pos = [], col = [], edges = [], cond = [];
|
|
775
|
+
const r = this.where(name);
|
|
776
|
+
if (r)
|
|
777
|
+
this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond);
|
|
778
|
+
const m = { name: r?.doc.main ?? name, pos: new Float32Array(pos), col: new Uint32Array(col), edges: new Float32Array(edges), cond: new Float32Array(cond), nrm: smoothNormals(pos, edges, cond) };
|
|
779
|
+
this.meshCache.set(name, m);
|
|
780
|
+
return m;
|
|
781
|
+
}
|
|
782
|
+
emit(doc, fname, m, color, inv, clip, pos, col, edges, cond) {
|
|
783
|
+
const f = doc.files.get(fname);
|
|
784
|
+
if (!f)
|
|
785
|
+
return;
|
|
786
|
+
let ccw = true, invertNext = false, clipLocal = clip;
|
|
787
|
+
const flip = inv !== det(m) < 0;
|
|
788
|
+
const cc = (c) => c === 16 ? color : c;
|
|
789
|
+
for (const c of f.cmds) {
|
|
790
|
+
if (c.t === 0) {
|
|
791
|
+
if (c.bfc) {
|
|
792
|
+
if (c.bfc.includes("CW") && !c.bfc.includes("CCW"))
|
|
793
|
+
ccw = false;
|
|
794
|
+
if (c.bfc.includes("CCW"))
|
|
795
|
+
ccw = true;
|
|
796
|
+
if (c.bfc.includes("INVERTNEXT"))
|
|
797
|
+
invertNext = true;
|
|
798
|
+
if (/\bNOCLIP\b/.test(c.bfc))
|
|
799
|
+
clipLocal = false;
|
|
800
|
+
else if (/\bCLIP\b/.test(c.bfc))
|
|
801
|
+
clipLocal = true;
|
|
802
|
+
}
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (c.t === 1) {
|
|
806
|
+
const sub = doc.files.has(c.file) ? doc : this.where(c.file)?.doc;
|
|
807
|
+
if (sub)
|
|
808
|
+
this.emit(sub, sub === doc ? c.file : sub.main, mul(m, c.m), cc(c.color), inv !== invertNext, clipLocal, pos, col, edges, cond);
|
|
809
|
+
invertNext = false;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const p = c.p, v = [];
|
|
813
|
+
for (let i = 0;i < p.length; i += 3)
|
|
814
|
+
v.push(xf(m, p[i], p[i + 1], p[i + 2]));
|
|
815
|
+
if (c.t === 2) {
|
|
816
|
+
edges.push(...v[0], ...v[1]);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (c.t === 5) {
|
|
820
|
+
cond.push(...v[0], ...v[1], ...v[2] ?? [0, 0, 0], ...v[3] ?? [0, 0, 0]);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const ordered = ccw !== flip ? v : [...v].reverse();
|
|
824
|
+
const tri = (a, b, d) => {
|
|
825
|
+
pos.push(...a, ...b, ...d);
|
|
826
|
+
col.push(cc(c.color));
|
|
827
|
+
};
|
|
828
|
+
const emitFace = (w) => {
|
|
829
|
+
tri(w[0], w[1], w[2]);
|
|
830
|
+
if (c.t === 4)
|
|
831
|
+
tri(w[0], w[2], w[3]);
|
|
832
|
+
};
|
|
833
|
+
emitFace(ordered);
|
|
834
|
+
if (!clipLocal)
|
|
835
|
+
emitFace([...ordered].reverse());
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
flatten(doc) {
|
|
839
|
+
const out = [];
|
|
840
|
+
const walk = (d, fname, m, color, path, stepPath) => {
|
|
841
|
+
const f = d.files.get(fname);
|
|
842
|
+
if (!f)
|
|
843
|
+
return;
|
|
844
|
+
let s = 0;
|
|
845
|
+
for (const c of f.cmds) {
|
|
846
|
+
if (c.t === 0 && c.step)
|
|
847
|
+
s++;
|
|
848
|
+
if (c.t !== 1)
|
|
849
|
+
continue;
|
|
850
|
+
const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
|
|
851
|
+
if (d.files.has(c.file)) {
|
|
852
|
+
walk(d, c.file, mm, col, path + "/" + c.file, sp);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const r = this.where(c.file);
|
|
856
|
+
if (r?.kind === "model")
|
|
857
|
+
walk(r.doc, r.doc.main, mm, col, path + "/" + c.file, sp);
|
|
858
|
+
else {
|
|
859
|
+
const part = r ? r.doc.main : c.file;
|
|
860
|
+
out.push({ part, color: col, m: mm, step: sp[0], localStep: s, steps: sp, path, missing: !r, aliasOf: r?.aliasOf });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
walk(doc, doc.main, I, 16, doc.main, []);
|
|
865
|
+
return out;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// src/node/build-pack.ts
|
|
869
|
+
import fs3 from "node:fs";
|
|
870
|
+
import path3 from "node:path";
|
|
871
|
+
|
|
872
|
+
// src/node/resolver.ts
|
|
873
|
+
import fs from "node:fs";
|
|
874
|
+
import path from "node:path";
|
|
875
|
+
var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
|
|
876
|
+
var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
|
|
877
|
+
var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
|
|
878
|
+
var threeName = (ref) => {
|
|
879
|
+
const n = norm2(ref);
|
|
880
|
+
if (n.startsWith("s/"))
|
|
881
|
+
return "parts/" + n;
|
|
882
|
+
if (n.startsWith("48/"))
|
|
883
|
+
return "p/" + n;
|
|
884
|
+
return n;
|
|
885
|
+
};
|
|
886
|
+
function parseType1(line) {
|
|
887
|
+
const t = line.trim().split(/\s+/);
|
|
888
|
+
if (t[0] !== "1" || t.length < 15)
|
|
889
|
+
return null;
|
|
890
|
+
const v = t.slice(2, 14).map(Number);
|
|
891
|
+
return { color: t[1], m: v, name: t.slice(14).join(" ") };
|
|
892
|
+
}
|
|
893
|
+
function xf2(m, p) {
|
|
894
|
+
return [
|
|
895
|
+
m[0] + m[3] * p[0] + m[4] * p[1] + m[5] * p[2],
|
|
896
|
+
m[1] + m[6] * p[0] + m[7] * p[1] + m[8] * p[2],
|
|
897
|
+
m[2] + m[9] * p[0] + m[10] * p[1] + m[11] * p[2]
|
|
898
|
+
];
|
|
899
|
+
}
|
|
900
|
+
function compose(a, b) {
|
|
901
|
+
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];
|
|
902
|
+
const t = xf2(a, [b[0], b[1], b[2]]);
|
|
903
|
+
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)];
|
|
904
|
+
}
|
|
905
|
+
var IDENT = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
906
|
+
|
|
907
|
+
class Library2 {
|
|
908
|
+
root;
|
|
909
|
+
index;
|
|
910
|
+
text;
|
|
911
|
+
stats;
|
|
912
|
+
constructor(root) {
|
|
913
|
+
this.root = root;
|
|
914
|
+
this.index = new Map;
|
|
915
|
+
this.text = new Map;
|
|
916
|
+
this.stats = new Map;
|
|
917
|
+
for (const d of ["parts", "p", "models"])
|
|
918
|
+
this.#walk(path.join(root, d), d);
|
|
919
|
+
}
|
|
920
|
+
#walk(dir, rel) {
|
|
921
|
+
if (!fs.existsSync(dir))
|
|
922
|
+
return;
|
|
923
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
924
|
+
const r = rel + "/" + e.name;
|
|
925
|
+
if (e.isDirectory())
|
|
926
|
+
this.#walk(path.join(dir, e.name), r);
|
|
927
|
+
else
|
|
928
|
+
this.index.set(r.toLowerCase(), path.join(dir, e.name));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
resolve(ref) {
|
|
932
|
+
const n = norm2(ref);
|
|
933
|
+
for (const pre of ["parts/", "p/", "models/"])
|
|
934
|
+
if (this.index.has(pre + n))
|
|
935
|
+
return pre + n;
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
read(key) {
|
|
939
|
+
if (!this.text.has(key))
|
|
940
|
+
this.text.set(key, fs.readFileSync(this.index.get(key), "latin1"));
|
|
941
|
+
return this.text.get(key);
|
|
942
|
+
}
|
|
943
|
+
isPart(key) {
|
|
944
|
+
return key.startsWith("parts/") && !key.startsWith("parts/s/");
|
|
945
|
+
}
|
|
946
|
+
fileStats(key) {
|
|
947
|
+
if (this.stats.has(key))
|
|
948
|
+
return this.stats.get(key);
|
|
949
|
+
const s = { tris: 0, studTris: 0, underTris: 0, studs: [], min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity], missing: new Set };
|
|
950
|
+
this.stats.set(key, s);
|
|
951
|
+
const base = path.basename(key);
|
|
952
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
953
|
+
const grow = (p) => {
|
|
954
|
+
for (let i = 0;i < 3; i++) {
|
|
955
|
+
if (p[i] < s.min[i])
|
|
956
|
+
s.min[i] = p[i];
|
|
957
|
+
if (p[i] > s.max[i])
|
|
958
|
+
s.max[i] = p[i];
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
for (const line of this.read(key).split(/\r?\n/)) {
|
|
962
|
+
const t = line.trim();
|
|
963
|
+
const c = t[0];
|
|
964
|
+
if (c === "3" || c === "4") {
|
|
965
|
+
const v = t.split(/\s+/).slice(2).map(Number);
|
|
966
|
+
const nt = c === "3" ? 1 : 2;
|
|
967
|
+
s.tris += nt;
|
|
968
|
+
for (let i = 0;i + 2 < v.length; i += 3)
|
|
969
|
+
grow([v[i], v[i + 1], v[i + 2]]);
|
|
970
|
+
} else if (c === "1") {
|
|
971
|
+
const r = parseType1(t);
|
|
972
|
+
if (!r)
|
|
973
|
+
continue;
|
|
974
|
+
const ck = this.resolve(r.name);
|
|
975
|
+
if (!ck) {
|
|
976
|
+
s.missing.add(norm2(r.name));
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const cs = this.fileStats(ck);
|
|
980
|
+
s.tris += cs.tris;
|
|
981
|
+
s.studTris += cs.studTris;
|
|
982
|
+
s.underTris += cs.underTris;
|
|
983
|
+
for (const m of cs.missing)
|
|
984
|
+
s.missing.add(m);
|
|
985
|
+
if (TOP_STUD_RE.test(path.basename(ck)))
|
|
986
|
+
s.studs.push([r.m[0], r.m[1], r.m[2]]);
|
|
987
|
+
else
|
|
988
|
+
for (const p of cs.studs)
|
|
989
|
+
s.studs.push(xf2(r.m, p));
|
|
990
|
+
if (isFinite(cs.min[0])) {
|
|
991
|
+
for (const x of [cs.min[0], cs.max[0]])
|
|
992
|
+
for (const y of [cs.min[1], cs.max[1]])
|
|
993
|
+
for (const z of [cs.min[2], cs.max[2]])
|
|
994
|
+
grow(xf2(r.m, [x, y, z]));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (isTop) {
|
|
999
|
+
s.studTris = s.tris;
|
|
1000
|
+
}
|
|
1001
|
+
if (isUnder) {
|
|
1002
|
+
s.underTris = s.tris;
|
|
1003
|
+
}
|
|
1004
|
+
return s;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function splitModel(text, fallbackName = "main.ldr") {
|
|
1008
|
+
const files = new Map;
|
|
1009
|
+
let cur = null, buf = [], main = null;
|
|
1010
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1011
|
+
const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
|
|
1012
|
+
if (m) {
|
|
1013
|
+
if (cur)
|
|
1014
|
+
files.set(cur, buf.join(`
|
|
1015
|
+
`));
|
|
1016
|
+
cur = norm2(m[1]);
|
|
1017
|
+
buf = [];
|
|
1018
|
+
if (!main)
|
|
1019
|
+
main = cur;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
if (/^\s*0\s+NOFILE/i.test(line))
|
|
1023
|
+
continue;
|
|
1024
|
+
if (!cur) {
|
|
1025
|
+
cur = norm2(fallbackName);
|
|
1026
|
+
main = cur;
|
|
1027
|
+
}
|
|
1028
|
+
buf.push(line);
|
|
1029
|
+
}
|
|
1030
|
+
if (cur)
|
|
1031
|
+
files.set(cur, buf.join(`
|
|
1032
|
+
`));
|
|
1033
|
+
return { files, main };
|
|
1034
|
+
}
|
|
1035
|
+
function flatten2(model, lib, onInstance) {
|
|
1036
|
+
const missing = new Map;
|
|
1037
|
+
const walk = (name, parentM, parentColor, depth) => {
|
|
1038
|
+
if (depth > 32)
|
|
1039
|
+
return;
|
|
1040
|
+
for (const line of model.files.get(name).split(`
|
|
1041
|
+
`)) {
|
|
1042
|
+
const r = parseType1(line);
|
|
1043
|
+
if (!r)
|
|
1044
|
+
continue;
|
|
1045
|
+
const color = r.color === "16" ? parentColor : r.color;
|
|
1046
|
+
const m = compose(parentM, r.m);
|
|
1047
|
+
const n = norm2(r.name);
|
|
1048
|
+
if (model.files.has(n)) {
|
|
1049
|
+
walk(n, m, color, depth + 1);
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const key = lib.resolve(r.name);
|
|
1053
|
+
if (!key) {
|
|
1054
|
+
missing.set(n, (missing.get(n) || 0) + 1);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
onInstance({ part: key, ref: n, color, m });
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
walk(model.main, IDENT, "16", 0);
|
|
1061
|
+
return { missing };
|
|
1062
|
+
}
|
|
1063
|
+
function collectDeps(lib, refs) {
|
|
1064
|
+
const out = new Map;
|
|
1065
|
+
const stack = [...refs];
|
|
1066
|
+
while (stack.length) {
|
|
1067
|
+
const ref = stack.pop();
|
|
1068
|
+
const tn = threeName(ref);
|
|
1069
|
+
if (out.has(tn))
|
|
1070
|
+
continue;
|
|
1071
|
+
const key = lib.resolve(ref);
|
|
1072
|
+
if (!key)
|
|
1073
|
+
continue;
|
|
1074
|
+
out.set(tn, key);
|
|
1075
|
+
for (const line of lib.read(key).split(/\r?\n/)) {
|
|
1076
|
+
const r = parseType1(line);
|
|
1077
|
+
if (r)
|
|
1078
|
+
stack.push(r.name);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return out;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/node/compile.ts
|
|
1085
|
+
import fs2 from "node:fs";
|
|
1086
|
+
import path2 from "node:path";
|
|
1087
|
+
function loadColors(root) {
|
|
1088
|
+
const map = new Map;
|
|
1089
|
+
for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
|
|
1090
|
+
const m = /!COLOUR\s+\S+\s+CODE\s+(\d+)\s+VALUE\s+#([0-9A-Fa-f]{6})/.exec(l);
|
|
1091
|
+
if (m) {
|
|
1092
|
+
const h = parseInt(m[2], 16);
|
|
1093
|
+
map.set(m[1], [(h >> 16) / 255, (h >> 8 & 255) / 255, (h & 255) / 255]);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return map;
|
|
1097
|
+
}
|
|
1098
|
+
var det3 = (m) => m[3] * (m[7] * m[11] - m[8] * m[10]) - m[4] * (m[6] * m[11] - m[8] * m[9]) + m[5] * (m[6] * m[10] - m[7] * m[9]);
|
|
1099
|
+
|
|
1100
|
+
class Compiler {
|
|
1101
|
+
lib;
|
|
1102
|
+
opts;
|
|
1103
|
+
colors;
|
|
1104
|
+
cache = new Map;
|
|
1105
|
+
constructor(lib, opts) {
|
|
1106
|
+
this.lib = lib;
|
|
1107
|
+
this.opts = opts;
|
|
1108
|
+
this.colors = loadColors(lib.root);
|
|
1109
|
+
}
|
|
1110
|
+
resolve(ref, isStud) {
|
|
1111
|
+
const key = this.lib.resolve(ref);
|
|
1112
|
+
if (!key)
|
|
1113
|
+
return null;
|
|
1114
|
+
const low = isStud ? this.opts.studs === "low" : this.opts.prims === "low";
|
|
1115
|
+
if (low && key.startsWith("p/") && !key.startsWith("p/8/") && !key.startsWith("p/48/")) {
|
|
1116
|
+
const alt = "p/8/" + key.slice(2);
|
|
1117
|
+
if (this.lib.index.has(alt))
|
|
1118
|
+
return alt;
|
|
1119
|
+
}
|
|
1120
|
+
if (low && key.startsWith("p/48/")) {
|
|
1121
|
+
const alt = "p/8/" + key.slice(5);
|
|
1122
|
+
if (this.lib.index.has(alt))
|
|
1123
|
+
return alt;
|
|
1124
|
+
const a2 = "p/" + key.slice(5);
|
|
1125
|
+
if (this.lib.index.has(a2))
|
|
1126
|
+
return a2;
|
|
1127
|
+
}
|
|
1128
|
+
return key;
|
|
1129
|
+
}
|
|
1130
|
+
compile(key, separateStuds = this.opts.separateStuds) {
|
|
1131
|
+
const ck = key + (separateStuds ? "" : "#nostudsep");
|
|
1132
|
+
if (this.cache.has(ck))
|
|
1133
|
+
return this.cache.get(ck);
|
|
1134
|
+
const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
|
|
1135
|
+
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];
|
|
1136
|
+
const walk = (fk, M, color, invert, target, depth) => {
|
|
1137
|
+
if (depth > 40)
|
|
1138
|
+
return;
|
|
1139
|
+
let ccw = true, certified = false, invertNext = false;
|
|
1140
|
+
for (const raw of this.lib.read(fk).split(/\r?\n/)) {
|
|
1141
|
+
const t = raw.trim();
|
|
1142
|
+
if (!t)
|
|
1143
|
+
continue;
|
|
1144
|
+
const c = t[0];
|
|
1145
|
+
if (c === "0") {
|
|
1146
|
+
if (/^0\s+BFC/.test(t)) {
|
|
1147
|
+
if (/CERTIFY/.test(t) && !/NOCERTIFY/.test(t))
|
|
1148
|
+
certified = true;
|
|
1149
|
+
if (/\bCW\b/.test(t))
|
|
1150
|
+
ccw = false;
|
|
1151
|
+
if (/\bCCW\b/.test(t))
|
|
1152
|
+
ccw = true;
|
|
1153
|
+
if (/INVERTNEXT/.test(t))
|
|
1154
|
+
invertNext = true;
|
|
1155
|
+
}
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (c === "1") {
|
|
1159
|
+
const r = parseType1(t);
|
|
1160
|
+
if (!r)
|
|
1161
|
+
continue;
|
|
1162
|
+
const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
|
|
1163
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
1164
|
+
const ck2 = this.resolve(r.name, isTop);
|
|
1165
|
+
if (!ck2) {
|
|
1166
|
+
invertNext = false;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const childColor = r.color === "16" ? color : r.color;
|
|
1170
|
+
const Mc = compose(M, r.m);
|
|
1171
|
+
const inv = invert !== invertNext !== det3(r.m) < 0;
|
|
1172
|
+
invertNext = false;
|
|
1173
|
+
if (isTop && separateStuds) {
|
|
1174
|
+
out.studs.push({ kind: ck2, color: childColor, m: Mc });
|
|
1175
|
+
growBox(out.bbox, Mc, this.compile(ck2, false).bbox);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
walk(ck2, Mc, childColor, inv, isUnder ? "under" : target, depth + 1);
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const tok = t.split(/\s+/);
|
|
1182
|
+
if (c === "2") {
|
|
1183
|
+
const v = tok.slice(2, 8).map(Number);
|
|
1184
|
+
out.edges.push(...xf2(M, v.slice(0, 3)), ...xf2(M, v.slice(3, 6)));
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
if (c === "5") {
|
|
1188
|
+
out.condLines++;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (c === "3" || c === "4") {
|
|
1192
|
+
const code = tok[1] === "16" ? color : tok[1];
|
|
1193
|
+
const n = c === "3" ? 3 : 4;
|
|
1194
|
+
const v = tok.slice(2, 2 + n * 3).map(Number);
|
|
1195
|
+
const pts = [];
|
|
1196
|
+
for (let i = 0;i < n; i++)
|
|
1197
|
+
pts.push(xf2(M, v.slice(i * 3, i * 3 + 3)));
|
|
1198
|
+
for (const p of pts)
|
|
1199
|
+
grow(out.bbox, p);
|
|
1200
|
+
let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
|
|
1201
|
+
if (!ccw !== invert)
|
|
1202
|
+
order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
|
|
1203
|
+
const rgba = colorOf(code);
|
|
1204
|
+
const b = out[target];
|
|
1205
|
+
for (const i of order) {
|
|
1206
|
+
b.pos.push(...pts[i]);
|
|
1207
|
+
b.col.push(...rgba);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (!certified)
|
|
1212
|
+
out.uncertified = true;
|
|
1213
|
+
};
|
|
1214
|
+
walk(key, IDENT, "16", false, "body", 0);
|
|
1215
|
+
const [x0, y0, z0, x1, y1, z1] = out.bbox;
|
|
1216
|
+
let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
|
|
1217
|
+
for (const b of [out.body, out.under])
|
|
1218
|
+
for (let i = 0;i < b.pos.length; i += 3)
|
|
1219
|
+
if (b.pos[i + 1] > y1 - 0.5) {
|
|
1220
|
+
ax0 = Math.min(ax0, b.pos[i]);
|
|
1221
|
+
ax1 = Math.max(ax1, b.pos[i]);
|
|
1222
|
+
az0 = Math.min(az0, b.pos[i + 2]);
|
|
1223
|
+
az1 = Math.max(az1, b.pos[i + 2]);
|
|
1224
|
+
}
|
|
1225
|
+
out.fullBottom = isFinite(x0) && ax0 <= x0 + 0.5 && ax1 >= x1 - 0.5 && az0 <= z0 + 0.5 && az1 >= z1 - 0.5;
|
|
1226
|
+
if (separateStuds && isFinite(x0))
|
|
1227
|
+
out.bottomMask = enclosureMask(out);
|
|
1228
|
+
this.cache.set(ck, out);
|
|
1229
|
+
return out;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
function enclosureMask(c) {
|
|
1233
|
+
const [x0, , z0, x1, y1, z1] = c.bbox;
|
|
1234
|
+
const nx = Math.round((x1 - x0) / 20), nz = Math.round((z1 - z0) / 20);
|
|
1235
|
+
if (!nx || !nz || nx * nz > 4096)
|
|
1236
|
+
return;
|
|
1237
|
+
const tris = [];
|
|
1238
|
+
for (const b of [c.body, c.under])
|
|
1239
|
+
for (let i = 0;i < b.pos.length; i += 9) {
|
|
1240
|
+
const t = b.pos.slice(i, i + 9);
|
|
1241
|
+
if (Math.max(t[1], t[4], t[7]) < y1 - 6)
|
|
1242
|
+
continue;
|
|
1243
|
+
tris.push(t);
|
|
1244
|
+
}
|
|
1245
|
+
let yRay = y1 - 2;
|
|
1246
|
+
const bits = [];
|
|
1247
|
+
const BIN = 4;
|
|
1248
|
+
const buckets = [new Map, new Map];
|
|
1249
|
+
for (const t of tris) {
|
|
1250
|
+
if (Math.min(t[1], t[4], t[7]) > y1 - 0.5 || Math.max(t[1], t[4], t[7]) < y1 - 4)
|
|
1251
|
+
continue;
|
|
1252
|
+
for (const [ai, o] of [[0, 2], [1, 0]]) {
|
|
1253
|
+
const lo = Math.floor(Math.min(t[o], t[3 + o], t[6 + o]) / BIN), hi = Math.floor(Math.max(t[o], t[3 + o], t[6 + o]) / BIN);
|
|
1254
|
+
for (let k = lo;k <= hi; k++)
|
|
1255
|
+
(buckets[ai].get(k) || buckets[ai].set(k, []).get(k)).push(t);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const hit = (px, pz, axis, sign) => {
|
|
1259
|
+
const o = axis === 0 ? 2 : 0;
|
|
1260
|
+
const pv = axis === 0 ? pz : px;
|
|
1261
|
+
for (const t of buckets[axis === 0 ? 0 : 1].get(Math.floor(pv / BIN)) || []) {
|
|
1262
|
+
const a = [t[1], t[o]], b = [t[4], t[3 + o]], d = [t[7], t[6 + o]];
|
|
1263
|
+
const den = (b[1] - d[1]) * (a[0] - d[0]) + (d[0] - b[0]) * (a[1] - d[1]);
|
|
1264
|
+
if (Math.abs(den) < 0.000000001)
|
|
1265
|
+
continue;
|
|
1266
|
+
const y = yRay;
|
|
1267
|
+
const w1 = ((b[1] - d[1]) * (y - d[0]) + (d[0] - b[0]) * (pv - d[1])) / den, w2 = ((d[1] - a[1]) * (y - d[0]) + (a[0] - d[0]) * (pv - d[1])) / den, w3 = 1 - w1 - w2;
|
|
1268
|
+
if (w1 < -0.000001 || w2 < -0.000001 || w3 < -0.000001)
|
|
1269
|
+
continue;
|
|
1270
|
+
const along = w1 * t[axis] + w2 * t[3 + axis] + w3 * t[6 + axis];
|
|
1271
|
+
const start = axis === 0 ? px : pz;
|
|
1272
|
+
if ((along - start) * sign > 0.01)
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
};
|
|
1277
|
+
for (let a = 0;a < nx; a++)
|
|
1278
|
+
for (let b = 0;b < nz; b++) {
|
|
1279
|
+
const px = x0 + 10 + 20 * a, pz = z0 + 10 + 20 * b;
|
|
1280
|
+
let ok = true;
|
|
1281
|
+
for (const yy of [y1 - 1, y1 - 3.5]) {
|
|
1282
|
+
yRay = yy;
|
|
1283
|
+
for (const [ox, oz] of [[0, 0], [5, 5], [-5, 5], [5, -5], [-5, -5]]) {
|
|
1284
|
+
const qx = px + ox, qz = pz + oz;
|
|
1285
|
+
if (!(hit(qx, qz, 0, 1) && hit(qx, qz, 0, -1) && hit(qx, qz, 2, 1) && hit(qx, qz, 2, -1))) {
|
|
1286
|
+
ok = false;
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (!ok)
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
bits.push(ok ? 1 : 0);
|
|
1294
|
+
}
|
|
1295
|
+
return { nx, nz, bits };
|
|
1296
|
+
}
|
|
1297
|
+
function grow(b, p) {
|
|
1298
|
+
for (let i = 0;i < 3; i++) {
|
|
1299
|
+
if (p[i] < b[i])
|
|
1300
|
+
b[i] = p[i];
|
|
1301
|
+
if (p[i] > b[i + 3])
|
|
1302
|
+
b[i + 3] = p[i];
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function growBox(b, M, cb) {
|
|
1306
|
+
if (!isFinite(cb[0]))
|
|
1307
|
+
return;
|
|
1308
|
+
for (const x of [cb[0], cb[3]])
|
|
1309
|
+
for (const y of [cb[1], cb[4]])
|
|
1310
|
+
for (const z of [cb[2], cb[5]])
|
|
1311
|
+
grow(b, xf2(M, [x, y, z]));
|
|
1312
|
+
}
|
|
1313
|
+
function writePack(file, parts, studKinds, weld, extra) {
|
|
1314
|
+
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
|
|
1315
|
+
}
|
|
1316
|
+
function encodePack(parts, studKinds, weld, extra) {
|
|
1317
|
+
const chunks = [];
|
|
1318
|
+
let off = 0;
|
|
1319
|
+
const put = (a) => {
|
|
1320
|
+
const pad = (4 - off % 4) % 4;
|
|
1321
|
+
if (pad) {
|
|
1322
|
+
chunks.push(new Uint8Array(pad));
|
|
1323
|
+
off += pad;
|
|
1324
|
+
}
|
|
1325
|
+
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
|
|
1326
|
+
chunks.push(a);
|
|
1327
|
+
off += a.byteLength;
|
|
1328
|
+
return r;
|
|
1329
|
+
};
|
|
1330
|
+
const bucket = (b) => {
|
|
1331
|
+
const n = b.pos.length / 3;
|
|
1332
|
+
if (!n)
|
|
1333
|
+
return null;
|
|
1334
|
+
const nrm = new Float32Array(n * 3);
|
|
1335
|
+
for (let i = 0;i < n; i += 3) {
|
|
1336
|
+
const p = b.pos;
|
|
1337
|
+
const a = i * 3;
|
|
1338
|
+
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];
|
|
1339
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
1340
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
1341
|
+
nx /= l;
|
|
1342
|
+
ny /= l;
|
|
1343
|
+
nz /= l;
|
|
1344
|
+
for (let k = 0;k < 3; k++) {
|
|
1345
|
+
nrm[(i + k) * 3] = nx;
|
|
1346
|
+
nrm[(i + k) * 3 + 1] = ny;
|
|
1347
|
+
nrm[(i + k) * 3 + 2] = nz;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (!weld)
|
|
1351
|
+
return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
|
|
1352
|
+
const map = new Map;
|
|
1353
|
+
const P = [], N = [], C = [], I = [];
|
|
1354
|
+
for (let i = 0;i < n; i++) {
|
|
1355
|
+
const k = `${b.pos[i * 3].toFixed(3)},${b.pos[i * 3 + 1].toFixed(3)},${b.pos[i * 3 + 2].toFixed(3)},${nrm[i * 3].toFixed(3)},${nrm[i * 3 + 1].toFixed(3)},${nrm[i * 3 + 2].toFixed(3)},${b.col.slice(i * 4, i * 4 + 4).join()}`;
|
|
1356
|
+
let j = map.get(k);
|
|
1357
|
+
if (j === undefined) {
|
|
1358
|
+
j = P.length / 3;
|
|
1359
|
+
map.set(k, j);
|
|
1360
|
+
P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
|
|
1361
|
+
N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
|
|
1362
|
+
C.push(...b.col.slice(i * 4, i * 4 + 4));
|
|
1363
|
+
}
|
|
1364
|
+
I.push(j);
|
|
1365
|
+
}
|
|
1366
|
+
const vcount = P.length / 3;
|
|
1367
|
+
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 };
|
|
1368
|
+
};
|
|
1369
|
+
const enc = (c) => ({ body: bucket(c.body), under: bucket(c.under), edges: c.edges.length ? put(new Float32Array(c.edges)) : null, cond: c.condLines, 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 });
|
|
1370
|
+
const header = { version: 1, weld, parts: Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), studKinds: Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)])), ...extra };
|
|
1371
|
+
const hj = new TextEncoder().encode(JSON.stringify(header));
|
|
1372
|
+
const hpad = (4 - (4 + hj.length) % 4) % 4;
|
|
1373
|
+
const head = new Uint8Array(4 + hj.length + hpad);
|
|
1374
|
+
new DataView(head.buffer).setUint32(0, hj.length + hpad, true);
|
|
1375
|
+
head.set(hj, 4);
|
|
1376
|
+
head.fill(32, 4 + hj.length);
|
|
1377
|
+
return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/node/build-pack.ts
|
|
1381
|
+
function buildPack(o) {
|
|
1382
|
+
const lib = new Library2(o.ldrawRoot);
|
|
1383
|
+
const refs = new Map;
|
|
1384
|
+
const missing = new Set;
|
|
1385
|
+
for (const m of o.models ?? []) {
|
|
1386
|
+
const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
|
|
1387
|
+
const r = flatten2(splitModel(text, name), lib, (i) => refs.set(i.ref, i.part));
|
|
1388
|
+
for (const k of r.missing.keys())
|
|
1389
|
+
missing.add(k);
|
|
1390
|
+
}
|
|
1391
|
+
for (const p of o.parts ?? []) {
|
|
1392
|
+
const k = lib.resolve(p);
|
|
1393
|
+
if (k)
|
|
1394
|
+
refs.set(norm2(p), k);
|
|
1395
|
+
else
|
|
1396
|
+
missing.add(norm2(p));
|
|
1397
|
+
}
|
|
1398
|
+
if (o.allParts) {
|
|
1399
|
+
for (const k of lib.index.keys())
|
|
1400
|
+
if (lib.isPart(k) && /\.dat$/.test(k))
|
|
1401
|
+
refs.set(k.slice(6), k);
|
|
1402
|
+
}
|
|
1403
|
+
const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
|
|
1404
|
+
const parts = new Map, kinds = new Map;
|
|
1405
|
+
let n = 0;
|
|
1406
|
+
for (const [ref, key] of refs) {
|
|
1407
|
+
try {
|
|
1408
|
+
const c = comp.compile(key);
|
|
1409
|
+
parts.set(ref, c);
|
|
1410
|
+
for (const s of c.studs)
|
|
1411
|
+
if (!kinds.has(s.kind))
|
|
1412
|
+
kinds.set(s.kind, comp.compile(s.kind, false));
|
|
1413
|
+
} catch {
|
|
1414
|
+
missing.add(ref);
|
|
1415
|
+
}
|
|
1416
|
+
o.onProgress?.(++n, refs.size);
|
|
1417
|
+
}
|
|
1418
|
+
let extra;
|
|
1419
|
+
if (o.colors !== false) {
|
|
1420
|
+
const cfg = path3.join(o.ldrawRoot, "LDConfig.ldr");
|
|
1421
|
+
if (fs3.existsSync(cfg))
|
|
1422
|
+
extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
|
|
1423
|
+
}
|
|
1424
|
+
return { data: encodePack(parts, kinds, comp.opts.weld, extra), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
|
|
1425
|
+
}
|
|
1426
|
+
export {
|
|
1427
|
+
Compiler,
|
|
1428
|
+
I as IDENTITY,
|
|
1429
|
+
Library,
|
|
1430
|
+
Library2 as ResolverLibrary,
|
|
1431
|
+
buildLdrScene,
|
|
1432
|
+
buildPack,
|
|
1433
|
+
collectDeps,
|
|
1434
|
+
computeCulling,
|
|
1435
|
+
createInstancedEdges,
|
|
1436
|
+
createLdrObject,
|
|
1437
|
+
decodePack,
|
|
1438
|
+
decompressPack,
|
|
1439
|
+
encodePack,
|
|
1440
|
+
flatten,
|
|
1441
|
+
flatten2 as flattenWithLibrary,
|
|
1442
|
+
isDirectColor,
|
|
1443
|
+
loadColors,
|
|
1444
|
+
loadPack,
|
|
1445
|
+
makeMaterial,
|
|
1446
|
+
mul,
|
|
1447
|
+
norm,
|
|
1448
|
+
parse,
|
|
1449
|
+
parseColor,
|
|
1450
|
+
parseLDConfig,
|
|
1451
|
+
parseModel,
|
|
1452
|
+
sequenceSteps,
|
|
1453
|
+
smoothNormals,
|
|
1454
|
+
splitModel,
|
|
1455
|
+
writePack
|
|
1456
|
+
};
|