use axiolid_construct::extrude::extrude_profile_exact;
use axiolid_core::{Tolerance, Vec3};
use axiolid_measure::{exact_properties, ExactMeasureError};
use axiolid_profile::{Profile, RectangleProfile};
fn tol() -> Tolerance {
Tolerance::new(1e-6, 1e-9).expect("tolerance")
}
fn rectangle(x: f64, y: f64) -> Profile {
Profile::Rectangle(RectangleProfile {
x,
y,
thickness: None,
outer_radius: None,
inner_radius: None,
})
}
#[test]
fn an_exact_prism_matches_its_closed_form() {
let (x, y, depth) = (3.0, 5.0, 7.0);
let brep = extrude_profile_exact(&rectangle(x, y), Vec3::Z, depth, tol())
.expect("a rectangle extrudes exactly");
let props = exact_properties(&brep, tol()).expect("a closed prism is measurable");
let expected_volume = x * y * depth;
assert!(
(props.signed_volume - expected_volume).abs() < 1e-12 * expected_volume,
"expected signed volume {expected_volume}, got {}",
props.signed_volume
);
let expected_area = 2.0 * (x * y) + 2.0 * depth * (x + y);
assert!(
(props.area - expected_area).abs() < 1e-12 * expected_area,
"expected area {expected_area}, got {}",
props.area
);
}
#[test]
fn the_centroid_is_where_symmetry_demands() {
let depth = 4.0;
let brep = extrude_profile_exact(&rectangle(2.0, 2.0), Vec3::Z, depth, tol())
.expect("a rectangle extrudes exactly");
let props = exact_properties(&brep, tol()).expect("measurable");
assert!(
props.centroid.x.abs() < 1e-12 && props.centroid.y.abs() < 1e-12,
"a profile centred on the origin has a centroid on the axis: {:?}",
props.centroid
);
assert!(
(props.centroid.z - depth / 2.0).abs() < 1e-12,
"expected mid-height {}, got {}",
depth / 2.0,
props.centroid.z
);
}
#[test]
fn a_curved_face_is_integrated_not_refused() {
use axiolid_profile::CircleProfile;
let circle = Profile::Circle(CircleProfile {
radius: 1.0,
thickness: None,
});
let brep =
extrude_profile_exact(&circle, Vec3::Z, 2.0, tol()).expect("a circle extrudes exactly");
let props = exact_properties(&brep, tol()).expect("a cylinder is measurable");
let expected = core::f64::consts::PI * 2.0;
assert!(
(props.signed_volume - expected).abs() < 1e-12 * expected,
"expected {expected}, got {}",
props.signed_volume
);
}
#[test]
fn a_refusal_names_the_approximate_path() {
let text =
ExactMeasureError::NonPlanarFace("a curved face short of its error bound").to_string();
assert!(
text.contains("MeshMeasure"),
"refusal should point at the approximate path, got: {text}"
);
}
#[test]
fn the_exact_and_mesh_paths_agree() {
use axiolid_construct::extrude::extrude_profile;
use axiolid_construct::profile::profile_rings;
use axiolid_measure::{Measure, MeshMeasure};
let (x, y, depth) = (3.0, 5.0, 7.0);
let profile = rectangle(x, y);
let brep = extrude_profile_exact(&profile, Vec3::Z, depth, tol()).expect("exact extrusion");
let exact = exact_properties(&brep, tol()).expect("measurable");
let rings = profile_rings(&profile, 1e-5, tol()).expect("profile rings");
let mesh = extrude_profile(&rings, Vec3::Z, depth, tol()).expect("mesh extrusion");
let meshed = MeshMeasure.measure(&mesh, tol()).expect("measurable");
assert!(
(exact.signed_volume - meshed.signed_volume).abs() < 1e-9,
"exact {} vs mesh {}",
exact.signed_volume,
meshed.signed_volume
);
assert!(
(exact.area - meshed.area).abs() < 1e-9,
"exact area {} vs mesh area {}",
exact.area,
meshed.area
);
assert!(
(exact.centroid - meshed.centroid).length() < 1e-9,
"exact centroid {:?} vs mesh {:?}",
exact.centroid,
meshed.centroid
);
}
#[test]
fn a_raised_solid_measures_the_same_as_one_on_the_ground() {
use axiolid_construct::boolean_exact::{boolean_arc_prisms_exact, ArcPrism};
use axiolid_core::{BooleanOperator, Point2};
use axiolid_overlay::{ArcRing, ArcVertex};
let square = |half: f64| {
ArcRing::new(
[(-half, -half), (half, -half), (half, half), (-half, half)]
.into_iter()
.map(|(x, y)| ArcVertex::straight(Point2::new(x, y)))
.collect(),
)
};
let block = |bottom: f64, top: f64| ArcPrism {
section: square(0.5),
bottom,
top,
};
let cover = |bottom: f64, top: f64| ArcPrism {
section: square(1.0),
bottom,
top,
};
let at = |bottom: f64| {
let solid = boolean_arc_prisms_exact(
&block(bottom, bottom + 1.0),
&cover(bottom, bottom + 1.0),
BooleanOperator::Intersection,
tol(),
)
.expect("a unit cube");
exact_properties(&solid, tol()).expect("all-planar")
};
let ground = at(0.0);
let raised = at(2.0);
assert!(
(ground.signed_volume - 1.0).abs() < 1e-12,
"{}",
ground.signed_volume
);
assert!(
(raised.signed_volume - 1.0).abs() < 1e-12,
"{}",
raised.signed_volume
);
assert!(
(raised.centroid.z - 2.5).abs() < 1e-12,
"{:?}",
raised.centroid
);
assert!(
(ground.centroid.z - 0.5).abs() < 1e-12,
"{:?}",
ground.centroid
);
}
#[test]
fn a_planar_hole_subtracts_its_area() {
use axiolid_core::{Interval, Point2};
use axiolid_curve::{Curve2, Line2};
use axiolid_profile::{Contour, ContourProfile, ProfileSegment};
let square = |half: f64| {
let corners = [
Point2::new(-half, -half),
Point2::new(half, -half),
Point2::new(half, half),
Point2::new(-half, half),
];
Contour::new(
(0..4)
.map(|i| ProfileSegment {
curve: Curve2::Line(Line2 {
origin: corners[i],
direction: corners[(i + 1) % 4] - corners[i],
}),
domain: Interval::UNIT,
same_sense: true,
})
.collect(),
)
};
let depth = 3.0;
let profile = Profile::Contour(ContourProfile {
outer: square(2.0),
holes: vec![square(1.0)],
});
let brep = extrude_profile_exact(&profile, Vec3::Z, depth, tol()).expect("extrudes");
let props = exact_properties(&brep, tol()).expect("measurable");
let section = 16.0 - 4.0;
assert!(
(props.signed_volume - section * depth).abs() < 1e-12,
"{}",
props.signed_volume
);
let area = 2.0 * section + (16.0 + 8.0) * depth;
assert!(
(props.area - area).abs() < 1e-12,
"expected {area}, got {}",
props.area
);
}
fn site() -> axiolid_core::Transform3 {
use axiolid_core::Transform3;
Transform3::from_translation(Vec3::new(
600_013.917_809_576_6,
5_599_985.940_231_986,
204.5,
)) * Transform3::from_rotation_z(0.039_618_973_884_5)
}
#[test]
fn a_box_on_a_national_grid_measures_its_closed_form() {
use axiolid_construct::extrude::extrude_profile;
use axiolid_construct::profile::profile_rings;
use axiolid_measure::volume_properties;
let (x, y, depth) = (1.01, 2.26, 0.25);
let local = extrude_profile_exact(&rectangle(x, y), Vec3::Z, depth, tol()).expect("extrudes");
let at_origin = exact_properties(&local, tol()).expect("measurable");
let placed = local.transformed(&site()).expect("rigid");
let props = exact_properties(&placed, tol()).expect("measurable");
let volume = x * y * depth;
let area = 2.0 * x * y + 2.0 * depth * (x + y);
let spacing = 4.0 * site().translation.length() * f64::EPSILON;
let grid = area * spacing;
assert!(
(props.signed_volume - volume).abs() < grid,
"expected {volume} within {grid:e}, got {}",
props.signed_volume
);
let edges = 4.0 * (x + y + depth);
assert!(
(props.area - area).abs() < edges * spacing,
"expected area {area}, got {}",
props.area
);
let centroid = site().transform_point3(at_origin.centroid);
assert!(
(props.centroid - centroid).length() < 1e-8,
"expected centroid {centroid:?}, got {:?}",
props.centroid
);
let (sin, cos) = 0.039_618_973_884_5_f64.sin_cos();
let central = Vec3::new(
volume * (cos * cos * x * x + sin * sin * y * y) / 12.0,
volume * (sin * sin * x * x + cos * cos * y * y) / 12.0,
volume * depth * depth / 12.0,
);
let moments = central + volume * centroid * centroid;
for axis in 0..3 {
let (got, want) = (props.second_moment_diagonal[axis], moments[axis]);
assert!(
(got - want).abs() < (grid / volume + 1e-12) * want,
"second moment {axis}: expected {want}, got {got}"
);
}
let rings = profile_rings(&rectangle(x, y), 1e-5, tol()).expect("rings");
let mut mesh = extrude_profile(&rings, Vec3::Z, depth, tol()).expect("mesh");
for p in &mut mesh.positions {
*p = site().transform_point3(*p);
}
let meshed = volume_properties(&mesh, tol()).expect("closed");
assert!(
(props.signed_volume - meshed.signed_volume).abs() < grid,
"exact {} vs mesh {}",
props.signed_volume,
meshed.signed_volume
);
}
#[test]
fn a_cylinder_on_a_national_grid_measures_its_closed_form() {
use axiolid_profile::CircleProfile;
let (radius, height) = (0.3, 2.5);
let circle = Profile::Circle(CircleProfile {
radius,
thickness: None,
});
let local = extrude_profile_exact(&circle, Vec3::Z, height, tol()).expect("extrudes");
let placed = local.transformed(&site()).expect("rigid");
let props = exact_properties(&placed, tol()).expect("measurable");
let volume = core::f64::consts::PI * radius * radius * height;
assert!(
(props.signed_volume - volume).abs() < 1e-12 * volume,
"expected {volume}, got {}",
props.signed_volume
);
let centroid = site().transform_point3(axiolid_core::Point3::new(0.0, 0.0, height / 2.0));
assert!(
(props.centroid - centroid).length() < 1e-8,
"expected centroid {centroid:?}, got {:?}",
props.centroid
);
let central = Vec3::new(
volume * radius * radius / 4.0,
volume * radius * radius / 4.0,
volume * height * height / 12.0,
);
let moments = central + volume * centroid * centroid;
let area = 2.0 * core::f64::consts::PI * radius * (radius + height);
let grid = area * 4.0 * site().translation.length() * f64::EPSILON;
for axis in 0..3 {
let (got, want) = (props.second_moment_diagonal[axis], moments[axis]);
assert!(
(got - want).abs() < (grid / volume + 1e-12) * want,
"second moment {axis}: expected {want}, got {got}"
);
}
}