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.
Files changed (47) hide show
  1. package/README.md +180 -107
  2. package/README.zh.md +180 -107
  3. package/THIRD_PARTY_NOTICES.md +54 -52
  4. package/assets/hdri/LICENSE.md +17 -16
  5. package/dist/client.fbx.js +14 -0
  6. package/dist/client.gltf.js +14 -0
  7. package/dist/client.js +4472 -0
  8. package/dist/client.obj.js +19 -0
  9. package/dist/client.stl.js +14 -0
  10. package/dist/client.tex-exr.js +14 -0
  11. package/dist/client.tex-hdr.js +18 -0
  12. package/dist/client.tex-tga.js +14 -0
  13. package/index.js +167 -7
  14. package/package.json +72 -48
  15. package/assets/preview.png +0 -0
  16. package/build.mjs +0 -49
  17. package/client.js +0 -4462
  18. package/scripts/embed-hdri.mjs +0 -70
  19. package/src/bytes.js +0 -158
  20. package/src/client.js +0 -57
  21. package/src/formats.js +0 -100
  22. package/src/generated/hdri-data.js +0 -8
  23. package/src/loaders/gltf.js +0 -251
  24. package/src/loaders/model-loader.js +0 -232
  25. package/src/loaders/workspace.js +0 -295
  26. package/src/model.js +0 -417
  27. package/src/scene/animation.js +0 -149
  28. package/src/scene/blue-noise.js +0 -48
  29. package/src/scene/camera.js +0 -83
  30. package/src/scene/createSceneView.js +0 -375
  31. package/src/scene/environment.js +0 -187
  32. package/src/scene/ground.js +0 -286
  33. package/src/scene/lighting.js +0 -41
  34. package/src/scene/materials.js +0 -263
  35. package/src/scene/postprocessing.js +0 -155
  36. package/src/scene/render-loop.js +0 -98
  37. package/src/scene/viewer-error.js +0 -16
  38. package/src/ui/AnimationBar.js +0 -95
  39. package/src/ui/ModelViewer.js +0 -278
  40. package/src/ui/ViewerSurface.js +0 -87
  41. package/src/ui/chrome.js +0 -220
  42. package/src/ui/dictionaries.js +0 -99
  43. package/src/ui/icons.js +0 -88
  44. package/src/ui/options.js +0 -23
  45. package/src/ui/primitives.js +0 -88
  46. package/src/ui/styles.js +0 -89
  47. package/src/vendor/FBXLoaderPbr.js +0 -4772
@@ -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
- }