1use std::collections::HashSet;
8
9use brepkit_topology::Topology;
10use brepkit_topology::edge::EdgeId;
11use brepkit_topology::face::FaceId;
12use brepkit_topology::shell::Shell;
13use brepkit_topology::solid::{Solid, SolidId};
14
15use crate::analytic;
16use crate::builder_utils::sample_nurbs_endpoints;
17use crate::spine::Spine;
18use crate::stripe::StripeResult;
19use crate::trimmer::{self, TrimKeep};
20use crate::{BlendError, BlendResult};
21
22enum ChamferEdgeSet {
24 TwoDistance {
26 edges: Vec<EdgeId>,
28 d1: f64,
30 d2: f64,
32 },
33 DistanceAngle {
35 edges: Vec<EdgeId>,
37 distance: f64,
39 angle: f64,
41 },
42}
43
44pub struct ChamferBuilder<'a> {
49 topo: &'a mut Topology,
50 solid: SolidId,
51 edge_sets: Vec<ChamferEdgeSet>,
52}
53
54impl<'a> ChamferBuilder<'a> {
55 #[must_use]
57 pub fn new(topo: &'a mut Topology, solid: SolidId) -> Self {
58 Self {
59 topo,
60 solid,
61 edge_sets: Vec::new(),
62 }
63 }
64
65 pub fn add_edges_symmetric(&mut self, edges: &[EdgeId], d: f64) -> &mut Self {
69 self.edge_sets.push(ChamferEdgeSet::TwoDistance {
70 edges: edges.to_vec(),
71 d1: d,
72 d2: d,
73 });
74 self
75 }
76
77 pub fn add_edges_asymmetric(&mut self, edges: &[EdgeId], d1: f64, d2: f64) -> &mut Self {
83 self.edge_sets.push(ChamferEdgeSet::TwoDistance {
84 edges: edges.to_vec(),
85 d1,
86 d2,
87 });
88 self
89 }
90
91 pub fn add_edges_distance_angle(
98 &mut self,
99 edges: &[EdgeId],
100 distance: f64,
101 angle: f64,
102 ) -> &mut Self {
103 self.edge_sets.push(ChamferEdgeSet::DistanceAngle {
104 edges: edges.to_vec(),
105 distance,
106 angle,
107 });
108 self
109 }
110
111 #[allow(clippy::too_many_lines)]
129 pub fn build(self) -> Result<BlendResult, BlendError> {
130 let all_edges: Vec<(EdgeId, f64, f64)> = self
131 .edge_sets
132 .into_iter()
133 .flat_map(|set| {
134 let (edges, d1, d2) = match set {
135 ChamferEdgeSet::TwoDistance { edges, d1, d2 } => (edges, d1, d2),
136 ChamferEdgeSet::DistanceAngle {
137 edges,
138 distance,
139 angle,
140 } => {
141 let d2 = distance * angle.tan();
142 (edges, distance, d2)
143 }
144 };
145 edges.into_iter().map(move |eid| (eid, d1, d2))
146 })
147 .collect();
148
149 if all_edges.is_empty() {
150 return Err(BlendError::Topology(
151 brepkit_topology::TopologyError::Empty {
152 entity: "chamfer edge set",
153 },
154 ));
155 }
156
157 let topo = self.topo;
158
159 let adjacency = topo.build_adjacency(self.solid)?;
160
161 let shell_id = topo.solid(self.solid)?.outer_shell();
162 let original_faces: Vec<FaceId> = topo.shell(shell_id)?.faces().to_vec();
163
164 let mut touched_faces: HashSet<FaceId> = HashSet::new();
165
166 let mut succeeded: Vec<EdgeId> = Vec::new();
167 let mut failed: Vec<(EdgeId, BlendError)> = Vec::new();
168 let mut stripe_results: Vec<StripeResult> = Vec::new();
169
170 for (edge_id, d1, d2) in &all_edges {
171 let result = compute_chamfer_stripe(topo, &adjacency, *edge_id, *d1, *d2);
172 match result {
173 Ok(sr) => {
174 touched_faces.insert(sr.stripe.face1);
175 touched_faces.insert(sr.stripe.face2);
176 stripe_results.push(sr);
177 succeeded.push(*edge_id);
178 }
179 Err(e) => {
180 failed.push((*edge_id, e));
181 }
182 }
183 }
184
185 if stripe_results.is_empty() {
187 return Ok(BlendResult {
188 solid: self.solid,
189 succeeded: Vec::new(),
190 failed,
191 is_partial: false,
192 });
193 }
194
195 let mut face_replacements: std::collections::HashMap<FaceId, FaceId> =
196 std::collections::HashMap::new();
197
198 let mut stripe_contact_edges: Vec<(
199 Option<brepkit_topology::edge::EdgeId>,
200 Option<brepkit_topology::edge::EdgeId>,
201 )> = Vec::new();
202 for sr in &stripe_results {
203 let stripe = &sr.stripe;
204 stripe_contact_edges.push((None, None));
205
206 let contact1_pts = sample_nurbs_endpoints(&stripe.contact1);
207 let contact2_pts = sample_nurbs_endpoints(&stripe.contact2);
208
209 let spine_pt = stripe.spine.evaluate(topo, 0.0)?;
219 let keep = TrimKeep::AwayFrom(spine_pt);
220
221 let current_face1 = face_replacements
222 .get(&stripe.face1)
223 .copied()
224 .unwrap_or(stripe.face1);
225 let trim1 = trimmer::trim_face(
226 topo,
227 current_face1,
228 &contact1_pts,
229 &[(0.0, 0.0), (1.0, 0.0)],
230 keep,
231 );
232
233 match trim1 {
234 Ok(tr) if tr.trimmed_face != current_face1 => {
235 if let Some(slot) = stripe_contact_edges.last_mut() {
236 slot.0 = tr.contact_edge;
237 }
238 face_replacements.insert(stripe.face1, tr.trimmed_face);
239 }
240 Ok(_) => {}
241 Err(e) => {
242 log::warn!("chamfer trimming failed on face {:?}: {e}", stripe.face1);
243 }
244 }
245
246 let current_face2 = face_replacements
247 .get(&stripe.face2)
248 .copied()
249 .unwrap_or(stripe.face2);
250 let trim2 = trimmer::trim_face(
251 topo,
252 current_face2,
253 &contact2_pts,
254 &[(0.0, 0.0), (1.0, 0.0)],
255 keep,
256 );
257
258 match trim2 {
259 Ok(tr) if tr.trimmed_face != current_face2 => {
260 if let Some(slot) = stripe_contact_edges.last_mut() {
261 slot.1 = tr.contact_edge;
262 }
263 face_replacements.insert(stripe.face2, tr.trimmed_face);
264 }
265 Ok(_) => {}
266 Err(e) => {
267 log::warn!("chamfer trimming failed on face {:?}: {e}", stripe.face2);
268 }
269 }
270 }
271
272 let mut blend_face_ids: Vec<FaceId> = Vec::new();
273
274 for (si, sr) in stripe_results.iter().enumerate() {
275 let (c1, c2) = stripe_contact_edges
279 .get(si)
280 .copied()
281 .unwrap_or((None, None));
282 let blend_face_id =
283 crate::builder_utils::create_blend_face_with_contacts(topo, &sr.stripe, c1, c2)?
284 .face;
285 blend_face_ids.push(blend_face_id);
286 }
287
288 let mut result_faces: Vec<FaceId> = Vec::new();
289
290 for &fid in &original_faces {
291 if !touched_faces.contains(&fid) {
292 result_faces.push(fid);
293 }
294 }
295
296 for &fid in &touched_faces {
297 let replacement = face_replacements.get(&fid).copied();
298 result_faces.push(replacement.unwrap_or(fid));
299 }
300
301 result_faces.extend(&blend_face_ids);
302
303 let new_shell = Shell::new(result_faces)?;
304 let new_shell_id = topo.add_shell(new_shell);
305 let new_solid = Solid::new(new_shell_id, Vec::new());
306 let new_solid_id = topo.add_solid(new_solid);
307
308 let is_partial = !failed.is_empty();
309 Ok(BlendResult {
310 solid: new_solid_id,
311 succeeded,
312 failed,
313 is_partial,
314 })
315 }
316}
317
318fn compute_chamfer_stripe(
325 topo: &Topology,
326 adjacency: &brepkit_topology::adjacency::AdjacencyIndex,
327 edge_id: EdgeId,
328 d1: f64,
329 d2: f64,
330) -> Result<StripeResult, BlendError> {
331 let adj_faces = adjacency.faces_for_edge(edge_id);
332 if adj_faces.len() != 2 {
333 log::warn!(
334 "edge {edge_id:?} has {} adjacent faces (expected 2) — cannot chamfer non-manifold or boundary edges",
335 adj_faces.len()
336 );
337 return Err(BlendError::StartSolutionFailure {
338 edge: edge_id,
339 t: 0.0,
340 });
341 }
342 let face1 = adj_faces[0];
343 let face2 = adj_faces[1];
344
345 let surf1 = topo.face(face1)?.surface().clone();
346 let surf2 = topo.face(face2)?.surface().clone();
347
348 let spine = Spine::from_single_edge(topo, edge_id)?;
349
350 if let Some(result) =
351 analytic::try_analytic_chamfer(&surf1, &surf2, &spine, topo, d1, d2, face1, face2)?
352 {
353 return Ok(result);
354 }
355
356 log::debug!(
357 target: "brepkit_approx",
358 "chamfer: analytic path unavailable for {}+{} — v1 has no walker fallback, returning UnsupportedSurface",
359 surf1.type_tag(),
360 surf2.type_tag()
361 );
362 Err(BlendError::UnsupportedSurface {
364 face: face1,
365 surface_tag: format!(
366 "{}+{} (walker not yet integrated)",
367 surf1.type_tag(),
368 surf2.type_tag()
369 ),
370 })
371}
372
373#[cfg(test)]
374mod tests {
375 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
376
377 use super::*;
378 use brepkit_topology::adjacency::AdjacencyIndex;
379 use brepkit_topology::face::FaceSurface;
380 use brepkit_topology::test_utils::make_unit_cube_manifold;
381
382 fn find_manifold_edge(topo: &Topology, solid: SolidId) -> EdgeId {
384 let adjacency = AdjacencyIndex::build(topo, solid).unwrap();
385 let shell_id = topo.solid(solid).unwrap().outer_shell();
386 let faces = topo.shell(shell_id).unwrap().faces().to_vec();
387
388 for &fid in &faces {
389 let face = topo.face(fid).unwrap();
390 let wire = topo.wire(face.outer_wire()).unwrap();
391 for oe in wire.edges() {
392 let adj = adjacency.faces_for_edge(oe.edge());
393 if adj.len() == 2 {
394 return oe.edge();
395 }
396 }
397 }
398 panic!("cube should have manifold edges");
399 }
400
401 #[test]
402 fn chamfer_builder_symmetric() {
403 let mut topo = Topology::new();
404 let solid = make_unit_cube_manifold(&mut topo);
405 let target_edge = find_manifold_edge(&topo, solid);
406
407 let shell_id = topo.solid(solid).unwrap().outer_shell();
408 let original_face_count = topo.shell(shell_id).unwrap().faces().len();
409
410 let mut builder = ChamferBuilder::new(&mut topo, solid);
411 builder.add_edges_symmetric(&[target_edge], 0.1);
412 let result = builder.build().expect("chamfer build should succeed");
413
414 let result_solid = topo.solid(result.solid).unwrap();
415 let result_shell = topo.shell(result_solid.outer_shell()).unwrap();
416
417 assert!(
418 result_shell.faces().len() > original_face_count,
419 "expected more faces after chamfer: got {}, original {}",
420 result_shell.faces().len(),
421 original_face_count,
422 );
423
424 assert!(result.succeeded.contains(&target_edge));
425 assert!(result.failed.is_empty());
426 assert!(!result.is_partial);
427
428 let mut found_chamfer_plane = false;
429 for &fid in result_shell.faces() {
430 let face = topo.face(fid).unwrap();
431 if matches!(face.surface(), FaceSurface::Plane { .. }) {
432 found_chamfer_plane = true;
433 }
434 }
435 assert!(
436 found_chamfer_plane,
437 "chamfer should produce a planar blend surface"
438 );
439 }
440
441 #[test]
442 fn chamfer_builder_distance_angle() {
443 let mut topo = Topology::new();
444 let solid = make_unit_cube_manifold(&mut topo);
445 let target_edge = find_manifold_edge(&topo, solid);
446
447 let shell_id = topo.solid(solid).unwrap().outer_shell();
448 let original_face_count = topo.shell(shell_id).unwrap().faces().len();
449
450 let distance = 0.15;
452 let angle = std::f64::consts::FRAC_PI_4;
453
454 let mut builder = ChamferBuilder::new(&mut topo, solid);
455 builder.add_edges_distance_angle(&[target_edge], distance, angle);
456 let result = builder.build().expect("chamfer build should succeed");
457
458 let result_solid = topo.solid(result.solid).unwrap();
459 let result_shell = topo.shell(result_solid.outer_shell()).unwrap();
460
461 assert!(
462 result_shell.faces().len() > original_face_count,
463 "expected more faces after distance-angle chamfer"
464 );
465 assert!(result.succeeded.contains(&target_edge));
466 assert!(result.failed.is_empty());
467 }
468
469 #[test]
470 fn chamfer_builder_empty_edges_error() {
471 let mut topo = Topology::new();
472 let solid = make_unit_cube_manifold(&mut topo);
473
474 let builder = ChamferBuilder::new(&mut topo, solid);
475 let result = builder.build();
476 assert!(result.is_err(), "empty edge set should produce an error");
477 }
478}