diff --git a/crates/ui-egui/src/sketch_corner_ui_tests.rs b/crates/ui-egui/src/sketch_corner_ui_tests.rs index c07e49d2..d5359c9b 100644 --- a/crates/ui-egui/src/sketch_corner_ui_tests.rs +++ b/crates/ui-egui/src/sketch_corner_ui_tests.rs @@ -431,3 +431,47 @@ fn ordinary_drawing_keeps_origin_snap_and_real_origin_corners_work() { assert!(ss.sketch.curves.iter().any(|c| matches!(c.kind, CurveKind::Arc { .. }))); assert_eq!(ss.profiles.len(), 1); } + +/// #1824: where a third line ends at a corner's point, the point names no single corner; the +/// Fillet window takes the corner's two lines instead and rounds the corner between them. +#[test] +fn fillet_window_rounds_the_corner_between_two_picked_lines() { + let mut h = crate::scenario::Harness::new(); + h.app.run("sketch.create", json!({"plane":"XY"})).unwrap(); + h.app.run("sketch.line", json!({"points":[[0,0],[10,0]],"ids":["left"]})).unwrap(); + h.app.run("sketch.line", json!({"points":["left.end",[20,0]],"ids":["right"]})).unwrap(); + h.app.run("sketch.line", json!({"points":["left.end",[18,8]],"ids":["diag"]})).unwrap(); + h.settle(); + let sid = h.app.session.active_sketch.unwrap(); + h.step(&json!({"start":"sketch.fillet"})).unwrap(); + // The shared point ends three lines, so it names no single corner: pick the two lines. + h.step(&json!({"click":{"world":[16,0,0]}})).unwrap(); + h.step(&json!({"click":{"world":[13,3,0]}})).unwrap(); + assert_eq!(dialog_curves(&h), vec!["right".to_string(), "diag".to_string()], "{:?}", h.app.status); + h.step(&json!({"widget":"OK"})).unwrap(); + assert!(h.app.dialog.is_none(), "{:?}", h.app.status); + let sk = h.app.session.model.state().sketch(sid).unwrap().sketch.clone(); + assert_eq!(sk.curves.iter().filter(|c| matches!(c.kind, CurveKind::Arc { .. })).count(), 1, "{:?}", h.app.status); +} + +/// #1824 (review): the Chamfer window takes a corner's two lines the same way. +#[test] +fn chamfer_window_cuts_the_corner_between_two_picked_lines() { + let mut h = crate::scenario::Harness::new(); + h.app.run("sketch.create", json!({"plane":"XY"})).unwrap(); + h.app.run("sketch.line", json!({"points":[[0,0],[10,0]],"ids":["left"]})).unwrap(); + h.app.run("sketch.line", json!({"points":["left.end",[20,0]],"ids":["right"]})).unwrap(); + h.app.run("sketch.line", json!({"points":["left.end",[18,8]],"ids":["diag"]})).unwrap(); + h.settle(); + let sid = h.app.session.active_sketch.unwrap(); + let before = h.app.session.model.state().sketch(sid).unwrap().sketch.curves.len(); + h.step(&json!({"start":"sketch.chamfer.equal_distance"})).unwrap(); + h.step(&json!({"click":{"world":[16,0,0]}})).unwrap(); + h.step(&json!({"click":{"world":[13,3,0]}})).unwrap(); + assert_eq!(dialog_curves(&h), vec!["right".to_string(), "diag".to_string()], "{:?}", h.app.status); + h.step(&json!({"widget":"OK"})).unwrap(); + assert!(h.app.dialog.is_none(), "{:?}", h.app.status); + let sk = h.app.session.model.state().sketch(sid).unwrap().sketch.clone(); + assert_eq!(sk.curves.len(), before + 1, "a chamfer line: {:?}", h.app.status); + assert!(sk.curve_index("left").is_some(), "the third line stays"); +} diff --git a/crates/ui-egui/src/sketch_modify_ui.rs b/crates/ui-egui/src/sketch_modify_ui.rs index 53f5e973..2f6cf12c 100644 --- a/crates/ui-egui/src/sketch_modify_ui.rs +++ b/crates/ui-egui/src/sketch_modify_ui.rs @@ -150,9 +150,10 @@ fn inputs(k: &Mod) -> Vec { // The centre is required: `sketch.pattern.circular` has no default for it. Mod::PatternCirc { .. } => vec![SelInput::new("Entities", CURVES | SKETCH_POINTS, true), SelInput::new("Center", SKETCH_POINTS, false)], // A fillet rounds any number of corners to one radius (`points`). Each chamfer command - // cuts one corner, so its input holds one. - Mod::Corner { chamfer: false, .. } => vec![SelInput::new("Corners", SKETCH_POINTS, true)], - Mod::Corner { .. } => vec![SelInput::new("Corner", SKETCH_POINTS, false)], + // cuts one corner: its point, or its two lines. Two lines name the corner between them + // where more lines meet at its point (#1824), for both. + Mod::Corner { chamfer: false, .. } => vec![SelInput::new("Corners", SKETCH_POINTS | CURVES, true)], + Mod::Corner { .. } => vec![SelInput::new("Corner", SKETCH_POINTS | CURVES, true)], } } @@ -548,6 +549,17 @@ fn length(app: &SolveApp, label: &str, text: &str) -> Result { } Ok(v) } +/// The curve ids picked in input `i` (a corner's two lines, #1824). +fn line_ids(inputs: &[SelInput], i: usize) -> Vec { + items(inputs, i) + .iter() + .filter_map(|x| match x { + Sel::SketchCurve { id } => Some(id.clone()), + _ => None, + }) + .collect() +} + /// Corner point ids within the active sketch, including qualified script selections. fn corner_ids(app: &SolveApp, inputs: &[SelInput], i: usize) -> Vec { items(inputs, i).iter().filter_map(|sel| point_id(&app.session, sel).map(str::to_string)).collect() @@ -734,15 +746,26 @@ pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map { let corners = corner_ids(app, inputs, 0); - if corners.is_empty() { - return Err("select the corners to round first".into()); - } + let lines = line_ids(inputs, 0); // The radius is checked where it is evaluated, against the document's parameters. - ("sketch.fillet", json!({"points": corners, "radius": size.trim()})) + match (corners.is_empty(), lines.as_slice()) { + (true, []) => return Err("select the corners to round first".into()), + (false, []) => ("sketch.fillet", json!({"points": corners, "radius": size.trim()})), + // Two lines name their corner even where other lines end at its point (#1824). + (true, [a, b]) => ("sketch.fillet", json!({"a": a, "b": b, "radius": size.trim()})), + _ => return Err("select corner points, or the two lines of one corner".into()), + } } Mod::Corner { ctype, size, distance2, angle, flip, .. } => { - let point = corner_ids(app, inputs, 0).into_iter().next().ok_or("select the corner to cut first")?; - let mut p = corner_ref(app, &point, *flip && *ctype > 0)?; + let flipped = *flip && *ctype > 0; + let mut p = match (corner_ids(app, inputs, 0).as_slice(), line_ids(inputs, 0).as_slice()) { + ([point], []) => corner_ref(app, point, flipped)?, + // Flip swaps the lines, so `distance` applies to the other leg. + ([], [a, b]) if flipped => json!({"a": b, "b": a}), + ([], [a, b]) => json!({"a": a, "b": b}), + ([], []) => return Err("select the corner to cut first".into()), + _ => return Err("select one corner point, or the two lines of one corner".into()), + }; p["distance"] = json!(corner_size(app, size_label(true, *ctype), size, ValueKind::Length)?); match ctype { 1 => {