use axiolid_construct::feature::BlendCorner;
use axiolid_construct::fillet_variable::tapered_blend_surface;
use axiolid_core::{Point2, Point3};
fn evaluate(net: &[Vec<Point3>], weights: &[Vec<f64>], u: f64, v: f64) -> Point3 {
let bu = [(1.0 - u) * (1.0 - u), 2.0 * u * (1.0 - u), u * u];
let bv = [1.0 - v, v];
let mut num = [0.0f64; 3];
let mut den = 0.0f64;
for (i, bu_i) in bu.iter().enumerate() {
for (j, bv_j) in bv.iter().enumerate() {
let w = bu_i * bv_j * weights[i][j];
num[0] += w * net[i][j].x;
num[1] += w * net[i][j].y;
num[2] += w * net[i][j].z;
den += w;
}
}
Point3::new(num[0] / den, num[1] / den, num[2] / den)
}
#[test]
fn blend_matches_closed_form() {
let theta: f64 = std::f64::consts::FRAC_PI_2;
let half = theta / 2.0;
let bottom_radius = 0.30_f64;
let top_radius = 0.70_f64;
let height = 2.0_f64;
let corner = Point2::new(0.0, 0.0);
let centre_at = |r: f64| Point2::new(r / half.sin(), 0.0);
let bottom = BlendCorner {
centre: centre_at(bottom_radius),
start: Point2::new(
bottom_radius / half.tan() * half.cos(),
bottom_radius / half.tan() * half.sin(),
),
end: Point2::new(
bottom_radius / half.tan() * half.cos(),
-bottom_radius / half.tan() * half.sin(),
),
sweep: std::f64::consts::PI - theta,
};
let top = BlendCorner {
centre: centre_at(top_radius),
start: Point2::new(
top_radius / half.tan() * half.cos(),
top_radius / half.tan() * half.sin(),
),
end: Point2::new(
top_radius / half.tan() * half.cos(),
-top_radius / half.tan() * half.sin(),
),
sweep: std::f64::consts::PI - theta,
};
let _ = corner;
let surface = tapered_blend_surface(&bottom, &top, height).expect("tapered blend");
let weights = surface.weights.clone().expect("rational");
let mut worst = 0.0_f64;
for vi in 0..=8 {
let v = f64::from(vi) / 8.0;
let radius = bottom_radius + (top_radius - bottom_radius) * v;
let centre = centre_at(radius);
for ui in 0..=8 {
let u = f64::from(ui) / 8.0;
let point = evaluate(&surface.control_points, &weights, u, v);
let dx = point.x - centre.x;
let dy = point.y - centre.y;
worst = worst.max((dx.hypot(dy) - radius).abs());
worst = worst.max((point.z - height * v).abs());
}
}
assert!(
worst < 1e-12,
"the tapered blend must equal the closed-form fillet, worst error {worst:e}"
);
}
fn wedge(theta: f64, radius: f64) -> BlendCorner {
let half = theta / 2.0;
let setback = radius / half.tan();
BlendCorner {
centre: Point2::new(radius / half.sin(), 0.0),
start: Point2::new(setback * half.cos(), setback * half.sin()),
end: Point2::new(setback * half.cos(), -setback * half.sin()),
sweep: std::f64::consts::PI - theta,
}
}
#[test]
fn blend_is_tangent_to_both_walls() {
for degrees in [60.0_f64, 90.0, 120.0, 150.0] {
let theta = degrees.to_radians();
let half = theta / 2.0;
let bottom_radius = 0.25_f64;
let top_radius = 0.55_f64;
let surface =
tapered_blend_surface(&wedge(theta, bottom_radius), &wedge(theta, top_radius), 1.5)
.expect("tapered blend");
let weights = surface.weights.clone().expect("rational");
let upper = (-half.sin(), half.cos());
let lower = (-half.sin(), -half.cos());
for vi in 0..=6 {
let v = f64::from(vi) / 6.0;
let radius = bottom_radius + (top_radius - bottom_radius) * v;
let start = evaluate(&surface.control_points, &weights, 0.0, v);
let end = evaluate(&surface.control_points, &weights, 1.0, v);
let d_start = (start.x * upper.0 + start.y * upper.1).abs();
let d_end = (end.x * lower.0 + end.y * lower.1).abs();
assert!(
d_start < 1e-12 && d_end < 1e-12,
"seams must lie on the wall planes at theta={degrees}, v={v}: {d_start:e} {d_end:e}"
);
let centre = (radius / half.sin(), 0.0);
let rs = (start.x - centre.0).hypot(start.y - centre.1);
assert!(
(rs - radius).abs() < 1e-12,
"seam must sit at radius {radius} from the centre, got {rs}"
);
}
}
}
#[test]
fn a_reversed_taper_is_accepted_symmetrically() {
let theta = std::f64::consts::FRAC_PI_2;
let growing = tapered_blend_surface(&wedge(theta, 0.2), &wedge(theta, 0.6), 1.0);
let shrinking = tapered_blend_surface(&wedge(theta, 0.6), &wedge(theta, 0.2), 1.0);
assert!(growing.is_ok() && shrinking.is_ok());
}
#[test]
fn a_constant_radius_taper_is_a_cylinder_section() {
let theta = std::f64::consts::FRAC_PI_2;
let radius = 0.4_f64;
let surface =
tapered_blend_surface(&wedge(theta, radius), &wedge(theta, radius), 1.0).expect("blend");
let weights = surface.weights.clone().expect("rational");
let centre = (radius / (theta / 2.0).sin(), 0.0);
for vi in 0..=4 {
let v = f64::from(vi) / 4.0;
for ui in 0..=4 {
let u = f64::from(ui) / 4.0;
let point = evaluate(&surface.control_points, &weights, u, v);
let r = (point.x - centre.0).hypot(point.y - centre.1);
assert!(
(r - radius).abs() < 1e-12,
"constant taper must stay at radius {radius}, got {r}"
);
}
}
}
#[test]
fn the_end_seam_is_the_start_rotated_by_the_sweep() {
let centre = Point2::new(1.3, -0.4);
let start = Point2::new(1.3 + 0.5, -0.4);
let sweep = 1.1_f64;
let bottom = BlendCorner {
centre,
start,
end: Point2::new(0.0, 0.0),
sweep,
};
let top = BlendCorner {
centre: Point2::new(1.3, -0.4),
start: Point2::new(1.3 + 0.9, -0.4),
end: Point2::new(0.0, 0.0),
sweep,
};
let surface = tapered_blend_surface(&bottom, &top, 1.0).expect("blend");
let weights = surface.weights.clone().expect("rational");
let (sin, cos) = sweep.sin_cos();
let radial = (start.x - centre.x, start.y - centre.y);
let expected = (
centre.x + radial.0 * cos - radial.1 * sin,
centre.y + radial.0 * sin + radial.1 * cos,
);
let got = evaluate(&surface.control_points, &weights, 1.0, 0.0);
let error = (got.x - expected.0).hypot(got.y - expected.1);
assert!(
error < 1e-12,
"u=1 must be the start rotated by the sweep, off by {error:e}"
);
}