use super::*;
use crate::projection::project_point_to_curve;
pub(crate) fn enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| match std::env::var("BREP_SOUNDNESS_RESOLVE") {
Ok(value) => value == "1" || value == "on",
Err(std::env::VarError::NotPresent) => true,
Err(_) => false,
})
}
pub(crate) fn refuse_enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| matches!(std::env::var("BREP_SOUNDNESS_REFUSE_UNDECIDED").as_deref(), Ok("1") | Ok("on")))
}
pub(super) const SEEDS: usize = 5;
const ITERATIONS: usize = 24;
pub(super) const MAX_RESOLVED: usize = 256;
const MIN_SINE: f64 = 1e-3;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CrossingUndecidedCause {
Witness,
Pierce,
MissingFace,
}
#[derive(Clone, Debug, Serialize)]
pub struct UndecidedCrossing {
pub face_a: u64,
pub face_b: u64,
pub cause: CrossingUndecidedCause,
pub point: Vec3,
}
pub(super) enum Resolution {
Crossing { point: Vec3, straddle: f64 },
Clear,
Undecided,
}
type Common = (Vec3, (f64, f64), (f64, f64));
fn common_point(a: &NurbsSurface, b: &NurbsSurface, seed: Vec3, tolerance: f64, reach: f64) -> Option<Common> {
let (fa, fb) = (project_point_to_surface(a, seed).ok()?, project_point_to_surface(b, seed).ok()?);
let (da_u, da_v) = (a.domain_u().ok()?, a.domain_v().ok()?);
let (db_u, db_v) = (b.domain_u().ok()?, b.domain_v().ok()?);
let (mut ua, mut va, mut ub, mut vb) = (fa.u, fa.v, fb.u, fb.v);
for _ in 0..ITERATIONS {
let (sa, sb) = (a.derivatives(ua, va, 1).ok()?, b.derivatives(ub, vb, 1).ok()?);
let residual = sa[0][0].sub(sb[0][0]);
if residual.length() <= tolerance {
let point = sa[0][0].add(sb[0][0]).scale(0.5);
return (point.sub(seed).length() <= reach).then_some((point, (ua, va), (ub, vb)));
}
let columns = [sa[1][0], sa[0][1], sb[1][0].scale(-1.), sb[0][1].scale(-1.)];
let mut m = [[0.0f64; 3]; 3];
for c in &columns {
let v = [c.x, c.y, c.z];
for i in 0..3 {
for j in 0..3 {
m[i][j] += v[i] * v[j];
}
}
}
let lambda = solve3(m, [-residual.x, -residual.y, -residual.z])?;
let lambda = Vec3::new(lambda[0], lambda[1], lambda[2]);
let step = columns.map(|c| c.dot(lambda));
ua = (ua + step[0]).clamp(da_u[0], da_u[1]);
va = (va + step[1]).clamp(da_v[0], da_v[1]);
ub = (ub + step[2]).clamp(db_u[0], db_u[1]);
vb = (vb + step[3]).clamp(db_v[0], db_v[1]);
}
None
}
fn solve3(m: [[f64; 3]; 3], r: [f64; 3]) -> Option<[f64; 3]> {
let det = |m: [[f64; 3]; 3]| {
m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])
};
let d = det(m);
if !(d.abs() > 1e-300) || !d.is_finite() {
return None;
}
let mut out = [0.; 3];
for (col, slot) in out.iter_mut().enumerate() {
let mut k = m;
for row in 0..3 {
k[row][col] = r[row];
}
*slot = det(k) / d;
}
out.iter().all(|x| x.is_finite()).then_some(out)
}
fn owns(face: &FaceRecord, (u, v): (f64, f64)) -> Option<PolygonClass> {
parameter_point_in_face(face, Vec2 { x: u, y: v }, TRIM_SAMPLE_TOLERANCE).ok()
}
fn straddle_across(face: &FaceRecord, at: (f64, f64), across: Vec3, other: &NurbsSurface, h: f64) -> Option<f64> {
let d = face.surface.derivatives(at.0, at.1, 1).ok()?;
let (su, sv) = (d[1][0], d[0][1]);
let (a, b, c) = (su.dot(su), su.dot(sv), sv.dot(sv));
let det = a * c - b * b;
if !(det > 0.) {
return None;
}
let (p, q) = (su.dot(across), sv.dot(across));
let (du, dv) = ((c * p - b * q) / det, (a * q - b * p) / det);
let scale = h / su.scale(du).add(sv.scale(dv)).length();
if !scale.is_finite() {
return None;
}
let mut sides = [0.; 2];
for (slot, sign) in sides.iter_mut().zip([1., -1.]) {
let (u, v) = (at.0 + sign * scale * du, at.1 + sign * scale * dv);
if !matches!(owns(face, (u, v))?, PolygonClass::Inside) {
return None;
}
*slot = signed_distance(other, face.surface.evaluate(u, v).ok()?)?;
}
(sides[0] * sides[1] < 0.).then(|| sides[0].abs().min(sides[1].abs()))
}
pub(super) fn resolve(
face_a: &FaceRecord,
face_b: &FaceRecord,
shared: &[&NurbsCurve],
corners: &[Vec3],
segment: (Vec3, Vec3),
options: SelfIntersectionOptions,
) -> Resolution {
let band = options.straddle_band;
let reach = 4. * options.chord_tolerance + segment.1.sub(segment.0).length();
let mut all_clear = true;
for i in 0..SEEDS {
let t = i as f64 / (SEEDS - 1) as f64;
let seed = segment.0.add(segment.1.sub(segment.0).scale(t));
let magnitude = seed.x.abs().max(seed.y.abs()).max(seed.z.abs());
let tolerance = (options.weld * 1e-3).max(64. * f64::EPSILON * magnitude);
let Some((point, pa, pb)) = common_point(&face_a.surface, &face_b.surface, seed, tolerance, reach) else {
all_clear = false;
continue;
};
let on_shared = shared.iter().any(|curve| {
project_point_to_curve(curve, point).is_ok_and(|foot| {
let off = |surface: &NurbsSurface| project_point_to_surface(surface, foot.point).map_or(f64::INFINITY, |f| f.distance);
let deviation = off(&face_a.surface) + off(&face_b.surface);
let sine = match (face_a.surface.normal(pa.0, pa.1), face_b.surface.normal(pb.0, pb.1)) {
(Ok(na), Ok(nb)) => match (na.normalized(), nb.normalized()) {
(Ok(na), Ok(nb)) => na.cross(nb).length(),
_ => 0.,
},
_ => 0.,
};
foot.distance <= band || (deviation.is_finite() && sine > 0. && foot.distance <= band + 4. * deviation / sine)
})
});
let on_corner = corners.iter().any(|corner| corner.sub(point).length() <= band.max(tolerance));
if on_shared || on_corner {
continue;
}
let (Some(in_a), Some(in_b)) = (owns(face_a, pa), owns(face_b, pb)) else {
all_clear = false;
continue;
};
if matches!(in_a, PolygonClass::Outside) || matches!(in_b, PolygonClass::Outside) {
continue;
}
all_clear = false;
if !(matches!(in_a, PolygonClass::Inside) && matches!(in_b, PolygonClass::Inside)) {
continue;
}
let (Ok(na), Ok(nb)) = (face_a.surface.normal(pa.0, pa.1), face_b.surface.normal(pb.0, pb.1)) else { continue };
let (Ok(na), Ok(nb)) = (na.normalized(), nb.normalized()) else { continue };
let along = na.cross(nb);
let sine = along.length();
if !(sine >= MIN_SINE) {
continue;
}
let along = along.scale(1. / sine);
let h = 4. * band / sine;
let (Some(forward), Some(backward)) = (
straddle_across(face_a, pa, nb.sub(along.scale(nb.dot(along))), &face_b.surface, h),
straddle_across(face_b, pb, na.sub(along.scale(na.dot(along))), &face_a.surface, h),
) else {
continue;
};
if forward > band && backward > band {
return Resolution::Crossing { point, straddle: forward.min(backward) };
}
}
if all_clear { Resolution::Clear } else { Resolution::Undecided }
}