use crate::{KernelRefusal, KernelStage, OrRefuse};
use super::*;
pub fn blend_open_edge(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let _operation = crate::blend::BlendOperation::enter();
if !(radius > 0.0) || !radius.is_finite() {
return Err(KernelRefusal::input(KernelStage::Collect, "radius", "blend: radius must be positive"));
}
blend_open_edge_impl(solid, edge_id, &|_| radius, Some(radius), chamfer, name)
}
pub(super) fn blend_open_edge_impl(
solid: &BrepSolid,
edge_id: u64,
radius_at: &dyn Fn(f64) -> f64,
constant: Option<f64>,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let edge = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| format!("blend: edge {edge_id} not found")).or_refuse(KernelStage::Refine, "ok_or_else")?;
if edge.start_vertex_id == edge.end_vertex_id {
return Err(KernelRefusal::input(KernelStage::Collect, "open_edge", "blend: blend_open_edge requires an open edge"));
}
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 mid_radius = radius_at(edge.t0 + (edge.t1 - edge.t0) * 0.5);
let (rho1, rho2) = signed_radii(
edge,
first_face,
first_coedge,
second_face,
second_coedge,
mid_radius,
)?;
let first_mate = BlendMate {
face: first_face,
coedge: first_coedge,
loop_index: first_loop,
rho: rho1,
};
let second_mate = BlendMate {
face: second_face,
coedge: second_coedge,
loop_index: second_loop,
rho: rho2,
};
let debug = std::env::var("BREP_DEBUG_BLEND_MARCH").is_ok();
if debug {
let sp = edge.curve.evaluate(edge.t0);
let ep = edge.curve.evaluate(edge.t1);
eprintln!(
"OPEN edge {} v{}->v{} first_face {} second_face {} p0={:?} p1={:?}",
edge.id,
edge.start_vertex_id,
edge.end_vertex_id,
first_face.id,
second_face.id,
sp,
ep
);
}
let rail_bar = 0.5 * crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let wall_bar = crate::KernelTolerances::for_solid(solid, 1e-7).intersection_fit;
let model = crate::KernelTolerances::for_solid(solid, 1e-7).model;
let wall_radius = constant.filter(|radius| radius.is_finite() && *radius > 0.0);
let wall_check = !chamfer && wall_radius.is_some() && crate::blend::station_refinement_on();
let compute = |overshoot_fraction: f64, station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, bool, OpenRungReading), KernelRefusal> {
let (stations, vertex_indices, refinement, dense) = march_open_stations_core(
edge,
&first_mate,
&second_mate,
radius_at,
overshoot_fraction,
[edge.t0, edge.t1],
station_count,
[false, false],
[false, false],
crate::blend::station_refinement_on().then_some(rail_bar),
if wall_check { OPEN_WALL_CENTRES } else { 0 },
true,
)?;
let parameters = station_parameters(&stations);
let rows = fit_open_rows(
&stations,
¶meters,
chamfer,
Some(vertex_indices),
extrusion_direction(edge, &first_mate, &second_mate),
)?;
crate::blend::fold::check_rows_loop(
&rows, &stations, ¶meters, Some(vertex_indices), radius_at(0.5 * (edge.t0 + edge.t1)), vec![first_mate.face.id, second_mate.face.id], crate::solid_scale(solid),
)?;
let mut ends = Vec::with_capacity(2);
let mut in_range = true;
for (vertex, at_start) in [(edge.start_vertex_id, true), (edge.end_vertex_id, false)] {
let (end, side_in_range) = resolve_free_end(
solid,
edge_id,
&first_mate,
&second_mate,
&rows,
vertex,
at_start,
)?;
if debug {
eprintln!(
" [os {overshoot_fraction:.2}] end v{vertex} at_start={at_start} end_face={} first_boundary={}(t={:.4}) second_boundary={}(t={:.4}) cr={:.4} cs={:.4} in_range={side_in_range}",
end.end_face_id,
end.first_edge_id,
end.first_edge_parameter,
end.second_edge_id,
end.second_edge_parameter,
end.cr_parameter,
end.cs_parameter,
);
}
in_range &= side_in_range;
ends.push(end);
}
let windows = [
shipped_window(¶meters, ends[0].cr_parameter, ends[1].cr_parameter),
shipped_window(¶meters, ends[0].cs_parameter, ends[1].cs_parameter),
];
let (rails, rail_samples) =
rails_on_shipped_windows(&rows, ¶meters, &stations, [&first_mate.face.surface, &second_mate.face.surface], windows);
let walls = match (wall_check, windows) {
(false, _) => Vec::new(),
(true, [Some(first), Some(second)]) => open_wall_readings_why(
&rows,
¶meters,
&stations,
[first[0].min(second[0]), first[1].max(second[1])],
&dense,
wall_radius.unwrap_or(f64::NAN),
),
(true, _) => vec![Err("no shipped window to read".to_string())],
};
let first_unread = walls.iter().find_map(|wall| wall.as_ref().err().cloned());
let walls: Vec<Option<f64>> = if wall_check && windows.iter().any(|window| window.is_none()) {
Vec::new()
} else {
walls.into_iter().map(|wall| wall.ok()).collect()
};
let reading = OpenRungReading {
station_count,
stations: stations.len(),
rails,
rail_samples,
walls,
first_unread,
refine_exit: refinement.exit,
inserted: refinement.inserted,
};
Ok((rows, ends, in_range, reading))
};
let seeded_fraction: Option<f64> = (|| {
let span = edge.t1 - edge.t0;
let mut length = 0.0f64;
let mut previous = edge.curve.evaluate(edge.t0).ok()?;
for index in 1..=16 {
let t = edge.t0 + span * index as f64 / 16.0;
let point = edge.curve.evaluate(t).ok()?;
length += point.sub(previous).length();
previous = point;
}
if !(length > 0.0) || !length.is_finite() {
return None;
}
let mid_t = edge.t0 + span * 0.5;
let uv1 = edge_uv_on_face(first_mate.coedge, edge, mid_t).ok()?;
let uv2 = edge_uv_on_face(second_mate.coedge, edge, mid_t).ok()?;
let n1 = raw_normal(&first_mate.face.surface, uv1[0], uv1[1]).ok()?;
let n2 = raw_normal(&second_mate.face.surface, uv2[0], uv2[1]).ok()?;
let cos_alpha =
(rho1.signum() * rho2.signum() * n1.dot(n2)).clamp(-1.0, 1.0);
let tan_half = ((1.0 - cos_alpha).max(0.0) / (1.0 + cos_alpha).max(1e-9)).sqrt();
let end_radius = radius_at(edge.t0).abs().max(radius_at(edge.t1).abs());
let fraction = (1.5 * end_radius * tan_half / length).clamp(0.08, 0.45);
if !fraction.is_finite() || (fraction - 0.08).abs() < 1e-12 {
return None;
}
Some(fraction)
})();
let choose = |station_count: usize| -> Result<(FittedRows, Vec<EndSurgery>, OpenRungReading), KernelRefusal> {
let mut chosen: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
let mut last_error: Option<KernelRefusal> = None;
if let Some(fraction) = seeded_fraction {
match compute(fraction, station_count) {
Ok((rows, ends, true, reading)) => chosen = Some((rows, ends, reading)),
Ok(_) => {}
Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
Err(error) => last_error = Some(error),
}
}
if chosen.is_none() {
for &overshoot_fraction in &[0.08f64, 0.16, 0.28, 0.45] {
match compute(overshoot_fraction, station_count) {
Ok((rows, ends, in_range, reading)) => {
let fallback = chosen.is_none();
if in_range {
chosen = Some((rows, ends, reading));
break;
} else if fallback {
chosen = Some((rows, ends, reading));
}
}
Err(error) if crate::blend::is_wall_fold(&error) => return Err(error),
Err(error) => last_error = Some(error),
}
}
}
chosen.ok_or_else(|| {
last_error.unwrap_or_else(|| {
KernelRefusal::internal(
KernelStage::Refine,
"open_march",
"blend: open march failed at every overshoot",
)
})
})
};
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 station_count = STATIONS;
let mut accepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
let mut unaccepted: Option<(FittedRows, Vec<EndSurgery>, OpenRungReading)> = None;
let mut request_rungs = 0usize;
let mut unmet: Option<crate::BudgetReason> = None;
loop {
if accepted.is_some() {
request_rungs += 1;
}
let chosen = choose(station_count);
let (rows, ends, reading) = match chosen {
Ok(chosen) => chosen,
Err(_) if accepted.is_some() => {
unmet = Some(crate::BudgetReason::Incoherent);
break;
}
Err(error) => return Err(error),
};
#[cfg(not(test))]
let reject_request = false;
crate::blend::carve::carve_trace(format_args!(
"blend open edge {}: {} stations a rung ({} in all), rails {:.3e} (bar {rail_bar:.3e}), wall {:.3e} (bar {wall_bar:.3e}){}",
edge.id, reading.station_count, reading.stations, reading.rails,
reading.worst_wall().unwrap_or(f64::INFINITY),
if accepted.is_some() { ", a request rung" } else { "" }
));
let accepted_now = accepts(&reading) && !reject_request;
if accepted.is_none() {
if accepted_now {
let met = !wall_check || meets_model(&reading);
accepted = Some((rows, ends, reading));
if met {
break;
}
} else if station_count < MAX_OPEN_STATIONS {
station_count *= 2;
continue;
} else {
crate::blend::record_blend_note(format!(
"blend: open edge {} at {} stations a rung ({} in all) — the top of the open march's \
ladder — still leaves its rails {:.3e} from their carriers (bar {rail_bar:.3e}) or its \
wall off the rolling ball (bar {wall_bar:.3e}); the wall ships as built, unaccepted",
edge.id, reading.station_count, reading.stations, reading.rails
));
unaccepted = Some((rows, ends, reading));
break;
}
} else {
if !accepted_now {
unmet = Some(crate::BudgetReason::Rejected);
break;
}
let met = meets_model(&reading);
accepted = Some((rows, ends, reading));
if met {
unmet = None;
break;
}
}
if station_count < MAX_OPEN_STATIONS {
station_count *= 2;
} else {
unmet = Some(crate::BudgetReason::StationCeiling);
break;
}
}
let (shipped_accepted, (rows, ends, reading)) = match (accepted, unaccepted) {
(Some(rung), _) => (true, rung),
(None, Some(rung)) => (false, rung),
(None, None) => {
return Err(KernelRefusal::internal(KernelStage::Refine, "open_ladder", "blend: the open ladder ended without a rung"));
}
};
let report: Option<(crate::BudgetReason, f64, f64, String)> = if !shipped_accepted {
Some(acceptance_deficit(&reading, rail_bar, wall_bar, wall_check, reading.rails_read(), "no rung of the open ladder was accepted"))
} else if wall_check && !meets_model(&reading) {
let reason = unmet.unwrap_or(crate::BudgetReason::StationCeiling);
let declared = reading.worst_wall().unwrap_or(f64::INFINITY);
Some((reason, model, declared, String::new()))
} else {
None
};
let [start_end, finish_end] = match <[EndSurgery; 2]>::try_from(ends) {
Ok(pair) => pair,
Err(_) => return Err(KernelRefusal::internal(KernelStage::Sew, "open_surgery_ends", "blend: open surgery needs exactly two ends")),
};
let mut crossings = SpokeCrossings::default();
crossings.note(start_end.escalated || finish_end.escalated);
let mut result = solid.clone();
let mut take_id = fresh_id_source(solid);
let sewn = build_open_surgery(
solid,
&mut result,
&mut take_id,
edge,
&first_mate,
&second_mate,
rows,
[EndPlan::Free(start_end), EndPlan::Free(finish_end)],
name,
)?;
prune_orphan_vertices(&mut result);
crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
check_snap_closure(solid, &result, sewn.snap)?;
let built = crossings.gate(result)?;
let sewn_surface = built
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == sewn.blend_face_id)
.map(|face| face.surface.clone())
.ok_or(KernelRefusal::internal(KernelStage::Sew, "open_blend_face", "blend: the sewn open blend face is not in the built body"))?;
if let Some((reason, bar, measured, deficit)) = report {
let rungs_used = if shipped_accepted { request_rungs } else { (reading.station_count / STATIONS).trailing_zeros() as usize };
let mut detail = format!(
"the shipped rung's local rail refinement ended {:?} with {} stations inserted",
reading.refine_exit, reading.inserted
);
if !deficit.is_empty() {
detail = format!("{deficit}; {detail}");
}
crate::blend::record_wall_model_report(crate::blend::WallModelReport {
name: name.map(str::to_string),
surface: sewn_surface,
request: bar,
residual: measured,
detail: Some(detail),
budget: crate::ApproximationBudget {
reason,
rounds_used: rungs_used,
rounds_limit: OPEN_REQUEST_RUNGS,
stations: reading.stations,
station_limit: REFINE_STATION_FACTOR * (MAX_OPEN_STATIONS + 1),
mechanism: crate::BudgetMechanism::StationRungs,
measured_component: crate::MeasuredComponent::Wall,
unread: 0,
},
});
}
Ok(built)
}
pub(in crate::blend) fn acceptance_deficit(
reading: &OpenRungReading,
rail_bar: f64,
wall_bar: f64,
wall_check: bool,
rails_read: bool,
what: &str,
) -> (crate::BudgetReason, f64, f64, String) {
let rails = (rails_read && reading.rails.is_finite() && reading.rails >= 0.0).then_some(reading.rails);
let wall = if wall_check { reading.worst_wall() } else { None };
let unread = rails.is_none() || (wall_check && wall.is_none());
let mut worst = (0.0_f64, rail_bar);
for (value, bar) in [(rails, rail_bar), (wall, wall_bar)] {
if let Some(value) = value {
if value / bar > worst.0 / worst.1 {
worst = (value, bar);
}
}
}
let rail_text = match rails {
Some(value) => format!("rails {value:.3e} from their carriers (bar {rail_bar:.3e})"),
None => format!(
"rails UNREAD ({:?} over {} + {} samples) against bar {rail_bar:.3e}",
reading.rails, reading.rail_samples[0], reading.rail_samples[1]
),
};
let wall_text = match (wall_check, wall) {
(false, _) => "the wall not judged (no constant-radius fillet provenance)".to_string(),
(true, Some(value)) => format!("wall {value:.3e} off the rolling ball (bar {wall_bar:.3e})"),
(true, None) => format!(
"wall UNREAD on {} of {} intervals against bar {wall_bar:.3e}{}",
reading.walls.iter().filter(|wall| !matches!(wall, Some(value) if value.is_finite())).count(),
reading.walls.len(),
reading.first_unread.as_deref().map(|why| format!(" (first: {why})")).unwrap_or_default()
),
};
let reason = if unread { crate::BudgetReason::Unread } else { crate::BudgetReason::Unaccepted };
(reason, worst.1, worst.0, format!("{what}: {rail_text}, {wall_text}"))
}
const OPEN_WALL_SECTIONS: usize = 8;
pub(in crate::blend) const OPEN_WALL_CENTRES: usize = 32;
pub(in crate::blend) const OPEN_REQUEST_RUNGS: usize = (MAX_OPEN_STATIONS / STATIONS).trailing_zeros() as usize;
#[derive(Clone, Debug)]
pub(in crate::blend) struct OpenRungReading {
pub(in crate::blend) station_count: usize,
pub(in crate::blend) stations: usize,
pub(in crate::blend) rails: f64,
pub(in crate::blend) rail_samples: [usize; 2],
pub(in crate::blend) walls: Vec<Option<f64>>,
pub(in crate::blend) first_unread: Option<String>,
pub(in crate::blend) refine_exit: crate::blend::stations::StationRefineExit,
pub(in crate::blend) inserted: usize,
}
impl OpenRungReading {
pub(in crate::blend) fn rails_read(&self) -> bool {
self.rails.is_finite() && self.rails >= 0.0 && self.rail_samples.iter().all(|&samples| samples > 0)
}
pub(in crate::blend) fn worst_wall(&self) -> Option<f64> {
if self.walls.is_empty() {
return None;
}
self.walls.iter().try_fold(0.0_f64, |worst, wall| match wall {
Some(declared) if declared.is_finite() && *declared >= 0.0 => Some(worst.max(*declared)),
_ => None,
})
}
}
pub(in crate::blend) fn shipped_window(parameters: &[f64], a: f64, b: f64) -> Option<[f64; 2]> {
let (&first, &last) = (parameters.first()?, parameters.last()?);
let (low, high) = (a.min(b).max(first), a.max(b).min(last));
(low.is_finite() && high.is_finite() && low < high).then_some([low, high])
}
fn interval_at(parameters: &[f64], u: f64) -> Option<(usize, f64)> {
let last = parameters.len().checked_sub(2)?;
let interval = parameters.partition_point(|¶meter| parameter <= u).saturating_sub(1).min(last);
let (from, to) = (parameters[interval], parameters[interval + 1]);
(to > from).then(|| (interval, ((u - from) / (to - from)).clamp(0.0, 1.0)))
}
const AFFINE_READS_DEFAULT: bool = true;
pub(in crate::blend) fn affine_reads_on() -> bool {
switch!(live "BREP_BLEND_AFFINE_READS", AFFINE_READS_DEFAULT)
}
fn affine_plane(surface: &NurbsSurface) -> Option<(Vec3, Vec3)> {
let (origin, _, _) = affine_plane_frame(surface)?;
let net = &surface.control_points;
let [p00, p01, p10] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?];
Some((origin, p10.sub(p00).cross(p01.sub(p00)).normalized().ok()?))
}
pub(in crate::blend) fn affine_plane_frame(surface: &NurbsSurface) -> Option<(Vec3, Vec3, Vec3)> {
if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || surface.degree_u != 1 || surface.degree_v != 1 {
return None;
}
let net = &surface.control_points;
if net.len() != 2 || net.iter().any(|row| row.len() != 2) {
return None;
}
let weight = net[0][0].w;
if !(weight > 0.0) || net.iter().flatten().any(|point| point.w != weight) {
return None;
}
let [p00, p01, p10, p11] = [net[0][0].point().ok()?, net[0][1].point().ok()?, net[1][0].point().ok()?, net[1][1].point().ok()?];
let scale = [p00, p01, p10, p11].iter().fold(0.0f64, |worst, point| worst.max(point.length()));
if p11.sub(p10).sub(p01.sub(p00)).length() > 1e-12 * (1.0 + scale) {
return None;
}
let [u0, u1] = surface.domain_u().ok()?;
let [v0, v1] = surface.domain_v().ok()?;
if !(u1 > u0 && v1 > v0) {
return None;
}
Some((p00, p10.sub(p00).scale(1.0 / (u1 - u0)), p01.sub(p00).scale(1.0 / (v1 - v0))))
}
fn affine_curve(curve: &NurbsCurve) -> bool {
curve.degree == 1
&& curve.control_points.len() == 2
&& curve.control_points[0].w > 0.0
&& curve.control_points[0].w == curve.control_points[1].w
}
pub(in crate::blend) fn affine_rail_standoff(rail: &NurbsCurve, carrier: &NurbsSurface, window: [f64; 2]) -> Option<f64> {
if !affine_curve(rail) {
return None;
}
let (origin, normal) = affine_plane(carrier)?;
let mut worst = 0.0f64;
for u in window {
let point = rail.evaluate(u).ok()?;
let off = point.sub(origin).dot(normal).abs();
if !off.is_finite() {
return None;
}
worst = worst.max(off);
}
Some(worst)
}
const EXACT_REVOLUTION_READS_DEFAULT: bool = true;
pub(in crate::blend) fn exact_revolution_reads_on() -> bool {
switch!(live "BREP_EXACT_REVOLUTION_READS", EXACT_REVOLUTION_READS_DEFAULT)
}
pub(in crate::blend) fn exact_revolution_read_bound(rail: &NurbsCurve, carrier: &NurbsSurface, pcurve: &NurbsCurve) -> Option<f64> {
if let Some((origin, s_u, s_v)) = affine_plane_frame(carrier) {
if pcurve.degree != rail.degree || pcurve.knots != rail.knots || pcurve.control_points.len() != rail.control_points.len() {
return None;
}
let ([u0, _], [v0, _]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
let image: Vec<Vec4> = pcurve
.control_points
.iter()
.map(|control| {
let (u, v) = (control.x / control.w, control.y / control.w);
let point = origin.add(s_u.scale(u - u0)).add(s_v.scale(v - v0));
Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w }
})
.collect();
return crate::blend::stations::net_bound(&rail.control_points, &image);
}
if !matches!(carrier.analytic(), Some(crate::AnalyticSurface::RuledRevolution { .. } | crate::AnalyticSurface::Revolution { .. } | crate::AnalyticSurface::Torus { .. } | crate::AnalyticSurface::Sphere { .. })) {
return None;
}
if !affine_curve(pcurve) {
return None;
}
let [q0, q1] = pcurve.domain().ok()?;
let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
let ([u0, u1], [v0, v1]) = (carrier.domain_u().ok()?, carrier.domain_v().ok()?);
let (circle, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x && a.y >= v0 && a.y <= v1 {
(carrier.iso_curve_v(a.y).ok()?, a.x, b.x)
} else if a.x.to_bits() == b.x.to_bits() && a.y != b.y && a.x >= u0 && a.x <= u1 {
(carrier.iso_curve_u(a.x).ok()?, a.y, b.y)
} else {
return None;
};
let [c0, c1] = circle.domain().ok()?;
let reversed = to < from;
let circle = if reversed { circle.reversed().ok()? } else { circle };
let on_curve = |along: f64| if reversed { c0 + c1 - along } else { along };
let (lo, hi) = (on_curve(from), on_curve(to));
if !(hi > lo) {
return None;
}
let segment = super::support::extend_conic_ends(&circle, lo, hi).ok()?;
let [r0, r1] = segment.domain().ok()?;
let degree = segment.degree;
let mut knots = segment.knots.clone();
for knot in knots.iter_mut() {
*knot = ((*knot - r0) / (r1 - r0)).clamp(0.0, 1.0);
}
for knot in knots.iter_mut().take(degree + 1) {
*knot = 0.0;
}
let count = knots.len();
for knot in knots.iter_mut().skip(count - degree - 1) {
*knot = 1.0;
}
if rail.degree != degree || rail.knots != knots || rail.control_points.len() != segment.control_points.len() {
return None;
}
crate::blend::stations::net_bound(&rail.control_points, &segment.control_points)
}
pub(in crate::blend) fn rails_on_shipped_windows(
rows: &FittedRows,
parameters: &[f64],
stations: &[Station],
carriers: [&NurbsSurface; 2],
windows: [Option<[f64; 2]>; 2],
) -> (f64, [usize; 2]) {
read_rail_windows([&rows.cr, &rows.cs], parameters, stations, carriers, windows,
rows.exact_extrusion && affine_reads_on(), certified_rail_bounds(rows, carriers), None)
}
fn certified_rail_bounds(rows: &FittedRows, carriers: [&NurbsSurface; 2]) -> [Option<f64>; 2] {
if rows.revolution_axis.is_none() || !exact_revolution_reads_on() {
return [None, None];
}
[
exact_revolution_read_bound(&rows.cr, carriers[0], &rows.cr_pcurve),
exact_revolution_read_bound(&rows.cs, carriers[1], &rows.cs_pcurve),
]
}
fn read_rail_windows(
rails: [&NurbsCurve; 2], parameters: &[f64], stations: &[Station],
carriers: [&NurbsSurface; 2], windows: [Option<[f64; 2]>; 2], affine: bool,
certified: [Option<f64>; 2],
mut memo: Option<&mut RailFeet>,
) -> (f64, [usize; 2]) {
let mut worst = 0.0_f64;
let mut samples = [0usize; 2];
for (side, (rail, carrier)) in [(rails[0], carriers[0]), (rails[1], carriers[1])].into_iter().enumerate() {
let Some([low, high]) = windows[side] else { continue };
if let Some(off) = affine.then(|| affine_rail_standoff(rail, carrier, [low, high])).flatten() {
crate::blend::record_count("count.read.affine_rails", 1);
samples[side] += 2;
if !worst.is_nan() && off > worst {
worst = off;
}
continue;
}
if let Some(bound) = certified[side] {
crate::blend::record_count("count.read.revolution_rails", 1);
samples[side] += 1;
if !worst.is_nan() && bound > worst {
worst = bound;
}
continue;
}
let mut at = vec![low, high];
for pair in parameters.windows(2) {
for fraction in [0.25, 0.5, 0.75] {
let u = pair[0] + (pair[1] - pair[0]) * fraction;
if u > low && u < high {
at.push(u);
}
}
}
for u in at {
let cached = memo.as_ref().and_then(|memo| memo.values[side].get(&u.to_bits()).copied());
let off = if let Some(off) = cached {
if let Some(memo) = memo.as_mut() { memo.hits += 1; }
crate::blend::record_count("count.read.rail_foot_reused", 1);
off
} else {
if let Some(memo) = memo.as_mut() {
memo.reads += 1;
crate::blend::record_count("count.read.rail_foot_fresh", 1);
}
let off = interval_at(parameters, u)
.and_then(|(interval, fraction)| {
let (left, right) = (&stations[interval], &stations[interval + 1]);
let (a, b) = if side == 0 { (left.uv1, right.uv1) } else { (left.uv2, right.uv2) };
let seed = [a[0] + fraction * (b[0] - a[0]), a[1] + fraction * (b[1] - a[1])];
let point = rail.evaluate(u).ok()?;
crate::blend::stations::extended_foot_distance(carrier, point, seed).ok()
})
.filter(|off| off.is_finite())
.unwrap_or(f64::NAN);
if off.is_finite() {
if let Some(memo) = memo.as_mut() { memo.values[side].insert(u.to_bits(), off); }
}
off
};
samples[side] += 1;
if !worst.is_nan() && (off.is_nan() || off > worst) {
worst = off;
}
}
}
(worst, samples)
}
pub(in crate::blend) struct RailWindowReader {
rails: [NurbsCurve; 2],
carriers: [NurbsSurface; 2],
parameters: Vec<f64>,
stations: Vec<Station>,
affine: bool,
certified: [Option<f64>; 2],
feet: RailFeet,
}
#[derive(Default)]
struct RailFeet {
values: [rustc_hash::FxHashMap<u64, f64>; 2],
reads: u64,
hits: u64,
}
impl RailWindowReader {
pub(in crate::blend) fn new(rows: &FittedRows, parameters: &[f64], stations: &[Station], carriers: [&NurbsSurface; 2]) -> Self {
Self {
rails: [rows.cr.clone(), rows.cs.clone()],
carriers: [carriers[0].clone(), carriers[1].clone()],
parameters: parameters.to_vec(), stations: stations.to_vec(),
affine: rows.exact_extrusion && affine_reads_on(), certified: certified_rail_bounds(rows, carriers), feet: RailFeet::default(),
}
}
pub(in crate::blend) fn read(&mut self, windows: [Option<[f64; 2]>; 2]) -> (f64, [usize; 2]) {
read_rail_windows([&self.rails[0], &self.rails[1]], &self.parameters, &self.stations,
[&self.carriers[0], &self.carriers[1]], windows, self.affine, self.certified, Some(&mut self.feet))
}
}
pub(in crate::blend) fn network_rail_reuse_on() -> bool {
std::env::var("BREP_NETWORK_RAIL_REUSE").map_or(true, |value| value != "0" && value != "off")
}
pub(in crate::blend) fn junction_stencil(
left_nodes: &[Vec3],
right_nodes: &[Vec3],
station_ts: &[f64],
left: usize,
) -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
let half = OPEN_WALL_CENTRES / 2;
if left_nodes.len() != OPEN_WALL_CENTRES + 1 || right_nodes.len() != OPEN_WALL_CENTRES + 1 || left + 2 >= station_ts.len() {
return Err(format!("interval {left}: junction needs two {}-node intervals and their three station parameters", OPEN_WALL_CENTRES + 1));
}
let (t0, t1, t2) = (station_ts[left], station_ts[left + 1], station_ts[left + 2]);
let (w_left, w_right) = (t1 - t0, t2 - t1);
if !(t0.is_finite() && t1.is_finite() && t2.is_finite() && w_left > 0.0 && w_right > 0.0 && w_left.is_finite() && w_right.is_finite()) {
return Err(format!("interval {left}: junction station parameters {t0}, {t1}, {t2} are not finite and strictly increasing"));
}
let nodes: Vec<Vec3> = left_nodes[half..].iter().chain(&right_nodes[1..=half]).copied().collect();
if w_left.to_bits() == w_right.to_bits() {
return Ok((nodes, None));
}
let abscissae: Vec<f64> = (half..=OPEN_WALL_CENTRES)
.map(|j| if j == OPEN_WALL_CENTRES { t1 } else { t0 + w_left * j as f64 / OPEN_WALL_CENTRES as f64 })
.chain((1..=half).map(|j| t1 + w_right * j as f64 / OPEN_WALL_CENTRES as f64))
.collect();
Ok((nodes, Some(abscissae)))
}
pub(in crate::blend) fn read_on_stencil(point: Vec3, nodes: &[Vec3], abscissae: Option<&[f64]>, step: usize, radius: f64) -> Result<(f64, f64), String> {
match abscissae {
None => crate::blend::chain::declared_wall_probe_why(point, nodes, step, radius),
Some(ts) => crate::blend::chain::declared_wall_probe_at_why(point, nodes, ts, step, radius),
}
}
pub(in crate::blend) fn exact_extrusion_wall_readings(
rows: &FittedRows,
parameters: &[f64],
window: [f64; 2],
radius: f64,
) -> Vec<Result<f64, String>> {
let intervals = parameters.len().saturating_sub(1);
let overlapped: Vec<usize> =
(0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
let line = match (rows.exact_extrusion, rows.center.as_ref()) {
(true, Some(center)) => center.domain().and_then(|[a, b]| Ok((center.evaluate(a)?, center.evaluate(b)?))),
(false, _) => Err("the rows are not an exact extrusion".to_string()),
(true, None) => Err("the exact rows carry no centre line".to_string()),
};
let (anchor, direction) = match line.and_then(|(a, b)| b.sub(a).normalized().map(|d| (a, d))) {
Ok(line) => line,
Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
};
let Ok([v_low, v_high]) = rows.surface.domain_v() else {
return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
};
overlapped
.into_iter()
.map(|interval| -> Result<f64, String> {
let (from, to) = (parameters[interval], parameters[interval + 1]);
let inside = |u: f64| u >= window[0] && u <= window[1];
let mut probes: Vec<f64> =
(1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
if probes.is_empty() {
return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
}
let mut worst = 0.0_f64;
for u in probes {
for k in 1..OPEN_WALL_SECTIONS {
let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
let offset = point.sub(anchor);
let standoff = offset.sub(direction.scale(offset.dot(direction))).length();
let reading = (standoff - radius).abs();
if !reading.is_finite() {
return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
}
worst = worst.max(reading);
}
}
Ok(worst)
})
.collect()
}
pub(in crate::blend) fn exact_revolution_wall_readings(
rows: &FittedRows,
parameters: &[f64],
window: [f64; 2],
radius: f64,
) -> Vec<Result<f64, String>> {
let intervals = parameters.len().saturating_sub(1);
let overlapped: Vec<usize> =
(0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
let circle = match (rows.revolution_axis, rows.center.as_ref()) {
(Some((origin, axis)), Some(center)) => center.domain().and_then(|[a, _]| center.evaluate(a)).map(|point| (origin, axis, point)),
(None, _) => Err("the rows are not an exact revolution".to_string()),
(Some(_), None) => Err("the exact rows carry no centre circle".to_string()),
};
let (origin, axis, centre_radius, centre_height) = match circle {
Ok((origin, axis, point)) => {
let offset = point.sub(origin);
let height = offset.dot(axis);
(origin, axis, offset.sub(axis.scale(height)).length(), height)
}
Err(error) => return overlapped.iter().map(|interval| Err(format!("interval {interval}: {error}"))).collect(),
};
let Ok([v_low, v_high]) = rows.surface.domain_v() else {
return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
};
overlapped
.into_iter()
.map(|interval| -> Result<f64, String> {
let (from, to) = (parameters[interval], parameters[interval + 1]);
let inside = |u: f64| u >= window[0] && u <= window[1];
let mut probes: Vec<f64> =
(1..4).map(|step| from + (to - from) * step as f64 / 4.0).filter(|u| inside(*u)).collect();
probes.extend(window.into_iter().filter(|end| *end > from && *end < to));
if probes.is_empty() {
return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
}
let mut worst = 0.0_f64;
for u in probes {
for k in 1..OPEN_WALL_SECTIONS {
let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
let offset = point.sub(origin);
let height = offset.dot(axis);
let radial = offset.sub(axis.scale(height)).length();
let standoff = (radial - centre_radius).hypot(height - centre_height);
let reading = (standoff - radius).abs();
if !reading.is_finite() {
return Err(format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) reads {reading}"));
}
worst = worst.max(reading);
}
}
Ok(worst)
})
.collect()
}
pub(in crate::blend) fn open_wall_readings_why(
rows: &FittedRows,
parameters: &[f64],
stations: &[Station],
window: [f64; 2],
dense: &OpenDenseCentres,
radius: f64,
) -> Vec<Result<f64, String>> {
let intervals = parameters.len().saturating_sub(1);
let overlapped: Vec<usize> =
(0..intervals).filter(|&interval| parameters[interval + 1] > window[0] && parameters[interval] < window[1]).collect();
let Ok([v_low, v_high]) = rows.surface.domain_v() else {
return overlapped.iter().map(|interval| Err(format!("interval {interval}: wall surface v domain unreadable"))).collect();
};
let nodes_of = |interval: usize| -> Result<Vec<Vec3>, String> {
let inner = dense.centres.get(interval).ok_or_else(|| format!("interval {interval}: no dense centre row"))?;
if dense.subdivisions != OPEN_WALL_CENTRES || inner.len() + 1 != OPEN_WALL_CENTRES {
return Err(format!("interval {interval}: dense row {} centres at {} subdivisions", inner.len(), dense.subdivisions));
}
let mut centres = Vec::with_capacity(OPEN_WALL_CENTRES + 1);
centres.push(stations[interval].center);
for (j, centre) in inner.iter().enumerate() {
centres.push(centre.ok_or_else(|| format!("interval {interval}: exact centre {} of {OPEN_WALL_CENTRES} unsolved", j + 1))?);
}
centres.push(stations[interval + 1].center);
Ok(centres)
};
let half = OPEN_WALL_CENTRES / 2;
let junction = |left: usize| -> Result<(Vec<Vec3>, Option<Vec<f64>>), String> {
if dense.station_ts.len() != parameters.len() {
return Err(format!("interval {left}: junction without the native station parameters"));
}
junction_stencil(&nodes_of(left)?, &nodes_of(left + 1)?, &dense.station_ts, left)
};
overlapped
.into_iter()
.map(|interval| -> Result<f64, String> {
let centres = nodes_of(interval)?;
let (from, to) = (parameters[interval], parameters[interval + 1]);
let inside = |u: f64| u >= window[0] && u <= window[1];
let mut probes: Vec<(f64, usize, Option<(Vec<Vec3>, Option<Vec<f64>>)>)> = (2..OPEN_WALL_CENTRES)
.step_by(2)
.map(|step| (from + (to - from) * step as f64 / OPEN_WALL_CENTRES as f64, step))
.filter(|(u, _)| inside(*u))
.map(|(u, step)| (u, step, None))
.collect();
for end in window {
if end > from && end < to {
let fraction = (end - from) / (to - from);
let nearest = 2 * ((fraction * OPEN_WALL_CENTRES as f64 / 2.0).round() as usize);
#[cfg(not(test))]
let junction_off = false;
if junction_off {
probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
} else if nearest < 2 && interval > 0 {
probes.push((end, half, Some(junction(interval - 1)?)));
} else if nearest > OPEN_WALL_CENTRES - 2 && interval + 1 < intervals {
probes.push((end, half, Some(junction(interval)?)));
} else {
probes.push((end, nearest.clamp(2, OPEN_WALL_CENTRES - 2), None));
}
}
}
if probes.is_empty() {
return Err(format!("interval {interval}: no probe in its shipped part of [{:.17}, {:.17}]", window[0], window[1]));
}
let mut declared = 0.0_f64;
for (u, step, stencil) in probes {
let (nodes, abscissae, what) = match &stencil {
Some((nodes, None)) => (nodes, None, "junction"),
Some((nodes, Some(ts))) => (nodes, Some(ts), "native-abscissa junction"),
None => (¢res, None, "interval"),
};
for k in 1..OPEN_WALL_SECTIONS {
let v = v_low + (v_high - v_low) * k as f64 / OPEN_WALL_SECTIONS as f64;
let point = rows.surface.evaluate(u, v).map_err(|error| format!("interval {interval}: wall point (u {u:.9}, v {v:.6}) unreadable: {error}"))?;
let read = if stencil.is_none() {
crate::blend::chain::declared_wall_probe_located_why(point, nodes, step, (u - from) / (to - from), radius)
} else {
read_on_stencil(point, nodes, abscissae.map(|ts| ts.as_slice()), step, radius)
};
let (reading, uncertainty) = read.map_err(|why| {
format!(
"interval {interval}: declared reader declined probe node {step} of the {what} stencil, section {k}/{OPEN_WALL_SECTIONS} \
(u {u:.17}): {why}"
)
})?;
declared = declared.max(reading + uncertainty);
}
}
if declared.is_finite() { Ok(declared) } else { Err(format!("interval {interval}: non-finite reading {declared}")) }
})
.collect()
}
pub(in crate::blend) fn fresh_id_source(solid: &BrepSolid) -> impl FnMut() -> u64 {
let mut next_id = solid
.vertices
.iter()
.map(|vertex| vertex.id)
.chain(solid.edges.iter().map(|edge| edge.id))
.chain(
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.flat_map(|face| {
face.loops
.iter()
.map(|loop_record| loop_record.id)
.chain(face.loops.iter().flat_map(|loop_record| {
loop_record.coedges.iter().map(|coedge| coedge.id)
}))
.chain(std::iter::once(face.id))
}),
)
.max()
.unwrap_or(0)
+ 1;
move || {
let id = next_id;
next_id += 1;
id
}
}
pub(in crate::blend) fn march_trim_stands(read: &Result<(f64, f64, usize, f64), KernelRefusal>, floor: f64) -> bool {
matches!(read, Ok((miss, standoff, _, _)) if *miss + *standoff <= floor)
}
fn exact_planar_trim(rail: &NurbsCurve, surface: &NurbsSurface, d0: f64, d1: f64) -> Result<Option<NurbsCurve>, KernelRefusal> {
if !matches!(surface.analytic(), Some(crate::AnalyticSurface::Plane { .. })) || !(d1 > d0) {
return Ok(None);
}
let feet_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.feet_ms");
let mut control = Vec::with_capacity(2);
for t in [d0, d1] {
let point = rail.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
let foot = crate::project_point_to_surface(surface, point).or_refuse(KernelStage::Refine, "project")?;
control.push(crate::Vec4::from_point(crate::Vec3::new(foot.u, foot.v, 0.0), 1.0));
}
let line = NurbsCurve::new(1, vec![d0, d0, d1, d1], control).or_refuse(KernelStage::Refine, "new")?;
feet_timing.stop();
let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
let read = match affine_reads_on().then(|| affine_trim_read(rail, surface, &line, d0, d1)).flatten() {
Some(read) => {
crate::blend::record_count("count.read.affine_trims", 1);
Ok(read)
}
None => crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &line, d0, d1),
};
read_timing.stop();
Ok(march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE).then_some(line))
}
fn affine_trim_read(rail: &NurbsCurve, surface: &NurbsSurface, pcurve: &NurbsCurve, d0: f64, d1: f64) -> Option<(f64, f64, usize, f64)> {
if !affine_curve(rail) || !affine_curve(pcurve) {
return None;
}
let (origin, normal) = affine_plane(surface)?;
let (mut miss, mut standoff, mut worst) = (0.0f64, 0.0f64, d0);
for t in [d0, d1] {
let point = rail.evaluate(t).ok()?;
let foot = point.sub(normal.scale(point.sub(origin).dot(normal)));
let uv = pcurve.evaluate(t).ok()?;
let image = crate::blend::track_fit::carrier_point(surface, uv.x, uv.y).ok()?;
let (residual, off) = (image.sub(foot).length(), point.sub(foot).length());
if !(residual.is_finite() && off.is_finite()) {
return None;
}
if residual > miss {
(miss, worst) = (residual, t);
}
standoff = standoff.max(off);
}
Some((miss, standoff, 2, worst))
}
pub(in crate::blend) fn build_open_surgery(
solid: &BrepSolid,
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
rows: FittedRows,
ends: [EndPlan; 2],
name: Option<&str>,
) -> Result<SewnStripe, KernelRefusal> {
build_open_surgery_with_tail(solid, result, take_id, edge, first, second, rows, ends, name, [None, None])
}
#[allow(clippy::too_many_arguments)]
pub(in crate::blend) fn build_open_surgery_with_tail(
solid: &BrepSolid,
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
edge: &EdgeRecord,
first: &BlendMate,
second: &BlendMate,
rows: FittedRows,
ends: [EndPlan; 2],
name: Option<&str>,
tail: [Option<crate::blend::rollover::TailSplice<'_>>; 2],
) -> Result<SewnStripe, KernelRefusal> {
let [start_end, finish_end] = ends;
let trim_row = |row: &NurbsCurve, a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
let [low, high] = row.domain().or_refuse(KernelStage::Refine, "domain")?;
let tail = if a == low { row.clone() } else { row.split(a).or_refuse(KernelStage::Refine, "split")?.1 };
if b == high {
return Ok(tail);
}
let (middle, _) = tail.split(b).or_refuse(KernelStage::Refine, "split")?;
Ok(middle)
};
let kind = |plan: &EndPlan| match plan {
EndPlan::Free(_) => "free",
EndPlan::Corner(_) => "corner",
EndPlan::Miter(_) => "miter",
EndPlan::Cap(_) => "cap",
};
let describe_trim = |error: KernelRefusal| {
error.with_message(|error| {
format!(
"{error} (edge {} rows trimmed for a {} start at cr {:.6}/cs {:.6} and a {} finish \
at cr {:.6}/cs {:.6})",
edge.id,
kind(&start_end),
start_end.cr_parameter(),
start_end.cs_parameter(),
kind(&finish_end),
finish_end.cr_parameter(),
finish_end.cs_parameter()
)
})
};
let band = consumed_band(solid);
let exact_trim = crate::blend::exact_trim_on();
let collapses = |row: &NurbsCurve, a: f64, b: f64| -> Result<bool, KernelRefusal> {
let from = row.evaluate(a).or_refuse(KernelStage::Refine, "evaluate")?;
Ok(row.evaluate(b).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band
&& row.evaluate(0.5 * (a + b)).or_refuse(KernelStage::Refine, "evaluate")?.sub(from).length() <= band)
};
let cr_collapsed = collapses(&rows.cr, start_end.cr_parameter(), finish_end.cr_parameter())?;
let cs_collapsed = collapses(&rows.cs, start_end.cs_parameter(), finish_end.cs_parameter())?;
if cr_collapsed || cs_collapsed {
let unsupported = if start_end.free().is_some() && finish_end.free().is_some() {
Some("neither end is a corner")
} else if first.face.id == second.face.id {
Some("both rails lie on one face")
} else if matches!(start_end, EndPlan::Cap(_)) || matches!(finish_end, EndPlan::Cap(_)) {
Some("a capped end has no corner to share the point with")
} else {
None
};
if let Some(reason) = unsupported {
return Err(KernelRefusal::unsupported(KernelStage::Sew, super::RAIL_COLLAPSE_WHAT, format!(
"{RAIL_COLLAPSE_UNSUPPORTED} edge {}'s rail shrinks to a point and {reason}",
edge.id
)));
}
}
let trim_rail = |collapsed: bool,
row: &NurbsCurve,
pcurve: &NurbsCurve,
surface: &NurbsSurface,
a: f64,
b: f64|
-> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
if collapsed {
return Ok(None);
}
let rows_timing = crate::blend::BlendTimer::start("timing.surgery.trim.rows_ms");
let rail = trim_row(row, a, b).map_err(describe_trim)?;
let interpolated = trim_row(pcurve, a, b)?;
rows_timing.stop();
{
let overrun_timing = crate::blend::BlendTimer::start("timing.surgery.trim.overrun_ms");
let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
let overrun = crate::blend::planar_chart_overrun(surface, &|t| rail.evaluate(t), d0, d1)?;
overrun_timing.stop();
if overrun.is_some_and(|excursion| excursion > band) {
return Ok(Some((rail, interpolated)));
}
}
let [d0, d1] = rail.domain().or_refuse(KernelStage::Refine, "domain")?;
if exact_trim && rows.exact_extrusion {
if let Some(line) = exact_planar_trim(&rail, surface, d0, d1)? {
crate::blend::record_count("count.surgery.exact_trim", 1);
return Ok(Some((rail, line)));
}
}
if rows.revolution_axis.is_some() {
if exact_revolution_reads_on() {
let cert_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.cert_ms");
let bound = exact_revolution_read_bound(row, surface, pcurve);
cert_timing.stop();
if bound.is_some_and(|bound| 3.0 * bound <= crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
crate::blend::record_count("count.surgery.exact_revolution_trim_certified", 1);
return Ok(Some((rail, interpolated)));
}
}
let read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.exact.read_ms");
let read = crate::blend::track_fit::read_shipped_pcurve(surface, &|t| rail.evaluate(t), &interpolated, d0, d1);
read_timing.stop();
if march_trim_stands(&read, crate::pcurve::PCURVE_REFINEMENT_TOLERANCE) {
crate::blend::record_count("count.surgery.exact_revolution_trim", 1);
return Ok(Some((rail, interpolated)));
}
crate::blend::record_count("count.surgery.exact_revolution_trim_declined", 1);
}
let mut march_feet = crate::blend::track_fit::FootMemo::default();
let mut refit_feet = crate::blend::track_fit::FootMemo::default();
let march_read_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.march_ms");
let march_read = crate::blend::track_fit::read_shipped_pcurve_memo(surface, &|t| rail.evaluate(t), &interpolated, d0, d1, &mut march_feet);
march_read_timing.stop();
let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
let declined = |why: KernelRefusal| -> Result<Option<(NurbsCurve, NurbsCurve)>, KernelRefusal> {
let stands = march_trim_stands(&march_read, floor);
if stands {
Ok(Some((rail.clone(), interpolated.clone())))
} else {
Err(why)
}
};
let refit_timing = crate::blend::BlendTimer::start("timing.surgery.trim.refit_ms");
let refit = super::closed::refit_closed_support_pcurve_read(&rail, &interpolated, surface, &mut refit_feet);
refit_timing.stop();
let _reads_timing = crate::blend::BlendTimer::start("timing.surgery.trim.reads_ms");
match refit {
Ok((fitted, refit_read)) => {
let (march_standoff, refit_standoff) = match (&march_read, Ok::<_, KernelRefusal>(refit_read)) {
(_, Err(error)) => return declined(error),
(Err(error), Ok(_)) => {
return Err(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch_unread", format!(
"blend: a support trim's refit cannot be shown to stand on its rail's branch: the march's own \
trim is unreadable ({})",
error.message
)));
}
(Ok((_, march_standoff, _, _)), Ok((_, refit_standoff, _, _))) => (*march_standoff, refit_standoff),
};
let excess_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.excess_ms");
let excess = crate::blend::track_fit::standoff_excess_memo(
surface, &|t| rail.evaluate(t), &interpolated, &fitted, d0, d1, &mut march_feet, &mut refit_feet,
);
excess_timing.stop();
let excess = match excess {
Ok(excess) => excess,
Err(error) => return declined(error),
};
if !(excess <= floor) {
return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_branch", format!(
"blend: a support trim's refit stands up to {excess:.3e} farther off its rail than the march's own trim \
at the same rail parameter (largest standoffs {refit_standoff:.3e} and {march_standoff:.3e}): it took \
another branch of the carrier"
)));
}
let nearest_timing = crate::blend::BlendTimer::start("timing.surgery.trim.read.nearest_ms");
let absolute = crate::blend::track_fit::standoff_over_nearest_memo(surface, &|t| rail.evaluate(t), &fitted, d0, d1, &mut refit_feet);
nearest_timing.stop();
let absolute = match absolute {
Ok(absolute) => absolute,
Err(error) => return declined(error),
};
if !(absolute <= floor) {
return declined(KernelRefusal::non_convergence(KernelStage::Refine, "support_refit_off_rail", format!(
"blend: a support trim's refit stands up to {absolute:.3e} farther off its rail than the rail stands \
off its carrier: its foot is not the rail's nearest point"
)));
}
Ok(Some((rail, fitted)))
}
Err(error) => declined(error),
}
};
let trim_timing = crate::blend::BlendTimer::start("timing.surgery.trim_rails_ms");
let cr_rail = trim_rail(
cr_collapsed,
&rows.cr,
&rows.cr_pcurve,
&first.face.surface,
start_end.cr_parameter(),
finish_end.cr_parameter(),
)?;
let cs_rail = trim_rail(
cs_collapsed,
&rows.cs,
&rows.cs_pcurve,
&second.face.surface,
start_end.cs_parameter(),
finish_end.cs_parameter(),
)?;
trim_timing.stop();
let cr_domain = match &cr_rail {
Some((cr, _)) => cr.domain().or_refuse(KernelStage::Refine, "domain")?,
None => [start_end.cr_parameter(), finish_end.cr_parameter()],
};
let cs_domain = match &cs_rail {
Some((cs, _)) => cs.domain().or_refuse(KernelStage::Refine, "domain")?,
None => [start_end.cs_parameter(), finish_end.cs_parameter()],
};
let cr_at = |parameter: f64| match &cr_rail {
Some((cr, _)) => cr.evaluate(parameter),
None => rows.cr.evaluate(parameter),
};
let cs_at = |parameter: f64| match &cs_rail {
Some((cs, _)) => cs.evaluate(parameter),
None => rows.cs.evaluate(parameter),
};
let old_start = edge.start_vertex_id;
let old_finish = edge.end_vertex_id;
let classify =
|boundary_id: u64, boundary_param: f64, corner: u64| -> Result<RimResolution, KernelRefusal> {
let boundary = result
.edges
.iter()
.find(|candidate| candidate.id == boundary_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_boundary_edge", "blend: end boundary edge missing during surgery"))?;
let span = (boundary.t1 - boundary.t0).abs().max(1e-12);
let far_vertex = if boundary.start_vertex_id == corner {
Some((boundary.end_vertex_id, boundary.t1, boundary.t0))
} else if boundary.end_vertex_id == corner {
Some((boundary.start_vertex_id, boundary.t0, boundary.t1))
} else {
None
};
if let Some((far_id, far_t, near_t)) = far_vertex {
let far_point = result
.vertices
.iter()
.find(|candidate| candidate.id == far_id)
.map(|candidate| candidate.point)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"))?;
let crossing = boundary.curve.evaluate_extended(boundary_param).or_refuse(KernelStage::Refine, "evaluate_extended")?;
let _ = span;
let past_far = crossing.sub(far_point).length();
if past_far <= band {
if !result
.vertices
.iter()
.any(|candidate| candidate.id == far_id)
{
return Err(KernelRefusal::internal(KernelStage::Sew, "far_vertex", "blend: consumed boundary far vertex missing"));
}
return Ok(RimResolution::Consumed(far_id));
}
if (boundary_param - far_t) * (far_t - near_t) > 0.0 {
return Err(KernelRefusal::unsupported(KernelStage::Sew, "blend_wider_than_face", format!(
"{BLEND_WIDER_THAN_FACE} its rail crosses edge {boundary_id} {past_far:.3e} \
past that edge's far vertex, and a rail within {band:.3e} of it is the \
most this surgery snaps onto the edge"
)));
}
}
Ok(RimResolution::Fresh)
};
let resolve_side = |plan: &EndPlan,
side_first: bool,
corner: u64|
-> Result<RimResolution, KernelRefusal> {
if let Some(vertex) = plan.planned_rim(side_first) {
return Ok(RimResolution::Corner(vertex));
}
match plan {
EndPlan::Free(free) => classify(
if side_first {
free.first_edge_id
} else {
free.second_edge_id
},
if side_first {
free.first_edge_parameter
} else {
free.second_edge_parameter
},
corner,
),
EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => {
Err(KernelRefusal::internal(KernelStage::Sew, "rim_vertices", "blend: planned end without rim vertices"))
}
}
};
let crossings_timing = crate::blend::BlendTimer::start("timing.surgery.crossings_ms");
let start_first = resolve_side(&start_end, true, old_start)?;
let start_second = resolve_side(&start_end, false, old_start)?;
let finish_first = resolve_side(&finish_end, true, old_finish)?;
let finish_second = resolve_side(&finish_end, false, old_finish)?;
crossings_timing.stop();
let _topology_timing = crate::blend::BlendTimer::start("timing.surgery.topology_ms");
let first_use_forward = first.coedge.forward;
let blend_cr_forward = !first_use_forward;
let (sew_first, sew_second) = match (start_end.free(), finish_end.free(), &cr_rail, &cs_rail) {
(Some(start_free), Some(finish_free), Some((cr, _)), Some((cs, _))) => (
detect_row_coincidence(
solid,
first,
edge.id,
start_free.first_edge_id,
finish_free.first_edge_id,
start_first.is_consumed(),
start_first.existing().unwrap_or(0),
finish_first.is_consumed(),
finish_first.existing().unwrap_or(0),
cr,
blend_cr_forward,
band,
)?,
detect_row_coincidence(
solid,
second,
edge.id,
start_free.second_edge_id,
finish_free.second_edge_id,
start_second.is_consumed(),
start_second.existing().unwrap_or(0),
finish_second.is_consumed(),
finish_second.existing().unwrap_or(0),
cs,
!blend_cr_forward,
band,
)?,
),
_ => (None, None),
};
let (sew_first, sew_second): (Option<RowSew>, Option<RowSew>) =
if first.face.id == second.face.id {
(None, None)
} else {
(sew_first, sew_second)
};
let mut resolve = |rim: &RimResolution, point: Result<Vec3, String>| -> Result<u64, KernelRefusal> {
match rim.existing() {
Some(id) => Ok(id),
None => {
let id = take_id();
result.vertices.push(VertexRecord { id, point: point.or_refuse(KernelStage::Refine, "point")? });
Ok(id)
}
}
};
let w1a = resolve(&start_first, cr_at(cr_domain[0]))?;
let w1b = resolve(&finish_first, cr_at(cr_domain[1]))?;
let w2a = resolve(&start_second, cs_at(cs_domain[0]))?;
let w2b = resolve(&finish_second, cs_at(cs_domain[1]))?;
let mut snap = [&sew_first, &sew_second]
.into_iter()
.flatten()
.fold(0.0f64, |worst, sew| worst.max(sew.deviation));
for (rim, vertex, row_end) in [
(&start_first, w1a, cr_at(cr_domain[0]).or_refuse(KernelStage::Refine, "cr_at")?),
(&finish_first, w1b, cr_at(cr_domain[1]).or_refuse(KernelStage::Refine, "cr_at")?),
(&start_second, w2a, cs_at(cs_domain[0]).or_refuse(KernelStage::Refine, "cs_at")?),
(&finish_second, w2b, cs_at(cs_domain[1]).or_refuse(KernelStage::Refine, "cs_at")?),
] {
if !rim.is_consumed() {
continue;
}
let point = result
.vertices
.iter()
.find(|candidate| candidate.id == vertex)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "rim_vertex", "blend: consumed rim vertex missing"))?
.point;
snap = snap.max(row_end.sub(point).length());
}
let cr_edge_id = match (&sew_first, &cr_rail) {
(Some(sew), _) => Some(sew.edge_id),
(None, None) => None,
(None, Some((cr, _))) => {
let id = take_id();
result.edges.push(EdgeRecord {
id,
curve: cr.clone(),
t0: cr_domain[0],
t1: cr_domain[1],
start_vertex_id: w1a,
end_vertex_id: w1b,
degenerate: false,
name: None,
});
Some(id)
}
};
let cs_edge_id = match (&sew_second, &cs_rail) {
(Some(sew), _) => Some(sew.edge_id),
(None, None) => None,
(None, Some((cs, _))) => {
let id = take_id();
result.edges.push(EdgeRecord {
id,
curve: cs.clone(),
t0: cs_domain[0],
t1: cs_domain[1],
start_vertex_id: w2a,
end_vertex_id: w2b,
degenerate: false,
name: None,
});
Some(id)
}
};
let mut commit_split = |plan: &EndPlan, second_rim: u64, corner: u64| -> Result<Option<super::split_floor::SplitCommit>, KernelRefusal> {
match plan.free().and_then(|free| free.split.as_ref()) {
Some(split) => super::split_floor::commit_split_end(result, &mut *take_id, split, second_rim, corner).map(Some),
None => Ok(None),
}
};
let start_split = commit_split(&start_end, w2a, old_start)?;
let finish_split = commit_split(&finish_end, w2b, old_finish)?;
let mut commit_end_edge = |plan: &EndPlan,
first_rim: u64,
second_rim: u64|
-> Result<Option<u64>, KernelRefusal> {
match plan {
EndPlan::Corner(_) | EndPlan::Miter(_) | EndPlan::Cap(_) => Ok(None),
EndPlan::Free(free) => {
let id = take_id();
let domain = free.transverse_curve.domain().or_refuse(KernelStage::Refine, "domain")?;
result.edges.push(EdgeRecord {
id,
curve: free.transverse_curve.clone(),
t0: domain[0],
t1: domain[1],
start_vertex_id: first_rim,
end_vertex_id: second_rim,
degenerate: false,
name: None,
});
Ok(Some(id))
}
}
};
let transverse_a_id = commit_end_edge(&start_end, w1a, start_split.as_ref().map_or(w2a, |split| split.q1_vertex))?;
let transverse_b_id = commit_end_edge(&finish_end, w1b, finish_split.as_ref().map_or(w2b, |split| split.q1_vertex))?;
for (mate, rail, sewn) in [
(first, cr_edge_id.zip(cr_rail.as_ref()), sew_first.is_some()),
(second, cs_edge_id.zip(cs_rail.as_ref()), sew_second.is_some()),
] {
if sewn {
continue;
}
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == mate.face.id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
let loop_record = &mut face.loops[mate.loop_index];
let position = loop_record
.coedges
.iter()
.position(|coedge| coedge.edge_id == edge.id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "edge_coedge", "blend: edge coedge lost during surgery"))?;
let Some((new_edge_id, (_, pcurve_forward))) = rail else {
loop_record.coedges.remove(position);
continue;
};
let old_forward = loop_record.coedges[position].forward;
loop_record.coedges[position] = CoedgeRecord {
id: loop_record.coedges[position].id,
edge_id: new_edge_id,
forward: old_forward,
pcurve: if old_forward {
pcurve_forward.clone()
} else {
pcurve_forward.reversed().or_refuse(KernelStage::Refine, "reversed")?
},
};
}
for (plan, at_start) in [(&start_end, true), (&finish_end, false)] {
let EndPlan::Cap(cap) = plan else {
continue;
};
for (mate, rail_edge_id, leg, rim) in [
(first, cr_edge_id, &cap.first_leg, if at_start { w1a } else { w1b }),
(second, cs_edge_id, &cap.second_leg, if at_start { w2a } else { w2b }),
] {
let rail_edge_id = rail_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_rail", "blend: a capped end lost its rail"))?;
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == mate.face.id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during cap splice"))?;
let loop_record = &mut face.loops[mate.loop_index];
let count = loop_record.coedges.len();
let rail_at = loop_record
.coedges
.iter()
.position(|coedge| coedge.edge_id == rail_edge_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "rail_coedge", "blend: rail coedge lost during cap splice"))?;
let rail_forward = loop_record.coedges[rail_at].forward;
let end_is_traversal_end = if rail_forward { !at_start } else { at_start };
let (insert_at, forward, pcurve) = if end_is_traversal_end {
(rail_at + 1, true, leg.1.clone())
} else {
(rail_at, false, leg.1.reversed().or_refuse(KernelStage::Refine, "reversed")?)
};
let _ = (count, rim);
loop_record.coedges.insert(
insert_at,
CoedgeRecord {
id: take_id(),
edge_id: leg.0,
forward,
pcurve,
},
);
}
}
let mut consumed_edges: Vec<u64> = Vec::new();
let start_free = start_end.free();
let finish_free = finish_end.free();
for (mate, sewn, sides) in [
(
first,
sew_first.is_some(),
[
(&start_first, start_free.map(|free| free.first_edge_id)),
(&finish_first, finish_free.map(|free| free.first_edge_id)),
],
),
(
second,
sew_second.is_some(),
[
(&start_second, start_free.map(|free| free.second_edge_id)),
(&finish_second, finish_free.map(|free| free.second_edge_id)),
],
),
] {
for (cross, boundary_id) in sides {
if !cross.is_consumed() {
continue;
}
let Some(boundary_id) = boundary_id else {
continue;
};
if !sewn {
let face = result
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.find(|face| face.id == mate.face.id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_face", "blend: mate face lost during surgery"))?;
face.loops[mate.loop_index]
.coedges
.retain(|coedge| coedge.edge_id != boundary_id);
}
if !consumed_edges.contains(&boundary_id) {
consumed_edges.push(boundary_id);
}
}
}
for sew in [&sew_first, &sew_second].into_iter().flatten() {
for id in &sew.collapsed_edges {
if !consumed_edges.contains(id) {
consumed_edges.push(*id);
}
}
}
let mut end_plans = Vec::with_capacity(2);
for (end, transverse_id, corner, first_consumed, second_consumed) in [
(
start_end.free(),
transverse_a_id,
old_start,
start_first.is_consumed(),
start_second.is_consumed(),
),
(
finish_end.free(),
transverse_b_id,
old_finish,
finish_first.is_consumed(),
finish_second.is_consumed(),
),
] {
let (Some(end), Some(transverse_id)) = (end, transverse_id) else {
continue;
};
if end.split.is_some() {
continue;
}
let face = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == end.end_face_id)
.ok_or(KernelRefusal::internal(KernelStage::Sew, "end_face", "blend: end face lost during surgery"))?;
let Some(located) = locate_end_corner(
&result,
face,
corner,
end.first_edge_id,
end.second_edge_id,
) else {
return Err(KernelRefusal::internal(KernelStage::Sew, "end_face_corner", "blend: end-face corner (adjacent boundary coedges) not found"));
};
for pole in &located.pole_edge_ids {
if !consumed_edges.contains(pole) {
consumed_edges.push(*pole);
}
}
end_plans.push((
end.end_face_id,
transverse_id,
end.transverse_end_pcurve.clone(),
located,
first_consumed,
second_consumed,
));
}
let split_corners = |end: Option<&EndSurgery>, corner: u64, result: &BrepSolid| -> Result<Option<[super::support::EndCorner; 2]>, KernelRefusal> {
match end.and_then(|end| end.split.as_ref().map(|split| (end, split))) {
Some((end, split)) => super::split_floor::locate_split_corners(result, end, split, corner).map(Some),
None => Ok(None),
}
};
let start_split_corners = split_corners(start_free, old_start, result)?;
let finish_split_corners = split_corners(finish_free, old_finish, result)?;
if (start_split_corners.is_some() && (start_first.is_consumed() || start_second.is_consumed()))
|| (finish_split_corners.is_some() && (finish_first.is_consumed() || finish_second.is_consumed()))
{
return Err(KernelRefusal::unsupported(KernelStage::Sew, "split_floor_consumed", "blend: a two-face end whose rail consumes its boundary"));
}
for (rim_resolution, boundary, corner, rim) in [
(
&start_first,
start_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
old_start,
w1a,
),
(
&start_second,
start_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
old_start,
w2a,
),
(
&finish_first,
finish_free.map(|free| (free.first_edge_id, free.first_edge_parameter)),
old_finish,
w1b,
),
(
&finish_second,
finish_free.map(|free| (free.second_edge_id, free.second_edge_parameter)),
old_finish,
w2b,
),
] {
if !rim_resolution.trims_boundary() {
continue;
}
let Some((boundary_id, boundary_param)) = boundary else {
continue;
};
trim_edge_at(result, boundary_id, boundary_param, corner, rim)?;
}
for (end_face, transverse_id, transverse_end_pcurve, located, first_consumed, second_consumed) in
end_plans
{
let forward = located.x_is_first;
let pcurve = if forward {
transverse_end_pcurve
} else {
transverse_end_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?
};
let (x_consumed, y_consumed) = if located.x_is_first {
(first_consumed, second_consumed)
} else {
(second_consumed, first_consumed)
};
let transverse_coedge = CoedgeRecord {
id: take_id(),
edge_id: transverse_id,
forward,
pcurve,
};
splice_end_corner(
result,
end_face,
&located,
transverse_coedge,
x_consumed,
y_consumed,
)?;
}
let (mid_u, station_uv) = match (&cr_rail, &cs_rail) {
(Some((_, cr_pcurve)), _) => {
let mid_u = (cr_domain[0] + cr_domain[1]) * 0.5;
(mid_u, cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
}
(None, Some(_)) => {
let mid_u = (cs_domain[0] + cs_domain[1]) * 0.5;
(mid_u, rows.cr_pcurve.evaluate(mid_u).or_refuse(KernelStage::Refine, "evaluate")?)
}
(None, None) => (cr_domain[0], rows.cr_pcurve.evaluate(cr_domain[0]).or_refuse(KernelStage::Refine, "evaluate")?),
};
let blend_normal = raw_normal(&rows.surface, mid_u, 0.0)?;
let n1 = raw_normal(&first.face.surface, station_uv.x, station_uv.y)?;
let out1 = if first.face.same_sense {
n1
} else {
n1.scale(-1.0)
};
let same_sense = blend_normal.dot(out1) >= 0.0;
let loop_id = take_id();
let cr_forward = sew_first
.as_ref()
.map_or(blend_cr_forward, |sew| sew.forward);
let cs_forward = sew_second
.as_ref()
.map_or(!blend_cr_forward, |sew| sew.forward);
let mut end_slot = |plan: &EndPlan,
free_edge_id: Option<u64>,
natural_forward: bool|
-> Result<Vec<CoedgeRecord>, KernelRefusal> {
match plan {
EndPlan::Corner(end) => Ok(vec![CoedgeRecord {
id: take_id(),
edge_id: end.arc_edge_id,
forward: true,
pcurve: end.arc_blend_pcurve.clone(),
}]),
EndPlan::Cap(end) => Ok(vec![CoedgeRecord {
id: take_id(),
edge_id: end.arc_edge_id,
forward: true,
pcurve: end.arc_blend_pcurve.clone(),
}]),
EndPlan::Miter(end) => end
.edges
.iter()
.map(|(edge_id, forward, pcurve)| {
Ok(CoedgeRecord {
id: take_id(),
edge_id: *edge_id,
forward: *forward,
pcurve: pcurve.clone(),
})
})
.collect(),
EndPlan::Free(free) => {
let edge_id =
free_edge_id.ok_or(KernelRefusal::internal(KernelStage::Sew, "transverse_edge", "blend: free end without its transverse edge"))?;
Ok(vec![if natural_forward {
CoedgeRecord {
id: take_id(),
edge_id,
forward: true,
pcurve: free.transverse_blend_pcurve.clone(),
}
} else {
CoedgeRecord {
id: take_id(),
edge_id,
forward: false,
pcurve: free.transverse_blend_pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")?,
}
}])
}
}
};
let start_slot = end_slot(&start_end, transverse_a_id, !blend_cr_forward)?;
let finish_slot = end_slot(&finish_end, transverse_b_id, blend_cr_forward)?;
let start_slot = match (start_free, &start_split, transverse_a_id) {
(Some(end), Some(commit), Some(id)) => super::split_floor::split_slot(end, end.split.as_ref().expect("planned"), commit, id, !blend_cr_forward, &mut *take_id)?,
_ => start_slot,
};
let finish_slot = match (finish_free, &finish_split, transverse_b_id) {
(Some(end), Some(commit), Some(id)) => super::split_floor::split_slot(end, end.split.as_ref().expect("planned"), commit, id, blend_cr_forward, &mut *take_id)?,
_ => finish_slot,
};
for (end, commit, id, corners, corner, forward) in [
(start_free, &start_split, transverse_a_id, &start_split_corners, old_start, !blend_cr_forward),
(finish_free, &finish_split, transverse_b_id, &finish_split_corners, old_finish, blend_cr_forward),
] {
if let (Some(end), Some(commit), Some(id), Some(corners)) = (end, commit, id, corners) {
super::split_floor::finish_split_end(result, &mut *take_id, end, end.split.as_ref().expect("planned"), commit, id, corners, corner, forward, name)?;
}
}
let coedges = if blend_cr_forward {
let mut coedges = Vec::new();
if let Some(cr_edge_id) = cr_edge_id {
coedges.push(CoedgeRecord {
id: take_id(),
edge_id: cr_edge_id,
forward: cr_forward,
pcurve: crate::sweep_topology::parameter_line(
cr_domain[0],
0.0,
cr_domain[1],
0.0,
).or_refuse(KernelStage::Refine, "parameter_line")?,
});
}
coedges.extend(finish_slot);
if let Some(cs_edge_id) = cs_edge_id {
coedges.push(CoedgeRecord {
id: take_id(),
edge_id: cs_edge_id,
forward: cs_forward,
pcurve: cs_blend_pcurve_reversed(&cs_domain)?,
});
}
coedges.extend(start_slot);
coedges
} else {
let mut coedges = Vec::new();
if let Some(cr_edge_id) = cr_edge_id {
coedges.push(CoedgeRecord {
id: take_id(),
edge_id: cr_edge_id,
forward: cr_forward,
pcurve: crate::sweep_topology::parameter_line(
cr_domain[1],
0.0,
cr_domain[0],
0.0,
).or_refuse(KernelStage::Refine, "parameter_line")?,
});
}
coedges.extend(start_slot);
if let Some(cs_edge_id) = cs_edge_id {
coedges.push(CoedgeRecord {
id: take_id(),
edge_id: cs_edge_id,
forward: cs_forward,
pcurve: crate::sweep_topology::parameter_line(
cs_domain[0],
1.0,
cs_domain[1],
1.0,
).or_refuse(KernelStage::Refine, "parameter_line")?,
});
}
coedges.extend(finish_slot);
coedges
};
let blend_face_id = take_id();
let mut blend_face = FaceRecord {
id: blend_face_id,
surface: rows.surface,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
};
if rows.exact_extrusion {
transpose_face(&mut blend_face)?;
}
let shell_index = result
.shells
.iter()
.position(|shell| shell.faces.iter().any(|face| face.id == first.face.id))
.ok_or(KernelRefusal::internal(KernelStage::Sew, "mate_shell", "blend: mate shell lost during surgery"))?;
for (mate, sew) in [(first, &sew_first), (second, &sew_second)] {
if sew.is_some() {
for shell in &mut result.shells {
shell.faces.retain(|face| face.id != mate.face.id);
}
}
}
result.shells[shell_index].faces.push(blend_face);
result.edges.retain(|candidate| candidate.id != edge.id);
result
.edges
.retain(|candidate| !consumed_edges.contains(&candidate.id));
let mut identified = Vec::new();
for (collapsed, from, to) in [(cr_collapsed, w1a, w1b), (cs_collapsed, w2a, w2b)] {
if collapsed && from != to {
identified.push((from, to));
}
}
let mut cr_edge_id = cr_edge_id;
let mut cs_edge_id = cs_edge_id;
if tail.iter().any(Option::is_some) {
let EndPlan::Cap(cap) = &finish_end else {
return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the rolled stripe's finish end is not a cap, which is the only end this lane closes on the neighbours"));
};
let mut footprint = StoppedEndFootprint::default();
{
let sharp = result.vertices.iter().find(|vertex| vertex.id == cap.sharp_vertex).map(|vertex| vertex.point)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: the sharp vertex vanished"))?;
let d = edge.curve.derivatives(edge.t1, 1).or_refuse(KernelStage::Refine, "derivatives")?;
footprint.plane = (sharp, d[1].normalized().or_refuse(KernelStage::Refine, "normalized")?);
footprint.sharp_vertex = cap.sharp_vertex;
}
for (side, splice) in tail.into_iter().enumerate() {
let Some(splice) = splice else { continue };
let (mate, rail_edge_id, rim, sewn) = if side == 0 {
(first, cr_edge_id, w1b, sew_first.is_some())
} else {
(second, cs_edge_id, w2b, sew_second.is_some())
};
let Some(rail_edge_id) = rail_edge_id.filter(|_| !sewn) else {
return Err(KernelRefusal::unsupported(KernelStage::Sew, "stopped_end_rollover", "blend stopped-end rollover: a rolled side's rail collapsed or sewed onto an existing edge, which this lane does not split"));
};
let part_on_mate = roll_side_footprint(result, take_id, mate, &splice, rail_edge_id, rim, cap.sharp_vertex, &mut footprint)?;
if side == 0 { cr_edge_id = Some(part_on_mate); } else { cs_edge_id = Some(part_on_mate); }
}
retrace_stopped_end_footprint(result, take_id, footprint)?;
}
Ok(SewnStripe {
cr_edge_id,
cs_edge_id,
blend_face_id,
consumed: [sew_first.is_some(), sew_second.is_some()],
snap,
identified,
})
}
#[derive(Default)]
struct StoppedEndFootprint {
pieces: Vec<(u64, u64, u64, u64, NurbsCurve)>,
faces: Vec<u64>,
plane: (Vec3, Vec3),
sharp_vertex: u64,
}
#[allow(clippy::too_many_arguments)]
fn roll_side_footprint(
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
mate: &BlendMate,
splice: &crate::blend::rollover::TailSplice<'_>,
rail_edge_id: u64,
rim_vertex: u64,
sharp_vertex: u64,
footprint: &mut StoppedEndFootprint,
) -> Result<u64, KernelRefusal> {
let what = "stopped_end_rollover";
let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
let _ = sharp_vertex;
let rolled = splice.rolled;
let crossing_vertex = take_id();
result.vertices.push(VertexRecord { id: crossing_vertex, point: rolled.point });
let rail_parts = crate::blend::rollover_surgery::split_edge_at_vertices(result, rail_edge_id, &[(crossing_vertex, rolled.point)], take_id)?;
let [rail_near, rail_far] = [rail_parts[0], rail_parts[1]];
let rail_far_edge = result.edges.iter().find(|edge| edge.id == rail_far).cloned()
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the rail's far part vanished"))?;
if rail_far_edge.end_vertex_id != rim_vertex {
return Err(not_built(format!("the rail's far part ends at vertex {} rather than the finish rim {rim_vertex}", rail_far_edge.end_vertex_id)));
}
let holds_crossing = |result: &BrepSolid, id: u64| -> Result<bool, KernelRefusal> {
let Some(edge) = result.edges.iter().find(|edge| edge.id == id) else { return Ok(false) };
let foot = crate::project_point_to_curve(&edge.curve, rolled.point).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
Ok(foot.distance <= 1e-6 && foot.u > edge.t0.min(edge.t1) && foot.u < edge.t0.max(edge.t1))
};
let crossed = if result.edges.iter().any(|edge| edge.id == rolled.edge_id) {
rolled.edge_id
} else {
let candidates: Vec<u64> = result.edges.iter().map(|edge| edge.id).collect();
let mut found = None;
for id in candidates {
if holds_crossing(result, id)? {
found = Some(id);
break;
}
}
found.ok_or_else(|| not_built(format!("no part of tangent edge {} holds the crossing any more", rolled.edge_id)))?
};
crate::blend::rollover_surgery::split_edge_at_vertices(result, crossed, &[(crossing_vertex, rolled.point)], take_id)?;
let neighbour_surface = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == rolled.neighbour_face_id)
.map(|face| face.surface.clone())
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the neighbour face vanished"))?;
let far_pcurve = {
const SAMPLES: usize = 24;
let [t0, t1] = [rail_far_edge.t0, rail_far_edge.t1];
let mut uvs = Vec::with_capacity(SAMPLES + 1);
let mut ts = Vec::with_capacity(SAMPLES + 1);
for k in 0..=SAMPLES {
let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
let point = rail_far_edge.curve.evaluate(t).or_refuse(KernelStage::Refine, "evaluate")?;
let projection = crate::project_point_to_surface(&neighbour_surface, point).or_refuse(KernelStage::Refine, "project_point_to_surface")?;
uvs.push(Vec3::new(projection.u, projection.v, 0.0));
ts.push(t);
}
crate::fit::interpolate_curve(&uvs, 3, &ts).or_refuse(KernelStage::Refine, "interpolate_curve")?
};
let leg_edge_id = splice.legs.first().map(|piece| piece.edge_id)
.ok_or_else(|| not_built("a rolled side has no cap leg".to_string()))?;
let mate_face = result
.shells
.iter_mut()
.flat_map(|shell| shell.faces.iter_mut())
.find(|face| face.id == mate.face.id)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: the mate face vanished"))?;
mate_face.loops[mate.loop_index].coedges.retain(|coedge| coedge.edge_id != rail_far && coedge.edge_id != leg_edge_id);
footprint.pieces.push((rolled.neighbour_face_id, rail_far, crossing_vertex, rim_vertex, far_pcurve));
for piece in &splice.legs {
footprint.pieces.push((piece.face_id, piece.edge_id, piece.from, piece.to, piece.pcurve.clone()));
}
for face_id in [mate.face.id, rolled.neighbour_face_id].into_iter().chain(splice.legs.iter().map(|piece| piece.face_id)) {
if !footprint.faces.contains(&face_id) {
footprint.faces.push(face_id);
}
}
Ok(rail_near)
}
fn retrace_stopped_end_footprint(
result: &mut BrepSolid,
take_id: &mut dyn FnMut() -> u64,
footprint: StoppedEndFootprint,
) -> Result<(), KernelRefusal> {
let what = "stopped_end_rollover";
let not_built = |message: String| KernelRefusal::unsupported(KernelStage::Sew, what, format!("blend stopped-end rollover: {message}"));
let (sharp_point, tangent) = footprint.plane;
let mut boundary_vertices: Vec<u64> = vec![footprint.sharp_vertex];
for (_, _, from, to, _) in &footprint.pieces {
for vertex in [*from, *to] {
if !boundary_vertices.contains(&vertex) {
boundary_vertices.push(vertex);
}
}
}
let piece_edges: Vec<u64> = footprint.pieces.iter().map(|piece| piece.1).collect();
let mut interior: Vec<u64> = Vec::new();
for face in result.shells.iter().flat_map(|shell| &shell.faces).filter(|face| footprint.faces.contains(&face.id)) {
for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
let Some(edge) = result.edges.iter().find(|edge| edge.id == coedge.edge_id) else { continue };
if piece_edges.contains(&edge.id) || interior.contains(&edge.id) {
continue;
}
if !(boundary_vertices.contains(&edge.start_vertex_id) && boundary_vertices.contains(&edge.end_vertex_id)) {
continue;
}
let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).or_refuse(KernelStage::Refine, "evaluate")?;
if mid.sub(sharp_point).dot(tangent) < 0.0 {
interior.push(edge.id);
}
}
}
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("stopped-end rollover: footprint faces {:?}, interior edges {:?}, boundary vertices {:?}", footprint.faces, interior, boundary_vertices);
}
let edge_ends: std::collections::HashMap<u64, (u64, u64)> = result.edges.iter().map(|edge| (edge.id, (edge.start_vertex_id, edge.end_vertex_id))).collect();
let mut new_faces: Vec<FaceRecord> = Vec::new();
let mut taken: Vec<String> = result.shells.iter().flat_map(|shell| &shell.faces).filter_map(|face| face.name.clone()).collect();
for face_id in footprint.faces.clone() {
let face = result
.shells
.iter_mut()
.flat_map(|shell| shell.faces.iter_mut())
.find(|face| face.id == face_id)
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: face {face_id} vanished")))?;
let pieces: Vec<&(u64, u64, u64, u64, NurbsCurve)> = footprint.pieces.iter().filter(|piece| piece.0 == face_id).collect();
let mut rebuilt_loops: Vec<LoopRecord> = Vec::new();
for loop_record in &face.loops {
let touched = loop_record.coedges.iter().any(|coedge| interior.contains(&coedge.edge_id))
|| loop_record.coedges.iter().any(|coedge| {
edge_ends.get(&coedge.edge_id).is_some_and(|(a, b)| boundary_vertices.contains(a) || boundary_vertices.contains(b))
});
if !touched {
rebuilt_loops.push(loop_record.clone());
continue;
}
let n = loop_record.coedges.len();
let mut remaining: Vec<(CoedgeRecord, u64, u64, usize)> = Vec::new();
for (index, coedge) in loop_record.coedges.iter().enumerate() {
if interior.contains(&coedge.edge_id) {
continue;
}
let (start, end) = edge_ends.get(&coedge.edge_id).copied()
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: coedge {} names a missing edge {}", coedge.id, coedge.edge_id)))?;
let (from, to) = if coedge.forward { (start, end) } else { (end, start) };
remaining.push((coedge.clone(), from, to, index));
}
let mut loose: Vec<&(u64, u64, u64, u64, NurbsCurve)> = pieces.clone();
while !remaining.is_empty() {
let start_at = remaining.iter().position(|(_, from, to, _)| from != to).unwrap_or(0);
let (first, from0, to0, index0) = remaining.remove(start_at);
let mut walk: Vec<CoedgeRecord> = vec![first];
let mut at = to0;
let mut previous = index0;
let mut guard = 0usize;
loop {
guard += 1;
if guard > 4096 {
return Err(not_built(format!("face {face_id}: the re-trace does not close")));
}
let mut next_remaining: Vec<usize> = remaining.iter().enumerate().filter(|(_, (_, from, _, _))| *from == at).map(|(k, _)| k).collect();
let successor_is_start = (previous + 1) % n == index0;
if at == from0 && (successor_is_start || next_remaining.is_empty()) {
if at == from0 && successor_is_start {
break;
}
if loose.iter().all(|piece| piece.2 != at && piece.3 != at) {
break;
}
}
if next_remaining.len() > 1 {
if let Some(k) = next_remaining.iter().copied().find(|k| remaining[*k].3 == (previous + 1) % n) {
next_remaining = vec![k];
}
}
let next_pieces: Vec<usize> = loose.iter().enumerate().filter(|(_, piece)| piece.2 == at || piece.3 == at).map(|(k, _)| k).collect();
match (next_remaining.len(), next_pieces.len()) {
(1, 0) => {
let (coedge, _, to, index) = remaining.remove(next_remaining[0]);
walk.push(coedge);
at = to;
previous = index;
}
(0, 1) => {
let piece = loose.remove(next_pieces[0]);
let (edge_id, from, to, pcurve) = (piece.1, piece.2, piece.3, &piece.4);
let (start, _) = edge_ends.get(&edge_id).copied()
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, format!("blend stopped-end rollover: footprint edge {edge_id} missing")))?;
let along = from == at;
let coedge_forward = if along { start == from } else { start == to };
walk.push(CoedgeRecord {
id: take_id(),
edge_id,
forward: coedge_forward,
pcurve: if along { pcurve.clone() } else { pcurve.reversed().or_refuse(KernelStage::Refine, "reversed")? },
});
at = if along { to } else { from };
}
(r, p) => {
return Err(not_built(format!("face {face_id}: vertex {at} has {} ways on ({r} boundary, {p} new), not one", r + p)));
}
}
}
rebuilt_loops.push(LoopRecord { id: if rebuilt_loops.is_empty() { loop_record.id } else { take_id() }, coedges: walk });
}
if !loose.is_empty() {
return Err(not_built(format!("face {face_id}: {} new boundary piece(s) joined no loop", loose.len())));
}
}
let kept = rebuilt_loops.remove(0);
face.loops = vec![kept];
let surface = face.surface.clone();
let same_sense = face.same_sense;
let base_name = face.name.clone();
for extra in rebuilt_loops {
let name = base_name.as_ref().map(|base| {
(1..).map(|k| format!("{base}_{k}")).find(|candidate| !taken.contains(candidate)).expect("a free name")
});
if let Some(name) = &name {
taken.push(name.clone());
}
new_faces.push(FaceRecord { id: take_id(), surface: surface.clone(), same_sense, loops: vec![extra], name });
}
}
if !new_faces.is_empty() {
let shell_index = result.shells.iter().position(|shell| !shell.faces.is_empty())
.ok_or_else(|| KernelRefusal::internal(KernelStage::Sew, what, "blend stopped-end rollover: no shell"))?;
result.shells[shell_index].faces.extend(new_faces);
}
result.edges.retain(|edge| !interior.contains(&edge.id));
Ok(())
}
pub(in crate::blend) fn transpose_face(face: &mut FaceRecord) -> Result<(), KernelRefusal> {
let surface = &face.surface;
let rows_u = surface.control_points.len();
let rows_v = surface.control_points.first().map(|row| row.len()).unwrap_or(0);
let mut transposed = vec![Vec::with_capacity(rows_u); rows_v];
for row in &surface.control_points {
for (j, point) in row.iter().enumerate() {
transposed[j].push(*point);
}
}
face.surface = NurbsSurface::new(
surface.degree_v,
surface.degree_u,
surface.knots_v.clone(),
surface.knots_u.clone(),
transposed,
).or_refuse(KernelStage::Refine, "new")?;
for loop_record in &mut face.loops {
for coedge in &mut loop_record.coedges {
for control in &mut coedge.pcurve.control_points {
std::mem::swap(&mut control.x, &mut control.y);
}
}
}
face.same_sense = !face.same_sense;
Ok(())
}
pub(in crate::blend) fn prune_orphan_vertices(result: &mut BrepSolid) {
let used: rustc_hash::FxHashSet<u64> = result
.edges
.iter()
.flat_map(|candidate| [candidate.start_vertex_id, candidate.end_vertex_id])
.collect();
result
.vertices
.retain(|candidate| used.contains(&candidate.id));
}
fn cs_blend_pcurve_reversed(cs_domain: &[f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
crate::sweep_topology::parameter_line(cs_domain[1], 1.0, cs_domain[0], 1.0).or_refuse(KernelStage::Refine, "parameter_line")
}