# SnapController

Import it from `@grafloria/renderer`.

```ts
class SnapController
```

**Methods**

- `constructor(config: Partial<SnapConfig> = {})`
- `getConfig(): SnapConfig`
- `updateConfig(patch: Partial<SnapConfig>): void`
- `syncWithEngineConfig(engine: DiagramEngine): void` — Read the snapping knobs off the engine's interaction config.

`snapToPortRadius` has existed in `InteractionConfig` since the first
interaction phase and was read by NOTHING — the config panel wrote it, the
default was 30, and no code path ever consulted it. This is the seam that
finally consumes it (see {@link findPortMagnet} / {@link findProximityConnection}),
along with the waypoint editor's grid settings.
- `computeSnap(box: Rectangle, others: Rectangle[]): SnapResult` — Snap `box` against `others`.

Precedence per axis (strongest first):
  1. alignment snapline (edge/centre) within `snapThreshold`
  2. equal-spacing guide within `snapThreshold`
  3. grid
then keep-in-bounds clamps whatever survived. Precedence matters: a grid snap
that fought an alignment snap would visibly jitter between the two.
- `snapPointToGrid(point: Point): Point` — Quantise a single point to the grid (used by the vertex drag).
- `keepInBounds(box: Rectangle, bounds: Rectangle): Rectangle` — Clamp a box so it stays fully inside `bounds` (no-op when it cannot fit).
- `siblingBoxes(engine: DiagramEngine, excludeIds: Iterable<string> = []): Rectangle[]` — World bounding boxes of every node EXCEPT `excludeIds` (the moving ones).
- `portPosition(node: NodeModel, port: PortModel): Point` — World position of a port — THE one the port is actually drawn at.

BUG: this used `port.getAbsolutePosition(node.getBoundingBox())`,
which walks the BOUNDING BOX and lands on an edge midpoint — blind to the
node's silhouette and to how many ports share the side. The renderer draws
ports with `getPortPositionForShape`. So on a circle, a diamond, a hexagon,
a cylinder — or ANY side carrying more than one port — the magnet was
snapping to a point several pixels away from the port you could see, and
proximity-connect measured its radius from the wrong place.
- `findPortMagnet( engine: DiagramEngine, worldX: number, worldY: number, options: { excludeNodeId?: string; radius?: number; filter?: (port: PortModel, node: NodeModel) => boolean; } = {} ): PortHit | null` — The nearest port to a world point within `snapToPortRadius` — the magnet the
connection drag snaps to. `excludeNodeId` keeps a drag from snapping back to
the node it started on.
- `findProximityConnection( engine: DiagramEngine, nodeId: string, radius?: number ): ProximityCandidate | null` — React-Flow-style proximity connect: after dragging `nodeId`, is one of ITS
ports close enough to a compatible port on another node to auto-link them?

Validity is the same rule the connection drag enforces: different nodes,
direction-compatible ports (output→input, or a `bi` port on either side),
allowed by the connection groups, and not already connected.
- `buildProximityLinkCommand(candidate: ProximityCandidate): Command` — Turn a candidate into the ONE undoable command that creates the link.
- `highlightProximityTarget(engine: DiagramEngine, candidate: ProximityCandidate | null): void` — Paint the nearest valid target: exactly the flags the SVG renderer already
draws for a connection drag (`isValidTarget` = eligible, `isHighlighted` =
the one that would win). Clears every other port so the highlight can never
go stale — the failure mode of "highlight on, nothing turns it off".
