Adds first version of the app. Local storage only.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { acceptedFamilies, getPortType, hasBridgingCableType } from './project'
|
||||
import type { Connection, Device, PortType, Project } 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(
|
||||
project: Project,
|
||||
candidate: ConnectionCandidate,
|
||||
existingConnections: Connection[] = project.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 = project.devices.find((d) => d.id === sourceDeviceId)
|
||||
const targetDevice = project.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(project, sourcePort.portTypeId)
|
||||
const targetPortType = getPortType(project, 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(project, 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
|
||||
}
|
||||
Reference in New Issue
Block a user