dwf-viewer 0.5.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/CHANGELOG.md +12 -0
- package/LICENSE +235 -0
- package/NOTICE +10 -0
- package/PRODUCTION_3D_NOTES.md +48 -0
- package/README.md +203 -0
- package/dist/format/document.d.ts +186 -0
- package/dist/format/document.js +9 -0
- package/dist/format/dwf.d.ts +4 -0
- package/dist/format/dwf.js +372 -0
- package/dist/format/dwfx.d.ts +6 -0
- package/dist/format/dwfx.js +425 -0
- package/dist/format/emodelMetadata.d.ts +10 -0
- package/dist/format/emodelMetadata.js +368 -0
- package/dist/format/inflate.d.ts +4 -0
- package/dist/format/inflate.js +28 -0
- package/dist/format/opc.d.ts +28 -0
- package/dist/format/opc.js +85 -0
- package/dist/format/open.d.ts +3 -0
- package/dist/format/open.js +69 -0
- package/dist/format/types.d.ts +61 -0
- package/dist/format/types.js +6 -0
- package/dist/format/util.d.ts +18 -0
- package/dist/format/util.js +324 -0
- package/dist/format/w2dBinary.d.ts +19 -0
- package/dist/format/w2dBinary.js +629 -0
- package/dist/format/w2dText.d.ts +13 -0
- package/dist/format/w2dText.js +166 -0
- package/dist/format/w3d.d.ts +8 -0
- package/dist/format/w3d.js +826 -0
- package/dist/format/zip.d.ts +30 -0
- package/dist/format/zip.js +141 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/render/PageRenderer.d.ts +27 -0
- package/dist/render/PageRenderer.js +92 -0
- package/dist/render/ThreeJsSceneAdapter.d.ts +29 -0
- package/dist/render/ThreeJsSceneAdapter.js +52 -0
- package/dist/render/ThreeW3dRenderer.d.ts +24 -0
- package/dist/render/ThreeW3dRenderer.js +372 -0
- package/dist/render/W2dRenderer.d.ts +24 -0
- package/dist/render/W2dRenderer.js +198 -0
- package/dist/render/WebGlW2dBackend.d.ts +38 -0
- package/dist/render/WebGlW2dBackend.js +400 -0
- package/dist/render/XpsRenderer.d.ts +20 -0
- package/dist/render/XpsRenderer.js +310 -0
- package/dist/render/style.d.ts +16 -0
- package/dist/render/style.js +115 -0
- package/dist/render/viewport.d.ts +16 -0
- package/dist/render/viewport.js +27 -0
- package/dist/render/xpsPath.d.ts +41 -0
- package/dist/render/xpsPath.js +335 -0
- package/dist/viewer/DwfViewer.d.ts +69 -0
- package/dist/viewer/DwfViewer.js +386 -0
- package/dist/wasm/WasmRasterBackend.d.ts +21 -0
- package/dist/wasm/WasmRasterBackend.js +84 -0
- package/package.json +91 -0
- package/public/dwfv-render.wasm +0 -0
- package/styles/dwf-viewer.css +51 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import { BrowserInflateProvider } from './inflate.js';
|
|
2
|
+
import { diag } from './types.js';
|
|
3
|
+
const TKSH_COMPRESSED_POINTS = 0x01;
|
|
4
|
+
const TKSH_TRISTRIPS = 0x04;
|
|
5
|
+
const TKSH_FIRSTPASS = 0x10;
|
|
6
|
+
const TKSH_BOUNDING_ONLY = 0x20;
|
|
7
|
+
const TKSH_CONNECTIVITY_COMPRESSION = 0x40;
|
|
8
|
+
const TKSH_EXPANDED = 0x80;
|
|
9
|
+
const TKSH2_COLLECTION = 0x0001;
|
|
10
|
+
const TKSH2_NULL = 0x0002;
|
|
11
|
+
const TKSH2_HAS_NEGATIVE_FACES = 0x0004;
|
|
12
|
+
const TKSH2_GLOBAL_QUANTIZATION = 0x0008;
|
|
13
|
+
const CS_TRIVIAL = 1;
|
|
14
|
+
const CS_NONE = 4;
|
|
15
|
+
const CS_EDGEBREAKER = 5;
|
|
16
|
+
const MTABLE_HAS_LENGTHS = 0x01;
|
|
17
|
+
const MTABLE_HAS_M2STACKOFFSETS = 0x02;
|
|
18
|
+
const MTABLE_HAS_M2GATEOFFSETS = 0x04;
|
|
19
|
+
const MTABLE_HAS_DUMMIES = 0x08;
|
|
20
|
+
const MTABLE_HAS_PATCHES = 0x10;
|
|
21
|
+
const MTABLE_HAS_BOUNDING = 0x20;
|
|
22
|
+
const MTABLE_HAS_QUANTIZATION = 0x40;
|
|
23
|
+
const MTABLE_HAS_QUANTIZATION_NORMALS = 0x80;
|
|
24
|
+
const CASE_C = 0;
|
|
25
|
+
const CASE_L = 1;
|
|
26
|
+
const CASE_E = 2;
|
|
27
|
+
const CASE_R = 3;
|
|
28
|
+
const CASE_S = 4;
|
|
29
|
+
const CASE_M = 5;
|
|
30
|
+
const CASE_M2 = 6;
|
|
31
|
+
const GARBAGE_EDGE = -2139062144;
|
|
32
|
+
const DUMMY_VERTEX = -2147483645;
|
|
33
|
+
const POINTMAP_DUMMY = -2139062145;
|
|
34
|
+
const POINTMAP_ALIAS = -2139062146;
|
|
35
|
+
export async function parseW3dModel(bytes, options = {}) {
|
|
36
|
+
const diagnostics = [];
|
|
37
|
+
const stream = await inflateW3dStream(bytes, diagnostics, options.sourcePath);
|
|
38
|
+
const meshes = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const maxMeshes = options.maxMeshes ?? 10000;
|
|
41
|
+
const maxTriangles = options.maxTriangles ?? 10000000;
|
|
42
|
+
let unsupportedEdgebreaker = 0;
|
|
43
|
+
let falseShellCandidates = 0;
|
|
44
|
+
let triangles = 0;
|
|
45
|
+
for (let pos = 0; pos < stream.length && meshes.length < maxMeshes && triangles < maxTriangles; pos++) {
|
|
46
|
+
if (stream[pos] !== 0x53)
|
|
47
|
+
continue; // TKE_Shell ('S')
|
|
48
|
+
try {
|
|
49
|
+
const parsed = parseShellCandidate(stream, pos, meshes.length + 1);
|
|
50
|
+
if (!parsed)
|
|
51
|
+
continue;
|
|
52
|
+
const key = `${parsed.sourceStart}:${parsed.sourceEnd}`;
|
|
53
|
+
if (seen.has(key))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
meshes.push(parsed);
|
|
57
|
+
triangles += parsed.triangleCount;
|
|
58
|
+
pos = Math.max(pos, parsed.sourceEnd - 1);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const msg = String(err);
|
|
62
|
+
if (/edgebreaker/i.test(msg))
|
|
63
|
+
unsupportedEdgebreaker++;
|
|
64
|
+
else
|
|
65
|
+
falseShellCandidates++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (unsupportedEdgebreaker > 0) {
|
|
69
|
+
diagnostics.push(diag('warning', 'W3D_EDGEBREAKER_PARTIAL', `${unsupportedEdgebreaker} connectivity-compressed shell candidate(s) were skipped.`, options.sourcePath));
|
|
70
|
+
}
|
|
71
|
+
void falseShellCandidates; // filtered byte-pattern candidates are expected in mixed HSF streams; keep diagnostics noise-free.
|
|
72
|
+
if (meshes.length === 0) {
|
|
73
|
+
diagnostics.push(diag('error', 'W3D_NO_MESHES', 'No renderable W3D shell meshes were decoded.', options.sourcePath));
|
|
74
|
+
}
|
|
75
|
+
const bounds = computeModelBounds(meshes);
|
|
76
|
+
return {
|
|
77
|
+
kind: 'w3d-model',
|
|
78
|
+
title: options.title ?? '3D Model',
|
|
79
|
+
meshes,
|
|
80
|
+
bounds,
|
|
81
|
+
diagnostics,
|
|
82
|
+
sourcePath: options.sourcePath,
|
|
83
|
+
stats: {
|
|
84
|
+
meshCount: meshes.length,
|
|
85
|
+
vertexCount: meshes.reduce((s, m) => s + m.vertexCount, 0),
|
|
86
|
+
triangleCount: meshes.reduce((s, m) => s + m.triangleCount, 0),
|
|
87
|
+
decodedBytes: stream.byteLength,
|
|
88
|
+
edgeCount: meshes.reduce((s, m) => s + Math.floor((m.edgeIndices?.length ?? 0) / 2), 0),
|
|
89
|
+
materialCount: 0,
|
|
90
|
+
textureCount: 0,
|
|
91
|
+
pmiCount: 0,
|
|
92
|
+
nodeCount: 0
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function inflateW3dStream(bytes, diagnostics, sourcePath) {
|
|
97
|
+
// HSF/W3D files often contain an uncompressed banner followed by
|
|
98
|
+
// TKE_Start_Compression ('Z') and a zlib-wrapped deflate stream.
|
|
99
|
+
//
|
|
100
|
+
// Important browser detail: Chromium's DecompressionStream('deflate') can be
|
|
101
|
+
// stricter than Node when the zlib stream is followed by HSF padding bytes.
|
|
102
|
+
// The Robot Arm eModel file has one trailing NUL byte after the zlib stream.
|
|
103
|
+
// Node accepts it; browsers can reject it, which was the root cause of the
|
|
104
|
+
// "W3D exists but no renderable shell geometry was decoded" unsupported page.
|
|
105
|
+
//
|
|
106
|
+
// To be deterministic in production, try a browser-safe raw-deflate payload
|
|
107
|
+
// first: strip the 2-byte zlib header, trim trailing NUL padding, and remove
|
|
108
|
+
// the 4-byte Adler32 trailer. Then fall back to the exact zlib candidate and
|
|
109
|
+
// finally to the historical broad candidate.
|
|
110
|
+
const inflater = new BrowserInflateProvider();
|
|
111
|
+
for (let p = 0; p + 6 < bytes.length; p++) {
|
|
112
|
+
if (bytes[p] !== 0x78)
|
|
113
|
+
continue;
|
|
114
|
+
const b = bytes[p + 1];
|
|
115
|
+
if (b !== 0x01 && b !== 0x5e && b !== 0x9c && b !== 0xda)
|
|
116
|
+
continue;
|
|
117
|
+
const zlibCandidate = bytes.subarray(p);
|
|
118
|
+
const trimmed = trimTrailingNulPadding(zlibCandidate);
|
|
119
|
+
const attempts = [];
|
|
120
|
+
if (trimmed.length > 6)
|
|
121
|
+
attempts.push({ label: 'raw-deflate-body', data: trimmed.subarray(2, trimmed.length - 4) });
|
|
122
|
+
if (zlibCandidate.length > 2)
|
|
123
|
+
attempts.push({ label: 'raw-deflate-body-with-trailer', data: zlibCandidate.subarray(2) });
|
|
124
|
+
attempts.push({ label: 'zlib-trimmed', data: trimmed });
|
|
125
|
+
if (trimmed.length !== zlibCandidate.length)
|
|
126
|
+
attempts.push({ label: 'zlib-with-padding', data: zlibCandidate });
|
|
127
|
+
for (const attempt of attempts) {
|
|
128
|
+
try {
|
|
129
|
+
const inflated = await inflater.inflateRaw(attempt.data);
|
|
130
|
+
void sourcePath;
|
|
131
|
+
return inflated;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Continue with the next interpretation. The same byte sequence can
|
|
135
|
+
// also appear in comments or binary attribute payloads.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
void sourcePath;
|
|
140
|
+
return bytes;
|
|
141
|
+
}
|
|
142
|
+
function trimTrailingNulPadding(data) {
|
|
143
|
+
let end = data.length;
|
|
144
|
+
while (end > 0 && data[end - 1] === 0)
|
|
145
|
+
end--;
|
|
146
|
+
return end === data.length ? data : data.subarray(0, end);
|
|
147
|
+
}
|
|
148
|
+
function parseShellCandidate(bytes, start, ordinal) {
|
|
149
|
+
const r = reader(bytes, start + 1);
|
|
150
|
+
const subop = getU8(r);
|
|
151
|
+
let subop2 = 0;
|
|
152
|
+
if ((subop & TKSH_EXPANDED) !== 0)
|
|
153
|
+
subop2 = getU16(r);
|
|
154
|
+
if ((subop & TKSH_BOUNDING_ONLY) !== 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
if ((subop & TKSH_FIRSTPASS) === 0)
|
|
157
|
+
getI32(r); // shell index for additional LOD/pass
|
|
158
|
+
getU8(r); // lod level
|
|
159
|
+
if ((subop2 & (TKSH2_COLLECTION | TKSH2_NULL)) !== 0)
|
|
160
|
+
return undefined;
|
|
161
|
+
const compressedShell = (subop & (TKSH_COMPRESSED_POINTS | TKSH_CONNECTIVITY_COMPRESSION)) !== 0;
|
|
162
|
+
const scheme = compressedShell ? getU8(r) : CS_NONE;
|
|
163
|
+
let positions;
|
|
164
|
+
let indices;
|
|
165
|
+
let decodeKind;
|
|
166
|
+
if (scheme === CS_EDGEBREAKER) {
|
|
167
|
+
const len = getI32(r);
|
|
168
|
+
if (len <= 0 || r.pos + len > bytes.length || len > 100000000)
|
|
169
|
+
throw new Error('Bad edgebreaker block length.');
|
|
170
|
+
const ebStart = r.pos;
|
|
171
|
+
const eb = bytes.subarray(r.pos, r.pos + len);
|
|
172
|
+
r.pos += len;
|
|
173
|
+
const ebDecoded = decodeEdgebreaker(eb);
|
|
174
|
+
// Since HSF 7+, TK_Shell may store edgebreaker connectivity and then write full-resolution
|
|
175
|
+
// vertex positions immediately after the edgebreaker block. Autodesk Inventor's eModel DWFx
|
|
176
|
+
// files commonly use this form; it gives exact coordinates without having to reconstruct them
|
|
177
|
+
// from parallelogram-predicted quantized residuals.
|
|
178
|
+
if ((subop & TKSH_COMPRESSED_POINTS) === 0) {
|
|
179
|
+
const pointFloats = ebDecoded.pointCount * 3;
|
|
180
|
+
if (r.pos + pointFloats * 4 > bytes.length)
|
|
181
|
+
throw new Error('Edgebreaker shell has no following full-resolution vertex array.');
|
|
182
|
+
positions = readFloat32Array(bytes, r.pos, pointFloats);
|
|
183
|
+
r.pos += pointFloats * 4;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
throw new Error('edgebreaker quantized-point reconstruction is not enabled for this shell.');
|
|
187
|
+
}
|
|
188
|
+
indices = indexArrayFor(ebDecoded.faces, ebDecoded.pointCount);
|
|
189
|
+
decodeKind = `edgebreaker-v${eb[0] ?? 0}`;
|
|
190
|
+
if (indices.length < 3)
|
|
191
|
+
throw new Error('Empty edgebreaker face list.');
|
|
192
|
+
}
|
|
193
|
+
else if (scheme === CS_NONE) {
|
|
194
|
+
const pointCount = getI32(r);
|
|
195
|
+
if (pointCount <= 0 || pointCount > 10000000)
|
|
196
|
+
throw new Error('Bad uncompressed shell point count.');
|
|
197
|
+
positions = readFloat32Array(bytes, r.pos, pointCount * 3);
|
|
198
|
+
r.pos += pointCount * 3 * 4;
|
|
199
|
+
const faceResult = readCompressedFaceList(r, pointCount, (subop & TKSH_TRISTRIPS) !== 0, false);
|
|
200
|
+
indices = indexArrayFor(faceResult.indices, pointCount);
|
|
201
|
+
decodeKind = 'uncompressed';
|
|
202
|
+
}
|
|
203
|
+
else if (scheme === CS_TRIVIAL) {
|
|
204
|
+
const pointCount = getI32(r);
|
|
205
|
+
if (pointCount <= 0 || pointCount > 10000000)
|
|
206
|
+
throw new Error('Bad compressed shell point count.');
|
|
207
|
+
const bounds = (subop2 & TKSH2_GLOBAL_QUANTIZATION) === 0 ? readFloatTuple(r, 6) : [0, 0, 0, 1, 1, 1];
|
|
208
|
+
const bitsPerSample = getU8(r);
|
|
209
|
+
const dataLen = getI32(r);
|
|
210
|
+
if (dataLen <= 0 || r.pos + dataLen > bytes.length)
|
|
211
|
+
throw new Error('Bad compressed shell point payload length.');
|
|
212
|
+
positions = decodeQuantizedPoints(bytes.subarray(r.pos, r.pos + dataLen), pointCount, bitsPerSample, bounds);
|
|
213
|
+
r.pos += dataLen;
|
|
214
|
+
const faceResult = readCompressedFaceList(r, pointCount, (subop & TKSH_TRISTRIPS) !== 0, (subop2 & TKSH2_HAS_NEGATIVE_FACES) !== 0);
|
|
215
|
+
indices = indexArrayFor(faceResult.indices, pointCount);
|
|
216
|
+
decodeKind = `quantized-${bitsPerSample}`;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
throw new Error(`Unsupported W3D shell compression scheme ${scheme}.`);
|
|
220
|
+
}
|
|
221
|
+
if (indices.length < 3 || positions.length < 9)
|
|
222
|
+
return undefined;
|
|
223
|
+
const normals = computeVertexNormals(positions, indices);
|
|
224
|
+
const edgeIndices = computeFeatureEdges(positions, indices);
|
|
225
|
+
const bounds = boundsFromPositions(positions);
|
|
226
|
+
return {
|
|
227
|
+
id: `w3d-mesh-${ordinal}`,
|
|
228
|
+
name: `Mesh ${ordinal}`,
|
|
229
|
+
positions,
|
|
230
|
+
normals,
|
|
231
|
+
indices,
|
|
232
|
+
edgeIndices,
|
|
233
|
+
vertexCount: Math.floor(positions.length / 3),
|
|
234
|
+
triangleCount: Math.floor(indices.length / 3),
|
|
235
|
+
bounds,
|
|
236
|
+
color: colorForOrdinal(ordinal),
|
|
237
|
+
sourceStart: start,
|
|
238
|
+
sourceEnd: r.pos,
|
|
239
|
+
decodeKind
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function decodeEdgebreaker(block) {
|
|
243
|
+
if (block.byteLength < 24)
|
|
244
|
+
throw new Error('Edgebreaker block is too short.');
|
|
245
|
+
const view = new DataView(block.buffer, block.byteOffset, block.byteLength);
|
|
246
|
+
const scheme = block[0];
|
|
247
|
+
const mtableScheme = block[1];
|
|
248
|
+
const pointsScheme = block[2];
|
|
249
|
+
const normalsScheme = block[3];
|
|
250
|
+
const opsLen = view.getInt32(4, true);
|
|
251
|
+
const mtableLen = view.getInt32(8, true);
|
|
252
|
+
const pointsLen = view.getInt32(12, true);
|
|
253
|
+
const pointCount = view.getInt32(16, true);
|
|
254
|
+
const normalsLen = view.getInt32(20, true);
|
|
255
|
+
if (scheme < 0 || scheme > 2 || mtableScheme !== 0 || pointsScheme < 0 || pointsScheme > 3 || normalsScheme < 0 || normalsScheme > 3) {
|
|
256
|
+
throw new Error('Unsupported edgebreaker header.');
|
|
257
|
+
}
|
|
258
|
+
if (opsLen <= 0 || mtableLen < 0 || pointCount <= 0)
|
|
259
|
+
throw new Error('Invalid edgebreaker sizes.');
|
|
260
|
+
let p = scheme >= 1 ? 24 : 20;
|
|
261
|
+
const ops = block.subarray(p, p + opsLen);
|
|
262
|
+
p += opsLen + toNext4(p + opsLen);
|
|
263
|
+
if (p + mtableLen > block.length)
|
|
264
|
+
throw new Error('Invalid edgebreaker MTable offset.');
|
|
265
|
+
const mtable = unpackMTable(block.subarray(p, p + mtableLen));
|
|
266
|
+
p += mtableLen + toNext4(p + mtableLen);
|
|
267
|
+
const processed = processEdgebreakerOpcodes(ops, mtable);
|
|
268
|
+
const patched = patchEdgebreakerFaces(processed.opcodePointCount, mtable, processed.faces);
|
|
269
|
+
const faces = [];
|
|
270
|
+
for (const f of patched)
|
|
271
|
+
faces.push(f[1], f[2], f[3]);
|
|
272
|
+
const maxIndex = faces.reduce((m, v) => Math.max(m, v), -1);
|
|
273
|
+
const minIndex = faces.reduce((m, v) => Math.min(m, v), pointCount);
|
|
274
|
+
if (minIndex < 0 || maxIndex >= pointCount)
|
|
275
|
+
throw new Error(`Edgebreaker face index range ${minIndex}..${maxIndex} is outside point count ${pointCount}.`);
|
|
276
|
+
const diagnostics = [];
|
|
277
|
+
void pointsLen;
|
|
278
|
+
void normalsLen;
|
|
279
|
+
return { pointCount, faces, triangleCount: faces.length / 3, diagnostics };
|
|
280
|
+
}
|
|
281
|
+
function unpackMTable(buf) {
|
|
282
|
+
const r = reader(buf, 0);
|
|
283
|
+
const flags = getI32(r);
|
|
284
|
+
const mlengthsUsed = (flags & MTABLE_HAS_LENGTHS) !== 0 ? getI32(r) : 0;
|
|
285
|
+
const m2stackUsed = (flags & MTABLE_HAS_M2STACKOFFSETS) !== 0 ? getI32(r) : 0;
|
|
286
|
+
const m2gateUsed = (flags & MTABLE_HAS_M2GATEOFFSETS) !== 0 ? m2stackUsed : 0;
|
|
287
|
+
const dummiesUsed = (flags & MTABLE_HAS_DUMMIES) !== 0 ? getI32(r) : 0;
|
|
288
|
+
const patchesUsed = (flags & MTABLE_HAS_PATCHES) !== 0 ? getI32(r) : 0;
|
|
289
|
+
const mlengths = readIntArray(r, mlengthsUsed);
|
|
290
|
+
const m2stackoffsets = readIntArray(r, m2stackUsed);
|
|
291
|
+
const m2gateoffsets = readIntArray(r, m2gateUsed);
|
|
292
|
+
const dummies = [];
|
|
293
|
+
let prev = 0;
|
|
294
|
+
for (let i = 0; i < dummiesUsed; i++) {
|
|
295
|
+
prev += getI32(r);
|
|
296
|
+
dummies.push(prev);
|
|
297
|
+
}
|
|
298
|
+
const patches = [];
|
|
299
|
+
const patchMap = new Map();
|
|
300
|
+
prev = 0;
|
|
301
|
+
for (let i = 0; i < patchesUsed; i += 2) {
|
|
302
|
+
const oldVertex = getI32(r) + prev;
|
|
303
|
+
prev = oldVertex;
|
|
304
|
+
const newVertex = getI32(r);
|
|
305
|
+
patches.push(oldVertex, newVertex);
|
|
306
|
+
patchMap.set(oldVertex, newVertex);
|
|
307
|
+
}
|
|
308
|
+
const bounding = (flags & MTABLE_HAS_BOUNDING) !== 0 ? readFloatTuple(r, 6) : undefined;
|
|
309
|
+
const quantization = (flags & MTABLE_HAS_QUANTIZATION) !== 0 ? [getI32(r), getI32(r), getI32(r)] : [11, 11, 11];
|
|
310
|
+
const normalQuantization = (flags & MTABLE_HAS_QUANTIZATION_NORMALS) !== 0 ? [getI32(r), getI32(r), getI32(r)] : [11, 11, 11];
|
|
311
|
+
return { flags, mlengths, m2stackoffsets, m2gateoffsets, dummies, patches, patchMap, bounding, quantization, normalQuantization };
|
|
312
|
+
}
|
|
313
|
+
function processEdgebreakerOpcodes(opsBytes, mtable) {
|
|
314
|
+
const ops = Array.from(opsBytes);
|
|
315
|
+
const faces = [];
|
|
316
|
+
let v = 0;
|
|
317
|
+
let i = 0;
|
|
318
|
+
while (i < ops.length) {
|
|
319
|
+
const { ecount, offsets } = preprocessEdgebreakerOps(ops, i, mtable);
|
|
320
|
+
if (ecount <= 0)
|
|
321
|
+
throw new Error(`Invalid edgebreaker boundary loop length ${ecount}.`);
|
|
322
|
+
const N = [];
|
|
323
|
+
const P = [];
|
|
324
|
+
const start = [];
|
|
325
|
+
const twin = [];
|
|
326
|
+
const edges = [];
|
|
327
|
+
for (let j = 0; j < ecount; j++) {
|
|
328
|
+
P[j] = j - 1;
|
|
329
|
+
N[j] = j + 1;
|
|
330
|
+
start[j] = v++;
|
|
331
|
+
twin[j] = GARBAGE_EDGE;
|
|
332
|
+
}
|
|
333
|
+
P[0] = ecount - 1;
|
|
334
|
+
N[ecount - 1] = 0;
|
|
335
|
+
let gi = 0;
|
|
336
|
+
let scount = 0;
|
|
337
|
+
let hashUsed = ecount;
|
|
338
|
+
const gateStack = [];
|
|
339
|
+
const availableEdges = [];
|
|
340
|
+
for (;;) {
|
|
341
|
+
if (availableEdges.length === 0)
|
|
342
|
+
availableEdges.push(hashUsed++);
|
|
343
|
+
while (hashUsed + 2 >= N.length) {
|
|
344
|
+
N.push(0);
|
|
345
|
+
P.push(0);
|
|
346
|
+
start.push(0);
|
|
347
|
+
twin.push(GARBAGE_EDGE);
|
|
348
|
+
}
|
|
349
|
+
const ei = edges.length;
|
|
350
|
+
edges.push({ start: 0, twin: GARBAGE_EDGE }, { start: 0, twin: GARBAGE_EDGE }, { start: 0, twin: GARBAGE_EDGE });
|
|
351
|
+
const face = [3, start[gi], start[N[gi]], -1];
|
|
352
|
+
if (twin[gi] !== GARBAGE_EDGE && twin[gi] >= 0 && twin[gi] < edges.length)
|
|
353
|
+
edges[twin[gi]].twin = ei;
|
|
354
|
+
edges[ei].twin = twin[gi];
|
|
355
|
+
edges[ei].start = start[gi];
|
|
356
|
+
edges[ei + 1].start = start[N[gi]];
|
|
357
|
+
const op = ops[i++];
|
|
358
|
+
if (op === CASE_C) {
|
|
359
|
+
face[3] = v;
|
|
360
|
+
edges[ei + 1].twin = GARBAGE_EDGE;
|
|
361
|
+
edges[ei + 2].twin = GARBAGE_EDGE;
|
|
362
|
+
edges[ei + 2].start = v;
|
|
363
|
+
const hi = availableEdges.pop();
|
|
364
|
+
N[P[gi]] = hi;
|
|
365
|
+
P[hi] = P[gi];
|
|
366
|
+
N[hi] = gi;
|
|
367
|
+
P[gi] = hi;
|
|
368
|
+
start[hi] = start[gi];
|
|
369
|
+
start[gi] = v;
|
|
370
|
+
twin[gi] = ei + 1;
|
|
371
|
+
twin[hi] = ei + 2;
|
|
372
|
+
v++;
|
|
373
|
+
}
|
|
374
|
+
else if (op === CASE_L) {
|
|
375
|
+
face[3] = start[P[gi]];
|
|
376
|
+
edges[ei + 1].twin = GARBAGE_EDGE;
|
|
377
|
+
edges[ei + 2].twin = twin[P[gi]];
|
|
378
|
+
if (twin[P[gi]] !== GARBAGE_EDGE && twin[P[gi]] >= 0 && twin[P[gi]] < edges.length)
|
|
379
|
+
edges[twin[P[gi]]].twin = ei + 2;
|
|
380
|
+
edges[ei + 2].start = start[P[gi]];
|
|
381
|
+
start[gi] = start[P[gi]];
|
|
382
|
+
availableEdges.push(P[gi]);
|
|
383
|
+
P[gi] = P[P[gi]];
|
|
384
|
+
N[P[gi]] = gi;
|
|
385
|
+
twin[gi] = ei + 1;
|
|
386
|
+
}
|
|
387
|
+
else if (op === CASE_E) {
|
|
388
|
+
face[3] = start[P[gi]];
|
|
389
|
+
edges[ei + 1].twin = twin[N[gi]];
|
|
390
|
+
if (twin[N[gi]] !== GARBAGE_EDGE && twin[N[gi]] >= 0 && twin[N[gi]] < edges.length)
|
|
391
|
+
edges[twin[N[gi]]].twin = ei + 1;
|
|
392
|
+
edges[ei + 2].twin = twin[P[gi]];
|
|
393
|
+
if (twin[P[gi]] !== GARBAGE_EDGE && twin[P[gi]] >= 0 && twin[P[gi]] < edges.length)
|
|
394
|
+
edges[twin[P[gi]]].twin = ei + 2;
|
|
395
|
+
edges[ei + 2].start = start[P[gi]];
|
|
396
|
+
availableEdges.push(gi, P[gi], N[gi]);
|
|
397
|
+
if (isValidFace(face))
|
|
398
|
+
faces.push(face);
|
|
399
|
+
if (gateStack.length > 0)
|
|
400
|
+
gi = gateStack.pop();
|
|
401
|
+
else
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
else if (op === CASE_R) {
|
|
405
|
+
face[3] = start[N[N[gi]]];
|
|
406
|
+
edges[ei + 1].twin = twin[N[gi]];
|
|
407
|
+
if (twin[N[gi]] !== GARBAGE_EDGE && twin[N[gi]] >= 0 && twin[N[gi]] < edges.length)
|
|
408
|
+
edges[twin[N[gi]]].twin = ei + 1;
|
|
409
|
+
edges[ei + 2].twin = GARBAGE_EDGE;
|
|
410
|
+
edges[ei + 2].start = start[N[N[gi]]];
|
|
411
|
+
availableEdges.push(N[gi]);
|
|
412
|
+
N[gi] = N[N[gi]];
|
|
413
|
+
P[N[gi]] = gi;
|
|
414
|
+
twin[gi] = ei + 2;
|
|
415
|
+
}
|
|
416
|
+
else if (op === CASE_S) {
|
|
417
|
+
let bi = gi;
|
|
418
|
+
const nSteps = (offsets[scount] ?? 0) + 1;
|
|
419
|
+
for (let k = 0; k < nSteps; k++)
|
|
420
|
+
bi = N[bi];
|
|
421
|
+
face[3] = start[N[bi]];
|
|
422
|
+
edges[ei + 1].twin = GARBAGE_EDGE;
|
|
423
|
+
edges[ei + 2].twin = GARBAGE_EDGE;
|
|
424
|
+
edges[ei + 2].start = start[N[bi]];
|
|
425
|
+
scount++;
|
|
426
|
+
const hi = availableEdges.pop();
|
|
427
|
+
gateStack.push(hi);
|
|
428
|
+
N[P[gi]] = hi;
|
|
429
|
+
P[hi] = P[gi];
|
|
430
|
+
N[hi] = N[bi];
|
|
431
|
+
P[N[bi]] = hi;
|
|
432
|
+
twin[hi] = ei + 2;
|
|
433
|
+
start[hi] = start[gi];
|
|
434
|
+
start[gi] = start[N[bi]];
|
|
435
|
+
twin[gi] = ei + 1;
|
|
436
|
+
P[gi] = bi;
|
|
437
|
+
N[bi] = gi;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
throw new Error(`Unsupported edgebreaker opcode ${op}.`);
|
|
441
|
+
}
|
|
442
|
+
if (op !== CASE_E) {
|
|
443
|
+
if (isValidFace(face))
|
|
444
|
+
faces.push(face);
|
|
445
|
+
else
|
|
446
|
+
throw new Error('Invalid edgebreaker triangle.');
|
|
447
|
+
}
|
|
448
|
+
if (i >= ops.length)
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { faces, opcodePointCount: v };
|
|
453
|
+
}
|
|
454
|
+
function preprocessEdgebreakerOps(ops, start, mtable) {
|
|
455
|
+
let ecount = 0;
|
|
456
|
+
let scount = 0;
|
|
457
|
+
let mcount = 0;
|
|
458
|
+
let m2count = 0;
|
|
459
|
+
const eStack = [];
|
|
460
|
+
const sStack = [];
|
|
461
|
+
const offsets = [];
|
|
462
|
+
for (let i = start; i < ops.length; i++) {
|
|
463
|
+
const op = ops[i];
|
|
464
|
+
if (op === CASE_C)
|
|
465
|
+
ecount -= 1;
|
|
466
|
+
else if (op === CASE_L)
|
|
467
|
+
ecount += 1;
|
|
468
|
+
else if (op === CASE_E) {
|
|
469
|
+
ecount += 3;
|
|
470
|
+
if (eStack.length > 0) {
|
|
471
|
+
const s = sStack.pop();
|
|
472
|
+
offsets[s] = ecount - 2 - eStack.pop();
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (op === CASE_R)
|
|
479
|
+
ecount += 1;
|
|
480
|
+
else if (op === CASE_S) {
|
|
481
|
+
ecount -= 1;
|
|
482
|
+
sStack.push(scount);
|
|
483
|
+
eStack.push(ecount);
|
|
484
|
+
offsets[scount] = 0;
|
|
485
|
+
scount++;
|
|
486
|
+
}
|
|
487
|
+
else if (op === CASE_M) {
|
|
488
|
+
const length = mtable.mlengths[mcount++] ?? 0;
|
|
489
|
+
ecount -= length + 1;
|
|
490
|
+
}
|
|
491
|
+
else if (op === CASE_M2) {
|
|
492
|
+
const length = mtable.mlengths[m2count] ?? 0;
|
|
493
|
+
ecount -= 1;
|
|
494
|
+
void length;
|
|
495
|
+
m2count++;
|
|
496
|
+
}
|
|
497
|
+
else
|
|
498
|
+
throw new Error(`Bad edgebreaker opcode ${op}.`);
|
|
499
|
+
}
|
|
500
|
+
return { ecount, offsets };
|
|
501
|
+
}
|
|
502
|
+
function patchEdgebreakerFaces(opcodePointCount, mtable, faces) {
|
|
503
|
+
const pointMap = new Int32Array(opcodePointCount);
|
|
504
|
+
for (const d of mtable.dummies)
|
|
505
|
+
if (d >= 0 && d < opcodePointCount)
|
|
506
|
+
pointMap[d] = POINTMAP_DUMMY;
|
|
507
|
+
for (let i = 0; i + 1 < mtable.patches.length; i += 2) {
|
|
508
|
+
const oldVertex = mtable.patches[i];
|
|
509
|
+
if (oldVertex >= 0 && oldVertex < opcodePointCount && pointMap[oldVertex] !== POINTMAP_DUMMY)
|
|
510
|
+
pointMap[oldVertex] = POINTMAP_ALIAS;
|
|
511
|
+
}
|
|
512
|
+
let shift = 0;
|
|
513
|
+
for (let i = 0; i < opcodePointCount; i++) {
|
|
514
|
+
if (pointMap[i] < 0)
|
|
515
|
+
shift++;
|
|
516
|
+
else
|
|
517
|
+
pointMap[i] = shift;
|
|
518
|
+
}
|
|
519
|
+
const out = [];
|
|
520
|
+
for (const f of faces) {
|
|
521
|
+
const a = f[1], b = f[2], c = f[3];
|
|
522
|
+
if (isDummyOrOutOfRange(a, pointMap) || isDummyOrOutOfRange(b, pointMap) || isDummyOrOutOfRange(c, pointMap))
|
|
523
|
+
continue;
|
|
524
|
+
const mapped = [3, mapPatchedVertex(a, pointMap, mtable), mapPatchedVertex(b, pointMap, mtable), mapPatchedVertex(c, pointMap, mtable)];
|
|
525
|
+
if (isValidFace(mapped))
|
|
526
|
+
out.push(mapped);
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
function mapPatchedVertex(v, pointMap, mtable) {
|
|
531
|
+
return pointMap[v] === POINTMAP_ALIAS ? (mtable.patchMap.get(v) ?? v) : v - pointMap[v];
|
|
532
|
+
}
|
|
533
|
+
function isDummyOrOutOfRange(v, pointMap) {
|
|
534
|
+
return v < 0 || v >= pointMap.length || pointMap[v] === POINTMAP_DUMMY;
|
|
535
|
+
}
|
|
536
|
+
function isValidFace(face) {
|
|
537
|
+
return face[1] !== face[2] && face[2] !== face[3] && face[3] !== face[1] && face[1] >= 0 && face[2] >= 0 && face[3] >= 0;
|
|
538
|
+
}
|
|
539
|
+
function readCompressedFaceList(r, pointCount, tristrips, signed) {
|
|
540
|
+
getU8(r); // face-list compression scheme; currently CS_TRIVIAL in supported streams.
|
|
541
|
+
const len = getI32(r);
|
|
542
|
+
if (len <= 0 || r.pos + len > r.bytes.length)
|
|
543
|
+
throw new Error('Bad compressed face-list payload length.');
|
|
544
|
+
const payload = r.bytes.subarray(r.pos, r.pos + len);
|
|
545
|
+
r.pos += len;
|
|
546
|
+
const values = decodeTrivialFaceList(payload, signed);
|
|
547
|
+
const indices = triangulateFaceList(values, pointCount, tristrips);
|
|
548
|
+
return { indices, values };
|
|
549
|
+
}
|
|
550
|
+
function decodeTrivialFaceList(payload, signed) {
|
|
551
|
+
if (payload.length < 2)
|
|
552
|
+
throw new Error('Face payload is too short.');
|
|
553
|
+
const bits = payload[0];
|
|
554
|
+
const bytesPerSample = bits / 8;
|
|
555
|
+
if (bytesPerSample !== 1 && bytesPerSample !== 2 && bytesPerSample !== 4)
|
|
556
|
+
throw new Error(`Unsupported face-list sample width ${bits}.`);
|
|
557
|
+
const out = [];
|
|
558
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
559
|
+
for (let p = 1; p + bytesPerSample <= payload.length; p += bytesPerSample) {
|
|
560
|
+
if (bytesPerSample === 1)
|
|
561
|
+
out.push(signed ? view.getInt8(p) : view.getUint8(p));
|
|
562
|
+
else if (bytesPerSample === 2)
|
|
563
|
+
out.push(signed ? view.getInt16(p, true) : view.getUint16(p, true));
|
|
564
|
+
else
|
|
565
|
+
out.push(signed ? view.getInt32(p, true) : view.getUint32(p, true));
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
function triangulateFaceList(values, pointCount, tristrips) {
|
|
570
|
+
const out = [];
|
|
571
|
+
let i = 0;
|
|
572
|
+
while (i < values.length) {
|
|
573
|
+
const rawCount = values[i++];
|
|
574
|
+
const count = Math.abs(rawCount);
|
|
575
|
+
if (count < 3 || i + count > values.length)
|
|
576
|
+
throw new Error('Invalid face list run.');
|
|
577
|
+
const verts = values.slice(i, i + count);
|
|
578
|
+
i += count;
|
|
579
|
+
if (verts.some(v => v < 0 || v >= pointCount))
|
|
580
|
+
throw new Error('Face index out of range.');
|
|
581
|
+
if (tristrips) {
|
|
582
|
+
if (rawCount > 0) {
|
|
583
|
+
for (let k = 2; k < verts.length; k++) {
|
|
584
|
+
if ((k & 1) === 0)
|
|
585
|
+
out.push(verts[k - 2], verts[k - 1], verts[k]);
|
|
586
|
+
else
|
|
587
|
+
out.push(verts[k - 1], verts[k - 2], verts[k]);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
for (let k = 2; k < verts.length; k++)
|
|
592
|
+
out.push(verts[0], verts[k - 1], verts[k]);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else if (rawCount > 0) {
|
|
596
|
+
for (let k = 2; k < verts.length; k++)
|
|
597
|
+
out.push(verts[0], verts[k - 1], verts[k]);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
function decodeQuantizedPoints(payload, count, bitsPerSample, bounds) {
|
|
603
|
+
if (bitsPerSample === 8) {
|
|
604
|
+
if (payload.length < count * 3)
|
|
605
|
+
throw new Error('Short 8-bit quantized point payload.');
|
|
606
|
+
const out = new Float32Array(count * 3);
|
|
607
|
+
for (let i = 0; i < count; i++) {
|
|
608
|
+
for (let a = 0; a < 3; a++) {
|
|
609
|
+
const v = payload[i * 3 + a];
|
|
610
|
+
const lo = bounds[a];
|
|
611
|
+
const hi = bounds[a + 3];
|
|
612
|
+
out[i * 3 + a] = v === 255 ? hi : lo + (hi - lo) * (v / 255);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
// Generic bit-packed quantization. HSF stores packed values byte-swapped for 32-bit alignment;
|
|
618
|
+
// MSB-first decoding matches the DWF Toolkit BPack reader after SwapBytes for common >= 6.50 streams.
|
|
619
|
+
const out = new Float32Array(count * 3);
|
|
620
|
+
let bit = 0;
|
|
621
|
+
const max = (1 << bitsPerSample) - 1;
|
|
622
|
+
for (let i = 0; i < count * 3; i++) {
|
|
623
|
+
let raw = 0;
|
|
624
|
+
for (let k = 0; k < bitsPerSample; k++) {
|
|
625
|
+
const b = payload[bit >> 3] ?? 0;
|
|
626
|
+
raw = (raw << 1) | ((b >> (7 - (bit & 7))) & 1);
|
|
627
|
+
bit++;
|
|
628
|
+
}
|
|
629
|
+
const a = i % 3;
|
|
630
|
+
const lo = bounds[a];
|
|
631
|
+
const hi = bounds[a + 3];
|
|
632
|
+
out[i] = raw === max ? hi : lo + (hi - lo) * (raw / max);
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
function indexArrayFor(indices, vertexCount) {
|
|
637
|
+
if (vertexCount <= 65535)
|
|
638
|
+
return new Uint16Array(indices);
|
|
639
|
+
return new Uint32Array(indices);
|
|
640
|
+
}
|
|
641
|
+
function computeVertexNormals(positions, indices) {
|
|
642
|
+
const normals = new Float32Array(positions.length);
|
|
643
|
+
for (let i = 0; i + 2 < indices.length; i += 3) {
|
|
644
|
+
const ia = indices[i] * 3, ib = indices[i + 1] * 3, ic = indices[i + 2] * 3;
|
|
645
|
+
const ax = positions[ia], ay = positions[ia + 1], az = positions[ia + 2];
|
|
646
|
+
const bx = positions[ib], by = positions[ib + 1], bz = positions[ib + 2];
|
|
647
|
+
const cx = positions[ic], cy = positions[ic + 1], cz = positions[ic + 2];
|
|
648
|
+
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
649
|
+
const acx = cx - ax, acy = cy - ay, acz = cz - az;
|
|
650
|
+
const nx = aby * acz - abz * acy;
|
|
651
|
+
const ny = abz * acx - abx * acz;
|
|
652
|
+
const nz = abx * acy - aby * acx;
|
|
653
|
+
normals[ia] = (normals[ia] ?? 0) + nx;
|
|
654
|
+
normals[ia + 1] = (normals[ia + 1] ?? 0) + ny;
|
|
655
|
+
normals[ia + 2] = (normals[ia + 2] ?? 0) + nz;
|
|
656
|
+
normals[ib] = (normals[ib] ?? 0) + nx;
|
|
657
|
+
normals[ib + 1] = (normals[ib + 1] ?? 0) + ny;
|
|
658
|
+
normals[ib + 2] = (normals[ib + 2] ?? 0) + nz;
|
|
659
|
+
normals[ic] = (normals[ic] ?? 0) + nx;
|
|
660
|
+
normals[ic + 1] = (normals[ic + 1] ?? 0) + ny;
|
|
661
|
+
normals[ic + 2] = (normals[ic + 2] ?? 0) + nz;
|
|
662
|
+
}
|
|
663
|
+
for (let i = 0; i < normals.length; i += 3) {
|
|
664
|
+
const nx = normals[i], ny = normals[i + 1], nz = normals[i + 2];
|
|
665
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
666
|
+
normals[i] = nx / len;
|
|
667
|
+
normals[i + 1] = ny / len;
|
|
668
|
+
normals[i + 2] = nz / len;
|
|
669
|
+
}
|
|
670
|
+
return normals;
|
|
671
|
+
}
|
|
672
|
+
function computeFeatureEdges(positions, indices, creaseAngleRadians = Math.PI / 5) {
|
|
673
|
+
if (indices.length < 3)
|
|
674
|
+
return undefined;
|
|
675
|
+
const triNormals = [];
|
|
676
|
+
const edges = new Map();
|
|
677
|
+
for (let i = 0, tri = 0; i + 2 < indices.length; i += 3, tri++) {
|
|
678
|
+
const a = indices[i], b = indices[i + 1], c = indices[i + 2];
|
|
679
|
+
const n = faceNormal(positions, a, b, c);
|
|
680
|
+
triNormals.push(n);
|
|
681
|
+
addEdge(edges, a, b, tri);
|
|
682
|
+
addEdge(edges, b, c, tri);
|
|
683
|
+
addEdge(edges, c, a, tri);
|
|
684
|
+
}
|
|
685
|
+
const threshold = Math.cos(creaseAngleRadians);
|
|
686
|
+
const out = [];
|
|
687
|
+
for (const e of edges.values()) {
|
|
688
|
+
if (e.tris.length === 1) {
|
|
689
|
+
out.push(e.a, e.b);
|
|
690
|
+
}
|
|
691
|
+
else if (e.tris.length >= 2) {
|
|
692
|
+
let sharp = false;
|
|
693
|
+
for (let i = 0; i < e.tris.length && !sharp; i++) {
|
|
694
|
+
for (let j = i + 1; j < e.tris.length; j++) {
|
|
695
|
+
const na = triNormals[e.tris[i]];
|
|
696
|
+
const nb = triNormals[e.tris[j]];
|
|
697
|
+
const dot = na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2];
|
|
698
|
+
if (dot < threshold) {
|
|
699
|
+
sharp = true;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (sharp)
|
|
705
|
+
out.push(e.a, e.b);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (out.length === 0)
|
|
709
|
+
return undefined;
|
|
710
|
+
const vertexCount = Math.floor(positions.length / 3);
|
|
711
|
+
return indexArrayFor(out, vertexCount);
|
|
712
|
+
}
|
|
713
|
+
function faceNormal(positions, a, b, c) {
|
|
714
|
+
const ia = a * 3, ib = b * 3, ic = c * 3;
|
|
715
|
+
const ax = positions[ia], ay = positions[ia + 1], az = positions[ia + 2];
|
|
716
|
+
const bx = positions[ib], by = positions[ib + 1], bz = positions[ib + 2];
|
|
717
|
+
const cx = positions[ic], cy = positions[ic + 1], cz = positions[ic + 2];
|
|
718
|
+
const abx = bx - ax, aby = by - ay, abz = bz - az;
|
|
719
|
+
const acx = cx - ax, acy = cy - ay, acz = cz - az;
|
|
720
|
+
let nx = aby * acz - abz * acy;
|
|
721
|
+
let ny = abz * acx - abx * acz;
|
|
722
|
+
let nz = abx * acy - aby * acx;
|
|
723
|
+
const len = Math.hypot(nx, ny, nz) || 1;
|
|
724
|
+
nx /= len;
|
|
725
|
+
ny /= len;
|
|
726
|
+
nz /= len;
|
|
727
|
+
return [nx, ny, nz];
|
|
728
|
+
}
|
|
729
|
+
function addEdge(edges, a, b, tri) {
|
|
730
|
+
const x = Math.min(a, b), y = Math.max(a, b);
|
|
731
|
+
const key = `${x}:${y}`;
|
|
732
|
+
let e = edges.get(key);
|
|
733
|
+
if (!e) {
|
|
734
|
+
e = { a: x, b: y, tris: [] };
|
|
735
|
+
edges.set(key, e);
|
|
736
|
+
}
|
|
737
|
+
e.tris.push(tri);
|
|
738
|
+
}
|
|
739
|
+
function computeModelBounds(meshes) {
|
|
740
|
+
if (meshes.length === 0)
|
|
741
|
+
return { min: [0, 0, 0], max: [1, 1, 1], center: [0.5, 0.5, 0.5], radius: 1 };
|
|
742
|
+
const min = [Infinity, Infinity, Infinity];
|
|
743
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
744
|
+
for (const mesh of meshes) {
|
|
745
|
+
for (let a = 0; a < 3; a++) {
|
|
746
|
+
min[a] = Math.min(min[a], mesh.bounds.min[a]);
|
|
747
|
+
max[a] = Math.max(max[a], mesh.bounds.max[a]);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const center = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2];
|
|
751
|
+
const radius = Math.max(1e-6, Math.hypot(max[0] - center[0], max[1] - center[1], max[2] - center[2]));
|
|
752
|
+
return { min: min, max: max, center, radius };
|
|
753
|
+
}
|
|
754
|
+
function boundsFromPositions(positions) {
|
|
755
|
+
const min = [Infinity, Infinity, Infinity];
|
|
756
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
757
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
758
|
+
const x = positions[i], y = positions[i + 1], z = positions[i + 2];
|
|
759
|
+
if (x < min[0])
|
|
760
|
+
min[0] = x;
|
|
761
|
+
if (y < min[1])
|
|
762
|
+
min[1] = y;
|
|
763
|
+
if (z < min[2])
|
|
764
|
+
min[2] = z;
|
|
765
|
+
if (x > max[0])
|
|
766
|
+
max[0] = x;
|
|
767
|
+
if (y > max[1])
|
|
768
|
+
max[1] = y;
|
|
769
|
+
if (z > max[2])
|
|
770
|
+
max[2] = z;
|
|
771
|
+
}
|
|
772
|
+
return { min, max };
|
|
773
|
+
}
|
|
774
|
+
function colorForOrdinal(i) {
|
|
775
|
+
const hue = (i * 0.61803398875) % 1;
|
|
776
|
+
const s = 0.34, l = 0.62;
|
|
777
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
778
|
+
const p = 2 * l - q;
|
|
779
|
+
return [hue2rgb(p, q, hue + 1 / 3), hue2rgb(p, q, hue), hue2rgb(p, q, hue - 1 / 3)];
|
|
780
|
+
}
|
|
781
|
+
function hue2rgb(p, q, t) {
|
|
782
|
+
if (t < 0)
|
|
783
|
+
t += 1;
|
|
784
|
+
if (t > 1)
|
|
785
|
+
t -= 1;
|
|
786
|
+
if (t < 1 / 6)
|
|
787
|
+
return p + (q - p) * 6 * t;
|
|
788
|
+
if (t < 1 / 2)
|
|
789
|
+
return q;
|
|
790
|
+
if (t < 2 / 3)
|
|
791
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
792
|
+
return p;
|
|
793
|
+
}
|
|
794
|
+
function readFloat32Array(bytes, offset, count) {
|
|
795
|
+
if (offset + count * 4 > bytes.length)
|
|
796
|
+
throw new Error('Float array exceeds W3D stream length.');
|
|
797
|
+
const out = new Float32Array(count);
|
|
798
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, count * 4);
|
|
799
|
+
for (let i = 0; i < count; i++)
|
|
800
|
+
out[i] = view.getFloat32(i * 4, true);
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
function readFloatTuple(r, count) {
|
|
804
|
+
const out = [];
|
|
805
|
+
for (let i = 0; i < count; i++)
|
|
806
|
+
out.push(getF32(r));
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
function readIntArray(r, count) {
|
|
810
|
+
const out = [];
|
|
811
|
+
for (let i = 0; i < count; i++)
|
|
812
|
+
out.push(getI32(r));
|
|
813
|
+
return out;
|
|
814
|
+
}
|
|
815
|
+
function reader(bytes, pos = 0) {
|
|
816
|
+
return { bytes, pos, view: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) };
|
|
817
|
+
}
|
|
818
|
+
function getU8(r) {
|
|
819
|
+
if (r.pos >= r.bytes.length)
|
|
820
|
+
throw new Error('Unexpected end of W3D stream.');
|
|
821
|
+
return r.bytes[r.pos++];
|
|
822
|
+
}
|
|
823
|
+
function getU16(r) { const v = r.view.getUint16(r.pos, true); r.pos += 2; return v; }
|
|
824
|
+
function getI32(r) { const v = r.view.getInt32(r.pos, true); r.pos += 4; return v; }
|
|
825
|
+
function getF32(r) { const v = r.view.getFloat32(r.pos, true); r.pos += 4; return v; }
|
|
826
|
+
function toNext4(i) { return 3 - ((i - 1) % 4); }
|