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 <noreply@anthropic.com>
This commit is contained in:
Valentin Klein
2026-10-10 18:09:10 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1418130d45
commit 409968ecbb
3 changed files with 155 additions and 1 deletions
+6 -1
View File
@@ -99,7 +99,9 @@ pub fn specs() -> Vec<CommandSpec> {
.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;
+100
View File
@@ -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<Handle>,
asking_angle: bool,
}
impl TEditM {
fn run(&self, s: &mut Session, mut p: Value) -> Result<Step> {
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<Step> {
// 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<Step> {
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<EntityKind> {
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)]
}
}
+49
View File
@@ -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));
}