use cadrum::{Edge, Error, Solid};
use glam::DVec3;
use std::f64::consts::PI;
fn write_outputs(solids: &[Solid], name: &str) {
std::fs::create_dir_all("out").unwrap();
let mut f = std::fs::File::create(format!("out/{name}.step")).unwrap();
cadrum::io::write_step(solids, &mut f).expect("step write");
let mut f = std::fs::File::create(format!("out/{name}.stl")).unwrap();
cadrum::io::write_stl(solids, 0.1, &mut f).expect("stl write");
let mut f = std::fs::File::create(format!("out/{name}.svg")).unwrap();
cadrum::io::write_svg(solids, DVec3::new(1.0, 1.0, 2.0), 0.5, true, &mut f).expect("svg write");
}
#[test]
fn test_loft_01_frustum_volume_matches_analytical() {
let r1 = 3.0;
let r2 = 2.0;
let h = 10.0;
let lower = vec![Edge::circle(r1, DVec3::Z).unwrap()];
let upper = vec![Edge::circle(r2, DVec3::Z).unwrap().translate(DVec3::Z * h)];
let frustum = Solid::loft(&[lower, upper]).expect("frustum loft should succeed");
let expected = PI / 3.0 * h * (r1 * r1 + r1 * r2 + r2 * r2);
let actual = frustum.volume();
let rel_err = (actual - expected).abs() / expected;
assert!(
rel_err < 0.01,
"frustum volume {:.4} vs analytical {:.4} (relative error {:.4})",
actual, expected, rel_err
);
assert_eq!(frustum.shell_count(), 1);
write_outputs(std::slice::from_ref(&frustum), "test_loft_01_frustum_volume_matches_analytical");
println!("frustum loft: volume = {:.4} (expected {:.4})", actual, expected);
}
#[test]
fn test_loft_02_single_section_returns_loft_failed() {
let only = vec![Edge::circle(1.0, DVec3::Z).unwrap()];
let result = Solid::loft(&[only]);
let err = result.err().expect("single section must return Err");
match err {
Error::LoftFailed(msg) => {
assert!(
msg.contains("≥2") || msg.contains(">=2") || msg.contains("got 1"),
"error message should mention min section count, got: {}",
msg
);
}
other => panic!("expected Error::LoftFailed, got {:?}", other),
}
}
#[test]
fn test_loft_03_empty_section_returns_loft_failed() {
let s1 = vec![Edge::circle(1.0, DVec3::Z).unwrap()];
let empty: Vec<Edge> = vec![];
let s3 = vec![Edge::circle(1.0, DVec3::Z).unwrap().translate(DVec3::Z * 10.0)];
let result = Solid::loft(&[s1, empty, s3]);
let err = result.err().expect("empty section must return Err");
match err {
Error::LoftFailed(msg) => {
assert!(
msg.contains("empty"),
"error message should mention empty section, got: {}",
msg
);
}
other => panic!("expected Error::LoftFailed, got {:?}", other),
}
}
#[test]
fn test_loft_04_closure_iterator_form() {
let ribs: Vec<Edge> = (0..3)
.map(|i| {
Edge::circle(2.0 + i as f64 * 0.5, DVec3::Z)
.unwrap()
.translate(DVec3::Z * i as f64 * 5.0)
})
.collect();
let plasma = Solid::loft(ribs.iter().map(|e| [e])).expect("closure-form loft should succeed");
assert_eq!(plasma.shell_count(), 1);
assert!(plasma.volume() > 0.0);
write_outputs(std::slice::from_ref(&plasma), "test_loft_05_closure_iterator_form");
}