Previewing .3mf, .step & .stp with three.js
If you build your own 3D preview on top of three.js, you'll quickly find that .stl and .obj "just work" โ but .3mf files exported from a slicer fail to render, and .step / .stp files aren't supported at all. This guide explains why, starting from three.js fundamentals, then walks through adding browser-side support for all three in TypeScript.
This is a client-side rendering concern, not a Cloud Slicer API concern. The Cloud Slicer API already accepts and quotes .stl, .obj, .3mf, .amf, .step, and .stp โ see Managing Files. This page is about drawing a preview in your own front-end.
three.js in 60 seconds
three.js is a WebGL library for rendering 3D scenes in the browser. Four pieces matter here:
- Scene โ the container holding everything you draw.
- Camera โ the viewpoint (usually a
PerspectiveCamera). - Renderer โ
WebGLRenderer, which paints the scene to a<canvas>. - Mesh โ a visible object, built from a
BufferGeometry(the triangles: vertex positions + indices) and aMaterial(how it looks).
The single most important thing to understand:
three.js only renders meshes โ i.e. triangles. A loader's entire job is to turn a file into a BufferGeometry. Whether a format "works" comes down to one question: does a loader exist that can produce triangles from it?
three.js ships extra loaders in three/examples/jsm/loaders (re-exported by the popular three-stdlib package):
| Format | Loader | Status |
|---|---|---|
.stl | STLLoader | โ Works |
.obj | OBJLoader | โ Works |
.amf | AMFLoader | โ Works |
.3mf | ThreeMFLoader | โ ๏ธ Works for simple files, breaks on slicer files |
.step / .stp | โ | โ No loader exists |
The rest of this guide closes the last two rows.
Why .3mf breaks (the production extension)
A .3mf file is a ZIP archive. A simple one stores all its geometry inline in 3D/3dmodel.model. But Bambu Studio, OrcaSlicer, and PrusaSlicer write the 3MF production extension (requiredextensions="p"): the root 3D/3dmodel.model contains only <build> items and <components> that reference the actual geometry, which lives in separate parts like 3D/Objects/object_1.model, linked by a p:path attribute.
Code
The stock ThreeMFLoader reads every .model part into one flat, objectid-keyed map and only resolves build items from the root part โ it never follows p:path into the external parts. The result is empty geometry, and the loader typically throws. Because slicer "project" files are the dominant .3mf flavor in 3D printing, the stock loader is effectively unusable for real uploads.
A .3mf that renders fine in a quick test (a hand-authored or single-part file) can still fail in production once users upload real slicer exports. Always test with a file exported from Bambu Studio / OrcaSlicer / PrusaSlicer.
The fix: a production-extension-aware loader
Read the ZIP with jszip, parse every *.model part, then resolve each build item by walking its components across parts (using p:path to pick the part) while accumulating transforms.
Code
The transform handling matters: 3MF stores a 4ร3 row-vector affine as 12 numbers. Map it so three.js's column-vector applyMatrix4 reproduces the spec's v * M, and your previews stay dimensionally correct:
Code
Why .step / .stp need a CAD kernel
STEP and IGES are parametric boundary representations (B-rep) โ they describe NURBS surfaces, not triangles. There is nothing for three.js to draw until those surfaces are tessellated into a mesh, and that requires a CAD geometry kernel. This is why three.js has no STEP loader by design.
The fix: OpenCASCADE in the browser (WASM)
occt-import-js is an OpenCASCADE build compiled to WebAssembly. It reads STEP/STP/IGES and returns position/normal/index buffers that map straight onto a BufferGeometry.
Code
You'll need to serve the WASM as a static asset (e.g. copy occt-import-js.wasm into public/wasm/ on postinstall) and add a one-line ambient module declaration since the package ships no types.
Don't "normalize" STEP units with a bounding-box size heuristic. occt already reads the unit declared in the file and emits millimeters โ scaling small parts by a guessed factor corrupts their real dimensions.
Putting it together: format dispatch
Branch on the file extension. The native formats use the stock loaders; the two hard cases use the helpers above.
Code
OBJLoader and AMFLoader return an Object3D/Group, so flatten them (mergeMeshes) by traversing for isMesh children, cloning each geometry, applying mesh.matrixWorld, and merging with mergeBufferGeometries.
If you use a server-side-rendered framework (Next.js, Remix, etc.), make sure the 3MF loader runs client-side only โ it relies on the browser's DOMParser. Use a client component or a dynamic import with SSR disabled.
Verify
- Open one file of each type. For
.3mf, use a file exported from a slicer (Bambu Studio / OrcaSlicer / PrusaSlicer), not just a hand-authored one. - Confirm reported dimensions look right for a part of known size.
- In DevTools โ Network, confirm
occt-import-js.wasmis fetched only when a STEP/STP file is opened โ not on initial page load.
Download: LLM implementation skill
Prefer to hand this to a coding agent? The link below is a self-contained skill file (Markdown with full, copy-pasteable TypeScript for both loaders, install steps, wiring, and gotchas). Drop it into an LLM coding tool โ e.g. as a Claude Code skill โ and ask it to apply the changes to your viewer.
threejs-model-preview-skill.md
Need help?
- Check the API Reference for the file and quote endpoints.
- Visit our GitHub Issues.
- Join our Discord community for live support.