feat(ui): sketch tool shortcuts open a sketch on the selected plane (#737)
Outside a sketch, a sketch tool's shortcut opens Create Sketch on the selected plane or planar face (or asks for one), then starts that tool in the new sketch.
This commit is contained in:
1 parent
ff609bc320
commit
1a5e961362
4 files changed
+136
-11
No files matched your search
@@ -294,6 +294,8 @@ pub struct Dialog {
|
||||
pub extra: serde_json::Map<String, Value>,
|
||||
/// What the inputs were when OK was refused: `error` stays only while they still are.
|
||||
pub refused: Option<Refused>,
|
||||
/// Create Sketch opened by a sketch tool's shortcut: that tool starts once the sketch is open.
|
||||
pub then_start: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// The commands a dialog's inputs make (or why they make none), its picks, and the design's
|
||||
@@ -350,6 +352,7 @@ impl Dialog {
|
||||
collapsed: false,
|
||||
extra: serde_json::Map::new(),
|
||||
refused: None,
|
||||
then_start: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2216,12 +2219,16 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
cancel = true;
|
||||
}
|
||||
// A plane picked for a new sketch starts it right away.
|
||||
let mut then_start = None;
|
||||
if matches!(d.kind, Kind::Sketch)
|
||||
&& d.error.is_none()
|
||||
&& let Some(sel) = d.inputs.first().and_then(|i| i.items.first()).cloned()
|
||||
{
|
||||
match start_sketch(app, &sel) {
|
||||
Ok(()) => cancel = true,
|
||||
Ok(()) => {
|
||||
cancel = true;
|
||||
then_start = d.then_start;
|
||||
}
|
||||
Err(error) => d.refuse(app, error),
|
||||
}
|
||||
}
|
||||
@@ -2247,6 +2254,10 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// Editing done: put the timeline marker back where it was.
|
||||
let _ = roll_marker(app, marker);
|
||||
}
|
||||
// The shortcut that asked for this sketch starts its tool in it.
|
||||
if let Some(id) = then_start {
|
||||
app.start(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the dialog without applying it (Esc, Cancel). An edit puts the timeline marker back.
|
||||
|
||||
@@ -77,6 +77,31 @@ pub fn command_for(app: &SolveApp, key: &str) -> Option<&'static str> {
|
||||
commands_for(app, key).into_iter().find(|id| solidcraft_engine::find_command(id).is_some_and(|c| c.info(&app.session).enabled))
|
||||
}
|
||||
|
||||
/// Run a shortcut key: its enabled command; outside a sketch, a sketch tool's key opens Create
|
||||
/// Sketch (on the selected plane or planar face, else on the plane picked next) and the tool
|
||||
/// starts in the new sketch.
|
||||
pub fn press(app: &mut SolveApp, key: &str) {
|
||||
if let Some(id) = command_for(app, key) {
|
||||
app.start(id);
|
||||
} else if let Some(id) = sketch_tool_for(app, key) {
|
||||
app.start("sketch.create");
|
||||
if let Some(d) = app.dialog.as_mut().filter(|d| matches!(d.kind, crate::dialogs::Kind::Sketch)) {
|
||||
d.then_start = Some(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Outside a sketch: the SKETCH-tab command bound to `key` that a new sketch would start. Not
|
||||
/// Normal/Construction (a mode of the drawing in progress) nor the Finish Sketch panel.
|
||||
fn sketch_tool_for(app: &SolveApp, key: &str) -> Option<&'static str> {
|
||||
if app.session.active_sketch.is_some() {
|
||||
return None;
|
||||
}
|
||||
commands_for(app, key).into_iter().find(|id| {
|
||||
solidcraft_engine::find_command(id).is_some_and(|c| c.tab == "SKETCH" && c.panel != "FINISH SKETCH" && c.id != "sketch.construction")
|
||||
})
|
||||
}
|
||||
|
||||
/// Bind `key` to a command (`""` clears it). Refused for reserved keys, and when other commands
|
||||
/// use the key unless `replace` (they lose it).
|
||||
pub fn bind(app: &mut SolveApp, id: &str, key: &str, replace: bool) -> Result<(), String> {
|
||||
@@ -298,3 +323,7 @@ mod tests {
|
||||
assert_eq!(key_name(egui::Key::K, egui::Modifiers { ctrl: true, shift: true, ..Default::default() }), "Ctrl+Shift+K");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_shortcut_tests.rs"]
|
||||
mod sketch_shortcut_tests;
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::scenario::Harness;
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::{FeatureKind, PlaneRef};
|
||||
|
||||
fn tool(h: &Harness) -> Option<&str> {
|
||||
h.app.tool.as_ref().map(|t| t.cmd)
|
||||
}
|
||||
|
||||
fn normal(h: &Harness) -> solidcraft_engine::geom::Vec3 {
|
||||
let active = h.app.session.active_sketch;
|
||||
h.app.session.world_state().sketches.iter().find(|s| Some(s.feature) == active).map(|s| s.plane.normal()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn press(h: &mut Harness, key: &str) {
|
||||
h.step(&json!({"key": key})).unwrap();
|
||||
h.settle();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_keys_outside_a_sketch_sketch_on_the_selected_plane_and_start_their_tool() {
|
||||
for (key, cmd) in
|
||||
[("C", "sketch.circle.center"), ("L", "sketch.line"), ("R", "sketch.rectangle.two_point"), ("D", "sketch.dimension"), ("P", "sketch.project")]
|
||||
{
|
||||
let mut h = Harness::new();
|
||||
h.app.session.selection = vec![Sel::Plane { name: "XZ".into() }];
|
||||
press(&mut h, key);
|
||||
assert!(h.app.session.active_sketch.is_some(), "{key}: no sketch opened");
|
||||
assert_eq!(h.app.session.world_state().sketches.len(), 1, "{key}");
|
||||
assert!(normal(&h).y.abs() > 0.999, "{key}: not on XZ: {:?}", normal(&h));
|
||||
assert_eq!(tool(&h), Some(cmd), "{key}");
|
||||
assert!(h.app.dialog.is_none(), "{key}: dialog left open");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sketch_key_sketches_on_the_selected_planar_face() {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.show_origin = false;
|
||||
h.app.ui.pick_bodies = false;
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"BaseBox"})).unwrap();
|
||||
h.call("ui.view", json!({"view":"top"}));
|
||||
h.call("ui.view", json!({"view":"fit"}));
|
||||
h.settle();
|
||||
h.step(&json!({"click":{"world":[3,4,10]}})).unwrap();
|
||||
assert!(matches!(h.app.session.selection.as_slice(), [Sel::Face { body, .. }] if body == "BaseBox"), "{:?}", h.app.session.selection);
|
||||
press(&mut h, "C");
|
||||
let sketch = h.app.session.active_sketch.expect("sketch opened on the face");
|
||||
let FeatureKind::Sketch { plane: PlaneRef::Face { anchor: Some(anchor), .. }, .. } = &h.app.session.doc.feature(sketch).unwrap().kind else {
|
||||
panic!("the sketch must sit on the picked face");
|
||||
};
|
||||
assert_eq!(anchor.target.body, "BaseBox");
|
||||
assert_eq!(tool(&h), Some("sketch.circle.center"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sketch_key_with_nothing_selected_asks_for_the_plane_then_starts_the_tool() {
|
||||
let mut h = Harness::new();
|
||||
press(&mut h, "C");
|
||||
assert!(matches!(h.app.dialog.as_ref().map(|d| &d.kind), Some(crate::dialogs::Kind::Sketch)));
|
||||
assert!(h.app.session.active_sketch.is_none());
|
||||
assert!(tool(&h).is_none());
|
||||
h.step(&json!({"click":{"plane":"XY"}})).unwrap();
|
||||
h.settle();
|
||||
assert!(normal(&h).z.abs() > 0.999, "{:?}", normal(&h));
|
||||
assert_eq!(tool(&h), Some("sketch.circle.center"));
|
||||
assert!(h.app.dialog.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_drops_the_tool_waiting_for_a_sketch() {
|
||||
let mut h = Harness::new();
|
||||
press(&mut h, "C");
|
||||
press(&mut h, "Escape");
|
||||
assert!(h.app.dialog.is_none());
|
||||
h.step(&json!({"start":"sketch.create"})).unwrap();
|
||||
h.step(&json!({"click":{"plane":"XY"}})).unwrap();
|
||||
h.settle();
|
||||
assert!(h.app.session.active_sketch.is_some());
|
||||
assert!(tool(&h).is_none(), "a cancelled tool came back: {:?}", tool(&h));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_key_outside_a_sketch_opens_nothing_and_keys_in_a_sketch_keep_it() {
|
||||
let mut h = Harness::new();
|
||||
press(&mut h, "X");
|
||||
assert!(h.app.dialog.is_none());
|
||||
assert!(h.app.session.active_sketch.is_none());
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
press(&mut h, "C");
|
||||
assert_eq!(h.app.session.world_state().sketches.len(), 1);
|
||||
assert_eq!(tool(&h), Some("sketch.circle.center"));
|
||||
}
|
||||
@@ -1173,11 +1173,7 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Key::Z => drop(app.run("edit.undo", json!({}))),
|
||||
Key::Y => drop(app.run("edit.redo", json!({}))),
|
||||
Key::S => save(app),
|
||||
_ => {
|
||||
if let Some(id) = crate::keymap::command_for(app, &crate::keymap::key_name(k, mods)) {
|
||||
app.start(id);
|
||||
}
|
||||
}
|
||||
_ => crate::keymap::press(app, &crate::keymap::key_name(k, mods)),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1214,11 +1210,7 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
Key::F6 => app.animate_view("fit"),
|
||||
Key::Delete | Key::Backspace => crate::viewport::delete_selection(app),
|
||||
_ => {
|
||||
if let Some(id) = crate::keymap::command_for(app, &crate::keymap::key_name(k, mods)) {
|
||||
app.start(id);
|
||||
}
|
||||
}
|
||||
_ => crate::keymap::press(app, &crate::keymap::key_name(k, mods)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user