use super::*;
pub(super) struct CornerStrip {
face_id: u64,
boundary: [u64; 4],
neighbours: [u64; 4],
corner_index: usize,
walls: [usize; 2],
cap_index: usize,
}
impl CornerStrip {
fn wall_ids(&self) -> [u64; 2] {
[self.neighbours[self.walls[0]], self.neighbours[self.walls[1]]]
}
fn cap_id(&self) -> u64 {
self.neighbours[self.cap_index]
}
}
pub(super) struct CornerBlendGroup {
shell_index: usize,
corner_id: u64,
corner_edges: Vec<u64>,
strips: Vec<CornerStrip>,
}
fn read_corner_strip(
solid: &BrepSolid,
strip_id: u64,
corner_id: u64,
selected: &HashSet<u64>,
) -> Option<CornerStrip> {
let (shell_index, face_index) = find_face(solid, strip_id)?;
let face = &solid.shells[shell_index].faces[face_index];
if face.loops.len() != 1 || face.loops[0].coedges.len() != 4 {
return None;
}
let mut boundary = [0u64; 4];
for (index, coedge) in face.loops[0].coedges.iter().enumerate() {
boundary[index] = coedge.edge_id;
}
if boundary.iter().copied().collect::<HashSet<u64>>().len() != 4 {
return None;
}
let mut neighbours = [0u64; 4];
for (index, &edge_id) in boundary.iter().enumerate() {
neighbours[index] = other_face_of_edge(solid, edge_id, strip_id).ok()?;
}
if neighbours.iter().copied().collect::<HashSet<u64>>().len() != 4 {
return None;
}
let corner_index = neighbours.iter().position(|id| *id == corner_id)?;
let walls = [(corner_index + 1) % 4, (corner_index + 3) % 4];
let cap_index = (corner_index + 2) % 4;
for index in [walls[0], walls[1], cap_index] {
if selected.contains(&neighbours[index]) {
return None;
}
}
Some(CornerStrip {
face_id: strip_id,
boundary,
neighbours,
corner_index,
walls,
cap_index,
})
}
pub(super) fn classify_corner_blend_group(
solid: &BrepSolid,
face_ids: &[u64],
) -> Option<CornerBlendGroup> {
if face_ids.len() != 4 {
return None;
}
let selected: HashSet<u64> = face_ids.iter().copied().collect();
if selected.len() != 4 {
return None;
}
for &corner_id in face_ids {
let Some((shell_index, face_index)) = find_face(solid, corner_id) else {
return None;
};
let corner = &solid.shells[shell_index].faces[face_index];
if corner.loops.len() != 1 || corner.loops[0].coedges.len() != 3 {
continue;
}
let corner_edges: Vec<u64> = corner.loops[0]
.coedges
.iter()
.map(|coedge| coedge.edge_id)
.collect();
if corner_edges.iter().copied().collect::<HashSet<u64>>().len() != 3 {
continue;
}
let mut strips: Vec<CornerStrip> = Vec::with_capacity(3);
for &edge_id in &corner_edges {
let Ok(strip_id) = other_face_of_edge(solid, edge_id, corner_id) else {
break;
};
if !selected.contains(&strip_id) {
break;
}
let Some(strip) = read_corner_strip(solid, strip_id, corner_id, &selected) else {
break;
};
if find_face(solid, strip_id).map(|(shell, _)| shell) != Some(shell_index) {
break;
}
strips.push(strip);
}
if strips.len() != 3 {
continue;
}
if strips
.iter()
.map(|strip| strip.face_id)
.collect::<HashSet<u64>>()
.len()
!= 3
{
continue;
}
let mut wall_uses: HashMap<u64, usize> = HashMap::default();
for strip in &strips {
for wall in strip.wall_ids() {
*wall_uses.entry(wall).or_insert(0) += 1;
}
}
if wall_uses.len() != 3 || wall_uses.values().any(|count| *count != 2) {
continue;
}
if strips
.iter()
.any(|strip| wall_uses.contains_key(&strip.cap_id()))
{
continue;
}
return Some(CornerBlendGroup {
shell_index,
corner_id,
corner_edges,
strips,
});
}
None
}
fn rederive_side_edge_on_planes(
solid: &BrepSolid,
edge: &EdgeRecord,
planes: &HashMap<u64, Plane>,
start_target: Option<(u64, Vec3)>,
end_target: Option<(u64, Vec3)>,
plane_tolerance: f64,
tolerance: f64,
) -> Result<Option<EdgeRecord>, String> {
let mut flanking: Vec<u64> = Vec::new();
for shell in &solid.shells {
for face in &shell.faces {
if face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| coedge.edge_id == edge.id)
{
flanking.push(face.id);
}
}
}
if flanking.len() != 2 {
return Ok(None);
}
let (Some(plane_a), Some(plane_b)) = (planes.get(&flanking[0]), planes.get(&flanking[1])) else {
return Ok(None);
};
let Some(line) = intersect_planes(plane_a, plane_b) else {
return Ok(None);
};
let on_line = |point: Vec3| -> bool {
let along = point.sub(line.point).dot(line.dir);
point.sub(line.point.add(line.dir.scale(along))).length() <= plane_tolerance
};
let start_point = match start_target {
Some((_, point)) => point,
None => edge.curve.evaluate(edge.t0)?,
};
let end_point = match end_target {
Some((_, point)) => point,
None => edge.curve.evaluate(edge.t1)?,
};
if !on_line(start_point) || !on_line(end_point) {
return Ok(None);
}
if start_point.sub(end_point).length() <= tolerance {
return Ok(None);
}
let mut record = edge.clone();
record.curve = make_line(start_point, end_point)?;
record.t0 = 0.0;
record.t1 = 1.0;
if let Some((vertex, _)) = start_target {
record.start_vertex_id = vertex;
}
if let Some((vertex, _)) = end_target {
record.end_vertex_id = vertex;
}
Ok(Some(record))
}
struct StripPlan {
far_point: Vec3,
corner_point: Vec3,
}
pub(super) fn heal_corner_blend_group(
solid: &BrepSolid,
group: &CornerBlendGroup,
op: &str,
) -> Result<BrepSolid, String> {
let mut solid = solid.clone();
let scale = solid_model_scale(&solid);
let tolerance = (scale * 1e-7).max(1e-9);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let mut planes: HashMap<u64, Plane> = HashMap::default();
for strip in &group.strips {
for neighbour_id in strip
.wall_ids()
.into_iter()
.chain(std::iter::once(strip.cap_id()))
{
if planes.contains_key(&neighbour_id) {
continue;
}
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("{op}: missing neighbour {neighbour_id}"))?;
let plane = plane_of_surface(
&solid.shells[ns].faces[nf].surface,
plane_tolerance,
op,
)?;
planes.insert(neighbour_id, plane);
}
}
let wall_ids: HashSet<u64> = group
.strips
.iter()
.flat_map(|strip| strip.wall_ids())
.collect();
let group_edges: HashSet<u64> = group
.corner_edges
.iter()
.copied()
.chain(
group
.strips
.iter()
.flat_map(|strip| strip.boundary.iter().copied()),
)
.collect();
let mut group_vertices: Vec<u64> = Vec::new();
for edge in &solid.edges {
if group_edges.contains(&edge.id) {
group_vertices.push(edge.start_vertex_id);
group_vertices.push(edge.end_vertex_id);
}
}
group_vertices.sort_unstable();
group_vertices.dedup();
if group_vertices.is_empty() {
return Err(format!("{op}: corner blend group has no vertices"));
}
let mut centre = Vec3::default();
for &vertex_id in &group_vertices {
centre = centre.add(edge_point(&solid, vertex_id)?);
}
centre = centre.scale(1.0 / group_vertices.len() as f64);
let mut reach = 0.0f64;
for &vertex_id in &group_vertices {
reach = reach.max(edge_point(&solid, vertex_id)?.sub(centre).length());
}
let reach = reach * 3.0 + tolerance;
let mut plans: Vec<StripPlan> = Vec::with_capacity(3);
for strip in &group.strips {
let [wall_a, wall_b] = strip.wall_ids();
let line = intersect_planes(&planes[&wall_a], &planes[&wall_b]).ok_or_else(|| {
format!(
"{op}: the walls flanking blend face {} are parallel — they cannot \
re-intersect into a sharp edge",
strip.face_id
)
})?;
let far_point = intersect_line_plane(&line, &planes[&strip.cap_id()])
.filter(|point| point.sub(centre).length() <= reach)
.ok_or_else(|| {
format!(
"{op}: the sharp edge recovered for blend face {} does not cross the \
face it runs out into",
strip.face_id
)
})?;
let third = wall_ids
.iter()
.copied()
.find(|id| *id != wall_a && *id != wall_b)
.ok_or_else(|| format!("{op}: the corner's walls do not close a cycle"))?;
let corner_point = intersect_line_plane(&line, &planes[&third])
.filter(|point| point.sub(centre).length() <= reach)
.ok_or_else(|| {
format!(
"{op}: the sharp edge recovered for blend face {} never reaches the \
corner's third wall",
strip.face_id
)
})?;
plans.push(StripPlan {
far_point,
corner_point,
});
}
let mut corner_point = Vec3::default();
for plan in &plans {
corner_point = corner_point.add(plan.corner_point);
}
corner_point = corner_point.scale(1.0 / plans.len() as f64);
for plan in &plans {
let drift = plan.corner_point.sub(corner_point).length();
if drift > plane_tolerance {
return Err(format!(
"{op}: the blend strips' recovered edges miss each other by {drift:.6} — \
the selection is not one corner"
));
}
}
for (index, plan) in plans.iter().enumerate() {
if plan.far_point.sub(corner_point).length() <= tolerance {
return Err(format!(
"{op}: the sharp edge recovered for blend face {} collapses to the corner \
vertex",
group.strips[index].face_id
));
}
}
let mut next_id = max_topology_id(&solid) + 1;
let mut alloc = || {
let value = next_id;
next_id += 1;
value
};
let corner_vertex = alloc();
let mut new_vertices: Vec<(u64, Vec3)> = vec![(corner_vertex, corner_point)];
let mut vertex_points: HashMap<u64, Vec3> = HashMap::default();
vertex_points.insert(corner_vertex, corner_point);
let mut sharp_edges: Vec<EdgeRecord> = Vec::with_capacity(3);
let mut collapse: HashMap<u64, u64> = HashMap::default();
for (index, strip) in group.strips.iter().enumerate() {
let far_vertex = alloc();
new_vertices.push((far_vertex, plans[index].far_point));
vertex_points.insert(far_vertex, plans[index].far_point);
let sharp_edge_id = alloc();
sharp_edges.push(EdgeRecord {
id: sharp_edge_id,
curve: make_line(plans[index].far_point, corner_point)?,
t0: 0.0,
t1: 1.0,
start_vertex_id: far_vertex,
end_vertex_id: corner_vertex,
degenerate: false,
name: None,
});
for (boundary_index, target) in [
(strip.cap_index, far_vertex),
(strip.corner_index, corner_vertex),
] {
let edge_id = strip.boundary[boundary_index];
let edge = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.ok_or_else(|| format!("{op}: missing edge {edge_id}"))?;
collapse.insert(edge.start_vertex_id, target);
collapse.insert(edge.end_vertex_id, target);
}
}
for &vertex_id in &group_vertices {
if !collapse.contains_key(&vertex_id) {
return Err(format!(
"{op}: corner blend vertex {vertex_id} is not on a cap or corner edge \
(unexpected strip ordering)"
));
}
}
let pending = pending_edge_relocations(&solid, &group_edges, &collapse, &vertex_points);
for (index, start_target, end_target) in pending {
let mut record = solid.edges[index].clone();
let widened = relocate_open_side_edge(
&mut record,
start_target,
end_target,
plane_tolerance,
tolerance,
op,
)?;
if !widened {
match rederive_side_edge_on_planes(
&solid,
&solid.edges[index],
&planes,
start_target,
end_target,
plane_tolerance,
tolerance,
)? {
Some(rebuilt) => record = rebuilt,
None => {
record = rederive_side_edge_on_carriers(
&solid,
&solid.edges[index],
start_target,
end_target,
plane_tolerance,
tolerance,
op,
)?
}
}
}
solid.edges[index] = record;
}
let mut edges_by_id: HashMap<u64, EdgeRecord> = solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
for edge in &sharp_edges {
edges_by_id.insert(edge.id, edge.clone());
}
let resolve = |vertex_id: u64| -> u64 { collapse.get(&vertex_id).copied().unwrap_or(vertex_id) };
for (index, strip) in group.strips.iter().enumerate() {
let sharp_edge_id = sharp_edges[index].id;
let far_vertex = sharp_edges[index].start_vertex_id;
for &wall_index in &strip.walls {
let rim_edge = strip.boundary[wall_index];
let neighbour_id = strip.neighbours[wall_index];
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("{op}: missing wall {neighbour_id}"))?;
let face = &mut solid.shells[ns].faces[nf];
let (loop_index, coedge_index) = locate_coedge(face, rim_edge).ok_or_else(|| {
format!("{op}: wall {neighbour_id} does not use edge {rim_edge}")
})?;
let coedges = &face.loops[loop_index].coedges;
let count = coedges.len();
let previous = &coedges[(coedge_index + count - 1) % count];
let next = &coedges[(coedge_index + 1) % count];
let required_from = coedge_to_vertex(previous, &edges_by_id)
.map(resolve)
.ok_or_else(|| format!("{op}: could not resolve loop connectivity"))?;
let required_to = coedge_from_vertex(next, &edges_by_id)
.map(resolve)
.ok_or_else(|| format!("{op}: could not resolve loop connectivity"))?;
let forward = if required_from == far_vertex && required_to == corner_vertex {
true
} else if required_from == corner_vertex && required_to == far_vertex {
false
} else {
return Err(format!(
"{op}: the recovered sharp edge does not close wall {neighbour_id}'s loop \
(unexpected connectivity)"
));
};
let new_coedge = CoedgeRecord {
id: alloc(),
edge_id: sharp_edge_id,
forward,
pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))?,
};
face.loops[loop_index].coedges[coedge_index] = new_coedge;
}
}
for strip in &group.strips {
let cap_edge = strip.boundary[strip.cap_index];
let neighbour_id = strip.cap_id();
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("{op}: missing cap {neighbour_id}"))?;
let face = &mut solid.shells[ns].faces[nf];
let (loop_index, coedge_index) = locate_coedge(face, cap_edge)
.ok_or_else(|| format!("{op}: cap {neighbour_id} does not use edge {cap_edge}"))?;
face.loops[loop_index].coedges.remove(coedge_index);
if face.loops[loop_index].coedges.is_empty() {
return Err(format!("{op}: healing emptied a cap face loop"));
}
}
let removed_faces: HashSet<u64> = group
.strips
.iter()
.map(|strip| strip.face_id)
.chain(std::iter::once(group.corner_id))
.collect();
solid.shells[group.shell_index]
.faces
.retain(|face| !removed_faces.contains(&face.id));
solid.edges.retain(|edge| !group_edges.contains(&edge.id));
let removed_vertices: HashSet<u64> = group_vertices.iter().copied().collect();
solid
.vertices
.retain(|vertex| !removed_vertices.contains(&vertex.id));
for edge in &sharp_edges {
solid.edges.push(edge.clone());
}
for &(vertex_id, point) in &new_vertices {
solid.vertices.push(VertexRecord {
id: vertex_id,
point,
});
}
let final_edges: HashMap<u64, EdgeRecord> = solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
let mut touched: HashSet<u64> = HashSet::default();
let mut retrim_order: Vec<u64> = planes.keys().copied().collect();
retrim_order.sort_unstable();
for neighbour_id in retrim_order {
let plane = &planes[&neighbour_id];
let (ns, nf) = find_face(&solid, neighbour_id)
.ok_or_else(|| format!("{op}: missing neighbour {neighbour_id}"))?;
retrim_planar_face(
&mut solid.shells[ns].faces[nf],
plane,
&final_edges,
scale,
op,
)?;
touched.clear();
touched.extend(
solid.shells[ns].faces[nf]
.loops
.iter()
.flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id)),
);
refit_touched_pcurves(
&mut solid.shells[ns].faces[nf],
&final_edges,
&touched,
true,
tolerance,
op,
)?;
}
let issues = solid.validate();
if !issues.is_empty() {
return Err(format!(
"{op}: corner blend heal failed validation: {issues:?}"
));
}
Ok(solid)
}