use std::collections::HashSet;
use brepkit_topology::Topology;
use brepkit_topology::edge::EdgeId;
use brepkit_topology::face::FaceId;
use brepkit_topology::shell::Shell;
use brepkit_topology::solid::{Solid, SolidId};
use crate::analytic;
use crate::builder_utils::sample_nurbs_endpoints;
use crate::spine::Spine;
use crate::stripe::StripeResult;
use crate::trimmer::{self, TrimKeep, TrimSide};
use crate::{BlendError, BlendResult};
enum ChamferEdgeSet {
TwoDistance {
edges: Vec<EdgeId>,
d1: f64,
d2: f64,
},
DistanceAngle {
edges: Vec<EdgeId>,
distance: f64,
angle: f64,
},
}
pub struct ChamferBuilder<'a> {
topo: &'a mut Topology,
solid: SolidId,
edge_sets: Vec<ChamferEdgeSet>,
}
impl<'a> ChamferBuilder<'a> {
#[must_use]
pub fn new(topo: &'a mut Topology, solid: SolidId) -> Self {
Self {
topo,
solid,
edge_sets: Vec::new(),
}
}
pub fn add_edges_symmetric(&mut self, edges: &[EdgeId], d: f64) -> &mut Self {
self.edge_sets.push(ChamferEdgeSet::TwoDistance {
edges: edges.to_vec(),
d1: d,
d2: d,
});
self
}
pub fn add_edges_asymmetric(&mut self, edges: &[EdgeId], d1: f64, d2: f64) -> &mut Self {
self.edge_sets.push(ChamferEdgeSet::TwoDistance {
edges: edges.to_vec(),
d1,
d2,
});
self
}
pub fn add_edges_distance_angle(
&mut self,
edges: &[EdgeId],
distance: f64,
angle: f64,
) -> &mut Self {
self.edge_sets.push(ChamferEdgeSet::DistanceAngle {
edges: edges.to_vec(),
distance,
angle,
});
self
}
#[allow(clippy::too_many_lines)]
pub fn build(self) -> Result<BlendResult, BlendError> {
let all_edges: Vec<(EdgeId, f64, f64)> = self
.edge_sets
.into_iter()
.flat_map(|set| {
let (edges, d1, d2) = match set {
ChamferEdgeSet::TwoDistance { edges, d1, d2 } => (edges, d1, d2),
ChamferEdgeSet::DistanceAngle {
edges,
distance,
angle,
} => {
let d2 = distance * angle.tan();
(edges, distance, d2)
}
};
edges.into_iter().map(move |eid| (eid, d1, d2))
})
.collect();
if all_edges.is_empty() {
return Err(BlendError::Topology(
brepkit_topology::TopologyError::Empty {
entity: "chamfer edge set",
},
));
}
let topo = self.topo;
let adjacency = topo.build_adjacency(self.solid)?;
let shell_id = topo.solid(self.solid)?.outer_shell();
let original_faces: Vec<FaceId> = topo.shell(shell_id)?.faces().to_vec();
let mut touched_faces: HashSet<FaceId> = HashSet::new();
let mut succeeded: Vec<EdgeId> = Vec::new();
let mut failed: Vec<(EdgeId, BlendError)> = Vec::new();
let mut stripe_results: Vec<StripeResult> = Vec::new();
for (edge_id, d1, d2) in &all_edges {
let result = compute_chamfer_stripe(topo, &adjacency, *edge_id, *d1, *d2);
match result {
Ok(sr) => {
touched_faces.insert(sr.stripe.face1);
touched_faces.insert(sr.stripe.face2);
stripe_results.push(sr);
succeeded.push(*edge_id);
}
Err(e) => {
failed.push((*edge_id, e));
}
}
}
if stripe_results.is_empty() {
return Ok(BlendResult {
solid: self.solid,
succeeded: Vec::new(),
failed,
is_partial: false,
});
}
let mut face_replacements: std::collections::HashMap<FaceId, FaceId> =
std::collections::HashMap::new();
let mut stripe_contact_edges: Vec<(
Option<brepkit_topology::edge::EdgeId>,
Option<brepkit_topology::edge::EdgeId>,
)> = Vec::new();
for sr in &stripe_results {
let stripe = &sr.stripe;
stripe_contact_edges.push((None, None));
let contact1_pts = sample_nurbs_endpoints(&stripe.contact1);
let contact2_pts = sample_nurbs_endpoints(&stripe.contact2);
let keep_side1 =
if let (Some(sec), Ok(face)) = (stripe.sections.first(), topo.face(stripe.face1)) {
let n = face.surface().normal(0.0, 0.0);
if n.dot(sec.center - sec.p1) > 0.0 {
TrimSide::Right
} else {
TrimSide::Left
}
} else {
TrimSide::Right
};
let keep_side2 =
if let (Some(sec), Ok(face)) = (stripe.sections.first(), topo.face(stripe.face2)) {
let n = face.surface().normal(0.0, 0.0);
if n.dot(sec.center - sec.p2) > 0.0 {
TrimSide::Right
} else {
TrimSide::Left
}
} else {
TrimSide::Right
};
let current_face1 = face_replacements
.get(&stripe.face1)
.copied()
.unwrap_or(stripe.face1);
let trim1 = trimmer::trim_face(
topo,
current_face1,
&contact1_pts,
&[(0.0, 0.0), (1.0, 0.0)],
TrimKeep::Side(keep_side1),
);
match trim1 {
Ok(tr) if tr.trimmed_face != current_face1 => {
if let Some(slot) = stripe_contact_edges.last_mut() {
slot.0 = tr.contact_edge;
}
face_replacements.insert(stripe.face1, tr.trimmed_face);
}
Ok(_) => {}
Err(e) => {
log::warn!("chamfer trimming failed on face {:?}: {e}", stripe.face1);
}
}
let current_face2 = face_replacements
.get(&stripe.face2)
.copied()
.unwrap_or(stripe.face2);
let trim2 = trimmer::trim_face(
topo,
current_face2,
&contact2_pts,
&[(0.0, 0.0), (1.0, 0.0)],
TrimKeep::Side(keep_side2),
);
match trim2 {
Ok(tr) if tr.trimmed_face != current_face2 => {
if let Some(slot) = stripe_contact_edges.last_mut() {
slot.1 = tr.contact_edge;
}
face_replacements.insert(stripe.face2, tr.trimmed_face);
}
Ok(_) => {}
Err(e) => {
log::warn!("chamfer trimming failed on face {:?}: {e}", stripe.face2);
}
}
}
let mut blend_face_ids: Vec<FaceId> = Vec::new();
for (si, sr) in stripe_results.iter().enumerate() {
let (c1, c2) = stripe_contact_edges
.get(si)
.copied()
.unwrap_or((None, None));
let blend_face_id =
crate::builder_utils::create_blend_face_with_contacts(topo, &sr.stripe, c1, c2)?
.face;
blend_face_ids.push(blend_face_id);
}
let mut result_faces: Vec<FaceId> = Vec::new();
for &fid in &original_faces {
if !touched_faces.contains(&fid) {
result_faces.push(fid);
}
}
for &fid in &touched_faces {
let replacement = face_replacements.get(&fid).copied();
result_faces.push(replacement.unwrap_or(fid));
}
result_faces.extend(&blend_face_ids);
let new_shell = Shell::new(result_faces)?;
let new_shell_id = topo.add_shell(new_shell);
let new_solid = Solid::new(new_shell_id, Vec::new());
let new_solid_id = topo.add_solid(new_solid);
let is_partial = !failed.is_empty();
Ok(BlendResult {
solid: new_solid_id,
succeeded,
failed,
is_partial,
})
}
}
fn compute_chamfer_stripe(
topo: &Topology,
adjacency: &brepkit_topology::adjacency::AdjacencyIndex,
edge_id: EdgeId,
d1: f64,
d2: f64,
) -> Result<StripeResult, BlendError> {
let adj_faces = adjacency.faces_for_edge(edge_id);
if adj_faces.len() != 2 {
log::warn!(
"edge {edge_id:?} has {} adjacent faces (expected 2) — cannot chamfer non-manifold or boundary edges",
adj_faces.len()
);
return Err(BlendError::StartSolutionFailure {
edge: edge_id,
t: 0.0,
});
}
let face1 = adj_faces[0];
let face2 = adj_faces[1];
let surf1 = topo.face(face1)?.surface().clone();
let surf2 = topo.face(face2)?.surface().clone();
let spine = Spine::from_single_edge(topo, edge_id)?;
if let Some(result) =
analytic::try_analytic_chamfer(&surf1, &surf2, &spine, topo, d1, d2, face1, face2)?
{
return Ok(result);
}
log::debug!(
target: "brepkit_approx",
"chamfer: analytic path unavailable for {}+{} — v1 has no walker fallback, returning UnsupportedSurface",
surf1.type_tag(),
surf2.type_tag()
);
Err(BlendError::UnsupportedSurface {
face: face1,
surface_tag: format!(
"{}+{} (walker not yet integrated)",
surf1.type_tag(),
surf2.type_tag()
),
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use brepkit_topology::adjacency::AdjacencyIndex;
use brepkit_topology::face::FaceSurface;
use brepkit_topology::test_utils::make_unit_cube_manifold;
fn find_manifold_edge(topo: &Topology, solid: SolidId) -> EdgeId {
let adjacency = AdjacencyIndex::build(topo, solid).unwrap();
let shell_id = topo.solid(solid).unwrap().outer_shell();
let faces = topo.shell(shell_id).unwrap().faces().to_vec();
for &fid in &faces {
let face = topo.face(fid).unwrap();
let wire = topo.wire(face.outer_wire()).unwrap();
for oe in wire.edges() {
let adj = adjacency.faces_for_edge(oe.edge());
if adj.len() == 2 {
return oe.edge();
}
}
}
panic!("cube should have manifold edges");
}
#[test]
fn chamfer_builder_symmetric() {
let mut topo = Topology::new();
let solid = make_unit_cube_manifold(&mut topo);
let target_edge = find_manifold_edge(&topo, solid);
let shell_id = topo.solid(solid).unwrap().outer_shell();
let original_face_count = topo.shell(shell_id).unwrap().faces().len();
let mut builder = ChamferBuilder::new(&mut topo, solid);
builder.add_edges_symmetric(&[target_edge], 0.1);
let result = builder.build().expect("chamfer build should succeed");
let result_solid = topo.solid(result.solid).unwrap();
let result_shell = topo.shell(result_solid.outer_shell()).unwrap();
assert!(
result_shell.faces().len() > original_face_count,
"expected more faces after chamfer: got {}, original {}",
result_shell.faces().len(),
original_face_count,
);
assert!(result.succeeded.contains(&target_edge));
assert!(result.failed.is_empty());
assert!(!result.is_partial);
let mut found_chamfer_plane = false;
for &fid in result_shell.faces() {
let face = topo.face(fid).unwrap();
if matches!(face.surface(), FaceSurface::Plane { .. }) {
found_chamfer_plane = true;
}
}
assert!(
found_chamfer_plane,
"chamfer should produce a planar blend surface"
);
}
#[test]
fn chamfer_builder_distance_angle() {
let mut topo = Topology::new();
let solid = make_unit_cube_manifold(&mut topo);
let target_edge = find_manifold_edge(&topo, solid);
let shell_id = topo.solid(solid).unwrap().outer_shell();
let original_face_count = topo.shell(shell_id).unwrap().faces().len();
let distance = 0.15;
let angle = std::f64::consts::FRAC_PI_4;
let mut builder = ChamferBuilder::new(&mut topo, solid);
builder.add_edges_distance_angle(&[target_edge], distance, angle);
let result = builder.build().expect("chamfer build should succeed");
let result_solid = topo.solid(result.solid).unwrap();
let result_shell = topo.shell(result_solid.outer_shell()).unwrap();
assert!(
result_shell.faces().len() > original_face_count,
"expected more faces after distance-angle chamfer"
);
assert!(result.succeeded.contains(&target_edge));
assert!(result.failed.is_empty());
}
#[test]
fn chamfer_builder_empty_edges_error() {
let mut topo = Topology::new();
let solid = make_unit_cube_manifold(&mut topo);
let builder = ChamferBuilder::new(&mut topo, solid);
let result = builder.build();
assert!(result.is_err(), "empty edge set should produce an error");
}
}