vizcore 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/frontend/index.html +24 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +447 -57
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +391 -10
- data/frontend/src/renderer/layer-manager.js +472 -71
- data/frontend/src/runtime-control-preset.js +44 -0
- data/frontend/src/scene-patches.js +159 -0
- data/frontend/src/shader-error-overlay.js +1 -0
- data/frontend/src/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +13 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/text-renderer.js +13 -0
- data/frontend/src/websocket-client.js +6 -0
- data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
- data/lib/vizcore/analysis/feature_recorder.rb +117 -7
- data/lib/vizcore/analysis/feature_replay.rb +48 -9
- data/lib/vizcore/analysis/pipeline.rb +258 -9
- data/lib/vizcore/analysis/tap_tempo.rb +17 -2
- data/lib/vizcore/audio/calibration.rb +156 -0
- data/lib/vizcore/audio/file_input.rb +28 -0
- data/lib/vizcore/audio/input_manager.rb +36 -1
- data/lib/vizcore/audio/midi_input.rb +5 -0
- data/lib/vizcore/audio/ring_buffer.rb +22 -0
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/dsl_reference.rb +64 -8
- data/lib/vizcore/cli/plugin_checker.rb +93 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
- data/lib/vizcore/cli/scene_inspector.rb +35 -1
- data/lib/vizcore/cli/scene_validator.rb +487 -39
- data/lib/vizcore/cli/shader_template.rb +7 -2
- data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
- data/lib/vizcore/cli.rb +268 -15
- data/lib/vizcore/config.rb +40 -3
- data/lib/vizcore/control_preset.rb +29 -0
- data/lib/vizcore/deep_copy.rb +21 -0
- data/lib/vizcore/dsl/color_helpers.rb +155 -0
- data/lib/vizcore/dsl/engine.rb +219 -23
- data/lib/vizcore/dsl/layer_builder.rb +278 -15
- data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
- data/lib/vizcore/dsl/layout_helpers.rb +290 -0
- data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
- data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
- data/lib/vizcore/dsl/reaction_builder.rb +1 -0
- data/lib/vizcore/dsl/scene_builder.rb +83 -13
- data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
- data/lib/vizcore/dsl/style_builder.rb +3 -0
- data/lib/vizcore/dsl/timeline_builder.rb +91 -8
- data/lib/vizcore/dsl/transition_controller.rb +157 -18
- data/lib/vizcore/dsl.rb +2 -0
- data/lib/vizcore/layer_catalog.rb +1 -0
- data/lib/vizcore/plugin_asset_policy.rb +55 -0
- data/lib/vizcore/project_manifest.rb +12 -2
- data/lib/vizcore/renderer/render_sequence.rb +104 -13
- data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +469 -23
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +676 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +39 -16
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +33 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +55 -4
- metadata +18 -3
data/frontend/src/main.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { BAND_KEYS, DEFAULT_FFT_BINS, buildAudioInspectorState, formatMeterValue } from "./audio-inspector.js";
|
|
2
2
|
import {
|
|
3
3
|
createLiveControlState,
|
|
4
|
+
isLiveControlEnabled,
|
|
4
5
|
isTapTempoShortcut,
|
|
5
6
|
keyboardActionForKey,
|
|
6
7
|
liveControlStatusText,
|
|
7
8
|
normalizeKeyboardMappings,
|
|
9
|
+
normalizeLiveControlPayload,
|
|
8
10
|
shortcutActionForKey,
|
|
9
11
|
shortcutSceneIndexForKey,
|
|
10
12
|
toggleLiveControl,
|
|
@@ -15,7 +17,10 @@ import {
|
|
|
15
17
|
recordConnectionStatus,
|
|
16
18
|
recordLatencyProbe,
|
|
17
19
|
recordRenderFrame,
|
|
20
|
+
recordRendererCapabilities,
|
|
21
|
+
recordRendererSafeMode,
|
|
18
22
|
recordShaderCompile,
|
|
23
|
+
recordWebSocketBackpressure,
|
|
19
24
|
recordSocketFrame,
|
|
20
25
|
} from "./performance-monitor.js";
|
|
21
26
|
import {
|
|
@@ -29,7 +34,7 @@ import {
|
|
|
29
34
|
upsertMidiLearnBinding,
|
|
30
35
|
} from "./midi-learn.js";
|
|
31
36
|
import { applyProjectorMode, resolveProjectorMode } from "./projector-mode.js";
|
|
32
|
-
import { Engine } from "./renderer/engine.js";
|
|
37
|
+
import { Engine, RENDERER_CAPABILITIES_EVENT, RENDERER_SAFE_MODE_EVENT } from "./renderer/engine.js";
|
|
33
38
|
import { SHADER_COMPILE_EVENT } from "./renderer/shader-manager.js";
|
|
34
39
|
import {
|
|
35
40
|
customShapeParamControlEntries,
|
|
@@ -58,6 +63,7 @@ import {
|
|
|
58
63
|
saveVisualSettingsPreset,
|
|
59
64
|
visualSettingFromUnit
|
|
60
65
|
} from "./visual-settings-preset.js";
|
|
66
|
+
import { applyScenePayload, resolveScenePayload } from "./scene-patches.js";
|
|
61
67
|
import { WebSocketClient } from "./websocket-client.js";
|
|
62
68
|
|
|
63
69
|
window.__vizcoreMainStarted = true;
|
|
@@ -67,6 +73,7 @@ const wsStatusElement = document.querySelector("#ws-status");
|
|
|
67
73
|
const sceneStatusElement = document.querySelector("#scene-status");
|
|
68
74
|
const transitionStatusElement = document.querySelector("#transition-status");
|
|
69
75
|
const frameStatusElement = document.querySelector("#frame-status");
|
|
76
|
+
const runtimeErrorStatusElement = document.querySelector("#runtime-error-status");
|
|
70
77
|
const bpmStatusElement = document.querySelector("#bpm-status");
|
|
71
78
|
const beatStatusElement = document.querySelector("#beat-status");
|
|
72
79
|
const blackoutButton = document.querySelector("#blackout-toggle");
|
|
@@ -87,6 +94,7 @@ const inspectorBandElements = Object.fromEntries(
|
|
|
87
94
|
);
|
|
88
95
|
const fftPreviewElement = document.querySelector("#fft-preview");
|
|
89
96
|
const audioSourceStatusElement = document.querySelector("#audio-source-status");
|
|
97
|
+
const audioHealthStatusElement = document.querySelector("#audio-health-status");
|
|
90
98
|
const audioTrackStatusElement = document.querySelector("#audio-track-status");
|
|
91
99
|
const audioPlaybackStatusElement = document.querySelector("#audio-playback-status");
|
|
92
100
|
const sceneSwitcherElement = document.querySelector("#scene-switcher");
|
|
@@ -133,8 +141,13 @@ let pendingSceneRequestedAt = 0;
|
|
|
133
141
|
let tapTempoKey = null;
|
|
134
142
|
let runtimeGlobalsReceived = false;
|
|
135
143
|
let runtimeControlPresetApplied = false;
|
|
144
|
+
let runtimeControlPresetSceneApplied = null;
|
|
145
|
+
let runtimeControlPresetSceneOverrides = {};
|
|
146
|
+
let runtimeControlPresetVisualBase = null;
|
|
147
|
+
let runtimeControlPresetMidiBase = null;
|
|
136
148
|
let controlPresetSaveUrl = null;
|
|
137
149
|
let shaderParamOverrides = {};
|
|
150
|
+
let scenePayload = null;
|
|
138
151
|
let shaderParamControlsSignature = "";
|
|
139
152
|
let shapeEditorOverrides = {};
|
|
140
153
|
let shapeEditorControlsSignature = "";
|
|
@@ -148,6 +161,7 @@ applyProjectorMode(document.body, projectorMode);
|
|
|
148
161
|
const engine = new Engine(canvas);
|
|
149
162
|
let rendererReady = false;
|
|
150
163
|
bindShaderCompileMetrics();
|
|
164
|
+
bindRendererMetrics();
|
|
151
165
|
try {
|
|
152
166
|
engine.init();
|
|
153
167
|
rendererReady = true;
|
|
@@ -183,10 +197,26 @@ startPerformanceMonitorLoop();
|
|
|
183
197
|
const websocketUrl = buildWebSocketUrl();
|
|
184
198
|
const client = new WebSocketClient(websocketUrl, {
|
|
185
199
|
onFrame: (frame) => {
|
|
200
|
+
const resolvedScene = resolveScenePayload({
|
|
201
|
+
incomingScene: frame?.scene,
|
|
202
|
+
currentScene: scenePayload,
|
|
203
|
+
frameVersion: frame?.scene_version,
|
|
204
|
+
});
|
|
205
|
+
if (resolvedScene) {
|
|
206
|
+
scenePayload = resolvedScene;
|
|
207
|
+
}
|
|
208
|
+
const normalizedFrame = {
|
|
209
|
+
...frame,
|
|
210
|
+
scene: scenePayload
|
|
211
|
+
};
|
|
212
|
+
|
|
186
213
|
updatePerformanceMonitor(recordSocketFrame(performanceMonitor, frame, Date.now()));
|
|
187
|
-
engine.setAudioFrame(
|
|
214
|
+
engine.setAudioFrame(normalizedFrame);
|
|
188
215
|
frameCount += 1;
|
|
189
|
-
|
|
216
|
+
const scene = scenePayload;
|
|
217
|
+
let sceneName = String(scene?.name || currentSceneName);
|
|
218
|
+
document.body.dataset.vizcoreFrameCount = String(frameCount);
|
|
219
|
+
document.body.dataset.vizcoreScene = sceneName;
|
|
190
220
|
const now = performance.now();
|
|
191
221
|
if (
|
|
192
222
|
pendingSceneName &&
|
|
@@ -199,18 +229,8 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
199
229
|
pendingSceneName = null;
|
|
200
230
|
pendingSceneRequestedAt = 0;
|
|
201
231
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (sceneChanged) {
|
|
205
|
-
shaderParamControlsSignature = "";
|
|
206
|
-
shapeEditorControlsSignature = "";
|
|
207
|
-
customShapeParamControlsSignature = "";
|
|
208
|
-
mappingTargetSelectorSignature = "";
|
|
209
|
-
}
|
|
210
|
-
updateShaderParamControls(frame?.scene?.layers);
|
|
211
|
-
updateShapeEditorControls(frame?.scene?.layers);
|
|
212
|
-
updateCustomShapeParamControls(frame?.scene?.layers);
|
|
213
|
-
updateMappingTargetSelector(frame?.scene?.layers);
|
|
232
|
+
applyRuntimeControlPresetForScene(sceneName);
|
|
233
|
+
updateSceneControls(scene);
|
|
214
234
|
const amplitude = Number(frame?.audio?.amplitude || 0).toFixed(4);
|
|
215
235
|
const bpm = Number(frame?.audio?.bpm || 0);
|
|
216
236
|
const beat = !!frame?.audio?.beat;
|
|
@@ -220,9 +240,6 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
220
240
|
}
|
|
221
241
|
const beatVisible = performance.now() < beatFlashUntil;
|
|
222
242
|
sceneStatusElement.textContent = `Scene: ${sceneName}`;
|
|
223
|
-
if (sceneChanged) {
|
|
224
|
-
renderSceneButtons();
|
|
225
|
-
}
|
|
226
243
|
frameStatusElement.textContent = `Amplitude: ${amplitude} | Frames: ${frameCount}`;
|
|
227
244
|
bpmStatusElement.textContent = `BPM: ${bpm > 0 ? bpm.toFixed(1) : "--"}`;
|
|
228
245
|
beatStatusElement.textContent = `Beat: ${beatVisible ? "ON" : "off"} | Count: ${beatCount}`;
|
|
@@ -236,6 +253,7 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
236
253
|
pendingSceneRequestedAt = 0;
|
|
237
254
|
currentSceneName = to;
|
|
238
255
|
sceneStatusElement.textContent = `Scene: ${to}`;
|
|
256
|
+
applyRuntimeControlPresetForScene(to, { force: true });
|
|
239
257
|
transitionStatusElement.textContent = `Transition: ${from} -> ${to}`;
|
|
240
258
|
renderSceneButtons();
|
|
241
259
|
},
|
|
@@ -243,17 +261,9 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
243
261
|
updateAvailableScenes(payload?.scenes);
|
|
244
262
|
const sceneName = payload?.scene?.name;
|
|
245
263
|
if (sceneName) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
shaderParamControlsSignature = "";
|
|
250
|
-
shapeEditorControlsSignature = "";
|
|
251
|
-
customShapeParamControlsSignature = "";
|
|
252
|
-
mappingTargetSelectorSignature = "";
|
|
253
|
-
updateShaderParamControls(payload?.scene?.layers);
|
|
254
|
-
updateShapeEditorControls(payload?.scene?.layers);
|
|
255
|
-
updateCustomShapeParamControls(payload?.scene?.layers);
|
|
256
|
-
updateMappingTargetSelector(payload?.scene?.layers);
|
|
264
|
+
scenePayload = applyScenePayload(payload.scene);
|
|
265
|
+
applyRuntimeControlPresetForScene(sceneName, { force: true });
|
|
266
|
+
updateSceneControls(scenePayload, { forceReset: true });
|
|
257
267
|
}
|
|
258
268
|
if (Object.prototype.hasOwnProperty.call(payload || {}, "tap_tempo_key")) {
|
|
259
269
|
updateTapTempoKey(payload?.tap_tempo_key);
|
|
@@ -267,7 +277,6 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
267
277
|
}
|
|
268
278
|
if (Object.prototype.hasOwnProperty.call(payload || {}, "live_controls")) {
|
|
269
279
|
applyLiveControls({
|
|
270
|
-
...liveControls,
|
|
271
280
|
...normalizeLiveControls(payload?.live_controls),
|
|
272
281
|
});
|
|
273
282
|
}
|
|
@@ -275,12 +284,18 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
275
284
|
onLatencyProbe: (payload) => {
|
|
276
285
|
updatePerformanceMonitor(recordLatencyProbe(performanceMonitor, payload, Date.now()));
|
|
277
286
|
},
|
|
287
|
+
onRuntimeError: (payload) => {
|
|
288
|
+
updateRuntimeErrorStatus(payload);
|
|
289
|
+
},
|
|
278
290
|
onStatus: (status) => {
|
|
279
291
|
updatePerformanceMonitor(recordConnectionStatus(performanceMonitor, status));
|
|
280
292
|
if (status === "connected") {
|
|
281
293
|
lastConnectedAt = new Date();
|
|
282
294
|
startLatencyProbeLoop();
|
|
283
295
|
syncAudioTransportToServer({ force: true });
|
|
296
|
+
if (runtimeErrorStatusElement) {
|
|
297
|
+
runtimeErrorStatusElement.textContent = "Runtime: ok";
|
|
298
|
+
}
|
|
284
299
|
} else {
|
|
285
300
|
stopLatencyProbeLoop();
|
|
286
301
|
pendingSceneName = null;
|
|
@@ -288,9 +303,12 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
288
303
|
currentSceneName = "unknown";
|
|
289
304
|
sceneStatusElement.textContent = "Scene: unknown";
|
|
290
305
|
renderSceneButtons();
|
|
306
|
+
if (runtimeErrorStatusElement) {
|
|
307
|
+
runtimeErrorStatusElement.textContent = "Runtime: disconnected";
|
|
308
|
+
}
|
|
291
309
|
}
|
|
292
|
-
|
|
293
|
-
|
|
310
|
+
const connectedAt = lastConnectedAt ? ` | Last connected: ${formatClock(lastConnectedAt)}` : "";
|
|
311
|
+
wsStatusElement.textContent = `WebSocket: ${status} (${websocketUrl})${connectedAt}`;
|
|
294
312
|
}
|
|
295
313
|
});
|
|
296
314
|
|
|
@@ -333,9 +351,11 @@ function applyRuntime(runtime) {
|
|
|
333
351
|
applyRuntimeGlobals(runtime?.globals);
|
|
334
352
|
}
|
|
335
353
|
applyRuntimeControlPreset(runtime?.control_preset);
|
|
354
|
+
updatePerformanceMonitor(recordWebSocketBackpressure(performanceMonitor, runtime?.websocket_backpressure));
|
|
336
355
|
|
|
337
356
|
const fileName = runtime?.audio_file_name;
|
|
338
357
|
const fileUrl = runtime?.audio_file_url;
|
|
358
|
+
applyRuntimeAudioInputHealth(runtime?.input);
|
|
339
359
|
if (!fileUrl) {
|
|
340
360
|
engine.setMediaElement(null);
|
|
341
361
|
audioTrackStatusElement.textContent = "Track: none";
|
|
@@ -348,6 +368,33 @@ function applyRuntime(runtime) {
|
|
|
348
368
|
setupAudioPlayback(fileUrl);
|
|
349
369
|
}
|
|
350
370
|
|
|
371
|
+
function applyRuntimeAudioInputHealth(input) {
|
|
372
|
+
if (!audioHealthStatusElement) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const source = String(input?.source || "unknown");
|
|
377
|
+
const sampleRate = Number(input?.sample_rate);
|
|
378
|
+
const requestedSampleRate = Number(input?.requested_sample_rate);
|
|
379
|
+
const frameSize = Number(input?.frame_size);
|
|
380
|
+
const ringBuffer = input?.ring_buffer || {};
|
|
381
|
+
const overrun = Number(ringBuffer?.overrun_count || 0);
|
|
382
|
+
const underrun = Number(ringBuffer?.underrun_count || 0);
|
|
383
|
+
const sampleRateText = Number.isFinite(sampleRate) && sampleRate > 0
|
|
384
|
+
? `${Math.round(sampleRate)}Hz`
|
|
385
|
+
: "--";
|
|
386
|
+
const requestedSampleRateText = Number.isFinite(requestedSampleRate) &&
|
|
387
|
+
requestedSampleRate > 0 &&
|
|
388
|
+
requestedSampleRate !== sampleRate
|
|
389
|
+
? ` (${Math.round(requestedSampleRate)}Hz requested)`
|
|
390
|
+
: "";
|
|
391
|
+
const frameText = Number.isFinite(frameSize) && frameSize > 0
|
|
392
|
+
? ` | Frame ${Math.round(frameSize)}`
|
|
393
|
+
: "";
|
|
394
|
+
|
|
395
|
+
audioHealthStatusElement.textContent = `Input: ${source} | Sample ${sampleRateText}${requestedSampleRateText} | ${frameText} | Overrun ${Math.max(0, overrun)} | Underrun ${Math.max(0, underrun)}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
351
398
|
function applyRuntimeGlobals(globals) {
|
|
352
399
|
engine.setRuntimeGlobals(globals);
|
|
353
400
|
}
|
|
@@ -361,28 +408,205 @@ function updateControlPresetPersistence(runtime) {
|
|
|
361
408
|
}
|
|
362
409
|
|
|
363
410
|
function applyRuntimeControlPreset(value) {
|
|
364
|
-
if (runtimeControlPresetApplied) {
|
|
411
|
+
if (!value || runtimeControlPresetApplied) {
|
|
365
412
|
return;
|
|
366
413
|
}
|
|
367
414
|
|
|
368
415
|
const preset = normalizeRuntimeControlPreset(value);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
416
|
+
const hasVisual = Boolean(preset.visualSettings);
|
|
417
|
+
const hasMidi = Boolean(preset.midiLearnBindings);
|
|
418
|
+
const hasSceneOverrides = preset.sceneOverrides && Object.keys(preset.sceneOverrides).length > 0;
|
|
419
|
+
if (!hasVisual && !hasMidi && !hasSceneOverrides) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (hasVisual) {
|
|
424
|
+
const imported = importVisualSettingsPreset(
|
|
425
|
+
{ visual_settings: preset.visualSettings },
|
|
426
|
+
{ fallback: visualSettings }
|
|
427
|
+
);
|
|
428
|
+
Object.assign(visualSettings, imported);
|
|
429
|
+
runtimeControlPresetVisualBase = cloneRuntimeValue(visualSettings);
|
|
430
|
+
saveVisualSettingsPreset(browserStorage(), visualSettings);
|
|
373
431
|
syncVisualControls();
|
|
374
432
|
engine.setVisualSettings(visualSettings);
|
|
375
433
|
renderReactivityStatus("Project preset");
|
|
376
|
-
|
|
434
|
+
} else {
|
|
435
|
+
runtimeControlPresetVisualBase = cloneRuntimeValue(visualSettings);
|
|
377
436
|
}
|
|
378
437
|
|
|
379
|
-
if (
|
|
380
|
-
midiLearnBindings =
|
|
438
|
+
if (hasMidi) {
|
|
439
|
+
midiLearnBindings = cloneRuntimeValue(preset.midiLearnBindings);
|
|
440
|
+
runtimeControlPresetMidiBase = cloneRuntimeValue(midiLearnBindings);
|
|
441
|
+
saveMidiLearnBindings(browserStorage(), midiLearnBindings);
|
|
381
442
|
renderMidiLearnStatus();
|
|
382
|
-
|
|
443
|
+
} else {
|
|
444
|
+
runtimeControlPresetMidiBase = cloneRuntimeValue(midiLearnBindings);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
runtimeControlPresetSceneOverrides = cloneRuntimeValue(preset.sceneOverrides || {});
|
|
448
|
+
runtimeControlPresetApplied = true;
|
|
449
|
+
runtimeControlPresetSceneApplied = null;
|
|
450
|
+
applyRuntimeControlPresetForScene(currentSceneName, { force: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function applyRuntimeControlPresetForScene(sceneName, { force = false } = {}) {
|
|
454
|
+
if (!runtimeControlPresetApplied) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const normalizedScene = normalizeSceneName(sceneName || currentSceneName);
|
|
459
|
+
if (!force && runtimeControlPresetSceneApplied === normalizedScene) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
runtimeControlPresetSceneApplied = normalizedScene;
|
|
463
|
+
|
|
464
|
+
const override = normalizeRuntimeSceneOverride(runtimeControlPresetSceneOverrides[normalizedScene]) || {};
|
|
465
|
+
const nextVisual = deriveSceneVisualSettings(runtimeControlPresetVisualBase, override.visualSettings);
|
|
466
|
+
if (nextVisual) {
|
|
467
|
+
const hasVisualChanges = hasVisualSettingsChanges(visualSettings, nextVisual);
|
|
468
|
+
if (hasVisualChanges) {
|
|
469
|
+
Object.assign(visualSettings, nextVisual);
|
|
470
|
+
syncVisualControls();
|
|
471
|
+
engine.setVisualSettings(visualSettings);
|
|
472
|
+
renderReactivityStatus("Project preset");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const nextBindings = deriveSceneMidiBindings(runtimeControlPresetMidiBase, override.midiLearnBindings);
|
|
477
|
+
if (nextBindings && hasMidiLearnBindingChanges(midiLearnBindings, nextBindings)) {
|
|
478
|
+
midiLearnBindings = nextBindings;
|
|
479
|
+
renderMidiLearnStatus();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function deriveSceneVisualSettings(baseSettings, sceneVisualSettings) {
|
|
484
|
+
const base = baseSettings && typeof baseSettings === "object" ? cloneRuntimeValue(baseSettings) : {};
|
|
485
|
+
if (sceneVisualSettings) {
|
|
486
|
+
return Object.assign(base, cloneRuntimeValue(sceneVisualSettings));
|
|
487
|
+
}
|
|
488
|
+
return Object.keys(base).length ? base : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function deriveSceneMidiBindings(baseBindings, sceneMidiBindings) {
|
|
492
|
+
const base = baseBindings && typeof baseBindings === "object" ? cloneRuntimeValue(baseBindings) : {};
|
|
493
|
+
if (sceneMidiBindings) {
|
|
494
|
+
return Object.assign(base, cloneRuntimeValue(sceneMidiBindings));
|
|
495
|
+
}
|
|
496
|
+
return Object.keys(base).length ? base : null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function normalizeRuntimeSceneOverride(value) {
|
|
500
|
+
if (!value || typeof value !== "object") {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const input = value && typeof value === "object" ? value : {};
|
|
505
|
+
const output = {};
|
|
506
|
+
const visualSettings = objectValue(input.visualSettings) || objectValue(input.visual_settings);
|
|
507
|
+
const midiLearnBindings = objectValue(input.midiLearnBindings) || objectValue(input.midi_learn_bindings);
|
|
508
|
+
if (visualSettings) {
|
|
509
|
+
output.visualSettings = visualSettings;
|
|
510
|
+
}
|
|
511
|
+
if (midiLearnBindings) {
|
|
512
|
+
output.midiLearnBindings = midiLearnBindings;
|
|
513
|
+
}
|
|
514
|
+
return Object.keys(output).length ? output : null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function syncRuntimeControlPresetSceneVisualSetting(key, value) {
|
|
518
|
+
if (!runtimeControlPresetApplied) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const sceneName = normalizeSceneName(currentSceneName);
|
|
523
|
+
const override = runtimeControlPresetSceneOverrides[sceneName];
|
|
524
|
+
if (override && typeof override === "object") {
|
|
525
|
+
const normalizedSceneOverride = normalizeRuntimeSceneOverride(override) || {};
|
|
526
|
+
const currentOverride = cloneRuntimeValue(normalizedSceneOverride);
|
|
527
|
+
currentOverride.visualSettings ||= {};
|
|
528
|
+
currentOverride.visualSettings[key] = value;
|
|
529
|
+
runtimeControlPresetSceneOverrides[sceneName] = currentOverride;
|
|
530
|
+
applyRuntimeControlPresetForScene(sceneName, { force: true });
|
|
531
|
+
return;
|
|
383
532
|
}
|
|
384
533
|
|
|
385
|
-
|
|
534
|
+
runtimeControlPresetVisualBase = runtimeControlPresetVisualBase || cloneRuntimeValue(visualSettings);
|
|
535
|
+
runtimeControlPresetVisualBase[key] = value;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function syncRuntimeControlPresetMidiBindings(nextBindings) {
|
|
539
|
+
if (!runtimeControlPresetApplied) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const sceneName = normalizeSceneName(currentSceneName);
|
|
544
|
+
const normalizedBindings = objectValue(nextBindings) ? cloneRuntimeValue(nextBindings) : {};
|
|
545
|
+
const override = runtimeControlPresetSceneOverrides[sceneName];
|
|
546
|
+
if (override && typeof override === "object") {
|
|
547
|
+
const normalizedSceneOverride = normalizeRuntimeSceneOverride(override) || {};
|
|
548
|
+
const currentOverride = cloneRuntimeValue(normalizedSceneOverride);
|
|
549
|
+
currentOverride.midiLearnBindings = normalizedBindings;
|
|
550
|
+
runtimeControlPresetSceneOverrides[sceneName] = currentOverride;
|
|
551
|
+
applyRuntimeControlPresetForScene(sceneName, { force: true });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
runtimeControlPresetMidiBase = normalizedBindings;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function syncRuntimeControlPresetBaseWithRuntime(nextVisualSettings = visualSettings) {
|
|
559
|
+
if (!runtimeControlPresetApplied) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
runtimeControlPresetVisualBase = cloneRuntimeValue(nextVisualSettings);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function hasVisualSettingsChanges(currentValue, nextValue) {
|
|
567
|
+
return (
|
|
568
|
+
currentValue.visualGain !== nextValue.visualGain ||
|
|
569
|
+
currentValue.bassBoost !== nextValue.bassBoost ||
|
|
570
|
+
currentValue.smoothing !== nextValue.smoothing ||
|
|
571
|
+
currentValue.beatHoldMs !== nextValue.beatHoldMs ||
|
|
572
|
+
currentValue.wobbleAmount !== nextValue.wobbleAmount
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function hasMidiLearnBindingChanges(currentBindings, nextBindings) {
|
|
577
|
+
if (!nextBindings) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const currentKeys = Object.keys(currentBindings || {});
|
|
581
|
+
const nextKeys = Object.keys(nextBindings);
|
|
582
|
+
if (currentKeys.length !== nextKeys.length) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
for (const signature of nextKeys) {
|
|
586
|
+
if (!Object.prototype.hasOwnProperty.call(currentBindings, signature)) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
if (JSON.stringify(currentBindings[signature]) !== JSON.stringify(nextBindings[signature])) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function normalizeSceneName(sceneName) {
|
|
597
|
+
return String(sceneName || "").trim() || "unknown";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function objectValue(value) {
|
|
601
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function cloneRuntimeValue(value) {
|
|
605
|
+
if (!value || typeof value !== "object") {
|
|
606
|
+
return {};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return JSON.parse(JSON.stringify(value));
|
|
386
610
|
}
|
|
387
611
|
|
|
388
612
|
function updateAvailableScenes(sceneValues) {
|
|
@@ -404,6 +628,35 @@ function updateKeyboardMappings(mappings) {
|
|
|
404
628
|
renderSceneButtons();
|
|
405
629
|
}
|
|
406
630
|
|
|
631
|
+
function updateSceneControls(scene, { forceReset = false } = {}) {
|
|
632
|
+
const sceneLayers = scene?.layers;
|
|
633
|
+
const hasLayers = Array.isArray(sceneLayers);
|
|
634
|
+
const nextSceneName = String(scene?.name || currentSceneName);
|
|
635
|
+
|
|
636
|
+
if (currentSceneName !== nextSceneName) {
|
|
637
|
+
currentSceneName = nextSceneName;
|
|
638
|
+
sceneStatusElement.textContent = `Scene: ${currentSceneName}`;
|
|
639
|
+
renderSceneButtons();
|
|
640
|
+
forceReset = true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!scene) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (forceReset) {
|
|
648
|
+
shaderParamControlsSignature = "";
|
|
649
|
+
shapeEditorControlsSignature = "";
|
|
650
|
+
customShapeParamControlsSignature = "";
|
|
651
|
+
mappingTargetSelectorSignature = "";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
updateShaderParamControls(hasLayers ? sceneLayers : null);
|
|
655
|
+
updateShapeEditorControls(hasLayers ? sceneLayers : null);
|
|
656
|
+
updateCustomShapeParamControls(hasLayers ? sceneLayers : null);
|
|
657
|
+
updateMappingTargetSelector(hasLayers ? sceneLayers : null);
|
|
658
|
+
}
|
|
659
|
+
|
|
407
660
|
function normalizeSceneNames(sceneValues) {
|
|
408
661
|
const seen = new Set();
|
|
409
662
|
const names = [];
|
|
@@ -762,7 +1015,7 @@ function renderMappingTargetSelector(options) {
|
|
|
762
1015
|
mappingTargetSelectorElement.hidden = false;
|
|
763
1016
|
}
|
|
764
1017
|
|
|
765
|
-
function requestSceneSwitch(sceneName) {
|
|
1018
|
+
function requestSceneSwitch(sceneName, effect = null) {
|
|
766
1019
|
if (!sceneName || sceneName === currentSceneName) {
|
|
767
1020
|
return;
|
|
768
1021
|
}
|
|
@@ -772,17 +1025,29 @@ function requestSceneSwitch(sceneName) {
|
|
|
772
1025
|
currentSceneName = sceneName;
|
|
773
1026
|
sceneStatusElement.textContent = `Scene: ${sceneName}`;
|
|
774
1027
|
renderSceneButtons();
|
|
775
|
-
|
|
1028
|
+
const payload = { scene: sceneName };
|
|
1029
|
+
const normalizedEffect = normalizeTransitionEffect(effect);
|
|
1030
|
+
if (normalizedEffect) {
|
|
1031
|
+
payload.effect = normalizedEffect;
|
|
1032
|
+
}
|
|
1033
|
+
client.send("switch_scene", payload);
|
|
776
1034
|
}
|
|
777
1035
|
|
|
778
1036
|
function applyKeyboardAction(action) {
|
|
779
1037
|
if (action?.type === "switch_scene") {
|
|
780
|
-
requestSceneSwitch(action.scene);
|
|
1038
|
+
requestSceneSwitch(action.scene, action.effect);
|
|
781
1039
|
return;
|
|
782
1040
|
}
|
|
783
1041
|
|
|
784
1042
|
if (action?.type === "live_control") {
|
|
785
|
-
|
|
1043
|
+
if (Object.prototype.hasOwnProperty.call(action, "value")) {
|
|
1044
|
+
applyLiveControls({
|
|
1045
|
+
[action?.control]: normalizeLiveControlPayload(action),
|
|
1046
|
+
});
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
applyLiveControls(toggleLiveControl(liveControls, action?.control));
|
|
786
1051
|
}
|
|
787
1052
|
}
|
|
788
1053
|
|
|
@@ -814,6 +1079,20 @@ function sendLatencyProbe() {
|
|
|
814
1079
|
});
|
|
815
1080
|
}
|
|
816
1081
|
|
|
1082
|
+
function normalizeTransitionEffect(effect) {
|
|
1083
|
+
if (!effect) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof effect === "string" || typeof effect === "number" || typeof effect === "symbol") {
|
|
1087
|
+
return String(effect);
|
|
1088
|
+
}
|
|
1089
|
+
if (typeof effect !== "object" || Array.isArray(effect)) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return effect;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
817
1096
|
function updatePerformanceMonitor(nextState) {
|
|
818
1097
|
Object.assign(performanceMonitor, nextState);
|
|
819
1098
|
renderPerformanceMonitor();
|
|
@@ -833,6 +1112,15 @@ function bindShaderCompileMetrics() {
|
|
|
833
1112
|
});
|
|
834
1113
|
}
|
|
835
1114
|
|
|
1115
|
+
function bindRendererMetrics() {
|
|
1116
|
+
window.addEventListener(RENDERER_CAPABILITIES_EVENT, (event) => {
|
|
1117
|
+
updatePerformanceMonitor(recordRendererCapabilities(performanceMonitor, event.detail));
|
|
1118
|
+
});
|
|
1119
|
+
window.addEventListener(RENDERER_SAFE_MODE_EVENT, (event) => {
|
|
1120
|
+
updatePerformanceMonitor(recordRendererSafeMode(performanceMonitor, event.detail));
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
836
1124
|
function bindLiveControls() {
|
|
837
1125
|
bindLiveControlButton(blackoutButton, "blackout");
|
|
838
1126
|
bindLiveControlButton(freezeButton, "freeze");
|
|
@@ -847,7 +1135,7 @@ function bindLiveControls() {
|
|
|
847
1135
|
const action = shortcutActionForKey(event);
|
|
848
1136
|
if (action) {
|
|
849
1137
|
event.preventDefault();
|
|
850
|
-
|
|
1138
|
+
applyKeyboardAction({ type: "live_control", control: action });
|
|
851
1139
|
return;
|
|
852
1140
|
}
|
|
853
1141
|
|
|
@@ -881,16 +1169,54 @@ function bindLiveControlButton(button, control) {
|
|
|
881
1169
|
}
|
|
882
1170
|
|
|
883
1171
|
function applyLiveControls(nextState) {
|
|
884
|
-
|
|
1172
|
+
if (!nextState || typeof nextState !== "object") {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const nextBlackout = mergeLiveControlState(
|
|
1177
|
+
liveControls.blackout,
|
|
1178
|
+
nextState.blackout
|
|
1179
|
+
);
|
|
1180
|
+
const nextFreeze = mergeLiveControlState(
|
|
1181
|
+
liveControls.freeze,
|
|
1182
|
+
nextState.freeze
|
|
1183
|
+
);
|
|
1184
|
+
Object.assign(liveControls, {
|
|
1185
|
+
blackout: nextBlackout,
|
|
1186
|
+
freeze: nextFreeze,
|
|
1187
|
+
});
|
|
885
1188
|
engine.setLiveControls(liveControls);
|
|
886
1189
|
renderLiveControlStatus();
|
|
887
1190
|
}
|
|
888
1191
|
|
|
1192
|
+
function mergeLiveControlState(currentState, nextState) {
|
|
1193
|
+
const current = normalizeLiveControlPayload(currentState);
|
|
1194
|
+
if (nextState === undefined) {
|
|
1195
|
+
return current;
|
|
1196
|
+
}
|
|
1197
|
+
if (!nextState || typeof nextState !== "object" || Array.isArray(nextState)) {
|
|
1198
|
+
return normalizeLiveControlPayload(nextState);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const normalized = normalizeLiveControlPayload(nextState);
|
|
1202
|
+
const nextHasFade = Object.prototype.hasOwnProperty.call(nextState, "fade");
|
|
1203
|
+
const nextHasRelease = Object.prototype.hasOwnProperty.call(nextState, "release");
|
|
1204
|
+
const nextHasColor = Object.prototype.hasOwnProperty.call(nextState, "color");
|
|
1205
|
+
|
|
1206
|
+
return {
|
|
1207
|
+
...current,
|
|
1208
|
+
...normalized,
|
|
1209
|
+
...(nextHasFade ? { fade: normalized.fade } : {}),
|
|
1210
|
+
...(nextHasRelease ? { release: normalized.release } : {}),
|
|
1211
|
+
...(nextHasColor ? { color: normalized.color } : {}),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
889
1215
|
function normalizeLiveControls(value) {
|
|
890
1216
|
const input = value && typeof value === "object" ? value : {};
|
|
891
1217
|
return {
|
|
892
|
-
blackout:
|
|
893
|
-
freeze:
|
|
1218
|
+
blackout: normalizeLiveControlPayload(input.blackout),
|
|
1219
|
+
freeze: normalizeLiveControlPayload(input.freeze),
|
|
894
1220
|
};
|
|
895
1221
|
}
|
|
896
1222
|
|
|
@@ -900,13 +1226,15 @@ function renderLiveControlStatus() {
|
|
|
900
1226
|
}
|
|
901
1227
|
|
|
902
1228
|
if (blackoutButton) {
|
|
903
|
-
|
|
904
|
-
blackoutButton.
|
|
1229
|
+
const isActive = isLiveControlEnabled(liveControls.blackout);
|
|
1230
|
+
blackoutButton.classList.toggle("is-active", isActive);
|
|
1231
|
+
blackoutButton.setAttribute("aria-pressed", String(isActive));
|
|
905
1232
|
}
|
|
906
1233
|
|
|
907
1234
|
if (freezeButton) {
|
|
908
|
-
|
|
909
|
-
freezeButton.
|
|
1235
|
+
const isActive = isLiveControlEnabled(liveControls.freeze);
|
|
1236
|
+
freezeButton.classList.toggle("is-active", isActive);
|
|
1237
|
+
freezeButton.setAttribute("aria-pressed", String(isActive));
|
|
910
1238
|
}
|
|
911
1239
|
}
|
|
912
1240
|
|
|
@@ -1015,6 +1343,7 @@ function bindVisualControl(control, key, parser = Number) {
|
|
|
1015
1343
|
visualSettings[key] = parser(control.value);
|
|
1016
1344
|
engine.setVisualSettings(visualSettings);
|
|
1017
1345
|
renderReactivityStatus();
|
|
1346
|
+
syncRuntimeControlPresetSceneVisualSetting(key, visualSettings[key]);
|
|
1018
1347
|
});
|
|
1019
1348
|
}
|
|
1020
1349
|
|
|
@@ -1022,6 +1351,7 @@ function bindVisualPresetControls() {
|
|
|
1022
1351
|
if (reactivitySaveButton) {
|
|
1023
1352
|
reactivitySaveButton.addEventListener("click", () => {
|
|
1024
1353
|
Object.assign(visualSettings, saveVisualSettingsPreset(browserStorage(), visualSettings));
|
|
1354
|
+
syncRuntimeControlPresetBaseWithRuntime(visualSettings);
|
|
1025
1355
|
renderReactivityStatus("Saved");
|
|
1026
1356
|
});
|
|
1027
1357
|
}
|
|
@@ -1031,6 +1361,7 @@ function bindVisualPresetControls() {
|
|
|
1031
1361
|
Object.assign(visualSettings, loadVisualSettingsPreset(browserStorage(), { fallback: visualSettings }));
|
|
1032
1362
|
syncVisualControls();
|
|
1033
1363
|
engine.setVisualSettings(visualSettings);
|
|
1364
|
+
syncRuntimeControlPresetBaseWithRuntime(visualSettings);
|
|
1034
1365
|
renderReactivityStatus("Loaded");
|
|
1035
1366
|
});
|
|
1036
1367
|
}
|
|
@@ -1075,6 +1406,7 @@ function bindVisualPresetControls() {
|
|
|
1075
1406
|
Object.assign(visualSettings, saveVisualSettingsPreset(browserStorage(), visualSettings));
|
|
1076
1407
|
syncVisualControls();
|
|
1077
1408
|
engine.setVisualSettings(visualSettings);
|
|
1409
|
+
syncRuntimeControlPresetBaseWithRuntime(visualSettings);
|
|
1078
1410
|
renderReactivityStatus("Imported");
|
|
1079
1411
|
});
|
|
1080
1412
|
}
|
|
@@ -1088,6 +1420,7 @@ async function saveProjectControlPreset() {
|
|
|
1088
1420
|
body: JSON.stringify({
|
|
1089
1421
|
visual_settings: visualSettings,
|
|
1090
1422
|
midi_learn_bindings: midiLearnBindings,
|
|
1423
|
+
scene_overrides: runtimeControlPresetSceneOverrides,
|
|
1091
1424
|
}),
|
|
1092
1425
|
});
|
|
1093
1426
|
return response.ok;
|
|
@@ -1172,6 +1505,7 @@ function handleMidiMessage(event) {
|
|
|
1172
1505
|
if (pendingMidiLearnAction && midiMessageActive(event?.data)) {
|
|
1173
1506
|
midiLearnBindings = upsertMidiLearnBinding(midiLearnBindings, signature, pendingMidiLearnAction);
|
|
1174
1507
|
midiLearnBindings = saveMidiLearnBindings(browserStorage(), midiLearnBindings);
|
|
1508
|
+
syncRuntimeControlPresetMidiBindings(midiLearnBindings);
|
|
1175
1509
|
renderMidiLearnStatus(`Learned ${midiSignatureLabel(signature)} -> ${midiLearnActionLabel(pendingMidiLearnAction)}`);
|
|
1176
1510
|
pendingMidiLearnAction = null;
|
|
1177
1511
|
return;
|
|
@@ -1190,6 +1524,7 @@ function applyMidiLearnAction(action, unitValue, active) {
|
|
|
1190
1524
|
visualSettings[action.key] = visualSettingFromUnit(action.key, unitValue, visualSettings[action.key]);
|
|
1191
1525
|
syncVisualControls();
|
|
1192
1526
|
engine.setVisualSettings(visualSettings);
|
|
1527
|
+
syncRuntimeControlPresetSceneVisualSetting(action.key, visualSettings[action.key]);
|
|
1193
1528
|
renderReactivityStatus("MIDI");
|
|
1194
1529
|
return;
|
|
1195
1530
|
}
|
|
@@ -1199,13 +1534,19 @@ function applyMidiLearnAction(action, unitValue, active) {
|
|
|
1199
1534
|
}
|
|
1200
1535
|
|
|
1201
1536
|
if (action.type === "switch_scene") {
|
|
1202
|
-
requestSceneSwitch(action.scene);
|
|
1537
|
+
requestSceneSwitch(action.scene, action.effect);
|
|
1203
1538
|
renderMidiLearnStatus(`MIDI: ${midiLearnActionLabel(action)}`);
|
|
1204
1539
|
return;
|
|
1205
1540
|
}
|
|
1206
1541
|
|
|
1207
1542
|
if (action.type === "live_control") {
|
|
1208
|
-
|
|
1543
|
+
if (Object.prototype.hasOwnProperty.call(action, "value")) {
|
|
1544
|
+
applyLiveControls({
|
|
1545
|
+
[action.control]: normalizeLiveControlPayload(action),
|
|
1546
|
+
});
|
|
1547
|
+
} else {
|
|
1548
|
+
applyLiveControls(toggleLiveControl(liveControls, action.control));
|
|
1549
|
+
}
|
|
1209
1550
|
renderMidiLearnStatus(`MIDI: ${midiLearnActionLabel(action)}`);
|
|
1210
1551
|
}
|
|
1211
1552
|
}
|
|
@@ -1274,6 +1615,7 @@ function renderMidiLearnStatus(prefix = null) {
|
|
|
1274
1615
|
function bindShaderErrorOverlay() {
|
|
1275
1616
|
window.addEventListener(SHADER_ERROR_EVENT, (event) => {
|
|
1276
1617
|
renderShaderError(event.detail);
|
|
1618
|
+
reportShaderErrorToServer(event.detail);
|
|
1277
1619
|
});
|
|
1278
1620
|
if (shaderErrorCloseButton) {
|
|
1279
1621
|
shaderErrorCloseButton.addEventListener("click", () => {
|
|
@@ -1294,6 +1636,31 @@ function renderShaderError(detail) {
|
|
|
1294
1636
|
shaderErrorOverlay.hidden = false;
|
|
1295
1637
|
}
|
|
1296
1638
|
|
|
1639
|
+
function reportShaderErrorToServer(detail = {}) {
|
|
1640
|
+
const source = String(detail?.source || "shader").trim() || "shader";
|
|
1641
|
+
const layer = String(detail?.name || "layer").trim();
|
|
1642
|
+
const shader = String(detail?.shader || "unknown").trim();
|
|
1643
|
+
const event = String(detail?.event || "shader_failed").trim() || "shader_failed";
|
|
1644
|
+
const phase = String(detail?.phase || "").trim();
|
|
1645
|
+
const message = String(detail?.message || "").trim();
|
|
1646
|
+
const fullMessage = `${layer} (${shader}) ${phase ? `[${phase}] ` : ""}${message}`;
|
|
1647
|
+
client.send("client_runtime_error", {
|
|
1648
|
+
source,
|
|
1649
|
+
event,
|
|
1650
|
+
context: "shader compile failed",
|
|
1651
|
+
message: fullMessage,
|
|
1652
|
+
layer,
|
|
1653
|
+
shader,
|
|
1654
|
+
phase
|
|
1655
|
+
});
|
|
1656
|
+
updateRuntimeErrorStatus({
|
|
1657
|
+
source,
|
|
1658
|
+
event,
|
|
1659
|
+
context: "shader compile failed",
|
|
1660
|
+
message: fullMessage
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1297
1664
|
function initializeFftPreview(container) {
|
|
1298
1665
|
if (!container) {
|
|
1299
1666
|
return [];
|
|
@@ -1333,6 +1700,26 @@ function renderAudioInspector(audio) {
|
|
|
1333
1700
|
}
|
|
1334
1701
|
}
|
|
1335
1702
|
|
|
1703
|
+
function updateRuntimeErrorStatus(payload = {}) {
|
|
1704
|
+
if (!runtimeErrorStatusElement) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const source = String(payload?.source || "runtime").trim();
|
|
1709
|
+
const event = String(payload?.event || "").trim();
|
|
1710
|
+
const context = String(payload?.context || "runtime error").trim();
|
|
1711
|
+
const message = String(payload?.message || "").trim();
|
|
1712
|
+
const frameId = payload?.frame_id;
|
|
1713
|
+
|
|
1714
|
+
const detail = [context, event].filter(Boolean).join(" / ");
|
|
1715
|
+
const frameText = Number.isFinite(frameId) ? ` (frame ${frameId})` : "";
|
|
1716
|
+
const text = message
|
|
1717
|
+
? `Runtime (${source}): ${detail}${frameText} | ${message}`
|
|
1718
|
+
: `Runtime (${source}): ${detail}${frameText}`;
|
|
1719
|
+
|
|
1720
|
+
runtimeErrorStatusElement.textContent = text;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1336
1723
|
function setMeter(fill, valueElement, value, digits) {
|
|
1337
1724
|
if (fill) {
|
|
1338
1725
|
fill.style.setProperty("--meter-value", value.toFixed(4));
|
|
@@ -1344,5 +1731,8 @@ function setMeter(fill, valueElement, value, digits) {
|
|
|
1344
1731
|
|
|
1345
1732
|
function buildWebSocketUrl() {
|
|
1346
1733
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
1347
|
-
|
|
1734
|
+
const mode = new URLSearchParams(window.location.search || "").get("mode");
|
|
1735
|
+
const normalizedMode = String(mode || "").toLowerCase();
|
|
1736
|
+
const role = projectorMode || normalizedMode === "projector" ? "projector" : normalizedMode === "monitor" ? "monitor" : "control";
|
|
1737
|
+
return `${protocol}://${window.location.host}/ws?role=${encodeURIComponent(role)}`;
|
|
1348
1738
|
}
|