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:
1 parent
be18bd6573
commit
62746045af
2 files changed
+84
-1
No files matched your search
@@ -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, .. } => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user