@viji-dev/core 0.2.19 → 0.3.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 +153 -32
- package/dist/artist-dts-p5.js +1 -1
- package/dist/artist-dts.js +1 -1
- package/dist/artist-global.d.ts +115 -14
- package/dist/artist-js-ambient.d.ts +43 -1
- package/dist/artist-jsdoc.d.ts +43 -1
- package/dist/assets/{viji.worker-C8mrsLDG.js → viji.worker-b3XR7zKX.js} +546 -55
- package/dist/assets/viji.worker-b3XR7zKX.js.map +1 -0
- package/dist/assets/wasm/essentia-wasm.web.wasm +0 -0
- package/dist/essentia-wasm.web-CO8uMw0d.js +5696 -0
- package/dist/essentia-wasm.web-CO8uMw0d.js.map +1 -0
- package/dist/essentia.js-core.es-DnrJE0uR.js +3174 -0
- package/dist/essentia.js-core.es-DnrJE0uR.js.map +1 -0
- package/dist/index-BdLMCFEN.js +16180 -0
- package/dist/index-BdLMCFEN.js.map +1 -0
- package/dist/index.d.ts +1129 -37
- package/dist/index.js +5 -2613
- package/dist/index.js.map +1 -1
- package/dist/shader-uniforms.js +125 -0
- package/package.json +5 -2
- package/dist/assets/viji.worker-C8mrsLDG.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,2616 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
constructor(message, code, context) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.code = code;
|
|
5
|
-
this.context = context;
|
|
6
|
-
this.name = "VijiCoreError";
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
class IFrameManager {
|
|
10
|
-
constructor(hostContainer) {
|
|
11
|
-
this.hostContainer = hostContainer;
|
|
12
|
-
}
|
|
13
|
-
iframe = null;
|
|
14
|
-
canvas = null;
|
|
15
|
-
offscreenCanvas = null;
|
|
16
|
-
isInitialized = false;
|
|
17
|
-
scale = 1;
|
|
18
|
-
// Debug logging control
|
|
19
|
-
debugMode = false;
|
|
20
|
-
/**
|
|
21
|
-
* Enable or disable debug logging
|
|
22
|
-
*/
|
|
23
|
-
setDebugMode(enabled) {
|
|
24
|
-
this.debugMode = enabled;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Debug logging helper
|
|
28
|
-
*/
|
|
29
|
-
debugLog(message, ...args) {
|
|
30
|
-
if (this.debugMode) {
|
|
31
|
-
console.log(message, ...args);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// Phase 7: Interaction event listeners
|
|
35
|
-
interactionListeners = /* @__PURE__ */ new Map();
|
|
36
|
-
isInteractionEnabled = true;
|
|
37
|
-
// Mouse canvas tracking
|
|
38
|
-
isMouseInCanvas = false;
|
|
39
|
-
// Touch tracking for gesture recognition
|
|
40
|
-
activeTouchIds = /* @__PURE__ */ new Set();
|
|
41
|
-
/**
|
|
42
|
-
* Creates a secure IFrame with proper sandbox attributes
|
|
43
|
-
*/
|
|
44
|
-
async createSecureIFrame() {
|
|
45
|
-
try {
|
|
46
|
-
const iframe = document.createElement("iframe");
|
|
47
|
-
iframe.sandbox.add("allow-scripts");
|
|
48
|
-
iframe.sandbox.add("allow-same-origin");
|
|
49
|
-
iframe.style.width = "100%";
|
|
50
|
-
iframe.style.height = "100%";
|
|
51
|
-
iframe.style.border = "none";
|
|
52
|
-
iframe.style.display = "block";
|
|
53
|
-
const iframeContent = this.generateIFrameHTML();
|
|
54
|
-
const blob = new Blob([iframeContent], { type: "text/html" });
|
|
55
|
-
iframe.src = URL.createObjectURL(blob);
|
|
56
|
-
this.hostContainer.appendChild(iframe);
|
|
57
|
-
await new Promise((resolve, reject) => {
|
|
58
|
-
const timeout = setTimeout(() => {
|
|
59
|
-
reject(new VijiCoreError("IFrame load timeout", "IFRAME_TIMEOUT"));
|
|
60
|
-
}, 5e3);
|
|
61
|
-
const checkReady = () => {
|
|
62
|
-
if (iframe.contentDocument && iframe.contentDocument.readyState === "complete") {
|
|
63
|
-
clearTimeout(timeout);
|
|
64
|
-
resolve();
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
iframe.onload = () => {
|
|
68
|
-
if (iframe.contentDocument?.readyState === "complete") {
|
|
69
|
-
clearTimeout(timeout);
|
|
70
|
-
resolve();
|
|
71
|
-
} else {
|
|
72
|
-
iframe.contentDocument?.addEventListener("DOMContentLoaded", checkReady);
|
|
73
|
-
setTimeout(checkReady, 100);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
iframe.onerror = () => {
|
|
77
|
-
clearTimeout(timeout);
|
|
78
|
-
reject(new VijiCoreError("IFrame load failed", "IFRAME_LOAD_ERROR"));
|
|
79
|
-
};
|
|
80
|
-
});
|
|
81
|
-
this.iframe = iframe;
|
|
82
|
-
return iframe;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
throw new VijiCoreError(
|
|
85
|
-
`Failed to create secure IFrame: ${error}`,
|
|
86
|
-
"IFRAME_CREATION_ERROR",
|
|
87
|
-
{ error }
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Creates canvas inside the IFrame and returns OffscreenCanvas for WebWorker
|
|
93
|
-
*/
|
|
94
|
-
async createCanvas() {
|
|
95
|
-
if (!this.iframe?.contentWindow) {
|
|
96
|
-
throw new VijiCoreError("IFrame not ready for canvas creation", "IFRAME_NOT_READY");
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
const iframeDoc = await this.waitForIFrameDocument();
|
|
100
|
-
const canvas = iframeDoc.createElement("canvas");
|
|
101
|
-
canvas.id = "viji-canvas";
|
|
102
|
-
this.canvas = canvas;
|
|
103
|
-
const { width, height } = this.calculateCanvasSize();
|
|
104
|
-
canvas.width = width * this.scale;
|
|
105
|
-
canvas.height = height * this.scale;
|
|
106
|
-
canvas.style.width = "100%";
|
|
107
|
-
canvas.style.height = "100%";
|
|
108
|
-
canvas.style.display = "block";
|
|
109
|
-
const body = iframeDoc.querySelector("body");
|
|
110
|
-
if (!body) {
|
|
111
|
-
throw new VijiCoreError("IFrame body not found", "IFRAME_BODY_ERROR");
|
|
112
|
-
}
|
|
113
|
-
body.appendChild(canvas);
|
|
114
|
-
this.setupInteractionListeners(canvas, iframeDoc);
|
|
115
|
-
const offscreenCanvas = canvas.transferControlToOffscreen();
|
|
116
|
-
this.offscreenCanvas = offscreenCanvas;
|
|
117
|
-
this.isInitialized = true;
|
|
118
|
-
return offscreenCanvas;
|
|
119
|
-
} catch (error) {
|
|
120
|
-
throw new VijiCoreError(
|
|
121
|
-
`Failed to create canvas: ${error}`,
|
|
122
|
-
"CANVAS_CREATION_ERROR",
|
|
123
|
-
{ error }
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Waits for iframe document to be accessible with retry logic
|
|
129
|
-
*/
|
|
130
|
-
async waitForIFrameDocument() {
|
|
131
|
-
const maxRetries = 15;
|
|
132
|
-
const retryDelay = 150;
|
|
133
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
134
|
-
try {
|
|
135
|
-
const iframeDoc = this.iframe?.contentDocument;
|
|
136
|
-
if (iframeDoc && (iframeDoc.readyState === "complete" || iframeDoc.readyState === "interactive") && iframeDoc.body && this.iframe?.contentWindow) {
|
|
137
|
-
this.debugLog(`IFrame document ready after ${i + 1} attempts`);
|
|
138
|
-
return iframeDoc;
|
|
139
|
-
}
|
|
140
|
-
this.debugLog(`IFrame not ready attempt ${i + 1}/${maxRetries}:`, {
|
|
141
|
-
hasDocument: !!iframeDoc,
|
|
142
|
-
readyState: iframeDoc?.readyState,
|
|
143
|
-
hasBody: !!iframeDoc?.body,
|
|
144
|
-
hasWindow: !!this.iframe?.contentWindow
|
|
145
|
-
});
|
|
146
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
147
|
-
} catch (error) {
|
|
148
|
-
this.debugLog(`IFrame access error attempt ${i + 1}/${maxRetries}:`, error);
|
|
149
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
throw new VijiCoreError("Cannot access IFrame document after retries", "IFRAME_ACCESS_ERROR");
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Updates canvas resolution setting (scaling handled in worker)
|
|
156
|
-
*/
|
|
157
|
-
updateScale(scale) {
|
|
158
|
-
this.scale = scale;
|
|
159
|
-
return this.getEffectiveResolution();
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Gets current scale
|
|
163
|
-
*/
|
|
164
|
-
getScale() {
|
|
165
|
-
return this.scale;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Destroys the IFrame and cleans up resources
|
|
169
|
-
*/
|
|
170
|
-
destroy() {
|
|
171
|
-
try {
|
|
172
|
-
if (this.iframe) {
|
|
173
|
-
if (this.iframe.src.startsWith("blob:")) {
|
|
174
|
-
URL.revokeObjectURL(this.iframe.src);
|
|
175
|
-
}
|
|
176
|
-
this.iframe.remove();
|
|
177
|
-
this.iframe = null;
|
|
178
|
-
}
|
|
179
|
-
this.offscreenCanvas = null;
|
|
180
|
-
this.isInitialized = false;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.warn("Error during IFrame cleanup:", error);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Checks if IFrame is ready for use
|
|
187
|
-
*/
|
|
188
|
-
get ready() {
|
|
189
|
-
return this.isInitialized && this.iframe !== null && this.offscreenCanvas !== null;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Gets the IFrame element
|
|
193
|
-
*/
|
|
194
|
-
get element() {
|
|
195
|
-
return this.iframe;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Generates the HTML content for the secure IFrame
|
|
199
|
-
*/
|
|
200
|
-
generateIFrameHTML() {
|
|
201
|
-
return `
|
|
202
|
-
<!DOCTYPE html>
|
|
203
|
-
<html>
|
|
204
|
-
<head>
|
|
205
|
-
<meta charset="utf-8">
|
|
206
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
207
|
-
<title>Viji Scene Container</title>
|
|
208
|
-
<style>
|
|
209
|
-
* {
|
|
210
|
-
margin: 0;
|
|
211
|
-
padding: 0;
|
|
212
|
-
box-sizing: border-box;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
html, body {
|
|
216
|
-
width: 100%;
|
|
217
|
-
height: 100%;
|
|
218
|
-
overflow: hidden;
|
|
219
|
-
background: #000;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
canvas {
|
|
223
|
-
display: block;
|
|
224
|
-
width: 100%;
|
|
225
|
-
height: 100%;
|
|
226
|
-
image-rendering: auto;
|
|
227
|
-
}
|
|
228
|
-
</style>
|
|
229
|
-
</head>
|
|
230
|
-
<body>
|
|
231
|
-
<!-- Canvas will be created dynamically -->
|
|
232
|
-
</body>
|
|
233
|
-
</html>
|
|
234
|
-
`.trim();
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Calculates canvas dimensions based on container size
|
|
238
|
-
* Canvas internal size starts as container size, then worker updates it based on resolution
|
|
239
|
-
*/
|
|
240
|
-
calculateCanvasSize() {
|
|
241
|
-
const containerRect = this.hostContainer.getBoundingClientRect();
|
|
242
|
-
return {
|
|
243
|
-
width: Math.round(containerRect.width),
|
|
244
|
-
height: Math.round(containerRect.height)
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Gets the effective resolution that should be used for rendering
|
|
249
|
-
*/
|
|
250
|
-
getEffectiveResolution() {
|
|
251
|
-
const containerRect = this.hostContainer.getBoundingClientRect();
|
|
252
|
-
const containerWidth = Math.round(containerRect.width);
|
|
253
|
-
const containerHeight = Math.round(containerRect.height);
|
|
254
|
-
const scale = Math.max(0.1, Math.min(1, this.scale));
|
|
255
|
-
return {
|
|
256
|
-
width: Math.round(containerWidth * scale),
|
|
257
|
-
height: Math.round(containerHeight * scale)
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
// Phase 7: Interaction Management Methods
|
|
261
|
-
/**
|
|
262
|
-
* Sets up interaction event listeners on the canvas and document
|
|
263
|
-
*/
|
|
264
|
-
setupInteractionListeners(canvas, iframeDoc) {
|
|
265
|
-
if (!this.isInteractionEnabled) return;
|
|
266
|
-
canvas.tabIndex = 0;
|
|
267
|
-
canvas.style.outline = "none";
|
|
268
|
-
canvas.addEventListener("mousedown", this.handleMouseEvent.bind(this), { passive: false });
|
|
269
|
-
canvas.addEventListener("mousemove", this.handleMouseEvent.bind(this), { passive: false });
|
|
270
|
-
canvas.addEventListener("mouseup", this.handleMouseEvent.bind(this), { passive: false });
|
|
271
|
-
canvas.addEventListener("mouseenter", this.handleMouseEnter.bind(this), { passive: false });
|
|
272
|
-
canvas.addEventListener("mouseleave", this.handleMouseLeave.bind(this), { passive: false });
|
|
273
|
-
canvas.addEventListener("wheel", this.handleWheelEvent.bind(this), { passive: false });
|
|
274
|
-
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
275
|
-
iframeDoc.addEventListener("keydown", this.handleKeyboardEvent.bind(this), { passive: false });
|
|
276
|
-
iframeDoc.addEventListener("keyup", this.handleKeyboardEvent.bind(this), { passive: false });
|
|
277
|
-
canvas.addEventListener("touchstart", this.handleTouchEvent.bind(this), { passive: false });
|
|
278
|
-
canvas.addEventListener("touchmove", this.handleTouchEvent.bind(this), { passive: false });
|
|
279
|
-
canvas.addEventListener("touchend", this.handleTouchEvent.bind(this), { passive: false });
|
|
280
|
-
canvas.addEventListener("touchcancel", this.handleTouchEvent.bind(this), { passive: false });
|
|
281
|
-
canvas.addEventListener("mousedown", () => canvas.focus());
|
|
282
|
-
canvas.addEventListener("touchstart", () => canvas.focus());
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Handles mouse events and transforms coordinates
|
|
286
|
-
*/
|
|
287
|
-
handleMouseEvent(event) {
|
|
288
|
-
if (!this.canvas || !this.isInteractionEnabled) return;
|
|
289
|
-
event.preventDefault();
|
|
290
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
291
|
-
const x = (event.clientX - rect.left) * (this.canvas.width / rect.width);
|
|
292
|
-
const y = (event.clientY - rect.top) * (this.canvas.height / rect.height);
|
|
293
|
-
const deltaX = event.movementX || 0;
|
|
294
|
-
const deltaY = event.movementY || 0;
|
|
295
|
-
const data = {
|
|
296
|
-
x,
|
|
297
|
-
y,
|
|
298
|
-
buttons: event.buttons,
|
|
299
|
-
deltaX,
|
|
300
|
-
deltaY,
|
|
301
|
-
wheelDeltaX: 0,
|
|
302
|
-
wheelDeltaY: 0,
|
|
303
|
-
isInCanvas: this.isMouseInCanvas,
|
|
304
|
-
timestamp: performance.now()
|
|
305
|
-
};
|
|
306
|
-
this.emitInteractionEvent("mouse-update", data);
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Handles mouse enter events
|
|
310
|
-
*/
|
|
311
|
-
handleMouseEnter(event) {
|
|
312
|
-
if (!this.isInteractionEnabled) return;
|
|
313
|
-
this.isMouseInCanvas = true;
|
|
314
|
-
this.handleMouseEvent(event);
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Handles mouse leave events
|
|
318
|
-
*/
|
|
319
|
-
handleMouseLeave(event) {
|
|
320
|
-
if (!this.isInteractionEnabled) return;
|
|
321
|
-
this.isMouseInCanvas = false;
|
|
322
|
-
this.handleMouseEvent(event);
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Handles wheel events
|
|
326
|
-
*/
|
|
327
|
-
handleWheelEvent(event) {
|
|
328
|
-
if (!this.canvas || !this.isInteractionEnabled) return;
|
|
329
|
-
event.preventDefault();
|
|
330
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
331
|
-
const x = (event.clientX - rect.left) * (this.canvas.width / rect.width);
|
|
332
|
-
const y = (event.clientY - rect.top) * (this.canvas.height / rect.height);
|
|
333
|
-
const data = {
|
|
334
|
-
x,
|
|
335
|
-
y,
|
|
336
|
-
buttons: event.buttons,
|
|
337
|
-
deltaX: 0,
|
|
338
|
-
deltaY: 0,
|
|
339
|
-
wheelDeltaX: event.deltaX,
|
|
340
|
-
wheelDeltaY: event.deltaY,
|
|
341
|
-
timestamp: performance.now()
|
|
342
|
-
};
|
|
343
|
-
this.emitInteractionEvent("mouse-update", data);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Handles keyboard events
|
|
347
|
-
*/
|
|
348
|
-
handleKeyboardEvent(event) {
|
|
349
|
-
if (!this.isInteractionEnabled) return;
|
|
350
|
-
const allowedKeys = ["Tab", "F1", "F2", "F3", "F4", "F5", "F11", "F12"];
|
|
351
|
-
if (!allowedKeys.includes(event.key)) {
|
|
352
|
-
event.preventDefault();
|
|
353
|
-
}
|
|
354
|
-
const data = {
|
|
355
|
-
type: event.type,
|
|
356
|
-
key: event.key,
|
|
357
|
-
code: event.code,
|
|
358
|
-
shiftKey: event.shiftKey,
|
|
359
|
-
ctrlKey: event.ctrlKey,
|
|
360
|
-
altKey: event.altKey,
|
|
361
|
-
metaKey: event.metaKey,
|
|
362
|
-
timestamp: performance.now()
|
|
363
|
-
};
|
|
364
|
-
this.emitInteractionEvent("keyboard-update", data);
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Handles touch events and tracks multi-touch
|
|
368
|
-
*/
|
|
369
|
-
handleTouchEvent(event) {
|
|
370
|
-
if (!this.canvas || !this.isInteractionEnabled) return;
|
|
371
|
-
event.preventDefault();
|
|
372
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
373
|
-
const scaleX = this.canvas.width / rect.width;
|
|
374
|
-
const scaleY = this.canvas.height / rect.height;
|
|
375
|
-
const touches = Array.from(event.touches).map((touch) => ({
|
|
376
|
-
identifier: touch.identifier,
|
|
377
|
-
clientX: (touch.clientX - rect.left) * scaleX,
|
|
378
|
-
clientY: (touch.clientY - rect.top) * scaleY,
|
|
379
|
-
pressure: touch.pressure || 0,
|
|
380
|
-
radiusX: touch.radiusX || 10,
|
|
381
|
-
radiusY: touch.radiusY || 10,
|
|
382
|
-
rotationAngle: touch.rotationAngle || 0,
|
|
383
|
-
force: touch.force || touch.pressure || 0
|
|
384
|
-
}));
|
|
385
|
-
if (event.type === "touchstart") {
|
|
386
|
-
for (const touch of event.changedTouches) {
|
|
387
|
-
this.activeTouchIds.add(touch.identifier);
|
|
388
|
-
}
|
|
389
|
-
} else if (event.type === "touchend" || event.type === "touchcancel") {
|
|
390
|
-
for (const touch of event.changedTouches) {
|
|
391
|
-
this.activeTouchIds.delete(touch.identifier);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
const data = {
|
|
395
|
-
type: event.type,
|
|
396
|
-
touches,
|
|
397
|
-
timestamp: performance.now()
|
|
398
|
-
};
|
|
399
|
-
this.emitInteractionEvent("touch-update", data);
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Emits an interaction event to registered listeners
|
|
403
|
-
*/
|
|
404
|
-
emitInteractionEvent(eventType, data) {
|
|
405
|
-
const listener = this.interactionListeners.get(eventType);
|
|
406
|
-
if (listener) {
|
|
407
|
-
listener(data);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Registers an interaction event listener
|
|
412
|
-
*/
|
|
413
|
-
onInteractionEvent(eventType, listener) {
|
|
414
|
-
this.interactionListeners.set(eventType, listener);
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Removes an interaction event listener
|
|
418
|
-
*/
|
|
419
|
-
offInteractionEvent(eventType) {
|
|
420
|
-
this.interactionListeners.delete(eventType);
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Enables or disables interaction event capture
|
|
424
|
-
*/
|
|
425
|
-
setInteractionEnabled(enabled) {
|
|
426
|
-
this.isInteractionEnabled = enabled;
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Gets the canvas element (for coordinate calculations)
|
|
430
|
-
*/
|
|
431
|
-
getCanvas() {
|
|
432
|
-
return this.canvas;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
function WorkerWrapper(options) {
|
|
436
|
-
return new Worker(
|
|
437
|
-
"" + new URL("assets/viji.worker-C8mrsLDG.js", import.meta.url).href,
|
|
438
|
-
{
|
|
439
|
-
type: "module",
|
|
440
|
-
name: options?.name
|
|
441
|
-
}
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
class WorkerManager {
|
|
445
|
-
constructor(sceneCode, offscreenCanvas) {
|
|
446
|
-
this.sceneCode = sceneCode;
|
|
447
|
-
this.offscreenCanvas = offscreenCanvas;
|
|
448
|
-
}
|
|
449
|
-
worker = null;
|
|
450
|
-
messageId = 0;
|
|
451
|
-
pendingMessages = /* @__PURE__ */ new Map();
|
|
452
|
-
messageHandlers = /* @__PURE__ */ new Map();
|
|
453
|
-
isInitialized = false;
|
|
454
|
-
/**
|
|
455
|
-
* Creates and initializes the WebWorker with artist code
|
|
456
|
-
*/
|
|
457
|
-
async createWorker() {
|
|
458
|
-
try {
|
|
459
|
-
this.worker = new WorkerWrapper();
|
|
460
|
-
this.setupMessageHandling();
|
|
461
|
-
await this.initializeWorker();
|
|
462
|
-
this.postMessage("set-scene-code", { sceneCode: this.sceneCode });
|
|
463
|
-
this.isInitialized = true;
|
|
464
|
-
} catch (error) {
|
|
465
|
-
throw new VijiCoreError(
|
|
466
|
-
`Failed to create worker: ${error}`,
|
|
467
|
-
"WORKER_CREATION_ERROR",
|
|
468
|
-
{ error }
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Sends a message to the worker and returns a promise for the response
|
|
474
|
-
*/
|
|
475
|
-
async sendMessage(type, data, timeout = 5e3) {
|
|
476
|
-
if (!this.worker) {
|
|
477
|
-
throw new VijiCoreError("Worker not initialized", "WORKER_NOT_READY");
|
|
478
|
-
}
|
|
479
|
-
const id = `msg_${++this.messageId}`;
|
|
480
|
-
const message = {
|
|
481
|
-
type,
|
|
482
|
-
id,
|
|
483
|
-
timestamp: Date.now(),
|
|
484
|
-
data
|
|
485
|
-
};
|
|
486
|
-
return new Promise((resolve, reject) => {
|
|
487
|
-
const timeoutId = setTimeout(() => {
|
|
488
|
-
this.pendingMessages.delete(id);
|
|
489
|
-
reject(new VijiCoreError(`Message timeout: ${type}`, "MESSAGE_TIMEOUT"));
|
|
490
|
-
}, timeout);
|
|
491
|
-
this.pendingMessages.set(id, {
|
|
492
|
-
resolve,
|
|
493
|
-
reject,
|
|
494
|
-
timeout: timeoutId
|
|
495
|
-
});
|
|
496
|
-
this.worker.postMessage(message);
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
postMessage(type, data, transfer) {
|
|
500
|
-
if (!this.worker) {
|
|
501
|
-
console.warn("Attempted to post message to uninitialized worker");
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
const message = {
|
|
505
|
-
type,
|
|
506
|
-
id: `fire_${++this.messageId}`,
|
|
507
|
-
timestamp: Date.now(),
|
|
508
|
-
data
|
|
509
|
-
};
|
|
510
|
-
if (transfer && transfer.length > 0) {
|
|
511
|
-
this.worker.postMessage(message, transfer);
|
|
512
|
-
} else {
|
|
513
|
-
this.worker.postMessage(message);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Registers a handler for worker messages
|
|
518
|
-
*/
|
|
519
|
-
onMessage(type, handler) {
|
|
520
|
-
this.messageHandlers.set(type, handler);
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Removes a message handler
|
|
524
|
-
*/
|
|
525
|
-
offMessage(type) {
|
|
526
|
-
this.messageHandlers.delete(type);
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Terminates the worker and cleans up resources
|
|
530
|
-
*/
|
|
531
|
-
destroy() {
|
|
532
|
-
try {
|
|
533
|
-
this.pendingMessages.forEach(({ timeout, reject }) => {
|
|
534
|
-
clearTimeout(timeout);
|
|
535
|
-
reject(new VijiCoreError("Worker destroyed", "WORKER_DESTROYED"));
|
|
536
|
-
});
|
|
537
|
-
this.pendingMessages.clear();
|
|
538
|
-
this.messageHandlers.clear();
|
|
539
|
-
if (this.worker) {
|
|
540
|
-
this.worker.terminate();
|
|
541
|
-
this.worker = null;
|
|
542
|
-
}
|
|
543
|
-
this.isInitialized = false;
|
|
544
|
-
} catch (error) {
|
|
545
|
-
console.warn("Error during worker cleanup:", error);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Checks if worker is ready for use
|
|
550
|
-
*/
|
|
551
|
-
get ready() {
|
|
552
|
-
return this.isInitialized && this.worker !== null;
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Gets the worker instance
|
|
556
|
-
*/
|
|
557
|
-
get instance() {
|
|
558
|
-
return this.worker;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Sets up message handling for worker communication
|
|
562
|
-
*/
|
|
563
|
-
setupMessageHandling() {
|
|
564
|
-
if (!this.worker) return;
|
|
565
|
-
this.worker.onmessage = (event) => {
|
|
566
|
-
const message = event.data;
|
|
567
|
-
if (this.pendingMessages.has(message.id)) {
|
|
568
|
-
const pending = this.pendingMessages.get(message.id);
|
|
569
|
-
clearTimeout(pending.timeout);
|
|
570
|
-
this.pendingMessages.delete(message.id);
|
|
571
|
-
if (message.type === "error") {
|
|
572
|
-
pending.reject(new VijiCoreError(
|
|
573
|
-
message.data?.message || "Worker error",
|
|
574
|
-
message.data?.code || "WORKER_ERROR",
|
|
575
|
-
message.data
|
|
576
|
-
));
|
|
577
|
-
} else {
|
|
578
|
-
pending.resolve(message.data);
|
|
579
|
-
}
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
const handler = this.messageHandlers.get(message.type);
|
|
583
|
-
if (handler) {
|
|
584
|
-
try {
|
|
585
|
-
handler(message.data);
|
|
586
|
-
} catch (error) {
|
|
587
|
-
console.error(`Error in message handler for ${message.type}:`, error);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
this.worker.onerror = (error) => {
|
|
592
|
-
console.error("Worker error:", error);
|
|
593
|
-
this.pendingMessages.forEach(({ timeout, reject }) => {
|
|
594
|
-
clearTimeout(timeout);
|
|
595
|
-
reject(new VijiCoreError("Worker error", "WORKER_ERROR", error));
|
|
596
|
-
});
|
|
597
|
-
this.pendingMessages.clear();
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Initializes the worker with canvas and basic setup
|
|
602
|
-
* Waits for init-response message (worker will also send separate 'ready' event)
|
|
603
|
-
*/
|
|
604
|
-
async initializeWorker() {
|
|
605
|
-
if (!this.worker) {
|
|
606
|
-
throw new VijiCoreError("Worker not created", "WORKER_NOT_CREATED");
|
|
607
|
-
}
|
|
608
|
-
const id = `msg_${++this.messageId}`;
|
|
609
|
-
const message = {
|
|
610
|
-
type: "init",
|
|
611
|
-
id,
|
|
612
|
-
timestamp: Date.now(),
|
|
613
|
-
data: {
|
|
614
|
-
canvas: this.offscreenCanvas
|
|
615
|
-
}
|
|
616
|
-
};
|
|
617
|
-
return new Promise((resolve, reject) => {
|
|
618
|
-
const timeoutId = setTimeout(() => {
|
|
619
|
-
this.pendingMessages.delete(id);
|
|
620
|
-
reject(new VijiCoreError("Canvas transfer timeout", "CANVAS_TRANSFER_TIMEOUT"));
|
|
621
|
-
}, 1e4);
|
|
622
|
-
this.pendingMessages.set(id, {
|
|
623
|
-
resolve,
|
|
624
|
-
reject,
|
|
625
|
-
timeout: timeoutId
|
|
626
|
-
});
|
|
627
|
-
this.worker.postMessage(message, [this.offscreenCanvas]);
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
class InteractionManager {
|
|
632
|
-
// Mouse state
|
|
633
|
-
mouseState = {
|
|
634
|
-
x: 0,
|
|
635
|
-
y: 0,
|
|
636
|
-
isInCanvas: false,
|
|
637
|
-
isPressed: false,
|
|
638
|
-
leftButton: false,
|
|
639
|
-
rightButton: false,
|
|
640
|
-
middleButton: false,
|
|
641
|
-
velocity: { x: 0, y: 0 },
|
|
642
|
-
deltaX: 0,
|
|
643
|
-
deltaY: 0,
|
|
644
|
-
wheelDelta: 0,
|
|
645
|
-
wheelX: 0,
|
|
646
|
-
wheelY: 0,
|
|
647
|
-
wasPressed: false,
|
|
648
|
-
wasReleased: false,
|
|
649
|
-
wasMoved: false
|
|
650
|
-
};
|
|
651
|
-
// Mouse velocity tracking
|
|
652
|
-
mouseVelocityHistory = [];
|
|
653
|
-
// Keyboard state
|
|
654
|
-
keyboardState = {
|
|
655
|
-
isPressed: (key) => this.activeKeys.has(key.toLowerCase()),
|
|
656
|
-
wasPressed: (key) => this.pressedThisFrame.has(key.toLowerCase()),
|
|
657
|
-
wasReleased: (key) => this.releasedThisFrame.has(key.toLowerCase()),
|
|
658
|
-
activeKeys: /* @__PURE__ */ new Set(),
|
|
659
|
-
pressedThisFrame: /* @__PURE__ */ new Set(),
|
|
660
|
-
releasedThisFrame: /* @__PURE__ */ new Set(),
|
|
661
|
-
lastKeyPressed: "",
|
|
662
|
-
lastKeyReleased: "",
|
|
663
|
-
shift: false,
|
|
664
|
-
ctrl: false,
|
|
665
|
-
alt: false,
|
|
666
|
-
meta: false
|
|
667
|
-
};
|
|
668
|
-
activeKeys = /* @__PURE__ */ new Set();
|
|
669
|
-
pressedThisFrame = /* @__PURE__ */ new Set();
|
|
670
|
-
releasedThisFrame = /* @__PURE__ */ new Set();
|
|
671
|
-
// Touch state
|
|
672
|
-
touchState = {
|
|
673
|
-
points: [],
|
|
674
|
-
count: 0,
|
|
675
|
-
started: [],
|
|
676
|
-
moved: [],
|
|
677
|
-
ended: [],
|
|
678
|
-
primary: null,
|
|
679
|
-
gestures: {
|
|
680
|
-
isPinching: false,
|
|
681
|
-
isRotating: false,
|
|
682
|
-
isPanning: false,
|
|
683
|
-
isTapping: false,
|
|
684
|
-
pinchScale: 1,
|
|
685
|
-
pinchDelta: 0,
|
|
686
|
-
rotationAngle: 0,
|
|
687
|
-
rotationDelta: 0,
|
|
688
|
-
panDelta: { x: 0, y: 0 },
|
|
689
|
-
tapCount: 0,
|
|
690
|
-
lastTapTime: 0,
|
|
691
|
-
tapPosition: null
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
activeTouches = /* @__PURE__ */ new Map();
|
|
695
|
-
gestureState = {
|
|
696
|
-
initialDistance: 0,
|
|
697
|
-
initialAngle: 0,
|
|
698
|
-
lastPinchScale: 1,
|
|
699
|
-
lastRotationAngle: 0,
|
|
700
|
-
panStartPosition: { x: 0, y: 0 },
|
|
701
|
-
tapStartTime: 0,
|
|
702
|
-
tapCount: 0,
|
|
703
|
-
lastTapTime: 0
|
|
704
|
-
};
|
|
705
|
-
constructor() {
|
|
706
|
-
}
|
|
707
|
-
/**
|
|
708
|
-
* Processes mouse update from the host
|
|
709
|
-
*/
|
|
710
|
-
updateMouse(data) {
|
|
711
|
-
const canvasX = data.x;
|
|
712
|
-
const canvasY = data.y;
|
|
713
|
-
const deltaX = canvasX - this.mouseState.x;
|
|
714
|
-
const deltaY = canvasY - this.mouseState.y;
|
|
715
|
-
this.updateMouseVelocity(deltaX, deltaY, data.timestamp);
|
|
716
|
-
const prevPressed = this.mouseState.isPressed;
|
|
717
|
-
const currentPressed = data.buttons > 0;
|
|
718
|
-
this.mouseState.wasPressed = !prevPressed && currentPressed;
|
|
719
|
-
this.mouseState.wasReleased = prevPressed && !currentPressed;
|
|
720
|
-
this.mouseState.wasMoved = deltaX !== 0 || deltaY !== 0;
|
|
721
|
-
this.mouseState.x = canvasX;
|
|
722
|
-
this.mouseState.y = canvasY;
|
|
723
|
-
this.mouseState.deltaX = deltaX;
|
|
724
|
-
this.mouseState.deltaY = deltaY;
|
|
725
|
-
this.mouseState.isPressed = currentPressed;
|
|
726
|
-
this.mouseState.leftButton = (data.buttons & 1) !== 0;
|
|
727
|
-
this.mouseState.rightButton = (data.buttons & 2) !== 0;
|
|
728
|
-
this.mouseState.middleButton = (data.buttons & 4) !== 0;
|
|
729
|
-
this.mouseState.isInCanvas = data.isInCanvas !== void 0 ? data.isInCanvas : true;
|
|
730
|
-
this.mouseState.wheelDelta = data.wheelDeltaY;
|
|
731
|
-
this.mouseState.wheelX = data.wheelDeltaX;
|
|
732
|
-
this.mouseState.wheelY = data.wheelDeltaY;
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Updates mouse velocity with smoothing
|
|
736
|
-
*/
|
|
737
|
-
updateMouseVelocity(deltaX, deltaY, timestamp) {
|
|
738
|
-
this.mouseVelocityHistory.push({ x: deltaX, y: deltaY, time: timestamp });
|
|
739
|
-
const cutoff = timestamp - 100;
|
|
740
|
-
this.mouseVelocityHistory = this.mouseVelocityHistory.filter((sample) => sample.time > cutoff);
|
|
741
|
-
if (this.mouseVelocityHistory.length > 1) {
|
|
742
|
-
const recent = this.mouseVelocityHistory.slice(-5);
|
|
743
|
-
const avgX = recent.reduce((sum, s) => sum + s.x, 0) / recent.length;
|
|
744
|
-
const avgY = recent.reduce((sum, s) => sum + s.y, 0) / recent.length;
|
|
745
|
-
this.mouseState.velocity.x = avgX;
|
|
746
|
-
this.mouseState.velocity.y = avgY;
|
|
747
|
-
} else {
|
|
748
|
-
this.mouseState.velocity.x = deltaX;
|
|
749
|
-
this.mouseState.velocity.y = deltaY;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Processes keyboard update from the host
|
|
754
|
-
*/
|
|
755
|
-
updateKeyboard(data) {
|
|
756
|
-
const key = data.key.toLowerCase();
|
|
757
|
-
if (data.type === "keydown") {
|
|
758
|
-
if (!this.activeKeys.has(key)) {
|
|
759
|
-
this.activeKeys.add(key);
|
|
760
|
-
this.pressedThisFrame.add(key);
|
|
761
|
-
this.keyboardState.lastKeyPressed = data.key;
|
|
762
|
-
}
|
|
763
|
-
} else if (data.type === "keyup") {
|
|
764
|
-
this.activeKeys.delete(key);
|
|
765
|
-
this.releasedThisFrame.add(key);
|
|
766
|
-
this.keyboardState.lastKeyReleased = data.key;
|
|
767
|
-
}
|
|
768
|
-
this.keyboardState.shift = data.shiftKey;
|
|
769
|
-
this.keyboardState.ctrl = data.ctrlKey;
|
|
770
|
-
this.keyboardState.alt = data.altKey;
|
|
771
|
-
this.keyboardState.meta = data.metaKey;
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Processes touch update from the host
|
|
775
|
-
*/
|
|
776
|
-
updateTouch(data) {
|
|
777
|
-
this.touchState.started = [];
|
|
778
|
-
this.touchState.moved = [];
|
|
779
|
-
this.touchState.ended = [];
|
|
780
|
-
if (data.type === "touchstart") {
|
|
781
|
-
this.processTouchStart(data.touches, data.timestamp);
|
|
782
|
-
} else if (data.type === "touchmove") {
|
|
783
|
-
this.processTouchMove(data.touches, data.timestamp);
|
|
784
|
-
} else if (data.type === "touchend" || data.type === "touchcancel") {
|
|
785
|
-
this.processTouchEnd(data.touches, data.timestamp);
|
|
786
|
-
}
|
|
787
|
-
this.touchState.points = Array.from(this.activeTouches.values());
|
|
788
|
-
this.touchState.count = this.touchState.points.length;
|
|
789
|
-
this.touchState.primary = this.touchState.points[0] || null;
|
|
790
|
-
this.updateGestures();
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Processes touch start events
|
|
794
|
-
*/
|
|
795
|
-
processTouchStart(touches, timestamp) {
|
|
796
|
-
for (const touch of touches) {
|
|
797
|
-
const touchPoint = this.createTouchPoint(touch, timestamp, true);
|
|
798
|
-
this.activeTouches.set(touch.identifier, touchPoint);
|
|
799
|
-
this.touchState.started.push(touchPoint);
|
|
800
|
-
}
|
|
801
|
-
if (this.touchState.count === 1) {
|
|
802
|
-
this.gestureState.tapStartTime = timestamp;
|
|
803
|
-
const touch = this.touchState.points[0];
|
|
804
|
-
this.touchState.gestures.tapPosition = { x: touch.x, y: touch.y };
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Processes touch move events
|
|
809
|
-
*/
|
|
810
|
-
processTouchMove(touches, timestamp) {
|
|
811
|
-
for (const touch of touches) {
|
|
812
|
-
const existing = this.activeTouches.get(touch.identifier);
|
|
813
|
-
if (existing) {
|
|
814
|
-
const updated = this.createTouchPoint(touch, timestamp, false, existing);
|
|
815
|
-
this.activeTouches.set(touch.identifier, updated);
|
|
816
|
-
this.touchState.moved.push(updated);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Processes touch end events
|
|
822
|
-
*/
|
|
823
|
-
processTouchEnd(touches, timestamp) {
|
|
824
|
-
for (const touch of touches) {
|
|
825
|
-
const existing = this.activeTouches.get(touch.identifier);
|
|
826
|
-
if (existing) {
|
|
827
|
-
const ended = { ...existing, isEnding: true, isActive: false };
|
|
828
|
-
this.touchState.ended.push(ended);
|
|
829
|
-
this.activeTouches.delete(touch.identifier);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
if (this.touchState.count === 0 && this.gestureState.tapStartTime > 0) {
|
|
833
|
-
const tapDuration = timestamp - this.gestureState.tapStartTime;
|
|
834
|
-
if (tapDuration < 300) {
|
|
835
|
-
this.handleTap(timestamp);
|
|
836
|
-
}
|
|
837
|
-
this.gestureState.tapStartTime = 0;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
/**
|
|
841
|
-
* Creates a touch point from raw touch data
|
|
842
|
-
*/
|
|
843
|
-
createTouchPoint(touch, timestamp, isNew, previous) {
|
|
844
|
-
const x = touch.clientX;
|
|
845
|
-
const y = touch.clientY;
|
|
846
|
-
const deltaX = previous ? x - previous.x : 0;
|
|
847
|
-
const deltaY = previous ? y - previous.y : 0;
|
|
848
|
-
const timeDelta = previous ? timestamp - previous.timestamp : 16;
|
|
849
|
-
const velocityX = timeDelta > 0 ? deltaX / timeDelta * 1e3 : 0;
|
|
850
|
-
const velocityY = timeDelta > 0 ? deltaY / timeDelta * 1e3 : 0;
|
|
851
|
-
return {
|
|
852
|
-
id: touch.identifier,
|
|
853
|
-
x,
|
|
854
|
-
y,
|
|
855
|
-
pressure: touch.pressure || 0,
|
|
856
|
-
radius: Math.max(touch.radiusX || 0, touch.radiusY || 0),
|
|
857
|
-
radiusX: touch.radiusX || 0,
|
|
858
|
-
radiusY: touch.radiusY || 0,
|
|
859
|
-
rotationAngle: touch.rotationAngle || 0,
|
|
860
|
-
force: touch.force || touch.pressure || 0,
|
|
861
|
-
deltaX,
|
|
862
|
-
deltaY,
|
|
863
|
-
velocity: { x: velocityX, y: velocityY },
|
|
864
|
-
isNew,
|
|
865
|
-
isActive: true,
|
|
866
|
-
isEnding: false
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Updates gesture recognition
|
|
871
|
-
*/
|
|
872
|
-
updateGestures() {
|
|
873
|
-
const touches = this.touchState.points;
|
|
874
|
-
const gestures = this.touchState.gestures;
|
|
875
|
-
if (touches.length === 2) {
|
|
876
|
-
const touch1 = touches[0];
|
|
877
|
-
const touch2 = touches[1];
|
|
878
|
-
const distance = Math.sqrt(
|
|
879
|
-
Math.pow(touch2.x - touch1.x, 2) + Math.pow(touch2.y - touch1.y, 2)
|
|
880
|
-
);
|
|
881
|
-
const angle = Math.atan2(touch2.y - touch1.y, touch2.x - touch1.x);
|
|
882
|
-
if (this.gestureState.initialDistance === 0) {
|
|
883
|
-
this.gestureState.initialDistance = distance;
|
|
884
|
-
this.gestureState.initialAngle = angle;
|
|
885
|
-
this.gestureState.lastPinchScale = 1;
|
|
886
|
-
this.gestureState.lastRotationAngle = 0;
|
|
887
|
-
}
|
|
888
|
-
const scale = distance / this.gestureState.initialDistance;
|
|
889
|
-
const scaleDelta = scale - this.gestureState.lastPinchScale;
|
|
890
|
-
gestures.isPinching = Math.abs(scaleDelta) > 0.01;
|
|
891
|
-
gestures.pinchScale = scale;
|
|
892
|
-
gestures.pinchDelta = scaleDelta;
|
|
893
|
-
this.gestureState.lastPinchScale = scale;
|
|
894
|
-
const rotationAngle = angle - this.gestureState.initialAngle;
|
|
895
|
-
const rotationDelta = rotationAngle - this.gestureState.lastRotationAngle;
|
|
896
|
-
gestures.isRotating = Math.abs(rotationDelta) > 0.02;
|
|
897
|
-
gestures.rotationAngle = rotationAngle;
|
|
898
|
-
gestures.rotationDelta = rotationDelta;
|
|
899
|
-
this.gestureState.lastRotationAngle = rotationAngle;
|
|
900
|
-
} else {
|
|
901
|
-
this.gestureState.initialDistance = 0;
|
|
902
|
-
gestures.isPinching = false;
|
|
903
|
-
gestures.isRotating = false;
|
|
904
|
-
gestures.pinchDelta = 0;
|
|
905
|
-
gestures.rotationDelta = 0;
|
|
906
|
-
}
|
|
907
|
-
if (touches.length > 0) {
|
|
908
|
-
const primaryTouch = touches[0];
|
|
909
|
-
if (this.gestureState.panStartPosition.x === 0) {
|
|
910
|
-
this.gestureState.panStartPosition = { x: primaryTouch.x, y: primaryTouch.y };
|
|
911
|
-
}
|
|
912
|
-
const panDeltaX = primaryTouch.x - this.gestureState.panStartPosition.x;
|
|
913
|
-
const panDeltaY = primaryTouch.y - this.gestureState.panStartPosition.y;
|
|
914
|
-
const panDistance = Math.sqrt(panDeltaX * panDeltaX + panDeltaY * panDeltaY);
|
|
915
|
-
gestures.isPanning = panDistance > 10;
|
|
916
|
-
gestures.panDelta = { x: panDeltaX, y: panDeltaY };
|
|
917
|
-
} else {
|
|
918
|
-
this.gestureState.panStartPosition = { x: 0, y: 0 };
|
|
919
|
-
gestures.isPanning = false;
|
|
920
|
-
gestures.panDelta = { x: 0, y: 0 };
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Handles tap gesture detection
|
|
925
|
-
*/
|
|
926
|
-
handleTap(timestamp) {
|
|
927
|
-
const timeSinceLastTap = timestamp - this.gestureState.lastTapTime;
|
|
928
|
-
if (timeSinceLastTap < 300) {
|
|
929
|
-
this.gestureState.tapCount++;
|
|
930
|
-
} else {
|
|
931
|
-
this.gestureState.tapCount = 1;
|
|
932
|
-
}
|
|
933
|
-
this.touchState.gestures.tapCount = this.gestureState.tapCount;
|
|
934
|
-
this.touchState.gestures.lastTapTime = timestamp;
|
|
935
|
-
this.touchState.gestures.isTapping = true;
|
|
936
|
-
this.gestureState.lastTapTime = timestamp;
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Called at the start of each frame to reset frame-based events
|
|
940
|
-
*/
|
|
941
|
-
frameStart() {
|
|
942
|
-
this.mouseState.wasPressed = false;
|
|
943
|
-
this.mouseState.wasReleased = false;
|
|
944
|
-
this.mouseState.wasMoved = false;
|
|
945
|
-
this.mouseState.wheelDelta = 0;
|
|
946
|
-
this.mouseState.wheelX = 0;
|
|
947
|
-
this.mouseState.wheelY = 0;
|
|
948
|
-
this.pressedThisFrame.clear();
|
|
949
|
-
this.releasedThisFrame.clear();
|
|
950
|
-
this.touchState.gestures.isTapping = false;
|
|
951
|
-
this.touchState.gestures.pinchDelta = 0;
|
|
952
|
-
this.touchState.gestures.rotationDelta = 0;
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Get current mouse state (read-only)
|
|
956
|
-
*/
|
|
957
|
-
getMouseState() {
|
|
958
|
-
return this.mouseState;
|
|
959
|
-
}
|
|
960
|
-
/**
|
|
961
|
-
* Get current keyboard state (read-only)
|
|
962
|
-
*/
|
|
963
|
-
getKeyboardState() {
|
|
964
|
-
return this.keyboardState;
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Get current touch state (read-only)
|
|
968
|
-
*/
|
|
969
|
-
getTouchState() {
|
|
970
|
-
return this.touchState;
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Cleanup resources
|
|
974
|
-
*/
|
|
975
|
-
destroy() {
|
|
976
|
-
this.mouseVelocityHistory.length = 0;
|
|
977
|
-
this.activeTouches.clear();
|
|
978
|
-
this.activeKeys.clear();
|
|
979
|
-
this.pressedThisFrame.clear();
|
|
980
|
-
this.releasedThisFrame.clear();
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
class AudioSystem {
|
|
984
|
-
// Audio context and analysis nodes
|
|
985
|
-
audioContext = null;
|
|
986
|
-
analyser = null;
|
|
987
|
-
mediaStreamSource = null;
|
|
988
|
-
currentStream = null;
|
|
989
|
-
// Debug logging control
|
|
990
|
-
debugMode = false;
|
|
991
|
-
/**
|
|
992
|
-
* Enable or disable debug logging
|
|
993
|
-
*/
|
|
994
|
-
setDebugMode(enabled) {
|
|
995
|
-
this.debugMode = enabled;
|
|
996
|
-
}
|
|
997
|
-
/**
|
|
998
|
-
* Debug logging helper
|
|
999
|
-
*/
|
|
1000
|
-
debugLog(message, ...args) {
|
|
1001
|
-
if (this.debugMode) {
|
|
1002
|
-
console.log(message, ...args);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// Analysis configuration (good balance, leaning towards quality)
|
|
1006
|
-
fftSize = 2048;
|
|
1007
|
-
// Good balance for quality vs performance
|
|
1008
|
-
smoothingTimeConstant = 0.8;
|
|
1009
|
-
// Smooth but responsive
|
|
1010
|
-
// Analysis data arrays
|
|
1011
|
-
frequencyData = null;
|
|
1012
|
-
timeDomainData = null;
|
|
1013
|
-
// Audio analysis state (host-side state)
|
|
1014
|
-
audioState = {
|
|
1015
|
-
isConnected: false,
|
|
1016
|
-
volume: {
|
|
1017
|
-
rms: 0,
|
|
1018
|
-
peak: 0
|
|
1019
|
-
},
|
|
1020
|
-
bands: {
|
|
1021
|
-
bass: 0,
|
|
1022
|
-
mid: 0,
|
|
1023
|
-
treble: 0,
|
|
1024
|
-
subBass: 0,
|
|
1025
|
-
lowMid: 0,
|
|
1026
|
-
highMid: 0,
|
|
1027
|
-
presence: 0,
|
|
1028
|
-
brilliance: 0
|
|
1029
|
-
}
|
|
1030
|
-
};
|
|
1031
|
-
// Analysis loop
|
|
1032
|
-
analysisLoopId = null;
|
|
1033
|
-
isAnalysisRunning = false;
|
|
1034
|
-
// Callback to send results to worker
|
|
1035
|
-
sendAnalysisResults = null;
|
|
1036
|
-
constructor(sendAnalysisResultsCallback) {
|
|
1037
|
-
this.handleAudioStreamUpdate = this.handleAudioStreamUpdate.bind(this);
|
|
1038
|
-
this.performAnalysis = this.performAnalysis.bind(this);
|
|
1039
|
-
this.sendAnalysisResults = sendAnalysisResultsCallback || null;
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Get the current audio analysis state (for host-side usage)
|
|
1043
|
-
*/
|
|
1044
|
-
getAudioState() {
|
|
1045
|
-
return { ...this.audioState };
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Handle audio stream update (called from VijiCore)
|
|
1049
|
-
*/
|
|
1050
|
-
handleAudioStreamUpdate(data) {
|
|
1051
|
-
try {
|
|
1052
|
-
if (data.audioStream) {
|
|
1053
|
-
this.setAudioStream(data.audioStream);
|
|
1054
|
-
} else {
|
|
1055
|
-
this.disconnectAudioStream();
|
|
1056
|
-
}
|
|
1057
|
-
if (data.analysisConfig) {
|
|
1058
|
-
this.updateAnalysisConfig(data.analysisConfig);
|
|
1059
|
-
}
|
|
1060
|
-
} catch (error) {
|
|
1061
|
-
console.error("Error handling audio stream update:", error);
|
|
1062
|
-
this.audioState.isConnected = false;
|
|
1063
|
-
this.sendAnalysisResultsToWorker();
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Set the audio stream for analysis
|
|
1068
|
-
*/
|
|
1069
|
-
async setAudioStream(audioStream) {
|
|
1070
|
-
this.disconnectAudioStream();
|
|
1071
|
-
const audioTracks = audioStream.getAudioTracks();
|
|
1072
|
-
if (audioTracks.length === 0) {
|
|
1073
|
-
console.warn("No audio tracks in provided stream");
|
|
1074
|
-
this.audioState.isConnected = false;
|
|
1075
|
-
this.sendAnalysisResultsToWorker();
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
try {
|
|
1079
|
-
if (!this.audioContext) {
|
|
1080
|
-
this.audioContext = new AudioContext();
|
|
1081
|
-
if (this.audioContext.state === "suspended") {
|
|
1082
|
-
await this.audioContext.resume();
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
this.analyser = this.audioContext.createAnalyser();
|
|
1086
|
-
this.analyser.fftSize = this.fftSize;
|
|
1087
|
-
this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
|
|
1088
|
-
this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioStream);
|
|
1089
|
-
this.mediaStreamSource.connect(this.analyser);
|
|
1090
|
-
const bufferLength = this.analyser.frequencyBinCount;
|
|
1091
|
-
this.frequencyData = new Uint8Array(bufferLength);
|
|
1092
|
-
this.timeDomainData = new Uint8Array(bufferLength);
|
|
1093
|
-
this.currentStream = audioStream;
|
|
1094
|
-
this.audioState.isConnected = true;
|
|
1095
|
-
this.startAnalysisLoop();
|
|
1096
|
-
this.debugLog("Audio stream connected successfully (host-side)", {
|
|
1097
|
-
sampleRate: this.audioContext.sampleRate,
|
|
1098
|
-
fftSize: this.fftSize,
|
|
1099
|
-
bufferLength
|
|
1100
|
-
});
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
console.error("Failed to set up audio analysis:", error);
|
|
1103
|
-
this.audioState.isConnected = false;
|
|
1104
|
-
this.disconnectAudioStream();
|
|
1105
|
-
}
|
|
1106
|
-
this.sendAnalysisResultsToWorker();
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Disconnect current audio stream and clean up resources
|
|
1110
|
-
*/
|
|
1111
|
-
disconnectAudioStream() {
|
|
1112
|
-
this.stopAnalysisLoop();
|
|
1113
|
-
if (this.mediaStreamSource) {
|
|
1114
|
-
this.mediaStreamSource.disconnect();
|
|
1115
|
-
this.mediaStreamSource = null;
|
|
1116
|
-
}
|
|
1117
|
-
if (this.analyser) {
|
|
1118
|
-
this.analyser.disconnect();
|
|
1119
|
-
this.analyser = null;
|
|
1120
|
-
}
|
|
1121
|
-
this.frequencyData = null;
|
|
1122
|
-
this.timeDomainData = null;
|
|
1123
|
-
this.currentStream = null;
|
|
1124
|
-
this.audioState.isConnected = false;
|
|
1125
|
-
this.resetAudioValues();
|
|
1126
|
-
this.sendAnalysisResultsToWorker();
|
|
1127
|
-
this.debugLog("Audio stream disconnected (host-side)");
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Update analysis configuration
|
|
1131
|
-
*/
|
|
1132
|
-
updateAnalysisConfig(config) {
|
|
1133
|
-
let needsReconnect = false;
|
|
1134
|
-
if (config.fftSize && config.fftSize !== this.fftSize) {
|
|
1135
|
-
this.fftSize = config.fftSize;
|
|
1136
|
-
needsReconnect = true;
|
|
1137
|
-
}
|
|
1138
|
-
if (config.smoothing !== void 0) {
|
|
1139
|
-
this.smoothingTimeConstant = config.smoothing;
|
|
1140
|
-
if (this.analyser) {
|
|
1141
|
-
this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
if (needsReconnect && this.currentStream) {
|
|
1145
|
-
const stream = this.currentStream;
|
|
1146
|
-
this.setAudioStream(stream);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Start the audio analysis loop
|
|
1151
|
-
*/
|
|
1152
|
-
startAnalysisLoop() {
|
|
1153
|
-
if (this.isAnalysisRunning) return;
|
|
1154
|
-
this.isAnalysisRunning = true;
|
|
1155
|
-
this.performAnalysis();
|
|
1156
|
-
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Stop the audio analysis loop
|
|
1159
|
-
*/
|
|
1160
|
-
stopAnalysisLoop() {
|
|
1161
|
-
this.isAnalysisRunning = false;
|
|
1162
|
-
if (this.analysisLoopId !== null) {
|
|
1163
|
-
cancelAnimationFrame(this.analysisLoopId);
|
|
1164
|
-
this.analysisLoopId = null;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Perform audio analysis (called every frame)
|
|
1169
|
-
*/
|
|
1170
|
-
performAnalysis() {
|
|
1171
|
-
if (!this.isAnalysisRunning || !this.analyser || !this.frequencyData || !this.timeDomainData) {
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
this.analyser.getByteFrequencyData(this.frequencyData);
|
|
1175
|
-
this.analyser.getByteTimeDomainData(this.timeDomainData);
|
|
1176
|
-
this.calculateVolumeMetrics();
|
|
1177
|
-
this.calculateFrequencyBands();
|
|
1178
|
-
this.sendAnalysisResultsToWorker();
|
|
1179
|
-
this.analysisLoopId = requestAnimationFrame(() => this.performAnalysis());
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* Calculate RMS and peak volume from time domain data
|
|
1183
|
-
*/
|
|
1184
|
-
calculateVolumeMetrics() {
|
|
1185
|
-
if (!this.timeDomainData) return;
|
|
1186
|
-
let rmsSum = 0;
|
|
1187
|
-
let peak = 0;
|
|
1188
|
-
for (let i = 0; i < this.timeDomainData.length; i++) {
|
|
1189
|
-
const sample = (this.timeDomainData[i] - 128) / 128;
|
|
1190
|
-
rmsSum += sample * sample;
|
|
1191
|
-
const absValue = Math.abs(sample);
|
|
1192
|
-
if (absValue > peak) {
|
|
1193
|
-
peak = absValue;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
const rms = Math.sqrt(rmsSum / this.timeDomainData.length);
|
|
1197
|
-
this.audioState.volume.rms = rms;
|
|
1198
|
-
this.audioState.volume.peak = peak;
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* Calculate frequency band values from frequency data
|
|
1202
|
-
*/
|
|
1203
|
-
calculateFrequencyBands() {
|
|
1204
|
-
if (!this.frequencyData || !this.audioContext) return;
|
|
1205
|
-
const nyquist = this.audioContext.sampleRate / 2;
|
|
1206
|
-
const binCount = this.frequencyData.length;
|
|
1207
|
-
const bands = {
|
|
1208
|
-
subBass: { min: 20, max: 60 },
|
|
1209
|
-
// Sub-bass
|
|
1210
|
-
bass: { min: 60, max: 250 },
|
|
1211
|
-
// Bass
|
|
1212
|
-
lowMid: { min: 250, max: 500 },
|
|
1213
|
-
// Low midrange
|
|
1214
|
-
mid: { min: 500, max: 2e3 },
|
|
1215
|
-
// Midrange
|
|
1216
|
-
highMid: { min: 2e3, max: 4e3 },
|
|
1217
|
-
// High midrange
|
|
1218
|
-
presence: { min: 4e3, max: 6e3 },
|
|
1219
|
-
// Presence
|
|
1220
|
-
brilliance: { min: 6e3, max: 2e4 },
|
|
1221
|
-
// Brilliance
|
|
1222
|
-
treble: { min: 2e3, max: 2e4 }
|
|
1223
|
-
// Treble (combined high frequencies)
|
|
1224
|
-
};
|
|
1225
|
-
for (const [bandName, range] of Object.entries(bands)) {
|
|
1226
|
-
const startBin = Math.floor(range.min / nyquist * binCount);
|
|
1227
|
-
const endBin = Math.min(Math.floor(range.max / nyquist * binCount), binCount - 1);
|
|
1228
|
-
let sum = 0;
|
|
1229
|
-
let count = 0;
|
|
1230
|
-
for (let i = startBin; i <= endBin; i++) {
|
|
1231
|
-
sum += this.frequencyData[i];
|
|
1232
|
-
count++;
|
|
1233
|
-
}
|
|
1234
|
-
const average = count > 0 ? sum / count : 0;
|
|
1235
|
-
this.audioState.bands[bandName] = average / 255;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* Send analysis results to worker
|
|
1240
|
-
*/
|
|
1241
|
-
sendAnalysisResultsToWorker() {
|
|
1242
|
-
if (this.sendAnalysisResults) {
|
|
1243
|
-
const frequencyData = this.frequencyData ? new Uint8Array(this.frequencyData) : new Uint8Array(0);
|
|
1244
|
-
this.sendAnalysisResults({
|
|
1245
|
-
type: "audio-analysis-update",
|
|
1246
|
-
data: {
|
|
1247
|
-
...this.audioState,
|
|
1248
|
-
frequencyData,
|
|
1249
|
-
// For getFrequencyData() access
|
|
1250
|
-
timestamp: performance.now()
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Reset audio values to defaults
|
|
1257
|
-
*/
|
|
1258
|
-
resetAudioValues() {
|
|
1259
|
-
this.audioState.volume.rms = 0;
|
|
1260
|
-
this.audioState.volume.peak = 0;
|
|
1261
|
-
for (const band in this.audioState.bands) {
|
|
1262
|
-
this.audioState.bands[band] = 0;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Reset all audio state (called when destroying)
|
|
1267
|
-
*/
|
|
1268
|
-
resetAudioState() {
|
|
1269
|
-
this.disconnectAudioStream();
|
|
1270
|
-
if (this.audioContext && this.audioContext.state !== "closed") {
|
|
1271
|
-
this.audioContext.close();
|
|
1272
|
-
this.audioContext = null;
|
|
1273
|
-
}
|
|
1274
|
-
this.resetAudioValues();
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Get current analysis configuration
|
|
1278
|
-
*/
|
|
1279
|
-
getAnalysisConfig() {
|
|
1280
|
-
return {
|
|
1281
|
-
fftSize: this.fftSize,
|
|
1282
|
-
smoothing: this.smoothingTimeConstant
|
|
1283
|
-
};
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
class VideoCoordinator {
|
|
1287
|
-
// Video elements for MediaStream processing
|
|
1288
|
-
videoElement = null;
|
|
1289
|
-
canvas = null;
|
|
1290
|
-
ctx = null;
|
|
1291
|
-
// Note: currentStream was removed as it was unused
|
|
1292
|
-
// Transfer coordination
|
|
1293
|
-
transferLoopId = null;
|
|
1294
|
-
isTransferRunning = false;
|
|
1295
|
-
lastTransferTime = 0;
|
|
1296
|
-
transferInterval = 1e3 / 30;
|
|
1297
|
-
// Transfer at 30 FPS
|
|
1298
|
-
// Video state (lightweight - main state is in worker)
|
|
1299
|
-
coordinatorState = {
|
|
1300
|
-
isConnected: false,
|
|
1301
|
-
sourceType: "",
|
|
1302
|
-
frameWidth: 0,
|
|
1303
|
-
frameHeight: 0
|
|
1304
|
-
};
|
|
1305
|
-
// Track if OffscreenCanvas has been sent to worker
|
|
1306
|
-
hasTransferredCanvas = false;
|
|
1307
|
-
// Callback to send data to worker
|
|
1308
|
-
sendToWorker = null;
|
|
1309
|
-
// Debug logging control
|
|
1310
|
-
debugMode = false;
|
|
1311
|
-
/**
|
|
1312
|
-
* Enable or disable debug logging
|
|
1313
|
-
*/
|
|
1314
|
-
setDebugMode(enabled) {
|
|
1315
|
-
this.debugMode = enabled;
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Debug logging helper
|
|
1319
|
-
*/
|
|
1320
|
-
debugLog(message, ...args) {
|
|
1321
|
-
if (this.debugMode) {
|
|
1322
|
-
console.log(message, ...args);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
constructor(sendToWorkerCallback) {
|
|
1326
|
-
this.handleVideoStreamUpdate = this.handleVideoStreamUpdate.bind(this);
|
|
1327
|
-
this.transferVideoFrame = this.transferVideoFrame.bind(this);
|
|
1328
|
-
this.sendToWorker = sendToWorkerCallback || null;
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Get the current video coordinator state (for host-side usage)
|
|
1332
|
-
*/
|
|
1333
|
-
getCoordinatorState() {
|
|
1334
|
-
return { ...this.coordinatorState };
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Handle video stream update (called from VijiCore)
|
|
1338
|
-
*/
|
|
1339
|
-
handleVideoStreamUpdate(data) {
|
|
1340
|
-
try {
|
|
1341
|
-
if (data.videoStream) {
|
|
1342
|
-
this.setVideoStream(data.videoStream);
|
|
1343
|
-
} else {
|
|
1344
|
-
this.disconnectVideoStream();
|
|
1345
|
-
}
|
|
1346
|
-
if (data.targetFrameRate || data.cvConfig) {
|
|
1347
|
-
this.sendConfigurationToWorker({
|
|
1348
|
-
...data.targetFrameRate && { targetFrameRate: data.targetFrameRate },
|
|
1349
|
-
...data.cvConfig && { cvConfig: data.cvConfig },
|
|
1350
|
-
timestamp: data.timestamp
|
|
1351
|
-
});
|
|
1352
|
-
}
|
|
1353
|
-
} catch (error) {
|
|
1354
|
-
console.error("Error handling video stream update:", error);
|
|
1355
|
-
this.coordinatorState.isConnected = false;
|
|
1356
|
-
this.sendDisconnectionToWorker();
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Set the video stream for processing
|
|
1361
|
-
*/
|
|
1362
|
-
async setVideoStream(videoStream) {
|
|
1363
|
-
this.disconnectVideoStream();
|
|
1364
|
-
const videoTracks = videoStream.getVideoTracks();
|
|
1365
|
-
if (videoTracks.length === 0) {
|
|
1366
|
-
console.warn("No video tracks in provided stream");
|
|
1367
|
-
this.coordinatorState.isConnected = false;
|
|
1368
|
-
this.sendDisconnectionToWorker();
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
try {
|
|
1372
|
-
this.videoElement = document.createElement("video");
|
|
1373
|
-
this.videoElement.autoplay = true;
|
|
1374
|
-
this.videoElement.muted = true;
|
|
1375
|
-
this.videoElement.playsInline = true;
|
|
1376
|
-
this.canvas = document.createElement("canvas");
|
|
1377
|
-
this.ctx = this.canvas.getContext("2d");
|
|
1378
|
-
if (!this.ctx) {
|
|
1379
|
-
throw new Error("Failed to get 2D context from canvas");
|
|
1380
|
-
}
|
|
1381
|
-
this.videoElement.srcObject = videoStream;
|
|
1382
|
-
await new Promise((resolve, reject) => {
|
|
1383
|
-
const timeout = setTimeout(() => reject(new Error("Video metadata load timeout")), 5e3);
|
|
1384
|
-
this.videoElement.addEventListener("loadedmetadata", async () => {
|
|
1385
|
-
clearTimeout(timeout);
|
|
1386
|
-
try {
|
|
1387
|
-
await this.videoElement.play();
|
|
1388
|
-
this.debugLog("✅ Video element is now playing:", {
|
|
1389
|
-
videoWidth: this.videoElement.videoWidth,
|
|
1390
|
-
videoHeight: this.videoElement.videoHeight,
|
|
1391
|
-
readyState: this.videoElement.readyState,
|
|
1392
|
-
paused: this.videoElement.paused
|
|
1393
|
-
});
|
|
1394
|
-
resolve();
|
|
1395
|
-
} catch (playError) {
|
|
1396
|
-
console.error("🔴 Failed to start video playback:", playError);
|
|
1397
|
-
reject(playError);
|
|
1398
|
-
}
|
|
1399
|
-
}, { once: true });
|
|
1400
|
-
this.videoElement.addEventListener("error", (e) => {
|
|
1401
|
-
clearTimeout(timeout);
|
|
1402
|
-
reject(new Error(`Video load error: ${e}`));
|
|
1403
|
-
}, { once: true });
|
|
1404
|
-
});
|
|
1405
|
-
this.canvas.width = this.videoElement.videoWidth;
|
|
1406
|
-
this.canvas.height = this.videoElement.videoHeight;
|
|
1407
|
-
this.coordinatorState.isConnected = true;
|
|
1408
|
-
this.coordinatorState.frameWidth = this.videoElement.videoWidth;
|
|
1409
|
-
this.coordinatorState.frameHeight = this.videoElement.videoHeight;
|
|
1410
|
-
this.coordinatorState.sourceType = "MediaStream";
|
|
1411
|
-
await this.transferOffscreenCanvasToWorker(this.videoElement.videoWidth, this.videoElement.videoHeight);
|
|
1412
|
-
this.startTransferLoop();
|
|
1413
|
-
this.debugLog("Video stream connected successfully (host-side coordinator)", {
|
|
1414
|
-
width: this.videoElement.videoWidth,
|
|
1415
|
-
height: this.videoElement.videoHeight
|
|
1416
|
-
});
|
|
1417
|
-
} catch (error) {
|
|
1418
|
-
console.error("Failed to set up video coordination:", error);
|
|
1419
|
-
this.coordinatorState.isConnected = false;
|
|
1420
|
-
this.disconnectVideoStream();
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* ✅ CORRECT: Transfer OffscreenCanvas to worker BEFORE getting context
|
|
1425
|
-
*/
|
|
1426
|
-
async transferOffscreenCanvasToWorker(width, height) {
|
|
1427
|
-
if (this.hasTransferredCanvas) {
|
|
1428
|
-
this.sendConfigurationToWorker({
|
|
1429
|
-
width,
|
|
1430
|
-
height,
|
|
1431
|
-
timestamp: performance.now()
|
|
1432
|
-
});
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
try {
|
|
1436
|
-
const offscreenCanvas = new OffscreenCanvas(width, height);
|
|
1437
|
-
if (this.sendToWorker) {
|
|
1438
|
-
this.sendToWorker({
|
|
1439
|
-
type: "video-canvas-setup",
|
|
1440
|
-
data: {
|
|
1441
|
-
offscreenCanvas,
|
|
1442
|
-
width,
|
|
1443
|
-
height,
|
|
1444
|
-
timestamp: performance.now()
|
|
1445
|
-
}
|
|
1446
|
-
}, [offscreenCanvas]);
|
|
1447
|
-
this.hasTransferredCanvas = true;
|
|
1448
|
-
this.debugLog("✅ OffscreenCanvas transferred to worker (correct approach)", {
|
|
1449
|
-
width,
|
|
1450
|
-
height
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
console.error("Failed to transfer OffscreenCanvas to worker:", error);
|
|
1455
|
-
throw error;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
/**
|
|
1459
|
-
* Disconnect current video stream and clean up resources
|
|
1460
|
-
*/
|
|
1461
|
-
disconnectVideoStream() {
|
|
1462
|
-
this.stopTransferLoop();
|
|
1463
|
-
if (this.videoElement) {
|
|
1464
|
-
this.videoElement.pause();
|
|
1465
|
-
this.videoElement.srcObject = null;
|
|
1466
|
-
this.videoElement = null;
|
|
1467
|
-
}
|
|
1468
|
-
if (this.canvas) {
|
|
1469
|
-
this.canvas = null;
|
|
1470
|
-
this.ctx = null;
|
|
1471
|
-
}
|
|
1472
|
-
this.coordinatorState.isConnected = false;
|
|
1473
|
-
this.coordinatorState.frameWidth = 0;
|
|
1474
|
-
this.coordinatorState.frameHeight = 0;
|
|
1475
|
-
this.coordinatorState.sourceType = "";
|
|
1476
|
-
this.hasTransferredCanvas = false;
|
|
1477
|
-
this.sendDisconnectionToWorker();
|
|
1478
|
-
this.debugLog("Video stream disconnected (host-side coordinator)");
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Start the video frame transfer loop
|
|
1482
|
-
*/
|
|
1483
|
-
startTransferLoop() {
|
|
1484
|
-
if (this.isTransferRunning) return;
|
|
1485
|
-
this.isTransferRunning = true;
|
|
1486
|
-
this.lastTransferTime = performance.now();
|
|
1487
|
-
this.transferVideoFrame();
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Stop the video frame transfer loop
|
|
1491
|
-
*/
|
|
1492
|
-
stopTransferLoop() {
|
|
1493
|
-
this.isTransferRunning = false;
|
|
1494
|
-
if (this.transferLoopId !== null) {
|
|
1495
|
-
cancelAnimationFrame(this.transferLoopId);
|
|
1496
|
-
this.transferLoopId = null;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Transfer video frame to worker using ImageBitmap (for worker to draw on its OffscreenCanvas)
|
|
1501
|
-
*/
|
|
1502
|
-
transferVideoFrame() {
|
|
1503
|
-
if (!this.isTransferRunning || !this.videoElement || !this.canvas || !this.ctx) {
|
|
1504
|
-
if (!this.isTransferRunning) {
|
|
1505
|
-
this.debugLog("🔴 Transfer loop stopped");
|
|
1506
|
-
}
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
const currentTime = performance.now();
|
|
1510
|
-
const deltaTime = currentTime - this.lastTransferTime;
|
|
1511
|
-
if (deltaTime >= this.transferInterval) {
|
|
1512
|
-
if (Math.random() < 0.01) {
|
|
1513
|
-
this.debugLog(`🔄 Transfer loop tick: ${deltaTime.toFixed(1)}ms since last frame`);
|
|
1514
|
-
}
|
|
1515
|
-
this.transferFrameToWorker().catch((error) => {
|
|
1516
|
-
console.error("🔴 Error transferring video frame to worker:", error);
|
|
1517
|
-
});
|
|
1518
|
-
this.lastTransferTime = currentTime;
|
|
1519
|
-
}
|
|
1520
|
-
this.transferLoopId = requestAnimationFrame(() => this.transferVideoFrame());
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Async frame transfer using ImageBitmap (for worker to draw)
|
|
1524
|
-
*/
|
|
1525
|
-
async transferFrameToWorker() {
|
|
1526
|
-
if (!this.videoElement || !this.canvas || !this.ctx) {
|
|
1527
|
-
console.warn("🔴 Frame transfer called but missing elements:", {
|
|
1528
|
-
hasVideo: !!this.videoElement,
|
|
1529
|
-
hasCanvas: !!this.canvas,
|
|
1530
|
-
hasCtx: !!this.ctx
|
|
1531
|
-
});
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
try {
|
|
1535
|
-
if (this.videoElement.readyState < 2) {
|
|
1536
|
-
console.warn("🔴 Video not ready for frame capture, readyState:", this.videoElement.readyState);
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
if (this.videoElement.videoWidth === 0 || this.videoElement.videoHeight === 0) {
|
|
1540
|
-
console.warn("🔴 Video has no dimensions:", {
|
|
1541
|
-
width: this.videoElement.videoWidth,
|
|
1542
|
-
height: this.videoElement.videoHeight
|
|
1543
|
-
});
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
this.ctx.drawImage(this.videoElement, 0, 0, this.canvas.width, this.canvas.height);
|
|
1547
|
-
const imageBitmap = await createImageBitmap(this.canvas);
|
|
1548
|
-
if (Math.random() < 0.01) {
|
|
1549
|
-
this.debugLog("✅ Frame captured and ImageBitmap created:", {
|
|
1550
|
-
videoDimensions: `${this.videoElement.videoWidth}x${this.videoElement.videoHeight}`,
|
|
1551
|
-
canvasDimensions: `${this.canvas.width}x${this.canvas.height}`,
|
|
1552
|
-
bitmapDimensions: `${imageBitmap.width}x${imageBitmap.height}`
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
this.sendFrameToWorker(imageBitmap);
|
|
1556
|
-
} catch (error) {
|
|
1557
|
-
console.error("🔴 Failed to create ImageBitmap:", error);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
/**
|
|
1561
|
-
* Send ImageBitmap frame to worker (for worker to draw on its OffscreenCanvas)
|
|
1562
|
-
*/
|
|
1563
|
-
sendFrameToWorker(imageBitmap) {
|
|
1564
|
-
if (this.sendToWorker) {
|
|
1565
|
-
this.sendToWorker({
|
|
1566
|
-
type: "video-frame-update",
|
|
1567
|
-
data: {
|
|
1568
|
-
imageBitmap,
|
|
1569
|
-
timestamp: performance.now()
|
|
1570
|
-
}
|
|
1571
|
-
}, [imageBitmap]);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
/**
|
|
1575
|
-
* Send configuration updates to worker
|
|
1576
|
-
*/
|
|
1577
|
-
sendConfigurationToWorker(config) {
|
|
1578
|
-
if (this.sendToWorker) {
|
|
1579
|
-
this.sendToWorker({
|
|
1580
|
-
type: "video-config-update",
|
|
1581
|
-
data: config
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
/**
|
|
1586
|
-
* Send disconnection notification to worker
|
|
1587
|
-
*/
|
|
1588
|
-
sendDisconnectionToWorker() {
|
|
1589
|
-
if (this.sendToWorker) {
|
|
1590
|
-
this.sendToWorker({
|
|
1591
|
-
type: "video-config-update",
|
|
1592
|
-
data: {
|
|
1593
|
-
disconnect: true,
|
|
1594
|
-
timestamp: performance.now()
|
|
1595
|
-
}
|
|
1596
|
-
});
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
/**
|
|
1600
|
-
* Handle image file as video source
|
|
1601
|
-
*/
|
|
1602
|
-
async setImageSource(imageFile) {
|
|
1603
|
-
try {
|
|
1604
|
-
const img = new Image();
|
|
1605
|
-
const url = URL.createObjectURL(imageFile);
|
|
1606
|
-
await new Promise((resolve, reject) => {
|
|
1607
|
-
img.onload = () => {
|
|
1608
|
-
URL.revokeObjectURL(url);
|
|
1609
|
-
resolve();
|
|
1610
|
-
};
|
|
1611
|
-
img.onerror = () => {
|
|
1612
|
-
URL.revokeObjectURL(url);
|
|
1613
|
-
reject(new Error("Failed to load image"));
|
|
1614
|
-
};
|
|
1615
|
-
img.src = url;
|
|
1616
|
-
});
|
|
1617
|
-
this.disconnectVideoStream();
|
|
1618
|
-
this.coordinatorState.isConnected = true;
|
|
1619
|
-
this.coordinatorState.frameWidth = img.width;
|
|
1620
|
-
this.coordinatorState.frameHeight = img.height;
|
|
1621
|
-
this.coordinatorState.sourceType = "Image";
|
|
1622
|
-
await this.transferOffscreenCanvasToWorker(img.width, img.height);
|
|
1623
|
-
const imageBitmap = await createImageBitmap(img);
|
|
1624
|
-
this.sendFrameToWorker(imageBitmap);
|
|
1625
|
-
this.debugLog("Image source set successfully (host-side coordinator)", {
|
|
1626
|
-
width: img.width,
|
|
1627
|
-
height: img.height
|
|
1628
|
-
});
|
|
1629
|
-
} catch (error) {
|
|
1630
|
-
console.error("Failed to set image source:", error);
|
|
1631
|
-
this.coordinatorState.isConnected = false;
|
|
1632
|
-
this.sendDisconnectionToWorker();
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
/**
|
|
1636
|
-
* Reset all video coordinator state (called when destroying)
|
|
1637
|
-
*/
|
|
1638
|
-
resetVideoState() {
|
|
1639
|
-
this.disconnectVideoStream();
|
|
1640
|
-
}
|
|
1641
|
-
/**
|
|
1642
|
-
* Get current coordinator configuration
|
|
1643
|
-
*/
|
|
1644
|
-
getCoordinatorConfig() {
|
|
1645
|
-
return {
|
|
1646
|
-
transferInterval: this.transferInterval
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
class VijiCore {
|
|
1651
|
-
iframeManager = null;
|
|
1652
|
-
workerManager = null;
|
|
1653
|
-
interactionManager = null;
|
|
1654
|
-
audioSystem = null;
|
|
1655
|
-
videoCoordinator = null;
|
|
1656
|
-
isInitialized = false;
|
|
1657
|
-
isDestroyed = false;
|
|
1658
|
-
isInitializing = false;
|
|
1659
|
-
instanceId;
|
|
1660
|
-
screenRefreshRate = 60;
|
|
1661
|
-
// Will be detected
|
|
1662
|
-
debugMode = false;
|
|
1663
|
-
// Debug logging control
|
|
1664
|
-
/**
|
|
1665
|
-
* Debug logging helper
|
|
1666
|
-
*/
|
|
1667
|
-
debugLog(message, ...args) {
|
|
1668
|
-
if (this.debugMode) {
|
|
1669
|
-
console.log(message, ...args);
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
// Configuration
|
|
1673
|
-
config;
|
|
1674
|
-
// Audio stream management
|
|
1675
|
-
currentAudioStream = null;
|
|
1676
|
-
// Video stream management
|
|
1677
|
-
currentVideoStream = null;
|
|
1678
|
-
// Interaction state management
|
|
1679
|
-
currentInteractionEnabled;
|
|
1680
|
-
// Parameter system for Phase 2
|
|
1681
|
-
parameterGroups = /* @__PURE__ */ new Map();
|
|
1682
|
-
parameterValues = /* @__PURE__ */ new Map();
|
|
1683
|
-
parametersInitialized = false;
|
|
1684
|
-
// Event listeners for parameter system
|
|
1685
|
-
parameterListeners = /* @__PURE__ */ new Map();
|
|
1686
|
-
parameterDefinedListeners = /* @__PURE__ */ new Set();
|
|
1687
|
-
parameterErrorListeners = /* @__PURE__ */ new Set();
|
|
1688
|
-
capabilitiesChangeListeners = /* @__PURE__ */ new Set();
|
|
1689
|
-
// Performance tracking (basic for Phase 1)
|
|
1690
|
-
stats = {
|
|
1691
|
-
frameTime: 0,
|
|
1692
|
-
resolution: { width: 0, height: 0 },
|
|
1693
|
-
scale: 1,
|
|
1694
|
-
frameRate: {
|
|
1695
|
-
mode: "full",
|
|
1696
|
-
screenRefreshRate: 60,
|
|
1697
|
-
effectiveRefreshRate: 60
|
|
1698
|
-
},
|
|
1699
|
-
rendererType: "native",
|
|
1700
|
-
parameterCount: 0
|
|
1701
|
-
};
|
|
1702
|
-
constructor(config) {
|
|
1703
|
-
this.validateConfig(config);
|
|
1704
|
-
this.instanceId = `viji-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1705
|
-
this.config = {
|
|
1706
|
-
...config,
|
|
1707
|
-
frameRateMode: config.frameRateMode || "full",
|
|
1708
|
-
autoOptimize: config.autoOptimize ?? true,
|
|
1709
|
-
parameters: config.parameters || [],
|
|
1710
|
-
noInputs: config.noInputs ?? false,
|
|
1711
|
-
allowUserInteraction: config.allowUserInteraction ?? true
|
|
1712
|
-
};
|
|
1713
|
-
this.currentInteractionEnabled = this.config.allowUserInteraction;
|
|
1714
|
-
this.debugLog(`VijiCore instance created: ${this.instanceId}`);
|
|
1715
|
-
}
|
|
1716
|
-
/**
|
|
1717
|
-
* Capture current scene frame as an image Blob.
|
|
1718
|
-
* Resolution can be a scale (0-1+) or explicit width/height with center-crop.
|
|
1719
|
-
*/
|
|
1720
|
-
async captureFrame(options = {}) {
|
|
1721
|
-
if (!this.workerManager || !this.workerManager.ready) {
|
|
1722
|
-
throw new VijiCoreError("Core not initialized", "NOT_INITIALIZED");
|
|
1723
|
-
}
|
|
1724
|
-
const response = await this.workerManager.sendMessage("capture-frame", {
|
|
1725
|
-
type: options.type,
|
|
1726
|
-
resolution: options.resolution
|
|
1727
|
-
}, 1e4);
|
|
1728
|
-
const buffer = response.buffer;
|
|
1729
|
-
const mimeType = response.mimeType || options.type || "image/jpeg";
|
|
1730
|
-
return new Blob([buffer], { type: mimeType });
|
|
1731
|
-
}
|
|
1732
|
-
/**
|
|
1733
|
-
* Enable or disable debug logging
|
|
1734
|
-
*/
|
|
1735
|
-
setDebugMode(enabled) {
|
|
1736
|
-
this.debugMode = enabled;
|
|
1737
|
-
if (this.iframeManager && "setDebugMode" in this.iframeManager) {
|
|
1738
|
-
this.iframeManager.setDebugMode(enabled);
|
|
1739
|
-
}
|
|
1740
|
-
if (this.workerManager && "setDebugMode" in this.workerManager) {
|
|
1741
|
-
this.workerManager.setDebugMode(enabled);
|
|
1742
|
-
}
|
|
1743
|
-
if (this.interactionManager && "setDebugMode" in this.interactionManager) {
|
|
1744
|
-
this.interactionManager.setDebugMode(enabled);
|
|
1745
|
-
}
|
|
1746
|
-
if (this.audioSystem && "setDebugMode" in this.audioSystem) {
|
|
1747
|
-
this.audioSystem.setDebugMode(enabled);
|
|
1748
|
-
}
|
|
1749
|
-
if (this.videoCoordinator && "setDebugMode" in this.videoCoordinator) {
|
|
1750
|
-
this.videoCoordinator.setDebugMode(enabled);
|
|
1751
|
-
}
|
|
1752
|
-
if (this.workerManager) {
|
|
1753
|
-
this.workerManager.postMessage("debug-mode", { enabled });
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
/**
|
|
1757
|
-
* Get current debug mode status
|
|
1758
|
-
*/
|
|
1759
|
-
getDebugMode() {
|
|
1760
|
-
return this.debugMode;
|
|
1761
|
-
}
|
|
1762
|
-
/**
|
|
1763
|
-
* Initializes the core components in sequence
|
|
1764
|
-
*/
|
|
1765
|
-
async initialize() {
|
|
1766
|
-
try {
|
|
1767
|
-
if (this.isDestroyed) {
|
|
1768
|
-
throw new VijiCoreError("Cannot initialize destroyed instance", "INSTANCE_DESTROYED");
|
|
1769
|
-
}
|
|
1770
|
-
if (this.isInitializing) {
|
|
1771
|
-
throw new VijiCoreError("Initialization already in progress", "CONCURRENT_INITIALIZATION");
|
|
1772
|
-
}
|
|
1773
|
-
if (this.isInitialized) {
|
|
1774
|
-
throw new VijiCoreError("Core already initialized", "ALREADY_INITIALIZED");
|
|
1775
|
-
}
|
|
1776
|
-
this.isInitializing = true;
|
|
1777
|
-
this.debugLog(`Starting VijiCore initialization... (${this.instanceId})`);
|
|
1778
|
-
this.config.hostContainer.innerHTML = "";
|
|
1779
|
-
this.iframeManager = new IFrameManager(
|
|
1780
|
-
this.config.hostContainer
|
|
1781
|
-
);
|
|
1782
|
-
await this.iframeManager.createSecureIFrame();
|
|
1783
|
-
const offscreenCanvas = await this.createCanvasWithRetry();
|
|
1784
|
-
this.interactionManager = new InteractionManager();
|
|
1785
|
-
this.setupInteractionSystem();
|
|
1786
|
-
this.workerManager = new WorkerManager(
|
|
1787
|
-
this.config.sceneCode,
|
|
1788
|
-
offscreenCanvas
|
|
1789
|
-
);
|
|
1790
|
-
this.setupCommunication();
|
|
1791
|
-
await this.workerManager.createWorker();
|
|
1792
|
-
this.audioSystem = new AudioSystem((message) => {
|
|
1793
|
-
if (this.workerManager) {
|
|
1794
|
-
this.workerManager.postMessage(message.type, message.data);
|
|
1795
|
-
}
|
|
1796
|
-
});
|
|
1797
|
-
this.videoCoordinator = new VideoCoordinator((message, transfer) => {
|
|
1798
|
-
if (this.workerManager) {
|
|
1799
|
-
if (transfer && transfer.length > 0) {
|
|
1800
|
-
this.workerManager.postMessage(message.type, message.data, transfer);
|
|
1801
|
-
} else {
|
|
1802
|
-
this.workerManager.postMessage(message.type, message.data);
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
});
|
|
1806
|
-
const effectiveResolution = this.iframeManager.getEffectiveResolution();
|
|
1807
|
-
this.workerManager.postMessage("resolution-update", {
|
|
1808
|
-
effectiveWidth: effectiveResolution.width,
|
|
1809
|
-
effectiveHeight: effectiveResolution.height
|
|
1810
|
-
});
|
|
1811
|
-
await this.detectScreenRefreshRate();
|
|
1812
|
-
this.workerManager.postMessage("refresh-rate-update", {
|
|
1813
|
-
screenRefreshRate: this.screenRefreshRate
|
|
1814
|
-
});
|
|
1815
|
-
if (this.config.audioStream) {
|
|
1816
|
-
await this.setAudioStream(this.config.audioStream);
|
|
1817
|
-
}
|
|
1818
|
-
if (this.config.videoStream) {
|
|
1819
|
-
await this.setVideoStream(this.config.videoStream);
|
|
1820
|
-
}
|
|
1821
|
-
this.stats.resolution = effectiveResolution;
|
|
1822
|
-
this.stats.scale = this.iframeManager.getScale();
|
|
1823
|
-
this.updateFrameRateStats();
|
|
1824
|
-
this.isInitialized = true;
|
|
1825
|
-
this.isInitializing = false;
|
|
1826
|
-
this.debugLog(`VijiCore initialized successfully (${this.instanceId}):`, {
|
|
1827
|
-
resolution: `${effectiveResolution.width}x${effectiveResolution.height}`,
|
|
1828
|
-
frameRateMode: this.config.frameRateMode,
|
|
1829
|
-
hasAudio: !!this.config.audioStream,
|
|
1830
|
-
hasVideo: !!this.config.videoStream
|
|
1831
|
-
});
|
|
1832
|
-
} catch (error) {
|
|
1833
|
-
this.isInitializing = false;
|
|
1834
|
-
await this.cleanup();
|
|
1835
|
-
throw new VijiCoreError(
|
|
1836
|
-
`Failed to initialize VijiCore: ${error}`,
|
|
1837
|
-
"INITIALIZATION_ERROR",
|
|
1838
|
-
{ error, config: this.config }
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
/**
|
|
1843
|
-
* Creates canvas with retry logic to handle timing issues
|
|
1844
|
-
*/
|
|
1845
|
-
async createCanvasWithRetry() {
|
|
1846
|
-
const maxRetries = 3;
|
|
1847
|
-
const retryDelay = 200;
|
|
1848
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
1849
|
-
if (this.isDestroyed || !this.isInitializing) {
|
|
1850
|
-
throw new VijiCoreError("Initialization cancelled", "INITIALIZATION_CANCELLED");
|
|
1851
|
-
}
|
|
1852
|
-
if (!this.iframeManager) {
|
|
1853
|
-
throw new VijiCoreError("IFrameManager not available", "MANAGER_NOT_READY");
|
|
1854
|
-
}
|
|
1855
|
-
try {
|
|
1856
|
-
return await this.iframeManager.createCanvas();
|
|
1857
|
-
} catch (error) {
|
|
1858
|
-
console.warn(`Canvas creation attempt ${i + 1}/${maxRetries} failed:`, error);
|
|
1859
|
-
if (i === maxRetries - 1) {
|
|
1860
|
-
throw error;
|
|
1861
|
-
}
|
|
1862
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
throw new VijiCoreError("Canvas creation failed after all retries", "CANVAS_CREATION_TIMEOUT");
|
|
1866
|
-
}
|
|
1867
|
-
/**
|
|
1868
|
-
* Sets up the interaction system for Phase 7
|
|
1869
|
-
*/
|
|
1870
|
-
setupInteractionSystem() {
|
|
1871
|
-
if (!this.iframeManager || !this.interactionManager) return;
|
|
1872
|
-
this.iframeManager.onInteractionEvent("mouse-update", (data) => {
|
|
1873
|
-
this.interactionManager?.updateMouse(data);
|
|
1874
|
-
if (this.workerManager) {
|
|
1875
|
-
this.workerManager.postMessage("mouse-update", data);
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
1878
|
-
this.iframeManager.onInteractionEvent("keyboard-update", (data) => {
|
|
1879
|
-
this.interactionManager?.updateKeyboard(data);
|
|
1880
|
-
if (this.workerManager) {
|
|
1881
|
-
this.workerManager.postMessage("keyboard-update", data);
|
|
1882
|
-
}
|
|
1883
|
-
});
|
|
1884
|
-
this.iframeManager.onInteractionEvent("touch-update", (data) => {
|
|
1885
|
-
this.interactionManager?.updateTouch(data);
|
|
1886
|
-
if (this.workerManager) {
|
|
1887
|
-
this.workerManager.postMessage("touch-update", data);
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
this.iframeManager.setInteractionEnabled(this.currentInteractionEnabled);
|
|
1891
|
-
}
|
|
1892
|
-
/**
|
|
1893
|
-
* Sets up communication between components
|
|
1894
|
-
*/
|
|
1895
|
-
setupCommunication() {
|
|
1896
|
-
if (!this.workerManager) return;
|
|
1897
|
-
this.workerManager.onMessage("ready", (data) => {
|
|
1898
|
-
this.debugLog("Worker ready:", data);
|
|
1899
|
-
if (data.rendererType !== void 0) {
|
|
1900
|
-
this.stats.rendererType = data.rendererType;
|
|
1901
|
-
}
|
|
1902
|
-
});
|
|
1903
|
-
this.workerManager.onMessage("error", (data) => {
|
|
1904
|
-
console.error("Worker error:", data);
|
|
1905
|
-
});
|
|
1906
|
-
this.workerManager.onMessage("performance-warning", (data) => {
|
|
1907
|
-
console.warn("Performance warning:", data);
|
|
1908
|
-
});
|
|
1909
|
-
this.workerManager.onMessage("performance-update", (data) => {
|
|
1910
|
-
if (data.effectiveRefreshRate !== void 0) {
|
|
1911
|
-
this.stats.frameRate.effectiveRefreshRate = data.effectiveRefreshRate;
|
|
1912
|
-
this.stats.frameRate.mode = data.frameRateMode;
|
|
1913
|
-
this.stats.frameRate.screenRefreshRate = data.screenRefreshRate;
|
|
1914
|
-
}
|
|
1915
|
-
if (data.rendererType !== void 0) {
|
|
1916
|
-
this.stats.rendererType = data.rendererType;
|
|
1917
|
-
}
|
|
1918
|
-
if (data.parameterCount !== void 0) {
|
|
1919
|
-
this.stats.parameterCount = data.parameterCount;
|
|
1920
|
-
}
|
|
1921
|
-
if (data.cv !== void 0) {
|
|
1922
|
-
this.stats.cv = data.cv;
|
|
1923
|
-
}
|
|
1924
|
-
});
|
|
1925
|
-
this.workerManager.onMessage("parameters-defined", (data) => {
|
|
1926
|
-
this.handleParametersDefined(data);
|
|
1927
|
-
});
|
|
1928
|
-
this.workerManager.onMessage("parameter-validation-error", (data) => {
|
|
1929
|
-
this.handleParameterError(data);
|
|
1930
|
-
});
|
|
1931
|
-
}
|
|
1932
|
-
// Parameter system implementation for Phase 2
|
|
1933
|
-
/**
|
|
1934
|
-
* Handle parameter definitions received from worker
|
|
1935
|
-
*/
|
|
1936
|
-
handleParametersDefined(data) {
|
|
1937
|
-
try {
|
|
1938
|
-
this.parameterGroups.clear();
|
|
1939
|
-
this.parameterValues.clear();
|
|
1940
|
-
for (const group of data.groups) {
|
|
1941
|
-
this.parameterGroups.set(group.groupName, group);
|
|
1942
|
-
for (const [paramName, paramDef] of Object.entries(group.parameters)) {
|
|
1943
|
-
this.parameterValues.set(paramName, paramDef.defaultValue);
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
this.parametersInitialized = true;
|
|
1947
|
-
this.debugLog(`Parameters initialized: ${this.parameterValues.size} parameters in ${this.parameterGroups.size} groups`);
|
|
1948
|
-
this.syncAllParametersToWorker();
|
|
1949
|
-
for (const listener of this.parameterDefinedListeners) {
|
|
1950
|
-
try {
|
|
1951
|
-
listener(Array.from(this.parameterGroups.values()));
|
|
1952
|
-
} catch (error) {
|
|
1953
|
-
console.error("Error in parameter defined listener:", error);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
} catch (error) {
|
|
1957
|
-
console.error("Error handling parameters defined:", error);
|
|
1958
|
-
this.handleParameterError({
|
|
1959
|
-
message: `Failed to process parameter definitions: ${error.message}`,
|
|
1960
|
-
code: "PARAMETER_PROCESSING_ERROR"
|
|
1961
|
-
});
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
/**
|
|
1965
|
-
* Handle parameter validation errors
|
|
1966
|
-
*/
|
|
1967
|
-
handleParameterError(error) {
|
|
1968
|
-
console.error("Parameter error:", error);
|
|
1969
|
-
for (const listener of this.parameterErrorListeners) {
|
|
1970
|
-
try {
|
|
1971
|
-
listener(error);
|
|
1972
|
-
} catch (listenerError) {
|
|
1973
|
-
console.error("Error in parameter error listener:", listenerError);
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
/**
|
|
1978
|
-
* Get parameter definition by name
|
|
1979
|
-
*/
|
|
1980
|
-
getParameterDefinition(name) {
|
|
1981
|
-
for (const group of this.parameterGroups.values()) {
|
|
1982
|
-
if (group.parameters[name]) {
|
|
1983
|
-
return group.parameters[name];
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
return void 0;
|
|
1987
|
-
}
|
|
1988
|
-
/**
|
|
1989
|
-
* Set a single parameter value
|
|
1990
|
-
* Handles all parameter types including images intelligently
|
|
1991
|
-
*/
|
|
1992
|
-
async setParameter(name, value) {
|
|
1993
|
-
this.validateReady();
|
|
1994
|
-
if (!this.parametersInitialized) {
|
|
1995
|
-
throw new VijiCoreError("Parameters not yet initialized", "PARAMETERS_NOT_INITIALIZED");
|
|
1996
|
-
}
|
|
1997
|
-
if (!this.parameterValues.has(name)) {
|
|
1998
|
-
throw new VijiCoreError(`Unknown parameter: ${name}`, "UNKNOWN_PARAMETER");
|
|
1999
|
-
}
|
|
2000
|
-
const paramDef = this.getParameterDefinition(name);
|
|
2001
|
-
if (paramDef?.type === "image") {
|
|
2002
|
-
await this.handleImageParameter(name, value);
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
const oldValue = this.parameterValues.get(name);
|
|
2006
|
-
this.parameterValues.set(name, value);
|
|
2007
|
-
if (this.workerManager) {
|
|
2008
|
-
this.workerManager.postMessage("parameter-update", {
|
|
2009
|
-
name,
|
|
2010
|
-
value
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
if (oldValue !== value) {
|
|
2014
|
-
this.notifyParameterListeners(name, value);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
/**
|
|
2018
|
-
* Internal method to handle image parameter loading and transfer
|
|
2019
|
-
* Loads the image on the host side and creates TWO ImageBitmaps:
|
|
2020
|
-
* - One for the host (listeners, previews)
|
|
2021
|
-
* - One to transfer to the worker
|
|
2022
|
-
*/
|
|
2023
|
-
async handleImageParameter(name, value) {
|
|
2024
|
-
try {
|
|
2025
|
-
let hostImageBitmap = null;
|
|
2026
|
-
let workerImageBitmap = null;
|
|
2027
|
-
if (value === null || value === void 0) {
|
|
2028
|
-
hostImageBitmap = null;
|
|
2029
|
-
workerImageBitmap = null;
|
|
2030
|
-
} else if (typeof value === "string") {
|
|
2031
|
-
const response = await fetch(value);
|
|
2032
|
-
const blob = await response.blob();
|
|
2033
|
-
[hostImageBitmap, workerImageBitmap] = await Promise.all([
|
|
2034
|
-
createImageBitmap(blob),
|
|
2035
|
-
createImageBitmap(blob)
|
|
2036
|
-
]);
|
|
2037
|
-
} else if (value instanceof File || value instanceof Blob) {
|
|
2038
|
-
[hostImageBitmap, workerImageBitmap] = await Promise.all([
|
|
2039
|
-
createImageBitmap(value),
|
|
2040
|
-
createImageBitmap(value)
|
|
2041
|
-
]);
|
|
2042
|
-
} else if (value instanceof ImageBitmap) {
|
|
2043
|
-
const canvas = document.createElement("canvas");
|
|
2044
|
-
canvas.width = value.width;
|
|
2045
|
-
canvas.height = value.height;
|
|
2046
|
-
const ctx = canvas.getContext("2d");
|
|
2047
|
-
ctx.drawImage(value, 0, 0);
|
|
2048
|
-
hostImageBitmap = value;
|
|
2049
|
-
workerImageBitmap = await createImageBitmap(canvas);
|
|
2050
|
-
} else {
|
|
2051
|
-
throw new Error(`Invalid value type for image parameter. Expected File, Blob, string (URL), or null.`);
|
|
2052
|
-
}
|
|
2053
|
-
const oldValue = this.parameterValues.get(name);
|
|
2054
|
-
this.parameterValues.set(name, hostImageBitmap);
|
|
2055
|
-
if (this.workerManager) {
|
|
2056
|
-
const messageData = {
|
|
2057
|
-
name,
|
|
2058
|
-
value: workerImageBitmap
|
|
2059
|
-
};
|
|
2060
|
-
const transferList = workerImageBitmap ? [workerImageBitmap] : [];
|
|
2061
|
-
this.workerManager.postMessage("parameter-update", messageData, transferList);
|
|
2062
|
-
}
|
|
2063
|
-
if (oldValue !== hostImageBitmap) {
|
|
2064
|
-
this.notifyParameterListeners(name, hostImageBitmap);
|
|
2065
|
-
}
|
|
2066
|
-
this.debugLog(`Image parameter '${name}' ${hostImageBitmap ? "loaded" : "cleared"} (${this.instanceId})`);
|
|
2067
|
-
} catch (error) {
|
|
2068
|
-
throw new VijiCoreError(
|
|
2069
|
-
`Failed to load image for parameter '${name}': ${error}`,
|
|
2070
|
-
"IMAGE_LOAD_ERROR",
|
|
2071
|
-
{ error, name }
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
/**
|
|
2076
|
-
* Set multiple parameter values efficiently
|
|
2077
|
-
*/
|
|
2078
|
-
async setParameters(values) {
|
|
2079
|
-
this.validateReady();
|
|
2080
|
-
if (!this.parametersInitialized) {
|
|
2081
|
-
throw new VijiCoreError("Parameters not yet initialized", "PARAMETERS_NOT_INITIALIZED");
|
|
2082
|
-
}
|
|
2083
|
-
const updates = [];
|
|
2084
|
-
const changedParams = [];
|
|
2085
|
-
for (const [name, value] of Object.entries(values)) {
|
|
2086
|
-
if (!this.parameterValues.has(name)) {
|
|
2087
|
-
console.warn(`Unknown parameter: ${name}`);
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
|
-
const oldValue = this.parameterValues.get(name);
|
|
2091
|
-
this.parameterValues.set(name, value);
|
|
2092
|
-
updates.push({
|
|
2093
|
-
name,
|
|
2094
|
-
value
|
|
2095
|
-
});
|
|
2096
|
-
if (oldValue !== value) {
|
|
2097
|
-
changedParams.push({ name, value });
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
if (updates.length > 0 && this.workerManager) {
|
|
2101
|
-
this.workerManager.postMessage("parameter-batch-update", {
|
|
2102
|
-
updates
|
|
2103
|
-
});
|
|
2104
|
-
}
|
|
2105
|
-
for (const { name, value } of changedParams) {
|
|
2106
|
-
this.notifyParameterListeners(name, value);
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
/**
|
|
2110
|
-
* Get current parameter value
|
|
2111
|
-
*/
|
|
2112
|
-
getParameter(name) {
|
|
2113
|
-
return this.parameterValues.get(name);
|
|
2114
|
-
}
|
|
2115
|
-
/**
|
|
2116
|
-
* Get all current parameter values
|
|
2117
|
-
*/
|
|
2118
|
-
getParameterValues() {
|
|
2119
|
-
const values = {};
|
|
2120
|
-
for (const [name, value] of this.parameterValues) {
|
|
2121
|
-
values[name] = value;
|
|
2122
|
-
}
|
|
2123
|
-
return values;
|
|
2124
|
-
}
|
|
2125
|
-
/**
|
|
2126
|
-
* Get parameter groups (for UI generation)
|
|
2127
|
-
*/
|
|
2128
|
-
getParameterGroups() {
|
|
2129
|
-
return Array.from(this.parameterGroups.values());
|
|
2130
|
-
}
|
|
2131
|
-
/**
|
|
2132
|
-
* Get current core capabilities (what's currently active)
|
|
2133
|
-
*/
|
|
2134
|
-
getCapabilities() {
|
|
2135
|
-
return {
|
|
2136
|
-
hasAudio: this.currentAudioStream !== null,
|
|
2137
|
-
hasVideo: this.currentVideoStream !== null,
|
|
2138
|
-
hasInteraction: this.currentInteractionEnabled,
|
|
2139
|
-
hasGeneral: true
|
|
2140
|
-
// General parameters are always available
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2143
|
-
/**
|
|
2144
|
-
* Get parameter groups filtered by active capabilities
|
|
2145
|
-
*/
|
|
2146
|
-
getVisibleParameterGroups() {
|
|
2147
|
-
const capabilities = this.getCapabilities();
|
|
2148
|
-
const allGroups = this.getParameterGroups();
|
|
2149
|
-
return allGroups.filter((group) => {
|
|
2150
|
-
switch (group.category) {
|
|
2151
|
-
case "audio":
|
|
2152
|
-
return capabilities.hasAudio;
|
|
2153
|
-
case "video":
|
|
2154
|
-
return capabilities.hasVideo;
|
|
2155
|
-
case "interaction":
|
|
2156
|
-
return capabilities.hasInteraction;
|
|
2157
|
-
case "general":
|
|
2158
|
-
default:
|
|
2159
|
-
return capabilities.hasGeneral;
|
|
2160
|
-
}
|
|
2161
|
-
}).map((group) => ({
|
|
2162
|
-
...group,
|
|
2163
|
-
// Also filter individual parameters within groups
|
|
2164
|
-
parameters: Object.fromEntries(
|
|
2165
|
-
Object.entries(group.parameters).filter(([_, paramDef]) => {
|
|
2166
|
-
switch (paramDef.category) {
|
|
2167
|
-
case "audio":
|
|
2168
|
-
return capabilities.hasAudio;
|
|
2169
|
-
case "video":
|
|
2170
|
-
return capabilities.hasVideo;
|
|
2171
|
-
case "interaction":
|
|
2172
|
-
return capabilities.hasInteraction;
|
|
2173
|
-
case "general":
|
|
2174
|
-
default:
|
|
2175
|
-
return capabilities.hasGeneral;
|
|
2176
|
-
}
|
|
2177
|
-
})
|
|
2178
|
-
)
|
|
2179
|
-
}));
|
|
2180
|
-
}
|
|
2181
|
-
/**
|
|
2182
|
-
* Get all parameter groups without capability filtering.
|
|
2183
|
-
* Returns a deep-cloned structure to prevent external mutation.
|
|
2184
|
-
*/
|
|
2185
|
-
getAllParameterGroups() {
|
|
2186
|
-
const allGroups = this.getParameterGroups();
|
|
2187
|
-
return allGroups.map((group) => ({
|
|
2188
|
-
...group,
|
|
2189
|
-
parameters: Object.fromEntries(
|
|
2190
|
-
Object.entries(group.parameters).map(([name, def]) => {
|
|
2191
|
-
const clonedDef = { ...def };
|
|
2192
|
-
if (def.config) {
|
|
2193
|
-
clonedDef.config = { ...def.config };
|
|
2194
|
-
}
|
|
2195
|
-
return [name, clonedDef];
|
|
2196
|
-
})
|
|
2197
|
-
)
|
|
2198
|
-
}));
|
|
2199
|
-
}
|
|
2200
|
-
/**
|
|
2201
|
-
* Check if a specific parameter category is currently active
|
|
2202
|
-
*/
|
|
2203
|
-
isCategoryActive(category) {
|
|
2204
|
-
const capabilities = this.getCapabilities();
|
|
2205
|
-
switch (category) {
|
|
2206
|
-
case "audio":
|
|
2207
|
-
return capabilities.hasAudio;
|
|
2208
|
-
case "video":
|
|
2209
|
-
return capabilities.hasVideo;
|
|
2210
|
-
case "interaction":
|
|
2211
|
-
return capabilities.hasInteraction;
|
|
2212
|
-
case "general":
|
|
2213
|
-
default:
|
|
2214
|
-
return capabilities.hasGeneral;
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
/**
|
|
2218
|
-
* Check if parameters have been initialized
|
|
2219
|
-
*/
|
|
2220
|
-
get parametersReady() {
|
|
2221
|
-
return this.parametersInitialized;
|
|
2222
|
-
}
|
|
2223
|
-
/**
|
|
2224
|
-
* Send all current parameter values to worker (used for initial sync)
|
|
2225
|
-
*/
|
|
2226
|
-
syncAllParametersToWorker() {
|
|
2227
|
-
if (!this.workerManager || this.parameterValues.size === 0) {
|
|
2228
|
-
return;
|
|
2229
|
-
}
|
|
2230
|
-
const updates = [];
|
|
2231
|
-
for (const [name, value] of this.parameterValues) {
|
|
2232
|
-
updates.push({
|
|
2233
|
-
name,
|
|
2234
|
-
value
|
|
2235
|
-
});
|
|
2236
|
-
}
|
|
2237
|
-
this.workerManager.postMessage("parameter-batch-update", {
|
|
2238
|
-
updates
|
|
2239
|
-
});
|
|
2240
|
-
this.debugLog(`Synced ${updates.length} parameter values to worker`);
|
|
2241
|
-
}
|
|
2242
|
-
/**
|
|
2243
|
-
* Add listener for when parameters are defined
|
|
2244
|
-
*/
|
|
2245
|
-
onParametersDefined(listener) {
|
|
2246
|
-
this.parameterDefinedListeners.add(listener);
|
|
2247
|
-
}
|
|
2248
|
-
/**
|
|
2249
|
-
* Remove parameter defined listener
|
|
2250
|
-
*/
|
|
2251
|
-
offParametersDefined(listener) {
|
|
2252
|
-
this.parameterDefinedListeners.delete(listener);
|
|
2253
|
-
}
|
|
2254
|
-
/**
|
|
2255
|
-
* Add listener for parameter value changes
|
|
2256
|
-
*/
|
|
2257
|
-
onParameterChange(parameterName, listener) {
|
|
2258
|
-
if (!this.parameterListeners.has(parameterName)) {
|
|
2259
|
-
this.parameterListeners.set(parameterName, /* @__PURE__ */ new Set());
|
|
2260
|
-
}
|
|
2261
|
-
this.parameterListeners.get(parameterName).add(listener);
|
|
2262
|
-
}
|
|
2263
|
-
/**
|
|
2264
|
-
* Remove parameter change listener
|
|
2265
|
-
*/
|
|
2266
|
-
offParameterChange(parameterName, listener) {
|
|
2267
|
-
const listeners = this.parameterListeners.get(parameterName);
|
|
2268
|
-
if (listeners) {
|
|
2269
|
-
listeners.delete(listener);
|
|
2270
|
-
if (listeners.size === 0) {
|
|
2271
|
-
this.parameterListeners.delete(parameterName);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
/**
|
|
2276
|
-
* Add listener for parameter errors
|
|
2277
|
-
*/
|
|
2278
|
-
onParameterError(listener) {
|
|
2279
|
-
this.parameterErrorListeners.add(listener);
|
|
2280
|
-
}
|
|
2281
|
-
/**
|
|
2282
|
-
* Add listener for when core capabilities change (audio/video/interaction state)
|
|
2283
|
-
*/
|
|
2284
|
-
onCapabilitiesChange(listener) {
|
|
2285
|
-
this.capabilitiesChangeListeners.add(listener);
|
|
2286
|
-
}
|
|
2287
|
-
/**
|
|
2288
|
-
* Remove capabilities change listener
|
|
2289
|
-
*/
|
|
2290
|
-
removeCapabilitiesListener(listener) {
|
|
2291
|
-
this.capabilitiesChangeListeners.delete(listener);
|
|
2292
|
-
}
|
|
2293
|
-
/**
|
|
2294
|
-
* Notify capability change listeners
|
|
2295
|
-
*/
|
|
2296
|
-
notifyCapabilitiesChange() {
|
|
2297
|
-
const capabilities = this.getCapabilities();
|
|
2298
|
-
for (const listener of this.capabilitiesChangeListeners) {
|
|
2299
|
-
try {
|
|
2300
|
-
listener(capabilities);
|
|
2301
|
-
} catch (error) {
|
|
2302
|
-
console.error("Error in capabilities change listener:", error);
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
/**
|
|
2307
|
-
* Remove parameter error listener
|
|
2308
|
-
*/
|
|
2309
|
-
offParameterError(listener) {
|
|
2310
|
-
this.parameterErrorListeners.delete(listener);
|
|
2311
|
-
}
|
|
2312
|
-
/**
|
|
2313
|
-
* Notify parameter change listeners
|
|
2314
|
-
*/
|
|
2315
|
-
notifyParameterListeners(name, value) {
|
|
2316
|
-
const listeners = this.parameterListeners.get(name);
|
|
2317
|
-
if (listeners) {
|
|
2318
|
-
for (const listener of listeners) {
|
|
2319
|
-
try {
|
|
2320
|
-
listener(value);
|
|
2321
|
-
} catch (error) {
|
|
2322
|
-
console.error(`Error in parameter listener for '${name}':`, error);
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
/**
|
|
2328
|
-
* Sets the scene frame rate mode
|
|
2329
|
-
* @param mode - 'full' for every animation frame, 'half' for every second frame
|
|
2330
|
-
*/
|
|
2331
|
-
async setFrameRate(mode) {
|
|
2332
|
-
this.validateReady();
|
|
2333
|
-
this.config.frameRateMode = mode;
|
|
2334
|
-
if (this.workerManager) {
|
|
2335
|
-
this.workerManager.postMessage("frame-rate-update", { mode });
|
|
2336
|
-
}
|
|
2337
|
-
this.updateFrameRateStats();
|
|
2338
|
-
this.debugLog(`Scene frame rate set to ${mode} (${this.instanceId})`);
|
|
2339
|
-
}
|
|
2340
|
-
/**
|
|
2341
|
-
* Sets the CV processing frame rate mode (relative to scene frame rate)
|
|
2342
|
-
* @param mode - CV processing rate: 'full', 'half', 'quarter', or 'eighth' of scene rate
|
|
2343
|
-
*/
|
|
2344
|
-
async setCVFrameRate(mode) {
|
|
2345
|
-
this.validateReady();
|
|
2346
|
-
if (this.workerManager) {
|
|
2347
|
-
this.workerManager.postMessage("cv-frame-rate-update", { mode });
|
|
2348
|
-
}
|
|
2349
|
-
this.debugLog(`CV frame rate set to ${mode} of scene rate (${this.instanceId})`);
|
|
2350
|
-
}
|
|
2351
|
-
/**
|
|
2352
|
-
* Updates the canvas resolution by sending effective dimensions to the worker
|
|
2353
|
-
*/
|
|
2354
|
-
updateResolution() {
|
|
2355
|
-
this.validateReady();
|
|
2356
|
-
if (!this.iframeManager || !this.workerManager) {
|
|
2357
|
-
throw new VijiCoreError("Managers not available", "MANAGERS_NOT_READY");
|
|
2358
|
-
}
|
|
2359
|
-
const effectiveResolution = this.iframeManager.getEffectiveResolution();
|
|
2360
|
-
this.workerManager.postMessage("resolution-update", {
|
|
2361
|
-
effectiveWidth: effectiveResolution.width,
|
|
2362
|
-
effectiveHeight: effectiveResolution.height
|
|
2363
|
-
});
|
|
2364
|
-
this.stats.resolution = effectiveResolution;
|
|
2365
|
-
this.stats.scale = this.iframeManager.getScale();
|
|
2366
|
-
this.debugLog(`Resolution updated successfully (${this.instanceId})`, effectiveResolution);
|
|
2367
|
-
}
|
|
2368
|
-
/**
|
|
2369
|
-
* Sets the audio stream for analysis
|
|
2370
|
-
*/
|
|
2371
|
-
async setAudioStream(audioStream) {
|
|
2372
|
-
if (this.isInitialized && !this.isInitializing) {
|
|
2373
|
-
this.validateReady();
|
|
2374
|
-
}
|
|
2375
|
-
const previouslyHadAudio = this.currentAudioStream !== null;
|
|
2376
|
-
this.currentAudioStream = audioStream;
|
|
2377
|
-
const nowHasAudio = this.currentAudioStream !== null;
|
|
2378
|
-
if (this.audioSystem) {
|
|
2379
|
-
this.audioSystem.handleAudioStreamUpdate({
|
|
2380
|
-
audioStream,
|
|
2381
|
-
...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
|
|
2382
|
-
timestamp: performance.now()
|
|
2383
|
-
});
|
|
2384
|
-
}
|
|
2385
|
-
if (previouslyHadAudio !== nowHasAudio) {
|
|
2386
|
-
this.notifyCapabilitiesChange();
|
|
2387
|
-
}
|
|
2388
|
-
this.debugLog(`Audio stream ${audioStream ? "connected" : "disconnected"} (${this.instanceId})`);
|
|
2389
|
-
}
|
|
2390
|
-
/**
|
|
2391
|
-
* Sets the video stream for processing
|
|
2392
|
-
*/
|
|
2393
|
-
async setVideoStream(videoStream) {
|
|
2394
|
-
if (this.isInitialized && !this.isInitializing) {
|
|
2395
|
-
this.validateReady();
|
|
2396
|
-
}
|
|
2397
|
-
const previouslyHadVideo = this.currentVideoStream !== null;
|
|
2398
|
-
this.currentVideoStream = videoStream;
|
|
2399
|
-
const nowHasVideo = this.currentVideoStream !== null;
|
|
2400
|
-
if (this.videoCoordinator) {
|
|
2401
|
-
this.videoCoordinator.handleVideoStreamUpdate({
|
|
2402
|
-
videoStream,
|
|
2403
|
-
targetFrameRate: 30,
|
|
2404
|
-
// Default target FPS
|
|
2405
|
-
timestamp: performance.now()
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
if (previouslyHadVideo !== nowHasVideo) {
|
|
2409
|
-
this.notifyCapabilitiesChange();
|
|
2410
|
-
}
|
|
2411
|
-
this.debugLog(`Video stream ${videoStream ? "connected" : "disconnected"} (${this.instanceId})`);
|
|
2412
|
-
}
|
|
2413
|
-
/**
|
|
2414
|
-
* Gets the current audio stream
|
|
2415
|
-
*/
|
|
2416
|
-
getAudioStream() {
|
|
2417
|
-
return this.currentAudioStream;
|
|
2418
|
-
}
|
|
2419
|
-
/**
|
|
2420
|
-
* Gets the current video stream
|
|
2421
|
-
*/
|
|
2422
|
-
getVideoStream() {
|
|
2423
|
-
return this.currentVideoStream;
|
|
2424
|
-
}
|
|
2425
|
-
/**
|
|
2426
|
-
* Enables or disables user interactions (mouse, keyboard, touch) at runtime
|
|
2427
|
-
*/
|
|
2428
|
-
async setInteractionEnabled(enabled) {
|
|
2429
|
-
if (this.isInitialized && !this.isInitializing) {
|
|
2430
|
-
this.validateReady();
|
|
2431
|
-
}
|
|
2432
|
-
const previouslyHadInteraction = this.currentInteractionEnabled;
|
|
2433
|
-
this.currentInteractionEnabled = enabled;
|
|
2434
|
-
if (this.iframeManager) {
|
|
2435
|
-
this.iframeManager.setInteractionEnabled(enabled);
|
|
2436
|
-
}
|
|
2437
|
-
if (this.workerManager) {
|
|
2438
|
-
this.workerManager.postMessage("interaction-enabled", { enabled });
|
|
2439
|
-
}
|
|
2440
|
-
if (previouslyHadInteraction !== enabled) {
|
|
2441
|
-
this.notifyCapabilitiesChange();
|
|
2442
|
-
}
|
|
2443
|
-
this.debugLog(`Interaction ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
|
|
2444
|
-
}
|
|
2445
|
-
/**
|
|
2446
|
-
* Gets the current interaction enabled state
|
|
2447
|
-
*/
|
|
2448
|
-
getInteractionEnabled() {
|
|
2449
|
-
return this.currentInteractionEnabled;
|
|
2450
|
-
}
|
|
2451
|
-
/**
|
|
2452
|
-
* Updates audio analysis configuration
|
|
2453
|
-
*/
|
|
2454
|
-
async setAudioAnalysisConfig(config) {
|
|
2455
|
-
this.validateReady();
|
|
2456
|
-
this.config.analysisConfig = { ...this.config.analysisConfig, ...config };
|
|
2457
|
-
if (this.audioSystem) {
|
|
2458
|
-
this.audioSystem.handleAudioStreamUpdate({
|
|
2459
|
-
audioStream: this.currentAudioStream,
|
|
2460
|
-
...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
|
|
2461
|
-
timestamp: performance.now()
|
|
2462
|
-
});
|
|
2463
|
-
}
|
|
2464
|
-
this.debugLog(`Audio analysis config updated (${this.instanceId})`, config);
|
|
2465
|
-
}
|
|
2466
|
-
/**
|
|
2467
|
-
* Updates the canvas resolution by scale
|
|
2468
|
-
*/
|
|
2469
|
-
setResolution(scale) {
|
|
2470
|
-
this.validateReady();
|
|
2471
|
-
if (!this.iframeManager || !this.workerManager) {
|
|
2472
|
-
throw new VijiCoreError("Managers not available", "MANAGERS_NOT_READY");
|
|
2473
|
-
}
|
|
2474
|
-
this.debugLog(`Updating resolution scale to:`, scale, `(${this.instanceId})`);
|
|
2475
|
-
const effectiveResolution = this.iframeManager.updateScale(scale);
|
|
2476
|
-
this.workerManager.postMessage("resolution-update", {
|
|
2477
|
-
effectiveWidth: effectiveResolution.width,
|
|
2478
|
-
effectiveHeight: effectiveResolution.height
|
|
2479
|
-
});
|
|
2480
|
-
this.stats.resolution = effectiveResolution;
|
|
2481
|
-
this.stats.scale = scale;
|
|
2482
|
-
this.debugLog(`Resolution updated successfully (${this.instanceId})`, effectiveResolution);
|
|
2483
|
-
}
|
|
2484
|
-
/**
|
|
2485
|
-
* Detects the screen refresh rate
|
|
2486
|
-
*/
|
|
2487
|
-
async detectScreenRefreshRate() {
|
|
2488
|
-
return new Promise((resolve) => {
|
|
2489
|
-
let frameCount = 0;
|
|
2490
|
-
let startTime = performance.now();
|
|
2491
|
-
const measureFrames = () => {
|
|
2492
|
-
frameCount++;
|
|
2493
|
-
if (frameCount === 60) {
|
|
2494
|
-
const elapsed = performance.now() - startTime;
|
|
2495
|
-
this.screenRefreshRate = Math.round(6e4 / elapsed);
|
|
2496
|
-
this.debugLog("Detected screen refresh rate:", this.screenRefreshRate + "Hz");
|
|
2497
|
-
resolve();
|
|
2498
|
-
} else if (frameCount < 60) {
|
|
2499
|
-
requestAnimationFrame(measureFrames);
|
|
2500
|
-
}
|
|
2501
|
-
};
|
|
2502
|
-
requestAnimationFrame(measureFrames);
|
|
2503
|
-
});
|
|
2504
|
-
}
|
|
2505
|
-
/**
|
|
2506
|
-
* Updates frame rate statistics
|
|
2507
|
-
*/
|
|
2508
|
-
updateFrameRateStats() {
|
|
2509
|
-
this.stats.frameRate = {
|
|
2510
|
-
mode: this.config.frameRateMode,
|
|
2511
|
-
screenRefreshRate: this.screenRefreshRate,
|
|
2512
|
-
effectiveRefreshRate: 0
|
|
2513
|
-
// Will be updated by worker during execution
|
|
2514
|
-
};
|
|
2515
|
-
}
|
|
2516
|
-
/**
|
|
2517
|
-
* Gets current performance statistics
|
|
2518
|
-
*/
|
|
2519
|
-
getStats() {
|
|
2520
|
-
this.validateReady();
|
|
2521
|
-
if (!this.iframeManager) {
|
|
2522
|
-
throw new VijiCoreError("IFrame manager not available", "MANAGER_NOT_READY");
|
|
2523
|
-
}
|
|
2524
|
-
return this.stats;
|
|
2525
|
-
}
|
|
2526
|
-
/**
|
|
2527
|
-
* Checks if the core is ready for use
|
|
2528
|
-
*/
|
|
2529
|
-
get ready() {
|
|
2530
|
-
return this.isInitialized && !this.isDestroyed && this.iframeManager?.ready === true && this.workerManager?.ready === true;
|
|
2531
|
-
}
|
|
2532
|
-
/**
|
|
2533
|
-
* Gets the current configuration
|
|
2534
|
-
*/
|
|
2535
|
-
get configuration() {
|
|
2536
|
-
return { ...this.config };
|
|
2537
|
-
}
|
|
2538
|
-
/**
|
|
2539
|
-
* Destroys the core instance and cleans up all resources
|
|
2540
|
-
*/
|
|
2541
|
-
async destroy() {
|
|
2542
|
-
if (this.isDestroyed) {
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
|
-
this.isDestroyed = true;
|
|
2546
|
-
this.parameterGroups.clear();
|
|
2547
|
-
this.parameterValues.clear();
|
|
2548
|
-
this.parameterListeners.clear();
|
|
2549
|
-
this.parameterDefinedListeners.clear();
|
|
2550
|
-
this.parameterErrorListeners.clear();
|
|
2551
|
-
this.capabilitiesChangeListeners.clear();
|
|
2552
|
-
if (this.audioSystem) {
|
|
2553
|
-
this.audioSystem.resetAudioState();
|
|
2554
|
-
this.audioSystem = null;
|
|
2555
|
-
}
|
|
2556
|
-
this.currentAudioStream = null;
|
|
2557
|
-
if (this.videoCoordinator) {
|
|
2558
|
-
this.videoCoordinator.resetVideoState();
|
|
2559
|
-
this.videoCoordinator = null;
|
|
2560
|
-
}
|
|
2561
|
-
this.currentVideoStream = null;
|
|
2562
|
-
await this.cleanup();
|
|
2563
|
-
this.debugLog(`VijiCore destroyed (${this.instanceId})`);
|
|
2564
|
-
}
|
|
2565
|
-
/**
|
|
2566
|
-
* Validates that the core is ready for operations
|
|
2567
|
-
*/
|
|
2568
|
-
validateReady() {
|
|
2569
|
-
if (this.isDestroyed) {
|
|
2570
|
-
throw new VijiCoreError("Core instance has been destroyed", "INSTANCE_DESTROYED");
|
|
2571
|
-
}
|
|
2572
|
-
if (!this.ready) {
|
|
2573
|
-
throw new VijiCoreError("Core is not ready", "CORE_NOT_READY");
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
/**
|
|
2577
|
-
* Validates the provided configuration
|
|
2578
|
-
*/
|
|
2579
|
-
validateConfig(config) {
|
|
2580
|
-
if (!config.hostContainer) {
|
|
2581
|
-
throw new VijiCoreError("hostContainer is required", "INVALID_CONFIG");
|
|
2582
|
-
}
|
|
2583
|
-
if (!config.sceneCode || typeof config.sceneCode !== "string") {
|
|
2584
|
-
throw new VijiCoreError("sceneCode must be a non-empty string", "INVALID_CONFIG");
|
|
2585
|
-
}
|
|
2586
|
-
if (config.frameRateMode !== void 0 && config.frameRateMode !== "full" && config.frameRateMode !== "half") {
|
|
2587
|
-
throw new VijiCoreError('frameRateMode must be either "full" or "half"', "INVALID_CONFIG");
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
/**
|
|
2591
|
-
* Cleans up all resources
|
|
2592
|
-
*/
|
|
2593
|
-
async cleanup() {
|
|
2594
|
-
try {
|
|
2595
|
-
if (this.workerManager) {
|
|
2596
|
-
this.workerManager.destroy();
|
|
2597
|
-
this.workerManager = null;
|
|
2598
|
-
}
|
|
2599
|
-
if (this.iframeManager) {
|
|
2600
|
-
this.iframeManager.destroy();
|
|
2601
|
-
this.iframeManager = null;
|
|
2602
|
-
}
|
|
2603
|
-
this.isInitialized = false;
|
|
2604
|
-
this.isInitializing = false;
|
|
2605
|
-
} catch (error) {
|
|
2606
|
-
console.warn("Error during cleanup:", error);
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
const VERSION = "0.2.18";
|
|
1
|
+
import { A, V, a, b } from "./index-BdLMCFEN.js";
|
|
2611
2
|
export {
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
3
|
+
A as AudioSystem,
|
|
4
|
+
V as VERSION,
|
|
5
|
+
a as VijiCore,
|
|
6
|
+
b as VijiCoreError
|
|
2615
7
|
};
|
|
2616
8
|
//# sourceMappingURL=index.js.map
|