#![allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
use ogeom_core::Tolerances;
use ogeom_geom::Curve3d as _;
use ogeom_math::{Circle, Frame, Point};
use ogeom_topo::{Filter, ShapeType, explore};
const T: Tolerances = Tolerances::millimetres();
fn fine() -> ogeom_mesh::Deflection {
ogeom_mesh::Deflection {
chord: 1e-4,
..ogeom_mesh::Deflection::default()
}
}
fn volume(model: &ogeom_topo::Model, shape: &ogeom_topo::Shape) -> f64 {
ogeom_algo::volume_properties(model, shape, fine(), T)
.unwrap()
.mass
}
#[test]
fn a_straight_pipe_is_a_cylinder() {
let mut model = ogeom_topo::Model::new();
let line = ogeom_geom::LineCurve::segment(Point::ORIGIN, Point::new(0.0, 0.0, 2.0), T).unwrap();
let curve = ogeom_geom::Curve::Line(line);
let domain = curve.domain();
let spine = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let result = ogeom_offset::make_pipe(&mut model, &spine, 0.3, T).unwrap();
let expected = core::f64::consts::PI * 0.09 * 2.0;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < 5e-4,
"straight pipe volume {measured} against {expected}"
);
assert!(!result.history.generated(&spine).is_empty());
}
#[test]
fn a_quarter_arc_pipe_is_a_torus_segment() {
let mut model = ogeom_topo::Model::new();
let circle = Circle::new(Frame::WORLD, 2.0, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let spine = ogeom_algo::make_edge(&mut model, curve, (0.0, core::f64::consts::FRAC_PI_2), T)
.unwrap()
.shape;
let result = ogeom_offset::make_pipe(&mut model, &spine, 0.3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let expected = core::f64::consts::PI * 0.09 * 2.0 * core::f64::consts::FRAC_PI_2;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < 1e-3,
"arc pipe volume {measured} against {expected}"
);
let faces = ogeom_topo::explore(
&model,
&result.shape,
Filter::OfType(ogeom_topo::ShapeType::Face),
)
.unwrap();
assert_eq!(faces.len(), 4);
}
#[test]
fn a_closed_circular_pipe_is_the_whole_torus() {
let mut model = ogeom_topo::Model::new();
let circle = Circle::new(Frame::WORLD, 2.0, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let spine = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let result = ogeom_offset::make_pipe(&mut model, &spine, 0.3, T).unwrap();
let expected = 2.0 * core::f64::consts::PI * core::f64::consts::PI * 2.0 * 0.09;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < 2e-3,
"torus pipe volume {measured} against {expected}"
);
}
#[test]
fn a_pipe_that_swallows_its_spine_is_refused() {
let mut model = ogeom_topo::Model::new();
let circle = Circle::new(Frame::WORLD, 0.5, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let spine = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
assert!(ogeom_offset::make_pipe(&mut model, &spine, 0.6, T).is_err());
}
fn square(model: &mut ogeom_topo::Model, half: f64, z: f64) -> ogeom_topo::Shape {
let corners = [
Point::new(-half, -half, z),
Point::new(half, -half, z),
Point::new(half, half, z),
Point::new(-half, half, z),
];
ogeom_algo::make_polygon(model, &corners, true, T)
.unwrap()
.shape
}
#[test]
fn a_polygonal_loft_is_the_frustum_pyramid() {
let mut model = ogeom_topo::Model::new();
let bottom = square(&mut model, 1.0, 0.0);
let top = square(&mut model, 0.5, 2.0);
let result = ogeom_offset::make_loft(&mut model, &bottom, &top, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let expected = 2.0 / 3.0 * (4.0 + 1.0 + 2.0);
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < 1e-9,
"polygonal loft volume {measured} against {expected}"
);
assert!(!result.history.generated(&bottom).is_empty());
}
#[test]
fn a_circular_loft_is_the_cone_frustum() {
let mut model = ogeom_topo::Model::new();
let ring = |model: &mut ogeom_topo::Model, r: f64, z: f64| {
let frame = Frame::new(
Point::new(0.0, 0.0, z),
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, r, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let edge = ogeom_algo::make_edge(model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let bottom = ring(&mut model, 1.0, 0.0);
let top = ring(&mut model, 0.5, 2.0);
let result = ogeom_offset::make_loft(&mut model, &bottom, &top, T).unwrap();
let pi = core::f64::consts::PI;
let expected = pi * 2.0 / 3.0 * 0.5_f64.mul_add(0.5, 1.0f64.mul_add(1.0, 1.0 * 0.5));
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < 1e-3,
"circular loft volume {measured} against {expected}"
);
}
#[test]
fn a_twisted_loft_s_walls_are_bilinear_and_measure_as_the_prismoid() {
let mut model = ogeom_topo::Model::new();
let bottom = square(&mut model, 1.0, 0.0);
let corners = [
Point::new(0.0, -0.7, 2.0),
Point::new(0.7, 0.0, 2.0),
Point::new(0.0, 0.7, 2.0),
Point::new(-0.7, 0.0, 2.0),
];
let top = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let result = ogeom_offset::make_loft(&mut model, &bottom, &top, T).unwrap();
let low = [
Point::new(-1.0, -1.0, 0.0),
Point::new(1.0, -1.0, 0.0),
Point::new(1.0, 1.0, 0.0),
Point::new(-1.0, 1.0, 0.0),
];
let shoelace = |c: &[Point]| -> f64 {
(0..c.len())
.map(|i| {
let (a, b) = (c[i], c[(i + 1) % c.len()]);
a.x * b.y - b.x * a.y
})
.sum::<f64>()
.abs()
/ 2.0
};
let mid: Vec<Point> = low
.iter()
.zip(&corners)
.map(|(a, b)| a.midpoint(*b))
.collect();
let (a0, a_mid, a1) = (shoelace(&low), shoelace(&mid), shoelace(&corners));
let expected = 2.0 * (a0 + 4.0 * a_mid + a1) / 6.0;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < expected * 5e-3,
"twisted loft volume {measured} against the prismoid's {expected}"
);
}
#[test]
fn a_skinned_loft_through_cone_sections_measures_as_the_frustum() {
let mut model = ogeom_topo::Model::new();
let ring = |model: &mut ogeom_topo::Model, r: f64, z: f64| {
let frame = Frame::new(
Point::new(0.0, 0.0, z),
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, r, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let edge = ogeom_algo::make_edge(model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let sections = [
ring(&mut model, 1.0, 0.0),
ring(&mut model, 0.75, 1.0),
ring(&mut model, 0.5, 2.0),
];
let result = ogeom_offset::make_loft_skinned(&mut model, §ions, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let pi = core::f64::consts::PI;
let expected = pi * 2.0 / 3.0 * (1.0 + 0.5 + 0.25);
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < 1e-2,
"skinned frustum volume {measured} against {expected}"
);
}
#[test]
fn a_pipe_along_a_free_form_spine_holds_pappus() {
let mut model = ogeom_topo::Model::new();
let spine_curve = ogeom_geom::Curve::BSpline(
ogeom_geom::BSplineCurve::new(
ogeom_math::KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).unwrap(),
vec![
Point::new(0.0, 0.0, 0.0),
Point::new(2.0, 0.0, 1.0),
Point::new(4.0, 0.0, -1.0),
Point::new(6.0, 0.0, 0.0),
],
T,
)
.unwrap(),
);
let domain = ogeom_geom::Curve3d::domain(&spine_curve);
let spine = ogeom_algo::make_edge(&mut model, spine_curve.clone(), domain, T)
.unwrap()
.shape;
let r = 0.2;
let result = ogeom_offset::make_pipe_skinned(&mut model, &spine, r, 1e-4, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let length = ogeom_algo::curve_length(&spine_curve, domain, T).unwrap();
let pi = core::f64::consts::PI;
let expected = pi * r * r * length;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < expected * 0.01,
"free-form pipe volume {measured} against {expected}"
);
assert!(!result.history.generated(&spine).is_empty());
}
fn square_profile(
model: &mut ogeom_topo::Model,
centre: ogeom_math::Point,
tangent: ogeom_math::Vector,
side: f64,
) -> ogeom_topo::Shape {
use ogeom_math::Direction;
let normal = Direction::new(tangent, T).unwrap();
let plane = ogeom_math::Plane::through(centre, normal);
let frame = plane.frame();
let h = side / 2.0;
let corners: Vec<ogeom_math::Point> = [(-h, -h), (h, -h), (h, h), (-h, h)]
.iter()
.map(|(a, b)| centre + frame.x().vector() * *a + frame.y().vector() * *b)
.collect();
let wire = ogeom_algo::make_polygon(model, &corners, true, T)
.unwrap()
.shape;
let surface: ogeom_geom::SurfaceGeometry =
ogeom_geom::PlaneSurface::over(plane, (-side * 2.0, side * 2.0), (-side * 2.0, side * 2.0))
.unwrap()
.into();
ogeom_algo::make_face(model, surface, std::slice::from_ref(&wire), T)
.unwrap()
.shape
}
fn quarter_arc(model: &mut ogeom_topo::Model, r: f64) -> ogeom_topo::Shape {
let circle = ogeom_math::Circle::new(Frame::WORLD, r, T).unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
ogeom_algo::make_edge(model, curve, (0.0, core::f64::consts::FRAC_PI_2), T)
.unwrap()
.shape
}
#[test]
fn a_square_face_along_an_arc_sweeps_the_volume_pappus_names() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let spine = quarter_arc(&mut model, r);
let start = Point::new(r, 0.0, 0.0);
let profile = square_profile(&mut model, start, ogeom_math::Vector::Y, 4.0);
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let expected = 16.0 * (core::f64::consts::FRAC_PI_2 * r);
let measured = ogeom_algo::volume_properties(
&model,
&result.shape,
ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
T,
)
.unwrap()
.mass;
assert!(
(measured - expected).abs() / expected < 0.01,
"pipe shell volume {measured} against {expected}"
);
assert!(!result.history.generated(&spine).is_empty());
assert!(!result.history.generated(&profile).is_empty());
}
#[test]
fn a_triangle_along_a_helix_makes_a_thread() {
use ogeom_geom::Curve3d as _;
let mut model = ogeom_topo::Model::new();
let helix = ogeom_geom::HelixCurve::new(Frame::WORLD, 5.0, 4.0, 2.0).unwrap();
let curve: ogeom_geom::Curve = helix.into();
let domain = curve.domain();
let length = {
let mut sum = 0.0;
let mut last = curve.point_at(domain.0, T).unwrap();
for i in 1..=512 {
let t = domain.0 + (domain.1 - domain.0) * f64::from(i) / 512.0;
let p = curve.point_at(t, T).unwrap();
sum += last.distance(p);
last = p;
}
sum
};
let start = curve.point_at(domain.0, T).unwrap();
let tangent = curve.d1_at(domain.0, T).unwrap();
let spine = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let normal = ogeom_math::Direction::new(tangent, T).unwrap();
let plane = ogeom_math::Plane::through(start, normal);
let frame = plane.frame();
let corners: Vec<Point> = [(0.6, 0.0), (-0.3, 0.45), (-0.3, -0.45)]
.iter()
.map(|(a, b)| start + frame.x().vector() * *a + frame.y().vector() * *b)
.collect();
let profile = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, true, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let area = 0.9 * 0.45; let expected = area * length;
let measured =
ogeom_algo::volume_properties(&model, &result.shape, ogeom_mesh::Deflection::default(), T)
.unwrap()
.mass;
assert!(
(measured - expected).abs() / expected < 0.1,
"thread volume {measured} against {expected}"
);
}
#[test]
fn a_profile_with_a_hole_sweeps_the_hole() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let spine = quarter_arc(&mut model, r);
let start = Point::new(r, 0.0, 0.0);
let normal = ogeom_math::Direction::new(ogeom_math::Vector::Y, T).unwrap();
let plane = ogeom_math::Plane::through(start, normal);
let frame = plane.frame();
let h = 2.0;
let corners: Vec<Point> = [(-h, -h), (h, -h), (h, h), (-h, h)]
.iter()
.map(|(a, b)| start + frame.x().vector() * *a + frame.y().vector() * *b)
.collect();
let outer = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let hole_circle =
ogeom_math::Circle::new(ogeom_math::Frame::about(start, normal), 1.0, T).unwrap();
let hole_curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(hole_circle).into();
let hole_domain = ogeom_geom::Curve3d::domain(&hole_curve);
let hole_edge = ogeom_algo::make_edge(&mut model, hole_curve, hole_domain, T)
.unwrap()
.shape;
let hole = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&hole_edge), T)
.unwrap()
.shape;
let surface: ogeom_geom::SurfaceGeometry =
ogeom_geom::PlaneSurface::over(plane, (-8.0, 8.0), (-8.0, 8.0))
.unwrap()
.into();
let profile = ogeom_algo::make_face(&mut model, surface, &[outer, hole], T)
.unwrap()
.shape;
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let pi = core::f64::consts::PI;
let expected = (16.0 - pi) * (core::f64::consts::FRAC_PI_2 * r);
let measured = ogeom_algo::volume_properties(
&model,
&result.shape,
ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
T,
)
.unwrap()
.mass;
assert!(
(measured - expected).abs() / expected < 0.01,
"holed pipe shell volume {measured} against {expected}"
);
}
#[test]
fn a_frenet_frame_on_a_straight_spine_is_refused_by_name() {
let mut model = ogeom_topo::Model::new();
let line =
ogeom_geom::LineCurve::segment(Point::new(0.0, 0.0, 0.0), Point::new(0.0, 10.0, 0.0), T)
.unwrap();
let curve: ogeom_geom::Curve = line.into();
let domain = ogeom_geom::Curve3d::domain(&curve);
let spine = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let profile = square_profile(
&mut model,
Point::new(0.0, 0.0, 0.0),
ogeom_math::Vector::Y,
2.0,
);
let err =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, true, 1e-3, T).unwrap_err();
assert!(err.to_string().contains("Frenet"), "{err}");
}
#[test]
fn a_leaning_pipe_shell_profile_is_refused_by_name() {
let mut model = ogeom_topo::Model::new();
let spine = quarter_arc(&mut model, 20.0);
let profile = square_profile(
&mut model,
Point::new(20.0, 0.0, 0.0),
ogeom_math::Vector::Z,
4.0,
);
let err =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap_err();
assert!(err.to_string().contains("leans"), "{err}");
}
#[test]
fn a_closed_loft_through_four_sections_is_a_watertight_ring() {
let mut model = ogeom_topo::Model::new();
let ring_r = 10.0;
let mut sections = Vec::new();
for i in 0..16 {
let angle = core::f64::consts::TAU / 16.0 * f64::from(i);
let centre = Point::new(ring_r * angle.cos(), ring_r * angle.sin(), 0.0);
let tangent = ogeom_math::Vector::new(-angle.sin(), angle.cos(), 0.0);
let normal = ogeom_math::Direction::new(tangent, T).unwrap();
let frame = Frame::new(centre, normal, ogeom_math::Direction::Z, T).unwrap();
let circle = Circle::new(frame, 1.0, T).unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
let domain = curve.domain();
let edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
sections.push(
ogeom_algo::make_wire(&mut model, std::slice::from_ref(&edge), T)
.unwrap()
.shape,
);
}
let result = ogeom_offset::make_loft_skinned_closed(&mut model, §ions, 2e-2, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
assert_eq!(
explore(&model, &result.shape, Filter::OfType(ShapeType::Face))
.unwrap()
.len(),
1
);
let expected = core::f64::consts::PI * (core::f64::consts::TAU * ring_r);
let measured = ogeom_algo::volume_properties(
&model,
&result.shape,
ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
T,
)
.unwrap()
.mass;
assert!(
(measured - expected).abs() / expected < 0.05,
"closed loft volume {measured} against {expected}"
);
for section in §ions {
assert!(!result.history.generated(section).is_empty());
}
}
#[test]
fn a_rectangle_lofted_to_a_point_is_the_pyramid_the_closed_form_names() {
let mut model = ogeom_topo::Model::new();
let corners = [
Point::new(0.0, 0.0, 0.0),
Point::new(4.0, 0.0, 0.0),
Point::new(4.0, 3.0, 0.0),
Point::new(0.0, 3.0, 0.0),
];
let base = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let apex = ogeom_algo::make_vertex(&mut model, Point::new(1.0, 1.0, 6.0)).shape;
let result = ogeom_offset::make_loft(&mut model, &base, &apex, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let expected = 4.0 * 3.0 * 6.0 / 3.0;
let measured =
ogeom_algo::volume_properties(&model, &result.shape, ogeom_mesh::Deflection::default(), T)
.unwrap()
.mass;
assert!(
(measured - expected).abs() < 1e-6,
"pyramid volume {measured} against {expected}"
);
assert!(!result.history.generated(&base).is_empty());
assert!(!result.history.generated(&apex).is_empty());
}
#[test]
fn a_circle_lofted_to_a_point_on_its_axis_is_a_cone() {
let mut model = ogeom_topo::Model::new();
let circle = Circle::new(Frame::WORLD, 2.0, T).unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
let domain = curve.domain();
let ring = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let base = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&ring), T)
.unwrap()
.shape;
let apex = ogeom_algo::make_vertex(&mut model, Point::new(0.0, 0.0, 5.0)).shape;
let result = ogeom_offset::make_loft(&mut model, &base, &apex, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let pi = core::f64::consts::PI;
let expected = pi * 4.0 * 5.0 / 3.0;
let measured = ogeom_algo::volume_properties(
&model,
&result.shape,
ogeom_mesh::Deflection::with_chord(1e-4).unwrap(),
T,
)
.unwrap()
.mass;
assert!(
(measured - expected).abs() < 1e-3,
"cone volume {measured} against {expected}"
);
let mut second = ogeom_topo::Model::new();
let circle = Circle::new(Frame::WORLD, 2.0, T).unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
let domain = curve.domain();
let ring = ogeom_algo::make_edge(&mut second, curve, domain, T)
.unwrap()
.shape;
let base = ogeom_algo::make_wire(&mut second, std::slice::from_ref(&ring), T)
.unwrap()
.shape;
let leaning = ogeom_algo::make_vertex(&mut second, Point::new(1.0, 0.0, 5.0)).shape;
assert!(ogeom_offset::make_loft(&mut second, &base, &leaning, T).is_err());
}
#[test]
fn an_alignment_hint_untwists_a_loft() {
let build = |model: &mut ogeom_topo::Model, rotate: usize, z: f64| -> ogeom_topo::Shape {
let corners = [
Point::new(0.0, 0.0, z),
Point::new(2.0, 0.0, z),
Point::new(2.0, 2.0, z),
Point::new(0.0, 2.0, z),
];
let rotated: Vec<Point> = (0..4).map(|i| corners[(i + rotate) % 4]).collect();
ogeom_algo::make_polygon(model, &rotated, true, T)
.unwrap()
.shape
};
let mut model = ogeom_topo::Model::new();
let bottom = build(&mut model, 0, 0.0);
let top = build(&mut model, 1, 3.0);
let hints = [Point::new(0.0, 0.0, 0.0), Point::new(0.0, 0.0, 3.0)];
let aligned = ogeom_offset::make_loft_skinned_aligned(
&mut model,
&[bottom.clone(), top.clone()],
&hints,
5e-2,
T,
)
.unwrap();
let volume_of = |model: &ogeom_topo::Model, shape: &ogeom_topo::Shape| {
ogeom_algo::volume_properties(
model,
shape,
ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
T,
)
.unwrap()
.mass
};
let straight = volume_of(&model, &aligned.shape);
assert!(
(straight - 12.0).abs() / 12.0 < 0.05,
"aligned loft volume {straight} against 12"
);
let twisted = ogeom_offset::make_loft_skinned(&mut model, &[bottom, top], 5e-2, T).unwrap();
let sheared = volume_of(&model, &twisted.shape);
assert!(
sheared < straight * 0.95,
"the twist should cost volume: {sheared} vs {straight}"
);
}
#[test]
fn a_ruled_loft_between_tilted_polygons_still_builds() {
let mut model = ogeom_topo::Model::new();
let bottom = ogeom_algo::make_polygon(
&mut model,
&[
Point::new(0.0, 0.0, 0.0),
Point::new(2.0, 0.0, 0.0),
Point::new(2.0, 2.0, 0.0),
Point::new(0.0, 2.0, 0.0),
],
true,
T,
)
.unwrap()
.shape;
let angle = 25.0_f64.to_radians();
let turn = |y: f64, z: f64| -> (f64, f64) {
let (dy, dz) = (y - 1.0, z - 3.0);
(
angle.cos().mul_add(dy, -(angle.sin() * dz)) + 1.0,
angle.sin().mul_add(dy, angle.cos() * dz) + 3.0,
)
};
let corners: Vec<Point> = [(0.0_f64, 0.0_f64), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)]
.iter()
.map(|(x, y)| {
let (ty, tz) = turn(*y, 3.0);
Point::new(*x, ty, tz)
})
.collect();
let top = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let result = ogeom_offset::make_loft(&mut model, &bottom, &top, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let measured =
ogeom_algo::volume_properties(&model, &result.shape, ogeom_mesh::Deflection::default(), T)
.unwrap()
.mass;
assert!(
measured > 1.0,
"the tilted loft encloses volume: {measured}"
);
}
#[test]
fn a_pipe_shell_round_a_closed_circle_matches_the_torus() {
let mut model = ogeom_topo::Model::new();
let ring_r = 10.0;
let circle = Circle::new(Frame::WORLD, ring_r, T).unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
let domain = curve.domain();
let spine = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let start = Point::new(ring_r, 0.0, 0.0);
let normal = ogeom_math::Direction::new(ogeom_math::Vector::Y, T).unwrap();
let section = Circle::new(
Frame::new(start, normal, ogeom_math::Direction::Z, T).unwrap(),
1.0,
T,
)
.unwrap();
let scurve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(section).into();
let sdomain = scurve.domain();
let sedge = ogeom_algo::make_edge(&mut model, scurve, sdomain, T)
.unwrap()
.shape;
let profile = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&sedge), T)
.unwrap()
.shape;
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 5e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let expected = 2.0 * core::f64::consts::PI * core::f64::consts::PI * ring_r;
let measured = ogeom_algo::volume_properties(
&model,
&result.shape,
ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
T,
)
.unwrap()
.mass;
assert!(
(measured - expected).abs() / expected < 0.01,
"closed pipe shell volume {measured} against {expected}"
);
assert!(!result.history.generated(&spine).is_empty());
}
#[test]
fn a_round_profile_along_a_closed_square_spine_is_a_ring() {
let mut model = ogeom_topo::Model::new();
let (half, r) = (8.0, 2.0);
let flat = half - r;
let mut edges: Vec<ogeom_topo::Shape> = Vec::new();
let mut vertices: Vec<(ogeom_topo::Shape, Point)> = Vec::new();
let corner_centres = [
Point::new(flat, flat, 0.0),
Point::new(-flat, flat, 0.0),
Point::new(-flat, -flat, 0.0),
Point::new(flat, -flat, 0.0),
];
for (i, _) in corner_centres.iter().enumerate() {
let angle = core::f64::consts::FRAC_PI_2 * f64::from(u8::try_from(i).unwrap());
let (c, s_) = (angle.cos(), angle.sin());
let out = ogeom_math::Vector::new(c, s_, 0.0);
let along = ogeom_math::Vector::new(-s_, c, 0.0);
let from = Point::new(0.0, 0.0, 0.0) + out * half - along * flat;
let to = Point::new(0.0, 0.0, 0.0) + out * half + along * flat;
vertices.push((ogeom_algo::make_vertex(&mut model, from).shape, from));
vertices.push((ogeom_algo::make_vertex(&mut model, to).shape, to));
let _ = &corner_centres[i];
}
for i in 0..4 {
let (vf, pf) = vertices[2 * i].clone();
let (vt, pt) = vertices[2 * i + 1].clone();
let line = ogeom_geom::LineCurve::segment(pf, pt, T).unwrap();
let curve: ogeom_geom::Curve = line.into();
let domain = curve.domain();
edges.push(
ogeom_algo::make_edge_between(&mut model, curve, domain, &vf, &vt, T)
.unwrap()
.shape,
);
let (vn, _) = vertices[(2 * i + 2) % 8].clone();
let centre = corner_centres[i];
let frame = Frame::new(
centre,
ogeom_math::Direction::Z,
ogeom_math::Direction::new(pt - centre, T).unwrap(),
T,
)
.unwrap();
let circle = Circle::new(frame, r, T).unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(circle).into();
edges.push(
ogeom_algo::make_edge_between(
&mut model,
curve,
(0.0, core::f64::consts::FRAC_PI_2),
&vt,
&vn,
T,
)
.unwrap()
.shape,
);
}
let spine = ogeom_algo::make_wire(&mut model, &edges, T).unwrap().shape;
let start = vertices[0].1;
let tangent = vertices[1].1 - vertices[0].1;
let normal = ogeom_math::Direction::new(tangent, T).unwrap();
let section = Circle::new(
Frame::new(start, normal, ogeom_math::Direction::Z, T).unwrap(),
1.0,
T,
)
.unwrap();
let scurve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(section).into();
let sdomain = scurve.domain();
let sedge = ogeom_algo::make_edge(&mut model, scurve, sdomain, T)
.unwrap()
.shape;
let profile = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&sedge), T)
.unwrap()
.shape;
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 5e-2, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
assert_eq!(
explore(&model, &result.shape, Filter::OfType(ShapeType::Face))
.unwrap()
.len(),
1
);
let perimeter = (2.0 * flat).mul_add(4.0, core::f64::consts::TAU * r);
let expected = core::f64::consts::PI * perimeter;
let measured = ogeom_algo::volume_properties(
&model,
&result.shape,
ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
T,
)
.unwrap()
.mass;
assert!(
(measured - expected).abs() / expected < 0.02,
"square ring volume {measured} against {expected}"
);
}
#[test]
fn a_sharp_cornered_closed_spine_mitres_a_smooth_profile() {
let mut model = ogeom_topo::Model::new();
let corners = [
Point::new(8.0, -8.0, 0.0),
Point::new(8.0, 8.0, 0.0),
Point::new(-8.0, 8.0, 0.0),
Point::new(-8.0, -8.0, 0.0),
];
let spine = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let start = corners[0];
let normal = ogeom_math::Direction::new(corners[1] - corners[0], T).unwrap();
let section = Circle::new(
Frame::new(start, normal, ogeom_math::Direction::Z, T).unwrap(),
1.0,
T,
)
.unwrap();
let scurve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(section).into();
let sdomain = scurve.domain();
let sedge = ogeom_algo::make_edge(&mut model, scurve, sdomain, T)
.unwrap()
.shape;
let profile = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&sedge), T)
.unwrap()
.shape;
let ring = ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 5e-2, T).unwrap();
let measured = volume(&model, &ring.shape);
let expected = core::f64::consts::PI * 64.0;
assert!(
(measured - expected).abs() < expected * 0.02,
"mitred round ring volume {measured} against {expected}"
);
}
#[test]
fn an_l_spine_mitres_its_corner_and_the_runs_share_the_ring() {
let mut model = ogeom_topo::Model::new();
let a = Point::new(0.0, 0.0, 0.0);
let b = Point::new(20.0, 0.0, 0.0);
let c = Point::new(20.0, 20.0, 0.0);
let va = ogeom_algo::make_vertex(&mut model, a).shape;
let vb = ogeom_algo::make_vertex(&mut model, b).shape;
let vc = ogeom_algo::make_vertex(&mut model, c).shape;
let seg = |model: &mut ogeom_topo::Model,
f: (&ogeom_topo::Shape, Point),
t: (&ogeom_topo::Shape, Point)|
-> ogeom_topo::Shape {
let line = ogeom_geom::LineCurve::segment(f.1, t.1, T).unwrap();
let curve = ogeom_geom::Curve::Line(line);
let d = ogeom_geom::Curve3d::domain(&curve);
ogeom_algo::make_edge_between(model, curve, d, f.0, t.0, T)
.unwrap()
.shape
};
let e1 = seg(&mut model, (&va, a), (&vb, b));
let e2 = seg(&mut model, (&vb, b), (&vc, c));
let spine = ogeom_algo::make_wire(&mut model, &[e1, e2], T)
.unwrap()
.shape;
let profile = square_profile(&mut model, a, ogeom_math::Vector::X, 4.0);
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let measured = volume(&model, &result.shape);
assert!(
(measured - 640.0).abs() < 640.0 * 1e-3,
"mitred elbow volume {measured} against 640"
);
}
#[test]
fn a_corner_against_a_curved_leg_meets_it_on_its_generators() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let arc_curve: ogeom_geom::Curve =
ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(Frame::WORLD, r, T).unwrap()).into();
let a = Point::new(r, 0.0, 0.0);
let b = Point::new(0.0, r, 0.0);
let c = Point::new(0.0, r + 20.0, 0.0);
let va = ogeom_algo::make_vertex(&mut model, a).shape;
let vb = ogeom_algo::make_vertex(&mut model, b).shape;
let vc = ogeom_algo::make_vertex(&mut model, c).shape;
let arc = ogeom_algo::make_edge_between(
&mut model,
arc_curve,
(0.0, core::f64::consts::FRAC_PI_2),
&va,
&vb,
T,
)
.unwrap()
.shape;
let line = ogeom_geom::LineCurve::segment(b, c, T).unwrap();
let lcurve = ogeom_geom::Curve::Line(line);
let ldomain = ogeom_geom::Curve3d::domain(&lcurve);
let leg = ogeom_algo::make_edge_between(&mut model, lcurve, ldomain, &vb, &vc, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, &[arc, leg], T)
.unwrap()
.shape;
let profile = square_profile(&mut model, a, ogeom_math::Vector::Y, 4.0);
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let (ro, ri, half) = (r + 2.0, r - 2.0, 2.0);
let annulus = core::f64::consts::FRAC_PI_4 * (ro * ro - ri * ri);
let leg = 2.0 * half * 20.0;
let overlap =
half * (ro * ro - half * half).sqrt() / 2.0 + ro * ro / 2.0 * (half / ro).asin() - r * half;
let fill = half * half;
let expected = (annulus + leg - overlap + fill) * 2.0 * half;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < expected * 5e-3,
"curved-leg corner volume {measured} against {expected}"
);
}
#[test]
fn a_skinned_loft_to_an_offset_point_measures_as_frustum_plus_cone() {
let mut model = ogeom_topo::Model::new();
let ring = |model: &mut ogeom_topo::Model, r: f64, z: f64| {
let frame = Frame::new(
Point::new(0.0, 0.0, z),
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, r, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let edge = ogeom_algo::make_edge(model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let s0 = ring(&mut model, 10.0, 0.0);
let s1 = ring(&mut model, 6.0, 6.0);
let apex = ogeom_algo::make_vertex(&mut model, Point::new(2.0, 1.0, 15.0)).shape;
let built = ogeom_offset::make_loft_skinned(&mut model, &[s0, s1, apex], 1e-2, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the apex loft is a valid solid"
);
let frustum = core::f64::consts::PI * 6.0 / 3.0 * (100.0 + 60.0 + 36.0);
let cone = core::f64::consts::PI * 36.0 * 9.0 / 3.0;
let measured = volume(&model, &built.shape);
let reference = frustum + cone;
assert!(
(measured - reference).abs() / reference < 0.01,
"apex loft volume {measured} against {reference}"
);
}
#[test]
fn a_non_planar_middle_section_lofts() {
let mut model = ogeom_topo::Model::new();
let flat = |model: &mut ogeom_topo::Model, r: f64, z: f64| {
let frame = Frame::new(
Point::new(0.0, 0.0, z),
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, r, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let edge = ogeom_algo::make_edge(model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let wavy = {
let n = 64_i32;
let pts: Vec<Point> = (0..=n)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let a = core::f64::consts::TAU * f64::from(i % n) / f64::from(n);
Point::new(8.0 * a.cos(), 8.0 * a.sin(), 5.0 + 0.5 * (3.0 * a).sin())
})
.collect();
let fitted = ogeom_geom::fit::fit_points_closed(&pts, 3, 1e-3, T).unwrap();
let curve = ogeom_geom::Curve::BSpline(fitted.curve);
let domain = curve.domain();
let edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(&mut model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let s0 = flat(&mut model, 10.0, 0.0);
let s2 = flat(&mut model, 9.0, 10.0);
let built = ogeom_offset::make_loft_skinned(&mut model, &[s0, wavy, s2], 5e-2, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the wavy loft is a valid solid"
);
let v = volume(&model, &built.shape);
let lo = core::f64::consts::PI * 64.0 * 10.0;
let hi = core::f64::consts::PI * 100.0 * 10.0;
assert!(
lo < v && v < hi,
"wavy loft volume {v} outside ({lo}, {hi})"
);
}
#[test]
fn a_non_planar_end_section_is_capped_by_a_skinned_patch() {
let mut model = ogeom_topo::Model::new();
let s0 = {
let frame = Frame::new(
Point::ORIGIN,
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, 10.0, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(&mut model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let wavy = {
let n = 64_i32;
let pts: Vec<Point> = (0..=n)
.map(|i| {
let a = core::f64::consts::TAU * f64::from(i % n) / f64::from(n);
Point::new(8.0 * a.cos(), 8.0 * a.sin(), 5.0 + 0.5 * (3.0 * a).sin())
})
.collect();
let fitted = ogeom_geom::fit::fit_points_closed(&pts, 3, 1e-3, T).unwrap();
let curve = ogeom_geom::Curve::BSpline(fitted.curve);
let domain = curve.domain();
let edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(&mut model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
};
let built = ogeom_offset::make_loft_skinned(&mut model, &[s0, wavy], 5e-2, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &built.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let faces =
ogeom_topo::explore_unique(&model, &built.shape, ogeom_topo::ShapeType::Face).unwrap();
assert_eq!(faces.len(), 3, "a wall, a plane cap and a skinned cap");
let mesh = ogeom_mesh::triangulate(&model, &built.shape, fine(), T).unwrap();
assert!(mesh.is_closed(), "the skinned cap closes the solid");
let v = volume(&model, &built.shape);
let frustum = core::f64::consts::PI * 5.0 / 3.0 * (100.0 + 80.0 + 64.0);
assert!(
(v - frustum).abs() < frustum * 0.02,
"loft with a wavy rim measures {v} against the frustum's {frustum}"
);
}
#[test]
fn a_square_profile_along_a_closed_circle_is_a_pappus_ring() {
let mut model = ogeom_topo::Model::new();
let frame = Frame::new(
Point::new(0.0, 0.0, 0.0),
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, 20.0, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let spine_edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&spine_edge), T)
.unwrap()
.shape;
let corners = [
Point::new(18.0, 0.0, -2.0),
Point::new(22.0, 0.0, -2.0),
Point::new(22.0, 0.0, 2.0),
Point::new(18.0, 0.0, 2.0),
];
let profile = ogeom_algo::make_polygon(&mut model, &corners, true, T)
.unwrap()
.shape;
let built =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 5e-3, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the square ring is a valid solid"
);
let expected = 16.0 * core::f64::consts::TAU * 20.0;
let measured = volume(&model, &built.shape);
assert!(
(measured - expected).abs() / expected < 5e-3,
"square ring volume {measured} against Pappus {expected}"
);
}
#[test]
fn a_holed_profile_round_a_closed_spine_carries_its_tunnel() {
let mut model = ogeom_topo::Model::new();
let frame = Frame::new(
Point::new(0.0, 0.0, 0.0),
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let circle = Circle::new(frame, 20.0, T).unwrap();
let curve = ogeom_geom::Curve::Circle(ogeom_geom::CircleCurve::new(circle));
let domain = curve.domain();
let spine_edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&spine_edge), T)
.unwrap()
.shape;
let outer = ogeom_algo::make_polygon(
&mut model,
&[
Point::new(17.0, 0.0, -3.0),
Point::new(23.0, 0.0, -3.0),
Point::new(23.0, 0.0, 3.0),
Point::new(17.0, 0.0, 3.0),
],
true,
T,
)
.unwrap()
.shape;
let hole = ogeom_algo::make_polygon(
&mut model,
&[
Point::new(19.0, 0.0, -1.0),
Point::new(21.0, 0.0, -1.0),
Point::new(21.0, 0.0, 1.0),
Point::new(19.0, 0.0, 1.0),
],
true,
T,
)
.unwrap()
.shape;
let plane = ogeom_math::Plane::through(Point::new(20.0, 0.0, 0.0), ogeom_math::Direction::Y);
let surface: ogeom_geom::SurfaceGeometry =
ogeom_geom::PlaneSurface::over(plane, (-10.0, 10.0), (-10.0, 10.0))
.unwrap()
.into();
let profile = ogeom_algo::make_face(&mut model, surface, &[outer, hole], T)
.unwrap()
.shape;
let built =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 5e-3, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the holed ring is a valid solid"
);
let expected = (36.0 - 4.0) * core::f64::consts::TAU * 20.0;
let measured = volume(&model, &built.shape);
assert!(
(measured - expected).abs() / expected < 5e-3,
"holed ring volume {measured} against Pappus {expected}"
);
}
#[test]
fn a_faceted_ring_round_a_wavy_spine_closes_and_measures() {
let mut model = ogeom_topo::Model::new();
let n = 96_i32;
let pts: Vec<Point> = (0..=n)
.map(|i| {
let a = core::f64::consts::TAU * f64::from(i % n) / f64::from(n);
Point::new(
20.0 * a.cos(),
20.0 * a.sin(),
1.5 * (1.0 - (3.0 * a).cos()),
)
})
.collect();
let fitted = ogeom_geom::fit::fit_points_closed(&pts, 3, 1e-4, T).unwrap();
let curve = ogeom_geom::Curve::BSpline(fitted.curve);
let domain = curve.domain();
let arc = {
let mut total = 0.0;
let mut last = curve.point_at(domain.0, T).unwrap();
for k in 1..=2048 {
let t = domain.0 + (domain.1 - domain.0) * f64::from(k) / 2048.0;
let p = curve.point_at(t, T).unwrap();
total += last.distance(p);
last = p;
}
total
};
let (start, tangent) = {
let p = curve.point_at(domain.0, T).unwrap();
let d = curve.d1_at(domain.0, T).unwrap();
(p, ogeom_math::Direction::new(d, T).unwrap())
};
let spine_edge = ogeom_algo::make_edge(&mut model, curve, domain, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, std::slice::from_ref(&spine_edge), T)
.unwrap()
.shape;
let (ex, ey) = {
let plane = ogeom_math::Plane::through(start, tangent);
let f = plane.frame();
(f.x().vector(), f.y().vector())
};
let corner = |a: f64, b: f64| start + ex * a + ey * b;
let profile = ogeom_algo::make_polygon(
&mut model,
&[corner(-1.5, -1.0), corner(1.5, -1.0), corner(0.0, 1.6)],
true,
T,
)
.unwrap()
.shape;
let area = 0.5 * 3.0 * 2.6;
let expected = area * arc;
for frenet in [false, true] {
let built =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, frenet, 5e-3, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the wavy ring is a valid solid (frenet {frenet})"
);
let measured = volume(&model, &built.shape);
assert!(
(measured - expected).abs() / expected < 0.02,
"wavy ring volume {measured} against A*L {expected} (frenet {frenet})"
);
}
}
#[test]
fn a_mitred_square_ring_measures_pappus_exactly() {
let mut model = ogeom_topo::Model::new();
let spine = ogeom_algo::make_polygon(
&mut model,
&[
ogeom_math::Point::new(0.0, 0.0, 0.0),
ogeom_math::Point::new(30.0, 0.0, 0.0),
ogeom_math::Point::new(30.0, 30.0, 0.0),
ogeom_math::Point::new(0.0, 30.0, 0.0),
],
true,
T,
)
.unwrap()
.shape;
let profile = square_profile(
&mut model,
ogeom_math::Point::new(0.0, 0.0, 0.0),
ogeom_math::Vector::new(1.0, 0.0, 0.0),
4.0,
);
let ring = ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-4, T).unwrap();
let measured = volume(&model, &ring.shape);
let exact = 16.0 * 120.0;
assert!(
(measured - exact).abs() < exact * 1e-3,
"mitred ring volume {measured} against {exact}"
);
}
#[test]
fn a_cornered_ring_seamed_mid_leg_measures_pappus() {
let mut model = ogeom_topo::Model::new();
let spine = ogeom_algo::make_polygon(
&mut model,
&[
ogeom_math::Point::new(15.0, 0.0, 0.0),
ogeom_math::Point::new(30.0, 0.0, 0.0),
ogeom_math::Point::new(30.0, 30.0, 0.0),
ogeom_math::Point::new(0.0, 30.0, 0.0),
ogeom_math::Point::new(0.0, 0.0, 0.0),
],
true,
T,
)
.unwrap()
.shape;
let profile = square_profile(
&mut model,
ogeom_math::Point::new(15.0, 0.0, 0.0),
ogeom_math::Vector::new(1.0, 0.0, 0.0),
4.0,
);
let built =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-4, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the mid-leg-seamed ring is a valid solid"
);
let measured = volume(&model, &built.shape);
assert!(
(measured - 1920.0).abs() < 1e-6,
"mid-leg seam: {measured} against Pappus' 1920"
);
assert_eq!(
ogeom_topo::explore_unique(&model, &built.shape, ogeom_topo::ShapeType::Face)
.unwrap()
.len(),
20,
"four legs of four walls, the seamed leg in two pieces"
);
}
#[test]
fn a_skew_cornered_ring_closes_on_its_mitres() {
let mut model = ogeom_topo::Model::new();
let pts = [
ogeom_math::Point::new(0.0, 0.0, 0.0),
ogeom_math::Point::new(20.0, 0.0, 4.0),
ogeom_math::Point::new(30.0, 15.0, 0.0),
ogeom_math::Point::new(20.0, 30.0, 4.0),
ogeom_math::Point::new(0.0, 30.0, 0.0),
ogeom_math::Point::new(-10.0, 15.0, 4.0),
];
let spine = ogeom_algo::make_polygon(&mut model, &pts, true, T)
.unwrap()
.shape;
let profile = square_profile(&mut model, pts[0], pts[1] - pts[0], 4.0);
let built =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
assert!(
ogeom_algo::check(&model, &built.shape, T)
.unwrap()
.is_valid(),
"the skew ring is a valid solid"
);
let perimeter: f64 = (0..pts.len())
.map(|i| pts[i].distance(pts[(i + 1) % pts.len()]))
.sum();
let expected = 16.0 * perimeter;
let measured = volume(&model, &built.shape);
assert!(
(measured - expected).abs() / expected < 0.03,
"skew ring volume {measured} against A*L {expected}"
);
}
#[test]
fn a_holed_profile_rounds_a_mitred_ring() {
use ogeom_math::{Direction, Point, Vector};
let mut model = ogeom_topo::Model::new();
let spine = ogeom_algo::make_polygon(
&mut model,
&[
Point::new(0.0, 0.0, 0.0),
Point::new(30.0, 0.0, 0.0),
Point::new(30.0, 30.0, 0.0),
Point::new(0.0, 30.0, 0.0),
],
true,
T,
)
.unwrap()
.shape;
let plane = ogeom_math::Plane::through(Point::ORIGIN, Direction::new(Vector::X, T).unwrap());
let frame = plane.frame();
let ring_at = |model: &mut ogeom_topo::Model, half: f64| {
let corners: Vec<Point> = [(-half, -half), (half, -half), (half, half), (-half, half)]
.iter()
.map(|(a, b)| Point::ORIGIN + frame.x().vector() * *a + frame.y().vector() * *b)
.collect();
ogeom_algo::make_polygon(model, &corners, true, T)
.unwrap()
.shape
};
let outer = ring_at(&mut model, 3.0);
let hole = ring_at(&mut model, 1.0);
let face = ogeom_algo::make_face(
&mut model,
ogeom_geom::PlaneSurface::over(plane, (-12.0, 12.0), (-12.0, 12.0))
.unwrap()
.into(),
&[outer, hole],
T,
)
.unwrap()
.shape;
let ring = ogeom_offset::make_pipe_shell(&mut model, &face, &spine, false, 1e-4, T).unwrap();
let measured = volume(&model, &ring.shape);
let exact = 32.0 * 120.0;
assert!(
(measured - exact).abs() < exact * 1e-3,
"holed mitred ring volume {measured} against {exact}"
);
}
fn circle_profile(
model: &mut ogeom_topo::Model,
centre: Point,
tangent: ogeom_math::Vector,
radius: f64,
) -> ogeom_topo::Shape {
let normal = ogeom_math::Direction::new(tangent, T).unwrap();
let section = Circle::new(
Frame::new(centre, normal, ogeom_math::Direction::Z, T).unwrap(),
radius,
T,
)
.unwrap();
let curve: ogeom_geom::Curve = ogeom_geom::CircleCurve::new(section).into();
let domain = curve.domain();
let edge = ogeom_algo::make_edge(model, curve, domain, T)
.unwrap()
.shape;
ogeom_algo::make_wire(model, std::slice::from_ref(&edge), T)
.unwrap()
.shape
}
fn arc_between(
model: &mut ogeom_topo::Model,
centre: Point,
r: f64,
(t0, t1): (f64, f64),
va: &ogeom_topo::Shape,
vb: &ogeom_topo::Shape,
) -> ogeom_topo::Shape {
let frame = Frame::new(
centre,
ogeom_math::Direction::Z,
ogeom_math::Direction::X,
T,
)
.unwrap();
let curve: ogeom_geom::Curve =
ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(frame, r, T).unwrap()).into();
ogeom_algo::make_edge_between(model, curve, (t0, t1), va, vb, T)
.unwrap()
.shape
}
fn simpson(a: f64, b: f64, n: usize, f: impl Fn(f64) -> f64) -> f64 {
#[allow(clippy::cast_precision_loss)]
let h = (b - a) / (n as f64);
let mut sum = f(a) + f(b);
for i in 1..n {
#[allow(clippy::cast_precision_loss)]
let x = a + h * (i as f64);
sum += f(x) * if i % 2 == 1 { 4.0 } else { 2.0 };
}
sum * h / 3.0
}
fn arc_then_leg_slice(r: f64, w: f64, leg: f64) -> f64 {
let ro = r + w;
let annulus = core::f64::consts::FRAC_PI_4 * (ro * ro - (r - w) * (r - w));
let overlap = w * (ro * ro - w * w).sqrt() / 2.0 + ro * ro / 2.0 * (w / ro).asin() - r * w;
annulus + 2.0 * w * leg - overlap + w * w
}
#[test]
fn a_round_profile_meets_a_curved_leg_corner_on_its_generators() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let a = Point::new(r, 0.0, 0.0);
let b = Point::new(0.0, r, 0.0);
let c = Point::new(0.0, r + 20.0, 0.0);
let va = ogeom_algo::make_vertex(&mut model, a).shape;
let vb = ogeom_algo::make_vertex(&mut model, b).shape;
let vc = ogeom_algo::make_vertex(&mut model, c).shape;
let arc = arc_between(
&mut model,
Point::ORIGIN,
r,
(0.0, core::f64::consts::FRAC_PI_2),
&va,
&vb,
);
let lcurve = ogeom_geom::Curve::Line(ogeom_geom::LineCurve::segment(b, c, T).unwrap());
let ldomain = ogeom_geom::Curve3d::domain(&lcurve);
let leg = ogeom_algo::make_edge_between(&mut model, lcurve, ldomain, &vb, &vc, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, &[arc, leg], T)
.unwrap()
.shape;
let profile = circle_profile(&mut model, a, ogeom_math::Vector::Y, 2.0);
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let expected = simpson(-2.0, 2.0, 4000, |z| {
let w = (4.0 - z * z).max(0.0).sqrt();
arc_then_leg_slice(r, w, 20.0)
});
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < expected * 1e-2,
"round curved-leg corner volume {measured} against {expected}"
);
}
#[test]
fn two_curved_legs_meet_at_a_corner_on_their_generators() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let a = Point::new(r, 0.0, 0.0);
let b = Point::new(0.0, r, 0.0);
let d = Point::new(r, 2.0 * r, 0.0);
let centre2 = Point::new(r, r, 0.0);
let va = ogeom_algo::make_vertex(&mut model, a).shape;
let vb = ogeom_algo::make_vertex(&mut model, b).shape;
let vd = ogeom_algo::make_vertex(&mut model, d).shape;
let arc1 = arc_between(
&mut model,
Point::ORIGIN,
r,
(0.0, core::f64::consts::FRAC_PI_2),
&va,
&vb,
);
let arc2 = arc_between(
&mut model,
centre2,
r,
(core::f64::consts::FRAC_PI_2, core::f64::consts::PI),
&vd,
&vb,
);
let spine = ogeom_algo::make_wire(&mut model, &[arc1, arc2.reversed()], T)
.unwrap()
.shape;
let profile = square_profile(&mut model, a, ogeom_math::Vector::Y, 4.0);
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let w = 2.0;
let width = |y: f64| -> f64 {
let mut spans: Vec<(f64, f64)> = Vec::new();
let (ro, ri) = (r + w, r - w);
if (0.0..=ro).contains(&y) {
let lo = (ri * ri - y * y).max(0.0).sqrt();
let hi = (ro * ro - y * y).sqrt();
spans.push((lo, hi));
}
let e = y - r;
if (0.0..=ro).contains(&e) {
let lo = r - (ro * ro - e * e).sqrt();
let hi = r - (ri * ri - e * e).max(0.0).sqrt();
spans.push((lo, hi));
}
if (r - w..=r).contains(&y) {
spans.push((-w, 0.0));
}
spans.sort_by(|p, q| p.0.partial_cmp(&q.0).unwrap());
let mut total = 0.0;
let mut reach = f64::NEG_INFINITY;
for (lo, hi) in spans {
let lo = lo.max(reach);
if hi > lo {
total += hi - lo;
reach = hi;
}
}
total
};
let area = simpson(0.0, 2.0 * r + w, 40000, width);
let expected = area * 2.0 * w;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < expected * 5e-3,
"arc-to-arc corner volume {measured} against {expected}"
);
}
#[test]
fn a_d_shaped_ring_corners_its_curved_leg_at_both_ends() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let a = Point::new(r, 0.0, 0.0);
let b = Point::new(-r, 0.0, 0.0);
let va = ogeom_algo::make_vertex(&mut model, a).shape;
let vb = ogeom_algo::make_vertex(&mut model, b).shape;
let arc = arc_between(
&mut model,
Point::ORIGIN,
r,
(0.0, core::f64::consts::PI),
&va,
&vb,
);
let lcurve = ogeom_geom::Curve::Line(ogeom_geom::LineCurve::segment(b, a, T).unwrap());
let ldomain = ogeom_geom::Curve3d::domain(&lcurve);
let leg = ogeom_algo::make_edge_between(&mut model, lcurve, ldomain, &vb, &va, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, &[arc, leg], T)
.unwrap()
.shape;
let profile = square_profile(&mut model, a, ogeom_math::Vector::Y, 4.0);
let result =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap();
let diagnosis = ogeom_algo::check(&model, &result.shape, T).unwrap();
assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
let w = 2.0;
let (ro, ri) = (r + w, r - w);
let annulus = core::f64::consts::FRAC_PI_2 * (ro * ro - ri * ri);
let leg_area = 2.0 * w * 2.0 * r;
let overlap = r * w - (w * (ri * ri - w * w).sqrt() / 2.0 + ri * ri / 2.0 * (w / ri).asin());
let expected = (annulus + leg_area - 2.0 * overlap + 2.0 * w * w) * 2.0 * w;
let measured = volume(&model, &result.shape);
assert!(
(measured - expected).abs() < expected * 5e-3,
"D ring volume {measured} against {expected}"
);
}
#[test]
fn a_skew_corner_against_a_curved_leg_is_refused_by_name() {
let mut model = ogeom_topo::Model::new();
let r = 20.0;
let a = Point::new(r, 0.0, 0.0);
let b = Point::new(0.0, r, 0.0);
let c = Point::new(0.0, r, 20.0);
let va = ogeom_algo::make_vertex(&mut model, a).shape;
let vb = ogeom_algo::make_vertex(&mut model, b).shape;
let vc = ogeom_algo::make_vertex(&mut model, c).shape;
let arc = arc_between(
&mut model,
Point::ORIGIN,
r,
(0.0, core::f64::consts::FRAC_PI_2),
&va,
&vb,
);
let lcurve = ogeom_geom::Curve::Line(ogeom_geom::LineCurve::segment(b, c, T).unwrap());
let ldomain = ogeom_geom::Curve3d::domain(&lcurve);
let leg = ogeom_algo::make_edge_between(&mut model, lcurve, ldomain, &vb, &vc, T)
.unwrap()
.shape;
let spine = ogeom_algo::make_wire(&mut model, &[arc, leg], T)
.unwrap()
.shape;
let profile = square_profile(&mut model, a, ogeom_math::Vector::Y, 4.0);
let err =
ogeom_offset::make_pipe_shell(&mut model, &profile, &spine, false, 1e-3, T).unwrap_err();
assert!(
err.to_string().contains("skew corner against a curved leg"),
"{err}"
);
}