use super::*;
struct FacePatch {
shell_index: usize,
face_ids: HashSet<u64>,
dropped_loops: Vec<(usize, usize, usize)>,
merges: Vec<FaceMerge>,
edges: HashSet<u64>,
bridges: Vec<EdgeRecord>,
}
struct FaceMerge {
faces: Vec<usize>,
loops: Vec<LoopRecord>,
surface: Option<NurbsSurface>,
}
struct Fragment {
face: usize,
coedges: Vec<CoedgeRecord>,
start: u64,
end: u64,
}
#[derive(Clone, Copy, Default)]
struct EdgeCensus {
selected: usize,
kept: usize,
}
fn census(solid: &BrepSolid, face_ids: &HashSet<u64>) -> HashMap<u64, EdgeCensus> {
let mut counts: HashMap<u64, EdgeCensus> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
let selected = face_ids.contains(&face.id);
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
let entry = counts.entry(coedge.edge_id).or_default();
if selected {
entry.selected += 1;
} else {
entry.kept += 1;
}
}
}
counts
}
fn classify_patch(solid: &BrepSolid, face_ids: &[u64]) -> Option<FacePatch> {
let selected: HashSet<u64> = face_ids.iter().copied().collect();
let mut shells = face_ids
.iter()
.filter_map(|face_id| find_face(solid, *face_id))
.map(|(shell_index, _)| shell_index);
let shell_index = shells.next()?;
if shells.any(|other| other != shell_index) {
return None;
}
let counts = census(solid, &selected);
let patch_edges: HashSet<u64> = counts
.iter()
.filter(|(_, count)| count.selected > 0)
.map(|(edge_id, _)| *edge_id)
.collect();
let edges: HashMap<u64, &EdgeRecord> = solid.edges.iter().map(|edge| (edge.id, edge)).collect();
if patch_edges.iter().any(|edge_id| {
let count = counts[edge_id];
!edges.get(edge_id).is_some_and(|edge| edge.degenerate)
&& count.selected + count.kept != 2
}) {
return None;
}
let mut dropped_loops = Vec::new();
let mut fragments: Vec<Fragment> = Vec::new();
for (shell_position, shell) in solid.shells.iter().enumerate() {
for (face_position, face) in shell.faces.iter().enumerate() {
if selected.contains(&face.id) {
continue;
}
for (loop_index, loop_record) in face.loops.iter().enumerate() {
let consumed: Vec<bool> = loop_record
.coedges
.iter()
.map(|coedge| patch_edges.contains(&coedge.edge_id))
.collect();
if !consumed.iter().any(|flag| *flag) {
continue;
}
if consumed.iter().all(|flag| *flag) {
dropped_loops.push((shell_position, face_position, loop_index));
continue;
}
if shell_position != shell_index {
return None;
}
fragments.extend(loop_fragments(face_position, loop_record, &consumed, &edges)?);
}
}
}
if dropped_loops.is_empty() && fragments.is_empty() {
return None;
}
let (merges, bridges) = splice_fragments(solid, shell_index, &fragments, &patch_edges)?;
Some(FacePatch {
shell_index,
face_ids: selected,
dropped_loops,
merges,
edges: patch_edges,
bridges,
})
}
fn coedge_ends(coedge: &CoedgeRecord, edges: &HashMap<u64, &EdgeRecord>) -> Option<(u64, u64)> {
let edge = edges.get(&coedge.edge_id)?;
Some(if coedge.forward {
(edge.start_vertex_id, edge.end_vertex_id)
} else {
(edge.end_vertex_id, edge.start_vertex_id)
})
}
fn pcurve_ends(coedge: &CoedgeRecord) -> Option<([f64; 2], [f64; 2])> {
let [q0, q1] = coedge.pcurve.domain().ok()?;
let start = coedge.pcurve.evaluate(q0).ok()?;
let end = coedge.pcurve.evaluate(q1).ok()?;
Some(([start.x, start.y], [end.x, end.y]))
}
fn loop_fragments(
face: usize,
loop_record: &LoopRecord,
consumed: &[bool],
edges: &HashMap<u64, &EdgeRecord>,
) -> Option<Vec<Fragment>> {
let count = loop_record.coedges.len();
let first_cut = consumed.iter().position(|flag| *flag)?;
let mut fragments = Vec::new();
let mut run: Vec<CoedgeRecord> = Vec::new();
for step in 1..=count {
let index = (first_cut + step) % count;
if consumed[index] {
if !run.is_empty() {
fragments.push(fragment(face, std::mem::take(&mut run), edges)?);
}
continue;
}
run.push(loop_record.coedges[index].clone());
}
debug_assert!(run.is_empty());
Some(fragments)
}
fn fragment(
face: usize,
coedges: Vec<CoedgeRecord>,
edges: &HashMap<u64, &EdgeRecord>,
) -> Option<Fragment> {
let start = coedge_ends(coedges.first()?, edges)?.0;
let end = coedge_ends(coedges.last()?, edges)?.1;
Some(Fragment {
face,
coedges,
start,
end,
})
}
fn splice_fragments(
solid: &BrepSolid,
shell_index: usize,
fragments: &[Fragment],
patch_edges: &HashSet<u64>,
) -> Option<(Vec<FaceMerge>, Vec<EdgeRecord>)> {
if fragments.is_empty() {
return Some((Vec::new(), Vec::new()));
}
let mut by_start: HashMap<u64, usize> = HashMap::default();
for (index, fragment) in fragments.iter().enumerate() {
if by_start.insert(fragment.start, index).is_some() {
return None;
}
}
let mut used = vec![false; fragments.len()];
let mut circuits: Vec<Vec<usize>> = Vec::new();
for seed in 0..fragments.len() {
if used[seed] {
continue;
}
used[seed] = true;
let mut circuit = vec![seed];
let mut end = fragments[seed].end;
while end != fragments[seed].start {
let next = *by_start.get(&end)?;
if used[next] {
return None;
}
used[next] = true;
circuit.push(next);
end = fragments[next].end;
}
circuits.push(circuit);
}
let scale = solid_model_scale(solid);
let carriers = carrier_classes(solid, shell_index, fragments, scale)?;
let corners = circuit_corners(fragments, &circuits, &carriers)?;
let (rebuilt, bridges) = if corners.is_empty() {
(
circuits
.iter()
.map(|circuit| {
let mut coedges = Vec::new();
let mut faces = Vec::new();
for index in circuit {
coedges.extend(fragments[*index].coedges.iter().cloned());
faces.push(fragments[*index].face);
}
faces.sort_unstable();
faces.dedup();
(coedges, faces)
})
.collect(),
Vec::new(),
)
} else {
rejoin_across_corners(solid, fragments, &circuits, &carriers, &corners)?
};
let mut parent: HashMap<usize, usize> = HashMap::default();
for (_, faces) in &rebuilt {
for face in faces {
parent.entry(*face).or_insert(*face);
}
}
for (_, faces) in &rebuilt {
let mut members = faces.iter();
let anchor = root(&mut parent, *members.next()?);
for face in members {
let other = root(&mut parent, *face);
if other != anchor {
parent.insert(other, anchor);
}
}
}
let mut grouped: HashMap<usize, Vec<usize>> = HashMap::default();
for face in parent.keys().copied().collect::<Vec<usize>>() {
let group = root(&mut parent, face);
grouped.entry(group).or_default().push(face);
}
let carrier_tolerance = (scale * 1e-9).max(1e-12);
let mut next_loop_id = rebuilt
.iter()
.flat_map(|(coedges, _)| coedges.iter().map(|coedge| coedge.id))
.chain(bridges.iter().map(|bridge| bridge.id))
.fold(max_topology_id(solid), u64::max)
+ 1;
let mut edges: HashMap<u64, EdgeRecord> = solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
for bridge in &bridges {
edges.insert(bridge.id, bridge.clone());
}
let mut groups: Vec<usize> = grouped.keys().copied().collect();
groups.sort_unstable();
let mut merges = Vec::with_capacity(groups.len());
for group in groups {
let mut faces = grouped[&group].clone();
faces.sort_unstable();
let host = &solid.shells[shell_index].faces[faces[0]];
for other in faces.iter().skip(1) {
let face = &solid.shells[shell_index].faces[*other];
let one_patch = face.same_sense == host.same_sense
&& crate::face_merge::same_surface(
&face.surface,
&host.surface,
carrier_tolerance,
);
let rejoinable =
one_patch || (!bridges.is_empty() && carriers[other] == carriers[&faces[0]]);
if !rejoinable {
return None;
}
}
let mut loops: Vec<LoopRecord> = Vec::new();
let mut bridged = false;
for (coedges, members) in &rebuilt {
if root(&mut parent, members[0]) != group {
continue;
}
bridged |= coedges
.iter()
.any(|coedge| bridges.iter().any(|bridge| bridge.id == coedge.edge_id));
loops.push(LoopRecord {
id: next_loop_id,
coedges: coedges.clone(),
});
next_loop_id += 1;
}
for face_position in &faces {
let face = &solid.shells[shell_index].faces[*face_position];
for loop_record in &face.loops {
if loop_record
.coedges
.iter()
.any(|coedge| patch_edges.contains(&coedge.edge_id))
{
continue;
}
loops.push(loop_record.clone());
}
}
let surface = if bridged {
let mut candidate = FaceRecord {
id: host.id,
surface: host.surface.clone(),
same_sense: host.same_sense,
loops: loops.clone(),
name: host.name.clone(),
};
regrow_and_refit_carrier(&mut candidate, &edges, scale, "delete_faces_and_heal").ok()?;
if !loops_ride_carrier(&candidate.surface, &candidate.loops, &edges, scale) {
return None;
}
loops = candidate.loops;
Some(candidate.surface)
} else {
None
};
let carrier = surface.as_ref().unwrap_or(&host.surface);
for loop_record in &loops {
if !closes_in_parameter_space(carrier, &loop_record.coedges) {
return None;
}
}
let outer = widest_loop_on(carrier, host.same_sense, host.id, &loops)?;
loops.swap(0, outer);
merges.push(FaceMerge {
faces,
loops,
surface,
});
}
Some((merges, bridges))
}
struct Corner {
incoming: usize,
outgoing: usize,
vertex: u64,
spine: u64,
}
fn loops_ride_carrier(
surface: &NurbsSurface,
loops: &[LoopRecord],
edges: &HashMap<u64, EdgeRecord>,
scale: f64,
) -> bool {
let tolerance = (scale * 1e-7).max(1e-9);
for coedge in loops.iter().flat_map(|loop_record| &loop_record.coedges) {
let Some(edge) = edges.get(&coedge.edge_id) else {
return false;
};
if edge.degenerate {
continue;
}
for step in 0..=8 {
let t = edge.t0 + (edge.t1 - edge.t0) * step as f64 / 8.0;
let Ok(point) = edge.curve.evaluate(t) else {
return false;
};
match crate::project_point_to_surface(surface, point) {
Ok(projection) if projection.distance <= tolerance => {}
_ => return false,
}
}
}
true
}
fn carrier_classes(
solid: &BrepSolid,
shell_index: usize,
fragments: &[Fragment],
scale: f64,
) -> Option<HashMap<usize, usize>> {
let tolerance = (scale * 1e-9).max(1e-12);
let mut classes: HashMap<usize, usize> = HashMap::default();
let mut representatives: Vec<usize> = Vec::new();
for fragment in fragments {
if classes.contains_key(&fragment.face) {
continue;
}
let face = &solid.shells[shell_index].faces[fragment.face];
let mut found = None;
for (index, representative) in representatives.iter().enumerate() {
let other = &solid.shells[shell_index].faces[*representative];
if crate::face_merge::faces_are_cosurface(other, face, tolerance).unwrap_or(false) {
found = Some(index);
break;
}
}
let class = found.unwrap_or_else(|| {
representatives.push(fragment.face);
representatives.len() - 1
});
classes.insert(fragment.face, class);
}
Some(classes)
}
fn circuit_corners(
fragments: &[Fragment],
circuits: &[Vec<usize>],
carriers: &HashMap<usize, usize>,
) -> Option<Vec<Corner>> {
let mut corners = Vec::new();
for circuit in circuits {
for (position, index) in circuit.iter().copied().enumerate() {
let next = circuit[(position + 1) % circuit.len()];
if next == index || carriers[&fragments[index].face] == carriers[&fragments[next].face]
{
continue;
}
let arriving = fragments[index].coedges.last()?.edge_id;
let leaving = fragments[next].coedges.first()?.edge_id;
if arriving != leaving {
return None;
}
corners.push(Corner {
incoming: index,
outgoing: next,
vertex: fragments[index].end,
spine: arriving,
});
}
}
Some(corners)
}
fn rejoin_across_corners(
solid: &BrepSolid,
fragments: &[Fragment],
circuits: &[Vec<usize>],
carriers: &HashMap<usize, usize>,
corners: &[Corner],
) -> Option<(Vec<(Vec<CoedgeRecord>, Vec<usize>)>, Vec<EdgeRecord>)> {
let signature = |corner: &Corner| {
(
carriers[&fragments[corner.incoming].face],
carriers[&fragments[corner.outgoing].face],
)
};
let mut partner = vec![usize::MAX; corners.len()];
for (index, corner) in corners.iter().enumerate() {
let (arriving, leaving) = signature(corner);
let mut matches = corners
.iter()
.enumerate()
.filter(|(other, candidate)| *other != index && signature(candidate) == (leaving, arriving));
let (found, _) = matches.next()?;
if matches.next().is_some() {
return None;
}
partner[index] = found;
}
if (0..corners.len()).any(|index| partner[partner[index]] != index) {
return None;
}
let scale = solid_model_scale(solid);
let tolerance = (scale * 1e-7).max(1e-9);
let mut next_id = max_topology_id(solid) + 1;
let mut bridges: Vec<EdgeRecord> = Vec::new();
let mut bridge_at: HashMap<usize, (u64, bool)> = HashMap::default();
for index in 0..corners.len() {
if bridge_at.contains_key(&index) {
continue;
}
let other = partner[index];
let bridge = bridge_edge(solid, &corners[index], &corners[other], next_id, tolerance)?;
next_id += 1;
bridge_at.insert(index, (bridge.id, true));
bridge_at.insert(other, (bridge.id, false));
bridges.push(bridge);
}
let ends_at: HashMap<usize, usize> = corners
.iter()
.enumerate()
.map(|(index, corner)| (corner.incoming, index))
.collect();
let mut arcs: Vec<(usize, Vec<usize>, usize)> = Vec::new();
for circuit in circuits {
let length = circuit.len();
let Some(first) = (0..length).find(|position| ends_at.contains_key(&circuit[*position]))
else {
return None;
};
let mut open: Vec<usize> = Vec::new();
let mut start = ends_at[&circuit[first]];
for step in 1..=length {
let index = circuit[(first + step) % length];
open.push(index);
if let Some(corner) = ends_at.get(&index) {
arcs.push((start, std::mem::take(&mut open), *corner));
start = *corner;
}
}
if !open.is_empty() {
return None;
}
}
let starts_at: HashMap<usize, usize> = arcs
.iter()
.enumerate()
.map(|(index, arc)| (arc.0, index))
.collect();
if starts_at.len() != arcs.len() {
return None;
}
let mut used = vec![false; arcs.len()];
let mut rebuilt: Vec<(Vec<CoedgeRecord>, Vec<usize>)> = Vec::new();
for seed in 0..arcs.len() {
if used[seed] {
continue;
}
used[seed] = true;
let mut coedges: Vec<CoedgeRecord> = Vec::new();
let mut faces: Vec<usize> = Vec::new();
let mut current = seed;
loop {
for fragment in &arcs[current].1 {
coedges.extend(fragments[*fragment].coedges.iter().cloned());
faces.push(fragments[*fragment].face);
}
let (edge_id, forward) = bridge_at[&arcs[current].2];
coedges.push(CoedgeRecord {
id: next_id,
edge_id,
forward,
pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0)).ok()?,
});
next_id += 1;
let next = *starts_at.get(&partner[arcs[current].2])?;
if next == seed {
break;
}
if used[next] {
return None;
}
used[next] = true;
current = next;
}
faces.sort_unstable();
faces.dedup();
rebuilt.push((coedges, faces));
}
Some((rebuilt, bridges))
}
fn bridge_edge(
solid: &BrepSolid,
from: &Corner,
to: &Corner,
id: u64,
tolerance: f64,
) -> Option<EdgeRecord> {
let start = edge_point(solid, from.vertex).ok()?;
let end = edge_point(solid, to.vertex).ok()?;
let span = end.sub(start);
let length = span.length();
if length <= tolerance {
return None;
}
let direction = span.scale(1.0 / length);
for (corner, anchor, sense) in [(from, start, -1.0f64), (to, end, 1.0f64)] {
let spine = solid.edges.iter().find(|edge| edge.id == corner.spine)?;
let mut reach = 0.0f64;
for step in 0..=8 {
let t = spine.t0 + (spine.t1 - spine.t0) * step as f64 / 8.0;
let point = spine.curve.evaluate(t).ok()?;
let offset = point.sub(anchor);
let along = offset.dot(direction);
if offset.sub(direction.scale(along)).length() > tolerance {
return None;
}
if along * sense < -tolerance {
return None;
}
if along * sense > reach * sense {
reach = along;
}
}
if reach * sense <= tolerance {
return None;
}
}
let curve = make_line(start, end).ok()?;
let [t0, t1] = curve.domain().ok()?;
Some(EdgeRecord {
id,
curve,
t0,
t1,
start_vertex_id: from.vertex,
end_vertex_id: to.vertex,
degenerate: false,
name: None,
})
}
fn root(parent: &mut HashMap<usize, usize>, mut node: usize) -> usize {
while let Some(next) = parent.get(&node).copied() {
if next == node {
break;
}
node = next;
}
node
}
fn closes_in_parameter_space(surface: &NurbsSurface, coedges: &[CoedgeRecord]) -> bool {
let (u_span, v_span) = match parameter_spans(surface) {
Some(spans) => spans,
None => return false,
};
let tolerance = (u_span + v_span) * 1e-6;
let mut ends: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(coedges.len());
for coedge in coedges {
match pcurve_ends(coedge) {
Some(pair) => ends.push(pair),
None => return false,
}
}
for index in 0..ends.len() {
let leaving = ends[index].1;
let arriving = ends[(index + 1) % ends.len()].0;
let gap = (leaving[0] - arriving[0]).hypot(leaving[1] - arriving[1]);
if gap > tolerance {
return false;
}
}
true
}
fn parameter_spans(surface: &NurbsSurface) -> Option<(f64, f64)> {
let u = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)
.ok()?
.domain();
let v = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)
.ok()?
.domain();
Some((u[1] - u[0], v[1] - v[0]))
}
fn single_loop_area(host: &FaceRecord, loop_record: &LoopRecord) -> Result<f64, String> {
parameter_space_area(&FaceRecord {
id: host.id,
surface: host.surface.clone(),
same_sense: host.same_sense,
loops: vec![loop_record.clone()],
name: None,
})
}
fn widest_loop_on(
surface: &NurbsSurface,
same_sense: bool,
id: u64,
loops: &[LoopRecord],
) -> Option<usize> {
let host = FaceRecord {
id,
surface: surface.clone(),
same_sense,
loops: Vec::new(),
name: None,
};
let mut widest = (0usize, f64::NEG_INFINITY);
for (index, loop_record) in loops.iter().enumerate() {
let area = single_loop_area(&host, loop_record).ok()?.abs();
if area > widest.1 {
widest = (index, area);
}
}
(widest.1 > f64::NEG_INFINITY).then_some(widest.0)
}
fn euler_characteristic(solid: &BrepSolid) -> i64 {
let referenced: HashSet<u64> = solid
.edges
.iter()
.filter(|edge| !edge.degenerate)
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect();
let vertices = solid
.vertices
.iter()
.filter(|vertex| referenced.contains(&vertex.id))
.count() as i64;
let edges = solid.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
let faces = solid
.shells
.iter()
.map(|shell| shell.faces.len())
.sum::<usize>() as i64;
let holes: i64 = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.map(|face| face.loops.len().saturating_sub(1) as i64)
.sum();
vertices - edges + faces - holes
}
fn face_label(face: &FaceRecord) -> String {
match &face.name {
Some(name) => format!("`{name}`"),
None => format!("face {}", face.id),
}
}
fn dropped_per_face(patch: &FacePatch) -> HashMap<(usize, usize), Vec<usize>> {
let mut per_face: HashMap<(usize, usize), Vec<usize>> = HashMap::default();
for (shell_position, face_position, loop_index) in &patch.dropped_loops {
per_face
.entry((*shell_position, *face_position))
.or_default()
.push(*loop_index);
}
per_face
}
fn cap_preconditions(solid: &BrepSolid, patch: &FacePatch, op: &str) -> Result<(), String> {
for ((shell_position, face_position), loop_indices) in &dropped_per_face(patch) {
let face = &solid.shells[*shell_position].faces[*face_position];
let rebuilt = *shell_position == patch.shell_index
&& patch
.merges
.iter()
.any(|merge| merge.faces.contains(face_position));
if !rebuilt && loop_indices.len() >= face.loops.len() {
return Err(format!(
"{op}: the selection is the whole boundary of {} — it is part of the \
pocket, not the face the pocket was sunk into. Select it as well \
(a patch is capped by the face AROUND it, which has to keep a loop).",
face_label(face)
));
}
if face.loops.len() < 2 {
continue;
}
let mut areas = Vec::with_capacity(face.loops.len());
for index in 0..face.loops.len() {
areas.push(loop_signed_area(face, index)?);
}
let host = (0..areas.len())
.max_by(|a, b| areas[*a].abs().total_cmp(&areas[*b].abs()))
.expect("the face has at least two loops here");
for loop_index in loop_indices {
if *loop_index == host || areas[host] * areas[*loop_index] >= 0.0 {
return Err(format!(
"{op}: the loop the selection would leave open in {} bounds that \
face's material rather than a hole in it — capping it would erase \
the face (deferred)",
face_label(face)
));
}
}
}
for merge in &patch.merges {
let host = &solid.shells[patch.shell_index].faces[merge.faces[0]];
if let Some(surface) = &merge.surface {
let rejoined = FaceRecord {
id: host.id,
surface: surface.clone(),
same_sense: host.same_sense,
loops: merge.loops.clone(),
name: None,
};
let mut before = 0.0;
for face_position in &merge.faces {
before += crate::face_area(&solid.shells[patch.shell_index].faces[*face_position])?;
}
let after = crate::face_area(&rejoined)?;
let winding = parameter_space_area(&rejoined)?;
let host_winding = loop_signed_area(host, 0)?;
if after <= before * (1.0 + 1e-9) || winding * host_winding <= 0.0 {
return Err(format!(
"{op}: rejoining {} across the openings its free boundary leaves \
would not take in the region they enclose (area {before} -> {after}, \
winding {host_winding} -> {winding}) — that boundary bounds material \
rather than an opening (deferred)",
face_label(host)
));
}
continue;
}
let mut before = 0.0;
for face_position in &merge.faces {
let face = &solid.shells[patch.shell_index].faces[*face_position];
for loop_record in &face.loops {
before += single_loop_area(host, loop_record)?;
}
}
let mut after = 0.0;
for loop_record in &merge.loops {
after += single_loop_area(host, loop_record)?;
}
if (after - before) * before.signum() <= before.abs() * 1e-9 {
return Err(format!(
"{op}: rejoining {} would not take in the region its free boundary \
encloses (parameter-space area {before} -> {after}) — that boundary \
bounds material rather than an opening (deferred)",
face_label(host)
));
}
}
Ok(())
}
fn cap_face_patch(solid: &BrepSolid, patch: &FacePatch, op: &str) -> Result<BrepSolid, String> {
cap_preconditions(solid, patch, op)?;
let per_face = dropped_per_face(patch);
let mut healed = solid.clone();
let mut merged_away: HashSet<u64> = HashSet::default();
for merge in &patch.merges {
let faces = &mut healed.shells[patch.shell_index].faces;
faces[merge.faces[0]].loops = merge.loops.clone();
if let Some(surface) = &merge.surface {
faces[merge.faces[0]].surface = surface.clone();
}
for face_position in merge.faces.iter().skip(1) {
merged_away.insert(faces[*face_position].id);
}
}
for ((shell_position, face_position), loop_indices) in &per_face {
if *shell_position == patch.shell_index
&& patch
.merges
.iter()
.any(|merge| merge.faces.contains(face_position))
{
continue;
}
let mut loop_indices = loop_indices.clone();
loop_indices.sort_unstable_by(|a, b| b.cmp(a));
for loop_index in loop_indices {
healed.shells[*shell_position].faces[*face_position]
.loops
.remove(loop_index);
}
}
for shell in &mut healed.shells {
shell
.faces
.retain(|face| !patch.face_ids.contains(&face.id) && !merged_away.contains(&face.id));
}
healed.edges.retain(|edge| !patch.edges.contains(&edge.id));
healed.edges.extend(patch.bridges.iter().cloned());
let used: HashSet<u64> = healed
.edges
.iter()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect();
healed.vertices.retain(|vertex| used.contains(&vertex.id));
if !faces_are_connected(&healed.shells[patch.shell_index].faces) {
return Err(format!(
"{op}: the selected faces are what joins two otherwise separate parts of \
the body — removing them would sever the solid, which this operation \
cannot represent (deferred)"
));
}
let shift = euler_characteristic(solid) - euler_characteristic(&healed);
if shift % 2 != 0 {
return Err(format!(
"{op}: the selection does not close into whole handles \
(Euler characteristic shifts by an odd {shift}) — refusing rather than \
emitting a solid whose genus is a guess"
));
}
healed.genus += shift / 2;
if healed.genus < 0 {
return Err(format!(
"{op}: capping the selection leaves genus {}, so the solid's stated genus \
did not account for the feature it carries (deferred)",
healed.genus
));
}
let issues = healed.validate();
if !issues.is_empty() {
return Err(format!("{op}: the capped solid failed validation: {issues:?}"));
}
Ok(healed)
}
fn delete_one_face(solid: &BrepSolid, face_id: u64, op: &str) -> Result<BrepSolid, String> {
if let Some(patch) = classify_patch(solid, &[face_id]) {
if cap_preconditions(solid, &patch, op).is_ok() {
return cap_face_patch(solid, &patch, op);
}
}
delete_face_and_heal(solid, face_id)
}
fn connected_components(solid: &BrepSolid, face_ids: &[u64]) -> Vec<Vec<u64>> {
let selected: HashSet<u64> = face_ids.iter().copied().collect();
let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::default();
let mut by_edge: HashMap<u64, Vec<u64>> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if !selected.contains(&face.id) {
continue;
}
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
by_edge.entry(coedge.edge_id).or_default().push(face.id);
}
}
for sharing in by_edge.values() {
for a in sharing {
for b in sharing {
if a != b {
adjacency.entry(*a).or_default().push(*b);
}
}
}
}
let position: HashMap<u64, usize> = face_ids
.iter()
.enumerate()
.map(|(index, face_id)| (*face_id, index))
.collect();
let mut seen: HashSet<u64> = HashSet::default();
let mut components: Vec<Vec<u64>> = Vec::new();
for start in face_ids {
if !seen.insert(*start) {
continue;
}
let mut component = vec![*start];
let mut stack = vec![*start];
while let Some(face_id) = stack.pop() {
for next in adjacency.get(&face_id).into_iter().flatten() {
if seen.insert(*next) {
component.push(*next);
stack.push(*next);
}
}
}
component.sort_unstable_by_key(|face_id| position[face_id]);
components.push(component);
}
components
}
pub fn delete_faces_and_heal(solid: &BrepSolid, face_ids: &[u64]) -> Result<BrepSolid, String> {
let op = "delete_faces_and_heal";
let mut seen: HashSet<u64> = HashSet::default();
let face_ids: Vec<u64> = face_ids
.iter()
.copied()
.filter(|face_id| seen.insert(*face_id))
.collect();
if face_ids.is_empty() {
return Err(format!("{op}: no faces selected"));
}
for face_id in &face_ids {
if find_face(solid, *face_id).is_none() {
return Err(format!("{op}: no face with id {face_id}"));
}
}
if face_ids.len() == 1 {
return Ok(coalesce_healed_edges(&delete_one_face(solid, face_ids[0], op)?));
}
if let Some(patch) = classify_patch(solid, &face_ids) {
return Ok(coalesce_healed_edges(&cap_face_patch(solid, &patch, op)?));
}
if let Some(group) = classify_corner_blend_group(solid, &face_ids) {
return Ok(coalesce_healed_edges(&heal_corner_blend_group(
solid, &group, op,
)?));
}
let components = connected_components(solid, &face_ids);
let mut healed = solid.clone();
if components.len() < 2 {
for face_id in &face_ids {
healed = delete_one_face(&healed, *face_id, op)?;
}
return Ok(coalesce_healed_edges(&healed));
}
let mut chained: Vec<Vec<u64>> = Vec::new();
for component in components {
if component.len() == 1 {
chained.push(component);
continue;
}
match classify_patch(&healed, &component) {
Some(patch) => healed = cap_face_patch(&healed, &patch, op)?,
None => chained.push(component),
}
}
for component in chained {
if component.len() > 1 {
if let Some(group) = classify_corner_blend_group(&healed, &component) {
healed = heal_corner_blend_group(&healed, &group, op)?;
continue;
}
}
for face_id in &component {
healed = delete_one_face(&healed, *face_id, op)?;
}
}
Ok(coalesce_healed_edges(&healed))
}
fn coalesce_healed_edges(solid: &BrepSolid) -> BrepSolid {
let tolerance = (solid_model_scale(solid) * 1e-7).max(1e-9);
match crate::merge_curve_continuation_edges(solid, tolerance) {
Ok(merged) if merged.validate().len() <= solid.validate().len() => merged,
_ => solid.clone(),
}
}