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