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
Parameter Type Description 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 );
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 );
});
}
});
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' ;
}
});
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
Objects not being detected
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)
See Also