An AST evaluator for the OpenSCAD language, producing Manifold CSG geometry from a parsed AST.
openscad_evaluator takes an OpenSCAD AST — as produced by
openscad_lalr_parser — and walks it to
produce Manifold meshes: a two-pass resolve (AST walk,
no CSG calls) then generate (bottom-up Manifold/CrossSection construction) pipeline, with
$fn/$fa/$fs, full built-in coverage (primitives, transforms, boolean ops, 2D geometry,
text(), surface(), DXF/3MF import, roof()), and a content-hash geometry cache
(ManifoldCache) so repeated renders/debugger pauses skip unchanged Manifold work.
It's GUI- and toolkit-agnostic: the only way it talks back to a caller is a handful of optional
callback parameters on Evaluator.__init__ (echo_fn, debug_hook, error_break_fn,
return_hook) — no QObject, no signals, no direct rendering. This is the evaluator that powers
BelfrySCAD.
See docs/evaluator.md for the full architecture reference: scope
processing, assignment order, the built-ins table, 2D/3D geometry handling, error format,
$variables scoping, include/use, and the Manifold provenance / AST-to-geometry-ID mapping
used for WYSIWYG picking.
pip install openscad-evaluatorFor DXF import support:
pip install openscad-evaluator[dxf]STL/OBJ/OFF/3MF export needs nothing extra -- all four writers are pure Python (export.py; 3MF
is just a ZIP of XML, built with the standard library's zipfile/xml.etree.ElementTree, not the
platform-limited lib3mf).
git clone https://github.com/BelfrySCAD/openscad_evaluator.git
cd openscad_evaluator
pip install -e ".[dev]"from openscad_lalr_parser import getASTfromString, build_scopes
from openscad_evaluator import Evaluator
nodes = getASTfromString("cube([10, 10, 10]);")
root_scope = build_scopes(nodes)
ev = Evaluator()
bodies, id_to_node = ev.evaluate(nodes, root_scope)
for body in bodies:
print(body.body.num_tri(), "triangles")Installing the package also installs an openscad-evaluator script that evaluates a .scad file
and exports the result -- STL, OBJ, OFF, or 3MF, inferred from -o's extension:
openscad-evaluator model.scad -o model.stl
openscad-evaluator model.scad -o model.3mf
openscad-evaluator model.scad -o model.mesh --format obj # explicit format overrides the extensionecho()/warning output goes to stdout; a parse or evaluation error goes to stderr and exits
non-zero.
--debug drops into a small, gdb-style interactive debugger instead of running straight through:
set breakpoints and run, then step/next/finish/continue, print <name>, backtrace, and
set <name>=<value> to override a variable before resuming.
$ openscad-evaluator model.scad -o model.stl --debug
Reading symbols from model.scad...
(scad-dbg) break 12
Breakpoint set at model.scad:12
(scad-dbg) run
Breakpoint hit at model.scad:12
10 module bracket(w) {
11 h = w * 2;
-> 12 cube([w, h, 3]);
13 }
(scad-dbg) print h
$1 = 20
(scad-dbg) continue
Exported to model.stl
See src/openscad_evaluator/_debug_repl.py for the full command list (help at either prompt also
prints it), and examples/minimal_debugger.py for the underlying debug_hook protocol if you're
integrating your own debugger rather than using this one.
Runnable, self-checking scripts under examples/ go beyond the Quick Start above to
cover the two optional integration points most callers ask about:
examples/minimal_debugger.py— hooking a debugger intoEvaluator(debug_hook=...): tracing every statement, stopping evaluation at a breakpoint, and overriding a variable's value mid-run via the hook'smodsreturn value.examples/manifold_cache_reuse.py— sharing oneManifoldCacheacross repeatedevaluate()calls so an unchanged subtree (e.g. everything but the part of the script a user just edited) skips re-running Manifold work on the next render.
python examples/minimal_debugger.py
python examples/manifold_cache_reuse.pyBoth are also run under pytest (tests/test_examples.py) so they fail CI if they drift out of
sync with the real API.
uv sync --all-extras
uv run pytest