use crate::{KernelRefusal, KernelStage, RefusalClass, SoundnessDefect, Vec3};
fn fold_trace(message: std::fmt::Arguments<'_>) {
if std::env::var("BREP_BLEND_STATION_TRACE").ok().as_deref() == Some("1") {
eprintln!("{message}");
}
}
pub(crate) fn is_wall_fold(refusal: &KernelRefusal) -> bool {
matches!(
refusal.class,
RefusalClass::UnsoundResult {
defect: SoundnessDefect::Fold,
..
}
) && refusal.stage == KernelStage::Classify
}
pub(crate) const WALL_FOLDS: &str = "blend: no wall of radius";
pub(super) fn wall_fold_refusal(faces: Vec<u64>, message: String) -> KernelRefusal {
KernelRefusal::unsound(KernelStage::Classify, SoundnessDefect::Fold, faces, message)
}
const PROBE_FRACTION: f64 = 5.0e-4;
const REFINE_SAMPLES: usize = 8;
#[derive(Clone, Copy)]
pub(super) struct FoldSample {
pub(super) factor: f64,
pub(super) curvature_radius: f64,
pub(super) t: f64,
pub(super) centre: Vec3,
}
pub(super) type SolveCentre<'a> =
&'a dyn Fn(f64, [f64; 4]) -> Result<([f64; 4], Vec3, Vec3, Vec3), KernelRefusal>;
fn curvature_vector(before: Vec3, centre: Vec3, after: Vec3) -> Option<Vec3> {
let u = before.sub(centre);
let v = after.sub(centre);
let cross = u.cross(v);
let denominator = 2.0 * cross.dot(cross);
if !(denominator > 0.0) || !denominator.is_finite() {
return None;
}
let to_circumcentre = v
.cross(cross)
.scale(u.dot(u))
.add(cross.cross(u).scale(v.dot(v)))
.scale(1.0 / denominator);
let squared = to_circumcentre.dot(to_circumcentre);
if !(squared > 0.0) || !squared.is_finite() {
return None;
}
Some(to_circumcentre.scale(1.0 / squared))
}
fn factor_of(radius: f64, curvature: Vec3, centre: Vec3, p1: Vec3, p2: Vec3) -> Option<f64> {
let first = p1.sub(centre).normalized().ok()?;
let second = p2.sub(centre).normalized().ok()?;
let endpoint = curvature.dot(first).max(curvature.dot(second)) * radius;
let across = second.sub(first.scale(second.dot(first)));
let Ok(across) = across.normalized() else {
return Some(endpoint);
};
let total = second.dot(first).clamp(-1.0, 1.0).acos();
let along_first = curvature.dot(first);
let along_across = curvature.dot(across);
let angle = along_across.atan2(along_first);
if !(0.0..=total).contains(&angle) {
return Some(endpoint);
}
let in_plane = along_first.hypot(along_across);
Some(endpoint.max(radius * in_plane))
}
struct Probe {
uv: [f64; 4],
contacts: [Vec3; 2],
centre: Vec3,
before: ([Vec3; 2], Vec3),
after: ([Vec3; 2], Vec3),
}
fn probe(radius: f64, span: f64, t: f64, seed: [f64; 4], solve: SolveCentre<'_>) -> Option<Probe> {
let step = span.abs() * PROBE_FRACTION;
let measure = |at: f64| -> Option<([f64; 4], [Vec3; 2], Vec3)> {
match solve(at, seed) {
Ok((uv, p1, p2, centre)) => Some((uv, [p1, p2], centre)),
Err(error) => {
fold_trace(format_args!(" fold probe at t {at:.9}: {error}"));
None
}
}
};
let (uv, contacts, centre) = measure(t)?;
let (_, before_contacts, before) = measure(t - step)?;
let (_, after_contacts, after) = measure(t + step)?;
for (label, probed) in [("before", before), ("after", after)] {
let moved = probed.sub(centre).length();
if !(moved < radius) {
fold_trace(format_args!(
" fold probe at t {t:.9}: the {label} step moved the ball centre \
{moved:.6}, which is not less than the radius {radius} — station unmeasured"
));
return None;
}
}
Some(Probe {
uv,
contacts,
centre,
before: (before_contacts, before),
after: (after_contacts, after),
})
}
fn sample(radius: f64, t: f64, probe: &Probe) -> Option<FoldSample> {
let curvature = curvature_vector(probe.before.1, probe.centre, probe.after.1)?;
let [p1, p2] = probe.contacts;
let factor = factor_of(radius, curvature, probe.centre, p1, p2)?;
Some(FoldSample {
factor,
curvature_radius: 1.0 / curvature.length(),
t,
centre: probe.centre,
})
}
fn worst_fold(
radius_at: &dyn Fn(f64) -> f64,
span: f64,
stations: &[(f64, [f64; 4])],
solve: SolveCentre<'_>,
visit: &mut dyn FnMut(f64, &Probe) -> Result<(), KernelRefusal>,
) -> Result<Option<FoldSample>, KernelRefusal> {
let mut worst: Option<(usize, FoldSample)> = None;
let mut unmeasured = 0usize;
for (index, (t, seed)) in stations.iter().enumerate() {
let radius = radius_at(*t);
let Some(probed) = probe(radius, span, *t, *seed, solve) else {
unmeasured += 1;
continue;
};
visit(radius, &probed)?;
let Some(measured) = sample(radius, *t, &probed) else {
unmeasured += 1;
continue;
};
if worst.map(|(_, best)| measured.factor > best.factor).unwrap_or(true) {
worst = Some((index, measured));
}
}
if unmeasured > 0 {
fold_trace(format_args!(
"fold probe: {unmeasured} of {} station(s) unmeasured",
stations.len()
));
}
let Some((index, mut best)) = worst else {
return Ok(None);
};
let low = index.saturating_sub(1);
let high = (index + 1).min(stations.len() - 1);
let (t0, t1) = (stations[low].0, stations[high].0);
if t1 > t0 {
for step in 1..REFINE_SAMPLES {
let t = t0 + (t1 - t0) * step as f64 / REFINE_SAMPLES as f64;
let radius = radius_at(t);
let measured = probe(radius, span, t, stations[index].1, solve)
.and_then(|probed| sample(radius, t, &probed));
if let Some(measured) = measured {
if measured.factor > best.factor {
best = measured;
}
}
}
}
Ok(Some(best))
}
pub(crate) const RAIL_FOLDS: &str = "along the ball's track on it";
pub(crate) fn is_rail_fold(refusal: &KernelRefusal) -> bool {
is_wall_fold(refusal) && refusal.message.contains(RAIL_FOLDS)
}
fn rail_curvature(
surface: &crate::NurbsSurface,
uv: [f64; 2],
contact: Vec3,
centre: Vec3,
travel: Vec3,
scale: f64,
) -> Option<f64> {
let (closed_u, closed_v) = surface.closed_directions().ok()?;
let onto = |value: f64, [low, high]: [f64; 2], closed: bool| {
if closed {
low + (value - low).rem_euclid(high - low)
} else {
value.clamp(low, high)
}
};
let u = onto(uv[0], surface.domain_u().ok()?, closed_u);
let v = onto(uv[1], surface.domain_v().ok()?, closed_v);
let derivatives = surface.derivatives(u, v, 2).ok()?;
if derivatives[0][0].sub(contact).length() > 1.0e-9 * (1.0 + scale) {
return None;
}
let (su, sv) = (derivatives[1][0], derivatives[0][1]);
let inward = centre.sub(contact).normalized().ok()?;
let (e, f, g) = (su.dot(su), su.dot(sv), sv.dot(sv));
let determinant = e * g - f * f;
if !(determinant > 0.0) {
return None;
}
let (along_u, along_v) = (su.dot(travel), sv.dot(travel));
let du = (g * along_u - f * along_v) / determinant;
let dv = (e * along_v - f * along_u) / determinant;
let first = e * du * du + 2.0 * f * du * dv + g * dv * dv;
if !(first > 0.0) {
return None;
}
let second = derivatives[2][0].dot(inward) * du * du
+ 2.0 * derivatives[1][1].dot(inward) * du * dv
+ derivatives[0][2].dot(inward) * dv * dv;
Some(second / first)
}
fn radius_is_constant(radius_at: &dyn Fn(f64) -> f64, stations: &[(f64, [f64; 4])]) -> bool {
let mut radii = stations.iter().map(|(t, _)| radius_at(*t));
let Some(first) = radii.next() else {
return true;
};
let bar = 1.0e-11 * (1.0 + first.abs());
radii.all(|radius| (radius - first).abs() <= bar)
}
pub(super) fn check_wall_fold(
radius_at: &dyn Fn(f64) -> f64,
span: f64,
stations: &[(f64, [f64; 4])],
solve: SolveCentre<'_>,
edge: &crate::topology::EdgeRecord,
mates: [&crate::topology::FaceRecord; 2],
scale: f64,
) -> Result<(), KernelRefusal> {
if !radius_is_constant(radius_at, stations) {
fold_trace(format_args!(
"fold probe: the radius varies over these {} station(s) — not measured",
stations.len()
));
return Ok(());
}
let mut rail = |radius: f64, probed: &Probe| -> Result<(), KernelRefusal> {
for side in 0..2 {
let travel = probed.after.0[side].sub(probed.before.0[side]);
let Ok(travel) = travel.normalized() else {
continue;
};
let Some(curvature) = rail_curvature(
&mates[side].surface,
[probed.uv[2 * side], probed.uv[2 * side + 1]],
probed.contacts[side],
probed.centre,
travel,
scale,
) else {
continue;
};
if !(curvature > 0.0) {
continue;
}
let tighter = 1.0 / curvature;
if radius - tighter <= 1.0e-11 * (1.0 + radius) {
continue;
}
let face = mates[side];
let contact = probed.contacts[side];
let named = |name: &Option<String>| {
name.as_deref()
.map(|name| format!(" `{name}`"))
.unwrap_or_default()
};
return Err(wall_fold_refusal(
vec![face.id],
format!(
"{WALL_FOLDS} {radius} fits this edge: along edge {}{}, face {}{} turns tighter \
than the ball {RAIL_FOLDS} at ({:.6}, {:.6}, {:.6}) — radius of curvature \
{tighter:.6} against a blend radius of {radius} — so the contact there runs backward \
against the ball's centre and the fold reaches the wall's rail: the ball \
pokes out through that face, and the wall has no lens between its rails to \
carve",
edge.id,
named(&edge.name),
face.id,
named(&face.name),
contact.x,
contact.y,
contact.z,
),
));
}
Ok(())
};
let Some(worst) = worst_fold(radius_at, span, stations, solve, &mut rail)? else {
return Ok(());
};
if worst.factor <= 1.0 {
return Ok(());
}
let radius = radius_at(worst.t);
Err(wall_fold_refusal(
vec![mates[0].id, mates[1].id],
format!(
"{WALL_FOLDS} {radius} fits this edge: the rolling ball's centre curve turns tighter \
than the ball at ({:.6}, {:.6}, {:.6}) — radius of curvature {:.6} against a blend \
radius of {radius}, so the envelope's section sweeps back through itself there \
(fold factor {:.6})",
worst.centre.x,
worst.centre.y,
worst.centre.z,
worst.curvature_radius,
worst.factor,
),
))
}