cadrum
Rust CAD library powered by statically linked, headless OpenCASCADE (OCCT 8.0.0).

Summary
Other Rust CAD bindings either require the user to install OCCT ahead of time
(with all the version skew this entails on Linux distros and Windows) or
expose OCCT's class hierarchy 1:1, where building a cube ends up touching
gp_Pnt, gp_Ax2, BRepPrimAPI_MakeBox, and TopoDS_Shape before any
geometry actually appears.
cadrum takes a different bet:
- Static linking with prebuilt binaries.
cargo build on a supported
target downloads a self-contained OCCT 8.0.0 tarball and links it
statically. No system OCCT, no dynamic libraries to ship, no
LD_LIBRARY_PATH in production.
- A minimal type surface. Three concrete shape types —
Solid, Edge,
Face — plus a triangle Mesh for visual output and glam vectors for
input. Operations are inherent methods on the shape types, so
Solid::cube(...).rotate_z(0.5).translate(DVec3::X * 10.0) chains like
any value-returning Rust API.
- Single-type surface; collections via iterators. Every operation lives
on the concrete shape types (
Solid, Edge). A collection is just a
Vec<Solid> / [Solid; N] — transform or aggregate it with ordinary
iterator idioms (parts.iter().map(|s| s.translate(v)).collect(),
parts.iter().map(|s| s.volume()).sum::<f64>()). Booleans, sweep, loft,
extrude and I/O take any IntoIterator<Item = &Solid> / &Edge directly.
Introduction
OpenCASCADE represents shapes as a boundary representation (BRep): a solid
is a topological assembly of faces, faces are trimmed surfaces bounded by
edges, edges are 3D curves with a parameter range. Booleans, fillets,
sweeps, and the like rebuild this assembly under the hood; CAD I/O formats
like STEP / IGES preserve it exactly across applications.
Working at that level pays off when the application needs to reason about
geometry — closest-point queries, swept profiles along arbitrary spines,
history-tracked face derivation through booleans — and not merely render
triangles. Triangle meshes are a separate, lossy projection that cadrum
exposes through Solid::mesh when an STL export or SVG render is required.
Capabilities
| Area |
Methods |
| Primitives |
Solid::cube, Solid::sphere, Solid::cylinder, Solid::cone, Solid::torus, Solid::half_space |
| Curves |
Edge::line, Edge::arc_3pts, Edge::circle, Edge::polygon, Edge::helix, Edge::bspline |
| Surfacing |
Solid::extrude, Solid::sweep, Solid::loft, Solid::bspline |
| Editing |
Solid::shell, Solid::fillet_edges, Solid::chamfer_edges, Solid::clean |
| Queries |
Solid::volume, Solid::area, Solid::center, Solid::inertia, Solid::bounding_box, Solid::contains |
| Topology |
Solid::iter_face, Solid::iter_edge, Face::iter_edge, Face::project, Edge::project |
| Identity / history |
Solid::id, Face::id, Edge::id, Solid::iter_history |
| I/O |
Solid::read_step / Solid::write_step, Solid::read_brep_binary / Solid::write_brep_binary, Solid::read_brep_text / Solid::write_brep_text |
| Mesh |
Solid::mesh → Mesh, Mesh::write_stl, Mesh::write_svg |
Color (feature color) |
per-face color preserved across STEP / BRep / STL / SVG round-trips |
Build
Add this to your Cargo.toml:
[dependencies]
cadrum = "^0.8"
cargo build automatically downloads a prebuilt OCCT 8.0.0 binary for the targets below.
|
Target |
Prebuilt |
 |
x86_64-unknown-linux-gnu |
✅ |
 |
aarch64-unknown-linux-gnu |
✅ |
 |
x86_64-pc-windows-msvc |
✅ |
 |
x86_64-pc-windows-gnu |
✅ |
 |
aarch64-apple-darwin |
✅ |
 |
x86_64-apple-darwin |
✅ |
For other targets, build OCCT from source:
OCCT_ROOT=/path/to/occt cargo build --features source-build
If OCCT_ROOT is not set, built binaries are cached under target/.
Requirements when building OpenCASCADE from source
- C++17 compiler (GCC, Clang, or MSVC)
- CMake
Examples
Primitives
Primitive solids: box, cylinder, sphere, cone, torus — colored and exported as STEP + SVG.
cargo run --example 01_primitives
use cadrum::{DVec3, Solid};
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let solids = [
Solid::cube(DVec3::ZERO, DVec3::new(10.0, 20.0, 30.0))
.color("#4a90d9"),
Solid::cylinder(8.0, DVec3::Z * 30.0)
.translate(DVec3::X * 30.0)
.color("#e67e22"),
Solid::sphere(8.0)
.translate(DVec3::X * 60.0 + DVec3::Z * 15.0)
.color("#2ecc71"),
Solid::cone(8.0, 1.0, DVec3::Z * 30.0)
.translate(DVec3::X * 90.0)
.color("#e74c3c"),
Solid::torus(12.0, 4.0, DVec3::Z)
.translate(DVec3::X * 130.0 + DVec3::Z * 15.0)
.color("#9b59b6"),
];
Solid::write_step(&solids, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&solids, Default::default())?;
let scene = mesh.scene(Default::default());
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 01_primitives.png | 01_primitives.step | 01_primitives.glb | 01_primitives.stl | 01_primitives.svg
Write read
Read and write: chain STEP, BRep text, and BRep binary round-trips with progressive rotation.
cargo run --example 02_write_read
use cadrum::{DVec3, Solid};
use std::f64::consts::FRAC_PI_8;
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let step_path = format!("{example_name}.step");
let text_path = format!("{example_name}_text.brep");
let brep_path = format!("{example_name}.brep");
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let original = Solid::read_step(
&mut std::fs::File::open(format!("{manifest_dir}/steps/colored_box.step")).expect("open file"),
)?;
let a_written: Vec<Solid> = original.clone().into_iter().map(|s| s.rotate_x(FRAC_PI_8)).collect();
Solid::write_step(&a_written, &mut std::fs::File::create(&step_path).expect("create file"))?;
let a = Solid::read_step(&mut std::fs::File::open(&step_path).expect("open file"))?;
let b_written: Vec<Solid> = a.clone().into_iter().map(|s| s.rotate_x(FRAC_PI_8)).collect();
Solid::write_brep_text(&b_written, &mut std::fs::File::create(&text_path).expect("create file"))?;
let b = Solid::read_brep_text(&mut std::fs::File::open(&text_path).expect("open file"))?;
let c_written: Vec<Solid> = b.clone().into_iter().map(|s| s.rotate_x(FRAC_PI_8)).collect();
Solid::write_brep_binary(&c_written, &mut std::fs::File::create(&brep_path).expect("create file"))?;
let c = Solid::read_brep_binary(&mut std::fs::File::open(&brep_path).expect("open file"))?;
let [min, max] = original[0].bounding_box();
let spacing = (max - min).length() * 1.5;
let all: Vec<Solid> = [original, a, b, c].into_iter()
.enumerate()
.flat_map(|(i, solids)| solids.into_iter().map(move |s| s.translate(DVec3::X * spacing * i as f64)))
.collect();
let mesh = Solid::mesh(&all, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(1.0, 1.0, 2.0), ..Default::default() });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
let stl_path = format!("{example_name}.stl");
for (label, path) in [("STEP", &step_path), ("BRep text", &text_path), ("BRep binary", &brep_path), ("STL", &stl_path)] {
let size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
println!("{label:12} {path:30} {size:>8} bytes");
}
Ok(())
}
Output: 02_write_read.png | 02_write_read.step | 02_write_read.glb | 02_write_read.brep | 02_write_read_text.brep | 02_write_read.stl | 02_write_read.svg
Transform
Transform operations: translate, rotate, scale, and mirror applied to a cone.
cargo run --example 03_transform
use cadrum::{DVec3, Solid};
use std::f64::consts::PI;
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let base = Solid::cone(8.0, 0.0, DVec3::Z * 20.0)
.color("#888888");
let solids = [
base.clone(),
base.clone()
.color("#4a90d9")
.translate(DVec3::X * 40.0 + DVec3::Z * 20.0),
base.clone()
.color("#e67e22")
.rotate_x(PI / 2.0)
.translate(DVec3::X * 80.0),
base.clone()
.color("#2ecc71")
.scale(DVec3::ZERO, 1.5)
.translate(DVec3::X * 120.0),
base.clone()
.color("#e74c3c")
.mirror(DVec3::ZERO, DVec3::Z)
.translate(DVec3::X * 160.0),
];
Solid::write_step(&solids, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&solids, Default::default())?;
let scene = mesh.scene(Default::default());
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 03_transform.png | 03_transform.step | 03_transform.glb | 03_transform.stl | 03_transform.svg
Boolean
Boolean operations: union, subtract, and intersect between a box and a cylinder.
cargo run --example 04_boolean
use cadrum::{Boolean, DVec3, Solid};
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let make_box = Solid::cube(DVec3::ZERO, DVec3::splat(20.0))
.translate(DVec3::X * -10.+ DVec3:: Y*-10.)
.color("#4a90d9");
let make_cyl = Solid::cylinder(8.0, DVec3::Z * 30.0)
.translate(DVec3::Z*-5.)
.color("#e67e22");
let union: Solid = (&make_box + &make_cyl).build()?;
let subtract: Solid = (&make_box - &make_cyl).build()?;
let intersect: Solid = (&make_box * &make_cyl).build()?;
let cylinder = Solid::cylinder(8.0, DVec3::Z * 30.0)
.translate(DVec3::X*4.);
let [cylinder0, cylinder1, cylinder2] = [cylinder.clone(), cylinder.clone().rotate_z(std::f64::consts::TAU/3.), cylinder.clone().rotate_z(-std::f64::consts::TAU/3.)];
let sum: Solid = [&cylinder0, &cylinder1, &cylinder2].into_iter().map(Boolean::from).reduce(|a, s| a + s).unwrap().build()?;
let sum = sum.color("#d875ff");
let product: Solid = [&cylinder0, &cylinder1, &cylinder2].into_iter().map(Boolean::from).reduce(|a, b| a * b).unwrap().build()?;
let product = product.color("#00ff22");
let shapes = [
union.translate(DVec3::X * 0.0),
subtract.translate(DVec3::X * 40.0),
intersect.translate(DVec3::X * 80.0),
sum.translate(DVec3::X * 20.0 + DVec3::Y * 40.0),
product.translate(DVec3::X * 60.0 + DVec3::Y * 40.0)
];
Solid::write_step(&shapes, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&shapes, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(1.0, 1.0, 2.0), ..Default::default() });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 04_boolean.png | 04_boolean.step | 04_boolean.glb | 04_boolean.stl | 04_boolean.svg
Extrude
Demo of Solid::extrude: push a closed 2D profile along a direction vector.
cargo run --example 05_extrude
use cadrum::{BSplineEnd, DVec3, Edge, Error, Solid};
fn build_box() -> Result<Solid, Error> {
let profile = Edge::polygon(&[
DVec3::new(0.0, 0.0, 0.0),
DVec3::new(5.0, 0.0, 0.0),
DVec3::new(5.0, 5.0, 0.0),
DVec3::new(0.0, 5.0, 0.0),
])?;
Solid::extrude(&profile, DVec3::Z * 8.0)
}
fn build_oblique_cylinder() -> Result<Solid, Error> {
let profile = [Edge::circle(3.0, DVec3::Z)?];
Solid::extrude(&profile, DVec3::new(-4.0, -6.0, 8.0))
}
fn build_l_beam() -> Result<Solid, Error> {
let profile = Edge::polygon(&[
DVec3::new(0.0, 0.0, 0.0),
DVec3::new(4.0, 0.0, 0.0),
DVec3::new(4.0, 1.0, 0.0),
DVec3::new(1.0, 1.0, 0.0),
DVec3::new(1.0, 3.0, 0.0),
DVec3::new(0.0, 3.0, 0.0),
])?;
Solid::extrude(&profile, DVec3::Z * 12.0)
}
fn build_heart() -> Result<Solid, Error> {
let profile = [Edge::bspline(
&[
DVec3::new(0.0, -4.0, 0.0), DVec3::new(2.0, -1.5, 0.0),
DVec3::new(4.0, 1.5, 0.0),
DVec3::new(2.5, 3.5, 0.0), DVec3::new(0.0, 2.0, 0.0), DVec3::new(-2.5, 3.5, 0.0), DVec3::new(-4.0, 1.5, 0.0),
DVec3::new(-2.0, -1.5, 0.0),
],
BSplineEnd::Periodic,
)?];
Solid::extrude(&profile, DVec3::Z * 7.0)
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let box_solid = build_box()?.color("#b0d4f1");
let oblique = build_oblique_cylinder()?.color("#f1c8b0").translate(DVec3::X * 10.0);
let l_beam = build_l_beam()?.color("#b0f1c8").translate(DVec3::X * 20.0);
let heart = build_heart()?.color("#f1b0b0").translate(DVec3::X * 30.0);
let result = [box_solid, oblique, l_beam, heart];
Solid::write_step(&result, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&result, Default::default())?;
let scene = mesh.scene(Default::default());
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 05_extrude.png | 05_extrude.step | 05_extrude.glb | 05_extrude.stl | 05_extrude.svg
Loft
Demo of Solid::loft: skin a smooth solid through cross-section wires.
cargo run --example 06_loft
use cadrum::{DVec3, Edge, Error, Solid};
fn build_frustum() -> Result<Solid, Error> {
let lower = [Edge::circle(3.0, DVec3::Z)?];
let upper = [Edge::circle(1.5, DVec3::Z)?.translate(DVec3::Z * 8.0)];
Ok(Solid::loft(&[lower, upper])?.color("#cd853f"))
}
fn build_morph() -> Result<Solid, Error> {
let r = 2.5;
let square = Edge::polygon(&[
DVec3::new(-r, -r, 0.0),
DVec3::new(r, -r, 0.0),
DVec3::new(r, r, 0.0),
DVec3::new(-r, r, 0.0),
])?;
let circle = Edge::circle(r, DVec3::Z)?.translate(DVec3::Z * 10.0);
Ok(Solid::loft([square.as_slice(), std::slice::from_ref(&circle)])?.color("#808000"))
}
fn build_tilted() -> Result<Solid, Error> {
let bottom = [Edge::circle(2.5, DVec3::Z)?];
let mid = [Edge::circle(2.0, DVec3::new(0.3, 0.0, 1.0).normalize())?
.translate(DVec3::X + DVec3::Z * 5.0)];
let top = [Edge::circle(1.5, DVec3::new(-0.2, 0.3, 1.0).normalize())?
.translate(DVec3::new(-0.5, 1.0, 10.0))];
Ok(Solid::loft(&[bottom, mid, top])?.color("#4682b4"))
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let frustum = build_frustum()?;
let morph = build_morph()?.translate(DVec3::X * 10.0);
let tilted = build_tilted()?.translate(DVec3::X * 20.0);
let result = [frustum, morph, tilted];
Solid::write_step(&result, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&result, Default::default())?;
let scene = mesh.scene(Default::default());
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 06_loft.png | 06_loft.step | 06_loft.glb | 06_loft.stl | 06_loft.svg
Sweep
Sweep showcase: M2 screw (helix spine) + U-shaped pipe (line+arc+line spine)
cargo run --example 07_sweep
use cadrum::{DVec3, Edge, Error, ProfileOrient, Solid};
fn build_m2_screw() -> Result<Solid, Error> {
let r = 1.0;
let h_pitch = 0.4;
let h_thread = 6.0;
let r_head = 1.75;
let h_head = 1.3;
let r_delta = 3f64.sqrt() / 2.0 * h_pitch;
let helix = Edge::helix(r - r_delta, h_pitch, h_thread, DVec3::Z, DVec3::X)?;
let profile = Edge::polygon(&[DVec3::new(0.0, -h_pitch / 2.0, 0.0), DVec3::new(r_delta, 0.0, 0.0), DVec3::new(0.0, h_pitch / 2.0, 0.0)])?;
let profile: Vec<Edge> = profile.into_iter().map(|e| e.align_z(helix.start_tangent(), helix.start_point()).translate(helix.start_point())).collect();
let thread = Solid::sweep(&profile, &[helix], ProfileOrient::Up(DVec3::Z))?;
let shaft = Solid::cylinder(r - r_delta * 6.0 / 8.0, DVec3::Z * h_thread);
let crest = Solid::cylinder(r - r_delta / 8.0, DVec3::Z * h_thread);
let thread_shaft: Solid = ((&thread + &shaft) * &crest).build()?;
let head = Solid::cylinder(r_head, DVec3::Z * h_head).translate(DVec3::Z * h_thread);
let res: Solid = (&thread_shaft + &head).build()?;
Ok(res.color("red"))
}
fn build_u_pipe() -> Result<Solid, Error> {
let pipe_radius = 0.4;
let leg_length = 6.0;
let gap = 3.0;
let half_gap = gap / 2.0;
let bend_radius = half_gap;
let a = DVec3::new(-half_gap, 0.0, 0.0);
let b = DVec3::new(-half_gap, 0.0, leg_length);
let arc_mid = DVec3::new(0.0, 0.0, leg_length + bend_radius);
let c = DVec3::new(half_gap, 0.0, leg_length);
let d = DVec3::new(half_gap, 0.0, 0.0);
let up_leg = Edge::line(a, b)?;
let bend = Edge::arc_3pts(b, arc_mid, c)?;
let down_leg = Edge::line(c, d)?;
let profile = Edge::circle(pipe_radius, DVec3::Z)?.translate(a);
let pipe = Solid::sweep(&[profile], &[up_leg, bend, down_leg], ProfileOrient::Up(DVec3::Y))?;
Ok(pipe.translate(DVec3::X * 6.0).color("blue"))
}
fn build_twisted_ribbon() -> Result<Solid, Error> {
let h = 8.0;
let aux_r = 3.0;
let spine = Edge::line(DVec3::ZERO, DVec3::Z * h)?;
let aux = Edge::helix(aux_r, h, h, DVec3::Z, DVec3::X)?;
let profile = Edge::polygon(&[DVec3::new(-2.0, -0.2, 0.0), DVec3::new(2.0, -0.2, 0.0), DVec3::new(2.0, 0.2, 0.0), DVec3::new(-2.0, 0.2, 0.0)])?;
let ribbon = Solid::sweep(&profile, &[spine], ProfileOrient::Auxiliary(&[aux]))?;
Ok(ribbon.translate(DVec3::X * 12.0).color("green"))
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let all = [build_m2_screw()?, build_u_pipe()?, build_twisted_ribbon()?];
Solid::write_step(&all, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&all, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(1.0, 1.0, -1.0), hidden_edges: false, ..Default::default() });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png ({} solids)", all.len());
Ok(())
}
Output: 07_sweep.png | 07_sweep.step | 07_sweep.glb | 07_sweep.stl | 07_sweep.svg
Shell
Demo of Solid::shell:
cargo run --example 08_shell
use cadrum::{DVec3, Error, Solid};
fn hollow_cube() -> Result<Solid, Error> {
let cube = Solid::cube(DVec3::ZERO, DVec3::splat(8.0));
let top = cube.iter_face().last().expect("cube has faces");
cube.shell(-1.0, [top])
}
fn sealed_cube() -> Result<Solid, Error> {
let cube = Solid::cube(DVec3::ZERO, DVec3::splat(8.0));
cube.shell(-1.0, std::iter::empty::<&cadrum::Face>())
}
fn halved_shelled_torus(thickness: f64) -> Result<Solid, Error> {
let torus = Solid::torus(6.0, 2.0, DVec3::Y);
let cutter = Solid::half_space(DVec3::ZERO, -DVec3::Z);
let cutter_face_ids: std::collections::HashSet<u64> =
cutter.iter_face().map(|f| f.id()).collect();
let half: Solid = (&torus * &cutter).build()?;
let from_cutter: std::collections::HashSet<u64> = half
.iter_history()
.filter_map(|[post, src]| cutter_face_ids.contains(&src).then_some(post))
.collect();
half.shell(thickness, half.iter_face().filter(|f| from_cutter.contains(&f.id())))
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let result = [
hollow_cube()?.color("#d0a878"),
sealed_cube()?.color("#6fbf73").translate(DVec3::Y * 10.0),
halved_shelled_torus(1.0)?.color("#ff5e00").translate(DVec3::X * 18.0),
halved_shelled_torus(-1.0)?.color("#0052ff").translate(DVec3::X * 18.0 + DVec3::Y * 10.0),
];
Solid::write_step(&result, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&result, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(1.0, 1.0, 2.0), shading: true, ..Default::default() });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 08_shell.png | 08_shell.step | 08_shell.glb | 08_shell.stl | 08_shell.svg
Bspline
cargo run --example 09_bspline
use cadrum::{DQuat, DVec3, Solid};
use std::f64::consts::TAU;
const M: usize = 48; const N: usize = 24; const RING_R: f64 = 6.0;
fn point(i: usize, j: usize) -> DVec3 {
let phi = TAU * (i as f64) / (M as f64);
let theta = TAU * (j as f64) / (N as f64);
let two_phi = 2.0 * phi;
let a = 1.8 + 0.6 * two_phi.sin();
let b = 1.0 + 0.4 * two_phi.cos();
let psi = two_phi; let z_shift = 1.0 * two_phi.sin();
let local_raw = DVec3::X * (a * theta.cos()) + DVec3::Z * (b * theta.sin());
let local_twisted = DQuat::from_axis_angle(DVec3::Y, psi) * local_raw;
let local_shifted = local_twisted + DVec3::Z * z_shift;
let translated = local_shifted + DVec3::X * RING_R;
DQuat::from_axis_angle(DVec3::Z, phi) * translated
}
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let plasma = Solid::bspline(M, N, true, point).expect("2-period bspline torus should succeed");
let objects = [plasma.color("cyan")];
Solid::write_step(&objects, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&objects, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(0.05, 0.05, 1.0), up: DVec3::Y, hidden_edges: false, shading: true });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 09_bspline.png | 09_bspline.step | 09_bspline.glb | 09_bspline.stl | 09_bspline.svg
Fillet
Demo of Solid::fillet_edges:
cargo run --example 10_fillet
use cadrum::{DVec3, Error, Solid};
fn rounded_cube(size: f64) -> Result<Solid, Error> {
let cube = Solid::cube(DVec3::ZERO, DVec3::splat(size)).translate(-DVec3::ONE * (size / 2.0));
let radius = size * 0.2;
cube.fillet_edges(radius, cube.iter_edge())
}
fn soft_top_cube(size: f64) -> Result<Solid, Error> {
let cube = Solid::cube(DVec3::ZERO, DVec3::splat(size)).translate(-DVec3::ONE * (size / 2.0));
let radius = size * 0.2;
let top_edges = cube
.iter_edge()
.filter(|e| [e.start_point(), e.end_point()].iter().all(|p| (p.z - size / 2.0).abs() < 1e-6));
cube.fillet_edges(radius, top_edges)
}
fn coin(radius: f64, height: f64) -> Result<Solid, Error> {
let cyl = Solid::cylinder(radius, DVec3::Z * height);
let radius = height * 0.3;
let top_circle = cyl
.iter_edge()
.filter(|e| [e.start_point(), e.end_point()].iter().all(|p| (p.z - height).abs() < 1e-6));
cyl.fillet_edges(radius, top_circle)
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let result = [
rounded_cube(8.0)?.color("#d0a878"),
soft_top_cube(8.0)?.color("#6fbf73").translate(DVec3::X * 12.0),
coin(4.0, 2.0)?.color("#0052ff").translate(DVec3::X * 24.0),
];
Solid::write_step(&result, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&result, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(1.0, 1.0, 2.0), shading: true, ..Default::default() });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 10_fillet.png | 10_fillet.step | 10_fillet.glb | 10_fillet.stl | 10_fillet.svg
Chamfer
Demo of Solid::chamfer_edges — mirror of 10_fillet.rs using bevels:
cargo run --example 11_chamfer
use cadrum::{DVec3, Error, Solid};
fn beveled_cube(size: f64) -> Result<Solid, Error> {
let cube = Solid::cube(DVec3::ZERO, DVec3::splat(size)).translate(-DVec3::ONE * (size / 2.0));
let distance = size * 0.2;
cube.chamfer_edges(distance, cube.iter_edge())
}
fn beveled_top_cube(size: f64) -> Result<Solid, Error> {
let cube = Solid::cube(DVec3::ZERO, DVec3::splat(size)).translate(-DVec3::ONE * (size / 2.0));
let distance = size * 0.2;
let top_edges = cube
.iter_edge()
.filter(|e| [e.start_point(), e.end_point()].iter().all(|p| (p.z - size / 2.0).abs() < 1e-6));
cube.chamfer_edges(distance, top_edges)
}
fn beveled_coin(radius: f64, height: f64) -> Result<Solid, Error> {
let cyl = Solid::cylinder(radius, DVec3::Z * height);
let distance = height * 0.3;
let top_circle = cyl
.iter_edge()
.filter(|e| [e.start_point(), e.end_point()].iter().all(|p| (p.z - height).abs() < 1e-6));
cyl.chamfer_edges(distance, top_circle)
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let result = [
beveled_cube(8.0)?.color("#d0a878"),
beveled_top_cube(8.0)?.color("#6fbf73").translate(DVec3::X * 12.0),
beveled_coin(4.0, 2.0)?.color("#0052ff").translate(DVec3::X * 24.0),
];
Solid::write_step(&result, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;
let mesh = Solid::mesh(&result, Default::default())?;
let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(1.0, 1.0, 2.0), shading: true, ..Default::default() });
scene.write_svg(&mut std::fs::File::create(format!("{example_name}.svg")).unwrap())?;
scene.write_png([640, 640], &mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
Ok(())
}
Output: 11_chamfer.png | 11_chamfer.step | 11_chamfer.glb | 11_chamfer.stl | 11_chamfer.svg
Multiview
Fixed 4-view multiview PNG for LLM-driven design loops.
cargo run --example 12_multiview
use cadrum::{DVec3, Solid};
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let block = Solid::cube(DVec3::ZERO, DVec3::new(40.0, 30.0, 20.0))
.translate(-DVec3::new(20.0, 15.0, 10.0));
let hole = Solid::cylinder(5.0, DVec3::Z * 30.0)
.translate(-DVec3::Z * 15.0);
let corner_cut = Solid::sphere(10.0)
.translate(DVec3::new(20.0, 15.0, 10.0));
let part: Solid = (&block - &hole - &corner_cut).build()?;
part.write_multiview_png(&mut std::fs::File::create(format!("{example_name}.png")).unwrap())?;
let mesh = Solid::mesh([&part], Default::default())?;
mesh.write_stl(&mut std::fs::File::create(format!("{example_name}.stl")).unwrap())?;
mesh.write_gltf_binary(&mut std::fs::File::create(format!("{example_name}.glb")).unwrap())?;
println!("wrote {example_name}.png / {example_name}.stl / {example_name}.glb");
Ok(())
}
Output: 12_multiview.png | 12_multiview.glb | 12_multiview.stl
The Type Map
Three concrete shape types form the whole public surface — there are no
collection wrapper traits:
Edge ── single 3D curve ┐
Face ── trimmed 3D surface │ concrete BRep handles
Solid ── connected closed body ┘
Every method is inherent on the concrete type — no trait import is ever
needed:
# use cadrum::{DVec3, Solid};
let s = Solid::cube(DVec3::ZERO, DVec3::ONE).rotate_z(0.5).translate(DVec3::X);
let v = s.volume();
A collection of shapes is just a Vec<Solid> / [Solid; N] (or Vec<Edge>
for a wire). cadrum does not special-case collections with a trait; you
transform and aggregate them with ordinary iterator idioms:
use cadrum::{DVec3, Solid};
let parts: Vec<Solid> = vec![
Solid::cube(DVec3::ZERO, DVec3::ONE),
Solid::sphere(1.0),
];
let shifted: Vec<Solid> = parts.into_iter().map(|s| s.translate(DVec3::X * 5.0)).collect();
let total: f64 = shifted.iter().map(|s| s.volume()).sum();
An ordered Vec<Edge> is a wire (open or closed polyline); sweep / loft /
extrude all take any IntoIterator<Item = &Edge>.
Errors
Every fallible operation returns Result<T, Error> with Error
enumerating the failure modes (Error::SweepFailed,
Error::FilletFailed, Error::InvalidEdge, etc.). Variants that need
detail carry a String payload identifying which constructor or parameter
combination tripped OCCT, so panics are reserved for true logic bugs.
Features
color (default): Enables Solid::color and per-face colormap
propagation through STEP / BRep / STL / SVG I/O via OCCT's XDE document
model. Disable for a smaller binary if shape color is irrelevant.
source-build: When the prebuilt-binary cache is empty, fall back
to building OCCT from upstream sources via CMake instead of failing.
Required on targets without a published prebuilt (anything outside the
four-way Linux / Windows × x86_64 / aarch64 table). Pulls cmake in as
a build-dep.
Showcase
Try it now →
A browser-based configurator that lets you tweak dimensions of a STEP model and get an instant 3D preview and quote. cadrum powers the parametric reshaping and meshing on the backend.
License
This project is licensed under the MIT License.
Compiled binaries include OpenCASCADE Technology (OCCT),
which is licensed under the LGPL 2.1.
Users who distribute applications built with cadrum must comply with the LGPL 2.1 terms.
Since cadrum builds OCCT from source, end users can rebuild and relink OCCT to satisfy this requirement.