-ATTDEF at the command line: attribute modes, AFLAGS, and the text's prompts (#2597)

* -ATTDEF at the command line: attribute modes, AFLAGS, and the text's prompts

-ATTDEF asked for the tag first. A script's mode answer ("_V", "_C",
"_P") became the tag; the attribute kept no mode; AFLAGS never changed.
The definition was made at the start point, so the height and rotation a
script gives next ran as commands. It now asks as AutoCAD 2024 does:

- "Current attribute modes:" from AFLAGS, then "Enter an option to
  change [Invisible/Constant/Verify/Preset/Lock position/Annotative/
  Multiple lines] <done>", each answer toggling a mode, kept in AFLAGS.
- The tag (spaces taken, upper-cased; an empty one gets "Tag cannot be
  null"), the prompt and default value, or a constant attribute's value.
- "Current text style: ...", the point with Justify and Style, the
  height and the rotation.

The definition takes Invisible, Constant, Verify, Preset and Lock
position from the modes; a constant attribute stays in its block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* -ATTDEF: a typed height is a distance, as TEXT takes it

The height was read with str::parse, so 1'6" (or any distance TEXT
takes) was refused as "Requires a positive height." It now goes through
the shared distance parser, `number`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* -ATTDEF: a wrong answer at Justify is asked again

A wrong answer at "Enter an option [Left/Center/...]", or Align or Fit
(not available here), went on to the point prompt with the justification
unchanged, so a script's next answers landed one prompt off. Only a
justification goes on to its point now; anything else is asked again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* -ATTDEF test: the modes start from AFLAGS, not from the default

The other cases start from AFLAGS 16, which is also the default, so
reading the modes from the drawing went untested. This starts from 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2ez4gcx
2026-11-01 14:37:49 -05:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4b4ec8043e
commit 520394858c
5 changed files with 483 additions and 47 deletions
+2 -47
View File
@@ -17,6 +17,7 @@ use super::{CommandSpec, bad, bool_or, f64_or, find_command, ok, point_param, po
use super::has_doc;
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
mod attdef;
mod attedit;
mod burst;
mod description;
@@ -45,7 +46,7 @@ pub fn specs() -> Vec<CommandSpec> {
.menu(&["Draw", "Block", "Define Attributes..."])
.alias(&["att", "-attdef"])
.params("{tag, prompt?, default?, at, height?, invisible?}")
.interactive(|_| Ok(Box::new(AttdefM::default()))),
.interactive(|_| Ok(Box::new(attdef::AttdefM::default()))),
attedit::attedit_spec(),
attedit::dash_attedit_spec(),
CommandSpec::new("wblock", "Write Block", run_wblock).alias(&["w"]).params("{path, name? | handles?, base?}"),
@@ -983,52 +984,6 @@ impl Interactive for InsertM {
}
}
#[derive(Default)]
struct AttdefM {
tag: Option<String>,
prompt: Option<String>,
default: Option<String>,
}
impl Interactive for AttdefM {
fn name(&self) -> &'static str {
"ATTDEF"
}
fn prompt(&self, _s: &Session) -> Prompt {
match (&self.tag, &self.prompt, &self.default) {
(None, ..) => Prompt::new("Enter attribute tag name", Accept::TEXT),
(Some(_), None, _) => Prompt::new("Enter attribute prompt", Accept::TEXT),
(Some(_), Some(_), None) => Prompt::new("Enter default attribute value", Accept::TEXT),
_ => Prompt::new("Specify start point of text", Accept::POINT),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let text = match &i {
Input::Text(t) => Some(t.clone()),
Input::Enter => Some(String::new()),
_ => None,
};
match (&self.tag, &self.prompt, &self.default) {
(None, ..) => {
if let Some(t) = text.filter(|t| !t.trim().is_empty() && !t.contains(' ')) {
self.tag = Some(t.to_ascii_uppercase());
}
}
(Some(_), None, _) => self.prompt = text,
(Some(_), Some(_), None) => self.default = text,
(Some(tag), Some(pr), Some(def)) => {
if let Input::Point(p) = i {
let h = s.doc()?.header.f64("TEXTSIZE", 0.2);
let k = attdef_kind(s, tag, pr, def, p, h, false);
s.add_entity(k)?;
return Ok(Step::Done);
}
}
}
Ok(Step::Continue)
}
}
mod attribs;
mod battman;
mod blockreplace;
+293
View File
@@ -0,0 +1,293 @@
//! -ATTDEF at the command line, as AutoCAD 2024 runs it (accoreconsole): the attribute modes
//! (kept in AFLAGS), the tag, prompt and default value (a constant attribute's value instead),
//! then the text's point, justification, style, height and rotation.
use cadcraft_doc::{AttribModes, HAlign, VAlign};
use super::{Accept, Attrib, EntityKind, Input, Interactive, Prompt, Result, Session, Step, Text, Vec2, number, v3};
use crate::cmd::curves::KW;
use crate::cmd::draw::justify;
/// AFLAGS bits, as AutoCAD numbers the modes.
const INVISIBLE: i64 = 1;
const CONSTANT: i64 = 2;
const VERIFY: i64 = 4;
const PRESET: i64 = 8;
const LOCK: i64 = 16;
const MULTILINE: i64 = 32;
const MODES: [&str; 7] = ["Invisible", "Constant", "Verify", "Preset", "Lock position", "Annotative", "Multiple lines"];
const JUSTIFY: [&str; 15] = ["Left", "Center", "Right", "Align", "Middle", "Fit", "TL", "TC", "TR", "ML", "MC", "MR", "BL", "BC", "BR"];
/// Where -ATTDEF is in its prompts.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Ask {
#[default]
Modes,
Tag,
Prompt,
Value,
Point,
Justify,
Style,
Height,
Rotation,
}
pub(super) struct AttdefM {
ask: Ask,
flags: i64,
tag: String,
prompt: String,
value: String,
justify: (HAlign, VAlign),
style: String,
at: Vec2,
height: f64,
}
impl Default for AttdefM {
fn default() -> Self {
AttdefM {
ask: Ask::Modes,
flags: LOCK,
tag: String::new(),
prompt: String::new(),
value: String::new(),
justify: (HAlign::Left, VAlign::Baseline),
style: "Standard".into(),
at: Vec2::ZERO,
height: 0.0,
}
}
}
impl AttdefM {
fn echo_modes(&self, s: &mut Session) {
let yn = |bit: i64| if self.flags & bit != 0 { "Y" } else { "N" };
s.echo("Current attribute modes:");
s.echo(format!(
"Invisible={} Constant={} Verify={} Preset={} Lock position={} Annotative=N Multiple line={}",
yn(INVISIBLE),
yn(CONSTANT),
yn(VERIFY),
yn(PRESET),
yn(LOCK),
yn(MULTILINE)
));
}
fn echo_style(&self, s: &mut Session) -> Result<()> {
let th = s.doc()?.header.f64("TEXTSIZE", 0.2);
s.echo(format!("Current text style: \"{}\" Text height: {th:.4} Justify: {}", self.style, justify_name(self.justify)));
Ok(())
}
fn point_prompt(&self) -> &'static str {
match self.justify {
(HAlign::Center, VAlign::Baseline) => "Specify center point of text",
(HAlign::Right, VAlign::Baseline) => "Specify right endpoint of text baseline",
(HAlign::Middle, _) | (HAlign::Center, VAlign::Middle) => "Specify middle point of text",
(HAlign::Left, VAlign::Top) => "Specify top-left point of text",
(HAlign::Center, VAlign::Top) => "Specify top-center point of text",
(HAlign::Right, VAlign::Top) => "Specify top-right point of text",
(HAlign::Left, VAlign::Middle) => "Specify middle-left point of text",
(HAlign::Right, VAlign::Middle) => "Specify middle-right point of text",
(HAlign::Left, VAlign::Bottom) => "Specify bottom-left point of text",
(HAlign::Center, VAlign::Bottom) => "Specify bottom-center point of text",
(HAlign::Right, VAlign::Bottom) => "Specify bottom-right point of text",
_ => "Specify start point of text",
}
}
fn finish(&mut self, s: &mut Session, rotation: f64) -> Result<Step> {
let (h, v) = self.justify;
let left = h == HAlign::Left && v == VAlign::Baseline;
s.add_entity(EntityKind::AttDef(Attrib {
tag: self.tag.to_uppercase(),
text: Text {
insert: v3(self.at),
align_pt: if left { None } else { Some(v3(self.at)) },
height: self.height,
value: std::mem::take(&mut self.value),
rotation,
width_factor: 1.0,
oblique: 0.0,
style: self.style.clone(),
halign: h,
valign: v,
},
invisible: self.flags & INVISIBLE != 0,
constant: self.flags & CONSTANT != 0,
prompt: std::mem::take(&mut self.prompt),
props: Default::default(),
multiline: None,
modes: AttribModes { verify: self.flags & VERIFY != 0, preset: self.flags & PRESET != 0, lock_position: self.flags & LOCK != 0 },
}))?;
Ok(Step::Done)
}
}
/// The justification's name in "Current text style: … Justify: …".
fn justify_name(j: (HAlign, VAlign)) -> &'static str {
JUSTIFY.into_iter().find(|k| justify(k) == Some(j)).unwrap_or("Left")
}
impl Interactive for AttdefM {
fn name(&self) -> &'static str {
"ATTDEF"
}
fn begin(&mut self, s: &mut Session) -> Result<Step> {
let d = s.doc()?;
self.flags = d.header.i64("AFLAGS", LOCK);
self.style = d.header.str("TEXTSTYLE", "Standard");
self.echo_modes(s);
Ok(Step::Continue)
}
fn prompt(&self, s: &Session) -> Prompt {
match self.ask {
Ask::Modes => Prompt::new("Enter an option to change", KW).kw(&MODES).default("done"),
Ask::Tag => Prompt::new("Enter attribute tag name", Accept::TEXT),
Ask::Prompt => Prompt::new("Enter attribute prompt", Accept::TEXT),
Ask::Value if self.flags & CONSTANT != 0 => Prompt::new("Enter attribute value", Accept::TEXT),
Ask::Value => Prompt::new("Enter default attribute value", Accept::TEXT),
// Once a justification is chosen AutoCAD asks for its point alone.
Ask::Point if self.justify == (HAlign::Left, VAlign::Baseline) => {
Prompt::new(self.point_prompt(), Accept::POINT).kw(&["Justify", "Style"])
}
Ask::Point => Prompt::new(self.point_prompt(), Accept::POINT),
Ask::Justify => Prompt::new("Enter an option", Accept::TEXT).kw(&JUSTIFY),
Ask::Style => Prompt::new("Enter style name", Accept::TEXT).kw(&["?"]).default(self.style.clone()),
Ask::Height => {
let th = s.doc().map(|d| d.header.f64("TEXTSIZE", 0.2)).unwrap_or(0.2);
Prompt::new("Specify height", Accept::POINT_OR_NUMBER).default(format!("{th:.4}")).base(self.at)
}
Ask::Rotation => Prompt::new("Specify rotation angle of text", Accept::POINT_OR_NUMBER).default("0").base(self.at),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match self.ask {
Ask::Modes => match &i {
Input::Keyword(k) => {
let bit = match k.as_str() {
"Invisible" => INVISIBLE,
"Constant" => CONSTANT,
"Verify" => VERIFY,
"Preset" => PRESET,
"Lock position" => LOCK,
_ => {
s.echo(format!("{k} attributes are not available yet."));
return Ok(Step::Continue);
}
};
self.flags ^= bit;
s.doc_mut()?.header.set_i64("AFLAGS", self.flags);
self.echo_modes(s);
}
Input::Enter => self.ask = Ask::Tag,
Input::Text(_) => s.echo("Invalid option keyword."),
_ => {}
},
Ask::Tag => match &i {
// AutoCAD takes spaces in a tag here, and upper-cases it.
Input::Text(t) if !t.trim().is_empty() => {
self.tag = t.trim().to_string();
self.ask = if self.flags & CONSTANT != 0 { Ask::Value } else { Ask::Prompt };
}
Input::Text(_) | Input::Enter => s.echo("Tag cannot be null"),
_ => {}
},
Ask::Prompt | Ask::Value => {
let t = match i {
Input::Text(t) => t,
Input::Enter => String::new(),
_ => return Ok(Step::Continue),
};
if self.ask == Ask::Prompt {
self.prompt = t;
self.ask = Ask::Value;
} else {
self.value = t;
self.echo_style(s)?;
self.ask = Ask::Point;
}
}
Ask::Point => match i {
Input::Point(p) => {
self.at = p;
self.ask = Ask::Height;
}
Input::Keyword(k) if k == "Justify" => self.ask = Ask::Justify,
Input::Keyword(_) => self.ask = Ask::Style,
_ => {}
},
Ask::Justify => {
// A justification goes on to its point; anything else is asked again.
if let Input::Keyword(k) | Input::Text(k) = &i {
match justify(k) {
// Aligned and fitted text take two points: not here yet.
Some((HAlign::Aligned | HAlign::Fit, _)) => s.echo("Align and Fit are not available here yet."),
Some(j) => {
self.justify = j;
self.ask = Ask::Point;
}
None => s.echo("Invalid option keyword."),
}
}
}
Ask::Style => {
let name = match i {
Input::Enter => self.style.clone(),
Input::Text(t) | Input::Keyword(t) => t.trim().to_string(),
_ => return Ok(Step::Continue),
};
if name == "?" {
let names: Vec<String> = s.doc()?.text_styles.iter().map(|st| st.name.clone()).collect();
s.echo(format!("Text styles: {}", names.join(", ")));
return Ok(Step::Continue);
}
match s.doc()?.text_style(&name).map(|st| st.name.clone()) {
Some(n) => {
self.style = n;
self.echo_style(s)?;
self.ask = Ask::Point;
}
None => s.echo(format!("Cannot find text style \"{name}\".")),
}
}
Ask::Height => {
let th = s.doc()?.header.f64("TEXTSIZE", 0.2);
let h = match &i {
Input::Enter => th,
Input::Point(p) => self.at.dist(*p),
// A distance, as TEXT takes its height (1'6" in feet and inches, too).
Input::Text(t) => number(t).unwrap_or(0.0),
_ => return Ok(Step::Continue),
};
if !(h.is_finite() && h > 0.0) {
s.echo("Requires a positive height.");
return Ok(Step::Continue);
}
self.height = h;
s.doc_mut()?.header.set_f64("TEXTSIZE", h);
self.ask = Ask::Rotation;
}
Ask::Rotation => {
let r = match &i {
Input::Enter => 0.0,
Input::Point(p) => self.at.angle_to(*p),
Input::Text(t) => match s.angle_settings().direction(t) {
Some(a) => a,
None => {
s.echo("Requires an angle.");
return Ok(Step::Continue);
}
},
_ => return Ok(Step::Continue),
};
return self.finish(s, r);
}
}
Ok(Step::Continue)
}
}
+1
View File
@@ -33,6 +33,7 @@ use point::{DonutM, PointM, RayM, run_donut, run_point, run_ray};
use polygon::{PolygonM, run_polygon};
use solid::{SolidM, run_solid};
use spline::{SplineM, run_spline};
pub(crate) use text::justify;
use text::{MTextM, TextM, run_mtext, run_text};
/// Every draw command's spec: JSON parameters, menu path, aliases and the interactive prompt
+186
View File
@@ -0,0 +1,186 @@
//! -ATTDEF at the command line, as AutoCAD 2024 runs it (accoreconsole: these scripts with
//! TEXTSIZE 2.5 and AFLAGS 16, the drawing and AFLAGS read back afterwards).
use cadcraft_engine::doc::{Attrib, EntityKind, HAlign, VAlign};
use cadcraft_engine::{Session, sysvars};
use serde_json::json;
fn session() -> Session {
let mut s = Session::new();
s.execute("setvar", &json!({ "name": "TEXTSIZE", "value": 2.5 })).unwrap();
s.execute("setvar", &json!({ "name": "AFLAGS", "value": 16 })).unwrap();
s
}
fn attdefs(s: &Session) -> Vec<Attrib> {
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::AttDef(a) = &e.kind { Some(a.clone()) } else { None }).collect()
}
fn aflags(s: &Session) -> i64 {
sysvars::get(s, "AFLAGS").and_then(|v| v.as_f64()).unwrap() as i64
}
fn run(script: &str) -> Session {
let mut s = session();
s.cmdline(script).unwrap();
assert!(s.running.is_none(), "{script}: {}", s.prompt_text());
s
}
#[test]
fn modes_are_toggled_kept_in_aflags_and_stored_on_the_definition() {
// (script, AFLAGS after, invisible, constant, verify, preset, lock position, tag, prompt, value)
type Case = (&'static str, i64, [bool; 5], &'static str, &'static str, &'static str);
let cases: [Case; 5] = [
// ATTDEF-000: the modes as they are.
(
"(command \"_.-ATTDEF\" \"\" \"T1\" \"Nhap ten\" \"MAC\" '(5.0 10.0) 2.5 0)",
16,
[false, false, false, false, true],
"T1",
"Nhap ten",
"MAC",
),
// ATTDEF-001: Invisible and Verify.
(
"(command \"_.-ATTDEF\" \"_I\" \"_V\" \"\" \"T2\" \"Hoi hai\" \"D2\" '(5.0 10.0) 3.0 30)",
21,
[true, false, true, false, true],
"T2",
"Hoi hai",
"D2",
),
// ATTDEF-002: Constant asks for the value, not a prompt and a default.
("(command \"_.-ATTDEF\" \"_C\" \"\" \"T3\" \"GIATRI\" '(5.0 10.0) 2.5 0)", 18, [false, true, false, false, true], "T3", "", "GIATRI"),
// ATTDEF-003: Preset.
(
"(command \"_.-ATTDEF\" \"_P\" \"\" \"T4\" \"Hoi bon\" \"D4\" '(612350.678 2398775.432) 2.5 0)",
24,
[false, false, false, true, true],
"T4",
"Hoi bon",
"D4",
),
// ATTDEF-004: Lock position off.
(
"(command \"_.-ATTDEF\" \"_L\" \"\" \"T5\" \"Hoi nam\" \"D5\" '(5.0 10.0) 2.5 0)",
0,
[false, false, false, false, false],
"T5",
"Hoi nam",
"D5",
),
];
for (script, flags, [inv, con, ver, pre, lock], tag, prompt, value) in cases {
let s = run(script);
assert_eq!(aflags(&s), flags, "{script}");
let a = attdefs(&s).remove(0);
assert_eq!((a.invisible, a.constant, a.modes.verify, a.modes.preset, a.modes.lock_position), (inv, con, ver, pre, lock), "{script}");
assert_eq!((a.tag.as_str(), a.prompt.as_str(), a.text.value.as_str()), (tag, prompt, value), "{script}");
}
// ATTDEF-001's height 3 and 30 degrees.
let a = attdefs(&run(cases[1].0)).remove(0);
assert!((a.text.height - 3.0).abs() < 1e-12 && (a.text.rotation - 30f64.to_radians()).abs() < 1e-12, "{a:?}");
}
#[test]
fn tags_justification_and_style_as_autocad_takes_them() {
// ATTDEF-021, ATTDEF-022: a tag is upper-cased, spaces and all.
for (typed, tag) in [("maso", "MASO"), ("ten ban ve", "TEN BAN VE")] {
let s = run(&format!("(command \"_.-ATTDEF\" \"\" \"{typed}\" \"Hoi\" \"D\" '(5.0 10.0) 2.5 0)"));
assert_eq!(attdefs(&s)[0].tag, tag);
}
// ATTDEF-023: an empty prompt stays empty.
let s = run("(command \"_.-ATTDEF\" \"\" \"T10\" \"\" \"D10\" '(612350.678 2398775.432) 2.5 0)");
assert_eq!(attdefs(&s)[0].prompt, "");
// ATTDEF-025: an empty tag is refused and asked again.
let s = run("(command \"_.-ATTDEF\" \"\" \"\" \"T11\" \"Hoi muoi mot\" \"D11\" '(5.0 10.0) 2.5 0)");
assert!(s.log.iter().any(|l| l == "Tag cannot be null"), "{:?}", s.log);
assert_eq!(attdefs(&s)[0].tag, "T11");
// ATTDEF-005: Justify MC at the middle point.
let s = run("(command \"_.-ATTDEF\" \"\" \"T6\" \"Hoi sau\" \"D6\" \"_J\" \"_MC\" '(5.0 10.0) 2.5 0)");
let a = attdefs(&s).remove(0);
assert_eq!((a.text.halign, a.text.valign), (HAlign::Center, VAlign::Middle));
assert!(a.text.align_pt.is_some_and(|p| p.x == 5.0 && p.y == 10.0), "{a:?}");
// ATTDEF-006: Style, then the point.
let s = run("(command \"_.-ATTDEF\" \"\" \"T7\" \"Hoi bay\" \"D7\" \"_S\" \"Standard\" '(0.505 0.26) 2.5 0)");
let a = attdefs(&s).remove(0);
assert!(a.text.style == "Standard" && a.text.insert.x == 0.505 && a.text.insert.y == 0.26, "{a:?}");
}
#[test]
fn the_prompts_are_autocads() {
let mut s = session();
s.cmdline("-attdef").unwrap();
assert!(s.log.iter().any(|l| l == "Current attribute modes:"), "{:?}", s.log);
assert_eq!(s.log.last().map(String::as_str), Some("Invisible=N Constant=N Verify=N Preset=N Lock position=Y Annotative=N Multiple line=N"));
assert!(s.prompt_text().contains("[Invisible/Constant/Verify/Preset/Lock position/Annotative/Multiple lines] <done>:"), "{}", s.prompt_text());
s.cmdline("X").unwrap();
assert_eq!(s.log.last().map(String::as_str), Some("Invalid option keyword."));
s.cmdline("_C").unwrap();
assert_eq!(s.log.last().map(String::as_str), Some("Invisible=N Constant=Y Verify=N Preset=N Lock position=Y Annotative=N Multiple line=N"));
s.cmdline("").unwrap();
assert!(s.prompt_text().ends_with("Enter attribute tag name:"), "{}", s.prompt_text());
s.cmdline("T3").unwrap();
assert!(s.prompt_text().ends_with("Enter attribute value:"), "{}", s.prompt_text());
s.cmdline("GIATRI").unwrap();
assert_eq!(s.log.last().map(String::as_str), Some("Current text style: \"Standard\" Text height: 2.5000 Justify: Left"));
assert!(s.prompt_text().ends_with("Specify start point of text or [Justify/Style]:"), "{}", s.prompt_text());
s.cmdline("_J").unwrap();
// A wrong answer, or a justification not available here, is asked again.
for typed in ["X", "_A"] {
s.cmdline(typed).unwrap();
assert!(s.prompt_text().contains("[Left/Center/Right/Align/Middle/Fit/TL/"), "{typed}: {}", s.prompt_text());
}
s.cmdline("_MC").unwrap();
assert!(s.prompt_text().ends_with("Specify middle point of text:"), "{}", s.prompt_text());
s.cmdline("0,0").unwrap();
assert!(s.prompt_text().ends_with("Specify height <2.5000>:"), "{}", s.prompt_text());
s.cmdline("").unwrap();
assert!(s.prompt_text().ends_with("Specify rotation angle of text <0>:"), "{}", s.prompt_text());
s.cancel();
}
#[test]
fn a_constant_attribute_stays_in_the_block() {
// INSERTATT-011: the constant TC is no attribute of the reference; the other one, TN, is.
let mut s = session();
s.execute("line", &json!({ "points": [[612345.678, 2398765.432], [612355.678, 2398765.432]] })).unwrap();
s.cmdline("(setq hl_A (entlast))").unwrap();
s.cmdline("(progn (command \"_.-ATTDEF\" \"_C\" \"\" \"TC\" \"HANG\" '(612350.678 2398775.432) 2.5 0) (setq hl_a1 (entlast)))").unwrap();
s.cmdline("(setvar \"AFLAGS\" 16)").unwrap();
s.cmdline("(progn (command \"_.-ATTDEF\" \"\" \"TN\" \"Hoi N\" \"MN\" '(612350.678 2398785.432) 2.5 0) (setq hl_a2 (entlast)))").unwrap();
s.cmdline("(command \"_.-BLOCK\" \"B11\" '(612345.678 2398765.432) hl_a1 hl_a2 hl_A \"\")").unwrap();
s.cmdline("(command \"_.-INSERT\" \"B11\" '(612445.678 2398815.432) 1 1 0 \"XN\")").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let ins = s.doc().unwrap().model.iter().find_map(|e| if let EntityKind::Insert(i) = &e.kind { Some(i.clone()) } else { None }).unwrap();
let tags: Vec<(String, String)> = ins.attribs.iter().map(|a| (a.tag.clone(), a.text.value.clone())).collect();
assert_eq!(tags, [("TN".to_string(), "XN".to_string())]);
}
#[test]
fn a_typed_height_is_a_distance() {
let mut s = session();
for typed in ["-attdef", "", "T1", "P", "V", "5,10", "x"] {
s.cmdline(typed).unwrap();
}
assert_eq!(s.log.last().map(String::as_str), Some("Requires a positive height."));
// Feet and inches, as TEXT takes them.
for typed in ["1'6\"", "0"] {
s.cmdline(typed).unwrap();
}
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(attdefs(&s)[0].text.height, 18.0);
}
#[test]
fn the_modes_start_from_aflags() {
// AFLAGS 0, not the default 16: Invisible toggled on gives 1, and nothing else.
let mut s = session();
s.execute("setvar", &json!({ "name": "AFLAGS", "value": 0 })).unwrap();
s.cmdline("(command \"_.-ATTDEF\" \"_I\" \"\" \"T1\" \"Hoi\" \"D\" '(5.0 10.0) 2.5 0)").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert_eq!(aflags(&s), 1);
let a = attdefs(&s).remove(0);
assert_eq!((a.invisible, a.modes.lock_position), (true, false));
}
+1
View File
@@ -5,6 +5,7 @@ mod annotative;
mod arc_ctrl_direction;
mod array_commands;
mod assoc_array;
mod attdef_command;
mod attdisp_attsync;
mod attedit_prompts;
mod attrib_modes;