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/materials.js
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DoubleSide,
|
|
3
|
-
MeshBasicMaterial,
|
|
4
|
-
MeshStandardMaterial,
|
|
5
|
-
NoColorSpace,
|
|
6
|
-
SRGBColorSpace,
|
|
7
|
-
} from 'three';
|
|
8
|
-
|
|
9
|
-
/** Fallback materials offered by the material rail. */
|
|
10
|
-
export const MATERIAL_PRESETS = Object.freeze({
|
|
11
|
-
diffuse: Object.freeze({ color: 0xd9d9d6, metalness: 0, roughness: 1 }),
|
|
12
|
-
plastic: Object.freeze({ color: 0xe8eef5, metalness: 0, roughness: 0.1 }),
|
|
13
|
-
metal: Object.freeze({ color: 0xc5cbd3, metalness: 1, roughness: 0 }),
|
|
14
|
-
'metal-polished': Object.freeze({ color: 0xd9dde4, metalness: 1, roughness: 0.3 }),
|
|
15
|
-
clay: Object.freeze({ color: 0xb46f52, metalness: 0, roughness: 0.84 }),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
/** Preset id that keeps the original converted FBX material. */
|
|
19
|
-
export const DEFAULT_MATERIAL = 'original';
|
|
20
|
-
|
|
21
|
-
/** Texture slots carried over from the source material to the PBR replacement. */
|
|
22
|
-
const TEXTURE_SLOTS = Object.freeze([
|
|
23
|
-
'map',
|
|
24
|
-
'normalMap',
|
|
25
|
-
'bumpMap',
|
|
26
|
-
'displacementMap',
|
|
27
|
-
'aoMap',
|
|
28
|
-
'lightMap',
|
|
29
|
-
'alphaMap',
|
|
30
|
-
'emissiveMap',
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
/** Slots whose pixels are sRGB-encoded color data. */
|
|
34
|
-
const SRGB_TEXTURE_SLOTS = new Set(['map', 'emissiveMap', 'lightMap']);
|
|
35
|
-
|
|
36
|
-
/** Scalar material fields copied verbatim, with the value used when unset. */
|
|
37
|
-
const SCALAR_DEFAULTS = Object.freeze({
|
|
38
|
-
emissiveIntensity: 1,
|
|
39
|
-
bumpScale: 1,
|
|
40
|
-
displacementScale: 1,
|
|
41
|
-
displacementBias: 0,
|
|
42
|
-
aoMapIntensity: 1,
|
|
43
|
-
lightMapIntensity: 1,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
/** Material state copied verbatim from the source material. */
|
|
47
|
-
const COPIED_PROPERTIES = Object.freeze([
|
|
48
|
-
'transparent',
|
|
49
|
-
'opacity',
|
|
50
|
-
'alphaTest',
|
|
51
|
-
'side',
|
|
52
|
-
'shadowSide',
|
|
53
|
-
'depthWrite',
|
|
54
|
-
'depthTest',
|
|
55
|
-
'blending',
|
|
56
|
-
'premultipliedAlpha',
|
|
57
|
-
'polygonOffset',
|
|
58
|
-
'polygonOffsetFactor',
|
|
59
|
-
'polygonOffsetUnits',
|
|
60
|
-
'toneMapped',
|
|
61
|
-
'vertexColors',
|
|
62
|
-
'flatShading',
|
|
63
|
-
'wireframe',
|
|
64
|
-
'visible',
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Approximate a PBR roughness from a Phong specular exponent.
|
|
69
|
-
*
|
|
70
|
-
* @param {import('three').Material} material Source FBX material.
|
|
71
|
-
* @returns {number} GGX roughness in [0.2, 0.9].
|
|
72
|
-
*/
|
|
73
|
-
export function pbrRoughness(material) {
|
|
74
|
-
const shininess = typeof material.shininess === 'number' ? material.shininess : 0;
|
|
75
|
-
if (shininess <= 0) return 0.62;
|
|
76
|
-
return Math.min(0.9, Math.max(0.2, 1 - Math.log2(shininess + 1) / 10));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Replace an FBX Phong/Lambert material with a metallic-roughness PBR material.
|
|
81
|
-
*
|
|
82
|
-
* Three's MeshStandardMaterial is its Cook-Torrance path and evaluates
|
|
83
|
-
* BRDF_Lambert for diffuse, D_GGX, F_Schlick and the correlated Smith-GGX
|
|
84
|
-
* visibility (V_GGX_SmithCorrelated, equivalent to Filament's
|
|
85
|
-
* V_SmithGGXCorrelated). Textures and material state are retained.
|
|
86
|
-
*
|
|
87
|
-
* @param {import('three').Material | import('three').Material[] | null | undefined} source Source material(s).
|
|
88
|
-
* @param {Map<string, import('three').Material>} cache Shared converted materials.
|
|
89
|
-
* @returns {import('three').Material | import('three').Material[] | null | undefined} PBR material(s).
|
|
90
|
-
*/
|
|
91
|
-
export function toPbrMaterial(source, cache) {
|
|
92
|
-
if (source === null || source === undefined) return source;
|
|
93
|
-
if (Array.isArray(source)) return source.map((entry) => toPbrMaterial(entry, cache));
|
|
94
|
-
if (source.isMeshStandardMaterial === true || source.isMeshPhysicalMaterial === true) return source;
|
|
95
|
-
|
|
96
|
-
const cached = cache.get(source.uuid);
|
|
97
|
-
if (cached !== undefined) return cached;
|
|
98
|
-
|
|
99
|
-
const pbr = new MeshStandardMaterial();
|
|
100
|
-
pbr.name = source.name;
|
|
101
|
-
// FBXLoaderPbr converts FBX Diffuse/Emissive/Specular colors from sRGB to
|
|
102
|
-
// the linear working color space; copy those linear values unchanged.
|
|
103
|
-
if (source.color !== undefined) pbr.color.copy(source.color);
|
|
104
|
-
if (source.emissive !== undefined) pbr.emissive.copy(source.emissive);
|
|
105
|
-
for (const [property, fallback] of Object.entries(SCALAR_DEFAULTS)) {
|
|
106
|
-
pbr[property] = source[property] ?? fallback;
|
|
107
|
-
}
|
|
108
|
-
for (const slot of TEXTURE_SLOTS) {
|
|
109
|
-
pbr[slot] = source[slot] ?? null;
|
|
110
|
-
}
|
|
111
|
-
if (source.normalScale !== undefined) pbr.normalScale.copy(source.normalScale);
|
|
112
|
-
|
|
113
|
-
// Normalize texture color spaces explicitly. Color maps are decoded from
|
|
114
|
-
// sRGB by the shader; normal/AO/alpha/displacement/bump maps stay linear.
|
|
115
|
-
for (const slot of TEXTURE_SLOTS) {
|
|
116
|
-
const texture = pbr[slot];
|
|
117
|
-
if (texture === null) continue;
|
|
118
|
-
texture.colorSpace = SRGB_TEXTURE_SLOTS.has(slot) ? SRGBColorSpace : NoColorSpace;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const fbxRoughness = source.userData?.fbxRoughness;
|
|
122
|
-
pbr.roughness = typeof fbxRoughness === 'number' && Number.isFinite(fbxRoughness)
|
|
123
|
-
? Math.min(1, Math.max(0, fbxRoughness))
|
|
124
|
-
: pbrRoughness(source);
|
|
125
|
-
const fbxMetalness = source.userData?.fbxMetalness;
|
|
126
|
-
pbr.metalness = typeof fbxMetalness === 'number' && Number.isFinite(fbxMetalness)
|
|
127
|
-
? Math.min(1, Math.max(0, fbxMetalness))
|
|
128
|
-
: 0;
|
|
129
|
-
for (const property of COPIED_PROPERTIES) {
|
|
130
|
-
pbr[property] = source[property];
|
|
131
|
-
}
|
|
132
|
-
pbr.userData = { ...source.userData };
|
|
133
|
-
|
|
134
|
-
cache.set(source.uuid, pbr);
|
|
135
|
-
return pbr;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Resolve a material preset id, falling back to the original material.
|
|
140
|
-
*
|
|
141
|
-
* @param {unknown} id Requested preset.
|
|
142
|
-
* @returns {string} A valid preset id.
|
|
143
|
-
*/
|
|
144
|
-
export function normalizeMaterialPreset(id) {
|
|
145
|
-
return id === DEFAULT_MATERIAL || (typeof id === 'string' && MATERIAL_PRESETS[id] !== undefined)
|
|
146
|
-
? id
|
|
147
|
-
: DEFAULT_MATERIAL;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Convert every mesh once, then switch between original, preset, and wireframe
|
|
152
|
-
* materials without touching geometry.
|
|
153
|
-
*
|
|
154
|
-
* @param {import('three').Object3D} root Parsed FBX graph.
|
|
155
|
-
* @returns {{
|
|
156
|
-
* setPreset: (id: unknown) => boolean,
|
|
157
|
-
* setWireframe: (enabled: boolean) => boolean,
|
|
158
|
-
* dispose: () => void,
|
|
159
|
-
* readonly preset: string,
|
|
160
|
-
* readonly wireframe: boolean,
|
|
161
|
-
* }} Material controls.
|
|
162
|
-
*/
|
|
163
|
-
export function createMaterialController(root) {
|
|
164
|
-
const pbrMaterials = new Map();
|
|
165
|
-
const presetMaterials = new Map();
|
|
166
|
-
const meshStates = [];
|
|
167
|
-
let preset = DEFAULT_MATERIAL;
|
|
168
|
-
let wireframeActive = false;
|
|
169
|
-
|
|
170
|
-
root.traverse((child) => {
|
|
171
|
-
if (child.isMesh !== true) return;
|
|
172
|
-
|
|
173
|
-
child.material = toPbrMaterial(child.material, pbrMaterials);
|
|
174
|
-
child.castShadow = true;
|
|
175
|
-
child.receiveShadow = true;
|
|
176
|
-
meshStates.push({
|
|
177
|
-
mesh: child,
|
|
178
|
-
material: child.material,
|
|
179
|
-
castShadow: true,
|
|
180
|
-
receiveShadow: true,
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const wireframeMaterial = new MeshBasicMaterial({
|
|
185
|
-
color: 0x4d5561,
|
|
186
|
-
wireframe: true,
|
|
187
|
-
side: DoubleSide,
|
|
188
|
-
toneMapped: false,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
function getPresetMaterial(id) {
|
|
192
|
-
const cached = presetMaterials.get(id);
|
|
193
|
-
if (cached !== undefined) return cached;
|
|
194
|
-
|
|
195
|
-
const definition = MATERIAL_PRESETS[id];
|
|
196
|
-
if (definition === undefined) return null;
|
|
197
|
-
|
|
198
|
-
const material = new MeshStandardMaterial({
|
|
199
|
-
name: `dsh3dv-${id}`,
|
|
200
|
-
color: definition.color,
|
|
201
|
-
metalness: definition.metalness,
|
|
202
|
-
roughness: definition.roughness,
|
|
203
|
-
side: DoubleSide,
|
|
204
|
-
});
|
|
205
|
-
presetMaterials.set(id, material);
|
|
206
|
-
return material;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function apply() {
|
|
210
|
-
const override = !wireframeActive && preset !== DEFAULT_MATERIAL
|
|
211
|
-
? getPresetMaterial(preset)
|
|
212
|
-
: null;
|
|
213
|
-
|
|
214
|
-
for (const state of meshStates) {
|
|
215
|
-
state.mesh.material = wireframeActive
|
|
216
|
-
? wireframeMaterial
|
|
217
|
-
: (override ?? state.material);
|
|
218
|
-
state.mesh.castShadow = wireframeActive ? false : state.castShadow;
|
|
219
|
-
state.mesh.receiveShadow = wireframeActive ? false : state.receiveShadow;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function setPreset(id) {
|
|
224
|
-
const next = normalizeMaterialPreset(id);
|
|
225
|
-
if (next === preset) return false;
|
|
226
|
-
|
|
227
|
-
preset = next;
|
|
228
|
-
if (!wireframeActive) apply();
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function setWireframe(enabled) {
|
|
233
|
-
const next = Boolean(enabled);
|
|
234
|
-
if (next === wireframeActive) return false;
|
|
235
|
-
|
|
236
|
-
wireframeActive = next;
|
|
237
|
-
apply();
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function dispose() {
|
|
242
|
-
for (const state of meshStates) {
|
|
243
|
-
state.mesh.material = state.material;
|
|
244
|
-
state.mesh.castShadow = state.castShadow;
|
|
245
|
-
state.mesh.receiveShadow = state.receiveShadow;
|
|
246
|
-
}
|
|
247
|
-
for (const material of presetMaterials.values()) material.dispose();
|
|
248
|
-
presetMaterials.clear();
|
|
249
|
-
wireframeMaterial.dispose();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
setPreset,
|
|
254
|
-
setWireframe,
|
|
255
|
-
dispose,
|
|
256
|
-
get preset() {
|
|
257
|
-
return preset;
|
|
258
|
-
},
|
|
259
|
-
get wireframe() {
|
|
260
|
-
return wireframeActive;
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
2
|
-
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|
3
|
-
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
|
|
4
|
-
import { BLUE_NOISE_SIZE, createBlueNoiseTexture } from './blue-noise.js';
|
|
5
|
-
|
|
6
|
-
const POST_VERTEX_SHADER = `
|
|
7
|
-
varying vec2 vUv;
|
|
8
|
-
|
|
9
|
-
void main() {
|
|
10
|
-
vUv = uv;
|
|
11
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
12
|
-
}
|
|
13
|
-
`;
|
|
14
|
-
|
|
15
|
-
const POST_FRAGMENT_SHADER = `
|
|
16
|
-
uniform sampler2D tDiffuse;
|
|
17
|
-
uniform sampler2D uBlueNoise;
|
|
18
|
-
uniform float uExposure;
|
|
19
|
-
uniform float uBlueNoiseScale;
|
|
20
|
-
|
|
21
|
-
varying vec2 vUv;
|
|
22
|
-
|
|
23
|
-
vec3 RRTAndODTFit(vec3 v) {
|
|
24
|
-
vec3 a = v * (v + 0.0245786) - 0.000090537;
|
|
25
|
-
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
|
|
26
|
-
return a / b;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
vec3 acesFilmic(vec3 color) {
|
|
30
|
-
const mat3 ACESInputMat = mat3(
|
|
31
|
-
vec3(0.59719, 0.07600, 0.02840),
|
|
32
|
-
vec3(0.35458, 0.90834, 0.13383),
|
|
33
|
-
vec3(0.04823, 0.01566, 0.83777)
|
|
34
|
-
);
|
|
35
|
-
const mat3 ACESOutputMat = mat3(
|
|
36
|
-
vec3(1.60475, -0.10208, -0.00327),
|
|
37
|
-
vec3(-0.53108, 1.10813, -0.07276),
|
|
38
|
-
vec3(-0.07367, -0.00605, 1.07602)
|
|
39
|
-
);
|
|
40
|
-
color = ACESInputMat * color;
|
|
41
|
-
color = RRTAndODTFit(color);
|
|
42
|
-
color = ACESOutputMat * color;
|
|
43
|
-
return clamp(color, 0.0, 1.0);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
vec3 sRGBTransferOETF(vec3 value) {
|
|
47
|
-
return mix(
|
|
48
|
-
pow(value, vec3(0.41666)) * 1.055 - 0.055,
|
|
49
|
-
value * 12.92,
|
|
50
|
-
lessThanEqual(value, vec3(0.0031308))
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
float ditherNoise(vec2 p) {
|
|
55
|
-
// Blue-noise mask, sampled at native pixel coordinates. The amplitude is
|
|
56
|
-
// +/- 1.5 8-bit display steps; the alpha mask below keeps white clean.
|
|
57
|
-
float noise = texture2D(uBlueNoise, p * uBlueNoiseScale).r;
|
|
58
|
-
return (noise - 0.5) * (3.0 / 255.0);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
void main() {
|
|
62
|
-
vec4 base = texture2D(tDiffuse, vUv);
|
|
63
|
-
float alpha = clamp(base.a, 0.0, 1.0);
|
|
64
|
-
vec3 linear = base.rgb / max(alpha, 1e-4);
|
|
65
|
-
|
|
66
|
-
linear *= uExposure / 0.6;
|
|
67
|
-
linear = acesFilmic(linear);
|
|
68
|
-
vec3 color = sRGBTransferOETF(linear);
|
|
69
|
-
vec3 finalColor = mix(vec3(1.0), color, alpha);
|
|
70
|
-
|
|
71
|
-
// Keep the pure white background clean; only dither pixels that actually
|
|
72
|
-
// contain scene content.
|
|
73
|
-
float ditherMask = smoothstep(0.0, 0.02, alpha);
|
|
74
|
-
finalColor += ditherNoise(gl_FragCoord.xy) * ditherMask;
|
|
75
|
-
gl_FragColor = vec4(clamp(finalColor, 0.0, 1.0), 1.0);
|
|
76
|
-
}
|
|
77
|
-
`;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Render the transparent scene into an MSAA target, then tone-map and
|
|
81
|
-
* composite it over a pure white background in a single output pass.
|
|
82
|
-
*
|
|
83
|
-
* @param {{
|
|
84
|
-
* renderer: import('three').WebGLRenderer,
|
|
85
|
-
* scene: import('three').Scene,
|
|
86
|
-
* camera: import('three').PerspectiveCamera,
|
|
87
|
-
* width: number,
|
|
88
|
-
* height: number,
|
|
89
|
-
* pixelRatio: number,
|
|
90
|
-
* }} options Renderer, scene, camera, and initial surface size.
|
|
91
|
-
* @returns {{
|
|
92
|
-
* render: (delta: number) => void,
|
|
93
|
-
* setSize: (width: number, height: number, pixelRatio: number) => boolean,
|
|
94
|
-
* dispose: () => void,
|
|
95
|
-
* }} Post-processing controls.
|
|
96
|
-
*/
|
|
97
|
-
export function createPostProcessing({ renderer, scene, camera, width, height, pixelRatio }) {
|
|
98
|
-
const composer = new EffectComposer(renderer);
|
|
99
|
-
const samples = Math.min(4, renderer.capabilities.maxSamples || 0);
|
|
100
|
-
if (samples > 0) {
|
|
101
|
-
composer.renderTarget1.samples = samples;
|
|
102
|
-
composer.renderTarget2.samples = samples;
|
|
103
|
-
}
|
|
104
|
-
composer.setPixelRatio(pixelRatio);
|
|
105
|
-
composer.setSize(width, height);
|
|
106
|
-
|
|
107
|
-
const renderPass = new RenderPass(scene, camera);
|
|
108
|
-
const blueNoise = createBlueNoiseTexture();
|
|
109
|
-
const outputPass = new ShaderPass({
|
|
110
|
-
uniforms: {
|
|
111
|
-
tDiffuse: { value: null },
|
|
112
|
-
uBlueNoise: { value: blueNoise },
|
|
113
|
-
uExposure: { value: renderer.toneMappingExposure },
|
|
114
|
-
uBlueNoiseScale: { value: 1 / BLUE_NOISE_SIZE },
|
|
115
|
-
},
|
|
116
|
-
vertexShader: POST_VERTEX_SHADER,
|
|
117
|
-
fragmentShader: POST_FRAGMENT_SHADER,
|
|
118
|
-
});
|
|
119
|
-
outputPass.material.toneMapped = false;
|
|
120
|
-
outputPass.renderToScreen = true;
|
|
121
|
-
|
|
122
|
-
composer.addPass(renderPass);
|
|
123
|
-
composer.addPass(outputPass);
|
|
124
|
-
|
|
125
|
-
let currentWidth = width;
|
|
126
|
-
let currentHeight = height;
|
|
127
|
-
let currentPixelRatio = pixelRatio;
|
|
128
|
-
|
|
129
|
-
function setSize(nextWidth, nextHeight, nextPixelRatio) {
|
|
130
|
-
const sizeChanged = nextWidth !== currentWidth || nextHeight !== currentHeight;
|
|
131
|
-
const ratioChanged = nextPixelRatio !== currentPixelRatio;
|
|
132
|
-
if (!sizeChanged && !ratioChanged) return false;
|
|
133
|
-
|
|
134
|
-
if (ratioChanged) composer.setPixelRatio(nextPixelRatio);
|
|
135
|
-
if (sizeChanged) composer.setSize(nextWidth, nextHeight);
|
|
136
|
-
|
|
137
|
-
currentWidth = nextWidth;
|
|
138
|
-
currentHeight = nextHeight;
|
|
139
|
-
currentPixelRatio = nextPixelRatio;
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function render(delta) {
|
|
144
|
-
composer.render(delta);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function dispose() {
|
|
148
|
-
composer.dispose();
|
|
149
|
-
outputPass.dispose();
|
|
150
|
-
blueNoise.dispose();
|
|
151
|
-
renderPass.dispose?.();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return { render, setSize, dispose };
|
|
155
|
-
}
|
package/src/scene/render-loop.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A coalescing frame scheduler.
|
|
3
|
-
*
|
|
4
|
-
* Continuous loops run only while requested (auto-rotate or animation).
|
|
5
|
-
* One-off invalidations are merged into a single animation frame, so multiple
|
|
6
|
-
* OrbitControls change events in the same frame render once.
|
|
7
|
-
*
|
|
8
|
-
* @param {(delta: number, timestamp: number) => void} onFrame Frame callback.
|
|
9
|
-
* @returns {{
|
|
10
|
-
* invalidate: () => void,
|
|
11
|
-
* start: () => void,
|
|
12
|
-
* stop: () => void,
|
|
13
|
-
* setActive: (active: boolean) => void,
|
|
14
|
-
* dispose: () => void,
|
|
15
|
-
* readonly looping: boolean,
|
|
16
|
-
* }} Scheduler controls.
|
|
17
|
-
*/
|
|
18
|
-
export function createRenderLoop(onFrame) {
|
|
19
|
-
let looping = false;
|
|
20
|
-
let active = true;
|
|
21
|
-
let disposed = false;
|
|
22
|
-
let dirty = false;
|
|
23
|
-
let frameHandle = 0;
|
|
24
|
-
let lastTimestamp = 0;
|
|
25
|
-
|
|
26
|
-
function schedule() {
|
|
27
|
-
if (disposed || !active || frameHandle !== 0) return;
|
|
28
|
-
frameHandle = requestAnimationFrame(frame);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function frame(timestamp) {
|
|
32
|
-
frameHandle = 0;
|
|
33
|
-
if (disposed || !active) return;
|
|
34
|
-
|
|
35
|
-
const delta = lastTimestamp === 0 ? 0 : Math.min((timestamp - lastTimestamp) / 1000, 0.1);
|
|
36
|
-
lastTimestamp = timestamp;
|
|
37
|
-
dirty = false;
|
|
38
|
-
onFrame(delta, timestamp);
|
|
39
|
-
|
|
40
|
-
if (looping) schedule();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function invalidate() {
|
|
44
|
-
if (disposed) return;
|
|
45
|
-
dirty = true;
|
|
46
|
-
schedule();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function start() {
|
|
50
|
-
if (disposed || looping) return;
|
|
51
|
-
looping = true;
|
|
52
|
-
lastTimestamp = 0;
|
|
53
|
-
schedule();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function stop() {
|
|
57
|
-
looping = false;
|
|
58
|
-
lastTimestamp = 0;
|
|
59
|
-
if (frameHandle !== 0) {
|
|
60
|
-
cancelAnimationFrame(frameHandle);
|
|
61
|
-
frameHandle = 0;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function setActive(next) {
|
|
66
|
-
const value = Boolean(next);
|
|
67
|
-
if (value === active) return;
|
|
68
|
-
|
|
69
|
-
active = value;
|
|
70
|
-
if (!active) {
|
|
71
|
-
if (frameHandle !== 0) {
|
|
72
|
-
cancelAnimationFrame(frameHandle);
|
|
73
|
-
frameHandle = 0;
|
|
74
|
-
}
|
|
75
|
-
lastTimestamp = 0;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (looping || dirty) schedule();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function dispose() {
|
|
83
|
-
disposed = true;
|
|
84
|
-
dirty = false;
|
|
85
|
-
stop();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
invalidate,
|
|
90
|
-
start,
|
|
91
|
-
stop,
|
|
92
|
-
setActive,
|
|
93
|
-
dispose,
|
|
94
|
-
get looping() {
|
|
95
|
-
return looping;
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An error with a stable code the React surface can localize.
|
|
3
|
-
*/
|
|
4
|
-
export class ViewerError extends Error {
|
|
5
|
-
/**
|
|
6
|
-
* @param {'webgl' | 'scene' | 'parse'} code Failure category.
|
|
7
|
-
* @param {string} message Technical detail, kept for the disclosure panel.
|
|
8
|
-
* @param {unknown} [cause] Original error.
|
|
9
|
-
*/
|
|
10
|
-
constructor(code, message, cause) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'ViewerError';
|
|
13
|
-
this.code = code;
|
|
14
|
-
this.cause = cause;
|
|
15
|
-
}
|
|
16
|
-
}
|
package/src/ui/AnimationBar.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { PauseIcon, PlayIcon } from './icons.js';
|
|
3
|
-
import { formatSeconds } from './primitives.js';
|
|
4
|
-
|
|
5
|
-
const h = React.createElement;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Animation transport bar.
|
|
9
|
-
*
|
|
10
|
-
* The bar owns playback state and subscribes to the viewer's progress stream,
|
|
11
|
-
* so 30 Hz ticks re-render this small component instead of the whole viewer.
|
|
12
|
-
*
|
|
13
|
-
* @param {{
|
|
14
|
-
* view: {
|
|
15
|
-
* setAnimationPlaying: (playing: boolean) => void,
|
|
16
|
-
* setAnimationClip: (index: number) => void,
|
|
17
|
-
* seekAnimation: (seconds: number) => void,
|
|
18
|
-
* subscribeAnimation: (listener: (state: { time: number, duration: number }) => void) => () => void,
|
|
19
|
-
* },
|
|
20
|
-
* clips: Array<{ name: string, duration: number }>,
|
|
21
|
-
* t: (key: string, params?: Record<string, unknown>) => string,
|
|
22
|
-
* }} props Viewer, clip metadata, and translator.
|
|
23
|
-
*/
|
|
24
|
-
export function AnimationBar({ view, clips, t }) {
|
|
25
|
-
const [playing, setPlaying] = React.useState(true);
|
|
26
|
-
const [index, setIndex] = React.useState(0);
|
|
27
|
-
const [time, setTime] = React.useState(0);
|
|
28
|
-
|
|
29
|
-
React.useEffect(() => {
|
|
30
|
-
view.setAnimationPlaying(playing);
|
|
31
|
-
}, [view, playing]);
|
|
32
|
-
|
|
33
|
-
React.useEffect(() => {
|
|
34
|
-
view.setAnimationClip(index);
|
|
35
|
-
setTime(0);
|
|
36
|
-
}, [view, index]);
|
|
37
|
-
|
|
38
|
-
React.useEffect(() => view.subscribeAnimation((state) => {
|
|
39
|
-
setTime(state.time);
|
|
40
|
-
}), [view]);
|
|
41
|
-
|
|
42
|
-
const currentClip = clips[Math.min(index, clips.length - 1)];
|
|
43
|
-
const animationLabel = currentClip !== undefined && currentClip !== null
|
|
44
|
-
? (currentClip.name || t('animationFallback', { n: index + 1 }))
|
|
45
|
-
: '';
|
|
46
|
-
const animationMax = Number.isFinite(currentClip?.duration)
|
|
47
|
-
? Math.max(Number(currentClip.duration), 0)
|
|
48
|
-
: 0;
|
|
49
|
-
const currentSeconds = Math.min(Math.max(Number(time) || 0, 0), animationMax);
|
|
50
|
-
const timeLabel = `${formatSeconds(currentSeconds)}s / ${formatSeconds(animationMax)}s`;
|
|
51
|
-
|
|
52
|
-
return h('div', {
|
|
53
|
-
className: 'dsh3dv-anim',
|
|
54
|
-
'data-dsh3d-animation': '',
|
|
55
|
-
role: 'group',
|
|
56
|
-
'aria-label': t('animation'),
|
|
57
|
-
},
|
|
58
|
-
h('button', {
|
|
59
|
-
type: 'button',
|
|
60
|
-
className: 'dsh3dv-play',
|
|
61
|
-
title: playing ? t('pause') : t('play'),
|
|
62
|
-
'aria-label': playing ? t('pause') : t('play'),
|
|
63
|
-
'aria-pressed': playing,
|
|
64
|
-
onClick: () => setPlaying((value) => !value),
|
|
65
|
-
}, playing ? h(PauseIcon) : h(PlayIcon)),
|
|
66
|
-
clips.length > 1
|
|
67
|
-
? h('select', {
|
|
68
|
-
className: 'dsh3dv-anim-select',
|
|
69
|
-
value: index,
|
|
70
|
-
'aria-label': t('animation'),
|
|
71
|
-
onChange: (event) => {
|
|
72
|
-
setIndex(Number(event.currentTarget.value));
|
|
73
|
-
setTime(0);
|
|
74
|
-
},
|
|
75
|
-
}, clips.map((clip, clipIndex) => h('option', {
|
|
76
|
-
key: clipIndex,
|
|
77
|
-
value: clipIndex,
|
|
78
|
-
}, clip.name || t('animationFallback', { n: clipIndex + 1 }))))
|
|
79
|
-
: h('span', { className: 'dsh3dv-anim-name', title: animationLabel }, animationLabel),
|
|
80
|
-
h('input', {
|
|
81
|
-
type: 'range',
|
|
82
|
-
className: 'dsh3dv-progress',
|
|
83
|
-
min: 0,
|
|
84
|
-
max: Math.max(animationMax, 0.01),
|
|
85
|
-
step: 0.01,
|
|
86
|
-
value: currentSeconds,
|
|
87
|
-
'aria-label': t('animationProgress'),
|
|
88
|
-
onChange: (event) => {
|
|
89
|
-
const next = Number(event.currentTarget.value);
|
|
90
|
-
setTime(next);
|
|
91
|
-
view.seekAnimation(next);
|
|
92
|
-
},
|
|
93
|
-
}),
|
|
94
|
-
h('span', { className: 'dsh3dv-anim-time' }, timeLabel));
|
|
95
|
-
}
|