use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
use crate::topology::BrepSolid;
use crate::Vec3;
pub(crate) const FULL_WIDTH_COLLAPSE_UNSOUND: &str =
"blend: the full-width corner does not collapse to a sound body —";
pub(crate) const FULL_WIDTH_COLLAPSE_WHAT: &str = "full_width_collapse";
pub(crate) const FULL_WIDTH_COLLAPSE_EDGE_WHAT: &str = "full_width_collapse_edge";
pub(crate) fn is_full_width_collapse(refusal: &KernelRefusal) -> bool {
matches!(&refusal.class, RefusalClass::UnsupportedGeometry { what } if what == FULL_WIDTH_COLLAPSE_WHAT) ||
matches!(&refusal.class, RefusalClass::Internal { what } if what == FULL_WIDTH_COLLAPSE_EDGE_WHAT)
}
#[derive(Default)]
pub(in crate::blend) struct Collapse {
pub(in crate::blend) merged_edges: Vec<(u64, u64)>,
pub(in crate::blend) dropped_faces: Vec<u64>,
pub(in crate::blend) moved: f64,
}
impl Collapse {
pub(in crate::blend) fn changed(&self) -> bool {
!self.merged_edges.is_empty() || !self.dropped_faces.is_empty() || self.moved > 0.0
}
pub(in crate::blend) fn edge(&self, mut edge_id: u64) -> u64 {
while let Some((_, kept)) = self.merged_edges.iter().find(|(merged, _)| *merged == edge_id) {
edge_id = *kept;
}
edge_id
}
}
const COINCIDENCE_SAMPLES: usize = 16;
pub(in crate::blend) fn collapse_full_width(
input: &BrepSolid,
result: &mut BrepSolid,
identified: &[(u64, u64)],
collapsed: bool,
band: f64,
) -> Result<Collapse, KernelRefusal> {
let mut collapse = Collapse::default();
if !collapsed && identified.is_empty() {
return Ok(collapse);
}
let is_input_vertex = |id: u64| input.vertices.iter().any(|vertex| vertex.id == id);
let point_of = |solid: &BrepSolid, id: u64| -> Result<Vec3, KernelRefusal> {
solid
.vertices
.iter()
.find(|vertex| vertex.id == id)
.map(|vertex| vertex.point)
.ok_or_else(|| format!("{FULL_WIDTH_COLLAPSE_UNSOUND} vertex {id} is missing")).or_refuse(KernelStage::Refine, "ok_or_else")
};
let mut representative: Vec<(u64, u64)> = Vec::new();
let find = |representative: &[(u64, u64)], mut id: u64| {
while let Some((_, to)) = representative.iter().find(|(from, _)| *from == id) {
id = *to;
}
id
};
for &(a, b) in identified {
let (a, b) = (find(&representative, a), find(&representative, b));
if a == b {
continue;
}
let (kept, merged) = match (is_input_vertex(a), is_input_vertex(b)) {
(true, true) => {
return Err(KernelRefusal::unsupported(KernelStage::Validate, FULL_WIDTH_COLLAPSE_WHAT, format!(
"{FULL_WIDTH_COLLAPSE_UNSOUND} a collapsed rail would identify two vertices \
of the part, {a} and {b}"
)))
}
(true, false) => (a, b),
(false, true) => (b, a),
(false, false) => (a.min(b), a.max(b)),
};
let gap = point_of(result, kept)?.sub(point_of(result, merged)?).length();
if gap > band {
return Err(KernelRefusal::unsupported(KernelStage::Validate, FULL_WIDTH_COLLAPSE_WHAT, format!(
"{FULL_WIDTH_COLLAPSE_UNSOUND} a collapsed rail's two rims stand {gap:.3e} apart, \
more than the {band:.3e} band that called the rail a point"
)));
}
collapse.moved = collapse.moved.max(gap);
representative.push((merged, kept));
}
if !representative.is_empty() {
for edge in &mut result.edges {
edge.start_vertex_id = find(&representative, edge.start_vertex_id);
edge.end_vertex_id = find(&representative, edge.end_vertex_id);
}
result
.vertices
.retain(|vertex| !representative.iter().any(|(merged, _)| *merged == vertex.id));
}
let loop_edges = |face: &crate::topology::FaceRecord| -> Vec<Vec<u64>> {
face.loops
.iter()
.map(|loop_record| loop_record.coedges.iter().map(|c| c.edge_id).collect())
.collect()
};
let original: rustc_hash::FxHashMap<u64, Vec<Vec<u64>>> = input
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| (face.id, loop_edges(face)))
.collect();
loop {
let face_ids: Vec<u64> = result
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| original.get(&face.id) != Some(&loop_edges(face)))
.map(|face| face.id)
.collect();
let mut progressed = false;
for face_id in face_ids {
let Some(face) = result.shells.iter().flat_map(|shell| &shell.faces).find(|f| f.id == face_id)
else {
continue;
};
let empty = face.loops.iter().filter(|l| l.coedges.is_empty()).count();
if empty > 0 {
if empty != face.loops.len() {
return Err(KernelRefusal::unsupported(KernelStage::Validate, FULL_WIDTH_COLLAPSE_WHAT, format!(
"{FULL_WIDTH_COLLAPSE_UNSOUND} face {face_id} lost {empty} of its \
{} loops whole",
face.loops.len()
)));
}
drop_face(result, face_id);
collapse.dropped_faces.push(face_id);
progressed = true;
continue;
}
if face.loops.len() != 1 || face.loops[0].coedges.len() != 2 {
continue;
}
let [first, second] = [&face.loops[0].coedges[0], &face.loops[0].coedges[1]];
if first.edge_id == second.edge_id {
continue;
}
let edge_of = |id: u64| result.edges.iter().find(|edge| edge.id == id);
let (Some(a), Some(b)) = (edge_of(first.edge_id), edge_of(second.edge_id)) else {
return Err(KernelRefusal::internal(KernelStage::Validate, FULL_WIDTH_COLLAPSE_EDGE_WHAT, format!(
"{FULL_WIDTH_COLLAPSE_UNSOUND} face {face_id} uses a missing edge"
)));
};
let aligned = a.start_vertex_id == b.start_vertex_id && a.end_vertex_id == b.end_vertex_id;
let opposed = a.start_vertex_id == b.end_vertex_id && a.end_vertex_id == b.start_vertex_id;
if !(aligned || opposed) || a.start_vertex_id == a.end_vertex_id {
continue;
}
let mut affine = true;
let mut coincident = true;
for sample in 0..=COINCIDENCE_SAMPLES {
let fraction = sample as f64 / COINCIDENCE_SAMPLES as f64;
let on_b = b.curve.evaluate(b.t0 + (b.t1 - b.t0) * fraction).or_refuse(KernelStage::Refine, "evaluate")?;
let a_fraction = if aligned { fraction } else { 1.0 - fraction };
let on_a = a.curve.evaluate(a.t0 + (a.t1 - a.t0) * a_fraction).or_refuse(KernelStage::Refine, "evaluate")?;
if on_a.sub(on_b).length() <= band {
continue;
}
affine = false;
let foot = crate::project_point_to_curve(&a.curve, on_b).or_refuse(KernelStage::Refine, "project_point_to_curve")?;
let inside = foot.u >= a.t0.min(a.t1) - 1e-9 && foot.u <= a.t0.max(a.t1) + 1e-9;
if foot.distance > band || !inside {
coincident = false;
break;
}
}
if !coincident {
continue;
}
let a_input = input.edges.iter().any(|edge| edge.id == a.id);
let b_input = input.edges.iter().any(|edge| edge.id == b.id);
let (kept, merged) = if a_input || (!b_input && a.id < b.id) {
(a.id, b.id)
} else {
(b.id, a.id)
};
drop_face(result, face_id);
collapse.dropped_faces.push(face_id);
merge_edge(result, merged, kept, affine)?;
collapse.merged_edges.push((merged, kept));
progressed = true;
}
if !progressed {
break;
}
}
Ok(collapse)
}
fn drop_face(result: &mut BrepSolid, face_id: u64) {
for shell in &mut result.shells {
shell.faces.retain(|face| face.id != face_id);
}
}
fn merge_edge(
result: &mut BrepSolid,
merged: u64,
kept: u64,
affine: bool,
) -> Result<(), KernelRefusal> {
let edge_of = |id: u64| {
result
.edges
.iter()
.find(|edge| edge.id == id)
.cloned()
.ok_or_else(|| format!("{FULL_WIDTH_COLLAPSE_UNSOUND} edge {id} is missing"))
};
let (merged_edge, kept_edge) = (edge_of(merged).or_refuse(KernelStage::Refine, "edge_of")?, edge_of(kept).or_refuse(KernelStage::Refine, "edge_of")?);
let flipped = merged_edge.start_vertex_id != kept_edge.start_vertex_id;
for face in result.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
let surface = face.surface.clone();
for coedge in face.loops.iter_mut().flat_map(|loop_record| &mut loop_record.coedges) {
if coedge.edge_id != merged {
continue;
}
coedge.edge_id = kept;
if flipped {
coedge.forward = !coedge.forward;
}
if !affine {
let forward = coedge.forward;
let at = |fraction: f64| {
let along = if forward { fraction } else { 1.0 - fraction };
kept_edge.curve.evaluate(kept_edge.t0 + (kept_edge.t1 - kept_edge.t0) * along)
};
let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
let breaks = super::track_fit::curve_breaks(
&kept_edge.curve,
kept_edge.t0,
kept_edge.t1,
forward,
);
let fit =
super::track_fit::fit_curve_track(&surface, &at, &breaks, floor, 256, 4096)?;
if !fit.on_floor {
return Err(KernelRefusal::unsupported(KernelStage::Validate, FULL_WIDTH_COLLAPSE_WHAT, format!(
"{FULL_WIDTH_COLLAPSE_UNSOUND} the merged edge {kept}'s pcurve misses it by \
{:.3e} at {} samples against a floor of {floor:.1e}",
fit.miss, fit.samples
)));
}
coedge.pcurve = fit.curve;
}
}
}
result.edges.retain(|edge| edge.id != merged);
Ok(())
}
pub(in crate::blend) fn check_collapse_closure(
input: &BrepSolid,
result: &BrepSolid,
) -> Result<(), KernelRefusal> {
let report = crate::shell_vector_areas(result);
if !report.is_flagged() && report.unreadable.is_empty() {
return Ok(());
}
if crate::shell_vector_areas(input).is_flagged() {
return Ok(());
}
let (residual, bar) = report
.worst_shell()
.map_or((f64::NAN, f64::NAN), |shell| (shell.residual(), shell.bar));
Err(KernelRefusal::unsupported(KernelStage::Validate, FULL_WIDTH_COLLAPSE_WHAT, format!(
"{FULL_WIDTH_COLLAPSE_UNSOUND} its trims close to {residual:.3e} against a bar of \
{bar:.3e}{}",
if report.unreadable.is_empty() {
String::new()
} else {
format!(" ({} faces unreadable)", report.unreadable.len())
}
)))
}