Uzay

Interactions

Drag, click, and hover events on scene items

Overview

Uzay has a built-in interaction system that lets users drag, click, and hover over items in the scene. Some items (like points and vectors) come with default drag behavior, but you can also attach your own event handlers to any item.

Built-in Dragging

Points and vectors are draggable by default. When a user clicks and drags on a point, it moves in 3D space:

const point = scene.create("point3d", {
  coords: vec3(1, 0, 0),
  color: "red",
});
// That's it, the point is already draggable

Drag Constraints

The draggable property controls which axes an item can be dragged along:

// Free movement in all directions (default)
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "xyz" });

// Only horizontal movement (xz-plane)
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "xz" });

// Only vertical movement (y-axis)
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "y" });

// Not draggable at all
scene.create("point3d", { coords: vec3(0, 0, 0), draggable: "none" });

Available constraints: "x", "y", "z", "xy", "xz", "yz", "xyz", "custom", "none".

Dragging requires a writable atom. If you pass a derived (read-only) atom for the position, dragging is automatically disabled even if draggable is set. The exception is "custom" mode, which skips the default drag behavior entirely.

Custom Drag Behavior

Setting draggable to "custom" tells the engine that you'll handle all drag logic yourself. The engine will still show the grab cursor and fire drag events, but it won't compute a projected world position or update the item's position. You provide your own logic via .on("drag").

This is useful when the built-in axis and plane constraints don't fit your use case, for example snapping to a grid, constraining to a curved surface, or implementing entirely custom movement rules.

const point = scene.create("point3d", {
  coords: vec3(0, 0, 0),
  color: "gold",
  draggable: "custom",
});

// Snap to integer grid positions on the xz-plane
point.on("drag", (event) => {
  if (event.phase === "start") return;

  const pos = event.worldPosition;
  point.coords.set(vec3(
    Math.round(pos.x),
    0,
    Math.round(pos.z),
  ));
});

In "custom" mode, event.worldPosition and event.delta are not updated by the engine (since there's no projection to compute them from). Use event.ray if you need to project the pointer into 3D space yourself. The ray contains the camera position (origin) and the direction through the pointer (direction), and it updates every frame as the user drags.

Event Handlers

You can listen for drag, click, and hover events on any item using .on():

You can only have a single handler function attached to an event type. Adding a new handler to the same event type with .on() will override the previous handler.

Drag Events

const point = scene.create("point3d", {
  coords: vec3(0, 0, 0),
  color: "gold",
});

point.on("drag", (event) => {
  console.log(event.phase);         // "start", "move", or "end"
  console.log(event.worldPosition); // Current 3D position of the pointer
  console.log(event.delta);         // Movement since last event
});

Drag events have three phases:

  • "start": fired once when the user begins dragging
  • "move": fired continuously as the user drags
  • "end": fired once when the user releases

Click Events

const sphere = scene.create("sphere3d", {
  center: vec3(0, 0, 0),
  radius: 1,
  color: "tomato",
});

sphere.on("click", (event) => {
  console.log(event.worldPosition); // Where the click hit in 3D
  console.log(event.screenPosition); // Pixel coordinates on the canvas
});

Hover Events

const sphere = scene.create("sphere3d", {
  center: vec3(0, 0, 0),
  radius: 1,
  color: "skyblue",
});

sphere.on("hover", (event) => {
  if (event.phase === "enter") {
    sphere.color.set("yellow");
  } else if (event.phase === "leave") {
    sphere.color.set("skyblue");
  }
});

Hover events have three phases:

  • "enter": pointer enters the item
  • "move": pointer moves while still over the item
  • "leave": pointer leaves the item

Removing Handlers

Use .off() to remove a handler:

point.on("click", (event) => { /* ... */ });

// Later
point.off("click");

Event Properties

All events include these common fields:

FieldTypeDescription
itemIdstringID of the item that received the event
itemKindstringType of the item (e.g. "point3d")
worldPositionVec33D position where the event occurred
screenPositionVec2Pixel coordinates on the canvas
ray{ origin: Vec3, direction: Vec3 }Camera ray through the pointer

Drag events additionally include:

FieldTypeDescription
phase"start" | "move" | "end"Current drag phase
startWorldPositionVec3Where the drag began
deltaVec3Movement since last event

Pointer Events

Every item has a pointerEvents property that controls whether it participates in the interaction system:

// This grid won't block clicks on items behind it
scene.create("grid3d", {
  pointerEvents: "none",
});

// This sphere will receive clicks and hovers
scene.create("sphere3d", {
  center: vec3(0, 0, 0),
  pointerEvents: "auto", // This is the default for most items
});
  • "auto": the item can be clicked, hovered, and dragged (default)
  • "none": the item is invisible to the pointer and events pass through it

This is useful for items that you don't want intercepting mouse events meant for the items behind them.

On this page