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);
};

Learn more

Check out the MapTiler GeoSplats SDK JS reference

Add 3D scenes

How to add a splat model to your map

Add splat model

Examples

Learn 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 map and splats events

Capture events

Examples

Learn how to capture both global map events and specific splats events like load, update, mouseover, and click.

Change map basemap

Change basemap

Examples

Learn how to dynamically switch between different MapTiler basemaps while maintaining photorealistic 3D splats models in your scene.

Change the opacity and visibility of splats

Opacity and visibility

Examples

Learn how to dynamically control the transparency and visibility of photorealistic splats objects using the MapTiler GeoSplats SDK.

Was this helpful?