@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.
@@ -0,0 +1,1287 @@
1
+ (function() {
2
+ "use strict";
3
+ class ParameterSystem {
4
+ // Parameter system for Phase 2 (new object-based approach)
5
+ parameterDefinitions = /* @__PURE__ */ new Map();
6
+ parameterGroups = /* @__PURE__ */ new Map();
7
+ parameterValues = /* @__PURE__ */ new Map();
8
+ parameterObjects = /* @__PURE__ */ new Map();
9
+ // Maps parameter names to their objects
10
+ parametersDefined = false;
11
+ initialValuesSynced = false;
12
+ // Track if initial values have been synced from host
13
+ // Debug logging control
14
+ debugMode = false;
15
+ /**
16
+ * Enable or disable debug logging
17
+ */
18
+ setDebugMode(enabled) {
19
+ this.debugMode = enabled;
20
+ }
21
+ /**
22
+ * Debug logging helper
23
+ */
24
+ debugLog(message, ...args) {
25
+ if (this.debugMode) {
26
+ console.log(message, ...args);
27
+ }
28
+ }
29
+ // Message posting callback
30
+ postMessageCallback;
31
+ constructor(postMessageCallback) {
32
+ this.postMessageCallback = postMessageCallback;
33
+ }
34
+ // Parameter helper function implementations (return parameter objects)
35
+ createSliderParameter(defaultValue, config) {
36
+ const paramName = config.label;
37
+ const sliderObject = {
38
+ value: defaultValue,
39
+ min: config.min ?? 0,
40
+ max: config.max ?? 100,
41
+ step: config.step ?? 1,
42
+ label: config.label,
43
+ description: config.description ?? "",
44
+ group: config.group ?? "general",
45
+ category: config.category ?? "general"
46
+ };
47
+ const definition = {
48
+ type: "slider",
49
+ defaultValue,
50
+ label: sliderObject.label,
51
+ description: sliderObject.description,
52
+ group: sliderObject.group,
53
+ category: sliderObject.category,
54
+ config: {
55
+ min: sliderObject.min,
56
+ max: sliderObject.max,
57
+ step: sliderObject.step
58
+ }
59
+ };
60
+ this.storeParameterDefinition(paramName, definition);
61
+ this.parameterObjects.set(paramName, sliderObject);
62
+ return sliderObject;
63
+ }
64
+ createColorParameter(defaultValue, config) {
65
+ const paramName = config.label;
66
+ const colorObject = {
67
+ value: defaultValue,
68
+ label: config.label,
69
+ description: config.description ?? "",
70
+ group: config.group ?? "general",
71
+ category: config.category ?? "general"
72
+ };
73
+ const definition = {
74
+ type: "color",
75
+ defaultValue,
76
+ label: colorObject.label,
77
+ description: colorObject.description,
78
+ group: colorObject.group,
79
+ category: colorObject.category
80
+ };
81
+ this.storeParameterDefinition(paramName, definition);
82
+ this.parameterObjects.set(paramName, colorObject);
83
+ return colorObject;
84
+ }
85
+ createToggleParameter(defaultValue, config) {
86
+ const paramName = config.label;
87
+ const toggleObject = {
88
+ value: defaultValue,
89
+ label: config.label,
90
+ description: config.description ?? "",
91
+ group: config.group ?? "general",
92
+ category: config.category ?? "general"
93
+ };
94
+ const definition = {
95
+ type: "toggle",
96
+ defaultValue,
97
+ label: toggleObject.label,
98
+ description: toggleObject.description,
99
+ group: toggleObject.group,
100
+ category: toggleObject.category
101
+ };
102
+ this.storeParameterDefinition(paramName, definition);
103
+ this.parameterObjects.set(paramName, toggleObject);
104
+ return toggleObject;
105
+ }
106
+ createSelectParameter(defaultValue, config) {
107
+ const paramName = config.label;
108
+ const selectObject = {
109
+ value: defaultValue,
110
+ options: config.options,
111
+ label: config.label,
112
+ description: config.description ?? "",
113
+ group: config.group ?? "general",
114
+ category: config.category ?? "general"
115
+ };
116
+ const definition = {
117
+ type: "select",
118
+ defaultValue,
119
+ label: selectObject.label,
120
+ description: selectObject.description,
121
+ group: selectObject.group,
122
+ category: selectObject.category,
123
+ config: {
124
+ options: selectObject.options
125
+ }
126
+ };
127
+ this.storeParameterDefinition(paramName, definition);
128
+ this.parameterObjects.set(paramName, selectObject);
129
+ return selectObject;
130
+ }
131
+ createTextParameter(defaultValue, config) {
132
+ const paramName = config.label;
133
+ const textObject = {
134
+ value: defaultValue,
135
+ maxLength: config.maxLength ?? 1e3,
136
+ label: config.label,
137
+ description: config.description ?? "",
138
+ group: config.group ?? "general",
139
+ category: config.category ?? "general"
140
+ };
141
+ const definition = {
142
+ type: "text",
143
+ defaultValue,
144
+ label: textObject.label,
145
+ description: textObject.description,
146
+ group: textObject.group,
147
+ category: textObject.category,
148
+ config: {
149
+ maxLength: textObject.maxLength
150
+ }
151
+ };
152
+ this.storeParameterDefinition(paramName, definition);
153
+ this.parameterObjects.set(paramName, textObject);
154
+ return textObject;
155
+ }
156
+ createNumberParameter(defaultValue, config) {
157
+ const paramName = config.label;
158
+ const numberObject = {
159
+ value: defaultValue,
160
+ min: config.min ?? 0,
161
+ max: config.max ?? 100,
162
+ step: config.step ?? 1,
163
+ label: config.label,
164
+ description: config.description ?? "",
165
+ group: config.group ?? "general",
166
+ category: config.category ?? "general"
167
+ };
168
+ const definition = {
169
+ type: "number",
170
+ defaultValue,
171
+ label: numberObject.label,
172
+ description: numberObject.description,
173
+ group: numberObject.group,
174
+ category: numberObject.category,
175
+ config: {
176
+ min: numberObject.min,
177
+ max: numberObject.max,
178
+ step: numberObject.step
179
+ }
180
+ };
181
+ this.storeParameterDefinition(paramName, definition);
182
+ this.parameterObjects.set(paramName, numberObject);
183
+ return numberObject;
184
+ }
185
+ storeParameterDefinition(name, definition) {
186
+ this.parameterDefinitions.set(name, definition);
187
+ this.parameterValues.set(name, definition.defaultValue);
188
+ }
189
+ updateParameterValue(name, value) {
190
+ const definition = this.parameterDefinitions.get(name);
191
+ if (!definition) {
192
+ console.warn(`Unknown parameter: ${name}. Available parameters:`, Array.from(this.parameterDefinitions.keys()));
193
+ return false;
194
+ }
195
+ if (!this.validateParameterValue(name, value, definition)) {
196
+ console.warn(`Validation failed for parameter ${name} = ${value}`);
197
+ return false;
198
+ }
199
+ const currentValue = this.parameterValues.get(name);
200
+ const isInitialSync = !this.initialValuesSynced;
201
+ if (currentValue === value && !isInitialSync) {
202
+ return false;
203
+ }
204
+ this.parameterValues.set(name, value);
205
+ const parameterObject = this.parameterObjects.get(name);
206
+ if (parameterObject) {
207
+ parameterObject.value = value;
208
+ }
209
+ return true;
210
+ }
211
+ validateParameterValue(name, value, definition) {
212
+ if (definition.validate && !definition.validate(value)) {
213
+ console.error(`Custom validation failed for parameter '${name}': ${value}`);
214
+ return false;
215
+ }
216
+ switch (definition.type) {
217
+ case "slider":
218
+ case "number":
219
+ if (typeof value !== "number" || isNaN(value)) {
220
+ console.error(`Parameter '${name}' must be a number, got: ${value}`);
221
+ return false;
222
+ }
223
+ if (definition.config?.min !== void 0 && value < definition.config.min) {
224
+ console.error(`Parameter '${name}' value ${value} is below minimum ${definition.config.min}`);
225
+ return false;
226
+ }
227
+ if (definition.config?.max !== void 0 && value > definition.config.max) {
228
+ console.error(`Parameter '${name}' value ${value} is above maximum ${definition.config.max}`);
229
+ return false;
230
+ }
231
+ break;
232
+ case "color":
233
+ if (typeof value !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(value)) {
234
+ console.error(`Parameter '${name}' must be a valid hex color, got: ${value}`);
235
+ return false;
236
+ }
237
+ break;
238
+ case "toggle":
239
+ if (typeof value !== "boolean") {
240
+ console.error(`Parameter '${name}' must be a boolean, got: ${value}`);
241
+ return false;
242
+ }
243
+ break;
244
+ case "select":
245
+ if (!definition.config?.options || !definition.config.options.includes(value)) {
246
+ console.error(`Parameter '${name}' value ${value} is not in options: ${definition.config?.options}`);
247
+ return false;
248
+ }
249
+ break;
250
+ case "text":
251
+ if (typeof value !== "string") {
252
+ console.error(`Parameter '${name}' must be a string, got: ${value}`);
253
+ return false;
254
+ }
255
+ if (definition.config?.maxLength && value.length > definition.config.maxLength) {
256
+ console.error(`Parameter '${name}' text too long: ${value.length} > ${definition.config.maxLength}`);
257
+ return false;
258
+ }
259
+ break;
260
+ }
261
+ return true;
262
+ }
263
+ // Reset parameter state (called when loading new scene)
264
+ resetParameterState() {
265
+ this.parametersDefined = false;
266
+ this.initialValuesSynced = false;
267
+ this.parameterDefinitions.clear();
268
+ this.parameterGroups.clear();
269
+ this.parameterValues.clear();
270
+ this.parameterObjects.clear();
271
+ }
272
+ // Send all parameters (from helper functions) to host
273
+ sendAllParametersToHost() {
274
+ if (this.parametersDefined || this.parameterDefinitions.size === 0) {
275
+ return;
276
+ }
277
+ try {
278
+ const groups = /* @__PURE__ */ new Map();
279
+ for (const [paramName, paramDef] of this.parameterDefinitions) {
280
+ const groupName = paramDef.group || "general";
281
+ if (!groups.has(groupName)) {
282
+ const category = paramDef.category || "general";
283
+ groups.set(groupName, {
284
+ groupName,
285
+ category,
286
+ parameters: {}
287
+ });
288
+ }
289
+ const group = groups.get(groupName);
290
+ group.parameters[paramName] = paramDef;
291
+ }
292
+ this.parametersDefined = true;
293
+ this.postMessageCallback("parameters-defined", {
294
+ groups: Array.from(groups.values()),
295
+ timestamp: performance.now()
296
+ });
297
+ this.debugLog(`All parameters sent to host: ${this.parameterDefinitions.size} parameters in ${groups.size} groups`);
298
+ } catch (error) {
299
+ this.postMessageCallback("parameter-validation-error", {
300
+ message: `Failed to send parameters to host: ${error.message}`,
301
+ code: "PARAMETER_SENDING_ERROR"
302
+ });
303
+ }
304
+ }
305
+ // Mark initial values as synced
306
+ markInitialValuesSynced() {
307
+ this.initialValuesSynced = true;
308
+ }
309
+ // Get parameter count for performance reporting
310
+ getParameterCount() {
311
+ return this.parameterDefinitions.size;
312
+ }
313
+ }
314
+ class InteractionSystem {
315
+ // Mouse interaction state
316
+ mouseState = {
317
+ x: 0,
318
+ y: 0,
319
+ isInCanvas: false,
320
+ isPressed: false,
321
+ leftButton: false,
322
+ rightButton: false,
323
+ middleButton: false,
324
+ velocity: { x: 0, y: 0 },
325
+ deltaX: 0,
326
+ deltaY: 0,
327
+ wheelDelta: 0,
328
+ wheelX: 0,
329
+ wheelY: 0,
330
+ wasPressed: false,
331
+ wasReleased: false,
332
+ wasMoved: false
333
+ };
334
+ // Keyboard interaction state
335
+ keyboardState = {
336
+ isPressed: (key) => this.keyboardState.activeKeys.has(key.toLowerCase()),
337
+ wasPressed: (key) => this.keyboardState.pressedThisFrame.has(key.toLowerCase()),
338
+ wasReleased: (key) => this.keyboardState.releasedThisFrame.has(key.toLowerCase()),
339
+ activeKeys: /* @__PURE__ */ new Set(),
340
+ pressedThisFrame: /* @__PURE__ */ new Set(),
341
+ releasedThisFrame: /* @__PURE__ */ new Set(),
342
+ lastKeyPressed: "",
343
+ lastKeyReleased: "",
344
+ shift: false,
345
+ ctrl: false,
346
+ alt: false,
347
+ meta: false
348
+ };
349
+ // Touch interaction state
350
+ touchState = {
351
+ points: [],
352
+ count: 0,
353
+ started: [],
354
+ moved: [],
355
+ ended: [],
356
+ primary: null,
357
+ gestures: {
358
+ isPinching: false,
359
+ isRotating: false,
360
+ isPanning: false,
361
+ isTapping: false,
362
+ pinchScale: 1,
363
+ pinchDelta: 0,
364
+ rotationAngle: 0,
365
+ rotationDelta: 0,
366
+ panDelta: { x: 0, y: 0 },
367
+ tapCount: 0,
368
+ lastTapTime: 0,
369
+ tapPosition: null
370
+ }
371
+ };
372
+ constructor() {
373
+ this.handleMouseUpdate = this.handleMouseUpdate.bind(this);
374
+ this.handleKeyboardUpdate = this.handleKeyboardUpdate.bind(this);
375
+ this.handleTouchUpdate = this.handleTouchUpdate.bind(this);
376
+ this.frameStart = this.frameStart.bind(this);
377
+ }
378
+ /**
379
+ * Get the interaction APIs for inclusion in the viji object
380
+ */
381
+ getInteractionAPIs() {
382
+ return {
383
+ mouse: this.mouseState,
384
+ keyboard: this.keyboardState,
385
+ touches: this.touchState
386
+ };
387
+ }
388
+ /**
389
+ * Called at the start of each frame to reset frame-based events
390
+ */
391
+ frameStart() {
392
+ this.mouseState.wasPressed = false;
393
+ this.mouseState.wasReleased = false;
394
+ this.mouseState.wasMoved = false;
395
+ this.mouseState.wheelDelta = 0;
396
+ this.mouseState.wheelX = 0;
397
+ this.mouseState.wheelY = 0;
398
+ this.keyboardState.pressedThisFrame.clear();
399
+ this.keyboardState.releasedThisFrame.clear();
400
+ this.touchState.started = [];
401
+ this.touchState.moved = [];
402
+ this.touchState.ended = [];
403
+ this.touchState.gestures.isTapping = false;
404
+ this.touchState.gestures.pinchDelta = 0;
405
+ this.touchState.gestures.rotationDelta = 0;
406
+ }
407
+ /**
408
+ * Handle mouse update messages from the host
409
+ */
410
+ handleMouseUpdate(data) {
411
+ this.mouseState.x = data.x;
412
+ this.mouseState.y = data.y;
413
+ this.mouseState.isInCanvas = data.isInCanvas !== void 0 ? data.isInCanvas : true;
414
+ this.mouseState.leftButton = (data.buttons & 1) !== 0;
415
+ this.mouseState.rightButton = (data.buttons & 2) !== 0;
416
+ this.mouseState.middleButton = (data.buttons & 4) !== 0;
417
+ this.mouseState.isPressed = data.buttons > 0;
418
+ this.mouseState.deltaX = data.deltaX || 0;
419
+ this.mouseState.deltaY = data.deltaY || 0;
420
+ this.mouseState.wheelDelta = data.wheelDeltaY || 0;
421
+ this.mouseState.wheelX = data.wheelDeltaX || 0;
422
+ this.mouseState.wheelY = data.wheelDeltaY || 0;
423
+ this.mouseState.velocity.x = data.deltaX || 0;
424
+ this.mouseState.velocity.y = data.deltaY || 0;
425
+ this.mouseState.wasPressed = data.wasPressed || false;
426
+ this.mouseState.wasReleased = data.wasReleased || false;
427
+ this.mouseState.wasMoved = data.deltaX !== 0 || data.deltaY !== 0;
428
+ }
429
+ /**
430
+ * Handle keyboard update messages from the host
431
+ */
432
+ handleKeyboardUpdate(data) {
433
+ const key = data.key.toLowerCase();
434
+ if (data.type === "keydown") {
435
+ if (!this.keyboardState.activeKeys.has(key)) {
436
+ this.keyboardState.activeKeys.add(key);
437
+ this.keyboardState.pressedThisFrame.add(key);
438
+ this.keyboardState.lastKeyPressed = data.key;
439
+ }
440
+ } else if (data.type === "keyup") {
441
+ this.keyboardState.activeKeys.delete(key);
442
+ this.keyboardState.releasedThisFrame.add(key);
443
+ this.keyboardState.lastKeyReleased = data.key;
444
+ }
445
+ this.keyboardState.shift = data.shiftKey;
446
+ this.keyboardState.ctrl = data.ctrlKey;
447
+ this.keyboardState.alt = data.altKey;
448
+ this.keyboardState.meta = data.metaKey;
449
+ }
450
+ /**
451
+ * Handle touch update messages from the host
452
+ */
453
+ handleTouchUpdate(data) {
454
+ this.touchState.started = [];
455
+ this.touchState.moved = [];
456
+ this.touchState.ended = [];
457
+ const touches = data.touches.map((touch) => ({
458
+ id: touch.identifier,
459
+ x: touch.clientX,
460
+ y: touch.clientY,
461
+ pressure: touch.pressure || 0,
462
+ radius: Math.max(touch.radiusX || 0, touch.radiusY || 0),
463
+ radiusX: touch.radiusX || 0,
464
+ radiusY: touch.radiusY || 0,
465
+ rotationAngle: touch.rotationAngle || 0,
466
+ force: touch.force || touch.pressure || 0,
467
+ deltaX: 0,
468
+ // Could be calculated if we track previous positions
469
+ deltaY: 0,
470
+ velocity: { x: 0, y: 0 },
471
+ // Could be calculated if we track movement
472
+ isNew: data.type === "touchstart",
473
+ isActive: true,
474
+ isEnding: data.type === "touchend" || data.type === "touchcancel"
475
+ }));
476
+ this.touchState.points = touches;
477
+ this.touchState.count = touches.length;
478
+ this.touchState.primary = touches[0] || null;
479
+ if (data.type === "touchstart") {
480
+ this.touchState.started = touches;
481
+ } else if (data.type === "touchmove") {
482
+ this.touchState.moved = touches;
483
+ } else if (data.type === "touchend" || data.type === "touchcancel") {
484
+ this.touchState.ended = touches;
485
+ }
486
+ this.touchState.gestures = {
487
+ isPinching: false,
488
+ isRotating: false,
489
+ isPanning: false,
490
+ isTapping: false,
491
+ pinchScale: 1,
492
+ pinchDelta: 0,
493
+ rotationAngle: 0,
494
+ rotationDelta: 0,
495
+ panDelta: { x: 0, y: 0 },
496
+ tapCount: 0,
497
+ lastTapTime: 0,
498
+ tapPosition: null
499
+ };
500
+ }
501
+ /**
502
+ * Reset all interaction state (called when loading new scene)
503
+ */
504
+ resetInteractionState() {
505
+ Object.assign(this.mouseState, {
506
+ x: 0,
507
+ y: 0,
508
+ isInCanvas: false,
509
+ isPressed: false,
510
+ leftButton: false,
511
+ rightButton: false,
512
+ middleButton: false,
513
+ velocity: { x: 0, y: 0 },
514
+ deltaX: 0,
515
+ deltaY: 0,
516
+ wheelDelta: 0,
517
+ wheelX: 0,
518
+ wheelY: 0,
519
+ wasPressed: false,
520
+ wasReleased: false,
521
+ wasMoved: false
522
+ });
523
+ this.keyboardState.activeKeys.clear();
524
+ this.keyboardState.pressedThisFrame.clear();
525
+ this.keyboardState.releasedThisFrame.clear();
526
+ this.keyboardState.lastKeyPressed = "";
527
+ this.keyboardState.lastKeyReleased = "";
528
+ this.keyboardState.shift = false;
529
+ this.keyboardState.ctrl = false;
530
+ this.keyboardState.alt = false;
531
+ this.keyboardState.meta = false;
532
+ this.touchState.points = [];
533
+ this.touchState.count = 0;
534
+ this.touchState.started = [];
535
+ this.touchState.moved = [];
536
+ this.touchState.ended = [];
537
+ this.touchState.primary = null;
538
+ Object.assign(this.touchState.gestures, {
539
+ isPinching: false,
540
+ isRotating: false,
541
+ isPanning: false,
542
+ isTapping: false,
543
+ pinchScale: 1,
544
+ pinchDelta: 0,
545
+ rotationAngle: 0,
546
+ rotationDelta: 0,
547
+ panDelta: { x: 0, y: 0 },
548
+ tapCount: 0,
549
+ lastTapTime: 0,
550
+ tapPosition: null
551
+ });
552
+ }
553
+ }
554
+ class VideoSystem {
555
+ // ✅ CORRECT: Worker-owned OffscreenCanvas (transferred from host)
556
+ offscreenCanvas = null;
557
+ ctx = null;
558
+ gl = null;
559
+ // Debug logging control
560
+ debugMode = false;
561
+ /**
562
+ * Enable or disable debug logging
563
+ */
564
+ setDebugMode(enabled) {
565
+ this.debugMode = enabled;
566
+ }
567
+ /**
568
+ * Debug logging helper
569
+ */
570
+ debugLog(message, ...args) {
571
+ if (this.debugMode) {
572
+ console.log(message, ...args);
573
+ }
574
+ }
575
+ // Frame processing configuration
576
+ targetFrameRate = 30;
577
+ // Default target FPS for video processing
578
+ lastFrameTime = 0;
579
+ frameInterval = 1e3 / this.targetFrameRate;
580
+ // ms between frames
581
+ // Processing state
582
+ hasLoggedFirstFrame = false;
583
+ frameCount = 0;
584
+ // Video state for artist API
585
+ videoState = {
586
+ isConnected: false,
587
+ currentFrame: null,
588
+ frameWidth: 0,
589
+ frameHeight: 0,
590
+ frameRate: 0,
591
+ frameData: null
592
+ };
593
+ // Phase 11 preparation - CV processing placeholder
594
+ cvFeatures = {
595
+ faceDetection: false,
596
+ handTracking: false,
597
+ bodySegmentation: false
598
+ };
599
+ cvResults = {
600
+ faces: [],
601
+ hands: [],
602
+ bodySegmentation: null
603
+ };
604
+ constructor() {
605
+ }
606
+ /**
607
+ * Get the video API for inclusion in the viji object
608
+ */
609
+ getVideoAPI() {
610
+ return {
611
+ isConnected: this.videoState.isConnected,
612
+ currentFrame: this.videoState.currentFrame,
613
+ frameWidth: this.videoState.frameWidth,
614
+ frameHeight: this.videoState.frameHeight,
615
+ frameRate: this.videoState.frameRate,
616
+ getFrameData: () => this.videoState.frameData,
617
+ faces: this.cvResults.faces,
618
+ hands: this.cvResults.hands
619
+ };
620
+ }
621
+ /**
622
+ * ✅ CORRECT: Receive OffscreenCanvas transfer from host
623
+ */
624
+ handleCanvasSetup(data) {
625
+ try {
626
+ this.disconnectVideo();
627
+ this.offscreenCanvas = data.offscreenCanvas;
628
+ this.ctx = this.offscreenCanvas.getContext("2d", {
629
+ willReadFrequently: true
630
+ // Optimize for frequent getImageData calls
631
+ });
632
+ if (!this.ctx) {
633
+ throw new Error("Failed to get 2D context from transferred OffscreenCanvas");
634
+ }
635
+ try {
636
+ this.gl = this.offscreenCanvas.getContext("webgl2") || this.offscreenCanvas.getContext("webgl");
637
+ } catch (e) {
638
+ this.debugLog("WebGL not available, using 2D context only");
639
+ }
640
+ this.videoState.isConnected = true;
641
+ this.videoState.currentFrame = this.offscreenCanvas;
642
+ this.videoState.frameWidth = data.width;
643
+ this.videoState.frameHeight = data.height;
644
+ this.frameCount = 0;
645
+ this.hasLoggedFirstFrame = false;
646
+ this.debugLog("✅ OffscreenCanvas received and setup completed (worker-side)", {
647
+ width: data.width,
648
+ height: data.height,
649
+ hasWebGL: !!this.gl,
650
+ targetFrameRate: this.targetFrameRate
651
+ });
652
+ this.debugLog("🎬 CORRECT OffscreenCanvas approach - Worker has full GPU access!");
653
+ } catch (error) {
654
+ console.error("Failed to setup OffscreenCanvas in worker:", error);
655
+ this.disconnectVideo();
656
+ }
657
+ }
658
+ /**
659
+ * ✅ CORRECT: Receive ImageBitmap frame and draw to worker's OffscreenCanvas
660
+ */
661
+ handleFrameUpdate(data) {
662
+ if (!this.offscreenCanvas || !this.ctx) {
663
+ console.warn("🔴 Received frame but OffscreenCanvas not setup");
664
+ return;
665
+ }
666
+ try {
667
+ if (this.frameCount % 150 === 0) {
668
+ this.debugLog("✅ Worker received ImageBitmap frame:", {
669
+ bitmapSize: `${data.imageBitmap.width}x${data.imageBitmap.height}`,
670
+ canvasSize: `${this.offscreenCanvas.width}x${this.offscreenCanvas.height}`,
671
+ frameCount: this.frameCount,
672
+ timestamp: data.timestamp
673
+ });
674
+ }
675
+ this.ctx.drawImage(data.imageBitmap, 0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
676
+ this.processCurrentFrame(data.timestamp);
677
+ data.imageBitmap.close();
678
+ this.frameCount++;
679
+ } catch (error) {
680
+ console.error("🔴 Error processing video frame (worker-side):", error);
681
+ }
682
+ }
683
+ /**
684
+ * Process current frame (called when new frame is drawn)
685
+ */
686
+ processCurrentFrame(timestamp) {
687
+ if (!this.offscreenCanvas || !this.ctx) {
688
+ return;
689
+ }
690
+ try {
691
+ this.videoState.frameData = this.ctx.getImageData(
692
+ 0,
693
+ 0,
694
+ this.offscreenCanvas.width,
695
+ this.offscreenCanvas.height
696
+ );
697
+ const deltaTime = timestamp - this.lastFrameTime;
698
+ this.videoState.frameRate = deltaTime > 0 ? 1e3 / deltaTime : 0;
699
+ if (!this.hasLoggedFirstFrame) {
700
+ this.debugLog(`🎯 Worker-side OffscreenCanvas processing active: ${this.videoState.frameRate.toFixed(1)} FPS (${this.offscreenCanvas.width}x${this.offscreenCanvas.height})`);
701
+ this.debugLog("✅ Full GPU access available for custom effects and CV analysis");
702
+ this.hasLoggedFirstFrame = true;
703
+ }
704
+ this.performCVAnalysis();
705
+ this.lastFrameTime = timestamp;
706
+ } catch (error) {
707
+ console.error("Error processing video frame (worker-side):", error);
708
+ }
709
+ }
710
+ /**
711
+ * Handle video configuration updates (including disconnection and resize)
712
+ */
713
+ handleVideoConfigUpdate(data) {
714
+ try {
715
+ if (data.disconnect) {
716
+ this.disconnectVideo();
717
+ return;
718
+ }
719
+ if (data.width && data.height && this.offscreenCanvas) {
720
+ this.resizeCanvas(data.width, data.height);
721
+ }
722
+ if (data.targetFrameRate) {
723
+ this.updateProcessingConfig(data.targetFrameRate);
724
+ }
725
+ if (data.cvConfig) {
726
+ this.updateCVConfig(data.cvConfig);
727
+ }
728
+ } catch (error) {
729
+ console.error("Error handling video config update:", error);
730
+ }
731
+ }
732
+ /**
733
+ * Resize the OffscreenCanvas (when video dimensions change)
734
+ */
735
+ resizeCanvas(width, height) {
736
+ if (!this.offscreenCanvas) return;
737
+ try {
738
+ this.offscreenCanvas.width = width;
739
+ this.offscreenCanvas.height = height;
740
+ this.videoState.frameWidth = width;
741
+ this.videoState.frameHeight = height;
742
+ if (this.gl) {
743
+ this.gl.viewport(0, 0, width, height);
744
+ }
745
+ this.debugLog(`📐 OffscreenCanvas resized to ${width}x${height} (worker-side)`);
746
+ } catch (error) {
747
+ console.error("Error resizing OffscreenCanvas:", error);
748
+ }
749
+ }
750
+ /**
751
+ * Disconnect video and clean up resources
752
+ */
753
+ disconnectVideo() {
754
+ if (this.offscreenCanvas && this.ctx) {
755
+ this.ctx.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
756
+ this.debugLog("🧹 Cleared OffscreenCanvas on disconnect");
757
+ }
758
+ this.offscreenCanvas = null;
759
+ this.ctx = null;
760
+ this.gl = null;
761
+ this.videoState.isConnected = false;
762
+ this.videoState.currentFrame = null;
763
+ this.videoState.frameWidth = 0;
764
+ this.videoState.frameHeight = 0;
765
+ this.videoState.frameRate = 0;
766
+ this.videoState.frameData = null;
767
+ this.resetCVResults();
768
+ this.hasLoggedFirstFrame = false;
769
+ this.frameCount = 0;
770
+ this.debugLog("Video disconnected (worker-side)");
771
+ }
772
+ /**
773
+ * Update video processing configuration
774
+ */
775
+ updateProcessingConfig(targetFrameRate) {
776
+ this.targetFrameRate = Math.max(1, Math.min(60, targetFrameRate));
777
+ this.frameInterval = 1e3 / this.targetFrameRate;
778
+ this.debugLog(`Video processing frame rate updated to ${this.targetFrameRate} FPS (worker-side)`);
779
+ }
780
+ /**
781
+ * Phase 11 preparation - Update CV configuration
782
+ */
783
+ updateCVConfig(cvConfig) {
784
+ this.cvFeatures = {
785
+ faceDetection: cvConfig.faceDetection || false,
786
+ handTracking: cvConfig.handTracking || false,
787
+ bodySegmentation: cvConfig.bodySegmentation || false
788
+ };
789
+ this.debugLog("CV configuration updated (Phase 11 preparation, worker-side):", this.cvFeatures);
790
+ }
791
+ /**
792
+ * Phase 11 preparation - Perform computer vision analysis
793
+ */
794
+ performCVAnalysis() {
795
+ if (this.cvFeatures.faceDetection) ;
796
+ if (this.cvFeatures.handTracking) ;
797
+ if (this.cvFeatures.bodySegmentation) ;
798
+ }
799
+ /**
800
+ * Reset CV results
801
+ */
802
+ resetCVResults() {
803
+ this.cvResults = {
804
+ faces: [],
805
+ hands: [],
806
+ bodySegmentation: null
807
+ };
808
+ }
809
+ /**
810
+ * Reset all video state (called when loading new scene)
811
+ */
812
+ resetVideoState() {
813
+ this.disconnectVideo();
814
+ this.resetCVResults();
815
+ }
816
+ /**
817
+ * Get current processing configuration
818
+ */
819
+ getProcessingConfig() {
820
+ return {
821
+ targetFrameRate: this.targetFrameRate,
822
+ frameInterval: this.frameInterval,
823
+ frameCount: this.frameCount
824
+ };
825
+ }
826
+ /**
827
+ * Get WebGL context for advanced effects (if available)
828
+ */
829
+ getWebGLContext() {
830
+ return this.gl;
831
+ }
832
+ /**
833
+ * ✅ WORKER API: Artists can access the OffscreenCanvas directly for custom effects
834
+ */
835
+ getCanvasForArtistEffects() {
836
+ return this.offscreenCanvas;
837
+ }
838
+ }
839
+ class VijiWorkerRuntime {
840
+ canvas = null;
841
+ ctx = null;
842
+ gl = null;
843
+ isRunning = false;
844
+ frameCount = 0;
845
+ lastTime = 0;
846
+ startTime = 0;
847
+ frameRateMode = "full";
848
+ skipNextFrame = false;
849
+ screenRefreshRate = 60;
850
+ // Will be detected
851
+ // Debug logging control
852
+ debugMode = false;
853
+ /**
854
+ * Enable or disable debug logging
855
+ */
856
+ setDebugMode(enabled) {
857
+ this.debugMode = enabled;
858
+ if (this.videoSystem) this.videoSystem.setDebugMode(enabled);
859
+ if (this.parameterSystem && "setDebugMode" in this.parameterSystem) {
860
+ this.parameterSystem.setDebugMode(enabled);
861
+ }
862
+ if (this.interactionSystem && "setDebugMode" in this.interactionSystem) {
863
+ this.interactionSystem.setDebugMode(enabled);
864
+ }
865
+ }
866
+ /**
867
+ * Debug logging helper
868
+ */
869
+ debugLog(message, ...args) {
870
+ if (this.debugMode) {
871
+ console.log(message, ...args);
872
+ }
873
+ }
874
+ // Effective refresh rate tracking
875
+ effectiveFrameTimes = [];
876
+ lastEffectiveRateReport = 0;
877
+ effectiveRateReportInterval = 1e3;
878
+ // Report every 1 second
879
+ // Parameter system
880
+ parameterSystem;
881
+ // Interaction system (Phase 7)
882
+ interactionSystem;
883
+ // Video system (Phase 10) - worker-side video processing
884
+ videoSystem;
885
+ // Audio state (Phase 5) - receives analysis results from host
886
+ audioState = {
887
+ isConnected: false,
888
+ volume: { rms: 0, peak: 0 },
889
+ bands: {
890
+ bass: 0,
891
+ mid: 0,
892
+ treble: 0,
893
+ subBass: 0,
894
+ lowMid: 0,
895
+ highMid: 0,
896
+ presence: 0,
897
+ brilliance: 0
898
+ },
899
+ frequencyData: new Uint8Array(0)
900
+ };
901
+ // Video state is now managed by the worker-side VideoSystem
902
+ // Artist API object
903
+ viji = {
904
+ // Canvas (will be set during init)
905
+ canvas: null,
906
+ ctx: null,
907
+ gl: null,
908
+ width: 0,
909
+ height: 0,
910
+ pixelRatio: 1,
911
+ // Timing
912
+ time: 0,
913
+ deltaTime: 0,
914
+ frameCount: 0,
915
+ fps: 60,
916
+ // Audio API (Phase 5) - will be set in constructor
917
+ audio: {},
918
+ video: {
919
+ isConnected: false,
920
+ currentFrame: null,
921
+ frameWidth: 0,
922
+ frameHeight: 0,
923
+ frameRate: 0,
924
+ getFrameData: () => null,
925
+ faces: [],
926
+ hands: []
927
+ },
928
+ // Interaction APIs will be added during construction
929
+ mouse: {},
930
+ keyboard: {},
931
+ touches: {},
932
+ // Parameter helper functions (return parameter objects) - delegate to parameter system
933
+ slider: (defaultValue, config) => {
934
+ return this.parameterSystem.createSliderParameter(defaultValue, config);
935
+ },
936
+ color: (defaultValue, config) => {
937
+ return this.parameterSystem.createColorParameter(defaultValue, config);
938
+ },
939
+ toggle: (defaultValue, config) => {
940
+ return this.parameterSystem.createToggleParameter(defaultValue, config);
941
+ },
942
+ select: (defaultValue, config) => {
943
+ return this.parameterSystem.createSelectParameter(defaultValue, config);
944
+ },
945
+ text: (defaultValue, config) => {
946
+ return this.parameterSystem.createTextParameter(defaultValue, config);
947
+ },
948
+ number: (defaultValue, config) => {
949
+ return this.parameterSystem.createNumberParameter(defaultValue, config);
950
+ },
951
+ // Context selection
952
+ useContext: (type) => {
953
+ if (type === "2d") {
954
+ if (!this.ctx && this.canvas) {
955
+ this.ctx = this.canvas.getContext("2d");
956
+ this.viji.ctx = this.ctx;
957
+ }
958
+ return this.ctx;
959
+ } else if (type === "webgl") {
960
+ if (!this.gl && this.canvas) {
961
+ this.gl = this.canvas.getContext("webgl2") || this.canvas.getContext("webgl");
962
+ this.viji.gl = this.gl;
963
+ if (this.gl) {
964
+ this.gl.viewport(0, 0, this.viji.width, this.viji.height);
965
+ }
966
+ }
967
+ return this.gl;
968
+ }
969
+ return null;
970
+ }
971
+ };
972
+ constructor() {
973
+ this.parameterSystem = new ParameterSystem((type, data) => {
974
+ this.postMessage(type, data);
975
+ });
976
+ this.interactionSystem = new InteractionSystem();
977
+ this.videoSystem = new VideoSystem();
978
+ Object.assign(this.viji, this.interactionSystem.getInteractionAPIs());
979
+ Object.assign(this.viji.video, this.videoSystem.getVideoAPI());
980
+ this.viji.audio = {
981
+ ...this.audioState,
982
+ getFrequencyData: () => this.audioState.frequencyData
983
+ };
984
+ this.setupMessageHandling();
985
+ }
986
+ // Reset parameter state (called when loading new scene)
987
+ resetParameterState() {
988
+ this.parameterSystem.resetParameterState();
989
+ this.interactionSystem.resetInteractionState();
990
+ this.audioState = {
991
+ isConnected: false,
992
+ volume: { rms: 0, peak: 0 },
993
+ bands: {
994
+ bass: 0,
995
+ mid: 0,
996
+ treble: 0,
997
+ subBass: 0,
998
+ lowMid: 0,
999
+ highMid: 0,
1000
+ presence: 0,
1001
+ brilliance: 0
1002
+ },
1003
+ frequencyData: new Uint8Array(0)
1004
+ };
1005
+ this.viji.audio = {
1006
+ ...this.audioState,
1007
+ getFrequencyData: () => this.audioState.frequencyData
1008
+ };
1009
+ this.videoSystem.resetVideoState();
1010
+ Object.assign(this.viji.video, this.videoSystem.getVideoAPI());
1011
+ }
1012
+ // Send all parameters (from helper functions) to host
1013
+ sendAllParametersToHost() {
1014
+ this.parameterSystem.sendAllParametersToHost();
1015
+ }
1016
+ setupMessageHandling() {
1017
+ self.onmessage = (event) => {
1018
+ const message = event.data;
1019
+ switch (message.type) {
1020
+ case "init":
1021
+ this.handleInit(message);
1022
+ break;
1023
+ case "frame-rate-update":
1024
+ this.handleFrameRateUpdate(message);
1025
+ break;
1026
+ case "refresh-rate-update":
1027
+ this.handleRefreshRateUpdate(message);
1028
+ break;
1029
+ case "resolution-update":
1030
+ this.handleResolutionUpdate(message);
1031
+ break;
1032
+ case "set-scene-code":
1033
+ this.handleSetSceneCode(message);
1034
+ break;
1035
+ case "debug-mode":
1036
+ this.setDebugMode(message.data.enabled);
1037
+ break;
1038
+ case "parameter-update":
1039
+ this.handleParameterUpdate(message);
1040
+ break;
1041
+ case "parameter-batch-update":
1042
+ this.handleParameterBatchUpdate(message);
1043
+ break;
1044
+ case "stream-update":
1045
+ this.handleStreamUpdate(message);
1046
+ break;
1047
+ case "audio-analysis-update":
1048
+ this.handleAudioAnalysisUpdate(message);
1049
+ break;
1050
+ case "video-canvas-setup":
1051
+ this.handleVideoCanvasSetup(message);
1052
+ break;
1053
+ case "video-frame-update":
1054
+ this.handleVideoFrameUpdate(message);
1055
+ break;
1056
+ case "video-config-update":
1057
+ this.handleVideoConfigUpdate(message);
1058
+ break;
1059
+ case "mouse-update":
1060
+ this.handleMouseUpdate(message);
1061
+ break;
1062
+ case "keyboard-update":
1063
+ this.handleKeyboardUpdate(message);
1064
+ break;
1065
+ case "touch-update":
1066
+ this.handleTouchUpdate(message);
1067
+ break;
1068
+ case "performance-update":
1069
+ this.handlePerformanceUpdate(message);
1070
+ break;
1071
+ }
1072
+ };
1073
+ }
1074
+ handleInit(message) {
1075
+ try {
1076
+ this.canvas = message.data.canvas;
1077
+ this.viji.canvas = this.canvas;
1078
+ this.viji.width = this.canvas.width;
1079
+ this.viji.height = this.canvas.height;
1080
+ this.startRenderLoop();
1081
+ this.postMessage("ready", {
1082
+ id: message.id,
1083
+ canvasSize: { width: this.canvas.width, height: this.canvas.height }
1084
+ });
1085
+ } catch (error) {
1086
+ this.postMessage("error", {
1087
+ id: message.id,
1088
+ message: error.message,
1089
+ code: "INIT_ERROR"
1090
+ });
1091
+ }
1092
+ }
1093
+ handleFrameRateUpdate(message) {
1094
+ if (message.data && message.data.mode) {
1095
+ this.frameRateMode = message.data.mode;
1096
+ this.debugLog("Frame rate mode updated to:", message.data.mode);
1097
+ }
1098
+ }
1099
+ handleRefreshRateUpdate(message) {
1100
+ if (message.data && message.data.screenRefreshRate) {
1101
+ this.screenRefreshRate = message.data.screenRefreshRate;
1102
+ this.debugLog("Screen refresh rate updated to:", message.data.screenRefreshRate + "Hz");
1103
+ }
1104
+ }
1105
+ trackEffectiveFrameTime(currentTime) {
1106
+ this.effectiveFrameTimes.push(currentTime);
1107
+ if (this.effectiveFrameTimes.length > 60) {
1108
+ this.effectiveFrameTimes.shift();
1109
+ }
1110
+ }
1111
+ reportEffectiveRefreshRate(currentTime) {
1112
+ if (currentTime - this.lastEffectiveRateReport >= this.effectiveRateReportInterval) {
1113
+ if (this.effectiveFrameTimes.length >= 2) {
1114
+ const totalTime = this.effectiveFrameTimes[this.effectiveFrameTimes.length - 1] - this.effectiveFrameTimes[0];
1115
+ const frameCount = this.effectiveFrameTimes.length - 1;
1116
+ const effectiveRefreshRate = Math.round(frameCount / totalTime * 1e3);
1117
+ this.postMessage("performance-update", {
1118
+ effectiveRefreshRate,
1119
+ frameRateMode: this.frameRateMode,
1120
+ screenRefreshRate: this.screenRefreshRate,
1121
+ parameterCount: this.parameterSystem.getParameterCount()
1122
+ });
1123
+ }
1124
+ this.lastEffectiveRateReport = currentTime;
1125
+ }
1126
+ }
1127
+ handleResolutionUpdate(message) {
1128
+ if (message.data) {
1129
+ if (this.canvas) {
1130
+ this.canvas.width = Math.round(message.data.effectiveWidth);
1131
+ this.canvas.height = Math.round(message.data.effectiveHeight);
1132
+ }
1133
+ this.viji.width = Math.round(message.data.effectiveWidth);
1134
+ this.viji.height = Math.round(message.data.effectiveHeight);
1135
+ if (this.gl) {
1136
+ this.gl.viewport(0, 0, this.viji.width, this.viji.height);
1137
+ }
1138
+ this.debugLog("Canvas resolution updated to:", this.viji.width + "x" + this.viji.height);
1139
+ }
1140
+ }
1141
+ handleParameterUpdate(message) {
1142
+ if (message.data && message.data.name !== void 0 && message.data.value !== void 0) {
1143
+ this.parameterSystem.updateParameterValue(message.data.name, message.data.value);
1144
+ }
1145
+ }
1146
+ handleParameterBatchUpdate(message) {
1147
+ if (message.data && message.data.updates) {
1148
+ for (const update of message.data.updates) {
1149
+ this.parameterSystem.updateParameterValue(update.name, update.value);
1150
+ }
1151
+ this.parameterSystem.markInitialValuesSynced();
1152
+ this.debugLog("Parameter system initialized successfully");
1153
+ }
1154
+ }
1155
+ handleStreamUpdate(message) {
1156
+ this.debugLog("Stream update:", message.data);
1157
+ }
1158
+ handleAudioAnalysisUpdate(message) {
1159
+ this.audioState = {
1160
+ isConnected: message.data.isConnected,
1161
+ volume: message.data.volume,
1162
+ bands: message.data.bands,
1163
+ frequencyData: message.data.frequencyData
1164
+ };
1165
+ this.viji.audio = {
1166
+ ...this.audioState,
1167
+ getFrequencyData: () => this.audioState.frequencyData
1168
+ };
1169
+ }
1170
+ handleVideoCanvasSetup(message) {
1171
+ this.videoSystem.handleCanvasSetup({
1172
+ offscreenCanvas: message.data.offscreenCanvas,
1173
+ width: message.data.width,
1174
+ height: message.data.height,
1175
+ timestamp: message.data.timestamp
1176
+ });
1177
+ Object.assign(this.viji.video, this.videoSystem.getVideoAPI());
1178
+ }
1179
+ handleVideoFrameUpdate(message) {
1180
+ this.videoSystem.handleFrameUpdate({
1181
+ imageBitmap: message.data.imageBitmap,
1182
+ timestamp: message.data.timestamp
1183
+ });
1184
+ Object.assign(this.viji.video, this.videoSystem.getVideoAPI());
1185
+ }
1186
+ handleVideoConfigUpdate(message) {
1187
+ this.videoSystem.handleVideoConfigUpdate({
1188
+ ...message.data.targetFrameRate && { targetFrameRate: message.data.targetFrameRate },
1189
+ ...message.data.cvConfig && { cvConfig: message.data.cvConfig },
1190
+ ...message.data.width && { width: message.data.width },
1191
+ ...message.data.height && { height: message.data.height },
1192
+ ...message.data.disconnect && { disconnect: message.data.disconnect },
1193
+ timestamp: message.data.timestamp
1194
+ });
1195
+ }
1196
+ handlePerformanceUpdate(message) {
1197
+ this.debugLog("Performance update:", message.data);
1198
+ }
1199
+ handleSetSceneCode(message) {
1200
+ if (message.data && message.data.sceneCode) {
1201
+ self.setSceneCode(message.data.sceneCode);
1202
+ }
1203
+ }
1204
+ startRenderLoop() {
1205
+ this.isRunning = true;
1206
+ this.startTime = performance.now();
1207
+ this.lastTime = this.startTime;
1208
+ this.renderFrame();
1209
+ }
1210
+ renderFrame() {
1211
+ if (!this.isRunning) return;
1212
+ const currentTime = performance.now();
1213
+ this.interactionSystem.frameStart();
1214
+ this.viji.fps = this.frameRateMode === "full" ? this.screenRefreshRate : this.screenRefreshRate / 2;
1215
+ let shouldRender = true;
1216
+ if (this.frameRateMode === "half") {
1217
+ shouldRender = !this.skipNextFrame;
1218
+ this.skipNextFrame = !this.skipNextFrame;
1219
+ }
1220
+ if (shouldRender) {
1221
+ this.viji.deltaTime = (currentTime - this.lastTime) / 1e3;
1222
+ this.viji.time = (currentTime - this.startTime) / 1e3;
1223
+ this.viji.frameCount = ++this.frameCount;
1224
+ this.trackEffectiveFrameTime(currentTime);
1225
+ this.lastTime = currentTime;
1226
+ try {
1227
+ const renderFunction2 = self.renderFunction;
1228
+ if (renderFunction2 && typeof renderFunction2 === "function") {
1229
+ renderFunction2(this.viji);
1230
+ }
1231
+ } catch (error) {
1232
+ console.error("Render error:", error);
1233
+ this.postMessage("error", {
1234
+ message: error.message,
1235
+ code: "RENDER_ERROR",
1236
+ stack: error.stack
1237
+ });
1238
+ }
1239
+ }
1240
+ this.reportEffectiveRefreshRate(currentTime);
1241
+ requestAnimationFrame(() => this.renderFrame());
1242
+ }
1243
+ postMessage(type, data) {
1244
+ self.postMessage({
1245
+ type,
1246
+ id: data?.id || `${type}_${Date.now()}`,
1247
+ timestamp: Date.now(),
1248
+ data
1249
+ });
1250
+ }
1251
+ // Phase 7: Interaction Message Handlers (delegated to InteractionSystem)
1252
+ handleMouseUpdate(message) {
1253
+ this.interactionSystem.handleMouseUpdate(message.data);
1254
+ }
1255
+ handleKeyboardUpdate(message) {
1256
+ this.interactionSystem.handleKeyboardUpdate(message.data);
1257
+ }
1258
+ handleTouchUpdate(message) {
1259
+ this.interactionSystem.handleTouchUpdate(message.data);
1260
+ }
1261
+ }
1262
+ const runtime = new VijiWorkerRuntime();
1263
+ let renderFunction = null;
1264
+ function setSceneCode(sceneCode) {
1265
+ try {
1266
+ runtime.resetParameterState();
1267
+ const functionBody = sceneCode + '\nif (typeof render === "function") {\n return render;\n}\nthrow new Error("Scene code must define a render function");';
1268
+ const sceneFunction = new Function("viji", functionBody);
1269
+ renderFunction = sceneFunction(runtime.viji);
1270
+ self.renderFunction = renderFunction;
1271
+ runtime.sendAllParametersToHost();
1272
+ } catch (error) {
1273
+ console.error("Failed to load scene code:", error);
1274
+ self.postMessage({
1275
+ type: "error",
1276
+ id: `scene_error_${Date.now()}`,
1277
+ timestamp: Date.now(),
1278
+ data: {
1279
+ message: `Scene code error: ${error.message}`,
1280
+ code: "SCENE_CODE_ERROR"
1281
+ }
1282
+ });
1283
+ }
1284
+ }
1285
+ self.setSceneCode = setSceneCode;
1286
+ })();
1287
+ //# sourceMappingURL=viji.worker-Cozsmke0.js.map