Class-A surfacing 3/3: blend curves, curved-edge fillets, the G2 corner and continuity analysis (#1642)
Thanks @midasdf.
This commit is contained in:
1 parent
3126962cd4
commit
bbd4f29ebb
43 files changed
+4753
-132
No files matched your search
@@ -0,0 +1,154 @@
|
||||
//! What the G2 corner patch has to prove on a corner whose answer is known analytically: the box.
|
||||
|
||||
use super::*;
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
|
||||
use crate::body::v3;
|
||||
|
||||
/// One seam of a box corner: a quarter circle about `centre` from direction `u` to direction `w`,
|
||||
/// with `leave` the way the patch leaves the blend's surface. A fillet is flat along its own axis,
|
||||
/// so its curvature across the seam is nothing.
|
||||
fn seam(centre: Vec3, u: Vec3, w: Vec3, leave: Vec3, r: f64, n: usize) -> Vec<Frame> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let theta = FRAC_PI_2 * i as f64 / (n - 1) as f64;
|
||||
Frame { p: centre + (u * theta.cos() + w * theta.sin()) * r, leave, bend: Vec3::ZERO, bend_change: Vec3::ZERO }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A box corner at the origin (material x, y, z ≥ 0) with three fillets of radius `r`: the three
|
||||
/// section curves where the fillets stop, meeting at (0,r,r), (r,0,r) and (r,r,0), and the middle of
|
||||
/// the ball's octant a rolling ball would leave there.
|
||||
fn box_corner(r: f64, n: usize) -> (Vec<Vec<Frame>>, Vec3, Vec3) {
|
||||
let c = Vec3::new(r, r, r);
|
||||
let (a, b, d) = (Vec3::new(0.0, r, r), Vec3::new(r, 0.0, r), Vec3::new(r, r, 0.0));
|
||||
let dir = |p: Vec3| (p - c).normalized().unwrap_or(Vec3::X);
|
||||
let sides = vec![
|
||||
// A → B on the z edge's cylinder (flat along z), B → C on the x edge's, C → A on the y
|
||||
// edge's; each patch leaves the blend towards the material the corner caps.
|
||||
seam(c, dir(a), dir(b), Vec3::new(0.0, 0.0, -1.0), r, n),
|
||||
seam(c, dir(b), dir(d), Vec3::new(-1.0, 0.0, 0.0), r, n),
|
||||
seam(c, dir(d), dir(a), Vec3::new(0.0, -1.0, 0.0), r, n),
|
||||
];
|
||||
let apex = c - Vec3::new(1.0, 1.0, 1.0).normalized().unwrap_or(Vec3::Z) * r;
|
||||
(sides, apex, c)
|
||||
}
|
||||
|
||||
/// The rolling-ball corner the kernel builds today: the sphere through the same three seams, made
|
||||
/// the way `curvecorner` makes it (a meridian arc swept about the axis through the ball's centre).
|
||||
fn rolling_ball(r: f64, c: Vec3) -> mt::Surface {
|
||||
let axis = Vec3::new(1.0, 1.0, 1.0).normalized().unwrap_or(Vec3::Z);
|
||||
let side = axis.any_perp();
|
||||
let meridian = builder::circle_arc(&builder::vertex(p3(c + axis * r)), &builder::vertex(p3(c - axis * r)), p3(c + side * r));
|
||||
mt::Surface::RevolutedCurve(mt::Processor::new(mt::RevolutedCurve::by_revolution(meridian.oriented_curve(), p3(c), v3(axis))))
|
||||
}
|
||||
|
||||
/// The stations clear of the corners. Two blends meet at a corner with their seams 90° apart and
|
||||
/// ask for different curvature at that one point, which no single patch can hold, and the corner's
|
||||
/// own spline rounds the seam there; both effects fade within a few stations, so the measurement
|
||||
/// starts clear of them.
|
||||
fn interior(sides: &[Vec<Frame>]) -> Vec<Vec<Frame>> {
|
||||
sides.iter().map(|s| s.iter().skip(4).take(s.len().saturating_sub(8)).copied().collect()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_box_corner_is_curvature_continuous_with_every_blend() {
|
||||
let r = 2.0;
|
||||
let (sides, apex, centre) = box_corner(r, 25);
|
||||
let faces = corner_g2_patch(&sides, apex).unwrap();
|
||||
let face = faces.first().expect("a corner patch");
|
||||
let surface = face.oriented_surface();
|
||||
let inner = interior(&sides);
|
||||
let (gap, angle, curvature) = seam_error(&surface, &inner, 1.0 / r).unwrap();
|
||||
println!("G2 corner: gap {gap:e}, normal angle {angle:e} rad, curvature {curvature:e} (relative to 1/r)");
|
||||
assert!(gap <= r * 1e-5, "the patch is {gap} from the seam");
|
||||
// The stated tolerances: the normal angle within 0.01° and the curvature across the seam within
|
||||
// 1% of the blend's own (here the fillet is flat across the seam, so that is 1% of 1/r).
|
||||
assert!(angle < 0.01f64.to_radians(), "normal angle {angle} rad");
|
||||
assert!(curvature < 1e-2, "curvature across the seam is {curvature} of the blend's");
|
||||
|
||||
// The same measurement on the rolling-ball corner the kernel builds today: G1 (angle nought)
|
||||
// but the ball's own 1/r across each seam, which is the whole of the blend's curvature.
|
||||
let ball = rolling_ball(r, centre);
|
||||
let (ball_gap, ball_angle, ball_curvature) = seam_error(&ball, &inner, 1.0 / r).unwrap();
|
||||
println!("rolling ball: gap {ball_gap:e}, normal angle {ball_angle:e} rad, curvature {ball_curvature:e}");
|
||||
assert!(ball_angle < 0.01f64.to_radians(), "the ball is tangent to each blend");
|
||||
assert!(ball_curvature > 0.5, "the ball keeps its own curvature across the seam: {ball_curvature}");
|
||||
assert!(curvature * 20.0 < ball_curvature, "the G2 patch has to be far closer than the ball");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_corner_meets_three_blends_of_different_shape_apart() {
|
||||
// Blends that differ along their seams: a fillet of radius 2 next to rounder walls, each asking
|
||||
// for its own curvature across the seam, at its own direction.
|
||||
let r = 2.0;
|
||||
let (mut sides, apex, _) = box_corner(r, 33);
|
||||
let bends = [0.25, 0.4, 0.6];
|
||||
for (i, bend) in bends.iter().enumerate() {
|
||||
let Some(side) = sides.get_mut(i) else { continue };
|
||||
for f in side.iter_mut() {
|
||||
// Across the seam, perpendicular to both the leave direction and the seam.
|
||||
f.bend = f.leave.any_perp() * *bend;
|
||||
}
|
||||
}
|
||||
let faces = corner_g2_patch(&sides, apex).unwrap();
|
||||
let surface = faces.first().expect("a corner patch").oriented_surface();
|
||||
let inner = interior(&sides);
|
||||
let (gap, angle, curvature) = seam_error(&surface, &inner, bends.iter().copied().fold(0.0, f64::max)).unwrap();
|
||||
println!("three different blends: gap {gap:e}, angle {angle:e} rad, curvature {curvature:e}");
|
||||
assert!(gap <= r * 1e-6, "{gap}");
|
||||
assert!(angle < 0.01f64.to_radians(), "{angle}");
|
||||
assert!(curvature < 1e-2, "each blend's own curvature across the seam: {curvature}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_g2_corner_refuses_what_it_cannot_hold() {
|
||||
let r = 2.0;
|
||||
let (sides, apex, _) = box_corner(r, 9);
|
||||
assert!(corner_g2_patch(&sides, apex).is_ok());
|
||||
|
||||
// Two blends (the ball's patch still fills that corner), or one.
|
||||
assert!(corner_g2_patch(&sides[..2], apex).is_err());
|
||||
assert!(corner_g2_patch(&sides[..1], apex).is_err());
|
||||
assert!(corner_g2_patch(&[], apex).is_err());
|
||||
|
||||
// Too few stations to interpolate a cubic through, and too many to work with.
|
||||
let thin: Vec<Vec<Frame>> = sides.iter().map(|s| s[..3].to_vec()).collect();
|
||||
assert!(corner_g2_patch(&thin, apex).is_err());
|
||||
let fat: Vec<Vec<Frame>> = sides.iter().map(|s| s[..1].iter().cycle().take(MAX_STATIONS + 1).copied().collect()).collect();
|
||||
assert!(corner_g2_patch(&fat, apex).is_err());
|
||||
|
||||
// A vertex the construction cannot hold: the section curves do not meet at their corners.
|
||||
let mut apart = sides.clone();
|
||||
if let Some(first) = apart.first_mut().and_then(|s| s.first_mut()) {
|
||||
first.p += Vec3::new(0.5, 0.0, 0.0);
|
||||
}
|
||||
assert!(corner_g2_patch(&apart, apex).is_err());
|
||||
|
||||
// A section curve that runs along the seam, so the patch has no direction to leave it in.
|
||||
let along: Vec<Vec<Frame>> = sides
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.windows(2)
|
||||
.map(|w| {
|
||||
let d = w.first().zip(w.get(1)).map(|(a, b)| (b.p - a.p).normalized().unwrap_or(Vec3::X)).unwrap_or(Vec3::X);
|
||||
Frame { leave: d, ..w.first().copied().unwrap_or(s[0]) }
|
||||
})
|
||||
.chain(s.last().copied())
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
assert!(corner_g2_patch(&along, apex).is_err());
|
||||
|
||||
// Hostile numbers never panic.
|
||||
let mut nan = sides.clone();
|
||||
if let Some(f) = nan.first_mut().and_then(|s| s.get_mut(1)) {
|
||||
f.p = Vec3::new(f64::NAN, 0.0, 0.0);
|
||||
}
|
||||
assert!(corner_g2_patch(&nan, apex).is_err());
|
||||
assert!(corner_g2_patch(&sides, Vec3::new(f64::INFINITY, 0.0, 0.0)).is_err());
|
||||
// A corner with no room: every station on the apex.
|
||||
let on_apex: Vec<Vec<Frame>> = sides.iter().map(|s| s.iter().map(|f| Frame { p: apex, ..*f }).collect()).collect();
|
||||
assert!(corner_g2_patch(&on_apex, apex).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user