docs.rs failed to build cadrum-0.5.0
Please check the
build logs for more information.
See
Builds for ideas on how to fix a failed build,
or
Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault,
open an issue.
cadrum

Rust CAD library powered by OpenCASCADE (OCCT 7.9.3).
Usage
More examples with source code are available at lzpel.github.io/cadrum.
Add this to your Cargo.toml:
[dependencies]
cadrum = "^0.4"
Primitives: box, cylinder, sphere, cone, torus — colored and exported as STEP + SVG. (examples/01_primitives.rs)
Example
Primitive solids: box, cylinder, sphere, cone, torus — colored and exported as STEP + SVG.
cargo run --example 01_primitives
use cadrum::Solid;
use glam::DVec3;
fn main() {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let solids = [
Solid::cube(10.0, 20.0, 30.0)
.color("#4a90d9"),
Solid::cylinder(8.0, DVec3::Z, 30.0)
.translate(DVec3::new(30.0, 0.0, 0.0))
.color("#e67e22"),
Solid::sphere(8.0)
.translate(DVec3::new(60.0, 0.0, 15.0))
.color("#2ecc71"),
Solid::cone(8.0, 0.0, DVec3::Z, 30.0)
.translate(DVec3::new(90.0, 0.0, 0.0))
.color("#e74c3c"),
Solid::torus(12.0, 4.0, DVec3::Z)
.translate(DVec3::new(130.0, 0.0, 15.0))
.color("#9b59b6"),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create file");
cadrum::io::write_step(&solids, &mut f).expect("failed to write STEP");
let mut svg = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::io::write_svg(&solids, DVec3::new(1.0, 1.0, 1.0), 0.5, true, &mut svg).expect("failed to write SVG");
}
Requirements
- A C++17 compiler (GCC, Clang, or MSVC)
- CMake
Tested with GCC 15.2.0 (MinGW-w64) and CMake 3.31.11 on Windows.
Build
By default, cargo build downloads OCCT 7.9.3 source and builds it automatically.
The built library is placed in target/occt/ and removed by cargo clean.
To cache the OCCT build across cargo clean, set OCCT_ROOT to a persistent directory:
export OCCT_ROOT=~/occt
cargo build
- If
OCCT_ROOT is set and the directory already contains OCCT libraries, they are linked directly (no rebuild).
- If
OCCT_ROOT is set but the directory is empty or missing, OCCT is built and installed there.
- To force a rebuild, remove the directory:
rm -rf ~/occt
Features
color (default): Colored STEP I/O via XDE (STEPCAFControl). Enables write_step_with_colors,
read_step_with_colors, and per-face color on Solid.
Colors are preserved through boolean operations and other transformations.
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.
Other examples
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::{Solid, Transform};
use glam::DVec3;
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 = cadrum::io::read_step(
&mut std::fs::File::open(format!("{manifest_dir}/steps/colored_box.step")).expect("open file"),
)?;
let a_written = original.clone().rotate_x(FRAC_PI_8);
cadrum::io::write_step(&a_written, &mut std::fs::File::create(&step_path).expect("create file"))?;
let a = cadrum::io::read_step(&mut std::fs::File::open(&step_path).expect("open file"))?;
let b_written = a.clone().rotate_x(FRAC_PI_8);
cadrum::io::write_brep_text(&b_written, &mut std::fs::File::create(&text_path).expect("create file"))?;
let b = cadrum::io::read_brep_text(&mut std::fs::File::open(&text_path).expect("open file"))?;
let c_written = b.clone().rotate_x(FRAC_PI_8);
cadrum::io::write_brep_binary(&c_written, &mut std::fs::File::create(&brep_path).expect("create file"))?;
let c = cadrum::io::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.translate(DVec3::X * spacing * i as f64))
.collect();
let mut svg = std::fs::File::create(format!("{example_name}.svg")).expect("create file");
cadrum::io::write_svg(&all, DVec3::new(1.0, 1.0, 2.0), 0.5, true, &mut svg)?;
let mut stl = std::fs::File::create(format!("{example_name}.stl")).expect("create file");
cadrum::io::write_stl(&all, 0.1, &mut stl)?;
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(())
}
Transform
Transform operations: translate, rotate, scale, and mirror applied to a cone.
cargo run --example 03_transform
use cadrum::Solid;
use glam::DVec3;
use std::f64::consts::PI;
fn main() {
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::new(40.0, 0.0, 20.0)),
base.clone()
.color("#e67e22")
.rotate_x(PI / 2.0)
.translate(DVec3::new(80.0, 0.0, 0.0)),
base.clone()
.color("#2ecc71")
.scale(DVec3::ZERO, 1.5)
.translate(DVec3::new(120.0, 0.0, 0.0)),
base.clone()
.color("#e74c3c")
.mirror(DVec3::ZERO, DVec3::Z)
.translate(DVec3::new(160.0, 0.0, 0.0)),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create file");
cadrum::io::write_step(&solids, &mut f).expect("failed to write STEP");
let mut svg = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::io::write_svg(&solids, DVec3::new(1.0, 1.0, 1.0), 0.5, true, &mut svg).expect("failed to write SVG");
}
Boolean
Boolean operations: union, subtract, and intersect between a box and a cylinder.
cargo run --example 04_boolean
use cadrum::{Solid, Transform};
use glam::DVec3;
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let make_box = Solid::cube(20.0, 20.0, 20.0)
.color("#4a90d9");
let make_cyl = Solid::cylinder(8.0, DVec3::Z, 30.0)
.translate(DVec3::new(10.0, 10.0, -5.0))
.color("#e67e22");
let union = make_box.clone()
.union(&[make_cyl.clone()])?;
let subtract = make_box.clone()
.subtract(&[make_cyl.clone()])?
.translate(DVec3::new(40.0, 0.0, 0.0));
let intersect = make_box.clone()
.intersect(&[make_cyl.clone()])?
.translate(DVec3::new(80.0, 0.0, 0.0));
let shapes: Vec<Solid> = [union, subtract, intersect].concat();
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create file");
cadrum::io::write_step(&shapes, &mut f).expect("failed to write STEP");
let mut svg = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::io::write_svg(&shapes, DVec3::new(1.0, 1.0, 2.0), 0.5, true, &mut svg).expect("failed to write SVG");
Ok(())
}
Loft
Demo of Solid::loft: skin a smooth solid through cross-section wires.
cargo run --example 05_loft
use cadrum::{Edge, Error, Solid};
use glam::DVec3;
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::new(1.0, 0.0, 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::new(10.0, 0.0, 0.0));
let tilted = build_tilted()?.translate(DVec3::new(20.0, 0.0, 0.0));
let result = [frustum, morph, tilted];
let step_path = format!("{example_name}.step");
let mut f = std::fs::File::create(&step_path).expect("failed to create STEP file");
cadrum::io::write_step(&result, &mut f).expect("failed to write STEP");
println!("wrote {step_path}");
let svg_path = format!("{example_name}.svg");
let mut f = std::fs::File::create(&svg_path).expect("failed to create SVG file");
cadrum::io::write_svg(&result, DVec3::new(1.0, 1.0, 1.0), 0.5, true, &mut f).expect("failed to write SVG");
println!("wrote {svg_path}");
Ok(())
}
Sweep
Sweep showcase: M2 screw (helix spine) + U-shaped pipe (line+arc+line spine).
cargo run --example 06_sweep
use cadrum::{Edge, Error, ProfileOrient, Solid, SolidExt, Transform};
use glam::DVec3;
fn build_m2_screw() -> Result<Vec<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 = profile.align_z(helix.start_tangent(), helix.start_point()).translate(helix.start_point());
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 = thread.union([&shaft])?.intersect([&crest])?;
let head = Solid::cylinder(r_head, DVec3::Z, h_head).translate(DVec3::Z * h_thread);
thread_shaft.union([&head])
}
fn build_u_pipe() -> Result<Vec<Solid>, Error> {
let pipe_radius = 0.4;
let leg_length = 6.0;
let gap = 3.0;
let bend_radius = gap / 2.0;
let a = DVec3::ZERO;
let b = DVec3::new(0.0, 0.0, leg_length);
let arc_mid = DVec3::new(bend_radius, 0.0, leg_length + bend_radius);
let c = DVec3::new(gap, 0.0, leg_length);
let d = DVec3::new(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)?;
let pipe = Solid::sweep(&[profile], &[up_leg, bend, down_leg], ProfileOrient::Up(DVec3::Y))?;
Ok(vec![pipe])
}
fn main() {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let x_offset = 6.0;
let mut all: Vec<Solid> = Vec::new();
match build_m2_screw() {
Ok(screw) => {
all.extend(screw.color("red"));
println!("✓ screw built (red, centered at origin)");
}
Err(e) => eprintln!("✗ screw failed: {e}"),
}
match build_u_pipe() {
Ok(pipe) => {
let placed: Vec<Solid> = pipe.translate(DVec3::X * x_offset).color("blue");
all.extend(placed);
println!("✓ U-pipe built (blue, offset x={x_offset})");
}
Err(e) => eprintln!("✗ U-pipe failed: {e}"),
}
if all.is_empty() {
eprintln!("nothing to write");
return;
}
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::io::write_step(&all, &mut f).expect("failed to write STEP");
let mut f_svg = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::io::write_svg(&all, DVec3::new(1.0, 1.0, -1.0), 0.5, false, &mut f_svg).expect("failed to write SVG");
println!("wrote {example_name}.step / {example_name}.svg ({} solids)", all.len());
}
Sweep sections
Demo of Solid::sweep_sections: morph between cross-section profiles
cargo run --example 07_sweep_sections
use cadrum::{BSplineEnd, Edge, Error, ProfileOrient, Solid};
use glam::DVec3;
use std::f64::consts::TAU;
fn plasma_rib(phi: f64, ring_r: f64, a: f64, b: f64, twist_per_phi: f64, n: usize) -> Edge {
let center = DVec3::new(ring_r * phi.cos(), ring_r * phi.sin(), 0.0);
let radial = DVec3::new(phi.cos(), phi.sin(), 0.0);
let axial = DVec3::Z;
let twist = twist_per_phi * phi;
let cos_t = twist.cos();
let sin_t = twist.sin();
let pts: Vec<DVec3> = (0..n)
.map(|i| {
let theta = TAU * i as f64 / n as f64;
let lx = a * theta.cos();
let ly = b * theta.sin();
let r_offset = lx * cos_t - ly * sin_t;
let z_offset = lx * sin_t + ly * cos_t;
center + radial * r_offset + axial * z_offset
})
.collect();
Edge::bspline(pts, BSplineEnd::Periodic).expect("plasma rib bspline")
}
fn build_plasma() -> Result<Solid, Error> {
const N_RIBS: usize = 8;
const N_POINTS: usize = 32;
const RING_R: f64 = 6.0;
let spine = Edge::circle(RING_R, DVec3::Z)?;
let sections: Vec<Vec<Edge>> = (0..N_RIBS)
.map(|i| {
let phi = TAU * i as f64 / N_RIBS as f64;
vec![plasma_rib(phi, RING_R, 1.8, 1.2, 1.0, N_POINTS)]
})
.collect();
Ok(Solid::sweep_sections(§ions, std::slice::from_ref(&spine), ProfileOrient::Torsion)?.color("#87ceeb"))
}
fn blended_section(radius: f64, squareness: f64, z: f64, n_pts: usize) -> Edge {
let pts: Vec<DVec3> = (0..n_pts)
.map(|i| {
let theta = TAU * i as f64 / n_pts as f64;
let p = 2.0 + 8.0 * squareness; let ct = theta.cos();
let st = theta.sin();
let x = radius * ct.abs().powf(2.0 / p) * ct.signum();
let y = radius * st.abs().powf(2.0 / p) * st.signum();
DVec3::new(x, y, z)
})
.collect();
Edge::bspline(pts, BSplineEnd::Periodic).expect("blended section bspline")
}
fn build_morphing_pipe() -> Result<Solid, Error> {
const N_SECTIONS: usize = 5;
const N_POINTS: usize = 32;
const RADIUS: f64 = 2.0;
const LENGTH: f64 = 16.0;
let spine = Edge::line(DVec3::ZERO, DVec3::Z * LENGTH)?;
let sections: Vec<Vec<Edge>> = (0..N_SECTIONS)
.map(|i| {
let t = i as f64 / (N_SECTIONS - 1) as f64;
let z = t * LENGTH;
vec![blended_section(RADIUS, t, z, N_POINTS)]
})
.collect();
Ok(Solid::sweep_sections(§ions, std::slice::from_ref(&spine), ProfileOrient::Fixed)?.color("#d2691e"))
}
fn main() -> Result<(), Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let plasma = build_plasma()?;
let morphing = build_morphing_pipe()?.translate(DVec3::new(18.0, 0.0, -8.0));
let result = [plasma, morphing];
let step_path = format!("{example_name}.step");
let mut f = std::fs::File::create(&step_path).expect("failed to create STEP file");
cadrum::io::write_step(&result, &mut f).expect("failed to write STEP");
println!("wrote {step_path}");
let svg_path = format!("{example_name}.svg");
let mut f = std::fs::File::create(&svg_path).expect("failed to create SVG file");
cadrum::io::write_svg(&result, DVec3::new(1.0, 1.0, 1.0), 0.5, true, &mut f).expect("failed to write SVG");
println!("wrote {svg_path}");
Ok(())
}
Chijin
Build a chijin (hand drum from Amami Oshima) with colors, boolean ops, and SVG export.
cargo run --example 10_chijin
use cadrum::{Color, Edge, ProfileOrient, Solid, SolidExt};
use glam::DVec3;
use std::f64::consts::PI;
pub fn chijin() -> Result<Solid, cadrum::Error> {
let cylinder = Solid::cylinder(15.0, DVec3::Z, 8.0)
.translate(DVec3::new(0.0, 0.0, -4.0))
.color("#999");
let cross_section = Edge::polygon([
DVec3::new(0.0, 0.0, 5.0),
DVec3::new(15.0, 0.0, 5.0),
DVec3::new(17.0, 0.0, 3.0),
DVec3::new(15.0, 0.0, 4.0),
DVec3::new(0.0, 0.0, 4.0),
])?;
let spine = Edge::circle(1.0, DVec3::Z)?;
let sheet = Solid::sweep(&cross_section, &[spine], ProfileOrient::Up(DVec3::Z))?
.color("#fff");
let sheets = [sheet.clone().mirror(DVec3::ZERO, DVec3::Z), sheet];
let block_proto = Solid::cube(2.0, 8.0, 1.0)
.translate(DVec3::new(-1.0, -4.0, -0.5))
.rotate_z(60.0_f64.to_radians())
.translate(DVec3::new(0.0, 15.0, 0.0));
let hole_proto = Solid::cylinder(0.7, DVec3::new(10.0, 0.0, 30.0), 30.0)
.translate(DVec3::new(-5.0, 16.0, -15.0));
const N: usize = 20;
let angle = |i: usize| 2.0 * PI * (i as f64) / (N as f64);
let color = |i: usize| Color::from_hsv(i as f32 / N as f32, 1.0, 1.0);
let blocks: [Solid; N] = std::array::from_fn(|i| block_proto.clone().rotate_z(angle(i)).color(color(i)));
let holes: [Solid; N] = std::array::from_fn(|i| hole_proto.clone().rotate_z(angle(i)));
let result = [cylinder]
.union(&sheets)?
.subtract(&holes)?
.union(&blocks)?;
assert!(result.len() == 1);
Ok(result.into_iter().next().unwrap())
}
fn main() -> Result<(), cadrum::Error> {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let result = [chijin()?];
let step_path = format!("{example_name}.step");
let mut f = std::fs::File::create(&step_path).expect("failed to create STEP file");
cadrum::io::write_step(&result, &mut f).expect("failed to write STEP");
println!("wrote {step_path}");
let svg_path = format!("{example_name}.svg");
let mut f = std::fs::File::create(&svg_path).expect("failed to create SVG file");
cadrum::io::write_svg(&result, DVec3::new(1.0, 1.0, 1.0), 0.5, true, &mut f).expect("failed to write SVG");
println!("wrote {svg_path}");
Ok(())
}
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.