---
title: "Edit model in custom editor | Geosplats sdk"
description: "Build a custom 3D editor to adjust the position, rotation, and scale of splat models and save their properties to a database."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs-qmgv.pages.dev/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# 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](https://docs-qmgv.pages.dev/guides/geosplats/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

```bash
npm install --save @maptiler/geosplats
```

#### main.js

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

#### index.html

```html
<!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>
```

#### style.css

```css
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;
}
```

### Basic JavaScript setup

```html
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Splat Editor</title>
  <link rel="stylesheet"
    href="https://cdn.maptiler.com/maptiler-geosplats/v1.1.2/maptiler-geosplats.css">
  <style>
    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;
    }
  </style>
</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">
    import { SplatModel, Map, config } from "https://cdn.maptiler.com/maptiler-geosplats/v1.1.2/maptiler-geosplats.mjs";;
    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);
    };
  </script>
</body>

</html>
```

## Learn more

Check out the [MapTiler GeoSplats SDK JS reference](https://docs-qmgv.pages.dev/geosplats-sdk/api/)

[Add 3D scenes](https://docs-qmgv.pages.dev/guides/geosplats/)

## Related examples

### [Add splat model](https://docs-qmgv.pages.dev/geosplats-sdk/examples/add-geosplats/)

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 events](https://docs-qmgv.pages.dev/geosplats-sdk/examples/events/)

Examples

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

### [Change basemap](https://docs-qmgv.pages.dev/geosplats-sdk/examples/change-basemap/)

Examples

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

### [Opacity and visibility](https://docs-qmgv.pages.dev/geosplats-sdk/examples/opacity-visibility/)

Examples

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