Make SolveApp::start honour a command's enabled flag, and disable solid.move inside a sketch (#740)
`SolveApp::start` now honours a command's enabled flag, so shortcuts, the command palette and the control channel can't start a disabled command, and Move is disabled while a sketch is open.
This commit is contained in:
1 parent
62a7f903f2
commit
ff609bc320
8 files changed
+132
-4
No files matched your search
@@ -4,7 +4,7 @@ use serde_json::{Value, json};
|
||||
use solidcraft_doc::{AxisRef, Direction, Extent, FeatureKind, Operation, PointExpr, ProfileSel, expr::Kind, point_expr};
|
||||
use solidcraft_geom::Vec3;
|
||||
|
||||
use super::CommandSpec;
|
||||
use super::{CommandSpec, not_in_sketch};
|
||||
use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec2, vec3};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
@@ -107,7 +107,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("presspull")
|
||||
.key("Q")
|
||||
.params("face: [x,y,z] and distance: expr (positive adds material, negative cuts), or edges: [[x,y,z]…] and distance (a fillet)"),
|
||||
CommandSpec::new("solid.move", "Move/Copy", move_bodies).at("SOLID", "MODIFY").icon("move").key("M").params("bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr; copy?: bool (default false: keeps the original bodies and transforms a new copy instead of moving them in place)"),
|
||||
CommandSpec::new("solid.move", "Move/Copy", move_bodies).at("SOLID", "MODIFY").icon("move").key("M").enabled(not_in_sketch).params("bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr; copy?: bool (default false: keeps the original bodies and transforms a new copy instead of moving them in place)"),
|
||||
];
|
||||
|
||||
/// Where an extrude starts and which way is positive: a point inside the (first) profile or
|
||||
@@ -1595,3 +1595,7 @@ mod stacked_face_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "solid_move_copy_tests.rs"]
|
||||
mod solid_move_copy_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "move_in_sketch_tests.rs"]
|
||||
mod move_in_sketch_tests;
|
||||
@@ -186,6 +186,12 @@ pub fn in_sketch(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.active_sketch.is_some() { Ok(()) } else { Err("no sketch is being edited (Create Sketch or Edit Sketch first)".into()) }
|
||||
}
|
||||
|
||||
/// The mirror of [`in_sketch`], for a command that acts on bodies and has nothing to offer the
|
||||
/// geometry under the cursor while a sketch is open.
|
||||
pub fn not_in_sketch(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.active_sketch.is_none() { Ok(()) } else { Err("a sketch is being edited (this works on bodies: Finish Sketch first)".into()) }
|
||||
}
|
||||
|
||||
/// Every command, in toolbar order.
|
||||
pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
let mut v: Vec<&'static CommandSpec> = Vec::new();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! `solid.move` moves bodies, so it has nothing to offer the geometry under the cursor while a
|
||||
//! sketch is being edited. It is disabled there, with a reason, and comes back once the sketch
|
||||
//! is finished.
|
||||
use super::*;
|
||||
|
||||
fn cube(s: &mut Session) {
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20})).unwrap();
|
||||
}
|
||||
|
||||
fn spec() -> &'static crate::cmd::CommandSpec {
|
||||
crate::find_command("solid.move").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_is_disabled_while_a_sketch_is_being_edited_and_says_why() {
|
||||
let mut s = Session::default();
|
||||
cube(&mut s);
|
||||
assert!(spec().info(&s).enabled, "Move is enabled with no sketch open");
|
||||
|
||||
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
||||
let info = spec().info(&s);
|
||||
assert!(!info.enabled, "Move must be disabled while a sketch is being edited");
|
||||
let why = info.disabled_reason.unwrap_or_default();
|
||||
assert!(why.contains("sketch is being edited"), "the reason must name the sketch: {why}");
|
||||
assert!(why.contains("bodies"), "the reason must say what Move acts on: {why}");
|
||||
assert!(why.contains("Finish Sketch"), "the reason must say the way out: {why}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_move_in_a_sketch_is_refused_and_changes_nothing() {
|
||||
let mut s = Session::default();
|
||||
cube(&mut s);
|
||||
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
||||
let features = s.doc.features.len();
|
||||
|
||||
let e = s.execute("solid.move", &json!({"bodies": ["Body1"], "translate": [30, 0, 0]})).unwrap_err();
|
||||
assert!(matches!(e, EngineError::Disabled(ref id, _) if id == "solid.move"), "{e:?}");
|
||||
|
||||
assert_eq!(s.doc.features.len(), features, "a refused command adds no feature");
|
||||
assert!(s.active_sketch.is_some(), "the sketch is still being edited");
|
||||
let bounds = s.model.state().body("Body1").unwrap().mesh().bounds();
|
||||
assert!(bounds.min.dist(Vec3::ZERO) < 1e-6, "the body did not move: {bounds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_works_again_once_the_sketch_is_finished() {
|
||||
let mut s = Session::default();
|
||||
cube(&mut s);
|
||||
s.execute("sketch.create", &json!({"plane": "XY"})).unwrap();
|
||||
s.execute("sketch.finish", &json!({})).unwrap();
|
||||
assert!(spec().info(&s).enabled, "Move comes back once the sketch is finished");
|
||||
|
||||
s.execute("solid.move", &json!({"bodies": ["Body1"], "translate": [30, 0, 0]})).unwrap();
|
||||
let bounds = s.model.state().body("Body1").unwrap().mesh().bounds();
|
||||
assert!(bounds.min.dist(Vec3::new(30.0, 0.0, 0.0)) < 1e-6, "{bounds:?}");
|
||||
}
|
||||
Reference in new issue
Block a user