Skip to main content

Overview

Threadbox uses Three.js Raycaster to detect which 3D objects are under the mouse cursor. This enables object selection, hover effects, and precise interaction with 3D models on the map.

How Raycasting Works

Raycasting shoots an invisible ray from the camera through the mouse position to detect intersections with 3D objects:
// Threebox handles this internally
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePosition, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
Threadbox automatically initializes and manages the raycaster when you enable object interactions.

Query Rendered Features

The main method for raycasting in Threebox:
const intersects = tb.queryRenderedFeatures(point);

Parameters

ParameterTypeDescription
point{x: number, y: number}Screen coordinates (pixels)

Returns

Array of intersection objects, ordered by distance (closest first):
[
  {
    distance: 45.2,        // Distance from camera
    point: Vector3,        // 3D intersection point
    object: Mesh,          // Intersected mesh
    face: Face3,           // Intersected face
    faceIndex: 342,        // Face index
    uv: Vector2            // UV coordinates at intersection
  },
  // ... more intersections
]

Basic Usage

Getting Objects Under Mouse

map.on('click', (e) => {
  const intersects = tb.queryRenderedFeatures(e.point);
  
  if (intersects.length > 0) {
    const nearestMesh = intersects[0];
    console.log('Clicked on:', nearestMesh.object.name);
    console.log('Distance:', nearestMesh.distance);
    console.log('3D point:', nearestMesh.point);
  }
});

Finding Parent Object

Meshes are often children of the main object. Find the parent:
map.on('click', (e) => {
  const intersects = tb.queryRenderedFeatures(e.point);
  
  if (intersects.length > 0) {
    const parentObject = tb.findParent3DObject(intersects[0]);
    
    if (parentObject) {
      console.log('Parent object:', parentObject.name);
      console.log('UUID:', parentObject.uuid);
      parentObject.selected = true;
    }
  }
});

Raycaster Configuration

Threadbox initializes the raycaster with default settings:
this.raycaster = new THREE.Raycaster();
this.raycaster.layers.set(0); // Only raycast layer 0

Custom Threshold

Adjust picking precision for lines and points:
// After creating Threebox instance
tb.raycaster.params.Line.threshold = 0.5;    // Line picking tolerance
tb.raycaster.params.Points.threshold = 0.2;  // Point picking tolerance

Layer Control

Control which objects can be raycasted using Three.js layers:
// Disable raycasting for specific object
model.traverse((child) => {
  if (child.isMesh) {
    child.layers.disable(0); // Remove from raycast layer
    child.layers.enable(1);  // Add to non-raycast layer
  }
});

// Re-enable raycasting
model.traverse((child) => {
  if (child.isMesh) {
    child.layers.enable(0);
    child.layers.disable(1);
  }
});

Raycasted Property

Threadbox provides a convenient property:
// Disable raycasting
model.raycasted = false;

// Enable raycasting
model.raycasted = true;

// Check state
if (model.raycasted) {
  console.log('Object can be selected');
}

Screen to World Coordinates

Convert screen coordinates to 3D world position:
map.on('mousemove', (e) => {
  // Screen coordinates
  const screenX = e.point.x;
  const screenY = e.point.y;
  
  // Convert to normalized device coordinates (-1 to +1)
  const mouse = new THREE.Vector2();
  mouse.x = (screenX / map.transform.width) * 2 - 1;
  mouse.y = 1 - (screenY / map.transform.height) * 2;
  
  // Cast ray and get intersections
  tb.raycaster.setFromCamera(mouse, tb.camera);
  const intersects = tb.raycaster.intersectObjects(tb.world.children, true);
  
  if (intersects.length > 0) {
    const worldPos = intersects[0].point;
    console.log('World position:', worldPos);
  }
});

Recursive Raycasting

By default, Threebox raycasts through object hierarchies:
// Raycast recursively through all children
const intersects = tb.raycaster.intersectObjects(
  tb.world.children,
  true  // recursive = true
);

Intersection Details

Distance

Distance from camera to intersection:
const intersects = tb.queryRenderedFeatures(point);

if (intersects.length > 0) {
  const distance = intersects[0].distance;
  console.log('Object is', distance, 'units from camera');
}

Intersection Point

Exact 3D coordinates where ray hit the object:
const intersectionPoint = intersects[0].point;
console.log('Hit at:', intersectionPoint.x, intersectionPoint.y, intersectionPoint.z);

// Convert to lng/lat
const lngLat = tb.unprojectFromWorld(intersectionPoint);
console.log('Lng/Lat:', lngLat);

Face Information

Details about the intersected triangle:
const intersection = intersects[0];
console.log('Face index:', intersection.faceIndex);
console.log('Face normal:', intersection.face.normal);

Practical Examples

Hover Highlighting

let hoveredObject = null;

map.on('mousemove', (e) => {
  const intersects = tb.queryRenderedFeatures(e.point);
  
  // Remove previous highlight
  if (hoveredObject) {
    hoveredObject.over = false;
    hoveredObject = null;
  }
  
  // Highlight new object
  if (intersects.length > 0) {
    const object = tb.findParent3DObject(intersects[0]);
    if (object) {
      object.over = true;
      hoveredObject = object;
      map.getCanvasContainer().style.cursor = 'pointer';
    }
  } else {
    map.getCanvasContainer().style.cursor = 'default';
  }
});

Measuring Distance

let firstPoint = null;

map.on('click', (e) => {
  const intersects = tb.queryRenderedFeatures(e.point);
  
  if (intersects.length > 0) {
    const point = intersects[0].point;
    
    if (!firstPoint) {
      firstPoint = point;
      console.log('First point selected');
    } else {
      const distance = firstPoint.distanceTo(point);
      console.log('Distance:', distance, 'meters');
      firstPoint = null;
    }
  }
});

Click-to-Place Objects

map.on('click', (e) => {
  const intersects = tb.queryRenderedFeatures(e.point);
  
  if (intersects.length > 0) {
    const worldPos = intersects[0].point;
    const lngLat = tb.unprojectFromWorld(worldPos);
    
    // Place new object at clicked location
    tb.loadObj({
      obj: '/models/marker.glb',
      type: 'gltf',
      scale: 1,
      units: 'meters'
    }, (marker) => {
      marker.setCoords([lngLat[0], lngLat[1], worldPos.z]);
      tb.add(marker);
    });
  }
});

Object Information Panel

map.on('click', (e) => {
  const intersects = tb.queryRenderedFeatures(e.point);
  
  if (intersects.length > 0) {
    const object = tb.findParent3DObject(intersects[0]);
    
    if (object && object.userData.feature) {
      const props = object.userData.feature.properties;
      
      // Display info panel
      document.getElementById('panel').innerHTML = `
        <h3>${props.name || 'Unnamed Object'}</h3>
        <p>Type: ${props.type}</p>
        <p>Height: ${props.height || 0}m</p>
        <p>Coordinates: ${object.coordinates}</p>
      `;
      document.getElementById('panel').style.display = 'block';
    }
  } else {
    document.getElementById('panel').style.display = 'none';
  }
});

Performance Optimization

Limit Raycasted Objects

Use raycasted: false for decorative objects that don’t need interaction

Use Layers

Organize objects into layers and only raycast interactive layers

Throttle Events

Throttle mousemove events to reduce raycasting frequency

Bounding Boxes

Use simplified bounding box geometry for complex models

Throttle Raycasting

let lastRaycast = 0;
const raycastDelay = 50; // ms

map.on('mousemove', (e) => {
  const now = Date.now();
  if (now - lastRaycast < raycastDelay) return;
  lastRaycast = now;
  
  const intersects = tb.queryRenderedFeatures(e.point);
  // ... handle intersections
});

Common Issues

  • Verify enableSelectingObjects: true in Threebox options
  • Check that objects are on layer 0: model.raycasted = true
  • Ensure objects are visible: model.visible = true
  • Check object has meshes: model.traverse(c => console.log(c.type))
  • Use tb.findParent3DObject() to get the main object instead of child mesh
  • Check object ordering in scene (closer objects should be selected first)
  • Disable raycasting for non-interactive objects
  • Throttle mousemove event handlers
  • Use simplified collision geometry
  • Limit recursive raycasting depth

See Also

Build docs developers (and LLMs) love