use super::*;
pub fn fillet_edge(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
check_mixed_concavity(solid, edge_id, "fillet_edge")?;
check_support_extent(solid, edge_id, radius, "fillet_edge")?;
fillet_or_chamfer(solid, edge_id, radius, false, name, ToolEnds::default(), Lane::GeneralFirst)
}
pub(crate) fn fillet_edge_cutter(
solid: &BrepSolid,
edge_id: u64,
radius: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
check_mixed_concavity(solid, edge_id, "fillet_edge")?;
check_support_extent(solid, edge_id, radius, "fillet_edge")?;
fillet_or_chamfer(solid, edge_id, radius, false, name, ToolEnds::default(), Lane::CutterFirst)
.map(settle_cutter_scaffold)
}
pub fn chamfer_edge(
solid: &BrepSolid,
edge_id: u64,
distance: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
check_mixed_concavity(solid, edge_id, "chamfer_edge")?;
check_support_extent(solid, edge_id, distance, "chamfer_edge")?;
fillet_or_chamfer(solid, edge_id, distance, true, name, ToolEnds::default(), Lane::GeneralFirst)
}
fn apply_chamfer_offsets_profile(
solid: &BrepSolid,
cross: &EdgeCross,
profile: &[NurbsCurve],
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let mut tool = match &cross.path {
EdgePath::Straight { direction, length } => {
extrude_profile_brep(profile, *direction, *length)?
}
EdgePath::Circular { .. } => {
return Err(KernelRefusal::unsupported(
KernelStage::Classify,
"asymmetric_curved_edge",
"chamfer_edge_asymmetric: only straight edges on planar faces are supported \
in this slice (asymmetric chamfer on general/curved edges is out of scope)",
));
}
};
let mut side_index = 0usize;
for shell in &mut tool.shells {
for face in &mut shell.faces {
if side_index == 1 {
if face.name.is_none() {
face.name = name.map(str::to_string);
}
} else {
face.name = Some(scaffold_tag(name));
}
side_index += 1;
}
}
apply_tool(solid, &tool, cross.convex).map(settle_cutter_scaffold)
}
pub fn chamfer_edge_asymmetric(
solid: &BrepSolid,
edge_id: u64,
d1: f64,
d2: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
if !(d1 > 0.0) || !(d2 > 0.0) || !d1.is_finite() || !d2.is_finite() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"setback_distances",
"chamfer_edge_asymmetric: both setback distances must be positive",
));
}
let cross = analyze_edge(solid, edge_id, d1.min(d2))?;
let profile = chamfer_cross_section_offsets(&cross, d1, d2)?;
apply_chamfer_offsets_profile(solid, &cross, &profile, name)
}
pub fn chamfer_edge_angle(
solid: &BrepSolid,
edge_id: u64,
d1: f64,
angle_rad: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
if !(d1 > 0.0) || !d1.is_finite() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"setback_d1",
"chamfer_edge_angle: setback distance d1 must be positive",
));
}
let cross = analyze_edge(solid, edge_id, d1)?;
let d2 = chamfer_angle_second_distance(&cross, d1, angle_rad)?;
let profile = chamfer_cross_section_offsets(&cross, d1, d2)?;
apply_chamfer_offsets_profile(solid, &cross, &profile, name)
}
pub(super) fn heal_edge_vertex_gaps(
solid: &mut BrepSolid,
radius: f64,
) -> Result<(), KernelRefusal> {
let search = (radius.abs() * 1e-3).max(1e-7);
crate::boolean::commit_nearby_edge_endpoints(solid, search)
}
fn resolve_edge_by_point(solid: &BrepSolid, point: Vec3) -> Result<u64, KernelRefusal> {
match crate::topology::nearest_edge(solid, point) {
Some((edge_id, distance)) if distance <= 1e-3 => Ok(edge_id),
Some((_, distance)) => Err(KernelRefusal::input(
KernelStage::Collect,
"edge_point",
format!(
"fillet_edges: no edge within tolerance of the point (nearest {distance:.6})"
),
)),
None => Err(KernelRefusal::input(
KernelStage::Collect,
"no_edges",
"fillet_edges: solid has no edges",
)),
}
}
pub fn fillet_edges(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
let entry = if chamfer { "chamfer_edges" } else { "fillet_edges" };
let (result, selection) =
fillet_edges_reported(solid, edge_points, edge_names, radius, chamfer, name)?;
if selection.is_complete() {
return Ok(result);
}
let dropped: Vec<String> = selection
.rejected
.iter()
.map(|index| {
let point = edge_points[*index];
let named = edge_names
.and_then(|names| names.get(*index))
.map(|name| format!("`{name}` "))
.unwrap_or_default();
format!(
"{named}#{index} at ({:.6}, {:.6}, {:.6})",
point.x, point.y, point.z
)
})
.collect();
Err(KernelRefusal::ill_posed(
KernelStage::Select,
"incomplete_selection",
format!(
"{entry}: {} of the {} selected edges were blended — {} could not co-blend with the \
rest of the selection and carry no wall in the result ({}). The group refusal was: \
{}. Blend them separately, or call the reported entry point to accept the partial \
selection deliberately.",
selection.applied.len(),
selection.requested,
selection.rejected.len(),
dropped.join(", "),
selection
.reason
.as_deref()
.unwrap_or("the whole group built but not every edge grew a wall"),
),
))
}
#[derive(Clone, Debug)]
pub struct BlendSelection {
pub requested: usize,
pub applied: Vec<usize>,
pub rejected: Vec<usize>,
pub reason: Option<String>,
}
impl BlendSelection {
pub fn is_complete(&self) -> bool {
self.rejected.is_empty()
}
}
pub fn fillet_edges_reported(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<(BrepSolid, BlendSelection), KernelRefusal> {
if !(radius > 0.0) || !radius.is_finite() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"radius",
"fillet_edges: radius must be positive",
));
}
if edge_points.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"selection",
"fillet_edges: no edges selected",
));
}
let entry = if chamfer { "chamfer_edges" } else { "fillet_edges" };
let mut selected_ids = Vec::with_capacity(edge_points.len());
for point in edge_points {
let edge_id = resolve_edge_by_point(solid, *point)?;
check_mixed_concavity(solid, edge_id, entry)?;
check_support_extent(solid, edge_id, radius, entry)?;
selected_ids.push(edge_id);
}
if !chamfer {
check_mixed_corner_convexity(solid, &selected_ids, edge_points, edge_names, radius, name)?;
check_degenerate_corner_setbacks(
solid,
&selected_ids,
edge_points,
edge_names,
radius,
name,
)?;
}
let n = edge_points.len();
let complete = |applied: Vec<usize>| BlendSelection {
requested: n,
applied,
rejected: Vec::new(),
reason: None,
};
let mut memo = ComponentMemo::default();
let group_err = match fillet_edges_group_in(
solid,
edge_points,
edge_names,
radius,
chamfer,
name,
Some(&mut memo),
) {
Ok(result) => {
let missing =
walls_missing(solid, &result, edge_points, &(0..n).collect::<Vec<_>>(), radius, chamfer, entry)?;
if missing.is_empty() {
return Ok((result, complete((0..n).collect())));
}
let applied: Vec<usize> = (0..n).filter(|index| !missing.contains(index)).collect();
return Ok((
result,
BlendSelection {
requested: n,
applied,
rejected: missing,
reason: None,
},
));
}
Err(error) => error,
};
if crate::blend::is_wall_fold(&group_err) {
return Err(group_err);
}
if n < 2 || n > 12 {
return Err(group_err);
}
for drop in 1..n {
for dropped in index_combinations(n, drop) {
let keep: Vec<usize> = (0..n).filter(|i| !dropped.contains(i)).collect();
let points: Vec<Vec3> = keep.iter().map(|i| edge_points[*i]).collect();
let kept_names: Option<Vec<String>> =
edge_names.map(|names| keep.iter().map(|i| names[*i].clone()).collect());
let Ok(result) = fillet_edges_group_in(
solid,
&points,
kept_names.as_deref(),
radius,
chamfer,
name,
Some(&mut memo),
) else {
continue;
};
if !result.validate().is_empty() {
continue;
}
if !walls_missing(solid, &result, edge_points, &keep, radius, chamfer, entry)?.is_empty() {
continue;
}
return Ok((
result,
BlendSelection {
requested: n,
applied: keep,
rejected: dropped,
reason: Some(group_err.message),
},
));
}
}
Err(group_err)
}
pub(super) fn walls_missing(
original: &BrepSolid,
result: &BrepSolid,
edge_points: &[Vec3],
selected: &[usize],
size: f64,
chamfer: bool,
entry: &str,
) -> Result<Vec<usize>, KernelRefusal> {
use crate::{PointClass, SolidClassifier};
if selected.is_empty() {
return Ok(Vec::new());
}
let band = crate::KernelTolerances::for_solid(result, 1e-7).model;
let visible = band * 100.0;
let classifier = SolidClassifier::new(result, band)
.or_refuse(KernelStage::Validate, "wall_classifier")?;
const STATIONS: usize = 9;
let mut missing = Vec::new();
for &index in selected {
let point = edge_points[index];
let edge_id = resolve_edge_by_point(original, point)?;
let (edge, faces) = super::analyze::edge_and_faces(original, edge_id)?;
let mut best: Option<(Vec3, f64)> = None;
for step in 1..=STATIONS {
let t = edge.t0 + (edge.t1 - edge.t0) * step as f64 / (STATIONS + 1) as f64;
let Ok(station) = edge.curve.evaluate(t) else {
continue;
};
let clearance = match faces[..] {
[first, second] => {
let (Ok((_, a)), Ok((_, b))) = (
super::analyze::face_plane_normal(first, station),
super::analyze::face_plane_normal(second, station),
) else {
continue;
};
let half: f64 = 0.5 * a.dot(b).clamp(-1.0, 1.0).acos();
if chamfer {
size * half.sin()
} else {
size * (1.0 / half.cos().max(1e-12) - 1.0)
}
}
_ => f64::INFINITY,
};
if best.map_or(true, |(_, known)| clearance > known) {
best = Some((station, clearance));
}
}
let Some((station, clearance)) = best else {
return Err(KernelRefusal::internal(
KernelStage::Validate,
"wall_attribution_stations",
format!(
"{entry}: cannot attribute a wall to edge #{index} at ({:.6}, {:.6}, {:.6}) — no \
station of it could be evaluated",
point.x, point.y, point.z
),
));
};
if !(clearance > visible) {
return Err(KernelRefusal::ill_posed(
KernelStage::Validate,
"wall_attribution_band",
format!(
"{entry}: cannot attribute a wall to edge #{index} at ({:.6}, {:.6}, {:.6}) — \
its faces are so nearly tangent that a size-{size} blend moves the edge by at \
most {clearance:.3e}, inside the {visible:.3e} the classifier can see; the \
selection is refused rather than reported on a guess",
point.x, point.y, point.z
),
));
}
if classifier
.classify(station)
.or_refuse(KernelStage::Validate, "wall_classify")?
.class
== PointClass::On
{
missing.push(index);
}
}
Ok(missing)
}
fn per_edge_name<'a>(
edge_names: Option<&'a [String]>,
base: Option<&'a str>,
i: usize,
) -> Option<&'a str> {
match edge_names {
Some(names) => names.get(i).map(|value| value.as_str()),
None => base,
}
}
fn corner_face_name(
edge_names: Option<&[String]>,
base: Option<&str>,
adjacent: &[usize],
) -> Option<String> {
match (edge_names, base) {
(Some(names), Some(base)) => {
let prefix = format!("{base}:");
let mut raws: Vec<&str> = adjacent
.iter()
.filter_map(|&i| names.get(i))
.map(|composed| composed.strip_prefix(&prefix).unwrap_or(composed.as_str()))
.collect();
raws.sort_unstable();
raws.dedup();
Some(format!("{base}:CORNER:{}", raws.join("+")))
}
_ => base.map(|value| value.to_string()),
}
}
fn check_mixed_corner_convexity(
solid: &BrepSolid,
edge_ids: &[u64],
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
name: Option<&str>,
) -> Result<(), KernelRefusal> {
match crate::blend::mixed_convexity_corner(solid, edge_ids, radius) {
Some(corner) => {
let measured = match crate::blend::plan_runout(solid, edge_ids, radius, &corner) {
Ok(plan) => Some(plan),
Err(error) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("runout not measured: {error}");
}
None
}
};
Err(KernelRefusal::unsupported(
KernelStage::Classify,
"mixed_corner_convexity",
mixed_corner_message(
&corner,
edge_points,
edge_names,
name,
measured.as_ref(),
),
))
}
None => Ok(()),
}
}
fn check_degenerate_corner_setbacks(
solid: &BrepSolid,
edge_ids: &[u64],
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
name: Option<&str>,
) -> Result<(), KernelRefusal> {
let Some(setback) = crate::blend::degenerate_corner_setbacks(solid, edge_ids, radius) else {
return Ok(());
};
let [from, to] = setback.stops;
let how = if setback.crossed {
format!(
"run {:.3e} PAST each other along it (the radius is beyond the limit)",
setback.strip
)
} else {
format!(
"meet there ({:.3e} apart against a bar of {:.3e})",
setback.strip, setback.bar
)
};
Err(KernelRefusal::input(
KernelStage::Classify,
"degenerate_corner_setbacks",
format!(
"fillet_edges: at r = {radius} the corner setbacks consume the whole of {} — the rolling \
balls seated at its two corners touch face {} at ({:.4}, {:.4}, {:.4}) and ({:.4}, \
{:.4}, {:.4}) and {how}, so no blend strip is left between them and the faces it lies \
on are consumed whole. The radius is at or past the degenerate limit for this \
selection; reduce it.",
selection_label(setback.edge, edge_points, edge_names, name),
setback.face_id,
from.x,
from.y,
from.z,
to.x,
to.y,
to.z,
),
))
}
fn selection_label(
index: usize,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
name: Option<&str>,
) -> String {
let named = edge_names.and_then(|names| names.get(index)).map(|composed| match name {
Some(base) => composed
.strip_prefix(&format!("{base}:"))
.unwrap_or(composed)
.to_string(),
None => composed.clone(),
});
named.unwrap_or_else(|| match edge_points.get(index) {
Some(point) => format!("the edge at ({:.3}, {:.3}, {:.3})", point.x, point.y, point.z),
None => format!("selection #{}", index + 1),
})
}
fn mixed_corner_message(
corner: &crate::blend::MixedCorner,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
name: Option<&str>,
measured: Option<&crate::blend::RunoutPlan>,
) -> String {
let label = |index: usize| selection_label(index, edge_points, edge_names, name);
let list = |indices: &[usize]| -> String {
let parts: Vec<String> = indices.iter().map(|index| label(*index)).collect();
match parts.split_last() {
None => "none".to_string(),
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} and {last}", rest.join(", ")),
}
};
let mut message = format!(
"fillet_edges: the selection mixes convexity at the corner ({:.3}, {:.3}, {:.3}) — convex \
{} meets concave {} there. One rolling ball cannot touch the face they share from both \
sides at once, so that corner has no closure: the convex blend runs out against the \
concave ones partway along the edge, and this kernel does not build that runout yet. \
Blending the concave edges in one fillet and the convex ones in another does not reach \
a wall on every one of them either: the convex wall comes out at FULL length in either \
order, and its contact leaves the faces it runs along where their own loops would then \
cross. Blend the concave edges first and the blend acceptance refuses each convex edge \
whose wall runs out into them; blend the convex ones first and the subset search drops \
the concave edges it cannot compose instead, leaving those unfilleted.",
corner.point.x,
corner.point.y,
corner.point.z,
list(&corner.convex),
list(&corner.concave),
);
if let Some(plan) = measured {
message.push_str(&format!(
" Measured on this selection: the convex edge {}'s ball becomes tangent to face {} — \
the face its two concave neighbours share — at s = {:.6} from the corner, centred \
({:.6}, {:.6}, {:.6}), where its contact on face {} reaches edge {}'s own rail \
(ball-to-stripe residual {:.3e}). Past that station the second carrier is edge {}'s \
STRIPE SURFACE, not a face of the input, and the march holds on it for {} \
stations that fit rows reproducing their own contacts to {:.3e}. On the way the \
kept mate's contact walks {:.3e} of a span off that face's own patch — into the \
strip the concave stripe's pad ADDS to it — which is the shape, not an escape: \
the bar a march is refused at is a full span.",
plan.convex_edge,
plan.shared_face,
plan.station.s,
plan.station.center.x,
plan.station.center.y,
plan.station.center.z,
plan.replaced_mate,
plan.second_carrier,
plan.station.stripe_residual,
plan.second_carrier,
plan.marched,
plan.fit_residual,
plan.carrier_excursion,
));
match (&plan.stop, plan.first_carrier) {
(Some(stop), Some(first)) => message.push_str(&format!(
" It does not simply stop there: at s = {:.6} the KEPT mate (face {}) runs out too \
— the ball's contact on it reaches edge {}'s rail, residual {:.3e} — so the FIRST \
carrier becomes that stripe as well and the last stretch rolls on two stripes.",
stop.s, plan.kept_mate, first, stop.stripe_residual,
)),
_ => message.push_str(&format!(
" Where the kept mate (face {}) runs out was not measurable on this selection.",
plan.kept_mate
)),
}
if let Some(pole) = plan.pole {
message.push_str(&format!(
" That stretch ends at a POLE, ({:.6}, {:.6}, {:.6}), where the two concave \
stripes' rails on face {} meet and the ball's two contacts merge, so the blend \
section shrinks to a point. Two carrier switches and a degenerate terminus is \
more than a stop station on a stripe, and none of it is composed yet.",
pole.x, pole.y, pole.z, plan.shared_face,
));
}
}
message
}
fn index_combinations(n: usize, k: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
if k == 0 || k > n {
return out;
}
let mut idx: Vec<usize> = (0..k).collect();
loop {
out.push(idx.clone());
let mut i = k;
loop {
if i == 0 {
return out;
}
i -= 1;
if idx[i] != i + n - k {
break;
}
}
idx[i] += 1;
for j in (i + 1)..k {
idx[j] = idx[j - 1] + 1;
}
}
}
thread_local! {
pub(crate) static CUTTER_COMPOSITIONS: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
}
fn fillet_edges_group(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
fillet_edges_group_in(solid, edge_points, edge_names, radius, chamfer, name, None)
}
thread_local! {
pub(crate) static GROUP_BUILDS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
type ComponentKey = (Vec<[u64; 3]>, Option<Vec<String>>);
#[derive(Default)]
struct ComponentMemo {
built: std::collections::HashMap<Vec<ComponentKey>, Result<BrepSolid, KernelRefusal>>,
}
fn fillet_edges_group_in(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
chamfer: bool,
name: Option<&str>,
memo: Option<&mut ComponentMemo>,
) -> Result<BrepSolid, KernelRefusal> {
GROUP_BUILDS.with(|count| count.set(count.get() + 1));
let entry = if chamfer { "chamfer_edges" } else { "fillet_edges" };
let result =
fillet_edges_group_unchecked(solid, edge_points, edge_names, radius, chamfer, name, memo)?;
check_loop_self_crossings(&result, entry)?;
crate::accept_sound(result, entry)
}
fn fillet_edges_group_unchecked(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radius: f64,
chamfer: bool,
name: Option<&str>,
mut memo: Option<&mut ComponentMemo>,
) -> Result<BrepSolid, KernelRefusal> {
use rustc_hash::FxHashSet as HashSet;
let mut healed_input = solid.clone();
let heal_policy = crate::KernelTolerances::for_solid(&healed_input, 1e-7);
crate::heal::heal_operands(&mut healed_input, &heal_policy)?;
let solid = &healed_input;
let mut endpoints: Vec<(Vec3, usize)> = Vec::with_capacity(edge_points.len() * 2);
let mut original_extents: Vec<(Vec3, Vec3)> = Vec::with_capacity(edge_points.len());
let mut selected_edge_ids: Vec<u64> = Vec::with_capacity(edge_points.len());
for (i, point) in edge_points.iter().enumerate() {
let edge_id = resolve_edge_by_point(solid, *point)?;
let edge = solid
.edges
.iter()
.find(|e| e.id == edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"resolved_edge",
"fillet_edges: resolved edge vanished",
)
})?;
let (start, end) = (
edge.curve
.evaluate(edge.t0)
.or_refuse(KernelStage::Collect, "edge_start")?,
edge.curve
.evaluate(edge.t1)
.or_refuse(KernelStage::Collect, "edge_end")?,
);
endpoints.push((start, i));
endpoints.push((end, i));
original_extents.push((start, end));
selected_edge_ids.push(edge_id);
}
let mut corners: Vec<Vec3> = Vec::new();
let mut corner_edges: Vec<Vec<usize>> = Vec::new();
let mut chain_corner_count = 0usize;
let mut chain_corners: Vec<(Vec3, [usize; 2])> = Vec::new();
let mut used = vec![false; endpoints.len()];
for i in 0..endpoints.len() {
if used[i] {
continue;
}
used[i] = true;
let mut edges_here: HashSet<usize> = HashSet::default();
edges_here.insert(endpoints[i].1);
for j in (i + 1)..endpoints.len() {
if used[j] {
continue;
}
if endpoints[i].0.sub(endpoints[j].0).length() < 1e-6 {
used[j] = true;
edges_here.insert(endpoints[j].1);
}
}
if edges_here.len() >= 3 {
corners.push(endpoints[i].0);
let mut adjacent: Vec<usize> = edges_here.into_iter().collect();
adjacent.sort_unstable();
corner_edges.push(adjacent);
} else if edges_here.len() == 2 {
chain_corner_count += 1;
let mut adjacent = edges_here.into_iter().collect::<Vec<_>>();
adjacent.sort_unstable();
chain_corners.push((endpoints[i].0, [adjacent[0], adjacent[1]]));
}
}
let mut component_of = vec![usize::MAX; edge_points.len()];
let mut components: Vec<Vec<usize>> = Vec::new();
for seed in 0..edge_points.len() {
if component_of[seed] != usize::MAX {
continue;
}
let component_index = components.len();
component_of[seed] = component_index;
let mut component = vec![seed];
let mut cursor = 0;
while cursor < component.len() {
let current = component[cursor];
cursor += 1;
for candidate in 0..edge_points.len() {
if component_of[candidate] != usize::MAX {
continue;
}
let connected = [original_extents[current].0, original_extents[current].1]
.into_iter()
.any(|a| {
[original_extents[candidate].0, original_extents[candidate].1]
.into_iter()
.any(|b| a.sub(b).length() < 1e-6)
});
if connected {
component_of[candidate] = component_index;
component.push(candidate);
}
}
}
component.sort_unstable();
components.push(component);
}
if components.len() > 1 {
let mut separated = solid.clone();
let mut chain: Vec<ComponentKey> = Vec::with_capacity(components.len());
for component in components {
let points = component
.iter()
.map(|index| edge_points[*index])
.collect::<Vec<_>>();
let names = edge_names.map(|all| {
component
.iter()
.map(|index| all[*index].clone())
.collect::<Vec<_>>()
});
let Some(memo) = memo.as_deref_mut() else {
separated = fillet_edges_group(
&separated,
&points,
names.as_deref(),
radius,
chamfer,
name,
)?;
continue;
};
let bits = points.iter().map(|p| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]);
chain.push((bits.collect(), names.clone()));
let built = match memo.built.get(&chain) {
Some(built) => built.clone(),
None => {
let built = fillet_edges_group(
&separated,
&points,
names.as_deref(),
radius,
chamfer,
name,
);
memo.built.insert(chain.clone(), built.clone());
built
}
};
separated = built?;
}
return Ok(separated);
}
if std::env::var("BREP_NO_NETWORK").is_err() {
if selected_edge_ids.len() == 1 {
let edge_id = selected_edge_ids[0];
let closed = solid
.edges
.iter()
.find(|edge| edge.id == edge_id)
.is_some_and(|edge| edge.start_vertex_id == edge.end_vertex_id);
if closed {
let rim_name = per_edge_name(edge_names, name, 0);
match crate::blend::blend_closed_edge(solid, edge_id, radius, chamfer, rim_name) {
Ok(mut rim) => {
let entry = if chamfer { "chamfer_edges" } else { "fillet_edges" };
let healed = heal_edge_vertex_gaps(&mut rim, radius);
crate::blend::fit_planar_charts_to_trims(solid, &mut rim)?;
let issues = rim.validate();
let interference =
check_blend_interference(solid, &rim, &selected_edge_ids, entry);
let bowtie = check_loop_self_crossings(&rim, entry);
if healed.is_ok()
&& issues.is_empty()
&& interference.is_ok()
&& bowtie.is_ok()
{
return Ok(rim);
}
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"closed-rim result rejected: heal={healed:?} issues={issues:?} \
interference={interference:?} bowtie={bowtie:?}"
);
}
}
Err(refusal) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("closed-rim refused: {refusal}");
}
}
}
}
}
let network_names: Vec<Option<String>> = (0..edge_points.len())
.map(|index| per_edge_name(edge_names, name, index).map(str::to_string))
.collect();
let network_corner_name =
|adjacent: &[usize]| corner_face_name(edge_names, name, adjacent);
match crate::blend::blend_star_network(
solid,
&selected_edge_ids,
radius,
chamfer,
&network_names,
&network_corner_name,
) {
Ok(mut network) => {
let entry = if chamfer { "chamfer_edges" } else { "fillet_edges" };
let healed = heal_edge_vertex_gaps(&mut network, radius);
crate::blend::fit_planar_charts_to_trims(solid, &mut network)?;
let issues = network.validate();
let interference =
check_blend_interference(solid, &network, &selected_edge_ids, entry);
let bowtie = check_loop_self_crossings(&network, entry);
if healed.is_ok() && issues.is_empty() && interference.is_ok() && bowtie.is_ok() {
return Ok(network);
}
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!(
"network result rejected: heal={healed:?} issues={issues:?} \
interference={interference:?} bowtie={bowtie:?}"
);
dump_loops_debug("INPUT", solid);
dump_loops_debug("RESULT", &network);
}
}
Err(refusal) => {
if std::env::var("BREP_DEBUG_NETWORK").is_ok() {
eprintln!("network refused: {refusal}");
}
if crate::blend::is_marched_fit_off_carriers(&refusal)
|| crate::blend::is_consumed_snap(&refusal)
|| crate::blend::is_rail_collapse(&refusal)
|| crate::blend::is_full_width_collapse(&refusal)
|| crate::blend::is_planar_chart_refusal(&refusal)
{
return Err(refusal);
}
}
}
}
CUTTER_COMPOSITIONS.with(|count| count.set(count.get() + 1));
let miter_operation = if (chain_corner_count > 0 || !corners.is_empty())
&& (chamfer || corners.is_empty())
{
let mut any_convex = false;
let mut any_concave = false;
for point in edge_points {
match resolve_edge_by_point(solid, *point)
.and_then(|edge_id| super::analyze::scan_dihedral(solid, edge_id))
{
Ok(profile) if profile.samples > 0 && !profile.is_mixed() => {
any_convex |= profile.any_convex;
any_concave |= profile.any_concave;
}
_ => {
any_convex = true;
any_concave = true;
break;
}
}
}
match (any_convex, any_concave) {
(true, false) => Some(crate::BooleanOperation::Intersect),
(false, true) => Some(crate::BooleanOperation::Union),
_ => None,
}
} else {
None
};
let composition_lane = if corners.is_empty() && chain_corner_count == 0 {
Lane::GeneralFirst
} else {
Lane::CutterFirst
};
let build_sequential = || -> Result<BrepSolid, KernelRefusal> {
let mut sequential = solid.clone();
for (index, point) in edge_points.iter().enumerate() {
let edge_id = resolve_edge_by_point(&sequential, *point)?;
let ends = tool_ends_to_original_extent(
&sequential,
edge_id,
radius,
original_extents[index],
if chamfer { &[] } else { &corners },
);
let edge_name = per_edge_name(edge_names, name, index);
sequential = fillet_or_chamfer(
&sequential,
edge_id,
radius,
chamfer,
edge_name,
ends,
composition_lane,
)?;
}
Ok(sequential)
};
let mut result = if let Some(operation) = miter_operation {
let options = crate::BooleanOptions {
keep_unmerged_name_substrs: vec![CUTTER_SCAFFOLD_NAME.to_string()],
..crate::BooleanOptions::default()
};
let mut combined: Result<Option<BrepSolid>, KernelRefusal> = Ok(None);
for (index, point) in edge_points.iter().enumerate() {
let edge_id = resolve_edge_by_point(solid, *point)?;
let edge_name = per_edge_name(edge_names, name, index);
let blended = {
let entry = if chamfer { "chamfer_edges" } else { "fillet_edges" };
check_mixed_concavity(solid, edge_id, entry)?;
check_support_extent(solid, edge_id, radius, entry)?;
fillet_or_chamfer(
solid,
edge_id,
radius,
chamfer,
edge_name,
ToolEnds::default(),
Lane::CutterFirst,
)?
};
combined = match combined {
Err(e) => Err(e),
Ok(None) => Ok(Some(blended)),
Ok(Some(previous)) => {
crate::boolean_operation(&previous, &blended, operation, &options)
.map(Some)
.map_err(|error| {
error.with_message(|error| {
format!("fillet_edges: chain-corner miter composition failed: {error}")
})
})
}
};
if combined.is_err() {
break;
}
}
match combined {
Ok(Some(mitered)) => mitered,
Ok(None) => {
return Err(KernelRefusal::input(
KernelStage::Collect,
"miter_selection",
"fillet_edges: no edges selected",
))
}
Err(miter_err) => match build_sequential() {
Ok(seq) if seq.validate().is_empty() => seq,
_ => return Err(miter_err),
},
}
} else {
build_sequential()?
};
if !chamfer {
for (corner, adjacent) in &chain_corners {
let has_concave_unselected_edge = solid.edges.iter().any(|edge| {
if selected_edge_ids.contains(&edge.id) {
return false;
}
let Ok(a) = edge.curve.evaluate(edge.t0) else {
return false;
};
let Ok(b) = edge.curve.evaluate(edge.t1) else {
return false;
};
(a.sub(*corner).length() < 1e-6 || b.sub(*corner).length() < 1e-6)
&& analyze_edge(solid, edge.id, radius)
.map(|cross| !cross.convex)
.unwrap_or(false)
});
if !has_concave_unselected_edge {
continue;
}
let corner_name = corner_face_name(edge_names, name, adjacent);
if let Ok(rounded) = crate::blend::round_concave_chain_corner(
&result,
solid,
*corner,
[
selected_edge_ids[adjacent[0]],
selected_edge_ids[adjacent[1]],
],
radius,
corner_name.as_deref(),
) {
result = rounded;
}
}
}
if !chamfer {
for (ci, corner) in corners.iter().enumerate() {
let corner_name = corner_face_name(edge_names, name, &corner_edges[ci]);
if let Ok(rounded) =
crate::blend::round_convex_corner(&result, *corner, radius, corner_name.as_deref())
{
result = rounded;
}
}
}
heal_edge_vertex_gaps(&mut result, radius)?;
crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
Ok(settle_cutter_scaffold(result))
}
pub fn fillet_edges_variable(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
radii: &[(f64, f64)],
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
if edge_points.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"variable_selection",
"fillet_edges_variable: no edges selected",
));
}
let per_edge_stops: Vec<Vec<(f64, f64)>> = vec![radii.to_vec(); edge_points.len()];
fillet_edges_variable_impl(
solid,
edge_points,
edge_names,
&per_edge_stops,
chamfer,
name,
"fillet_edges_variable",
true,
)
}
#[allow(clippy::too_many_arguments)]
fn fillet_edges_variable_impl(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
per_edge_stops: &[Vec<(f64, f64)>],
chamfer: bool,
name: Option<&str>,
entry: &str,
allow_tapered_sequential: bool,
) -> Result<BrepSolid, KernelRefusal> {
debug_assert_eq!(per_edge_stops.len(), edge_points.len());
if let Some(constant) = per_edge_stops
.first()
.and_then(|stops| stops.first())
.map(|(_, radius)| *radius)
{
let uniform = per_edge_stops.iter().all(|stops| {
stops
.iter()
.all(|(_, radius)| (radius - constant).abs() <= 1e-12 * (1.0 + constant.abs()))
});
if uniform && constant > 0.0 {
return fillet_edges(solid, edge_points, edge_names, constant, chamfer, name);
}
}
let max_radius = per_edge_stops
.iter()
.flat_map(|stops| stops.iter())
.map(|(_, r)| r.abs())
.fold(0.0_f64, f64::max);
let mut chain_corner = false;
{
use rustc_hash::FxHashSet as HashSet;
let mut endpoints: Vec<(Vec3, usize)> = Vec::with_capacity(edge_points.len() * 2);
for (i, point) in edge_points.iter().enumerate() {
if let Ok(edge_id) = resolve_edge_by_point(solid, *point) {
if let Some(edge) = solid.edges.iter().find(|e| e.id == edge_id) {
if let (Ok(a), Ok(b)) =
(edge.curve.evaluate(edge.t0), edge.curve.evaluate(edge.t1))
{
endpoints.push((a, i));
endpoints.push((b, i));
}
}
}
}
let mut used = vec![false; endpoints.len()];
for i in 0..endpoints.len() {
if used[i] {
continue;
}
used[i] = true;
let mut edges_here: HashSet<usize> = HashSet::default();
edges_here.insert(endpoints[i].1);
for j in (i + 1)..endpoints.len() {
if used[j] {
continue;
}
if endpoints[i].0.sub(endpoints[j].0).length() < 1e-6 {
used[j] = true;
edges_here.insert(endpoints[j].1);
}
}
if edges_here.len() == 2 {
chain_corner = true;
}
}
}
let miter_operation = if chain_corner {
let probe_radius = if max_radius > 0.0 { max_radius } else { 1.0 };
let mut any_convex = false;
let mut any_concave = false;
for point in edge_points {
match resolve_edge_by_point(solid, *point)
.and_then(|edge_id| analyze_edge(solid, edge_id, probe_radius))
{
Ok(cross) if cross.convex => any_convex = true,
Ok(_) => any_concave = true,
Err(_) => {
any_convex = true;
any_concave = true;
break;
}
}
}
match (any_convex, any_concave) {
(true, false) => Some(crate::BooleanOperation::Intersect),
(false, true) => Some(crate::BooleanOperation::Union),
_ => None,
}
} else {
None
};
let miter_attempt: Option<BrepSolid> = if let Some(operation) = miter_operation {
let options = crate::BooleanOptions::default();
let mut combined: Option<BrepSolid> = None;
let mut failed = false;
for (index, point) in edge_points.iter().enumerate() {
let Ok(edge_id) = resolve_edge_by_point(solid, *point) else {
failed = true;
break;
};
let edge_name = per_edge_name(edge_names, name, index);
let Ok(blended) = crate::blend::blend_edge_variable(
solid,
edge_id,
&per_edge_stops[index],
chamfer,
edge_name,
) else {
failed = true;
break;
};
let next = match combined.take() {
None => blended,
Some(previous) => {
match crate::boolean_operation(&previous, &blended, operation, &options) {
Ok(next) => next,
Err(_) => {
failed = true;
break;
}
}
}
};
combined = Some(next);
}
if failed {
None
} else {
combined.filter(|s| s.validate().is_empty())
}
} else {
None
};
let mut result = match miter_attempt {
Some(mitered) => mitered,
None => {
let tapered = per_edge_stops.iter().any(|stops| {
stops.first().is_some_and(|(_, first)| {
stops
.iter()
.any(|(_, r)| (r - first).abs() > 1e-12 * (1.0 + first.abs()))
})
});
if chain_corner && tapered && !allow_tapered_sequential {
return Err(KernelRefusal::unsupported(
KernelStage::Sew,
"tapered_chain_miter",
format!(
"{entry}: the tapered blends across the selected chain's shared corners \
did not compose to a valid solid (the fitted blend boundaries could not \
be mitered); fillet fewer edges per operation or reduce the taper"
),
));
}
let mut sequential = solid.clone();
for (index, point) in edge_points.iter().enumerate() {
let edge_id = resolve_edge_by_point(&sequential, *point)?;
let edge_name = per_edge_name(edge_names, name, index);
sequential = crate::blend::blend_edge_variable(
&sequential,
edge_id,
&per_edge_stops[index],
chamfer,
edge_name,
)?;
}
sequential
}
};
heal_edge_vertex_gaps(&mut result, max_radius)?;
crate::blend::fit_planar_charts_to_trims(solid, &mut result)?;
let issues = result.validate();
if !issues.is_empty() {
let (detail, class) = if allow_tapered_sequential {
(
"tapered blends meet at a shared chain vertex with \
mismatched radii — the radius-transition corner patch is not implemented; \
fillet the edges in separate operations or use matching stop radii",
RefusalClass::UnsupportedGeometry {
what: "taper_transition_patch".into(),
},
)
} else {
(
"the composed radius-law blend produced invalid topology — the blends \
across a shared chain corner failed to reassemble",
RefusalClass::Internal {
what: "law_chain_topology".into(),
},
)
};
return Err(KernelRefusal::new(
class,
KernelStage::Validate,
format!(
"{entry}: {detail} ({} validation issues, first: {})",
issues.len(),
issues
.first()
.map(|issue| issue.message.clone())
.unwrap_or_default()
),
));
}
crate::accept_sound(result, entry)
}
struct ChainLink {
input_index: usize,
forward: bool,
arc: Vec<(f64, f64)>,
length: f64,
abscissa: f64,
}
fn edge_arc_table(
curve: &NurbsCurve,
t0: f64,
t1: f64,
tol: f64,
) -> Result<(Vec<(f64, f64)>, f64), KernelRefusal> {
let evaluate = |t: f64| curve.evaluate(t).or_refuse(KernelStage::Collect, "arc_table");
let build = |n: usize| -> Result<(Vec<(f64, f64)>, f64), KernelRefusal> {
let mut table = Vec::with_capacity(n + 1);
let mut cumulative = 0.0;
let mut previous = evaluate(t0)?;
table.push((0.0, 0.0));
for j in 1..=n {
let fraction = j as f64 / n as f64;
let point = evaluate(t0 + (t1 - t0) * fraction)?;
cumulative += point.sub(previous).length();
previous = point;
table.push((fraction, cumulative));
}
Ok((table, cumulative))
};
let mut n = 16usize;
let (mut table, mut length) = build(n)?;
while n < 4096 {
n *= 2;
let (next_table, next_length) = build(n)?;
let converged = (next_length - length).abs() <= tol;
table = next_table;
length = next_length;
if converged {
break;
}
}
Ok((table, length))
}
fn arc_length_at_fraction(table: &[(f64, f64)], fraction: f64) -> f64 {
let fraction = fraction.clamp(0.0, 1.0);
let intervals = table.len() - 1;
let scaled = fraction * intervals as f64;
let index = (scaled.floor() as usize).min(intervals - 1);
let local = scaled - index as f64;
let (_, a) = table[index];
let (_, b) = table[index + 1];
a + (b - a) * local
}
fn resolve_selected_chain(
solid: &BrepSolid,
edge_points: &[Vec3],
entry: &str,
tol: f64,
) -> Result<Vec<ChainLink>, KernelRefusal> {
let band = crate::tolerance::COINCIDENCE_DISTANCE_FLOOR;
struct Resolved {
edge_id: u64,
start: Vec3,
end: Vec3,
arc: Vec<(f64, f64)>,
length: f64,
}
let mut resolved: Vec<Resolved> = Vec::with_capacity(edge_points.len());
for point in edge_points {
let edge_id = resolve_edge_by_point(solid, *point)?;
if resolved.iter().any(|r| r.edge_id == edge_id) {
return Err(KernelRefusal::input(
KernelStage::Collect,
"duplicate_edge",
format!("{entry}: the same edge was selected more than once"),
));
}
let edge = solid
.edges
.iter()
.find(|e| e.id == edge_id)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"chain_resolved_edge",
format!("{entry}: resolved edge vanished"),
)
})?;
let (arc, length) = edge_arc_table(&edge.curve, edge.t0, edge.t1, tol)?;
if !(length > 0.0) {
return Err(KernelRefusal::input(
KernelStage::Collect,
"zero_length_edge",
format!("{entry}: selected edge has zero length"),
));
}
resolved.push(Resolved {
edge_id,
start: edge
.curve
.evaluate(edge.t0)
.or_refuse(KernelStage::Collect, "chain_edge_start")?,
end: edge
.curve
.evaluate(edge.t1)
.or_refuse(KernelStage::Collect, "chain_edge_end")?,
arc,
length,
});
}
let n = resolved.len();
if n == 1 {
let only = resolved.remove(0);
return Ok(vec![ChainLink {
input_index: 0,
forward: true,
arc: only.arc,
length: only.length,
abscissa: 0.0,
}]);
}
let mut clusters: Vec<(Vec3, Vec<(usize, bool)>)> = Vec::new();
for (i, r) in resolved.iter().enumerate() {
for (point, is_start) in [(r.start, true), (r.end, false)] {
match clusters
.iter_mut()
.find(|(anchor, _)| anchor.sub(point).length() < band)
{
Some((_, members)) => members.push((i, is_start)),
None => clusters.push((point, vec![(i, is_start)])),
}
}
}
if clusters.iter().any(|(_, members)| members.len() > 2) {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chain_branching",
format!(
"{entry}: selected edges must form one open chain \
(a vertex is shared by three or more selected edges)"
),
));
}
let free: Vec<usize> = clusters
.iter()
.enumerate()
.filter(|(_, (_, members))| members.len() == 1)
.map(|(c, _)| c)
.collect();
if free.len() != 2 {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chain_open",
format!(
"{entry}: selected edges must form one OPEN chain \
(closed rings and disconnected selections are not supported)"
),
));
}
let start_cluster = *free
.iter()
.min_by_key(|&&c| clusters[c].1[0].0)
.expect("two free ends");
let mut links: Vec<ChainLink> = Vec::with_capacity(n);
let mut visited = vec![false; n];
let mut abscissa = 0.0_f64;
let mut cluster = start_cluster;
for _ in 0..n {
let Some(&(edge_index, entered_at_start)) = clusters[cluster]
.1
.iter()
.find(|(edge_index, _)| !visited[*edge_index])
else {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chain_walk",
format!(
"{entry}: selected edges are not connected into one chain"
),
));
};
visited[edge_index] = true;
let r = &resolved[edge_index];
links.push(ChainLink {
input_index: edge_index,
forward: entered_at_start,
arc: r.arc.clone(),
length: r.length,
abscissa,
});
abscissa += r.length;
let exit_point = if entered_at_start { r.end } else { r.start };
cluster = clusters
.iter()
.position(|(anchor, _)| anchor.sub(exit_point).length() < band)
.ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"chain_cluster",
format!("{entry}: chain walk lost an endpoint cluster"),
)
})?;
}
if visited.iter().any(|v| !v) {
return Err(KernelRefusal::input(
KernelStage::Collect,
"chain_connected",
format!(
"{entry}: selected edges are not connected into one chain"
),
));
}
Ok(links)
}
fn law_stops_for_link(
link: &ChainLink,
law: &crate::law::RadiusLaw,
scale: f64,
tol: f64,
) -> Vec<(f64, f64)> {
let radius_at_fraction = |fraction: f64| -> f64 {
let arc = arc_length_at_fraction(&link.arc, fraction);
let chain_s = if link.forward {
link.abscissa + arc
} else {
link.abscissa + (link.length - arc)
};
law.radius_at(chain_s * scale)
};
let curvature = law
.max_second_derivative(link.abscissa * scale, (link.abscissa + link.length) * scale)
* (scale * link.length).powi(2);
let base = if curvature * 0.125 <= tol {
1usize
} else {
((curvature / (8.0 * tol)).sqrt().ceil() as usize).clamp(1, 256)
};
let mut stops: Vec<(f64, f64)> = (0..=base)
.map(|j| {
let fraction = j as f64 / base as f64;
(fraction, radius_at_fraction(fraction))
})
.collect();
let mut depth = 0usize;
while depth < 8 {
let mut refined: Vec<(f64, f64)> = Vec::with_capacity(stops.len());
let mut inserted = false;
for pair in stops.windows(2) {
refined.push(pair[0]);
let mid_fraction = 0.5 * (pair[0].0 + pair[1].0);
let law_mid = radius_at_fraction(mid_fraction);
let linear_mid = 0.5 * (pair[0].1 + pair[1].1);
if (law_mid - linear_mid).abs() > tol {
refined.push((mid_fraction, law_mid));
inserted = true;
}
}
refined.push(*stops.last().expect("at least two stops"));
stops = refined;
if !inserted {
break;
}
depth += 1;
}
stops
}
pub fn fillet_edges_variable_law(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
law: &crate::law::RadiusLaw,
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
const ENTRY: &str = "fillet_edges_variable_law";
if edge_points.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"law_selection",
format!("{ENTRY}: no edges selected"),
));
}
let tolerances = crate::KernelTolerances::for_solid(solid, 1e-7);
let chain = resolve_selected_chain(solid, edge_points, ENTRY, tolerances.intersection_fit)?;
fillet_variable_law_on_chain(
solid,
edge_points,
edge_names,
&chain,
law,
chamfer,
name,
ENTRY,
tolerances.intersection_fit,
)
}
pub fn fillet_edges_variable_vertex_radii(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
vertex_radii: &[f64],
chamfer: bool,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
const ENTRY: &str = "fillet_edges_variable_vertex_radii";
if edge_points.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"vertex_radii_selection",
format!("{ENTRY}: no edges selected"),
));
}
let tolerances = crate::KernelTolerances::for_solid(solid, 1e-7);
let chain = resolve_selected_chain(solid, edge_points, ENTRY, tolerances.intersection_fit)?;
let lengths: Vec<f64> = chain.iter().map(|link| link.length).collect();
let law = crate::law::RadiusLaw::from_vertex_radii(&lengths, vertex_radii)
.map_err(|error| error.with_message(|error| format!("{ENTRY}: {error}")))?;
fillet_variable_law_on_chain(
solid,
edge_points,
edge_names,
&chain,
&law,
chamfer,
name,
ENTRY,
tolerances.intersection_fit,
)
}
#[allow(clippy::too_many_arguments)]
fn fillet_variable_law_on_chain(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
chain: &[ChainLink],
law: &crate::law::RadiusLaw,
chamfer: bool,
name: Option<&str>,
entry: &str,
tol: f64,
) -> Result<BrepSolid, KernelRefusal> {
let chain_length: f64 = chain.iter().map(|link| link.length).sum();
let scale = law.total_length() / chain_length;
let mut per_edge_stops: Vec<Vec<(f64, f64)>> = vec![Vec::new(); edge_points.len()];
for link in chain {
per_edge_stops[link.input_index] = law_stops_for_link(link, law, scale, tol);
}
fillet_edges_variable_impl(
solid,
edge_points,
edge_names,
&per_edge_stops,
chamfer,
name,
entry,
false,
)
}
pub fn chamfer_edges_asymmetric(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
d1: f64,
d2: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
if edge_points.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"asymmetric_selection",
"chamfer_edges_asymmetric: no edges selected",
));
}
let mut result = solid.clone();
for (index, point) in edge_points.iter().enumerate() {
let edge_id = resolve_edge_by_point(&result, *point)?;
let edge_name = per_edge_name(edge_names, name, index);
result = chamfer_edge_asymmetric(&result, edge_id, d1, d2, edge_name)?;
}
Ok(result)
}
pub fn chamfer_edges_angle(
solid: &BrepSolid,
edge_points: &[Vec3],
edge_names: Option<&[String]>,
d1: f64,
angle_rad: f64,
name: Option<&str>,
) -> Result<BrepSolid, KernelRefusal> {
if edge_points.is_empty() {
return Err(KernelRefusal::input(
KernelStage::Collect,
"angle_selection",
"chamfer_edges_angle: no edges selected",
));
}
let mut result = solid.clone();
for (index, point) in edge_points.iter().enumerate() {
let edge_id = resolve_edge_by_point(&result, *point)?;
let edge_name = per_edge_name(edge_names, name, index);
result = chamfer_edge_angle(&result, edge_id, d1, angle_rad, edge_name)?;
}
Ok(result)
}
fn dump_loops_debug(label: &str, solid: &BrepSolid) {
for shell in &solid.shells {
for face in &shell.faces {
for (index, loop_record) in face.loops.iter().enumerate() {
let walk: Vec<String> = loop_record
.coedges
.iter()
.map(|coedge| {
match solid.edges.iter().find(|e| e.id == coedge.edge_id) {
Some(edge) => format!(
"c{}:e{}{}({}->{})",
coedge.id,
edge.id,
if coedge.forward { "+" } else { "-" },
edge.start_vertex_id,
edge.end_vertex_id
),
None => format!("c{}:e{}?MISSING", coedge.id, coedge.edge_id),
}
})
.collect();
eprintln!(
"{label} face {} ({:?}) loop {index}: {}",
face.id,
face.name,
walk.join(" ")
);
}
}
}
}