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 / -elementThe 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:
- Printer dashboards & farm software — show each queued or running job, and overlay live progress from Moonraker / Klipper / Bambu / OctoPrint-class telemetry onto the real toolpath.
- Job & file managers — thumbnail and inspect uploads: layer count, print-time estimate, build volume, which slicer produced them.
- Slicer-adjacent and print-prep tools — let users clip to a layer, scrub segment-by-segment, and check seams, retractions, or travel moves before committing a print.
- Telemetry / progress UIs — render an honest "printed so far" overlay that degrades gracefully when the signal is coarse or stale.
- CNC, laser, and plotter tooling — preview cut/burn/draw paths, color by cut-vs-rapid or tool power, and expand canned drilling cycles (support is tier-gated by hardware evidence — see Formats & compatibility).
Every one of those maps to something the library actually does, below.
- Layer range & segment scrub — isolate any band of layers, or step through the path move by move, with a keyboard-operable slider.
- Time scrub & print-time estimate — scrub along the time axis and read an estimated print time, labeled by provenance (the slicer's own estimate, or a kinematic approximation).
- Feature inspection — toggle travel, wipe, seam, and retraction/de-retraction markers, or
hide a whole feature role (
setFeatureRoleVisible— e.g. hide brim/skirt to declutter a part). - Source-line ↔ segment mapping — go from a byte in the file to the segment it drew, and back.
| 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:
- Known position (e.g. Moonraker
file_position) → a precise cut and a byte-exact marker. - Approximated (a layer index or a bare percentage) → an uncertainty band, not a fake dot.
- Stale signal → the overlay grays out instead of silently freezing a lie.
- User scrub always wins over incoming telemetry.
- Three.js renderer — tube or line geometry with automatic quality fallback, per-file build plates, themes, orthographic/perspective cameras, and WebGL context-loss recovery.
- Canvas 2D fallback — an optional
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. - Camera presets & saved views — seven preset angles (top / bottom / front / back / left /
right / iso), plus a serializable
CameraStateyou can persist and restore. - Frame what matters —
frameContentfits the camera to the printed object (excluding skirt, prime line, and purge) instead of the whole machine volume; the build-volume cage is a separateshowVolumeCagetoggle, independent of the bed/plate. - Interaction-aware quality —
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. - Degradation is disclosed — over a large-file threshold the renderer decimates and tells you the exact reduction factor rather than dropping detail silently.
- Any build surface, not just a rectangle — rectangular, circular / delta, and polygonal
beds render with the outline and grid following the real shape (
buildVolume.shape). - Capture / export —
capture()returns the current view as aBlob(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".
![]() |
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 [#<i> LT #<holes>]
#<a> = [#<i> * 360 / #<holes>]
G0 X[#<cx> + #<r> * COS[#<a>]] Y[#<cy> + #<r> * SIN[#<a>]] ; computed hole position
o100 call ; drill (subroutine)
#<i> = [#<i> + 1]
o200 endwhileParameters, 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> |
- Off-thread parsing — a
GcodeParseSessionruns the parser in a Web Worker with streaming input, progressive previews for large files, resource limits, and cancellation. - Parallel geometry build — a geometry worker pool builds extrusion tubes off the main thread (byte-identical to serial), bounded by a memory budget and degrading gracefully (pool → serial → lines) with every step disclosed. Big plates stay responsive.
- Preparation is staged, the reveal is clean — a
stageevent walksparsing → classifying → building-geometry → preparing-gpu → readywith a real build percentage, andprogressivePreview: 'hold'suppresses the half-built scene so the model appears in one clean reveal instead of visibly growing. - Render diagnostics —
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. - Batteries-included worker — the adapters wire up a worker with every dialect and
.gcode.3mfsupport via the bundler-nativenew Worker(new URL(...))pattern (Vite works out of the box); acreateWorkerhook is the escape hatch for slim builds, custom dialects, or strict-CSP hosts. - Headless still render —
renderStillproduces a single non-interactive image from anOffscreenCanvas, 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 threeimport { 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.
Vue
<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().
Svelte
<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().
Web Component (no framework)
<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.
- Manual & getting started, the visual feature gallery (what it does, shown rather than described), and the API reference (generated from source).
- Concepts: workers & performance · IR & capabilities · dialects & containers · progress & motion
- Guides: cross-adapter tour · recipes
- References: progress-signal contract · live-progress consumer notes · headless still render · support & deprecation policy · multiple previews on one page
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:5199tools/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 andGcodeParseSessionclient: 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.bgcodedecode (license-clean MeatPack + heatshrink), registered as a container adapter so.bgcode"just works".@chestnutlabs/gcode-colors— the renderer-agnosticColorModemodel (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 2DLayerView2D(no WebGL, nothree): 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 headlessrenderModelStill. 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 linksTo 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.


















