Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

img

Getting started

cadrum is a Rust CAD crate for engineering and scientific work. It consist of Open CASCASE kernel and a few topologically defined structs (Solid, Face, Edge) and a wide variety of fuctions which manipulate the topological sturucts. cadrum provide prebuilt Open CASCADE binaries for each environment including wasm32, which are automatically downloaded to save your time, instead of building it with CMake.

Installation

cadrum canbe installed via cargo from crates.io like cargo add cadrum

Hello world!

use cadrum::{DVec3, Solid};

fn main() -> Result<(), cadrum::Error> {
	let solids = [Solid::cube(DVec3::ZERO, DVec3::new(10.0, 20.0, 30.0)).color("#4a90d9")];
	let mesh = Solid::mesh(&solids, Default::default())?;
	mesh.scene(Default::default()).write_png([640, 640], &mut std::fs::File::create("hello_world.png").unwrap())?;
	Ok(())
}

Citation

@software{cadrum,
  author = {Satoshi Misumi},
  title = {cadrum: a Rust CAD crate using statically-linked, headless OpenCASCADE},
  year = {2026},
  url = {https://github.com/lzpel/cadrum},
  license = {MIT}
}

Primitives

Primitive solids: box, cylinder, sphere, cone, torus — colored and exported as STEP + SVG.

//! Primitive solids: box, cylinder, sphere, cone, torus — colored and exported as STEP + SVG.

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

01_primitives

Write read

Read and write: chain STEP and BRep round-trips with progressive rotation.

//! Read and write: chain STEP and BRep round-trips with progressive rotation.

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 brep_path = format!("{example_name}.brep");

	// 0. Original: read colored_box.step
	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"))?;

	// 1. STEP round-trip: rotate 30° → write → read
	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"))?;

	// 2. BRep round-trip: rotate another 30° → write → read
	let b_written: Vec<Solid> = a.clone().into_iter().map(|s| s.rotate_x(FRAC_PI_8)).collect();
	Solid::write_brep(&b_written, &mut std::fs::File::create(&brep_path).expect("create file"))?;
	let b = Solid::read_brep(&mut std::fs::File::open(&brep_path).expect("open file"))?;

	// 3. Arrange side by side and export SVG + STL
	let [min, max] = original[0].bounding_box();
	let spacing = (max - min).length() * 1.5;
	let all: Vec<Solid> = [original, a, b].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())?;

	// 4. Print summary
	let stl_path = format!("{example_name}.stl");
	for (label, path) in [("STEP", &step_path), ("BRep", &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.stl | 02_write_read.svg

02_write_read

Transform

Transform operations: translate, rotate, scale, and mirror applied to a cone.

//! Transform operations: translate, rotate, scale, and mirror applied to a cone.

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 = [
		// original — reference, no transform
		base.clone(),
		// translate — shift +20 along Z
		base.clone().color("#4a90d9").translate(DVec3::X * 40.0 + DVec3::Z * 20.0),
		// rotate — 90° around X axis so the cone tips toward Y
		base.clone().color("#e67e22").rotate_x(PI / 2.0).translate(DVec3::X * 80.0),
		// scaled — 1.5x from its local origin
		base.clone().color("#2ecc71").scale(DVec3::ZERO, 1.5).translate(DVec3::X * 120.0),
		// mirror — flip across Z=0 plane so the tip points down
		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

03_transform

Boolean

Boolean operations: union, subtract, and intersect between a box and a cylinder.

//! Boolean operations: union, subtract, and intersect between a box and a cylinder.

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.);

	// union: merge both shapes into one — offset X=0
	let union: Solid = (&make_box + &make_cyl).build()?;

	// subtract: box minus cylinder — offset X=40
	let subtract: Solid = (&make_box - &make_cyl).build()?;

	// intersect: only the overlapping volume — offset X=80
	let intersect: Solid = (&make_box * &make_cyl).build()?;

	let cylinder = Solid::cylinder(8.0, DVec3::Z * 30.0).translate(DVec3::X * 5.);
	let [cylinder0, cylinder1, cylinder2] = [cylinder.clone(), cylinder.clone().rotate_z(std::f64::consts::TAU / 3.), cylinder.clone().rotate_z(-std::f64::consts::TAU / 3.)];

	// union of all cylinders (fold from Boolean::default() = ⊥)
	let sum: Solid = [&cylinder0, &cylinder1, &cylinder2].into_iter().map(Boolean::from).reduce(|a, s| a + s).unwrap().build()?.clean()?;
	let sum = sum.color("#d875ff");

	// intersection of all cylinders (reduce — intersect has no fixed init)
	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

04_boolean

Extrude

Demo of Solid::extrude: push a closed 2D profile along a direction vector.

//! Demo of `Solid::extrude`: push a closed 2D profile along a direction vector.

use cadrum::{BSplineEnd, DVec3, Edge, Error, Solid};

/// Circle extruded at a steep angle → oblique cylinder.
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))
}

/// L-shaped polygon → L-beam.
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)
}

/// Square plate with a round bore: `Edge::loops` splits the profile, the first
/// loop bounds the solid and the second becomes the hole.
fn build_plate_with_bore() -> Result<Solid, Error> {
	let outer = Edge::polygon(&[DVec3::new(-4.0, -3.0, 0.0), DVec3::new(4.0, -3.0, 0.0), DVec3::new(4.0, 3.0, 0.0), DVec3::new(-4.0, 3.0, 0.0)])?;
	let bore = Edge::circle(1.5, DVec3::Z)?;
	Solid::extrude(&[outer, vec![bore]].concat(), DVec3::Z * 2.0)
}

/// Heart-shaped BSpline profile extruded along Z.
fn build_heart() -> Result<Solid, Error> {
	let profile = [Edge::bspline(
		&[
			DVec3::new(0.0, -4.0, 0.0), // bottom tip
			DVec3::new(2.0, -1.5, 0.0),
			DVec3::new(4.0, 1.5, 0.0),
			DVec3::new(2.5, 3.5, 0.0),  // right lobe top
			DVec3::new(0.0, 2.0, 0.0),  // center dip
			DVec3::new(-2.5, 3.5, 0.0), // left lobe top
			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 result = [build_oblique_cylinder()?.color("#f1c8b0").translate(DVec3::X * 10.0), build_l_beam()?.color("#b0f1c8").translate(DVec3::X * 20.0), build_heart()?.color("#f1b0b0").translate(DVec3::X * 30.0), build_plate_with_bore()?.color("#d4b0f1").translate(DVec3::X * 40.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(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

05_extrude

Revolve

Demo of Solid::revolve: sweep a closed profile about an axis.

//! Demo of `Solid::revolve`: sweep a closed profile about an axis.
//!
//! - **Pipe**: rectangle in the XZ plane turned a full circle about Z
//! - **Sphere**: half disc turned about its own diameter — a profile may touch the axis
//! - **Partial**: the same rectangle turned three quarters of the way, showing the angle
//! - **Channel**: rectangle with a circular hole → ring with a hidden toroidal channel

use cadrum::{DVec3, Edge, Error, Solid};
use std::f64::consts::TAU;

/// Rectangle `x ∈ [x0, x1]`, `z ∈ [z0, z1]` in the XZ plane, the plane that contains the Z axis.
fn rect(x0: f64, x1: f64, z0: f64, z1: f64) -> Result<Vec<Edge>, Error> {
	Edge::polygon(&[DVec3::new(x0, 0.0, z0), DVec3::new(x1, 0.0, z0), DVec3::new(x1, 0.0, z1), DVec3::new(x0, 0.0, z1)])
}

/// Full turn → pipe (simplest revolve).
fn build_pipe() -> Result<Solid, Error> {
	Solid::revolve(&rect(3.0, 5.0, 0.0, 4.0)?, DVec3::ZERO, DVec3::Z, TAU)
}

/// Half disc about its diameter → sphere. The straight edge lies on the axis.
fn build_sphere() -> Result<Solid, Error> {
	let r = 3.0;
	let profile = [Edge::arc_3pts(DVec3::new(0.0, 0.0, -r), DVec3::new(r, 0.0, 0.0), DVec3::new(0.0, 0.0, r))?, Edge::line(DVec3::new(0.0, 0.0, r), DVec3::new(0.0, 0.0, -r))?];
	Solid::revolve(&profile, DVec3::ZERO, DVec3::Z, TAU)
}

/// Three quarters of a turn → open ring segment.
fn build_partial() -> Result<Solid, Error> {
	Solid::revolve(&rect(3.0, 5.0, 0.0, 4.0)?, DVec3::ZERO, DVec3::Z, TAU * 0.75)
}

/// Rectangle with a circular hole: the hole sweeps into a toroidal channel inside the ring.
fn build_channel() -> Result<Solid, Error> {
	let hole = Edge::circle(1.0, DVec3::Y)?.translate(DVec3::new(4.0, 0.0, 2.0));
	Solid::revolve(&[rect(2.0, 6.0, 0.0, 4.0)?, vec![hole]].concat(), DVec3::ZERO, DVec3::Z, -TAU * 0.6)
}

fn main() -> Result<(), Error> {
	let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
	let result = [build_pipe()?.color("#b0d4f1"), build_sphere()?.color("#f1c8b0").translate(DVec3::X * 14.0), build_partial()?.color("#b0f1c8").translate(DVec3::X * 28.0), build_channel()?.color("#d4b0f1").translate(DVec3::X * 42.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(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_revolve.png | 06_revolve.step | 06_revolve.glb | 06_revolve.stl | 06_revolve.svg

06_revolve

Loft

Demo of Solid::loft: skin a solid through cross-section wires.

//! Demo of `Solid::loft`: skin a solid through cross-section wires.
//!
//! - **Frustum**: two circles of different radii → truncated cone (minimal loft)
//! - **Morph**: square polygon → circle (cross-section shape transition)
//! - **Tilted**: three non-parallel circular sections → twisted loft
//! - **Wing**: three NACA0012 sections lofted with `ruled=true` (straight ruled panels between sections — the sheet-metal / developable variant)

use cadrum::{BSplineEnd, DVec2, DVec3, Edge, Error, Solid};

/// Two circles → frustum (minimal loft example).
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], false)?.color("#cd853f"))
}

/// Square polygon → circle (2-section morph loft).
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)], false)?.color("#808000"))
}

/// Three non-parallel circular sections → twisted loft.
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], false)?.color("#4682b4"))
}

/// NACA0012-like airfoil section points (unit chord, 2D: x = chord, y = thickness).
/// Cosine spacing walks TE → upper → LE → lower → TE, returning a closed loop
/// with the TE point duplicated at the end (a closed section with a sharp TE,
/// interpolated as a NotAKnot open curve).
fn naca_points(n: usize) -> Vec<DVec2> {
	let half = |x: f64| 5.0 * 0.12 * (0.2969 * x.sqrt() - 0.1260 * x - 0.3516 * x * x + 0.2843 * x.powi(3) - 0.1036 * x.powi(4));
	let upper: Vec<DVec2> = (0..=n)
		.map(|i| {
			let x = (1.0 + (std::f64::consts::PI * i as f64 / n as f64).cos()) / 2.0;
			DVec2::new(x, half(x))
		})
		.collect();
	let lower: Vec<DVec2> = (1..=n)
		.map(|i| {
			let x = (1.0 - (std::f64::consts::PI * i as f64 / n as f64).cos()) / 2.0;
			DVec2::new(x, -half(x))
		})
		.collect();
	[upper, lower].concat()
}

/// Three NACA sections → tapered wing, lofted with `ruled=true` (straight panels).
fn build_wing(scale: f64) -> Result<Solid, Error> {
	let stations = [(1.0, 0.0), (0.6, 1.0), (0.5, 2.0)];
	let sections: Vec<[Edge; 1]> = stations
		.iter()
		.map(|&(c, z)| {
			let points: Vec<DVec3> = naca_points(60).into_iter().map(|p| DVec3::new(c * p.x, c * p.y, z) * scale).collect();
			[Edge::bspline(&points, BSplineEnd::NotAKnot).expect("NACA bspline section")]
		})
		.collect();
	Ok(Solid::loft(&sections, true)?.color("silver"))
}

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 wing = build_wing(10.0)?.align_z(-DVec3::X, -DVec3::Y).translate(DVec3::X * 20.0 + DVec3::Y * 12.0);

	let result = [frustum, morph, tilted, wing];

	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: 07_loft.png | 07_loft.step | 07_loft.glb | 07_loft.stl | 07_loft.svg

07_loft

Sweep

Sweep showcase: M2 screw (helix spine) + U-shaped pipe (line+arc+line spine)

//! Sweep showcase: M2 screw (helix spine) + U-shaped pipe (line+arc+line spine)
//!
//! `ProfileOrient` controls how the profile is oriented as it travels along the spine:
//!
//! - `Fixed`: profile is parallel-transported without rotating. Cross-sections
//!   stay parallel to the starting orientation. Suited for straight extrusions;
//!   on a curved spine the profile drifts off the tangent and the result breaks.
//! - `Torsion`: profile follows the spine's principal normal (raw Frenet–Serret
//!   frame). Suited for constant-curvature/torsion curves like helices and for
//!   3D free curves where the natural twist should carry into the profile.
//!   Fails near inflection points where the principal normal flips.
//! - `Up(axis)`: profile keeps `axis` as its binormal — at every point the
//!   profile is rotated around the tangent so one in-plane axis stays in the
//!   tangent–`axis` plane. Suited for roads/rails/pipes that must preserve a
//!   gravity direction. On a helix, `Up(helix_axis)` is equivalent to `Torsion`.
//!   Fails when the tangent becomes parallel to `axis`.
//! - `Auxiliary(aux_spine)`: profile's tracked axis points from the main spine
//!   toward a parallel auxiliary spine. Arbitrary twist control — e.g. a
//!   helical `aux_spine` on a straight `spine` produces a twisted ribbon.

use cadrum::{BSplineEnd, DQuat, DVec3, Edge, Error, ProfileOrient, Solid};

// ==================== Component 1: M2 ISO screw ====================

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;
	// ISO M thread fundamental triangle height: H = √3/2 · P (sharp 60° triangle).
	let r_delta = 3f64.sqrt() / 2.0 * h_pitch;

	// Helix spine at the root radius. x_ref=+X anchors the start at (r-r_delta, 0, 0).
	let helix = Edge::helix(r - r_delta, h_pitch, h_thread, DVec3::Z, DVec3::X)?;

	// Closed triangular profile in local coords (x: radial, y: along helix tangent).
	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)])?;

	// Align profile +Z with the helix start tangent, then translate to the start point.
	let profile: Vec<Edge> = profile.into_iter().map(|e| e.align_z(helix.start_tangent(), helix.start_point()).translate(helix.start_point())).collect();

	// Sweep along the helix. Up(+Z) ≡ Torsion for a helix and yields a correct thread.
	let thread = Solid::sweep(&profile, &[helix], ProfileOrient::Up(DVec3::Z))?;

	// Reconstruct the ISO 68-1 basic profile (trapezoid) from the sharp triangle:
	//   union(shaft) fills the bottom H/4 → P/4-wide flat at the root
	//   intersect(crest) trims the top H/8 → P/8-wide flat at the crest
	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()?;

	// Stack the flat head on top. Screw ends up centered on the origin.
	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"))
}

// ==================== Component 2: U-shaped pipe ====================

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;

	// U-shaped path in the XZ plane, centered on origin in X: A↑B ⌒ C↓D.
	let a = DVec3::new(0.0, -half_gap, 0.0);
	let b = DVec3::new(0.0, -half_gap, leg_length);
	let arc_mid = DVec3::new(0.0, 0.0, leg_length + bend_radius);
	let c = DVec3::new(0.0, half_gap, leg_length);
	let d = DVec3::new(0.0, half_gap, 0.0);

	// Spine wire: line → semicircle → line.
	let up_leg = Edge::line(a, b)?;
	let bend = Edge::arc_3pts(b, arc_mid, c)?;
	let down_leg = Edge::line(c, d)?;

	// Circular profile in XY (normal +Z) translated to the spine start `a`.
	// Spine tangent at `a` is +Z, so the XY-plane circle is already aligned.
	let profile = Edge::circle(pipe_radius, DVec3::Z)?.translate(a);

	// Up(+Y) fixes the binormal to the path-plane normal, avoiding Frenet
	// degeneracy on the straight segments.
	let pipe = Solid::sweep(&[profile], &[up_leg, bend, down_leg], ProfileOrient::Torsion)?;
	Ok(pipe.translate(DVec3::X * 6.0).color("blue"))
}

fn build_moebius(r: f64) -> Result<Solid, Error> {
	const N: usize = 24;
	let phi = |phi: f64| -> [DVec3; 2] {
		let [p, d] = [DVec3::X * r, DVec3::X * r / 10.];
		let [rz, ry] = [DQuat::from_rotation_z(phi), DQuat::from_rotation_y(2. * phi)];
		[rz * p, rz * (p + ry * d)]
	};
	let ring: [[DVec3; 2]; N] = std::array::from_fn(|i| phi(i as f64 / N as f64 * std::f64::consts::TAU));
	let spine = Edge::bspline(&(ring.map(|v| v[0])), BSplineEnd::Periodic)?;
	let aux = Edge::bspline(&(ring.map(|v| v[1])), BSplineEnd::Periodic)?;
	let profile = Edge::polygon(&[DVec3::new(-0.3, -0.3, 0.0), DVec3::new(0.5, -0.5, 0.0), DVec3::new(0.3, 0.3, 0.0), DVec3::new(-0.3, 0.3, 0.0)])?;
	let profile: Vec<Edge> = profile.into_iter().map(|e| e.align_z(spine.start_tangent(), DVec3::Y).translate(spine.start_point())).collect();
	let band = Solid::sweep(&profile, &[spine], ProfileOrient::Auxiliary(&[aux]))?;
	Ok(band.align_z(DVec3::X, DVec3::Z).translate(DVec3::X * 12.0 + DVec3::Z * r).color("#2ebc71"))
}

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_moebius(3.)?];

	Solid::write_step(&all, &mut std::fs::File::create(format!("{example_name}.step")).unwrap())?;

	// Helical threads have dense hidden lines that clutter the output; disable them.
	let mesh = Solid::mesh(&all, Default::default())?;
	let scene = mesh.scene(cadrum::SceneOption { view: DVec3::new(-1.0, 1.0, 0.5), 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: 08_sweep.png | 08_sweep.step | 08_sweep.glb | 08_sweep.stl | 08_sweep.svg

08_sweep

Shell

Demo of Solid::shell:

//! Demo of `Solid::shell`:
//! - Cube: remove top face, offset inward → open-top container
//! - Sealed cube: empty open_faces → solid with an internal void (outer skin
//!   + reversed inner shell)
//! - Torus: bisect with a half-space to introduce planar cut faces, then
//!   shell using those cut faces as the openings → thin-walled half-ring
//!   with both cross-sections exposed

use cadrum::{DVec3, Error, Solid};

fn hollow_cube() -> Result<Solid, Error> {
	let cube = Solid::cube(DVec3::ZERO, DVec3::splat(8.0));
	// TopExp_Explorer order on a box is stable; +Z face ends up last.
	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);
	// Bisect with Y=0 half-space (normal +Y): keep the +Y half of the ring — always 1 solid.
	let cutter = Solid::half_space(DVec3::ZERO, -DVec3::Z);
	// `iter_history()` yields [post_id, src_id] pairs for every result face.
	// Filter to those whose src_id is one of the cutter's faces, then collect
	// their post_ids — these are the planar cut faces in the result that we
	// want to use as shell openings.
	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())?;

	// Isometric view from (1, 1, 2) with shading so the cavity depth reads
	// naturally.
	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: 09_shell.png | 09_shell.step | 09_shell.glb | 09_shell.stl | 09_shell.svg

09_shell

Bspline

// 2 field-period stellarator-like torus.
// `Solid::bspline` is fed a 2D control-point grid to build a periodic B-spline solid.
// Every variation below is invariant under phi → phi+π (or shifts by a multiple
// of 2π), so the resulting shape has 180° rotational symmetry around the Z axis:
//   a(phi)       = 1.8 + 0.6 * sin(2φ)      radial semi-axis
//   b(phi)       = 1.0 + 0.4 * cos(2φ)      Z semi-axis
//   psi(phi)     = 2 * phi                  cross-section twist (2 turns per loop)
//   z_shift(phi) = 1.0 * sin(2φ)            vertical undulation
use cadrum::{DQuat, DVec3, Solid};
use std::f64::consts::TAU;
const M: usize = 48; // toroidal (U) — must be even for 180° symmetry
const N: usize = 24; // poloidal (V) — arbitrary
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; // twist: 2 full turns per toroidal loop
	let z_shift = 1.0 * two_phi.sin();
	// 1. Local cross-section (pre-twist ellipse in the (X, Z) plane)
	let local_raw = DVec3::X * (a * theta.cos()) + DVec3::Z * (b * theta.sin());
	// 2. Rotate by psi around the local Y axis (major-circle tangent) — the twist
	let local_twisted = DQuat::from_axis_angle(DVec3::Y, psi) * local_raw;
	// 3. Undulate vertically in the local frame
	let local_shifted = local_twisted + DVec3::Z * z_shift;
	// 4. Push outward along the major radius by RING_R
	let translated = local_shifted + DVec3::X * RING_R;
	// 5. Rotate the whole point around the global Z axis by phi
	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, 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: 10_bspline.png | 10_bspline.step | 10_bspline.glb | 10_bspline.stl | 10_bspline.svg

10_bspline

Fillet

Demo of Solid::fillet_edges:

//! Demo of `Solid::fillet_edges`:
//! - All 12 cube edges filleted uniformly (rounded cube)
//! - Only top 4 edges filleted (soft top, sharp base)
//! - Cylinder top circular edge filleted (coin shape)

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;
	// Top cap boundary: a closed circular edge whose start == end lives at z = h.
	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;
	// Top cap boundary: a closed circular edge whose start == end lives at z = h.
	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: 11_fillet.png | 11_fillet.step | 11_fillet.glb | 11_fillet.stl | 11_fillet.svg

11_fillet

Chamfer

Demo of Solid::chamfer_edges — mirror of 11_fillet.rs using bevels:

//! Demo of `Solid::chamfer_edges` — mirror of `11_fillet.rs` using bevels:
//! - All 12 cube edges chamfered uniformly (beveled cube)
//! - Only top 4 edges chamfered (soft top, sharp base)
//! - Cylinder top circular edge chamfered (coin with beveled rim)

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;
	// Top cap boundary: a closed circular edge whose start == end lives at z = h.
	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;
	// Top cap boundary: a closed circular edge whose start == end lives at z = h.
	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: 12_chamfer.png | 12_chamfer.step | 12_chamfer.glb | 12_chamfer.stl | 12_chamfer.svg

12_chamfer

Offset

Signed surface offset: one drilled block grown by +3, the original, and shrunk by -3 side by side — every face moves along its normal, so the hole shrinks as the body grows.

//! Signed surface offset: one drilled block grown by +3, the original, and shrunk by -3 side by side — every face moves along its normal, so the hole shrinks as the body grows.

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(24.0, 40.0, 16.0));
	let hole = Solid::cylinder(5.0, DVec3::Z * 16.0).translate(DVec3::new(12.0, 20.0, 0.0));
	let part = (&block - &hole).build()?.color("#4a90d9");

	// offset > 0 grows outward (hole tightens), offset < 0 shrinks inward (hole widens).
	let grown = part.offset(3.0, part.iter_face())?.translate(-DVec3::X * 50.0).color("#e67e22");
	let shrunk = part.offset(-3.0, part.iter_face())?.translate(DVec3::X * 50.0).color("#2ecc71");

	let solids = [grown, part, shrunk];
	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: 13_offset.png | 13_offset.step | 13_offset.glb | 13_offset.stl | 13_offset.svg

13_offset

Sew

Demo of Solid::sew on faces cut from elementary surfaces by Face::face, with no planar caps beyond the tetrahedron.

//! Demo of `Solid::sew` on faces cut from elementary surfaces by `Face::face`, with no planar caps beyond the tetrahedron.
//!
//! - **Tetrahedron**: four triangles sharing six lines between every other corner of a cube
//! - **Napkin ring**: a sphere band around a cylinder hole; its volume is π h³ / 6 whatever the sphere's radius
//! - **Ice cream cone**: an upside-down cone and a scoop hanging out of its rim, about to fall

use cadrum::{DVec3, Edge, Error, Face, SceneOption, Solid, Surface, SurfaceKind};
use std::f64::consts::{FRAC_PI_2, FRAC_PI_8};

/// Regular tetrahedron on every other corner of `[-a, a]³`, its faces sharing the six lines between them.
fn sew_tetrahedron(a: f64) -> Result<Solid, Error> {
	let cube: [DVec3; 8] = std::array::from_fn(|i| DVec3::new(if i & 1 != 0 { a } else { -a }, if i & 2 != 0 { a } else { -a }, if i & 4 != 0 { a } else { -a }));
	let pairs = [(0, 3), (0, 6), (0, 5), (3, 6), (3, 5), (6, 5)];
	let edges: [Edge; 6] = std::array::from_fn(|n| Edge::line(cube[pairs[n].0], cube[pairs[n].1]).expect("distinct corners"));
	let loops: [[usize; 3]; 4] = [[3, 5, 4], [2, 5, 1], [0, 4, 2], [1, 3, 0]];
	let faces: [Face; 4] = loops.map(|l| {
		let (origin, u) = (edges[l[0]].start_point(), edges[l[0]].end_point() - edges[l[0]].start_point());
		let normal = u.cross(edges[l[1]].end_point() - edges[l[1]].start_point()).normalize();
		let normal = if normal.dot(origin) < 0.0 { -normal } else { normal };
		Face::face(&Surface { origin, axis_z: normal, axis_x: u.normalize(), kind: SurfaceKind::Plane }, l.map(|e| &edges[e])).expect("each loop winds counterclockwise")
	});
	Solid::sew(&faces, 1e-6)
}

/// Sphere of radius `r` drilled through along Z, leaving a ring of height `h`.
fn sew_napkin_ring() -> Result<Solid, Error> {
	let (r, h) = (3.0_f64, 4.0);
	let rho = (r * r - h * h / 4.0).sqrt();
	// Both faces lie left of the circles, between them.
	let rims = [Edge::circle(rho, DVec3::Z)?.translate(DVec3::NEG_Z * h / 2.0), Edge::circle(rho, DVec3::NEG_Z)?.translate(DVec3::Z * h / 2.0)];
	let band = Face::face(&Surface { origin: DVec3::ZERO, axis_z: DVec3::Z, axis_x: DVec3::X, kind: SurfaceKind::Sphere { radius: r } }, &rims)?;
	let hole = Face::face(&Surface { origin: DVec3::ZERO, axis_z: DVec3::Z, axis_x: DVec3::X, kind: SurfaceKind::Cylinder { radius: rho } }, &rims)?;
	Solid::sew([&band, &hole], 1e-6)
}

/// Upside-down cone of height `h` from the apex at the origin, with a scoop whose center hangs `d` below the rim.
fn sew_ice_cream_cone() -> Result<Solid, Error> {
	let (h, phi, d) = (6.0_f64, FRAC_PI_8, 1.5_f64);
	let rim = h * phi.tan();
	let r = rim.hypot(d);
	let cone = Face::face(&Surface { origin: DVec3::ZERO, axis_z: DVec3::NEG_Z, axis_x: DVec3::X, kind: SurfaceKind::Cone { radius: 0.0, semi_angle: phi } }, [&Edge::circle(rim, DVec3::Z)?.translate(DVec3::NEG_Z * h)])?;
	let scoop = Face::face(&Surface { origin: DVec3::NEG_Z * (h + d), axis_z: DVec3::NEG_Z, axis_x: DVec3::X, kind: SurfaceKind::Sphere { radius: r } }, [&Edge::circle(rim, DVec3::NEG_Z)?.translate(DVec3::NEG_Z * h)])?;
	Solid::sew([&cone, &scoop], 1e-6)
}

fn main() -> Result<(), Error> {
	let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
	let result = [sew_tetrahedron(2.0)?.color("#b0d4f1").rotate_z(FRAC_PI_2), sew_napkin_ring()?.color("#f1c8b0").translate(DVec3::X * 10.0), sew_ice_cream_cone()?.color("#b0f1c8").translate(DVec3::new(20.0, 0.0, 5.2))];
	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(SceneOption { view: DVec3::X + DVec3::Y + DVec3::Z * 1.5, ..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: 14_sew.png | 14_sew.step | 14_sew.glb | 14_sew.stl | 14_sew.svg

14_sew

Multiview

Fixed 4-view multiview PNG for LLM-driven design loops.

//! Fixed 4-view multiview PNG for LLM-driven design loops.
//!
//! A single call to `Solid::write_multiview_png` produces a 1024×1024 PNG that lays out
//! 4 views — ISO plus the axis cyclic order (+X / +Y / +Z) — at the same scale. With no
//! parameters to tune, Solid maps 1:1 to an image, which suits state-snapshot rendering
//! for LLMs and automated design loops.

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);
	// Axis-orientation check: carve only the +X+Y+Z corner with a sphere.
	// Which corner the notch appears in on each panel uniquely confirms the gnomon's direction.
	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: 15_multiview.png | 15_multiview.glb | 15_multiview.stl

15_multiview

Chijin

Build a chijin (hand drum from Amami Oshima) with colors, boolean ops, and SVG export.

//! Build a chijin (hand drum from Amami Oshima) with colors, boolean ops, and SVG export.

use cadrum::{Color, DVec3, Edge, Solid};
use std::f64::consts::PI;

/// Paint every face. A face colour outranks the solid colour, and boolean ops carry it
/// via history, so it survives where `Solid::color` (the whole solid) would be overwritten.
fn color_faces(mut solid: Solid, color: impl Into<Color>) -> Solid {
	let c = color.into();
	let ids: Vec<u64> = solid.iter_face().map(|f| f.id()).collect();
	for id in ids {
		solid.colormap_mut().insert(id, c);
	}
	solid
}

fn chijin() -> Result<Solid, cadrum::Error> {
	// ── Body (cylinder): r=15, h=8, centered at origin (y=-4..+4) ────────
	let cylinder = Solid::cylinder(15.0, DVec3::Y * 8.0).translate(DVec3::Y * -4.0).color("#999");

	// ── Sheet: closed polygon in the XY plane (z=0), swept 360° around Y
	// により面と縁を一体で生成する。Face::from_polygon + Face::revolve の置換版:
	//   - Edge::polygon は最後の点 → 最初の点を自動補完して閉じる
	//   - spine は Y 軸まわりの円。半径によらずプロファイルを Y 周りに純粋回転
	//     させるだけなので任意の正の値で可
	//   - ProfileOrient::Up(Y) でプロファイルの上方向を Y 固定 → 回転(revolve)と等価
	let cross_section = Edge::polygon(&[DVec3::new(0.0, 5.0, 0.0), DVec3::new(15.0, 5.0, 0.0), DVec3::new(17.0, 3.0, 0.0), DVec3::new(15.0, 4.0, 0.0), DVec3::new(0.0, 4.0, 0.0)])?;
	let sheet = color_faces(Solid::revolve(&cross_section, DVec3::ZERO, DVec3::Y, std::f64::consts::PI * 2.)?, "#fff");
	let sheets = [sheet.clone().mirror(DVec3::ZERO, DVec3::Y), sheet];

	// ── Lacing blocks: 2x1x8, rotated 60° around Y, placed at z=15 ──────
	let block_proto = Solid::cube(DVec3::ZERO, DVec3::new(2.0, 1.0, 8.0)).translate(DVec3::new(-1.0, -0.5, -4.0)).rotate_y(-60.0_f64.to_radians()).translate(DVec3::Z * 15.0);

	// ── Lacing holes: thin cylinders through each block ──────────────────
	let hole_proto = Solid::cylinder(0.7, (DVec3::X * 10.0 + DVec3::Y * 30.0).normalize() * 30.0).translate(DVec3::new(-5.0, -15.0, 16.0));

	// Distribute N blocks and holes evenly around Y, each block in a rainbow color
	// N 個のブロックと穴を Y 軸周りに等間隔配置、各ブロックに虹色を割り当て
	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| color_faces(block_proto.clone().rotate_y(-angle(i)), color(i)));
	let holes: [Solid; N] = std::array::from_fn(|i| hole_proto.clone().rotate_y(-angle(i)));
	// ── Assemble with boolean operations: union, subtract, union ─────────
	let mut result: Solid = (&cylinder + &sheets[0] + &sheets[1]).build()?;
	for i in 0..N {
		result = (&result - &holes[i] + &blocks[i]).build()?;
	}
	Ok(result)
}

fn main() -> Result<(), cadrum::Error> {
	let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
	let result = [chijin()?];

	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([1280, 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())?;
	// This size 1280 x 640 is special because 100_chijin.png is used for github repository social media preview image.
	// > we recommend a size of at least 640 by 320 pixels (1280 by 640 pixels for best display). See https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/customizing-your-repositorys-social-media-preview

	println!("wrote {example_name}.step / {example_name}.svg / {example_name}.png");
	Ok(())
}

Output: 100_chijin.png | 100_chijin.step | 100_chijin.glb | 100_chijin.stl | 100_chijin.svg

100_chijin

Joint

use cadrum::{Boolean, Edge, Error, Solid};
use glam::DVec3;
fn part(inner: f64, outer: f64, height: f64) -> Result<[Solid; 3], Error> {
	let outer_solid = Solid::cube(DVec3::ZERO, DVec3::new(outer, outer, height)).translate(DVec3::ONE * -outer / 2.0);
	let between_edge = Edge::polygon(&[DVec3::new(outer / 2.0, height - outer / 2.0, 0.0), DVec3::new(outer / 2.0, outer / 2.0, 0.0), DVec3::new(height - outer / 2.0, outer / 2.0, 0.0)])?;
	let between_solid = Solid::extrude(&between_edge, DVec3::Z * outer / 2.0)?;
	let inner_solid = Solid::cylinder(inner / 2.0, DVec3::Z * height * 1000.0);
	Ok([outer_solid, between_solid, inner_solid])
}
fn main() -> Result<(), Error> {
	let example_name = std::path::Path::new(file!()).file_stem().unwrap().to_str().unwrap();
	let (inner, outer, height) = (8.1, 20., 60.0);
	let base = part(inner, outer, height)?;
	let parts = [base.clone(), base.clone().map(|s| s.rotate(DVec3::ZERO, DVec3::ONE, std::f64::consts::TAU / 3.0)), base.clone().map(|s| s.rotate(DVec3::ZERO, DVec3::ONE, std::f64::consts::TAU * 2.0 / 3.0))];
	// positive = 各 part の [outer, between] (= p[0], p[1]) を全部 union
	let positive: Solid = parts.iter().flat_map(|p| [&p[0], &p[1]]).map(Boolean::from).reduce(|a, b| a + b).unwrap().build()?;
	// negative = 各 part の inner cylinder (= p[2]) を全部 union
	let negative: Solid = parts.iter().map(|p| &p[2]).map(Boolean::from).reduce(|a, b| a + b).unwrap().build()?;
	let result = [(&positive - &negative).build()?];

	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 { 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 / {example_name}.stl / {example_name}.glb");
	Ok(())
}

/*
										   .:.
										::....::.
								   ..::.   ...  .::..
								..:-.   .:-.::-..   .-..
							  .-:.   .-:..  :  ..-.   ..-:.
							 .::.   .::..   :   .::.   .::-
							 ..  ::..   .-..:.::   ..:-.  :
							 ..    .-=..  ....   .:=.     :
							 ..    :. .:::.   .::. .-.    :
							 ..  .-.      .-=:       :.   :
							 ..  -.       .:-:        :.  :
							 ...:.       .-.:.-.       :. :
							 ..-.       .:  : .:        :::
							 .:        .:   :  .-.       .=
							.-.       :.    :   .:.       .-
						   .:        ::     :    .-.       .:
						  .-.      .:.      :      :.       .-.
						 :..       -.       :      .-.       .:.
						.:.      .:.        :        :.       .-
					  .:.       .-.         :        .:.        :.
					  ..       .:           :          ::       .+.
					 -.        -.           :          .::        :.
					:.       .:             :            .:       .-.
				  .-.       .-.             :             .:.       ..
				  :.       .:.              :              .:       .:.
				.-.       .:.               :               .-        .:
			   .:.       ..                 :                .:.       ::
			  .-        .:                 .-                 .+        .-
		   ::.-.       :.               ::....:-.              .:.       .::-.
	   .:-. .-        ::            .::.        .:::.           .-.       .:..::.
   ..::.   .:.      .-.          .-:.               .-..          :.       .:.   .-..
 .-..     ::        :.        .-..                    ..:-.       .-.       .:.    ..::
:::.     .:       .-.      :::.                          ..::.      :.       .-.    .::=
:  .:-..:.       .:.   ..-.                                   ::..   -.        -..:-.  -
:    ...-.      .-. .-:..                                       ..-.  .:       -:..    -
:  .::.  ..::. .-.-:.                                              ..::::  .::.   .-.  -
:  .:..-:.   .:+:.......................................................-=..   .::...  -
:  .:    .-:  ..                                                        .:  .-.    ..  -
:  .:  .::.:  ..                                                        .:  :..:.  ..  -
:  .:::.   :  ..                                                        .:  :.  ..::.  -
:   .::..  :  ..                                                        .:  :.  ..-.   -
.:-.   ..-.:  .............:-..............................:-............:  :.-:..   .-.
   .::.   ..  ..       .-:..                                 .::..      .:  ...  .::.
	   .:..   ..    .:-.                                        .:::.   .:   ..::.
		  .-:... .-:                                                .-: .: .=:.
			..:-..                                                    ..:-..

																							*/

Output: 101_joint.png | 101_joint.step | 101_joint.glb | 101_joint.stl | 101_joint.svg

101_joint