use axiolid_brep::ExactBRep;
use axiolid_core::{Point2, Scalar, Vec3};
use axiolid_evaluate::surface::{evaluate, partials};
use axiolid_surface::Surface;
use axiolid_topology::Face;
use crate::exact::{ExactMeasureError, Sums, COMPONENTS};
use crate::exact_domain::{assemble, chart, pole_on_domain_side};
const RELATIVE: Scalar = 1e-13;
const MAX_PIECES: usize = 2048;
const DIMENSION: [i32; COMPONENTS] = [2, 3, 4, 4, 4, 5, 5, 5];
pub(crate) fn face_sums(
brep: &ExactBRep,
face: &Face<axiolid_brep::SurfaceId>,
surface: &Surface,
linear: Scalar,
scale: Scalar,
) -> Result<Sums, ExactMeasureError> {
let chart = chart(surface)?;
let boundary = assemble(brep, face, surface, &chart, linear)?;
let floor = noise_floor(scale);
let along_v = boundary.wraps[1];
if along_v {
if boundary.wraps[0] || boundary.winding[1] != 0 {
return Err(ExactMeasureError::NonPlanarFace(
"face boundary winds around the surface in both directions",
));
}
}
let reference = if along_v {
boundary.anchor.x
} else if boundary.winding[0] != 0 {
pole_on_domain_side(&chart, &boundary)?
} else {
boundary.anchor.y
};
let mut total = [0.0; COMPONENTS];
for piece in &boundary.pieces {
for (a, b) in piece.smooth_spans() {
let sums = adaptive(a, b, &floor, &mut |t| {
let (point, tangent) = piece.at(t)?;
let (weight, inner) = if along_v {
(tangent.y, inner_along_u(surface, reference, point, &floor)?)
} else {
(
-tangent.x,
inner_along_v(surface, reference, point, &floor)?,
)
};
let mut value = inner;
for slot in &mut value {
*slot *= weight;
}
Ok(value)
})?;
for (slot, value) in total.iter_mut().zip(sums) {
*slot += value;
}
}
}
Ok(total)
}
fn inner_along_v(
surface: &Surface,
reference: Scalar,
at: Point2,
floor: &Sums,
) -> Result<Sums, ExactMeasureError> {
adaptive(reference, at.y, floor, &mut |v| density(surface, at.x, v))
}
fn inner_along_u(
surface: &Surface,
reference: Scalar,
at: Point2,
floor: &Sums,
) -> Result<Sums, ExactMeasureError> {
adaptive(reference, at.x, floor, &mut |u| density(surface, u, at.y))
}
fn density(surface: &Surface, u: Scalar, v: Scalar) -> Result<Sums, ExactMeasureError> {
let p = evaluate(surface, u, v).map_err(|_| crate::exact::EVALUATION)?;
let (su, sv) = partials(surface, u, v).map_err(|_| crate::exact::EVALUATION)?;
let n: Vec3 = su.cross(sv);
let w = p.dot(n);
Ok([
n.length(),
w / 3.0,
p.x * w / 4.0,
p.y * w / 4.0,
p.z * w / 4.0,
p.x * p.x * w / 5.0,
p.y * p.y * w / 5.0,
p.z * p.z * w / 5.0,
])
}
fn noise_floor(scale: Scalar) -> Sums {
let mut floor = [0.0; COMPONENTS];
for (slot, dimension) in floor.iter_mut().zip(DIMENSION) {
*slot = 1e-15 * scale.powi(dimension);
}
floor
}
const XGK: [Scalar; 8] = [
0.991_455_371_120_812_6,
0.949_107_912_342_758_5,
0.864_864_423_359_769_1,
0.741_531_185_599_394_4,
0.586_087_235_467_691_1,
0.405_845_151_377_397_2,
0.207_784_955_007_898_5,
0.0,
];
const WGK: [Scalar; 8] = [
0.022_935_322_010_529_22,
0.063_092_092_629_978_55,
0.104_790_010_322_250_2,
0.140_653_259_715_525_9,
0.169_004_726_639_267_9,
0.190_350_578_064_785_4,
0.204_432_940_075_298_9,
0.209_482_141_084_727_8,
];
const WG: [Scalar; 4] = [
0.129_484_966_168_869_7,
0.279_705_391_489_276_7,
0.381_830_050_505_118_9,
0.417_959_183_673_469_4,
];
fn kronrod(
a: Scalar,
b: Scalar,
f: &mut dyn FnMut(Scalar) -> Result<Sums, ExactMeasureError>,
) -> Result<(Sums, Sums, Sums), ExactMeasureError> {
let centre = 0.5 * (a + b);
let half = 0.5 * (b - a);
let mut k = [0.0; COMPONENTS];
let mut g = [0.0; COMPONENTS];
let mut magnitude = [0.0; COMPONENTS];
let mut add = |value: Sums, kw: Scalar, gw: Scalar| {
for c in 0..COMPONENTS {
k[c] += kw * value[c];
g[c] += gw * value[c];
magnitude[c] += kw * value[c].abs();
}
};
add(f(centre)?, WGK[7], WG[3]);
for j in 0..7 {
let dx = half * XGK[j];
let gw = if j % 2 == 1 { WG[j / 2] } else { 0.0 };
add(f(centre - dx)?, WGK[j], gw);
add(f(centre + dx)?, WGK[j], gw);
}
let mut value = [0.0; COMPONENTS];
let mut error = [0.0; COMPONENTS];
let mut scale = [0.0; COMPONENTS];
for c in 0..COMPONENTS {
value[c] = half * k[c];
scale[c] = (half * magnitude[c]).abs();
let raw = (half * (k[c] - g[c])).abs();
error[c] = if scale[c] > 0.0 && raw > 0.0 {
scale[c] * (200.0 * raw / scale[c]).powf(1.5).min(1.0)
} else {
raw
};
}
Ok((value, error, scale))
}
fn adaptive(
a: Scalar,
b: Scalar,
floor: &Sums,
f: &mut dyn FnMut(Scalar) -> Result<Sums, ExactMeasureError>,
) -> Result<Sums, ExactMeasureError> {
let mut total = [0.0; COMPONENTS];
if a == b {
return Ok(total);
}
let mut pending = vec![(a, b)];
let mut pieces = 0;
while let Some((lo, hi)) = pending.pop() {
pieces += 1;
if pieces > MAX_PIECES {
return Err(crate::exact::NOT_CONVERGED);
}
let (value, error, scale) = kronrod(lo, hi, f)?;
let accepted = (0..COMPONENTS).all(|c| error[c] <= (RELATIVE * scale[c]).max(floor[c]));
if accepted {
for c in 0..COMPONENTS {
total[c] += value[c];
}
} else {
let mid = 0.5 * (lo + hi);
if mid <= lo.min(hi) || mid >= lo.max(hi) {
return Err(crate::exact::NOT_CONVERGED);
}
pending.push((lo, mid));
pending.push((mid, hi));
}
}
if total.iter().all(|value| value.is_finite()) {
Ok(total)
} else {
Err(crate::exact::NOT_CONVERGED)
}
}
#[cfg(test)]
mod tests {
use crate::exact::{exact_properties, ExactMeasureError};
use axiolid_brep::{ExactBRep, ExactBRepBuilder};
use axiolid_core::{Frame2, Frame3, Interval, Point3, Tolerance, Vec2, Vec3};
use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, KnotSpec, Line2};
use axiolid_surface::{BSplineSurface, Cone, Cylinder, Plane, Sphere, Surface, Torus};
use axiolid_topology::{
Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Shell, Solid, Vertex,
};
use core::f64::consts::{PI, TAU};
const WORLD: Frame3 = Frame3 {
origin: Point3::ZERO,
x: Vec3::X,
y: Vec3::Y,
z: Vec3::Z,
};
fn capped(surface: Surface, r: f64, upward: bool) -> ExactBRep {
capped_at(surface, r, upward, 0.0)
}
fn at_height(z: f64) -> Frame3 {
Frame3 {
origin: Point3::new(0.0, 0.0, z),
..WORLD
}
}
fn capped_at(surface: Surface, r: f64, upward: bool, height: f64) -> ExactBRep {
let disc = Surface::Plane(Plane {
frame: at_height(height),
});
let round = Circle2 {
frame: Frame2 {
origin: Vec2::ZERO,
x: Vec2::X,
y: Vec2::Y,
},
radius: r,
};
capped_with(surface, r, upward, disc, round, height)
}
fn capped_with(
surface: Surface,
r: f64,
upward: bool,
disc: Surface,
round: Circle2,
height: f64,
) -> ExactBRep {
let mut b = ExactBRepBuilder::default();
let vertex = b.topology_mut().add_vertex(Vertex {
position: Point3::new(r, 0.0, height),
});
let circle = b.add_curve3(Curve3::Circle(Circle3 {
frame: at_height(height),
radius: r,
}));
let edge = b.topology_mut().add_edge(Edge {
start: vertex,
end: vertex,
curve: Some(circle),
});
b.set_edge_interval(edge, Interval::new(0.0, TAU));
let (wall_use, disc_use, forward, backward) = if upward {
(
Orientation::Forward,
Orientation::Reversed,
Interval::new(0.0, TAU),
Interval::new(TAU, 0.0),
)
} else {
(
Orientation::Reversed,
Orientation::Forward,
Interval::new(TAU, 0.0),
Interval::new(0.0, TAU),
)
};
let rim = b.add_curve2(Curve2::Line(Line2 {
origin: Vec2::ZERO,
direction: Vec2::X,
}));
let wall_loop = b.topology_mut().add_loop(Loop {
edges: vec![EdgeUse {
edge,
orientation: wall_use,
pcurve: Some(rim),
}],
});
b.set_pcurve_interval(wall_loop, 0, forward);
let round = b.add_curve2(Curve2::Circle(round));
let disc_loop = b.topology_mut().add_loop(Loop {
edges: vec![EdgeUse {
edge,
orientation: disc_use,
pcurve: Some(round),
}],
});
b.set_pcurve_interval(disc_loop, 0, backward);
let wall_surface = b.add_surface(surface);
let plane = b.add_surface(disc);
let mut faces = Vec::new();
for (surface, loop_id) in [(wall_surface, wall_loop), (plane, disc_loop)] {
faces.push((
b.topology_mut().add_face(Face {
surface: Some(surface),
bounds: vec![FaceBound {
loop_id,
orientation: Orientation::Forward,
outer: true,
}],
orientation: Orientation::Forward,
}),
Orientation::Forward,
));
}
let outer = b.topology_mut().add_shell(Shell {
faces,
closed: true,
});
b.topology_mut().add_solid(Solid {
outer,
voids: Vec::new(),
});
b.finish().expect("a valid capped solid")
}
fn close(what: &str, got: f64, expected: f64) {
assert!(
(got - expected).abs() <= 1e-11 * expected.abs().max(1.0),
"{what}: expected {expected}, got {got}"
);
}
#[test]
fn a_hemisphere_reaches_its_north_pole() {
let r = 1.5;
let solid = capped(
Surface::Sphere(Sphere {
frame: WORLD,
radius: r,
}),
r,
true,
);
let props = exact_properties(&solid, Tolerance::METRE).expect("measurable");
close("volume", props.signed_volume, 2.0 / 3.0 * PI * r.powi(3));
close("area", props.area, 3.0 * PI * r * r);
close("centroid z", props.centroid.z, 3.0 * r / 8.0);
close(
"second moment z",
props.second_moment_diagonal.z,
2.0 / 15.0 * PI * r.powi(5),
);
}
#[test]
fn a_lower_hemisphere_reaches_its_south_pole() {
let r = 0.75;
let solid = capped(
Surface::Sphere(Sphere {
frame: WORLD,
radius: r,
}),
r,
false,
);
let props = exact_properties(&solid, Tolerance::METRE).expect("measurable");
close("volume", props.signed_volume, 2.0 / 3.0 * PI * r.powi(3));
close("centroid z", props.centroid.z, -3.0 * r / 8.0);
}
#[test]
fn a_cone_reaches_its_apex() {
let (r, h) = (2.0, 3.0);
let solid = capped(
Surface::Cone(Cone {
frame: WORLD,
radius: r,
semi_angle: (-r / h).atan(),
}),
r,
true,
);
let props = exact_properties(&solid, Tolerance::METRE).expect("measurable");
close("volume", props.signed_volume, PI * r * r * h / 3.0);
close(
"area",
props.area,
PI * r * r + PI * r * (r * r + h * h).sqrt(),
);
close("centroid z", props.centroid.z, h / 4.0);
}
#[test]
fn a_b_spline_face_is_integrated_like_any_other() {
let r = 1.25;
let corner = |x: f64, y: f64| Point3::new(x, y, 0.0);
let patch = BSplineSurface {
u_degree: 1,
v_degree: 1,
control_points: vec![
vec![corner(-r, -r), corner(-r, r)],
vec![corner(r, -r), corner(r, r)],
],
u_knots: vec![0.0, 1.0],
u_multiplicities: vec![2, 2],
v_knots: vec![0.0, 1.0],
v_multiplicities: vec![2, 2],
weights: None,
u_closed: false,
v_closed: false,
knot_spec: KnotSpec::Unspecified,
self_intersect: None,
};
let rim = Circle2 {
frame: Frame2 {
origin: Vec2::new(0.5, 0.5),
x: Vec2::X,
y: Vec2::Y,
},
radius: 0.5,
};
let solid = capped_with(
Surface::Sphere(Sphere {
frame: WORLD,
radius: r,
}),
r,
true,
Surface::BSpline(patch),
rim,
0.0,
);
let props = exact_properties(&solid, Tolerance::METRE).expect("measurable");
close("volume", props.signed_volume, 2.0 / 3.0 * PI * r.powi(3));
close("area", props.area, 3.0 * PI * r * r);
}
fn half_torus(major: f64, minor: f64) -> ExactBRep {
let mut b = ExactBRepBuilder::default();
let meridian = |b: &mut ExactBRepBuilder, x: f64| {
let frame = Frame3 {
origin: Point3::new(x * major, 0.0, 0.0),
x: Vec3::X * x,
y: Vec3::Z,
z: (Vec3::X * x).cross(Vec3::Z),
};
let vertex = b.topology_mut().add_vertex(Vertex {
position: Point3::new(x * (major + minor), 0.0, 0.0),
});
let circle = b.add_curve3(Curve3::Circle(Circle3 {
frame,
radius: minor,
}));
let edge = b.topology_mut().add_edge(Edge {
start: vertex,
end: vertex,
curve: Some(circle),
});
b.set_edge_interval(edge, Interval::new(0.0, TAU));
edge
};
let start = meridian(&mut b, 1.0);
let end = meridian(&mut b, -1.0);
let up = |b: &mut ExactBRepBuilder, u: f64| {
b.add_curve2(Curve2::Line(Line2 {
origin: Vec2::new(u, 0.0),
direction: Vec2::Y,
}))
};
let (at_start, at_end) = (up(&mut b, 0.0), up(&mut b, PI));
let tube_end = b.topology_mut().add_loop(Loop {
edges: vec![EdgeUse {
edge: end,
orientation: Orientation::Forward,
pcurve: Some(at_end),
}],
});
b.set_pcurve_interval(tube_end, 0, Interval::new(0.0, TAU));
let tube_start = b.topology_mut().add_loop(Loop {
edges: vec![EdgeUse {
edge: start,
orientation: Orientation::Forward,
pcurve: Some(at_start),
}],
});
b.set_pcurve_interval(tube_start, 0, Interval::new(0.0, TAU));
let disc_frame = |x: f64| Frame3 {
origin: Point3::new(x * major, 0.0, 0.0),
x: Vec3::X,
y: Vec3::Z,
z: -Vec3::Y,
};
let rim = |b: &mut ExactBRepBuilder, x: f64| {
b.add_curve2(Curve2::Circle(Circle2 {
frame: Frame2 {
origin: Vec2::ZERO,
x: Vec2::new(x, 0.0),
y: Vec2::Y,
},
radius: minor,
}))
};
let (rim_start, rim_end) = (rim(&mut b, 1.0), rim(&mut b, -1.0));
let disc_start = b.topology_mut().add_loop(Loop {
edges: vec![EdgeUse {
edge: start,
orientation: Orientation::Forward,
pcurve: Some(rim_start),
}],
});
b.set_pcurve_interval(disc_start, 0, Interval::new(0.0, TAU));
let disc_end = b.topology_mut().add_loop(Loop {
edges: vec![EdgeUse {
edge: end,
orientation: Orientation::Reversed,
pcurve: Some(rim_end),
}],
});
b.set_pcurve_interval(disc_end, 0, Interval::new(TAU, 0.0));
let torus = b.add_surface(Surface::Torus(Torus {
frame: WORLD,
major_radius: major,
minor_radius: minor,
}));
let plane_start = b.add_surface(Surface::Plane(Plane {
frame: disc_frame(1.0),
}));
let plane_end = b.add_surface(Surface::Plane(Plane {
frame: disc_frame(-1.0),
}));
let bound = |loop_id, outer| FaceBound {
loop_id,
orientation: Orientation::Forward,
outer,
};
let reversed = FaceBound {
loop_id: tube_start,
orientation: Orientation::Reversed,
outer: false,
};
let mut faces = Vec::new();
for (surface, bounds) in [
(torus, vec![bound(tube_end, true), reversed]),
(plane_start, vec![bound(disc_start, true)]),
(plane_end, vec![bound(disc_end, true)]),
] {
let face = b.topology_mut().add_face(Face {
surface: Some(surface),
bounds,
orientation: Orientation::Forward,
});
faces.push((face, Orientation::Forward));
}
let outer = b.topology_mut().add_shell(Shell {
faces,
closed: true,
});
b.topology_mut().add_solid(Solid {
outer,
voids: Vec::new(),
});
b.finish().expect("a valid half torus")
}
#[test]
fn a_face_winding_round_the_tube_is_integrated_along_it() {
let (major, minor) = (3.0, 1.0);
let props =
exact_properties(&half_torus(major, minor), Tolerance::METRE).expect("measurable");
close(
"volume",
props.signed_volume,
PI * PI * major * minor * minor,
);
close(
"area",
props.area,
2.0 * PI * PI * major * minor + 2.0 * PI * minor * minor,
);
let moment = 2.0 * (PI * minor * minor * major * major + PI * minor.powi(4) / 4.0);
close(
"centroid y",
props.centroid.y,
moment / (PI * PI * major * minor * minor),
);
}
#[test]
fn the_adaptive_rule_meets_its_bound_where_one_panel_cannot() {
use super::{adaptive, kronrod, COMPONENTS};
let runge = |x: f64| 1.0 / (1.0 + 100.0 * x * x);
let exact = 2.0 * 10.0_f64.atan() / 10.0;
let floor = [0.0; COMPONENTS];
let mut f = |x: f64| Ok([runge(x); COMPONENTS]);
let (one_panel, _, _) = kronrod(-1.0, 1.0, &mut f).expect("finite");
assert!((one_panel[0] - exact).abs() > 1e-6, "{}", one_panel[0]);
let value = adaptive(-1.0, 1.0, &floor, &mut f).expect("converges");
for component in value {
assert!(
(component - exact).abs() < 1e-14,
"expected {exact}, got {component}"
);
}
}
#[test]
fn two_poles_face_each_other_across_a_certified_gap() {
use crate::exact_distance::boundary_distance;
let north = capped(
Surface::Sphere(Sphere {
frame: WORLD,
radius: 1.0,
}),
1.0,
true,
);
let south = capped_at(
Surface::Sphere(Sphere {
frame: at_height(3.0),
radius: 1.0,
}),
1.0,
false,
3.0,
);
let bounds = boundary_distance(&north, &south, 1e-8, Tolerance::METRE).expect("bounded");
assert!(
bounds.lower <= 1.0 && 1.0 <= bounds.upper && bounds.upper - bounds.lower <= 1e-8,
"{bounds:?}"
);
}
#[test]
fn a_cone_apex_is_never_pruned_as_non_critical() {
use crate::exact_distance::boundary_distance;
let cone = capped(
Surface::Cone(Cone {
frame: WORLD,
radius: 1.0,
semi_angle: (-1.0_f64).atan(),
}),
1.0,
true,
);
let south = capped_at(
Surface::Sphere(Sphere {
frame: at_height(2.5),
radius: 1.0,
}),
1.0,
false,
2.5,
);
let bounds = boundary_distance(&cone, &south, 1e-6, Tolerance::METRE).expect("bounded");
assert!(
bounds.lower <= 0.5 && 0.5 <= bounds.upper && bounds.upper - bounds.lower <= 1e-6,
"{bounds:?}"
);
}
#[test]
fn a_face_domain_answers_for_every_turn_of_an_angle() {
use crate::exact_domain::FaceDomain;
let solid = capped(
Surface::Sphere(Sphere {
frame: WORLD,
radius: 1.0,
}),
1.0,
true,
);
let face = solid.topology().face_id_at(0).unwrap();
let domain = FaceDomain::new(&solid, face, Tolerance::METRE)
.unwrap()
.expect("a classifiable face");
for u in [-0.2, TAU - 0.2, 2.0 * TAU - 0.2, -TAU - 0.2] {
assert_eq!(
domain.contains(axiolid_core::Point2::new(u, 0.7)).unwrap(),
Some(true),
"angle {u}"
);
assert_eq!(
domain.contains(axiolid_core::Point2::new(u, -0.7)).unwrap(),
Some(false),
"angle {u} below the rim"
);
}
}
#[test]
fn a_lone_circle_on_a_cylinder_bounds_nothing() {
let solid = capped(
Surface::Cylinder(Cylinder {
frame: WORLD,
radius: 1.0,
}),
1.0,
true,
);
let error = exact_properties(&solid, Tolerance::METRE).expect_err("unbounded");
assert!(
matches!(error, ExactMeasureError::NonPlanarFace(_)),
"got {error:?}"
);
}
}