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();
|
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 expandedNodes = new Set(); // Set of node IDs that are expanded
|
||||||
const nodeParents = new Map(); // Map of child node ID -> parent VoiceAllocator ID
|
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
|
// Wait for DOM insertion
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const drawflowDiv = container.querySelector("#drawflow");
|
const drawflowDiv = container.querySelector("#drawflow");
|
||||||
|
|
@ -6226,43 +6235,39 @@ function nodeEditor() {
|
||||||
|
|
||||||
// Store editor reference in context
|
// Store editor reference in context
|
||||||
context.nodeEditor = editor;
|
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
|
// Add reconnection support: dragging from a connected input disconnects and starts new connection
|
||||||
drawflowDiv.addEventListener('mousedown', (e) => {
|
drawflowDiv.addEventListener('mousedown', (e) => {
|
||||||
console.log('Mousedown on drawflow, target:', e.target);
|
|
||||||
|
|
||||||
// Check if clicking on an input port
|
// Check if clicking on an input port
|
||||||
const inputPort = e.target.closest('.input');
|
const inputPort = e.target.closest('.input');
|
||||||
console.log('Found input port:', inputPort);
|
|
||||||
|
|
||||||
if (inputPort) {
|
if (inputPort) {
|
||||||
// Get the node and port information - the drawflow-node div has the id
|
// Get the node and port information - the drawflow-node div has the id
|
||||||
const drawflowNode = inputPort.closest('.drawflow-node');
|
const drawflowNode = inputPort.closest('.drawflow-node');
|
||||||
console.log('Found drawflow node:', drawflowNode, 'ID:', drawflowNode?.id);
|
|
||||||
if (!drawflowNode) return;
|
if (!drawflowNode) return;
|
||||||
|
|
||||||
const nodeId = parseInt(drawflowNode.id.replace('node-', ''));
|
const nodeId = parseInt(drawflowNode.id.replace('node-', ''));
|
||||||
console.log('Node ID:', nodeId);
|
|
||||||
|
|
||||||
// Access the node data directly from the current module
|
// Access the node data directly from the current module
|
||||||
const moduleName = editor.module;
|
const moduleName = editor.module;
|
||||||
const node = editor.drawflow.drawflow[moduleName]?.data[nodeId];
|
const node = editor.drawflow.drawflow[moduleName]?.data[nodeId];
|
||||||
console.log('Node data:', node);
|
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
// Get the port class (input_1, input_2, etc.)
|
// Get the port class (input_1, input_2, etc.)
|
||||||
const portClasses = Array.from(inputPort.classList);
|
const portClasses = Array.from(inputPort.classList);
|
||||||
const portClass = portClasses.find(c => c.startsWith('input_'));
|
const portClass = portClasses.find(c => c.startsWith('input_'));
|
||||||
console.log('Port class:', portClass, 'All classes:', portClasses);
|
|
||||||
if (!portClass) return;
|
if (!portClass) return;
|
||||||
|
|
||||||
// Check if this input has any connections
|
// Check if this input has any connections
|
||||||
const inputConnections = node.inputs[portClass];
|
const inputConnections = node.inputs[portClass];
|
||||||
console.log('Input connections:', inputConnections);
|
|
||||||
if (inputConnections && inputConnections.connections && inputConnections.connections.length > 0) {
|
if (inputConnections && inputConnections.connections && inputConnections.connections.length > 0) {
|
||||||
// Get the first connection (inputs should only have one connection)
|
// Get the first connection (inputs should only have one connection)
|
||||||
const connection = inputConnections.connections[0];
|
const connection = inputConnections.connections[0];
|
||||||
console.log('Found connection to disconnect:', connection);
|
|
||||||
|
|
||||||
// Prevent default to avoid interfering with the drag
|
// Prevent default to avoid interfering with the drag
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -6279,10 +6284,8 @@ function nodeEditor() {
|
||||||
// Now trigger Drawflow's connection drag from the output that was connected
|
// Now trigger Drawflow's connection drag from the output that was connected
|
||||||
// We need to simulate starting a drag from the output port
|
// We need to simulate starting a drag from the output port
|
||||||
const outputNodeElement = document.getElementById(`node-${connection.node}`);
|
const outputNodeElement = document.getElementById(`node-${connection.node}`);
|
||||||
console.log('Output node element:', outputNodeElement);
|
|
||||||
if (outputNodeElement) {
|
if (outputNodeElement) {
|
||||||
const outputPort = outputNodeElement.querySelector(`.${connection.input}`);
|
const outputPort = outputNodeElement.querySelector(`.${connection.input}`);
|
||||||
console.log('Output port:', outputPort, 'class:', connection.input);
|
|
||||||
if (outputPort) {
|
if (outputPort) {
|
||||||
// Dispatch a synthetic mousedown event on the output port
|
// Dispatch a synthetic mousedown event on the output port
|
||||||
// This will trigger Drawflow's normal connection start logic
|
// This will trigger Drawflow's normal connection start logic
|
||||||
|
|
@ -6512,6 +6515,18 @@ function nodeEditor() {
|
||||||
}, 50);
|
}, 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
|
// Node moved - resize parent VoiceAllocator
|
||||||
editor.on("nodeMoved", (nodeId) => {
|
editor.on("nodeMoved", (nodeId) => {
|
||||||
const node = editor.getNodeFromId(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
|
// Node removed - prevent deletion of template nodes
|
||||||
editor.on("nodeRemoved", (nodeId) => {
|
editor.on("nodeRemoved", (nodeId) => {
|
||||||
const nodeElement = document.getElementById(`node-${nodeId}`);
|
const nodeElement = document.getElementById(`node-${nodeId}`);
|
||||||
|
|
@ -6529,6 +6567,32 @@ function nodeEditor() {
|
||||||
return;
|
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
|
// Stop oscilloscope visualization if this was an Oscilloscope node
|
||||||
stopOscilloscopeVisualization(nodeId);
|
stopOscilloscopeVisualization(nodeId);
|
||||||
|
|
||||||
|
|
@ -6536,6 +6600,9 @@ function nodeEditor() {
|
||||||
const parentId = nodeParents.get(nodeId);
|
const parentId = nodeParents.get(nodeId);
|
||||||
nodeParents.delete(nodeId);
|
nodeParents.delete(nodeId);
|
||||||
|
|
||||||
|
// Clean up node data cache
|
||||||
|
nodeDataCache.delete(nodeId);
|
||||||
|
|
||||||
// Resize parent if needed
|
// Resize parent if needed
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
resizeVoiceAllocatorToFit(parentId);
|
resizeVoiceAllocatorToFit(parentId);
|
||||||
|
|
@ -6675,6 +6742,29 @@ function nodeEditor() {
|
||||||
|
|
||||||
console.log("Verifying stored backend ID:", editor.getNodeFromId(drawflowNodeId).data.backendId);
|
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 this is an AudioOutput node, automatically set it as the graph output
|
||||||
if (nodeType === "AudioOutput") {
|
if (nodeType === "AudioOutput") {
|
||||||
console.log(`Setting node ${backendNodeId} as graph output`);
|
console.log(`Setting node ${backendNodeId} as graph output`);
|
||||||
|
|
@ -6822,9 +6912,30 @@ function nodeEditor() {
|
||||||
|
|
||||||
const sliders = nodeElement.querySelectorAll('input[type="range"]');
|
const sliders = nodeElement.querySelectorAll('input[type="range"]');
|
||||||
sliders.forEach(slider => {
|
sliders.forEach(slider => {
|
||||||
|
// Track parameter change action for undo/redo
|
||||||
|
let paramAction = null;
|
||||||
|
|
||||||
// Prevent node dragging when interacting with slider
|
// Prevent node dragging when interacting with slider
|
||||||
slider.addEventListener("mousedown", (e) => {
|
slider.addEventListener("mousedown", (e) => {
|
||||||
e.stopPropagation();
|
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) => {
|
slider.addEventListener("pointerdown", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -6863,7 +6974,7 @@ function nodeEditor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to backend
|
// Send to backend in real-time
|
||||||
if (nodeData.data.backendId !== null) {
|
if (nodeData.data.backendId !== null) {
|
||||||
const currentTrackId = getCurrentMidiTrack();
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
if (currentTrackId !== null) {
|
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
|
// Handle Load Sample button for SimpleSampler
|
||||||
|
|
@ -7234,9 +7355,9 @@ function nodeEditor() {
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 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);
|
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();
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
if (currentTrackId === null) return;
|
if (currentTrackId === null) return;
|
||||||
|
|
||||||
|
|
@ -7298,7 +7419,7 @@ function nodeEditor() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} 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}`);
|
console.log(`Connecting: node ${outputNode.data.backendId} port ${outputPort} -> node ${inputNode.data.backendId} port ${inputPort}`);
|
||||||
invoke("graph_connect", {
|
invoke("graph_connect", {
|
||||||
trackId: currentTrackId,
|
trackId: currentTrackId,
|
||||||
|
|
@ -7308,6 +7429,25 @@ function nodeEditor() {
|
||||||
toPort: inputPort
|
toPort: inputPort
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
console.log("Connection successful");
|
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 => {
|
}).catch(err => {
|
||||||
console.error("Failed to connect nodes:", err);
|
console.error("Failed to connect nodes:", err);
|
||||||
showError("Connection failed: " + err);
|
showError("Connection failed: " + err);
|
||||||
|
|
@ -7364,8 +7504,8 @@ function nodeEditor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to backend
|
// Send to backend (skip if action is handling it)
|
||||||
if (outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
if (!suppressActionRecording && outputNode.data.backendId !== null && inputNode.data.backendId !== null) {
|
||||||
const currentTrackId = getCurrentMidiTrack();
|
const currentTrackId = getCurrentMidiTrack();
|
||||||
if (currentTrackId !== null) {
|
if (currentTrackId !== null) {
|
||||||
invoke("graph_disconnect", {
|
invoke("graph_disconnect", {
|
||||||
|
|
@ -7374,6 +7514,25 @@ function nodeEditor() {
|
||||||
fromPort: outputPort,
|
fromPort: outputPort,
|
||||||
toNode: inputNode.data.backendId,
|
toNode: inputNode.data.backendId,
|
||||||
toPort: inputPort
|
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 => {
|
}).catch(err => {
|
||||||
console.error("Failed to disconnect nodes:", err);
|
console.error("Failed to disconnect nodes:", err);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue