mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
geom: a degenerate point's normal steps into the parameter interval
At a pole or apex `EvalSurface::eval` takes the normal from a step beside the point, trying (+h, +h) first. At the far end of an interval (a sphere's north pole at u = 1) that step lands past the end, where the extrapolated surface folds back over the pole, and the normal came out turned over: the pole vertex shaded dark with a light ring around it (ladder 01/005, every sphere and dome). Steps now stay inside the active interval of a spline or a revolution's profile. The pole test checked only that the normal was radial, either way; it now checks the side too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+28
-8
@@ -249,10 +249,15 @@ impl EvalSurface {
|
||||
if let Some(n) = n.normalized().filter(|_| n.len2() > 1e-20 * (du.len2() * dv.len2()).max(1e-300)) {
|
||||
return (p, n);
|
||||
}
|
||||
// Step into the parameter domain around the degenerate point.
|
||||
// Step into the parameter domain around the degenerate point: never past the end of an
|
||||
// interval, where the extrapolated surface folds back over the pole and its normal
|
||||
// points the other way.
|
||||
let h = 1e-6 * (1.0 + u.abs().max(v.abs()));
|
||||
let inside = |d: usize, t: f64, s: f64| self.interval(d).is_none_or(|(lo, hi)| (lo..=hi).contains(&(t + s * h)));
|
||||
for (su, sv) in [(1.0, 1.0), (-1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)] {
|
||||
if let Some((_, a, b)) = self.derivs(u + su * h, v + sv * h)
|
||||
if inside(0, u, su)
|
||||
&& inside(1, v, sv)
|
||||
&& let Some((_, a, b)) = self.derivs(u + su * h, v + sv * h)
|
||||
&& let Some(n) = self.oriented(a.cross(b)).normalized()
|
||||
{
|
||||
return (p, n);
|
||||
@@ -261,6 +266,20 @@ impl EvalSurface {
|
||||
(p, Vec3::Z)
|
||||
}
|
||||
|
||||
/// The active parameter interval along `d` (0: u, 1: v) of a spline, or of a revolution's
|
||||
/// profile; `None` where the parameter is unbounded (a plane, a revolution's angle).
|
||||
fn interval(&self, d: usize) -> Option<(f64, f64)> {
|
||||
match self {
|
||||
EvalSurface::Plane { .. } => None,
|
||||
EvalSurface::Nurbs { degree, knots, count, .. } => {
|
||||
Some((*knots.get(d)?.get(*degree.get(d)?)?, *knots.get(d)?.get(*count.get(d)?)?))
|
||||
}
|
||||
EvalSurface::Revolved { profile, swap, .. } => (d == usize::from(*swap))
|
||||
.then(|| Some((*profile.knots.get(profile.degree)?, *profile.knots.get(profile.ctrl.len())?)))
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror transforms reverse the cross product of the mapped derivatives.
|
||||
fn oriented(&self, n: Vec3) -> Vec3 {
|
||||
match self {
|
||||
@@ -276,7 +295,7 @@ impl EvalSurface {
|
||||
pub fn wrap(&self, uv: [f64; 2]) -> [f64; 2] {
|
||||
if let EvalSurface::Revolved { profile, swap, .. } = self {
|
||||
let d = usize::from(*swap);
|
||||
let range = (|| Some((*profile.knots.get(profile.degree)?, *profile.knots.get(profile.ctrl.len())?)))();
|
||||
let range = self.interval(d);
|
||||
let mut out = uv;
|
||||
if let (Some(r), Some(x)) = (range, out.get_mut(d))
|
||||
&& !(r.0..=r.1).contains(x)
|
||||
@@ -286,11 +305,10 @@ impl EvalSurface {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let EvalSurface::Nurbs { degree, knots, count, closed, .. } = self else { return uv };
|
||||
let range = |d: usize| Some((*knots.get(d)?.get(*degree.get(d)?)?, *knots.get(d)?.get(*count.get(d)?)?));
|
||||
let EvalSurface::Nurbs { closed, .. } = self else { return uv };
|
||||
let mut out = uv;
|
||||
for d in 0..2 {
|
||||
if let (true, Some(r)) = (closed[d], range(d))
|
||||
if let (true, Some(r)) = (closed[d], self.interval(d))
|
||||
&& !(r.0..=r.1).contains(&uv[d])
|
||||
{
|
||||
out[d] = uv::wrap_into(uv[d], r);
|
||||
@@ -639,8 +657,10 @@ mod tests {
|
||||
let (u, v) = (i as f64 / 10.0, j as f64);
|
||||
let (p, n) = s.eval(u, v);
|
||||
assert!((p.len() - 3.0).abs() < 1e-12, "{p:?}");
|
||||
// At the pole (u = 1) the normal comes from a step inside: within ~1e-6 rad.
|
||||
assert!(n.cross(p).len() < 3e-5, "normal {n:?} not radial at {p:?}");
|
||||
// S_u × S_v points into this sphere. At the pole (u = 1) the normal comes from
|
||||
// a step inside: within ~1e-6 rad, and the same way (a step past the pole would
|
||||
// turn it over).
|
||||
assert!(n.cross(p).len() < 3e-5 && n.dot(p) < 0.0, "normal {n:?} not inward at {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user