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