Shape surfaces and application-owned graphs
On this page
Ordinary SnapLine connectors are visible ports with circular hit areas and center anchors. A custom-root connector keeps the same connection lifecycle while letting the application render the source input shape, customize target admission, and decide where a line attaches.
Use an SVG path with native stroke hit testing when a box, circle, or other shape should act as its own port:
<svg viewBox="0 0 160 80">
<Connector
id="node-1:surface"
name="surface"
rules={{
maxOutgoing: "unlimited",
maxIncoming: "unlimited",
reconnect: false,
allowParallel: true
}}
colliderRadius={100}
{surfaceStrategies}
>
{#snippet children(bindConnector)}
<path
use:bindConnector
d={borderPath}
fill="none"
stroke="transparent"
stroke-width="16"
pointer-events="stroke"
/>
{/snippet}
</Connector>
</svg> 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.
The snippet action binds that path to the logical ConnectorMirror. SnapEngine
then uses ordinary DOM event targeting and pointer capture for the source
gesture; there is no separate headless input route. Make a transparent stroke
wide enough for the desired hit area, or bind any other HTML/SVG element whose
native pointer area matches the interaction.
React’s equivalent is a render function:
<svg viewBox="0 0 160 80">
<Connector
name="surface"
colliderRadius={100}
surfaceStrategies={surfaceStrategies}
>
{({ ref }) => (
<path
ref={ref}
d={borderPath}
fill="none"
stroke="transparent"
strokeWidth={16}
pointerEvents="stroke"
/>
)}
</Connector>
</svg> The Svelte and React adapters apply connector prop changes to the existing
core object. Vanilla integrations can update the same runtime options with connector.updateConfig(...) and bind an HTML or SVG root with connector.bindElement(...); 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 targetHitTest and resolveAnchor.
Target 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 = {
targetHitTest: ({ position, geometry }) => hitBorder(position, geometry),
resolveAnchor: ({ geometry, peerGeometry, position }) =>
boundaryToward(geometry, peerGeometry?.center ?? position),
}; Target discovery first asks collisionEngine.queryPoint() for every connector
collider under the pointer, in collider registration order. Set colliderRadius large enough to enclose a custom target shape. targetHitTest then applies the connector-specific narrow policy; when at
least one target hit test is configured, its hits replace the default
center candidate. Return null or false for a miss. When several targets
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
Source targeting, target snapping, and line attachment are separate:
- The bound DOM root decides whether a source gesture starts.
- The connector collider and optional target hit test decide whether a target is under the pointer.
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
onLineChangeRequest={(request) => (doc.lines = applyLineChange(doc.lines, request))}
/> The handler returns the list that should now be canonical; the bridge adopts it directly.
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.