Skip to content
D
Documentation

InteractionController

reference
5 min readUpdated

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

Was this page helpful?

InteractionController — Grafloria