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