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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
OnConnect,
|
OnConnect,
|
||||||
@ -27,6 +28,8 @@ import {
|
|||||||
nodesDeleted,
|
nodesDeleted,
|
||||||
selectedEdgesChanged,
|
selectedEdgesChanged,
|
||||||
selectedNodesChanged,
|
selectedNodesChanged,
|
||||||
|
selectionCopied,
|
||||||
|
selectionPasted,
|
||||||
viewportChanged,
|
viewportChanged,
|
||||||
} from '../store/nodesSlice';
|
} from '../store/nodesSlice';
|
||||||
import { CustomConnectionLine } from './CustomConnectionLine';
|
import { CustomConnectionLine } from './CustomConnectionLine';
|
||||||
@ -121,8 +124,17 @@ export const Flow = () => {
|
|||||||
dispatch(contextMenusClosed());
|
dispatch(contextMenusClosed());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useHotkeys(['Ctrl+c', 'Meta+c'], () => {
|
||||||
|
dispatch(selectionCopied());
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys(['Ctrl+v', 'Meta+v'], () => {
|
||||||
|
dispatch(selectionPasted());
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
id="workflow-editor"
|
||||||
defaultViewport={viewport}
|
defaultViewport={viewport}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
OutputFieldValue,
|
OutputFieldValue,
|
||||||
} from '../types/types';
|
} from '../types/types';
|
||||||
import { buildInputFieldValue } from '../util/fieldValueBuilders';
|
import { buildInputFieldValue } from '../util/fieldValueBuilders';
|
||||||
import { DRAG_HANDLE_CLASSNAME } from '../types/constants';
|
import { DRAG_HANDLE_CLASSNAME, NODE_WIDTH } from '../types/constants';
|
||||||
|
|
||||||
const templatesSelector = createSelector(
|
const templatesSelector = createSelector(
|
||||||
[(state: RootState) => state.nodes],
|
[(state: RootState) => state.nodes],
|
||||||
@ -34,10 +34,24 @@ export const useBuildNodeData = () => {
|
|||||||
(type: AnyInvocationType | 'current_image' | 'notes') => {
|
(type: AnyInvocationType | 'current_image' | 'notes') => {
|
||||||
const nodeId = uuidv4();
|
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({
|
const { x, y } = flow.project({
|
||||||
x: window.innerWidth / 2.5,
|
x: _x,
|
||||||
y: window.innerHeight / 8,
|
y: _y,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === 'current_image') {
|
if (type === 'current_image') {
|
||||||
const node: Node<CurrentImageNodeData> = {
|
const node: Node<CurrentImageNodeData> = {
|
||||||
...SHARED_NODE_PROPERTIES,
|
...SHARED_NODE_PROPERTIES,
|
||||||
|
@ -11,4 +11,6 @@ export const nodesPersistDenylist: (keyof NodesState)[] = [
|
|||||||
'selectedNodes',
|
'selectedNodes',
|
||||||
'selectedEdges',
|
'selectedEdges',
|
||||||
'isReady',
|
'isReady',
|
||||||
|
'nodesToCopy',
|
||||||
|
'edgesToCopy',
|
||||||
];
|
];
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
appSocketInvocationError,
|
appSocketInvocationError,
|
||||||
appSocketInvocationStarted,
|
appSocketInvocationStarted,
|
||||||
} from 'services/events/actions';
|
} from 'services/events/actions';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { DRAG_HANDLE_CLASSNAME } from '../types/constants';
|
import { DRAG_HANDLE_CLASSNAME } from '../types/constants';
|
||||||
import {
|
import {
|
||||||
BooleanInputFieldValue,
|
BooleanInputFieldValue,
|
||||||
@ -52,6 +53,7 @@ import {
|
|||||||
Workflow,
|
Workflow,
|
||||||
} from '../types/types';
|
} from '../types/types';
|
||||||
import { NodesState } from './types';
|
import { NodesState } from './types';
|
||||||
|
import { findUnoccupiedPosition } from './util/findUnoccupiedPosition';
|
||||||
|
|
||||||
export const initialNodesState: NodesState = {
|
export const initialNodesState: NodesState = {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@ -83,6 +85,8 @@ export const initialNodesState: NodesState = {
|
|||||||
nodeExecutionStates: {},
|
nodeExecutionStates: {},
|
||||||
viewport: { x: 0, y: 0, zoom: 1 },
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
mouseOverField: null,
|
mouseOverField: null,
|
||||||
|
nodesToCopy: [],
|
||||||
|
edgesToCopy: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
|
type FieldValueAction<T extends InputFieldValue> = PayloadAction<{
|
||||||
@ -124,6 +128,12 @@ const nodesSlice = createSlice({
|
|||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const node = action.payload;
|
const node = action.payload;
|
||||||
|
const position = findUnoccupiedPosition(
|
||||||
|
state.nodes,
|
||||||
|
node.position.x,
|
||||||
|
node.position.y
|
||||||
|
);
|
||||||
|
node.position = position;
|
||||||
state.nodes.push(node);
|
state.nodes.push(node);
|
||||||
|
|
||||||
if (!isInvocationNode(node)) {
|
if (!isInvocationNode(node)) {
|
||||||
@ -595,6 +605,85 @@ const nodesSlice = createSlice({
|
|||||||
) => {
|
) => {
|
||||||
state.mouseOverField = action.payload;
|
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) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
builder.addCase(receivedOpenAPISchema.pending, (state) => {
|
||||||
@ -703,6 +792,8 @@ export const {
|
|||||||
fieldLabelChanged,
|
fieldLabelChanged,
|
||||||
viewportChanged,
|
viewportChanged,
|
||||||
mouseOverFieldChanged,
|
mouseOverFieldChanged,
|
||||||
|
selectionCopied,
|
||||||
|
selectionPasted,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -31,4 +31,6 @@ export type NodesState = {
|
|||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
mouseOverField: FieldIdentifier | null;
|
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