Show a 3D print or CNC/laser job in the browser — before, during, or after it runs.
Drop .gcode, .gcode.3mf, or Prusa .bgcode into a web page and get an interactive toolpath
view: orbit it, clip it to a layer, scrub through it, color it by feature or speed, and overlay
live printer progress. Or view the source model (STL / 3MF) before it's sliced. Parsing runs in
a Web Worker, so a 250 MB file never freezes your UI.

▶ Try the live demo — open the interactive Feature Lab, or the React / Vue / Svelte / Web Component examples, right in your browser. Nothing is uploaded; parsing runs in a Web Worker.
G-code Preview covers both halves of looking at a print job. Preview turns a sliced G-code
file into an interactive picture of the actual toolpath — reading the file the way the printer will,
every move, layer, feature, and tool change, drawn with Three.js (or a Canvas 2D fallback for
low-GPU devices). Prepare shows the source model (STL / 3MF) before slicing — the object,
its parts, and its material colors. It ships as a set of @chestnutlabs/* npm packages with
ready-made Vue, React, Svelte, and Web Component components, all built on one shared engine.
One rule runs through the whole thing: it shows what it can prove and refuses to fake the rest. If a slicer didn't record feature types, the viewer says so instead of inventing colors. If live telemetry only reports a percentage, it draws an uncertainty band, not a false-precise nozzle dot.
npm install @chestnutlabs/gcode-preview-react three # or -vue / -svelte / -element
| Prepare — the source model | Preview — the sliced toolpath |
|---|---|
![]() |
![]() |
| STL / 3MF solid model — objects, materials, model controls, honest material-color tier. | Sliced G-code — layers, features, speed, travel, retractions, and toolpath controls. |
The controls and capabilities are front-and-centre, not hidden behind a render:
| Color modes, capability-gated | Render diagnostics & capabilities |
|---|---|
![]() |
![]() |
You can drive all of this yourself in the live demos. A cropped beauty render — the same engine, no UI:

You are building something that needs to look at a G-code file, and you don't want to write a parser and a WebGL renderer to do it:
Every one of those maps to something the library actually does, below.
![]() |
![]() |
| Layer clipping & scrub — set a layer range or scrub segment-by-segment. Draw-range updates, no geometry rebuilds. | Color by speed — and by feature role, tool, object, per-layer height, or M600 color change. |
![]() |
![]() |
| Retraction & seam inspection — toggle retraction/de-retraction markers, wipe, and seam moves. | Color by layer height — spot variable-layer-height regions at a glance. |
setFeatureRoleVisible — e.g. hide brim/skirt to declutter a part).| Known position | Approximated position |
|---|---|
![]() |
![]() |
| Byte-exact telemetry → a precise cut and an exact position marker. | A layer index or bare percentage → an uncertainty band, not a fake dot. |
Feed it your printer's telemetry and it maps that signal onto the toolpath at the confidence the signal deserves:
file_position) → a precise cut and a byte-exact marker.renderer="2d" layer view with no WebGL and no Three.js, for
low-GPU / low-memory / WebGL-blocked devices. A 2D-only bundle never ships Three.js.CameraState you can persist and restore.frameContent fits the camera to the printed object (excluding skirt,
prime line, and purge) instead of the whole machine volume; the build-volume cage is a separate
showVolumeCage toggle, independent of the bed/plate.interactionQuality: 'auto' drops render detail while the camera
is moving and restores it when the view settles, keeping orbit responsive on big models without
giving up final-frame fidelity. The hard GPU/vertex-budget fallback still applies underneath.buildVolume.shape).capture() returns the current view as a Blob (PNG/JPEG/WebP) from the
interactive viewer or the headless still, including a transparent background for compositing
onto cards — the same primitive behind server thumbnails and "save image".![]() |
Non-rectangular beds. A circular (delta) or polygonal build surface draws its true outline and clips the grid to the shape, instead of forcing a rectangle. Pass a shape on the build volume; rectangular stays the default. |
![]() |
Canvas 2D fallback. The same parse feeds a flat layer view with no WebGL and no Three.js — current layer plus adjacent "ghost" layers — for devices where the 3D renderer can't run. |
The same 3DBenchy from three preset angles — one click each:
![]() |
![]() |
![]() |
| Front (ortho) | Top (ortho) | Iso |

Non-extrusion toolpaths classify tool-engaged moves as Cut, expose a modal tool-power channel
(laser power / spindle RPM), and expand canned drilling cycles (G81 / G82 / G83). Coloring by
cut-vs-rapid or tool power makes the difference visible. This support is honesty-tiered:
a controller's semantic claims are reported inferred until confirmed on real hardware, then known
— today GRBL/LightBurn laser is hardware-validated; GRBL-mill and LinuxCNC are experimental.
Geometry always parses regardless of tier.
Some G-code doesn't spell out its geometry move by move — it computes it. A LinuxCNC / CAM
program can set variables, evaluate expressions as coordinates, loop, branch, and call subroutines,
so a bolt circle is a while loop over an angle instead of twelve explicit moves. The parser runs
that programming layer, so these files resolve to the real toolpath instead of rendering empty.

o200 while [# LT #]
# = [# * 360 / #]
G0 X[# + # * COS[#]] Y[# + # * SIN[#]] ; computed hole position
o100 call ; drill (subroutine)
# = [# + 1]
o200 endwhile
Parameters, expressions, if / while / do / repeat control flow, and in-file subroutines all
run — bounded (a hostile file can't hang the parser: maxProgramIterations / maxCallDepth) and
honest (a clean run reports parametricProgram: 'known'; a limit or unsupported construct drops
to approximated with a specific warning). It engages only on files that use # / [ / O-words;
every FDM file is byte-for-byte unchanged. See
Parametric programs.
Four adapters over one engine, with matching options, events, and TypeScript types — enforced by a
shared behavioral test suite (with a controls/state parity guard) that runs against all four in CI.
Each adapter exposes both viewers: the toolpath <GcodePreview> from the package root, and the
source-model <ModelViewer> from the package's /model subpath (the Prepare side).
| Adapter | Package | Preview (toolpath) | Prepare (source model, /model) |
|---|---|---|---|
| Vue 3 | @chestnutlabs/gcode-preview-vue |
<GcodePreview> + useGcodePreview() |
<ModelViewer> + useModelViewer() |
| React | @chestnutlabs/gcode-preview-react |
<GcodePreview> + useGcodePreview() (StrictMode-safe) |
<ModelViewer> + useModelViewer() |
| Svelte | @chestnutlabs/gcode-preview-svelte |
<GcodePreview> + createGcodePreview() |
<ModelViewer> + createModelViewer() |
| Web Component | @chestnutlabs/gcode-preview-element |
<gcode-preview> (no framework peer) |
<gcode-model-viewer> |
GcodeParseSession runs the parser in a Web Worker with streaming
input, progressive previews for large files, resource limits, and cancellation.stage event walks
parsing → classifying → building-geometry → preparing-gpu → ready with a real build percentage,
and progressivePreview: 'hold' suppresses the half-built scene so the model appears in one clean
reveal instead of visibly growing.getRenderStats() reports what actually happened: backend and WebGL
version, hardware-vs-software GPU (unmasked renderer/vendor), geometry mode, rendered segment and
draw-call counts, whether decimation applied, worker count, and a timing breakdown — a ready-made
"what am I running on / why is this slow" panel, never fabricated..gcode.3mf
support via the bundler-native new Worker(new URL(...)) pattern (Vite works out of the box); a
createWorker hook is the escape hatch for slim builds, custom dialects, or strict-CSP hosts.renderStill produces a single
non-interactive image from an OffscreenCanvas, an Electron hidden window, or headless Chromium,
for server-side thumbnails.Everything above renders the toolpath — the moves the machine makes. That answers how the job runs: where the seams are, which layer a retraction happens on, how travel threads between islands. But sometimes you don't want the toolpath at all — you want a clean picture of what the object is, the way a slicer's file browser shows a part. Those are two different jobs, so they're two different renderers:

| Toolpath renderer | Model renderer | |
|---|---|---|
| Package | @chestnutlabs/gcode-renderer-three (+ -2d) |
@chestnutlabs/gcode-model-renderer |
| Input | Parsed G-code (ToolpathIR) |
The source model — .stl or .3mf mesh |
| Answers | How does this print/cut run? | What object is this? |
| Looks like | Extrusion tubes / lines, layers, travel, color modes | A solid, studio-lit part at a fixed 3/4 angle |
| Use it for | Inspection, clip/scrub, live progress, seams | Thumbnails, cards, library grids, "what's in this file" |
The model renderer is a presentation surface, not a second toolpath viewer. It takes an STL or a 3MF mesh, frames it, lights it with a neutral studio rig, and draws it — nothing about layers, moves, or print order. 3MF brings its own multi-object structure and per-object / per-triangle material colors; a bare STL is a single object with no declared material.
It keeps the same honesty rule as the rest of the library. When the source actually declares colors
(3MF basematerials), the render uses them. When it doesn't — a plain STL, or a proprietary paint
format the standard doesn't cover — it draws a neutral default and reports materials: 'unavailable'
rather than inventing a color. The headless
renderModelStill mirrors renderStill: hand it bytes,
get back a canvas plus a stable cacheKey and the materials confidence for that render.
import { renderModelStill } from '@chestnutlabs/gcode-model-renderer';
const { canvas, materials, cacheKey } = await renderModelStill(
{ kind: 'stl', bytes }, // or { kind: '3mf', bytes }
{ canvas: new OffscreenCanvas(512, 512), background: 'transparent' }
);
// materials: 'known' when the source carried colors, 'unavailable' when it didn't.
Need it live rather than as a thumbnail — a "View in 3D" for the part? Interactive source-model
viewing is a first-class, declarative half of the SDK: drop <ModelViewer source={…} /> from any
framework's /model subpath (@chestnutlabs/gcode-preview-{react,vue,svelte}/model, or
<gcode-model-viewer> from the Web Component) and orbit/zoom/pan the same STL / 3MF (including
production multicolor) with camera presets, serializable camera state, per-plate render scope, and the
honest materials capability tier — the same parity (props, events, lifecycle, cleanup) as the
toolpath component. It wraps the framework-neutral createModelPreviewController over the shared
createModelViewer engine. See the adapters guide ("Two viewers: Preview
and Prepare") and the model-renderer README.
Multi-plate, multi-object, or just a subset. A 3MF project can hold several plates and many
objects. Parse or render one plate at a time (parseOptions.plate), and a render scope
({ plateId }, { objectIds }, or an instance filter) narrows a still or the viewer to exactly the
plate or objects you want — a single plate's thumbnail, or one part isolated from the rest — with a
stable cache key per scope.
Install an adapter plus three (the renderer declares three as a peer dependency, range
^0.178.0; npm ≥ 7 installs it automatically, pnpm/yarn users add it explicitly):
npm install @chestnutlabs/gcode-preview-react three
import { GcodePreview } from '@chestnutlabs/gcode-preview-react';
function Viewer({ file }) { // file: a File from an <input>, or a Uint8Array
return (
<div style={{ height: '70vh' }}>
<GcodePreview source={file} onReady={(s) => console.log(`${s.segments} segments`)} />
</div>
);
}
<GcodePreview source={file} /> is the whole thin path; the full viewer — layer clip, scrub, color
modes, cameras, live progress — is reachable through props and the lower-level hook without
switching APIs.
<script setup>
import { GcodePreview } from '@chestnutlabs/gcode-preview-vue';
import { shallowRef } from 'vue';
const file = shallowRef(null);
</script>
<template>
<input type="file" accept=".gcode,.3mf,.bgcode" @change="file = $event.target.files?.[0] ?? null" />
<div style="height: 70vh">
<GcodePreview :source="file" @ready="(s) => console.log(`${s.segments} segments`)" />
</div>
</template>
Lower level: useGcodePreview().
<script>
import GcodePreview from '@chestnutlabs/gcode-preview-svelte/GcodePreview.svelte';
let file = null;
</script>
<div style="height: 70vh">
<GcodePreview source={file} on:ready={(e) => console.log(`${e.detail.segments} segments`)} />
</div>
Ships as raw .svelte (your bundler's Svelte plugin compiles it). Lower level:
createGcodePreview().
<script type="module">
import '@chestnutlabs/gcode-preview-element/define'; // registers <gcode-preview>
</script>
<gcode-preview quality="tubes" style="display:block;height:70vh"></gcode-preview>
<script type="module">
const el = document.querySelector('gcode-preview');
el.source = await (await fetch('/model.gcode')).arrayBuffer(); // Uint8Array | ArrayBuffer | File
</script>
See @chestnutlabs/gcode-preview-element for the full
attribute/property table.
All four adapters share the same option surface — source, parseOptions, buildVolume,
quality, colorMode, layerRange, scrub, showTravel, progress, cameraMode, view,
cameraState, showVolumeCage, frameContent, interactionQuality, theme, createWorker —
with matching events (ready — now carrying parsed slice metadata when the dialect supplied it —
parse-progress, build-complete, quality-fallback, error, and more). The
cross-adapter guide and each package README are the canonical reference.
Input formats
| Format | Support |
|---|---|
.gcode (plain) |
Full geometry always. |
.gcode.3mf (Orca/Bambu container) |
Bounded, zero-dependency ZIP extraction; multi-plate via parseOptions.plate. |
.bgcode (Prusa binary) |
Decoded (heatshrink / DEFLATE / MeatPack) to plain G-code through the same pipeline, byte-for-byte equivalent. |
Slicer & firmware dialects annotate the toolpath with feature roles, objects, bed geometry, and more: PrusaSlicer, OrcaSlicer / Bambu Studio, Cura, Klipper, Marlin, and RepRap-flavor. Every annotation carries a confidence tier (below). See the evidence-dated compatibility matrix and G-code motion coverage for the exact per-dialect state.
The honesty model. Every derived fact is tagged with how sure the parser is:
| Tier | Meaning |
|---|---|
known |
Observed directly from the file. |
inferred |
Not stated, resolved from a defensible default — and disclosed as a default. |
approximated |
Derived with known error (e.g. a position between two sparse signals). |
unavailable |
Cannot be determined from this input. |
This is why capability-gated features (a color mode, a live-progress marker, a CNC classification) either work from real data or explain why they can't — they never render something the file didn't contain. Details: ToolpathIR & the capability model.
tools/demo is the showcase: the whole pipeline behind one control panel — corpus picker, dialect
annotations, quality/color modes, layer clip + time/segment scrub, camera presets and save/restore,
simulated live-progress tiers, themes, and STL export.
cd tools/demo
npm install
npm run dev # http://localhost:5199
tools/example-react and tools/example-svelte are complete standalone Vite apps. All three consume
the packages exactly as an external consumer would.
The stack is a pipeline of small packages, each doing one job:
G-code ─▶ parser (Web Worker) ─▶ ToolpathIR ─▶ toolpath renderer ─▶ canvas
dialects · containers (neutral, three / 2d
· bgcode decode versioned) + colors
STL / 3MF mesh ─────────────────▶ ModelScene ─▶ model renderer ─▶ canvas
(presentation) (shared render stage)
The two rows share the render "stage" (framing + GL builder) but are otherwise independent: the top row inspects the toolpath, the bottom row presents the source model (see Two views, two jobs).
@chestnutlabs/toolpath-core — ToolpathIR (structure-of-arrays
geometry + metadata + source index), the capability model, progress mapping, and time estimation.
The neutral seam every other package reads or writes.@chestnutlabs/gcode-parser — the worker parse core and
GcodeParseSession client: streaming input, progressive previews, resource limits, cancellation.@chestnutlabs/gcode-dialects — slicer/firmware annotators. They
add metadata and optional channels; they can never alter geometry.@chestnutlabs/gcode-containers — bounded, in-memory
.gcode.3mf / ZIP extraction, hardened against adversarial archives.@chestnutlabs/gcode-bgcode — Prusa .bgcode decode (license-clean
MeatPack + heatshrink), registered as a container adapter so .bgcode "just works".@chestnutlabs/gcode-colors — the renderer-agnostic ColorMode
model (feature, speed, tool, object, layer-height, color-change, tool-power, cut-vs-rapid), shared
by both toolpath renderers.@chestnutlabs/gcode-renderer-three — the Three.js toolpath
renderer (peer: three): layer chunks, decimation disclosure, draw-range clip/scrub, cameras,
themes, live-progress overlay. Also exports the shared render "stage" (framing pose + GL builder).@chestnutlabs/gcode-renderer-2d — the Canvas 2D LayerView2D
(no WebGL, no three): current + adjacent "ghost" layers.@chestnutlabs/gcode-model-renderer — a separate
presentation renderer (peer: three) for the source model — STL and 3MF multi-object/material —
with headless renderModelStill. It answers "what object is this?", not "how does the toolpath
run?"; see Two views, two jobs.@chestnutlabs/gcode-preview-core — the framework-neutral
controller, immutable state model, renderStill, and the portable behavioral suite.-vue · -react ·
-svelte · -element — the
four framework adapters.Design rationale, boundaries, and the accepted architecture live in
docs/design/; the docs index maps the whole set. Fourteen
packages publish to npm in lockstep (currently v0.20.1) with npm provenance.
# Node >= 22
npm ci
npm run build # legacy engine build (rollup)
npm run test # root suite (IR goldens, manifest validation, adapters)
npm run test:packages # all workspace package suites
npm run lint && npm run typeCheck && npm run license:check
npm run docs:links # verify documentation links
To rebuild the workspace packages the demo consumes, build them in dependency order
(toolpath-core → colors/containers/dialects → bgcode → parser → renderers → core → adapters);
see tools/screenshots/README.md for the documentation-media
capture harness.
Contributing: CONTRIBUTING.md · security policy: SECURITY.md · documentation standard: docs/USER_FACING_DOCS_STYLE.md.
This project began as a fork of
xyz-tools/gcode-preview (project identity
remcoder/gcode-preview) by Remco Veldkamp and contributors, MIT-licensed. Chestnut Labs has
rebuilt it into the worker-based, multi-package browser toolpath toolkit described above. The
inherited Git history is preserved, upstream copyright notices are retained in LICENSE,
and the full provenance record lives in NOTICE.md,
docs/UPSTREAM_PROVENANCE.md, and the
upstream & licensing policy. Upstream
changes are adopted deliberately through review, never auto-synced.
MIT — inherited code © 2017–2025 Remco Veldkamp and the xyz-tools/gcode-preview
contributors; Chestnut Labs additions © 2026 Chestnut Labs.