Files
cadcraft/crates/engine/tests/fillet_chamfer.rs
Valentin KleinandClaude Opus 5.5 776d99a1e0 FILLET/CHAMFER: Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes #248) (#251)
* Pick box at object-selection prompts; prompts remember the last option

Part of #34.

The crosshair showed the pick box only at "Select objects" prompts. The
commands that pick one object themselves (FILLET, CHAMFER, OFFSET, TRIM,
EXTEND, BREAK, the dimension and constraint picks...) ask with a plain
point prompt, so the cursor stayed a bare crosshair there and hitting an
object was guesswork. The idle crosshair also showed the box with
PICKFIRST off.

- Engine: Prompt::picks_objects() (a selection prompt, or a point prompt
  that starts with "Select") and Session::picking_objects() (that, or no
  command with PICKFIRST on). The canvas uses it for the pick box and the
  rollover highlight.
- The box is sized from PICKBOX as the pick aperture the click uses
  (PICKBOX x 1.5 px half side), hidden at PICKBOX 0, and drawn with a new
  `crosshair` theme token.
- HATCH/GRADIENT: "Select objects" mode is a real mode now ("Select
  objects or [picK internal point/...]", Enter creates the hatch) and the
  next HATCH starts in the mode used last. Pattern, scale and angle
  sub-prompts show the current value as <default>. At select prompts P, L
  and ALL stay selection modes even when a keyword shares the letter.
- POLYGON offers the last Inscribed/Circumscribed choice, ROTATE the last
  angle, SCALE the last factor (Session::last_used, session only).
- FILLET shows the radius as <default>; CHAMFER asks for the second
  distance too, defaulting to the first (FILLETRAD, CHAMFERA/B).

JSON calls are unchanged: explicit params and documented defaults win.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* FILLET/CHAMFER Trim, Multiple, Undo; CHAMFER Angle, mEthod, Polyline (fixes #248)

Trim asks for the trim mode (TRIMMODE); with No trim the fillet arc or
chamfer line is added and both objects are kept. Multiple repeats until
Enter and Undo takes back the last fillet/chamfer of the command. CHAMFER
Angle asks for a length on the first line and an angle from it (CHAMFERC,
CHAMFERD, CHAMMODE = 1), mEthod picks Distance or Angle, and Polyline
chamfers every straight corner of a 2D polyline. The JSON forms take
`trim`, and CHAMFER `length`/`angle` and `polyline`.

Keyword matching now prefers a whole keyword or its capital-letter
shortcut over a prefix of another keyword, so `M` is Multiple rather than
the start of mEthod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:56:01 -04:00

124 lines
4.6 KiB
Rust

//! FILLET and CHAMFER options at the command line: Trim, Multiple, Undo, and CHAMFER's Angle,
//! mEthod and Polyline.
use cadcraft_engine::Session;
use cadcraft_engine::doc::EntityKind;
use serde_json::json;
type Seg = ((f64, f64), (f64, f64));
fn r(v: f64) -> f64 {
(v * 1e6).round() / 1e6
}
/// Every line with its ends in a fixed order (rounded), sorted.
fn lines(s: &Session) -> Vec<Seg> {
let mut v: Vec<Seg> = s
.doc()
.unwrap()
.model
.iter()
.filter_map(|e| if let EntityKind::Line(l) = &e.kind { Some(((r(l.a.x), r(l.a.y)), (r(l.b.x), r(l.b.y)))) } else { None })
.map(|(a, b)| if (a.0, a.1) <= (b.0, b.1) { (a, b) } else { (b, a) })
.collect();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v
}
fn arcs(s: &Session) -> usize {
s.doc().unwrap().model.iter().filter(|e| matches!(e.kind, EntityKind::Arc(_))).count()
}
/// A horizontal line through a vertical one, crossing at (10,0); with `top`, a second horizontal
/// line crossing the vertical one at (10,10).
fn corner(top: bool) -> Session {
let mut s = Session::new();
let mut ls = vec!["0,0 12,0", "10,-2 10,12"];
if top {
ls.push("0,10 12,10");
}
for l in ls {
s.cmdline(&format!("LINE {l}")).unwrap();
s.cmdline("").unwrap();
}
s
}
#[test]
fn fillet_no_trim_keeps_the_lines() {
let mut s = corner(false);
s.script("FILLET T N R 1 5,0 10,5\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![((0.0, 0.0), (12.0, 0.0)), ((10.0, -2.0), (10.0, 12.0))]);
assert_eq!(arcs(&s), 1);
// The mode is remembered (TRIMMODE) and Trim switches it back.
assert_eq!(s.doc().unwrap().header.i64("TRIMMODE", 1), 0);
s.script("FILLET T T R 0 5,0 10,5\n").unwrap();
assert_eq!(lines(&s), vec![((0.0, 0.0), (10.0, 0.0)), ((10.0, 0.0), (10.0, 12.0))]);
}
#[test]
fn fillet_multiple_and_undo() {
let mut s = corner(true);
// Two corners in one command; Undo takes the second back, Enter ends.
s.script("FILLET M 5,0 10,5 5,10 10,5 U\n").unwrap();
assert!(s.running.is_some(), "Multiple keeps asking");
s.cmdline("").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![((0.0, 0.0), (10.0, 0.0)), ((0.0, 10.0), (12.0, 10.0)), ((10.0, 0.0), (10.0, 12.0))]);
}
#[test]
fn chamfer_angle_and_method() {
let mut s = corner(false);
// Length 2 on the first line at 45 degrees from it.
s.script("CHAMFER A 2 45 5,0 10,5\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![((0.0, 0.0), (8.0, 0.0)), ((8.0, 0.0), (10.0, 2.0)), ((10.0, 2.0), (10.0, 12.0))]);
assert_eq!(s.doc().unwrap().header.i64("CHAMMODE", 0), 1);
// mEthod switches back to the two distances.
let mut s = corner(false);
s.script("CHAMFER A 2 45 E D D 1 3 5,0 10,5\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![((0.0, 0.0), (9.0, 0.0)), ((9.0, 0.0), (10.0, 3.0)), ((10.0, 3.0), (10.0, 12.0))]);
}
#[test]
fn chamfer_no_trim_and_multiple() {
let mut s = corner(true);
s.script("CHAMFER T N D 1 1 M 5,0 10,5 5,10 10,5\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(
lines(&s),
vec![
((0.0, 0.0), (12.0, 0.0)),
((0.0, 10.0), (12.0, 10.0)),
((9.0, 0.0), (10.0, 1.0)),
((9.0, 10.0), (10.0, 9.0)),
((10.0, -2.0), (10.0, 12.0))
]
);
}
#[test]
fn chamfer_polyline() {
let mut s = Session::new();
s.script("PLINE 0,0 10,0 10,10 0,10 C\n").unwrap();
s.script("CHAMFER D 1 2 P 5,0\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let d = s.doc().unwrap();
let pl = d.model.iter().find_map(|e| if let EntityKind::LwPolyline(p) = &e.kind { Some(p.clone()) } else { None }).unwrap();
let pts: Vec<(f64, f64)> = pl.vertices.iter().map(|v| (r(v.p.x), r(v.p.y))).collect();
assert_eq!(pts.len(), 8, "{pts:?}");
// The corner at (10,0): 1 back along the incoming edge, 2 along the outgoing one.
assert!(pts.contains(&(9.0, 0.0)) && pts.contains(&(10.0, 2.0)), "{pts:?}");
}
#[test]
fn json_chamfer_angle_and_trim() {
let mut s = corner(false);
let hs: Vec<String> = s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect();
s.execute("chamfer", &json!({ "h1": hs[0], "p1": [5, 0], "h2": hs[1], "p2": [10, 5], "length": 2, "angle": 45, "trim": false })).unwrap();
assert_eq!(lines(&s), vec![((0.0, 0.0), (12.0, 0.0)), ((8.0, 0.0), (10.0, 2.0)), ((10.0, -2.0), (10.0, 12.0))]);
}