Enables the user to draw points, lines and polygons.

Click on the map to add points. Right-click to end the shape.
Click on the map to add points to the line. Right-click to end the shape.
Click on the map to add points to the polygon. Click on the first point or right-click to end the shape.
Click on a point and drag it to move it. Click on an edge to create a new point and drag it to move it. Right-click to end.
Click on a drawn shape to edit it.

The DrawTool class allows you to draw geometries, such as points, lines and polygons, on surfaces. A temporary geometry is displayed while you add points, then removed when you're finished. The DrawTool then returns a GeoJSON object of the created geometry.

index.js
import * as turf from '@turf/turf';
import XYZ from 'ol/source/XYZ.js';
import {
    LineBasicMaterial,
    MeshBasicMaterial,
    PointsMaterial,
    ShapeUtils,
    Vector2,
    Vector3,
} from 'three';
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';

import Extent from '@giro3d/giro3d/core/geographic/Extent.js';
import Instance from '@giro3d/giro3d/core/Instance.js';
import ElevationLayer from '@giro3d/giro3d/core/layer/ElevationLayer.js';
import Map from '@giro3d/giro3d/entities/Map.js';
import Inspector from '@giro3d/giro3d/gui/Inspector.js';
import GeoTIFFFormat from '@giro3d/giro3d/formats/GeoTIFFFormat.js';
import DrawTool, { DrawToolMode, DrawToolState } from '@giro3d/giro3d/interactions/DrawTool.js';
import Drawing from '@giro3d/giro3d/interactions/Drawing.js';
import DrawingCollection from '@giro3d/giro3d/entities/DrawingCollection.js';
import Fetcher from '@giro3d/giro3d/utils/Fetcher';
import TiledImageSource from '@giro3d/giro3d/sources/TiledImageSource.js';



// Create some functions for measurement

function getPerimeter(drawing) {
    if (drawing.coordinates.length < 6) return null;

    let length = 0;
    for (let i = 0; i < drawing.coordinates.length / 3 - 1; i += 1) {
        length += new Vector3(
            drawing.coordinates[i * 3 + 0],
            drawing.coordinates[i * 3 + 1],
            drawing.coordinates[i * 3 + 2],
        ).distanceTo(
            new Vector3(
                drawing.coordinates[(i + 1) * 3 + 0],
                drawing.coordinates[(i + 1) * 3 + 1],
                drawing.coordinates[(i + 1) * 3 + 2],
            ),
        );
    }
    return length;
}

function getMinMaxAltitudes(drawing) {
    let min = +Infinity;
    let max = -Infinity;

    for (let i = 0; i < drawing.coordinates.length / 3; i += 1) {
        min = Math.min(min, drawing.coordinates[i * 3 + 2]);
        max = Math.max(max, drawing.coordinates[i * 3 + 2]);
    }
    return [min, max];
}

function getArea(drawing) {
    if (drawing.coordinates.length < 6) return null;

    const localFlatCoords = drawing.localCoordinates;
    const localCoords = new Array(localFlatCoords.length / 3);
    for (let i = 0; i < localFlatCoords.length / 3; i += 1) {
        localCoords[i] = new Vector2(localFlatCoords[i * 3 + 0], localFlatCoords[i * 3 + 1]);
    }
    return Math.abs(ShapeUtils.area(localCoords));
}

// Initialize Giro3D (see tifftiles for more details)
const x = -13602618.385789588;
const y = 5811042.273912458;

const extent = new Extent('EPSG:3857', x - 12000, x + 13000, y - 4000, y + 26000);

const instance = new Instance(document.getElementById('viewerDiv'), {
    crs: extent.crs(),
    renderer: {
        clearColor: 0x0a3b59,
    },
});

const map = new Map('planar', {
    extent,
    hillshading: true,
    segments: 128,
    discardNoData: true,
    backgroundColor: 'white',
});

instance.add(map);

let footprint;

/**
 * A function that will override the default intersection test for image sources (by default
 * performing intersection on extents, i.e rectangles). Here we want to exclude tiles that do not
 * intersect with the GeoJSON footprint of the dataset.
 *
 * @param {Extent} tileExtent The extent to test.
 */
function customIntersectionTest(tileExtent) {
    if (!footprint) {
        return true;
    }

    const corners = [
        [tileExtent.topLeft().x, tileExtent.topLeft().y],
        [tileExtent.topRight().x, tileExtent.topRight().y],
        [tileExtent.bottomRight().x, tileExtent.bottomRight().y],
        [tileExtent.bottomLeft().x, tileExtent.bottomLeft().y],
    ];

    const extentAsPolygon = turf.helpers.polygon([
        [corners[0], corners[1], corners[2], corners[3], corners[0]],
    ]);

    const intersects = turf.booleanIntersects(turf.toWgs84(extentAsPolygon), footprint);

    return intersects;
}

Fetcher.json('data/MtStHelens-footprint.geojson')
    .then(geojson => {
        footprint = turf.toWgs84(geojson);

        const source = new TiledImageSource({
            containsFn: customIntersectionTest, // Here we specify our custom intersection test
            source: new XYZ({
                minZoom: 10,
                maxZoom: 16,
                url: 'https://3d.oslandia.com/dem/MtStHelens-tiles/{z}/{x}/{y}.tif',
            }),
            format: new GeoTIFFFormat(),
        });

        map.addLayer(
            new ElevationLayer({
                name: 'osm',
                extent,
                source,
            }),
        ).catch(e => console.error(e));
    })
    .catch(e => console.error(e));

const center = extent.centerAsVector3();
instance.camera.camera3D.position.set(center.x, center.y - 1, 50000);

// Instanciates controls
// Beware: we need to bind them to *instance.domElement* so we can interact over 2D labels!
const controls = new MapControls(instance.camera.camera3D, instance.domElement);

controls.target.copy(center);
instance.useTHREEControls(controls);

Inspector.attach(document.getElementById('panelDiv'), instance);
instance.domElement.addEventListener('dblclick', e => console.log(instance.pickObjectsAt(e)));

// Instanciate drawtool
const drawToolOptions = {
    drawObject3DOptions: {
        minExtrudeDepth: 40,
        maxExtrudeDepth: 100,
    },
    enableDragging: document.getElementById('dragging').value === '0',
    splicingHitTolerance: document.getElementById('splicingtoleranceEnabled').checked
        ? parseInt(document.getElementById('splicingtolerance').value, 10)
        : undefined,
    minPoints: document.getElementById('minpointsEnabled').checked
        ? parseInt(document.getElementById('minpoints').value, 10)
        : undefined,
    maxPoints: document.getElementById('maxpointsEnabled').checked
        ? parseInt(document.getElementById('maxpoints').value, 10)
        : undefined,
    enableAddPointsOnEdit: document.getElementById('addpointsEnabled').checked,
    use3Dpoints: document.getElementById('pointsrendering').value === '0',
    point2DFactory: point2DFactoryHighlighted,
};
const drawTool = new DrawTool(instance, drawToolOptions);

// Prevent drawing when the user is interacting (panning, etc.)
controls.addEventListener('change', () => drawTool.pause());
controls.addEventListener('end', () => setTimeout(() => drawTool.continue(), 0));

// Let's plug our buttons to the drawTool API
const updateDragging = () => {
    drawToolOptions.enableDragging = document.getElementById('dragging').value === '0';
    drawTool.setOptions(drawToolOptions);
};
const updateSplicingTolerance = () => {
    drawToolOptions.splicingHitTolerance = document.getElementById('splicingtoleranceEnabled')
        .checked
        ? parseInt(document.getElementById('splicingtolerance').value, 10)
        : undefined;
    drawTool.setOptions(drawToolOptions);
};
const updateMinpoints = () => {
    drawToolOptions.minPoints = document.getElementById('minpointsEnabled').checked
        ? parseInt(document.getElementById('minpoints').value, 10)
        : undefined;
    drawTool.setOptions(drawToolOptions);
};
const updateMaxpoints = () => {
    drawToolOptions.maxPoints = document.getElementById('maxpointsEnabled').checked
        ? parseInt(document.getElementById('maxpoints').value, 10)
        : undefined;
    drawTool.setOptions(drawToolOptions);
};
const updateAddPoints = () => {
    drawToolOptions.enableAddPointsOnEdit = document.getElementById('addpointsEnabled').checked;
    drawTool.setOptions(drawToolOptions);
};

document.getElementById('dragging').addEventListener('change', updateDragging);
document
    .getElementById('splicingtoleranceEnabled')
    .addEventListener('change', updateSplicingTolerance);
document.getElementById('splicingtolerance').addEventListener('change', updateSplicingTolerance);
document.getElementById('minpointsEnabled').addEventListener('change', updateMinpoints);
document.getElementById('minpoints').addEventListener('change', updateMinpoints);
document.getElementById('maxpointsEnabled').addEventListener('change', updateMaxpoints);
document.getElementById('maxpoints').addEventListener('change', updateMaxpoints);
document.getElementById('addpointsEnabled').addEventListener('change', updateAddPoints);

document.getElementById('addPoint').onclick = () => {
    if (drawTool.state !== DrawToolState.READY) {
        // We're already drawing, do something with the current drawing
        if (drawTool.mode === DrawToolMode.EDIT) drawTool.end();
        else drawTool.reset();
    }

    // Display help
    for (const o of document.getElementsByClassName('helper')) {
        o.classList.add('d-none');
    }
    document.getElementById('addPointHelper').classList.remove('d-none');
    document.getElementById('options').setAttribute('disabled', true);

    // Start drawing!
    drawTool.start('MultiPoint');
};

document.getElementById('addLine').onclick = () => {
    if (drawTool.state !== DrawToolState.READY) {
        if (drawTool.mode === DrawToolMode.EDIT) drawTool.end();
        else drawTool.reset();
    }

    for (const o of document.getElementsByClassName('helper')) {
        o.classList.add('d-none');
    }
    document.getElementById('addLineHelper').classList.remove('d-none');
    document.getElementById('options').setAttribute('disabled', true);

    drawTool.start('LineString');
};

document.getElementById('addPolygon').onclick = () => {
    if (drawTool.state !== DrawToolState.READY) {
        if (drawTool.mode === DrawToolMode.EDIT) drawTool.end();
        else drawTool.reset();
    }

    for (const o of document.getElementsByClassName('helper')) {
        o.classList.add('d-none');
    }
    document.getElementById('addPolygonHelper').classList.remove('d-none');
    document.getElementById('options').setAttribute('disabled', true);

    drawTool.start('Polygon');
};

// Hide the help when we're done drawing
drawTool.addEventListener('end', () => {
    for (const o of document.getElementsByClassName('helper')) {
        o.classList.add('d-none');
    }
    document.getElementById('mainHelper').classList.remove('d-none');
    document.getElementById('options').removeAttribute('disabled');
});

// When we're done drawing, the drawTool removes the shape.
// We want to keep it displayed so we can edit it.
// We'll keep track of our shapes, so we can edit their rendering parameters
// and optimize the picking on hover&click.
const drawEntity = new DrawingCollection();
instance.add(drawEntity);

// We'll use different materials for displaying drawn shapes
const drawnFaceMaterial = new MeshBasicMaterial({
    color: 0x433c73,
    opacity: 0.2,
});
const drawnSideMaterial = new MeshBasicMaterial({
    color: 0x433c73,
    opacity: 0.8,
});
const drawnLineMaterial = new LineBasicMaterial({
    color: 0x252140,
});
const drawnPointMaterial = new PointsMaterial({
    color: 0x433c73,
    size: 100,
});

// If using CSS2DRenderer for points, we define our own (optional) factory
function point2DFactory(text) {
    const pt = document.createElement('div');
    pt.style.position = 'absolute';
    pt.style.borderRadius = '50%';
    pt.style.width = '28px';
    pt.style.height = '28px';
    pt.style.backgroundColor = '#433C73';
    pt.style.color = '#ffffff';
    pt.style.border = '2px solid #070607';
    pt.style.fontSize = '14px';
    pt.style.textAlign = 'center';
    pt.style.pointerEvents = 'none';
    pt.style.cursor = 'pointer';
    pt.innerText = text;
    return pt;
}

function point2DFactoryHighlighted(text) {
    const pt = document.createElement('div');
    pt.style.position = 'absolute';
    pt.style.borderRadius = '50%';
    pt.style.width = '28px';
    pt.style.height = '28px';
    pt.style.backgroundColor = '#347330';
    pt.style.color = '#ffffff';
    pt.style.border = '2px solid #070607';
    pt.style.fontSize = '14px';
    pt.style.textAlign = 'center';
    pt.style.pointerEvents = 'none';
    pt.style.cursor = 'pointer';
    pt.innerText = text;
    return pt;
}

const updatePointsRendering = () => {
    // Update existing drawings
    for (const o of drawEntity.children) {
        if (o.geometryType === 'MultiPoint') {
            o.use3Dpoints = document.getElementById('pointsrendering').value === '0';
            instance.notifyChange(o);
        }
    }
};
document.getElementById('pointsrendering').addEventListener('change', updatePointsRendering);

function addShape(geojson) {
    // Create and show a new object with the same geometry but with different materials
    const o = new Drawing(
        {
            faceMaterial: drawnFaceMaterial,
            sideMaterial: drawnSideMaterial,
            lineMaterial: drawnLineMaterial,
            pointMaterial: drawnPointMaterial,
            minExtrudeDepth: 40,
            maxExtrudeDepth: 100,
            use3Dpoints: document.getElementById('pointsrendering').value === '0',
            point2DFactory,
        },
        geojson,
    );

    // Compute some measurements
    o.userData.measurements = {
        minmax: getMinMaxAltitudes(o),
        nbPoints: o.coordinates.length / 3,
    };

    if (o.geometryType === 'Polygon' || o.geometryType === 'LineString') {
        o.userData.measurements.perimeter = getPerimeter(o);
    }
    if (o.geometryType === 'Polygon') {
        o.userData.measurements.area = getArea(o);
    }

    // Add it to our scene
    drawEntity.add(o);
    instance.notifyChange(drawEntity);
}

// Listen to when we are done drawing
drawTool.addEventListener('end', evt => addShape(evt.geojson));

// At this point we:
// - can add new shapes,
// - have them displayed when done.

// Let's add selection & edition!

// Display a nice pointer when the user is over a drawn shape
// and show measurement info
instance.domElement.addEventListener('mousemove', evt => {
    // Do nothing if we're editing a shape
    if (drawTool.state !== DrawToolState.READY) return;

    // In case we're using points with CSS2DRenderer, instance.pickObjectsAt will take
    // care of returning the elements corresponding to the points
    // You can therefore use the same API whatever rendering method you're using for points
    const picked = instance
        .pickObjectsAt(evt, {
            where: [drawEntity],
            limit: 1,
            radius: 5,
            pickFeatures: true,
        })
        .at(0);
    instance.domElement.style.cursor = picked ? 'pointer' : 'default';

    if (picked && picked.drawing) {
        const mesurements = picked.drawing.userData.measurements;
        const hoverHelper = document.getElementById('hoverHelper');
        hoverHelper.innerText = `
            Number of points: ${mesurements.nbPoints}
            Min altitude: ${mesurements.minmax[0].toFixed()}m
            Max altitude: ${mesurements.minmax[1].toFixed()}m
            ${mesurements.perimeter ? `Perimeter: ${mesurements.perimeter.toFixed()}m` : ''}
            ${mesurements.area ? `Area: ${mesurements.area.toFixed()}m²` : ''}
        `;
        hoverHelper.classList.remove('d-none');
    } else {
        document.getElementById('hoverHelper').classList.add('d-none');
    }
});

// Edit a shape when clicking on it
instance.domElement.addEventListener('click', evt => {
    if (drawTool.state !== DrawToolState.READY) return;
    const picked = instance.pickObjectsAt(evt, { where: [drawEntity], limit: 1, radius: 5 }).at(0);
    if (picked) {
        const drawing = picked.drawing;

        for (const o of document.getElementsByClassName('helper')) {
            o.classList.add('d-none');
        }
        document.getElementById('editHelper').classList.remove('d-none');
        document.getElementById('options').setAttribute('disabled', true);

        instance.domElement.style.cursor = 'default';

        // When editing a Drawing object directly, materials and options are not reset from drawTool
        drawing.setMaterials({}); // Reset the materials
        instance.notifyChange(drawEntity); // And notify Giro3D for the changes

        drawEntity.remove(drawing);
        drawTool.edit(drawing); // Start the edit
    }
});

// Load some shapes
Fetcher.json('https://3d.oslandia.com/dem/features.json').then(features => {
    features.forEach(feature => {
        // Mess around with the API
        drawTool.edit(feature);
        drawTool.insertPointAt(0, new Vector3(0, 0, 0));
        drawTool.updatePointAt(0, new Vector2(1, 2, 3));
        drawTool.deletePoint(0);
        drawTool.end();
        // In real use case, we'd call addShape directly
    });
});

// Add some shapes via API
drawTool.start('Polygon');
drawTool.addPointAt(new Vector3(0, 0, 0));
drawTool.addPointAt(new Vector3(1, 2, 3));
drawTool.addPointAt(new Vector3(-13601375.735757545, 5811313.553932933, 2263.4501953125));
drawTool.addPointAt(new Vector3(-13601183.080786511, 5811718.900814531, 2169.449462890625));
drawTool.addPointAt(new Vector3(-13601435.612581342, 5811988.171339845, 2113.301025390625));
drawTool.addPointAt(new Vector3(-13601692.241683275, 5812247.386654718, 2098.3310546875));
drawTool.addPointAt(new Vector3(-13601229.646728978, 5812162.072989969, 2098.95068359375));
drawTool.addPointAt(new Vector3(-13601285.151505515, 5812670.5826594215, 2013.54736328125));
drawTool.addPointAt(new Vector3(-13601435.248480782, 5813288.373160986, 1857.339111328125));
drawTool.addPointAt(new Vector3(-13601878.109128818, 5813344.696319774, 1802.1083984375));
drawTool.addPointAt(new Vector3(-13602217.049078688, 5813161.852430431, 1857.4925537109375));
drawTool.addPointAt(new Vector3(-13602524.077633068, 5812790.909256665, 1967.9317626953125));
drawTool.addPointAt(new Vector3(-13602730.889452294, 5812173.281410588, 2093.4921875));
drawTool.addPointAt(new Vector3(-13602525.81417714, 5811603.776026396, 2215.937744140625));
drawTool.updatePointAt(0, new Vector3(-13601908.711527146, 5811403.3703095, 2241.1591796875));
drawTool.deletePoint(1);
drawTool.end();
index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Giro3D - Draw shapes</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
  <style>
    body {
      padding: 0;
      margin: 0;
      width: 100vw;
      height: 100vh;
    }

    #viewerDiv {
      width: 100%;
      height: 100%;
    }

    #panelDiv {
      position: absolute;
      top: 0;
      left: 0;
    }
    
  </style>
</head>

<body>
  <div id="viewerDiv"></div>
  <div id="panelDiv"></div>
  <div class="m-3 position-absolute top-0 end-0" style="width: 20vw">
    <div class="row">
        <button class="col btn btn-primary" id="addPoint">Points</button>
        <button class="col mx-1 btn btn-primary" id="addLine">Line</button>
        <button class="col btn btn-primary" id="addPolygon">Polygon</button>
    </div>
    <div class="row mt-2">
        <div class="p-2 card d-none helper" id="addPointHelper">
            Click on the map to add points. Right-click to end the shape.
        </div>
        <div class="p-2 card d-none helper" id="addLineHelper">
            Click on the map to add points to the line. Right-click to end the shape.
        </div>
        <div class="p-2 card d-none helper" id="addPolygonHelper">
            Click on the map to add points to the polygon. Click on the first point or right-click
            to end the shape.
        </div>
        <div class="p-2 card d-none helper" id="editHelper">
            Click on a point and drag it to move it. Click on an edge to create a new point and drag
            it to move it. Right-click to end.
        </div>
        <div class="p-2 card d-none helper" id="mainHelper">Click on a drawn shape to edit it.</div>
    </div>
    <div class="row mt-2 card">
        <button
            class="btn btn-secondary dropdown-toggle"
            type="button"
            data-bs-toggle="collapse"
            data-bs-target="#options"
            aria-expanded="false"
            aria-controls="options"
        >
            Options
        </button>
        <fieldset id="options" class="p-2 collapse">
            <div class="container">
                <div class="row mt-1">
                    <select
                        class="btn btn-light border border-dark dropdown-toggle w-100"
                        id="dragging"
                    >
                        <option value="0" selected>Move points via drag-and-drop</option>
                        <option value="1">Move points on click</option>
                    </select>
                </div>
                <div class="row mt-1">
                    <div class="input-group p-0">
                        <div class="input-group-text">
                            <div class="form-check form-switch">
                                <input
                                    class="form-check-input"
                                    type="checkbox"
                                    checked="true"
                                    role="switch"
                                    id="splicingtoleranceEnabled"
                                />
                                <label
                                    class="form-check-label"
                                    for="splicingtoleranceEnabled"
                                    title="Unit in 3D world space (when disabled, auto)"
                                    >Splicing hit tolerance</label
                                >
                            </div>
                        </div>
                        <input
                            type="number"
                            class="form-control"
                            min="0"
                            max="100"
                            value="15"
                            id="splicingtolerance"
                        />
                    </div>
                </div>
                <div class="row mt-1">
                    <div class="input-group p-0">
                        <div class="input-group-text">
                            <div class="form-check form-switch">
                                <input
                                    class="form-check-input"
                                    type="checkbox"
                                    checked="true"
                                    role="switch"
                                    id="minpointsEnabled"
                                />
                                <label
                                    class="form-check-label"
                                    for="minpointsEnabled"
                                    title="Minimum points before the geometry can be finished (when disabled, auto)"
                                    >Minimum points</label
                                >
                            </div>
                        </div>
                        <input
                            type="number"
                            class="form-control"
                            min="0"
                            max="10"
                            value="0"
                            id="minpoints"
                        />
                    </div>
                </div>
                <div class="row mt-1">
                    <div class="input-group p-0">
                        <div class="input-group-text">
                            <div class="form-check form-switch">
                                <input
                                    class="form-check-input"
                                    type="checkbox"
                                    checked="true"
                                    role="switch"
                                    id="maxpointsEnabled"
                                />
                                <label
                                    class="form-check-label"
                                    for="maxpointsEnabled"
                                    title="Number of points at which the geometry is automatically considered as finished"
                                    >Maximum points</label
                                >
                            </div>
                        </div>
                        <input
                            type="number"
                            class="form-control"
                            min="0"
                            max="10"
                            value="10"
                            id="maxpoints"
                        />
                    </div>
                </div>
                <div class="row mt-1">
                    <div>
                        <div class="form-check form-switch">
                            <input
                                class="form-check-input"
                                type="checkbox"
                                checked="true"
                                role="switch"
                                id="addpointsEnabled"
                            />
                            <label class="form-check-label" for="addpointsEnabled"
                                >Add new points when editing</label
                            >
                        </div>
                    </div>
                </div>
                <div class="row mt-1">
                    <select
                        class="btn btn-light border border-dark dropdown-toggle w-100"
                        id="pointsrendering"
                    >
                        <option value="0">Render points as 3D objects</option>
                        <option value="1" selected>Render points via CSS2DRenderer</option>
                    </select>
                </div>
            </div>
        </fieldset>
    </div>
</div>

<div class="me-3 mb-5 position-absolute bottom-0 end-0" style="width: 20vw">
    <div class="p-2 card d-none" id="hoverHelper"></div>
</div>

  <script type="module" src="index.js"></script>
  <script>
    /* activate popovers */
    const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
    popoverTriggerList.map(
      // bootstrap is used as script in the template, disable warning about undef
      // eslint-disable-next-line no-undef
      popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl, {
        trigger: 'hover',
        placement: 'left',
        content: document.getElementById(popoverTriggerEl.getAttribute('data-bs-content')).innerHTML,
        html: true,
      }),
    );
  </script>
</body>

</html>
package.json
{
  "name": "drawtool",
  "dependencies": {
    "@giro3d/giro3d": "0.35.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}