Parameters
URL
Error target
Selected object
| Name | Value |
|---|---|
| ? | ? |
Display any 3D Tiles from a url.
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { Toast } from "bootstrap";
import OSM from "ol/source/OSM.js";
import { AmbientLight, Color, DirectionalLight, Mesh, Vector3 } from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
import GlobeControls from "@giro3d/giro3d/controls/GlobeControls.js";
import CoordinateSystem from "@giro3d/giro3d/core/geographic/CoordinateSystem.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import Globe from "@giro3d/giro3d/entities/Globe.js";
import Tiles3D from "@giro3d/giro3d/entities/Tiles3D.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
function bindButton(id, onClick) {
const element = document.getElementById(id);
if (!(element instanceof HTMLButtonElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.onclick = () => {
onClick(element);
};
return element;
}
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
function bindNumberInput(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(element.valueAsNumber);
};
const callback = (v) => {
element.value = v.toString();
onChange(element.valueAsNumber);
};
return [callback, element.valueAsNumber, element];
}
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
function bindSlider(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
onChange(element.valueAsNumber);
};
const setValue = (v, min, max, step) => {
if (min != null && max != null) {
element.min = min.toString();
element.max = max.toString();
if (step != null) {
element.step = step;
}
}
element.valueAsNumber = v;
onChange(element.valueAsNumber);
};
const initialValue = element.valueAsNumber;
return [setValue, initialValue, element];
}
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
function bindToggle(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
onChange(element.checked);
};
const callback = (v) => {
element.checked = v;
onChange(element.checked);
};
return [callback, element.checked, element];
}
function resolveTilesetUrl() {
const searchParam = new URL(document.URL).searchParams.get("url");
const input = document.getElementById("url");
const inputValue = input.value;
if (inputValue) {
const value = input.value;
const url = new URL(document.URL);
url.searchParams.delete("url");
url.searchParams.append("url", value);
window.history.replaceState({}, null, url.toString());
return value;
} else if (searchParam) {
input.value = searchParam;
return searchParam;
} else {
// TODO defaults ?
}
}
const params = {
globeMode: false,
globeOpacity: 1,
tilesetOpacity: 1,
errorTarget: 8,
};
let tileset = undefined;
let instance = undefined;
let globe = undefined;
let inspector = undefined;
function initControls(target) {
if (params.globeMode) {
const globeControls = new GlobeControls({
scene: globe.object3d,
ellipsoid: globe.ellipsoid,
camera: instance.view.camera,
domElement: instance.domElement,
});
const updateControls = () => {
globeControls.update();
instance.notifyChange(globe);
instance.notifyChange(tileset);
requestAnimationFrame(updateControls);
};
updateControls();
} else {
// create controls
const mapControls = new MapControls(
instance.view.camera,
instance.domElement,
);
mapControls.enableDamping = true;
mapControls.dampingFactor = 0.25;
mapControls.update();
mapControls.target.copy(target);
instance.view.setControls(mapControls);
}
}
// setup the error displaying
const toastLiveExample = document.getElementById("liveToast");
const toastBootstrap = Toast.getOrCreateInstance(toastLiveExample);
function displayError(evt) {
document.getElementById("error").innerText = evt.error.message;
toastBootstrap.show();
}
async function run(url) {
instance?.dispose();
inspector?.detach();
// init instance
instance = new Instance({
target: "view", // The id of the <div> to attach the instance
crs: params.globeMode
? CoordinateSystem.epsg4978
: CoordinateSystem.epsg3857,
backgroundColor: 0xcccccc,
});
if (params.globeMode) {
globe = new Globe({
backgroundColor: "#aad3df",
});
globe.helperColor = "black";
await instance.add(globe);
const layer = new ColorLayer({
source: new TiledImageSource({ source: new OSM() }),
});
await globe.addLayer(layer);
}
// Add a sunlight
const sun = new DirectionalLight("#ffffff", 1.4);
sun.position.set(1, 0, 1).normalize();
sun.updateMatrixWorld(true);
instance.scene.add(sun);
// We can look below the floor, so let's light also a bit there
const sun2 = new DirectionalLight("#ffffff", 0.5);
sun2.position.set(0, -1, 1);
sun2.updateMatrixWorld();
instance.scene.add(sun2);
// Add ambient light
const ambientLight = new AmbientLight(0xffffff, 1);
instance.scene.add(ambientLight);
instance.view.minNearPlane = 0.5;
tileset = new Tiles3D({ url: url.toString() });
// If the tileset comes from an ifc converted with py3dtiles, hide some elements that don't bring visual value
tileset.addEventListener("object-created", (evt) => {
const scene = evt.obj;
scene.traverse((obj) => {
if (obj.userData?.class === "IfcSpace") {
obj.visible = false;
instance.notifyChange();
}
});
});
await instance.add(tileset);
const pov = instance.view.goTo(tileset);
initControls(pov.target);
inspector = Inspector.attach("inspector", instance);
}
run(resolveTilesetUrl()).catch(console.error);
// picking and highlighting logic
const resultsTable = document.getElementById("results");
let highlighted;
let highlightColor = new Color(0xff7171);
let canPick = true;
function highlight(evt) {
if (!instance || !tileset) {
return;
}
if (!canPick) {
return;
}
const picked = instance.pickObjectsAt(evt, {
radius: 5,
limit: 10,
where: [tileset],
filter: (pick) => pick.object.visible, // Ignore invisible objects, such as IfcSpace elements
});
if (highlighted && highlighted.material.color != null) {
// reset style
const material = highlighted.material;
material.color.copy(material.userData.oldColor);
instance.notifyChange(highlighted);
}
if (picked.length === 0) {
document.getElementById("pick-result").style.display = "none";
} else {
document.getElementById("pick-result").style.display = "block";
const obj = picked[0].object;
if (obj instanceof Mesh) {
const material = obj.material;
// keep the old color to reset it later
if (material.color != null) {
if (!material.userData.oldColor) {
material.userData.oldColor = material.color.clone();
}
material.color.copy(highlightColor);
}
instance.notifyChange(obj);
highlighted = obj;
}
const rows = [];
for (const [name, value] of Object.entries(obj.userData)) {
if (name !== "oldColor" && name !== "parentEntity") {
const row = document.createElement("tr");
const nameCell = document.createElement("td");
nameCell.innerHTML = `<code>${name}</code>`;
const valueCell = document.createElement("td");
valueCell.innerText = value.toString().substring(0, 20);
valueCell.title = value;
row.append(nameCell, valueCell);
rows.push(row);
}
}
resultsTable.replaceChildren(...rows);
}
}
// Prevent picking if user is dragging mouse
instance.domElement.addEventListener("mousedown", () => (canPick = true));
instance.domElement.addEventListener("mousemove", () => (canPick = false));
instance.domElement.addEventListener("mouseup", highlight);
function updateParams() {
if (globe) {
globe.opacity = params.globeOpacity;
globe.visible = params.globeOpacity > 0;
instance.notifyChange(globe);
}
if (tileset) {
tileset.opacity = params.tilesetOpacity;
tileset.visible = params.tilesetOpacity > 0;
tileset.errorTarget = params.errorTarget;
instance.notifyChange(tileset);
}
}
updateParams();
bindSlider("globe-opacity", (v) => {
params.globeOpacity = v;
updateParams();
});
bindSlider("tileset-opacity", (v) => {
params.tilesetOpacity = v;
updateParams();
});
bindButton("center-view", () => {
if (tileset) {
instance.view.goTo(tileset);
}
});
bindToggle("globe-mode", (v) => {
params.globeMode = v;
});
bindNumberInput("error-target", (v) => {
params.errorTarget = v;
updateParams();
});
bindButton("start", () => {
run(resolveTilesetUrl()).catch(console.error);
});
<!doctype html>
<html lang="en">
<head>
<title>3D Tiles Simple Viewer</title>
<meta charset="UTF-8" />
<meta name="name" content="3d-tiles-simple-viewer" />
<meta name="description" content="Display any 3D Tiles from a url." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://giro3d.org/images/favicon.svg" />
<link
href="https://giro3d.org/assets/bootstrap-custom.css"
rel="stylesheet"
/>
<script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
<link
rel="stylesheet"
type="text/css"
href="https://giro3d.org/latest/examples/css/example.css"
/>
</head>
<body>
<div id="view" class="m-0 p-0 w-100 h-100"></div>
<div
id="inspector"
class="position-absolute top-0 start-0 mh-100 overflow-auto"
></div>
<div class="side-pane-with-status-bar pe-none">
<div class="card pe-auto">
<div class="card-header">Parameters</div>
<div class="card-body">
<!-- Tileset URL -->
<div class="input-group">
<span class="input-group-text" id="url-label">URL</span>
<input
type="url"
class="form-control"
id="url"
placeholder="https://3d.oslandia.com/3dtiles/19_rue_Marc_Antoine_Petit_ifc/tileset.json"
value="https://3d.oslandia.com/3dtiles/19_rue_Marc_Antoine_Petit_ifc/tileset.json"
aria-label="URL"
autocomplete="off"
aria-describedby="url"
/>
</div>
<!-- Enable globe mode -->
<div class="form-check form-switch mt-3">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="globe-mode"
autocomplete="off"
/>
<label class="form-check-label" for="globe-mode">Globe mode</label>
</div>
<button class="btn btn-primary w-100 mt-3" id="start">
<i class="bi bi-caret-right-fill"></i>
Load tileset
</button>
<hr />
<label for="globe-opacity" id="label-globe-opacity" class="form-label"
>Globe opacity</label
>
<div class="input-group">
<input
type="range"
min="0"
max="1"
step="0.01"
value="1"
class="form-range"
id="globe-opacity"
autocomplete="off"
/>
</div>
<label
for="tileset-opacity"
id="label-tileset-opacity"
class="form-label"
>Tileset opacity</label
>
<div class="input-group">
<input
type="range"
min="0"
max="1"
step="0.01"
value="1"
class="form-range"
id="tileset-opacity"
autocomplete="off"
/>
</div>
<!-- Tileset URL -->
<div class="input-group mt-3">
<span class="input-group-text" id="error-target-label"
>Error target</span
>
<input
type="number"
class="form-control"
id="error-target"
value="8"
aria-label="Error target"
autocomplete="off"
aria-describedby="error-target"
/>
</div>
<!-- Center view -->
<button
type="button"
class="btn btn-primary w-100 mt-3"
id="center-view"
>
<i class="bi bi-eye-fill"></i>
Center view
</button>
</div>
</div>
<div class="card pe-auto mt-3" id="pick-result" style="display: none">
<div class="card-header">Selected object</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody id="results">
<tr>
<td>?</td>
<td>?</td>
</tr>
</tbody>
</table>
</div>
</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>
{
"name": "3d-tiles-simple-viewer",
"dependencies": {
"@giro3d/giro3d": "3.0.0-dev"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
},
},
})