vizcore 0.1.0 → 1.1.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/README.md +70 -117
- data/docs/.nojekyll +0 -0
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/assets/site.css +744 -0
- data/docs/assets/vizcore-demo.gif +0 -0
- data/docs/assets/vizcore-poster.png +0 -0
- data/docs/assets/vj-tunnel.js +159 -0
- data/docs/index.html +225 -0
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/examples/README.md +59 -0
- data/examples/assets/README.md +19 -0
- data/examples/audio_inspector.rb +34 -0
- data/examples/club_intro_drop.rb +78 -0
- data/examples/kansai_rubykaigi_visual.rb +70 -0
- data/examples/live_coding_minimal.rb +22 -0
- data/examples/midi_controller_show.rb +78 -0
- data/examples/midi_scene_switch.rb +3 -1
- data/examples/parser_visualizer.rb +48 -0
- data/examples/readme_demo.rb +17 -0
- data/examples/rhythm_geometry.rb +34 -0
- data/examples/ruby_crystal_show.rb +35 -0
- data/examples/shader_playground.rb +18 -0
- data/examples/unyo_liquid.rb +59 -0
- data/examples/vj_ambient_chill_room.rb +124 -0
- data/examples/vj_dnb_jungle.rb +170 -0
- data/examples/vj_festival_mainstage.rb +245 -0
- data/examples/vj_festival_mainstage.yml +17 -0
- data/examples/vj_glitch_industrial.rb +164 -0
- data/examples/vj_hiphop_cipher.rb +167 -0
- data/examples/vj_jpop_idol_live.rb +210 -0
- data/examples/vj_synthwave_retro.rb +173 -0
- data/examples/vj_techno_warehouse.rb +195 -0
- data/frontend/index.html +494 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +1060 -16
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +194 -0
- data/frontend/src/performance-monitor.js +183 -0
- data/frontend/src/plugin-runtime.js +130 -0
- data/frontend/src/projector-mode.js +56 -0
- data/frontend/src/renderer/engine.js +157 -3
- data/frontend/src/renderer/layer-manager.js +442 -30
- data/frontend/src/renderer/shader-manager.js +26 -0
- data/frontend/src/runtime-control-preset.js +11 -0
- data/frontend/src/shader-error-overlay.js +29 -0
- data/frontend/src/shader-param-controls.js +93 -0
- data/frontend/src/shaders/builtins.js +380 -2
- data/frontend/src/shaders/post-effects.js +52 -0
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +666 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/frontend/src/visuals/text-renderer.js +112 -11
- data/frontend/src/websocket-client.js +12 -1
- data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
- data/lib/vizcore/analysis/beat_detector.rb +4 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
- data/lib/vizcore/analysis/feature_recorder.rb +159 -0
- data/lib/vizcore/analysis/feature_replay.rb +84 -0
- data/lib/vizcore/analysis/pipeline.rb +235 -11
- data/lib/vizcore/analysis/tap_tempo.rb +74 -0
- data/lib/vizcore/analysis.rb +4 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
- data/lib/vizcore/audio/fixture_input.rb +65 -0
- data/lib/vizcore/audio/input_manager.rb +4 -2
- data/lib/vizcore/audio/mic_input.rb +24 -8
- data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/doctor.rb +159 -0
- data/lib/vizcore/cli/dsl_reference.rb +99 -0
- data/lib/vizcore/cli/layer_docs.rb +46 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
- data/lib/vizcore/cli/scene_inspector.rb +136 -0
- data/lib/vizcore/cli/scene_validator.rb +337 -0
- data/lib/vizcore/cli/shader_template.rb +68 -0
- data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
- data/lib/vizcore/cli.rb +689 -18
- data/lib/vizcore/config.rb +103 -2
- data/lib/vizcore/control_preset.rb +68 -0
- data/lib/vizcore/dsl/engine.rb +277 -5
- data/lib/vizcore/dsl/layer_builder.rb +1280 -23
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
- data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
- data/lib/vizcore/dsl/reaction_builder.rb +44 -0
- data/lib/vizcore/dsl/scene_builder.rb +61 -5
- data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
- data/lib/vizcore/dsl/style_builder.rb +68 -0
- data/lib/vizcore/dsl/timeline_builder.rb +138 -0
- data/lib/vizcore/dsl/transition_controller.rb +77 -0
- data/lib/vizcore/dsl.rb +5 -1
- data/lib/vizcore/layer_catalog.rb +275 -0
- data/lib/vizcore/project_manifest.rb +152 -0
- data/lib/vizcore/renderer/png_writer.rb +57 -0
- data/lib/vizcore/renderer/render_sequence.rb +153 -0
- data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
- data/lib/vizcore/renderer/scene_serializer.rb +36 -3
- data/lib/vizcore/renderer/snapshot.rb +38 -0
- data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +143 -8
- data/lib/vizcore/server/gallery_app.rb +155 -0
- data/lib/vizcore/server/gallery_page.rb +100 -0
- data/lib/vizcore/server/gallery_runner.rb +48 -0
- data/lib/vizcore/server/rack_app.rb +203 -4
- data/lib/vizcore/server/runner.rb +391 -22
- data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
- data/lib/vizcore/server/websocket_handler.rb +60 -10
- data/lib/vizcore/server.rb +4 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/sync/osc_message.rb +103 -0
- data/lib/vizcore/sync/osc_receiver.rb +68 -0
- data/lib/vizcore/sync.rb +4 -0
- data/lib/vizcore/templates/midi_control_scene.rb +3 -1
- data/lib/vizcore/templates/plugin_layer.rb +20 -0
- data/lib/vizcore/templates/plugin_readme.md +23 -0
- data/lib/vizcore/templates/plugin_renderer.js +43 -0
- data/lib/vizcore/templates/plugin_scene.rb +14 -0
- data/lib/vizcore/templates/project_readme.md +7 -23
- data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +28 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +461 -0
- metadata +94 -3
- data/docs/GETTING_STARTED.md +0 -105
data/lib/vizcore/cli.rb
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "net/http"
|
|
4
5
|
require "pathname"
|
|
5
6
|
require "thor"
|
|
6
7
|
require_relative "../vizcore"
|
|
8
|
+
require_relative "analysis"
|
|
7
9
|
require_relative "audio"
|
|
10
|
+
require_relative "cli/doctor"
|
|
11
|
+
require_relative "cli/dsl_reference"
|
|
12
|
+
require_relative "cli/layer_docs"
|
|
13
|
+
require_relative "cli/scene_diagnostics"
|
|
14
|
+
require_relative "cli/shader_template"
|
|
15
|
+
require_relative "cli/shader_uniform_docs"
|
|
8
16
|
require_relative "config"
|
|
17
|
+
require_relative "project_manifest"
|
|
9
18
|
require_relative "server"
|
|
10
19
|
|
|
11
20
|
module Vizcore
|
|
@@ -22,48 +31,196 @@ module Vizcore
|
|
|
22
31
|
|
|
23
32
|
default_command :help
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
SCAFFOLD_TEMPLATES = {
|
|
35
|
+
"standard" => {
|
|
36
|
+
label: "standard",
|
|
37
|
+
start_scene: "scenes/basic.rb",
|
|
38
|
+
files: [
|
|
39
|
+
["basic_scene.rb", "scenes/basic.rb", "Minimal wireframe starter"],
|
|
40
|
+
["intro_drop_scene.rb", "scenes/intro_drop.rb", "Transition flow with beat trigger"],
|
|
41
|
+
["midi_control_scene.rb", "scenes/midi_control.rb", "MIDI note/CC mapping example"],
|
|
42
|
+
["custom_shader_scene.rb", "scenes/custom_shader.rb", "Custom GLSL + post/VJ effect example"],
|
|
43
|
+
["custom_wave.frag", "shaders/custom_wave.frag", "Custom GLSL fragment shader"]
|
|
44
|
+
],
|
|
45
|
+
notes: [
|
|
46
|
+
"`scenes/custom_shader.rb` references `shaders/custom_wave.frag`.",
|
|
47
|
+
"Use `vizcore devices midi` before running `scenes/midi_control.rb`."
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"minimal" => {
|
|
51
|
+
label: "minimal",
|
|
52
|
+
start_scene: "scenes/basic.rb",
|
|
53
|
+
files: [
|
|
54
|
+
["basic_scene.rb", "scenes/basic.rb", "Minimal wireframe starter"]
|
|
55
|
+
],
|
|
56
|
+
notes: []
|
|
57
|
+
},
|
|
58
|
+
"shader" => {
|
|
59
|
+
label: "shader",
|
|
60
|
+
start_scene: "scenes/custom_shader.rb",
|
|
61
|
+
files: [
|
|
62
|
+
["custom_shader_scene.rb", "scenes/custom_shader.rb", "Custom GLSL + post/VJ effect example"],
|
|
63
|
+
["custom_wave.frag", "shaders/custom_wave.frag", "Custom GLSL fragment shader"]
|
|
64
|
+
],
|
|
65
|
+
notes: [
|
|
66
|
+
"`scenes/custom_shader.rb` references `shaders/custom_wave.frag`."
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"midi" => {
|
|
70
|
+
label: "midi",
|
|
71
|
+
start_scene: "scenes/midi_control.rb",
|
|
72
|
+
files: [
|
|
73
|
+
["midi_control_scene.rb", "scenes/midi_control.rb", "MIDI note/CC mapping example"]
|
|
74
|
+
],
|
|
75
|
+
notes: [
|
|
76
|
+
"Run `vizcore devices midi` before starting the MIDI scene."
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"live-set" => {
|
|
80
|
+
label: "live-set",
|
|
81
|
+
start_scene: "scenes/live_set.rb",
|
|
82
|
+
files: [
|
|
83
|
+
["intro_drop_scene.rb", "scenes/live_set.rb", "Two-scene transition flow with beat trigger"]
|
|
84
|
+
],
|
|
85
|
+
notes: [
|
|
86
|
+
"Use file audio or a microphone input with clear beats for transition triggers."
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"rubykaigi" => {
|
|
90
|
+
label: "rubykaigi",
|
|
91
|
+
start_scene: "scenes/rubykaigi.rb",
|
|
92
|
+
files: [
|
|
93
|
+
["rubykaigi_scene.rb", "scenes/rubykaigi.rb", "Ruby conference visual starter"]
|
|
94
|
+
],
|
|
95
|
+
notes: [
|
|
96
|
+
"This scene uses Ruby-red text and audio-reactive geometry for talk or event visuals."
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
}.freeze
|
|
100
|
+
|
|
101
|
+
PLUGIN_SCAFFOLD_FILES = [
|
|
102
|
+
["plugin_readme.md", "README.md"],
|
|
103
|
+
["plugin_layer.rb", "lib/{{plugin_name}}.rb"],
|
|
104
|
+
["plugin_renderer.js", "frontend/{{plugin_name}}-renderer.js"],
|
|
105
|
+
["plugin_scene.rb", "examples/{{plugin_name}}_scene.rb"]
|
|
106
|
+
].freeze
|
|
107
|
+
DEFAULT_CAPTURE_PORT = 4579
|
|
108
|
+
|
|
109
|
+
desc "start [SCENE_FILE]", "Start vizcore HTTP/WebSocket server"
|
|
110
|
+
option :manifest, type: :string, desc: "Project manifest YAML path"
|
|
111
|
+
option :profile, type: :string, desc: "Project manifest profile name"
|
|
26
112
|
option :host, type: :string, default: Config::DEFAULT_HOST, desc: "Bind host"
|
|
27
113
|
option :port, type: :numeric, default: Config::DEFAULT_PORT, desc: "Bind port"
|
|
28
|
-
option :audio_source, type: :string,
|
|
114
|
+
option :audio_source, type: :string, desc: "Audio source: mic, file, dummy"
|
|
29
115
|
option :audio_file, type: :string, desc: "Path to audio file used when --audio-source file (wav/mp3/flac)"
|
|
116
|
+
option :audio_device, type: :string, desc: "Audio input device index or name used when --audio-source mic"
|
|
117
|
+
option :feature_file, type: :string, desc: "Replay recorded feature JSON instead of live audio analysis"
|
|
118
|
+
option :control_preset, type: :string, desc: "Control preset JSON for browser HUD and MIDI learn"
|
|
119
|
+
option :noise_gate, type: :numeric, default: Config::DEFAULT_NOISE_GATE, desc: "RMS level below which audio is treated as silence"
|
|
120
|
+
option :bpm, type: :numeric, desc: "Fixed BPM value used with --bpm-lock"
|
|
121
|
+
option :bpm_lock, type: :boolean, default: false, desc: "Lock analysis BPM output to --bpm"
|
|
122
|
+
option :osc_port, type: :numeric, desc: "UDP port for OSC sync (/vizcore/scene, /vizcore/tap)"
|
|
123
|
+
option :reload, type: :boolean, default: Config::DEFAULT_RELOAD, desc: "Reload the scene file when it changes"
|
|
124
|
+
option :projector, type: :boolean, default: false, desc: "Hide browser operator UI for projection output"
|
|
30
125
|
# Start the Vizcore server with the given scene file.
|
|
31
126
|
#
|
|
32
127
|
# @param scene_file [String] path to a Ruby scene DSL file
|
|
33
128
|
# @raise [Thor::Error] when CLI arguments are invalid
|
|
34
129
|
# @return [void]
|
|
35
|
-
def start(scene_file)
|
|
130
|
+
def start(scene_file = nil)
|
|
131
|
+
manifest = load_project_manifest(options[:manifest])
|
|
132
|
+
profile = options[:profile]
|
|
133
|
+
load_manifest_plugins(manifest, profile: profile)
|
|
134
|
+
defaults = manifest&.config_defaults(profile: profile) || {}
|
|
36
135
|
config = Config.new(
|
|
37
|
-
scene_file: scene_file,
|
|
136
|
+
scene_file: scene_file || defaults[:scene_file],
|
|
38
137
|
host: options.fetch(:host),
|
|
39
138
|
port: options.fetch(:port),
|
|
40
|
-
audio_source: options
|
|
41
|
-
audio_file: options[:audio_file]
|
|
139
|
+
audio_source: options[:audio_source] || defaults[:audio_source] || Config::DEFAULT_AUDIO_SOURCE,
|
|
140
|
+
audio_file: options[:audio_file] || defaults[:audio_file],
|
|
141
|
+
audio_device: options[:audio_device] || defaults[:audio_device],
|
|
142
|
+
feature_file: options[:feature_file] || defaults[:feature_file],
|
|
143
|
+
control_preset: options[:control_preset] || defaults[:control_preset],
|
|
144
|
+
plugin_assets: defaults[:plugin_assets],
|
|
145
|
+
noise_gate: options.fetch(:noise_gate),
|
|
146
|
+
bpm: options[:bpm],
|
|
147
|
+
bpm_lock: options.fetch(:bpm_lock),
|
|
148
|
+
osc_port: options[:osc_port] || defaults[:osc_port],
|
|
149
|
+
reload: options.fetch(:reload),
|
|
150
|
+
projector_mode: options.fetch(:projector)
|
|
42
151
|
)
|
|
43
152
|
Server::Runner.new(config).run
|
|
44
153
|
rescue ArgumentError => e
|
|
45
154
|
raise Thor::Error, e.message
|
|
46
155
|
end
|
|
47
156
|
|
|
157
|
+
desc "demo", "Start the bundled audio-reactive demo"
|
|
158
|
+
option :host, type: :string, default: Config::DEFAULT_HOST, desc: "Bind host"
|
|
159
|
+
option :port, type: :numeric, default: Config::DEFAULT_PORT, desc: "Bind port"
|
|
160
|
+
option :noise_gate, type: :numeric, default: Config::DEFAULT_NOISE_GATE, desc: "RMS level below which audio is treated as silence"
|
|
161
|
+
option :bpm, type: :numeric, desc: "Fixed BPM value used with --bpm-lock"
|
|
162
|
+
option :bpm_lock, type: :boolean, default: false, desc: "Lock analysis BPM output to --bpm"
|
|
163
|
+
option :control_preset, type: :string, desc: "Control preset JSON for browser HUD and MIDI learn"
|
|
164
|
+
option :osc_port, type: :numeric, desc: "UDP port for OSC sync (/vizcore/scene, /vizcore/tap)"
|
|
165
|
+
option :projector, type: :boolean, default: false, desc: "Hide browser operator UI for projection output"
|
|
166
|
+
# Start a bundled scene with bundled audio for first-run verification.
|
|
167
|
+
#
|
|
168
|
+
# @return [void]
|
|
169
|
+
def demo
|
|
170
|
+
config = Config.new(
|
|
171
|
+
scene_file: Vizcore.root.join("examples", "rhythm_geometry.rb"),
|
|
172
|
+
host: options.fetch(:host),
|
|
173
|
+
port: options.fetch(:port),
|
|
174
|
+
audio_source: :file,
|
|
175
|
+
audio_file: Vizcore.root.join("examples", "assets", "complex_demo_loop.wav"),
|
|
176
|
+
noise_gate: options.fetch(:noise_gate),
|
|
177
|
+
bpm: options[:bpm],
|
|
178
|
+
bpm_lock: options.fetch(:bpm_lock),
|
|
179
|
+
control_preset: options[:control_preset],
|
|
180
|
+
osc_port: options[:osc_port],
|
|
181
|
+
projector_mode: options.fetch(:projector)
|
|
182
|
+
)
|
|
183
|
+
Server::Runner.new(config).run
|
|
184
|
+
rescue ArgumentError => e
|
|
185
|
+
raise Thor::Error, e.message
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
desc "gallery", "Start the bundled example gallery"
|
|
189
|
+
option :host, type: :string, default: Config::DEFAULT_HOST, desc: "Bind host"
|
|
190
|
+
option :port, type: :numeric, default: Vizcore::Server::GalleryRunner::DEFAULT_PORT, desc: "Bind port"
|
|
191
|
+
# Start a browser gallery for bundled example scenes.
|
|
192
|
+
#
|
|
193
|
+
# @return [void]
|
|
194
|
+
def gallery
|
|
195
|
+
Vizcore::Server::GalleryRunner.new(
|
|
196
|
+
host: options.fetch(:host),
|
|
197
|
+
port: options.fetch(:port)
|
|
198
|
+
).run
|
|
199
|
+
end
|
|
200
|
+
|
|
48
201
|
desc "new NAME", "Create a starter project scaffold"
|
|
202
|
+
option :template,
|
|
203
|
+
type: :string,
|
|
204
|
+
default: "standard",
|
|
205
|
+
desc: "Scaffold template: standard, minimal, shader, midi, live-set, rubykaigi"
|
|
49
206
|
# Generate a new Vizcore project scaffold.
|
|
50
207
|
#
|
|
51
208
|
# @param name [String] directory name for the new project
|
|
52
209
|
# @return [void]
|
|
53
210
|
def new(name)
|
|
211
|
+
scaffold = scaffold_template(options.fetch(:template))
|
|
54
212
|
root = Pathname.new(name).expand_path
|
|
55
|
-
FileUtils.mkdir_p(root
|
|
56
|
-
FileUtils.mkdir_p(root.join("shaders"))
|
|
213
|
+
FileUtils.mkdir_p(root)
|
|
57
214
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
write_template("custom_shader_scene.rb", root.join("scenes", "custom_shader.rb"), project_name: name)
|
|
63
|
-
write_template("custom_wave.frag", root.join("shaders", "custom_wave.frag"), project_name: name)
|
|
215
|
+
write_project_readme(root.join("README.md"), project_name: name, scaffold: scaffold)
|
|
216
|
+
scaffold.fetch(:files).each do |template_name, destination, _description|
|
|
217
|
+
write_template(template_name, root.join(destination), project_name: name)
|
|
218
|
+
end
|
|
64
219
|
|
|
65
|
-
say("Created project scaffold: #{root}")
|
|
66
|
-
say("Next: cd #{name} && vizcore start
|
|
220
|
+
say("Created project scaffold (#{scaffold.fetch(:label)}): #{root}")
|
|
221
|
+
say("Next: cd #{name} && vizcore start #{scaffold.fetch(:start_scene)}")
|
|
222
|
+
rescue ArgumentError => e
|
|
223
|
+
raise Thor::Error, e.message
|
|
67
224
|
end
|
|
68
225
|
|
|
69
226
|
desc "devices [TYPE]", "Show available devices (audio or midi)"
|
|
@@ -86,14 +243,528 @@ module Vizcore
|
|
|
86
243
|
end
|
|
87
244
|
end
|
|
88
245
|
|
|
246
|
+
desc "doctor", "Check local dependencies and device availability"
|
|
247
|
+
# Print local environment checks for Vizcore runtime dependencies.
|
|
248
|
+
#
|
|
249
|
+
# @raise [Thor::Error] when a required check fails
|
|
250
|
+
# @return [void]
|
|
251
|
+
def doctor
|
|
252
|
+
report = Vizcore::CLISupport::Doctor.new.call
|
|
253
|
+
report.checks.each do |check|
|
|
254
|
+
say("#{status_label(check.status)} #{check.name}: #{check.message}")
|
|
255
|
+
end
|
|
256
|
+
raise Thor::Error, "vizcore doctor found required failures" if report.failure?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
map "inspect" => :inspect_scene
|
|
260
|
+
desc "inspect SCENE_FILE", "Print scenes, layers, mappings, and transitions"
|
|
261
|
+
# Load a scene DSL file and print its runtime structure.
|
|
262
|
+
#
|
|
263
|
+
# @param scene_file [String] path to a Ruby scene DSL file
|
|
264
|
+
# @raise [Thor::Error] when scene loading fails
|
|
265
|
+
# @return [void]
|
|
266
|
+
def inspect_scene(scene_file)
|
|
267
|
+
diagnostics = Vizcore::CLISupport::SceneDiagnostics.new(scene_file: scene_file)
|
|
268
|
+
result = diagnostics.validate
|
|
269
|
+
print_issues(result.issues)
|
|
270
|
+
raise Thor::Error, "scene inspection failed" unless result.definition
|
|
271
|
+
|
|
272
|
+
diagnostics.inspect_lines(result.definition).each { |line| say(line) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
desc "validate SCENE_FILE", "Validate a scene DSL file"
|
|
276
|
+
# Load and validate a scene DSL file without starting the server.
|
|
277
|
+
#
|
|
278
|
+
# @param scene_file [String] path to a Ruby scene DSL file
|
|
279
|
+
# @raise [Thor::Error] when validation fails
|
|
280
|
+
# @return [void]
|
|
281
|
+
def validate(scene_file)
|
|
282
|
+
result = Vizcore::CLISupport::SceneDiagnostics.new(scene_file: scene_file).validate
|
|
283
|
+
print_issues(result.issues)
|
|
284
|
+
raise Thor::Error, "scene validation failed" unless result.valid?
|
|
285
|
+
|
|
286
|
+
say("Scene valid: #{scene_file}")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
desc "layers", "Print built-in layer capability metadata"
|
|
290
|
+
# Print supported layer types, params, and browser-side capabilities.
|
|
291
|
+
#
|
|
292
|
+
# @return [void]
|
|
293
|
+
def layers
|
|
294
|
+
Vizcore::CLISupport::LayerDocs.new.lines.each { |line| say(line) }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
map "dsl-docs" => :dsl_docs
|
|
298
|
+
desc "dsl-docs", "Print generated Ruby DSL reference"
|
|
299
|
+
# Print generated documentation for the Ruby scene DSL.
|
|
300
|
+
#
|
|
301
|
+
# @return [void]
|
|
302
|
+
def dsl_docs
|
|
303
|
+
Vizcore::CLISupport::DslReference.new.lines.each { |line| say(line) }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
map "shader-docs" => :shader_docs
|
|
307
|
+
desc "shader-docs", "Print custom GLSL shader uniform reference"
|
|
308
|
+
# Print generated documentation for custom GLSL uniforms.
|
|
309
|
+
#
|
|
310
|
+
# @return [void]
|
|
311
|
+
def shader_docs
|
|
312
|
+
Vizcore::CLISupport::ShaderUniformDocs.new.lines.each { |line| say(line) }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
desc "shader COMMAND [NAME]", "Manage custom GLSL shader helpers"
|
|
316
|
+
option :out, type: :string, desc: "Output path for `shader new`"
|
|
317
|
+
# Run custom shader helper commands.
|
|
318
|
+
#
|
|
319
|
+
# @param command [String]
|
|
320
|
+
# @param name [String, nil]
|
|
321
|
+
# @raise [Thor::Error] when the subcommand or arguments are invalid
|
|
322
|
+
# @return [void]
|
|
323
|
+
def shader(command = nil, name = nil)
|
|
324
|
+
case command.to_s
|
|
325
|
+
when "new"
|
|
326
|
+
create_shader_template(name)
|
|
327
|
+
else
|
|
328
|
+
raise Thor::Error, "Unknown shader command: #{command || '(nil)'}. Use `vizcore shader new NAME`."
|
|
329
|
+
end
|
|
330
|
+
rescue ArgumentError => e
|
|
331
|
+
raise Thor::Error, e.message
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
desc "plugin COMMAND [NAME]", "Manage Vizcore plugin helpers"
|
|
335
|
+
option :out, type: :string, desc: "Output directory for `plugin new`"
|
|
336
|
+
# Run plugin helper commands.
|
|
337
|
+
#
|
|
338
|
+
# @param command [String, nil]
|
|
339
|
+
# @param name [String, nil]
|
|
340
|
+
# @raise [Thor::Error] when the subcommand or arguments are invalid
|
|
341
|
+
# @return [void]
|
|
342
|
+
def plugin(command = nil, name = nil)
|
|
343
|
+
case command.to_s
|
|
344
|
+
when "new"
|
|
345
|
+
create_plugin_scaffold(name)
|
|
346
|
+
else
|
|
347
|
+
raise Thor::Error, "Unknown plugin command: #{command || '(nil)'}. Use `vizcore plugin new NAME`."
|
|
348
|
+
end
|
|
349
|
+
rescue ArgumentError => e
|
|
350
|
+
raise Thor::Error, e.message
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
map "browser-capture" => :browser_capture
|
|
354
|
+
desc "browser-capture URL", "Capture a browser-rendered Vizcore canvas to PNG"
|
|
355
|
+
option :out, type: :string, default: "browser-capture.png", desc: "Output PNG path"
|
|
356
|
+
option :selector, type: :string, default: "#vizcore-canvas", desc: "Element selector to capture"
|
|
357
|
+
option :wait, type: :numeric, default: 1000, desc: "Milliseconds to wait after page load"
|
|
358
|
+
option :width, type: :numeric, default: 1280, desc: "Browser viewport width"
|
|
359
|
+
option :height, type: :numeric, default: 720, desc: "Browser viewport height"
|
|
360
|
+
# Capture browser-rendered output from a running Vizcore server.
|
|
361
|
+
#
|
|
362
|
+
# @param url [String]
|
|
363
|
+
# @raise [Thor::Error] when Playwright capture fails
|
|
364
|
+
# @return [void]
|
|
365
|
+
def browser_capture(url)
|
|
366
|
+
run_browser_capture(
|
|
367
|
+
url,
|
|
368
|
+
out: options.fetch(:out),
|
|
369
|
+
selector: options.fetch(:selector),
|
|
370
|
+
wait: options.fetch(:wait),
|
|
371
|
+
width: options.fetch(:width),
|
|
372
|
+
height: options.fetch(:height)
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
desc "capture SCENE_FILE", "Start a temporary server and capture the browser-rendered canvas"
|
|
377
|
+
option :host, type: :string, default: Config::DEFAULT_HOST, desc: "Temporary server host"
|
|
378
|
+
option :port, type: :numeric, default: DEFAULT_CAPTURE_PORT, desc: "Temporary server port"
|
|
379
|
+
option :audio_source, type: :string, default: "dummy", desc: "Audio source: dummy, file, mic"
|
|
380
|
+
option :audio_file, type: :string, desc: "Path to audio file used when --audio-source file"
|
|
381
|
+
option :feature_file, type: :string, desc: "Replay recorded feature JSON instead of live audio analysis"
|
|
382
|
+
option :control_preset, type: :string, desc: "Control preset JSON for browser HUD and MIDI learn"
|
|
383
|
+
option :out, type: :string, default: "browser-capture.png", desc: "Output PNG path"
|
|
384
|
+
option :selector, type: :string, default: "#vizcore-canvas", desc: "Element selector to capture"
|
|
385
|
+
option :wait, type: :numeric, default: 1000, desc: "Milliseconds to wait after page load"
|
|
386
|
+
option :timeout, type: :numeric, default: 10, desc: "Seconds to wait for the temporary server"
|
|
387
|
+
option :width, type: :numeric, default: 1280, desc: "Browser viewport width"
|
|
388
|
+
option :height, type: :numeric, default: 720, desc: "Browser viewport height"
|
|
389
|
+
# Start Vizcore and capture a browser-rendered canvas from the projector route.
|
|
390
|
+
#
|
|
391
|
+
# @param scene_file [String]
|
|
392
|
+
# @raise [Thor::Error] when server startup or capture fails
|
|
393
|
+
# @return [void]
|
|
394
|
+
def capture(scene_file)
|
|
395
|
+
config = Config.new(
|
|
396
|
+
scene_file: scene_file,
|
|
397
|
+
host: options.fetch(:host),
|
|
398
|
+
port: options.fetch(:port),
|
|
399
|
+
audio_source: options.fetch(:audio_source),
|
|
400
|
+
audio_file: options[:audio_file],
|
|
401
|
+
feature_file: options[:feature_file],
|
|
402
|
+
control_preset: options[:control_preset],
|
|
403
|
+
reload: false,
|
|
404
|
+
projector_mode: true
|
|
405
|
+
)
|
|
406
|
+
validate_snapshot_config!(config)
|
|
407
|
+
|
|
408
|
+
pid = Kernel.spawn(*temporary_server_command(config), out: File::NULL, err: File::NULL)
|
|
409
|
+
begin
|
|
410
|
+
wait_for_http("http://#{config.host}:#{config.port}/health", timeout: options.fetch(:timeout))
|
|
411
|
+
run_browser_capture(
|
|
412
|
+
"http://#{config.host}:#{config.port}/projector",
|
|
413
|
+
out: options.fetch(:out),
|
|
414
|
+
selector: options.fetch(:selector),
|
|
415
|
+
wait: options.fetch(:wait),
|
|
416
|
+
width: options.fetch(:width),
|
|
417
|
+
height: options.fetch(:height)
|
|
418
|
+
)
|
|
419
|
+
ensure
|
|
420
|
+
stop_temporary_server(pid)
|
|
421
|
+
end
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
raise Thor::Error, e.message
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
desc "snapshot SCENE_FILE", "Render one scene frame to a PNG snapshot"
|
|
427
|
+
option :audio_source, type: :string, default: "dummy", desc: "Audio source: dummy, file, mic"
|
|
428
|
+
option :audio_file, type: :string, desc: "Path to audio file used when --audio-source file"
|
|
429
|
+
option :audio_device, type: :string, desc: "Audio input device index or name used when --audio-source mic"
|
|
430
|
+
option :noise_gate, type: :numeric, default: Config::DEFAULT_NOISE_GATE, desc: "RMS level below which audio is treated as silence"
|
|
431
|
+
option :bpm, type: :numeric, desc: "Fixed BPM value used with --bpm-lock"
|
|
432
|
+
option :bpm_lock, type: :boolean, default: false, desc: "Lock analysis BPM output to --bpm"
|
|
433
|
+
option :out, type: :string, default: "snapshot.png", desc: "Output PNG path"
|
|
434
|
+
option :width, type: :numeric, default: Vizcore::Renderer::SnapshotRenderer::DEFAULT_WIDTH, desc: "Snapshot width"
|
|
435
|
+
option :height, type: :numeric, default: Vizcore::Renderer::SnapshotRenderer::DEFAULT_HEIGHT, desc: "Snapshot height"
|
|
436
|
+
# Load a scene DSL file and write a software-rendered PNG preview.
|
|
437
|
+
#
|
|
438
|
+
# @param scene_file [String] path to a Ruby scene DSL file
|
|
439
|
+
# @raise [Thor::Error] when scene loading or snapshot writing fails
|
|
440
|
+
# @return [void]
|
|
441
|
+
def snapshot(scene_file)
|
|
442
|
+
config = Config.new(
|
|
443
|
+
scene_file: scene_file,
|
|
444
|
+
audio_source: options.fetch(:audio_source),
|
|
445
|
+
audio_file: options[:audio_file],
|
|
446
|
+
audio_device: options[:audio_device],
|
|
447
|
+
noise_gate: options.fetch(:noise_gate),
|
|
448
|
+
bpm: options[:bpm],
|
|
449
|
+
bpm_lock: options.fetch(:bpm_lock)
|
|
450
|
+
)
|
|
451
|
+
validate_snapshot_config!(config)
|
|
452
|
+
|
|
453
|
+
result = Vizcore::Renderer::Snapshot.new(
|
|
454
|
+
config: config,
|
|
455
|
+
width: options.fetch(:width),
|
|
456
|
+
height: options.fetch(:height)
|
|
457
|
+
).write(out: options.fetch(:out))
|
|
458
|
+
say("Snapshot written: #{result[:path]} (scene=#{result[:scene]}, #{result[:width]}x#{result[:height]})")
|
|
459
|
+
rescue StandardError => e
|
|
460
|
+
raise Thor::Error, e.message
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
desc "render SCENE_FILE", "Render a PNG image sequence or MP4 video for a scene"
|
|
464
|
+
option :audio_source, type: :string, default: "dummy", desc: "Audio source: dummy, file, mic"
|
|
465
|
+
option :audio_file, type: :string, desc: "Path to audio file used when --audio-source file"
|
|
466
|
+
option :audio_device, type: :string, desc: "Audio input device index or name used when --audio-source mic"
|
|
467
|
+
option :noise_gate, type: :numeric, default: Config::DEFAULT_NOISE_GATE, desc: "RMS level below which audio is treated as silence"
|
|
468
|
+
option :bpm, type: :numeric, desc: "Fixed BPM value used with --bpm-lock"
|
|
469
|
+
option :bpm_lock, type: :boolean, default: false, desc: "Lock analysis BPM output to --bpm"
|
|
470
|
+
option :out, type: :string, default: "frames", desc: "Output directory for PNG frames, or .mp4 video path"
|
|
471
|
+
option :frames, type: :numeric, default: Vizcore::Renderer::RenderSequence::DEFAULT_FRAME_COUNT, desc: "Number of frames to write"
|
|
472
|
+
option :fps, type: :numeric, default: Vizcore::Renderer::RenderSequence::DEFAULT_FRAME_RATE, desc: "Render frame rate"
|
|
473
|
+
option :width, type: :numeric, default: Vizcore::Renderer::SnapshotRenderer::DEFAULT_WIDTH, desc: "Frame width"
|
|
474
|
+
option :height, type: :numeric, default: Vizcore::Renderer::SnapshotRenderer::DEFAULT_HEIGHT, desc: "Frame height"
|
|
475
|
+
# Load a scene DSL file and write a software-rendered PNG image sequence or MP4.
|
|
476
|
+
#
|
|
477
|
+
# @param scene_file [String] path to a Ruby scene DSL file
|
|
478
|
+
# @raise [Thor::Error] when scene loading or frame writing fails
|
|
479
|
+
# @return [void]
|
|
480
|
+
def render(scene_file)
|
|
481
|
+
config = Config.new(
|
|
482
|
+
scene_file: scene_file,
|
|
483
|
+
audio_source: options.fetch(:audio_source),
|
|
484
|
+
audio_file: options[:audio_file],
|
|
485
|
+
audio_device: options[:audio_device],
|
|
486
|
+
noise_gate: options.fetch(:noise_gate),
|
|
487
|
+
bpm: options[:bpm],
|
|
488
|
+
bpm_lock: options.fetch(:bpm_lock)
|
|
489
|
+
)
|
|
490
|
+
validate_snapshot_config!(config)
|
|
491
|
+
|
|
492
|
+
result = Vizcore::Renderer::RenderSequence.new(
|
|
493
|
+
config: config,
|
|
494
|
+
frames: options.fetch(:frames),
|
|
495
|
+
fps: options.fetch(:fps),
|
|
496
|
+
width: options.fetch(:width),
|
|
497
|
+
height: options.fetch(:height)
|
|
498
|
+
).write(out: options.fetch(:out))
|
|
499
|
+
return say(render_video_message(result)) if result[:format] == :mp4
|
|
500
|
+
|
|
501
|
+
say(
|
|
502
|
+
"Frames written: #{result[:path]} " \
|
|
503
|
+
"(scene=#{result[:scene]}, frames=#{result[:frames]}, fps=#{result[:fps]}, #{result[:width]}x#{result[:height]})"
|
|
504
|
+
)
|
|
505
|
+
rescue StandardError => e
|
|
506
|
+
raise Thor::Error, e.message
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
map "record-features" => :record_features
|
|
510
|
+
desc "record-features AUDIO_FILE", "Record audio analysis features to JSON"
|
|
511
|
+
option :out, type: :string, default: "features.json", desc: "Output JSON path"
|
|
512
|
+
option :frames, type: :numeric, default: Vizcore::Analysis::FeatureRecorder::DEFAULT_FRAME_COUNT, desc: "Number of analysis frames to record"
|
|
513
|
+
option :fps, type: :numeric, default: Vizcore::Analysis::FeatureRecorder::DEFAULT_FRAME_RATE, desc: "Analysis frame rate"
|
|
514
|
+
option :noise_gate, type: :numeric, default: Config::DEFAULT_NOISE_GATE, desc: "RMS level below which audio is treated as silence"
|
|
515
|
+
option :audio_normalize, type: :boolean, default: false, desc: "Apply adaptive feature normalization"
|
|
516
|
+
option :bpm, type: :numeric, desc: "Fixed BPM value used with --bpm-lock"
|
|
517
|
+
option :bpm_lock, type: :boolean, default: false, desc: "Lock analysis BPM output to --bpm"
|
|
518
|
+
# Analyze an audio file and persist feature frames as JSON.
|
|
519
|
+
#
|
|
520
|
+
# @param audio_file [String] path to WAV/MP3/FLAC audio file
|
|
521
|
+
# @raise [Thor::Error] when audio loading or JSON writing fails
|
|
522
|
+
# @return [void]
|
|
523
|
+
def record_features(audio_file)
|
|
524
|
+
result = Vizcore::Analysis::FeatureRecorder.new(
|
|
525
|
+
audio_file: audio_file,
|
|
526
|
+
frames: options.fetch(:frames),
|
|
527
|
+
fps: options.fetch(:fps),
|
|
528
|
+
noise_gate: options.fetch(:noise_gate),
|
|
529
|
+
audio_normalize: feature_audio_normalize_setting,
|
|
530
|
+
bpm: options[:bpm],
|
|
531
|
+
bpm_lock: options.fetch(:bpm_lock)
|
|
532
|
+
).write(out: options.fetch(:out))
|
|
533
|
+
say(
|
|
534
|
+
"Features written: #{result[:path]} " \
|
|
535
|
+
"(frames=#{result[:frames]}, fps=#{result[:fps]}, sample_rate=#{result[:sample_rate]})"
|
|
536
|
+
)
|
|
537
|
+
rescue StandardError => e
|
|
538
|
+
raise Thor::Error, e.message
|
|
539
|
+
end
|
|
540
|
+
|
|
89
541
|
private
|
|
90
542
|
|
|
91
|
-
def
|
|
543
|
+
def status_label(status)
|
|
544
|
+
case status
|
|
545
|
+
when :ok
|
|
546
|
+
"[ok]"
|
|
547
|
+
when :warn
|
|
548
|
+
"[warn]"
|
|
549
|
+
else
|
|
550
|
+
"[fail]"
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def print_issues(issues)
|
|
555
|
+
issues.each do |issue|
|
|
556
|
+
label = issue.error? ? "[error]" : "[warn]"
|
|
557
|
+
say("#{label} #{issue.message}")
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def validate_snapshot_config!(config)
|
|
562
|
+
raise ArgumentError, "Scene file not found: #{config.scene_file || '(nil)'}" unless config.scene_exists?
|
|
563
|
+
return unless config.audio_source == :file
|
|
564
|
+
return if config.audio_file&.file?
|
|
565
|
+
|
|
566
|
+
raise ArgumentError, "Audio file not found: #{config.audio_file || '(nil)'}"
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def create_shader_template(name)
|
|
570
|
+
raise ArgumentError, "shader name is required" if name.to_s.strip.empty?
|
|
571
|
+
|
|
572
|
+
destination = options[:out] || Vizcore::CLISupport::ShaderTemplate.default_path(name)
|
|
573
|
+
path = Vizcore::CLISupport::ShaderTemplate.new.write(destination)
|
|
574
|
+
say("Shader template written: #{path}")
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def write_template(template_name, destination, project_name: nil, replacements: {})
|
|
92
578
|
template_path = Vizcore.templates_root.join(template_name)
|
|
93
|
-
|
|
579
|
+
values = replacements.transform_keys(&:to_s)
|
|
580
|
+
values["{{project_name}}"] = project_name if project_name
|
|
581
|
+
body = template_path.read
|
|
582
|
+
values.each do |placeholder, value|
|
|
583
|
+
body = body.gsub(placeholder, value.to_s)
|
|
584
|
+
end
|
|
585
|
+
FileUtils.mkdir_p(destination.dirname)
|
|
586
|
+
destination.write(body)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def write_project_readme(destination, project_name:, scaffold:)
|
|
590
|
+
template_path = Vizcore.templates_root.join("project_readme.md")
|
|
591
|
+
body = template_path.read
|
|
592
|
+
.gsub("{{project_name}}", project_name)
|
|
593
|
+
.gsub("{{template_name}}", scaffold.fetch(:label))
|
|
594
|
+
.gsub("{{start_scene}}", scaffold.fetch(:start_scene))
|
|
595
|
+
.gsub("{{included_files}}", scaffold_files(scaffold))
|
|
596
|
+
.gsub("{{template_notes}}", scaffold_notes(scaffold))
|
|
597
|
+
FileUtils.mkdir_p(destination.dirname)
|
|
94
598
|
destination.write(body)
|
|
95
599
|
end
|
|
96
600
|
|
|
601
|
+
def scaffold_template(name)
|
|
602
|
+
key = name.to_s.strip.downcase
|
|
603
|
+
key = "standard" if key.empty? || key == "default"
|
|
604
|
+
scaffold = SCAFFOLD_TEMPLATES[key]
|
|
605
|
+
return scaffold if scaffold
|
|
606
|
+
|
|
607
|
+
raise ArgumentError, "Unknown template: #{name}. Use one of: #{SCAFFOLD_TEMPLATES.keys.join(', ')}"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def scaffold_files(scaffold)
|
|
611
|
+
scaffold.fetch(:files).map do |_template_name, destination, description|
|
|
612
|
+
"- `#{destination}`: #{description}"
|
|
613
|
+
end.join("\n")
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def scaffold_notes(scaffold)
|
|
617
|
+
notes = Array(scaffold[:notes])
|
|
618
|
+
return "No extra setup is required." if notes.empty?
|
|
619
|
+
|
|
620
|
+
notes.map { |note| "- #{note}" }.join("\n")
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def render_video_message(result)
|
|
624
|
+
"Video written: #{result[:path]} " \
|
|
625
|
+
"(scene=#{result[:scene]}, frames=#{result[:frames]}, fps=#{result[:fps]}, #{result[:width]}x#{result[:height]})"
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def run_browser_capture(url, out:, selector:, wait:, width:, height:)
|
|
629
|
+
script = Vizcore.root.join("scripts", "browser_capture.mjs")
|
|
630
|
+
command = [
|
|
631
|
+
"node",
|
|
632
|
+
script.to_s,
|
|
633
|
+
url.to_s,
|
|
634
|
+
"--out",
|
|
635
|
+
out.to_s,
|
|
636
|
+
"--selector",
|
|
637
|
+
selector.to_s,
|
|
638
|
+
"--wait",
|
|
639
|
+
wait.to_s,
|
|
640
|
+
"--width",
|
|
641
|
+
width.to_s,
|
|
642
|
+
"--height",
|
|
643
|
+
height.to_s
|
|
644
|
+
]
|
|
645
|
+
success = Kernel.system(*command)
|
|
646
|
+
raise Thor::Error, "browser capture failed" unless success
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def temporary_server_command(config)
|
|
650
|
+
command = [
|
|
651
|
+
Gem.ruby,
|
|
652
|
+
"-I#{Vizcore.root.join('lib')}",
|
|
653
|
+
Vizcore.root.join("exe", "vizcore").to_s,
|
|
654
|
+
"start",
|
|
655
|
+
config.scene_file.to_s,
|
|
656
|
+
"--host",
|
|
657
|
+
config.host,
|
|
658
|
+
"--port",
|
|
659
|
+
config.port.to_s,
|
|
660
|
+
"--audio-source",
|
|
661
|
+
config.audio_source.to_s,
|
|
662
|
+
"--no-reload",
|
|
663
|
+
"--projector"
|
|
664
|
+
]
|
|
665
|
+
command.concat(["--audio-file", config.audio_file.to_s]) if config.audio_file
|
|
666
|
+
command.concat(["--feature-file", config.feature_file.to_s]) if config.feature_file
|
|
667
|
+
command.concat(["--control-preset", config.control_preset.to_s]) if config.control_preset
|
|
668
|
+
command
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def wait_for_http(url, timeout:)
|
|
672
|
+
return true if Float(timeout) <= 0
|
|
673
|
+
|
|
674
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Float(timeout)
|
|
675
|
+
uri = URI(url)
|
|
676
|
+
loop do
|
|
677
|
+
response = Net::HTTP.get_response(uri)
|
|
678
|
+
return true if response.is_a?(Net::HTTPSuccess)
|
|
679
|
+
raise "Timed out waiting for #{url}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
680
|
+
|
|
681
|
+
sleep(0.1)
|
|
682
|
+
rescue StandardError
|
|
683
|
+
raise "Timed out waiting for #{url}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
684
|
+
|
|
685
|
+
sleep(0.1)
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def stop_temporary_server(pid)
|
|
690
|
+
Process.kill("TERM", pid)
|
|
691
|
+
Process.wait(pid)
|
|
692
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
|
693
|
+
nil
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def create_plugin_scaffold(name)
|
|
697
|
+
metadata = plugin_scaffold_metadata(name)
|
|
698
|
+
root = Pathname.new(options[:out] || metadata.fetch(:plugin_name)).expand_path
|
|
699
|
+
replacements = metadata.transform_keys { |key| "{{#{key}}}" }
|
|
700
|
+
|
|
701
|
+
FileUtils.mkdir_p(root)
|
|
702
|
+
PLUGIN_SCAFFOLD_FILES.each do |template_name, destination|
|
|
703
|
+
rendered_destination = destination.dup
|
|
704
|
+
replacements.each do |placeholder, value|
|
|
705
|
+
rendered_destination = rendered_destination.gsub(placeholder, value.to_s)
|
|
706
|
+
end
|
|
707
|
+
write_template(template_name, root.join(rendered_destination), replacements: replacements)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
say("Created plugin scaffold: #{root}")
|
|
711
|
+
say("Next: require_relative \"#{root.basename}/lib/#{metadata.fetch(:plugin_name)}\" in your scene")
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def plugin_scaffold_metadata(name)
|
|
715
|
+
raw_name = name.to_s.strip
|
|
716
|
+
raise ArgumentError, "plugin name is required" if raw_name.empty?
|
|
717
|
+
|
|
718
|
+
plugin_name = normalize_plugin_name(raw_name)
|
|
719
|
+
raise ArgumentError, "plugin name must contain letters or numbers" if plugin_name.empty?
|
|
720
|
+
|
|
721
|
+
module_name = plugin_module_name(plugin_name)
|
|
722
|
+
{
|
|
723
|
+
plugin_name: plugin_name,
|
|
724
|
+
plugin_module: module_name,
|
|
725
|
+
plugin_renderer: "render#{module_name}",
|
|
726
|
+
plugin_type: "#{plugin_name}_layer",
|
|
727
|
+
plugin_title: plugin_name.split("_").map(&:capitalize).join(" ")
|
|
728
|
+
}
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def normalize_plugin_name(name)
|
|
732
|
+
name.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
733
|
+
.tr("- ", "__")
|
|
734
|
+
.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
735
|
+
.gsub(/_+/, "_")
|
|
736
|
+
.downcase
|
|
737
|
+
.sub(/\A_+/, "")
|
|
738
|
+
.sub(/_+\z/, "")
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def plugin_module_name(plugin_name)
|
|
742
|
+
module_name = plugin_name.split("_").map(&:capitalize).join
|
|
743
|
+
module_name.match?(/\A[A-Z]/) ? module_name : "Plugin#{module_name}"
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def load_project_manifest(path)
|
|
747
|
+
return nil if path.to_s.strip.empty?
|
|
748
|
+
|
|
749
|
+
Vizcore::ProjectManifest.load(path)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def load_manifest_plugins(manifest, profile: nil)
|
|
753
|
+
return unless manifest
|
|
754
|
+
|
|
755
|
+
manifest.plugins(profile: profile).each do |plugin|
|
|
756
|
+
Vizcore.plugin(plugin)
|
|
757
|
+
rescue LoadError => e
|
|
758
|
+
raise Thor::Error, "Failed to load manifest plugin #{plugin}: #{e.message}"
|
|
759
|
+
end
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def feature_audio_normalize_setting
|
|
763
|
+
return nil unless options.fetch(:audio_normalize)
|
|
764
|
+
|
|
765
|
+
{ mode: :adaptive }
|
|
766
|
+
end
|
|
767
|
+
|
|
97
768
|
def print_audio_devices
|
|
98
769
|
say("Audio devices:")
|
|
99
770
|
Vizcore::Audio::InputManager.available_audio_devices.each do |device|
|