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,187 +0,0 @@
1
- import {
2
- DataTexture,
3
- EquirectangularReflectionMapping,
4
- PMREMGenerator,
5
- RGBAFormat,
6
- } from 'three';
7
- import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
8
- import { decodeBase64 } from '../bytes.js';
9
- import { HDRI_DATA_URLS } from '../generated/hdri-data.js';
10
-
11
- /** Environment used when an unknown id is requested. */
12
- export const DEFAULT_ENVIRONMENT = 'nature';
13
-
14
- /** Per-environment IBL strength. */
15
- export const ENVIRONMENT_INTENSITIES = Object.freeze({
16
- nature: 1.0,
17
- city: 1.0,
18
- indoor: 0.8,
19
- studio: 0.9,
20
- });
21
-
22
- /**
23
- * Resolve an environment id against the embedded HDRI map.
24
- *
25
- * @param {unknown} id Requested environment id.
26
- * @param {Record<string, string>} dataUrls Embedded data URLs.
27
- * @returns {string} A known environment id.
28
- */
29
- export function normalizeEnvironmentId(id, dataUrls = HDRI_DATA_URLS) {
30
- return typeof id === 'string' && Object.hasOwn(dataUrls, id)
31
- ? id
32
- : DEFAULT_ENVIRONMENT;
33
- }
34
-
35
- /**
36
- * Decode the base64 payload of an embedded data URL.
37
- *
38
- * @param {unknown} dataUrl Embedded data URL.
39
- * @returns {ArrayBuffer | null} Decoded bytes, or null when the input is not a data URL.
40
- */
41
- export function dataUrlToArrayBuffer(dataUrl) {
42
- if (typeof dataUrl !== 'string') return null;
43
- const comma = dataUrl.indexOf(',');
44
- if (comma < 0) return null;
45
-
46
- try {
47
- const bytes = decodeBase64(dataUrl.slice(comma + 1));
48
- // Reuse the decoder's buffer when it covers the whole payload; copying
49
- // multi-megabyte HDRIs is otherwise pure waste.
50
- return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
51
- ? bytes.buffer
52
- : bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- /**
59
- * Turn raw RGBE data into an equirectangular, non-mipmapped DataTexture.
60
- *
61
- * @param {ArrayBuffer} buffer Raw HDR bytes.
62
- * @param {HDRLoader} loader Reusable HDR parser.
63
- * @returns {DataTexture} Linear RGBE texture.
64
- */
65
- export function createEquirectangularTexture(buffer, loader = new HDRLoader()) {
66
- const texData = loader.parse(buffer);
67
- const texture = new DataTexture(
68
- texData.data,
69
- texData.width,
70
- texData.height,
71
- RGBAFormat,
72
- texData.type,
73
- );
74
- texture.colorSpace = texData.colorSpace;
75
- texture.minFilter = texData.minFilter;
76
- texture.magFilter = texData.magFilter;
77
- texture.generateMipmaps = texData.generateMipmaps;
78
- texture.flipY = texData.flipY;
79
- texture.unpackAlignment = 1;
80
- texture.mapping = EquirectangularReflectionMapping;
81
- texture.needsUpdate = true;
82
- return texture;
83
- }
84
-
85
- /**
86
- * Lazily build and cache PMREM environment maps for the embedded HDRIs.
87
- *
88
- * Only the environments the user selects are decoded; the first environment
89
- * is built on first use, and repeated selections reuse the cached target.
90
- *
91
- * @param {{
92
- * renderer: import('three').WebGLRenderer,
93
- * scene: import('three').Scene,
94
- * dataUrls?: Record<string, string>,
95
- * intensities?: Record<string, number>,
96
- * }} options Renderer, target scene, embedded HDRIs, and per-environment IBL strengths.
97
- * @returns {{
98
- * set: (id: unknown) => boolean,
99
- * dispose: () => void,
100
- * readonly activeId: string | null,
101
- * }} Environment controls.
102
- */
103
- export function createEnvironment({
104
- renderer,
105
- scene,
106
- dataUrls = HDRI_DATA_URLS,
107
- intensities = ENVIRONMENT_INTENSITIES,
108
- }) {
109
- const targets = new Map();
110
- const loader = new HDRLoader();
111
- let pmremGenerator = null;
112
- let activeId = null;
113
- let disposed = false;
114
-
115
- function getPmremGenerator() {
116
- if (pmremGenerator === null) pmremGenerator = new PMREMGenerator(renderer);
117
- return pmremGenerator;
118
- }
119
-
120
- function buildTarget(id) {
121
- const dataUrl = dataUrls[id];
122
- if (typeof dataUrl !== 'string') return null;
123
-
124
- const buffer = dataUrlToArrayBuffer(dataUrl);
125
- if (buffer === null) return null;
126
-
127
- const texture = createEquirectangularTexture(buffer, loader);
128
- try {
129
- return getPmremGenerator().fromEquirectangular(texture);
130
- } finally {
131
- texture.dispose();
132
- }
133
- }
134
-
135
- function getTarget(id) {
136
- const cached = targets.get(id);
137
- if (cached !== undefined) return cached;
138
-
139
- let target = null;
140
- try {
141
- target = buildTarget(id);
142
- } catch (error) {
143
- console.warn(`[dsh-3d-viewer] Could not build the "${id}" environment.`, error);
144
- }
145
-
146
- if (target === null && id !== DEFAULT_ENVIRONMENT) {
147
- target = getTarget(DEFAULT_ENVIRONMENT);
148
- }
149
- if (target !== null) targets.set(id, target);
150
- return target;
151
- }
152
-
153
- function set(id) {
154
- if (disposed) return false;
155
-
156
- const next = normalizeEnvironmentId(id, dataUrls);
157
- if (next === activeId) return false;
158
-
159
- const target = getTarget(next);
160
- if (target === null) return false;
161
-
162
- scene.environment = target.texture;
163
- scene.environmentIntensity = intensities[next] ?? intensities[DEFAULT_ENVIRONMENT] ?? 1.0;
164
- activeId = next;
165
- return true;
166
- }
167
-
168
- function dispose() {
169
- if (disposed) return;
170
- disposed = true;
171
- activeId = null;
172
- scene.environment = null;
173
-
174
- for (const target of new Set(targets.values())) target.dispose();
175
- targets.clear();
176
- pmremGenerator?.dispose();
177
- pmremGenerator = null;
178
- }
179
-
180
- return {
181
- set,
182
- dispose,
183
- get activeId() {
184
- return activeId;
185
- },
186
- };
187
- }
@@ -1,286 +0,0 @@
1
- import {
2
- Color,
3
- DataTexture,
4
- DoubleSide,
5
- LinearFilter,
6
- LinearMipmapLinearFilter,
7
- Mesh,
8
- NoColorSpace,
9
- PlaneGeometry,
10
- RGBAFormat,
11
- RepeatWrapping,
12
- ShaderMaterial,
13
- ShadowMaterial,
14
- UnsignedByteType,
15
- Vector4,
16
- } from 'three';
17
-
18
- const GRID_TEXTURE_SIZE = 512;
19
- const GRID_TEXTURE_CELLS = 5;
20
- const GRID_MINOR_HALF_WIDTH_RATIO = 0.016;
21
- const GRID_MAJOR_HALF_WIDTH_RATIO = 0.022;
22
- const GRID_TEXTURE_AA = 0.75;
23
-
24
- const GRID_VERTEX_SHADER = `
25
- varying vec3 vWorldPosition;
26
-
27
- void main() {
28
- vec4 worldPosition = modelMatrix * vec4(position, 1.0);
29
- vWorldPosition = worldPosition.xyz;
30
- gl_Position = projectionMatrix * viewMatrix * worldPosition;
31
- }
32
- `;
33
-
34
- const GRID_FRAGMENT_SHADER = `
35
- uniform sampler2D uGridMap;
36
- uniform vec3 uColor;
37
- uniform float uGridScale;
38
- uniform float uMinorAlpha;
39
- uniform float uMajorAlpha;
40
- uniform float uFogStart;
41
- uniform float uFogEnd;
42
-
43
- varying vec3 vWorldPosition;
44
-
45
- void main() {
46
- // One normalized projection-depth factor drives fog color, transparency,
47
- // and the final disappearance together.
48
- float depthT = clamp(
49
- (gl_FragCoord.z - uFogStart) / max(uFogEnd - uFogStart, 1e-6),
50
- 0.0,
51
- 1.0
52
- );
53
- if (depthT >= 0.998) discard;
54
-
55
- float opacity = 1.0 - depthT;
56
- vec2 gridUv = vWorldPosition.xz * uGridScale;
57
- vec2 lines = texture2D(uGridMap, gridUv).rg;
58
- float alpha = max(lines.r * uMinorAlpha, lines.g * uMajorAlpha) * opacity;
59
- vec3 color = mix(uColor, vec3(1.0), depthT);
60
-
61
- if (alpha <= 0.002) discard;
62
- gl_FragColor = vec4(color, alpha);
63
- #include <colorspace_fragment>
64
- }
65
- `;
66
-
67
- const _depthProbe = new Vector4();
68
-
69
- /** Rasterized grid pixels, shared by every viewer on the page. */
70
- let gridPixelCache = null;
71
-
72
- /**
73
- * Pick a decimal grid step near one twelfth of the model's largest dimension.
74
- *
75
- * @param {number} size Largest model dimension.
76
- * @returns {number} A 1/2/5 x 10^n world-space spacing.
77
- */
78
- export function niceGridStep(size) {
79
- const raw = size / 12;
80
- if (!Number.isFinite(raw) || raw <= 0) return 1;
81
- const power = 10 ** Math.floor(Math.log10(raw));
82
- const normalized = raw / power;
83
- const multiplier = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
84
- return multiplier * power;
85
- }
86
-
87
- /**
88
- * Convert a camera-space distance into the projection-window depth used by the
89
- * grid shader. The value is independent of the viewport size.
90
- *
91
- * @param {import('three').PerspectiveCamera} camera Active camera.
92
- * @param {number} viewDistance Distance from the camera along -Z.
93
- * @returns {number} Depth in [0, 1].
94
- */
95
- function projectionWindowDepth(camera, viewDistance) {
96
- _depthProbe.set(0, 0, -viewDistance, 1).applyMatrix4(camera.projectionMatrix);
97
- return (_depthProbe.z / _depthProbe.w) * 0.5 + 0.5;
98
- }
99
-
100
- /**
101
- * Build the repeating 5x5-cell grid map used by the mipmapped grid shader.
102
- *
103
- * R stores minor-line coverage and G stores major-line coverage. The texture is
104
- * sampled with mipmaps and maximum anisotropy instead of screen-space
105
- * derivatives, so distant lines fade smoothly rather than breaking into dashes.
106
- *
107
- * The pixel data is deterministic, so it is rasterized once per page and
108
- * reused by every viewer; only the GPU texture object is per-renderer.
109
- *
110
- * @returns {Uint8Array} RGBA grid coverage pixels.
111
- */
112
- function gridPixelData() {
113
- if (gridPixelCache !== null) return gridPixelCache;
114
-
115
- const size = GRID_TEXTURE_SIZE;
116
- const cellSize = size / GRID_TEXTURE_CELLS;
117
- const minorHalfWidth = cellSize * GRID_MINOR_HALF_WIDTH_RATIO;
118
- const majorHalfWidth = cellSize * GRID_MAJOR_HALF_WIDTH_RATIO;
119
- const data = new Uint8Array(size * size * 4);
120
-
121
- const coverage = (distance, halfWidth) => {
122
- const inner = Math.max(halfWidth - GRID_TEXTURE_AA, 0);
123
- const outer = halfWidth + GRID_TEXTURE_AA;
124
- if (distance <= inner) return 1;
125
- if (distance >= outer) return 0;
126
- const t = (distance - inner) / (outer - inner);
127
- return 1 - t * t * (3 - 2 * t);
128
- };
129
-
130
- for (let y = 0; y < size; y += 1) {
131
- const yPos = y + 0.5;
132
- const minorY = yPos % cellSize;
133
- const minorDistanceY = Math.min(minorY, cellSize - minorY);
134
- const majorDistanceY = Math.min(yPos, size - yPos);
135
-
136
- for (let x = 0; x < size; x += 1) {
137
- const xPos = x + 0.5;
138
- const minorX = xPos % cellSize;
139
- const minorDistanceX = Math.min(minorX, cellSize - minorX);
140
- const majorDistanceX = Math.min(xPos, size - xPos);
141
-
142
- const minor = Math.max(
143
- coverage(minorDistanceX, minorHalfWidth),
144
- coverage(minorDistanceY, minorHalfWidth),
145
- );
146
- const major = Math.max(
147
- coverage(majorDistanceX, majorHalfWidth),
148
- coverage(majorDistanceY, majorHalfWidth),
149
- );
150
-
151
- const offset = (y * size + x) * 4;
152
- data[offset] = Math.round(minor * 255);
153
- data[offset + 1] = Math.round(major * 255);
154
- data[offset + 2] = 0;
155
- data[offset + 3] = 255;
156
- }
157
- }
158
-
159
- gridPixelCache = data;
160
- return data;
161
- }
162
-
163
- /**
164
- * Wrap the shared grid pixels in a mipmapped, repeating DataTexture.
165
- *
166
- * @param {number} anisotropy Maximum anisotropy supported by the renderer.
167
- * @returns {DataTexture} Repeating grid coverage texture.
168
- */
169
- function createGridTexture(anisotropy) {
170
- const size = GRID_TEXTURE_SIZE;
171
- const texture = new DataTexture(gridPixelData(), size, size, RGBAFormat, UnsignedByteType);
172
- texture.name = 'dsh3dv-grid';
173
- texture.colorSpace = NoColorSpace;
174
- texture.wrapS = RepeatWrapping;
175
- texture.wrapT = RepeatWrapping;
176
- texture.magFilter = LinearFilter;
177
- texture.minFilter = LinearMipmapLinearFilter;
178
- texture.generateMipmaps = true;
179
- texture.anisotropy = Math.max(1, Math.floor(anisotropy));
180
- texture.needsUpdate = true;
181
- return texture;
182
- }
183
-
184
- /**
185
- * Build the procedural infinite grid and the transparent shadow receiver.
186
- *
187
- * @param {{
188
- * scene: import('three').Scene,
189
- * stats: ReturnType<import('../model.js').collectModelStats>,
190
- * maxDimension: number,
191
- * maxAnisotropy?: number,
192
- * }} options Target scene, model bounds, and renderer anisotropy support.
193
- * @returns {{
194
- * update: (camera: import('three').PerspectiveCamera, target: import('three').Vector3, distance: number) => void,
195
- * setGridVisible: (visible: boolean) => boolean,
196
- * setShadowVisible: (visible: boolean) => boolean,
197
- * dispose: () => void,
198
- * }} Ground controls.
199
- */
200
- export function createGround({ scene, stats, maxDimension, maxAnisotropy = 1 }) {
201
- const floorY = Number.isFinite(stats.box.min.y)
202
- ? stats.box.min.y - maxDimension * 0.003
203
- : -maxDimension * 0.003;
204
- const gridCell = niceGridStep(maxDimension);
205
- const gridTexture = createGridTexture(maxAnisotropy);
206
-
207
- const gridGeometry = new PlaneGeometry(maxDimension * 10000, maxDimension * 10000);
208
- gridGeometry.rotateX(-Math.PI / 2);
209
- const gridUniforms = {
210
- uGridMap: { value: gridTexture },
211
- uColor: { value: new Color(0x7b8087) },
212
- uGridScale: { value: 1 / (gridCell * GRID_TEXTURE_CELLS) },
213
- uMinorAlpha: { value: 0.25 },
214
- uMajorAlpha: { value: 0.5 },
215
- uFogStart: { value: 0.9 },
216
- uFogEnd: { value: 0.99 },
217
- };
218
- const gridMaterial = new ShaderMaterial({
219
- uniforms: gridUniforms,
220
- vertexShader: GRID_VERTEX_SHADER,
221
- fragmentShader: GRID_FRAGMENT_SHADER,
222
- transparent: true,
223
- depthWrite: false,
224
- side: DoubleSide,
225
- toneMapped: false,
226
- });
227
- const grid = new Mesh(gridGeometry, gridMaterial);
228
- grid.position.set(stats.center.x, floorY, stats.center.z);
229
- grid.renderOrder = 0;
230
- scene.add(grid);
231
-
232
- const shadowGeometry = new PlaneGeometry(maxDimension * 20, maxDimension * 20);
233
- shadowGeometry.rotateX(-Math.PI / 2);
234
- const shadowMaterial = new ShadowMaterial({
235
- color: 0x000000,
236
- opacity: 0.24,
237
- transparent: true,
238
- depthWrite: false,
239
- });
240
- const shadowPlane = new Mesh(shadowGeometry, shadowMaterial);
241
- shadowPlane.position.set(stats.center.x, floorY + maxDimension * 0.0002, stats.center.z);
242
- shadowPlane.receiveShadow = true;
243
- shadowPlane.renderOrder = 1;
244
- scene.add(shadowPlane);
245
-
246
- function update(camera, target, distance) {
247
- grid.position.x = target.x;
248
- grid.position.z = target.z;
249
- grid.updateMatrixWorld();
250
-
251
- const fogEndWorld = Math.min(distance * 6, maxDimension * 4000);
252
- const fogStartWorld = Math.min(distance * 1.8, fogEndWorld * 0.3);
253
- gridUniforms.uFogStart.value = projectionWindowDepth(camera, fogStartWorld);
254
- gridUniforms.uFogEnd.value = projectionWindowDepth(camera, fogEndWorld);
255
- }
256
-
257
- function setGridVisible(visible) {
258
- const next = Boolean(visible);
259
- if (grid.visible === next) return false;
260
- grid.visible = next;
261
- return true;
262
- }
263
-
264
- function setShadowVisible(visible) {
265
- const next = Boolean(visible);
266
- if (shadowPlane.visible === next) return false;
267
- shadowPlane.visible = next;
268
- return true;
269
- }
270
-
271
- function dispose() {
272
- scene.remove(grid, shadowPlane);
273
- gridGeometry.dispose();
274
- gridTexture.dispose();
275
- gridMaterial.dispose();
276
- shadowGeometry.dispose();
277
- shadowMaterial.dispose();
278
- }
279
-
280
- return {
281
- update,
282
- setGridVisible,
283
- setShadowVisible,
284
- dispose,
285
- };
286
- }
@@ -1,41 +0,0 @@
1
- import { DirectionalLight, Vector3 } from 'three';
2
-
3
- const KEY_OFFSET = new Vector3(-3.2, 5.0, 3.0);
4
-
5
- /**
6
- * Add the shadow-casting key light used by the preview.
7
- *
8
- * @param {{
9
- * scene: import('three').Scene,
10
- * center: import('three').Vector3,
11
- * maxDimension: number,
12
- * }} options Target scene and model bounds.
13
- * @returns {{ dispose: () => void }} Lighting controls.
14
- */
15
- export function createLighting({ scene, center, maxDimension }) {
16
- const key = new DirectionalLight(0xffffff, 5.0);
17
-
18
- key.position.copy(center).addScaledVector(KEY_OFFSET, maxDimension);
19
- key.target.position.copy(center);
20
- key.castShadow = true;
21
- key.shadow.mapSize.set(2048, 2048);
22
- key.shadow.camera.left = -maxDimension * 2;
23
- key.shadow.camera.right = maxDimension * 2;
24
- key.shadow.camera.top = maxDimension * 2;
25
- key.shadow.camera.bottom = -maxDimension * 2;
26
- key.shadow.camera.near = maxDimension * 2;
27
- key.shadow.camera.far = maxDimension * 12;
28
- key.shadow.bias = -0.0001;
29
- key.shadow.normalBias = maxDimension * 0.002;
30
- key.shadow.radius = 2;
31
- key.shadow.camera.updateProjectionMatrix();
32
-
33
- scene.add(key, key.target);
34
-
35
- function dispose() {
36
- scene.remove(key, key.target);
37
- key.dispose();
38
- }
39
-
40
- return { dispose };
41
- }