tscircuit/ti
ti tscircuit library
- Version
- 1.0.197
- License
- unset
- Stars
- 0
system-block-ui/src/editor/types.ts
import type { Edge, Node } from "@xyflow/react";
import type {
BlockInstance,
BlockPosition,
ConnectionKind,
ConnectionKindInput,
ConnectionProtocol,
ConnectionResolutionError,
LogicalConnection,
PortDefinition,
ResolvedConnection,
SubcircuitDefinition,
} from "../model/types";
export type SystemBlockPosition = BlockPosition;
export const SYSTEM_BLOCK_SOCKET_LABELS = {
power: "Power",
data: "Data",
} satisfies Record<ConnectionKind, string>;
export const SYSTEM_BLOCK_CONNECTION_COLORS = {
power: "#d97706",
data: "#377bd4",
} satisfies Record<ConnectionKind, string>;
const OUTPUT_ROLES = new Set(["provider", "source", "host", "controller"]);
const INPUT_ROLES = new Set(["consumer", "sink", "device", "peripheral"]);
const PROTOCOL_LABELS: Readonly<Record<string, string>> = {
"can-bus": "CAN Bus",
"can-controller": "CAN",
"hci-uart": "HCI UART",
"motor-control": "Motor Control",
gpio: "GPIO",
i2c: "I²C",
i2s: "I²S",
power: "Power",
uart: "UART",
};
export function formatProtocolLabel(protocol: string): string {
return (
PROTOCOL_LABELS[protocol] ??
protocol
.split(/[-_]/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ")
);
}
export function normalizeConnectionKind(
kind: ConnectionKindInput,
): ConnectionKind {
return kind.toLowerCase() as ConnectionKind;
}
export function getSystemBlockHandleId(
side: "input" | "output",
kind: ConnectionKind,
): string {
return `${side}:${kind}`;
}
/** @deprecated Use getSystemBlockHandleId. */
export const getSystemBlockSocketKey = getSystemBlockHandleId;
function portSupportsSide(
port: PortDefinition,
side: "input" | "output",
): boolean {
if (port.role === "peer") return true;
return side === "input"
? INPUT_ROLES.has(port.role)
: OUTPUT_ROLES.has(port.role);
}
export interface SemanticInterfaceSummary {
kind: ConnectionKind;
label: string;
color: string;
inputPorts: readonly PortDefinition[];
outputPorts: readonly PortDefinition[];
protocols: readonly string[];
}
export type SystemBlockNodeData = {
block: BlockInstance;
definition: SubcircuitDefinition;
interfaces: readonly SemanticInterfaceSummary[];
};
export type SystemBlockNode = Node<SystemBlockNodeData, "systemBlock">;
export type SystemBlockConnectionData = {
logical: LogicalConnection;
resolved: ResolvedConnection;
kind: ConnectionKind;
label: string;
color: string;
};
export type SystemBlockConnection = Edge<
SystemBlockConnectionData,
"semantic"
> & { data: SystemBlockConnectionData };
export function getSemanticInterfaces(
definition: SubcircuitDefinition,
): readonly SemanticInterfaceSummary[] {
return (["power", "data"] as const).flatMap((kind) => {
const inputPorts = definition.ports.filter(
(port) => port.kind === kind && portSupportsSide(port, "input"),
);
const outputPorts = definition.ports.filter(
(port) => port.kind === kind && portSupportsSide(port, "output"),
);
if (inputPorts.length === 0 && outputPorts.length === 0) return [];
const protocols = [
...new Set(
[...inputPorts, ...outputPorts]
.map((port) => port.protocol)
.filter((protocol): protocol is string => Boolean(protocol)),
),
];
return [
{
kind,
label: SYSTEM_BLOCK_SOCKET_LABELS[kind],
color: SYSTEM_BLOCK_CONNECTION_COLORS[kind],
inputPorts,
outputPorts,
protocols,
},
];
});
}
export function createSystemBlockNode(
block: BlockInstance,
definition: SubcircuitDefinition,
): SystemBlockNode {
const position = block.position ?? { x: 0, y: 0 };
const normalizedBlock: BlockInstance = {
...block,
definitionId: definition.id,
position: { ...position },
};
return {
id: normalizedBlock.id,
type: "systemBlock",
position: { ...position },
data: {
block: normalizedBlock,
definition,
interfaces: getSemanticInterfaces(definition),
},
ariaLabel: `${definition.title} system block`,
deletable: true,
draggable: true,
selectable: true,
};
}
export function createSystemBlockConnection(
source: SystemBlockNode,
target: SystemBlockNode,
logical: LogicalConnection,
resolved: ResolvedConnection,
): SystemBlockConnection {
const kind = normalizeConnectionKind(logical.kind);
const normalizedLogical: LogicalConnection = { ...logical, kind };
const label =
kind === "power"
? "Power"
: resolved.protocol
? `Data · ${formatProtocolLabel(resolved.protocol)}`
: "Data";
return {
id: logical.id,
type: "semantic",
source: source.id,
target: target.id,
sourceHandle: getSystemBlockHandleId("output", kind),
targetHandle: getSystemBlockHandleId("input", kind),
data: {
logical: normalizedLogical,
resolved: {
...resolved,
traces: resolved.traces.map((trace) => ({ ...trace })),
},
kind,
label,
color: SYSTEM_BLOCK_CONNECTION_COLORS[kind],
},
ariaLabel: `${label} connection from ${source.data.definition.title} to ${target.data.definition.title}`,
deletable: true,
focusable: true,
selectable: true,
};
}
export interface SystemBlockGraphSnapshot {
blocks: BlockInstance[];
connections: LogicalConnection[];
resolvedConnections: ResolvedConnection[];
}
export interface SystemBlockInitialGraph {
blocks: readonly BlockInstance[];
connections: readonly LogicalConnection[];
}
export interface ConnectBlocksOptions {
id?: string;
protocol?: ConnectionProtocol;
}
export type GraphChangeListener = (snapshot: SystemBlockGraphSnapshot) => void;
export interface RejectedConnection {
error: ConnectionResolutionError;
connection?: LogicalConnection;
}
export interface CreateSystemBlockEditorOptions {
catalog?: readonly SubcircuitDefinition[];
initialGraph?: SystemBlockInitialGraph;
onBlockSelectionRequested?: (blockId: string) => void;
onGraphChange?: GraphChangeListener;
onConnectionRejected?: (rejection: RejectedConnection) => void;
}
export interface ReplaceBlockDefinitionResult {
block: BlockInstance;
removedConnections: readonly LogicalConnection[];
retainedConnectionCount: number;
}
export interface SystemBlockRenderState {
revision: number;
nodes: SystemBlockNode[];
edges: SystemBlockConnection[];
}