Shape surfaces and application-owned graphs

Connect whole node borders, customize snapping geometry, and keep domain data in your framework.

Ordinary SnapLine connectors are visible ports with circular hit areas and center anchors. A surface connector keeps the same connection lifecycle while letting the application define where a gesture can start, where it can land, and where a line attaches.

Use a surface connector for diagrams whose boxes, circles, or other shapes act as their own ports:

<Connector
  id="node-1:surface"
  name="surface"
  virtual
  rules={{
    maxOutgoing: "unlimited",
    maxIncoming: "unlimited",
    reconnect: false,
    allowParallel: true
  }}
  {surfaceStrategies}
/>

The rules shown override defaults of maxOutgoing: "unlimited", maxIncoming: 1, reconnect: true, allowParallel: false, and onFull: "reject" — a full-border diagram surface typically wants unlimited incoming lines and parallel edges, while onFull: "replace-oldest" suits a single-input port that should adopt the newest connection instead of rejecting it.

virtual omits the connector element. The logical ConnectorMirror still belongs to its node, participates in connection policy, and owns its lines. Source surfaces are registered with the engine’s input router, so their hit area may extend beyond the parent node’s DOM box (for example, a 16-pixel rim around a shape). A visible connector under the pointer still wins over a wider virtual surface; otherwise matching surfaces are considered before the node or background starts its ordinary gesture.

The Svelte and React adapters apply connector prop changes to the existing core object. Toggling virtual safely detaches or mounts the visible port without deleting connector topology. Vanilla integrations can update the same runtime options with connector.updateConfig(...); name remains construction-only.

Use the connector’s onPointerDown callback for selection or other UI that must happen as soon as the surface claims the pointer. It receives the connector, normalized position, pointer ID, and original PointerEvent; onDragStart still waits until the drag threshold is crossed.

Hit testing

A ConnectorSurfaceStrategy may provide separate sourceHitTest and targetHitTest callbacks. Callbacks receive the pointer in SnapEngine world, camera, and screen coordinates together with a cached geometry snapshot. They return a projected anchor and a score:

const borderSurface: ConnectorSurfaceStrategy = {
  sourceHitTest: ({ position, geometry }) => hitBorder(position, geometry),
  targetHitTest: ({ position, geometry }) => hitBorder(position, geometry),
  resolveAnchor: ({ geometry, peerGeometry, position }) =>
    boundaryToward(geometry, peerGeometry?.center ?? position),
};

Return null or false for a miss. When several surfaces match, SnapLine sorts by priority, then distance, then falls back to internal object identity order and strategy order for exact ties. This makes overlapping targets deterministic without requiring DOM z-index tricks.

The geometry passed to callbacks is measured during SnapLine’s read phase. Surface callbacks should use that snapshot instead of calling getBoundingClientRect() during pointer movement.

Preview and settled anchors

Hit testing and line attachment are separate:

  • Source and target hits decide whether the pointer is on a valid surface.
  • resolveAnchor projects each endpoint for free previews, target previews, drops, and settled connections.
  • Settled anchors are resolved again whenever either node moves or resizes.

This separation allows a pointer to activate a wide border band while the visible line remains attached to the exact rim of a circle or rectangle.

Your document stays authoritative

Topology is always controlled: your graph document is the single source of truth for which lines exist. Mount ControlledGraph with your LineRecords and apply each gesture’s atomic proposal:

<ControlledGraph
  lines={doc.lines}
  onLineChangeRequest={(request) => applyToDocument(request)}
/>

A gesture-created line carries a SnapLine-minted stable id in request.add[0].id; keeping that id in the record you store settles the dragged line in place — no flicker, same mirror. Records carry an optional payload that lands on LineMirror.payload for custom renderers, and admission rules can inspect the proposed line through rules.isValidConnection({ line, source, target, phase }) on either endpoint.

Restoring a saved graph is just pushing its records — hydration never calls your request handler, so a reload cannot create duplicate application edges. Node and connector metadata remain available for small integration hints; treat metadata and payloads as opaque links, not as a second copy of the application graph.