use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{FeatureContext, FeatureRefusal, FeatureResult};
use crate::{KernelRefusal, KernelStage};
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, FeatureRefusal> {
let selection = common::resolve_blend_selection(ctx)?;
let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
result.unresolved = selection.unresolved;
if selection.multi_solid {
return Ok(result);
}
let Some(mut target) = selection.target else {
if result.unresolved.is_empty() {
return Ok(result);
}
let mut refused = ctx.fail(unresolved_refusal("fillet", &result.unresolved));
refused.unresolved = result.unresolved;
return Ok(refused);
};
result.note_partial_resolution(&common::reference_names(ctx.param("edges")));
let radius = ctx.number("radius")?;
if !(radius.is_finite() && radius > 0.0) {
return Ok(result);
}
let taper = common::optional_number(ctx, "radiusEnd")?
.filter(|value| value.is_finite() && *value > 0.0 && (*value - radius).abs() > 1e-9);
let followed = if follow_tangent(ctx) { follow_tangent_chains(&mut target)? } else { Vec::new() };
if !followed.is_empty() {
let requested = common::reference_names(ctx.param("edges"));
let fulfilment = result.fulfilment.get_or_insert_with(|| crate::feature_pipeline::Fulfilment {
applied: requested.clone(),
requested,
..Default::default()
});
fulfilment.followed = followed;
}
let blend_base = common::blend_face_base(&ctx.id);
let blend_names: Vec<String> = target
.edge_names
.iter()
.map(|edge_name| common::blend_face_name(&ctx.id, edge_name))
.collect();
let blended = crate::with_registered_solid_typed(target.handle, |solid| match taper {
Some(radius_end) => crate::fillet_edges_variable(
solid,
&target.edge_points,
Some(&blend_names),
&[(0.0, radius), (1.0, radius_end)],
false,
Some(&blend_base),
),
None => crate::fillet_edges(
solid,
&target.edge_points,
Some(&blend_names),
radius,
false,
Some(&blend_base),
),
})
.map_err(|error| error.with_message(|error| format!("fillet failed: {error}")));
let blended = match blended {
Ok(blended) => blended,
Err(refusal) => {
let rerun = if taper.is_none() { along_tangent_chain(ctx, &target, radius, &blend_base) } else { None };
let Some((blended, extended)) = rerun else { return Err(refusal.into()) };
let requested = common::reference_names(ctx.param("edges"));
let fulfilment = result.fulfilment.get_or_insert_with(|| crate::feature_pipeline::Fulfilment {
applied: requested.clone(),
requested,
..Default::default()
});
fulfilment.extended = extended;
blended
}
};
result.added.push(common::register_added(blended, &target.name));
result.removed.push(target.name);
Ok(result)
}
fn follow_tangent(ctx: &FeatureContext) -> bool {
ctx.param("followTangent").and_then(|value| value.as_bool()).unwrap_or(false)
}
fn follow_tangent_chains(target: &mut common::BlendTarget) -> Result<Vec<String>, FeatureRefusal> {
let expansion = crate::with_registered_solid_str(target.handle, |solid| {
common::tangent_chain_expansion(solid, &target.picked_edge_ids, &target.edge_ids)
})
.map_err(|error| KernelRefusal::internal(KernelStage::Collect, "tangent_chain", error))?;
let mut added = Vec::with_capacity(expansion.len());
for (id, point, name) in expansion {
target.edge_ids.push(id);
target.edge_points.push(point);
target.edge_names.push(name.clone());
added.push(name);
}
Ok(added)
}
fn along_tangent_chain(
ctx: &FeatureContext,
target: &common::BlendTarget,
radius: f64,
blend_base: &str,
) -> Option<(crate::BrepSolid, Vec<String>)> {
crate::with_registered_solid_typed(target.handle, |solid| {
let extension = common::open_tangent_chain_extension(solid, &target.edge_ids)
.map_err(|error| KernelRefusal::internal(KernelStage::Collect, "tangent_chain", error))?;
if extension.is_empty() {
return Err(KernelRefusal::internal(KernelStage::Collect, "tangent_chain", "no tangent continuation"));
}
let mut points = target.edge_points.clone();
let mut names: Vec<String> = target.edge_names.iter().map(|name| common::blend_face_name(&ctx.id, name)).collect();
let mut extended = Vec::with_capacity(extension.len());
for (_, point, name) in extension {
points.push(point);
names.push(common::blend_face_name(&ctx.id, &name));
extended.push(name);
}
crate::fillet_edges(solid, &points, Some(&names), radius, false, Some(blend_base)).map(|body| (body, extended))
})
.ok()
}
pub(super) fn unresolved_refusal(feature: &str, unresolved: &[String]) -> KernelRefusal {
let names: Vec<String> = unresolved.iter().map(|name| format!("`{name}`")).collect();
KernelRefusal::input(
KernelStage::Collect,
"unresolved_reference",
format!(
"{feature}: none of the {} selected reference{} resolves on this model ({}); a blend \
of nothing is not a result, so the selection is refused — repair the reference{} \
and rerun",
unresolved.len(),
if unresolved.len() == 1 { "" } else { "s" },
names.join(", "),
if unresolved.len() == 1 { "" } else { "s" },
),
)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.faces > 0 || probe.edges > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "F",
"shortName": "F",
"longName": "Fillet",
"ribbonPath": "Home/Blend/Fillet",
"commandSize": "Compact",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the fillet feature"
},
"edges": {
"type": "reference_selection",
"selectionFilter": [
"FACE",
"EDGE"
],
"timestampDependency": "parentSolid",
"multiple": true,
"default_value": null,
"hint": "Select faces (or an edge) to fillet along shared edges"
},
"radius": {
"type": "number",
"step": 0.1,
"default_value": 1,
"hint": "Fillet radius"
},
"radiusEnd": {
"type": "number",
"step": 0.1,
"default_value": 0,
"hint": "Optional end radius for a VARIABLE (tapered) fillet. 0 = constant radius. When > 0 and different from radius, each selected edge tapers from radius at its start to this value at its end (kernel-exact path; corners are not rounded in variable mode)."
},
"followTangent": {
"type": "boolean",
"default_value": true,
"label": "Follow tangent",
"hint": "Grow each selected edge to its whole tangent (G1) chain: pick one edge of a rounded rectangle and the fillet runs around the whole loop. A chain stops at a sharp corner and at a vertex where more than one edge continues it tangentially. The added edges are listed on the feature. New fillets start with it on; a fillet saved before the option existed reads as off."
}
}
})
}