@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.
- package/CHANGELOG.md +39 -0
- package/README.md +46 -6
- package/dist/camera/index.js +6 -10
- package/dist/camera/index.js.map +1 -1
- package/dist/camera/index.mjs +6 -10
- package/dist/camera/index.mjs.map +1 -1
- package/dist/core/index.d.ts +21 -1
- package/dist/core/index.js +70 -9
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +70 -10
- package/dist/core/index.mjs.map +1 -1
- package/dist/effect/index.js +185 -230
- package/dist/effect/index.js.map +1 -1
- package/dist/effect/index.mjs +185 -230
- package/dist/effect/index.mjs.map +1 -1
- package/dist/index.d.ts +61 -28
- package/dist/index.js +812 -806
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +808 -807
- package/dist/index.mjs.map +1 -1
- package/dist/interaction/index.d.ts +12 -6
- package/dist/interaction/index.js +26 -14
- package/dist/interaction/index.js.map +1 -1
- package/dist/interaction/index.mjs +26 -14
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/loader/index.d.ts +17 -2
- package/dist/loader/index.js +385 -386
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/index.mjs +384 -387
- package/dist/loader/index.mjs.map +1 -1
- package/dist/setup/index.d.ts +13 -21
- package/dist/setup/index.js +120 -167
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/index.mjs +119 -168
- package/dist/setup/index.mjs.map +1 -1
- package/dist/ui/index.js +15 -17
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +15 -17
- package/dist/ui/index.mjs.map +1 -1
- 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 =
|
|
71
|
-
const updateInterval =
|
|
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 =
|
|
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 =
|
|
100
|
-
el.style.background =
|
|
101
|
-
el.style.padding =
|
|
102
|
-
el.style.borderRadius =
|
|
103
|
-
el.style.fontSize =
|
|
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 =
|
|
655
|
-
this.ignoreRaycastNames = new Set(
|
|
656
|
-
this.fadeOpacity =
|
|
657
|
-
this.fadeBrightness =
|
|
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
|
-
*
|
|
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 =
|
|
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:
|
|
951
|
-
opacity:
|
|
952
|
-
speed:
|
|
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 =
|
|
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 =
|
|
1227
|
-
const padding =
|
|
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 =
|
|
1231
|
-
const azimuth =
|
|
1232
|
-
const elevation =
|
|
1233
|
-
const easing =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
1584
|
-
|
|
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
|
|
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
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
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
|
|
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
|
|
1825
|
+
catch { }
|
|
1752
1826
|
}
|
|
1753
1827
|
});
|
|
1754
1828
|
try {
|
|
1755
1829
|
if (typeof mat.dispose === 'function')
|
|
1756
1830
|
mat.dispose();
|
|
1757
1831
|
}
|
|
1758
|
-
catch
|
|
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(
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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 =
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
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
|
-
//
|
|
1841
|
-
|
|
1842
|
-
envRenderTarget = null;
|
|
1921
|
+
// fallback: use cube texture directly (works but not prefiltered)
|
|
1922
|
+
scene.environment = texture;
|
|
1843
1923
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
1849
|
-
|
|
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
|
-
|
|
1946
|
+
envRenderTarget.dispose();
|
|
1880
1947
|
}
|
|
1881
|
-
catch
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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(
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
//
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
if (options.cache)
|
|
2051
|
+
equirectCache.set(key, { handle, refCount: 1 });
|
|
2052
|
+
return handle;
|
|
1985
2053
|
}
|
|
1986
|
-
function loadSkybox(
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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
|
-
|
|
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:
|
|
2241
|
-
color:
|
|
2242
|
-
background:
|
|
2243
|
-
padding:
|
|
2244
|
-
borderRadius:
|
|
2245
|
-
lift:
|
|
2246
|
-
dotSize:
|
|
2247
|
-
dotSpacing:
|
|
2248
|
-
lineColor:
|
|
2249
|
-
lineWidth:
|
|
2250
|
-
updateInterval:
|
|
2251
|
-
fadeInDuration:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
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
|
-
|
|
2656
|
-
|
|
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
|
-
|
|
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 (
|
|
2661
|
-
|
|
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
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2757
|
-
|
|
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
|
|
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
|
|
2849
|
+
catch { }
|
|
2795
2850
|
const s = new THREE__namespace.Vector3();
|
|
2796
2851
|
try {
|
|
2797
2852
|
m.getWorldPosition(s);
|
|
2798
2853
|
}
|
|
2799
|
-
catch
|
|
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,
|
|
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:
|
|
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(
|
|
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
|
|
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 =
|
|
3079
|
-
const mode =
|
|
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
|
-
|
|
3122
|
-
const
|
|
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 (
|
|
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 =
|
|
3166
|
-
if (!
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
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:
|
|
3262
|
-
elevation:
|
|
3263
|
-
enableShadows:
|
|
3264
|
-
shadowMapSize:
|
|
3265
|
-
directionalCount:
|
|
3266
|
-
setMeshShadowProps:
|
|
3267
|
-
renderer:
|
|
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
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
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
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
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
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
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.
|
|
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
|