Files
cadcraft/crates/engine/tests/mocoro.rs

236 lines
9.9 KiB
Rust

//! MOCORO (Express Tools Move/Copy/Rotate): select objects and a base point, then Move, Copy,
//! Rotate, Scale, Base and Undo at one prompt until eXit.
use cadcraft_engine::doc::{EntityKind, Handle};
use cadcraft_engine::geom::Vec2;
use cadcraft_engine::{Input, Session};
use serde_json::{Value, json};
fn line(s: &mut Session, a: [f64; 2], b: [f64; 2]) -> String {
let r = s.execute("line", &json!({"points": [a, b]})).unwrap();
r["handle"].as_str().or_else(|| r["handles"][0].as_str()).unwrap().to_string()
}
/// The end points of a line.
fn ends(s: &Session, h: &str) -> ((f64, f64), (f64, f64)) {
let e = s.doc().unwrap().entity(Handle::parse_hex(h).unwrap()).unwrap().kind.clone();
match e {
EntityKind::Line(l) => ((l.a.x, l.a.y), (l.b.x, l.b.y)),
k => panic!("not a line: {k:?}"),
}
}
/// Every line in model space, by its end points.
fn lines(s: &Session) -> Vec<((f64, f64), (f64, f64))> {
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))) } else { None })
.collect()
}
fn near(a: ((f64, f64), (f64, f64)), b: ((f64, f64), (f64, f64))) -> bool {
let c = |x: f64, y: f64| (x - y).abs() < 1e-9;
c(a.0.0, b.0.0) && c(a.0.1, b.0.1) && c(a.1.0, b.1.0) && c(a.1.1, b.1.1)
}
fn undo_len(s: &Session) -> usize {
s.state().unwrap().undo.len()
}
#[test]
fn move_carries_the_base_point_and_rotate_turns_about_it() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let undo = undo_len(&s);
s.script("MOCORO ALL 0,0 M 10,0\n").unwrap();
assert!(near(ends(&s, &l), ((10.0, 0.0), (20.0, 0.0))), "{:?} {:?}", ends(&s, &l), s.log);
assert!(s.prompt_text().contains("Move"), "back at the option prompt: {}", s.prompt_text());
// The base point moved with the objects: the turn is about 10,0.
s.script("R 90\n").unwrap();
assert!(near(ends(&s, &l), ((10.0, 0.0), (10.0, 10.0))), "{:?}", ends(&s, &l));
s.script("X\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(undo_len(&s), undo + 1, "the whole command is one undo step");
s.undo().unwrap();
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))));
}
#[test]
fn copy_leaves_the_originals_and_later_options_act_on_them() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.script("MOCORO ALL 0,0 C 0,5\n").unwrap();
assert_eq!(lines(&s).len(), 2, "{:?}", s.log);
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))), "the original stays");
// The next option moves the original from the same base point, not the copy.
s.script("M 100,0 X\n").unwrap();
assert!(near(ends(&s, &l), ((100.0, 0.0), (110.0, 0.0))), "{:?}", ends(&s, &l));
assert!(lines(&s).iter().any(|x| near(*x, ((0.0, 5.0), (10.0, 5.0)))), "{:?}", lines(&s));
assert!(s.running.is_none());
}
#[test]
fn scale_by_factor_or_point_and_refuses_a_zero_factor() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.script("MOCORO ALL 0,0 S 2\n").unwrap();
assert!(near(ends(&s, &l), ((0.0, 0.0), (20.0, 0.0))), "{:?} {:?}", ends(&s, &l), s.log);
// A factor given by a point is its distance from the base point.
s.script("S 3,4\n").unwrap();
assert!(near(ends(&s, &l), ((0.0, 0.0), (100.0, 0.0))), "{:?}", ends(&s, &l));
// Zero is refused and the factor is asked again; Enter goes back to the option prompt.
s.script("S 0\n").unwrap();
assert!(s.prompt_text().contains("scale factor"), "{}", s.prompt_text());
s.cmdline("").unwrap();
assert!(s.prompt_text().contains("Rotate"), "{}", s.prompt_text());
assert!(near(ends(&s, &l), ((0.0, 0.0), (100.0, 0.0))));
s.cmdline("").unwrap();
assert!(s.running.is_none(), "Enter at the option prompt is eXit");
}
#[test]
fn rotate_follows_angdir() {
let mut s = Session::new();
s.execute("setvar", &json!({"name": "ANGDIR", "value": 1})).unwrap();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.script("MOCORO ALL 0,0 R 90 X\n").unwrap();
assert!(near(ends(&s, &l), ((0.0, 0.0), (0.0, -10.0))), "clockwise: {:?}", ends(&s, &l));
}
#[test]
fn undo_steps_back_one_option_and_restores_the_base_point() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.script("MOCORO ALL 0,0 M 10,0 C 0,5 R 90\n").unwrap();
assert_eq!(lines(&s).len(), 2);
s.script("U\n").unwrap();
assert!(near(ends(&s, &l), ((10.0, 0.0), (20.0, 0.0))), "{:?}", ends(&s, &l));
s.script("U\n").unwrap();
assert_eq!(lines(&s).len(), 1, "the copy is undone");
s.script("U\n").unwrap();
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))));
s.script("U\n").unwrap();
assert!(s.log.iter().any(|m| m.contains("Nothing to undo")), "{:?}", s.log);
// The base point is back at 0,0 too.
s.script("R 90 X\n").unwrap();
assert!(near(ends(&s, &l), ((0.0, 0.0), (0.0, 10.0))), "{:?}", ends(&s, &l));
}
#[test]
fn escape_keeps_what_was_done_as_one_undo_step() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let undo = undo_len(&s);
s.script("MOCORO ALL 0,0 M 10,0 C 0,5 S\n").unwrap();
assert!(s.prompt_text().contains("scale factor"), "{}", s.prompt_text());
s.cancel();
assert!(s.running.is_none());
assert_eq!(lines(&s).len(), 2);
assert_eq!(undo_len(&s), undo + 1);
s.undo().unwrap();
assert_eq!(lines(&s).len(), 1);
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))));
}
#[test]
fn a_point_at_the_option_prompt_is_not_an_option() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let undo = undo_len(&s);
s.script("MOCORO ALL 0,0\n").unwrap();
s.input(Input::Point(Vec2::new(5.0, 5.0))).unwrap();
assert!(s.log.iter().any(|m| m.contains("Invalid option keyword")), "{:?}", s.log);
assert!(s.running.is_some());
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))));
s.cmdline("").unwrap();
assert!(s.running.is_none());
assert_eq!(undo_len(&s), undo, "nothing done, no undo step");
}
#[test]
fn objects_on_locked_layers_stay_put_but_can_be_copied() {
let mut s = Session::new();
let free = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.execute("layer.new", &json!({"name": "L"})).unwrap();
s.execute("layer.current", &json!({"name": "L"})).unwrap();
let held = line(&mut s, [0.0, 5.0], [10.0, 5.0]);
s.execute("layer.current", &json!({"name": "0"})).unwrap();
s.execute("layer.set", &json!({"name": "L", "locked": true})).unwrap();
s.script("MOCORO ALL 0,0 M 1,0\n").unwrap();
assert!(near(ends(&s, &free), ((1.0, 0.0), (11.0, 0.0))));
assert!(near(ends(&s, &held), ((0.0, 5.0), (10.0, 5.0))));
assert!(s.log.iter().any(|m| m.contains("1 object was on a locked layer")), "{:?}", s.log);
s.script("X\n").unwrap();
// Everything locked: nothing moves and the command leaves no undo step.
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.execute("layer.set", &json!({"name": "0", "locked": true})).unwrap();
let undo = undo_len(&s);
s.script("MOCORO ALL 0,0 M 1,0 R 90 X\n").unwrap();
assert!(s.running.is_none());
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))));
assert_eq!(undo_len(&s), undo, "{:?}", s.log);
}
#[test]
fn json_form_runs_the_options_in_order() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let r = s
.execute(
"mocoro",
&json!({"handles": [l], "base": [0, 0], "ops": [{"copy": [0, 5]}, {"move": [10, 0]}, {"rotate": 90}, {"base": [0, 0]}, {"scale": 2}]}),
)
.unwrap();
assert_eq!(r["handles"].as_array().map(Vec::len), Some(1), "{r}");
// Moved to 10..20, turned about 10,0 to 10,0..10,10, scaled about 0,0.
assert!(near(ends(&s, &l), ((20.0, 0.0), (20.0, 20.0))), "{:?}", ends(&s, &l));
assert!(lines(&s).iter().any(|x| near(*x, ((0.0, 5.0), (10.0, 5.0)))));
}
#[test]
fn json_form_is_checked_before_the_drawing_changes() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
let bad: [Value; 7] = [
json!({"handles": [l], "ops": [{"move": [1, 0]}]}),
json!({"handles": [l], "base": [0, 0]}),
json!({"handles": [l], "base": [0, 0], "ops": [{"move": [1, 0]}, {"scale": 0}]}),
json!({"handles": [l], "base": [0, 0], "ops": [{"move": [1, 0]}, {"rotate": "x"}]}),
json!({"handles": [l], "base": [0, 0], "ops": [{"move": [1, 0]}, {"spin": 3}]}),
json!({"handles": [l], "base": [0, 0], "ops": [{"move": [1, 0], "copy": [0, 1]}]}),
json!({"handles": [l], "base": [0, 0], "ops": [{"move": [1, 0]}, {"scale": 1e-300}]}),
];
for p in bad {
assert!(s.execute("mocoro", &p).is_err(), "{p}");
assert!(near(ends(&s, &l), ((0.0, 0.0), (10.0, 0.0))), "{p} moved the line");
assert_eq!(lines(&s).len(), 1);
}
}
#[test]
fn undo_after_base_undoes_only_the_new_base_point() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.script("MOCORO ALL 0,0 M 10,0 B 50,50 U\n").unwrap();
assert!(near(ends(&s, &l), ((10.0, 0.0), (20.0, 0.0))), "the move stays: {:?}", ends(&s, &l));
// The base point is back where the move left it: the turn is about 10,0.
s.script("R 90 X\n").unwrap();
assert!(near(ends(&s, &l), ((10.0, 0.0), (10.0, 10.0))), "{:?}", ends(&s, &l));
}
#[test]
fn options_on_objects_that_are_gone_record_nothing() {
let mut s = Session::new();
let l = line(&mut s, [0.0, 0.0], [10.0, 0.0]);
s.execute("erase", &json!({"handles": [l]})).unwrap();
let undo = undo_len(&s);
let r = s.execute("mocoro", &json!({"handles": [l], "base": [0, 0], "ops": [{"move": [1, 0]}, {"copy": [0, 1]}, {"rotate": 90}]})).unwrap();
assert_eq!(r["handles"].as_array().map(Vec::len), Some(0), "{r}");
assert!(lines(&s).is_empty());
assert_eq!(undo_len(&s), undo, "no empty undo step");
}