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
package/dist/index.js
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
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
|
+
export {
|
|
698
|
+
I as IDENTITY,
|
|
699
|
+
buildLdrScene,
|
|
700
|
+
computeCulling,
|
|
701
|
+
createInstancedEdges,
|
|
702
|
+
createLdrObject,
|
|
703
|
+
decodePack,
|
|
704
|
+
decompressPack,
|
|
705
|
+
flatten,
|
|
706
|
+
isDirectColor,
|
|
707
|
+
loadPack,
|
|
708
|
+
makeMaterial,
|
|
709
|
+
mul,
|
|
710
|
+
norm,
|
|
711
|
+
parse,
|
|
712
|
+
parseColor,
|
|
713
|
+
parseLDConfig,
|
|
714
|
+
parseModel,
|
|
715
|
+
sequenceSteps,
|
|
716
|
+
smoothNormals
|
|
717
|
+
};
|