mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(nodes): Center pasted nodes at mouse location (#4595)
* Initial commit. Feature works, but code might need some cleanup * Cleaned up diff * Made mousePosition a XYPosition again so its nicely typed * Fixed yarn issues * Paste now properly takes node width/height into account when pasting * feat(ui): use react's types in the `onMouseMove` `reactflow` handler * feat(ui): use refs to access `reactflow`'s DOM elements * feat(ui): use a ref to store cursor position in nodes --------- Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
parent
183e2c3ee0
commit
f87b042162
@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
||||||
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
||||||
import { useCallback } from 'react';
|
import { MouseEvent, useCallback, useRef } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
OnSelectionChangeFunc,
|
OnSelectionChangeFunc,
|
||||||
ProOptions,
|
ProOptions,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
|
XYPosition,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
|
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
|
||||||
import {
|
import {
|
||||||
@ -79,7 +80,8 @@ export const Flow = () => {
|
|||||||
const edges = useAppSelector((state) => state.nodes.edges);
|
const edges = useAppSelector((state) => state.nodes.edges);
|
||||||
const viewport = useAppSelector((state) => state.nodes.viewport);
|
const viewport = useAppSelector((state) => state.nodes.viewport);
|
||||||
const { shouldSnapToGrid, selectionMode } = useAppSelector(selector);
|
const { shouldSnapToGrid, selectionMode } = useAppSelector(selector);
|
||||||
|
const flowWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
const cursorPosition = useRef<XYPosition>();
|
||||||
const isValidConnection = useIsValidConnection();
|
const isValidConnection = useIsValidConnection();
|
||||||
|
|
||||||
const [borderRadius] = useToken('radii', ['base']);
|
const [borderRadius] = useToken('radii', ['base']);
|
||||||
@ -154,6 +156,17 @@ export const Flow = () => {
|
|||||||
flow.fitView();
|
flow.fitView();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
|
||||||
|
const bounds = flowWrapper.current?.getBoundingClientRect();
|
||||||
|
if (bounds) {
|
||||||
|
const pos = $flow.get()?.project({
|
||||||
|
x: event.clientX - bounds.left,
|
||||||
|
y: event.clientY - bounds.top,
|
||||||
|
});
|
||||||
|
cursorPosition.current = pos;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
|
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch(selectionCopied());
|
dispatch(selectionCopied());
|
||||||
@ -166,18 +179,20 @@ export const Flow = () => {
|
|||||||
|
|
||||||
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
|
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dispatch(selectionPasted());
|
dispatch(selectionPasted({ cursorPosition: cursorPosition.current }));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
id="workflow-editor"
|
id="workflow-editor"
|
||||||
|
ref={flowWrapper}
|
||||||
defaultViewport={viewport}
|
defaultViewport={viewport}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onInit={onInit}
|
onInit={onInit}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onEdgesDelete={onEdgesDelete}
|
onEdgesDelete={onEdgesDelete}
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
OnConnectStartParams,
|
OnConnectStartParams,
|
||||||
SelectionMode,
|
SelectionMode,
|
||||||
Viewport,
|
Viewport,
|
||||||
|
XYPosition,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||||
import { sessionCanceled, sessionInvoked } from 'services/api/thunks/session';
|
import { sessionCanceled, sessionInvoked } from 'services/api/thunks/session';
|
||||||
@ -715,8 +716,30 @@ const nodesSlice = createSlice({
|
|||||||
selectionCopied: (state) => {
|
selectionCopied: (state) => {
|
||||||
state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep);
|
state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep);
|
||||||
state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep);
|
state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep);
|
||||||
|
|
||||||
|
if (state.nodesToCopy.length > 0) {
|
||||||
|
const averagePosition = { x: 0, y: 0 };
|
||||||
|
state.nodesToCopy.forEach((e) => {
|
||||||
|
const xOffset = 0.15 * (e.width ?? 0);
|
||||||
|
const yOffset = 0.5 * (e.height ?? 0);
|
||||||
|
averagePosition.x += e.position.x + xOffset;
|
||||||
|
averagePosition.y += e.position.y + yOffset;
|
||||||
|
});
|
||||||
|
|
||||||
|
averagePosition.x /= state.nodesToCopy.length;
|
||||||
|
averagePosition.y /= state.nodesToCopy.length;
|
||||||
|
|
||||||
|
state.nodesToCopy.forEach((e) => {
|
||||||
|
e.position.x -= averagePosition.x;
|
||||||
|
e.position.y -= averagePosition.y;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
selectionPasted: (state) => {
|
selectionPasted: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ cursorPosition?: XYPosition }>
|
||||||
|
) => {
|
||||||
|
const { cursorPosition } = action.payload;
|
||||||
const newNodes = state.nodesToCopy.map(cloneDeep);
|
const newNodes = state.nodesToCopy.map(cloneDeep);
|
||||||
const oldNodeIds = newNodes.map((n) => n.data.id);
|
const oldNodeIds = newNodes.map((n) => n.data.id);
|
||||||
const newEdges = state.edgesToCopy
|
const newEdges = state.edgesToCopy
|
||||||
@ -745,8 +768,8 @@ const nodesSlice = createSlice({
|
|||||||
|
|
||||||
const position = findUnoccupiedPosition(
|
const position = findUnoccupiedPosition(
|
||||||
state.nodes,
|
state.nodes,
|
||||||
node.position.x,
|
node.position.x + (cursorPosition?.x ?? 0),
|
||||||
node.position.y
|
node.position.y + (cursorPosition?.y ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
node.position = position;
|
node.position = position;
|
||||||
|
Loading…
Reference in New Issue
Block a user