diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 0460a48d..f56162aa 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -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::>())); + 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, ke enter } +/// A variable fillet's middle radii as (position, radius expression). +fn mid_points(extra: &serde_json::Map) -> 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, 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::>()); + 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) -> Option { 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::>())); + 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, .. } => { diff --git a/crates/ui-egui/src/preselect_tests.rs b/crates/ui-egui/src/preselect_tests.rs index f7bbfd89..afc2ea45 100644 --- a/crates/ui-egui/src/preselect_tests.rs +++ b/crates/ui-egui/src/preselect_tests.rs @@ -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();