/** * @file Utilities for resolving the nearest connectable port during connection interactions. */ import type { Node, NodeId, Port, PortId, Position } from "../../../types/core"; import type { ConnectablePortsResult } from "./connectableTypes"; import { PORT_INTERACTION_THRESHOLD } from "../../../constants/interaction"; import { createActionPort } from "../identity/variant"; import { parsePortKey } from "../identity/key"; export type ConnectionCandidateSearchParams = { pointerCanvasPosition: Position; connectablePorts: ConnectablePortsResult; nodes: Record; getNodePorts: (nodeId: NodeId) => Port[]; getConnectionPoint: (nodeId: NodeId, portId: PortId) => Position | null; excludePort?: { nodeId: NodeId; portId: PortId }; snapDistance?: number; }; type CandidateWithDistance = { port: Port; distance: number; }; const toDescriptorTuples = (connectablePorts: ConnectablePortsResult): Array<{ nodeId: NodeId; portId: PortId }> => { if (connectablePorts.descriptors.size > 0) { return Array.from(connectablePorts.descriptors.values()).map((descriptor) => ({ nodeId: descriptor.nodeId, portId: descriptor.portId, })); } return Array.from(connectablePorts.ids) .map((key) => parsePortKey(key)) .filter((parsed): parsed is { nodeId: NodeId; portId: PortId } => parsed !== null); }; /** * Resolve the nearest connectable port to the provided pointer position. * Returns null when no candidate is within the snap distance or port data is unavailable. */ export function findNearestConnectablePort({ pointerCanvasPosition, connectablePorts, nodes, getNodePorts, getConnectionPoint, excludePort, snapDistance = PORT_INTERACTION_THRESHOLD.CONNECTION_SNAP_DISTANCE, }: ConnectionCandidateSearchParams): Port | null { if (connectablePorts.ids.size === 0) { return null; } const descriptorInputs = toDescriptorTuples(connectablePorts); const candidates = descriptorInputs .filter(({ nodeId, portId }) => { if (!nodeId || !portId) { return false; } if (!nodes[nodeId]) { return false; } if (excludePort && excludePort.nodeId === nodeId && excludePort.portId === portId) { return false; } return true; }) .map(({ nodeId, portId }) => { const connectionPoint = getConnectionPoint(nodeId, portId); if (!connectionPoint) { return null; } const dx = connectionPoint.x - pointerCanvasPosition.x; const dy = connectionPoint.y - pointerCanvasPosition.y; const distance = Math.hypot(dx, dy); if (distance > snapDistance) { return null; } const port = getNodePorts(nodeId).find((candidate) => candidate.id === portId); if (!port) { return null; } return { port: createActionPort(port), distance, }; }) .filter((entry): entry is CandidateWithDistance => entry !== null); if (candidates.length === 0) { return null; } const nearest = candidates.reduce((best, current) => { if (!best) { return current; } return current.distance < best.distance ? current : best; }, null); return nearest ? nearest.port : null; } // Debugging reference note: // Consulted connectionSwitchBehavior.ts and portConnectability.ts to ensure candidate selection // cooperates with existing connection validation and port filtering logic.