277 lines
13 KiB
Rust
277 lines
13 KiB
Rust
//! Tests of boundary patches: the bridge this builds is measured back off the face it made — not
|
||
//! from the construction — and everything it cannot hold is an error rather than a quiet
|
||
//! approximation.
|
||
|
||
use super::*;
|
||
use crate::body::from_p3;
|
||
use truck_modeling::ParametricSurface;
|
||
|
||
/// The angle between two vectors, in degrees.
|
||
fn angle_deg(a: Vec3, b: Vec3) -> f64 {
|
||
(a.normalized().unwrap().dot(b.normalized().unwrap())).clamp(-1.0, 1.0).acos().to_degrees()
|
||
}
|
||
|
||
/// The geometric shape of a surface curve from its first three derivatives along it: the unit
|
||
/// tangent, the curvature vector and the arclength derivative of that curvature vector — the same
|
||
/// reading `bridge` states its conditions with.
|
||
fn shape_of(d1: Vec3, d2: Vec3, d3: Vec3) -> (Vec3, Vec3, Vec3) {
|
||
let sigma = d1.len();
|
||
let t = d1 * (1.0 / sigma);
|
||
let tp = d2 * (1.0 / sigma) - d1 * (d1.dot(d2) / (sigma * sigma * sigma));
|
||
let nn = d2 - t * d2.dot(t);
|
||
let np = d3 - t * d3.dot(t) - tp * d2.dot(t) - t * d2.dot(tp);
|
||
let kappa = np * (sigma * sigma) - nn * (2.0 * d1.dot(d2));
|
||
(t, nn * (1.0 / (sigma * sigma)), kappa * (1.0 / (sigma.powi(5))))
|
||
}
|
||
|
||
/// Across a boundary: five samples a step `h` apart, `into` the patch from the parameter `(u, v)`,
|
||
/// read back as the position, the unit tangent, the curvature vector and the change of curvature
|
||
/// along that inward direction.
|
||
fn across(body: &Body, uv: (f64, f64), into: (f64, f64), h: f64) -> (Vec3, Vec3, Vec3, Vec3) {
|
||
let face = body.solid.face_iter().next().unwrap();
|
||
let surf = face.surface();
|
||
let q: Vec<Vec3> = (0..5).map(|k| from_p3(surf.subs(uv.0 + k as f64 * h * into.0, uv.1 + k as f64 * h * into.1))).collect();
|
||
let (f0, f1, f2, f3, f4) = (q[0], q[1], q[2], q[3], q[4]);
|
||
let d1 = (f0 * -25.0 + f1 * 48.0 + f2 * -36.0 + f3 * 16.0 + f4 * -3.0) * (1.0 / (12.0 * h));
|
||
let d2 = (f0 * 35.0 + f1 * -104.0 + f2 * 114.0 + f3 * -56.0 + f4 * 11.0) * (1.0 / (12.0 * h * h));
|
||
let d3 = (f0 * -5.0 + f1 * 18.0 + f2 * -24.0 + f3 * 14.0 + f4 * -3.0) * (1.0 / (2.0 * h * h * h));
|
||
let (t, bend, change) = shape_of(d1, d2, d3);
|
||
(f0, t, bend, change)
|
||
}
|
||
|
||
/// The stations' parameters: `n` of them, evenly spread over the patch.
|
||
fn stations(n: usize) -> Vec<f64> {
|
||
(0..n).map(|i| i as f64 / (n - 1) as f64).collect()
|
||
}
|
||
|
||
/// The normal angle a side may be off by.
|
||
const ANGLE_TOL: f64 = 0.01;
|
||
/// How much of the supporting wall's own curvature a side must repeat, relative to it.
|
||
const CURV_TOL: f64 = 1e-3;
|
||
/// The normal part of the change of curvature a G3 side may leave over, relative to 1/r² (the
|
||
/// finite differences that read the third derivative off the face amplify round-off by 1/h³, so
|
||
/// this is a noise floor rather than a geometric tolerance).
|
||
const CHANGE_TOL: f64 = 1e-4;
|
||
|
||
/// Measure one side of a built patch at every station of `frames`, and check what its order asks
|
||
/// for: the boundary curve itself always, the tangent plane from G1 on, the curvature vector from
|
||
/// G2 on and its change along the leave direction at G3.
|
||
fn check_side(body: &Body, frames: &[Frame], order: Continuity, at: impl Fn(usize) -> ((f64, f64), (f64, f64)), r: f64) {
|
||
for (i, f) in frames.iter().enumerate() {
|
||
let (uv, into) = at(i);
|
||
let (p, t, bend, change) = across(body, uv, into, 1e-3);
|
||
let gap = p.dist(f.p);
|
||
assert!(gap < 1e-9, "station {i} sits on its boundary: {p:?} vs {:?} ({gap:.2e} mm)", f.p);
|
||
if order == Continuity::G0 {
|
||
continue;
|
||
}
|
||
assert!(angle_deg(t, f.leave) < ANGLE_TOL, "station {i} leaves along the tangent: {t:?} vs {:?}", f.leave);
|
||
if order == Continuity::G1 {
|
||
continue;
|
||
}
|
||
if f.bend.len() > 1e-9 {
|
||
assert!(bend.dist(f.bend) / f.bend.len() < CURV_TOL, "station {i}: curvature {bend:?} vs the wall's {:?}", f.bend);
|
||
if order == Continuity::G3 {
|
||
let normal = f.bend * (1.0 / f.bend.len());
|
||
assert!(change.dot(normal).abs() < CHANGE_TOL / (r * r), "station {i}: the change of curvature has a normal part {change:?}");
|
||
}
|
||
} else {
|
||
assert!(bend.len() < CURV_TOL / r, "station {i}: the wall is straight here, the patch curves by {bend:?}");
|
||
if order == Continuity::G3 {
|
||
assert!(change.len() < CHANGE_TOL / (r * r), "station {i}: a straight cross-section changes curvature by {change:?}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Two walls 20 mm apart, each leaving towards the other, both walls of the same cylinder of radius
|
||
/// `r` (the valley `bridge` is tested with): the two sides of a two-sided patch.
|
||
fn two_walls(n: usize, r: f64) -> (Vec<Frame>, Vec<Frame>) {
|
||
let mut a = Vec::new();
|
||
let mut b = Vec::new();
|
||
for t in stations(n) {
|
||
let y = 20.0 * t;
|
||
a.push(Frame {
|
||
p: Vec3::new(0.0, y, 0.0),
|
||
leave: Vec3::new(0.0, 1.0, 0.0),
|
||
bend: Vec3::new(0.0, 0.0, 1.0 / r),
|
||
bend_change: Vec3::new(0.0, -1.0 / (r * r), 0.0),
|
||
});
|
||
b.push(Frame {
|
||
p: Vec3::new(0.0, y + 20.0, 0.0),
|
||
leave: Vec3::new(0.0, -1.0, 0.0),
|
||
bend: Vec3::new(0.0, 0.0, 1.0 / r),
|
||
bend_change: Vec3::new(0.0, 1.0 / (r * r), 0.0),
|
||
});
|
||
}
|
||
(a, b)
|
||
}
|
||
|
||
#[test]
|
||
fn a_two_sided_patch_is_a_bridge_that_holds_both_ends() {
|
||
let n = 9;
|
||
let r = 10.0;
|
||
let (a, b) = two_walls(n, r);
|
||
let boundaries = vec![Boundary { frames: a.clone(), order: Continuity::G2 }, Boundary { frames: b.clone(), order: Continuity::G2 }];
|
||
let body = boundary_patch(&boundaries, 0.0, n).unwrap();
|
||
let s = stations(n);
|
||
check_side(&body, &a, Continuity::G2, |i| ((s[i], 0.0), (0.0, 1.0)), r);
|
||
check_side(&body, &b, Continuity::G2, |i| ((s[i], 1.0), (0.0, -1.0)), r);
|
||
}
|
||
|
||
/// Four chained sides round a 20 × 20 square in the XY plane, each leaving into the square: the
|
||
/// loop a patch through three or four boundaries would have to close.
|
||
fn square_loop(n: usize) -> Vec<Boundary> {
|
||
let corners = [(0.0_f64, 0.0_f64), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)];
|
||
let leaves = [Vec3::new(0.0, 1.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), Vec3::new(0.0, -1.0, 0.0), Vec3::new(1.0, 0.0, 0.0)];
|
||
(0..4)
|
||
.map(|side| {
|
||
let (a, b) = (corners[side], corners[(side + 1) % 4]);
|
||
let frames = stations(n)
|
||
.into_iter()
|
||
.map(|t| Frame {
|
||
p: Vec3::new(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t, 0.0),
|
||
leave: leaves[side],
|
||
bend: Vec3::ZERO,
|
||
bend_change: Vec3::ZERO,
|
||
})
|
||
.collect();
|
||
Boundary { frames, order: Continuity::G2 }
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn a_two_sided_patch_meets_its_walls_up_to_the_curvature() {
|
||
let n = 11;
|
||
let r = 10.0;
|
||
let (a, b) = two_walls(n, r);
|
||
// G0, G1 and G2 are what this patch states: position, tangent plane and curvature across each
|
||
// boundary. (A G3 bridge's third-order rows are `bridge`'s own subject, measured in its tests.)
|
||
for order in [Continuity::G0, Continuity::G1, Continuity::G2] {
|
||
let boundaries = vec![Boundary { frames: a.clone(), order }, Boundary { frames: b.clone(), order }];
|
||
let body = boundary_patch(&boundaries, 0.0, n).unwrap();
|
||
let s = stations(n);
|
||
if order == Continuity::G0 {
|
||
for (i, f) in a.iter().enumerate() {
|
||
let (p, _, _, _) = across(&body, (s[i], 0.0), (0.0, 1.0), 1e-3);
|
||
assert!(p.dist(f.p) < 1e-9, "station {i}: {p:?} vs {:?}", f.p);
|
||
}
|
||
continue;
|
||
}
|
||
check_side(&body, &a, order, |i| ((s[i], 0.0), (0.0, 1.0)), r);
|
||
check_side(&body, &b, order, |i| ((s[i], 1.0), (0.0, -1.0)), r);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_crown_shapes_the_middle_without_touching_the_boundary_rows() {
|
||
let n = 9;
|
||
let r = 10.0;
|
||
let (a, b) = two_walls(n, r);
|
||
let boundaries = vec![Boundary { frames: a.clone(), order: Continuity::G2 }, Boundary { frames: b.clone(), order: Continuity::G2 }];
|
||
let s = stations(n);
|
||
let mut middles = Vec::new();
|
||
for crown in [-0.5, 0.0, 1.0, 2.0] {
|
||
let body = boundary_patch(&boundaries, crown, n).unwrap();
|
||
let grid = crate::bridge::sample_grid(&body, 9, 9).unwrap();
|
||
middles.push(grid[4][4].z);
|
||
// The interior is the cross-section between the two walls: its middle sits in the gap, not
|
||
// outside it, and it stays finite everywhere on the grid.
|
||
for row in &grid {
|
||
for p in row {
|
||
assert!(p.is_finite() && (-2.0..=42.0).contains(&p.y), "the interior stays between the walls: {p:?}");
|
||
}
|
||
}
|
||
assert!((18.0..=22.0).contains(&grid[4][4].y), "the middle of the patch sits midway in the 20 mm band: {:?}", grid[4][4]);
|
||
// The conditions are untouched: the same boundary points, tangent and curvature.
|
||
check_side(&body, &a, Continuity::G2, |i| ((s[i], 0.0), (0.0, 1.0)), r);
|
||
check_side(&body, &b, Continuity::G2, |i| ((s[i], 1.0), (0.0, -1.0)), r);
|
||
}
|
||
let steps: Vec<f64> = middles.windows(2).map(|w| w[1] - w[0]).collect();
|
||
assert!(steps.iter().all(|d| d.abs() > 1e-9), "a fuller crown moves the middle: {middles:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_patch_refuses_what_it_cannot_hold() {
|
||
let n = 9;
|
||
let (a, b) = two_walls(n, 10.0);
|
||
let two = vec![Boundary { frames: a.clone(), order: Continuity::G2 }, Boundary { frames: b.clone(), order: Continuity::G2 }];
|
||
// Not 2 to 4 boundaries.
|
||
assert!(boundary_patch(&[], 0.0, n).is_err());
|
||
assert!(boundary_patch(&two[..1], 0.0, n).is_err());
|
||
let five = vec![two[0].clone(), two[1].clone(), two[0].clone(), two[1].clone(), two[0].clone()];
|
||
assert!(boundary_patch(&five, 0.0, n).is_err());
|
||
// Three or four sides are refused: a tensor-product patch cannot hold two sides' data at a
|
||
// corner, which needs a Gregory-type corner (issue #763). The message says so.
|
||
let square = square_loop(n);
|
||
let e = boundary_patch(&square, 0.0, n).unwrap_err().to_string();
|
||
assert!(e.contains("not supported yet") && e.contains("#763"), "4 sides: {e}");
|
||
// Three sides that do close: a triangle, whose last side runs from the second one's end back to
|
||
// the first one's start.
|
||
let triangle: Vec<Boundary> = vec![
|
||
square[0].clone(),
|
||
square[1].clone(),
|
||
Boundary {
|
||
frames: stations(n)
|
||
.into_iter()
|
||
.map(|t| Frame {
|
||
p: Vec3::new(20.0 * (1.0 - t), 20.0 * (1.0 - t), 0.0),
|
||
leave: Vec3::new(-std::f64::consts::FRAC_1_SQRT_2, -std::f64::consts::FRAC_1_SQRT_2, 0.0),
|
||
bend: Vec3::ZERO,
|
||
bend_change: Vec3::ZERO,
|
||
})
|
||
.collect(),
|
||
order: Continuity::G2,
|
||
},
|
||
];
|
||
let e = boundary_patch(&triangle, 0.0, n).unwrap_err().to_string();
|
||
assert!(e.contains("not supported yet") && e.contains("#763"), "3 sides: {e}");
|
||
// A loop whose sides do not even meet is refused earlier and for its own reason.
|
||
let broken = square_loop(n);
|
||
let mut moved = broken[1].frames.clone();
|
||
moved[0] = Frame { p: Vec3::new(30.0, 0.0, 0.0), ..moved[0] };
|
||
let broken = vec![broken[0].clone(), Boundary { frames: moved, order: Continuity::G2 }, broken[2].clone()];
|
||
let e = boundary_patch(&broken, 0.0, n).unwrap_err().to_string();
|
||
assert!(e.contains("run on into one another"), "{e}");
|
||
// The stations and the crown.
|
||
let short = vec![Boundary { frames: a[..n - 1].to_vec(), order: Continuity::G2 }, two[1].clone()];
|
||
assert!(boundary_patch(&short, 0.0, n).is_err(), "a boundary with the wrong number of stations");
|
||
assert!(boundary_patch(&two, 0.0, n - 1).is_err());
|
||
assert!(boundary_patch(&two, 0.0, 1).is_err());
|
||
for crown in [-1.5, 3.5, f64::NAN, f64::INFINITY] {
|
||
assert!(boundary_patch(&two, crown, n).is_err(), "crown {crown}");
|
||
}
|
||
// A station that is not finite, and a station with no leave direction.
|
||
for field in 0..4 {
|
||
let mut bad = two.clone();
|
||
let mut frames = bad[0].frames.clone();
|
||
frames[3] = match field {
|
||
0 => Frame { p: Vec3::new(f64::NAN, 0.0, 0.0), ..frames[3] },
|
||
1 => Frame { leave: Vec3::new(f64::NAN, 0.0, 0.0), ..frames[3] },
|
||
2 => Frame { bend: Vec3::new(f64::INFINITY, 0.0, 0.0), ..frames[3] },
|
||
_ => Frame { bend_change: Vec3::new(f64::NAN, 0.0, 0.0), ..frames[3] },
|
||
};
|
||
if let Some(b) = bad.first_mut() {
|
||
b.frames = frames;
|
||
}
|
||
assert!(boundary_patch(&bad, 0.0, n).is_err(), "a station that is not finite: field {field}");
|
||
}
|
||
let mut flat = two.clone();
|
||
if let Some(b) = flat.first_mut() {
|
||
b.frames = vec![Frame { p: Vec3::new(0.0, 1.0, 0.0), leave: Vec3::ZERO, bend: Vec3::ZERO, bend_change: Vec3::ZERO }; n];
|
||
}
|
||
assert!(boundary_patch(&flat, 0.0, n).is_err(), "a station with no leave direction");
|
||
let point = vec![
|
||
Boundary {
|
||
frames: vec![Frame { p: Vec3::new(1.0, 1.0, 1.0), leave: Vec3::X, bend: Vec3::ZERO, bend_change: Vec3::ZERO }; n],
|
||
order: Continuity::G1,
|
||
},
|
||
Boundary {
|
||
frames: vec![Frame { p: Vec3::new(1.0, 1.0, 1.0), leave: Vec3::X, bend: Vec3::ZERO, bend_change: Vec3::ZERO }; n],
|
||
order: Continuity::G1,
|
||
},
|
||
];
|
||
assert!(boundary_patch(&point, 0.0, n).is_err(), "a boundary with no length");
|
||
}
|