Integration Guide
This guide is for developers who want to embed the OrcaSlicer WASM engine in their own application — a web app, a Node.js backend, a CI pipeline, or any environment that can execute WebAssembly.
What the engine provides
| Capability | Function |
|---|---|
| Slice STL → G-code | orc_session_create + orc_init + orc_slice |
| Slice multiple STLs on one plate → G-code | orc_session_create + orc_init + orc_slice_multi |
| Convert OBJ → binary STL | orc_obj_to_stl (no session needed) |
| Convert STEP → binary STL | orc_cad_to_stl (no session needed) |
orc_init/orc_slice/orc_slice_multi take an opaque session handle (from orc_session_create()) as their first argument — see API Reference → orc_session_create and ADR-008. Create one session and reuse it for every slice; free it with orc_session_destroy() when done. The config set by orc_init persists on that session between slicing calls. Regardless of variant (see below), always run the engine off the main thread — a Web Worker (browser) or a Worker Thread (Node.js) — since a slice call blocks synchronously until it returns.
Engine variants: single-threaded (ST) vs multithreaded (MT)
The build publishes two artifact sets: ST (slicer.js/slicer.wasm) and MT (slicer-mt.js/slicer-mt.wasm, real oneTBB linked against Emscripten pthreads). Most embedders should use ST — it works anywhere. MT requires the page to be crossOriginIsolated (both Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp response headers, which enables SharedArrayBuffer); without cross-origin isolation the MT module will fail to load. See ADR-011 for the deployment requirements and the ST vs MT benchmark for when MT is actually faster — it is not a universal speedup.
Obtaining the WASM artifacts
The two required files are published as an immutable GitHub Release: the
first build for a given OrcaSlicer version is tagged wasm-v2.4.2; a later
fix to orca-wasm/ for the same OrcaSlicer version publishes
wasm-v2.4.2-patchN as a new release instead of overwriting the previous
one. scripts/download-wasm.mjs and deploy.yml both resolve whichever tag
has the highest patch number automatically.
| File | Size | Purpose |
|---|---|---|
slicer.wasm |
~29 MB | Compiled ST engine (OrcaSlicer v2.4.2 + OCCT 7.8.1) |
slicer.js |
~210 KB | Emscripten JS glue (CommonJS IIFE) |
The MT variant (slicer-mt.wasm ~36 MB / slicer-mt.js) is published alongside as a separate release family — see Engine variants above.
Download via the provided script:
Or fetch manually from GitHub Releases and serve the two files from any static host.
Quick start — browser (Web Worker)
Running the engine in a Web Worker keeps the main thread free while the ~29 MB WASM module loads and while slicing runs.
1. Load the engine in a worker
// slicer.worker.ts
import { loadOrcaModule, sliceStl } from './wasm-loader'
let module: Awaited<ReturnType<typeof loadOrcaModule>> | null = null
let session = 0 // created once, reused for every slice (see ADR-008)
self.addEventListener('message', async (e) => {
const { type } = e.data
if (type === 'LOAD') {
module = await loadOrcaModule() // no argument — uses built-in WASM_BASE
session = module._orc_session_create()
self.postMessage({ type: 'READY' })
return
}
if (type === 'SLICE' && module) {
const { stl, config } = e.data
try {
const gcode = sliceStl(module, session, new Uint8Array(stl), JSON.stringify(config))
self.postMessage({ type: 'DONE', gcode })
} catch (err) {
self.postMessage({ type: 'ERROR', message: String(err) })
}
}
})
2. Use it from the main thread
const worker = new Worker(new URL('./slicer.worker.ts', import.meta.url), {
type: 'module',
})
worker.postMessage({ type: 'LOAD' })
worker.onmessage = async (e) => {
if (e.data.type === 'READY') {
console.log('Engine ready')
// Slice a file
const stlBytes: ArrayBuffer = await fetch('/model.stl').then(r => r.arrayBuffer())
worker.postMessage(
{ type: 'SLICE', stl: stlBytes, config: { layer_height: 0.2 } },
[stlBytes], // transfer ownership — zero-copy
)
}
if (e.data.type === 'DONE') {
console.log('G-code length:', e.data.gcode.length)
}
}
Quick start — Node.js (Worker Thread)
The WASM engine runs under Node.js without changes. Use worker_threads instead of Web Worker.
// slicer.mts
import { workerData, parentPort } from 'node:worker_threads'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
// Load Emscripten CommonJS glue
const require = createRequire(import.meta.url)
const jsText = readFileSync('./wasm/slicer.js', 'utf8')
const wasmBinary = readFileSync('./wasm/slicer.wasm').buffer
// Eval the IIFE so OrcaModule factory is available
const fn = new Function('module', 'exports', jsText + '\nreturn OrcaModule')
const factory = fn({}, {})
const module = await factory({ wasmBinary })
// One session per worker/process, reused for every slice (ADR-008)
const session = module._orc_session_create()
if (!session) throw new Error('orc_session_create failed')
// Slice
const stl = readFileSync('./model.stl')
const configJson = JSON.stringify({ layer_height: 0.2, nozzle_diameter: 0.4 })
const enc = new TextEncoder()
const configBytes = enc.encode(configJson)
const configPtr = module._malloc(configBytes.length)
module.HEAPU8.set(configBytes, configPtr)
const initCode = module._orc_init(session, configPtr, configBytes.length)
module._free(configPtr)
if (initCode !== 0) throw new Error(`orc_init failed: ${initCode}`)
const stlPtr = module._malloc(stl.length)
module.HEAPU8.set(stl, stlPtr) // Buffer extends Uint8Array — pass directly to avoid pool-offset bug
const ptrPtr = module._malloc(4)
const lenPtr = module._malloc(4)
const rc = module._orc_slice(session, stlPtr, stl.length, ptrPtr, lenPtr)
module._free(stlPtr)
if (rc !== 0) throw new Error(`orc_slice failed: ${rc} — ${module.UTF8ToString(module._orc_decode_exception(session))}`)
const gcodePtr = module.getValue(ptrPtr, 'i32')
const gcodeLen = module.getValue(lenPtr, 'i32')
const gcode = module.UTF8ToString(gcodePtr, gcodeLen)
module._orc_free(gcodePtr)
module._free(ptrPtr)
module._free(lenPtr)
console.log('G-code lines:', gcode.split('\n').length)
Loading slicer.js without a bundler
Emscripten emits a CommonJS IIFE (var OrcaModule = ...). Browsers cannot import() it directly. The standard pattern used by this project:
async function loadEngine(baseUrl: string) {
const [jsText, wasmBinary] = await Promise.all([
fetch(`${baseUrl}/slicer.js`).then(r => r.text()),
fetch(`${baseUrl}/slicer.wasm`).then(r => r.arrayBuffer()),
])
// Append an ES default export so the IIFE becomes importable
const blob = new Blob(
[`${jsText}\nexport default OrcaModule;`],
{ type: 'application/javascript' },
)
const url = URL.createObjectURL(blob)
try {
const { default: factory } = await import(/* @vite-ignore */ url)
return factory({ wasmBinary, locateFile: (p: string) => `${baseUrl}/${p}` })
} finally {
URL.revokeObjectURL(url)
}
}
The wasmBinary option tells Emscripten not to re-fetch slicer.wasm — the binary is already in memory.
Slicing an STL file
import { sliceStl } from './lib/wasm-loader'
// module = loaded OrcaModule (see above)
// session = module._orc_session_create(), created once and reused (ADR-008)
// stlData = Uint8Array of the .stl file (binary or ASCII both accepted)
// configJson = JSON string of OrcaConfig fields
const gcode: string = sliceStl(module, session, stlData, configJson)
Under the hood, sliceStl calls orc_init then orc_slice on the given session and handles WASM heap allocation / deallocation. It throws OrcaSliceError on failure.
Minimal config
All fields are optional — unset fields use OrcaSlicer built-in defaults.
Slicing multiple files onto one plate
orc_slice_multi arranges all objects automatically using OrcaSlicer's arrange_objects() (libnest2d + NLopt) with a 2 mm gap. Objects that don't fit are placed at bed centre instead of failing.
import { sliceMultiStl } from './lib/wasm-loader'
// stls = array of Uint8Array, one per file
// offsets = Int32Array with pairs [start0, len0, start1, len1, ...]
// built by concatenating all stls into one buffer
const totalLen = stls.reduce((s, a) => s + a.length, 0)
const combined = new Uint8Array(totalLen)
const offsets = new Int32Array(stls.length * 2)
let pos = 0
for (let i = 0; i < stls.length; i++) {
combined.set(stls[i], pos)
offsets[i * 2] = pos
offsets[i * 2 + 1] = stls[i].length
pos += stls[i].length
}
const gcode = sliceMultiStl(module, session, combined, offsets, stls.length, configJson)
Optionally assign each object a 1-based "extruder"/filament slot (single-nozzle multi-material — see API Reference → orc_slice_multi):
const extruderIds = Int32Array.from([1, 2]) // one entry per file, 0 = default
const gcode = sliceMultiStl(module, session, combined, offsets, stls.length, configJson, extruderIds)
Converting OBJ to STL
orc_obj_to_stl does not require orc_init. Call it standalone.
import { objToStl } from './lib/wasm-loader'
const stlBytes: Uint8Array = objToStl(module, objData)
// stlBytes is binary STL — feed it to orc_slice or preview in a 3D viewer
Supported OBJ features: triangle and quad faces (quads auto-triangulated), multiple named objects (merged into one mesh), v/vt/vn vertex format variants.
Ignored: MTL material files, vertex colours, NURBS curves/surfaces.
Converting STEP to STL
orc_cad_to_stl reads STEP files using OrcaSlicer's embedded OCCT 7.8.1 reader. Does not require orc_init.
import { cadToStl } from './lib/wasm-loader'
const stlBytes: Uint8Array = cadToStl(module, stepData)
// stlBytes is binary STL
IGES is not supported
OrcaSlicer's STEP reader (STEPCAFControl_Reader) does not accept IGES files. Only .step/.stp extensions are supported.
Tessellation parameters used: linear deflection 0.003 mm, angular deflection 0.5° (OrcaSlicer defaults).
Error handling
All four helper functions (sliceStl, sliceMultiStl, objToStl, cadToStl) throw OrcaSliceError on failure.
import { OrcaSliceError } from './lib/wasm-loader'
try {
const gcode = sliceStl(module, session, stlData, configJson)
} catch (err) {
if (err instanceof OrcaSliceError) {
console.error(`Slice failed (code ${err.code}): ${err.message}`)
}
}
OrcaSliceError.message is the human-readable message returned by orc_decode_exception (last error stored by the C++ bridge), with a fallback to a code-based description.
When calling C functions directly, retrieve the last error with the same session used for the failing call:
For orc_obj_to_stl/orc_cad_to_stl (no session), pass 0:
Using the built-in Worker message protocol
If you don't want to write your own worker, use the existing slicer.worker.ts via the worker-singleton.ts module. This is how the OrcaWeb app itself works.
import { getWorker, addWorkerListener } from './lib/worker-singleton'
// Start loading (only happens once per session)
const worker = getWorker()
// Listen for responses
const unsub = addWorkerListener((msg) => {
switch (msg.type) {
case 'WASM_LOADED':
console.log('Engine ready')
break
case 'SLICE_COMPLETE':
console.log('G-code:', msg.gcode.slice(0, 200))
unsub()
break
case 'SLICE_ERROR':
console.error(msg.message)
unsub()
break
}
})
// Wait for WASM_LOADED before sending SLICE
worker.postMessage({
type: 'SLICE',
stl: stlBuffer, // ArrayBuffer — transferred zero-copy
config: { layer_height: 0.2 },
}, [stlBuffer])
See the API Reference → Worker message protocol for the full list of message types.
Config reference (key fields)
See API Reference → Config JSON schema for the full schema. The most commonly set fields:
{
// Bed geometry — used for model centering
bed_size_x: 256, // mm
bed_size_y: 256, // mm
bed_shape: 'rectangle', // or 'circle' for delta printers
// Nozzle
nozzle_diameter: 0.4, // mm
// Layer
layer_height: 0.2, // mm
initial_layer_height: 0.2,
// Filament
filament_type: 'PLA',
nozzle_temperature: 220, // °C
bed_temperature: 60, // °C
// Infill
sparse_infill_density: 15, // %
sparse_infill_pattern: 'grid',
// Supports
enable_support: false,
// Pass-through for any OrcaSlicer key not listed above
_passthrough: {
max_layer_height: '0.28',
min_layer_height: '0.07',
},
}
_passthrough field
Any OrcaSlicer config key not covered by OrcaConfig can be forwarded verbatim via _passthrough. Values must be strings (the same format OrcaSlicer stores them internally):
_passthrough: {
fuzzy_skin: 'external',
fuzzy_skin_thickness: '0.3',
fuzzy_skin_point_dist: '0.8',
}
Memory management (direct C API)
The WASM heap is managed by Emscripten. All input buffers must be copied onto the heap before calling C functions, and all output buffers must be freed with orc_free.
// One session per module instance, created once and freed at shutdown
const session = module._orc_session_create()
// ... use it for every orc_init/orc_slice/orc_slice_multi call ...
module._orc_session_destroy(session)
// Allocate + write
const ptr = module._malloc(bytes.length)
module.HEAPU8.set(bytes, ptr)
// Read an int pointer
const innerPtr = module.getValue(ptrPtr, 'i32')
const length = module.getValue(lenPtr, 'i32')
// Read a C string
const str = module.UTF8ToString(ptr) // null-terminated
const str = module.UTF8ToString(ptr, length) // known length
// Free C-allocated buffers (returned by orc_slice / orc_obj_to_stl / orc_cad_to_stl)
module._orc_free(innerPtr)
// Free JS-allocated buffers (allocated with _malloc)
module._free(ptr)
orc_free vs _free
Use module._orc_free(ptr) for buffers returned by orc_slice, orc_slice_multi, orc_obj_to_stl, and orc_cad_to_stl. These are malloc'd by the C++ bridge.
Use module._free(ptr) for buffers you allocated yourself with module._malloc().
Use module._orc_session_destroy(session) — not _free/_orc_free — to release a session handle.
Bed geometry
bed_size_x and bed_size_y are used to center models on the virtual bed. bed_shape affects how orc_slice_multi bounds the auto-arrangement area:
bed_shape |
Arrangement boundary |
|---|---|
rectangle (default) |
Full bed_size_x × bed_size_y rectangle |
circle |
Largest square inscribed in the circle (side = radius × √2) |
For circular beds, the effective printable area used by the arranger is the inscribed square — objects are never placed in the corners that fall outside the circle.
Complete TypeScript example (browser, no framework)
// Module-level cache — engine is fetched and compiled only once
let _enginePromise: Promise<any> | null = null
function loadEngine(wasmBase: string): Promise<any> {
if (!_enginePromise) {
_enginePromise = (async () => {
const [jsText, wasmBinary] = await Promise.all([
fetch(`${wasmBase}/slicer.js`).then(r => r.text()),
fetch(`${wasmBase}/slicer.wasm`).then(r => r.arrayBuffer()),
])
const blob = new Blob([`${jsText}\nexport default OrcaModule;`], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
try {
const { default: factory } = await import(url)
return factory({ wasmBinary })
} finally {
URL.revokeObjectURL(url)
}
})()
}
return _enginePromise
}
// One session, created once and reused for every slice (ADR-008)
let _session = 0
// Slice a single file (engine + session are reused across calls)
async function sliceFile(stlBytes: Uint8Array, config: object): Promise<string> {
const module = await loadEngine('/wasm')
if (!_session) {
_session = module._orc_session_create()
if (!_session) throw new Error('orc_session_create failed')
}
const enc = new TextEncoder()
const cfgBytes = enc.encode(JSON.stringify(config))
const cfgPtr = module._malloc(cfgBytes.length)
module.HEAPU8.set(cfgBytes, cfgPtr)
const initCode = module._orc_init(_session, cfgPtr, cfgBytes.length)
module._free(cfgPtr)
if (initCode !== 0) {
throw new Error(`orc_init: ${module.UTF8ToString(module._orc_decode_exception(_session))}`)
}
const stlPtr = module._malloc(stlBytes.length)
module.HEAPU8.set(stlBytes, stlPtr)
const ptrPtr = module._malloc(4)
const lenPtr = module._malloc(4)
const rc = module._orc_slice(_session, stlPtr, stlBytes.length, ptrPtr, lenPtr)
module._free(stlPtr)
if (rc !== 0) {
module._free(ptrPtr)
module._free(lenPtr)
throw new Error(`orc_slice (${rc}): ${module.UTF8ToString(module._orc_decode_exception(_session))}`)
}
const gcodePtr = module.getValue(ptrPtr, 'i32')
const gcodeLen = module.getValue(lenPtr, 'i32')
const gcode = module.UTF8ToString(gcodePtr, gcodeLen)
module._orc_free(gcodePtr)
module._free(ptrPtr)
module._free(lenPtr)
return gcode
}
// Usage
const stl = await fetch('/model.stl').then(r => r.arrayBuffer())
const gcode = await sliceFile(new Uint8Array(stl), {
layer_height: 0.2,
nozzle_diameter: 0.4,
bed_size_x: 256,
bed_size_y: 256,
nozzle_temperature: 220,
bed_temperature: 60,
})
console.log(gcode.slice(0, 500))