import React, { useCallback, useRef, useState } from 'react'; import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, useReactFlow, type EdgeProps, } from '@xyflow/react'; import { bindWindowDrag } from '../../utils/pointerDrag'; import type { EdgeHandleBinding } from '../../utils/strategyFlowLayout'; import { buildPositionsFromNodes, getEdgeDragZone, pickSourceHandleAtPoint, pickTargetHandleAtPoint, type EdgeDragZone, } from '../../utils/strategyEdgeInteraction'; import { edgeBindingUpdateRef, edgeDisconnectRef } from './strategyEditorCallbacks'; type StrategyEdgeData = { binding?: EdgeHandleBinding; }; export function StrategyFlowEdge({ id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, selected, data, }: EdgeProps) { const { screenToFlowPosition, getNodes } = useReactFlow(); const binding = (data as StrategyEdgeData | undefined)?.binding; const hasCenter = binding?.manualCenter && binding.centerX != null && binding.centerY != null; const [edgePath, labelX, labelY] = getSmoothStepPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, ...(hasCenter ? { centerX: binding!.centerX!, centerY: binding!.centerY! } : {}), }); const dragRef = useRef<{ zone: EdgeDragZone; startCenterX: number; startCenterY: number; } | null>(null); const unbindRef = useRef<(() => void) | null>(null); const [dragZone, setDragZone] = useState(null); const commitBinding = useCallback((patch: EdgeHandleBinding) => { edgeBindingUpdateRef.current?.(id, patch); }, [id]); const positionsFromFlow = useCallback(() => { return buildPositionsFromNodes(getNodes()); }, [getNodes]); const onHitPointerDown = useCallback((e: React.PointerEvent) => { if (e.button !== 0) return; if ((e.target as Element).closest('.se-flow-edge-del')) return; e.preventDefault(); e.stopPropagation(); const flow = screenToFlowPosition({ x: e.clientX, y: e.clientY }); const zone = getEdgeDragZone(sourceX, sourceY, targetX, targetY, flow.x, flow.y); setDragZone(zone); try { (e.currentTarget as SVGPathElement).setPointerCapture(e.pointerId); } catch { /* ignore */ } const positions = positionsFromFlow(); const startCenterX = binding?.centerX ?? labelX; const startCenterY = binding?.centerY ?? labelY; dragRef.current = { zone, startCenterX, startCenterY }; if (zone === 'source') { commitBinding({ sourceHandle: pickSourceHandleAtPoint(source, flow, positions), manualSource: true, }); } else if (zone === 'target') { commitBinding({ targetHandle: pickTargetHandleAtPoint(target, flow, positions), manualTarget: true, }); } else { commitBinding({ centerX: flow.x, centerY: flow.y, manualCenter: true, }); } unbindRef.current?.(); unbindRef.current = bindWindowDrag( ({ x, y }) => { const d = dragRef.current; if (!d) return; const pos = screenToFlowPosition({ x, y }); const positions = positionsFromFlow(); if (d.zone === 'source') { commitBinding({ sourceHandle: pickSourceHandleAtPoint(source, pos, positions), manualSource: true, }); } else if (d.zone === 'target') { commitBinding({ targetHandle: pickTargetHandleAtPoint(target, pos, positions), manualTarget: true, }); } else { commitBinding({ centerX: pos.x, centerY: pos.y, manualCenter: true, }); } }, () => { dragRef.current = null; setDragZone(null); unbindRef.current = null; }, ); }, [ binding?.centerX, binding?.centerY, commitBinding, labelX, labelY, positionsFromFlow, screenToFlowPosition, source, sourceX, sourceY, target, targetX, targetY, ]); const onHitPointerUp = useCallback((e: React.PointerEvent) => { dragRef.current = null; setDragZone(null); unbindRef.current?.(); unbindRef.current = null; try { (e.currentTarget as SVGPathElement).releasePointerCapture(e.pointerId); } catch { /* ignore */ } }, []); const hitCursor = dragZone === 'source' ? 'grab' : dragZone === 'target' ? 'grab' : dragZone === 'middle' ? 'move' : 'pointer'; return ( <> {selected && edgeDisconnectRef.current && ( )} ); }