use super::*;
fn cross_section_profile(
cross: &EdgeCross,
radius: f64,
chamfer: bool,
) -> Result<Vec<NurbsCurve>, String> {
let corner = cross.start;
let cos_theta = cross.into_first.dot(cross.into_second).clamp(-1.0, 1.0);
let theta = cos_theta.acos();
if theta <= 1e-6 || theta >= std::f64::consts::PI - 1e-6 {
return Err("fillet: dihedral angle too degenerate".into());
}
let tangent_offset = radius / (theta * 0.5).tan();
let first_tangency = corner.add(cross.into_first.scale(tangent_offset));
let second_tangency = corner.add(cross.into_second.scale(tangent_offset));
let mut profile = vec![make_line(corner, first_tangency)?];
if chamfer {
profile.push(make_line(first_tangency, second_tangency)?);
} else {
let bisector = cross.into_first.add(cross.into_second).normalized()?;
let center = corner.add(bisector.scale(radius / (theta * 0.5).sin()));
let x_axis = first_tangency.sub(center).scale(1.0 / radius);
let sweep = std::f64::consts::PI - theta;
let y_axis_raw = second_tangency.sub(center).scale(1.0 / radius);
let y_axis = y_axis_raw
.sub(x_axis.scale(y_axis_raw.dot(x_axis)))
.normalized()?;
profile.push(make_arc(center, x_axis, y_axis, radius, 0.0, sweep)?);
}
profile.push(make_line(second_tangency, corner)?);
Ok(profile)
}
pub(super) fn chamfer_cross_section_offsets(
cross: &EdgeCross,
d1: f64,
d2: f64,
) -> Result<Vec<NurbsCurve>, String> {
let o = cross.start;
let p1 = o.add(cross.into_first.scale(d1));
let p2 = o.add(cross.into_second.scale(d2));
Ok(vec![
make_line(o, p1)?,
make_line(p1, p2)?,
make_line(p2, o)?,
])
}
pub(super) fn chamfer_angle_second_distance(
cross: &EdgeCross,
d1: f64,
angle_rad: f64,
) -> Result<f64, String> {
let e1 = cross.into_first;
let cos_theta = cross.into_first.dot(cross.into_second).clamp(-1.0, 1.0);
let theta = cos_theta.acos();
if theta <= 1e-6 || theta >= std::f64::consts::PI - 1e-6 {
return Err("chamfer_edge_angle: dihedral angle too degenerate".into());
}
if !(angle_rad > 1e-9) || angle_rad >= theta - 1e-9 {
return Err(format!(
"chamfer_edge_angle: angle {angle_rad} must lie in (0, dihedral θ={theta})"
));
}
let e2 = cross
.into_second
.sub(e1.scale(cross.into_second.dot(e1)))
.normalized()?;
let f2x = cross.into_second.dot(e1);
let f2y = cross.into_second.dot(e2);
let (ca, sa) = (angle_rad.cos(), angle_rad.sin());
let det = ca * (-f2y) - f2x * sa;
if det.abs() < 1e-12 {
return Err("chamfer_edge_angle: chamfer ray is parallel to face 2".into());
}
let t = (ca * 0.0 - sa * d1) / det;
if !(t > 0.0) || !t.is_finite() {
return Err(format!(
"chamfer_edge_angle: constructed a non-positive setback d2={t}"
));
}
Ok(t)
}
fn tool_boolean_setup(convex: bool) -> (BooleanOperation, BooleanOptions) {
let operation = if convex {
BooleanOperation::Subtract
} else {
BooleanOperation::Union
};
let options = BooleanOptions {
merge_coplanar_faces: false,
..BooleanOptions::default()
};
(operation, options)
}
pub(super) fn apply_tool(solid: &BrepSolid, tool: &BrepSolid, convex: bool) -> Result<BrepSolid, String> {
let (operation, options) = tool_boolean_setup(convex);
Ok(boolean_operation(solid, tool, operation, &options)?)
}
pub(super) fn apply_tool_exact(
solid: &BrepSolid,
tool: &BrepSolid,
convex: bool,
) -> Result<BrepSolid, String> {
let (operation, options) = tool_boolean_setup(convex);
Ok(crate::boolean_operation_with_diagnostics(solid, tool, operation, &options)?.value)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum Lane {
GeneralFirst,
CutterFirst,
}
pub(super) fn fillet_or_chamfer(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
ends: ToolEnds,
lane: Lane,
) -> Result<BrepSolid, String> {
let mut healed = solid.clone();
let policy = crate::KernelTolerances::for_solid(&healed, 1e-7);
crate::heal::heal_operands(&mut healed, &policy)?;
let mut result = fillet_or_chamfer_inner(&healed, edge_id, radius, chamfer, name, ends, lane)?;
heal_edge_vertex_gaps(&mut result, radius)?;
Ok(result)
}
fn fillet_or_chamfer_inner(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
ends: ToolEnds,
lane: Lane,
) -> Result<BrepSolid, String> {
let closed = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.map(|edge| edge.start_vertex_id == edge.end_vertex_id)
.unwrap_or(false);
let general = if ends != ToolEnds::default() {
Err("a padded cutter was requested".to_string())
} else if lane == Lane::CutterFirst {
Err("the cutter has the first turn in this composition".to_string())
} else if std::env::var("BREP_CUTTER_FIRST").is_ok() {
Err("BREP_CUTTER_FIRST set".to_string())
} else if closed {
crate::blend::blend_closed_edge(solid, edge_id, radius, chamfer, name)
} else {
crate::blend::blend_smooth_chain_if_closed(solid, edge_id, radius, chamfer, name)
.or_else(|_| {
let names = [name.map(str::to_string)];
crate::blend::blend_star_network(solid, &[edge_id], radius, chamfer, &names, &|_| None)
})
.or_else(|_| crate::blend::blend_open_edge(solid, edge_id, radius, chamfer, name))
};
let entry = if chamfer { "chamfer" } else { "fillet" };
let general_error = match general {
Ok(result) if result.validate().is_empty() => {
match check_blend_interference(solid, &result, &[edge_id], entry) {
Ok(()) => return Ok(result),
Err(untrimmed) => untrimmed,
}
}
Ok(_) => "the general blend assembled an invalid solid".to_string(),
Err(error) => error,
};
if general_error.starts_with(crate::blend::BALL_OFF_CARRIER) {
return Err(general_error);
}
match fillet_or_chamfer_exact(solid, edge_id, radius, chamfer, name, ends, lane) {
Ok(result) => Ok(result),
Err(exact_error) => {
let general = if closed {
crate::blend::blend_closed_edge(solid, edge_id, radius, chamfer, name)
} else {
crate::blend::blend_smooth_chain(solid, edge_id, radius, chamfer, name).or_else(
|_| crate::blend::blend_open_edge(solid, edge_id, radius, chamfer, name),
)
};
general
.and_then(|result| {
check_blend_interference(solid, &result, &[edge_id], entry).map(|()| result)
})
.map_err(|error| {
format!(
"exact cutter failed: {exact_error}; general blend also failed: {error} \
(first attempt: {general_error})"
)
})
}
}
}
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub(super) struct ToolEnds {
start: f64,
end: f64,
}
fn build_exact_tool(
cross: &EdgeCross,
profile: &[NurbsCurve],
overshoot: f64,
ends: ToolEnds,
name: Option<&str>,
) -> Result<BrepSolid, String> {
let start_pad = overshoot + ends.start.max(0.0);
let end_pad = overshoot + ends.end.max(0.0);
let mut tool = match &cross.path {
EdgePath::Straight { direction, length } => {
let shifted: Vec<NurbsCurve> = if start_pad > 0.0 {
profile
.iter()
.map(|curve| {
let mut shifted = curve.clone();
for cp in shifted.control_points.iter_mut() {
let delta = direction.scale(start_pad * cp.w);
cp.x -= delta.x;
cp.y -= delta.y;
cp.z -= delta.z;
}
shifted
})
.collect()
} else {
profile.to_vec()
};
extrude_profile_brep(&shifted, *direction, *length + start_pad + end_pad)?
}
EdgePath::Circular {
center,
axis,
sweep,
} => {
let path_radius = {
let radial = cross.start.sub(*center);
radial.sub(axis.scale(radial.dot(*axis))).length()
};
let (start_angle, end_angle) = if path_radius > 1e-12 {
(start_pad / path_radius, end_pad / path_radius)
} else {
(0.0, 0.0)
};
let total = (*sweep + start_angle + end_angle).min(std::f64::consts::TAU);
let rotated: Vec<NurbsCurve> = if start_angle > 0.0 {
profile
.iter()
.map(|curve| rotate_curve_about_axis(curve, *center, *axis, -start_angle))
.collect::<Result<_, _>>()?
} else {
profile.to_vec()
};
revolve_profile_brep(&rotated, *center, *axis, total)?
}
};
let mut side_index = 0usize;
for shell in &mut tool.shells {
for face in &mut shell.faces {
if side_index == 1 {
if let Some(name) = name {
if face.name.is_none() {
face.name = Some(name.to_string());
}
}
} else {
face.name = Some(CUTTER_SCAFFOLD_NAME.to_string());
}
side_index += 1;
}
}
Ok(tool)
}
const CUTTER_SCAFFOLD_NAME: &str = "\u{1}blend-cutter-scaffold";
fn strip_cutter_scaffold(solid: &mut BrepSolid) -> usize {
let mut survivors = 0usize;
for shell in &mut solid.shells {
for face in &mut shell.faces {
if face
.name
.as_deref()
.is_some_and(|name| name.starts_with(CUTTER_SCAFFOLD_NAME))
{
face.name = None;
survivors += 1;
}
}
}
survivors
}
fn rotate_curve_about_axis(
curve: &NurbsCurve,
center: Vec3,
axis: Vec3,
angle: f64,
) -> Result<NurbsCurve, String> {
let (cosine, sine) = (angle.cos(), angle.sin());
NurbsCurve::new(
curve.degree,
curve.knots.clone(),
curve
.control_points
.iter()
.map(|cp| {
let point = Vec3::new(cp.x / cp.w, cp.y / cp.w, cp.z / cp.w);
let vector = point.sub(center);
let rotated = center
.add(vector.scale(cosine))
.add(axis.cross(vector).scale(sine))
.add(axis.scale(axis.dot(vector) * (1.0 - cosine)));
crate::Vec4::from_point(rotated, cp.w)
})
.collect(),
)
}
pub(super) fn tool_ends_to_original_extent(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
original: (Vec3, Vec3),
blocked: &[Vec3],
) -> ToolEnds {
let zero = ToolEnds::default();
let Ok(cross) = analyze_edge(solid, edge_id, radius) else {
return zero;
};
let Some(edge) = solid.edges.iter().find(|edge| edge.id == edge_id) else {
return zero;
};
if edge.start_vertex_id == edge.end_vertex_id {
return zero;
}
let (Ok(current_start), Ok(current_end)) =
(edge.curve.evaluate(edge.t0), edge.curve.evaluate(edge.t1))
else {
return zero;
};
let station = |point: Vec3| -> Option<(f64, f64)> {
match &cross.path {
EdgePath::Straight { direction, .. } => {
let offset = point.sub(current_start);
let along = offset.dot(*direction);
Some((along, offset.sub(direction.scale(along)).length()))
}
EdgePath::Circular { center, axis, .. } => {
let radial = |p: Vec3| {
let v = p.sub(*center);
v.sub(axis.scale(v.dot(*axis)))
};
let from = radial(current_start);
let to = radial(point);
let path_radius = from.length();
if path_radius <= 1e-12 {
return None;
}
let mut angle = from.cross(to).dot(*axis).atan2(from.dot(to));
if angle > std::f64::consts::PI {
angle -= std::f64::consts::TAU;
}
let off_axis =
(point.sub(*center).dot(*axis)) - (current_start.sub(*center).dot(*axis));
let off_circle = (to.length() - path_radius).hypot(off_axis);
Some((angle * path_radius, off_circle))
}
}
};
let length = match &cross.path {
EdgePath::Straight { length, .. } => *length,
EdgePath::Circular { .. } => match station(current_end) {
Some((along, _)) if along > 0.0 => along,
_ => return zero,
},
};
if !(length > 0.0) {
return zero;
}
let tolerance = 1e-6 * (1.0 + length);
let (Some((a, a_off)), Some((b, b_off))) = (station(original.0), station(original.1)) else {
return zero;
};
if a_off > tolerance || b_off > tolerance {
return zero;
}
let ((low, low_point), (high, high_point)) = if a <= b {
((a, original.0), (b, original.1))
} else {
((b, original.1), (a, original.0))
};
if low > tolerance || high < length - tolerance {
return zero;
}
let is_blocked = |point: Vec3| {
blocked
.iter()
.any(|corner| corner.sub(point).length() < 1e-6)
};
ToolEnds {
start: if is_blocked(low_point) {
0.0
} else {
(-low).max(0.0)
},
end: if is_blocked(high_point) {
0.0
} else {
(high - length).max(0.0)
},
}
}
fn fillet_or_chamfer_exact(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
ends: ToolEnds,
lane: Lane,
) -> Result<BrepSolid, String> {
let cross = analyze_edge(solid, edge_id, radius)?;
let profile = cross_section_profile(&cross, radius, chamfer)?;
let overshoots: &[f64] = match &cross.path {
EdgePath::Straight { .. } => &[0.0, radius],
EdgePath::Circular { .. } => &[0.0],
};
let prefer_through = lane == Lane::GeneralFirst && ends == ToolEnds::default();
let mut last_error = String::new();
let mut capped: Option<BrepSolid> = None;
for exact_only in [true, false] {
for (attempt, &overshoot) in overshoots.iter().enumerate() {
let tool = build_exact_tool(&cross, &profile, overshoot, ends, name)?;
let assembled = if exact_only {
apply_tool_exact(solid, &tool, cross.convex)
} else {
apply_tool(solid, &tool, cross.convex)
};
match assembled {
Ok(mut result) => {
let survivors = strip_cutter_scaffold(&mut result);
if !prefer_through {
return Ok(result);
}
if survivors == 0 && (capped.is_none() || result.validate().is_empty()) {
return Ok(result);
}
if capped.is_none() {
capped = Some(result);
}
last_error = format!(
"the cutter left {survivors} of its own faces standing on the result \
(overshoot {overshoot})"
);
}
Err(error) => {
last_error = if exact_only && attempt == 0 {
error
} else {
format!("{last_error}; overshoot retry also failed: {error}")
};
}
}
}
if let Some(result) = capped.take() {
return Ok(result);
}
}
Err(last_error)
}