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/scene/animation.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { AnimationMixer, LoopRepeat } from 'three';
|
|
2
|
-
|
|
3
|
-
const NOTIFY_INTERVAL_MS = 33;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Drive the FBX animation clips without coupling playback state to React.
|
|
7
|
-
*
|
|
8
|
-
* Only the first clip is prepared at construction; it starts paused so the
|
|
9
|
-
* caller decides when playback begins. Progress notifications are throttled
|
|
10
|
-
* to roughly 30 Hz and are emitted only while the mixer is actually running.
|
|
11
|
-
*
|
|
12
|
-
* @param {import('three').Object3D} object Parsed FBX graph.
|
|
13
|
-
* @param {{ onTick?: (state: { time: number, duration: number, index: number, playing: boolean }) => void }} [options] Progress callback.
|
|
14
|
-
* @returns {{
|
|
15
|
-
* animations: Array<{ name: string, duration: number }>,
|
|
16
|
-
* setPlaying: (enabled: boolean) => boolean,
|
|
17
|
-
* setClip: (index: number) => boolean,
|
|
18
|
-
* seek: (seconds: number) => boolean,
|
|
19
|
-
* update: (delta: number, timestamp: number) => void,
|
|
20
|
-
* snapshot: () => { time: number, duration: number, index: number, playing: boolean },
|
|
21
|
-
* isPlaying: () => boolean,
|
|
22
|
-
* dispose: () => void,
|
|
23
|
-
* }} Animation controls.
|
|
24
|
-
*/
|
|
25
|
-
export function createAnimationController(object, { onTick } = {}) {
|
|
26
|
-
const clips = Array.isArray(object.animations)
|
|
27
|
-
? object.animations.filter((clip) => Number.isFinite(clip.duration)
|
|
28
|
-
&& clip.duration > 0
|
|
29
|
-
&& clip.tracks.length > 0)
|
|
30
|
-
: [];
|
|
31
|
-
const animations = clips.map((clip) => ({ name: clip.name, duration: clip.duration }));
|
|
32
|
-
const mixer = clips.length > 0 ? new AnimationMixer(object) : null;
|
|
33
|
-
const actions = [];
|
|
34
|
-
let index = 0;
|
|
35
|
-
let playing = false;
|
|
36
|
-
let lastNotify = -Infinity;
|
|
37
|
-
let disposed = false;
|
|
38
|
-
|
|
39
|
-
if (mixer !== null) {
|
|
40
|
-
for (const clip of clips) {
|
|
41
|
-
const action = mixer.clipAction(clip);
|
|
42
|
-
action.setLoop(LoopRepeat, Infinity);
|
|
43
|
-
action.clampWhenFinished = false;
|
|
44
|
-
actions.push(action);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const first = actions[0];
|
|
48
|
-
first.reset().play();
|
|
49
|
-
first.paused = true;
|
|
50
|
-
mixer.update(0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function currentAction() {
|
|
54
|
-
return actions[index];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function currentDuration() {
|
|
58
|
-
const action = currentAction();
|
|
59
|
-
return action === undefined ? 0 : action.getClip().duration;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function currentTime() {
|
|
63
|
-
const action = currentAction();
|
|
64
|
-
return action === undefined ? 0 : action.time;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function snapshot() {
|
|
68
|
-
return { time: currentTime(), duration: currentDuration(), index, playing };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function emit() {
|
|
72
|
-
if (disposed || typeof onTick !== 'function') return;
|
|
73
|
-
onTick(snapshot());
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function update(delta, timestamp) {
|
|
77
|
-
if (disposed || mixer === null || !playing) return;
|
|
78
|
-
|
|
79
|
-
mixer.update(delta);
|
|
80
|
-
if (timestamp - lastNotify >= NOTIFY_INTERVAL_MS) {
|
|
81
|
-
lastNotify = timestamp;
|
|
82
|
-
emit();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function setPlaying(enabled) {
|
|
87
|
-
if (disposed || mixer === null) return false;
|
|
88
|
-
|
|
89
|
-
const next = Boolean(enabled);
|
|
90
|
-
if (next === playing) return false;
|
|
91
|
-
|
|
92
|
-
playing = next;
|
|
93
|
-
const action = currentAction();
|
|
94
|
-
if (action !== undefined) action.paused = !playing;
|
|
95
|
-
lastNotify = -Infinity;
|
|
96
|
-
emit();
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function setClip(nextIndex) {
|
|
101
|
-
if (disposed || mixer === null) return false;
|
|
102
|
-
if (!Number.isInteger(nextIndex) || nextIndex < 0 || nextIndex >= actions.length) return false;
|
|
103
|
-
|
|
104
|
-
index = nextIndex;
|
|
105
|
-
for (const action of actions) action.stop();
|
|
106
|
-
const action = actions[index];
|
|
107
|
-
action.reset().play();
|
|
108
|
-
action.paused = !playing;
|
|
109
|
-
mixer.update(0);
|
|
110
|
-
lastNotify = -Infinity;
|
|
111
|
-
emit();
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function seek(seconds) {
|
|
116
|
-
if (disposed || mixer === null) return false;
|
|
117
|
-
|
|
118
|
-
const action = currentAction();
|
|
119
|
-
if (action === undefined) return false;
|
|
120
|
-
|
|
121
|
-
const duration = action.getClip().duration;
|
|
122
|
-
const next = Number(seconds);
|
|
123
|
-
action.time = Math.max(0, Math.min(duration, Number.isFinite(next) ? next : 0));
|
|
124
|
-
mixer.update(0);
|
|
125
|
-
lastNotify = -Infinity;
|
|
126
|
-
emit();
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function dispose() {
|
|
131
|
-
if (disposed) return;
|
|
132
|
-
disposed = true;
|
|
133
|
-
if (mixer !== null) {
|
|
134
|
-
mixer.stopAllAction();
|
|
135
|
-
mixer.uncacheRoot(object);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
animations,
|
|
141
|
-
setPlaying,
|
|
142
|
-
setClip,
|
|
143
|
-
seek,
|
|
144
|
-
update,
|
|
145
|
-
snapshot,
|
|
146
|
-
isPlaying: () => playing,
|
|
147
|
-
dispose,
|
|
148
|
-
};
|
|
149
|
-
}
|
package/src/scene/blue-noise.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DataTexture,
|
|
3
|
-
NearestFilter,
|
|
4
|
-
NoColorSpace,
|
|
5
|
-
RedFormat,
|
|
6
|
-
RepeatWrapping,
|
|
7
|
-
UnsignedByteType,
|
|
8
|
-
} from 'three';
|
|
9
|
-
import { decodeBase64 } from '../bytes.js';
|
|
10
|
-
|
|
11
|
-
/** Edge length of the embedded 8-bit blue-noise mask. */
|
|
12
|
-
export const BLUE_NOISE_SIZE = 64;
|
|
13
|
-
|
|
14
|
-
// Generated offline from an isotropic high-pass random field, then rank-normalized
|
|
15
|
-
// to a uniform 0..1 distribution. The mask has most of its energy above the
|
|
16
|
-
// low-frequency band, which keeps dithering visually even without a visible
|
|
17
|
-
// repeating pattern.
|
|
18
|
-
const BLUE_NOISE_BASE64 = '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';
|
|
19
|
-
|
|
20
|
-
/** Decoded mask, shared by every viewer on the page. */
|
|
21
|
-
let blueNoiseBytes = null;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Create the repeating blue-noise texture used by the post-processing dither.
|
|
25
|
-
*
|
|
26
|
-
* The mask is decoded once and wrapped in a per-composer GPU texture.
|
|
27
|
-
*
|
|
28
|
-
* @returns {import('three').DataTexture} Nearest-filtered blue-noise mask.
|
|
29
|
-
*/
|
|
30
|
-
export function createBlueNoiseTexture() {
|
|
31
|
-
blueNoiseBytes ??= decodeBase64(BLUE_NOISE_BASE64);
|
|
32
|
-
const texture = new DataTexture(
|
|
33
|
-
blueNoiseBytes,
|
|
34
|
-
BLUE_NOISE_SIZE,
|
|
35
|
-
BLUE_NOISE_SIZE,
|
|
36
|
-
RedFormat,
|
|
37
|
-
UnsignedByteType,
|
|
38
|
-
);
|
|
39
|
-
texture.name = 'dsh3dv-blue-noise';
|
|
40
|
-
texture.colorSpace = NoColorSpace;
|
|
41
|
-
texture.wrapS = RepeatWrapping;
|
|
42
|
-
texture.wrapT = RepeatWrapping;
|
|
43
|
-
texture.magFilter = NearestFilter;
|
|
44
|
-
texture.minFilter = NearestFilter;
|
|
45
|
-
texture.generateMipmaps = false;
|
|
46
|
-
texture.needsUpdate = true;
|
|
47
|
-
return texture;
|
|
48
|
-
}
|
package/src/scene/camera.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { Spherical, Vector3 } from 'three';
|
|
2
|
-
|
|
3
|
-
const FIT_DIRECTION = new Vector3(1, 0.55, 1.3).normalize();
|
|
4
|
-
const KEY_STEP = Math.PI / 36;
|
|
5
|
-
const _offset = new Vector3();
|
|
6
|
-
const _spherical = new Spherical();
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Keep the near/far planes proportional to the current orbit distance so the
|
|
10
|
-
* grid's projection-depth fog stays stable across zoom levels.
|
|
11
|
-
*
|
|
12
|
-
* @param {import('three').PerspectiveCamera} camera Active camera.
|
|
13
|
-
* @param {number} distance Camera-to-target distance.
|
|
14
|
-
* @param {number} maxDimension Largest model dimension.
|
|
15
|
-
*/
|
|
16
|
-
export function updateClipPlanes(camera, distance, maxDimension) {
|
|
17
|
-
const near = Math.max(distance / 500, maxDimension / 10000, 0.0001);
|
|
18
|
-
const far = Math.max(distance * 40, maxDimension * 20);
|
|
19
|
-
if (camera.near === near && camera.far === far) return;
|
|
20
|
-
|
|
21
|
-
camera.near = near;
|
|
22
|
-
camera.far = far;
|
|
23
|
-
camera.updateProjectionMatrix();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Frame the model from a three-quarter view and set orbit limits.
|
|
28
|
-
*
|
|
29
|
-
* @param {import('three').PerspectiveCamera} camera Active camera.
|
|
30
|
-
* @param {import('three').OrbitControls} controls Orbit controls.
|
|
31
|
-
* @param {ReturnType<import('../model.js').collectModelStats>} stats Model bounds.
|
|
32
|
-
* @param {number} maxDimension Largest model dimension.
|
|
33
|
-
* @returns {number} Camera-to-target distance.
|
|
34
|
-
*/
|
|
35
|
-
export function frameModel(camera, controls, stats, maxDimension) {
|
|
36
|
-
const fov = (camera.fov * Math.PI) / 180;
|
|
37
|
-
const aspect = camera.aspect > 0 ? camera.aspect : 1;
|
|
38
|
-
const fitHeight = stats.size.y / (2 * Math.tan(fov / 2));
|
|
39
|
-
const fitWidth = stats.size.x / (2 * Math.tan(fov / 2) * aspect);
|
|
40
|
-
const fitDepth = stats.size.z * 0.8;
|
|
41
|
-
let distance = Math.max(fitHeight, fitWidth, fitDepth) * 2.5;
|
|
42
|
-
if (!Number.isFinite(distance) || distance <= 0) distance = maxDimension * 2.5;
|
|
43
|
-
distance = Math.max(distance, maxDimension * 0.9);
|
|
44
|
-
|
|
45
|
-
camera.position.copy(stats.center).addScaledVector(FIT_DIRECTION, distance);
|
|
46
|
-
controls.target.copy(stats.center);
|
|
47
|
-
controls.minDistance = Math.max(distance * 0.02, maxDimension * 0.03);
|
|
48
|
-
controls.maxDistance = Math.max(distance * 30, maxDimension * 10);
|
|
49
|
-
controls.update();
|
|
50
|
-
return distance;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether a key is handled by the orbit keyboard controls.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} key KeyboardEvent.key value.
|
|
57
|
-
* @returns {boolean} True for the four arrow keys.
|
|
58
|
-
*/
|
|
59
|
-
export function isOrbitKey(key) {
|
|
60
|
-
return key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowDown';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Orbit the camera by one keyboard step.
|
|
65
|
-
*
|
|
66
|
-
* @param {import('three').PerspectiveCamera} camera Active camera.
|
|
67
|
-
* @param {import('three').OrbitControls} controls Orbit controls.
|
|
68
|
-
* @param {string} key KeyboardEvent.key value.
|
|
69
|
-
*/
|
|
70
|
-
export function orbitByKey(camera, controls, key) {
|
|
71
|
-
_offset.copy(camera.position).sub(controls.target);
|
|
72
|
-
_spherical.setFromVector3(_offset);
|
|
73
|
-
|
|
74
|
-
if (key === 'ArrowLeft') _spherical.theta += KEY_STEP;
|
|
75
|
-
else if (key === 'ArrowRight') _spherical.theta -= KEY_STEP;
|
|
76
|
-
else if (key === 'ArrowUp') _spherical.phi = Math.max(controls.minPolarAngle, _spherical.phi - KEY_STEP);
|
|
77
|
-
else if (key === 'ArrowDown') _spherical.phi = Math.min(controls.maxPolarAngle, _spherical.phi + KEY_STEP);
|
|
78
|
-
else return;
|
|
79
|
-
|
|
80
|
-
camera.position.copy(controls.target).add(_offset.setFromSpherical(_spherical));
|
|
81
|
-
camera.lookAt(controls.target);
|
|
82
|
-
controls.update();
|
|
83
|
-
}
|
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ACESFilmicToneMapping,
|
|
3
|
-
PCFSoftShadowMap,
|
|
4
|
-
PerspectiveCamera,
|
|
5
|
-
Scene,
|
|
6
|
-
SRGBColorSpace,
|
|
7
|
-
WebGLRenderer,
|
|
8
|
-
} from 'three';
|
|
9
|
-
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
10
|
-
import { disposeObject } from '../model.js';
|
|
11
|
-
import { createAnimationController } from './animation.js';
|
|
12
|
-
import { frameModel, isOrbitKey, orbitByKey, updateClipPlanes } from './camera.js';
|
|
13
|
-
import { DEFAULT_ENVIRONMENT, createEnvironment } from './environment.js';
|
|
14
|
-
import { createGround } from './ground.js';
|
|
15
|
-
import { createLighting } from './lighting.js';
|
|
16
|
-
import { createMaterialController } from './materials.js';
|
|
17
|
-
import { createPostProcessing } from './postprocessing.js';
|
|
18
|
-
import { createRenderLoop } from './render-loop.js';
|
|
19
|
-
import { ViewerError } from './viewer-error.js';
|
|
20
|
-
|
|
21
|
-
const MAX_PIXEL_RATIO = 2;
|
|
22
|
-
|
|
23
|
-
/** Device pixel ratio, clamped so 4K/5K displays do not quadruple the cost. */
|
|
24
|
-
function readPixelRatio() {
|
|
25
|
-
return Math.min(globalThis.devicePixelRatio || 1, MAX_PIXEL_RATIO);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Attach a disposable, orbitable Three.js view to a host element.
|
|
30
|
-
*
|
|
31
|
-
* The renderer is transparent so the host's themed background stays visible.
|
|
32
|
-
* Frames render on demand; only auto-rotate or animation keeps a continuous
|
|
33
|
-
* loop, and that loop pauses while the viewer is off-screen or the page is
|
|
34
|
-
* hidden.
|
|
35
|
-
*
|
|
36
|
-
* @param {HTMLElement} host Element that will own the canvas.
|
|
37
|
-
* @param {import('three').Object3D} object Parsed FBX graph.
|
|
38
|
-
* @param {ReturnType<import('../model.js').collectModelStats>} stats Model bounds and counts.
|
|
39
|
-
* @param {{ ariaLabel?: string, environment?: string }} [options] Localized accessibility text and initial IBL environment.
|
|
40
|
-
* @returns {{
|
|
41
|
-
* animations: Array<{ name: string, duration: number }>,
|
|
42
|
-
* subscribeAnimation: (listener: (state: { time: number, duration: number, index: number, playing: boolean }) => void) => () => void,
|
|
43
|
-
* setAnimationPlaying: (enabled: boolean) => void,
|
|
44
|
-
* setAnimationClip: (index: number) => void,
|
|
45
|
-
* seekAnimation: (seconds: number) => void,
|
|
46
|
-
* setAutoRotate: (enabled: boolean) => boolean,
|
|
47
|
-
* setGrid: (visible: boolean) => boolean,
|
|
48
|
-
* setEnvironment: (id: string) => boolean,
|
|
49
|
-
* setMaterialPreset: (id: string) => boolean,
|
|
50
|
-
* setWireframe: (enabled: boolean) => boolean,
|
|
51
|
-
* reset: () => void,
|
|
52
|
-
* invalidate: () => void,
|
|
53
|
-
* dispose: () => void,
|
|
54
|
-
* }} View controls.
|
|
55
|
-
*/
|
|
56
|
-
export function createSceneView(host, object, stats, options = {}) {
|
|
57
|
-
let renderer;
|
|
58
|
-
try {
|
|
59
|
-
renderer = new WebGLRenderer({
|
|
60
|
-
alpha: true,
|
|
61
|
-
antialias: true,
|
|
62
|
-
powerPreference: 'high-performance',
|
|
63
|
-
});
|
|
64
|
-
} catch (error) {
|
|
65
|
-
throw new ViewerError('webgl', 'A WebGL context could not be created.', error);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const canvas = renderer.domElement;
|
|
69
|
-
canvas.className = 'dsh3dv-canvas';
|
|
70
|
-
canvas.dataset.dsh3dCanvas = '';
|
|
71
|
-
canvas.tabIndex = 0;
|
|
72
|
-
canvas.setAttribute('role', 'img');
|
|
73
|
-
canvas.setAttribute('aria-label', options.ariaLabel ?? '3D model');
|
|
74
|
-
canvas.style.touchAction = 'none';
|
|
75
|
-
host.appendChild(canvas);
|
|
76
|
-
|
|
77
|
-
const initialWidth = Math.max(host.clientWidth, 1);
|
|
78
|
-
const initialHeight = Math.max(host.clientHeight, 1);
|
|
79
|
-
const initialPixelRatio = readPixelRatio();
|
|
80
|
-
|
|
81
|
-
renderer.setPixelRatio(initialPixelRatio);
|
|
82
|
-
renderer.setSize(initialWidth, initialHeight, false);
|
|
83
|
-
renderer.outputColorSpace = SRGBColorSpace;
|
|
84
|
-
renderer.toneMapping = ACESFilmicToneMapping;
|
|
85
|
-
renderer.toneMappingExposure = 1.0;
|
|
86
|
-
renderer.setClearColor(0x000000, 0);
|
|
87
|
-
renderer.shadowMap.enabled = true;
|
|
88
|
-
renderer.shadowMap.type = PCFSoftShadowMap;
|
|
89
|
-
|
|
90
|
-
const scene = new Scene();
|
|
91
|
-
const camera = new PerspectiveCamera(38, initialWidth / initialHeight, 0.1, 1000);
|
|
92
|
-
const maxDimension = stats.maxDimension > 0 ? stats.maxDimension : 1;
|
|
93
|
-
|
|
94
|
-
const environment = createEnvironment({ renderer, scene });
|
|
95
|
-
environment.set(options.environment ?? DEFAULT_ENVIRONMENT);
|
|
96
|
-
renderer.setRenderTarget(null);
|
|
97
|
-
renderer.setClearColor(0x000000, 0);
|
|
98
|
-
|
|
99
|
-
const lighting = createLighting({ scene, center: stats.center, maxDimension });
|
|
100
|
-
scene.add(object);
|
|
101
|
-
|
|
102
|
-
const materials = createMaterialController(object);
|
|
103
|
-
const ground = createGround({
|
|
104
|
-
scene,
|
|
105
|
-
stats,
|
|
106
|
-
maxDimension,
|
|
107
|
-
maxAnisotropy: renderer.capabilities.getMaxAnisotropy(),
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
const animationListeners = new Set();
|
|
111
|
-
const animation = createAnimationController(object, {
|
|
112
|
-
onTick(state) {
|
|
113
|
-
for (const listener of animationListeners) {
|
|
114
|
-
try {
|
|
115
|
-
listener(state);
|
|
116
|
-
} catch (error) {
|
|
117
|
-
console.error('[dsh-3d-viewer] Animation listener failed.', error);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const post = createPostProcessing({
|
|
124
|
-
renderer,
|
|
125
|
-
scene,
|
|
126
|
-
camera,
|
|
127
|
-
width: initialWidth,
|
|
128
|
-
height: initialHeight,
|
|
129
|
-
pixelRatio: initialPixelRatio,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const controls = new OrbitControls(camera, canvas);
|
|
133
|
-
controls.enableDamping = false;
|
|
134
|
-
controls.screenSpacePanning = true;
|
|
135
|
-
controls.rotateSpeed = 0.9;
|
|
136
|
-
controls.zoomSpeed = 0.9;
|
|
137
|
-
controls.panSpeed = 0.8;
|
|
138
|
-
controls.autoRotateSpeed = 1.6;
|
|
139
|
-
controls.minPolarAngle = 0.02;
|
|
140
|
-
controls.maxPolarAngle = Math.PI * 0.98;
|
|
141
|
-
|
|
142
|
-
let autoRotate = false;
|
|
143
|
-
let disposed = false;
|
|
144
|
-
let onScreen = true;
|
|
145
|
-
let lastWidth = 0;
|
|
146
|
-
let lastHeight = 0;
|
|
147
|
-
let lastPixelRatio = 0;
|
|
148
|
-
let lastSyncDistance = Number.NaN;
|
|
149
|
-
let lastSyncTargetX = Number.NaN;
|
|
150
|
-
let lastSyncTargetZ = Number.NaN;
|
|
151
|
-
let resizeObserver;
|
|
152
|
-
let intersectionObserver;
|
|
153
|
-
|
|
154
|
-
const loop = createRenderLoop((delta, timestamp) => {
|
|
155
|
-
animation.update(delta, timestamp);
|
|
156
|
-
if (autoRotate) controls.update();
|
|
157
|
-
post.render(delta);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
function syncView() {
|
|
161
|
-
const distance = Math.max(camera.position.distanceTo(controls.target), maxDimension * 0.05);
|
|
162
|
-
const targetX = controls.target.x;
|
|
163
|
-
const targetZ = controls.target.z;
|
|
164
|
-
|
|
165
|
-
// Auto-rotation changes the camera orientation but not its distance or
|
|
166
|
-
// target, so the grid and clip planes can stay untouched in that case.
|
|
167
|
-
if (distance === lastSyncDistance
|
|
168
|
-
&& targetX === lastSyncTargetX
|
|
169
|
-
&& targetZ === lastSyncTargetZ) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
lastSyncDistance = distance;
|
|
173
|
-
lastSyncTargetX = targetX;
|
|
174
|
-
lastSyncTargetZ = targetZ;
|
|
175
|
-
|
|
176
|
-
updateClipPlanes(camera, distance, maxDimension);
|
|
177
|
-
ground.update(camera, controls.target, distance);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function updateLoop() {
|
|
181
|
-
if (autoRotate || animation.isPlaying()) loop.start();
|
|
182
|
-
else {
|
|
183
|
-
loop.stop();
|
|
184
|
-
loop.invalidate();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function onControlsChange() {
|
|
189
|
-
syncView();
|
|
190
|
-
loop.invalidate();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function resize() {
|
|
194
|
-
if (disposed) return;
|
|
195
|
-
|
|
196
|
-
const width = Math.max(host.clientWidth, 1);
|
|
197
|
-
const height = Math.max(host.clientHeight, 1);
|
|
198
|
-
const pixelRatio = readPixelRatio();
|
|
199
|
-
if (width === lastWidth && height === lastHeight && pixelRatio === lastPixelRatio) return;
|
|
200
|
-
|
|
201
|
-
const pixelRatioChanged = pixelRatio !== lastPixelRatio;
|
|
202
|
-
const sizeChanged = width !== lastWidth || height !== lastHeight;
|
|
203
|
-
lastWidth = width;
|
|
204
|
-
lastHeight = height;
|
|
205
|
-
lastPixelRatio = pixelRatio;
|
|
206
|
-
|
|
207
|
-
if (pixelRatioChanged) renderer.setPixelRatio(pixelRatio);
|
|
208
|
-
if (sizeChanged) renderer.setSize(width, height, false);
|
|
209
|
-
post.setSize(width, height, pixelRatio);
|
|
210
|
-
|
|
211
|
-
if (sizeChanged) {
|
|
212
|
-
camera.aspect = width / height;
|
|
213
|
-
camera.updateProjectionMatrix();
|
|
214
|
-
}
|
|
215
|
-
loop.invalidate();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function onKeyDown(event) {
|
|
219
|
-
if (!isOrbitKey(event.key)) return;
|
|
220
|
-
event.preventDefault();
|
|
221
|
-
orbitByKey(camera, controls, event.key);
|
|
222
|
-
loop.invalidate();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function onPointerDown() {
|
|
226
|
-
canvas.focus({ preventScroll: true });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function onVisibilityChange() {
|
|
230
|
-
loop.setActive(onScreen && !document.hidden);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function setAutoRotate(enabled) {
|
|
234
|
-
const next = Boolean(enabled);
|
|
235
|
-
if (next === autoRotate) return false;
|
|
236
|
-
|
|
237
|
-
autoRotate = next;
|
|
238
|
-
controls.autoRotate = next;
|
|
239
|
-
updateLoop();
|
|
240
|
-
if (!next) loop.invalidate();
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function setGrid(visible) {
|
|
245
|
-
if (!ground.setGridVisible(visible)) return false;
|
|
246
|
-
loop.invalidate();
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function setEnvironment(id) {
|
|
251
|
-
if (!environment.set(id)) return false;
|
|
252
|
-
loop.invalidate();
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function setMaterialPreset(id) {
|
|
257
|
-
if (!materials.setPreset(id)) return false;
|
|
258
|
-
loop.invalidate();
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function setWireframe(enabled) {
|
|
263
|
-
if (!materials.setWireframe(enabled)) return false;
|
|
264
|
-
ground.setShadowVisible(!materials.wireframe);
|
|
265
|
-
loop.invalidate();
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function setAnimationPlaying(enabled) {
|
|
270
|
-
if (!animation.setPlaying(enabled)) return;
|
|
271
|
-
updateLoop();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function setAnimationClip(index) {
|
|
275
|
-
if (!animation.setClip(index)) return;
|
|
276
|
-
updateLoop();
|
|
277
|
-
loop.invalidate();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function seekAnimation(seconds) {
|
|
281
|
-
if (animation.seek(seconds)) loop.invalidate();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function subscribeAnimation(listener) {
|
|
285
|
-
if (typeof listener !== 'function' || disposed) return () => {};
|
|
286
|
-
|
|
287
|
-
animationListeners.add(listener);
|
|
288
|
-
listener(animation.snapshot());
|
|
289
|
-
return () => {
|
|
290
|
-
animationListeners.delete(listener);
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function reset() {
|
|
295
|
-
frameModel(camera, controls, stats, maxDimension);
|
|
296
|
-
syncView();
|
|
297
|
-
loop.invalidate();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function invalidate() {
|
|
301
|
-
loop.invalidate();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function dispose() {
|
|
305
|
-
if (disposed) return;
|
|
306
|
-
disposed = true;
|
|
307
|
-
|
|
308
|
-
loop.dispose();
|
|
309
|
-
resizeObserver?.disconnect();
|
|
310
|
-
intersectionObserver?.disconnect();
|
|
311
|
-
window.removeEventListener('resize', resize);
|
|
312
|
-
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
313
|
-
controls.removeEventListener('change', onControlsChange);
|
|
314
|
-
canvas.removeEventListener('keydown', onKeyDown);
|
|
315
|
-
canvas.removeEventListener('pointerdown', onPointerDown);
|
|
316
|
-
animationListeners.clear();
|
|
317
|
-
|
|
318
|
-
animation.dispose();
|
|
319
|
-
materials.dispose();
|
|
320
|
-
ground.dispose();
|
|
321
|
-
lighting.dispose();
|
|
322
|
-
environment.dispose();
|
|
323
|
-
controls.dispose();
|
|
324
|
-
post.dispose();
|
|
325
|
-
disposeObject(scene);
|
|
326
|
-
scene.clear();
|
|
327
|
-
renderer.dispose();
|
|
328
|
-
renderer.forceContextLoss();
|
|
329
|
-
canvas.remove();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
controls.addEventListener('change', onControlsChange);
|
|
333
|
-
canvas.addEventListener('keydown', onKeyDown);
|
|
334
|
-
canvas.addEventListener('pointerdown', onPointerDown);
|
|
335
|
-
window.addEventListener('resize', resize);
|
|
336
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
337
|
-
|
|
338
|
-
if (typeof ResizeObserver === 'function') {
|
|
339
|
-
resizeObserver = new ResizeObserver(resize);
|
|
340
|
-
resizeObserver.observe(host);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (typeof IntersectionObserver === 'function') {
|
|
344
|
-
intersectionObserver = new IntersectionObserver((entries) => {
|
|
345
|
-
const entry = entries[entries.length - 1];
|
|
346
|
-
if (entry === undefined) return;
|
|
347
|
-
onScreen = entry.isIntersecting;
|
|
348
|
-
onVisibilityChange();
|
|
349
|
-
}, { threshold: 0.01 });
|
|
350
|
-
intersectionObserver.observe(host);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
resize();
|
|
354
|
-
frameModel(camera, controls, stats, maxDimension);
|
|
355
|
-
syncView();
|
|
356
|
-
loop.setActive(onScreen && !document.hidden);
|
|
357
|
-
if (autoRotate || animation.isPlaying()) loop.start();
|
|
358
|
-
else loop.invalidate();
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
animations: animation.animations,
|
|
362
|
-
subscribeAnimation,
|
|
363
|
-
setAnimationPlaying,
|
|
364
|
-
setAnimationClip,
|
|
365
|
-
seekAnimation,
|
|
366
|
-
setAutoRotate,
|
|
367
|
-
setGrid,
|
|
368
|
-
setEnvironment,
|
|
369
|
-
setMaterialPreset,
|
|
370
|
-
setWireframe,
|
|
371
|
-
reset,
|
|
372
|
-
invalidate,
|
|
373
|
-
dispose,
|
|
374
|
-
};
|
|
375
|
-
}
|