Parameters
Set constructor options once and use methods for later changes. Optional arguments in signatures use a question mark; omitted fields retain their documented defaults.
Initialization
new StlViewer(container, options = {})
Pass a DOM element with a usable height. The second argument is an object, not an array. Browser viewing requires WebGL 2. These defaults describe the standalone plugin; the ViewSTL website supplies its own configuration.
| Parameter | Type / default | Meaning |
|---|---|---|
models |
ModelOptions[], empty |
Models to start loading after initialization. See Models below. |
width, height |
CSS string or pixel number; "100%" |
Canvas dimensions. Override canvas_width, canvas_height aliases. Give the parent an explicit height when using a percentage. |
canvas_width, canvas_height |
Same types | Legacy aliases, used only when width/height are omitted. |
bg_color, bgcolor |
Color string; "transparent" |
Background. bg_color takes precedence. Use a hex color such as "#ffffff". |
camerax, cameray, cameraz |
Numbers; 0, 0, 100 |
Initial camera position. Automatic fitting after loading may replace it. |
camera_state |
CameraState, omitted | {position:{x,y,z}, up:{x,y,z}, target:{x,y,z}}; restores the initial camera after loading. |
zoom |
Number; -1 |
-1 fits loaded models automatically. A nonnegative value sets camera Z; it is not a magnification factor. |
center_models |
Boolean; true |
Center each subsequently loaded model around its own bounds. Set false to retain source coordinates, as the preview site does. |
auto_resize |
Boolean; true |
Observe container/window resizing and update the drawing surface. This does not mean auto-fit. |
auto_rotate |
Boolean; false |
Rotate the camera continuously; separate from per-model animation. Provide a pause control if enabled. |
mouse_zoom |
Boolean; true |
Enable wheel/pinch camera zoom. |
controls |
0 or 1; 0 |
Native orbit (0) or trackball-style (1) controls; no Three.js control objects. |
allow_drag_and_drop |
Boolean; true |
Enable the low-level canvas local-file drop handler. Use import_scene for transactional scene/companion handling. |
grid |
Boolean; false |
Initial finite grid. For the infinite grid use set_infinite_grid. |
send_no_model_click_event |
Boolean; false |
Also report misses to the pointer callback, with id null and distance 0. |
pixel_ratio |
Positive number; device ratio capped at 2 | Drawing resolution; increasing it costs GPU memory and rendering time. |
worker_url |
String or URL; beside ESM/classic entry | Path to viewstl.worker.js. Set explicitly for CommonJS/bundled integration if automatic resolution is unavailable. Keep companion chunks/CAD assets together. |
worker_factory |
Function returning Worker; omitted | Custom worker construction. The worker must run the matching ViewSTL worker implementation. |
worker_count |
Integer 1–4; 2 |
Parser/worker pool size. |
worker_timeout |
Positive milliseconds; 120000 |
Worker job timeout. |
url_import_endpoint |
Same-origin path/URL; omitted | Your compatible streaming relay for remote_url and URL-based import_scene. The plugin does not configure or contact ViewSTL’s relay automatically. |
max_file_bytes |
Positive integer; 268435456 |
256 MiB input/output file ceiling. |
max_triangles |
Positive integer; 5000000 |
Triangle ceiling. |
max_uncompressed_bytes |
Positive integer; 268435456 |
256 MiB archive expansion ceiling. |
max_archive_entries |
Positive integer; 1024 |
Archive entry ceiling. |
max_memory_bytes |
Positive integer; Number.MAX_SAFE_INTEGER |
Optional estimated working-set budget, not a measurement of available RAM. |
three |
Host-supplied Three.js namespace; omitted | Optional bridge for get_model_mesh; also checks globalThis.THREE. Native rendering never imports Three.js. |
load_three_files |
String or false; omitted | Deprecated worker-directory alias, relative to the page. It never loads Three.js. Explicit worker_url wins; false/omitted uses normal entry-point resolution. |
jszip_path, jszip_utils_path |
Deprecated strings | Accepted with no effect. Archive dependencies are supplied as lazy package chunks. |
| Callbacks | Functions; omitted | See the individually linked Events / Callbacks entries below. |
// The page contains <div id="viewer" style="height:500px"></div>.
const viewer = new StlViewer(document.getElementById('viewer'), {
center_models: false,
bg_color: '#ffffff',
models: [{ id: 0, filename: '/model.stl' }],
load_error_callback: message => console.error(message)
});
await viewer.when_idle();
View code
Copy the complete build/ folder from the plugin download beside this HTML file, add the models below, and serve the folder over HTTP(S). This is the same example program with local file paths.
These are the original legacy ViewSTL example models. Save the required files in a models/ folder beside your HTML: viewstl_plugin.stl.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ViewSTL — Initialization options</title>
<style>
body { font-family: system-ui, sans-serif; margin: 20px; }
#example-viewer { width: 100%; height: 500px; }
button { padding: 10px 16px; }
</style>
</head>
<body>
<div id="example-viewer"></div>
<p id="example-status" role="status">Opening example…</p>
<button id="example-motion" type="button" hidden>Pause camera rotation</button>
<button id="example-retry" type="button" hidden>Retry example</button>
<script type="module">
import StlViewer from './build/viewstl.js';
const container = document.getElementById('example-viewer');
const status = document.getElementById('example-status');
const retry = document.getElementById('example-retry');
const modelURL = new URL('./models/viewstl_plugin.stl', import.meta.url).href;
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
let loadError = null;
function showError(message) {
loadError = message;
status.textContent = message;
retry.hidden = false;
}
retry.addEventListener('click', () => location.reload());
const viewer = new StlViewer(container, {
bg_color: '#e1f3ed',
auto_resize: true,
auto_rotate: !reducedMotion.matches,
allow_drag_and_drop: true,
models: [{ id: 0, filename: modelURL, color: '#428a83' }],
load_error_callback: showError
});
const setPlaying = enabled => viewer.set_auto_rotate(enabled);
// Refit through the plugin when the example container changes width.
let lastWidth = container.clientWidth;
const resize = new ResizeObserver(() => {
if (container.clientWidth === lastWidth) return;
lastWidth = container.clientWidth;
viewer.do_resize();
viewer.set_auto_zoom();
});
resize.observe(container);
try {
await viewer.when_idle();
if (loadError) throw new Error(loadError);
viewer.set_camera_view('isometric');
status.textContent = 'Ready. Drag to rotate, scroll to zoom. Keyboard: arrows, + / − and F.';
const motionButton = document.getElementById('example-motion');
let playing = !reducedMotion.matches;
function updateMotion() {
motionButton.textContent = (playing ? 'Pause' : 'Resume') + ' camera rotation';
motionButton.setAttribute('aria-pressed', String(playing));
}
motionButton.hidden = false;
updateMotion();
motionButton.addEventListener('click', () => {
playing = !playing;
setPlaying(playing);
updateMotion();
});
reducedMotion.addEventListener('change', event => {
if (event.matches) {
playing = false;
setPlaying(false);
updateMotion();
}
});
} catch (error) {
showError(error.message);
viewer.dispose();
}
addEventListener('pagehide', event => { if (!event.persisted) { resize.disconnect(); viewer.dispose(); } });
</script>
</body>
</html>Models
models: [{ ...modelOptions }]
Use one geometry source per model. STL/OBJ coordinates are unitless; 3MF/STEP/IGES units are normalized to millimetres during parsing. OBJ material/texture companion loading is not supported.
| Parameter | Type / default | Meaning |
|---|---|---|
id |
Nonnegative safe integer; automatic | Omit or use -1 for the next ID. Zero is valid; duplicate IDs fail. |
filename |
String | Direct model URL/path. Same-origin or CORS permissions are needed. Also the source reference saved by VSJ. |
local_file |
File or Blob | A browser-selected local file, not a filesystem path or an HTML input element. For a nameless Blob, supply filename for format detection. |
remote_url, remote_format |
Public URL; optional format | Use the configured URL relay. Format: stl, obj, 3mf, step/stp, iges/igs; empty/omitted infers it. |
data |
GeometryData | Copied triangle positions (9 numbers per triangle) plus optional RGB colors (one RGB per vertex, 0–1). Float32Array or number arrays. |
mesh |
Static supported mesh | Optional existing Three.js mesh input; see the mesh-bridge guide for restrictions. Native data is usually simpler. |
orig_filename, display_name |
Optional strings | Original-name metadata and a separate display label. display_name does not replace the VSJ source filename. |
x, y, z |
Finite numbers; 0 |
Scene translation in model-coordinate units, normally mm. |
rotationx, rotationy, rotationz |
Finite radians; 0 |
Model rotation. |
scale |
Positive number; 1 |
Uniform scale. |
scalex, scaley, scalez |
Positive numbers; scale or 1 | Per-axis scale overrides. |
display |
String; "flat" |
flat, smooth, wireframe, edges, mesh, xray, studio or orientation. |
smooth_shading |
Boolean; true for smooth | Independent smoothing for the modern view modes. |
color |
Hex string or null; null |
Null preserves imported colors (or default gray). A hex value overrides them; transparent is a compatibility color value. |
opacity |
Number 0–1; 1 |
Transparency; zero is valid. A mesh input can supply material opacity. |
view_edges |
Boolean; false |
Legacy feature-edge overlay. |
section |
Section or null; null |
{axis:"x"/"y"/"z", position:0..1, flip:false}; open visual cut, not geometry editing. |
units |
mm or inch; "mm" |
Legacy model-unit label/convention; does not by itself reinterpret unitless coordinates. Use set_file_units for that. |
animation |
Animation or null | Per-model motion; see Animation parameters. |
animation_state, size_state |
Serialized scene metadata | Normally generated by scene saving; retain when round-tripping rather than construct manually. |
viewer.add_models([
{ id: 1, filename: '/part.stl', color: '#498ba6', x: 30 },
{ id: 2, filename: '/part.obj', scale: 2, opacity: 0.7 }
]);
await viewer.when_idle();
View code
Copy the complete build/ folder from the plugin download beside this HTML file, add the models below, and serve the folder over HTTP(S). This is the same example program with local file paths.
These are the original legacy ViewSTL example models. Save the required files in a models/ folder beside your HTML: viewstl_plugin.stl · bat.stl.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ViewSTL — Model options</title>
<style>
body { font-family: system-ui, sans-serif; margin: 20px; }
#example-viewer { width: 100%; height: 500px; }
button { padding: 10px 16px; }
</style>
</head>
<body>
<div id="example-viewer"></div>
<p id="example-status" role="status">Opening example…</p>
<button id="example-retry" type="button" hidden>Retry example</button>
<script type="module">
import StlViewer from './build/viewstl.js';
const container = document.getElementById('example-viewer');
const status = document.getElementById('example-status');
const retry = document.getElementById('example-retry');
const modelURL = new URL('./models/viewstl_plugin.stl', import.meta.url).href;
const secondModelURL = new URL('./models/bat.stl', import.meta.url).href;
let loadError = null;
function showError(message) {
loadError = message;
status.textContent = message;
retry.hidden = false;
}
retry.addEventListener('click', () => location.reload());
const viewer = new StlViewer(container, {
models: [
{ id: 0, filename: modelURL, x: -55, scale: 1.5,
color: '#db7667', opacity: 0.6, rotationx: 0.2, display: 'edges' },
{ id: 1, filename: secondModelURL, x: 55, scale: 2,
color: '#e5b54c', display: 'smooth' }
],
load_error_callback: showError
});
// Refit through the plugin when the example container changes width.
let lastWidth = container.clientWidth;
const resize = new ResizeObserver(() => {
if (container.clientWidth === lastWidth) return;
lastWidth = container.clientWidth;
viewer.do_resize();
viewer.set_auto_zoom();
});
resize.observe(container);
try {
await viewer.when_idle();
if (loadError) throw new Error(loadError);
viewer.set_camera_view('isometric');
status.textContent = 'Ready. Drag to rotate, scroll to zoom. Keyboard: arrows, + / − and F.';
} catch (error) {
showError(error.message);
viewer.dispose();
}
addEventListener('pagehide', event => { if (!event.persisted) { resize.disconnect(); viewer.dispose(); } });
</script>
</body>
</html>Animation parameters
animation: { delta?: track, exact?: track }
delta adds changes to the starting pose; exact specifies target values. They can run together on different properties, but cannot claim the same property. Rotations use radians and translation uses scene coordinates, not pixels.
| Parameter | Type / default | Meaning |
|---|---|---|
msec |
Positive milliseconds; 300 |
Duration of this track. |
loop |
Boolean; false |
Continuous accumulation for delta only. Exact tracks cannot loop. |
x, y, z |
Finite numbers | Translation deltas or target coordinates. Omitted fields are unchanged. |
rotationx, rotationy, rotationz |
Finite radians | Rotation deltas or target angles. |
scale, scalex, scaley, scalez |
Finite scale values | Delta scale is additive; exact scale is absolute. Resulting scales must remain positive. Negative looping scale deltas are rejected. |
Manual transform/size changes stop that model’s animation. VSJ/VSB preserve definitions, phase and paused state. Camera auto-rotation is separate.
viewer.animate_model(0, {
delta: { rotationz: 2 * Math.PI, msec: 5000, loop: true }
});
viewer.pause_model_animation(0);
viewer.resume_model_animation(0);
View code
Copy the complete build/ folder from the plugin download beside this HTML file, add the models below, and serve the folder over HTTP(S). This is the same example program with local file paths.
These are the original legacy ViewSTL example models. Save the required files in a models/ folder beside your HTML: knot.stl · bar.stl.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ViewSTL — Model animation</title>
<style>
body { font-family: system-ui, sans-serif; margin: 20px; }
#example-viewer { width: 100%; height: 500px; }
button { padding: 10px 16px; }
</style>
</head>
<body>
<div id="example-viewer"></div>
<p id="example-status" role="status">Opening example…</p>
<button id="example-motion" type="button" hidden>Pause animation</button>
<button id="example-retry" type="button" hidden>Retry example</button>
<script type="module">
import StlViewer from './build/viewstl.js';
const container = document.getElementById('example-viewer');
const status = document.getElementById('example-status');
const retry = document.getElementById('example-retry');
const modelURL = new URL('./models/knot.stl', import.meta.url).href;
const secondModelURL = new URL('./models/bar.stl', import.meta.url).href;
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
let loadError = null;
function showError(message) {
loadError = message;
status.textContent = message;
retry.hidden = false;
}
retry.addEventListener('click', () => location.reload());
const animations = [
{ delta: { rotationz: 2 * Math.PI, msec: 6000, loop: true } },
{
delta: { rotationx: 0.5, rotationy: 0.7, rotationz: 1, msec: 4000, loop: true },
exact: { x: 110, y: 10, z: 10, scale: 0.9, msec: 3500 }
}
];
const viewer = new StlViewer(container, {
models: [
{ id: 0, filename: modelURL, x: -90, scale: 0.55, color: '#64a5b2', display: 'smooth',
animation: reducedMotion.matches ? null : animations[0] },
{ id: 1, filename: secondModelURL, x: 90, scale: 0.7, color: '#e79a65',
animation: reducedMotion.matches ? null : animations[1] }
],
load_error_callback: showError
});
function setPlaying(enabled) {
animations.forEach((definition, id) => {
const state = viewer.get_model_animation_state(id);
if (enabled && state?.status === 'idle') viewer.animate_model(id, definition);
else if (enabled) viewer.resume_model_animation(id);
else viewer.pause_model_animation(id);
});
}
// Refit through the plugin when the example container changes width.
let lastWidth = container.clientWidth;
const resize = new ResizeObserver(() => {
if (container.clientWidth === lastWidth) return;
lastWidth = container.clientWidth;
viewer.do_resize();
viewer.set_auto_zoom();
});
resize.observe(container);
try {
await viewer.when_idle();
if (loadError) throw new Error(loadError);
viewer.set_camera_view('isometric');
status.textContent = 'Ready. Drag to rotate, scroll to zoom. Keyboard: arrows, + / − and F.';
const motionButton = document.getElementById('example-motion');
let playing = !reducedMotion.matches;
function updateMotion() {
motionButton.textContent = (playing ? 'Pause' : 'Resume') + ' animation';
motionButton.setAttribute('aria-pressed', String(playing));
}
motionButton.hidden = false;
updateMotion();
motionButton.addEventListener('click', () => {
playing = !playing;
setPlaying(playing);
updateMotion();
});
reducedMotion.addEventListener('change', event => {
if (event.matches) {
playing = false;
setPlaying(false);
updateMotion();
}
});
} catch (error) {
showError(error.message);
viewer.dispose();
}
addEventListener('pagehide', event => { if (!event.persisted) { resize.disconnect(); viewer.dispose(); } });
</script>
</body>
</html>Export options
ExportOptions / SceneExportOptions
| Parameter | Type / default | Meaning |
|---|---|---|
format |
Required string | Model: stl, obj, 3mf, step/stp, iges/igs. Scene: vsb or vsj. |
model_ids |
Number array or selected; all if omitted | Model exports only. For selected, call set_selection first. Scene exports always include the whole scene. |
filename |
Optional string | The modern exporter supplies/replaces the extension. |
units |
mm or inch; mm | Model output units. Scene coordinates use mm; inch output divides coordinates by 25.4. |
apply_transforms |
Boolean; true | Model export: bake current pose/scale/centering. False uses source coordinates plus accumulated unit conversion only. |
colors |
Boolean; true | OBJ/3MF color export. Colored OBJ is a ZIP with OBJ and MTL; ignored for STL/STEP/IGES. |
force_basename |
Boolean; false | VSJ only: strip paths/query strings from file references. Companions must still be supplied when reopening. |
signal |
AbortSignal | Cancel an active/queued modern export. |
on_progress |
Function | Model export: loaded/total triangles, phase exporting/complete. Scene: loaded/total models, phase exporting/packaging/complete. |
Call when_idle first. Errors reject modern export Promises, rather than invoking load_error_callback. STL/OBJ have no standard unit metadata. STEP/IGES contain triangle surfaces, without reconstructed CAD solids, history or analytic curves. 3MF requires closed, consistently oriented geometry. Local export does not upload files.
const controller = new AbortController();
const result = await viewer.export_models({
format: 'obj', model_ids: [0], colors: false,
signal: controller.signal, on_progress: p => console.log(p.phase)
});
console.log(result.filename, result.blob.size);
Scene import options
import_scene(source, options = {})
| Parameter | Type / default | Meaning |
|---|---|---|
source |
File, Blob or public HTTP/HTTPS URL | VSB/VSJ source. URL sources require the explicitly configured same-origin relay. |
format |
vsb or vsj; inferred | Set for a nameless Blob or an extensionless URL. |
files |
File[]; empty | Local companion models referenced by a VSJ. Match by relative path or unambiguous basename. |
signal |
AbortSignal | Cancellation preserves the existing scene. |
on_progress |
Function | Receives loaded, total and phase (reading-scene, opening-model, complete). |
Incoming models are staged and appended with new IDs after all succeed. Errors or cancellation preserve the existing scene. Saved camera, presentation, bookmarks and animation are restored; embedding canvas dimensions and resize/mouse controls are not imported. A VSB embeds meshes; a VSJ needs its companion sources.
// Round-trip a self-contained scene without a server.
const saved = await viewer.export_scene({ format: 'vsb' });
const imported = await viewer.import_scene(saved.blob, { format: 'vsb' });
console.log(imported.model_ids);
Print-check options
check_printability(options = {})
| Parameter | Type / default | Meaning |
|---|---|---|
model_ids |
Number array or selected; all if omitted | Loaded models to check. |
level |
basic or detailed; basic | Detailed adds intersection, containment and validated-volume checks. |
apply_transforms |
Boolean; true | Check current pose/scale. |
units_confirmed |
Boolean; false | Confirm physical scale for unitless input; this does not resize geometry. |
profile |
Optional object | technology: fdm; optional build_volume_mm [x,y,z], min_wall_mm, line_width_mm and overhang_degrees measured from vertical. No printer defaults are assumed. |
max_time_ms |
Positive milliseconds; 30000, max 120000 | Per-model time budget. Inconclusive checks remain incomplete. |
signal, on_progress |
AbortSignal; function | Cancel or observe phase, model_id/model_index, total_models and optional elapsed_ms. |
Results are bounded mesh checks, not a print guarantee or repair. Basic results remain incomplete because detailed checks were not run. Wall sampling cannot certify a minimum thickness.
const report = await viewer.check_printability({
model_ids: [0], level: 'detailed', units_confirmed: true
});
console.log(report.summary, report.models[0].checks);
Viewer properties
viewer.when_ready, viewer.ready, viewer.error, …
when_ready is a Promise, not a method. ready reports renderer initialization; error holds the most recent reported error message (it is not reset after every successful operation). models_count includes queued/loading models. load_session increases when a loading batch settles. canvas is the owned canvas element. controls.enabled enables/disables pointer/keyboard camera controls; use set_mouse_zoom for zoom. display_units is managed by set_display_units. Static StlViewer.version and StlViewer.workerURL expose build version and worker resolution. Internal renderer/camera/models are native objects; do not call Three.js internals on them.
await viewer.when_ready;
console.log(StlViewer.version, viewer.ready, viewer.models_count);
viewer.controls.enabled = true;
Methods
Examples assume a ready viewer named viewer with model ID 0 loaded, unless the example loads its own models. Replace example file paths with your own files. Angles are radians. Most legacy setters report errors through load_error_callback; newer size/bookmark APIs can throw and asynchronous jobs reject. Do not rely on setters returning a success flag.
when_idle
when_idle() → Promise<StlViewer>
Wait for initialization, pending model/scene imports and derived view geometry. Individual load failures are reported through load_error_callback; a settled batch does not prove every model loaded. It does not wait for animation, modern exports or analysis to finish.
await viewer.when_idle();
console.log(viewer.get_model_info(0, { measurements: false }));
add_model
add_model(model) → void
Queue one model. Choose an explicit unique ID if you need to reference it later. Loading is asynchronous: awaiting add_model itself does not wait for completion. Errors use load_error_callback. VSJ/VSB filenames use the legacy scene loaders; prefer import_scene for staged imports.
viewer.add_model({ id: 1, filename: '/part.stl' });
await viewer.when_idle();
add_models
add_models(models) → void
Queue an array of model descriptors. This is not an atomic batch: successful models remain if another fails.
viewer.add_models([{ id: 1, filename: '/a.stl' }, { id: 2, filename: '/b.obj' }]);
await viewer.when_idle();
remove_model
remove_model(id) → void
Remove one model and cancel its pending work. Other models remain.
viewer.remove_model(0);
set_display
set_display(id, display) → void
Choose flat, smooth, wireframe, edges, mesh, xray, studio or orientation. Flat/smooth also set their legacy shading state. Derived geometry may load asynchronously; a failed preparation preserves the previous view.
viewer.set_display(0, 'smooth');
await viewer.when_idle();
set_view
set_view(id, {display?, smooth_shading?}) → void
Update mode and smoothing together. Omitted properties retain their current/requested values.
viewer.set_view(0, { display: 'studio', smooth_shading: true });
await viewer.when_idle();
set_smooth
set_smooth(id, enabled) → void
Change smoothing independently for modern modes. It is visually ignored in wireframe but retained for the next surface view.
viewer.set_smooth(0, true);
await viewer.when_idle();
set_section
set_section(id, sectionOrNull) → void
Section axis is x/y/z; position is 0–1 across source bounds, with optional flip. Pass null to clear. This open visual cut affects picking/PNG but not exported geometry or mesh measurements.
viewer.set_section(0, { axis: 'z', position: 0.5, flip: false });
// viewer.set_section(0, null);
set_color
set_color(id, colorOrNull) → void
Set a hex color override, or null to restore source colors/default gray. transparent is accepted for compatibility; set_opacity gives numeric transparency control.
viewer.set_color(0, '#357da1');
set_opacity
set_opacity(id, opacity) → void
Set opacity from 0 (invisible) to 1 (opaque). Rendering of intersecting transparent surfaces is approximate.
viewer.set_opacity(0, 0.5);
set_position
set_position(id, x?, y?, z?) → void
Set absolute translation; undefined axes stay unchanged. Stops this model’s animation.
viewer.set_position(0, 20, undefined, 10);
set_scale
set_scale(id, x, y = x, z = x, keepPrevious = false) → void
Set positive absolute axis scales about the existing pivot. A single factor scales all axes. The legacy keepPrevious option preserves reported scales while applying a uniform geometry factor; x/currentX, y/currentY and z/currentZ must agree. Prefer resize_model for size changes that preserve the world-space center.
viewer.set_scale(0, 2);
set_rotation
set_rotation(id, x?, y?, z?, increment = false) → void
Set radians on supplied axes, or add them when increment is true. Omitted axes are unchanged. Stops this model’s animation.
viewer.set_rotation(0, 0, 0, Math.PI / 2);
rotate
rotate(id, x?, y?, z?) → void
Add rotation in radians to the supplied axes. This is incremental, unlike the default set_rotation.
viewer.rotate(0, 0, 0, Math.PI / 4);
set_edges
set_edges(id, enabled) → void
Toggle the legacy one-degree feature-edge overlay. The modern edges view uses a 30-degree threshold. Preparation can run in a worker.
viewer.set_edges(0, true);
await viewer.when_idle();
set_grid
set_grid(enabled, size?, divisions = 10) → void
Show/hide a finite grid. Omit size to cover the scene; explicit size must be positive. Divisions is an integer from 1 to 1000. This replaces any infinite grid.
viewer.set_grid(true, 100, 10);
set_infinite_grid
set_infinite_grid(enabled, spacing = 1, height = 0) → void
Show/hide the infinite grid. Positive spacing and finite height are scene-coordinate values. It is a visual guide, not a print bed or a model transform.
viewer.set_infinite_grid(true, 10, 0);
set_selection
set_selection(idOrNull) → void
Show the editing frame for one model; null clears it. Also supplies the selected scope for export/check APIs. Frames are excluded from exported images and scenes.
viewer.set_selection(0);
pick_model
pick_model(clientX, clientY) → {id, distance} | null
Pick the nearest visible model surface at viewport coordinates, such as event.clientX/Y. Honors visibility and sections. Returns null for no hit.
const rect = viewer.canvas.getBoundingClientRect();
console.log(viewer.pick_model(rect.x + rect.width / 2, rect.y + rect.height / 2));
set_model_units
set_model_units(id, units, scale = false) → void
Legacy signature requires the model ID. units is mm or inch. With scale false, updates the legacy unit convention; true converts geometry only when the label changes (×25.4 for inch, ÷25.4 for mm). For source-unit correction use set_file_units; for readouts use set_display_units.
viewer.set_model_units(0, 'inch', false);
get_model_size
get_model_size(id) → ModelSize
Get dimensions_mm, baseline_mm, file_units, editable and unit_factor metadata. Dimensions are axis sizes before model rotation. Throws on an unknown/not-ready model.
console.log(viewer.get_model_size(0).dimensions_mm);
resize_model
resize_model(id, [xFactor, yFactor, zFactor]) → ModelSize
Multiply size by positive factors about the model’s world-space center. Preserves placement/rotation and stops this model’s animation. Invalid input throws.
const size = viewer.resize_model(0, [2, 2, 2]);
console.log(size.dimensions_mm);
set_file_units
set_file_units(id, units) → ModelSize
Reinterpret a unitless STL/OBJ source as mm or inch while preserving its center. Reversing the choice reverses the conversion. Files with explicit CAD/3MF units cannot be reinterpreted; invalid requests throw.
// For a unitless STL/OBJ authored in inches:
viewer.set_file_units(0, 'inch');
reset_model_size
reset_model_size(id) → ModelSize
Restore the recorded size baseline, keeping source-unit correction and world-space center. Stops this model’s animation.
viewer.reset_model_size(0);
duplicate_model
duplicate_model(id) → number
Return a new model ID at the same pose, with independent appearance/transforms. Immutable geometry is shared. Animation definitions/phase are copied paused. Move the copy to see it separately.
const copyId = viewer.duplicate_model(0);
viewer.set_position(copyId, 50, 0, 0);
set_display_units
set_display_units(units) → void
Choose mm/inch for optional app readouts and scene metadata. Geometry, scene position values and grid spacing are unchanged.
viewer.set_display_units('inch');
set_bg_color
set_bg_color(color) → void
Set the canvas background to a hex color or transparent.
viewer.set_bg_color('#ffffff');
set_center_models
set_center_models(enabled) → void
Control centering of subsequently loaded models only. Existing model geometry/positions are unchanged.
viewer.set_center_models(false);
set_zoom
set_zoom(value = -1) → void
-1 enables automatic fitting. A nonnegative value sets camera Z (clamped above zero), not an optical zoom multiplier.
viewer.set_zoom(-1);
set_auto_zoom
set_auto_zoom() → void
Fit the loaded bounds, preserving the viewing direction, and enable automatic fitting after later load batches.
viewer.set_auto_zoom();
set_auto_rotate
set_auto_rotate(enabled) → void
Start/stop continuous camera rotation. This does not animate model transforms. Hosts should provide pause and honor reduced-motion preferences.
viewer.set_auto_rotate(false);
set_mouse_zoom
set_mouse_zoom(enabled) → void
Enable/disable wheel and pinch zoom in native camera controls.
viewer.set_mouse_zoom(true);
set_auto_resize
set_auto_resize(enabled) → void
Enable/disable resize observation. Does not fit models; use set_auto_zoom for fitting.
viewer.set_auto_resize(true);
do_resize
do_resize() → void
Refresh the drawing surface after a layout change, especially when automatic resize is disabled.
viewer.do_resize();
set_on_model_mouseclick
set_on_model_mouseclick(callbackOrNull) → void
Install the legacy-named pointer callback, replacing any existing pointer handler. Alias of set_on_model_mousedown; pass null to remove it.
viewer.set_on_model_mouseclick((id, event, distance, type) => {
if (id !== null) viewer.set_selection(id);
});
set_on_model_mousedown
set_on_model_mousedown(callbackOrNull) → void
Install the shared pointer handler. It receives pointer-down and double-click/touch notifications, not only a completed click. See event type codes under on_model_mousedown.
viewer.set_on_model_mousedown((id, event, distance, type) => {
console.log(id, type, distance);
});
set_drag_and_drop
set_drag_and_drop(enabled) → void
Toggle the low-level canvas local-file drop listeners. They call add_model for each file; the optional app provides richer URL/scene-companion handling.
viewer.set_drag_and_drop(true);
get_camera_state
get_camera_state() → CameraState
Return a copy of position, up and target vectors, each with x/y/z numbers.
const camera = viewer.get_camera_state();
console.log(camera.position);
set_camera_state
set_camera_state(state) → void
Restore any supplied position/up/target vectors; omitted vectors remain unchanged. Up must be nonzero and position must differ from target.
const camera = viewer.get_camera_state();
viewer.set_camera_state(camera);
set_camera
set_camera(x?, y?, z?) → void
Set camera position coordinates, preserving omitted axes, up and target.
viewer.set_camera(100, 100, 100);
set_camera_view
set_camera_view(name) → void
Fit from front, back, left, right, top, bottom or isometric using Z-up and perspective projection. Models do not rotate.
viewer.set_camera_view('top');
get_camera_bookmarks
get_camera_bookmarks() → CameraBookmark[]
Return a defensive copy of named camera states. Restore one with set_camera_state.
const bookmarks = viewer.get_camera_bookmarks();
if (bookmarks.length) viewer.set_camera_state(bookmarks[0].camera);
set_camera_bookmarks
set_camera_bookmarks(bookmarks) → void
Replace the bookmark list. Each entry is {name, camera}; at most 32 unique names, each at most 80 characters. Saved in VSJ/VSB, not localStorage. Invalid lists throw.
viewer.set_camera_bookmarks([{
name: 'Overview', camera: viewer.get_camera_state()
}]);
get_model_info
get_model_info(id, {measurements?: boolean} = {}) → ModelInfo | null
Synchronous metadata: name, orig_filename, position, dims, rotation, display, smooth_shading, section, color, scale, triangles, source bounds, has_colors, source_units, units, opacity and file_bytes. dims includes scale but not rotation. By default area/volume are calculated synchronously; volume is not validated. Use measurements:false for quick metadata (area/volume null), or measure_model for validated volume.
const info = viewer.get_model_info(0, { measurements: false });
console.log(info?.dims, info?.triangles);
measure_model
measure_model(id, options = {}) → Promise<ModelMeasurements>
Worker analysis returns area_mm2, volume_mm3 (nullable), volume_valid and volume_reason. Supports signal, max_time_ms (default 30000) and on_progress({phase, elapsed_ms}). Open/invalid/inconclusive geometry does not receive a validated volume. Rejections must be caught by the caller.
const result = await viewer.measure_model(0);
console.log(result.area_mm2, result.volume_valid ? result.volume_mm3 : result.volume_reason);
get_model_data
get_model_data(id) → GeometryData | null
Return copied raw positions/colors in source space, without scene transforms. Mutating the copy cannot change the viewer. Prefer duplicate_model when you want a full model copy.
const data = viewer.get_model_data(0);
if (data) console.log(data.positions.length / 9);
get_model_mesh
get_model_mesh(id) → Three.js Mesh | null
Optional interoperability only: needs options.three or globalThis.THREE supplied by the host. Returns an independent mesh; dispose its geometry/material afterward. Without a bridge, reports an error and returns null. The native viewer does not download Three.js.
// Only when this viewer was given the host's Three.js namespace:
const mesh = viewer.get_model_mesh(0);
if (mesh) {
console.log(mesh.geometry);
mesh.geometry.dispose();
mesh.material.dispose();
}
animate_model
animate_model(id, animationOrNull) → void
Start/replace a validated delta/exact animation. Pass null to stop and keep the current pose. Invalid definitions leave existing motion intact. See Animation parameters.
viewer.animate_model(0, { delta: { rotationz: Math.PI * 2, msec: 5000, loop: true } });
View code
Copy the complete build/ folder from the plugin download beside this HTML file, add the models below, and serve the folder over HTTP(S). This is the same example program with local file paths.
These are the original legacy ViewSTL example models. Save the required files in a models/ folder beside your HTML: knot.stl · bar.stl.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ViewSTL — Model animation</title>
<style>
body { font-family: system-ui, sans-serif; margin: 20px; }
#example-viewer { width: 100%; height: 500px; }
button { padding: 10px 16px; }
</style>
</head>
<body>
<div id="example-viewer"></div>
<p id="example-status" role="status">Opening example…</p>
<button id="example-motion" type="button" hidden>Pause animation</button>
<button id="example-retry" type="button" hidden>Retry example</button>
<script type="module">
import StlViewer from './build/viewstl.js';
const container = document.getElementById('example-viewer');
const status = document.getElementById('example-status');
const retry = document.getElementById('example-retry');
const modelURL = new URL('./models/knot.stl', import.meta.url).href;
const secondModelURL = new URL('./models/bar.stl', import.meta.url).href;
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
let loadError = null;
function showError(message) {
loadError = message;
status.textContent = message;
retry.hidden = false;
}
retry.addEventListener('click', () => location.reload());
const animations = [
{ delta: { rotationz: 2 * Math.PI, msec: 6000, loop: true } },
{
delta: { rotationx: 0.5, rotationy: 0.7, rotationz: 1, msec: 4000, loop: true },
exact: { x: 110, y: 10, z: 10, scale: 0.9, msec: 3500 }
}
];
const viewer = new StlViewer(container, {
models: [
{ id: 0, filename: modelURL, x: -90, scale: 0.55, color: '#64a5b2', display: 'smooth',
animation: reducedMotion.matches ? null : animations[0] },
{ id: 1, filename: secondModelURL, x: 90, scale: 0.7, color: '#e79a65',
animation: reducedMotion.matches ? null : animations[1] }
],
load_error_callback: showError
});
function setPlaying(enabled) {
animations.forEach((definition, id) => {
const state = viewer.get_model_animation_state(id);
if (enabled && state?.status === 'idle') viewer.animate_model(id, definition);
else if (enabled) viewer.resume_model_animation(id);
else viewer.pause_model_animation(id);
});
}
// Refit through the plugin when the example container changes width.
let lastWidth = container.clientWidth;
const resize = new ResizeObserver(() => {
if (container.clientWidth === lastWidth) return;
lastWidth = container.clientWidth;
viewer.do_resize();
viewer.set_auto_zoom();
});
resize.observe(container);
try {
await viewer.when_idle();
if (loadError) throw new Error(loadError);
viewer.set_camera_view('isometric');
status.textContent = 'Ready. Drag to rotate, scroll to zoom. Keyboard: arrows, + / − and F.';
const motionButton = document.getElementById('example-motion');
let playing = !reducedMotion.matches;
function updateMotion() {
motionButton.textContent = (playing ? 'Pause' : 'Resume') + ' animation';
motionButton.setAttribute('aria-pressed', String(playing));
}
motionButton.hidden = false;
updateMotion();
motionButton.addEventListener('click', () => {
playing = !playing;
setPlaying(playing);
updateMotion();
});
reducedMotion.addEventListener('change', event => {
if (event.matches) {
playing = false;
setPlaying(false);
updateMotion();
}
});
} catch (error) {
showError(error.message);
viewer.dispose();
}
addEventListener('pagehide', event => { if (!event.persisted) { resize.disconnect(); viewer.dispose(); } });
</script>
</body>
</html>pause_model_animation
pause_model_animation(id) → void
Pause while retaining pose, definition and elapsed time.
viewer.pause_model_animation(0);
resume_model_animation
resume_model_animation(id) → void
Resume a paused model animation. Hidden documents/lost graphics contexts may still temporarily suspend it.
viewer.resume_model_animation(0);
get_model_animation_state
get_model_animation_state(id) → AnimationPlaybackState | null
Return status (idle/running/paused/stopped/completed), elapsed_ms, suspended and track summaries {mode,msec,loop,progress}. Unknown IDs return null.
console.log(viewer.get_model_animation_state(0));
check_printability
check_printability(options = {}) → Promise<PrintReport>
Run local basic/detailed checks; see Print-check options. Returns overall summary and per-model checks with pass/issue/inconclusive/not_run status, messages and bounded highlights. Does not repair geometry or guarantee printing.
const report = await viewer.check_printability({ model_ids: [0], level: 'basic' });
console.log(report.summary);
is_printability_current
is_printability_current(report) → boolean
Check whether the report still matches the viewer’s geometry, transforms and units before displaying it. Camera/color changes do not invalidate it.
const report = await viewer.check_printability({ model_ids: [0] });
console.log(viewer.is_printability_current(report));
show_printability
show_printability(report, {issue, model_id?}) → number
Show bounded diagnostic geometry for a check code, such as boundary_edges. Returns highlighted-element count. Requires a current report. Highlights do not enter PNG, model or scene exports.
const report = await viewer.check_printability({ model_ids: [0] });
if (viewer.is_printability_current(report)) {
viewer.show_printability(report, { model_id: 0, issue: 'boundary_edges' });
}
clear_printability
clear_printability() → void
Remove temporary diagnostic highlights without removing models.
viewer.clear_printability();
clean
clean() → void
Cancel viewer work and remove all models, selection/highlights, bookmarks and model animation. Display units reset to mm. Keeps the viewer/canvas usable for another load.
viewer.clean();
viewer.add_model({ id: 0, filename: '/replacement.stl' });
await viewer.when_idle();
dispose
dispose() → void
Permanently release this viewer’s workers, GPU resources, listeners and owned DOM. Other children of the container remain. Create a new instance to view again.
viewer.dispose();
Events and callbacks
Pass callbacks in the constructor options. The examples show independent configurations; combine the options you need in one viewer. Callback names are options, not DOM event names or an addEventListener API.
ready_callback
ready_callback()
Runs after renderer initialization, before initial models have finished loading. It no longer means “Three.js finished loading.” Use viewer.when_ready if a Promise is preferable.
// Pass this option when constructing the viewer.
const options = {
ready_callback() {
console.log("Viewer initialized");
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
all_loaded_callback
all_loaded_callback()
Runs when a nonempty loading batch settles, including failed/cancelled jobs. An empty initial viewer does not emit it. For ordinary model batches it runs before the final model_loaded_callback; record errors separately.
// Pass this option when constructing the viewer.
const options = {
all_loaded_callback() {
console.log("Loading batch settled");
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
loading_progress_callback
loading_progress_callback(status, session)
status maps model IDs to {loaded,total} byte counts; totals may be unknown (0). Optional CAD phase is loading-cad or parsing-cad. session is the load-session number. Do not interpret a missing total as completion.
// Pass this option when constructing the viewer.
const options = {
loading_progress_callback(status, session) {
for (const [id, p] of Object.entries(status)) {
console.log(session, id, p.total ? p.loaded / p.total : p.loaded, p.phase);
}
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
model_loaded_callback
model_loaded_callback(id)
Runs after a model successfully loads. id identifies it. No success event is emitted for a failed model.
// Pass this option when constructing the viewer.
const options = {
model_loaded_callback(id) {
console.log("Loaded model", id);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
load_error_callback
load_error_callback(message)
Receives an error string for initialization, loading, legacy guarded methods and worker/view failures. Not every API uses this channel: modern exports, import_scene and analysis reject Promises and should be caught with try/catch. Host callback exceptions are logged rather than allowed to break the viewer.
// Pass this option when constructing the viewer.
const options = {
load_error_callback(message) {
console.error(message);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
on_model_drop
on_model_drop(filename)
Low-level canvas callback before each local dropped file is passed to add_model. filename is a name string, not a File. It is a notification, not a cancellable hook.
// Pass this option when constructing the viewer.
const options = {
on_model_drop(filename) {
console.log("Dropped", filename);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
on_model_mouseclick
on_model_mouseclick(id, event, distance, type)
Legacy-named alias of on_model_mousedown. There is one shared pointer callback; on_model_mousedown wins if both constructor options are supplied. Use either setter to replace it.
// Pass this option when constructing the viewer.
const options = {
on_model_mouseclick(id, event, distance, type) {
console.log(id, type, distance);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
on_model_mousedown
on_model_mousedown(id, event, distance, type)
Native pointer notification. type: 1 for ordinary mouse pointer-down, 3 for right-button pointer-down, 11 for double-click, 20 for touch pointer-down. id is null on misses only when send_no_model_click_event is true; then distance is 0. event is the native PointerEvent/MouseEvent. A double-click also has its ordinary pointer-down events.
// Pass this option when constructing the viewer.
const options = {
on_model_mousedown(id, event, distance, type) {
if (id !== null && type === 11) console.log("Double-clicked", id);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
animation_state_callback
animation_state_callback(id, state, reason)
Deferred state-change notification, not a per-frame callback. state is AnimationPlaybackState. Reasons include started, restored, replaced, paused, resumed, completed, stopped, manual, removed, clean and error. Manual transforms stop model motion.
// Pass this option when constructing the viewer.
const options = {
animation_state_callback(id, state, reason) {
console.log(id, state.status, reason);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
animation_complete_callback
animation_complete_callback(id, state)
Fires once when all finite tracks complete, following animation_state_callback with reason completed. Pausing, stopping and looping motion do not count as completion.
// Pass this option when constructing the viewer.
const options = {
animation_complete_callback(id, state) {
console.log("Animation finished", id, state.elapsed_ms);
}
};
const viewer = new StlViewer(document.getElementById('viewer'), {
...options, models: [{ id: 0, filename: '/model.stl' }]
});
Save and load methods
Modern Promise-based methods support explicit scope, progress and cancellation. The older methods remain for compatibility, with their distinct behavior documented below. VSB/VSJ save a whole scene; ordinary model formats save the models explicitly selected by the API caller. All local encoding stays on-device.
export_models
export_models(options) → Promise<ExportResult>
Export one or more meshes into a local Blob. Required format and other fields are under Export options. Returns {blob,filename,format,units,model_ids,triangles,representation:"mesh"}. Omitted model_ids means all loaded models. The preview’s Save UI explicitly selects just one model.
await viewer.when_idle();
const result = await viewer.export_models({ format: 'stl', model_ids: [0] });
console.log(result.filename, result.blob.size);
download_models
download_models(options) → Promise<ExportResult>
Same export settings/result as export_models, plus a browser download. Use explicit model_ids or selected for one-model scope. Trigger from a user action.
await viewer.download_models({ format: 'step', model_ids: [0], filename: 'part' });
get_stl_bin
get_stl_bin(id) → ArrayBuffer | null
Legacy synchronous binary STL. Applies scale and centering, but does not bake scene translation/rotation. Missing/not-ready models return null (unknown IDs also report an error). Use export_models for current-pose, cancellable export.
const bytes = viewer.get_stl_bin(0);
if (bytes) console.log(bytes.byteLength);
download_model
download_model(id, filename?) → void
Legacy STL-only browser download, with get_stl_bin transform behavior. The filename is not a format selector: giving .step does not create STEP. Prefer download_models for modern formats and correct extensions.
viewer.download_model(0, 'part.stl');
get_image_blob
get_image_blob(type = "image/png") → Promise<Blob>
Wait for pending viewing work, draw and capture the canvas. Returns a PNG by default. Sections, background and camera are included; selection/diagnostic overlays are excluded. Other MIME types depend on browser support.
const png = await viewer.get_image_blob();
console.log(png.type, png.size);
export_scene
export_scene(options) → Promise<SceneExportResult>
Capture the whole loaded scene as vsb or vsj. Returns {blob,filename,format,model_ids,triangles,representation:"scene"}. VSB embeds meshes; VSJ holds references. Both preserve camera, appearance, size/unit metadata, bookmarks and animation. Pending loads/empty scenes reject; call when_idle first.
await viewer.when_idle();
const saved = await viewer.export_scene({ format: 'vsb', filename: 'my-scene' });
console.log(saved.blob.size);
download_scene
download_scene(options) → Promise<SceneExportResult>
Export the whole scene and start its browser download. Use VSB for a self-contained file; VSJ companions must be kept alongside the manifest.
await viewer.download_scene({ format: 'vsj', filename: 'scene', force_basename: true });
import_scene
import_scene(source, options = {}) → Promise<{model_ids, scene, used_files}>
Preferred staged scene loader. Accepts File/Blob or public URL; see Scene import options. Appends models with new IDs atomically and restores saved settings. Local VSJ companions go in files; URL inputs/references require the configured relay. Errors reject without partially replacing the scene.
const saved = await viewer.export_scene({ format: 'vsb' });
const result = await viewer.import_scene(saved.blob, { format: 'vsb' });
console.log(result.model_ids);
get_vsj
get_vsj(as_js_obj = false, force_basename = false, for_vsb = false) → string | Scene
Legacy synchronous scene description of already loaded models. false returns JSON text; true returns an object. force_basename removes paths from references. for_vsb prepares embedded filenames/geometry conventions for packaging; normally leave false and use get_vsb/export_scene. Does not embed model data or wait for loading.
await viewer.when_idle();
const json = viewer.get_vsj();
const scene = viewer.get_vsj(true);
console.log(scene.models.length, json.length);
download_vsj
download_vsj(filename = "1.vsj") → void
Legacy JSON scene download. Includes references only, not local file bytes. Wait for loading before calling.
await viewer.when_idle();
viewer.download_vsj('scene.vsj');
load_vsj
load_vsj(source) → void
Legacy asynchronous loader for a URL/path or File/Blob (not raw JSON text). Direct browser URL fetch needs CORS for another origin. It may change settings or add models before a later error, and has no companion-files option. Use import_scene for atomic loading and local companions. Completion uses when_idle/callbacks.
viewer.load_vsj('/scene.vsj');
await viewer.when_idle();
get_vsb
get_vsb() → {vsj, files:[{id, bin}]}
Legacy synchronous package ingredients: scene object and binary STL ArrayBuffers. This is not a ZIP Blob yet. Includes already loaded models only and can allocate substantial memory.
await viewer.when_idle();
const bundle = viewer.get_vsb();
console.log(bundle.files.length);
get_vsb_blob
get_vsb_blob() → Promise<Blob>
Package legacy VSB ingredients into a ZIP Blob. Wait for when_idle explicitly; this method only awaits initialization internally. VSB stores meshes, not original CAD history, and binary STL color precision is limited.
await viewer.when_idle();
const blob = await viewer.get_vsb_blob();
console.log(blob.size);
download_vsb
download_vsb(filename = "1.vsb") → Promise<void | null>
Legacy VSB download; appends .vsb if needed. Errors use load_error_callback and resolve null. Use download_scene for rejection-based error handling and cancellation.
await viewer.when_idle();
await viewer.download_vsb('scene.vsb');
load_vsb
load_vsb(source) → void
Legacy asynchronous VSB loader for URL/path or File/Blob. Direct URL loading follows browser CORS. It does not provide import_scene’s transaction/cancellation contract; wait with when_idle and observe error callbacks.
const blob = await viewer.get_vsb_blob();
viewer.load_vsb(blob);
await viewer.when_idle();
Guides and examples
ViewSTL 2 — native WebGL2 plugin
Version: 2.0.0-rc.5 (ViewSTL v2.00 release candidate, GitHub and npm).
On main, the new plugin is in src/ and build/; the legacy tree is in
legacy/v1/. The unchanged master branch retains original legacy paths.
The v2 website is available at /.
Quick start
Download the plugin ZIP from the GitHub release,
then copy its complete build/ folder to /viewstl-v2/ on your website.
Paste these three lines into your page's <body>:
<div id="viewer" style="height:500px"></div>
<script src="/viewstl-v2/viewstl.min.js"></script>
<script>new StlViewer(document.getElementById('viewer'), { models: [{ filename: 'model.stl' }] });</script>
Replace model.stl with your model's path and open the page over HTTP(S).
You can also drag local files onto the viewer. The full distribution folder is
required so the viewer can find its worker and optional format assets.
Browser script
Copy the complete build/ directory to your server, including the
worker, its chunks/ and cad/ directories and third-party notices. Then:
<div id="viewer" style="width:100%;height:500px"></div>
<script src="/viewstl-v2/stl_viewer.min.js"></script>
<script>
const viewer = new StlViewer(document.getElementById('viewer'), {
models: [{ id: 0, filename: '/models/part.stl', color: '#568bdc' }],
load_error_callback: message => console.error(message)
});
</script>
viewstl.min.js is an identical classic-script alias. The canvas requires
a container with a nonzero height. Files must be served over HTTP(S),
including during local development; browsers restrict workers on file: URLs.
ES modules and npm
import StlViewer from './viewstl-v2/viewstl.js';
const viewer = new StlViewer(container, {
models: [{ id: 0, local_file: fileInput.files[0] }]
});
await viewer.when_idle();
console.log(viewer.get_model_info(0));
The package contains ESM, CommonJS and TypeScript entry points. Importing the package is safe during server-side rendering; constructing a viewer requires a browser DOM and WebGL2.
Install with npm install viewstl. No version suffix is required: the latest
tag selects the current release candidate. The established @omrips/viewstl
package is also updated with the same plugin; npm install @omrips/viewstl
works without a version suffix. Use either package name consistently in imports
and asset-copy paths; there is no need to install both.
With a bundler, copy the complete node_modules/viewstl/build/
directory to your app's public assets (for example public/viewstl-v2/), keeping
the worker, chunks/, cad/ and notices together. Set worker_url to its served
URL, or provide your bundler's worker factory.
Do not assume a bundler copies a module worker's dynamic chunks merely
because it sees a URL string.
import StlViewer from 'viewstl';
const viewer = new StlViewer(container, {
worker_url: '/viewstl-v2/viewstl.worker.js'
});
For Vite, the optional factory form is:
import ViewerWorker from 'viewstl/worker?worker';
const viewer = new StlViewer(container, {
worker_factory: () => new ViewerWorker()
});
The static-assets/worker_url integration above was verified in production
builds using Vite 8.3.4 and Webpack 5.112.0, including STEP/IGES and lazy archive
assets. The optional Vite worker-factory example is separate integration guidance
and was not part of those checks. A factory must return a new worker for each
scheduled job and arrange for all dynamic chunks and CAD assets to be served.
The CommonJS entry point also requires
worker_url or worker_factory because it cannot infer a browser asset URL.
Node.js model analysis
Run npm install viewstl. Node.js 20+ resolves the analysis
worker and optional CAD WASM directly inside the package; there is no asset-copy
step for server use.
import { analyzeFile } from 'viewstl/node';
const report = await analyzeFile('part.stl', { units: 'mm' });
console.log(report.dimensions_mm, report.surface_area_mm2, report.volume_mm3);
CommonJS is supported with const { analyzeFile } = require('viewstl/node').
The /node entry point is separate from the browser viewer and has no DOM,
WebGL or Three.js dependency. It supports STL, OBJ, 3MF, STEP/STP and IGES/IGS.
STEP/IGES measurements describe the tessellated mesh, not exact CAD surfaces.
Scene files, rendering, thumbnails and server-side exporting are outside this API.
Input and units
analyzeFile(path, options) accepts a local regular-file path or a file: URL
object. analyzeBuffer(data, options) accepts a Node Buffer, Uint8Array or
ArrayBuffer, and requires filename or format to identify the format:
import { analyzeBuffer } from 'viewstl/node';
const report = await analyzeBuffer(uploadBuffer, { filename: 'part.stl', units: 'mm' });
The input buffer is copied without modifying or detaching the caller's data. HTTP(S) URLs and network-share paths are rejected. The module does not fetch external model resources, run a web service, or write input files to disk. An application's own upload handler determines how files reach its server and whether they are stored. Browser-selected files sent to a server are uploads; the preview website continues to perform its checks on the user's device.
STL and OBJ carry no reliable physical units. Supply units: 'mm', 'cm', 'm',
'in', 'ft' or 'um' when the input scale is known. With no scale, units and
all physical measurement fields (dimensions_mm, surface_area_mm2,
volume_mm3) are null; the unit check is inconclusive. The generic dimensions,
surface_area and volume fields then use input coordinate units. When units
are known, these generic fields also use mm, mm² and mm³.
3MF and CAD file units are honored and normalized to mm automatically. They
cannot be overridden with a different input scale. units_source is 'file',
'option' or null. volume_valid describes validation of the mesh volume;
it does not confirm a physical scale or guarantee printability. Invalid or
unverified volume is returned as null. level: 'basic' skips detailed
intersection and validated-volume checks; the default is 'detailed'.
Jobs, limits and cancellation
The convenience functions share an analyzer with two active workers, up to 16 waiting jobs, and at most 128 MiB of copied buffer data waiting in the queue. Create an independent analyzer when the host needs different concurrency:
import { createAnalyzer } from 'viewstl/node';
const analyzer = createAnalyzer({ concurrency: 1, max_queue: 4 });
try {
const reports = await Promise.all(paths.map(path => analyzer.analyzeFile(path, {
units: 'mm', signal: controller.signal, timeout_ms: 60000,
limits: { max_file_bytes: 32 * 1024 ** 2, max_memory_bytes: 512 * 1024 ** 2 }
})));
} finally {
await analyzer.dispose();
}
Here paths is the application's array of file paths and controller is its
AbortController. Keep submitted batches within the configured queue capacity.
concurrency is 1–8; max_queue may be zero; max_queued_bytes controls queued
buffer copies. Exceeding queue admission rejects with ERR_ANALYZER_BUSY.
Use a shared analyzer for a service rather than creating an unbounded number
of independent analyzers per request.
Per-job defaults are 64 MiB input, 1,000,000 triangles, 128 MiB expanded archive
data, 1,024 archive entries and a 1 GiB estimated working-set budget. These are
independent ceilings: the memory estimate can reject a mesh below the triangle
ceiling. Override them in limits with max_file_bytes, max_triangles,
max_uncompressed_bytes, max_archive_entries and max_memory_bytes.
Unknown limit names are rejected. CAD input/output and WASM-heap checks are
also applied, and retained CAD memory counts towards the analysis budget.
timeout_ms defaults to 120,000 and covers waiting, reading, parsing and checks.
Expiry terminates the worker and rejects with ERR_ANALYSIS_TIMEOUT.
signal cancels queued or active work with an AbortError (ABORT_ERR).
dispose() cancels outstanding jobs and waits for their workers to terminate.
Workers are also terminated after success or failure before their slots are
reused, releasing parser and CAD state. Processing failures do not disable
the analyzer.
max_time_ms separately limits the mesh-check stage (30,000 by default, up to
120,000). It may yield a partial report with limit_reason: 'time'. Inspect
summary and each check's status; an incomplete check is not a pass.
on_progress receives phase and elapsed-time updates. The optional profile
uses the same FDM build-volume, overhang, bed-contact and sampled wall-thickness
settings as browser print checks. FDM results remain estimates.
Memory limits are conservative admission estimates plus a worker JS-heap limit, not a hard limit on process RSS. ArrayBuffers and WASM memory are not fully covered by the JS-heap limit. Size host/container limits for total concurrency; use process/container isolation when serving untrusted uploads. The library does not provide an HTTP endpoint, authentication or a filesystem sandbox.
Full app controls
mountViewstlApp is the optional app controller, exported from the same
viewstl.js bundle as StlViewer (and as a global in the classic script).
It owns the file queue, memory admission, cancellation, selection, scene
placement, translation/rotation/depth gestures, viewing modes, sectioning,
colors, grid, PNG downloads, keyboard handling, and control state.
import { mountViewstlApp } from './viewer/viewstl.js';
const app = mountViewstlApp(document, {
sample_url: new URL('./sample-vase.stl', import.meta.url).href,
sample_name: 'Twist vase.stl'
});
await app.openFiles(fileInput.files);
console.log(app.getState()); // detached metadata snapshot, no geometry copies
app.dispose(); // cancels work and releases listeners, workers and WebGL
This controller binds the preview's HTML control layout; the host supplies
markup and CSS. The reference layout is Viewstl/site/index.html in the sibling
website workspace. requiredControls in src/app.js validates the control
IDs before registering handlers. Missing controls and duplicate mounts produce
a descriptive error. Custom integrations without that layout use StlViewer.
Branding, page metadata, advertisements and static help/privacy text belong
to the host page. The preview's JavaScript is only the initialization call.
For an embedded app, pass an HTMLElement containing the layout. Queries,
keyboard events and drops are scoped to that root. For multiple roots, prefix
each layout's IDs and label/ARIA references and pass the matching id_prefix;
the controller separates radio groups as well. dispose() permits remounting
and prevents late reads from updating the new app.
openFiles() accepts Files or configured {name, url, size?} sources and
resolves when the queue settles. File errors appear in the notice; success is
reflected by getState().models. Other methods are select(id), remove(id),
cancelLoading() and clear(). An omitted sample_url hides the sample action.
worker_url, worker_factory and worker_timeout support alternate hosting,
bundlers and CommonJS use.
Loading the bundle does not create a canvas or worker. The first file creates
the WebGL2 viewer. App source modules are bundled into viewstl.js; the host
ships no separate memory/layout/transform helpers. Worker JS, archive chunks
and CAD WASM remain separate plugin-owned assets to keep parsing off the main
thread and avoid downloading CAD code for STL/OBJ users.
get_model_info() also exposes copied source bounds, has_colors, and
source_units (null for unitless sources), so consumers need not inspect
internal geometry or retain its buffers.
Rendering and file processing
- WebGL2 only; no CanvasRenderer, WebGL1 or IE fallback.
- No Three.js or other third-party library in the STL/OBJ rendering core.
- ASCII/binary STL, VisCAM/MeshLab and Materialise colors, plus the legacy ViewSTL colored-STL convention.
- OBJ polygon fan triangulation and negative indices. OBJ materials, textures and robust triangulation of concave polygons are not included.
- Core 3MF meshes, nested components, build transforms, units, base materials and vertex colors. Root model relationships are followed. Texture/composite materials, external component parts and unsupported required extensions produce explicit errors rather than silent loss.
- STEP/STP and IGES/IGS solid/surface import via the optional pinned Open CASCADE WASM reader. Units normalize to millimetres; assembly placements and mesh/face colors are retained. All surfaces in a file form one model. The display mesh is an approximation (relative deflection 0.001, angular deflection 0.5 radians), not an exact CAD editing or measurement kernel. Use self-contained exports: external part files are not fetched, and native SolidWorks/Solid Edge formats, feature history, PMI and curve-only files are unsupported. Incomplete external-reference assemblies may lack parts.
- Deploy the entire
build/cad/directory alongsidechunks/and the worker. CAD code/WASM is loaded only for CAD files; local input bytes are never uploaded. Each job's worker is terminated on completion, cancellation, error or timeout. CAD admission estimates 128 MiB plus 40 times the input size. The WASM heap is separately capped to at most 512 MiB and to part of the remaining scene budget. Output arrays and triangle counts are also checked; these checks cannot predict exact peak browser/GPU memory for every CAD file. - A strict CSP must allow
script-src 'self' 'wasm-unsafe-eval'for the worker; JavaScriptunsafe-evalis not needed. Serve.wasmasapplication/wasm.scripts/prepare-cad.mjsreplaces the pinned glue's dynamic bindings with static closures and adds its heap ceiling. The WASM is unmodified. Adapted glue, the recipe, license texts and source references ship with the assets. - VSJ JSON and VSB archives containing
json_data.vsjand STL files. - ZIP/3MF uses separately loaded worker chunks containing pinned fflate, saxes and xmlchars. Those chunks are not fetched for ordinary STL/OBJ.
- ZIP accepts stored and deflated entries, validates CRC/length and bounds expanded data. ZIP64, encryption and multi-disk archives are unsupported.
- Local files never leave the browser. Remote URLs obey normal CORS and same-origin credential rules; there is no upload service or CORS proxy.
Parsing, bounds, flat normals and initial requested smooth/edge geometry are calculated in workers. Results use transferable buffers. Workers are bounded and terminated after jobs, on cancellation, and on disposal.
Changing an already-loaded model to smooth/edges/wireframe derives its additional data in a worker. That operation intentionally copies positions once, since the main thread retains them for synchronous export and picking. Nonuniform-scale area queries also require a cached synchronous scan. These are distinct from the initial file handoff, which does not clone geometry.
Rendering runs only when needed, including during interaction and animation. Transparency uses model-level back-to-front sorting; intersecting transparent surfaces can still have ordering artifacts. STL export stores face color, so a 3MF triangle's per-vertex gradient cannot survive STL/VSB export exactly.
Model animation
Animation is included in 2.0.0-rc.5 on GitHub and in both npm packages.
Playback remains inside the native WebGL2 plugin.
After a model has loaded, one call starts continuous rotation:
await viewer.when_idle();
viewer.animate_model(0, {
delta: { rotationz: 2 * Math.PI, msec: 5000, loop: true },
});
The legacy animation model option also works. delta adds a change to the
starting pose; exact interpolates toward an absolute target. They can operate
together on different properties and finish independently:
viewer.animate_model(0, {
delta: { rotationx: 1, rotationy: 1.1, rotationz: 1.2, msec: 1000, loop: true },
exact: { x: 1000, y: 150, z: 200, scale: 5, msec: 3000 },
});
viewer.pause_model_animation(0); // Keep the pose and elapsed time.
viewer.resume_model_animation(0);
console.log(viewer.get_model_animation_state(0));
viewer.animate_model(0, null); // Stop at the current pose; discard playback.
Supported properties are x/y/z, rotationx/y/z in radians and
scale or scalex/scaley/scalez. Durations are positive milliseconds, defaulting
to 300. Scale deltas are additive, as in the legacy API; all resulting scales
must stay positive. Only delta tracks repeat, accumulating their change each
cycle. A negative looping scale delta is rejected. Delta and exact may not claim
the same property. Inputs must be finite and within JavaScript's safe numeric
range. Invalid replacements leave the current animation intact.
Starting a new animation replaces the old one from the current pose. Manual
position, rotation, scale and unit setters stop that model's playback first.
Camera interaction, color and viewing-mode changes remain usable. Completed
tracks no longer write their properties. Removal, clean() and dispose()
release playback; paused/completed models do not keep the render loop running.
Timing follows elapsed frame timestamps, not frame count. Hidden documents and
lost WebGL contexts suspend playback without including that gap on resumption.
User pause is independent of this automatic suspension. State queries return
status, elapsed_ms, suspended and active tracks; unknown IDs return null.
Set callbacks in the viewer options:
const viewer = new StlViewer(container, {
animation_state_callback(id, state, reason) {
console.log(id, state.status, reason);
},
animation_complete_callback(id, state) {
console.log('Finished', id, state.elapsed_ms);
},
});
Callbacks describe transitions, not every frame, and are deferred until the
current operation finishes. Completion fires once when every track finishes;
an animation with a looping track does not complete. Call playback controls on
loaded models, for example after when_idle().
VSB/VSJ preserve the animation definition, original pose, elapsed phase, rotation pivot and pause flag. Both scene-loading APIs start incoming animations together after all incoming models load. VSJ still needs its referenced model files. Animated VSB entries retain unscaled mesh data and explicit scale metadata; static entries retain the previous scale-baked convention. This prevents scale being applied twice. Older viewers may read the pose but cannot restore the new playback-state extension exactly.
PNG and modern export_models() capture the current pose without stopping
playback. Legacy get_stl_bin() keeps its documented scale/centering-only export
convention. Regular model files do not store animation. Print checks should run
while the affected models are paused; subsequent motion makes the report stale.
A self-contained example is in examples/v2/animation.html in the source
checkout (serve with pnpm start). It includes Play, Pause, Resume, Stop and Reset.
The app controller also supports an optional animation-* group (see the new
site's HTML template): animation-details, animation-fields, animation-axis,
animation-seconds, animation-direction, animation-play, animation-pause,
animation-stop, animation-status and animation-error. Supply the whole group
or omit it. It exposes selected-model spin around the mesh center, duration
(0.1–3,600 seconds per turn), direction and playback buttons. Other models keep
playing when selection changes. Manual tools capture the current animated pose.
Readouts update at most four times per second; geometry and GPU buffers are reused.
mountViewstlApp composes animation_state_callback and
animation_complete_callback options with its own UI behavior. app.getState()
includes each model's current transform and animation state. Ordinary files
never auto-spin. Imported running animations are paused when the browser reports
prefers-reduced-motion: reduce; explicit user playback remains available.
App print checks pause running target models. Optional print-resume and
print-animation-note elements expose resumption; closing the panel does not
silently restart motion. Resuming clears stale results and keeps the panel open.
Scene saving includes playback; PNG and regular model saving capture a pose.
Existing API and intentional differences
The existing constructor, model options and documented method names are
implemented. See the full inventory. Existing getters
remain synchronous. get_vsb_blob() continues to return a Promise.
width/heightoverride theircanvas_width/canvas_heightaliases.bg_coloroverridesbgcolor.on_model_mousedownoverrideson_model_mouseclick; both setter names work.set_model_units(model_id, units, scale)keeps the executable three-argument signature. The old documentation omittedmodel_idincorrectly.load_three_files: 'directory/'remains a legacy alias for the worker directory, resolved against the page. It never loads Three.js. An explicitworker_urltakes precedence; false/omitted uses the entry point's directory.jszip_pathandjszip_utils_pathare accepted deprecated options, with no effect: archive support is shipped in the package's lazy worker chunks. No scripts are fetched from those legacy paths.get_model_info().dimsnow updates after scaling. The old implementation returned stale dimensions even when area and volume had changed.- Zero IDs, zero coordinates, zero rotations and zero opacity are valid. Invalid positive-only scales, colors, transforms and IDs report errors.
- Model-loading batches preserve the legacy final callback order:
all_loaded_callbackprecedes the finalmodel_loaded_callback. Failed/cancelled jobs settle the batch instead of leaving it hanging. An empty initial viewer does not emitall_loaded_callback. set_center_modelscontrols centering of subsequently loaded models.- Model data remains CPU-resident for synchronous export/picking and context
restoration.
clean()anddispose()release GPU buffers and worker jobs. dispose()removes only the viewer's owned DOM/listeners, not its siblings.- The renderer, camera and models are native objects. Direct Three.js calls
into
.camera,.scene,.renderer, or.modelsare not compatible.
STL export retains the legacy convention: geometry scale and centering are baked into vertices; scene position and rotation are not baked. VSB saves position/rotation in its manifest and avoids applying scale twice.
Volume is a signed-tetrahedron mesh calculation reported as an absolute value, not a watertightness or printability certification. For inch models, area/volume follow the legacy conversion convention.
Optional Three.js mesh bridge
The ordinary viewer never imports Three.js. To request a real Three.js
Mesh from get_model_mesh, pass your own instance:
const viewer = new StlViewer(container, { three: THREE });
// after loading:
const mesh = viewer.get_model_mesh(0);
The bridge uses options.three or globalThis.THREE. With neither present,
get_model_mesh reports a load error and returns null. It does not return
a misleading imitation Mesh.
Import supports static BufferGeometry meshes and legacy Geometry vertices/ faces. It copies geometry and basic material color/opacity. Placement is controlled by the supplied model options, as in the old plugin. Skinning, instances, morph targets, child objects, multiple/textured/shader materials are rejected. Export returns an independent BufferGeometry Mesh. Dispose the returned mesh's geometry and material when finished with it.
Use get_model_data(id) for a native copy of raw geometry, and
add_model({ data, ...options }) to add it without Three.js. Data does not
include scene transforms; copy those options explicitly when duplicating.
Use get_image_blob() for screenshots; direct renderer.domElement.toDataURL
outside a draw is not guaranteed because continuous drawing-buffer
preservation is disabled.
Additional options and helpers
| Option | Default |
|---|---|
worker_url |
Beside the ESM/classic script |
worker_factory |
None |
worker_count |
2, allowed range 1–4 |
worker_timeout |
120,000 ms per job |
pixel_ratio |
Device pixel ratio capped at 2 |
max_file_bytes |
256 MiB |
max_triangles |
5,000,000 |
max_uncompressed_bytes |
256 MiB per archive |
max_archive_entries |
1,024 |
max_memory_bytes |
Number.MAX_SAFE_INTEGER (optional estimated working-set budget) |
Limits are configurable positive integers. These are ceilings, not guarantees that every mobile device has enough memory for the maximum. Derived smooth normals/edge maps can use substantially more memory than flat rendering.
max_memory_bytes adds conservative checks for parsed geometry, text parsing,
archive expansion, expanded 3MF instances, and derived smooth/wire/edge maps.
It estimates working memory, including GPU buffers; it does not measure free
memory. Existing default byte/triangle limits remain unchanged for embedders.
The ViewSTL preview site configures its own device-dependent limits: half the
approximate navigator.deviceMemory report, or half a 2 GiB fallback if absent.
This leaves a 50% margin against that estimate, not a guaranteed allocation or
protection against an OS/browser killing an exhausted tab.
Local binary STL files are parsed in chunks of at most 65,536 triangles
(3.125 MiB input data) plus their 84-byte header. This avoids holding a complete
input file and a second assembled copy alongside geometry. ASCII/OBJ/archive
input is still read as a complete buffer; OBJ no longer creates a full array
of every input line. Native GPU allocation errors clean up partial resources
and report through load_error_callback.
when_ready resolves after renderer initialization, before initial models
finish. when_idle() waits for pending model, scene and derived-geometry
work; inspect load_error_callback for individual failures. get_image_blob
waits for that work, draws and returns a PNG Blob by default.
Keyboard: arrows orbit; + / - zoom; F fits the scene. Mouse: drag orbits, shift/right/middle drag pans, wheel zooms. Touch: one finger rotates and two fingers pan/pinch. Real mobile-device validation remains necessary.
For a same-origin standalone deployment, allow your script and worker assets
in the site's Content Security Policy (script-src 'self'; worker-src 'self'),
and permit intended remote model hosts in connect-src. SharedArrayBuffer,
cross-origin isolation, eval and worker blob bootstrapping are not required.
Additional views and section cuts
The existing flat, smooth and wireframe display values remain supported.
New display values are edges (solid with 30-degree feature edges), mesh
(triangle overlay), xray, studio and orientation (blue front faces, red
back faces). None requires Three.js. view_edges / set_edges retain the
legacy one-degree edge threshold.
viewer.set_view(0, {display: 'studio', smooth_shading: true});
await viewer.when_idle();
viewer.set_smooth(0, false); // Independent of the selected modern view
viewer.set_section(0, {axis: 'z', position: 0.5, flip: false});
viewer.set_section(0, null); // Restore the complete model
These fields are also accepted in model options, returned by get_model_info,
and preserved by VSJ/VSB scene export. set_display('flat') and
set_display('smooth') keep their legacy shading behavior. Smooth shading is
ignored visually in wireframe and retained for the next surface view. Derived
view changes commit after preparation succeeds; a rejected memory estimate or
failed worker preserves the previous appearance. Wait for when_idle() and
inspect load_error_callback as with model loading.
Mesh overlay uses shader barycentric coordinates without an extra geometry buffer. Studio, orientation, X-ray and clipping also reuse existing geometry. Smooth normals, wireframe and feature edges are computed on demand in a worker and checked against the memory budget, including cached derived arrays.
Section position is a fraction from 0 to 1 of the source model's bounding box along its chosen axis. The plane follows model transforms, clips picking and PNG rendering, and does not modify exported STL geometry or model statistics. The cut remains open, with an amber boundary highlight; no cap is generated. X-ray uses lightweight back-face/front-face alpha passes; it is an illustrative view, not exact order-independent transparency for intersecting surfaces. Face orientation describes winding relative to the camera, not an automatic watertightness or printability diagnosis.
Optional streaming URL imports
The plugin does not silently route existing filename requests through a server.
To retrieve a public URL without depending on the source server's CORS policy,
configure a same-origin relay and explicitly use remote_url:
const viewer = new StlViewer(container, {url_import_endpoint: '/api/import-url'});
viewer.add_model({id: 0, remote_url: 'https://example.com/model.stl'});
// Optional remote_format: 'stl', 'obj', '3mf', 'step', 'stp', 'iges', 'igs'.
mountViewstlApp accepts the same endpoint and exposes app.openURL(url, format).
Its optional url-dialog/url-form controls are shown in the preview site's HTML.
The site supplies markup, styles and configuration; URL interaction, loading,
progress, cancellation and parsing live in the plugin. App jobs allow ten minutes
by default; low-level viewer worker_timeout remains configurable as before.
The relay receives the URL in a JSON POST body and streams the response without file storage. A signed URL's query string is not displayed as a model name or included in scene exports. Local files still never leave the browser. URL imports are a separate privacy path: the relay sees the remote bytes in transit.
Binary STL is decoded incrementally in the worker. Other formats use bounded
browser buffers, including archives and CAD. A complete model appears after
download and validation; progressive partial rendering is not implemented.
get_model_info().file_bytes reports the imported byte count when available.
Downloaded names are metadata, not reloadable URLs; use VSB to package a scene
containing these models. Embedders must provide their own compatible relay; no
ViewSTL server is implicitly used by the library.
When url_import_endpoint is configured, mountViewstlApp also accepts dropped
links (text/uri-list), Firefox URL/title data, and plain HTTP/HTTPS URL text.
URL drops use openURL and its existing queue, relay, progress, cancellation and
error handling. Multiple URI-list links are deduplicated per drop. Local files
take priority over accompanying URL metadata. Plain text is not searched for
embedded links, HTML drag contents are not parsed, and drops into text inputs or
editable content retain their normal behavior. This belongs to the optional app
controller; the low-level viewer's existing file-drop contract is unchanged.
The reference relay lives in the website repository's relay/ directory. Its
request is {url, format, max_bytes} and its response supplies X-ViewSTL-Format,
percent-encoded X-ViewSTL-Filename, and optional Content-Length. All relay and
parser failures use the existing error callback. Cancellation terminates the
worker/fetch; the server then closes the upstream connection.
Local mesh export
The optional mountViewstlApp controller can bind a Save dialog using the
save-* control group in the preview site's site/index.html. Include the entire
group or omit it; incomplete groups fail before mounting. All event handling,
selection, progress, cancellation and download dispatch compile into viewstl.js.
The host supplies markup, styles and explanatory copy (format option data-note
attributes and the CAD limitation paragraph). No Save UI is injected by the
low-level viewer.
The reference dialog exports only the current app selection for mesh formats,
with positions/rotations applied and millimetre output. Unitless STL/OBJ inputs
are assumed to be millimetres. Its optional color setting applies to OBJ/3MF;
colored OBJ uses ZIP. It calls download_models with explicit model IDs, so
closing an editing panel does not clear the export selection. VSB/VSJ always
export the whole scene. save-scope is a text status element displaying the
selected filename or whole-scene model count, not a scope dropdown. Cancel, Escape,
the close button, backdrop dismissal, changing selection, starting an import,
removing/clearing models and app disposal cancel an active export. Errors stay
in the dialog and permit another format. Existing PNG controls remain separate.
All exporters live in the plugin and run in disposable workers. They generate local Blobs; no relay, upload, server conversion or third-party network endpoint is used. Export modules and ZIP compression load only on demand. STEP/IGES writing uses JavaScript and does not download the CAD importer or WASM engine.
await viewer.when_idle();
const controller = new AbortController();
const result = await viewer.export_models({
format: '3mf', // stl, obj, 3mf, step/stp, iges/igs
model_ids: [0, 2], // omitted = all; 'selected' = selected model
filename: 'assembly', // correct extension added/replaced automatically
units: 'mm', // mm (default), inch
apply_transforms: true, // default: bake position, rotation, scale and centering
colors: true, // default for OBJ/3MF; ignored for STL/STEP/IGES
signal: controller.signal,
on_progress({ loaded, total, phase }) {
// Triangle counts. phase is 'exporting' or 'complete'.
// Encoding can reach total before ZIP packaging completes.
},
});
// {blob, filename, format, units, model_ids, triangles, representation: 'mesh'}
// Keep result.blob, or use the convenience method to initiate a browser download:
await viewer.download_models({format: 'step', model_ids: 'selected'});
// controller.abort() rejects an active/queued operation with AbortError.
Exports capture the model state at dispatch; subsequent edits/removal do not
change an in-flight export. Buffers used for rendering are never transferred or
detached. No selection, an unknown ID, duplicate IDs or an unfinished model
rejects the promise. Call when_idle() first to include pending imports. Errors
reject the promise and progress goes only to the supplied callback; exporters do
not show dialogs or invoke the viewer's import-error callback. A throwing progress
callback rejects its export and terminates the worker. clean()/dispose() cancel
active and queued jobs. The caller owns the returned Blob and its lifetime.
apply_transforms: false exports source coordinates and their accumulated unit
conversion, without centering, presentation movement/rotation or user scaling.
Scene coordinates are treated as millimeters; units: 'inch' divides output
coordinates by 25.4 and writes inch metadata where the format supports it.
Unitless inputs must be interpreted/scaled appropriately by the embedding app.
STL and OBJ have no standardized units; OBJ includes an informational comment.
| Format | Output and scope |
|---|---|
| STL | Standard binary STL; selected meshes combined into one triangle stream. Attributes are zero, so no ambiguous vendor color encoding. |
| OBJ | With colors: true, a ZIP containing model.obj and materials.mtl, including object groups, face normals, per-face colors and opacity. Vertex colors are averaged per face and colors/opacity quantized to 8-bit channels. With colors: false, a plain .obj file. |
| 3MF | ZIP/XML package with separate mesh objects, indexed vertices, units and per-vertex color/opacity using the Materials extension. Requires non-degenerate triangles, paired consistently oriented edges, and positive volume. Open/inconsistently wound meshes reject; there is no automatic repair or full self-intersection validation. |
| STEP | AP214 AUTOMOTIVE_DESIGN: separate named products made of planar triangle faces in open shells. Exact displayed mesh coordinates are written; this is a surface model, not a reconstructed CAD solid. |
| IGES | IGES 5.3 bounded planes (108/form 1) with closed 3D polylines (106/form 12). Models use separate levels. Triangle surfaces, not stitched CAD solids. |
STEP/IGES preserve mesh geometry and placement, but omit colors, textures, analytic curves, original CAD topology/assemblies and feature history. They work with open triangle meshes, but reject zero-area triangles. No F3D export is provided. A consuming site can describe these limitations in its own interface; the low-level export API is UI-free. CAD interoperability has been verified against Open CASCADE, not every commercial CAD application.
Section cuts, camera orientation, grid, selection frames and viewing/shading modes are presentation settings and do not modify exported geometry. Use VSJ/VSB for ViewSTL scene settings. Texture/UV data discarded at import cannot be recreated.
The scene memory budget includes pending exports. Output is encoded in chunks
instead of building one large text string; Blobs still require browser storage
and this API does not stream directly to a filesystem handle. Output obeys
max_file_bytes, archives also obey max_uncompressed_bytes and
max_archive_entries, and the complete selection obeys max_triangles.
Conservative format-specific memory estimates can reject an export before copying
geometry. Cancellation terminates its worker, including compression.
The existing synchronous get_stl_bin/download_model and VSJ/VSB methods retain
their legacy behavior. Use the new API for scene transforms, correct filename
extensions, asynchronous execution and cancellation.
Development and packaging
pnpm install --frozen-lockfile
pnpm build
pnpm test
pnpm check:types
pnpm check:node-types
pnpm check:legacy
pnpm start
pnpm pack
Open /examples/v2/ for the demo and /tests/v2/browser.html for real worker/
WebGL checks. pnpm pack builds a local tarball; it does not publish anything.
The package allowlist includes v2 artifacts, types, documentation and license
notices. It excludes legacy snapshots, old dependencies, tests and server data.
Before a stable release, validate real mobile hardware, Safari and Firefox, larger user fixtures, npm/bundler integrations and the integrations used by the production site. Automated and desktop browser results are recorded in verification.
Multiple models, colors and an infinite grid
Use center_models: false to preserve the coordinates in assembly files, then call
add_model / add_models to append files. set_position changes presentation
coordinates without rewriting source geometry. The preview site's Models panel
adds selection, individual removal and reversible side-by-side placement.
const viewer = new StlViewer(element, { center_models: false });
viewer.add_models([{id: 0, local_file: partA}, {id: 1, local_file: partB}]);
await viewer.when_idle();
viewer.set_color(0, '#dd766b');
viewer.set_color(0, null); // Restore embedded file colors (or neutral gray if absent).
viewer.set_bg_color('#202d35');
viewer.set_infinite_grid(true, 1, 0); // enabled, base spacing, Z height
The infinite XY grid is a procedural shader. Its spacing adapts as the camera
zooms and it fades near the horizon. Panning does not allocate a larger grid;
opaque models occlude it. set_grid retains its existing finite-grid behavior.
Infinite settings round trip through VSJ/VSB as infinite_grid: {spacing,height}.
Colors change shader uniforms without rebuilding the mesh buffers.
An explicit max_memory_bytes budget now covers the whole scene, including
resident geometry, cached view arrays and pending derived geometry. Parsing is
serialized under that budget so each new model receives the remaining allowance.
Removing a model frees its allowance and cancels its pending derived work.
These remain conservative estimates, not measurements of free RAM or GPU memory.
Selecting a model for editing
set_selection(id) draws a light 3D bounding frame using that model's transform.
set_selection(null) clears it. The frame uses one 288-byte vertex buffer, follows
rotation/translation/scale without rebuilding mesh buffers, and is excluded from
PNG and scene exports. Removing or clearing its model releases the frame.
pick_model(clientX, clientY) returns {id, distance} for the nearest visible
surface, or null for empty space. Coordinates use the same viewport coordinate
system as mouse events. It respects transforms, hidden models and section cuts.
The preview calls it on double-click only; ordinary dragging does not run a mesh
intersection scan. The existing legacy click callbacks retain their behavior.
The preview provides Move, Rotate and Depth controls per model. Rotation uses
degrees in the UI and compensates for the original mesh center; the plugin's
set_rotation API continues to accept radians with its existing origin behavior.
Depth translates along the eye-to-model ray while holding the camera fixed.
Saving ViewSTL scenes
Use export_scene for a Blob or download_scene for a browser download:
const result = await viewer.export_scene({
format: 'vsb', // or 'vsj'
filename: 'my-scene',
signal: abortController.signal,
on_progress({ loaded, total, phase }) {
// Model counts; exporting, packaging (VSB only), complete.
},
});
await viewer.download_scene({ format: 'vsj', filename: 'scene', force_basename: true });
Both methods always capture the entire loaded scene. They return {blob, filename, format, model_ids, triangles, representation: 'scene'}. Pending loads,
empty scenes, excess model/triangle counts, memory and output limits reject.
Errors reject the Promise instead of using the import-error callback. Cancellation
and throwing progress callbacks release the worker and memory reservation.
VSB uses the existing ZIP layout: json_data.vsj plus one colored binary STL per
model. Mesh encoding and compression run in a disposable worker using copied
snapshot buffers; renderer buffers remain intact. Camera, background, grid,
transforms, model colors/opacity, viewing modes and sections stay in the manifest.
Original model names are kept as orig_filename. As with the legacy VSB format,
STL geometry uses float32 positions and 5-bit per-face colors, taking the first
vertex color of each triangle. Texture/UV data and original CAD topology/history
are not reconstructed or included.
VSJ is JSON settings and model references, without embedded geometry. The
force_basename option defaults to false for API callers; the preview uses true
to write only filenames. Referenced files must be supplied separately. A local-file
reference alone does not grant browser access to a file on disk. VSB is the
self-contained choice. The preview opens both formats through the file picker,
drag and drop, and URL opening. Existing get_vsj, get_vsb, get_vsb_blob and their legacy
download methods retain their signatures and behavior.
In the app Save dialog, VSB/VSJ save the entire scene. STL/OBJ/3MF/STEP/IGES save
only the selected model. There is no user-selectable scope dropdown. The low-level
export_models API still permits explicit multiple-model selections for embedders.
Opening ViewSTL scenes
Use the Promise-based import_scene API for local files or public URLs:
const result = await viewer.import_scene(sceneFile, {
files: companionFiles, // File[]: model files referenced by a local VSJ.
signal: abortController.signal,
on_progress({ loaded, total, phase }) {
// reading-scene: reader progress; opening-model/complete: model counts.
},
});
console.log(result.model_ids, result.scene, result.used_files);
// Requires the viewer's same-origin url_import_endpoint configuration.
await viewer.import_scene('https://example.com/scene.vsb');
await viewer.import_scene('https://example.com/download?id=123', { format: 'vsj' });
VSB archives contain json_data.vsj and their model files. VSJ manifests reference
separate files: supplied local files match by exact relative path or unambiguous
basename. Missing or ambiguous files reject with an actionable error. Absolute
HTTP/HTTPS references use the configured URL relay. For a remote VSJ, relative
references resolve against its final directory after redirects, also through the
relay. Local files and manifests are not uploaded. The relay streams remote files
without saving them. It still applies its request, size and network limits, so a
scene with many remote references can hit the relay's rate limit.
Imports append models with new IDs and restore camera, background/grid, transforms,
colors/opacity, view modes, smoothing and sections. Saved centering is converted
to equivalent model positions without changing existing models' centering. Embed
canvas size and resize/mouse controls are not imported. The development animation
update also restores per-model animation and preserves its original pivot.
Scene reading and mesh parsing use workers and the existing memory/triangle/archive
limits. Models become visible together after all have loaded. Errors, cancellation,
or a throwing progress callback remove the incoming models and retain the existing
scene. clean() and dispose() also cancel pending scene imports. Existing
load_vsj and load_vsb retain their previous signatures and behavior.
In the preview, choose or drop a local VSJ together with its referenced model files; companions used by the scene are not added twice. A VSB needs no separate files. Choose VSB/VSJ explicitly in Open URL for a URL without a scene extension. Scene opening, progress, cancellation and restored editing controls all live in the plugin's app controller; the website initializer remains minimal.
Checking meshes for 3D printing
The optional checks run locally on demand. They analyze the loaded triangle mesh from any supported format, including models restored from VSB/VSJ. CAD accuracy is limited by the imported tessellation. Analysis does not repair or modify models.
const controller = new AbortController();
const report = await viewer.check_printability({
model_ids: [selectedId], // Omit for all loaded models; 'selected' uses the editing selection.
level: 'detailed', // 'basic' is the default.
apply_transforms: true, // Default: current placement/rotation/scale, like mesh export.
units_confirmed: true, // Confirm the current mm scale for unitless input.
profile: { // Optional; omit for geometry checks only.
technology: 'fdm',
build_volume_mm: [220, 220, 250],
min_wall_mm: 0.8, // User-chosen thresholds, not universal printing rules.
overhang_degrees: 45 // Measured from vertical; +Z is print up.
},
max_time_ms: 30000, // Per model, maximum 120000.
signal: controller.signal,
on_progress: ({ phase, model_index, total_models }) => console.log(phase)
});
if (viewer.is_printability_current(report)) {
viewer.show_printability(report, { model_id: selectedId, issue: 'boundary_edges' });
}
viewer.clear_printability();
Basic checks cover degenerate and nearly degenerate triangles, duplicates, open boundaries, non-manifold edges and vertices, inconsistent winding, connected components and physical-unit provenance. Detailed checks add a native-JavaScript BVH for self-intersections (including coplanar overlaps), shell containment, orientation and enclosed volume. Separate components can be intentional. Hollow models require opposite cavity-shell orientation. Vertex connectivity uses exact source coordinates; small gaps are not silently welded closed. Intersection checks use the reported scale-dependent numerical tolerance; near-contact cases can be inconclusive. The existing 3MF export guards remain independent and unchanged.
Optional FDM checks compare dimensions with a build-volume box, identify potential
overhang regions, estimate bed contact and sample wall thickness. line_width_mm
can supply the wall threshold when min_wall_mm is omitted. No printer defaults
are assumed. Build fit tests current orientation dimensions, not actual placement,
exclusion zones or alternative rotations. Bed contact virtually lowers each model
to its minimum Z; the visible grid and camera do not define the print bed.
Thickness tests at most 2,048 face centres using inward normal rays and can miss
small features or oblique thin regions. No finding from those samples is explicitly
inconclusive, never proof of a minimum thickness. Overhangs do not simulate
bridging/supports; contact area does not predict adhesion/stability. Resin, toolpaths,
cross-model collisions and Boolean unions are outside these checks.
Reports include each check's pass, issue, inconclusive or not_run status,
severity, counts, explanations, bounded highlight samples, dimensions, tolerance,
timing and optional estimates. volume_valid means a geometrically validated
enclosed volume; its physical size still depends on the units check. Overall
status is issues_found, incomplete or no_issues_found. A basic report remains
incomplete because detailed checks were not run. Errors and time/memory limits
never imply a pass. This API does not certify a successful print.
Workers and analysis modules load only when requested. Models are checked in sequence, with cooperative snapshot copying and aggregate memory admission. The working estimate is 64 MiB plus 2,400 bytes per triangle, with bounded report allowances; a model that can be viewed may be too large to analyze on the same device. One bounded report cache is retained, not the topology/BVH. Geometry, transforms, scale and unit changes invalidate reports; colors and camera changes do not. Removing a checked model, clean/dispose, AbortSignal cancellation or a throwing progress callback releases the active worker and reservations. Report metadata uses weak references so retaining a report cannot retain removed meshes.
Highlights use temporary native WebGL line buffers and are excluded from PNG,
mesh and scene exports. They show up to 128 faces or 256 edges per check, through
occluding surfaces. Closing the site panel clears highlights. The preview exposes
the feature under Info → Check for 3D printing, with selected/all model scope,
basic/detailed depth and collapsed optional FDM settings. Unit confirmation resets
when the checked scope or an existing result's geometry/transform changes. A complete
optional print-* control group is bound by mountViewstlApp; hosts omitting the
entire group keep their previous behavior. The website only supplies markup/styles.
The check panel stays open while using the canvas, other controls or dialogs, loading files, selecting models and clearing the scene. Only its close button or Escape with focus inside the panel dismisses it. Reopening it from Info reveals and focuses the existing panel. Loading or changing the checked selection cancels an active check; stale results are cleared. Loading and empty scenes disable Run and show the current availability without closing the panel.
Model size, units, duplicates, cameras and measurements (rc.6)
All operations below are implemented inside the plugin. The optional site controls remain a thin HTML/CSS host.
viewer.resize_model(id, [2, 2, 2]) doubles the model about its world-space bounds center, preserving rotation and placement. Positive finite XYZ factors are required; independent factors allow nonuniform scaling. It stops only that model's animation. get_model_size(id) returns dimensions and the reset baseline in millimetres. reset_model_size(id) restores that baseline while retaining file-unit corrections.
viewer.set_file_units(id, 'inch') reinterprets a unitless STL/OBJ mesh (default mm) without moving its center. Switching back reverses the conversion. Explicit 3MF/STEP/IGES units are normalized to mm during parsing and cannot be reinterpreted. set_display_units('inch') changes the optional app's size/Info readouts only; scene coordinates, position controls and the grid remain in mm. Existing set_model_units and set_scale retain their legacy semantics.
const copy = viewer.duplicate_model(id) returns a new ID at the same pose, with independent appearance, transforms and metadata. Immutable geometry and matching GPU buffers are shared; memory admission remains conservative. The source reference remains unchanged for VSJ, while display_name identifies the copy. Copied animation definitions and phase are retained but paused; the original keeps playing.
viewer.set_camera_view('top') fits the scene from front/back/left/right/top/bottom/isometric using Z up and the existing perspective projection. It never rotates the models. Camera bookmarks are explicit data:
viewer.set_camera_bookmarks([{name: 'Detail', camera: viewer.get_camera_state()}])
Use get_camera_bookmarks() to obtain a defensive copy and set_camera_state(bookmark.camera) to restore one. Up to 32 uniquely named views (80 characters per name) are supported. They are kept in memory and in scene exports, not localStorage.
const {area_mm2, volume_mm3, volume_valid, volume_reason} = await viewer.measure_model(id)
Measurements use workers and support signal, max_time_ms (default 30 seconds) and on_progress. Area includes all triangles at current scale. Volume is nullable: detailed topology, intersections and shell-orientation checks must establish a closed volume. An open/invalid mesh or an inconclusive/time-limited check never becomes a plausible-looking volume. Results are cached by mesh and scale; position/rotation do not invalidate them and uniform resizing updates cached measurements algebraically. Nonuniform area is recalculated in the worker. Memory limits can reject the calculation before buffers are copied. This is a bounded mesh analysis, not a guarantee of printability.
VSB/VSJ retain size baselines, file-unit interpretation, display units, duplicate labels and bookmarks. VSB still embeds geometry; VSJ still needs the referenced source files. No second unit conversion occurs during import. Appending scenes merges bookmark names with numbered suffixes; a combined total above 32 rejects the incoming scene. Old files without the new optional fields continue to load. The size_state.unit_factor serialization field preserves source-unit correction separately from resize factors for exports with apply_transforms:false.
The optional app control group is listed in src/app-tools.js (toolsControls). Provide the whole group or omit it when embedding older markup. The preview's site/index.html is a complete example. Models contains collapsed Size & units and Duplicate; View contains collapsed Camera & saved views; Info contains area and the volume calculation. No server is involved for local files or measurements.