use crate::topology::{BrepSolid, EdgeRecord};
use crate::{KernelRefusal, KernelStage, OrRefuse, Vec3};
use super::stations::{edge_uv_on_face, locate_mate, march_model_scale, march_section, signed_radii, solve_station, tangency_residual};
pub(crate) const FOLD_LIMIT: f64 = 0.9;
const MARGIN: [f64; ATTEMPTS] = [1.0, 0.8, 0.8, 0.65];
const RAMP_SLOPE: [f64; ATTEMPTS] = [0.25, 0.15, 0.1, 0.1];
pub(crate) const ATTEMPTS: usize = 4;
const CEILING_STATIONS: usize = 48;
const CONTINUATION_STEPS: usize = 12;
const BISECTIONS: usize = 7;
const SECTION_ADVANCE_FLOOR: f64 = 0.1;
const SECTION_SAMPLES: usize = 8;
const REFINE: usize = 4;
const REFINE_STRESS: f64 = 0.5;
const PEAK_LEVELS: usize = 2;
const LAW_SAMPLES: usize = 1024;
const RELIEF_DEFAULT: bool = true;
thread_local! {
static RELIEF_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
static RELIEVING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static MISFIT_OFF: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn radius_relief_on() -> bool {
!RELIEVING.with(|active| active.get()) && relief_switch()
}
pub(crate) fn tapered_flush_join_on() -> bool {
relief_switch()
}
fn relief_switch() -> bool {
if let Some(forced) = RELIEF_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_RADIUS_RELIEF").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => RELIEF_DEFAULT,
}
}
pub(crate) fn relief_enabled() -> bool {
relief_switch()
}
pub(crate) fn relieving() -> bool {
RELIEVING.with(|active| active.get())
}
pub(crate) struct Relieving;
impl Relieving {
pub(crate) fn enter() -> Relieving {
RELIEVING.with(|active| active.set(true));
Relieving
}
}
impl Drop for Relieving {
fn drop(&mut self) {
RELIEVING.with(|active| active.set(false));
}
}
fn trace_on() -> bool {
std::env::var("BREP_DEBUG_RELIEF").is_ok()
}
fn debug_on() -> bool {
std::env::var("BREP_DEBUG_NETWORK").is_ok()
}
pub(crate) fn edge_ceilings(solid: &BrepSolid, edge: &EdgeRecord, radius: f64) -> Result<Vec<(f64, f64)>, KernelRefusal> {
Ok(station_ceilings(solid, edge, radius, false)?.into_iter().map(|(fraction, ceiling, _)| (fraction, ceiling)).collect())
}
fn station_ceilings(solid: &BrepSolid, edge: &EdgeRecord, radius: f64, strict: bool) -> Result<Vec<(f64, f64, bool)>, KernelRefusal> {
let (fold_limit, advance_floor) = if strict { (1.0, 0.0) } else { (FOLD_LIMIT, SECTION_ADVANCE_FLOOR) };
let (first_face, first_loop, first_coedge) = locate_mate(solid, edge.id, None)?;
let (second_face, _, second_coedge) = locate_mate(solid, edge.id, Some((first_face.id, first_loop)))?;
let (rho1, rho2) = signed_radii(edge, first_face, first_coedge, second_face, second_coedge, radius)?;
let signs = [rho1.signum(), rho2.signum()];
let surfaces = [&first_face.surface, &second_face.surface];
let span = edge.t1 - edge.t0;
let scale = march_model_scale(&edge.curve, edge.t0, edge.t1, radius)?;
let step = span / CEILING_STATIONS as f64;
let ceiling_at = |fraction: f64| -> Result<(f64, bool, f64), KernelRefusal> {
let t = edge.t0 + span * fraction;
let at_edge = [edge_uv_on_face(first_coedge, edge, t)?, edge_uv_on_face(second_coedge, edge, t)?];
let seed0 = [at_edge[0][0], at_edge[0][1], at_edge[1][0], at_edge[1][1]];
let solve_at = |r: f64, seed: [f64; 4]| -> Option<[f64; 4]> {
let (point, tangent) = march_section(edge, t, step, "radius relief").ok()?;
solve_station(surfaces[0], surfaces[1], [signs[0] * r, signs[1] * r], seed, point, tangent, scale).ok()
};
let stress = std::cell::Cell::new(0.0_f64);
let fits = |r: f64, seed: [f64; 4]| -> Result<[f64; 4], bool> {
let uv = solve_at(r, seed);
if uv.is_none() && trace_on() {
eprintln!("relief probe: edge {} fraction {fraction:.5} r {r:.5} does not solve", edge.id);
}
let uv = uv.ok_or(false)?;
let read = || -> Option<bool> {
let rho = [signs[0] * r, signs[1] * r];
let probe = |at: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3, Vec3, Vec3), KernelRefusal> {
let (point, tangent) = march_section(edge, at, step, "radius relief")?;
let uv = solve_station(surfaces[0], surfaces[1], rho, seed, point, tangent, scale)?;
let (_, p1, p2, centre) = tangency_residual(surfaces[0], surfaces[1], rho, uv, point, tangent)?;
Ok((uv, p1, p2, centre))
};
let reading = super::fold::fold_number(r, span, t, uv, &probe, surfaces, scale);
if trace_on() {
eprintln!("relief probe: edge {} fraction {fraction:.5} r {r:.5} reading {reading:?}", edge.id);
}
let reading = reading?;
let step = span * super::fold::PROBE_FRACTION;
let (before, after) = (march_section(edge, t - step, step, "radius relief").ok()?, march_section(edge, t + step, step, "radius relief").ok()?);
let along = after.0.sub(before.0).length();
let [(before_contacts, before_centre), (after_contacts, after_centre)] = reading.sections;
let ball = after_centre.sub(before_centre).normalized().ok()?;
let advance = section_points(before_contacts, before_centre)?
.iter()
.zip(§ion_points(after_contacts, after_centre)?)
.map(|(a, b)| b.sub(*a).dot(ball) / along)
.fold(f64::INFINITY, f64::min);
if trace_on() {
eprintln!("relief probe: edge {} fraction {fraction:.5} r {r:.5} section advance {advance:.6}", edge.id);
}
let fit = reading.number < fold_limit && advance > advance_floor;
if fit {
stress.set(stress.get().max((reading.number / fold_limit).max((1.0 - advance) / (1.0 - advance_floor))));
}
Some(fit)
};
match read() {
Some(true) => Ok(uv),
Some(false) => Err(true),
None => Err(false),
}
};
let mut seed = seed0;
let mut good = 0.0;
let mut bad = None;
for step_index in 1..=CONTINUATION_STEPS {
let r = radius * step_index as f64 / CONTINUATION_STEPS as f64;
match fits(r, seed) {
Ok(uv) => {
good = r;
seed = uv;
}
Err(folds) => {
bad = Some((r, folds));
break;
}
}
}
if let Some((first_bad, false)) = bad {
let mut step_index = (first_bad / radius * CONTINUATION_STEPS as f64).round() as usize + 1;
while step_index <= CONTINUATION_STEPS {
let r = radius * step_index as f64 / CONTINUATION_STEPS as f64;
if let Err(true) = fits(r, seed) {
bad = Some((first_bad, true));
break;
}
step_index += 1;
}
}
let ceiling = match bad {
None => (radius, false),
Some((mut high, mut folds)) => {
for _ in 0..BISECTIONS {
let middle = 0.5 * (good + high);
match fits(middle, seed) {
Ok(uv) => {
good = middle;
seed = uv;
}
Err(why) => {
high = middle;
folds |= why;
}
}
}
(good, folds)
}
};
Ok((ceiling.0, ceiling.1, stress.get()))
};
let mut all: Vec<(f64, f64, bool, f64)> = Vec::with_capacity(CEILING_STATIONS + 4 * REFINE * (PEAK_LEVELS + 1));
for index in 0..CEILING_STATIONS {
let fraction = (index as f64 + 0.5) / CEILING_STATIONS as f64;
let (ceiling, folds, stress) = ceiling_at(fraction)?;
all.push((fraction, ceiling, folds, stress));
}
let peak = |index: usize| -> bool {
let around = |other: Option<usize>| other.and_then(|other| all.get(other)).map(|s| s.3).unwrap_or(0.0);
all[index].3 >= REFINE_STRESS && all[index].3 >= around(index.checked_sub(1)) && all[index].3 >= around(Some(index + 1))
};
let short: Vec<usize> = (0..CEILING_STATIONS).filter(|index| all[*index].1 < radius || peak(*index)).collect();
let mut refined = Vec::new();
for index in short {
let low = (index as f64 - 1.0 + 0.5).max(0.0) / CEILING_STATIONS as f64;
let high = (index as f64 + 1.0 + 0.5).min(CEILING_STATIONS as f64) / CEILING_STATIONS as f64;
for sub in 1..2 * REFINE {
let fraction = low + (high - low) * sub as f64 / (2 * REFINE) as f64;
if all.iter().chain(&refined).any(|s: &(f64, f64, bool, f64)| (s.0 - fraction).abs() < 1e-12) {
continue;
}
let (ceiling, folds, stress) = ceiling_at(fraction)?;
refined.push((fraction, ceiling, folds, stress));
}
}
all.extend(refined);
let mut spacing = 1.0 / (CEILING_STATIONS as f64 * REFINE as f64);
for _ in 0..PEAK_LEVELS {
if all.iter().any(|s| s.1 < radius) {
break;
}
let Some(&(peak_fraction, _, _, _)) = all.iter().filter(|s| s.3 >= REFINE_STRESS).max_by(|a, b| a.3.total_cmp(&b.3)) else {
break;
};
spacing /= REFINE as f64;
for k in 1..=2 * REFINE as i64 {
for fraction in [peak_fraction - k as f64 * spacing, peak_fraction + k as f64 * spacing] {
if !(0.0..=1.0).contains(&fraction) || all.iter().any(|s| (s.0 - fraction).abs() < 1e-12) {
continue;
}
let (ceiling, folds, stress) = ceiling_at(fraction)?;
all.push((fraction, ceiling, folds, stress));
}
}
}
let mut ceilings: Vec<(f64, f64, bool)> = all.into_iter().map(|(f, c, folds, _)| (f, c, folds)).collect();
ceilings.sort_by(|a, b| a.0.total_cmp(&b.0));
if debug_on() {
let least = ceilings.iter().map(|(_, c, _)| *c).fold(f64::INFINITY, f64::min);
let short: Vec<String> = ceilings
.iter()
.filter(|(_, c, _)| *c < radius)
.map(|(f, c, folds)| format!("{f:.3}:{c:.3}{}", if *folds { "" } else { "?" }))
.collect();
eprintln!("radius relief: edge {} ceilings at r {radius}: least {least:.6} short {}", edge.id, short.join(" "));
}
Ok(ceilings)
}
fn section_points(contacts: [Vec3; 2], centre: Vec3) -> Option<Vec<Vec3>> {
let (a, b) = (contacts[0].sub(centre), contacts[1].sub(centre));
let radius = a.length();
let (unit_a, unit_b) = (a.normalized().ok()?, b.normalized().ok()?);
let angle = unit_a.cross(unit_b).length().atan2(unit_a.dot(unit_b));
let normal = unit_a.cross(unit_b).normalized().ok()?;
let across = normal.cross(unit_a);
Some(
(0..SECTION_SAMPLES)
.map(|k| {
let phi = angle * k as f64 / (SECTION_SAMPLES - 1) as f64;
centre.add(unit_a.scale(radius * phi.cos())).add(across.scale(radius * phi.sin()))
})
.collect(),
)
}
pub(crate) fn measure_ceilings(solid: &BrepSolid, edge_ids: &[u64], radius: f64) -> Result<Vec<Vec<(f64, f64)>>, KernelRefusal> {
edge_ids
.iter()
.map(|id| {
let edge = solid.edges.iter().find(|edge| edge.id == *id).ok_or_else(|| {
KernelRefusal::internal(KernelStage::Collect, "relief_edge", format!("radius relief: edge {id} not found"))
})?;
edge_ceilings(solid, edge, radius)
})
.collect()
}
pub(crate) fn join_continuations(solid: &BrepSolid, edge_ids: &[u64]) -> Option<BrepSolid> {
let mut uses: Vec<(u64, usize)> = Vec::new();
for id in edge_ids {
let edge = solid.edges.iter().find(|edge| edge.id == *id)?;
if edge.start_vertex_id == edge.end_vertex_id {
continue;
}
for vertex in [edge.start_vertex_id, edge.end_vertex_id] {
match uses.iter_mut().find(|(v, _)| *v == vertex) {
Some((_, count)) => *count += 1,
None => uses.push((vertex, 1)),
}
}
}
let at: rustc_hash::FxHashSet<u64> = uses.iter().filter(|(_, count)| *count == 2).map(|(vertex, _)| *vertex).collect();
if at.is_empty() {
return None;
}
let tolerance = crate::KernelTolerances::for_solid(solid, crate::BooleanOptions::default().tolerance).model;
let joined = crate::coalesce::merge_curve_continuation_edges_at(solid, tolerance, &at).ok()?;
(joined.edges.len() < solid.edges.len() && joined.validate().is_empty()).then_some(joined)
}
pub(crate) fn first_misfit(solid: &BrepSolid, edge_ids: &[u64], radius: f64) -> Option<(u64, f64)> {
if MISFIT_OFF.with(|off| off.get()) {
return None;
}
let mut misfit = None;
let mut fits_somewhere = false;
for &edge_id in edge_ids {
let edge = solid.edges.iter().find(|edge| edge.id == edge_id)?;
let Ok(ceilings) = station_ceilings(solid, edge, radius, true) else {
continue;
};
fits_somewhere |= ceilings.iter().any(|(_, c, _)| *c >= radius);
let least = ceilings
.iter()
.filter(|(_, c, folds)| *folds && *c > 0.0)
.map(|(_, c, _)| *c)
.fold(f64::INFINITY, f64::min);
if least < radius && misfit.is_none() {
misfit = Some((edge_id, least));
}
}
misfit.filter(|_| fits_somewhere)
}
pub(crate) fn is_misfit_fold(refusal: &KernelRefusal) -> bool {
refusal.message.contains(MISFIT_MARK)
}
const MISFIT_MARK: &str = "the largest ball that rolls without folding is";
pub(crate) struct MisfitOff;
impl MisfitOff {
pub(crate) fn enter() -> MisfitOff {
MISFIT_OFF.with(|off| off.set(true));
MisfitOff
}
}
impl Drop for MisfitOff {
fn drop(&mut self) {
MISFIT_OFF.with(|off| off.set(false));
}
}
#[derive(Clone, Debug)]
pub(crate) struct ReliefZone {
pub(crate) radius: f64,
pub(crate) edges: Vec<usize>,
pub(crate) ceiling: f64,
}
#[derive(Clone, Debug)]
pub(crate) struct ReliefPlan {
pub(crate) stops: Vec<Vec<(f64, f64)>>,
pub(crate) zones: Vec<ReliefZone>,
}
struct LawGraph {
nodes: Vec<Vec<usize>>,
adjacency: Vec<Vec<(usize, f64)>>,
}
fn law_graph(edges: &[&EdgeRecord]) -> Result<LawGraph, KernelRefusal> {
let mut vertex_nodes: Vec<(u64, usize)> = Vec::new();
let mut count = 0usize;
let mut node_of_vertex = |vertex: u64, count: &mut usize| -> usize {
if let Some((_, node)) = vertex_nodes.iter().find(|(id, _)| *id == vertex) {
return *node;
}
let node = *count;
*count += 1;
vertex_nodes.push((vertex, node));
node
};
let mut nodes = Vec::with_capacity(edges.len());
let mut lengths = Vec::with_capacity(edges.len());
for edge in edges {
let mut row = Vec::with_capacity(LAW_SAMPLES + 1);
let closed = edge.start_vertex_id == edge.end_vertex_id;
row.push(node_of_vertex(edge.start_vertex_id, &mut count));
for _ in 1..LAW_SAMPLES {
row.push(count);
count += 1;
}
row.push(if closed { row[0] } else { node_of_vertex(edge.end_vertex_id, &mut count) });
let mut spans = Vec::with_capacity(LAW_SAMPLES);
let mut previous = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Refine, "evaluate")?;
for index in 1..=LAW_SAMPLES {
let t = edge.t0 + (edge.t1 - edge.t0) * index as f64 / LAW_SAMPLES as f64;
let point = edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
spans.push(point.sub(previous).length());
previous = point;
}
nodes.push(row);
lengths.push(spans);
}
let mut adjacency = vec![Vec::new(); count];
for (row, spans) in nodes.iter().zip(&lengths) {
for index in 0..LAW_SAMPLES {
let (a, b) = (row[index], row[index + 1]);
adjacency[a].push((b, spans[index]));
adjacency[b].push((a, spans[index]));
}
}
Ok(LawGraph { nodes, adjacency })
}
fn distances(graph: &LawGraph, sources: &[usize]) -> Vec<f64> {
let mut distance = vec![f64::INFINITY; graph.adjacency.len()];
let mut heap = std::collections::BinaryHeap::new();
for &source in sources {
distance[source] = 0.0;
heap.push(std::cmp::Reverse((0u64, source)));
}
while let Some(std::cmp::Reverse((bits, node))) = heap.pop() {
let here = f64::from_bits(bits);
if here > distance[node] {
continue;
}
for &(next, length) in &graph.adjacency[node] {
let through = here + length;
if through < distance[next] {
distance[next] = through;
heap.push(std::cmp::Reverse((through.to_bits(), next)));
}
}
}
distance
}
fn smoothstep(x: f64) -> f64 {
let x = x.clamp(0.0, 1.0);
x * x * x * (x * (6.0 * x - 15.0) + 10.0)
}
pub(crate) fn plan_relief(
solid: &BrepSolid,
edge_ids: &[u64],
radius: f64,
measured: &[Vec<(f64, f64)>],
attempt: usize,
) -> Result<Option<ReliefPlan>, KernelRefusal> {
let attempt = attempt.min(ATTEMPTS - 1);
let edges: Vec<&EdgeRecord> = edge_ids
.iter()
.map(|id| {
solid.edges.iter().find(|edge| edge.id == *id).ok_or_else(|| {
KernelRefusal::internal(KernelStage::Collect, "relief_edge", format!("radius relief: edge {id} not found"))
})
})
.collect::<Result<_, _>>()?;
let graph = law_graph(&edges)?;
let node_count = graph.adjacency.len();
let mut ceiling = vec![f64::INFINITY; node_count];
for (index, stations) in measured.iter().enumerate() {
for (sample, &node) in graph.nodes[index].iter().enumerate() {
let fraction = sample as f64 / LAW_SAMPLES as f64;
let reach = 1.0 / LAW_SAMPLES as f64 + 0.5 / CEILING_STATIONS as f64;
let value = stations
.iter()
.filter(|(f, _)| (f - fraction).abs() <= reach)
.map(|(_, c)| *c)
.fold(f64::INFINITY, f64::min);
let value = if value.is_finite() {
value
} else {
stations.iter().min_by(|a, b| (a.0 - fraction).abs().total_cmp(&(b.0 - fraction).abs())).map(|(_, c)| *c).unwrap_or(radius)
};
ceiling[node] = ceiling[node].min(value);
}
}
let target: Vec<f64> = ceiling.iter().map(|c| (MARGIN[attempt] * c).min(radius)).collect();
let short = |node: usize| ceiling[node] < radius;
let mut zone_of = vec![usize::MAX; node_count];
let mut zones: Vec<Vec<usize>> = Vec::new();
for start in 0..node_count {
if !short(start) || zone_of[start] != usize::MAX {
continue;
}
let id = zones.len();
let mut members = vec![start];
zone_of[start] = id;
let mut cursor = 0;
while cursor < members.len() {
let node = members[cursor];
cursor += 1;
for &(next, _) in &graph.adjacency[node] {
if short(next) && zone_of[next] == usize::MAX {
zone_of[next] = id;
members.push(next);
}
}
}
zones.push(members);
}
if zones.is_empty() {
return Ok(None);
}
let plateau = |members: &[usize]| members.iter().map(|&n| target[n]).fold(f64::INFINITY, f64::min);
let ramp = |level: f64| (radius - level) / RAMP_SLOPE[attempt];
loop {
let fields: Vec<Vec<f64>> = zones.iter().map(|members| distances(&graph, members)).collect();
let mut merged = None;
'pairs: for a in 0..zones.len() {
for b in a + 1..zones.len() {
let gap = zones[b].iter().map(|&n| fields[a][n]).fold(f64::INFINITY, f64::min);
if gap < ramp(plateau(&zones[a])) + ramp(plateau(&zones[b])) {
merged = Some((a, b, gap));
break 'pairs;
}
}
}
let Some((a, b, gap)) = merged else {
break;
};
let mut members: Vec<usize> = (0..node_count)
.filter(|&n| fields[a][n] + fields[b][n] <= gap * (1.0 + 1e-9) + 1e-12)
.collect();
members.extend(zones[a].iter().chain(&zones[b]).copied());
members.sort_unstable();
members.dedup();
zones.remove(b);
zones[a] = members;
}
let levels: Vec<f64> = zones.iter().map(|members| plateau(members)).collect();
if let Some(level) = levels.iter().find(|level| !(**level > 0.0)) {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"relief_nothing_fits",
format!("radius relief: no ball fits somewhere along the selection (plateau {level})"),
));
}
let fields: Vec<Vec<f64>> = zones.iter().map(|members| distances(&graph, members)).collect();
let law = |node: usize| -> f64 {
let mut value = radius;
for (zone, level) in levels.iter().enumerate() {
let length = ramp(*level);
let reading = level + (radius - level) * smoothstep(fields[zone][node] / length);
value = value.min(reading);
}
value
};
let stops: Vec<Vec<(f64, f64)>> = graph
.nodes
.iter()
.map(|row| {
row.iter()
.enumerate()
.map(|(sample, &node)| (sample as f64 / LAW_SAMPLES as f64, law(node)))
.collect()
})
.collect();
let zones = zones
.iter()
.zip(&levels)
.map(|(members, level)| ReliefZone {
radius: *level,
edges: graph
.nodes
.iter()
.enumerate()
.filter(|(_, row)| row.iter().any(|node| members.contains(node)))
.map(|(index, _)| index)
.collect(),
ceiling: members.iter().map(|&n| ceiling[n]).fold(f64::INFINITY, f64::min),
})
.collect();
if debug_on() {
for (index, row) in stops.iter().enumerate() {
let least = row.iter().map(|(_, r)| *r).fold(f64::INFINITY, f64::min);
let reduced: Vec<String> = row.iter().filter(|(_, r)| *r < radius).map(|(f, r)| format!("{f:.3}:{r:.3}")).collect();
eprintln!("radius relief: edge {} law least {least:.6} (attempt {attempt}) reduced {}", edge_ids[index], reduced.join(" "));
}
}
Ok(Some(ReliefPlan { stops, zones }))
}
pub(crate) fn misfit_fold(edge_id: u64, least: f64, radius: f64, refusal: &KernelRefusal) -> KernelRefusal {
super::fold::wall_fold_refusal(
Vec::new(),
format!(
"{} {radius} fits this edge: along edge {edge_id} {MISFIT_MARK} {least:.6} (the network refused: {refusal})",
super::fold::WALL_FOLDS
),
)
}