<bmx-model-viewer>
Shows a 3D model in the page: STL, OBJ (with its MTL colours), glTF and GLB. Turn it, move it and zoom; pick a standard view or an orthographic projection; see it shaded, with its edges, or as an X-ray; cut a section through it; pull its parts apart in an exploded view; hide, isolate and select parts from the parts list; measure between two points; and pin notes to its surface, which the page can save and load as JSON.
17 properties · 5 events · 12 methods · 5 parts
Example
Drag to turn the model, right-drag to move it, scroll to zoom. Try Section, the exploded view and Measure in the toolbar; the parts list hides, isolates and selects parts.
Show markup
<bmx-model-viewer id="ex-mv" label="Camera mount" units="mm" style="--bmx-model-viewer-height: 26rem"></bmx-model-viewer>
<p class="note" id="ex-mv-out" role="status" style="margin: 12px 0 0">Drag to turn the model, right-drag to move it, scroll to zoom. Try Section, the exploded view and Measure in the toolbar; the parts list hides, isolates and selects parts.</p>
<script type="module">
await customElements.whenDefined('bmx-model-viewer');
const viewer = document.getElementById('ex-mv');
const out = document.getElementById('ex-mv-out');
// A small assembly written as OBJ here, so the page needs no model file:
// boxes and cylinders, grouped into parts, coloured by an MTL file.
const lines = ['mtllib mount.mtl'];
let vertices = 0;
let normals = 0;
const r3 = n => Math.round(n * 1000) / 1000;
const vertex = p => (lines.push(`v ${p.map(r3).join(' ')}`), ++vertices);
const normal = n => (lines.push(`vn ${n.map(r3).join(' ')}`), ++normals);
const face = (ids, n) => lines.push(`f ${ids.map((id, i) => `${id}//${Array.isArray(n) ? n[i] : n}`).join(' ')}`);
const part = (object, group, material) => {
if (object) lines.push(`o ${object}`);
lines.push(`g ${group}`, `usemtl ${material}`);
};
const box = (c, s) => {
const p = [];
for (let i = 0; i < 8; i += 1) p.push(vertex([c[0] + ((i & 1 ? 1 : -1) * s[0]) / 2, c[1] + ((i & 2 ? 1 : -1) * s[1]) / 2, c[2] + ((i & 4 ? 1 : -1) * s[2]) / 2]));
const sides = [[[0, 2, 3, 1], [0, 0, -1]], [[4, 5, 7, 6], [0, 0, 1]], [[0, 4, 6, 2], [-1, 0, 0]], [[1, 3, 7, 5], [1, 0, 0]], [[0, 1, 5, 4], [0, -1, 0]], [[2, 6, 7, 3], [0, 1, 0]]];
for (const [ids, n] of sides) face(ids.map(i => p[i]), normal(n));
};
const cylinder = (axis, c, radius, length, segments) => {
const at = (a, end) => {
const u = Math.cos(a) * radius;
const w = Math.sin(a) * radius;
const t = (end * length) / 2;
return axis === 'x' ? [c[0] + t, c[1] + u, c[2] + w] : [c[0] + u, c[1] + t, c[2] + w];
};
const out = a => (axis === 'x' ? [0, Math.cos(a), Math.sin(a)] : [Math.cos(a), 0, Math.sin(a)]);
const low = [];
const high = [];
const ns = [];
for (let i = 0; i < segments; i += 1) {
const a = (i / segments) * Math.PI * 2;
low.push(vertex(at(a, -1)));
high.push(vertex(at(a, 1)));
ns.push(normal(out(a)));
}
for (let i = 0; i < segments; i += 1) {
const j = (i + 1) % segments;
if (segments > 8) face([low[i], low[j], high[j], high[i]], [ns[i], ns[j], ns[j], ns[i]]);
else face([low[i], low[j], high[j], high[i]], normal(out(((i + 0.5) / segments) * Math.PI * 2)));
}
const axisNormal = axis === 'x' ? [1, 0, 0] : [0, 1, 0];
face(high, normal(axisNormal));
face([...low].reverse(), normal(axisNormal.map(n => -n)));
};
part('Base', 'Base plate', 'anodised');
box([0, 4, 0], [140, 8, 90]);
[[-58, -33], [58, -33], [-58, 33], [58, 33]].forEach(([x, z], i) => {
part('', `Bolt ${i + 1}`, 'steel');
cylinder('y', [x, 11, z], 6, 6, 6);
});
part('Frame', 'Left upright', 'anodised');
box([-45, 40, 0], [10, 64, 44]);
part('', 'Right upright', 'anodised');
box([45, 40, 0], [10, 64, 44]);
part('Tilt head', 'Shaft', 'steel');
cylinder('x', [0, 56, 0], 6, 116, 32);
part('', 'Clamp', 'black');
box([0, 56, 0], [36, 26, 34]);
part('', 'Camera plate', 'black');
box([0, 72, 0], [60, 6, 50]);
part('', 'Knob', 'brass');
cylinder('x', [64, 56, 0], 14, 10, 24);
const mtl = 'newmtl anodised\nKd 0.2 0.42 0.74\nnewmtl steel\nKd 0.68 0.7 0.74\nnewmtl black\nKd 0.16 0.17 0.19\nnewmtl brass\nKd 0.82 0.64 0.3\n';
viewer.addEventListener('bmxSelect', e => (out.textContent = e.detail.part ? `Selected: ${e.detail.name}.` : 'Nothing selected.'));
viewer.addEventListener('bmxMeasure', e => (out.textContent = `Measured ${e.detail.distance.toFixed(1)} ${e.detail.units}.`));
viewer.addEventListener('bmxChange', e => (out.textContent = `${e.detail.annotations.length} note(s) - save them as JSON with the model.`));
await viewer.load(lines.join('\n'), { format: 'obj', name: 'Camera mount', resolve: async () => new TextEncoder().encode(mtl).buffer });
viewer.annotations = [
{ id: 'n1', text: 'Hand-tighten the knob; 2 N·m at most.', position: [69, 56, 0], part: 'Knob' },
{ id: 'n2', text: 'M6 bolts into the bench, 4 off.', position: [58, 14, 33], part: 'Bolt 4' },
];
</script>
<bmx-model-viewer src="/models/bracket.glb" units="mm" label="Mounting bracket"></bmx-model-viewer>
Everything runs in the browser with WebGL 2; nothing is uploaded. Drawing happens only when something changes, so a still model costs nothing.
Keyboard: the view takes focus with Tab. Arrow keys turn the model, Shift with the arrows moves it, plus and minus zoom, Home fits it, 1 to 7 choose front, back, left, right, top, bottom and isometric, P switches projection. Measuring, adding notes and selecting work from the keyboard too: Enter picks the point under the cross in the middle of the view. The parts list is a tree: Space shows or hides a part, Enter selects it.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
allowOpen |
allow-open |
boolean |
false |
Offer an Open button, and open model files dropped on the view. |
annotations |
annotations |
BmxModelAnnotation[] | string |
[] |
Notes pinned to the model, as an array or JSON. Two-way: bmxChange reports edits. |
explode |
explode |
number |
0 |
How far apart the parts are pulled, 0 to 1. Two-way. |
format |
format |
BmxModelFormat |
— | The format, when the address does not end in .stl, .obj, .gltf or .glb. |
label |
label |
string |
— | The model's name for assistive technology and the toolbar. Default: its file name. |
panel |
panel |
boolean |
true |
Show the side panel (parts, notes, measurements). Two-way (the toolbar's panel button). |
projection |
projection |
'perspective' | 'orthographic' |
'perspective' |
Two-way. |
readonly |
readonly |
boolean |
false |
Notes can be read but not added, changed or removed. |
renderMode |
render-mode |
BmxModelRenderMode |
'shaded' |
How surfaces are drawn. Two-way. |
section |
section |
'none' | 'x' | 'y' | 'z' |
'none' |
A section through the model across this axis, or none. Two-way. |
sectionFlip |
section-flip |
boolean |
false |
Keep the other side of the cut. Two-way. |
sectionPosition |
section-position |
number |
0.5 |
Where the cut is, from 0 (one end of the model) to 1 (the other). Two-way. |
src |
src |
string |
— | The model's address. Files it refers to (a glTF's .bin and textures, an OBJ's .mtl) are fetched beside it. |
strings |
strings |
Partial<BmxModelViewerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
toolbar |
toolbar |
boolean |
true |
Show the toolbar. |
units |
units |
string |
'' |
The unit lengths are in, shown after measurements and sizes (mm, in). |
upAxis |
up-axis |
'auto' | 'y' | 'z' |
'auto' |
Which way is up in the file. auto: Z for STL (as most CAD exports it), Y otherwise. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
{ readonly annotations: BmxModelAnnotation[]; } |
The notes changed: added, edited or removed. |
bmxError |
{ readonly message: string; } |
A model could not be read. |
bmxLoad |
BmxModelLoadDetail |
A model was read and shown. |
bmxMeasure |
BmxModelMeasureDetail |
A measurement was taken. |
bmxSelect |
{ readonly part: string | null; readonly name: string; } |
The selected part changed (part is null when nothing is selected). |
Methods
| Method | Signature | Description |
|---|---|---|
clearMeasurements |
clearMeasurements() => Promise<void> |
Removes every measurement. |
fit |
fit(part?: string) => Promise<void> |
Fits the camera to the whole model, or to one part. |
getCamera |
getCamera() => Promise<BmxModelCamera> |
|
getParts |
getParts() => Promise<BmxModelPartInfo[]> |
The model's parts, in tree order. |
isolate |
isolate(part: string) => Promise<void> |
Shows one part alone. |
load |
load(data: ArrayBuffer | Uint8Array | string | Blob | readonly File[], options?: BmxModelLoadOptions) => Promise<void> |
Shows a model from its data: bytes, text, a File or Blob, or several files at once (a glTF with its .bin and textures, an OBJ with its .mtl), the main one found by its extension. |
screenshot |
screenshot(options?: { type?: "image/png" | "image/jpeg" | "image/webp"; quality?: number; background?: boolean; }) => Promise<Blob | null> |
A picture of the view, as drawn now (without the notes' pins). |
select |
select(part: string | null) => Promise<void> |
Selects a part (highlighted, and reported by bmxSelect), or nothing. |
setCamera |
setCamera(camera: Partial<BmxModelCamera>) => Promise<void> |
Moves the camera (a saved view, for example). Parts left out keep their values. |
setPartVisible |
setPartVisible(part: string, visible: boolean) => Promise<void> |
Shows or hides a part and everything in it. |
setView |
setView(view: BmxModelView) => Promise<void> |
Looks from a standard direction, keeping what is looked at. |
showAll |
showAll() => Promise<void> |
Shows every part. |
CSS shadow parts
| Part | Description |
|---|---|
panel |
The side panel (parts, notes, measurements). |
pin |
A note's pin on the model. |
stage |
The area the model is drawn in. |
status |
The line under the view. |
toolbar |
The bar of commands. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-model-viewer-background |
Behind the model. |
--bmx-model-viewer-cap |
The inside of a section cut. |
--bmx-model-viewer-edge |
Edge lines. |
--bmx-model-viewer-height |
The height of the view. |
--bmx-model-viewer-highlight |
The selected part's tint, and its edges. |
--bmx-model-viewer-measure |
Measurement lines and points. |
--bmx-model-viewer-panel-width |
The side panel's width. |
--bmx-model-viewer-pin |
A note's pin. |
--bmx-model-viewer-radius |
Corner radius of the frame. |
--bmx-model-viewer-surface |
The colour a saved picture is laid on. |