ldr 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/cli.js +784 -188
- package/dist/index.d.ts +3 -3
- package/dist/index.js +554 -32
- package/dist/node/build-pack.d.ts +6 -0
- package/dist/node/compile.d.ts +29 -2
- package/dist/node/index.js +969 -71
- package/dist/node/library.d.ts +9 -2
- package/dist/node/resolver.d.ts +1 -0
- package/dist/render.d.ts +26 -0
- package/dist/scene.d.ts +18 -0
- package/dist/spec.d.ts +83 -7
- package/package.json +1 -1
package/dist/node/index.js
CHANGED
|
@@ -6,15 +6,139 @@ function parseColor(tok) {
|
|
|
6
6
|
return +tok;
|
|
7
7
|
}
|
|
8
8
|
var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
|
|
9
|
+
function texNames(rest) {
|
|
10
|
+
const out = [];
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < rest.length) {
|
|
13
|
+
while (rest[i] === " " || rest[i] === "\t")
|
|
14
|
+
i++;
|
|
15
|
+
if (i >= rest.length)
|
|
16
|
+
break;
|
|
17
|
+
if (rest[i] === '"') {
|
|
18
|
+
let v = "";
|
|
19
|
+
i++;
|
|
20
|
+
while (i < rest.length && rest[i] !== '"') {
|
|
21
|
+
if (rest[i] === "\\" && i + 1 < rest.length)
|
|
22
|
+
i++;
|
|
23
|
+
v += rest[i++];
|
|
24
|
+
}
|
|
25
|
+
i++;
|
|
26
|
+
out.push(v);
|
|
27
|
+
} else {
|
|
28
|
+
let j = i;
|
|
29
|
+
while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
|
|
30
|
+
j++;
|
|
31
|
+
out.push(rest.slice(i, j));
|
|
32
|
+
i = j;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function parseTexMap(tail, next = false) {
|
|
38
|
+
const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
|
|
39
|
+
if (!m)
|
|
40
|
+
return null;
|
|
41
|
+
const method = m[1].toUpperCase();
|
|
42
|
+
const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
|
|
43
|
+
if (!n)
|
|
44
|
+
return null;
|
|
45
|
+
const parts = m[2].trim().split(/\s+/);
|
|
46
|
+
if (parts.length < n + 1)
|
|
47
|
+
return null;
|
|
48
|
+
const v = parts.slice(0, n).map(Number);
|
|
49
|
+
if (!v.every(Number.isFinite))
|
|
50
|
+
return null;
|
|
51
|
+
let rest = m[2].trim();
|
|
52
|
+
for (let k = 0;k < n; k++)
|
|
53
|
+
rest = rest.replace(/^\S+\s*/, "");
|
|
54
|
+
const names = texNames(rest);
|
|
55
|
+
if (!names.length)
|
|
56
|
+
return null;
|
|
57
|
+
const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
|
|
58
|
+
if (n >= 10)
|
|
59
|
+
t.a = v[9];
|
|
60
|
+
if (n === 11)
|
|
61
|
+
t.b = v[10];
|
|
62
|
+
const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
|
|
63
|
+
if (g > 0 && names[g + 1])
|
|
64
|
+
t.glossmap = names[g + 1];
|
|
65
|
+
if (next)
|
|
66
|
+
t.next = true;
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
69
|
+
var b64 = (s) => {
|
|
70
|
+
const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
|
|
71
|
+
const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
|
|
72
|
+
const bin = atob(pad);
|
|
73
|
+
const out = new Uint8Array(bin.length);
|
|
74
|
+
for (let i = 0;i < bin.length; i++)
|
|
75
|
+
out[i] = bin.charCodeAt(i);
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
78
|
+
function geomLine(tk) {
|
|
79
|
+
const t = +tk[0];
|
|
80
|
+
if (!(t >= 1 && t <= 5) || tk.length < 2)
|
|
81
|
+
return null;
|
|
82
|
+
const color = parseColor(tk[1]);
|
|
83
|
+
const nums = tk.slice(2).map(Number);
|
|
84
|
+
if (t === 1)
|
|
85
|
+
return { t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) };
|
|
86
|
+
if (t === 2)
|
|
87
|
+
return { t: 2, color, p: nums.slice(0, 6) };
|
|
88
|
+
if (t === 3)
|
|
89
|
+
return { t: 3, color, p: nums.slice(0, 9) };
|
|
90
|
+
if (t === 4)
|
|
91
|
+
return { t: 4, color, p: nums.slice(0, 12) };
|
|
92
|
+
return { t: 5, color, p: nums.slice(0, 12) };
|
|
93
|
+
}
|
|
9
94
|
function parse(src, name = "main.ldr") {
|
|
10
95
|
const files = new Map;
|
|
11
96
|
let cur = null;
|
|
12
97
|
let main = "";
|
|
98
|
+
let data;
|
|
99
|
+
let dataCur = null;
|
|
100
|
+
let texStack = [];
|
|
101
|
+
let texNext = null;
|
|
102
|
+
const closeData = () => {
|
|
103
|
+
if (!dataCur)
|
|
104
|
+
return;
|
|
105
|
+
const bytes = b64(dataCur.chunks.join(""));
|
|
106
|
+
const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
107
|
+
(data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
|
|
108
|
+
dataCur = null;
|
|
109
|
+
};
|
|
13
110
|
const start = (n) => {
|
|
111
|
+
closeData();
|
|
14
112
|
cur = { name: norm(n), cmds: [] };
|
|
15
113
|
files.set(cur.name, cur);
|
|
16
114
|
if (!main)
|
|
17
115
|
main = cur.name;
|
|
116
|
+
texStack = [];
|
|
117
|
+
texNext = null;
|
|
118
|
+
};
|
|
119
|
+
let skip = false, sawBlock = false;
|
|
120
|
+
const firstBlock = () => {
|
|
121
|
+
if (sawBlock)
|
|
122
|
+
return;
|
|
123
|
+
sawBlock = true;
|
|
124
|
+
files.clear();
|
|
125
|
+
cur = null;
|
|
126
|
+
main = "";
|
|
127
|
+
};
|
|
128
|
+
const annotate = (c, fromColon) => {
|
|
129
|
+
if (texNext) {
|
|
130
|
+
c.tex = texNext;
|
|
131
|
+
texNext = null;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const top = texStack[texStack.length - 1];
|
|
135
|
+
if (!top)
|
|
136
|
+
return;
|
|
137
|
+
if (top.fallback) {
|
|
138
|
+
if (!fromColon)
|
|
139
|
+
c.fallback = true;
|
|
140
|
+
} else
|
|
141
|
+
c.tex = top.map;
|
|
18
142
|
};
|
|
19
143
|
for (const raw of src.split(/\r?\n/)) {
|
|
20
144
|
const line = raw.trim();
|
|
@@ -25,19 +149,40 @@ function parse(src, name = "main.ldr") {
|
|
|
25
149
|
if (t === 0) {
|
|
26
150
|
const text = line.slice(1).trim();
|
|
27
151
|
if (tk[1] === "FILE") {
|
|
152
|
+
firstBlock();
|
|
153
|
+
skip = false;
|
|
28
154
|
start(text.slice(5));
|
|
29
155
|
continue;
|
|
30
156
|
}
|
|
31
157
|
if (tk[1] === "NOFILE") {
|
|
158
|
+
closeData();
|
|
32
159
|
cur = null;
|
|
160
|
+
skip = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (tk[1] === "!DATA") {
|
|
164
|
+
firstBlock();
|
|
165
|
+
closeData();
|
|
166
|
+
cur = null;
|
|
167
|
+
skip = false;
|
|
168
|
+
dataCur = { name: text.slice(5).trim(), chunks: [] };
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (skip)
|
|
172
|
+
continue;
|
|
173
|
+
if (dataCur) {
|
|
174
|
+
if (tk[1] === "!:")
|
|
175
|
+
dataCur.chunks.push(tk.slice(2).join(""));
|
|
33
176
|
continue;
|
|
34
177
|
}
|
|
35
178
|
if (!cur)
|
|
36
179
|
start(name);
|
|
37
180
|
const c = { t: 0, text };
|
|
38
|
-
if (tk[1] === "STEP")
|
|
181
|
+
if (tk[1] === "STEP") {
|
|
39
182
|
c.step = true;
|
|
40
|
-
|
|
183
|
+
texStack = [];
|
|
184
|
+
texNext = null;
|
|
185
|
+
} else if (tk[1] === "BFC")
|
|
41
186
|
c.bfc = tk.slice(2).join(" ");
|
|
42
187
|
else if (tk[1] === "ROTSTEP") {
|
|
43
188
|
c.meta = "ROTSTEP";
|
|
@@ -53,6 +198,37 @@ function parse(src, name = "main.ldr") {
|
|
|
53
198
|
c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
|
|
54
199
|
} else if (tk[1]?.startsWith("!"))
|
|
55
200
|
c.meta = tk[1];
|
|
201
|
+
if (tk[1] === "!TEXMAP") {
|
|
202
|
+
const op = (tk[2] ?? "").toUpperCase();
|
|
203
|
+
texNext = null;
|
|
204
|
+
if (op === "START" || op === "NEXT") {
|
|
205
|
+
const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
|
|
206
|
+
c.texmap = { op, ...map ? { map } : {} };
|
|
207
|
+
if (map) {
|
|
208
|
+
(cur.texmaps ??= []).push(map);
|
|
209
|
+
if (op === "START")
|
|
210
|
+
texStack.push({ map, fallback: false });
|
|
211
|
+
else
|
|
212
|
+
texNext = map;
|
|
213
|
+
}
|
|
214
|
+
} else if (op === "FALLBACK") {
|
|
215
|
+
c.texmap = { op };
|
|
216
|
+
const top = texStack[texStack.length - 1];
|
|
217
|
+
if (top)
|
|
218
|
+
top.fallback = true;
|
|
219
|
+
} else if (op === "END") {
|
|
220
|
+
c.texmap = { op };
|
|
221
|
+
texStack.pop();
|
|
222
|
+
}
|
|
223
|
+
} else if (tk[1] === "!:") {
|
|
224
|
+
texNext = null;
|
|
225
|
+
const g = geomLine(tk.slice(2));
|
|
226
|
+
if (g) {
|
|
227
|
+
annotate(g, true);
|
|
228
|
+
c.geom = g;
|
|
229
|
+
}
|
|
230
|
+
} else
|
|
231
|
+
texNext = null;
|
|
56
232
|
const moved = text.match(MOVED_RE);
|
|
57
233
|
if (moved && !cur.movedTo) {
|
|
58
234
|
let target = moved[1].trim().replace(/\\/g, "/");
|
|
@@ -63,22 +239,109 @@ function parse(src, name = "main.ldr") {
|
|
|
63
239
|
cur.cmds.push(c);
|
|
64
240
|
continue;
|
|
65
241
|
}
|
|
242
|
+
if (dataCur || skip)
|
|
243
|
+
continue;
|
|
66
244
|
if (!cur)
|
|
67
245
|
start(name);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
246
|
+
const g = geomLine(tk);
|
|
247
|
+
if (!g)
|
|
248
|
+
continue;
|
|
249
|
+
annotate(g, false);
|
|
250
|
+
cur.cmds.push(g);
|
|
251
|
+
}
|
|
252
|
+
closeData();
|
|
253
|
+
return data ? { main, files, data } : { main, files };
|
|
254
|
+
}
|
|
255
|
+
function resolveTexture(doc, name) {
|
|
256
|
+
if (!doc.data)
|
|
257
|
+
return null;
|
|
258
|
+
const n = norm(name);
|
|
259
|
+
return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
|
|
260
|
+
}
|
|
261
|
+
var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
262
|
+
var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
263
|
+
var cross3 = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
264
|
+
var unit3 = (a) => {
|
|
265
|
+
const l = Math.hypot(a[0], a[1], a[2]) || 1;
|
|
266
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
267
|
+
};
|
|
268
|
+
var DEG = Math.PI / 180;
|
|
269
|
+
function texmapUV(map, p) {
|
|
270
|
+
const w = texmapTriangleUV(map, [p, p, p]);
|
|
271
|
+
const u = w[0], v = w[1];
|
|
272
|
+
return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
|
|
273
|
+
}
|
|
274
|
+
var radial = (p, c, n) => {
|
|
275
|
+
const d = sub3(p, c);
|
|
276
|
+
const k = dot3(d, n);
|
|
277
|
+
const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
|
|
278
|
+
return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
|
|
279
|
+
};
|
|
280
|
+
function texmapTriangleUV(map, pts) {
|
|
281
|
+
const { p1, p2, p3 } = map;
|
|
282
|
+
const out = new Array(pts.length * 2).fill(0);
|
|
283
|
+
if (map.method === "PLANAR") {
|
|
284
|
+
const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
|
|
285
|
+
const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
|
|
286
|
+
pts.forEach((p, i) => {
|
|
287
|
+
const d = sub3(p, p1);
|
|
288
|
+
out[i * 2] = dot3(d, e1) / l1;
|
|
289
|
+
out[i * 2 + 1] = dot3(d, e2) / l2;
|
|
290
|
+
});
|
|
291
|
+
return out;
|
|
80
292
|
}
|
|
81
|
-
|
|
293
|
+
const cyl = map.method === "CYLINDRICAL";
|
|
294
|
+
const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
|
|
295
|
+
const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
|
|
296
|
+
const A = (map.a ?? 360) * DEG;
|
|
297
|
+
let base = null, baseAng = 0;
|
|
298
|
+
const sAng = (q) => {
|
|
299
|
+
if (!base) {
|
|
300
|
+
base = q;
|
|
301
|
+
baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
|
|
302
|
+
return baseAng;
|
|
303
|
+
}
|
|
304
|
+
return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
|
|
305
|
+
};
|
|
306
|
+
const miss = [];
|
|
307
|
+
let uSum = 0;
|
|
308
|
+
const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
|
|
309
|
+
pts.forEach((p, i) => {
|
|
310
|
+
if (cyl) {
|
|
311
|
+
const q = dir ? radial(p, p1, n) : null;
|
|
312
|
+
if (q) {
|
|
313
|
+
const a = sAng(q);
|
|
314
|
+
out[i * 2] = 0.5 + a / A;
|
|
315
|
+
uSum += out[i * 2];
|
|
316
|
+
} else
|
|
317
|
+
miss.push(i);
|
|
318
|
+
out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
|
|
319
|
+
} else {
|
|
320
|
+
const pd = unit3(sub3(p, p1));
|
|
321
|
+
const c = dot3(pd, n);
|
|
322
|
+
if (Math.abs(c) > 1 - 0.000000001) {
|
|
323
|
+
miss.push(i);
|
|
324
|
+
out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const a = sAng(radial(p, p1, n));
|
|
328
|
+
uSum += a;
|
|
329
|
+
out[i * 2] = 0.5 + a / A;
|
|
330
|
+
const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
|
|
331
|
+
const r = unit3([pd[0] * ct + cr[0] * st + n[0] * k, pd[1] * ct + cr[1] * st + n[1] * k, pd[2] * ct + cr[2] * st + n[2] * k]);
|
|
332
|
+
out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
if (miss.length && miss.length < pts.length) {
|
|
336
|
+
const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
|
|
337
|
+
for (const i of miss)
|
|
338
|
+
out[i * 2] = avg;
|
|
339
|
+
}
|
|
340
|
+
return out;
|
|
341
|
+
}
|
|
342
|
+
function transformTexMap(map, m) {
|
|
343
|
+
const x = (q) => [m[3] * q[0] + m[4] * q[1] + m[5] * q[2] + m[0], m[6] * q[0] + m[7] * q[1] + m[8] * q[2] + m[1], m[9] * q[0] + m[10] * q[1] + m[11] * q[2] + m[2]];
|
|
344
|
+
return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
|
|
82
345
|
}
|
|
83
346
|
function mul(a, b) {
|
|
84
347
|
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;
|
|
@@ -241,7 +504,7 @@ function decodePack(buf) {
|
|
|
241
504
|
const hl = new DataView(buf).getUint32(0, true);
|
|
242
505
|
const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, hl)));
|
|
243
506
|
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]));
|
|
507
|
+
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]) : r[2] === "u8" ? new Uint8Array(buf, base + r[0], r[1]) : new Uint32Array(buf, base + r[0], r[1]));
|
|
245
508
|
return { header, arr, bytes: buf.byteLength };
|
|
246
509
|
}
|
|
247
510
|
function geom(b, arr) {
|
|
@@ -253,6 +516,8 @@ function geom(b, arr) {
|
|
|
253
516
|
g.setAttribute("ldrColor", new THREE.BufferAttribute(arr(b.col), 4));
|
|
254
517
|
if (b.idx)
|
|
255
518
|
g.setIndex(new THREE.BufferAttribute(arr(b.idx), 1));
|
|
519
|
+
if (b.uv)
|
|
520
|
+
g.setAttribute("ldrUV", new THREE.BufferAttribute(arr(b.uv), 2));
|
|
256
521
|
return g;
|
|
257
522
|
}
|
|
258
523
|
function makeMaterial(side) {
|
|
@@ -268,6 +533,41 @@ function makeMaterial(side) {
|
|
|
268
533
|
m.customProgramCacheKey = () => "ldrColor";
|
|
269
534
|
return m;
|
|
270
535
|
}
|
|
536
|
+
function makeTexturedMaterial(side, map, wrapU = false) {
|
|
537
|
+
const m = makeMaterial(side);
|
|
538
|
+
const base = m.onBeforeCompile;
|
|
539
|
+
m.onBeforeCompile = (sh, r) => {
|
|
540
|
+
base.call(m, sh, r);
|
|
541
|
+
sh.uniforms.ldrMap = { value: map };
|
|
542
|
+
sh.uniforms.ldrWrapU = { value: wrapU ? 1 : 0 };
|
|
543
|
+
sh.vertexShader = `attribute vec2 ldrUV; varying vec2 vLdrUV;
|
|
544
|
+
` + sh.vertexShader.replace("#include <uv_vertex>", `#include <uv_vertex>
|
|
545
|
+
vLdrUV = ldrUV;`);
|
|
546
|
+
sh.fragmentShader = `uniform sampler2D ldrMap; uniform int ldrWrapU; varying vec2 vLdrUV;
|
|
547
|
+
` + sh.fragmentShader.replace("#include <color_fragment>", `#include <color_fragment>
|
|
548
|
+
{ vec2 uv = vec2(ldrWrapU == 1 ? fract(vLdrUV.x) : vLdrUV.x, vLdrUV.y); vec4 t = texture2D(ldrMap, uv);
|
|
549
|
+
float inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);
|
|
550
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, t.rgb, t.a * inside); }`);
|
|
551
|
+
};
|
|
552
|
+
m.customProgramCacheKey = () => "ldrColorTex";
|
|
553
|
+
m.userData.ldrMap = map;
|
|
554
|
+
return m;
|
|
555
|
+
}
|
|
556
|
+
function packTexture(pack, i) {
|
|
557
|
+
const t = new THREE.Texture;
|
|
558
|
+
t.flipY = false;
|
|
559
|
+
t.colorSpace = THREE.SRGBColorSpace;
|
|
560
|
+
t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
|
|
561
|
+
const e = pack.header.textures?.[i];
|
|
562
|
+
const bytes = e?.data ? pack.arr(e.data) : null;
|
|
563
|
+
if (!bytes || typeof createImageBitmap === "undefined")
|
|
564
|
+
return { texture: t, ready: Promise.resolve() };
|
|
565
|
+
const ready = createImageBitmap(new Blob([bytes], { type: e.mime || "image/png" }), { premultiplyAlpha: "none", colorSpaceConversion: "none" }).then((img) => {
|
|
566
|
+
t.image = img;
|
|
567
|
+
t.needsUpdate = true;
|
|
568
|
+
}).catch(() => {});
|
|
569
|
+
return { texture: t, ready };
|
|
570
|
+
}
|
|
271
571
|
var lineMat = () => new THREE.ShaderMaterial({
|
|
272
572
|
vertexShader: `attribute vec4 im0; attribute vec4 im1; attribute vec4 im2; attribute vec4 im3;
|
|
273
573
|
void main(){ gl_Position = projectionMatrix * modelViewMatrix * mat4(im0,im1,im2,im3) * vec4(position,1.0); }`,
|
|
@@ -296,6 +596,109 @@ var weldLines = (pos) => {
|
|
|
296
596
|
function createInstancedEdges(pos, im, material = sharedLineMat()) {
|
|
297
597
|
return sharedLines(pos, im, material);
|
|
298
598
|
}
|
|
599
|
+
function conditionalLines(pack, entry, which = "all") {
|
|
600
|
+
if (which === "all") {
|
|
601
|
+
const a = conditionalLines(pack, entry, "body"), b = conditionalLines(pack, entry, "under");
|
|
602
|
+
if (!a || !b)
|
|
603
|
+
return a ?? b;
|
|
604
|
+
const o = new Float32Array(a.length + b.length);
|
|
605
|
+
o.set(a);
|
|
606
|
+
o.set(b, a.length);
|
|
607
|
+
return o;
|
|
608
|
+
}
|
|
609
|
+
const c = which === "under" ? entry?.condlu : entry?.condl;
|
|
610
|
+
if (!c)
|
|
611
|
+
return null;
|
|
612
|
+
const { arr } = pack;
|
|
613
|
+
if (c.raw)
|
|
614
|
+
return arr(c.raw);
|
|
615
|
+
const pools = [entry.body ? arr(entry.body.pos) : [], entry.under ? arr(entry.under.pos) : [], entry.edges ? arr(entry.edges) : [], c.x ? arr(c.x) : []];
|
|
616
|
+
const idx = arr(c.idx);
|
|
617
|
+
const out = new Float32Array(idx.length * 3);
|
|
618
|
+
const starts = [];
|
|
619
|
+
let base = 0;
|
|
620
|
+
for (const p of pools) {
|
|
621
|
+
starts.push(base);
|
|
622
|
+
base += p.length / 3;
|
|
623
|
+
}
|
|
624
|
+
let prev = 0;
|
|
625
|
+
for (let i = 0;i < idx.length; i++) {
|
|
626
|
+
const z = idx[i];
|
|
627
|
+
const j = prev + (z & 1 ? -(z + 1) / 2 : z / 2);
|
|
628
|
+
prev = j;
|
|
629
|
+
let k = 3;
|
|
630
|
+
while (k > 0 && j < starts[k])
|
|
631
|
+
k--;
|
|
632
|
+
const p = pools[k], o = (j - starts[k]) * 3;
|
|
633
|
+
out[i * 3] = p[o];
|
|
634
|
+
out[i * 3 + 1] = p[o + 1];
|
|
635
|
+
out[i * 3 + 2] = p[o + 2];
|
|
636
|
+
}
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
var condLineMat = () => new THREE.ShaderMaterial({
|
|
640
|
+
uniforms: { ldrViewProj: { value: new THREE.Matrix4 } },
|
|
641
|
+
vertexShader: `uniform mat4 ldrViewProj; attribute mat4 instanceMatrix; attribute vec3 ldrP1; attribute vec3 ldrC0; attribute vec3 ldrC1; attribute float ldrSide;
|
|
642
|
+
void main(){
|
|
643
|
+
mat4 mvp = ldrViewProj * instanceMatrix;
|
|
644
|
+
vec4 a = mvp * vec4(position, 1.0), b = mvp * vec4(ldrP1, 1.0), c0 = mvp * vec4(ldrC0, 1.0), c1 = mvp * vec4(ldrC1, 1.0);
|
|
645
|
+
vec2 A = a.xy / a.w, B = b.xy / b.w, C0 = c0.xy / c0.w, C1 = c1.xy / c1.w, d = B - A;
|
|
646
|
+
float s0 = d.x * (C0.y - A.y) - d.y * (C0.x - A.x), s1 = d.x * (C1.y - A.y) - d.y * (C1.x - A.x);
|
|
647
|
+
gl_Position = s0 * s1 >= 0.0 ? (ldrSide > 0.5 ? b : a) : vec4(2.0, 2.0, 2.0, 1.0);
|
|
648
|
+
}`,
|
|
649
|
+
fragmentShader: `void main(){ gl_FragColor = vec4(0.13,0.13,0.13,1.0); }`
|
|
650
|
+
});
|
|
651
|
+
function conditionalLineVisible(cond, i, mvp) {
|
|
652
|
+
const e = mvp.elements;
|
|
653
|
+
const pr = (o) => {
|
|
654
|
+
const x = cond[o], y = cond[o + 1], z = cond[o + 2];
|
|
655
|
+
const w = e[3] * x + e[7] * y + e[11] * z + e[15];
|
|
656
|
+
return [(e[0] * x + e[4] * y + e[8] * z + e[12]) / w, (e[1] * x + e[5] * y + e[9] * z + e[13]) / w];
|
|
657
|
+
};
|
|
658
|
+
const o = i * 12;
|
|
659
|
+
const A = pr(o), B = pr(o + 3), C0 = pr(o + 6), C1 = pr(o + 9);
|
|
660
|
+
const dx = B[0] - A[0], dy = B[1] - A[1];
|
|
661
|
+
const s0 = dx * (C0[1] - A[1]) - dy * (C0[0] - A[0]), s1 = dx * (C1[1] - A[1]) - dy * (C1[0] - A[0]);
|
|
662
|
+
return s0 * s1 >= 0;
|
|
663
|
+
}
|
|
664
|
+
function createInstancedConditionalEdges(cond, im, material = condLineMat()) {
|
|
665
|
+
return condEdges(condBuffers(cond), im, material);
|
|
666
|
+
}
|
|
667
|
+
function condBuffers(cond) {
|
|
668
|
+
const n = Math.floor(cond.length / 12);
|
|
669
|
+
const buf = new Float32Array(n * 24), side = new Float32Array(n * 2);
|
|
670
|
+
for (let l = 0;l < n; l++) {
|
|
671
|
+
for (let k = 0;k < 12; k++) {
|
|
672
|
+
const v = cond[l * 12 + k];
|
|
673
|
+
buf[l * 24 + k] = v;
|
|
674
|
+
buf[l * 24 + 12 + k] = v;
|
|
675
|
+
}
|
|
676
|
+
side[l * 2 + 1] = 1;
|
|
677
|
+
}
|
|
678
|
+
return { ib: new THREE.InterleavedBuffer(buf, 12), side: new THREE.BufferAttribute(side, 1) };
|
|
679
|
+
}
|
|
680
|
+
function condEdges({ ib, side }, im, material) {
|
|
681
|
+
const g = new THREE.InstancedBufferGeometry;
|
|
682
|
+
g.setAttribute("position", new THREE.InterleavedBufferAttribute(ib, 3, 0));
|
|
683
|
+
g.setAttribute("ldrP1", new THREE.InterleavedBufferAttribute(ib, 3, 3));
|
|
684
|
+
g.setAttribute("ldrC0", new THREE.InterleavedBufferAttribute(ib, 3, 6));
|
|
685
|
+
g.setAttribute("ldrC1", new THREE.InterleavedBufferAttribute(ib, 3, 9));
|
|
686
|
+
g.setAttribute("ldrSide", side);
|
|
687
|
+
g.setAttribute("instanceMatrix", im.instanceMatrix);
|
|
688
|
+
g.instanceCount = im.count;
|
|
689
|
+
const l = new THREE.LineSegments(g, material);
|
|
690
|
+
l.frustumCulled = false;
|
|
691
|
+
l.name = "ldrCondLines";
|
|
692
|
+
l.onBeforeRender = (_r, _s, cam, _g, mat) => {
|
|
693
|
+
const u = mat.uniforms?.ldrViewProj;
|
|
694
|
+
if (!u)
|
|
695
|
+
return;
|
|
696
|
+
l.modelViewMatrix.multiplyMatrices(cam.matrixWorldInverse, l.matrixWorld);
|
|
697
|
+
u.value.multiplyMatrices(cam.projectionMatrix, l.modelViewMatrix);
|
|
698
|
+
mat.uniformsNeedUpdate = true;
|
|
699
|
+
};
|
|
700
|
+
return l;
|
|
701
|
+
}
|
|
299
702
|
function sharedLines(pos, im, material = sharedLineMat()) {
|
|
300
703
|
const { P, I } = weldLines(pos);
|
|
301
704
|
const g = new THREE.InstancedBufferGeometry;
|
|
@@ -426,29 +829,96 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
426
829
|
const cull = computeCulling(insts, H);
|
|
427
830
|
mark("culling");
|
|
428
831
|
const edgeObjs = [];
|
|
832
|
+
const condObjs = [];
|
|
833
|
+
const wantCond = P.cond !== false && P.edges === "idx";
|
|
834
|
+
let condMat = null;
|
|
835
|
+
const cmat = () => condMat ??= condLineMat();
|
|
836
|
+
const condCache = new Map;
|
|
837
|
+
const addCond = (entry, im, which = "body") => {
|
|
838
|
+
if (!wantCond)
|
|
839
|
+
return null;
|
|
840
|
+
const ck = which === "under" ? entry.condlu : entry.condl;
|
|
841
|
+
if (!ck)
|
|
842
|
+
return null;
|
|
843
|
+
let cb = condCache.get(ck);
|
|
844
|
+
if (cb === undefined) {
|
|
845
|
+
const c = conditionalLines(pack, entry, which);
|
|
846
|
+
cb = c && c.length ? condBuffers(c) : null;
|
|
847
|
+
condCache.set(ck, cb);
|
|
848
|
+
}
|
|
849
|
+
if (!cb)
|
|
850
|
+
return null;
|
|
851
|
+
const l = condEdges(cb, im, cmat());
|
|
852
|
+
edgeObjs.push(l);
|
|
853
|
+
condObjs.push(l);
|
|
854
|
+
root.add(l);
|
|
855
|
+
return l;
|
|
856
|
+
};
|
|
429
857
|
const stats = { studsTotal: cull.studs.length, studsHidden: cull.studs.filter((s) => s.hidden).length, undersTotal: 0, undersHidden: 0 };
|
|
430
858
|
const toggles = [];
|
|
431
859
|
const studMeshes = [];
|
|
860
|
+
const wantTex = P.tex !== false && !!H.textures;
|
|
861
|
+
const texCache = new Map;
|
|
862
|
+
const texMats = new Map;
|
|
863
|
+
const pending = [];
|
|
864
|
+
const texMat = (t, base, wrapU) => {
|
|
865
|
+
const role = base === matF ? "F" : base === matB ? "B" : "D";
|
|
866
|
+
const k = t + ":" + role + ":" + (wrapU ? 1 : 0);
|
|
867
|
+
let m = texMats.get(k);
|
|
868
|
+
if (!m) {
|
|
869
|
+
let tx = texCache.get(t);
|
|
870
|
+
if (!tx) {
|
|
871
|
+
const r = packTexture(pack, t);
|
|
872
|
+
tx = r.texture;
|
|
873
|
+
pending.push(r.ready);
|
|
874
|
+
texCache.set(t, tx);
|
|
875
|
+
}
|
|
876
|
+
texMats.set(k, m = makeTexturedMaterial(base.side, tx, wrapU));
|
|
877
|
+
}
|
|
878
|
+
return m;
|
|
879
|
+
};
|
|
880
|
+
let texBatches = 0;
|
|
432
881
|
for (const [pk, ids] of byPart) {
|
|
433
882
|
const part = pk.replace(/#m$/, "");
|
|
434
883
|
const p = H.parts[part];
|
|
435
884
|
const mat = P.uncertDouble !== false && p.uncert ? (nUncert++, matD) : pk.endsWith("#m") ? matB : matF;
|
|
436
|
-
const
|
|
885
|
+
const textured = wantTex && !!p.tex;
|
|
886
|
+
const body = geom(textured ? p.tbody : p.body, arr);
|
|
437
887
|
let bodyIm = null;
|
|
438
|
-
|
|
439
|
-
const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
|
|
888
|
+
const place = (im) => {
|
|
440
889
|
ids.forEach((id, k) => {
|
|
441
890
|
im.setMatrixAt(k, toM4(insts[id].m));
|
|
442
891
|
im.setColorAt(k, color(insts[id].color));
|
|
443
892
|
});
|
|
444
893
|
im.computeBoundingSphere();
|
|
445
894
|
root.add(im);
|
|
895
|
+
};
|
|
896
|
+
if (body) {
|
|
897
|
+
const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
|
|
898
|
+
place(im);
|
|
446
899
|
}
|
|
900
|
+
if (textured)
|
|
901
|
+
for (const tb of p.tex) {
|
|
902
|
+
const g = geom(tb, arr);
|
|
903
|
+
const im = new THREE.InstancedMesh(g, texMat(tb.t, mat, !!tb.wrapU), ids.length);
|
|
904
|
+
texBatches++;
|
|
905
|
+
if (bodyIm) {
|
|
906
|
+
im.instanceMatrix = bodyIm.instanceMatrix;
|
|
907
|
+
im.instanceColor = bodyIm.instanceColor;
|
|
908
|
+
im.boundingSphere = bodyIm.boundingSphere;
|
|
909
|
+
root.add(im);
|
|
910
|
+
} else {
|
|
911
|
+
place(im);
|
|
912
|
+
bodyIm = im;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
447
915
|
if (P.edges === "idx" && p.edges && bodyIm) {
|
|
448
916
|
const l = sharedLines(arr(p.edges), bodyIm);
|
|
449
917
|
edgeObjs.push(l);
|
|
450
918
|
root.add(l);
|
|
451
919
|
}
|
|
920
|
+
if (bodyIm)
|
|
921
|
+
addCond(p, bodyIm);
|
|
452
922
|
const under = geom(p.under, arr);
|
|
453
923
|
if (under) {
|
|
454
924
|
const ord = [...ids.filter((i) => !cull.underHidden[i]), ...ids.filter((i) => cull.underHidden[i])];
|
|
@@ -464,6 +934,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
464
934
|
im.count = P.cullUnder ? vis : ids.length;
|
|
465
935
|
root.add(im);
|
|
466
936
|
toggles.push({ mesh: im, all: ids.length, visible: vis, flag: "under" });
|
|
937
|
+
{
|
|
938
|
+
const cl = addCond(p, im, "under");
|
|
939
|
+
if (cl) {
|
|
940
|
+
toggles.push({ mesh: { set count(n) {
|
|
941
|
+
cl.geometry.instanceCount = n;
|
|
942
|
+
} }, all: ids.length, visible: vis, flag: "under" });
|
|
943
|
+
cl.geometry.instanceCount = im.count;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
467
946
|
}
|
|
468
947
|
if (P.edges === true && p.edges) {
|
|
469
948
|
const mats = new Float32Array(ids.length * 16);
|
|
@@ -508,6 +987,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
508
987
|
} }, all: ord.length, visible: vis, flag: "studs" });
|
|
509
988
|
l.geometry.instanceCount = im.count;
|
|
510
989
|
}
|
|
990
|
+
{
|
|
991
|
+
const cl = P.condStuds === false ? null : addCond(k, im);
|
|
992
|
+
if (cl) {
|
|
993
|
+
toggles.push({ mesh: { set count(n) {
|
|
994
|
+
cl.geometry.instanceCount = n;
|
|
995
|
+
} }, all: ord.length, visible: vis, flag: "studs" });
|
|
996
|
+
cl.geometry.instanceCount = im.count;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
511
999
|
if (P.edges === true && k.edges) {
|
|
512
1000
|
const mats = new Float32Array(ord.length * 16);
|
|
513
1001
|
ord.forEach((s, i) => toM4(s.w).toArray(mats, i * 16));
|
|
@@ -521,7 +1009,19 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
521
1009
|
}
|
|
522
1010
|
stats.uncertParts = Object.values(H.parts).filter((x) => x.uncert).length;
|
|
523
1011
|
stats.uncertBatches = nUncert;
|
|
524
|
-
|
|
1012
|
+
let condOn = true, edgesOn = true;
|
|
1013
|
+
const setEdges = (v) => {
|
|
1014
|
+
edgesOn = v;
|
|
1015
|
+
edgeObjs.forEach((o) => o.visible = v && (condOn || !condObjs.includes(o)));
|
|
1016
|
+
};
|
|
1017
|
+
const setCond = (v) => {
|
|
1018
|
+
condOn = v;
|
|
1019
|
+
condObjs.forEach((o) => o.visible = v && edgesOn);
|
|
1020
|
+
};
|
|
1021
|
+
stats.condBatches = condObjs.length;
|
|
1022
|
+
stats.texBatches = texBatches;
|
|
1023
|
+
stats.textures = texCache.size;
|
|
1024
|
+
const texturesReady = Promise.all(pending).then(() => {});
|
|
525
1025
|
const setCull = (studs, under) => {
|
|
526
1026
|
for (const t of toggles)
|
|
527
1027
|
t.mesh.count = (t.flag === "studs" ? studs : under) ? t.visible : t.all;
|
|
@@ -537,6 +1037,11 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
537
1037
|
matF.side = front ? THREE.FrontSide : THREE.DoubleSide;
|
|
538
1038
|
matB.side = front ? THREE.BackSide : THREE.DoubleSide;
|
|
539
1039
|
matF.needsUpdate = matB.needsUpdate = true;
|
|
1040
|
+
for (const [k, m] of texMats) {
|
|
1041
|
+
const role = k.split(":")[1];
|
|
1042
|
+
m.side = role === "D" || !front ? THREE.DoubleSide : role === "B" ? THREE.BackSide : THREE.FrontSide;
|
|
1043
|
+
m.needsUpdate = true;
|
|
1044
|
+
}
|
|
540
1045
|
};
|
|
541
1046
|
const swapStuds = (alt) => {
|
|
542
1047
|
const saved = studMeshes.map((s) => s.im.geometry);
|
|
@@ -547,7 +1052,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
547
1052
|
}
|
|
548
1053
|
return () => studMeshes.forEach((s, i) => s.im.geometry = saved[i]);
|
|
549
1054
|
};
|
|
550
|
-
return { root, stats, setCull, setSide, setEdges, swapStuds, box };
|
|
1055
|
+
return { root, stats, setCull, setSide, setEdges, setCond, swapStuds, box, texturesReady };
|
|
551
1056
|
}
|
|
552
1057
|
// src/scene.ts
|
|
553
1058
|
import * as THREE2 from "three";
|
|
@@ -584,13 +1089,14 @@ function toPlastic(old) {
|
|
|
584
1089
|
if (!m) {
|
|
585
1090
|
const p = new THREE2.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.3, clearcoat: 0.3, clearcoatRoughness: 0.2, specularIntensity: 0.6, side: old.side });
|
|
586
1091
|
p.onBeforeCompile = old.onBeforeCompile;
|
|
587
|
-
p.customProgramCacheKey = () => "
|
|
1092
|
+
p.customProgramCacheKey = () => "Phys" + old.customProgramCacheKey() + old.side;
|
|
1093
|
+
p.userData = old.userData;
|
|
588
1094
|
plasticCache.set(old, m = p);
|
|
589
1095
|
}
|
|
590
1096
|
return m;
|
|
591
1097
|
}
|
|
592
1098
|
function createLdrObject(instances, pack, options = {}) {
|
|
593
|
-
const o = { edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
|
|
1099
|
+
const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
|
|
594
1100
|
const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
|
|
595
1101
|
const all = instances.map((i) => ({ part: i.part, color: String(i.color), m: i.m, steps: i.steps }));
|
|
596
1102
|
const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
|
|
@@ -606,7 +1112,7 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
606
1112
|
object.scale.setScalar(o.scale);
|
|
607
1113
|
const pairs = new Map;
|
|
608
1114
|
let cur = null;
|
|
609
|
-
let { edges, cullStuds: cs, cullUndersides: cu, side } = o;
|
|
1115
|
+
let { edges, conditionalLines: cond, cullStuds: cs, cullUndersides: cu, side } = o;
|
|
610
1116
|
const stats = { instances: all.length, drawn: 0, missing, studsTotal: 0, studsHidden: 0, undersTotal: 0, undersHidden: 0, buildMs: 0 };
|
|
611
1117
|
const disposeCur = () => {
|
|
612
1118
|
if (!cur)
|
|
@@ -620,17 +1126,17 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
620
1126
|
if (x.dispose && x.isInstancedMesh)
|
|
621
1127
|
x.dispose();
|
|
622
1128
|
});
|
|
623
|
-
for (const m of mats)
|
|
1129
|
+
for (const m of [...mats, ...pairs.values()]) {
|
|
1130
|
+
m.userData.ldrMap?.dispose();
|
|
624
1131
|
m.dispose();
|
|
625
|
-
|
|
626
|
-
old.dispose();
|
|
1132
|
+
}
|
|
627
1133
|
cur = null;
|
|
628
1134
|
};
|
|
629
1135
|
const build = (step) => {
|
|
630
1136
|
const t0 = performance.now();
|
|
631
1137
|
disposeCur();
|
|
632
1138
|
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, () => {});
|
|
1139
|
+
cur = buildLdrScene(insts, pack, { pack: "", cullStuds: cs, cullUnder: cu, edges: edges ? "idx" : false, side, cond, condStuds: o.conditionalStudLines !== false, tex: o.textures !== false }, color, () => {});
|
|
634
1140
|
pairs.clear();
|
|
635
1141
|
if (o.material === "plastic")
|
|
636
1142
|
cur.root.traverse((x) => {
|
|
@@ -642,11 +1148,15 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
642
1148
|
});
|
|
643
1149
|
if (!edges)
|
|
644
1150
|
cur.setEdges(false);
|
|
1151
|
+
if (!cond)
|
|
1152
|
+
cur.setCond(false);
|
|
645
1153
|
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 });
|
|
1154
|
+
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, condBatches: cur.stats.condBatches ?? 0, texBatches: cur.stats.texBatches ?? 0, textures: cur.stats.textures ?? 0, buildMs: performance.now() - t0 });
|
|
1155
|
+
api.texturesReady = cur.texturesReady;
|
|
647
1156
|
};
|
|
1157
|
+
const api = { texturesReady: Promise.resolve() };
|
|
648
1158
|
build(o.step ?? null);
|
|
649
|
-
return {
|
|
1159
|
+
return Object.assign(api, {
|
|
650
1160
|
object,
|
|
651
1161
|
stats,
|
|
652
1162
|
stepCount: uniq.length,
|
|
@@ -655,6 +1165,10 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
655
1165
|
edges = v;
|
|
656
1166
|
cur.setEdges(v);
|
|
657
1167
|
},
|
|
1168
|
+
setConditionalLines: (v) => {
|
|
1169
|
+
cond = v;
|
|
1170
|
+
cur.setCond(v);
|
|
1171
|
+
},
|
|
658
1172
|
setCulling: (a, b) => {
|
|
659
1173
|
cs = a;
|
|
660
1174
|
cu = b;
|
|
@@ -681,7 +1195,7 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
681
1195
|
return target;
|
|
682
1196
|
},
|
|
683
1197
|
dispose: disposeCur
|
|
684
|
-
};
|
|
1198
|
+
});
|
|
685
1199
|
}
|
|
686
1200
|
// src/pack-io.ts
|
|
687
1201
|
import { decompress } from "fzstd";
|
|
@@ -766,46 +1280,91 @@ class Library {
|
|
|
766
1280
|
}
|
|
767
1281
|
return out;
|
|
768
1282
|
}
|
|
769
|
-
mesh(name) {
|
|
1283
|
+
mesh(name, opts = {}) {
|
|
770
1284
|
name = norm(name);
|
|
771
|
-
const
|
|
1285
|
+
const ck = opts.texmap ? name + "#texmap" : name;
|
|
1286
|
+
const hit = this.meshCache.get(ck);
|
|
772
1287
|
if (hit)
|
|
773
1288
|
return hit;
|
|
774
1289
|
const pos = [], col = [], edges = [], cond = [];
|
|
1290
|
+
const tx = opts.texmap ? { uv: [], tri: [], names: [] } : undefined;
|
|
775
1291
|
const r = this.where(name);
|
|
776
1292
|
if (r)
|
|
777
|
-
this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond);
|
|
1293
|
+
this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond, tx, null);
|
|
778
1294
|
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
|
-
|
|
1295
|
+
if (tx) {
|
|
1296
|
+
m.uv = new Float32Array(tx.uv);
|
|
1297
|
+
m.texture = new Int32Array(tx.tri);
|
|
1298
|
+
m.textures = tx.names;
|
|
1299
|
+
}
|
|
1300
|
+
this.meshCache.set(ck, m);
|
|
780
1301
|
return m;
|
|
781
1302
|
}
|
|
782
|
-
|
|
1303
|
+
texture(name, doc) {
|
|
1304
|
+
if (doc) {
|
|
1305
|
+
const d = resolveTexture(doc, name);
|
|
1306
|
+
if (d)
|
|
1307
|
+
return d.bytes;
|
|
1308
|
+
}
|
|
1309
|
+
const n = norm(name);
|
|
1310
|
+
const roots = [this.root];
|
|
1311
|
+
const unoff = this.unofficialRoot ?? (existsSync(join(this.root, "unofficial")) ? join(this.root, "unofficial") : undefined);
|
|
1312
|
+
if (unoff)
|
|
1313
|
+
roots.push(unoff);
|
|
1314
|
+
for (const cand of ["textures/" + n, n])
|
|
1315
|
+
for (const root of roots)
|
|
1316
|
+
for (const d of ["parts", "p", "models"]) {
|
|
1317
|
+
const f = join(root, d, cand);
|
|
1318
|
+
if (existsSync(f))
|
|
1319
|
+
return new Uint8Array(readFileSync(f));
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
emit(doc, fname, m, color, inv, clip, pos, col, edges, cond, tx, texIn = null) {
|
|
783
1324
|
const f = doc.files.get(fname);
|
|
784
1325
|
if (!f)
|
|
785
1326
|
return;
|
|
786
1327
|
let ccw = true, invertNext = false, clipLocal = clip;
|
|
787
1328
|
const flip = inv !== det(m) < 0;
|
|
788
1329
|
const cc = (c) => c === 16 ? color : c;
|
|
789
|
-
|
|
1330
|
+
const texIdx = (t) => {
|
|
1331
|
+
let i = tx.names.indexOf(t.texture);
|
|
1332
|
+
if (i < 0) {
|
|
1333
|
+
i = tx.names.length;
|
|
1334
|
+
tx.names.push(t.texture);
|
|
1335
|
+
}
|
|
1336
|
+
return i;
|
|
1337
|
+
};
|
|
1338
|
+
for (const c0 of f.cmds) {
|
|
1339
|
+
let c = c0;
|
|
790
1340
|
if (c.t === 0) {
|
|
791
|
-
if (c.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (c.bfc
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1341
|
+
if (tx && c.geom)
|
|
1342
|
+
c = c.geom;
|
|
1343
|
+
else {
|
|
1344
|
+
if (c.bfc) {
|
|
1345
|
+
if (c.bfc.includes("CW") && !c.bfc.includes("CCW"))
|
|
1346
|
+
ccw = false;
|
|
1347
|
+
if (c.bfc.includes("CCW"))
|
|
1348
|
+
ccw = true;
|
|
1349
|
+
if (c.bfc.includes("INVERTNEXT"))
|
|
1350
|
+
invertNext = true;
|
|
1351
|
+
if (/\bNOCLIP\b/.test(c.bfc))
|
|
1352
|
+
clipLocal = false;
|
|
1353
|
+
else if (/\bCLIP\b/.test(c.bfc))
|
|
1354
|
+
clipLocal = true;
|
|
1355
|
+
}
|
|
1356
|
+
continue;
|
|
802
1357
|
}
|
|
803
|
-
continue;
|
|
804
1358
|
}
|
|
1359
|
+
if (c.t === 0)
|
|
1360
|
+
continue;
|
|
1361
|
+
if (tx && c.fallback)
|
|
1362
|
+
continue;
|
|
1363
|
+
const tex = tx ? c.tex ? transformTexMap(c.tex, m) : texIn : null;
|
|
805
1364
|
if (c.t === 1) {
|
|
806
1365
|
const sub = doc.files.has(c.file) ? doc : this.where(c.file)?.doc;
|
|
807
1366
|
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);
|
|
1367
|
+
this.emit(sub, sub === doc ? c.file : sub.main, mul(m, c.m), cc(c.color), inv !== invertNext, clipLocal, pos, col, edges, cond, tx, tex);
|
|
809
1368
|
invertNext = false;
|
|
810
1369
|
continue;
|
|
811
1370
|
}
|
|
@@ -821,9 +1380,18 @@ class Library {
|
|
|
821
1380
|
continue;
|
|
822
1381
|
}
|
|
823
1382
|
const ordered = ccw !== flip ? v : [...v].reverse();
|
|
1383
|
+
const ti = tx && tex ? texIdx(tex) : -1;
|
|
1384
|
+
const code = cc(c.color);
|
|
824
1385
|
const tri = (a, b, d) => {
|
|
825
1386
|
pos.push(...a, ...b, ...d);
|
|
826
|
-
col.push(
|
|
1387
|
+
col.push(code);
|
|
1388
|
+
if (tx) {
|
|
1389
|
+
tx.tri.push(ti);
|
|
1390
|
+
if (tex)
|
|
1391
|
+
tx.uv.push(...texmapTriangleUV(tex, [a, b, d]));
|
|
1392
|
+
else
|
|
1393
|
+
tx.uv.push(NaN, NaN, NaN, NaN, NaN, NaN);
|
|
1394
|
+
}
|
|
827
1395
|
};
|
|
828
1396
|
const emitFace = (w) => {
|
|
829
1397
|
tri(w[0], w[1], w[2]);
|
|
@@ -1006,31 +1574,67 @@ class Library2 {
|
|
|
1006
1574
|
}
|
|
1007
1575
|
function splitModel(text, fallbackName = "main.ldr") {
|
|
1008
1576
|
const files = new Map;
|
|
1577
|
+
const data = new Map;
|
|
1578
|
+
let dataCur = null;
|
|
1009
1579
|
let cur = null, buf = [], main = null;
|
|
1580
|
+
let skip = false, sawBlock = false;
|
|
1581
|
+
const flush = () => {
|
|
1582
|
+
if (cur)
|
|
1583
|
+
files.set(cur, buf.join(`
|
|
1584
|
+
`));
|
|
1585
|
+
cur = null;
|
|
1586
|
+
buf = [];
|
|
1587
|
+
};
|
|
1588
|
+
const firstBlock = () => {
|
|
1589
|
+
if (sawBlock)
|
|
1590
|
+
return;
|
|
1591
|
+
sawBlock = true;
|
|
1592
|
+
files.clear();
|
|
1593
|
+
cur = null;
|
|
1594
|
+
buf = [];
|
|
1595
|
+
main = null;
|
|
1596
|
+
};
|
|
1010
1597
|
for (const line of text.split(/\r?\n/)) {
|
|
1011
1598
|
const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
|
|
1012
1599
|
if (m) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1600
|
+
firstBlock();
|
|
1601
|
+
flush();
|
|
1602
|
+
skip = false;
|
|
1603
|
+
dataCur = null;
|
|
1016
1604
|
cur = norm2(m[1]);
|
|
1017
|
-
buf = [];
|
|
1018
1605
|
if (!main)
|
|
1019
1606
|
main = cur;
|
|
1020
1607
|
continue;
|
|
1021
1608
|
}
|
|
1022
|
-
if (/^\s*0\s+NOFILE/i.test(line))
|
|
1609
|
+
if (/^\s*0\s+(NOFILE|!DATA)(\s|$)/i.test(line)) {
|
|
1610
|
+
dataCur = null;
|
|
1611
|
+
if (/!DATA/i.test(line)) {
|
|
1612
|
+
firstBlock();
|
|
1613
|
+
const dn = norm2(line.replace(/^\s*0\s+!DATA\s*/i, ""));
|
|
1614
|
+
dataCur = dn;
|
|
1615
|
+
data.set(dn, "");
|
|
1616
|
+
}
|
|
1617
|
+
flush();
|
|
1618
|
+
skip = true;
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
if (skip) {
|
|
1622
|
+
const dc = dataCur;
|
|
1623
|
+
if (dc) {
|
|
1624
|
+
const d = /^\s*0\s+!:\s*(.*)$/.exec(line);
|
|
1625
|
+
if (d)
|
|
1626
|
+
data.set(dc, data.get(dc) + d[1].trim());
|
|
1627
|
+
}
|
|
1023
1628
|
continue;
|
|
1629
|
+
}
|
|
1024
1630
|
if (!cur) {
|
|
1025
1631
|
cur = norm2(fallbackName);
|
|
1026
1632
|
main = cur;
|
|
1027
1633
|
}
|
|
1028
1634
|
buf.push(line);
|
|
1029
1635
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
`));
|
|
1033
|
-
return { files, main };
|
|
1636
|
+
flush();
|
|
1637
|
+
return { files, main, data };
|
|
1034
1638
|
}
|
|
1035
1639
|
function flatten2(model, lib, onInstance) {
|
|
1036
1640
|
const missing = new Map;
|
|
@@ -1131,7 +1735,7 @@ class Compiler {
|
|
|
1131
1735
|
const ck = key + (separateStuds ? "" : "#nostudsep");
|
|
1132
1736
|
if (this.cache.has(ck))
|
|
1133
1737
|
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 };
|
|
1738
|
+
const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, cond: this.opts.cond ? [] : undefined, condUnder: this.opts.cond ? [] : undefined, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
|
|
1135
1739
|
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
1740
|
const walk = (fk, M, color, invert, target, depth) => {
|
|
1137
1741
|
if (depth > 40)
|
|
@@ -1186,6 +1790,13 @@ class Compiler {
|
|
|
1186
1790
|
}
|
|
1187
1791
|
if (c === "5") {
|
|
1188
1792
|
out.condLines++;
|
|
1793
|
+
const dst = target === "under" ? out.condUnder : out.cond;
|
|
1794
|
+
if (dst) {
|
|
1795
|
+
const v = t.split(/\s+/).slice(2, 14).map(Number);
|
|
1796
|
+
if (v.length === 12 && v.every(Number.isFinite))
|
|
1797
|
+
for (let i = 0;i < 12; i += 3)
|
|
1798
|
+
dst.push(...xf2(M, v.slice(i, i + 3)));
|
|
1799
|
+
}
|
|
1189
1800
|
continue;
|
|
1190
1801
|
}
|
|
1191
1802
|
if (c === "3" || c === "4") {
|
|
@@ -1212,6 +1823,8 @@ class Compiler {
|
|
|
1212
1823
|
out.uncertified = true;
|
|
1213
1824
|
};
|
|
1214
1825
|
walk(key, IDENT, "16", false, "body", 0);
|
|
1826
|
+
if (this.opts.tex && this.hasTex(key))
|
|
1827
|
+
this.texPass(key, out, separateStuds, colorOf);
|
|
1215
1828
|
const [x0, y0, z0, x1, y1, z1] = out.bbox;
|
|
1216
1829
|
let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
|
|
1217
1830
|
for (const b of [out.body, out.under])
|
|
@@ -1228,6 +1841,169 @@ class Compiler {
|
|
|
1228
1841
|
this.cache.set(ck, out);
|
|
1229
1842
|
return out;
|
|
1230
1843
|
}
|
|
1844
|
+
texMemo = new Map;
|
|
1845
|
+
hasTex(key) {
|
|
1846
|
+
const m = this.texMemo.get(key);
|
|
1847
|
+
if (m !== undefined)
|
|
1848
|
+
return m;
|
|
1849
|
+
this.texMemo.set(key, false);
|
|
1850
|
+
const txt = this.lib.read(key);
|
|
1851
|
+
let r = /^\s*0\s+!TEXMAP/im.test(txt);
|
|
1852
|
+
if (!r)
|
|
1853
|
+
for (const l of txt.split(/\r?\n/)) {
|
|
1854
|
+
const t = l.trim();
|
|
1855
|
+
const p = t.startsWith("0 !:") ? parseType1(t.slice(4)) : t[0] === "1" ? parseType1(t) : null;
|
|
1856
|
+
if (p) {
|
|
1857
|
+
const k = this.lib.resolve(p.name);
|
|
1858
|
+
if (k && this.hasTex(k)) {
|
|
1859
|
+
r = true;
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
this.texMemo.set(key, r);
|
|
1865
|
+
return r;
|
|
1866
|
+
}
|
|
1867
|
+
texture(name) {
|
|
1868
|
+
const n = norm2(name);
|
|
1869
|
+
for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
|
|
1870
|
+
const k = pre + n;
|
|
1871
|
+
if (this.lib.index.has(k))
|
|
1872
|
+
return new Uint8Array(fs2.readFileSync(this.lib.index.get(k)));
|
|
1873
|
+
}
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
texPass(key, out, separateStuds, colorOf) {
|
|
1877
|
+
const tbody = { pos: [], col: [] }, texb = new Map;
|
|
1878
|
+
const walk = (fk, M, color, invert, target, depth, texIn) => {
|
|
1879
|
+
if (depth > 40)
|
|
1880
|
+
return;
|
|
1881
|
+
let ccw = true, invertNext = false;
|
|
1882
|
+
let stack = [];
|
|
1883
|
+
let next = null;
|
|
1884
|
+
for (const raw of this.lib.read(fk).split(/\r?\n/)) {
|
|
1885
|
+
let t = raw.trim();
|
|
1886
|
+
if (!t)
|
|
1887
|
+
continue;
|
|
1888
|
+
let colon = false;
|
|
1889
|
+
if (t[0] === "0") {
|
|
1890
|
+
const m = /^0\s+(\S+)\s*(.*)$/.exec(t);
|
|
1891
|
+
const kw = (m?.[1] ?? "").toUpperCase(), rest = m?.[2] ?? "";
|
|
1892
|
+
if (kw === "!TEXMAP") {
|
|
1893
|
+
const op = (rest.split(/\s+/)[0] || "").toUpperCase();
|
|
1894
|
+
next = null;
|
|
1895
|
+
if (op === "START" || op === "NEXT") {
|
|
1896
|
+
const map = parseTexMap(rest.replace(/^\S+/, ""), op === "NEXT");
|
|
1897
|
+
if (map) {
|
|
1898
|
+
const pm = transformTexMap(map, M);
|
|
1899
|
+
if (op === "START")
|
|
1900
|
+
stack.push({ map: pm, fallback: false });
|
|
1901
|
+
else
|
|
1902
|
+
next = pm;
|
|
1903
|
+
}
|
|
1904
|
+
} else if (op === "FALLBACK") {
|
|
1905
|
+
const top = stack[stack.length - 1];
|
|
1906
|
+
if (top)
|
|
1907
|
+
top.fallback = true;
|
|
1908
|
+
} else if (op === "END")
|
|
1909
|
+
stack.pop();
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
if (kw === "!:") {
|
|
1913
|
+
next = null;
|
|
1914
|
+
t = rest.trim();
|
|
1915
|
+
colon = true;
|
|
1916
|
+
if (!t || t[0] === "0")
|
|
1917
|
+
continue;
|
|
1918
|
+
} else {
|
|
1919
|
+
if (kw === "BFC") {
|
|
1920
|
+
if (/\bCW\b/.test(t))
|
|
1921
|
+
ccw = false;
|
|
1922
|
+
if (/\bCCW\b/.test(t))
|
|
1923
|
+
ccw = true;
|
|
1924
|
+
if (/INVERTNEXT/.test(t))
|
|
1925
|
+
invertNext = true;
|
|
1926
|
+
}
|
|
1927
|
+
if (kw === "STEP")
|
|
1928
|
+
stack = [];
|
|
1929
|
+
next = null;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
let tex = texIn;
|
|
1934
|
+
if (next) {
|
|
1935
|
+
tex = next;
|
|
1936
|
+
next = null;
|
|
1937
|
+
} else {
|
|
1938
|
+
const top = stack[stack.length - 1];
|
|
1939
|
+
if (top) {
|
|
1940
|
+
if (top.fallback) {
|
|
1941
|
+
if (!colon)
|
|
1942
|
+
continue;
|
|
1943
|
+
} else
|
|
1944
|
+
tex = top.map;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const c = t[0];
|
|
1948
|
+
if (c === "1") {
|
|
1949
|
+
const r = parseType1(t);
|
|
1950
|
+
if (!r)
|
|
1951
|
+
continue;
|
|
1952
|
+
const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
|
|
1953
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
1954
|
+
const ck2 = this.resolve(r.name, isTop);
|
|
1955
|
+
if (!ck2) {
|
|
1956
|
+
invertNext = false;
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
const inv = invert !== invertNext !== det3(r.m) < 0;
|
|
1960
|
+
invertNext = false;
|
|
1961
|
+
if (isTop && separateStuds)
|
|
1962
|
+
continue;
|
|
1963
|
+
walk(ck2, compose(M, r.m), r.color === "16" ? color : r.color, inv, isUnder ? "under" : target, depth + 1, tex);
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (c !== "3" && c !== "4")
|
|
1967
|
+
continue;
|
|
1968
|
+
const tok = t.split(/\s+/);
|
|
1969
|
+
const code = tok[1] === "16" ? color : tok[1];
|
|
1970
|
+
const n = c === "3" ? 3 : 4;
|
|
1971
|
+
const v = tok.slice(2, 2 + n * 3).map(Number);
|
|
1972
|
+
const pts = [];
|
|
1973
|
+
for (let i = 0;i < n; i++)
|
|
1974
|
+
pts.push(xf2(M, v.slice(i * 3, i * 3 + 3)));
|
|
1975
|
+
let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
|
|
1976
|
+
if (!ccw !== invert)
|
|
1977
|
+
order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
|
|
1978
|
+
const rgba = colorOf(code);
|
|
1979
|
+
if (tex) {
|
|
1980
|
+
let b = texb.get(tex.texture);
|
|
1981
|
+
if (!b)
|
|
1982
|
+
texb.set(tex.texture, b = { pos: [], col: [], uv: [] });
|
|
1983
|
+
if (Math.abs(tex.a ?? 0) >= 360)
|
|
1984
|
+
b.wrapU = true;
|
|
1985
|
+
for (let k = 0;k < order.length; k += 3) {
|
|
1986
|
+
const tri = [pts[order[k]], pts[order[k + 1]], pts[order[k + 2]]];
|
|
1987
|
+
const w = texmapTriangleUV(tex, tri);
|
|
1988
|
+
for (let j = 0;j < 3; j++) {
|
|
1989
|
+
b.pos.push(...tri[j]);
|
|
1990
|
+
b.col.push(...rgba);
|
|
1991
|
+
b.uv.push(w[j * 2], w[j * 2 + 1]);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
} else if (target === "body")
|
|
1995
|
+
for (const i of order) {
|
|
1996
|
+
tbody.pos.push(...pts[i]);
|
|
1997
|
+
tbody.col.push(...rgba);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
walk(key, IDENT, "16", false, "body", 0, null);
|
|
2002
|
+
if (texb.size) {
|
|
2003
|
+
out.tbody = tbody;
|
|
2004
|
+
out.texb = texb;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
1231
2007
|
}
|
|
1232
2008
|
function enclosureMask(c) {
|
|
1233
2009
|
const [x0, , z0, x1, y1, z1] = c.bbox;
|
|
@@ -1310,10 +2086,10 @@ function growBox(b, M, cb) {
|
|
|
1310
2086
|
for (const z of [cb[2], cb[5]])
|
|
1311
2087
|
grow(b, xf2(M, [x, y, z]));
|
|
1312
2088
|
}
|
|
1313
|
-
function writePack(file, parts, studKinds, weld, extra) {
|
|
1314
|
-
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
|
|
2089
|
+
function writePack(file, parts, studKinds, weld, extra, options) {
|
|
2090
|
+
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
|
|
1315
2091
|
}
|
|
1316
|
-
function encodePack(parts, studKinds, weld, extra) {
|
|
2092
|
+
function encodePack(parts, studKinds, weld, extra, options = {}) {
|
|
1317
2093
|
const chunks = [];
|
|
1318
2094
|
let off = 0;
|
|
1319
2095
|
const put = (a) => {
|
|
@@ -1322,13 +2098,15 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
1322
2098
|
chunks.push(new Uint8Array(pad));
|
|
1323
2099
|
off += pad;
|
|
1324
2100
|
}
|
|
1325
|
-
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
|
|
2101
|
+
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
|
|
1326
2102
|
chunks.push(a);
|
|
1327
2103
|
off += a.byteLength;
|
|
1328
2104
|
return r;
|
|
1329
2105
|
};
|
|
2106
|
+
let lastPos = [];
|
|
1330
2107
|
const bucket = (b) => {
|
|
1331
2108
|
const n = b.pos.length / 3;
|
|
2109
|
+
lastPos = [];
|
|
1332
2110
|
if (!n)
|
|
1333
2111
|
return null;
|
|
1334
2112
|
const nrm = new Float32Array(n * 3);
|
|
@@ -1347,8 +2125,10 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
1347
2125
|
nrm[(i + k) * 3 + 2] = nz;
|
|
1348
2126
|
}
|
|
1349
2127
|
}
|
|
1350
|
-
if (!weld)
|
|
2128
|
+
if (!weld) {
|
|
2129
|
+
lastPos = b.pos;
|
|
1351
2130
|
return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
|
|
2131
|
+
}
|
|
1352
2132
|
const map = new Map;
|
|
1353
2133
|
const P = [], N = [], C = [], I = [];
|
|
1354
2134
|
for (let i = 0;i < n; i++) {
|
|
@@ -1364,10 +2144,111 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
1364
2144
|
I.push(j);
|
|
1365
2145
|
}
|
|
1366
2146
|
const vcount = P.length / 3;
|
|
2147
|
+
lastPos = P;
|
|
1367
2148
|
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
2149
|
};
|
|
1369
|
-
|
|
1370
|
-
const
|
|
2150
|
+
let anyCond = false;
|
|
2151
|
+
const condl = (a, pools) => {
|
|
2152
|
+
if (!a || !a.length)
|
|
2153
|
+
return;
|
|
2154
|
+
anyCond = true;
|
|
2155
|
+
const key = (p, i) => Math.round(p[i] * 1000) + "," + Math.round(p[i + 1] * 1000) + "," + Math.round(p[i + 2] * 1000);
|
|
2156
|
+
const need = new Map;
|
|
2157
|
+
for (let i = 0;i < a.length; i += 3)
|
|
2158
|
+
need.set(key(a, i), -1);
|
|
2159
|
+
let base = 0;
|
|
2160
|
+
for (const p of pools) {
|
|
2161
|
+
for (let i = 0;i < p.length; i += 3) {
|
|
2162
|
+
const k = key(p, i);
|
|
2163
|
+
if (need.get(k) === -1)
|
|
2164
|
+
need.set(k, base + i / 3);
|
|
2165
|
+
}
|
|
2166
|
+
base += p.length / 3;
|
|
2167
|
+
}
|
|
2168
|
+
const X = [], I = new Array(a.length / 3);
|
|
2169
|
+
for (let i = 0;i < a.length; i += 3) {
|
|
2170
|
+
const k = key(a, i);
|
|
2171
|
+
let j = need.get(k);
|
|
2172
|
+
if (j === -1) {
|
|
2173
|
+
j = base + X.length / 3;
|
|
2174
|
+
need.set(k, j);
|
|
2175
|
+
X.push(a[i], a[i + 1], a[i + 2]);
|
|
2176
|
+
}
|
|
2177
|
+
I[i / 3] = j;
|
|
2178
|
+
}
|
|
2179
|
+
let prev = 0, max = 0;
|
|
2180
|
+
for (let i = 0;i < I.length; i++) {
|
|
2181
|
+
const d = I[i] - prev;
|
|
2182
|
+
prev = I[i];
|
|
2183
|
+
const z = d >= 0 ? d * 2 : -d * 2 - 1;
|
|
2184
|
+
I[i] = z;
|
|
2185
|
+
if (z > max)
|
|
2186
|
+
max = z;
|
|
2187
|
+
}
|
|
2188
|
+
return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
|
|
2189
|
+
};
|
|
2190
|
+
const texNames = [];
|
|
2191
|
+
const texBucket = (b) => {
|
|
2192
|
+
const n = b.pos.length / 3;
|
|
2193
|
+
if (!n)
|
|
2194
|
+
return null;
|
|
2195
|
+
const nrm = new Float32Array(n * 3);
|
|
2196
|
+
for (let i = 0;i < n; i += 3) {
|
|
2197
|
+
const p = b.pos;
|
|
2198
|
+
const a = i * 3;
|
|
2199
|
+
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];
|
|
2200
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
2201
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
2202
|
+
nx /= l;
|
|
2203
|
+
ny /= l;
|
|
2204
|
+
nz /= l;
|
|
2205
|
+
for (let k = 0;k < 3; k++) {
|
|
2206
|
+
nrm[(i + k) * 3] = nx;
|
|
2207
|
+
nrm[(i + k) * 3 + 1] = ny;
|
|
2208
|
+
nrm[(i + k) * 3 + 2] = nz;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const map = new Map;
|
|
2212
|
+
const P = [], N = [], C = [], U = [], I = [];
|
|
2213
|
+
for (let i = 0;i < n; i++) {
|
|
2214
|
+
const k = weld ? `${b.pos.slice(i * 3, i * 3 + 3).map((x) => x.toFixed(3))},${[...nrm.subarray(i * 3, i * 3 + 3)].map((x) => x.toFixed(3))},${b.col.slice(i * 4, i * 4 + 4).join()},${b.uv[i * 2].toFixed(5)},${b.uv[i * 2 + 1].toFixed(5)}` : String(i);
|
|
2215
|
+
let j = map.get(k);
|
|
2216
|
+
if (j === undefined) {
|
|
2217
|
+
j = P.length / 3;
|
|
2218
|
+
map.set(k, j);
|
|
2219
|
+
P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
|
|
2220
|
+
N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
|
|
2221
|
+
C.push(...b.col.slice(i * 4, i * 4 + 4));
|
|
2222
|
+
U.push(b.uv[i * 2], b.uv[i * 2 + 1]);
|
|
2223
|
+
}
|
|
2224
|
+
I.push(j);
|
|
2225
|
+
}
|
|
2226
|
+
return { pos: put(new Float32Array(P)), nrm: put(new Float32Array(N)), col: put(new Float32Array(C)), uv: put(new Float32Array(U)), idx: put(P.length / 3 < 65536 ? new Uint16Array(I) : new Uint32Array(I)), tris: n / 3 };
|
|
2227
|
+
};
|
|
2228
|
+
const encTex = (c) => {
|
|
2229
|
+
if (!c.texb)
|
|
2230
|
+
return {};
|
|
2231
|
+
const tex = [...c.texb].map(([name, b]) => {
|
|
2232
|
+
let t = texNames.indexOf(name);
|
|
2233
|
+
if (t < 0)
|
|
2234
|
+
t = texNames.push(name) - 1;
|
|
2235
|
+
return { t, ...b.wrapU ? { wrapU: true } : {}, ...texBucket(b) };
|
|
2236
|
+
});
|
|
2237
|
+
return { tbody: bucket(c.tbody), tex };
|
|
2238
|
+
};
|
|
2239
|
+
const enc = (c) => {
|
|
2240
|
+
const body = bucket(c.body), bodyP = lastPos, under = bucket(c.under), underP = lastPos;
|
|
2241
|
+
const edges = c.edges.length ? put(new Float32Array(c.edges)) : null;
|
|
2242
|
+
const condlB = condl(c.cond, [bodyP, underP, c.edges]), condlU = condl(c.condUnder, [bodyP, underP, c.edges]);
|
|
2243
|
+
return { body, under, edges, cond: c.condLines, condl: condlB, condlu: condlU, studs: c.studs.map((s) => [s.kind, s.color, ...s.m.map((x) => +x.toFixed(4))]), bbox: c.bbox, fullBottom: c.fullBottom, uncert: !!c.uncertified, mask: c.bottomMask ? [c.bottomMask.nx, c.bottomMask.nz, c.bottomMask.bits.join("")] : null, ...encTex(c) };
|
|
2244
|
+
};
|
|
2245
|
+
const encParts = Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), encKinds = Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)]));
|
|
2246
|
+
const textures = texNames.map((name) => {
|
|
2247
|
+
const b = options.texture?.(name);
|
|
2248
|
+
return b ? { name, mime: "image/png", data: put(b) } : { name, missing: true };
|
|
2249
|
+
});
|
|
2250
|
+
const features = [...anyCond ? ["cond"] : [], ...texNames.length ? ["tex"] : []];
|
|
2251
|
+
const header = { version: features.length ? 2 : 1, ...features.length ? { features } : {}, weld, parts: encParts, studKinds: encKinds, ...texNames.length ? { textures } : {}, ...extra };
|
|
1371
2252
|
const hj = new TextEncoder().encode(JSON.stringify(header));
|
|
1372
2253
|
const hpad = (4 - (4 + hj.length) % 4) % 4;
|
|
1373
2254
|
const head = new Uint8Array(4 + hj.length + hpad);
|
|
@@ -1382,9 +2263,14 @@ function buildPack(o) {
|
|
|
1382
2263
|
const lib = new Library2(o.ldrawRoot);
|
|
1383
2264
|
const refs = new Map;
|
|
1384
2265
|
const missing = new Set;
|
|
2266
|
+
const data = new Map;
|
|
1385
2267
|
for (const m of o.models ?? []) {
|
|
1386
2268
|
const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
|
|
1387
|
-
const
|
|
2269
|
+
const sm = splitModel(text, name);
|
|
2270
|
+
for (const [k, v] of sm.data)
|
|
2271
|
+
if (!data.has(k))
|
|
2272
|
+
data.set(k, v);
|
|
2273
|
+
const r = flatten2(sm, lib, (i) => refs.set(i.ref, i.part));
|
|
1388
2274
|
for (const k of r.missing.keys())
|
|
1389
2275
|
missing.add(k);
|
|
1390
2276
|
}
|
|
@@ -1400,7 +2286,7 @@ function buildPack(o) {
|
|
|
1400
2286
|
if (lib.isPart(k) && /\.dat$/.test(k))
|
|
1401
2287
|
refs.set(k.slice(6), k);
|
|
1402
2288
|
}
|
|
1403
|
-
const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
|
|
2289
|
+
const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true, cond: !!o.cond, tex: !!o.textures });
|
|
1404
2290
|
const parts = new Map, kinds = new Map;
|
|
1405
2291
|
let n = 0;
|
|
1406
2292
|
for (const [ref, key] of refs) {
|
|
@@ -1421,7 +2307,11 @@ function buildPack(o) {
|
|
|
1421
2307
|
if (fs3.existsSync(cfg))
|
|
1422
2308
|
extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
|
|
1423
2309
|
}
|
|
1424
|
-
|
|
2310
|
+
const texture = (n) => {
|
|
2311
|
+
const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
|
|
2312
|
+
return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
|
|
2313
|
+
};
|
|
2314
|
+
return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing] };
|
|
1425
2315
|
}
|
|
1426
2316
|
export {
|
|
1427
2317
|
Compiler,
|
|
@@ -1432,6 +2322,9 @@ export {
|
|
|
1432
2322
|
buildPack,
|
|
1433
2323
|
collectDeps,
|
|
1434
2324
|
computeCulling,
|
|
2325
|
+
conditionalLineVisible,
|
|
2326
|
+
conditionalLines,
|
|
2327
|
+
createInstancedConditionalEdges,
|
|
1435
2328
|
createInstancedEdges,
|
|
1436
2329
|
createLdrObject,
|
|
1437
2330
|
decodePack,
|
|
@@ -1449,8 +2342,13 @@ export {
|
|
|
1449
2342
|
parseColor,
|
|
1450
2343
|
parseLDConfig,
|
|
1451
2344
|
parseModel,
|
|
2345
|
+
parseTexMap,
|
|
2346
|
+
resolveTexture,
|
|
1452
2347
|
sequenceSteps,
|
|
1453
2348
|
smoothNormals,
|
|
1454
2349
|
splitModel,
|
|
2350
|
+
texmapTriangleUV,
|
|
2351
|
+
texmapUV,
|
|
2352
|
+
transformTexMap,
|
|
1455
2353
|
writePack
|
|
1456
2354
|
};
|