use super::*;
use crate::{project_point_to_surface, project_point_to_surface_seeded};
use crate::{KernelRefusal, KernelStage, OrRefuse};
#[derive(Clone, Debug)]
pub(super) enum TripleCarrier<'a> {
Plane {
origin: Vec3,
normal: Vec3,
},
Ruled {
origin: Vec3,
axis: Vec3,
rho0: f64,
slope: f64,
},
Sphere {
center: Vec3,
radius: f64,
},
Torus {
origin: Vec3,
axis: Vec3,
major: f64,
minor: f64,
},
Patch(&'a NurbsSurface),
}
pub(super) fn carrier_kind(carrier: &TripleCarrier) -> &'static str {
match carrier {
TripleCarrier::Plane { .. } => "plane",
TripleCarrier::Ruled { .. } => "cylinder/cone",
TripleCarrier::Sphere { .. } => "sphere",
TripleCarrier::Torus { .. } => "torus",
TripleCarrier::Patch(_) => "fitted patch",
}
}
impl<'a> TripleCarrier<'a> {
pub(super) fn of(surface: &'a NurbsSurface) -> Result<Self, KernelRefusal> {
let Some(analytic) = surface.analytic() else {
return Ok(Self::Patch(surface));
};
Ok(match analytic {
AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => Self::Plane {
origin: *origin,
normal: u_dir.cross(*v_dir).normalized().or_refuse(KernelStage::Classify, "carrier_normal")?,
},
AnalyticSurface::RuledRevolution {
frame,
rho0,
rho1,
height,
} => {
if height.abs() <= PARALLEL_EPS {
return Ok(Self::Patch(surface));
}
Self::Ruled {
origin: frame.origin,
axis: frame.axis.normalized().or_refuse(KernelStage::Classify, "carrier_axis")?,
rho0: *rho0,
slope: (rho1 - rho0) / height,
}
}
AnalyticSurface::Sphere { frame, radius } => Self::Sphere {
center: frame.origin,
radius: *radius,
},
AnalyticSurface::Torus {
frame,
major_radius,
minor_radius,
} => Self::Torus {
origin: frame.origin,
axis: frame.axis.normalized().or_refuse(KernelStage::Classify, "carrier_axis")?,
major: *major_radius,
minor: *minor_radius,
},
AnalyticSurface::Revolution {
frame, generatrix, ..
} => {
let axis = frame.axis.normalized().or_refuse(KernelStage::Classify, "carrier_axis")?;
let Some((start, end)) = generatrix.straight_segment(PARALLEL_EPS) else {
return Ok(Self::Patch(surface));
};
let meridian = |point: Vec3| {
let delta = point.sub(frame.origin);
let axial = delta.dot(axis);
(delta.sub(axis.scale(axial)).length(), axial)
};
let (rho_start, axial_start) = meridian(start);
let (rho_end, axial_end) = meridian(end);
let rise = axial_end - axial_start;
if rise.abs() <= PARALLEL_EPS {
Self::Plane {
origin: start,
normal: axis,
}
} else {
let slope = (rho_end - rho_start) / rise;
Self::Ruled {
origin: frame.origin,
axis,
rho0: rho_start - slope * axial_start,
slope,
}
}
}
})
}
fn distance(
&self,
point: Vec3,
foot: &mut Option<(f64, f64)>,
) -> Result<(f64, Vec3, f64), KernelRefusal> {
const FOCAL: f64 = 1e-12;
match self {
Self::Plane { origin, normal } => {
let signed = point.sub(*origin).dot(*normal);
Ok((signed, *normal, signed.abs()))
}
Self::Ruled {
origin,
axis,
rho0,
slope,
} => {
let delta = point.sub(*origin);
let axial = delta.dot(*axis);
let radial = delta.sub(axis.scale(axial));
let rho = radial.length();
if rho <= FOCAL * (1.0 + axial.abs()) {
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"focal_axis",
"the point lies on the cylinder/cone axis",
));
}
let norm = (1.0 + slope * slope).sqrt();
let signed = (rho - rho0 - slope * axial) / norm;
let gradient = radial.scale(1.0 / rho).sub(axis.scale(*slope)).scale(1.0 / norm);
Ok((signed, gradient, signed.abs()))
}
Self::Sphere { center, radius } => {
let delta = point.sub(*center);
let length = delta.length();
if length <= FOCAL * (1.0 + radius) {
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"focal_centre",
"the point lies on the sphere's centre",
));
}
let signed = length - radius;
Ok((signed, delta.scale(1.0 / length), signed.abs()))
}
Self::Torus {
origin,
axis,
major,
minor,
} => {
let delta = point.sub(*origin);
let axial = delta.dot(*axis);
let radial = delta.sub(axis.scale(axial));
let rho = radial.length();
if rho <= FOCAL * (1.0 + major) {
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"focal_torus_axis",
"the point lies on the torus axis",
));
}
let across = rho - major;
let tube = (across * across + axial * axial).sqrt();
if tube <= FOCAL * (1.0 + minor) {
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"focal_tube",
"the point lies on the torus's tube circle",
));
}
let signed = tube - minor;
let gradient = radial
.scale(across / (rho * tube))
.add(axis.scale(axial / tube));
Ok((signed, gradient, signed.abs()))
}
Self::Patch(surface) => {
let projection = match *foot {
Some((u, v)) => project_point_to_surface_seeded(surface, point, u, v)
.or_refuse(KernelStage::Classify, "project")?,
None => project_point_to_surface(surface, point).or_refuse(KernelStage::Classify, "project")?,
};
*foot = Some((projection.u, projection.v));
let normal = surface
.normal(projection.u, projection.v)
.or_refuse(KernelStage::Classify, "normal")?
.normalized()
.or_refuse(KernelStage::Classify, "normal")?;
let offset = point.sub(projection.point);
Ok((offset.dot(normal), normal, offset.length()))
}
}
}
pub(super) fn signed_and_true_distance(
&self,
point: Vec3,
foot: &mut Option<(f64, f64)>,
) -> Result<(f64, f64), KernelRefusal> {
self.distance(point, foot).map(|(signed, _, distance)| (signed, distance))
}
pub(super) fn signed_distance(&self, point: Vec3) -> Result<f64, KernelRefusal> {
let mut foot = None;
self.distance(point, &mut foot).map(|(signed, _, _)| signed)
}
pub(super) fn normal_at(&self, point: Vec3) -> Result<Vec3, KernelRefusal> {
let mut foot = None;
self.distance(point, &mut foot).map(|(_, gradient, _)| gradient)
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct TriplePolicy {
pub(super) tolerance: f64,
pub(super) reach: f64,
pub(super) position_bar: f64,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct TriplePoint {
pub(super) point: Vec3,
pub(super) iterations: usize,
pub(super) residual: f64,
pub(super) sigma_min: f64,
pub(super) error_bound: f64,
pub(super) sensitivity: f64,
pub(super) seed_distance: f64,
}
#[derive(Clone, Debug)]
pub(super) enum TripleRefusal {
Unreadable { carrier: usize, reason: KernelRefusal },
Singular { iteration: usize, sigma_min: f64 },
Wandered { iteration: usize, distance: f64, reach: f64 },
NotConverged { iterations: usize, residual: f64, step: f64 },
OffPatch { carrier: usize, distance: f64 },
IllConditioned { sigma_min: f64, sensitivity: f64, bar: f64 },
}
impl TripleRefusal {
pub(super) fn describe(&self) -> String {
match self {
Self::Unreadable { carrier, reason } => {
format!("carrier {carrier} could not be read ({reason})")
}
Self::Singular { iteration, sigma_min } => format!(
"the three carriers' normals are dependent at iteration {iteration} \
(σ_min {sigma_min:.3e})"
),
Self::Wandered {
iteration,
distance,
reach,
} => format!(
"the solve left the seed's neighbourhood at iteration {iteration} \
({distance:.3e} from the seed, reach {reach:.3e})"
),
Self::NotConverged {
iterations,
residual,
step,
} => format!(
"the solve did not converge in {iterations} iterations (residual \
{residual:.3e}, last step {step:.3e})"
),
Self::OffPatch { carrier, distance } => format!(
"the point lies {distance:.3e} off fitted carrier {carrier}, beyond its patch"
),
Self::IllConditioned {
sigma_min,
sensitivity,
bar,
} => format!(
"the carriers are too near tangent to place the point: σ_min {sigma_min:.3e} \
moves it {sensitivity:.3e} per model tolerance, over the bar {bar:.3e}"
),
}
}
pub(super) fn into_refusal(self, wrap: impl FnOnce(&str) -> String) -> KernelRefusal {
let text = wrap(&self.describe());
match self {
Self::Unreadable { reason, .. } => reason.with_message(|_| text),
Self::Singular { .. } => KernelRefusal::ill_posed(KernelStage::Refine, "singular_normals", text),
Self::Wandered { .. } => KernelRefusal::non_convergence(KernelStage::Refine, "wandered", text),
Self::NotConverged { .. } => {
KernelRefusal::non_convergence(KernelStage::Refine, "newton_budget", text)
}
Self::OffPatch { .. } => KernelRefusal::unsupported(KernelStage::Refine, "off_patch", text),
Self::IllConditioned { .. } => {
KernelRefusal::ill_posed(KernelStage::Refine, "ill_conditioned", text)
}
}
}
}
const MAX_ITERATIONS: usize = 64;
fn determinant(rows: &[Vec3; 3]) -> f64 {
rows[0].dot(rows[1].cross(rows[2]))
}
pub(super) fn smallest_singular_value(rows: &[Vec3; 3]) -> f64 {
let column = |index: usize| -> Vec3 {
let pick = |row: &Vec3| match index {
0 => row.x,
1 => row.y,
_ => row.z,
};
Vec3::new(pick(&rows[0]), pick(&rows[1]), pick(&rows[2]))
};
let columns = [column(0), column(1), column(2)];
let a = |i: usize, j: usize| columns[i].dot(columns[j]);
let (a00, a11, a22, a01, a02, a12) = (a(0, 0), a(1, 1), a(2, 2), a(0, 1), a(0, 2), a(1, 2));
let off = a01 * a01 + a02 * a02 + a12 * a12;
let q = (a00 + a11 + a22) / 3.0;
let spread = (a00 - q).powi(2) + (a11 - q).powi(2) + (a22 - q).powi(2) + 2.0 * off;
let (largest, middle) = if spread <= 0.0 {
(q, q)
} else {
let p = (spread / 6.0).sqrt();
let b = |value: f64| value / p;
let (b00, b11, b22) = (b(a00 - q), b(a11 - q), b(a22 - q));
let (b01, b02, b12) = (b(a01), b(a02), b(a12));
let half_det = 0.5
* (b00 * (b11 * b22 - b12 * b12) - b01 * (b01 * b22 - b12 * b02)
+ b02 * (b01 * b12 - b11 * b02));
let phi = half_det.clamp(-1.0, 1.0).acos() / 3.0;
let largest = q + 2.0 * p * phi.cos();
let smallest = q + 2.0 * p * (phi + 2.0 * std::f64::consts::FRAC_PI_3).cos();
(largest, 3.0 * q - largest - smallest)
};
let product = (largest.max(0.0) * middle.max(0.0)).sqrt();
if product <= 0.0 {
return 0.0;
}
determinant(rows).abs() / product
}
pub(super) fn solve_triple_point(
carriers: [&TripleCarrier; 3],
seed: Vec3,
policy: &TriplePolicy,
) -> Result<TriplePoint, TripleRefusal> {
let mut point = seed;
let mut feet: [Option<(f64, f64)>; 3] = [None; 3];
let step_floor = (policy.tolerance * 1e-4).max(1e-15 * (1.0 + seed.length()));
let mut last_step = f64::INFINITY;
for iteration in 0..MAX_ITERATIONS {
let mut signed = [0.0f64; 3];
let mut rows = [Vec3::default(); 3];
for index in 0..3 {
let (distance, gradient, _) = carriers[index]
.distance(point, &mut feet[index])
.map_err(|reason| TripleRefusal::Unreadable {
carrier: index,
reason,
})?;
signed[index] = distance;
rows[index] = gradient;
}
let det = determinant(&rows);
if det.abs() <= 1e-14 {
return Err(TripleRefusal::Singular {
iteration,
sigma_min: smallest_singular_value(&rows),
});
}
let rhs = Vec3::new(-signed[0], -signed[1], -signed[2]);
let replace = |column: usize| -> [Vec3; 3] {
let mut replaced = rows;
for (row, value) in replaced.iter_mut().zip([rhs.x, rhs.y, rhs.z]) {
match column {
0 => row.x = value,
1 => row.y = value,
_ => row.z = value,
}
}
replaced
};
let step = Vec3::new(
determinant(&replace(0)) / det,
determinant(&replace(1)) / det,
determinant(&replace(2)) / det,
);
point = point.add(step);
last_step = step.length();
let wandered = point.sub(seed).length();
if !(wandered <= policy.reach) {
return Err(TripleRefusal::Wandered {
iteration,
distance: wandered,
reach: policy.reach,
});
}
if last_step <= step_floor {
return finish(carriers, point, seed, iteration + 1, &mut feet, policy);
}
}
let mut residual = 0.0f64;
for index in 0..3 {
if let Ok((_, _, distance)) = carriers[index].distance(point, &mut feet[index]) {
residual = residual.max(distance);
}
}
Err(TripleRefusal::NotConverged {
iterations: MAX_ITERATIONS,
residual,
step: last_step,
})
}
fn finish(
carriers: [&TripleCarrier; 3],
point: Vec3,
seed: Vec3,
iterations: usize,
feet: &mut [Option<(f64, f64)>; 3],
policy: &TriplePolicy,
) -> Result<TriplePoint, TripleRefusal> {
let mut residual = 0.0f64;
let mut rows = [Vec3::default(); 3];
for index in 0..3 {
let (_, gradient, distance) = carriers[index]
.distance(point, &mut feet[index])
.map_err(|reason| TripleRefusal::Unreadable {
carrier: index,
reason,
})?;
if matches!(carriers[index], TripleCarrier::Patch(_)) && distance > policy.tolerance {
return Err(TripleRefusal::OffPatch {
carrier: index,
distance,
});
}
residual = residual.max(distance);
rows[index] = gradient;
}
let sigma_min = smallest_singular_value(&rows);
if !(sigma_min > 0.0) {
return Err(TripleRefusal::Singular {
iteration: iterations,
sigma_min,
});
}
let sensitivity = 3f64.sqrt() * policy.tolerance / sigma_min;
if sensitivity > policy.position_bar {
return Err(TripleRefusal::IllConditioned {
sigma_min,
sensitivity,
bar: policy.position_bar,
});
}
Ok(TriplePoint {
point,
iterations,
residual,
sigma_min,
error_bound: 3f64.sqrt() * residual / sigma_min,
sensitivity,
seed_distance: point.sub(seed).length(),
})
}
pub(super) fn carrier_crossings(
curve: &NurbsCurve,
range: [f64; 2],
carrier: &TripleCarrier,
center: Vec3,
reach: f64,
) -> Vec<Vec3> {
const SAMPLES: usize = 256;
let [t0, t1] = range;
let height = |t: f64| -> Option<(f64, Vec3)> {
let point = curve.evaluate(t).ok()?;
carrier.signed_distance(point).ok().map(|value| (value, point))
};
let mut crossings: Vec<Vec3> = Vec::new();
let Some((mut previous_h, _)) = height(t0) else {
return crossings;
};
let mut previous_t = t0;
for index in 1..=SAMPLES {
let t = t0 + (t1 - t0) * index as f64 / SAMPLES as f64;
let Some((h, _)) = height(t) else {
previous_t = t;
continue;
};
if previous_h * h <= 0.0 && !(previous_h == 0.0 && index > 1) {
let (mut low, mut high, mut low_h) = (previous_t, t, previous_h);
for _ in 0..80 {
let mid = 0.5 * (low + high);
let Some((mid_h, _)) = height(mid) else {
break;
};
if low_h * mid_h <= 0.0 {
high = mid;
} else {
low = mid;
low_h = mid_h;
}
}
if let Ok(point) = curve.evaluate(0.5 * (low + high)) {
if point.sub(center).length() <= reach {
crossings.push(point);
}
}
}
previous_t = t;
previous_h = h;
}
crossings
}
pub(super) fn carrier_kinds(carriers: [&TripleCarrier; 3]) -> String {
format!(
"{} × {} × {}",
carrier_kind(carriers[0]),
carrier_kind(carriers[1]),
carrier_kind(carriers[2])
)
}