@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.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
|
}
|
|
@@ -944,12 +1003,11 @@ class LiquidFillerGroup {
|
|
|
944
1003
|
this.abortController = new AbortController();
|
|
945
1004
|
const modelArray = Array.isArray(models) ? models : [models];
|
|
946
1005
|
modelArray.forEach(model => {
|
|
947
|
-
var _a, _b, _c;
|
|
948
1006
|
try {
|
|
949
1007
|
const options = {
|
|
950
|
-
color:
|
|
951
|
-
opacity:
|
|
952
|
-
speed:
|
|
1008
|
+
color: defaultOptions?.color ?? 0x00ff00,
|
|
1009
|
+
opacity: defaultOptions?.opacity ?? 0.6,
|
|
1010
|
+
speed: defaultOptions?.speed ?? 0.05,
|
|
953
1011
|
};
|
|
954
1012
|
// Save original materials
|
|
955
1013
|
const originalMaterials = new Map();
|
|
@@ -1196,7 +1254,6 @@ const EASING_FUNCTIONS = {
|
|
|
1196
1254
|
* - Robust error handling
|
|
1197
1255
|
*/
|
|
1198
1256
|
function followModels(camera, targets, options = {}) {
|
|
1199
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1200
1257
|
// Cancel previous animation
|
|
1201
1258
|
cancelFollow(camera);
|
|
1202
1259
|
// Boundary check
|
|
@@ -1223,14 +1280,14 @@ function followModels(camera, targets, options = {}) {
|
|
|
1223
1280
|
box.getBoundingSphere(sphere);
|
|
1224
1281
|
const center = sphere.center.clone();
|
|
1225
1282
|
const radiusBase = Math.max(0.001, sphere.radius);
|
|
1226
|
-
const duration =
|
|
1227
|
-
const padding =
|
|
1283
|
+
const duration = options.duration ?? 700;
|
|
1284
|
+
const padding = options.padding ?? 1.0;
|
|
1228
1285
|
const minDistance = options.minDistance;
|
|
1229
1286
|
const maxDistance = options.maxDistance;
|
|
1230
|
-
const controls =
|
|
1231
|
-
const azimuth =
|
|
1232
|
-
const elevation =
|
|
1233
|
-
const easing =
|
|
1287
|
+
const controls = options.controls ?? null;
|
|
1288
|
+
const azimuth = options.azimuth ?? Math.PI / 4;
|
|
1289
|
+
const elevation = options.elevation ?? Math.PI / 4;
|
|
1290
|
+
const easing = options.easing ?? 'easeOut';
|
|
1234
1291
|
const onProgress = options.onProgress;
|
|
1235
1292
|
// Get easing function
|
|
1236
1293
|
const easingFn = EASING_FUNCTIONS[easing] || EASING_FUNCTIONS.easeOut;
|
|
@@ -1368,9 +1425,6 @@ function setView(camera, controls, targetObj, position = 'front', options = {})
|
|
|
1368
1425
|
console.warn('setView: Failed to calculate bounding box');
|
|
1369
1426
|
return Promise.reject(new Error('Invalid bounding box'));
|
|
1370
1427
|
}
|
|
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
1428
|
// Use mapping table for creating view angles
|
|
1375
1429
|
const viewAngles = {
|
|
1376
1430
|
'front': { azimuth: 0, elevation: 0 },
|
|
@@ -1422,37 +1476,22 @@ const ViewPresets = {
|
|
|
1422
1476
|
top: (camera, controls, target, options) => setView(camera, controls, target, 'top', options)
|
|
1423
1477
|
};
|
|
1424
1478
|
|
|
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;
|
|
1479
|
+
let globalConfig = {
|
|
1480
|
+
dracoDecoderPath: '/draco/',
|
|
1481
|
+
ktx2TranscoderPath: '/basis/',
|
|
1455
1482
|
};
|
|
1483
|
+
/**
|
|
1484
|
+
* Update global loader configuration (e.g., set path to CDN)
|
|
1485
|
+
*/
|
|
1486
|
+
function setLoaderConfig(config) {
|
|
1487
|
+
globalConfig = { ...globalConfig, ...config };
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Get current global loader configuration
|
|
1491
|
+
*/
|
|
1492
|
+
function getLoaderConfig() {
|
|
1493
|
+
return globalConfig;
|
|
1494
|
+
}
|
|
1456
1495
|
|
|
1457
1496
|
/**
|
|
1458
1497
|
* @file modelLoader.ts
|
|
@@ -1470,19 +1509,22 @@ const DEFAULT_OPTIONS$1 = {
|
|
|
1470
1509
|
maxTextureSize: null,
|
|
1471
1510
|
useSimpleMaterials: false,
|
|
1472
1511
|
skipSkinned: true,
|
|
1512
|
+
useCache: true,
|
|
1473
1513
|
};
|
|
1514
|
+
const modelCache = new Map();
|
|
1474
1515
|
/** Automatically determine which options to enable based on extension (smart judgment) */
|
|
1475
1516
|
function normalizeOptions(url, opts) {
|
|
1476
1517
|
const ext = (url.split('.').pop() || '').toLowerCase();
|
|
1477
|
-
const merged =
|
|
1518
|
+
const merged = { ...DEFAULT_OPTIONS$1, ...opts };
|
|
1478
1519
|
if (ext === 'gltf' || ext === 'glb') {
|
|
1520
|
+
const globalConfig = getLoaderConfig();
|
|
1479
1521
|
// gltf/glb defaults to trying draco/ktx2 if user didn't specify
|
|
1480
1522
|
if (merged.dracoDecoderPath === undefined)
|
|
1481
|
-
merged.dracoDecoderPath =
|
|
1523
|
+
merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
|
|
1482
1524
|
if (merged.useKTX2 === undefined)
|
|
1483
1525
|
merged.useKTX2 = true;
|
|
1484
1526
|
if (merged.ktx2TranscoderPath === undefined)
|
|
1485
|
-
merged.ktx2TranscoderPath =
|
|
1527
|
+
merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
|
|
1486
1528
|
}
|
|
1487
1529
|
else {
|
|
1488
1530
|
// fbx/obj/ply/stl etc. do not need draco/ktx2
|
|
@@ -1492,103 +1534,108 @@ function normalizeOptions(url, opts) {
|
|
|
1492
1534
|
}
|
|
1493
1535
|
return merged;
|
|
1494
1536
|
}
|
|
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);
|
|
1537
|
+
async function loadModelByUrl(url, options = {}) {
|
|
1538
|
+
if (!url)
|
|
1539
|
+
throw new Error('url required');
|
|
1540
|
+
const ext = (url.split('.').pop() || '').toLowerCase();
|
|
1541
|
+
const opts = normalizeOptions(url, options);
|
|
1542
|
+
const manager = opts.manager ?? new THREE__namespace.LoadingManager();
|
|
1543
|
+
// Cache key includes URL and relevant optimization options
|
|
1544
|
+
const cacheKey = `${url}_${opts.mergeGeometries}_${opts.maxTextureSize}_${opts.useSimpleMaterials}`;
|
|
1545
|
+
if (opts.useCache && modelCache.has(cacheKey)) {
|
|
1546
|
+
return modelCache.get(cacheKey).clone();
|
|
1547
|
+
}
|
|
1548
|
+
let loader;
|
|
1549
|
+
if (ext === 'gltf' || ext === 'glb') {
|
|
1550
|
+
const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
|
|
1551
|
+
const gltfLoader = new GLTFLoader(manager);
|
|
1552
|
+
if (opts.dracoDecoderPath) {
|
|
1553
|
+
const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
|
|
1554
|
+
const draco = new DRACOLoader();
|
|
1555
|
+
draco.setDecoderPath(opts.dracoDecoderPath);
|
|
1556
|
+
gltfLoader.setDRACOLoader(draco);
|
|
1535
1557
|
}
|
|
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
|
-
});
|
|
1558
|
+
if (opts.useKTX2 && opts.ktx2TranscoderPath) {
|
|
1559
|
+
const { KTX2Loader } = await import('three/examples/jsm/loaders/KTX2Loader.js');
|
|
1560
|
+
const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
|
|
1561
|
+
gltfLoader.__ktx2Loader = ktx2Loader;
|
|
1578
1562
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1563
|
+
loader = gltfLoader;
|
|
1564
|
+
}
|
|
1565
|
+
else if (ext === 'fbx') {
|
|
1566
|
+
const { FBXLoader } = await import('three/examples/jsm/loaders/FBXLoader.js');
|
|
1567
|
+
loader = new FBXLoader(manager);
|
|
1568
|
+
}
|
|
1569
|
+
else if (ext === 'obj') {
|
|
1570
|
+
const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
|
|
1571
|
+
loader = new OBJLoader(manager);
|
|
1572
|
+
}
|
|
1573
|
+
else if (ext === 'ply') {
|
|
1574
|
+
const { PLYLoader } = await import('three/examples/jsm/loaders/PLYLoader.js');
|
|
1575
|
+
loader = new PLYLoader(manager);
|
|
1576
|
+
}
|
|
1577
|
+
else if (ext === 'stl') {
|
|
1578
|
+
const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
|
|
1579
|
+
loader = new STLLoader(manager);
|
|
1580
|
+
}
|
|
1581
|
+
else {
|
|
1582
|
+
throw new Error(`Unsupported model extension: .${ext}`);
|
|
1583
|
+
}
|
|
1584
|
+
const object = await new Promise((resolve, reject) => {
|
|
1585
|
+
loader.load(url, (res) => {
|
|
1586
|
+
if (ext === 'gltf' || ext === 'glb') {
|
|
1587
|
+
const sceneObj = res.scene || res;
|
|
1588
|
+
// --- Critical: Expose animations to scene.userData (or scene.animations) ---
|
|
1589
|
+
// So the caller can access clips simply by getting sceneObj.userData.animations
|
|
1590
|
+
sceneObj.userData = sceneObj?.userData || {};
|
|
1591
|
+
sceneObj.userData.animations = res.animations ?? [];
|
|
1592
|
+
resolve(sceneObj);
|
|
1582
1593
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1594
|
+
else {
|
|
1595
|
+
resolve(res);
|
|
1596
|
+
}
|
|
1597
|
+
}, undefined, (err) => reject(err));
|
|
1598
|
+
});
|
|
1599
|
+
// Optimize
|
|
1600
|
+
object.traverse((child) => {
|
|
1601
|
+
const mesh = child;
|
|
1602
|
+
if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
|
|
1603
|
+
try {
|
|
1604
|
+
mesh.geometry = new THREE__namespace.BufferGeometry().fromGeometry?.(mesh.geometry) ?? mesh.geometry;
|
|
1585
1605
|
}
|
|
1606
|
+
catch { }
|
|
1586
1607
|
}
|
|
1587
|
-
return object;
|
|
1588
1608
|
});
|
|
1609
|
+
if (opts.maxTextureSize && opts.maxTextureSize > 0)
|
|
1610
|
+
await downscaleTexturesInObject(object, opts.maxTextureSize);
|
|
1611
|
+
if (opts.useSimpleMaterials) {
|
|
1612
|
+
object.traverse((child) => {
|
|
1613
|
+
const m = child.material;
|
|
1614
|
+
if (!m)
|
|
1615
|
+
return;
|
|
1616
|
+
if (Array.isArray(m))
|
|
1617
|
+
child.material = m.map((mat) => toSimpleMaterial(mat));
|
|
1618
|
+
else
|
|
1619
|
+
child.material = toSimpleMaterial(m);
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
if (opts.mergeGeometries) {
|
|
1623
|
+
try {
|
|
1624
|
+
await tryMergeGeometries(object, { skipSkinned: opts.skipSkinned ?? true });
|
|
1625
|
+
}
|
|
1626
|
+
catch (e) {
|
|
1627
|
+
console.warn('mergeGeometries failed', e);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (opts.useCache) {
|
|
1631
|
+
modelCache.set(cacheKey, object);
|
|
1632
|
+
return object.clone();
|
|
1633
|
+
}
|
|
1634
|
+
return object;
|
|
1589
1635
|
}
|
|
1590
|
-
/** Runtime downscale textures in mesh to maxSize (canvas
|
|
1591
|
-
function downscaleTexturesInObject(obj, maxSize) {
|
|
1636
|
+
/** Runtime downscale textures in mesh to maxSize (createImageBitmap or canvas) to save GPU memory */
|
|
1637
|
+
async function downscaleTexturesInObject(obj, maxSize) {
|
|
1638
|
+
const tasks = [];
|
|
1592
1639
|
obj.traverse((ch) => {
|
|
1593
1640
|
if (!ch.isMesh)
|
|
1594
1641
|
return;
|
|
@@ -1607,27 +1654,44 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
1607
1654
|
const max = maxSize;
|
|
1608
1655
|
if (image.width <= max && image.height <= max)
|
|
1609
1656
|
return;
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1657
|
+
tasks.push((async () => {
|
|
1658
|
+
try {
|
|
1659
|
+
const scale = Math.min(max / image.width, max / image.height);
|
|
1660
|
+
const newWidth = Math.floor(image.width * scale);
|
|
1661
|
+
const newHeight = Math.floor(image.height * scale);
|
|
1662
|
+
let newSource;
|
|
1663
|
+
if (typeof createImageBitmap !== 'undefined') {
|
|
1664
|
+
newSource = await createImageBitmap(image, {
|
|
1665
|
+
resizeWidth: newWidth,
|
|
1666
|
+
resizeHeight: newHeight,
|
|
1667
|
+
resizeQuality: 'high'
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
// Fallback for environments without createImageBitmap
|
|
1672
|
+
const canvas = document.createElement('canvas');
|
|
1673
|
+
canvas.width = newWidth;
|
|
1674
|
+
canvas.height = newHeight;
|
|
1675
|
+
const ctx = canvas.getContext('2d');
|
|
1676
|
+
if (ctx) {
|
|
1677
|
+
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
|
1678
|
+
newSource = canvas;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (newSource) {
|
|
1682
|
+
const newTex = new THREE__namespace.Texture(newSource);
|
|
1683
|
+
newTex.needsUpdate = true;
|
|
1684
|
+
newTex.encoding = tex.encoding;
|
|
1685
|
+
mat[p] = newTex;
|
|
1686
|
+
}
|
|
1624
1687
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}
|
|
1688
|
+
catch (e) {
|
|
1689
|
+
console.warn('downscale texture failed', e);
|
|
1690
|
+
}
|
|
1691
|
+
})());
|
|
1629
1692
|
});
|
|
1630
1693
|
});
|
|
1694
|
+
await Promise.all(tasks);
|
|
1631
1695
|
}
|
|
1632
1696
|
/**
|
|
1633
1697
|
* Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
|
|
@@ -1635,81 +1699,77 @@ function downscaleTexturesInObject(obj, maxSize) {
|
|
|
1635
1699
|
* - Merging will group by material UUID (different materials cannot be merged)
|
|
1636
1700
|
* - Merge function is compatible with common export names of BufferGeometryUtils
|
|
1637
1701
|
*/
|
|
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)
|
|
1702
|
+
async function tryMergeGeometries(root, opts) {
|
|
1703
|
+
// collect meshes by material uuid
|
|
1704
|
+
const groups = new Map();
|
|
1705
|
+
root.traverse((ch) => {
|
|
1706
|
+
if (!ch.isMesh)
|
|
1667
1707
|
return;
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1708
|
+
const mesh = ch;
|
|
1709
|
+
if (opts.skipSkinned && mesh.isSkinnedMesh)
|
|
1710
|
+
return;
|
|
1711
|
+
const mat = mesh.material;
|
|
1712
|
+
// don't merge transparent or morph-enabled or skinned meshes
|
|
1713
|
+
if (!mesh.geometry || mesh.visible === false)
|
|
1714
|
+
return;
|
|
1715
|
+
if (mat && mat.transparent)
|
|
1716
|
+
return;
|
|
1717
|
+
const geom = mesh.geometry.clone();
|
|
1718
|
+
mesh.updateWorldMatrix(true, false);
|
|
1719
|
+
geom.applyMatrix4(mesh.matrixWorld);
|
|
1720
|
+
// ensure attributes compatible? we'll rely on merge function to return null if incompatible
|
|
1721
|
+
const key = (mat && mat.uuid) || 'default';
|
|
1722
|
+
const bucket = groups.get(key) ?? { material: mat ?? new THREE__namespace.MeshStandardMaterial(), geoms: [] };
|
|
1723
|
+
bucket.geoms.push(geom);
|
|
1724
|
+
groups.set(key, bucket);
|
|
1725
|
+
// mark for removal (we'll remove meshes after)
|
|
1726
|
+
mesh.userData.__toRemoveForMerge = true;
|
|
1727
|
+
});
|
|
1728
|
+
if (groups.size === 0)
|
|
1729
|
+
return;
|
|
1730
|
+
// dynamic import BufferGeometryUtils and find merge function name
|
|
1731
|
+
const bufUtilsMod = await import('three/examples/jsm/utils/BufferGeometryUtils.js');
|
|
1732
|
+
// use || chain (avoid mixing ?? with || without parentheses)
|
|
1733
|
+
const mergeFn = bufUtilsMod.mergeBufferGeometries ||
|
|
1734
|
+
bufUtilsMod.mergeGeometries ||
|
|
1735
|
+
bufUtilsMod.mergeBufferGeometries || // defensive duplicate
|
|
1736
|
+
bufUtilsMod.mergeGeometries;
|
|
1737
|
+
if (!mergeFn)
|
|
1738
|
+
throw new Error('No merge function found in BufferGeometryUtils');
|
|
1739
|
+
// for each group, try merge
|
|
1740
|
+
for (const [key, { material, geoms }] of groups) {
|
|
1741
|
+
if (geoms.length <= 1) {
|
|
1742
|
+
// nothing to merge
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
// call merge function - signature typically mergeBufferGeometries(array, useGroups)
|
|
1746
|
+
const merged = mergeFn(geoms, false);
|
|
1747
|
+
if (!merged) {
|
|
1748
|
+
console.warn('merge returned null for group', key);
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
// create merged mesh at root (world-space geometry already applied)
|
|
1752
|
+
const mergedMesh = new THREE__namespace.Mesh(merged, material);
|
|
1753
|
+
root.add(mergedMesh);
|
|
1754
|
+
}
|
|
1755
|
+
// now remove original meshes flagged for removal
|
|
1756
|
+
const toRemove = [];
|
|
1757
|
+
root.traverse((ch) => {
|
|
1758
|
+
if (ch.userData?.__toRemoveForMerge)
|
|
1759
|
+
toRemove.push(ch);
|
|
1760
|
+
});
|
|
1761
|
+
toRemove.forEach((m) => {
|
|
1762
|
+
if (m.parent)
|
|
1763
|
+
m.parent.remove(m);
|
|
1764
|
+
// free original resources (geometries already cloned/applied), but careful with shared materials
|
|
1765
|
+
if (m.isMesh) {
|
|
1766
|
+
const mm = m;
|
|
1767
|
+
try {
|
|
1768
|
+
mm.geometry.dispose();
|
|
1688
1769
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
root.add(mergedMesh);
|
|
1770
|
+
catch { }
|
|
1771
|
+
// we do NOT dispose material because it may be reused by mergedMesh
|
|
1692
1772
|
}
|
|
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
1773
|
});
|
|
1714
1774
|
}
|
|
1715
1775
|
/* ---------------------
|
|
@@ -1726,7 +1786,7 @@ function disposeObject(obj) {
|
|
|
1726
1786
|
try {
|
|
1727
1787
|
m.geometry.dispose();
|
|
1728
1788
|
}
|
|
1729
|
-
catch
|
|
1789
|
+
catch { }
|
|
1730
1790
|
}
|
|
1731
1791
|
const mat = m.material;
|
|
1732
1792
|
if (mat) {
|
|
@@ -1748,14 +1808,14 @@ function disposeMaterial(mat) {
|
|
|
1748
1808
|
try {
|
|
1749
1809
|
mat[k].dispose();
|
|
1750
1810
|
}
|
|
1751
|
-
catch
|
|
1811
|
+
catch { }
|
|
1752
1812
|
}
|
|
1753
1813
|
});
|
|
1754
1814
|
try {
|
|
1755
1815
|
if (typeof mat.dispose === 'function')
|
|
1756
1816
|
mat.dispose();
|
|
1757
1817
|
}
|
|
1758
|
-
catch
|
|
1818
|
+
catch { }
|
|
1759
1819
|
}
|
|
1760
1820
|
// Helper to convert to simple material (stub)
|
|
1761
1821
|
function toSimpleMaterial(mat) {
|
|
@@ -1798,100 +1858,97 @@ const equirectCache = new Map();
|
|
|
1798
1858
|
* @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
|
|
1799
1859
|
* @param opts SkyboxOptions
|
|
1800
1860
|
*/
|
|
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
|
|
1861
|
+
async function loadCubeSkybox(renderer, scene, paths, opts = {}) {
|
|
1862
|
+
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
1863
|
+
if (!Array.isArray(paths) || paths.length !== 6)
|
|
1864
|
+
throw new Error('cube skybox requires 6 image paths');
|
|
1865
|
+
const key = paths.join('|');
|
|
1866
|
+
// Cache handling
|
|
1867
|
+
if (options.cache && cubeCache.has(key)) {
|
|
1868
|
+
const rec = cubeCache.get(key);
|
|
1869
|
+
rec.refCount += 1;
|
|
1870
|
+
// reapply to scene (in case it was removed)
|
|
1829
1871
|
if (options.setAsBackground)
|
|
1830
|
-
scene.background =
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1872
|
+
scene.background = rec.handle.backgroundTexture;
|
|
1873
|
+
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
1874
|
+
scene.environment = rec.handle.envRenderTarget.texture;
|
|
1875
|
+
return rec.handle;
|
|
1876
|
+
}
|
|
1877
|
+
// Load cube texture
|
|
1878
|
+
const loader = new THREE__namespace.CubeTextureLoader();
|
|
1879
|
+
const texture = await new Promise((resolve, reject) => {
|
|
1880
|
+
loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
1881
|
+
});
|
|
1882
|
+
// Set encoding and mapping
|
|
1883
|
+
if (options.useSRGBEncoding)
|
|
1884
|
+
texture.encoding = THREE__namespace.sRGBEncoding;
|
|
1885
|
+
texture.mapping = THREE__namespace.CubeReflectionMapping;
|
|
1886
|
+
// apply as background if required
|
|
1887
|
+
if (options.setAsBackground)
|
|
1888
|
+
scene.background = texture;
|
|
1889
|
+
// environment: use PMREM to produce a proper prefiltered env map for PBR
|
|
1890
|
+
let pmremGenerator = options.pmremGenerator ?? new THREE__namespace.PMREMGenerator(renderer);
|
|
1891
|
+
pmremGenerator.compileCubemapShader?.( /* optional */);
|
|
1892
|
+
// fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
|
|
1893
|
+
let envRenderTarget = null;
|
|
1894
|
+
if (pmremGenerator.fromCubemap) {
|
|
1895
|
+
envRenderTarget = pmremGenerator.fromCubemap(texture);
|
|
1896
|
+
}
|
|
1897
|
+
else {
|
|
1898
|
+
// Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
|
|
1899
|
+
// Simpler fallback: use the cube texture directly as environment (less correct for reflections).
|
|
1900
|
+
envRenderTarget = null;
|
|
1901
|
+
}
|
|
1902
|
+
if (options.setAsEnvironment) {
|
|
1903
|
+
if (envRenderTarget) {
|
|
1904
|
+
scene.environment = envRenderTarget.texture;
|
|
1838
1905
|
}
|
|
1839
1906
|
else {
|
|
1840
|
-
//
|
|
1841
|
-
|
|
1842
|
-
envRenderTarget = null;
|
|
1907
|
+
// fallback: use cube texture directly (works but not prefiltered)
|
|
1908
|
+
scene.environment = texture;
|
|
1843
1909
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1910
|
+
}
|
|
1911
|
+
const handle = {
|
|
1912
|
+
key,
|
|
1913
|
+
backgroundTexture: options.setAsBackground ? texture : null,
|
|
1914
|
+
envRenderTarget: envRenderTarget,
|
|
1915
|
+
pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
|
|
1916
|
+
setAsBackground: !!options.setAsBackground,
|
|
1917
|
+
setAsEnvironment: !!options.setAsEnvironment,
|
|
1918
|
+
dispose() {
|
|
1919
|
+
// remove from scene
|
|
1920
|
+
if (options.setAsBackground && scene.background === texture)
|
|
1921
|
+
scene.background = null;
|
|
1922
|
+
if (options.setAsEnvironment && scene.environment) {
|
|
1923
|
+
// only clear if it's the same texture we set
|
|
1924
|
+
if (envRenderTarget && scene.environment === envRenderTarget.texture)
|
|
1925
|
+
scene.environment = null;
|
|
1926
|
+
else if (scene.environment === texture)
|
|
1927
|
+
scene.environment = null;
|
|
1847
1928
|
}
|
|
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
|
-
}
|
|
1929
|
+
// dispose resources only if not cached/shared
|
|
1930
|
+
if (envRenderTarget) {
|
|
1878
1931
|
try {
|
|
1879
|
-
|
|
1932
|
+
envRenderTarget.dispose();
|
|
1880
1933
|
}
|
|
1881
|
-
catch
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1934
|
+
catch { }
|
|
1935
|
+
}
|
|
1936
|
+
try {
|
|
1937
|
+
texture.dispose();
|
|
1938
|
+
}
|
|
1939
|
+
catch { }
|
|
1940
|
+
// dispose pmremGenerator we created
|
|
1941
|
+
if (!options.pmremGenerator && pmremGenerator) {
|
|
1942
|
+
try {
|
|
1943
|
+
pmremGenerator.dispose();
|
|
1888
1944
|
}
|
|
1945
|
+
catch { }
|
|
1889
1946
|
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
if (options.cache)
|
|
1950
|
+
cubeCache.set(key, { handle, refCount: 1 });
|
|
1951
|
+
return handle;
|
|
1895
1952
|
}
|
|
1896
1953
|
/**
|
|
1897
1954
|
* Load Equirectangular/Single Image (Supports HDR via RGBELoader)
|
|
@@ -1900,95 +1957,90 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
|
|
|
1900
1957
|
* @param url string - *.hdr, *.exr, *.jpg, *.png
|
|
1901
1958
|
* @param opts SkyboxOptions
|
|
1902
1959
|
*/
|
|
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
|
-
|
|
1960
|
+
async function loadEquirectSkybox(renderer, scene, url, opts = {}) {
|
|
1961
|
+
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
1962
|
+
const key = url;
|
|
1963
|
+
if (options.cache && equirectCache.has(key)) {
|
|
1964
|
+
const rec = equirectCache.get(key);
|
|
1965
|
+
rec.refCount += 1;
|
|
1966
|
+
if (options.setAsBackground)
|
|
1967
|
+
scene.background = rec.handle.backgroundTexture;
|
|
1968
|
+
if (options.setAsEnvironment && rec.handle.envRenderTarget)
|
|
1969
|
+
scene.environment = rec.handle.envRenderTarget.texture;
|
|
1970
|
+
return rec.handle;
|
|
1971
|
+
}
|
|
1972
|
+
// Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
|
|
1973
|
+
const isHDR = /\.hdr$|\.exr$/i.test(url);
|
|
1974
|
+
let hdrTexture;
|
|
1975
|
+
if (isHDR) {
|
|
1976
|
+
const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js');
|
|
1977
|
+
hdrTexture = await new Promise((resolve, reject) => {
|
|
1978
|
+
new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
|
|
1979
|
+
});
|
|
1980
|
+
// RGBE textures typically use LinearEncoding
|
|
1981
|
+
hdrTexture.encoding = THREE__namespace.LinearEncoding;
|
|
1982
|
+
}
|
|
1983
|
+
else {
|
|
1984
|
+
// ordinary image - use TextureLoader
|
|
1985
|
+
const loader = new THREE__namespace.TextureLoader();
|
|
1986
|
+
hdrTexture = await new Promise((resolve, reject) => {
|
|
1987
|
+
loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
|
|
1988
|
+
});
|
|
1989
|
+
if (options.useSRGBEncoding)
|
|
1990
|
+
hdrTexture.encoding = THREE__namespace.sRGBEncoding;
|
|
1991
|
+
}
|
|
1992
|
+
// PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
|
|
1993
|
+
const pmremGenerator = options.pmremGenerator ?? new THREE__namespace.PMREMGenerator(renderer);
|
|
1994
|
+
pmremGenerator.compileEquirectangularShader?.();
|
|
1995
|
+
const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
|
|
1996
|
+
// envTexture to use for scene.environment
|
|
1997
|
+
const envTexture = envRenderTarget.texture;
|
|
1998
|
+
// set background and/or environment
|
|
1999
|
+
if (options.setAsBackground) {
|
|
2000
|
+
// for background it's ok to use the equirect texture directly or the envTexture
|
|
2001
|
+
// envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
|
|
2002
|
+
scene.background = envTexture;
|
|
2003
|
+
}
|
|
2004
|
+
if (options.setAsEnvironment) {
|
|
2005
|
+
scene.environment = envTexture;
|
|
2006
|
+
}
|
|
2007
|
+
// We can dispose the original hdrTexture (the PMREM target contains the needed data)
|
|
2008
|
+
try {
|
|
2009
|
+
hdrTexture.dispose();
|
|
2010
|
+
}
|
|
2011
|
+
catch { }
|
|
2012
|
+
const handle = {
|
|
2013
|
+
key,
|
|
2014
|
+
backgroundTexture: options.setAsBackground ? envTexture : null,
|
|
2015
|
+
envRenderTarget,
|
|
2016
|
+
pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
|
|
2017
|
+
setAsBackground: !!options.setAsBackground,
|
|
2018
|
+
setAsEnvironment: !!options.setAsEnvironment,
|
|
2019
|
+
dispose() {
|
|
2020
|
+
if (options.setAsBackground && scene.background === envTexture)
|
|
2021
|
+
scene.background = null;
|
|
2022
|
+
if (options.setAsEnvironment && scene.environment === envTexture)
|
|
2023
|
+
scene.environment = null;
|
|
2024
|
+
try {
|
|
2025
|
+
envRenderTarget.dispose();
|
|
2026
|
+
}
|
|
2027
|
+
catch { }
|
|
2028
|
+
if (!options.pmremGenerator && pmremGenerator) {
|
|
1969
2029
|
try {
|
|
1970
|
-
|
|
1971
|
-
}
|
|
1972
|
-
catch (_a) { }
|
|
1973
|
-
if (!options.pmremGenerator && pmremGenerator) {
|
|
1974
|
-
try {
|
|
1975
|
-
pmremGenerator.dispose();
|
|
1976
|
-
}
|
|
1977
|
-
catch (_b) { }
|
|
2030
|
+
pmremGenerator.dispose();
|
|
1978
2031
|
}
|
|
2032
|
+
catch { }
|
|
1979
2033
|
}
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
if (options.cache)
|
|
2037
|
+
equirectCache.set(key, { handle, refCount: 1 });
|
|
2038
|
+
return handle;
|
|
1985
2039
|
}
|
|
1986
|
-
function loadSkybox(
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
return loadEquirectSkybox(renderer, scene, params.url, opts);
|
|
1991
|
-
});
|
|
2040
|
+
async function loadSkybox(renderer, scene, params, opts = {}) {
|
|
2041
|
+
if (params.type === 'cube')
|
|
2042
|
+
return loadCubeSkybox(renderer, scene, params.paths, opts);
|
|
2043
|
+
return loadEquirectSkybox(renderer, scene, params.url, opts);
|
|
1992
2044
|
}
|
|
1993
2045
|
/* -------------------------
|
|
1994
2046
|
Cache / Reference Counting Helper Methods
|
|
@@ -2198,10 +2250,9 @@ class BlueSkyManager {
|
|
|
2198
2250
|
* Usually called when the scene is completely destroyed or the application exits
|
|
2199
2251
|
*/
|
|
2200
2252
|
destroy() {
|
|
2201
|
-
var _a;
|
|
2202
2253
|
this.cancelLoad();
|
|
2203
2254
|
this.dispose();
|
|
2204
|
-
|
|
2255
|
+
this.pmremGen?.dispose();
|
|
2205
2256
|
this.isInitialized = false;
|
|
2206
2257
|
this.loadingState = 'idle';
|
|
2207
2258
|
}
|
|
@@ -2235,20 +2286,19 @@ const BlueSky = new BlueSkyManager();
|
|
|
2235
2286
|
* - RAF management optimization
|
|
2236
2287
|
*/
|
|
2237
2288
|
function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, options) {
|
|
2238
|
-
var _a, _b, _c, _d, _e, _f;
|
|
2239
2289
|
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:
|
|
2290
|
+
fontSize: options?.fontSize || '12px',
|
|
2291
|
+
color: options?.color || '#ffffff',
|
|
2292
|
+
background: options?.background || '#1890ff',
|
|
2293
|
+
padding: options?.padding || '6px 10px',
|
|
2294
|
+
borderRadius: options?.borderRadius || '6px',
|
|
2295
|
+
lift: options?.lift ?? 100,
|
|
2296
|
+
dotSize: options?.dotSize ?? 6,
|
|
2297
|
+
dotSpacing: options?.dotSpacing ?? 2,
|
|
2298
|
+
lineColor: options?.lineColor || 'rgba(200,200,200,0.7)',
|
|
2299
|
+
lineWidth: options?.lineWidth ?? 1,
|
|
2300
|
+
updateInterval: options?.updateInterval ?? 0, // Default update every frame
|
|
2301
|
+
fadeInDuration: options?.fadeInDuration ?? 300, // Fade-in duration
|
|
2252
2302
|
};
|
|
2253
2303
|
const container = document.createElement('div');
|
|
2254
2304
|
container.style.position = 'absolute';
|
|
@@ -2271,7 +2321,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2271
2321
|
svg.style.zIndex = '1';
|
|
2272
2322
|
container.appendChild(svg);
|
|
2273
2323
|
let currentModel = parentModel;
|
|
2274
|
-
let currentLabelsMap =
|
|
2324
|
+
let currentLabelsMap = { ...modelLabelsMap };
|
|
2275
2325
|
let labels = [];
|
|
2276
2326
|
let isActive = true;
|
|
2277
2327
|
let isPaused = false;
|
|
@@ -2352,9 +2402,8 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2352
2402
|
if (!currentModel)
|
|
2353
2403
|
return;
|
|
2354
2404
|
currentModel.traverse((child) => {
|
|
2355
|
-
var _a;
|
|
2356
2405
|
if (child.isMesh || child.type === 'Group') {
|
|
2357
|
-
const labelText =
|
|
2406
|
+
const labelText = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))?.[1];
|
|
2358
2407
|
if (!labelText)
|
|
2359
2408
|
return;
|
|
2360
2409
|
const wrapper = document.createElement('div');
|
|
@@ -2459,7 +2508,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2459
2508
|
rebuildLabels();
|
|
2460
2509
|
},
|
|
2461
2510
|
updateLabelsMap(newMap) {
|
|
2462
|
-
currentLabelsMap =
|
|
2511
|
+
currentLabelsMap = { ...newMap };
|
|
2463
2512
|
rebuildLabels();
|
|
2464
2513
|
},
|
|
2465
2514
|
// Pause update
|
|
@@ -2557,205 +2606,197 @@ class GroupExploder {
|
|
|
2557
2606
|
* @param newSet The new set of meshes
|
|
2558
2607
|
* @param contextId Optional context ID to distinguish business scenarios
|
|
2559
2608
|
*/
|
|
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
|
-
|
|
2609
|
+
async setMeshes(newSet, options) {
|
|
2610
|
+
const autoRestorePrev = options?.autoRestorePrev ?? true;
|
|
2611
|
+
const restoreDuration = options?.restoreDuration ?? 300;
|
|
2612
|
+
this.log(`setMeshes called. newSetSize=${newSet ? newSet.size : 0}, autoRestorePrev=${autoRestorePrev}`);
|
|
2613
|
+
// If the newSet is null and currentSet is null -> nothing
|
|
2614
|
+
if (!newSet && !this.currentSet) {
|
|
2615
|
+
this.log('setMeshes: both newSet and currentSet are null, nothing to do');
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
// If both exist and are the same reference, we still must detect content changes.
|
|
2619
|
+
const sameReference = this.currentSet === newSet;
|
|
2620
|
+
// Prepare prevSet snapshot (we copy current to prev)
|
|
2621
|
+
if (this.currentSet) {
|
|
2622
|
+
this.prevSet = this.currentSet;
|
|
2623
|
+
this.prevStateMap = new Map(this.stateMap);
|
|
2624
|
+
this.log(`setMeshes: backed up current->prev prevSetSize=${this.prevSet.size}`);
|
|
2625
|
+
}
|
|
2626
|
+
else {
|
|
2627
|
+
this.prevSet = null;
|
|
2628
|
+
this.prevStateMap = new Map();
|
|
2629
|
+
}
|
|
2630
|
+
// If we used to be exploded and need to restore prevSet, do that first (await)
|
|
2631
|
+
if (this.prevSet && autoRestorePrev && this.isExploded) {
|
|
2632
|
+
this.log('setMeshes: need to restore prevSet before applying newSet');
|
|
2633
|
+
await this.restoreSet(this.prevSet, this.prevStateMap, restoreDuration, { debug: true });
|
|
2634
|
+
this.log('setMeshes: prevSet restore done');
|
|
2635
|
+
this.prevStateMap.clear();
|
|
2636
|
+
this.prevSet = null;
|
|
2637
|
+
}
|
|
2638
|
+
// Now register newSet: we clear and rebuild stateMap carefully.
|
|
2639
|
+
// But we must handle the case where caller reuses same Set object and just mutated elements.
|
|
2640
|
+
// We will compute additions and removals.
|
|
2641
|
+
const oldSet = this.currentSet;
|
|
2642
|
+
this.currentSet = newSet;
|
|
2643
|
+
// If newSet is null -> simply clear stateMap
|
|
2644
|
+
if (!this.currentSet) {
|
|
2645
|
+
this.stateMap.clear();
|
|
2646
|
+
this.log('setMeshes: newSet is null -> cleared stateMap');
|
|
2647
|
+
this.isExploded = false;
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
// If we have oldSet (could be same reference) then compute diffs
|
|
2651
|
+
if (oldSet) {
|
|
2652
|
+
// If same reference but size or content differs -> handle diffs
|
|
2653
|
+
const wasSameRef = sameReference;
|
|
2654
|
+
let added = [];
|
|
2655
|
+
let removed = [];
|
|
2656
|
+
// Build maps of membership
|
|
2657
|
+
const oldMembers = new Set(Array.from(oldSet));
|
|
2658
|
+
const newMembers = new Set(Array.from(this.currentSet));
|
|
2659
|
+
// find removals
|
|
2660
|
+
oldMembers.forEach((m) => {
|
|
2661
|
+
if (!newMembers.has(m))
|
|
2662
|
+
removed.push(m);
|
|
2663
|
+
});
|
|
2664
|
+
// find additions
|
|
2665
|
+
newMembers.forEach((m) => {
|
|
2666
|
+
if (!oldMembers.has(m))
|
|
2667
|
+
added.push(m);
|
|
2668
|
+
});
|
|
2669
|
+
if (wasSameRef && added.length === 0 && removed.length === 0) {
|
|
2670
|
+
// truly identical (no content changes)
|
|
2671
|
+
this.log('setMeshes: same reference and identical contents -> nothing to update');
|
|
2601
2672
|
return;
|
|
2602
2673
|
}
|
|
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;
|
|
2674
|
+
this.log(`setMeshes: diff detected -> added=${added.length}, removed=${removed.length}`);
|
|
2675
|
+
// Remove snapshots for removed meshes
|
|
2676
|
+
removed.forEach((m) => {
|
|
2677
|
+
if (this.stateMap.has(m)) {
|
|
2678
|
+
this.stateMap.delete(m);
|
|
2626
2679
|
}
|
|
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
|
-
});
|
|
2680
|
+
});
|
|
2681
|
+
// Ensure snapshots exist for current set members (create for newly added meshes)
|
|
2682
|
+
await this.ensureSnapshotsForSet(this.currentSet);
|
|
2683
|
+
this.log(`setMeshes: after diff handling, stateMap size=${this.stateMap.size}`);
|
|
2684
|
+
this.isExploded = false;
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
else {
|
|
2688
|
+
// no oldSet -> brand new registration
|
|
2689
|
+
this.stateMap.clear();
|
|
2690
|
+
await this.ensureSnapshotsForSet(this.currentSet);
|
|
2691
|
+
this.log(`setMeshes: recorded stateMap entries for newSet size=${this.stateMap.size}`);
|
|
2692
|
+
this.isExploded = false;
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2649
2695
|
}
|
|
2650
2696
|
/**
|
|
2651
2697
|
* ensureSnapshotsForSet: for each mesh in set, ensure stateMap has an entry.
|
|
2652
2698
|
* If missing, record current matrixWorld as originalMatrixWorld (best-effort).
|
|
2653
2699
|
*/
|
|
2654
|
-
ensureSnapshotsForSet(set) {
|
|
2655
|
-
|
|
2656
|
-
|
|
2700
|
+
async ensureSnapshotsForSet(set) {
|
|
2701
|
+
set.forEach((m) => {
|
|
2702
|
+
try {
|
|
2703
|
+
m.updateMatrixWorld(true);
|
|
2704
|
+
}
|
|
2705
|
+
catch { }
|
|
2706
|
+
if (!this.stateMap.has(m)) {
|
|
2657
2707
|
try {
|
|
2658
|
-
|
|
2708
|
+
this.stateMap.set(m, {
|
|
2709
|
+
originalParent: m.parent || null,
|
|
2710
|
+
originalMatrixWorld: (m.matrixWorld && m.matrixWorld.clone()) || new THREE__namespace.Matrix4().copy(m.matrix),
|
|
2711
|
+
});
|
|
2712
|
+
// Also store in userData for extra resilience
|
|
2713
|
+
m.userData.__originalMatrixWorld = this.stateMap.get(m).originalMatrixWorld.clone();
|
|
2659
2714
|
}
|
|
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
|
-
}
|
|
2715
|
+
catch (e) {
|
|
2716
|
+
this.log(`ensureSnapshotsForSet: failed to snapshot mesh ${m.name || m.id}: ${e.message}`);
|
|
2673
2717
|
}
|
|
2674
|
-
}
|
|
2718
|
+
}
|
|
2675
2719
|
});
|
|
2676
2720
|
}
|
|
2677
2721
|
/**
|
|
2678
2722
|
* explode: compute targets first, compute targetBound using targets + mesh radii,
|
|
2679
2723
|
* animate camera to that targetBound, then animate meshes to targets.
|
|
2680
2724
|
*/
|
|
2681
|
-
explode(opts) {
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2725
|
+
async explode(opts) {
|
|
2726
|
+
if (!this.currentSet || this.currentSet.size === 0) {
|
|
2727
|
+
this.log('explode: empty currentSet, nothing to do');
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
const { spacing = 2, duration = 1000, lift = 0.5, cameraPadding = 1.5, mode = 'spiral', dimOthers = { enabled: true, opacity: 0.25 }, debug = false, } = opts || {};
|
|
2731
|
+
this.log(`explode called. setSize=${this.currentSet.size}, mode=${mode}, spacing=${spacing}, duration=${duration}, lift=${lift}, dim=${dimOthers.enabled}`);
|
|
2732
|
+
this.cancelAnimations();
|
|
2733
|
+
const meshes = Array.from(this.currentSet);
|
|
2734
|
+
// ensure snapshots exist for any meshes that may have been added after initial registration
|
|
2735
|
+
await this.ensureSnapshotsForSet(this.currentSet);
|
|
2736
|
+
// compute center/radius from current meshes (fallback)
|
|
2737
|
+
const initial = this.computeBoundingSphereForMeshes(meshes);
|
|
2738
|
+
const center = initial.center;
|
|
2739
|
+
const baseRadius = Math.max(1, initial.radius);
|
|
2740
|
+
this.log(`explode: initial center=${center.toArray().map((n) => n.toFixed(3))}, baseRadius=${baseRadius.toFixed(3)}`);
|
|
2741
|
+
// compute targets (pure calculation)
|
|
2742
|
+
const targets = this.computeTargetsByMode(meshes, center, baseRadius + spacing, { lift, mode });
|
|
2743
|
+
this.log(`explode: computed ${targets.length} target positions`);
|
|
2744
|
+
// compute target-based bounding sphere (targets + per-mesh radius)
|
|
2745
|
+
const targetBound = this.computeBoundingSphereForPositionsAndMeshes(targets, meshes);
|
|
2746
|
+
this.log(`explode: targetBound center=${targetBound.center.toArray().map((n) => n.toFixed(3))}, radius=${targetBound.radius.toFixed(3)}`);
|
|
2747
|
+
await this.animateCameraToFit(targetBound.center, targetBound.radius, { duration: Math.min(600, duration), padding: cameraPadding });
|
|
2748
|
+
this.log('explode: camera animation to target bound completed');
|
|
2749
|
+
// apply dim if needed with context id
|
|
2750
|
+
const contextId = dimOthers?.enabled ? this.applyDimToOthers(meshes, dimOthers.opacity ?? 0.25, { debug }) : null;
|
|
2751
|
+
if (contextId)
|
|
2752
|
+
this.log(`explode: applied dim for context ${contextId}`);
|
|
2753
|
+
// capture starts after camera move
|
|
2754
|
+
const starts = meshes.map((m) => {
|
|
2755
|
+
const v = new THREE__namespace.Vector3();
|
|
2756
|
+
try {
|
|
2757
|
+
m.getWorldPosition(v);
|
|
2687
2758
|
}
|
|
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);
|
|
2759
|
+
catch {
|
|
2760
|
+
// fallback to originalMatrixWorld if available
|
|
2761
|
+
const st = this.stateMap.get(m);
|
|
2762
|
+
if (st)
|
|
2763
|
+
v.setFromMatrixPosition(st.originalMatrixWorld);
|
|
2764
|
+
}
|
|
2765
|
+
return v;
|
|
2766
|
+
});
|
|
2767
|
+
const startTime = performance.now();
|
|
2768
|
+
const total = Math.max(1, duration);
|
|
2769
|
+
const tick = (now) => {
|
|
2770
|
+
const t = Math.min(1, (now - startTime) / total);
|
|
2771
|
+
const eased = easeInOutQuad(t);
|
|
2772
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
2773
|
+
const m = meshes[i];
|
|
2774
|
+
const s = starts[i];
|
|
2775
|
+
const tar = targets[i];
|
|
2776
|
+
const cur = s.clone().lerp(tar, eased);
|
|
2777
|
+
if (m.parent) {
|
|
2778
|
+
const local = cur.clone();
|
|
2779
|
+
m.parent.worldToLocal(local);
|
|
2780
|
+
m.position.copy(local);
|
|
2749
2781
|
}
|
|
2750
2782
|
else {
|
|
2751
|
-
|
|
2752
|
-
this.isExploded = true;
|
|
2753
|
-
this.log(`explode: completed. contextId=${contextId !== null && contextId !== void 0 ? contextId : 'none'}`);
|
|
2783
|
+
m.position.copy(cur);
|
|
2754
2784
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2785
|
+
m.updateMatrix();
|
|
2786
|
+
}
|
|
2787
|
+
if (this.controls && typeof this.controls.update === 'function')
|
|
2788
|
+
this.controls.update();
|
|
2789
|
+
if (t < 1) {
|
|
2790
|
+
this.animId = requestAnimationFrame(tick);
|
|
2791
|
+
}
|
|
2792
|
+
else {
|
|
2793
|
+
this.animId = null;
|
|
2794
|
+
this.isExploded = true;
|
|
2795
|
+
this.log(`explode: completed. contextId=${contextId ?? 'none'}`);
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
this.animId = requestAnimationFrame(tick);
|
|
2799
|
+
return;
|
|
2759
2800
|
}
|
|
2760
2801
|
/**
|
|
2761
2802
|
* Restore all exploded meshes to their original transform:
|
|
@@ -2778,7 +2819,7 @@ class GroupExploder {
|
|
|
2778
2819
|
*/
|
|
2779
2820
|
restoreSet(set, stateMap, duration = 400, opts) {
|
|
2780
2821
|
if (!set || set.size === 0) {
|
|
2781
|
-
if (opts
|
|
2822
|
+
if (opts?.debug)
|
|
2782
2823
|
this.log('restoreSet: empty set, nothing to restore');
|
|
2783
2824
|
return Promise.resolve();
|
|
2784
2825
|
}
|
|
@@ -2791,12 +2832,12 @@ class GroupExploder {
|
|
|
2791
2832
|
try {
|
|
2792
2833
|
m.updateMatrixWorld(true);
|
|
2793
2834
|
}
|
|
2794
|
-
catch
|
|
2835
|
+
catch { }
|
|
2795
2836
|
const s = new THREE__namespace.Vector3();
|
|
2796
2837
|
try {
|
|
2797
2838
|
m.getWorldPosition(s);
|
|
2798
2839
|
}
|
|
2799
|
-
catch
|
|
2840
|
+
catch {
|
|
2800
2841
|
s.set(0, 0, 0);
|
|
2801
2842
|
}
|
|
2802
2843
|
starts.push(s);
|
|
@@ -2894,7 +2935,7 @@ class GroupExploder {
|
|
|
2894
2935
|
});
|
|
2895
2936
|
}
|
|
2896
2937
|
// material dim with context id
|
|
2897
|
-
applyDimToOthers(explodingMeshes, opacity = 0.25,
|
|
2938
|
+
applyDimToOthers(explodingMeshes, opacity = 0.25, _opts) {
|
|
2898
2939
|
const contextId = `ctx_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
|
2899
2940
|
const explodingSet = new Set(explodingMeshes);
|
|
2900
2941
|
const touched = new Set();
|
|
@@ -2905,11 +2946,10 @@ class GroupExploder {
|
|
|
2905
2946
|
if (explodingSet.has(mesh))
|
|
2906
2947
|
return;
|
|
2907
2948
|
const applyMat = (mat) => {
|
|
2908
|
-
var _a;
|
|
2909
2949
|
if (!this.materialSnaps.has(mat)) {
|
|
2910
2950
|
this.materialSnaps.set(mat, {
|
|
2911
2951
|
transparent: !!mat.transparent,
|
|
2912
|
-
opacity:
|
|
2952
|
+
opacity: mat.opacity ?? 1,
|
|
2913
2953
|
depthWrite: mat.depthWrite,
|
|
2914
2954
|
});
|
|
2915
2955
|
}
|
|
@@ -2936,7 +2976,7 @@ class GroupExploder {
|
|
|
2936
2976
|
return contextId;
|
|
2937
2977
|
}
|
|
2938
2978
|
// clean contexts for meshes (restore materials whose contexts are removed)
|
|
2939
|
-
cleanContextsForMeshes(
|
|
2979
|
+
cleanContextsForMeshes(_meshes) {
|
|
2940
2980
|
// conservative strategy: for each context we created, delete it and restore materials accordingly
|
|
2941
2981
|
for (const [contextId, mats] of Array.from(this.contextMaterials.entries())) {
|
|
2942
2982
|
mats.forEach((mat) => {
|
|
@@ -3050,7 +3090,7 @@ class GroupExploder {
|
|
|
3050
3090
|
}
|
|
3051
3091
|
}
|
|
3052
3092
|
}
|
|
3053
|
-
catch
|
|
3093
|
+
catch {
|
|
3054
3094
|
radius = 0;
|
|
3055
3095
|
}
|
|
3056
3096
|
if (!isFinite(radius) || radius < 0 || radius > 1e8)
|
|
@@ -3073,10 +3113,9 @@ class GroupExploder {
|
|
|
3073
3113
|
}
|
|
3074
3114
|
// computeTargetsByMode (unchanged logic but pure function)
|
|
3075
3115
|
computeTargetsByMode(meshes, center, baseRadius, opts) {
|
|
3076
|
-
var _a, _b;
|
|
3077
3116
|
const n = meshes.length;
|
|
3078
|
-
const lift =
|
|
3079
|
-
const mode =
|
|
3117
|
+
const lift = opts.lift ?? 0.5;
|
|
3118
|
+
const mode = opts.mode ?? 'ring';
|
|
3080
3119
|
const targets = [];
|
|
3081
3120
|
if (mode === 'ring') {
|
|
3082
3121
|
for (let i = 0; i < n; i++) {
|
|
@@ -3118,9 +3157,8 @@ class GroupExploder {
|
|
|
3118
3157
|
return targets;
|
|
3119
3158
|
}
|
|
3120
3159
|
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;
|
|
3160
|
+
const duration = opts?.duration ?? 600;
|
|
3161
|
+
const padding = opts?.padding ?? 1.5;
|
|
3124
3162
|
if (!(this.camera instanceof THREE__namespace.PerspectiveCamera)) {
|
|
3125
3163
|
if (this.controls && this.controls.target) {
|
|
3126
3164
|
// Fallback for non-PerspectiveCamera
|
|
@@ -3132,14 +3170,13 @@ class GroupExploder {
|
|
|
3132
3170
|
const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
|
|
3133
3171
|
const startTime = performance.now();
|
|
3134
3172
|
const tick = (now) => {
|
|
3135
|
-
var _a;
|
|
3136
3173
|
const t = Math.min(1, (now - startTime) / duration);
|
|
3137
3174
|
const k = easeInOutQuad(t);
|
|
3138
3175
|
if (this.controls && this.controls.target) {
|
|
3139
3176
|
this.controls.target.lerpVectors(startTarget, endTarget, k);
|
|
3140
3177
|
}
|
|
3141
3178
|
this.camera.position.lerpVectors(startPos, endPos, k);
|
|
3142
|
-
if (
|
|
3179
|
+
if (this.controls?.update)
|
|
3143
3180
|
this.controls.update();
|
|
3144
3181
|
if (t < 1) {
|
|
3145
3182
|
this.cameraAnimId = requestAnimationFrame(tick);
|
|
@@ -3162,8 +3199,8 @@ class GroupExploder {
|
|
|
3162
3199
|
const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
|
|
3163
3200
|
const dist = Math.max(distV, distH) * padding;
|
|
3164
3201
|
const startPos = this.camera.position.clone();
|
|
3165
|
-
const startTarget =
|
|
3166
|
-
if (!
|
|
3202
|
+
const startTarget = this.controls?.target ? this.controls.target.clone() : new THREE__namespace.Vector3(); // assumption
|
|
3203
|
+
if (!this.controls?.target) {
|
|
3167
3204
|
this.camera.getWorldDirection(startTarget);
|
|
3168
3205
|
startTarget.add(startPos);
|
|
3169
3206
|
}
|
|
@@ -3176,13 +3213,12 @@ class GroupExploder {
|
|
|
3176
3213
|
return new Promise((resolve) => {
|
|
3177
3214
|
const startTime = performance.now();
|
|
3178
3215
|
const tick = (now) => {
|
|
3179
|
-
var _a, _b;
|
|
3180
3216
|
const t = Math.min(1, (now - startTime) / duration);
|
|
3181
3217
|
const k = easeInOutQuad(t);
|
|
3182
3218
|
this.camera.position.lerpVectors(startPos, endPos, k);
|
|
3183
3219
|
if (this.controls && this.controls.target) {
|
|
3184
3220
|
this.controls.target.lerpVectors(startTarget, endTarget, k);
|
|
3185
|
-
|
|
3221
|
+
this.controls.update?.();
|
|
3186
3222
|
}
|
|
3187
3223
|
else {
|
|
3188
3224
|
this.camera.lookAt(endTarget); // simple lookAt if no controls
|
|
@@ -3231,183 +3267,134 @@ class GroupExploder {
|
|
|
3231
3267
|
* @file autoSetup.ts
|
|
3232
3268
|
* @description
|
|
3233
3269
|
* 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
3270
|
*/
|
|
3239
3271
|
/**
|
|
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
|
|
3272
|
+
* Fit camera to object bounding box
|
|
3253
3273
|
*/
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3274
|
+
function fitCameraToObject(camera, object, padding = 1.2, elevation = 0.2) {
|
|
3275
|
+
const box = new THREE__namespace.Box3().setFromObject(object);
|
|
3276
|
+
if (!isFinite(box.min.x))
|
|
3277
|
+
return { center: new THREE__namespace.Vector3(), radius: 0 };
|
|
3278
|
+
const sphere = new THREE__namespace.Sphere();
|
|
3279
|
+
box.getBoundingSphere(sphere);
|
|
3280
|
+
const center = sphere.center.clone();
|
|
3281
|
+
const radius = Math.max(0.001, sphere.radius);
|
|
3282
|
+
const fov = (camera.fov * Math.PI) / 180;
|
|
3283
|
+
const halfFov = fov / 2;
|
|
3284
|
+
const sinHalfFov = Math.max(Math.sin(halfFov), 0.001);
|
|
3285
|
+
const distance = (radius * padding) / sinHalfFov;
|
|
3286
|
+
const dir = new THREE__namespace.Vector3(0, Math.sin(elevation), Math.cos(elevation)).normalize();
|
|
3287
|
+
const desiredPos = center.clone().add(dir.multiplyScalar(distance));
|
|
3288
|
+
camera.position.copy(desiredPos);
|
|
3289
|
+
camera.lookAt(center);
|
|
3290
|
+
camera.near = Math.max(0.001, radius / 1000);
|
|
3291
|
+
camera.far = Math.max(1000, radius * 50);
|
|
3292
|
+
camera.updateProjectionMatrix();
|
|
3293
|
+
return { center, radius };
|
|
3294
|
+
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Setup default lighting for a model
|
|
3297
|
+
*/
|
|
3298
|
+
function setupDefaultLights(scene, model, options = {}) {
|
|
3299
|
+
const box = new THREE__namespace.Box3().setFromObject(model);
|
|
3300
|
+
const sphere = new THREE__namespace.Sphere();
|
|
3301
|
+
box.getBoundingSphere(sphere);
|
|
3302
|
+
const center = sphere.center.clone();
|
|
3303
|
+
const radius = Math.max(0.001, sphere.radius);
|
|
3260
3304
|
const opts = {
|
|
3261
|
-
padding:
|
|
3262
|
-
elevation:
|
|
3263
|
-
enableShadows:
|
|
3264
|
-
shadowMapSize:
|
|
3265
|
-
directionalCount:
|
|
3266
|
-
setMeshShadowProps:
|
|
3267
|
-
renderer:
|
|
3305
|
+
padding: options.padding ?? 1.2,
|
|
3306
|
+
elevation: options.elevation ?? 0.2,
|
|
3307
|
+
enableShadows: options.enableShadows ?? false,
|
|
3308
|
+
shadowMapSize: options.shadowMapSize ?? 1024,
|
|
3309
|
+
directionalCount: options.directionalCount ?? 4,
|
|
3310
|
+
setMeshShadowProps: options.setMeshShadowProps ?? true,
|
|
3311
|
+
renderer: options.renderer ?? null,
|
|
3268
3312
|
};
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3313
|
+
if (opts.renderer && opts.enableShadows) {
|
|
3314
|
+
opts.renderer.shadowMap.enabled = true;
|
|
3315
|
+
opts.renderer.shadowMap.type = THREE__namespace.PCFSoftShadowMap;
|
|
3316
|
+
}
|
|
3317
|
+
const lightsGroup = new THREE__namespace.Group();
|
|
3318
|
+
lightsGroup.name = 'autoSetupLightsGroup';
|
|
3319
|
+
lightsGroup.position.copy(center);
|
|
3320
|
+
scene.add(lightsGroup);
|
|
3321
|
+
const hemi = new THREE__namespace.HemisphereLight(0xffffff, 0x444444, 0.6);
|
|
3322
|
+
hemi.position.set(0, radius * 2.0, 0);
|
|
3323
|
+
lightsGroup.add(hemi);
|
|
3324
|
+
const ambient = new THREE__namespace.AmbientLight(0xffffff, 0.25);
|
|
3325
|
+
lightsGroup.add(ambient);
|
|
3326
|
+
const dirCount = Math.max(1, Math.floor(opts.directionalCount));
|
|
3327
|
+
const dirs = [new THREE__namespace.Vector3(0, 1, 0)];
|
|
3328
|
+
for (let i = 0; i < dirCount; i++) {
|
|
3329
|
+
const angle = (i / dirCount) * Math.PI * 2;
|
|
3330
|
+
const v = new THREE__namespace.Vector3(Math.cos(angle), 0.3, Math.sin(angle)).normalize();
|
|
3331
|
+
dirs.push(v);
|
|
3332
|
+
}
|
|
3333
|
+
const shadowCamSize = Math.max(1, radius * 1.5);
|
|
3334
|
+
dirs.forEach((d, i) => {
|
|
3335
|
+
const light = new THREE__namespace.DirectionalLight(0xffffff, i === 0 ? 1.5 : 1.2);
|
|
3336
|
+
light.position.copy(d.clone().multiplyScalar(radius * 2.5));
|
|
3337
|
+
light.target.position.copy(center);
|
|
3338
|
+
light.name = `auto_dir_${i}`;
|
|
3339
|
+
lightsGroup.add(light);
|
|
3340
|
+
lightsGroup.add(light.target);
|
|
3341
|
+
if (opts.enableShadows) {
|
|
3342
|
+
light.castShadow = true;
|
|
3343
|
+
light.shadow.mapSize.width = opts.shadowMapSize;
|
|
3344
|
+
light.shadow.mapSize.height = opts.shadowMapSize;
|
|
3345
|
+
const cam = light.shadow.camera;
|
|
3346
|
+
const s = shadowCamSize;
|
|
3347
|
+
cam.left = -s;
|
|
3348
|
+
cam.right = s;
|
|
3349
|
+
cam.top = s;
|
|
3350
|
+
cam.bottom = -s;
|
|
3351
|
+
cam.near = 0.1;
|
|
3352
|
+
cam.far = radius * 10 + 50;
|
|
3353
|
+
light.shadow.bias = -5e-4;
|
|
3275
3354
|
}
|
|
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;
|
|
3355
|
+
});
|
|
3356
|
+
if (opts.setMeshShadowProps) {
|
|
3357
|
+
model.traverse((ch) => {
|
|
3358
|
+
if (ch.isMesh) {
|
|
3359
|
+
const mesh = ch;
|
|
3360
|
+
const isSkinned = mesh.isSkinnedMesh;
|
|
3361
|
+
mesh.castShadow = opts.enableShadows && !isSkinned ? true : mesh.castShadow;
|
|
3362
|
+
mesh.receiveShadow = opts.enableShadows ? true : mesh.receiveShadow;
|
|
3363
|
+
}
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
const handle = {
|
|
3367
|
+
lightsGroup,
|
|
3368
|
+
center,
|
|
3369
|
+
radius,
|
|
3370
|
+
updateLightIntensity(factor) {
|
|
3371
|
+
lightsGroup.traverse((node) => {
|
|
3372
|
+
if (node.isLight) {
|
|
3373
|
+
const light = node;
|
|
3374
|
+
light.intensity *= factor; // Simple implementation
|
|
3362
3375
|
}
|
|
3363
3376
|
});
|
|
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);
|
|
3377
|
+
},
|
|
3378
|
+
dispose: () => {
|
|
3379
|
+
if (lightsGroup.parent)
|
|
3380
|
+
lightsGroup.parent.remove(lightsGroup);
|
|
3381
|
+
lightsGroup.traverse((node) => {
|
|
3382
|
+
if (node.isLight) {
|
|
3383
|
+
const l = node;
|
|
3384
|
+
if (l.shadow && l.shadow.map)
|
|
3385
|
+
l.shadow.map.dispose();
|
|
3402
3386
|
}
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
return handle;
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Automatically setup camera and basic lighting (Combine fitCameraToObject and setupDefaultLights)
|
|
3394
|
+
*/
|
|
3395
|
+
function autoSetupCameraAndLight(camera, scene, model, options = {}) {
|
|
3396
|
+
fitCameraToObject(camera, model, options.padding, options.elevation);
|
|
3397
|
+
return setupDefaultLights(scene, model, options);
|
|
3411
3398
|
}
|
|
3412
3399
|
|
|
3413
3400
|
/**
|
|
@@ -3417,14 +3404,15 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
|
|
|
3417
3404
|
* @packageDocumentation
|
|
3418
3405
|
*/
|
|
3419
3406
|
// Core utilities
|
|
3420
|
-
// Version
|
|
3421
|
-
const VERSION = '1.0.
|
|
3407
|
+
// Version (keep in sync with package.json)
|
|
3408
|
+
const VERSION = '1.0.4';
|
|
3422
3409
|
|
|
3423
3410
|
exports.ArrowGuide = ArrowGuide;
|
|
3424
3411
|
exports.BlueSky = BlueSky;
|
|
3425
3412
|
exports.FOLLOW_ANGLES = FOLLOW_ANGLES;
|
|
3426
3413
|
exports.GroupExploder = GroupExploder;
|
|
3427
3414
|
exports.LiquidFillerGroup = LiquidFillerGroup;
|
|
3415
|
+
exports.ResourceManager = ResourceManager;
|
|
3428
3416
|
exports.VERSION = VERSION;
|
|
3429
3417
|
exports.ViewPresets = ViewPresets;
|
|
3430
3418
|
exports.addChildModelLabels = addChildModelLabels;
|
|
@@ -3436,12 +3424,16 @@ exports.createModelsLabel = createModelsLabel;
|
|
|
3436
3424
|
exports.disposeMaterial = disposeMaterial;
|
|
3437
3425
|
exports.disposeObject = disposeObject;
|
|
3438
3426
|
exports.enableHoverBreath = enableHoverBreath;
|
|
3427
|
+
exports.fitCameraToObject = fitCameraToObject;
|
|
3439
3428
|
exports.followModels = followModels;
|
|
3429
|
+
exports.getLoaderConfig = getLoaderConfig;
|
|
3440
3430
|
exports.initPostProcessing = initPostProcessing;
|
|
3441
3431
|
exports.loadCubeSkybox = loadCubeSkybox;
|
|
3442
3432
|
exports.loadEquirectSkybox = loadEquirectSkybox;
|
|
3443
3433
|
exports.loadModelByUrl = loadModelByUrl;
|
|
3444
3434
|
exports.loadSkybox = loadSkybox;
|
|
3445
3435
|
exports.releaseSkybox = releaseSkybox;
|
|
3436
|
+
exports.setLoaderConfig = setLoaderConfig;
|
|
3446
3437
|
exports.setView = setView;
|
|
3438
|
+
exports.setupDefaultLights = setupDefaultLights;
|
|
3447
3439
|
//# sourceMappingURL=index.js.map
|