@viji-dev/core 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2466 @@
1
+ class VijiCoreError extends Error {
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-Cozsmke0.js", import.meta.url).href,
438
+ {
439
+ name: options?.name
440
+ }
441
+ );
442
+ }
443
+ class WorkerManager {
444
+ constructor(sceneCode, offscreenCanvas) {
445
+ this.sceneCode = sceneCode;
446
+ this.offscreenCanvas = offscreenCanvas;
447
+ }
448
+ worker = null;
449
+ messageId = 0;
450
+ pendingMessages = /* @__PURE__ */ new Map();
451
+ messageHandlers = /* @__PURE__ */ new Map();
452
+ isInitialized = false;
453
+ /**
454
+ * Creates and initializes the WebWorker with artist code
455
+ */
456
+ async createWorker() {
457
+ try {
458
+ this.worker = new WorkerWrapper();
459
+ this.setupMessageHandling();
460
+ this.postMessage("set-scene-code", { sceneCode: this.sceneCode });
461
+ await this.initializeWorker();
462
+ this.isInitialized = true;
463
+ return this.worker;
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
+ */
603
+ async initializeWorker() {
604
+ if (!this.worker) {
605
+ throw new VijiCoreError("Worker not created", "WORKER_NOT_CREATED");
606
+ }
607
+ const id = `msg_${++this.messageId}`;
608
+ const message = {
609
+ type: "init",
610
+ id,
611
+ timestamp: Date.now(),
612
+ data: {
613
+ canvas: this.offscreenCanvas
614
+ }
615
+ };
616
+ return new Promise((resolve, reject) => {
617
+ const timeoutId = setTimeout(() => {
618
+ this.pendingMessages.delete(id);
619
+ reject(new VijiCoreError("Canvas transfer timeout", "CANVAS_TRANSFER_TIMEOUT"));
620
+ }, 1e4);
621
+ this.pendingMessages.set(id, {
622
+ resolve,
623
+ reject,
624
+ timeout: timeoutId
625
+ });
626
+ this.worker.postMessage(message, [this.offscreenCanvas]);
627
+ });
628
+ }
629
+ }
630
+ class InteractionManager {
631
+ // Mouse state
632
+ mouseState = {
633
+ x: 0,
634
+ y: 0,
635
+ isInCanvas: false,
636
+ isPressed: false,
637
+ leftButton: false,
638
+ rightButton: false,
639
+ middleButton: false,
640
+ velocity: { x: 0, y: 0 },
641
+ deltaX: 0,
642
+ deltaY: 0,
643
+ wheelDelta: 0,
644
+ wheelX: 0,
645
+ wheelY: 0,
646
+ wasPressed: false,
647
+ wasReleased: false,
648
+ wasMoved: false
649
+ };
650
+ // Mouse velocity tracking
651
+ mouseVelocityHistory = [];
652
+ // Keyboard state
653
+ keyboardState = {
654
+ isPressed: (key) => this.activeKeys.has(key.toLowerCase()),
655
+ wasPressed: (key) => this.pressedThisFrame.has(key.toLowerCase()),
656
+ wasReleased: (key) => this.releasedThisFrame.has(key.toLowerCase()),
657
+ activeKeys: /* @__PURE__ */ new Set(),
658
+ pressedThisFrame: /* @__PURE__ */ new Set(),
659
+ releasedThisFrame: /* @__PURE__ */ new Set(),
660
+ lastKeyPressed: "",
661
+ lastKeyReleased: "",
662
+ shift: false,
663
+ ctrl: false,
664
+ alt: false,
665
+ meta: false
666
+ };
667
+ activeKeys = /* @__PURE__ */ new Set();
668
+ pressedThisFrame = /* @__PURE__ */ new Set();
669
+ releasedThisFrame = /* @__PURE__ */ new Set();
670
+ // Touch state
671
+ touchState = {
672
+ points: [],
673
+ count: 0,
674
+ started: [],
675
+ moved: [],
676
+ ended: [],
677
+ primary: null,
678
+ gestures: {
679
+ isPinching: false,
680
+ isRotating: false,
681
+ isPanning: false,
682
+ isTapping: false,
683
+ pinchScale: 1,
684
+ pinchDelta: 0,
685
+ rotationAngle: 0,
686
+ rotationDelta: 0,
687
+ panDelta: { x: 0, y: 0 },
688
+ tapCount: 0,
689
+ lastTapTime: 0,
690
+ tapPosition: null
691
+ }
692
+ };
693
+ activeTouches = /* @__PURE__ */ new Map();
694
+ gestureState = {
695
+ initialDistance: 0,
696
+ initialAngle: 0,
697
+ lastPinchScale: 1,
698
+ lastRotationAngle: 0,
699
+ panStartPosition: { x: 0, y: 0 },
700
+ tapStartTime: 0,
701
+ tapCount: 0,
702
+ lastTapTime: 0
703
+ };
704
+ constructor() {
705
+ }
706
+ /**
707
+ * Processes mouse update from the host
708
+ */
709
+ updateMouse(data) {
710
+ const canvasX = data.x;
711
+ const canvasY = data.y;
712
+ const deltaX = canvasX - this.mouseState.x;
713
+ const deltaY = canvasY - this.mouseState.y;
714
+ this.updateMouseVelocity(deltaX, deltaY, data.timestamp);
715
+ const prevPressed = this.mouseState.isPressed;
716
+ const currentPressed = data.buttons > 0;
717
+ this.mouseState.wasPressed = !prevPressed && currentPressed;
718
+ this.mouseState.wasReleased = prevPressed && !currentPressed;
719
+ this.mouseState.wasMoved = deltaX !== 0 || deltaY !== 0;
720
+ this.mouseState.x = canvasX;
721
+ this.mouseState.y = canvasY;
722
+ this.mouseState.deltaX = deltaX;
723
+ this.mouseState.deltaY = deltaY;
724
+ this.mouseState.isPressed = currentPressed;
725
+ this.mouseState.leftButton = (data.buttons & 1) !== 0;
726
+ this.mouseState.rightButton = (data.buttons & 2) !== 0;
727
+ this.mouseState.middleButton = (data.buttons & 4) !== 0;
728
+ this.mouseState.isInCanvas = data.isInCanvas !== void 0 ? data.isInCanvas : true;
729
+ this.mouseState.wheelDelta = data.wheelDeltaY;
730
+ this.mouseState.wheelX = data.wheelDeltaX;
731
+ this.mouseState.wheelY = data.wheelDeltaY;
732
+ }
733
+ /**
734
+ * Updates mouse velocity with smoothing
735
+ */
736
+ updateMouseVelocity(deltaX, deltaY, timestamp) {
737
+ this.mouseVelocityHistory.push({ x: deltaX, y: deltaY, time: timestamp });
738
+ const cutoff = timestamp - 100;
739
+ this.mouseVelocityHistory = this.mouseVelocityHistory.filter((sample) => sample.time > cutoff);
740
+ if (this.mouseVelocityHistory.length > 1) {
741
+ const recent = this.mouseVelocityHistory.slice(-5);
742
+ const avgX = recent.reduce((sum, s) => sum + s.x, 0) / recent.length;
743
+ const avgY = recent.reduce((sum, s) => sum + s.y, 0) / recent.length;
744
+ this.mouseState.velocity.x = avgX;
745
+ this.mouseState.velocity.y = avgY;
746
+ } else {
747
+ this.mouseState.velocity.x = deltaX;
748
+ this.mouseState.velocity.y = deltaY;
749
+ }
750
+ }
751
+ /**
752
+ * Processes keyboard update from the host
753
+ */
754
+ updateKeyboard(data) {
755
+ const key = data.key.toLowerCase();
756
+ if (data.type === "keydown") {
757
+ if (!this.activeKeys.has(key)) {
758
+ this.activeKeys.add(key);
759
+ this.pressedThisFrame.add(key);
760
+ this.keyboardState.lastKeyPressed = data.key;
761
+ }
762
+ } else if (data.type === "keyup") {
763
+ this.activeKeys.delete(key);
764
+ this.releasedThisFrame.add(key);
765
+ this.keyboardState.lastKeyReleased = data.key;
766
+ }
767
+ this.keyboardState.shift = data.shiftKey;
768
+ this.keyboardState.ctrl = data.ctrlKey;
769
+ this.keyboardState.alt = data.altKey;
770
+ this.keyboardState.meta = data.metaKey;
771
+ }
772
+ /**
773
+ * Processes touch update from the host
774
+ */
775
+ updateTouch(data) {
776
+ this.touchState.started = [];
777
+ this.touchState.moved = [];
778
+ this.touchState.ended = [];
779
+ if (data.type === "touchstart") {
780
+ this.processTouchStart(data.touches, data.timestamp);
781
+ } else if (data.type === "touchmove") {
782
+ this.processTouchMove(data.touches, data.timestamp);
783
+ } else if (data.type === "touchend" || data.type === "touchcancel") {
784
+ this.processTouchEnd(data.touches, data.timestamp);
785
+ }
786
+ this.touchState.points = Array.from(this.activeTouches.values());
787
+ this.touchState.count = this.touchState.points.length;
788
+ this.touchState.primary = this.touchState.points[0] || null;
789
+ this.updateGestures(data.timestamp);
790
+ }
791
+ /**
792
+ * Processes touch start events
793
+ */
794
+ processTouchStart(touches, timestamp) {
795
+ for (const touch of touches) {
796
+ const touchPoint = this.createTouchPoint(touch, timestamp, true);
797
+ this.activeTouches.set(touch.identifier, touchPoint);
798
+ this.touchState.started.push(touchPoint);
799
+ }
800
+ if (this.touchState.count === 1) {
801
+ this.gestureState.tapStartTime = timestamp;
802
+ const touch = this.touchState.points[0];
803
+ this.touchState.gestures.tapPosition = { x: touch.x, y: touch.y };
804
+ }
805
+ }
806
+ /**
807
+ * Processes touch move events
808
+ */
809
+ processTouchMove(touches, timestamp) {
810
+ for (const touch of touches) {
811
+ const existing = this.activeTouches.get(touch.identifier);
812
+ if (existing) {
813
+ const updated = this.createTouchPoint(touch, timestamp, false, existing);
814
+ this.activeTouches.set(touch.identifier, updated);
815
+ this.touchState.moved.push(updated);
816
+ }
817
+ }
818
+ }
819
+ /**
820
+ * Processes touch end events
821
+ */
822
+ processTouchEnd(touches, timestamp) {
823
+ for (const touch of touches) {
824
+ const existing = this.activeTouches.get(touch.identifier);
825
+ if (existing) {
826
+ const ended = { ...existing, isEnding: true, isActive: false };
827
+ this.touchState.ended.push(ended);
828
+ this.activeTouches.delete(touch.identifier);
829
+ }
830
+ }
831
+ if (this.touchState.count === 0 && this.gestureState.tapStartTime > 0) {
832
+ const tapDuration = timestamp - this.gestureState.tapStartTime;
833
+ if (tapDuration < 300) {
834
+ this.handleTap(timestamp);
835
+ }
836
+ this.gestureState.tapStartTime = 0;
837
+ }
838
+ }
839
+ /**
840
+ * Creates a touch point from raw touch data
841
+ */
842
+ createTouchPoint(touch, timestamp, isNew, previous) {
843
+ const x = touch.clientX;
844
+ const y = touch.clientY;
845
+ const deltaX = previous ? x - previous.x : 0;
846
+ const deltaY = previous ? y - previous.y : 0;
847
+ const timeDelta = previous ? timestamp - previous.timestamp : 16;
848
+ const velocityX = timeDelta > 0 ? deltaX / timeDelta * 1e3 : 0;
849
+ const velocityY = timeDelta > 0 ? deltaY / timeDelta * 1e3 : 0;
850
+ return {
851
+ id: touch.identifier,
852
+ x,
853
+ y,
854
+ pressure: touch.pressure || 0,
855
+ radius: Math.max(touch.radiusX || 0, touch.radiusY || 0),
856
+ radiusX: touch.radiusX || 0,
857
+ radiusY: touch.radiusY || 0,
858
+ rotationAngle: touch.rotationAngle || 0,
859
+ force: touch.force || touch.pressure || 0,
860
+ deltaX,
861
+ deltaY,
862
+ velocity: { x: velocityX, y: velocityY },
863
+ isNew,
864
+ isActive: true,
865
+ isEnding: false
866
+ };
867
+ }
868
+ /**
869
+ * Updates gesture recognition
870
+ */
871
+ updateGestures(timestamp) {
872
+ const touches = this.touchState.points;
873
+ const gestures = this.touchState.gestures;
874
+ if (touches.length === 2) {
875
+ const touch1 = touches[0];
876
+ const touch2 = touches[1];
877
+ const distance = Math.sqrt(
878
+ Math.pow(touch2.x - touch1.x, 2) + Math.pow(touch2.y - touch1.y, 2)
879
+ );
880
+ const angle = Math.atan2(touch2.y - touch1.y, touch2.x - touch1.x);
881
+ if (this.gestureState.initialDistance === 0) {
882
+ this.gestureState.initialDistance = distance;
883
+ this.gestureState.initialAngle = angle;
884
+ this.gestureState.lastPinchScale = 1;
885
+ this.gestureState.lastRotationAngle = 0;
886
+ }
887
+ const scale = distance / this.gestureState.initialDistance;
888
+ const scaleDelta = scale - this.gestureState.lastPinchScale;
889
+ gestures.isPinching = Math.abs(scaleDelta) > 0.01;
890
+ gestures.pinchScale = scale;
891
+ gestures.pinchDelta = scaleDelta;
892
+ this.gestureState.lastPinchScale = scale;
893
+ const rotationAngle = angle - this.gestureState.initialAngle;
894
+ const rotationDelta = rotationAngle - this.gestureState.lastRotationAngle;
895
+ gestures.isRotating = Math.abs(rotationDelta) > 0.02;
896
+ gestures.rotationAngle = rotationAngle;
897
+ gestures.rotationDelta = rotationDelta;
898
+ this.gestureState.lastRotationAngle = rotationAngle;
899
+ } else {
900
+ this.gestureState.initialDistance = 0;
901
+ gestures.isPinching = false;
902
+ gestures.isRotating = false;
903
+ gestures.pinchDelta = 0;
904
+ gestures.rotationDelta = 0;
905
+ }
906
+ if (touches.length > 0) {
907
+ const primaryTouch = touches[0];
908
+ if (this.gestureState.panStartPosition.x === 0) {
909
+ this.gestureState.panStartPosition = { x: primaryTouch.x, y: primaryTouch.y };
910
+ }
911
+ const panDeltaX = primaryTouch.x - this.gestureState.panStartPosition.x;
912
+ const panDeltaY = primaryTouch.y - this.gestureState.panStartPosition.y;
913
+ const panDistance = Math.sqrt(panDeltaX * panDeltaX + panDeltaY * panDeltaY);
914
+ gestures.isPanning = panDistance > 10;
915
+ gestures.panDelta = { x: panDeltaX, y: panDeltaY };
916
+ } else {
917
+ this.gestureState.panStartPosition = { x: 0, y: 0 };
918
+ gestures.isPanning = false;
919
+ gestures.panDelta = { x: 0, y: 0 };
920
+ }
921
+ }
922
+ /**
923
+ * Handles tap gesture detection
924
+ */
925
+ handleTap(timestamp) {
926
+ const timeSinceLastTap = timestamp - this.gestureState.lastTapTime;
927
+ if (timeSinceLastTap < 300) {
928
+ this.gestureState.tapCount++;
929
+ } else {
930
+ this.gestureState.tapCount = 1;
931
+ }
932
+ this.touchState.gestures.tapCount = this.gestureState.tapCount;
933
+ this.touchState.gestures.lastTapTime = timestamp;
934
+ this.touchState.gestures.isTapping = true;
935
+ this.gestureState.lastTapTime = timestamp;
936
+ }
937
+ /**
938
+ * Called at the start of each frame to reset frame-based events
939
+ */
940
+ frameStart() {
941
+ this.mouseState.wasPressed = false;
942
+ this.mouseState.wasReleased = false;
943
+ this.mouseState.wasMoved = false;
944
+ this.mouseState.wheelDelta = 0;
945
+ this.mouseState.wheelX = 0;
946
+ this.mouseState.wheelY = 0;
947
+ this.pressedThisFrame.clear();
948
+ this.releasedThisFrame.clear();
949
+ this.touchState.gestures.isTapping = false;
950
+ this.touchState.gestures.pinchDelta = 0;
951
+ this.touchState.gestures.rotationDelta = 0;
952
+ }
953
+ /**
954
+ * Get current mouse state (read-only)
955
+ */
956
+ getMouseState() {
957
+ return this.mouseState;
958
+ }
959
+ /**
960
+ * Get current keyboard state (read-only)
961
+ */
962
+ getKeyboardState() {
963
+ return this.keyboardState;
964
+ }
965
+ /**
966
+ * Get current touch state (read-only)
967
+ */
968
+ getTouchState() {
969
+ return this.touchState;
970
+ }
971
+ /**
972
+ * Cleanup resources
973
+ */
974
+ destroy() {
975
+ this.mouseVelocityHistory.length = 0;
976
+ this.activeTouches.clear();
977
+ this.activeKeys.clear();
978
+ this.pressedThisFrame.clear();
979
+ this.releasedThisFrame.clear();
980
+ }
981
+ }
982
+ class AudioSystem {
983
+ // Audio context and analysis nodes
984
+ audioContext = null;
985
+ analyser = null;
986
+ mediaStreamSource = null;
987
+ currentStream = null;
988
+ // Debug logging control
989
+ debugMode = false;
990
+ /**
991
+ * Enable or disable debug logging
992
+ */
993
+ setDebugMode(enabled) {
994
+ this.debugMode = enabled;
995
+ }
996
+ /**
997
+ * Debug logging helper
998
+ */
999
+ debugLog(message, ...args) {
1000
+ if (this.debugMode) {
1001
+ console.log(message, ...args);
1002
+ }
1003
+ }
1004
+ // Analysis configuration (good balance, leaning towards quality)
1005
+ fftSize = 2048;
1006
+ // Good balance for quality vs performance
1007
+ smoothingTimeConstant = 0.8;
1008
+ // Smooth but responsive
1009
+ // Analysis data arrays
1010
+ frequencyData = null;
1011
+ timeDomainData = null;
1012
+ // Audio analysis state (host-side state)
1013
+ audioState = {
1014
+ isConnected: false,
1015
+ volume: {
1016
+ rms: 0,
1017
+ peak: 0
1018
+ },
1019
+ bands: {
1020
+ bass: 0,
1021
+ mid: 0,
1022
+ treble: 0,
1023
+ subBass: 0,
1024
+ lowMid: 0,
1025
+ highMid: 0,
1026
+ presence: 0,
1027
+ brilliance: 0
1028
+ }
1029
+ };
1030
+ // Analysis loop
1031
+ analysisLoopId = null;
1032
+ isAnalysisRunning = false;
1033
+ // Callback to send results to worker
1034
+ sendAnalysisResults = null;
1035
+ constructor(sendAnalysisResultsCallback) {
1036
+ this.handleAudioStreamUpdate = this.handleAudioStreamUpdate.bind(this);
1037
+ this.performAnalysis = this.performAnalysis.bind(this);
1038
+ this.sendAnalysisResults = sendAnalysisResultsCallback || null;
1039
+ }
1040
+ /**
1041
+ * Get the current audio analysis state (for host-side usage)
1042
+ */
1043
+ getAudioState() {
1044
+ return { ...this.audioState };
1045
+ }
1046
+ /**
1047
+ * Handle audio stream update (called from VijiCore)
1048
+ */
1049
+ handleAudioStreamUpdate(data) {
1050
+ try {
1051
+ if (data.audioStream) {
1052
+ this.setAudioStream(data.audioStream);
1053
+ } else {
1054
+ this.disconnectAudioStream();
1055
+ }
1056
+ if (data.analysisConfig) {
1057
+ this.updateAnalysisConfig(data.analysisConfig);
1058
+ }
1059
+ } catch (error) {
1060
+ console.error("Error handling audio stream update:", error);
1061
+ this.audioState.isConnected = false;
1062
+ this.sendAnalysisResultsToWorker();
1063
+ }
1064
+ }
1065
+ /**
1066
+ * Set the audio stream for analysis
1067
+ */
1068
+ async setAudioStream(audioStream) {
1069
+ this.disconnectAudioStream();
1070
+ const audioTracks = audioStream.getAudioTracks();
1071
+ if (audioTracks.length === 0) {
1072
+ console.warn("No audio tracks in provided stream");
1073
+ this.audioState.isConnected = false;
1074
+ this.sendAnalysisResultsToWorker();
1075
+ return;
1076
+ }
1077
+ try {
1078
+ if (!this.audioContext) {
1079
+ this.audioContext = new AudioContext();
1080
+ if (this.audioContext.state === "suspended") {
1081
+ await this.audioContext.resume();
1082
+ }
1083
+ }
1084
+ this.analyser = this.audioContext.createAnalyser();
1085
+ this.analyser.fftSize = this.fftSize;
1086
+ this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
1087
+ this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioStream);
1088
+ this.mediaStreamSource.connect(this.analyser);
1089
+ const bufferLength = this.analyser.frequencyBinCount;
1090
+ this.frequencyData = new Uint8Array(bufferLength);
1091
+ this.timeDomainData = new Uint8Array(bufferLength);
1092
+ this.currentStream = audioStream;
1093
+ this.audioState.isConnected = true;
1094
+ this.startAnalysisLoop();
1095
+ this.debugLog("Audio stream connected successfully (host-side)", {
1096
+ sampleRate: this.audioContext.sampleRate,
1097
+ fftSize: this.fftSize,
1098
+ bufferLength
1099
+ });
1100
+ } catch (error) {
1101
+ console.error("Failed to set up audio analysis:", error);
1102
+ this.audioState.isConnected = false;
1103
+ this.disconnectAudioStream();
1104
+ }
1105
+ this.sendAnalysisResultsToWorker();
1106
+ }
1107
+ /**
1108
+ * Disconnect current audio stream and clean up resources
1109
+ */
1110
+ disconnectAudioStream() {
1111
+ this.stopAnalysisLoop();
1112
+ if (this.mediaStreamSource) {
1113
+ this.mediaStreamSource.disconnect();
1114
+ this.mediaStreamSource = null;
1115
+ }
1116
+ if (this.analyser) {
1117
+ this.analyser.disconnect();
1118
+ this.analyser = null;
1119
+ }
1120
+ this.frequencyData = null;
1121
+ this.timeDomainData = null;
1122
+ this.currentStream = null;
1123
+ this.audioState.isConnected = false;
1124
+ this.resetAudioValues();
1125
+ this.sendAnalysisResultsToWorker();
1126
+ this.debugLog("Audio stream disconnected (host-side)");
1127
+ }
1128
+ /**
1129
+ * Update analysis configuration
1130
+ */
1131
+ updateAnalysisConfig(config) {
1132
+ let needsReconnect = false;
1133
+ if (config.fftSize && config.fftSize !== this.fftSize) {
1134
+ this.fftSize = config.fftSize;
1135
+ needsReconnect = true;
1136
+ }
1137
+ if (config.smoothing !== void 0) {
1138
+ this.smoothingTimeConstant = config.smoothing;
1139
+ if (this.analyser) {
1140
+ this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
1141
+ }
1142
+ }
1143
+ if (needsReconnect && this.currentStream) {
1144
+ const stream = this.currentStream;
1145
+ this.setAudioStream(stream);
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Start the audio analysis loop
1150
+ */
1151
+ startAnalysisLoop() {
1152
+ if (this.isAnalysisRunning) return;
1153
+ this.isAnalysisRunning = true;
1154
+ this.performAnalysis();
1155
+ }
1156
+ /**
1157
+ * Stop the audio analysis loop
1158
+ */
1159
+ stopAnalysisLoop() {
1160
+ this.isAnalysisRunning = false;
1161
+ if (this.analysisLoopId !== null) {
1162
+ cancelAnimationFrame(this.analysisLoopId);
1163
+ this.analysisLoopId = null;
1164
+ }
1165
+ }
1166
+ /**
1167
+ * Perform audio analysis (called every frame)
1168
+ */
1169
+ performAnalysis() {
1170
+ if (!this.isAnalysisRunning || !this.analyser || !this.frequencyData || !this.timeDomainData) {
1171
+ return;
1172
+ }
1173
+ this.analyser.getByteFrequencyData(this.frequencyData);
1174
+ this.analyser.getByteTimeDomainData(this.timeDomainData);
1175
+ this.calculateVolumeMetrics();
1176
+ this.calculateFrequencyBands();
1177
+ this.sendAnalysisResultsToWorker();
1178
+ this.analysisLoopId = requestAnimationFrame(() => this.performAnalysis());
1179
+ }
1180
+ /**
1181
+ * Calculate RMS and peak volume from time domain data
1182
+ */
1183
+ calculateVolumeMetrics() {
1184
+ if (!this.timeDomainData) return;
1185
+ let rmsSum = 0;
1186
+ let peak = 0;
1187
+ for (let i = 0; i < this.timeDomainData.length; i++) {
1188
+ const sample = (this.timeDomainData[i] - 128) / 128;
1189
+ rmsSum += sample * sample;
1190
+ const absValue = Math.abs(sample);
1191
+ if (absValue > peak) {
1192
+ peak = absValue;
1193
+ }
1194
+ }
1195
+ const rms = Math.sqrt(rmsSum / this.timeDomainData.length);
1196
+ this.audioState.volume.rms = rms;
1197
+ this.audioState.volume.peak = peak;
1198
+ }
1199
+ /**
1200
+ * Calculate frequency band values from frequency data
1201
+ */
1202
+ calculateFrequencyBands() {
1203
+ if (!this.frequencyData || !this.audioContext) return;
1204
+ const nyquist = this.audioContext.sampleRate / 2;
1205
+ const binCount = this.frequencyData.length;
1206
+ const bands = {
1207
+ subBass: { min: 20, max: 60 },
1208
+ // Sub-bass
1209
+ bass: { min: 60, max: 250 },
1210
+ // Bass
1211
+ lowMid: { min: 250, max: 500 },
1212
+ // Low midrange
1213
+ mid: { min: 500, max: 2e3 },
1214
+ // Midrange
1215
+ highMid: { min: 2e3, max: 4e3 },
1216
+ // High midrange
1217
+ presence: { min: 4e3, max: 6e3 },
1218
+ // Presence
1219
+ brilliance: { min: 6e3, max: 2e4 },
1220
+ // Brilliance
1221
+ treble: { min: 2e3, max: 2e4 }
1222
+ // Treble (combined high frequencies)
1223
+ };
1224
+ for (const [bandName, range] of Object.entries(bands)) {
1225
+ const startBin = Math.floor(range.min / nyquist * binCount);
1226
+ const endBin = Math.min(Math.floor(range.max / nyquist * binCount), binCount - 1);
1227
+ let sum = 0;
1228
+ let count = 0;
1229
+ for (let i = startBin; i <= endBin; i++) {
1230
+ sum += this.frequencyData[i];
1231
+ count++;
1232
+ }
1233
+ const average = count > 0 ? sum / count : 0;
1234
+ this.audioState.bands[bandName] = average / 255;
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Send analysis results to worker
1239
+ */
1240
+ sendAnalysisResultsToWorker() {
1241
+ if (this.sendAnalysisResults) {
1242
+ const frequencyData = this.frequencyData ? new Uint8Array(this.frequencyData) : new Uint8Array(0);
1243
+ this.sendAnalysisResults({
1244
+ type: "audio-analysis-update",
1245
+ data: {
1246
+ ...this.audioState,
1247
+ frequencyData,
1248
+ // For getFrequencyData() access
1249
+ timestamp: performance.now()
1250
+ }
1251
+ });
1252
+ }
1253
+ }
1254
+ /**
1255
+ * Reset audio values to defaults
1256
+ */
1257
+ resetAudioValues() {
1258
+ this.audioState.volume.rms = 0;
1259
+ this.audioState.volume.peak = 0;
1260
+ for (const band in this.audioState.bands) {
1261
+ this.audioState.bands[band] = 0;
1262
+ }
1263
+ }
1264
+ /**
1265
+ * Reset all audio state (called when destroying)
1266
+ */
1267
+ resetAudioState() {
1268
+ this.disconnectAudioStream();
1269
+ if (this.audioContext && this.audioContext.state !== "closed") {
1270
+ this.audioContext.close();
1271
+ this.audioContext = null;
1272
+ }
1273
+ this.resetAudioValues();
1274
+ }
1275
+ /**
1276
+ * Get current analysis configuration
1277
+ */
1278
+ getAnalysisConfig() {
1279
+ return {
1280
+ fftSize: this.fftSize,
1281
+ smoothing: this.smoothingTimeConstant
1282
+ };
1283
+ }
1284
+ }
1285
+ class VideoCoordinator {
1286
+ // Video elements for MediaStream processing
1287
+ videoElement = null;
1288
+ canvas = null;
1289
+ ctx = null;
1290
+ // Note: currentStream was removed as it was unused
1291
+ // Transfer coordination
1292
+ transferLoopId = null;
1293
+ isTransferRunning = false;
1294
+ lastTransferTime = 0;
1295
+ transferInterval = 1e3 / 30;
1296
+ // Transfer at 30 FPS
1297
+ // Video state (lightweight - main state is in worker)
1298
+ coordinatorState = {
1299
+ isConnected: false,
1300
+ sourceType: "",
1301
+ frameWidth: 0,
1302
+ frameHeight: 0
1303
+ };
1304
+ // Track if OffscreenCanvas has been sent to worker
1305
+ hasTransferredCanvas = false;
1306
+ // Callback to send data to worker
1307
+ sendToWorker = null;
1308
+ // Debug logging control
1309
+ debugMode = false;
1310
+ /**
1311
+ * Enable or disable debug logging
1312
+ */
1313
+ setDebugMode(enabled) {
1314
+ this.debugMode = enabled;
1315
+ }
1316
+ /**
1317
+ * Debug logging helper
1318
+ */
1319
+ debugLog(message, ...args) {
1320
+ if (this.debugMode) {
1321
+ console.log(message, ...args);
1322
+ }
1323
+ }
1324
+ constructor(sendToWorkerCallback) {
1325
+ this.handleVideoStreamUpdate = this.handleVideoStreamUpdate.bind(this);
1326
+ this.transferVideoFrame = this.transferVideoFrame.bind(this);
1327
+ this.sendToWorker = sendToWorkerCallback || null;
1328
+ }
1329
+ /**
1330
+ * Get the current video coordinator state (for host-side usage)
1331
+ */
1332
+ getCoordinatorState() {
1333
+ return { ...this.coordinatorState };
1334
+ }
1335
+ /**
1336
+ * Handle video stream update (called from VijiCore)
1337
+ */
1338
+ handleVideoStreamUpdate(data) {
1339
+ try {
1340
+ if (data.videoStream) {
1341
+ this.setVideoStream(data.videoStream);
1342
+ } else {
1343
+ this.disconnectVideoStream();
1344
+ }
1345
+ if (data.targetFrameRate || data.cvConfig) {
1346
+ this.sendConfigurationToWorker({
1347
+ ...data.targetFrameRate && { targetFrameRate: data.targetFrameRate },
1348
+ ...data.cvConfig && { cvConfig: data.cvConfig },
1349
+ timestamp: data.timestamp
1350
+ });
1351
+ }
1352
+ } catch (error) {
1353
+ console.error("Error handling video stream update:", error);
1354
+ this.coordinatorState.isConnected = false;
1355
+ this.sendDisconnectionToWorker();
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Set the video stream for processing
1360
+ */
1361
+ async setVideoStream(videoStream) {
1362
+ this.disconnectVideoStream();
1363
+ const videoTracks = videoStream.getVideoTracks();
1364
+ if (videoTracks.length === 0) {
1365
+ console.warn("No video tracks in provided stream");
1366
+ this.coordinatorState.isConnected = false;
1367
+ this.sendDisconnectionToWorker();
1368
+ return;
1369
+ }
1370
+ try {
1371
+ this.videoElement = document.createElement("video");
1372
+ this.videoElement.autoplay = true;
1373
+ this.videoElement.muted = true;
1374
+ this.videoElement.playsInline = true;
1375
+ this.canvas = document.createElement("canvas");
1376
+ this.ctx = this.canvas.getContext("2d");
1377
+ if (!this.ctx) {
1378
+ throw new Error("Failed to get 2D context from canvas");
1379
+ }
1380
+ this.videoElement.srcObject = videoStream;
1381
+ await new Promise((resolve, reject) => {
1382
+ const timeout = setTimeout(() => reject(new Error("Video metadata load timeout")), 5e3);
1383
+ this.videoElement.addEventListener("loadedmetadata", async () => {
1384
+ clearTimeout(timeout);
1385
+ try {
1386
+ await this.videoElement.play();
1387
+ this.debugLog("✅ Video element is now playing:", {
1388
+ videoWidth: this.videoElement.videoWidth,
1389
+ videoHeight: this.videoElement.videoHeight,
1390
+ readyState: this.videoElement.readyState,
1391
+ paused: this.videoElement.paused
1392
+ });
1393
+ resolve();
1394
+ } catch (playError) {
1395
+ console.error("🔴 Failed to start video playback:", playError);
1396
+ reject(playError);
1397
+ }
1398
+ }, { once: true });
1399
+ this.videoElement.addEventListener("error", (e) => {
1400
+ clearTimeout(timeout);
1401
+ reject(new Error(`Video load error: ${e}`));
1402
+ }, { once: true });
1403
+ });
1404
+ this.canvas.width = this.videoElement.videoWidth;
1405
+ this.canvas.height = this.videoElement.videoHeight;
1406
+ this.coordinatorState.isConnected = true;
1407
+ this.coordinatorState.frameWidth = this.videoElement.videoWidth;
1408
+ this.coordinatorState.frameHeight = this.videoElement.videoHeight;
1409
+ this.coordinatorState.sourceType = "MediaStream";
1410
+ await this.transferOffscreenCanvasToWorker(this.videoElement.videoWidth, this.videoElement.videoHeight);
1411
+ this.startTransferLoop();
1412
+ this.debugLog("Video stream connected successfully (host-side coordinator)", {
1413
+ width: this.videoElement.videoWidth,
1414
+ height: this.videoElement.videoHeight
1415
+ });
1416
+ } catch (error) {
1417
+ console.error("Failed to set up video coordination:", error);
1418
+ this.coordinatorState.isConnected = false;
1419
+ this.disconnectVideoStream();
1420
+ }
1421
+ }
1422
+ /**
1423
+ * ✅ CORRECT: Transfer OffscreenCanvas to worker BEFORE getting context
1424
+ */
1425
+ async transferOffscreenCanvasToWorker(width, height) {
1426
+ if (this.hasTransferredCanvas) {
1427
+ this.sendConfigurationToWorker({
1428
+ width,
1429
+ height,
1430
+ timestamp: performance.now()
1431
+ });
1432
+ return;
1433
+ }
1434
+ try {
1435
+ const offscreenCanvas = new OffscreenCanvas(width, height);
1436
+ if (this.sendToWorker) {
1437
+ this.sendToWorker({
1438
+ type: "video-canvas-setup",
1439
+ data: {
1440
+ offscreenCanvas,
1441
+ width,
1442
+ height,
1443
+ timestamp: performance.now()
1444
+ }
1445
+ }, [offscreenCanvas]);
1446
+ this.hasTransferredCanvas = true;
1447
+ this.debugLog("✅ OffscreenCanvas transferred to worker (correct approach)", {
1448
+ width,
1449
+ height
1450
+ });
1451
+ }
1452
+ } catch (error) {
1453
+ console.error("Failed to transfer OffscreenCanvas to worker:", error);
1454
+ throw error;
1455
+ }
1456
+ }
1457
+ /**
1458
+ * Disconnect current video stream and clean up resources
1459
+ */
1460
+ disconnectVideoStream() {
1461
+ this.stopTransferLoop();
1462
+ if (this.videoElement) {
1463
+ this.videoElement.pause();
1464
+ this.videoElement.srcObject = null;
1465
+ this.videoElement = null;
1466
+ }
1467
+ if (this.canvas) {
1468
+ this.canvas = null;
1469
+ this.ctx = null;
1470
+ }
1471
+ this.coordinatorState.isConnected = false;
1472
+ this.coordinatorState.frameWidth = 0;
1473
+ this.coordinatorState.frameHeight = 0;
1474
+ this.coordinatorState.sourceType = "";
1475
+ this.hasTransferredCanvas = false;
1476
+ this.sendDisconnectionToWorker();
1477
+ this.debugLog("Video stream disconnected (host-side coordinator)");
1478
+ }
1479
+ /**
1480
+ * Start the video frame transfer loop
1481
+ */
1482
+ startTransferLoop() {
1483
+ if (this.isTransferRunning) return;
1484
+ this.isTransferRunning = true;
1485
+ this.lastTransferTime = performance.now();
1486
+ this.transferVideoFrame();
1487
+ }
1488
+ /**
1489
+ * Stop the video frame transfer loop
1490
+ */
1491
+ stopTransferLoop() {
1492
+ this.isTransferRunning = false;
1493
+ if (this.transferLoopId !== null) {
1494
+ cancelAnimationFrame(this.transferLoopId);
1495
+ this.transferLoopId = null;
1496
+ }
1497
+ }
1498
+ /**
1499
+ * Transfer video frame to worker using ImageBitmap (for worker to draw on its OffscreenCanvas)
1500
+ */
1501
+ transferVideoFrame() {
1502
+ if (!this.isTransferRunning || !this.videoElement || !this.canvas || !this.ctx) {
1503
+ if (!this.isTransferRunning) {
1504
+ this.debugLog("🔴 Transfer loop stopped");
1505
+ }
1506
+ return;
1507
+ }
1508
+ const currentTime = performance.now();
1509
+ const deltaTime = currentTime - this.lastTransferTime;
1510
+ if (deltaTime >= this.transferInterval) {
1511
+ if (Math.random() < 0.01) {
1512
+ this.debugLog(`🔄 Transfer loop tick: ${deltaTime.toFixed(1)}ms since last frame`);
1513
+ }
1514
+ this.transferFrameToWorker().catch((error) => {
1515
+ console.error("🔴 Error transferring video frame to worker:", error);
1516
+ });
1517
+ this.lastTransferTime = currentTime;
1518
+ }
1519
+ this.transferLoopId = requestAnimationFrame(() => this.transferVideoFrame());
1520
+ }
1521
+ /**
1522
+ * Async frame transfer using ImageBitmap (for worker to draw)
1523
+ */
1524
+ async transferFrameToWorker() {
1525
+ if (!this.videoElement || !this.canvas || !this.ctx) {
1526
+ console.warn("🔴 Frame transfer called but missing elements:", {
1527
+ hasVideo: !!this.videoElement,
1528
+ hasCanvas: !!this.canvas,
1529
+ hasCtx: !!this.ctx
1530
+ });
1531
+ return;
1532
+ }
1533
+ try {
1534
+ if (this.videoElement.readyState < 2) {
1535
+ console.warn("🔴 Video not ready for frame capture, readyState:", this.videoElement.readyState);
1536
+ return;
1537
+ }
1538
+ if (this.videoElement.videoWidth === 0 || this.videoElement.videoHeight === 0) {
1539
+ console.warn("🔴 Video has no dimensions:", {
1540
+ width: this.videoElement.videoWidth,
1541
+ height: this.videoElement.videoHeight
1542
+ });
1543
+ return;
1544
+ }
1545
+ this.ctx.drawImage(this.videoElement, 0, 0, this.canvas.width, this.canvas.height);
1546
+ const imageBitmap = await createImageBitmap(this.canvas);
1547
+ if (Math.random() < 0.01) {
1548
+ this.debugLog("✅ Frame captured and ImageBitmap created:", {
1549
+ videoDimensions: `${this.videoElement.videoWidth}x${this.videoElement.videoHeight}`,
1550
+ canvasDimensions: `${this.canvas.width}x${this.canvas.height}`,
1551
+ bitmapDimensions: `${imageBitmap.width}x${imageBitmap.height}`
1552
+ });
1553
+ }
1554
+ this.sendFrameToWorker(imageBitmap);
1555
+ } catch (error) {
1556
+ console.error("🔴 Failed to create ImageBitmap:", error);
1557
+ }
1558
+ }
1559
+ /**
1560
+ * Send ImageBitmap frame to worker (for worker to draw on its OffscreenCanvas)
1561
+ */
1562
+ sendFrameToWorker(imageBitmap) {
1563
+ if (this.sendToWorker) {
1564
+ this.sendToWorker({
1565
+ type: "video-frame-update",
1566
+ data: {
1567
+ imageBitmap,
1568
+ timestamp: performance.now()
1569
+ }
1570
+ }, [imageBitmap]);
1571
+ }
1572
+ }
1573
+ /**
1574
+ * Send configuration updates to worker
1575
+ */
1576
+ sendConfigurationToWorker(config) {
1577
+ if (this.sendToWorker) {
1578
+ this.sendToWorker({
1579
+ type: "video-config-update",
1580
+ data: config
1581
+ });
1582
+ }
1583
+ }
1584
+ /**
1585
+ * Send disconnection notification to worker
1586
+ */
1587
+ sendDisconnectionToWorker() {
1588
+ if (this.sendToWorker) {
1589
+ this.sendToWorker({
1590
+ type: "video-config-update",
1591
+ data: {
1592
+ disconnect: true,
1593
+ timestamp: performance.now()
1594
+ }
1595
+ });
1596
+ }
1597
+ }
1598
+ /**
1599
+ * Handle image file as video source
1600
+ */
1601
+ async setImageSource(imageFile) {
1602
+ try {
1603
+ const img = new Image();
1604
+ const url = URL.createObjectURL(imageFile);
1605
+ await new Promise((resolve, reject) => {
1606
+ img.onload = () => {
1607
+ URL.revokeObjectURL(url);
1608
+ resolve();
1609
+ };
1610
+ img.onerror = () => {
1611
+ URL.revokeObjectURL(url);
1612
+ reject(new Error("Failed to load image"));
1613
+ };
1614
+ img.src = url;
1615
+ });
1616
+ this.disconnectVideoStream();
1617
+ this.coordinatorState.isConnected = true;
1618
+ this.coordinatorState.frameWidth = img.width;
1619
+ this.coordinatorState.frameHeight = img.height;
1620
+ this.coordinatorState.sourceType = "Image";
1621
+ await this.transferOffscreenCanvasToWorker(img.width, img.height);
1622
+ const imageBitmap = await createImageBitmap(img);
1623
+ this.sendFrameToWorker(imageBitmap);
1624
+ this.debugLog("Image source set successfully (host-side coordinator)", {
1625
+ width: img.width,
1626
+ height: img.height
1627
+ });
1628
+ } catch (error) {
1629
+ console.error("Failed to set image source:", error);
1630
+ this.coordinatorState.isConnected = false;
1631
+ this.sendDisconnectionToWorker();
1632
+ }
1633
+ }
1634
+ /**
1635
+ * Reset all video coordinator state (called when destroying)
1636
+ */
1637
+ resetVideoState() {
1638
+ this.disconnectVideoStream();
1639
+ }
1640
+ /**
1641
+ * Get current coordinator configuration
1642
+ */
1643
+ getCoordinatorConfig() {
1644
+ return {
1645
+ transferInterval: this.transferInterval
1646
+ };
1647
+ }
1648
+ }
1649
+ class VijiCore {
1650
+ iframeManager = null;
1651
+ workerManager = null;
1652
+ interactionManager = null;
1653
+ audioSystem = null;
1654
+ videoCoordinator = null;
1655
+ isInitialized = false;
1656
+ isDestroyed = false;
1657
+ isInitializing = false;
1658
+ instanceId;
1659
+ screenRefreshRate = 60;
1660
+ // Will be detected
1661
+ debugMode = false;
1662
+ // Debug logging control
1663
+ /**
1664
+ * Debug logging helper
1665
+ */
1666
+ debugLog(message, ...args) {
1667
+ if (this.debugMode) {
1668
+ console.log(message, ...args);
1669
+ }
1670
+ }
1671
+ // Configuration
1672
+ config;
1673
+ // Audio stream management
1674
+ currentAudioStream = null;
1675
+ // Video stream management
1676
+ currentVideoStream = null;
1677
+ // Parameter system for Phase 2
1678
+ parameterGroups = /* @__PURE__ */ new Map();
1679
+ parameterValues = /* @__PURE__ */ new Map();
1680
+ parametersInitialized = false;
1681
+ // Event listeners for parameter system
1682
+ parameterListeners = /* @__PURE__ */ new Map();
1683
+ parameterDefinedListeners = /* @__PURE__ */ new Set();
1684
+ parameterErrorListeners = /* @__PURE__ */ new Set();
1685
+ capabilitiesChangeListeners = /* @__PURE__ */ new Set();
1686
+ // Performance tracking (basic for Phase 1)
1687
+ stats = {
1688
+ frameTime: 0,
1689
+ resolution: { width: 0, height: 0 },
1690
+ scale: 1,
1691
+ frameRate: {
1692
+ mode: "full",
1693
+ screenRefreshRate: 60,
1694
+ effectiveRefreshRate: 60
1695
+ },
1696
+ parameterCount: 0
1697
+ };
1698
+ constructor(config) {
1699
+ this.validateConfig(config);
1700
+ this.instanceId = `viji-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1701
+ this.config = {
1702
+ ...config,
1703
+ frameRateMode: config.frameRateMode || "full",
1704
+ autoOptimize: config.autoOptimize ?? true,
1705
+ parameters: config.parameters || [],
1706
+ noInputs: config.noInputs ?? false,
1707
+ allowUserInteraction: config.allowUserInteraction ?? true
1708
+ };
1709
+ this.debugLog(`VijiCore instance created: ${this.instanceId}`);
1710
+ }
1711
+ /**
1712
+ * Enable or disable debug logging
1713
+ */
1714
+ setDebugMode(enabled) {
1715
+ this.debugMode = enabled;
1716
+ if (this.iframeManager && "setDebugMode" in this.iframeManager) {
1717
+ this.iframeManager.setDebugMode(enabled);
1718
+ }
1719
+ if (this.workerManager && "setDebugMode" in this.workerManager) {
1720
+ this.workerManager.setDebugMode(enabled);
1721
+ }
1722
+ if (this.interactionManager && "setDebugMode" in this.interactionManager) {
1723
+ this.interactionManager.setDebugMode(enabled);
1724
+ }
1725
+ if (this.audioSystem && "setDebugMode" in this.audioSystem) {
1726
+ this.audioSystem.setDebugMode(enabled);
1727
+ }
1728
+ if (this.videoCoordinator && "setDebugMode" in this.videoCoordinator) {
1729
+ this.videoCoordinator.setDebugMode(enabled);
1730
+ }
1731
+ if (this.workerManager) {
1732
+ this.workerManager.postMessage("debug-mode", { enabled });
1733
+ }
1734
+ }
1735
+ /**
1736
+ * Get current debug mode status
1737
+ */
1738
+ getDebugMode() {
1739
+ return this.debugMode;
1740
+ }
1741
+ /**
1742
+ * Initializes the core components in sequence
1743
+ */
1744
+ async initialize() {
1745
+ try {
1746
+ if (this.isDestroyed) {
1747
+ throw new VijiCoreError("Cannot initialize destroyed instance", "INSTANCE_DESTROYED");
1748
+ }
1749
+ if (this.isInitializing) {
1750
+ throw new VijiCoreError("Initialization already in progress", "CONCURRENT_INITIALIZATION");
1751
+ }
1752
+ if (this.isInitialized) {
1753
+ throw new VijiCoreError("Core already initialized", "ALREADY_INITIALIZED");
1754
+ }
1755
+ this.isInitializing = true;
1756
+ this.debugLog(`Starting VijiCore initialization... (${this.instanceId})`);
1757
+ this.config.hostContainer.innerHTML = "";
1758
+ this.iframeManager = new IFrameManager(
1759
+ this.config.hostContainer
1760
+ );
1761
+ await this.iframeManager.createSecureIFrame();
1762
+ const offscreenCanvas = await this.createCanvasWithRetry();
1763
+ if (this.config.allowUserInteraction) {
1764
+ this.interactionManager = new InteractionManager();
1765
+ this.setupInteractionSystem();
1766
+ }
1767
+ this.workerManager = new WorkerManager(
1768
+ this.config.sceneCode,
1769
+ offscreenCanvas
1770
+ );
1771
+ this.setupCommunication();
1772
+ await this.workerManager.createWorker();
1773
+ this.audioSystem = new AudioSystem((message) => {
1774
+ if (this.workerManager) {
1775
+ this.workerManager.postMessage(message.type, message.data);
1776
+ }
1777
+ });
1778
+ this.videoCoordinator = new VideoCoordinator((message, transfer) => {
1779
+ if (this.workerManager) {
1780
+ if (transfer && transfer.length > 0) {
1781
+ this.workerManager.postMessage(message.type, message.data, transfer);
1782
+ } else {
1783
+ this.workerManager.postMessage(message.type, message.data);
1784
+ }
1785
+ }
1786
+ });
1787
+ const effectiveResolution = this.iframeManager.getEffectiveResolution();
1788
+ this.workerManager.postMessage("resolution-update", {
1789
+ effectiveWidth: effectiveResolution.width,
1790
+ effectiveHeight: effectiveResolution.height
1791
+ });
1792
+ await this.detectScreenRefreshRate();
1793
+ this.workerManager.postMessage("refresh-rate-update", {
1794
+ screenRefreshRate: this.screenRefreshRate
1795
+ });
1796
+ if (this.config.audioStream) {
1797
+ await this.setAudioStream(this.config.audioStream);
1798
+ }
1799
+ if (this.config.videoStream) {
1800
+ await this.setVideoStream(this.config.videoStream);
1801
+ }
1802
+ this.stats.resolution = effectiveResolution;
1803
+ this.stats.scale = this.iframeManager.getScale();
1804
+ this.updateFrameRateStats();
1805
+ this.isInitialized = true;
1806
+ this.isInitializing = false;
1807
+ this.debugLog(`VijiCore initialized successfully (${this.instanceId}):`, {
1808
+ resolution: `${effectiveResolution.width}x${effectiveResolution.height}`,
1809
+ frameRateMode: this.config.frameRateMode,
1810
+ hasAudio: !!this.config.audioStream,
1811
+ hasVideo: !!this.config.videoStream
1812
+ });
1813
+ } catch (error) {
1814
+ this.isInitializing = false;
1815
+ await this.cleanup();
1816
+ throw new VijiCoreError(
1817
+ `Failed to initialize VijiCore: ${error}`,
1818
+ "INITIALIZATION_ERROR",
1819
+ { error, config: this.config }
1820
+ );
1821
+ }
1822
+ }
1823
+ /**
1824
+ * Creates canvas with retry logic to handle timing issues
1825
+ */
1826
+ async createCanvasWithRetry() {
1827
+ const maxRetries = 3;
1828
+ const retryDelay = 200;
1829
+ for (let i = 0; i < maxRetries; i++) {
1830
+ if (this.isDestroyed || !this.isInitializing) {
1831
+ throw new VijiCoreError("Initialization cancelled", "INITIALIZATION_CANCELLED");
1832
+ }
1833
+ if (!this.iframeManager) {
1834
+ throw new VijiCoreError("IFrameManager not available", "MANAGER_NOT_READY");
1835
+ }
1836
+ try {
1837
+ return await this.iframeManager.createCanvas();
1838
+ } catch (error) {
1839
+ console.warn(`Canvas creation attempt ${i + 1}/${maxRetries} failed:`, error);
1840
+ if (i === maxRetries - 1) {
1841
+ throw error;
1842
+ }
1843
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
1844
+ }
1845
+ }
1846
+ throw new VijiCoreError("Canvas creation failed after all retries", "CANVAS_CREATION_TIMEOUT");
1847
+ }
1848
+ /**
1849
+ * Sets up the interaction system for Phase 7
1850
+ */
1851
+ setupInteractionSystem() {
1852
+ if (!this.iframeManager || !this.interactionManager) return;
1853
+ this.iframeManager.onInteractionEvent("mouse-update", (data) => {
1854
+ this.interactionManager?.updateMouse(data);
1855
+ if (this.workerManager) {
1856
+ this.workerManager.postMessage("mouse-update", data);
1857
+ }
1858
+ });
1859
+ this.iframeManager.onInteractionEvent("keyboard-update", (data) => {
1860
+ this.interactionManager?.updateKeyboard(data);
1861
+ if (this.workerManager) {
1862
+ this.workerManager.postMessage("keyboard-update", data);
1863
+ }
1864
+ });
1865
+ this.iframeManager.onInteractionEvent("touch-update", (data) => {
1866
+ this.interactionManager?.updateTouch(data);
1867
+ if (this.workerManager) {
1868
+ this.workerManager.postMessage("touch-update", data);
1869
+ }
1870
+ });
1871
+ this.iframeManager.setInteractionEnabled(true);
1872
+ }
1873
+ /**
1874
+ * Sets up communication between components
1875
+ */
1876
+ setupCommunication() {
1877
+ if (!this.workerManager) return;
1878
+ this.workerManager.onMessage("ready", (data) => {
1879
+ this.debugLog("Worker ready:", data);
1880
+ });
1881
+ this.workerManager.onMessage("error", (data) => {
1882
+ console.error("Worker error:", data);
1883
+ });
1884
+ this.workerManager.onMessage("performance-warning", (data) => {
1885
+ console.warn("Performance warning:", data);
1886
+ });
1887
+ this.workerManager.onMessage("performance-update", (data) => {
1888
+ if (data.effectiveRefreshRate !== void 0) {
1889
+ this.stats.frameRate.effectiveRefreshRate = data.effectiveRefreshRate;
1890
+ this.stats.frameRate.mode = data.frameRateMode;
1891
+ this.stats.frameRate.screenRefreshRate = data.screenRefreshRate;
1892
+ }
1893
+ if (data.parameterCount !== void 0) {
1894
+ this.stats.parameterCount = data.parameterCount;
1895
+ }
1896
+ });
1897
+ this.workerManager.onMessage("parameters-defined", (data) => {
1898
+ this.handleParametersDefined(data);
1899
+ });
1900
+ this.workerManager.onMessage("parameter-validation-error", (data) => {
1901
+ this.handleParameterError(data);
1902
+ });
1903
+ }
1904
+ // Parameter system implementation for Phase 2
1905
+ /**
1906
+ * Handle parameter definitions received from worker
1907
+ */
1908
+ handleParametersDefined(data) {
1909
+ try {
1910
+ this.parameterGroups.clear();
1911
+ this.parameterValues.clear();
1912
+ for (const group of data.groups) {
1913
+ this.parameterGroups.set(group.groupName, group);
1914
+ for (const [paramName, paramDef] of Object.entries(group.parameters)) {
1915
+ this.parameterValues.set(paramName, paramDef.defaultValue);
1916
+ }
1917
+ }
1918
+ this.parametersInitialized = true;
1919
+ this.debugLog(`Parameters initialized: ${this.parameterValues.size} parameters in ${this.parameterGroups.size} groups`);
1920
+ this.syncAllParametersToWorker();
1921
+ for (const listener of this.parameterDefinedListeners) {
1922
+ try {
1923
+ listener(Array.from(this.parameterGroups.values()));
1924
+ } catch (error) {
1925
+ console.error("Error in parameter defined listener:", error);
1926
+ }
1927
+ }
1928
+ } catch (error) {
1929
+ console.error("Error handling parameters defined:", error);
1930
+ this.handleParameterError({
1931
+ message: `Failed to process parameter definitions: ${error.message}`,
1932
+ code: "PARAMETER_PROCESSING_ERROR"
1933
+ });
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Handle parameter validation errors
1938
+ */
1939
+ handleParameterError(error) {
1940
+ console.error("Parameter error:", error);
1941
+ for (const listener of this.parameterErrorListeners) {
1942
+ try {
1943
+ listener(error);
1944
+ } catch (listenerError) {
1945
+ console.error("Error in parameter error listener:", listenerError);
1946
+ }
1947
+ }
1948
+ }
1949
+ /**
1950
+ * Set a single parameter value
1951
+ */
1952
+ async setParameter(name, value) {
1953
+ this.validateReady();
1954
+ if (!this.parametersInitialized) {
1955
+ throw new VijiCoreError("Parameters not yet initialized", "PARAMETERS_NOT_INITIALIZED");
1956
+ }
1957
+ if (!this.parameterValues.has(name)) {
1958
+ throw new VijiCoreError(`Unknown parameter: ${name}`, "UNKNOWN_PARAMETER");
1959
+ }
1960
+ const oldValue = this.parameterValues.get(name);
1961
+ this.parameterValues.set(name, value);
1962
+ if (this.workerManager) {
1963
+ this.workerManager.postMessage("parameter-update", {
1964
+ name,
1965
+ value
1966
+ });
1967
+ }
1968
+ if (oldValue !== value) {
1969
+ this.notifyParameterListeners(name, value);
1970
+ }
1971
+ }
1972
+ /**
1973
+ * Set multiple parameter values efficiently
1974
+ */
1975
+ async setParameters(values) {
1976
+ this.validateReady();
1977
+ if (!this.parametersInitialized) {
1978
+ throw new VijiCoreError("Parameters not yet initialized", "PARAMETERS_NOT_INITIALIZED");
1979
+ }
1980
+ const updates = [];
1981
+ const changedParams = [];
1982
+ for (const [name, value] of Object.entries(values)) {
1983
+ if (!this.parameterValues.has(name)) {
1984
+ console.warn(`Unknown parameter: ${name}`);
1985
+ continue;
1986
+ }
1987
+ const oldValue = this.parameterValues.get(name);
1988
+ this.parameterValues.set(name, value);
1989
+ updates.push({
1990
+ name,
1991
+ value,
1992
+ timestamp: performance.now()
1993
+ });
1994
+ if (oldValue !== value) {
1995
+ changedParams.push({ name, value });
1996
+ }
1997
+ }
1998
+ if (updates.length > 0 && this.workerManager) {
1999
+ this.workerManager.postMessage("parameter-batch-update", {
2000
+ updates,
2001
+ timestamp: performance.now()
2002
+ });
2003
+ }
2004
+ for (const { name, value } of changedParams) {
2005
+ this.notifyParameterListeners(name, value);
2006
+ }
2007
+ }
2008
+ /**
2009
+ * Get current parameter value
2010
+ */
2011
+ getParameter(name) {
2012
+ return this.parameterValues.get(name);
2013
+ }
2014
+ /**
2015
+ * Get all current parameter values
2016
+ */
2017
+ getParameterValues() {
2018
+ const values = {};
2019
+ for (const [name, value] of this.parameterValues) {
2020
+ values[name] = value;
2021
+ }
2022
+ return values;
2023
+ }
2024
+ /**
2025
+ * Get parameter groups (for UI generation)
2026
+ */
2027
+ getParameterGroups() {
2028
+ return Array.from(this.parameterGroups.values());
2029
+ }
2030
+ /**
2031
+ * Get current core capabilities (what's currently active)
2032
+ */
2033
+ getCapabilities() {
2034
+ return {
2035
+ hasAudio: this.currentAudioStream !== null,
2036
+ hasVideo: this.currentVideoStream !== null,
2037
+ hasInteraction: this.config.allowUserInteraction,
2038
+ hasGeneral: true
2039
+ // General parameters are always available
2040
+ };
2041
+ }
2042
+ /**
2043
+ * Get parameter groups filtered by active capabilities
2044
+ */
2045
+ getVisibleParameterGroups() {
2046
+ const capabilities = this.getCapabilities();
2047
+ const allGroups = this.getParameterGroups();
2048
+ return allGroups.filter((group) => {
2049
+ switch (group.category) {
2050
+ case "audio":
2051
+ return capabilities.hasAudio;
2052
+ case "video":
2053
+ return capabilities.hasVideo;
2054
+ case "interaction":
2055
+ return capabilities.hasInteraction;
2056
+ case "general":
2057
+ default:
2058
+ return capabilities.hasGeneral;
2059
+ }
2060
+ }).map((group) => ({
2061
+ ...group,
2062
+ // Also filter individual parameters within groups
2063
+ parameters: Object.fromEntries(
2064
+ Object.entries(group.parameters).filter(([_, paramDef]) => {
2065
+ switch (paramDef.category) {
2066
+ case "audio":
2067
+ return capabilities.hasAudio;
2068
+ case "video":
2069
+ return capabilities.hasVideo;
2070
+ case "interaction":
2071
+ return capabilities.hasInteraction;
2072
+ case "general":
2073
+ default:
2074
+ return capabilities.hasGeneral;
2075
+ }
2076
+ })
2077
+ )
2078
+ }));
2079
+ }
2080
+ /**
2081
+ * Check if a specific parameter category is currently active
2082
+ */
2083
+ isCategoryActive(category) {
2084
+ const capabilities = this.getCapabilities();
2085
+ switch (category) {
2086
+ case "audio":
2087
+ return capabilities.hasAudio;
2088
+ case "video":
2089
+ return capabilities.hasVideo;
2090
+ case "interaction":
2091
+ return capabilities.hasInteraction;
2092
+ case "general":
2093
+ default:
2094
+ return capabilities.hasGeneral;
2095
+ }
2096
+ }
2097
+ /**
2098
+ * Check if parameters have been initialized
2099
+ */
2100
+ get parametersReady() {
2101
+ return this.parametersInitialized;
2102
+ }
2103
+ /**
2104
+ * Send all current parameter values to worker (used for initial sync)
2105
+ */
2106
+ syncAllParametersToWorker() {
2107
+ if (!this.workerManager || this.parameterValues.size === 0) {
2108
+ return;
2109
+ }
2110
+ const updates = [];
2111
+ for (const [name, value] of this.parameterValues) {
2112
+ updates.push({
2113
+ name,
2114
+ value,
2115
+ timestamp: performance.now()
2116
+ });
2117
+ }
2118
+ this.workerManager.postMessage("parameter-batch-update", {
2119
+ updates,
2120
+ timestamp: performance.now()
2121
+ });
2122
+ this.debugLog(`Synced ${updates.length} parameter values to worker`);
2123
+ }
2124
+ /**
2125
+ * Add listener for when parameters are defined
2126
+ */
2127
+ onParametersDefined(listener) {
2128
+ this.parameterDefinedListeners.add(listener);
2129
+ }
2130
+ /**
2131
+ * Remove parameter defined listener
2132
+ */
2133
+ offParametersDefined(listener) {
2134
+ this.parameterDefinedListeners.delete(listener);
2135
+ }
2136
+ /**
2137
+ * Add listener for parameter value changes
2138
+ */
2139
+ onParameterChange(parameterName, listener) {
2140
+ if (!this.parameterListeners.has(parameterName)) {
2141
+ this.parameterListeners.set(parameterName, /* @__PURE__ */ new Set());
2142
+ }
2143
+ this.parameterListeners.get(parameterName).add(listener);
2144
+ }
2145
+ /**
2146
+ * Remove parameter change listener
2147
+ */
2148
+ offParameterChange(parameterName, listener) {
2149
+ const listeners = this.parameterListeners.get(parameterName);
2150
+ if (listeners) {
2151
+ listeners.delete(listener);
2152
+ if (listeners.size === 0) {
2153
+ this.parameterListeners.delete(parameterName);
2154
+ }
2155
+ }
2156
+ }
2157
+ /**
2158
+ * Add listener for parameter errors
2159
+ */
2160
+ onParameterError(listener) {
2161
+ this.parameterErrorListeners.add(listener);
2162
+ }
2163
+ /**
2164
+ * Add listener for when core capabilities change (audio/video/interaction state)
2165
+ */
2166
+ onCapabilitiesChange(listener) {
2167
+ this.capabilitiesChangeListeners.add(listener);
2168
+ }
2169
+ /**
2170
+ * Remove capabilities change listener
2171
+ */
2172
+ removeCapabilitiesListener(listener) {
2173
+ this.capabilitiesChangeListeners.delete(listener);
2174
+ }
2175
+ /**
2176
+ * Notify capability change listeners
2177
+ */
2178
+ notifyCapabilitiesChange() {
2179
+ const capabilities = this.getCapabilities();
2180
+ for (const listener of this.capabilitiesChangeListeners) {
2181
+ try {
2182
+ listener(capabilities);
2183
+ } catch (error) {
2184
+ console.error("Error in capabilities change listener:", error);
2185
+ }
2186
+ }
2187
+ }
2188
+ /**
2189
+ * Remove parameter error listener
2190
+ */
2191
+ offParameterError(listener) {
2192
+ this.parameterErrorListeners.delete(listener);
2193
+ }
2194
+ /**
2195
+ * Notify parameter change listeners
2196
+ */
2197
+ notifyParameterListeners(name, value) {
2198
+ const listeners = this.parameterListeners.get(name);
2199
+ if (listeners) {
2200
+ for (const listener of listeners) {
2201
+ try {
2202
+ listener(value);
2203
+ } catch (error) {
2204
+ console.error(`Error in parameter listener for '${name}':`, error);
2205
+ }
2206
+ }
2207
+ }
2208
+ }
2209
+ /**
2210
+ * Sets the frame rate to full speed (every animation frame)
2211
+ */
2212
+ async setFullFrameRate() {
2213
+ this.validateReady();
2214
+ this.config.frameRateMode = "full";
2215
+ if (this.workerManager) {
2216
+ this.workerManager.postMessage("frame-rate-update", { mode: "full" });
2217
+ }
2218
+ this.updateFrameRateStats();
2219
+ this.debugLog(`Frame rate set to full (${this.instanceId})`);
2220
+ }
2221
+ /**
2222
+ * Sets the frame rate to half speed (every second animation frame)
2223
+ */
2224
+ async setHalfFrameRate() {
2225
+ this.validateReady();
2226
+ this.config.frameRateMode = "half";
2227
+ if (this.workerManager) {
2228
+ this.workerManager.postMessage("frame-rate-update", { mode: "half" });
2229
+ }
2230
+ this.updateFrameRateStats();
2231
+ this.debugLog(`Frame rate set to half (${this.instanceId})`);
2232
+ }
2233
+ /**
2234
+ * Updates the canvas resolution by sending effective dimensions to the worker
2235
+ */
2236
+ updateResolution() {
2237
+ this.validateReady();
2238
+ if (!this.iframeManager || !this.workerManager) {
2239
+ throw new VijiCoreError("Managers not available", "MANAGERS_NOT_READY");
2240
+ }
2241
+ const effectiveResolution = this.iframeManager.getEffectiveResolution();
2242
+ this.workerManager.postMessage("resolution-update", {
2243
+ effectiveWidth: effectiveResolution.width,
2244
+ effectiveHeight: effectiveResolution.height
2245
+ });
2246
+ this.stats.resolution = effectiveResolution;
2247
+ this.stats.scale = this.iframeManager.getScale();
2248
+ this.debugLog(`Resolution updated successfully (${this.instanceId})`, effectiveResolution);
2249
+ }
2250
+ /**
2251
+ * Sets the audio stream for analysis
2252
+ */
2253
+ async setAudioStream(audioStream) {
2254
+ if (this.isInitialized && !this.isInitializing) {
2255
+ this.validateReady();
2256
+ }
2257
+ const previouslyHadAudio = this.currentAudioStream !== null;
2258
+ this.currentAudioStream = audioStream;
2259
+ const nowHasAudio = this.currentAudioStream !== null;
2260
+ if (this.audioSystem) {
2261
+ this.audioSystem.handleAudioStreamUpdate({
2262
+ audioStream,
2263
+ ...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
2264
+ timestamp: performance.now()
2265
+ });
2266
+ }
2267
+ if (previouslyHadAudio !== nowHasAudio) {
2268
+ this.notifyCapabilitiesChange();
2269
+ }
2270
+ this.debugLog(`Audio stream ${audioStream ? "connected" : "disconnected"} (${this.instanceId})`);
2271
+ }
2272
+ /**
2273
+ * Sets the video stream for processing
2274
+ */
2275
+ async setVideoStream(videoStream) {
2276
+ if (this.isInitialized && !this.isInitializing) {
2277
+ this.validateReady();
2278
+ }
2279
+ const previouslyHadVideo = this.currentVideoStream !== null;
2280
+ this.currentVideoStream = videoStream;
2281
+ const nowHasVideo = this.currentVideoStream !== null;
2282
+ if (this.videoCoordinator) {
2283
+ this.videoCoordinator.handleVideoStreamUpdate({
2284
+ videoStream,
2285
+ targetFrameRate: 30,
2286
+ // Default target FPS
2287
+ timestamp: performance.now()
2288
+ });
2289
+ }
2290
+ if (previouslyHadVideo !== nowHasVideo) {
2291
+ this.notifyCapabilitiesChange();
2292
+ }
2293
+ this.debugLog(`Video stream ${videoStream ? "connected" : "disconnected"} (${this.instanceId})`);
2294
+ }
2295
+ /**
2296
+ * Gets the current audio stream
2297
+ */
2298
+ getAudioStream() {
2299
+ return this.currentAudioStream;
2300
+ }
2301
+ /**
2302
+ * Updates audio analysis configuration
2303
+ */
2304
+ async setAudioAnalysisConfig(config) {
2305
+ this.validateReady();
2306
+ this.config.analysisConfig = { ...this.config.analysisConfig, ...config };
2307
+ if (this.audioSystem) {
2308
+ this.audioSystem.handleAudioStreamUpdate({
2309
+ audioStream: this.currentAudioStream,
2310
+ ...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
2311
+ timestamp: performance.now()
2312
+ });
2313
+ }
2314
+ this.debugLog(`Audio analysis config updated (${this.instanceId})`, config);
2315
+ }
2316
+ /**
2317
+ * Updates the canvas resolution by scale
2318
+ */
2319
+ setResolution(scale) {
2320
+ this.validateReady();
2321
+ if (!this.iframeManager || !this.workerManager) {
2322
+ throw new VijiCoreError("Managers not available", "MANAGERS_NOT_READY");
2323
+ }
2324
+ this.debugLog(`Updating resolution scale to:`, scale, `(${this.instanceId})`);
2325
+ const effectiveResolution = this.iframeManager.updateScale(scale);
2326
+ this.workerManager.postMessage("resolution-update", {
2327
+ effectiveWidth: effectiveResolution.width,
2328
+ effectiveHeight: effectiveResolution.height
2329
+ });
2330
+ this.stats.resolution = effectiveResolution;
2331
+ this.stats.scale = scale;
2332
+ this.debugLog(`Resolution updated successfully (${this.instanceId})`, effectiveResolution);
2333
+ }
2334
+ /**
2335
+ * Detects the screen refresh rate
2336
+ */
2337
+ async detectScreenRefreshRate() {
2338
+ return new Promise((resolve) => {
2339
+ let frameCount = 0;
2340
+ let startTime = performance.now();
2341
+ const measureFrames = () => {
2342
+ frameCount++;
2343
+ if (frameCount === 60) {
2344
+ const elapsed = performance.now() - startTime;
2345
+ this.screenRefreshRate = Math.round(6e4 / elapsed);
2346
+ this.debugLog("Detected screen refresh rate:", this.screenRefreshRate + "Hz");
2347
+ resolve();
2348
+ } else if (frameCount < 60) {
2349
+ requestAnimationFrame(measureFrames);
2350
+ }
2351
+ };
2352
+ requestAnimationFrame(measureFrames);
2353
+ });
2354
+ }
2355
+ /**
2356
+ * Updates frame rate statistics
2357
+ */
2358
+ updateFrameRateStats() {
2359
+ this.stats.frameRate = {
2360
+ mode: this.config.frameRateMode,
2361
+ screenRefreshRate: this.screenRefreshRate,
2362
+ effectiveRefreshRate: 0
2363
+ // Will be updated by worker during execution
2364
+ };
2365
+ }
2366
+ /**
2367
+ * Gets current performance statistics
2368
+ */
2369
+ getStats() {
2370
+ this.validateReady();
2371
+ if (!this.iframeManager) {
2372
+ throw new VijiCoreError("IFrame manager not available", "MANAGER_NOT_READY");
2373
+ }
2374
+ return this.stats;
2375
+ }
2376
+ /**
2377
+ * Checks if the core is ready for use
2378
+ */
2379
+ get ready() {
2380
+ return this.isInitialized && !this.isDestroyed && this.iframeManager?.ready === true && this.workerManager?.ready === true;
2381
+ }
2382
+ /**
2383
+ * Gets the current configuration
2384
+ */
2385
+ get configuration() {
2386
+ return { ...this.config };
2387
+ }
2388
+ /**
2389
+ * Destroys the core instance and cleans up all resources
2390
+ */
2391
+ async destroy() {
2392
+ if (this.isDestroyed) {
2393
+ return;
2394
+ }
2395
+ this.isDestroyed = true;
2396
+ this.parameterGroups.clear();
2397
+ this.parameterValues.clear();
2398
+ this.parameterListeners.clear();
2399
+ this.parameterDefinedListeners.clear();
2400
+ this.parameterErrorListeners.clear();
2401
+ this.capabilitiesChangeListeners.clear();
2402
+ if (this.audioSystem) {
2403
+ this.audioSystem.resetAudioState();
2404
+ this.audioSystem = null;
2405
+ }
2406
+ this.currentAudioStream = null;
2407
+ if (this.videoCoordinator) {
2408
+ this.videoCoordinator.resetVideoState();
2409
+ this.videoCoordinator = null;
2410
+ }
2411
+ this.currentVideoStream = null;
2412
+ await this.cleanup();
2413
+ this.debugLog(`VijiCore destroyed (${this.instanceId})`);
2414
+ }
2415
+ /**
2416
+ * Validates that the core is ready for operations
2417
+ */
2418
+ validateReady() {
2419
+ if (this.isDestroyed) {
2420
+ throw new VijiCoreError("Core instance has been destroyed", "INSTANCE_DESTROYED");
2421
+ }
2422
+ if (!this.ready) {
2423
+ throw new VijiCoreError("Core is not ready", "CORE_NOT_READY");
2424
+ }
2425
+ }
2426
+ /**
2427
+ * Validates the provided configuration
2428
+ */
2429
+ validateConfig(config) {
2430
+ if (!config.hostContainer) {
2431
+ throw new VijiCoreError("hostContainer is required", "INVALID_CONFIG");
2432
+ }
2433
+ if (!config.sceneCode || typeof config.sceneCode !== "string") {
2434
+ throw new VijiCoreError("sceneCode must be a non-empty string", "INVALID_CONFIG");
2435
+ }
2436
+ if (config.frameRateMode !== void 0 && config.frameRateMode !== "full" && config.frameRateMode !== "half") {
2437
+ throw new VijiCoreError('frameRateMode must be either "full" or "half"', "INVALID_CONFIG");
2438
+ }
2439
+ }
2440
+ /**
2441
+ * Cleans up all resources
2442
+ */
2443
+ async cleanup() {
2444
+ try {
2445
+ if (this.workerManager) {
2446
+ this.workerManager.destroy();
2447
+ this.workerManager = null;
2448
+ }
2449
+ if (this.iframeManager) {
2450
+ this.iframeManager.destroy();
2451
+ this.iframeManager = null;
2452
+ }
2453
+ this.isInitialized = false;
2454
+ this.isInitializing = false;
2455
+ } catch (error) {
2456
+ console.warn("Error during cleanup:", error);
2457
+ }
2458
+ }
2459
+ }
2460
+ const VERSION = "0.1.0-alpha.1";
2461
+ export {
2462
+ VERSION,
2463
+ VijiCore,
2464
+ VijiCoreError
2465
+ };
2466
+ //# sourceMappingURL=index.js.map