brepkit_operations/
query.rs1use std::collections::{HashMap, HashSet};
4
5use brepkit_topology::Topology;
6use brepkit_topology::edge::EdgeId;
7use brepkit_topology::face::{FaceId, FaceSurface};
8use brepkit_topology::solid::SolidId;
9
10use crate::OperationsError;
11
12pub fn filter_planar_edges(
21 topo: &Topology,
22 solid_id: SolidId,
23 edge_ids: &[EdgeId],
24) -> Result<Vec<EdgeId>, OperationsError> {
25 let solid_data = topo.solid(solid_id)?;
26 let shell = topo.shell(solid_data.outer_shell())?;
27
28 let mut edge_faces: HashMap<usize, Vec<FaceId>> = HashMap::new();
29 for &fid in shell.faces() {
30 let face = topo.face(fid)?;
31 let wire = topo.wire(face.outer_wire())?;
32 for oe in wire.edges() {
33 edge_faces.entry(oe.edge().index()).or_default().push(fid);
34 }
35 }
36
37 let mut result = Vec::new();
38 for &eid in edge_ids {
39 if let Some(adj_faces) = edge_faces.get(&eid.index()) {
40 let all_planar = adj_faces.iter().all(|&fid| {
41 topo.face(fid)
42 .map(|f| matches!(f.surface(), FaceSurface::Plane { .. }))
43 .unwrap_or(false)
44 });
45 if all_planar {
46 result.push(eid);
47 }
48 }
49 }
50 Ok(result)
51}
52
53pub fn filter_filletable_edges(
71 topo: &Topology,
72 solid_id: SolidId,
73 edge_ids: &[EdgeId],
74) -> Result<Vec<EdgeId>, OperationsError> {
75 let solid_data = topo.solid(solid_id)?;
76 let shell = topo.shell(solid_data.outer_shell())?;
77
78 let mut edge_faces: HashMap<usize, HashSet<FaceId>> = HashMap::new();
82 for &fid in shell.faces() {
83 let face = topo.face(fid)?;
84 let mut wires = vec![face.outer_wire()];
85 wires.extend(face.inner_wires().iter().copied());
86 for wid in wires {
87 for oe in topo.wire(wid)?.edges() {
88 edge_faces.entry(oe.edge().index()).or_default().insert(fid);
89 }
90 }
91 }
92
93 let mut result = Vec::new();
94 for &eid in edge_ids {
95 let Some(adj_faces) = edge_faces.get(&eid.index()) else {
96 continue;
97 };
98 if adj_faces.len() != 2 {
99 continue;
100 }
101 if edge_is_tangent(topo, eid, adj_faces)? {
102 continue;
103 }
104 result.push(eid);
105 }
106 Ok(result)
107}
108
109fn edge_is_tangent(
114 topo: &Topology,
115 eid: EdgeId,
116 faces: &HashSet<FaceId>,
117) -> Result<bool, OperationsError> {
118 let mut it = faces.iter().copied();
119 let (Some(f1), Some(f2)) = (it.next(), it.next()) else {
120 return Ok(true);
121 };
122 let edge = topo.edge(eid)?;
123 let a = topo.vertex(edge.start())?.point();
124 let b = topo.vertex(edge.end())?.point();
125 let mid = a + (b - a) * 0.5;
126
127 let normal = |fid: FaceId| -> Option<brepkit_math::vec::Vec3> {
128 let face = topo.face(fid).ok()?;
129 let n = match face.surface() {
130 FaceSurface::Plane { normal, .. } => *normal,
131 other => {
132 let (u, v) = other.project_point(mid)?;
133 other.normal(u, v)
134 }
135 };
136 let n = if face.is_reversed() { -n } else { n };
137 n.normalize().ok()
138 };
139
140 match (normal(f1), normal(f2)) {
141 (Some(n1), Some(n2)) => {
142 let cos = n1.dot(n2).clamp(-1.0, 1.0);
143 Ok(cos.abs() > 0.9962)
145 }
146 _ => Ok(false),
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 #![allow(clippy::unwrap_used, clippy::expect_used, deprecated)]
155
156 use brepkit_topology::explorer::solid_edges;
157
158 use super::*;
159
160 #[test]
161 fn filletable_edges_all_planar_box() {
162 let mut topo = Topology::new();
163 let cube = crate::primitives::make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
164 let edges = solid_edges(&topo, cube).unwrap();
165 let filletable = filter_filletable_edges(&topo, cube, &edges).unwrap();
166 assert_eq!(
167 filletable.len(),
168 edges.len(),
169 "every box edge is plane↔plane and filletable"
170 );
171 assert_eq!(edges.len(), 12);
172 }
173
174 #[test]
175 fn filletable_edges_keep_nontangent_blend_edges_drop_tangent() {
176 let mut topo = Topology::new();
180 let cube = crate::primitives::make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
181 let edges = solid_edges(&topo, cube).unwrap();
182 let filleted =
183 crate::fillet::fillet_rolling_ball(&mut topo, cube, &[edges[0]], 1.0).unwrap();
184 let r_edges = solid_edges(&topo, filleted).unwrap();
185 let filletable: HashSet<usize> = filter_filletable_edges(&topo, filleted, &r_edges)
186 .unwrap()
187 .iter()
188 .map(|e| e.index())
189 .collect();
190
191 let sh = topo
192 .shell(topo.solid(filleted).unwrap().outer_shell())
193 .unwrap();
194 let blend_faces: HashSet<usize> = sh
197 .faces()
198 .iter()
199 .filter(|&&f| !topo.face(f).unwrap().surface().is_planar())
200 .map(|f| f.index())
201 .collect();
202 assert!(
203 !blend_faces.is_empty(),
204 "first fillet must create a blend face"
205 );
206
207 let mut ef: HashMap<usize, HashSet<FaceId>> = HashMap::new();
208 for &fid in sh.faces() {
209 for oe in topo
210 .wire(topo.face(fid).unwrap().outer_wire())
211 .unwrap()
212 .edges()
213 {
214 ef.entry(oe.edge().index()).or_default().insert(fid);
215 }
216 }
217
218 let (mut saw_kept, mut saw_dropped_tangent) = (false, false);
219 for &e in &r_edges {
220 let Some(fs) = ef.get(&e.index()) else {
221 continue;
222 };
223 if fs.len() != 2 || !fs.iter().any(|f| blend_faces.contains(&f.index())) {
224 continue;
225 }
226 if edge_is_tangent(&topo, e, fs).unwrap() {
227 assert!(
228 !filletable.contains(&e.index()),
229 "tangent blend-contact edge {} must be excluded",
230 e.index()
231 );
232 saw_dropped_tangent = true;
233 } else {
234 assert!(
235 filletable.contains(&e.index()),
236 "non-tangent blend-adjacent edge {} must stay filletable",
237 e.index()
238 );
239 saw_kept = true;
240 }
241 }
242 assert!(saw_kept, "expected a kept non-tangent NURBS-blend edge");
243 assert!(
244 saw_dropped_tangent,
245 "expected an excluded tangent contact edge"
246 );
247 }
248}