Edit model in custom editor
This example shows how to create a simple 3D editor for 3D models. You can adjust the model’s position, altitude, scale, and rotation programmatically or via interactive controls and drag-and-drop.
For editing models, we recommend using the 3D Editor in most cases. This is a demonstration of the SDK’s capabilities for very specific scenarios where you need to integrate an editor into your application.
Key features
- Dynamic loading: Initialize the splat model with properties from a JSON object (simulating a database record).
- Interactive editing: Drag the model to reposition it, and use sliders to fine-tune altitude, scale, and rotation.
- State management: Capture the updated properties to “save” them back to your backend.
NPM module setup
npm install --save @maptiler/geosplats
import { SplatModel, Map, config } from "@maptiler/geosplats";
import '@maptiler/geosplats/dist/maptiler-geosplats.css';
config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
// Simulated data from a database
const modelRecord = {
id: "splat_001",
model: "YOUR_MAPTILER_SPLAT_MODEL_ID_HERE",
position: { lng: -1.5722732333707086, lat: 53.77795104082164 },
altitude: 40.8,
scale: 0.034,
rotation: { xRot: 0, yRot: 26, zRot: 180 }
};
const map = new Map({
apiKey: config.apiKey,
container: "map",
center: [modelRecord.position.lng, modelRecord.position.lat],
});
let splatModel;
map.on("load", () => {
splatModel = new SplatModel({
model: modelRecord.model,
center: [modelRecord.position.lng, modelRecord.position.lat],
draggable: true,
});
map.addSplatModel(splatModel);
// Initialize UI values
updateUI();
});
// --- Editor Logic ---
const sliders = {
alt: document.getElementById('alt-slider'),
scale: document.getElementById('scale-slider'),
rotX: document.getElementById('rot-x-slider'),
rotY: document.getElementById('rot-y-slider'),
rotZ: document.getElementById('rot-z-slider'),
};
const displays = {
alt: document.getElementById('alt-val'),
scale: document.getElementById('scale-val'),
rotX: document.getElementById('rot-x-val'),
rotY: document.getElementById('rot-y-val'),
rotZ: document.getElementById('rot-z-val'),
};
function updateUI() {
if (!splatModel) return;
const alt = splatModel.getAltitude();
const scale = splatModel.getScale() || 1;
const rot = splatModel.getRotationDegrees() || { xRot: 0, yRot: 0, zRot: 0 };
sliders.alt.value = alt;
displays.alt.innerText = `${alt.toFixed(1)}m`;
sliders.scale.value = scale;
displays.scale.innerText = `${scale.toFixed(3)}x`;
sliders.rotX.value = rot.xRot;
displays.rotX.innerText = `${rot.xRot.toFixed(0)}°`;
sliders.rotY.value = rot.yRot;
displays.rotY.innerText = `${rot.yRot.toFixed(0)}°`;
sliders.rotZ.value = rot.zRot;
displays.rotZ.innerText = `${rot.zRot.toFixed(0)}°`;
}
function updateModelInitialLoad() {
if (!splatModel) return;
const alt = modelRecord.altitude;
sliders.alt.min = alt - 200;
sliders.alt.max = alt + 200;
splatModel.setAltitude(alt);
splatModel.setScale(modelRecord.scale);
splatModel.setRotationDegree(modelRecord.rotation);
splatModel.setBoundingBox(true, '#f2da1f');
}
// Input listeners
sliders.alt.oninput = (e) => {
const val = parseFloat(e.target.value);
splatModel.setAltitude(val);
displays.alt.innerText = `${val.toFixed(1)}m`;
};
sliders.scale.oninput = (e) => {
const val = parseFloat(e.target.value);
splatModel.setScale(val);
displays.scale.innerText = `${val.toFixed(3)}x`;
};
const updateRotation = () => {
const x = parseFloat(sliders.rotX.value);
const y = parseFloat(sliders.rotY.value);
const z = parseFloat(sliders.rotZ.value);
splatModel.setRotationDegree({ xRot: x, yRot: y, zRot: z });
displays.rotX.innerText = `${x.toFixed(0)}°`;
displays.rotY.innerText = `${y.toFixed(0)}°`;
displays.rotZ.innerText = `${z.toFixed(0)}°`;
};
sliders.rotX.oninput = updateRotation;
sliders.rotY.oninput = updateRotation;
sliders.rotZ.oninput = updateRotation;
// Toggle UI
document.getElementById('ui-toggle').onclick = () => {
document.getElementById('editor-ui').classList.toggle('collapsed');
};
// Map events
map.on("splatmodel:load", (e) => {
updateModelInitialLoad();
updateUI();
});
map.on("splatmodel:update", (e) => {
// Sync UI if changed by dragging (e.g. altitude might change depending on terrain)
updateUI();
});
document.getElementById('save-btn').onclick = () => {
const config = {
position: splatModel.getCenter(),
altitude: splatModel.getAltitude(),
scale: splatModel.getScale(),
rotation: splatModel.getRotationDegrees()
};
console.log("Saving to database:", config);
const log = document.getElementById('output-log');
log.style.display = 'block';
log.innerText = `Saved state: ${JSON.stringify(config, null, 2)}`;
setTimeout(() => {
log.style.display = 'none';
}, 5000);
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>3D Splat Editor</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="map"></div>
<div id="editor-ui" class="collapsed">
<div class="ui-header" id="ui-toggle">
<h3>Splat Editor</h3>
<svg class="chevron" viewBox="0 0 24 24">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" />
</svg>
</div>
<div id="ui-content">
<p style="margin: 0; font-size: 10px; color: #666;">Drag model to reposition</p>
<div class="control-group">
<label>Altitude (m)</label>
<input type="range" id="alt-slider" min="-50" max="200" step="1" value="0">
<div class="value-display" id="alt-val">0m</div>
</div>
<div class="control-group">
<label>Scale</label>
<input type="range" id="scale-slider" min="0.001" max="0.2" step="0.001" value="0.1">
<div class="value-display" id="scale-val">0.1x</div>
</div>
<div class="control-group">
<label>Rotation X (°)</label>
<input type="range" id="rot-x-slider" min="0" max="360" step="1" value="0">
<div class="value-display" id="rot-x-val">0°</div>
</div>
<div class="control-group">
<label>Rotation Y (°)</label>
<input type="range" id="rot-y-slider" min="0" max="360" step="1" value="0">
<div class="value-display" id="rot-y-val">0°</div>
</div>
<div class="control-group">
<label>Rotation Z (°)</label>
<input type="range" id="rot-z-slider" min="0" max="360" step="1" value="0">
<div class="value-display" id="rot-z-val">0°</div>
</div>
<button id="save-btn">Save Configuration</button>
</div>
</div>
<div id="output-log"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
body {
font-family: Inter, sans-serif;
margin: 0;
overflow: hidden;
}
#map {
width: 100%;
height: 100vh;
}
#editor-ui {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.95);
padding: 12px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 10px;
width: 240px;
max-height: calc(100% - 20px);
overflow-y: auto;
transition: all 0.3s ease-in-out;
z-index: 1000;
}
#editor-ui.collapsed {
width: 130px;
height: 38px;
padding: 8px 12px;
overflow: hidden;
gap: 0;
}
.ui-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
}
.ui-header h3 {
margin: 0;
font-size: 14px;
}
.chevron {
width: 24px;
height: 24px;
transition: transform 0.3s;
fill: #666;
}
.collapsed .chevron {
transform: rotate(-90deg);
}
#ui-content {
display: flex;
flex-direction: column;
gap: 10px;
transition: opacity 0.2s;
}
.collapsed #ui-content {
opacity: 0;
pointer-events: none;
}
.control-group {
display: flex;
flex-direction: column;
gap: 2px;
}
label {
font-size: 11px;
font-weight: 600;
color: #444;
}
input[type="range"] {
width: 100%;
height: 14px;
margin: 0;
}
.value-display {
font-family: monospace;
font-size: 10px;
color: #666;
text-align: right;
}
button {
font:
600 12px/18px "Helvetica Neue",
Arial,
Helvetica,
sans-serif;
background-color: #3386c0;
color: #fff;
padding: 6px 12px;
border: none;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
button:hover {
background-color: #4ea0da;
}
button#save-btn {
background-color: #28a745;
margin-top: 4px;
}
button#save-btn:hover {
background-color: #218838;
}
#output-log {
position: absolute;
bottom: 20px;
left: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: #00ff00;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 10px;
max-height: fit-content;
overflow-y: auto;
pointer-events: none;
display: none;
z-index: 1001;
}
Learn more
Check out the MapTiler GeoSplats SDK JS reference
Related examples
Add splat model
ExamplesLearn how to load a map using the GeoSplats SDK and add a photorealistic splat model. A splat model offer a modern alternative to 3D meshes, perfect for complex scenes.
Capture events
ExamplesLearn how to capture both global map events and specific splats events like load, update, mouseover, and click.
Change basemap
ExamplesLearn how to dynamically switch between different MapTiler basemaps while maintaining photorealistic 3D splats models in your scene.
Opacity and visibility
ExamplesLearn how to dynamically control the transparency and visibility of photorealistic splats objects using the MapTiler GeoSplats SDK.