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
tb . setStyle ( styleId , options ) {
this . clear (). then (() => {
this . map . setStyle ( styleId , options );
});
}
This method:
Clears all 3D objects from tb.world
Disposes resources properly
Changes the map style
Allows re-adding objects on style.load
Basic Usage
Simple Style Change
With Options
// 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:
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 : 10 px ;
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:
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' );