Fillet dialog: middle radii for variable fillets (add, remove, position and radius) and the Smooth toggle

Variable type shows a Mid Radius row per point (position along the edge from the start end, 0.01 to 0.99, and a radius expression, with a remove button), an Add Mid Radius button and, with points present, Smooth. They go to solid.fillet as mid and smooth and come back on Edit Feature. Test: the round trip and removing every point.

Refs #9

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-10 06:15:30 +00:00
1 parent be18bd6573
commit 62746045af
2 files changed
+84 -1

No files matched your search

+60 -1
View File
@@ -1422,6 +1422,37 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
row_label(ui, "Swap Ends");
extra_check(ui, &mut d.extra, "swap_ends");
ui.end_row();
// Radii between the ends: position along the edge (from the start end)
// and radius.
let mut mids = mid_points(&d.extra);
let mut remove = None;
for (k, (t, r)) in mids.iter_mut().enumerate() {
row_label(ui, "Mid Radius");
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(t).speed(0.01).range(0.01..=0.99).fixed_decimals(2))
.on_hover_text("Position along the edge, 0 at the start end, 1 at the other");
enter |= field(ui, r);
if ui.small_button("×").on_hover_text("Remove this point").clicked() {
remove = Some(k);
}
});
ui.end_row();
}
if let Some(k) = remove {
mids.remove(k);
}
row_label(ui, "");
if ui.button("Add Mid Radius").clicked() && mids.len() < 20 {
let t = if mids.is_empty() { 0.5 } else { (mids.iter().map(|m| m.0).fold(0.0, f64::max) + 1.0) * 0.5 };
mids.push((t.min(0.99), radius.clone()));
}
ui.end_row();
d.extra.insert("mid".into(), json!(mids.iter().map(|(t, r)| json!([t, r])).collect::<Vec<_>>()));
if !mids.is_empty() {
row_label(ui, "Smooth");
extra_check(ui, &mut d.extra, "smooth");
ui.end_row();
}
}
row_label(ui, "Tangent Chain");
ui.checkbox(chain, "");
@@ -1913,6 +1944,25 @@ fn extra_field(ui: &mut egui::Ui, extra: &mut serde_json::Map<String, Value>, ke
enter
}
/// A variable fillet's middle radii as (position, radius expression).
fn mid_points(extra: &serde_json::Map<String, Value>) -> Vec<(f64, String)> {
extra
.get("mid")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|m| {
let t = m.get(0)?.as_f64()?;
let r = match m.get(1)? {
Value::String(x) => x.clone(),
Value::Number(n) => n.to_string(),
_ => return None,
};
Some((t, r))
})
.collect()
}
const CHAMFER_TYPES: [&str; 3] = ["Equal Distance", "Two Distances", "Distance and Angle"];
/// A new Fillet or Chamfer dialog's values.
@@ -2170,6 +2220,13 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
if let Some((a, b)) = ends {
p["start"] = pt(if swap { b } else { a });
}
let mids = mid_points(&d.extra);
if !mids.is_empty() {
p["mid"] = json!(mids.iter().map(|(t, r)| json!([t, r])).collect::<Vec<_>>());
if d.extra.get("smooth").and_then(Value::as_bool).unwrap_or(false) {
p["smooth"] = json!(true);
}
}
}
("solid.fillet", p)
}
@@ -2790,9 +2847,11 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FilletStyle::Chord => {
d.extra.insert("type".into(), json!("chord"));
}
FilletStyle::Variable { radius2, start, .. } => {
FilletStyle::Variable { radius2, start, mid, smooth } => {
d.extra.insert("type".into(), json!("variable"));
d.extra.insert("radius2".into(), json!(radius2));
d.extra.insert("mid".into(), json!(mid.iter().map(|(t, r)| json!([t, r])).collect::<Vec<_>>()));
d.extra.insert("smooth".into(), json!(smooth));
// The start was the first edge's last point: ends swapped.
let first_end = edges.first().and_then(|p| edge_sel(s, *p)).and_then(|x| match x {
Sel::Edge { body, index, .. } => {
+24
View File
@@ -261,6 +261,30 @@ fn variable_fillet_round_trip() {
assert_eq!(e.extra.get("radius2"), Some(&json!("4 mm")));
}
#[test]
fn variable_fillet_mid_radii_round_trip() {
let mut app = boxed();
let e0 = edge(&app, Vec3::new(40.0, 15.0, 20.0));
let mut d = start(&mut app, vec![e0], "solid.fillet");
d.inputs[0].items.truncate(1);
d.extra.insert("type".into(), json!("variable"));
d.extra.insert("radius2".into(), json!("2 mm"));
d.extra.insert("mid".into(), json!([[0.5, "4 mm"]]));
d.extra.insert("smooth".into(), json!(true));
let p = apply_commands(&app, &d).unwrap().remove(0).1;
assert_eq!((p["mid"].clone(), p["smooth"].clone()), (json!([[0.5, "4 mm"]]), json!(true)), "{p}");
let id = apply(&mut app, &d);
let e = crate::dialogs::for_feature(&app, id, None).unwrap();
assert_eq!(e.extra.get("mid"), Some(&json!([[0.5, "4 mm"]])));
assert_eq!(e.extra.get("smooth"), Some(&json!(true)));
// All middle radii removed: a plain two-radius fillet again.
let mut d = e;
d.extra.insert("mid".into(), json!([]));
let p = apply_commands(&app, &d).unwrap().remove(0).1;
// (an edit: the feature's new parameters)
assert_eq!(p["params"]["mid"], json!([]), "{p}");
}
#[test]
fn newer_options_reach_their_commands() {
let mut app = boxed();