mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): updatable edges in workflow editor (#4701)
- Drag the end of an edge away from its handle to disconnect it - Drop in empty space to delete the edge - Drop on valid handle to reconnect it - Update connection logic slightly to allow edge updates
This commit is contained in:
parent
a4cdaa245e
commit
a953944894
@ -717,6 +717,7 @@
|
|||||||
"cannotConnectInputToInput": "Cannot connect input to input",
|
"cannotConnectInputToInput": "Cannot connect input to input",
|
||||||
"cannotConnectOutputToOutput": "Cannot connect output to output",
|
"cannotConnectOutputToOutput": "Cannot connect output to output",
|
||||||
"cannotConnectToSelf": "Cannot connect to self",
|
"cannotConnectToSelf": "Cannot connect to self",
|
||||||
|
"cannotDuplicateConnection": "Cannot create duplicate connections",
|
||||||
"clipField": "Clip",
|
"clipField": "Clip",
|
||||||
"clipFieldDescription": "Tokenizer and text_encoder submodels.",
|
"clipFieldDescription": "Tokenizer and text_encoder submodels.",
|
||||||
"collection": "Collection",
|
"collection": "Collection",
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
OnConnect,
|
OnConnect,
|
||||||
OnConnectEnd,
|
OnConnectEnd,
|
||||||
OnConnectStart,
|
OnConnectStart,
|
||||||
|
OnEdgeUpdateFunc,
|
||||||
OnEdgesChange,
|
OnEdgesChange,
|
||||||
OnEdgesDelete,
|
OnEdgesDelete,
|
||||||
OnInit,
|
OnInit,
|
||||||
@ -21,6 +22,7 @@ import {
|
|||||||
OnSelectionChangeFunc,
|
OnSelectionChangeFunc,
|
||||||
ProOptions,
|
ProOptions,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
|
ReactFlowProps,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
|
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
|
||||||
@ -28,6 +30,8 @@ import {
|
|||||||
connectionEnded,
|
connectionEnded,
|
||||||
connectionMade,
|
connectionMade,
|
||||||
connectionStarted,
|
connectionStarted,
|
||||||
|
edgeAdded,
|
||||||
|
edgeDeleted,
|
||||||
edgesChanged,
|
edgesChanged,
|
||||||
edgesDeleted,
|
edgesDeleted,
|
||||||
nodesChanged,
|
nodesChanged,
|
||||||
@ -167,6 +171,63 @@ export const Flow = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// #region Updatable Edges
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from https://reactflow.dev/docs/examples/edges/updatable-edge/
|
||||||
|
* and https://reactflow.dev/docs/examples/edges/delete-edge-on-drop/
|
||||||
|
*
|
||||||
|
* - Edges can be dragged from one handle to another.
|
||||||
|
* - If the user drags the edge away from the node and drops it, delete the edge.
|
||||||
|
* - Do not delete the edge if the cursor didn't move (resolves annoying behaviour
|
||||||
|
* where the edge is deleted if you click it accidentally).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// We have a ref for cursor position, but it is the *projected* cursor position.
|
||||||
|
// Easiest to just keep track of the last mouse event for this particular feature
|
||||||
|
const edgeUpdateMouseEvent = useRef<MouseEvent>();
|
||||||
|
|
||||||
|
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> =
|
||||||
|
useCallback(
|
||||||
|
(e, edge, _handleType) => {
|
||||||
|
// update mouse event
|
||||||
|
edgeUpdateMouseEvent.current = e;
|
||||||
|
// always delete the edge when starting an updated
|
||||||
|
dispatch(edgeDeleted(edge.id));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
|
||||||
|
(_oldEdge, newConnection) => {
|
||||||
|
// instead of updating the edge (we deleted it earlier), we instead create
|
||||||
|
// a new one.
|
||||||
|
dispatch(connectionMade(newConnection));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> =
|
||||||
|
useCallback(
|
||||||
|
(e, edge, _handleType) => {
|
||||||
|
// Handle the case where user begins a drag but didn't move the cursor -
|
||||||
|
// bc we deleted the edge, we need to add it back
|
||||||
|
if (
|
||||||
|
// ignore touch events
|
||||||
|
!('touches' in e) &&
|
||||||
|
edgeUpdateMouseEvent.current?.clientX === e.clientX &&
|
||||||
|
edgeUpdateMouseEvent.current?.clientY === e.clientY
|
||||||
|
) {
|
||||||
|
dispatch(edgeAdded(edge));
|
||||||
|
}
|
||||||
|
// reset mouse event
|
||||||
|
edgeUpdateMouseEvent.current = undefined;
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
|
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch(selectionCopied());
|
dispatch(selectionCopied());
|
||||||
@ -196,6 +257,9 @@ export const Flow = () => {
|
|||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onEdgesDelete={onEdgesDelete}
|
onEdgesDelete={onEdgesDelete}
|
||||||
|
onEdgeUpdate={onEdgeUpdate}
|
||||||
|
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||||
|
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||||
onNodesDelete={onNodesDelete}
|
onNodesDelete={onNodesDelete}
|
||||||
onConnectStart={onConnectStart}
|
onConnectStart={onConnectStart}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
@ -53,13 +53,12 @@ export const useIsValidConnection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
edges
|
edges.find((edge) => {
|
||||||
.filter((edge) => {
|
edge.target === target &&
|
||||||
return edge.target === target && edge.targetHandle === targetHandle;
|
edge.targetHandle === targetHandle &&
|
||||||
})
|
edge.source === source &&
|
||||||
.find((edge) => {
|
edge.sourceHandle === sourceHandle;
|
||||||
edge.source === source && edge.sourceHandle === sourceHandle;
|
})
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
// We already have a connection from this source to this target
|
// We already have a connection from this source to this target
|
||||||
return false;
|
return false;
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
NodeChange,
|
NodeChange,
|
||||||
OnConnectStartParams,
|
OnConnectStartParams,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
|
updateEdge,
|
||||||
Viewport,
|
Viewport,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
@ -182,6 +183,16 @@ const nodesSlice = createSlice({
|
|||||||
edgesChanged: (state, action: PayloadAction<EdgeChange[]>) => {
|
edgesChanged: (state, action: PayloadAction<EdgeChange[]>) => {
|
||||||
state.edges = applyEdgeChanges(action.payload, state.edges);
|
state.edges = applyEdgeChanges(action.payload, state.edges);
|
||||||
},
|
},
|
||||||
|
edgeAdded: (state, action: PayloadAction<Edge>) => {
|
||||||
|
state.edges = addEdge(action.payload, state.edges);
|
||||||
|
},
|
||||||
|
edgeUpdated: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ oldEdge: Edge; newConnection: Connection }>
|
||||||
|
) => {
|
||||||
|
const { oldEdge, newConnection } = action.payload;
|
||||||
|
state.edges = updateEdge(oldEdge, newConnection, state.edges);
|
||||||
|
},
|
||||||
connectionStarted: (state, action: PayloadAction<OnConnectStartParams>) => {
|
connectionStarted: (state, action: PayloadAction<OnConnectStartParams>) => {
|
||||||
state.connectionStartParams = action.payload;
|
state.connectionStartParams = action.payload;
|
||||||
const { nodeId, handleId, handleType } = action.payload;
|
const { nodeId, handleId, handleType } = action.payload;
|
||||||
@ -366,6 +377,7 @@ const nodesSlice = createSlice({
|
|||||||
target: edge.target,
|
target: edge.target,
|
||||||
type: 'collapsed',
|
type: 'collapsed',
|
||||||
data: { count: 1 },
|
data: { count: 1 },
|
||||||
|
updatable: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -388,6 +400,7 @@ const nodesSlice = createSlice({
|
|||||||
target: edge.target,
|
target: edge.target,
|
||||||
type: 'collapsed',
|
type: 'collapsed',
|
||||||
data: { count: 1 },
|
data: { count: 1 },
|
||||||
|
updatable: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,6 +413,9 @@ const nodesSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
edgeDeleted: (state, action: PayloadAction<string>) => {
|
||||||
|
state.edges = state.edges.filter((e) => e.id !== action.payload);
|
||||||
|
},
|
||||||
edgesDeleted: (state, action: PayloadAction<Edge[]>) => {
|
edgesDeleted: (state, action: PayloadAction<Edge[]>) => {
|
||||||
const edges = action.payload;
|
const edges = action.payload;
|
||||||
const collapsedEdges = edges.filter((e) => e.type === 'collapsed');
|
const collapsedEdges = edges.filter((e) => e.type === 'collapsed');
|
||||||
@ -890,69 +906,72 @@ const nodesSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
nodesChanged,
|
addNodePopoverClosed,
|
||||||
edgesChanged,
|
addNodePopoverOpened,
|
||||||
nodeAdded,
|
addNodePopoverToggled,
|
||||||
nodesDeleted,
|
connectionEnded,
|
||||||
connectionMade,
|
connectionMade,
|
||||||
connectionStarted,
|
connectionStarted,
|
||||||
connectionEnded,
|
edgeDeleted,
|
||||||
shouldShowFieldTypeLegendChanged,
|
edgesChanged,
|
||||||
shouldShowMinimapPanelChanged,
|
edgesDeleted,
|
||||||
nodeTemplatesBuilt,
|
edgeUpdated,
|
||||||
nodeEditorReset,
|
|
||||||
imageCollectionFieldValueChanged,
|
|
||||||
fieldStringValueChanged,
|
|
||||||
fieldNumberValueChanged,
|
|
||||||
fieldBoardValueChanged,
|
fieldBoardValueChanged,
|
||||||
fieldBooleanValueChanged,
|
fieldBooleanValueChanged,
|
||||||
fieldImageValueChanged,
|
|
||||||
fieldColorValueChanged,
|
fieldColorValueChanged,
|
||||||
fieldMainModelValueChanged,
|
|
||||||
fieldVaeModelValueChanged,
|
|
||||||
fieldLoRAModelValueChanged,
|
|
||||||
fieldEnumModelValueChanged,
|
|
||||||
fieldControlNetModelValueChanged,
|
fieldControlNetModelValueChanged,
|
||||||
|
fieldEnumModelValueChanged,
|
||||||
|
fieldImageValueChanged,
|
||||||
fieldIPAdapterModelValueChanged,
|
fieldIPAdapterModelValueChanged,
|
||||||
|
fieldLabelChanged,
|
||||||
|
fieldLoRAModelValueChanged,
|
||||||
|
fieldMainModelValueChanged,
|
||||||
|
fieldNumberValueChanged,
|
||||||
fieldRefinerModelValueChanged,
|
fieldRefinerModelValueChanged,
|
||||||
fieldSchedulerValueChanged,
|
fieldSchedulerValueChanged,
|
||||||
|
fieldStringValueChanged,
|
||||||
|
fieldVaeModelValueChanged,
|
||||||
|
imageCollectionFieldValueChanged,
|
||||||
|
mouseOverFieldChanged,
|
||||||
|
mouseOverNodeChanged,
|
||||||
|
nodeAdded,
|
||||||
|
nodeEditorReset,
|
||||||
|
nodeEmbedWorkflowChanged,
|
||||||
|
nodeExclusivelySelected,
|
||||||
|
nodeIsIntermediateChanged,
|
||||||
nodeIsOpenChanged,
|
nodeIsOpenChanged,
|
||||||
nodeLabelChanged,
|
nodeLabelChanged,
|
||||||
nodeNotesChanged,
|
nodeNotesChanged,
|
||||||
edgesDeleted,
|
|
||||||
shouldValidateGraphChanged,
|
|
||||||
shouldAnimateEdgesChanged,
|
|
||||||
nodeOpacityChanged,
|
nodeOpacityChanged,
|
||||||
shouldSnapToGridChanged,
|
nodesChanged,
|
||||||
shouldColorEdgesChanged,
|
nodesDeleted,
|
||||||
selectedNodesChanged,
|
nodeTemplatesBuilt,
|
||||||
selectedEdgesChanged,
|
nodeUseCacheChanged,
|
||||||
workflowNameChanged,
|
|
||||||
workflowDescriptionChanged,
|
|
||||||
workflowTagsChanged,
|
|
||||||
workflowAuthorChanged,
|
|
||||||
workflowNotesChanged,
|
|
||||||
workflowVersionChanged,
|
|
||||||
workflowContactChanged,
|
|
||||||
workflowLoaded,
|
|
||||||
notesNodeValueChanged,
|
notesNodeValueChanged,
|
||||||
|
selectedAll,
|
||||||
|
selectedEdgesChanged,
|
||||||
|
selectedNodesChanged,
|
||||||
|
selectionCopied,
|
||||||
|
selectionModeChanged,
|
||||||
|
selectionPasted,
|
||||||
|
shouldAnimateEdgesChanged,
|
||||||
|
shouldColorEdgesChanged,
|
||||||
|
shouldShowFieldTypeLegendChanged,
|
||||||
|
shouldShowMinimapPanelChanged,
|
||||||
|
shouldSnapToGridChanged,
|
||||||
|
shouldValidateGraphChanged,
|
||||||
|
viewportChanged,
|
||||||
|
workflowAuthorChanged,
|
||||||
|
workflowContactChanged,
|
||||||
|
workflowDescriptionChanged,
|
||||||
workflowExposedFieldAdded,
|
workflowExposedFieldAdded,
|
||||||
workflowExposedFieldRemoved,
|
workflowExposedFieldRemoved,
|
||||||
fieldLabelChanged,
|
workflowLoaded,
|
||||||
viewportChanged,
|
workflowNameChanged,
|
||||||
mouseOverFieldChanged,
|
workflowNotesChanged,
|
||||||
selectionCopied,
|
workflowTagsChanged,
|
||||||
selectionPasted,
|
workflowVersionChanged,
|
||||||
selectedAll,
|
edgeAdded,
|
||||||
addNodePopoverOpened,
|
|
||||||
addNodePopoverClosed,
|
|
||||||
addNodePopoverToggled,
|
|
||||||
selectionModeChanged,
|
|
||||||
nodeEmbedWorkflowChanged,
|
|
||||||
nodeIsIntermediateChanged,
|
|
||||||
mouseOverNodeChanged,
|
|
||||||
nodeExclusivelySelected,
|
|
||||||
nodeUseCacheChanged,
|
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -55,9 +55,29 @@ export const makeConnectionErrorSelector = (
|
|||||||
return i18n.t('nodes.cannotConnectInputToInput');
|
return i18n.t('nodes.cannotConnectInputToInput');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we have to figure out which is the target and which is the source
|
||||||
|
const target = handleType === 'target' ? nodeId : connectionNodeId;
|
||||||
|
const targetHandle =
|
||||||
|
handleType === 'target' ? fieldName : connectionFieldName;
|
||||||
|
const source = handleType === 'source' ? nodeId : connectionNodeId;
|
||||||
|
const sourceHandle =
|
||||||
|
handleType === 'source' ? fieldName : connectionFieldName;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
edges.find((edge) => {
|
edges.find((edge) => {
|
||||||
return edge.target === nodeId && edge.targetHandle === fieldName;
|
edge.target === target &&
|
||||||
|
edge.targetHandle === targetHandle &&
|
||||||
|
edge.source === source &&
|
||||||
|
edge.sourceHandle === sourceHandle;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
// We already have a connection from this source to this target
|
||||||
|
return i18n.t('nodes.cannotDuplicateConnection');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
edges.find((edge) => {
|
||||||
|
return edge.target === target && edge.targetHandle === targetHandle;
|
||||||
}) &&
|
}) &&
|
||||||
// except CollectionItem inputs can have multiples
|
// except CollectionItem inputs can have multiples
|
||||||
targetType !== 'CollectionItem'
|
targetType !== 'CollectionItem'
|
||||||
|
Loading…
Reference in New Issue
Block a user