use super::*;
fn cross_section_profile(
cross: &EdgeCross,
radius: f64,
chamfer: bool,
) -> Result<Vec<NurbsCurve>, KernelRefusal> {
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(KernelRefusal::input(
KernelStage::Classify,
"dihedral_degenerate",
"fillet: dihedral angle too degenerate",
));
}
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 line = |from: Vec3, to: Vec3| {
make_line(from, to).or_refuse(KernelStage::Fragment, "profile_line")
};
let mut profile = vec![line(corner, first_tangency)?];
if chamfer {
profile.push(line(first_tangency, second_tangency)?);
} else {
let bisector = cross
.into_first
.add(cross.into_second)
.normalized()
.or_refuse(KernelStage::Fragment, "profile_bisector")?;
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()
.or_refuse(KernelStage::Fragment, "profile_arc_axis")?;
profile.push(
make_arc(center, x_axis, y_axis, radius, 0.0, sweep)
.or_refuse(KernelStage::Fragment, "profile_arc")?,
);
}
profile.push(line(second_tangency, corner)?);
Ok(profile)
}
fn full_round_profile(
cross: &EdgeCross,
radius: f64,
chamfer: bool,
) -> Result<Option<Vec<NurbsCurve>>, KernelRefusal> {
let EdgePath::Circular { center, axis, .. } = &cross.path else {
return Ok(None);
};
if chamfer || !cross.convex {
return Ok(None);
}
let corner = cross.start;
let theta = cross.into_first.dot(cross.into_second).clamp(-1.0, 1.0).acos();
if theta <= 1e-6 || theta >= std::f64::consts::PI - 1e-6 {
return Ok(None);
}
let tangent_offset = radius / (theta * 0.5).tan();
let radial = |point: Vec3| {
let offset = point.sub(*center);
offset.sub(axis.scale(offset.dot(*axis))).length()
};
let rim_radius = radial(corner);
let on_axis = |point: Vec3| radial(point) <= 1e-9 * rim_radius.max(1.0);
let first_tangency = corner.add(cross.into_first.scale(tangent_offset));
let second_tangency = corner.add(cross.into_second.scale(tangent_offset));
let (pole, wall, into_wall) = if on_axis(first_tangency) {
(first_tangency, second_tangency, cross.into_second)
} else if on_axis(second_tangency) {
(second_tangency, first_tangency, cross.into_first)
} else {
return Ok(None);
};
let bisector = cross
.into_first
.add(cross.into_second)
.normalized()
.or_refuse(KernelStage::Fragment, "full_round_bisector")?;
let ball = corner.add(bisector.scale(radius / (theta * 0.5).sin()));
let lift = into_wall.scale(-radius);
let corner_up = corner.add(lift);
let pole_up = pole.add(lift);
let line = |from: Vec3, to: Vec3| {
make_line(from, to).or_refuse(KernelStage::Fragment, "full_round_line")
};
let x_axis = wall.sub(ball).scale(1.0 / radius);
let toward_pole = pole.sub(ball).scale(1.0 / radius);
let y_axis = toward_pole
.sub(x_axis.scale(toward_pole.dot(x_axis)))
.normalized()
.or_refuse(KernelStage::Fragment, "full_round_arc_axis")?;
let arc = make_arc(ball, x_axis, y_axis, radius, 0.0, std::f64::consts::PI - theta)
.or_refuse(KernelStage::Fragment, "full_round_arc")?;
Ok(Some(vec![
line(corner_up, wall)?,
arc,
line(pole, pole_up)?,
line(pole_up, corner_up)?,
]))
}
pub(super) fn chamfer_cross_section_offsets(
cross: &EdgeCross,
d1: f64,
d2: f64,
) -> Result<Vec<NurbsCurve>, KernelRefusal> {
let o = cross.start;
let p1 = o.add(cross.into_first.scale(d1));
let p2 = o.add(cross.into_second.scale(d2));
let line = |from: Vec3, to: Vec3| {
make_line(from, to).or_refuse(KernelStage::Fragment, "chamfer_profile_line")
};
Ok(vec![line(o, p1)?, line(p1, p2)?, line(p2, o)?])
}
pub(super) fn chamfer_angle_second_distance(
cross: &EdgeCross,
d1: f64,
angle_rad: f64,
) -> Result<f64, KernelRefusal> {
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(KernelRefusal::input(
KernelStage::Classify,
"angle_dihedral_degenerate",
"chamfer_edge_angle: dihedral angle too degenerate",
));
}
if !(angle_rad > 1e-9) || angle_rad >= theta - 1e-9 {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chamfer_angle",
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()
.or_refuse(KernelStage::Classify, "chamfer_basis")?;
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(KernelRefusal::internal(
KernelStage::Classify,
"chamfer_ray_parallel",
"chamfer_edge_angle: chamfer ray is parallel to face 2",
));
}
let t = (ca * 0.0 - sa * d1) / det;
if !(t > 0.0) || !t.is_finite() {
return Err(KernelRefusal::internal(
KernelStage::Classify,
"chamfer_setback",
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, KernelRefusal> {
let (operation, options) = tool_boolean_setup(convex);
boolean_operation(solid, tool, operation, &options)
}
pub(super) fn apply_tool_exact(
solid: &BrepSolid,
tool: &BrepSolid,
convex: bool,
) -> Result<BrepSolid, KernelRefusal> {
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, KernelRefusal> {
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)?;
crate::blend::fit_planar_charts_to_trims(&healed, &mut result)?;
if lane == Lane::GeneralFirst {
result = settle_cutter_scaffold(result);
}
Ok(result)
}
pub(super) fn is_terminal_blend_refusal(refusal: &KernelRefusal) -> bool {
crate::blend::is_wall_fold(refusal)
|| crate::blend::is_marched_fit_off_carriers(refusal)
|| crate::blend::is_consumed_snap(refusal)
|| crate::blend::is_planar_chart_refusal(refusal)
|| crate::blend::is_tangential_transverse_end(refusal)
}
fn fillet_or_chamfer_inner(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
ends: ToolEnds,
lane: Lane,
) -> Result<BrepSolid, KernelRefusal> {
let flush = ends.start.abs() <= 1e-9 * (1.0 + radius) && ends.end.abs() <= 1e-9 * (1.0 + radius);
if chamfer && flush && lane == Lane::GeneralFirst && chamfer_through_end_faces_on() {
let through = analyze_edge(solid, edge_id, radius).ok().and_then(|cross| {
let profile = match full_round_profile(&cross, radius, true).ok()? {
Some(profile) => profile,
None => cross_section_profile(&cross, radius, true).ok()?,
};
chamfer_through_end_faces(solid, edge_id, &cross, &profile, name)
});
if let Some(result) = through {
return Ok(result);
}
}
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 lane_note = |what: &'static str, message: &str| {
KernelRefusal::unsupported(KernelStage::Classify, what, message)
};
let clip_probe = crate::blend::BlendTimer::start("timing.cutter.clip_probe_ms");
let clipped_fill = !chamfer && ends == ToolEnds::default() && concave_clip_face(solid, edge_id, radius).is_some();
clip_probe.stop();
let general_first = crate::blend::BlendTimer::start("timing.cutter.general_first_ms");
let general = if clipped_fill {
Err(lane_note("clipped_fill", "a concave fill past its support face is clipped by the cutter"))
} else if ends != ToolEnds::default() {
Err(lane_note("padded_cutter", "a padded cutter was requested"))
} else if lane == Lane::CutterFirst {
Err(lane_note(
"cutter_first",
"the cutter has the first turn in this composition",
))
} else if std::env::var("BREP_CUTTER_FIRST").is_ok() {
Err(lane_note("cutter_first_env", "BREP_CUTTER_FIRST set"))
} 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(|error| {
if is_terminal_blend_refusal(&error) {
return Err(error);
}
let names = [name.map(str::to_string)];
crate::blend::blend_star_network(solid, &[edge_id], radius, chamfer, &names, &|_| None)
})
.or_else(|error| {
if is_terminal_blend_refusal(&error) {
return Err(error);
}
crate::blend::blend_open_edge(solid, edge_id, radius, chamfer, name)
})
};
let entry = if chamfer { "chamfer" } else { "fillet" };
general_first.stop();
let general_error = match general {
Ok(result) if result.validate().is_empty() => {
let interference = crate::blend::BlendTimer::start("timing.cutter.general_first.interference_ms");
let checked = check_blend_interference(solid, &result, &[edge_id], entry).and_then(|()| check_blend_wall_loops(&result, entry));
interference.stop();
match checked {
Ok(()) => return Ok(result),
Err(untrimmed) => untrimmed,
}
}
Ok(_) => KernelRefusal::internal(
KernelStage::Validate,
"general_invalid",
"the general blend assembled an invalid solid",
),
Err(error) => error,
};
if crate::blend::is_ball_off_carrier(&general_error) {
return Err(general_error);
}
if is_terminal_blend_refusal(&general_error) {
return Err(general_error);
}
let exact_timer = crate::blend::BlendTimer::start("timing.cutter.exact_ms");
let exact = fillet_or_chamfer_exact(solid, edge_id, radius, chamfer, name, ends, lane);
exact_timer.stop();
match exact {
Ok(result) => Ok(result),
Err(exact_error) => {
let _general_last = crate::blend::BlendTimer::start("timing.cutter.general_last_ms");
let general = if closed {
crate::blend::blend_closed_edge(solid, edge_id, radius, chamfer, name).or_else(|first| {
if crate::blend::is_pcurve_floor_refusal(&first) {
crate::blend::blend_closed_edge_knot_aware(solid, edge_id, radius, chamfer, name).map_err(|_| first)
} else {
Err(first)
}
})
} else {
let rung = crate::blend::BlendTimer::start("timing.cutter.general_last.smooth_chain_ms");
let chain = crate::blend::blend_smooth_chain(solid, edge_id, radius, chamfer, name);
let chain_ms = rung.stop();
if let Err(error) = &chain {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("last rung, smooth chain of edge {edge_id} refused after {chain_ms:.1} ms: {error}");
}
}
chain
.or_else(|error| {
if is_terminal_blend_refusal(&error) {
return Err(error);
}
let rung = crate::blend::BlendTimer::start("timing.cutter.general_last.open_edge_ms");
let open = crate::blend::blend_open_edge(solid, edge_id, radius, chamfer, name);
let open_ms = rung.stop();
if let Err(error) = &open {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("last rung, open march of edge {edge_id} refused after {open_ms:.1} ms: {error}");
}
}
open
})
.or_else(|error| {
if is_terminal_blend_refusal(&error) {
return Err(error);
}
let _rung = crate::blend::BlendTimer::start("timing.cutter.general_last.star_network_ms");
let names = [name.map(str::to_string)];
match crate::blend::blend_star_network(
solid,
&[edge_id],
radius,
chamfer,
&names,
&|_| None,
) {
Ok(result) if result.validate().is_empty() => Ok(result),
Ok(_) => Err(error),
Err(network_error) => {
if is_terminal_blend_refusal(&network_error) {
return Err(network_error);
}
Err(error)
}
}
})
};
general
.and_then(|result| {
let rung = crate::blend::BlendTimer::start("timing.cutter.general_last.interference_ms");
let checked = check_blend_interference(solid, &result, &[edge_id], entry).and_then(|()| check_blend_wall_loops(&result, entry)).map(|()| result);
rung.stop();
checked
})
.map_err(|error| {
if is_terminal_blend_refusal(&error) {
return error;
}
error.with_message(|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,
}
impl ToolEnds {
pub(super) fn key_bits(&self) -> [u64; 2] {
[self.start.to_bits(), self.end.to_bits()]
}
}
fn build_exact_tool(
cross: &EdgeCross,
profile: &[NurbsCurve],
overshoot: f64,
ends: ToolEnds,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
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)
.or_refuse(KernelStage::Fragment, "revolve_profile")?
}
};
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(scaffold_tag(name));
}
side_index += 1;
}
}
Ok(tool)
}
fn clip_fill_by_face_outline(
solid: &BrepSolid,
tool: &BrepSolid,
face_id: u64,
edge_id: u64,
height: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let unclippable = |what: &'static str| {
KernelRefusal::unsupported(KernelStage::Fragment, what, "the fill's clip outline cannot be built")
};
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.ok_or_else(|| unclippable("clip_face"))?;
let angular = crate::KernelTolerances::for_solid(solid, 1e-7).angular;
let loops = concave_clip_outline(solid, face, edge_id, angular).ok_or_else(|| unclippable("clip_outline"))?;
let first_edge = face.loops[loops[0]].coedges.first().ok_or_else(|| unclippable("clip_loop"))?.edge_id;
let first = solid.edges.iter().find(|edge| edge.id == first_edge).ok_or_else(|| unclippable("clip_edge"))?;
let (_, normal) = face_plane_normal(face, first.curve.evaluate(first.t0).or_refuse(KernelStage::Fragment, "clip_anchor")?)?;
let depth = height;
let prism_of = |loop_record: &crate::topology::LoopRecord| -> Result<BrepSolid, KernelRefusal> {
let mut profile = Vec::with_capacity(loop_record.coedges.len() + 1);
for coedge in &loop_record.coedges {
let border = solid.edges.iter().find(|edge| edge.id == coedge.edge_id).ok_or_else(|| unclippable("clip_edge"))?;
let curve = crate::step::edge_subcurve(border).or_refuse(KernelStage::Fragment, "clip_subcurve")?;
let mut curve = if coedge.forward {
curve
} else {
curve.reversed().or_refuse(KernelStage::Fragment, "clip_reverse")?
};
for cp in curve.control_points.iter_mut() {
let delta = normal.scale(-depth * cp.w);
cp.x += delta.x;
cp.y += delta.y;
cp.z += delta.z;
}
profile.push(curve);
}
if profile.len() == 1 {
let [t0, t1] = profile[0].domain().or_refuse(KernelStage::Fragment, "clip_domain")?;
let (head, tail) = profile[0].split(0.5 * (t0 + t1)).or_refuse(KernelStage::Fragment, "clip_split")?;
profile = vec![head, tail];
}
let mut prism = extrude_profile_brep(&profile, normal, depth + 2.0 * height)?;
for shell in &mut prism.shells {
for face in &mut shell.faces {
face.name = Some(scaffold_tag(name));
}
}
Ok(prism)
};
let options = BooleanOptions {
merge_coplanar_faces: false,
..BooleanOptions::default()
};
let outer = prism_of(&face.loops[loops[0]])?;
let mut clipped = crate::boolean_operation_with_diagnostics(tool, &outer, BooleanOperation::Intersect, &options)?.value;
for &hole in &loops[1..] {
let prism = prism_of(&face.loops[hole])?;
let tolerance = crate::KernelTolerances::for_solid(solid, 1e-7).model;
if !crate::recentre::solid_bounds(&clipped).intersects(crate::recentre::solid_bounds(&prism), tolerance) {
continue;
}
clipped = crate::boolean_operation_with_diagnostics(&clipped, &prism, BooleanOperation::Subtract, &options)?.value;
}
Ok(clipped)
}
pub(super) const CUTTER_SCAFFOLD_NAME: &str = "\u{1}blend-cutter-scaffold:";
pub(super) fn scaffold_tag(wall: Option<&str>) -> String {
format!("{CUTTER_SCAFFOLD_NAME}{}\u{1}", wall.unwrap_or(""))
}
fn scaffold_wall(name: Option<&str>) -> Option<&str> {
let rest = name?.strip_prefix(CUTTER_SCAFFOLD_NAME)?;
Some(rest.split('\u{1}').next().unwrap_or(""))
}
fn count_cutter_scaffold(solid: &BrepSolid) -> usize {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| scaffold_wall(face.name.as_deref()).is_some())
.count()
}
pub(super) fn settle_cutter_scaffold(solid: BrepSolid) -> BrepSolid {
let tagged: rustc_hash::FxHashSet<u64> = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| scaffold_wall(face.name.as_deref()).is_some())
.map(|face| face.id)
.collect();
if tagged.is_empty() {
return solid;
}
let tolerance = crate::KernelTolerances::for_solid(&solid, BooleanOptions::default().tolerance).model;
let corners: rustc_hash::FxHashSet<u64> = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| tagged.contains(&face.id))
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
.filter_map(|coedge| solid.edges.iter().find(|edge| edge.id == coedge.edge_id))
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect();
let mut settled = match crate::face_merge::absorb_faces_into_carriers(&solid, tolerance, &tagged) {
Ok(absorbed) => {
match crate::coalesce::merge_curve_continuation_edges_at(&absorbed, tolerance, &corners) {
Ok(joined) if joined.validate().is_empty() => joined,
_ => absorbed,
}
}
Err(_) => solid,
};
for shell in &mut settled.shells {
for face in &mut shell.faces {
if let Some(wall) = scaffold_wall(face.name.as_deref()) {
face.name = (!wall.is_empty()).then(|| format!("{wall}:CAP"));
}
}
}
settled
}
fn rotate_curve_about_axis(
curve: &NurbsCurve,
center: Vec3,
axis: Vec3,
angle: f64,
) -> Result<NurbsCurve, KernelRefusal> {
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(),
)
.or_refuse(KernelStage::Fragment, "rotate_curve")
}
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)
},
}
}
pub(super) fn chamfer_through_end_faces_on() -> bool {
switch!("BREP_CHAMFER_THROUGH_END_FACES", true)
}
fn rail_overrun(
solid: &BrepSolid,
faces: &[&FaceRecord],
rail: Vec3,
direction: Vec3,
length: f64,
scale: f64,
) -> Option<(f64, f64)> {
let face = faces.iter().find(|face| match face.surface.analytic() {
Some(crate::AnalyticSurface::Plane { origin, u_dir, v_dir, .. }) => u_dir
.cross(*v_dir)
.normalized()
.is_ok_and(|normal| rail.sub(*origin).dot(normal).abs() <= 1e-7 * scale),
_ => false,
})?;
let Some(crate::AnalyticSurface::Plane { u_dir, v_dir, .. }) = face.surface.analytic() else {
return None;
};
let across = u_dir.cross(*v_dir).cross(direction).normalized().ok()?;
let mut stations = Vec::new();
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
let edge = solid.edges.iter().find(|edge| edge.id == coedge.edge_id)?;
if edge.degenerate {
continue;
}
const SAMPLES: usize = 32;
let mut previous: Option<(f64, f64)> = None;
for index in 0..=SAMPLES {
let t = edge.t0 + (edge.t1 - edge.t0) * index as f64 / SAMPLES as f64;
let point = edge.curve.evaluate(t).ok()?.sub(rail);
let off = point.dot(across);
let here = (if off.abs() <= 1e-9 * scale { 0.0 } else { off }, point.dot(direction));
if let Some((off_a, along_a)) = previous {
let (off_b, along_b) = here;
if off_a == 0.0 && off_b == 0.0 {
stations.extend([along_a, along_b]);
} else if (off_a <= 0.0 && off_b >= 0.0) || (off_a >= 0.0 && off_b <= 0.0) {
let s = off_a / (off_a - off_b);
stations.push(along_a + (along_b - along_a) * s);
}
}
previous = Some(here);
}
}
let middle = 0.5 * length;
let low = stations.iter().copied().filter(|&s| s < middle).fold(f64::NEG_INFINITY, f64::max);
let high = stations.iter().copied().filter(|&s| s > middle).fold(f64::INFINITY, f64::min);
if !low.is_finite() || !high.is_finite() {
return None;
}
Some((-low, high - length))
}
pub(super) fn chamfer_through_end_faces(
solid: &BrepSolid,
edge_id: u64,
cross: &EdgeCross,
profile: &[NurbsCurve],
name: Option<&str>,
) -> Option<BrepSolid> {
if !chamfer_through_end_faces_on() || !cross.convex {
return None;
}
let EdgePath::Straight { direction, length } = &cross.path else {
return None;
};
let wall = profile.get(1)?;
let [w0, w1] = wall.domain().ok()?;
let rails = [wall.evaluate(w0).ok()?, wall.evaluate(w1).ok()?];
let setback = rails.iter().map(|rail| rail.sub(cross.start).length()).fold(0.0, f64::max);
let scale = 1.0 + length + setback;
let (edge, faces) = super::analyze::edge_and_faces(solid, edge_id).ok()?;
let borders = |face: &FaceRecord, other: u64| {
face.loops.iter().flat_map(|loop_record| &loop_record.coedges).any(|coedge| coedge.edge_id == other)
};
let ends_at = [edge.start_vertex_id, edge.end_vertex_id];
let split = solid.edges.iter().any(|other| {
other.id != edge_id
&& !other.degenerate
&& (ends_at.contains(&other.start_vertex_id) || ends_at.contains(&other.end_vertex_id))
&& faces.iter().all(|face| borders(face, other.id))
});
if split {
return None;
}
let tolerance = 1e-9 * scale;
let mut ends = ToolEnds::default();
let mut needed = [false, false];
for rail in rails {
let (start, end) = rail_overrun(solid, &faces, rail, *direction, *length, scale)?;
ends.start = ends.start.max(start);
ends.end = ends.end.max(end);
needed[0] |= start.abs() > tolerance;
needed[1] |= end.abs() > tolerance;
}
if needed == [false, false] {
return None;
}
for margin in [0.25, 1.0, 4.0].map(|share| share * setback) {
let padded = ToolEnds {
start: if needed[0] { ends.start + margin } else { 0.0 },
end: if needed[1] { ends.end + margin } else { 0.0 },
};
let tool = build_exact_tool(cross, profile, 0.0, padded, name).ok()?;
let Ok(result) = apply_tool_exact(solid, &tool, cross.convex) else {
continue;
};
if count_cutter_scaffold(&result) == 0 && result.validate().is_empty() {
return Some(result);
}
}
None
}
fn fillet_or_chamfer_exact(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
chamfer: bool,
name: Option<&str>,
ends: ToolEnds,
lane: Lane,
) -> Result<BrepSolid, KernelRefusal> {
let cross = analyze_edge(solid, edge_id, radius)?;
let clip = if chamfer || ends != ToolEnds::default() {
None
} else {
concave_clip_face(solid, edge_id, radius)
};
let profile = match full_round_profile(&cross, radius, chamfer)? {
Some(profile) => profile,
None => 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 = KernelRefusal::internal(KernelStage::Fragment, "cutter", 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 tool = match clip {
Some(face) => clip_fill_by_face_outline(solid, &tool, face, edge_id, 2.0 * radius, name)?,
None => tool,
};
let assembled = if exact_only {
apply_tool_exact(solid, &tool, cross.convex)
} else {
apply_tool(solid, &tool, cross.convex)
};
match assembled {
Ok(result) => {
let survivors = count_cutter_scaffold(&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 = KernelRefusal::internal(
KernelStage::Validate,
"cutter_scaffold",
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 {
error.with_message(|error| {
format!("{last_error}; overshoot retry also failed: {error}")
})
};
}
}
}
if let Some(result) = capped.take() {
return Ok(result);
}
}
Err(last_error)
}