| 1 |
"use client"; |
| 2 |
|
| 3 |
import React from "react"; |
| 4 |
|
| 5 |
export function Connections({ |
| 6 |
connections, |
| 7 |
nodes, |
| 8 |
scale, |
| 9 |
onDeleteConnection, |
| 10 |
connectingFrom, |
| 11 |
mousePos, |
| 12 |
panOffset, |
| 13 |
}) { |
| 14 |
const getNodePosition = (nodeId, handle = "right") => { |
| 15 |
const node = nodes.find((n) => n.id === nodeId); |
| 16 |
if (!node) return { x: 0, y: 0 }; |
| 17 |
|
| 18 |
const nodeWidth = 180; |
| 19 |
const nodeHeight = 56; |
| 20 |
const labelHeight = 24; |
| 21 |
|
| 22 |
let x = node.position.x * scale; |
| 23 |
let y = (node.position.y + labelHeight) * scale; |
| 24 |
|
| 25 |
switch (handle) { |
| 26 |
case "left": |
| 27 |
y += (nodeHeight * scale) / 2; |
| 28 |
break; |
| 29 |
case "right": |
| 30 |
x += nodeWidth * scale; |
| 31 |
y += (nodeHeight * scale) / 2; |
| 32 |
break; |
| 33 |
default: |
| 34 |
x += nodeWidth * scale; |
| 35 |
y += (nodeHeight * scale) / 2; |
| 36 |
} |
| 37 |
|
| 38 |
return { x, y }; |
| 39 |
}; |
| 40 |
|
| 41 |
const createBezierPath = (from, to) => { |
| 42 |
const dx = Math.abs(to.x - from.x); |
| 43 |
const controlOffset = Math.min(dx * 0.5, 100); |
| 44 |
|
| 45 |
return `M ${from.x} ${from.y} C ${from.x + controlOffset} ${from.y}, ${to.x - controlOffset} ${to.y}, ${to.x} ${to.y}`; |
| 46 |
}; |
| 47 |
|
| 48 |
const getConnectingLineEnd = () => { |
| 49 |
if (!connectingFrom) return null; |
| 50 |
|
| 51 |
const fromPos = getNodePosition( |
| 52 |
connectingFrom.nodeId, |
| 53 |
connectingFrom.handle |
| 54 |
); |
| 55 |
|
| 56 |
return { |
| 57 |
from: fromPos, |
| 58 |
to: { |
| 59 |
x: mousePos.x - panOffset.x, |
| 60 |
y: mousePos.y - panOffset.y, |
| 61 |
}, |
| 62 |
}; |
| 63 |
}; |
| 64 |
|
| 65 |
const connectingLine = getConnectingLineEnd(); |
| 66 |
|
| 67 |
return ( |
| 68 |
<svg |
| 69 |
className="absolute inset-0 pointer-events-none overflow-visible" |
| 70 |
style={{ width: "100%", height: "100%", minWidth: "2000px", minHeight: "2000px" }} |
| 71 |
> |
| 72 |
<defs> |
| 73 |
<marker |
| 74 |
id="arrowhead" |
| 75 |
markerWidth="10" |
| 76 |
markerHeight="10" |
| 77 |
refX="8" |
| 78 |
refY="5" |
| 79 |
orient="auto" |
| 80 |
> |
| 81 |
<path |
| 82 |
d="M 0 0 L 10 5 L 0 10 z" |
| 83 |
fill="currentColor" |
| 84 |
className="text-foreground/50" |
| 85 |
/> |
| 86 |
</marker> |
| 87 |
<marker |
| 88 |
id="arrowhead-active" |
| 89 |
markerWidth="10" |
| 90 |
markerHeight="10" |
| 91 |
refX="8" |
| 92 |
refY="5" |
| 93 |
orient="auto" |
| 94 |
> |
| 95 |
<path d="M 0 0 L 10 5 L 0 10 z" className="fill-primary" /> |
| 96 |
</marker> |
| 97 |
</defs> |
| 98 |
|
| 99 |
{/* Existing connections */} |
| 100 |
{connections.map((conn) => { |
| 101 |
const fromPos = getNodePosition(conn.from, conn.fromHandle || "right"); |
| 102 |
const toPos = getNodePosition(conn.to, conn.toHandle || "left"); |
| 103 |
const path = createBezierPath(fromPos, toPos); |
| 104 |
|
| 105 |
return ( |
| 106 |
<g key={conn.id} className="group/connection"> |
| 107 |
{/* Invisible wider path for easier clicking */} |
| 108 |
<path |
| 109 |
d={path} |
| 110 |
fill="none" |
| 111 |
stroke="transparent" |
| 112 |
strokeWidth={20} |
| 113 |
className="pointer-events-auto cursor-pointer" |
| 114 |
onClick={() => onDeleteConnection(conn.id)} |
| 115 |
/> |
| 116 |
{/* Visible path */} |
| 117 |
<path |
| 118 |
d={path} |
| 119 |
fill="none" |
| 120 |
stroke="currentColor" |
| 121 |
strokeWidth={2} |
| 122 |
className="text-foreground/40 pointer-events-none" |
| 123 |
markerEnd="url(#arrowhead)" |
| 124 |
/> |
| 125 |
{/* Hover state path */} |
| 126 |
<path |
| 127 |
d={path} |
| 128 |
fill="none" |
| 129 |
stroke="currentColor" |
| 130 |
strokeWidth={3} |
| 131 |
className="text-destructive opacity-0 group-hover/connection:opacity-100 transition-opacity pointer-events-none" |
| 132 |
markerEnd="url(#arrowhead)" |
| 133 |
/> |
| 134 |
</g> |
| 135 |
); |
| 136 |
})} |
| 137 |
|
| 138 |
{/* Active connecting line */} |
| 139 |
{connectingLine && ( |
| 140 |
<path |
| 141 |
d={createBezierPath(connectingLine.from, connectingLine.to)} |
| 142 |
fill="none" |
| 143 |
stroke="currentColor" |
| 144 |
strokeWidth={2} |
| 145 |
strokeDasharray="5,5" |
| 146 |
className="text-primary" |
| 147 |
markerEnd="url(#arrowhead-active)" |
| 148 |
/> |
| 149 |
)} |
| 150 |
</svg> |
| 151 |
); |
| 152 |
} |
| 153 |
|
| 154 |
export default Connections; |
| 155 |
|