use crate::{KernelRefusal, KernelStage, OrRefuse};
use super::*;
pub(super) struct RimPiece {
pub(super) face_id: u64,
pub(super) loop_index: usize,
pub(super) window: [f64; 2],
pub(super) edge_id: u64,
}
pub fn blend_smooth_chain(
solid: &BrepSolid,
seed_edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let _operation = crate::blend::BlendOperation::enter();
if !(radius > 0.0) || !radius.is_finite() {
return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
}
let chain = collect_smooth_chain(solid, seed_edge_id)?;
if chain.segments.len() < 2 {
return Err(KernelRefusal::internal(KernelStage::Refine, "chain_single_segment", "blend: chain collapsed to a single segment"));
}
if chain.closed {
blend_closed_smooth_chain(solid, &chain.segments, radius, chamfer, name)
} else {
blend_open_smooth_chain(solid, &chain, radius, chamfer, name)
}
}
pub fn blend_smooth_chain_if_closed(
solid: &BrepSolid,
seed_edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let _operation = crate::blend::BlendOperation::enter();
if !(radius > 0.0) || !radius.is_finite() {
return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
}
let chain = collect_smooth_chain(solid, seed_edge_id)?;
if chain.segments.len() < 2 {
return Err(KernelRefusal::internal(KernelStage::Refine, "chain_single_segment", "blend: chain collapsed to a single segment"));
}
if !chain.closed {
return Err(
KernelRefusal::unsupported(KernelStage::Classify, "open_tangent_chain", "blend: the edge is one arc of an OPEN tangent chain; the unselected \
continuation is capped, not blended"),
);
}
blend_closed_smooth_chain(solid, &chain.segments, radius, chamfer, name)
}
fn blend_closed_smooth_chain(
solid: &BrepSolid,
segments: &[ChainSegment<'_>],
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let mut budget = ChainBudget::for_chain(segments, CHAIN_PER_SEGMENT)?;
let first = loop {
let per_segment = budget.rung;
let rung = build_closed_smooth_chain(
solid,
segments,
radius,
chamfer,
name,
&budget,
FoldPolicy::Refuse,
bar,
);
match rung {
Ok(Rung::TooCoarse { deviation }) => {
crate::blend::carve::carve_trace(format_args!(
"blend chain: {} leaves the rails \
{deviation:.6e} from the rolling ball, against {bar:.3e}",
budget.describe()
));
if budget.largest() * 2 > CHAIN_CARVE_MAX_PER_SEGMENT {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "chain_ladder_top", format!(
"blend: at {per_segment} stations per segment — the top of the chain's \
ladder — this collar's fitted rails are still {deviation:.6e} from the \
rolling ball's own contacts against a bar of {bar:.3e}, so there is no \
wall accurate enough to build"
)));
}
budget = budget.doubled();
}
Ok(Rung::StationFailed(failure)) => {
crate::blend::carve::carve_trace(format_args!(
"blend chain: {per_segment} stations per segment: segment {} position {} \
(t {:.9}) continued from position {} (t {:.9}, seed {:?}) did not converge: {}",
failure.segment, failure.position, failure.t, failure.from_position,
failure.from_t, failure.seed_uv, failure.error
));
if budget.largest() * 2 > CHAIN_CARVE_MAX_PER_SEGMENT {
break Err(failure.error);
}
budget = budget.doubled();
}
other => break other,
}
};
if let Err(error) = &first {
crate::blend::carve::carve_trace(format_args!(
"blend chain: the march at {} refused: {error}", budget.describe()
));
}
match first {
Ok(Rung::Built(built)) => Ok(built),
Ok(Rung::TooCoarse { .. }) | Ok(Rung::StationFailed(_)) => {
Err(KernelRefusal::internal(KernelStage::Refine, "chain_ladder_rung", "blend: the chain ladder left an accuracy rung unhandled"))
}
Err(error) if crate::blend::fold::is_rail_fold(&error) => Err(error),
Err(error) if crate::blend::is_wall_fold(&error) && !chamfer => {
carve_ladder(solid, segments, radius, name, bar)
.map_err(|carve_error| {
if crate::blend::is_wall_fold(&carve_error) {
carve_error
} else {
crate::blend::fold::wall_fold_refusal(
Vec::new(),
format!(
"{} {} fits this edge: the wall folds, and the fold band could not \
be carved — {carve_error}",
crate::blend::WALL_FOLDS,
radius.abs()
),
)
}
})
}
Err(error) => Err(error),
}
}
fn carve_ladder(
solid: &BrepSolid,
segments: &[ChainSegment<'_>],
radius: f64,
name: Option<&str>,
bar: f64,
) -> Result<BrepSolid, KernelRefusal> {
let mut budget = ChainBudget::for_chain(segments, CHAIN_PER_SEGMENT)?;
loop {
let per_segment = budget.rung;
match build_closed_smooth_chain(
solid,
segments,
radius,
false,
name,
&budget,
FoldPolicy::Carve,
bar,
)? {
Rung::Built(built) => {
crate::blend::carve::carve_trace(format_args!(
"blend carve: {} is the first rung inside \
{bar:.3e}",
budget.describe()
));
return Ok(built);
}
Rung::StationFailed(failure) => return Err(failure.error),
Rung::TooCoarse { deviation } => {
crate::blend::carve::carve_trace(format_args!(
"blend carve: {} leaves the rails \
{deviation:.6e} from the rolling ball, against {bar:.3e}",
budget.describe()
));
if budget.largest() * 2 > CHAIN_CARVE_MAX_PER_SEGMENT {
return Err(crate::blend::fold::wall_fold_refusal(
Vec::new(),
format!(
"{} {} fits this edge: the wall folds, and at {per_segment} stations \
per segment — the top of the carve's ladder — its fitted rails are \
still {deviation:.6e} from the rolling ball's own contacts against a \
bar of {bar:.3e}, so there is no wall accurate enough to carve",
crate::blend::WALL_FOLDS,
radius.abs()
),
));
}
budget = budget.doubled();
}
}
}
}
enum Rung {
Built(BrepSolid),
TooCoarse { deviation: f64 },
StationFailed(ChainStationFailure),
}
fn rail_deviation(
samples: &[ChainSample],
cr: &NurbsCurve,
cs: &NurbsCurve,
fit_low: f64,
fit_range: f64,
budget: &ChainBudget,
bar: f64,
) -> Result<(f64, Vec<usize>), KernelRefusal> {
let mut order: Vec<usize> = (0..samples.len())
.filter(|&index| (0..=budget.count(samples[index].segment) as isize).contains(&samples[index].position))
.collect();
order.sort_by(|&a, &b| {
let (a, b) = (&samples[a], &samples[b]);
a.segment.cmp(&b.segment).then(a.position.cmp(&b.position)).then(a.offset.total_cmp(&b.offset))
});
let next_of: std::collections::HashMap<usize, f64> = order
.windows(2)
.filter(|pair| samples[pair[0]].segment == samples[pair[1]].segment)
.map(|pair| (pair[0], samples[pair[1]].parameter))
.collect();
let mut failing = Vec::new();
let closest = closest_on_rail;
let mut worst: f64 = 0.0;
let mut per_segment_worst: Vec<(f64, isize, f64, f64, Vec3, Vec3)> = Vec::new();
for &index in &order {
let sample = &samples[index];
let Some([p1, p2]) = sample.midpoint else {
continue;
};
let Some(&next) = next_of.get(&index) else {
continue;
};
let global = (0.5 * (sample.parameter + next)).rem_euclid(1.0);
let seed = (global - fit_low) / fit_range;
let reach = (next - sample.parameter).abs() / fit_range;
let (miss1, miss2) = (closest(cr, p1, seed, reach)?, closest(cs, p2, seed, reach)?);
worst = worst.max(miss1).max(miss2);
if miss1.max(miss2) > bar {
failing.push(index);
}
if per_segment_worst.len() <= sample.segment {
per_segment_worst.resize(
sample.segment + 1,
(0.0, 0, 0.0, 0.0, Vec3::default(), Vec3::default()),
);
}
if miss1.max(miss2) >= per_segment_worst[sample.segment].0 {
per_segment_worst[sample.segment] = (miss1.max(miss2), sample.position, miss1, miss2, p1, p2);
}
}
for (segment, (miss, position, miss1, miss2, p1, p2)) in per_segment_worst.iter().enumerate() {
crate::blend::carve::carve_trace(format_args!(
" rail miss segment {segment}: worst {miss:.6e} after station {position} \
(cr {miss1:.6e} at ({:.6}, {:.6}, {:.6}), cs {miss2:.6e} at ({:.6}, {:.6}, {:.6}))",
p1.x, p1.y, p1.z, p2.x, p2.y, p2.z
));
}
Ok((worst, failing))
}
const PIECE_TRACK_CEILING: usize = 4096;
fn closest_on_rail(curve: &NurbsCurve, target: Vec3, seed: f64, reach: f64) -> Result<f64, KernelRefusal> {
let [low, high] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let distance = |u: f64| -> Result<f64, KernelRefusal> {
Ok(curve.derivatives_extended(u, 0).or_refuse(KernelStage::Refine, "derivatives_extended")?[0].sub(target).length())
};
let mut u = seed.clamp(low, high);
let mut best = distance(u)?;
for _ in 0..40 {
let derivatives = curve.derivatives_extended(u, 2).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let offset = derivatives[0].sub(target);
let slope = offset.dot(derivatives[1]);
let curvature = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
let mut step = if curvature > 0.0 {
(slope / curvature).clamp(-reach, reach)
} else {
reach.copysign(slope)
};
let mut trial = (u - step).clamp(low, high);
let mut trial_distance = distance(trial)?;
for _ in 0..30 {
if trial_distance < best {
break;
}
step *= 0.5;
trial = (u - step).clamp(low, high);
trial_distance = distance(trial)?;
}
if !(trial_distance < best) {
break;
}
let moved = (trial - u).abs();
u = trial;
best = trial_distance;
if moved < 1e-15 {
break;
}
}
Ok(best)
}
pub(super) const LOCAL_RAIL_STENCIL: usize = 16;
fn rail_pair_miss(cr_miss: f64, cs_miss: f64) -> Option<f64> {
(cr_miss.is_finite() && cs_miss.is_finite()).then(|| cr_miss.max(cs_miss))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn dense_rail_misses(
contacts: &crate::blend::chain::DenseContacts,
samples: &[ChainSample],
cr: &NurbsCurve,
cs: &NurbsCurve,
low: f64,
range: f64,
wrap: bool,
) -> Result<(Vec<(usize, f64)>, Vec<usize>), KernelRefusal> {
let mut readable = Vec::with_capacity(contacts.solved.len());
let mut unreadable = Vec::new();
for (left, next, fraction, p1, p2) in &contacts.solved {
let (from, to) = (samples[*left].parameter, samples[*next].parameter);
let global = from + fraction * (to - from);
let global = if wrap { global.rem_euclid(1.0) } else { global };
let seed = (global - low) / range;
let reach = (to - from).abs() / range;
let cr_miss = closest_on_rail(cr, *p1, seed, reach).unwrap_or(f64::NAN);
let cs_miss = closest_on_rail(cs, *p2, seed, reach).unwrap_or(f64::NAN);
match rail_pair_miss(cr_miss, cs_miss) {
Some(miss) => readable.push((*left, miss)),
None => unreadable.push(*left),
}
}
Ok((readable, unreadable))
}
pub(super) fn combine_rail_verdict(
halfway: (f64, Vec<usize>),
dense: &[(usize, f64)],
unsolved: &[usize],
bar: f64,
) -> (f64, Vec<usize>) {
let (mut worst, mut failing) = halfway;
for &(left, miss) in dense {
if !miss.is_finite() {
failing.push(left);
continue;
}
if miss > bar {
failing.push(left);
}
worst = worst.max(miss);
}
failing.extend_from_slice(unsolved);
failing.sort_unstable();
failing.dedup();
(worst, failing)
}
pub(super) fn model_rounds_limit() -> usize {
REFINE_ROUNDS
}
pub(super) fn model_request_on() -> bool {
wall_verdict_on()
}
pub(super) fn wall_verdict_on() -> bool {
true
}
pub(super) const WALL_SECTION_PROBES: usize = 8;
pub(in crate::blend) fn sweep_residual(p: Vec3, nodes: &[Vec3], j: usize, radius: f64) -> Option<f64> {
let j = locate_foot(p, nodes, j, radius)?;
sweep_residual_on(p, nodes, (j - 1).min(nodes.len() - 4), j, radius)
}
const FOOT_TRAVEL: usize = 4;
fn locate_foot(p: Vec3, nodes: &[Vec3], j: usize, radius: f64) -> Option<usize> {
let n = nodes.len();
if n < 4 || j == 0 || j + 1 >= n {
return None;
}
let mut tried: Vec<usize> = Vec::new();
let mut at = j;
loop {
let base = (at - 1).min(n - 4);
match stencil_solve(p, nodes, base, at, radius) {
Solve::Read(_) => return Some(at),
Solve::Unread => return None,
Solve::Window(s) => {
tried.push(at);
let next = (base as f64 + s).round();
let next = if next.is_finite() { next.clamp(1.0, (n - 2) as f64) as usize } else { at };
if tried.contains(&next) || next.abs_diff(j) > FOOT_TRAVEL {
wall_decline(|| format!(
"foot search from node {j} of {n}: tried {tried:?}, next {next} ({})",
if tried.contains(&next) { "already tried" } else { "beyond FOOT_TRAVEL" }
));
return None;
}
at = next;
}
}
}
}
fn sweep_residual_on(p: Vec3, nodes: &[Vec3], base: usize, j: usize, radius: f64) -> Option<f64> {
match stencil_solve(p, nodes, base, j, radius) {
Solve::Read(distance) => Some(distance),
Solve::Window(_) | Solve::Unread => None,
}
}
enum Solve {
Read(f64),
Window(f64),
Unread,
}
thread_local! {
static WALL_DECLINES: std::cell::RefCell<Option<Vec<String>>> = const { std::cell::RefCell::new(None) };
}
fn wall_decline(reason: impl FnOnce() -> String) {
WALL_DECLINES.with(|log| {
if let Some(lines) = log.borrow_mut().as_mut() {
lines.push(reason());
}
});
}
pub(in crate::blend) fn declared_wall_probe_why(p: Vec3, fine: &[Vec3], jf: usize, radius: f64) -> Result<(f64, f64), String> {
let previous = WALL_DECLINES.with(|log| log.replace(Some(Vec::new())));
let read = declared_wall_probe(p, fine, jf, radius);
let lines = WALL_DECLINES.with(|log| log.replace(previous)).unwrap_or_default();
read.ok_or_else(|| if lines.is_empty() { "declined (no inner cause recorded)".to_string() } else { lines.join(" <- ") })
}
fn stencil_solve(p: Vec3, nodes: &[Vec3], base: usize, j: usize, radius: f64) -> Solve {
let finite = |v: Vec3| v.x.is_finite() && v.y.is_finite() && v.z.is_finite();
let n = nodes.len();
if n < 4 || j == 0 || j + 1 >= n || base + 4 > n || !radius.is_finite() || !finite(p) || !nodes.iter().all(|&c| finite(c)) {
wall_decline(|| format!("stencil {n} nodes base {base} j {j}: unreadable input"));
return Solve::Unread;
}
let [p0, p1, p2, p3] = [nodes[base], nodes[base + 1], nodes[base + 2], nodes[base + 3]];
let d1 = p1.sub(p0);
let d2 = p2.sub(p1.scale(2.0)).add(p0).scale(0.5);
let d3 = p3.sub(p2.scale(3.0)).add(p1.scale(3.0)).sub(p0).scale(1.0 / 6.0);
let curve = |s: f64| -> (Vec3, Vec3, Vec3) {
let c = p0.add(d1.scale(s)).add(d2.scale(s * (s - 1.0))).add(d3.scale(s * (s - 1.0) * (s - 2.0)));
let c1 = d1.add(d2.scale(2.0 * s - 1.0)).add(d3.scale(3.0 * s * s - 6.0 * s + 2.0));
let c2 = d2.scale(2.0).add(d3.scale(6.0 * s - 6.0));
(c, c1, c2)
};
let start = j as f64 - base as f64;
let mut s = start;
let mut converged = false;
for _ in 0..50 {
let (c, c1, c2) = curve(s);
let w = c.sub(p);
let (g, slope) = (w.dot(c1), c1.dot(c1) + w.dot(c2));
if !(g.is_finite() && slope.is_finite()) || slope == 0.0 {
wall_decline(|| format!("stencil {n} nodes base {base} j {j}: slope {slope:e} g {g:e} at s {s}"));
return Solve::Unread;
}
let stationary = g.abs() <= 16.0 * f64::EPSILON * (p.length() + c.length()) * c1.length();
if stationary {
converged = true;
break;
}
let step = g / slope;
let before = s;
s -= step;
if !((s - start).abs() <= 1.0) {
wall_decline(|| {
let (c, c1, _) = curve(s);
let g_after = c.sub(p).dot(c1);
let stationary_after = g_after.abs() <= 16.0 * f64::EPSILON * (p.length() + c.length()) * c1.length();
format!(
"stencil {n} nodes base {base} j {j}: window exit s {before} -> {s} (start {start}, |s - start| - 1 = {:e}), \
g {g:e} -> {g_after:e}, stationary at the overshot s: {stationary_after}",
(s - start).abs() - 1.0
)
});
return if s.is_finite() { Solve::Window(s) } else { Solve::Unread };
}
if step.abs() <= 1e-14 * (1.0 + s.abs()) {
converged = true;
break;
}
}
if !converged {
wall_decline(|| format!("stencil {n} nodes base {base} j {j}: no convergence in 50 steps, s {s}"));
return Solve::Unread;
}
let distance = curve(s).0.sub(p).length() - radius;
if distance.is_finite() {
Solve::Read(distance)
} else {
wall_decline(|| format!("stencil {n} nodes base {base} j {j}: non-finite distance at s {s}"));
Solve::Unread
}
}
pub(in crate::blend) fn wall_probe_reading(p: Vec3, nodes: &[Vec3], j: usize, radius: f64) -> Option<(f64, f64)> {
let n = nodes.len();
if n < 5 || j == 0 || j + 1 >= n {
return None;
}
let j = locate_foot(p, nodes, j, radius)?;
let first = (j - 1).min(n - 4);
let second = if j >= 2 { (j - 2).min(n - 4) } else { j.min(n - 4) };
let second = if second == first { if first >= 1 { first - 1 } else { first + 1 } } else { second };
let a = sweep_residual_on(p, nodes, first, j, radius)?;
let b = sweep_residual_on(p, nodes, second, j, radius)?;
Some((a.abs().max(b.abs()), (a - b).abs()))
}
pub(in crate::blend) fn declared_wall_probe(p: Vec3, fine: &[Vec3], jf: usize, radius: f64) -> Option<(f64, f64)> {
if jf % 2 != 0 || fine.len() < 9 || fine.len() % 2 == 0 {
wall_decline(|| format!("declared reader: node {jf} of {} not an even node of an odd fine set", fine.len()));
return None;
}
let Some((reading, spread)) = wall_probe_reading(p, fine, jf, radius) else {
wall_decline(|| format!("FINE two-stencil reading at node {jf} of {}", fine.len()));
return None;
};
let Some(at_fine) = sweep_residual(p, fine, jf, radius) else {
wall_decline(|| format!("FINE sweep at node {jf} of {}", fine.len()));
return None;
};
let coarse: Vec<Vec3> = fine.iter().step_by(2).copied().collect();
let Some(at_coarse) = sweep_residual(p, &coarse, jf / 2, radius) else {
wall_decline(|| format!("COARSE sweep at node {} of {}", jf / 2, coarse.len()));
return None;
};
let uncertainty = spread.max((at_fine - at_coarse).abs());
uncertainty.is_finite().then_some((reading, uncertainty))
}
pub(in crate::blend) fn centre_path_node(fine: &[Vec3], fraction: f64) -> Option<usize> {
let n = fine.len();
if n < 7 || !(fraction.is_finite()) {
return None;
}
let mut cumulative = Vec::with_capacity(n);
cumulative.push(0.0_f64);
for pair in fine.windows(2) {
let step = pair[1].sub(pair[0]).length();
cumulative.push(cumulative.last().copied().unwrap_or(0.0) + step);
}
let total = *cumulative.last()?;
if !(total.is_finite() && total > 0.0) {
return None;
}
let target = fraction.clamp(0.0, 1.0) * total;
let nearest = (0..n).min_by(|&a, &b| (cumulative[a] - target).abs().total_cmp(&(cumulative[b] - target).abs()))?;
let even = 2 * ((nearest as f64 / 2.0).round() as usize);
Some(even.clamp(2, (n - 3) & !1))
}
pub(in crate::blend) fn declared_wall_probe_located_why(p: Vec3, fine: &[Vec3], jf: usize, fraction: f64, radius: f64) -> Result<(f64, f64), String> {
let first = match declared_wall_probe_why(p, fine, jf, radius) {
Ok(read) => return Ok(read),
Err(why) => why,
};
match centre_path_node(fine, fraction) {
Some(located) if located != jf => declared_wall_probe_why(p, fine, located, radius)
.map_err(|again| format!("{first} -- and from centre-path node {located}: {again}")),
_ => Err(first),
}
}
pub(in crate::blend) fn declared_wall_probe_located(p: Vec3, fine: &[Vec3], jf: usize, fraction: f64, radius: f64) -> Option<(f64, f64)> {
match declared_wall_probe(p, fine, jf, radius) {
Some(read) => Some(read),
None => match centre_path_node(fine, fraction) {
Some(located) if located != jf => declared_wall_probe(p, fine, located, radius),
_ => None,
},
}
}
pub(in crate::blend) fn declared_wall_probe_at_why(p: Vec3, fine: &[Vec3], ts: &[f64], jf: usize, radius: f64) -> Result<(f64, f64), String> {
if jf % 2 != 0
|| fine.len() < 9
|| fine.len() % 2 == 0
|| ts.len() != fine.len()
|| !ts.iter().all(|t| t.is_finite())
|| !ts.windows(2).all(|pair| pair[1] > pair[0])
{
return Err(format!("native-abscissa reader: node {jf} of {} with {} finite, strictly increasing abscissae required", fine.len(), ts.len()));
}
let previous = WALL_DECLINES.with(|log| log.replace(Some(Vec::new())));
let read = (|| -> Option<(f64, f64)> {
let (reading, spread) = wall_probe_reading_at(p, fine, ts, jf, radius).or_else(|| {
wall_decline(|| format!("FINE two-stencil reading (native abscissae) at node {jf} of {}", fine.len()));
None
})?;
let at_fine = sweep_residual_at(p, fine, ts, jf, radius).or_else(|| {
wall_decline(|| format!("FINE sweep (native abscissae) at node {jf} of {}", fine.len()));
None
})?;
let coarse: Vec<Vec3> = fine.iter().step_by(2).copied().collect();
let coarse_ts: Vec<f64> = ts.iter().step_by(2).copied().collect();
let at_coarse = sweep_residual_at(p, &coarse, &coarse_ts, jf / 2, radius).or_else(|| {
wall_decline(|| format!("COARSE sweep (native abscissae) at node {} of {}", jf / 2, coarse.len()));
None
})?;
let uncertainty = spread.max((at_fine - at_coarse).abs());
uncertainty.is_finite().then_some((reading, uncertainty))
})();
let lines = WALL_DECLINES.with(|log| log.replace(previous)).unwrap_or_default();
read.ok_or_else(|| if lines.is_empty() { "declined (no inner cause recorded)".to_string() } else { lines.join(" <- ") })
}
fn wall_probe_reading_at(p: Vec3, nodes: &[Vec3], ts: &[f64], j: usize, radius: f64) -> Option<(f64, f64)> {
let n = nodes.len();
if n < 5 || j == 0 || j + 1 >= n {
return None;
}
let j = locate_foot_at(p, nodes, ts, j, radius)?;
let first = (j - 1).min(n - 4);
let second = if j >= 2 { (j - 2).min(n - 4) } else { j.min(n - 4) };
let second = if second == first { if first >= 1 { first - 1 } else { first + 1 } } else { second };
let a = match stencil_solve_at(p, nodes, ts, first, j, radius) { Solve::Read(distance) => distance, _ => return None };
let b = match stencil_solve_at(p, nodes, ts, second, j, radius) { Solve::Read(distance) => distance, _ => return None };
Some((a.abs().max(b.abs()), (a - b).abs()))
}
fn sweep_residual_at(p: Vec3, nodes: &[Vec3], ts: &[f64], j: usize, radius: f64) -> Option<f64> {
let j = locate_foot_at(p, nodes, ts, j, radius)?;
match stencil_solve_at(p, nodes, ts, (j - 1).min(nodes.len() - 4), j, radius) {
Solve::Read(distance) => Some(distance),
Solve::Window(_) | Solve::Unread => None,
}
}
fn locate_foot_at(p: Vec3, nodes: &[Vec3], ts: &[f64], j: usize, radius: f64) -> Option<usize> {
let n = nodes.len();
if n < 4 || j == 0 || j + 1 >= n {
return None;
}
let mut tried: Vec<usize> = Vec::new();
let mut at = j;
loop {
let base = (at - 1).min(n - 4);
match stencil_solve_at(p, nodes, ts, base, at, radius) {
Solve::Read(_) => return Some(at),
Solve::Unread => return None,
Solve::Window(t) => {
tried.push(at);
let nearest = (0..n).min_by(|&a, &b| (ts[a] - t).abs().total_cmp(&(ts[b] - t).abs())).unwrap_or(at);
let next = if t.is_finite() { nearest.clamp(1, n - 2) } else { at };
if tried.contains(&next) || next.abs_diff(j) > FOOT_TRAVEL {
wall_decline(|| format!(
"foot search (native abscissae) from node {j} of {n}: tried {tried:?}, next {next} ({})",
if tried.contains(&next) { "already tried" } else { "beyond FOOT_TRAVEL" }
));
return None;
}
at = next;
}
}
}
}
fn stencil_solve_at(p: Vec3, nodes: &[Vec3], ts: &[f64], base: usize, j: usize, radius: f64) -> Solve {
let finite = |v: Vec3| v.x.is_finite() && v.y.is_finite() && v.z.is_finite();
let n = nodes.len();
if n < 4 || j == 0 || j + 1 >= n || base + 4 > n || ts.len() != n || !radius.is_finite() || !finite(p) || !nodes.iter().all(|&c| finite(c)) {
wall_decline(|| format!("native stencil {n} nodes base {base} j {j}: unreadable input"));
return Solve::Unread;
}
let [p0, p1, p2, p3] = [nodes[base], nodes[base + 1], nodes[base + 2], nodes[base + 3]];
let [t0, t1, t2, t3] = [ts[base], ts[base + 1], ts[base + 2], ts[base + 3]];
let d1 = p1.sub(p0).scale(1.0 / (t1 - t0));
let d12 = p2.sub(p1).scale(1.0 / (t2 - t1));
let d23 = p3.sub(p2).scale(1.0 / (t3 - t2));
let d2 = d12.sub(d1).scale(1.0 / (t2 - t0));
let d123 = d23.sub(d12).scale(1.0 / (t3 - t1));
let d3 = d123.sub(d2).scale(1.0 / (t3 - t0));
let curve = |t: f64| -> (Vec3, Vec3, Vec3) {
let (a, b, c) = (t - t0, t - t1, t - t2);
let point = p0.add(d1.scale(a)).add(d2.scale(a * b)).add(d3.scale(a * b * c));
let first = d1.add(d2.scale(a + b)).add(d3.scale(b * c + a * c + a * b));
let second = d2.scale(2.0).add(d3.scale(2.0 * (a + b + c)));
(point, first, second)
};
let (low, high) = (ts[j - 1], ts[j + 1]);
let start = ts[j];
let mut t = start;
let mut converged = false;
for _ in 0..50 {
let (c, c1, c2) = curve(t);
let w = c.sub(p);
let (g, slope) = (w.dot(c1), c1.dot(c1) + w.dot(c2));
if !(g.is_finite() && slope.is_finite()) || slope == 0.0 {
wall_decline(|| format!("native stencil {n} nodes base {base} j {j}: slope {slope:e} g {g:e} at t {t}"));
return Solve::Unread;
}
if g.abs() <= 16.0 * f64::EPSILON * (p.length() + c.length()) * c1.length() {
converged = true;
break;
}
let step = g / slope;
let before = t;
t -= step;
if !(t >= low && t <= high) {
wall_decline(|| format!("native stencil {n} nodes base {base} j {j}: window exit t {before} -> {t} (window [{low}, {high}])"));
return if t.is_finite() { Solve::Window(t) } else { Solve::Unread };
}
let node = 0.5 * (high - low);
if (step / node).abs() <= 1e-14 * (1.0 + ((t - t0) / node).abs()) {
converged = true;
break;
}
}
if !converged {
wall_decline(|| format!("native stencil {n} nodes base {base} j {j}: no convergence in 50 steps, t {t}"));
return Solve::Unread;
}
let distance = curve(t).0.sub(p).length() - radius;
if distance.is_finite() {
Solve::Read(distance)
} else {
wall_decline(|| format!("native stencil {n} nodes base {base} j {j}: non-finite distance at t {t}"));
Solve::Unread
}
}
pub(in crate::blend) fn wall_model_missing(readings: &[(usize, f64, f64, f64)], model: f64) -> Vec<usize> {
readings.iter().filter(|&&(_, declared, _, _)| !(declared <= model)).map(|&(left, _, _, _)| left).collect()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn wall_interior_misses(
contacts: &crate::blend::chain::DenseContacts,
samples: &[ChainSample],
surface: &crate::NurbsSurface,
low: f64,
range: f64,
radius: f64,
subdivisions: usize,
probes: usize,
wrap: bool,
) -> (Vec<(usize, f64, f64, f64)>, Vec<usize>) {
use std::collections::BTreeMap;
let mut nodes: BTreeMap<usize, (usize, Vec<Option<Vec3>>)> = BTreeMap::new();
for (index, (left, next, fraction, _, _)) in contacts.solved.iter().enumerate() {
let entry = nodes.entry(*left).or_insert_with(|| {
let mut row = vec![None; subdivisions + 1];
row[0] = Some(samples[*left].station.center);
row[subdivisions] = Some(samples[*next].station.center);
(*next, row)
});
let step = (fraction * subdivisions as f64).round() as usize;
if (1..subdivisions).contains(&step) {
entry.1[step] = contacts.centers.get(index).copied().flatten();
}
}
let mut readable = Vec::new();
let mut unreadable: Vec<usize> = contacts.unsolved_at.clone();
for (&left, (next, row)) in &nodes {
let Some(centres) = row.iter().copied().collect::<Option<Vec<Vec3>>>() else {
unreadable.push(left);
continue;
};
let (from, to) = (samples[left].parameter, samples[*next].parameter);
let (mut declared, mut worst, mut spread) = (0.0_f64, 0.0_f64, 0.0_f64);
let mut read = true;
'probes: for step in (2..subdivisions).step_by(2) {
let global = from + (step as f64 / subdivisions as f64) * (to - from);
let global = if wrap { global.rem_euclid(1.0) } else { global };
let u = (global - low) / range;
for k in 1..probes {
let v = k as f64 / probes as f64;
let reading = surface.evaluate(u, v).ok().and_then(|p| {
declared_wall_probe_located(p, ¢res, step, step as f64 / subdivisions as f64, radius)
});
match reading {
Some((distance, gap)) => {
declared = declared.max(distance + gap);
worst = worst.max(distance);
spread = spread.max(gap);
}
None => {
read = false;
break 'probes;
}
}
}
}
if read {
readable.push((left, declared, worst, spread));
} else {
unreadable.push(left);
}
}
unreadable.sort_unstable();
unreadable.dedup();
(readable, unreadable)
}
fn build_closed_smooth_chain(
solid: &BrepSolid,
segments: &[ChainSegment<'_>],
radius: f64,
chamfer: bool,
name: Option<&str>,
budget: &ChainBudget,
fold: FoldPolicy,
bar: f64,
) -> Result<Rung, KernelRefusal> {
let samples = match march_chain_typed(segments, radius, false, budget, fold) {
Ok(samples) => samples,
Err(ChainMarchError::Station(failure)) => return Ok(Rung::StationFailed(failure)),
Err(ChainMarchError::Incoherent(failure)) => return Err(failure.error),
Err(ChainMarchError::Other(error)) => return Err(error),
};
CHAIN_DEEPEST_ROUND.with(|deepest| deepest.set(0));
build_closed_chain_from_samples(solid, segments, radius, chamfer, name, budget, fold, bar, samples, 0)
}
thread_local! {
static CHAIN_DEEPEST_ROUND: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[allow(clippy::too_many_arguments)]
fn build_closed_chain_from_samples(
solid: &BrepSolid,
segments: &[ChainSegment<'_>],
radius: f64,
chamfer: bool,
name: Option<&str>,
budget: &ChainBudget,
fold: FoldPolicy,
bar: f64,
mut samples: Vec<ChainSample>,
round: usize,
) -> Result<Rung, KernelRefusal> {
let per_segment = budget.rung;
CHAIN_DEEPEST_ROUND.with(|deepest| deepest.set(deepest.get().max(round)));
let mut global: Vec<(f64, &ChainSample)> = samples
.iter()
.filter(|sample| (0..budget.count(sample.segment) as isize).contains(&sample.position))
.map(|sample| (sample.parameter.rem_euclid(1.0), sample))
.collect();
global.sort_by(|a, b| a.0.total_cmp(&b.0));
let count = global.len();
let degree = FIT_DEGREE;
let mut extended_params = Vec::new();
let mut samples_cr = Vec::new();
let mut samples_mid = Vec::new();
let mut samples_cs = Vec::new();
let mut push = |station: &Station, parameter: f64| {
extended_params.push(parameter);
samples_cr.push(Vec4::from_point(station.p1, 1.0));
samples_cs.push(Vec4::from_point(station.p2, 1.0));
samples_mid.push(Vec4 {
x: station.apex.x * station.weight,
y: station.apex.y * station.weight,
z: station.apex.z * station.weight,
w: station.weight,
});
};
for offset in (1..=degree).rev() {
let (parameter, sample) = &global[count - offset];
push(&sample.station, parameter - 1.0);
}
for (parameter, sample) in &global {
push(&sample.station, *parameter);
}
push(&global[0].1.station, global[0].0 + 1.0);
for offset in 1..=degree {
let (parameter, sample) = &global[offset];
push(&sample.station, parameter + 1.0);
}
let low = extended_params[0];
let high = *extended_params.last().unwrap();
let range = high - low;
let normalized: Vec<f64> = extended_params
.iter()
.map(|parameter| (parameter - low) / range)
.collect();
let seam_low = (0.0 - low) / range;
let seam_high = (1.0 - low) / range;
let fit_row = |row: &[Vec4]| -> Result<NurbsCurve, KernelRefusal> {
let curve = fit::interpolate_homogeneous(row, degree, &normalized).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let (_, tail) = curve.split(seam_low).or_refuse(KernelStage::Refine, "split")?;
let (middle, _) = tail.split(seam_high).or_refuse(KernelStage::Refine, "split")?;
Ok(middle)
};
let cr = fit_row(&samples_cr)?;
let cs = fit_row(&samples_cs)?;
let mid = if chamfer {
None
} else {
Some(fit_row(&samples_mid)?)
};
let u_domain = cr.domain().or_refuse(KernelStage::Refine, "domain")?;
let surface = crate::blend::rows::surface_from_rows(degree, &cr, &cs, mid.as_ref(), true)?;
let (halfway, halfway_failing) = rail_deviation(&samples, &cr, &cs, low, range, budget, bar)?;
let stencil = if round > 0 {
Some(crate::blend::chain::chain_dense_contacts(segments, radius, budget, &samples, LOCAL_RAIL_STENCIL)?)
} else {
None
};
let wall_stencil = if !chamfer && wall_verdict_on() {
Some(crate::blend::chain::chain_dense_contacts(segments, radius, budget, &samples, 2 * LOCAL_RAIL_STENCIL)?)
} else {
None
};
let (mut deviation, mut failing, _stencil_reads) = if round > 0 {
let contacts = stencil.as_ref().expect("the local path solves the stencil");
let (dense, unreadable) = dense_rail_misses(contacts, &samples, &cr, &cs, low, range, true)?;
let reads = dense.len() + unreadable.len() + contacts.unsolved;
let failed_to_read: Vec<usize> = contacts.unsolved_at.iter().chain(&unreadable).copied().collect();
let verdict = combine_rail_verdict((halfway, halfway_failing), &dense, &failed_to_read, bar);
crate::blend::carve::carve_trace(format_args!(
"blend chain local rails: rung {per_segment} round {round}: stencil verdict {:.9e} (halfway {halfway:.9e}), \
{} failing interval(s), {} unsolved contact(s)",
verdict.0, verdict.1.len(), contacts.unsolved
));
(verdict.0, verdict.1, reads)
} else {
(halfway, halfway_failing, 0)
};
let mut _wall_worst = 0.0_f64;
let mut _wall_spread = 0.0_f64;
let mut wall_readings: Vec<(usize, f64, f64, f64)> = Vec::new();
if let Some(contacts) = wall_stencil.as_ref() {
let (wall, unreadable) =
wall_interior_misses(contacts, &samples, &surface, low, range, radius, 2 * LOCAL_RAIL_STENCIL, WALL_SECTION_PROBES, true);
wall_readings = wall.clone();
for &(left, declared, _, uncertainty) in &wall {
if declared > bar {
failing.push(left);
}
_wall_worst = _wall_worst.max(declared);
_wall_spread = _wall_spread.max(uncertainty);
}
failing.extend_from_slice(&unreadable);
failing.sort_unstable();
failing.dedup();
deviation = deviation.max(_wall_worst);
crate::blend::carve::carve_trace(format_args!(
"blend chain wall: rung {per_segment} round {round}: wall interior worst {_wall_worst:.9e} over {} \
interval(s), {} unreadable, bar {bar:.3e}",
wall.len(),
unreadable.len()
));
}
if deviation > bar || !failing.is_empty() {
let in_segment = |segment: usize| {
samples
.iter()
.filter(|sample| sample.segment == segment && (0..budget.count(segment) as isize).contains(&sample.position))
.count()
};
let within_ceiling = (0..segments.len()).all(|segment| {
in_segment(segment) + failing.iter().filter(|&&index| samples[index].segment == segment).count()
<= CHAIN_CARVE_MAX_PER_SEGMENT
});
crate::blend::carve::carve_trace(format_args!(
"blend chain local rails: rung {per_segment} round {round} stations {} failing {} \
worst {deviation:.9e} bar {bar:.9e} ceiling {}",
samples.len(),
failing.len(),
if within_ceiling { "ok" } else { "reached" }
));
if fold != FoldPolicy::Refuse || round >= REFINE_ROUNDS || !within_ceiling {
return Ok(Rung::TooCoarse { deviation });
}
CHAIN_DEEPEST_ROUND.with(|deepest| deepest.set(deepest.get().max(round + 1)));
match insert_chain_stations(segments, radius, budget, &mut samples, &failing) {
Ok(()) => {}
Err(ChainMarchError::Station(failure)) => return Ok(Rung::StationFailed(failure)),
Err(ChainMarchError::Incoherent(failure)) => {
crate::blend::carve::carve_trace(format_args!(
"blend chain local rails: rung {per_segment} round {round}: segment {} station \
at t {:.9} does not reach its retained right neighbour: {}",
failure.segment, failure.t, failure.error
));
return Ok(Rung::TooCoarse { deviation });
}
Err(ChainMarchError::Other(error)) => return Err(error),
}
assign_chain_parameters(&mut samples, segments.len(), budget, false);
return build_closed_chain_from_samples(
solid, segments, radius, chamfer, name, budget, fold, bar, samples, round + 1,
);
}
crate::blend::carve::carve_trace(format_args!(
"blend chain: {per_segment} stations per segment leaves the rails {deviation:.6e} \
from the rolling ball, inside {bar:.3e}"
));
let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
let stations_now = (0..segments.len())
.map(|segment| {
samples
.iter()
.filter(|sample| sample.segment == segment && (0..budget.count(segment) as isize).contains(&sample.position))
.count()
})
.max()
.unwrap_or(0);
let mut unmet: Option<(f64, crate::BudgetReason)> = None;
let mut request_detail: Option<String> = None;
let mut request_component = crate::MeasuredComponent::Wall;
let mut request_unread = 0usize;
if !chamfer && fold == FoldPolicy::Refuse && model_request_on() {
let wall_missing = wall_model_missing(&wall_readings, model);
let (rail_dense, rail_unread) = match wall_stencil.as_ref() {
Some(contacts) => dense_rail_misses(contacts, &samples, &cr, &cs, low, range, true)?,
None => (Vec::new(), Vec::new()),
};
let rail_over: std::collections::BTreeSet<usize> =
rail_dense.iter().filter(|&&(_, miss)| !(miss <= model)).map(|&(left, _)| left).collect();
let rail_unread_intervals: std::collections::BTreeSet<usize> = rail_unread.iter().copied().collect();
let rail_missing: Vec<usize> = rail_over.union(&rail_unread_intervals).copied().collect();
let mut missing: Vec<usize> = wall_missing.iter().chain(&rail_missing).copied().collect();
missing.sort_unstable();
missing.dedup();
if !missing.is_empty() {
let wall_short = wall_readings
.iter()
.filter(|reading| wall_missing.contains(&reading.0))
.map(|&(_, declared, _, _)| declared)
.fold(0.0_f64, f64::max);
let rail_short = rail_dense.iter().filter(|&&(_, miss)| !(miss <= model)).map(|&(_, miss)| miss).fold(0.0_f64, f64::max);
let short = wall_short.max(rail_short);
request_component = if rail_short > wall_short { crate::MeasuredComponent::Rails } else { crate::MeasuredComponent::Wall };
request_unread = rail_unread_intervals.len();
request_detail = Some(format!(
"short of {model:.3e}: rails {rail_short:.3e} off the exact contacts over {} interval(s) ({} unread), \
wall {wall_short:.3e} over {} interval(s)",
rail_over.len(),
rail_unread_intervals.len(),
wall_missing.len()
));
let within_ceiling = (0..segments.len()).all(|segment| {
let present = samples
.iter()
.filter(|sample| sample.segment == segment && (0..budget.count(segment) as isize).contains(&sample.position))
.count();
present + missing.iter().filter(|&&index| samples[index].segment == segment).count()
<= CHAIN_CARVE_MAX_PER_SEGMENT
});
crate::blend::carve::carve_trace(format_args!(
"blend chain wall model: rung {per_segment} round {round}: {} interval(s) short of {model:.3e} \
(worst reading + spread {short:.3e}), ceiling {}",
missing.len(),
if within_ceiling { "ok" } else { "reached" }
));
unmet = if round >= model_rounds_limit() {
Some((short, crate::BudgetReason::RoundsSpent))
} else if !within_ceiling {
Some((short, crate::BudgetReason::StationCeiling))
} else {
let mut refined = samples.clone();
CHAIN_DEEPEST_ROUND.with(|deepest| deepest.set(deepest.get().max(round + 1)));
match insert_chain_stations(segments, radius, budget, &mut refined, &missing) {
Ok(()) => {
assign_chain_parameters(&mut refined, segments.len(), budget, false);
match build_closed_chain_from_samples(
solid, segments, radius, chamfer, name, budget, fold, bar, refined, round + 1,
) {
Ok(Rung::Built(built)) => return Ok(Rung::Built(built)),
_ => Some((short, crate::BudgetReason::Rejected)),
}
}
Err(_) => {
CHAIN_DEEPEST_ROUND.with(|deepest| deepest.set(deepest.get().max(round + 1)));
Some((short, crate::BudgetReason::Incoherent))
}
}
};
}
}
if let Some(subdivisions) = std::env::var("BREP_DEBUG_CHAIN_DENSE_RAILS").ok().and_then(|value| value.parse::<usize>().ok()) {
let contacts = crate::blend::chain::chain_dense_contacts(segments, radius, budget, &samples, subdivisions.max(2))?;
let mut worst = (0.0_f64, 0usize, 0isize, 0.0_f64);
let mut unreadable = 0usize;
for (left, next, fraction, p1, p2) in &contacts.solved {
let (from, to) = (samples[*left].parameter, samples[*next].parameter);
let global = (from + fraction * (to - from)).rem_euclid(1.0);
let seed = (global - low) / range;
let reach = (to - from).abs() / range;
match rail_pair_miss(closest_on_rail(&cr, *p1, seed, reach).unwrap_or(f64::NAN), closest_on_rail(&cs, *p2, seed, reach).unwrap_or(f64::NAN)) {
Some(miss) if miss > worst.0 => worst = (miss, samples[*left].segment, samples[*left].position, *fraction),
Some(_) => {}
None => unreadable += 1,
}
}
eprintln!(
"blend chain dense rails: rung {per_segment} round {round}: {} contacts at {subdivisions} per interval, \
worst {:.9e} (halfway reading {halfway:.9e}, bar {bar:.3e}) at segment {} position {} fraction {:.4}; {} unsolved, {unreadable} unreadable",
contacts.solved.len(), worst.0, worst.1, worst.2, worst.3, contacts.unsolved
);
}
let crease = if fold == FoldPolicy::Carve {
let bar = crate::KernelTolerances::for_solid(solid, 1e-7).pcurve_consistency;
let Some(carved) = crate::blend::carve::trace_wall_crease(&surface, radius.abs())? else {
return Err(KernelRefusal::internal(KernelStage::Refine, "carve_no_fold", format!(
"blend carve: the wall re-marched at {per_segment} stations per segment carries no fold to carve, but the march at {CHAIN_PER_SEGMENT} refused one"
)));
};
Some(crate::blend::carve::fit_crease(&surface, &carved, bar)?)
} else {
None
};
let single_face = |side: usize| -> bool {
let first_id = segments[0].mate(side).face.id;
segments
.iter()
.all(|segment| segment.mate(side).face.id == first_id)
};
let mut whole_pcurves: [Option<NurbsCurve>; 2] = [None, None];
for side in 0..2 {
if !single_face(side) {
continue;
}
let mut row = Vec::new();
let select = |station: &Station| -> [f64; 2] {
if side == 0 {
station.uv1
} else {
station.uv2
}
};
let mut push_uv = |station: &Station| {
let uv = select(station);
row.push(Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0));
};
for offset in (1..=degree).rev() {
push_uv(&global[count - offset].1.station);
}
for (_, sample) in &global {
push_uv(&sample.station);
}
push_uv(&global[0].1.station);
for offset in 1..=degree {
push_uv(&global[offset].1.station);
}
whole_pcurves[side] = Some(fit_row(&row)?);
}
let mut pcurves1 = Vec::with_capacity(segments.len());
let mut pcurves2 = Vec::with_capacity(segments.len());
for segment in 0..segments.len() {
let mut window: Vec<&ChainSample> = samples
.iter()
.filter(|sample| sample.segment == segment)
.collect();
window.sort_by(|a, b| a.parameter.total_cmp(&b.parameter));
let params: Vec<f64> = window.iter().map(|sample| sample.parameter).collect();
let low = params[0];
let high = *params.last().unwrap();
let normalized: Vec<f64> = params
.iter()
.map(|parameter| (parameter - low) / (high - low))
.collect();
let fit_uv = |select: &dyn Fn(&Station) -> [f64; 2]| -> Result<NurbsCurve, KernelRefusal> {
let points: Vec<Vec4> = window
.iter()
.map(|sample| {
let uv = select(&sample.station);
Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0)
})
.collect();
let curve = fit::interpolate_homogeneous(&points, degree, &normalized).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
Ok(curve)
};
pcurves1.push((low, high, fit_uv(&|station| station.uv1)?));
pcurves2.push((low, high, fit_uv(&|station| station.uv2)?));
}
let wall_surface = unmet.as_ref().map(|_| surface.clone());
let built = chain_surgery(
solid,
segments,
ChainRows {
surface,
cr,
cs,
u_domain,
fit_low: low,
fit_range: range,
pcurves1,
pcurves2,
whole_pcurves,
crease,
},
name,
)?;
if let (Some((residual, reason)), Some(surface)) = (unmet, wall_surface) {
crate::blend::record_wall_model_report(crate::blend::WallModelReport {
name: name.map(str::to_string),
surface,
request: model,
residual,
detail: request_detail,
budget: crate::ApproximationBudget {
reason,
rounds_used: CHAIN_DEEPEST_ROUND.with(|deepest| deepest.get()).max(round),
rounds_limit: model_rounds_limit(),
stations: stations_now,
station_limit: CHAIN_CARVE_MAX_PER_SEGMENT,
mechanism: crate::BudgetMechanism::LocalRounds,
measured_component: request_component,
unread: request_unread,
},
});
}
Ok(Rung::Built(built))
}
pub(super) struct ChainRows {
pub(super) surface: NurbsSurface,
pub(super) cr: NurbsCurve,
pub(super) cs: NurbsCurve,
pub(super) u_domain: [f64; 2],
pub(super) fit_low: f64,
pub(super) fit_range: f64,
pub(super) pcurves1: Vec<(f64, f64, NurbsCurve)>,
pub(super) pcurves2: Vec<(f64, f64, NurbsCurve)>,
pub(super) whole_pcurves: [Option<NurbsCurve>; 2],
pub(super) crease: Option<crate::blend::carve::CarvedCrease>,
}
impl ChainRows {
pub(super) fn to_global(&self, fit_parameter: f64) -> f64 {
self.fit_low + fit_parameter * self.fit_range
}
}
pub(super) fn pcurve_portion(fitted: &(f64, f64, NurbsCurve), a: f64, b: f64) -> Result<NurbsCurve, KernelRefusal> {
let (low, high, curve) = fitted;
let to_local = |value: f64| {
let mut best = value;
for candidate in [value, value + 1.0, value - 1.0] {
if candidate >= low - 1e-9 && candidate <= high + 1e-9 {
best = candidate;
break;
}
}
((best - low) / (high - low)).clamp(0.0, 1.0)
};
let mut local_a = to_local(a);
let mut local_b = to_local(b);
if local_a > local_b {
std::mem::swap(&mut local_a, &mut local_b);
}
let epsilon = 1e-9;
let (_, tail) = if local_a > epsilon {
curve.split(local_a).or_refuse(KernelStage::Refine, "split")?
} else {
(curve.clone(), curve.clone())
};
let tail = if local_a > epsilon {
tail
} else {
curve.clone()
};
let portion = if local_b < 1.0 - epsilon {
tail.split(local_b).or_refuse(KernelStage::Refine, "split")?.0
} else {
tail
};
Ok(portion)
}
pub(super) fn cross_edge_at(
solid: &BrepSolid,
face_before: &FaceRecord,
loop_before: usize,
face_after: &FaceRecord,
loop_after: usize,
vertex: u64,
before_edge: u64,
after_edge: u64,
) -> Result<Option<u64>, KernelRefusal> {
let mut found: Option<u64> = None;
let mut consider = |face: &FaceRecord, loop_index: usize| -> Result<(), KernelRefusal> {
for coedge in &face.loops[loop_index].coedges {
if coedge.edge_id == before_edge || coedge.edge_id == after_edge {
continue;
}
let edge = solid
.edges
.iter()
.find(|edge| edge.id == coedge.edge_id)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "missing_edge", "blend: loop references missing edge"))?;
if edge.start_vertex_id == vertex || edge.end_vertex_id == vertex {
if found.is_some() && found != Some(edge.id) {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "chain_cross_edges", "blend: multiple cross edges at a chain vertex"));
}
found = Some(edge.id);
}
}
Ok(())
};
consider(face_before, loop_before)?;
if !(face_after.id == face_before.id && loop_after == loop_before) {
consider(face_after, loop_after)?;
}
Ok(found)
}
fn nearest_seam_image(value: f64, low: f64, high: f64) -> f64 {
let period = high - low;
low + ((value - low) / period).round() * period
}
pub(in crate::blend) fn project_piece_pcurve(
piece: &NurbsCurve,
surface: &NurbsSurface,
start_is_crossing: bool,
end_is_crossing: bool,
) -> Result<NurbsCurve, KernelRefusal> {
project_piece_pcurve_from(piece, surface, start_is_crossing, end_is_crossing, None)
}
pub(in crate::blend) fn pcurve_guide<'a>(old: &'a NurbsCurve, surface: &NurbsSurface) -> Result<impl Fn(f64) -> Option<[f64; 2]> + 'a, KernelRefusal> {
let [o0, o1] = old.domain().or_refuse(KernelStage::Refine, "domain")?;
let (closed_u, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let into = move |value: f64, closed: bool, low: f64, high: f64| {
if closed && high > low {
low + (value - low).rem_euclid(high - low)
} else {
value.clamp(low, high)
}
};
Ok(move |fraction: f64| {
let uv = old.evaluate(o0 + (o1 - o0) * fraction).ok()?;
let seed = [into(uv.x, closed_u, u0, u1), into(uv.y, closed_v, v0, v1)];
seed.iter().all(|value| value.is_finite()).then_some(seed)
})
}
pub(in crate::blend) fn project_piece_pcurve_from(
piece: &NurbsCurve,
surface: &NurbsSurface,
start_is_crossing: bool,
end_is_crossing: bool,
guide: Option<&dyn Fn(f64) -> Option<[f64; 2]>>,
) -> Result<NurbsCurve, KernelRefusal> {
project_piece_pcurve_within(piece, surface, start_is_crossing, end_is_crossing, guide, &[])
}
pub(in crate::blend) fn project_piece_pcurve_within(
piece: &NurbsCurve,
surface: &NurbsSurface,
start_is_crossing: bool,
end_is_crossing: bool,
guide: Option<&dyn Fn(f64) -> Option<[f64; 2]>>,
refine_near: &[f64],
) -> Result<NurbsCurve, KernelRefusal> {
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Refine, "domain_v")?;
let (_, closed_v) = surface.closed_directions().or_refuse(KernelStage::Refine, "closed_directions")?;
let period = u1 - u0;
let period_v = v1 - v0;
let [d0, d1] = piece.domain().or_refuse(KernelStage::Refine, "domain")?;
let dense = (8 * piece.control_points.len()).clamp(96, PIECE_TRACK_CEILING);
crate::blend::record_count("count.track.piece_samples", dense as u64 + 1);
let mut proj_u = Vec::with_capacity(dense + 1);
let mut proj_v = Vec::with_capacity(dense + 1);
let mut point3 = Vec::with_capacity(dense + 1);
let mut params = Vec::with_capacity(dense + 1);
let mut previous_u: Option<f64> = None;
let mut previous_foot: Option<[f64; 2]> = None;
let mut feet: Vec<[f64; 2]> = Vec::with_capacity(dense + 1);
for section in 0..=dense {
let t = d0 + (d1 - d0) * section as f64 / dense as f64;
let point = piece.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
let guide = guide.filter(|_| {
true
});
let seeds: Vec<[f64; 2]> = guide.and_then(|guide| guide(section as f64 / dense as f64)).into_iter().chain(previous_foot).collect();
let mut projection: Option<crate::projection::SurfaceProjection> = None;
for seed in &seeds {
let found = crate::projection::project_point_to_surface_from_seed(surface, point, *seed).or_refuse(KernelStage::Refine, "project_point_to_surface_from_seed")?;
if projection.as_ref().map_or(true, |known| found.distance < known.distance) {
projection = Some(found);
}
}
let projection = match projection {
Some(projection) => projection,
None => crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?,
};
previous_foot = Some([projection.u, projection.v]);
feet.push([projection.u, projection.v]);
let mut u = projection.u;
if let Some(previous) = previous_u {
while u - previous > period * 0.5 {
u -= period;
}
while previous - u > period * 0.5 {
u += period;
}
}
previous_u = Some(u);
proj_u.push(u);
proj_v.push(projection.v);
point3.push(point);
params.push(section as f64 / dense as f64);
}
if closed_v {
for index in 2..=dense {
while proj_v[index] - proj_v[index - 1] > period_v * 0.5 {
proj_v[index] -= period_v;
}
while proj_v[index - 1] - proj_v[index] > period_v * 0.5 {
proj_v[index] += period_v;
}
}
while proj_v[0] - proj_v[1] > period_v * 0.5 {
proj_v[0] -= period_v;
}
while proj_v[1] - proj_v[0] > period_v * 0.5 {
proj_v[0] += period_v;
}
}
let mean_u: f64 = proj_u[1..dense].iter().sum::<f64>() / (dense - 1) as f64;
let seam_u = if mean_u - u0 > u1 - mean_u { u1 } else { u0 };
let pinned = |index: usize,
neighbour: usize,
proj_u: &[f64],
proj_v: &[f64]|
-> Result<(f64, f64), KernelRefusal> {
let point = point3[index];
let u_error = surface
.evaluate(seam_u, proj_v[index]).or_refuse(KernelStage::Refine, "evaluate")?
.sub(point)
.length();
if closed_v {
let seam_v = nearest_seam_image(proj_v[neighbour], v0, v1);
let v_error = surface
.evaluate(proj_u[index], seam_v).or_refuse(KernelStage::Refine, "evaluate")?
.sub(point)
.length();
if v_error < u_error {
return Ok((proj_u[index], seam_v));
}
}
Ok((seam_u, proj_v[index]))
};
if start_is_crossing {
let (u, v) = pinned(0, 1, &proj_u, &proj_v)?;
proj_u[0] = u;
proj_v[0] = v;
}
if end_is_crossing {
let (u, v) = pinned(dense, dense - 1, &proj_u, &proj_v)?;
proj_u[dense] = u;
proj_v[dense] = v;
}
let mut shift = 0.0;
while mean_u + shift > u1 + 1e-9 {
shift -= period;
}
while mean_u + shift < u0 - 1e-9 {
shift += period;
}
for u in proj_u.iter_mut() {
*u += shift;
}
if closed_v {
let mean_v: f64 = proj_v[1..dense].iter().sum::<f64>() / (dense - 1) as f64;
let mut shift_v = 0.0;
while mean_v + shift_v > v1 + 1e-9 {
shift_v -= period_v;
}
while mean_v + shift_v < v0 - 1e-9 {
shift_v += period_v;
}
for v in proj_v.iter_mut() {
*v += shift_v;
}
}
let tolerance = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
let at = |fraction: f64| piece.evaluate(d0 + (d1 - d0) * fraction);
let mut track = crate::blend::track_fit::Track {
fractions: params,
uv: proj_u.iter().zip(&proj_v).map(|(u, v)| [*u, *v]).collect(),
feet,
breaks: Vec::new(),
};
let breaks = crate::blend::track_fit::curve_breaks(piece, d0, d1, true);
crate::blend::track_fit::polish_track(surface, &at, &mut track)?;
crate::blend::track_fit::insert_knot_crossings(surface, &at, &mut track, &breaks)?;
for &fraction in refine_near {
for _ in 0..2 {
let Some(index) = track.fractions.windows(2).position(|pair| pair[0] <= fraction && fraction <= pair[1]) else { break };
if crate::blend::track_fit::refine_track_intervals(surface, &at, &mut track, &[index], PIECE_TRACK_CEILING)?.is_none() {
break;
}
}
}
let (fit, inserted) = crate::blend::track_fit::fit_track_locally(
surface,
&mut track,
&at,
tolerance,
REFINE_ROUNDS,
PIECE_TRACK_CEILING,
)?;
if std::env::var("BREP_DEBUG_TRACK_FIT").is_ok() {
eprintln!("TRACK_FIT closed-chain piece: {inserted} sample(s) inserted locally");
eprintln!(
"TRACK_FIT closed-chain piece: miss {:.3e} at {} samples of {} ({} breaks), on floor {}",
fit.miss,
fit.samples,
track.fractions.len(),
track.breaks.len(),
fit.on_floor
);
if let Some(worst) = fit.worst {
let parameter = d0 + (d1 - d0) * worst.fraction;
let spans: Vec<[f64; 2]> = piece.knots.windows(2)
.filter(|pair| pair[1] > pair[0] && pair[1] > d0 && pair[0] < d1)
.map(|pair| [pair[0], pair[1]]).collect();
let mean = (d1 - d0) / spans.len().max(1) as f64;
let span = spans.iter().position(|pair| parameter >= pair[0] && parameter <= pair[1]);
let width = span.map(|index| spans[index][1] - spans[index][0]);
eprintln!(
"TRACK_FIT closed-chain worst: fraction {:.9} parameter {:.9} (pinned start {start_is_crossing}, pinned end {end_is_crossing}) \
knot span {:?} of {} width {:?} vs mean {:.3e}; dense bracket [{:.9}, {:.9}] (interval {})",
worst.fraction, parameter, span, spans.len(), width, mean,
worst.bracket[0], worst.bracket[1], worst.interval
);
}
}
if !fit.on_floor {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "chain_pcurve_floor", format!(
"{} a closed chain's support piece misses its pcurve by {:.3e} at {} samples, \
against a floor of {tolerance:.1e}",
crate::blend::PCURVE_OFF_FLOOR,
fit.miss,
fit.samples
)));
}
Ok(fit.curve)
}