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 { $flow } from 'features/nodes/store/reactFlowInstance';
|
||||
import { contextMenusClosed } from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
import { MouseEvent, useCallback, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
Background,
|
||||
@ -21,6 +21,7 @@ import {
|
||||
OnSelectionChangeFunc,
|
||||
ProOptions,
|
||||
ReactFlow,
|
||||
XYPosition,
|
||||
} from 'reactflow';
|
||||
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
|
||||
import {
|
||||
@ -79,7 +80,8 @@ export const Flow = () => {
|
||||
const edges = useAppSelector((state) => state.nodes.edges);
|
||||
const viewport = useAppSelector((state) => state.nodes.viewport);
|
||||
const { shouldSnapToGrid, selectionMode } = useAppSelector(selector);
|
||||
|
||||
const flowWrapper = useRef<HTMLDivElement>(null);
|
||||
const cursorPosition = useRef<XYPosition>();
|
||||
const isValidConnection = useIsValidConnection();
|
||||
|
||||
const [borderRadius] = useToken('radii', ['base']);
|
||||
@ -154,6 +156,17 @@ export const Flow = () => {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
dispatch(selectionCopied());
|
||||
@ -166,18 +179,20 @@ export const Flow = () => {
|
||||
|
||||
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(selectionPasted());
|
||||
dispatch(selectionPasted({ cursorPosition: cursorPosition.current }));
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
id="workflow-editor"
|
||||
ref={flowWrapper}
|
||||
defaultViewport={viewport}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onMouseMove={onMouseMove}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
OnConnectStartParams,
|
||||
SelectionMode,
|
||||
Viewport,
|
||||
XYPosition,
|
||||
} from 'reactflow';
|
||||
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
|
||||
import { sessionCanceled, sessionInvoked } from 'services/api/thunks/session';
|
||||
@ -715,8 +716,30 @@ const nodesSlice = createSlice({
|
||||
selectionCopied: (state) => {
|
||||
state.nodesToCopy = state.nodes.filter((n) => n.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 oldNodeIds = newNodes.map((n) => n.data.id);
|
||||
const newEdges = state.edgesToCopy
|
||||
@ -745,8 +768,8 @@ const nodesSlice = createSlice({
|
||||
|
||||
const position = findUnoccupiedPosition(
|
||||
state.nodes,
|
||||
node.position.x,
|
||||
node.position.y
|
||||
node.position.x + (cursorPosition?.x ?? 0),
|
||||
node.position.y + (cursorPosition?.y ?? 0)
|
||||
);
|
||||
|
||||
node.position = position;
|
||||
|
Loading…
Reference in New Issue
Block a user