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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/index.html +24 -2
  3. data/frontend/src/audio-inspector.js +9 -0
  4. data/frontend/src/live-controls.js +219 -7
  5. data/frontend/src/main.js +447 -57
  6. data/frontend/src/midi-learn.js +22 -2
  7. data/frontend/src/performance-monitor.js +137 -1
  8. data/frontend/src/renderer/engine.js +391 -10
  9. data/frontend/src/renderer/layer-manager.js +472 -71
  10. data/frontend/src/runtime-control-preset.js +44 -0
  11. data/frontend/src/scene-patches.js +159 -0
  12. data/frontend/src/shader-error-overlay.js +1 -0
  13. data/frontend/src/visuals/image-renderer.js +19 -0
  14. data/frontend/src/visuals/particle-system.js +10 -0
  15. data/frontend/src/visuals/shape-renderer.js +13 -0
  16. data/frontend/src/visuals/spectrogram-renderer.js +14 -0
  17. data/frontend/src/visuals/text-renderer.js +13 -0
  18. data/frontend/src/websocket-client.js +6 -0
  19. data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
  20. data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
  21. data/lib/vizcore/analysis/feature_recorder.rb +117 -7
  22. data/lib/vizcore/analysis/feature_replay.rb +48 -9
  23. data/lib/vizcore/analysis/pipeline.rb +258 -9
  24. data/lib/vizcore/analysis/tap_tempo.rb +17 -2
  25. data/lib/vizcore/audio/calibration.rb +156 -0
  26. data/lib/vizcore/audio/file_input.rb +28 -0
  27. data/lib/vizcore/audio/input_manager.rb +36 -1
  28. data/lib/vizcore/audio/midi_input.rb +5 -0
  29. data/lib/vizcore/audio/ring_buffer.rb +22 -0
  30. data/lib/vizcore/audio.rb +1 -0
  31. data/lib/vizcore/cli/dsl_reference.rb +64 -8
  32. data/lib/vizcore/cli/plugin_checker.rb +93 -0
  33. data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
  34. data/lib/vizcore/cli/scene_inspector.rb +35 -1
  35. data/lib/vizcore/cli/scene_validator.rb +487 -39
  36. data/lib/vizcore/cli/shader_template.rb +7 -2
  37. data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
  38. data/lib/vizcore/cli.rb +268 -15
  39. data/lib/vizcore/config.rb +40 -3
  40. data/lib/vizcore/control_preset.rb +29 -0
  41. data/lib/vizcore/deep_copy.rb +21 -0
  42. data/lib/vizcore/dsl/color_helpers.rb +155 -0
  43. data/lib/vizcore/dsl/engine.rb +219 -23
  44. data/lib/vizcore/dsl/layer_builder.rb +278 -15
  45. data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
  46. data/lib/vizcore/dsl/layout_helpers.rb +290 -0
  47. data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
  49. data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
  50. data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
  51. data/lib/vizcore/dsl/reaction_builder.rb +1 -0
  52. data/lib/vizcore/dsl/scene_builder.rb +83 -13
  53. data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
  54. data/lib/vizcore/dsl/style_builder.rb +3 -0
  55. data/lib/vizcore/dsl/timeline_builder.rb +91 -8
  56. data/lib/vizcore/dsl/transition_controller.rb +157 -18
  57. data/lib/vizcore/dsl.rb +2 -0
  58. data/lib/vizcore/layer_catalog.rb +1 -0
  59. data/lib/vizcore/plugin_asset_policy.rb +55 -0
  60. data/lib/vizcore/project_manifest.rb +12 -2
  61. data/lib/vizcore/renderer/render_sequence.rb +104 -13
  62. data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
  63. data/lib/vizcore/renderer/scene_serializer.rb +38 -0
  64. data/lib/vizcore/renderer/snapshot.rb +4 -3
  65. data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
  66. data/lib/vizcore/scene_trust.rb +31 -0
  67. data/lib/vizcore/server/frame_broadcaster.rb +469 -23
  68. data/lib/vizcore/server/rack_app.rb +151 -4
  69. data/lib/vizcore/server/runner.rb +676 -82
  70. data/lib/vizcore/server/websocket_handler.rb +236 -14
  71. data/lib/vizcore/server.rb +21 -0
  72. data/lib/vizcore/shape.rb +39 -16
  73. data/lib/vizcore/sync/osc_message.rb +66 -9
  74. data/lib/vizcore/version.rb +1 -1
  75. data/lib/vizcore.rb +33 -0
  76. data/scripts/browser_capture.mjs +31 -2
  77. data/sig/vizcore.rbs +55 -4
  78. metadata +18 -3
@@ -4,6 +4,7 @@ require "json"
4
4
  require "pathname"
5
5
  require "rack"
6
6
  require_relative "../control_preset"
7
+ require_relative "../plugin_asset_policy"
7
8
  require_relative "websocket_handler"
8
9
 
9
10
  module Vizcore
@@ -29,6 +30,7 @@ module Vizcore
29
30
  # @param control_preset_path [String, Pathname, nil]
30
31
  # @param plugin_assets [Array<String, Pathname>, nil]
31
32
  # @param projector_mode [Boolean]
33
+ # @param runtime_status_provider [#call, nil]
32
34
  def initialize(
33
35
  frontend_root:,
34
36
  websocket_path: "/ws",
@@ -41,7 +43,8 @@ module Vizcore
41
43
  control_preset: nil,
42
44
  control_preset_path: nil,
43
45
  plugin_assets: nil,
44
- projector_mode: false
46
+ projector_mode: false,
47
+ runtime_status_provider: nil
45
48
  )
46
49
  @frontend_root = frontend_root.expand_path
47
50
  @websocket_path = websocket_path
@@ -55,6 +58,7 @@ module Vizcore
55
58
  @control_preset_path = control_preset_path ? Pathname.new(control_preset_path).expand_path : nil
56
59
  @plugin_assets = normalize_plugin_assets(plugin_assets)
57
60
  @projector_mode = !!projector_mode
61
+ @runtime_status_provider = runtime_status_provider
58
62
  end
59
63
 
60
64
  # @param env [Hash]
@@ -96,7 +100,11 @@ module Vizcore
96
100
  control_preset_writable: !!@control_preset_path,
97
101
  control_preset_url: @control_preset_path ? CONTROL_PRESET_PATH : nil,
98
102
  plugin_assets: @plugin_assets.map { |asset| asset.fetch(:url) },
99
- projector_mode: @projector_mode
103
+ projector_mode: @projector_mode,
104
+ websocket_clients: WebSocketHandler.connection_count,
105
+ dropped_frames: WebSocketHandler.dropped_frame_count,
106
+ websocket_backpressure: WebSocketHandler.backpressure_status,
107
+ runtime: runtime_status
100
108
  }
101
109
 
102
110
  if audio_file_available?
@@ -187,6 +195,35 @@ module Vizcore
187
195
  @projector_mode ? "projector" : "auto"
188
196
  end
189
197
 
198
+ def runtime_status
199
+ return {} unless @runtime_status_provider.respond_to?(:call)
200
+
201
+ normalize_runtime_status(@runtime_status_provider.call)
202
+ rescue StandardError => e
203
+ { "last_error" => e.message }
204
+ end
205
+
206
+ def normalize_runtime_status(values)
207
+ Hash(values || {}).each_with_object({}) do |(key, value), output|
208
+ output[key.to_s] = normalize_runtime_status_value(value)
209
+ end
210
+ rescue StandardError
211
+ {}
212
+ end
213
+
214
+ def normalize_runtime_status_value(value)
215
+ case value
216
+ when Hash
217
+ normalize_runtime_status(value)
218
+ when Array
219
+ value.map { |entry| normalize_runtime_status_value(entry) }
220
+ when Symbol
221
+ value.to_s
222
+ else
223
+ value
224
+ end
225
+ end
226
+
190
227
  def static_response(body, content_type:)
191
228
  headers = {
192
229
  "content-type" => content_type,
@@ -280,7 +317,23 @@ module Vizcore
280
317
  control = (action[:control] || action["control"]).to_s.strip
281
318
  return nil unless %w[blackout freeze].include?(control)
282
319
 
283
- { type: "live_control", control: control }
320
+ live_control = { type: "live_control", control: control }
321
+ if action.key?(:value) || action.key?("value")
322
+ live_control[:value] = action[:value]
323
+ live_control[:value] = action["value"] if action.key?("value")
324
+ end
325
+ fade = action.key?(:fade) ? action[:fade] : action["fade"]
326
+ release = action.key?(:release) ? action[:release] : action["release"]
327
+ color = action.key?(:color) ? action[:color] : action["color"]
328
+ normalized_fade = finite_float(fade)
329
+ normalized_release = finite_float(release)
330
+ normalized_color = normalize_control_color(color)
331
+ return live_control if normalized_fade.nil? && normalized_release.nil? && !live_control.key?(:value) && normalized_color.nil?
332
+
333
+ live_control[:fade] = normalized_fade if normalized_fade
334
+ live_control[:release] = normalized_release if normalized_release
335
+ live_control[:color] = normalized_color if normalized_color
336
+ live_control
284
337
  end
285
338
  end
286
339
 
@@ -299,21 +352,115 @@ module Vizcore
299
352
  preset = values.is_a?(Hash) ? values : {}
300
353
  visual_settings = preset[:visual_settings] || preset["visual_settings"] || preset[:visualSettings] || preset["visualSettings"]
301
354
  midi_learn_bindings = preset[:midi_learn_bindings] || preset["midi_learn_bindings"] || preset[:midiLearnBindings] || preset["midiLearnBindings"]
355
+ scene_overrides = preset[:scene_overrides] || preset["scene_overrides"] || preset[:sceneOverrides] || preset["sceneOverrides"]
302
356
 
303
357
  {}.tap do |payload|
304
358
  payload["visual_settings"] = visual_settings if visual_settings.is_a?(Hash)
305
359
  payload["midi_learn_bindings"] = midi_learn_bindings if midi_learn_bindings.is_a?(Hash)
360
+ normalized_scene_overrides = normalize_scene_overrides(scene_overrides)
361
+ payload["scene_overrides"] = normalized_scene_overrides if normalized_scene_overrides
362
+ end
363
+ rescue StandardError
364
+ {}
365
+ end
366
+
367
+ def normalize_scene_overrides(values)
368
+ return nil unless values
369
+
370
+ raw_overrides = values.is_a?(Hash) ? values : {}
371
+ normalized = {}
372
+
373
+ raw_overrides.each do |raw_scene, raw_override|
374
+ scene_name = raw_scene.to_s.strip
375
+ next if scene_name.empty?
376
+
377
+ scene_override = normalize_scene_override(raw_override)
378
+ next if scene_override.empty?
379
+
380
+ normalized[scene_name] = scene_override
381
+ end
382
+
383
+ normalized.empty? ? nil : normalized
384
+ rescue StandardError
385
+ nil
386
+ end
387
+
388
+ def normalize_scene_override(value)
389
+ input = value.is_a?(Hash) ? value : {}
390
+
391
+ {
392
+ visual_settings: input[:visual_settings] || input["visual_settings"] || input[:visualSettings] || input["visualSettings"],
393
+ midi_learn_bindings: input[:midi_learn_bindings] || input["midi_learn_bindings"] || input[:midiLearnBindings] || input["midiLearnBindings"] || input[:midi] || input["midi"]
394
+ }.each_with_object({}) do |(key, value), output|
395
+ output[key.to_s] = value if value.is_a?(Hash)
306
396
  end
307
397
  rescue StandardError
308
398
  {}
309
399
  end
310
400
 
401
+ def finite_float(value)
402
+ numeric = Float(value)
403
+ return numeric if numeric.finite?
404
+
405
+ nil
406
+ rescue ArgumentError, TypeError
407
+ nil
408
+ end
409
+
410
+ def normalize_control_color(value)
411
+ return nil if value.nil?
412
+ if value.is_a?(Array)
413
+ return nil unless (3..4).cover?(value.length)
414
+
415
+ channels = Array(value).map { |entry| Float(entry, exception: false) }
416
+ return nil if channels.include?(nil)
417
+
418
+ rgb = channels.take(3)
419
+ alpha = channels[3]
420
+ normalized_rgb = if rgb.all? { |channel| channel.between?(0.0, 1.0) }
421
+ rgb
422
+ else
423
+ rgb.map { |channel| channel / 255.0 }
424
+ end
425
+ normalized = normalized_rgb.map { |channel| [0.0, [1.0, channel].min].max }
426
+ return alpha.nil? ? normalized : normalized + [normalize_control_alpha(alpha)]
427
+ end
428
+
429
+ raw = value.to_s.strip
430
+ match = raw.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/)
431
+ return nil unless match
432
+
433
+ raw_hex = match[1]
434
+ hex = raw_hex.length == 3 || raw_hex.length == 4 ? raw_hex.chars.map { |entry| "#{entry}#{entry}" }.join("") : raw_hex
435
+
436
+ [
437
+ Integer("0x#{hex[0, 2]}", 16),
438
+ Integer("0x#{hex[2, 2]}", 16),
439
+ Integer("0x#{hex[4, 2]}", 16),
440
+ Integer("0x#{hex[6, 2]}", 16)
441
+ ].take(raw_hex.length > 4 ? 4 : 3).map { |channel| [0.0, [1.0, channel / 255.0].min].max }
442
+ rescue ArgumentError, TypeError
443
+ nil
444
+ end
445
+
446
+ def normalize_control_alpha(value)
447
+ return nil if value.nil?
448
+
449
+ alpha = Float(value, exception: false)
450
+ return nil if alpha.nil?
451
+
452
+ return [0.0, [1.0, alpha].min].max if alpha.between?(0.0, 1.0)
453
+
454
+ [0.0, [1.0, alpha / 255.0].min].max
455
+ end
456
+
311
457
  def normalize_plugin_assets(values)
312
458
  Array(values).each_with_index.filter_map do |value, index|
313
459
  raw_value = value.to_s.strip
314
460
  next if raw_value.empty?
315
461
 
316
- path = value.is_a?(Pathname) ? value.expand_path : Pathname.new(raw_value).expand_path
462
+ raw_path = value.is_a?(Pathname) ? value.expand_path : Pathname.new(raw_value).expand_path
463
+ path = Vizcore::PluginAssetPolicy.validate!(raw_path)
317
464
  {
318
465
  path: path,
319
466
  url: "#{PLUGIN_ASSET_PREFIX}#{index}/#{rack_escape_path(path.basename.to_s)}"