Per organized-ideas.md §8: the storage layer (DiagramRepository etc.) was already renamed in an earlier phase; this finishes it everywhere else. - domain/types.ts: Project -> Diagram. domain/project.ts -> domain/diagram.ts (createEmptyProject -> createEmptyDiagram, default name "Untitled Diagram"). - domain/compatibility.ts, domain/bom.ts: Project param/type -> Diagram. - data/exportImport.ts: ProjectImportError -> DiagramImportError, projectToJson/downloadProjectFile/readProjectFile/normalizeProject -> their Diagram equivalents. - state/projectStore.ts -> state/diagramStore.ts: useProjectStore -> useDiagramStore, the `project` field -> `diagram`, newProject/renameProject/ importProject/applyRestoredProject -> *Diagram, restoredProjectUpdatedAt -> restoredDiagramUpdatedAt. - Every component updated to match, compiler-guided (tsc -b enumerated each remaining call site after the core rename, the same approach used for the earlier catalog-parameter refactor). - README updated for the terminology, and to match the repository class names (which had already been renamed but the README hadn't caught up). No backend/schema changes: the JSON shape stored in diagrams.data never changed, only TypeScript-side identifiers, so existing diagrams are unaffected. One SQL comment fixed for accuracy (no migration needed). Verified: tsc -b and oxlint clean; grepped src/ for any remaining Project/project reference (none) after the sweep.
105 lines
4.1 KiB
TypeScript
105 lines
4.1 KiB
TypeScript
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
|
|
}
|