@lupinum/board-connections
Edge and connection management plugin with routing, anchors, and an SVG connection layer.
Install
pnpm add @lupinum/board-connections @lupinum/board-core @lupinum/vue-boardconnectionsPlugin
Creates the connections plugin. Install it during engine creation via createBoardEngine({ plugins }).
const plugin = connectionsPlugin({ routing: 'bezier' })Options
| Name | Type | Default | Description |
|---|---|---|---|
routing | ConnectionRouting | 'bezier' | Default edge routing style. |
endpointMode | 'auto' | 'manual' | 'auto' | Whether UI-created endpoints adapt or lock to side anchors. |
defaultArrow | 'none' | 'start' | 'end' | 'both' | 'end' | Default arrowhead placement. |
import { createBoardEngine } from '@lupinum/board-core'
import { connectionsPlugin } from '@lupinum/board-connections'
const engine = createBoardEngine({
plugins: [connectionsPlugin({ routing: 'bezier' })],
})Plugin API
After installing the plugin, the connections API is available on engine.plugins.connections.
createEdge
Creates a new edge between two nodes. Returns the created edge.
createEdge<T>(input: {
id?: EdgeId
from: NodeId
to: NodeId
fromAnchor?: AnchorPosition
toAnchor?: AnchorPosition
fromEnd?: EdgeEnd
toEnd?: EdgeEnd
label?: string
color?: string
data: T
zIndex?: number
}): BoardEdge<T>const edge = engine.plugins.connections.createEdge({
from: nodeA,
to: nodeB,
label: 'depends on',
color: '#0f766e',
data: {},
})deleteEdge
Removes an edge by ID.
deleteEdge(id: EdgeId): voidupdateEdge
Updates an existing edge in place. This is the API used by endpoint reconnect interactions.
updateEdge<T>(id: EdgeId, patch: BoardEdgePatch<T>): BoardEdge<T>engine.plugins.connections.updateEdge(edge.id, {
to: anotherNodeId,
toAnchor: undefined,
})getEdge
Returns a single edge by ID.
getEdge(id: EdgeId): BoardEdge | undefinedgetEdges
Returns all edges.
getEdges(): BoardEdge[]getEdgesFrom
Returns all edges originating from a node.
getEdgesFrom(id: NodeId): BoardEdge[]getEdgesTo
Returns all edges pointing to a node.
getEdgesTo(id: NodeId): BoardEdge[]getEdgesBetween
Returns all directed edges from from to to.
getEdgesBetween(from: NodeId, to: NodeId): BoardEdge[]BoardConnectionLayer
A Vue component that renders edges as SVG paths inside a BoardRoot.
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'Props
| Name | Type | Default | Description |
|---|---|---|---|
routing | ConnectionRouting | undefined | plugin default | Routing style for all edges. |
endpointMode | ConnectionEndpointMode | undefined | plugin default | UI endpoint behavior: auto side resolution or manual side locking. |
createNodeForConnection | (ctx: CreateNodeForConnectionContext) => BoardNode | null | null | Optional host policy for creating a node when a new connection is dropped on empty space. |
Slots
edge
Custom edge rendering. If not provided, edges render as SVG <path> elements.
| Prop | Type | Description |
|---|---|---|
edge | BoardEdge | The edge data. |
source | ResolvedConnectionEndpoint | Resolved source endpoint metadata. |
target | ResolvedConnectionEndpoint | Resolved target endpoint metadata. |
route | ConnectionRoute | Routed geometry, bounds, label point, and path string. |
<BoardConnectionLayer routing="smooth-step">
<template #edge="{ edge, route }">
<path :d="route.path" stroke="blue" stroke-width="2" fill="none" />
<text :x="route.labelPoint.x" :y="route.labelPoint.y">{{ edge.label }}</text>
</template>
</BoardConnectionLayer>Usage
Render BoardConnectionLayer under BoardRoot. It teleports the SVG layer to the board root and applies the camera transform itself:
<BoardRoot :engine="engine">
<BoardConnectionLayer />
</BoardRoot>BoardConnectionLayer handles both connection creation and reconnect. Hover a card edge to reveal a filled midpoint handle and drag it to another card to create a new edge. Hover or select an existing edge to reveal reconnect handles; dragging either end previews the route live and commits via updateEdge() when dropped on another node. Dropping a new connection on empty space cancels unless createNodeForConnection returns a node for the layer to connect.
By default, UI-created edges use endpointMode: 'auto': they do not store fromAnchor or toAnchor, so each endpoint resolves to the best node side as nodes move. Dragging an existing endpoint onto a node side stores that endpoint as { side, offset }, where offset is the exact point under the pointer. Use endpointMode: 'manual' when newly created edges should also lock to the dragged side offsets. Selected manual edges expose reset actions that clear one or both anchors back to auto.
Utility functions
resolveAnchorPoint
Resolves an anchor position to a world-space point on a node.
function resolveAnchorPoint(
node: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
anchor: AnchorPosition,
): PointresolveAutoAnchorSide
Chooses the best side for an auto-routed endpoint and uses a deadband to reduce flicker near diagonals. Auto-routed endpoints then attach at the center of that side.
function resolveAutoAnchorSide(
source: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
target: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
role: 'source' | 'target',
previousSide?: AnchorSide,
): AnchorSideresolveConnectionEndpoint
Resolves one edge endpoint to a node side, normalized side offset, and world-space point. Explicit anchors are preserved; automatic endpoints choose the best side from the paired node.
function resolveConnectionEndpoint(
edge: BoardEdge,
node: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
otherNode: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
role: 'source' | 'target',
previousSide?: AnchorSide,
): ResolvedConnectionEndpointbuildConnectionRoute
Builds a routed connection path from fully resolved source and target endpoints.
function buildConnectionRoute(input: {
source: ResolvedConnectionEndpoint
target: ResolvedConnectionEndpoint
routing?: ConnectionRouting
}): ConnectionRoute| Routing | Description |
|---|---|
'bezier' | Smooth cubic bezier curve (default). |
'smooth-step' | Rounded orthogonal connector. |
'step' | Right-angle stepped path. |
'straight' | Direct straight line. |
'arc' | Curved arc route. |
buildArcRoute
Builds a curved arc route for hand-drawn or sketch-style edge rendering.
function buildArcRoute(
source: ResolvedConnectionEndpoint,
target: ResolvedConnectionEndpoint,
options?: ArcOptions,
): ConnectionRouteEdge color helpers
The package exports preset helpers for edge UI:
import {
EDGE_COLOR_PRESETS,
colorForPreset,
presetForColor,
resolvePresetColor,
} from '@lupinum/board-connections'Use these helpers when a toolbar stores edge colors as presets but the renderer needs a CSS color string.
resolveFloatingEndpoint
Builds a temporary endpoint around a free pointer position for reconnect previews.
function resolveFloatingEndpoint(
point: Point,
otherPoint: Point,
role: 'source' | 'target',
previousSide?: AnchorSide,
): ResolvedConnectionEndpointresolveEdgeRenderState
Resolves source/target endpoints and the routed path in one step.
function resolveEdgeRenderState(
edge: BoardEdge,
sourceNode: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
targetNode: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>,
options?: {
routing?: ConnectionRouting
previousSourceSide?: AnchorSide
previousTargetSide?: AnchorSide
},
): {
source: ResolvedConnectionEndpoint
target: ResolvedConnectionEndpoint
route: ConnectionRoute
}getVisibleEdges
Returns edges whose routed path bounds intersect the given viewport bounds.
function getVisibleEdges(
engine: BoardEngine,
bounds: Bounds,
routing?: ConnectionRouting,
): BoardEdge[]Types
BoardEdge
interface BoardEdge<T = Record<string, unknown>> {
id: EdgeId
from: NodeId
to: NodeId
fromAnchor?: AnchorPosition // where the edge attaches on the source node
toAnchor?: AnchorPosition // where the edge attaches on the target node
fromEnd?: EdgeEnd
toEnd?: EdgeEnd
label?: string
color?: string
data: T // custom edge payload
zIndex: number
}BoardEdgePatch
interface BoardEdgePatch<T = Record<string, unknown>> {
from?: NodeId
to?: NodeId
fromAnchor?: AnchorPosition
toAnchor?: AnchorPosition
fromEnd?: EdgeEnd
toEnd?: EdgeEnd
label?: string
color?: string
data?: T
}AnchorPosition
interface AnchorPosition {
side: AnchorSide // 'top' | 'right' | 'bottom' | 'left'
offset: number // 0–1 position along the side (0.5 = center)
}When fromAnchor / toAnchor are omitted, the connection layer resolves the side automatically and uses offset = 0.5.
Set an anchor to undefined with updateEdge() to reset that endpoint to automatic side resolution:
engine.plugins.connections.updateEdge(edgeId, {
fromAnchor: undefined,
})AnchorSide
type AnchorSide = 'top' | 'right' | 'bottom' | 'left'ConnectionRouting
type ConnectionRouting = 'bezier' | 'smooth-step' | 'step' | 'straight' | 'arc'ConnectionEndpointMode
type ConnectionEndpointMode = 'auto' | 'manual'EdgeEnd
type EdgeEnd = 'none' | 'arrow'ConnectionConfig
Resolved defaults installed by connectionsPlugin() and returned by engine.plugins.connections.getConfig().
interface ConnectionConfig {
routing: ConnectionRouting
endpointMode: ConnectionEndpointMode
defaultArrow: 'none' | 'start' | 'end' | 'both'
}CreateNodeForConnectionContext
Context passed to BoardConnectionLayer when a host opts into creating a node from an empty connection drop.
interface CreateNodeForConnectionContext {
sourceNodeId: NodeId
sourceSide: AnchorSide
pointerWorld: Point
candidateAnchor: AnchorPosition | null
}ResolvedConnectionEndpoint
interface ResolvedConnectionEndpoint {
nodeId: NodeId
node: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>
side: AnchorSide
offset: number
point: Point
kind: 'explicit' | 'auto'
}ConnectionRoute
interface ConnectionRoute {
routing: ConnectionRouting
path: string
labelPoint: Point
bounds: Bounds
waypoints: Point[]
segments: ConnectionRouteSegment[]
}ConnectionRouteSegment
A routed connection is made of path segments used by custom edge renderers.
type ConnectionRouteSegment =
| { type: 'line'; from: Point; to: Point }
| {
type: 'cubic'
from: Point
control1: Point
control2: Point
to: Point
}ConnectionsApi
Installed by connectionsPlugin() on engine.plugins.connections.
interface ConnectionsApi {
createEdge<T>(input: CreateEdgeInput<T>): BoardEdge<T>
updateEdge<T>(id: EdgeId, patch: BoardEdgePatch<T>): BoardEdge<T>
deleteEdge(id: EdgeId): void
getEdge(id: EdgeId): BoardEdge | undefined
getEdges(): BoardEdge[]
getEdgesFrom(id: NodeId): BoardEdge[]
getEdgesTo(id: NodeId): BoardEdge[]
getEdgesBetween(from: NodeId, to: NodeId): BoardEdge[]
getConfig(): ConnectionConfig
}Events
The connections plugin adds these events to BoardEventMap:
| Event | Handler | Description |
|---|---|---|
edge:created | (edge: BoardEdge) => void | An edge was created. |
edge:updated | (edge: BoardEdge, prev: BoardEdge) => void | An edge was updated or reconnected. |
edge:deleted | (edgeId: EdgeId) => void | An edge was removed. |
engine.on('edge:created', (edge) => {
console.log('New edge:', edge.from, '->', edge.to)
})
engine.on('edge:updated', (edge, prev) => {
console.log('Moved edge:', prev.id, prev.to, '->', edge.to)
})Edges are automatically deleted when either endpoint node is deleted.