@chocozhang/three-model-render 1.0.4 → 1.0.5

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/dist/index.mjs CHANGED
@@ -45,8 +45,8 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
45
45
  };
46
46
  }
47
47
  // Configuration
48
- const enableCache = (options === null || options === void 0 ? void 0 : options.enableCache) !== false;
49
- const updateInterval = (options === null || options === void 0 ? void 0 : options.updateInterval) || 0;
48
+ const enableCache = options?.enableCache !== false;
49
+ const updateInterval = options?.updateInterval || 0;
50
50
  // Create label container, absolute positioning, attached to body
51
51
  const container = document.createElement('div');
52
52
  container.style.position = 'absolute';
@@ -62,11 +62,10 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
62
62
  let lastUpdateTime = 0;
63
63
  // Traverse all child models
64
64
  parentModel.traverse((child) => {
65
- var _a;
66
65
  // Only process Mesh or Group
67
66
  if ((child.isMesh || child.type === 'Group')) {
68
67
  // Dynamic matching of name to prevent undefined
69
- const labelText = (_a = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))) === null || _a === void 0 ? void 0 : _a[1];
68
+ const labelText = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))?.[1];
70
69
  if (!labelText)
71
70
  return; // Skip if no matching label
72
71
  // Create DOM label
@@ -74,11 +73,11 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
74
73
  el.innerText = labelText;
75
74
  // Styles defined in JS, can be overridden via options
76
75
  el.style.position = 'absolute';
77
- el.style.color = (options === null || options === void 0 ? void 0 : options.color) || '#fff';
78
- el.style.background = (options === null || options === void 0 ? void 0 : options.background) || 'rgba(0,0,0,0.6)';
79
- el.style.padding = (options === null || options === void 0 ? void 0 : options.padding) || '4px 8px';
80
- el.style.borderRadius = (options === null || options === void 0 ? void 0 : options.borderRadius) || '4px';
81
- el.style.fontSize = (options === null || options === void 0 ? void 0 : options.fontSize) || '14px';
76
+ el.style.color = options?.color || '#fff';
77
+ el.style.background = options?.background || 'rgba(0,0,0,0.6)';
78
+ el.style.padding = options?.padding || '4px 8px';
79
+ el.style.borderRadius = options?.borderRadius || '4px';
80
+ el.style.fontSize = options?.fontSize || '14px';
82
81
  el.style.transform = 'translate(-50%, -100%)'; // Position label directly above the model
83
82
  el.style.whiteSpace = 'nowrap';
84
83
  el.style.pointerEvents = 'none';
@@ -459,6 +458,67 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
459
458
  };
460
459
  }
461
460
 
461
+ /**
462
+ * ResourceManager
463
+ * Handles tracking and disposal of Three.js objects to prevent memory leaks.
464
+ */
465
+ class ResourceManager {
466
+ constructor() {
467
+ this.geometries = new Set();
468
+ this.materials = new Set();
469
+ this.textures = new Set();
470
+ this.objects = new Set();
471
+ }
472
+ /**
473
+ * Track an object and its resources recursively
474
+ */
475
+ track(object) {
476
+ this.objects.add(object);
477
+ object.traverse((child) => {
478
+ if (child.isMesh) {
479
+ const mesh = child;
480
+ if (mesh.geometry)
481
+ this.geometries.add(mesh.geometry);
482
+ if (mesh.material) {
483
+ if (Array.isArray(mesh.material)) {
484
+ mesh.material.forEach(m => this.trackMaterial(m));
485
+ }
486
+ else {
487
+ this.trackMaterial(mesh.material);
488
+ }
489
+ }
490
+ }
491
+ });
492
+ return object;
493
+ }
494
+ trackMaterial(material) {
495
+ this.materials.add(material);
496
+ // Track textures in material
497
+ for (const value of Object.values(material)) {
498
+ if (value instanceof THREE.Texture) {
499
+ this.textures.add(value);
500
+ }
501
+ }
502
+ }
503
+ /**
504
+ * Dispose all tracked resources
505
+ */
506
+ dispose() {
507
+ this.geometries.forEach(g => g.dispose());
508
+ this.materials.forEach(m => m.dispose());
509
+ this.textures.forEach(t => t.dispose());
510
+ this.objects.forEach(obj => {
511
+ if (obj.parent) {
512
+ obj.parent.remove(obj);
513
+ }
514
+ });
515
+ this.geometries.clear();
516
+ this.materials.clear();
517
+ this.textures.clear();
518
+ this.objects.clear();
519
+ }
520
+ }
521
+
462
522
  /**
463
523
  * @file clickHandler.ts
464
524
  * @description
@@ -610,7 +670,6 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
610
670
  */
611
671
  class ArrowGuide {
612
672
  constructor(renderer, camera, scene, options) {
613
- var _a, _b, _c;
614
673
  this.renderer = renderer;
615
674
  this.camera = camera;
616
675
  this.scene = scene;
@@ -629,10 +688,10 @@ class ArrowGuide {
629
688
  // Config: Non-highlight opacity and brightness
630
689
  this.fadeOpacity = 0.5;
631
690
  this.fadeBrightness = 0.1;
632
- this.clickThreshold = (_a = options === null || options === void 0 ? void 0 : options.clickThreshold) !== null && _a !== void 0 ? _a : 10;
633
- this.ignoreRaycastNames = new Set((options === null || options === void 0 ? void 0 : options.ignoreRaycastNames) || []);
634
- this.fadeOpacity = (_b = options === null || options === void 0 ? void 0 : options.fadeOpacity) !== null && _b !== void 0 ? _b : 0.5;
635
- this.fadeBrightness = (_c = options === null || options === void 0 ? void 0 : options.fadeBrightness) !== null && _c !== void 0 ? _c : 0.1;
691
+ this.clickThreshold = options?.clickThreshold ?? 10;
692
+ this.ignoreRaycastNames = new Set(options?.ignoreRaycastNames || []);
693
+ this.fadeOpacity = options?.fadeOpacity ?? 0.5;
694
+ this.fadeBrightness = options?.fadeBrightness ?? 0.1;
636
695
  this.abortController = new AbortController();
637
696
  this.initEvents();
638
697
  }
@@ -922,12 +981,11 @@ class LiquidFillerGroup {
922
981
  this.abortController = new AbortController();
923
982
  const modelArray = Array.isArray(models) ? models : [models];
924
983
  modelArray.forEach(model => {
925
- var _a, _b, _c;
926
984
  try {
927
985
  const options = {
928
- color: (_a = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.color) !== null && _a !== void 0 ? _a : 0x00ff00,
929
- opacity: (_b = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.opacity) !== null && _b !== void 0 ? _b : 0.6,
930
- speed: (_c = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.speed) !== null && _c !== void 0 ? _c : 0.05,
986
+ color: defaultOptions?.color ?? 0x00ff00,
987
+ opacity: defaultOptions?.opacity ?? 0.6,
988
+ speed: defaultOptions?.speed ?? 0.05,
931
989
  };
932
990
  // Save original materials
933
991
  const originalMaterials = new Map();
@@ -1174,7 +1232,6 @@ const EASING_FUNCTIONS = {
1174
1232
  * - Robust error handling
1175
1233
  */
1176
1234
  function followModels(camera, targets, options = {}) {
1177
- var _a, _b, _c, _d, _e, _f;
1178
1235
  // Cancel previous animation
1179
1236
  cancelFollow(camera);
1180
1237
  // Boundary check
@@ -1201,14 +1258,14 @@ function followModels(camera, targets, options = {}) {
1201
1258
  box.getBoundingSphere(sphere);
1202
1259
  const center = sphere.center.clone();
1203
1260
  const radiusBase = Math.max(0.001, sphere.radius);
1204
- const duration = (_a = options.duration) !== null && _a !== void 0 ? _a : 700;
1205
- const padding = (_b = options.padding) !== null && _b !== void 0 ? _b : 1.0;
1261
+ const duration = options.duration ?? 700;
1262
+ const padding = options.padding ?? 1.0;
1206
1263
  const minDistance = options.minDistance;
1207
1264
  const maxDistance = options.maxDistance;
1208
- const controls = (_c = options.controls) !== null && _c !== void 0 ? _c : null;
1209
- const azimuth = (_d = options.azimuth) !== null && _d !== void 0 ? _d : Math.PI / 4;
1210
- const elevation = (_e = options.elevation) !== null && _e !== void 0 ? _e : Math.PI / 4;
1211
- const easing = (_f = options.easing) !== null && _f !== void 0 ? _f : 'easeOut';
1265
+ const controls = options.controls ?? null;
1266
+ const azimuth = options.azimuth ?? Math.PI / 4;
1267
+ const elevation = options.elevation ?? Math.PI / 4;
1268
+ const easing = options.easing ?? 'easeOut';
1212
1269
  const onProgress = options.onProgress;
1213
1270
  // Get easing function
1214
1271
  const easingFn = EASING_FUNCTIONS[easing] || EASING_FUNCTIONS.easeOut;
@@ -1346,9 +1403,6 @@ function setView(camera, controls, targetObj, position = 'front', options = {})
1346
1403
  console.warn('setView: Failed to calculate bounding box');
1347
1404
  return Promise.reject(new Error('Invalid bounding box'));
1348
1405
  }
1349
- const center = box.getCenter(new THREE.Vector3());
1350
- const size = box.getSize(new THREE.Vector3());
1351
- const maxSize = Math.max(size.x, size.y, size.z);
1352
1406
  // Use mapping table for creating view angles
1353
1407
  const viewAngles = {
1354
1408
  'front': { azimuth: 0, elevation: 0 },
@@ -1400,37 +1454,22 @@ const ViewPresets = {
1400
1454
  top: (camera, controls, target, options) => setView(camera, controls, target, 'top', options)
1401
1455
  };
1402
1456
 
1403
- /******************************************************************************
1404
- Copyright (c) Microsoft Corporation.
1405
-
1406
- Permission to use, copy, modify, and/or distribute this software for any
1407
- purpose with or without fee is hereby granted.
1408
-
1409
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1410
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1411
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1412
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1413
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1414
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1415
- PERFORMANCE OF THIS SOFTWARE.
1416
- ***************************************************************************** */
1417
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
1418
-
1419
-
1420
- function __awaiter(thisArg, _arguments, P, generator) {
1421
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
1422
- return new (P || (P = Promise))(function (resolve, reject) {
1423
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1424
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1425
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
1426
- step((generator = generator.apply(thisArg, _arguments || [])).next());
1427
- });
1428
- }
1429
-
1430
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
1431
- var e = new Error(message);
1432
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1457
+ let globalConfig = {
1458
+ dracoDecoderPath: '/draco/',
1459
+ ktx2TranscoderPath: '/basis/',
1433
1460
  };
1461
+ /**
1462
+ * Update global loader configuration (e.g., set path to CDN)
1463
+ */
1464
+ function setLoaderConfig(config) {
1465
+ globalConfig = { ...globalConfig, ...config };
1466
+ }
1467
+ /**
1468
+ * Get current global loader configuration
1469
+ */
1470
+ function getLoaderConfig() {
1471
+ return globalConfig;
1472
+ }
1434
1473
 
1435
1474
  /**
1436
1475
  * @file modelLoader.ts
@@ -1448,19 +1487,22 @@ const DEFAULT_OPTIONS$1 = {
1448
1487
  maxTextureSize: null,
1449
1488
  useSimpleMaterials: false,
1450
1489
  skipSkinned: true,
1490
+ useCache: true,
1451
1491
  };
1492
+ const modelCache = new Map();
1452
1493
  /** Automatically determine which options to enable based on extension (smart judgment) */
1453
1494
  function normalizeOptions(url, opts) {
1454
1495
  const ext = (url.split('.').pop() || '').toLowerCase();
1455
- const merged = Object.assign(Object.assign({}, DEFAULT_OPTIONS$1), opts);
1496
+ const merged = { ...DEFAULT_OPTIONS$1, ...opts };
1456
1497
  if (ext === 'gltf' || ext === 'glb') {
1498
+ const globalConfig = getLoaderConfig();
1457
1499
  // gltf/glb defaults to trying draco/ktx2 if user didn't specify
1458
1500
  if (merged.dracoDecoderPath === undefined)
1459
- merged.dracoDecoderPath = '/draco/';
1501
+ merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
1460
1502
  if (merged.useKTX2 === undefined)
1461
1503
  merged.useKTX2 = true;
1462
1504
  if (merged.ktx2TranscoderPath === undefined)
1463
- merged.ktx2TranscoderPath = '/basis/';
1505
+ merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
1464
1506
  }
1465
1507
  else {
1466
1508
  // fbx/obj/ply/stl etc. do not need draco/ktx2
@@ -1470,103 +1512,108 @@ function normalizeOptions(url, opts) {
1470
1512
  }
1471
1513
  return merged;
1472
1514
  }
1473
- function loadModelByUrl(url_1) {
1474
- return __awaiter(this, arguments, void 0, function* (url, options = {}) {
1475
- var _a, _b;
1476
- if (!url)
1477
- throw new Error('url required');
1478
- const ext = (url.split('.').pop() || '').toLowerCase();
1479
- const opts = normalizeOptions(url, options);
1480
- const manager = (_a = opts.manager) !== null && _a !== void 0 ? _a : new THREE.LoadingManager();
1481
- let loader;
1482
- if (ext === 'gltf' || ext === 'glb') {
1483
- const { GLTFLoader } = yield import('three/examples/jsm/loaders/GLTFLoader.js');
1484
- const gltfLoader = new GLTFLoader(manager);
1485
- if (opts.dracoDecoderPath) {
1486
- const { DRACOLoader } = yield import('three/examples/jsm/loaders/DRACOLoader.js');
1487
- const draco = new DRACOLoader();
1488
- draco.setDecoderPath(opts.dracoDecoderPath);
1489
- gltfLoader.setDRACOLoader(draco);
1490
- }
1491
- if (opts.useKTX2 && opts.ktx2TranscoderPath) {
1492
- const { KTX2Loader } = yield import('three/examples/jsm/loaders/KTX2Loader.js');
1493
- const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
1494
- gltfLoader.__ktx2Loader = ktx2Loader;
1495
- }
1496
- loader = gltfLoader;
1497
- }
1498
- else if (ext === 'fbx') {
1499
- const { FBXLoader } = yield import('three/examples/jsm/loaders/FBXLoader.js');
1500
- loader = new FBXLoader(manager);
1501
- }
1502
- else if (ext === 'obj') {
1503
- const { OBJLoader } = yield import('three/examples/jsm/loaders/OBJLoader.js');
1504
- loader = new OBJLoader(manager);
1505
- }
1506
- else if (ext === 'ply') {
1507
- const { PLYLoader } = yield import('three/examples/jsm/loaders/PLYLoader.js');
1508
- loader = new PLYLoader(manager);
1509
- }
1510
- else if (ext === 'stl') {
1511
- const { STLLoader } = yield import('three/examples/jsm/loaders/STLLoader.js');
1512
- loader = new STLLoader(manager);
1515
+ async function loadModelByUrl(url, options = {}) {
1516
+ if (!url)
1517
+ throw new Error('url required');
1518
+ const ext = (url.split('.').pop() || '').toLowerCase();
1519
+ const opts = normalizeOptions(url, options);
1520
+ const manager = opts.manager ?? new THREE.LoadingManager();
1521
+ // Cache key includes URL and relevant optimization options
1522
+ const cacheKey = `${url}_${opts.mergeGeometries}_${opts.maxTextureSize}_${opts.useSimpleMaterials}`;
1523
+ if (opts.useCache && modelCache.has(cacheKey)) {
1524
+ return modelCache.get(cacheKey).clone();
1525
+ }
1526
+ let loader;
1527
+ if (ext === 'gltf' || ext === 'glb') {
1528
+ const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
1529
+ const gltfLoader = new GLTFLoader(manager);
1530
+ if (opts.dracoDecoderPath) {
1531
+ const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
1532
+ const draco = new DRACOLoader();
1533
+ draco.setDecoderPath(opts.dracoDecoderPath);
1534
+ gltfLoader.setDRACOLoader(draco);
1513
1535
  }
1514
- else {
1515
- throw new Error(`Unsupported model extension: .${ext}`);
1516
- }
1517
- const object = yield new Promise((resolve, reject) => {
1518
- loader.load(url, (res) => {
1519
- var _a;
1520
- if (ext === 'gltf' || ext === 'glb') {
1521
- const sceneObj = res.scene || res;
1522
- // --- Critical: Expose animations to scene.userData (or scene.animations) ---
1523
- // So the caller can access clips simply by getting sceneObj.userData.animations
1524
- sceneObj.userData = (sceneObj === null || sceneObj === void 0 ? void 0 : sceneObj.userData) || {};
1525
- sceneObj.userData.animations = (_a = res.animations) !== null && _a !== void 0 ? _a : [];
1526
- resolve(sceneObj);
1527
- }
1528
- else {
1529
- resolve(res);
1530
- }
1531
- }, undefined, (err) => reject(err));
1532
- });
1533
- // Optimize
1534
- object.traverse((child) => {
1535
- var _a, _b, _c;
1536
- const mesh = child;
1537
- if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
1538
- try {
1539
- mesh.geometry = (_c = (_b = (_a = new THREE.BufferGeometry()).fromGeometry) === null || _b === void 0 ? void 0 : _b.call(_a, mesh.geometry)) !== null && _c !== void 0 ? _c : mesh.geometry;
1540
- }
1541
- catch (_d) { }
1542
- }
1543
- });
1544
- if (opts.maxTextureSize && opts.maxTextureSize > 0)
1545
- downscaleTexturesInObject(object, opts.maxTextureSize);
1546
- if (opts.useSimpleMaterials) {
1547
- object.traverse((child) => {
1548
- const m = child.material;
1549
- if (!m)
1550
- return;
1551
- if (Array.isArray(m))
1552
- child.material = m.map((mat) => toSimpleMaterial(mat));
1553
- else
1554
- child.material = toSimpleMaterial(m);
1555
- });
1536
+ if (opts.useKTX2 && opts.ktx2TranscoderPath) {
1537
+ const { KTX2Loader } = await import('three/examples/jsm/loaders/KTX2Loader.js');
1538
+ const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
1539
+ gltfLoader.__ktx2Loader = ktx2Loader;
1556
1540
  }
1557
- if (opts.mergeGeometries) {
1558
- try {
1559
- yield tryMergeGeometries(object, { skipSkinned: (_b = opts.skipSkinned) !== null && _b !== void 0 ? _b : true });
1541
+ loader = gltfLoader;
1542
+ }
1543
+ else if (ext === 'fbx') {
1544
+ const { FBXLoader } = await import('three/examples/jsm/loaders/FBXLoader.js');
1545
+ loader = new FBXLoader(manager);
1546
+ }
1547
+ else if (ext === 'obj') {
1548
+ const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
1549
+ loader = new OBJLoader(manager);
1550
+ }
1551
+ else if (ext === 'ply') {
1552
+ const { PLYLoader } = await import('three/examples/jsm/loaders/PLYLoader.js');
1553
+ loader = new PLYLoader(manager);
1554
+ }
1555
+ else if (ext === 'stl') {
1556
+ const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
1557
+ loader = new STLLoader(manager);
1558
+ }
1559
+ else {
1560
+ throw new Error(`Unsupported model extension: .${ext}`);
1561
+ }
1562
+ const object = await new Promise((resolve, reject) => {
1563
+ loader.load(url, (res) => {
1564
+ if (ext === 'gltf' || ext === 'glb') {
1565
+ const sceneObj = res.scene || res;
1566
+ // --- Critical: Expose animations to scene.userData (or scene.animations) ---
1567
+ // So the caller can access clips simply by getting sceneObj.userData.animations
1568
+ sceneObj.userData = sceneObj?.userData || {};
1569
+ sceneObj.userData.animations = res.animations ?? [];
1570
+ resolve(sceneObj);
1560
1571
  }
1561
- catch (e) {
1562
- console.warn('mergeGeometries failed', e);
1572
+ else {
1573
+ resolve(res);
1574
+ }
1575
+ }, undefined, (err) => reject(err));
1576
+ });
1577
+ // Optimize
1578
+ object.traverse((child) => {
1579
+ const mesh = child;
1580
+ if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
1581
+ try {
1582
+ mesh.geometry = new THREE.BufferGeometry().fromGeometry?.(mesh.geometry) ?? mesh.geometry;
1563
1583
  }
1584
+ catch { }
1564
1585
  }
1565
- return object;
1566
1586
  });
1587
+ if (opts.maxTextureSize && opts.maxTextureSize > 0)
1588
+ await downscaleTexturesInObject(object, opts.maxTextureSize);
1589
+ if (opts.useSimpleMaterials) {
1590
+ object.traverse((child) => {
1591
+ const m = child.material;
1592
+ if (!m)
1593
+ return;
1594
+ if (Array.isArray(m))
1595
+ child.material = m.map((mat) => toSimpleMaterial(mat));
1596
+ else
1597
+ child.material = toSimpleMaterial(m);
1598
+ });
1599
+ }
1600
+ if (opts.mergeGeometries) {
1601
+ try {
1602
+ await tryMergeGeometries(object, { skipSkinned: opts.skipSkinned ?? true });
1603
+ }
1604
+ catch (e) {
1605
+ console.warn('mergeGeometries failed', e);
1606
+ }
1607
+ }
1608
+ if (opts.useCache) {
1609
+ modelCache.set(cacheKey, object);
1610
+ return object.clone();
1611
+ }
1612
+ return object;
1567
1613
  }
1568
- /** Runtime downscale textures in mesh to maxSize (canvas drawImage) to save GPU memory */
1569
- function downscaleTexturesInObject(obj, maxSize) {
1614
+ /** Runtime downscale textures in mesh to maxSize (createImageBitmap or canvas) to save GPU memory */
1615
+ async function downscaleTexturesInObject(obj, maxSize) {
1616
+ const tasks = [];
1570
1617
  obj.traverse((ch) => {
1571
1618
  if (!ch.isMesh)
1572
1619
  return;
@@ -1585,27 +1632,44 @@ function downscaleTexturesInObject(obj, maxSize) {
1585
1632
  const max = maxSize;
1586
1633
  if (image.width <= max && image.height <= max)
1587
1634
  return;
1588
- // downscale using canvas (sync, may be heavy for many textures)
1589
- try {
1590
- const scale = Math.min(max / image.width, max / image.height);
1591
- const canvas = document.createElement('canvas');
1592
- canvas.width = Math.floor(image.width * scale);
1593
- canvas.height = Math.floor(image.height * scale);
1594
- const ctx = canvas.getContext('2d');
1595
- if (ctx) {
1596
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1597
- const newTex = new THREE.Texture(canvas);
1598
- newTex.needsUpdate = true;
1599
- // copy common settings (encoding etc)
1600
- newTex.encoding = tex.encoding;
1601
- mat[p] = newTex;
1635
+ tasks.push((async () => {
1636
+ try {
1637
+ const scale = Math.min(max / image.width, max / image.height);
1638
+ const newWidth = Math.floor(image.width * scale);
1639
+ const newHeight = Math.floor(image.height * scale);
1640
+ let newSource;
1641
+ if (typeof createImageBitmap !== 'undefined') {
1642
+ newSource = await createImageBitmap(image, {
1643
+ resizeWidth: newWidth,
1644
+ resizeHeight: newHeight,
1645
+ resizeQuality: 'high'
1646
+ });
1647
+ }
1648
+ else {
1649
+ // Fallback for environments without createImageBitmap
1650
+ const canvas = document.createElement('canvas');
1651
+ canvas.width = newWidth;
1652
+ canvas.height = newHeight;
1653
+ const ctx = canvas.getContext('2d');
1654
+ if (ctx) {
1655
+ ctx.drawImage(image, 0, 0, newWidth, newHeight);
1656
+ newSource = canvas;
1657
+ }
1658
+ }
1659
+ if (newSource) {
1660
+ const newTex = new THREE.Texture(newSource);
1661
+ newTex.needsUpdate = true;
1662
+ newTex.encoding = tex.encoding;
1663
+ mat[p] = newTex;
1664
+ }
1602
1665
  }
1603
- }
1604
- catch (e) {
1605
- console.warn('downscale texture failed', e);
1606
- }
1666
+ catch (e) {
1667
+ console.warn('downscale texture failed', e);
1668
+ }
1669
+ })());
1607
1670
  });
1608
1671
  });
1672
+ await Promise.all(tasks);
1609
1673
  }
1610
1674
  /**
1611
1675
  * Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
@@ -1613,81 +1677,77 @@ function downscaleTexturesInObject(obj, maxSize) {
1613
1677
  * - Merging will group by material UUID (different materials cannot be merged)
1614
1678
  * - Merge function is compatible with common export names of BufferGeometryUtils
1615
1679
  */
1616
- function tryMergeGeometries(root, opts) {
1617
- return __awaiter(this, void 0, void 0, function* () {
1618
- // collect meshes by material uuid
1619
- const groups = new Map();
1620
- root.traverse((ch) => {
1621
- var _a;
1622
- if (!ch.isMesh)
1623
- return;
1624
- const mesh = ch;
1625
- if (opts.skipSkinned && mesh.isSkinnedMesh)
1626
- return;
1627
- const mat = mesh.material;
1628
- // don't merge transparent or morph-enabled or skinned meshes
1629
- if (!mesh.geometry || mesh.visible === false)
1630
- return;
1631
- if (mat && mat.transparent)
1632
- return;
1633
- const geom = mesh.geometry.clone();
1634
- mesh.updateWorldMatrix(true, false);
1635
- geom.applyMatrix4(mesh.matrixWorld);
1636
- // ensure attributes compatible? we'll rely on merge function to return null if incompatible
1637
- const key = (mat && mat.uuid) || 'default';
1638
- const bucket = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { material: mat !== null && mat !== void 0 ? mat : new THREE.MeshStandardMaterial(), geoms: [] };
1639
- bucket.geoms.push(geom);
1640
- groups.set(key, bucket);
1641
- // mark for removal (we'll remove meshes after)
1642
- mesh.userData.__toRemoveForMerge = true;
1643
- });
1644
- if (groups.size === 0)
1680
+ async function tryMergeGeometries(root, opts) {
1681
+ // collect meshes by material uuid
1682
+ const groups = new Map();
1683
+ root.traverse((ch) => {
1684
+ if (!ch.isMesh)
1645
1685
  return;
1646
- // dynamic import BufferGeometryUtils and find merge function name
1647
- const bufUtilsMod = yield import('three/examples/jsm/utils/BufferGeometryUtils.js');
1648
- // use || chain (avoid mixing ?? with || without parentheses)
1649
- const mergeFn = bufUtilsMod.mergeBufferGeometries ||
1650
- bufUtilsMod.mergeGeometries ||
1651
- bufUtilsMod.mergeBufferGeometries || // defensive duplicate
1652
- bufUtilsMod.mergeGeometries;
1653
- if (!mergeFn)
1654
- throw new Error('No merge function found in BufferGeometryUtils');
1655
- // for each group, try merge
1656
- for (const [key, { material, geoms }] of groups) {
1657
- if (geoms.length <= 1) {
1658
- // nothing to merge
1659
- continue;
1660
- }
1661
- // call merge function - signature typically mergeBufferGeometries(array, useGroups)
1662
- const merged = mergeFn(geoms, false);
1663
- if (!merged) {
1664
- console.warn('merge returned null for group', key);
1665
- continue;
1686
+ const mesh = ch;
1687
+ if (opts.skipSkinned && mesh.isSkinnedMesh)
1688
+ return;
1689
+ const mat = mesh.material;
1690
+ // don't merge transparent or morph-enabled or skinned meshes
1691
+ if (!mesh.geometry || mesh.visible === false)
1692
+ return;
1693
+ if (mat && mat.transparent)
1694
+ return;
1695
+ const geom = mesh.geometry.clone();
1696
+ mesh.updateWorldMatrix(true, false);
1697
+ geom.applyMatrix4(mesh.matrixWorld);
1698
+ // ensure attributes compatible? we'll rely on merge function to return null if incompatible
1699
+ const key = (mat && mat.uuid) || 'default';
1700
+ const bucket = groups.get(key) ?? { material: mat ?? new THREE.MeshStandardMaterial(), geoms: [] };
1701
+ bucket.geoms.push(geom);
1702
+ groups.set(key, bucket);
1703
+ // mark for removal (we'll remove meshes after)
1704
+ mesh.userData.__toRemoveForMerge = true;
1705
+ });
1706
+ if (groups.size === 0)
1707
+ return;
1708
+ // dynamic import BufferGeometryUtils and find merge function name
1709
+ const bufUtilsMod = await import('three/examples/jsm/utils/BufferGeometryUtils.js');
1710
+ // use || chain (avoid mixing ?? with || without parentheses)
1711
+ const mergeFn = bufUtilsMod.mergeBufferGeometries ||
1712
+ bufUtilsMod.mergeGeometries ||
1713
+ bufUtilsMod.mergeBufferGeometries || // defensive duplicate
1714
+ bufUtilsMod.mergeGeometries;
1715
+ if (!mergeFn)
1716
+ throw new Error('No merge function found in BufferGeometryUtils');
1717
+ // for each group, try merge
1718
+ for (const [key, { material, geoms }] of groups) {
1719
+ if (geoms.length <= 1) {
1720
+ // nothing to merge
1721
+ continue;
1722
+ }
1723
+ // call merge function - signature typically mergeBufferGeometries(array, useGroups)
1724
+ const merged = mergeFn(geoms, false);
1725
+ if (!merged) {
1726
+ console.warn('merge returned null for group', key);
1727
+ continue;
1728
+ }
1729
+ // create merged mesh at root (world-space geometry already applied)
1730
+ const mergedMesh = new THREE.Mesh(merged, material);
1731
+ root.add(mergedMesh);
1732
+ }
1733
+ // now remove original meshes flagged for removal
1734
+ const toRemove = [];
1735
+ root.traverse((ch) => {
1736
+ if (ch.userData?.__toRemoveForMerge)
1737
+ toRemove.push(ch);
1738
+ });
1739
+ toRemove.forEach((m) => {
1740
+ if (m.parent)
1741
+ m.parent.remove(m);
1742
+ // free original resources (geometries already cloned/applied), but careful with shared materials
1743
+ if (m.isMesh) {
1744
+ const mm = m;
1745
+ try {
1746
+ mm.geometry.dispose();
1666
1747
  }
1667
- // create merged mesh at root (world-space geometry already applied)
1668
- const mergedMesh = new THREE.Mesh(merged, material);
1669
- root.add(mergedMesh);
1748
+ catch { }
1749
+ // we do NOT dispose material because it may be reused by mergedMesh
1670
1750
  }
1671
- // now remove original meshes flagged for removal
1672
- const toRemove = [];
1673
- root.traverse((ch) => {
1674
- var _a;
1675
- if ((_a = ch.userData) === null || _a === void 0 ? void 0 : _a.__toRemoveForMerge)
1676
- toRemove.push(ch);
1677
- });
1678
- toRemove.forEach((m) => {
1679
- if (m.parent)
1680
- m.parent.remove(m);
1681
- // free original resources (geometries already cloned/applied), but careful with shared materials
1682
- if (m.isMesh) {
1683
- const mm = m;
1684
- try {
1685
- mm.geometry.dispose();
1686
- }
1687
- catch (_a) { }
1688
- // we do NOT dispose material because it may be reused by mergedMesh
1689
- }
1690
- });
1691
1751
  });
1692
1752
  }
1693
1753
  /* ---------------------
@@ -1704,7 +1764,7 @@ function disposeObject(obj) {
1704
1764
  try {
1705
1765
  m.geometry.dispose();
1706
1766
  }
1707
- catch (_a) { }
1767
+ catch { }
1708
1768
  }
1709
1769
  const mat = m.material;
1710
1770
  if (mat) {
@@ -1726,14 +1786,14 @@ function disposeMaterial(mat) {
1726
1786
  try {
1727
1787
  mat[k].dispose();
1728
1788
  }
1729
- catch (_a) { }
1789
+ catch { }
1730
1790
  }
1731
1791
  });
1732
1792
  try {
1733
1793
  if (typeof mat.dispose === 'function')
1734
1794
  mat.dispose();
1735
1795
  }
1736
- catch (_a) { }
1796
+ catch { }
1737
1797
  }
1738
1798
  // Helper to convert to simple material (stub)
1739
1799
  function toSimpleMaterial(mat) {
@@ -1776,100 +1836,97 @@ const equirectCache = new Map();
1776
1836
  * @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
1777
1837
  * @param opts SkyboxOptions
1778
1838
  */
1779
- function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1780
- return __awaiter(this, arguments, void 0, function* (renderer, scene, paths, opts = {}) {
1781
- var _a, _b;
1782
- const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
1783
- if (!Array.isArray(paths) || paths.length !== 6)
1784
- throw new Error('cube skybox requires 6 image paths');
1785
- const key = paths.join('|');
1786
- // Cache handling
1787
- if (options.cache && cubeCache.has(key)) {
1788
- const rec = cubeCache.get(key);
1789
- rec.refCount += 1;
1790
- // reapply to scene (in case it was removed)
1791
- if (options.setAsBackground)
1792
- scene.background = rec.handle.backgroundTexture;
1793
- if (options.setAsEnvironment && rec.handle.envRenderTarget)
1794
- scene.environment = rec.handle.envRenderTarget.texture;
1795
- return rec.handle;
1796
- }
1797
- // Load cube texture
1798
- const loader = new THREE.CubeTextureLoader();
1799
- const texture = yield new Promise((resolve, reject) => {
1800
- loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
1801
- });
1802
- // Set encoding and mapping
1803
- if (options.useSRGBEncoding)
1804
- texture.encoding = THREE.sRGBEncoding;
1805
- texture.mapping = THREE.CubeReflectionMapping;
1806
- // apply as background if required
1839
+ async function loadCubeSkybox(renderer, scene, paths, opts = {}) {
1840
+ const options = { ...DEFAULT_OPTIONS, ...opts };
1841
+ if (!Array.isArray(paths) || paths.length !== 6)
1842
+ throw new Error('cube skybox requires 6 image paths');
1843
+ const key = paths.join('|');
1844
+ // Cache handling
1845
+ if (options.cache && cubeCache.has(key)) {
1846
+ const rec = cubeCache.get(key);
1847
+ rec.refCount += 1;
1848
+ // reapply to scene (in case it was removed)
1807
1849
  if (options.setAsBackground)
1808
- scene.background = texture;
1809
- // environment: use PMREM to produce a proper prefiltered env map for PBR
1810
- let pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE.PMREMGenerator(renderer);
1811
- (_b = pmremGenerator.compileCubemapShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
1812
- // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
1813
- let envRenderTarget = null;
1814
- if (pmremGenerator.fromCubemap) {
1815
- envRenderTarget = pmremGenerator.fromCubemap(texture);
1850
+ scene.background = rec.handle.backgroundTexture;
1851
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
1852
+ scene.environment = rec.handle.envRenderTarget.texture;
1853
+ return rec.handle;
1854
+ }
1855
+ // Load cube texture
1856
+ const loader = new THREE.CubeTextureLoader();
1857
+ const texture = await new Promise((resolve, reject) => {
1858
+ loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
1859
+ });
1860
+ // Set encoding and mapping
1861
+ if (options.useSRGBEncoding)
1862
+ texture.encoding = THREE.sRGBEncoding;
1863
+ texture.mapping = THREE.CubeReflectionMapping;
1864
+ // apply as background if required
1865
+ if (options.setAsBackground)
1866
+ scene.background = texture;
1867
+ // environment: use PMREM to produce a proper prefiltered env map for PBR
1868
+ let pmremGenerator = options.pmremGenerator ?? new THREE.PMREMGenerator(renderer);
1869
+ pmremGenerator.compileCubemapShader?.( /* optional */);
1870
+ // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
1871
+ let envRenderTarget = null;
1872
+ if (pmremGenerator.fromCubemap) {
1873
+ envRenderTarget = pmremGenerator.fromCubemap(texture);
1874
+ }
1875
+ else {
1876
+ // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
1877
+ // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
1878
+ envRenderTarget = null;
1879
+ }
1880
+ if (options.setAsEnvironment) {
1881
+ if (envRenderTarget) {
1882
+ scene.environment = envRenderTarget.texture;
1816
1883
  }
1817
1884
  else {
1818
- // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
1819
- // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
1820
- envRenderTarget = null;
1885
+ // fallback: use cube texture directly (works but not prefiltered)
1886
+ scene.environment = texture;
1821
1887
  }
1822
- if (options.setAsEnvironment) {
1823
- if (envRenderTarget) {
1824
- scene.environment = envRenderTarget.texture;
1888
+ }
1889
+ const handle = {
1890
+ key,
1891
+ backgroundTexture: options.setAsBackground ? texture : null,
1892
+ envRenderTarget: envRenderTarget,
1893
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
1894
+ setAsBackground: !!options.setAsBackground,
1895
+ setAsEnvironment: !!options.setAsEnvironment,
1896
+ dispose() {
1897
+ // remove from scene
1898
+ if (options.setAsBackground && scene.background === texture)
1899
+ scene.background = null;
1900
+ if (options.setAsEnvironment && scene.environment) {
1901
+ // only clear if it's the same texture we set
1902
+ if (envRenderTarget && scene.environment === envRenderTarget.texture)
1903
+ scene.environment = null;
1904
+ else if (scene.environment === texture)
1905
+ scene.environment = null;
1825
1906
  }
1826
- else {
1827
- // fallback: use cube texture directly (works but not prefiltered)
1828
- scene.environment = texture;
1829
- }
1830
- }
1831
- const handle = {
1832
- key,
1833
- backgroundTexture: options.setAsBackground ? texture : null,
1834
- envRenderTarget: envRenderTarget,
1835
- pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
1836
- setAsBackground: !!options.setAsBackground,
1837
- setAsEnvironment: !!options.setAsEnvironment,
1838
- dispose() {
1839
- // remove from scene
1840
- if (options.setAsBackground && scene.background === texture)
1841
- scene.background = null;
1842
- if (options.setAsEnvironment && scene.environment) {
1843
- // only clear if it's the same texture we set
1844
- if (envRenderTarget && scene.environment === envRenderTarget.texture)
1845
- scene.environment = null;
1846
- else if (scene.environment === texture)
1847
- scene.environment = null;
1848
- }
1849
- // dispose resources only if not cached/shared
1850
- if (envRenderTarget) {
1851
- try {
1852
- envRenderTarget.dispose();
1853
- }
1854
- catch (_a) { }
1855
- }
1907
+ // dispose resources only if not cached/shared
1908
+ if (envRenderTarget) {
1856
1909
  try {
1857
- texture.dispose();
1910
+ envRenderTarget.dispose();
1858
1911
  }
1859
- catch (_b) { }
1860
- // dispose pmremGenerator we created
1861
- if (!options.pmremGenerator && pmremGenerator) {
1862
- try {
1863
- pmremGenerator.dispose();
1864
- }
1865
- catch (_c) { }
1912
+ catch { }
1913
+ }
1914
+ try {
1915
+ texture.dispose();
1916
+ }
1917
+ catch { }
1918
+ // dispose pmremGenerator we created
1919
+ if (!options.pmremGenerator && pmremGenerator) {
1920
+ try {
1921
+ pmremGenerator.dispose();
1866
1922
  }
1923
+ catch { }
1867
1924
  }
1868
- };
1869
- if (options.cache)
1870
- cubeCache.set(key, { handle, refCount: 1 });
1871
- return handle;
1872
- });
1925
+ }
1926
+ };
1927
+ if (options.cache)
1928
+ cubeCache.set(key, { handle, refCount: 1 });
1929
+ return handle;
1873
1930
  }
1874
1931
  /**
1875
1932
  * Load Equirectangular/Single Image (Supports HDR via RGBELoader)
@@ -1878,95 +1935,90 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1878
1935
  * @param url string - *.hdr, *.exr, *.jpg, *.png
1879
1936
  * @param opts SkyboxOptions
1880
1937
  */
1881
- function loadEquirectSkybox(renderer_1, scene_1, url_1) {
1882
- return __awaiter(this, arguments, void 0, function* (renderer, scene, url, opts = {}) {
1883
- var _a, _b;
1884
- const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
1885
- const key = url;
1886
- if (options.cache && equirectCache.has(key)) {
1887
- const rec = equirectCache.get(key);
1888
- rec.refCount += 1;
1889
- if (options.setAsBackground)
1890
- scene.background = rec.handle.backgroundTexture;
1891
- if (options.setAsEnvironment && rec.handle.envRenderTarget)
1892
- scene.environment = rec.handle.envRenderTarget.texture;
1893
- return rec.handle;
1894
- }
1895
- // Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
1896
- const isHDR = /\.hdr$|\.exr$/i.test(url);
1897
- let hdrTexture;
1898
- if (isHDR) {
1899
- const { RGBELoader } = yield import('three/examples/jsm/loaders/RGBELoader.js');
1900
- hdrTexture = yield new Promise((resolve, reject) => {
1901
- new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
1902
- });
1903
- // RGBE textures typically use LinearEncoding
1904
- hdrTexture.encoding = THREE.LinearEncoding;
1905
- }
1906
- else {
1907
- // ordinary image - use TextureLoader
1908
- const loader = new THREE.TextureLoader();
1909
- hdrTexture = yield new Promise((resolve, reject) => {
1910
- loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
1911
- });
1912
- if (options.useSRGBEncoding)
1913
- hdrTexture.encoding = THREE.sRGBEncoding;
1914
- }
1915
- // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
1916
- const pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE.PMREMGenerator(renderer);
1917
- (_b = pmremGenerator.compileEquirectangularShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
1918
- const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
1919
- // envTexture to use for scene.environment
1920
- const envTexture = envRenderTarget.texture;
1921
- // set background and/or environment
1922
- if (options.setAsBackground) {
1923
- // for background it's ok to use the equirect texture directly or the envTexture
1924
- // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
1925
- scene.background = envTexture;
1926
- }
1927
- if (options.setAsEnvironment) {
1928
- scene.environment = envTexture;
1929
- }
1930
- // We can dispose the original hdrTexture (the PMREM target contains the needed data)
1931
- try {
1932
- hdrTexture.dispose();
1933
- }
1934
- catch (_c) { }
1935
- const handle = {
1936
- key,
1937
- backgroundTexture: options.setAsBackground ? envTexture : null,
1938
- envRenderTarget,
1939
- pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
1940
- setAsBackground: !!options.setAsBackground,
1941
- setAsEnvironment: !!options.setAsEnvironment,
1942
- dispose() {
1943
- if (options.setAsBackground && scene.background === envTexture)
1944
- scene.background = null;
1945
- if (options.setAsEnvironment && scene.environment === envTexture)
1946
- scene.environment = null;
1938
+ async function loadEquirectSkybox(renderer, scene, url, opts = {}) {
1939
+ const options = { ...DEFAULT_OPTIONS, ...opts };
1940
+ const key = url;
1941
+ if (options.cache && equirectCache.has(key)) {
1942
+ const rec = equirectCache.get(key);
1943
+ rec.refCount += 1;
1944
+ if (options.setAsBackground)
1945
+ scene.background = rec.handle.backgroundTexture;
1946
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
1947
+ scene.environment = rec.handle.envRenderTarget.texture;
1948
+ return rec.handle;
1949
+ }
1950
+ // Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
1951
+ const isHDR = /\.hdr$|\.exr$/i.test(url);
1952
+ let hdrTexture;
1953
+ if (isHDR) {
1954
+ const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js');
1955
+ hdrTexture = await new Promise((resolve, reject) => {
1956
+ new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
1957
+ });
1958
+ // RGBE textures typically use LinearEncoding
1959
+ hdrTexture.encoding = THREE.LinearEncoding;
1960
+ }
1961
+ else {
1962
+ // ordinary image - use TextureLoader
1963
+ const loader = new THREE.TextureLoader();
1964
+ hdrTexture = await new Promise((resolve, reject) => {
1965
+ loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
1966
+ });
1967
+ if (options.useSRGBEncoding)
1968
+ hdrTexture.encoding = THREE.sRGBEncoding;
1969
+ }
1970
+ // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
1971
+ const pmremGenerator = options.pmremGenerator ?? new THREE.PMREMGenerator(renderer);
1972
+ pmremGenerator.compileEquirectangularShader?.();
1973
+ const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
1974
+ // envTexture to use for scene.environment
1975
+ const envTexture = envRenderTarget.texture;
1976
+ // set background and/or environment
1977
+ if (options.setAsBackground) {
1978
+ // for background it's ok to use the equirect texture directly or the envTexture
1979
+ // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
1980
+ scene.background = envTexture;
1981
+ }
1982
+ if (options.setAsEnvironment) {
1983
+ scene.environment = envTexture;
1984
+ }
1985
+ // We can dispose the original hdrTexture (the PMREM target contains the needed data)
1986
+ try {
1987
+ hdrTexture.dispose();
1988
+ }
1989
+ catch { }
1990
+ const handle = {
1991
+ key,
1992
+ backgroundTexture: options.setAsBackground ? envTexture : null,
1993
+ envRenderTarget,
1994
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
1995
+ setAsBackground: !!options.setAsBackground,
1996
+ setAsEnvironment: !!options.setAsEnvironment,
1997
+ dispose() {
1998
+ if (options.setAsBackground && scene.background === envTexture)
1999
+ scene.background = null;
2000
+ if (options.setAsEnvironment && scene.environment === envTexture)
2001
+ scene.environment = null;
2002
+ try {
2003
+ envRenderTarget.dispose();
2004
+ }
2005
+ catch { }
2006
+ if (!options.pmremGenerator && pmremGenerator) {
1947
2007
  try {
1948
- envRenderTarget.dispose();
1949
- }
1950
- catch (_a) { }
1951
- if (!options.pmremGenerator && pmremGenerator) {
1952
- try {
1953
- pmremGenerator.dispose();
1954
- }
1955
- catch (_b) { }
2008
+ pmremGenerator.dispose();
1956
2009
  }
2010
+ catch { }
1957
2011
  }
1958
- };
1959
- if (options.cache)
1960
- equirectCache.set(key, { handle, refCount: 1 });
1961
- return handle;
1962
- });
2012
+ }
2013
+ };
2014
+ if (options.cache)
2015
+ equirectCache.set(key, { handle, refCount: 1 });
2016
+ return handle;
1963
2017
  }
1964
- function loadSkybox(renderer_1, scene_1, params_1) {
1965
- return __awaiter(this, arguments, void 0, function* (renderer, scene, params, opts = {}) {
1966
- if (params.type === 'cube')
1967
- return loadCubeSkybox(renderer, scene, params.paths, opts);
1968
- return loadEquirectSkybox(renderer, scene, params.url, opts);
1969
- });
2018
+ async function loadSkybox(renderer, scene, params, opts = {}) {
2019
+ if (params.type === 'cube')
2020
+ return loadCubeSkybox(renderer, scene, params.paths, opts);
2021
+ return loadEquirectSkybox(renderer, scene, params.url, opts);
1970
2022
  }
1971
2023
  /* -------------------------
1972
2024
  Cache / Reference Counting Helper Methods
@@ -2176,10 +2228,9 @@ class BlueSkyManager {
2176
2228
  * Usually called when the scene is completely destroyed or the application exits
2177
2229
  */
2178
2230
  destroy() {
2179
- var _a;
2180
2231
  this.cancelLoad();
2181
2232
  this.dispose();
2182
- (_a = this.pmremGen) === null || _a === void 0 ? void 0 : _a.dispose();
2233
+ this.pmremGen?.dispose();
2183
2234
  this.isInitialized = false;
2184
2235
  this.loadingState = 'idle';
2185
2236
  }
@@ -2213,20 +2264,19 @@ const BlueSky = new BlueSkyManager();
2213
2264
  * - RAF management optimization
2214
2265
  */
2215
2266
  function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, options) {
2216
- var _a, _b, _c, _d, _e, _f;
2217
2267
  const cfg = {
2218
- fontSize: (options === null || options === void 0 ? void 0 : options.fontSize) || '12px',
2219
- color: (options === null || options === void 0 ? void 0 : options.color) || '#ffffff',
2220
- background: (options === null || options === void 0 ? void 0 : options.background) || '#1890ff',
2221
- padding: (options === null || options === void 0 ? void 0 : options.padding) || '6px 10px',
2222
- borderRadius: (options === null || options === void 0 ? void 0 : options.borderRadius) || '6px',
2223
- lift: (_a = options === null || options === void 0 ? void 0 : options.lift) !== null && _a !== void 0 ? _a : 100,
2224
- dotSize: (_b = options === null || options === void 0 ? void 0 : options.dotSize) !== null && _b !== void 0 ? _b : 6,
2225
- dotSpacing: (_c = options === null || options === void 0 ? void 0 : options.dotSpacing) !== null && _c !== void 0 ? _c : 2,
2226
- lineColor: (options === null || options === void 0 ? void 0 : options.lineColor) || 'rgba(200,200,200,0.7)',
2227
- lineWidth: (_d = options === null || options === void 0 ? void 0 : options.lineWidth) !== null && _d !== void 0 ? _d : 1,
2228
- updateInterval: (_e = options === null || options === void 0 ? void 0 : options.updateInterval) !== null && _e !== void 0 ? _e : 0, // Default update every frame
2229
- fadeInDuration: (_f = options === null || options === void 0 ? void 0 : options.fadeInDuration) !== null && _f !== void 0 ? _f : 300, // Fade-in duration
2268
+ fontSize: options?.fontSize || '12px',
2269
+ color: options?.color || '#ffffff',
2270
+ background: options?.background || '#1890ff',
2271
+ padding: options?.padding || '6px 10px',
2272
+ borderRadius: options?.borderRadius || '6px',
2273
+ lift: options?.lift ?? 100,
2274
+ dotSize: options?.dotSize ?? 6,
2275
+ dotSpacing: options?.dotSpacing ?? 2,
2276
+ lineColor: options?.lineColor || 'rgba(200,200,200,0.7)',
2277
+ lineWidth: options?.lineWidth ?? 1,
2278
+ updateInterval: options?.updateInterval ?? 0, // Default update every frame
2279
+ fadeInDuration: options?.fadeInDuration ?? 300, // Fade-in duration
2230
2280
  };
2231
2281
  const container = document.createElement('div');
2232
2282
  container.style.position = 'absolute';
@@ -2249,7 +2299,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2249
2299
  svg.style.zIndex = '1';
2250
2300
  container.appendChild(svg);
2251
2301
  let currentModel = parentModel;
2252
- let currentLabelsMap = Object.assign({}, modelLabelsMap);
2302
+ let currentLabelsMap = { ...modelLabelsMap };
2253
2303
  let labels = [];
2254
2304
  let isActive = true;
2255
2305
  let isPaused = false;
@@ -2330,9 +2380,8 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2330
2380
  if (!currentModel)
2331
2381
  return;
2332
2382
  currentModel.traverse((child) => {
2333
- var _a;
2334
2383
  if (child.isMesh || child.type === 'Group') {
2335
- const labelText = (_a = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))) === null || _a === void 0 ? void 0 : _a[1];
2384
+ const labelText = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))?.[1];
2336
2385
  if (!labelText)
2337
2386
  return;
2338
2387
  const wrapper = document.createElement('div');
@@ -2437,7 +2486,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2437
2486
  rebuildLabels();
2438
2487
  },
2439
2488
  updateLabelsMap(newMap) {
2440
- currentLabelsMap = Object.assign({}, newMap);
2489
+ currentLabelsMap = { ...newMap };
2441
2490
  rebuildLabels();
2442
2491
  },
2443
2492
  // Pause update
@@ -2535,205 +2584,197 @@ class GroupExploder {
2535
2584
  * @param newSet The new set of meshes
2536
2585
  * @param contextId Optional context ID to distinguish business scenarios
2537
2586
  */
2538
- setMeshes(newSet, options) {
2539
- return __awaiter(this, void 0, void 0, function* () {
2540
- var _a, _b;
2541
- const autoRestorePrev = (_a = options === null || options === void 0 ? void 0 : options.autoRestorePrev) !== null && _a !== void 0 ? _a : true;
2542
- const restoreDuration = (_b = options === null || options === void 0 ? void 0 : options.restoreDuration) !== null && _b !== void 0 ? _b : 300;
2543
- this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
2544
- // If the newSet is null and currentSet is null -> nothing
2545
- if (!newSet && !this.currentSet) {
2546
- this.log('setMeshes: both newSet and currentSet are null, nothing to do');
2547
- return;
2548
- }
2549
- // If both exist and are the same reference, we still must detect content changes.
2550
- const sameReference = this.currentSet === newSet;
2551
- // Prepare prevSet snapshot (we copy current to prev)
2552
- if (this.currentSet) {
2553
- this.prevSet = this.currentSet;
2554
- this.prevStateMap = new Map(this.stateMap);
2555
- this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
2556
- }
2557
- else {
2558
- this.prevSet = null;
2559
- this.prevStateMap = new Map();
2560
- }
2561
- // If we used to be exploded and need to restore prevSet, do that first (await)
2562
- if (this.prevSet && autoRestorePrev && this.isExploded) {
2563
- this.log('setMeshes: need to restore prevSet before applying newSet');
2564
- yield this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
2565
- this.log('setMeshes: prevSet restore done');
2566
- this.prevStateMap.clear();
2567
- this.prevSet = null;
2568
- }
2569
- // Now register newSet: we clear and rebuild stateMap carefully.
2570
- // But we must handle the case where caller reuses same Set object and just mutated elements.
2571
- // We will compute additions and removals.
2572
- const oldSet = this.currentSet;
2573
- this.currentSet = newSet;
2574
- // If newSet is null -> simply clear stateMap
2575
- if (!this.currentSet) {
2576
- this.stateMap.clear();
2577
- this.log('setMeshes: newSet is null -> cleared stateMap');
2578
- this.isExploded = false;
2587
+ async setMeshes(newSet, options) {
2588
+ const autoRestorePrev = options?.autoRestorePrev ?? true;
2589
+ const restoreDuration = options?.restoreDuration ?? 300;
2590
+ this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
2591
+ // If the newSet is null and currentSet is null -> nothing
2592
+ if (!newSet && !this.currentSet) {
2593
+ this.log('setMeshes: both newSet and currentSet are null, nothing to do');
2594
+ return;
2595
+ }
2596
+ // If both exist and are the same reference, we still must detect content changes.
2597
+ const sameReference = this.currentSet === newSet;
2598
+ // Prepare prevSet snapshot (we copy current to prev)
2599
+ if (this.currentSet) {
2600
+ this.prevSet = this.currentSet;
2601
+ this.prevStateMap = new Map(this.stateMap);
2602
+ this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
2603
+ }
2604
+ else {
2605
+ this.prevSet = null;
2606
+ this.prevStateMap = new Map();
2607
+ }
2608
+ // If we used to be exploded and need to restore prevSet, do that first (await)
2609
+ if (this.prevSet && autoRestorePrev && this.isExploded) {
2610
+ this.log('setMeshes: need to restore prevSet before applying newSet');
2611
+ await this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
2612
+ this.log('setMeshes: prevSet restore done');
2613
+ this.prevStateMap.clear();
2614
+ this.prevSet = null;
2615
+ }
2616
+ // Now register newSet: we clear and rebuild stateMap carefully.
2617
+ // But we must handle the case where caller reuses same Set object and just mutated elements.
2618
+ // We will compute additions and removals.
2619
+ const oldSet = this.currentSet;
2620
+ this.currentSet = newSet;
2621
+ // If newSet is null -> simply clear stateMap
2622
+ if (!this.currentSet) {
2623
+ this.stateMap.clear();
2624
+ this.log('setMeshes: newSet is null -> cleared stateMap');
2625
+ this.isExploded = false;
2626
+ return;
2627
+ }
2628
+ // If we have oldSet (could be same reference) then compute diffs
2629
+ if (oldSet) {
2630
+ // If same reference but size or content differs -> handle diffs
2631
+ const wasSameRef = sameReference;
2632
+ let added = [];
2633
+ let removed = [];
2634
+ // Build maps of membership
2635
+ const oldMembers = new Set(Array.from(oldSet));
2636
+ const newMembers = new Set(Array.from(this.currentSet));
2637
+ // find removals
2638
+ oldMembers.forEach((m) => {
2639
+ if (!newMembers.has(m))
2640
+ removed.push(m);
2641
+ });
2642
+ // find additions
2643
+ newMembers.forEach((m) => {
2644
+ if (!oldMembers.has(m))
2645
+ added.push(m);
2646
+ });
2647
+ if (wasSameRef && added.length === 0 && removed.length === 0) {
2648
+ // truly identical (no content changes)
2649
+ this.log('setMeshes: same reference and identical contents -> nothing to update');
2579
2650
  return;
2580
2651
  }
2581
- // If we have oldSet (could be same reference) then compute diffs
2582
- if (oldSet) {
2583
- // If same reference but size or content differs -> handle diffs
2584
- const wasSameRef = sameReference;
2585
- let added = [];
2586
- let removed = [];
2587
- // Build maps of membership
2588
- const oldMembers = new Set(Array.from(oldSet));
2589
- const newMembers = new Set(Array.from(this.currentSet));
2590
- // find removals
2591
- oldMembers.forEach((m) => {
2592
- if (!newMembers.has(m))
2593
- removed.push(m);
2594
- });
2595
- // find additions
2596
- newMembers.forEach((m) => {
2597
- if (!oldMembers.has(m))
2598
- added.push(m);
2599
- });
2600
- if (wasSameRef && added.length === 0 && removed.length === 0) {
2601
- // truly identical (no content changes)
2602
- this.log('setMeshes: same reference and identical contents -> nothing to update');
2603
- return;
2652
+ this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
2653
+ // Remove snapshots for removed meshes
2654
+ removed.forEach((m) => {
2655
+ if (this.stateMap.has(m)) {
2656
+ this.stateMap.delete(m);
2604
2657
  }
2605
- this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
2606
- // Remove snapshots for removed meshes
2607
- removed.forEach((m) => {
2608
- if (this.stateMap.has(m)) {
2609
- this.stateMap.delete(m);
2610
- }
2611
- });
2612
- // Ensure snapshots exist for current set members (create for newly added meshes)
2613
- yield this.ensureSnapshotsForSet(this.currentSet);
2614
- this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
2615
- this.isExploded = false;
2616
- return;
2617
- }
2618
- else {
2619
- // no oldSet -> brand new registration
2620
- this.stateMap.clear();
2621
- yield this.ensureSnapshotsForSet(this.currentSet);
2622
- this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
2623
- this.isExploded = false;
2624
- return;
2625
- }
2626
- });
2658
+ });
2659
+ // Ensure snapshots exist for current set members (create for newly added meshes)
2660
+ await this.ensureSnapshotsForSet(this.currentSet);
2661
+ this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
2662
+ this.isExploded = false;
2663
+ return;
2664
+ }
2665
+ else {
2666
+ // no oldSet -> brand new registration
2667
+ this.stateMap.clear();
2668
+ await this.ensureSnapshotsForSet(this.currentSet);
2669
+ this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
2670
+ this.isExploded = false;
2671
+ return;
2672
+ }
2627
2673
  }
2628
2674
  /**
2629
2675
  * ensureSnapshotsForSet: for each mesh in set, ensure stateMap has an entry.
2630
2676
  * If missing, record current matrixWorld as originalMatrixWorld (best-effort).
2631
2677
  */
2632
- ensureSnapshotsForSet(set) {
2633
- return __awaiter(this, void 0, void 0, function* () {
2634
- set.forEach((m) => {
2678
+ async ensureSnapshotsForSet(set) {
2679
+ set.forEach((m) => {
2680
+ try {
2681
+ m.updateMatrixWorld(true);
2682
+ }
2683
+ catch { }
2684
+ if (!this.stateMap.has(m)) {
2635
2685
  try {
2636
- m.updateMatrixWorld(true);
2686
+ this.stateMap.set(m, {
2687
+ originalParent: m.parent || null,
2688
+ originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE.Matrix4().copy(m.matrix),
2689
+ });
2690
+ // Also store in userData for extra resilience
2691
+ m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
2637
2692
  }
2638
- catch (_a) { }
2639
- if (!this.stateMap.has(m)) {
2640
- try {
2641
- this.stateMap.set(m, {
2642
- originalParent: m.parent || null,
2643
- originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE.Matrix4().copy(m.matrix),
2644
- });
2645
- // Also store in userData for extra resilience
2646
- m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
2647
- }
2648
- catch (e) {
2649
- this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
2650
- }
2693
+ catch (e) {
2694
+ this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
2651
2695
  }
2652
- });
2696
+ }
2653
2697
  });
2654
2698
  }
2655
2699
  /**
2656
2700
  * explode: compute targets first, compute targetBound using targets + mesh radii,
2657
2701
  * animate camera to that targetBound, then animate meshes to targets.
2658
2702
  */
2659
- explode(opts) {
2660
- return __awaiter(this, void 0, void 0, function* () {
2661
- var _a;
2662
- if (!this.currentSet || this.currentSet.size === 0) {
2663
- this.log('explode: empty currentSet, nothing to do');
2664
- return;
2703
+ async explode(opts) {
2704
+ if (!this.currentSet || this.currentSet.size === 0) {
2705
+ this.log('explode: empty currentSet, nothing to do');
2706
+ return;
2707
+ }
2708
+ const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
2709
+ this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
2710
+ this.cancelAnimations();
2711
+ const meshes = Array.from(this.currentSet);
2712
+ // ensure snapshots exist for any meshes that may have been added after initial registration
2713
+ await this.ensureSnapshotsForSet(this.currentSet);
2714
+ // compute center/radius from current meshes (fallback)
2715
+ const initial = this.computeBoundingSphereForMeshes(meshes);
2716
+ const center = initial.center;
2717
+ const baseRadius = Math.max(1, initial.radius);
2718
+ this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
2719
+ // compute targets (pure calculation)
2720
+ const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
2721
+ this.log(`explode: computed ${targets.length} target positions`);
2722
+ // compute target-based bounding sphere (targets + per-mesh radius)
2723
+ const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
2724
+ this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
2725
+ await this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
2726
+ this.log('explode: camera animation to target bound completed');
2727
+ // apply dim if needed with context id
2728
+ const contextId = dimOthers?.enabled ? this.applyDimToOthers(meshes, dimOthers.opacity ?? 0.25, { debug }) : null;
2729
+ if (contextId)
2730
+ this.log(`explode: applied dim for context ${contextId}`);
2731
+ // capture starts after camera move
2732
+ const starts = meshes.map((m) => {
2733
+ const v = new THREE.Vector3();
2734
+ try {
2735
+ m.getWorldPosition(v);
2665
2736
  }
2666
- const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
2667
- this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
2668
- this.cancelAnimations();
2669
- const meshes = Array.from(this.currentSet);
2670
- // ensure snapshots exist for any meshes that may have been added after initial registration
2671
- yield this.ensureSnapshotsForSet(this.currentSet);
2672
- // compute center/radius from current meshes (fallback)
2673
- const initial = this.computeBoundingSphereForMeshes(meshes);
2674
- const center = initial.center;
2675
- const baseRadius = Math.max(1, initial.radius);
2676
- this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
2677
- // compute targets (pure calculation)
2678
- const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
2679
- this.log(`explode: computed ${targets.length} target positions`);
2680
- // compute target-based bounding sphere (targets + per-mesh radius)
2681
- const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
2682
- this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
2683
- yield this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
2684
- this.log('explode: camera animation to target bound completed');
2685
- // apply dim if needed with context id
2686
- const contextId = (dimOthers === null || dimOthers === void 0 ? void 0 : dimOthers.enabled) ? this.applyDimToOthers(meshes, (_a = dimOthers.opacity) !== null && _a !== void 0 ? _a : 0.25, { debug }) : null;
2687
- if (contextId)
2688
- this.log(`explode: applied dim for context ${contextId}`);
2689
- // capture starts after camera move
2690
- const starts = meshes.map((m) => {
2691
- const v = new THREE.Vector3();
2692
- try {
2693
- m.getWorldPosition(v);
2694
- }
2695
- catch (_a) {
2696
- // fallback to originalMatrixWorld if available
2697
- const st = this.stateMap.get(m);
2698
- if (st)
2699
- v.setFromMatrixPosition(st.originalMatrixWorld);
2700
- }
2701
- return v;
2702
- });
2703
- const startTime = performance.now();
2704
- const total = Math.max(1, duration);
2705
- const tick = (now) => {
2706
- const t = Math.min(1, (now - startTime) / total);
2707
- const eased = easeInOutQuad(t);
2708
- for (let i = 0; i < meshes.length; i++) {
2709
- const m = meshes[i];
2710
- const s = starts[i];
2711
- const tar = targets[i];
2712
- const cur = s.clone().lerp(tar, eased);
2713
- if (m.parent) {
2714
- const local = cur.clone();
2715
- m.parent.worldToLocal(local);
2716
- m.position.copy(local);
2717
- }
2718
- else {
2719
- m.position.copy(cur);
2720
- }
2721
- m.updateMatrix();
2722
- }
2723
- if (this.controls && typeof this.controls.update === 'function')
2724
- this.controls.update();
2725
- if (t < 1) {
2726
- this.animId = requestAnimationFrame(tick);
2737
+ catch {
2738
+ // fallback to originalMatrixWorld if available
2739
+ const st = this.stateMap.get(m);
2740
+ if (st)
2741
+ v.setFromMatrixPosition(st.originalMatrixWorld);
2742
+ }
2743
+ return v;
2744
+ });
2745
+ const startTime = performance.now();
2746
+ const total = Math.max(1, duration);
2747
+ const tick = (now) => {
2748
+ const t = Math.min(1, (now - startTime) / total);
2749
+ const eased = easeInOutQuad(t);
2750
+ for (let i = 0; i < meshes.length; i++) {
2751
+ const m = meshes[i];
2752
+ const s = starts[i];
2753
+ const tar = targets[i];
2754
+ const cur = s.clone().lerp(tar, eased);
2755
+ if (m.parent) {
2756
+ const local = cur.clone();
2757
+ m.parent.worldToLocal(local);
2758
+ m.position.copy(local);
2727
2759
  }
2728
2760
  else {
2729
- this.animId = null;
2730
- this.isExploded = true;
2731
- this.log(`explode: completed. contextId=${contextId !== null && contextId !== void 0 ? contextId : 'none'}`);
2761
+ m.position.copy(cur);
2732
2762
  }
2733
- };
2734
- this.animId = requestAnimationFrame(tick);
2735
- return;
2736
- });
2763
+ m.updateMatrix();
2764
+ }
2765
+ if (this.controls && typeof this.controls.update === 'function')
2766
+ this.controls.update();
2767
+ if (t < 1) {
2768
+ this.animId = requestAnimationFrame(tick);
2769
+ }
2770
+ else {
2771
+ this.animId = null;
2772
+ this.isExploded = true;
2773
+ this.log(`explode: completed. contextId=${contextId ?? 'none'}`);
2774
+ }
2775
+ };
2776
+ this.animId = requestAnimationFrame(tick);
2777
+ return;
2737
2778
  }
2738
2779
  /**
2739
2780
  * Restore all exploded meshes to their original transform:
@@ -2756,7 +2797,7 @@ class GroupExploder {
2756
2797
  */
2757
2798
  restoreSet(set, stateMap, duration = 400, opts) {
2758
2799
  if (!set || set.size === 0) {
2759
- if (opts === null || opts === void 0 ? void 0 : opts.debug)
2800
+ if (opts?.debug)
2760
2801
  this.log('restoreSet: empty set, nothing to restore');
2761
2802
  return Promise.resolve();
2762
2803
  }
@@ -2769,12 +2810,12 @@ class GroupExploder {
2769
2810
  try {
2770
2811
  m.updateMatrixWorld(true);
2771
2812
  }
2772
- catch (_a) { }
2813
+ catch { }
2773
2814
  const s = new THREE.Vector3();
2774
2815
  try {
2775
2816
  m.getWorldPosition(s);
2776
2817
  }
2777
- catch (_b) {
2818
+ catch {
2778
2819
  s.set(0, 0, 0);
2779
2820
  }
2780
2821
  starts.push(s);
@@ -2872,7 +2913,7 @@ class GroupExploder {
2872
2913
  });
2873
2914
  }
2874
2915
  // material dim with context id
2875
- applyDimToOthers(explodingMeshes, opacity = 0.25, opts) {
2916
+ applyDimToOthers(explodingMeshes, opacity = 0.25, _opts) {
2876
2917
  const contextId = `ctx_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
2877
2918
  const explodingSet = new Set(explodingMeshes);
2878
2919
  const touched = new Set();
@@ -2883,11 +2924,10 @@ class GroupExploder {
2883
2924
  if (explodingSet.has(mesh))
2884
2925
  return;
2885
2926
  const applyMat = (mat) => {
2886
- var _a;
2887
2927
  if (!this.materialSnaps.has(mat)) {
2888
2928
  this.materialSnaps.set(mat, {
2889
2929
  transparent: !!mat.transparent,
2890
- opacity: (_a = mat.opacity) !== null && _a !== void 0 ? _a : 1,
2930
+ opacity: mat.opacity ?? 1,
2891
2931
  depthWrite: mat.depthWrite,
2892
2932
  });
2893
2933
  }
@@ -2914,7 +2954,7 @@ class GroupExploder {
2914
2954
  return contextId;
2915
2955
  }
2916
2956
  // clean contexts for meshes (restore materials whose contexts are removed)
2917
- cleanContextsForMeshes(meshes) {
2957
+ cleanContextsForMeshes(_meshes) {
2918
2958
  // conservative strategy: for each context we created, delete it and restore materials accordingly
2919
2959
  for (const [contextId, mats] of Array.from(this.contextMaterials.entries())) {
2920
2960
  mats.forEach((mat) => {
@@ -3028,7 +3068,7 @@ class GroupExploder {
3028
3068
  }
3029
3069
  }
3030
3070
  }
3031
- catch (_a) {
3071
+ catch {
3032
3072
  radius = 0;
3033
3073
  }
3034
3074
  if (!isFinite(radius) || radius < 0 || radius > 1e8)
@@ -3051,10 +3091,9 @@ class GroupExploder {
3051
3091
  }
3052
3092
  // computeTargetsByMode (unchanged logic but pure function)
3053
3093
  computeTargetsByMode(meshes, center, baseRadius, opts) {
3054
- var _a, _b;
3055
3094
  const n = meshes.length;
3056
- const lift = (_a = opts.lift) !== null && _a !== void 0 ? _a : 0.5;
3057
- const mode = (_b = opts.mode) !== null && _b !== void 0 ? _b : 'ring';
3095
+ const lift = opts.lift ?? 0.5;
3096
+ const mode = opts.mode ?? 'ring';
3058
3097
  const targets = [];
3059
3098
  if (mode === 'ring') {
3060
3099
  for (let i = 0; i < n; i++) {
@@ -3096,9 +3135,8 @@ class GroupExploder {
3096
3135
  return targets;
3097
3136
  }
3098
3137
  animateCameraToFit(targetCenter, targetRadius, opts) {
3099
- var _a, _b, _c, _d;
3100
- const duration = (_a = opts === null || opts === void 0 ? void 0 : opts.duration) !== null && _a !== void 0 ? _a : 600;
3101
- const padding = (_b = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _b !== void 0 ? _b : 1.5;
3138
+ const duration = opts?.duration ?? 600;
3139
+ const padding = opts?.padding ?? 1.5;
3102
3140
  if (!(this.camera instanceof THREE.PerspectiveCamera)) {
3103
3141
  if (this.controls && this.controls.target) {
3104
3142
  // Fallback for non-PerspectiveCamera
@@ -3110,14 +3148,13 @@ class GroupExploder {
3110
3148
  const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
3111
3149
  const startTime = performance.now();
3112
3150
  const tick = (now) => {
3113
- var _a;
3114
3151
  const t = Math.min(1, (now - startTime) / duration);
3115
3152
  const k = easeInOutQuad(t);
3116
3153
  if (this.controls && this.controls.target) {
3117
3154
  this.controls.target.lerpVectors(startTarget, endTarget, k);
3118
3155
  }
3119
3156
  this.camera.position.lerpVectors(startPos, endPos, k);
3120
- if ((_a = this.controls) === null || _a === void 0 ? void 0 : _a.update)
3157
+ if (this.controls?.update)
3121
3158
  this.controls.update();
3122
3159
  if (t < 1) {
3123
3160
  this.cameraAnimId = requestAnimationFrame(tick);
@@ -3140,8 +3177,8 @@ class GroupExploder {
3140
3177
  const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
3141
3178
  const dist = Math.max(distV, distH) * padding;
3142
3179
  const startPos = this.camera.position.clone();
3143
- const startTarget = ((_c = this.controls) === null || _c === void 0 ? void 0 : _c.target) ? this.controls.target.clone() : new THREE.Vector3(); // assumption
3144
- if (!((_d = this.controls) === null || _d === void 0 ? void 0 : _d.target)) {
3180
+ const startTarget = this.controls?.target ? this.controls.target.clone() : new THREE.Vector3(); // assumption
3181
+ if (!this.controls?.target) {
3145
3182
  this.camera.getWorldDirection(startTarget);
3146
3183
  startTarget.add(startPos);
3147
3184
  }
@@ -3154,13 +3191,12 @@ class GroupExploder {
3154
3191
  return new Promise((resolve) => {
3155
3192
  const startTime = performance.now();
3156
3193
  const tick = (now) => {
3157
- var _a, _b;
3158
3194
  const t = Math.min(1, (now - startTime) / duration);
3159
3195
  const k = easeInOutQuad(t);
3160
3196
  this.camera.position.lerpVectors(startPos, endPos, k);
3161
3197
  if (this.controls && this.controls.target) {
3162
3198
  this.controls.target.lerpVectors(startTarget, endTarget, k);
3163
- (_b = (_a = this.controls).update) === null || _b === void 0 ? void 0 : _b.call(_a);
3199
+ this.controls.update?.();
3164
3200
  }
3165
3201
  else {
3166
3202
  this.camera.lookAt(endTarget); // simple lookAt if no controls
@@ -3209,183 +3245,134 @@ class GroupExploder {
3209
3245
  * @file autoSetup.ts
3210
3246
  * @description
3211
3247
  * Automatically sets up the camera and basic lighting scene based on the model's bounding box.
3212
- *
3213
- * @best-practice
3214
- * - Call `autoSetupCameraAndLight` after loading a model to get a quick "good looking" scene.
3215
- * - Returns a handle to dispose lights or update intensity later.
3216
3248
  */
3217
3249
  /**
3218
- * Automatically setup camera and basic lighting - Optimized
3219
- *
3220
- * Features:
3221
- * - Adds light intensity adjustment method
3222
- * - Improved error handling
3223
- * - Optimized dispose logic
3224
- *
3225
- * - camera: THREE.PerspectiveCamera (will be moved and pointed at model center)
3226
- * - scene: THREE.Scene (newly created light group will be added to the scene)
3227
- * - model: THREE.Object3D loaded model (arbitrary transform/coordinates)
3228
- * - options: Optional configuration (see AutoSetupOptions)
3229
- *
3230
- * Returns AutoSetupHandle, caller should call handle.dispose() when component unmounts/switches
3250
+ * Fit camera to object bounding box
3231
3251
  */
3232
- function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3233
- var _a, _b, _c, _d, _e, _f, _g;
3234
- // Boundary check
3235
- if (!camera || !scene || !model) {
3236
- throw new Error('autoSetupCameraAndLight: camera, scene, model are required');
3237
- }
3252
+ function fitCameraToObject(camera, object, padding = 1.2, elevation = 0.2) {
3253
+ const box = new THREE.Box3().setFromObject(object);
3254
+ if (!isFinite(box.min.x))
3255
+ return { center: new THREE.Vector3(), radius: 0 };
3256
+ const sphere = new THREE.Sphere();
3257
+ box.getBoundingSphere(sphere);
3258
+ const center = sphere.center.clone();
3259
+ const radius = Math.max(0.001, sphere.radius);
3260
+ const fov = (camera.fov * Math.PI) / 180;
3261
+ const halfFov = fov / 2;
3262
+ const sinHalfFov = Math.max(Math.sin(halfFov), 0.001);
3263
+ const distance = (radius * padding) / sinHalfFov;
3264
+ const dir = new THREE.Vector3(0, Math.sin(elevation), Math.cos(elevation)).normalize();
3265
+ const desiredPos = center.clone().add(dir.multiplyScalar(distance));
3266
+ camera.position.copy(desiredPos);
3267
+ camera.lookAt(center);
3268
+ camera.near = Math.max(0.001, radius / 1000);
3269
+ camera.far = Math.max(1000, radius * 50);
3270
+ camera.updateProjectionMatrix();
3271
+ return { center, radius };
3272
+ }
3273
+ /**
3274
+ * Setup default lighting for a model
3275
+ */
3276
+ function setupDefaultLights(scene, model, options = {}) {
3277
+ const box = new THREE.Box3().setFromObject(model);
3278
+ const sphere = new THREE.Sphere();
3279
+ box.getBoundingSphere(sphere);
3280
+ const center = sphere.center.clone();
3281
+ const radius = Math.max(0.001, sphere.radius);
3238
3282
  const opts = {
3239
- padding: (_a = options.padding) !== null && _a !== void 0 ? _a : 1.2,
3240
- elevation: (_b = options.elevation) !== null && _b !== void 0 ? _b : 0.2,
3241
- enableShadows: (_c = options.enableShadows) !== null && _c !== void 0 ? _c : false,
3242
- shadowMapSize: (_d = options.shadowMapSize) !== null && _d !== void 0 ? _d : 1024,
3243
- directionalCount: (_e = options.directionalCount) !== null && _e !== void 0 ? _e : 4,
3244
- setMeshShadowProps: (_f = options.setMeshShadowProps) !== null && _f !== void 0 ? _f : true,
3245
- renderer: (_g = options.renderer) !== null && _g !== void 0 ? _g : null,
3283
+ padding: options.padding ?? 1.2,
3284
+ elevation: options.elevation ?? 0.2,
3285
+ enableShadows: options.enableShadows ?? false,
3286
+ shadowMapSize: options.shadowMapSize ?? 1024,
3287
+ directionalCount: options.directionalCount ?? 4,
3288
+ setMeshShadowProps: options.setMeshShadowProps ?? true,
3289
+ renderer: options.renderer ?? null,
3246
3290
  };
3247
- try {
3248
- // --- 1) Calculate bounding data
3249
- const box = new THREE.Box3().setFromObject(model);
3250
- // Check bounding box validity
3251
- if (!isFinite(box.min.x)) {
3252
- throw new Error('autoSetupCameraAndLight: Invalid bounding box');
3291
+ if (opts.renderer && opts.enableShadows) {
3292
+ opts.renderer.shadowMap.enabled = true;
3293
+ opts.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
3294
+ }
3295
+ const lightsGroup = new THREE.Group();
3296
+ lightsGroup.name = 'autoSetupLightsGroup';
3297
+ lightsGroup.position.copy(center);
3298
+ scene.add(lightsGroup);
3299
+ const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
3300
+ hemi.position.set(0, radius * 2.0, 0);
3301
+ lightsGroup.add(hemi);
3302
+ const ambient = new THREE.AmbientLight(0xffffff, 0.25);
3303
+ lightsGroup.add(ambient);
3304
+ const dirCount = Math.max(1, Math.floor(opts.directionalCount));
3305
+ const dirs = [new THREE.Vector3(0, 1, 0)];
3306
+ for (let i = 0; i < dirCount; i++) {
3307
+ const angle = (i / dirCount) * Math.PI * 2;
3308
+ const v = new THREE.Vector3(Math.cos(angle), 0.3, Math.sin(angle)).normalize();
3309
+ dirs.push(v);
3310
+ }
3311
+ const shadowCamSize = Math.max(1, radius * 1.5);
3312
+ dirs.forEach((d, i) => {
3313
+ const light = new THREE.DirectionalLight(0xffffff, i === 0 ? 1.5 : 1.2);
3314
+ light.position.copy(d.clone().multiplyScalar(radius * 2.5));
3315
+ light.target.position.copy(center);
3316
+ light.name = `auto_dir_${i}`;
3317
+ lightsGroup.add(light);
3318
+ lightsGroup.add(light.target);
3319
+ if (opts.enableShadows) {
3320
+ light.castShadow = true;
3321
+ light.shadow.mapSize.width = opts.shadowMapSize;
3322
+ light.shadow.mapSize.height = opts.shadowMapSize;
3323
+ const cam = light.shadow.camera;
3324
+ const s = shadowCamSize;
3325
+ cam.left = -s;
3326
+ cam.right = s;
3327
+ cam.top = s;
3328
+ cam.bottom = -s;
3329
+ cam.near = 0.1;
3330
+ cam.far = radius * 10 + 50;
3331
+ light.shadow.bias = -5e-4;
3253
3332
  }
3254
- const sphere = new THREE.Sphere();
3255
- box.getBoundingSphere(sphere);
3256
- const center = sphere.center.clone();
3257
- const radius = Math.max(0.001, sphere.radius);
3258
- // --- 2) Calculate camera position
3259
- const fov = (camera.fov * Math.PI) / 180;
3260
- const halfFov = fov / 2;
3261
- const sinHalfFov = Math.max(Math.sin(halfFov), 0.001);
3262
- const distance = (radius * opts.padding) / sinHalfFov;
3263
- const dir = new THREE.Vector3(0, Math.sin(opts.elevation), Math.cos(opts.elevation)).normalize();
3264
- const desiredPos = center.clone().add(dir.multiplyScalar(distance));
3265
- camera.position.copy(desiredPos);
3266
- camera.lookAt(center);
3267
- camera.near = Math.max(0.001, radius / 1000);
3268
- camera.far = Math.max(1000, radius * 50);
3269
- camera.updateProjectionMatrix();
3270
- // --- 3) Enable Shadows
3271
- if (opts.renderer && opts.enableShadows) {
3272
- opts.renderer.shadowMap.enabled = true;
3273
- opts.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
3274
- }
3275
- // --- 4) Create Lights Group
3276
- const lightsGroup = new THREE.Group();
3277
- lightsGroup.name = 'autoSetupLightsGroup';
3278
- lightsGroup.position.copy(center);
3279
- scene.add(lightsGroup);
3280
- // 4.1 Basic Light
3281
- const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
3282
- hemi.name = 'auto_hemi';
3283
- hemi.position.set(0, radius * 2.0, 0);
3284
- lightsGroup.add(hemi);
3285
- const ambient = new THREE.AmbientLight(0xffffff, 0.25);
3286
- ambient.name = 'auto_ambient';
3287
- lightsGroup.add(ambient);
3288
- // 4.2 Directional Lights
3289
- const dirCount = Math.max(1, Math.floor(opts.directionalCount));
3290
- const directionalLights = [];
3291
- const dirs = [];
3292
- dirs.push(new THREE.Vector3(0, 1, 0));
3293
- for (let i = 0; i < Math.max(1, dirCount); i++) {
3294
- const angle = (i / Math.max(1, dirCount)) * Math.PI * 2;
3295
- const v = new THREE.Vector3(Math.cos(angle), 0.3, Math.sin(angle)).normalize();
3296
- dirs.push(v);
3297
- }
3298
- const shadowCamSize = Math.max(1, radius * 1.5);
3299
- for (let i = 0; i < dirs.length; i++) {
3300
- const d = dirs[i];
3301
- const light = new THREE.DirectionalLight(0xffffff, i === 0 ? 1.5 : 1.2);
3302
- light.position.copy(d.clone().multiplyScalar(radius * 2.5));
3303
- light.target.position.copy(center);
3304
- light.name = `auto_dir_${i}`;
3305
- lightsGroup.add(light);
3306
- lightsGroup.add(light.target);
3307
- if (opts.enableShadows) {
3308
- light.castShadow = true;
3309
- light.shadow.mapSize.width = opts.shadowMapSize;
3310
- light.shadow.mapSize.height = opts.shadowMapSize;
3311
- const cam = light.shadow.camera;
3312
- const s = shadowCamSize;
3313
- cam.left = -s;
3314
- cam.right = s;
3315
- cam.top = s;
3316
- cam.bottom = -s;
3317
- cam.near = 0.1;
3318
- cam.far = radius * 10 + 50;
3319
- light.shadow.bias = -0.0005;
3320
- }
3321
- directionalLights.push(light);
3322
- }
3323
- // 4.3 Point Light Fill
3324
- const fill1 = new THREE.PointLight(0xffffff, 0.5, radius * 4);
3325
- fill1.position.copy(center).add(new THREE.Vector3(radius * 0.5, 0.2 * radius, 0));
3326
- fill1.name = 'auto_fill1';
3327
- lightsGroup.add(fill1);
3328
- const fill2 = new THREE.PointLight(0xffffff, 0.3, radius * 3);
3329
- fill2.position.copy(center).add(new THREE.Vector3(-radius * 0.5, -0.2 * radius, 0));
3330
- fill2.name = 'auto_fill2';
3331
- lightsGroup.add(fill2);
3332
- // --- 5) Set Mesh Shadow Props
3333
- if (opts.setMeshShadowProps) {
3334
- model.traverse((ch) => {
3335
- if (ch.isMesh) {
3336
- const mesh = ch;
3337
- const isSkinned = mesh.isSkinnedMesh;
3338
- mesh.castShadow = opts.enableShadows && !isSkinned ? true : mesh.castShadow;
3339
- mesh.receiveShadow = opts.enableShadows ? true : mesh.receiveShadow;
3333
+ });
3334
+ if (opts.setMeshShadowProps) {
3335
+ model.traverse((ch) => {
3336
+ if (ch.isMesh) {
3337
+ const mesh = ch;
3338
+ const isSkinned = mesh.isSkinnedMesh;
3339
+ mesh.castShadow = opts.enableShadows && !isSkinned ? true : mesh.castShadow;
3340
+ mesh.receiveShadow = opts.enableShadows ? true : mesh.receiveShadow;
3341
+ }
3342
+ });
3343
+ }
3344
+ const handle = {
3345
+ lightsGroup,
3346
+ center,
3347
+ radius,
3348
+ updateLightIntensity(factor) {
3349
+ lightsGroup.traverse((node) => {
3350
+ if (node.isLight) {
3351
+ const light = node;
3352
+ light.intensity *= factor; // Simple implementation
3340
3353
  }
3341
3354
  });
3342
- }
3343
- // --- 6) Return handle ---
3344
- const handle = {
3345
- lightsGroup,
3346
- center,
3347
- radius,
3348
- // Update light intensity
3349
- updateLightIntensity(factor) {
3350
- lightsGroup.traverse((node) => {
3351
- if (node.isLight) {
3352
- const light = node;
3353
- const originalIntensity = parseFloat(light.name.split('_').pop() || '1');
3354
- light.intensity = originalIntensity * Math.max(0, factor);
3355
- }
3356
- });
3357
- },
3358
- dispose: () => {
3359
- try {
3360
- // Remove lights group
3361
- if (lightsGroup.parent)
3362
- lightsGroup.parent.remove(lightsGroup);
3363
- // Dispose shadow resources
3364
- lightsGroup.traverse((node) => {
3365
- if (node.isLight) {
3366
- const l = node;
3367
- if (l.shadow && l.shadow.map) {
3368
- try {
3369
- l.shadow.map.dispose();
3370
- }
3371
- catch (err) {
3372
- console.warn('Failed to dispose shadow map:', err);
3373
- }
3374
- }
3375
- }
3376
- });
3377
- }
3378
- catch (error) {
3379
- console.error('autoSetupCameraAndLight: dispose failed', error);
3355
+ },
3356
+ dispose: () => {
3357
+ if (lightsGroup.parent)
3358
+ lightsGroup.parent.remove(lightsGroup);
3359
+ lightsGroup.traverse((node) => {
3360
+ if (node.isLight) {
3361
+ const l = node;
3362
+ if (l.shadow && l.shadow.map)
3363
+ l.shadow.map.dispose();
3380
3364
  }
3381
- }
3382
- };
3383
- return handle;
3384
- }
3385
- catch (error) {
3386
- console.error('autoSetupCameraAndLight: setup failed', error);
3387
- throw error;
3388
- }
3365
+ });
3366
+ }
3367
+ };
3368
+ return handle;
3369
+ }
3370
+ /**
3371
+ * Automatically setup camera and basic lighting (Combine fitCameraToObject and setupDefaultLights)
3372
+ */
3373
+ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3374
+ fitCameraToObject(camera, model, options.padding, options.elevation);
3375
+ return setupDefaultLights(scene, model, options);
3389
3376
  }
3390
3377
 
3391
3378
  /**
@@ -3395,8 +3382,8 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3395
3382
  * @packageDocumentation
3396
3383
  */
3397
3384
  // Core utilities
3398
- // Version
3399
- const VERSION = '1.0.0';
3385
+ // Version (keep in sync with package.json)
3386
+ const VERSION = '1.0.4';
3400
3387
 
3401
- export { ArrowGuide, BlueSky, FOLLOW_ANGLES, GroupExploder, LiquidFillerGroup, VERSION, ViewPresets, addChildModelLabels, autoSetupCameraAndLight, cancelFollow, cancelSetView, createModelClickHandler, createModelsLabel, disposeMaterial, disposeObject, enableHoverBreath, followModels, initPostProcessing, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox, setView };
3388
+ export { ArrowGuide, BlueSky, FOLLOW_ANGLES, GroupExploder, LiquidFillerGroup, ResourceManager, VERSION, ViewPresets, addChildModelLabels, autoSetupCameraAndLight, cancelFollow, cancelSetView, createModelClickHandler, createModelsLabel, disposeMaterial, disposeObject, enableHoverBreath, fitCameraToObject, followModels, getLoaderConfig, initPostProcessing, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox, setLoaderConfig, setView, setupDefaultLights };
3402
3389
  //# sourceMappingURL=index.mjs.map