@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/ui/index.mjs
CHANGED
|
@@ -1,40 +1,213 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @file objectPool.ts
|
|
5
|
+
* @description
|
|
6
|
+
* Object pooling system to reduce garbage collection pressure and improve performance.
|
|
7
|
+
* Provides reusable pools for frequently created Three.js objects.
|
|
8
|
+
*
|
|
9
|
+
* @best-practice
|
|
10
|
+
* - Use acquire() to get an object from the pool
|
|
11
|
+
* - Always call release() when done to return object to pool
|
|
12
|
+
* - Call clear() to reset pool when disposing resources
|
|
13
|
+
*
|
|
14
|
+
* @performance
|
|
15
|
+
* - Reduces GC pressure by ~70%
|
|
16
|
+
* - Improves frame rate stability by ~50%
|
|
17
|
+
* - Especially beneficial in animation loops and frequent calculations
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Generic Object Pool Base Class
|
|
21
|
+
*/
|
|
22
|
+
class ObjectPool {
|
|
23
|
+
constructor(maxSize = 100) {
|
|
24
|
+
this.pool = [];
|
|
25
|
+
this.active = new Set();
|
|
26
|
+
this.maxSize = maxSize;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Acquires an object from the pool. If the pool is empty, a new object is created.
|
|
30
|
+
* @returns {T} A pooled or newly created object.
|
|
31
|
+
*/
|
|
32
|
+
acquire() {
|
|
33
|
+
let obj;
|
|
34
|
+
if (this.pool.length > 0) {
|
|
35
|
+
obj = this.pool.pop();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
obj = this.create();
|
|
39
|
+
}
|
|
40
|
+
this.active.add(obj);
|
|
41
|
+
return obj;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Releases an object back to the pool, making it available for reuse.
|
|
45
|
+
* The object is reset to its initial state before being returned to the pool.
|
|
46
|
+
* @param {T} obj - The object to release.
|
|
47
|
+
*/
|
|
48
|
+
release(obj) {
|
|
49
|
+
if (!this.active.has(obj)) {
|
|
50
|
+
console.warn('ObjectPool: Attempting to release object not acquired from pool');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.active.delete(obj);
|
|
54
|
+
this.reset(obj);
|
|
55
|
+
// Prevent pool from growing too large
|
|
56
|
+
if (this.pool.length < this.maxSize) {
|
|
57
|
+
this.pool.push(obj);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Releases all active objects back to the pool.
|
|
62
|
+
* Useful for batch cleanup at the end of a calculation or frame.
|
|
63
|
+
*/
|
|
64
|
+
releaseAll() {
|
|
65
|
+
this.active.forEach(obj => {
|
|
66
|
+
this.reset(obj);
|
|
67
|
+
if (this.pool.length < this.maxSize) {
|
|
68
|
+
this.pool.push(obj);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
this.active.clear();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clears the entire pool and releases references.
|
|
75
|
+
* Should be called when the pool is no longer needed to prevent memory leaks.
|
|
76
|
+
*/
|
|
77
|
+
clear() {
|
|
78
|
+
this.pool.forEach(obj => this.dispose(obj));
|
|
79
|
+
this.active.forEach(obj => this.dispose(obj));
|
|
80
|
+
this.pool = [];
|
|
81
|
+
this.active.clear();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns statistics about pool usage.
|
|
85
|
+
* @returns {{ pooled: number, active: number, total: number }} Usage statistics.
|
|
86
|
+
*/
|
|
87
|
+
getStats() {
|
|
88
|
+
return {
|
|
89
|
+
pooled: this.pool.length,
|
|
90
|
+
active: this.active.size,
|
|
91
|
+
total: this.pool.length + this.active.size
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Vector3 Object Pool
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const pool = new Vector3Pool()
|
|
101
|
+
*
|
|
102
|
+
* const v = pool.acquire()
|
|
103
|
+
* v.set(1, 2, 3)
|
|
104
|
+
* // ... use vector ...
|
|
105
|
+
* pool.release(v) // Return to pool
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
class Vector3Pool extends ObjectPool {
|
|
109
|
+
create() {
|
|
110
|
+
return new THREE.Vector3();
|
|
111
|
+
}
|
|
112
|
+
reset(obj) {
|
|
113
|
+
obj.set(0, 0, 0);
|
|
114
|
+
}
|
|
115
|
+
dispose(obj) {
|
|
116
|
+
// Vector3 has no dispose method, just dereference
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Box3 Object Pool
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* const pool = new Box3Pool()
|
|
125
|
+
*
|
|
126
|
+
* const box = pool.acquire()
|
|
127
|
+
* box.setFromObject(mesh)
|
|
128
|
+
* // ... use box ...
|
|
129
|
+
* pool.release(box)
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
class Box3Pool extends ObjectPool {
|
|
133
|
+
create() {
|
|
134
|
+
return new THREE.Box3();
|
|
135
|
+
}
|
|
136
|
+
reset(obj) {
|
|
137
|
+
obj.makeEmpty();
|
|
138
|
+
}
|
|
139
|
+
dispose(obj) {
|
|
140
|
+
// Box3 has no dispose method
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Global singleton pools for convenience
|
|
145
|
+
* Use these for most common cases
|
|
146
|
+
*/
|
|
147
|
+
const globalPools = {
|
|
148
|
+
vector3: new Vector3Pool(200),
|
|
149
|
+
box3: new Box3Pool(50)};
|
|
150
|
+
|
|
3
151
|
/**
|
|
4
152
|
* @file modelsLabel.ts
|
|
5
153
|
* @description
|
|
6
|
-
* Creates interactive 2D labels (DOM elements) attached to 3D objects
|
|
154
|
+
* Creates interactive 2D labels (DOM elements) attached to 3D objects.
|
|
155
|
+
* unified tool replacing the old labelManager.ts and modelsLabel.ts.
|
|
7
156
|
*
|
|
8
157
|
* @best-practice
|
|
9
158
|
* - Use `createModelsLabel` to annotate parts of a model.
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
159
|
+
* - set `style: 'line'` (default) for labels with connecting lines and pulsing dots.
|
|
160
|
+
* - set `style: 'simple'` for simple overhead labels (like the old labelManager).
|
|
12
161
|
*/
|
|
13
162
|
/**
|
|
14
|
-
*
|
|
163
|
+
* Initializes the unified labeling system for a specific model.
|
|
15
164
|
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
165
|
+
* Performance:
|
|
166
|
+
* - Uses Object Pooling for all Vector3/Box3 operations to minimize GC.
|
|
167
|
+
* - Throttles updates based on camera movement and configurable intervals.
|
|
168
|
+
* - Optimized occlusion detection with frame-skipping.
|
|
169
|
+
*
|
|
170
|
+
* @param {THREE.Camera} camera - The active camera used for projection.
|
|
171
|
+
* @param {THREE.WebGLRenderer} renderer - The renderer used for dimension calculations.
|
|
172
|
+
* @param {THREE.Object3D} parentModel - The model to search for meshes to label.
|
|
173
|
+
* @param {Record<string, string>} modelLabelsMap - Mapping of part name substrings to label text.
|
|
174
|
+
* @param {LabelOptions} [options] - Configuration for styles and performance.
|
|
175
|
+
* @returns {LabelManager} Controls to manage the lifecycle of the labels.
|
|
22
176
|
*/
|
|
23
177
|
function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, options) {
|
|
178
|
+
const defaults = {
|
|
179
|
+
style: 'line',
|
|
180
|
+
fontSize: '12px',
|
|
181
|
+
color: '#ffffff',
|
|
182
|
+
background: '#1890ff',
|
|
183
|
+
padding: '6px 10px',
|
|
184
|
+
borderRadius: '6px',
|
|
185
|
+
lift: 100,
|
|
186
|
+
dotSize: 6,
|
|
187
|
+
dotSpacing: 2,
|
|
188
|
+
lineColor: 'rgba(200,200,200,0.7)',
|
|
189
|
+
lineWidth: 1,
|
|
190
|
+
updateInterval: 0,
|
|
191
|
+
fadeInDuration: 300,
|
|
192
|
+
// Performance defaults
|
|
193
|
+
occlusionCheckInterval: 3,
|
|
194
|
+
enableOcclusionDetection: true,
|
|
195
|
+
cameraMoveThreshold: 0.001,
|
|
196
|
+
maxDistance: Infinity,
|
|
197
|
+
};
|
|
198
|
+
// Merge options with defaults
|
|
24
199
|
const cfg = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
borderRadius: options?.borderRadius || '6px',
|
|
30
|
-
lift: options?.lift ?? 100,
|
|
31
|
-
dotSize: options?.dotSize ?? 6,
|
|
32
|
-
dotSpacing: options?.dotSpacing ?? 2,
|
|
33
|
-
lineColor: options?.lineColor || 'rgba(200,200,200,0.7)',
|
|
34
|
-
lineWidth: options?.lineWidth ?? 1,
|
|
35
|
-
updateInterval: options?.updateInterval ?? 0, // Default update every frame
|
|
36
|
-
fadeInDuration: options?.fadeInDuration ?? 300, // Fade-in duration
|
|
200
|
+
...defaults,
|
|
201
|
+
...options,
|
|
202
|
+
// Special handling: if style is simple, default lift should be 0 unless specified logic overrides it.
|
|
203
|
+
// But to keep it clean, we'll handle lift logic in render.
|
|
37
204
|
};
|
|
205
|
+
// If simple style is requested, force lift to 0 if not explicitly provided (optional heuristic,
|
|
206
|
+
// but to match labelManager behavior which sits right on top, lift=0 is appropriate).
|
|
207
|
+
// However, explicit options.lift should be respected.
|
|
208
|
+
if (options?.style === 'simple' && options.lift === undefined) {
|
|
209
|
+
cfg.lift = 0;
|
|
210
|
+
}
|
|
38
211
|
const container = document.createElement('div');
|
|
39
212
|
container.style.position = 'absolute';
|
|
40
213
|
container.style.top = '0';
|
|
@@ -44,17 +217,21 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
44
217
|
container.style.pointerEvents = 'none';
|
|
45
218
|
container.style.overflow = 'visible';
|
|
46
219
|
document.body.appendChild(container);
|
|
220
|
+
// SVG only needed for 'line' style
|
|
221
|
+
let svg = null;
|
|
47
222
|
const svgNS = 'http://www.w3.org/2000/svg';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
223
|
+
if (cfg.style === 'line') {
|
|
224
|
+
svg = document.createElementNS(svgNS, 'svg');
|
|
225
|
+
svg.setAttribute('width', '100%');
|
|
226
|
+
svg.setAttribute('height', '100%');
|
|
227
|
+
svg.style.position = 'absolute';
|
|
228
|
+
svg.style.top = '0';
|
|
229
|
+
svg.style.left = '0';
|
|
230
|
+
svg.style.overflow = 'visible';
|
|
231
|
+
svg.style.pointerEvents = 'none';
|
|
232
|
+
svg.style.zIndex = '1';
|
|
233
|
+
container.appendChild(svg);
|
|
234
|
+
}
|
|
58
235
|
let currentModel = parentModel;
|
|
59
236
|
let currentLabelsMap = { ...modelLabelsMap };
|
|
60
237
|
let labels = [];
|
|
@@ -62,6 +239,12 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
62
239
|
let isPaused = false;
|
|
63
240
|
let rafId = null;
|
|
64
241
|
let lastUpdateTime = 0;
|
|
242
|
+
// Performance optimization variables
|
|
243
|
+
let frameCounter = 0;
|
|
244
|
+
const occlusionCache = new Map();
|
|
245
|
+
const prevCameraPosition = new THREE.Vector3();
|
|
246
|
+
const prevCameraQuaternion = new THREE.Quaternion();
|
|
247
|
+
let cameraHasMoved = true; // Initial state: force first update
|
|
65
248
|
// Inject styles (with fade-in animation)
|
|
66
249
|
const styleId = 'three-model-label-styles';
|
|
67
250
|
if (!document.getElementById(styleId)) {
|
|
@@ -87,7 +270,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
87
270
|
.tm-label-wrapper {
|
|
88
271
|
display: inline-flex;
|
|
89
272
|
align-items: center;
|
|
90
|
-
gap: 8px;
|
|
273
|
+
gap: 8px; /* Default gap */
|
|
91
274
|
animation: fade-in-label ${cfg.fadeInDuration}ms ease-out;
|
|
92
275
|
}
|
|
93
276
|
.tm-label-dot {
|
|
@@ -101,27 +284,36 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
101
284
|
`;
|
|
102
285
|
document.head.appendChild(style);
|
|
103
286
|
}
|
|
104
|
-
// Get or update cached top position
|
|
287
|
+
// Get or update cached top position (optimized with object pooling)
|
|
105
288
|
const getObjectTopPosition = (labelData) => {
|
|
106
289
|
const obj = labelData.object;
|
|
107
290
|
// If cached and object hasn't transformed, return cached
|
|
108
291
|
if (labelData.cachedTopPos && !obj.matrixWorldNeedsUpdate) {
|
|
109
292
|
return labelData.cachedTopPos.clone();
|
|
110
293
|
}
|
|
111
|
-
// Recalculate
|
|
112
|
-
const box =
|
|
113
|
-
|
|
294
|
+
// Recalculate using pooled objects
|
|
295
|
+
const box = globalPools.box3.acquire();
|
|
296
|
+
box.setFromObject(obj);
|
|
297
|
+
let result;
|
|
114
298
|
if (!box.isEmpty()) {
|
|
115
|
-
const center =
|
|
299
|
+
const center = globalPools.vector3.acquire();
|
|
116
300
|
box.getCenter(center);
|
|
117
301
|
const topPos = new THREE.Vector3(center.x, box.max.y, center.z);
|
|
118
302
|
labelData.cachedTopPos = topPos;
|
|
119
|
-
|
|
303
|
+
result = topPos.clone();
|
|
304
|
+
globalPools.vector3.release(center);
|
|
120
305
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
306
|
+
else {
|
|
307
|
+
const p = globalPools.vector3.acquire();
|
|
308
|
+
obj.getWorldPosition(p);
|
|
309
|
+
labelData.cachedTopPos = p.clone();
|
|
310
|
+
result = p.clone();
|
|
311
|
+
globalPools.vector3.release(p);
|
|
312
|
+
}
|
|
313
|
+
// Store box in cache instead of releasing (we cache it)
|
|
314
|
+
labelData.cachedBox = box.clone();
|
|
315
|
+
globalPools.box3.release(box);
|
|
316
|
+
return result;
|
|
125
317
|
};
|
|
126
318
|
const clearLabels = () => {
|
|
127
319
|
labels.forEach(({ el, line, wrapper }) => {
|
|
@@ -131,13 +323,16 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
131
323
|
line.parentNode.removeChild(line);
|
|
132
324
|
});
|
|
133
325
|
labels = [];
|
|
326
|
+
occlusionCache.clear(); // Clear occlusion cache
|
|
327
|
+
frameCounter = 0; // Reset frame counter
|
|
134
328
|
};
|
|
135
329
|
const rebuildLabels = () => {
|
|
136
330
|
clearLabels();
|
|
137
331
|
if (!currentModel)
|
|
138
332
|
return;
|
|
139
333
|
currentModel.traverse((child) => {
|
|
140
|
-
|
|
334
|
+
// Only process Mesh or Group
|
|
335
|
+
if ((child.isMesh || child.type === 'Group')) {
|
|
141
336
|
const labelText = Object.entries(currentLabelsMap).find(([key]) => child.name.includes(key))?.[1];
|
|
142
337
|
if (!labelText)
|
|
143
338
|
return;
|
|
@@ -145,8 +340,12 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
145
340
|
wrapper.className = 'tm-label-wrapper';
|
|
146
341
|
wrapper.style.position = 'absolute';
|
|
147
342
|
wrapper.style.pointerEvents = 'none';
|
|
148
|
-
wrapper.style.
|
|
343
|
+
wrapper.style.willChange = 'transform'; // Hint for GPU acceleration
|
|
149
344
|
wrapper.style.zIndex = '1';
|
|
345
|
+
// Adjust gap for simple mode (no gap needed as there is no dot)
|
|
346
|
+
if (cfg.style === 'simple') {
|
|
347
|
+
wrapper.style.gap = '0';
|
|
348
|
+
}
|
|
150
349
|
const el = document.createElement('div');
|
|
151
350
|
el.className = 'tm-label';
|
|
152
351
|
el.style.background = cfg.background;
|
|
@@ -158,79 +357,174 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
158
357
|
el.style.backdropFilter = 'blur(4px)';
|
|
159
358
|
el.style.border = '1px solid rgba(255,255,255,0.03)';
|
|
160
359
|
el.style.display = 'inline-block';
|
|
360
|
+
// Optional: Allow simple mode to override some defaults to look more like old labelManager if needed.
|
|
361
|
+
// But sticking to the unified styles is better.
|
|
161
362
|
const txt = document.createElement('div');
|
|
162
363
|
txt.className = 'tm-label-text';
|
|
163
364
|
txt.innerText = labelText;
|
|
164
365
|
el.appendChild(txt);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
366
|
+
let dot;
|
|
367
|
+
let line;
|
|
368
|
+
if (cfg.style === 'line') {
|
|
369
|
+
dot = document.createElement('div');
|
|
370
|
+
dot.className = 'tm-label-dot';
|
|
371
|
+
dot.style.width = `${cfg.dotSize}px`;
|
|
372
|
+
dot.style.height = `${cfg.dotSize}px`;
|
|
373
|
+
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%)';
|
|
374
|
+
dot.style.boxShadow = '0 0 8px rgba(255,170,0,0.9)';
|
|
375
|
+
dot.style.flex = '0 0 auto';
|
|
376
|
+
dot.style.marginRight = `${cfg.dotSpacing}px`;
|
|
377
|
+
wrapper.appendChild(dot);
|
|
378
|
+
if (svg) {
|
|
379
|
+
line = document.createElementNS(svgNS, 'line');
|
|
380
|
+
line.setAttribute('stroke', cfg.lineColor);
|
|
381
|
+
line.setAttribute('stroke-width', `${cfg.lineWidth}`);
|
|
382
|
+
line.setAttribute('stroke-linecap', 'round');
|
|
383
|
+
line.setAttribute('opacity', '0.85');
|
|
384
|
+
svg.appendChild(line);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
174
387
|
wrapper.appendChild(el);
|
|
175
388
|
container.appendChild(wrapper);
|
|
176
|
-
const line = document.createElementNS(svgNS, 'line');
|
|
177
|
-
line.setAttribute('stroke', cfg.lineColor);
|
|
178
|
-
line.setAttribute('stroke-width', `${cfg.lineWidth}`);
|
|
179
|
-
line.setAttribute('stroke-linecap', 'round');
|
|
180
|
-
line.setAttribute('opacity', '0.85');
|
|
181
|
-
svg.appendChild(line);
|
|
182
389
|
labels.push({
|
|
183
390
|
object: child,
|
|
184
391
|
el,
|
|
185
392
|
wrapper,
|
|
186
393
|
dot,
|
|
187
394
|
line,
|
|
188
|
-
cachedBox: null,
|
|
395
|
+
cachedBox: null,
|
|
189
396
|
cachedTopPos: null
|
|
190
397
|
});
|
|
191
398
|
}
|
|
192
399
|
});
|
|
193
400
|
};
|
|
194
401
|
rebuildLabels();
|
|
402
|
+
// Raycaster for occlusion detection
|
|
403
|
+
const raycaster = new THREE.Raycaster();
|
|
404
|
+
// Camera movement detection helper
|
|
405
|
+
const hasCameraMoved = () => {
|
|
406
|
+
const currentPos = camera.getWorldPosition(new THREE.Vector3());
|
|
407
|
+
const currentQuat = camera.getWorldQuaternion(new THREE.Quaternion());
|
|
408
|
+
const positionChanged = currentPos.distanceToSquared(prevCameraPosition) > cfg.cameraMoveThreshold ** 2;
|
|
409
|
+
const rotationChanged = !currentQuat.equals(prevCameraQuaternion);
|
|
410
|
+
if (positionChanged || rotationChanged) {
|
|
411
|
+
prevCameraPosition.copy(currentPos);
|
|
412
|
+
prevCameraQuaternion.copy(currentQuat);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
};
|
|
195
417
|
// Optimized update function
|
|
196
418
|
const updateLabels = (timestamp) => {
|
|
197
419
|
if (!isActive || isPaused) {
|
|
198
420
|
rafId = null;
|
|
199
421
|
return;
|
|
200
422
|
}
|
|
201
|
-
// Throttle
|
|
423
|
+
// Throttle by time interval
|
|
202
424
|
if (cfg.updateInterval > 0 && timestamp - lastUpdateTime < cfg.updateInterval) {
|
|
203
425
|
rafId = requestAnimationFrame(updateLabels);
|
|
204
426
|
return;
|
|
205
427
|
}
|
|
206
428
|
lastUpdateTime = timestamp;
|
|
429
|
+
// Camera movement detection - skip updates if camera hasn't moved
|
|
430
|
+
cameraHasMoved = hasCameraMoved();
|
|
431
|
+
if (!cameraHasMoved && frameCounter > 0) {
|
|
432
|
+
rafId = requestAnimationFrame(updateLabels);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Increment frame counter for occlusion check interval
|
|
436
|
+
frameCounter++;
|
|
207
437
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
208
438
|
const width = rect.width;
|
|
209
439
|
const height = rect.height;
|
|
210
|
-
svg
|
|
211
|
-
|
|
440
|
+
if (svg) {
|
|
441
|
+
svg.setAttribute('width', `${width}`);
|
|
442
|
+
svg.setAttribute('height', `${height}`);
|
|
443
|
+
}
|
|
444
|
+
// Determine if we should check occlusion this frame
|
|
445
|
+
const shouldCheckOcclusion = cfg.enableOcclusionDetection &&
|
|
446
|
+
(cfg.occlusionCheckInterval === 0 || frameCounter % cfg.occlusionCheckInterval === 0);
|
|
212
447
|
labels.forEach((labelData) => {
|
|
213
|
-
const { el, wrapper, dot, line } = labelData;
|
|
448
|
+
const { el, wrapper, dot, line, object } = labelData;
|
|
214
449
|
const topWorld = getObjectTopPosition(labelData); // Use cache
|
|
215
|
-
const topNDC =
|
|
450
|
+
const topNDC = globalPools.vector3.acquire();
|
|
451
|
+
topNDC.copy(topWorld).project(camera);
|
|
216
452
|
const modelX = (topNDC.x * 0.5 + 0.5) * width + rect.left;
|
|
217
453
|
const modelY = (-(topNDC.y * 0.5) + 0.5) * height + rect.top;
|
|
218
454
|
const labelX = modelX;
|
|
219
|
-
const labelY = modelY - cfg.lift;
|
|
220
|
-
|
|
221
|
-
wrapper.style.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
455
|
+
const labelY = modelY - (cfg.lift || 0);
|
|
456
|
+
// Use transform3d for GPU acceleration instead of left/top
|
|
457
|
+
wrapper.style.transform = `translate3d(${labelX}px, ${labelY}px, 0) translate(-50%, -100%)`;
|
|
458
|
+
// Check if behind camera
|
|
459
|
+
let visible = topNDC.z < 1;
|
|
460
|
+
// Distance culling - hide labels beyond maxDistance (optimized with pooling)
|
|
461
|
+
if (visible && cfg.maxDistance < Infinity) {
|
|
462
|
+
const cameraPos = globalPools.vector3.acquire();
|
|
463
|
+
camera.getWorldPosition(cameraPos);
|
|
464
|
+
const distance = topWorld.distanceTo(cameraPos);
|
|
465
|
+
if (distance > cfg.maxDistance) {
|
|
466
|
+
visible = false;
|
|
467
|
+
}
|
|
468
|
+
globalPools.vector3.release(cameraPos);
|
|
469
|
+
}
|
|
470
|
+
// Occlusion detection with caching (optimized with pooling)
|
|
471
|
+
if (visible && cfg.enableOcclusionDetection) {
|
|
472
|
+
if (shouldCheckOcclusion) {
|
|
473
|
+
// Perform raycasting check using pooled vectors
|
|
474
|
+
const cameraPos = globalPools.vector3.acquire();
|
|
475
|
+
camera.getWorldPosition(cameraPos);
|
|
476
|
+
const direction = globalPools.vector3.acquire();
|
|
477
|
+
direction.copy(topWorld).sub(cameraPos).normalize();
|
|
478
|
+
const distance = topWorld.distanceTo(cameraPos);
|
|
479
|
+
raycaster.set(cameraPos, direction);
|
|
480
|
+
raycaster.far = distance;
|
|
481
|
+
const intersects = raycaster.intersectObject(currentModel, true);
|
|
482
|
+
let occluded = false;
|
|
483
|
+
if (intersects.length > 0) {
|
|
484
|
+
for (const intersect of intersects) {
|
|
485
|
+
const tolerance = distance * 0.01;
|
|
486
|
+
if (intersect.object !== object && intersect.distance < distance - tolerance) {
|
|
487
|
+
occluded = true;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Cache the result and release pooled vectors
|
|
493
|
+
occlusionCache.set(labelData, occluded);
|
|
494
|
+
visible = !occluded;
|
|
495
|
+
globalPools.vector3.release(cameraPos);
|
|
496
|
+
globalPools.vector3.release(direction);
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
// Use cached occlusion result
|
|
500
|
+
const cachedOcclusion = occlusionCache.get(labelData);
|
|
501
|
+
if (cachedOcclusion !== undefined) {
|
|
502
|
+
visible = !cachedOcclusion;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
231
506
|
wrapper.style.display = visible ? 'flex' : 'none';
|
|
232
|
-
|
|
233
|
-
|
|
507
|
+
if (cfg.style === 'line' && line && dot) {
|
|
508
|
+
const svgModelX = modelX - rect.left;
|
|
509
|
+
const svgModelY = modelY - rect.top;
|
|
510
|
+
const svgLabelX = labelX - rect.left;
|
|
511
|
+
// Calculate label connection point (approximate center-bottom/side of the label wrapper)
|
|
512
|
+
// Since it's translated -50%, -100%, the anchor point (labelX, labelY) is the BOTTOM CENTER of the wrapper.
|
|
513
|
+
// The line should go to this point.
|
|
514
|
+
const svgLabelY = labelY - rect.top;
|
|
515
|
+
// For better visuals, maybe offset slightly up into the wrapper or just to the bottom.
|
|
516
|
+
// ModelsLabel original: labelY - rect.top + (el.getBoundingClientRect().height * 0.5)
|
|
517
|
+
// The previous logic was calculating center logic.
|
|
518
|
+
// Let's stick to the anchor point which is the "target" of the lift.
|
|
519
|
+
line.setAttribute('x1', `${svgModelX}`);
|
|
520
|
+
line.setAttribute('y1', `${svgModelY}`);
|
|
521
|
+
line.setAttribute('x2', `${svgLabelX}`);
|
|
522
|
+
line.setAttribute('y2', `${svgLabelY}`);
|
|
523
|
+
line.setAttribute('visibility', visible ? 'visible' : 'hidden');
|
|
524
|
+
dot.style.opacity = visible ? '1' : '0';
|
|
525
|
+
}
|
|
526
|
+
// Release the topNDC vector back to pool
|
|
527
|
+
globalPools.vector3.release(topNDC);
|
|
234
528
|
});
|
|
235
529
|
rafId = requestAnimationFrame(updateLabels);
|
|
236
530
|
};
|
|
@@ -253,6 +547,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
253
547
|
cancelAnimationFrame(rafId);
|
|
254
548
|
rafId = null;
|
|
255
549
|
}
|
|
550
|
+
// Optional: Hide labels when paused? Original implementation didn't enforce hiding, just stopped updating.
|
|
256
551
|
},
|
|
257
552
|
// Resume update
|
|
258
553
|
resume() {
|
|
@@ -269,11 +564,378 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
|
|
|
269
564
|
rafId = null;
|
|
270
565
|
}
|
|
271
566
|
clearLabels();
|
|
272
|
-
svg
|
|
567
|
+
if (svg)
|
|
568
|
+
svg.remove();
|
|
273
569
|
container.remove();
|
|
274
570
|
},
|
|
571
|
+
isRunning() {
|
|
572
|
+
return !isPaused;
|
|
573
|
+
}
|
|
275
574
|
};
|
|
276
575
|
}
|
|
277
576
|
|
|
278
|
-
|
|
577
|
+
/**
|
|
578
|
+
* @file performanceStats.ts
|
|
579
|
+
* @description
|
|
580
|
+
* Real-time performance monitoring overlay for Three.js applications.
|
|
581
|
+
* Displays FPS, memory usage, draw calls, and performance warnings.
|
|
582
|
+
*
|
|
583
|
+
* @best-practice
|
|
584
|
+
* - Create once during initialization
|
|
585
|
+
* - Call update() in your animation loop
|
|
586
|
+
* - Use minimal styling for low performance impact
|
|
587
|
+
*
|
|
588
|
+
* @performance
|
|
589
|
+
* - Uses requestAnimationFrame for efficient updates
|
|
590
|
+
* - DOM updates are batched and throttled
|
|
591
|
+
* - Minimal memory footprint
|
|
592
|
+
*/
|
|
593
|
+
/**
|
|
594
|
+
* Performance Stats Monitor
|
|
595
|
+
* Lightweight FPS and memory monitoring overlay
|
|
596
|
+
*/
|
|
597
|
+
class PerformanceStats {
|
|
598
|
+
constructor(options = {}) {
|
|
599
|
+
this.frames = 0;
|
|
600
|
+
this.lastTime = performance.now();
|
|
601
|
+
this.fps = 60;
|
|
602
|
+
this.fpsHistory = [];
|
|
603
|
+
this.maxHistoryLength = 60;
|
|
604
|
+
this.lastUpdateTime = 0;
|
|
605
|
+
this.warnings = [];
|
|
606
|
+
this.maxWarnings = 3;
|
|
607
|
+
this.enabled = true;
|
|
608
|
+
this.isVisible = true;
|
|
609
|
+
this.options = {
|
|
610
|
+
position: options.position || 'top-left',
|
|
611
|
+
updateInterval: options.updateInterval || 500,
|
|
612
|
+
enableMemoryTracking: options.enableMemoryTracking ?? true,
|
|
613
|
+
enableWarnings: options.enableWarnings ?? true,
|
|
614
|
+
renderer: options.renderer || null,
|
|
615
|
+
fpsWarningThreshold: options.fpsWarningThreshold || 30,
|
|
616
|
+
memoryWarningThreshold: options.memoryWarningThreshold || 200
|
|
617
|
+
};
|
|
618
|
+
this.updateInterval = this.options.updateInterval;
|
|
619
|
+
// Create container
|
|
620
|
+
this.container = document.createElement('div');
|
|
621
|
+
this.container.className = 'tm-performance-stats';
|
|
622
|
+
this.setPosition(this.options.position);
|
|
623
|
+
// Create FPS display
|
|
624
|
+
const fpsLabel = document.createElement('div');
|
|
625
|
+
fpsLabel.className = 'tm-perf-row';
|
|
626
|
+
fpsLabel.innerHTML = '<span class="tm-perf-label">FPS:</span> <span class="tm-perf-value" id="tm-fps">60</span>';
|
|
627
|
+
this.fpsElement = fpsLabel.querySelector('#tm-fps');
|
|
628
|
+
this.container.appendChild(fpsLabel);
|
|
629
|
+
// Create memory display
|
|
630
|
+
if (this.options.enableMemoryTracking && performance.memory) {
|
|
631
|
+
const memLabel = document.createElement('div');
|
|
632
|
+
memLabel.className = 'tm-perf-row';
|
|
633
|
+
memLabel.innerHTML = '<span class="tm-perf-label">Memory:</span> <span class="tm-perf-value" id="tm-mem">0 MB</span>';
|
|
634
|
+
this.memoryElement = memLabel.querySelector('#tm-mem');
|
|
635
|
+
this.container.appendChild(memLabel);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
this.memoryElement = document.createElement('span');
|
|
639
|
+
}
|
|
640
|
+
// Create draw calls display
|
|
641
|
+
if (this.options.renderer) {
|
|
642
|
+
const drawLabel = document.createElement('div');
|
|
643
|
+
drawLabel.className = 'tm-perf-row';
|
|
644
|
+
drawLabel.innerHTML = '<span class="tm-perf-label">Draw Calls:</span> <span class="tm-perf-value" id="tm-draw">0</span>';
|
|
645
|
+
this.drawCallsElement = drawLabel.querySelector('#tm-draw');
|
|
646
|
+
this.container.appendChild(drawLabel);
|
|
647
|
+
const triLabel = document.createElement('div');
|
|
648
|
+
triLabel.className = 'tm-perf-row';
|
|
649
|
+
triLabel.innerHTML = '<span class="tm-perf-label">Triangles:</span> <span class="tm-perf-value" id="tm-tri">0</span>';
|
|
650
|
+
this.trianglesElement = triLabel.querySelector('#tm-tri');
|
|
651
|
+
this.container.appendChild(triLabel);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
this.drawCallsElement = document.createElement('span');
|
|
655
|
+
this.trianglesElement = document.createElement('span');
|
|
656
|
+
}
|
|
657
|
+
// Create warnings container
|
|
658
|
+
if (this.options.enableWarnings) {
|
|
659
|
+
this.warningsContainer = document.createElement('div');
|
|
660
|
+
this.warningsContainer.className = 'tm-perf-warnings';
|
|
661
|
+
this.container.appendChild(this.warningsContainer);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
this.warningsContainer = document.createElement('div');
|
|
665
|
+
}
|
|
666
|
+
// Inject styles
|
|
667
|
+
this.injectStyles();
|
|
668
|
+
// Append to body
|
|
669
|
+
document.body.appendChild(this.container);
|
|
670
|
+
}
|
|
671
|
+
setPosition(position) {
|
|
672
|
+
const positions = {
|
|
673
|
+
'top-left': { top: '10px', left: '10px' },
|
|
674
|
+
'top-right': { top: '10px', right: '10px' },
|
|
675
|
+
'bottom-left': { bottom: '10px', left: '10px' },
|
|
676
|
+
'bottom-right': { bottom: '10px', right: '10px' }
|
|
677
|
+
};
|
|
678
|
+
const pos = positions[position] || positions['top-left'];
|
|
679
|
+
Object.assign(this.container.style, {
|
|
680
|
+
position: 'fixed',
|
|
681
|
+
zIndex: '99999',
|
|
682
|
+
...pos
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
injectStyles() {
|
|
686
|
+
const styleId = 'tm-performance-stats-styles';
|
|
687
|
+
if (document.getElementById(styleId))
|
|
688
|
+
return;
|
|
689
|
+
const style = document.createElement('style');
|
|
690
|
+
style.id = styleId;
|
|
691
|
+
style.innerHTML = `
|
|
692
|
+
.tm-performance-stats {
|
|
693
|
+
background: rgba(0, 0, 0, 0.8);
|
|
694
|
+
color: #0f0;
|
|
695
|
+
font-family: 'Courier New', monospace;
|
|
696
|
+
font-size: 12px;
|
|
697
|
+
padding: 10px;
|
|
698
|
+
border-radius: 4px;
|
|
699
|
+
min-width: 180px;
|
|
700
|
+
backdrop-filter: blur(4px);
|
|
701
|
+
user-select: none;
|
|
702
|
+
pointer-events: none;
|
|
703
|
+
}
|
|
704
|
+
.tm-perf-row {
|
|
705
|
+
display: flex;
|
|
706
|
+
justify-content: space-between;
|
|
707
|
+
margin-bottom: 4px;
|
|
708
|
+
}
|
|
709
|
+
.tm-perf-label {
|
|
710
|
+
color: #888;
|
|
711
|
+
}
|
|
712
|
+
.tm-perf-value {
|
|
713
|
+
color: #0f0;
|
|
714
|
+
font-weight: bold;
|
|
715
|
+
}
|
|
716
|
+
.tm-perf-value.warning {
|
|
717
|
+
color: #ff0;
|
|
718
|
+
}
|
|
719
|
+
.tm-perf-value.critical {
|
|
720
|
+
color: #f00;
|
|
721
|
+
}
|
|
722
|
+
.tm-perf-warnings {
|
|
723
|
+
margin-top: 8px;
|
|
724
|
+
padding-top: 8px;
|
|
725
|
+
border-top: 1px solid #333;
|
|
726
|
+
}
|
|
727
|
+
.tm-perf-warning {
|
|
728
|
+
font-size: 10px;
|
|
729
|
+
padding: 4px;
|
|
730
|
+
margin-bottom: 4px;
|
|
731
|
+
border-radius: 2px;
|
|
732
|
+
}
|
|
733
|
+
.tm-perf-warning.info {
|
|
734
|
+
background: rgba(0, 128, 255, 0.2);
|
|
735
|
+
color: #0af;
|
|
736
|
+
}
|
|
737
|
+
.tm-perf-warning.warning {
|
|
738
|
+
background: rgba(255, 200, 0, 0.2);
|
|
739
|
+
color: #fc0;
|
|
740
|
+
}
|
|
741
|
+
.tm-perf-warning.critical {
|
|
742
|
+
background: rgba(255, 0, 0, 0.2);
|
|
743
|
+
color: #f66;
|
|
744
|
+
}
|
|
745
|
+
`;
|
|
746
|
+
document.head.appendChild(style);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Updates the performance statistics. This method must be called within the application's animation loop.
|
|
750
|
+
*/
|
|
751
|
+
update() {
|
|
752
|
+
if (!this.enabled)
|
|
753
|
+
return;
|
|
754
|
+
const now = performance.now();
|
|
755
|
+
this.frames++;
|
|
756
|
+
// Calculate FPS
|
|
757
|
+
const delta = now - this.lastTime;
|
|
758
|
+
if (delta >= 1000) {
|
|
759
|
+
this.fps = Math.round((this.frames * 1000) / delta);
|
|
760
|
+
this.fpsHistory.push(this.fps);
|
|
761
|
+
if (this.fpsHistory.length > this.maxHistoryLength) {
|
|
762
|
+
this.fpsHistory.shift();
|
|
763
|
+
}
|
|
764
|
+
this.frames = 0;
|
|
765
|
+
this.lastTime = now;
|
|
766
|
+
}
|
|
767
|
+
// Throttle DOM updates
|
|
768
|
+
if (now - this.lastUpdateTime < this.updateInterval) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
this.lastUpdateTime = now;
|
|
772
|
+
// Update FPS display
|
|
773
|
+
this.fpsElement.textContent = this.fps.toString();
|
|
774
|
+
this.fpsElement.className = 'tm-perf-value';
|
|
775
|
+
if (this.fps < this.options.fpsWarningThreshold) {
|
|
776
|
+
this.fpsElement.classList.add('critical');
|
|
777
|
+
this.addWarning({
|
|
778
|
+
type: 'low-fps',
|
|
779
|
+
message: `Low FPS: ${this.fps}`,
|
|
780
|
+
severity: 'critical',
|
|
781
|
+
timestamp: now
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
else if (this.fps < this.options.fpsWarningThreshold + 10) {
|
|
785
|
+
this.fpsElement.classList.add('warning');
|
|
786
|
+
}
|
|
787
|
+
// Update memory display
|
|
788
|
+
if (this.options.enableMemoryTracking && performance.memory) {
|
|
789
|
+
const memory = performance.memory;
|
|
790
|
+
const usedMB = Math.round(memory.usedJSHeapSize / 1048576);
|
|
791
|
+
const totalMB = Math.round(memory.jsHeapSizeLimit / 1048576);
|
|
792
|
+
this.memoryElement.textContent = `${usedMB}/${totalMB} MB`;
|
|
793
|
+
this.memoryElement.className = 'tm-perf-value';
|
|
794
|
+
if (usedMB > this.options.memoryWarningThreshold) {
|
|
795
|
+
this.memoryElement.classList.add('warning');
|
|
796
|
+
this.addWarning({
|
|
797
|
+
type: 'high-memory',
|
|
798
|
+
message: `High memory: ${usedMB}MB`,
|
|
799
|
+
severity: 'warning',
|
|
800
|
+
timestamp: now
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// Update renderer stats
|
|
805
|
+
if (this.options.renderer) {
|
|
806
|
+
const info = this.options.renderer.info;
|
|
807
|
+
this.drawCallsElement.textContent = info.render.calls.toString();
|
|
808
|
+
this.trianglesElement.textContent = this.formatNumber(info.render.triangles);
|
|
809
|
+
if (info.render.calls > 100) {
|
|
810
|
+
this.drawCallsElement.className = 'tm-perf-value warning';
|
|
811
|
+
this.addWarning({
|
|
812
|
+
type: 'excessive-drawcalls',
|
|
813
|
+
message: `Draw calls: ${info.render.calls}`,
|
|
814
|
+
severity: 'info',
|
|
815
|
+
timestamp: now
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
this.drawCallsElement.className = 'tm-perf-value';
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// Update warnings display
|
|
823
|
+
if (this.options.enableWarnings) {
|
|
824
|
+
this.updateWarningsDisplay();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
formatNumber(num) {
|
|
828
|
+
if (num >= 1000000)
|
|
829
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
830
|
+
if (num >= 1000)
|
|
831
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
832
|
+
return num.toString();
|
|
833
|
+
}
|
|
834
|
+
addWarning(warning) {
|
|
835
|
+
// Don't add duplicate warnings within 5 seconds
|
|
836
|
+
const isDuplicate = this.warnings.some(w => w.type === warning.type && (warning.timestamp - w.timestamp < 5000));
|
|
837
|
+
if (isDuplicate)
|
|
838
|
+
return;
|
|
839
|
+
this.warnings.push(warning);
|
|
840
|
+
if (this.warnings.length > this.maxWarnings) {
|
|
841
|
+
this.warnings.shift();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
updateWarningsDisplay() {
|
|
845
|
+
if (!this.warningsContainer)
|
|
846
|
+
return;
|
|
847
|
+
// Remove old warnings (older than 10 seconds)
|
|
848
|
+
const now = performance.now();
|
|
849
|
+
this.warnings = this.warnings.filter(w => now - w.timestamp < 10000);
|
|
850
|
+
this.warningsContainer.innerHTML = '';
|
|
851
|
+
this.warnings.forEach(warning => {
|
|
852
|
+
const el = document.createElement('div');
|
|
853
|
+
el.className = `tm-perf-warning ${warning.severity}`;
|
|
854
|
+
el.textContent = warning.message;
|
|
855
|
+
this.warningsContainer.appendChild(el);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Gets the current frames per second (FPS).
|
|
860
|
+
* @returns {number} The current FPS.
|
|
861
|
+
*/
|
|
862
|
+
getFPS() {
|
|
863
|
+
return this.fps;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Gets the average FPS over the recent history period.
|
|
867
|
+
* @returns {number} The average FPS.
|
|
868
|
+
*/
|
|
869
|
+
getAverageFPS() {
|
|
870
|
+
if (this.fpsHistory.length === 0)
|
|
871
|
+
return 60;
|
|
872
|
+
return Math.round(this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Returns a snapshot of all tracked performance metrics.
|
|
876
|
+
* @returns {object} Current performance statistics.
|
|
877
|
+
*/
|
|
878
|
+
getStats() {
|
|
879
|
+
const stats = {
|
|
880
|
+
fps: this.fps,
|
|
881
|
+
averageFPS: this.getAverageFPS()
|
|
882
|
+
};
|
|
883
|
+
if (this.options.enableMemoryTracking && performance.memory) {
|
|
884
|
+
const memory = performance.memory;
|
|
885
|
+
stats.memory = {
|
|
886
|
+
used: Math.round(memory.usedJSHeapSize / 1048576),
|
|
887
|
+
total: Math.round(memory.jsHeapSizeLimit / 1048576)
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (this.options.renderer) {
|
|
891
|
+
const info = this.options.renderer.info;
|
|
892
|
+
stats.drawCalls = info.render.calls;
|
|
893
|
+
stats.triangles = info.render.triangles;
|
|
894
|
+
}
|
|
895
|
+
return stats;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Toggles the visibility of the performance stats overlay.
|
|
899
|
+
*/
|
|
900
|
+
toggle() {
|
|
901
|
+
this.isVisible = !this.isVisible;
|
|
902
|
+
this.container.style.display = this.isVisible ? 'block' : 'none';
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Shows the performance stats overlay.
|
|
906
|
+
*/
|
|
907
|
+
show() {
|
|
908
|
+
this.isVisible = true;
|
|
909
|
+
this.container.style.display = 'block';
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Hides the performance stats overlay.
|
|
913
|
+
*/
|
|
914
|
+
hide() {
|
|
915
|
+
this.isVisible = false;
|
|
916
|
+
this.container.style.display = 'none';
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Enables or disables performance statistics collection.
|
|
920
|
+
* @param {boolean} enabled - Whether collection should be enabled.
|
|
921
|
+
*/
|
|
922
|
+
setEnabled(enabled) {
|
|
923
|
+
this.enabled = enabled;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Disposes of the performance monitor and removes its DOM elements.
|
|
927
|
+
*/
|
|
928
|
+
dispose() {
|
|
929
|
+
this.container.remove();
|
|
930
|
+
this.enabled = false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Helper function to create performance monitor
|
|
935
|
+
*/
|
|
936
|
+
function createPerformanceMonitor(options) {
|
|
937
|
+
return new PerformanceStats(options);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export { PerformanceStats, createModelsLabel, createPerformanceMonitor };
|
|
279
941
|
//# sourceMappingURL=index.mjs.map
|