Skip to main content

Overview

Threebox provides the setStyle method to change the Mapbox map style at runtime while properly managing 3D objects and resources.

The Problem

When using standard Mapbox map.setStyle(), all custom layers (including Threebox layers) are removed, but the 3D objects in tb.world remain in memory, causing:
  • Memory leaks
  • Orphaned 3D objects
  • Resource management issues
  • Style inconsistencies

The Solution: tb.setStyle

Threebox.js:869-873
tb.setStyle(styleId, options) {
  this.clear().then(() => {
    this.map.setStyle(styleId, options);
  });
}
This method:
  1. Clears all 3D objects from tb.world
  2. Disposes resources properly
  3. Changes the map style
  4. Allows re-adding objects on style.load

Basic Usage

// Initialize Threebox
window.tb = new Threebox(
  map,
  map.getCanvas().getContext('webgl'),
  { defaultLights: true }
);

// Change style using Threebox method
tb.setStyle('mapbox://styles/mapbox/dark-v10');

Complete Example

mapboxgl.accessToken = 'YOUR_TOKEN';

let map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v11',
  center: [2.294625, 48.861085],
  zoom: 16,
  pitch: 60
});

window.tb = new Threebox(
  map,
  map.getCanvas().getContext('webgl'),
  { defaultLights: true }
);

map.on('style.load', () => {
  map.addLayer({
    id: 'custom-layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function (map, gl) {
      // Load 3D model
      let options = {
        obj: './models/eiffel.glb',
        type: 'gltf',
        scale: { x: 5621, y: 6480, z: 5621 },
        units: 'meters',
        rotation: { x: 90, y: 0, z: 0 }
      };
      
      tb.loadObj(options, function (model) {
        model.setCoords([2.294514, 48.857475, 0]);
        model.setRotation({ x: 0, y: 0, z: 45.7 });
        model.addTooltip("Eiffel Tower", true);
        tb.add(model);
      });
    },
    render: function (gl, matrix) {
      tb.update();
    }
  });
});

// Style switching function
function switchStyle(styleId) {
  tb.setStyle('mapbox://styles/mapbox/' + styleId);
}

// UI controls
document.getElementById('streets').onclick = () => switchStyle('streets-v11');
document.getElementById('dark').onclick = () => switchStyle('dark-v10');
document.getElementById('light').onclick = () => switchStyle('light-v10');
document.getElementById('satellite').onclick = () => switchStyle('satellite-v9');

Style.Load Event

The style.load event fires every time a style loads, including after setStyle calls:
Threebox.js:113-132
this.map.on('style.load', function () {
  this.tb.zoomLayers = [];
  
  // Recreate default layer if multiLayer enabled
  if (this.tb.options.multiLayer) {
    this.addLayer({
      id: "threebox_layer",
      type: 'custom',
      renderingMode: '3d',
      map: this,
      onAdd: function (map, gl) { },
      render: function (gl, matrix) {
        this.map.tb.update();
      }
    });
  }
  
  this.once('idle', () => {
    this.tb.setObjectsScale();
  });
  
  // Handle sky and terrain options
  if (this.tb.options.sky) {
    this.tb.sky = true;
  }
  if (this.tb.options.terrain) {
    this.tb.terrain = true;
  }
});
Objects must be re-added in the style.load event handler after each style change.

Style Switcher UI Example

<!DOCTYPE html>
<html>
<head>
  <title>Style Switcher</title>
  <style>
    #menu {
      position: absolute;
      background: #fff;
      padding: 10px;
      font-family: 'Open Sans', sans-serif;
      z-index: 1;
    }
  </style>
</head>
<body>
  <div id='map'></div>
  <div id="menu">
    <input id="streets-v11" type="radio" name="style" value="streets" checked />
    <label for="streets-v11">Streets</label>
    
    <input id="light-v10" type="radio" name="style" value="light" />
    <label for="light-v10">Light</label>
    
    <input id="dark-v10" type="radio" name="style" value="dark" />
    <label for="dark-v10">Dark</label>
    
    <input id="outdoors-v11" type="radio" name="style" value="outdoors" />
    <label for="outdoors-v11">Outdoors</label>
    
    <input id="satellite-v9" type="radio" name="style" value="satellite" />
    <label for="satellite-v9">Satellite</label>
  </div>
  
  <script>
    let currentStyle = 'streets-v11';
    
    function switchLayer(layer) {
      const newStyle = layer.target.id;
      if (currentStyle !== newStyle) {
        currentStyle = newStyle;
        tb.setStyle('mapbox://styles/mapbox/' + newStyle);
      }
    }
    
    // Attach event listeners
    let styleList = document.getElementById('menu');
    let inputs = styleList.getElementsByTagName('input');
    for (let i = 0; i < inputs.length; i++) {
      inputs[i].onclick = switchLayer;
    }
  </script>
</body>
</html>

Clear Method

The setStyle method uses clear() internally:
Threebox.js:947-971
async tb.clear(layerId = null, dispose = false) {
  return new Promise((resolve, reject) => {
    let objects = [];
    this.world.children.forEach(function (object) {
      objects.push(object);
    });
    
    for (let i = 0; i < objects.length; i++) {
      let obj = objects[i];
      // If layerId specified, check layer; otherwise always remove
      if (obj.layer === layerId || !layerId) {
        this.remove(obj);
      }
    }
    
    if (dispose) {
      this.objectsCache.forEach((value) => {
        value.promise.then(obj => {
          obj.dispose();
          obj = null;
        })
      })
    }
    
    resolve("clear");
  });
}

Best Practices

Always Use tb.setStyle

Never use map.setStyle directly when using Threebox

Reload Objects

Re-add objects in the style.load event handler

Cache Object Options

Store object configuration for easy re-creation

Handle Transitions

Consider user experience during style transitions

Object Persistence Pattern

// Store object configurations
const objectConfigs = [];

function addObject(config) {
  objectConfigs.push(config);
  
  tb.loadObj(config, function (model) {
    model.setCoords(config.coords);
    if (config.rotation) model.setRotation(config.rotation);
    if (config.tooltip) model.addTooltip(config.tooltip, true);
    tb.add(model);
  });
}

function restoreObjects() {
  objectConfigs.forEach(config => {
    tb.loadObj(config, function (model) {
      model.setCoords(config.coords);
      if (config.rotation) model.setRotation(config.rotation);
      if (config.tooltip) model.addTooltip(config.tooltip, true);
      tb.add(model);
    });
  });
}

map.on('style.load', () => {
  map.addLayer({
    id: 'custom-layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function (map, gl) {
      if (objectConfigs.length > 0) {
        restoreObjects();
      } else {
        // Initial load
        addObject({
          type: 'gltf',
          obj: './model.glb',
          coords: [-122.4194, 37.7749],
          rotation: { x: 90, y: 0, z: 0 },
          tooltip: 'My Model'
        });
      }
    },
    render: function (gl, matrix) {
      tb.update();
    }
  });
});

Style Options

The options parameter supports Mapbox style options:
tb.setStyle(styleId, {
  diff: false,        // Force complete reload
  localIdeographFontFamily: false,  // Font handling
  transformStyle: (previous, next) => next  // Custom transform
});
Memory Management: Always use tb.setStyle instead of map.setStyle to prevent memory leaks from orphaned 3D objects.

MultiLayer Compatibility

When using multiLayer: true, the default Threebox layer is automatically recreated:
window.tb = new Threebox(
  map,
  map.getCanvas().getContext('webgl'),
  {
    defaultLights: true,
    multiLayer: true  // Default layer recreated on style change
  }
);

// Style change still requires tb.setStyle
tb.setStyle('mapbox://styles/mapbox/dark-v10');

Build docs developers (and LLMs) love