use std::collections::{HashMap, HashSet};
use brepkit_topology::Topology;
use brepkit_topology::edge::EdgeId;
use brepkit_topology::face::{FaceId, FaceSurface};
use brepkit_topology::solid::SolidId;
use crate::OperationsError;
pub fn filter_planar_edges(
topo: &Topology,
solid_id: SolidId,
edge_ids: &[EdgeId],
) -> Result<Vec<EdgeId>, OperationsError> {
let solid_data = topo.solid(solid_id)?;
let shell = topo.shell(solid_data.outer_shell())?;
let mut edge_faces: HashMap<usize, Vec<FaceId>> = HashMap::new();
for &fid in shell.faces() {
let face = topo.face(fid)?;
let wire = topo.wire(face.outer_wire())?;
for oe in wire.edges() {
edge_faces.entry(oe.edge().index()).or_default().push(fid);
}
}
let mut result = Vec::new();
for &eid in edge_ids {
if let Some(adj_faces) = edge_faces.get(&eid.index()) {
let all_planar = adj_faces.iter().all(|&fid| {
topo.face(fid)
.map(|f| matches!(f.surface(), FaceSurface::Plane { .. }))
.unwrap_or(false)
});
if all_planar {
result.push(eid);
}
}
}
Ok(result)
}
pub fn filter_filletable_edges(
topo: &Topology,
solid_id: SolidId,
edge_ids: &[EdgeId],
) -> Result<Vec<EdgeId>, OperationsError> {
let solid_data = topo.solid(solid_id)?;
let shell = topo.shell(solid_data.outer_shell())?;
let mut edge_faces: HashMap<usize, HashSet<FaceId>> = HashMap::new();
for &fid in shell.faces() {
let face = topo.face(fid)?;
let mut wires = vec![face.outer_wire()];
wires.extend(face.inner_wires().iter().copied());
for wid in wires {
for oe in topo.wire(wid)?.edges() {
edge_faces.entry(oe.edge().index()).or_default().insert(fid);
}
}
}
let mut result = Vec::new();
for &eid in edge_ids {
let Some(adj_faces) = edge_faces.get(&eid.index()) else {
continue;
};
if adj_faces.len() != 2 {
continue;
}
if edge_is_tangent(topo, eid, adj_faces)? {
continue;
}
result.push(eid);
}
Ok(result)
}
fn edge_is_tangent(
topo: &Topology,
eid: EdgeId,
faces: &HashSet<FaceId>,
) -> Result<bool, OperationsError> {
let mut it = faces.iter().copied();
let (Some(f1), Some(f2)) = (it.next(), it.next()) else {
return Ok(true);
};
let edge = topo.edge(eid)?;
let a = topo.vertex(edge.start())?.point();
let b = topo.vertex(edge.end())?.point();
let mid = a + (b - a) * 0.5;
let normal = |fid: FaceId| -> Option<brepkit_math::vec::Vec3> {
let face = topo.face(fid).ok()?;
let n = match face.surface() {
FaceSurface::Plane { normal, .. } => *normal,
other => {
let (u, v) = other.project_point(mid)?;
other.normal(u, v)
}
};
let n = if face.is_reversed() { -n } else { n };
n.normalize().ok()
};
match (normal(f1), normal(f2)) {
(Some(n1), Some(n2)) => {
let cos = n1.dot(n2).clamp(-1.0, 1.0);
Ok(cos.abs() > 0.9962)
}
_ => Ok(false),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, deprecated)]
use brepkit_topology::explorer::solid_edges;
use super::*;
#[test]
fn filletable_edges_all_planar_box() {
let mut topo = Topology::new();
let cube = crate::primitives::make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, cube).unwrap();
let filletable = filter_filletable_edges(&topo, cube, &edges).unwrap();
assert_eq!(
filletable.len(),
edges.len(),
"every box edge is plane↔plane and filletable"
);
assert_eq!(edges.len(), 12);
}
#[test]
fn filletable_edges_keep_nontangent_blend_edges_drop_tangent() {
let mut topo = Topology::new();
let cube = crate::primitives::make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
let edges = solid_edges(&topo, cube).unwrap();
let filleted =
crate::fillet::fillet_rolling_ball(&mut topo, cube, &[edges[0]], 1.0).unwrap();
let r_edges = solid_edges(&topo, filleted).unwrap();
let filletable: HashSet<usize> = filter_filletable_edges(&topo, filleted, &r_edges)
.unwrap()
.iter()
.map(|e| e.index())
.collect();
let sh = topo
.shell(topo.solid(filleted).unwrap().outer_shell())
.unwrap();
let blend_faces: HashSet<usize> = sh
.faces()
.iter()
.filter(|&&f| !topo.face(f).unwrap().surface().is_planar())
.map(|f| f.index())
.collect();
assert!(
!blend_faces.is_empty(),
"first fillet must create a blend face"
);
let mut ef: HashMap<usize, HashSet<FaceId>> = HashMap::new();
for &fid in sh.faces() {
for oe in topo
.wire(topo.face(fid).unwrap().outer_wire())
.unwrap()
.edges()
{
ef.entry(oe.edge().index()).or_default().insert(fid);
}
}
let (mut saw_kept, mut saw_dropped_tangent) = (false, false);
for &e in &r_edges {
let Some(fs) = ef.get(&e.index()) else {
continue;
};
if fs.len() != 2 || !fs.iter().any(|f| blend_faces.contains(&f.index())) {
continue;
}
if edge_is_tangent(&topo, e, fs).unwrap() {
assert!(
!filletable.contains(&e.index()),
"tangent blend-contact edge {} must be excluded",
e.index()
);
saw_dropped_tangent = true;
} else {
assert!(
filletable.contains(&e.index()),
"non-tangent blend-adjacent edge {} must stay filletable",
e.index()
);
saw_kept = true;
}
}
assert!(saw_kept, "expected a kept non-tangent NURBS-blend edge");
assert!(
saw_dropped_tangent,
"expected an excluded tangent contact edge"
);
}
}