use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, VertexRecord};
use crate::{NurbsCurve, Vec3};
use super::collapse::{check_collapse_closure, collapse_full_width};
use super::corner::*;
use super::edge::*;
use super::miter::*;
use super::restrict::{restrict_third_faces, StripeFaces};
use super::stations::*;
#[path = "network/cap.rs"]
mod cap;
struct Stripe<'a> {
edge: &'a EdgeRecord,
first: BlendMate<'a>,
second: BlendMate<'a>,
rows: FittedRows,
name: Option<String>,
poles: [bool; 2],
measure: StripeMeasure,
law: super::StripeRadius,
end_radius: [f64; 2],
}
struct StripeRung {
rows: FittedRows,
parameters: Vec<f64>,
stations: Vec<Station>,
dense: OpenDenseCentres,
reading: OpenRungReading,
}
struct StripeMeasure {
parameters: Vec<f64>,
stations: Vec<Station>,
dense: OpenDenseCentres,
ladder_accepted: bool,
ladder_reading: OpenRungReading,
unmet: Option<crate::BudgetReason>,
request_rungs: usize,
stationary_end: bool,
wall_check: bool,
exact_lane: bool,
}
impl Stripe<'_> {
fn walk_first_to_second(&self, at_start: bool) -> bool {
let blend_cr_forward = !self.first.coedge.forward;
if at_start {
!blend_cr_forward
} else {
blend_cr_forward
}
}
fn at_start(&self, vertex: u64) -> bool {
self.edge.start_vertex_id == vertex
}
}
struct Corner {
center: Vec3,
outward: bool,
tangency: Vec<(u64, Vec3, u64)>,
radius: f64,
}
impl Corner {
fn tangency_on(&self, face_id: u64) -> Result<(Vec3, u64), KernelRefusal> {
self.tangency
.iter()
.find(|(id, _, _)| *id == face_id)
.map(|(_, point, vertex)| (*point, *vertex))
.ok_or_else(|| {
format!("blend network: the corner ball has no tangency on face {face_id}")
}).or_refuse(KernelStage::Refine, "ok_or_else")
}
}
enum VertexKind<'a> {
Star { corner: Corner },
RibbonStar { corner: Corner },
Miter {
corner: Corner,
shared: &'a FaceRecord,
sharp: &'a EdgeRecord,
},
Flush { join: Option<FlushJoin> },
Reentrant {
cap: &'a FaceRecord,
sharp: &'a EdgeRecord,
},
MixedStar(MixedStar),
}
#[derive(Clone, Copy, Debug)]
struct MixedStar {
radius: f64,
cap: u64,
wall_a: u64,
wall_b: u64,
q: usize,
a: usize,
b: usize,
v_a: Vec3,
v_b: Vec3,
w_a: Vec3,
w_b: Vec3,
q_v: Vec3,
q_w: Vec3,
axis: Vec3,
c_a: Vec3,
c_b: Vec3,
}
const MIXED_STAR_DEFAULT: bool = true;
thread_local! {
static MIXED_STAR_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(crate) fn mixed_star_on() -> bool {
if let Some(forced) = MIXED_STAR_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_MIXED_STAR").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => MIXED_STAR_DEFAULT,
}
}
fn planar_outward_normal(face: &FaceRecord) -> Option<Vec3> {
let Some(crate::AnalyticSurface::Plane { u_dir, v_dir, .. }) = face.surface.analytic() else {
return None;
};
let normal = u_dir.cross(*v_dir).normalized().ok()?;
Some(if face.same_sense { normal } else { normal.scale(-1.0) })
}
fn detect_mixed_star(
mates: &[(&EdgeRecord, BlendMate, BlendMate)],
stripe_indices: &[usize],
point: Vec3,
radius: f64,
) -> Option<MixedStar> {
if stripe_indices.len() != 3 || !(radius > 0.0) {
return None;
}
let convex: Vec<(usize, bool)> = stripe_indices
.iter()
.map(|&index| {
let (edge, first, second) = &mates[index];
edge_is_convex(edge, first, second).ok().map(|convex| (index, convex))
})
.collect::<Option<Vec<_>>>()?;
let concave: Vec<usize> = convex.iter().filter(|(_, c)| !*c).map(|(i, _)| *i).collect();
let rims: Vec<usize> = convex.iter().filter(|(_, c)| *c).map(|(i, _)| *i).collect();
let ([q], [r0, r1]) = (concave.as_slice(), rims.as_slice()) else {
return None;
};
let faces_of = |index: usize| [mates[index].1.face, mates[index].2.face];
let [q0, q1] = faces_of(*q);
let wall_of = |index: usize| -> Option<(&FaceRecord, &FaceRecord)> {
let [f0, f1] = faces_of(index);
match (f0.id == q0.id || f0.id == q1.id, f1.id == q0.id || f1.id == q1.id) {
(true, false) => Some((f0, f1)),
(false, true) => Some((f1, f0)),
_ => None,
}
};
let (wall_a, cap_a) = wall_of(*r0)?;
let (wall_b, cap_b) = wall_of(*r1)?;
if cap_a.id != cap_b.id || wall_a.id == wall_b.id {
return None;
}
let (na, nb, nc) = (planar_outward_normal(wall_a)?, planar_outward_normal(wall_b)?, planar_outward_normal(cap_a)?);
let k = na.dot(nb);
if na.dot(nc).abs() > 1e-9 || nb.dot(nc).abs() > 1e-9 || k <= -1.0 + 1e-6 {
return None;
}
for face in [wall_a, wall_b, cap_a] {
let Some(crate::AnalyticSurface::Plane { origin, .. }) = face.surface.analytic() else {
return None;
};
let normal = planar_outward_normal(face)?;
if normal.dot(point.sub(*origin)).abs() > 1e-7 * (1.0 + radius) {
return None;
}
}
let r = radius;
let q_w = point.add(na.add(nb).scale(r / (1.0 + k)));
let q_v = q_w.sub(nc.scale(r));
Some(MixedStar {
radius: r,
cap: cap_a.id,
wall_a: wall_a.id,
wall_b: wall_b.id,
q: *q,
a: *r0,
b: *r1,
v_a: q_v.sub(na.scale(r)),
v_b: q_v.sub(nb.scale(r)),
w_a: q_w.sub(na.scale(2.0 * r)),
w_b: q_w.sub(nb.scale(2.0 * r)),
q_v,
q_w,
axis: nc.scale(-1.0),
c_a: q_v.sub(na.scale(2.0 * r)),
c_b: q_v.sub(nb.scale(2.0 * r)),
})
}
pub(super) const OVERSHOOTS: [f64; 4] = [0.08, 0.16, 0.28, 0.45];
fn mate_at_radius<'a>(mate: &BlendMate<'a>, radius: f64) -> BlendMate<'a> {
BlendMate {
face: mate.face,
coedge: mate.coedge,
loop_index: mate.loop_index,
rho: mate.rho.signum() * radius,
}
}
pub(super) fn tangent_away_from(edge: &EdgeRecord, vertex: u64) -> Result<Vec3, KernelRefusal> {
let (t, sign) = if edge.start_vertex_id == vertex {
(edge.t0, 1.0)
} else {
(edge.t1, -1.0)
};
edge.curve
.unit_tangent(t, edge.t0, edge.t1)
.map(|tangent| tangent.scale(sign))
.map_err(|error| format!("blend network: edge {}: {error}", edge.id)).or_refuse(KernelStage::Refine, "unit_tangent")
}
fn stationary_at_vertex(edge: &EdgeRecord, vertex: u64) -> Result<bool, KernelRefusal> {
let t = if edge.start_vertex_id == vertex {
edge.t0
} else {
edge.t1
};
Ok(edge.curve.derivatives(t, 1).or_refuse(KernelStage::Refine, "derivatives")?[1].normalized().is_err())
}
pub(super) fn stripe_mates<'a>(
solid: &'a BrepSolid,
edge_ids: &[u64],
radius: f64,
) -> Result<Vec<(&'a EdgeRecord, BlendMate<'a>, BlendMate<'a>)>, KernelRefusal> {
let laws = vec![super::StripeRadius::Constant(radius); edge_ids.len()];
stripe_mates_laws(solid, edge_ids, &laws)
}
fn stripe_mates_laws<'a>(
solid: &'a BrepSolid,
edge_ids: &[u64],
laws: &[super::StripeRadius],
) -> Result<Vec<(&'a EdgeRecord, BlendMate<'a>, BlendMate<'a>)>, KernelRefusal> {
let mut mates: Vec<(&EdgeRecord, BlendMate, BlendMate)> = Vec::with_capacity(edge_ids.len());
for (&edge_id, law) in edge_ids.iter().zip(laws) {
let radius = law.at_fraction(0.5);
let edge = solid
.edges
.iter()
.find(|candidate| candidate.id == edge_id)
.ok_or_else(|| format!("blend network: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
if edge.start_vertex_id == edge.end_vertex_id {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "closed_edge_corner", format!(
"blend network: edge {edge_id} is closed and has no corner to share"
)));
}
let (first_face, first_loop, first_coedge) = locate_mate(solid, edge_id, None)?;
let (second_face, second_loop, second_coedge) =
locate_mate(solid, edge_id, Some((first_face.id, first_loop)))?;
let (rho1, rho2) = signed_radii(
edge,
first_face,
first_coedge,
second_face,
second_coedge,
radius,
)?;
mates.push((
edge,
BlendMate {
face: first_face,
coedge: first_coedge,
loop_index: first_loop,
rho: rho1,
},
BlendMate {
face: second_face,
coedge: second_coedge,
loop_index: second_loop,
rho: rho2,
},
));
}
Ok(mates)
}
fn stripes_by_vertex(mates: &[(&EdgeRecord, BlendMate, BlendMate)]) -> Vec<(u64, Vec<usize>)> {
let mut vertex_stripes: Vec<(u64, Vec<usize>)> = Vec::new();
for (index, (edge, _, _)) in mates.iter().enumerate() {
for vertex in [edge.start_vertex_id, edge.end_vertex_id] {
match vertex_stripes.iter_mut().find(|(id, _)| *id == vertex) {
Some((_, list)) => list.push(index),
None => vertex_stripes.push((vertex, vec![index])),
}
}
}
vertex_stripes
}
pub(crate) struct MixedCorner {
pub(crate) vertex: u64,
pub(crate) point: Vec3,
pub(crate) convex: Vec<usize>,
pub(crate) concave: Vec<usize>,
}
pub(crate) fn mixed_convexity_corner(
solid: &BrepSolid,
edge_ids: &[u64],
radius: f64,
) -> Option<MixedCorner> {
mixed_convexity_corners(solid, edge_ids, radius).into_iter().next()
}
pub(crate) fn mixed_convexity_corners(
solid: &BrepSolid,
edge_ids: &[u64],
radius: f64,
) -> Vec<MixedCorner> {
let mut found = Vec::new();
let Ok(mates) = stripe_mates(solid, edge_ids, radius) else {
return found;
};
for (vertex_id, stripe_indices) in stripes_by_vertex(&mates) {
if stripe_indices.len() < 2 {
continue;
}
let mut faces: Vec<(u64, f64)> = Vec::new();
let mut mixed = false;
for &index in &stripe_indices {
for mate in [&mates[index].1, &mates[index].2] {
match faces.iter().find(|(id, _)| *id == mate.face.id) {
Some((_, rho)) => mixed |= (*rho - mate.rho).abs() > 1e-9 * (1.0 + radius),
None => faces.push((mate.face.id, mate.rho)),
}
}
}
if !mixed {
continue;
}
let Some(point) = solid
.vertices
.iter()
.find(|vertex| vertex.id == vertex_id)
.map(|vertex| vertex.point)
else {
continue;
};
let mut convex = Vec::new();
let mut concave = Vec::new();
for &index in &stripe_indices {
let (edge, first, second) = &mates[index];
match edge_is_convex(edge, first, second) {
Ok(true) => convex.push(index),
Ok(false) => concave.push(index),
Err(_) => {}
}
}
if concave.len() < 2 || convex.is_empty() {
continue;
}
found.push(MixedCorner {
vertex: vertex_id,
point,
convex,
concave,
});
}
found
}
pub(crate) const REENTRANT_CONCAVE_WHAT: &str = "reentrant_concave";
pub(crate) fn is_reentrant_concave(refusal: &KernelRefusal) -> bool {
matches!(&refusal.class, RefusalClass::UnsupportedGeometry { what } if what == REENTRANT_CONCAVE_WHAT)
}
const CONCAVE_MITER_DEFAULT: bool = true;
thread_local! {
static CONCAVE_MITER_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(crate) fn concave_miter_on() -> bool {
if let Some(forced) = CONCAVE_MITER_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_CONCAVE_MITER").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => CONCAVE_MITER_DEFAULT,
}
}
const CREASED_SEAM_DEFAULT: bool = true;
thread_local! {
static CREASED_SEAM_FOR_TEST: std::cell::Cell<Option<bool>> = const { std::cell::Cell::new(None) };
}
pub(crate) fn creased_seam_on() -> bool {
if let Some(forced) = CREASED_SEAM_FOR_TEST.with(|forced| forced.get()) {
return forced;
}
match std::env::var("BREP_BLEND_CREASED_SEAM").as_deref() {
Ok("0") | Ok("off") => false,
Ok("1") | Ok("on") => true,
_ => CREASED_SEAM_DEFAULT,
}
}
pub(crate) fn selection_convexity(solid: &BrepSolid, edge_ids: &[u64], radius: f64) -> Option<Vec<bool>> {
let mates = stripe_mates(solid, edge_ids, radius).ok()?;
mates
.iter()
.map(|(edge, first, second)| edge_is_convex(edge, first, second).ok())
.collect()
}
pub(crate) struct DegenerateSetback {
pub(crate) edge: usize,
pub(crate) face_id: u64,
pub(crate) stops: [Vec3; 2],
pub(crate) strip: f64,
pub(crate) bar: f64,
pub(crate) crossed: bool,
}
pub(crate) fn degenerate_corner_setbacks(
solid: &BrepSolid,
edge_ids: &[u64],
radius: f64,
) -> Option<DegenerateSetback> {
let mates = stripe_mates(solid, edge_ids, radius).ok()?;
let scale = solid
.vertices
.iter()
.fold(0.0f64, |worst, vertex| worst.max(vertex.point.length()))
.max(1.0);
let bar = corner_station_bar(scale, radius);
let mut stars: Vec<(u64, Vec<(u64, Vec3)>)> = Vec::new();
for (vertex_id, stripe_indices) in stripes_by_vertex(&mates) {
if stripe_indices.len() < 3 {
continue;
}
let point = solid
.vertices
.iter()
.find(|vertex| vertex.id == vertex_id)?
.point;
let mut faces: Vec<(&FaceRecord, f64)> = Vec::new();
let mut consistent = true;
for &index in &stripe_indices {
for mate in [&mates[index].1, &mates[index].2] {
match faces.iter().find(|(face, _)| face.id == mate.face.id) {
Some((_, rho)) => {
consistent &= (*rho - mate.rho).abs() <= 1e-9 * (1.0 + radius);
}
None => faces.push((mate.face, mate.rho)),
}
}
}
if !consistent || faces.len() != stripe_indices.len() {
continue;
}
let face_refs: Vec<&FaceRecord> = faces.iter().map(|(face, _)| *face).collect();
let rho_of = |id: u64| faces.iter().find(|(face, _)| face.id == id).map(|(_, rho)| *rho);
let Ok(prepared) = corner_faces(&face_refs, &rho_of, point) else {
continue;
};
let Ok(ball) = solve_corner_ball(&prepared, scale) else {
continue;
};
if ball.residual > 1e-7 * (1.0 + scale) {
continue;
}
stars.push((
vertex_id,
ball.contacts
.iter()
.map(|contact| (contact.face_id, contact.point))
.collect(),
));
}
let tangency = |vertex: u64, face_id: u64| -> Option<Vec3> {
stars
.iter()
.find(|(id, _)| *id == vertex)?
.1
.iter()
.find(|(id, _)| *id == face_id)
.map(|(_, point)| *point)
};
for (index, (edge, first, second)) in mates.iter().enumerate() {
if edge.start_vertex_id == edge.end_vertex_id {
continue;
}
let chord = vertex_point_of(solid, edge.end_vertex_id)
.ok()?
.sub(vertex_point_of(solid, edge.start_vertex_id).ok()?);
for mate in [first, second] {
let (Some(from), Some(to)) = (
tangency(edge.start_vertex_id, mate.face.id),
tangency(edge.end_vertex_id, mate.face.id),
) else {
continue;
};
let span = to.sub(from);
let strip = span.length();
let crossed = strip > bar && span.dot(chord) < 0.0;
if strip <= bar || crossed {
return Some(DegenerateSetback {
edge: index,
face_id: mate.face.id,
stops: [from, to],
strip,
bar,
crossed,
});
}
}
}
None
}
pub(crate) fn blend_star_network(
solid: &BrepSolid,
edge_ids: &[u64],
radius: f64,
chamfer: bool,
edge_names: &[Option<String>],
corner_name: &dyn Fn(&[usize]) -> Option<String>,
) -> Result<BrepSolid, KernelRefusal> {
blend_star_network_with_corner_policy(solid, edge_ids, radius, chamfer, edge_names, corner_name, true)
}
pub(crate) fn blend_star_network_with_corner_policy(
solid: &BrepSolid,
edge_ids: &[u64],
radius: f64,
chamfer: bool,
edge_names: &[Option<String>],
corner_name: &dyn Fn(&[usize]) -> Option<String>,
wrap_concave: bool,
) -> Result<BrepSolid, KernelRefusal> {
let laws = vec![super::StripeRadius::Constant(radius); edge_ids.len()];
blend_star_network_laws(solid, edge_ids, &laws, chamfer, edge_names, corner_name, wrap_concave)
}
pub(crate) fn blend_star_network_laws(
solid: &BrepSolid,
edge_ids: &[u64],
laws: &[super::StripeRadius],
chamfer: bool,
edge_names: &[Option<String>],
corner_name: &dyn Fn(&[usize]) -> Option<String>,
wrap_concave: bool,
) -> Result<BrepSolid, KernelRefusal> {
let mark = super::blend_notes_mark();
let result = blend_star_network_building(solid, edge_ids, laws, chamfer, edge_names, corner_name, wrap_concave);
if result.is_err() {
super::blend_notes_truncate(mark);
}
result
}
fn blend_star_network_building(
solid: &BrepSolid,
edge_ids: &[u64],
laws: &[super::StripeRadius],
chamfer: bool,
edge_names: &[Option<String>],
corner_name: &dyn Fn(&[usize]) -> Option<String>,
wrap_concave: bool,
) -> Result<BrepSolid, KernelRefusal> {
if edge_ids.is_empty() {
return Err(KernelRefusal::input(KernelStage::Collect, "empty_selection", "blend network: no edges selected"));
}
if laws.len() != edge_ids.len() || laws.iter().any(|law| !law.is_positive()) {
return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend network: radius must be positive"));
}
let radius = laws.iter().map(super::StripeRadius::largest).fold(0.0f64, f64::max);
let _network_timing = super::BlendTimer::start("timing.network_ms");
let analysis = super::BlendTimer::start("timing.network_analysis_ms");
let mates = stripe_mates_laws(solid, edge_ids, laws)?;
let vertex_stripes = stripes_by_vertex(&mates);
analysis.stop();
let corner_balls = super::BlendTimer::start("timing.corner_balls_ms");
let mut result = solid.clone();
let mut take_id = fresh_id_source(solid);
let scale = solid
.vertices
.iter()
.fold(0.0f64, |worst, vertex| worst.max(vertex.point.length()))
.max(1.0);
let mut corners: Vec<(u64, Vec<usize>, VertexKind)> = Vec::new();
let mut creased_vertices: Vec<u64> = Vec::new();
let mut vertex_radii: Vec<(u64, f64)> = Vec::new();
for (vertex_id, stripe_indices) in &vertex_stripes {
if stripe_indices.len() < 2 {
continue;
}
let point = solid
.vertices
.iter()
.find(|vertex| vertex.id == *vertex_id)
.ok_or_else(|| format!("blend network: vertex {vertex_id} missing")).or_refuse(KernelStage::Refine, "ok_or_else")?
.point;
if !chamfer && stripe_indices.len() == 3 && mixed_star_on() {
let ends: Vec<f64> = stripe_indices
.iter()
.map(|&index| laws[index].end(mates[index].0.start_vertex_id == *vertex_id))
.collect();
let constant = stripe_indices.iter().all(|&index| laws[index].constant().is_some());
if constant && ends.iter().all(|end| (end - ends[0]).abs() <= 1e-12 * (1.0 + ends[0])) {
if let Some(mixed) = detect_mixed_star(&mates, stripe_indices, point, ends[0]) {
vertex_radii.push((*vertex_id, ends[0]));
corners.push((*vertex_id, stripe_indices.clone(), VertexKind::MixedStar(mixed)));
continue;
}
}
}
let mut faces: Vec<(&FaceRecord, f64, usize)> = Vec::new();
let mut unequal: Option<(u64, f64, f64)> = None;
for &index in stripe_indices {
let end_radius = laws[index].end(mates[index].0.start_vertex_id == *vertex_id);
for mate in [&mates[index].1, &mates[index].2] {
let rho_end = mate.rho.signum() * end_radius;
match faces.iter_mut().find(|(face, _, _)| face.id == mate.face.id) {
Some((_, rho, uses)) => {
if rho.signum() != rho_end.signum() {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "mixed_convexity", format!(
"blend network: the stripes at vertex {vertex_id} disagree on \
which side of face {} the ball sits (mixed convexity)",
mate.face.id
)));
}
if (*rho - rho_end).abs() > 1e-9 * (1.0 + radius) {
if unequal.is_none() {
unequal = Some((mate.face.id, rho.abs(), end_radius));
}
if rho_end.abs() > rho.abs() {
*rho = rho_end;
}
}
*uses += 1;
}
None => faces.push((mate.face, rho_end, 1)),
}
}
}
let corner_radius = faces.iter().map(|(_, rho, _)| rho.abs()).fold(0.0f64, f64::max);
vertex_radii.push((*vertex_id, corner_radius));
let tapered_here = stripe_indices.iter().any(|&index| laws[index].constant().is_none());
let is_star = stripe_indices.len() >= 3;
let refuse_unequal = |why: &str| -> KernelRefusal {
let (face_id, first, second) = unequal.unwrap_or((0, 0.0, 0.0));
KernelRefusal::unsupported(KernelStage::Classify, "unequal_corner_radius", format!(
"blend network: the stripes at vertex {vertex_id} end at different radii \
on face {face_id} ({first:.6} and {second:.6}); {why}"
))
};
let mut ribbon_star = false;
if unequal.is_some() {
if is_star {
if !super::no_common_ball_star_on() {
return Err(refuse_unequal("an unequal-radius star has no common ball and its N-sided fill is not in this lane"));
}
if tapered_here {
return Err(refuse_unequal("an unequal-radius star at a varying radius is not in this lane"));
}
ribbon_star = true;
} else {
if !super::unequal_miter_on() {
return Err(refuse_unequal("an unequal-radius corner is not in this lane"));
}
if tapered_here {
return Err(refuse_unequal("an unequal-radius corner at a varying radius is not in this lane"));
}
}
}
if is_star && faces.len() != stripe_indices.len() {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "partial_corner_selection", format!(
"blend network: vertex {vertex_id} has {} selected edges around {} faces — \
not every edge at the corner is selected, so no ball seats there",
stripe_indices.len(),
faces.len()
)));
}
let mut miter: Option<(&FaceRecord, &EdgeRecord)> = None;
let mut creased = false;
if !is_star {
let [a, b] = [stripe_indices[0], stripe_indices[1]];
let tangent_a = tangent_away_from(mates[a].0, *vertex_id)?;
let tangent_b = tangent_away_from(mates[b].0, *vertex_id)?;
if tangent_a.dot(tangent_b) <= -(1.0 - 1e-6) {
if unequal.is_some() {
return Err(refuse_unequal("a flush join at two radii is a step, not a corner, and is not in this lane"));
}
if tapered_here {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "tapered_flush_join", format!(
"blend network: a tapered stripe runs through the smooth vertex {vertex_id}; \
a flush join at a varying radius is not in this lane"
)));
}
match flush_join(solid, &mates[a], &mates[b], *vertex_id, point, scale) {
Ok(join) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
match &join {
Some(join) => eprintln!(
"flush join at vertex {vertex_id} (edges {} and {}): ball centre ({:.6}, {:.6}, {:.6}); contacts {}",
mates[a].0.id,
mates[b].0.id,
join.center.x,
join.center.y,
join.center.z,
join.contacts
.iter()
.map(|contact| format!(
"faces {:?} at ({:.6}, {:.6}, {:.6}) seam {:?}",
contact.faces, contact.point.x, contact.point.y, contact.point.z, contact.seam
))
.collect::<Vec<_>>()
.join("; ")
),
None => eprintln!("flush join at vertex {vertex_id} (edges {} and {}): the vertex section (same two faces)", mates[a].0.id, mates[b].0.id),
}
}
corners.push((*vertex_id, stripe_indices.clone(), VertexKind::Flush { join }));
continue;
}
Err(refusal)
if creased_seam_on()
&& matches!(&refusal.class, crate::RefusalClass::UnsupportedGeometry { what } if what == "flush_join") =>
{
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("creased seam at vertex {vertex_id}: {refusal}; trying the miter lane");
}
creased = true;
creased_vertices.push(*vertex_id);
}
Err(refusal) => return Err(refusal),
}
}
if faces.len() != 3 {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "corner_face_count", format!(
"blend network: vertex {vertex_id} has two selected edges around {} faces \
(expected exactly three: one shared, one on each side)",
faces.len()
)));
}
let shared = faces
.iter()
.find(|(_, _, uses)| *uses == 2)
.map(|(face, _, _)| *face)
.ok_or_else(|| {
format!("blend network: the two edges at vertex {vertex_id} share no face")
}).or_refuse(KernelStage::Refine, "ok_or_else")?;
let others: Vec<&FaceRecord> = faces
.iter()
.filter(|(face, _, _)| face.id != shared.id)
.map(|(face, _, _)| *face)
.collect();
let (other_a, other_b) = (others[0], others[1]);
let sharp_a = boundary_edge_at_vertex(solid, other_a, *vertex_id, mates[a].0.id)
.or_else(|_| boundary_edge_at_vertex(solid, other_a, *vertex_id, mates[b].0.id))?;
let sharp_b = boundary_edge_at_vertex(solid, other_b, *vertex_id, mates[a].0.id)
.or_else(|_| boundary_edge_at_vertex(solid, other_b, *vertex_id, mates[b].0.id))?;
if sharp_a != sharp_b {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "corner_sharp_edges", format!(
"blend network: vertex {vertex_id} has more than one unselected edge \
between the side faces (found {sharp_a} and {sharp_b})"
)));
}
let sharp = solid
.edges
.iter()
.find(|edge| edge.id == sharp_a)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "sharp_edge", "blend network: the sharp edge vanished"))?;
if sharp.start_vertex_id == sharp.end_vertex_id {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "closed_sharp_edge", "blend network: the sharp edge at a miter is closed"));
}
let sharp_convex = sharp_edge_is_convex(solid, sharp, other_a, other_b, corner_radius)?;
let selected_convex = [a, b].iter().all(|&index| {
let (edge, first, second) = &mates[index];
edge_is_convex(edge, first, second).unwrap_or(false)
});
let selected_concave = [a, b].iter().all(|&index| {
let (edge, first, second) = &mates[index];
edge_is_convex(edge, first, second).is_ok_and(|convex| !convex)
});
let plane_normal = |face: &FaceRecord| match face.surface.analytic() {
Some(crate::AnalyticSurface::Plane { u_dir, v_dir, .. }) => u_dir.cross(*v_dir).normalized().ok(),
_ => None,
};
let perpendicular_cap = plane_normal(shared).zip(plane_normal(other_a))
.zip(plane_normal(other_b))
.is_some_and(|((cap, a), b)| cap.dot(a).abs() < 1e-6 && cap.dot(b).abs() < 1e-6);
if (!sharp_convex && selected_convex)
|| (wrap_concave && !chamfer && sharp_convex && selected_concave && perpendicular_cap) {
if unequal.is_some() {
return Err(refuse_unequal("the horn-torus closure at a re-entrant vertex needs one ball and is not in this lane at two radii"));
}
if tapered_here {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "tapered_reentrant", format!(
"blend network: a tapered stripe ends at the re-entrant vertex {vertex_id}; \
the horn-torus closure at a varying radius is not in this lane"
)));
}
corners.push((
*vertex_id,
stripe_indices.clone(),
VertexKind::Reentrant { cap: shared, sharp },
));
continue;
}
if !sharp_convex && !concave_miter_on() {
return Err(KernelRefusal::unsupported(KernelStage::Classify, REENTRANT_CONCAVE_WHAT, format!(
"blend network: vertex {vertex_id} at ({:.3}, {:.3}, {:.3}) is an inside corner: the selected \
concave edges {} and {} meet the unselected concave edge {}; the ball in the air is seated by \
all three faces and cannot wrap that edge, so the closure is the two pads' seam up to it (the \
mirror of the convex two-edge miter, not of the horn torus), which this lane does not build",
point.x, point.y, point.z, mates[a].0.id, mates[b].0.id, sharp.id
)));
}
miter = Some((shared, sharp));
}
let face_refs: Vec<&FaceRecord> = faces.iter().map(|(face, _, _)| *face).collect();
let rho_of = |id: u64| {
faces
.iter()
.find(|(face, _, _)| face.id == id)
.map(|(_, rho, _)| *rho)
};
let seed = match (creased, miter) {
(true, Some((_, sharp))) => point.add(tangent_away_from(sharp, *vertex_id)?.scale(corner_radius)),
_ => point,
};
let prepared = corner_faces(&face_refs, &rho_of, seed)?;
let ball = solve_corner_ball(&prepared, scale)?;
if creased && std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("creased seam corner ball at vertex {vertex_id}: centre {:?} residual {:.3e} contacts {:?}", ball.center, ball.residual, ball.contacts.iter().map(|c| (c.face_id, c.point)).collect::<Vec<_>>());
}
let bar = 1e-7 * (1.0 + scale);
if ball.residual > bar && !ribbon_star {
if is_star && super::no_common_ball_star_on() && !tapered_here {
ribbon_star = true;
} else {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "n_sided_corner", format!(
"blend network: the {} fillets at vertex {vertex_id} have no common inscribed \
ball (residual {:.3e} > {bar:.3e}); an N-sided corner fill is not implemented",
stripe_indices.len(),
ball.residual
)));
}
}
let mut inside = 0usize;
let mut in_air = 0usize;
for contact in &ball.contacts {
let face = face_refs
.iter()
.find(|face| face.id == contact.face_id)
.ok_or(KernelRefusal::unsupported(KernelStage::Refine, "ball_off_corner", "blend network: the ball touched a face that is not at the corner"))?;
let face_outward = if face.same_sense {
contact.normal
} else {
contact.normal.scale(-1.0)
};
let ball_inside_material = ball.center.sub(contact.point).dot(face_outward) < 0.0;
if ball_inside_material {
inside += 1;
} else {
in_air += 1;
}
if let Some((shared, _)) = miter.filter(|_| ball_inside_material) {
if contact.face_id != shared.id {
let class = crate::parameter_point_in_face(
face,
crate::Vec2 {
x: contact.uv[0],
y: contact.uv[1],
},
1e-6,
).or_refuse(KernelStage::Refine, "parameter_point_in_face")?;
if class != crate::PolygonClass::Inside
&& class != crate::PolygonClass::Boundary
{
return Err(KernelRefusal::unsupported(KernelStage::Classify, "reentrant_horn_torus", format!(
"blend network: vertex {vertex_id} is re-entrant — the ball \
touches face {} beyond its edge, so the two blends do not meet \
in a seam but wrap the concave edge (horn-torus closure, not in \
this lane)",
contact.face_id
)));
}
}
}
}
let outward = match (inside, in_air) {
(_, 0) => true,
(0, _) => false,
_ => {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "mixed_corner", format!(
"blend network: vertex {vertex_id} mixes convex and concave edges ({inside} \
faces with the ball inside, {in_air} with it in the air); a mixed corner \
is not closed by a sphere and this lane does not build it"
)))
}
};
if point.sub(ball.center).length() < corner_radius * (1.0 - 1e-6) {
return Err(KernelRefusal::internal(KernelStage::Refine, "ball_branch", format!(
"blend network: the ball solved at vertex {vertex_id} swallows the corner \
itself ({:.6} < {corner_radius}) — wrong branch",
point.sub(ball.center).length()
)));
}
let rail_rail_stripe = |index: usize, shared_id: u64| -> RailRailStripe<'_> {
let end_radius = laws[index].end(mates[index].0.start_vertex_id == *vertex_id);
let (_, first, second) = &mates[index];
let (on_shared, on_side) = if first.face.id == shared_id {
(first, second)
} else {
(second, first)
};
RailRailStripe {
side: on_side.face,
rho_shared: on_shared.rho.signum() * end_radius,
rho_side: on_side.rho.signum() * end_radius,
}
};
let rail_rail = match (unequal, miter) {
(Some(_), Some((shared, _))) => {
if !outward {
return Err(refuse_unequal("a concave unequal-radius corner is not in this lane"));
}
let described = [rail_rail_stripe(stripe_indices[0], shared.id), rail_rail_stripe(stripe_indices[1], shared.id)];
let solved = solve_rail_rail_point(shared, [&described[0], &described[1]], point, scale)?;
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"network unequal miter at vertex {vertex_id}: rail/rail point {:?} on face {} \
(uv {:?}, residual {:.3e}), centres {:?}",
solved.point, shared.id, solved.uv, solved.residual, solved.centers
);
}
Some(solved)
}
_ => None,
};
let star_points: Vec<(u64, RailRailPoint)> = if ribbon_star {
if chamfer {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "n_sided_corner", format!(
"blend network: the {} chamfers at vertex {vertex_id} have no common inscribed \
ball; the ribbon fill is a fillet's and a chamfer's facet is not in this lane",
stripe_indices.len()
)));
}
if !outward {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "n_sided_corner", format!(
"blend network: the {} fillets at vertex {vertex_id} have no common inscribed \
ball and the corner is concave; the ribbon fill is not in this lane there",
stripe_indices.len()
)));
}
let mut solved = Vec::with_capacity(faces.len());
for (face, _, _) in &faces {
let bordering: Vec<usize> = stripe_indices
.iter()
.copied()
.filter(|&index| mates[index].1.face.id == face.id || mates[index].2.face.id == face.id)
.collect();
if bordering.len() != 2 {
return Err(KernelRefusal::internal(KernelStage::Refine, "star_face_stripes", format!(
"blend network: face {} at vertex {vertex_id} borders {} selected stripes, not two",
face.id,
bordering.len()
)));
}
let described = [rail_rail_stripe(bordering[0], face.id), rail_rail_stripe(bordering[1], face.id)];
let point_on = solve_rail_rail_point(face, [&described[0], &described[1]], point, scale)?;
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"network ribbon star at vertex {vertex_id}: rail/rail point {:?} on face {} \
(residual {:.3e})",
point_on.point, face.id, point_on.residual
);
}
solved.push((face.id, point_on));
}
solved
} else {
Vec::new()
};
let mut tangency = Vec::with_capacity(ball.contacts.len());
for contact in &ball.contacts {
let needed = match miter {
Some((shared, _)) => contact.face_id == shared.id,
None => true,
};
let point = match (&rail_rail, miter) {
(Some(solved), Some((shared, _))) if contact.face_id == shared.id => solved.point,
_ => star_points
.iter()
.find(|(face_id, _)| *face_id == contact.face_id)
.map(|(_, solved)| solved.point)
.unwrap_or(contact.point),
};
let id = if needed {
let id = take_id();
result.vertices.push(VertexRecord { id, point });
id
} else {
0
};
tangency.push((contact.face_id, point, id));
}
let corner = Corner {
center: ball.center,
outward,
tangency,
radius: corner_radius,
};
let kind = match miter {
Some((shared, sharp)) => VertexKind::Miter {
corner,
shared,
sharp,
},
None if ribbon_star => VertexKind::RibbonStar { corner },
None => VertexKind::Star { corner },
};
corners.push((*vertex_id, stripe_indices.clone(), kind));
}
corner_balls.stop();
super::record_count("count.corners", corners.len() as u64);
let vertex_radius_of = |vertex: u64| -> Option<f64> {
vertex_radii.iter().find(|(id, _)| *id == vertex).map(|(_, radius)| *radius)
};
let mut stripes: Vec<Stripe> = Vec::with_capacity(mates.len());
super::record_count("count.stripes", mates.len() as u64);
for (index, (edge, first, second)) in mates.iter().enumerate() {
let mut ends = [edge.t0, edge.t1];
for (slot, vertex) in [edge.start_vertex_id, edge.end_vertex_id].into_iter().enumerate() {
if let Some((_, _, VertexKind::Flush { join: Some(join) })) =
corners.iter().find(|(id, _, _)| *id == vertex)
{
let before = ends[slot];
ends[slot] = section_parameter_through(edge, join.center, ends[slot], scale)
.map_err(|error| {
format!(
"blend network: edge {}'s section through the flush join's ball at \
vertex {vertex}: {error}",
edge.id
)
}).or_refuse(KernelStage::Refine, "map_err")?;
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"flush join at vertex {vertex}: edge {}'s end moves from t={before:.6} to the seam ball's section t={:.6} (edge t in [{:.6}, {:.6}]); ball centre {:?}",
edge.id, ends[slot], edge.t0, edge.t1, join.center
);
}
}
}
let law = &laws[index];
let end_radius = [law.end(true), law.end(false)];
let law_ends: [f64; 2] = {
let mut law_ends = ends;
if law.constant().is_none() {
for (slot, vertex) in [edge.start_vertex_id, edge.end_vertex_id].into_iter().enumerate() {
if let Some((_, _, VertexKind::Star { corner } | VertexKind::Miter { corner, .. })) =
corners.iter().find(|(id, _, _)| *id == vertex)
{
law_ends[slot] = section_parameter_through(edge, corner.center, law_ends[slot], scale)
.map_err(|error| {
format!(
"blend network: edge {}'s section through the corner ball at \
vertex {vertex}: {error}",
edge.id
)
}).or_refuse(KernelStage::Refine, "map_err")?;
}
}
}
law_ends
};
if law.constant().is_none() {
ends = law_ends;
}
let radius_at = |t: f64| -> f64 {
match law.constant() {
Some(radius) => radius,
None => law.at_fraction((t - law_ends[0]) / (law_ends[1] - law_ends[0])),
}
};
let wall_radius = law.constant();
let mut poles = [false, false];
for (slot, vertex) in [edge.start_vertex_id, edge.end_vertex_id].into_iter().enumerate() {
if corners.iter().any(|(id, _, _)| *id == vertex) {
continue;
}
let (first_end, second_end) = (mate_at_radius(first, end_radius[slot]), mate_at_radius(second, end_radius[slot]));
poles[slot] =
pole_end(edge, &first_end, &second_end, slot == 0, corner_station_bar(scale, end_radius[slot]))
.unwrap_or(false);
}
let bounded_ends = [edge.start_vertex_id, edge.end_vertex_id].map(|vertex| {
!corners.iter().any(|(id, _, _)| *id == vertex)
&& solid.edges.iter().any(|boundary| {
boundary.degenerate && boundary.start_vertex_id == vertex
})
});
let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let flush_reach = {
let span = (edge.t1 - edge.t0).abs();
[(edge.t0 - ends[0]) / span, (ends[1] - edge.t1) / span]
.into_iter()
.filter(|reach| reach.is_finite())
.fold(0.0f64, f64::max)
};
let ladder = concave_corner_ladder(edge, &corners, flush_reach);
let wall_check = !chamfer && wall_radius.is_some() && super::station_refinement_on();
let wall_bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
let accepts = |reading: &OpenRungReading| {
reading.rails_read()
&& reading.rails <= rail_bar
&& (!wall_check || reading.worst_wall().is_some_and(|wall| wall <= wall_bar))
};
let meets_model = |reading: &OpenRungReading| reading.worst_wall().is_some_and(|wall| wall <= model);
let mut chosen: Option<(FittedRows, Vec<f64>, StationRefinement, Vec<Station>, [usize; 2], OpenDenseCentres)> = None;
let mut last_error: Option<KernelRefusal> = None;
let mut station_count = super::stations::STATIONS;
let mut accepted: Option<StripeRung> = None;
let mut unaccepted: Option<StripeRung> = None;
let mut request_rungs = 0usize;
let mut unmet: Option<crate::BudgetReason> = None;
let extrusion = wall_radius.and_then(|_| extrusion_direction(edge, first, second));
let mut exact_lane = super::exact_lane_on() && extrusion.is_some();
let cornered_ends = [edge.start_vertex_id, edge.end_vertex_id]
.iter()
.all(|vertex| corners.iter().any(|(id, _, _)| id == vertex));
let mut revolution = if exact_lane || wall_radius.is_none() || !cornered_ends || !super::exact_revolution_on() {
None
} else {
revolution_lane_axis(edge, first, second)
};
let stationary_end = [edge.t0, edge.t1].into_iter().any(|t| {
edge.curve
.derivatives(t, 1)
.map_or(false, |derivatives| derivatives[1].normalized().is_err())
});
'rung: loop {
if accepted.is_some() {
request_rungs += 1;
}
chosen = None;
for &overshoot in &ladder {
let march_timing = super::BlendTimer::start("timing.march_ms");
let rung_measure: &'static str = match station_count {
64 => "timing.march.stations_64_ms",
128 => "timing.march.stations_128_ms",
256 => "timing.march.stations_256_ms",
_ => "timing.march.stations_other_ms",
};
let rung_timing = super::BlendTimer::start(rung_measure);
let probes_off = exact_lane || revolution.is_some();
let marched = march_open_stations_core(
edge, first, second, &radius_at, overshoot, ends, station_count, poles, bounded_ends,
if probes_off { None } else { super::station_refinement_on().then_some(rail_bar) },
if wall_check && !probes_off { OPEN_WALL_CENTRES } else { 0 },
!probes_off,
);
let march_ms = march_timing.stop();
rung_timing.stop();
match marched {
Ok((stations, vertex_indices, refinement, dense)) => {
let mut stations = stations;
let mut parameters = station_parameters(&stations);
let mut vertex_indices = vertex_indices;
let mut boundary_breaks: Vec<usize> = refinement.boundary_breaks.iter().flatten().copied().collect();
for &inserted in refinement.boundary_inserted[..refinement.boundary_inserted_count].iter().rev() {
if inserted == 0 || inserted + 1 >= stations.len() {
continue;
}
let with_previous = parameters[inserted] <= parameters[inserted - 1];
let with_next = parameters[inserted + 1] <= parameters[inserted];
if !(with_previous || with_next) {
continue;
}
stations.remove(inserted);
parameters = station_parameters(&stations);
for vertex in vertex_indices.iter_mut() {
if *vertex > inserted {
*vertex -= 1;
}
}
for break_index in boundary_breaks.iter_mut() {
if *break_index > inserted {
*break_index -= 1;
} else if *break_index == inserted {
*break_index = if with_previous { inserted - 1 } else { inserted };
}
}
super::record_count("count.march.boundary_stations_coincident", 1);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("open march: edge {}'s boundary station {inserted} coincides with its neighbour in the rows' chord parameter; dropped", edge.id);
}
}
boundary_breaks.sort_unstable();
boundary_breaks.dedup();
let fit_timing = super::BlendTimer::start("timing.rows_fit_ms");
let fitted = if let Some(axis) = revolution {
match exact_revolution_rows(&stations, vertex_indices, chamfer, axis, wall_radius.unwrap_or(radius), first, second) {
Ok(ExactRevolution { rows, parameters: exact_parameters, stations: exact_stations }) => {
parameters = exact_parameters;
stations = exact_stations;
Ok(rows)
}
Err(reason) => {
super::record_count("count.march.exact_revolution_declined", 1);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("open march: edge {}'s stations are not an exact revolution ({reason}); the ladder takes it", edge.id);
}
revolution = None;
continue 'rung;
}
}
} else {
fit_open_rows_breaking_at(&stations, ¶meters, chamfer, Some(vertex_indices), extrusion, &boundary_breaks).and_then(|rows| {
super::fold::check_rows_loop(&rows, &stations, ¶meters, Some(vertex_indices), radius, vec![first.face.id, second.face.id], scale)?;
Ok(rows)
})
};
let at_vertices = vertex_stations(¶meters, vertex_indices);
let fit_ms = fit_timing.stop();
if exact_lane && !fitted.as_ref().map_or(false, |rows| rows.exact_extrusion) {
super::record_count("count.march.exact_lane_declined", 1);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("open march: edge {}'s stations are not an exact extrusion; the ladder takes it", edge.id);
}
exact_lane = false;
continue 'rung;
}
if exact_lane {
super::record_count("count.march.exact_lane", 1);
}
if let Ok(rows) = &fitted {
super::record_count(
if rows.exact_extrusion {
"count.rows.exact_extrusion"
} else if rows.revolution_axis.is_some() {
"count.rows.exact_revolution"
} else {
"count.rows.fitted"
},
1,
);
}
if super::timing::enabled() && std::env::var_os("BREP_PROFILE").is_some() {
eprintln!(
"blend.stripe edge={} stations={station_count} overshoot={overshoot:.3} \
marched={} inserted={} march_ms={march_ms:.3} fit_ms={fit_ms:.3} exact={}",
edge.id,
parameters.len(),
refinement.inserted,
fitted.as_ref().map_or(false, |rows| rows.exact_extrusion)
);
}
match fitted {
Ok(mut rows) => {
rows.vertex_stations = at_vertices;
let settled_ends: [bool; 2] = std::array::from_fn(|slot| {
let vertex = [edge.start_vertex_id, edge.end_vertex_id][slot];
let at_start = slot == 0;
if poles[slot] {
return true;
}
if let Some((_, _, kind)) =
corners.iter().find(|(id, _, _)| *id == vertex)
{
return corner_rails_reach(
&rows,
first,
second,
kind,
corner_station_bar(scale, radius),
);
}
resolve_free_end(
solid, edge.id, first, second, &rows, vertex, at_start
).is_ok_and(|(end, reached)| {
reached && (!bounded_ends[slot]
|| [end.cr_parameter, end.cs_parameter].iter()
.all(|t| *t > RIM_MARGIN && *t < 1.0 - RIM_MARGIN))
})
});
let settled = settled_ends.iter().all(|reached| *reached);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"rung: edge {} overshoot {overshoot:.3} stations {} ends [{:.6}, {:.6}] marched t [{:.6}, {:.6}] vertex stations {:?} settled {settled_ends:?}",
edge.id,
parameters.len(),
ends[0],
ends[1],
edge.t0 - overshoot * (edge.t1 - edge.t0),
edge.t1 + overshoot * (edge.t1 - edge.t0),
vertex_indices,
);
}
let bounded_reached = (0..2)
.all(|slot| !bounded_ends[slot] || settled_ends[slot]);
if (chosen.is_none() || settled) && bounded_reached
{
chosen = Some((rows, parameters.clone(), refinement, stations, vertex_indices, dense));
}
if settled {
break;
}
}
Err(error) if super::fold::is_wall_fold(&error) => return Err(error),
Err(error) => last_error = Some(error),
}
}
Err(error) => last_error = Some(error),
}
}
let Some((rows, parameters, refinement, stations, vertex_indices, dense)) = chosen.take() else {
if accepted.is_some() {
unmet = Some(crate::BudgetReason::Incoherent);
break;
}
if bounded_ends.iter().any(|bounded| *bounded)
&& station_count < super::stations::MAX_OPEN_STATIONS
{
station_count *= 2;
continue;
}
return Err(last_error.unwrap_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"network_march",
format!("blend network: the march failed on edge {}", edge.id),
)
}));
};
let window = {
let (low, high) = (vertex_indices[0].min(vertex_indices[1]), vertex_indices[0].max(vertex_indices[1]));
shipped_window(¶meters, parameters[low], parameters[high])
};
let reading_timing = super::BlendTimer::start("timing.rung_reading_ms");
let rails_timing = super::BlendTimer::start("timing.rung_reading.rails_ms");
let (rails, rail_samples) =
rails_on_shipped_windows(&rows, ¶meters, &stations, [&first.face.surface, &second.face.surface], [window, window]);
rails_timing.stop();
let walls_timing = super::BlendTimer::start("timing.rung_reading.walls_ms");
let walls_why = match (wall_check, window) {
(true, Some(window)) if rows.revolution_axis.is_some() => exact_revolution_wall_readings(&rows, ¶meters, window, wall_radius.unwrap_or(radius)),
(true, Some(window)) if exact_lane => exact_extrusion_wall_readings(&rows, ¶meters, window, wall_radius.unwrap_or(radius)),
(true, Some(window)) => open_wall_readings_why(&rows, ¶meters, &stations, window, &dense, wall_radius.unwrap_or(radius)),
_ => Vec::new(),
};
walls_timing.stop();
reading_timing.stop();
let first_unread = walls_why.iter().find_map(|wall| wall.as_ref().err().cloned());
let walls: Vec<Option<f64>> = walls_why.into_iter().map(|wall| wall.ok()).collect();
let mut reading = OpenRungReading {
station_count,
stations: parameters.len(),
rails,
rail_samples,
walls,
first_unread,
refine_exit: refinement.exit,
inserted: refinement.inserted,
};
if stationary_end && !(reading.rails_read() && reading.rails <= rail_bar) {
reading.rails = f64::NAN;
}
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
let clamped = rails_off_carriers(&rows, ¶meters, [&first.face.surface, &second.face.surface]);
eprintln!(
"open march: edge {} at {station_count} stations (+{} refined, {:?}; {} in all), \
rails {:.3e} off their carriers' extension (clamped projection {:?}; bar {rail_bar:.3e}), \
wall {:?} (bar {wall_bar:.3e}){}",
edge.id,
refinement.inserted,
refinement.exit,
parameters.len(),
reading.rails,
clamped.map_err(|error| error.message),
reading.worst_wall(),
if accepted.is_some() { ", a request rung" } else { "" }
);
}
let accepted_now = accepts(&reading);
let rung = StripeRung { rows, parameters, stations, dense, reading };
if accepted.is_none() {
if accepted_now {
let met = !wall_check || meets_model(&rung.reading);
accepted = Some(rung);
if met {
break;
}
} else if station_count < super::stations::MAX_OPEN_STATIONS {
station_count *= 2;
continue;
} else {
if stationary_end {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"open march: edge {} is stationary at a vertex; its rails' reading is \
reported unread, never as a distance",
edge.id
);
}
} else {
let worst = if refinement.worst_miss > 0.0 {
format!(
", worst mid-span miss {:.2e} at edge parameter {:.6}",
refinement.worst_miss, refinement.worst_t
)
} else {
String::new()
};
super::record_blend_note(format!(
"blend: edge {}'s rails stand {:.2e} off their carriers after {} stations \
(bar {rail_bar:.1e}; local refinement {:?}, {} stations inserted{worst}); the \
wall ships as marched",
edge.id,
rung.reading.rails,
rung.parameters.len(),
refinement.exit,
refinement.inserted,
));
}
unaccepted = Some(rung);
break;
}
} else {
if !accepted_now {
unmet = Some(crate::BudgetReason::Rejected);
break;
}
let met = meets_model(&rung.reading);
accepted = Some(rung);
if met {
unmet = None;
break;
}
}
if station_count < super::stations::MAX_OPEN_STATIONS {
station_count *= 2;
} else {
unmet = Some(crate::BudgetReason::StationCeiling);
break;
}
}
let (ladder_accepted, rung) = match (accepted, unaccepted) {
(Some(rung), _) => (true, rung),
(None, Some(rung)) => (false, rung),
(None, None) => {
return Err(KernelRefusal::internal(KernelStage::Refine, "network_ladder", "blend network: the stripe ladder ended without a rung"));
}
};
let StripeRung { rows, parameters, stations, dense, reading } = rung;
super::record_count("count.stations_shipped", parameters.len() as u64);
super::record_count("count.request_rungs", request_rungs as u64);
if !ladder_accepted {
super::record_count("count.stripes_unaccepted", 1);
}
stripes.push(Stripe {
edge,
first: BlendMate {
face: first.face,
coedge: first.coedge,
loop_index: first.loop_index,
rho: first.rho,
},
second: BlendMate {
face: second.face,
coedge: second.coedge,
loop_index: second.loop_index,
rho: second.rho,
},
rows,
name: edge_names.get(index).cloned().flatten(),
poles,
measure: StripeMeasure {
parameters,
stations,
dense,
ladder_accepted,
ladder_reading: reading,
unmet,
request_rungs,
stationary_end,
wall_check,
exact_lane,
},
law: law.clone(),
end_radius,
});
}
let station_on = |stripe: &Stripe, face_id: u64, point: Vec3| -> Result<f64, KernelRefusal> {
let row = if stripe.first.face.id == face_id {
&stripe.rows.cr
} else if stripe.second.face.id == face_id {
&stripe.rows.cs
} else {
return Err(KernelRefusal::internal(KernelStage::Refine, "stripe_face", format!(
"blend network: edge {} does not touch face {face_id}",
stripe.edge.id
)));
};
rail_station(row, point, corner_station_bar(scale, radius)).map_err(|reason| match reason {
RailMiss::OffRail(distance) => format!(
"blend network: edge {}'s contact rail on face {face_id} misses the corner \
ball's tangency point by {distance:.3e}",
stripe.edge.id
),
RailMiss::OffSpan(station) => format!(
"blend network: the corner station for edge {} lands at {station:.6}, outside \
the marched rows — the radius is too large for this edge",
stripe.edge.id
),
RailMiss::Projection(error) => error,
}).or_refuse(KernelStage::Refine, "map_err")
};
let mut crossings = SpokeCrossings::default();
let end_plans_timing = super::BlendTimer::start("timing.end_plans_ms");
struct PendingPatch {
center: Vec3,
outward: bool,
normals: Vec<Vec3>,
arcs: Vec<EdgeRecord>,
name: Option<String>,
radius: f64,
}
struct PendingMiter {
vertex: u64,
sharp_edge: u64,
sharp_parameter: f64,
sharp_vertex: u64,
connector: Option<(usize, u64, u64, NurbsCurve)>,
}
struct PendingRibbonPatch {
center: Vec3,
outward: bool,
ribs: Vec<(usize, EdgeRecord, NurbsCurve)>,
name: Option<String>,
}
const RIB_SAMPLES: usize = 48;
let mut end_plans: Vec<[Option<EndPlan>; 2]> =
(0..stripes.len()).map(|_| [None, None]).collect();
let mut patches: Vec<PendingPatch> = Vec::new();
let mut ribbon_patches: Vec<PendingRibbonPatch> = Vec::new();
let mut pending_miters: Vec<PendingMiter> = Vec::new();
let mut consumed_sharp_edge = false;
struct CrossedStripe {
index: usize,
shared: u64,
u_start: f64,
u_finish: f64,
strip: f64,
p_start: Vec3,
p_finish: Vec3,
}
let mut crossed: Vec<CrossedStripe> = Vec::new();
for (index, stripe) in stripes.iter().enumerate() {
let corner_at = |vertex: u64| -> Option<(u64, &Corner)> {
corners.iter().find(|(id, _, _)| *id == vertex).and_then(|(_, _, kind)| match kind {
VertexKind::Miter { corner, shared, .. } => Some((shared.id, corner)),
_ => None,
})
};
let (Some((shared_start, corner_start)), Some((shared_finish, corner_finish))) =
(corner_at(stripe.edge.start_vertex_id), corner_at(stripe.edge.end_vertex_id))
else {
continue;
};
if shared_start != shared_finish || stripe.edge.start_vertex_id == stripe.edge.end_vertex_id {
continue;
}
let shared = shared_start;
let (p_start, _) = corner_start.tangency_on(shared)?;
let (p_finish, _) = corner_finish.tangency_on(shared)?;
let (Ok(u_start), Ok(u_finish)) =
(station_on(stripe, shared, p_start), station_on(stripe, shared, p_finish))
else {
continue;
};
let strip = p_finish.sub(p_start).length();
let chord = vertex_point_of(solid, stripe.edge.end_vertex_id)?
.sub(vertex_point_of(solid, stripe.edge.start_vertex_id)?);
let band = consumed_band(solid);
if strip > band && (u_finish < u_start || p_finish.sub(p_start).dot(chord) < 0.0) {
super::record_count("count.crossed_rails", 1);
crossed.push(CrossedStripe { index, shared, u_start, u_finish, strip, p_start, p_finish });
}
}
let refuse_crossed = |crossing: &CrossedStripe, why: &str| -> KernelRefusal {
KernelRefusal::unsupported(KernelStage::Refine, "crossed_rails", format!(
"blend network: edge {}'s rails cross — the corner stops at its two ends run \
{:.3e} past each other on face {} (stations {:.6} and {:.6}), so the rail between \
them points against the edge (C4); the radius exceeds the face between the two \
corners, and {why}",
stripes[crossing.index].edge.id, crossing.strip, crossing.shared, crossing.u_start, crossing.u_finish
))
};
if let Some(first) = crossed.first().filter(|_| !super::crossed_rails_on()) {
return Err(refuse_crossed(first, "converting the stripe into a rib between the merged corners is not built here"));
}
let mut handled_corners: Vec<u64> = Vec::new();
let mut converted_rows: Vec<(usize, FittedRows)> = Vec::new();
for crossing in &crossed {
let r_index = crossing.index;
let r_stripe = &stripes[r_index];
let shared_id = crossing.shared;
if chamfer {
return Err(refuse_crossed(crossing, "the hood over the merged corners is a fillet's; a chamfer's is not built"));
}
let corner_parts = |vertex: u64| -> Result<(&Corner, &FaceRecord, &EdgeRecord, &Vec<usize>), KernelRefusal> {
corners
.iter()
.find(|(id, _, _)| *id == vertex)
.and_then(|(_, indices, kind)| match kind {
VertexKind::Miter { corner, shared, sharp } => Some((corner, *shared, *sharp, indices)),
_ => None,
})
.ok_or_else(|| KernelRefusal::internal(KernelStage::Refine, "crossed_corner", "blend network: a crossed stripe's corner vanished"))
};
let (v_start, v_finish) = (r_stripe.edge.start_vertex_id, r_stripe.edge.end_vertex_id);
let (corner_start, shared_face, sharp_start, indices_start) = corner_parts(v_start)?;
let (corner_finish, _, sharp_finish, indices_finish) = corner_parts(v_finish)?;
if handled_corners.contains(&v_start) || handled_corners.contains(&v_finish) {
return Err(refuse_crossed(crossing, "two crossed stripes share a corner, which the conversion does not merge"));
}
let other = |indices: &Vec<usize>| indices.iter().copied().find(|&index| index != r_index);
let (Some(f_index), Some(b_index)) = (other(indices_start), other(indices_finish)) else {
return Err(refuse_crossed(crossing, "a corner has no second stripe"));
};
if f_index == b_index {
return Err(refuse_crossed(crossing, "both corners are closed by one neighbouring stripe (a two-edge loop), which the conversion does not merge"));
}
if !(corner_start.outward && corner_finish.outward) {
return Err(refuse_crossed(crossing, "the corners are concave; the hood over the merged corners is built at convex corners only"));
}
let radius_here = corner_start.radius;
let agree = |value: f64| (value - radius_here).abs() <= 1e-9 * (1.0 + radius_here);
let end_radius_at = |index: usize, vertex: u64| -> f64 {
let stripe = &stripes[index];
stripe.end_radius[usize::from(!stripe.at_start(vertex))]
};
if !(agree(corner_finish.radius)
&& agree(end_radius_at(r_index, v_start))
&& agree(end_radius_at(r_index, v_finish))
&& agree(end_radius_at(f_index, v_start))
&& agree(end_radius_at(b_index, v_finish))
&& stripes[r_index].law.constant().is_some())
{
return Err(refuse_crossed(crossing, "the stripes meet at different radii; the hood over the merged corners is built at one constant radius"));
}
let plane_outward = |face: &FaceRecord| -> Option<Vec3> {
match face.surface.analytic() {
Some(crate::AnalyticSurface::Plane { u_dir, v_dir, .. }) => {
let normal = u_dir.cross(*v_dir).normalized().ok()?;
Some(if face.same_sense { normal } else { normal.scale(-1.0) })
}
_ => None,
}
};
let top_is_first_r = r_stripe.first.face.id == shared_id;
let r_side_face = if top_is_first_r { r_stripe.second.face } else { r_stripe.first.face };
let (Some(n_top), Some(n_side)) = (plane_outward(shared_face), plane_outward(r_side_face)) else {
return Err(refuse_crossed(crossing, "the hood over the merged corners needs a planar shared face and a planar side face"));
};
let rail_rail_of = |index: usize, vertex: u64| -> RailRailStripe<'_> {
let stripe = &stripes[index];
let end_radius = end_radius_at(index, vertex);
let (on_shared, on_side) = if stripe.first.face.id == shared_id {
(&stripe.first, &stripe.second)
} else {
(&stripe.second, &stripe.first)
};
RailRailStripe {
side: on_side.face,
rho_shared: on_shared.rho.signum() * end_radius,
rho_side: on_side.rho.signum() * end_radius,
}
};
let described = [rail_rail_of(f_index, v_start), rail_rail_of(b_index, v_finish)];
let seed = crossing.p_start.add(crossing.p_finish).scale(0.5);
let m = solve_rail_rail_point(shared_face, [&described[0], &described[1]], seed, scale).map_err(|error| {
refuse_crossed(crossing, &format!("the two neighbouring stripes' rails on the shared face do not cross ({})", error.message))
})?;
let origin = vertex_point_of(solid, v_start)?;
let a = -m.point.sub(origin).dot(n_side);
if !(a > radius_here * (1.0 + 1e-9)) {
return Err(refuse_crossed(crossing, &format!(
"the neighbours' rail/rail point stands {a:.6} inside the side face, not beyond the stripe's own rail at {radius_here:.6}"
)));
}
let frame = HoodFrame { n_top, n_side, origin, a, r: radius_here };
let hood = hood_rows(&r_stripe.rows, top_is_first_r, &frame)?;
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"network crossed stripe {}: rail/rail point {:?} (residual {:.3e}), hood semi-axes {a:.6} by {radius_here:.6}",
r_stripe.edge.id, m.point, m.residual
);
}
let m_vertex = take_id();
result.vertices.push(VertexRecord { id: m_vertex, point: m.point });
for corner in [corner_start, corner_finish] {
if let Some((_, _, id)) = corner.tangency.iter().find(|(face_id, _, _)| *face_id == shared_id) {
if *id != 0 {
result.vertices.retain(|vertex| vertex.id != *id);
}
}
}
let fit_tolerance = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let band = consumed_band(solid);
let hood_top_line = if top_is_first_r { &hood.cr } else { &hood.cs };
let r_side_row = if top_is_first_r { &r_stripe.rows.cs } else { &r_stripe.rows.cr };
for (vertex_id, sharp, n_index) in [(v_start, sharp_start, f_index), (v_finish, sharp_finish, b_index)] {
let n_stripe = &stripes[n_index];
let n_at_start = n_stripe.at_start(vertex_id);
let n_top_first = n_stripe.first.face.id == shared_id;
let (n_side_row, z_top, z_side) = if n_top_first {
(&n_stripe.rows.cs, 0.0, 1.0)
} else {
(&n_stripe.rows.cr, 1.0, 0.0)
};
let u_top_n = station_on(n_stripe, shared_id, m.point)?;
let (u_side_n, _, q_point_n, escalated_n) = support_crossing(solid, n_side_row, sharp, n_at_start).map_err(|error| {
refuse_crossed(crossing, &format!("edge {}'s side rail does not reach the sharp edge at vertex {vertex_id} ({})", n_stripe.edge.id, error.message))
})?;
crossings.note(escalated_n);
let r_at_start = r_stripe.at_start(vertex_id);
let (u_side_r, t_sharp, q_point, escalated_r) = support_crossing(solid, r_side_row, sharp, r_at_start).map_err(|error| {
refuse_crossed(crossing, &format!("the stripe's side rail does not reach the sharp edge at vertex {vertex_id} ({})", error.message))
})?;
crossings.note(escalated_r);
if q_point_n.sub(q_point).length() > band {
return Err(refuse_crossed(crossing, &format!(
"the two side rails meet the sharp edge at vertex {vertex_id} {:.3e} apart",
q_point_n.sub(q_point).length()
)));
}
let far_vertex = if sharp.start_vertex_id == vertex_id { sharp.end_vertex_id } else { sharp.start_vertex_id };
let far_point = vertex_point_of(solid, far_vertex)?;
let q_vertex = if q_point.sub(far_point).length() <= band {
far_vertex
} else {
let id = take_id();
result.vertices.push(VertexRecord { id, point: q_point });
id
};
let marched = march_rib(
&n_stripe.rows.surface,
&hood.surface,
&frame,
m.point,
u_top_n,
z_top,
q_point,
u_side_n,
z_side,
n_stripe.edge.id,
)?;
let n_surface = &n_stripe.rows.surface;
let hood_surface = &hood.surface;
let (rib_curve, rib_on_n, rib_on_hood) = fit_marched(
&marched,
"merged-corner rib",
&|point: Vec3| Ok(off_surface(n_surface, point)?.max(off_surface(hood_surface, point)?)),
fit_tolerance,
)?;
let domain = rib_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let rib_id = take_id();
result.edges.push(EdgeRecord {
id: rib_id,
curve: rib_curve,
t0: domain[0],
t1: domain[1],
start_vertex_id: m_vertex,
end_vertex_id: q_vertex,
degenerate: false,
name: None,
});
let plan = |top_first: bool, walk_p_to_far: bool, u_top: f64, u_side: f64, pcurve: &NurbsCurve| -> Result<EndPlan, KernelRefusal> {
let (cr_parameter, cs_parameter, first_vertex, second_vertex) = if top_first {
(u_top, u_side, m_vertex, q_vertex)
} else {
(u_side, u_top, q_vertex, m_vertex)
};
let edges = if walk_p_to_far {
vec![(rib_id, true, pcurve.clone())]
} else {
vec![(rib_id, false, pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?)]
};
Ok(EndPlan::Miter(MiterEnd { cr_parameter, cs_parameter, first_vertex, second_vertex, edges }))
};
let walk_n = n_stripe.walk_first_to_second(n_at_start) == n_top_first;
end_plans[n_index][usize::from(!n_at_start)] = Some(plan(n_top_first, walk_n, u_top_n, u_side_n, &rib_on_n)?);
let u_top_r = crate::project_point_to_curve(hood_top_line, m.point)
.or_refuse(KernelStage::Refine, "project_point_to_curve")?
.u;
let walk_r = r_stripe.walk_first_to_second(r_at_start) == top_is_first_r;
end_plans[r_index][usize::from(!r_at_start)] = Some(plan(top_is_first_r, walk_r, u_top_r, u_side_r, &rib_on_hood)?);
pending_miters.push(PendingMiter {
vertex: vertex_id,
sharp_edge: sharp.id,
sharp_parameter: t_sharp,
sharp_vertex: q_vertex,
connector: None,
});
handled_corners.push(vertex_id);
}
converted_rows.push((r_index, hood));
super::record_count("count.crossed_rails_converted", 1);
}
let stars_miters_timing = super::BlendTimer::start("timing.end_plans.stars_miters_ms");
for (vertex_id, stripe_indices, kind) in &corners {
if handled_corners.contains(vertex_id) {
continue;
}
match kind {
VertexKind::Reentrant { .. } | VertexKind::Flush { .. } | VertexKind::MixedStar(_) => continue,
VertexKind::Star { corner } => {
let radius = corner.radius;
let mut arcs: Vec<EdgeRecord> = Vec::with_capacity(stripe_indices.len());
let mut normals: Vec<Vec3> = Vec::with_capacity(stripe_indices.len());
for (_, point, _) in &corner.tangency {
normals.push(point.sub(corner.center).normalized().or_refuse(KernelStage::Refine, "normalized")?);
}
for &index in stripe_indices {
let stripe = &stripes[index];
let at_start = stripe.at_start(*vertex_id);
let (first_point, first_vertex) = corner.tangency_on(stripe.first.face.id)?;
let (second_point, second_vertex) =
corner.tangency_on(stripe.second.face.id)?;
let station_first = station_on(stripe, stripe.first.face.id, first_point)?;
let station_second =
station_on(stripe, stripe.second.face.id, second_point)?;
if (station_first - station_second).abs() > 1e-3 {
return Err(KernelRefusal::internal(KernelStage::Refine, "stripe_vertex_stations", format!(
"blend network: edge {} meets vertex {vertex_id} at inconsistent \
stations ({station_first:.6} on one support, {station_second:.6} \
on the other)",
stripe.edge.id
)));
}
let station = (station_first + station_second) * 0.5;
let walk_first_to_second = stripe.walk_first_to_second(at_start);
let (from, to, from_vertex, to_vertex) = if walk_first_to_second {
(first_point, second_point, first_vertex, second_vertex)
} else {
(second_point, first_point, second_vertex, first_vertex)
};
let arc = if chamfer {
crate::make_line(from, to).or_refuse(KernelStage::Refine, "make_line")?
} else {
corner_section_arc(corner.center, radius, from, to)?
};
let arc_domain = arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let arc_edge_id = take_id();
let record = EdgeRecord {
id: arc_edge_id,
curve: arc,
t0: arc_domain[0],
t1: arc_domain[1],
start_vertex_id: from_vertex,
end_vertex_id: to_vertex,
degenerate: false,
name: None,
};
result.edges.push(record.clone());
arcs.push(record);
let arc_blend_pcurve = if walk_first_to_second {
crate::sweep_topology::parameter_line(station, 0.0, station, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(station, 1.0, station, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
end_plans[index][usize::from(!at_start)] = Some(EndPlan::Corner(CornerEnd {
cr_parameter: station,
cs_parameter: station,
first_vertex,
second_vertex,
arc_edge_id,
arc_blend_pcurve,
}));
}
patches.push(PendingPatch {
center: corner.center,
outward: corner.outward,
normals,
arcs,
name: corner_name(stripe_indices),
radius,
});
}
VertexKind::RibbonStar { corner } => {
let fit_tolerance = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let mut ribs: Vec<(usize, EdgeRecord, NurbsCurve)> = Vec::with_capacity(stripe_indices.len());
for &index in stripe_indices {
let stripe = &stripes[index];
let at_start = stripe.at_start(*vertex_id);
let (first_point, first_vertex) = corner.tangency_on(stripe.first.face.id)?;
let (second_point, second_vertex) = corner.tangency_on(stripe.second.face.id)?;
let u_first = station_on(stripe, stripe.first.face.id, first_point)?;
let u_second = station_on(stripe, stripe.second.face.id, second_point)?;
let walk_first_to_second = stripe.walk_first_to_second(at_start);
let vertex_point = vertex_point_of(solid, *vertex_id)?;
let leave_along = |face_id: u64, at: Vec3, uv: [f64; 2]| -> Result<[f64; 2], KernelRefusal> {
let other = stripe_indices
.iter()
.copied()
.find(|&other| other != index && (stripes[other].first.face.id == face_id || stripes[other].second.face.id == face_id))
.ok_or(KernelRefusal::internal(KernelStage::Refine, "star_rail_pair", format!(
"blend network: no second stripe on face {face_id} at vertex {vertex_id}"
)))?;
let other_stripe = &stripes[other];
let rail = if other_stripe.first.face.id == face_id { &other_stripe.rows.cr } else { &other_stripe.rows.cs };
let u_other = station_on(other_stripe, face_id, at)?;
let derivatives = rail.derivatives(u_other, 1).or_refuse(KernelStage::Refine, "derivatives")?;
let mut direction = derivatives[1].normalized().or_refuse(KernelStage::Refine, "normalized")?;
if direction.dot(vertex_point.sub(at)) < 0.0 {
direction = direction.scale(-1.0);
}
let surface_derivatives = stripe.rows.surface.derivatives(uv[0], uv[1], 1).or_refuse(KernelStage::Refine, "derivatives")?;
let (s_u, s_z) = (surface_derivatives[1][0], surface_derivatives[0][1]);
let (a, b, c) = (s_u.dot(s_u), s_u.dot(s_z), s_z.dot(s_z));
let (r0, r1) = (s_u.dot(direction), s_z.dot(direction));
let det = a * c - b * b;
if !(det.abs() > 1e-18) {
return Err(KernelRefusal::ill_posed(KernelStage::Refine, "rib_direction", format!(
"blend network: the blend of edge {} is singular at its rail/rail point on face {face_id}",
stripe.edge.id
)));
}
Ok([(c * r0 - b * r1) / det, (a * r1 - b * r0) / det])
};
let (from, to) = if walk_first_to_second {
(([u_first, 0.0], first_point, first_vertex), ([u_second, 1.0], second_point, second_vertex))
} else {
(([u_second, 1.0], second_point, second_vertex), ([u_first, 0.0], first_point, first_vertex))
};
let (face_from, face_to) = if walk_first_to_second {
(stripe.first.face.id, stripe.second.face.id)
} else {
(stripe.second.face.id, stripe.first.face.id)
};
let d_from = leave_along(face_from, from.1, from.0)?;
let d_to = leave_along(face_to, to.1, to.0)?;
let chord = ((to.0[0] - from.0[0]).powi(2) + (to.0[1] - from.0[1]).powi(2)).sqrt();
let unit = |d: [f64; 2]| -> Result<[f64; 2], KernelRefusal> {
let length = (d[0] * d[0] + d[1] * d[1]).sqrt();
if !(length > 0.0) {
return Err(KernelRefusal::ill_posed(KernelStage::Refine, "rib_direction", "blend network: a rib's leaving direction is zero"));
}
Ok([d[0] / length * chord, d[1] / length * chord])
};
let (t_from, t_to) = (unit(d_from)?, unit([-d_to[0], -d_to[1]])?);
if !(t_from[1] * (to.0[1] - from.0[1]) > 0.0 && t_to[1] * (to.0[1] - from.0[1]) > 0.0) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "n_sided_corner", format!(
"blend network: edge {}'s rib would leave its rail/rail point away from its strip \
(directions {t_from:?} and {t_to:?} in (u, z)); the ribbon fill is not built here",
stripe.edge.id
)));
}
let hermite = |fraction: f64| -> [f64; 2] {
let (s2, s3) = (fraction * fraction, fraction * fraction * fraction);
let (h00, h10, h01, h11) = (2.0 * s3 - 3.0 * s2 + 1.0, s3 - 2.0 * s2 + fraction, -2.0 * s3 + 3.0 * s2, s3 - s2);
[
h00 * from.0[0] + h10 * t_from[0] + h01 * to.0[0] + h11 * t_to[0],
h00 * from.0[1] + h10 * t_from[1] + h01 * to.0[1] + h11 * t_to[1],
]
};
let mut marched = MarchedCurve { points: Vec::new(), uv_first: Vec::new(), uv_second: Vec::new() };
for sample in 0..=RIB_SAMPLES {
let fraction = sample as f64 / RIB_SAMPLES as f64;
let uv = hermite(fraction);
let point = if sample == 0 {
from.1
} else if sample == RIB_SAMPLES {
to.1
} else {
stripe.rows.surface.evaluate_extended(uv[0], uv[1]).or_refuse(KernelStage::Refine, "evaluate_extended")?
};
marched.points.push(point);
marched.uv_first.push(uv);
marched.uv_second.push(uv);
}
let surface = &stripe.rows.surface;
let (rib_curve, rib_pcurve, _) = fit_marched(
&marched,
"rib",
&|point: Vec3| off_surface(surface, point),
fit_tolerance,
)?;
let domain = rib_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let rib_id = take_id();
let record = EdgeRecord {
id: rib_id,
curve: rib_curve,
t0: domain[0],
t1: domain[1],
start_vertex_id: from.2,
end_vertex_id: to.2,
degenerate: false,
name: None,
};
result.edges.push(record.clone());
end_plans[index][usize::from(!at_start)] = Some(EndPlan::Miter(MiterEnd {
cr_parameter: u_first,
cs_parameter: u_second,
first_vertex,
second_vertex,
edges: vec![(rib_id, true, rib_pcurve.clone())],
}));
ribs.push((index, record, rib_pcurve));
}
ribbon_patches.push(PendingRibbonPatch {
center: corner.center,
outward: corner.outward,
ribs,
name: corner_name(stripe_indices),
});
}
VertexKind::Miter {
corner,
shared,
sharp,
} => {
let radius = corner.radius;
let [mut ia, mut ib] = [stripe_indices[0], stripe_indices[1]];
let end_radius_at = |index: usize| -> f64 {
let stripe = &stripes[index];
stripe.end_radius[usize::from(!stripe.at_start(*vertex_id))]
};
if end_radius_at(ib) < end_radius_at(ia) * (1.0 - 1e-9) {
std::mem::swap(&mut ia, &mut ib);
}
let (p_point, p_vertex) = corner.tangency_on(shared.id)?;
let describe = |index: usize| -> Result<MiterStripe, KernelRefusal> {
let stripe = &stripes[index];
let shared_first = stripe.first.face.id == shared.id;
let other_face = if shared_first {
stripe.second.face
} else {
stripe.first.face
};
let at_start = stripe.at_start(*vertex_id);
Ok(MiterStripe {
rows: &stripe.rows,
shared_first,
at_start,
u_ball: station_on(stripe, shared.id, p_point)?,
other_face,
radius: stripe.end_radius[usize::from(!at_start)],
})
};
let mut described = [describe(ia)?, describe(ib)?];
let fit_tolerance = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let creased_bar = if creased_vertices.contains(vertex_id) {
let rail_on_seam = |index: usize| -> Result<Vec3, KernelRefusal> {
let stripe = &stripes[index];
let side_row = if stripe.first.face.id == shared.id { &stripe.rows.cs } else { &stripe.rows.cr };
Ok(support_crossing(solid, side_row, sharp, stripe.at_start(*vertex_id))?.2)
};
let apart = rail_on_seam(ia)?.sub(rail_on_seam(ib)?).length();
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("creased seam at vertex {vertex_id}: the side rails reach the seam {apart:.3e} apart");
}
(apart > 1e-9 * (1.0 + scale)).then(|| 1e-10 * (1.0 + radius))
} else {
None
};
let _miter_timing = super::BlendTimer::start("timing.end_plans.miter_ms");
let solve = |described: &[MiterStripe; 2]| {
solve_miter(
[&described[0], &described[1]],
radius,
chamfer,
sharp,
scale,
fit_tolerance,
consumed_band(solid),
creased_bar,
)
};
let closure = match solve(&described) {
Err(refusal) if super::miter::is_miter_seam_fold(&refusal) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("miter at vertex {vertex_id}: {refusal}; marching the seam on the other stripe");
}
super::record_count("count.miter.fold_swapped", 1);
std::mem::swap(&mut ia, &mut ib);
described = [describe(ia)?, describe(ib)?];
solve(&described)?
}
other => other?,
};
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
let seam = match &closure {
MiterClosure::Symmetric { seam, .. } => seam,
MiterClosure::Asymmetric { seam, .. } => seam,
};
let mut worst = 0.0f64;
for point in &seam.points {
for index in [ia, ib] {
let rows = &stripes[index].rows;
let own_radius = described[usize::from(index == ib)].radius;
let d = if chamfer {
crate::project_point_to_surface(&rows.surface, *point)
.map(|p| p.distance)
.unwrap_or(f64::NAN)
} else {
rows.center
.as_ref()
.and_then(|center| {
crate::project_point_to_curve(center, *point).ok()
})
.map(|p| (p.distance - own_radius).abs())
.unwrap_or(f64::NAN)
};
worst = worst.max(d);
}
}
let what = if chamfer { "off both bevels" } else { "|dist-to-centre - r|" };
eprintln!(
"network seam at vertex {vertex_id}: {} samples, worst {what} {worst:.3e}",
seam.points.len()
);
}
let commit_curve = |result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
curve: NurbsCurve,
from: u64,
to: u64|
-> Result<u64, KernelRefusal> {
let domain = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let id = take_id();
result.edges.push(EdgeRecord {
id,
curve,
t0: domain[0],
t1: domain[1],
start_vertex_id: from,
end_vertex_id: to,
degenerate: false,
name: None,
});
Ok(id)
};
let commit_vertex = |result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
point: Vec3|
-> u64 {
let id = take_id();
result.vertices.push(VertexRecord { id, point });
id
};
let plan_for = |index: usize,
far_station: f64,
far_vertex: u64,
edges: Vec<(u64, NurbsCurve)>|
-> Result<EndPlan, KernelRefusal> {
let stripe = &stripes[index];
let at_start = stripe.at_start(*vertex_id);
let shared_first = stripe.first.face.id == shared.id;
let u_ball = described[usize::from(index == ib)].u_ball;
let (cr_parameter, cs_parameter, first_vertex, second_vertex) =
if shared_first {
(u_ball, far_station, p_vertex, far_vertex)
} else {
(far_station, u_ball, far_vertex, p_vertex)
};
let walk_p_to_far = stripe.walk_first_to_second(at_start) == shared_first;
let slot = if walk_p_to_far {
edges
.into_iter()
.map(|(id, pcurve)| (id, true, pcurve))
.collect()
} else {
edges
.into_iter()
.rev()
.map(|(id, pcurve)| Ok((id, false, pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?)))
.collect::<Result<Vec<_>, KernelRefusal>>()?
};
Ok(EndPlan::Miter(MiterEnd {
cr_parameter,
cs_parameter,
first_vertex,
second_vertex,
edges: slot,
}))
};
let slot_of = |index: usize| usize::from(!stripes[index].at_start(*vertex_id));
let far_vertex = if sharp.start_vertex_id == *vertex_id {
sharp.end_vertex_id
} else {
sharp.start_vertex_id
};
let far_point = vertex_point_of(solid, far_vertex)?;
let band = consumed_band(solid);
let commit_on_sharp =
|result: &mut BrepSolid, take_id: &mut dyn FnMut() -> u64, point: Vec3| {
if point.sub(far_point).length() <= band {
far_vertex
} else {
commit_vertex(result, take_id, point)
}
};
match closure {
MiterClosure::Symmetric {
seam_fit: (curve, on_a, on_b),
exit,
sharp_parameter,
..
} => {
let q_point = sharp.curve.evaluate(sharp_parameter).or_refuse(KernelStage::Refine, "evaluate")?;
let q_vertex = commit_on_sharp(&mut result, &mut take_id, q_point);
let seam_id =
commit_curve(&mut result, &mut take_id, curve, p_vertex, q_vertex)?;
end_plans[ia][slot_of(ia)] =
Some(plan_for(ia, exit[0], q_vertex, vec![(seam_id, on_a)])?);
end_plans[ib][slot_of(ib)] =
Some(plan_for(ib, exit[1], q_vertex, vec![(seam_id, on_b)])?);
pending_miters.push(PendingMiter {
vertex: *vertex_id,
sharp_edge: sharp.id,
sharp_parameter,
sharp_vertex: q_vertex,
connector: None,
});
}
MiterClosure::Asymmetric {
seam,
seam_fit: (seam_curve, seam_on_a, seam_on_b),
leader,
exit_leader,
connector_fit: (connector_curve, on_follower, on_face),
exit_follower,
sharp_parameter,
..
} => {
let (leader_index, follower_index) = if leader == 0 {
(ia, ib)
} else {
(ib, ia)
};
let exit_point = *seam.points.last().ok_or(KernelRefusal::internal(KernelStage::Refine, "miter_seam", "miter: empty seam"))?;
let x_vertex = commit_vertex(&mut result, &mut take_id, exit_point);
let (sharp_parameter, q_point) = if creased_bar.is_some() {
let follower = &stripes[follower_index];
let (on_shared, on_side) = if follower.first.face.id == shared.id {
(&follower.first, &follower.second)
} else {
(&follower.second, &follower.first)
};
let ball = solve_flush_seam_ball(
on_shared.face,
on_shared.rho,
on_side.face,
on_side.rho,
sharp,
*vertex_id,
vertex_point_of(solid, *vertex_id)?,
scale,
)?;
(ball.seam_parameter, ball.seam_point)
} else {
(sharp_parameter, sharp.curve.evaluate(sharp_parameter).or_refuse(KernelStage::Refine, "evaluate")?)
};
let q_vertex = commit_on_sharp(&mut result, &mut take_id, q_point);
let seam_id = commit_curve(
&mut result,
&mut take_id,
seam_curve,
p_vertex,
x_vertex,
)?;
let connector_id = commit_curve(
&mut result,
&mut take_id,
connector_curve,
x_vertex,
q_vertex,
)?;
let (seam_on_leader, seam_on_follower) = if leader == 0 {
(seam_on_a, seam_on_b)
} else {
(seam_on_b, seam_on_a)
};
end_plans[leader_index][slot_of(leader_index)] = Some(plan_for(
leader_index,
exit_leader,
x_vertex,
vec![(seam_id, seam_on_leader)],
)?);
end_plans[follower_index][slot_of(follower_index)] = Some(plan_for(
follower_index,
exit_follower,
q_vertex,
vec![(seam_id, seam_on_follower), (connector_id, on_follower)],
)?);
pending_miters.push(PendingMiter {
vertex: *vertex_id,
sharp_edge: sharp.id,
sharp_parameter,
sharp_vertex: q_vertex,
connector: Some((
leader_index,
described[usize::from(leader != 0)].other_face.id,
connector_id,
on_face,
)),
});
}
}
}
}
}
struct PendingHorn {
vertex: u64,
cap_face: u64,
sharp_edge: u64,
sharp_parameter: f64,
pole_vertex: u64,
pole: Vec3,
axis: Vec3,
ends: Vec<(usize, EdgeRecord, u64, Vec3, Vec3)>,
name: Option<String>,
}
let mut pending_horns: Vec<PendingHorn> = Vec::new();
stars_miters_timing.stop();
let reentrant_timing = super::BlendTimer::start("timing.end_plans.reentrant_ms");
for (vertex_id, stripe_indices, kind) in &corners {
let radius = vertex_radius_of(*vertex_id).unwrap_or(radius);
let VertexKind::Reentrant { cap, sharp } = kind else {
continue;
};
let corner_point = solid
.vertices
.iter()
.find(|vertex| vertex.id == *vertex_id)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "reentrant_vertex", "blend network: re-entrant vertex missing"))?
.point;
let mut ends: Vec<(usize, f64, f64, Vec3, Vec3, Vec3)> = Vec::new(); for &index in stripe_indices {
let stripe = &stripes[index];
let at_start = stripe.at_start(*vertex_id);
let cap_first = stripe.first.face.id == cap.id;
let (wall_rail, cap_rail) = if cap_first {
(&stripe.rows.cs, &stripe.rows.cr)
} else {
(&stripe.rows.cr, &stripe.rows.cs)
};
let (u_stop, t_pole, pole, escalated) = support_crossing(solid, wall_rail, sharp, at_start)
.map_err(|error| {
format!(
"blend network: edge {}'s wall rail does not reach the concave edge at \
vertex {vertex_id} ({error})",
stripe.edge.id
)
}).or_refuse(KernelStage::Refine, "map_err")?;
crossings.note(escalated);
if !(u_stop > RIM_MARGIN && u_stop < 1.0 - RIM_MARGIN) {
return Err(KernelRefusal::internal(KernelStage::Refine, "concave_stop", format!(
"blend network: edge {}'s stop at the concave edge lands at {u_stop:.6}, \
outside the marched rows",
stripe.edge.id
)));
}
let cap_point = cap_rail.evaluate(u_stop).or_refuse(KernelStage::Refine, "evaluate")?;
let center = stripe
.rows
.center
.as_ref()
.ok_or(KernelRefusal::internal(KernelStage::Refine, "centre_path", "blend network: stripe rows carry no centre path"))?
.evaluate(u_stop).or_refuse(KernelStage::Refine, "evaluate")?;
ends.push((index, u_stop, t_pole, pole, cap_point, center));
}
let pole = ends[0].3;
let pole_gap = ends[1].3.sub(pole).length();
if pole_gap > 1e-6 * (1.0 + radius) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "horn_torus_dihedrals", format!(
"blend network: the two blends at vertex {vertex_id} reach the concave edge \
{pole_gap:.3e} apart (unequal wall dihedrals) — the closure is not a horn torus \
and this lane does not build it"
)));
}
let axis = tangent_away_from(sharp, *vertex_id)?;
for (index, u_stop, _, _, cap_point, center) in &ends {
let stripe = &stripes[*index];
let centre_path = stripe
.rows
.center
.as_ref()
.ok_or(KernelRefusal::internal(KernelStage::Refine, "centre_path", "blend network: stripe rows carry no centre path"))?;
let [c0, c1] = centre_path.domain().or_refuse(KernelStage::Refine, "domain")?;
let direction = centre_path.unit_tangent(*u_stop, c0, c1).map_err(|error| {
format!(
"blend network: edge {}'s centre path at u = {u_stop}: {error}",
stripe.edge.id
)
}).or_refuse(KernelStage::Refine, "map_err")?;
let radial = center.sub(pole);
if direction.dot(axis).abs() > 1e-6
|| (radial.length() - radius).abs() > 1e-6 * (1.0 + radius)
|| radial.dot(axis).abs() > 1e-6 * (1.0 + radius)
{
return Err(KernelRefusal::unsupported(KernelStage::Refine, "horn_torus_tilt", format!(
"blend network: edge {}'s section at vertex {vertex_id} is not a meridian \
of the horn torus about the concave edge (tilted class) — not in this lane",
stripe.edge.id
)));
}
if (cap_point.sub(corner_point).length() - radius).abs() > 1e-6 * (1.0 + radius) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "horn_torus_cap", format!(
"blend network: edge {}'s cap rail stops {:.6} from the re-entrant vertex, \
not one radius — the cap is not planar or the walls are not perpendicular \
to it",
stripe.edge.id,
cap_point.sub(corner_point).length()
)));
}
}
let pole_vertex = {
let id = take_id();
result.vertices.push(VertexRecord { id, point: pole });
id
};
let mut committed: Vec<(usize, EdgeRecord, u64, Vec3, Vec3)> = Vec::new();
for (index, u_stop, _, _, cap_point, center) in &ends {
let stripe = &stripes[*index];
let at_start = stripe.at_start(*vertex_id);
let cap_first = stripe.first.face.id == cap.id;
let cap_vertex = take_id();
result.vertices.push(VertexRecord {
id: cap_vertex,
point: *cap_point,
});
let (first_point, first_vertex, second_point, second_vertex) = if cap_first {
(*cap_point, cap_vertex, pole, pole_vertex)
} else {
(pole, pole_vertex, *cap_point, cap_vertex)
};
let walk_first_to_second = stripe.walk_first_to_second(at_start);
let (from, to, from_vertex, to_vertex) = if walk_first_to_second {
(first_point, second_point, first_vertex, second_vertex)
} else {
(second_point, first_point, second_vertex, first_vertex)
};
let arc = if chamfer {
crate::make_line(from, to).or_refuse(KernelStage::Refine, "make_line")?
} else {
corner_section_arc(*center, radius, from, to)?
};
let arc_domain = arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let arc_edge_id = take_id();
let record = EdgeRecord {
id: arc_edge_id,
curve: arc,
t0: arc_domain[0],
t1: arc_domain[1],
start_vertex_id: from_vertex,
end_vertex_id: to_vertex,
degenerate: false,
name: None,
};
result.edges.push(record.clone());
let arc_blend_pcurve = if walk_first_to_second {
crate::sweep_topology::parameter_line(*u_stop, 0.0, *u_stop, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(*u_stop, 1.0, *u_stop, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
end_plans[*index][usize::from(!at_start)] = Some(EndPlan::Corner(CornerEnd {
cr_parameter: *u_stop,
cs_parameter: *u_stop,
first_vertex,
second_vertex,
arc_edge_id,
arc_blend_pcurve,
}));
committed.push((*index, record, cap_vertex, *cap_point, *center));
}
pending_horns.push(PendingHorn {
vertex: *vertex_id,
cap_face: cap.id,
sharp_edge: sharp.id,
sharp_parameter: ends[0].2,
pole_vertex,
pole,
axis,
ends: committed,
name: corner_name(stripe_indices),
});
}
struct PendingMixedStar {
vertex: u64,
mixed: MixedStar,
arcs: [EdgeRecord; 3],
v_a: u64,
v_b: u64,
w_a: u64,
w_b: u64,
name: Option<String>,
}
let mut pending_mixed: Vec<PendingMixedStar> = Vec::new();
reentrant_timing.stop();
let mixed_star_timing = super::BlendTimer::start("timing.end_plans.mixed_star_ms");
for (vertex_id, stripe_indices, kind) in &corners {
let VertexKind::MixedStar(mixed) = kind else {
continue;
};
let mut new_vertex = |point: Vec3, result: &mut BrepSolid| {
let id = take_id();
result.vertices.push(VertexRecord { id, point });
id
};
let v_a = new_vertex(mixed.v_a, &mut result);
let v_b = new_vertex(mixed.v_b, &mut result);
let w_a = new_vertex(mixed.w_a, &mut result);
let w_b = new_vertex(mixed.w_b, &mut result);
let plans = [
(mixed.a, [(mixed.wall_a, mixed.v_a, v_a), (mixed.cap, mixed.w_a, w_a)], mixed.c_a),
(mixed.b, [(mixed.wall_b, mixed.v_b, v_b), (mixed.cap, mixed.w_b, w_b)], mixed.c_b),
(mixed.q, [(mixed.wall_a, mixed.v_a, v_a), (mixed.wall_b, mixed.v_b, v_b)], mixed.q_v),
];
let mut arcs: Vec<EdgeRecord> = Vec::with_capacity(3);
for (index, contacts, center) in plans {
let stripe = &stripes[index];
let at_start = stripe.at_start(*vertex_id);
let station_0 = station_on(stripe, contacts[0].0, contacts[0].1)?;
let station_1 = station_on(stripe, contacts[1].0, contacts[1].1)?;
if (station_0 - station_1).abs() > 1e-3 {
return Err(KernelRefusal::internal(KernelStage::Refine, "mixed_star_stations", format!(
"blend network: edge {} meets the mixed star at vertex {vertex_id} at inconsistent \
stations ({station_0:.6} and {station_1:.6})",
stripe.edge.id
)));
}
let station = 0.5 * (station_0 + station_1);
let on = |face_id: u64| contacts.iter().find(|(face, _, _)| *face == face_id).copied();
let (_, first_point, first_vertex) = on(stripe.first.face.id).ok_or(KernelRefusal::internal(
KernelStage::Refine, "mixed_star_face", "blend network: a mixed star stripe misses its face"))?;
let (_, second_point, second_vertex) = on(stripe.second.face.id).ok_or(KernelRefusal::internal(
KernelStage::Refine, "mixed_star_face", "blend network: a mixed star stripe misses its face"))?;
let walk_first_to_second = stripe.walk_first_to_second(at_start);
let (from, to, from_vertex, to_vertex) = if walk_first_to_second {
(first_point, second_point, first_vertex, second_vertex)
} else {
(second_point, first_point, second_vertex, first_vertex)
};
let arc = corner_section_arc(center, mixed.radius, from, to)?;
let arc_domain = arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let record = EdgeRecord {
id: take_id(),
curve: arc,
t0: arc_domain[0],
t1: arc_domain[1],
start_vertex_id: from_vertex,
end_vertex_id: to_vertex,
degenerate: false,
name: None,
};
result.edges.push(record.clone());
let arc_blend_pcurve = if walk_first_to_second {
crate::sweep_topology::parameter_line(station, 0.0, station, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(station, 1.0, station, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
end_plans[index][usize::from(!at_start)] = Some(EndPlan::Corner(CornerEnd {
cr_parameter: station,
cs_parameter: station,
first_vertex,
second_vertex,
arc_edge_id: record.id,
arc_blend_pcurve,
}));
arcs.push(record);
}
let arcs: [EdgeRecord; 3] = arcs.try_into().map_err(|_| KernelRefusal::internal(KernelStage::Refine, "mixed_star_arcs", "blend network: a mixed star needs three arcs"))?;
pending_mixed.push(PendingMixedStar {
vertex: *vertex_id,
mixed: *mixed,
arcs,
v_a,
v_b,
w_a,
w_b,
name: corner_name(stripe_indices),
});
}
struct PendingFlush {
vertex: u64,
seam_trims: Vec<(u64, f64, u64)>,
}
let mut pending_flush: Vec<PendingFlush> = Vec::new();
mixed_star_timing.stop();
let flush_timing = super::BlendTimer::start("timing.end_plans.flush_ms");
for (vertex_id, stripe_indices, kind) in &corners {
let radius = vertex_radius_of(*vertex_id).unwrap_or(radius);
let VertexKind::Flush { join } = kind else {
continue;
};
let [ia, ib] = [stripe_indices[0], stripe_indices[1]];
let section = |index: usize| -> Result<(f64, Vec3, Vec3, Vec3), KernelRefusal> {
let stripe = &stripes[index];
if let Some(join) = join {
let contact = |face_id: u64| {
join.contact_on(face_id).ok_or_else(|| {
format!(
"blend network: the flush join at vertex {vertex_id} has no contact \
on edge {}'s face {face_id}",
stripe.edge.id
)
})
};
let (first_contact, second_contact) =
(contact(stripe.first.face.id).or_refuse(KernelStage::Refine, "contact")?, contact(stripe.second.face.id).or_refuse(KernelStage::Refine, "contact")?);
let seam_first = first_contact.seam.is_some();
let (lead_face, lead_row, lead, other_face, other_row, other) = if seam_first {
(
stripe.first.face.id,
&stripe.rows.cr,
first_contact,
stripe.second.face.id,
&stripe.rows.cs,
second_contact,
)
} else {
(
stripe.second.face.id,
&stripe.rows.cs,
second_contact,
stripe.first.face.id,
&stripe.rows.cr,
first_contact,
)
};
let station_bar = corner_station_bar(scale, radius);
let station = rail_station(lead_row, lead.point, station_bar).map_err(
|reason| match reason {
RailMiss::OffRail(distance) => format!(
"blend network: edge {}'s contact rail on face {lead_face} misses \
the flush join's seam contact at vertex {vertex_id} by \
{distance:.3e}",
stripe.edge.id
),
RailMiss::OffSpan(station) => format!(
"blend network: the flush join station for edge {} lands at \
{station:.6}, outside the marched rows",
stripe.edge.id
),
RailMiss::Projection(error) => error,
},
).or_refuse(KernelStage::Refine, "map_err")?;
let other_miss = other_row.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?.sub(other.point).length();
if other_miss > station_bar {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: edge {}'s contact rail on face {other_face} misses the \
flush join's contact at vertex {vertex_id} by {other_miss:.3e} at the \
station its seam contact is on",
stripe.edge.id
)));
}
return Ok((station, first_contact.point, second_contact.point, join.center));
}
let at_start = stripe.at_start(*vertex_id);
let station = stripe
.rows
.vertex_stations
.ok_or(KernelRefusal::internal(KernelStage::Refine, "vertex_stations", "blend network: stripe rows carry no vertex stations"))?
[usize::from(!at_start)];
if !(station > RIM_MARGIN && station < 1.0 - RIM_MARGIN) {
return Err(KernelRefusal::internal(KernelStage::Refine, "vertex_section", format!(
"blend network: edge {}'s vertex section lands at {station:.6}, outside \
the marched rows",
stripe.edge.id
)));
}
let center = stripe
.rows
.center
.as_ref()
.ok_or(KernelRefusal::internal(KernelStage::Refine, "centre_path", "blend network: stripe rows carry no centre path"))?
.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?;
Ok((
station,
stripe.rows.cr.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?,
stripe.rows.cs.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?,
center,
))
};
let (station_a, cr_a, cs_a, center_a) = section(ia)?;
let (station_b, cr_b, cs_b, center_b) = section(ib)?;
let bar = 1e-6 * (1.0 + radius);
if center_a.sub(center_b).length() > bar {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: the two blends at the smooth vertex {vertex_id} sit on \
different balls ({:.3e} apart) — not a flush join",
center_a.sub(center_b).length()
)));
}
let (b_first_matches_a_first, mismatch) = {
let direct = cr_b.sub(cr_a).length().max(cs_b.sub(cs_a).length());
let crossed = cr_b.sub(cs_a).length().max(cs_b.sub(cr_a).length());
if direct <= crossed {
(true, direct)
} else {
(false, crossed)
}
};
if mismatch > bar {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: the two sections at the smooth vertex {vertex_id} differ by \
{mismatch:.3e} — not a flush join"
)));
}
let rim_first = take_id();
result.vertices.push(VertexRecord {
id: rim_first,
point: cr_a,
});
let rim_second = take_id();
result.vertices.push(VertexRecord {
id: rim_second,
point: cs_a,
});
let stripe_a = &stripes[ia];
let at_start_a = stripe_a.at_start(*vertex_id);
let walk_a_first_to_second = stripe_a.walk_first_to_second(at_start_a);
let (from, to, from_vertex, to_vertex) = if walk_a_first_to_second {
(cr_a, cs_a, rim_first, rim_second)
} else {
(cs_a, cr_a, rim_second, rim_first)
};
let arc = if chamfer {
crate::make_line(from, to).or_refuse(KernelStage::Refine, "make_line")?
} else {
corner_section_arc(center_a, radius, from, to)?
};
let arc_domain = arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let arc_edge_id = take_id();
result.edges.push(EdgeRecord {
id: arc_edge_id,
curve: arc,
t0: arc_domain[0],
t1: arc_domain[1],
start_vertex_id: from_vertex,
end_vertex_id: to_vertex,
degenerate: false,
name: None,
});
let pcurve_a = if walk_a_first_to_second {
crate::sweep_topology::parameter_line(station_a, 0.0, station_a, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(station_a, 1.0, station_a, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
end_plans[ia][usize::from(!at_start_a)] = Some(EndPlan::Corner(CornerEnd {
cr_parameter: station_a,
cs_parameter: station_a,
first_vertex: rim_first,
second_vertex: rim_second,
arc_edge_id,
arc_blend_pcurve: pcurve_a,
}));
let stripe_b = &stripes[ib];
let at_start_b = stripe_b.at_start(*vertex_id);
let (b_first_vertex, b_second_vertex) = if b_first_matches_a_first {
(rim_first, rim_second)
} else {
(rim_second, rim_first)
};
let b_walk_first_to_second = stripe_b.walk_first_to_second(at_start_b);
let b_walk_in_a_terms = b_walk_first_to_second == b_first_matches_a_first;
let forward = b_walk_in_a_terms == walk_a_first_to_second;
let pcurve_b = if b_walk_first_to_second {
crate::sweep_topology::parameter_line(station_b, 0.0, station_b, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(station_b, 1.0, station_b, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
end_plans[ib][usize::from(!at_start_b)] = Some(EndPlan::Miter(MiterEnd {
cr_parameter: station_b,
cs_parameter: station_b,
first_vertex: b_first_vertex,
second_vertex: b_second_vertex,
edges: vec![(arc_edge_id, forward, pcurve_b)],
}));
if forward {
return Err(KernelRefusal::internal(KernelStage::Refine, "flush_join_orientation", format!(
"blend network: the two blends at the smooth vertex {vertex_id} walk their \
shared section the same way — inconsistent orientation"
)));
}
let seam_trims = join
.iter()
.flat_map(|join| &join.contacts)
.filter_map(|contact| {
let (seam_id, parameter) = contact.seam?;
let rim = if contact.faces.contains(&stripe_a.first.face.id) {
rim_first
} else {
rim_second
};
Some((seam_id, parameter, rim))
})
.collect();
pending_flush.push(PendingFlush {
vertex: *vertex_id,
seam_trims,
});
}
let mut pending_caps: Vec<PendingCap> = Vec::new();
flush_timing.stop();
let free_ends_timing = super::BlendTimer::start("timing.end_plans.free_ends_ms");
for (index, stripe) in stripes.iter().enumerate() {
for (slot, (vertex, at_start)) in [
(stripe.edge.start_vertex_id, true),
(stripe.edge.end_vertex_id, false),
]
.into_iter()
.enumerate()
{
if end_plans[index][slot].is_some() {
continue;
}
if stripe.poles[slot] {
end_plans[index][slot] = Some(EndPlan::Corner(plan_pole_end(
solid,
&mut result,
&mut take_id,
stripe,
vertex,
at_start,
)?));
continue;
}
match resolve_free_end(
solid,
stripe.edge.id,
&stripe.first,
&stripe.second,
&stripe.rows,
vertex,
at_start,
) {
Ok((end, _)) => {
crossings.note(end.escalated);
end_plans[index][slot] = Some(EndPlan::Free(end));
}
Err(free_error) => {
let continuing = [stripe.first.face, stripe.second.face]
.iter()
.find_map(|face| {
let id = boundary_edge_at_vertex(solid, face, vertex, stripe.edge.id)
.ok()?;
let boundary = solid.edges.iter().find(|e| e.id == id)?;
let own = tangent_away_from(stripe.edge, vertex).ok()?;
let other = tangent_away_from(boundary, vertex).ok()?;
let alignment = own.dot(other);
let tangent = alignment <= -(1.0 - 1e-6);
let missing_crossing = matches!(
&free_error.class,
RefusalClass::UnsupportedGeometry { what }
if what == "support_crossing_uv_outside"
|| what == "support_crossing_uv_none"
|| what == super::edge::END_TANGENT_CONTINUATION_WHAT
);
(tangent || (missing_crossing && alignment <= -(1.0 - 1e-4)))
.then_some((boundary, tangent))
});
let Some((boundary, tangent)) = continuing else {
return Err(free_error);
};
if stationary_at_vertex(stripe.edge, vertex)?
|| stationary_at_vertex(boundary, vertex)?
{
return Err(KernelRefusal::unsupported(KernelStage::Refine, "stationary_join", format!(
"blend network: edge {} continues smoothly into edge {} at vertex \
{vertex} only by the one-sided limit of a stationary tangent (a \
first derivative that vanishes at the vertex); a stripe capped \
through a stationary join is not verified, so it is not built",
stripe.edge.id, boundary.id
)));
}
let (plan, needs_validation) = plan_cap_end(
solid,
&mut result,
&mut take_id,
stripe,
vertex,
at_start,
stripe.end_radius[slot],
&mut pending_caps,
)?;
crossings.note(needs_validation || !tangent);
end_plans[index][slot] = Some(EndPlan::Cap(plan));
}
}
}
}
free_ends_timing.stop();
end_plans_timing.stop();
let surgery_timing = super::BlendTimer::start("timing.surgery_ms");
let mut sewn: Vec<SewnStripe> = Vec::with_capacity(stripes.len());
let mut owed: Vec<OwedWallReport> = Vec::new();
for (index, stripe) in stripes.iter().enumerate() {
let [start_plan, finish_plan] = std::mem::replace(&mut end_plans[index], [None, None]);
let (Some(start_plan), Some(finish_plan)) = (start_plan, finish_plan) else {
return Err(KernelRefusal::internal(KernelStage::Refine, "stripe_end_plan", "blend network: a stripe end was left unplanned"));
};
let band = consumed_band(solid);
let converted = converted_rows.iter().find(|(converted_index, _)| *converted_index == index).map(|(_, rows)| rows);
let stripe_rows: &FittedRows = converted.unwrap_or(&stripe.rows);
for (row, start, finish, which) in [
(
&stripe_rows.cr,
start_plan.cr_parameter(),
finish_plan.cr_parameter(),
"first",
),
(
&stripe_rows.cs,
start_plan.cs_parameter(),
finish_plan.cs_parameter(),
"second",
),
] {
let strip = row.evaluate(finish).or_refuse(KernelStage::Refine, "evaluate")?.sub(row.evaluate(start).or_refuse(KernelStage::Refine, "evaluate")?).length();
if finish <= start && strip > band {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "stripe_pinch", format!(
"blend network: edge {} is shorter than the two corner setbacks that meet \
on it (its {which} rail would run from {start:.6} to {finish:.6}, stops \
{strip:.3e} past each other against a band of {band:.3e}); the blend strip \
pinches out and pinch splitting is not implemented",
stripe.edge.id
)));
}
}
let rows = FittedRows {
surface: stripe_rows.surface.clone(),
cr: stripe_rows.cr.clone(),
cs: stripe_rows.cs.clone(),
cr_pcurve: stripe_rows.cr_pcurve.clone(),
cs_pcurve: stripe_rows.cs_pcurve.clone(),
u_domain: stripe_rows.u_domain,
center: None,
exact_extrusion: stripe_rows.exact_extrusion,
vertex_stations: stripe_rows.vertex_stations,
revolution_axis: stripe_rows.revolution_axis,
};
let spans = [
shipped_span(&stripe.measure.parameters, &stripe_rows.cr, start_plan.cr_parameter(), finish_plan.cr_parameter(), band),
shipped_span(&stripe.measure.parameters, &stripe_rows.cs, start_plan.cs_parameter(), finish_plan.cs_parameter(), band),
];
let report_timing = super::BlendTimer::start("timing.surgery.wall_report_ms");
let report = if converted.is_some() {
None
} else {
shipped_wall_report(stripe, spans, stripe.law.constant().unwrap_or(radius), solid)
};
report_timing.stop();
sewn.push(build_open_surgery(
solid,
&mut result,
&mut take_id,
stripe.edge,
&stripe.first,
&stripe.second,
rows,
[start_plan, finish_plan],
stripe.name.as_deref(),
)?);
if let Some((reason, bar, measured, detail)) = report {
let face_id = sewn[sewn.len() - 1].blend_face_id;
let face = result.shells.iter().flat_map(|shell| &shell.faces).find(|face| face.id == face_id).ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"network_blend_face",
format!("blend network: edge {}'s sewn blend face {face_id} is not in the body it was sewn into", stripe.edge.id),
)
})?;
owed.push(OwedWallReport {
edge: stripe.edge.id,
name: stripe.name.clone(),
surface: face.surface.clone(),
reason,
bar,
measured,
detail,
});
}
}
let closures_timing = super::BlendTimer::start("timing.surgery.closures_ms");
for patch in patches {
let face = if chamfer {
build_chamfer_corner_facet(
patch.center,
&patch.normals,
&patch.arcs,
patch.outward,
patch.name.as_deref(),
&mut take_id,
)?
} else {
let (face, pole) = build_corner_patch_exact(
patch.center,
patch.radius,
&patch.normals,
&patch.arcs,
patch.outward,
patch.name.as_deref(),
&mut take_id,
)?;
if let Some(edge) = pole {
result.edges.push(edge);
}
face
};
let shell_index = result
.shells
.iter()
.position(|shell| !shell.faces.is_empty())
.ok_or(KernelRefusal::internal(KernelStage::Sew, "shell", "blend network: the solid has no shell to sew the corner patch into"))?;
result.shells[shell_index].faces.push(face);
}
for patch in ribbon_patches {
let ribs: Vec<RibbonRib<'_>> = patch
.ribs
.iter()
.map(|(index, edge, pcurve)| RibbonRib {
edge: edge.clone(),
surface: &stripes[*index].rows.surface,
pcurve: pcurve.clone(),
})
.collect();
let (face, reading) = build_ribbon_star_patch(&ribs, patch.center, patch.outward, patch.name.as_deref(), &mut take_id)?;
super::record_count("count.ribbon_star_fills", 1);
if super::timing::enabled() && std::env::var_os("BREP_PROFILE").is_some() {
eprintln!(
"blend.ribbon_star residual={:.3e} scale={:.3e} twist={:.3e}",
reading.cross_derivative_residual, reading.cross_derivative_scale, reading.twist_discrepancy
);
}
let shell_index = result
.shells
.iter()
.position(|shell| !shell.faces.is_empty())
.ok_or(KernelRefusal::internal(KernelStage::Sew, "shell", "blend network: the solid has no shell to sew the ribbon fill into"))?;
result.shells[shell_index].faces.push(face);
}
for miter in pending_miters {
let consumed_whole = result
.edges
.iter()
.find(|edge| edge.id == miter.sharp_edge)
.is_some_and(|edge| {
edge.start_vertex_id == miter.sharp_vertex || edge.end_vertex_id == miter.sharp_vertex
});
if consumed_whole {
consume_sharp_edge(&mut result, miter.sharp_edge);
consumed_sharp_edge = true;
if miter.connector.is_none() {
continue;
}
} else {
trim_edge_at(
&mut result,
miter.sharp_edge,
miter.sharp_parameter,
miter.vertex,
miter.sharp_vertex,
)?;
}
let Some((leader_index, face_id, connector_id, on_face)) = miter.connector else {
continue;
};
let leader = &stripes[leader_index];
let rail_id = if leader.first.face.id == face_id {
sewn[leader_index].cr_edge_id
} else {
sewn[leader_index].cs_edge_id
}
.ok_or_else(|| {
format!(
"{RAIL_COLLAPSE_UNSUPPORTED} the miter connector at vertex {} runs from a leader \
rail that collapsed to a point",
miter.vertex
)
}).or_refuse(KernelStage::Refine, "ok_or_else")?;
let vertices_of = |result: &BrepSolid, id: u64| {
result
.edges
.iter()
.find(|edge| edge.id == id)
.map(|edge| (edge.start_vertex_id, edge.end_vertex_id))
.ok_or(KernelRefusal::internal(
KernelStage::Sew,
"connector_edges",
"blend network: a miter edge vanished before the connector was spliced",
))
};
let (rail_start, rail_end) = vertices_of(&result, rail_id)?;
let (connector_start, _) = vertices_of(&result, connector_id)?;
let coedge_id = take_id();
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == face_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "connector_face", "blend network: the connector's face is missing"))?;
let mut spliced = false;
for loop_record in &mut face.loops {
let count = loop_record.coedges.len();
let Some(rail_at) = loop_record
.coedges
.iter()
.position(|coedge| coedge.edge_id == rail_id)
else {
continue;
};
let (insert_at, forward, pcurve) = if consumed_whole {
let rail = &loop_record.coedges[rail_at];
let (walk_start, walk_end) = if rail.forward {
(rail_start, rail_end)
} else {
(rail_end, rail_start)
};
if walk_end == connector_start {
(rail_at + 1, true, on_face.clone())
} else if walk_start == connector_start {
(rail_at, false, on_face.reversed().or_refuse(KernelStage::Refine, "reversed")?)
} else {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"connector_adjacency",
"blend network: the leader's rail does not end at the seam's exit on \
the connector's face",
));
}
} else {
let Some(sharp_at) = loop_record
.coedges
.iter()
.position(|coedge| coedge.edge_id == miter.sharp_edge)
else {
continue;
};
if (rail_at + 1) % count == sharp_at {
(rail_at + 1, true, on_face.clone())
} else if (sharp_at + 1) % count == rail_at {
(sharp_at + 1, false, on_face.reversed().or_refuse(KernelStage::Refine, "reversed")?)
} else {
return Err(
KernelRefusal::internal(KernelStage::Sew, "connector_adjacency", "blend network: the leader's rail and the sharp edge are not adjacent on \
the connector's face"),
);
}
};
loop_record.coedges.insert(
insert_at,
CoedgeRecord {
id: coedge_id,
edge_id: connector_id,
forward,
pcurve,
},
);
spliced = true;
break;
}
if !spliced {
return Err(
KernelRefusal::internal(KernelStage::Sew, "connector_loop", "blend network: no loop on the connector's face holds the leader's rail and \
the sharp edge"),
);
}
}
for horn in pending_horns {
let radius = vertex_radius_of(horn.vertex).unwrap_or(radius);
trim_edge_at(
&mut result,
horn.sharp_edge,
horn.sharp_parameter,
horn.vertex,
horn.pole_vertex,
)?;
let corner_point = solid
.vertices
.iter()
.find(|vertex| vertex.id == horn.vertex)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "reentrant_vertex", "blend network: re-entrant vertex missing"))?
.point;
let [(index_a, arc_a, cap_a, cap_point_a, center_a), (index_b, arc_b, cap_b, cap_point_b, center_b)] =
<[_; 2]>::try_from(horn.ends).map_err(|_| KernelRefusal::internal(KernelStage::Refine, "horn_ends", "blend network: a horn needs two ends"))?;
let cap_arc = corner_section_arc(corner_point, radius, cap_point_a, cap_point_b)?;
let cap_domain = cap_arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let cap_arc_id = take_id();
result.edges.push(EdgeRecord {
id: cap_arc_id,
curve: cap_arc.clone(),
t0: cap_domain[0],
t1: cap_domain[1],
start_vertex_id: cap_a,
end_vertex_id: cap_b,
degenerate: false,
name: None,
});
let cap_forward_on_cap: bool;
let horn_rail = |index: usize| {
if stripes[index].first.face.id == horn.cap_face {
sewn[index].cr_edge_id
} else {
sewn[index].cs_edge_id
}
.ok_or_else(|| {
format!(
"{RAIL_COLLAPSE_UNSUPPORTED} a cap rail at the re-entrant vertex {} collapsed \
to a point",
horn.vertex
)
})
};
let (rail_a, rail_b) = (horn_rail(index_a).or_refuse(KernelStage::Refine, "horn_rail")?, horn_rail(index_b).or_refuse(KernelStage::Refine, "horn_rail")?);
{
let cap_face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == horn.cap_face)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "cap_face", "blend network: the cap face is missing"))?;
let cap_pcurve = {
let mut samples = Vec::with_capacity(17);
let mut parameters = Vec::with_capacity(17);
for k in 0..=16 {
let t = cap_domain[0] + (cap_domain[1] - cap_domain[0]) * k as f64 / 16.0;
let point = cap_arc.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
let projection = crate::project_point_to_surface(&cap_face.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
samples.push(crate::Vec4::from_point(
Vec3::new(projection.u, projection.v, 0.0),
1.0,
));
parameters.push(k as f64 / 16.0);
}
crate::fit::interpolate_homogeneous(&samples, 3, ¶meters).or_refuse(KernelStage::Refine, "interpolate_homogeneous")?
};
let mut spliced: Option<bool> = None;
for loop_record in &mut cap_face.loops {
let count = loop_record.coedges.len();
let (Some(at_a), Some(at_b)) = (
loop_record.coedges.iter().position(|c| c.edge_id == rail_a),
loop_record.coedges.iter().position(|c| c.edge_id == rail_b),
) else {
continue;
};
let (insert_at, forward, pcurve) = if (at_a + 1) % count == at_b {
(at_a + 1, true, cap_pcurve.clone())
} else if (at_b + 1) % count == at_a {
(at_b + 1, false, cap_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?)
} else {
return Err(
KernelRefusal::internal(KernelStage::Sew, "cap_rails", "blend network: the two cap rails are not adjacent on the cap"),
);
};
loop_record.coedges.insert(
insert_at,
CoedgeRecord {
id: take_id(),
edge_id: cap_arc_id,
forward,
pcurve,
},
);
spliced = Some(forward);
break;
}
let Some(forward) = spliced else {
return Err(KernelRefusal::internal(KernelStage::Sew, "cap_loop", "blend network: no cap loop holds both cap rails"));
};
cap_forward_on_cap = forward;
}
let radial_a = center_a.sub(horn.pole).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let radial_b = center_b.sub(horn.pole).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let sweep = radial_a.dot(radial_b).clamp(-1.0, 1.0).acos();
if sweep < 1e-9 {
return Err(KernelRefusal::internal(KernelStage::Refine, "horn_torus_sweep", "blend network: degenerate horn-torus sweep"));
}
let revolve_axis = if radial_a.cross(radial_b).dot(horn.axis) > 0.0 {
horn.axis
} else {
horn.axis.scale(-1.0)
};
let generatrix = if chamfer {
crate::make_line(horn.pole, cap_point_a).or_refuse(KernelStage::Refine, "make_line")?
} else {
corner_section_arc(center_a, radius, horn.pole, cap_point_a)?
};
let surface = crate::make_revolution(horn.pole, revolve_axis, &generatrix, sweep).or_refuse(KernelStage::Refine, "make_revolution")?;
let far = surface.evaluate(1.0, 1.0).or_refuse(KernelStage::Refine, "evaluate")?;
if far.sub(cap_point_b).length() > 1e-6 * (1.0 + radius) {
return Err(KernelRefusal::internal(KernelStage::Refine, "horn_torus_closure", format!(
"blend network: the horn torus does not close on the second cap point ({:.3e} off)",
far.sub(cap_point_b).length()
)));
}
let pole_edge_id = take_id();
result.edges.push(EdgeRecord {
id: pole_edge_id,
curve: crate::make_line(horn.pole, horn.pole).or_refuse(KernelStage::Refine, "make_line")?,
t0: 0.0,
t1: 1.0,
start_vertex_id: horn.pole_vertex,
end_vertex_id: horn.pole_vertex,
degenerate: true,
name: None,
});
let pl = |u0, v0, u1, v1| crate::sweep_topology::parameter_line(u0, v0, u1, v1);
let a_walks_down = arc_a.start_vertex_id == horn.pole_vertex; let b_walks_up = arc_b.end_vertex_id == horn.pole_vertex; let cap_walks_back = cap_forward_on_cap; if a_walks_down != b_walks_up || a_walks_down != cap_walks_back {
return Err(
KernelRefusal::internal(KernelStage::Sew, "horn_boundary_walk", "blend network: the blends and the cap do not walk the horn's boundary \
consistently"),
);
}
let coedges = if a_walks_down {
vec![
CoedgeRecord {
id: take_id(),
edge_id: arc_a.id,
forward: false,
pcurve: pl(0.0, 1.0, 0.0, 0.0).or_refuse(KernelStage::Refine, "pl")?,
},
CoedgeRecord {
id: take_id(),
edge_id: pole_edge_id,
forward: true,
pcurve: pl(0.0, 0.0, 1.0, 0.0).or_refuse(KernelStage::Refine, "pl")?,
},
CoedgeRecord {
id: take_id(),
edge_id: arc_b.id,
forward: false,
pcurve: pl(1.0, 0.0, 1.0, 1.0).or_refuse(KernelStage::Refine, "pl")?,
},
CoedgeRecord {
id: take_id(),
edge_id: cap_arc_id,
forward: false,
pcurve: pl(1.0, 1.0, 0.0, 1.0).or_refuse(KernelStage::Refine, "pl")?,
},
]
} else {
vec![
CoedgeRecord {
id: take_id(),
edge_id: arc_a.id,
forward: false,
pcurve: pl(0.0, 0.0, 0.0, 1.0).or_refuse(KernelStage::Refine, "pl")?,
},
CoedgeRecord {
id: take_id(),
edge_id: cap_arc_id,
forward: true,
pcurve: pl(0.0, 1.0, 1.0, 1.0).or_refuse(KernelStage::Refine, "pl")?,
},
CoedgeRecord {
id: take_id(),
edge_id: arc_b.id,
forward: false,
pcurve: pl(1.0, 1.0, 1.0, 0.0).or_refuse(KernelStage::Refine, "pl")?,
},
CoedgeRecord {
id: take_id(),
edge_id: pole_edge_id,
forward: true,
pcurve: pl(1.0, 0.0, 0.0, 0.0).or_refuse(KernelStage::Refine, "pl")?,
},
]
};
let contact = surface.evaluate(0.5, 1.0).or_refuse(KernelStage::Refine, "evaluate")?;
let cap = solid.shells.iter().flat_map(|shell| &shell.faces)
.find(|face| face.id == horn.cap_face)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "cap_face", "blend network: missing horn cap"))?;
let projection = crate::project_point_to_surface(&cap.surface, contact)
.or_refuse(KernelStage::Refine, "project")?;
let cap_normal = cap.surface.normal(projection.u, projection.v)
.or_refuse(KernelStage::Refine, "normal")?;
let outward = if cap.same_sense { cap_normal } else { cap_normal.scale(-1.0) };
let surface_normal = surface.normal(0.5, 1.0).or_refuse(KernelStage::Refine, "normal")?;
let same_sense = surface_normal.dot(outward) > 0.0;
let loop_id = take_id();
let face = FaceRecord {
id: take_id(),
surface,
same_sense,
loops: vec![crate::topology::LoopRecord {
id: loop_id,
coedges,
}],
name: horn.name,
};
let shell_index = result
.shells
.iter()
.position(|shell| !shell.faces.is_empty())
.ok_or(KernelRefusal::internal(KernelStage::Sew, "shell", "blend network: the solid has no shell to sew the horn torus into"))?;
result.shells[shell_index].faces.push(face);
}
for pending in pending_mixed {
sew_mixed_star(&mut result, solid, &mut take_id, &stripes, &sewn, pending.vertex, &pending.mixed, &pending.arcs, [pending.v_a, pending.v_b, pending.w_a, pending.w_b], pending.name)?;
}
for flush in pending_flush {
for (seam_id, parameter, rim) in flush.seam_trims {
trim_edge_at(&mut result, seam_id, parameter, flush.vertex, rim)?;
}
}
for cap in pending_caps {
sew_cap(&mut result, &mut take_id, cap)?;
}
let mut consumed_poles = Vec::new();
for face in result.shells.iter().flat_map(|shell| &shell.faces) {
for ring in &face.loops {
let n = ring.coedges.len();
for (i, coedge) in ring.coedges.iter().enumerate() {
let edge = result.edges.iter().find(|e| e.id == coedge.edge_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "pole_edge",
"blend network: pole cleanup references a missing edge"))?;
if !edge.degenerate || n < 3 {
continue;
}
let (_, before) = coedge_walk(&result, &ring.coedges[(i + n - 1) % n])?;
let (after, _) = coedge_walk(&result, &ring.coedges[(i + 1) % n])?;
if before == after && before != edge.start_vertex_id {
consumed_poles.push(edge.id);
}
}
}
}
for edge in consumed_poles {
let uses = result.shells.iter().flat_map(|shell| &shell.faces)
.flat_map(|face| &face.loops).flat_map(|ring| &ring.coedges)
.filter(|coedge| coedge.edge_id == edge).count();
if uses == 1 {
consume_sharp_edge(&mut result, edge);
}
}
let identified: Vec<(u64, u64)> =
sewn.iter().flat_map(|stripe| stripe.identified.iter().copied()).collect();
let collapse = collapse_full_width(
solid,
&mut result,
&identified,
consumed_sharp_edge,
consumed_band(solid),
)?;
let face_exists = |result: &BrepSolid, id: u64| {
result.shells.iter().flat_map(|shell| &shell.faces).any(|face| face.id == id)
};
for stripe in &mut sewn {
stripe.cr_edge_id = stripe.cr_edge_id.map(|id| collapse.edge(id));
stripe.cs_edge_id = stripe.cs_edge_id.map(|id| collapse.edge(id));
}
for (index, stripe) in stripes.iter().enumerate() {
for (side, face) in [stripe.first.face.id, stripe.second.face.id].into_iter().enumerate() {
sewn[index].consumed[side] |= !face_exists(&result, face);
}
}
let mut rail_faces: Vec<(u64, u64)> = Vec::with_capacity(sewn.len() * 2);
for (index, stripe) in stripes.iter().enumerate() {
for (side, face, rail) in [
(0, stripe.first.face.id, sewn[index].cr_edge_id),
(1, stripe.second.face.id, sewn[index].cs_edge_id),
] {
if let (false, Some(rail)) = (sewn[index].consumed[side], rail) {
rail_faces.push((face, rail));
}
}
}
let mut rail_edges: Vec<u64> = rail_faces.iter().map(|(_, rail)| *rail).collect();
let mut seam_faces: Vec<u64> = rail_faces.iter().map(|(face, _)| *face).collect();
seam_faces.sort_unstable();
seam_faces.dedup();
for face_id in seam_faces {
for _ in 0..2 {
let Some(plan) = plan_carrier_seam_split(&result, &rail_edges, face_id)? else {
break;
};
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
match &plan {
CarrierSeamRepair::Split(plan) => eprintln!(
"carrier seam: face {face_id} seam {} crossed by rail {} — split",
plan.seam_edge, plan.rail_edge
),
CarrierSeamRepair::Trim(plan) => eprintln!(
"carrier seam: face {face_id} seam {} met by rails at vertex {} — re-footed",
plan.seam_edge, plan.rim_vertex
),
}
}
match plan {
CarrierSeamRepair::Split(plan) => {
let original_rail = plan.rail_edge;
let pieces = apply_carrier_seam_split(&mut result, &mut take_id, plan)?;
rail_edges.retain(|&edge| edge != original_rail);
rail_edges.extend(pieces);
}
CarrierSeamRepair::Trim(plan) => apply_carrier_seam_trim(&mut result, plan)?,
}
}
}
closures_timing.stop();
surgery_timing.stop();
let restriction_timing = super::BlendTimer::start("timing.restriction_ms");
let stripe_faces: Vec<StripeFaces> = stripes
.iter()
.enumerate()
.filter(|(index, _)| face_exists(&result, sewn[*index].blend_face_id))
.map(|(index, stripe)| {
Ok(StripeFaces {
blend_face: sewn[index].blend_face_id,
mates: [stripe.first.face.id, stripe.second.face.id],
consumed: sewn[index].consumed,
convex: edge_is_convex(stripe.edge, &stripe.first, &stripe.second)?,
size: stripe.law.largest(),
})
})
.collect::<Result<Vec<_>, KernelRefusal>>()?;
if std::env::var("BREP_NO_THIRD_FACE_TRIM").is_err() {
let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
let third_faces_timing = super::BlendTimer::start("timing.restriction.third_faces_ms");
let fragments = restrict_third_faces(&mut result, &mut take_id, &stripe_faces, &policy)?;
third_faces_timing.stop();
let original_faces: Vec<u64> = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| face.id)
.chain(fragments)
.collect();
let walls: Vec<u64> = sewn.iter().map(|stripe| stripe.blend_face_id).collect();
let closures: Vec<u64> = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| face.id)
.filter(|id| !original_faces.contains(id) && !walls.contains(id))
.collect();
let closures_timing = super::BlendTimer::start("timing.restriction.closures_ms");
super::restrict::restrict_closures(&mut result, &mut take_id, &closures, solid, radius, &policy)?;
closures_timing.stop();
}
restriction_timing.stop();
let gate_timing = super::BlendTimer::start("timing.network_gate_ms");
prune_orphan_vertices(&mut result);
if !collapse.merged_edges.is_empty() {
let blend_faces: Vec<u64> = sewn.iter().map(|stripe| stripe.blend_face_id).collect();
result = absorb_cosurface_halves(result, &collapse.merged_edges, &blend_faces)?;
}
let snap = sewn.iter().fold(0.0f64, |worst, stripe| worst.max(stripe.snap));
check_snap_closure(solid, &result, snap)?;
if collapse.changed() {
check_collapse_closure(solid, &result)?;
}
let built = crossings.gate(result)?;
gate_timing.stop();
for report in owed {
let carried = built.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
face.name == report.name && super::same_surface(&face.surface, &report.surface)
});
if carried {
super::record_wall_model_report(super::WallModelReport {
name: report.name,
surface: report.surface,
request: report.bar,
residual: report.measured,
detail: Some(report.detail),
budget: report.reason,
});
} else {
super::record_blend_note(format!(
"blend network: edge {}'s shipped wall owes a {:?} report ({:.3e} against {:.3e}; {}), but a \
later network step replaced its face, so it is named here instead",
report.edge, report.reason.reason, report.measured, report.bar, report.detail
));
}
}
Ok(built)
}
struct OwedWallReport {
edge: u64,
name: Option<String>,
surface: crate::NurbsSurface,
reason: crate::ApproximationBudget,
bar: f64,
measured: f64,
detail: String,
}
#[derive(Clone, Copy, Debug)]
enum ShippedSpan {
Window([f64; 2]),
Collapsed,
Unread,
}
fn shipped_span(parameters: &[f64], row: &NurbsCurve, start: f64, finish: f64, band: f64) -> ShippedSpan {
let (Some(&first), Some(&last)) = (parameters.first(), parameters.last()) else {
return ShippedSpan::Unread;
};
let inside = |t: f64| t.is_finite() && t >= first && t <= last;
if !(inside(start) && inside(finish)) {
return ShippedSpan::Unread;
}
if finish > start {
return ShippedSpan::Window([start, finish]);
}
let strip = row.evaluate(finish).ok().zip(row.evaluate(start).ok()).map(|(a, b)| a.sub(b).length());
match strip {
Some(strip) if strip.is_finite() && strip <= band => ShippedSpan::Collapsed,
_ => ShippedSpan::Unread,
}
}
fn shipped_wall_report(
stripe: &Stripe,
spans: [ShippedSpan; 2],
radius: f64,
solid: &BrepSolid,
) -> Option<(crate::ApproximationBudget, f64, f64, String)> {
if spans.iter().all(|span| matches!(span, ShippedSpan::Collapsed)) {
return None;
}
let windows = spans.map(|span| match span {
ShippedSpan::Window(window) => Some(window),
ShippedSpan::Collapsed | ShippedSpan::Unread => None,
});
let measure = &stripe.measure;
let tolerances = crate::KernelTolerances::for_solid(solid, 1e-7);
let rail_bar = 0.5 * tolerances.intersection_fit;
let (wall_bar, model) = (tolerances.intersection_fit, tolerances.model);
let rails_timing = super::BlendTimer::start("timing.surgery.wall_report.rails_ms");
let (rails, rail_samples) = rails_on_shipped_windows(
&stripe.rows,
&measure.parameters,
&measure.stations,
[&stripe.first.face.surface, &stripe.second.face.surface],
windows,
);
rails_timing.stop();
let envelope = windows.iter().flatten().fold(None, |envelope: Option<[f64; 2]>, window| {
Some(envelope.map_or(*window, |known| [known[0].min(window[0]), known[1].max(window[1])]))
});
let any_unread = spans.iter().any(|span| matches!(span, ShippedSpan::Unread));
let walls_timing = super::BlendTimer::start("timing.surgery.wall_report.walls_ms");
let walls_why = match (measure.wall_check, envelope, any_unread) {
(true, Some(envelope), false) if stripe.rows.revolution_axis.is_some() => {
exact_revolution_wall_readings(&stripe.rows, &measure.parameters, envelope, radius)
}
(true, Some(envelope), false) if measure.exact_lane => {
exact_extrusion_wall_readings(&stripe.rows, &measure.parameters, envelope, radius)
}
(true, Some(envelope), false) => {
open_wall_readings_why(&stripe.rows, &measure.parameters, &measure.stations, envelope, &measure.dense, radius)
}
_ => Vec::new(),
};
walls_timing.stop();
let first_unread = walls_why.iter().find_map(|wall| wall.as_ref().err().cloned());
let walls: Vec<Option<f64>> = walls_why.into_iter().map(|wall| wall.ok()).collect();
let ladder = &measure.ladder_reading;
let mut shipped = OpenRungReading {
station_count: ladder.station_count,
stations: ladder.stations,
rails,
rail_samples,
walls,
first_unread,
refine_exit: ladder.refine_exit,
inserted: ladder.inserted,
};
let rails_read = |reading: &OpenRungReading| {
reading.rails.is_finite()
&& reading.rails >= 0.0
&& spans.iter().zip(reading.rail_samples).all(|(span, samples)| match span {
ShippedSpan::Window(_) => samples > 0,
ShippedSpan::Collapsed => true,
ShippedSpan::Unread => false,
})
};
if measure.stationary_end && !(rails_read(&shipped) && shipped.rails <= rail_bar) {
shipped.rails = f64::NAN;
}
let read = rails_read(&shipped);
let accepted = read
&& shipped.rails <= rail_bar
&& (!measure.wall_check || shipped.worst_wall().is_some_and(|wall| wall <= wall_bar));
let rungs_past_first = (ladder.station_count / STATIONS).trailing_zeros() as usize;
let scope = format!(
"read on the shipped spans {spans:?}; the ladder {} the vertex window at {} stations a rung (rails {:.3e}, wall {:?}); \
the shipped rung's local rail refinement ended {:?} with {} stations inserted",
if measure.ladder_accepted { "accepted" } else { "never accepted" },
ladder.station_count,
ladder.rails,
ladder.worst_wall(),
ladder.refine_exit,
ladder.inserted
);
let budget = |reason: crate::BudgetReason, rounds_used: usize| crate::ApproximationBudget {
reason,
rounds_used,
rounds_limit: OPEN_REQUEST_RUNGS,
stations: ladder.stations,
station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
mechanism: crate::BudgetMechanism::StationRungs,
measured_component: crate::MeasuredComponent::Wall,
unread: 0,
};
if !accepted {
let (reason, bar, measured, deficit) =
acceptance_deficit(&shipped, rail_bar, wall_bar, measure.wall_check, read, "the shipped windows fail acceptance");
let rounds = if measure.ladder_accepted { measure.request_rungs } else { rungs_past_first };
return Some((budget(reason, rounds), bar, measured, format!("{deficit}; {scope}")));
}
if measure.wall_check {
let declared = shipped.worst_wall()?;
if !(declared <= model) {
let reason = measure.unmet.unwrap_or(crate::BudgetReason::StationCeiling);
return Some((budget(reason, measure.request_rungs), model, declared, scope));
}
}
None
}
fn absorb_cosurface_halves(
result: BrepSolid,
merged_edges: &[(u64, u64)],
blend_faces: &[u64],
) -> Result<BrepSolid, KernelRefusal> {
let tolerance =
crate::KernelTolerances::for_solid(&result, crate::BooleanOptions::default().tolerance)
.model;
let mut absorbed = rustc_hash::FxHashSet::default();
let mut rail_ends = rustc_hash::FxHashSet::default();
for &(_, kept) in merged_edges {
let faces: Vec<&FaceRecord> = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| {
face.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == kept)
})
.collect();
let [first, second] = faces[..] else {
continue;
};
if !(blend_faces.contains(&first.id) && blend_faces.contains(&second.id)) {
continue;
}
if !crate::face_merge::faces_are_cosurface(first, second, tolerance)
?
{
continue;
}
absorbed.insert(first.id.max(second.id));
if let Some(edge) = result.edges.iter().find(|edge| edge.id == kept) {
rail_ends.insert(edge.start_vertex_id);
rail_ends.insert(edge.end_vertex_id);
}
}
if absorbed.is_empty() {
return Ok(result);
}
let Ok(merged) = crate::face_merge::absorb_faces_into_carriers(&result, tolerance, &absorbed)
else {
return Ok(result);
};
Ok(match crate::coalesce::merge_curve_continuation_edges_at(&merged, tolerance, &rail_ends) {
Ok(joined) if joined.validate().is_empty() => joined,
_ => merged,
})
}
fn consume_sharp_edge(result: &mut BrepSolid, edge_id: u64) {
for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
for loop_record in &mut face.loops {
loop_record.coedges.retain(|coedge| coedge.edge_id != edge_id);
}
}
result.edges.retain(|edge| edge.id != edge_id);
}
struct PendingCap {
plane_origin: Vec3,
plane_u: Vec3,
plane_v: Vec3,
plane_normal: Vec3,
arc: (u64, u64, u64),
first_leg: (u64, u64, u64),
second_leg: (u64, u64, u64),
outward: Vec3,
name: Option<String>,
}
#[allow(clippy::too_many_arguments)]
fn plan_cap_end(
solid: &BrepSolid,
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
stripe: &Stripe,
vertex: u64,
at_start: bool,
radius: f64,
pending: &mut Vec<PendingCap>,
) -> Result<(CapEnd, bool), KernelRefusal> {
let sharp_point = solid
.vertices
.iter()
.find(|candidate| candidate.id == vertex)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "cap_vertex", "blend network: cap vertex missing"))?
.point;
let station = stripe
.rows
.vertex_stations
.ok_or(KernelRefusal::internal(KernelStage::Refine, "vertex_stations", "blend network: stripe rows carry no vertex stations"))?[usize::from(!at_start)];
if !(station > RIM_MARGIN && station < 1.0 - RIM_MARGIN) {
return Err(KernelRefusal::internal(KernelStage::Refine, "vertex_section", format!(
"blend network: edge {}'s vertex section lands at {station:.6}, outside the \
marched rows",
stripe.edge.id
)));
}
let first_point = stripe.rows.cr.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?;
let second_point = stripe.rows.cs.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?;
let center = stripe
.rows
.center
.as_ref()
.ok_or(KernelRefusal::internal(KernelStage::Refine, "centre_path", "blend network: stripe rows carry no centre path"))?
.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?;
let normal = tangent_away_from(stripe.edge, vertex)?;
let planar = [first_point, second_point, center].iter().all(|point| {
point.sub(sharp_point).dot(normal).abs() <= 1e-5 * (1.0 + radius)
});
let straight_legs = [(first_point, &stripe.first), (second_point, &stripe.second)]
.into_iter().all(|(point, mate)| {
crate::project_point_to_surface(&mate.face.surface, point.add(sharp_point).scale(0.5))
.is_ok_and(|projection| projection.distance <= 1e-6 * (1.0 + radius))
});
let section = if planar && straight_legs {
None
} else {
let first_boundary = boundary_edge_at_vertex(solid, stripe.first.face, vertex, stripe.edge.id)?;
let second_boundary = boundary_edge_at_vertex(solid, stripe.second.face, vertex, stripe.edge.id)?;
if first_boundary == second_boundary {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"cap_continuous_rim",
"blend network: curved cap on a split continuous rim requires the tangent-chain lane",
));
}
Some(cap::plane_section(stripe, sharp_point, normal, station, at_start)?)
};
let (cr_parameter, cs_parameter, first_point, second_point) = match §ion {
Some(section) => (section.cr, section.cs, section.first, section.second),
None => (station, station, first_point, second_point),
};
let sharp_vertex = vertex;
let first_vertex = take_id();
result.vertices.push(VertexRecord {
id: first_vertex,
point: first_point,
});
let second_vertex = take_id();
result.vertices.push(VertexRecord {
id: second_vertex,
point: second_point,
});
let walk_first_to_second = stripe.walk_first_to_second(at_start);
let (from, to, from_vertex, to_vertex) = if walk_first_to_second {
(first_point, second_point, first_vertex, second_vertex)
} else {
(second_point, first_point, second_vertex, first_vertex)
};
let arc = if let Some(section) = §ion {
if walk_first_to_second {
section.curve.clone()
} else {
section.curve.reversed().or_refuse(KernelStage::Refine, "reversed")?
}
} else {
corner_section_arc(center, radius, from, to)?
};
let arc_domain = arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let arc_edge_id = take_id();
result.edges.push(EdgeRecord {
id: arc_edge_id,
curve: arc,
t0: arc_domain[0],
t1: arc_domain[1],
start_vertex_id: from_vertex,
end_vertex_id: to_vertex,
degenerate: false,
name: None,
});
let arc_blend_pcurve = if let Some(section) = §ion {
if walk_first_to_second {
section.pcurve.clone()
} else {
section.pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
}
} else if walk_first_to_second {
crate::sweep_topology::parameter_line(station, 0.0, station, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(station, 1.0, station, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
let mut leg = |rim_point: Vec3,
rim_vertex: u64,
face: &FaceRecord,
curved: Option<&(NurbsCurve, NurbsCurve)>|
-> Result<(u64, NurbsCurve), KernelRefusal> {
let id = take_id();
result.edges.push(EdgeRecord {
id,
curve: match curved {
Some((curve, _)) => curve.clone(),
None => crate::make_line(rim_point, sharp_point).or_refuse(KernelStage::Refine, "make_line")?,
},
t0: 0.0,
t1: 1.0,
start_vertex_id: rim_vertex,
end_vertex_id: sharp_vertex,
degenerate: false,
name: None,
});
if let Some((_, pcurve)) = curved {
return Ok((id, pcurve.clone()));
}
let a = crate::project_point_to_surface(&face.surface, rim_point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let b = crate::project_point_to_surface(&face.surface, sharp_point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
Ok((id, crate::sweep_topology::parameter_line(a.u, a.v, b.u, b.v).or_refuse(KernelStage::Refine, "parameter_line")?))
};
let first_leg = leg(first_point, first_vertex, stripe.first.face, section.as_ref().map(|s| &s.legs[0]))?;
let second_leg = leg(second_point, second_vertex, stripe.second.face, section.as_ref().map(|s| &s.legs[1]))?;
let plane_u = second_point.sub(first_point).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let plane_v = normal.cross(plane_u).normalized().or_refuse(KernelStage::Refine, "normalized")?;
pending.push(PendingCap {
plane_origin: sharp_point,
plane_u,
plane_v,
plane_normal: normal,
arc: (arc_edge_id, from_vertex, to_vertex),
first_leg: (first_leg.0, first_vertex, sharp_vertex),
second_leg: (second_leg.0, second_vertex, sharp_vertex),
outward: normal.scale(-1.0),
name: stripe.name.clone(),
});
Ok((
CapEnd {
cr_parameter,
cs_parameter,
first_vertex,
second_vertex,
arc_edge_id,
arc_blend_pcurve,
first_leg,
second_leg,
sharp_vertex,
},
section.is_some(),
))
}
fn sew_cap(
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
cap: PendingCap,
) -> Result<(), KernelRefusal> {
let reach = {
let a = vertex_point_of(result, cap.first_leg.1)?;
let b = vertex_point_of(result, cap.second_leg.1)?;
a.sub(cap.plane_origin)
.length()
.max(b.sub(cap.plane_origin).length())
.max(1e-6)
* 4.0
};
let origin = cap
.plane_origin
.sub(cap.plane_u.scale(reach))
.sub(cap.plane_v.scale(reach));
let plane = crate::make_plane(origin, cap.plane_u, cap.plane_v, 2.0 * reach, 2.0 * reach).or_refuse(KernelStage::Refine, "make_plane")?;
let uv_of = |point: Vec3| -> Result<(f64, f64), KernelRefusal> {
let projection = crate::project_point_to_surface(&plane, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
Ok((projection.u, projection.v))
};
let edge_by_id = |id: u64| -> Result<&EdgeRecord, KernelRefusal> {
result
.edges
.iter()
.find(|edge| edge.id == id)
.ok_or_else(|| format!("blend network: cap edge {id} missing")).or_refuse(KernelStage::Refine, "ok_or_else")
};
let (arc_id, arc_start, arc_end) = cap.arc;
let leg_to = |rim: u64| -> Result<(u64, u64, u64), KernelRefusal> {
for leg in [cap.first_leg, cap.second_leg] {
if leg.1 == rim {
return Ok(leg);
}
}
Err(KernelRefusal::internal(KernelStage::Sew, "cap_leg", "blend network: cap leg for the rim is missing"))
};
let leg_in = leg_to(arc_end)?; let leg_out = leg_to(arc_start)?; let mapped_pcurve = |id: u64| -> Result<NurbsCurve, KernelRefusal> {
let edge = edge_by_id(id)?;
let mut mapped = Vec::with_capacity(edge.curve.control_points.len());
for control in &edge.curve.control_points {
let point = control.point().or_refuse(KernelStage::Refine, "point")?;
let (u, v) = uv_of(point)?;
mapped.push(crate::Vec4 { x: u * control.w, y: v * control.w, z: 0.0, w: control.w });
}
NurbsCurve::new(edge.curve.degree, edge.curve.knots.clone(), mapped)
.or_refuse(KernelStage::Refine, "new")
};
let coedges = vec![
CoedgeRecord {
id: take_id(),
edge_id: leg_in.0,
forward: false,
pcurve: mapped_pcurve(leg_in.0)?.reversed().or_refuse(KernelStage::Refine, "reversed")?,
},
CoedgeRecord {
id: take_id(),
edge_id: arc_id,
forward: false,
pcurve: mapped_pcurve(arc_id)?.reversed().or_refuse(KernelStage::Refine, "reversed")?,
},
CoedgeRecord {
id: take_id(),
edge_id: leg_out.0,
forward: true,
pcurve: mapped_pcurve(leg_out.0)?,
},
];
let plane_normal = plane.normal(0.5, 0.5).or_refuse(KernelStage::Refine, "normal")?;
let same_sense = plane_normal.dot(cap.outward) > 0.0;
let loop_id = take_id();
let face = FaceRecord {
id: take_id(),
surface: plane,
same_sense,
loops: vec![crate::topology::LoopRecord {
id: loop_id,
coedges,
}],
name: cap.name.map(|name| format!("{name}:CAP")),
};
let _ = cap.plane_normal;
let shell_index = result
.shells
.iter()
.position(|shell| !shell.faces.is_empty())
.ok_or(KernelRefusal::internal(KernelStage::Sew, "shell", "blend network: the solid has no shell to sew the cap into"))?;
result.shells[shell_index].faces.push(face);
Ok(())
}
fn vertex_point_of(solid: &BrepSolid, id: u64) -> Result<Vec3, KernelRefusal> {
solid
.vertices
.iter()
.find(|vertex| vertex.id == id)
.map(|vertex| vertex.point)
.ok_or_else(|| format!("blend network: vertex {id} missing")).or_refuse(KernelStage::Refine, "ok_or_else")
}
enum RailMiss {
OffRail(f64),
OffSpan(f64),
Projection(String),
}
fn corner_station_bar(scale: f64, radius: f64) -> f64 {
1e-6 * (1.0 + scale.max(radius))
}
fn rail_station(row: &NurbsCurve, point: Vec3, bar: f64) -> Result<f64, RailMiss> {
let projection =
crate::project_point_to_curve(row, point).map_err(RailMiss::Projection)?;
if projection.distance > bar {
return Err(RailMiss::OffRail(projection.distance));
}
if !(projection.u > RIM_MARGIN && projection.u < 1.0 - RIM_MARGIN) {
return Err(RailMiss::OffSpan(projection.u));
}
Ok(projection.u)
}
fn concave_corner_reach(
edge: &EdgeRecord,
corners: &[(u64, Vec<usize>, VertexKind<'_>)],
vertex: u64,
at_start: bool,
) -> Option<f64> {
let (_, _, kind) = corners.iter().find(|(id, _, _)| *id == vertex)?;
let corner = match kind {
VertexKind::Star { corner } | VertexKind::RibbonStar { corner } | VertexKind::Miter { corner, .. } => corner,
VertexKind::Flush { .. } | VertexKind::Reentrant { .. } | VertexKind::MixedStar(_) => return None,
};
if corner.outward {
return None;
}
let t = if at_start { edge.t0 } else { edge.t1 };
let derivatives = edge.curve.derivatives(t, 1).ok()?;
let speed = derivatives[1].length();
let tangent = derivatives[1].normalized().ok()?;
let runs_up = edge.t1 >= edge.t0;
let away = if at_start == runs_up { tangent.scale(-1.0) } else { tangent };
let beyond = corner.center.sub(derivatives[0]).dot(away);
let span = (edge.t1 - edge.t0).abs();
if !(beyond > 0.0) || !(speed > 0.0) || !(span > 0.0) {
return None;
}
Some(beyond / speed / span)
}
fn concave_corner_ladder(
edge: &EdgeRecord,
corners: &[(u64, Vec<usize>, VertexKind<'_>)],
flush_reach: f64,
) -> Vec<f64> {
let mut ladder = OVERSHOOTS.to_vec();
let needed = [(edge.start_vertex_id, true), (edge.end_vertex_id, false)]
.into_iter()
.filter_map(|(vertex, at_start)| concave_corner_reach(edge, corners, vertex, at_start))
.fold(flush_reach.max(0.0), f64::max);
let top = *OVERSHOOTS.last().unwrap_or(&0.0);
for factor in [1.25, 2.0] {
let rung = needed * factor;
if rung > top {
ladder.push(rung);
}
}
ladder
}
fn corner_rails_reach(
rows: &FittedRows,
first: &BlendMate,
second: &BlendMate,
kind: &VertexKind,
bar: f64,
) -> bool {
let reaches = |face_id: u64, point: Vec3| {
let row = if first.face.id == face_id {
&rows.cr
} else if second.face.id == face_id {
&rows.cs
} else {
return false;
};
let station = rail_station(row, point, bar);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"corner reach: face {face_id} point ({:.6}, {:.6}, {:.6}) -> {}",
point.x,
point.y,
point.z,
match &station {
Ok(u) => format!("on the rail at {u:.6}"),
Err(RailMiss::OffRail(distance)) => format!("off the rail by {distance:.3e} (bar {bar:.3e})"),
Err(RailMiss::OffSpan(u)) => format!("off the span at {u:.6}"),
Err(RailMiss::Projection(error)) => format!("projection failed: {error}"),
}
);
}
station.is_ok()
};
let on = |corner: &Corner, face_id: u64| {
corner
.tangency_on(face_id)
.map(|(point, _)| reaches(face_id, point))
.unwrap_or(false)
};
match kind {
VertexKind::Star { corner } | VertexKind::RibbonStar { corner } => {
on(corner, first.face.id) && on(corner, second.face.id)
}
VertexKind::Miter { corner, shared, .. } => on(corner, shared.id),
VertexKind::Flush { join: Some(join) } => [first.face.id, second.face.id]
.into_iter()
.all(|face_id| {
join.contact_on(face_id)
.is_some_and(|contact| reaches(face_id, contact.point))
}),
VertexKind::Reentrant { .. } | VertexKind::Flush { join: None } => true,
VertexKind::MixedStar(mixed) => {
let contact = |face_id: u64| -> Option<Vec3> {
let (a, b) = (first.face.id, second.face.id);
let pair = |x: u64, y: u64| (a == x && b == y) || (a == y && b == x);
if pair(mixed.wall_a, mixed.cap) {
Some(if face_id == mixed.wall_a { mixed.v_a } else { mixed.w_a })
} else if pair(mixed.wall_b, mixed.cap) {
Some(if face_id == mixed.wall_b { mixed.v_b } else { mixed.w_b })
} else if pair(mixed.wall_a, mixed.wall_b) {
Some(if face_id == mixed.wall_a { mixed.v_a } else { mixed.v_b })
} else {
None
}
};
[first.face.id, second.face.id]
.into_iter()
.all(|face_id| contact(face_id).is_some_and(|point| reaches(face_id, point)))
}
}
}
#[allow(clippy::too_many_arguments)]
fn sew_mixed_star(
result: &mut BrepSolid,
solid: &BrepSolid,
take_id: &mut dyn FnMut() -> u64,
stripes: &[Stripe],
sewn: &[SewnStripe],
vertex: u64,
mixed: &MixedStar,
arcs: &[EdgeRecord; 3],
[v_a, v_b, w_a, w_b]: [u64; 4],
name: Option<String>,
) -> Result<(), KernelRefusal> {
let r = mixed.radius;
let cap_rail = |index: usize| -> Result<u64, KernelRefusal> {
if stripes[index].first.face.id == mixed.cap { sewn[index].cr_edge_id } else { sewn[index].cs_edge_id }
.ok_or_else(|| KernelRefusal::unsupported(KernelStage::Sew, RAIL_COLLAPSE_WHAT, format!(
"{RAIL_COLLAPSE_UNSUPPORTED} a cap rail at the mixed star {vertex} collapsed to a point"
)))
};
let (rail_a, rail_b) = (cap_rail(mixed.a)?, cap_rail(mixed.b)?);
let cap_arc = corner_section_arc(mixed.q_w, 2.0 * r, mixed.w_a, mixed.w_b)?;
let cap_domain = cap_arc.domain().or_refuse(KernelStage::Refine, "domain")?;
let cap_arc_id = take_id();
result.edges.push(EdgeRecord {
id: cap_arc_id,
curve: cap_arc.clone(),
t0: cap_domain[0],
t1: cap_domain[1],
start_vertex_id: w_a,
end_vertex_id: w_b,
degenerate: false,
name: None,
});
let cap_forward_on_cap = {
let cap_face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == mixed.cap)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "cap_face", "blend network: the mixed star's cap is missing"))?;
let cap_pcurve = crate::pcurve::build_pcurve_on_surface(&cap_face.surface, &cap_arc).or_refuse(KernelStage::Refine, "build_pcurve")?;
let mut spliced: Option<bool> = None;
for loop_record in &mut cap_face.loops {
let count = loop_record.coedges.len();
let (Some(at_a), Some(at_b)) = (
loop_record.coedges.iter().position(|c| c.edge_id == rail_a),
loop_record.coedges.iter().position(|c| c.edge_id == rail_b),
) else {
continue;
};
let (insert_at, forward, pcurve) = if (at_a + 1) % count == at_b {
(at_a + 1, true, cap_pcurve.clone())
} else if (at_b + 1) % count == at_a {
(at_b + 1, false, cap_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?)
} else {
return Err(KernelRefusal::internal(KernelStage::Sew, "mixed_star_cap_rails", "blend network: the mixed star's two cap rails are not adjacent on the cap"));
};
loop_record.coedges.insert(insert_at, CoedgeRecord { id: take_id(), edge_id: cap_arc_id, forward, pcurve });
spliced = Some(forward);
break;
}
spliced.ok_or(KernelRefusal::internal(KernelStage::Sew, "mixed_star_cap_loop", "blend network: no cap loop holds both of the mixed star's cap rails"))?
};
let radial_a = mixed.c_a.sub(mixed.q_v).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let radial_b = mixed.c_b.sub(mixed.q_v).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let sweep = radial_a.dot(radial_b).clamp(-1.0, 1.0).acos();
if sweep < 1e-9 {
return Err(KernelRefusal::internal(KernelStage::Refine, "mixed_star_sweep", "blend network: degenerate mixed star sweep"));
}
let revolve_axis = if radial_a.cross(radial_b).dot(mixed.axis) > 0.0 { mixed.axis } else { mixed.axis.scale(-1.0) };
let generatrix = corner_section_arc(mixed.c_a, r, mixed.v_a, mixed.w_a)?;
let surface = crate::make_revolution(mixed.q_v, revolve_axis, &generatrix, sweep).or_refuse(KernelStage::Refine, "make_revolution")?;
let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Refine, "domain_u")?;
let [g0, g1] = generatrix.domain().or_refuse(KernelStage::Refine, "domain")?;
let far = surface.evaluate(u1, g1).or_refuse(KernelStage::Refine, "evaluate")?;
if far.sub(mixed.w_b).length() > 1e-6 * (1.0 + r) {
return Err(KernelRefusal::internal(KernelStage::Refine, "mixed_star_closure", format!(
"blend network: the mixed star's torus does not close on W_B ({:.3e} off)",
far.sub(mixed.w_b).length()
)));
}
let uv = |id: u64| -> Option<(f64, f64)> {
if id == v_a { Some((u0, g0)) } else if id == w_a { Some((u0, g1)) } else if id == v_b { Some((u1, g0)) } else if id == w_b { Some((u1, g1)) } else { None }
};
let cap_arc_record = result.edges.iter().find(|e| e.id == cap_arc_id).cloned().ok_or(KernelRefusal::internal(KernelStage::Sew, "mixed_star_cap_arc", "blend network: the cap arc vanished"))?;
let sides: [(EdgeRecord, bool); 4] = [
(arcs[0].clone(), false),
(arcs[1].clone(), false),
(arcs[2].clone(), false),
(cap_arc_record, !cap_forward_on_cap),
];
let walk = |edge: &EdgeRecord, forward: bool| if forward { (edge.start_vertex_id, edge.end_vertex_id) } else { (edge.end_vertex_id, edge.start_vertex_id) };
let mut order = vec![0usize];
let mut cursor = walk(&sides[0].0, sides[0].1).1;
for _ in 0..3 {
let next = (0..4)
.find(|s| !order.contains(s) && walk(&sides[*s].0, sides[*s].1).0 == cursor)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mixed_star_walk", "blend network: the blends and the cap do not walk the mixed star's boundary consistently"))?;
cursor = walk(&sides[next].0, sides[next].1).1;
order.push(next);
}
if cursor != walk(&sides[0].0, sides[0].1).0 {
return Err(KernelRefusal::internal(KernelStage::Sew, "mixed_star_loop", "blend network: the mixed star's boundary does not close"));
}
let mut coedges = Vec::with_capacity(4);
for s in order {
let (edge, forward) = &sides[s];
let (start, end) = walk(edge, *forward);
let (Some((su, sv)), Some((eu, ev))) = (uv(start), uv(end)) else {
return Err(KernelRefusal::internal(KernelStage::Sew, "mixed_star_uv", "blend network: a mixed star side ends off the sector's corners"));
};
coedges.push(CoedgeRecord {
id: take_id(),
edge_id: edge.id,
forward: *forward,
pcurve: crate::sweep_topology::parameter_line(su, sv, eu, ev).or_refuse(KernelStage::Refine, "parameter_line")?,
});
}
let (um, gm) = (0.5 * (u0 + u1), g1);
let contact = surface.evaluate(um, gm).or_refuse(KernelStage::Refine, "evaluate")?;
let cap = solid.shells.iter().flat_map(|shell| &shell.faces)
.find(|face| face.id == mixed.cap)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "cap_face", "blend network: missing mixed star cap"))?;
let projection = crate::project_point_to_surface(&cap.surface, contact).or_refuse(KernelStage::Refine, "project")?;
let cap_normal = cap.surface.normal(projection.u, projection.v).or_refuse(KernelStage::Refine, "normal")?;
let outward = if cap.same_sense { cap_normal } else { cap_normal.scale(-1.0) };
let surface_normal = surface.normal(um, gm).or_refuse(KernelStage::Refine, "normal")?;
let same_sense = surface_normal.dot(outward) > 0.0;
let loop_id = take_id();
let face = FaceRecord {
id: take_id(),
surface,
same_sense,
loops: vec![crate::topology::LoopRecord { id: loop_id, coedges }],
name,
};
let shell_index = result
.shells
.iter()
.position(|shell| !shell.faces.is_empty())
.ok_or(KernelRefusal::internal(KernelStage::Sew, "shell", "blend network: the solid has no shell to sew the mixed star into"))?;
result.shells[shell_index].faces.push(face);
Ok(())
}
fn sharp_edge_is_convex(
solid: &BrepSolid,
sharp: &EdgeRecord,
wall_a: &FaceRecord,
wall_b: &FaceRecord,
radius: f64,
) -> Result<bool, KernelRefusal> {
let _ = solid;
let t = (sharp.t0 + sharp.t1) * 0.5;
let point = sharp.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
let tangent = sharp
.curve
.unit_tangent(t, sharp.t0, sharp.t1)
.map_err(|error| format!("blend network: sharp edge {}: {error}", sharp.id)).or_refuse(KernelStage::Refine, "unit_tangent")?;
let probe = (radius * 0.25).max(1e-4);
let into_a = crate::fillet::into_face_direction(wall_a, point, tangent, point, tangent, probe)?;
let into_b = crate::fillet::into_face_direction(wall_b, point, tangent, point, tangent, probe)?;
let outward = |face: &FaceRecord| -> Result<Vec3, KernelRefusal> {
let projection = crate::project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let normal = raw_normal(&face.surface, projection.u, projection.v)?;
Ok(if face.same_sense { normal } else { normal.scale(-1.0) })
};
let (n_a, n_b) = (outward(wall_a)?, outward(wall_b)?);
Ok(into_a.dot(n_b) < -1e-9 || into_b.dot(n_a) < -1e-9)
}
pub(super) fn edge_is_convex(edge: &EdgeRecord, first: &BlendMate, second: &BlendMate) -> Result<bool, KernelRefusal> {
let t = (edge.t0 + edge.t1) * 0.5;
let mut inside = true;
for mate in [first, second] {
let uv = edge_uv_on_face(mate.coedge, edge, t)?;
let normal = raw_normal(&mate.face.surface, uv[0], uv[1])?;
let face_outward = if mate.face.same_sense { normal } else { normal.scale(-1.0) };
inside &= normal.scale(mate.rho).dot(face_outward) < 0.0;
}
Ok(inside)
}
fn corner_section_arc(
center: Vec3,
radius: f64,
from: Vec3,
to: Vec3,
) -> Result<NurbsCurve, KernelRefusal> {
let x_axis = from.sub(center).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let toward = to.sub(center).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let y_axis = toward.sub(x_axis.scale(toward.dot(x_axis))).normalized().or_refuse(KernelStage::Refine, "normalized")?;
let sweep = toward.dot(x_axis).clamp(-1.0, 1.0).acos();
if !(sweep > 1e-9) {
return Err(KernelRefusal::internal(KernelStage::Refine, "corner_section_arc", "blend network: the corner section arc is degenerate"));
}
crate::make_arc(center, x_axis, y_axis, radius, 0.0, sweep).or_refuse(KernelStage::Refine, "make_arc")
}
fn plan_pole_end(
solid: &BrepSolid,
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
stripe: &Stripe,
vertex: u64,
at_start: bool,
) -> Result<CornerEnd, KernelRefusal> {
let point = solid
.vertices
.iter()
.find(|candidate| candidate.id == vertex)
.ok_or_else(|| format!("blend network: pole vertex {vertex} missing")).or_refuse(KernelStage::Refine, "ok_or_else")?
.point;
let station = stripe
.rows
.vertex_stations
.ok_or(KernelRefusal::internal(KernelStage::Refine, "vertex_stations", "blend network: stripe rows carry no vertex stations"))?
[usize::from(!at_start)];
let [low, high] = stripe.rows.cr.domain().or_refuse(KernelStage::Refine, "domain")?;
if station != if at_start { low } else { high } {
return Err(KernelRefusal::internal(KernelStage::Refine, "pole_station", format!(
"blend network: edge {}'s pole station {station:.6} is not the end of its rows",
stripe.edge.id
)));
}
let bar = corner_station_bar(
solid
.vertices
.iter()
.fold(0.0f64, |worst, candidate| worst.max(candidate.point.length()))
.max(1.0),
stripe.first.rho.abs(),
);
for (row, which) in [(&stripe.rows.cr, "first"), (&stripe.rows.cs, "second")] {
let miss = row.evaluate(station).or_refuse(KernelStage::Refine, "evaluate")?.sub(point).length();
if miss > bar {
return Err(KernelRefusal::internal(KernelStage::Refine, "pole_rail", format!(
"blend network: edge {}'s {which} rail ends {miss:.3e} off its pole vertex {vertex}",
stripe.edge.id
)));
}
}
let pole_edge_id = take_id();
result.edges.push(EdgeRecord {
id: pole_edge_id,
curve: crate::make_line(point, point).or_refuse(KernelStage::Refine, "make_line")?,
t0: 0.0,
t1: 1.0,
start_vertex_id: vertex,
end_vertex_id: vertex,
degenerate: true,
name: None,
});
let arc_blend_pcurve = if stripe.walk_first_to_second(at_start) {
crate::sweep_topology::parameter_line(station, 0.0, station, 1.0).or_refuse(KernelStage::Refine, "parameter_line")?
} else {
crate::sweep_topology::parameter_line(station, 1.0, station, 0.0).or_refuse(KernelStage::Refine, "parameter_line")?
};
Ok(CornerEnd {
cr_parameter: station,
cs_parameter: station,
first_vertex: vertex,
second_vertex: vertex,
arc_edge_id: pole_edge_id,
arc_blend_pcurve,
})
}
struct FlushContact {
faces: Vec<u64>,
point: Vec3,
seam: Option<(u64, f64)>,
}
struct FlushJoin {
center: Vec3,
contacts: [FlushContact; 2],
}
impl FlushJoin {
fn contact_on(&self, face_id: u64) -> Option<&FlushContact> {
self.contacts.iter().find(|contact| contact.faces.contains(&face_id))
}
}
fn flush_join(
solid: &BrepSolid,
a: &(&EdgeRecord, BlendMate, BlendMate),
b: &(&EdgeRecord, BlendMate, BlendMate),
vertex_id: u64,
vertex_point: Vec3,
scale: f64,
) -> Result<Option<FlushJoin>, KernelRefusal> {
let shared: Vec<&BlendMate> = [&a.1, &a.2]
.into_iter()
.filter(|mate| [&b.1, &b.2].iter().any(|other| other.face.id == mate.face.id))
.collect();
if shared.len() == 2 {
return Ok(None);
}
let bounds = |face_id: u64, edge_id: u64| {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| face.id == face_id)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == edge_id)
};
let seam_between = |face_a: u64, face_b: u64| {
solid.edges.iter().find(|edge| {
(edge.start_vertex_id == vertex_id || edge.end_vertex_id == vertex_id)
&& edge.id != a.0.id
&& edge.id != b.0.id
&& bounds(face_a, edge.id)
&& bounds(face_b, edge.id)
})
};
let bar = corner_station_bar(scale, a.1.rho.abs());
let across = |mate: &BlendMate, seam_point: Vec3, center: Vec3| -> Result<(), KernelRefusal> {
let on = crate::project_point_to_surface(&mate.face.surface, seam_point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let seated = blend_offset(&mate.face.surface).at(on.u, on.v, mate.rho).or_refuse(KernelStage::Refine, "at")?.point;
let apart = seated.sub(center).length();
if on.distance > bar || apart > bar {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: the side faces at the smooth vertex {vertex_id} are not tangent \
across their seam — the two blends seat balls {apart:.3e} apart on it \
({:.3e} off face {}) — not a flush join",
on.distance, mate.face.id
)));
}
Ok(())
};
if let [shared] = shared.as_slice() {
let side_of = |first: &BlendMate<'_>, second: &BlendMate<'_>| -> (u64, f64) {
if first.face.id == shared.face.id {
(second.face.id, second.rho)
} else {
(first.face.id, first.rho)
}
};
let ((side_a_id, rho_a), (side_b_id, _)) = (side_of(&a.1, &a.2), side_of(&b.1, &b.2));
let side_a = if a.1.face.id == side_a_id { a.1.face } else { a.2.face };
let side_b = if b.1.face.id == side_b_id { &b.1 } else { &b.2 };
let seam = seam_between(side_a.id, side_b.face.id).ok_or_else(|| {
format!(
"blend network: the side faces at the smooth vertex {vertex_id} differ but \
share no seam edge there"
)
}).or_refuse(KernelStage::Refine, "ok_or_else")?;
let ball = solve_flush_seam_ball(
shared.face,
shared.rho,
side_a,
rho_a,
seam,
vertex_id,
vertex_point,
scale,
)?;
across(side_b, ball.seam_point, ball.center)?;
return Ok(Some(FlushJoin {
center: ball.center,
contacts: [
FlushContact {
faces: vec![shared.face.id],
point: ball.shared_point,
seam: None,
},
FlushContact {
faces: vec![side_a.id, side_b.face.id],
point: ball.seam_point,
seam: Some((seam.id, ball.seam_parameter)),
},
],
}));
}
let pair = |mate: &BlendMate| {
[&b.1, &b.2]
.into_iter()
.find_map(|other| seam_between(mate.face.id, other.face.id).map(|seam| (other, seam)))
};
let (Some((b_first, seam_first)), Some((b_second, seam_second))) = (pair(&a.1), pair(&a.2))
else {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: both side pairs differ at the smooth vertex {vertex_id}, and not \
each carrier runs into the other blend's across a seam there"
)));
};
if b_first.face.id == b_second.face.id || seam_first.id == seam_second.id {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: both side pairs differ at the smooth vertex {vertex_id}, and both \
carriers run into one face of the other blend"
)));
}
let ball = solve_flush_seam_ball(
a.2.face,
a.2.rho,
a.1.face,
a.1.rho,
seam_first,
vertex_id,
vertex_point,
scale,
)?;
let on_second = crate::project_point_to_curve(&seam_second.curve, ball.shared_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let span = seam_second.t1 - seam_second.t0;
let fraction = (on_second.u - seam_second.t0) / span;
if on_second.distance > bar {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join", format!(
"blend network: both carriers change at the smooth vertex {vertex_id}, and the ball \
crosses their seams at different sections — seated on seam edge {}, its contact on \
face {} stands {:.3e} off seam edge {} — so between the two switches it rolls on \
one face of each pair, which this join does not build",
seam_first.id, a.2.face.id, on_second.distance, seam_second.id
)));
}
if !(fraction > 1e-9 && fraction < 1.0 - 1e-9) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join_seam", format!(
"blend network: the flush join's ball at the smooth vertex {vertex_id} touches seam \
edge {} at fraction {fraction:.6}, off the edge itself",
seam_second.id
)));
}
across(b_first, ball.seam_point, ball.center)?;
across(b_second, on_second.point, ball.center)?;
Ok(Some(FlushJoin {
center: ball.center,
contacts: [
FlushContact {
faces: vec![a.1.face.id, b_first.face.id],
point: ball.seam_point,
seam: Some((seam_first.id, ball.seam_parameter)),
},
FlushContact {
faces: vec![a.2.face.id, b_second.face.id],
point: on_second.point,
seam: Some((seam_second.id, on_second.u)),
},
],
}))
}
fn section_parameter_through(
edge: &EdgeRecord,
point: Vec3,
seed: f64,
scale: f64,
) -> Result<f64, KernelRefusal> {
let mut t = seed;
let tolerance = 1e-11 * (1.0 + scale);
for _ in 0..NEWTON_ITERATIONS {
let derivatives = edge.curve.derivatives_extended(t, 2).or_refuse(KernelStage::Refine, "derivatives_extended")?;
let offset = point.sub(derivatives[0]);
let speed = derivatives[1].length();
if !(speed > 0.0) {
return Err(KernelRefusal::internal(KernelStage::Refine, "stationary_tangent", "the edge has a stationary tangent there"));
}
let plane = offset.dot(derivatives[1]) / speed;
if plane.abs() <= tolerance {
return Ok(t);
}
let slope = offset.dot(derivatives[2]) - derivatives[1].dot(derivatives[1]);
if !(slope.abs() > 0.0) {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "section_newton", "the section Newton is singular"));
}
t -= offset.dot(derivatives[1]) / slope;
}
Err(KernelRefusal::non_convergence(KernelStage::Refine, "section_newton", "the section Newton did not converge"))
}
struct SeamBall {
center: Vec3,
shared_point: Vec3,
seam_parameter: f64,
seam_point: Vec3,
}
#[allow(clippy::too_many_arguments)]
fn solve_flush_seam_ball(
shared: &FaceRecord,
rho_shared: f64,
side: &FaceRecord,
rho_side: f64,
seam: &EdgeRecord,
vertex_id: u64,
vertex_point: Vec3,
scale: f64,
) -> Result<SeamBall, KernelRefusal> {
const ITERATIONS: usize = 40;
let coedge = side
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == seam.id)
.ok_or_else(|| {
format!("blend network: the seam edge {} is not on face {}", seam.id, side.id)
}).or_refuse(KernelStage::Refine, "ok_or_else")?;
let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let at_start_of_edge = seam.start_vertex_id == vertex_id;
let from_vertex = if at_start_of_edge == coedge.forward { q0 } else { q1 };
let on_shared = crate::project_point_to_surface(&shared.surface, vertex_point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
let mut x = [from_vertex, on_shared.u, on_shared.v];
let state = |x: &[f64; 3]| -> Result<([f64; 3], Vec3, Vec3, Vec3), KernelRefusal> {
let uv = coedge.pcurve.evaluate(x[0]).or_refuse(KernelStage::Refine, "evaluate")?;
let side_offset = blend_offset(&side.surface).at(uv.x, uv.y, rho_side).or_refuse(KernelStage::Refine, "at")?;
let shared_offset = blend_offset(&shared.surface).at(x[1], x[2], rho_shared).or_refuse(KernelStage::Refine, "at")?;
let mismatch = side_offset.point.sub(shared_offset.point);
Ok((
[mismatch.x, mismatch.y, mismatch.z],
shared_offset.point,
side_offset.source,
shared_offset.source,
))
};
let tolerance = 1e-11 * (1.0 + scale);
for _ in 0..ITERATIONS {
let (residual, center, side_point, shared_point) = state(&x)?;
if residual.iter().fold(0.0f64, |worst, value| worst.max(value.abs())) <= tolerance {
let fraction = (x[0] - q0) / (q1 - q0);
if !(fraction > 1e-9 && fraction < 1.0 - 1e-9) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "flush_join_seam", format!(
"blend network: the flush join's ball at the smooth vertex {vertex_id} \
touches the seam's carrier at fraction {fraction:.6}, off the seam edge \
{} itself",
seam.id
)));
}
if vertex_point.sub(center).length() < rho_shared.abs() * (1.0 - 1e-6) {
return Err(KernelRefusal::internal(KernelStage::Refine, "flush_join_branch", format!(
"blend network: the flush join's ball at the smooth vertex {vertex_id} \
swallows the vertex — wrong branch"
)));
}
let on_seam = crate::project_point_to_curve(&seam.curve, side_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let bar = corner_station_bar(scale, rho_shared.abs());
if on_seam.distance > bar {
return Err(KernelRefusal::internal(KernelStage::Refine, "seam_pcurve", format!(
"blend network: the seam edge {}'s pcurve on face {} runs {:.3e} off the \
edge at the flush join's contact (smooth vertex {vertex_id})",
seam.id, side.id, on_seam.distance
)));
}
return Ok(SeamBall {
center,
shared_point,
seam_parameter: on_seam.u,
seam_point: on_seam.point,
});
}
let step = 1e-7;
let mut jacobian = [[0.0f64; 3]; 3];
for column in 0..3 {
let mut probe = x;
let signed = if column == 0 && x[0] + step > q0.max(q1) {
-step
} else {
step
};
probe[column] += signed;
let (probed, ..) = state(&probe)?;
for row in 0..3 {
jacobian[row][column] = (probed[row] - residual[row]) / signed;
}
}
let delta = crate::fit::solve_small::<3>(jacobian, residual, 3).map_err(|error| {
format!(
"blend network: the flush join's seam ball at the smooth vertex {vertex_id} is \
singular ({error})"
)
}).or_refuse(KernelStage::Refine, "map_err")?;
for (value, correction) in x.iter_mut().zip(delta) {
*value -= correction;
}
x[0] = x[0].clamp(q0.min(q1), q0.max(q1));
}
Err(KernelRefusal::non_convergence(KernelStage::Refine, "flush_join_ball", format!(
"blend network: the flush join's seam ball at the smooth vertex {vertex_id} did not \
converge"
)))
}
pub(in crate::blend) enum CarrierSeamRepair {
Split(CarrierSeamSplit),
Trim(CarrierSeamTrim),
}
pub(in crate::blend) struct CarrierSeamTrim {
face_id: u64,
loop_index: usize,
leaving_position: usize,
arriving_position: usize,
seam_edge: u64,
dangling_vertex: u64,
rim_vertex: u64,
level_in: f64,
level_out: f64,
period: f64,
}
pub(in crate::blend) struct CarrierSeamSplit {
face_id: u64,
loop_index: usize,
crossing_position: usize,
seam_in_position: usize,
seam_out_position: usize,
seam_edge: u64,
dangling_vertex: u64,
level_in: f64,
level_out: f64,
period: f64,
rail_edge: u64,
rail_parameter: f64,
}
fn coedge_walk(solid: &BrepSolid, coedge: &CoedgeRecord) -> Result<(u64, u64), KernelRefusal> {
let edge = solid
.edges
.iter()
.find(|edge| edge.id == coedge.edge_id)
.ok_or(KernelRefusal::internal(KernelStage::Refine, "missing_edge", "blend network: loop references a missing edge"))?;
Ok(if coedge.forward {
(edge.start_vertex_id, edge.end_vertex_id)
} else {
(edge.end_vertex_id, edge.start_vertex_id)
})
}
fn pcurve_u_range(pcurve: &NurbsCurve) -> Result<(f64, f64), KernelRefusal> {
let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
for index in 0..=SEAM_SAMPLES {
let q = q0 + (q1 - q0) * index as f64 / SEAM_SAMPLES as f64;
let u = pcurve.evaluate(q).or_refuse(KernelStage::Refine, "evaluate")?.x;
low = low.min(u);
high = high.max(u);
}
Ok((low, high))
}
const SEAM_SAMPLES: usize = 256;
const SEAM_BISECTIONS: usize = 64;
fn pcurve_u_crossing(pcurve: &NurbsCurve, level: f64) -> Result<Option<f64>, KernelRefusal> {
let [q0, q1] = pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let at = |q: f64| -> Result<f64, KernelRefusal> { Ok(pcurve.evaluate(q).or_refuse(KernelStage::Refine, "evaluate")?.x - level) };
let interior = |q: f64| (1e-9..=1.0 - 1e-9).contains(&((q - q0) / (q1 - q0)));
let mut previous = (q0, at(q0)?);
for index in 1..=SEAM_SAMPLES {
let q = q0 + (q1 - q0) * index as f64 / SEAM_SAMPLES as f64;
let value = at(q)?;
if previous.1 == 0.0 && interior(previous.0) {
return Ok(Some(previous.0));
}
if (previous.1 < 0.0) != (value < 0.0) {
let (mut low, mut high) = (previous, (q, value));
for _ in 0..SEAM_BISECTIONS {
let mid = 0.5 * (low.0 + high.0);
let value = at(mid)?;
if (low.1 < 0.0) != (value < 0.0) {
high = (mid, value);
} else {
low = (mid, value);
}
}
let crossing = 0.5 * (low.0 + high.0);
if interior(crossing) {
return Ok(Some(crossing));
}
}
previous = (q, value);
}
Ok(None)
}
pub(in crate::blend) fn plan_carrier_seam_split(
result: &BrepSolid,
rail_edges: &[u64],
face_id: u64,
) -> Result<Option<CarrierSeamRepair>, KernelRefusal> {
let face = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.ok_or(KernelRefusal::internal(
KernelStage::Sew,
"mate_face",
"blend network: mate face lost before the seam split",
))?;
if !face
.surface
.closed_directions()
.or_refuse(KernelStage::Refine, "closed_directions")?
.0
{
return Ok(None);
}
let [u0, u1] = face
.surface
.domain_u()
.or_refuse(KernelStage::Refine, "domain_u")?;
let period = u1 - u0;
if !(period > 0.0) {
return Ok(None);
}
for (loop_index, loop_record) in face.loops.iter().enumerate() {
let count = loop_record.coedges.len();
if count < 3 {
continue;
}
let mut walks = Vec::with_capacity(count);
for coedge in &loop_record.coedges {
walks.push(coedge_walk(result, coedge)?);
}
let breaks: Vec<usize> = (0..count)
.filter(|&index| walks[index].1 != walks[(index + 1) % count].0)
.collect();
if breaks.len() < 2 || breaks.len() % 2 != 0 {
continue;
}
let read = |arriving_break: usize, leaving_break: usize| {
let seam_in_position = arriving_break;
let seam_out_position = (leaving_break + 1) % count;
let seam_edge = loop_record.coedges[seam_in_position].edge_id;
let paired = loop_record.coedges[seam_out_position].edge_id == seam_edge
&& loop_record
.coedges
.iter()
.filter(|coedge| coedge.edge_id == seam_edge)
.count()
== 2
&& walks[seam_in_position].1 == walks[seam_out_position].0;
paired.then_some((seam_in_position, seam_out_position, seam_edge))
};
let mut pairs = Vec::new();
for &arriving in &breaks {
for &leaving in &breaks {
if arriving != leaving {
if let Some(pair) = read(arriving, leaving) {
pairs.push(pair);
}
}
}
}
if breaks.iter().any(|&index| {
pairs
.iter()
.filter(|&&(incoming, outgoing, _)| {
incoming == index || (outgoing + count - 1) % count == index
})
.count()
!= 1
}) {
continue;
}
for (seam_in_position, seam_out_position, seam_edge) in pairs {
let dangling_vertex = walks[seam_in_position].1;
let level_at = |position: usize| -> Result<f64, KernelRefusal> {
let coedge = &loop_record.coedges[position];
let [q0, q1] = coedge
.pcurve
.domain()
.or_refuse(KernelStage::Refine, "domain")?;
let at_walk_end = position == seam_in_position;
Ok(coedge
.pcurve
.evaluate(if at_walk_end == coedge.forward {
q1
} else {
q0
})
.or_refuse(KernelStage::Refine, "evaluate")?
.x)
};
let level_in = level_at(seam_in_position)?;
let level_out = level_at(seam_out_position)?;
let leaving_position = (seam_in_position + 1) % count;
let arriving_position = (seam_out_position + count - 1) % count;
let leaving = &loop_record.coedges[leaving_position];
let arriving = &loop_record.coedges[arriving_position];
let rim_vertex = walks[leaving_position].0;
let on_seam = if rail_edges.contains(&leaving.edge_id)
&& rail_edges.contains(&arriving.edge_id)
&& rim_vertex == walks[arriving_position].1
&& rim_vertex != dangling_vertex
{
let seam = result
.edges
.iter()
.find(|edge| edge.id == seam_edge)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "carrier_seam_edge", "blend network: the carrier seam edge vanished"))?;
let point = result
.vertices
.iter()
.find(|vertex| vertex.id == rim_vertex)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "seam_rim_vertex", "blend network: the rails' shared rim vertex vanished"))?
.point;
let off = crate::project_point_to_curve(&seam.curve, point).or_refuse(KernelStage::Refine, "project_point_to_curve")?.distance;
let reach = seam_meeting_reach(result);
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("carrier seam: rim vertex {rim_vertex} off the seam curve by {off:.3e} (reach {reach:.3e})");
}
off <= reach
} else {
false
};
if on_seam {
let walk_end_u = |coedge: &CoedgeRecord, at_end: bool| -> Result<f64, KernelRefusal> {
let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
Ok(coedge
.pcurve
.evaluate(if at_end { q1 } else { q0 })
.or_refuse(KernelStage::Refine, "evaluate")?
.x)
};
let snap_band = 1e-3 * period;
if (walk_end_u(leaving, false)? - level_in).abs() <= snap_band
&& (walk_end_u(arriving, true)? - level_out).abs() <= snap_band
{
return Ok(Some(CarrierSeamRepair::Trim(CarrierSeamTrim {
face_id,
loop_index,
leaving_position,
arriving_position,
seam_edge,
dangling_vertex,
rim_vertex,
level_in,
level_out,
period,
})));
}
}
let band = 1e-9 * period;
for (position, coedge) in loop_record.coedges.iter().enumerate() {
if !rail_edges.contains(&coedge.edge_id) {
continue;
}
let (low, high) = pcurve_u_range(&coedge.pcurve)?;
for level in [level_in, level_out] {
if low >= level - band || high <= level + band {
continue;
}
let Some(q) = pcurve_u_crossing(&coedge.pcurve, level)? else {
continue;
};
let edge = result
.edges
.iter()
.find(|edge| edge.id == coedge.edge_id)
.ok_or(KernelRefusal::internal(
KernelStage::Sew,
"crossing_rail",
"blend network: the crossing rail vanished",
))?;
let [q0, q1] = coedge
.pcurve
.domain()
.or_refuse(KernelStage::Refine, "domain")?;
let walked = (q - q0) / (q1 - q0);
let along = if coedge.forward { walked } else { 1.0 - walked };
return Ok(Some(CarrierSeamRepair::Split(CarrierSeamSplit {
face_id,
loop_index,
crossing_position: position,
seam_in_position,
seam_out_position,
seam_edge,
dangling_vertex,
level_in,
level_out,
period,
rail_edge: edge.id,
rail_parameter: edge.t0 + (edge.t1 - edge.t0) * along,
})));
}
}
}
}
Ok(None)
}
fn split_coedge(
coedge: &CoedgeRecord,
along: f64,
low_edge: u64,
high_edge: u64,
take_id: &mut dyn FnMut() -> u64,
) -> Result<[CoedgeRecord; 2], KernelRefusal> {
let [q0, q1] = coedge.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let walked = if coedge.forward { along } else { 1.0 - along };
let (first_pcurve, second_pcurve) = coedge.pcurve.split(q0 + (q1 - q0) * walked).or_refuse(KernelStage::Refine, "split")?;
let (first_edge, second_edge) = if coedge.forward {
(low_edge, high_edge)
} else {
(high_edge, low_edge)
};
Ok([
CoedgeRecord {
id: coedge.id,
edge_id: first_edge,
forward: coedge.forward,
pcurve: first_pcurve,
},
CoedgeRecord {
id: take_id(),
edge_id: second_edge,
forward: coedge.forward,
pcurve: second_pcurve,
},
])
}
pub(in crate::blend) fn apply_carrier_seam_split(
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
plan: CarrierSeamSplit,
) -> Result<[u64; 2], KernelRefusal> {
let rail = result
.edges
.iter()
.find(|edge| edge.id == plan.rail_edge)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "crossing_rail", "blend network: the crossing rail vanished before its split"))?
.clone();
let seam_curve = result
.edges
.iter()
.find(|edge| edge.id == plan.seam_edge)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "carrier_seam_edge", "blend network: the carrier seam edge vanished"))?
.curve
.clone();
let rail_parameter =
refine_seam_crossing(&rail, &seam_curve, plan.rail_parameter)?;
let span = rail.t1 - rail.t0;
let along = (rail_parameter - rail.t0) / span;
if !(1e-9..=1.0 - 1e-9).contains(&along) {
return Err(KernelRefusal::internal(KernelStage::Refine, "seam_crossing", format!(
"blend network: the seam crossing on rail {} lands at {along:.9} of its own span — \
the rail does not cross the carrier's meridian inside itself",
rail.id
)));
}
let crossing_point = rail.curve.evaluate(rail_parameter).or_refuse(KernelStage::Refine, "evaluate")?;
let (low_curve, high_curve) = rail.curve.split(rail_parameter).or_refuse(KernelStage::Refine, "split")?;
let crossing_vertex = take_id();
result.vertices.push(VertexRecord {
id: crossing_vertex,
point: crossing_point,
});
let low_edge = take_id();
let high_edge = take_id();
result.edges.push(EdgeRecord {
id: low_edge,
curve: low_curve,
t0: rail.t0,
t1: rail_parameter,
start_vertex_id: rail.start_vertex_id,
end_vertex_id: crossing_vertex,
degenerate: false,
name: rail.name.clone(),
});
result.edges.push(EdgeRecord {
id: high_edge,
curve: high_curve,
t0: rail_parameter,
t1: rail.t1,
start_vertex_id: crossing_vertex,
end_vertex_id: rail.end_vertex_id,
degenerate: false,
name: rail.name.clone(),
});
let mut spliced_elsewhere = false;
for shell in &mut result.shells {
for face in &mut shell.faces {
let on_carrier = face.id == plan.face_id;
for (loop_index, loop_record) in face.loops.iter_mut().enumerate() {
if on_carrier && loop_index == plan.loop_index {
continue;
}
let Some(position) = loop_record
.coedges
.iter()
.position(|coedge| coedge.edge_id == plan.rail_edge)
else {
continue;
};
let pieces = split_coedge(
&loop_record.coedges[position],
along,
low_edge,
high_edge,
take_id,
)?;
loop_record.coedges.splice(position..=position, pieces);
spliced_elsewhere = true;
}
}
}
if !spliced_elsewhere {
return Err(KernelRefusal::internal(KernelStage::Refine, "rail_blend_face", format!(
"blend network: rail {} is used only by the carrier — its blend face is missing",
rail.id
)));
}
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == plan.face_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend network: mate face lost during the seam split"))?;
let loop_record = &mut face.loops[plan.loop_index];
let mut pieces = split_coedge(
&loop_record.coedges[plan.crossing_position],
along,
low_edge,
high_edge,
take_id,
)?;
let centre = 0.5 * (plan.level_in + plan.level_out);
for piece in &mut pieces {
let [q0, q1] = piece.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
let middle = piece.pcurve.evaluate(0.5 * (q0 + q1)).or_refuse(KernelStage::Refine, "evaluate")?.x;
let shift = plan.period * ((middle - centre) / plan.period).round();
if shift != 0.0 {
for control in piece.pcurve.control_points.iter_mut() {
control.x -= shift * control.w;
}
}
}
let band = 1e-6 * plan.period;
let snap_band = 1e-3 * plan.period;
for (index_of_piece, at_end) in [(0usize, true), (1usize, false)] {
let controls = &mut pieces[index_of_piece].pcurve.control_points;
let index = if at_end { controls.len() - 1 } else { 0 };
let u = controls[index].x / controls[index].w;
let level = if (u - plan.level_in).abs() <= (u - plan.level_out).abs() {
plan.level_in
} else {
plan.level_out
};
if (u - level).abs() > snap_band {
return Err(KernelRefusal::internal(KernelStage::Refine, "seam_split", format!(
"blend network: the split of rail {} lands {:.3e} from the carrier's meridian \
— the crossing is not on the seam",
rail.id,
(u - level).abs()
)));
}
controls[index].x = level * controls[index].w;
}
let walk_start_u = |piece: &CoedgeRecord| -> Result<f64, KernelRefusal> {
let [q0, _] = piece.pcurve.domain().or_refuse(KernelStage::Refine, "domain")?;
Ok(piece.pcurve.evaluate(q0).or_refuse(KernelStage::Refine, "evaluate")?.x)
};
let leaving_first = (walk_start_u(&pieces[0])? - plan.level_in).abs() <= band;
let (leaving, arriving) = if leaving_first {
(pieces[0].clone(), pieces[1].clone())
} else {
(pieces[1].clone(), pieces[0].clone())
};
if (walk_start_u(&leaving)? - plan.level_in).abs() > band {
return Err(KernelRefusal::internal(KernelStage::Refine, "seam_split", format!(
"blend network: neither piece of rail {} starts on the carrier's meridian at \
{:.6}",
rail.id, plan.level_in
)));
}
let mut rebuilt = Vec::with_capacity(loop_record.coedges.len() + 1);
for (position, coedge) in loop_record.coedges.iter().enumerate() {
if position == plan.crossing_position {
continue;
}
if position == plan.seam_out_position {
rebuilt.push(arriving.clone());
}
rebuilt.push(coedge.clone());
if position == plan.seam_in_position {
rebuilt.push(leaving.clone());
}
}
loop_record.coedges = rebuilt;
let projection = crate::project_point_to_curve(&seam_curve, crossing_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let reach = crate::KernelTolerances::for_solid(result, 1e-7)
.pcurve_consistency
.max(1e-6 * (1.0 + crossing_point.length()));
if projection.distance > reach {
return Err(KernelRefusal::internal(KernelStage::Refine, "carrier_seam_edge", format!(
"blend network: the carrier seam edge {} does not pass through the rail's meridian \
crossing (off by {:.3e})",
plan.seam_edge, projection.distance
)));
}
trim_edge_at(
result,
plan.seam_edge,
projection.u,
plan.dangling_vertex,
crossing_vertex,
)?;
result.edges.retain(|edge| edge.id != plan.rail_edge);
Ok([low_edge, high_edge])
}
fn seam_meeting_reach(solid: &BrepSolid) -> f64 {
crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit
}
pub(in crate::blend) fn apply_carrier_seam_trim(
result: &mut BrepSolid,
plan: CarrierSeamTrim,
) -> Result<(), KernelRefusal> {
let seam_curve = result
.edges
.iter()
.find(|edge| edge.id == plan.seam_edge)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "carrier_seam_edge", "blend network: the carrier seam edge vanished"))?
.curve
.clone();
let rim_point = result
.vertices
.iter()
.find(|vertex| vertex.id == plan.rim_vertex)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "seam_rim_vertex", "blend network: the rails' shared rim vertex vanished before the seam trim"))?
.point;
let projection = crate::project_point_to_curve(&seam_curve, rim_point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let reach = seam_meeting_reach(result);
if projection.distance > reach {
return Err(KernelRefusal::internal(KernelStage::Refine, "carrier_seam_edge", format!(
"blend network: the carrier seam edge {} does not pass through the rim vertex its \
rails meet on (off by {:.3e})",
plan.seam_edge, projection.distance
)));
}
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == plan.face_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend network: mate face lost during the seam trim"))?;
let loop_record = &mut face.loops[plan.loop_index];
let snap_band = 1e-3 * plan.period;
for (position, at_end, level) in [
(plan.leaving_position, false, plan.level_in),
(plan.arriving_position, true, plan.level_out),
] {
let controls = &mut loop_record.coedges[position].pcurve.control_points;
let index = if at_end { controls.len() - 1 } else { 0 };
let u = controls[index].x / controls[index].w;
if (u - level).abs() > snap_band {
return Err(KernelRefusal::internal(KernelStage::Refine, "seam_trim", format!(
"blend network: the rail of edge {} ends {:.3e} from the carrier's meridian — \
the rails do not meet on the seam",
loop_record.coedges[position].edge_id,
(u - level).abs()
)));
}
controls[index].x = level * controls[index].w;
}
trim_edge_at(
result,
plan.seam_edge,
projection.u,
plan.dangling_vertex,
plan.rim_vertex,
)
}
fn refine_seam_crossing(
rail: &EdgeRecord,
seam: &NurbsCurve,
seed: f64,
) -> Result<f64, KernelRefusal> {
let span = rail.t1 - rail.t0;
let distance = |t: f64| -> Result<f64, KernelRefusal> {
let point = rail.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
Ok(crate::project_point_to_curve(seam, point).or_refuse(KernelStage::Refine, "project_point_to_curve")?.distance)
};
let reach = (span.abs() * 0.05).max(1e-12);
let (mut low, mut high) = (
(seed - reach).max(rail.t0.min(rail.t1)),
(seed + reach).min(rail.t0.max(rail.t1)),
);
for _ in 0..96 {
if high - low <= 1e-15 * (1.0 + span.abs()) {
break;
}
let a = low + (high - low) / 3.0;
let b = high - (high - low) / 3.0;
if distance(a)? <= distance(b)? {
high = b;
} else {
low = a;
}
}
let refined = 0.5 * (low + high);
if distance(refined)? <= distance(seed)? {
Ok(refined)
} else {
Ok(seed)
}
}