use crate::{KernelRefusal, KernelStage, OrRefuse};
use crate::topology::FaceRecord;
use crate::{NurbsCurve, NurbsSurface};
fn debug_enabled() -> bool {
std::env::var("BREP_OS_DEBUG").is_ok_and(|value| !value.is_empty() && value != "0")
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FoldSample {
pub(crate) factor: f64,
pub(crate) u: f64,
pub(crate) v: f64,
pub(crate) kappa: f64,
pub(crate) displacement: f64,
}
impl FoldSample {
pub(crate) fn radius(&self) -> f64 {
1.0 / self.kappa.abs().max(1e-300)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct RegularityScan {
pub(crate) sampled: usize,
pub(crate) collapsed: usize,
pub(crate) worst: Option<FoldSample>,
pub(crate) between_collapsed: usize,
pub(crate) between_regular: usize,
pub(crate) between_evaluations: usize,
pub(crate) between_unresolved: usize,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ScanBudget {
pub(crate) per_span: usize,
pub(crate) steps_per_distance: f64,
pub(crate) max_nodes: usize,
pub(crate) refine: bool,
}
impl ScanBudget {
pub(crate) const SHEET: Self = Self {
per_span: 9,
steps_per_distance: 4.0,
max_nodes: 40_000,
refine: true,
};
pub(crate) const SHELL_FACE: Self = Self {
per_span: 3,
steps_per_distance: 1.0,
max_nodes: 512,
refine: false,
};
}
pub(crate) struct TrimRegion {
loops: Vec<Vec<[f64; 2]>>,
bounds: [f64; 4],
}
impl TrimRegion {
pub(crate) fn whole_domain(surface: &NurbsSurface) -> Result<Self, KernelRefusal> {
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Classify, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Classify, "domain_v")?;
Ok(Self {
loops: Vec::new(),
bounds: [u0, u1, v0, v1],
})
}
pub(crate) fn from_pcurve_loops(
surface: &NurbsSurface,
loops: &[Vec<NurbsCurve>],
) -> Result<Self, KernelRefusal> {
let mut polylines = Vec::with_capacity(loops.len());
for loop_curves in loops {
let mut polyline = Vec::new();
for curve in loop_curves {
append_pcurve_polyline(curve, &mut polyline)?;
}
if polyline.len() >= 3 {
polylines.push(polyline);
}
}
Self::from_polylines(surface, polylines)
}
pub(crate) fn from_face(face: &FaceRecord) -> Result<Self, KernelRefusal> {
let mut polylines = Vec::with_capacity(face.loops.len());
for loop_record in &face.loops {
let mut polyline = Vec::new();
for coedge in &loop_record.coedges {
append_pcurve_polyline(&coedge.pcurve, &mut polyline)?;
}
if polyline.len() >= 3 {
polylines.push(polyline);
}
}
Self::from_polylines(&face.surface, polylines)
}
fn from_polylines(
surface: &NurbsSurface,
loops: Vec<Vec<[f64; 2]>>,
) -> Result<Self, KernelRefusal> {
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Classify, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Classify, "domain_v")?;
if loops.is_empty() {
return Self::whole_domain(surface);
}
let (mut lo_u, mut hi_u) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut lo_v, mut hi_v) = (f64::INFINITY, f64::NEG_INFINITY);
for polyline in &loops {
for point in polyline {
lo_u = lo_u.min(point[0]);
hi_u = hi_u.max(point[0]);
lo_v = lo_v.min(point[1]);
hi_v = hi_v.max(point[1]);
}
}
let bounds = [
lo_u.max(u0).min(u1),
hi_u.max(u0).min(u1),
lo_v.max(v0).min(v1),
hi_v.max(v0).min(v1),
];
if !(bounds[1] > bounds[0]) || !(bounds[3] > bounds[2]) {
if debug_enabled() {
eprintln!(
"regularity: trim box degenerate (u[{:.6},{:.6}] v[{:.6},{:.6}]) — scanning \
the whole carrier instead",
bounds[0], bounds[1], bounds[2], bounds[3]
);
}
return Self::whole_domain(surface);
}
Ok(Self { loops, bounds })
}
pub(crate) fn bounds(&self) -> [f64; 4] {
self.bounds
}
pub(crate) fn contains(&self, u: f64, v: f64) -> bool {
if self.loops.is_empty() {
return u >= self.bounds[0]
&& u <= self.bounds[1]
&& v >= self.bounds[2]
&& v <= self.bounds[3];
}
let mut inside = false;
for polyline in &self.loops {
let count = polyline.len();
let mut previous = count - 1;
for current in 0..count {
let (a, b) = (polyline[previous], polyline[current]);
if (a[1] > v) != (b[1] > v) {
let t = (v - a[1]) / (b[1] - a[1]);
if u < a[0] + t * (b[0] - a[0]) {
inside = !inside;
}
}
previous = current;
}
}
inside
}
fn boundary_points(&self) -> impl Iterator<Item = [f64; 2]> + '_ {
self.loops.iter().flat_map(|polyline| polyline.iter().copied())
}
}
fn append_pcurve_polyline(curve: &NurbsCurve, into: &mut Vec<[f64; 2]>) -> Result<(), KernelRefusal> {
let [t0, t1] = curve.domain().or_refuse(KernelStage::Classify, "domain")?;
if !(t1 > t0) {
return Ok(());
}
let spans = breakpoints(&curve.knots, t0, t1).len().saturating_sub(1).max(1);
let count = (8 * spans).clamp(16, 256);
for index in 0..count {
let t = t0 + (t1 - t0) * index as f64 / count as f64;
let point = curve.evaluate(t).or_refuse(KernelStage::Classify, "evaluate")?;
into.push([point.x, point.y]);
}
Ok(())
}
fn breakpoints(knots: &[f64], lo: f64, hi: f64) -> Vec<f64> {
let width = (hi - lo).abs();
let epsilon = 1e-12 * width.max(1.0);
let mut points = vec![lo];
for &knot in knots {
if knot > lo + epsilon && knot < hi - epsilon && knot > *points.last().unwrap() + epsilon {
points.push(knot);
}
}
points.push(hi);
points
}
fn thin(points: &mut Vec<f64>, max_spans: usize) {
while points.len() > max_spans.max(1) + 1 {
let kept: Vec<f64> = points
.iter()
.enumerate()
.filter(|(index, _)| *index == 0 || *index == points.len() - 1 || index % 2 == 0)
.map(|(_, value)| *value)
.collect();
if kept.len() == points.len() {
break;
}
*points = kept;
}
}
fn span_samples(breaks: &[f64], per_span: usize) -> Vec<f64> {
let per_span = per_span.max(1);
let mut samples = Vec::with_capacity(breaks.len() * per_span);
for window in breaks.windows(2) {
let (lo, hi) = (window[0], window[1]);
for index in 0..per_span {
samples.push(lo + (hi - lo) * (index as f64 + 0.5) / per_span as f64);
}
}
samples
}
fn metric_scale(surface: &NurbsSurface, bounds: [f64; 4]) -> (f64, f64) {
const PROBES: usize = 5;
let (mut su, mut sv) = (0.0f64, 0.0f64);
for iu in 0..PROBES {
let u = bounds[0] + (bounds[1] - bounds[0]) * (iu as f64 + 0.5) / PROBES as f64;
for iv in 0..PROBES {
let v = bounds[2] + (bounds[3] - bounds[2]) * (iv as f64 + 0.5) / PROBES as f64;
if let Ok(derivatives) = surface.derivatives(u, v, 1) {
su = su.max(derivatives[1][0].length());
sv = sv.max(derivatives[0][1].length());
}
}
}
(su, sv)
}
pub(crate) struct RegionGrid {
pub(crate) samples_u: Vec<f64>,
pub(crate) samples_v: Vec<f64>,
}
pub(crate) fn region_grid(
surface: &NurbsSurface,
bounds: [f64; 4],
reach: f64,
budget: ScanBudget,
) -> RegionGrid {
let mut breaks_u = breakpoints(&surface.knots_u, bounds[0], bounds[1]);
let mut breaks_v = breakpoints(&surface.knots_v, bounds[2], bounds[3]);
let max_spans = (budget.max_nodes as f64).sqrt().max(2.0) as usize;
thin(&mut breaks_u, max_spans);
thin(&mut breaks_v, max_spans);
let (spans_u, spans_v) = (breaks_u.len() - 1, breaks_v.len() - 1);
let mut per_u = budget.per_span.max(1);
let mut per_v = budget.per_span.max(1);
let step = reach / budget.steps_per_distance.max(1.0);
let (metric_u, metric_v) = metric_scale(surface, bounds);
let needed = |metric: f64, width: f64, spans: usize| -> usize {
if metric <= 0.0 || step <= 0.0 {
return 1;
}
((metric * width / step) / spans as f64).ceil().max(1.0).min(512.0) as usize
};
per_u = per_u.max(needed(metric_u, bounds[1] - bounds[0], spans_u));
per_v = per_v.max(needed(metric_v, bounds[3] - bounds[2], spans_v));
while spans_u * per_u * spans_v * per_v > budget.max_nodes && (per_u > 1 || per_v > 1) {
if per_u >= per_v {
per_u -= 1;
} else {
per_v -= 1;
}
}
RegionGrid {
samples_u: span_samples(&breaks_u, per_u),
samples_v: span_samples(&breaks_v, per_v),
}
}
pub(crate) fn fold_sample_at(
surface: &NurbsSurface,
u: f64,
v: f64,
displacements: &[f64],
) -> Option<FoldSample> {
let (kappa_min, kappa_max) = surface.principal_curvatures(u, v).ok()?;
let mut worst: Option<FoldSample> = None;
for &displacement in displacements {
if displacement == 0.0 {
continue;
}
for kappa in [kappa_min, kappa_max] {
let factor = 1.0 - displacement * kappa;
if worst.is_none_or(|current| factor < current.factor) {
worst = Some(FoldSample {
factor,
u,
v,
kappa,
displacement,
});
}
}
}
worst
}
impl RegularityScan {
fn keep_worst(&mut self, candidate: FoldSample) {
if self.worst.is_none_or(|current| candidate.factor < current.factor) {
self.worst = Some(candidate);
}
}
}
pub(crate) fn scan_offset_regularity(
surface: &NurbsSurface,
region: &TrimRegion,
displacements: &[f64],
collapse_factor: f64,
budget: ScanBudget,
) -> Result<RegularityScan, KernelRefusal> {
let mut scan = RegularityScan::default();
let reach = displacements
.iter()
.fold(0.0f64, |worst, distance| worst.max(distance.abs()));
if reach == 0.0 || surface.is_affine().or_refuse(KernelStage::Classify, "is_affine")? {
return Ok(scan);
}
let bounds = region.bounds();
let RegionGrid {
samples_u,
samples_v,
} = region_grid(surface, bounds, reach, budget);
let record = |scan: &mut RegularityScan, sample: FoldSample| {
scan.sampled += 1;
if sample.factor <= collapse_factor {
scan.collapsed += 1;
}
scan.keep_worst(sample);
};
let nodes = samples_u
.iter()
.map(|&u| {
samples_v
.iter()
.map(|&v| fold_sample_at(surface, u, v, displacements))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
for (iu, &u) in samples_u.iter().enumerate() {
for (iv, &v) in samples_v.iter().enumerate() {
if let Some(sample) = nodes[iu][iv] {
if region.contains(u, v) {
record(&mut scan, sample);
}
}
}
}
let rim: Vec<[f64; 2]> = region.boundary_points().collect();
let stride = (rim.len() / budget.max_nodes.max(1)).max(1);
for point in rim.iter().step_by(stride) {
let u = point[0].clamp(bounds[0], bounds[1]);
let v = point[1].clamp(bounds[2], bounds[3]);
if let Some(sample) = fold_sample_at(surface, u, v, displacements) {
record(&mut scan, sample);
}
}
let values = nodes
.iter()
.map(|column| {
column
.iter()
.map(|sample| sample.map(|sample| sample.factor - collapse_factor))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let between = refine_between_nodes(
surface,
region,
&samples_u,
&samples_v,
&values,
displacements,
collapse_factor,
);
scan.between_collapsed = between.collapsed;
scan.between_regular = between.regular;
scan.between_evaluations = between.evaluations;
scan.between_unresolved = between.unresolved;
if let Some(worst) = between.worst {
scan.keep_worst(worst);
}
if debug_enabled() && between.evaluations > 0 {
eprintln!(
"regularity: between-node refinement read {} sample(s) in {} risky cell(s) — {} \
collapsed and {} regular sample(s) the census's own nodes did not show, {} cell(s) \
still risky at the finest spacing",
between.evaluations,
between.cells,
between.collapsed,
between.regular,
between.unresolved
);
}
if budget.refine {
refine_worst(surface, region, displacements, &mut scan, bounds);
}
Ok(scan)
}
#[cfg_attr(not(test), allow(dead_code))]
#[derive(Clone, Copy, Debug)]
pub(crate) struct LeastFoldFactor {
pub(crate) least: Option<FoldSample>,
pub(crate) node_spacing: [f64; 2],
pub(crate) finest_spacing: [f64; 2],
pub(crate) bound: bool,
pub(crate) evaluations: usize,
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn least_fold_factor(
face: &FaceRecord,
displacements: &[f64],
level: f64,
budget: ScanBudget,
) -> Result<LeastFoldFactor, KernelRefusal> {
let region = TrimRegion::from_face(face)?;
let reach = displacements
.iter()
.fold(0.0f64, |worst, distance| worst.max(distance.abs()));
let grid = region_grid(&face.surface, region.bounds(), reach, budget);
let spacing = |samples: &[f64]| {
samples
.windows(2)
.map(|pair| pair[1] - pair[0])
.fold(0.0f64, f64::max)
};
let node_spacing = [spacing(&grid.samples_u), spacing(&grid.samples_v)];
let scan = scan_offset_regularity(&face.surface, ®ion, displacements, level, budget)?;
let finest = (1u64 << REFINE_DEPTH) as f64;
Ok(LeastFoldFactor {
least: scan.worst,
node_spacing,
finest_spacing: [node_spacing[0] / finest, node_spacing[1] / finest],
bound: scan.between_unresolved == 0,
evaluations: scan.between_evaluations,
})
}
const REFINE_SAFETY: f64 = 2.0;
const REFINE_DEPTH: usize = 7;
#[derive(Clone, Debug, Default)]
pub(crate) struct BetweenNodes {
pub(crate) collapsed: usize,
pub(crate) regular: usize,
pub(crate) brackets: Vec<([f64; 2], [f64; 2])>,
pub(crate) worst: Option<FoldSample>,
pub(crate) cells: usize,
pub(crate) evaluations: usize,
pub(crate) unresolved: usize,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn refine_between_nodes(
surface: &NurbsSurface,
region: &TrimRegion,
samples_u: &[f64],
samples_v: &[f64],
values: &[Vec<Option<f64>>],
displacements: &[f64],
level: f64,
) -> BetweenNodes {
let mut found = BetweenNodes::default();
let (nu, nv) = (samples_u.len(), samples_v.len());
if nu < 2 || nv < 2 {
return found;
}
let value = |iu: usize, iv: usize| values[iu][iv];
let slopes = |iu: usize, iv: usize| -> (f64, f64) {
let (mut lu, mut lv) = (0.0f64, 0.0f64);
let u_lo = iu.saturating_sub(1);
let u_hi = (iu + 2).min(nu - 1);
let v_lo = iv.saturating_sub(1);
let v_hi = (iv + 2).min(nv - 1);
for a in u_lo..=u_hi {
for b in v_lo..=v_hi {
if a < u_hi {
if let (Some(p), Some(q)) = (value(a, b), value(a + 1, b)) {
lu = lu.max((q - p).abs() / (samples_u[a + 1] - samples_u[a]));
}
}
if b < v_hi {
if let (Some(p), Some(q)) = (value(a, b), value(a, b + 1)) {
lv = lv.max((q - p).abs() / (samples_v[b + 1] - samples_v[b]));
}
}
}
}
(lu, lv)
};
struct Cell {
u: [f64; 2],
v: [f64; 2],
corners: [f64; 4],
slopes: (f64, f64),
depth: usize,
risk: f64,
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
self.risk.total_cmp(&other.risk).is_eq()
}
}
impl Eq for Cell {}
impl PartialOrd for Cell {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Cell {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.risk.total_cmp(&self.risk)
}
}
let risk_of = |corners: &[f64; 4], du: f64, dv: f64, (lu, lv): (f64, f64)| -> Option<f64> {
let margin = REFINE_SAFETY * (lu * du + lv * dv) / 2.0;
let smallest = corners.iter().fold(f64::INFINITY, |m, c| m.min(c.abs()));
(margin > 0.0 && smallest < margin).then(|| smallest / margin)
};
let mut work = std::collections::BinaryHeap::<Cell>::new();
for iu in 0..nu - 1 {
for iv in 0..nv - 1 {
let (Some(a), Some(b), Some(c), Some(d)) =
(value(iu, iv), value(iu + 1, iv), value(iu, iv + 1), value(iu + 1, iv + 1))
else {
continue;
};
let corners = [a, b, c, d];
let negative = corners.iter().filter(|x| **x <= 0.0).count();
if negative != 0 && negative != 4 {
continue; }
let (u0, u1) = (samples_u[iu], samples_u[iu + 1]);
let (v0, v1) = (samples_v[iv], samples_v[iv + 1]);
let touches = region.contains(u0, v0)
|| region.contains(u1, v0)
|| region.contains(u0, v1)
|| region.contains(u1, v1)
|| region.contains(0.5 * (u0 + u1), 0.5 * (v0 + v1));
if !touches {
continue;
}
let cell_slopes = slopes(iu, iv);
if let Some(risk) = risk_of(&corners, u1 - u0, v1 - v0, cell_slopes) {
work.push(Cell {
u: [u0, u1],
v: [v0, v1],
corners,
slopes: cell_slopes,
depth: 0,
risk,
});
}
}
}
found.cells = work.len();
while let Some(cell) = work.pop() {
let collapsed_side = cell.corners[0] <= 0.0;
let (um, vm) = (0.5 * (cell.u[0] + cell.u[1]), 0.5 * (cell.v[0] + cell.v[1]));
let points = [
[um, vm],
[um, cell.v[0]],
[cell.u[0], vm],
[cell.u[1], vm],
[um, cell.v[1]],
];
let mut read = [None; 5];
let mut flipped = false;
for (index, point) in points.iter().enumerate() {
found.evaluations += 1;
let Some(sample) = fold_sample_at(surface, point[0], point[1], displacements) else {
continue;
};
let f = sample.factor - level;
read[index] = Some(f);
let inside = region.contains(point[0], point[1]);
if inside && found.worst.is_none_or(|worst| sample.factor < worst.factor) {
found.worst = Some(sample);
}
if (f <= 0.0) != collapsed_side {
flipped = true;
if inside && f <= 0.0 {
found.collapsed += 1;
} else if inside {
found.regular += 1;
}
let corner = [
[cell.u[0], cell.v[0]],
[cell.u[1], cell.v[0]],
[cell.u[0], cell.v[1]],
[cell.u[1], cell.v[1]],
]
.into_iter()
.min_by(|p, q| {
let dp = (p[0] - point[0]).hypot(p[1] - point[1]);
let dq = (q[0] - point[0]).hypot(q[1] - point[1]);
dp.partial_cmp(&dq).unwrap_or(std::cmp::Ordering::Equal)
})
.expect("four corners");
found.brackets.push((*point, corner));
}
}
if flipped {
continue;
}
let [Some(centre), Some(bottom), Some(left), Some(right), Some(top)] = read else {
continue;
};
let [c00, c10, c01, c11] = cell.corners;
let children = [
([cell.u[0], um], [cell.v[0], vm], [c00, bottom, left, centre]),
([um, cell.u[1]], [cell.v[0], vm], [bottom, c10, centre, right]),
([cell.u[0], um], [vm, cell.v[1]], [left, centre, c01, top]),
([um, cell.u[1]], [vm, cell.v[1]], [centre, right, top, c11]),
];
for (u, v, corners) in children {
let (du, dv) = (u[1] - u[0], v[1] - v[0]);
let own = (
((corners[1] - corners[0]).abs().max((corners[3] - corners[2]).abs())) / du,
((corners[2] - corners[0]).abs().max((corners[3] - corners[1]).abs())) / dv,
);
let child_slopes = (cell.slopes.0.max(own.0), cell.slopes.1.max(own.1));
if let Some(risk) = risk_of(&corners, du, dv, child_slopes) {
if cell.depth + 1 >= REFINE_DEPTH {
found.unresolved += 1;
continue;
}
work.push(Cell {
u,
v,
corners,
slopes: child_slopes,
depth: cell.depth + 1,
risk,
});
}
}
}
found
}
fn refine_worst(
surface: &NurbsSurface,
region: &TrimRegion,
displacements: &[f64],
scan: &mut RegularityScan,
bounds: [f64; 4],
) {
const LEVELS: usize = 4;
const PROBES: usize = 4;
let Some(start) = scan.worst else { return };
let mut radius_u = (bounds[1] - bounds[0]) / 16.0;
let mut radius_v = (bounds[3] - bounds[2]) / 16.0;
let (mut centre_u, mut centre_v) = (start.u, start.v);
for _ in 0..LEVELS {
let mut best: Option<FoldSample> = None;
for iu in 0..=2 * PROBES {
let u = (centre_u + radius_u * (iu as f64 / PROBES as f64 - 1.0))
.clamp(bounds[0], bounds[1]);
for iv in 0..=2 * PROBES {
let v = (centre_v + radius_v * (iv as f64 / PROBES as f64 - 1.0))
.clamp(bounds[2], bounds[3]);
if !region.contains(u, v) {
continue;
}
if let Some(sample) = fold_sample_at(surface, u, v, displacements) {
if best.is_none_or(|current| sample.factor < current.factor) {
best = Some(sample);
}
}
}
}
if let Some(sample) = best {
scan.keep_worst(sample);
centre_u = sample.u;
centre_v = sample.v;
}
radius_u *= 0.5;
radius_v *= 0.5;
}
}