use super::delete_faces::euler_characteristic;
use super::*;
use crate::{KernelRefusal, KernelStage, OrRefuse};
pub(super) struct NetworkStrip {
face_id: u64,
rims: [(u64, u64); 2],
junctions: [usize; 2],
}
pub(super) struct KeptStrip {
origin: Vec3,
axis: Vec3,
radius: f64,
axial: [f64; 2],
rims: [(u64, u64); 2],
}
pub(super) struct NetworkCap {
face_id: u64,
plane: Plane,
apex: u64,
held: bool,
}
pub(super) struct BlendNetwork {
shell_index: usize,
pub(super) faces: HashSet<u64>,
edges: HashSet<u64>,
vertices: Vec<u64>,
pub(super) strips: Vec<NetworkStrip>,
pub(super) junctions: Vec<Vec<u64>>,
junction_of: HashMap<u64, usize>,
rim_strip: HashMap<u64, usize>,
planes: HashMap<u64, Plane>,
pub(super) kept: HashMap<u64, KeptStrip>,
kept_at: HashMap<usize, u64>,
pub(super) caps: Vec<NetworkCap>,
standing_caps: HashMap<u64, StandingCap>,
}
pub(super) enum NetworkRead {
NotANetwork,
Refused(KernelRefusal),
Network(BlendNetwork),
}
fn union_root(parent: &mut HashMap<u64, u64>, mut node: u64) -> u64 {
while let Some(&up) = parent.get(&node) {
if up == node {
break;
}
let grand = parent.get(&up).copied().unwrap_or(up);
parent.insert(node, grand);
node = up;
}
node
}
fn index_root(parents: &mut [usize], mut index: usize) -> usize {
while parents[index] != index {
parents[index] = parents[parents[index]];
index = parents[index];
}
index
}
fn face_title(face: &FaceRecord) -> String {
match &face.name {
Some(name) => format!("face {} `{name}`", face.id),
None => format!("face {}", face.id),
}
}
fn cylinder_of(surface: &NurbsSurface, tolerance: f64) -> Option<(Vec3, Vec3, f64)> {
match surface.analytic()? {
AnalyticSurface::RuledRevolution {
frame, rho0, rho1, ..
} => ((rho0 - rho1).abs() <= tolerance && *rho0 > tolerance)
.then_some((frame.origin, frame.axis, *rho0)),
AnalyticSurface::Revolution {
frame, generatrix, ..
} => {
let radial = |point: Vec3| {
let delta = point.sub(frame.origin);
delta.sub(frame.axis.scale(delta.dot(frame.axis))).length()
};
let [t0, t1] = generatrix.domain().ok()?;
let start = generatrix.evaluate(t0).ok()?;
let end = generatrix.evaluate(t1).ok()?;
let chord = end.sub(start);
let length = chord.length();
if length <= tolerance {
return None;
}
let direction = chord.scale(1.0 / length);
for control in &generatrix.control_points {
let offset = control.point().ok()?.sub(start);
if offset.sub(direction.scale(offset.dot(direction))).length() > tolerance {
return None;
}
}
let middle = generatrix.evaluate(0.5 * (t0 + t1)).ok()?;
let rho = radial(start);
((radial(end) - rho).abs() <= tolerance
&& (radial(middle) - rho).abs() <= tolerance
&& rho > tolerance)
.then_some((frame.origin, frame.axis, rho))
}
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn read_kept_strip(
face: &FaceRecord,
planes: &HashMap<u64, Plane>,
network_edges: &HashSet<u64>,
network_vertices: &HashSet<u64>,
owners: &HashMap<u64, Vec<u64>>,
edge_records: &HashMap<u64, &EdgeRecord>,
angular: f64,
plane_tolerance: f64,
) -> Result<KeptStrip, String> {
if face.loops.len() != 1 {
return Err(format!("it has {} boundary loops", face.loops.len()));
}
let Some((origin, axis, radius)) = cylinder_of(&face.surface, plane_tolerance) else {
return Err("its carrier is not a cylinder".into());
};
let axis = axis.normalized()?;
if !face.loops[0]
.coedges
.iter()
.any(|coedge| network_edges.contains(&coedge.edge_id))
{
return Err("it meets the selection only at a vertex".into());
}
let mut rims: Vec<(u64, u64)> = Vec::new();
for coedge in &face.loops[0].coedges {
if network_edges.contains(&coedge.edge_id) {
continue;
}
let edge = edge_records
.get(&coedge.edge_id)
.ok_or_else(|| format!("its edge {} is missing", coedge.edge_id))?;
let other = owners
.get(&coedge.edge_id)
.and_then(|sharing| sharing.iter().find(|id| **id != face.id))
.copied()
.ok_or_else(|| format!("its edge {} has no second face", coedge.edge_id))?;
let at_network = network_vertices.contains(&edge.start_vertex_id)
|| network_vertices.contains(&edge.end_vertex_id);
let tangent = match planes.get(&other) {
Some(plane) => {
let [q0, q1] = coedge.pcurve.domain()?;
let uv = coedge.pcurve.evaluate(0.5 * (q0 + q1))?;
face.surface.normal(uv.x, uv.y)?.cross(plane.normal).length() <= angular
}
None => false,
};
if tangent {
let run = edge
.curve
.evaluate(edge.t1)?
.sub(edge.curve.evaluate(edge.t0)?);
if run.length() <= plane_tolerance || run.normalized()?.cross(axis).length() > angular {
return Err(format!("its rim edge {} is not a straight ruling", edge.id));
}
rims.push((edge.id, other));
} else if at_network {
return Err(format!(
"its edge {} runs into the network without being a rim on a planar wall",
edge.id
));
}
}
if rims.len() != 2 || rims[0].1 == rims[1].1 {
return Err(format!(
"it is not a strip between two planar walls ({} tangent rims)",
rims.len()
));
}
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let mut axial = [f64::INFINITY, f64::NEG_INFINITY];
for (u, v) in [(u0, v0), (u0, v1), (u1, v0), (u1, v1)] {
let along = face.surface.evaluate(u, v)?.sub(origin).dot(axis);
axial = [axial[0].min(along), axial[1].max(along)];
}
Ok(KeptStrip {
origin,
axis,
radius,
axial,
rims: [rims[0], rims[1]],
})
}
struct CapRead {
strip: u64,
plane: Plane,
apex: u64,
walls: [u64; 2],
}
struct StandingCap {
strip: u64,
apex: u64,
walls: [u64; 2],
}
fn read_cap(
solid: &BrepSolid,
face: &FaceRecord,
owners: &HashMap<u64, Vec<u64>>,
edge_records: &HashMap<u64, &EdgeRecord>,
plane_tolerance: f64,
) -> Option<CapRead> {
let strip_name = without_index(face.name.as_deref()?).strip_suffix(":CAP")?;
if face.loops.len() != 1 || face.loops[0].coedges.len() != 3 {
return None;
}
let plane = plane_of_surface(&face.surface, plane_tolerance, "blend cap").ok()?;
let mut arcs: Vec<(u64, u64)> = Vec::new();
let mut legs: Vec<(u64, u64)> = Vec::new();
for coedge in &face.loops[0].coedges {
let other = across_face(owners, coedge.edge_id, face.id)?;
let (shell, position) = find_face(solid, other)?;
let other_name = solid.shells[shell].faces[position].name.as_deref();
if other_name.is_some_and(|name| name == strip_name || without_index(name) == strip_name) {
arcs.push((coedge.edge_id, other));
} else {
legs.push((coedge.edge_id, other));
}
}
let ([(arc, strip)], [(leg_a, wall_a), (leg_b, wall_b)]) = (arcs.as_slice(), legs.as_slice())
else {
return None;
};
let (arc, leg_a, leg_b) = (edge_records.get(arc)?, edge_records.get(leg_a)?, edge_records.get(leg_b)?);
let apex = [leg_a.start_vertex_id, leg_a.end_vertex_id]
.into_iter()
.find(|vertex| [leg_b.start_vertex_id, leg_b.end_vertex_id].contains(vertex))?;
if [arc.start_vertex_id, arc.end_vertex_id].contains(&apex) {
return None;
}
for leg in [leg_a, leg_b] {
let middle = leg.curve.evaluate(0.5 * (leg.t0 + leg.t1)).ok()?;
let chord = leg
.curve
.evaluate(leg.t0)
.ok()?
.add(leg.curve.evaluate(leg.t1).ok()?)
.scale(0.5);
if middle.sub(chord).length() > plane_tolerance {
return None;
}
}
let (shell, position) = find_face(solid, *strip)?;
let strip_walls: HashSet<u64> = solid.shells[shell].faces[position]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.filter_map(|coedge| across_face(owners, coedge.edge_id, *strip))
.collect();
(wall_a != wall_b && strip_walls.contains(wall_a) && strip_walls.contains(wall_b)).then_some(
CapRead {
strip: *strip,
plane,
apex,
walls: [*wall_a, *wall_b],
},
)
}
fn without_index(name: &str) -> &str {
name.strip_suffix(']')
.and_then(|head| head.rsplit_once('['))
.filter(|(_, index)| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
.map_or(name, |(base, _)| base)
}
fn across_face(owners: &HashMap<u64, Vec<u64>>, edge_id: u64, face_id: u64) -> Option<u64> {
owners.get(&edge_id)?.iter().copied().find(|id| *id != face_id)
}
struct CapScan {
owned: Vec<(u64, CapRead)>,
standing: HashMap<u64, StandingCap>,
}
fn scan_caps(
solid: &BrepSolid,
selected: &HashSet<u64>,
owners: &HashMap<u64, Vec<u64>>,
edge_records: &HashMap<u64, &EdgeRecord>,
plane_tolerance: f64,
op: &str,
) -> Result<CapScan, KernelRefusal> {
let mut scan = CapScan {
owned: Vec::new(),
standing: HashMap::default(),
};
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
let Some(cap) = read_cap(solid, face, owners, edge_records, plane_tolerance) else {
continue;
};
if selected.contains(&cap.strip) {
scan.owned.push((face.id, cap));
} else if selected.contains(&face.id) {
return Err(KernelRefusal::unsupported(
KernelStage::Collect,
"cap_without_strip",
format!(
"{op}: {} is the end cap of blend face {}, which is not selected — a cap is the \
end of its strip's fill and goes with it; select the strip as well",
face_title(face),
cap.strip
),
));
} else {
scan.standing.insert(
face.id,
StandingCap {
strip: cap.strip,
apex: cap.apex,
walls: cap.walls,
},
);
}
}
Ok(scan)
}
pub(super) fn with_owned_caps(solid: &BrepSolid, face_ids: &[u64], op: &str) -> Result<Vec<u64>, KernelRefusal> {
let selected: HashSet<u64> = face_ids.iter().copied().collect();
let edge_records: HashMap<u64, &EdgeRecord> =
solid.edges.iter().map(|edge| (edge.id, edge)).collect();
let mut owners: HashMap<u64, Vec<u64>> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
owners.entry(coedge.edge_id).or_default().push(face.id);
}
}
let plane_tolerance = (solid_model_scale(solid) * 1e-6).max(1e-7);
let scan = scan_caps(solid, &selected, &owners, &edge_records, plane_tolerance, op)?;
let mut with_caps = face_ids.to_vec();
with_caps.extend(
scan.owned
.iter()
.map(|(face_id, _)| *face_id)
.filter(|face_id| !selected.contains(face_id)),
);
Ok(with_caps)
}
pub(super) fn read_blend_network(solid: &BrepSolid, face_ids: &[u64], op: &str) -> NetworkRead {
read_network(solid, face_ids, op).unwrap_or(NetworkRead::NotANetwork)
}
fn read_network(solid: &BrepSolid, face_ids: &[u64], op: &str) -> Option<NetworkRead> {
let selected: HashSet<u64> = face_ids.iter().copied().collect();
if selected.is_empty() {
return None;
}
let shell_index = find_face(solid, face_ids[0])?.0;
for face_id in face_ids {
if find_face(solid, *face_id)?.0 != shell_index {
return None;
}
}
let scale = solid_model_scale(solid);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let angular = crate::KernelTolerances::for_solid(solid, 1e-7).angular;
let edge_records: HashMap<u64, &EdgeRecord> =
solid.edges.iter().map(|edge| (edge.id, edge)).collect();
let mut owners: HashMap<u64, Vec<u64>> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
owners.entry(coedge.edge_id).or_default().push(face.id);
}
}
let scan = match scan_caps(solid, &selected, &owners, &edge_records, plane_tolerance, op) {
Ok(scan) => scan,
Err(reason) => return Some(NetworkRead::Refused(reason)),
};
let mut faces = selected.clone();
faces.extend(scan.owned.iter().map(|(face_id, _)| *face_id));
let standing_caps = scan.standing;
let mut ordered: Vec<u64> = faces.iter().copied().collect();
ordered.sort_unstable();
let mut network_edges: HashSet<u64> = HashSet::default();
for face_id in &ordered {
let (shell, position) = find_face(solid, *face_id)?;
for coedge in solid.shells[shell].faces[position]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
{
network_edges.insert(coedge.edge_id);
}
}
let mut vertex_set: HashSet<u64> = HashSet::default();
for edge_id in &network_edges {
let edge = edge_records.get(edge_id)?;
if edge.degenerate || owners.get(edge_id).map_or(0, Vec::len) != 2 {
return None;
}
vertex_set.insert(edge.start_vertex_id);
vertex_set.insert(edge.end_vertex_id);
}
let caps: Vec<NetworkCap> = scan
.owned
.into_iter()
.map(|(face_id, CapRead { plane, apex, .. })| NetworkCap {
face_id,
plane,
apex,
held: solid.edges.iter().any(|edge| {
!network_edges.contains(&edge.id)
&& (edge.start_vertex_id == apex || edge.end_vertex_id == apex)
}),
})
.collect();
if faces.len() < 2 && !standing_caps.values().any(|cap| vertex_set.contains(&cap.apex)) {
return None;
}
let fixed_apexes: HashSet<u64> = caps
.iter()
.filter(|cap| cap.held)
.map(|cap| cap.apex)
.chain(standing_caps.values().map(|cap| cap.apex))
.filter(|apex| vertex_set.contains(apex))
.collect();
let mut planes: HashMap<u64, Plane> = HashMap::default();
let mut curved: Vec<u64> = Vec::new();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if faces.contains(&face.id) {
continue;
}
let edges = || {
face.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.filter_map(|coedge| edge_records.get(&coedge.edge_id))
};
let touched: HashSet<u64> = edges()
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.filter(|vertex| vertex_set.contains(vertex))
.collect();
if touched.is_empty() {
continue;
}
if find_face(solid, face.id)?.0 != shell_index {
return None;
}
match plane_of_surface(&face.surface, plane_tolerance, "blend network") {
Ok(plane) => {
planes.insert(face.id, plane);
}
Err(_) => {
let at_apexes_only = touched.is_subset(&fixed_apexes)
&& !edges().any(|edge| network_edges.contains(&edge.id));
if !at_apexes_only {
curved.push(face.id);
}
}
}
}
curved.sort_unstable();
let mut kept: HashMap<u64, KeptStrip> = HashMap::default();
let mut refusals: Vec<KernelRefusal> = Vec::new();
for face_id in &curved {
let (shell, position) = find_face(solid, *face_id)?;
let face = &solid.shells[shell].faces[position];
match read_kept_strip(
face,
&planes,
&network_edges,
&vertex_set,
&owners,
&edge_records,
angular,
plane_tolerance,
) {
Ok(strip) => {
kept.insert(*face_id, strip);
}
Err(reason) => refusals.push(KernelRefusal::unsupported(
KernelStage::Classify,
"kept_face",
format!(
"{op}: {} is left beside the selected blend network but cannot stay: {reason} \
— only planar faces and straight cylindrical blend strips between two planar \
walls can be kept beside a deleted network; select it as well",
face_title(face)
),
)),
}
}
let mut end_edges: HashSet<u64> = HashSet::default();
let mut strips: Vec<NetworkStrip> = Vec::new();
let mut rim_strip: HashMap<u64, usize> = HashMap::default();
let cap_faces: HashSet<u64> = caps.iter().map(|cap| cap.face_id).collect();
for &face_id in &ordered {
let (shell, position) = find_face(solid, face_id)?;
let face = &solid.shells[shell].faces[position];
let mut rims: Vec<(u64, u64)> = Vec::new();
let mut meets_survivor = false;
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
let other = *owners[&coedge.edge_id].iter().find(|id| **id != face_id)?;
if faces.contains(&other) || kept.contains_key(&other) {
end_edges.insert(coedge.edge_id);
continue;
}
meets_survivor = true;
let [q0, q1] = coedge.pcurve.domain().ok()?;
let uv = coedge.pcurve.evaluate(0.5 * (q0 + q1)).ok()?;
let normal = face.surface.normal(uv.x, uv.y).ok()?;
let across = match planes.get(&other) {
Some(plane) => plane.normal,
None => {
let (other_shell, other_position) = find_face(solid, other)?;
let surface = &solid.shells[other_shell].faces[other_position].surface;
let point = face.surface.evaluate(uv.x, uv.y).ok()?;
let foot = crate::project_point_to_surface(surface, point).ok()?;
surface.normal(foot.u, foot.v).ok()?
}
};
if normal.cross(across).length() <= angular {
rims.push((coedge.edge_id, other));
} else {
end_edges.insert(coedge.edge_id);
}
}
match rims.len() {
0 if !meets_survivor || cap_faces.contains(&face_id) => {}
2 if rims[0].1 != rims[1].1 && rims[0].0 != rims[1].0 => {
for (edge_id, _) in &rims {
rim_strip.insert(*edge_id, strips.len());
}
strips.push(NetworkStrip {
face_id,
rims: [rims[0], rims[1]],
junctions: [0, 0],
});
}
_ => return None,
}
}
if strips.is_empty() {
return None;
}
let mut parent: HashMap<u64, u64> = HashMap::default();
let mut sorted_ends: Vec<u64> = end_edges.iter().copied().collect();
sorted_ends.sort_unstable();
for edge_id in &sorted_ends {
let edge = edge_records[edge_id];
parent.entry(edge.start_vertex_id).or_insert(edge.start_vertex_id);
parent.entry(edge.end_vertex_id).or_insert(edge.end_vertex_id);
let a = union_root(&mut parent, edge.start_vertex_id);
let b = union_root(&mut parent, edge.end_vertex_id);
if a != b {
parent.insert(a.max(b), a.min(b));
}
}
let mut vertices: Vec<u64> = vertex_set.iter().copied().collect();
vertices.sort_unstable();
let mut root_junction: HashMap<u64, usize> = HashMap::default();
let mut junctions: Vec<Vec<u64>> = Vec::new();
let mut junction_of: HashMap<u64, usize> = HashMap::default();
for &vertex_id in &vertices {
if !parent.contains_key(&vertex_id) {
return None;
}
let root = union_root(&mut parent, vertex_id);
let index = *root_junction.entry(root).or_insert_with(|| {
junctions.push(Vec::new());
junctions.len() - 1
});
junctions[index].push(vertex_id);
junction_of.insert(vertex_id, index);
}
let mut junction_root: Vec<usize> = (0..junctions.len()).collect();
let mut face_junction: Vec<usize> = Vec::with_capacity(ordered.len());
for face_id in &ordered {
let (shell, position) = find_face(solid, *face_id)?;
let mut touched = solid.shells[shell].faces[position]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.flat_map(|coedge| {
let edge = edge_records[&coedge.edge_id];
[edge.start_vertex_id, edge.end_vertex_id]
})
.map(|vertex| junction_of[&vertex]);
let first = touched.next()?;
for other in touched {
let (a, b) = (index_root(&mut junction_root, first), index_root(&mut junction_root, other));
junction_root[a.max(b)] = a.min(b);
}
face_junction.push(first);
}
let piece = index_root(&mut junction_root, face_junction[0]);
if face_junction
.iter()
.any(|junction| index_root(&mut junction_root, *junction) != piece)
{
return None;
}
for strip in &mut strips {
let ends = |edge_id: u64| -> (usize, usize) {
let edge = edge_records[&edge_id];
(junction_of[&edge.start_vertex_id], junction_of[&edge.end_vertex_id])
};
let (a0, b0) = ends(strip.rims[0].0);
let (a1, b1) = ends(strip.rims[1].0);
if a0 == b0 || !((a0 == a1 && b0 == b1) || (a0 == b1 && b0 == a1)) {
return None;
}
strip.junctions = [a0, b0];
}
let mut kept_at: HashMap<usize, u64> = HashMap::default();
let mut kept_ids: Vec<u64> = kept.keys().copied().collect();
kept_ids.sort_unstable();
for kept_id in kept_ids {
let (shell, position) = find_face(solid, kept_id)?;
let face = &solid.shells[shell].faces[position];
let mut touched: Vec<usize> = face.loops[0]
.coedges
.iter()
.filter(|coedge| network_edges.contains(&coedge.edge_id))
.flat_map(|coedge| {
let edge = edge_records[&coedge.edge_id];
[junction_of[&edge.start_vertex_id], junction_of[&edge.end_vertex_id]]
})
.collect();
touched.sort_unstable();
touched.dedup();
for junction in touched {
let whole_width = kept[&kept_id].rims.iter().all(|(rim, _)| {
let edge = edge_records[rim];
[edge.start_vertex_id, edge.end_vertex_id]
.iter()
.filter(|vertex| junction_of.get(vertex) == Some(&junction))
.count()
== 1
});
if !whole_width {
refusals.push(KernelRefusal::ill_posed(
KernelStage::Classify,
"kept_strip_end",
format!(
"{op}: {} is left beside the selected blend network but does not end in \
it across its whole width — refusing rather than guessing its end",
face_title(face)
),
));
}
if let Some(other) = kept_at.insert(junction, kept_id) {
refusals.push(KernelRefusal::unsupported(
KernelStage::Classify,
"two_kept_at_junction",
format!(
"{op}: blend faces {other} and {kept_id} are both left standing where they \
end in one junction of the selected blend network — select one of them as well"
),
));
}
}
}
if let Some(reason) = refusals.into_iter().next() {
return Some(NetworkRead::Refused(reason));
}
Some(NetworkRead::Network(BlendNetwork {
shell_index,
faces,
edges: network_edges,
vertices,
strips,
junctions,
junction_of,
rim_strip,
planes,
kept,
kept_at,
caps,
standing_caps,
}))
}
fn three_plane_point(a: &Plane, b: &Plane, c: &Plane) -> Option<(Vec3, f64)> {
let determinant = a.normal.dot(b.normal.cross(c.normal));
if determinant.abs() <= PARALLEL_EPS {
return None;
}
let da = a.normal.dot(a.origin);
let db = b.normal.dot(b.origin);
let dc = c.normal.dot(c.origin);
let point = b
.normal
.cross(c.normal)
.scale(da)
.add(c.normal.cross(a.normal).scale(db))
.add(a.normal.cross(b.normal).scale(dc))
.scale(1.0 / determinant);
Some((point, determinant.abs()))
}
enum JunctionEnd {
Point {
vertex: u64,
point: Vec3,
},
Split {
kept: u64,
walls: [u64; 2],
vertices: [u64; 2],
points: [Vec3; 2],
},
}
impl JunctionEnd {
fn end_for(&self, flanking: &[u64], what: &str, op: &str) -> Result<(u64, Vec3), KernelRefusal> {
match self {
JunctionEnd::Point { vertex, point } => Ok((*vertex, *point)),
JunctionEnd::Split {
kept,
walls,
vertices,
points,
} => {
let sides: Vec<usize> = (0..2).filter(|side| flanking.contains(&walls[*side])).collect();
match sides.as_slice() {
[side] => Ok((vertices[*side], points[*side])),
_ => Err(KernelRefusal::ill_posed(
KernelStage::Sew,
"kept_end_side",
format!(
"{op}: {what} reaches the end of kept blend face {kept} on {} of its \
walls — refusing rather than guessing which rim it ends on",
if sides.is_empty() { "neither" } else { "both" }
),
)),
}
}
}
}
}
pub(super) fn heal_blend_network(
solid: &BrepSolid,
network: &BlendNetwork,
op: &str,
) -> Result<BrepSolid, KernelRefusal> {
let mut solid = solid.clone();
let chi_before = euler_characteristic(&solid);
let scale = solid_model_scale(&solid);
let tolerance = (scale * 1e-7).max(1e-9);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let angular = crate::KernelTolerances::for_solid(&solid, 1e-7).angular;
let edge_records: HashMap<u64, EdgeRecord> =
solid.edges.iter().map(|edge| (edge.id, edge.clone())).collect();
let mut owners: HashMap<u64, Vec<u64>> = HashMap::default();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
owners.entry(coedge.edge_id).or_default().push(face.id);
}
}
let mut centre = Vec3::default();
for &vertex_id in &network.vertices {
centre = centre.add(edge_point(&solid, vertex_id)?);
}
centre = centre.scale(1.0 / network.vertices.len() as f64);
let mut reach = 0.0f64;
for &vertex_id in &network.vertices {
reach = reach.max(edge_point(&solid, vertex_id)?.sub(centre).length());
}
let reach = reach * 3.0 + tolerance;
let outside_region = |point: Vec3| point.sub(centre).length() > reach;
let mut next_id = max_topology_id(&solid) + 1;
let mut alloc = || {
let value = next_id;
next_id += 1;
value
};
let mut ends: Vec<JunctionEnd> = Vec::with_capacity(network.junctions.len());
let mut arcs: Vec<EdgeRecord> = Vec::new();
let mut spokes: Vec<(EdgeRecord, [u64; 2])> = Vec::new();
for (junction_index, junction) in network.junctions.iter().enumerate() {
let members: HashSet<u64> = junction.iter().copied().collect();
let mut incident: Vec<u64> = network
.planes
.keys()
.copied()
.filter(|face_id| {
let Some((shell, position)) = find_face(&solid, *face_id) else {
return false;
};
solid.shells[shell].faces[position]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.filter_map(|coedge| edge_records.get(&coedge.edge_id))
.any(|edge| {
members.contains(&edge.start_vertex_id)
|| members.contains(&edge.end_vertex_id)
})
})
.collect();
incident.sort_unstable();
let near = edge_point(&solid, junction[0])?;
let held: Vec<&NetworkCap> = network
.caps
.iter()
.filter(|cap| cap.held && members.contains(&cap.apex))
.collect();
let mut standing: Vec<(u64, &StandingCap)> = network
.standing_caps
.iter()
.filter(|(_, cap)| members.contains(&cap.apex))
.map(|(face_id, cap)| (*face_id, cap))
.collect();
standing.sort_unstable_by_key(|(face_id, _)| *face_id);
if let (Some(cap), Some((standing_id, _))) = (held.first(), standing.first()) {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"held_and_standing",
format!(
"{op}: the blend junction near ({:.6}, {:.6}, {:.6}) holds the apex of blend cap \
face {} and the apex of blend cap face {standing_id}, which stays — reading \
both in one junction is not supported",
near.x, near.y, near.z, cap.face_id
),
));
}
if let Some(kept_id) = network.kept_at.get(&junction_index) {
if let Some(cap) = held
.first()
.map(|cap| cap.face_id)
.or(standing.first().map(|(face_id, _)| *face_id))
{
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"kept_and_apex",
format!(
"{op}: kept blend face {kept_id} ends in the blend junction near \
({:.6}, {:.6}, {:.6}), which also holds the apex of blend cap face {} — \
splitting a junction across both is not supported",
near.x, near.y, near.z, cap
),
));
}
let kept = &network.kept[kept_id];
let walls = [kept.rims[0].1, kept.rims[1].1];
let caps: Vec<u64> = incident
.iter()
.copied()
.filter(|face_id| !walls.contains(face_id))
.collect();
if caps.len() != 1 || !walls.iter().all(|wall| incident.contains(wall)) {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"kept_cap_count",
format!(
"{op}: kept blend face {kept_id} ends in the blend junction near \
({:.6}, {:.6}, {:.6}) against {} faces besides its walls — one cap plane \
is needed to end it",
near.x,
near.y,
near.z,
caps.len()
),
));
}
let cap = &network.planes[&caps[0]];
if cap.normal.cross(kept.axis).length() > angular {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"oblique_cap",
format!(
"{op}: face {} would end kept blend face {kept_id} obliquely to its axis — \
that section is an ellipse, which this lane does not build",
caps[0]
),
));
}
let mut points = [Vec3::default(); 2];
for side in 0..2 {
let rim = &edge_records[&kept.rims[side].0];
let on_rim = rim
.curve
.evaluate(0.5 * (rim.t0 + rim.t1))
.or_refuse(KernelStage::Sew, "evaluate")?;
let wall = &network.planes[&walls[side]];
let point = intersect_line_plane(
&Line {
point: on_rim,
dir: kept.axis,
},
cap,
)
.ok_or_else(|| {
KernelRefusal::unsupported(
KernelStage::Sew,
"rim_parallel_cap",
format!("{op}: kept blend face {kept_id}'s rim runs parallel to its cap"),
)
})?;
let delta = point.sub(kept.origin);
let along = delta.dot(kept.axis);
let radial = delta.sub(kept.axis.scale(along)).length();
if point.sub(wall.origin).dot(wall.normal).abs() > plane_tolerance
|| (radial - kept.radius).abs() > plane_tolerance
{
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"kept_rim_cap_miss",
format!(
"{op}: kept blend face {kept_id}'s rim on face {} does not meet its cap \
face {} on both the wall and the cylinder",
walls[side], caps[0]
),
));
}
if along < kept.axial[0] - plane_tolerance || along > kept.axial[1] + plane_tolerance
{
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"kept_carrier_short",
format!(
"{op}: kept blend face {kept_id} does not reach its cap face {} — \
extending its carrier is not supported",
caps[0]
),
));
}
if outside_region(point) {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"kept_end_outside",
format!(
"{op}: the end of kept blend face {kept_id} lands outside the network's \
own region"
),
));
}
points[side] = point;
}
let centre_point = intersect_line_plane(
&Line {
point: kept.origin,
dir: kept.axis,
},
cap,
)
.ok_or_else(|| {
KernelRefusal::unsupported(
KernelStage::Sew,
"axis_misses_cap",
format!("{op}: kept blend face {kept_id}'s axis misses its cap"),
)
})?;
let x_axis = points[0]
.sub(centre_point)
.normalized()
.or_refuse(KernelStage::Sew, "section_axis")?;
let to_second = points[1].sub(centre_point);
let across = to_second.sub(x_axis.scale(to_second.dot(x_axis)));
if across.length() <= tolerance {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"degenerate_section",
format!("{op}: kept blend face {kept_id}'s rims meet its cap at a degenerate section"),
));
}
let y_axis = across.normalized().or_refuse(KernelStage::Sew, "section_axis")?;
let sweep = to_second.dot(y_axis).atan2(to_second.dot(x_axis));
let (kept_shell, kept_position) = find_face(&solid, *kept_id).ok_or_else(|| {
KernelRefusal::internal(KernelStage::Collect, "face", format!("{op}: missing face {kept_id}"))
})?;
let mitre = solid.shells[kept_shell].faces[kept_position]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.filter_map(|coedge| edge_records.get(&coedge.edge_id))
.find(|edge| {
network.edges.contains(&edge.id)
&& members.contains(&edge.start_vertex_id)
&& members.contains(&edge.end_vertex_id)
})
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"kept_end_edge",
format!("{op}: kept blend face {kept_id} wears no end edge at its junction"),
)
})?;
let inside = mitre
.curve
.evaluate(0.5 * (mitre.t0 + mitre.t1))
.or_refuse(KernelStage::Sew, "evaluate")?
.sub(kept.origin);
let azimuth = inside.dot(y_axis).atan2(inside.dot(x_axis));
if !(azimuth > 0.0 && azimuth < sweep) {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"long_arc",
format!(
"{op}: kept blend face {kept_id} does not lie on the short arc between its \
rims at its cap face {}",
caps[0]
),
));
}
let vertices = [alloc(), alloc()];
let curve = crate::make_arc(centre_point, x_axis, y_axis, kept.radius, 0.0, sweep)
.or_refuse(KernelStage::Sew, "make_arc")?;
let [d0, d1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
let arc_at = |t: f64| curve.evaluate(t).or_refuse(KernelStage::Sew, "evaluate");
if arc_at(d0)?.sub(points[0]).length() > plane_tolerance
|| arc_at(d1)?.sub(points[1]).length() > plane_tolerance
{
return Err(KernelRefusal::internal(
KernelStage::Sew,
"arc_ends",
format!("{op}: the section arc ending kept blend face {kept_id} misses its rims"),
));
}
arcs.push(EdgeRecord {
id: alloc(),
curve,
t0: d0,
t1: d1,
start_vertex_id: vertices[0],
end_vertex_id: vertices[1],
degenerate: false,
name: None,
});
ends.push(JunctionEnd::Split {
kept: *kept_id,
walls,
vertices,
points,
});
continue;
}
let pinning: Vec<(u64, &Plane)> = incident
.iter()
.filter(|face_id| !standing.iter().any(|(standing_id, _)| standing_id == *face_id))
.map(|face_id| (*face_id, &network.planes[face_id]))
.chain(held.iter().map(|cap| (cap.face_id, &cap.plane)))
.collect();
let mut best: Option<(Vec3, f64)> = None;
for i in 0..pinning.len() {
for j in i + 1..pinning.len() {
for k in j + 1..pinning.len() {
if let Some(found) = three_plane_point(pinning[i].1, pinning[j].1, pinning[k].1) {
if best.map_or(true, |(_, conditioning)| found.1 > conditioning) {
best = Some(found);
}
}
}
}
}
let Some((point, _)) = best else {
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"junction_unpinned",
format!(
"{op}: the {} faces meeting at the blend junction near ({:.6}, {:.6}, {:.6}) \
do not pin a vertex — refusing rather than emitting an invalid solid",
pinning.len(),
near.x,
near.y,
near.z
),
));
};
for (face_id, plane) in &pinning {
let miss = point.sub(plane.origin).dot(plane.normal).abs();
if miss > plane_tolerance {
let what = match network.standing_caps.get(face_id) {
Some(StandingCap { strip, .. }) => format!(
"blend cap face {face_id} stays with blend face {strip}, which is not \
selected, and it"
),
None => format!("face {face_id}"),
};
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"junction_disagree",
format!(
"{op}: {what} misses the vertex its blend junction near \
({:.6}, {:.6}, {:.6}) recovers by {miss:.6} — the faces there do not \
meet in one point",
near.x, near.y, near.z
),
));
}
}
if outside_region(point) {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"junction_outside",
format!(
"{op}: the blend junction near ({:.6}, {:.6}, {:.6}) recovers a vertex outside \
the network's own region",
near.x, near.y, near.z
),
));
}
let vertex = match held.as_slice() {
[] => alloc(),
[cap, rest @ ..] => {
let apex = edge_point(&solid, cap.apex)?;
if rest.iter().any(|other| other.apex != cap.apex)
|| apex.sub(point).length() > plane_tolerance
{
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"held_apex_moves",
format!(
"{op}: the blend junction near ({:.6}, {:.6}, {:.6}) recovers \
({:.6}, {:.6}, {:.6}), which is not the apex of its blend cap face {} — \
the edge that continues past the cap stays, so its vertex cannot move",
near.x, near.y, near.z, point.x, point.y, point.z, cap.face_id
),
));
}
cap.apex
}
};
for (cap_id, cap) in &standing {
let apex = edge_point(&solid, cap.apex)?;
let on_walls = cap.walls.iter().all(|wall| {
network.planes.get(wall).is_some_and(|plane| {
[apex, point]
.iter()
.all(|at| at.sub(plane.origin).dot(plane.normal).abs() <= plane_tolerance)
})
});
if !on_walls || apex.sub(point).length() <= tolerance {
return Err(KernelRefusal::ill_posed(
KernelStage::Classify,
"standing_apex_join",
format!(
"{op}: blend cap face {cap_id} stays with blend face {}, but the blend \
junction near ({:.6}, {:.6}, {:.6}) recovers ({:.6}, {:.6}, {:.6}), which is \
{} — refusing rather than guessing how the cap's apex joins it",
cap.strip,
near.x,
near.y,
near.z,
point.x,
point.y,
point.z,
if on_walls { "the apex itself" } else { "not on both of the cap's walls" }
),
));
}
spokes.push((
EdgeRecord {
id: alloc(),
curve: make_line(apex, point).or_refuse(KernelStage::Sew, "make_line")?,
t0: 0.0,
t1: 1.0,
start_vertex_id: cap.apex,
end_vertex_id: vertex,
degenerate: false,
name: None,
},
cap.walls,
));
}
ends.push(JunctionEnd::Point { vertex, point });
}
let mut sharp_edges: Vec<EdgeRecord> = Vec::with_capacity(network.strips.len());
let mut spans: HashSet<(u64, u64, u64, u64)> = HashSet::default();
for strip in &network.strips {
let [junction_a, junction_b] = strip.junctions;
let [wall_a, wall_b] = [strip.rims[0].1, strip.rims[1].1];
let what = format!("the sharp edge recovered for blend face {}", strip.face_id);
let (start_vertex, start) = ends[junction_a].end_for(&[wall_a, wall_b], &what, op)?;
let (end_vertex, end) = ends[junction_b].end_for(&[wall_a, wall_b], &what, op)?;
if start.sub(end).length() <= tolerance {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"edge_collapse",
format!(
"{op}: the sharp edge recovered for blend face {} collapses to a point",
strip.face_id
),
));
}
if intersect_planes(&network.planes[&wall_a], &network.planes[&wall_b]).is_none() {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"parallel_walls",
format!(
"{op}: the walls flanking blend face {} are parallel — they cannot \
re-intersect into a sharp edge",
strip.face_id
),
));
}
let key = (
junction_a.min(junction_b) as u64,
junction_a.max(junction_b) as u64,
wall_a.min(wall_b),
wall_a.max(wall_b),
);
if !spans.insert(key) {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"duplicate_edge",
format!(
"{op}: two blend strips recover the same sharp edge (blend face {})",
strip.face_id
),
));
}
sharp_edges.push(EdgeRecord {
id: alloc(),
curve: make_line(start, end).or_refuse(KernelStage::Sew, "make_line")?,
t0: 0.0,
t1: 1.0,
start_vertex_id: start_vertex,
end_vertex_id: end_vertex,
degenerate: false,
name: None,
});
}
let fixed_apexes: HashSet<u64> = network
.caps
.iter()
.filter(|cap| cap.held)
.map(|cap| cap.apex)
.chain(spokes.iter().map(|(spoke, _)| spoke.start_vertex_id))
.collect();
let mut pending: Vec<(usize, Option<(u64, Vec3)>, Option<(u64, Vec3)>)> = Vec::new();
for (index, edge) in solid.edges.iter().enumerate() {
if network.edges.contains(&edge.id) {
continue;
}
let at_start = network.junction_of.get(&edge.start_vertex_id);
let at_end = network.junction_of.get(&edge.end_vertex_id);
if at_start.is_none() && at_end.is_none() {
continue;
}
let flanking = owners.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
let what = format!("edge {}", edge.id);
let target = |junction: Option<&usize>, vertex: u64| -> Result<Option<(u64, Vec3)>, KernelRefusal> {
if fixed_apexes.contains(&vertex) {
return Ok(None);
}
Ok(match junction {
Some(junction) => Some(ends[*junction].end_for(flanking, &what, op)?)
.filter(|(target, _)| *target != vertex),
None => None,
})
};
let start_target = target(at_start, edge.start_vertex_id)?;
let end_target = target(at_end, edge.end_vertex_id)?;
if start_target.is_none() && end_target.is_none() {
continue;
}
pending.push((index, start_target, end_target));
}
let relocated: HashSet<u64> = pending
.iter()
.map(|(index, _, _)| solid.edges[*index].id)
.collect();
let unmoved = solid.edges.clone();
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],
&network.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;
}
moved_edges_lie_on_their_faces(&unmoved, &solid, &relocated, &network.planes, plane_tolerance, op)?;
let mut orientation: HashMap<u64, Vec<bool>> = HashMap::default();
let mut touched_faces: Vec<u64> = Vec::new();
for shell in &mut solid.shells {
for face in &mut shell.faces {
if network.faces.contains(&face.id) {
continue;
}
let uses_network = face
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| network.edges.contains(&coedge.edge_id));
if !uses_network {
continue;
}
touched_faces.push(face.id);
for loop_record in &mut face.loops {
let mut rebuilt: Vec<CoedgeRecord> = Vec::with_capacity(loop_record.coedges.len());
for coedge in &loop_record.coedges {
if !network.edges.contains(&coedge.edge_id) {
rebuilt.push(coedge.clone());
continue;
}
let Some(&strip_index) = network.rim_strip.get(&coedge.edge_id) else {
continue;
};
let rim = &edge_records[&coedge.edge_id];
let from = if coedge.forward { rim.start_vertex_id } else { rim.end_vertex_id };
let sharp = &sharp_edges[strip_index];
let forward =
network.junction_of[&from] == network.strips[strip_index].junctions[0];
orientation.entry(sharp.id).or_default().push(forward);
rebuilt.push(CoedgeRecord {
id: alloc(),
edge_id: sharp.id,
forward,
pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))
.or_refuse(KernelStage::Sew, "make_line")?,
});
}
if rebuilt.is_empty() {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"emptied_loop",
format!(
"{op}: healing the blend network emptied a loop of face {}",
face.id
),
));
}
loop_record.coedges = rebuilt;
}
}
}
if !arcs.is_empty() || !spokes.is_empty() {
let mut vertex_ends: HashMap<u64, (u64, u64)> = HashMap::default();
for edge in solid.edges.iter().chain(&sharp_edges).chain(&arcs) {
vertex_ends.insert(edge.id, (edge.start_vertex_id, edge.end_vertex_id));
}
let traversed = |coedge: &CoedgeRecord| -> Option<(u64, u64)> {
let (start, end) = *vertex_ends.get(&coedge.edge_id)?;
Some(if coedge.forward { (start, end) } else { (end, start) })
};
let joins = |edge: &EdgeRecord, from: u64, to: u64| {
(edge.start_vertex_id == from && edge.end_vertex_id == to)
|| (edge.start_vertex_id == to && edge.end_vertex_id == from)
};
for shell in &mut solid.shells {
for face in &mut shell.faces {
if !touched_faces.contains(&face.id) {
continue;
}
let face_spokes: Vec<&EdgeRecord> = spokes
.iter()
.filter(|(_, walls)| walls.contains(&face.id))
.map(|(spoke, _)| spoke)
.collect();
for loop_record in &mut face.loops {
let count = loop_record.coedges.len();
let mut closed: Vec<CoedgeRecord> = Vec::with_capacity(count + 2);
for position in 0..count {
let coedge = &loop_record.coedges[position];
closed.push(coedge.clone());
let next = &loop_record.coedges[(position + 1) % count];
let (Some((_, tail)), Some((head, _))) = (traversed(coedge), traversed(next))
else {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"unknown_edge",
format!(
"{op}: a healed loop of face {} uses an unknown edge",
face.id
),
));
};
if tail == head {
continue;
}
let mut path: Vec<(&EdgeRecord, bool)> = Vec::new();
if let Some(arc) = arcs.iter().find(|arc| joins(arc, tail, head)) {
path.push((arc, arc.start_vertex_id == tail));
} else if let Some(spoke) = face_spokes.iter().find(|spoke| joins(spoke, tail, head)) {
path.push((spoke, spoke.start_vertex_id == tail));
} else {
let first = face_spokes.iter().find(|spoke| spoke.start_vertex_id == tail);
let second = face_spokes.iter().find(|spoke| spoke.start_vertex_id == head);
if let (Some(first), Some(second)) = (first, second) {
if first.end_vertex_id == second.end_vertex_id {
path.push((first, true));
path.push((second, false));
}
}
}
if path.is_empty() {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"loop_gap",
format!(
"{op}: healing the blend network left a gap in a loop of face {}",
face.id
),
));
}
for (edge, forward) in path {
orientation.entry(edge.id).or_default().push(forward);
closed.push(CoedgeRecord {
id: alloc(),
edge_id: edge.id,
forward,
pcurve: make_line(Vec3::default(), Vec3::new(1.0, 0.0, 0.0))
.or_refuse(KernelStage::Sew, "make_line")?,
});
}
}
loop_record.coedges = closed;
}
}
}
}
for edge in sharp_edges
.iter()
.chain(&arcs)
.chain(spokes.iter().map(|(spoke, _)| spoke))
{
let uses = orientation.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
if uses.len() != 2 || uses[0] == uses[1] {
return Err(KernelRefusal::internal(
KernelStage::Sew,
"edge_uses",
format!(
"{op}: a recovered edge is not used once in each direction by its two \
faces (unexpected connectivity)"
),
));
}
}
solid.shells[network.shell_index]
.faces
.retain(|face| !network.faces.contains(&face.id));
solid.edges.retain(|edge| !network.edges.contains(&edge.id));
let removed_vertices: HashSet<u64> = network
.vertices
.iter()
.copied()
.filter(|vertex| !fixed_apexes.contains(vertex))
.collect();
solid
.vertices
.retain(|vertex| !removed_vertices.contains(&vertex.id));
solid.edges.extend(sharp_edges.iter().cloned());
solid.edges.extend(arcs.iter().cloned());
solid.edges.extend(spokes.iter().map(|(spoke, _)| spoke.clone()));
for end in &ends {
match end {
JunctionEnd::Point { vertex, .. } if fixed_apexes.contains(vertex) => {}
JunctionEnd::Point { vertex, point } => solid.vertices.push(VertexRecord {
id: *vertex,
point: *point,
}),
JunctionEnd::Split {
vertices, points, ..
} => {
for side in 0..2 {
solid.vertices.push(VertexRecord {
id: vertices[side],
point: points[side],
});
}
}
}
}
let final_edges: HashMap<u64, EdgeRecord> = solid
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
let mut retrim_order: Vec<u64> = network
.planes
.keys()
.copied()
.filter(|face_id| {
touched_faces.contains(face_id)
|| find_face(&solid, *face_id).is_some_and(|(shell, position)| {
solid.shells[shell].faces[position]
.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.any(|coedge| relocated.contains(&coedge.edge_id))
})
})
.collect();
retrim_order.sort_unstable();
let missing_face = |id: u64| {
KernelRefusal::internal(KernelStage::Collect, "face", format!("{op}: missing face {id}"))
};
for face_id in retrim_order {
let (ns, nf) = find_face(&solid, face_id).ok_or_else(|| missing_face(face_id))?;
retrim_planar_face(
&mut solid.shells[ns].faces[nf],
&network.planes[&face_id],
&final_edges,
scale,
op,
)
?;
let touched: HashSet<u64> = solid.shells[ns].faces[nf]
.loops
.iter()
.flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
.collect();
refit_touched_pcurves(
&mut solid.shells[ns].faces[nf],
&final_edges,
&touched,
true,
tolerance,
op,
)?;
}
let mut kept_order: Vec<u64> = network.kept.keys().copied().collect();
kept_order.sort_unstable();
for face_id in kept_order {
let (ns, nf) = find_face(&solid, face_id).ok_or_else(|| missing_face(face_id))?;
let touched: HashSet<u64> = solid.shells[ns].faces[nf]
.loops
.iter()
.flat_map(|loop_record| loop_record.coedges.iter().map(|coedge| coedge.edge_id))
.collect();
refit_touched_pcurves(
&mut solid.shells[ns].faces[nf],
&final_edges,
&touched,
false,
tolerance,
op,
)?;
}
let issues = solid.validate();
if !issues.is_empty() {
return Err(KernelRefusal::internal(
KernelStage::Validate,
"validation",
format!("{op}: blend network heal failed validation: {issues:?}"),
));
}
let chi_after = euler_characteristic(&solid);
if chi_after != chi_before {
return Err(KernelRefusal::internal(
KernelStage::Validate,
"euler",
format!(
"{op}: healing the blend network changed the Euler characteristic from \
{chi_before} to {chi_after} — refusing rather than adding or removing a handle"
),
));
}
Ok(solid)
}