Skip to main content

@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-board
npm install @lupinum/board-connections @lupinum/board-core @lupinum/vue-board

connectionsPlugin

Creates the connections plugin. Install it during engine creation via createBoardEngine({ plugins }).

ts
const plugin = connectionsPlugin({ routing: 'bezier' })

Options

NameTypeDefaultDescription
routingConnectionRouting'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.
ts
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.

ts
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>
ts
const edge = engine.plugins.connections.createEdge({
  from: nodeA,
  to: nodeB,
  label: 'depends on',
  color: '#0f766e',
  data: {},
})

deleteEdge

Removes an edge by ID.

ts
deleteEdge(id: EdgeId): void

updateEdge

Updates an existing edge in place. This is the API used by endpoint reconnect interactions.

ts
updateEdge<T>(id: EdgeId, patch: BoardEdgePatch<T>): BoardEdge<T>
ts
engine.plugins.connections.updateEdge(edge.id, {
  to: anotherNodeId,
  toAnchor: undefined,
})

getEdge

Returns a single edge by ID.

ts
getEdge(id: EdgeId): BoardEdge | undefined

getEdges

Returns all edges.

ts
getEdges(): BoardEdge[]

getEdgesFrom

Returns all edges originating from a node.

ts
getEdgesFrom(id: NodeId): BoardEdge[]

getEdgesTo

Returns all edges pointing to a node.

ts
getEdgesTo(id: NodeId): BoardEdge[]

getEdgesBetween

Returns all directed edges from from to to.

ts
getEdgesBetween(from: NodeId, to: NodeId): BoardEdge[]

BoardConnectionLayer

A Vue component that renders edges as SVG paths inside a BoardRoot.

ts
import { BoardConnectionLayer } from '@lupinum/board-connections/vue'

Props

NameTypeDefaultDescription
routingConnectionRouting | undefinedplugin defaultRouting style for all edges.
endpointModeConnectionEndpointMode | undefinedplugin defaultUI endpoint behavior: auto side resolution or manual side locking.
createNodeForConnection(ctx: CreateNodeForConnectionContext) => BoardNode | nullnullOptional 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.

PropTypeDescription
edgeBoardEdgeThe edge data.
sourceResolvedConnectionEndpointResolved source endpoint metadata.
targetResolvedConnectionEndpointResolved target endpoint metadata.
routeConnectionRouteRouted geometry, bounds, label point, and path string.
vue
<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:

vue
<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.

ts
function resolveAnchorPoint(
  node: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
  anchor: AnchorPosition,
): Point

resolveAutoAnchorSide

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.

ts
function resolveAutoAnchorSide(
  source: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
  target: Pick<BoardNode, 'x' | 'y' | 'width' | 'height'>,
  role: 'source' | 'target',
  previousSide?: AnchorSide,
): AnchorSide

resolveConnectionEndpoint

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.

ts
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,
): ResolvedConnectionEndpoint

buildConnectionRoute

Builds a routed connection path from fully resolved source and target endpoints.

ts
function buildConnectionRoute(input: {
  source: ResolvedConnectionEndpoint
  target: ResolvedConnectionEndpoint
  routing?: ConnectionRouting
}): ConnectionRoute
RoutingDescription
'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.

ts
function buildArcRoute(
  source: ResolvedConnectionEndpoint,
  target: ResolvedConnectionEndpoint,
  options?: ArcOptions,
): ConnectionRoute

Edge color helpers

The package exports preset helpers for edge UI:

ts
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.

ts
function resolveFloatingEndpoint(
  point: Point,
  otherPoint: Point,
  role: 'source' | 'target',
  previousSide?: AnchorSide,
): ResolvedConnectionEndpoint

resolveEdgeRenderState

Resolves source/target endpoints and the routed path in one step.

ts
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.

ts
function getVisibleEdges(
  engine: BoardEngine,
  bounds: Bounds,
  routing?: ConnectionRouting,
): BoardEdge[]

Types

BoardEdge

ts
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

ts
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

ts
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:

ts
engine.plugins.connections.updateEdge(edgeId, {
  fromAnchor: undefined,
})

AnchorSide

ts
type AnchorSide = 'top' | 'right' | 'bottom' | 'left'

ConnectionRouting

ts
type ConnectionRouting = 'bezier' | 'smooth-step' | 'step' | 'straight' | 'arc'

ConnectionEndpointMode

ts
type ConnectionEndpointMode = 'auto' | 'manual'

EdgeEnd

ts
type EdgeEnd = 'none' | 'arrow'

ConnectionConfig

Resolved defaults installed by connectionsPlugin() and returned by engine.plugins.connections.getConfig().

ts
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.

ts
interface CreateNodeForConnectionContext {
  sourceNodeId: NodeId
  sourceSide: AnchorSide
  pointerWorld: Point
  candidateAnchor: AnchorPosition | null
}

ResolvedConnectionEndpoint

ts
interface ResolvedConnectionEndpoint {
  nodeId: NodeId
  node: Pick<BoardNode, 'id' | 'x' | 'y' | 'width' | 'height'>
  side: AnchorSide
  offset: number
  point: Point
  kind: 'explicit' | 'auto'
}

ConnectionRoute

ts
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.

ts
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.

ts
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:

EventHandlerDescription
edge:created(edge: BoardEdge) => voidAn edge was created.
edge:updated(edge: BoardEdge, prev: BoardEdge) => voidAn edge was updated or reconnected.
edge:deleted(edgeId: EdgeId) => voidAn edge was removed.
ts
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.