use super::*;
use crate::{KernelRefusal, KernelStage, OrRefuse};
const NODE_RANK_FLOOR: f64 = 1e-6;
const REFINE_ITERATIONS: usize = 60;
const PARAMETER_STALL: f64 = 1e-12;
const RESIDUAL_PROGRESS: f64 = 1e-3;
const REFINE_PATIENCE: usize = 4;
const LEVENBERG_DAMPING: f64 = 1e-10;
const JACOBIAN_STEP: f64 = 1e-7;
#[derive(Clone, Copy, Debug)]
pub(super) struct TangentContact {
pub point: Vec3,
pub rank_ratio: f64,
pub tangency_residual: f64,
pub curvature_contrast: f64,
pub saddle: bool,
}
impl TangentContact {
pub(super) fn is_isolated_node(&self) -> bool {
self.saddle && self.rank_ratio >= NODE_RANK_FLOOR
}
pub(super) fn describe(&self) -> String {
let where_ = format!(
"({:.6},{:.6},{:.6})",
self.point.x, self.point.y, self.point.z
);
if self.rank_ratio < NODE_RANK_FLOOR {
format!(
"the contact at {where_} is EXTENDED, not an isolated node — the two carriers \
agree to second order along one direction there (height-difference Hessian \
rank ratio {:.3e}, curvature contrast {:.3e}, resolved to {:.3e}), so they \
touch along a CURVE and there is no section to imprint",
self.rank_ratio, self.curvature_contrast, self.tangency_residual
)
} else {
format!(
"the carriers touch at {where_} without crossing (height-difference Hessian is \
definite, rank ratio {:.3e}), so the transverse section found elsewhere does \
not pass through it",
self.rank_ratio
)
}
}
}
#[derive(Clone, Copy)]
struct ContactParameters {
ua: f64,
va: f64,
ub: f64,
vb: f64,
}
struct SurfaceDomain {
u0: f64,
u1: f64,
v0: f64,
v1: f64,
}
fn domain_of(surface: &NurbsSurface) -> Result<SurfaceDomain, KernelRefusal> {
let [u0, u1] = surface
.domain_u()
.or_refuse(KernelStage::Intersect, "domain_u")?;
let [v0, v1] = surface
.domain_v()
.or_refuse(KernelStage::Intersect, "domain_v")?;
Ok(SurfaceDomain { u0, u1, v0, v1 })
}
fn tangent_basis(normal: Vec3) -> Result<(Vec3, Vec3), KernelRefusal> {
let first = normal
.perpendicular()
.or_refuse(KernelStage::Intersect, "perpendicular")?;
let second = normal
.cross(first)
.normalized()
.or_refuse(KernelStage::Intersect, "normalized")?;
Ok((first, second))
}
fn contact_residual(
first: &NurbsSurface,
second: &NurbsSurface,
parameters: ContactParameters,
) -> Option<[f64; 5]> {
let point_a = first.evaluate(parameters.ua, parameters.va).ok()?;
let point_b = second.evaluate(parameters.ub, parameters.vb).ok()?;
let normal_a = first.normal(parameters.ua, parameters.va).ok()?;
let normal_b = second.normal(parameters.ub, parameters.vb).ok()?;
let (basis_u, basis_v) = tangent_basis(normal_a).ok()?;
let cross = normal_a.cross(normal_b);
let gap = point_a.sub(point_b);
Some([
gap.x,
gap.y,
gap.z,
cross.dot(basis_u),
cross.dot(basis_v),
])
}
fn refine_contact(
first: &NurbsSurface,
second: &NurbsSurface,
seed: Vec3,
tolerance: f64,
) -> Result<Option<(ContactParameters, f64)>, KernelRefusal> {
let seed_a = project_point_to_surface(first, seed)
.or_refuse(KernelStage::Intersect, "project_point_to_surface")?;
let seed_b = project_point_to_surface(second, seed)
.or_refuse(KernelStage::Intersect, "project_point_to_surface")?;
let first_domain = domain_of(first)?;
let second_domain = domain_of(second)?;
let spans = [
first_domain.u1 - first_domain.u0,
first_domain.v1 - first_domain.v0,
second_domain.u1 - second_domain.u0,
second_domain.v1 - second_domain.v0,
];
let mut current = ContactParameters {
ua: seed_a.u,
va: seed_a.v,
ub: seed_b.u,
vb: seed_b.v,
};
let clamp = |parameters: ContactParameters| ContactParameters {
ua: parameters.ua.clamp(first_domain.u0, first_domain.u1),
va: parameters.va.clamp(first_domain.v0, first_domain.v1),
ub: parameters.ub.clamp(second_domain.u0, second_domain.u1),
vb: parameters.vb.clamp(second_domain.v0, second_domain.v1),
};
let shifted = |parameters: ContactParameters, index: usize, delta: f64| {
let mut moved = parameters;
match index {
0 => moved.ua += delta,
1 => moved.va += delta,
2 => moved.ub += delta,
_ => moved.vb += delta,
}
clamp(moved)
};
let mut best = current;
let mut best_residual = f64::INFINITY;
let mut best_tangency = f64::INFINITY;
let mut without_progress = 0usize;
for _ in 0..REFINE_ITERATIONS {
let Some(residual) = contact_residual(first, second, current) else {
return Ok(None);
};
let gap = (residual[0] * residual[0] + residual[1] * residual[1] + residual[2] * residual[2])
.sqrt();
let tangency = (residual[3] * residual[3] + residual[4] * residual[4]).sqrt();
let worst = gap.max(tangency);
if worst < best_residual * (1.0 - RESIDUAL_PROGRESS) {
without_progress = 0;
} else {
without_progress += 1;
}
if worst < best_residual {
best_residual = worst;
best_tangency = tangency;
best = current;
}
if worst == 0.0 || without_progress >= REFINE_PATIENCE {
break;
}
let mut jacobian = [[0.0f64; 4]; 5];
for index in 0..4 {
let step = spans[index] * JACOBIAN_STEP;
if step <= 0.0 {
return Ok(None);
}
let (Some(plus), Some(minus)) = (
contact_residual(first, second, shifted(current, index, step)),
contact_residual(first, second, shifted(current, index, -step)),
) else {
return Ok(None);
};
for row in 0..5 {
jacobian[row][index] = (plus[row] - minus[row]) / (2.0 * step);
}
}
let mut matrix = [[0.0f64; 4]; 4];
let mut rhs = [0.0f64; 4];
for row in 0..4 {
for column in 0..4 {
matrix[row][column] = (0..5).map(|k| jacobian[k][row] * jacobian[k][column]).sum();
}
rhs[row] = -(0..5).map(|k| jacobian[k][row] * residual[k]).sum::<f64>();
}
let diagonal = (0..4).fold(0.0f64, |largest, index| largest.max(matrix[index][index]));
if diagonal <= 0.0 {
return Ok(None);
}
for index in 0..4 {
matrix[index][index] += LEVENBERG_DAMPING * diagonal;
}
let Ok(changes) = crate::fit::solve_small(matrix, rhs, 4) else {
return Ok(None);
};
let mut scale = 1.0f64;
for index in 0..4 {
let limit = spans[index] / 4.0;
if changes[index].abs() > limit {
scale = scale.min(limit / changes[index].abs());
}
}
let next = clamp(ContactParameters {
ua: current.ua + changes[0] * scale,
va: current.va + changes[1] * scale,
ub: current.ub + changes[2] * scale,
vb: current.vb + changes[3] * scale,
});
let moved = (0..4)
.map(|index| {
let (a, b) = match index {
0 => (next.ua, current.ua),
1 => (next.va, current.va),
2 => (next.ub, current.ub),
_ => (next.vb, current.vb),
};
(a - b).abs() / spans[index]
})
.fold(0.0f64, f64::max);
current = next;
if moved <= PARAMETER_STALL {
break;
}
}
if std::env::var("BREP_DEBUG_PAIRS").is_ok() {
eprintln!(
" refine_contact: best gap/tangency {best_residual:.3e} tangency {best_tangency:.3e}"
);
}
Ok((best_residual <= tolerance).then_some((best, best_tangency)))
}
fn second_fundamental_form(
surface: &NurbsSurface,
u: f64,
v: f64,
basis_u: Vec3,
basis_v: Vec3,
normal: Vec3,
) -> Option<[[f64; 2]; 2]> {
let derivatives = surface.derivatives(u, v, 2).ok()?;
let s_u = derivatives[1][0];
let s_v = derivatives[0][1];
let s_uu = derivatives[2][0];
let s_uv = derivatives[1][1];
let s_vv = derivatives[0][2];
let a11 = s_u.dot(s_u);
let a12 = s_u.dot(s_v);
let a22 = s_v.dot(s_v);
let determinant = a11 * a22 - a12 * a12;
if determinant.abs() <= f64::MIN_POSITIVE {
return None;
}
let coefficients = |target: Vec3| -> (f64, f64) {
let b1 = s_u.dot(target);
let b2 = s_v.dot(target);
(
(b1 * a22 - b2 * a12) / determinant,
(a11 * b2 - a12 * b1) / determinant,
)
};
let along_u = coefficients(basis_u);
let along_v = coefficients(basis_v);
let l = s_uu.dot(normal);
let m = s_uv.dot(normal);
let n = s_vv.dot(normal);
let form = |x: (f64, f64), y: (f64, f64)| l * x.0 * y.0 + m * (x.0 * y.1 + x.1 * y.0) + n * x.1 * y.1;
Some([
[form(along_u, along_u), form(along_u, along_v)],
[form(along_v, along_u), form(along_v, along_v)],
])
}
fn eigenvalues(matrix: [[f64; 2]; 2]) -> (f64, f64) {
let trace = matrix[0][0] + matrix[1][1];
let determinant = matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
let discriminant = (trace * trace / 4.0 - determinant).max(0.0).sqrt();
(trace / 2.0 + discriminant, trace / 2.0 - discriminant)
}
fn spectral_norm(matrix: [[f64; 2]; 2]) -> f64 {
let (first, second) = eigenvalues(matrix);
first.abs().max(second.abs())
}
pub(super) fn classify_tangent_contact(
first: &NurbsSurface,
second: &NurbsSurface,
witness: Vec3,
tolerance: f64,
) -> Result<Option<TangentContact>, KernelRefusal> {
let _clock = ImprintSubphaseClock::new("tangent_classify");
let Some((parameters, tangency_residual)) = refine_contact(first, second, witness, tolerance)?
else {
return Ok(None);
};
let point = first
.evaluate(parameters.ua, parameters.va)
.or_refuse(KernelStage::Intersect, "evaluate")?;
let normal_a = first
.normal(parameters.ua, parameters.va)
.or_refuse(KernelStage::Intersect, "normal")?;
let normal_b = second
.normal(parameters.ub, parameters.vb)
.or_refuse(KernelStage::Intersect, "normal")?;
let orientation = if normal_a.dot(normal_b) < 0.0 { -1.0 } else { 1.0 };
let (basis_u, basis_v) = tangent_basis(normal_a)?;
let (Some(form_a), Some(form_b)) = (
second_fundamental_form(first, parameters.ua, parameters.va, basis_u, basis_v, normal_a),
second_fundamental_form(
second,
parameters.ub,
parameters.vb,
basis_u,
basis_v,
normal_b.scale(orientation),
),
) else {
return Ok(None);
};
let mut difference = [[0.0f64; 2]; 2];
for row in 0..2 {
for column in 0..2 {
difference[row][column] = form_a[row][column] - form_b[row][column];
}
}
let curvature_scale = spectral_norm(form_a).max(spectral_norm(form_b));
let (lambda_first, lambda_second) = eigenvalues(difference);
let smallest = lambda_first.abs().min(lambda_second.abs());
let rank_ratio = if curvature_scale > 0.0 {
smallest / curvature_scale
} else {
0.0
};
let largest = lambda_first.abs().max(lambda_second.abs());
Ok(Some(TangentContact {
point,
rank_ratio,
tangency_residual,
curvature_contrast: if curvature_scale > 0.0 {
largest / curvature_scale
} else {
0.0
},
saddle: lambda_first * lambda_second < 0.0,
}))
}