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