forked from 1b-agent/fieldcraft-upstream
CommandSpec gains `requires: &[Requirement]` (crates/engine/src/requirement.rs): an open
design, geometry, nodes, a mesh, something to analyse, materials and sections for the
elements, sets, the definition an edit or delete acts on, a step, a result. Session::execute
checks them before running, so a command missing one fails at once with no partial work,
naming each missing piece and the toolbar entry that provides it ("Needs a material —
Properties › Material"). Requirements are real prerequisites (the model's own reference
checks and model.check's rules), never a workflow order. engine.commands / list_commands
list them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
77 lines
2.6 KiB
Rust
77 lines
2.6 KiB
Rust
//! Undo and redo ([`crate::history`]).
|
|
|
|
use serde_json::Value;
|
|
|
|
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
|
|
|
/// Most steps one undo or redo takes (the undo history holds at most this many).
|
|
const MAX_COUNT: u64 = crate::history::MAX_UNDO_STEPS as u64;
|
|
|
|
/// `count` steps (default 1), each a whole number in 1..=MAX_COUNT.
|
|
fn count(p: &Value) -> Result<usize> {
|
|
match p.get("count") {
|
|
None | Some(Value::Null) => Ok(1),
|
|
Some(v) => v
|
|
.as_u64()
|
|
.filter(|n| (1..=MAX_COUNT).contains(n))
|
|
.map(|n| n as usize)
|
|
.ok_or_else(|| EngineError::Params(format!("`count` must be a whole number from 1 to {MAX_COUNT}"))),
|
|
}
|
|
}
|
|
|
|
/// Run `step` `count` times; the reply is the last one's (an error when not even one could).
|
|
fn repeat(s: &mut Session, p: &Value, step: fn(&mut Session) -> Result<Value>) -> Result<Value> {
|
|
let n = count(p)?;
|
|
let mut reply = step(s)?;
|
|
for _ in 1..n {
|
|
match step(s) {
|
|
Ok(r) => reply = r,
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
Ok(reply)
|
|
}
|
|
|
|
pub const UNDO: CommandSpec = CommandSpec {
|
|
id: "edit.undo",
|
|
label: "Undo",
|
|
params: r#"{"count"?: 1} — takes back the last command that changed the model (it leaves the journal and can be redone); `count` takes back that many, as far as the history reaches"#,
|
|
form: Some(super::forms::COUNT),
|
|
kind: CommandKind::Session,
|
|
requires: &[],
|
|
run: |s, p| repeat(s, p, Session::undo),
|
|
};
|
|
|
|
pub const REDO: CommandSpec = CommandSpec {
|
|
id: "edit.redo",
|
|
label: "Redo",
|
|
params: r#"{"count"?: 1} — runs the last undone command's change again; `count` redoes that many, as far as there are undone commands"#,
|
|
form: Some(super::forms::COUNT),
|
|
kind: CommandKind::Session,
|
|
requires: &[],
|
|
run: |s, p| repeat(s, p, Session::redo),
|
|
};
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
|
|
#[test]
|
|
fn undo_and_redo_take_a_count_and_stop_at_the_ends() {
|
|
let mut s = Session::default();
|
|
for l in 1..=4 {
|
|
s.execute("mesh.node.add", &json!({"label": l, "xyz": [l, 0, 0]})).unwrap();
|
|
}
|
|
s.execute("edit.undo", &json!({"count": 3})).unwrap();
|
|
assert_eq!(s.journal.len(), 1);
|
|
s.execute("edit.redo", &json!({"count": 100})).unwrap();
|
|
assert_eq!(s.journal.len(), 4, "as far as there are undone commands");
|
|
for bad in [json!({"count": 0}), json!({"count": -1}), json!({"count": 1e9}), json!({"count": "x"}), json!({"count": 2.5})] {
|
|
assert!(s.execute("edit.undo", &bad).is_err(), "{bad}");
|
|
}
|
|
assert_eq!(s.journal.len(), 4);
|
|
}
|
|
}
|