@push.rocks/smartscad
Fast native OpenSCAD rendering for Node.js. smartscad runs OpenSCAD as a long-lived native worker process instead of starting the openscad CLI per render. The worker keeps OpenSCAD's geometry cache between renders, so re-rendering an edited model only re-evaluates the parts that changed, and it computes geometry on several cores through Manifold's parallel backend.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add @push.rocks/smartscad
The package bundles the worker for Linux x64 (glibc 2.39 or newer, e.g. Ubuntu 24.04 and the ht-docker-node:lts image). The worker's shared libraries ship next to it, so nothing else needs to be installed. Use SmartScad.isPlatformSupported() to check at runtime; on other platforms start() and render() reject with EWORKER.
text() uses fontconfig, so models with text need fonts installed on the machine (for example fonts-liberation).
Models are rendered from source text, not from files: relative paths in use, include and import() do not resolve.
Usage
import { SmartScad } from '@push.rocks/smartscad';
const scad = new SmartScad();
const result = await scad.render('difference(){ cube(20, center=true); sphere(r=13, $fn=96); }');
result.data; // Buffer with a binary STL
result.messages; // echo() output, warnings: [{ group: 'ECHO', message: '...', line: 3 }]
result.timing; // { evaluateMs, exportMs }
await scad.stop();
Workers start on the first render, or up front with await scad.start(). They keep running until stop().
Re-rendering edited models
OpenSCAD caches the geometry of every subtree it evaluates. Because the worker stays alive, a render of an edited model reuses everything that did not change. A model of 100 spheres with a grid of cut cylinders, rendered through SmartScad.render() with threadsPerWorker: 8 on a busy 32-core machine:
| Render | End to end | OpenSCAD evaluation |
|---|---|---|
| first render | 3.0 s | 2.3 s |
| same source again | 0.23 s | 3 ms |
| one row of cylinders changed | 1.2 s | 0.4 s |
The rest of each render is extracting the mesh and transferring the 12.7 MB STL. The OpenSCAD WebAssembly build takes 4.9 s for every render of that model.
With several workers, pass a cacheKey so successive versions of one model land on the worker that holds its geometry:
const scad = new SmartScad({ workers: 4 });
await scad.render(source, { cacheKey: documentId });
Options
new SmartScad({
workers: 1, // worker processes; each renders one model at a time
threadsPerWorker: 8, // Manifold threads per render (default: available CPUs / workers)
cacheMegabytes: 1024, // geometry cache per worker
renderTimeoutMs: 120_000, // default timeout per render
binaryPath: '/opt/smartscad-worker', // explicit worker binary
});
await scad.render(source, {
format: 'binstl', // 'binstl' | 'asciistl' | 'stl' | 'off' | 'obj'
timeoutMs: 30_000,
cacheKey: 'part-42',
});
await scad.clearCache(); // drop every worker's cached geometry
Set threadsPerWorker to the CPUs actually available when the process runs under a CPU quota (a container with --cpus): the thread pool sizes itself by the machine's cores, and an oversubscribed pool is slower than a single thread.
Errors
Failed renders reject with a SmartScadError carrying a code and the messages OpenSCAD logged:
| Code | Meaning |
|---|---|
EPARSE |
The source does not parse; the ERROR message carries the line. |
EDIMENSION |
The top level object is not 3D. |
EEMPTY |
The top level object is empty. |
EFORMAT |
Unsupported export format. |
ERENDER |
OpenSCAD failed while evaluating or exporting. |
ETIMEOUT |
The render exceeded its timeout. OpenSCAD cannot abandon a render midway, so the worker is replaced; its cache is lost. |
EWORKER |
The worker is missing, failed to start or crashed, or the renderer was stopped. |
import { SmartScadError } from '@push.rocks/smartscad';
try {
await scad.render('cube(10');
} catch (error) {
if (error instanceof SmartScadError && error.code === 'EPARSE') {
console.log(error.messages); // [{ group: 'ERROR', message: 'Parser error: ...', line: 1 }]
}
}
How it works
cpp/worker.ccis a small C++ front end on OpenSCAD's library. It initializes OpenSCAD once, then reads render requests from stdin and answers on stdout using the newline-delimited JSON protocol of@push.rocks/smartrust. Each render parses the source, evaluates it, builds the geometry and exports it to memory; OpenSCAD's geometry cache stays alive between renders.- OpenSCAD is built headless (no GUI, no OpenGL) with the Manifold backend and its TBB parallel mode, from the commit pinned in
cpp/openscad.json. The worker target is added to OpenSCAD's own CMake build throughCMAKE_PROJECT_OpenSCAD_INCLUDE(cpp/smartscad-worker.cmake), so OpenSCAD's sources stay unmodified. - On the TypeScript side,
SmartScaddrives each worker through smartrust'sRustBridgeand queues renders per worker.
Building the worker
pnpm build # TypeScript and the native worker
pnpm build:native # only the native worker
build:native needs Docker and a Linux x64 host. It fetches the pinned OpenSCAD commit into .nogit/native/openscad, builds the worker in an Ubuntu 24.04 image (cpp/Dockerfile, cpp/build.sh) and assembles dist_cpp/linux_amd64/:
| Path | Content |
|---|---|
smartscad-worker |
the worker binary, which finds its libraries through $ORIGIN/lib |
lib/ |
every shared library the worker loads except the C and C++ runtime |
notices/ |
license texts for OpenSCAD, its bundled code and every Ubuntu package compiled in or shipped |
SOURCES.md |
where the corresponding source of each part is published |
build.json |
OpenSCAD commit and version, package versions, binary digest |
SMARTSCAD_BUILD_JOBS sets the compile parallelism (default: up to 8).
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license.md file.
The bundled worker binary (dist_cpp/) contains OpenSCAD (GPL-2.0-or-later) combined with CGAL code under GPL-3.0-or-later and Apache-2.0 code, so it is distributed under the GNU General Public License, version 3 or later. Its notices, the full license texts and the location of its corresponding source are in dist_cpp/linux_amd64/notices/ and dist_cpp/linux_amd64/SOURCES.md. The shared libraries in dist_cpp/linux_amd64/lib/ are unmodified Ubuntu builds under their own licenses. The worker is a separate program that smartscad runs as a child process.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.