use crate::{KernelRefusal, KernelStage, OrRefuse, 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)
}
pub(super) 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,
})
}
#[derive(Clone, Copy, PartialEq)]
enum Difference {
Central,
Forward,
Backward,
}
fn section_normal(contacts: [Vec3; 2], centre: Vec3) -> Option<Vec3> {
contacts[0]
.sub(centre)
.cross(contacts[1].sub(centre))
.normalized()
.ok()
}
fn sample_section(radius: f64, t: f64, probe: &Probe, difference: Difference) -> Option<FoldSample> {
let m = section_normal(probe.contacts, probe.centre)?;
let oriented = |contacts: [Vec3; 2], centre: Vec3| -> Option<Vec3> {
let normal = section_normal(contacts, centre)?;
Some(if normal.dot(m) < 0.0 { normal.scale(-1.0) } else { normal })
};
let (m_low, c_low, m_high, c_high) = match difference {
Difference::Central => (
oriented(probe.before.0, probe.before.1)?,
probe.before.1,
oriented(probe.after.0, probe.after.1)?,
probe.after.1,
),
Difference::Forward => (m, probe.centre, oriented(probe.after.0, probe.after.1)?, probe.after.1),
Difference::Backward => (oriented(probe.before.0, probe.before.1)?, probe.before.1, m, probe.centre),
};
let advance = c_high.sub(c_low).dot(m);
if !(advance.abs() > 0.0) || !advance.is_finite() {
return None;
}
let turn = m_high.sub(m_low).scale(1.0 / advance);
let [p1, p2] = probe.contacts;
let factor = factor_of(radius, turn, probe.centre, p1, p2)?;
let magnitude = turn.length();
Some(FoldSample {
factor,
curvature_radius: if magnitude > 0.0 { 1.0 / magnitude } else { f64::INFINITY },
t,
centre: probe.centre,
})
}
fn worst_fold(
radius_at: &dyn Fn(f64) -> f64,
span: f64,
stations: &[(f64, [f64; 4])],
solve: SolveCentre<'_>,
tapered: bool,
visit: &mut dyn FnMut(f64, &Probe) -> Result<(), KernelRefusal>,
) -> Result<Option<FoldSample>, KernelRefusal> {
let mut worst: Option<(usize, FoldSample)> = None;
let mut unmeasured = 0usize;
let t_low = stations.iter().map(|(t, _)| *t).fold(f64::INFINITY, f64::min);
let t_high = stations.iter().map(|(t, _)| *t).fold(f64::NEG_INFINITY, f64::max);
let trace = std::env::var("BREP_BLEND_STATION_TRACE").ok().as_deref() == Some("1");
let measure = |radius: f64, t: f64, probed: &Probe, difference: Difference| -> Option<FoldSample> {
let measured = if tapered {
sample_section(radius, t, probed, difference)
} else {
sample(radius, t, probed)
};
if trace {
let circumcircle = sample(radius, t, probed).map(|s| s.factor);
let section = sample_section(radius, t, probed, difference).map(|s| s.factor);
fold_trace(format_args!(
" fold probe at t {t:.9}: radius {radius:.9} factor circumcircle {circumcircle:?} section {section:?}"
));
}
measured
};
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 difference = if *t <= t_low {
Difference::Forward
} else if *t >= t_high {
Difference::Backward
} else {
Difference::Central
};
let Some(measured) = measure(radius, *t, &probed, difference) 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| measure(radius, t, &probed, Difference::Central));
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> {
let tapered = !radius_is_constant(radius_at, stations);
if tapered {
fold_trace(format_args!(
"fold probe: the radius varies over these {} station(s) — measuring the section turn",
stations.len()
));
}
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, tapered, &mut rail)? else {
return Ok(());
};
if worst.factor <= 1.0 {
return Ok(());
}
let radius = radius_at(worst.t);
if tapered {
let (low, high) = stations.iter().map(|(t, _)| radius_at(*t)).fold(
(f64::INFINITY, f64::NEG_INFINITY),
|(low, high), r| (low.min(r), high.max(r)),
);
return Err(wall_fold_refusal(
vec![mates[0].id, mates[1].id],
format!(
"{WALL_FOLDS} {radius} fits this edge where the taper reaches it: the tapered wall's \
sections turn tighter than the local ball at ({:.6}, {:.6}, {:.6}) — radius of turn \
{:.6} against a blend radius of {radius} there, so the section sweeps back through \
its neighbours (fold factor {:.6}; the radius runs from {low} to {high} along the edge)",
worst.centre.x,
worst.centre.y,
worst.centre.z,
worst.curvature_radius,
worst.factor,
),
));
}
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,
),
))
}
#[derive(Clone, Copy, Debug)]
pub(super) struct FoldReading {
pub(super) number: f64,
pub(super) turn: Vec3,
pub(super) sections: [([Vec3; 2], Vec3); 2],
}
pub(super) fn fold_number(
radius: f64,
span: f64,
t: f64,
seed: [f64; 4],
solve: SolveCentre<'_>,
surfaces: [&crate::NurbsSurface; 2],
scale: f64,
) -> Option<FoldReading> {
let probed = probe(radius, span, t, seed, solve)?;
let turn = curvature_vector(probed.before.1, probed.centre, probed.after.1).unwrap_or_default();
let factor = sample(radius, t, &probed).map(|sample| sample.factor);
let mut worst = factor.unwrap_or(0.0);
let mut rails = [None; 2];
for side in 0..2 {
let Ok(travel) = probed.after.0[side].sub(probed.before.0[side]).normalized() else {
continue;
};
let uv = [probed.uv[2 * side], probed.uv[2 * side + 1]];
if let Some(curvature) = rail_curvature(surfaces[side], uv, probed.contacts[side], probed.centre, travel, scale) {
rails[side] = Some(radius * curvature);
worst = worst.max(radius * curvature);
}
}
if std::env::var("BREP_DEBUG_RELIEF").is_ok() {
eprintln!(
" fold number at t {t:.6} r {radius:.5}: factor {factor:?} centre turn radius {:.6} rails {rails:?}",
1.0 / turn.length()
);
}
Some(FoldReading { number: worst, turn, sections: [probed.before, probed.after] })
}
fn rows_loop_check_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var("BREP_BLEND_ROWS_LOOP_CHECK").as_deref() != Ok("0"))
}
const ROWS_LOOP_SUBSTEPS: usize = 4;
const ROWS_LOOP_SECTION_SAMPLES: usize = 16;
pub(super) fn check_rows_loop(
rows: &super::stations::FittedRows,
stations: &[super::stations::Station],
parameters: &[f64],
span: Option<[usize; 2]>,
radius: f64,
faces: Vec<u64>,
scale: f64,
) -> Result<(), KernelRefusal> {
if !rows_loop_check_enabled() || rows.exact_extrusion || stations.len() < 2 || parameters.len() != stations.len() {
return Ok(());
}
let (low, high) = match span {
Some([a, b]) => (a.min(b), a.max(b)),
None => (0, stations.len() - 1),
};
let high = high.min(stations.len() - 1);
let [u0, u1] = rows.surface.domain_u().or_refuse(KernelStage::Refine, "domain")?;
let [v0, v1] = rows.surface.domain_v().or_refuse(KernelStage::Refine, "domain")?;
let (p_first, p_last) = (parameters[0], parameters[parameters.len() - 1]);
if !(p_last > p_first) {
return Ok(());
}
let to_u = |p: f64| u0 + (u1 - u0) * (p - p_first) / (p_last - p_first);
let bar = 1.0e-7 * (1.0 + scale);
let debug = std::env::var_os("BREP_DEBUG_ROWS_LOOP").is_some();
let mut least = f64::INFINITY;
for index in low..high {
let advance = stations[index + 1].center.sub(stations[index].center);
let length = advance.length();
if !(length > bar) {
continue;
}
let m = advance.scale(1.0 / length);
let (pa, pb) = (parameters[index], parameters[index + 1]);
for sub in 0..ROWS_LOOP_SUBSTEPS {
let qa = pa + (pb - pa) * sub as f64 / ROWS_LOOP_SUBSTEPS as f64;
let qb = pa + (pb - pa) * (sub + 1) as f64 / ROWS_LOOP_SUBSTEPS as f64;
for k in 0..=ROWS_LOOP_SECTION_SAMPLES {
let v = v0 + (v1 - v0) * k as f64 / ROWS_LOOP_SECTION_SAMPLES as f64;
let a = rows.surface.evaluate(to_u(qa), v).or_refuse(KernelStage::Refine, "evaluate")?;
let b = rows.surface.evaluate(to_u(qb), v).or_refuse(KernelStage::Refine, "evaluate")?;
let along = b.sub(a).dot(m);
least = least.min(along);
if along < -bar {
if debug {
eprintln!(
"rows-loop check: loop at ({:.6}, {:.6}, {:.6}) between station centres ({:.6}, {:.6}, {:.6}) and ({:.6}, {:.6}, {:.6})",
a.x, a.y, a.z,
stations[index].center.x, stations[index].center.y, stations[index].center.z,
stations[index + 1].center.x, stations[index + 1].center.y, stations[index + 1].center.z
);
}
return Err(wall_fold_refusal(
faces,
format!(
"{WALL_FOLDS} {radius:.6} stands on this edge: its fitted rows pass back through \
themselves between stations {index} and {} (row parameter {qa:.6}..{qb:.6}, \
section parameter {v:.6}) — the row moves {:.3e} against the ball's advance, \
so the wall carries two parameters on one point there",
index + 1,
-along
),
));
}
}
}
}
if debug {
eprintln!(
"rows-loop check: {} stations, radius {radius:.6}, least advance {least:.3e} against bar {bar:.3e}, u [{u0:.6}, {u1:.6}] v [{v0:.6}, {v1:.6}]",
stations.len()
);
}
Ok(())
}
pub(crate) fn wall_loop_refusal(face: u64, message: String) -> KernelRefusal {
wall_fold_refusal(vec![face], message)
}