mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): node delete, copy, paste
This commit is contained in:
parent
567d46b646
commit
519bcb38c1
@ -2,6 +2,7 @@ import { useToken } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
Background,
|
||||
OnConnect,
|
||||
@ -27,6 +28,8 @@ import {
|
||||
nodesDeleted,
|
||||
selectedEdgesChanged,
|
||||
selectedNodesChanged,
|
||||
selectionCopied,
|
||||
selectionPasted,
|
||||
viewportChanged,
|
||||
} from '../store/nodesSlice';
|
||||
import { CustomConnectionLine } from './CustomConnectionLine';
|
||||
@ -121,8 +124,17 @@ export const Flow = () => {
|
||||
dispatch(contextMenusClosed());
|
||||
}, [dispatch]);
|
||||
|
||||
useHotkeys(['Ctrl+c', 'Meta+c'], () => {
|
||||
dispatch(selectionCopied());
|
||||
});
|
||||
|
||||
useHotkeys(['Ctrl+v', 'Meta+v'], () => {
|
||||
dispatch(selectionPasted());
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
id="workflow-editor"
|
||||
defaultViewport={viewport}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
OutputFieldValue,
|
||||
} from '../types/types';
|
||||
import { buildInputFieldValue } from '../util/fieldValueBuilders';
|
||||
import { DRAG_HANDLE_CLASSNAME } from '../types/constants';
|
||||
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../types/constants';
|
||||
|
||||
const templatesSelector = createSelector(
|
||||
[(state: RootState) => state.nodes],
|
||||
@ -34,10 +34,24 @@ export const useBuildNodeData = () => {
|
||||
(type: AnyInvocationType | 'current_image' | 'notes') => {
|
||||
const nodeId = uuidv4();
|
||||
|
||||
let _x = window.innerWidth / 2;
|
||||
let _y = window.innerHeight / 2;
|
||||
|
||||
// attempt to center the node in the middle of the flow
|
||||
const rect = document
|
||||
.querySelector('#workflow-editor')
|
||||
?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
_x = rect.width / 2 - NODE_WIDTH / 2;
|
||||
_y = rect.height / 2 - NODE_WIDTH / 2;
|
||||
}
|
||||
|
||||
const { x, y } = flow.project({
|
||||
x: window.innerWidth / 2.5,
|
||||
y: window.innerHeight / 8,
|
||||
x: _x,
|
||||
y: _y,
|
||||
});
|
||||
|
||||
if (type === 'current_image') {
|
||||
const node: Node<CurrentImageNodeData> = {
|
||||
...SHARED_NODE_PROPERTIES,
|
||||
|
@ -11,4 +11,6 @@ export const nodesPersistDenylist: (keyof NodesState)[] = [
|
||||
'selectedNodes',
|
||||
'selectedEdges',
|
||||
'isReady',
|
||||
'nodesToCopy',
|
||||
'edgesToCopy',
|
||||
];
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
appSocketInvocationError,
|
||||
appSocketInvocationStarted,
|
||||
} from 'services/events/actions';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { DRAG_HANDLE_CLASSNAME } from '../types/constants';
|
||||
import {
|
||||
BooleanInputFieldValue,
|
||||
@ -52,6 +53,7 @@ import {
|
||||
Workflow,
|
||||
} from '../types/types';
|
||||
import { NodesState } from './types';
|
||||
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';
|
||||
|
||||
export const initialNodesState: NodesState = {
|
||||
nodes: [],
|
||||
@ -83,6 +85,8 @@ export const initialNodesState: NodesState = {
|
||||
nodeExecutionStates: {},
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
mouseOverField: null,
|
||||
nodesToCopy: [],
|
||||
edgesToCopy: [],
|
||||
};
|
||||
|
||||
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
|
||||
@ -124,6 +128,12 @@ const nodesSlice = createSlice({
|
||||
>
|
||||
) => {
|
||||
const node = action.payload;
|
||||
const position = findUnoccupiedPosition(
|
||||
state.nodes,
|
||||
node.position.x,
|
||||
node.position.y
|
||||
);
|
||||
node.position = position;
|
||||
state.nodes.push(node);
|
||||
|
||||
if (!isInvocationNode(node)) {
|
||||
@ -595,6 +605,85 @@ const nodesSlice = createSlice({
|
||||
) => {
|
||||
state.mouseOverField = action.payload;
|
||||
},
|
||||
selectionCopied: (state) => {
|
||||
state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep);
|
||||
state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep);
|
||||
},
|
||||
selectionPasted: (state) => {
|
||||
const newNodes = state.nodesToCopy.map(cloneDeep);
|
||||
const oldNodeIds = newNodes.map((n) => n.data.id);
|
||||
const newEdges = state.edgesToCopy
|
||||
.filter(
|
||||
(e) => oldNodeIds.includes(e.source) && oldNodeIds.includes(e.target)
|
||||
)
|
||||
.map(cloneDeep);
|
||||
|
||||
newEdges.forEach((e) => (e.selected = true));
|
||||
|
||||
newNodes.forEach((node) => {
|
||||
const newNodeId = uuidv4();
|
||||
newEdges.forEach((edge) => {
|
||||
if (edge.source === node.data.id) {
|
||||
edge.source = newNodeId;
|
||||
edge.id = edge.id.replace(node.data.id, newNodeId);
|
||||
}
|
||||
if (edge.target === node.data.id) {
|
||||
edge.target = newNodeId;
|
||||
edge.id = edge.id.replace(node.data.id, newNodeId);
|
||||
}
|
||||
});
|
||||
node.selected = true;
|
||||
node.id = newNodeId;
|
||||
node.data.id = newNodeId;
|
||||
|
||||
const position = findUnoccupiedPosition(
|
||||
state.nodes,
|
||||
node.position.x,
|
||||
node.position.y
|
||||
);
|
||||
|
||||
node.position = position;
|
||||
});
|
||||
|
||||
const nodeAdditions: NodeChange[] = newNodes.map((n) => ({
|
||||
item: n,
|
||||
type: 'add',
|
||||
}));
|
||||
const nodeSelectionChanges: NodeChange[] = state.nodes.map((n) => ({
|
||||
id: n.data.id,
|
||||
type: 'select',
|
||||
selected: false,
|
||||
}));
|
||||
|
||||
const edgeAdditions: EdgeChange[] = newEdges.map((e) => ({
|
||||
item: e,
|
||||
type: 'add',
|
||||
}));
|
||||
const edgeSelectionChanges: EdgeChange[] = state.edges.map((e) => ({
|
||||
id: e.id,
|
||||
type: 'select',
|
||||
selected: false,
|
||||
}));
|
||||
|
||||
state.nodes = applyNodeChanges(
|
||||
nodeAdditions.concat(nodeSelectionChanges),
|
||||
state.nodes
|
||||
);
|
||||
|
||||
state.edges = applyEdgeChanges(
|
||||
edgeAdditions.concat(edgeSelectionChanges),
|
||||
state.edges
|
||||
);
|
||||
|
||||
newNodes.forEach((node) => {
|
||||
state.nodeExecutionStates[node.id] = {
|
||||
status: NodeStatus.PENDING,
|
||||
error: null,
|
||||
progress: null,
|
||||
progressImage: null,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
||||
@ -703,6 +792,8 @@ export const {
|
||||
fieldLabelChanged,
|
||||
viewportChanged,
|
||||
mouseOverFieldChanged,
|
||||
selectionCopied,
|
||||
selectionPasted,
|
||||
} = nodesSlice.actions;
|
||||
|
||||
export default nodesSlice.reducer;
|
||||
|
@ -31,4 +31,6 @@ export type NodesState = {
|
||||
viewport: Viewport;
|
||||
isReady: boolean;
|
||||
mouseOverField: FieldIdentifier | null;
|
||||
nodesToCopy: Node<NodeData>[];
|
||||
edgesToCopy: Edge<InvocationEdgeExtra>[];
|
||||
};
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { Node } from 'reactflow';
|
||||
|
||||
export const findUnoccupiedPosition = (nodes: Node[], x: number, y: number) => {
|
||||
let newX = x;
|
||||
let newY = y;
|
||||
while (nodes.find((n) => n.position.x === newX && n.position.y === newY)) {
|
||||
newX = newX + 50;
|
||||
newY = newY + 50;
|
||||
}
|
||||
return { x: newX, y: newY };
|
||||
};
|
Loading…
Reference in New Issue
Block a user