@myned-ai/gsplat-flame-avatar-renderer 1.0.5 → 1.0.6

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Logger - Structured logging system
3
+ *
4
+ * Provides leveled logging with optional output suppression for production.
5
+ * Replaces direct console.log calls for better control and debugging.
6
+ */
7
+
8
+ /**
9
+ * Logger levels in order of severity
10
+ * Note: Named LoggerLevel to avoid conflict with existing LogLevel enum
11
+ */
12
+ export const LoggerLevel = Object.freeze({
13
+ DEBUG: 0,
14
+ INFO: 1,
15
+ WARN: 2,
16
+ ERROR: 3,
17
+ NONE: 4
18
+ });
19
+
20
+ /**
21
+ * Logger class for structured application logging
22
+ */
23
+ export class Logger {
24
+ /**
25
+ * Create a Logger instance
26
+ * @param {string} namespace - Logger namespace (e.g., 'Renderer', 'Loader')
27
+ * @param {number} [minLevel=LoggerLevel.INFO] - Minimum log level to output
28
+ */
29
+ constructor(namespace, minLevel = LoggerLevel.INFO) {
30
+ this.namespace = namespace;
31
+ this.minLevel = minLevel;
32
+ }
33
+
34
+ /**
35
+ * Set minimum log level
36
+ * @param {number} level - Minimum level from LoggerLevel enum
37
+ */
38
+ setLevel(level) {
39
+ this.minLevel = level;
40
+ }
41
+
42
+ /**
43
+ * Format log message with namespace and timestamp
44
+ * @private
45
+ * @param {string} level - Log level name
46
+ * @param {Array} args - Log arguments
47
+ * @returns {Array} Formatted arguments
48
+ */
49
+ _format(level, args) {
50
+ const timestamp = new Date().toISOString();
51
+ const prefix = `[${timestamp}] [${level}] [${this.namespace}]`;
52
+ return [prefix, ...args];
53
+ }
54
+
55
+ /**
56
+ * Log debug message (most verbose)
57
+ * @param {...*} args - Arguments to log
58
+ */
59
+ debug(...args) {
60
+ if (this.minLevel <= LoggerLevel.DEBUG) {
61
+ console.debug(...this._format('DEBUG', args));
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Log info message
67
+ * @param {...*} args - Arguments to log
68
+ */
69
+ info(...args) {
70
+ if (this.minLevel <= LoggerLevel.INFO) {
71
+ console.info(...this._format('INFO', args));
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Log warning message
77
+ * @param {...*} args - Arguments to log
78
+ */
79
+ warn(...args) {
80
+ if (this.minLevel <= LoggerLevel.WARN) {
81
+ console.warn(...this._format('WARN', args));
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Log error message
87
+ * @param {...*} args - Arguments to log
88
+ */
89
+ error(...args) {
90
+ if (this.minLevel <= LoggerLevel.ERROR) {
91
+ console.error(...this._format('ERROR', args));
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Log error with stack trace
97
+ * @param {Error} error - Error object
98
+ * @param {string} [context] - Additional context
99
+ */
100
+ errorWithTrace(error, context = '') {
101
+ if (this.minLevel <= LoggerLevel.ERROR) {
102
+ const contextStr = context ? ` Context: ${context}` : '';
103
+ console.error(...this._format('ERROR', [
104
+ `${error.message}${contextStr}`,
105
+ '\nStack:', error.stack
106
+ ]));
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Create a child logger with extended namespace
112
+ * @param {string} childNamespace - Child namespace to append
113
+ * @returns {Logger} New logger with combined namespace
114
+ */
115
+ child(childNamespace) {
116
+ return new Logger(`${this.namespace}:${childNamespace}`, this.minLevel);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Global logger registry
122
+ * @private
123
+ */
124
+ const loggers = new Map();
125
+
126
+ /**
127
+ * Global log level (affects all new loggers)
128
+ * @private
129
+ */
130
+ let globalLogLevel = LoggerLevel.INFO;
131
+
132
+ /**
133
+ * Set global log level for all loggers
134
+ * @param {number} level - Log level from LoggerLevel enum
135
+ */
136
+ export function setGlobalLogLevel(level) {
137
+ globalLogLevel = level;
138
+ // Update existing loggers
139
+ for (const logger of loggers.values()) {
140
+ logger.setLevel(level);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Get or create a logger for a namespace
146
+ * @param {string} namespace - Logger namespace
147
+ * @returns {Logger} Logger instance
148
+ */
149
+ export function getLogger(namespace) {
150
+ if (!loggers.has(namespace)) {
151
+ loggers.set(namespace, new Logger(namespace, globalLogLevel));
152
+ }
153
+ return loggers.get(namespace);
154
+ }
155
+
156
+ /**
157
+ * Configure logging for production (suppress all logs)
158
+ */
159
+ export function configureForProduction() {
160
+ setGlobalLogLevel(LoggerLevel.NONE);
161
+ }
162
+
163
+ /**
164
+ * Configure logging for development (show all logs)
165
+ */
166
+ export function configureForDevelopment() {
167
+ setGlobalLogLevel(LoggerLevel.DEBUG);
168
+ }
169
+
170
+ // Export default logger for convenience
171
+ export default getLogger('App');
@@ -0,0 +1,248 @@
1
+ /**
2
+ * ObjectPool - Memory-efficient object pooling
3
+ *
4
+ * Reduces garbage collection pressure by reusing objects instead of
5
+ * repeatedly allocating and deallocating them. Critical for real-time rendering.
6
+ */
7
+
8
+ import { Vector3, Matrix4, Quaternion, Euler } from 'three';
9
+
10
+ /**
11
+ * Generic object pool
12
+ */
13
+ export class ObjectPool {
14
+ /**
15
+ * Create an ObjectPool
16
+ * @param {Function} factory - Function that creates new objects
17
+ * @param {Function} reset - Function that resets an object to initial state
18
+ * @param {number} [initialSize=10] - Number of objects to pre-allocate
19
+ */
20
+ constructor(factory, reset, initialSize = 10) {
21
+ this._factory = factory;
22
+ this._reset = reset;
23
+ this._pool = [];
24
+ this._allocated = 0;
25
+ this._maxSize = initialSize * 10; // Prevent unbounded growth
26
+
27
+ // Pre-allocate initial objects
28
+ for (let i = 0; i < initialSize; i++) {
29
+ this._pool.push(factory());
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Acquire an object from the pool
35
+ * @returns {*} Pooled object
36
+ */
37
+ acquire() {
38
+ this._allocated++;
39
+ if (this._pool.length > 0) {
40
+ return this._pool.pop();
41
+ }
42
+ // Pool exhausted, create new object
43
+ return this._factory();
44
+ }
45
+
46
+ /**
47
+ * Release an object back to the pool
48
+ * @param {*} obj - Object to return to pool
49
+ */
50
+ release(obj) {
51
+ this._allocated--;
52
+ if (this._pool.length < this._maxSize) {
53
+ this._reset(obj);
54
+ this._pool.push(obj);
55
+ }
56
+ // If pool is at max size, let object be garbage collected
57
+ }
58
+
59
+ /**
60
+ * Release multiple objects
61
+ * @param {Array} objects - Objects to return to pool
62
+ */
63
+ releaseAll(objects) {
64
+ for (const obj of objects) {
65
+ this.release(obj);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get pool statistics
71
+ * @returns {object} Pool stats
72
+ */
73
+ getStats() {
74
+ return {
75
+ available: this._pool.length,
76
+ allocated: this._allocated,
77
+ maxSize: this._maxSize
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Clear the pool and release all objects
83
+ */
84
+ dispose() {
85
+ this._pool.length = 0;
86
+ this._allocated = 0;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Pre-configured pools for common Three.js objects
92
+ */
93
+
94
+ /**
95
+ * Vector3 pool
96
+ * @type {ObjectPool}
97
+ */
98
+ export const vector3Pool = new ObjectPool(
99
+ () => new Vector3(),
100
+ (v) => v.set(0, 0, 0),
101
+ 50 // Pre-allocate 50 vectors
102
+ );
103
+
104
+ /**
105
+ * Matrix4 pool
106
+ * @type {ObjectPool}
107
+ */
108
+ export const matrix4Pool = new ObjectPool(
109
+ () => new Matrix4(),
110
+ (m) => m.identity(),
111
+ 20 // Pre-allocate 20 matrices
112
+ );
113
+
114
+ /**
115
+ * Quaternion pool
116
+ * @type {ObjectPool}
117
+ */
118
+ export const quaternionPool = new ObjectPool(
119
+ () => new Quaternion(),
120
+ (q) => q.set(0, 0, 0, 1),
121
+ 30 // Pre-allocate 30 quaternions
122
+ );
123
+
124
+ /**
125
+ * Euler pool
126
+ * @type {ObjectPool}
127
+ */
128
+ export const eulerPool = new ObjectPool(
129
+ () => new Euler(),
130
+ (e) => e.set(0, 0, 0),
131
+ 30 // Pre-allocate 30 eulers
132
+ );
133
+
134
+ /**
135
+ * Scoped pool allocation helper
136
+ *
137
+ * Automatically releases pooled objects when scope exits.
138
+ * Use with try/finally to ensure cleanup.
139
+ *
140
+ * @example
141
+ * const scope = new PoolScope();
142
+ * try {
143
+ * const v1 = scope.vector3();
144
+ * const v2 = scope.vector3();
145
+ * // Use vectors...
146
+ * } finally {
147
+ * scope.releaseAll();
148
+ * }
149
+ */
150
+ export class PoolScope {
151
+ constructor() {
152
+ this._allocated = [];
153
+ }
154
+
155
+ /**
156
+ * Acquire a Vector3 from pool
157
+ * @returns {Vector3} Pooled vector
158
+ */
159
+ vector3() {
160
+ const obj = vector3Pool.acquire();
161
+ this._allocated.push({ pool: vector3Pool, obj });
162
+ return obj;
163
+ }
164
+
165
+ /**
166
+ * Acquire a Matrix4 from pool
167
+ * @returns {Matrix4} Pooled matrix
168
+ */
169
+ matrix4() {
170
+ const obj = matrix4Pool.acquire();
171
+ this._allocated.push({ pool: matrix4Pool, obj });
172
+ return obj;
173
+ }
174
+
175
+ /**
176
+ * Acquire a Quaternion from pool
177
+ * @returns {Quaternion} Pooled quaternion
178
+ */
179
+ quaternion() {
180
+ const obj = quaternionPool.acquire();
181
+ this._allocated.push({ pool: quaternionPool, obj });
182
+ return obj;
183
+ }
184
+
185
+ /**
186
+ * Acquire an Euler from pool
187
+ * @returns {Euler} Pooled euler
188
+ */
189
+ euler() {
190
+ const obj = eulerPool.acquire();
191
+ this._allocated.push({ pool: eulerPool, obj });
192
+ return obj;
193
+ }
194
+
195
+ /**
196
+ * Release all objects allocated in this scope
197
+ */
198
+ releaseAll() {
199
+ for (const { pool, obj } of this._allocated) {
200
+ pool.release(obj);
201
+ }
202
+ this._allocated.length = 0;
203
+ }
204
+
205
+ /**
206
+ * Get count of allocated objects in this scope
207
+ * @returns {number} Number of allocated objects
208
+ */
209
+ getAllocatedCount() {
210
+ return this._allocated.length;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Module-level temporary objects for hot-path reuse
216
+ * IMPORTANT: These are NOT thread-safe. Only use in single-threaded contexts.
217
+ * Reset these before use to avoid stale data.
218
+ */
219
+ export const tempVector3A = new Vector3();
220
+ export const tempVector3B = new Vector3();
221
+ export const tempVector3C = new Vector3();
222
+ export const tempMatrix4A = new Matrix4();
223
+ export const tempMatrix4B = new Matrix4();
224
+ export const tempQuaternionA = new Quaternion();
225
+ export const tempQuaternionB = new Quaternion();
226
+
227
+ /**
228
+ * Get pool statistics for all pre-configured pools
229
+ * @returns {object} Statistics for all pools
230
+ */
231
+ export function getPoolStats() {
232
+ return {
233
+ vector3: vector3Pool.getStats(),
234
+ matrix4: matrix4Pool.getStats(),
235
+ quaternion: quaternionPool.getStats(),
236
+ euler: eulerPool.getStats()
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Dispose all pre-configured pools
242
+ */
243
+ export function disposeAllPools() {
244
+ vector3Pool.dispose();
245
+ matrix4Pool.dispose();
246
+ quaternionPool.dispose();
247
+ eulerPool.dispose();
248
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * RenderLoop - Frame-independent animation loop with budget management
3
+ *
4
+ * Provides a robust requestAnimationFrame loop with:
5
+ * - Delta time calculation for frame-independent updates
6
+ * - Frame budget management to prevent frame drops
7
+ * - Deferred task execution
8
+ * - Performance monitoring
9
+ */
10
+
11
+ import { getLogger } from './Logger.js';
12
+
13
+ const logger = getLogger('RenderLoop');
14
+
15
+ /**
16
+ * RenderLoop - Manages animation frame loop
17
+ */
18
+ export class RenderLoop {
19
+ /**
20
+ * Create a RenderLoop
21
+ * @param {Function} updateFn - Update function called each frame with deltaTime
22
+ * @param {Function} renderFn - Render function called each frame
23
+ * @param {object} [options] - Configuration options
24
+ * @param {number} [options.targetFps=60] - Target frames per second
25
+ * @param {number} [options.maxDeltaTime=0.1] - Maximum delta time in seconds (prevents spiral of death)
26
+ */
27
+ constructor(updateFn, renderFn, options = {}) {
28
+ this._update = updateFn;
29
+ this._render = renderFn;
30
+
31
+ this._targetFps = options.targetFps || 60;
32
+ this._maxDeltaTime = options.maxDeltaTime || 0.1; // 100ms max
33
+ this._frameBudget = 1000 / this._targetFps; // ms per frame
34
+
35
+ this._running = false;
36
+ this._rafId = null;
37
+ this._lastTime = 0;
38
+ this._frameCount = 0;
39
+ this._deferredTasks = [];
40
+
41
+ // Performance tracking
42
+ this._fpsHistory = [];
43
+ this._fpsHistorySize = 60; // Track last 60 frames
44
+ this._lastFpsUpdate = 0;
45
+ this._currentFps = 0;
46
+ }
47
+
48
+ /**
49
+ * Start the render loop
50
+ */
51
+ start() {
52
+ if (this._running) {
53
+ logger.warn('RenderLoop already running');
54
+ return;
55
+ }
56
+
57
+ this._running = true;
58
+ this._lastTime = performance.now();
59
+ this._frameCount = 0;
60
+ logger.info('RenderLoop started');
61
+
62
+ this._tick();
63
+ }
64
+
65
+ /**
66
+ * Stop the render loop
67
+ */
68
+ stop() {
69
+ if (!this._running) {
70
+ return;
71
+ }
72
+
73
+ this._running = false;
74
+
75
+ if (this._rafId !== null) {
76
+ cancelAnimationFrame(this._rafId);
77
+ this._rafId = null;
78
+ }
79
+
80
+ logger.info(`RenderLoop stopped after ${this._frameCount} frames`);
81
+ }
82
+
83
+ /**
84
+ * Main loop tick
85
+ * @private
86
+ */
87
+ _tick = () => {
88
+ if (!this._running) {
89
+ return;
90
+ }
91
+
92
+ const frameStart = performance.now();
93
+ const rawDeltaTime = (frameStart - this._lastTime) / 1000; // Convert to seconds
94
+
95
+ // Clamp delta time to prevent spiral of death
96
+ const deltaTime = Math.min(rawDeltaTime, this._maxDeltaTime);
97
+
98
+ this._lastTime = frameStart;
99
+ this._frameCount++;
100
+
101
+ try {
102
+ // Update logic
103
+ this._update(deltaTime);
104
+
105
+ // Render
106
+ this._render();
107
+
108
+ // Process deferred tasks if time permits
109
+ const frameElapsed = performance.now() - frameStart;
110
+ const remainingTime = this._frameBudget - frameElapsed;
111
+
112
+ if (remainingTime > 1 && this._deferredTasks.length > 0) {
113
+ this._processDeferredTasks(remainingTime - 1); // Leave 1ms margin
114
+ }
115
+
116
+ // Update FPS tracking
117
+ this._updateFpsTracking(performance.now() - frameStart);
118
+
119
+ } catch (error) {
120
+ logger.error('Error in render loop:', error);
121
+ // Continue loop despite error
122
+ }
123
+
124
+ // Schedule next frame
125
+ this._rafId = requestAnimationFrame(this._tick);
126
+ };
127
+
128
+ /**
129
+ * Update FPS tracking
130
+ * @private
131
+ * @param {number} frameTime - Time taken for this frame in ms
132
+ */
133
+ _updateFpsTracking(frameTime) {
134
+ this._fpsHistory.push(1000 / frameTime);
135
+
136
+ if (this._fpsHistory.length > this._fpsHistorySize) {
137
+ this._fpsHistory.shift();
138
+ }
139
+
140
+ // Update FPS every second
141
+ const now = performance.now();
142
+ if (now - this._lastFpsUpdate > 1000) {
143
+ this._currentFps = this._fpsHistory.reduce((a, b) => a + b, 0) / this._fpsHistory.length;
144
+ this._lastFpsUpdate = now;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Process deferred tasks within time budget
150
+ * @private
151
+ * @param {number} maxTime - Maximum time in ms to spend on tasks
152
+ */
153
+ _processDeferredTasks(maxTime) {
154
+ const startTime = performance.now();
155
+
156
+ while (this._deferredTasks.length > 0) {
157
+ if (performance.now() - startTime >= maxTime) {
158
+ break;
159
+ }
160
+
161
+ const task = this._deferredTasks.shift();
162
+
163
+ try {
164
+ task.fn();
165
+ } catch (error) {
166
+ logger.error(`Error in deferred task: ${task.label}`, error);
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Execute task if within frame budget, otherwise defer
173
+ *
174
+ * @param {Function} task - Task function to execute
175
+ * @param {number} [priority=0] - Task priority (higher = more important)
176
+ * @param {string} [label=''] - Task label for debugging
177
+ */
178
+ executeOrDefer(task, priority = 0, label = '') {
179
+ const frameElapsed = performance.now() - this._lastTime;
180
+
181
+ if (frameElapsed < this._frameBudget * 0.8) {
182
+ // Within budget, execute now
183
+ task();
184
+ } else {
185
+ // Over budget, defer
186
+ this._deferredTasks.push({ fn: task, priority, label });
187
+
188
+ // Sort by priority (higher first)
189
+ this._deferredTasks.sort((a, b) => b.priority - a.priority);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Get current FPS
195
+ * @returns {number} Average FPS over recent frames
196
+ */
197
+ getFps() {
198
+ return Math.round(this._currentFps);
199
+ }
200
+
201
+ /**
202
+ * Get performance stats
203
+ * @returns {object} Performance statistics
204
+ */
205
+ getStats() {
206
+ return {
207
+ fps: this.getFps(),
208
+ frameCount: this._frameCount,
209
+ deferredTaskCount: this._deferredTasks.length,
210
+ running: this._running
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Check if loop is running
216
+ * @returns {boolean} True if running
217
+ */
218
+ isRunning() {
219
+ return this._running;
220
+ }
221
+
222
+ /**
223
+ * Get frame count
224
+ * @returns {number} Total frames rendered
225
+ */
226
+ getFrameCount() {
227
+ return this._frameCount;
228
+ }
229
+
230
+ /**
231
+ * Clear all deferred tasks
232
+ */
233
+ clearDeferredTasks() {
234
+ this._deferredTasks.length = 0;
235
+ logger.debug('Cleared all deferred tasks');
236
+ }
237
+ }
238
+
239
+ /**
240
+ * FrameBudgetMonitor - Monitors and alerts on frame budget violations
241
+ */
242
+ export class FrameBudgetMonitor {
243
+ /**
244
+ * Create a FrameBudgetMonitor
245
+ * @param {number} [targetFps=60] - Target FPS
246
+ * @param {Function} [onViolation] - Callback when budget is violated
247
+ */
248
+ constructor(targetFps = 60, onViolation = null) {
249
+ this._targetFps = targetFps;
250
+ this._frameBudget = 1000 / targetFps;
251
+ this._onViolation = onViolation;
252
+ this._violations = 0;
253
+ this._frameStart = 0;
254
+ }
255
+
256
+ /**
257
+ * Mark start of frame
258
+ */
259
+ startFrame() {
260
+ this._frameStart = performance.now();
261
+ }
262
+
263
+ /**
264
+ * Check if frame is within budget
265
+ * @param {string} [location] - Location identifier for debugging
266
+ * @returns {boolean} True if within budget
267
+ */
268
+ checkBudget(location = '') {
269
+ const elapsed = performance.now() - this._frameStart;
270
+ const withinBudget = elapsed < this._frameBudget;
271
+
272
+ if (!withinBudget) {
273
+ this._violations++;
274
+
275
+ if (this._onViolation) {
276
+ this._onViolation({
277
+ location,
278
+ elapsed,
279
+ budget: this._frameBudget,
280
+ overrun: elapsed - this._frameBudget
281
+ });
282
+ }
283
+
284
+ logger.warn(`Frame budget violation at ${location}: ${elapsed.toFixed(2)}ms / ${this._frameBudget.toFixed(2)}ms`);
285
+ }
286
+
287
+ return withinBudget;
288
+ }
289
+
290
+ /**
291
+ * Get violation count
292
+ * @returns {number} Total violations
293
+ */
294
+ getViolationCount() {
295
+ return this._violations;
296
+ }
297
+
298
+ /**
299
+ * Reset violation count
300
+ */
301
+ resetViolations() {
302
+ this._violations = 0;
303
+ }
304
+ }
305
+
306
+ export default RenderLoop;