Node graph improvements and fixes
This commit is contained in:
parent
d2354e4864
commit
9d6eaa5bba
|
|
@ -1204,6 +1204,15 @@ impl Engine {
|
||||||
QueryResponse::GraphState(Err(format!("Track {} not found or is not a MIDI track", track_id)))
|
QueryResponse::GraphState(Err(format!("Track {} not found or is not a MIDI track", track_id)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Query::GetOscilloscopeData(track_id, node_id, sample_count) => {
|
||||||
|
match self.project.get_oscilloscope_data(track_id, node_id, sample_count) {
|
||||||
|
Some(data) => QueryResponse::OscilloscopeData(Ok(data)),
|
||||||
|
None => QueryResponse::OscilloscopeData(Err(format!(
|
||||||
|
"Failed to get oscilloscope data from track {} node {}",
|
||||||
|
track_id, node_id
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send response back
|
// Send response back
|
||||||
|
|
@ -1759,4 +1768,26 @@ impl EngineController {
|
||||||
|
|
||||||
Err("Query timeout".to_string())
|
Err("Query timeout".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query oscilloscope data from a node
|
||||||
|
pub fn query_oscilloscope_data(&mut self, track_id: TrackId, node_id: u32, sample_count: usize) -> Result<Vec<f32>, String> {
|
||||||
|
// Send query
|
||||||
|
if let Err(_) = self.query_tx.push(Query::GetOscilloscopeData(track_id, node_id, sample_count)) {
|
||||||
|
return Err("Failed to send query - queue full".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for response (with timeout)
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let timeout = std::time::Duration::from_millis(100);
|
||||||
|
|
||||||
|
while start.elapsed() < timeout {
|
||||||
|
if let Ok(QueryResponse::OscilloscopeData(result)) = self.query_response_rx.pop() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Small sleep to avoid busy-waiting
|
||||||
|
std::thread::sleep(std::time::Duration::from_micros(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("Query timeout".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub enum TriggerMode {
|
||||||
FreeRunning = 0,
|
FreeRunning = 0,
|
||||||
RisingEdge = 1,
|
RisingEdge = 1,
|
||||||
FallingEdge = 2,
|
FallingEdge = 2,
|
||||||
|
VoltPerOctave = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerMode {
|
impl TriggerMode {
|
||||||
|
|
@ -20,6 +21,7 @@ impl TriggerMode {
|
||||||
match value.round() as i32 {
|
match value.round() as i32 {
|
||||||
1 => TriggerMode::RisingEdge,
|
1 => TriggerMode::RisingEdge,
|
||||||
2 => TriggerMode::FallingEdge,
|
2 => TriggerMode::FallingEdge,
|
||||||
|
3 => TriggerMode::VoltPerOctave,
|
||||||
_ => TriggerMode::FreeRunning,
|
_ => TriggerMode::FreeRunning,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +82,9 @@ pub struct OscilloscopeNode {
|
||||||
trigger_mode: TriggerMode,
|
trigger_mode: TriggerMode,
|
||||||
trigger_level: f32, // -1.0 to 1.0
|
trigger_level: f32, // -1.0 to 1.0
|
||||||
last_sample: f32, // For edge detection
|
last_sample: f32, // For edge detection
|
||||||
|
voct_value: f32, // Current V/oct input value
|
||||||
|
sample_counter: usize, // Counter for V/oct triggering
|
||||||
|
trigger_period: usize, // Period in samples for V/oct triggering
|
||||||
|
|
||||||
// Shared buffer for reading from Tauri commands
|
// Shared buffer for reading from Tauri commands
|
||||||
buffer: Arc<Mutex<CircularBuffer>>,
|
buffer: Arc<Mutex<CircularBuffer>>,
|
||||||
|
|
@ -95,6 +100,7 @@ impl OscilloscopeNode {
|
||||||
|
|
||||||
let inputs = vec![
|
let inputs = vec![
|
||||||
NodePort::new("Audio In", SignalType::Audio, 0),
|
NodePort::new("Audio In", SignalType::Audio, 0),
|
||||||
|
NodePort::new("V/oct", SignalType::CV, 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let outputs = vec![
|
let outputs = vec![
|
||||||
|
|
@ -103,7 +109,7 @@ impl OscilloscopeNode {
|
||||||
|
|
||||||
let parameters = vec![
|
let parameters = vec![
|
||||||
Parameter::new(PARAM_TIME_SCALE, "Time Scale", 10.0, 1000.0, 100.0, ParameterUnit::Time),
|
Parameter::new(PARAM_TIME_SCALE, "Time Scale", 10.0, 1000.0, 100.0, ParameterUnit::Time),
|
||||||
Parameter::new(PARAM_TRIGGER_MODE, "Trigger", 0.0, 2.0, 0.0, ParameterUnit::Generic),
|
Parameter::new(PARAM_TRIGGER_MODE, "Trigger", 0.0, 3.0, 0.0, ParameterUnit::Generic),
|
||||||
Parameter::new(PARAM_TRIGGER_LEVEL, "Trigger Level", -1.0, 1.0, 0.0, ParameterUnit::Generic),
|
Parameter::new(PARAM_TRIGGER_LEVEL, "Trigger Level", -1.0, 1.0, 0.0, ParameterUnit::Generic),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -113,6 +119,9 @@ impl OscilloscopeNode {
|
||||||
trigger_mode: TriggerMode::FreeRunning,
|
trigger_mode: TriggerMode::FreeRunning,
|
||||||
trigger_level: 0.0,
|
trigger_level: 0.0,
|
||||||
last_sample: 0.0,
|
last_sample: 0.0,
|
||||||
|
voct_value: 0.0,
|
||||||
|
sample_counter: 0,
|
||||||
|
trigger_period: 480, // Default to ~100Hz at 48kHz
|
||||||
buffer: Arc::new(Mutex::new(CircularBuffer::new(BUFFER_SIZE))),
|
buffer: Arc::new(Mutex::new(CircularBuffer::new(BUFFER_SIZE))),
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
|
|
@ -141,6 +150,12 @@ impl OscilloscopeNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert V/oct to frequency in Hz (matches oscillator convention)
|
||||||
|
/// 0V = A4 (440 Hz), ±1V per octave
|
||||||
|
fn voct_to_frequency(voct: f32) -> f32 {
|
||||||
|
440.0 * 2.0_f32.powf(voct)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if trigger condition is met
|
/// Check if trigger condition is met
|
||||||
fn is_triggered(&self, current_sample: f32) -> bool {
|
fn is_triggered(&self, current_sample: f32) -> bool {
|
||||||
match self.trigger_mode {
|
match self.trigger_mode {
|
||||||
|
|
@ -151,6 +166,10 @@ impl OscilloscopeNode {
|
||||||
TriggerMode::FallingEdge => {
|
TriggerMode::FallingEdge => {
|
||||||
self.last_sample >= self.trigger_level && current_sample < self.trigger_level
|
self.last_sample >= self.trigger_level && current_sample < self.trigger_level
|
||||||
}
|
}
|
||||||
|
TriggerMode::VoltPerOctave => {
|
||||||
|
// Trigger at the start of each period
|
||||||
|
self.sample_counter == 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,7 +215,7 @@ impl AudioNode for OscilloscopeNode {
|
||||||
outputs: &mut [&mut [f32]],
|
outputs: &mut [&mut [f32]],
|
||||||
_midi_inputs: &[&[MidiEvent]],
|
_midi_inputs: &[&[MidiEvent]],
|
||||||
_midi_outputs: &mut [&mut Vec<MidiEvent>],
|
_midi_outputs: &mut [&mut Vec<MidiEvent>],
|
||||||
_sample_rate: u32,
|
sample_rate: u32,
|
||||||
) {
|
) {
|
||||||
if inputs.is_empty() || outputs.is_empty() {
|
if inputs.is_empty() || outputs.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
|
@ -206,6 +225,20 @@ impl AudioNode for OscilloscopeNode {
|
||||||
let output = &mut outputs[0];
|
let output = &mut outputs[0];
|
||||||
let len = input.len().min(output.len());
|
let len = input.len().min(output.len());
|
||||||
|
|
||||||
|
// Read V/oct input if available and update trigger period
|
||||||
|
if inputs.len() > 1 && !inputs[1].is_empty() {
|
||||||
|
self.voct_value = inputs[1][0]; // Use first sample of V/oct input
|
||||||
|
let frequency = Self::voct_to_frequency(self.voct_value);
|
||||||
|
// Calculate period in samples, clamped to reasonable range
|
||||||
|
let period_samples = (sample_rate as f32 / frequency).max(1.0);
|
||||||
|
self.trigger_period = period_samples as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sample counter for V/oct triggering
|
||||||
|
if self.trigger_mode == TriggerMode::VoltPerOctave {
|
||||||
|
self.sample_counter = (self.sample_counter + len) % self.trigger_period;
|
||||||
|
}
|
||||||
|
|
||||||
// Pass through audio (copy input to output)
|
// Pass through audio (copy input to output)
|
||||||
output[..len].copy_from_slice(&input[..len]);
|
output[..len].copy_from_slice(&input[..len]);
|
||||||
|
|
||||||
|
|
@ -222,6 +255,8 @@ impl AudioNode for OscilloscopeNode {
|
||||||
|
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.last_sample = 0.0;
|
self.last_sample = 0.0;
|
||||||
|
self.voct_value = 0.0;
|
||||||
|
self.sample_counter = 0;
|
||||||
self.clear_buffer();
|
self.clear_buffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,6 +275,9 @@ impl AudioNode for OscilloscopeNode {
|
||||||
trigger_mode: self.trigger_mode,
|
trigger_mode: self.trigger_mode,
|
||||||
trigger_level: self.trigger_level,
|
trigger_level: self.trigger_level,
|
||||||
last_sample: 0.0,
|
last_sample: 0.0,
|
||||||
|
voct_value: 0.0,
|
||||||
|
sample_counter: 0,
|
||||||
|
trigger_period: 480,
|
||||||
buffer: Arc::new(Mutex::new(CircularBuffer::new(BUFFER_SIZE))),
|
buffer: Arc::new(Mutex::new(CircularBuffer::new(BUFFER_SIZE))),
|
||||||
inputs: self.inputs.clone(),
|
inputs: self.inputs.clone(),
|
||||||
outputs: self.outputs.clone(),
|
outputs: self.outputs.clone(),
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,8 @@ pub enum Query {
|
||||||
GetGraphState(TrackId),
|
GetGraphState(TrackId),
|
||||||
/// Get a voice allocator's template graph state as JSON (track_id, voice_allocator_id)
|
/// Get a voice allocator's template graph state as JSON (track_id, voice_allocator_id)
|
||||||
GetTemplateState(TrackId, u32),
|
GetTemplateState(TrackId, u32),
|
||||||
|
/// Get oscilloscope data from a node (track_id, node_id, sample_count)
|
||||||
|
GetOscilloscopeData(TrackId, u32, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Responses to synchronous queries
|
/// Responses to synchronous queries
|
||||||
|
|
@ -214,4 +216,6 @@ pub enum Query {
|
||||||
pub enum QueryResponse {
|
pub enum QueryResponse {
|
||||||
/// Graph state as JSON string
|
/// Graph state as JSON string
|
||||||
GraphState(Result<String, String>),
|
GraphState(Result<String, String>),
|
||||||
|
/// Oscilloscope data samples
|
||||||
|
OscilloscopeData(Result<Vec<f32>, String>),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1131,6 +1131,22 @@ pub async fn multi_sampler_remove_layer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_oscilloscope_data(
|
||||||
|
state: tauri::State<'_, Arc<Mutex<AudioState>>>,
|
||||||
|
track_id: u32,
|
||||||
|
node_id: u32,
|
||||||
|
sample_count: usize,
|
||||||
|
) -> Result<Vec<f32>, String> {
|
||||||
|
let mut audio_state = state.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(controller) = &mut audio_state.controller {
|
||||||
|
controller.query_oscilloscope_data(track_id, node_id, sample_count)
|
||||||
|
} else {
|
||||||
|
Err("Audio not initialized".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, Clone)]
|
#[derive(serde::Serialize, Clone)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum SerializedAudioEvent {
|
pub enum SerializedAudioEvent {
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ pub fn run() {
|
||||||
audio::multi_sampler_get_layers,
|
audio::multi_sampler_get_layers,
|
||||||
audio::multi_sampler_update_layer,
|
audio::multi_sampler_update_layer,
|
||||||
audio::multi_sampler_remove_layer,
|
audio::multi_sampler_remove_layer,
|
||||||
|
audio::get_oscilloscope_data,
|
||||||
])
|
])
|
||||||
// .manage(window_counter)
|
// .manage(window_counter)
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,11 @@
|
||||||
padding-bottom: var(--dfNodePaddingBottom);
|
padding-bottom: var(--dfNodePaddingBottom);
|
||||||
-webkit-box-shadow: var(--dfNodeBoxShadowHL) var(--dfNodeBoxShadowVL) var(--dfNodeBoxShadowBR) var(--dfNodeBoxShadowS) var(--dfNodeBoxShadowColor);
|
-webkit-box-shadow: var(--dfNodeBoxShadowHL) var(--dfNodeBoxShadowVL) var(--dfNodeBoxShadowBR) var(--dfNodeBoxShadowS) var(--dfNodeBoxShadowColor);
|
||||||
box-shadow: var(--dfNodeBoxShadowHL) var(--dfNodeBoxShadowVL) var(--dfNodeBoxShadowBR) var(--dfNodeBoxShadowS) var(--dfNodeBoxShadowColor);
|
box-shadow: var(--dfNodeBoxShadowHL) var(--dfNodeBoxShadowVL) var(--dfNodeBoxShadowBR) var(--dfNodeBoxShadowS) var(--dfNodeBoxShadowColor);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawflow .drawflow-node * {
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawflow .drawflow-node:hover {
|
.drawflow .drawflow-node:hover {
|
||||||
|
|
|
||||||
410
src/main.js
410
src/main.js
|
|
@ -6093,6 +6093,26 @@ function nodeEditor() {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.id = "node-editor-container";
|
container.id = "node-editor-container";
|
||||||
|
|
||||||
|
// Prevent text selection during drag operations
|
||||||
|
container.addEventListener('selectstart', (e) => {
|
||||||
|
// Allow selection on input elements
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
container.addEventListener('mousedown', (e) => {
|
||||||
|
// Don't prevent default on inputs, textareas, or palette items (draggable)
|
||||||
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't prevent default on palette items or their children
|
||||||
|
if (e.target.closest('.node-palette-item') || e.target.closest('.node-category-item')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
// Track editing context: null = main graph, {voiceAllocatorId, voiceAllocatorName} = editing template
|
// Track editing context: null = main graph, {voiceAllocatorId, voiceAllocatorName} = editing template
|
||||||
let editingContext = null;
|
let editingContext = null;
|
||||||
|
|
||||||
|
|
@ -6207,6 +6227,97 @@ function nodeEditor() {
|
||||||
// Store editor reference in context
|
// Store editor reference in context
|
||||||
context.nodeEditor = editor;
|
context.nodeEditor = editor;
|
||||||
|
|
||||||
|
// Add reconnection support: dragging from a connected input disconnects and starts new connection
|
||||||
|
drawflowDiv.addEventListener('mousedown', (e) => {
|
||||||
|
console.log('Mousedown on drawflow, target:', e.target);
|
||||||
|
|
||||||
|
// Check if clicking on an input port
|
||||||
|
const inputPort = e.target.closest('.input');
|
||||||
|
console.log('Found input port:', inputPort);
|
||||||
|
|
||||||
|
if (inputPort) {
|
||||||
|
// Get the node and port information - the drawflow-node div has the id
|
||||||
|
const drawflowNode = inputPort.closest('.drawflow-node');
|
||||||
|
console.log('Found drawflow node:', drawflowNode, 'ID:', drawflowNode?.id);
|
||||||
|
if (!drawflowNode) return;
|
||||||
|
|
||||||
|
const nodeId = parseInt(drawflowNode.id.replace('node-', ''));
|
||||||
|
console.log('Node ID:', nodeId);
|
||||||
|
|
||||||
|
// Access the node data directly from the current module
|
||||||
|
const moduleName = editor.module;
|
||||||
|
const node = editor.drawflow.drawflow[moduleName]?.data[nodeId];
|
||||||
|
console.log('Node data:', node);
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
// Get the port class (input_1, input_2, etc.)
|
||||||
|
const portClasses = Array.from(inputPort.classList);
|
||||||
|
const portClass = portClasses.find(c => c.startsWith('input_'));
|
||||||
|
console.log('Port class:', portClass, 'All classes:', portClasses);
|
||||||
|
if (!portClass) return;
|
||||||
|
|
||||||
|
// Check if this input has any connections
|
||||||
|
const inputConnections = node.inputs[portClass];
|
||||||
|
console.log('Input connections:', inputConnections);
|
||||||
|
if (inputConnections && inputConnections.connections && inputConnections.connections.length > 0) {
|
||||||
|
// Get the first connection (inputs should only have one connection)
|
||||||
|
const connection = inputConnections.connections[0];
|
||||||
|
console.log('Found connection to disconnect:', connection);
|
||||||
|
|
||||||
|
// Prevent default to avoid interfering with the drag
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Remove the connection
|
||||||
|
editor.removeSingleConnection(
|
||||||
|
connection.node,
|
||||||
|
nodeId,
|
||||||
|
connection.input,
|
||||||
|
portClass
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now trigger Drawflow's connection drag from the output that was connected
|
||||||
|
// We need to simulate starting a drag from the output port
|
||||||
|
const outputNodeElement = document.getElementById(`node-${connection.node}`);
|
||||||
|
console.log('Output node element:', outputNodeElement);
|
||||||
|
if (outputNodeElement) {
|
||||||
|
const outputPort = outputNodeElement.querySelector(`.${connection.input}`);
|
||||||
|
console.log('Output port:', outputPort, 'class:', connection.input);
|
||||||
|
if (outputPort) {
|
||||||
|
// Dispatch a synthetic mousedown event on the output port
|
||||||
|
// This will trigger Drawflow's normal connection start logic
|
||||||
|
setTimeout(() => {
|
||||||
|
const rect = outputPort.getBoundingClientRect();
|
||||||
|
const syntheticEvent = new MouseEvent('mousedown', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
clientX: rect.left + rect.width / 2,
|
||||||
|
clientY: rect.top + rect.height / 2,
|
||||||
|
button: 0
|
||||||
|
});
|
||||||
|
outputPort.dispatchEvent(syntheticEvent);
|
||||||
|
|
||||||
|
// Then immediately dispatch a mousemove to the original cursor position
|
||||||
|
// to start dragging the connection line
|
||||||
|
setTimeout(() => {
|
||||||
|
const mousemoveEvent = new MouseEvent('mousemove', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
view: window,
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
button: 0
|
||||||
|
});
|
||||||
|
document.dispatchEvent(mousemoveEvent);
|
||||||
|
}, 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, true); // Use capture phase to intercept before Drawflow
|
||||||
|
|
||||||
// Add trackpad/mousewheel scrolling support for panning
|
// Add trackpad/mousewheel scrolling support for panning
|
||||||
drawflowDiv.addEventListener('wheel', (e) => {
|
drawflowDiv.addEventListener('wheel', (e) => {
|
||||||
// Don't scroll if hovering over palette or other UI elements
|
// Don't scroll if hovering over palette or other UI elements
|
||||||
|
|
@ -6268,7 +6379,22 @@ function nodeEditor() {
|
||||||
const item = e.target.closest(".node-palette-item");
|
const item = e.target.closest(".node-palette-item");
|
||||||
if (item) {
|
if (item) {
|
||||||
const nodeType = item.getAttribute("data-node-type");
|
const nodeType = item.getAttribute("data-node-type");
|
||||||
addNode(nodeType, 100, 100, null);
|
|
||||||
|
// Calculate center of visible canvas viewport
|
||||||
|
const rect = drawflowDiv.getBoundingClientRect();
|
||||||
|
const canvasX = editor.canvas_x || 0;
|
||||||
|
const canvasY = editor.canvas_y || 0;
|
||||||
|
const zoom = editor.zoom || 1;
|
||||||
|
|
||||||
|
// Approximate node dimensions (nodes have min-width: 160px, typical height ~150px)
|
||||||
|
const nodeWidth = 160;
|
||||||
|
const nodeHeight = 150;
|
||||||
|
|
||||||
|
// Center position in world coordinates, offset by half node size
|
||||||
|
const centerX = (rect.width / 2 - canvasX) / zoom - nodeWidth / 2;
|
||||||
|
const centerY = (rect.height / 2 - canvasY) / zoom - nodeHeight / 2;
|
||||||
|
|
||||||
|
addNode(nodeType, centerX, centerY, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -6311,13 +6437,31 @@ function nodeEditor() {
|
||||||
|
|
||||||
// Get drop position relative to the editor
|
// Get drop position relative to the editor
|
||||||
const rect = drawflowDiv.getBoundingClientRect();
|
const rect = drawflowDiv.getBoundingClientRect();
|
||||||
const precanvasX = editor.precanvas?.x || 0;
|
|
||||||
const precanvasY = editor.precanvas?.y || 0;
|
|
||||||
const zoom = editor.zoom || 1;
|
|
||||||
const x = (e.clientX - rect.left - precanvasX) / zoom;
|
|
||||||
const y = (e.clientY - rect.top - precanvasY) / zoom;
|
|
||||||
|
|
||||||
console.log('Position calculation:', { clientX: e.clientX, clientY: e.clientY, rectLeft: rect.left, rectTop: rect.top, precanvasX, precanvasY, zoom, x, y });
|
// Use canvas_x and canvas_y which are set by the wheel scroll handler
|
||||||
|
const canvasX = editor.canvas_x || 0;
|
||||||
|
const canvasY = editor.canvas_y || 0;
|
||||||
|
const zoom = editor.zoom || 1;
|
||||||
|
|
||||||
|
// Approximate node dimensions (nodes have min-width: 160px, typical height ~150px)
|
||||||
|
const nodeWidth = 160;
|
||||||
|
const nodeHeight = 150;
|
||||||
|
|
||||||
|
// Calculate position accounting for canvas pan offset, centered on cursor
|
||||||
|
const x = (e.clientX - rect.left - canvasX) / zoom - nodeWidth / 2;
|
||||||
|
const y = (e.clientY - rect.top - canvasY) / zoom - nodeHeight / 2;
|
||||||
|
|
||||||
|
console.log('Position calculation:', JSON.stringify({
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
rectLeft: rect.left,
|
||||||
|
rectTop: rect.top,
|
||||||
|
canvasX,
|
||||||
|
canvasY,
|
||||||
|
zoom,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
}));
|
||||||
|
|
||||||
// Check if dropping into an expanded VoiceAllocator
|
// Check if dropping into an expanded VoiceAllocator
|
||||||
let parentNodeId = null;
|
let parentNodeId = null;
|
||||||
|
|
@ -6385,6 +6529,9 @@ function nodeEditor() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop oscilloscope visualization if this was an Oscilloscope node
|
||||||
|
stopOscilloscopeVisualization(nodeId);
|
||||||
|
|
||||||
// Clean up parent-child tracking
|
// Clean up parent-child tracking
|
||||||
const parentId = nodeParents.get(nodeId);
|
const parentId = nodeParents.get(nodeId);
|
||||||
nodeParents.delete(nodeId);
|
nodeParents.delete(nodeId);
|
||||||
|
|
@ -6423,6 +6570,24 @@ function nodeEditor() {
|
||||||
html
|
html
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update all IDs in the HTML to use drawflowNodeId instead of nodeId
|
||||||
|
// This ensures parameter setup can find the correct elements
|
||||||
|
if (nodeId !== drawflowNodeId) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const nodeElement = document.getElementById(`node-${drawflowNodeId}`);
|
||||||
|
if (nodeElement) {
|
||||||
|
// Update all elements with IDs containing the old nodeId
|
||||||
|
const elementsWithIds = nodeElement.querySelectorAll('[id*="-' + nodeId + '"]');
|
||||||
|
elementsWithIds.forEach(el => {
|
||||||
|
const oldId = el.id;
|
||||||
|
const newId = oldId.replace('-' + nodeId, '-' + drawflowNodeId);
|
||||||
|
el.id = newId;
|
||||||
|
console.log(`Updated element ID: ${oldId} -> ${newId}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
// Track parent-child relationship
|
// Track parent-child relationship
|
||||||
if (parentNodeId !== null) {
|
if (parentNodeId !== null) {
|
||||||
nodeParents.set(drawflowNodeId, parentNodeId);
|
nodeParents.set(drawflowNodeId, parentNodeId);
|
||||||
|
|
@ -6526,6 +6691,18 @@ function nodeEditor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is an Oscilloscope node, start the visualization
|
||||||
|
if (nodeType === "Oscilloscope") {
|
||||||
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
|
if (currentTrackId !== null) {
|
||||||
|
console.log(`Starting oscilloscope visualization for node ${drawflowNodeId} (backend ID: ${backendNodeId})`);
|
||||||
|
// Wait for DOM to update before starting visualization
|
||||||
|
setTimeout(() => {
|
||||||
|
startOscilloscopeVisualization(drawflowNodeId, currentTrackId, backendNodeId);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If this is a VoiceAllocator, automatically create template I/O nodes inside it
|
// If this is a VoiceAllocator, automatically create template I/O nodes inside it
|
||||||
if (nodeType === "VoiceAllocator") {
|
if (nodeType === "VoiceAllocator") {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -6613,10 +6790,8 @@ function nodeEditor() {
|
||||||
inputs.forEach((input, index) => {
|
inputs.forEach((input, index) => {
|
||||||
if (index < nodeDef.inputs.length) {
|
if (index < nodeDef.inputs.length) {
|
||||||
const portDef = nodeDef.inputs[index];
|
const portDef = nodeDef.inputs[index];
|
||||||
const connector = input.querySelector(".input_0, .input_1, .input_2, .input_3");
|
// Add connector styling class directly to the input element
|
||||||
if (connector) {
|
input.classList.add(getPortClass(portDef.type));
|
||||||
connector.classList.add(getPortClass(portDef.type));
|
|
||||||
}
|
|
||||||
// Add label
|
// Add label
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
label.textContent = portDef.name;
|
label.textContent = portDef.name;
|
||||||
|
|
@ -6629,10 +6804,8 @@ function nodeEditor() {
|
||||||
outputs.forEach((output, index) => {
|
outputs.forEach((output, index) => {
|
||||||
if (index < nodeDef.outputs.length) {
|
if (index < nodeDef.outputs.length) {
|
||||||
const portDef = nodeDef.outputs[index];
|
const portDef = nodeDef.outputs[index];
|
||||||
const connector = output.querySelector(".output_0, .output_1, .output_2, .output_3");
|
// Add connector styling class directly to the output element
|
||||||
if (connector) {
|
output.classList.add(getPortClass(portDef.type));
|
||||||
connector.classList.add(getPortClass(portDef.type));
|
|
||||||
}
|
|
||||||
// Add label
|
// Add label
|
||||||
const label = document.createElement("span");
|
const label = document.createElement("span");
|
||||||
label.textContent = portDef.name;
|
label.textContent = portDef.name;
|
||||||
|
|
@ -6661,18 +6834,35 @@ function nodeEditor() {
|
||||||
const paramId = parseInt(e.target.getAttribute("data-param"));
|
const paramId = parseInt(e.target.getAttribute("data-param"));
|
||||||
const value = parseFloat(e.target.value);
|
const value = parseFloat(e.target.value);
|
||||||
|
|
||||||
|
console.log(`[setupNodeParameters] Slider input - nodeId: ${nodeId}, paramId: ${paramId}, value: ${value}`);
|
||||||
|
|
||||||
// Update display
|
// Update display
|
||||||
const nodeData = editor.getNodeFromId(nodeId);
|
const nodeData = editor.getNodeFromId(nodeId);
|
||||||
if (nodeData) {
|
if (nodeData) {
|
||||||
const nodeDef = nodeTypes[nodeData.name];
|
const nodeDef = nodeTypes[nodeData.name];
|
||||||
|
console.log(`[setupNodeParameters] Found node type: ${nodeData.name}, parameters:`, nodeDef?.parameters);
|
||||||
if (nodeDef && nodeDef.parameters[paramId]) {
|
if (nodeDef && nodeDef.parameters[paramId]) {
|
||||||
const param = nodeDef.parameters[paramId];
|
const param = nodeDef.parameters[paramId];
|
||||||
|
console.log(`[setupNodeParameters] Looking for span: #${param.name}-${nodeId}`);
|
||||||
const displaySpan = nodeElement.querySelector(`#${param.name}-${nodeId}`);
|
const displaySpan = nodeElement.querySelector(`#${param.name}-${nodeId}`);
|
||||||
|
console.log(`[setupNodeParameters] Found span:`, displaySpan);
|
||||||
if (displaySpan) {
|
if (displaySpan) {
|
||||||
|
// Special formatting for oscilloscope trigger mode
|
||||||
|
if (param.name === 'trigger_mode') {
|
||||||
|
const modes = ['Free', 'Rising', 'Falling', 'V/oct'];
|
||||||
|
displaySpan.textContent = modes[Math.round(value)] || 'Free';
|
||||||
|
} else {
|
||||||
displaySpan.textContent = value.toFixed(param.unit === 'Hz' ? 0 : 2);
|
displaySpan.textContent = value.toFixed(param.unit === 'Hz' ? 0 : 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update oscilloscope time scale if this is a time_scale parameter
|
||||||
|
if (param.name === 'time_scale' && oscilloscopeTimeScales) {
|
||||||
|
oscilloscopeTimeScales.set(nodeId, value);
|
||||||
|
console.log(`Updated oscilloscope time scale for node ${nodeId}: ${value}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send to backend
|
// Send to backend
|
||||||
if (nodeData.data.backendId !== null) {
|
if (nodeData.data.backendId !== null) {
|
||||||
const currentTrackId = getCurrentMidiTrack();
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
|
|
@ -7008,6 +7198,32 @@ function nodeEditor() {
|
||||||
|
|
||||||
console.log("Types match - proceeding with connection");
|
console.log("Types match - proceeding with connection");
|
||||||
|
|
||||||
|
// Auto-switch Oscilloscope to V/oct trigger mode when connecting to V/oct input
|
||||||
|
if (inputNode.name === 'Oscilloscope' && inputPort === 1) {
|
||||||
|
console.log(`Auto-switching Oscilloscope node ${connection.input_id} to V/oct trigger mode`);
|
||||||
|
// Set trigger_mode parameter (id: 1) to value 3 (V/oct)
|
||||||
|
const triggerModeSlider = document.querySelector(`#node-${connection.input_id} input[data-param="1"]`);
|
||||||
|
const triggerModeSpan = document.querySelector(`#trigger_mode-${connection.input_id}`);
|
||||||
|
if (triggerModeSlider) {
|
||||||
|
triggerModeSlider.value = 3;
|
||||||
|
if (triggerModeSpan) {
|
||||||
|
triggerModeSpan.textContent = 'V/oct';
|
||||||
|
}
|
||||||
|
// Update backend parameter
|
||||||
|
if (inputNode.data.backendId !== null) {
|
||||||
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
|
if (currentTrackId !== null) {
|
||||||
|
invoke("graph_set_parameter", {
|
||||||
|
trackId: currentTrackId,
|
||||||
|
nodeId: inputNode.data.backendId,
|
||||||
|
paramId: 1,
|
||||||
|
value: 3.0
|
||||||
|
}).catch(err => console.error("Failed to set V/oct trigger mode:", err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Style the connection based on signal type
|
// Style the connection based on signal type
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const connectionElement = document.querySelector(
|
const connectionElement = document.querySelector(
|
||||||
|
|
@ -7123,6 +7339,31 @@ function nodeEditor() {
|
||||||
const outputPort = parseInt(connection.output_class.replace("output_", "")) - 1;
|
const outputPort = parseInt(connection.output_class.replace("output_", "")) - 1;
|
||||||
const inputPort = parseInt(connection.input_class.replace("input_", "")) - 1;
|
const inputPort = parseInt(connection.input_class.replace("input_", "")) - 1;
|
||||||
|
|
||||||
|
// Auto-switch Oscilloscope back to Free mode when disconnecting V/oct input
|
||||||
|
if (inputNode.name === 'Oscilloscope' && inputPort === 1) {
|
||||||
|
console.log(`Auto-switching Oscilloscope node ${connection.input_id} back to Free trigger mode`);
|
||||||
|
const triggerModeSlider = document.querySelector(`#node-${connection.input_id} input[data-param="1"]`);
|
||||||
|
const triggerModeSpan = document.querySelector(`#trigger_mode-${connection.input_id}`);
|
||||||
|
if (triggerModeSlider) {
|
||||||
|
triggerModeSlider.value = 0;
|
||||||
|
if (triggerModeSpan) {
|
||||||
|
triggerModeSpan.textContent = 'Free';
|
||||||
|
}
|
||||||
|
// Update backend parameter
|
||||||
|
if (inputNode.data.backendId !== null) {
|
||||||
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
|
if (currentTrackId !== null) {
|
||||||
|
invoke("graph_set_parameter", {
|
||||||
|
trackId: currentTrackId,
|
||||||
|
nodeId: inputNode.data.backendId,
|
||||||
|
paramId: 1,
|
||||||
|
value: 0.0
|
||||||
|
}).catch(err => console.error("Failed to set Free trigger mode:", err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send to backend
|
// Send to backend
|
||||||
if (outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
if (outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
||||||
const currentTrackId = getCurrentMidiTrack();
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
|
|
@ -7483,6 +7724,11 @@ function nodeEditor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For Oscilloscope nodes, start the visualization
|
||||||
|
if (nodeType === 'Oscilloscope' && serializedNode.id && trackId) {
|
||||||
|
startOscilloscopeVisualization(drawflowId, trackId, serializedNode.id);
|
||||||
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
}, 100);
|
}, 100);
|
||||||
}));
|
}));
|
||||||
|
|
@ -7501,6 +7747,28 @@ function nodeEditor() {
|
||||||
`output_${conn.from_port + 1}`,
|
`output_${conn.from_port + 1}`,
|
||||||
`input_${conn.to_port + 1}`
|
`input_${conn.to_port + 1}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Style the connection based on signal type
|
||||||
|
// We need to look up the node type and get the output port signal type
|
||||||
|
setupPromises.push(new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const outputNode = editor.getNodeFromId(outputDrawflowId);
|
||||||
|
if (outputNode) {
|
||||||
|
const nodeType = outputNode.data.nodeType;
|
||||||
|
const nodeDef = nodeTypes[nodeType];
|
||||||
|
if (nodeDef && conn.from_port < nodeDef.outputs.length) {
|
||||||
|
const signalType = nodeDef.outputs[conn.from_port].type;
|
||||||
|
const connectionElement = document.querySelector(
|
||||||
|
`.connection.node_in_node-${inputDrawflowId}.node_out_node-${outputDrawflowId}`
|
||||||
|
);
|
||||||
|
if (connectionElement) {
|
||||||
|
connectionElement.classList.add(`connection-${signalType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}, 10);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -8525,6 +8793,118 @@ async function loadMIDIFile(trackId, path, startTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Oscilloscope Visualization ==========
|
||||||
|
|
||||||
|
// Store oscilloscope update intervals by node ID
|
||||||
|
const oscilloscopeIntervals = new Map();
|
||||||
|
// Store oscilloscope time scales by node ID
|
||||||
|
const oscilloscopeTimeScales = new Map();
|
||||||
|
|
||||||
|
// Start oscilloscope visualization for a node
|
||||||
|
function startOscilloscopeVisualization(nodeId, trackId, backendNodeId) {
|
||||||
|
// Clear any existing interval for this node
|
||||||
|
stopOscilloscopeVisualization(nodeId);
|
||||||
|
|
||||||
|
// Find the canvas by traversing from the node element
|
||||||
|
const nodeElement = document.getElementById(`node-${nodeId}`);
|
||||||
|
if (!nodeElement) {
|
||||||
|
console.warn(`Node element not found for node ${nodeId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = nodeElement.querySelector('canvas[id^="oscilloscope-canvas-"]');
|
||||||
|
if (!canvas) {
|
||||||
|
console.warn(`Oscilloscope canvas not found in node ${nodeId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found oscilloscope canvas for node ${nodeId}:`, canvas.id);
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
// Initialize time scale to default (100ms)
|
||||||
|
if (!oscilloscopeTimeScales.has(nodeId)) {
|
||||||
|
oscilloscopeTimeScales.set(nodeId, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update function to fetch and draw oscilloscope data
|
||||||
|
const updateOscilloscope = async () => {
|
||||||
|
try {
|
||||||
|
// Calculate samples needed based on time scale
|
||||||
|
// Assuming 48kHz sample rate
|
||||||
|
const timeScaleMs = oscilloscopeTimeScales.get(nodeId) || 100;
|
||||||
|
const sampleRate = 48000;
|
||||||
|
const samplesNeeded = Math.floor((timeScaleMs / 1000) * sampleRate);
|
||||||
|
// Cap at 2 seconds worth of samples to avoid excessive memory usage
|
||||||
|
const sampleCount = Math.min(samplesNeeded, sampleRate * 2);
|
||||||
|
|
||||||
|
// Fetch oscilloscope data
|
||||||
|
const data = await invoke('get_oscilloscope_data', {
|
||||||
|
trackId: trackId,
|
||||||
|
nodeId: backendNodeId,
|
||||||
|
sampleCount: sampleCount
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.fillStyle = '#1a1a1a';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Draw grid lines
|
||||||
|
ctx.strokeStyle = '#2a2a2a';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
|
// Horizontal grid lines
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, height / 2);
|
||||||
|
ctx.lineTo(width, height / 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw waveform
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
ctx.strokeStyle = '#4CAF50';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
const xStep = width / data.length;
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const x = i * xStep;
|
||||||
|
// Map sample value from [-1, 1] to canvas height
|
||||||
|
const y = height / 2 - (data[i] * height / 2);
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update oscilloscope:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
updateOscilloscope();
|
||||||
|
|
||||||
|
// Update every 50ms (20 FPS)
|
||||||
|
const interval = setInterval(updateOscilloscope, 50);
|
||||||
|
oscilloscopeIntervals.set(nodeId, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop oscilloscope visualization for a node
|
||||||
|
function stopOscilloscopeVisualization(nodeId) {
|
||||||
|
const interval = oscilloscopeIntervals.get(nodeId);
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
oscilloscopeIntervals.delete(nodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== End Oscilloscope Visualization ==========
|
||||||
|
|
||||||
async function testAudio() {
|
async function testAudio() {
|
||||||
console.log("Starting rust")
|
console.log("Starting rust")
|
||||||
await init();
|
await init();
|
||||||
|
|
|
||||||
|
|
@ -310,28 +310,29 @@ export const nodeTypes = {
|
||||||
category: NodeCategory.UTILITY,
|
category: NodeCategory.UTILITY,
|
||||||
description: 'Visual audio signal monitor (pass-through)',
|
description: 'Visual audio signal monitor (pass-through)',
|
||||||
inputs: [
|
inputs: [
|
||||||
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 }
|
{ name: 'Audio In', type: SignalType.AUDIO, index: 0 },
|
||||||
|
{ name: 'V/oct', type: SignalType.CV, index: 1 }
|
||||||
],
|
],
|
||||||
outputs: [
|
outputs: [
|
||||||
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
|
{ name: 'Audio Out', type: SignalType.AUDIO, index: 0 }
|
||||||
],
|
],
|
||||||
parameters: [
|
parameters: [
|
||||||
{ id: 0, name: 'time_scale', label: 'Time Scale', min: 10, max: 1000, default: 100, unit: 'ms' },
|
{ id: 0, name: 'time_scale', label: 'Time Scale', min: 10, max: 1000, default: 100, unit: 'ms' },
|
||||||
{ id: 1, name: 'trigger_mode', label: 'Trigger', min: 0, max: 2, default: 0, unit: '' },
|
{ id: 1, name: 'trigger_mode', label: 'Trigger', min: 0, max: 3, default: 0, unit: '' },
|
||||||
{ id: 2, name: 'trigger_level', label: 'Trigger Level', min: -1, max: 1, default: 0, unit: '' }
|
{ id: 2, name: 'trigger_level', label: 'Trigger Level', min: -1, max: 1, default: 0, unit: '' }
|
||||||
],
|
],
|
||||||
getHTML: (nodeId) => `
|
getHTML: (nodeId) => `
|
||||||
<div class="node-content">
|
<div class="node-content">
|
||||||
<div class="node-title">Oscilloscope</div>
|
<div class="node-title">Oscilloscope</div>
|
||||||
|
<canvas id="oscilloscope-canvas-${nodeId}" width="200" height="80" style="width: 200px; height: 80px; background: #1a1a1a; border: 1px solid #444; border-radius: 2px; display: block; margin: 4px 0;"></canvas>
|
||||||
<div class="node-param">
|
<div class="node-param">
|
||||||
<label>Time: <span id="time-${nodeId}">100</span>ms</label>
|
<label>Time: <span id="time_scale-${nodeId}">100</span>ms</label>
|
||||||
<input type="range" data-node="${nodeId}" data-param="0" min="10" max="1000" value="100" step="10">
|
<input type="range" class="node-slider" data-node="${nodeId}" data-param="0" min="10" max="1000" value="100" step="10">
|
||||||
</div>
|
</div>
|
||||||
<div class="node-param">
|
<div class="node-param">
|
||||||
<label>Trigger: <span id="trig-${nodeId}">Free</span></label>
|
<label>Trigger: <span id="trigger_mode-${nodeId}">Free</span></label>
|
||||||
<input type="range" data-node="${nodeId}" data-param="1" min="0" max="2" value="0" step="1">
|
<input type="range" class="node-slider" data-node="${nodeId}" data-param="1" min="0" max="3" value="0" step="1">
|
||||||
</div>
|
</div>
|
||||||
<div class="node-info" style="margin-top: 4px; font-size: 10px;">Pass-through monitor</div>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,19 @@ button {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-grid, .horizontal-grid {
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
@ -1182,6 +1195,7 @@ button {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--node-bg);
|
background: var(--node-bg);
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Node editor header and breadcrumb */
|
/* Node editor header and breadcrumb */
|
||||||
|
|
@ -1197,6 +1211,11 @@ button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-editor-header * {
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-breadcrumb {
|
.context-breadcrumb {
|
||||||
|
|
@ -1247,6 +1266,11 @@ button {
|
||||||
max-height: calc(100% - 100px);
|
max-height: calc(100% - 100px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-palette * {
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-palette h3 {
|
.node-palette h3 {
|
||||||
|
|
@ -1454,7 +1478,8 @@ button {
|
||||||
/* Signal Type Connectors */
|
/* Signal Type Connectors */
|
||||||
|
|
||||||
/* Audio ports - Blue circles (matches audio clips) */
|
/* Audio ports - Blue circles (matches audio clips) */
|
||||||
.connector-audio {
|
.drawflow .drawflow-node .input.connector-audio,
|
||||||
|
.drawflow .drawflow-node .output.connector-audio {
|
||||||
width: 14px !important;
|
width: 14px !important;
|
||||||
height: 14px !important;
|
height: 14px !important;
|
||||||
border-radius: 50% !important;
|
border-radius: 50% !important;
|
||||||
|
|
@ -1462,8 +1487,16 @@ button {
|
||||||
border: 2px solid #1565C0 !important;
|
border: 2px solid #1565C0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawflow .drawflow-node .input.connector-audio:hover,
|
||||||
|
.drawflow .drawflow-node .output.connector-audio:hover {
|
||||||
|
background: #42A5F5 !important;
|
||||||
|
border: 2px solid #1976D2 !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* MIDI ports - Green squares (matches MIDI clips) */
|
/* MIDI ports - Green squares (matches MIDI clips) */
|
||||||
.connector-midi {
|
.drawflow .drawflow-node .input.connector-midi,
|
||||||
|
.drawflow .drawflow-node .output.connector-midi {
|
||||||
width: 14px !important;
|
width: 14px !important;
|
||||||
height: 14px !important;
|
height: 14px !important;
|
||||||
border-radius: 2px !important;
|
border-radius: 2px !important;
|
||||||
|
|
@ -1471,14 +1504,30 @@ button {
|
||||||
border: 2px solid #2E7D32 !important;
|
border: 2px solid #2E7D32 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawflow .drawflow-node .input.connector-midi:hover,
|
||||||
|
.drawflow .drawflow-node .output.connector-midi:hover {
|
||||||
|
background: #66BB6A !important;
|
||||||
|
border: 2px solid #388E3C !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* CV ports - Orange diamonds */
|
/* CV ports - Orange diamonds */
|
||||||
.connector-cv {
|
.drawflow .drawflow-node .input.connector-cv,
|
||||||
|
.drawflow .drawflow-node .output.connector-cv {
|
||||||
width: 12px !important;
|
width: 12px !important;
|
||||||
height: 12px !important;
|
height: 12px !important;
|
||||||
background: #FF9800 !important;
|
background: #FF9800 !important;
|
||||||
border: 2px solid #E65100 !important;
|
border: 2px solid #E65100 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
transform: rotate(45deg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawflow .drawflow-node .input.connector-cv:hover,
|
||||||
|
.drawflow .drawflow-node .output.connector-cv:hover {
|
||||||
|
background: #FFA726 !important;
|
||||||
|
border: 2px solid #EF6C00 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
transform: rotate(45deg) !important;
|
transform: rotate(45deg) !important;
|
||||||
border-radius: 2px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Connection line styling - Override Drawflow defaults */
|
/* Connection line styling - Override Drawflow defaults */
|
||||||
|
|
@ -1655,6 +1704,7 @@ button {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-item {
|
.preset-item {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue