461 lines
25 KiB
Rust
461 lines
25 KiB
Rust
//! Tests of the multi-section blend: analytic cross-sections given as frames, and the surface
|
||
//! measured back off the face it built (a grid of points, five-point differences along the run).
|
||
|
||
use super::*;
|
||
use crate::body::from_p3;
|
||
use crate::bridge::{bridge, sample_grid};
|
||
use truck_modeling as mt;
|
||
use truck_modeling::ParametricSurface;
|
||
|
||
/// The blend's surface: it is one face.
|
||
fn surface_of(body: &Body) -> mt::Surface {
|
||
body.solid.face_iter().next().unwrap().surface()
|
||
}
|
||
|
||
/// Position and the run direction's first three derivatives at `(t, v)` of the face, read with the
|
||
/// five-point differences `bridge_tests` reads a cross-section with. The samples run inwards from
|
||
/// the section (so nothing is asked of the face outside its own domain); `backwards` reads with
|
||
/// the parameter falling and turns the odd derivatives round, so every value is a derivative in
|
||
/// the run direction `u`.
|
||
fn run_derivs(body: &Body, t: f64, v: f64, backwards: bool, h: f64) -> (Vec3, Vec3, Vec3, Vec3) {
|
||
let face = body.solid.face_iter().next().unwrap();
|
||
let surf = face.surface();
|
||
let dir = if backwards { -1.0 } else { 1.0 };
|
||
let q: Vec<Vec3> = (0..5).map(|k| from_p3(surf.subs(t + dir * k as f64 * h, v))).collect();
|
||
let d1 = (q[0] * -25.0 + q[1] * 48.0 + q[2] * -36.0 + q[3] * 16.0 + q[4] * -3.0) * (dir / (12.0 * h));
|
||
let d2 = (q[0] * 35.0 + q[1] * -104.0 + q[2] * 114.0 + q[3] * -56.0 + q[4] * 11.0) * (1.0 / (12.0 * h * h));
|
||
let d3 = (q[0] * -5.0 + q[1] * 18.0 + q[2] * -24.0 + q[3] * 14.0 + q[4] * -3.0) * (dir / (2.0 * h * h * h));
|
||
(q[0], d1, d2, d3)
|
||
}
|
||
|
||
/// The geometric shape of a curve from its first three derivatives: the unit tangent, the
|
||
/// curvature vector and the arclength derivative of that curvature vector (`bridge`'s own
|
||
/// reading, kept local to the test).
|
||
fn curve_shape(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)))
|
||
}
|
||
|
||
/// 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()
|
||
}
|
||
|
||
/// A circle of radius `r` at height `z` about the run (the z axis), sampled at `n` stations. Each
|
||
/// frame leaves along `leave`; its `bend` is given per station as a function of the station's
|
||
/// outward radial direction (a support surface curving in the leave direction), and its change of
|
||
/// curvature is `change`.
|
||
fn circle_section(r: f64, z: f64, n: usize, leave: Vec3, bend: impl Fn(Vec3) -> Vec3, change: Vec3) -> Vec<Frame> {
|
||
(0..n)
|
||
.map(|s| {
|
||
let a = std::f64::consts::TAU * s as f64 / n as f64;
|
||
let p = Vec3::new(r * a.cos(), r * a.sin(), z);
|
||
Frame { p, leave, bend: bend(Vec3::new(p.x / r, p.y / r, 0.0)), bend_change: change }
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// `count` straight cross-sections along x, `10` apart in z and rising in order: the run is +z.
|
||
/// Every frame leaves along +z except the last one, which leaves back into the blend along −z as
|
||
/// a bridge's far boundary does. `bend` and `change` are the same at every station, and `bend` is
|
||
/// perpendicular to the run (the only shape a support surface can have along a direction it does
|
||
/// not bend around).
|
||
fn line_sections(count: usize, n: usize, bend: Vec3, change: Vec3) -> Vec<Vec<Frame>> {
|
||
(0..count)
|
||
.map(|i| {
|
||
let leave = if i + 1 == count { Vec3::new(0.0, 0.0, -1.0) } else { Vec3::new(0.0, 0.0, 1.0) };
|
||
(0..n).map(|s| Frame { p: Vec3::new(10.0 * s as f64 / (n - 1) as f64, 0.0, 10.0 * i as f64), leave, bend, bend_change: change }).collect()
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// A point at arclength `s` along the perimeter of the rounded rectangle of half-width `a`,
|
||
/// half-height `b` and corner radius `r` (in the z = 0 plane, counter-clockwise from the right
|
||
/// edge's lower end).
|
||
fn rounded_rect_at(a: f64, b: f64, r: f64, s: f64) -> Vec3 {
|
||
let (sa, sb) = ((a - r).max(0.0), (b - r).max(0.0));
|
||
let quarter = std::f64::consts::FRAC_PI_2;
|
||
let segs = [2.0 * sb, quarter * r, 2.0 * sa, quarter * r, 2.0 * sb, quarter * r, 2.0 * sa, quarter * r];
|
||
let mut left = s;
|
||
let mut k = 0;
|
||
while k + 1 < segs.len() && left > segs[k] {
|
||
left -= segs[k];
|
||
k += 1;
|
||
}
|
||
let f = if segs[k] > 0.0 { left / segs[k] } else { 0.0 };
|
||
let arc = |centre: Vec3, from: f64| {
|
||
let ang = from + quarter * f;
|
||
Vec3::new(centre.x + r * ang.cos(), centre.y + r * ang.sin(), 0.0)
|
||
};
|
||
match k {
|
||
0 => Vec3::new(a, -sb + 2.0 * sb * f, 0.0),
|
||
1 => arc(Vec3::new(sa, sb, 0.0), 0.0),
|
||
2 => Vec3::new(sa - 2.0 * sa * f, b, 0.0),
|
||
3 => arc(Vec3::new(-sa, sb, 0.0), quarter),
|
||
4 => Vec3::new(-a, sb - 2.0 * sb * f, 0.0),
|
||
5 => arc(Vec3::new(-sa, -sb, 0.0), std::f64::consts::PI),
|
||
6 => Vec3::new(-sa + 2.0 * sa * f, -b, 0.0),
|
||
_ => arc(Vec3::new(sa, -sb, 0.0), 3.0 * quarter),
|
||
}
|
||
}
|
||
|
||
/// A rounded rectangle at height `z`, sampled at `n` even arclength steps around it: a section
|
||
/// that is not a scaled copy of the circles it can sit between, with a plan support (no bend).
|
||
fn rounded_rect_section(a: f64, b: f64, r: f64, z: f64, n: usize) -> Vec<Frame> {
|
||
let total = 4.0 * ((a - r).max(0.0) + (b - r).max(0.0)) + 2.0 * std::f64::consts::PI * r;
|
||
(0..n)
|
||
.map(|s| {
|
||
let p = rounded_rect_at(a, b, r, total * s as f64 / n as f64);
|
||
Frame { p: Vec3::new(p.x, p.y, z), leave: Vec3::new(0.0, 0.0, 1.0), bend: Vec3::ZERO, bend_change: Vec3::ZERO }
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Two parallel circular cylinders of radius `r` 20 apart in y, leaving towards each other along
|
||
/// ∓y, each bending up towards its own axis and with a curvature that does not change as it goes
|
||
/// round (`bridge_tests`'s own case, for the two-section comparison).
|
||
fn valley_between_cylinders(count: usize, r: f64) -> (Vec<Frame>, Vec<Frame>) {
|
||
let mut a = Vec::new();
|
||
let mut b = Vec::new();
|
||
for i in 0..count {
|
||
let y = 10.0 * i as f64 / (count - 1) as f64;
|
||
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)
|
||
}
|
||
|
||
/// Three circles of different sizes at three heights, the middle one smaller: a shape that
|
||
/// changes as the face runs up it.
|
||
fn three_circles(n: usize) -> Vec<Vec<Frame>> {
|
||
vec![
|
||
// The first section is the equator of a sphere of its own radius: the support bends
|
||
// inwards by 1/r along the run, and its curvature does not change as it goes round.
|
||
circle_section(10.0, 0.0, n, Vec3::new(0.0, 0.0, 1.0), |radial| radial * -0.1, Vec3::new(0.0, 0.0, -0.01)),
|
||
// The middle section's own shape data are ignored: the blend reads it from its neighbours.
|
||
circle_section(8.0, 10.0, n, Vec3::new(0.0, 0.0, 1.0), |_| Vec3::ZERO, Vec3::ZERO),
|
||
// The last section's frames leave back into the blend, along −z.
|
||
circle_section(12.0, 20.0, n, Vec3::new(0.0, 0.0, -1.0), |_| Vec3::ZERO, Vec3::ZERO),
|
||
]
|
||
}
|
||
|
||
/// Every station of every section is on the face, within the tolerance a blend promises.
|
||
fn assert_runs_through(body: &Body, sections: &[Vec<Frame>], n: usize) {
|
||
let grid = sample_grid(body, sections.len(), n).unwrap();
|
||
for (k, section) in sections.iter().enumerate() {
|
||
for (s, f) in section.iter().enumerate() {
|
||
assert!(grid[k][s].dist(f.p) < 1e-9, "section {k} station {s}: {:?} vs {:?}", grid[k][s], f.p);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn three_sections_run_through_every_station() {
|
||
let n = 9;
|
||
let sections = three_circles(n);
|
||
let body = loft_blend(§ions, [Continuity::G2, Continuity::G2], [0.0, 0.0], n).unwrap();
|
||
assert_runs_through(&body, §ions, n);
|
||
|
||
// The ends hold what their frames ask for: the leave direction is the tangent, and the
|
||
// support's own curvature (1/10 inwards on the sphere, nothing on the plane) is the face's.
|
||
for s in [0usize, 4, 8] {
|
||
let v = s as f64 / (n - 1) as f64;
|
||
// The stations run round the circle at even angles.
|
||
let angle = std::f64::consts::TAU * s as f64 / n as f64;
|
||
let radial = Vec3::new(angle.cos(), angle.sin(), 0.0);
|
||
let (_, d1, d2, d3) = run_derivs(&body, 0.0, v, false, 1e-3);
|
||
let (t, bend, _) = curve_shape(d1, d2, d3);
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, 1.0)) < 0.01, "the face leaves along +z: {t:?}");
|
||
assert!(bend.dist(radial * -0.1) < 1e-4, "the face leaves the sphere with its curvature: {bend:?}");
|
||
// The last section's frames leave back into the blend, so read the shape that way round.
|
||
let (_, d1, d2, d3) = run_derivs(&body, 1.0, v, true, 1e-3);
|
||
let (t, bend, _) = curve_shape(-d1, d2, -d3);
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, -1.0)) < 0.01, "and arrives along the far leave: {t:?}");
|
||
assert!(bend.len() < 1e-3, "the last section's support is flat: {bend:?}");
|
||
}
|
||
|
||
// Three sections: the one interior joint has no fourth station to take a third difference
|
||
// over, so it holds position, tangent and curvature — C² — and both sides agree.
|
||
for s in [0usize, 4, 8] {
|
||
let v = s as f64 / (n - 1) as f64;
|
||
let (from_left, left1, left2, left3) = run_derivs(&body, 0.5, v, false, 1e-4);
|
||
let (from_right, right1, right2, right3) = run_derivs(&body, 0.5, v, true, 1e-4);
|
||
assert!(from_left.dist(from_right) < 1e-9, "the joint is on the middle section");
|
||
assert!((left1 - right1).len() < 1e-7 * (1.0 + left1.len()), "the tangent is continuous: {left1:?} vs {right1:?}");
|
||
assert!((left2 - right2).len() < 1e-4 * (1.0 + left2.len()), "the curvature is continuous: {left2:?} vs {right2:?}");
|
||
// But the change of curvature is not: a three-point stencil carries no third difference,
|
||
// so the joint is C² and not C³, and the difference stays whatever the step size is.
|
||
assert!((left3 - right3).len() > 0.05 * (1.0 + left3.len()), "a C² joint is not C³: {left3:?} vs {right3:?}");
|
||
// What it holds is the sections' own finite differences along the run: the secant of the
|
||
// two neighbours for the tangent, their second difference for the curvature.
|
||
let p = |k: usize| sections[k][s].p;
|
||
let secant = p(2) - p(0);
|
||
let second = (p(2) - p(1) * 2.0 + p(0)) * 4.0;
|
||
assert!((left1 - secant).len() < 1e-7 * (1.0 + secant.len()), "the tangent is the neighbours' secant: {left1:?} vs {secant:?}");
|
||
assert!((left2 - second).len() < 1e-4 * (1.0 + second.len()), "the curvature is the neighbours' second difference: {left2:?} vs {second:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_ends_hold_the_shape_their_frames_ask_for() {
|
||
let stations = 7;
|
||
let bend = Vec3::new(0.0, 0.2, 0.0);
|
||
let change = Vec3::new(0.0, 0.5, 0.0);
|
||
let sections = line_sections(3, stations, bend, change);
|
||
let z = |s: f64| s * 10.0;
|
||
|
||
// G1 holds the tangent and nothing more.
|
||
let g1 = loft_blend(§ions, [Continuity::G1, Continuity::G1], [0.0, 0.0], stations).unwrap();
|
||
for (v, x) in [(0.0, 0.0), (0.5, 5.0), (1.0, 10.0)] {
|
||
let (p, d1, d2, d3) = run_derivs(&g1, 0.0, v, false, 1e-3);
|
||
let (t, _, _) = curve_shape(d1, d2, d3);
|
||
assert!(p.dist(Vec3::new(x, 0.0, 0.0)) < 1e-9, "the face starts on the first section: {p:?}");
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, 1.0)) < 0.01, "a G1 blend leaves along leave: {t:?}");
|
||
}
|
||
|
||
// G2 holds the tangent and the support's curvature, at both ends.
|
||
let g2 = loft_blend(§ions, [Continuity::G2, Continuity::G2], [0.0, 0.0], stations).unwrap();
|
||
for (v, x) in [(0.0, 0.0), (0.5, 5.0), (1.0, 10.0)] {
|
||
let (p, d1, d2, d3) = run_derivs(&g2, 0.0, v, false, 1e-3);
|
||
let (t, k, _) = curve_shape(d1, d2, d3);
|
||
assert!(p.dist(Vec3::new(x, 0.0, 0.0)) < 1e-9, "the face starts on the first section: {p:?}");
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, 1.0)) < 0.01, "a G2 blend leaves along leave: {t:?}");
|
||
assert!(k.dist(bend) / bend.len() < 1e-3, "a G2 blend holds the support's curvature: {k:?} vs {bend:?}");
|
||
let (p, d1, d2, d3) = run_derivs(&g2, 1.0, v, true, 1e-3);
|
||
let (t, k, c) = curve_shape(-d1, d2, -d3);
|
||
assert!(p.dist(Vec3::new(x, 0.0, z(2.0))) < 1e-9, "the face ends on the last section: {p:?}");
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, -1.0)) < 0.01, "reading back, it arrives along the far leave: {t:?}");
|
||
assert!(k.dist(bend) / bend.len() < 1e-3, "the far end holds the curvature too: {k:?}");
|
||
assert!(c.len() < 1e3);
|
||
}
|
||
// A G2 blend leaves the change of curvature free: moving the frames' own value does not move
|
||
// the face at the first section, while a G3 blend follows it.
|
||
let (_, d1, d2, d3) = run_derivs(&g2, 0.0, 0.5, false, 1e-3);
|
||
let free = curve_shape(d1, d2, d3).2;
|
||
let moved = line_sections(3, stations, bend, change + Vec3::new(0.0, 1.0, 0.0));
|
||
let g2_moved = loft_blend(&moved, [Continuity::G2, Continuity::G2], [0.0, 0.0], stations).unwrap();
|
||
let (_, d1, d2, d3) = run_derivs(&g2_moved, 0.0, 0.5, false, 1e-3);
|
||
assert!(curve_shape(d1, d2, d3).2.dist(free) < 1e-9, "a G2 blend does not read the far frame's change");
|
||
|
||
// G3 holds the change of curvature as well: its normal part is what a surface match can hold,
|
||
// and here the frame's own change is normal (along its curvature vector).
|
||
let g3 = loft_blend(§ions, [Continuity::G3, Continuity::G3], [0.0, 0.0], stations).unwrap();
|
||
for (v, x) in [(0.0, 0.0), (0.5, 5.0), (1.0, 10.0)] {
|
||
let (_, d1, d2, d3) = run_derivs(&g3, 0.0, v, false, 1e-3);
|
||
let (t, k, c) = curve_shape(d1, d2, d3);
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, 1.0)) < 0.01);
|
||
assert!(k.dist(bend) / bend.len() < 1e-3, "a G3 blend still holds the curvature: {k:?}");
|
||
assert!((c.y - change.y).abs() < 1e-3, "a G3 blend holds the change of curvature: {c:?} vs {change:?}");
|
||
let (p, d1, d2, d3) = run_derivs(&g3, 1.0, v, true, 1e-3);
|
||
let (t, k, c) = curve_shape(-d1, d2, -d3);
|
||
assert!(angle_deg(t, Vec3::new(0.0, 0.0, -1.0)) < 0.01);
|
||
assert!(k.dist(bend) / bend.len() < 1e-3);
|
||
assert!((c.y - change.y).abs() < 1e-3, "and the far end holds it too: {c:?} vs {change:?}");
|
||
assert!(p.dist(Vec3::new(x, 0.0, z(2.0))) < 1e-9);
|
||
}
|
||
// With G3, moving the frames' own value does move the face there.
|
||
let g3_moved = loft_blend(&moved, [Continuity::G3, Continuity::G3], [0.0, 0.0], stations).unwrap();
|
||
let (_, d1, d2, d3) = run_derivs(&g3_moved, 0.0, 0.5, false, 1e-3);
|
||
assert!((curve_shape(d1, d2, d3).2.y - free.y).abs() > 0.5, "a G3 blend follows the frame's change");
|
||
}
|
||
|
||
#[test]
|
||
fn four_sections_join_at_c3() {
|
||
let n = 9;
|
||
let sections: Vec<Vec<Frame>> = [(10.0, 0.0), (9.0, 10.0), (8.0, 20.0), (11.0, 30.0)]
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, &(r, z))| {
|
||
let leave = if i == 3 { Vec3::new(0.0, 0.0, -1.0) } else { Vec3::new(0.0, 0.0, 1.0) };
|
||
circle_section(r, z, n, leave, |_| Vec3::ZERO, Vec3::ZERO)
|
||
})
|
||
.collect();
|
||
let body = loft_blend(§ions, [Continuity::G3, Continuity::G3], [0.0, 0.0], n).unwrap();
|
||
assert_runs_through(&body, §ions, n);
|
||
|
||
// Every joint now has a fourth station for the third difference, so the two sides agree in
|
||
// the tangent, the curvature and its change: the joints are C³.
|
||
let h = 1.0 / 3.0;
|
||
for (i, s) in [(1usize, 0usize), (1, 4), (1, 8), (2, 0), (2, 4), (2, 8)] {
|
||
let t = i as f64 / 3.0;
|
||
let v = s as f64 / (n - 1) as f64;
|
||
let (_, left1, left2, left3) = run_derivs(&body, t, v, true, 1e-4);
|
||
let (_, right1, right2, right3) = run_derivs(&body, t, v, false, 1e-4);
|
||
assert!((left1 - right1).len() < 1e-7 * (1.0 + left1.len()), "joint {i} station {s}: the tangent agrees");
|
||
assert!((left2 - right2).len() < 1e-4 * (1.0 + left2.len()), "joint {i} station {s}: the curvature agrees");
|
||
// The change of curvature agrees too, as far as finite differences can tell: its
|
||
// mismatch falls away with the step size, so the joint is C³, not just C².
|
||
assert!(
|
||
(left3 - right3).len() < 2e-2 * (1.0 + left3.len()),
|
||
"joint {i} station {s}: the change of curvature agrees: {left3:?} vs {right3:?}"
|
||
);
|
||
// What they hold are the sections' own differences along the run.
|
||
let p = |k: usize| sections[k][s].p;
|
||
let d1 = (p(i + 1) - p(i - 1)) * (1.0 / (2.0 * h));
|
||
let d2 = (p(i + 1) - p(i) * 2.0 + p(i - 1)) * (1.0 / (h * h));
|
||
let d3 = if i == 1 {
|
||
(p(0) * -1.0 + p(1) * 3.0 - p(2) * 3.0 + p(3)) * (1.0 / (h * h * h))
|
||
} else {
|
||
(p(0) - p(1) * 3.0 + p(2) * 3.0 - p(3)) * (1.0 / (h * h * h))
|
||
};
|
||
assert!((left1 - d1).len() < 1e-7 * (1.0 + d1.len()), "the tangent is the secant: {left1:?} vs {d1:?}");
|
||
assert!((left2 - d2).len() < 1e-4 * (1.0 + d2.len()), "the curvature is the second difference: {left2:?} vs {d2:?}");
|
||
assert!((left3 - d3).len() < 2e-2 * (1.0 + d3.len()), "the change of curvature is the third difference: {left3:?} vs {d3:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_shape_that_is_not_a_scaled_copy_still_runs_through() {
|
||
let n = 17;
|
||
let sections = vec![
|
||
circle_section(10.0, 0.0, n, Vec3::new(0.0, 0.0, 1.0), |_| Vec3::ZERO, Vec3::ZERO),
|
||
rounded_rect_section(9.0, 7.0, 2.5, 10.0, n),
|
||
circle_section(8.0, 20.0, n, Vec3::new(0.0, 0.0, -1.0), |_| Vec3::ZERO, Vec3::ZERO),
|
||
];
|
||
let body = loft_blend(§ions, [Continuity::G1, Continuity::G1], [0.0, 0.0], n).unwrap();
|
||
assert_runs_through(&body, §ions, n);
|
||
|
||
// The interior joint is still smooth to C² and holds the neighbours' own estimates, whatever
|
||
// the sections' shapes are.
|
||
for s in [0usize, 4, 8, 12, 16] {
|
||
let v = s as f64 / (n - 1) as f64;
|
||
let (_, left1, left2, _) = run_derivs(&body, 0.5, v, false, 2e-4);
|
||
let (_, right1, right2, _) = run_derivs(&body, 0.5, v, true, 2e-4);
|
||
assert!((left1 - right1).len() < 1e-6 * (1.0 + left1.len()), "station {s}: the tangent is continuous");
|
||
assert!((left2 - right2).len() < 1e-6 * (1.0 + left2.len()), "station {s}: the curvature is continuous");
|
||
let p = |k: usize| sections[k][s].p;
|
||
let secant = p(2) - p(0);
|
||
let second = (p(2) - p(1) * 2.0 + p(0)) * 4.0;
|
||
assert!((left1 - secant).len() < 1e-6 * (1.0 + secant.len()), "station {s}: the tangent is the secant: {left1:?} vs {secant:?}");
|
||
assert!((left2 - second).len() < 1e-6 * (1.0 + second.len()), "station {s}: the curvature is the second difference: {left2:?} vs {second:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_two_section_blend_is_the_bridge() {
|
||
let (a, b) = valley_between_cylinders(9, 10.0);
|
||
let made = loft_blend(&[a.clone(), b.clone()], [Continuity::G2, Continuity::G2], [0.0, 0.0], 9).unwrap();
|
||
let other = bridge(&a, &b, [Continuity::G2, Continuity::G2], [0.0, 0.0]).unwrap();
|
||
// The blend runs with the sections, the bridge crosses from one boundary to the other: the
|
||
// same face, its two parameters the other way round.
|
||
for (run, station) in [(0.0, 0.0), (0.25, 0.3), (0.5, 0.7), (0.75, 0.1), (1.0, 1.0)] {
|
||
let x = from_p3(surface_of(&made).subs(run, station));
|
||
let y = from_p3(surface_of(&other).subs(station, run));
|
||
assert!(x.dist(y) < 1e-9, "run {run} station {station}: {x:?} vs {y:?}");
|
||
}
|
||
let grid = sample_grid(&other, 9, 7).unwrap();
|
||
for (row, frame) in grid.iter().zip(&a) {
|
||
assert!(row[0].dist(frame.p) < 1e-9);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn crown_shapes_the_middle_without_moving_the_joints() {
|
||
let n = 9;
|
||
let sections = three_circles(n);
|
||
let mut heights = Vec::new();
|
||
let mut joints = Vec::new();
|
||
for crown in [-0.5, 0.0, 1.0, 2.0] {
|
||
let body = loft_blend(§ions, [Continuity::G2, Continuity::G2], [crown, crown], n).unwrap();
|
||
// The interior joint holds the sections' own estimates whatever the crown does: those do
|
||
// not depend on it.
|
||
let (p, d1, d2, _) = run_derivs(&body, 0.5, 0.5, false, 2e-4);
|
||
assert!(p.dist(sections[1][4].p) < 1e-9, "the crown left the joint where the sections put it");
|
||
joints.push((d1, d2));
|
||
// Halfway up the first span is where a fuller crown reaches further.
|
||
heights.push(from_p3(surface_of(&body).subs(0.25, 0.5)).z);
|
||
assert_runs_through(&body, §ions, n);
|
||
}
|
||
for (d1, d2) in &joints {
|
||
let (base1, base2) = joints[0];
|
||
assert!((*d1 - base1).len() < 1e-4 * (1.0 + base1.len()), "the crown left the joint's tangent alone: {d1:?}");
|
||
assert!((*d2 - base2).len() < 1e-4 * (1.0 + base2.len()), "the crown left the joint's curvature alone: {d2:?}");
|
||
}
|
||
let steps: Vec<f64> = heights.windows(2).map(|w| w[1] - w[0]).collect();
|
||
assert!(steps.iter().all(|d| d.abs() > 1e-6 && d.signum() == steps[0].signum()), "a fuller crown moves the middle one way: {heights:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_blend_refuses_what_it_cannot_hold() {
|
||
let n = 5;
|
||
let orders = [Continuity::G1, Continuity::G1];
|
||
let good = three_circles(n);
|
||
assert!(loft_blend(&good, orders, [0.0, 0.0], n).is_ok());
|
||
|
||
// Fewer than two sections, and more than the cap.
|
||
assert!(loft_blend(&good[..1], orders, [0.0, 0.0], n).is_err(), "one section is no blend");
|
||
let many: Vec<Vec<Frame>> =
|
||
(0..=MAX_BLEND_SECTIONS).map(|i| circle_section(10.0, i as f64, 3, Vec3::new(0.0, 0.0, 1.0), |_| Vec3::ZERO, Vec3::ZERO)).collect();
|
||
assert!(loft_blend(&many, orders, [0.0, 0.0], 3).is_err(), "more than {MAX_BLEND_SECTIONS} sections");
|
||
|
||
// Station counts that do not match each other, or the count asked for.
|
||
let mut unequal = good.clone();
|
||
unequal[1] = unequal[1][..n - 1].to_vec();
|
||
assert!(loft_blend(&unequal, orders, [0.0, 0.0], n).is_err(), "unequal station counts");
|
||
assert!(loft_blend(&good, orders, [0.0, 0.0], n + 1).is_err(), "the samples must match the sections");
|
||
for samples in [0usize, 1, MAX_BLEND_STATIONS + 1] {
|
||
assert!(loft_blend(&good, orders, [0.0, 0.0], samples).is_err(), "samples {samples}");
|
||
}
|
||
|
||
// A crown outside its range, and one that is not a number.
|
||
for crown in [f64::NAN, 5.0, -1.0, f64::INFINITY] {
|
||
assert!(loft_blend(&good, orders, [crown, crown], n).is_err(), "crown {crown}");
|
||
}
|
||
assert!(loft_blend(&good, orders, [0.0, f64::NAN], n).is_err());
|
||
|
||
// A frame that is not a number, wherever it is not, and a leave direction with no direction.
|
||
for pick in 0..4 {
|
||
let mut bad = good.clone();
|
||
let frame = bad[1][2];
|
||
bad[1][2] = match pick {
|
||
0 => Frame { p: Vec3::new(f64::NAN, 0.0, 0.0), ..frame },
|
||
1 => Frame { leave: Vec3::new(f64::INFINITY, 0.0, 0.0), ..frame },
|
||
2 => Frame { bend: Vec3::new(0.0, f64::NAN, 0.0), ..frame },
|
||
_ => Frame { bend_change: Vec3::new(0.0, 0.0, f64::NAN), ..frame },
|
||
};
|
||
assert!(loft_blend(&bad, orders, [0.0, 0.0], n).is_err(), "a non-finite frame {pick}");
|
||
}
|
||
let mut no_leave = good.clone();
|
||
no_leave[1][2].leave = Vec3::ZERO;
|
||
assert!(loft_blend(&no_leave, orders, [0.0, 0.0], n).is_err(), "a leave direction with no direction");
|
||
|
||
// Two sections that meet at a station: no room for a blend.
|
||
let mut meeting = good.clone();
|
||
meeting[1] = meeting[0].clone();
|
||
assert!(loft_blend(&meeting, orders, [0.0, 0.0], n).is_err(), "sections that meet");
|
||
let mut touching = good.clone();
|
||
touching[1][2].p = touching[0][2].p;
|
||
assert!(loft_blend(&touching, orders, [0.0, 0.0], n).is_err(), "a station where the sections meet");
|
||
|
||
// Every order from G0 to G3 builds, alone and together, and so does the largest blend.
|
||
for (near, far) in [
|
||
(Continuity::G0, Continuity::G0),
|
||
(Continuity::G1, Continuity::G0),
|
||
(Continuity::G0, Continuity::G3),
|
||
(Continuity::G3, Continuity::G3),
|
||
(Continuity::G2, Continuity::G3),
|
||
] {
|
||
let body = loft_blend(&good, [near, far], [0.0, 0.0], n).unwrap();
|
||
assert_runs_through(&body, &good, n);
|
||
}
|
||
let full: Vec<Vec<Frame>> =
|
||
(0..MAX_BLEND_SECTIONS).map(|i| circle_section(10.0, i as f64, 5, Vec3::new(0.0, 0.0, 1.0), |_| Vec3::ZERO, Vec3::ZERO)).collect();
|
||
let body = loft_blend(&full, [Continuity::G3, Continuity::G3], [0.0, 0.0], 5).unwrap();
|
||
assert_runs_through(&body, &full, 5);
|
||
}
|