@myned-ai/gsplat-flame-avatar-renderer 1.0.4 → 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.
- package/README.md +128 -40
- package/dist/gsplat-flame-avatar-renderer.cjs.js +3478 -722
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.cjs.min.js.map +1 -0
- package/dist/gsplat-flame-avatar-renderer.esm.js +3439 -724
- package/dist/gsplat-flame-avatar-renderer.esm.min.js +2 -0
- package/dist/gsplat-flame-avatar-renderer.esm.min.js.map +1 -0
- package/package.json +11 -14
- package/src/core/SplatMesh.js +53 -46
- package/src/core/Viewer.js +47 -196
- package/src/errors/ApplicationError.js +185 -0
- package/src/errors/index.js +17 -0
- package/src/flame/FlameAnimator.js +282 -57
- package/src/loaders/PlyLoader.js +302 -44
- package/src/materials/SplatMaterial.js +13 -10
- package/src/materials/SplatMaterial3D.js +72 -27
- package/src/renderer/AnimationManager.js +8 -5
- package/src/renderer/GaussianSplatRenderer.js +668 -217
- package/src/utils/BlobUrlManager.js +294 -0
- package/src/utils/EventEmitter.js +349 -0
- package/src/utils/LoaderUtils.js +2 -1
- package/src/utils/Logger.js +171 -0
- package/src/utils/ObjectPool.js +248 -0
- package/src/utils/RenderLoop.js +306 -0
- package/src/utils/Util.js +59 -18
- package/src/utils/ValidationUtils.js +331 -0
- package/src/utils/index.js +10 -1
- package/dist/gsplat-flame-avatar-renderer.cjs.js.map +0 -1
- package/dist/gsplat-flame-avatar-renderer.esm.js.map +0 -1
package/src/utils/Util.js
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core utility functions for Gaussian Splat rendering
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Derived from @mkkellogg/gaussian-splats-3d (MIT License)
|
|
5
5
|
* https://github.com/mkkellogg/GaussianSplats3D
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Import paths adjusted for gsplat-flame-avatar package structure.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { DataUtils } from 'three';
|
|
11
|
+
import { NetworkError } from '../errors/index.js';
|
|
12
|
+
import { getLogger } from './Logger.js';
|
|
13
|
+
|
|
14
|
+
const logger = getLogger('Util');
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Custom error for aborted operations
|
|
18
|
+
* @deprecated Use NetworkError instead for consistency
|
|
14
19
|
*/
|
|
15
20
|
export class AbortedPromiseError extends Error {
|
|
16
21
|
constructor(msg) {
|
|
17
22
|
super(msg);
|
|
18
23
|
this.name = 'AbortedPromiseError';
|
|
24
|
+
this.code = 'ABORTED';
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Fetch with progress tracking using standard AbortController
|
|
24
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* @param {string} path - URL to fetch
|
|
32
|
+
* @param {Function} [onProgress] - Progress callback (percent, percentLabel, chunk, fileSize)
|
|
33
|
+
* @param {boolean} [saveChunks=true] - Whether to save and return downloaded chunks
|
|
34
|
+
* @param {object} [headers] - Optional HTTP headers
|
|
35
|
+
* @returns {Promise<ArrayBuffer>} Promise that resolves with downloaded data
|
|
36
|
+
* @throws {NetworkError} If fetch fails or is aborted
|
|
25
37
|
*/
|
|
26
38
|
export const fetchWithProgress = function(path, onProgress, saveChunks = true, headers) {
|
|
27
39
|
|
|
@@ -32,9 +44,14 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
|
|
|
32
44
|
let onProgressCalledAtComplete = false;
|
|
33
45
|
const localOnProgress = (percent, percentLabel, chunk, fileSize) => {
|
|
34
46
|
if (onProgress && !onProgressCalledAtComplete) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
try {
|
|
48
|
+
onProgress(percent, percentLabel, chunk, fileSize);
|
|
49
|
+
if (percent === 100) {
|
|
50
|
+
onProgressCalledAtComplete = true;
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Don't let progress callback errors break the download
|
|
54
|
+
logger.error('Error in progress callback:', error);
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
57
|
};
|
|
@@ -42,20 +59,33 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
|
|
|
42
59
|
const promise = new Promise((resolve, reject) => {
|
|
43
60
|
const fetchOptions = { signal };
|
|
44
61
|
if (headers) fetchOptions.headers = headers;
|
|
45
|
-
|
|
62
|
+
|
|
46
63
|
fetch(path, fetchOptions)
|
|
47
64
|
.then(async (data) => {
|
|
48
|
-
// Handle error
|
|
65
|
+
// Handle HTTP error responses
|
|
49
66
|
if (!data.ok) {
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
let errorText = '';
|
|
68
|
+
try {
|
|
69
|
+
errorText = await data.text();
|
|
70
|
+
} catch {
|
|
71
|
+
// Ignore if we can't read error text
|
|
72
|
+
}
|
|
73
|
+
reject(new NetworkError(
|
|
74
|
+
`Fetch failed: ${data.statusText}${errorText ? ' - ' + errorText : ''}`,
|
|
75
|
+
data.status
|
|
76
|
+
));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const reader = data.body?.getReader();
|
|
81
|
+
if (!reader) {
|
|
82
|
+
reject(new NetworkError('Response body is not readable', 0));
|
|
52
83
|
return;
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
const reader = data.body.getReader();
|
|
56
86
|
let bytesDownloaded = 0;
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
const _fileSize = data.headers.get('Content-Length');
|
|
88
|
+
const fileSize = _fileSize ? parseInt(_fileSize, 10) : undefined;
|
|
59
89
|
|
|
60
90
|
const chunks = [];
|
|
61
91
|
|
|
@@ -76,7 +106,7 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
|
|
|
76
106
|
let percent;
|
|
77
107
|
let percentLabel;
|
|
78
108
|
if (fileSize !== undefined) {
|
|
79
|
-
percent = bytesDownloaded / fileSize * 100;
|
|
109
|
+
percent = (bytesDownloaded / fileSize) * 100;
|
|
80
110
|
percentLabel = `${percent.toFixed(2)}%`;
|
|
81
111
|
}
|
|
82
112
|
if (saveChunks) {
|
|
@@ -84,16 +114,27 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
|
|
|
84
114
|
}
|
|
85
115
|
localOnProgress(percent, percentLabel, chunk, fileSize);
|
|
86
116
|
} catch (error) {
|
|
87
|
-
reject(
|
|
117
|
+
reject(new NetworkError(
|
|
118
|
+
`Error reading response stream: ${error.message}`,
|
|
119
|
+
0,
|
|
120
|
+
error
|
|
121
|
+
));
|
|
88
122
|
return;
|
|
89
123
|
}
|
|
90
124
|
}
|
|
91
125
|
})
|
|
92
126
|
.catch((error) => {
|
|
93
|
-
if
|
|
94
|
-
|
|
127
|
+
// Don't wrap if already a NetworkError
|
|
128
|
+
if (error instanceof NetworkError) {
|
|
129
|
+
reject(error);
|
|
130
|
+
} else if (error.name === 'AbortError') {
|
|
131
|
+
reject(new NetworkError('Fetch aborted by user', 0, error));
|
|
95
132
|
} else {
|
|
96
|
-
reject(new
|
|
133
|
+
reject(new NetworkError(
|
|
134
|
+
`Fetch failed: ${error.message || 'Unknown error'}`,
|
|
135
|
+
0,
|
|
136
|
+
error
|
|
137
|
+
));
|
|
97
138
|
}
|
|
98
139
|
});
|
|
99
140
|
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValidationUtils - Security-focused validation utilities
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL: All external inputs MUST be validated before use.
|
|
5
|
+
* This module provides allowlist-based validation following security best practices.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ValidationError } from '../errors/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Allowed URL protocols (allowlist approach)
|
|
12
|
+
*/
|
|
13
|
+
const ALLOWED_PROTOCOLS = Object.freeze(['https:', 'http:', 'blob:', 'data:']);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate and sanitize a URL
|
|
17
|
+
*
|
|
18
|
+
* @param {string} url - URL to validate
|
|
19
|
+
* @param {string} [baseURL] - Base URL for relative URLs (defaults to window.location.href)
|
|
20
|
+
* @returns {string} Sanitized absolute URL
|
|
21
|
+
* @throws {ValidationError} If URL is invalid or uses disallowed protocol
|
|
22
|
+
*/
|
|
23
|
+
export function validateUrl(url, baseURL) {
|
|
24
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
25
|
+
throw new ValidationError('URL must be a non-empty string', 'url');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
// Use base URL if provided, otherwise use window location (browser only)
|
|
31
|
+
const base = baseURL || (typeof window !== 'undefined' ? window.location.href : undefined);
|
|
32
|
+
parsed = new URL(url, base);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new ValidationError(
|
|
35
|
+
`Invalid URL format: ${url}`,
|
|
36
|
+
'url',
|
|
37
|
+
error
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate protocol against allowlist
|
|
42
|
+
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
|
|
43
|
+
throw new ValidationError(
|
|
44
|
+
`Disallowed protocol: ${parsed.protocol}. Allowed protocols: ${ALLOWED_PROTOCOLS.join(', ')}`,
|
|
45
|
+
'url.protocol'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parsed.href;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate asset path to prevent path traversal attacks
|
|
54
|
+
*
|
|
55
|
+
* @param {string} path - File path to validate
|
|
56
|
+
* @returns {string} Validated path
|
|
57
|
+
* @throws {ValidationError} If path contains traversal sequences
|
|
58
|
+
*/
|
|
59
|
+
export function validateAssetPath(path) {
|
|
60
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
61
|
+
throw new ValidationError('Asset path must be a non-empty string', 'path');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for path traversal sequences
|
|
65
|
+
const dangerousPatterns = ['../', '..\\', '%2e%2e/', '%2e%2e\\'];
|
|
66
|
+
const normalizedPath = path.toLowerCase();
|
|
67
|
+
|
|
68
|
+
for (const pattern of dangerousPatterns) {
|
|
69
|
+
if (normalizedPath.includes(pattern)) {
|
|
70
|
+
throw new ValidationError(
|
|
71
|
+
`Path traversal detected in asset path: ${path}`,
|
|
72
|
+
'path'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return path;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate file extension against allowlist
|
|
82
|
+
*
|
|
83
|
+
* @param {string} filename - Filename to validate
|
|
84
|
+
* @param {string[]} allowedExtensions - Array of allowed extensions (e.g., ['.ply', '.glb'])
|
|
85
|
+
* @returns {string} Validated filename
|
|
86
|
+
* @throws {ValidationError} If extension is not allowed
|
|
87
|
+
*/
|
|
88
|
+
export function validateFileExtension(filename, allowedExtensions) {
|
|
89
|
+
if (typeof filename !== 'string' || filename.length === 0) {
|
|
90
|
+
throw new ValidationError('Filename must be a non-empty string', 'filename');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(allowedExtensions) || allowedExtensions.length === 0) {
|
|
94
|
+
throw new ValidationError(
|
|
95
|
+
'allowedExtensions must be a non-empty array',
|
|
96
|
+
'allowedExtensions'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const extension = filename.slice(filename.lastIndexOf('.')).toLowerCase();
|
|
101
|
+
const normalizedAllowed = allowedExtensions.map(ext => ext.toLowerCase());
|
|
102
|
+
|
|
103
|
+
if (!normalizedAllowed.includes(extension)) {
|
|
104
|
+
throw new ValidationError(
|
|
105
|
+
`File extension ${extension} not allowed. Allowed: ${allowedExtensions.join(', ')}`,
|
|
106
|
+
'filename'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return filename;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate numeric value is within range
|
|
115
|
+
*
|
|
116
|
+
* @param {number} value - Value to validate
|
|
117
|
+
* @param {number} min - Minimum allowed value (inclusive)
|
|
118
|
+
* @param {number} max - Maximum allowed value (inclusive)
|
|
119
|
+
* @param {string} fieldName - Name of field for error messages
|
|
120
|
+
* @returns {number} Validated value
|
|
121
|
+
* @throws {ValidationError} If value is not a number or out of range
|
|
122
|
+
*/
|
|
123
|
+
export function validateNumberInRange(value, min, max, fieldName) {
|
|
124
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
125
|
+
throw new ValidationError(
|
|
126
|
+
`${fieldName} must be a valid number`,
|
|
127
|
+
fieldName
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (value < min || value > max) {
|
|
132
|
+
throw new ValidationError(
|
|
133
|
+
`${fieldName} must be between ${min} and ${max}, got ${value}`,
|
|
134
|
+
fieldName
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate integer value
|
|
143
|
+
*
|
|
144
|
+
* @param {number} value - Value to validate
|
|
145
|
+
* @param {string} fieldName - Name of field for error messages
|
|
146
|
+
* @returns {number} Validated integer
|
|
147
|
+
* @throws {ValidationError} If value is not an integer
|
|
148
|
+
*/
|
|
149
|
+
export function validateInteger(value, fieldName) {
|
|
150
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
151
|
+
throw new ValidationError(
|
|
152
|
+
`${fieldName} must be an integer`,
|
|
153
|
+
fieldName
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate positive integer
|
|
162
|
+
*
|
|
163
|
+
* @param {number} value - Value to validate
|
|
164
|
+
* @param {string} fieldName - Name of field for error messages
|
|
165
|
+
* @returns {number} Validated positive integer
|
|
166
|
+
* @throws {ValidationError} If value is not a positive integer
|
|
167
|
+
*/
|
|
168
|
+
export function validatePositiveInteger(value, fieldName) {
|
|
169
|
+
validateInteger(value, fieldName);
|
|
170
|
+
|
|
171
|
+
if (value <= 0) {
|
|
172
|
+
throw new ValidationError(
|
|
173
|
+
`${fieldName} must be positive, got ${value}`,
|
|
174
|
+
fieldName
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate object has required properties
|
|
183
|
+
*
|
|
184
|
+
* @param {object} obj - Object to validate
|
|
185
|
+
* @param {string[]} requiredProps - Array of required property names
|
|
186
|
+
* @param {string} objectName - Name of object for error messages
|
|
187
|
+
* @returns {object} Validated object
|
|
188
|
+
* @throws {ValidationError} If object is invalid or missing required properties
|
|
189
|
+
*/
|
|
190
|
+
export function validateRequiredProperties(obj, requiredProps, objectName) {
|
|
191
|
+
if (obj === null || typeof obj !== 'object') {
|
|
192
|
+
throw new ValidationError(
|
|
193
|
+
`${objectName} must be an object`,
|
|
194
|
+
objectName
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const prop of requiredProps) {
|
|
199
|
+
if (!(prop in obj) || obj[prop] === undefined) {
|
|
200
|
+
throw new ValidationError(
|
|
201
|
+
`${objectName} missing required property: ${prop}`,
|
|
202
|
+
`${objectName}.${prop}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return obj;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate array buffer
|
|
212
|
+
*
|
|
213
|
+
* @param {ArrayBuffer} buffer - Buffer to validate
|
|
214
|
+
* @param {string} fieldName - Name of field for error messages
|
|
215
|
+
* @param {number} [minSize=0] - Minimum size in bytes
|
|
216
|
+
* @returns {ArrayBuffer} Validated buffer
|
|
217
|
+
* @throws {ValidationError} If buffer is invalid or too small
|
|
218
|
+
*/
|
|
219
|
+
export function validateArrayBuffer(buffer, fieldName, minSize = 0) {
|
|
220
|
+
if (!(buffer instanceof ArrayBuffer)) {
|
|
221
|
+
throw new ValidationError(
|
|
222
|
+
`${fieldName} must be an ArrayBuffer`,
|
|
223
|
+
fieldName
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (buffer.byteLength < minSize) {
|
|
228
|
+
throw new ValidationError(
|
|
229
|
+
`${fieldName} must be at least ${minSize} bytes, got ${buffer.byteLength}`,
|
|
230
|
+
fieldName
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return buffer;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate enum value against allowed values
|
|
239
|
+
*
|
|
240
|
+
* @param {*} value - Value to validate
|
|
241
|
+
* @param {Array} allowedValues - Array of allowed values
|
|
242
|
+
* @param {string} fieldName - Name of field for error messages
|
|
243
|
+
* @returns {*} Validated value
|
|
244
|
+
* @throws {ValidationError} If value is not in allowed values
|
|
245
|
+
*/
|
|
246
|
+
export function validateEnum(value, allowedValues, fieldName) {
|
|
247
|
+
if (!allowedValues.includes(value)) {
|
|
248
|
+
throw new ValidationError(
|
|
249
|
+
`${fieldName} must be one of: ${allowedValues.join(', ')}. Got: ${value}`,
|
|
250
|
+
fieldName
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate callback function
|
|
259
|
+
*
|
|
260
|
+
* @param {Function} callback - Callback to validate
|
|
261
|
+
* @param {string} fieldName - Name of field for error messages
|
|
262
|
+
* @param {boolean} [required=true] - Whether callback is required
|
|
263
|
+
* @returns {Function|null} Validated callback or null if not required and not provided
|
|
264
|
+
* @throws {ValidationError} If callback is required but not a function
|
|
265
|
+
*/
|
|
266
|
+
export function validateCallback(callback, fieldName, required = true) {
|
|
267
|
+
if (callback === null || callback === undefined) {
|
|
268
|
+
if (required) {
|
|
269
|
+
throw new ValidationError(
|
|
270
|
+
`${fieldName} is required`,
|
|
271
|
+
fieldName
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typeof callback !== 'function') {
|
|
278
|
+
throw new ValidationError(
|
|
279
|
+
`${fieldName} must be a function`,
|
|
280
|
+
fieldName
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return callback;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validate hex color string
|
|
289
|
+
*
|
|
290
|
+
* @param {string} value - Color string to validate
|
|
291
|
+
* @param {string} fieldName - Name of field for error messages
|
|
292
|
+
* @returns {string} Validated color string
|
|
293
|
+
* @throws {ValidationError} If color format is invalid
|
|
294
|
+
*/
|
|
295
|
+
export function validateHexColor(value, fieldName) {
|
|
296
|
+
if (typeof value !== 'string') {
|
|
297
|
+
throw new ValidationError(
|
|
298
|
+
`${fieldName} must be a string`,
|
|
299
|
+
fieldName
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const hexColorRegex = /^(#|0x)[0-9A-Fa-f]{6}$/i;
|
|
304
|
+
if (!hexColorRegex.test(value)) {
|
|
305
|
+
throw new ValidationError(
|
|
306
|
+
`${fieldName} must be a valid hex color (e.g., #FFFFFF or 0xFFFFFF)`,
|
|
307
|
+
fieldName
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Validate DOM element
|
|
316
|
+
*
|
|
317
|
+
* @param {HTMLElement} element - Element to validate
|
|
318
|
+
* @param {string} fieldName - Name of field for error messages
|
|
319
|
+
* @returns {HTMLElement} Validated element
|
|
320
|
+
* @throws {ValidationError} If element is not a valid DOM element
|
|
321
|
+
*/
|
|
322
|
+
export function validateDOMElement(element, fieldName) {
|
|
323
|
+
if (typeof HTMLElement !== 'undefined' && !(element instanceof HTMLElement)) {
|
|
324
|
+
throw new ValidationError(
|
|
325
|
+
`${fieldName} must be a valid HTML element`,
|
|
326
|
+
fieldName
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return element;
|
|
331
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gsplat-flame-avatar - Utils Module
|
|
3
3
|
* Shared utility functions.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Derived from @mkkellogg/gaussian-splats-3d (MIT License)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// Original utilities
|
|
8
9
|
export * from './LoaderUtils.js';
|
|
9
10
|
export * from './Util.js';
|
|
11
|
+
|
|
12
|
+
// New refactored utilities
|
|
13
|
+
export * from './ValidationUtils.js';
|
|
14
|
+
export * from './Logger.js';
|
|
15
|
+
export * from './ObjectPool.js';
|
|
16
|
+
export * from './BlobUrlManager.js';
|
|
17
|
+
export * from './RenderLoop.js';
|
|
18
|
+
export * from './EventEmitter.js';
|