From 409968ecbb4d052225a4fc85948de2c15b6862f2 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Sun, 11 Oct 2026 00:09:10 +0200 Subject: [PATCH] DIMTEDIT: command-line prompts (fixes #382) (#383) Typed at the command line or run from a script, DIMTEDIT ran the JSON form at once and failed with "select dimensions". It now asks "Select dimension", then "Specify new location for dimension text or [Left/Right/Center/Home/Angle]": a point moves the text there (the text follows the cursor in the preview), Left/Right/Center justify it, Home restores the default position and Angle asks for the text angle. A single preselected dimension is taken at once. Each answer runs the existing JSON form on the picked dimension. DIMTED is its alias. Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/annotate.rs | 7 +- crates/engine/src/cmd/annotate_tedit.rs | 100 ++++++++++++++++++++++++ crates/engine/tests/dimtedit_prompts.rs | 49 ++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 crates/engine/src/cmd/annotate_tedit.rs create mode 100644 crates/engine/tests/dimtedit_prompts.rs diff --git a/crates/engine/src/cmd/annotate.rs b/crates/engine/src/cmd/annotate.rs index f69ed7ffd..683c2e7e0 100644 --- a/crates/engine/src/cmd/annotate.rs +++ b/crates/engine/src/cmd/annotate.rs @@ -99,7 +99,9 @@ pub fn specs() -> Vec { .params("{handles?}"), CommandSpec::new("dimtedit.right", "Right", |s, p| run_tedit(s, p, "right")).menu(&["Dimension", "Align Text", "Right"]).params("{handles?}"), CommandSpec::new("dimtedit", "Dimension Text Edit", run_dimtedit) - .params("{handles?, at?: [x,y], mode?: home|angle|left|center|right, angle?}"), + .alias(&["dimted"]) + .params("{handles?, at?: [x,y], mode?: home|angle|left|center|right, angle?}") + .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)}"), @@ -1684,6 +1686,9 @@ impl Interactive for MLeaderM { } } +#[path = "annotate_tedit.rs"] +mod tedit; + #[cfg(test)] #[path = "annotate_tests.rs"] mod tests; diff --git a/crates/engine/src/cmd/annotate_tedit.rs b/crates/engine/src/cmd/annotate_tedit.rs new file mode 100644 index 000000000..8d3c02fc2 --- /dev/null +++ b/crates/engine/src/cmd/annotate_tedit.rs @@ -0,0 +1,100 @@ +//! DIMTEDIT at the command line: "Select dimension", then a new text location or +//! Left/Right/Center/Home/Angle. Each answer runs the JSON form on the picked dimension. + +use cadcraft_doc::{EntityKind, Handle}; +use cadcraft_geom::Vec2; +use serde_json::{Value, json}; + +use super::{dims_of, run_dimtedit, v3}; +use crate::{Accept, Input, Interactive, Prompt, Result, Session, Step}; + +#[derive(Default)] +pub(super) struct TEditM { + dim: Option, + asking_angle: bool, +} + +impl TEditM { + fn run(&self, s: &mut Session, mut p: Value) -> Result { + let Some(h) = self.dim else { return Ok(Step::Cancel) }; + if let Some(o) = p.as_object_mut() { + o.insert("handles".into(), json!([h.hex()])); + } + run_dimtedit(s, &p)?; + Ok(Step::Done) + } +} + +impl Interactive for TEditM { + fn name(&self) -> &'static str { + "DIMTEDIT" + } + fn begin(&mut self, s: &mut Session) -> Result { + // A single preselected dimension is taken at once. + if s.settings.pickfirst + && let [h] = dims_of(s, &s.selection())[..] + { + self.dim = Some(h); + } + Ok(Step::Continue) + } + fn prompt(&self, _s: &Session) -> Prompt { + if self.dim.is_none() { + Prompt::new("Select dimension", Accept::SELECT) + } else if self.asking_angle { + Prompt::new("Specify angle for dimension text", Accept::NUMBER) + } else { + Prompt::new("Specify new location for dimension text", Accept::POINT).kw(&["Left", "Right", "Center", "Home", "Angle"]) + } + } + fn input(&mut self, s: &mut Session, i: Input) -> Result { + if self.dim.is_none() { + return Ok(match i { + Input::Pick(hs) => { + match dims_of(s, &hs).first() { + Some(h) => self.dim = Some(*h), + None if hs.is_empty() => {} + None => s.echo("Object selected is not a dimension."), + } + Step::Continue + } + Input::Enter | Input::Cancel => Step::Cancel, + _ => Step::Continue, + }); + } + if self.asking_angle { + return match i { + Input::Text(t) => match s.angle_settings().direction(&t) { + Some(a) => self.run(s, json!({ "mode": "angle", "angle": a.to_degrees() })), + None => { + s.echo("Requires a valid angle."); + Ok(Step::Continue) + } + }, + Input::Enter | Input::Cancel => Ok(Step::Cancel), + _ => Ok(Step::Continue), + }; + } + match i { + Input::Point(p) => self.run(s, json!({ "at": [p.x, p.y] })), + Input::Keyword(k) if k == "Angle" => { + self.asking_angle = true; + Ok(Step::Continue) + } + Input::Keyword(k) => self.run(s, json!({ "mode": k.to_ascii_lowercase() })), + Input::Enter | Input::Cancel => Ok(Step::Cancel), + _ => Ok(Step::Continue), + } + } + /// The text follows the cursor. + fn preview(&self, s: &Session, c: Vec2) -> Vec { + if self.asking_angle { + return Vec::new(); + } + let Some(EntityKind::Dimension(mut d)) = self.dim.and_then(|h| s.doc().ok()?.entity(h).map(|e| e.kind.clone())) else { return Vec::new() }; + d.text_mid = v3(c); + d.user_text_pos = true; + d.block = None; + vec![EntityKind::Dimension(d)] + } +} diff --git a/crates/engine/tests/dimtedit_prompts.rs b/crates/engine/tests/dimtedit_prompts.rs new file mode 100644 index 000000000..38cbae7ce --- /dev/null +++ b/crates/engine/tests/dimtedit_prompts.rs @@ -0,0 +1,49 @@ +//! DIMTEDIT at the command line and in scripts: "Select dimension", then a new text location or +//! Left/Right/Center/Home/Angle. + +use cadcraft_engine::Session; +use cadcraft_engine::doc::{Dimension, EntityKind}; +use cadcraft_engine::geom::Vec2; + +fn the_dim(s: &Session) -> Dimension { + s.doc() + .unwrap() + .model + .iter() + .find_map(|e| match &e.kind { + EntityKind::Dimension(d) => Some(d.clone()), + _ => None, + }) + .unwrap() +} + +#[test] +fn dimtedit_moves_justifies_rotates_and_homes_typed_text() { + let mut s = Session::new(); + s.script("dimlinear 0,0 10,0 5,3\n").unwrap(); + // Pick the dimension on its dimension line, then a new text location. + s.script("dimtedit 2,3\n").unwrap(); + let p = s.prompt_text(); + assert!(p.contains("Specify new location for dimension text") && p.contains("[Left/Right/Center/Home/Angle]"), "{p}"); + s.script("7,6\n").unwrap(); + assert!(s.running.is_none()); + let d = the_dim(&s); + assert!(d.user_text_pos && d.text_mid.xy().dist(Vec2::new(7.0, 6.0)) < 1e-9, "{:?}", d.text_mid); + + s.script("dimtedit 2,3 h\n").unwrap(); + assert!(!the_dim(&s).user_text_pos, "Home puts the text back"); + + s.script("dimtedit 2,3 l\n").unwrap(); + assert_eq!(the_dim(&s).overrides.get("textJust").and_then(|v| v.as_i64()), Some(1)); + + s.script("dimtedit 2,3 a 30\n").unwrap(); + assert!(s.running.is_none()); + assert!((the_dim(&s).text_rotation - 30f64.to_radians()).abs() < 1e-9); + + // Something other than a dimension is refused and the prompt stays. + s.script("line 20,20 30,20\n\ndimtedit 25,20\n").unwrap(); + assert!(s.prompt_text().contains("Select dimension"), "{}", s.prompt_text()); + s.script("2,3 c\n").unwrap(); + assert!(s.running.is_none()); + assert_eq!(the_dim(&s).overrides.get("textJust").and_then(|v| v.as_i64()), Some(0)); +}