Thanks @jyviko.
This commit is contained in:
1 parent
77bdcc47e9
commit
fac3f96bdd
24 files changed
+2688
-77
No files matched your search
@@ -0,0 +1,191 @@
|
||||
use super::*;
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
const TOL: f64 = 1e-5;
|
||||
const ANGLE: f64 = 0.05 * PI / 180.0;
|
||||
|
||||
/// The surface at fractions `(x, y)` of its domain.
|
||||
fn at(s: &NurbsSurface, x: f64, y: f64) -> Vec3 {
|
||||
let [du, dv] = s.domain();
|
||||
s.eval((du[0] + (du[1] - du[0]) * x).min(du[1]), (dv[0] + (dv[1] - dv[0]) * y).min(dv[1])).unwrap()
|
||||
}
|
||||
|
||||
fn spec(points: &[Vec3]) -> FillSpec<'_> {
|
||||
FillSpec { tol: TOL, angle_tol: ANGLE, crown: 0.0, points }
|
||||
}
|
||||
|
||||
/// The radial and tangential unit vectors of a cylinder about z at angle `a`.
|
||||
fn radial(a: f64) -> Vec3 {
|
||||
Vec3::new(a.cos(), a.sin(), 0.0)
|
||||
}
|
||||
fn around(a: f64) -> Vec3 {
|
||||
Vec3::new(-a.sin(), a.cos(), 0.0)
|
||||
}
|
||||
|
||||
/// The open end of a tube about z (radius `r`, its wall below z = 0), one side: G1 (or G2) to
|
||||
/// the wall, which leaves straight up.
|
||||
fn tube_end(r: f64, g2: bool) -> impl FnMut(usize, &[f64]) -> Result<Vec<Station>, String> {
|
||||
move |_, ts| {
|
||||
Ok(ts
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let a = t * TAU;
|
||||
let across = Across { leave: Vec3::Z, normal: radial(a), bend: g2.then_some(Vec3::ZERO) };
|
||||
Station { p: radial(a) * r, up: around(a).cross(Vec3::Z), across: Some(across), corner: false, clash: false }
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A window in the wall of a cylinder about z (radius `r`): `a` either side of the x axis, `h`
|
||||
/// high, its four sides G1 to the wall around it (the same cylinder).
|
||||
fn window(r: f64, a: f64, h: f64) -> impl FnMut(usize, &[f64]) -> Result<Vec<Station>, String> {
|
||||
move |k, ts| {
|
||||
Ok(ts
|
||||
.iter()
|
||||
.map(|&t| {
|
||||
let (angle, z, tangent, leave) = match k {
|
||||
0 => (-a + 2.0 * a * t, 0.0, around(-a + 2.0 * a * t), Vec3::Z),
|
||||
1 => (a, h * t, Vec3::Z, around(a) * -1.0),
|
||||
2 => (a - 2.0 * a * t, h, around(a - 2.0 * a * t) * -1.0, Vec3::Z * -1.0),
|
||||
_ => (-a, h - h * t, Vec3::Z * -1.0, around(-a)),
|
||||
};
|
||||
let p = radial(angle) * r + Vec3::Z * z;
|
||||
let up = tangent.cross(leave);
|
||||
Station { p, up, across: Some(Across { leave, normal: radial(angle), bend: None }), corner: t == 0.0, clash: false }
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tube_end_capped_g1_meets_the_wall_and_rises_over_it() {
|
||||
for g2 in [false, true] {
|
||||
let f = fill_general(1, &mut tube_end(10.0, g2), &spec(&[])).unwrap();
|
||||
eprintln!(
|
||||
"tube end (g2 {g2}): {:?} spans, {} stations, off the rim {:e}, normals within {:.4}°, curvature mismatch {:e}",
|
||||
f.spans,
|
||||
f.stations,
|
||||
f.deviation,
|
||||
f.normal_deviation.to_degrees(),
|
||||
f.curvature_mismatch
|
||||
);
|
||||
assert!(f.deviation <= TOL && f.normal_deviation <= ANGLE);
|
||||
// The middle rises over the rim (the cap leaves the wall upwards).
|
||||
let mid = at(&f.surface, 0.5, 0.5);
|
||||
eprintln!("the cap's middle: {mid:?}");
|
||||
assert!(mid.z > 2.0 && mid.x.hypot(mid.y) < 1.0, "{mid:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_in_a_cylinder_wrapping_past_half_way_fills_close_to_the_cylinder() {
|
||||
// 240° around: no plane sees it as a height field.
|
||||
let (r, a, h) = (10.0, 2.0 * PI / 3.0, 10.0);
|
||||
let f = fill_general(4, &mut window(r, a, h), &spec(&[])).unwrap();
|
||||
eprintln!("window: {:?} spans, off {:e}, normals within {:.4}°", f.spans, f.deviation, f.normal_deviation.to_degrees());
|
||||
assert!(f.deviation <= TOL && f.normal_deviation <= ANGLE);
|
||||
// Inside, the fill stays near the cylinder it was cut from (it is the least-bending surface
|
||||
// with the cylinder's boundary data, not the cylinder itself).
|
||||
let mut worst = 0.0f64;
|
||||
for k in 1..20 {
|
||||
for j in 1..20 {
|
||||
let (ang, z) = (-a + 2.0 * a * k as f64 / 20.0, h * j as f64 / 20.0);
|
||||
// The fill's point nearest this cylinder point, by a coarse search.
|
||||
let target = radial(ang) * r + Vec3::Z * z;
|
||||
let mut best = f64::INFINITY;
|
||||
for u in 0..=120 {
|
||||
for v in 0..=120 {
|
||||
best = best.min(at(&f.surface, u as f64 / 120.0, v as f64 / 120.0).dist(target));
|
||||
}
|
||||
}
|
||||
worst = worst.max(best);
|
||||
}
|
||||
}
|
||||
eprintln!("window: the cylinder is within {worst} of the fill");
|
||||
assert!(worst < 0.25, "{worst}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_planar_polygon_is_its_plane_and_a_point_lifts_it() {
|
||||
// An L-shaped hexagon (one reflex corner) in z = 0, its neighbours standing up around it.
|
||||
let c = [[0.0, 0.0], [20.0, 0.0], [20.0, 10.0], [10.0, 10.0], [10.0, 20.0], [0.0, 20.0]];
|
||||
let mut stations = |k: usize, ts: &[f64]| -> Result<Vec<Station>, String> {
|
||||
let (Some(a), Some(b)) = (c.get(k), c.get((k + 1) % 6)) else { return Err("no side".into()) };
|
||||
let tangent = Vec3::new(b[0] - a[0], b[1] - a[1], 0.0).normalized().ok_or("no tangent")?;
|
||||
let inward = Vec3::Z.cross(tangent);
|
||||
Ok(ts
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let p = Vec3::new(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, 0.0);
|
||||
Station { p, up: Vec3::Z, across: Some(Across { leave: inward, normal: Vec3::Z, bend: None }), corner: *t == 0.0, clash: false }
|
||||
})
|
||||
.collect())
|
||||
};
|
||||
let f = fill_general(6, &mut stations, &spec(&[])).unwrap();
|
||||
let mut off = 0.0f64;
|
||||
for i in 0..=20 {
|
||||
for j in 0..=20 {
|
||||
off = off.max(at(&f.surface, i as f64 / 20.0, j as f64 / 20.0).z.abs());
|
||||
}
|
||||
}
|
||||
eprintln!("L: {:?} spans, the plane within {off:e}", f.spans);
|
||||
assert!(off < 1e-8, "{off}");
|
||||
// G0 with a point 3 above the inside of the L: the surface passes through it.
|
||||
let mut g0 = |k: usize, ts: &[f64]| stations(k, ts).map(|v| v.into_iter().map(|s| Station { across: None, ..s }).collect());
|
||||
let point = [Vec3::new(5.0, 5.0, 3.0)];
|
||||
let f = fill_general(6, &mut g0, &spec(&point)).unwrap();
|
||||
eprintln!("L through a point: {:?} spans, off the loop {:e}, off the point {:e}", f.spans, f.deviation, f.point_deviation);
|
||||
assert!(f.deviation <= TOL && f.point_deviation <= TOL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_loops_and_specs_are_refused() {
|
||||
let ok = |s: &FillSpec| fill_general(1, &mut tube_end(10.0, false), s);
|
||||
for (tol, angle, crown) in
|
||||
[(0.0, ANGLE, 0.0), (f64::NAN, ANGLE, 0.0), (TOL, -1.0, 0.0), (TOL, ANGLE, f64::NAN), (TOL, ANGLE, -0.95), (TOL, ANGLE, 1e300)]
|
||||
{
|
||||
assert!(ok(&FillSpec { tol, angle_tol: angle, crown, points: &[] }).is_err(), "{tol} {angle} {crown}");
|
||||
}
|
||||
assert!(ok(&spec(&[Vec3::new(f64::NAN, 0.0, 0.0)])).is_err());
|
||||
assert!(ok(&spec(&vec![Vec3::ZERO; MAX_FILL_POINTS + 1])).is_err());
|
||||
// A point not over the inside of the loop.
|
||||
assert!(ok(&spec(&[Vec3::new(30.0, 0.0, 0.0)])).is_err());
|
||||
assert!(fill_general(0, &mut tube_end(10.0, false), &spec(&[])).is_err());
|
||||
assert!(fill_general(10_000, &mut tube_end(10.0, false), &spec(&[])).is_err());
|
||||
assert!(fill_general(1, &mut |_, _| Err("no".into()), &spec(&[])).is_err());
|
||||
assert!(
|
||||
fill_general(
|
||||
1,
|
||||
&mut |_, ts| Ok(vec![Station { p: Vec3::ZERO, up: Vec3::Z, across: None, corner: false, clash: false }; ts.len()]),
|
||||
&spec(&[])
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
// A loop that runs out and straight back.
|
||||
let mut spike = |_: usize, ts: &[f64]| -> Result<Vec<Station>, String> {
|
||||
Ok(ts
|
||||
.iter()
|
||||
.map(|t| Station { p: Vec3::X * (1.0 - (2.0 * t - 1.0f64).abs()) * 10.0, up: Vec3::Z, across: None, corner: false, clash: false })
|
||||
.collect())
|
||||
};
|
||||
assert!(fill_general(1, &mut spike, &spec(&[])).is_err());
|
||||
// A figure eight: its development crosses itself.
|
||||
let mut eight = |_: usize, ts: &[f64]| -> Result<Vec<Station>, String> {
|
||||
Ok(ts
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let a = t * TAU;
|
||||
Station { p: Vec3::new(10.0 * a.sin(), 5.0 * (2.0 * a).sin(), 0.0), up: Vec3::Z, across: None, corner: false, clash: false }
|
||||
})
|
||||
.collect())
|
||||
};
|
||||
assert!(fill_general(1, &mut eight, &spec(&[])).is_err());
|
||||
// Huge and tiny loops do not panic.
|
||||
for scale in [1e300, 1e-300] {
|
||||
let mut s = |_: usize, ts: &[f64]| -> Result<Vec<Station>, String> {
|
||||
Ok(ts.iter().map(|t| Station { p: radial(t * TAU) * scale, up: Vec3::Z, across: None, corner: false, clash: false }).collect())
|
||||
};
|
||||
let _ = fill_general(1, &mut s, &spec(&[]));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user