mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 19:36:49 -06:00
* 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>
124 lines
4.6 KiB
Rust
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))]);
|
|
}
|