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