import { acceptedFamilies, getPortType, hasBridgingCableType } from './diagram' import type { Catalog, Connection, Device, Diagram, PortType } from './types' /** * Two ports connect if the sets of families they each accept (their own * family plus anything declared in `compatibleFamilyIds`) overlap at all. * This has to be a full set intersection, not just "does A's family appear * in B's list" — a pairing that only exists via a shared third "bridge" * family (e.g. USB-A Host and USB-B Device both listing 'usb-a-to-b', while * neither's own family is 'usb-a-to-b') would otherwise never match. */ export function portTypesCompatible(a: PortType, b: PortType): boolean { if (a.id === b.id) return true const aFamilies = acceptedFamilies(a) for (const family of acceptedFamilies(b)) { if (aFamilies.has(family)) return true } return false } export interface ConnectionCandidate { sourceDeviceId: string sourcePortId: string targetDeviceId: string targetPortId: string } export interface ConnectionValidation { valid: boolean reason?: string } /** * Central rule set for "can these two ports be wired together". Used both by * the canvas (to accept/reject a drag-to-connect gesture) and could be reused * server-side later since it only depends on plain domain data. */ export function validateConnection( diagram: Diagram, catalog: Catalog, candidate: ConnectionCandidate, existingConnections: Connection[] = diagram.connections, ): ConnectionValidation { const { sourceDeviceId, sourcePortId, targetDeviceId, targetPortId } = candidate if (sourceDeviceId === targetDeviceId && sourcePortId === targetPortId) { return { valid: false, reason: 'A port cannot connect to itself.' } } const sourceDevice = diagram.devices.find((d) => d.id === sourceDeviceId) const targetDevice = diagram.devices.find((d) => d.id === targetDeviceId) const sourcePort = sourceDevice?.ports.find((p) => p.id === sourcePortId) const targetPort = targetDevice?.ports.find((p) => p.id === targetPortId) if (!sourceDevice || !targetDevice || !sourcePort || !targetPort) { return { valid: false, reason: 'Unknown device or port.' } } if (sourcePort.direction === 'input' && targetPort.direction === 'input') { return { valid: false, reason: 'Cannot connect two input ports.' } } if (sourcePort.direction === 'output' && targetPort.direction === 'output') { return { valid: false, reason: 'Cannot connect two output ports.' } } const sourcePortType = getPortType(catalog, sourcePort.portTypeId) const targetPortType = getPortType(catalog, targetPort.portTypeId) if (!sourcePortType || !targetPortType) { return { valid: false, reason: 'Unknown port type.' } } // Directly compatible port types, or bridgeable by a specific adapter // cable (different connectors on each end, e.g. XLR-to-TRS) even when the // port types themselves declare no general compatibility. if (!portTypesCompatible(sourcePortType, targetPortType) && !hasBridgingCableType(catalog, sourcePortType, targetPortType)) { return { valid: false, reason: `${sourcePortType.name} is not compatible with ${targetPortType.name}.`, } } const maxSource = sourcePortType.maxConnections ?? 1 const usedSource = existingConnections.filter( (c) => c.sourcePortId === sourcePortId || c.targetPortId === sourcePortId, ).length if (usedSource >= maxSource) { return { valid: false, reason: `${sourcePort.name} already has the maximum number of cables.` } } const maxTarget = targetPortType.maxConnections ?? 1 const usedTarget = existingConnections.filter( (c) => c.sourcePortId === targetPortId || c.targetPortId === targetPortId, ).length if (usedTarget >= maxTarget) { return { valid: false, reason: `${targetPort.name} already has the maximum number of cables.` } } return { valid: true } } export function findDevicePort(devices: Device[], deviceId: string, portId: string) { const device = devices.find((d) => d.id === deviceId) const port = device?.ports.find((p) => p.id === portId) return device && port ? { device, port } : undefined }