<!-- Import the component -->
<script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.min.js"></script>
<model-viewer id="dimension-demo" ar ar-modes="webxr" ar-scale="fixed" camera-orbit="-30deg auto auto" max-camera-orbit="auto 100deg auto" shadow-intensity="1" camera-controls touch-action="pan-y" src="https://assets.codepen.io/9589/Chair.glb" alt="A 3D model of an armchair.">
<button slot="hotspot-dot+X-Y+Z" class="dot" data-position="1 -1 1" data-normal="1 0 0"></button>
<button slot="hotspot-dim+X-Y" class="dim" data-position="1 -1 0" data-normal="1 0 0"></button>
<button slot="hotspot-dot+X-Y-Z" class="dot" data-position="1 -1 -1" data-normal="1 0 0"></button>
<button slot="hotspot-dim+X-Z" class="dim" data-position="1 0 -1" data-normal="1 0 0"></button>
<button slot="hotspot-dot+X+Y-Z" class="dot show" data-position="1 1 -1" data-normal="0 1 0"></button>
<button slot="hotspot-dim+Y-Z" class="dim show" data-position="0 -1 -1" data-normal="0 1 0"></button>
<button slot="hotspot-dot-X+Y-Z" class="dot show" data-position="-1 1 -1" data-normal="0 1 0"></button>
<button slot="hotspot-dim-X-Z" class="dim" data-position="-1 0 -1" data-normal="-1 0 0"></button>
<button slot="hotspot-dot-X-Y-Z" class="dot" data-position="-1 -1 -1" data-normal="-1 0 0"></button>
<button slot="hotspot-dim-X-Y" class="dim" data-position="-1 -1 0" data-normal="-1 0 0"></button>
<button slot="hotspot-dot-X-Y+Z" class="dot" data-position="-1 -1 1" data-normal="-1 0 0"></button>
<svg id="lines" xmlns="http://www.w3.org/2000/svg" class="dimensionLineContainer">
<line class="dimensionLine"></line>
<line class="dimensionLine"></line>
<line class="dimensionLine"></line>
<line class="dimensionLine"></line>
<line class="dimensionLine"></line>
</svg>
<div id="controls" class="dim">
<label for="src">Product:</label>
<select id="src">
<option value="https://assets.codepen.io/9589/Chair.glb">Chair</option>
</select><br>
<label for="show-dimensions">Show Dimensions:</label>
<input id="show-dimensions" type="checkbox" checked="true">
</div>
</model-viewer>
<script type="module">
const modelViewer = document.querySelector('#dimension-demo');
modelViewer.querySelector('#src').addEventListener('input', (event) => {
modelViewer.src = event.target.value;
});
const checkbox = modelViewer.querySelector('#show-dimensions');
function setVisibility(element) {
if (checkbox.checked) {
element.classList.remove('hide');
} else {
element.classList.add('hide');
}
}
checkbox.addEventListener('change', () => {
setVisibility(modelViewer.querySelector('#lines'));
modelViewer.querySelectorAll('button').forEach((hotspot) => {
setVisibility(hotspot);
});
});
modelViewer.addEventListener('load', () => {
const center = modelViewer.getBoundingBoxCenter();
const size = modelViewer.getDimensions();
const x2 = size.x / 2;
const y2 = size.y / 2;
const z2 = size.z / 2;
modelViewer.updateHotspot({
name: 'hotspot-dot+X-Y+Z',
position: `${center.x + x2} ${center.y - y2} ${center.z + z2}`
});
modelViewer.updateHotspot({
name: 'hotspot-dim+X-Y',
position: `${center.x + x2} ${center.y - y2} ${center.z}`
});
modelViewer.querySelector('button[slot="hotspot-dim+X-Y"]').textContent =
`${(size.z * 100).toFixed(0)} cm`;
modelViewer.updateHotspot({
name: 'hotspot-dot+X-Y-Z',
position: `${center.x + x2} ${center.y - y2} ${center.z - z2}`
});
modelViewer.updateHotspot({
name: 'hotspot-dim+X-Z',
position: `${center.x + x2} ${center.y} ${center.z - z2}`
});
modelViewer.querySelector('button[slot="hotspot-dim+X-Z"]').textContent =
`${(size.y * 100).toFixed(0)} cm`;
modelViewer.updateHotspot({
name: 'hotspot-dot+X+Y-Z',
position: `${center.x + x2} ${center.y + y2} ${center.z - z2}`
});
modelViewer.updateHotspot({
name: 'hotspot-dim+Y-Z',
position: `${center.x} ${center.y + y2} ${center.z - z2}`
});
modelViewer.querySelector('button[slot="hotspot-dim+Y-Z"]').textContent =
`${(size.x * 100).toFixed(0)} cm`;
modelViewer.updateHotspot({
name: 'hotspot-dot-X+Y-Z',
position: `${center.x - x2} ${center.y + y2} ${center.z - z2}`
});
modelViewer.updateHotspot({
name: 'hotspot-dim-X-Z',
position: `${center.x - x2} ${center.y} ${center.z - z2}`
});
modelViewer.querySelector('button[slot="hotspot-dim-X-Z"]').textContent =
`${(size.y * 100).toFixed(0)} cm`;
modelViewer.updateHotspot({
name: 'hotspot-dot-X-Y-Z',
position: `${center.x - x2} ${center.y - y2} ${center.z - z2}`
});
modelViewer.updateHotspot({
name: 'hotspot-dim-X-Y',
position: `${center.x - x2} ${center.y - y2} ${center.z}`
});
modelViewer.querySelector('button[slot="hotspot-dim-X-Y"]').textContent =
`${(size.z * 100).toFixed(0)} cm`;
modelViewer.updateHotspot({
name: 'hotspot-dot-X-Y+Z',
position: `${center.x - x2} ${center.y - y2} ${center.z + z2}`
});
// update svg
function drawLine(svgLine, dotHotspot1, dotHotspot2, dimensionHotspot) {
if (dotHotspot1 && dotHotspot1) {
svgLine.setAttribute('x1', dotHotspot1.canvasPosition.x);
svgLine.setAttribute('y1', dotHotspot1.canvasPosition.y);
svgLine.setAttribute('x2', dotHotspot2.canvasPosition.x);
svgLine.setAttribute('y2', dotHotspot2.canvasPosition.y);
// use provided optional hotspot to tie visibility of this svg line to
if (dimensionHotspot && !dimensionHotspot.facingCamera) {
svgLine.classList.add('hide');
} else {
svgLine.classList.remove('hide');
}
}
}
const lines = modelViewer.querySelectorAll('line');
// use requestAnimationFrame to update with renderer
const startSVGRenderLoop = () => {
drawLine(lines[0], modelViewer.queryHotspot('hotspot-dot+X-Y+Z'), modelViewer.queryHotspot('hotspot-dot+X-Y-Z'), modelViewer.queryHotspot('hotspot-dim+X-Y'));
drawLine(lines[1], modelViewer.queryHotspot('hotspot-dot+X-Y-Z'), modelViewer.queryHotspot('hotspot-dot+X+Y-Z'), modelViewer.queryHotspot('hotspot-dim+X-Z'));
drawLine(lines[2], modelViewer.queryHotspot('hotspot-dot+X+Y-Z'), modelViewer.queryHotspot('hotspot-dot-X+Y-Z')); // always visible
drawLine(lines[3], modelViewer.queryHotspot('hotspot-dot-X+Y-Z'), modelViewer.queryHotspot('hotspot-dot-X-Y-Z'), modelViewer.queryHotspot('hotspot-dim-X-Z'));
drawLine(lines[4], modelViewer.queryHotspot('hotspot-dot-X-Y-Z'), modelViewer.queryHotspot('hotspot-dot-X-Y+Z'), modelViewer.queryHotspot('hotspot-dim-X-Y'));
requestAnimationFrame(startSVGRenderLoop);
};
startSVGRenderLoop();
});
</script>
<style>
#controls {
position: absolute;
bottom: 16px;
left: 16px;
max-width: unset;
transform: unset;
pointer-events: auto;
}
.dot {
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
background: #fff;
pointer-events: none;
--min-hotspot-opacity: 0;
}
.dim {
background: #fff;
border-radius: 4px;
border: none;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
color: rgba(0, 0, 0, 0.8);
display: block;
font-family: Futura, Helvetica Neue, sans-serif;
font-size: 18px;
font-weight: 700;
max-width: 128px;
overflow-wrap: break-word;
padding: 0.5em 1em;
position: absolute;
width: max-content;
height: max-content;
transform: translate3d(-50%, -50%, 0);
pointer-events: none;
--min-hotspot-opacity: 0;
}
.dimensionLineContainer {
pointer-events: none;
position: fixed;
display: block;
width: 100%;
height: 100%;
}
.dimensionLine {
stroke: #16a5e6;
stroke-width: 2;
stroke-dasharray: 2
}
.show {
--min-hotspot-opacity: 1;
}
.hide {
display: none;
}
/* This keeps child nodes hidden while the element loads */
:not(:defined)>* {
display: none;
}
</style>
body,
html {
margin: 0;
padding: 0;
background: #000;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#dimension-demo {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#controls {
display: none;
}
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.