three-cad-viewer 4.3.4 → 4.3.6

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 (59) hide show
  1. package/dist/scene/clipping.d.ts +6 -0
  2. package/dist/three-cad-viewer.esm.js +20 -5
  3. package/dist/three-cad-viewer.esm.js.map +1 -1
  4. package/dist/three-cad-viewer.esm.min.js +1 -1
  5. package/dist/three-cad-viewer.js +20 -5
  6. package/dist/three-cad-viewer.min.js +1 -1
  7. package/package.json +2 -3
  8. package/src/_version.ts +0 -1
  9. package/src/camera/camera.ts +0 -445
  10. package/src/camera/controls/CADOrbitControls.ts +0 -241
  11. package/src/camera/controls/CADTrackballControls.ts +0 -598
  12. package/src/camera/controls.ts +0 -380
  13. package/src/core/patches.ts +0 -16
  14. package/src/core/studio-manager.ts +0 -652
  15. package/src/core/types.ts +0 -892
  16. package/src/core/viewer-state.ts +0 -784
  17. package/src/core/viewer.ts +0 -4821
  18. package/src/index.ts +0 -151
  19. package/src/rendering/environment.ts +0 -840
  20. package/src/rendering/light-detection.ts +0 -327
  21. package/src/rendering/material-factory.ts +0 -735
  22. package/src/rendering/material-presets.ts +0 -289
  23. package/src/rendering/raycast.ts +0 -291
  24. package/src/rendering/room-environment.ts +0 -192
  25. package/src/rendering/studio-composer.ts +0 -577
  26. package/src/rendering/studio-floor.ts +0 -108
  27. package/src/rendering/texture-cache.ts +0 -324
  28. package/src/rendering/tree-model.ts +0 -542
  29. package/src/rendering/triplanar.ts +0 -329
  30. package/src/scene/animation.ts +0 -343
  31. package/src/scene/axes.ts +0 -108
  32. package/src/scene/bbox.ts +0 -223
  33. package/src/scene/clipping.ts +0 -640
  34. package/src/scene/grid.ts +0 -864
  35. package/src/scene/nestedgroup.ts +0 -1444
  36. package/src/scene/objectgroup.ts +0 -866
  37. package/src/scene/orientation.ts +0 -259
  38. package/src/scene/render-shape.ts +0 -634
  39. package/src/tools/cad_tools/measure.ts +0 -811
  40. package/src/tools/cad_tools/select.ts +0 -100
  41. package/src/tools/cad_tools/tools.ts +0 -231
  42. package/src/tools/cad_tools/ui.ts +0 -454
  43. package/src/tools/cad_tools/zebra.ts +0 -369
  44. package/src/types/html.d.ts +0 -5
  45. package/src/types/n8ao.d.ts +0 -28
  46. package/src/types/three-augmentation.d.ts +0 -60
  47. package/src/ui/display.ts +0 -3295
  48. package/src/ui/index.html +0 -505
  49. package/src/ui/info.ts +0 -177
  50. package/src/ui/slider.ts +0 -206
  51. package/src/ui/toolbar.ts +0 -347
  52. package/src/ui/treeview.ts +0 -945
  53. package/src/utils/decode-instances.ts +0 -233
  54. package/src/utils/font.ts +0 -60
  55. package/src/utils/gpu-tracker.ts +0 -265
  56. package/src/utils/logger.ts +0 -92
  57. package/src/utils/sizeof.ts +0 -116
  58. package/src/utils/timer.ts +0 -69
  59. package/src/utils/utils.ts +0 -446
@@ -1,840 +0,0 @@
1
- import * as THREE from "three";
2
- import { CleanRoomEnvironment } from "./room-environment.js";
3
- import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
4
- import type { StudioEnvironment, StudioBackground } from "../core/types.js";
5
- import { gpuTracker } from "../utils/gpu-tracker.js";
6
- import { logger } from "../utils/logger.js";
7
- import {
8
- detectDominantLights,
9
- getDefaultLights,
10
- type LightDetectionResult,
11
- } from "./light-detection.js";
12
-
13
- /**
14
- * Neutral grey background color for Studio mode.
15
- * A medium grey that works for both light and dark objects, and provides
16
- * a sensible backdrop for transmission/glass materials.
17
- */
18
- const STUDIO_BACKGROUND_GREY = new THREE.Color(0.18, 0.18, 0.18);
19
-
20
- /** Scratch vector for per-frame getDrawingBufferSize() calls. */
21
- const _bgSizeVec = new THREE.Vector2();
22
-
23
- /** Dark grey background for high-contrast viewing of light materials. */
24
- const STUDIO_BACKGROUND_DARKGREY = new THREE.Color(0.03, 0.03, 0.03);
25
-
26
- /** White background color for clean product shots. */
27
- const STUDIO_BACKGROUND_WHITE = new THREE.Color(1.0, 1.0, 1.0);
28
-
29
- /**
30
- * Create a radial vignette gradient texture (512×512 canvas).
31
- * Light center fading to darker edges.
32
- */
33
- function _createGradientTexture(centerColor: string, edgeColor: string): THREE.Texture {
34
- const size = 512;
35
- const canvas = document.createElement("canvas");
36
- canvas.width = size;
37
- canvas.height = size;
38
- const ctx = canvas.getContext("2d")!;
39
-
40
- const cx = size / 2;
41
- const cy = size / 2;
42
- const radius = size * 0.7;
43
- const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
44
- gradient.addColorStop(0, centerColor);
45
- gradient.addColorStop(1, edgeColor);
46
-
47
- ctx.fillStyle = edgeColor;
48
- ctx.fillRect(0, 0, size, size);
49
- ctx.fillStyle = gradient;
50
- ctx.fillRect(0, 0, size, size);
51
-
52
- const texture = new THREE.CanvasTexture(canvas);
53
- texture.colorSpace = THREE.SRGBColorSpace;
54
- return texture;
55
- }
56
-
57
- /** Lazy-cached gradient textures */
58
- let _gradientTexture: THREE.Texture | null = null;
59
- let _gradientDarkTexture: THREE.Texture | null = null;
60
-
61
- function _getGradientTexture(): THREE.Texture {
62
- if (!_gradientTexture) _gradientTexture = _createGradientTexture("#f0f0f0", "#c8c8c8");
63
- return _gradientTexture;
64
- }
65
-
66
- function _getGradientDarkTexture(): THREE.Texture {
67
- if (!_gradientDarkTexture) _gradientDarkTexture = _createGradientTexture("#808080", "#606060");
68
- return _gradientDarkTexture;
69
- }
70
-
71
- /**
72
- * Poly Haven CDN base URL for HDR files.
73
- * Pattern: {BASE}/{resolution}/{slug}_{resolution}.hdr
74
- */
75
- const PH_BASE = "https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr";
76
-
77
- /**
78
- * Default HDR preset slugs (Poly Haven asset names).
79
- *
80
- * All maps from Poly Haven (CC0 license, permissive CORS).
81
- * Resolution is selected at runtime via `use4kEnvMaps`.
82
- * Host applications can override URLs via the `presetUrls` constructor option.
83
- *
84
- * Curated for CAD/product visualization: neutral white studios that make
85
- * every material look good, plus two overcast outdoor options for context.
86
- */
87
- const DEFAULT_PRESET_SLUGS: string[] = [
88
- // --- Studio: Soft / neutral ---
89
- "studio_small_08",
90
- "studio_small_03",
91
- "white_studio_05",
92
- "white_studio_03",
93
- "photo_studio_01",
94
- "studio_small_09",
95
- "cyclorama_hard_light",
96
-
97
- // --- Outdoor ---
98
- "canary_wharf",
99
- "kiara_1_dawn",
100
- "empty_warehouse_01",
101
- "san_giuseppe_bridge",
102
- ];
103
-
104
- /** Build preset URL map for the given resolution tier. */
105
- function _buildPresetUrls(use4k: boolean): Record<string, string> {
106
- const res = use4k ? "4k" : "2k";
107
- const urls: Record<string, string> = {};
108
- for (const slug of DEFAULT_PRESET_SLUGS) {
109
- urls[slug] = `${PH_BASE}/${res}/${slug}_${res}.hdr`;
110
- }
111
- return urls;
112
- }
113
-
114
- /**
115
- * Configuration options for EnvironmentManager.
116
- */
117
- interface EnvironmentManagerOptions {
118
- /** Override URLs for the "neutral" and "outdoor" HDR presets */
119
- presetUrls?: Partial<Record<string, string>>;
120
- }
121
-
122
- /**
123
- * Manages environment maps for Studio mode.
124
- *
125
- * Handles three tiers of environment sources:
126
- * - **Tier 1 "studio"**: Procedural RoomEnvironment (bundled, zero network)
127
- * - **Tier 2 "neutral"/"outdoor"**: HDR presets loaded from configurable CDN URLs
128
- * - **Tier 3 custom URL**: User-provided HDR URL (same loading path as Tier 2)
129
- *
130
- * The environment map is used for IBL (image-based lighting) via
131
- * `scene.environment`. The scene background is configurable via the
132
- * `backgroundMode` parameter in `apply()` (grey, white, gradient,
133
- * blurred environment, or transparent).
134
- *
135
- * Features:
136
- * - PMREM generation and caching for all tiers
137
- * - In-flight promise deduplication (prevents duplicate loads on rapid switching)
138
- * - Lazy PMREMGenerator creation
139
- * - Fallback to "studio" on HDR load failure
140
- * - GPU resource tracking via gpuTracker
141
- */
142
- class EnvironmentManager {
143
- /** Cached PMREM render targets keyed by environment name or URL */
144
- private _cache: Map<string, THREE.WebGLRenderTarget> = new Map();
145
-
146
- /** Cached light detection results keyed by environment name or URL */
147
- private _lightDetectionCache: Map<string, LightDetectionResult> = new Map();
148
-
149
- /** In-flight load promises keyed by environment name or URL */
150
- private _inflight: Map<string, Promise<THREE.Texture>> = new Map();
151
-
152
- /** Lazily-created PMREMGenerator instance */
153
- private _pmremGenerator: THREE.PMREMGenerator | null = null;
154
-
155
- /** Resolved preset URLs (defaults merged with user overrides) */
156
- private _presetUrls: Record<string, string>;
157
-
158
- /** Whether 4K env maps are enabled (default false = 2K) */
159
- private _use4k: boolean = false;
160
-
161
- /** User-provided URL overrides from constructor */
162
- private _userOverrides: Record<string, string> = {};
163
-
164
- /** HDRLoader instance (created lazily on first HDR load) */
165
- private _hdrLoader: HDRLoader | null = null;
166
-
167
- /** The last loaded PMREM texture (stateful — used by apply() for IBL) */
168
- private _currentTexture: THREE.Texture | null = null;
169
-
170
- /** Whether this manager has been disposed */
171
- private _disposed = false;
172
-
173
- /**
174
- * Ortho env background workaround.
175
- *
176
- * Three.js cannot render PMREM/cubemap textures as scene.background with
177
- * orthographic cameras (renders as a tiny rectangle). We work around this by
178
- * rendering the env map to a render target using a virtual perspective camera,
179
- * then setting that 2D texture as scene.background. A 2D texture background
180
- * renders as a fullscreen quad regardless of camera projection, and the
181
- * transmission pass (glass refraction) also sees it correctly.
182
- */
183
- private _bgScene: THREE.Scene | null = null;
184
- private _bgCamera: THREE.PerspectiveCamera | null = null;
185
- private _bgRenderTarget: THREE.WebGLRenderTarget | null = null;
186
- private _orthoEnvMainScene: THREE.Scene | null = null;
187
-
188
- /** Whether the env background feature is active (ortho + environment background). */
189
- private _envBackgroundActive: boolean = false;
190
-
191
- /**
192
- * Deferred-apply state: if apply() was called with backgroundMode "environment"
193
- * while _currentTexture was null, store the arguments so loadEnvironment() can
194
- * re-apply once the texture is ready.
195
- */
196
- private _deferredApply: {
197
- scene: THREE.Scene;
198
- envIntensity: number;
199
- upIsZ: boolean;
200
- ortho: boolean;
201
- } | null = null;
202
-
203
- constructor(options: EnvironmentManagerOptions = {}) {
204
- this._userOverrides = (options.presetUrls as Record<string, string> | undefined) ?? {};
205
- this._presetUrls = {
206
- ..._buildPresetUrls(false),
207
- ...this._userOverrides,
208
- };
209
- }
210
-
211
- /**
212
- * Load or retrieve an environment map.
213
- *
214
- * Resolves the environment name to a loading strategy:
215
- * - `"studio"` -- procedural RoomEnvironment via PMREMGenerator.fromScene()
216
- * - `"neutral"` / `"outdoor"` -- HDR preset from configured CDN URL
217
- * - `"none"` -- returns null (caller should call `remove()` instead)
218
- * - Any other string -- treated as a custom HDR URL
219
- *
220
- * Results are cached. If a load is already in flight for the same key,
221
- * the existing promise is returned (no duplicate loads).
222
- *
223
- * @param name - Environment preset name or custom HDR URL
224
- * @param renderer - WebGL renderer (needed for PMREMGenerator)
225
- * @returns PMREM texture, or null for "none"
226
- */
227
- async loadEnvironment(
228
- name: StudioEnvironment | string,
229
- renderer: THREE.WebGLRenderer,
230
- ): Promise<THREE.Texture | null> {
231
- if (this._disposed) {
232
- logger.warn("EnvironmentManager.loadEnvironment() called after dispose");
233
- return null;
234
- }
235
-
236
- if (name === "none") {
237
- this._currentTexture = null;
238
- return null;
239
- }
240
-
241
- // Check cache first (name is the cache key for presets; URL string for custom)
242
- const cacheKey = name;
243
- const cached = this._cache.get(cacheKey);
244
- if (cached) {
245
- logger.debug(`Environment "${cacheKey}" loaded from cache`);
246
- this._currentTexture = cached.texture;
247
- return cached.texture;
248
- }
249
-
250
- // Check in-flight promise — await and set _currentTexture
251
- const inflight = this._inflight.get(cacheKey);
252
- if (inflight) {
253
- logger.debug(`Environment "${cacheKey}" already loading, reusing promise`);
254
- const texture = await inflight;
255
- this._currentTexture = texture;
256
- return texture;
257
- }
258
-
259
- // Start new load
260
- const promise = this._load(name, renderer);
261
- this._inflight.set(cacheKey, promise);
262
-
263
- try {
264
- const texture = await promise;
265
- this._currentTexture = texture;
266
-
267
- // Self-healing: if apply() was called with "environment" background
268
- // while texture was null, re-apply now that the texture is ready.
269
- if (this._deferredApply) {
270
- const { scene, envIntensity, upIsZ, ortho } = this._deferredApply;
271
- this._deferredApply = null;
272
- this.apply(scene, envIntensity, "environment", upIsZ, ortho);
273
- }
274
-
275
- return texture;
276
- } finally {
277
- this._inflight.delete(cacheKey);
278
- }
279
- }
280
-
281
- /**
282
- * Apply the current environment map to the scene.
283
- *
284
- * Sets `scene.environment` for PBR/IBL reflections and configures
285
- * `scene.background` according to the selected background mode:
286
- * - `"grey"`: Neutral grey color (default, clean product-shot look)
287
- * - `"white"`: Pure white background (e-commerce / documentation style)
288
- * - `"gradient"`: Radial vignette gradient (light grey center → darker edges)
289
- * - `"environment"`: Blurred, dimmed PMREM environment as backdrop
290
- * (color-matched to IBL, eliminates edge-glow artifacts on reflective objects)
291
- * - `"transparent"`: No background (canvas alpha shows through)
292
- *
293
- * @param scene - The Three.js scene to apply the environment to
294
- * @param envIntensity - Environment intensity multiplier (0-3, default 1.0)
295
- * @param backgroundMode - Background mode
296
- * @param upIsZ - Whether the scene uses Z-up coordinates (default true)
297
- * @param ortho - Whether the camera is orthographic (env background falls back to gradient)
298
- * @param envRotationDeg - Environment map rotation in degrees (default 0)
299
- */
300
- apply(
301
- scene: THREE.Scene,
302
- envIntensity: number,
303
- backgroundMode: StudioBackground = "grey",
304
- upIsZ: boolean = true,
305
- ortho: boolean = false,
306
- envRotationDeg: number = 0,
307
- ): void {
308
- const rotY = (envRotationDeg * Math.PI) / 180;
309
- if (this._currentTexture) {
310
- scene.environment = this._currentTexture;
311
- scene.environmentIntensity = envIntensity;
312
- // HDR maps assume Y-up; rotate 90° around X to align with Z-up scenes.
313
- // Additional rotation for user-controlled azimuthal rotation.
314
- if (upIsZ) {
315
- scene.environmentRotation.set(Math.PI / 2, 0, rotY);
316
- } else {
317
- scene.environmentRotation.set(0, rotY, 0);
318
- }
319
- } else {
320
- scene.environment = null;
321
- scene.environmentIntensity = 1.0;
322
- scene.environmentRotation.set(0, 0, 0);
323
- }
324
-
325
- // Clear deferred-apply if switching away from "environment" background
326
- if (backgroundMode !== "environment") {
327
- this._deferredApply = null;
328
- }
329
-
330
- // Configure background based on mode
331
- switch (backgroundMode) {
332
- case "white":
333
- scene.background = STUDIO_BACKGROUND_WHITE;
334
- scene.backgroundIntensity = 1.0;
335
- scene.backgroundBlurriness = 0;
336
- this._teardownEnvBackground();
337
- break;
338
- case "gradient":
339
- scene.background = _getGradientTexture();
340
- scene.backgroundIntensity = 1.0;
341
- scene.backgroundBlurriness = 0;
342
- this._teardownEnvBackground();
343
- break;
344
- case "gradient-dark":
345
- scene.background = _getGradientDarkTexture();
346
- scene.backgroundIntensity = 1.0;
347
- scene.backgroundBlurriness = 0;
348
- this._teardownEnvBackground();
349
- break;
350
- case "environment":
351
- if (this._currentTexture) {
352
- // Always use render-to-texture with a fixed-FOV bgCamera so the
353
- // background zoom level is identical in perspective and ortho modes.
354
- this._setupEnvBackground(scene, this._currentTexture, upIsZ, rotY);
355
- this._deferredApply = null;
356
- } else {
357
- // No environment loaded — fall back to grey.
358
- // Record deferred-apply so loadEnvironment() can re-apply once ready.
359
- this._deferredApply = { scene, envIntensity, upIsZ, ortho };
360
- scene.background = STUDIO_BACKGROUND_GREY;
361
- scene.backgroundIntensity = 1.0;
362
- scene.backgroundBlurriness = 0;
363
- this._teardownEnvBackground();
364
- }
365
- break;
366
- case "transparent":
367
- scene.background = null;
368
- scene.backgroundIntensity = 1.0;
369
- scene.backgroundBlurriness = 0;
370
- this._teardownEnvBackground();
371
- break;
372
- case "darkgrey":
373
- scene.background = STUDIO_BACKGROUND_DARKGREY;
374
- scene.backgroundIntensity = 1.0;
375
- scene.backgroundBlurriness = 0;
376
- this._teardownEnvBackground();
377
- break;
378
- case "grey":
379
- default:
380
- scene.background = STUDIO_BACKGROUND_GREY;
381
- scene.backgroundIntensity = 1.0;
382
- scene.backgroundBlurriness = 0;
383
- this._teardownEnvBackground();
384
- break;
385
- }
386
-
387
- }
388
-
389
- /**
390
- * Remove environment map from the scene.
391
- *
392
- * Clears `scene.environment`, `scene.background`, and resets
393
- * environment/background properties to defaults.
394
- *
395
- * @param scene - The Three.js scene to clear
396
- */
397
- remove(scene: THREE.Scene): void {
398
- this._deferredApply = null;
399
- this._teardownEnvBackground();
400
- scene.environment = null;
401
- scene.background = null;
402
- scene.environmentIntensity = 1.0;
403
- scene.environmentRotation.set(0, 0, 0);
404
- scene.backgroundIntensity = 1.0;
405
- scene.backgroundBlurriness = 0;
406
- scene.backgroundRotation.set(0, 0, 0);
407
- }
408
-
409
- /**
410
- * Switch between 2K and 4K environment map resolution.
411
- *
412
- * Rebuilds preset URLs, evicts cached HDR presets (so they reload at
413
- * the new resolution), and reloads the current environment if one is
414
- * active.
415
- *
416
- * @param use4k - True for 4K, false for 2K
417
- * @param currentEnvName - The currently active environment name (to reload)
418
- * @param renderer - WebGL renderer (needed for reload)
419
- * @returns Promise that resolves when the new texture is ready
420
- */
421
- async setUse4kEnvMaps(
422
- use4k: boolean,
423
- currentEnvName: string,
424
- renderer: THREE.WebGLRenderer,
425
- ): Promise<THREE.Texture | null> {
426
- if (use4k === this._use4k) return this._currentTexture;
427
- this._use4k = use4k;
428
-
429
- // Rebuild preset URLs at the new resolution
430
- this._presetUrls = {
431
- ..._buildPresetUrls(use4k),
432
- ...this._userOverrides,
433
- };
434
-
435
- // Evict cached HDR presets (they point to the old resolution).
436
- // "studio" (RoomEnvironment) is procedural and unaffected.
437
- for (const slug of DEFAULT_PRESET_SLUGS) {
438
- const cached = this._cache.get(slug);
439
- if (cached) {
440
- gpuTracker.untrack("texture", cached.texture);
441
- cached.dispose();
442
- this._cache.delete(slug);
443
- this._lightDetectionCache.delete(slug);
444
- logger.debug(`Evicted cached environment "${slug}" for resolution switch`);
445
- }
446
- }
447
-
448
- // Reload the current environment at the new resolution
449
- if (currentEnvName && currentEnvName !== "none" && currentEnvName !== "studio") {
450
- return this.loadEnvironment(currentEnvName, renderer);
451
- }
452
-
453
- return this._currentTexture;
454
- }
455
-
456
- /** Whether 4K env maps are currently enabled. */
457
- get use4kEnvMaps(): boolean {
458
- return this._use4k;
459
- }
460
-
461
- /**
462
- * Whether an environment name is a Poly Haven preset (resolution-switchable).
463
- * Returns false for "studio", "none", and custom URLs.
464
- */
465
- isPreset(name: string): boolean {
466
- return DEFAULT_PRESET_SLUGS.includes(name);
467
- }
468
-
469
- /**
470
- * Whether the render-to-texture env background path is currently active.
471
- * When true, the caller must call updateEnvBackground() each frame.
472
- */
473
- get isEnvBackgroundActive(): boolean {
474
- return this._envBackgroundActive;
475
- }
476
-
477
- /**
478
- * Get cached light detection result for an environment.
479
- *
480
- * @param envName - Environment name or URL (same key used in loadEnvironment)
481
- * @returns Detection result, or null if not yet analyzed
482
- */
483
- getLightDetection(envName: string): LightDetectionResult | null {
484
- return this._lightDetectionCache.get(envName) ?? null;
485
- }
486
-
487
- /**
488
- * Update the env background render target (ortho camera workaround).
489
- *
490
- * Renders the PMREM env map to a 2D render target using a fixed-FOV virtual
491
- * perspective camera whose quaternion is synced with the main camera. The
492
- * resulting 2D texture is set as the main scene's background, giving a
493
- * world-space environment that tracks camera orbit — matching how
494
- * scene.environment (IBL reflections) already behaves.
495
- *
496
- * Called every frame from the render loop when isEnvBackgroundActive is true.
497
- * Only active in ortho mode (perspective uses native cubemap background).
498
- *
499
- * @param renderer - WebGL renderer
500
- * @param mainCamera - The active camera whose orientation to match
501
- */
502
- updateEnvBackground(
503
- renderer: THREE.WebGLRenderer,
504
- mainCamera?: THREE.Camera,
505
- ): void {
506
- if (
507
- !this._envBackgroundActive ||
508
- !this._bgScene ||
509
- !this._bgCamera ||
510
- !this._orthoEnvMainScene
511
- ) {
512
- return;
513
- }
514
-
515
- // Match viewport size for the render target
516
- const size = renderer.getDrawingBufferSize(_bgSizeVec);
517
- const w = size.x;
518
- const h = size.y;
519
-
520
- if (!this._bgRenderTarget || this._bgRenderTarget.width !== w || this._bgRenderTarget.height !== h) {
521
- this._bgRenderTarget?.dispose();
522
- this._bgRenderTarget = new THREE.WebGLRenderTarget(w, h);
523
- }
524
-
525
- // Match viewport aspect ratio
526
- const aspect = w / h;
527
- if (this._bgCamera.aspect !== aspect) {
528
- this._bgCamera.aspect = aspect;
529
- this._bgCamera.updateProjectionMatrix();
530
- }
531
-
532
- // Sync bgCamera orientation with the main camera so the background
533
- // rotates with orbit, matching the world-space IBL reflections.
534
- if (mainCamera) {
535
- this._bgCamera.quaternion.copy(mainCamera.quaternion);
536
- }
537
-
538
- // Render env background to the render target
539
- renderer.setRenderTarget(this._bgRenderTarget);
540
- renderer.clear();
541
- renderer.render(this._bgScene, this._bgCamera);
542
- renderer.setRenderTarget(null);
543
-
544
- // Set the 2D texture as the main scene's background
545
- this._orthoEnvMainScene.background = this._bgRenderTarget.texture;
546
- this._orthoEnvMainScene.backgroundIntensity = 1.0;
547
- this._orthoEnvMainScene.backgroundBlurriness = 0;
548
- }
549
-
550
- /**
551
- * Dispose all cached resources.
552
- *
553
- * Disposes all cached PMREM render targets (and their textures) and
554
- * the PMREMGenerator. After disposal, this manager cannot be used again.
555
- *
556
- * Call this on `viewer.dispose()`, NOT on `viewer.clear()` --
557
- * the EnvironmentManager survives shape data clearing because
558
- * environments are independent of shape data.
559
- */
560
- dispose(): void {
561
- this._disposed = true;
562
- this._currentTexture = null;
563
- this._deferredApply = null;
564
- this._teardownEnvBackground();
565
- this._bgScene = null;
566
- this._bgCamera = null;
567
- if (this._bgRenderTarget) {
568
- this._bgRenderTarget.dispose();
569
- this._bgRenderTarget = null;
570
- }
571
-
572
- // Dispose all cached PMREM render targets (disposes textures too)
573
- for (const [key, renderTarget] of this._cache) {
574
- gpuTracker.untrack("texture", renderTarget.texture);
575
- renderTarget.dispose();
576
- logger.debug(`Disposed cached environment render target: ${key}`);
577
- }
578
- this._cache.clear();
579
- this._lightDetectionCache.clear();
580
-
581
- // Clear in-flight promises (they'll resolve but won't be cached)
582
- this._inflight.clear();
583
-
584
- // Dispose PMREMGenerator
585
- if (this._pmremGenerator) {
586
- this._pmremGenerator.dispose();
587
- this._pmremGenerator = null;
588
- logger.debug("Disposed PMREMGenerator");
589
- }
590
-
591
- // Null HDR loader reference (no explicit dispose needed)
592
- this._hdrLoader = null;
593
-
594
- // Dispose module-level gradient textures
595
- disposeGradientTextures();
596
- }
597
-
598
- // ---------------------------------------------------------------------------
599
- // Private helpers
600
- // ---------------------------------------------------------------------------
601
-
602
- /**
603
- * Set up the env background: a separate scene with the PMREM texture
604
- * as background and a fixed-FOV virtual perspective camera for rendering
605
- * to a 2D target. Used only for ortho cameras (perspective uses native
606
- * cubemap background).
607
- */
608
- private _setupEnvBackground(
609
- mainScene: THREE.Scene,
610
- texture: THREE.Texture,
611
- upIsZ: boolean,
612
- rotY: number = 0,
613
- ): void {
614
- if (!this._bgScene) {
615
- this._bgScene = new THREE.Scene();
616
- }
617
- if (!this._bgCamera) {
618
- this._bgCamera = new THREE.PerspectiveCamera(50, 1, 0.1, 10);
619
- }
620
-
621
- // bgCamera orientation is synced with the main camera in
622
- // updateEnvBackground() — no fixed rotation needed here.
623
-
624
- this._bgScene.background = texture;
625
- this._bgScene.backgroundIntensity = 1.0;
626
- this._bgScene.backgroundBlurriness = 0;
627
- if (upIsZ) {
628
- this._bgScene.backgroundRotation.set(Math.PI / 2, 0, rotY);
629
- } else {
630
- this._bgScene.backgroundRotation.set(0, rotY, 0);
631
- }
632
-
633
- this._orthoEnvMainScene = mainScene;
634
- this._envBackgroundActive = true;
635
- }
636
-
637
- /**
638
- * Tear down the env background state.
639
- */
640
- private _teardownEnvBackground(): void {
641
- this._envBackgroundActive = false;
642
- this._orthoEnvMainScene = null;
643
- }
644
-
645
- /**
646
- * Resolve environment name to an HDR URL, if applicable.
647
- *
648
- * Returns null for "studio" (uses RoomEnvironment, no URL).
649
- * Returns the preset URL for "neutral"/"outdoor".
650
- * Returns the name itself for custom URLs.
651
- */
652
- private _resolveUrl(name: string): string | null {
653
- if (name === "studio") {
654
- return null;
655
- }
656
- const presetUrl = this._presetUrls[name];
657
- if (presetUrl) {
658
- return presetUrl;
659
- }
660
- return name;
661
- }
662
-
663
- /**
664
- * Get or create the PMREMGenerator (lazy initialization).
665
- */
666
- private _ensurePmremGenerator(
667
- renderer: THREE.WebGLRenderer,
668
- ): THREE.PMREMGenerator {
669
- if (!this._pmremGenerator) {
670
- this._pmremGenerator = new THREE.PMREMGenerator(renderer);
671
- logger.debug("Created PMREMGenerator");
672
- }
673
- return this._pmremGenerator;
674
- }
675
-
676
- /**
677
- * Get or create the HDRLoader (lazy initialization).
678
- */
679
- private _ensureHdrLoader(): HDRLoader {
680
- if (!this._hdrLoader) {
681
- this._hdrLoader = new HDRLoader();
682
- logger.debug("Created HDRLoader");
683
- }
684
- return this._hdrLoader;
685
- }
686
-
687
- /**
688
- * Internal load dispatcher.
689
- *
690
- * Routes to RoomEnvironment generation or HDR loading based on the name.
691
- * On HDR failure, falls back to "studio" (RoomEnvironment).
692
- */
693
- private async _load(
694
- name: string,
695
- renderer: THREE.WebGLRenderer,
696
- ): Promise<THREE.Texture> {
697
- if (name === "studio") {
698
- return this._loadRoomEnvironment(name, renderer);
699
- }
700
-
701
- const url = this._resolveUrl(name)!;
702
-
703
- try {
704
- return await this._loadHdr(url, name, renderer);
705
- } catch (error) {
706
- if (this._disposed) throw error;
707
- const displayName =
708
- name in this._presetUrls ? name : `custom URL (${url})`;
709
- logger.warn(
710
- `Could not load environment "${displayName}", using default "studio" environment.`,
711
- error instanceof Error ? error.message : error,
712
- );
713
- // Fall back to RoomEnvironment
714
- return this._loadRoomEnvironment("studio", renderer);
715
- }
716
- }
717
-
718
- /**
719
- * Generate PMREM texture from the procedural RoomEnvironment.
720
- *
721
- * This is synchronous (no network), fast (~70ms), and always available.
722
- */
723
- private _loadRoomEnvironment(
724
- cacheKey: string,
725
- renderer: THREE.WebGLRenderer,
726
- ): THREE.Texture {
727
- if (this._disposed) {
728
- throw new Error("EnvironmentManager was disposed");
729
- }
730
-
731
- // Check cache again (fallback path may re-enter with "studio" key)
732
- const cached = this._cache.get(cacheKey);
733
- if (cached) {
734
- return cached.texture;
735
- }
736
-
737
- const pmremGenerator = this._ensurePmremGenerator(renderer);
738
- const roomScene = new CleanRoomEnvironment();
739
- const renderTarget = pmremGenerator.fromScene(roomScene, 0, 0.1, 100, {
740
- size: 2048,
741
- });
742
-
743
- // Dispose the intermediate scene (not needed after PMREM generation)
744
- roomScene.traverse((child) => {
745
- if (child instanceof THREE.Mesh) {
746
- child.geometry.dispose();
747
- if (Array.isArray(child.material)) {
748
- child.material.forEach((m) => m.dispose());
749
- } else {
750
- child.material.dispose();
751
- }
752
- }
753
- });
754
-
755
- // Cache render target and track its texture
756
- this._cache.set(cacheKey, renderTarget);
757
- gpuTracker.trackTexture(renderTarget.texture, `PMREM environment: ${cacheKey}`);
758
-
759
- // Cache default light detection for procedural environment
760
- this._lightDetectionCache.set(cacheKey, getDefaultLights());
761
-
762
- logger.debug(`Generated RoomEnvironment PMREM, cached as "${cacheKey}"`);
763
-
764
- return renderTarget.texture;
765
- }
766
-
767
- /**
768
- * Load an HDR file and generate a PMREM texture from it.
769
- *
770
- * Uses HDRLoader to fetch the .hdr file, then PMREMGenerator.fromEquirectangular()
771
- * to create the PMREM cubemap. The source equirectangular texture is disposed
772
- * after PMREM generation. The PMREM texture itself serves as both the IBL
773
- * environment and the background (in "environment" mode).
774
- *
775
- * @param url - URL of the .hdr file
776
- * @param cacheKey - Cache key for the resulting PMREM render target
777
- * @param renderer - WebGL renderer for PMREMGenerator
778
- * @returns PMREM texture
779
- * @throws If the HDR file cannot be loaded
780
- */
781
- private async _loadHdr(
782
- url: string,
783
- cacheKey: string,
784
- renderer: THREE.WebGLRenderer,
785
- ): Promise<THREE.Texture> {
786
- const loader = this._ensureHdrLoader();
787
- const pmremGenerator = this._ensurePmremGenerator(renderer);
788
-
789
- logger.debug(`Loading HDR environment from: ${url}`);
790
-
791
- // Load HDR file with timeout (30 seconds) to prevent indefinite blocking
792
- const hdrTexture = await Promise.race([
793
- loader.loadAsync(url),
794
- new Promise<never>((_, reject) =>
795
- setTimeout(() => reject(new Error(`HDR load timed out after 30 s: ${url}`)), 30_000),
796
- ),
797
- ]);
798
-
799
- // Bail out if disposed while loading
800
- if (this._disposed) {
801
- hdrTexture.dispose();
802
- throw new Error("EnvironmentManager was disposed during HDR load");
803
- }
804
-
805
- // Generate PMREM cubemap from equirectangular HDR
806
- const renderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
807
-
808
- // Analyze HDR pixel data for dominant light sources BEFORE disposing.
809
- // hdrTexture.image.data is Uint16Array (HalfFloatType) from HDRLoader.
810
- if (hdrTexture.image?.data && hdrTexture.image.width && hdrTexture.image.height) {
811
- const result = detectDominantLights(
812
- hdrTexture.image.data as Uint16Array,
813
- hdrTexture.image.width as number,
814
- hdrTexture.image.height as number,
815
- );
816
- this._lightDetectionCache.set(cacheKey, result);
817
- }
818
-
819
- // Dispose the source equirectangular texture (PMREM is now in GPU memory).
820
- hdrTexture.dispose();
821
-
822
- // Cache render target and track its texture
823
- this._cache.set(cacheKey, renderTarget);
824
- gpuTracker.trackTexture(renderTarget.texture, `PMREM environment: ${cacheKey}`);
825
- logger.debug(
826
- `Loaded HDR environment from "${url}", cached as "${cacheKey}"`,
827
- );
828
-
829
- return renderTarget.texture;
830
- }
831
- }
832
-
833
- /** Dispose lazy-cached gradient textures (called from EnvironmentManager.dispose). */
834
- function disposeGradientTextures(): void {
835
- if (_gradientTexture) { _gradientTexture.dispose(); _gradientTexture = null; }
836
- if (_gradientDarkTexture) { _gradientDarkTexture.dispose(); _gradientDarkTexture = null; }
837
- }
838
-
839
- export { EnvironmentManager, disposeGradientTextures };
840
- export type { EnvironmentManagerOptions };