ldr 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -6
- package/dist/cli.js +1212 -289
- package/dist/index.d.ts +3 -3
- package/dist/index.js +876 -43
- package/dist/model.d.ts +30 -3
- package/dist/node/build-pack.d.ts +11 -0
- package/dist/node/compile.d.ts +29 -2
- package/dist/node/index.js +1401 -134
- package/dist/node/library.d.ts +19 -4
- package/dist/node/resolver.d.ts +29 -1
- package/dist/render.d.ts +26 -0
- package/dist/scene.d.ts +22 -0
- package/dist/spec.d.ts +166 -9
- package/package.json +1 -1
package/dist/node/index.js
CHANGED
|
@@ -6,41 +6,250 @@ function parseColor(tok) {
|
|
|
6
6
|
return +tok;
|
|
7
7
|
}
|
|
8
8
|
var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
|
|
9
|
-
|
|
9
|
+
var SPECIAL0 = new Set(["FILE", "NOFILE", "!DATA", "!:", "STEP", "BFC", "ROTSTEP", "BUFEXCHG", "!TEXMAP"]);
|
|
10
|
+
var PART_ORG = new Set(["part", "subpart", "primitive", "8_primitive", "48_primitive", "shortcut", "helper"]);
|
|
11
|
+
var MODEL_ORG = new Set(["model", "submodel", "configuration"]);
|
|
12
|
+
var ORG_RE = /^(?:!?LDRAW_ORG|Unofficial|Official\s+LCAD)\s+(\S+)/i;
|
|
13
|
+
function orgType(text) {
|
|
14
|
+
const m = ORG_RE.exec(text);
|
|
15
|
+
if (!m)
|
|
16
|
+
return;
|
|
17
|
+
const t = m[1].toLowerCase().replace(/^unofficial_/, "");
|
|
18
|
+
return PART_ORG.has(t) || MODEL_ORG.has(t) ? t : undefined;
|
|
19
|
+
}
|
|
20
|
+
function texNames(rest) {
|
|
21
|
+
const out = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < rest.length) {
|
|
24
|
+
while (rest[i] === " " || rest[i] === "\t")
|
|
25
|
+
i++;
|
|
26
|
+
if (i >= rest.length)
|
|
27
|
+
break;
|
|
28
|
+
if (rest[i] === '"') {
|
|
29
|
+
let v = "";
|
|
30
|
+
i++;
|
|
31
|
+
while (i < rest.length && rest[i] !== '"') {
|
|
32
|
+
if (rest[i] === "\\" && i + 1 < rest.length)
|
|
33
|
+
i++;
|
|
34
|
+
v += rest[i++];
|
|
35
|
+
}
|
|
36
|
+
i++;
|
|
37
|
+
out.push(v);
|
|
38
|
+
} else {
|
|
39
|
+
let j = i;
|
|
40
|
+
while (j < rest.length && rest[j] !== " " && rest[j] !== "\t")
|
|
41
|
+
j++;
|
|
42
|
+
out.push(rest.slice(i, j));
|
|
43
|
+
i = j;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function parseTexMap(tail, next = false) {
|
|
49
|
+
const m = /^\s*(\S+)\s+(.*)$/.exec(tail);
|
|
50
|
+
if (!m)
|
|
51
|
+
return null;
|
|
52
|
+
const method = m[1].toUpperCase();
|
|
53
|
+
const n = method === "PLANAR" ? 9 : method === "CYLINDRICAL" ? 10 : method === "SPHERICAL" ? 11 : 0;
|
|
54
|
+
if (!n)
|
|
55
|
+
return null;
|
|
56
|
+
const parts = m[2].trim().split(/\s+/);
|
|
57
|
+
if (parts.length < n + 1)
|
|
58
|
+
return null;
|
|
59
|
+
const v = parts.slice(0, n).map(Number);
|
|
60
|
+
if (!v.every(Number.isFinite))
|
|
61
|
+
return null;
|
|
62
|
+
let rest = m[2].trim();
|
|
63
|
+
for (let k = 0;k < n; k++)
|
|
64
|
+
rest = rest.replace(/^\S+\s*/, "");
|
|
65
|
+
const names = texNames(rest);
|
|
66
|
+
if (!names.length)
|
|
67
|
+
return null;
|
|
68
|
+
const t = { method, p1: v.slice(0, 3), p2: v.slice(3, 6), p3: v.slice(6, 9), texture: names[0] };
|
|
69
|
+
if (n >= 10)
|
|
70
|
+
t.a = v[9];
|
|
71
|
+
if (n === 11)
|
|
72
|
+
t.b = v[10];
|
|
73
|
+
const g = names.findIndex((x, i) => i > 0 && x.toUpperCase() === "GLOSSMAP");
|
|
74
|
+
if (g > 0 && names[g + 1])
|
|
75
|
+
t.glossmap = names[g + 1];
|
|
76
|
+
if (next)
|
|
77
|
+
t.next = true;
|
|
78
|
+
return t;
|
|
79
|
+
}
|
|
80
|
+
var b64 = (s) => {
|
|
81
|
+
const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
|
|
82
|
+
const pad = clean + "=".repeat((4 - clean.length % 4) % 4);
|
|
83
|
+
const bin = atob(pad);
|
|
84
|
+
const out = new Uint8Array(bin.length);
|
|
85
|
+
for (let i = 0;i < bin.length; i++)
|
|
86
|
+
out[i] = bin.charCodeAt(i);
|
|
87
|
+
return out;
|
|
88
|
+
};
|
|
89
|
+
function geomLine(tk) {
|
|
90
|
+
const t = +tk[0];
|
|
91
|
+
if (!(t >= 1 && t <= 5) || tk.length < 2)
|
|
92
|
+
return null;
|
|
93
|
+
const color = parseColor(tk[1]);
|
|
94
|
+
const nums = tk.slice(2).map(Number);
|
|
95
|
+
if (t === 1)
|
|
96
|
+
return { t: 1, color, m: nums.slice(0, 12), file: norm(tk.slice(14).join(" ")) };
|
|
97
|
+
if (t === 2)
|
|
98
|
+
return { t: 2, color, p: nums.slice(0, 6) };
|
|
99
|
+
if (t === 3)
|
|
100
|
+
return { t: 3, color, p: nums.slice(0, 9) };
|
|
101
|
+
if (t === 4)
|
|
102
|
+
return { t: 4, color, p: nums.slice(0, 12) };
|
|
103
|
+
return { t: 5, color, p: nums.slice(0, 12) };
|
|
104
|
+
}
|
|
105
|
+
function parse(src, name = "main.ldr", popts = {}) {
|
|
106
|
+
const gtext = !!popts.geometryText;
|
|
10
107
|
const files = new Map;
|
|
11
108
|
let cur = null;
|
|
12
109
|
let main = "";
|
|
110
|
+
let data;
|
|
111
|
+
let dataCur = null;
|
|
112
|
+
let texStack = [];
|
|
113
|
+
let texNext = null;
|
|
114
|
+
const closeData = () => {
|
|
115
|
+
if (!dataCur)
|
|
116
|
+
return;
|
|
117
|
+
const bytes = b64(dataCur.chunks.join(""));
|
|
118
|
+
const png = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
119
|
+
(data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
|
|
120
|
+
dataCur = null;
|
|
121
|
+
};
|
|
122
|
+
let inHeader = true;
|
|
13
123
|
const start = (n) => {
|
|
124
|
+
closeData();
|
|
14
125
|
cur = { name: norm(n), cmds: [] };
|
|
15
126
|
files.set(cur.name, cur);
|
|
16
127
|
if (!main)
|
|
17
128
|
main = cur.name;
|
|
129
|
+
texStack = [];
|
|
130
|
+
texNext = null;
|
|
131
|
+
inHeader = true;
|
|
132
|
+
};
|
|
133
|
+
let skip = false, sawBlock = false;
|
|
134
|
+
const firstBlock = () => {
|
|
135
|
+
if (sawBlock)
|
|
136
|
+
return;
|
|
137
|
+
sawBlock = true;
|
|
138
|
+
files.clear();
|
|
139
|
+
cur = null;
|
|
140
|
+
main = "";
|
|
141
|
+
};
|
|
142
|
+
const annotate = (c, fromColon) => {
|
|
143
|
+
if (texNext) {
|
|
144
|
+
c.tex = texNext;
|
|
145
|
+
texNext = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const top = texStack[texStack.length - 1];
|
|
149
|
+
if (!top)
|
|
150
|
+
return;
|
|
151
|
+
if (top.fallback) {
|
|
152
|
+
if (!fromColon)
|
|
153
|
+
c.fallback = true;
|
|
154
|
+
} else
|
|
155
|
+
c.tex = top.map;
|
|
18
156
|
};
|
|
19
157
|
for (const raw of src.split(/\r?\n/)) {
|
|
20
158
|
const line = raw.trim();
|
|
21
159
|
if (!line)
|
|
22
160
|
continue;
|
|
161
|
+
if (gtext && line.charCodeAt(0) >= 50 && line.charCodeAt(0) <= 53 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
|
|
162
|
+
if (dataCur || skip)
|
|
163
|
+
continue;
|
|
164
|
+
if (!cur)
|
|
165
|
+
start(name);
|
|
166
|
+
const sp = line.slice(2).trimStart();
|
|
167
|
+
const e = sp.search(/\s/);
|
|
168
|
+
const g = { t: line.charCodeAt(0) - 48, color: parseColor(e < 0 ? sp : sp.slice(0, e)), p: [], src: line };
|
|
169
|
+
annotate(g, false);
|
|
170
|
+
cur.cmds.push(g);
|
|
171
|
+
inHeader = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (line.charCodeAt(0) === 48 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
|
|
175
|
+
let a = 2;
|
|
176
|
+
while (line.charCodeAt(a) === 32 || line.charCodeAt(a) === 9)
|
|
177
|
+
a++;
|
|
178
|
+
let b = a;
|
|
179
|
+
while (b < line.length && line.charCodeAt(b) !== 32 && line.charCodeAt(b) !== 9)
|
|
180
|
+
b++;
|
|
181
|
+
const kw = line.slice(a, b);
|
|
182
|
+
if (!SPECIAL0.has(kw)) {
|
|
183
|
+
if (skip || dataCur)
|
|
184
|
+
continue;
|
|
185
|
+
if (!cur)
|
|
186
|
+
start(name);
|
|
187
|
+
const text = line.slice(a);
|
|
188
|
+
const c = { t: 0, text };
|
|
189
|
+
if (kw.charCodeAt(0) === 33)
|
|
190
|
+
c.meta = kw;
|
|
191
|
+
texNext = null;
|
|
192
|
+
if (inHeader && !cur.org) {
|
|
193
|
+
const o = orgType(text);
|
|
194
|
+
if (o)
|
|
195
|
+
cur.org = o;
|
|
196
|
+
}
|
|
197
|
+
const moved = text.match(MOVED_RE);
|
|
198
|
+
if (moved && !cur.movedTo) {
|
|
199
|
+
let target = moved[1].trim().replace(/\\/g, "/");
|
|
200
|
+
if (!/\.(dat|ldr|mpd)$/i.test(target))
|
|
201
|
+
target += ".dat";
|
|
202
|
+
cur.movedTo = norm(target);
|
|
203
|
+
}
|
|
204
|
+
cur.cmds.push(c);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
23
208
|
const tk = line.split(/\s+/);
|
|
24
209
|
const t = +tk[0];
|
|
25
210
|
if (t === 0) {
|
|
26
211
|
const text = line.slice(1).trim();
|
|
27
212
|
if (tk[1] === "FILE") {
|
|
213
|
+
firstBlock();
|
|
214
|
+
skip = false;
|
|
28
215
|
start(text.slice(5));
|
|
29
216
|
continue;
|
|
30
217
|
}
|
|
31
218
|
if (tk[1] === "NOFILE") {
|
|
219
|
+
closeData();
|
|
32
220
|
cur = null;
|
|
221
|
+
skip = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (tk[1] === "!DATA") {
|
|
225
|
+
firstBlock();
|
|
226
|
+
closeData();
|
|
227
|
+
cur = null;
|
|
228
|
+
skip = false;
|
|
229
|
+
dataCur = { name: text.slice(5).trim(), chunks: [] };
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (skip)
|
|
233
|
+
continue;
|
|
234
|
+
if (dataCur) {
|
|
235
|
+
if (tk[1] === "!:")
|
|
236
|
+
dataCur.chunks.push(tk.slice(2).join(""));
|
|
33
237
|
continue;
|
|
34
238
|
}
|
|
35
239
|
if (!cur)
|
|
36
240
|
start(name);
|
|
37
241
|
const c = { t: 0, text };
|
|
38
|
-
if (tk[1] === "STEP")
|
|
242
|
+
if (tk[1] === "STEP") {
|
|
39
243
|
c.step = true;
|
|
40
|
-
|
|
244
|
+
texStack = [];
|
|
245
|
+
texNext = null;
|
|
246
|
+
} else if (tk[1] === "BFC")
|
|
41
247
|
c.bfc = tk.slice(2).join(" ");
|
|
42
248
|
else if (tk[1] === "ROTSTEP") {
|
|
43
249
|
c.meta = "ROTSTEP";
|
|
250
|
+
c.step = true;
|
|
251
|
+
texStack = [];
|
|
252
|
+
texNext = null;
|
|
44
253
|
if ((tk[2] ?? "").toUpperCase() === "END")
|
|
45
254
|
c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
|
|
46
255
|
else {
|
|
@@ -53,6 +262,42 @@ function parse(src, name = "main.ldr") {
|
|
|
53
262
|
c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
|
|
54
263
|
} else if (tk[1]?.startsWith("!"))
|
|
55
264
|
c.meta = tk[1];
|
|
265
|
+
if (inHeader && !cur.org) {
|
|
266
|
+
const o = orgType(text);
|
|
267
|
+
if (o)
|
|
268
|
+
cur.org = o;
|
|
269
|
+
}
|
|
270
|
+
if (tk[1] === "!TEXMAP") {
|
|
271
|
+
const op = (tk[2] ?? "").toUpperCase();
|
|
272
|
+
texNext = null;
|
|
273
|
+
if (op === "START" || op === "NEXT") {
|
|
274
|
+
const map = parseTexMap(text.replace(/^!TEXMAP\s+\S+/i, ""), op === "NEXT");
|
|
275
|
+
c.texmap = { op, ...map ? { map } : {} };
|
|
276
|
+
if (map) {
|
|
277
|
+
(cur.texmaps ??= []).push(map);
|
|
278
|
+
if (op === "START")
|
|
279
|
+
texStack.push({ map, fallback: false });
|
|
280
|
+
else
|
|
281
|
+
texNext = map;
|
|
282
|
+
}
|
|
283
|
+
} else if (op === "FALLBACK") {
|
|
284
|
+
c.texmap = { op };
|
|
285
|
+
const top = texStack[texStack.length - 1];
|
|
286
|
+
if (top)
|
|
287
|
+
top.fallback = true;
|
|
288
|
+
} else if (op === "END") {
|
|
289
|
+
c.texmap = { op };
|
|
290
|
+
texStack.pop();
|
|
291
|
+
}
|
|
292
|
+
} else if (tk[1] === "!:") {
|
|
293
|
+
texNext = null;
|
|
294
|
+
const g = geomLine(tk.slice(2));
|
|
295
|
+
if (g) {
|
|
296
|
+
annotate(g, true);
|
|
297
|
+
c.geom = g;
|
|
298
|
+
}
|
|
299
|
+
} else
|
|
300
|
+
texNext = null;
|
|
56
301
|
const moved = text.match(MOVED_RE);
|
|
57
302
|
if (moved && !cur.movedTo) {
|
|
58
303
|
let target = moved[1].trim().replace(/\\/g, "/");
|
|
@@ -63,22 +308,110 @@ function parse(src, name = "main.ldr") {
|
|
|
63
308
|
cur.cmds.push(c);
|
|
64
309
|
continue;
|
|
65
310
|
}
|
|
311
|
+
if (dataCur || skip)
|
|
312
|
+
continue;
|
|
66
313
|
if (!cur)
|
|
67
314
|
start(name);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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) });
|
|
315
|
+
const g = geomLine(tk);
|
|
316
|
+
if (!g)
|
|
317
|
+
continue;
|
|
318
|
+
annotate(g, false);
|
|
319
|
+
cur.cmds.push(g);
|
|
320
|
+
inHeader = false;
|
|
80
321
|
}
|
|
81
|
-
|
|
322
|
+
closeData();
|
|
323
|
+
return data ? { main, files, data } : { main, files };
|
|
324
|
+
}
|
|
325
|
+
function resolveTexture(doc, name) {
|
|
326
|
+
if (!doc.data)
|
|
327
|
+
return null;
|
|
328
|
+
const n = norm(name);
|
|
329
|
+
return doc.data.get(n) ?? doc.data.get(n.replace(/^textures\//, "")) ?? doc.data.get("textures/" + n) ?? null;
|
|
330
|
+
}
|
|
331
|
+
var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
332
|
+
var dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
333
|
+
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]];
|
|
334
|
+
var unit3 = (a) => {
|
|
335
|
+
const l = Math.hypot(a[0], a[1], a[2]) || 1;
|
|
336
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
337
|
+
};
|
|
338
|
+
var DEG = Math.PI / 180;
|
|
339
|
+
function texmapUV(map, p) {
|
|
340
|
+
const w = texmapTriangleUV(map, [p, p, p]);
|
|
341
|
+
const u = w[0], v = w[1];
|
|
342
|
+
return { u, v, inside: u >= -0.000001 && u <= 1 + 0.000001 && v >= -0.000001 && v <= 1 + 0.000001 };
|
|
343
|
+
}
|
|
344
|
+
var radial = (p, c, n) => {
|
|
345
|
+
const d = sub3(p, c);
|
|
346
|
+
const k = dot3(d, n);
|
|
347
|
+
const r = [d[0] - n[0] * k, d[1] - n[1] * k, d[2] - n[2] * k];
|
|
348
|
+
return Math.hypot(r[0], r[1], r[2]) < 0.00001 ? null : unit3(r);
|
|
349
|
+
};
|
|
350
|
+
function texmapTriangleUV(map, pts) {
|
|
351
|
+
const { p1, p2, p3 } = map;
|
|
352
|
+
const out = new Array(pts.length * 2).fill(0);
|
|
353
|
+
if (map.method === "PLANAR") {
|
|
354
|
+
const e1 = sub3(p2, p1), e2 = sub3(p3, p1);
|
|
355
|
+
const l1 = dot3(e1, e1) || 1, l2 = dot3(e2, e2) || 1;
|
|
356
|
+
pts.forEach((p, i) => {
|
|
357
|
+
const d = sub3(p, p1);
|
|
358
|
+
out[i * 2] = dot3(d, e1) / l1;
|
|
359
|
+
out[i * 2 + 1] = dot3(d, e2) / l2;
|
|
360
|
+
});
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
const cyl = map.method === "CYLINDRICAL";
|
|
364
|
+
const n = cyl ? unit3(sub3(p2, p1)) : unit3(cross3(sub3(p1, p2), sub3(p3, p2)).map((x) => -x));
|
|
365
|
+
const dir = cyl ? radial(p3, p1, n) : unit3(sub3(p2, p1));
|
|
366
|
+
const A = (map.a ?? 360) * DEG;
|
|
367
|
+
let base = null, baseAng = 0;
|
|
368
|
+
const sAng = (q) => {
|
|
369
|
+
if (!base) {
|
|
370
|
+
base = q;
|
|
371
|
+
baseAng = Math.atan2(dot3(cross3(dir, q), n), dot3(q, dir));
|
|
372
|
+
return baseAng;
|
|
373
|
+
}
|
|
374
|
+
return Math.atan2(dot3(cross3(base, q), n), dot3(q, base)) + baseAng;
|
|
375
|
+
};
|
|
376
|
+
const miss = [];
|
|
377
|
+
let uSum = 0;
|
|
378
|
+
const h = cyl ? Math.hypot(...sub3(p2, p1)) || 1 : 0, n2 = cyl ? null : unit3(cross3(n, sub3(p2, p1))), B = (map.b ?? 180) * DEG;
|
|
379
|
+
pts.forEach((p, i) => {
|
|
380
|
+
if (cyl) {
|
|
381
|
+
const q = dir ? radial(p, p1, n) : null;
|
|
382
|
+
if (q) {
|
|
383
|
+
const a = sAng(q);
|
|
384
|
+
out[i * 2] = 0.5 + a / A;
|
|
385
|
+
uSum += out[i * 2];
|
|
386
|
+
} else
|
|
387
|
+
miss.push(i);
|
|
388
|
+
out[i * 2 + 1] = dot3(n, sub3(p2, p)) / h;
|
|
389
|
+
} else {
|
|
390
|
+
const pd = unit3(sub3(p, p1));
|
|
391
|
+
const c = dot3(pd, n);
|
|
392
|
+
if (Math.abs(c) > 1 - 0.000000001) {
|
|
393
|
+
miss.push(i);
|
|
394
|
+
out[i * 2 + 1] = 0.5 + (c > 0 ? -Math.PI / 2 : Math.PI / 2) / B;
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const a = sAng(radial(p, p1, n));
|
|
398
|
+
uSum += a;
|
|
399
|
+
out[i * 2] = 0.5 + a / A;
|
|
400
|
+
const th = -a, ct = Math.cos(th), st = Math.sin(th), cr = cross3(n, pd), k = dot3(n, pd) * (1 - ct);
|
|
401
|
+
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]);
|
|
402
|
+
out[i * 2 + 1] = 0.5 + Math.atan2(dot3(cross3(dir, r), n2), dot3(r, dir)) / B;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (miss.length && miss.length < pts.length) {
|
|
406
|
+
const avg = cyl ? uSum / (pts.length - miss.length) : 0.5 + uSum / (pts.length - miss.length) / A;
|
|
407
|
+
for (const i of miss)
|
|
408
|
+
out[i * 2] = avg;
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
function transformTexMap(map, m) {
|
|
413
|
+
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]];
|
|
414
|
+
return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
|
|
82
415
|
}
|
|
83
416
|
function mul(a, b) {
|
|
84
417
|
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;
|
|
@@ -164,34 +497,249 @@ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
|
|
|
164
497
|
}
|
|
165
498
|
return out;
|
|
166
499
|
}
|
|
500
|
+
var kindMemo = new WeakMap;
|
|
501
|
+
var SUBREF_RE = /^(s|8|48)\//;
|
|
502
|
+
function fileKind(doc, name) {
|
|
503
|
+
const f = doc.files.get(norm(name));
|
|
504
|
+
if (!f)
|
|
505
|
+
return "part";
|
|
506
|
+
let k = kindMemo.get(f);
|
|
507
|
+
if (k)
|
|
508
|
+
return k;
|
|
509
|
+
if (f.org)
|
|
510
|
+
k = PART_ORG.has(f.org) ? "part" : "model";
|
|
511
|
+
else if (f.name === doc.main)
|
|
512
|
+
k = "model";
|
|
513
|
+
else
|
|
514
|
+
k = f.cmds.some((c) => c.t >= 2 || c.t === 0 && !!c.geom || c.t === 1 && SUBREF_RE.test(c.file)) ? "part" : "model";
|
|
515
|
+
kindMemo.set(f, k);
|
|
516
|
+
return k;
|
|
517
|
+
}
|
|
518
|
+
var looseMemo = new WeakMap;
|
|
519
|
+
function looseSteps(f) {
|
|
520
|
+
let r = looseMemo.get(f);
|
|
521
|
+
if (r)
|
|
522
|
+
return r;
|
|
523
|
+
r = [];
|
|
524
|
+
let s = 0;
|
|
525
|
+
for (const c of f.cmds) {
|
|
526
|
+
if (c.t === 0 && c.step) {
|
|
527
|
+
s++;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if ((c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) && r[r.length - 1] !== s)
|
|
531
|
+
r.push(s);
|
|
532
|
+
}
|
|
533
|
+
looseMemo.set(f, r);
|
|
534
|
+
return r;
|
|
535
|
+
}
|
|
536
|
+
var colorToken = (c) => c > 16777215 ? "0x" + c.toString(16).toUpperCase() : String(c);
|
|
537
|
+
var lineOf = (c) => c.t === 0 ? "0 " + c.text : c.t === 1 ? `1 ${colorToken(c.color)} ${c.m.join(" ")} ${c.file}` : c.src ? c.src : `${c.t} ${colorToken(c.color)} ${c.p.join(" ")}`;
|
|
538
|
+
var loosePartName = (file, step) => `${norm(file)}#${step}`;
|
|
539
|
+
function looseFile(f, step) {
|
|
540
|
+
const out = [];
|
|
541
|
+
let s = 0;
|
|
542
|
+
const isNext = (c) => !!c && c.t === 0 && c.texmap?.op === "NEXT";
|
|
543
|
+
for (const c of f.cmds) {
|
|
544
|
+
if (c.t === 0) {
|
|
545
|
+
if (c.step) {
|
|
546
|
+
s++;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (c.bfc !== undefined) {
|
|
550
|
+
out.push(c);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (s !== step)
|
|
554
|
+
continue;
|
|
555
|
+
if (c.texmap)
|
|
556
|
+
out.push(c);
|
|
557
|
+
else if (c.geom) {
|
|
558
|
+
if (c.geom.t >= 2)
|
|
559
|
+
out.push(c);
|
|
560
|
+
else if (isNext(out[out.length - 1]))
|
|
561
|
+
out.pop();
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (s !== step)
|
|
566
|
+
continue;
|
|
567
|
+
if (c.t === 1) {
|
|
568
|
+
if (isNext(out[out.length - 1]))
|
|
569
|
+
out.pop();
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
out.push(c);
|
|
573
|
+
}
|
|
574
|
+
return { name: loosePartName(f.name, step), cmds: out };
|
|
575
|
+
}
|
|
576
|
+
function looseSource(f, step) {
|
|
577
|
+
return [`0 ldr: own geometry of ${f.name}, step ${step}`, ...looseFile(f, step).cmds.map(lineOf)].join(`
|
|
578
|
+
`) + `
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
581
|
+
function hash32(s) {
|
|
582
|
+
let h = 2166136261;
|
|
583
|
+
for (let i = 0;i < s.length; i++) {
|
|
584
|
+
h ^= s.charCodeAt(i);
|
|
585
|
+
h = Math.imul(h, 16777619);
|
|
586
|
+
}
|
|
587
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
588
|
+
}
|
|
589
|
+
function invert(m) {
|
|
590
|
+
const [x, y, z, a, b, c, d, e, f, g, h, i] = m;
|
|
591
|
+
const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
|
|
592
|
+
const det = a * A + b * B + c * C;
|
|
593
|
+
if (!Number.isFinite(det) || Math.abs(det) < 0.000000000001)
|
|
594
|
+
return null;
|
|
595
|
+
const k = 1 / det;
|
|
596
|
+
const r = [A * k, -(b * i - c * h) * k, (b * f - c * e) * k, B * k, (a * i - c * g) * k, -(a * f - c * d) * k, C * k, -(a * h - b * g) * k, (a * e - b * d) * k];
|
|
597
|
+
return [-(r[0] * x + r[1] * y + r[2] * z), -(r[3] * x + r[4] * y + r[5] * z), -(r[6] * x + r[7] * y + r[8] * z), ...r];
|
|
598
|
+
}
|
|
599
|
+
var num = (x) => String(+x.toFixed(6) || 0);
|
|
600
|
+
var quote = (s) => '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"';
|
|
601
|
+
function texWrapper(part, map) {
|
|
602
|
+
const v = [...map.p1, ...map.p2, ...map.p3, ...map.method !== "PLANAR" ? [map.a ?? 360] : [], ...map.method === "SPHERICAL" ? [map.b ?? 180] : []].map(num).join(" ");
|
|
603
|
+
const source = `0 ldr: ${part} with a model-level !TEXMAP
|
|
604
|
+
0 BFC CERTIFY CCW
|
|
605
|
+
0 !TEXMAP START ${map.method} ${v} ${quote(map.texture)}${map.glossmap ? " GLOSSMAP " + quote(map.glossmap) : ""}
|
|
606
|
+
1 16 0 0 0 1 0 0 0 1 0 0 0 1 ${norm(part)}
|
|
607
|
+
0 !TEXMAP END
|
|
608
|
+
`;
|
|
609
|
+
return { name: `${norm(part)}#tex-${hash32(source)}`, source };
|
|
610
|
+
}
|
|
611
|
+
var mul3 = (a, b) => {
|
|
612
|
+
const r = new Array(9);
|
|
613
|
+
for (let i = 0;i < 3; i++)
|
|
614
|
+
for (let j = 0;j < 3; j++)
|
|
615
|
+
r[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
|
|
616
|
+
return r;
|
|
617
|
+
};
|
|
618
|
+
function rotStepMatrix(x, y, z) {
|
|
619
|
+
const D = Math.PI / 180, s1 = Math.sin(x * D), c1 = Math.cos(x * D), s2 = Math.sin(y * D), c2 = Math.cos(y * D), s3 = Math.sin(z * D), c3 = Math.cos(z * D);
|
|
620
|
+
return [c2 * c3, -c2 * s3, s2, c1 * s3 + s1 * s2 * c3, c1 * c3 - s1 * s2 * s3, -s1 * c2, s1 * s3 - c1 * s2 * c3, s1 * c3 + c1 * s2 * s3, c1 * c2];
|
|
621
|
+
}
|
|
622
|
+
function stepViewMatrix(view, defaultView = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {
|
|
623
|
+
return !view ? [...defaultView] : view.abs ? [...view.m] : mul3(defaultView, view.m);
|
|
624
|
+
}
|
|
625
|
+
var viewMemo = new WeakMap;
|
|
626
|
+
function fileStepViews(f) {
|
|
627
|
+
const hit = viewMemo.get(f);
|
|
628
|
+
if (hit)
|
|
629
|
+
return hit;
|
|
630
|
+
const out = [];
|
|
631
|
+
let cur = null;
|
|
632
|
+
for (const c of f.cmds) {
|
|
633
|
+
if (c.t !== 0 || !c.step)
|
|
634
|
+
continue;
|
|
635
|
+
const r = c.rotstep;
|
|
636
|
+
if (r) {
|
|
637
|
+
if (r.mode === "END")
|
|
638
|
+
cur = null;
|
|
639
|
+
else {
|
|
640
|
+
const R = rotStepMatrix(r.x, r.y, r.z);
|
|
641
|
+
cur = r.mode === "ABS" ? { abs: true, m: R, rotstep: r } : r.mode === "ADD" && cur ? { abs: cur.abs, m: mul3(cur.m, R), rotstep: r } : { abs: false, m: R, rotstep: r };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
out.push(cur);
|
|
645
|
+
}
|
|
646
|
+
out.push(cur);
|
|
647
|
+
viewMemo.set(f, out);
|
|
648
|
+
return out;
|
|
649
|
+
}
|
|
167
650
|
// src/model.ts
|
|
168
651
|
function flatten(doc, opts = {}) {
|
|
169
652
|
const out = [];
|
|
170
653
|
const maxDepth = opts.maxDepth ?? 64;
|
|
171
|
-
const
|
|
654
|
+
const loose = opts.looseGeometry !== false, tx = !!opts.textures;
|
|
655
|
+
const src = new Map;
|
|
656
|
+
const lazy = (f, k) => {
|
|
657
|
+
const key = f.name + "#" + k;
|
|
658
|
+
let fn = src.get(key);
|
|
659
|
+
if (!fn) {
|
|
660
|
+
let s;
|
|
661
|
+
fn = Object.assign(() => s ??= looseSource(f, k), { file: f, step: k });
|
|
662
|
+
src.set(key, fn);
|
|
663
|
+
}
|
|
664
|
+
return fn;
|
|
665
|
+
};
|
|
666
|
+
const walk = (d, fname, m, color, path, stepPath, depth, texIn) => {
|
|
172
667
|
const f = d.files.get(fname);
|
|
173
668
|
if (!f || depth > maxDepth)
|
|
174
669
|
return;
|
|
175
670
|
let s = 0;
|
|
176
|
-
|
|
177
|
-
|
|
671
|
+
const views = fileStepViews(f);
|
|
672
|
+
const ls = loose ? looseSteps(f) : [];
|
|
673
|
+
let li = 0;
|
|
674
|
+
const emit = (part, col, mm, sp, k, embedded, tex) => {
|
|
675
|
+
const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
|
|
676
|
+
if (embedded)
|
|
677
|
+
i.embedded = true;
|
|
678
|
+
if (tex) {
|
|
679
|
+
const w = texWrapper(part, tex);
|
|
680
|
+
i.texPart = w.name;
|
|
681
|
+
i.texSource = w.source;
|
|
682
|
+
}
|
|
683
|
+
const v = views[k];
|
|
684
|
+
if (v)
|
|
685
|
+
i.view = v;
|
|
686
|
+
out.push(i);
|
|
687
|
+
return i;
|
|
688
|
+
};
|
|
689
|
+
const flushLoose = (upTo) => {
|
|
690
|
+
while (li < ls.length && ls[li] <= upTo) {
|
|
691
|
+
const k = ls[li++];
|
|
692
|
+
emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null).source = lazy(f, k);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
for (const c0 of f.cmds) {
|
|
696
|
+
if (c0.t === 0 && c0.step) {
|
|
178
697
|
s++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
let c = c0;
|
|
701
|
+
if (c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) {
|
|
702
|
+
flushLoose(s);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (c.t === 0) {
|
|
706
|
+
if (tx && c.geom?.t === 1)
|
|
707
|
+
c = c.geom;
|
|
708
|
+
else
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
179
711
|
if (c.t !== 1)
|
|
180
712
|
continue;
|
|
713
|
+
if (tx && c.fallback)
|
|
714
|
+
continue;
|
|
181
715
|
const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
|
|
716
|
+
const tex = tx ? c.tex ?? texIn : null;
|
|
717
|
+
const local = tex ? invert(c.m) : null;
|
|
718
|
+
const lt = tex && local ? transformTexMap(tex, local) : null;
|
|
182
719
|
if (d.files.has(c.file)) {
|
|
183
|
-
|
|
720
|
+
if (fileKind(d, c.file) === "model") {
|
|
721
|
+
walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1, lt);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
emit(c.file, col, mm, sp, s, true, lt);
|
|
184
725
|
continue;
|
|
185
726
|
}
|
|
186
727
|
const ext = opts.resolveModel?.(c.file);
|
|
187
728
|
if (ext) {
|
|
188
|
-
walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
|
|
729
|
+
walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1, lt);
|
|
189
730
|
continue;
|
|
190
731
|
}
|
|
191
|
-
|
|
732
|
+
emit(c.file, col, mm, sp, s, false, lt);
|
|
192
733
|
}
|
|
734
|
+
flushLoose(Infinity);
|
|
193
735
|
};
|
|
194
|
-
|
|
736
|
+
if (fileKind(doc, doc.main) === "part") {
|
|
737
|
+
const f = doc.files.get(doc.main);
|
|
738
|
+
if (f)
|
|
739
|
+
out.push({ part: doc.main, color: 16, m: [...I], step: 0, steps: [0], localStep: 0, path: doc.main, embedded: true });
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
walk(doc, doc.main, I, 16, doc.main, [], 0, null);
|
|
195
743
|
return out;
|
|
196
744
|
}
|
|
197
745
|
function parseModel(text, name = "main.ldr", opts) {
|
|
@@ -202,8 +750,14 @@ function sequenceSteps(insts) {
|
|
|
202
750
|
const uniq = [...new Set(insts.map(key))].sort();
|
|
203
751
|
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
204
752
|
const stepOf = new Int32Array(insts.length);
|
|
205
|
-
|
|
206
|
-
|
|
753
|
+
const views = new Array(uniq.length).fill(null);
|
|
754
|
+
insts.forEach((i, k) => {
|
|
755
|
+
const r = rank.get(key(i));
|
|
756
|
+
stepOf[k] = r;
|
|
757
|
+
if (i.view)
|
|
758
|
+
views[r] = i.view;
|
|
759
|
+
});
|
|
760
|
+
return { count: uniq.length, stepOf, views };
|
|
207
761
|
}
|
|
208
762
|
function parseLDConfig(text) {
|
|
209
763
|
const out = new Map;
|
|
@@ -241,7 +795,7 @@ function decodePack(buf) {
|
|
|
241
795
|
const hl = new DataView(buf).getUint32(0, true);
|
|
242
796
|
const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, hl)));
|
|
243
797
|
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]));
|
|
798
|
+
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
799
|
return { header, arr, bytes: buf.byteLength };
|
|
246
800
|
}
|
|
247
801
|
function geom(b, arr) {
|
|
@@ -253,6 +807,8 @@ function geom(b, arr) {
|
|
|
253
807
|
g.setAttribute("ldrColor", new THREE.BufferAttribute(arr(b.col), 4));
|
|
254
808
|
if (b.idx)
|
|
255
809
|
g.setIndex(new THREE.BufferAttribute(arr(b.idx), 1));
|
|
810
|
+
if (b.uv)
|
|
811
|
+
g.setAttribute("ldrUV", new THREE.BufferAttribute(arr(b.uv), 2));
|
|
256
812
|
return g;
|
|
257
813
|
}
|
|
258
814
|
function makeMaterial(side) {
|
|
@@ -268,6 +824,41 @@ function makeMaterial(side) {
|
|
|
268
824
|
m.customProgramCacheKey = () => "ldrColor";
|
|
269
825
|
return m;
|
|
270
826
|
}
|
|
827
|
+
function makeTexturedMaterial(side, map, wrapU = false) {
|
|
828
|
+
const m = makeMaterial(side);
|
|
829
|
+
const base = m.onBeforeCompile;
|
|
830
|
+
m.onBeforeCompile = (sh, r) => {
|
|
831
|
+
base.call(m, sh, r);
|
|
832
|
+
sh.uniforms.ldrMap = { value: map };
|
|
833
|
+
sh.uniforms.ldrWrapU = { value: wrapU ? 1 : 0 };
|
|
834
|
+
sh.vertexShader = `attribute vec2 ldrUV; varying vec2 vLdrUV;
|
|
835
|
+
` + sh.vertexShader.replace("#include <uv_vertex>", `#include <uv_vertex>
|
|
836
|
+
vLdrUV = ldrUV;`);
|
|
837
|
+
sh.fragmentShader = `uniform sampler2D ldrMap; uniform int ldrWrapU; varying vec2 vLdrUV;
|
|
838
|
+
` + sh.fragmentShader.replace("#include <color_fragment>", `#include <color_fragment>
|
|
839
|
+
{ vec2 uv = vec2(ldrWrapU == 1 ? fract(vLdrUV.x) : vLdrUV.x, vLdrUV.y); vec4 t = texture2D(ldrMap, uv);
|
|
840
|
+
float inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);
|
|
841
|
+
diffuseColor.rgb = mix(diffuseColor.rgb, t.rgb, t.a * inside); }`);
|
|
842
|
+
};
|
|
843
|
+
m.customProgramCacheKey = () => "ldrColorTex";
|
|
844
|
+
m.userData.ldrMap = map;
|
|
845
|
+
return m;
|
|
846
|
+
}
|
|
847
|
+
function packTexture(pack, i) {
|
|
848
|
+
const t = new THREE.Texture;
|
|
849
|
+
t.flipY = false;
|
|
850
|
+
t.colorSpace = THREE.SRGBColorSpace;
|
|
851
|
+
t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
|
|
852
|
+
const e = pack.header.textures?.[i];
|
|
853
|
+
const bytes = e?.data ? pack.arr(e.data) : null;
|
|
854
|
+
if (!bytes || typeof createImageBitmap === "undefined")
|
|
855
|
+
return { texture: t, ready: Promise.resolve() };
|
|
856
|
+
const ready = createImageBitmap(new Blob([bytes], { type: e.mime || "image/png" }), { premultiplyAlpha: "none", colorSpaceConversion: "none" }).then((img) => {
|
|
857
|
+
t.image = img;
|
|
858
|
+
t.needsUpdate = true;
|
|
859
|
+
}).catch(() => {});
|
|
860
|
+
return { texture: t, ready };
|
|
861
|
+
}
|
|
271
862
|
var lineMat = () => new THREE.ShaderMaterial({
|
|
272
863
|
vertexShader: `attribute vec4 im0; attribute vec4 im1; attribute vec4 im2; attribute vec4 im3;
|
|
273
864
|
void main(){ gl_Position = projectionMatrix * modelViewMatrix * mat4(im0,im1,im2,im3) * vec4(position,1.0); }`,
|
|
@@ -296,6 +887,109 @@ var weldLines = (pos) => {
|
|
|
296
887
|
function createInstancedEdges(pos, im, material = sharedLineMat()) {
|
|
297
888
|
return sharedLines(pos, im, material);
|
|
298
889
|
}
|
|
890
|
+
function conditionalLines(pack, entry, which = "all") {
|
|
891
|
+
if (which === "all") {
|
|
892
|
+
const a = conditionalLines(pack, entry, "body"), b = conditionalLines(pack, entry, "under");
|
|
893
|
+
if (!a || !b)
|
|
894
|
+
return a ?? b;
|
|
895
|
+
const o = new Float32Array(a.length + b.length);
|
|
896
|
+
o.set(a);
|
|
897
|
+
o.set(b, a.length);
|
|
898
|
+
return o;
|
|
899
|
+
}
|
|
900
|
+
const c = which === "under" ? entry?.condlu : entry?.condl;
|
|
901
|
+
if (!c)
|
|
902
|
+
return null;
|
|
903
|
+
const { arr } = pack;
|
|
904
|
+
if (c.raw)
|
|
905
|
+
return arr(c.raw);
|
|
906
|
+
const pools = [entry.body ? arr(entry.body.pos) : [], entry.under ? arr(entry.under.pos) : [], entry.edges ? arr(entry.edges) : [], c.x ? arr(c.x) : []];
|
|
907
|
+
const idx = arr(c.idx);
|
|
908
|
+
const out = new Float32Array(idx.length * 3);
|
|
909
|
+
const starts = [];
|
|
910
|
+
let base = 0;
|
|
911
|
+
for (const p of pools) {
|
|
912
|
+
starts.push(base);
|
|
913
|
+
base += p.length / 3;
|
|
914
|
+
}
|
|
915
|
+
let prev = 0;
|
|
916
|
+
for (let i = 0;i < idx.length; i++) {
|
|
917
|
+
const z = idx[i];
|
|
918
|
+
const j = prev + (z & 1 ? -(z + 1) / 2 : z / 2);
|
|
919
|
+
prev = j;
|
|
920
|
+
let k = 3;
|
|
921
|
+
while (k > 0 && j < starts[k])
|
|
922
|
+
k--;
|
|
923
|
+
const p = pools[k], o = (j - starts[k]) * 3;
|
|
924
|
+
out[i * 3] = p[o];
|
|
925
|
+
out[i * 3 + 1] = p[o + 1];
|
|
926
|
+
out[i * 3 + 2] = p[o + 2];
|
|
927
|
+
}
|
|
928
|
+
return out;
|
|
929
|
+
}
|
|
930
|
+
var condLineMat = () => new THREE.ShaderMaterial({
|
|
931
|
+
uniforms: { ldrViewProj: { value: new THREE.Matrix4 } },
|
|
932
|
+
vertexShader: `uniform mat4 ldrViewProj; attribute mat4 instanceMatrix; attribute vec3 ldrP1; attribute vec3 ldrC0; attribute vec3 ldrC1; attribute float ldrSide;
|
|
933
|
+
void main(){
|
|
934
|
+
mat4 mvp = ldrViewProj * instanceMatrix;
|
|
935
|
+
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);
|
|
936
|
+
vec2 A = a.xy / a.w, B = b.xy / b.w, C0 = c0.xy / c0.w, C1 = c1.xy / c1.w, d = B - A;
|
|
937
|
+
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);
|
|
938
|
+
gl_Position = s0 * s1 >= 0.0 ? (ldrSide > 0.5 ? b : a) : vec4(2.0, 2.0, 2.0, 1.0);
|
|
939
|
+
}`,
|
|
940
|
+
fragmentShader: `void main(){ gl_FragColor = vec4(0.13,0.13,0.13,1.0); }`
|
|
941
|
+
});
|
|
942
|
+
function conditionalLineVisible(cond, i, mvp) {
|
|
943
|
+
const e = mvp.elements;
|
|
944
|
+
const pr = (o) => {
|
|
945
|
+
const x = cond[o], y = cond[o + 1], z = cond[o + 2];
|
|
946
|
+
const w = e[3] * x + e[7] * y + e[11] * z + e[15];
|
|
947
|
+
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];
|
|
948
|
+
};
|
|
949
|
+
const o = i * 12;
|
|
950
|
+
const A = pr(o), B = pr(o + 3), C0 = pr(o + 6), C1 = pr(o + 9);
|
|
951
|
+
const dx = B[0] - A[0], dy = B[1] - A[1];
|
|
952
|
+
const s0 = dx * (C0[1] - A[1]) - dy * (C0[0] - A[0]), s1 = dx * (C1[1] - A[1]) - dy * (C1[0] - A[0]);
|
|
953
|
+
return s0 * s1 >= 0;
|
|
954
|
+
}
|
|
955
|
+
function createInstancedConditionalEdges(cond, im, material = condLineMat()) {
|
|
956
|
+
return condEdges(condBuffers(cond), im, material);
|
|
957
|
+
}
|
|
958
|
+
function condBuffers(cond) {
|
|
959
|
+
const n = Math.floor(cond.length / 12);
|
|
960
|
+
const buf = new Float32Array(n * 24), side = new Float32Array(n * 2);
|
|
961
|
+
for (let l = 0;l < n; l++) {
|
|
962
|
+
for (let k = 0;k < 12; k++) {
|
|
963
|
+
const v = cond[l * 12 + k];
|
|
964
|
+
buf[l * 24 + k] = v;
|
|
965
|
+
buf[l * 24 + 12 + k] = v;
|
|
966
|
+
}
|
|
967
|
+
side[l * 2 + 1] = 1;
|
|
968
|
+
}
|
|
969
|
+
return { ib: new THREE.InterleavedBuffer(buf, 12), side: new THREE.BufferAttribute(side, 1) };
|
|
970
|
+
}
|
|
971
|
+
function condEdges({ ib, side }, im, material) {
|
|
972
|
+
const g = new THREE.InstancedBufferGeometry;
|
|
973
|
+
g.setAttribute("position", new THREE.InterleavedBufferAttribute(ib, 3, 0));
|
|
974
|
+
g.setAttribute("ldrP1", new THREE.InterleavedBufferAttribute(ib, 3, 3));
|
|
975
|
+
g.setAttribute("ldrC0", new THREE.InterleavedBufferAttribute(ib, 3, 6));
|
|
976
|
+
g.setAttribute("ldrC1", new THREE.InterleavedBufferAttribute(ib, 3, 9));
|
|
977
|
+
g.setAttribute("ldrSide", side);
|
|
978
|
+
g.setAttribute("instanceMatrix", im.instanceMatrix);
|
|
979
|
+
g.instanceCount = im.count;
|
|
980
|
+
const l = new THREE.LineSegments(g, material);
|
|
981
|
+
l.frustumCulled = false;
|
|
982
|
+
l.name = "ldrCondLines";
|
|
983
|
+
l.onBeforeRender = (_r, _s, cam, _g, mat) => {
|
|
984
|
+
const u = mat.uniforms?.ldrViewProj;
|
|
985
|
+
if (!u)
|
|
986
|
+
return;
|
|
987
|
+
l.modelViewMatrix.multiplyMatrices(cam.matrixWorldInverse, l.matrixWorld);
|
|
988
|
+
u.value.multiplyMatrices(cam.projectionMatrix, l.modelViewMatrix);
|
|
989
|
+
mat.uniformsNeedUpdate = true;
|
|
990
|
+
};
|
|
991
|
+
return l;
|
|
992
|
+
}
|
|
299
993
|
function sharedLines(pos, im, material = sharedLineMat()) {
|
|
300
994
|
const { P, I } = weldLines(pos);
|
|
301
995
|
const g = new THREE.InstancedBufferGeometry;
|
|
@@ -426,29 +1120,96 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
426
1120
|
const cull = computeCulling(insts, H);
|
|
427
1121
|
mark("culling");
|
|
428
1122
|
const edgeObjs = [];
|
|
1123
|
+
const condObjs = [];
|
|
1124
|
+
const wantCond = P.cond !== false && (P.edges === "idx" || P.edges === true);
|
|
1125
|
+
let condMat = null;
|
|
1126
|
+
const cmat = () => condMat ??= condLineMat();
|
|
1127
|
+
const condCache = new Map;
|
|
1128
|
+
const addCond = (entry, im, which = "body") => {
|
|
1129
|
+
if (!wantCond)
|
|
1130
|
+
return null;
|
|
1131
|
+
const ck = which === "under" ? entry.condlu : entry.condl;
|
|
1132
|
+
if (!ck)
|
|
1133
|
+
return null;
|
|
1134
|
+
let cb = condCache.get(ck);
|
|
1135
|
+
if (cb === undefined) {
|
|
1136
|
+
const c = conditionalLines(pack, entry, which);
|
|
1137
|
+
cb = c && c.length ? condBuffers(c) : null;
|
|
1138
|
+
condCache.set(ck, cb);
|
|
1139
|
+
}
|
|
1140
|
+
if (!cb)
|
|
1141
|
+
return null;
|
|
1142
|
+
const l = condEdges(cb, im, cmat());
|
|
1143
|
+
edgeObjs.push(l);
|
|
1144
|
+
condObjs.push(l);
|
|
1145
|
+
root.add(l);
|
|
1146
|
+
return l;
|
|
1147
|
+
};
|
|
429
1148
|
const stats = { studsTotal: cull.studs.length, studsHidden: cull.studs.filter((s) => s.hidden).length, undersTotal: 0, undersHidden: 0 };
|
|
430
1149
|
const toggles = [];
|
|
431
1150
|
const studMeshes = [];
|
|
1151
|
+
const wantTex = P.tex !== false && !!H.textures;
|
|
1152
|
+
const texCache = new Map;
|
|
1153
|
+
const texMats = new Map;
|
|
1154
|
+
const pending = [];
|
|
1155
|
+
const texMat = (t, base, wrapU) => {
|
|
1156
|
+
const role = base === matF ? "F" : base === matB ? "B" : "D";
|
|
1157
|
+
const k = t + ":" + role + ":" + (wrapU ? 1 : 0);
|
|
1158
|
+
let m = texMats.get(k);
|
|
1159
|
+
if (!m) {
|
|
1160
|
+
let tx = texCache.get(t);
|
|
1161
|
+
if (!tx) {
|
|
1162
|
+
const r = packTexture(pack, t);
|
|
1163
|
+
tx = r.texture;
|
|
1164
|
+
pending.push(r.ready);
|
|
1165
|
+
texCache.set(t, tx);
|
|
1166
|
+
}
|
|
1167
|
+
texMats.set(k, m = makeTexturedMaterial(base.side, tx, wrapU));
|
|
1168
|
+
}
|
|
1169
|
+
return m;
|
|
1170
|
+
};
|
|
1171
|
+
let texBatches = 0;
|
|
432
1172
|
for (const [pk, ids] of byPart) {
|
|
433
1173
|
const part = pk.replace(/#m$/, "");
|
|
434
1174
|
const p = H.parts[part];
|
|
435
1175
|
const mat = P.uncertDouble !== false && p.uncert ? (nUncert++, matD) : pk.endsWith("#m") ? matB : matF;
|
|
436
|
-
const
|
|
1176
|
+
const textured = wantTex && !!p.tex;
|
|
1177
|
+
const body = geom(textured ? p.tbody : p.body, arr);
|
|
437
1178
|
let bodyIm = null;
|
|
438
|
-
|
|
439
|
-
const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
|
|
1179
|
+
const place = (im) => {
|
|
440
1180
|
ids.forEach((id, k) => {
|
|
441
1181
|
im.setMatrixAt(k, toM4(insts[id].m));
|
|
442
1182
|
im.setColorAt(k, color(insts[id].color));
|
|
443
1183
|
});
|
|
444
1184
|
im.computeBoundingSphere();
|
|
445
1185
|
root.add(im);
|
|
1186
|
+
};
|
|
1187
|
+
if (body) {
|
|
1188
|
+
const im = bodyIm = new THREE.InstancedMesh(body, mat, ids.length);
|
|
1189
|
+
place(im);
|
|
446
1190
|
}
|
|
1191
|
+
if (textured)
|
|
1192
|
+
for (const tb of p.tex) {
|
|
1193
|
+
const g = geom(tb, arr);
|
|
1194
|
+
const im = new THREE.InstancedMesh(g, texMat(tb.t, mat, !!tb.wrapU), ids.length);
|
|
1195
|
+
texBatches++;
|
|
1196
|
+
if (bodyIm) {
|
|
1197
|
+
im.instanceMatrix = bodyIm.instanceMatrix;
|
|
1198
|
+
im.instanceColor = bodyIm.instanceColor;
|
|
1199
|
+
im.boundingSphere = bodyIm.boundingSphere;
|
|
1200
|
+
root.add(im);
|
|
1201
|
+
} else {
|
|
1202
|
+
place(im);
|
|
1203
|
+
bodyIm = im;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
447
1206
|
if (P.edges === "idx" && p.edges && bodyIm) {
|
|
448
1207
|
const l = sharedLines(arr(p.edges), bodyIm);
|
|
449
1208
|
edgeObjs.push(l);
|
|
450
1209
|
root.add(l);
|
|
451
1210
|
}
|
|
1211
|
+
if (bodyIm)
|
|
1212
|
+
addCond(p, bodyIm);
|
|
452
1213
|
const under = geom(p.under, arr);
|
|
453
1214
|
if (under) {
|
|
454
1215
|
const ord = [...ids.filter((i) => !cull.underHidden[i]), ...ids.filter((i) => cull.underHidden[i])];
|
|
@@ -464,6 +1225,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
464
1225
|
im.count = P.cullUnder ? vis : ids.length;
|
|
465
1226
|
root.add(im);
|
|
466
1227
|
toggles.push({ mesh: im, all: ids.length, visible: vis, flag: "under" });
|
|
1228
|
+
{
|
|
1229
|
+
const cl = addCond(p, im, "under");
|
|
1230
|
+
if (cl) {
|
|
1231
|
+
toggles.push({ mesh: { set count(n) {
|
|
1232
|
+
cl.geometry.instanceCount = n;
|
|
1233
|
+
} }, all: ids.length, visible: vis, flag: "under" });
|
|
1234
|
+
cl.geometry.instanceCount = im.count;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
467
1237
|
}
|
|
468
1238
|
if (P.edges === true && p.edges) {
|
|
469
1239
|
const mats = new Float32Array(ids.length * 16);
|
|
@@ -508,6 +1278,15 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
508
1278
|
} }, all: ord.length, visible: vis, flag: "studs" });
|
|
509
1279
|
l.geometry.instanceCount = im.count;
|
|
510
1280
|
}
|
|
1281
|
+
{
|
|
1282
|
+
const cl = P.condStuds === false ? null : addCond(k, im);
|
|
1283
|
+
if (cl) {
|
|
1284
|
+
toggles.push({ mesh: { set count(n) {
|
|
1285
|
+
cl.geometry.instanceCount = n;
|
|
1286
|
+
} }, all: ord.length, visible: vis, flag: "studs" });
|
|
1287
|
+
cl.geometry.instanceCount = im.count;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
511
1290
|
if (P.edges === true && k.edges) {
|
|
512
1291
|
const mats = new Float32Array(ord.length * 16);
|
|
513
1292
|
ord.forEach((s, i) => toM4(s.w).toArray(mats, i * 16));
|
|
@@ -521,7 +1300,19 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
521
1300
|
}
|
|
522
1301
|
stats.uncertParts = Object.values(H.parts).filter((x) => x.uncert).length;
|
|
523
1302
|
stats.uncertBatches = nUncert;
|
|
524
|
-
|
|
1303
|
+
let condOn = true, edgesOn = true;
|
|
1304
|
+
const setEdges = (v) => {
|
|
1305
|
+
edgesOn = v;
|
|
1306
|
+
edgeObjs.forEach((o) => o.visible = v && (condOn || !condObjs.includes(o)));
|
|
1307
|
+
};
|
|
1308
|
+
const setCond = (v) => {
|
|
1309
|
+
condOn = v;
|
|
1310
|
+
condObjs.forEach((o) => o.visible = v && edgesOn);
|
|
1311
|
+
};
|
|
1312
|
+
stats.condBatches = condObjs.length;
|
|
1313
|
+
stats.texBatches = texBatches;
|
|
1314
|
+
stats.textures = texCache.size;
|
|
1315
|
+
const texturesReady = Promise.all(pending).then(() => {});
|
|
525
1316
|
const setCull = (studs, under) => {
|
|
526
1317
|
for (const t of toggles)
|
|
527
1318
|
t.mesh.count = (t.flag === "studs" ? studs : under) ? t.visible : t.all;
|
|
@@ -537,6 +1328,11 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
537
1328
|
matF.side = front ? THREE.FrontSide : THREE.DoubleSide;
|
|
538
1329
|
matB.side = front ? THREE.BackSide : THREE.DoubleSide;
|
|
539
1330
|
matF.needsUpdate = matB.needsUpdate = true;
|
|
1331
|
+
for (const [k, m] of texMats) {
|
|
1332
|
+
const role = k.split(":")[1];
|
|
1333
|
+
m.side = role === "D" || !front ? THREE.DoubleSide : role === "B" ? THREE.BackSide : THREE.FrontSide;
|
|
1334
|
+
m.needsUpdate = true;
|
|
1335
|
+
}
|
|
540
1336
|
};
|
|
541
1337
|
const swapStuds = (alt) => {
|
|
542
1338
|
const saved = studMeshes.map((s) => s.im.geometry);
|
|
@@ -547,7 +1343,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
|
|
|
547
1343
|
}
|
|
548
1344
|
return () => studMeshes.forEach((s, i) => s.im.geometry = saved[i]);
|
|
549
1345
|
};
|
|
550
|
-
return { root, stats, setCull, setSide, setEdges, swapStuds, box };
|
|
1346
|
+
return { root, stats, setCull, setSide, setEdges, setCond, swapStuds, box, texturesReady };
|
|
551
1347
|
}
|
|
552
1348
|
// src/scene.ts
|
|
553
1349
|
import * as THREE2 from "three";
|
|
@@ -584,19 +1380,27 @@ function toPlastic(old) {
|
|
|
584
1380
|
if (!m) {
|
|
585
1381
|
const p = new THREE2.MeshPhysicalMaterial({ vertexColors: true, roughness: 0.3, clearcoat: 0.3, clearcoatRoughness: 0.2, specularIntensity: 0.6, side: old.side });
|
|
586
1382
|
p.onBeforeCompile = old.onBeforeCompile;
|
|
587
|
-
p.customProgramCacheKey = () => "
|
|
1383
|
+
p.customProgramCacheKey = () => "Phys" + old.customProgramCacheKey() + old.side;
|
|
1384
|
+
p.userData = old.userData;
|
|
588
1385
|
plasticCache.set(old, m = p);
|
|
589
1386
|
}
|
|
590
1387
|
return m;
|
|
591
1388
|
}
|
|
592
1389
|
function createLdrObject(instances, pack, options = {}) {
|
|
593
|
-
const o = { edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
|
|
1390
|
+
const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
|
|
594
1391
|
const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
|
|
595
|
-
const
|
|
1392
|
+
const useTex = o.textures !== false;
|
|
1393
|
+
const pick = (i) => useTex && i.texPart && pack.header.parts[i.texPart] ? i.texPart : i.part;
|
|
1394
|
+
const all = instances.map((i) => ({ part: pick(i), color: String(i.color), m: i.m, steps: i.steps }));
|
|
596
1395
|
const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
|
|
597
1396
|
const uniq = [...new Set(all.map(key))].sort();
|
|
598
1397
|
const rank = new Map(uniq.map((k, j) => [k, j]));
|
|
599
1398
|
const stepOf = all.map((i) => rank.get(key(i)));
|
|
1399
|
+
const stepViews = new Array(uniq.length).fill(null);
|
|
1400
|
+
instances.forEach((i, k) => {
|
|
1401
|
+
if (i.view)
|
|
1402
|
+
stepViews[stepOf[k]] = i.view;
|
|
1403
|
+
});
|
|
600
1404
|
const missing = [...new Set(all.filter((i) => !pack.header.parts[i.part]).map((i) => i.part))];
|
|
601
1405
|
const object = new THREE2.Group;
|
|
602
1406
|
object.name = "ldr";
|
|
@@ -606,7 +1410,7 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
606
1410
|
object.scale.setScalar(o.scale);
|
|
607
1411
|
const pairs = new Map;
|
|
608
1412
|
let cur = null;
|
|
609
|
-
let { edges, cullStuds: cs, cullUndersides: cu, side } = o;
|
|
1413
|
+
let { edges, conditionalLines: cond, cullStuds: cs, cullUndersides: cu, side } = o;
|
|
610
1414
|
const stats = { instances: all.length, drawn: 0, missing, studsTotal: 0, studsHidden: 0, undersTotal: 0, undersHidden: 0, buildMs: 0 };
|
|
611
1415
|
const disposeCur = () => {
|
|
612
1416
|
if (!cur)
|
|
@@ -620,17 +1424,17 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
620
1424
|
if (x.dispose && x.isInstancedMesh)
|
|
621
1425
|
x.dispose();
|
|
622
1426
|
});
|
|
623
|
-
for (const m of mats)
|
|
1427
|
+
for (const m of [...mats, ...pairs.values()]) {
|
|
1428
|
+
m.userData.ldrMap?.dispose();
|
|
624
1429
|
m.dispose();
|
|
625
|
-
|
|
626
|
-
old.dispose();
|
|
1430
|
+
}
|
|
627
1431
|
cur = null;
|
|
628
1432
|
};
|
|
629
1433
|
const build = (step) => {
|
|
630
1434
|
const t0 = performance.now();
|
|
631
1435
|
disposeCur();
|
|
632
1436
|
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, () => {});
|
|
1437
|
+
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
1438
|
pairs.clear();
|
|
635
1439
|
if (o.material === "plastic")
|
|
636
1440
|
cur.root.traverse((x) => {
|
|
@@ -642,19 +1446,28 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
642
1446
|
});
|
|
643
1447
|
if (!edges)
|
|
644
1448
|
cur.setEdges(false);
|
|
1449
|
+
if (!cond)
|
|
1450
|
+
cur.setCond(false);
|
|
645
1451
|
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 });
|
|
1452
|
+
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 });
|
|
1453
|
+
api.texturesReady = cur.texturesReady;
|
|
647
1454
|
};
|
|
1455
|
+
const api = { texturesReady: Promise.resolve() };
|
|
648
1456
|
build(o.step ?? null);
|
|
649
|
-
return {
|
|
1457
|
+
return Object.assign(api, {
|
|
650
1458
|
object,
|
|
651
1459
|
stats,
|
|
652
1460
|
stepCount: uniq.length,
|
|
1461
|
+
stepViews,
|
|
653
1462
|
setStep: (s) => build(s),
|
|
654
1463
|
setEdges: (v) => {
|
|
655
1464
|
edges = v;
|
|
656
1465
|
cur.setEdges(v);
|
|
657
1466
|
},
|
|
1467
|
+
setConditionalLines: (v) => {
|
|
1468
|
+
cond = v;
|
|
1469
|
+
cur.setCond(v);
|
|
1470
|
+
},
|
|
658
1471
|
setCulling: (a, b) => {
|
|
659
1472
|
cs = a;
|
|
660
1473
|
cu = b;
|
|
@@ -681,7 +1494,7 @@ function createLdrObject(instances, pack, options = {}) {
|
|
|
681
1494
|
return target;
|
|
682
1495
|
},
|
|
683
1496
|
dispose: disposeCur
|
|
684
|
-
};
|
|
1497
|
+
});
|
|
685
1498
|
}
|
|
686
1499
|
// src/pack-io.ts
|
|
687
1500
|
import { decompress } from "fzstd";
|
|
@@ -705,6 +1518,16 @@ class Library {
|
|
|
705
1518
|
unofficialRoot;
|
|
706
1519
|
cache = new Map;
|
|
707
1520
|
meshCache = new Map;
|
|
1521
|
+
embedded = new Map;
|
|
1522
|
+
register(name, doc, file = doc.main) {
|
|
1523
|
+
name = norm(name);
|
|
1524
|
+
const prev = this.embedded.get(name);
|
|
1525
|
+
if (prev && prev.files === doc.files && prev.main === file)
|
|
1526
|
+
return;
|
|
1527
|
+
if (prev || this.lookup(name))
|
|
1528
|
+
this.meshCache.clear();
|
|
1529
|
+
this.embedded.set(name, file === doc.main ? doc : { main: file, files: doc.files, ...doc.data ? { data: doc.data } : {} });
|
|
1530
|
+
}
|
|
708
1531
|
constructor(root, unofficialRoot) {
|
|
709
1532
|
this.root = root;
|
|
710
1533
|
this.unofficialRoot = unofficialRoot;
|
|
@@ -715,6 +1538,9 @@ class Library {
|
|
|
715
1538
|
if (seen.has(name))
|
|
716
1539
|
return null;
|
|
717
1540
|
seen.add(name);
|
|
1541
|
+
const emb = this.embedded.get(name);
|
|
1542
|
+
if (emb)
|
|
1543
|
+
return { kind: "part", doc: emb };
|
|
718
1544
|
const hit = this.lookup(name);
|
|
719
1545
|
if (!hit)
|
|
720
1546
|
return null;
|
|
@@ -766,46 +1592,91 @@ class Library {
|
|
|
766
1592
|
}
|
|
767
1593
|
return out;
|
|
768
1594
|
}
|
|
769
|
-
mesh(name) {
|
|
1595
|
+
mesh(name, opts = {}) {
|
|
770
1596
|
name = norm(name);
|
|
771
|
-
const
|
|
1597
|
+
const ck = opts.texmap ? name + "#texmap" : name;
|
|
1598
|
+
const hit = this.meshCache.get(ck);
|
|
772
1599
|
if (hit)
|
|
773
1600
|
return hit;
|
|
774
1601
|
const pos = [], col = [], edges = [], cond = [];
|
|
1602
|
+
const tx = opts.texmap ? { uv: [], tri: [], names: [] } : undefined;
|
|
775
1603
|
const r = this.where(name);
|
|
776
1604
|
if (r)
|
|
777
|
-
this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond);
|
|
1605
|
+
this.emit(r.doc, r.doc.main, I, 16, false, true, pos, col, edges, cond, tx, null);
|
|
778
1606
|
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
|
-
|
|
1607
|
+
if (tx) {
|
|
1608
|
+
m.uv = new Float32Array(tx.uv);
|
|
1609
|
+
m.texture = new Int32Array(tx.tri);
|
|
1610
|
+
m.textures = tx.names;
|
|
1611
|
+
}
|
|
1612
|
+
this.meshCache.set(ck, m);
|
|
780
1613
|
return m;
|
|
781
1614
|
}
|
|
782
|
-
|
|
1615
|
+
texture(name, doc) {
|
|
1616
|
+
if (doc) {
|
|
1617
|
+
const d = resolveTexture(doc, name);
|
|
1618
|
+
if (d)
|
|
1619
|
+
return d.bytes;
|
|
1620
|
+
}
|
|
1621
|
+
const n = norm(name);
|
|
1622
|
+
const roots = [this.root];
|
|
1623
|
+
const unoff = this.unofficialRoot ?? (existsSync(join(this.root, "unofficial")) ? join(this.root, "unofficial") : undefined);
|
|
1624
|
+
if (unoff)
|
|
1625
|
+
roots.push(unoff);
|
|
1626
|
+
for (const cand of ["textures/" + n, n])
|
|
1627
|
+
for (const root of roots)
|
|
1628
|
+
for (const d of ["parts", "p", "models"]) {
|
|
1629
|
+
const f = join(root, d, cand);
|
|
1630
|
+
if (existsSync(f))
|
|
1631
|
+
return new Uint8Array(readFileSync(f));
|
|
1632
|
+
}
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
emit(doc, fname, m, color, inv, clip, pos, col, edges, cond, tx, texIn = null) {
|
|
783
1636
|
const f = doc.files.get(fname);
|
|
784
1637
|
if (!f)
|
|
785
1638
|
return;
|
|
786
1639
|
let ccw = true, invertNext = false, clipLocal = clip;
|
|
787
1640
|
const flip = inv !== det(m) < 0;
|
|
788
1641
|
const cc = (c) => c === 16 ? color : c;
|
|
789
|
-
|
|
1642
|
+
const texIdx = (t) => {
|
|
1643
|
+
let i = tx.names.indexOf(t.texture);
|
|
1644
|
+
if (i < 0) {
|
|
1645
|
+
i = tx.names.length;
|
|
1646
|
+
tx.names.push(t.texture);
|
|
1647
|
+
}
|
|
1648
|
+
return i;
|
|
1649
|
+
};
|
|
1650
|
+
for (const c0 of f.cmds) {
|
|
1651
|
+
let c = c0;
|
|
790
1652
|
if (c.t === 0) {
|
|
791
|
-
if (c.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (c.bfc
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1653
|
+
if (tx && c.geom)
|
|
1654
|
+
c = c.geom;
|
|
1655
|
+
else {
|
|
1656
|
+
if (c.bfc) {
|
|
1657
|
+
if (c.bfc.includes("CW") && !c.bfc.includes("CCW"))
|
|
1658
|
+
ccw = false;
|
|
1659
|
+
if (c.bfc.includes("CCW"))
|
|
1660
|
+
ccw = true;
|
|
1661
|
+
if (c.bfc.includes("INVERTNEXT"))
|
|
1662
|
+
invertNext = true;
|
|
1663
|
+
if (/\bNOCLIP\b/.test(c.bfc))
|
|
1664
|
+
clipLocal = false;
|
|
1665
|
+
else if (/\bCLIP\b/.test(c.bfc))
|
|
1666
|
+
clipLocal = true;
|
|
1667
|
+
}
|
|
1668
|
+
continue;
|
|
802
1669
|
}
|
|
803
|
-
continue;
|
|
804
1670
|
}
|
|
1671
|
+
if (c.t === 0)
|
|
1672
|
+
continue;
|
|
1673
|
+
if (tx && c.fallback)
|
|
1674
|
+
continue;
|
|
1675
|
+
const tex = tx ? c.tex ? transformTexMap(c.tex, m) : texIn : null;
|
|
805
1676
|
if (c.t === 1) {
|
|
806
1677
|
const sub = doc.files.has(c.file) ? doc : this.where(c.file)?.doc;
|
|
807
1678
|
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);
|
|
1679
|
+
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
1680
|
invertNext = false;
|
|
810
1681
|
continue;
|
|
811
1682
|
}
|
|
@@ -821,9 +1692,18 @@ class Library {
|
|
|
821
1692
|
continue;
|
|
822
1693
|
}
|
|
823
1694
|
const ordered = ccw !== flip ? v : [...v].reverse();
|
|
1695
|
+
const ti = tx && tex ? texIdx(tex) : -1;
|
|
1696
|
+
const code = cc(c.color);
|
|
824
1697
|
const tri = (a, b, d) => {
|
|
825
1698
|
pos.push(...a, ...b, ...d);
|
|
826
|
-
col.push(
|
|
1699
|
+
col.push(code);
|
|
1700
|
+
if (tx) {
|
|
1701
|
+
tx.tri.push(ti);
|
|
1702
|
+
if (tex)
|
|
1703
|
+
tx.uv.push(...texmapTriangleUV(tex, [a, b, d]));
|
|
1704
|
+
else
|
|
1705
|
+
tx.uv.push(NaN, NaN, NaN, NaN, NaN, NaN);
|
|
1706
|
+
}
|
|
827
1707
|
};
|
|
828
1708
|
const emitFace = (w) => {
|
|
829
1709
|
tri(w[0], w[1], w[2]);
|
|
@@ -835,34 +1715,48 @@ class Library {
|
|
|
835
1715
|
emitFace([...ordered].reverse());
|
|
836
1716
|
}
|
|
837
1717
|
}
|
|
838
|
-
flatten(doc) {
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1718
|
+
flatten(doc, opts = {}) {
|
|
1719
|
+
for (const f of doc.files.keys())
|
|
1720
|
+
this.register(f, doc, f);
|
|
1721
|
+
const memo = new Map;
|
|
1722
|
+
const at = (n) => {
|
|
1723
|
+
if (!memo.has(n))
|
|
1724
|
+
memo.set(n, this.where(n));
|
|
1725
|
+
return memo.get(n);
|
|
1726
|
+
};
|
|
1727
|
+
const insts = flatten(doc, { ...opts, resolveModel: (n) => {
|
|
1728
|
+
const r = at(n);
|
|
1729
|
+
return r?.kind === "model" ? r.doc : null;
|
|
1730
|
+
} });
|
|
1731
|
+
const synth = new Map;
|
|
1732
|
+
for (const i of insts) {
|
|
1733
|
+
if (i.texPart && !synth.has(i.texPart)) {
|
|
1734
|
+
synth.set(i.texPart, i.texSource);
|
|
1735
|
+
this.register(i.texPart, parse(i.texSource, i.texPart));
|
|
1736
|
+
}
|
|
1737
|
+
if (i.embedded) {
|
|
1738
|
+
i.missing = false;
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
if (i.source) {
|
|
1742
|
+
if (!synth.has(i.part)) {
|
|
1743
|
+
synth.set(i.part, "");
|
|
1744
|
+
const s = i.source;
|
|
1745
|
+
const lf = s.file ? looseFile(s.file, s.step) : parse(s(), i.part).files.get(i.part);
|
|
1746
|
+
this.register(i.part, { main: i.part, files: new Map([[i.part, lf]]) });
|
|
861
1747
|
}
|
|
1748
|
+
i.missing = false;
|
|
1749
|
+
continue;
|
|
862
1750
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1751
|
+
const r = at(i.part);
|
|
1752
|
+
i.missing = !r;
|
|
1753
|
+
if (r) {
|
|
1754
|
+
i.part = r.doc.main;
|
|
1755
|
+
if (r.aliasOf)
|
|
1756
|
+
i.aliasOf = r.aliasOf;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return insts;
|
|
866
1760
|
}
|
|
867
1761
|
}
|
|
868
1762
|
// src/node/build-pack.ts
|
|
@@ -928,8 +1822,22 @@ class Library2 {
|
|
|
928
1822
|
this.index.set(r.toLowerCase(), path.join(dir, e.name));
|
|
929
1823
|
}
|
|
930
1824
|
}
|
|
1825
|
+
virtual = new Map;
|
|
1826
|
+
conflicts = new Set;
|
|
1827
|
+
addVirtual(name, text) {
|
|
1828
|
+
const n = norm(name), key = "mpd/" + n, prev = this.virtual.get(n);
|
|
1829
|
+
if (prev === undefined) {
|
|
1830
|
+
this.virtual.set(n, text);
|
|
1831
|
+
this.text.set(key, text);
|
|
1832
|
+
this.index.set(key, "(mpd) " + n);
|
|
1833
|
+
} else if (prev !== text)
|
|
1834
|
+
this.conflicts.add(n);
|
|
1835
|
+
return key;
|
|
1836
|
+
}
|
|
931
1837
|
resolve(ref) {
|
|
932
1838
|
const n = norm2(ref);
|
|
1839
|
+
if (this.virtual.size && this.virtual.has(n))
|
|
1840
|
+
return "mpd/" + n;
|
|
933
1841
|
for (const pre of ["parts/", "p/", "models/"])
|
|
934
1842
|
if (this.index.has(pre + n))
|
|
935
1843
|
return pre + n;
|
|
@@ -1006,58 +1914,104 @@ class Library2 {
|
|
|
1006
1914
|
}
|
|
1007
1915
|
function splitModel(text, fallbackName = "main.ldr") {
|
|
1008
1916
|
const files = new Map;
|
|
1917
|
+
const data = new Map;
|
|
1918
|
+
let dataCur = null;
|
|
1009
1919
|
let cur = null, buf = [], main = null;
|
|
1920
|
+
let skip = false, sawBlock = false;
|
|
1921
|
+
const flush = () => {
|
|
1922
|
+
if (cur)
|
|
1923
|
+
files.set(cur, buf.join(`
|
|
1924
|
+
`));
|
|
1925
|
+
cur = null;
|
|
1926
|
+
buf = [];
|
|
1927
|
+
};
|
|
1928
|
+
const firstBlock = () => {
|
|
1929
|
+
if (sawBlock)
|
|
1930
|
+
return;
|
|
1931
|
+
sawBlock = true;
|
|
1932
|
+
files.clear();
|
|
1933
|
+
cur = null;
|
|
1934
|
+
buf = [];
|
|
1935
|
+
main = null;
|
|
1936
|
+
};
|
|
1010
1937
|
for (const line of text.split(/\r?\n/)) {
|
|
1011
1938
|
const m = /^\s*0\s+FILE\s+(.+)$/i.exec(line);
|
|
1012
1939
|
if (m) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1940
|
+
firstBlock();
|
|
1941
|
+
flush();
|
|
1942
|
+
skip = false;
|
|
1943
|
+
dataCur = null;
|
|
1016
1944
|
cur = norm2(m[1]);
|
|
1017
|
-
buf = [];
|
|
1018
1945
|
if (!main)
|
|
1019
1946
|
main = cur;
|
|
1020
1947
|
continue;
|
|
1021
1948
|
}
|
|
1022
|
-
if (/^\s*0\s+NOFILE/i.test(line))
|
|
1949
|
+
if (/^\s*0\s+(NOFILE|!DATA)(\s|$)/i.test(line)) {
|
|
1950
|
+
dataCur = null;
|
|
1951
|
+
if (/!DATA/i.test(line)) {
|
|
1952
|
+
firstBlock();
|
|
1953
|
+
const dn = norm2(line.replace(/^\s*0\s+!DATA\s*/i, ""));
|
|
1954
|
+
dataCur = dn;
|
|
1955
|
+
data.set(dn, "");
|
|
1956
|
+
}
|
|
1957
|
+
flush();
|
|
1958
|
+
skip = true;
|
|
1023
1959
|
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (skip) {
|
|
1962
|
+
const dc = dataCur;
|
|
1963
|
+
if (dc) {
|
|
1964
|
+
const d = /^\s*0\s+!:\s*(.*)$/.exec(line);
|
|
1965
|
+
if (d)
|
|
1966
|
+
data.set(dc, data.get(dc) + d[1].trim());
|
|
1967
|
+
}
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1024
1970
|
if (!cur) {
|
|
1025
1971
|
cur = norm2(fallbackName);
|
|
1026
1972
|
main = cur;
|
|
1027
1973
|
}
|
|
1028
1974
|
buf.push(line);
|
|
1029
1975
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
`));
|
|
1033
|
-
return { files, main };
|
|
1976
|
+
flush();
|
|
1977
|
+
return { files, main, data };
|
|
1034
1978
|
}
|
|
1035
|
-
function
|
|
1979
|
+
function modelDoc(model) {
|
|
1980
|
+
const files = new Map;
|
|
1981
|
+
for (const [n, t] of model.files) {
|
|
1982
|
+
const f = parse(t, n, { geometryText: true }).files.get(norm(n)) ?? { name: norm(n), cmds: [] };
|
|
1983
|
+
files.set(norm(n), f);
|
|
1984
|
+
}
|
|
1985
|
+
return { main: norm(model.main ?? ""), files };
|
|
1986
|
+
}
|
|
1987
|
+
function flatten2(model, lib, onInstance, opts = {}) {
|
|
1036
1988
|
const missing = new Map;
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
const key = lib.resolve(r.name);
|
|
1053
|
-
if (!key) {
|
|
1054
|
-
missing.set(n, (missing.get(n) || 0) + 1);
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
onInstance({ part: key, ref: n, color, m });
|
|
1989
|
+
for (const [n, t] of model.files)
|
|
1990
|
+
lib.addVirtual(n, t);
|
|
1991
|
+
const doc = modelDoc(model);
|
|
1992
|
+
const at = new Map;
|
|
1993
|
+
for (const i of flatten(doc, opts)) {
|
|
1994
|
+
let key;
|
|
1995
|
+
if (i.embedded)
|
|
1996
|
+
key = "mpd/" + i.part;
|
|
1997
|
+
else if (i.source)
|
|
1998
|
+
key = lib.addVirtual(i.part, i.source());
|
|
1999
|
+
else {
|
|
2000
|
+
if (!at.has(i.part))
|
|
2001
|
+
at.set(i.part, lib.resolve(i.part));
|
|
2002
|
+
key = at.get(i.part);
|
|
1058
2003
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
2004
|
+
if (!key) {
|
|
2005
|
+
missing.set(i.part, (missing.get(i.part) || 0) + 1);
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
const r = { part: key, ref: i.part, color: colorToken(i.color), m: i.m, inst: i };
|
|
2009
|
+
if (i.texPart) {
|
|
2010
|
+
r.texRef = i.texPart;
|
|
2011
|
+
r.texPart = lib.addVirtual(i.texPart, i.texSource);
|
|
2012
|
+
}
|
|
2013
|
+
onInstance(r);
|
|
2014
|
+
}
|
|
1061
2015
|
return { missing };
|
|
1062
2016
|
}
|
|
1063
2017
|
function collectDeps(lib, refs) {
|
|
@@ -1131,7 +2085,7 @@ class Compiler {
|
|
|
1131
2085
|
const ck = key + (separateStuds ? "" : "#nostudsep");
|
|
1132
2086
|
if (this.cache.has(ck))
|
|
1133
2087
|
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 };
|
|
2088
|
+
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
2089
|
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
2090
|
const walk = (fk, M, color, invert, target, depth) => {
|
|
1137
2091
|
if (depth > 40)
|
|
@@ -1186,6 +2140,13 @@ class Compiler {
|
|
|
1186
2140
|
}
|
|
1187
2141
|
if (c === "5") {
|
|
1188
2142
|
out.condLines++;
|
|
2143
|
+
const dst = target === "under" ? out.condUnder : out.cond;
|
|
2144
|
+
if (dst) {
|
|
2145
|
+
const v = t.split(/\s+/).slice(2, 14).map(Number);
|
|
2146
|
+
if (v.length === 12 && v.every(Number.isFinite))
|
|
2147
|
+
for (let i = 0;i < 12; i += 3)
|
|
2148
|
+
dst.push(...xf2(M, v.slice(i, i + 3)));
|
|
2149
|
+
}
|
|
1189
2150
|
continue;
|
|
1190
2151
|
}
|
|
1191
2152
|
if (c === "3" || c === "4") {
|
|
@@ -1212,6 +2173,8 @@ class Compiler {
|
|
|
1212
2173
|
out.uncertified = true;
|
|
1213
2174
|
};
|
|
1214
2175
|
walk(key, IDENT, "16", false, "body", 0);
|
|
2176
|
+
if (this.opts.tex && this.hasTex(key))
|
|
2177
|
+
this.texPass(key, out, separateStuds, colorOf);
|
|
1215
2178
|
const [x0, y0, z0, x1, y1, z1] = out.bbox;
|
|
1216
2179
|
let ax0 = Infinity, ax1 = -Infinity, az0 = Infinity, az1 = -Infinity;
|
|
1217
2180
|
for (const b of [out.body, out.under])
|
|
@@ -1228,6 +2191,169 @@ class Compiler {
|
|
|
1228
2191
|
this.cache.set(ck, out);
|
|
1229
2192
|
return out;
|
|
1230
2193
|
}
|
|
2194
|
+
texMemo = new Map;
|
|
2195
|
+
hasTex(key) {
|
|
2196
|
+
const m = this.texMemo.get(key);
|
|
2197
|
+
if (m !== undefined)
|
|
2198
|
+
return m;
|
|
2199
|
+
this.texMemo.set(key, false);
|
|
2200
|
+
const txt = this.lib.read(key);
|
|
2201
|
+
let r = /^\s*0\s+!TEXMAP/im.test(txt);
|
|
2202
|
+
if (!r)
|
|
2203
|
+
for (const l of txt.split(/\r?\n/)) {
|
|
2204
|
+
const t = l.trim();
|
|
2205
|
+
const p = t.startsWith("0 !:") ? parseType1(t.slice(4)) : t[0] === "1" ? parseType1(t) : null;
|
|
2206
|
+
if (p) {
|
|
2207
|
+
const k = this.lib.resolve(p.name);
|
|
2208
|
+
if (k && this.hasTex(k)) {
|
|
2209
|
+
r = true;
|
|
2210
|
+
break;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
this.texMemo.set(key, r);
|
|
2215
|
+
return r;
|
|
2216
|
+
}
|
|
2217
|
+
texture(name) {
|
|
2218
|
+
const n = norm2(name);
|
|
2219
|
+
for (const pre of ["parts/textures/", "p/textures/", "models/textures/", "parts/", "p/", "models/"]) {
|
|
2220
|
+
const k = pre + n;
|
|
2221
|
+
if (this.lib.index.has(k))
|
|
2222
|
+
return new Uint8Array(fs2.readFileSync(this.lib.index.get(k)));
|
|
2223
|
+
}
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
texPass(key, out, separateStuds, colorOf) {
|
|
2227
|
+
const tbody = { pos: [], col: [] }, texb = new Map;
|
|
2228
|
+
const walk = (fk, M, color, invert, target, depth, texIn) => {
|
|
2229
|
+
if (depth > 40)
|
|
2230
|
+
return;
|
|
2231
|
+
let ccw = true, invertNext = false;
|
|
2232
|
+
let stack = [];
|
|
2233
|
+
let next = null;
|
|
2234
|
+
for (const raw of this.lib.read(fk).split(/\r?\n/)) {
|
|
2235
|
+
let t = raw.trim();
|
|
2236
|
+
if (!t)
|
|
2237
|
+
continue;
|
|
2238
|
+
let colon = false;
|
|
2239
|
+
if (t[0] === "0") {
|
|
2240
|
+
const m = /^0\s+(\S+)\s*(.*)$/.exec(t);
|
|
2241
|
+
const kw = (m?.[1] ?? "").toUpperCase(), rest = m?.[2] ?? "";
|
|
2242
|
+
if (kw === "!TEXMAP") {
|
|
2243
|
+
const op = (rest.split(/\s+/)[0] || "").toUpperCase();
|
|
2244
|
+
next = null;
|
|
2245
|
+
if (op === "START" || op === "NEXT") {
|
|
2246
|
+
const map = parseTexMap(rest.replace(/^\S+/, ""), op === "NEXT");
|
|
2247
|
+
if (map) {
|
|
2248
|
+
const pm = transformTexMap(map, M);
|
|
2249
|
+
if (op === "START")
|
|
2250
|
+
stack.push({ map: pm, fallback: false });
|
|
2251
|
+
else
|
|
2252
|
+
next = pm;
|
|
2253
|
+
}
|
|
2254
|
+
} else if (op === "FALLBACK") {
|
|
2255
|
+
const top = stack[stack.length - 1];
|
|
2256
|
+
if (top)
|
|
2257
|
+
top.fallback = true;
|
|
2258
|
+
} else if (op === "END")
|
|
2259
|
+
stack.pop();
|
|
2260
|
+
continue;
|
|
2261
|
+
}
|
|
2262
|
+
if (kw === "!:") {
|
|
2263
|
+
next = null;
|
|
2264
|
+
t = rest.trim();
|
|
2265
|
+
colon = true;
|
|
2266
|
+
if (!t || t[0] === "0")
|
|
2267
|
+
continue;
|
|
2268
|
+
} else {
|
|
2269
|
+
if (kw === "BFC") {
|
|
2270
|
+
if (/\bCW\b/.test(t))
|
|
2271
|
+
ccw = false;
|
|
2272
|
+
if (/\bCCW\b/.test(t))
|
|
2273
|
+
ccw = true;
|
|
2274
|
+
if (/INVERTNEXT/.test(t))
|
|
2275
|
+
invertNext = true;
|
|
2276
|
+
}
|
|
2277
|
+
if (kw === "STEP")
|
|
2278
|
+
stack = [];
|
|
2279
|
+
next = null;
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
let tex = texIn;
|
|
2284
|
+
if (next) {
|
|
2285
|
+
tex = next;
|
|
2286
|
+
next = null;
|
|
2287
|
+
} else {
|
|
2288
|
+
const top = stack[stack.length - 1];
|
|
2289
|
+
if (top) {
|
|
2290
|
+
if (top.fallback) {
|
|
2291
|
+
if (!colon)
|
|
2292
|
+
continue;
|
|
2293
|
+
} else
|
|
2294
|
+
tex = top.map;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
const c = t[0];
|
|
2298
|
+
if (c === "1") {
|
|
2299
|
+
const r = parseType1(t);
|
|
2300
|
+
if (!r)
|
|
2301
|
+
continue;
|
|
2302
|
+
const base = r.name.trim().replace(/\\/g, "/").split("/").pop().toLowerCase();
|
|
2303
|
+
const isTop = TOP_STUD_RE.test(base), isUnder = UNDER_STUD_RE.test(base);
|
|
2304
|
+
const ck2 = this.resolve(r.name, isTop);
|
|
2305
|
+
if (!ck2) {
|
|
2306
|
+
invertNext = false;
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
const inv = invert !== invertNext !== det3(r.m) < 0;
|
|
2310
|
+
invertNext = false;
|
|
2311
|
+
if (isTop && separateStuds)
|
|
2312
|
+
continue;
|
|
2313
|
+
walk(ck2, compose(M, r.m), r.color === "16" ? color : r.color, inv, isUnder ? "under" : target, depth + 1, tex);
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
if (c !== "3" && c !== "4")
|
|
2317
|
+
continue;
|
|
2318
|
+
const tok = t.split(/\s+/);
|
|
2319
|
+
const code = tok[1] === "16" ? color : tok[1];
|
|
2320
|
+
const n = c === "3" ? 3 : 4;
|
|
2321
|
+
const v = tok.slice(2, 2 + n * 3).map(Number);
|
|
2322
|
+
const pts = [];
|
|
2323
|
+
for (let i = 0;i < n; i++)
|
|
2324
|
+
pts.push(xf2(M, v.slice(i * 3, i * 3 + 3)));
|
|
2325
|
+
let order = n === 3 ? [0, 1, 2] : [0, 1, 2, 0, 2, 3];
|
|
2326
|
+
if (!ccw !== invert)
|
|
2327
|
+
order = n === 3 ? [0, 2, 1] : [0, 2, 1, 0, 3, 2];
|
|
2328
|
+
const rgba = colorOf(code);
|
|
2329
|
+
if (tex) {
|
|
2330
|
+
let b = texb.get(tex.texture);
|
|
2331
|
+
if (!b)
|
|
2332
|
+
texb.set(tex.texture, b = { pos: [], col: [], uv: [] });
|
|
2333
|
+
if (Math.abs(tex.a ?? 0) >= 360)
|
|
2334
|
+
b.wrapU = true;
|
|
2335
|
+
for (let k = 0;k < order.length; k += 3) {
|
|
2336
|
+
const tri = [pts[order[k]], pts[order[k + 1]], pts[order[k + 2]]];
|
|
2337
|
+
const w = texmapTriangleUV(tex, tri);
|
|
2338
|
+
for (let j = 0;j < 3; j++) {
|
|
2339
|
+
b.pos.push(...tri[j]);
|
|
2340
|
+
b.col.push(...rgba);
|
|
2341
|
+
b.uv.push(w[j * 2], w[j * 2 + 1]);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
} else if (target === "body")
|
|
2345
|
+
for (const i of order) {
|
|
2346
|
+
tbody.pos.push(...pts[i]);
|
|
2347
|
+
tbody.col.push(...rgba);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
walk(key, IDENT, "16", false, "body", 0, null);
|
|
2352
|
+
if (texb.size) {
|
|
2353
|
+
out.tbody = tbody;
|
|
2354
|
+
out.texb = texb;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
1231
2357
|
}
|
|
1232
2358
|
function enclosureMask(c) {
|
|
1233
2359
|
const [x0, , z0, x1, y1, z1] = c.bbox;
|
|
@@ -1310,10 +2436,10 @@ function growBox(b, M, cb) {
|
|
|
1310
2436
|
for (const z of [cb[2], cb[5]])
|
|
1311
2437
|
grow(b, xf2(M, [x, y, z]));
|
|
1312
2438
|
}
|
|
1313
|
-
function writePack(file, parts, studKinds, weld, extra) {
|
|
1314
|
-
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra));
|
|
2439
|
+
function writePack(file, parts, studKinds, weld, extra, options) {
|
|
2440
|
+
fs2.writeFileSync(file, encodePack(parts, studKinds, weld, extra, options));
|
|
1315
2441
|
}
|
|
1316
|
-
function encodePack(parts, studKinds, weld, extra) {
|
|
2442
|
+
function encodePack(parts, studKinds, weld, extra, options = {}) {
|
|
1317
2443
|
const chunks = [];
|
|
1318
2444
|
let off = 0;
|
|
1319
2445
|
const put = (a) => {
|
|
@@ -1322,13 +2448,15 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
1322
2448
|
chunks.push(new Uint8Array(pad));
|
|
1323
2449
|
off += pad;
|
|
1324
2450
|
}
|
|
1325
|
-
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : "u32"];
|
|
2451
|
+
const r = [off, a.length, a instanceof Float32Array ? "f32" : a instanceof Uint16Array ? "u16" : a instanceof Uint8Array ? "u8" : "u32"];
|
|
1326
2452
|
chunks.push(a);
|
|
1327
2453
|
off += a.byteLength;
|
|
1328
2454
|
return r;
|
|
1329
2455
|
};
|
|
2456
|
+
let lastPos = [];
|
|
1330
2457
|
const bucket = (b) => {
|
|
1331
2458
|
const n = b.pos.length / 3;
|
|
2459
|
+
lastPos = [];
|
|
1332
2460
|
if (!n)
|
|
1333
2461
|
return null;
|
|
1334
2462
|
const nrm = new Float32Array(n * 3);
|
|
@@ -1347,8 +2475,10 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
1347
2475
|
nrm[(i + k) * 3 + 2] = nz;
|
|
1348
2476
|
}
|
|
1349
2477
|
}
|
|
1350
|
-
if (!weld)
|
|
2478
|
+
if (!weld) {
|
|
2479
|
+
lastPos = b.pos;
|
|
1351
2480
|
return { pos: put(new Float32Array(b.pos)), nrm: put(nrm), col: put(new Float32Array(b.col)), tris: n / 3 };
|
|
2481
|
+
}
|
|
1352
2482
|
const map = new Map;
|
|
1353
2483
|
const P = [], N = [], C = [], I = [];
|
|
1354
2484
|
for (let i = 0;i < n; i++) {
|
|
@@ -1364,10 +2494,111 @@ function encodePack(parts, studKinds, weld, extra) {
|
|
|
1364
2494
|
I.push(j);
|
|
1365
2495
|
}
|
|
1366
2496
|
const vcount = P.length / 3;
|
|
2497
|
+
lastPos = P;
|
|
1367
2498
|
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
2499
|
};
|
|
1369
|
-
|
|
1370
|
-
const
|
|
2500
|
+
let anyCond = false;
|
|
2501
|
+
const condl = (a, pools) => {
|
|
2502
|
+
if (!a || !a.length)
|
|
2503
|
+
return;
|
|
2504
|
+
anyCond = true;
|
|
2505
|
+
const key = (p, i) => Math.round(p[i] * 1000) + "," + Math.round(p[i + 1] * 1000) + "," + Math.round(p[i + 2] * 1000);
|
|
2506
|
+
const need = new Map;
|
|
2507
|
+
for (let i = 0;i < a.length; i += 3)
|
|
2508
|
+
need.set(key(a, i), -1);
|
|
2509
|
+
let base = 0;
|
|
2510
|
+
for (const p of pools) {
|
|
2511
|
+
for (let i = 0;i < p.length; i += 3) {
|
|
2512
|
+
const k = key(p, i);
|
|
2513
|
+
if (need.get(k) === -1)
|
|
2514
|
+
need.set(k, base + i / 3);
|
|
2515
|
+
}
|
|
2516
|
+
base += p.length / 3;
|
|
2517
|
+
}
|
|
2518
|
+
const X = [], I = new Array(a.length / 3);
|
|
2519
|
+
for (let i = 0;i < a.length; i += 3) {
|
|
2520
|
+
const k = key(a, i);
|
|
2521
|
+
let j = need.get(k);
|
|
2522
|
+
if (j === -1) {
|
|
2523
|
+
j = base + X.length / 3;
|
|
2524
|
+
need.set(k, j);
|
|
2525
|
+
X.push(a[i], a[i + 1], a[i + 2]);
|
|
2526
|
+
}
|
|
2527
|
+
I[i / 3] = j;
|
|
2528
|
+
}
|
|
2529
|
+
let prev = 0, max = 0;
|
|
2530
|
+
for (let i = 0;i < I.length; i++) {
|
|
2531
|
+
const d = I[i] - prev;
|
|
2532
|
+
prev = I[i];
|
|
2533
|
+
const z = d >= 0 ? d * 2 : -d * 2 - 1;
|
|
2534
|
+
I[i] = z;
|
|
2535
|
+
if (z > max)
|
|
2536
|
+
max = z;
|
|
2537
|
+
}
|
|
2538
|
+
return { idx: put(max < 65536 ? new Uint16Array(I) : new Uint32Array(I)), ...X.length ? { x: put(new Float32Array(X)) } : {} };
|
|
2539
|
+
};
|
|
2540
|
+
const texNames = [];
|
|
2541
|
+
const texBucket = (b) => {
|
|
2542
|
+
const n = b.pos.length / 3;
|
|
2543
|
+
if (!n)
|
|
2544
|
+
return null;
|
|
2545
|
+
const nrm = new Float32Array(n * 3);
|
|
2546
|
+
for (let i = 0;i < n; i += 3) {
|
|
2547
|
+
const p = b.pos;
|
|
2548
|
+
const a = i * 3;
|
|
2549
|
+
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];
|
|
2550
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
2551
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
2552
|
+
nx /= l;
|
|
2553
|
+
ny /= l;
|
|
2554
|
+
nz /= l;
|
|
2555
|
+
for (let k = 0;k < 3; k++) {
|
|
2556
|
+
nrm[(i + k) * 3] = nx;
|
|
2557
|
+
nrm[(i + k) * 3 + 1] = ny;
|
|
2558
|
+
nrm[(i + k) * 3 + 2] = nz;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
const map = new Map;
|
|
2562
|
+
const P = [], N = [], C = [], U = [], I = [];
|
|
2563
|
+
for (let i = 0;i < n; i++) {
|
|
2564
|
+
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);
|
|
2565
|
+
let j = map.get(k);
|
|
2566
|
+
if (j === undefined) {
|
|
2567
|
+
j = P.length / 3;
|
|
2568
|
+
map.set(k, j);
|
|
2569
|
+
P.push(b.pos[i * 3], b.pos[i * 3 + 1], b.pos[i * 3 + 2]);
|
|
2570
|
+
N.push(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]);
|
|
2571
|
+
C.push(...b.col.slice(i * 4, i * 4 + 4));
|
|
2572
|
+
U.push(b.uv[i * 2], b.uv[i * 2 + 1]);
|
|
2573
|
+
}
|
|
2574
|
+
I.push(j);
|
|
2575
|
+
}
|
|
2576
|
+
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 };
|
|
2577
|
+
};
|
|
2578
|
+
const encTex = (c) => {
|
|
2579
|
+
if (!c.texb)
|
|
2580
|
+
return {};
|
|
2581
|
+
const tex = [...c.texb].map(([name, b]) => {
|
|
2582
|
+
let t = texNames.indexOf(name);
|
|
2583
|
+
if (t < 0)
|
|
2584
|
+
t = texNames.push(name) - 1;
|
|
2585
|
+
return { t, ...b.wrapU ? { wrapU: true } : {}, ...texBucket(b) };
|
|
2586
|
+
});
|
|
2587
|
+
return { tbody: bucket(c.tbody), tex };
|
|
2588
|
+
};
|
|
2589
|
+
const enc = (c) => {
|
|
2590
|
+
const body = bucket(c.body), bodyP = lastPos, under = bucket(c.under), underP = lastPos;
|
|
2591
|
+
const edges = c.edges.length ? put(new Float32Array(c.edges)) : null;
|
|
2592
|
+
const condlB = condl(c.cond, [bodyP, underP, c.edges]), condlU = condl(c.condUnder, [bodyP, underP, c.edges]);
|
|
2593
|
+
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) };
|
|
2594
|
+
};
|
|
2595
|
+
const encParts = Object.fromEntries([...parts].map(([k, c]) => [k, enc(c)])), encKinds = Object.fromEntries([...studKinds].map(([k, c]) => [k, enc(c)]));
|
|
2596
|
+
const textures = texNames.map((name) => {
|
|
2597
|
+
const b = options.texture?.(name);
|
|
2598
|
+
return b ? { name, mime: "image/png", data: put(b) } : { name, missing: true };
|
|
2599
|
+
});
|
|
2600
|
+
const features = [...anyCond ? ["cond"] : [], ...texNames.length ? ["tex"] : []];
|
|
2601
|
+
const header = { version: features.length ? 2 : 1, ...features.length ? { features } : {}, weld, parts: encParts, studKinds: encKinds, ...texNames.length ? { textures } : {}, ...extra };
|
|
1371
2602
|
const hj = new TextEncoder().encode(JSON.stringify(header));
|
|
1372
2603
|
const hpad = (4 - (4 + hj.length) % 4) % 4;
|
|
1373
2604
|
const head = new Uint8Array(4 + hj.length + hpad);
|
|
@@ -1382,11 +2613,22 @@ function buildPack(o) {
|
|
|
1382
2613
|
const lib = new Library2(o.ldrawRoot);
|
|
1383
2614
|
const refs = new Map;
|
|
1384
2615
|
const missing = new Set;
|
|
2616
|
+
const data = new Map;
|
|
1385
2617
|
for (const m of o.models ?? []) {
|
|
1386
2618
|
const { name, text } = typeof m === "string" ? { name: path3.basename(m), text: fs3.readFileSync(m, "latin1") } : m;
|
|
1387
|
-
const
|
|
1388
|
-
for (const k of
|
|
1389
|
-
|
|
2619
|
+
const sm = splitModel(text, name);
|
|
2620
|
+
for (const [k, v] of sm.data)
|
|
2621
|
+
if (!data.has(k))
|
|
2622
|
+
data.set(k, v);
|
|
2623
|
+
for (const textures of o.textures ? [false, true] : [false]) {
|
|
2624
|
+
const r = flatten2(sm, lib, (i) => {
|
|
2625
|
+
refs.set(i.ref, i.part);
|
|
2626
|
+
if (i.texRef)
|
|
2627
|
+
refs.set(i.texRef, i.texPart);
|
|
2628
|
+
}, { textures, looseGeometry: o.looseGeometry });
|
|
2629
|
+
for (const k of r.missing.keys())
|
|
2630
|
+
missing.add(k);
|
|
2631
|
+
}
|
|
1390
2632
|
}
|
|
1391
2633
|
for (const p of o.parts ?? []) {
|
|
1392
2634
|
const k = lib.resolve(p);
|
|
@@ -1400,7 +2642,7 @@ function buildPack(o) {
|
|
|
1400
2642
|
if (lib.isPart(k) && /\.dat$/.test(k))
|
|
1401
2643
|
refs.set(k.slice(6), k);
|
|
1402
2644
|
}
|
|
1403
|
-
const comp = new Compiler(lib, { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true });
|
|
2645
|
+
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
2646
|
const parts = new Map, kinds = new Map;
|
|
1405
2647
|
let n = 0;
|
|
1406
2648
|
for (const [ref, key] of refs) {
|
|
@@ -1421,7 +2663,12 @@ function buildPack(o) {
|
|
|
1421
2663
|
if (fs3.existsSync(cfg))
|
|
1422
2664
|
extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
|
|
1423
2665
|
}
|
|
1424
|
-
|
|
2666
|
+
const texture = (n) => {
|
|
2667
|
+
const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
|
|
2668
|
+
return b ? new Uint8Array(Buffer.from(b, "base64")) : comp.texture(n);
|
|
2669
|
+
};
|
|
2670
|
+
const warnings = [...lib.conflicts].map((n) => `${n}: defined differently by two models; the first definition is used`);
|
|
2671
|
+
return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture }), parts: parts.size, studKinds: kinds.size, missing: [...missing], ...warnings.length ? { warnings } : {} };
|
|
1425
2672
|
}
|
|
1426
2673
|
export {
|
|
1427
2674
|
Compiler,
|
|
@@ -1431,17 +2678,29 @@ export {
|
|
|
1431
2678
|
buildLdrScene,
|
|
1432
2679
|
buildPack,
|
|
1433
2680
|
collectDeps,
|
|
2681
|
+
colorToken,
|
|
1434
2682
|
computeCulling,
|
|
2683
|
+
conditionalLineVisible,
|
|
2684
|
+
conditionalLines,
|
|
2685
|
+
createInstancedConditionalEdges,
|
|
1435
2686
|
createInstancedEdges,
|
|
1436
2687
|
createLdrObject,
|
|
1437
2688
|
decodePack,
|
|
1438
2689
|
decompressPack,
|
|
1439
2690
|
encodePack,
|
|
2691
|
+
fileKind,
|
|
2692
|
+
fileStepViews,
|
|
1440
2693
|
flatten,
|
|
1441
2694
|
flatten2 as flattenWithLibrary,
|
|
2695
|
+
hash32,
|
|
2696
|
+
invert,
|
|
1442
2697
|
isDirectColor,
|
|
1443
2698
|
loadColors,
|
|
1444
2699
|
loadPack,
|
|
2700
|
+
looseFile,
|
|
2701
|
+
loosePartName,
|
|
2702
|
+
looseSource,
|
|
2703
|
+
looseSteps,
|
|
1445
2704
|
makeMaterial,
|
|
1446
2705
|
mul,
|
|
1447
2706
|
norm,
|
|
@@ -1449,8 +2708,16 @@ export {
|
|
|
1449
2708
|
parseColor,
|
|
1450
2709
|
parseLDConfig,
|
|
1451
2710
|
parseModel,
|
|
2711
|
+
parseTexMap,
|
|
2712
|
+
resolveTexture,
|
|
2713
|
+
rotStepMatrix,
|
|
1452
2714
|
sequenceSteps,
|
|
1453
2715
|
smoothNormals,
|
|
1454
2716
|
splitModel,
|
|
2717
|
+
stepViewMatrix,
|
|
2718
|
+
texWrapper,
|
|
2719
|
+
texmapTriangleUV,
|
|
2720
|
+
texmapUV,
|
|
2721
|
+
transformTexMap,
|
|
1455
2722
|
writePack
|
|
1456
2723
|
};
|