@chocozhang/three-model-render 1.0.4 → 1.0.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 (40) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +46 -6
  3. package/dist/camera/index.js +6 -10
  4. package/dist/camera/index.js.map +1 -1
  5. package/dist/camera/index.mjs +6 -10
  6. package/dist/camera/index.mjs.map +1 -1
  7. package/dist/core/index.d.ts +21 -1
  8. package/dist/core/index.js +70 -9
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +70 -10
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/effect/index.js +185 -230
  13. package/dist/effect/index.js.map +1 -1
  14. package/dist/effect/index.mjs +185 -230
  15. package/dist/effect/index.mjs.map +1 -1
  16. package/dist/index.d.ts +61 -28
  17. package/dist/index.js +812 -806
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +808 -807
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/interaction/index.d.ts +12 -6
  22. package/dist/interaction/index.js +26 -14
  23. package/dist/interaction/index.js.map +1 -1
  24. package/dist/interaction/index.mjs +26 -14
  25. package/dist/interaction/index.mjs.map +1 -1
  26. package/dist/loader/index.d.ts +17 -2
  27. package/dist/loader/index.js +385 -386
  28. package/dist/loader/index.js.map +1 -1
  29. package/dist/loader/index.mjs +384 -387
  30. package/dist/loader/index.mjs.map +1 -1
  31. package/dist/setup/index.d.ts +13 -21
  32. package/dist/setup/index.js +120 -167
  33. package/dist/setup/index.js.map +1 -1
  34. package/dist/setup/index.mjs +119 -168
  35. package/dist/setup/index.mjs.map +1 -1
  36. package/dist/ui/index.js +15 -17
  37. package/dist/ui/index.js.map +1 -1
  38. package/dist/ui/index.mjs +15 -17
  39. package/dist/ui/index.mjs.map +1 -1
  40. package/package.json +49 -21
package/dist/index.js CHANGED
@@ -67,8 +67,8 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
67
67
  };
68
68
  }
69
69
  // Configuration
70
- const enableCache = (options === null || options === void 0 ? void 0 : options.enableCache) !== false;
71
- const updateInterval = (options === null || options === void 0 ? void 0 : options.updateInterval) || 0;
70
+ const enableCache = options?.enableCache !== false;
71
+ const updateInterval = options?.updateInterval || 0;
72
72
  // Create label container, absolute positioning, attached to body
73
73
  const container = document.createElement('div');
74
74
  container.style.position = 'absolute';
@@ -84,11 +84,10 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
84
84
  let lastUpdateTime = 0;
85
85
  // Traverse all child models
86
86
  parentModel.traverse((child) => {
87
- var _a;
88
87
  // Only process Mesh or Group
89
88
  if ((child.isMesh || child.type === 'Group')) {
90
89
  // Dynamic matching of name to prevent undefined
91
- const labelText = (_a = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))) === null || _a === void 0 ? void 0 : _a[1];
90
+ const labelText = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))?.[1];
92
91
  if (!labelText)
93
92
  return; // Skip if no matching label
94
93
  // Create DOM label
@@ -96,11 +95,11 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
96
95
  el.innerText = labelText;
97
96
  // Styles defined in JS, can be overridden via options
98
97
  el.style.position = 'absolute';
99
- el.style.color = (options === null || options === void 0 ? void 0 : options.color) || '#fff';
100
- el.style.background = (options === null || options === void 0 ? void 0 : options.background) || 'rgba(0,0,0,0.6)';
101
- el.style.padding = (options === null || options === void 0 ? void 0 : options.padding) || '4px 8px';
102
- el.style.borderRadius = (options === null || options === void 0 ? void 0 : options.borderRadius) || '4px';
103
- el.style.fontSize = (options === null || options === void 0 ? void 0 : options.fontSize) || '14px';
98
+ el.style.color = options?.color || '#fff';
99
+ el.style.background = options?.background || 'rgba(0,0,0,0.6)';
100
+ el.style.padding = options?.padding || '4px 8px';
101
+ el.style.borderRadius = options?.borderRadius || '4px';
102
+ el.style.fontSize = options?.fontSize || '14px';
104
103
  el.style.transform = 'translate(-50%, -100%)'; // Position label directly above the model
105
104
  el.style.whiteSpace = 'nowrap';
106
105
  el.style.pointerEvents = 'none';
@@ -481,6 +480,67 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
481
480
  };
482
481
  }
483
482
 
483
+ /**
484
+ * ResourceManager
485
+ * Handles tracking and disposal of Three.js objects to prevent memory leaks.
486
+ */
487
+ class ResourceManager {
488
+ constructor() {
489
+ this.geometries = new Set();
490
+ this.materials = new Set();
491
+ this.textures = new Set();
492
+ this.objects = new Set();
493
+ }
494
+ /**
495
+ * Track an object and its resources recursively
496
+ */
497
+ track(object) {
498
+ this.objects.add(object);
499
+ object.traverse((child) => {
500
+ if (child.isMesh) {
501
+ const mesh = child;
502
+ if (mesh.geometry)
503
+ this.geometries.add(mesh.geometry);
504
+ if (mesh.material) {
505
+ if (Array.isArray(mesh.material)) {
506
+ mesh.material.forEach(m => this.trackMaterial(m));
507
+ }
508
+ else {
509
+ this.trackMaterial(mesh.material);
510
+ }
511
+ }
512
+ }
513
+ });
514
+ return object;
515
+ }
516
+ trackMaterial(material) {
517
+ this.materials.add(material);
518
+ // Track textures in material
519
+ for (const value of Object.values(material)) {
520
+ if (value instanceof THREE__namespace.Texture) {
521
+ this.textures.add(value);
522
+ }
523
+ }
524
+ }
525
+ /**
526
+ * Dispose all tracked resources
527
+ */
528
+ dispose() {
529
+ this.geometries.forEach(g => g.dispose());
530
+ this.materials.forEach(m => m.dispose());
531
+ this.textures.forEach(t => t.dispose());
532
+ this.objects.forEach(obj => {
533
+ if (obj.parent) {
534
+ obj.parent.remove(obj);
535
+ }
536
+ });
537
+ this.geometries.clear();
538
+ this.materials.clear();
539
+ this.textures.clear();
540
+ this.objects.clear();
541
+ }
542
+ }
543
+
484
544
  /**
485
545
  * @file clickHandler.ts
486
546
  * @description
@@ -632,7 +692,6 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
632
692
  */
633
693
  class ArrowGuide {
634
694
  constructor(renderer, camera, scene, options) {
635
- var _a, _b, _c;
636
695
  this.renderer = renderer;
637
696
  this.camera = camera;
638
697
  this.scene = scene;
@@ -651,10 +710,10 @@ class ArrowGuide {
651
710
  // Config: Non-highlight opacity and brightness
652
711
  this.fadeOpacity = 0.5;
653
712
  this.fadeBrightness = 0.1;
654
- this.clickThreshold = (_a = options === null || options === void 0 ? void 0 : options.clickThreshold) !== null && _a !== void 0 ? _a : 10;
655
- this.ignoreRaycastNames = new Set((options === null || options === void 0 ? void 0 : options.ignoreRaycastNames) || []);
656
- this.fadeOpacity = (_b = options === null || options === void 0 ? void 0 : options.fadeOpacity) !== null && _b !== void 0 ? _b : 0.5;
657
- this.fadeBrightness = (_c = options === null || options === void 0 ? void 0 : options.fadeBrightness) !== null && _c !== void 0 ? _c : 0.1;
713
+ this.clickThreshold = options?.clickThreshold ?? 10;
714
+ this.ignoreRaycastNames = new Set(options?.ignoreRaycastNames || []);
715
+ this.fadeOpacity = options?.fadeOpacity ?? 0.5;
716
+ this.fadeBrightness = options?.fadeBrightness ?? 0.1;
658
717
  this.abortController = new AbortController();
659
718
  this.initEvents();
660
719
  }
@@ -892,7 +951,8 @@ class ArrowGuide {
892
951
  * LiquidFillerGroup - Optimized
893
952
  * Supports single or multi-model liquid level animation with independent color control.
894
953
  *
895
- * Features:
954
+ * Capabilities:
955
+ * - Supports THREE.Object3D, Array<THREE.Object3D>, Set<THREE.Object3D> etc.
896
956
  * - Uses renderer.domElement instead of window events
897
957
  * - Uses AbortController to manage event lifecycle
898
958
  * - Adds error handling and boundary checks
@@ -902,7 +962,7 @@ class ArrowGuide {
902
962
  class LiquidFillerGroup {
903
963
  /**
904
964
  * Constructor
905
- * @param models Single or multiple THREE.Object3D
965
+ * @param models Single or multiple THREE.Object3D (Array, Set, etc.)
906
966
  * @param scene Scene
907
967
  * @param camera Camera
908
968
  * @param renderer Renderer
@@ -942,14 +1002,13 @@ class LiquidFillerGroup {
942
1002
  this.clickThreshold = clickThreshold;
943
1003
  // Create AbortController for event management
944
1004
  this.abortController = new AbortController();
945
- const modelArray = Array.isArray(models) ? models : [models];
1005
+ const modelArray = this.normalizeInput(models);
946
1006
  modelArray.forEach(model => {
947
- var _a, _b, _c;
948
1007
  try {
949
1008
  const options = {
950
- color: (_a = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.color) !== null && _a !== void 0 ? _a : 0x00ff00,
951
- opacity: (_b = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.opacity) !== null && _b !== void 0 ? _b : 0.6,
952
- speed: (_c = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.speed) !== null && _c !== void 0 ? _c : 0.05,
1009
+ color: defaultOptions?.color ?? 0x00ff00,
1010
+ opacity: defaultOptions?.opacity ?? 0.6,
1011
+ speed: defaultOptions?.speed ?? 0.05,
953
1012
  };
954
1013
  // Save original materials
955
1014
  const originalMaterials = new Map();
@@ -1026,9 +1085,22 @@ class LiquidFillerGroup {
1026
1085
  this.renderer.domElement.addEventListener('pointerdown', this.handlePointerDown, { signal });
1027
1086
  this.renderer.domElement.addEventListener('pointerup', this.handlePointerUp, { signal });
1028
1087
  }
1088
+ /**
1089
+ * Helper to normalize input to Array<THREE.Object3D>
1090
+ */
1091
+ normalizeInput(models) {
1092
+ if (models instanceof THREE__namespace.Object3D) {
1093
+ return [models];
1094
+ }
1095
+ if (Array.isArray(models)) {
1096
+ return models;
1097
+ }
1098
+ // Handle Iterable (Set, etc.)
1099
+ return Array.from(models);
1100
+ }
1029
1101
  /**
1030
1102
  * Set liquid level
1031
- * @param models Single model or array of models
1103
+ * @param models Single model or array/iterable of models
1032
1104
  * @param percent Liquid level percentage 0~1
1033
1105
  */
1034
1106
  fillTo(models, percent) {
@@ -1037,7 +1109,7 @@ class LiquidFillerGroup {
1037
1109
  console.warn('LiquidFillerGroup: percent must be between 0 and 1', percent);
1038
1110
  percent = Math.max(0, Math.min(1, percent));
1039
1111
  }
1040
- const modelArray = Array.isArray(models) ? models : [models];
1112
+ const modelArray = this.normalizeInput(models);
1041
1113
  modelArray.forEach(model => {
1042
1114
  const item = this.items.find(i => i.model === model);
1043
1115
  if (!item) {
@@ -1196,7 +1268,6 @@ const EASING_FUNCTIONS = {
1196
1268
  * - Robust error handling
1197
1269
  */
1198
1270
  function followModels(camera, targets, options = {}) {
1199
- var _a, _b, _c, _d, _e, _f;
1200
1271
  // Cancel previous animation
1201
1272
  cancelFollow(camera);
1202
1273
  // Boundary check
@@ -1223,14 +1294,14 @@ function followModels(camera, targets, options = {}) {
1223
1294
  box.getBoundingSphere(sphere);
1224
1295
  const center = sphere.center.clone();
1225
1296
  const radiusBase = Math.max(0.001, sphere.radius);
1226
- const duration = (_a = options.duration) !== null && _a !== void 0 ? _a : 700;
1227
- const padding = (_b = options.padding) !== null && _b !== void 0 ? _b : 1.0;
1297
+ const duration = options.duration ?? 700;
1298
+ const padding = options.padding ?? 1.0;
1228
1299
  const minDistance = options.minDistance;
1229
1300
  const maxDistance = options.maxDistance;
1230
- const controls = (_c = options.controls) !== null && _c !== void 0 ? _c : null;
1231
- const azimuth = (_d = options.azimuth) !== null && _d !== void 0 ? _d : Math.PI / 4;
1232
- const elevation = (_e = options.elevation) !== null && _e !== void 0 ? _e : Math.PI / 4;
1233
- const easing = (_f = options.easing) !== null && _f !== void 0 ? _f : 'easeOut';
1301
+ const controls = options.controls ?? null;
1302
+ const azimuth = options.azimuth ?? Math.PI / 4;
1303
+ const elevation = options.elevation ?? Math.PI / 4;
1304
+ const easing = options.easing ?? 'easeOut';
1234
1305
  const onProgress = options.onProgress;
1235
1306
  // Get easing function
1236
1307
  const easingFn = EASING_FUNCTIONS[easing] || EASING_FUNCTIONS.easeOut;
@@ -1368,9 +1439,6 @@ function setView(camera, controls, targetObj, position = 'front', options = {})
1368
1439
  console.warn('setView: Failed to calculate bounding box');
1369
1440
  return Promise.reject(new Error('Invalid bounding box'));
1370
1441
  }
1371
- const center = box.getCenter(new THREE__namespace.Vector3());
1372
- const size = box.getSize(new THREE__namespace.Vector3());
1373
- const maxSize = Math.max(size.x, size.y, size.z);
1374
1442
  // Use mapping table for creating view angles
1375
1443
  const viewAngles = {
1376
1444
  'front': { azimuth: 0, elevation: 0 },
@@ -1422,37 +1490,22 @@ const ViewPresets = {
1422
1490
  top: (camera, controls, target, options) => setView(camera, controls, target, 'top', options)
1423
1491
  };
1424
1492
 
1425
- /******************************************************************************
1426
- Copyright (c) Microsoft Corporation.
1427
-
1428
- Permission to use, copy, modify, and/or distribute this software for any
1429
- purpose with or without fee is hereby granted.
1430
-
1431
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1432
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1433
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1434
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1435
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1436
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1437
- PERFORMANCE OF THIS SOFTWARE.
1438
- ***************************************************************************** */
1439
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
1440
-
1441
-
1442
- function __awaiter(thisArg, _arguments, P, generator) {
1443
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
1444
- return new (P || (P = Promise))(function (resolve, reject) {
1445
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
1446
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
1447
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
1448
- step((generator = generator.apply(thisArg, _arguments || [])).next());
1449
- });
1450
- }
1451
-
1452
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
1453
- var e = new Error(message);
1454
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1493
+ let globalConfig = {
1494
+ dracoDecoderPath: '/draco/',
1495
+ ktx2TranscoderPath: '/basis/',
1455
1496
  };
1497
+ /**
1498
+ * Update global loader configuration (e.g., set path to CDN)
1499
+ */
1500
+ function setLoaderConfig(config) {
1501
+ globalConfig = { ...globalConfig, ...config };
1502
+ }
1503
+ /**
1504
+ * Get current global loader configuration
1505
+ */
1506
+ function getLoaderConfig() {
1507
+ return globalConfig;
1508
+ }
1456
1509
 
1457
1510
  /**
1458
1511
  * @file modelLoader.ts
@@ -1470,19 +1523,22 @@ const DEFAULT_OPTIONS$1 = {
1470
1523
  maxTextureSize: null,
1471
1524
  useSimpleMaterials: false,
1472
1525
  skipSkinned: true,
1526
+ useCache: true,
1473
1527
  };
1528
+ const modelCache = new Map();
1474
1529
  /** Automatically determine which options to enable based on extension (smart judgment) */
1475
1530
  function normalizeOptions(url, opts) {
1476
1531
  const ext = (url.split('.').pop() || '').toLowerCase();
1477
- const merged = Object.assign(Object.assign({}, DEFAULT_OPTIONS$1), opts);
1532
+ const merged = { ...DEFAULT_OPTIONS$1, ...opts };
1478
1533
  if (ext === 'gltf' || ext === 'glb') {
1534
+ const globalConfig = getLoaderConfig();
1479
1535
  // gltf/glb defaults to trying draco/ktx2 if user didn't specify
1480
1536
  if (merged.dracoDecoderPath === undefined)
1481
- merged.dracoDecoderPath = '/draco/';
1537
+ merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
1482
1538
  if (merged.useKTX2 === undefined)
1483
1539
  merged.useKTX2 = true;
1484
1540
  if (merged.ktx2TranscoderPath === undefined)
1485
- merged.ktx2TranscoderPath = '/basis/';
1541
+ merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
1486
1542
  }
1487
1543
  else {
1488
1544
  // fbx/obj/ply/stl etc. do not need draco/ktx2
@@ -1492,103 +1548,108 @@ function normalizeOptions(url, opts) {
1492
1548
  }
1493
1549
  return merged;
1494
1550
  }
1495
- function loadModelByUrl(url_1) {
1496
- return __awaiter(this, arguments, void 0, function* (url, options = {}) {
1497
- var _a, _b;
1498
- if (!url)
1499
- throw new Error('url required');
1500
- const ext = (url.split('.').pop() || '').toLowerCase();
1501
- const opts = normalizeOptions(url, options);
1502
- const manager = (_a = opts.manager) !== null && _a !== void 0 ? _a : new THREE__namespace.LoadingManager();
1503
- let loader;
1504
- if (ext === 'gltf' || ext === 'glb') {
1505
- const { GLTFLoader } = yield import('three/examples/jsm/loaders/GLTFLoader.js');
1506
- const gltfLoader = new GLTFLoader(manager);
1507
- if (opts.dracoDecoderPath) {
1508
- const { DRACOLoader } = yield import('three/examples/jsm/loaders/DRACOLoader.js');
1509
- const draco = new DRACOLoader();
1510
- draco.setDecoderPath(opts.dracoDecoderPath);
1511
- gltfLoader.setDRACOLoader(draco);
1512
- }
1513
- if (opts.useKTX2 && opts.ktx2TranscoderPath) {
1514
- const { KTX2Loader } = yield import('three/examples/jsm/loaders/KTX2Loader.js');
1515
- const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
1516
- gltfLoader.__ktx2Loader = ktx2Loader;
1517
- }
1518
- loader = gltfLoader;
1519
- }
1520
- else if (ext === 'fbx') {
1521
- const { FBXLoader } = yield import('three/examples/jsm/loaders/FBXLoader.js');
1522
- loader = new FBXLoader(manager);
1523
- }
1524
- else if (ext === 'obj') {
1525
- const { OBJLoader } = yield import('three/examples/jsm/loaders/OBJLoader.js');
1526
- loader = new OBJLoader(manager);
1527
- }
1528
- else if (ext === 'ply') {
1529
- const { PLYLoader } = yield import('three/examples/jsm/loaders/PLYLoader.js');
1530
- loader = new PLYLoader(manager);
1531
- }
1532
- else if (ext === 'stl') {
1533
- const { STLLoader } = yield import('three/examples/jsm/loaders/STLLoader.js');
1534
- loader = new STLLoader(manager);
1551
+ async function loadModelByUrl(url, options = {}) {
1552
+ if (!url)
1553
+ throw new Error('url required');
1554
+ const ext = (url.split('.').pop() || '').toLowerCase();
1555
+ const opts = normalizeOptions(url, options);
1556
+ const manager = opts.manager ?? new THREE__namespace.LoadingManager();
1557
+ // Cache key includes URL and relevant optimization options
1558
+ const cacheKey = `${url}_${opts.mergeGeometries}_${opts.maxTextureSize}_${opts.useSimpleMaterials}`;
1559
+ if (opts.useCache && modelCache.has(cacheKey)) {
1560
+ return modelCache.get(cacheKey).clone();
1561
+ }
1562
+ let loader;
1563
+ if (ext === 'gltf' || ext === 'glb') {
1564
+ const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
1565
+ const gltfLoader = new GLTFLoader(manager);
1566
+ if (opts.dracoDecoderPath) {
1567
+ const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
1568
+ const draco = new DRACOLoader();
1569
+ draco.setDecoderPath(opts.dracoDecoderPath);
1570
+ gltfLoader.setDRACOLoader(draco);
1535
1571
  }
1536
- else {
1537
- throw new Error(`Unsupported model extension: .${ext}`);
1538
- }
1539
- const object = yield new Promise((resolve, reject) => {
1540
- loader.load(url, (res) => {
1541
- var _a;
1542
- if (ext === 'gltf' || ext === 'glb') {
1543
- const sceneObj = res.scene || res;
1544
- // --- Critical: Expose animations to scene.userData (or scene.animations) ---
1545
- // So the caller can access clips simply by getting sceneObj.userData.animations
1546
- sceneObj.userData = (sceneObj === null || sceneObj === void 0 ? void 0 : sceneObj.userData) || {};
1547
- sceneObj.userData.animations = (_a = res.animations) !== null && _a !== void 0 ? _a : [];
1548
- resolve(sceneObj);
1549
- }
1550
- else {
1551
- resolve(res);
1552
- }
1553
- }, undefined, (err) => reject(err));
1554
- });
1555
- // Optimize
1556
- object.traverse((child) => {
1557
- var _a, _b, _c;
1558
- const mesh = child;
1559
- if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
1560
- try {
1561
- mesh.geometry = (_c = (_b = (_a = new THREE__namespace.BufferGeometry()).fromGeometry) === null || _b === void 0 ? void 0 : _b.call(_a, mesh.geometry)) !== null && _c !== void 0 ? _c : mesh.geometry;
1562
- }
1563
- catch (_d) { }
1564
- }
1565
- });
1566
- if (opts.maxTextureSize && opts.maxTextureSize > 0)
1567
- downscaleTexturesInObject(object, opts.maxTextureSize);
1568
- if (opts.useSimpleMaterials) {
1569
- object.traverse((child) => {
1570
- const m = child.material;
1571
- if (!m)
1572
- return;
1573
- if (Array.isArray(m))
1574
- child.material = m.map((mat) => toSimpleMaterial(mat));
1575
- else
1576
- child.material = toSimpleMaterial(m);
1577
- });
1572
+ if (opts.useKTX2 && opts.ktx2TranscoderPath) {
1573
+ const { KTX2Loader } = await import('three/examples/jsm/loaders/KTX2Loader.js');
1574
+ const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
1575
+ gltfLoader.__ktx2Loader = ktx2Loader;
1578
1576
  }
1579
- if (opts.mergeGeometries) {
1580
- try {
1581
- yield tryMergeGeometries(object, { skipSkinned: (_b = opts.skipSkinned) !== null && _b !== void 0 ? _b : true });
1577
+ loader = gltfLoader;
1578
+ }
1579
+ else if (ext === 'fbx') {
1580
+ const { FBXLoader } = await import('three/examples/jsm/loaders/FBXLoader.js');
1581
+ loader = new FBXLoader(manager);
1582
+ }
1583
+ else if (ext === 'obj') {
1584
+ const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
1585
+ loader = new OBJLoader(manager);
1586
+ }
1587
+ else if (ext === 'ply') {
1588
+ const { PLYLoader } = await import('three/examples/jsm/loaders/PLYLoader.js');
1589
+ loader = new PLYLoader(manager);
1590
+ }
1591
+ else if (ext === 'stl') {
1592
+ const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
1593
+ loader = new STLLoader(manager);
1594
+ }
1595
+ else {
1596
+ throw new Error(`Unsupported model extension: .${ext}`);
1597
+ }
1598
+ const object = await new Promise((resolve, reject) => {
1599
+ loader.load(url, (res) => {
1600
+ if (ext === 'gltf' || ext === 'glb') {
1601
+ const sceneObj = res.scene || res;
1602
+ // --- Critical: Expose animations to scene.userData (or scene.animations) ---
1603
+ // So the caller can access clips simply by getting sceneObj.userData.animations
1604
+ sceneObj.userData = sceneObj?.userData || {};
1605
+ sceneObj.userData.animations = res.animations ?? [];
1606
+ resolve(sceneObj);
1582
1607
  }
1583
- catch (e) {
1584
- console.warn('mergeGeometries failed', e);
1608
+ else {
1609
+ resolve(res);
1610
+ }
1611
+ }, undefined, (err) => reject(err));
1612
+ });
1613
+ // Optimize
1614
+ object.traverse((child) => {
1615
+ const mesh = child;
1616
+ if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
1617
+ try {
1618
+ mesh.geometry = new THREE__namespace.BufferGeometry().fromGeometry?.(mesh.geometry) ?? mesh.geometry;
1585
1619
  }
1620
+ catch { }
1586
1621
  }
1587
- return object;
1588
1622
  });
1623
+ if (opts.maxTextureSize && opts.maxTextureSize > 0)
1624
+ await downscaleTexturesInObject(object, opts.maxTextureSize);
1625
+ if (opts.useSimpleMaterials) {
1626
+ object.traverse((child) => {
1627
+ const m = child.material;
1628
+ if (!m)
1629
+ return;
1630
+ if (Array.isArray(m))
1631
+ child.material = m.map((mat) => toSimpleMaterial(mat));
1632
+ else
1633
+ child.material = toSimpleMaterial(m);
1634
+ });
1635
+ }
1636
+ if (opts.mergeGeometries) {
1637
+ try {
1638
+ await tryMergeGeometries(object, { skipSkinned: opts.skipSkinned ?? true });
1639
+ }
1640
+ catch (e) {
1641
+ console.warn('mergeGeometries failed', e);
1642
+ }
1643
+ }
1644
+ if (opts.useCache) {
1645
+ modelCache.set(cacheKey, object);
1646
+ return object.clone();
1647
+ }
1648
+ return object;
1589
1649
  }
1590
- /** Runtime downscale textures in mesh to maxSize (canvas drawImage) to save GPU memory */
1591
- function downscaleTexturesInObject(obj, maxSize) {
1650
+ /** Runtime downscale textures in mesh to maxSize (createImageBitmap or canvas) to save GPU memory */
1651
+ async function downscaleTexturesInObject(obj, maxSize) {
1652
+ const tasks = [];
1592
1653
  obj.traverse((ch) => {
1593
1654
  if (!ch.isMesh)
1594
1655
  return;
@@ -1607,27 +1668,44 @@ function downscaleTexturesInObject(obj, maxSize) {
1607
1668
  const max = maxSize;
1608
1669
  if (image.width <= max && image.height <= max)
1609
1670
  return;
1610
- // downscale using canvas (sync, may be heavy for many textures)
1611
- try {
1612
- const scale = Math.min(max / image.width, max / image.height);
1613
- const canvas = document.createElement('canvas');
1614
- canvas.width = Math.floor(image.width * scale);
1615
- canvas.height = Math.floor(image.height * scale);
1616
- const ctx = canvas.getContext('2d');
1617
- if (ctx) {
1618
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1619
- const newTex = new THREE__namespace.Texture(canvas);
1620
- newTex.needsUpdate = true;
1621
- // copy common settings (encoding etc)
1622
- newTex.encoding = tex.encoding;
1623
- mat[p] = newTex;
1671
+ tasks.push((async () => {
1672
+ try {
1673
+ const scale = Math.min(max / image.width, max / image.height);
1674
+ const newWidth = Math.floor(image.width * scale);
1675
+ const newHeight = Math.floor(image.height * scale);
1676
+ let newSource;
1677
+ if (typeof createImageBitmap !== 'undefined') {
1678
+ newSource = await createImageBitmap(image, {
1679
+ resizeWidth: newWidth,
1680
+ resizeHeight: newHeight,
1681
+ resizeQuality: 'high'
1682
+ });
1683
+ }
1684
+ else {
1685
+ // Fallback for environments without createImageBitmap
1686
+ const canvas = document.createElement('canvas');
1687
+ canvas.width = newWidth;
1688
+ canvas.height = newHeight;
1689
+ const ctx = canvas.getContext('2d');
1690
+ if (ctx) {
1691
+ ctx.drawImage(image, 0, 0, newWidth, newHeight);
1692
+ newSource = canvas;
1693
+ }
1694
+ }
1695
+ if (newSource) {
1696
+ const newTex = new THREE__namespace.Texture(newSource);
1697
+ newTex.needsUpdate = true;
1698
+ newTex.encoding = tex.encoding;
1699
+ mat[p] = newTex;
1700
+ }
1624
1701
  }
1625
- }
1626
- catch (e) {
1627
- console.warn('downscale texture failed', e);
1628
- }
1702
+ catch (e) {
1703
+ console.warn('downscale texture failed', e);
1704
+ }
1705
+ })());
1629
1706
  });
1630
1707
  });
1708
+ await Promise.all(tasks);
1631
1709
  }
1632
1710
  /**
1633
1711
  * Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
@@ -1635,81 +1713,77 @@ function downscaleTexturesInObject(obj, maxSize) {
1635
1713
  * - Merging will group by material UUID (different materials cannot be merged)
1636
1714
  * - Merge function is compatible with common export names of BufferGeometryUtils
1637
1715
  */
1638
- function tryMergeGeometries(root, opts) {
1639
- return __awaiter(this, void 0, void 0, function* () {
1640
- // collect meshes by material uuid
1641
- const groups = new Map();
1642
- root.traverse((ch) => {
1643
- var _a;
1644
- if (!ch.isMesh)
1645
- return;
1646
- const mesh = ch;
1647
- if (opts.skipSkinned && mesh.isSkinnedMesh)
1648
- return;
1649
- const mat = mesh.material;
1650
- // don't merge transparent or morph-enabled or skinned meshes
1651
- if (!mesh.geometry || mesh.visible === false)
1652
- return;
1653
- if (mat && mat.transparent)
1654
- return;
1655
- const geom = mesh.geometry.clone();
1656
- mesh.updateWorldMatrix(true, false);
1657
- geom.applyMatrix4(mesh.matrixWorld);
1658
- // ensure attributes compatible? we'll rely on merge function to return null if incompatible
1659
- const key = (mat && mat.uuid) || 'default';
1660
- const bucket = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { material: mat !== null && mat !== void 0 ? mat : new THREE__namespace.MeshStandardMaterial(), geoms: [] };
1661
- bucket.geoms.push(geom);
1662
- groups.set(key, bucket);
1663
- // mark for removal (we'll remove meshes after)
1664
- mesh.userData.__toRemoveForMerge = true;
1665
- });
1666
- if (groups.size === 0)
1716
+ async function tryMergeGeometries(root, opts) {
1717
+ // collect meshes by material uuid
1718
+ const groups = new Map();
1719
+ root.traverse((ch) => {
1720
+ if (!ch.isMesh)
1667
1721
  return;
1668
- // dynamic import BufferGeometryUtils and find merge function name
1669
- const bufUtilsMod = yield import('three/examples/jsm/utils/BufferGeometryUtils.js');
1670
- // use || chain (avoid mixing ?? with || without parentheses)
1671
- const mergeFn = bufUtilsMod.mergeBufferGeometries ||
1672
- bufUtilsMod.mergeGeometries ||
1673
- bufUtilsMod.mergeBufferGeometries || // defensive duplicate
1674
- bufUtilsMod.mergeGeometries;
1675
- if (!mergeFn)
1676
- throw new Error('No merge function found in BufferGeometryUtils');
1677
- // for each group, try merge
1678
- for (const [key, { material, geoms }] of groups) {
1679
- if (geoms.length <= 1) {
1680
- // nothing to merge
1681
- continue;
1682
- }
1683
- // call merge function - signature typically mergeBufferGeometries(array, useGroups)
1684
- const merged = mergeFn(geoms, false);
1685
- if (!merged) {
1686
- console.warn('merge returned null for group', key);
1687
- continue;
1722
+ const mesh = ch;
1723
+ if (opts.skipSkinned && mesh.isSkinnedMesh)
1724
+ return;
1725
+ const mat = mesh.material;
1726
+ // don't merge transparent or morph-enabled or skinned meshes
1727
+ if (!mesh.geometry || mesh.visible === false)
1728
+ return;
1729
+ if (mat && mat.transparent)
1730
+ return;
1731
+ const geom = mesh.geometry.clone();
1732
+ mesh.updateWorldMatrix(true, false);
1733
+ geom.applyMatrix4(mesh.matrixWorld);
1734
+ // ensure attributes compatible? we'll rely on merge function to return null if incompatible
1735
+ const key = (mat && mat.uuid) || 'default';
1736
+ const bucket = groups.get(key) ?? { material: mat ?? new THREE__namespace.MeshStandardMaterial(), geoms: [] };
1737
+ bucket.geoms.push(geom);
1738
+ groups.set(key, bucket);
1739
+ // mark for removal (we'll remove meshes after)
1740
+ mesh.userData.__toRemoveForMerge = true;
1741
+ });
1742
+ if (groups.size === 0)
1743
+ return;
1744
+ // dynamic import BufferGeometryUtils and find merge function name
1745
+ const bufUtilsMod = await import('three/examples/jsm/utils/BufferGeometryUtils.js');
1746
+ // use || chain (avoid mixing ?? with || without parentheses)
1747
+ const mergeFn = bufUtilsMod.mergeBufferGeometries ||
1748
+ bufUtilsMod.mergeGeometries ||
1749
+ bufUtilsMod.mergeBufferGeometries || // defensive duplicate
1750
+ bufUtilsMod.mergeGeometries;
1751
+ if (!mergeFn)
1752
+ throw new Error('No merge function found in BufferGeometryUtils');
1753
+ // for each group, try merge
1754
+ for (const [key, { material, geoms }] of groups) {
1755
+ if (geoms.length <= 1) {
1756
+ // nothing to merge
1757
+ continue;
1758
+ }
1759
+ // call merge function - signature typically mergeBufferGeometries(array, useGroups)
1760
+ const merged = mergeFn(geoms, false);
1761
+ if (!merged) {
1762
+ console.warn('merge returned null for group', key);
1763
+ continue;
1764
+ }
1765
+ // create merged mesh at root (world-space geometry already applied)
1766
+ const mergedMesh = new THREE__namespace.Mesh(merged, material);
1767
+ root.add(mergedMesh);
1768
+ }
1769
+ // now remove original meshes flagged for removal
1770
+ const toRemove = [];
1771
+ root.traverse((ch) => {
1772
+ if (ch.userData?.__toRemoveForMerge)
1773
+ toRemove.push(ch);
1774
+ });
1775
+ toRemove.forEach((m) => {
1776
+ if (m.parent)
1777
+ m.parent.remove(m);
1778
+ // free original resources (geometries already cloned/applied), but careful with shared materials
1779
+ if (m.isMesh) {
1780
+ const mm = m;
1781
+ try {
1782
+ mm.geometry.dispose();
1688
1783
  }
1689
- // create merged mesh at root (world-space geometry already applied)
1690
- const mergedMesh = new THREE__namespace.Mesh(merged, material);
1691
- root.add(mergedMesh);
1784
+ catch { }
1785
+ // we do NOT dispose material because it may be reused by mergedMesh
1692
1786
  }
1693
- // now remove original meshes flagged for removal
1694
- const toRemove = [];
1695
- root.traverse((ch) => {
1696
- var _a;
1697
- if ((_a = ch.userData) === null || _a === void 0 ? void 0 : _a.__toRemoveForMerge)
1698
- toRemove.push(ch);
1699
- });
1700
- toRemove.forEach((m) => {
1701
- if (m.parent)
1702
- m.parent.remove(m);
1703
- // free original resources (geometries already cloned/applied), but careful with shared materials
1704
- if (m.isMesh) {
1705
- const mm = m;
1706
- try {
1707
- mm.geometry.dispose();
1708
- }
1709
- catch (_a) { }
1710
- // we do NOT dispose material because it may be reused by mergedMesh
1711
- }
1712
- });
1713
1787
  });
1714
1788
  }
1715
1789
  /* ---------------------
@@ -1726,7 +1800,7 @@ function disposeObject(obj) {
1726
1800
  try {
1727
1801
  m.geometry.dispose();
1728
1802
  }
1729
- catch (_a) { }
1803
+ catch { }
1730
1804
  }
1731
1805
  const mat = m.material;
1732
1806
  if (mat) {
@@ -1748,14 +1822,14 @@ function disposeMaterial(mat) {
1748
1822
  try {
1749
1823
  mat[k].dispose();
1750
1824
  }
1751
- catch (_a) { }
1825
+ catch { }
1752
1826
  }
1753
1827
  });
1754
1828
  try {
1755
1829
  if (typeof mat.dispose === 'function')
1756
1830
  mat.dispose();
1757
1831
  }
1758
- catch (_a) { }
1832
+ catch { }
1759
1833
  }
1760
1834
  // Helper to convert to simple material (stub)
1761
1835
  function toSimpleMaterial(mat) {
@@ -1798,100 +1872,97 @@ const equirectCache = new Map();
1798
1872
  * @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
1799
1873
  * @param opts SkyboxOptions
1800
1874
  */
1801
- function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1802
- return __awaiter(this, arguments, void 0, function* (renderer, scene, paths, opts = {}) {
1803
- var _a, _b;
1804
- const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
1805
- if (!Array.isArray(paths) || paths.length !== 6)
1806
- throw new Error('cube skybox requires 6 image paths');
1807
- const key = paths.join('|');
1808
- // Cache handling
1809
- if (options.cache && cubeCache.has(key)) {
1810
- const rec = cubeCache.get(key);
1811
- rec.refCount += 1;
1812
- // reapply to scene (in case it was removed)
1813
- if (options.setAsBackground)
1814
- scene.background = rec.handle.backgroundTexture;
1815
- if (options.setAsEnvironment && rec.handle.envRenderTarget)
1816
- scene.environment = rec.handle.envRenderTarget.texture;
1817
- return rec.handle;
1818
- }
1819
- // Load cube texture
1820
- const loader = new THREE__namespace.CubeTextureLoader();
1821
- const texture = yield new Promise((resolve, reject) => {
1822
- loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
1823
- });
1824
- // Set encoding and mapping
1825
- if (options.useSRGBEncoding)
1826
- texture.encoding = THREE__namespace.sRGBEncoding;
1827
- texture.mapping = THREE__namespace.CubeReflectionMapping;
1828
- // apply as background if required
1875
+ async function loadCubeSkybox(renderer, scene, paths, opts = {}) {
1876
+ const options = { ...DEFAULT_OPTIONS, ...opts };
1877
+ if (!Array.isArray(paths) || paths.length !== 6)
1878
+ throw new Error('cube skybox requires 6 image paths');
1879
+ const key = paths.join('|');
1880
+ // Cache handling
1881
+ if (options.cache && cubeCache.has(key)) {
1882
+ const rec = cubeCache.get(key);
1883
+ rec.refCount += 1;
1884
+ // reapply to scene (in case it was removed)
1829
1885
  if (options.setAsBackground)
1830
- scene.background = texture;
1831
- // environment: use PMREM to produce a proper prefiltered env map for PBR
1832
- let pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE__namespace.PMREMGenerator(renderer);
1833
- (_b = pmremGenerator.compileCubemapShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
1834
- // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
1835
- let envRenderTarget = null;
1836
- if (pmremGenerator.fromCubemap) {
1837
- envRenderTarget = pmremGenerator.fromCubemap(texture);
1886
+ scene.background = rec.handle.backgroundTexture;
1887
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
1888
+ scene.environment = rec.handle.envRenderTarget.texture;
1889
+ return rec.handle;
1890
+ }
1891
+ // Load cube texture
1892
+ const loader = new THREE__namespace.CubeTextureLoader();
1893
+ const texture = await new Promise((resolve, reject) => {
1894
+ loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
1895
+ });
1896
+ // Set encoding and mapping
1897
+ if (options.useSRGBEncoding)
1898
+ texture.encoding = THREE__namespace.sRGBEncoding;
1899
+ texture.mapping = THREE__namespace.CubeReflectionMapping;
1900
+ // apply as background if required
1901
+ if (options.setAsBackground)
1902
+ scene.background = texture;
1903
+ // environment: use PMREM to produce a proper prefiltered env map for PBR
1904
+ let pmremGenerator = options.pmremGenerator ?? new THREE__namespace.PMREMGenerator(renderer);
1905
+ pmremGenerator.compileCubemapShader?.( /* optional */);
1906
+ // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
1907
+ let envRenderTarget = null;
1908
+ if (pmremGenerator.fromCubemap) {
1909
+ envRenderTarget = pmremGenerator.fromCubemap(texture);
1910
+ }
1911
+ else {
1912
+ // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
1913
+ // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
1914
+ envRenderTarget = null;
1915
+ }
1916
+ if (options.setAsEnvironment) {
1917
+ if (envRenderTarget) {
1918
+ scene.environment = envRenderTarget.texture;
1838
1919
  }
1839
1920
  else {
1840
- // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
1841
- // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
1842
- envRenderTarget = null;
1921
+ // fallback: use cube texture directly (works but not prefiltered)
1922
+ scene.environment = texture;
1843
1923
  }
1844
- if (options.setAsEnvironment) {
1845
- if (envRenderTarget) {
1846
- scene.environment = envRenderTarget.texture;
1924
+ }
1925
+ const handle = {
1926
+ key,
1927
+ backgroundTexture: options.setAsBackground ? texture : null,
1928
+ envRenderTarget: envRenderTarget,
1929
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
1930
+ setAsBackground: !!options.setAsBackground,
1931
+ setAsEnvironment: !!options.setAsEnvironment,
1932
+ dispose() {
1933
+ // remove from scene
1934
+ if (options.setAsBackground && scene.background === texture)
1935
+ scene.background = null;
1936
+ if (options.setAsEnvironment && scene.environment) {
1937
+ // only clear if it's the same texture we set
1938
+ if (envRenderTarget && scene.environment === envRenderTarget.texture)
1939
+ scene.environment = null;
1940
+ else if (scene.environment === texture)
1941
+ scene.environment = null;
1847
1942
  }
1848
- else {
1849
- // fallback: use cube texture directly (works but not prefiltered)
1850
- scene.environment = texture;
1851
- }
1852
- }
1853
- const handle = {
1854
- key,
1855
- backgroundTexture: options.setAsBackground ? texture : null,
1856
- envRenderTarget: envRenderTarget,
1857
- pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
1858
- setAsBackground: !!options.setAsBackground,
1859
- setAsEnvironment: !!options.setAsEnvironment,
1860
- dispose() {
1861
- // remove from scene
1862
- if (options.setAsBackground && scene.background === texture)
1863
- scene.background = null;
1864
- if (options.setAsEnvironment && scene.environment) {
1865
- // only clear if it's the same texture we set
1866
- if (envRenderTarget && scene.environment === envRenderTarget.texture)
1867
- scene.environment = null;
1868
- else if (scene.environment === texture)
1869
- scene.environment = null;
1870
- }
1871
- // dispose resources only if not cached/shared
1872
- if (envRenderTarget) {
1873
- try {
1874
- envRenderTarget.dispose();
1875
- }
1876
- catch (_a) { }
1877
- }
1943
+ // dispose resources only if not cached/shared
1944
+ if (envRenderTarget) {
1878
1945
  try {
1879
- texture.dispose();
1946
+ envRenderTarget.dispose();
1880
1947
  }
1881
- catch (_b) { }
1882
- // dispose pmremGenerator we created
1883
- if (!options.pmremGenerator && pmremGenerator) {
1884
- try {
1885
- pmremGenerator.dispose();
1886
- }
1887
- catch (_c) { }
1948
+ catch { }
1949
+ }
1950
+ try {
1951
+ texture.dispose();
1952
+ }
1953
+ catch { }
1954
+ // dispose pmremGenerator we created
1955
+ if (!options.pmremGenerator && pmremGenerator) {
1956
+ try {
1957
+ pmremGenerator.dispose();
1888
1958
  }
1959
+ catch { }
1889
1960
  }
1890
- };
1891
- if (options.cache)
1892
- cubeCache.set(key, { handle, refCount: 1 });
1893
- return handle;
1894
- });
1961
+ }
1962
+ };
1963
+ if (options.cache)
1964
+ cubeCache.set(key, { handle, refCount: 1 });
1965
+ return handle;
1895
1966
  }
1896
1967
  /**
1897
1968
  * Load Equirectangular/Single Image (Supports HDR via RGBELoader)
@@ -1900,95 +1971,90 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1900
1971
  * @param url string - *.hdr, *.exr, *.jpg, *.png
1901
1972
  * @param opts SkyboxOptions
1902
1973
  */
1903
- function loadEquirectSkybox(renderer_1, scene_1, url_1) {
1904
- return __awaiter(this, arguments, void 0, function* (renderer, scene, url, opts = {}) {
1905
- var _a, _b;
1906
- const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
1907
- const key = url;
1908
- if (options.cache && equirectCache.has(key)) {
1909
- const rec = equirectCache.get(key);
1910
- rec.refCount += 1;
1911
- if (options.setAsBackground)
1912
- scene.background = rec.handle.backgroundTexture;
1913
- if (options.setAsEnvironment && rec.handle.envRenderTarget)
1914
- scene.environment = rec.handle.envRenderTarget.texture;
1915
- return rec.handle;
1916
- }
1917
- // Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
1918
- const isHDR = /\.hdr$|\.exr$/i.test(url);
1919
- let hdrTexture;
1920
- if (isHDR) {
1921
- const { RGBELoader } = yield import('three/examples/jsm/loaders/RGBELoader.js');
1922
- hdrTexture = yield new Promise((resolve, reject) => {
1923
- new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
1924
- });
1925
- // RGBE textures typically use LinearEncoding
1926
- hdrTexture.encoding = THREE__namespace.LinearEncoding;
1927
- }
1928
- else {
1929
- // ordinary image - use TextureLoader
1930
- const loader = new THREE__namespace.TextureLoader();
1931
- hdrTexture = yield new Promise((resolve, reject) => {
1932
- loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
1933
- });
1934
- if (options.useSRGBEncoding)
1935
- hdrTexture.encoding = THREE__namespace.sRGBEncoding;
1936
- }
1937
- // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
1938
- const pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE__namespace.PMREMGenerator(renderer);
1939
- (_b = pmremGenerator.compileEquirectangularShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
1940
- const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
1941
- // envTexture to use for scene.environment
1942
- const envTexture = envRenderTarget.texture;
1943
- // set background and/or environment
1944
- if (options.setAsBackground) {
1945
- // for background it's ok to use the equirect texture directly or the envTexture
1946
- // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
1947
- scene.background = envTexture;
1948
- }
1949
- if (options.setAsEnvironment) {
1950
- scene.environment = envTexture;
1951
- }
1952
- // We can dispose the original hdrTexture (the PMREM target contains the needed data)
1953
- try {
1954
- hdrTexture.dispose();
1955
- }
1956
- catch (_c) { }
1957
- const handle = {
1958
- key,
1959
- backgroundTexture: options.setAsBackground ? envTexture : null,
1960
- envRenderTarget,
1961
- pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
1962
- setAsBackground: !!options.setAsBackground,
1963
- setAsEnvironment: !!options.setAsEnvironment,
1964
- dispose() {
1965
- if (options.setAsBackground && scene.background === envTexture)
1966
- scene.background = null;
1967
- if (options.setAsEnvironment && scene.environment === envTexture)
1968
- scene.environment = null;
1974
+ async function loadEquirectSkybox(renderer, scene, url, opts = {}) {
1975
+ const options = { ...DEFAULT_OPTIONS, ...opts };
1976
+ const key = url;
1977
+ if (options.cache && equirectCache.has(key)) {
1978
+ const rec = equirectCache.get(key);
1979
+ rec.refCount += 1;
1980
+ if (options.setAsBackground)
1981
+ scene.background = rec.handle.backgroundTexture;
1982
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
1983
+ scene.environment = rec.handle.envRenderTarget.texture;
1984
+ return rec.handle;
1985
+ }
1986
+ // Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
1987
+ const isHDR = /\.hdr$|\.exr$/i.test(url);
1988
+ let hdrTexture;
1989
+ if (isHDR) {
1990
+ const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js');
1991
+ hdrTexture = await new Promise((resolve, reject) => {
1992
+ new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
1993
+ });
1994
+ // RGBE textures typically use LinearEncoding
1995
+ hdrTexture.encoding = THREE__namespace.LinearEncoding;
1996
+ }
1997
+ else {
1998
+ // ordinary image - use TextureLoader
1999
+ const loader = new THREE__namespace.TextureLoader();
2000
+ hdrTexture = await new Promise((resolve, reject) => {
2001
+ loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
2002
+ });
2003
+ if (options.useSRGBEncoding)
2004
+ hdrTexture.encoding = THREE__namespace.sRGBEncoding;
2005
+ }
2006
+ // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
2007
+ const pmremGenerator = options.pmremGenerator ?? new THREE__namespace.PMREMGenerator(renderer);
2008
+ pmremGenerator.compileEquirectangularShader?.();
2009
+ const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
2010
+ // envTexture to use for scene.environment
2011
+ const envTexture = envRenderTarget.texture;
2012
+ // set background and/or environment
2013
+ if (options.setAsBackground) {
2014
+ // for background it's ok to use the equirect texture directly or the envTexture
2015
+ // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
2016
+ scene.background = envTexture;
2017
+ }
2018
+ if (options.setAsEnvironment) {
2019
+ scene.environment = envTexture;
2020
+ }
2021
+ // We can dispose the original hdrTexture (the PMREM target contains the needed data)
2022
+ try {
2023
+ hdrTexture.dispose();
2024
+ }
2025
+ catch { }
2026
+ const handle = {
2027
+ key,
2028
+ backgroundTexture: options.setAsBackground ? envTexture : null,
2029
+ envRenderTarget,
2030
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
2031
+ setAsBackground: !!options.setAsBackground,
2032
+ setAsEnvironment: !!options.setAsEnvironment,
2033
+ dispose() {
2034
+ if (options.setAsBackground && scene.background === envTexture)
2035
+ scene.background = null;
2036
+ if (options.setAsEnvironment && scene.environment === envTexture)
2037
+ scene.environment = null;
2038
+ try {
2039
+ envRenderTarget.dispose();
2040
+ }
2041
+ catch { }
2042
+ if (!options.pmremGenerator && pmremGenerator) {
1969
2043
  try {
1970
- envRenderTarget.dispose();
1971
- }
1972
- catch (_a) { }
1973
- if (!options.pmremGenerator && pmremGenerator) {
1974
- try {
1975
- pmremGenerator.dispose();
1976
- }
1977
- catch (_b) { }
2044
+ pmremGenerator.dispose();
1978
2045
  }
2046
+ catch { }
1979
2047
  }
1980
- };
1981
- if (options.cache)
1982
- equirectCache.set(key, { handle, refCount: 1 });
1983
- return handle;
1984
- });
2048
+ }
2049
+ };
2050
+ if (options.cache)
2051
+ equirectCache.set(key, { handle, refCount: 1 });
2052
+ return handle;
1985
2053
  }
1986
- function loadSkybox(renderer_1, scene_1, params_1) {
1987
- return __awaiter(this, arguments, void 0, function* (renderer, scene, params, opts = {}) {
1988
- if (params.type === 'cube')
1989
- return loadCubeSkybox(renderer, scene, params.paths, opts);
1990
- return loadEquirectSkybox(renderer, scene, params.url, opts);
1991
- });
2054
+ async function loadSkybox(renderer, scene, params, opts = {}) {
2055
+ if (params.type === 'cube')
2056
+ return loadCubeSkybox(renderer, scene, params.paths, opts);
2057
+ return loadEquirectSkybox(renderer, scene, params.url, opts);
1992
2058
  }
1993
2059
  /* -------------------------
1994
2060
  Cache / Reference Counting Helper Methods
@@ -2198,10 +2264,9 @@ class BlueSkyManager {
2198
2264
  * Usually called when the scene is completely destroyed or the application exits
2199
2265
  */
2200
2266
  destroy() {
2201
- var _a;
2202
2267
  this.cancelLoad();
2203
2268
  this.dispose();
2204
- (_a = this.pmremGen) === null || _a === void 0 ? void 0 : _a.dispose();
2269
+ this.pmremGen?.dispose();
2205
2270
  this.isInitialized = false;
2206
2271
  this.loadingState = 'idle';
2207
2272
  }
@@ -2235,20 +2300,19 @@ const BlueSky = new BlueSkyManager();
2235
2300
  * - RAF management optimization
2236
2301
  */
2237
2302
  function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, options) {
2238
- var _a, _b, _c, _d, _e, _f;
2239
2303
  const cfg = {
2240
- fontSize: (options === null || options === void 0 ? void 0 : options.fontSize) || '12px',
2241
- color: (options === null || options === void 0 ? void 0 : options.color) || '#ffffff',
2242
- background: (options === null || options === void 0 ? void 0 : options.background) || '#1890ff',
2243
- padding: (options === null || options === void 0 ? void 0 : options.padding) || '6px 10px',
2244
- borderRadius: (options === null || options === void 0 ? void 0 : options.borderRadius) || '6px',
2245
- lift: (_a = options === null || options === void 0 ? void 0 : options.lift) !== null && _a !== void 0 ? _a : 100,
2246
- dotSize: (_b = options === null || options === void 0 ? void 0 : options.dotSize) !== null && _b !== void 0 ? _b : 6,
2247
- dotSpacing: (_c = options === null || options === void 0 ? void 0 : options.dotSpacing) !== null && _c !== void 0 ? _c : 2,
2248
- lineColor: (options === null || options === void 0 ? void 0 : options.lineColor) || 'rgba(200,200,200,0.7)',
2249
- lineWidth: (_d = options === null || options === void 0 ? void 0 : options.lineWidth) !== null && _d !== void 0 ? _d : 1,
2250
- updateInterval: (_e = options === null || options === void 0 ? void 0 : options.updateInterval) !== null && _e !== void 0 ? _e : 0, // Default update every frame
2251
- fadeInDuration: (_f = options === null || options === void 0 ? void 0 : options.fadeInDuration) !== null && _f !== void 0 ? _f : 300, // Fade-in duration
2304
+ fontSize: options?.fontSize || '12px',
2305
+ color: options?.color || '#ffffff',
2306
+ background: options?.background || '#1890ff',
2307
+ padding: options?.padding || '6px 10px',
2308
+ borderRadius: options?.borderRadius || '6px',
2309
+ lift: options?.lift ?? 100,
2310
+ dotSize: options?.dotSize ?? 6,
2311
+ dotSpacing: options?.dotSpacing ?? 2,
2312
+ lineColor: options?.lineColor || 'rgba(200,200,200,0.7)',
2313
+ lineWidth: options?.lineWidth ?? 1,
2314
+ updateInterval: options?.updateInterval ?? 0, // Default update every frame
2315
+ fadeInDuration: options?.fadeInDuration ?? 300, // Fade-in duration
2252
2316
  };
2253
2317
  const container = document.createElement('div');
2254
2318
  container.style.position = 'absolute';
@@ -2271,7 +2335,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2271
2335
  svg.style.zIndex = '1';
2272
2336
  container.appendChild(svg);
2273
2337
  let currentModel = parentModel;
2274
- let currentLabelsMap = Object.assign({}, modelLabelsMap);
2338
+ let currentLabelsMap = { ...modelLabelsMap };
2275
2339
  let labels = [];
2276
2340
  let isActive = true;
2277
2341
  let isPaused = false;
@@ -2352,9 +2416,8 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2352
2416
  if (!currentModel)
2353
2417
  return;
2354
2418
  currentModel.traverse((child) => {
2355
- var _a;
2356
2419
  if (child.isMesh || child.type === 'Group') {
2357
- const labelText = (_a = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))) === null || _a === void 0 ? void 0 : _a[1];
2420
+ const labelText = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))?.[1];
2358
2421
  if (!labelText)
2359
2422
  return;
2360
2423
  const wrapper = document.createElement('div');
@@ -2459,7 +2522,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2459
2522
  rebuildLabels();
2460
2523
  },
2461
2524
  updateLabelsMap(newMap) {
2462
- currentLabelsMap = Object.assign({}, newMap);
2525
+ currentLabelsMap = { ...newMap };
2463
2526
  rebuildLabels();
2464
2527
  },
2465
2528
  // Pause update
@@ -2557,205 +2620,197 @@ class GroupExploder {
2557
2620
  * @param newSet The new set of meshes
2558
2621
  * @param contextId Optional context ID to distinguish business scenarios
2559
2622
  */
2560
- setMeshes(newSet, options) {
2561
- return __awaiter(this, void 0, void 0, function* () {
2562
- var _a, _b;
2563
- const autoRestorePrev = (_a = options === null || options === void 0 ? void 0 : options.autoRestorePrev) !== null && _a !== void 0 ? _a : true;
2564
- const restoreDuration = (_b = options === null || options === void 0 ? void 0 : options.restoreDuration) !== null && _b !== void 0 ? _b : 300;
2565
- this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
2566
- // If the newSet is null and currentSet is null -> nothing
2567
- if (!newSet && !this.currentSet) {
2568
- this.log('setMeshes: both newSet and currentSet are null, nothing to do');
2569
- return;
2570
- }
2571
- // If both exist and are the same reference, we still must detect content changes.
2572
- const sameReference = this.currentSet === newSet;
2573
- // Prepare prevSet snapshot (we copy current to prev)
2574
- if (this.currentSet) {
2575
- this.prevSet = this.currentSet;
2576
- this.prevStateMap = new Map(this.stateMap);
2577
- this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
2578
- }
2579
- else {
2580
- this.prevSet = null;
2581
- this.prevStateMap = new Map();
2582
- }
2583
- // If we used to be exploded and need to restore prevSet, do that first (await)
2584
- if (this.prevSet && autoRestorePrev && this.isExploded) {
2585
- this.log('setMeshes: need to restore prevSet before applying newSet');
2586
- yield this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
2587
- this.log('setMeshes: prevSet restore done');
2588
- this.prevStateMap.clear();
2589
- this.prevSet = null;
2590
- }
2591
- // Now register newSet: we clear and rebuild stateMap carefully.
2592
- // But we must handle the case where caller reuses same Set object and just mutated elements.
2593
- // We will compute additions and removals.
2594
- const oldSet = this.currentSet;
2595
- this.currentSet = newSet;
2596
- // If newSet is null -> simply clear stateMap
2597
- if (!this.currentSet) {
2598
- this.stateMap.clear();
2599
- this.log('setMeshes: newSet is null -> cleared stateMap');
2600
- this.isExploded = false;
2623
+ async setMeshes(newSet, options) {
2624
+ const autoRestorePrev = options?.autoRestorePrev ?? true;
2625
+ const restoreDuration = options?.restoreDuration ?? 300;
2626
+ this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
2627
+ // If the newSet is null and currentSet is null -> nothing
2628
+ if (!newSet && !this.currentSet) {
2629
+ this.log('setMeshes: both newSet and currentSet are null, nothing to do');
2630
+ return;
2631
+ }
2632
+ // If both exist and are the same reference, we still must detect content changes.
2633
+ const sameReference = this.currentSet === newSet;
2634
+ // Prepare prevSet snapshot (we copy current to prev)
2635
+ if (this.currentSet) {
2636
+ this.prevSet = this.currentSet;
2637
+ this.prevStateMap = new Map(this.stateMap);
2638
+ this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
2639
+ }
2640
+ else {
2641
+ this.prevSet = null;
2642
+ this.prevStateMap = new Map();
2643
+ }
2644
+ // If we used to be exploded and need to restore prevSet, do that first (await)
2645
+ if (this.prevSet && autoRestorePrev && this.isExploded) {
2646
+ this.log('setMeshes: need to restore prevSet before applying newSet');
2647
+ await this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
2648
+ this.log('setMeshes: prevSet restore done');
2649
+ this.prevStateMap.clear();
2650
+ this.prevSet = null;
2651
+ }
2652
+ // Now register newSet: we clear and rebuild stateMap carefully.
2653
+ // But we must handle the case where caller reuses same Set object and just mutated elements.
2654
+ // We will compute additions and removals.
2655
+ const oldSet = this.currentSet;
2656
+ this.currentSet = newSet;
2657
+ // If newSet is null -> simply clear stateMap
2658
+ if (!this.currentSet) {
2659
+ this.stateMap.clear();
2660
+ this.log('setMeshes: newSet is null -> cleared stateMap');
2661
+ this.isExploded = false;
2662
+ return;
2663
+ }
2664
+ // If we have oldSet (could be same reference) then compute diffs
2665
+ if (oldSet) {
2666
+ // If same reference but size or content differs -> handle diffs
2667
+ const wasSameRef = sameReference;
2668
+ let added = [];
2669
+ let removed = [];
2670
+ // Build maps of membership
2671
+ const oldMembers = new Set(Array.from(oldSet));
2672
+ const newMembers = new Set(Array.from(this.currentSet));
2673
+ // find removals
2674
+ oldMembers.forEach((m) => {
2675
+ if (!newMembers.has(m))
2676
+ removed.push(m);
2677
+ });
2678
+ // find additions
2679
+ newMembers.forEach((m) => {
2680
+ if (!oldMembers.has(m))
2681
+ added.push(m);
2682
+ });
2683
+ if (wasSameRef && added.length === 0 && removed.length === 0) {
2684
+ // truly identical (no content changes)
2685
+ this.log('setMeshes: same reference and identical contents -> nothing to update');
2601
2686
  return;
2602
2687
  }
2603
- // If we have oldSet (could be same reference) then compute diffs
2604
- if (oldSet) {
2605
- // If same reference but size or content differs -> handle diffs
2606
- const wasSameRef = sameReference;
2607
- let added = [];
2608
- let removed = [];
2609
- // Build maps of membership
2610
- const oldMembers = new Set(Array.from(oldSet));
2611
- const newMembers = new Set(Array.from(this.currentSet));
2612
- // find removals
2613
- oldMembers.forEach((m) => {
2614
- if (!newMembers.has(m))
2615
- removed.push(m);
2616
- });
2617
- // find additions
2618
- newMembers.forEach((m) => {
2619
- if (!oldMembers.has(m))
2620
- added.push(m);
2621
- });
2622
- if (wasSameRef && added.length === 0 && removed.length === 0) {
2623
- // truly identical (no content changes)
2624
- this.log('setMeshes: same reference and identical contents -> nothing to update');
2625
- return;
2688
+ this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
2689
+ // Remove snapshots for removed meshes
2690
+ removed.forEach((m) => {
2691
+ if (this.stateMap.has(m)) {
2692
+ this.stateMap.delete(m);
2626
2693
  }
2627
- this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
2628
- // Remove snapshots for removed meshes
2629
- removed.forEach((m) => {
2630
- if (this.stateMap.has(m)) {
2631
- this.stateMap.delete(m);
2632
- }
2633
- });
2634
- // Ensure snapshots exist for current set members (create for newly added meshes)
2635
- yield this.ensureSnapshotsForSet(this.currentSet);
2636
- this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
2637
- this.isExploded = false;
2638
- return;
2639
- }
2640
- else {
2641
- // no oldSet -> brand new registration
2642
- this.stateMap.clear();
2643
- yield this.ensureSnapshotsForSet(this.currentSet);
2644
- this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
2645
- this.isExploded = false;
2646
- return;
2647
- }
2648
- });
2694
+ });
2695
+ // Ensure snapshots exist for current set members (create for newly added meshes)
2696
+ await this.ensureSnapshotsForSet(this.currentSet);
2697
+ this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
2698
+ this.isExploded = false;
2699
+ return;
2700
+ }
2701
+ else {
2702
+ // no oldSet -> brand new registration
2703
+ this.stateMap.clear();
2704
+ await this.ensureSnapshotsForSet(this.currentSet);
2705
+ this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
2706
+ this.isExploded = false;
2707
+ return;
2708
+ }
2649
2709
  }
2650
2710
  /**
2651
2711
  * ensureSnapshotsForSet: for each mesh in set, ensure stateMap has an entry.
2652
2712
  * If missing, record current matrixWorld as originalMatrixWorld (best-effort).
2653
2713
  */
2654
- ensureSnapshotsForSet(set) {
2655
- return __awaiter(this, void 0, void 0, function* () {
2656
- set.forEach((m) => {
2714
+ async ensureSnapshotsForSet(set) {
2715
+ set.forEach((m) => {
2716
+ try {
2717
+ m.updateMatrixWorld(true);
2718
+ }
2719
+ catch { }
2720
+ if (!this.stateMap.has(m)) {
2657
2721
  try {
2658
- m.updateMatrixWorld(true);
2722
+ this.stateMap.set(m, {
2723
+ originalParent: m.parent || null,
2724
+ originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE__namespace.Matrix4().copy(m.matrix),
2725
+ });
2726
+ // Also store in userData for extra resilience
2727
+ m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
2659
2728
  }
2660
- catch (_a) { }
2661
- if (!this.stateMap.has(m)) {
2662
- try {
2663
- this.stateMap.set(m, {
2664
- originalParent: m.parent || null,
2665
- originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE__namespace.Matrix4().copy(m.matrix),
2666
- });
2667
- // Also store in userData for extra resilience
2668
- m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
2669
- }
2670
- catch (e) {
2671
- this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
2672
- }
2729
+ catch (e) {
2730
+ this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
2673
2731
  }
2674
- });
2732
+ }
2675
2733
  });
2676
2734
  }
2677
2735
  /**
2678
2736
  * explode: compute targets first, compute targetBound using targets + mesh radii,
2679
2737
  * animate camera to that targetBound, then animate meshes to targets.
2680
2738
  */
2681
- explode(opts) {
2682
- return __awaiter(this, void 0, void 0, function* () {
2683
- var _a;
2684
- if (!this.currentSet || this.currentSet.size === 0) {
2685
- this.log('explode: empty currentSet, nothing to do');
2686
- return;
2739
+ async explode(opts) {
2740
+ if (!this.currentSet || this.currentSet.size === 0) {
2741
+ this.log('explode: empty currentSet, nothing to do');
2742
+ return;
2743
+ }
2744
+ const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
2745
+ this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
2746
+ this.cancelAnimations();
2747
+ const meshes = Array.from(this.currentSet);
2748
+ // ensure snapshots exist for any meshes that may have been added after initial registration
2749
+ await this.ensureSnapshotsForSet(this.currentSet);
2750
+ // compute center/radius from current meshes (fallback)
2751
+ const initial = this.computeBoundingSphereForMeshes(meshes);
2752
+ const center = initial.center;
2753
+ const baseRadius = Math.max(1, initial.radius);
2754
+ this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
2755
+ // compute targets (pure calculation)
2756
+ const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
2757
+ this.log(`explode: computed ${targets.length} target positions`);
2758
+ // compute target-based bounding sphere (targets + per-mesh radius)
2759
+ const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
2760
+ this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
2761
+ await this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
2762
+ this.log('explode: camera animation to target bound completed');
2763
+ // apply dim if needed with context id
2764
+ const contextId = dimOthers?.enabled ? this.applyDimToOthers(meshes, dimOthers.opacity ?? 0.25, { debug }) : null;
2765
+ if (contextId)
2766
+ this.log(`explode: applied dim for context ${contextId}`);
2767
+ // capture starts after camera move
2768
+ const starts = meshes.map((m) => {
2769
+ const v = new THREE__namespace.Vector3();
2770
+ try {
2771
+ m.getWorldPosition(v);
2687
2772
  }
2688
- const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
2689
- this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
2690
- this.cancelAnimations();
2691
- const meshes = Array.from(this.currentSet);
2692
- // ensure snapshots exist for any meshes that may have been added after initial registration
2693
- yield this.ensureSnapshotsForSet(this.currentSet);
2694
- // compute center/radius from current meshes (fallback)
2695
- const initial = this.computeBoundingSphereForMeshes(meshes);
2696
- const center = initial.center;
2697
- const baseRadius = Math.max(1, initial.radius);
2698
- this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
2699
- // compute targets (pure calculation)
2700
- const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
2701
- this.log(`explode: computed ${targets.length} target positions`);
2702
- // compute target-based bounding sphere (targets + per-mesh radius)
2703
- const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
2704
- this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
2705
- yield this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
2706
- this.log('explode: camera animation to target bound completed');
2707
- // apply dim if needed with context id
2708
- 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;
2709
- if (contextId)
2710
- this.log(`explode: applied dim for context ${contextId}`);
2711
- // capture starts after camera move
2712
- const starts = meshes.map((m) => {
2713
- const v = new THREE__namespace.Vector3();
2714
- try {
2715
- m.getWorldPosition(v);
2716
- }
2717
- catch (_a) {
2718
- // fallback to originalMatrixWorld if available
2719
- const st = this.stateMap.get(m);
2720
- if (st)
2721
- v.setFromMatrixPosition(st.originalMatrixWorld);
2722
- }
2723
- return v;
2724
- });
2725
- const startTime = performance.now();
2726
- const total = Math.max(1, duration);
2727
- const tick = (now) => {
2728
- const t = Math.min(1, (now - startTime) / total);
2729
- const eased = easeInOutQuad(t);
2730
- for (let i = 0; i < meshes.length; i++) {
2731
- const m = meshes[i];
2732
- const s = starts[i];
2733
- const tar = targets[i];
2734
- const cur = s.clone().lerp(tar, eased);
2735
- if (m.parent) {
2736
- const local = cur.clone();
2737
- m.parent.worldToLocal(local);
2738
- m.position.copy(local);
2739
- }
2740
- else {
2741
- m.position.copy(cur);
2742
- }
2743
- m.updateMatrix();
2744
- }
2745
- if (this.controls && typeof this.controls.update === 'function')
2746
- this.controls.update();
2747
- if (t < 1) {
2748
- this.animId = requestAnimationFrame(tick);
2773
+ catch {
2774
+ // fallback to originalMatrixWorld if available
2775
+ const st = this.stateMap.get(m);
2776
+ if (st)
2777
+ v.setFromMatrixPosition(st.originalMatrixWorld);
2778
+ }
2779
+ return v;
2780
+ });
2781
+ const startTime = performance.now();
2782
+ const total = Math.max(1, duration);
2783
+ const tick = (now) => {
2784
+ const t = Math.min(1, (now - startTime) / total);
2785
+ const eased = easeInOutQuad(t);
2786
+ for (let i = 0; i < meshes.length; i++) {
2787
+ const m = meshes[i];
2788
+ const s = starts[i];
2789
+ const tar = targets[i];
2790
+ const cur = s.clone().lerp(tar, eased);
2791
+ if (m.parent) {
2792
+ const local = cur.clone();
2793
+ m.parent.worldToLocal(local);
2794
+ m.position.copy(local);
2749
2795
  }
2750
2796
  else {
2751
- this.animId = null;
2752
- this.isExploded = true;
2753
- this.log(`explode: completed. contextId=${contextId !== null && contextId !== void 0 ? contextId : 'none'}`);
2797
+ m.position.copy(cur);
2754
2798
  }
2755
- };
2756
- this.animId = requestAnimationFrame(tick);
2757
- return;
2758
- });
2799
+ m.updateMatrix();
2800
+ }
2801
+ if (this.controls && typeof this.controls.update === 'function')
2802
+ this.controls.update();
2803
+ if (t < 1) {
2804
+ this.animId = requestAnimationFrame(tick);
2805
+ }
2806
+ else {
2807
+ this.animId = null;
2808
+ this.isExploded = true;
2809
+ this.log(`explode: completed. contextId=${contextId ?? 'none'}`);
2810
+ }
2811
+ };
2812
+ this.animId = requestAnimationFrame(tick);
2813
+ return;
2759
2814
  }
2760
2815
  /**
2761
2816
  * Restore all exploded meshes to their original transform:
@@ -2778,7 +2833,7 @@ class GroupExploder {
2778
2833
  */
2779
2834
  restoreSet(set, stateMap, duration = 400, opts) {
2780
2835
  if (!set || set.size === 0) {
2781
- if (opts === null || opts === void 0 ? void 0 : opts.debug)
2836
+ if (opts?.debug)
2782
2837
  this.log('restoreSet: empty set, nothing to restore');
2783
2838
  return Promise.resolve();
2784
2839
  }
@@ -2791,12 +2846,12 @@ class GroupExploder {
2791
2846
  try {
2792
2847
  m.updateMatrixWorld(true);
2793
2848
  }
2794
- catch (_a) { }
2849
+ catch { }
2795
2850
  const s = new THREE__namespace.Vector3();
2796
2851
  try {
2797
2852
  m.getWorldPosition(s);
2798
2853
  }
2799
- catch (_b) {
2854
+ catch {
2800
2855
  s.set(0, 0, 0);
2801
2856
  }
2802
2857
  starts.push(s);
@@ -2894,7 +2949,7 @@ class GroupExploder {
2894
2949
  });
2895
2950
  }
2896
2951
  // material dim with context id
2897
- applyDimToOthers(explodingMeshes, opacity = 0.25, opts) {
2952
+ applyDimToOthers(explodingMeshes, opacity = 0.25, _opts) {
2898
2953
  const contextId = `ctx_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
2899
2954
  const explodingSet = new Set(explodingMeshes);
2900
2955
  const touched = new Set();
@@ -2905,11 +2960,10 @@ class GroupExploder {
2905
2960
  if (explodingSet.has(mesh))
2906
2961
  return;
2907
2962
  const applyMat = (mat) => {
2908
- var _a;
2909
2963
  if (!this.materialSnaps.has(mat)) {
2910
2964
  this.materialSnaps.set(mat, {
2911
2965
  transparent: !!mat.transparent,
2912
- opacity: (_a = mat.opacity) !== null && _a !== void 0 ? _a : 1,
2966
+ opacity: mat.opacity ?? 1,
2913
2967
  depthWrite: mat.depthWrite,
2914
2968
  });
2915
2969
  }
@@ -2936,7 +2990,7 @@ class GroupExploder {
2936
2990
  return contextId;
2937
2991
  }
2938
2992
  // clean contexts for meshes (restore materials whose contexts are removed)
2939
- cleanContextsForMeshes(meshes) {
2993
+ cleanContextsForMeshes(_meshes) {
2940
2994
  // conservative strategy: for each context we created, delete it and restore materials accordingly
2941
2995
  for (const [contextId, mats] of Array.from(this.contextMaterials.entries())) {
2942
2996
  mats.forEach((mat) => {
@@ -3050,7 +3104,7 @@ class GroupExploder {
3050
3104
  }
3051
3105
  }
3052
3106
  }
3053
- catch (_a) {
3107
+ catch {
3054
3108
  radius = 0;
3055
3109
  }
3056
3110
  if (!isFinite(radius) || radius < 0 || radius > 1e8)
@@ -3073,10 +3127,9 @@ class GroupExploder {
3073
3127
  }
3074
3128
  // computeTargetsByMode (unchanged logic but pure function)
3075
3129
  computeTargetsByMode(meshes, center, baseRadius, opts) {
3076
- var _a, _b;
3077
3130
  const n = meshes.length;
3078
- const lift = (_a = opts.lift) !== null && _a !== void 0 ? _a : 0.5;
3079
- const mode = (_b = opts.mode) !== null && _b !== void 0 ? _b : 'ring';
3131
+ const lift = opts.lift ?? 0.5;
3132
+ const mode = opts.mode ?? 'ring';
3080
3133
  const targets = [];
3081
3134
  if (mode === 'ring') {
3082
3135
  for (let i = 0; i < n; i++) {
@@ -3118,9 +3171,8 @@ class GroupExploder {
3118
3171
  return targets;
3119
3172
  }
3120
3173
  animateCameraToFit(targetCenter, targetRadius, opts) {
3121
- var _a, _b, _c, _d;
3122
- const duration = (_a = opts === null || opts === void 0 ? void 0 : opts.duration) !== null && _a !== void 0 ? _a : 600;
3123
- const padding = (_b = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _b !== void 0 ? _b : 1.5;
3174
+ const duration = opts?.duration ?? 600;
3175
+ const padding = opts?.padding ?? 1.5;
3124
3176
  if (!(this.camera instanceof THREE__namespace.PerspectiveCamera)) {
3125
3177
  if (this.controls && this.controls.target) {
3126
3178
  // Fallback for non-PerspectiveCamera
@@ -3132,14 +3184,13 @@ class GroupExploder {
3132
3184
  const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
3133
3185
  const startTime = performance.now();
3134
3186
  const tick = (now) => {
3135
- var _a;
3136
3187
  const t = Math.min(1, (now - startTime) / duration);
3137
3188
  const k = easeInOutQuad(t);
3138
3189
  if (this.controls && this.controls.target) {
3139
3190
  this.controls.target.lerpVectors(startTarget, endTarget, k);
3140
3191
  }
3141
3192
  this.camera.position.lerpVectors(startPos, endPos, k);
3142
- if ((_a = this.controls) === null || _a === void 0 ? void 0 : _a.update)
3193
+ if (this.controls?.update)
3143
3194
  this.controls.update();
3144
3195
  if (t < 1) {
3145
3196
  this.cameraAnimId = requestAnimationFrame(tick);
@@ -3162,8 +3213,8 @@ class GroupExploder {
3162
3213
  const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
3163
3214
  const dist = Math.max(distV, distH) * padding;
3164
3215
  const startPos = this.camera.position.clone();
3165
- const startTarget = ((_c = this.controls) === null || _c === void 0 ? void 0 : _c.target) ? this.controls.target.clone() : new THREE__namespace.Vector3(); // assumption
3166
- if (!((_d = this.controls) === null || _d === void 0 ? void 0 : _d.target)) {
3216
+ const startTarget = this.controls?.target ? this.controls.target.clone() : new THREE__namespace.Vector3(); // assumption
3217
+ if (!this.controls?.target) {
3167
3218
  this.camera.getWorldDirection(startTarget);
3168
3219
  startTarget.add(startPos);
3169
3220
  }
@@ -3176,13 +3227,12 @@ class GroupExploder {
3176
3227
  return new Promise((resolve) => {
3177
3228
  const startTime = performance.now();
3178
3229
  const tick = (now) => {
3179
- var _a, _b;
3180
3230
  const t = Math.min(1, (now - startTime) / duration);
3181
3231
  const k = easeInOutQuad(t);
3182
3232
  this.camera.position.lerpVectors(startPos, endPos, k);
3183
3233
  if (this.controls && this.controls.target) {
3184
3234
  this.controls.target.lerpVectors(startTarget, endTarget, k);
3185
- (_b = (_a = this.controls).update) === null || _b === void 0 ? void 0 : _b.call(_a);
3235
+ this.controls.update?.();
3186
3236
  }
3187
3237
  else {
3188
3238
  this.camera.lookAt(endTarget); // simple lookAt if no controls
@@ -3231,183 +3281,134 @@ class GroupExploder {
3231
3281
  * @file autoSetup.ts
3232
3282
  * @description
3233
3283
  * Automatically sets up the camera and basic lighting scene based on the model's bounding box.
3234
- *
3235
- * @best-practice
3236
- * - Call `autoSetupCameraAndLight` after loading a model to get a quick "good looking" scene.
3237
- * - Returns a handle to dispose lights or update intensity later.
3238
3284
  */
3239
3285
  /**
3240
- * Automatically setup camera and basic lighting - Optimized
3241
- *
3242
- * Features:
3243
- * - Adds light intensity adjustment method
3244
- * - Improved error handling
3245
- * - Optimized dispose logic
3246
- *
3247
- * - camera: THREE.PerspectiveCamera (will be moved and pointed at model center)
3248
- * - scene: THREE.Scene (newly created light group will be added to the scene)
3249
- * - model: THREE.Object3D loaded model (arbitrary transform/coordinates)
3250
- * - options: Optional configuration (see AutoSetupOptions)
3251
- *
3252
- * Returns AutoSetupHandle, caller should call handle.dispose() when component unmounts/switches
3286
+ * Fit camera to object bounding box
3253
3287
  */
3254
- function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3255
- var _a, _b, _c, _d, _e, _f, _g;
3256
- // Boundary check
3257
- if (!camera || !scene || !model) {
3258
- throw new Error('autoSetupCameraAndLight: camera, scene, model are required');
3259
- }
3288
+ function fitCameraToObject(camera, object, padding = 1.2, elevation = 0.2) {
3289
+ const box = new THREE__namespace.Box3().setFromObject(object);
3290
+ if (!isFinite(box.min.x))
3291
+ return { center: new THREE__namespace.Vector3(), radius: 0 };
3292
+ const sphere = new THREE__namespace.Sphere();
3293
+ box.getBoundingSphere(sphere);
3294
+ const center = sphere.center.clone();
3295
+ const radius = Math.max(0.001, sphere.radius);
3296
+ const fov = (camera.fov * Math.PI) / 180;
3297
+ const halfFov = fov / 2;
3298
+ const sinHalfFov = Math.max(Math.sin(halfFov), 0.001);
3299
+ const distance = (radius * padding) / sinHalfFov;
3300
+ const dir = new THREE__namespace.Vector3(0, Math.sin(elevation), Math.cos(elevation)).normalize();
3301
+ const desiredPos = center.clone().add(dir.multiplyScalar(distance));
3302
+ camera.position.copy(desiredPos);
3303
+ camera.lookAt(center);
3304
+ camera.near = Math.max(0.001, radius / 1000);
3305
+ camera.far = Math.max(1000, radius * 50);
3306
+ camera.updateProjectionMatrix();
3307
+ return { center, radius };
3308
+ }
3309
+ /**
3310
+ * Setup default lighting for a model
3311
+ */
3312
+ function setupDefaultLights(scene, model, options = {}) {
3313
+ const box = new THREE__namespace.Box3().setFromObject(model);
3314
+ const sphere = new THREE__namespace.Sphere();
3315
+ box.getBoundingSphere(sphere);
3316
+ const center = sphere.center.clone();
3317
+ const radius = Math.max(0.001, sphere.radius);
3260
3318
  const opts = {
3261
- padding: (_a = options.padding) !== null && _a !== void 0 ? _a : 1.2,
3262
- elevation: (_b = options.elevation) !== null && _b !== void 0 ? _b : 0.2,
3263
- enableShadows: (_c = options.enableShadows) !== null && _c !== void 0 ? _c : false,
3264
- shadowMapSize: (_d = options.shadowMapSize) !== null && _d !== void 0 ? _d : 1024,
3265
- directionalCount: (_e = options.directionalCount) !== null && _e !== void 0 ? _e : 4,
3266
- setMeshShadowProps: (_f = options.setMeshShadowProps) !== null && _f !== void 0 ? _f : true,
3267
- renderer: (_g = options.renderer) !== null && _g !== void 0 ? _g : null,
3319
+ padding: options.padding ?? 1.2,
3320
+ elevation: options.elevation ?? 0.2,
3321
+ enableShadows: options.enableShadows ?? false,
3322
+ shadowMapSize: options.shadowMapSize ?? 1024,
3323
+ directionalCount: options.directionalCount ?? 4,
3324
+ setMeshShadowProps: options.setMeshShadowProps ?? true,
3325
+ renderer: options.renderer ?? null,
3268
3326
  };
3269
- try {
3270
- // --- 1) Calculate bounding data
3271
- const box = new THREE__namespace.Box3().setFromObject(model);
3272
- // Check bounding box validity
3273
- if (!isFinite(box.min.x)) {
3274
- throw new Error('autoSetupCameraAndLight: Invalid bounding box');
3327
+ if (opts.renderer && opts.enableShadows) {
3328
+ opts.renderer.shadowMap.enabled = true;
3329
+ opts.renderer.shadowMap.type = THREE__namespace.PCFSoftShadowMap;
3330
+ }
3331
+ const lightsGroup = new THREE__namespace.Group();
3332
+ lightsGroup.name = 'autoSetupLightsGroup';
3333
+ lightsGroup.position.copy(center);
3334
+ scene.add(lightsGroup);
3335
+ const hemi = new THREE__namespace.HemisphereLight(0xffffff, 0x444444, 0.6);
3336
+ hemi.position.set(0, radius * 2.0, 0);
3337
+ lightsGroup.add(hemi);
3338
+ const ambient = new THREE__namespace.AmbientLight(0xffffff, 0.25);
3339
+ lightsGroup.add(ambient);
3340
+ const dirCount = Math.max(1, Math.floor(opts.directionalCount));
3341
+ const dirs = [new THREE__namespace.Vector3(0, 1, 0)];
3342
+ for (let i = 0; i < dirCount; i++) {
3343
+ const angle = (i / dirCount) * Math.PI * 2;
3344
+ const v = new THREE__namespace.Vector3(Math.cos(angle), 0.3, Math.sin(angle)).normalize();
3345
+ dirs.push(v);
3346
+ }
3347
+ const shadowCamSize = Math.max(1, radius * 1.5);
3348
+ dirs.forEach((d, i) => {
3349
+ const light = new THREE__namespace.DirectionalLight(0xffffff, i === 0 ? 1.5 : 1.2);
3350
+ light.position.copy(d.clone().multiplyScalar(radius * 2.5));
3351
+ light.target.position.copy(center);
3352
+ light.name = `auto_dir_${i}`;
3353
+ lightsGroup.add(light);
3354
+ lightsGroup.add(light.target);
3355
+ if (opts.enableShadows) {
3356
+ light.castShadow = true;
3357
+ light.shadow.mapSize.width = opts.shadowMapSize;
3358
+ light.shadow.mapSize.height = opts.shadowMapSize;
3359
+ const cam = light.shadow.camera;
3360
+ const s = shadowCamSize;
3361
+ cam.left = -s;
3362
+ cam.right = s;
3363
+ cam.top = s;
3364
+ cam.bottom = -s;
3365
+ cam.near = 0.1;
3366
+ cam.far = radius * 10 + 50;
3367
+ light.shadow.bias = -5e-4;
3275
3368
  }
3276
- const sphere = new THREE__namespace.Sphere();
3277
- box.getBoundingSphere(sphere);
3278
- const center = sphere.center.clone();
3279
- const radius = Math.max(0.001, sphere.radius);
3280
- // --- 2) Calculate camera position
3281
- const fov = (camera.fov * Math.PI) / 180;
3282
- const halfFov = fov / 2;
3283
- const sinHalfFov = Math.max(Math.sin(halfFov), 0.001);
3284
- const distance = (radius * opts.padding) / sinHalfFov;
3285
- const dir = new THREE__namespace.Vector3(0, Math.sin(opts.elevation), Math.cos(opts.elevation)).normalize();
3286
- const desiredPos = center.clone().add(dir.multiplyScalar(distance));
3287
- camera.position.copy(desiredPos);
3288
- camera.lookAt(center);
3289
- camera.near = Math.max(0.001, radius / 1000);
3290
- camera.far = Math.max(1000, radius * 50);
3291
- camera.updateProjectionMatrix();
3292
- // --- 3) Enable Shadows
3293
- if (opts.renderer && opts.enableShadows) {
3294
- opts.renderer.shadowMap.enabled = true;
3295
- opts.renderer.shadowMap.type = THREE__namespace.PCFSoftShadowMap;
3296
- }
3297
- // --- 4) Create Lights Group
3298
- const lightsGroup = new THREE__namespace.Group();
3299
- lightsGroup.name = 'autoSetupLightsGroup';
3300
- lightsGroup.position.copy(center);
3301
- scene.add(lightsGroup);
3302
- // 4.1 Basic Light
3303
- const hemi = new THREE__namespace.HemisphereLight(0xffffff, 0x444444, 0.6);
3304
- hemi.name = 'auto_hemi';
3305
- hemi.position.set(0, radius * 2.0, 0);
3306
- lightsGroup.add(hemi);
3307
- const ambient = new THREE__namespace.AmbientLight(0xffffff, 0.25);
3308
- ambient.name = 'auto_ambient';
3309
- lightsGroup.add(ambient);
3310
- // 4.2 Directional Lights
3311
- const dirCount = Math.max(1, Math.floor(opts.directionalCount));
3312
- const directionalLights = [];
3313
- const dirs = [];
3314
- dirs.push(new THREE__namespace.Vector3(0, 1, 0));
3315
- for (let i = 0; i < Math.max(1, dirCount); i++) {
3316
- const angle = (i / Math.max(1, dirCount)) * Math.PI * 2;
3317
- const v = new THREE__namespace.Vector3(Math.cos(angle), 0.3, Math.sin(angle)).normalize();
3318
- dirs.push(v);
3319
- }
3320
- const shadowCamSize = Math.max(1, radius * 1.5);
3321
- for (let i = 0; i < dirs.length; i++) {
3322
- const d = dirs[i];
3323
- const light = new THREE__namespace.DirectionalLight(0xffffff, i === 0 ? 1.5 : 1.2);
3324
- light.position.copy(d.clone().multiplyScalar(radius * 2.5));
3325
- light.target.position.copy(center);
3326
- light.name = `auto_dir_${i}`;
3327
- lightsGroup.add(light);
3328
- lightsGroup.add(light.target);
3329
- if (opts.enableShadows) {
3330
- light.castShadow = true;
3331
- light.shadow.mapSize.width = opts.shadowMapSize;
3332
- light.shadow.mapSize.height = opts.shadowMapSize;
3333
- const cam = light.shadow.camera;
3334
- const s = shadowCamSize;
3335
- cam.left = -s;
3336
- cam.right = s;
3337
- cam.top = s;
3338
- cam.bottom = -s;
3339
- cam.near = 0.1;
3340
- cam.far = radius * 10 + 50;
3341
- light.shadow.bias = -0.0005;
3342
- }
3343
- directionalLights.push(light);
3344
- }
3345
- // 4.3 Point Light Fill
3346
- const fill1 = new THREE__namespace.PointLight(0xffffff, 0.5, radius * 4);
3347
- fill1.position.copy(center).add(new THREE__namespace.Vector3(radius * 0.5, 0.2 * radius, 0));
3348
- fill1.name = 'auto_fill1';
3349
- lightsGroup.add(fill1);
3350
- const fill2 = new THREE__namespace.PointLight(0xffffff, 0.3, radius * 3);
3351
- fill2.position.copy(center).add(new THREE__namespace.Vector3(-radius * 0.5, -0.2 * radius, 0));
3352
- fill2.name = 'auto_fill2';
3353
- lightsGroup.add(fill2);
3354
- // --- 5) Set Mesh Shadow Props
3355
- if (opts.setMeshShadowProps) {
3356
- model.traverse((ch) => {
3357
- if (ch.isMesh) {
3358
- const mesh = ch;
3359
- const isSkinned = mesh.isSkinnedMesh;
3360
- mesh.castShadow = opts.enableShadows && !isSkinned ? true : mesh.castShadow;
3361
- mesh.receiveShadow = opts.enableShadows ? true : mesh.receiveShadow;
3369
+ });
3370
+ if (opts.setMeshShadowProps) {
3371
+ model.traverse((ch) => {
3372
+ if (ch.isMesh) {
3373
+ const mesh = ch;
3374
+ const isSkinned = mesh.isSkinnedMesh;
3375
+ mesh.castShadow = opts.enableShadows && !isSkinned ? true : mesh.castShadow;
3376
+ mesh.receiveShadow = opts.enableShadows ? true : mesh.receiveShadow;
3377
+ }
3378
+ });
3379
+ }
3380
+ const handle = {
3381
+ lightsGroup,
3382
+ center,
3383
+ radius,
3384
+ updateLightIntensity(factor) {
3385
+ lightsGroup.traverse((node) => {
3386
+ if (node.isLight) {
3387
+ const light = node;
3388
+ light.intensity *= factor; // Simple implementation
3362
3389
  }
3363
3390
  });
3364
- }
3365
- // --- 6) Return handle ---
3366
- const handle = {
3367
- lightsGroup,
3368
- center,
3369
- radius,
3370
- // Update light intensity
3371
- updateLightIntensity(factor) {
3372
- lightsGroup.traverse((node) => {
3373
- if (node.isLight) {
3374
- const light = node;
3375
- const originalIntensity = parseFloat(light.name.split('_').pop() || '1');
3376
- light.intensity = originalIntensity * Math.max(0, factor);
3377
- }
3378
- });
3379
- },
3380
- dispose: () => {
3381
- try {
3382
- // Remove lights group
3383
- if (lightsGroup.parent)
3384
- lightsGroup.parent.remove(lightsGroup);
3385
- // Dispose shadow resources
3386
- lightsGroup.traverse((node) => {
3387
- if (node.isLight) {
3388
- const l = node;
3389
- if (l.shadow && l.shadow.map) {
3390
- try {
3391
- l.shadow.map.dispose();
3392
- }
3393
- catch (err) {
3394
- console.warn('Failed to dispose shadow map:', err);
3395
- }
3396
- }
3397
- }
3398
- });
3399
- }
3400
- catch (error) {
3401
- console.error('autoSetupCameraAndLight: dispose failed', error);
3391
+ },
3392
+ dispose: () => {
3393
+ if (lightsGroup.parent)
3394
+ lightsGroup.parent.remove(lightsGroup);
3395
+ lightsGroup.traverse((node) => {
3396
+ if (node.isLight) {
3397
+ const l = node;
3398
+ if (l.shadow && l.shadow.map)
3399
+ l.shadow.map.dispose();
3402
3400
  }
3403
- }
3404
- };
3405
- return handle;
3406
- }
3407
- catch (error) {
3408
- console.error('autoSetupCameraAndLight: setup failed', error);
3409
- throw error;
3410
- }
3401
+ });
3402
+ }
3403
+ };
3404
+ return handle;
3405
+ }
3406
+ /**
3407
+ * Automatically setup camera and basic lighting (Combine fitCameraToObject and setupDefaultLights)
3408
+ */
3409
+ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3410
+ fitCameraToObject(camera, model, options.padding, options.elevation);
3411
+ return setupDefaultLights(scene, model, options);
3411
3412
  }
3412
3413
 
3413
3414
  /**
@@ -3417,14 +3418,15 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3417
3418
  * @packageDocumentation
3418
3419
  */
3419
3420
  // Core utilities
3420
- // Version
3421
- const VERSION = '1.0.0';
3421
+ // Version (keep in sync with package.json)
3422
+ const VERSION = '1.0.4';
3422
3423
 
3423
3424
  exports.ArrowGuide = ArrowGuide;
3424
3425
  exports.BlueSky = BlueSky;
3425
3426
  exports.FOLLOW_ANGLES = FOLLOW_ANGLES;
3426
3427
  exports.GroupExploder = GroupExploder;
3427
3428
  exports.LiquidFillerGroup = LiquidFillerGroup;
3429
+ exports.ResourceManager = ResourceManager;
3428
3430
  exports.VERSION = VERSION;
3429
3431
  exports.ViewPresets = ViewPresets;
3430
3432
  exports.addChildModelLabels = addChildModelLabels;
@@ -3436,12 +3438,16 @@ exports.createModelsLabel = createModelsLabel;
3436
3438
  exports.disposeMaterial = disposeMaterial;
3437
3439
  exports.disposeObject = disposeObject;
3438
3440
  exports.enableHoverBreath = enableHoverBreath;
3441
+ exports.fitCameraToObject = fitCameraToObject;
3439
3442
  exports.followModels = followModels;
3443
+ exports.getLoaderConfig = getLoaderConfig;
3440
3444
  exports.initPostProcessing = initPostProcessing;
3441
3445
  exports.loadCubeSkybox = loadCubeSkybox;
3442
3446
  exports.loadEquirectSkybox = loadEquirectSkybox;
3443
3447
  exports.loadModelByUrl = loadModelByUrl;
3444
3448
  exports.loadSkybox = loadSkybox;
3445
3449
  exports.releaseSkybox = releaseSkybox;
3450
+ exports.setLoaderConfig = setLoaderConfig;
3446
3451
  exports.setView = setView;
3452
+ exports.setupDefaultLights = setupDefaultLights;
3447
3453
  //# sourceMappingURL=index.js.map