use std::f64::consts::PI;
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{FeatureContext, FeatureResult};
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
common::require_blend_direction(ctx, "chamfer")?;
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 Err("chamfer requires the selected edges to belong to a single solid".into());
}
let Some(target) = selection.target else {
return Ok(result);
};
let distance = ctx.number("distance")?;
if !(distance.is_finite() && distance > 0.0) {
return Ok(result);
}
let distance2 = common::optional_number(ctx, "distance2")?.unwrap_or(0.0);
let angle_deg = common::optional_number(ctx, "angle")?.unwrap_or(0.0);
let d2 = (distance2.is_finite() && distance2 > 0.0).then_some(distance2);
let angle_rad = (d2.is_none() && angle_deg.is_finite() && angle_deg > 0.0)
.then(|| angle_deg * PI / 180.0);
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_str(target.handle, |solid| {
if let Some(d2) = d2 {
crate::chamfer_edges_asymmetric(
solid,
&target.edge_points,
Some(&blend_names),
distance,
d2,
Some(&blend_base),
)
} else if let Some(angle) = angle_rad {
crate::chamfer_edges_angle(
solid,
&target.edge_points,
Some(&blend_names),
distance,
angle,
Some(&blend_base),
)
} else {
crate::fillet_edges(
solid,
&target.edge_points,
Some(&blend_names),
distance,
true,
Some(&blend_base),
)
}
})
.map_err(|error| format!("chamfer failed: {error}"))?;
result.added.push(common::register_added(blended, &target.name));
result.removed.push(target.name);
Ok(result)
}
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": "CH",
"shortName": "CH",
"longName": "Chamfer",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the chamfer feature"
},
"edges": {
"type": "reference_selection",
"selectionFilter": [
"EDGE",
"FACE"
],
"timestampDependency": "parentSolid",
"multiple": true,
"default_value": null,
"hint": "Select edges or faces to apply the chamfer"
},
"distance": {
"type": "number",
"step": 0.1,
"default_value": 1,
"hint": "Chamfer distance. Symmetric (distance2 and angle both 0): the ROLLING-BALL RADIUS — the bevel joins the points a ball of this radius touches, which equals the setback along each face on a 90 degree planar edge and exceeds it on curved faces. Otherwise: the setback along the first face."
},
"distance2": {
"type": "number",
"step": 0.1,
"default_value": 0,
"hint": "Optional second setback for an ASYMMETRIC (two-distance) chamfer, measured along the second face. 0 = symmetric. When > 0, the edge gets a lopsided distance × distance2 bevel (kernel-exact path; straight edges on planar faces)."
},
"angle": {
"type": "number",
"step": 1,
"default_value": 0,
"hint": "Optional chamfer angle in DEGREES between the chamfer face and the first face, for a distance-angle chamfer. 0 = unused. Ignored when distance2 > 0. The second setback is computed from distance and this angle (kernel-exact path)."
},
"direction": {
"type": "options",
"options": [
"AUTO",
"INSET"
],
"default_value": "AUTO",
"hint": "Choose the chamfer side automatically (AUTO) or force INSET. There is no OUTSET: it belonged to the removed legacy mesh pipeline and the direction gate rejects it."
}
}
})
}