diff --git a/crates/engine/src/cmd/annotate.rs b/crates/engine/src/cmd/annotate.rs index b00414e2..92e8a009 100644 --- a/crates/engine/src/cmd/annotate.rs +++ b/crates/engine/src/cmd/annotate.rs @@ -8,7 +8,7 @@ use cadcraft_geom::{Segment, Vec2, Vec3}; use serde_json::{Value, json}; use super::helpers::v3; -use super::machines::SelectRun; +use super::machines::{SelOutcome, SelectPhase, SelectRun}; use super::*; use crate::{Accept, EngineError, Input, Interactive, LeaderOrder, MLeaderOptions, Prompt, Result, Session, Step}; @@ -106,7 +106,8 @@ pub fn specs() -> Vec { .interactive(|_| Ok(Box::new(tedit::TEditM::default()))), CommandSpec::new("dimspace", "Dimension Space", run_dimspace) .menu(&["Dimension", "Dimension Space"]) - .params("{base: hex, handles: [hex], spacing?: number (default DIMDLI)}"), + .params("{base: hex, handles: [hex], spacing?: number (default DIMDLI)}") + .interactive(|_| Ok(Box::new(DimSpaceM::default()))), ] .into_iter() .chain(dimedit::specs()) @@ -719,6 +720,90 @@ fn run_dimspace(s: &mut Session, p: &Value) -> Result { Ok(json!({ "spaced": others.len() })) } +/// DIMSPACE at the command line: select the base dimension, select the dimensions to space, then +/// the spacing (Enter or Auto takes the DIMDLI-based default). +#[derive(Default)] +struct DimSpaceM { + base: Option, + sel: SelectPhase, + spacing_phase: bool, +} + +impl DimSpaceM { + fn apply(&self, s: &mut Session, base: Handle, spacing: Option) -> Result { + let mut p = serde_json::Map::new(); + p.insert("base".into(), json!(base.hex())); + p.insert("handles".into(), json!(self.sel.picked.iter().map(|h| h.hex()).collect::>())); + if let Some(v) = spacing { + p.insert("spacing".into(), json!(v)); + } + let r = run_dimspace(s, &Value::Object(p))?; + s.set_selection(Vec::new()); + s.echo(format!("{} dimension(s) spaced.", r.get("spaced").and_then(Value::as_u64).unwrap_or(0))); + Ok(Step::Done) + } +} + +impl Interactive for DimSpaceM { + fn name(&self) -> &'static str { + "DIMSPACE" + } + fn begin(&mut self, _s: &mut Session) -> Result { + self.sel = SelectPhase::single(); + Ok(Step::Continue) + } + fn prompt(&self, _s: &Session) -> Prompt { + if self.spacing_phase { + return Prompt::new("Enter value", Accept::NUMBER).kw(&["Auto"]).default("Auto"); + } + if self.base.is_none() { + return Prompt::new("Select base dimension", Accept::SELECT); + } + Prompt::new("Select dimensions to space", Accept::SELECT) + } + fn input(&mut self, s: &mut Session, i: Input) -> Result { + if self.spacing_phase { + let Some(base) = self.base else { return Ok(Step::Cancel) }; + return match i { + Input::Enter => self.apply(s, base, None), + Input::Keyword(k) | Input::Text(k) if k.trim().eq_ignore_ascii_case("a") || k.trim().eq_ignore_ascii_case("auto") => { + self.apply(s, base, None) + } + Input::Text(t) => match super::machines::number(&t).filter(|v| v.is_finite() && *v >= 0.0) { + Some(v) => self.apply(s, base, Some(v)), + None => { + s.echo("Requires a non-negative number or Auto."); + Ok(Step::Continue) + } + }, + _ => Ok(Step::Continue), + }; + } + match self.sel.feed(s, &i)? { + SelOutcome::More => Ok(Step::Continue), + SelOutcome::Empty => Ok(Step::Done), + SelOutcome::Done(hs) => { + if self.base.is_none() { + match hs.first().copied().filter(|h| !dims_of(s, &[*h]).is_empty()) { + Some(b) => { + self.base = Some(b); + self.sel = SelectPhase::default(); + } + None => { + s.echo("Select a dimension."); + self.sel = SelectPhase::single(); + } + } + } else { + self.sel.picked = hs; + self.spacing_phase = true; + } + Ok(Step::Continue) + } + } + } +} + /// DIMOVERRIDE at the command line: variable, value (repeat), Enter, then select dimensions. struct OverrideM { vars: serde_json::Map, diff --git a/crates/engine/src/cmd/annotate_tests.rs b/crates/engine/src/cmd/annotate_tests.rs index 42481198..299f9159 100644 --- a/crates/engine/src/cmd/annotate_tests.rs +++ b/crates/engine/src/cmd/annotate_tests.rs @@ -2,7 +2,7 @@ use cadcraft_doc::{AssocSnap, DimKind, Dimension, EntityKind, Handle}; use cadcraft_geom::Vec2; use serde_json::{Value, json}; -use crate::Session; +use crate::{Input, Session}; fn h(v: &Value) -> Handle { Handle::parse_hex(v["handle"].as_str().unwrap()).unwrap() @@ -299,6 +299,25 @@ fn dimtedit_and_dimspace() { assert!((dim(&s, b).defpt.y - 1.5).abs() < 1e-9, "{:?}", dim(&s, b).defpt); } +#[test] +fn dimspace_prompts_for_its_inputs() { + let mut s = Session::new(); + let a = h(&s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [10, 0], "at": [0, 5] })).unwrap()); + let b = h(&s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [20, 0], "at": [0, 9] })).unwrap()); + // The menu / typed start used to fail with "`base` dimension is required". + s.start("dimspace").unwrap(); + assert!(s.prompt_text().contains("Select base dimension"), "{}", s.prompt_text()); + s.input(Input::Pick(vec![a])).unwrap(); + assert!(s.prompt_text().contains("Select dimensions to space"), "{}", s.prompt_text()); + s.input(Input::Pick(vec![b])).unwrap(); + s.input(Input::Enter).unwrap(); + assert!(s.prompt_text().contains("Enter value"), "{}", s.prompt_text()); + s.input(Input::Text("2".into())).unwrap(); + assert!(s.running.is_none()); + assert!((dim(&s, a).defpt.y - 5.0).abs() < 1e-9, "{:?}", dim(&s, a).defpt); + assert!((dim(&s, b).defpt.y - 7.0).abs() < 1e-9, "{:?}", dim(&s, b).defpt); +} + #[test] fn text_style_commands() { let mut s = Session::new();