use axiolid_brep_audit::geometric_audit;
use axiolid_construct::revolve_exact::revolve_profile_exact;
use axiolid_core::{Frame2, Interval, Point2, Point3, Tolerance, Vec2, Vec3};
use axiolid_curve::{Circle2, Curve2, Line2};
use axiolid_profile::{Contour, ContourProfile, Profile, ProfileSegment, SectionProfile};
use axiolid_surface::Surface;
const TAU: f64 = core::f64::consts::TAU;
fn line(from: Point2, to: Point2) -> ProfileSegment {
ProfileSegment {
curve: Curve2::Line(Line2 {
origin: from,
direction: to - from,
}),
domain: Interval::UNIT,
same_sense: true,
}
}
fn contour(segments: Vec<ProfileSegment>) -> Profile {
Profile::Contour(ContourProfile {
outer: Contour::new(segments),
holes: Vec::new(),
})
}
fn revolve(profile: &Profile, axis_x: f64) -> axiolid_brep::ExactBRep {
let solid = revolve_profile_exact(
profile,
Point3::new(axis_x, 0.0, 0.0),
Vec3::Y,
TAU,
Tolerance::METRE,
)
.expect("a contour clear of the axis revolves");
let health = geometric_audit(&solid, Tolerance::METRE);
assert!(
health.is_consistent(),
"revolved solid must audit clean, found {:?}",
health.defects()
);
solid
}
#[test]
fn a_rectangular_contour_matches_the_dedicated_rectangle_path() {
let as_contour = contour(vec![
line(Point2::new(4.0, -1.5), Point2::new(6.0, -1.5)),
line(Point2::new(6.0, -1.5), Point2::new(6.0, 1.5)),
line(Point2::new(6.0, 1.5), Point2::new(4.0, 1.5)),
line(Point2::new(4.0, 1.5), Point2::new(4.0, -1.5)),
]);
let solid = revolve(&as_contour, 0.0);
let mut radii: Vec<f64> = solid
.surfaces()
.iter()
.filter_map(|s| match s {
Surface::Cylinder(c) => Some(c.radius),
_ => None,
})
.collect();
radii.sort_by(f64::total_cmp);
assert_eq!(
radii.len(),
2,
"an annular tube has two walls, got {radii:?}"
);
assert!((radii[0] - 4.0).abs() < 1e-12, "inner: {radii:?}");
assert!((radii[1] - 6.0).abs() < 1e-12, "outer: {radii:?}");
let pappus = TAU * 5.0 * 6.0;
let value = axiolid_measure::exact_properties(&solid, Tolerance::METRE)
.expect("a revolved solid is measurable")
.signed_volume;
assert!(
(value - pappus).abs() < 1e-9,
"expected {pappus}, got {value}"
);
}
#[test]
fn an_oblique_segment_sweeps_a_cone() {
let trapezoid = contour(vec![
line(Point2::new(2.0, 0.0), Point2::new(4.0, 0.0)),
line(Point2::new(4.0, 0.0), Point2::new(3.0, 2.0)),
line(Point2::new(3.0, 2.0), Point2::new(2.0, 2.0)),
line(Point2::new(2.0, 2.0), Point2::new(2.0, 0.0)),
]);
let solid = revolve(&trapezoid, 0.0);
let cones = solid
.surfaces()
.iter()
.filter(|s| matches!(s, Surface::Cone(_)))
.count();
assert_eq!(cones, 1, "the oblique edge must sweep exactly one cone");
}
#[test]
fn an_arc_segment_sweeps_a_torus_and_matches_pappus() {
let r = 0.5;
let quarter = core::f64::consts::FRAC_PI_2;
let profile = contour(vec![
line(Point2::new(3.0, 0.0), Point2::new(5.0, 0.0)),
line(Point2::new(5.0, 0.0), Point2::new(5.0, 2.0 - r)),
ProfileSegment {
curve: Curve2::Circle(Circle2 {
frame: Frame2 {
origin: Point2::new(5.0 - r, 2.0 - r),
x: Vec2::X,
y: Vec2::Y,
},
radius: r,
}),
domain: Interval::new(0.0, quarter),
same_sense: true,
},
line(Point2::new(5.0 - r, 2.0), Point2::new(3.0, 2.0)),
line(Point2::new(3.0, 2.0), Point2::new(3.0, 0.0)),
]);
let solid = revolve(&profile, 0.0);
let tori: Vec<(f64, f64)> = solid
.surfaces()
.iter()
.filter_map(|s| match s {
Surface::Torus(t) => Some((t.major_radius, t.minor_radius)),
_ => None,
})
.collect();
assert_eq!(tori.len(), 1, "the arc must sweep one torus, got {tori:?}");
assert!(
(tori[0].0 - (5.0 - r)).abs() < 1e-12 && (tori[0].1 - r).abs() < 1e-12,
"torus radii should be ({}, {r}), got {:?}",
5.0 - r,
tori[0]
);
}
#[test]
fn a_section_profile_revolves_instead_of_refusing() {
let profile = Profile::Section(SectionProfile::Trapezium {
bottom_x: 1.0,
top_x: 0.6,
y: 1.5,
top_offset: 0.2,
});
let solid = revolve(&profile, -3.0);
assert!(
solid.topology().faces().len() >= 3,
"a revolved trapezium needs at least three faces"
);
}
#[test]
fn a_centre_line_profile_revolves_instead_of_refusing() {
let path = Contour::new(vec![line(Point2::new(2.0, 0.0), Point2::new(2.0, 3.0))]);
let profile = Profile::CenterLine(axiolid_profile::CenterLineProfile::from_width(path, 0.4));
let solid = revolve(&profile, 0.0);
let cylinders = solid
.surfaces()
.iter()
.filter(|s| matches!(s, Surface::Cylinder(_)))
.count();
assert_eq!(cylinders, 2, "an offset strip revolves to two walls");
}
#[test]
fn a_section_crossing_the_axis_is_still_refused() {
let straddling = contour(vec![
line(Point2::new(-1.0, 0.0), Point2::new(1.0, 0.0)),
line(Point2::new(1.0, 0.0), Point2::new(1.0, 1.0)),
line(Point2::new(1.0, 1.0), Point2::new(-1.0, 1.0)),
line(Point2::new(-1.0, 1.0), Point2::new(-1.0, 0.0)),
]);
let error = revolve_profile_exact(&straddling, Point3::ZERO, Vec3::Y, TAU, Tolerance::METRE)
.expect_err("a section crossing the axis must refuse");
assert!(
format!("{error:?}").contains("crossing the axis"),
"got {error:?}"
);
}
#[test]
fn a_clockwise_arc_sweeps_the_material_side_of_the_tube() {
let r = 0.5;
let quarter = core::f64::consts::FRAC_PI_2;
let profile = contour(vec![
line(Point2::new(3.0, 0.0), Point2::new(5.0, 0.0)),
line(Point2::new(5.0, 0.0), Point2::new(5.0, 2.0 - r)),
ProfileSegment {
curve: Curve2::Circle(Circle2 {
frame: Frame2 {
origin: Point2::new(5.0, 2.0),
x: Vec2::X,
y: Vec2::Y,
},
radius: r,
}),
domain: Interval::new(-quarter, -core::f64::consts::PI),
same_sense: true,
},
line(Point2::new(5.0 - r, 2.0), Point2::new(3.0, 2.0)),
line(Point2::new(3.0, 2.0), Point2::new(3.0, 0.0)),
]);
let solid = revolve(&profile, 0.0);
let tori: Vec<(f64, f64)> = solid
.surfaces()
.iter()
.filter_map(|s| match s {
Surface::Torus(t) => Some((t.major_radius, t.minor_radius)),
_ => None,
})
.collect();
assert_eq!(tori.len(), 1, "one arc sweeps one torus: {tori:?}");
assert!(
(tori[0].0 - 5.0).abs() < 1e-12,
"concave notch centres its tube at x = 5, got {tori:?}"
);
}
#[test]
fn an_arc_crossing_the_tube_branch_cut_keeps_its_stated_sweep() {
let r = 0.5;
let cx = 4.0;
let cy = 1.0;
let third = 3.0 * core::f64::consts::FRAC_PI_4; let fifth = 5.0 * core::f64::consts::FRAC_PI_4; let ax = cx + r * third.cos();
let ay = cy + r * third.sin();
let bx = cx + r * fifth.cos();
let by = cy + r * fifth.sin();
let profile = contour(vec![
ProfileSegment {
curve: Curve2::Circle(Circle2 {
frame: Frame2 {
origin: Point2::new(cx, cy),
x: Vec2::X,
y: Vec2::Y,
},
radius: r,
}),
domain: Interval::new(third, fifth),
same_sense: true,
},
line(Point2::new(bx, by), Point2::new(5.0, by)),
line(Point2::new(5.0, by), Point2::new(5.0, ay)),
line(Point2::new(5.0, ay), Point2::new(ax, ay)),
]);
let solid = revolve(&profile, 0.0);
let tori: Vec<(f64, f64)> = solid
.surfaces()
.iter()
.filter_map(|s| match s {
Surface::Torus(t) => Some((t.major_radius, t.minor_radius)),
_ => None,
})
.collect();
assert_eq!(tori.len(), 1, "one arc sweeps one torus: {tori:?}");
assert!(
(tori[0].0 - cx).abs() < 1e-12 && (tori[0].1 - r).abs() < 1e-12,
"tube must sit on the arc centre: {tori:?}"
);
let tube_spans: Vec<f64> = solid
.topology()
.loops()
.iter()
.flat_map(|loop_value| loop_value.edges.iter())
.filter_map(|edge_use| solid.edge_interval(edge_use.edge))
.map(|interval| interval.end - interval.start)
.filter(|span| (span.abs() - TAU).abs() > 1e-9 && span.abs() > 1e-9)
.collect();
let quarter = core::f64::consts::FRAC_PI_2;
assert!(
tube_spans.iter().any(|span| (span - quarter).abs() < 1e-9),
"the tube seam must span +90 degrees, got {tube_spans:?}"
);
assert!(
!tube_spans
.iter()
.any(|span| (span + 3.0 * quarter).abs() < 1e-9),
"a -270 degree seam means the end angle came from the endpoint: {tube_spans:?}"
);
}
#[test]
fn every_revolved_wall_faces_out_of_the_solid() {
use axiolid_evaluate::partials;
use axiolid_topology::Orientation;
let section = contour(vec![
line(Point2::new(4.0, -1.5), Point2::new(6.0, -1.5)),
line(Point2::new(6.0, -1.5), Point2::new(6.0, 1.5)),
line(Point2::new(6.0, 1.5), Point2::new(4.0, 1.5)),
line(Point2::new(4.0, 1.5), Point2::new(4.0, -1.5)),
]);
let solid = revolve(§ion, 0.0);
let topology = solid.topology();
let mut walls = 0;
for face in topology.faces() {
let surface = &solid.surfaces()[face.surface.expect("surface").index()];
let Surface::Cylinder(cylinder) = surface else {
continue;
};
walls += 1;
let bound = &face.bounds[0];
let wire = &topology.loops()[bound.loop_id.index()];
let mut corners = Vec::new();
for (index, use_) in wire.edges.iter().enumerate() {
let Curve2::Line(pcurve) = &solid.curves2()[use_.pcurve.expect("pcurve").index()]
else {
panic!("a seam loop is straight in its parameters");
};
let span = solid
.pcurve_interval(bound.loop_id, index)
.expect("interval");
corners.push(pcurve.origin + pcurve.direction * span.start);
}
let mut twice_area = 0.0;
for i in 0..corners.len() {
twice_area += corners[i].perp_dot(corners[(i + 1) % corners.len()]);
}
let mut sense = twice_area.signum();
if face.orientation == Orientation::Reversed {
sense = -sense;
}
if bound.orientation == Orientation::Reversed {
sense = -sense;
}
let (su, sv) = partials(surface, 1.0, 1.0).expect("partials");
let normal = su.cross(sv) * sense;
let point = axiolid_evaluate::surface::evaluate(surface, 1.0, 1.0).expect("point");
let radial = Vec3::new(point.x, 0.0, point.z).normalize();
let outward = if (cylinder.radius - 6.0).abs() < 1e-12 {
1.0
} else {
-1.0
};
assert!(
normal.dot(radial) * outward > 0.0,
"wall of radius {} faces into the solid",
cylinder.radius
);
}
assert_eq!(walls, 2, "an annular tube has two walls");
}