122 lines
3.0 KiB
TypeScript
122 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import type { MouseEvent as ReactMouseEvent } from "react";
|
|
import type { CanvasConnection, CanvasNode, CanvasPosition } from "@/lib/canvas/types";
|
|
import { getActiveConnectionPath, getConnectionPath } from "@/lib/canvas/geometry";
|
|
|
|
type CanvasConnectionsProps = {
|
|
connections: CanvasConnection[];
|
|
nodes: CanvasNode[];
|
|
selectedConnectionId: string | null;
|
|
activeConnection?: {
|
|
fromNodeId: string;
|
|
mouseWorld: CanvasPosition;
|
|
} | null;
|
|
onSelectConnection: (id: string) => void;
|
|
};
|
|
|
|
export function CanvasConnections({
|
|
connections,
|
|
nodes,
|
|
selectedConnectionId,
|
|
activeConnection,
|
|
onSelectConnection,
|
|
}: CanvasConnectionsProps) {
|
|
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
const activeFrom = activeConnection
|
|
? byId.get(activeConnection.fromNodeId)
|
|
: undefined;
|
|
|
|
return (
|
|
<svg
|
|
className="pointer-events-none absolute left-0 top-0 overflow-visible"
|
|
style={{ width: 1, height: 1 }}
|
|
>
|
|
{connections.map((connection) => {
|
|
const from = byId.get(connection.fromNodeId);
|
|
const to = byId.get(connection.toNodeId);
|
|
if (!from || !to) return null;
|
|
|
|
return (
|
|
<ConnectionPath
|
|
key={connection.id}
|
|
active={connection.id === selectedConnectionId}
|
|
connection={connection}
|
|
from={from}
|
|
to={to}
|
|
onSelect={() => onSelectConnection(connection.id)}
|
|
/>
|
|
);
|
|
})}
|
|
{activeConnection && activeFrom ? (
|
|
<path
|
|
d={getActiveConnectionPath(
|
|
{
|
|
x: activeFrom.position.x + activeFrom.width,
|
|
y: activeFrom.position.y + activeFrom.height / 2,
|
|
},
|
|
activeConnection.mouseWorld,
|
|
)}
|
|
fill="none"
|
|
stroke="#18181b"
|
|
strokeDasharray="7 7"
|
|
strokeWidth="2.5"
|
|
/>
|
|
) : null}
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ConnectionPath({
|
|
connection,
|
|
from,
|
|
to,
|
|
active,
|
|
onSelect,
|
|
}: {
|
|
connection: CanvasConnection;
|
|
from: CanvasNode;
|
|
to: CanvasNode;
|
|
active: boolean;
|
|
onSelect: () => void;
|
|
}) {
|
|
const path = getConnectionPath(from, to);
|
|
const stroke = active ? "#18181b" : "#a1a1aa";
|
|
|
|
function handleContextMenu(event: ReactMouseEvent<SVGPathElement>) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
onSelect();
|
|
}
|
|
|
|
return (
|
|
<g>
|
|
<path
|
|
data-connection-id={connection.id}
|
|
d={path}
|
|
fill="none"
|
|
stroke="transparent"
|
|
strokeWidth="18"
|
|
style={{ cursor: "pointer", pointerEvents: "stroke" }}
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onSelect();
|
|
}}
|
|
onContextMenu={handleContextMenu}
|
|
/>
|
|
<path
|
|
d={path}
|
|
fill="none"
|
|
stroke={stroke}
|
|
strokeLinecap="round"
|
|
strokeOpacity={active ? 1 : 0.72}
|
|
strokeWidth={active ? 3 : 2}
|
|
style={{
|
|
filter: active ? "drop-shadow(0 0 8px rgba(17,24,39,.25))" : undefined,
|
|
pointerEvents: "none",
|
|
}}
|
|
/>
|
|
</g>
|
|
);
|
|
}
|