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()(seeInteractionHandlerService) - 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.
tsclass InteractionController
Methods
setHitSlop(world: number): void— Grow every hit target byworldunits (0 = mouse-precision, the default).getHitSlop(): numbersetLinkHitAreaWidth(width: number): voidconstructor()dispose(): void— Dispose and cleanup resourcesgetPerformanceMetrics()— Get performance metricsinvalidatePortHitCache(): 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 debugginghandleConnectionDrag( worldX: number, worldY: number, engine: DiagramEngine ): boolean— Handle connection drag update Enhanced with performance monitoring and validation Updates connection preview during dragstartConnection(port: PortModel, worldX: number, worldY: number, engine: DiagramEngine): void— Start connection from port Enhanced with validation and error handling CRITICAL FIX: Added detailed loggingstartNodeBodyConnection( 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 handlingcancelConnection(engine: DiagramEngine): void— Cancel connectionstartLinkReconnection( 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— IscandidatePorta legal target for reconnectingendpointoflink? 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 atposition.
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 labellabelIndexoflink.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 otherwisedeleteSelectedLink(engine: DiagramEngine): boolean— Delete selected linkgetLinkAtPosition(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 stategetLinkHitAtPosition( 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 stateisInteracting(): boolean— Check if currently interactinggetCursor(engine: DiagramEngine): string— Get appropriate cursor for current statefindLinkHitAtPosition( worldX: number, worldY: number, diagram: any ): LinkPartHit | nullhitTestWaypoint(mouseX: number, mouseY: number, link: LinkModel): number | null— Hit test for waypoint handle at mouse position Returns the waypoint index if hit, null otherwisehitTestPath(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 waypointmoveWaypoint(worldX: number, worldY: number, engine: DiagramEngine): boolean— Move waypoint during drag The waypoint is just moved, orthogonal routing happens during renderingendWaypointDrag(engine?: DiagramEngine): void— End waypoint dragaddWaypoint(clickX: number, clickY: number, link: LinkModel): boolean— Add waypoint at click position on pathremoveWaypoint(waypointIndex: number, link: LinkModel): boolean— Remove waypoint at indexupdateWaypointEditorConfig(config: Partial<any>): void— Update waypoint editor configurationsyncWithEngineConfig(engine: DiagramEngine): void— Synchronize editor configs with engine interaction config ADDED: Call this when engine config changes to update editor visualsupdateHoveredWaypoint(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 cursordeleteHoveredWaypoint(): 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 otherwisestartControlPointDrag( segmentIndex: number, controlType: 'control1' | 'control2', link: LinkModel ): void— Start dragging a control pointmoveControlPoint(worldX: number, worldY: number, engine: DiagramEngine): boolean— Move control point during dragendControlPointDrag(): void— End control point dragupdateControlPointEditorConfig(config: Partial<any>): void— Update control point editor configurationupdateHoveredControlPoint(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 cursorresetHoveredControlPoint(): 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?