mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
fix(ui): rework edge update logic
This commit is contained in:
parent
9f7841a04b
commit
6b4e464d17
@ -8,12 +8,13 @@ import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'
|
|||||||
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
|
||||||
import {
|
import {
|
||||||
$cursorPos,
|
$cursorPos,
|
||||||
|
$didUpdateEdge,
|
||||||
$isAddNodePopoverOpen,
|
$isAddNodePopoverOpen,
|
||||||
$isUpdatingEdge,
|
$isUpdatingEdge,
|
||||||
|
$lastEdgeUpdateMouseEvent,
|
||||||
$pendingConnection,
|
$pendingConnection,
|
||||||
$viewport,
|
$viewport,
|
||||||
connectionMade,
|
connectionMade,
|
||||||
edgeAdded,
|
|
||||||
edgeDeleted,
|
edgeDeleted,
|
||||||
edgesChanged,
|
edgesChanged,
|
||||||
edgesDeleted,
|
edgesDeleted,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
undo,
|
undo,
|
||||||
} from 'features/nodes/store/nodesSlice';
|
} from 'features/nodes/store/nodesSlice';
|
||||||
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
import { $flow } from 'features/nodes/store/reactFlowInstance';
|
||||||
|
import { isString } from 'lodash-es';
|
||||||
import type { CSSProperties, MouseEvent } from 'react';
|
import type { CSSProperties, MouseEvent } from 'react';
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -39,7 +41,7 @@ import type {
|
|||||||
ReactFlowProps,
|
ReactFlowProps,
|
||||||
ReactFlowState,
|
ReactFlowState,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { Background, ReactFlow, useStore as useReactFlowStore } from 'reactflow';
|
import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from 'reactflow';
|
||||||
|
|
||||||
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
|
import CustomConnectionLine from './connectionLines/CustomConnectionLine';
|
||||||
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
|
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
|
||||||
@ -81,6 +83,7 @@ export const Flow = memo(() => {
|
|||||||
const flowWrapper = useRef<HTMLDivElement>(null);
|
const flowWrapper = useRef<HTMLDivElement>(null);
|
||||||
const isValidConnection = useIsValidConnection();
|
const isValidConnection = useIsValidConnection();
|
||||||
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
useWorkflowWatcher();
|
useWorkflowWatcher();
|
||||||
useSyncExecutionState();
|
useSyncExecutionState();
|
||||||
const [borderRadius] = useToken('radii', ['base']);
|
const [borderRadius] = useToken('radii', ['base']);
|
||||||
@ -157,45 +160,46 @@ export const Flow = memo(() => {
|
|||||||
* where the edge is deleted if you click it accidentally).
|
* where the edge is deleted if you click it accidentally).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// We have a ref for cursor position, but it is the *projected* cursor position.
|
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback((e, _edge, _handleType) => {
|
||||||
// Easiest to just keep track of the last mouse event for this particular feature
|
$isUpdatingEdge.set(true);
|
||||||
const edgeUpdateMouseEvent = useRef<MouseEvent>();
|
$didUpdateEdge.set(false);
|
||||||
|
$lastEdgeUpdateMouseEvent.set(e);
|
||||||
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback(
|
}, []);
|
||||||
(e, edge, _handleType) => {
|
|
||||||
$isUpdatingEdge.set(true);
|
|
||||||
// update mouse event
|
|
||||||
edgeUpdateMouseEvent.current = e;
|
|
||||||
// always delete the edge when starting an updated
|
|
||||||
dispatch(edgeDeleted(edge.id));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
|
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
|
||||||
(_oldEdge, newConnection) => {
|
(edge, newConnection) => {
|
||||||
// Because we deleted the edge when the update started, we must create a new edge from the connection
|
// This event is fired when an edge update is successful
|
||||||
|
$didUpdateEdge.set(true);
|
||||||
|
// When an edge update is successful, we need to delete the old edge and create a new one
|
||||||
|
dispatch(edgeDeleted(edge.id));
|
||||||
dispatch(connectionMade(newConnection));
|
dispatch(connectionMade(newConnection));
|
||||||
|
// Because we shift the position of handles depending on whether a field is connected or not, we must use
|
||||||
|
// updateNodeInternals to tell reactflow to recalculate the positions of the handles
|
||||||
|
const nodesToUpdate = [edge.source, edge.target, newConnection.source, newConnection.target].filter(isString);
|
||||||
|
updateNodeInternals(nodesToUpdate);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch, updateNodeInternals]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
|
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
|
||||||
(e, edge, _handleType) => {
|
(e, edge, _handleType) => {
|
||||||
$isUpdatingEdge.set(false);
|
const didUpdateEdge = $didUpdateEdge.get();
|
||||||
$pendingConnection.set(null);
|
// Fall back to a reasonable default event
|
||||||
// Handle the case where user begins a drag but didn't move the cursor - we deleted the edge when starting
|
const lastEvent = $lastEdgeUpdateMouseEvent.get() ?? { clientX: 0, clientY: 0 };
|
||||||
// the edge update - we need to add it back
|
// We have to narrow this event down to MouseEvents - could be TouchEvent
|
||||||
if (
|
const didMouseMove =
|
||||||
// ignore touch events
|
!('touches' in e) && Math.hypot(e.clientX - lastEvent.clientX, e.clientY - lastEvent.clientY) > 5;
|
||||||
!('touches' in e) &&
|
|
||||||
edgeUpdateMouseEvent.current?.clientX === e.clientX &&
|
// If we got this far and did not successfully update an edge, and the mouse moved away from the handle,
|
||||||
edgeUpdateMouseEvent.current?.clientY === e.clientY
|
// the user probably intended to delete the edge
|
||||||
) {
|
if (!didUpdateEdge && didMouseMove) {
|
||||||
dispatch(edgeAdded(edge));
|
dispatch(edgeDeleted(edge.id));
|
||||||
}
|
}
|
||||||
// reset mouse event
|
|
||||||
edgeUpdateMouseEvent.current = undefined;
|
$isUpdatingEdge.set(false);
|
||||||
|
$didUpdateEdge.set(false);
|
||||||
|
$pendingConnection.set(null);
|
||||||
|
$lastEdgeUpdateMouseEvent.set(null);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -255,9 +259,11 @@ export const Flow = memo(() => {
|
|||||||
useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey);
|
useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey);
|
||||||
|
|
||||||
const onEscapeHotkey = useCallback(() => {
|
const onEscapeHotkey = useCallback(() => {
|
||||||
$pendingConnection.set(null);
|
if (!$isUpdatingEdge.get()) {
|
||||||
$isAddNodePopoverOpen.set(false);
|
$pendingConnection.set(null);
|
||||||
cancelConnection();
|
$isAddNodePopoverOpen.set(false);
|
||||||
|
cancelConnection();
|
||||||
|
}
|
||||||
}, [cancelConnection]);
|
}, [cancelConnection]);
|
||||||
useHotkeys('esc', onEscapeHotkey);
|
useHotkeys('esc', onEscapeHotkey);
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@ import {
|
|||||||
import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
|
import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
|
||||||
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow';
|
import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow';
|
||||||
import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
|
import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
|
||||||
import type { UndoableOptions } from 'redux-undo';
|
import type { UndoableOptions } from 'redux-undo';
|
||||||
@ -125,9 +126,6 @@ export 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);
|
|
||||||
},
|
|
||||||
connectionMade: (state, action: PayloadAction<Connection>) => {
|
connectionMade: (state, action: PayloadAction<Connection>) => {
|
||||||
state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges);
|
state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges);
|
||||||
},
|
},
|
||||||
@ -495,7 +493,6 @@ export const {
|
|||||||
notesNodeValueChanged,
|
notesNodeValueChanged,
|
||||||
selectedAll,
|
selectedAll,
|
||||||
selectionPasted,
|
selectionPasted,
|
||||||
edgeAdded,
|
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
@ -507,6 +504,9 @@ export const $copiedEdges = atom<InvocationNodeEdge[]>([]);
|
|||||||
export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]);
|
export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]);
|
||||||
export const $pendingConnection = atom<PendingConnection | null>(null);
|
export const $pendingConnection = atom<PendingConnection | null>(null);
|
||||||
export const $isUpdatingEdge = atom(false);
|
export const $isUpdatingEdge = atom(false);
|
||||||
|
export const $didUpdateEdge = atom(false);
|
||||||
|
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
|
||||||
|
|
||||||
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
|
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||||
export const $isAddNodePopoverOpen = atom(false);
|
export const $isAddNodePopoverOpen = atom(false);
|
||||||
export const closeAddNodePopover = () => {
|
export const closeAddNodePopover = () => {
|
||||||
@ -609,6 +609,5 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
|
|||||||
nodesDeleted,
|
nodesDeleted,
|
||||||
nodeUseCacheChanged,
|
nodeUseCacheChanged,
|
||||||
notesNodeValueChanged,
|
notesNodeValueChanged,
|
||||||
selectionPasted,
|
selectionPasted
|
||||||
edgeAdded
|
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user