|
|
|
@ -362,6 +362,8 @@ function isLocalKeyboardTarget(target: EventTarget | null): boolean { |
|
|
|
const FIT_VIEW_OPTIONS = { maxZoom: 1 }; |
|
|
|
const CANVAS_MIN_ZOOM = 0.15; |
|
|
|
const CANVAS_MAX_ZOOM = 4; |
|
|
|
const CANVAS_MIN_ZOOM_PERCENT = CANVAS_MIN_ZOOM * 100; |
|
|
|
const CANVAS_MAX_ZOOM_PERCENT = CANVAS_MAX_ZOOM * 100; |
|
|
|
const WHEEL_ZOOM_FACTOR = 1.15; |
|
|
|
|
|
|
|
// Connection validation rules
|
|
|
|
@ -610,6 +612,8 @@ export function WorkflowCanvas() { |
|
|
|
const tutorialViewportSet = useRef(false); |
|
|
|
const gestureZoomStartRef = useRef<{ zoom: number } | null>(null); |
|
|
|
const lastCanvasPointerFlowPositionRef = useRef<ScreenPoint | null>(null); |
|
|
|
const pendingDragPositionChangesRef = useRef<Map<string, NodeChange<WorkflowNode>>>(new Map()); |
|
|
|
const pendingDragPositionFrameRef = useRef<number | null>(null); |
|
|
|
const [boxSelectionRect, setBoxSelectionRect] = useState<FlowRect | null>(null); |
|
|
|
|
|
|
|
const onNodesChange = useCallback((changes: NodeChange<WorkflowNode>[]) => { |
|
|
|
@ -624,10 +628,62 @@ export function WorkflowCanvas() { |
|
|
|
} |
|
|
|
}, [storeOnNodesChange]); |
|
|
|
|
|
|
|
const handleReactFlowNodesChange = useCallback((changes: NodeChange[]) => { |
|
|
|
onNodesChange(changes.filter((change) => !("id" in change) || change.id !== BATCH_SELECTION_NODE_ID) as NodeChange<WorkflowNode>[]); |
|
|
|
const flushPendingDragPositionChanges = useCallback(() => { |
|
|
|
if (pendingDragPositionFrameRef.current !== null) { |
|
|
|
cancelAnimationFrame(pendingDragPositionFrameRef.current); |
|
|
|
pendingDragPositionFrameRef.current = null; |
|
|
|
} |
|
|
|
|
|
|
|
const pendingChanges = pendingDragPositionChangesRef.current; |
|
|
|
if (pendingChanges.size === 0) return; |
|
|
|
|
|
|
|
const changes = Array.from(pendingChanges.values()); |
|
|
|
pendingChanges.clear(); |
|
|
|
onNodesChange(changes); |
|
|
|
}, [onNodesChange]); |
|
|
|
|
|
|
|
const scheduleDragPositionChanges = useCallback((changes: NodeChange<WorkflowNode>[]) => { |
|
|
|
const pendingChanges = pendingDragPositionChangesRef.current; |
|
|
|
for (const change of changes) { |
|
|
|
if (!("id" in change)) continue; |
|
|
|
pendingChanges.set(change.id, change); |
|
|
|
} |
|
|
|
|
|
|
|
if (pendingDragPositionFrameRef.current !== null) return; |
|
|
|
|
|
|
|
pendingDragPositionFrameRef.current = requestAnimationFrame(() => { |
|
|
|
pendingDragPositionFrameRef.current = null; |
|
|
|
const queuedChanges = Array.from(pendingChanges.values()); |
|
|
|
pendingChanges.clear(); |
|
|
|
if (queuedChanges.length > 0) { |
|
|
|
onNodesChange(queuedChanges); |
|
|
|
} |
|
|
|
}); |
|
|
|
}, [onNodesChange]); |
|
|
|
|
|
|
|
const handleReactFlowNodesChange = useCallback((changes: NodeChange[]) => { |
|
|
|
const workflowChanges = changes.filter((change) => !("id" in change) || change.id !== BATCH_SELECTION_NODE_ID) as NodeChange<WorkflowNode>[]; |
|
|
|
const dragPositionChanges: NodeChange<WorkflowNode>[] = []; |
|
|
|
const immediateChanges: NodeChange<WorkflowNode>[] = []; |
|
|
|
|
|
|
|
for (const change of workflowChanges) { |
|
|
|
if (change.type === "position" && (change as { dragging?: boolean }).dragging === true) { |
|
|
|
dragPositionChanges.push(change); |
|
|
|
} else { |
|
|
|
immediateChanges.push(change); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (immediateChanges.length > 0) { |
|
|
|
flushPendingDragPositionChanges(); |
|
|
|
onNodesChange(immediateChanges); |
|
|
|
} |
|
|
|
|
|
|
|
if (dragPositionChanges.length > 0) { |
|
|
|
scheduleDragPositionChanges(dragPositionChanges); |
|
|
|
} |
|
|
|
}, [flushPendingDragPositionChanges, onNodesChange, scheduleDragPositionChanges]); |
|
|
|
|
|
|
|
const addNodeWithOptionalGroup = useCallback(( |
|
|
|
type: NodeType, |
|
|
|
position: { x: number; y: number }, |
|
|
|
@ -726,6 +782,11 @@ export function WorkflowCanvas() { |
|
|
|
if (boxSelectionEndTimerRef.current) { |
|
|
|
clearTimeout(boxSelectionEndTimerRef.current); |
|
|
|
} |
|
|
|
if (pendingDragPositionFrameRef.current !== null) { |
|
|
|
cancelAnimationFrame(pendingDragPositionFrameRef.current); |
|
|
|
pendingDragPositionFrameRef.current = null; |
|
|
|
} |
|
|
|
pendingDragPositionChangesRef.current.clear(); |
|
|
|
document.documentElement.classList.remove("box-selection-active"); |
|
|
|
document.documentElement.classList.remove("canvas-interacting"); |
|
|
|
document.documentElement.classList.remove("canvas-node-drag-active"); |
|
|
|
@ -3059,8 +3120,8 @@ export function WorkflowCanvas() { |
|
|
|
/> |
|
|
|
<div className="ml-1 flex h-[25px] w-24 items-center [&_.ant-slider]:!m-0"> |
|
|
|
<Slider |
|
|
|
min={10} |
|
|
|
max={400} |
|
|
|
min={CANVAS_MIN_ZOOM_PERCENT} |
|
|
|
max={CANVAS_MAX_ZOOM_PERCENT} |
|
|
|
step={3} |
|
|
|
value={zoomPercent} |
|
|
|
onChange={handleZoomPercentChange} |
|
|
|
|