Skip to main content
Threebox provides built-in interaction capabilities including object selection, dragging, rotation, and raycasting for both 3D objects and fill-extrusion building layers.

Enable Interactions

Configure interaction features when initializing Threebox:
window.tb = new Threebox(
  map,
  mbxContext,
  {
    defaultLights: true,
    enableSelectingObjects: true,      // Click to select 3D objects
    enableSelectingFeatures: true,     // Click to select buildings
    enableDraggingObjects: true,       // Drag objects with [Shift]
    enableRotatingObjects: true,       // Rotate objects with [Alt]
    enableTooltips: true,              // Show tooltips on hover
    enableHelpTooltips: true           // Show help for interactions
  }
);

Interaction Options

OptionTypeDefaultDescription
enableSelectingObjectsbooleanfalseEnable 3D object selection
enableSelectingFeaturesbooleanfalseEnable fill-extrusion selection
enableDraggingObjectsbooleanfalseEnable object dragging
enableRotatingObjectsbooleanfalseEnable object rotation
enableTooltipsbooleanfalseShow default tooltips
enableHelpTooltipsbooleanfalseShow interaction help labels

Object Selection

Click on 3D objects to select them:
map.on('style.load', function() {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, mbxContext) {
      window.tb = new Threebox(
        map,
        mbxContext,
        {
          defaultLights: true,
          enableSelectingObjects: true,
          enableTooltips: true
        }
      );

      // Create selectable sphere
      var sphere = tb.sphere({
        radius: 30,
        color: 'green',
        material: 'MeshPhysicalMaterial',
        units: 'meters'
      }).setCoords([-122.3512, 47.6202, 0]);

      // Add tooltip
      sphere.addTooltip("Click to select", true);

      // Listen for selection changes
      sphere.addEventListener('SelectedChange', function(e) {
        if (e.detail.selected) {
          console.log('Object selected');
          sphere.setColor('yellow');
        } else {
          console.log('Object unselected');
          sphere.setColor('green');
        }
      }, false);

      tb.add(sphere);
    },

    render: function(gl, matrix) {
      tb.update();
    }
  });
});

Dragging Objects

Drag objects to new positions using keyboard modifiers:
  • [Shift] + Drag - Translate horizontally
  • [Ctrl] + Drag - Adjust altitude
window.tb = new Threebox(
  map,
  mbxContext,
  {
    defaultLights: true,
    enableSelectingObjects: true,
    enableDraggingObjects: true,  // Enable dragging
    enableTooltips: true
  }
);

// Create draggable object
var options = {
  obj: 'models/soldier.glb',
  type: 'gltf',
  scale: 100,
  units: 'meters',
  rotation: { x: 90, y: 0, z: 0 },
  anchor: 'center'
};

tb.loadObj(options, function(model) {
  var soldier = model.setCoords([-122.3491, 47.6207, 0]);
  soldier.addTooltip("Shift+Drag to move, Ctrl+Drag for altitude", true);
  
  // Listen to drag events
  soldier.addEventListener('ObjectDragged', function(e) {
    console.log('Dragged action:', e.detail.draggedAction);
    console.log('New position:', e.detail.draggedObject.coordinates);
  }, false);
  
  tb.add(soldier);
});

Altitude Step

Control altitude adjustment sensitivity:
window.tb = new Threebox(
  map,
  mbxContext,
  {
    enableDraggingObjects: true
  }
);

// Set altitude step (default is 5 meters)
tb.altitudeStep = 1;  // 1 meter increments

Rotating Objects

Rotate objects around their vertical axis:
  • [Alt] + Drag - Rotate on vertical axis
window.tb = new Threebox(
  map,
  mbxContext,
  {
    defaultLights: true,
    enableSelectingObjects: true,
    enableRotatingObjects: true,  // Enable rotation
    enableTooltips: true
  }
);

// Create rotatable tube
var tube = tb.tube({
  geometry: spiralPath,
  radius: 0.8,
  sides: 8,
  color: '#00ffff',
  material: 'MeshPhysicalMaterial'
});
tube.setCoords(origin);
tube.addTooltip("Alt+Drag to rotate", true);
tb.add(tube);

Raycasting

Raycasting enables precise object selection and interaction:
map.on('style.load', function() {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, mbxContext) {
      window.tb = new Threebox(
        map,
        mbxContext,
        {
          defaultLights: true,
          enableSelectingFeatures: true,   // Buildings
          enableSelectingObjects: true,    // 3D objects
          enableDraggingObjects: true,
          enableRotatingObjects: true,
          enableTooltips: true
        }
      );

      // Create cube without bounding box (still raycasted)
      var geometry = new THREE.BoxGeometry(30, 60, 120);
      var redMaterial = new THREE.MeshPhongMaterial({
        color: 0x660000,
        side: THREE.DoubleSide
      });
      
      var cube = tb.Object3D({
        obj: new THREE.Mesh(geometry, redMaterial),
        units: 'meters',
        bbox: false  // No bounding box, but still selectable
      }).setCoords([-122.3512, 47.6202, 0]);
      
      cube.addTooltip("Selectable without bounding box", true);
      tb.add(cube);

      // Create sphere
      var sphere = tb.sphere({
        radius: 30,
        units: 'meters',
        sides: 120,
        color: 'green',
        material: 'MeshPhysicalMaterial'
      }).setCoords([-122.34548, 47.617538, 0]);
      tb.add(sphere);

      // Load model
      var options = {
        obj: './models/soldier.glb',
        type: 'gltf',
        scale: 100,
        units: 'meters',
        rotation: { x: 90, y: 0, z: 0 },
        anchor: 'center'
      };

      tb.loadObj(options, function(model) {
        var soldier = model.setCoords([-122.3491, 47.6207, 0]);
        soldier.addTooltip("This is a custom tooltip", true);
        tb.add(soldier);
      });
    },

    render: function(gl, matrix) {
      tb.update();
    }
  });
});

Fill-Extrusion Selection

Select and interact with Mapbox fill-extrusion building layers:
var minZoom = 12;

// Create building layer
function createBuildingLayer() {
  return {
    'id': '3d-buildings',
    'source': 'composite',
    'source-layer': 'building',
    'filter': ['==', 'extrude', 'true'],
    'type': 'fill-extrusion',
    'minzoom': minZoom,
    'paint': {
      'fill-extrusion-color': [
        'case',
        ['boolean', ['feature-state', 'select'], false],
        "lightgreen",  // Selected color
        ['boolean', ['feature-state', 'hover'], false],
        "lightblue",   // Hover color
        '#aaa'          // Default color
      ],
      'fill-extrusion-height': [
        'interpolate',
        ['linear'],
        ['zoom'],
        minZoom,
        0,
        minZoom + 0.05,
        ['get', 'height']
      ],
      'fill-extrusion-base': [
        'interpolate',
        ['linear'],
        ['zoom'],
        minZoom,
        0,
        minZoom + 0.05,
        ['get', 'min_height']
      ],
      'fill-extrusion-opacity': 0.9
    }
  };
}

map.on('style.load', function() {
  // Initialize Threebox with feature selection
  window.tb = new Threebox(
    map,
    map.getCanvas().getContext('webgl'),
    {
      defaultLights: true,
      enableSelectingFeatures: true,  // Enable building selection
      enableTooltips: true
    }
  );

  // Add building layer
  map.addLayer(createBuildingLayer());

  // Listen for feature selection events
  map.on('SelectedFeatureChange', onSelectedFeatureChange);

  // Update Threebox on render
  map.on('render', function() {
    tb.update();
  });
});

function onSelectedFeatureChange(e) {
  var feature = e.detail;
  
  if (feature && feature.state && feature.state.select) {
    console.log('Building selected:', feature.id);
    
    // Get feature center
    var coords = tb.getFeatureCenter(feature, null, 0);
    
    // Show popup
    new mapboxgl.Popup({ offset: 0 })
      .setLngLat([coords[0], coords[1]])
      .setHTML('<strong>' + (feature.id || feature.type) + '</strong>')
      .addTo(map);
    
    // Log GeoJSON
    var geoJson = {
      "geometry": feature.geometry,
      "type": "Feature",
      "properties": feature.properties
    };
    console.log(JSON.stringify(geoJson, null, 2));
  }
}

Mouse Events

Handle mouse interactions on objects:
var sphere = tb.sphere({
  radius: 30,
  color: 'red',
  material: 'MeshToonMaterial'
}).setCoords(origin);

// Mouse over
sphere.addEventListener('ObjectMouseOver', function(e) {
  console.log('Mouse over:', e.detail.name);
  sphere.setColor('yellow');
}, false);

// Mouse out
sphere.addEventListener('ObjectMouseOut', function(e) {
  console.log('Mouse out:', e.detail.name);
  sphere.setColor('red');
}, false);

// Selection change
sphere.addEventListener('SelectedChange', function(e) {
  if (e.detail.selected) {
    console.log('Selected');
  } else {
    console.log('Unselected');
  }
}, false);

tb.add(sphere);

All Interaction Events

tb.loadObj(options, function(model) {
  model.setCoords(origin);
  
  // Selection
  model.addEventListener('SelectedChange', function(e) {
    console.log('Selected:', e.detail.selected);
  }, false);
  
  // Mouse events
  model.addEventListener('ObjectMouseOver', function(e) {
    console.log('Mouse over:', e.detail.name);
  }, false);
  
  model.addEventListener('ObjectMouseOut', function(e) {
    console.log('Mouse out:', e.detail.name);
  }, false);
  
  // Dragging
  model.addEventListener('ObjectDragged', function(e) {
    console.log('Dragged action:', e.detail.draggedAction);
    console.log('Object:', e.detail.draggedObject);
  }, false);
  
  // General changes
  model.addEventListener('ObjectChanged', function(e) {
    console.log('Action:', e.detail.action);
    console.log('Object:', e.detail.object);
  }, false);
  
  // Wireframe toggle
  model.addEventListener('Wireframed', function(e) {
    console.log('Wireframe:', e.detail.wireframe);
  }, false);
  
  // Animation state
  model.addEventListener('IsPlayingChanged', function(e) {
    console.log('Is playing:', e.detail.isPlaying);
  }, false);
  
  tb.add(model);
});

Interactive Controls

Create UI buttons to control selected objects:
<div class="controls">
  <button id="wireButton" disabled>Wireframe</button>
  <button id="playButton" disabled>Play</button>
  <button id="deleteButton" disabled>Delete</button>
</div>

<script>
var selectedObject = null;

// Track selection
model.addEventListener('SelectedChange', function(e) {
  if (e.detail.selected) {
    selectedObject = e.detail;
    // Enable buttons
    document.getElementById('wireButton').disabled = false;
    document.getElementById('playButton').disabled = !e.detail.hasDefaultAnimation;
    document.getElementById('deleteButton').disabled = false;
  } else {
    selectedObject = null;
    // Disable buttons
    document.getElementById('wireButton').disabled = true;
    document.getElementById('playButton').disabled = true;
    document.getElementById('deleteButton').disabled = true;
  }
}, false);

// Wireframe toggle
document.getElementById('wireButton').addEventListener('click', function() {
  if (selectedObject) {
    selectedObject.wireframe = !selectedObject.wireframe;
  }
});

// Play/pause animation
document.getElementById('playButton').addEventListener('click', function() {
  if (selectedObject) {
    if (selectedObject.isPlaying) {
      selectedObject.stop();
    } else {
      selectedObject.playAnimation({ animation: 1, duration: 10000 });
    }
  }
});

// Delete object
document.getElementById('deleteButton').addEventListener('click', function() {
  if (selectedObject) {
    tb.remove(selectedObject);
    selectedObject = null;
  }
});
</script>

Tooltips

Default Tooltips

window.tb = new Threebox(
  map,
  mbxContext,
  {
    enableTooltips: true  // Enable default tooltips
  }
);

var sphere = tb.sphere({
  radius: 30,
  color: 'green'
}).setCoords(origin);

// Add tooltip
sphere.addTooltip("This is a sphere", true);
tb.add(sphere);

Custom Tooltips

function createCustomTooltip(text) {
  var div = document.createElement('div');
  div.className = 'custom-tooltip';
  div.innerHTML = `
    <h3>${text}</h3>
    <p>Click to select</p>
  `;
  return div;
}

model.addLabel(createCustomTooltip("Custom Label"), true);

Help Tooltips

window.tb = new Threebox(
  map,
  mbxContext,
  {
    enableTooltips: true,
    enableHelpTooltips: true  // Show interaction hints
  }
);

// Automatically shows help when:
// - Dragging: "Shift + Drag to move"
// - Altitude: "Ctrl + Drag for altitude"
// - Rotation: "Alt + Drag to rotate"

FOV and Camera Controls

Adjust field of view dynamically:
import { GUI } from 'https://threejs.org/examples/jsm/libs/lil-gui.module.min.js';

var api = {
  fov: Math.atan(3 / 4) * 180 / Math.PI,
  orthographic: false
};

var gui = new GUI();

// FOV slider (2.5 - 45 degrees)
gui.add(api, 'fov', 2.5, 45.0).step(0.1).onChange(function() {
  tb.fov = api.fov;
});

// Orthographic camera toggle
gui.add(api, 'orthographic').name('Orthographic').onChange(function() {
  tb.orthographic = api.orthographic;
  tb.fov = api.fov;
});
FOV values below 2.5 degrees or above 45 degrees can cause rendering issues.

Complete Interactive Example

Here’s a full example with all interaction features:
<!doctype html>
<head>
  <title>Threebox Interactions</title>
  <link href="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.css" rel="stylesheet">
  <script src="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.js"></script>
  <script src="../dist/threebox.js"></script>
  <link href="../dist/threebox.css" rel="stylesheet" />
  <style>
    body, html { width: 100%; height: 100%; margin: 0; }
    #map { width: 100%; height: 100%; }
    .help {
      position: absolute;
      top: 10px;
      left: 10px;
      background: rgba(0,0,0,0.7);
      color: white;
      padding: 10px;
      border-radius: 5px;
      font-size: 12px;
    }
  </style>
</head>
<body>
  <div id='map'></div>
  <div class="help">
    <strong>Interactions:</strong><br>
    Click to select<br>
    Shift + Drag to move<br>
    Ctrl + Drag for altitude<br>
    Alt + Drag to rotate
  </div>

  <script type="module">
    mapboxgl.accessToken = 'YOUR_TOKEN';

    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/outdoors-v11',
      center: [-122.3491, 47.6207],
      zoom: 16.5,
      pitch: 60,
      antialias: true
    });

    map.on('style.load', function() {
      window.tb = new Threebox(
        map,
        map.getCanvas().getContext('webgl'),
        {
          defaultLights: true,
          enableSelectingObjects: true,
          enableDraggingObjects: true,
          enableRotatingObjects: true,
          enableTooltips: true,
          enableHelpTooltips: true
        }
      );

      map.addLayer({
        id: 'custom_layer',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function(map, mbxContext) {
          tb.altitudeStep = 1;

          // Cube
          var geometry = new THREE.BoxGeometry(30, 60, 120);
          var cube = tb.Object3D({
            obj: new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({
              color: 0x660000,
              side: THREE.DoubleSide
            })),
            units: 'meters'
          }).setCoords([-122.3512, 47.6202, 0]);
          cube.addTooltip("Red cube", true);
          tb.add(cube);

          // Sphere
          var sphere = tb.sphere({
            radius: 30,
            units: 'meters',
            color: 'green',
            material: 'MeshPhysicalMaterial'
          }).setCoords([-122.34548, 47.617538, 0]);
          sphere.addTooltip("Green sphere", true);
          tb.add(sphere);

          // Soldier
          var options = {
            obj: './models/soldier.glb',
            type: 'gltf',
            scale: 100,
            units: 'meters',
            rotation: { x: 90, y: 0, z: 0 },
            anchor: 'center'
          };

          tb.loadObj(options, function(model) {
            var soldier = model.setCoords([-122.3491, 47.6207, 0]);
            soldier.addTooltip("Soldier", true);
            
            soldier.addEventListener('SelectedChange', function(e) {
              console.log('Selected:', e.detail.selected);
            }, false);
            
            soldier.addEventListener('ObjectDragged', function(e) {
              console.log('Dragged:', e.detail.draggedAction);
            }, false);
            
            tb.add(soldier);
            soldier.playAnimation({ animation: 1, duration: 10000 });
          });
        },

        render: function(gl, matrix) {
          tb.update();
        }
      });
    });
  </script>
</body>

Next Steps

Examples Overview

Browse all interactive examples

Animations

Combine interactions with animations

3D Models

Load interactive 3D models

API Reference

Complete event API documentation