Text-to-CAD in Rust. A model is a short Rhai script; radgen builds it into B-rep solids on a sandboxed geometry kernel, exports STEP, STL, GLB and 3MF, and shows every rebuild live in a viewer that runs natively or in the browser. It started as a Rust take on text-to-cad, where the agent writes build123d Python; here it writes a script, and there is no compile step between edit and preview.
let plate = box(60, 30, 6);
let boss = cylinder(9, 14).translate(0, 0, 7);
let hole = cylinder(3.25, 18);
let body = plate + boss - hole.translate(-20, 0, 0) - hole.translate(20, 0, 0) - cylinder(4, 40);
body = body.edges("|Z and (>X or <X)").fillet(5); // round the plate's corners
body.color("#4a6fa5").name("bracket")Needs Rust 1.95 or newer and, for the kernel plugins, the WASI target:
rustup target add wasm32-wasip1
make pluginscargo run --release -p radgen-cli -- build models/bracket.rhai -o bracket.step -o bracket.3mfmake view MODEL=models/flange.rhaimake plugins builds the kernels into target/plugins/; without them radgen falls back to its
built-in truck kernel and says so. make help lists every target.
model.rhai ──► radgen-core ──► Kernel trait ──► solidcraft.wasm (default) ─┐
(Rhai) script engine, truck.wasm ├─► meshes, STEP
sketches, ◄── protobuf ── (sandboxed: Extism) │ │
edge selectors built-in truck ──────────┘ ▼
CLI exports, viewer
- radgen-core runs the script, which only records each part as a recipe (the tree of
operations that makes it), then builds the recipes on a kernel. It owns everything above the
kernel: the Rhai API, 2D sketches, CadQuery-style edge selectors, the recipe evaluator, meshes
and the STL/GLB/3MF writers. It talks to geometry only through the
KernelandMeshertraits, and builds for the browser. - Kernels build, combine, move, round, tessellate and STEP-export solids. They run as Extism wasm plugins speaking proto/kernel.proto: each call has a time limit and a memory cap, so a kernel that hangs or crashes is stopped and reported instead of taking radgen with it.
- Apps: the
radgenCLI and the viewer drive the engine and choose the kernel.
docs/architecture.html draws the pieces and their messages.
Recording first lets the evaluator (crates/radgen-core/src/recipe.rs) spend kernel time well:
- Batching. Booleans get slow when small features are added one by one to a big body, so a
chain like
body + a + b + cjoins the small operands first and the body once, andbody - a - b - cbecomes one cut by the union of the cutters. If a batched boolean fails, the chain is redone in the script's order, so batching never breaks a script that works. - Caching. Results are cached by a hash of their recipe and kept between builds, so an edit rebuilds only what it changed. Script lines don't count, so moving code around keeps the cache.
- Parallel parts. Each part builds on its own kernel instance: the viewer keeps a few
long-lived kernel workers (each with its cache), and
radgen builduses parallel kernels unless it writes STEP, which needs every solid in one kernel.
On the upright tester case (two parts, 19 booleans) a full build went from 29 s to 7.9 s with batching and 4.2 s with two kernels; in the viewer, changing one dimension rebuilds in about 1.2 s (4 of the 19 booleans).
| Kernel | Runs as | Notes |
|---|---|---|
solidcraft (default) |
plugin, solidcraft.wasm |
SolidCraft's kernel: truck with SolidCraft's fixes, plus fillets, chamfers and robust booleans (coplanar faces, revolved bodies, equal-footprint stacks). Exact arcs for circles. |
truck |
built in, or truck.wasm |
Pure-Rust truck with SolidCraft's patches. No fillets or chamfers. The browser viewer uses it. |
Choose with --kernel solidcraft|truck|<path.wasm> on the CLI or RADGEN_KERNEL for the
native viewer. Plugins are found in $RADGEN_PLUGINS, then plugins/ next to the binary (or one
level up, as make dist lays them out), then target/plugins/. A named plugin that is missing is
an error; the default falls back to built-in truck with a note.
--timeout <seconds> (default 60) bounds each kernel call. SolidCraft's kernel can hang on a
revolved body resting exactly on a face with holes in it (overlap the parts a little instead); the
timeout turns that into an error on the script line that caused it.
Writing a kernel: implement Kernel and Mesher (see
crates/radgen-core/src/kernel/mod.rs), then wrap it in a
plugin crate like plugins/truck: radgen-kernel-guest serves the
protocol for any kernel, so the plugin itself is a few lines.
A script's last expression is a part or an array of parts (an assembly). Lengths are mm, angles degrees; ints and floats mix freely. Parts are values: operations return new parts.
| Call | Meaning |
|---|---|
box(x, y, z), cylinder(r, h), sphere(r) |
primitives centred on the origin (cylinders along Z) |
a + b, a - b, a & b (or .union, .difference, .intersect) |
booleans |
.translate(x, y, z), .rotate_x/y/z(deg), .scale(s) |
transforms about the origin |
.edges(selector).fillet(r), .edges(selector).chamfer(d) |
round or bevel edges (SolidCraft) |
.color("#rrggbb"), .name("id") |
appearance and part name in exports |
Sketches are 2D profiles in the XY plane:
| Call | Meaning |
|---|---|
rect(w, h), circle(r), polygon([[x, y], ...]) |
profiles; rect and circle centred, a polygon closes itself |
.translate(x, y), .rotate(deg) |
move a sketch |
outer - inner |
cut a hole (inner should lie inside outer) |
extrude(sketch, h) |
prism from z = 0 to h |
revolve(sketch, deg) |
turn about Z, sketch x as the radius and y as the height; x ≥ 0 |
Edge selectors use CadQuery's syntax: >Z / <Z /
>(1,1,0) (furthest along or against a direction), >Z[1] (the next level; negative counts
from the other end), |Z (straight and parallel), #Z (straight and perpendicular),
%LINE / %CIRCLE, combined with and, or, not, exc and parentheses. Points work too:
.edges([[30, 15, 0]]) picks the edge through that point. A cylinder's seam is a straight edge,
so |Z alone also picks seams; narrow it with a position, as in the example above.
Rhai fns, loops and arrays build parts too; models/flange.rhai cuts four
bolt holes in a loop. On truck, booleans whose faces coincide are retried with the cutter
lengthened along its axis, which handles through-holes and flush pockets exactly; SolidCraft
needs no such help.
radgen build model.rhai [-o out.step|.stl|.glb|.3mf]... [--tol mm] [--kernel ...] [--timeout s]Prints the parts, bounds and triangle count, and writes each -o export (--tol is the mesh
chord tolerance, default 0.05 mm).
radgen solidcraft model.rhai -o model.jsonCompiles the script into a SolidCraft command
script instead, for solidcraft-cli run model.json or to open the design in the SolidCraft app.
Edge selectors are resolved by asking solidcraft-cli (or $SOLIDCRAFT_CLI) for the part's
edges as the script is written.
radgen-viewer model.rhai (or make view MODEL=...) shows the model and rebuilds whenever the
file changes on disk, so an agent's edits appear live; it also edits the script itself (Cmd/Ctrl+S
saves, Export writes .step and .stl beside it). Builds run on up to four kernel workers that
keep their caches between edits, with the current step and line shown; a newer edit cancels the
stale build between operations.
The browser build runs the same viewer with the built-in truck kernel, with no server:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.130 # must match Cargo.lock
make serveThe CLI and the native viewer log to
671E
stderr, filtered by RUST_LOG (default warn;
RUST_LOG=info shows every boolean and kernel call). Set OTEL_EXPORTER_OTLP_ENDPOINT to also
send OpenTelemetry traces, metrics and logs over OTLP/HTTP (protobuf):
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 radgen build models/bracket.rhaiSpans: radgen.build › radgen.eval › radgen.boolean / radgen.tessellate ›
radgen.kernel_call (op, request and response bytes), plus radgen.kernel_load. Metrics:
radgen_boolean_ms, radgen_kernel_call_ms, radgen_eval_errors and
radgen_kernel_plugin_stops. The service name is radgen (radgen-viewer for the viewer).
make check runs what CI should: format check, clippy with warnings as errors, all tests
(including the SolidCraft adapter's, natively) and the wasm builds. make dist bundles release
binaries, the plugins and the web viewer into dist/.
| Path | What |
|---|---|
crates/radgen-core |
script engine, sketches, selectors, recipe evaluator, Kernel / Mesher, exports, SolidCraft compiler, built-in truck |
crates/radgen-cli, crates/radgen-viewer |
the radgen command and the viewer |
crates/radgen-plugin-host |
runs kernel plugins under Extism; finds and chooses kernels |
crates/radgen-kernel-guest |
serves proto/kernel.proto for any kernel inside a plugin |
crates/radgen-plugin-proto |
protobuf types for the protocol (prost, no protoc needed) |
crates/radgen-telemetry |
where logs, traces and metrics go |
plugins/solidcraft, plugins/truck |
the kernel plugins (wasm32-wasip1) |
vendor/ |
SolidCraft's patched truck crates, see vendor/README.md |
models/ |
example models; spikes/ holds experiments |
Working: primitives, booleans, transforms, sketches with extrude and revolve, fillets and chamfers by selector, STEP/STL/GLB/3MF, live native and browser viewing, kernel plugins with time limits, SolidCraft interop and OpenTelemetry. Not yet: sketch constraints, arcs and splines in sketches, importing STEP or meshes, assemblies with joints, drawings, DXF, an MCP server and the fabrication skills of text-to-cad. docs/ROADMAP.md sizes each.
The browser viewer can't run plugins yet, so it has no fillets; that needs Extism's JS SDK in a Web Worker.
truck (Apache-2.0) and SolidCraft (MIT or Apache-2.0, whose kernel and truck patches radgen builds on), CadQuery's selector syntax, Extism, egui and text-to-cad, the idea this started from.