use super::*;
pub(in crate::blend) fn march_open_stations(
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
radius_at: &dyn Fn(f64) -> f64,
overshoot_fraction: f64,
) -> Result<(Vec<Station>, [usize; 2]), String> {
let span = edge.t1 - edge.t0;
let overshoot = span * overshoot_fraction;
let radius_extent = [0.0, 0.5, 1.0]
.into_iter()
.map(|fraction| radius_at(edge.t0 + span * fraction).abs())
.fold(0.0, f64::max);
let scale = march_model_scale(&edge.curve, edge.t0, edge.t1, radius_extent)?;
crate::report_scale_migration("blend_march_open", scale, || {
let a = edge.curve.evaluate(edge.t0).unwrap_or_default();
let b = edge.curve.evaluate(edge.t0 + span * 0.5).unwrap_or_default();
a.length().max(b.length()).max(1.0)
});
let surface1 = &first.face.surface;
let surface2 = &second.face.surface;
let signs = [first.rho.signum(), second.rho.signum()];
let rho_at = |t: f64| -> [f64; 2] {
let radius = radius_at(t);
[signs[0] * radius, signs[1] * radius]
};
let mid_index = STATIONS / 2;
let (station_ts, vertex_indices): (Vec<f64>, [usize; 2]) = {
let mut ts: Vec<f64> = (0..=STATIONS)
.map(|index| {
edge.t0 - overshoot + (span + 2.0 * overshoot) * index as f64 / STATIONS as f64
})
.collect();
let mut snapped = [0usize; 2];
for (slot, target) in [edge.t0, edge.t1].into_iter().enumerate() {
let mut nearest = 0usize;
for (index, t) in ts.iter().enumerate() {
if (t - target).abs() < (ts[nearest] - target).abs() {
nearest = index;
}
}
ts[nearest] = target;
snapped[slot] = nearest;
}
(ts, snapped)
};
let station_t = |index: usize| station_ts[index];
let solve_at = |t: f64, seed: [f64; 4]| -> Result<[f64; 4], String> {
let derivatives = edge.curve.derivatives_extended(t, 1)?;
solve_station(
surface1,
surface2,
rho_at(t),
seed,
derivatives[0],
derivatives[1].normalized()?,
scale,
)
};
let mid_seed = {
let t = station_t(mid_index).clamp(edge.t0, edge.t1);
let uv1 = edge_uv_on_face(first.coedge, edge, t)?;
let uv2 = edge_uv_on_face(second.coedge, edge, t)?;
[uv1[0], uv1[1], uv2[0], uv2[1]]
};
let mut solutions = vec![[0.0f64; 4]; STATIONS + 1];
solutions[mid_index] = solve_at(station_t(mid_index), mid_seed)?;
for index in (0..mid_index).rev() {
solutions[index] = solve_at(station_t(index), solutions[index + 1])?;
}
for index in mid_index + 1..=STATIONS {
solutions[index] = solve_at(station_t(index), solutions[index - 1])?;
}
let mut stations = Vec::with_capacity(STATIONS + 1);
for (index, uv) in solutions.iter().enumerate() {
let t = station_t(index);
let derivatives = edge.curve.derivatives_extended(t, 1)?;
let section_point = derivatives[0];
let section_tangent = derivatives[1].normalized()?;
let (_, p1, p2, center) = tangency_residual(
surface1,
surface2,
rho_at(t),
*uv,
section_point,
section_tangent,
)?;
let n1 = raw_normal(surface1, uv[0], uv[1])?;
let n2 = raw_normal(surface2, uv[2], uv[3])?;
let cos_alpha = signs[0] * signs[1] * n1.dot(n2);
let weight = ((1.0 + cos_alpha) * 0.5).max(0.0).sqrt();
if weight <= 1e-6 {
return Err("blend: faces are tangent at a station (ฮฑ = ฯ)".into());
}
let apex = apex_point(p1, n1, p2, n2, center)?;
stations.push(Station {
uv1: [uv[0], uv[1]],
uv2: [uv[2], uv[3]],
p1,
p2,
center,
weight,
apex,
});
}
Ok((stations, vertex_indices))
}
pub(in crate::blend) fn vertex_stations(
parameters: &[f64],
vertex_indices: [usize; 2],
) -> Option<[f64; 2]> {
if parameters.len() < 2 || vertex_indices[0] == vertex_indices[1] {
return None;
}
Some([
*parameters.get(vertex_indices[0])?,
*parameters.get(vertex_indices[1])?,
])
}
pub(in crate::blend) fn fit_open_rows(
stations: &[Station],
parameters: &[f64],
chamfer: bool,
) -> Result<FittedRows, String> {
let mut samples_cr = Vec::new();
let mut samples_mid = Vec::new();
let mut samples_cs = Vec::new();
let mut samples_uv1 = Vec::new();
let mut samples_uv2 = Vec::new();
let mut samples_center = Vec::new();
for station in stations {
samples_cr.push(Vec4::from_point(station.p1, 1.0));
samples_cs.push(Vec4::from_point(station.p2, 1.0));
samples_center.push(Vec4::from_point(station.center, 1.0));
samples_mid.push(Vec4 {
x: station.apex.x * station.weight,
y: station.apex.y * station.weight,
z: station.apex.z * station.weight,
w: station.weight,
});
samples_uv1.push(Vec4::from_point(
Vec3::new(station.uv1[0], station.uv1[1], 0.0),
1.0,
));
samples_uv2.push(Vec4::from_point(
Vec3::new(station.uv2[0], station.uv2[1], 0.0),
1.0,
));
}
let cr_pcurve = fit::interpolate_homogeneous(&samples_uv1, FIT_DEGREE, parameters)?;
let cs_pcurve = fit::interpolate_homogeneous(&samples_uv2, FIT_DEGREE, parameters)?;
if let Some(rows) = exact_extrusion_rows(stations, parameters, chamfer, cr_pcurve.clone(), cs_pcurve.clone())? {
return Ok(rows);
}
let cr = fit::interpolate_homogeneous(&samples_cr, FIT_DEGREE, parameters)?;
let cs = fit::interpolate_homogeneous(&samples_cs, FIT_DEGREE, parameters)?;
let mid = if chamfer {
None
} else {
Some(fit::interpolate_homogeneous(
&samples_mid,
FIT_DEGREE,
parameters,
)?)
};
let u_domain = cr.domain()?;
let surface = crate::blend::rows::surface_from_rows(FIT_DEGREE, &cr, &cs, mid.as_ref(), false)?;
let center = fit::interpolate_homogeneous(&samples_center, FIT_DEGREE, parameters)?;
Ok(FittedRows {
surface,
cr,
cs,
cr_pcurve,
cs_pcurve,
u_domain,
center: Some(center),
exact_extrusion: false,
vertex_stations: None,
})
}
fn exact_extrusion_rows(
stations: &[Station],
parameters: &[f64],
chamfer: bool,
cr_pcurve: NurbsCurve,
cs_pcurve: NurbsCurve,
) -> Result<Option<FittedRows>, String> {
let (Some(first), Some(last)) = (stations.first(), stations.last()) else {
return Ok(None);
};
if stations.len() < 3 {
return Ok(None);
}
let travel = last.center.sub(first.center);
let length = travel.length();
if !(length > 1e-12) {
return Ok(None);
}
let direction = travel.scale(1.0 / length);
let scale = stations
.iter()
.fold(0.0f64, |worst, station| worst.max(station.center.length()))
.max(1.0);
let bar = 1e-9 * (1.0 + scale);
for (station, parameter) in stations.iter().zip(parameters) {
let shift = station.center.sub(first.center);
if shift.sub(direction.scale(shift.dot(direction))).length() > bar
|| (shift.dot(direction) - parameter * length).abs() > bar
{
return Ok(None);
}
let translate = |point: Vec3| point.add(shift);
if station.p1.sub(translate(first.p1)).length() > bar
|| station.p2.sub(translate(first.p2)).length() > bar
|| station.apex.sub(translate(first.apex)).length() > bar
|| (station.weight - first.weight).abs() > 1e-12
{
return Ok(None);
}
}
let section = |z_index: usize, station: &Station| -> Vec4 {
match z_index {
0 => Vec4::from_point(station.p1, 1.0),
1 if !chamfer => Vec4 {
x: station.apex.x * station.weight,
y: station.apex.y * station.weight,
z: station.apex.z * station.weight,
w: station.weight,
},
_ => Vec4::from_point(station.p2, 1.0),
}
};
let columns = if chamfer { 2 } else { 3 };
let control = vec![
(0..columns).map(|j| section(j, first)).collect::<Vec<_>>(),
(0..columns).map(|j| section(j, last)).collect::<Vec<_>>(),
];
let (degree_v, knots_v) = crate::blend::rows::cross_section_basis(chamfer);
let surface = NurbsSurface::new(1, degree_v, vec![0.0, 0.0, 1.0, 1.0], knots_v, control)?;
let cr = crate::make_line(first.p1, last.p1)?;
let cs = crate::make_line(first.p2, last.p2)?;
let center = crate::make_line(first.center, last.center)?;
Ok(Some(FittedRows {
surface,
cr,
cs,
cr_pcurve,
cs_pcurve,
u_domain: [0.0, 1.0],
center: Some(center),
exact_extrusion: true,
vertex_stations: None,
}))
}
pub(in crate::blend) struct EndSurgery {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_edge_id: u64,
pub(in crate::blend) first_edge_parameter: f64,
pub(in crate::blend) second_edge_id: u64,
pub(in crate::blend) second_edge_parameter: f64,
pub(in crate::blend) end_face_id: u64,
pub(in crate::blend) transverse_curve: NurbsCurve,
pub(in crate::blend) transverse_blend_pcurve: NurbsCurve,
pub(in crate::blend) transverse_end_pcurve: NurbsCurve,
pub(in crate::blend) escalated: bool,
}
pub(in crate::blend) enum EndPlan {
Free(EndSurgery),
Corner(CornerEnd),
Miter(MiterEnd),
Cap(CapEnd),
}
pub(in crate::blend) struct CapEnd {
pub(in crate::blend) station: f64,
pub(in crate::blend) first_vertex: u64,
pub(in crate::blend) second_vertex: u64,
pub(in crate::blend) arc_edge_id: u64,
pub(in crate::blend) arc_blend_pcurve: NurbsCurve,
pub(in crate::blend) first_leg: (u64, NurbsCurve),
pub(in crate::blend) second_leg: (u64, NurbsCurve),
pub(in crate::blend) sharp_vertex: u64,
}
pub(in crate::blend) struct MiterEnd {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_vertex: u64,
pub(in crate::blend) second_vertex: u64,
pub(in crate::blend) edges: Vec<(u64, bool, NurbsCurve)>,
}
pub(in crate::blend) struct CornerEnd {
pub(in crate::blend) cr_parameter: f64,
pub(in crate::blend) cs_parameter: f64,
pub(in crate::blend) first_vertex: u64,
pub(in crate::blend) second_vertex: u64,
pub(in crate::blend) arc_edge_id: u64,
pub(in crate::blend) arc_blend_pcurve: NurbsCurve,
}
impl EndPlan {
pub(in crate::blend) fn cr_parameter(&self) -> f64 {
match self {
EndPlan::Free(end) => end.cr_parameter,
EndPlan::Corner(end) => end.cr_parameter,
EndPlan::Miter(end) => end.cr_parameter,
EndPlan::Cap(end) => end.station,
}
}
pub(in crate::blend) fn cs_parameter(&self) -> f64 {
match self {
EndPlan::Free(end) => end.cs_parameter,
EndPlan::Corner(end) => end.cs_parameter,
EndPlan::Miter(end) => end.cs_parameter,
EndPlan::Cap(end) => end.station,
}
}
pub(in crate::blend) fn free(&self) -> Option<&EndSurgery> {
match self {
EndPlan::Free(end) => Some(end),
EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => None,
}
}
fn planned_rims(&self) -> Option<(u64, u64)> {
match self {
EndPlan::Free(_) => None,
EndPlan::Corner(end) => Some((end.first_vertex, end.second_vertex)),
EndPlan::Miter(end) => Some((end.first_vertex, end.second_vertex)),
EndPlan::Cap(end) => Some((end.first_vertex, end.second_vertex)),
}
}
pub(in crate::blend) fn planned_rim(&self, side_first: bool) -> Option<u64> {
self.planned_rims()
.map(|(first, second)| if side_first { first } else { second })
}
}
pub(in crate::blend) struct SewnStripe {
pub(in crate::blend) cr_edge_id: u64,
pub(in crate::blend) cs_edge_id: u64,
pub(in crate::blend) blend_face_id: u64,
}
pub(in crate::blend) enum RimResolution {
Fresh,
Consumed(u64),
Corner(u64),
}
impl RimResolution {
pub(in crate::blend) fn existing(&self) -> Option<u64> {
match self {
RimResolution::Fresh => None,
RimResolution::Consumed(id) | RimResolution::Corner(id) => Some(*id),
}
}
pub(in crate::blend) fn is_consumed(&self) -> bool {
matches!(self, RimResolution::Consumed(_))
}
pub(in crate::blend) fn trims_boundary(&self) -> bool {
matches!(self, RimResolution::Fresh)
}
}
pub(in crate::blend) fn resolve_free_end(
solid: &BrepSolid,
edge_id: u64,
first: &BlendMate,
second: &BlendMate,
rows: &FittedRows,
vertex: u64,
at_start: bool,
) -> Result<(EndSurgery, bool), String> {
let first_face = first.face;
let second_face = second.face;
let first_edge_id = boundary_edge_at_vertex(solid, first_face, vertex, edge_id)?;
let second_edge_id = boundary_edge_at_vertex(solid, second_face, vertex, edge_id)?;
if first_edge_id == second_edge_id {
return Err("blend: mates share their end boundary edge (unsupported)".into());
}
let end_face = end_face_id(
solid,
first_face.id,
second_face.id,
first_edge_id,
second_edge_id,
)?;
let first_boundary = solid
.edges
.iter()
.find(|candidate| candidate.id == first_edge_id)
.ok_or("blend: end boundary edge missing")?;
let second_boundary = solid
.edges
.iter()
.find(|candidate| candidate.id == second_edge_id)
.ok_or("blend: end boundary edge missing")?;
let first_boundary_coedge = first_face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == first_edge_id)
.ok_or("blend: mate does not use its end boundary edge")?;
let second_boundary_coedge = second_face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.find(|coedge| coedge.edge_id == second_edge_id)
.ok_or("blend: mate does not use its end boundary edge")?;
let (cr_parameter, first_edge_parameter, cr_escalated) =
match support_crossing(solid, &rows.cr, first_boundary, at_start) {
Ok((s, t, _, escalated)) => (s, t, escalated),
Err(_) => support_crossing_uv(
solid,
&rows.cr,
&rows.cr_pcurve,
first_boundary,
first_boundary_coedge,
at_start,
)?,
};
let (cs_parameter, second_edge_parameter, cs_escalated) =
match support_crossing(solid, &rows.cs, second_boundary, at_start) {
Ok((s, t, _, escalated)) => (s, t, escalated),
Err(_) => support_crossing_uv(
solid,
&rows.cs,
&rows.cs_pcurve,
second_boundary,
second_boundary_coedge,
at_start,
)?,
};
let in_range = if at_start {
cr_parameter >= RIM_MARGIN && cs_parameter >= RIM_MARGIN
} else {
cr_parameter <= 1.0 - RIM_MARGIN && cs_parameter <= 1.0 - RIM_MARGIN
};
let end_record = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == end_face)
.ok_or("blend: end face missing")?;
let clamp01 = |value: f64| value.clamp(0.0, 1.0);
let (transverse, blend_pcurve, end_pcurve) = match exact_section_transverse(
rows,
end_record,
clamp01(cr_parameter),
clamp01(cs_parameter),
)? {
Some(exact) => exact,
None => transverse_curve(
rows,
end_record,
clamp01(cr_parameter),
clamp01(cs_parameter),
)?,
};
Ok((
EndSurgery {
cr_parameter,
cs_parameter,
first_edge_id,
first_edge_parameter,
second_edge_id,
second_edge_parameter,
end_face_id: end_face,
transverse_curve: transverse,
transverse_blend_pcurve: blend_pcurve,
transverse_end_pcurve: end_pcurve,
escalated: cr_escalated || cs_escalated,
},
in_range,
))
}
fn exact_section_transverse(
rows: &FittedRows,
end_face: &FaceRecord,
cr_parameter: f64,
cs_parameter: f64,
) -> Result<Option<(NurbsCurve, NurbsCurve, NurbsCurve)>, String> {
if !rows.exact_extrusion || (cr_parameter - cs_parameter).abs() > 1e-9 {
return Ok(None);
}
let Some(crate::AnalyticSurface::Plane { u_dir, v_dir, .. }) = end_face.surface.analytic()
else {
return Ok(None);
};
let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
return Ok(None);
};
let [u0, u1] = rows.u_domain;
let direction = rows
.cr
.evaluate(u1)?
.sub(rows.cr.evaluate(u0)?)
.normalized()?;
if direction.cross(normal).length() > 1e-9 {
return Ok(None);
}
let section = rows.surface.iso_curve_u(cr_parameter)?;
let [v0, v1] = section.domain()?;
let blend_pcurve =
crate::sweep_topology::parameter_line(cr_parameter, v0, cr_parameter, v1)?;
let mut mapped = Vec::with_capacity(section.control_points.len());
for control in §ion.control_points {
let point = control.point()?;
let projection = crate::project_point_to_surface(&end_face.surface, point)?;
mapped.push(Vec4 {
x: projection.u * control.w,
y: projection.v * control.w,
z: 0.0,
w: control.w,
});
}
let end_pcurve = NurbsCurve::new(section.degree, section.knots.clone(), mapped)?;
Ok(Some((section, blend_pcurve, end_pcurve)))
}
pub(in crate::blend) struct EndCorner {
pub(in crate::blend) loop_index: usize,
pub(in crate::blend) x_coedge_id: u64,
pub(in crate::blend) y_coedge_id: u64,
pub(in crate::blend) x_is_first: bool,
pub(in crate::blend) pole_coedge_ids: Vec<u64>,
pub(in crate::blend) pole_edge_ids: Vec<u64>,
}
pub(in crate::blend) fn locate_end_corner(
solid: &BrepSolid,
face: &FaceRecord,
corner: u64,
first_edge_id: u64,
second_edge_id: u64,
) -> Option<EndCorner> {
let edge_of = |coedge: &CoedgeRecord| {
solid
.edges
.iter()
.find(|candidate| candidate.id == coedge.edge_id)
};
let traversal = |coedge: &CoedgeRecord| -> Option<(u64, u64)> {
let edge = edge_of(coedge)?;
Some(if coedge.forward {
(edge.start_vertex_id, edge.end_vertex_id)
} else {
(edge.end_vertex_id, edge.start_vertex_id)
})
};
let is_pole = |coedge: &CoedgeRecord| -> bool {
edge_of(coedge).is_some_and(|edge| {
edge.degenerate && edge.start_vertex_id == corner && edge.end_vertex_id == corner
})
};
for (loop_index, loop_record) in face.loops.iter().enumerate() {
let count = loop_record.coedges.len();
for index in 0..count {
let this = &loop_record.coedges[index];
if is_pole(this) {
continue;
}
let mut pole_coedge_ids = Vec::new();
let mut pole_edge_ids = Vec::new();
let mut step = 1usize;
while step < count && is_pole(&loop_record.coedges[(index + step) % count]) {
let pole = &loop_record.coedges[(index + step) % count];
pole_coedge_ids.push(pole.id);
pole_edge_ids.push(pole.edge_id);
step += 1;
}
if step >= count {
continue;
}
let next = &loop_record.coedges[(index + step) % count];
let this_pair = (this.edge_id, next.edge_id);
if this_pair != (first_edge_id, second_edge_id)
&& this_pair != (second_edge_id, first_edge_id)
{
continue;
}
let (Some((_, this_end)), Some((next_start, _))) = (traversal(this), traversal(next))
else {
continue;
};
if this_end != corner || next_start != corner {
continue;
}
return Some(EndCorner {
loop_index,
x_coedge_id: this.id,
y_coedge_id: next.id,
x_is_first: this.edge_id == first_edge_id,
pole_coedge_ids,
pole_edge_ids,
});
}
}
None
}
pub(in crate::blend) fn splice_end_corner(
result: &mut BrepSolid,
end_face: u64,
corner: &EndCorner,
transverse: CoedgeRecord,
x_consumed: bool,
y_consumed: bool,
) -> Result<(), String> {
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == end_face)
.ok_or("blend: end face lost during surgery")?;
let loop_record = face
.loops
.get_mut(corner.loop_index)
.ok_or("blend: end-face corner loop lost during surgery")?;
let count = loop_record.coedges.len();
let seg_index = loop_record
.coedges
.iter()
.position(|coedge| coedge.id == corner.x_coedge_id)
.ok_or("blend: end-face corner coedge lost during surgery")?;
for (offset, pole) in corner.pole_coedge_ids.iter().enumerate() {
if loop_record.coedges[(seg_index + 1 + offset) % count].id != *pole {
return Err("blend: end-face corner pole no longer between the boundaries".into());
}
}
let y_index = (seg_index + 1 + corner.pole_coedge_ids.len()) % count;
if loop_record.coedges[y_index].id != corner.y_coedge_id {
return Err("blend: end-face corner pair no longer adjacent".into());
}
let mut transverse = Some(transverse);
let mut rebuilt = Vec::with_capacity(count + 1);
for index in 0..count {
let coedge = &loop_record.coedges[index];
if index == seg_index {
if !x_consumed {
rebuilt.push(coedge.clone());
}
rebuilt.push(transverse.take().expect("transverse spliced once"));
} else if index == y_index {
if !y_consumed {
rebuilt.push(coedge.clone());
}
} else if !corner.pole_coedge_ids.contains(&coedge.id) {
rebuilt.push(coedge.clone());
}
}
check_loop_branches(&face.surface, &rebuilt)?;
let loop_record = face
.loops
.get_mut(corner.loop_index)
.ok_or("blend: end-face corner loop lost during surgery")?;
loop_record.coedges = rebuilt;
Ok(())
}
fn check_loop_branches(surface: &NurbsSurface, coedges: &[CoedgeRecord]) -> Result<(), String> {
let (closed_u, closed_v) = surface.closed_directions().unwrap_or((false, false));
if !closed_u && !closed_v {
return Ok(());
}
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let periods = [
if closed_u { u1 - u0 } else { 0.0 },
if closed_v { v1 - v0 } else { 0.0 },
];
for index in 0..coedges.len() {
let this = &coedges[index];
let next = &coedges[(index + 1) % coedges.len()];
let [_, this_end] = this.pcurve.domain()?;
let [next_start, _] = next.pcurve.domain()?;
let leaving = this.pcurve.evaluate(this_end)?;
let arriving = next.pcurve.evaluate(next_start)?;
for (axis, gap) in [
(0usize, (leaving.x - arriving.x).abs()),
(1, (leaving.y - arriving.y).abs()),
] {
if periods[axis] > 0.0 && gap >= periods[axis] * 0.5 {
return Err(format!(
"blend: the end cross-section joins the end face's loop across its SEAM \
(the junction jumps {gap:.6} in the closed direction, a period of \
{:.6}) โ the blend runs past the corner into material a third face \
supplies, and closing it needs that face SPLIT along the blend, which \
the rolling-ball surgery does not do",
periods[axis]
));
}
}
}
Ok(())
}
pub(in crate::blend) const RIM_MARGIN: f64 = 2.0e-3;
const ROW_COINCIDENCE_REL: f64 = 1e-6;
pub(in crate::blend) struct RowSew {
pub(in crate::blend) edge_id: u64,
pub(in crate::blend) forward: bool,
pub(in crate::blend) collapsed_edges: Vec<u64>,
}
#[allow(clippy::too_many_arguments)]
pub(in crate::blend) fn detect_row_coincidence(
solid: &BrepSolid,
mate: &BlendMate,
blended_edge_id: u64,
boundary_start_id: u64,
boundary_finish_id: u64,
start_consumed: bool,
start_far_vertex: u64,
finish_consumed: bool,
finish_far_vertex: u64,
row: &NurbsCurve,
row_traversed_from_start: bool,
) -> Result<Option<RowSew>, String> {
if !(start_consumed && finish_consumed) {
return Ok(None);
}
if start_far_vertex == finish_far_vertex {
return Ok(None);
}
if mate.face.loops.len() != 1 || mate.loop_index != 0 {
return Ok(None);
}
let edge_by_id = |id: u64| solid.edges.iter().find(|candidate| candidate.id == id);
let vertex_point = |id: u64| {
solid
.vertices
.iter()
.find(|candidate| candidate.id == id)
.map(|vertex| vertex.point)
};
let mut sewn_coedge: Option<&CoedgeRecord> = None;
let mut leftovers: Vec<u64> = Vec::new();
for coedge in &mate.face.loops[0].coedges {
if coedge.edge_id == blended_edge_id
|| coedge.edge_id == boundary_start_id
|| coedge.edge_id == boundary_finish_id
{
continue;
}
let Some(candidate) = edge_by_id(coedge.edge_id) else {
return Err("blend: mate loop references a missing edge".into());
};
let joins = (candidate.start_vertex_id == start_far_vertex
&& candidate.end_vertex_id == finish_far_vertex)
|| (candidate.start_vertex_id == finish_far_vertex
&& candidate.end_vertex_id == start_far_vertex);
if joins {
if sewn_coedge.is_some() {
return Ok(None);
}
sewn_coedge = Some(coedge);
} else if !leftovers.contains(&candidate.id) {
leftovers.push(candidate.id);
}
}
let Some(sewn_coedge) = sewn_coedge else {
return Ok(None);
};
let Some(target) = edge_by_id(sewn_coedge.edge_id) else {
return Ok(None);
};
let (Some(start_point), Some(finish_point)) = (
vertex_point(start_far_vertex),
vertex_point(finish_far_vertex),
) else {
return Ok(None);
};
let extent = finish_point.sub(start_point).length();
let tolerances = crate::KernelTolerances::for_solid(solid, 1e-7);
let band = tolerances.heal_band(extent, ROW_COINCIDENCE_REL);
if extent <= band * 4.0 {
return Ok(None);
}
let aligned = target.start_vertex_id == start_far_vertex;
let [row_t0, row_t1] = row.domain()?;
const ROW_COINCIDENCE_SAMPLES: usize = 16;
for sample in 0..=ROW_COINCIDENCE_SAMPLES {
let fraction = sample as f64 / ROW_COINCIDENCE_SAMPLES as f64;
let on_row = row.evaluate(row_t0 + (row_t1 - row_t0) * fraction)?;
let edge_fraction = if aligned { fraction } else { 1.0 - fraction };
let on_edge = target
.curve
.evaluate(target.t0 + (target.t1 - target.t0) * edge_fraction)?;
if on_row.sub(on_edge).length() > band {
return Ok(None);
}
}
let quarter_row = row.evaluate(row_t0 + (row_t1 - row_t0) * 0.25)?;
let opposite_fraction = if aligned { 0.75 } else { 0.25 };
let quarter_opposite = target
.curve
.evaluate(target.t0 + (target.t1 - target.t0) * opposite_fraction)?;
if quarter_row.sub(quarter_opposite).length() <= band {
return Ok(None);
}
for id in &leftovers {
let Some(leftover) = edge_by_id(*id) else {
return Ok(None);
};
if leftover.start_vertex_id != leftover.end_vertex_id {
return Ok(None);
}
let a = leftover.curve.evaluate(leftover.t0)?;
let mid = leftover
.curve
.evaluate(leftover.t0 + (leftover.t1 - leftover.t0) * 0.5)?;
let b = leftover.curve.evaluate(leftover.t1)?;
if a.sub(b).length() > band || a.sub(mid).length() > band {
return Ok(None);
}
let used_elsewhere = solid.shells.iter().flat_map(|shell| &shell.faces).any(|face| {
face.id != mate.face.id
&& face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == *id)
});
if used_elsewhere {
return Ok(None);
}
}
let traversal_start = if row_traversed_from_start {
start_far_vertex
} else {
finish_far_vertex
};
let forward = target.start_vertex_id == traversal_start;
if sewn_coedge.forward != forward {
return Ok(None);
}
let mut opposing_uses = 0usize;
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if face.id == mate.face.id {
continue;
}
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
if coedge.edge_id != target.id {
continue;
}
if coedge.forward == forward {
return Ok(None);
}
opposing_uses += 1;
}
}
if opposing_uses != 1 {
return Ok(None);
}
Ok(Some(RowSew {
edge_id: target.id,
forward,
collapsed_edges: leftovers,
}))
}
pub(in crate::blend) fn boundary_edge_at_vertex(
solid: &BrepSolid,
face: &FaceRecord,
vertex: u64,
exclude: u64,
) -> Result<u64, String> {
let edge_at = |edge_id: u64| -> Option<&EdgeRecord> {
solid.edges.iter().find(|edge| edge.id == edge_id)
};
for loop_record in &face.loops {
let count = loop_record.coedges.len();
if count == 0 {
continue;
}
for index in 0..count {
if loop_record.coedges[index].edge_id != exclude {
continue;
}
for step in [count - 1, 1] {
let neighbour = &loop_record.coedges[(index + step) % count];
if neighbour.edge_id == exclude {
continue;
}
if let Some(edge) = edge_at(neighbour.edge_id) {
if edge.start_vertex_id == vertex || edge.end_vertex_id == vertex {
return Ok(edge.id);
}
}
}
}
}
let mut found = None;
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
if coedge.edge_id == exclude {
continue;
}
let edge = edge_at(coedge.edge_id).ok_or("blend: loop references missing edge")?;
if edge.start_vertex_id == vertex || edge.end_vertex_id == vertex {
if found.is_some() && found != Some(edge.id) {
return Err("blend: multiple boundary edges at the end vertex".into());
}
found = Some(edge.id);
}
}
}
found.ok_or_else(|| "blend: no boundary edge at the end vertex".to_string())
}
pub(in crate::blend) fn end_face_id(
solid: &BrepSolid,
first_face: u64,
second_face: u64,
first_edge: u64,
second_edge: u64,
) -> Result<u64, String> {
let mut found = None;
for shell in &solid.shells {
for face in &shell.faces {
if face.id == first_face || face.id == second_face {
continue;
}
let uses_first = face
.loops
.iter()
.flat_map(|l| &l.coedges)
.any(|coedge| coedge.edge_id == first_edge);
let uses_second = face
.loops
.iter()
.flat_map(|l| &l.coedges)
.any(|coedge| coedge.edge_id == second_edge);
if uses_first && uses_second {
if found.is_some() {
return Err("blend: ambiguous end face".into());
}
found = Some(face.id);
}
}
}
found.ok_or_else(|| "blend: no single end face across the corner".to_string())
}
pub(in crate::blend) fn support_crossing_uv(
solid: &BrepSolid,
support: &NurbsCurve,
support_pcurve: &NurbsCurve,
boundary: &EdgeRecord,
boundary_coedge: &CoedgeRecord,
at_start: bool,
) -> Result<(f64, f64, bool), String> {
let [s0, s1] = support_pcurve.domain()?;
let [q0, q1] = boundary_coedge.pcurve.domain()?;
let seed_s = if at_start { s0 } else { s1 };
let mut solved = None;
for seed_fraction in [0.0f64, 1.0, 0.5] {
let mut x = [seed_s, q0 + (q1 - q0) * seed_fraction];
let mut converged = false;
for _ in 0..NEWTON_ITERATIONS {
let a = support_pcurve.evaluate_extended(x[0])?;
let b = boundary_coedge.pcurve.evaluate_extended(x[1])?;
let residual = [a.x - b.x, a.y - b.y];
if residual[0].abs().max(residual[1].abs()) <= 1e-12 {
converged = true;
break;
}
let step = 1e-8;
let a_probe = support_pcurve.evaluate_extended(x[0] + step)?;
let b_probe = boundary_coedge.pcurve.evaluate_extended(x[1] + step)?;
let jacobian = [
[(a_probe.x - a.x) / step, -(b_probe.x - b.x) / step],
[(a_probe.y - a.y) / step, -(b_probe.y - b.y) / step],
];
let determinant = jacobian[0][0] * jacobian[1][1] - jacobian[0][1] * jacobian[1][0];
if determinant.abs() <= 1e-16 {
break;
}
x[0] -= (residual[0] * jacobian[1][1] - residual[1] * jacobian[0][1]) / determinant;
x[1] -= (residual[1] * jacobian[0][0] - residual[0] * jacobian[1][0]) / determinant;
}
if converged {
let rank = (x[0] - seed_s).abs();
if solved
.map(|(existing_s, _): (f64, f64)| (existing_s - seed_s).abs() > rank)
.unwrap_or(true)
{
solved = Some((x[0], x[1]));
}
}
}
let (s, q) = solved
.ok_or_else(|| "blend: support curve does not reach the end boundary edge".to_string())?;
let span = (s1 - s0).abs();
if s < s0 - 1e-6 * span || s > s1 + 1e-6 * span {
return Err(format!(
"blend: support curve does not reach the end boundary edge (the parameter-space \
crossing lands at {s:.4}, outside the row's [{s0:.4}, {s1:.4}])"
));
}
let fraction = (q - q0) / (q1 - q0);
let fraction = if boundary_coedge.forward {
fraction
} else {
1.0 - fraction
};
let t = boundary.t0 + (boundary.t1 - boundary.t0) * fraction;
let on_support = support.evaluate_extended(s)?;
let on_boundary = boundary.curve.evaluate_extended(t)?;
let scale = boundary
.curve
.evaluate(boundary.t0)?
.length()
.max(boundary.curve.evaluate(boundary.t1)?.length())
.max(1.0);
let gap = on_support.sub(on_boundary).length();
let historical = 1e-6 * scale;
let band = crate::KernelTolerances::for_solid(solid, 1e-7).pcurve_consistency;
if gap > band.max(historical) {
return Err(format!(
"blend: support curve does not reach the end boundary edge (the parameter-space \
crossing is {gap:.3e} off in 3D โ the boundary continues the blended edge rather \
than crossing its rail)"
));
}
Ok((s, t, gap > historical))
}
pub(in crate::blend) fn support_crossing(
solid: &BrepSolid,
support: &NurbsCurve,
boundary: &EdgeRecord,
at_start: bool,
) -> Result<(f64, f64, Vec3, bool), String> {
for tolerance in support_crossing_tolerances(solid) {
if let Some((s, t, point)) = support_crossing_at(support, boundary, at_start, tolerance)? {
return Ok((s, t, point, tolerance > 1e-7));
}
}
Err("blend: support curve does not reach the end boundary edge".to_string())
}
fn support_crossing_at(
support: &NurbsCurve,
boundary: &EdgeRecord,
at_start: bool,
tolerance: f64,
) -> Result<Option<(f64, f64, Vec3)>, String> {
let hits = crate::intersect_curves(support, &boundary.curve, tolerance)?;
let [s0, s1] = support.domain()?;
let mut best: Option<(f64, f64, Vec3)> = None;
for hit in hits {
if hit.t < boundary.t0 - 1e-9 || hit.t > boundary.t1 + 1e-9 {
continue;
}
let margin = 1e-9 * (1.0 + (s1 - s0).abs());
if hit.s < s0 - margin || hit.s > s1 + margin {
continue;
}
let rank = if at_start { hit.s - s0 } else { s1 - hit.s };
if best
.map(|(existing, ..)| {
let existing_rank = if at_start {
existing - s0
} else {
s1 - existing
};
rank < existing_rank
})
.unwrap_or(true)
{
best = Some((hit.s, hit.t, hit.point));
}
}
Ok(best)
}
pub(in crate::blend) fn support_crossing_tolerances(solid: &BrepSolid) -> [f64; 3] {
let policy = crate::KernelTolerances::for_solid(solid, 1e-7);
[1e-7, policy.intersection_fit, policy.pcurve_consistency]
}
pub(in crate::blend) fn spoke_crossing(
solid: &BrepSolid,
support: &NurbsCurve,
spoke: &EdgeRecord,
vertex_point: Vec3,
) -> Result<Option<(f64, f64, bool)>, String> {
for tolerance in support_crossing_tolerances(solid) {
let hits = crate::intersect_curves(support, &spoke.curve, tolerance)?;
let mut best: Option<(f64, f64, f64)> = None;
for hit in hits {
if hit.t < spoke.t0 - 1e-9 || hit.t > spoke.t1 + 1e-9 {
continue;
}
let distance = hit.point.sub(vertex_point).length();
if best.map(|(.., known)| distance < known).unwrap_or(true) {
best = Some((hit.s, hit.t, distance));
}
}
if let Some((crossing_support, crossing_spoke, _)) = best {
return Ok(Some((crossing_support, crossing_spoke, tolerance > 1e-7)));
}
}
Ok(None)
}
#[derive(Clone, Copy, Default)]
pub(in crate::blend) struct SpokeCrossings {
escalated: bool,
}
impl SpokeCrossings {
pub(in crate::blend) fn note(&mut self, escalated: bool) {
self.escalated |= escalated;
}
pub(in crate::blend) fn gate(&self, result: BrepSolid) -> Result<BrepSolid, String> {
if !self.escalated {
return Ok(result);
}
let issues = result.validate();
if issues.is_empty() {
return Ok(result);
}
Err(format!(
"blend: chain surgery across a fit-tolerance spoke crossing did not \
produce a valid solid ({})",
issues
.first()
.map(|issue| issue.message.clone())
.unwrap_or_default()
))
}
}
pub(in crate::blend) fn transverse_curve(
rows: &FittedRows,
end_face: &FaceRecord,
cr_parameter: f64,
cs_parameter: f64,
) -> Result<(NurbsCurve, NurbsCurve, NurbsCurve), String> {
const SECTIONS: usize = 16;
let mut points3 = Vec::with_capacity(SECTIONS + 1);
let mut blend_uv = Vec::with_capacity(SECTIONS + 1);
let mut end_uv: Vec<[f64; 2]> = Vec::with_capacity(SECTIONS + 1);
let mut parameters = Vec::with_capacity(SECTIONS + 1);
for section in 0..=SECTIONS {
let z = section as f64 / SECTIONS as f64;
let mut t = cr_parameter + (cs_parameter - cr_parameter) * z;
if section > 0 && section < SECTIONS {
for _ in 0..NEWTON_ITERATIONS {
let point = rows.surface.evaluate_extended(t, z)?;
let projection = crate::project_point_to_surface(&end_face.surface, point)?;
let normal = raw_normal(&end_face.surface, projection.u, projection.v)?;
let distance = point.sub(projection.point).dot(normal);
if distance.abs() <= 1e-11 {
break;
}
let step = 1e-7;
let probe = rows.surface.evaluate_extended(t + step, z)?;
let probe_projection = crate::project_point_to_surface(&end_face.surface, probe)?;
let probe_distance = probe.sub(probe_projection.point).dot(raw_normal(
&end_face.surface,
probe_projection.u,
probe_projection.v,
)?);
let derivative = (probe_distance - distance) / step;
if derivative.abs() <= 1e-14 {
return Err("blend: transverse Newton stalled".into());
}
t -= distance / derivative;
}
}
let point = rows.surface.evaluate_extended(t, z)?;
let projection = crate::project_point_to_surface(&end_face.surface, point)?;
points3.push(Vec4::from_point(point, 1.0));
blend_uv.push(Vec4::from_point(Vec3::new(t, z, 0.0), 1.0));
end_uv.push([projection.u, projection.v]);
parameters.push(z);
}
unwrap_seam_branch(&end_face.surface, &mut end_uv)?;
let end_uv: Vec<Vec4> = end_uv
.into_iter()
.map(|uv| Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0))
.collect();
let curve = fit::interpolate_homogeneous(&points3, FIT_DEGREE, ¶meters)?;
let blend_pcurve = fit::interpolate_homogeneous(&blend_uv, FIT_DEGREE, ¶meters)?;
let end_pcurve = fit::interpolate_homogeneous(&end_uv, FIT_DEGREE, ¶meters)?;
Ok((curve, blend_pcurve, end_pcurve))
}
fn unwrap_seam_branch(surface: &NurbsSurface, samples: &mut [[f64; 2]]) -> Result<(), String> {
let (closed_u, closed_v) = surface.closed_directions().unwrap_or((false, false));
if (!closed_u && !closed_v) || samples.len() < 2 {
return Ok(());
}
let [u0, u1] = surface.domain_u()?;
let [v0, v1] = surface.domain_v()?;
let periods = [
if closed_u { u1 - u0 } else { 0.0 },
if closed_v { v1 - v0 } else { 0.0 },
];
let snap = |value: f64, reference: f64, period: f64| -> f64 {
if !(period > 0.0) {
return value;
}
let shifted = value - period * ((value - reference) / period).round();
if (shifted - reference).abs() < (value - reference).abs() {
shifted
} else {
value
}
};
let middle = samples.len() / 2;
for index in (0..middle).rev() {
for axis in 0..2 {
samples[index][axis] = snap(samples[index][axis], samples[index + 1][axis], periods[axis]);
}
}
for index in middle + 1..samples.len() {
for axis in 0..2 {
samples[index][axis] = snap(samples[index][axis], samples[index - 1][axis], periods[axis]);
}
}
Ok(())
}
pub(in crate::blend) fn trim_edge_at(
solid: &mut BrepSolid,
edge_id: u64,
parameter: f64,
old_vertex: u64,
new_vertex: u64,
) -> Result<(), String> {
let (old_t0, old_t1, trims_start) = {
let edge = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or("blend: edge to trim is missing")?;
(edge.t0, edge.t1, edge.start_vertex_id == old_vertex)
};
let fraction = (parameter - old_t0) / (old_t1 - old_t0);
let interior = (1e-9..=1.0 - 1e-9).contains(&fraction);
let extends = if trims_start {
fraction < 1e-9
} else {
fraction > 1.0 - 1e-9
};
if !interior && !extends {
return Err("blend: end trim degenerates the boundary edge".into());
}
for shell in &mut solid.shells {
for face in &mut shell.faces {
for loop_record in &mut face.loops {
for coedge in &mut loop_record.coedges {
if coedge.edge_id != edge_id {
continue;
}
let [q0, q1] = coedge.pcurve.domain()?;
let split_q = if coedge.forward {
q0 + (q1 - q0) * fraction
} else {
q1 - (q1 - q0) * fraction
};
if interior {
let keep_upper = trims_start == coedge.forward;
let (low, high) = coedge.pcurve.split(split_q)?;
coedge.pcurve = if keep_upper { high } else { low };
} else {
if coedge.pcurve.degree != 1 {
return Err("blend: cannot extend a curved boundary pcurve".into());
}
let a = coedge.pcurve.evaluate(q0)?;
let b = coedge.pcurve.evaluate(q1)?;
let direction = b.sub(a);
let extended_fraction = (split_q - q0) / (q1 - q0);
let target = a.add(direction.scale(extended_fraction));
let controls = &mut coedge.pcurve.control_points;
let end_index = if (trims_start == coedge.forward) == false {
controls.len() - 1
} else {
0
};
let w = controls[end_index].w;
controls[end_index] = crate::Vec4::from_point(target, w);
}
}
}
}
}
let edge = solid
.edges
.iter_mut()
.find(|edge| edge.id == edge_id)
.ok_or("blend: edge to trim is missing")?;
if trims_start {
edge.t0 = parameter;
edge.start_vertex_id = new_vertex;
} else {
edge.t1 = parameter;
edge.end_vertex_id = new_vertex;
}
Ok(())
}