API reference

Every public class, method, and event in @codexo/exojs. Generated from source.

C

classInteractionSystem

@codexo/exojs / input / stable

Routes pointer events from the InputSystem to interactive scene-graph nodes via DOM-style event bubbling. Maintains a persistent dynamic-AABB-tree spatial index of interactive RenderNodes for hit-testing and updates it incrementally - nodes notify the system via the `_notify*` hooks when they enter/leave the scene, change interactivity, or move (causing bounds to dirty). Dispatches InteractionEvents of every type in InteractionEventType: `pointerdown` / `pointerup` / `pointermove` / `pointerover` / `pointerout` / `pointertap` / `contextmenu` / `dragstart` / `drag` / `dragend`. A `pointerdown` on a draggable node only marks a drag candidate; the drag begins once the pointer travels past `ApplicationOptions.input.dragThreshold`. The `contextmenu` event here only fires when a specific interactive node is actually under the pointer; a request over empty space, or one your handler never stops, still reaches `app.input.onContextMenu` - the engine-wide fallback, unconditional and scene-graph-independent. See that Signal's own doc comment for the full two-tier picture. Hit-testing honors a hard `RenderNode.clip` bound to `null` (the node's own world bounds) or a `Rectangle` clipShape - a descendant outside either is never a hit, matching the renderer's own scissor path. A `Geometry` (stencil) clipShape and `RenderNode.mask` (alpha masking) are NOT accounted for: both affect only what is *painted*, not what is *hit-tested*, and an interactive descendant under either stays hittable across its ancestor's full, unclipped bounds. See isWithinClip's doc comment. Constructed automatically by Application; you do not instantiate this class yourself.

2
props
12
methods
0
events
Import
import { InteractionSystem } from '@codexo/exojs'

Routes pointer events from the InputSystem to interactive scene-graph nodes via DOM-style event bubbling. Maintains a persistent dynamic-AABB-tree spatial index of interactive RenderNodes for hit-testing and updates it incrementally - nodes notify the system via the `_notify*` hooks when they enter/leave the scene, change interactivity, or move (causing bounds to dirty).

Dispatches InteractionEvents of every type in InteractionEventType: `pointerdown` / `pointerup` / `pointermove` / `pointerover` / `pointerout` / `pointertap` / `contextmenu` / `dragstart` / `drag` / `dragend`. A `pointerdown` on a draggable node only marks a drag candidate; the drag begins once the pointer travels past `ApplicationOptions.input.dragThreshold`.

The `contextmenu` event here only fires when a specific interactive node is actually under the pointer; a request over empty space, or one your handler never stops, still reaches `app.input.onContextMenu` - the engine-wide fallback, unconditional and scene-graph-independent. See that Signal's own doc comment for the full two-tier picture.

Hit-testing honors a hard `RenderNode.clip` bound to `null` (the node's own world bounds) or a `Rectangle` clipShape - a descendant outside either is never a hit, matching the renderer's own scissor path. A `Geometry` (stencil) clipShape and `RenderNode.mask` (alpha masking) are NOT accounted for: both affect only what is *painted*, not what is *hit-tested*, and an interactive descendant under either stays hittable across its ancestor's full, unclipped bounds. See isWithinClip's doc comment.

Constructed automatically by Application; you do not instantiate this class yourself.

Constructors1
Methods12
destroy(): void
Move keyboard focus to node. No-op when node is already focused, is not RenderNode.focusable, or is a Widget that is currently disabled. Fires onBlur on the previously focused node, then onFocus on node.
Move focus to the nearest focusable node lying in direction from the currently focused one - what an arrow key or a D-pad press does - or to the first candidate when nothing holds focus yet. Candidates are compared by the centre of their global bounds and navigation does not wrap, so nothing moves at the edge of the layout. No-op while focusNavigation is 'never'.
focusNext(): void
Move focus to the next focusable node of the active scope, in Tab order.
focusPrevious(): void
Move focus to the previous focusable node of the active scope, in Tab order.
Snapshot of nodes that currently have pointer-capture (a pointer pressed inside them and is being dragged). Used internally for drag routing; exposed read-only for diagnostic / debug consumers.
Return the deepest interactive node currently under the given pointer, or under any active pointer when pointerId is omitted (the first pointer with a hit wins). null when no pointer is hovering an interactive node.
The deepest interactive node at a point, or null where the point hits none. x and y are in the same space a pointer event reports - design/screen coordinates, not world ones - and are resolved exactly as an event would be: an active scope wins, otherwise the screen-fixed UI layer is tried before the camera world, and clipping, visibility and hit-test flags all apply. Purely a query. It changes no hover, capture or focus state and emits no events, so it is safe to call for a hovered tooltip at a computed position, an editor's selection, a gamepad-driven cursor, or a test - all cases where synthesising a pointer event would leave the interaction state believing a pointer had moved. For what the user's pointer is actually over, prefer getHoveredNode: it reports the result the last real event already resolved instead of testing again.
popScope(token: object): void
Release the scope token identifies - a targeted removal wherever it sits in the stack, never a rebuild of the entries above or below it (see pushScope). Idempotent: releasing an already-released or unknown token is a no-op, so a caller never needs to track whether it already let go of a scope.
SystemMethods.preFrame phase: dispatch this frame's node-level pointer events, then retire the pointers InputSystem flagged terminal. Registered on app.systems by the Application at SystemOrder.CoreInteraction, directly after InputSystem. The retirement runs in a finally because node-level dispatch above may still reference pointers flagged terminal this flush; only once it has drained is destroying them safe - and it must still happen if a node handler threw. Keeping both halves inside this one phase is what lets the pair be a single system: order alone could not express "B runs after A even when A throws".
Confine interaction to root's subtree until it is released via popScope with the returned token. Pointer events outside the subtree hit nothing, Tab traversal stays inside it, and - since the scope is a real focus trap - so does every programmatic InteractionSystem.focus call; a modal dialog (optionally with a full-screen backdrop to swallow clicks) shields everything beneath it. Scopes stack - the most recently pushed one wins - and nest freely with scopes pushed at any other level (app-wide or scene-scoped alike). A scope is not what makes a node interactive; node.interactive = true alone does that. Nor is it the browser pointer-capture taken during a drag, which is a private implementation detail. Prefer scene.interaction.scope() when the scope should end with its scene.
Properties2
Which nodes the arrow keys and a gamepad D-pad may move focus to. Defaults to 'ui', so directional navigation reaches the UI layer while the game keeps the arrow keys for itself.
Source