use super::*;
use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
pub(in crate::blend) fn march_open_stations(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
radius_at: &dyn Fn(f64) -> f64,
overshoot_fraction: f64,
) -> Result<(Vec<Station>, [usize; 2]), KernelRefusal> {
march_open_stations_ending(
edge,
first,
second,
radius_at,
overshoot_fraction,
[edge.t0, edge.t1],
STATIONS,
[false, false],
[false, false],
None,
)
.map(|(stations, vertex_indices, _)| (stations, vertex_indices))
}
#[allow(clippy::too_many_arguments)]
pub(in crate::blend) fn march_open_stations_ending(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
radius_at: &dyn Fn(f64) -> f64,
overshoot_fraction: f64,
ends: [f64; 2],
station_count: usize,
poles: [bool; 2],
bounded_ends: [bool; 2],
refine_bar: Option<f64>,
) -> Result<(Vec<Station>, [usize; 2], StationRefinement), KernelRefusal> {
march_open_stations_core(
edge, first, second, radius_at, overshoot_fraction, ends, station_count, poles, bounded_ends, refine_bar, 0, true,
)
.map(|(stations, vertex_indices, refinement, _)| (stations, vertex_indices, refinement))
}
#[derive(Clone, Debug, Default)]
pub(in crate::blend) struct OpenDenseCentres {
pub(in crate::blend) subdivisions: usize,
pub(in crate::blend) centres: Vec<Vec<Option<Vec3>>>,
pub(in crate::blend) station_ts: Vec<f64>,
}
fn predict_station_uv(history: &[(f64, [f64; 4])], t: f64) -> Option<[f64; 4]> {
let n = history.len();
if n < 2 {
return None;
}
let tail = &history[n.saturating_sub(3)..];
let mut uv = [0.0f64; 4];
for (i, &(ti, ui)) in tail.iter().enumerate() {
let mut weight = 1.0;
for (j, &(tj, _)) in tail.iter().enumerate() {
if i != j {
let denominator = ti - tj;
if denominator == 0.0 {
return None;
}
weight *= (t - tj) / denominator;
}
}
for k in 0..4 {
uv[k] += weight * ui[k];
}
}
uv.iter().all(|value| value.is_finite()).then_some(uv)
}
fn dense_chain_enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var("BREP_BLEND_DENSE_CHAIN").as_deref() != Ok("0"))
}
#[allow(clippy::too_many_arguments)]
const EXACT_STRIPES_DEFAULT: bool = false;
thread_local! {
static EXACT_STRIPES_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(in crate::blend) fn exact_stripes_on() -> bool {
if let Some(forced) = EXACT_STRIPES_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_EXACT_STRIPES").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => EXACT_STRIPES_DEFAULT,
}
}
#[derive(Clone, Copy)]
struct TranslatedStations {
direction: Vec3,
inverses: [[Vec3; 2]; 2],
}
#[allow(clippy::too_many_arguments)]
fn translated_stations(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
radius_at: &dyn Fn(f64) -> f64,
station_ts: &[f64],
poles: [bool; 2],
refine_bar: Option<f64>,
dense: usize,
probe_fold: bool,
) -> Option<TranslatedStations> {
if !exact_stripes_on() || crate::blend::open_pad_network_on() || poles != [false, false] || refine_bar.is_some() || dense > 1 || probe_fold {
return None;
}
let radius = radius_at(*station_ts.first()?).to_bits();
if station_ts.iter().any(|t| radius_at(*t).to_bits() != radius) {
return None;
}
let direction = extrusion_direction(edge, first, second)?;
let inverse = |surface: &NurbsSurface| -> Option<[Vec3; 2]> {
let (_, s_u, s_v) = super::open::affine_plane_frame(surface)?;
let (a, b, c) = (s_u.dot(s_u), s_u.dot(s_v), s_v.dot(s_v));
let det = a * c - b * b;
if !(det.abs() > 1e-300) || !det.is_finite() {
return None;
}
Some([s_u.scale(c / det).sub(s_v.scale(b / det)), s_v.scale(a / det).sub(s_u.scale(b / det))])
};
Some(TranslatedStations { direction, inverses: [inverse(&first.face.surface)?, inverse(&second.face.surface)?] })
}
pub(in crate::blend) fn march_open_stations_core(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
radius_at: &dyn Fn(f64) -> f64,
overshoot_fraction: f64,
ends: [f64; 2],
station_count: usize,
poles: [bool; 2],
bounded_ends: [bool; 2],
refine_bar: Option<f64>,
dense: usize,
probe_fold: bool,
) -> Result<(Vec<Station>, [usize; 2], StationRefinement, OpenDenseCentres), KernelRefusal> {
let span = edge.t1 - edge.t0;
let overshoot = span * overshoot_fraction;
let [overshoot_before, overshoot_after] =
poles.map(|pole| if pole { 0.0 } else { overshoot });
let marched = if poles == [false, false] {
span + 2.0 * overshoot
} else {
span + overshoot_before + overshoot_after
};
for (slot, pole) in poles.into_iter().enumerate() {
let vertex_t = if slot == 0 { edge.t0 } else { edge.t1 };
if pole && ends[slot] != vertex_t {
return Err(KernelRefusal::internal(
KernelStage::Collect,
"pole_end_vertex",
format!(
"blend march: edge {}'s pole end is not at its vertex",
edge.id
),
));
}
}
let radius_extent = [0.0, 0.5, 1.0]
.into_iter()
.map(|fraction| radius_at(edge.t0 + span * fraction).abs())
.fold(0.0, f64::max);
let scale = march_model_scale(&edge.curve, edge.t0, edge.t1, radius_extent)?;
crate::report_scale_migration("blend_march_open", scale, || {
let a = edge.curve.evaluate(edge.t0).unwrap_or_default();
let b = edge.curve.evaluate(edge.t0 + span * 0.5).unwrap_or_default();
a.length().max(b.length()).max(1.0)
});
let surface1 = &first.face.surface;
let surface2 = &second.face.surface;
let signs = [first.rho.signum(), second.rho.signum()];
let rho_at = |t: f64| -> [f64; 2] {
let radius = radius_at(t);
[signs[0] * radius, signs[1] * radius]
};
let mid_index = station_count / 2;
let (station_ts, vertex_indices) =
open_station_parameters(edge, overshoot_fraction, ends, station_count, poles);
let station_t = |index: usize| station_ts[index];
let station_step = marched.abs() / station_count as f64;
let section_frame = |t: f64| -> Result<(Vec3, Vec3), KernelRefusal> {
march_section(edge, t, station_step, "blend march")
};
let solve_centered = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3), KernelRefusal> {
let (section_point, section_tangent) = section_frame(t)?;
solve_station_centered(
surface1,
surface2,
rho_at(t),
seed,
section_point,
section_tangent,
scale,
)
};
let solve_at =
|t: f64, seed: [f64; 4]| -> Result<[f64; 4], KernelRefusal> { solve_centered(t, seed).map(|(uv, _)| uv) };
let mid_seed = {
let t = station_t(mid_index).clamp(edge.t0, edge.t1);
let uv1 = edge_uv_on_face(first.coedge, edge, t)?;
let uv2 = edge_uv_on_face(second.coedge, edge, t)?;
[uv1[0], uv1[1], uv2[0], uv2[1]]
};
let pole_index = |index: usize| {
(poles[0] && index == 0) || (poles[1] && index == station_count)
};
let pole_solution = |t: f64| -> Result<[f64; 4], KernelRefusal> {
let uv1 = edge_uv_on_face(first.coedge, edge, t)?;
let uv2 = edge_uv_on_face(second.coedge, edge, t)?;
Ok([uv1[0], uv1[1], uv2[0], uv2[1]])
};
let pole_station = |index: usize| -> Result<([f64; 4], Option<Vec3>), KernelRefusal> {
let t = station_t(index);
let uv = pole_solution(t)?;
let center = section_frame(t).and_then(|(section_point, section_tangent)| {
tangency_residual(surface1, surface2, rho_at(t), uv, section_point, section_tangent)
.map(|(_, _, _, center)| center)
});
Ok((uv, center.ok()))
};
let solve_timing = crate::blend::BlendTimer::start("timing.march.solve_ms");
let mut solutions = vec![[0.0f64; 4]; station_count + 1];
let mut centers: Vec<Option<Vec3>> = vec![None; station_count + 1];
if pole_index(mid_index) {
let (uv, center) = pole_station(mid_index)?;
solutions[mid_index] = uv;
centers[mid_index] = center;
} else {
let context = crate::blend::stations::NewtonContext {
phase: "open-initial",
stage: "initial",
index: Some(mid_index),
t: Some(station_t(mid_index)),
native: Some([edge.t0, edge.t1]),
..Default::default()
};
let (uv, center) = crate::blend::stations::with_newton_context(context, || solve_centered(station_t(mid_index), mid_seed))?;
solutions[mid_index] = uv;
centers[mid_index] = Some(center);
}
let hop = |left: &Continued, right: &Continued| {
branch_hop([surface1, surface2], edge, &rho_at, left, right)
};
let continue_from = |solutions: &mut [[f64; 4]],
centers: &mut [Option<Vec3>],
index: usize,
from: usize|
-> Result<(), KernelRefusal> {
if pole_index(index) {
let (uv, center) = pole_station(index)?;
solutions[index] = uv;
centers[index] = center;
return Ok(());
}
let left = Continued { t: station_t(from), uv: solutions[from], center: centers[from] };
let context = crate::blend::stations::NewtonContext {
phase: "open-march",
index: Some(index),
native: Some([edge.t0, edge.t1]),
..Default::default()
};
let node = crate::blend::stations::with_newton_context(context, || continue_station(&solve_centered, &hop, &left, station_t(index), 0))?;
solutions[index] = node.uv;
centers[index] = node.center;
Ok(())
};
let (mut first_kept, mut last_kept) = (0, station_count);
let translated = translated_stations(edge, first, second, radius_at, &station_ts, poles, refine_bar, dense, probe_fold);
if let Some(frame) = translated {
let (middle_point, _) = section_frame(station_t(mid_index))?;
for index in (0..=station_count).filter(|index| *index != mid_index) {
let (point, _) = section_frame(station_t(index))?;
let shift = frame.direction.scale(point.sub(middle_point).dot(frame.direction));
let mut uv = solutions[mid_index];
for (slot, jacobian) in frame.inverses.iter().enumerate() {
uv[2 * slot] += jacobian[0].dot(shift);
uv[2 * slot + 1] += jacobian[1].dot(shift);
}
solutions[index] = uv;
centers[index] = centers[mid_index].map(|center| center.add(shift));
}
crate::blend::record_count("count.march.translated_stations", station_count as u64);
}
for index in (0..mid_index).rev().filter(|_| translated.is_none()) {
if let Err(error) = continue_from(&mut solutions, &mut centers, index, index + 1) {
if !bounded_ends[0] {
return Err(error);
}
first_kept = index + 1;
break;
}
}
for index in (mid_index + 1..=station_count).filter(|_| translated.is_none()) {
if let Err(error) = continue_from(&mut solutions, &mut centers, index, index - 1) {
if !bounded_ends[1] {
return Err(error);
}
last_kept = index - 1;
break;
}
}
solve_timing.stop();
if last_kept - first_kept < FIT_DEGREE {
return Err(KernelRefusal::non_convergence(KernelStage::Refine,
"bounded_march", "blend: too few regular stations before a carrier pole"));
}
let station_ts = station_ts[first_kept..=last_kept].to_vec();
let vertex_indices = vertex_indices.map(|i| i.clamp(first_kept, last_kept) - first_kept);
let solutions = solutions[first_kept..=last_kept].to_vec();
let centers = centers[first_kept..=last_kept].to_vec();
let mut refinement = StationRefinement::default();
let refine_timing = refine_bar.map(|_| crate::blend::BlendTimer::start("timing.march.refine_ms"));
let (station_ts, vertex_indices, solutions, final_centers) = match refine_bar {
None => (station_ts, vertex_indices, solutions, centers),
Some(bar) => {
let contacts = |t: f64, uv: [f64; 4]| -> Result<(Vec3, Vec3), KernelRefusal> {
let (section_point, section_tangent) = section_frame(t)?;
let (_, p1, p2, _) = tangency_residual(
surface1,
surface2,
rho_at(t),
uv,
section_point,
section_tangent,
)?;
Ok((p1, p2))
};
let mut ts = station_ts.clone();
let mut sols = solutions;
let mut cens = centers;
let mut points = ts
.iter()
.zip(&sols)
.map(|(t, uv)| contacts(*t, *uv))
.collect::<Result<Vec<_>, _>>()?;
let end_ts = [ts[vertex_indices[0]], ts[vertex_indices[1]]];
let (low, high) = (end_ts[0].min(end_ts[1]), end_ts[0].max(end_ts[1]));
let pole_t = |t: f64| (poles[0] && t == ts_first(&station_ts)) || (poles[1] && t == ts_last(&station_ts));
let stationary_end = end_ts.map(|t| {
edge.curve
.derivatives(t.clamp(edge.t0.min(edge.t1), edge.t0.max(edge.t1)), 1)
.map_or(false, |derivatives| derivatives[1].normalized().is_err())
});
let at_stationary_end =
|t: f64| (stationary_end[0] && t == end_ts[0]) || (stationary_end[1] && t == end_ts[1]);
let ceiling = REFINE_STATION_FACTOR * (station_count + 1);
let mut dirty = vec![true; ts.len().saturating_sub(1)];
let mut previous_worst = f64::INFINITY;
refinement.exit = StationRefineExit::RoundBudget;
for _round in 0..REFINE_ROUNDS {
let mut kept: Vec<(usize, Continued, (Vec3, Vec3))> = Vec::new();
let mut worst = (0.0f64, 0.0f64);
for interval in 0..ts.len() - 1 {
let (a, b) = (ts[interval], ts[interval + 1]);
if !dirty[interval] || b <= low || a >= high || pole_t(a) || pole_t(b) || at_stationary_end(a) || at_stationary_end(b) || ts.len() < 4 {
continue;
}
let middle = 0.5 * (a + b);
let left = Continued { t: a, uv: sols[interval], center: cens[interval] };
let context = crate::blend::stations::NewtonContext {
phase: "open-refine",
index: Some(interval),
native: Some([edge.t0, edge.t1]),
..Default::default()
};
let Ok(node) = crate::blend::stations::with_newton_context(context, || continue_station(&solve_centered, &hop, &left, middle, 0)) else {
continue;
};
let Ok(exact) = contacts(middle, node.uv) else {
continue;
};
let first = interval.saturating_sub(1).min(ts.len() - 4);
let predicted = |pick: fn(&(Vec3, Vec3)) -> Vec3| {
lagrange_cubic(&ts[first..first + 4], &points[first..first + 4], pick, middle)
};
let miss = predicted(|p| p.0)
.sub(exact.0)
.length()
.max(predicted(|p| p.1).sub(exact.1).length());
if miss > worst.0 {
worst = (miss, middle);
}
if miss > bar {
kept.push((interval, node, exact));
}
}
refinement.worst_miss = worst.0;
refinement.worst_t = worst.1;
if kept.is_empty() {
refinement.exit = StationRefineExit::Converged;
refinement.worst_miss = 0.0;
break;
}
if ts.len() + kept.len() > ceiling {
refinement.exit = StationRefineExit::StationCeiling;
break;
}
if !(worst.0 < 0.9 * previous_worst) {
refinement.exit = StationRefineExit::NoProgress;
break;
}
previous_worst = worst.0;
let mut next_dirty = vec![false; dirty.len() + kept.len()];
for (interval, node, exact) in kept.iter().rev() {
let at = interval + 1;
ts.insert(at, node.t);
sols.insert(at, node.uv);
cens.insert(at, node.center);
points.insert(at, *exact);
refinement.inserted += 1;
}
let mut shift = 0usize;
for (interval, _, _) in &kept {
let at = interval + shift;
next_dirty[at] = true;
next_dirty[at + 1] = true;
shift += 1;
}
dirty = next_dirty;
}
let vertex_indices = end_ts.map(|t| ts.iter().position(|x| *x == t).unwrap_or(0));
(ts, vertex_indices, sols, cens)
}
};
if let Some(refine_timing) = refine_timing {
refine_timing.stop();
}
let (station_ts, vertex_indices, solutions, final_centers, boundary_breaks, boundary_inserted, boundary_inserted_count) = {
let mut station_ts = station_ts;
let mut vertex_indices = vertex_indices;
let mut solutions = solutions;
let mut final_centers = final_centers;
let mut breaks: [Option<usize>; 4] = [None; 4];
let mut found = 0usize;
let mut inserted_indices = [0usize; 20];
let mut inserted_count = 0usize;
let limits = |surface: &NurbsSurface| -> Result<[Option<[f64; 2]>; 2], KernelRefusal> {
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")?;
Ok([(!closed_u).then_some([u0, u1]), (!closed_v).then_some([v0, v1])])
};
let bounds = [limits(surface1)?, limits(surface2)?];
let scoped_on = crate::blend::open_pad_network_on();
let mut index = 0usize;
while scoped_on && index + 1 < station_ts.len() && found < breaks.len() {
let mut crossing: Option<f64> = None;
'axes: for mate in 0..2 {
for axis in 0..2 {
let Some([low, high]) = bounds[mate][axis] else { continue };
let slot = 2 * mate + axis;
let (a, b) = (solutions[index][slot], solutions[index + 1][slot]);
for limit in [low, high] {
if (a - limit) * (b - limit) < 0.0 {
let (mut lo, mut hi) = (station_ts[index], station_ts[index + 1]);
let mut lo_value = a - limit;
let mut seed = solutions[index];
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
let Ok(uv) = solve_at(mid, seed) else { break };
let value = uv[slot] - limit;
if value.abs() <= 1e-13 * (1.0 + limit.abs()) {
lo = mid;
hi = mid;
break;
}
if (value < 0.0) == (lo_value < 0.0) {
lo = mid;
lo_value = value;
} else {
hi = mid;
}
seed = uv;
}
crossing = Some(0.5 * (lo + hi));
break 'axes;
}
}
}
}
if let Some(t) = crossing {
let (left, right) = (station_ts[index], station_ts[index + 1]);
let hair = 1e-9 * (right - left).abs().max(1e-300);
if std::env::var_os("BREP_DEBUG_NETWORK").is_some() {
eprintln!(
"boundary station: edge {} crossing at t {t:.9} between stations {index}/{} ({left:.9}, {right:.9}), gap fraction {:.3e}",
edge.id, index + 1, ((t - left) / (right - left)).min((right - t) / (right - left))
);
}
if (t - left).abs() <= hair {
breaks[found] = Some(index);
found += 1;
} else if (t - right).abs() <= hair {
breaks[found] = Some(index + 1);
found += 1;
} else if let Ok((uv, center)) = solve_centered(t, solutions[index]) {
let (left_t, right_t) = (station_ts[index], station_ts[index + 1]);
let mut inserted: Vec<(f64, [f64; 4], Vec3)> = vec![(t, uv, center)];
let mut seed = solutions[index];
for fraction in [0.5, 0.75] {
let at = left_t + (t - left_t) * fraction;
if let Ok((uv, center)) = solve_centered(at, seed) {
seed = uv;
inserted.push((at, uv, center));
}
}
seed = uv;
for fraction in [0.25, 0.5] {
let at = t + (right_t - t) * fraction;
if let Ok((uv, center)) = solve_centered(at, seed) {
seed = uv;
inserted.push((at, uv, center));
}
}
inserted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let crossing_offset = inserted.iter().position(|(at, _, _)| *at == t).unwrap_or(0);
for (offset, (at, uv, center)) in inserted.iter().enumerate() {
station_ts.insert(index + 1 + offset, *at);
solutions.insert(index + 1 + offset, *uv);
final_centers.insert(index + 1 + offset, Some(*center));
if inserted_count < inserted_indices.len() {
inserted_indices[inserted_count] = index + 1 + offset;
inserted_count += 1;
}
}
let added = inserted.len();
for vertex_index in vertex_indices.iter_mut() {
if *vertex_index >= index + 1 {
*vertex_index += added;
}
}
breaks[found] = Some(index + 1 + crossing_offset);
found += 1;
index += added;
}
}
index += 1;
}
if found > 0 {
crate::blend::record_count("count.march.boundary_stations", found as u64);
}
(station_ts, vertex_indices, solutions, final_centers, breaks, inserted_indices, inserted_count)
};
refinement.boundary_breaks = boundary_breaks;
refinement.boundary_inserted = boundary_inserted;
refinement.boundary_inserted_count = boundary_inserted_count;
let station_count = station_ts.len() - 1;
let station_t = |index: usize| station_ts[index];
let pole_index = |index: usize| {
(poles[0] && index == 0) || (poles[1] && index == station_count)
};
open_station_trace(edge, &station_ts, &solutions, |t, uv| {
let (section_point, section_tangent) = section_frame(t)?;
tangency_residual(surface1, surface2, rho_at(t), uv, section_point, section_tangent)
.map(|(_, p1, p2, center)| (p1, p2, center, section_point))
});
if probe_fold {
let _fold_timing = crate::blend::BlendTimer::start("timing.march.fold_ms");
let probe = |t: f64,
seed: [f64; 4]|
-> Result<([f64; 4], Vec3, Vec3, Vec3), KernelRefusal> {
let uv = solve_at(t, seed)?;
let (section_point, section_tangent) = section_frame(t)?;
let (_, p1, p2, center) = tangency_residual(
surface1,
surface2,
rho_at(t),
uv,
section_point,
section_tangent,
)?;
Ok((uv, p1, p2, center))
};
let [low, high] = vertex_indices;
let (low, high) = (low.min(high), low.max(high));
let probed: Vec<(f64, [f64; 4])> = (low..=high)
.filter(|index| !pole_index(*index))
.map(|index| (station_t(index), solutions[index]))
.collect();
let fold_radius = |t: f64| radius_at(t).abs();
crate::blend::fold::check_wall_fold(
&fold_radius,
span,
&probed,
&probe,
edge,
[first.face, second.face],
scale,
)?;
}
let dense_timing = (dense > 1).then(|| crate::blend::BlendTimer::start("timing.march.dense_ms"));
let mut dense_centres = OpenDenseCentres { subdivisions: dense, centres: Vec::new(), station_ts: station_ts.clone() };
if dense > 1 {
for interval in 0..station_ts.len().saturating_sub(1) {
if pole_index(interval) || pole_index(interval + 1) {
dense_centres.centres.push(vec![None; dense - 1]);
continue;
}
let (a, b) = (station_ts[interval], station_ts[interval + 1]);
let left = Continued { t: a, uv: solutions[interval], center: final_centers[interval] };
let chain = dense_chain_enabled();
let mut previous = left;
let mut row = Vec::with_capacity(dense - 1);
let jacobian = std::cell::RefCell::new(None::<[[f64; 4]; 4]>);
let history = std::cell::RefCell::new(vec![(left.t, left.uv)]);
let solve_chord = |t: f64, seed: [f64; 4]| -> Result<([f64; 4], Vec3), KernelRefusal> {
let (section_point, section_tangent) = section_frame(t)?;
let predicted = predict_station_uv(&history.borrow(), t).unwrap_or(seed);
let mut carried = jacobian.borrow_mut();
crate::blend::stations::solve_station_state_chord(
surface1, surface2, rho_at(t), predicted, seed, section_point, section_tangent, scale, &mut carried,
)
.map(|(uv, state)| (uv, state.center))
};
for step in 1..dense {
let t = a + (b - a) * step as f64 / dense as f64;
let context = crate::blend::stations::NewtonContext {
phase: "open-dense",
index: Some(interval),
native: Some([edge.t0, edge.t1]),
..Default::default()
};
let node = if chain {
crate::blend::stations::with_newton_context(context, || continue_station(&solve_chord, &hop, &previous, t, 0)).ok()
} else {
crate::blend::stations::with_newton_context(context, || continue_station(&solve_centered, &hop, &left, t, 0)).ok()
};
if chain {
if let Some(node) = node {
previous = node;
history.borrow_mut().push((node.t, node.uv));
}
}
row.push(node.and_then(|node| node.center));
}
dense_centres.centres.push(row);
}
}
if let Some(dense_timing) = dense_timing {
dense_timing.stop();
}
let _assemble_timing = crate::blend::BlendTimer::start("timing.march.assemble_ms");
let mut stations = Vec::with_capacity(station_count + 1);
for (index, uv) in solutions.iter().enumerate() {
let t = station_t(index);
let (section_point, section_tangent) = section_frame(t)?;
let (_, p1, p2, center) = tangency_residual(
surface1,
surface2,
rho_at(t),
*uv,
section_point,
section_tangent,
)?;
let n1 = raw_normal(surface1, uv[0], uv[1])?;
let n2 = raw_normal(surface2, uv[2], uv[3])?;
let cos_alpha = signs[0] * signs[1] * n1.dot(n2);
let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
if weight <= 1e-6 {
return Err(KernelRefusal::unsupported(
KernelStage::Refine,
"tangent_station",
"blend: faces are tangent at a station (α = π)",
));
}
let apex = if pole_index(index) {
p1
} else {
apex_point(p1, n1, p2, n2, center)?
};
stations.push(Station {
uv1: [uv[0], uv[1]],
uv2: [uv[2], uv[3]],
p1,
p2,
center,
weight,
apex,
});
}
Ok((stations, vertex_indices, refinement, dense_centres))
}
fn ts_first(ts: &[f64]) -> f64 {
ts[0]
}
fn ts_last(ts: &[f64]) -> f64 {
ts[ts.len() - 1]
}
fn lagrange_cubic(
ts: &[f64],
samples: &[(Vec3, Vec3)],
pick: fn(&(Vec3, Vec3)) -> Vec3,
t: f64,
) -> Vec3 {
let mut sum = Vec3::default();
for i in 0..4 {
let mut weight = 1.0;
for j in 0..4 {
if i != j {
weight *= (t - ts[j]) / (ts[i] - ts[j]);
}
}
sum = sum.add(pick(&samples[i]).scale(weight));
}
sum
}
pub(in crate::blend) fn open_station_parameters(
edge: &EdgeRecord,
overshoot_fraction: f64,
ends: [f64; 2],
station_count: usize,
poles: [bool; 2],
) -> (Vec<f64>, [usize; 2]) {
let span = edge.t1 - edge.t0;
let overshoot = span * overshoot_fraction;
let [overshoot_before, overshoot_after] =
poles.map(|pole| if pole { 0.0 } else { overshoot });
let marched = if poles == [false, false] {
span + 2.0 * overshoot
} else {
span + overshoot_before + overshoot_after
};
let extend = crate::blend::open_pad_network_on();
let start = if extend && ends[0] < edge.t0 - overshoot_before { ends[0] - overshoot_before } else { edge.t0 - overshoot_before };
let finish = if extend && ends[1] > edge.t1 + overshoot_after { ends[1] + overshoot_after } else { edge.t1 + overshoot_after };
let mut ts: Vec<f64> = if start == edge.t0 - overshoot_before && finish == edge.t1 + overshoot_after {
(0..=station_count)
.map(|index| edge.t0 - overshoot_before + marched * index as f64 / station_count as f64)
.collect()
} else {
(0..=station_count)
.map(|index| start + (finish - start) * index as f64 / station_count as f64)
.collect()
};
let mut snapped = [0usize; 2];
for (slot, target) in ends.into_iter().enumerate() {
let mut nearest = 0usize;
for (index, t) in ts.iter().enumerate() {
if (t - target).abs() < (ts[nearest] - target).abs() {
nearest = index;
}
}
ts[nearest] = target;
snapped[slot] = nearest;
}
(ts, snapped)
}
fn open_station_trace(
edge: &EdgeRecord,
station_ts: &[f64],
solutions: &[[f64; 4]],
evaluate: impl Fn(f64, [f64; 4]) -> Result<(Vec3, Vec3, Vec3, Vec3), KernelRefusal>,
) {
if std::env::var("BREP_BLEND_STATION_TRACE").ok().as_deref() != Some("1") {
return;
}
eprintln!(
"blend open stations: edge {} {} station(s) over t [{:.9}, {:.9}]",
edge.id,
solutions.len(),
station_ts.first().copied().unwrap_or(f64::NAN),
station_ts.last().copied().unwrap_or(f64::NAN),
);
let mut previous: Option<Vec3> = None;
for (index, uv) in solutions.iter().enumerate() {
let t = station_ts[index];
let Ok((p1, p2, center, section)) = evaluate(t, *uv) else {
eprintln!(" pos {index:>3} t {t:.9} (station could not be evaluated)");
continue;
};
let step = previous
.map(|point| center.sub(point).length())
.unwrap_or(f64::NAN);
eprintln!(
" pos {:>3} t {:.9} p1 ({:.9}, {:.9}, {:.9}) p2 ({:.9}, {:.9}, {:.9}) \
c ({:.9}, {:.9}, {:.9}) s ({:.9}, {:.9}, {:.9}) uv1 ({:.9}, {:.9}) uv2 ({:.9}, {:.9}) \
dc {:.9}",
index, t, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, center.x, center.y, center.z,
section.x, section.y, section.z, uv[0], uv[1], uv[2], uv[3], step,
);
previous = Some(center);
}
}
pub(in crate::blend) fn pole_end(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
at_start: bool,
bar: f64,
) -> Result<bool, KernelRefusal> {
let t = if at_start { edge.t0 } else { edge.t1 };
let uv1 = edge_uv_on_face(first.coedge, edge, t)?;
let uv2 = edge_uv_on_face(second.coedge, edge, t)?;
let one = blend_offset(&first.face.surface).at(uv1[0], uv1[1], first.rho).or_refuse(KernelStage::Refine, "at")?;
let two = blend_offset(&second.face.surface).at(uv2[0], uv2[1], second.rho).or_refuse(KernelStage::Refine, "at")?;
Ok(one.point.sub(two.point).length() <= bar)
}
pub(in crate::blend) fn vertex_stations(
parameters: &[f64],
vertex_indices: [usize; 2],
) -> Option<[f64; 2]> {
if parameters.len() < 2 || vertex_indices[0] == vertex_indices[1] {
return None;
}
Some([
*parameters.get(vertex_indices[0])?,
*parameters.get(vertex_indices[1])?,
])
}
pub(in crate::blend) fn rails_off_carriers(
rows: &FittedRows,
parameters: &[f64],
carriers: [&NurbsSurface; 2],
) -> Result<f64, KernelRefusal> {
let [low, high] = match rows.vertex_stations {
Some([a, b]) if a < b => [a, b],
Some([a, b]) => [b, a],
None => [
*parameters.first().unwrap_or(&0.0),
*parameters.last().unwrap_or(&1.0),
],
};
let mut worst = 0.0f64;
for (rail, surface) in [(&rows.cr, carriers[0]), (&rows.cs, carriers[1])] {
for pair in parameters.windows(2) {
if pair[1] <= low || pair[0] >= high {
continue;
}
for fraction in [0.25, 0.5, 0.75] {
let u = pair[0] + (pair[1] - pair[0]) * fraction;
if u < low || u > high {
continue;
}
let point = rail.evaluate(u).or_refuse(KernelStage::Refine, "evaluate")?;
let off = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?.distance;
if !worst.is_nan() && (off.is_nan() || off > worst) {
worst = off;
}
}
}
}
Ok(worst)
}
pub(in crate::blend) fn fit_open_rows(
stations: &[Station],
parameters: &[f64],
chamfer: bool,
vertex_indices: Option<[usize; 2]>,
extrusion: Option<Vec3>,
) -> Result<FittedRows, KernelRefusal> {
fit_open_rows_breaking_at(stations, parameters, chamfer, vertex_indices, extrusion, &[])
}
pub(in crate::blend) fn fit_open_rows_breaking_at(
stations: &[Station],
parameters: &[f64],
chamfer: bool,
vertex_indices: Option<[usize; 2]>,
extrusion: Option<Vec3>,
extra: &[usize],
) -> Result<FittedRows, KernelRefusal> {
let mut samples_cr = Vec::new();
let mut samples_mid = Vec::new();
let mut samples_cs = Vec::new();
let mut samples_uv1 = Vec::new();
let mut samples_uv2 = Vec::new();
let mut samples_center = Vec::new();
for station in stations {
samples_cr.push(Vec4::from_point(station.p1, 1.0));
samples_cs.push(Vec4::from_point(station.p2, 1.0));
samples_center.push(Vec4::from_point(station.center, 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,
});
samples_uv1.push(Vec4::from_point(
Vec3::new(station.uv1[0], station.uv1[1], 0.0),
1.0,
));
samples_uv2.push(Vec4::from_point(
Vec3::new(station.uv2[0], station.uv2[1], 0.0),
1.0,
));
}
let cr_pcurve = fit::interpolate_homogeneous(&samples_uv1, FIT_DEGREE, parameters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let cs_pcurve = fit::interpolate_homogeneous(&samples_uv2, FIT_DEGREE, parameters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
if let Some(rows) =
exact_extrusion_rows(stations, parameters, chamfer, extrusion, cr_pcurve.clone(), cs_pcurve.clone())?
{
return Ok(rows);
}
let breaks = row_breaks(stations.len(), vertex_indices, extra);
let interpolate = |samples: &[Vec4]| interpolate_in_pieces(samples, parameters, &breaks);
let (cr_pcurve, cs_pcurve) = if breaks.is_empty() {
(cr_pcurve, cs_pcurve)
} else {
(interpolate(&samples_uv1)?, interpolate(&samples_uv2)?)
};
let cr = interpolate(&samples_cr)?;
let cs = interpolate(&samples_cs)?;
let mid = if chamfer {
None
} else {
Some(interpolate(&samples_mid)?)
};
let u_domain = cr.domain().or_refuse(KernelStage::Refine, "domain")?;
let surface = crate::blend::rows::surface_from_rows(FIT_DEGREE, &cr, &cs, mid.as_ref(), false)?;
let center = interpolate(&samples_center)?;
Ok(FittedRows {
surface,
cr,
cs,
cr_pcurve,
cs_pcurve,
u_domain,
center: Some(center),
exact_extrusion: false,
vertex_stations: None,
revolution_axis: None,
})
}
pub(in crate::blend) fn fit_rows_breaking(
stations: &[Station],
parameters: &[f64],
breaks: &[usize],
) -> Result<FittedRows, KernelRefusal> {
let mut bounds = vec![0usize];
bounds.extend(breaks.iter().copied());
bounds.push(stations.len().saturating_sub(1));
if bounds.windows(2).any(|pair| pair[1] < pair[0] + FIT_DEGREE) {
return Err(KernelRefusal::internal(
KernelStage::Refine,
"row_piece_short",
format!("blend: a row piece between breaks {breaks:?} is too short for the fit degree ({} stations)", stations.len()),
));
}
let mut samples_cr = Vec::new();
let mut samples_mid = Vec::new();
let mut samples_cs = Vec::new();
let mut samples_center = Vec::new();
for station in stations {
samples_cr.push(Vec4::from_point(station.p1, 1.0));
samples_cs.push(Vec4::from_point(station.p2, 1.0));
samples_center.push(Vec4::from_point(station.center, 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,
});
}
let interpolate = |samples: &[Vec4]| interpolate_in_pieces(samples, parameters, breaks);
let cr = interpolate(&samples_cr)?;
let cs = interpolate(&samples_cs)?;
let mid = interpolate(&samples_mid)?;
let center = interpolate(&samples_center)?;
let u_domain = cr.domain().or_refuse(KernelStage::Refine, "domain")?;
let surface = crate::blend::rows::surface_from_rows(FIT_DEGREE, &cr, &cs, Some(&mid), false)?;
let placeholder = fit::interpolate_homogeneous(
&[Vec4::from_point(Vec3::default(), 1.0), Vec4::from_point(Vec3::new(1.0, 0.0, 0.0), 1.0)],
1,
&[0.0, 1.0],
).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
Ok(FittedRows {
surface,
cr,
cs,
cr_pcurve: placeholder.clone(),
cs_pcurve: placeholder,
u_domain,
center: Some(center),
exact_extrusion: false,
vertex_stations: None,
revolution_axis: None,
})
}
pub(in crate::blend) fn interpolate_piece(
samples: &[Vec4],
parameters: &[f64],
breaks: &[usize],
) -> Result<NurbsCurve, KernelRefusal> {
if samples.len() < 2 || samples.len() != parameters.len() {
return Err(KernelRefusal::internal(KernelStage::Refine, "interpolate_piece", "blend: a pcurve piece needs matching samples and parameters"));
}
if !breaks.is_empty() {
return interpolate_in_pieces(samples, parameters, breaks);
}
let (low, high) = (parameters[0], parameters[parameters.len() - 1]);
if !(high > low) {
return Err(KernelRefusal::internal(KernelStage::Refine, "interpolate_piece", "blend: a pcurve piece has no parameter span"));
}
let local: Vec<f64> = parameters.iter().map(|parameter| (parameter - low) / (high - low)).collect();
let fitted = fit::interpolate_homogeneous(samples, FIT_DEGREE, &local).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let knots: Vec<f64> = fitted.knots.iter().map(|knot| low + knot * (high - low)).collect();
NurbsCurve::new(fitted.degree, knots, fitted.control_points).or_refuse(KernelStage::Refine, "new")
}
fn row_breaks(count: usize, vertex_indices: Option<[usize; 2]>, extra: &[usize]) -> Vec<usize> {
let mut breaks: Vec<usize> = vertex_indices
.into_iter()
.flatten()
.chain(extra.iter().copied())
.filter(|&index| index > 0 && index + 1 < count)
.collect();
if breaks.is_empty() {
return Vec::new();
}
breaks.sort_unstable();
breaks.dedup();
let mut bounds = Vec::with_capacity(breaks.len() + 2);
bounds.push(0);
bounds.extend(breaks.iter().copied());
bounds.push(count.saturating_sub(1));
if bounds.windows(2).all(|pair| pair[1] - pair[0] >= FIT_DEGREE) {
breaks
} else {
Vec::new()
}
}
fn interpolate_in_pieces(
samples: &[Vec4],
parameters: &[f64],
breaks: &[usize],
) -> Result<NurbsCurve, KernelRefusal> {
if breaks.is_empty() {
return fit::interpolate_homogeneous(samples, FIT_DEGREE, parameters).or_refuse(KernelStage::Refine, "interpolate_homogeneous");
}
let mut bounds = Vec::with_capacity(breaks.len() + 2);
bounds.push(0);
bounds.extend(breaks.iter().copied());
bounds.push(samples.len() - 1);
let pieces = bounds.len() - 1;
let mut knots: Vec<f64> = Vec::new();
let mut control: Vec<Vec4> = Vec::new();
for (piece, pair) in bounds.windows(2).enumerate() {
let (from, to) = (pair[0], pair[1]);
let (low, high) = (parameters[from], parameters[to]);
let local: Vec<f64> = parameters[from..=to]
.iter()
.map(|parameter| (parameter - low) / (high - low))
.collect();
let fitted = fit::interpolate_homogeneous(&samples[from..=to], FIT_DEGREE, &local).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
if fitted.degree != FIT_DEGREE {
return Err(KernelRefusal::internal(
KernelStage::Refine,
"row_piece_short",
"blend: a row piece is too short for the fit degree",
));
}
if piece == 0 {
knots.extend(std::iter::repeat(low).take(FIT_DEGREE + 1));
}
let interior = &fitted.knots[FIT_DEGREE + 1..fitted.knots.len() - FIT_DEGREE - 1];
knots.extend(interior.iter().map(|knot| low + knot * (high - low)));
let multiplicity = if piece + 1 == pieces { FIT_DEGREE + 1 } else { FIT_DEGREE };
knots.extend(std::iter::repeat(high).take(multiplicity));
control.extend(fitted.control_points.iter().skip(usize::from(piece > 0)).copied());
}
NurbsCurve::new(FIT_DEGREE, knots, control).or_refuse(KernelStage::Refine, "new")
}
pub(in crate::blend) fn extrusion_direction(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
) -> Option<Vec3> {
let tolerance = crate::KernelTolerances::for_scale(
march_model_scale(&edge.curve, edge.t0, edge.t1, 0.0).ok()?,
1e-7,
)
.model;
let (start, end) = edge.curve.straight_segment(tolerance)?;
let chord = end.sub(start);
let direction = chord.normalized().ok()?;
let lean = |vector: Vec3, axis: Vec3| vector.sub(axis.scale(vector.dot(axis))).length();
let invariant = |mate: &BlendMate| -> Option<()> {
match mate.face.surface.analytic()? {
crate::AnalyticSurface::Plane { u_dir, v_dir, .. } => {
let normal = u_dir.cross(*v_dir).normalized().ok()?;
(chord.dot(normal).abs() <= tolerance).then_some(())
}
crate::AnalyticSurface::RuledRevolution {
frame, rho0, rho1, ..
} => ((rho1 - rho0).abs() <= tolerance && lean(chord, frame.axis) <= tolerance)
.then_some(()),
crate::AnalyticSurface::Revolution {
frame, generatrix, ..
} => {
let (from, to) = generatrix.straight_segment(tolerance)?;
(lean(to.sub(from), frame.axis) <= tolerance && lean(chord, frame.axis) <= tolerance)
.then_some(())
}
_ => None,
}
};
invariant(first)?;
invariant(second)?;
Some(direction)
}
fn exact_extrusion_rows(
stations: &[Station],
parameters: &[f64],
chamfer: bool,
extrusion: Option<Vec3>,
cr_pcurve: NurbsCurve,
cs_pcurve: NurbsCurve,
) -> Result<Option<FittedRows>, KernelRefusal> {
let (Some(first), Some(last)) = (stations.first(), stations.last()) else {
return Ok(None);
};
if stations.len() < 3 {
return Ok(None);
}
let travel = last.center.sub(first.center);
let length = travel.length();
if !(length > 1e-12) {
return Ok(None);
}
let direction = travel.scale(1.0 / length);
let scale = stations
.iter()
.fold(0.0f64, |worst, station| worst.max(station.center.length()))
.max(1.0);
let bar = 1e-9 * (1.0 + scale);
let mut weights_agree = true;
for (station, parameter) in stations.iter().zip(parameters) {
let shift = station.center.sub(first.center);
if shift.sub(direction.scale(shift.dot(direction))).length() > bar
|| (shift.dot(direction) - parameter * length).abs() > bar
{
return Ok(None);
}
let translate = |point: Vec3| point.add(shift);
if station.p1.sub(translate(first.p1)).length() > bar
|| station.p2.sub(translate(first.p2)).length() > bar
|| station.apex.sub(translate(first.apex)).length() > bar
{
return Ok(None);
}
if (station.weight - first.weight).abs() > 1e-12 {
weights_agree = false;
}
}
let (end, offset) = if weights_agree {
(last, Vec3::default())
} else {
let Some(along) = extrusion else {
return Ok(None);
};
if travel.sub(along.scale(travel.dot(along))).length() > bar {
return Ok(None);
}
(first, travel)
};
let section = |z_index: usize, station: &Station, offset: Vec3| -> Vec4 {
match z_index {
0 => Vec4::from_point(station.p1.add(offset), 1.0),
1 if !chamfer => {
let apex = station.apex.add(offset);
Vec4 {
x: apex.x * station.weight,
y: apex.y * station.weight,
z: apex.z * station.weight,
w: station.weight,
}
}
_ => Vec4::from_point(station.p2.add(offset), 1.0),
}
};
let columns = if chamfer { 2 } else { 3 };
let control = vec![
(0..columns).map(|j| section(j, first, Vec3::default())).collect::<Vec<_>>(),
(0..columns).map(|j| section(j, end, offset)).collect::<Vec<_>>(),
];
let (degree_v, knots_v) = crate::blend::rows::cross_section_basis(chamfer);
let surface = NurbsSurface::new(1, degree_v, vec![0.0, 0.0, 1.0, 1.0], knots_v, control).or_refuse(KernelStage::Refine, "new")?;
let cr = crate::make_line(first.p1, end.p1.add(offset)).or_refuse(KernelStage::Refine, "make_line")?;
let cs = crate::make_line(first.p2, end.p2.add(offset)).or_refuse(KernelStage::Refine, "make_line")?;
let center = crate::make_line(first.center, end.center.add(offset)).or_refuse(KernelStage::Refine, "make_line")?;
Ok(Some(FittedRows {
surface,
cr,
cs,
cr_pcurve,
cs_pcurve,
u_domain: [0.0, 1.0],
center: Some(center),
exact_extrusion: true,
vertex_stations: None,
revolution_axis: None,
}))
}
pub(in crate::blend) fn revolution_lane_axis(edge: &EdgeRecord, first: &BlendMate, second: &BlendMate) -> Option<(Vec3, Vec3)> {
let (origin, axis) = closed_revolution_axis(first, second)?;
let axis = axis.normalized().ok()?;
let tolerance = crate::KernelTolerances::for_scale(
march_model_scale(&edge.curve, edge.t0, edge.t1, 0.0).ok()?,
1e-7,
)
.model;
let mut reference: Option<(f64, f64)> = None;
for step in 0..=8 {
let t = edge.t0 + (edge.t1 - edge.t0) * step as f64 / 8.0;
let offset = edge.curve.evaluate(t).ok()?.sub(origin);
let height = offset.dot(axis);
let radial = offset.sub(axis.scale(height)).length();
match reference {
None => reference = Some((radial, height)),
Some((known_radial, known_height)) => {
if (radial - known_radial).abs() > tolerance || (height - known_height).abs() > tolerance {
return None;
}
}
}
}
(reference?.0 > tolerance).then_some((origin, axis))
}
pub(in crate::blend) struct ExactRevolution {
pub(in crate::blend) rows: FittedRows,
pub(in crate::blend) parameters: Vec<f64>,
pub(in crate::blend) stations: Vec<Station>,
}
pub(in crate::blend) fn exact_revolution_rows(
stations: &[Station],
vertex_indices: [usize; 2],
chamfer: bool,
axis: (Vec3, Vec3),
radius: f64,
first: &BlendMate,
second: &BlendMate,
) -> Result<ExactRevolution, String> {
let (origin, axis) = axis;
let (lo, hi) = (vertex_indices[0].min(vertex_indices[1]), vertex_indices[0].max(vertex_indices[1]));
if stations.len() < 3 || hi >= stations.len() || hi < lo + 2 {
return Err("fewer than three stations between the vertices".to_string());
}
let scale = stations.iter().fold(0.0f64, |worst, station| worst.max(station.center.length())).max(1.0);
let bar = 1e-9 * (1.0 + scale);
let is_plane = |mate: &BlendMate| matches!(mate.face.surface.analytic(), Some(crate::AnalyticSurface::Plane { .. }));
let carrier_is_second = if !is_plane(second) {
true
} else if !is_plane(first) {
false
} else {
return Err("no revolution carrier among the mates".to_string());
};
let (carrier, mate) = if carrier_is_second { (second, first) } else { (first, second) };
let carrier_uv = |station: &Station| if carrier_is_second { station.uv2 } else { station.uv1 };
let mate_uv = |station: &Station| if carrier_is_second { station.uv1 } else { station.uv2 };
let contact = |station: &Station| if carrier_is_second { station.p2 } else { station.p1 };
let surface = &carrier.face.surface;
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let window = &stations[lo..=hi];
let (first_uv, last_uv) = (carrier_uv(&window[0]), carrier_uv(&window[window.len() - 1]));
let along_u = (last_uv[0] - first_uv[0]).abs() >= (last_uv[1] - first_uv[1]).abs();
let (along, held, held_span) = if along_u { (0usize, 1usize, v1 - v0) } else { (1, 0, u1 - u0) };
let held_value = window.iter().map(|station| carrier_uv(station)[held]).sum::<f64>() / window.len() as f64;
if window.iter().any(|station| (carrier_uv(station)[held] - held_value).abs() > 1e-8 * held_span.abs().max(1e-300)) {
return Err("the carrier contact drifts across the carrier".to_string());
}
let along_of = |station: &Station| carrier_uv(station)[along];
let sweep = along_of(&window[window.len() - 1]) - along_of(&window[0]);
if !(sweep.abs() > 0.0) || window.windows(2).any(|pair| (along_of(&pair[1]) - along_of(&pair[0])) * sweep <= 0.0) {
return Err("the carrier contact is not monotone along the carrier".to_string());
}
let circle = if along_u { surface.iso_curve_v(held_value) } else { surface.iso_curve_u(held_value) }?;
let [c0, c1] = circle.domain()?;
let reversed = sweep < 0.0;
let circle = if reversed { circle.reversed()? } else { circle };
let on_curve = |along: f64| if reversed { c0 + c1 - along } else { along };
let step = sweep / (hi - lo) as f64;
let along_at = |index: usize| -> f64 {
if index < lo {
along_of(&window[0]) - step * (lo - index) as f64
} else if index > hi {
along_of(&window[window.len() - 1]) + step * (index - hi) as f64
} else {
along_of(&stations[index])
}
};
let (parameter_lo, parameter_hi) = (on_curve(along_at(0)), on_curve(along_at(stations.len() - 1)));
if !(parameter_hi > parameter_lo) {
return Err("the rows' parameter range is empty".to_string());
}
let rail = extend_conic_ends(&circle, parameter_lo, parameter_hi)?;
let [r0, r1] = rail.domain()?;
let mut rail = rail;
for knot in rail.knots.iter_mut() {
*knot = ((*knot - r0) / (r1 - r0)).clamp(0.0, 1.0);
}
let degree = rail.degree;
for knot in rail.knots.iter_mut().take(degree + 1) {
*knot = 0.0;
}
let count = rail.knots.len();
for knot in rail.knots.iter_mut().skip(count - degree - 1) {
*knot = 1.0;
}
let rail = NurbsCurve::new(degree, rail.knots.clone(), rail.control_points.clone())?;
let mut parameters: Vec<f64> = (0..stations.len()).map(|index| (on_curve(along_at(index)) - parameter_lo) / (parameter_hi - parameter_lo)).collect();
parameters[0] = 0.0;
*parameters.last_mut().unwrap() = 1.0;
if parameters.windows(2).any(|pair| !(pair[1] > pair[0])) {
return Err("the rows' parameters are not increasing".to_string());
}
let seat = &window[0];
let axial = |point: Vec3| point.sub(origin).dot(axis);
let radial = |point: Vec3| {
let height = axial(point);
point.sub(origin).sub(axis.scale(height))
};
let seat_contact = contact(seat);
let contact_radius = radial(seat_contact).length();
if contact_radius <= bar {
return Err("the carrier contact sits on the axis".to_string());
}
let contact_height = axial(seat_contact);
let map_row = |target: Vec3| -> Result<NurbsCurve, String> {
let (target_radius, target_height) = (radial(target).length(), axial(target));
let factor = target_radius / contact_radius;
let control = rail
.control_points
.iter()
.map(|control| {
let weight = control.w;
let point = Vec3::new(control.x / weight, control.y / weight, control.z / weight);
let image = origin.add(axis.scale(axial(point) - contact_height + target_height)).add(radial(point).scale(factor));
Vec4 { x: image.x * weight, y: image.y * weight, z: image.z * weight, w: weight }
})
.collect();
NurbsCurve::new(rail.degree, rail.knots.clone(), control)
};
let centre_radius = radial(seat.center).length();
let section_radius = radial(seat.p1).length().min(radial(seat.p2).length()).min(radial(seat.apex).length());
if section_radius <= 1e-6 * scale {
return Err("the section reaches the axis".to_string());
}
if !chamfer && centre_radius <= radius.abs() * (1.0 + 1e-6) {
return Err("the centre circle is inside the ball (the wall would fold)".to_string());
}
let (cr, cs) = if carrier_is_second { (map_row(seat.p1)?, rail.clone()) } else { (rail.clone(), map_row(seat.p2)?) };
let apex = map_row(seat.apex)?;
let apex_weighted = NurbsCurve::new(
apex.degree,
apex.knots.clone(),
apex.control_points.iter().map(|control| control.scale(seat.weight)).collect(),
)?;
let center = map_row(seat.center)?;
let surface = crate::blend::rows::surface_from_rows(rail.degree, &cr, &cs, (!chamfer).then_some(&apex_weighted), false).map_err(|error| error.message)?;
for index in lo..=hi {
let (station, s) = (&stations[index], parameters[index]);
let off = |curve: &NurbsCurve, point: Vec3| -> Result<f64, String> { Ok(curve.evaluate(s)?.sub(point).length()) };
let worst = off(&cr, station.p1)?.max(off(&cs, station.p2)?).max(off(¢er, station.center)?).max(off(&apex, station.apex)?);
if !(worst <= bar) {
return Err(format!("station {index} stands {worst:.3e} off the revolved section (bar {bar:.1e})"));
}
}
let line = |from: [f64; 2], to: [f64; 2]| -> Result<NurbsCurve, String> {
NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Vec4::from_point(Vec3::new(from[0], from[1], 0.0), 1.0), Vec4::from_point(Vec3::new(to[0], to[1], 0.0), 1.0)],
)
};
let carrier_point = |along: f64| -> [f64; 2] { if along_u { [along, held_value] } else { [held_value, along] } };
let carrier_pcurve = line(carrier_point(along_at(0)), carrier_point(along_at(stations.len() - 1)))?;
let mate_row = if carrier_is_second { &cr } else { &cs };
let mate_pcurve = match plane_chart_pcurve(&mate.face.surface, mate_row) {
Some(pcurve) => pcurve,
None => {
let (a, b) = (mate_uv(&window[0]), mate_uv(&window[window.len() - 1]));
let (sa, sb) = (parameters[lo], parameters[hi]);
let at = |s: f64| -> [f64; 2] {
let fraction = (s - sa) / (sb - sa);
[a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])]
};
line(at(0.0), at(1.0))?
}
};
let (cr_pcurve, cs_pcurve) = if carrier_is_second { (mate_pcurve, carrier_pcurve) } else { (carrier_pcurve, mate_pcurve) };
let mut stations = stations.to_vec();
for index in (0..lo).chain(hi + 1..stations.len()) {
let s = parameters[index];
let uv = |pcurve: &NurbsCurve| -> Result<[f64; 2], String> {
let point = pcurve.evaluate(s)?;
Ok([point.x, point.y])
};
stations[index] = Station {
uv1: uv(&cr_pcurve)?,
uv2: uv(&cs_pcurve)?,
p1: cr.evaluate(s)?,
p2: cs.evaluate(s)?,
center: center.evaluate(s)?,
weight: seat.weight,
apex: apex.evaluate(s)?,
};
}
Ok(ExactRevolution {
rows: FittedRows {
surface,
cr,
cs,
cr_pcurve,
cs_pcurve,
u_domain: [0.0, 1.0],
center: Some(center),
exact_extrusion: false,
vertex_stations: None,
revolution_axis: Some((origin, axis)),
},
parameters,
stations,
})
}
pub(in crate::blend) fn extend_conic_ends(curve: &NurbsCurve, a: f64, b: f64) -> Result<NurbsCurve, String> {
let [d0, d1] = curve.domain()?;
if !(b > a) || !a.is_finite() || !b.is_finite() {
return Err(format!("extend_conic_ends: [{a}, {b}] is not a range"));
}
let degree = curve.degree;
let tolerance = crate::KNOT_IDENTITY_TOL;
let mut bezier = curve.clone();
let mut interior: Vec<f64> = curve.knots.iter().copied().filter(|knot| *knot > d0 + tolerance && *knot < d1 - tolerance).collect();
interior.dedup_by(|next, previous| (*next - *previous).abs() <= tolerance);
for knot in interior {
bezier = bezier.insert_knot(knot, degree)?;
}
let polygons = |piece: &[Vec4], s: f64| -> (Vec<Vec4>, Vec<Vec4>) {
let mut levels: Vec<Vec<Vec4>> = vec![piece.to_vec()];
while levels.last().map_or(false, |level| level.len() > 1) {
let previous = levels.last().unwrap();
let next: Vec<Vec4> = previous.windows(2).map(|pair| pair[0].scale(1.0 - s).add(pair[1].scale(s))).collect();
levels.push(next);
}
let left: Vec<Vec4> = levels.iter().map(|level| level[0]).collect();
let right: Vec<Vec4> = levels.iter().rev().map(|level| level[level.len() - 1]).collect();
(left, right)
};
let mut knots = bezier.knots.clone();
let mut control = bezier.control_points.clone();
let count = knots.len();
if a < d0 - tolerance {
let piece_end = knots[degree + 1];
let s = (a - d0) / (piece_end - d0);
let (_, right) = polygons(&control[..=degree], s);
control[..=degree].copy_from_slice(&right);
for knot in knots.iter_mut().take(degree + 1) {
*knot = a;
}
}
if b > d1 + tolerance {
let piece_start = knots[count - degree - 2];
let s = (b - piece_start) / (d1 - piece_start);
let last = control.len() - 1;
let (left, _) = polygons(&control[last - degree..=last], s);
control[last - degree..=last].copy_from_slice(&left);
for knot in knots.iter_mut().skip(count - degree - 1) {
*knot = b;
}
}
if control.iter().any(|point| !(point.w > 1e-12)) {
return Err("extend_conic_ends: the extended piece carries a non-positive weight".to_string());
}
let mut extended = NurbsCurve::new(degree, knots, control)?;
let [e0, e1] = extended.domain()?;
if a > e0 + tolerance {
extended = extended.split(a)?.1;
}
if b < e1 - tolerance {
extended = extended.split(b)?.0;
}
Ok(extended)
}
pub(in crate::blend) fn plane_chart_pcurve(surface: &NurbsSurface, curve: &NurbsCurve) -> Option<NurbsCurve> {
if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) {
return None;
}
let [u0, u1] = surface.domain_u().ok()?;
let [v0, v1] = surface.domain_v().ok()?;
let origin = surface.evaluate(u0, v0).ok()?;
let du = surface.evaluate(u1, v0).ok()?.sub(origin).scale(1.0 / (u1 - u0));
let dv = surface.evaluate(u0, v1).ok()?.sub(origin).scale(1.0 / (v1 - v0));
let (a, b, c) = (du.dot(du), du.dot(dv), dv.dot(dv));
let det = a * c - b * b;
let chart_scale = origin.length().max(du.length()).max(dv.length()).max(1.0);
if !(det > 1e-24 * chart_scale.powi(4)) {
return None;
}
let affine = |point: Vec3| -> [f64; 2] {
let offset = point.sub(origin);
let (r, s) = (offset.dot(du), offset.dot(dv));
[u0 + (c * r - b * s) / det, v0 + (a * s - b * r) / det]
};
let mut pcurve = curve.clone();
for control in &mut pcurve.control_points {
let weight = control.w;
let [u, v] = affine(Vec3::new(control.x / weight, control.y / weight, control.z / weight));
control.x = u * weight;
control.y = v * weight;
control.z = 0.0;
}
let pcurve = NurbsCurve::new(pcurve.degree, pcurve.knots.clone(), pcurve.control_points.clone()).ok()?;
let [t0, t1] = curve.domain().ok()?;
let floor = crate::KernelTolerances::for_scale(chart_scale, 1e-7).model;
for step in 0..=64 {
let t = t0 + (t1 - t0) * step as f64 / 64.0;
let uv = pcurve.evaluate(t).ok()?;
let on_chart = surface.evaluate_extended(uv.x, uv.y).ok()?;
if on_chart.sub(curve.evaluate(t).ok()?).length() > floor {
return None;
}
}
Some(pcurve)
}
pub(in crate::blend) struct EndSurgery {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_edge_id: u64,
pub(in crate::blend) first_edge_parameter: f64,
pub(in crate::blend) second_edge_id: u64,
pub(in crate::blend) second_edge_parameter: f64,
pub(in crate::blend) end_face_id: u64,
pub(in crate::blend) transverse_curve: NurbsCurve,
pub(in crate::blend) transverse_blend_pcurve: NurbsCurve,
pub(in crate::blend) transverse_end_pcurve: NurbsCurve,
pub(in crate::blend) escalated: bool,
}
pub(in crate::blend) enum EndPlan {
Free(EndSurgery),
Corner(CornerEnd),
Miter(MiterEnd),
Cap(CapEnd),
}
pub(in crate::blend) struct CapEnd {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_vertex: u64,
pub(in crate::blend) second_vertex: u64,
pub(in crate::blend) arc_edge_id: u64,
pub(in crate::blend) arc_blend_pcurve: NurbsCurve,
pub(in crate::blend) first_leg: (u64, NurbsCurve),
pub(in crate::blend) second_leg: (u64, NurbsCurve),
pub(in crate::blend) sharp_vertex: u64,
}
pub(in crate::blend) struct MiterEnd {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_vertex: u64,
pub(in crate::blend) second_vertex: u64,
pub(in crate::blend) edges: Vec<(u64, bool, NurbsCurve)>,
}
pub(in crate::blend) struct CornerEnd {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_vertex: u64,
pub(in crate::blend) second_vertex: u64,
pub(in crate::blend) arc_edge_id: u64,
pub(in crate::blend) arc_blend_pcurve: NurbsCurve,
}
impl EndPlan {
pub(in crate::blend) fn cr_parameter(&self) -> f64 {
match self {
EndPlan::Free(end) => end.cr_parameter,
EndPlan::Corner(end) => end.cr_parameter,
EndPlan::Miter(end) => end.cr_parameter,
EndPlan::Cap(end) => end.cr_parameter,
}
}
pub(in crate::blend) fn cs_parameter(&self) -> f64 {
match self {
EndPlan::Free(end) => end.cs_parameter,
EndPlan::Corner(end) => end.cs_parameter,
EndPlan::Miter(end) => end.cs_parameter,
EndPlan::Cap(end) => end.cs_parameter,
}
}
pub(in crate::blend) fn free(&self) -> Option<&EndSurgery> {
match self {
EndPlan::Free(end) => Some(end),
EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => None,
}
}
fn planned_rims(&self) -> Option<(u64, u64)> {
match self {
EndPlan::Free(_) => None,
EndPlan::Corner(end) => Some((end.first_vertex, end.second_vertex)),
EndPlan::Miter(end) => Some((end.first_vertex, end.second_vertex)),
EndPlan::Cap(end) => Some((end.first_vertex, end.second_vertex)),
}
}
pub(in crate::blend) fn planned_rim(&self, side_first: bool) -> Option<u64> {
self.planned_rims()
.map(|(first, second)| if side_first { first } else { second })
}
}
pub(in crate::blend) struct SewnStripe {
pub(in crate::blend) cr_edge_id: Option<u64>,
pub(in crate::blend) cs_edge_id: Option<u64>,
pub(in crate::blend) blend_face_id: u64,
pub(in crate::blend) consumed: [bool; 2],
pub(in crate::blend) snap: f64,
pub(in crate::blend) identified: Vec<(u64, u64)>,
}
pub(in crate::blend) enum RimResolution {
Fresh,
Consumed(u64),
Corner(u64),
}
impl RimResolution {
pub(in crate::blend) fn existing(&self) -> Option<u64> {
match self {
RimResolution::Fresh => None,
RimResolution::Consumed(id) | RimResolution::Corner(id) => Some(*id),
}
}
pub(in crate::blend) fn is_consumed(&self) -> bool {
matches!(self, RimResolution::Consumed(_))
}
pub(in crate::blend) fn trims_boundary(&self) -> bool {
matches!(self, RimResolution::Fresh)
}
}
pub(crate) const END_TANGENT_CONTINUATION_WHAT: &str = "end_tangent_continuation";
const END_CONTINUATION_BAND: f64 = 1e-4;
const END_CONTINUATION_DEFAULT: bool = true;
thread_local! {
static END_CONTINUATION_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(crate) fn end_continuation_on() -> bool {
if let Some(forced) = END_CONTINUATION_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_END_CONTINUATION").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => END_CONTINUATION_DEFAULT,
}
}
pub(in crate::blend) fn resolve_free_end(
solid: &BrepSolid,
edge_id: u64,
first: &BlendMate,
second: &BlendMate,
rows: &FittedRows,
vertex: u64,
at_start: bool,
) -> Result<(EndSurgery, bool), KernelRefusal> {
let first_face = first.face;
let second_face = second.face;
let first_edge_id = boundary_edge_at_vertex(solid, first_face, vertex, edge_id)?;
let second_edge_id = boundary_edge_at_vertex(solid, second_face, vertex, edge_id)?;
if first_edge_id == second_edge_id {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"shared_end_boundary",
"blend: mates share their end boundary edge (unsupported)",
));
}
let blended = solid
.edges
.iter()
.find(|candidate| candidate.id == edge_id)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Collect, "blended_edge", "blend: blended edge missing"))?;
let landed_past_trim = |boundary: &EdgeRecord, t: f64| -> bool {
let (low, high) = (boundary.t0.min(boundary.t1), boundary.t0.max(boundary.t1));
let margin = 1e-9 * (high - low).max(1e-12);
t < low - margin || t > high + margin
};
let continuation = |boundary: &EdgeRecord| -> Result<Option<KernelRefusal>, KernelRefusal> {
if !end_continuation_on() {
return Ok(None);
}
let away_blended = super::super::network::tangent_away_from(blended, vertex)?;
let away_boundary = super::super::network::tangent_away_from(boundary, vertex)?;
let gap = 1.0 + away_blended.dot(away_boundary);
Ok((gap <= END_CONTINUATION_BAND).then(|| {
KernelRefusal::unsupported(
KernelStage::Classify,
END_TANGENT_CONTINUATION_WHAT,
format!(
"blend: boundary edge {} continues the blended edge {edge_id} smoothly at vertex {vertex} \
(the two leave it {gap:.2e} from collinear) and the rail does not cross it: a tangent \
chain, not a transverse end",
boundary.id
),
)
}))
};
let end_face = end_face_id(
solid,
first_face.id,
second_face.id,
first_edge_id,
second_edge_id,
)?;
let first_boundary = solid
.edges
.iter()
.find(|candidate| candidate.id == first_edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"end_boundary_edge",
"blend: end boundary edge missing",
)
})?;
let second_boundary = solid
.edges
.iter()
.find(|candidate| candidate.id == second_edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"end_boundary_edge",
"blend: end boundary edge missing",
)
})?;
let first_boundary_coedge = first_face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == first_edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"mate_boundary_coedge",
"blend: mate does not use its end boundary edge",
)
})?;
let second_boundary_coedge = second_face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == second_edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"mate_boundary_coedge",
"blend: mate does not use its end boundary edge",
)
})?;
let end_record = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == end_face)
.ok_or_else(|| {
KernelRefusal::internal(KernelStage::Collect, "end_face", "blend: end face missing")
})?;
let (cr_parameter, first_edge_parameter, cr_escalated) =
match support_crossing(solid, &rows.cr, first_boundary, at_start) {
Ok((s, t, _, escalated)) => (s, t, escalated),
Err(_) => {
let landing = support_crossing_uv(
solid,
&rows.cr,
&rows.cr_pcurve,
first_boundary,
first_boundary_coedge,
at_start,
end_record,
)?;
if landing.2 && landed_past_trim(first_boundary, landing.1) {
if let Some(refusal) = continuation(first_boundary)? {
return Err(refusal);
}
}
landing
}
};
let (cs_parameter, second_edge_parameter, cs_escalated) =
match support_crossing(solid, &rows.cs, second_boundary, at_start) {
Ok((s, t, _, escalated)) => (s, t, escalated),
Err(_) => {
let landing = support_crossing_uv(
solid,
&rows.cs,
&rows.cs_pcurve,
second_boundary,
second_boundary_coedge,
at_start,
end_record,
)?;
if landing.2 && landed_past_trim(second_boundary, landing.1) {
if let Some(refusal) = continuation(second_boundary)? {
return Err(refusal);
}
}
landing
}
};
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
for (label, row, parameter, mate, boundary, edge_parameter) in [
("cr", &rows.cr, cr_parameter, &first_face.surface, first_boundary, first_edge_parameter),
("cs", &rows.cs, cs_parameter, &second_face.surface, second_boundary, second_edge_parameter),
] {
let point = row.evaluate_extended(parameter);
let standoff = point.as_ref().ok().and_then(|p| crate::project_point_to_surface(mate, *p).ok()).map(|projection| projection.distance);
let on_boundary = boundary.curve.evaluate_extended(edge_parameter);
let gap = match (&point, &on_boundary) { (Ok(p), Ok(b)) => Some(p.sub(*b).length()), _ => None };
eprintln!(
"free end v{vertex} at_start={at_start} {label}: parameter {parameter:.6} of {:?}, rail point {:?}, {:?} off mate {}, boundary {} t={edge_parameter:.6} (edge t in [{:.4}, {:.4}]), rail-to-boundary gap {:?}",
rows.u_domain, point.ok(), standoff, if label == "cr" { first_face.id } else { second_face.id }, boundary.id, boundary.t0, boundary.t1, gap
);
let mut profile = String::new();
let [d0, d1] = rows.u_domain;
for step in 0..=20 {
let u = d0 + (d1 - d0) * step as f64 / 20.0;
let off = row.evaluate_extended(u).ok().and_then(|p| crate::project_point_to_surface(mate, p).ok()).map(|p| p.distance).unwrap_or(f64::NAN);
profile.push_str(&format!(" {u:.2}:{off:.1e}"));
}
eprintln!(" {label} row standoff profile:{profile}");
}
}
for (boundary, parameter) in [
(first_boundary, first_edge_parameter),
(second_boundary, second_edge_parameter),
] {
let (near, far) = if boundary.start_vertex_id == vertex {
(boundary.t0, boundary.t1)
} else {
(boundary.t1, boundary.t0)
};
if !rows.exact_extrusion && (parameter - far) * (far - near) > 0.0 {
let distance = boundary.curve.evaluate_extended(parameter)
.or_refuse(KernelStage::Refine, "evaluate_extended")?
.sub(boundary.curve.evaluate(far).or_refuse(KernelStage::Refine, "evaluate")?)
.length();
if distance > consumed_band(solid) {
return Err(KernelRefusal::unsupported(KernelStage::Refine,
"end_crossing_extent", format!(
"blend: end crossing is {distance:.3e} past boundary edge {}'s far vertex",
boundary.id)));
}
}
}
let in_range = if at_start {
cr_parameter >= RIM_MARGIN && cs_parameter >= RIM_MARGIN
} else {
cr_parameter <= 1.0 - RIM_MARGIN && cs_parameter <= 1.0 - RIM_MARGIN
};
let clamp01 = |value: f64| value.clamp(0.0, 1.0);
let (transverse, blend_pcurve, end_pcurve) = match exact_section_transverse(
rows,
end_record,
clamp01(cr_parameter),
clamp01(cs_parameter),
)? {
Some(exact) => exact,
None => {
let extends = |boundary: &EdgeRecord, parameter: f64| {
let (low, high) = (boundary.t0.min(boundary.t1), boundary.t0.max(boundary.t1));
let slack = 1e-9 * (high - low).abs().max(1.0);
parameter < low - slack || parameter > high + slack
};
let planar_end = matches!(end_record.surface.analytic(), Some(crate::AnalyticSurface::Plane { .. }));
let exact_ends = if crate::blend::planar_chart::ruled_chart_on() && !planar_end {
[extends(first_boundary, first_edge_parameter), extends(second_boundary, second_edge_parameter)]
} else {
[false, false]
};
transverse_curve_with_ends(
rows,
end_record,
clamp01(cr_parameter),
clamp01(cs_parameter),
exact_ends,
)?
}
};
Ok((
EndSurgery {
cr_parameter,
cs_parameter,
first_edge_id,
first_edge_parameter,
second_edge_id,
second_edge_parameter,
end_face_id: end_face,
transverse_curve: transverse,
transverse_blend_pcurve: blend_pcurve,
transverse_end_pcurve: end_pcurve,
escalated: cr_escalated || cs_escalated,
},
in_range,
))
}
fn exact_section_transverse(
rows: &FittedRows,
end_face: &FaceRecord,
cr_parameter: f64,
cs_parameter: f64,
) -> Result<Option<(NurbsCurve, NurbsCurve, NurbsCurve)>, KernelRefusal> {
if !rows.exact_extrusion || (cr_parameter - cs_parameter).abs() > 1e-9 {
return Ok(None);
}
let Some(crate::AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
u_domain,
v_domain,
}) = end_face.surface.analytic()
else {
return Ok(None);
};
let (a, b, c) = (u_dir.dot(*u_dir), u_dir.dot(*v_dir), v_dir.dot(*v_dir));
let determinant = a * c - b * b;
if determinant.abs() <= 1e-16 * (a * c).max(1.0) {
return Ok(None);
}
let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
return Ok(None);
};
let [u0, u1] = rows.u_domain;
let direction = rows
.cr
.evaluate(u1).or_refuse(KernelStage::Refine, "evaluate")?
.sub(rows.cr.evaluate(u0).or_refuse(KernelStage::Refine, "evaluate")?)
.normalized().or_refuse(KernelStage::Refine, "normalized")?;
if direction.cross(normal).length() > 1e-9 {
return Ok(None);
}
let section = rows.surface.iso_curve_u(cr_parameter).or_refuse(KernelStage::Refine, "iso_curve_u")?;
let [v0, v1] = section.domain().or_refuse(KernelStage::Refine, "domain")?;
let blend_pcurve =
crate::sweep_topology::parameter_line(cr_parameter, v0, cr_parameter, v1).or_refuse(KernelStage::Refine, "parameter_line")?;
let mut mapped = Vec::with_capacity(section.control_points.len());
for control in §ion.control_points {
let offset = control.point().or_refuse(KernelStage::Refine, "point")?.sub(*origin);
let (fu, fv) = (u_dir.dot(offset), v_dir.dot(offset));
let u = (fu * c - fv * b) / determinant + u_domain[0];
let v = (fv * a - fu * b) / determinant + v_domain[0];
mapped.push(Vec4 {
x: u * control.w,
y: v * control.w,
z: 0.0,
w: control.w,
});
}
let end_pcurve = NurbsCurve::new(section.degree, section.knots.clone(), mapped).or_refuse(KernelStage::Refine, "new")?;
Ok(Some((section, blend_pcurve, end_pcurve)))
}
pub(in crate::blend) struct EndCorner {
pub(in crate::blend) loop_index: usize,
pub(in crate::blend) x_coedge_id: u64,
pub(in crate::blend) y_coedge_id: u64,
pub(in crate::blend) x_is_first: bool,
pub(in crate::blend) pole_coedge_ids: Vec<u64>,
pub(in crate::blend) pole_edge_ids: Vec<u64>,
}
pub(in crate::blend) fn locate_end_corner(
solid: &BrepSolid,
face: &FaceRecord,
corner: u64,
first_edge_id: u64,
second_edge_id: u64,
) -> Option<EndCorner> {
let edge_of = |coedge: &CoedgeRecord| {
solid
.edges
.iter()
.find(|candidate| candidate.id == coedge.edge_id)
};
let traversal = |coedge: &CoedgeRecord| -> Option<(u64, u64)> {
let edge = edge_of(coedge)?;
Some(if coedge.forward {
(edge.start_vertex_id, edge.end_vertex_id)
} else {
(edge.end_vertex_id, edge.start_vertex_id)
})
};
let is_pole = |coedge: &CoedgeRecord| -> bool {
edge_of(coedge).is_some_and(|edge| {
edge.degenerate && edge.start_vertex_id == corner && edge.end_vertex_id == corner
})
};
for (loop_index, loop_record) in face.loops.iter().enumerate() {
let count = loop_record.coedges.len();
for index in 0..count {
let this = &loop_record.coedges[index];
if is_pole(this) {
continue;
}
let mut pole_coedge_ids = Vec::new();
let mut pole_edge_ids = Vec::new();
let mut step = 1usize;
while step < count && is_pole(&loop_record.coedges[(index + step) % count]) {
let pole = &loop_record.coedges[(index + step) % count];
pole_coedge_ids.push(pole.id);
pole_edge_ids.push(pole.edge_id);
step += 1;
}
if step >= count {
continue;
}
let next = &loop_record.coedges[(index + step) % count];
let this_pair = (this.edge_id, next.edge_id);
if this_pair != (first_edge_id, second_edge_id)
&& this_pair != (second_edge_id, first_edge_id)
{
continue;
}
let (Some((_, this_end)), Some((next_start, _))) = (traversal(this), traversal(next))
else {
continue;
};
if this_end != corner || next_start != corner {
continue;
}
return Some(EndCorner {
loop_index,
x_coedge_id: this.id,
y_coedge_id: next.id,
x_is_first: this.edge_id == first_edge_id,
pole_coedge_ids,
pole_edge_ids,
});
}
}
None
}
pub(in crate::blend) fn splice_end_corner(
result: &mut BrepSolid,
end_face: u64,
corner: &EndCorner,
transverse: CoedgeRecord,
x_consumed: bool,
y_consumed: bool,
) -> Result<(), KernelRefusal> {
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == end_face)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"end_face_lost",
"blend: end face lost during surgery",
)
})?;
let loop_record = face
.loops
.get_mut(corner.loop_index)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"corner_loop_lost",
"blend: end-face corner loop lost during surgery",
)
})?;
let count = loop_record.coedges.len();
let seg_index = loop_record
.coedges
.iter()
.position(|coedge| coedge.id == corner.x_coedge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"corner_coedge_lost",
"blend: end-face corner coedge lost during surgery",
)
})?;
for (offset, pole) in corner.pole_coedge_ids.iter().enumerate() {
if loop_record.coedges[(seg_index + 1 + offset) % count].id != *pole {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"corner_pole_moved",
"blend: end-face corner pole no longer between the boundaries",
));
}
}
let y_index = (seg_index + 1 + corner.pole_coedge_ids.len()) % count;
if loop_record.coedges[y_index].id != corner.y_coedge_id {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"corner_pair_adjacent",
"blend: end-face corner pair no longer adjacent",
));
}
let mut transverse = Some(transverse);
let mut rebuilt = Vec::with_capacity(count + 1);
for index in 0..count {
let coedge = &loop_record.coedges[index];
if index == seg_index {
if !x_consumed {
rebuilt.push(coedge.clone());
}
rebuilt.push(transverse.take().expect("transverse spliced once"));
} else if index == y_index {
if !y_consumed {
rebuilt.push(coedge.clone());
}
} else if !corner.pole_coedge_ids.contains(&coedge.id) {
rebuilt.push(coedge.clone());
}
}
check_loop_branches(&face.surface, &rebuilt)?;
let loop_record = face
.loops
.get_mut(corner.loop_index)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"corner_loop_lost",
"blend: end-face corner loop lost during surgery",
)
})?;
loop_record.coedges = rebuilt;
Ok(())
}
fn check_loop_branches(surface: &NurbsSurface, coedges: &[CoedgeRecord]) -> Result<(), KernelRefusal> {
let (closed_u, closed_v) = surface.closed_directions().unwrap_or((false, false));
if !closed_u && !closed_v {
return Ok(());
}
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 periods = [
if closed_u { u1 - u0 } else { 0.0 },
if closed_v { v1 - v0 } else { 0.0 },
];
for index in 0..coedges.len() {
let this = &coedges[index];
let next = &coedges[(index + 1) % coedges.len()];
let [_, this_end] = this.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let [next_start, _] = next.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let leaving = this.pcurve.evaluate(this_end).or_refuse(KernelStage::Refine, "evaluate")?;
let arriving = next.pcurve.evaluate(next_start).or_refuse(KernelStage::Refine, "evaluate")?;
for (axis, gap) in [
(0usize, (leaving.x - arriving.x).abs()),
(1, (leaving.y - arriving.y).abs()),
] {
if periods[axis] > 0.0 && gap >= periods[axis] * 0.5 {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"loop_crosses_seam",
format!(
"blend: the end cross-section joins the end face's loop across its SEAM \
(the junction jumps {gap:.6} in the closed direction, a period of \
{:.6}) — the blend runs past the corner into material a third face \
supplies, and closing it needs that face SPLIT along the blend, which \
the rolling-ball surgery does not do",
periods[axis]
),
));
}
}
}
Ok(())
}
pub(in crate::blend) const BLEND_WIDER_THAN_FACE: &str =
"blend: the blend is wider than the face it must lie on —";
pub(crate) const RAIL_COLLAPSE_UNSUPPORTED: &str =
"blend: a full-width rail collapse is not built here —";
pub(crate) const RAIL_COLLAPSE_WHAT: &str = "rail_collapse";
pub(crate) fn is_rail_collapse(refusal: &KernelRefusal) -> bool {
matches!(&refusal.class, RefusalClass::UnsupportedGeometry { what } if what == RAIL_COLLAPSE_WHAT)
}
pub(in crate::blend) const RIM_MARGIN: f64 = 2.0e-3;
pub(crate) fn consumed_band(solid: &BrepSolid) -> f64 {
crate::KernelTolerances::for_solid(solid, 1e-7).sliver
}
pub(crate) const CONSUMED_SNAP_UNSOUND: &str =
"blend: the blend is within a sliver of its face's width without matching it";
pub(crate) const CONSUMED_SNAP_WHAT: &str = "consumed_snap";
pub(crate) fn is_consumed_snap(refusal: &KernelRefusal) -> bool {
matches!(&refusal.class, RefusalClass::IllPosed { what } if what == CONSUMED_SNAP_WHAT)
}
pub(in crate::blend) fn check_snap_closure(
input: &BrepSolid,
result: &BrepSolid,
snap: f64,
) -> Result<(), KernelRefusal> {
if !(snap > crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
return Ok(());
}
let report = crate::shell_vector_areas(result);
if !report.is_flagged() && report.unreadable.is_empty() {
return Ok(());
}
if crate::shell_vector_areas(input).is_flagged() {
return Ok(());
}
let (residual, bar) = report
.worst_shell()
.map_or((f64::NAN, f64::NAN), |shell| (shell.residual(), shell.bar));
Err(KernelRefusal::ill_posed(
KernelStage::Validate,
CONSUMED_SNAP_WHAT,
format!(
"{CONSUMED_SNAP_UNSOUND} — its rail ends {snap:.3e} from the face's far edge, inside \
the {:.3e} sliver band, so the strip between them is no face; and snapped onto that \
edge, the shell's trims close to {residual:.3e} against a bar of {bar:.3e}{}. Make \
the size the width, or move it away from the width by more than the band",
consumed_band(input),
if report.unreadable.is_empty() {
String::new()
} else {
format!(" ({} faces unreadable)", report.unreadable.len())
}
),
))
}
pub(in crate::blend) struct RowSew {
pub(in crate::blend) edge_id: u64,
pub(in crate::blend) forward: bool,
pub(in crate::blend) collapsed_edges: Vec<u64>,
pub(in crate::blend) deviation: f64,
}
#[allow(clippy::too_many_arguments)]
pub(in crate::blend) fn detect_row_coincidence(
solid: &BrepSolid,
mate: &BlendMate,
blended_edge_id: u64,
boundary_start_id: u64,
boundary_finish_id: u64,
start_consumed: bool,
start_far_vertex: u64,
finish_consumed: bool,
finish_far_vertex: u64,
row: &NurbsCurve,
row_traversed_from_start: bool,
band: f64,
) -> Result<Option<RowSew>, KernelRefusal> {
if !(start_consumed && finish_consumed) {
return Ok(None);
}
if start_far_vertex == finish_far_vertex {
return Ok(None);
}
if mate.face.loops.len() != 1 || mate.loop_index != 0 {
return Ok(None);
}
let edge_by_id = |id: u64| solid.edges.iter().find(|candidate| candidate.id == id);
let vertex_point = |id: u64| {
solid
.vertices
.iter()
.find(|candidate| candidate.id == id)
.map(|vertex| vertex.point)
};
let mut sewn_coedge: Option<&CoedgeRecord> = None;
let mut leftovers: Vec<u64> = Vec::new();
for coedge in &mate.face.loops[0].coedges {
if coedge.edge_id == blended_edge_id
|| coedge.edge_id == boundary_start_id
|| coedge.edge_id == boundary_finish_id
{
continue;
}
let Some(candidate) = edge_by_id(coedge.edge_id) else {
return Err(KernelRefusal::internal(
KernelStage::Classify,
"mate_loop_edge",
"blend: mate loop references a missing edge",
));
};
let joins = (candidate.start_vertex_id == start_far_vertex
&& candidate.end_vertex_id == finish_far_vertex)
|| (candidate.start_vertex_id == finish_far_vertex
&& candidate.end_vertex_id == start_far_vertex);
if joins {
if sewn_coedge.is_some() {
return Ok(None);
}
sewn_coedge = Some(coedge);
} else if !leftovers.contains(&candidate.id) {
leftovers.push(candidate.id);
}
}
let Some(sewn_coedge) = sewn_coedge else {
return Ok(None);
};
let Some(target) = edge_by_id(sewn_coedge.edge_id) else {
return Ok(None);
};
let (Some(start_point), Some(finish_point)) = (
vertex_point(start_far_vertex),
vertex_point(finish_far_vertex),
) else {
return Ok(None);
};
let extent = finish_point.sub(start_point).length();
if extent <= band * 4.0 {
return Ok(None);
}
let aligned = target.start_vertex_id == start_far_vertex;
let [row_t0, row_t1] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
let blended_middle = edge_by_id(blended_edge_id)
.map(|blended| blended.curve.evaluate(0.5 * (blended.t0 + blended.t1)))
.transpose()
.or_refuse(KernelStage::Classify, "evaluate")?;
const ROW_COINCIDENCE_SAMPLES: usize = 16;
let mut deviation = 0.0f64;
for sample in 0..=ROW_COINCIDENCE_SAMPLES {
let fraction = sample as f64 / ROW_COINCIDENCE_SAMPLES as f64;
let on_row = row.evaluate(row_t0 + (row_t1 - row_t0) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let edge_fraction = if aligned { fraction } else { 1.0 - fraction };
let on_edge = target
.curve
.evaluate(target.t0 + (target.t1 - target.t0) * edge_fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let gap = on_row.sub(on_edge).length();
if gap > band {
let outside = blended_middle
.is_some_and(|middle| on_row.sub(on_edge).dot(middle.sub(on_edge)) < 0.0);
if outside {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"blend_wider_than_face",
format!(
"{BLEND_WIDER_THAN_FACE} its rail runs {gap:.3e} past face {}'s far edge \
{} (more than the {band:.3e} it can be snapped across), outside the face",
mate.face.id, target.id
),
));
}
return Ok(None);
}
deviation = deviation.max(gap);
}
let quarter_row = row.evaluate(row_t0 + (row_t1 - row_t0) * 0.25).or_refuse(KernelStage::Refine, "evaluate")?;
let opposite_fraction = if aligned { 0.75 } else { 0.25 };
let quarter_opposite = target
.curve
.evaluate(target.t0 + (target.t1 - target.t0) * opposite_fraction).or_refuse(KernelStage::Refine, "evaluate")?;
if quarter_row.sub(quarter_opposite).length() <= band {
return Ok(None);
}
for id in &leftovers {
let Some(leftover) = edge_by_id(*id) else {
return Ok(None);
};
if leftover.start_vertex_id != leftover.end_vertex_id {
return Ok(None);
}
let a = leftover.curve.evaluate(leftover.t0).or_refuse(KernelStage::Refine, "evaluate")?;
let mid = leftover
.curve
.evaluate(leftover.t0 + (leftover.t1 - leftover.t0) * 0.5).or_refuse(KernelStage::Refine, "evaluate")?;
let b = leftover.curve.evaluate(leftover.t1).or_refuse(KernelStage::Refine, "evaluate")?;
if a.sub(b).length() > band || a.sub(mid).length() > band {
return Ok(None);
}
let used_elsewhere = solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
face.id != mate.face.id
&& face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == *id)
});
if used_elsewhere {
return Ok(None);
}
}
let traversal_start = if row_traversed_from_start {
start_far_vertex
} else {
finish_far_vertex
};
let forward = target.start_vertex_id == traversal_start;
if sewn_coedge.forward != forward {
return Ok(None);
}
let mut opposing_uses = 0usize;
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if face.id == mate.face.id {
continue;
}
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
if coedge.edge_id != target.id {
continue;
}
if coedge.forward == forward {
return Ok(None);
}
opposing_uses += 1;
}
}
if opposing_uses != 1 {
return Ok(None);
}
Ok(Some(RowSew {
edge_id: target.id,
forward,
collapsed_edges: leftovers,
deviation,
}))
}
pub(in crate::blend) fn boundary_edge_at_vertex(
solid: &BrepSolid,
face: &FaceRecord,
vertex: u64,
exclude: u64,
) -> Result<u64, KernelRefusal> {
let edge_at = |edge_id: u64| -> Option<&EdgeRecord> {
solid.edges.iter().find(|edge| edge.id == edge_id)
};
for loop_record in &face.loops {
let count = loop_record.coedges.len();
if count == 0 {
continue;
}
for index in 0..count {
if loop_record.coedges[index].edge_id != exclude {
continue;
}
for step in [count - 1, 1] {
let neighbour = &loop_record.coedges[(index + step) % count];
if neighbour.edge_id == exclude {
continue;
}
if let Some(edge) = edge_at(neighbour.edge_id) {
if !edge.degenerate
&& (edge.start_vertex_id == vertex || edge.end_vertex_id == vertex)
{
return Ok(edge.id);
}
}
}
}
}
let mut found = None;
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
if coedge.edge_id == exclude {
continue;
}
let edge = edge_at(coedge.edge_id).ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"loop_edge",
"blend: loop references missing edge",
)
})?;
if !edge.degenerate
&& (edge.start_vertex_id == vertex || edge.end_vertex_id == vertex)
{
if found.is_some() && found != Some(edge.id) {
return Err(KernelRefusal::ill_posed(
KernelStage::Collect,
"boundary_edge_ambiguous",
"blend: multiple boundary edges at the end vertex",
));
}
found = Some(edge.id);
}
}
}
found.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"boundary_edge_none",
"blend: no boundary edge at the end vertex",
)
})
}
pub(in crate::blend) fn end_face_id(
solid: &BrepSolid,
first_face: u64,
second_face: u64,
first_edge: u64,
second_edge: u64,
) -> Result<u64, KernelRefusal> {
let mut found = None;
for shell in &solid.shells {
for face in &shell.faces {
if face.id == first_face || face.id == second_face {
continue;
}
let uses_first = face
.loops
.iter()
.flat_map(|l| &l.coedges)
.any(|coedge| coedge.edge_id == first_edge);
let uses_second = face
.loops
.iter()
.flat_map(|l| &l.coedges)
.any(|coedge| coedge.edge_id == second_edge);
if uses_first && uses_second {
if found.is_some() {
return Err(KernelRefusal::ill_posed(
KernelStage::Collect,
"end_face_ambiguous",
"blend: ambiguous end face",
));
}
found = Some(face.id);
}
}
}
found.ok_or_else(|| {
KernelRefusal::unsupported(
KernelStage::Collect,
"end_face_none",
"blend: no single end face across the corner",
)
})
}
pub(in crate::blend) fn support_crossing_uv(
solid: &BrepSolid,
support: &NurbsCurve,
support_pcurve: &NurbsCurve,
boundary: &EdgeRecord,
boundary_coedge: &CoedgeRecord,
at_start: bool,
end_face: &FaceRecord,
) -> Result<(f64, f64, bool), KernelRefusal> {
let [s0, s1] = support_pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let [q0, q1] = boundary_coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let seed_s = if at_start { s0 } else { s1 };
let mut solved = None;
for seed_fraction in [0.0f64, 1.0, 0.5] {
let mut x = [seed_s, q0 + (q1 - q0) * seed_fraction];
let mut converged = false;
for _ in 0..NEWTON_ITERATIONS {
let a = support_pcurve.evaluate_extended(x[0]).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let b = boundary_coedge.pcurve.evaluate_extended(x[1]).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let residual = [a.x - b.x, a.y - b.y];
if residual[0].abs().max(residual[1].abs()) <= 1e-12 {
converged = true;
break;
}
let step = 1e-8;
let a_probe = support_pcurve.evaluate_extended(x[0] + step).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let b_probe = boundary_coedge.pcurve.evaluate_extended(x[1] + step).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let jacobian = [
[(a_probe.x - a.x) / step, -(b_probe.x - b.x) / step],
[(a_probe.y - a.y) / step, -(b_probe.y - b.y) / step],
];
let determinant = jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
if determinant.abs() <= 1e-16 {
break;
}
x[0] -= (residual[0] * jacobian[1][1] - residual[1] * jacobian[0][1]) / determinant;
x[1] -= (residual[1] * jacobian[0][0] - residual[0] * jacobian[1][0]) / determinant;
}
if converged {
let rank = (x[0] - seed_s).abs();
if solved
.map(|(existing_s, _): (f64, f64)| (existing_s - seed_s).abs() > rank)
.unwrap_or(true)
{
solved = Some((x[0], x[1]));
}
}
}
let (s, q) = solved.ok_or_else(|| {
KernelRefusal::unsupported(
KernelStage::Intersect,
"support_crossing_uv_none",
"blend: support curve does not reach the end boundary edge",
)
})?;
let span = (s1 - s0).abs();
if s < s0 - 1e-6 * span || s > s1 + 1e-6 * span {
return Err(KernelRefusal::unsupported(
KernelStage::Intersect,
"support_crossing_uv_outside",
format!(
"blend: support curve does not reach the end boundary edge (the parameter-space \
crossing lands at {s:.4}, outside the row's [{s0:.4}, {s1:.4}])"
),
));
}
let fraction = (q - q0) / (q1 - q0);
let fraction = if boundary_coedge.forward {
fraction
} else {
1.0 - fraction
};
let t = boundary.t0 + (boundary.t1 - boundary.t0) * fraction;
let extended = fraction < -1e-9 || fraction > 1.0 + 1e-9;
let scale = boundary
.curve
.evaluate(boundary.t0).or_refuse(KernelStage::Refine, "evaluate")?
.length()
.max(boundary.curve.evaluate(boundary.t1).or_refuse(KernelStage::Refine, "evaluate")?.length())
.max(1.0);
let historical = 1e-6 * scale;
let band = crate::KernelTolerances::for_solid(solid, 1e-7).pcurve_consistency;
let raw_gap = support
.evaluate_extended(s).or_refuse(KernelStage::Refine, "evaluate_extended")?
.sub(boundary.curve.evaluate_extended(t).or_refuse(KernelStage::Refine, "evaluate_extended")?)
.length();
let mut polished = false;
let (s, t) = if extended && raw_gap > historical {
let vertex_t = if fraction < 0.0 { boundary.t0 } else { boundary.t1 };
match polish_crossing_onto_end_face(support, boundary, end_face, s, t, vertex_t)? {
Some((s_polished, t_polished))
if s_polished >= s0 - 1e-6 * span && s_polished <= s1 + 1e-6 * span =>
{
polished = true;
(s_polished, t_polished)
}
_ => (s, t),
}
} else {
(s, t)
};
let gap = if polished {
0.0
} else {
raw_gap
};
if std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok() {
let on_support = support.evaluate_extended(s).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let on_boundary = boundary.curve.evaluate_extended(t).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let domain = boundary.curve.domain().unwrap_or([f64::NAN, f64::NAN]);
eprintln!(
" support_crossing_uv: boundary {} edge t=[{:.4},{:.4}] curve domain [{:.4},{:.4}] fraction {fraction:.4} extended={extended} polished={polished} s={s:.6} of [{s0:.4},{s1:.4}] t={t:.6} raw_gap={raw_gap:.3e} gap={gap:.3e} on_support=({:.6},{:.6},{:.6}) on_boundary=({:.6},{:.6},{:.6})",
boundary.id, boundary.t0, boundary.t1, domain[0], domain[1],
on_support.x, on_support.y, on_support.z, on_boundary.x, on_boundary.y, on_boundary.z
);
}
if gap > band.max(historical) {
return Err(KernelRefusal::unsupported(
KernelStage::Intersect,
"support_crossing_uv_phantom",
format!(
"blend: support curve does not reach the end boundary edge (the parameter-space \
crossing is {gap:.3e} off in 3D — the boundary continues the blended edge rather \
than crossing its rail)"
),
));
}
Ok((s, t, polished || gap > historical))
}
fn polish_crossing_onto_end_face(
support: &NurbsCurve,
boundary: &EdgeRecord,
end_face: &FaceRecord,
seed_s: f64,
seed_t: f64,
vertex_t: f64,
) -> Result<Option<(f64, f64)>, KernelRefusal> {
let Some(end_coedge) = end_face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == boundary.id)
else {
return Ok(None);
};
let seed_uv = edge_uv_on_face(end_coedge, boundary, vertex_t)?;
let vertex_point = boundary.curve.evaluate(vertex_t).or_refuse(KernelStage::Refine, "evaluate")?;
let landing = support.evaluate_extended(seed_s).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let reach = landing.sub(vertex_point).length();
let scale = landing.length().max(vertex_point.length()).max(1.0);
let tolerance = 1e-12 * scale;
let surface = &end_face.surface;
let mut x = [seed_s, seed_uv[0], seed_uv[1]];
let mut converged = false;
for _ in 0..NEWTON_ITERATIONS {
let a = support.derivatives_extended(x[0], 1).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let d = surface.derivatives_extended(x[1], x[2], 1).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let residual = a[0].sub(d[0][0]);
if residual.length() <= tolerance {
converged = true;
break;
}
let matrix = [
[a[1].x, -d[1][0].x, -d[0][1].x],
[a[1].y, -d[1][0].y, -d[0][1].y],
[a[1].z, -d[1][0].z, -d[0][1].z],
];
let Ok(delta) = fit::solve_small::<3>(matrix, [residual.x, residual.y, residual.z], 3) else {
return Ok(None);
};
if delta.iter().any(|value| !value.is_finite()) {
return Ok(None);
}
for (value, correction) in x.iter_mut().zip(delta) {
*value -= correction;
}
}
if !converged {
let a = support.evaluate_extended(x[0]).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let d = surface.evaluate_extended(x[1], x[2]).or_refuse(KernelStage::Refine, "evaluate_extended")?;
if a.sub(d).length() > 1e-9 * scale {
return Ok(None);
}
}
let polished = support.evaluate_extended(x[0]).or_refuse(KernelStage::Refine, "evaluate_extended")?;
if polished.sub(landing).length() > reach.max(1e-9 * scale) {
return Ok(None);
}
let mut t = seed_t;
for _ in 0..NEWTON_ITERATIONS {
let b = boundary.curve.derivatives_extended(t, 2).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let offset = b[0].sub(polished);
let g = offset.dot(b[1]);
let dg = b[1].dot(b[1]) + offset.dot(b[2]);
if !(dg.abs() > 0.0) || !dg.is_finite() {
break;
}
let step = g / dg;
if !step.is_finite() {
break;
}
t -= step;
if step.abs() <= 1e-14 * (1.0 + t.abs()) {
break;
}
}
if !t.is_finite() {
return Ok(None);
}
Ok(Some((x[0], t)))
}
pub(in crate::blend) fn support_crossing(
solid: &BrepSolid,
support: &NurbsCurve,
boundary: &EdgeRecord,
at_start: bool,
) -> Result<(f64, f64, Vec3, bool), KernelRefusal> {
for tolerance in support_crossing_tolerances(solid) {
if let Some((s, t, point)) = support_crossing_at(support, boundary, at_start, tolerance)? {
return Ok((s, t, point, tolerance > 1e-7));
}
}
Err(KernelRefusal::unsupported(
KernelStage::Intersect,
"support_crossing_none",
"blend: support curve does not reach the end boundary edge",
))
}
fn support_crossing_at(
support: &NurbsCurve,
boundary: &EdgeRecord,
at_start: bool,
tolerance: f64,
) -> Result<Option<(f64, f64, Vec3)>, KernelRefusal> {
let hits = crate::intersect_curves(support, &boundary.curve, tolerance).or_refuse(KernelStage::Refine, "intersect_curves")?;
let [s0, s1] = support.domain().or_refuse(KernelStage::Refine, "domain")?;
let mut best: Option<(f64, f64, Vec3)> = None;
for hit in hits {
if hit.t < boundary.t0 - 1e-9 || hit.t > boundary.t1 + 1e-9 {
continue;
}
let margin = 1e-9 * (1.0 + (s1 - s0).abs());
if hit.s < s0 - margin || hit.s > s1 + margin {
continue;
}
let rank = if at_start { hit.s - s0 } else { s1 - hit.s };
if best
.map(|(existing, ..)| {
let existing_rank = if at_start {
existing - s0
} else {
s1 - existing
};
rank < existing_rank
})
.unwrap_or(true)
{
best = Some((hit.s, hit.t, hit.point));
}
}
Ok(best)
}
pub(in crate::blend) fn support_crossing_tolerances(solid: &BrepSolid) -> [f64; 3] {
let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
[1e-7, policy.intersection_fit, policy.pcurve_consistency]
}
pub(in crate::blend) fn spoke_crossing(
solid: &BrepSolid,
support: &NurbsCurve,
spoke: &EdgeRecord,
vertex_point: Vec3,
) -> Result<Option<(f64, f64, bool)>, KernelRefusal> {
for tolerance in support_crossing_tolerances(solid) {
let hits = crate::intersect_curves(support, &spoke.curve, tolerance).or_refuse(KernelStage::Refine, "intersect_curves")?;
let mut best: Option<(f64, f64, f64)> = None;
for hit in hits {
if hit.t < spoke.t0 - 1e-9 || hit.t > spoke.t1 + 1e-9 {
continue;
}
let distance = hit.point.sub(vertex_point).length();
if best.map(|(.., known)| distance < known).unwrap_or(true) {
best = Some((hit.s, hit.t, distance));
}
}
if let Some((crossing_support, crossing_spoke, _)) = best {
return Ok(Some((crossing_support, crossing_spoke, tolerance > 1e-7)));
}
}
Ok(None)
}
#[derive(Clone, Copy, Default)]
pub(in crate::blend) struct SpokeCrossings {
escalated: bool,
}
impl SpokeCrossings {
pub(in crate::blend) fn note(&mut self, escalated: bool) {
self.escalated |= escalated;
}
pub(in crate::blend) fn gate(&self, result: BrepSolid) -> Result<BrepSolid, KernelRefusal> {
if !self.escalated {
return Ok(result);
}
let issues = result.validate();
if issues.is_empty() {
return Ok(result);
}
Err(KernelRefusal::new(
RefusalClass::InvalidResultTopology {
issues: issues.len() as u32,
},
KernelStage::Validate,
format!(
"blend: chain surgery across a fit-tolerance spoke crossing did not \
produce a valid solid ({})",
issues
.first()
.map(|issue| issue.message.clone())
.unwrap_or_default()
),
))
}
}
pub(in crate::blend) fn transverse_curve(
rows: &FittedRows,
end_face: &FaceRecord,
cr_parameter: f64,
cs_parameter: f64,
) -> Result<(NurbsCurve, NurbsCurve, NurbsCurve), KernelRefusal> {
transverse_curve_with_ends(rows, end_face, cr_parameter, cs_parameter, [false, false])
}
pub(in crate::blend) fn transverse_curve_with_ends(
rows: &FittedRows,
end_face: &FaceRecord,
cr_parameter: f64,
cs_parameter: f64,
exact_ends: [bool; 2],
) -> Result<(NurbsCurve, NurbsCurve, NurbsCurve), KernelRefusal> {
const FIRST_SECTIONS: usize = 16;
const MAX_SECTIONS: usize = 256;
if !transverse_ladder_on() {
return transverse_curve_sections(rows, end_face, cr_parameter, cs_parameter, exact_ends, FIRST_SECTIONS);
}
let mut sections = FIRST_SECTIONS;
let mut best: Option<((NurbsCurve, NurbsCurve, NurbsCurve), f64)> = None;
loop {
let built = transverse_curve_sections(rows, end_face, cr_parameter, cs_parameter, exact_ends, sections)?;
let miss = transverse_pcurve_miss(rows, &built, 4 * sections)?;
if std::env::var("BREP_DEBUG_TRANSVERSE_FIT").is_ok() {
let (sliver, length) = transverse_sliver(rows, &built, 4 * sections);
eprintln!(
"transverse ladder: end face {:?}, exact ends {exact_ends:?}, {sections} sections, pcurve images {miss:.3e} off the edge; \
sliver vector area {sliver:.3e} over length {length:.3e} ({:.3e} per length)",
end_face.name,
sliver / length
);
}
if sections > FIRST_SECTIONS {
crate::blend::record_count("count.transverse.refined_rungs", 1);
}
let done = miss <= crate::pcurve::PCURVE_REFINEMENT_TOLERANCE || sections >= MAX_SECTIONS;
if best.as_ref().map_or(true, |(_, known)| miss < *known) {
best = Some((built, miss));
}
if done {
break;
}
sections *= 2;
}
Ok(best.expect("one build at least").0)
}
const TRANSVERSE_LADDER_DEFAULT: bool = false;
thread_local! {
static TRANSVERSE_LADDER_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(in crate::blend) fn transverse_ladder_on() -> bool {
if let Some(forced) = TRANSVERSE_LADDER_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_TRANSVERSE_LADDER").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => TRANSVERSE_LADDER_DEFAULT,
}
}
fn transverse_pcurve_miss(
rows: &FittedRows,
built: &(NurbsCurve, NurbsCurve, NurbsCurve),
samples: usize,
) -> Result<f64, KernelRefusal> {
let (curve, blend_pcurve, _) = built;
let mut worst = 0.0f64;
for k in 0..samples {
let z = (k as f64 + 0.5) / samples as f64;
let read = (|| -> Result<f64, String> {
let point = curve.evaluate(z)?;
let on_wall = blend_pcurve.evaluate(z)?;
Ok(point.sub(rows.surface.evaluate_extended(on_wall.x, on_wall.y)?).length())
})();
match read {
Ok(miss) if miss.is_finite() => worst = worst.max(miss),
_ => return Ok(f64::INFINITY),
}
}
Ok(worst)
}
fn transverse_sliver(rows: &FittedRows, built: &(NurbsCurve, NurbsCurve, NurbsCurve), samples: usize) -> (f64, f64) {
let (curve, blend_pcurve, _) = built;
let (mut area, mut length) = (Vec3::new(0.0, 0.0, 0.0), 0.0);
let at = |z: f64| -> Option<(Vec3, Vec3)> {
let on_wall = blend_pcurve.evaluate(z).ok()?;
Some((curve.evaluate(z).ok()?, rows.surface.evaluate_extended(on_wall.x, on_wall.y).ok()?))
};
for k in 0..samples {
let (z0, z1) = (k as f64 / samples as f64, (k + 1) as f64 / samples as f64);
let (Some((p0, i0)), Some((p1, i1))) = (at(z0), at(z1)) else { return (f64::NAN, f64::NAN) };
let gap = i0.sub(p0).add(i1.sub(p1)).scale(0.5);
area = area.add(gap.cross(p1.sub(p0)));
length += p1.sub(p0).length();
}
(area.length(), length)
}
fn transverse_curve_sections(
rows: &FittedRows,
end_face: &FaceRecord,
cr_parameter: f64,
cs_parameter: f64,
exact_ends: [bool; 2],
sections: usize,
) -> Result<(NurbsCurve, NurbsCurve, NurbsCurve), KernelRefusal> {
let exact = exact_ends[0] || exact_ends[1];
let mut points3 = Vec::with_capacity(sections + 1);
let mut blend_uv = Vec::with_capacity(sections + 1);
let mut end_uv: Vec<[f64; 2]> = Vec::with_capacity(sections + 1);
let mut parameters = Vec::with_capacity(sections + 1);
let project = |point: Vec3, seed: Option<[f64; 2]>| match seed {
None => crate::project_point_to_surface(&end_face.surface, point)
.or_refuse(KernelStage::Refine, "project_point_to_surface"),
Some(seed) => crate::projection::project_point_to_surface_from_seed(&end_face.surface, point, seed)
.or_refuse(KernelStage::Refine, "project_point_to_surface_from_seed"),
};
for section in 0..=sections {
let z = section as f64 / sections as f64;
let mut t = cr_parameter + (cs_parameter - cr_parameter) * z;
let mut foot = end_uv.last().copied();
if section > 0 && section < sections {
for _ in 0..NEWTON_ITERATIONS {
let point = rows.surface.evaluate_extended(t, z).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let projection = project(point, foot)?;
foot = Some([projection.u, projection.v]);
let normal = raw_normal(&end_face.surface, projection.u, projection.v)?;
let distance = point.sub(projection.point).dot(normal);
if distance.abs() <= 1e-11 {
break;
}
let step = 1e-7;
let probe = rows.surface.evaluate_extended(t + step, z).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let probe_projection = project(probe, foot)?;
let probe_distance = probe.sub(probe_projection.point).dot(raw_normal(
&end_face.surface,
probe_projection.u,
probe_projection.v,
)?);
let derivative = (probe_distance - distance) / step;
if derivative.abs() <= 1e-14 {
return Err(KernelRefusal::non_convergence(
KernelStage::Refine,
"transverse_newton",
"blend: transverse Newton stalled",
));
}
t -= distance / derivative;
}
}
let point = rows.surface.evaluate_extended(t, z).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let projection = project(point, foot)?;
points3.push(Vec4::from_point(point, 1.0));
blend_uv.push(Vec4::from_point(Vec3::new(t, z, 0.0), 1.0));
if exact {
let (u, v, _) = crate::blend::planar_chart::extended_foot_from(&end_face.surface, point, [projection.u, projection.v])?;
if std::env::var("BREP_DEBUG_TRANSVERSE_FIT").is_ok() {
let du = end_face.surface.domain_u().unwrap_or([f64::NAN; 2]);
let dv = end_face.surface.domain_v().unwrap_or([f64::NAN; 2]);
eprintln!(
"transverse foot z {z:.4}: seeded ({:.6}, {:.6}) {:.3e} off; extension ({u:.6}, {v:.6}); end face {:?} kind {} domain u {du:?} v {dv:?}",
projection.u, projection.v, projection.distance, end_face.name,
end_face.surface.analytic().map(|_| "analytic").unwrap_or("nurbs")
);
}
end_uv.push([u, v]);
} else {
end_uv.push([projection.u, projection.v]);
}
parameters.push(z);
}
unwrap_seam_branch(&end_face.surface, &mut end_uv)?;
if exact {
let free3 = fit::interpolate_homogeneous(&points3, FIT_DEGREE, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let end_uv4: Vec<Vec4> = end_uv.iter().map(|uv| Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0)).collect();
let free_uv = fit::interpolate_homogeneous(&end_uv4, FIT_DEGREE, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let free_blend = fit::interpolate_homogeneous(&blend_uv, FIT_DEGREE, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let mut derivatives3 = [Vec3::default(); 2];
let mut derivatives_uv = [Vec3::default(); 2];
for (side, index) in [(0usize, 0usize), (1, sections)] {
let z = parameters[index];
let free_d3 = free3.derivatives(z, 1).or_refuse(KernelStage::Refine, "derivatives")?[1];
let free_duv = free_uv.derivatives(z, 1).or_refuse(KernelStage::Refine, "derivatives")?[1];
if !exact_ends[side] {
derivatives3[side] = free_d3;
derivatives_uv[side] = free_duv;
continue;
}
let wall = raw_normal(&rows.surface, blend_uv[index].x, z)?;
let [u, v] = end_uv[index];
let d = end_face.surface.derivatives_extended(u, v, 1).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let (su, sv) = (d[1][0], d[0][1]);
let end_normal = su.cross(sv).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let mut direction = wall.cross(end_normal).normalized().or_refuse(KernelStage::Refine, "normalized")?;
if direction.dot(free_d3) < 0.0 {
direction = direction.scale(-1.0);
}
let tangent = direction.scale(free_d3.dot(direction));
let (a11, a12, a22) = (su.dot(su), su.dot(sv), sv.dot(sv));
let (b1, b2) = (su.dot(tangent), sv.dot(tangent));
let det = a11 * a22 - a12 * a12;
if !(det.abs() > 1e-300) {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "transverse_exact_end", "blend: the end face's extension is singular at a transverse end"));
}
derivatives3[side] = tangent;
derivatives_uv[side] = Vec3::new((b1 * a22 - b2 * a12) / det, (a11 * b2 - a12 * b1) / det, 0.0);
if std::env::var("BREP_DEBUG_RULED_CHART").is_ok() {
let tilt = free_d3.normalized().map(|free| (1.0 - free.dot(direction)).max(0.0)).unwrap_or(f64::NAN);
eprintln!("transverse exact end {side}: the free fit's end tangent stood 1 - cos = {tilt:.3e} off the wall ∩ end-face direction");
}
}
let points: Vec<Vec3> = points3.iter().map(|p| p.point()).collect::<Result<_, _>>().or_refuse(KernelStage::Refine, "point")?;
let uv_points: Vec<Vec3> = end_uv.iter().map(|uv| Vec3::new(uv[0], uv[1], 0.0)).collect();
let blend_points: Vec<Vec3> = blend_uv.iter().map(|p| p.point()).collect::<Result<_, _>>().or_refuse(KernelStage::Refine, "point")?;
let blend_derivatives = [
free_blend.derivatives(parameters[0], 1).or_refuse(KernelStage::Refine, "derivatives")?[1],
free_blend.derivatives(parameters[sections], 1).or_refuse(KernelStage::Refine, "derivatives")?[1],
];
if let Ok(path) = std::env::var("BREP_DUMP_TRANSVERSE_ENDS") {
let row = |v: &Vec3| format!("[{:.17e},{:.17e},{:.17e}]", v.x, v.y, v.z);
let line = format!(
"{{\"points\":[{}],\"parameters\":[{}],\"start_derivative\":{},\"end_derivative\":{}}}\n",
points.iter().map(row).collect::<Vec<_>>().join(","),
parameters.iter().map(|t| format!("{t:.17e}")).collect::<Vec<_>>().join(","),
row(&derivatives3[0]),
row(&derivatives3[1]),
);
use std::io::Write as _;
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = file.write_all(line.as_bytes());
}
}
let refuse = |message: String| KernelRefusal::non_convergence(KernelStage::Refine, "transverse_exact_end", format!("blend: {message}"));
let shared = |points: &[Vec3], derivatives: [Vec3; 2]| {
crate::interpolate_curve_with_end_tangents(points, ¶meters, derivatives[0], derivatives[1]).map_err(refuse)
};
let curve = shared(&points, derivatives3)?;
let blend_pcurve = shared(&blend_points, blend_derivatives)?;
let end_pcurve = shared(&uv_points, derivatives_uv)?;
if std::env::var("BREP_DEBUG_TRANSVERSE_FIT").is_ok() {
let off = |fitted: &NurbsCurve| -> Result<(f64, f64), KernelRefusal> {
let (mut end_off, mut wall_off) = (0.0_f64, 0.0_f64);
for k in 0..sections * 8 {
let z = (k as f64 + 0.5) / (sections * 8) as f64;
let point = fitted.evaluate(z).or_refuse(KernelStage::Refine, "evaluate")?;
end_off = end_off.max(crate::blend::planar_chart::extended_foot(&end_face.surface, point)?.2);
wall_off = wall_off.max(crate::project_point_to_surface(&rows.surface, point).or_refuse(KernelStage::Refine, "project")?.distance);
}
Ok((end_off, wall_off))
};
let (free_end, free_wall) = off(&free3)?;
let (exact_end, exact_wall) = off(&curve)?;
eprintln!("transverse fit {exact_ends:?}: free fit off end face {free_end:.3e}, off wall {free_wall:.3e}; exact-end fit off end face {exact_end:.3e}, off wall {exact_wall:.3e}");
}
return Ok((curve, blend_pcurve, end_pcurve));
}
let end_uv: Vec<Vec4> = end_uv
.into_iter()
.map(|uv| Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0))
.collect();
let curve = fit::interpolate_homogeneous(&points3, FIT_DEGREE, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let blend_pcurve = fit::interpolate_homogeneous(&blend_uv, FIT_DEGREE, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
let end_pcurve = fit::interpolate_homogeneous(&end_uv, FIT_DEGREE, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?;
Ok((curve, blend_pcurve, end_pcurve))
}
fn unwrap_seam_branch(surface: &NurbsSurface, samples: &mut [[f64; 2]]) -> Result<(), KernelRefusal> {
let (closed_u, closed_v) = surface.closed_directions().unwrap_or((false, false));
if (!closed_u && !closed_v) || samples.len() < 2 {
return Ok(());
}
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 periods = [
if closed_u { u1 - u0 } else { 0.0 },
if closed_v { v1 - v0 } else { 0.0 },
];
let snap = |value: f64, reference: f64, period: f64| -> f64 {
if !(period > 0.0) {
return value;
}
let shifted = value - period * ((value - reference) / period).round();
if (shifted - reference).abs() < (value - reference).abs() {
shifted
} else {
value
}
};
let middle = samples.len() / 2;
for index in (0..middle).rev() {
for axis in 0..2 {
samples[index][axis] = snap(samples[index][axis], samples[index + 1][axis], periods[axis]);
}
}
for index in middle + 1..samples.len() {
for axis in 0..2 {
samples[index][axis] = snap(samples[index][axis], samples[index - 1][axis], periods[axis]);
}
}
Ok(())
}
pub(in crate::blend) fn trim_parameter_at_point(
surface: &crate::NurbsSurface,
pcurve: &NurbsCurve,
seed: f64,
target: Vec3,
band: f64,
) -> Result<f64, KernelRefusal> {
let [lo, hi] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let at = |q: f64| -> Result<(Vec3, Vec3), KernelRefusal> {
let d = pcurve.derivatives(q, 1).or_refuse(KernelStage::Refine, "derivatives")?;
let (point, su, sv) = surface.deriv1_extended(d[0].x, d[0].y)
.or_refuse(KernelStage::Refine, "deriv1_extended")?;
Ok((point, su.scale(d[1].x).add(sv.scale(d[1].y))))
};
if !band.is_finite() || band <= 0.0 || !seed.is_finite() {
return Err(KernelRefusal::unsupported(KernelStage::Sew, "trim_pcurve_crossing",
"blend: boundary trim requires a finite seed and positive finite consistency bar"));
}
let (_, initial_tangent) = at(seed)?;
let precision = (1e-11 * (1.0 + initial_tangent.length() * (hi - lo))).min(band * 0.01);
let mut seeds = vec![seed];
let mut breaks = vec![lo, hi];
breaks.extend(pcurve.knots.iter().copied().filter(|q| *q > lo && *q < hi));
breaks.sort_by(f64::total_cmp);
breaks.dedup();
for span in breaks.windows(2) {
for i in 0..=4 { seeds.push(span[0] + (span[1] - span[0]) * i as f64 / 4.0); }
}
let mut best = (f64::INFINITY, seed);
let mut visits = Vec::new();
for (index, mut q) in seeds.into_iter().enumerate() {
let mut local_best = (f64::INFINITY, q);
for _ in 0..32 {
let (point, tangent) = at(q)?;
let difference = point.sub(target);
let error = difference.length();
if error < local_best.0 { local_best = (error, q); }
if error <= precision { break; }
let speed2 = tangent.dot(tangent);
if speed2 <= 1e-300 { break; }
let step = difference.dot(tangent) / speed2;
let mut next = q;
for j in 0..16 {
let probe = (q - step * 0.5_f64.powi(j)).clamp(lo, hi);
if at(probe)?.0.sub(target).length() < error { next = probe; break; }
}
if next == q { break; }
q = next;
}
if index == 0 { best = local_best; }
if local_best.0.is_finite() && local_best.0 <= band {
visits.push(local_best.1);
}
}
if !best.0.is_finite() || best.0 > band {
return Err(KernelRefusal::unsupported(KernelStage::Sew, "trim_pcurve_crossing",
format!("blend: boundary trim cannot reach its new vertex on its carrier (miss {:.3e}, bar {band:.3e})", best.0)));
}
for q in visits {
let a = q.min(best.1);
let b = q.max(best.1);
let mut intervals = vec![a];
intervals.extend(breaks.iter().copied().filter(|t| *t > a && *t < b));
intervals.push(b);
for span in intervals.windows(2) {
for i in 1..4 {
let between = span[0] + (span[1] - span[0]) * i as f64 / 4.0;
let miss = at(between)?.0.sub(target).length();
if !miss.is_finite() || miss > band {
return Err(KernelRefusal::unsupported(KernelStage::Sew, "trim_pcurve_ambiguous",
"blend: boundary trim has separate physical crossing visits; traversal correspondence is ambiguous"));
}
}
}
}
Ok(best.1)
}
pub(in crate::blend) fn trim_edge_at(
solid: &mut BrepSolid,
edge_id: u64,
parameter: f64,
old_vertex: u64,
new_vertex: u64,
) -> Result<(), KernelRefusal> {
let (old_t0, old_t1, trims_start) = {
let edge = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"trim_edge",
"blend: edge to trim is missing",
)
})?;
(edge.t0, edge.t1, edge.start_vertex_id == old_vertex)
};
let target = solid.vertices.iter().find(|vertex| vertex.id == new_vertex)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "trim_vertex", "blend: new trim vertex is missing"))?.point;
let pcurve_band = crate::KernelTolerances::for_solid(solid, 1e-7).pcurve_consistency;
let fraction = (parameter - old_t0) / (old_t1 - old_t0);
let interior = (1e-9..=1.0 - 1e-9).contains(&fraction);
let extends = if trims_start {
fraction < 1e-9
} else {
fraction > 1.0 - 1e-9
};
if !interior && !extends {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"trim_degenerate",
"blend: end trim degenerates the boundary edge",
));
}
if extends {
let edge = solid.edges.iter().find(|edge| edge.id == edge_id).unwrap();
let linear = edge.curve.degree == 1
&& edge.curve.control_points.len() == 2
&& (edge.curve.control_points[0].w - edge.curve.control_points[1].w).abs() <= 1e-12;
let curved = !linear || solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == edge_id && coedge.pcurve.degree != 1);
if curved {
return extend_edge_on_carriers(solid, edge_id, parameter, old_vertex, new_vertex);
}
}
for shell in &mut solid.shells {
for face in &mut shell.faces {
for loop_record in &mut face.loops {
for coedge in &mut loop_record.coedges {
if coedge.edge_id != edge_id {
continue;
}
let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let split_q = if coedge.forward {
q0 + (q1 - q0) * fraction
} else {
q1 - (q1 - q0) * fraction
};
if interior {
let split_q = trim_parameter_at_point(&face.surface, &coedge.pcurve, split_q, target, pcurve_band)?;
let keep_upper = trims_start == coedge.forward;
let (low, high) = coedge.pcurve.split(split_q).or_refuse(KernelStage::Refine, "split")?;
coedge.pcurve = if keep_upper { high } else { low };
} else {
if coedge.pcurve.degree != 1 {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"extend_curved_pcurve",
"blend: cannot extend a curved boundary pcurve",
));
}
let a = coedge.pcurve.evaluate(q0).or_refuse(KernelStage::Refine, "evaluate")?;
let b = coedge.pcurve.evaluate(q1).or_refuse(KernelStage::Refine, "evaluate")?;
let direction = b.sub(a);
let extended_fraction = (split_q - q0) / (q1 - q0);
let target = a.add(direction.scale(extended_fraction));
let controls = &mut coedge.pcurve.control_points;
let end_index = if (trims_start == coedge.forward) == false {
controls.len() - 1
} else {
0
};
let w = controls[end_index].w;
controls[end_index] = crate::Vec4::from_point(target, w);
}
}
}
}
}
let edge = solid
.edges
.iter_mut()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| {
KernelRefusal::internal(KernelStage::Sew, "trim_edge", "blend: edge to trim is missing")
})?;
if extends {
let t0 = if trims_start { parameter } else { old_t0 };
let t1 = if trims_start { old_t1 } else { parameter };
let a = edge.curve.evaluate_extended(t0).or_refuse(KernelStage::Refine, "extend_line")?;
let b = edge.curve.evaluate_extended(t1).or_refuse(KernelStage::Refine, "extend_line")?;
edge.curve = NurbsCurve::new(1, vec![t0, t0, t1, t1],
vec![Vec4::from_point(a, 1.0), Vec4::from_point(b, 1.0)])
.or_refuse(KernelStage::Refine, "extend_line")?;
}
if trims_start {
edge.t0 = parameter;
edge.start_vertex_id = new_vertex;
} else {
edge.t1 = parameter;
edge.end_vertex_id = new_vertex;
}
Ok(())
}
const EXTENSION_REFIT_SAMPLES: usize = 96;
const EXTENSION_SAMPLES: usize = 8;
fn extend_edge_on_carriers(
solid: &mut BrepSolid,
edge_id: u64,
parameter: f64,
old_vertex: u64,
new_vertex: u64,
) -> Result<(), KernelRefusal> {
let edge = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"extend_edge",
"blend: edge to extend is missing",
)
})?
.clone();
let trims_start = edge.start_vertex_id == old_vertex;
let target = solid
.vertices
.iter()
.find(|vertex| vertex.id == new_vertex)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"extend_vertex",
"blend: the extended boundary's new vertex is missing",
)
})?
.point;
let mut uses: Vec<(u64, CoedgeRecord)> = Vec::new();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
if coedge.edge_id == edge_id {
uses.push((face.id, coedge.clone()));
}
}
}
let [(face_a, coedge_a), (face_b, coedge_b)] = <[(u64, CoedgeRecord); 2]>::try_from(uses)
.map_err(|uses| {
KernelRefusal::internal(
KernelStage::Sew,
"extend_uses",
format!(
"blend: cannot extend boundary edge {edge_id} along its carriers — it is used \
{} times, not twice",
uses.len()
),
)
})?;
let surface_of = |face_id: u64| {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.map(|face| face.surface.clone())
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"extend_face",
"blend: a face of the extended boundary is missing",
)
})
};
let (surface_a, surface_b) = (surface_of(face_a)?, surface_of(face_b)?);
let (t0, t1) = (edge.t0, edge.t1);
let uv_at = |coedge: &CoedgeRecord, t: f64| -> Result<[f64; 2], KernelRefusal> {
let point = edge_uv_on_face(coedge, &edge, t)?;
Ok(point)
};
let end_t = if trims_start { t0 } else { t1 };
let end_point = edge.curve.evaluate(end_t).or_refuse(KernelStage::Refine, "evaluate")?;
let chord = target.sub(end_point);
let reach = chord.length();
let direction = chord
.normalized()
.map_err(|_| {
KernelRefusal::internal(
KernelStage::Sew,
"extend_length",
"blend: the boundary extension has no length",
)
})?;
let scale = target.length().max(end_point.length()).max(1.0);
let tolerance = 1e-11 * (1.0 + scale);
let mut seed = {
let a = uv_at(&coedge_a, end_t)?;
let b = uv_at(&coedge_b, end_t)?;
[a[0], a[1], b[0], b[1]]
};
let mut extension: Vec<(f64, Vec3, [f64; 4])> = Vec::with_capacity(EXTENSION_SAMPLES);
for step in 1..=EXTENSION_SAMPLES {
let fraction = step as f64 / EXTENSION_SAMPLES as f64;
let level = reach * fraction;
let mut x = seed;
let mut solved = None;
for _ in 0..NEWTON_ITERATIONS {
let da = surface_a.derivatives_extended(x[0], x[1], 1).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let db = surface_b.derivatives_extended(x[2], x[3], 1).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let mismatch = da[0][0].sub(db[0][0]);
let plane = da[0][0].sub(end_point).dot(direction) - level;
if mismatch.length() <= tolerance && plane.abs() <= tolerance {
solved = Some(da[0][0]);
break;
}
let matrix = [
[da[1][0].x, da[0][1].x, -db[1][0].x, -db[0][1].x],
[da[1][0].y, da[0][1].y, -db[1][0].y, -db[0][1].y],
[da[1][0].z, da[0][1].z, -db[1][0].z, -db[0][1].z],
[da[1][0].dot(direction), da[0][1].dot(direction), 0.0, 0.0],
];
let delta = fit::solve_small::<4>(
matrix,
[mismatch.x, mismatch.y, mismatch.z, plane],
4,
)
.map_err(|error| {
KernelRefusal::ill_posed(
KernelStage::Refine,
"extend_tangent_carriers",
format!(
"blend: the carriers of boundary edge {edge_id} do not cross transversally \
where it must extend ({error})"
),
)
})?;
for (value, correction) in x.iter_mut().zip(delta) {
*value -= correction;
}
}
let point = solved.ok_or_else(|| {
KernelRefusal::non_convergence(
KernelStage::Refine,
"extend_march",
format!(
"blend: the carriers' intersection could not be followed along boundary edge \
{edge_id} to the blend"
),
)
})?;
let t = end_t + (parameter - end_t) * fraction;
extension.push((t, point, x));
seed = x;
}
let arrival = extension
.last()
.map(|(_, point, _)| point.sub(target).length())
.unwrap_or(f64::INFINITY);
let bar = crate::KernelTolerances::for_solid(solid, 1e-7).pcurve_consistency;
if arrival > bar {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"extend_arrival",
format!(
"blend: boundary edge {edge_id} extended along its carriers arrives {arrival:.3e} \
from the blend's rim"
),
));
}
let mut samples: Vec<(f64, Vec3, [f64; 2], [f64; 2])> =
Vec::with_capacity(EXTENSION_REFIT_SAMPLES + EXTENSION_SAMPLES + 1);
for index in 0..=EXTENSION_REFIT_SAMPLES {
let t = t0 + (t1 - t0) * index as f64 / EXTENSION_REFIT_SAMPLES as f64;
samples.push((
t,
edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?,
uv_at(&coedge_a, t)?,
uv_at(&coedge_b, t)?,
));
}
for (t, point, x) in extension {
samples.push((t, point, [x[0], x[1]], [x[2], x[3]]));
}
samples.sort_by(|a, b| a.0.total_cmp(&b.0));
let (new_t0, new_t1) = if trims_start {
(parameter, t1)
} else {
(t0, parameter)
};
let parameters: Vec<f64> = samples.iter().map(|sample| sample.0).collect();
if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"extend_direction",
format!(
"blend: boundary edge {edge_id} extends in the wrong direction"
),
));
}
let curve = fit::interpolate_curve(
&samples.iter().map(|sample| sample.1).collect::<Vec<_>>(),
3,
¶meters,
).or_refuse(KernelStage::Refine, "interpolate_curve")?;
let refit = |coedge: &CoedgeRecord, side: usize| -> Result<NurbsCurve, KernelRefusal> {
let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let mut pairs: Vec<(f64, Vec3)> = samples
.iter()
.map(|sample| {
let fraction = (sample.0 - new_t0) / (new_t1 - new_t0);
let q = if coedge.forward {
q0 + (q1 - q0) * fraction
} else {
q1 - (q1 - q0) * fraction
};
let uv = if side == 0 { sample.2 } else { sample.3 };
(q, Vec3::new(uv[0], uv[1], 0.0))
})
.collect();
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
fit::interpolate_curve(
&pairs.iter().map(|pair| pair.1).collect::<Vec<_>>(),
3,
&pairs.iter().map(|pair| pair.0).collect::<Vec<_>>(),
)
.or_refuse(KernelStage::Sew, "interpolate_curve")
};
let pcurve_a = refit(&coedge_a, 0)?;
let pcurve_b = refit(&coedge_b, 1)?;
for shell in &mut solid.shells {
for face in &mut shell.faces {
for loop_record in &mut face.loops {
for coedge in &mut loop_record.coedges {
if coedge.id == coedge_a.id {
coedge.pcurve = pcurve_a.clone();
} else if coedge.id == coedge_b.id {
coedge.pcurve = pcurve_b.clone();
}
}
}
}
}
let record = solid
.edges
.iter_mut()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"extend_edge",
"blend: edge to extend is missing",
)
})?;
record.curve = curve;
record.t0 = new_t0;
record.t1 = new_t1;
if trims_start {
record.start_vertex_id = new_vertex;
} else {
record.end_vertex_id = new_vertex;
}
Ok(())
}