cadrum

Rust CAD library powered by statically linked, headless OpenCASCADE (OCCT 8.0.0-rc5).
Usage
More examples with source code are available at lzpel.github.io/cadrum.
Add this to your Cargo.toml:
[dependencies]
cadrum = "^0.7"
Build
cargo build automatically downloads a prebuilt OCCT 8.0.0-rc5 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 |
✅ |
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() {
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::X * 30.0)
.color("#e67e22"),
Solid::sphere(8.0)
.translate(DVec3::X * 60.0 + DVec3::Z * 15.0)
.color("#2ecc71"),
Solid::cone(8.0, 0.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"),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create file");
cadrum::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::mesh(&solids, 0.5).and_then(|m| m.write_svg(DVec3::ONE, DVec3::Z, true, false, &mut svg)).expect("failed to write 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::{Compound, 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 = cadrum::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::write_step(&a_written, &mut std::fs::File::create(&step_path).expect("create file"))?;
let a = cadrum::read_step(&mut std::fs::File::open(&step_path).expect("open file"))?;
let b_written = a.clone().rotate_x(FRAC_PI_8);
cadrum::write_brep_text(&b_written, &mut std::fs::File::create(&text_path).expect("create file"))?;
let b = cadrum::read_brep_text(&mut std::fs::File::open(&text_path).expect("open file"))?;
let c_written = b.clone().rotate_x(FRAC_PI_8);
cadrum::write_brep_binary(&c_written, &mut std::fs::File::create(&brep_path).expect("create file"))?;
let c = cadrum::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::mesh(&all, 0.5).and_then(|m| m.write_svg(DVec3::new(1.0, 1.0, 2.0), DVec3::Z, true, false, &mut svg))?;
let mut stl = std::fs::File::create(format!("{example_name}.stl")).expect("create file");
cadrum::mesh(&all, 0.1).and_then(|m| m.write_stl(&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::{DVec3, Solid};
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::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),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create file");
cadrum::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::mesh(&solids, 0.5).and_then(|m| m.write_svg(DVec3::ONE, DVec3::Z, true, false, &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::{Compound, 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(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
.union(&[make_cyl.clone()])?;
let subtract = make_box
.subtract(&[make_cyl.clone()])?
.translate(DVec3::X * 40.0);
let intersect = make_box
.intersect(&[make_cyl])?
.translate(DVec3::X * 80.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::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::mesh(&shapes, 0.5).and_then(|m| m.write_svg(DVec3::new(1.0, 1.0, 2.0), DVec3::Z, true, false, &mut svg)).expect("failed to write SVG");
Ok(())
}
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];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::write_step(&result, &mut f).expect("failed to write STEP");
let mut f = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::mesh(&result, 0.5).and_then(|m| m.write_svg(DVec3::ONE, DVec3::Z, true, false, &mut f)).expect("failed to write SVG");
println!("wrote {example_name}.step / {example_name}.svg");
Ok(())
}
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];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::write_step(&result, &mut f).expect("failed to write STEP");
let mut f = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::mesh(&result, 0.5).and_then(|m| m.write_svg(DVec3::ONE, DVec3::Z, true, false, &mut f)).expect("failed to write SVG");
println!("wrote {example_name}.step / {example_name}.svg");
Ok(())
}
Sweep
Sweep showcase: M2 screw (helix spine) + U-shaped pipe (line+arc+line spine)
cargo run --example 07_sweep
use cadrum::{Compound, DVec3, Edge, Error, ProfileOrient, Solid, Wire};
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);
Ok(thread_shaft.union([&head])?.color("red"))
}
fn build_u_pipe() -> Result<Vec<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(vec![pipe].translate(DVec3::X * 6.0).color("blue"))
}
fn build_twisted_ribbon() -> Result<Vec<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(vec![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: Vec<Solid> = [build_m2_screw()?, build_u_pipe()?, build_twisted_ribbon()?].concat();
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::write_step(&all, &mut f)?;
let mut f_svg = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::mesh(&all, 0.5)?.write_svg(DVec3::new(1.0, 1.0, -1.0), DVec3::Z, false, false, &mut f_svg)?;
println!("wrote {example_name}.step / {example_name}.svg ({} solids)", all.len());
Ok(())
}
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(8.0, 8.0, 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(8.0, 8.0, 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 (mut halves, [_, from_cutter]) = torus.intersect_with_metadata(&[cutter])?;
let half = halves.pop().ok_or(Error::BooleanOperationFailed)?;
half.shell(thickness, half.iter_face().filter(|f| from_cutter.contains(&f.tshape_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),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::write_step(&result, &mut f).expect("failed to write STEP");
let mut f = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::mesh(&result, 0.2).and_then(|m| m.write_svg(DVec3::new(1.0, 1.0, 2.0), DVec3::Z, true, true, &mut f)).expect("failed to write SVG");
println!("wrote {example_name}.step / {example_name}.svg");
Ok(())
}
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() {
let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
let grid: [[DVec3; N]; M] = std::array::from_fn(|i| std::array::from_fn(|j| point(i, j)));
let plasma = Solid::bspline(grid, true).expect("2-period bspline torus should succeed");
let objects = [plasma.color("cyan")];
let mut f = std::fs::File::create(format!("{example_name}.step")).unwrap();
cadrum::write_step(&objects, &mut f).unwrap();
let mut f_svg = std::fs::File::create(format!("{example_name}.svg")).unwrap();
cadrum::mesh(&objects, 0.1).and_then(|m| m.write_svg(DVec3::new(0.05, 0.05, 1.0), DVec3::Y, false, true, &mut f_svg)).unwrap();
println!("wrote {example_name}.step / {example_name}.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(size, size, 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(size, size, 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),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::write_step(&result, &mut f).expect("failed to write STEP");
let mut f = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::mesh(&result, 0.2).and_then(|m| m.write_svg(DVec3::new(1.0, 1.0, 2.0), DVec3::Z, true, true, &mut f)).expect("failed to write SVG");
println!("wrote {example_name}.step / {example_name}.svg");
Ok(())
}
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(size, size, 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(size, size, 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),
];
let mut f = std::fs::File::create(format!("{example_name}.step")).expect("failed to create STEP file");
cadrum::write_step(&result, &mut f).expect("failed to write STEP");
let mut f = std::fs::File::create(format!("{example_name}.svg")).expect("failed to create SVG file");
cadrum::mesh(&result, 0.2).and_then(|m| m.write_svg(DVec3::new(1.0, 1.0, 2.0), DVec3::Z, true, true, &mut f)).expect("failed to write SVG");
println!("wrote {example_name}.step / {example_name}.svg");
Ok(())
}
Features
color (default): Colored STEP I/O via XDE. Enables write_step_with_colors,
read_step_with_colors, and per-face color on Solid.
source-build: Download and build OCCT from upstream sources via CMake.
Enable this on triples without a published prebuilt.
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.
Release Notes
0.6.0
source-build feature now gates cmake/walkdir as optional build-dependencies. Default cargo build no longer compiles them, significantly reducing build time on prebuilt targets. Users on unsupported targets must enable --features source-build (behavior unchanged — previously these targets also failed, just with a download error instead of a clear message).
x86_64-pc-windows-gnu prebuilt added via Docker cross-compilation with Debian mingw-w64 (posix thread model). All MinGW runtime DLLs are statically absorbed — the resulting exe depends only on Windows OS DLLs.
- LGPL 2.1 §2 compliance: source builds now retain only the ~9 patched OCCT source files alongside the
.a libraries, removing the unmodified bulk (~88 MB of data/dox/tests). The patched files carry timestamped headers per §2(a).
OCCT_ROOT relative path handling fixed: resolved via env::current_dir() instead of the unreliable CARGO_TARGET_DIR heuristic. --target <triple> flag now works correctly.
build.rs restructured: resolve_occt uses match chains with #[cfg] for source-build vs prebuilt paths. Source-build code lives in #[cfg(feature = "source-build")] mod source. patch_occt_sources split into walk_occt_sources + patch_or_none (side-effect-free).
- README simplified: Build section moved after Usage with a prebuilt target table + OS icons.
0.5.1
0.4.5 was published briefly but its version number was lower than the
already-published 0.5.0 (OCCT 7.9.3, older feature set), so cargo add cadrum would silently pick up 0.5.0 instead of the newer 0.4.5 code.
Re-released as 0.5.1 with identical contents. Prefer 0.5.1 over 0.4.5.
Solid::bspline<const M, const N>(grid, periodic) — new constructor: build a periodic B-spline solid from a 2D control-point grid. V (cross-section) is always periodic; U (longitudinal) is controlled by the periodic flag (torus when true, capped pipe when false). Implemented via GeomAPI_PointsToBSplineSurface::Interpolate over an augmented grid plus SetUPeriodic/SetVPeriodic.
write_svg / Mesh::to_svg now take shading: bool — opt-in Lambertian shading with head-on light. When true, triangles are tinted by 0.5 + 0.5 * (normal · dir) so curved/organic shapes read clearly; false reproduces the pre-0.5.1 flat rendering. Breaking vs 0.5.0: existing callers must add the flag (pass false to preserve earlier output).
examples/08_bspline.rs rewritten: 2 field-period stellarator-like torus with twisted + vertically undulating elliptic cross-sections, exercising Solid::bspline and shading=true.
tests/bspline.rs added: verifies 180° point symmetry of the stellarator shape via XZ/YZ half-space intersection (s1 ≈ s3, s2 ≈ s4).
Error::BsplineFailed(String) new variant. Breaking for downstream code that does exhaustive match on Error.
- OCCT 8.0.0 deprecation warnings resolved in
make_bspline_edge and make_bspline_solid (NCollection_HArray1<gp_Pnt> via local using alias to bypass the Handle() macro comma-splitting issue; NCollection_Array2<gp_Pnt> directly).
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.