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,
)
.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>,
}
#[allow(clippy::too_many_arguments)]
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,
) -> 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 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);
for index in (0..mid_index).rev() {
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 {
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;
}
}
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 (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)
}
};
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))
});
{
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 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 row = (1..dense)
.map(|step| {
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()
};
crate::blend::stations::with_newton_context(context, || continue_station(&solve_centered, &hop, &left, t, 0)).ok().and_then(|node| node.center)
})
.collect();
dense_centres.centres.push(row);
}
}
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 mut ts: Vec<f64> = (0..=station_count)
.map(|index| edge.t0 - overshoot_before + marched * 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> {
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);
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,
})
}
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,
})
}
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]>) -> Vec<usize> {
let Some(indices) = vertex_indices else {
return Vec::new();
};
let mut breaks: Vec<usize> = indices
.into_iter()
.filter(|&index| index > 0 && index + 1 < count)
.collect();
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,
}))
}
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(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 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 (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(_) => support_crossing_uv(
solid,
&rows.cr,
&rows.cr_pcurve,
first_boundary,
first_boundary_coedge,
at_start,
)?,
};
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(_) => support_crossing_uv(
solid,
&rows.cs,
&rows.cs_pcurve,
second_boundary,
second_boundary_coedge,
at_start,
)?,
};
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 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 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 => transverse_curve(
rows,
end_record,
clamp01(cr_parameter),
clamp01(cs_parameter),
)?,
};
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,
) -> 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 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 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 gap = on_support.sub(on_boundary).length();
let historical = 1e-6 * scale;
let band = crate::KernelTolerances::for_solid(solid, 1e-7).pcurve_consistency;
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, gap > historical))
}
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> {
const SECTIONS: usize = 16;
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));
end_uv.push([projection.u, projection.v]);
parameters.push(z);
}
unwrap_seam_branch(&end_face.surface, &mut end_uv)?;
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(())
}