import { useEffect, useRef, type PointerEvent as ReactPointerEvent, } from "react"; import { BaseEdge, useReactFlow, type EdgeProps, type EdgeTypes, } from "@xyflow/react"; import { createOrthogonalSegmentDetour, edgeSegmentAxis, moveOrthogonalCorner, moveOrthogonalSegment, orthogonalEdgePoints, orthogonalPolylinePath, type EdgeRouteData, type EdgeRoutePoint, } from "./edgeRouting"; export const CONTACT_AWARE_EDGE_TYPE = "contact-aware"; type RouteDrag = { basePoints: EdgeRoutePoint[]; cleanup: () => void; kind: "corner" | "detour" | "segment"; pointerId: number; routeIndex: number; startPointer: EdgeRoutePoint; }; function longestSegmentIndex(points: EdgeRoutePoint[]) { let bestIndex = 0; let bestLength = -1; for (let index = 0; index < points.length - 1; index += 1) { const length = Math.hypot( points[index + 1].x - points[index].x, points[index + 1].y - points[index].y, ); if (length > bestLength) { bestIndex = index; bestLength = length; } } return bestIndex; } function RoutedEdge({ data, id, interactionWidth, markerEnd, markerStart, selected, sourcePosition, sourceX, sourceY, style, targetPosition, targetX, targetY, }: EdgeProps) { const reactFlow = useReactFlow(); const dragRef = useRef(null); const edgeData = (data ?? {}) as EdgeRouteData; useEffect( () => () => { dragRef.current?.cleanup(); dragRef.current = null; }, [], ); const points = orthogonalEdgePoints({ routePoints: edgeData.routePoints, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY, }); const edgePath = orthogonalPolylinePath(points, edgeData.crossingJumps); const beginRouteDrag = ( event: ReactPointerEvent, basePoints: EdgeRoutePoint[], kind: RouteDrag["kind"], routeIndex: number, ) => { event.preventDefault(); event.stopPropagation(); dragRef.current?.cleanup(); const drag: RouteDrag = { basePoints: basePoints.map((point) => ({ ...point })), cleanup: () => undefined, kind, pointerId: event.pointerId, routeIndex, startPointer: reactFlow.screenToFlowPosition( { x: event.clientX, y: event.clientY }, { snapToGrid: false }, ), }; const moveRoute = (pointerEvent: PointerEvent) => { if ( dragRef.current !== drag || pointerEvent.pointerId !== drag.pointerId ) { return; } pointerEvent.preventDefault(); pointerEvent.stopPropagation(); const pointer = reactFlow.screenToFlowPosition( { x: pointerEvent.clientX, y: pointerEvent.clientY }, { snapToGrid: false }, ); const delta = { x: pointer.x - drag.startPointer.x, y: pointer.y - drag.startPointer.y, }; const nextPoints = drag.kind === "corner" ? moveOrthogonalCorner(drag.basePoints, drag.routeIndex, delta) : drag.kind === "detour" ? createOrthogonalSegmentDetour( drag.basePoints, drag.routeIndex, delta, ) : moveOrthogonalSegment( drag.basePoints, drag.routeIndex, delta, ); edgeData.onRoutePointsChange?.(id, nextPoints.slice(1, -1)); }; const finishRoute = (pointerEvent: PointerEvent) => { if ( dragRef.current !== drag || pointerEvent.pointerId !== drag.pointerId ) { return; } pointerEvent.preventDefault(); pointerEvent.stopPropagation(); drag.cleanup(); dragRef.current = null; }; drag.cleanup = () => { window.removeEventListener("pointermove", moveRoute); window.removeEventListener("pointerup", finishRoute); window.removeEventListener("pointercancel", finishRoute); }; dragRef.current = drag; window.addEventListener("pointermove", moveRoute, { passive: false }); window.addEventListener("pointerup", finishRoute); window.addEventListener("pointercancel", finishRoute); edgeData.onRouteEditStart?.(id); }; const editable = selected && edgeData.editable === true; const internalSegments = points .slice(0, -1) .map((start, index) => ({ end: points[index + 1], index, start })) .filter(({ index }) => index > 0 && index < points.length - 2); const internalCorners = points .map((point, index) => ({ index, point })) .filter(({ index }) => index > 0 && index < points.length - 1); const fallbackIndex = longestSegmentIndex(points); const fallbackStart = points[fallbackIndex]; const fallbackEnd = points[fallbackIndex + 1]; return ( <> {editable ? internalSegments.map(({ end, index, start }) => ( beginRouteDrag(event, points, "segment", index) } x1={start.x} x2={end.x} y1={start.y} y2={end.y} /> )) : null} {editable ? internalCorners.map(({ index, point }) => ( beginRouteDrag(event, points, "corner", index) } r={4.5} /> )) : null} {editable && internalCorners.length === 0 && fallbackEnd ? ( beginRouteDrag(event, points, "detour", fallbackIndex) } r={5} /> ) : null} ); } export function ContactAwareEdge(props: EdgeProps) { if (props.data?.isContactEdge === true) { return null; } return ; } export const contactAwareEdgeTypes: EdgeTypes = { [CONTACT_AWARE_EDGE_TYPE]: ContactAwareEdge, };