# InteractionController

Import it from `@grafloria/renderer`.

InteractionController — the framework-agnostic interaction brain.

Owns all pointer/keyboard-driven interaction LOGIC for a diagram: hover
detection, port connection dragging, link endpoint reconnection, inline label
repositioning, and waypoint / control-point editing.

## Framework contract

This class answers **"WHAT changed?"** — never **"who should re-render?"**. Every handler returns a boolean (or a value) telling the host whether a
re-render is warranted; the host framework decides how to act on that:

- Angular  → `cdr.markForCheck()`   (see `InteractionHandlerService`)
- React    → `setState` / `useSyncExternalStore`
- Vue      → touch a `ref`
- Vanilla  → call your own `render()`

It therefore has **zero framework imports** (no Angular, no DOM beyond the
ambient `performance.now()` clock) and is instantiated with a plain `new`. It takes WORLD coordinates only: converting client/screen pixels into world
space is the job of {@link ViewportController }, so this class never touches
an event, an element, or a bounding rect.

Supports three interaction modes (driven by the engine's interaction config):
- DIRECT: Drag node body to move, drag port to connect
- DELIBERATE: Select node first, then drag to move
- SMART: Visio-style with hover-based port visibility

The Angular `InteractionHandlerService` is a thin `@Injectable` subclass of
this class and adds no behaviour of its own.

```ts
class InteractionController
```

**Methods**

- `setHitSlop(world: number): void` — Grow every hit target by `world` units (0 = mouse-precision, the default).
- `getHitSlop(): number`
- `setLinkHitAreaWidth(width: number): void`
- `constructor()`
- `dispose(): void` — Dispose and cleanup resources
- `getPerformanceMetrics()` — Get performance metrics
- `invalidatePortHitCache(): void` — Invalidate port hit cache (call when nodes move or ports change)
- `handleMouseMove( worldX: number, worldY: number, engine: DiagramEngine ): boolean` — Handle mouse move for hover detection
Enhanced with performance monitoring and validation
Updates hover states for nodes, ports, and links
CRITICAL FIX: Added comprehensive debugging
- `handleConnectionDrag( worldX: number, worldY: number, engine: DiagramEngine ): boolean` — Handle connection drag update
Enhanced with performance monitoring and validation
Updates connection preview during drag
- `startConnection(port: PortModel, worldX: number, worldY: number, engine: DiagramEngine): void` — Start connection from port
Enhanced with validation and error handling
CRITICAL FIX: Added detailed logging
- `startNodeBodyConnection( node: NodeModel, worldX: number, worldY: number, engine: DiagramEngine ): boolean` — Start a connection from a
node BODY, not a port glyph. Picks the source port nearest the press point
(so a drag off the right side starts from the right port) and begins the
normal connection drag from it. Returns false when the node has no port to
start from. The whole gesture then flows through the existing connection
pipeline (preview on move, {@link completeConnection} on drop).
- `completeConnection(engine: DiagramEngine): boolean` — Complete connection to target port
Enhanced with validation and error handling
- `cancelConnection(engine: DiagramEngine): void` — Cancel connection
- `startLinkReconnection( link: LinkModel, endpoint: 'source' | 'target', worldX: number, worldY: number, engine: DiagramEngine ): void` — Start link reconnection.

Enters endpoint-drag mode: the dragged endpoint follows the cursor while
the OTHER endpoint stays put. Seeds the engine's {@link ReconnectionPreview }
so the renderer can draw a ghost link, and primes port validity highlights.
- `updateLinkReconnection(worldX: number, worldY: number, engine: DiagramEngine): boolean` — Update the in-progress endpoint reconnection as the cursor moves.

Refreshes the ghost-preview endpoint, recomputes which ports are valid drop
targets (highlighting them), and reflects whether the currently hovered
port would be accepted. Returns true when a re-render is warranted.
- `isValidReconnectionTarget( link: LinkModel, endpoint: 'source' | 'target', candidatePort: PortModel, engine: DiagramEngine ): boolean` — Is `candidatePort` a legal target for reconnecting `endpoint` of
`link`? The OTHER endpoint's port stays fixed; the candidate must differ
from it, live on a different node, be type-compatible (input↔output, or a
bidirectional port), and satisfy the connection-group rules. Pure w.r.t.
the passed models — the core of the reconnect-validation tests.
- `completeLinkReconnection(engine: DiagramEngine): boolean` — Complete link reconnection.

Drops the dragged endpoint on the hovered port. Rejects (and restores the
original connection) when there is no port under the cursor or the port
fails {@link isValidReconnectionTarget}.
- `cancelLinkReconnection(engine: DiagramEngine): void` — Cancel an in-progress endpoint reconnection, restoring the link to
its original connection. Safe to call when not reconnecting.
- `computeLabelDragUpdate( link: LinkModel, worldPoint: Point ): { position: number; offset: Point } | null` — Map a dragged world point to a model-space label placement.

Returns the `{ position, offset }` to store on the label such that the
renderer draws it exactly under the cursor now AND it sticks to the same
fraction of the path after re-routing:
  - `position` (0-1) = closest point on the path to the cursor;
  - `offset`         = cursor − on-path anchor at `position`.

The offset is measured against the SAME anchor the renderer uses
({@link LinkModel.getPointAtPosition}), so `getPointAtPosition(position) +
offset` reproduces the cursor by construction. Falls back to a direct
polyline projection over `link.points` when the model has no segments
(renderers sync `points` but leave `segments` stale). Returns null when the
link has no drawable path.
- `startLabelDrag(link: LinkModel, labelIndex: number): void` — Begin dragging label `labelIndex` of `link`.
- `moveLabelDrag(worldX: number, worldY: number): boolean` — Move the dragging label to follow the cursor. Writes the remapped
`{ position, offset }` back onto the model so the label survives re-routing. Returns true when a re-render is warranted.
- `endLabelDrag(): void` — End the label drag.
- `selectLink(link: LinkModel, engine: DiagramEngine, multiSelect: boolean = false): void` — Handle link selection
FIXED: Support multi-select with Ctrl key, deselect other links otherwise
- `deleteSelectedLink(engine: DiagramEngine): boolean` — Delete selected link
- `getLinkAtPosition(worldX: number, worldY: number, engine: DiagramEngine): LinkModel | null` — Find link at world position (public wrapper for hit testing)
Used for direct link selection without requiring hover state
- `getLinkHitAtPosition( worldX: number, worldY: number, engine: DiagramEngine ): LinkPartHit | null` — Part-aware public wrapper: like {@link getLinkAtPosition} but also reports
WHICH sub-part of the link was hit (body / label / endpoint / arrow) plus
local info. Foundation for later edge cards (label editing, endpoint
reconnection, edge toolbar placement).
- `getState()` — Get current interaction state
- `isInteracting(): boolean` — Check if currently interacting
- `getCursor(engine: DiagramEngine): string` — Get appropriate cursor for current state
- `findLinkHitAtPosition( worldX: number, worldY: number, diagram: any ): LinkPartHit | null`
- `hitTestWaypoint(mouseX: number, mouseY: number, link: LinkModel): number | null` — Hit test for waypoint handle at mouse position
Returns the waypoint index if hit, null otherwise
- `hitTestPath(mouseX: number, mouseY: number, link: LinkModel): boolean` — Hit test for clicking on link path (to add waypoint)
- `startWaypointDrag(waypointIndex: number, link: LinkModel): void` — Start dragging a waypoint
- `moveWaypoint(worldX: number, worldY: number, engine: DiagramEngine): boolean` — Move waypoint during drag
The waypoint is just moved, orthogonal routing happens during rendering
- `endWaypointDrag(engine?: DiagramEngine): void` — End waypoint drag
- `addWaypoint(clickX: number, clickY: number, link: LinkModel): boolean` — Add waypoint at click position on path
- `removeWaypoint(waypointIndex: number, link: LinkModel): boolean` — Remove waypoint at index
- `updateWaypointEditorConfig(config: Partial<any>): void` — Update waypoint editor configuration
- `syncWithEngineConfig(engine: DiagramEngine): void` — Synchronize editor configs with engine interaction config
ADDED: Call this when engine config changes to update editor visuals
- `updateHoveredWaypoint(worldX: number, worldY: number, link: LinkModel | null): void` — Update hovered waypoint (for Delete key support)
Call this from mousemove to track which waypoint is under cursor
- `deleteHoveredWaypoint(): boolean` — Delete currently hovered waypoint (for Delete key)
- `hitTestControlPoint( mouseX: number, mouseY: number, link: LinkModel ): { segmentIndex: number; controlType: 'control1' | 'control2' } | null` — Hit test for control point handle at mouse position
Returns the control point info if hit, null otherwise
- `startControlPointDrag( segmentIndex: number, controlType: 'control1' | 'control2', link: LinkModel ): void` — Start dragging a control point
- `moveControlPoint(worldX: number, worldY: number, engine: DiagramEngine): boolean` — Move control point during drag
- `endControlPointDrag(): void` — End control point drag
- `updateControlPointEditorConfig(config: Partial<any>): void` — Update control point editor configuration
- `updateHoveredControlPoint(worldX: number, worldY: number, link: LinkModel | null): void` — Update hovered control point (for Delete key support)
Call this from mousemove to track which control point is under cursor
- `resetHoveredControlPoint(): boolean` — Reset control point to auto-generated position (for Delete key)
This removes custom control point adjustment, reverting to default bezier
