Files
cadcraft/crates/engine/tests/offset_move.rs
T
skylar taylor-barrick 4ab8c384b3 #618: test that OFFSET takes a clicked distance and picks its source under grid snap
OFFSET only accepted a typed distance, and with grid snap on a click meant to pick the source object was moved to the nearest grid point. The engine test clicks two points for the distance and checks the guide, the distance and the resulting offset; the canvas test does the same through grid-snapped clicks and then picks the source line. Without the fix the engine test stays at the distance prompt and the canvas test never picks the line.
2026-10-22 16:34:32 -06:00

123 lines
5.1 KiB
Rust

//! OFFSET Erase/Layer/Multiple/Undo and the MOVE/COPY Displacement option at the command line.
use cadcraft_engine::Input;
use cadcraft_engine::Session;
use cadcraft_engine::doc::EntityKind;
use cadcraft_engine::geom::Vec2;
use serde_json::json;
/// Every line as ((ax, ay), (bx, by), layer), sorted by the first y.
fn lines(s: &Session) -> Vec<((f64, f64), (f64, f64), String)> {
let mut v: Vec<_> = s
.doc()
.unwrap()
.model
.iter()
.filter_map(|e| if let EntityKind::Line(l) = &e.kind { Some(((l.a.x, l.a.y), (l.b.x, l.b.y), e.common.layer.clone())) } else { None })
.collect();
v.sort_by(|a, b| a.0.1.total_cmp(&b.0.1).then(a.0.0.total_cmp(&b.0.0)));
v
}
fn one_line() -> Session {
let mut s = Session::new();
s.cmdline("LINE 0,0 10,0").unwrap();
s.cmdline("").unwrap();
s
}
fn l(a: (f64, f64), b: (f64, f64), layer: &str) -> ((f64, f64), (f64, f64), String) {
(a, b, layer.to_string())
}
#[test]
fn offset_erase_source() {
let mut s = one_line();
s.script("OFFSET E Y 2 5,0 5,1\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((0.0, 2.0), (10.0, 2.0), "0")]);
// The choice is remembered: the next OFFSET erases the source too, until No.
s.script("OFFSET 2 5,2 5,3\n\n").unwrap();
assert_eq!(lines(&s), vec![l((0.0, 4.0), (10.0, 4.0), "0")]);
s.script("OFFSET E N 2 5,4 5,5\n\n").unwrap();
assert_eq!(lines(&s).len(), 2);
}
#[test]
fn offset_layer_current() {
let mut s = one_line();
s.execute("layer.new", &json!({ "name": "Other", "current": true })).unwrap();
s.script("OFFSET L C 2 5,0 5,1\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.0), (10.0, 2.0), "Other")]);
// Source (the default) keeps the source object's layer.
s.script("OFFSET L S 2 5,0 5,-1\n\n").unwrap();
assert_eq!(lines(&s).first(), Some(&l((0.0, -2.0), (10.0, -2.0), "0")));
}
#[test]
fn offset_multiple_and_undo() {
let mut s = one_line();
// Multiple: each side point offsets the newest copy again; Undo takes the last one back.
s.script("OFFSET 2 5,0 M 5,1 5,3 5,5 U\n\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.0), (10.0, 2.0), "0"), l((0.0, 4.0), (10.0, 4.0), "0")]);
}
// Regression test for #618: OFFSET's distance prompt took only a typed number; clicking two points
// (the distance between them) was rejected, and the second click had no distance guide.
#[test]
fn offset_distance_from_two_clicked_points() {
let mut s = one_line();
s.start("offset").unwrap();
s.input(Input::Point(Vec2::new(20.0, 20.0))).unwrap();
let p = s.current_prompt().unwrap();
assert!(p.message.contains("second point"), "{}", s.prompt_text());
assert!(p.base.is_some_and(|b| b.near(Vec2::new(20.0, 20.0), 1e-9)), "the second click is measured from the first");
// The guide from the first click to the cursor is the previewed distance.
let guide = s.preview(Vec2::new(20.0, 22.5));
assert!(matches!(guide.first().map(|e| &e.kind), Some(EntityKind::Line(g)) if g.a.xy().near(Vec2::new(20.0, 20.0), 1e-9)), "{guide:?}");
s.input(Input::Point(Vec2::new(20.0, 22.5))).unwrap();
assert!(s.current_prompt().unwrap().message.contains("Select object"), "{}", s.prompt_text());
assert_eq!(s.doc().unwrap().header.f64("OFFSETDIST", -1.0), 2.5);
s.script("5,0 5,1\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 2.5), (10.0, 2.5), "0")]);
}
#[test]
fn offset_json_erase_and_layer() {
let mut s = one_line();
s.execute("layer.new", &json!({ "name": "Other", "current": true })).unwrap();
let h = s.doc().unwrap().model.iter().next().unwrap().handle.hex();
s.execute("offset", &json!({ "handle": h, "distance": 2, "side": [5, 1], "erase": true, "layer": "current" })).unwrap();
assert_eq!(lines(&s), vec![l((0.0, 2.0), (10.0, 2.0), "Other")]);
}
#[test]
fn move_displacement() {
// The Displacement keyword asks for the displacement directly.
let mut s = one_line();
s.script("MOVE 5,0 D 3,4\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((3.0, 4.0), (13.0, 4.0), "0")]);
// Enter at the base point prompt does the same.
let mut s = one_line();
s.script("MOVE 5,0\n\n\n3,4\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((3.0, 4.0), (13.0, 4.0), "0")]);
// Base and second point still work.
let mut s = one_line();
s.script("MOVE 5,0 1,1 2,3\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((1.0, 2.0), (11.0, 2.0), "0")]);
}
#[test]
fn copy_displacement() {
let mut s = one_line();
s.script("COPY 5,0 D 0,3\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(lines(&s), vec![l((0.0, 0.0), (10.0, 0.0), "0"), l((0.0, 3.0), (10.0, 3.0), "0")]);
}