dsh-3d-viewer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -107
- package/README.zh.md +180 -107
- package/THIRD_PARTY_NOTICES.md +54 -52
- package/assets/hdri/LICENSE.md +17 -16
- package/dist/client.fbx.js +14 -0
- package/dist/client.gltf.js +14 -0
- package/dist/client.js +4472 -0
- package/dist/client.obj.js +19 -0
- package/dist/client.stl.js +14 -0
- package/dist/client.tex-exr.js +14 -0
- package/dist/client.tex-hdr.js +18 -0
- package/dist/client.tex-tga.js +14 -0
- package/index.js +167 -7
- package/package.json +72 -48
- package/assets/preview.png +0 -0
- package/build.mjs +0 -49
- package/client.js +0 -4462
- package/scripts/embed-hdri.mjs +0 -70
- package/src/bytes.js +0 -158
- package/src/client.js +0 -57
- package/src/formats.js +0 -100
- package/src/generated/hdri-data.js +0 -8
- package/src/loaders/gltf.js +0 -251
- package/src/loaders/model-loader.js +0 -232
- package/src/loaders/workspace.js +0 -295
- package/src/model.js +0 -417
- package/src/scene/animation.js +0 -149
- package/src/scene/blue-noise.js +0 -48
- package/src/scene/camera.js +0 -83
- package/src/scene/createSceneView.js +0 -375
- package/src/scene/environment.js +0 -187
- package/src/scene/ground.js +0 -286
- package/src/scene/lighting.js +0 -41
- package/src/scene/materials.js +0 -263
- package/src/scene/postprocessing.js +0 -155
- package/src/scene/render-loop.js +0 -98
- package/src/scene/viewer-error.js +0 -16
- package/src/ui/AnimationBar.js +0 -95
- package/src/ui/ModelViewer.js +0 -278
- package/src/ui/ViewerSurface.js +0 -87
- package/src/ui/chrome.js +0 -220
- package/src/ui/dictionaries.js +0 -99
- package/src/ui/icons.js +0 -88
- package/src/ui/options.js +0 -23
- package/src/ui/primitives.js +0 -88
- package/src/ui/styles.js +0 -89
- package/src/vendor/FBXLoaderPbr.js +0 -4772
package/src/loaders/gltf.js
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { Box3, LoadingManager, Vector3 } from 'three';
|
|
2
|
-
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
3
|
-
import {
|
|
4
|
-
abortError,
|
|
5
|
-
decodeLocalPath,
|
|
6
|
-
decodeText,
|
|
7
|
-
isLocalPath,
|
|
8
|
-
mimeOf,
|
|
9
|
-
toArrayBuffer,
|
|
10
|
-
toBytes,
|
|
11
|
-
warn,
|
|
12
|
-
} from '../bytes.js';
|
|
13
|
-
import { disposeObject } from '../model.js';
|
|
14
|
-
import { WHITE_PIXEL } from './workspace.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extract the JSON chunk of a binary GLB container.
|
|
18
|
-
*
|
|
19
|
-
* @param {Uint8Array} bytes Complete GLB bytes.
|
|
20
|
-
* @returns {string | null} JSON text, or null when the container has no JSON chunk.
|
|
21
|
-
*/
|
|
22
|
-
function jsonChunkOfGlb(bytes) {
|
|
23
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
24
|
-
let offset = 12;
|
|
25
|
-
while (offset + 8 <= bytes.byteLength) {
|
|
26
|
-
const length = view.getUint32(offset, true);
|
|
27
|
-
const type = view.getUint32(offset + 4, true);
|
|
28
|
-
const start = offset + 8;
|
|
29
|
-
if (start + length > bytes.byteLength) break;
|
|
30
|
-
if (type === 0x4e4f534a) {
|
|
31
|
-
return decodeText(bytes.subarray(start, start + length)).replace(/\0+$/u, '').trim();
|
|
32
|
-
}
|
|
33
|
-
offset = start + length;
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Parse glTF JSON from either a `.gltf` text document or a `.glb` container.
|
|
40
|
-
*
|
|
41
|
-
* @param {Uint8Array | ArrayBuffer | string} data Complete glTF/GLB bytes.
|
|
42
|
-
* @returns {object | null} Parsed glTF JSON, or null when a GLB has no JSON chunk.
|
|
43
|
-
*/
|
|
44
|
-
function parseGltfJson(data) {
|
|
45
|
-
const bytes = toBytes(data);
|
|
46
|
-
if (bytes.byteLength >= 12
|
|
47
|
-
&& bytes[0] === 0x67
|
|
48
|
-
&& bytes[1] === 0x6c
|
|
49
|
-
&& bytes[2] === 0x54
|
|
50
|
-
&& bytes[3] === 0x46) {
|
|
51
|
-
const json = jsonChunkOfGlb(bytes);
|
|
52
|
-
return json === null ? null : JSON.parse(json);
|
|
53
|
-
}
|
|
54
|
-
return JSON.parse(decodeText(bytes).replace(/^\uFEFF/u, ''));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* List the external buffers and images a glTF document references.
|
|
59
|
-
*
|
|
60
|
-
* @param {any} json Parsed glTF JSON.
|
|
61
|
-
* @returns {Array<{ uri: string, kind: 'buffer' | 'image' }>} Local URIs in document order.
|
|
62
|
-
*/
|
|
63
|
-
function collectUris(json) {
|
|
64
|
-
const entries = [];
|
|
65
|
-
for (const buffer of Array.isArray(json.buffers) ? json.buffers : []) {
|
|
66
|
-
if (isLocalPath(buffer?.uri)) entries.push({ uri: buffer.uri, kind: 'buffer' });
|
|
67
|
-
}
|
|
68
|
-
for (const image of Array.isArray(json.images) ? json.images : []) {
|
|
69
|
-
if (isLocalPath(image?.uri)) entries.push({ uri: image.uri, kind: 'image' });
|
|
70
|
-
}
|
|
71
|
-
return entries;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createAssets() {
|
|
75
|
-
const blobUrls = new Set();
|
|
76
|
-
return {
|
|
77
|
-
onTextureLoad() {
|
|
78
|
-
return () => {};
|
|
79
|
-
},
|
|
80
|
-
track(url) {
|
|
81
|
-
blobUrls.add(url);
|
|
82
|
-
},
|
|
83
|
-
dispose() {
|
|
84
|
-
for (const url of blobUrls) URL.revokeObjectURL(url);
|
|
85
|
-
blobUrls.clear();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Hide Sketchfab-style baked contact-shadow planes.
|
|
92
|
-
*
|
|
93
|
-
* These are usually a single large quad at the model's ground plane with a
|
|
94
|
-
* transparent material and a baked shadow texture. They are not part of the
|
|
95
|
-
* vehicle/object itself and look like a gray slab when alpha is unavailable or
|
|
96
|
-
* the plane is viewed at an angle.
|
|
97
|
-
*
|
|
98
|
-
* @param {import('three').Object3D} object Parsed glTF scene.
|
|
99
|
-
* @returns {number} Number of hidden meshes.
|
|
100
|
-
*/
|
|
101
|
-
export function hideBakedShadowPlanes(object) {
|
|
102
|
-
object.updateMatrixWorld(true);
|
|
103
|
-
const modelBox = new Box3().setFromObject(object);
|
|
104
|
-
const modelSize = modelBox.getSize(new Vector3());
|
|
105
|
-
const maxDimension = Math.max(modelSize.x, modelSize.y, modelSize.z);
|
|
106
|
-
if (!Number.isFinite(maxDimension) || maxDimension <= 0) return 0;
|
|
107
|
-
|
|
108
|
-
let hidden = 0;
|
|
109
|
-
object.traverse((child) => {
|
|
110
|
-
if (child.isMesh !== true || child.isSkinnedMesh === true) return;
|
|
111
|
-
|
|
112
|
-
const geometry = child.geometry;
|
|
113
|
-
const position = geometry?.getAttribute?.('position');
|
|
114
|
-
if (position === undefined || position.count > 8) return;
|
|
115
|
-
|
|
116
|
-
const index = geometry.getIndex();
|
|
117
|
-
const triangles = (index === null ? position.count : index.count) / 3;
|
|
118
|
-
if (triangles > 4) return;
|
|
119
|
-
|
|
120
|
-
const materials = Array.isArray(child.material) ? child.material : [child.material];
|
|
121
|
-
const transparent = materials.some((material) => material !== null
|
|
122
|
-
&& (material.transparent === true || material.opacity < 1));
|
|
123
|
-
if (!transparent) return;
|
|
124
|
-
|
|
125
|
-
const label = `${child.name} ${materials.map((material) => material?.name ?? '').join(' ')}`;
|
|
126
|
-
if (/plane|shadow|ground|contact|bake/iu.test(label) === false) return;
|
|
127
|
-
|
|
128
|
-
const box = new Box3().setFromObject(child);
|
|
129
|
-
const size = box.getSize(new Vector3());
|
|
130
|
-
const thickness = Math.min(size.x, size.y, size.z);
|
|
131
|
-
const horizontalArea = size.x * size.z;
|
|
132
|
-
const modelArea = modelSize.x * modelSize.z;
|
|
133
|
-
|
|
134
|
-
if (thickness > maxDimension * 0.001) return;
|
|
135
|
-
if (box.min.y - modelBox.min.y > maxDimension * 0.005) return;
|
|
136
|
-
if (horizontalArea < modelArea * 0.25) return;
|
|
137
|
-
|
|
138
|
-
child.visible = false;
|
|
139
|
-
child.castShadow = false;
|
|
140
|
-
child.receiveShadow = false;
|
|
141
|
-
hidden += 1;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
return hidden;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Load a glTF or GLB scene, including external workspace buffers/images and
|
|
149
|
-
* animation clips. The file may be either JSON text or a binary GLB container.
|
|
150
|
-
*
|
|
151
|
-
* @param {Uint8Array | ArrayBuffer | string} data Complete glTF/GLB bytes.
|
|
152
|
-
* @param {{
|
|
153
|
-
* readWorkspaceFile?: Function,
|
|
154
|
-
* resourceAddress?: string,
|
|
155
|
-
* signal?: AbortSignal,
|
|
156
|
-
* onWarn?: (message: string) => void,
|
|
157
|
-
* }} [options] Workspace reader, document address, cancellation, and diagnostics.
|
|
158
|
-
* @returns {Promise<{ object: import('three').Object3D, assets: object }>} Scene and asset controls.
|
|
159
|
-
*/
|
|
160
|
-
export async function loadGltfModel(data, options = {}) {
|
|
161
|
-
const onWarn = options.onWarn ?? warn;
|
|
162
|
-
const assets = createAssets();
|
|
163
|
-
const blobs = new Map();
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
let json = null;
|
|
167
|
-
try {
|
|
168
|
-
json = parseGltfJson(data);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
throw new Error(`Invalid glTF JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (json !== null && typeof options.readWorkspaceFile === 'function'
|
|
174
|
-
&& typeof options.resourceAddress === 'string') {
|
|
175
|
-
// A document may reference the same buffer or image from several
|
|
176
|
-
// primitives; each unique (kind, uri) pair is read exactly once.
|
|
177
|
-
const entries = new Map();
|
|
178
|
-
for (const entry of collectUris(json)) {
|
|
179
|
-
entries.set(`${entry.kind}\u0000${entry.uri}`, entry);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
await Promise.all([...entries.values()].map(async (entry) => {
|
|
183
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
184
|
-
try {
|
|
185
|
-
const result = await options.readWorkspaceFile(options.resourceAddress, decodeLocalPath(entry.uri), {
|
|
186
|
-
signal: options.signal,
|
|
187
|
-
});
|
|
188
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
189
|
-
if (result?.ok === true) {
|
|
190
|
-
const blob = URL.createObjectURL(new Blob([result.value.data], {
|
|
191
|
-
type: entry.kind === 'image' ? mimeOf(entry.uri) : 'application/octet-stream',
|
|
192
|
-
}));
|
|
193
|
-
assets.track(blob);
|
|
194
|
-
blobs.set(entry.uri, blob);
|
|
195
|
-
const decoded = decodeLocalPath(entry.uri);
|
|
196
|
-
if (decoded !== entry.uri) blobs.set(decoded, blob);
|
|
197
|
-
} else if (entry.kind === 'image') {
|
|
198
|
-
onWarn(`[dsh-3d-viewer] Could not read glTF image "${entry.uri}"; using a white texture.`);
|
|
199
|
-
blobs.set(entry.uri, WHITE_PIXEL);
|
|
200
|
-
} else {
|
|
201
|
-
onWarn(`[dsh-3d-viewer] Could not read glTF buffer "${entry.uri}".`);
|
|
202
|
-
}
|
|
203
|
-
} catch (error) {
|
|
204
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
205
|
-
if (entry.kind === 'image') {
|
|
206
|
-
onWarn(`[dsh-3d-viewer] Could not read glTF image "${entry.uri}"; using a white texture.`);
|
|
207
|
-
blobs.set(entry.uri, WHITE_PIXEL);
|
|
208
|
-
} else {
|
|
209
|
-
onWarn(`[dsh-3d-viewer] Could not read glTF buffer "${entry.uri}": ${String(error)}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const manager = new LoadingManager();
|
|
216
|
-
manager.setURLModifier((url) => blobs.get(url) ?? url);
|
|
217
|
-
|
|
218
|
-
const gltf = await new Promise((resolve, reject) => {
|
|
219
|
-
const loader = new GLTFLoader(manager);
|
|
220
|
-
let settled = false;
|
|
221
|
-
const fail = (error) => {
|
|
222
|
-
if (settled) return;
|
|
223
|
-
settled = true;
|
|
224
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
loader.parse(toArrayBuffer(data), '', (result) => {
|
|
229
|
-
if (settled) return;
|
|
230
|
-
settled = true;
|
|
231
|
-
if (options.signal?.aborted === true) {
|
|
232
|
-
disposeObject(result.scene);
|
|
233
|
-
reject(abortError());
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
resolve(result);
|
|
237
|
-
}, fail);
|
|
238
|
-
} catch (error) {
|
|
239
|
-
fail(error);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const object = gltf.scene;
|
|
244
|
-
object.animations = Array.isArray(gltf.animations) ? gltf.animations : [];
|
|
245
|
-
hideBakedShadowPlanes(object);
|
|
246
|
-
return { object, assets };
|
|
247
|
-
} catch (error) {
|
|
248
|
-
assets.dispose();
|
|
249
|
-
throw error;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
|
|
2
|
-
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
|
3
|
-
import { abortError, decodeText, toArrayBuffer, warn } from '../bytes.js';
|
|
4
|
-
import { disposeObject, parseFbxBytes, parseStlBytes, stripEmbeddedSceneNodes } from '../model.js';
|
|
5
|
-
import { FBXLoader } from '../vendor/FBXLoaderPbr.js';
|
|
6
|
-
import { loadGltfModel } from './gltf.js';
|
|
7
|
-
import { createWorkspaceAssetLoader, readWithBasenameFallback } from './workspace.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Read every `mtllib` filename in an OBJ document.
|
|
11
|
-
*
|
|
12
|
-
* Scanning the text directly avoids splitting a multi-megabyte OBJ into one
|
|
13
|
-
* string per line just to find a handful of material-library declarations.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} text OBJ text.
|
|
16
|
-
* @returns {string[]} Referenced MTL paths in document order.
|
|
17
|
-
*/
|
|
18
|
-
function materialLibraries(text) {
|
|
19
|
-
const libraries = [];
|
|
20
|
-
for (const match of text.matchAll(/^[^\S\r\n]*mtllib[^\S\r\n]+(.+?)[^\S\r\n]*$/gimu)) {
|
|
21
|
-
libraries.push(match[1].trim());
|
|
22
|
-
}
|
|
23
|
-
return libraries;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Load the first readable MTL library and its textures.
|
|
28
|
-
*
|
|
29
|
-
* @param {string} text OBJ text.
|
|
30
|
-
* @param {{
|
|
31
|
-
* readWorkspaceFile: Function,
|
|
32
|
-
* resourceAddress: string,
|
|
33
|
-
* signal?: AbortSignal,
|
|
34
|
-
* onWarn: (message: string) => void,
|
|
35
|
-
* }} options Workspace reader and diagnostics.
|
|
36
|
-
* @returns {Promise<{ materials: object, assets: object } | null>} Materials and their asset loader.
|
|
37
|
-
*/
|
|
38
|
-
async function loadObjMaterials(text, options) {
|
|
39
|
-
const libraries = materialLibraries(text);
|
|
40
|
-
if (libraries.length === 0) return null;
|
|
41
|
-
|
|
42
|
-
const readLibrary = (path) => options.readWorkspaceFile(options.resourceAddress, path, {
|
|
43
|
-
signal: options.signal,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
for (const library of libraries) {
|
|
47
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
48
|
-
|
|
49
|
-
let result;
|
|
50
|
-
try {
|
|
51
|
-
result = await readWithBasenameFallback(readLibrary, library);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
54
|
-
options.onWarn(`[dsh-3d-viewer] Could not read MTL "${library}": ${String(error)}`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (result === undefined || result.ok !== true) {
|
|
58
|
-
options.onWarn(`[dsh-3d-viewer] Could not read MTL "${library}": ${result?.error?.message ?? 'unknown error'}`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let assets = null;
|
|
63
|
-
try {
|
|
64
|
-
const materialsText = decodeText(result.value.data);
|
|
65
|
-
assets = createWorkspaceAssetLoader({
|
|
66
|
-
readFile: (path) => options.readWorkspaceFile(options.resourceAddress, path, {
|
|
67
|
-
baseFile: result.value.absolutePath,
|
|
68
|
-
signal: options.signal,
|
|
69
|
-
}),
|
|
70
|
-
signal: options.signal,
|
|
71
|
-
onWarn: options.onWarn,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const loader = new MTLLoader(assets.manager);
|
|
75
|
-
const materials = loader.parse(materialsText, '');
|
|
76
|
-
materials.preload();
|
|
77
|
-
await assets.settle();
|
|
78
|
-
|
|
79
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
80
|
-
return { materials, assets };
|
|
81
|
-
} catch (error) {
|
|
82
|
-
assets?.dispose();
|
|
83
|
-
options.onWarn(`[dsh-3d-viewer] Could not parse MTL "${library}": ${String(error)}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Load an OBJ scene, including its MTL material library and image textures
|
|
92
|
-
* when the workspace reader is available.
|
|
93
|
-
*
|
|
94
|
-
* @param {Uint8Array | ArrayBuffer | string} data OBJ bytes or text.
|
|
95
|
-
* @param {{
|
|
96
|
-
* readWorkspaceFile?: Function,
|
|
97
|
-
* resourceAddress?: string,
|
|
98
|
-
* signal?: AbortSignal,
|
|
99
|
-
* onWarn?: (message: string) => void,
|
|
100
|
-
* }} [options] Workspace reader, address, and cancellation.
|
|
101
|
-
* @returns {Promise<{ object: import('three').Object3D, assets: object | null }>} Scene and asset controls.
|
|
102
|
-
*/
|
|
103
|
-
export async function loadObjModel(data, options = {}) {
|
|
104
|
-
const text = decodeText(data);
|
|
105
|
-
const onWarn = options.onWarn ?? warn;
|
|
106
|
-
let materials = null;
|
|
107
|
-
let assets = null;
|
|
108
|
-
|
|
109
|
-
if (typeof options.readWorkspaceFile === 'function' && typeof options.resourceAddress === 'string') {
|
|
110
|
-
const loaded = await loadObjMaterials(text, {
|
|
111
|
-
readWorkspaceFile: options.readWorkspaceFile,
|
|
112
|
-
resourceAddress: options.resourceAddress,
|
|
113
|
-
signal: options.signal,
|
|
114
|
-
onWarn,
|
|
115
|
-
});
|
|
116
|
-
materials = loaded?.materials ?? null;
|
|
117
|
-
assets = loaded?.assets ?? null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
122
|
-
const loader = new OBJLoader();
|
|
123
|
-
if (materials !== null) loader.setMaterials(materials);
|
|
124
|
-
const object = loader.parse(text);
|
|
125
|
-
return { object, assets };
|
|
126
|
-
} catch (error) {
|
|
127
|
-
assets?.dispose();
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Load an FBX scene. Embedded textures keep using the loader's blob/data
|
|
134
|
-
* paths; external texture references are resolved through the workspace.
|
|
135
|
-
*
|
|
136
|
-
* @param {Uint8Array | ArrayBuffer} data FBX bytes.
|
|
137
|
-
* @param {{
|
|
138
|
-
* readWorkspaceFile?: Function,
|
|
139
|
-
* resourceAddress?: string,
|
|
140
|
-
* signal?: AbortSignal,
|
|
141
|
-
* onWarn?: (message: string) => void,
|
|
142
|
-
* }} [options] Workspace reader, address, and cancellation.
|
|
143
|
-
* @returns {Promise<{ object: import('three').Object3D, assets: object | null }>} Scene and asset controls.
|
|
144
|
-
*/
|
|
145
|
-
export async function loadFbxModel(data, options = {}) {
|
|
146
|
-
const onWarn = options.onWarn ?? warn;
|
|
147
|
-
|
|
148
|
-
if (typeof options.readWorkspaceFile !== 'function' || typeof options.resourceAddress !== 'string') {
|
|
149
|
-
return { object: parseFbxBytes(data), assets: null };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const assets = createWorkspaceAssetLoader({
|
|
153
|
-
readFile: (path) => options.readWorkspaceFile(options.resourceAddress, path, {
|
|
154
|
-
signal: options.signal,
|
|
155
|
-
}),
|
|
156
|
-
signal: options.signal,
|
|
157
|
-
onWarn,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const loader = new FBXLoader(assets.manager);
|
|
162
|
-
loader.manager.onError = (url) => {
|
|
163
|
-
onWarn(`[dsh-3d-viewer] Could not load a referenced FBX resource: ${url}`);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const object = loader.parse(toArrayBuffer(data), '');
|
|
167
|
-
await assets.settle();
|
|
168
|
-
|
|
169
|
-
if (options.signal?.aborted === true) {
|
|
170
|
-
assets.dispose();
|
|
171
|
-
disposeObject(object);
|
|
172
|
-
throw abortError();
|
|
173
|
-
}
|
|
174
|
-
return { object, assets };
|
|
175
|
-
} catch (error) {
|
|
176
|
-
assets.dispose();
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Load an STL model.
|
|
183
|
-
*
|
|
184
|
-
* @param {Uint8Array | ArrayBuffer | string} data STL bytes or text.
|
|
185
|
-
* @param {{ signal?: AbortSignal }} [options] Cancellation.
|
|
186
|
-
* @returns {Promise<{ object: import('three').Mesh, assets: null }>} Parsed mesh.
|
|
187
|
-
*/
|
|
188
|
-
async function loadStlModel(data, options = {}) {
|
|
189
|
-
if (options.signal?.aborted === true) throw abortError();
|
|
190
|
-
const object = parseStlBytes(data);
|
|
191
|
-
if (options.signal?.aborted === true) {
|
|
192
|
-
disposeObject(object);
|
|
193
|
-
throw abortError();
|
|
194
|
-
}
|
|
195
|
-
return { object, assets: null };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Per-format loaders. glTF and GLB share one implementation because GLB is
|
|
200
|
-
* only a container for the same scene description.
|
|
201
|
-
*/
|
|
202
|
-
const LOADERS = Object.freeze({
|
|
203
|
-
obj: loadObjModel,
|
|
204
|
-
fbx: loadFbxModel,
|
|
205
|
-
gltf: loadGltfModel,
|
|
206
|
-
glb: loadGltfModel,
|
|
207
|
-
stl: loadStlModel,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Load one registered model format.
|
|
212
|
-
*
|
|
213
|
-
* @param {Uint8Array | ArrayBuffer | string} data Complete file bytes.
|
|
214
|
-
* @param {'fbx' | 'gltf' | 'glb' | 'obj' | 'stl'} format Document format.
|
|
215
|
-
* @param {{
|
|
216
|
-
* readWorkspaceFile?: Function,
|
|
217
|
-
* resourceAddress?: string,
|
|
218
|
-
* signal?: AbortSignal,
|
|
219
|
-
* onWarn?: (message: string) => void,
|
|
220
|
-
* }} [options] Workspace reader, address, and cancellation.
|
|
221
|
-
* @returns {Promise<{ object: import('three').Object3D, assets: object | null }>} Scene and asset controls.
|
|
222
|
-
*/
|
|
223
|
-
export async function loadModelBytes(data, format, options = {}) {
|
|
224
|
-
if (!Object.hasOwn(LOADERS, format)) {
|
|
225
|
-
throw new TypeError(`Unsupported 3D model format: ${String(format)}`);
|
|
226
|
-
}
|
|
227
|
-
const loaded = await LOADERS[format](data, options);
|
|
228
|
-
// The preview provides its own lighting and camera; never render the ones
|
|
229
|
-
// baked into the source file (FBX and glTF can both carry them).
|
|
230
|
-
stripEmbeddedSceneNodes(loaded.object);
|
|
231
|
-
return loaded;
|
|
232
|
-
}
|