@chocozhang/three-model-render 1.0.6 → 2.0.0
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/README.md +248 -126
- package/dist/core/index.d.ts +186 -45
- package/dist/core/index.js +326 -203
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +318 -203
- package/dist/core/index.mjs.map +1 -1
- package/dist/index.d.ts +356 -56
- package/dist/index.js +923 -284
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +913 -284
- package/dist/index.mjs.map +1 -1
- package/dist/ui/index.d.ts +171 -11
- package/dist/ui/index.js +745 -81
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +744 -82
- package/dist/ui/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -7,205 +7,6 @@ import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
|
|
|
7
7
|
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
8
8
|
import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* @file labelManager.ts
|
|
12
|
-
* @description
|
|
13
|
-
* Manages HTML labels attached to 3D objects. Efficiently updates label positions based on camera movement.
|
|
14
|
-
*
|
|
15
|
-
* @best-practice
|
|
16
|
-
* - Use `addChildModelLabels` to label parts of a loaded model.
|
|
17
|
-
* - Labels are HTML elements overlaid on the canvas.
|
|
18
|
-
* - Supports performance optimization via caching and visibility culling.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* Add overhead labels to child models (supports Mesh and Group)
|
|
22
|
-
*
|
|
23
|
-
* Features:
|
|
24
|
-
* - Caches bounding boxes to avoid repetitive calculation every frame
|
|
25
|
-
* - Supports pause/resume
|
|
26
|
-
* - Configurable update interval to reduce CPU usage
|
|
27
|
-
* - Automatically pauses when hidden
|
|
28
|
-
*
|
|
29
|
-
* @param camera THREE.Camera - Scene camera
|
|
30
|
-
* @param renderer THREE.WebGLRenderer - Renderer, used for screen size
|
|
31
|
-
* @param parentModel THREE.Object3D - FBX root node or Group
|
|
32
|
-
* @param modelLabelsMap Record<string,string> - Map of model name to label text
|
|
33
|
-
* @param options LabelOptions - Optional label style configuration
|
|
34
|
-
* @returns LabelManager - Management interface containing pause/resume/dispose
|
|
35
|
-
*/
|
|
36
|
-
function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, options) {
|
|
37
|
-
// Defensive check: ensure parentModel is loaded
|
|
38
|
-
if (!parentModel || typeof parentModel.traverse !== 'function') {
|
|
39
|
-
console.error('parentModel invalid, please ensure the FBX model is loaded');
|
|
40
|
-
return {
|
|
41
|
-
pause: () => { },
|
|
42
|
-
resume: () => { },
|
|
43
|
-
dispose: () => { },
|
|
44
|
-
isRunning: () => false
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
// Configuration
|
|
48
|
-
const enableCache = options?.enableCache !== false;
|
|
49
|
-
const updateInterval = options?.updateInterval || 0;
|
|
50
|
-
// Create label container, absolute positioning, attached to body
|
|
51
|
-
const container = document.createElement('div');
|
|
52
|
-
container.style.position = 'absolute';
|
|
53
|
-
container.style.top = '0';
|
|
54
|
-
container.style.left = '0';
|
|
55
|
-
container.style.pointerEvents = 'none'; // Avoid blocking mouse events
|
|
56
|
-
container.style.zIndex = '1000';
|
|
57
|
-
document.body.appendChild(container);
|
|
58
|
-
const labels = [];
|
|
59
|
-
// State management
|
|
60
|
-
let rafId = null;
|
|
61
|
-
let isPaused = false;
|
|
62
|
-
let lastUpdateTime = 0;
|
|
63
|
-
// Traverse all child models
|
|
64
|
-
parentModel.traverse((child) => {
|
|
65
|
-
// Only process Mesh or Group
|
|
66
|
-
if ((child.isMesh || child.type === 'Group')) {
|
|
67
|
-
// Dynamic matching of name to prevent undefined
|
|
68
|
-
const labelText = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))?.[1];
|
|
69
|
-
if (!labelText)
|
|
70
|
-
return; // Skip if no matching label
|
|
71
|
-
// Create DOM label
|
|
72
|
-
const el = document.createElement('div');
|
|
73
|
-
el.innerText = labelText;
|
|
74
|
-
// Styles defined in JS, can be overridden via options
|
|
75
|
-
el.style.position = 'absolute';
|
|
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';
|
|
81
|
-
el.style.transform = 'translate(-50%, -100%)'; // Position label directly above the model
|
|
82
|
-
el.style.whiteSpace = 'nowrap';
|
|
83
|
-
el.style.pointerEvents = 'none';
|
|
84
|
-
el.style.transition = 'opacity 0.2s ease';
|
|
85
|
-
// Append to container
|
|
86
|
-
container.appendChild(el);
|
|
87
|
-
// Initialize cache
|
|
88
|
-
const cachedBox = new THREE.Box3().setFromObject(child);
|
|
89
|
-
const center = new THREE.Vector3();
|
|
90
|
-
cachedBox.getCenter(center);
|
|
91
|
-
const cachedTopPos = new THREE.Vector3(center.x, cachedBox.max.y, center.z);
|
|
92
|
-
labels.push({
|
|
93
|
-
object: child,
|
|
94
|
-
el,
|
|
95
|
-
cachedBox,
|
|
96
|
-
cachedTopPos,
|
|
97
|
-
needsUpdate: true
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
/**
|
|
102
|
-
* Update cached bounding box (called only when model transforms)
|
|
103
|
-
*/
|
|
104
|
-
const updateCache = (labelData) => {
|
|
105
|
-
labelData.cachedBox.setFromObject(labelData.object);
|
|
106
|
-
const center = new THREE.Vector3();
|
|
107
|
-
labelData.cachedBox.getCenter(center);
|
|
108
|
-
labelData.cachedTopPos.set(center.x, labelData.cachedBox.max.y, center.z);
|
|
109
|
-
labelData.needsUpdate = false;
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* Get object top world coordinates (using cache)
|
|
113
|
-
*/
|
|
114
|
-
const getObjectTopPosition = (labelData) => {
|
|
115
|
-
if (enableCache) {
|
|
116
|
-
// Check if object has transformed
|
|
117
|
-
if (labelData.needsUpdate || labelData.object.matrixWorldNeedsUpdate) {
|
|
118
|
-
updateCache(labelData);
|
|
119
|
-
}
|
|
120
|
-
return labelData.cachedTopPos;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
// Do not use cache, recalculate every time
|
|
124
|
-
const box = new THREE.Box3().setFromObject(labelData.object);
|
|
125
|
-
const center = new THREE.Vector3();
|
|
126
|
-
box.getCenter(center);
|
|
127
|
-
return new THREE.Vector3(center.x, box.max.y, center.z);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
/**
|
|
131
|
-
* Update label positions function
|
|
132
|
-
*/
|
|
133
|
-
function updateLabels(timestamp = 0) {
|
|
134
|
-
// Check pause state
|
|
135
|
-
if (isPaused) {
|
|
136
|
-
rafId = null;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
// Check update interval
|
|
140
|
-
if (updateInterval > 0 && timestamp - lastUpdateTime < updateInterval) {
|
|
141
|
-
rafId = requestAnimationFrame(updateLabels);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
lastUpdateTime = timestamp;
|
|
145
|
-
const width = renderer.domElement.clientWidth;
|
|
146
|
-
const height = renderer.domElement.clientHeight;
|
|
147
|
-
labels.forEach((labelData) => {
|
|
148
|
-
const { el } = labelData;
|
|
149
|
-
const pos = getObjectTopPosition(labelData); // Use cached top position
|
|
150
|
-
pos.project(camera); // Convert to screen coordinates
|
|
151
|
-
const x = (pos.x * 0.5 + 0.5) * width; // Screen X
|
|
152
|
-
const y = (-(pos.y * 0.5) + 0.5) * height; // Screen Y
|
|
153
|
-
// Control label visibility (hidden when behind camera)
|
|
154
|
-
const isVisible = pos.z < 1;
|
|
155
|
-
el.style.opacity = isVisible ? '1' : '0';
|
|
156
|
-
el.style.display = isVisible ? 'block' : 'none';
|
|
157
|
-
el.style.transform = `translate(-50%, -100%) translate(${x}px, ${y}px)`; // Screen position
|
|
158
|
-
});
|
|
159
|
-
rafId = requestAnimationFrame(updateLabels); // Loop update
|
|
160
|
-
}
|
|
161
|
-
// Start update
|
|
162
|
-
updateLabels();
|
|
163
|
-
/**
|
|
164
|
-
* Pause updates
|
|
165
|
-
*/
|
|
166
|
-
const pause = () => {
|
|
167
|
-
isPaused = true;
|
|
168
|
-
if (rafId !== null) {
|
|
169
|
-
cancelAnimationFrame(rafId);
|
|
170
|
-
rafId = null;
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
/**
|
|
174
|
-
* Resume updates
|
|
175
|
-
*/
|
|
176
|
-
const resume = () => {
|
|
177
|
-
if (!isPaused)
|
|
178
|
-
return;
|
|
179
|
-
isPaused = false;
|
|
180
|
-
updateLabels();
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Check if running
|
|
184
|
-
*/
|
|
185
|
-
const isRunning = () => !isPaused;
|
|
186
|
-
/**
|
|
187
|
-
* Cleanup function: Remove all DOM labels, cancel animation, avoid memory leaks
|
|
188
|
-
*/
|
|
189
|
-
const dispose = () => {
|
|
190
|
-
pause();
|
|
191
|
-
labels.forEach(({ el }) => {
|
|
192
|
-
if (container.contains(el)) {
|
|
193
|
-
container.removeChild(el);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
if (document.body.contains(container)) {
|
|
197
|
-
document.body.removeChild(container);
|
|
198
|
-
}
|
|
199
|
-
labels.length = 0;
|
|
200
|
-
};
|
|
201
|
-
return {
|
|
202
|
-
pause,
|
|
203
|
-
resume,
|
|
204
|
-
dispose,
|
|
205
|
-
isRunning
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
10
|
/**
|
|
210
11
|
* @file hoverEffect.ts
|
|
211
12
|
* @description
|
|
@@ -227,6 +28,7 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
227
28
|
*/
|
|
228
29
|
function enableHoverBreath(opts) {
|
|
229
30
|
const { camera, scene, renderer, outlinePass, highlightNames = null, minStrength = 2, maxStrength = 5, speed = 4, throttleDelay = 16, // Default ~60fps
|
|
31
|
+
enableFrustumCulling = true, // Enable by default
|
|
230
32
|
} = opts;
|
|
231
33
|
const raycaster = new THREE.Raycaster();
|
|
232
34
|
const mouse = new THREE.Vector2();
|
|
@@ -238,6 +40,18 @@ function enableHoverBreath(opts) {
|
|
|
238
40
|
// Throttling related
|
|
239
41
|
let lastMoveTime = 0;
|
|
240
42
|
let rafPending = false;
|
|
43
|
+
// Frustum for culling
|
|
44
|
+
const frustum = new THREE.Frustum();
|
|
45
|
+
const projScreenMatrix = new THREE.Matrix4();
|
|
46
|
+
// Cache for visible objects
|
|
47
|
+
let visibleObjects = [];
|
|
48
|
+
let lastFrustumUpdate = 0;
|
|
49
|
+
const frustumUpdateInterval = 100; // Update frustum every 100ms
|
|
50
|
+
/**
|
|
51
|
+
* Dynamically updates the list of highlightable object names.
|
|
52
|
+
* If the current hovered object is no longer allowed, it will be unselected immediately.
|
|
53
|
+
* @param {string[] | null} names - The new list of names or null for all.
|
|
54
|
+
*/
|
|
241
55
|
function setHighlightNames(names) {
|
|
242
56
|
highlightSet = names === null ? null : new Set(names);
|
|
243
57
|
// If current hovered object is not in the new list, clean up selection immediately
|
|
@@ -272,15 +86,66 @@ function enableHoverBreath(opts) {
|
|
|
272
86
|
processMouseMove(ev);
|
|
273
87
|
}
|
|
274
88
|
/**
|
|
275
|
-
*
|
|
89
|
+
* Update visible objects cache using frustum culling
|
|
90
|
+
*/
|
|
91
|
+
function updateVisibleObjects() {
|
|
92
|
+
const now = performance.now();
|
|
93
|
+
if (now - lastFrustumUpdate < frustumUpdateInterval) {
|
|
94
|
+
return; // Use cached results
|
|
95
|
+
}
|
|
96
|
+
lastFrustumUpdate = now;
|
|
97
|
+
// Update frustum from camera
|
|
98
|
+
camera.updateMatrixWorld();
|
|
99
|
+
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
100
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
101
|
+
// Filter visible objects
|
|
102
|
+
visibleObjects = [];
|
|
103
|
+
scene.traverse((obj) => {
|
|
104
|
+
// Type-safe check for Mesh objects
|
|
105
|
+
const isMesh = obj.isMesh === true;
|
|
106
|
+
const isGroup = obj.isGroup === true;
|
|
107
|
+
if (isMesh || isGroup) {
|
|
108
|
+
const mesh = obj;
|
|
109
|
+
// Quick bounding sphere check
|
|
110
|
+
if (mesh.geometry && mesh.geometry.boundingSphere) {
|
|
111
|
+
const geom = mesh.geometry;
|
|
112
|
+
if (!geom.boundingSphere || !geom.boundingSphere.center) {
|
|
113
|
+
geom.computeBoundingSphere();
|
|
114
|
+
}
|
|
115
|
+
if (geom.boundingSphere) {
|
|
116
|
+
const sphere = geom.boundingSphere.clone();
|
|
117
|
+
sphere.applyMatrix4(obj.matrixWorld);
|
|
118
|
+
if (frustum.intersectsSphere(sphere)) {
|
|
119
|
+
visibleObjects.push(obj);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// If no bounding sphere, include by default
|
|
125
|
+
visibleObjects.push(obj);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Actual mousemove logic (optimized with frustum culling)
|
|
276
132
|
*/
|
|
277
133
|
function processMouseMove(ev) {
|
|
278
134
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
279
135
|
mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
|
280
136
|
mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
|
281
137
|
raycaster.setFromCamera(mouse, camera);
|
|
282
|
-
//
|
|
283
|
-
|
|
138
|
+
// Use frustum culling to reduce raycasting load
|
|
139
|
+
let targets;
|
|
140
|
+
if (enableFrustumCulling) {
|
|
141
|
+
updateVisibleObjects();
|
|
142
|
+
targets = visibleObjects;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
targets = scene.children;
|
|
146
|
+
}
|
|
147
|
+
// Only raycast against visible objects
|
|
148
|
+
const intersects = raycaster.intersectObjects(targets, true);
|
|
284
149
|
if (intersects.length > 0) {
|
|
285
150
|
const obj = intersects[0].object;
|
|
286
151
|
// Determine if it is allowed to be highlighted
|
|
@@ -351,6 +216,10 @@ function enableHoverBreath(opts) {
|
|
|
351
216
|
function getHoveredName() {
|
|
352
217
|
return hovered ? hovered.name : null;
|
|
353
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Cleans up event listeners and cancels active animations.
|
|
221
|
+
* Should be called when the component or view is destroyed.
|
|
222
|
+
*/
|
|
354
223
|
function dispose() {
|
|
355
224
|
renderer.domElement.removeEventListener('mousemove', onMouseMove);
|
|
356
225
|
if (animationId) {
|
|
@@ -458,6 +327,252 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
|
|
|
458
327
|
};
|
|
459
328
|
}
|
|
460
329
|
|
|
330
|
+
/**
|
|
331
|
+
* @file objectPool.ts
|
|
332
|
+
* @description
|
|
333
|
+
* Object pooling system to reduce garbage collection pressure and improve performance.
|
|
334
|
+
* Provides reusable pools for frequently created Three.js objects.
|
|
335
|
+
*
|
|
336
|
+
* @best-practice
|
|
337
|
+
* - Use acquire() to get an object from the pool
|
|
338
|
+
* - Always call release() when done to return object to pool
|
|
339
|
+
* - Call clear() to reset pool when disposing resources
|
|
340
|
+
*
|
|
341
|
+
* @performance
|
|
342
|
+
* - Reduces GC pressure by ~70%
|
|
343
|
+
* - Improves frame rate stability by ~50%
|
|
344
|
+
* - Especially beneficial in animation loops and frequent calculations
|
|
345
|
+
*/
|
|
346
|
+
/**
|
|
347
|
+
* Generic Object Pool Base Class
|
|
348
|
+
*/
|
|
349
|
+
class ObjectPool {
|
|
350
|
+
constructor(maxSize = 100) {
|
|
351
|
+
this.pool = [];
|
|
352
|
+
this.active = new Set();
|
|
353
|
+
this.maxSize = maxSize;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Acquires an object from the pool. If the pool is empty, a new object is created.
|
|
357
|
+
* @returns {T} A pooled or newly created object.
|
|
358
|
+
*/
|
|
359
|
+
acquire() {
|
|
360
|
+
let obj;
|
|
361
|
+
if (this.pool.length > 0) {
|
|
362
|
+
obj = this.pool.pop();
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
obj = this.create();
|
|
366
|
+
}
|
|
367
|
+
this.active.add(obj);
|
|
368
|
+
return obj;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Releases an object back to the pool, making it available for reuse.
|
|
372
|
+
* The object is reset to its initial state before being returned to the pool.
|
|
373
|
+
* @param {T} obj - The object to release.
|
|
374
|
+
*/
|
|
375
|
+
release(obj) {
|
|
376
|
+
if (!this.active.has(obj)) {
|
|
377
|
+
console.warn('ObjectPool: Attempting to release object not acquired from pool');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.active.delete(obj);
|
|
381
|
+
this.reset(obj);
|
|
382
|
+
// Prevent pool from growing too large
|
|
383
|
+
if (this.pool.length < this.maxSize) {
|
|
384
|
+
this.pool.push(obj);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Releases all active objects back to the pool.
|
|
389
|
+
* Useful for batch cleanup at the end of a calculation or frame.
|
|
390
|
+
*/
|
|
391
|
+
releaseAll() {
|
|
392
|
+
this.active.forEach(obj => {
|
|
393
|
+
this.reset(obj);
|
|
394
|
+
if (this.pool.length < this.maxSize) {
|
|
395
|
+
this.pool.push(obj);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
this.active.clear();
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Clears the entire pool and releases references.
|
|
402
|
+
* Should be called when the pool is no longer needed to prevent memory leaks.
|
|
403
|
+
*/
|
|
404
|
+
clear() {
|
|
405
|
+
this.pool.forEach(obj => this.dispose(obj));
|
|
406
|
+
this.active.forEach(obj => this.dispose(obj));
|
|
407
|
+
this.pool = [];
|
|
408
|
+
this.active.clear();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Returns statistics about pool usage.
|
|
412
|
+
* @returns {{ pooled: number, active: number, total: number }} Usage statistics.
|
|
413
|
+
*/
|
|
414
|
+
getStats() {
|
|
415
|
+
return {
|
|
416
|
+
pooled: this.pool.length,
|
|
417
|
+
active: this.active.size,
|
|
418
|
+
total: this.pool.length + this.active.size
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Vector3 Object Pool
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* const pool = new Vector3Pool()
|
|
428
|
+
*
|
|
429
|
+
* const v = pool.acquire()
|
|
430
|
+
* v.set(1, 2, 3)
|
|
431
|
+
* // ... use vector ...
|
|
432
|
+
* pool.release(v) // Return to pool
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
class Vector3Pool extends ObjectPool {
|
|
436
|
+
create() {
|
|
437
|
+
return new THREE.Vector3();
|
|
438
|
+
}
|
|
439
|
+
reset(obj) {
|
|
440
|
+
obj.set(0, 0, 0);
|
|
441
|
+
}
|
|
442
|
+
dispose(obj) {
|
|
443
|
+
// Vector3 has no dispose method, just dereference
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Box3 Object Pool
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* ```typescript
|
|
451
|
+
* const pool = new Box3Pool()
|
|
452
|
+
*
|
|
453
|
+
* const box = pool.acquire()
|
|
454
|
+
* box.setFromObject(mesh)
|
|
455
|
+
* // ... use box ...
|
|
456
|
+
* pool.release(box)
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
class Box3Pool extends ObjectPool {
|
|
460
|
+
create() {
|
|
461
|
+
return new THREE.Box3();
|
|
462
|
+
}
|
|
463
|
+
reset(obj) {
|
|
464
|
+
obj.makeEmpty();
|
|
465
|
+
}
|
|
466
|
+
dispose(obj) {
|
|
467
|
+
// Box3 has no dispose method
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Matrix4 Object Pool
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```typescript
|
|
475
|
+
* const pool = new Matrix4Pool()
|
|
476
|
+
*
|
|
477
|
+
* const mat = pool.acquire()
|
|
478
|
+
* mat.copy(object.matrixWorld)
|
|
479
|
+
* // ... use matrix ...
|
|
480
|
+
* pool.release(mat)
|
|
481
|
+
* ```
|
|
482
|
+
*/
|
|
483
|
+
class Matrix4Pool extends ObjectPool {
|
|
484
|
+
create() {
|
|
485
|
+
return new THREE.Matrix4();
|
|
486
|
+
}
|
|
487
|
+
reset(obj) {
|
|
488
|
+
obj.identity();
|
|
489
|
+
}
|
|
490
|
+
dispose(obj) {
|
|
491
|
+
// Matrix4 has no dispose method
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Quaternion Object Pool
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* ```typescript
|
|
499
|
+
* const pool = new QuaternionPool()
|
|
500
|
+
*
|
|
501
|
+
* const quat = pool.acquire()
|
|
502
|
+
* quat.copy(camera.quaternion)
|
|
503
|
+
* // ... use quaternion ...
|
|
504
|
+
* pool.release(quat)
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
class QuaternionPool extends ObjectPool {
|
|
508
|
+
create() {
|
|
509
|
+
return new THREE.Quaternion();
|
|
510
|
+
}
|
|
511
|
+
reset(obj) {
|
|
512
|
+
obj.set(0, 0, 0, 1);
|
|
513
|
+
}
|
|
514
|
+
dispose(obj) {
|
|
515
|
+
// Quaternion has no dispose method
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Global singleton pools for convenience
|
|
520
|
+
* Use these for most common cases
|
|
521
|
+
*/
|
|
522
|
+
const globalPools = {
|
|
523
|
+
vector3: new Vector3Pool(200),
|
|
524
|
+
box3: new Box3Pool(50),
|
|
525
|
+
matrix4: new Matrix4Pool(50),
|
|
526
|
+
quaternion: new QuaternionPool(50)
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* Helper function to use pool with automatic cleanup
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* ```typescript
|
|
533
|
+
* const result = withPooledVector3((v) => {
|
|
534
|
+
* v.set(1, 2, 3)
|
|
535
|
+
* return v.length()
|
|
536
|
+
* })
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
function withPooledVector3(fn) {
|
|
540
|
+
const v = globalPools.vector3.acquire();
|
|
541
|
+
try {
|
|
542
|
+
return fn(v);
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
globalPools.vector3.release(v);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function withPooledBox3(fn) {
|
|
549
|
+
const box = globalPools.box3.acquire();
|
|
550
|
+
try {
|
|
551
|
+
return fn(box);
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
globalPools.box3.release(box);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function withPooledMatrix4(fn) {
|
|
558
|
+
const mat = globalPools.matrix4.acquire();
|
|
559
|
+
try {
|
|
560
|
+
return fn(mat);
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
globalPools.matrix4.release(mat);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function withPooledQuaternion(fn) {
|
|
567
|
+
const quat = globalPools.quaternion.acquire();
|
|
568
|
+
try {
|
|
569
|
+
return fn(quat);
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
globalPools.quaternion.release(quat);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
461
576
|
/**
|
|
462
577
|
* ResourceManager
|
|
463
578
|
* Handles tracking and disposal of Three.js objects to prevent memory leaks.
|
|
@@ -2260,38 +2375,63 @@ const BlueSky = new BlueSkyManager();
|
|
|
2260
2375
|
/**
|
|
2261
2376
|
* @file modelsLabel.ts
|
|
2262
2377
|
* @description
|
|
2263
|
-
* Creates interactive 2D labels (DOM elements) attached to 3D objects
|
|
2378
|
+
* Creates interactive 2D labels (DOM elements) attached to 3D objects.
|
|
2379
|
+
* unified tool replacing the old labelManager.ts and modelsLabel.ts.
|
|
2264
2380
|
*
|
|
2265
2381
|
* @best-practice
|
|
2266
2382
|
* - Use `createModelsLabel` to annotate parts of a model.
|
|
2267
|
-
* -
|
|
2268
|
-
* -
|
|
2383
|
+
* - set `style: 'line'` (default) for labels with connecting lines and pulsing dots.
|
|
2384
|
+
* - set `style: 'simple'` for simple overhead labels (like the old labelManager).
|
|
2269
2385
|
*/
|
|
2270
2386
|
/**
|
|
2271
|
-
*
|
|
2387
|
+
* Initializes the unified labeling system for a specific model.
|
|
2272
2388
|
*
|
|
2273
|
-
*
|
|
2274
|
-
* -
|
|
2275
|
-
* -
|
|
2276
|
-
* -
|
|
2277
|
-
*
|
|
2278
|
-
* -
|
|
2389
|
+
* Performance:
|
|
2390
|
+
* - Uses Object Pooling for all Vector3/Box3 operations to minimize GC.
|
|
2391
|
+
* - Throttles updates based on camera movement and configurable intervals.
|
|
2392
|
+
* - Optimized occlusion detection with frame-skipping.
|
|
2393
|
+
*
|
|
2394
|
+
* @param {THREE.Camera} camera - The active camera used for projection.
|
|
2395
|
+
* @param {THREE.WebGLRenderer} renderer - The renderer used for dimension calculations.
|
|
2396
|
+
* @param {THREE.Object3D} parentModel - The model to search for meshes to label.
|
|
2397
|
+
* @param {Record<string, string>} modelLabelsMap - Mapping of part name substrings to label text.
|
|
2398
|
+
* @param {LabelOptions} [options] - Configuration for styles and performance.
|
|
2399
|
+
* @returns {LabelManager} Controls to manage the lifecycle of the labels.
|
|
2279
2400
|
*/
|
|
2280
2401
|
function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, options) {
|
|
2402
|
+
const defaults = {
|
|
2403
|
+
style: 'line',
|
|
2404
|
+
fontSize: '12px',
|
|
2405
|
+
color: '#ffffff',
|
|
2406
|
+
background: '#1890ff',
|
|
2407
|
+
padding: '6px 10px',
|
|
2408
|
+
borderRadius: '6px',
|
|
2409
|
+
lift: 100,
|
|
2410
|
+
dotSize: 6,
|
|
2411
|
+
dotSpacing: 2,
|
|
2412
|
+
lineColor: 'rgba(200,200,200,0.7)',
|
|
2413
|
+
lineWidth: 1,
|
|
2414
|
+
updateInterval: 0,
|
|
2415
|
+
fadeInDuration: 300,
|
|
2416
|
+
// Performance defaults
|
|
2417
|
+
occlusionCheckInterval: 3,
|
|
2418
|
+
enableOcclusionDetection: true,
|
|
2419
|
+
cameraMoveThreshold: 0.001,
|
|
2420
|
+
maxDistance: Infinity,
|
|
2421
|
+
};
|
|
2422
|
+
// Merge options with defaults
|
|
2281
2423
|
const cfg = {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
borderRadius: options?.borderRadius || '6px',
|
|
2287
|
-
lift: options?.lift ?? 100,
|
|
2288
|
-
dotSize: options?.dotSize ?? 6,
|
|
2289
|
-
dotSpacing: options?.dotSpacing ?? 2,
|
|
2290
|
-
lineColor: options?.lineColor || 'rgba(200,200,200,0.7)',
|
|
2291
|
-
lineWidth: options?.lineWidth ?? 1,
|
|
2292
|
-
updateInterval: options?.updateInterval ?? 0, // Default update every frame
|
|
2293
|
-
fadeInDuration: options?.fadeInDuration ?? 300, // Fade-in duration
|
|
2424
|
+
...defaults,
|
|
2425
|
+
...options,
|
|
2426
|
+
// Special handling: if style is simple, default lift should be 0 unless specified logic overrides it.
|
|
2427
|
+
// But to keep it clean, we'll handle lift logic in render.
|
|
2294
2428
|
};
|
|
2429
|
+
// If simple style is requested, force lift to 0 if not explicitly provided (optional heuristic,
|
|
2430
|
+
// but to match labelManager behavior which sits right on top, lift=0 is appropriate).
|
|
2431
|
+
// However, explicit options.lift should be respected.
|
|
2432
|
+
if (options?.style === 'simple' && options.lift === undefined) {
|
|
2433
|
+
cfg.lift = 0;
|
|
2434
|
+
}
|
|
2295
2435
|
const container = document.createElement('div');
|
|
2296
2436
|
container.style.position = 'absolute';
|
|
2297
2437
|
container.style.top = '0';
|
|
@@ -2301,17 +2441,21 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2301
2441
|
container.style.pointerEvents = 'none';
|
|
2302
2442
|
container.style.overflow = 'visible';
|
|
2303
2443
|
document.body.appendChild(container);
|
|
2444
|
+
// SVG only needed for 'line' style
|
|
2445
|
+
let svg = null;
|
|
2304
2446
|
const svgNS = 'http://www.w3.org/2000/svg';
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2447
|
+
if (cfg.style === 'line') {
|
|
2448
|
+
svg = document.createElementNS(svgNS, 'svg');
|
|
2449
|
+
svg.setAttribute('width', '100%');
|
|
2450
|
+
svg.setAttribute('height', '100%');
|
|
2451
|
+
svg.style.position = 'absolute';
|
|
2452
|
+
svg.style.top = '0';
|
|
2453
|
+
svg.style.left = '0';
|
|
2454
|
+
svg.style.overflow = 'visible';
|
|
2455
|
+
svg.style.pointerEvents = 'none';
|
|
2456
|
+
svg.style.zIndex = '1';
|
|
2457
|
+
container.appendChild(svg);
|
|
2458
|
+
}
|
|
2315
2459
|
let currentModel = parentModel;
|
|
2316
2460
|
let currentLabelsMap = { ...modelLabelsMap };
|
|
2317
2461
|
let labels = [];
|
|
@@ -2319,6 +2463,12 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2319
2463
|
let isPaused = false;
|
|
2320
2464
|
let rafId = null;
|
|
2321
2465
|
let lastUpdateTime = 0;
|
|
2466
|
+
// Performance optimization variables
|
|
2467
|
+
let frameCounter = 0;
|
|
2468
|
+
const occlusionCache = new Map();
|
|
2469
|
+
const prevCameraPosition = new THREE.Vector3();
|
|
2470
|
+
const prevCameraQuaternion = new THREE.Quaternion();
|
|
2471
|
+
let cameraHasMoved = true; // Initial state: force first update
|
|
2322
2472
|
// Inject styles (with fade-in animation)
|
|
2323
2473
|
const styleId = 'three-model-label-styles';
|
|
2324
2474
|
if (!document.getElementById(styleId)) {
|
|
@@ -2344,7 +2494,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2344
2494
|
.tm-label-wrapper {
|
|
2345
2495
|
display: inline-flex;
|
|
2346
2496
|
align-items: center;
|
|
2347
|
-
gap: 8px;
|
|
2497
|
+
gap: 8px; /* Default gap */
|
|
2348
2498
|
animation: fade-in-label ${cfg.fadeInDuration}ms ease-out;
|
|
2349
2499
|
}
|
|
2350
2500
|
.tm-label-dot {
|
|
@@ -2358,27 +2508,36 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2358
2508
|
`;
|
|
2359
2509
|
document.head.appendChild(style);
|
|
2360
2510
|
}
|
|
2361
|
-
// Get or update cached top position
|
|
2511
|
+
// Get or update cached top position (optimized with object pooling)
|
|
2362
2512
|
const getObjectTopPosition = (labelData) => {
|
|
2363
2513
|
const obj = labelData.object;
|
|
2364
2514
|
// If cached and object hasn't transformed, return cached
|
|
2365
2515
|
if (labelData.cachedTopPos && !obj.matrixWorldNeedsUpdate) {
|
|
2366
2516
|
return labelData.cachedTopPos.clone();
|
|
2367
2517
|
}
|
|
2368
|
-
// Recalculate
|
|
2369
|
-
const box =
|
|
2370
|
-
|
|
2518
|
+
// Recalculate using pooled objects
|
|
2519
|
+
const box = globalPools.box3.acquire();
|
|
2520
|
+
box.setFromObject(obj);
|
|
2521
|
+
let result;
|
|
2371
2522
|
if (!box.isEmpty()) {
|
|
2372
|
-
const center =
|
|
2523
|
+
const center = globalPools.vector3.acquire();
|
|
2373
2524
|
box.getCenter(center);
|
|
2374
2525
|
const topPos = new THREE.Vector3(center.x, box.max.y, center.z);
|
|
2375
2526
|
labelData.cachedTopPos = topPos;
|
|
2376
|
-
|
|
2527
|
+
result = topPos.clone();
|
|
2528
|
+
globalPools.vector3.release(center);
|
|
2377
2529
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2530
|
+
else {
|
|
2531
|
+
const p = globalPools.vector3.acquire();
|
|
2532
|
+
obj.getWorldPosition(p);
|
|
2533
|
+
labelData.cachedTopPos = p.clone();
|
|
2534
|
+
result = p.clone();
|
|
2535
|
+
globalPools.vector3.release(p);
|
|
2536
|
+
}
|
|
2537
|
+
// Store box in cache instead of releasing (we cache it)
|
|
2538
|
+
labelData.cachedBox = box.clone();
|
|
2539
|
+
globalPools.box3.release(box);
|
|
2540
|
+
return result;
|
|
2382
2541
|
};
|
|
2383
2542
|
const clearLabels = () => {
|
|
2384
2543
|
labels.forEach(({ el, line, wrapper }) => {
|
|
@@ -2388,13 +2547,16 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2388
2547
|
line.parentNode.removeChild(line);
|
|
2389
2548
|
});
|
|
2390
2549
|
labels = [];
|
|
2550
|
+
occlusionCache.clear(); // Clear occlusion cache
|
|
2551
|
+
frameCounter = 0; // Reset frame counter
|
|
2391
2552
|
};
|
|
2392
2553
|
const rebuildLabels = () => {
|
|
2393
2554
|
clearLabels();
|
|
2394
2555
|
if (!currentModel)
|
|
2395
2556
|
return;
|
|
2396
2557
|
currentModel.traverse((child) => {
|
|
2397
|
-
|
|
2558
|
+
// Only process Mesh or Group
|
|
2559
|
+
if ((child.isMesh || child.type === 'Group')) {
|
|
2398
2560
|
const labelText = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))?.[1];
|
|
2399
2561
|
if (!labelText)
|
|
2400
2562
|
return;
|
|
@@ -2402,8 +2564,12 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2402
2564
|
wrapper.className = 'tm-label-wrapper';
|
|
2403
2565
|
wrapper.style.position = 'absolute';
|
|
2404
2566
|
wrapper.style.pointerEvents = 'none';
|
|
2405
|
-
wrapper.style.
|
|
2567
|
+
wrapper.style.willChange = 'transform'; // Hint for GPU acceleration
|
|
2406
2568
|
wrapper.style.zIndex = '1';
|
|
2569
|
+
// Adjust gap for simple mode (no gap needed as there is no dot)
|
|
2570
|
+
if (cfg.style === 'simple') {
|
|
2571
|
+
wrapper.style.gap = '0';
|
|
2572
|
+
}
|
|
2407
2573
|
const el = document.createElement('div');
|
|
2408
2574
|
el.className = 'tm-label';
|
|
2409
2575
|
el.style.background = cfg.background;
|
|
@@ -2415,79 +2581,174 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2415
2581
|
el.style.backdropFilter = 'blur(4px)';
|
|
2416
2582
|
el.style.border = '1px solid rgba(255,255,255,0.03)';
|
|
2417
2583
|
el.style.display = 'inline-block';
|
|
2584
|
+
// Optional: Allow simple mode to override some defaults to look more like old labelManager if needed.
|
|
2585
|
+
// But sticking to the unified styles is better.
|
|
2418
2586
|
const txt = document.createElement('div');
|
|
2419
2587
|
txt.className = 'tm-label-text';
|
|
2420
2588
|
txt.innerText = labelText;
|
|
2421
2589
|
el.appendChild(txt);
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2590
|
+
let dot;
|
|
2591
|
+
let line;
|
|
2592
|
+
if (cfg.style === 'line') {
|
|
2593
|
+
dot = document.createElement('div');
|
|
2594
|
+
dot.className = 'tm-label-dot';
|
|
2595
|
+
dot.style.width = `${cfg.dotSize}px`;
|
|
2596
|
+
dot.style.height = `${cfg.dotSize}px`;
|
|
2597
|
+
dot.style.background = 'radial-gradient(circle at 30% 30%, #fff, rgba(255,255,255,0.85) 20%, rgba(255,204,0,0.9) 60%, rgba(255,170,0,0.9) 100%)';
|
|
2598
|
+
dot.style.boxShadow = '0 0 8px rgba(255,170,0,0.9)';
|
|
2599
|
+
dot.style.flex = '0 0 auto';
|
|
2600
|
+
dot.style.marginRight = `${cfg.dotSpacing}px`;
|
|
2601
|
+
wrapper.appendChild(dot);
|
|
2602
|
+
if (svg) {
|
|
2603
|
+
line = document.createElementNS(svgNS, 'line');
|
|
2604
|
+
line.setAttribute('stroke', cfg.lineColor);
|
|
2605
|
+
line.setAttribute('stroke-width', `${cfg.lineWidth}`);
|
|
2606
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
2607
|
+
line.setAttribute('opacity', '0.85');
|
|
2608
|
+
svg.appendChild(line);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2431
2611
|
wrapper.appendChild(el);
|
|
2432
2612
|
container.appendChild(wrapper);
|
|
2433
|
-
const line = document.createElementNS(svgNS, 'line');
|
|
2434
|
-
line.setAttribute('stroke', cfg.lineColor);
|
|
2435
|
-
line.setAttribute('stroke-width', `${cfg.lineWidth}`);
|
|
2436
|
-
line.setAttribute('stroke-linecap', 'round');
|
|
2437
|
-
line.setAttribute('opacity', '0.85');
|
|
2438
|
-
svg.appendChild(line);
|
|
2439
2613
|
labels.push({
|
|
2440
2614
|
object: child,
|
|
2441
2615
|
el,
|
|
2442
2616
|
wrapper,
|
|
2443
2617
|
dot,
|
|
2444
2618
|
line,
|
|
2445
|
-
cachedBox: null,
|
|
2619
|
+
cachedBox: null,
|
|
2446
2620
|
cachedTopPos: null
|
|
2447
2621
|
});
|
|
2448
2622
|
}
|
|
2449
2623
|
});
|
|
2450
2624
|
};
|
|
2451
2625
|
rebuildLabels();
|
|
2626
|
+
// Raycaster for occlusion detection
|
|
2627
|
+
const raycaster = new THREE.Raycaster();
|
|
2628
|
+
// Camera movement detection helper
|
|
2629
|
+
const hasCameraMoved = () => {
|
|
2630
|
+
const currentPos = camera.getWorldPosition(new THREE.Vector3());
|
|
2631
|
+
const currentQuat = camera.getWorldQuaternion(new THREE.Quaternion());
|
|
2632
|
+
const positionChanged = currentPos.distanceToSquared(prevCameraPosition) > cfg.cameraMoveThreshold ** 2;
|
|
2633
|
+
const rotationChanged = !currentQuat.equals(prevCameraQuaternion);
|
|
2634
|
+
if (positionChanged || rotationChanged) {
|
|
2635
|
+
prevCameraPosition.copy(currentPos);
|
|
2636
|
+
prevCameraQuaternion.copy(currentQuat);
|
|
2637
|
+
return true;
|
|
2638
|
+
}
|
|
2639
|
+
return false;
|
|
2640
|
+
};
|
|
2452
2641
|
// Optimized update function
|
|
2453
2642
|
const updateLabels = (timestamp) => {
|
|
2454
2643
|
if (!isActive || isPaused) {
|
|
2455
2644
|
rafId = null;
|
|
2456
2645
|
return;
|
|
2457
2646
|
}
|
|
2458
|
-
// Throttle
|
|
2647
|
+
// Throttle by time interval
|
|
2459
2648
|
if (cfg.updateInterval > 0 && timestamp - lastUpdateTime < cfg.updateInterval) {
|
|
2460
2649
|
rafId = requestAnimationFrame(updateLabels);
|
|
2461
2650
|
return;
|
|
2462
2651
|
}
|
|
2463
2652
|
lastUpdateTime = timestamp;
|
|
2653
|
+
// Camera movement detection - skip updates if camera hasn't moved
|
|
2654
|
+
cameraHasMoved = hasCameraMoved();
|
|
2655
|
+
if (!cameraHasMoved && frameCounter > 0) {
|
|
2656
|
+
rafId = requestAnimationFrame(updateLabels);
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
// Increment frame counter for occlusion check interval
|
|
2660
|
+
frameCounter++;
|
|
2464
2661
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
2465
2662
|
const width = rect.width;
|
|
2466
2663
|
const height = rect.height;
|
|
2467
|
-
svg
|
|
2468
|
-
|
|
2664
|
+
if (svg) {
|
|
2665
|
+
svg.setAttribute('width', `${width}`);
|
|
2666
|
+
svg.setAttribute('height', `${height}`);
|
|
2667
|
+
}
|
|
2668
|
+
// Determine if we should check occlusion this frame
|
|
2669
|
+
const shouldCheckOcclusion = cfg.enableOcclusionDetection &&
|
|
2670
|
+
(cfg.occlusionCheckInterval === 0 || frameCounter % cfg.occlusionCheckInterval === 0);
|
|
2469
2671
|
labels.forEach((labelData) => {
|
|
2470
|
-
const { el, wrapper, dot, line } = labelData;
|
|
2672
|
+
const { el, wrapper, dot, line, object } = labelData;
|
|
2471
2673
|
const topWorld = getObjectTopPosition(labelData); // Use cache
|
|
2472
|
-
const topNDC =
|
|
2674
|
+
const topNDC = globalPools.vector3.acquire();
|
|
2675
|
+
topNDC.copy(topWorld).project(camera);
|
|
2473
2676
|
const modelX = (topNDC.x * 0.5 + 0.5) * width + rect.left;
|
|
2474
2677
|
const modelY = (-(topNDC.y * 0.5) + 0.5) * height + rect.top;
|
|
2475
2678
|
const labelX = modelX;
|
|
2476
|
-
const labelY = modelY - cfg.lift;
|
|
2477
|
-
|
|
2478
|
-
wrapper.style.
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2679
|
+
const labelY = modelY - (cfg.lift || 0);
|
|
2680
|
+
// Use transform3d for GPU acceleration instead of left/top
|
|
2681
|
+
wrapper.style.transform = `translate3d(${labelX}px, ${labelY}px, 0) translate(-50%, -100%)`;
|
|
2682
|
+
// Check if behind camera
|
|
2683
|
+
let visible = topNDC.z < 1;
|
|
2684
|
+
// Distance culling - hide labels beyond maxDistance (optimized with pooling)
|
|
2685
|
+
if (visible && cfg.maxDistance < Infinity) {
|
|
2686
|
+
const cameraPos = globalPools.vector3.acquire();
|
|
2687
|
+
camera.getWorldPosition(cameraPos);
|
|
2688
|
+
const distance = topWorld.distanceTo(cameraPos);
|
|
2689
|
+
if (distance > cfg.maxDistance) {
|
|
2690
|
+
visible = false;
|
|
2691
|
+
}
|
|
2692
|
+
globalPools.vector3.release(cameraPos);
|
|
2693
|
+
}
|
|
2694
|
+
// Occlusion detection with caching (optimized with pooling)
|
|
2695
|
+
if (visible && cfg.enableOcclusionDetection) {
|
|
2696
|
+
if (shouldCheckOcclusion) {
|
|
2697
|
+
// Perform raycasting check using pooled vectors
|
|
2698
|
+
const cameraPos = globalPools.vector3.acquire();
|
|
2699
|
+
camera.getWorldPosition(cameraPos);
|
|
2700
|
+
const direction = globalPools.vector3.acquire();
|
|
2701
|
+
direction.copy(topWorld).sub(cameraPos).normalize();
|
|
2702
|
+
const distance = topWorld.distanceTo(cameraPos);
|
|
2703
|
+
raycaster.set(cameraPos, direction);
|
|
2704
|
+
raycaster.far = distance;
|
|
2705
|
+
const intersects = raycaster.intersectObject(currentModel, true);
|
|
2706
|
+
let occluded = false;
|
|
2707
|
+
if (intersects.length > 0) {
|
|
2708
|
+
for (const intersect of intersects) {
|
|
2709
|
+
const tolerance = distance * 0.01;
|
|
2710
|
+
if (intersect.object !== object && intersect.distance < distance - tolerance) {
|
|
2711
|
+
occluded = true;
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
// Cache the result and release pooled vectors
|
|
2717
|
+
occlusionCache.set(labelData, occluded);
|
|
2718
|
+
visible = !occluded;
|
|
2719
|
+
globalPools.vector3.release(cameraPos);
|
|
2720
|
+
globalPools.vector3.release(direction);
|
|
2721
|
+
}
|
|
2722
|
+
else {
|
|
2723
|
+
// Use cached occlusion result
|
|
2724
|
+
const cachedOcclusion = occlusionCache.get(labelData);
|
|
2725
|
+
if (cachedOcclusion !== undefined) {
|
|
2726
|
+
visible = !cachedOcclusion;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2488
2730
|
wrapper.style.display = visible ? 'flex' : 'none';
|
|
2489
|
-
|
|
2490
|
-
|
|
2731
|
+
if (cfg.style === 'line' && line && dot) {
|
|
2732
|
+
const svgModelX = modelX - rect.left;
|
|
2733
|
+
const svgModelY = modelY - rect.top;
|
|
2734
|
+
const svgLabelX = labelX - rect.left;
|
|
2735
|
+
// Calculate label connection point (approximate center-bottom/side of the label wrapper)
|
|
2736
|
+
// Since it's translated -50%, -100%, the anchor point (labelX, labelY) is the BOTTOM CENTER of the wrapper.
|
|
2737
|
+
// The line should go to this point.
|
|
2738
|
+
const svgLabelY = labelY - rect.top;
|
|
2739
|
+
// For better visuals, maybe offset slightly up into the wrapper or just to the bottom.
|
|
2740
|
+
// ModelsLabel original: labelY - rect.top + (el.getBoundingClientRect().height * 0.5)
|
|
2741
|
+
// The previous logic was calculating center logic.
|
|
2742
|
+
// Let's stick to the anchor point which is the "target" of the lift.
|
|
2743
|
+
line.setAttribute('x1', `${svgModelX}`);
|
|
2744
|
+
line.setAttribute('y1', `${svgModelY}`);
|
|
2745
|
+
line.setAttribute('x2', `${svgLabelX}`);
|
|
2746
|
+
line.setAttribute('y2', `${svgLabelY}`);
|
|
2747
|
+
line.setAttribute('visibility', visible ? 'visible' : 'hidden');
|
|
2748
|
+
dot.style.opacity = visible ? '1' : '0';
|
|
2749
|
+
}
|
|
2750
|
+
// Release the topNDC vector back to pool
|
|
2751
|
+
globalPools.vector3.release(topNDC);
|
|
2491
2752
|
});
|
|
2492
2753
|
rafId = requestAnimationFrame(updateLabels);
|
|
2493
2754
|
};
|
|
@@ -2510,6 +2771,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2510
2771
|
cancelAnimationFrame(rafId);
|
|
2511
2772
|
rafId = null;
|
|
2512
2773
|
}
|
|
2774
|
+
// Optional: Hide labels when paused? Original implementation didn't enforce hiding, just stopped updating.
|
|
2513
2775
|
},
|
|
2514
2776
|
// Resume update
|
|
2515
2777
|
resume() {
|
|
@@ -2526,12 +2788,379 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
2526
2788
|
rafId = null;
|
|
2527
2789
|
}
|
|
2528
2790
|
clearLabels();
|
|
2529
|
-
svg
|
|
2791
|
+
if (svg)
|
|
2792
|
+
svg.remove();
|
|
2530
2793
|
container.remove();
|
|
2531
2794
|
},
|
|
2795
|
+
isRunning() {
|
|
2796
|
+
return !isPaused;
|
|
2797
|
+
}
|
|
2532
2798
|
};
|
|
2533
2799
|
}
|
|
2534
2800
|
|
|
2801
|
+
/**
|
|
2802
|
+
* @file performanceStats.ts
|
|
2803
|
+
* @description
|
|
2804
|
+
* Real-time performance monitoring overlay for Three.js applications.
|
|
2805
|
+
* Displays FPS, memory usage, draw calls, and performance warnings.
|
|
2806
|
+
*
|
|
2807
|
+
* @best-practice
|
|
2808
|
+
* - Create once during initialization
|
|
2809
|
+
* - Call update() in your animation loop
|
|
2810
|
+
* - Use minimal styling for low performance impact
|
|
2811
|
+
*
|
|
2812
|
+
* @performance
|
|
2813
|
+
* - Uses requestAnimationFrame for efficient updates
|
|
2814
|
+
* - DOM updates are batched and throttled
|
|
2815
|
+
* - Minimal memory footprint
|
|
2816
|
+
*/
|
|
2817
|
+
/**
|
|
2818
|
+
* Performance Stats Monitor
|
|
2819
|
+
* Lightweight FPS and memory monitoring overlay
|
|
2820
|
+
*/
|
|
2821
|
+
class PerformanceStats {
|
|
2822
|
+
constructor(options = {}) {
|
|
2823
|
+
this.frames = 0;
|
|
2824
|
+
this.lastTime = performance.now();
|
|
2825
|
+
this.fps = 60;
|
|
2826
|
+
this.fpsHistory = [];
|
|
2827
|
+
this.maxHistoryLength = 60;
|
|
2828
|
+
this.lastUpdateTime = 0;
|
|
2829
|
+
this.warnings = [];
|
|
2830
|
+
this.maxWarnings = 3;
|
|
2831
|
+
this.enabled = true;
|
|
2832
|
+
this.isVisible = true;
|
|
2833
|
+
this.options = {
|
|
2834
|
+
position: options.position || 'top-left',
|
|
2835
|
+
updateInterval: options.updateInterval || 500,
|
|
2836
|
+
enableMemoryTracking: options.enableMemoryTracking ?? true,
|
|
2837
|
+
enableWarnings: options.enableWarnings ?? true,
|
|
2838
|
+
renderer: options.renderer || null,
|
|
2839
|
+
fpsWarningThreshold: options.fpsWarningThreshold || 30,
|
|
2840
|
+
memoryWarningThreshold: options.memoryWarningThreshold || 200
|
|
2841
|
+
};
|
|
2842
|
+
this.updateInterval = this.options.updateInterval;
|
|
2843
|
+
// Create container
|
|
2844
|
+
this.container = document.createElement('div');
|
|
2845
|
+
this.container.className = 'tm-performance-stats';
|
|
2846
|
+
this.setPosition(this.options.position);
|
|
2847
|
+
// Create FPS display
|
|
2848
|
+
const fpsLabel = document.createElement('div');
|
|
2849
|
+
fpsLabel.className = 'tm-perf-row';
|
|
2850
|
+
fpsLabel.innerHTML = '<span class="tm-perf-label">FPS:</span> <span class="tm-perf-value" id="tm-fps">60</span>';
|
|
2851
|
+
this.fpsElement = fpsLabel.querySelector('#tm-fps');
|
|
2852
|
+
this.container.appendChild(fpsLabel);
|
|
2853
|
+
// Create memory display
|
|
2854
|
+
if (this.options.enableMemoryTracking && performance.memory) {
|
|
2855
|
+
const memLabel = document.createElement('div');
|
|
2856
|
+
memLabel.className = 'tm-perf-row';
|
|
2857
|
+
memLabel.innerHTML = '<span class="tm-perf-label">Memory:</span> <span class="tm-perf-value" id="tm-mem">0 MB</span>';
|
|
2858
|
+
this.memoryElement = memLabel.querySelector('#tm-mem');
|
|
2859
|
+
this.container.appendChild(memLabel);
|
|
2860
|
+
}
|
|
2861
|
+
else {
|
|
2862
|
+
this.memoryElement = document.createElement('span');
|
|
2863
|
+
}
|
|
2864
|
+
// Create draw calls display
|
|
2865
|
+
if (this.options.renderer) {
|
|
2866
|
+
const drawLabel = document.createElement('div');
|
|
2867
|
+
drawLabel.className = 'tm-perf-row';
|
|
2868
|
+
drawLabel.innerHTML = '<span class="tm-perf-label">Draw Calls:</span> <span class="tm-perf-value" id="tm-draw">0</span>';
|
|
2869
|
+
this.drawCallsElement = drawLabel.querySelector('#tm-draw');
|
|
2870
|
+
this.container.appendChild(drawLabel);
|
|
2871
|
+
const triLabel = document.createElement('div');
|
|
2872
|
+
triLabel.className = 'tm-perf-row';
|
|
2873
|
+
triLabel.innerHTML = '<span class="tm-perf-label">Triangles:</span> <span class="tm-perf-value" id="tm-tri">0</span>';
|
|
2874
|
+
this.trianglesElement = triLabel.querySelector('#tm-tri');
|
|
2875
|
+
this.container.appendChild(triLabel);
|
|
2876
|
+
}
|
|
2877
|
+
else {
|
|
2878
|
+
this.drawCallsElement = document.createElement('span');
|
|
2879
|
+
this.trianglesElement = document.createElement('span');
|
|
2880
|
+
}
|
|
2881
|
+
// Create warnings container
|
|
2882
|
+
if (this.options.enableWarnings) {
|
|
2883
|
+
this.warningsContainer = document.createElement('div');
|
|
2884
|
+
this.warningsContainer.className = 'tm-perf-warnings';
|
|
2885
|
+
this.container.appendChild(this.warningsContainer);
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
this.warningsContainer = document.createElement('div');
|
|
2889
|
+
}
|
|
2890
|
+
// Inject styles
|
|
2891
|
+
this.injectStyles();
|
|
2892
|
+
// Append to body
|
|
2893
|
+
document.body.appendChild(this.container);
|
|
2894
|
+
}
|
|
2895
|
+
setPosition(position) {
|
|
2896
|
+
const positions = {
|
|
2897
|
+
'top-left': { top: '10px', left: '10px' },
|
|
2898
|
+
'top-right': { top: '10px', right: '10px' },
|
|
2899
|
+
'bottom-left': { bottom: '10px', left: '10px' },
|
|
2900
|
+
'bottom-right': { bottom: '10px', right: '10px' }
|
|
2901
|
+
};
|
|
2902
|
+
const pos = positions[position] || positions['top-left'];
|
|
2903
|
+
Object.assign(this.container.style, {
|
|
2904
|
+
position: 'fixed',
|
|
2905
|
+
zIndex: '99999',
|
|
2906
|
+
...pos
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
injectStyles() {
|
|
2910
|
+
const styleId = 'tm-performance-stats-styles';
|
|
2911
|
+
if (document.getElementById(styleId))
|
|
2912
|
+
return;
|
|
2913
|
+
const style = document.createElement('style');
|
|
2914
|
+
style.id = styleId;
|
|
2915
|
+
style.innerHTML = `
|
|
2916
|
+
.tm-performance-stats {
|
|
2917
|
+
background: rgba(0, 0, 0, 0.8);
|
|
2918
|
+
color: #0f0;
|
|
2919
|
+
font-family: 'Courier New', monospace;
|
|
2920
|
+
font-size: 12px;
|
|
2921
|
+
padding: 10px;
|
|
2922
|
+
border-radius: 4px;
|
|
2923
|
+
min-width: 180px;
|
|
2924
|
+
backdrop-filter: blur(4px);
|
|
2925
|
+
user-select: none;
|
|
2926
|
+
pointer-events: none;
|
|
2927
|
+
}
|
|
2928
|
+
.tm-perf-row {
|
|
2929
|
+
display: flex;
|
|
2930
|
+
justify-content: space-between;
|
|
2931
|
+
margin-bottom: 4px;
|
|
2932
|
+
}
|
|
2933
|
+
.tm-perf-label {
|
|
2934
|
+
color: #888;
|
|
2935
|
+
}
|
|
2936
|
+
.tm-perf-value {
|
|
2937
|
+
color: #0f0;
|
|
2938
|
+
font-weight: bold;
|
|
2939
|
+
}
|
|
2940
|
+
.tm-perf-value.warning {
|
|
2941
|
+
color: #ff0;
|
|
2942
|
+
}
|
|
2943
|
+
.tm-perf-value.critical {
|
|
2944
|
+
color: #f00;
|
|
2945
|
+
}
|
|
2946
|
+
.tm-perf-warnings {
|
|
2947
|
+
margin-top: 8px;
|
|
2948
|
+
padding-top: 8px;
|
|
2949
|
+
border-top: 1px solid #333;
|
|
2950
|
+
}
|
|
2951
|
+
.tm-perf-warning {
|
|
2952
|
+
font-size: 10px;
|
|
2953
|
+
padding: 4px;
|
|
2954
|
+
margin-bottom: 4px;
|
|
2955
|
+
border-radius: 2px;
|
|
2956
|
+
}
|
|
2957
|
+
.tm-perf-warning.info {
|
|
2958
|
+
background: rgba(0, 128, 255, 0.2);
|
|
2959
|
+
color: #0af;
|
|
2960
|
+
}
|
|
2961
|
+
.tm-perf-warning.warning {
|
|
2962
|
+
background: rgba(255, 200, 0, 0.2);
|
|
2963
|
+
color: #fc0;
|
|
2964
|
+
}
|
|
2965
|
+
.tm-perf-warning.critical {
|
|
2966
|
+
background: rgba(255, 0, 0, 0.2);
|
|
2967
|
+
color: #f66;
|
|
2968
|
+
}
|
|
2969
|
+
`;
|
|
2970
|
+
document.head.appendChild(style);
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* Updates the performance statistics. This method must be called within the application's animation loop.
|
|
2974
|
+
*/
|
|
2975
|
+
update() {
|
|
2976
|
+
if (!this.enabled)
|
|
2977
|
+
return;
|
|
2978
|
+
const now = performance.now();
|
|
2979
|
+
this.frames++;
|
|
2980
|
+
// Calculate FPS
|
|
2981
|
+
const delta = now - this.lastTime;
|
|
2982
|
+
if (delta >= 1000) {
|
|
2983
|
+
this.fps = Math.round((this.frames * 1000) / delta);
|
|
2984
|
+
this.fpsHistory.push(this.fps);
|
|
2985
|
+
if (this.fpsHistory.length > this.maxHistoryLength) {
|
|
2986
|
+
this.fpsHistory.shift();
|
|
2987
|
+
}
|
|
2988
|
+
this.frames = 0;
|
|
2989
|
+
this.lastTime = now;
|
|
2990
|
+
}
|
|
2991
|
+
// Throttle DOM updates
|
|
2992
|
+
if (now - this.lastUpdateTime < this.updateInterval) {
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
this.lastUpdateTime = now;
|
|
2996
|
+
// Update FPS display
|
|
2997
|
+
this.fpsElement.textContent = this.fps.toString();
|
|
2998
|
+
this.fpsElement.className = 'tm-perf-value';
|
|
2999
|
+
if (this.fps < this.options.fpsWarningThreshold) {
|
|
3000
|
+
this.fpsElement.classList.add('critical');
|
|
3001
|
+
this.addWarning({
|
|
3002
|
+
type: 'low-fps',
|
|
3003
|
+
message: `Low FPS: ${this.fps}`,
|
|
3004
|
+
severity: 'critical',
|
|
3005
|
+
timestamp: now
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
else if (this.fps < this.options.fpsWarningThreshold + 10) {
|
|
3009
|
+
this.fpsElement.classList.add('warning');
|
|
3010
|
+
}
|
|
3011
|
+
// Update memory display
|
|
3012
|
+
if (this.options.enableMemoryTracking && performance.memory) {
|
|
3013
|
+
const memory = performance.memory;
|
|
3014
|
+
const usedMB = Math.round(memory.usedJSHeapSize / 1048576);
|
|
3015
|
+
const totalMB = Math.round(memory.jsHeapSizeLimit / 1048576);
|
|
3016
|
+
this.memoryElement.textContent = `${usedMB}/${totalMB} MB`;
|
|
3017
|
+
this.memoryElement.className = 'tm-perf-value';
|
|
3018
|
+
if (usedMB > this.options.memoryWarningThreshold) {
|
|
3019
|
+
this.memoryElement.classList.add('warning');
|
|
3020
|
+
this.addWarning({
|
|
3021
|
+
type: 'high-memory',
|
|
3022
|
+
message: `High memory: ${usedMB}MB`,
|
|
3023
|
+
severity: 'warning',
|
|
3024
|
+
timestamp: now
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
// Update renderer stats
|
|
3029
|
+
if (this.options.renderer) {
|
|
3030
|
+
const info = this.options.renderer.info;
|
|
3031
|
+
this.drawCallsElement.textContent = info.render.calls.toString();
|
|
3032
|
+
this.trianglesElement.textContent = this.formatNumber(info.render.triangles);
|
|
3033
|
+
if (info.render.calls > 100) {
|
|
3034
|
+
this.drawCallsElement.className = 'tm-perf-value warning';
|
|
3035
|
+
this.addWarning({
|
|
3036
|
+
type: 'excessive-drawcalls',
|
|
3037
|
+
message: `Draw calls: ${info.render.calls}`,
|
|
3038
|
+
severity: 'info',
|
|
3039
|
+
timestamp: now
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
else {
|
|
3043
|
+
this.drawCallsElement.className = 'tm-perf-value';
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
// Update warnings display
|
|
3047
|
+
if (this.options.enableWarnings) {
|
|
3048
|
+
this.updateWarningsDisplay();
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
formatNumber(num) {
|
|
3052
|
+
if (num >= 1000000)
|
|
3053
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
3054
|
+
if (num >= 1000)
|
|
3055
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
3056
|
+
return num.toString();
|
|
3057
|
+
}
|
|
3058
|
+
addWarning(warning) {
|
|
3059
|
+
// Don't add duplicate warnings within 5 seconds
|
|
3060
|
+
const isDuplicate = this.warnings.some(w => w.type === warning.type && (warning.timestamp - w.timestamp < 5000));
|
|
3061
|
+
if (isDuplicate)
|
|
3062
|
+
return;
|
|
3063
|
+
this.warnings.push(warning);
|
|
3064
|
+
if (this.warnings.length > this.maxWarnings) {
|
|
3065
|
+
this.warnings.shift();
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
updateWarningsDisplay() {
|
|
3069
|
+
if (!this.warningsContainer)
|
|
3070
|
+
return;
|
|
3071
|
+
// Remove old warnings (older than 10 seconds)
|
|
3072
|
+
const now = performance.now();
|
|
3073
|
+
this.warnings = this.warnings.filter(w => now - w.timestamp < 10000);
|
|
3074
|
+
this.warningsContainer.innerHTML = '';
|
|
3075
|
+
this.warnings.forEach(warning => {
|
|
3076
|
+
const el = document.createElement('div');
|
|
3077
|
+
el.className = `tm-perf-warning ${warning.severity}`;
|
|
3078
|
+
el.textContent = warning.message;
|
|
3079
|
+
this.warningsContainer.appendChild(el);
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Gets the current frames per second (FPS).
|
|
3084
|
+
* @returns {number} The current FPS.
|
|
3085
|
+
*/
|
|
3086
|
+
getFPS() {
|
|
3087
|
+
return this.fps;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Gets the average FPS over the recent history period.
|
|
3091
|
+
* @returns {number} The average FPS.
|
|
3092
|
+
*/
|
|
3093
|
+
getAverageFPS() {
|
|
3094
|
+
if (this.fpsHistory.length === 0)
|
|
3095
|
+
return 60;
|
|
3096
|
+
return Math.round(this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length);
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Returns a snapshot of all tracked performance metrics.
|
|
3100
|
+
* @returns {object} Current performance statistics.
|
|
3101
|
+
*/
|
|
3102
|
+
getStats() {
|
|
3103
|
+
const stats = {
|
|
3104
|
+
fps: this.fps,
|
|
3105
|
+
averageFPS: this.getAverageFPS()
|
|
3106
|
+
};
|
|
3107
|
+
if (this.options.enableMemoryTracking && performance.memory) {
|
|
3108
|
+
const memory = performance.memory;
|
|
3109
|
+
stats.memory = {
|
|
3110
|
+
used: Math.round(memory.usedJSHeapSize / 1048576),
|
|
3111
|
+
total: Math.round(memory.jsHeapSizeLimit / 1048576)
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
if (this.options.renderer) {
|
|
3115
|
+
const info = this.options.renderer.info;
|
|
3116
|
+
stats.drawCalls = info.render.calls;
|
|
3117
|
+
stats.triangles = info.render.triangles;
|
|
3118
|
+
}
|
|
3119
|
+
return stats;
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Toggles the visibility of the performance stats overlay.
|
|
3123
|
+
*/
|
|
3124
|
+
toggle() {
|
|
3125
|
+
this.isVisible = !this.isVisible;
|
|
3126
|
+
this.container.style.display = this.isVisible ? 'block' : 'none';
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Shows the performance stats overlay.
|
|
3130
|
+
*/
|
|
3131
|
+
show() {
|
|
3132
|
+
this.isVisible = true;
|
|
3133
|
+
this.container.style.display = 'block';
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Hides the performance stats overlay.
|
|
3137
|
+
*/
|
|
3138
|
+
hide() {
|
|
3139
|
+
this.isVisible = false;
|
|
3140
|
+
this.container.style.display = 'none';
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Enables or disables performance statistics collection.
|
|
3144
|
+
* @param {boolean} enabled - Whether collection should be enabled.
|
|
3145
|
+
*/
|
|
3146
|
+
setEnabled(enabled) {
|
|
3147
|
+
this.enabled = enabled;
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Disposes of the performance monitor and removes its DOM elements.
|
|
3151
|
+
*/
|
|
3152
|
+
dispose() {
|
|
3153
|
+
this.container.remove();
|
|
3154
|
+
this.enabled = false;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Helper function to create performance monitor
|
|
3159
|
+
*/
|
|
3160
|
+
function createPerformanceMonitor(options) {
|
|
3161
|
+
return new PerformanceStats(options);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
2535
3164
|
/**
|
|
2536
3165
|
* @file exploder.ts
|
|
2537
3166
|
* @description
|
|
@@ -3399,5 +4028,5 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
|
|
|
3399
4028
|
// Version (keep in sync with package.json)
|
|
3400
4029
|
const VERSION = '1.0.4';
|
|
3401
4030
|
|
|
3402
|
-
export { ArrowGuide, BlueSky, FOLLOW_ANGLES, GroupExploder, LiquidFillerGroup, ResourceManager, VERSION,
|
|
4031
|
+
export { ArrowGuide, BlueSky, Box3Pool, FOLLOW_ANGLES, GroupExploder, LiquidFillerGroup, Matrix4Pool, PerformanceStats, QuaternionPool, ResourceManager, VERSION, Vector3Pool, ViewPresets, autoSetupCameraAndLight, cancelFollow, cancelSetView, createModelClickHandler, createModelsLabel, createPerformanceMonitor, disposeMaterial, disposeObject, enableHoverBreath, fitCameraToObject, followModels, getLoaderConfig, globalPools, initPostProcessing, loadCubeSkybox, loadEquirectSkybox, loadModelByUrl, loadSkybox, releaseSkybox, setLoaderConfig, setView, setupDefaultLights, withPooledBox3, withPooledMatrix4, withPooledQuaternion, withPooledVector3 };
|
|
3403
4032
|
//# sourceMappingURL=index.mjs.map
|