import { v4 as uuid } from 'uuid' import type { CableType, Catalog, Device, DeviceCategoryDef, DeviceTemplate, PortType, Project } from './types' export function allPortTypes(catalog: Catalog): PortType[] { return catalog.portTypes } export function allCableTypes(catalog: Catalog): CableType[] { return catalog.cableTypes } /** `hiddenPublicIds` is a per-user UI preference (see state/catalogStore.ts), * not part of the catalog data itself — you can hide any public template * from your own palette without affecting anyone else or the shared entry. */ export function allDeviceTemplates(catalog: Catalog, hiddenPublicIds: ReadonlySet = new Set()): DeviceTemplate[] { return catalog.deviceTemplates.filter((t) => t.custom || !hiddenPublicIds.has(t.id)) } /** Public templates the user has hidden, for a "restore" list. */ export function hiddenDeviceTemplates(catalog: Catalog, hiddenPublicIds: ReadonlySet): DeviceTemplate[] { return catalog.deviceTemplates.filter((t) => !t.custom && hiddenPublicIds.has(t.id)) } export function allDeviceCategories(catalog: Catalog): DeviceCategoryDef[] { return catalog.deviceCategories } export function getDeviceCategory(catalog: Catalog, categoryId: string): DeviceCategoryDef | undefined { return allDeviceCategories(catalog).find((c) => c.id === categoryId) } /** Friendly label for a category id, falling back to the raw id if it's * somehow unknown (e.g. data from an older export) rather than crashing. */ export function deviceCategoryName(catalog: Catalog, categoryId: string): string { return getDeviceCategory(catalog, categoryId)?.name ?? categoryId } export function getPortType(catalog: Catalog, portTypeId: string): PortType | undefined { return allPortTypes(catalog).find((pt) => pt.id === portTypeId) } export function getCableType(catalog: Catalog, cableTypeId: string): CableType | undefined { return allCableTypes(catalog).find((ct) => ct.id === cableTypeId) } /** The set of connector families a port type will mate with: its own family * plus anything it explicitly lists as also-compatible. Shared with * `compatibility.ts` so "does A connect to B" and "which cable fits" agree. */ export function acceptedFamilies(portType: PortType): Set { return new Set([portType.family, ...(portType.compatibleFamilyIds ?? [])]) } /** A cable's two connector ends as families — `family2` falls back to * `family` for a plain (non-adapter) cable, so it always has exactly two. */ function cableEnds(cableType: CableType): [string, string] { return [cableType.family, cableType.family2 ?? cableType.family] } /** Does this cable type fit between a port that accepts `sourceFamilies` and * one that accepts `targetFamilies`? Checked in both orientations since a * passive cable (including an adapter with different connectors on each * end) can be plugged in either way around. */ function cableFits(cableType: CableType, sourceFamilies: Set, targetFamilies: Set): boolean { const [endA, endB] = cableEnds(cableType) if (sourceFamilies.has(endA) && targetFamilies.has(endB)) return true if (sourceFamilies.has(endB) && targetFamilies.has(endA)) return true return false } /** Cable types usable with the given port type at (at least) one end — for * a plain cable that means its family matches; for an adapter cable it's * enough that either end matches. */ export function cableTypesForPortType(catalog: Catalog, portType: PortType): CableType[] { const families = acceptedFamilies(portType) return allCableTypes(catalog).filter((ct) => { const [endA, endB] = cableEnds(ct) return families.has(endA) || families.has(endB) }) } /** * Cable types valid for a connection between two specific ports. For a plain * cable this is the *intersection* of what each end accepts, not the union: * a "combo" jack (e.g. a patch bay accepting balanced or unbalanced) paired * with a plain balanced port should only offer balanced cable — the plain * end still dictates what's electrically correct, even though the combo end * alone would accept more. An adapter cable (different connector families on * each end — e.g. XLR-to-TRS) is included whenever its two ends line up with * the two ports, in either orientation; that's what lets two port types that * aren't declared generally compatible still be wired with the right cable, * without changing either port type's own compatibility rules. */ export function cableTypesForConnection(catalog: Catalog, sourcePortType: PortType, targetPortType: PortType): CableType[] { const sourceFamilies = acceptedFamilies(sourcePortType) const targetFamilies = acceptedFamilies(targetPortType) return allCableTypes(catalog).filter((ct) => cableFits(ct, sourceFamilies, targetFamilies)) } /** Whether any cable type (built-in or custom) could bridge these two port * types — used to allow a connection even when the port types aren't * declared directly compatible, as long as a specific adapter cable exists. */ export function hasBridgingCableType(catalog: Catalog, sourcePortType: PortType, targetPortType: PortType): boolean { return cableTypesForConnection(catalog, sourcePortType, targetPortType).length > 0 } /** * Resolves a set of "also compatible with" picks (port type ids, as checked * in the connector-type editor) into the family list to store on a new/edited * port type. Crucially this pulls in each picked type's *full* accepted-family * set, not just its own bare family — so picking an existing combo/hybrid type * (like "1/4" TRS/TS (Balanced or Unbalanced)") inherits everything *that* * type accepts, rather than only matching other ports that are that exact * combo type. Without this, "compatible with a combo type" wouldn't imply * "compatible with what the combo type itself accepts", which is the * confusing gap that makes newly-authored combo connectors fail to mate with * the plain connectors they were meant to cover. */ export function expandCompatibleFamilyIds(selectedPortTypeIds: string[], allTypes: PortType[]): string[] { const families = new Set() for (const id of selectedPortTypeIds) { const portType = allTypes.find((pt) => pt.id === id) if (!portType) continue for (const family of acceptedFamilies(portType)) families.add(family) } return [...families] } export function createEmptyProject(name = 'Untitled Project'): Project { const now = new Date().toISOString() return { id: uuid(), name, createdAt: now, updatedAt: now, devices: [], connections: [], } } /** Instantiates a device on the canvas from a template, deep-copying ports * (with fresh ids) so editing the instance never mutates the template. */ export function createDeviceFromTemplate(template: DeviceTemplate, position: { x: number; y: number }): Device { return { id: uuid(), name: template.name, templateId: template.id, category: template.category, manufacturer: template.manufacturer, model: template.model, ports: template.ports.map((port) => ({ ...port, id: uuid() })), cost: template.cost, position, } } /** "CV Out 1" -> "CV Out 2", "Port 3" -> "Port 4", "Gate In" -> "Gate In 2". * Only increments a number that's the very last thing in the name (trailing * whitespace aside), so labels like `1/4" TS In` aren't misread as ending * in a counter — they just get " 2" appended instead. Used by "clone port". */ export function incrementPortName(name: string): string { const match = name.match(/^(.*?)(\d+)\s*$/) if (match) { const [, prefix, digits] = match const next = String(Number(digits) + 1).padStart(digits.length, '0') return `${prefix}${next}` } return `${name.trim()} 2` }