Add undo/redo support for node graph editor
This commit is contained in:
parent
9d6eaa5bba
commit
a379266f99
|
|
@ -1957,4 +1957,354 @@ export const actions = {
|
|||
updateMenu();
|
||||
},
|
||||
},
|
||||
// Node graph actions
|
||||
graphAddNode: {
|
||||
create: (trackId, nodeType, position, nodeId, backendId) => {
|
||||
redoStack.length = 0;
|
||||
let action = {
|
||||
trackId: trackId,
|
||||
nodeType: nodeType,
|
||||
position: position,
|
||||
nodeId: nodeId, // Frontend node ID from Drawflow
|
||||
backendId: backendId
|
||||
};
|
||||
undoStack.push({ name: "graphAddNode", action: action });
|
||||
actions.graphAddNode.execute(action);
|
||||
updateMenu();
|
||||
},
|
||||
execute: async (action) => {
|
||||
// Re-add node via Tauri and reload frontend
|
||||
const result = await invoke('graph_add_node', {
|
||||
trackId: action.trackId,
|
||||
nodeType: action.nodeType,
|
||||
posX: action.position.x,
|
||||
posY: action.position.y
|
||||
});
|
||||
// Reload the entire graph to show the restored node
|
||||
if (context.reloadNodeEditor) {
|
||||
await context.reloadNodeEditor();
|
||||
}
|
||||
},
|
||||
rollback: async (action) => {
|
||||
// Remove node from backend
|
||||
await invoke('graph_remove_node', {
|
||||
trackId: action.trackId,
|
||||
nodeId: action.backendId
|
||||
});
|
||||
// Remove from frontend
|
||||
if (context.nodeEditor) {
|
||||
context.nodeEditor.removeNodeId(`node-${action.nodeId}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
graphRemoveNode: {
|
||||
create: (trackId, nodeId, backendId, nodeData) => {
|
||||
redoStack.length = 0;
|
||||
let action = {
|
||||
trackId: trackId,
|
||||
nodeId: nodeId,
|
||||
backendId: backendId,
|
||||
nodeData: nodeData, // Store full node data for restoration
|
||||
};
|
||||
undoStack.push({ name: "graphRemoveNode", action: action });
|
||||
actions.graphRemoveNode.execute(action);
|
||||
updateMenu();
|
||||
},
|
||||
execute: async (action) => {
|
||||
await invoke('graph_remove_node', {
|
||||
trackId: action.trackId,
|
||||
nodeId: action.backendId
|
||||
});
|
||||
if (context.nodeEditor) {
|
||||
context.nodeEditor.removeNodeId(`node-${action.nodeId}`);
|
||||
}
|
||||
},
|
||||
rollback: async (action) => {
|
||||
// Re-add node to backend
|
||||
const result = await invoke('graph_add_node', {
|
||||
trackId: action.trackId,
|
||||
nodeType: action.nodeData.nodeType,
|
||||
posX: action.nodeData.position.x,
|
||||
posY: action.nodeData.position.y
|
||||
});
|
||||
|
||||
// Store new backend ID
|
||||
const newBackendId = result.node_id || result;
|
||||
|
||||
// Re-add to frontend via reloadGraph
|
||||
if (context.reloadNodeEditor) {
|
||||
await context.reloadNodeEditor();
|
||||
}
|
||||
},
|
||||
},
|
||||
graphAddConnection: {
|
||||
create: (trackId, fromNode, fromPort, toNode, toPort, frontendFromId, frontendToId, fromPortClass, toPortClass) => {
|
||||
redoStack.length = 0;
|
||||
let action = {
|
||||
trackId: trackId,
|
||||
fromNode: fromNode,
|
||||
fromPort: fromPort,
|
||||
toNode: toNode,
|
||||
toPort: toPort,
|
||||
frontendFromId: frontendFromId,
|
||||
frontendToId: frontendToId,
|
||||
fromPortClass: fromPortClass,
|
||||
toPortClass: toPortClass
|
||||
};
|
||||
undoStack.push({ name: "graphAddConnection", action: action });
|
||||
actions.graphAddConnection.execute(action);
|
||||
updateMenu();
|
||||
},
|
||||
execute: async (action) => {
|
||||
// Suppress action recording during undo/redo
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('graph_connect', {
|
||||
trackId: action.trackId,
|
||||
fromNode: action.fromNode,
|
||||
fromPort: action.fromPort,
|
||||
toNode: action.toNode,
|
||||
toPort: action.toPort
|
||||
});
|
||||
// Add connection in frontend only if it doesn't exist
|
||||
if (context.nodeEditor) {
|
||||
const inputNode = context.nodeEditor.getNodeFromId(action.frontendToId);
|
||||
const inputConnections = inputNode?.inputs[action.toPortClass]?.connections;
|
||||
const alreadyConnected = inputConnections?.some(conn =>
|
||||
conn.node === action.frontendFromId && conn.input === action.fromPortClass
|
||||
);
|
||||
|
||||
if (!alreadyConnected) {
|
||||
context.nodeEditor.addConnection(
|
||||
action.frontendFromId,
|
||||
action.frontendToId,
|
||||
action.fromPortClass,
|
||||
action.toPortClass
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
rollback: async (action) => {
|
||||
// Suppress action recording during undo/redo
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('graph_disconnect', {
|
||||
trackId: action.trackId,
|
||||
fromNode: action.fromNode,
|
||||
fromPort: action.fromPort,
|
||||
toNode: action.toNode,
|
||||
toPort: action.toPort
|
||||
});
|
||||
// Remove from frontend
|
||||
if (context.nodeEditor) {
|
||||
context.nodeEditor.removeSingleConnection(
|
||||
action.frontendFromId,
|
||||
action.frontendToId,
|
||||
action.fromPortClass,
|
||||
action.toPortClass
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
graphRemoveConnection: {
|
||||
create: (trackId, fromNode, fromPort, toNode, toPort, frontendFromId, frontendToId, fromPortClass, toPortClass) => {
|
||||
redoStack.length = 0;
|
||||
let action = {
|
||||
trackId: trackId,
|
||||
fromNode: fromNode,
|
||||
fromPort: fromPort,
|
||||
toNode: toNode,
|
||||
toPort: toPort,
|
||||
frontendFromId: frontendFromId,
|
||||
frontendToId: frontendToId,
|
||||
fromPortClass: fromPortClass,
|
||||
toPortClass: toPortClass
|
||||
};
|
||||
undoStack.push({ name: "graphRemoveConnection", action: action });
|
||||
actions.graphRemoveConnection.execute(action);
|
||||
updateMenu();
|
||||
},
|
||||
execute: async (action) => {
|
||||
// Suppress action recording during undo/redo
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('graph_disconnect', {
|
||||
trackId: action.trackId,
|
||||
fromNode: action.fromNode,
|
||||
fromPort: action.fromPort,
|
||||
toNode: action.toNode,
|
||||
toPort: action.toPort
|
||||
});
|
||||
if (context.nodeEditor) {
|
||||
context.nodeEditor.removeSingleConnection(
|
||||
action.frontendFromId,
|
||||
action.frontendToId,
|
||||
action.fromPortClass,
|
||||
action.toPortClass
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
rollback: async (action) => {
|
||||
// Suppress action recording during undo/redo
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke('graph_connect', {
|
||||
trackId: action.trackId,
|
||||
fromNode: action.fromNode,
|
||||
fromPort: action.fromPort,
|
||||
toNode: action.toNode,
|
||||
toPort: action.toPort
|
||||
});
|
||||
// Re-add connection in frontend
|
||||
if (context.nodeEditor) {
|
||||
context.nodeEditor.addConnection(
|
||||
action.frontendFromId,
|
||||
action.frontendToId,
|
||||
action.fromPortClass,
|
||||
action.toPortClass
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.nodeEditorState) {
|
||||
context.nodeEditorState.suppressActionRecording = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
graphSetParameter: {
|
||||
initialize: (trackId, nodeId, paramId, frontendNodeId, currentValue) => {
|
||||
return {
|
||||
trackId: trackId,
|
||||
nodeId: nodeId,
|
||||
paramId: paramId,
|
||||
frontendNodeId: frontendNodeId,
|
||||
oldValue: currentValue,
|
||||
};
|
||||
},
|
||||
finalize: (action, newValue) => {
|
||||
action.newValue = newValue;
|
||||
// Only record if value actually changed
|
||||
if (action.oldValue !== action.newValue) {
|
||||
undoStack.push({ name: "graphSetParameter", action: action });
|
||||
updateMenu();
|
||||
}
|
||||
},
|
||||
create: (trackId, nodeId, paramId, frontendNodeId, newValue, oldValue) => {
|
||||
redoStack.length = 0;
|
||||
let action = {
|
||||
trackId: trackId,
|
||||
nodeId: nodeId,
|
||||
paramId: paramId,
|
||||
frontendNodeId: frontendNodeId,
|
||||
newValue: newValue,
|
||||
oldValue: oldValue,
|
||||
};
|
||||
undoStack.push({ name: "graphSetParameter", action: action });
|
||||
actions.graphSetParameter.execute(action);
|
||||
updateMenu();
|
||||
},
|
||||
execute: async (action) => {
|
||||
await invoke('graph_set_parameter', {
|
||||
trackId: action.trackId,
|
||||
nodeId: action.nodeId,
|
||||
paramId: action.paramId,
|
||||
value: action.newValue
|
||||
});
|
||||
// Update frontend slider if it exists
|
||||
const slider = document.querySelector(`#node-${action.frontendNodeId} input[data-param="${action.paramId}"]`);
|
||||
if (slider) {
|
||||
slider.value = action.newValue;
|
||||
// Trigger display update
|
||||
slider.dispatchEvent(new Event('input'));
|
||||
}
|
||||
},
|
||||
rollback: async (action) => {
|
||||
await invoke('graph_set_parameter', {
|
||||
trackId: action.trackId,
|
||||
nodeId: action.nodeId,
|
||||
paramId: action.paramId,
|
||||
value: action.oldValue
|
||||
});
|
||||
// Update frontend slider
|
||||
const slider = document.querySelector(`#node-${action.frontendNodeId} input[data-param="${action.paramId}"]`);
|
||||
if (slider) {
|
||||
slider.value = action.oldValue;
|
||||
slider.dispatchEvent(new Event('input'));
|
||||
}
|
||||
},
|
||||
},
|
||||
graphMoveNode: {
|
||||
create: (trackId, nodeId, oldPosition, newPosition) => {
|
||||
redoStack.length = 0;
|
||||
let action = {
|
||||
trackId: trackId,
|
||||
nodeId: nodeId,
|
||||
oldPosition: oldPosition,
|
||||
newPosition: newPosition,
|
||||
};
|
||||
undoStack.push({ name: "graphMoveNode", action: action });
|
||||
// Don't call execute - movement already happened in UI
|
||||
updateMenu();
|
||||
},
|
||||
execute: (action) => {
|
||||
// Move node in frontend
|
||||
if (context.nodeEditor) {
|
||||
const node = context.nodeEditor.getNodeFromId(action.nodeId);
|
||||
if (node) {
|
||||
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_x = action.newPosition.x;
|
||||
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_y = action.newPosition.y;
|
||||
// Update visual position
|
||||
const nodeElement = document.getElementById(`node-${action.nodeId}`);
|
||||
if (nodeElement) {
|
||||
nodeElement.style.left = action.newPosition.x + 'px';
|
||||
nodeElement.style.top = action.newPosition.y + 'px';
|
||||
}
|
||||
context.nodeEditor.updateConnectionNodes(`node-${action.nodeId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
rollback: (action) => {
|
||||
// Move node back to old position
|
||||
if (context.nodeEditor) {
|
||||
const node = context.nodeEditor.getNodeFromId(action.nodeId);
|
||||
if (node) {
|
||||
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_x = action.oldPosition.x;
|
||||
context.nodeEditor.drawflow.drawflow[context.nodeEditor.module].data[action.nodeId].pos_y = action.oldPosition.y;
|
||||
const nodeElement = document.getElementById(`node-${action.nodeId}`);
|
||||
if (nodeElement) {
|
||||
nodeElement.style.left = action.oldPosition.x + 'px';
|
||||
nodeElement.style.top = action.oldPosition.y + 'px';
|
||||
}
|
||||
context.nodeEditor.updateConnectionNodes(`node-${action.nodeId}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
193
src/main.js
193
src/main.js
|
|
@ -6213,6 +6213,15 @@ function nodeEditor() {
|
|||
const expandedNodes = new Set(); // Set of node IDs that are expanded
|
||||
const nodeParents = new Map(); // Map of child node ID -> parent VoiceAllocator ID
|
||||
|
||||
// Cache node data for undo/redo (nodeId -> {nodeType, backendId, position, parameters})
|
||||
const nodeDataCache = new Map();
|
||||
|
||||
// Track node movement for undo/redo (nodeId -> {oldX, oldY})
|
||||
const nodeMoveTracker = new Map();
|
||||
|
||||
// Flag to prevent recording actions during undo/redo operations
|
||||
let suppressActionRecording = false;
|
||||
|
||||
// Wait for DOM insertion
|
||||
setTimeout(() => {
|
||||
const drawflowDiv = container.querySelector("#drawflow");
|
||||
|
|
@ -6226,43 +6235,39 @@ function nodeEditor() {
|
|||
|
||||
// Store editor reference in context
|
||||
context.nodeEditor = editor;
|
||||
context.reloadNodeEditor = reloadGraph;
|
||||
context.nodeEditorState = {
|
||||
get suppressActionRecording() { return suppressActionRecording; },
|
||||
set suppressActionRecording(value) { suppressActionRecording = value; }
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
|
@ -6279,10 +6284,8 @@ function nodeEditor() {
|
|||
// 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
|
||||
|
|
@ -6512,6 +6515,18 @@ function nodeEditor() {
|
|||
}, 50);
|
||||
});
|
||||
|
||||
// Track node drag start for undo/redo
|
||||
drawflowDiv.addEventListener('mousedown', (e) => {
|
||||
const nodeElement = e.target.closest('.drawflow-node');
|
||||
if (nodeElement && !e.target.closest('.input') && !e.target.closest('.output')) {
|
||||
const nodeId = parseInt(nodeElement.id.replace('node-', ''));
|
||||
const node = editor.getNodeFromId(nodeId);
|
||||
if (node) {
|
||||
nodeMoveTracker.set(nodeId, { x: node.pos_x, y: node.pos_y });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Node moved - resize parent VoiceAllocator
|
||||
editor.on("nodeMoved", (nodeId) => {
|
||||
const node = editor.getNodeFromId(nodeId);
|
||||
|
|
@ -6520,6 +6535,29 @@ function nodeEditor() {
|
|||
}
|
||||
});
|
||||
|
||||
// Track node drag end for undo/redo
|
||||
drawflowDiv.addEventListener('mouseup', (e) => {
|
||||
// Check all tracked nodes for position changes
|
||||
for (const [nodeId, oldPos] of nodeMoveTracker.entries()) {
|
||||
const node = editor.getNodeFromId(nodeId);
|
||||
if (node && (node.pos_x !== oldPos.x || node.pos_y !== oldPos.y)) {
|
||||
// Position changed - record action
|
||||
redoStack.length = 0;
|
||||
undoStack.push({
|
||||
name: "graphMoveNode",
|
||||
action: {
|
||||
nodeId: nodeId,
|
||||
oldPosition: oldPos,
|
||||
newPosition: { x: node.pos_x, y: node.pos_y }
|
||||
}
|
||||
});
|
||||
updateMenu();
|
||||
}
|
||||
}
|
||||
// Clear tracker
|
||||
nodeMoveTracker.clear();
|
||||
});
|
||||
|
||||
// Node removed - prevent deletion of template nodes
|
||||
editor.on("nodeRemoved", (nodeId) => {
|
||||
const nodeElement = document.getElementById(`node-${nodeId}`);
|
||||
|
|
@ -6529,6 +6567,32 @@ function nodeEditor() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Get cached node data before removal
|
||||
const cachedData = nodeDataCache.get(nodeId);
|
||||
|
||||
if (cachedData && cachedData.backendId) {
|
||||
// Call backend to remove the node
|
||||
invoke('graph_remove_node', {
|
||||
trackId: cachedData.trackId,
|
||||
nodeId: cachedData.backendId
|
||||
}).catch(err => {
|
||||
console.error("Failed to remove node from backend:", err);
|
||||
});
|
||||
|
||||
// Record action for undo (don't call execute since node is already removed from frontend)
|
||||
redoStack.length = 0;
|
||||
undoStack.push({
|
||||
name: "graphRemoveNode",
|
||||
action: {
|
||||
trackId: cachedData.trackId,
|
||||
nodeId: nodeId,
|
||||
backendId: cachedData.backendId,
|
||||
nodeData: cachedData
|
||||
}
|
||||
});
|
||||
updateMenu();
|
||||
}
|
||||
|
||||
// Stop oscilloscope visualization if this was an Oscilloscope node
|
||||
stopOscilloscopeVisualization(nodeId);
|
||||
|
||||
|
|
@ -6536,6 +6600,9 @@ function nodeEditor() {
|
|||
const parentId = nodeParents.get(nodeId);
|
||||
nodeParents.delete(nodeId);
|
||||
|
||||
// Clean up node data cache
|
||||
nodeDataCache.delete(nodeId);
|
||||
|
||||
// Resize parent if needed
|
||||
if (parentId) {
|
||||
resizeVoiceAllocatorToFit(parentId);
|
||||
|
|
@ -6675,6 +6742,29 @@ function nodeEditor() {
|
|||
|
||||
console.log("Verifying stored backend ID:", editor.getNodeFromId(drawflowNodeId).data.backendId);
|
||||
|
||||
// Cache node data for undo/redo
|
||||
nodeDataCache.set(drawflowNodeId, {
|
||||
nodeType: nodeType,
|
||||
backendId: backendNodeId,
|
||||
position: { x, y },
|
||||
parentNodeId: parentNodeId,
|
||||
trackId: getCurrentMidiTrack()
|
||||
});
|
||||
|
||||
// Record action for undo (node is already added to frontend and backend)
|
||||
redoStack.length = 0;
|
||||
undoStack.push({
|
||||
name: "graphAddNode",
|
||||
action: {
|
||||
trackId: getCurrentMidiTrack(),
|
||||
nodeType: nodeType,
|
||||
position: { x, y },
|
||||
nodeId: drawflowNodeId,
|
||||
backendId: backendNodeId
|
||||
}
|
||||
});
|
||||
updateMenu();
|
||||
|
||||
// If this is an AudioOutput node, automatically set it as the graph output
|
||||
if (nodeType === "AudioOutput") {
|
||||
console.log(`Setting node ${backendNodeId} as graph output`);
|
||||
|
|
@ -6822,9 +6912,30 @@ function nodeEditor() {
|
|||
|
||||
const sliders = nodeElement.querySelectorAll('input[type="range"]');
|
||||
sliders.forEach(slider => {
|
||||
// Track parameter change action for undo/redo
|
||||
let paramAction = null;
|
||||
|
||||
// Prevent node dragging when interacting with slider
|
||||
slider.addEventListener("mousedown", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Initialize undo action
|
||||
const paramId = parseInt(e.target.getAttribute("data-param"));
|
||||
const currentValue = parseFloat(e.target.value);
|
||||
const nodeData = editor.getNodeFromId(nodeId);
|
||||
|
||||
if (nodeData && nodeData.data.backendId !== null) {
|
||||
const currentTrackId = getCurrentMidiTrack();
|
||||
if (currentTrackId !== null) {
|
||||
paramAction = actions.graphSetParameter.initialize(
|
||||
currentTrackId,
|
||||
nodeData.data.backendId,
|
||||
paramId,
|
||||
nodeId,
|
||||
currentValue
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
slider.addEventListener("pointerdown", (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -6863,7 +6974,7 @@ function nodeEditor() {
|
|||
}
|
||||
}
|
||||
|
||||
// Send to backend
|
||||
// Send to backend in real-time
|
||||
if (nodeData.data.backendId !== null) {
|
||||
const currentTrackId = getCurrentMidiTrack();
|
||||
if (currentTrackId !== null) {
|
||||
|
|
@ -6879,6 +6990,16 @@ function nodeEditor() {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Finalize parameter change for undo/redo when slider is released
|
||||
slider.addEventListener("change", (e) => {
|
||||
const newValue = parseFloat(e.target.value);
|
||||
|
||||
if (paramAction) {
|
||||
actions.graphSetParameter.finalize(paramAction, newValue);
|
||||
paramAction = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle Load Sample button for SimpleSampler
|
||||
|
|
@ -7234,9 +7355,9 @@ function nodeEditor() {
|
|||
}
|
||||
}, 10);
|
||||
|
||||
// Send to backend
|
||||
// Send to backend (skip if action is handling it)
|
||||
console.log("Backend IDs - output:", outputNode.data.backendId, "input:", inputNode.data.backendId);
|
||||
if (outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
||||
if (!suppressActionRecording && outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
||||
const currentTrackId = getCurrentMidiTrack();
|
||||
if (currentTrackId === null) return;
|
||||
|
||||
|
|
@ -7298,7 +7419,7 @@ function nodeEditor() {
|
|||
);
|
||||
});
|
||||
} else {
|
||||
// Normal connection in main graph
|
||||
// Normal connection in main graph (skip if action is handling it)
|
||||
console.log(`Connecting: node ${outputNode.data.backendId} port ${outputPort} -> node ${inputNode.data.backendId} port ${inputPort}`);
|
||||
invoke("graph_connect", {
|
||||
trackId: currentTrackId,
|
||||
|
|
@ -7308,6 +7429,25 @@ function nodeEditor() {
|
|||
toPort: inputPort
|
||||
}).then(() => {
|
||||
console.log("Connection successful");
|
||||
|
||||
// Record action for undo
|
||||
redoStack.length = 0;
|
||||
undoStack.push({
|
||||
name: "graphAddConnection",
|
||||
action: {
|
||||
trackId: currentTrackId,
|
||||
fromNode: outputNode.data.backendId,
|
||||
fromPort: outputPort,
|
||||
toNode: inputNode.data.backendId,
|
||||
toPort: inputPort,
|
||||
// Store frontend IDs for disconnection
|
||||
frontendFromId: connection.output_id,
|
||||
frontendToId: connection.input_id,
|
||||
fromPortClass: connection.output_class,
|
||||
toPortClass: connection.input_class
|
||||
}
|
||||
});
|
||||
updateMenu();
|
||||
}).catch(err => {
|
||||
console.error("Failed to connect nodes:", err);
|
||||
showError("Connection failed: " + err);
|
||||
|
|
@ -7364,8 +7504,8 @@ function nodeEditor() {
|
|||
}
|
||||
}
|
||||
|
||||
// Send to backend
|
||||
if (outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
||||
// Send to backend (skip if action is handling it)
|
||||
if (!suppressActionRecording && outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
||||
const currentTrackId = getCurrentMidiTrack();
|
||||
if (currentTrackId !== null) {
|
||||
invoke("graph_disconnect", {
|
||||
|
|
@ -7374,6 +7514,25 @@ function nodeEditor() {
|
|||
fromPort: outputPort,
|
||||
toNode: inputNode.data.backendId,
|
||||
toPort: inputPort
|
||||
}).then(() => {
|
||||
// Record action for undo
|
||||
redoStack.length = 0;
|
||||
undoStack.push({
|
||||
name: "graphRemoveConnection",
|
||||
action: {
|
||||
trackId: currentTrackId,
|
||||
fromNode: outputNode.data.backendId,
|
||||
fromPort: outputPort,
|
||||
toNode: inputNode.data.backendId,
|
||||
toPort: inputPort,
|
||||
// Store frontend IDs for reconnection
|
||||
frontendFromId: connection.output_id,
|
||||
frontendToId: connection.input_id,
|
||||
fromPortClass: connection.output_class,
|
||||
toPortClass: connection.input_class
|
||||
}
|
||||
});
|
||||
updateMenu();
|
||||
}).catch(err => {
|
||||
console.error("Failed to disconnect nodes:", err);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue