mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 11:26:46 -06:00
Squash of PR #464 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
216 lines
8.3 KiB
Rust
216 lines
8.3 KiB
Rust
//! UNDO Mark/Back, BEgin/End groups, Auto and Control (All/None/One/Combine/Layer), UNDOCTL and
|
|
//! UNDOMARKS, REDO and MREDO counts: JSON form, command line and scripts.
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::cmd::file::{IoHooks, set_io};
|
|
use serde_json::{Value, json};
|
|
|
|
fn circles(s: &Session) -> usize {
|
|
s.doc().unwrap().model.len()
|
|
}
|
|
|
|
fn circle(s: &mut Session, x: f64) {
|
|
s.execute("circle", &json!({ "center": [x, 0], "radius": 1 })).unwrap();
|
|
}
|
|
|
|
fn getvar(s: &mut Session, name: &str) -> Value {
|
|
s.execute("getvar", &json!({ "name": name })).unwrap()["value"].clone()
|
|
}
|
|
|
|
#[test]
|
|
fn mark_and_back_undo_to_the_mark() {
|
|
let mut s = Session::new();
|
|
let base = circles(&s);
|
|
circle(&mut s, 0.0);
|
|
s.execute("undo", &json!({ "mode": "mark" })).unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOMARKS"), json!(1));
|
|
circle(&mut s, 2.0);
|
|
circle(&mut s, 4.0);
|
|
let r = s.execute("undo", &json!({ "mode": "back" })).unwrap();
|
|
assert_eq!(r["undone"].as_array().unwrap().len(), 2);
|
|
assert_eq!(circles(&s), base + 1, "back to the mark");
|
|
assert_eq!(getvar(&mut s, "UNDOMARKS"), json!(0), "the mark goes with Back");
|
|
// No mark left: Back asks first; refusing keeps the drawing.
|
|
s.execute("undo", &json!({ "mode": "back", "confirm": false })).unwrap();
|
|
assert_eq!(circles(&s), base + 1);
|
|
s.execute("undo", &json!({ "mode": "back" })).unwrap();
|
|
assert_eq!(circles(&s), base, "no mark: everything undone");
|
|
}
|
|
|
|
#[test]
|
|
fn back_at_the_command_line_and_marks_undone_away() {
|
|
let mut s = Session::new();
|
|
let base = circles(&s);
|
|
s.cmdline("undo m").unwrap();
|
|
circle(&mut s, 0.0);
|
|
s.cmdline("undo m").unwrap();
|
|
circle(&mut s, 2.0);
|
|
// Undoing past the second mark (then drawing again) drops it; the first stays.
|
|
s.execute("u", &json!({})).unwrap();
|
|
s.execute("u", &json!({})).unwrap();
|
|
circle(&mut s, 3.0);
|
|
assert_eq!(getvar(&mut s, "UNDOMARKS"), json!(1));
|
|
circle(&mut s, 5.0);
|
|
s.cmdline("undo b").unwrap();
|
|
assert_eq!(circles(&s), base, "back to the first mark");
|
|
// Without a mark the command line asks; No keeps everything.
|
|
circle(&mut s, 0.0);
|
|
s.cmdline("undo b").unwrap();
|
|
assert!(s.running.is_some(), "asks before undoing everything");
|
|
s.cmdline("n").unwrap();
|
|
assert_eq!(circles(&s), base + 1);
|
|
s.cmdline("undo b y").unwrap();
|
|
assert_eq!(circles(&s), base);
|
|
}
|
|
|
|
#[test]
|
|
fn begin_end_group_is_one_undo_step() {
|
|
let mut s = Session::new();
|
|
let base = circles(&s);
|
|
let depth = s.state().unwrap().undo.len();
|
|
s.execute("undo", &json!({ "mode": "begin" })).unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 8, 8, "group open");
|
|
for x in 0..3 {
|
|
circle(&mut s, f64::from(x) * 2.0);
|
|
}
|
|
s.execute("undo", &json!({ "mode": "end" })).unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 8, 0);
|
|
assert_eq!(s.state().unwrap().undo.len(), depth + 1, "one step");
|
|
let r = s.execute("u", &json!({})).unwrap();
|
|
assert_eq!(r["undone"], json!(["Group"]));
|
|
assert_eq!(circles(&s), base);
|
|
s.execute("redo", &json!({})).unwrap();
|
|
assert_eq!(circles(&s), base + 3);
|
|
// End without BEgin does nothing.
|
|
s.execute("undo", &json!({ "mode": "end" })).unwrap();
|
|
assert_eq!(circles(&s), base + 3);
|
|
}
|
|
|
|
#[test]
|
|
fn group_from_a_script_and_nested_begin() {
|
|
let mut s = Session::new();
|
|
let base = circles(&s);
|
|
let depth = s.state().unwrap().undo.len();
|
|
s.script("UNDO BE\nCIRCLE 0,0 1\nUNDO BE\nCIRCLE 3,0 1\nCIRCLE 6,0 1\nUNDO E\n").unwrap();
|
|
// BEgin inside a group ends that group and starts a new one.
|
|
assert_eq!(s.state().unwrap().undo.len(), depth + 2);
|
|
s.cmdline("undo 1").unwrap();
|
|
assert_eq!(circles(&s), base + 1);
|
|
// UNDO inside an open group undoes the whole group so far.
|
|
s.cmdline("undo be").unwrap();
|
|
circle(&mut s, 9.0);
|
|
circle(&mut s, 12.0);
|
|
s.cmdline("u").unwrap();
|
|
assert_eq!(circles(&s), base + 1);
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 8, 0, "the group was ended");
|
|
}
|
|
|
|
#[test]
|
|
fn saving_closes_an_open_group() {
|
|
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Ok(b"0\nEOF\n".to_vec()), plot: None });
|
|
let mut s = Session::new();
|
|
let depth = s.state().unwrap().undo.len();
|
|
s.execute("undo", &json!({ "mode": "begin" })).unwrap();
|
|
circle(&mut s, 0.0);
|
|
circle(&mut s, 2.0);
|
|
let path = std::env::temp_dir().join(format!("cadcraft-undo-group-{}.dxf", std::process::id()));
|
|
s.execute("saveas", &json!({ "path": path.to_string_lossy() })).unwrap();
|
|
let _ = std::fs::remove_file(&path);
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 8, 0, "saving ends the group");
|
|
assert_eq!(s.state().unwrap().undo.len(), depth + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn control_none_and_one() {
|
|
let mut s = Session::new();
|
|
circle(&mut s, 0.0);
|
|
s.execute("undo", &json!({ "mode": "control", "control": "none" })).unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 1, 0);
|
|
assert!(s.state().unwrap().undo.is_empty(), "None discards undo information");
|
|
circle(&mut s, 2.0);
|
|
assert!(s.state().unwrap().undo.is_empty(), "nothing recorded");
|
|
assert!(s.execute("u", &json!({})).is_err(), "U says undo is off");
|
|
assert!(s.execute("undo", &json!({ "mode": "mark" })).is_err());
|
|
// With undo off the command line asks for the control option straight away.
|
|
s.cmdline("undo one").unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 3, 3);
|
|
let before = circles(&s);
|
|
circle(&mut s, 4.0);
|
|
circle(&mut s, 6.0);
|
|
assert_eq!(s.state().unwrap().undo.len(), 1, "One keeps a single step");
|
|
s.cmdline("undo 5").unwrap();
|
|
assert_eq!(circles(&s), before + 1);
|
|
s.cmdline("undo c a").unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL").as_i64().unwrap() & 3, 1);
|
|
circle(&mut s, 8.0);
|
|
circle(&mut s, 10.0);
|
|
assert_eq!(s.state().unwrap().undo.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_combine_and_layer_set_undoctl() {
|
|
let mut s = Session::new();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL"), json!(53));
|
|
s.cmdline("undo a off").unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL"), json!(49));
|
|
s.execute("undo", &json!({ "mode": "auto", "on": true })).unwrap();
|
|
s.cmdline("undo c c n").unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL"), json!(37));
|
|
s.execute("undo", &json!({ "mode": "control", "control": "layer", "on": false })).unwrap();
|
|
assert_eq!(getvar(&mut s, "UNDOCTL"), json!(5));
|
|
// Read-only.
|
|
assert!(s.execute("setvar", &json!({ "name": "UNDOCTL", "value": 1 })).is_err());
|
|
assert!(s.execute("setvar", &json!({ "name": "UNDOMARKS", "value": 1 })).is_err());
|
|
assert!(s.execute("undo", &json!({ "mode": "sideways" })).is_err());
|
|
assert!(s.execute("undo", &json!({ "mode": "auto" })).is_err(), "auto needs on");
|
|
}
|
|
|
|
#[test]
|
|
fn redo_and_mredo_counts() {
|
|
let mut s = Session::new();
|
|
let base = circles(&s);
|
|
for x in 0..4 {
|
|
circle(&mut s, f64::from(x) * 2.0);
|
|
}
|
|
s.execute("undo", &json!({ "count": 4 })).unwrap();
|
|
assert_eq!(circles(&s), base);
|
|
s.execute("redo", &json!({ "count": 2 })).unwrap();
|
|
assert_eq!(circles(&s), base + 2);
|
|
s.execute("mredo", &json!({ "all": true })).unwrap();
|
|
assert_eq!(circles(&s), base + 4);
|
|
s.cmdline("undo 3").unwrap();
|
|
s.cmdline("mredo 2").unwrap();
|
|
assert_eq!(circles(&s), base + 3);
|
|
s.cmdline("undo 3").unwrap();
|
|
s.cmdline("mredo l").unwrap();
|
|
assert_eq!(circles(&s), base + 1);
|
|
s.cmdline("mredo a").unwrap();
|
|
assert_eq!(circles(&s), base + 4);
|
|
assert!(s.cmdline("mredo -1").is_ok(), "a bad count re-prompts");
|
|
s.cancel();
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_undo_params_never_panic() {
|
|
let mut s = Session::new();
|
|
circle(&mut s, 0.0);
|
|
for p in [
|
|
json!({ "count": -1 }),
|
|
json!({ "count": u64::MAX }),
|
|
json!({ "mode": 7 }),
|
|
json!({ "mode": "control", "control": 3 }),
|
|
json!({ "mode": "back", "confirm": "maybe" }),
|
|
json!({ "mode": "mark" }),
|
|
] {
|
|
let _ = s.execute("undo", &p);
|
|
}
|
|
for p in [json!({ "count": u64::MAX }), json!({ "all": "yes" }), json!(null)] {
|
|
let _ = s.execute("mredo", &p);
|
|
}
|
|
// Many marks stay bounded.
|
|
for _ in 0..1100 {
|
|
s.execute("undo", &json!({ "mode": "mark" })).unwrap();
|
|
}
|
|
assert!(getvar(&mut s, "UNDOMARKS").as_u64().unwrap() <= 1000);
|
|
}
|