//! Command-line forms of the Block Definition tools: -BLOCK, -WBLOCK, //! -BEDIT and ATTSYNC, with the reference's prompts and messages, and the //! choices the Block Definition / Write Block dialogs remember. use codec::{CadDocument, Handle}; use glam::DVec3; use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; use crate::ui::window::block_definition::{invalid_block_name_char, BlockObjectMode}; // ── Remembered dialog choices ───────────────────────────────────────────── /// What the Block Definition and Write Block dialogs remember between /// uses (and the profile keeps): the on-screen switches, the object /// conversion, Allow exploding and Open in block editor. -BLOCK and -WBLOCK /// start from the same conversion. #[derive(Clone, Copy, Debug, PartialEq)] pub struct BlockDefaults { pub base_on_screen: bool, pub objects_on_screen: bool, pub mode: BlockObjectMode, pub allow_exploding: bool, pub open_in_editor: bool, pub wblock_mode: BlockObjectMode, } static DEFAULTS: std::sync::Mutex = std::sync::Mutex::new(BlockDefaults { base_on_screen: false, objects_on_screen: false, mode: BlockObjectMode::Convert, allow_exploding: true, open_in_editor: false, wblock_mode: BlockObjectMode::Retain, }); pub fn defaults() -> std::sync::MutexGuard<'static, BlockDefaults> { DEFAULTS.lock().unwrap_or_else(|e| e.into_inner()) } /// The settings code of a conversion (0 Retain, 1 Convert, 2 Delete). pub fn mode_code(mode: BlockObjectMode) -> u8 { match mode { BlockObjectMode::Retain => 0, BlockObjectMode::Convert => 1, BlockObjectMode::Delete => 2, } } pub fn mode_from_code(code: u8) -> BlockObjectMode { match code { 0 => BlockObjectMode::Retain, 2 => BlockObjectMode::Delete, _ => BlockObjectMode::Convert, } } fn mode_name(mode: BlockObjectMode) -> &'static str { match mode { BlockObjectMode::Retain => "Retain", BlockObjectMode::Convert => "Convert", BlockObjectMode::Delete => "Delete", } } /// `Current settings: Object conversion=…`, printed as -BLOCK and -WBLOCK start. pub fn current_settings_line(mode: BlockObjectMode) -> String { format!("Current settings: Object conversion={}", mode_name(mode)) } // ── Block table listing (`?`) ───────────────────────────────────────────── /// The block names of a drawing as the `?` listing sorts and counts them. #[derive(Clone, Debug, Default)] pub struct BlockTable { /// (name, kind) for every block but the model and paper spaces. pub entries: Vec<(String, BlockKind)>, /// Names of the blocks that hold attribute definitions. pub attributed: Vec, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BlockKind { User, External, Dependent, Unnamed, } impl BlockTable { pub fn of(doc: &CadDocument) -> Self { let mut entries = Vec::new(); let mut attributed = Vec::new(); for br in doc.block_records.iter() { let upper = br.name.to_ascii_uppercase(); if upper == "*MODEL_SPACE" || upper.starts_with("*PAPER_SPACE") { continue; } let kind = if br.flags.is_xref || br.flags.is_xref_overlay { BlockKind::External } else if br.name.contains('|') { BlockKind::Dependent } else if br.name.starts_with('*') || br.flags.anonymous { BlockKind::Unnamed } else { BlockKind::User }; if kind == BlockKind::User && br.entity_handles.iter().any(|h| { matches!( doc.get_entity(*h), Some(codec::EntityType::AttributeDefinition(_)) ) }) { attributed.push(br.name.clone()); } entries.push((br.name.clone(), kind)); } entries.sort_by(|a, b| sort_key(&a.0).cmp(&sort_key(&b.0))); attributed.sort_by(|a, b| sort_key(a).cmp(&sort_key(b))); Self { entries, attributed, } } pub fn user_names(&self) -> impl Iterator { self.entries .iter() .filter(|(_, kind)| *kind == BlockKind::User) .map(|(name, _)| name.as_str()) } /// The user block named `name`, in its stored case. pub fn find_user(&self, name: &str) -> Option { self.user_names() .find(|n| n.eq_ignore_ascii_case(name)) .map(str::to_string) } /// `Defined blocks.`, the matching names, and the four counts of the /// blocks the pattern matched. pub fn listing(&self, pattern: &str) -> String { let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() }; let matches = |name: &str| { pattern .split(',') .any(|p| crate::io::xref_model::wildcard_match(name, p.trim())) }; let mut out = String::from("Defined blocks."); let mut counts = [0usize; 4]; for (name, kind) in &self.entries { if !matches(name) { continue; } match kind { BlockKind::User => { out.push_str(&format!("\n \"{name}\"")); counts[0] += 1; } BlockKind::External => counts[1] += 1, BlockKind::Dependent => counts[2] += 1, BlockKind::Unnamed => counts[3] += 1, } } out.push_str(&format!( "\nUser External Dependent Unnamed\nBlocks References Blocks Blocks\n{:>5}{:>10}{:>12}{:>12}", counts[0], counts[1], counts[2], counts[3] )); out } } /// Punctuation sorts before digits and letters; letters ignore case. fn sort_key(name: &str) -> Vec<(u8, char)> { name.chars() .map(|c| { let lower = c.to_lowercase().next().unwrap_or(c); let rank = if c.is_alphanumeric() { 1 } else { 0 }; (rank, lower) }) .collect() } fn yes_no(text: &str, default: bool) -> Option { match text.trim().to_ascii_uppercase().as_str() { "" => Some(default), "Y" | "YES" => Some(true), "N" | "NO" => Some(false), _ => None, } } /// A block name -BLOCK accepts. fn valid_block_name(name: &str) -> bool { !name.is_empty() && !name.starts_with('*') && invalid_block_name_char(name).is_none() && name.chars().count() <= 255 } // ── -BLOCK ──────────────────────────────────────────────────────────────── #[derive(Clone, Copy, PartialEq)] enum BlockStep { Name, ListPattern, Redefine, Base, Annotative, Orient, Mode, Objects, } /// -BLOCK: `Enter block name or [?]:` → (redefine?) → /// `Specify insertion base point or [Annotative/mOde]:` → `Select objects:`. /// A preselection is taken as the objects; an empty selection defines an /// empty block. pub struct DashBlockCommand { step: BlockStep, table: BlockTable, ucs: crate::app::helpers::UcsXform, name: String, redefine: bool, mode: BlockObjectMode, annotative: bool, match_orientation: bool, base: DVec3, preselected: Vec, handles: Vec, /// The drawing's insertion units, given to the definition. units: i16, } impl DashBlockCommand { pub fn new( table: BlockTable, ucs: crate::app::helpers::UcsXform, preselected: Vec, units: i16, ) -> Self { Self { units, step: BlockStep::Name, table, ucs, name: String::new(), redefine: false, mode: defaults().mode, annotative: false, match_orientation: false, base: DVec3::ZERO, preselected, handles: Vec::new(), } } pub fn settings_line(&self) -> String { current_settings_line(self.mode) } fn create(&self, handles: Vec) -> CmdResult { CmdResult::CreateBlockWithOptions { options: Box::new(crate::scene::CreateBlockOptions { name: self.name.clone(), handles, base_point: self.base, world_to_block: self.ucs.to_ucs_transform_at(self.base), block_to_world: self.ucs.to_wcs_transform_at(self.base), mode: self.mode, annotative: self.annotative, match_orientation: self.match_orientation, scale_uniformly: self.annotative, allow_exploding: true, unit: self.units, description: String::new(), hyperlink_url: String::new(), hyperlink_desc: String::new(), redefine: self.redefine, }), } } fn after_base(&mut self) -> CmdResult { if self.preselected.is_empty() { self.step = BlockStep::Objects; CmdResult::NeedPoint } else { let handles = self.preselected.clone(); self.create(handles) } } } impl CadCommand for DashBlockCommand { fn name(&self) -> &'static str { "-BLOCK" } fn prompt(&self) -> String { match self.step { BlockStep::Name => "Enter block name or [?]:".to_string(), BlockStep::ListPattern => "Enter block(s) to list <*>:".to_string(), BlockStep::Redefine => format!( "Block \"{}\" already exists. Redefine it? [Yes/No] :", self.name ), BlockStep::Base => "Specify insertion base point or [Annotative/mOde]:".to_string(), BlockStep::Annotative => "Create annotative block [Yes/No] :".to_string(), BlockStep::Orient => { "Orient relative to sheet in paper space viewports [Yes/No] :".to_string() } BlockStep::Mode => format!( "Choose mode for selected object conversion [Convert to block/Retain/Delete] <{}>:", mode_name(self.mode) ), BlockStep::Objects => "Select objects:".to_string(), } } fn options(&self) -> Vec { let opts: &[(&str, &str)] = match self.step { BlockStep::Name => &[("?", "?")], BlockStep::Redefine | BlockStep::Annotative | BlockStep::Orient => { &[("Yes", "Y"), ("No", "N")] } BlockStep::Base => &[("Annotative", "A"), ("mOde", "O")], BlockStep::Mode => &[("Convert to block", "C"), ("Retain", "R"), ("Delete", "D")], _ => &[], }; opts.iter().map(|(l, k)| CmdOption::new(l, k)).collect() } fn wants_text_input(&self) -> bool { !matches!(self.step, BlockStep::Objects) } fn input_kind(&self) -> InputKind { match self.step { BlockStep::Name | BlockStep::ListPattern => InputKind::FreeText, BlockStep::Base => InputKind::Point, BlockStep::Objects => InputKind::Point, _ => InputKind::SingleToken, } } fn point_step_accepts_keywords(&self) -> bool { matches!(self.step, BlockStep::Base) } fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); let up = t.to_ascii_uppercase(); Some(match self.step { BlockStep::Name => { if t == "?" { self.step = BlockStep::ListPattern; return Some(CmdResult::NeedPoint); } if !valid_block_name(t) { return Some(CmdResult::ReportError("Invalid block name.".to_string())); } match self.table.find_user(t) { Some(existing) => { self.name = existing; self.step = BlockStep::Redefine; } None => { self.name = t.to_string(); self.step = BlockStep::Base; } } CmdResult::NeedPoint } BlockStep::ListPattern => CmdResult::Measurement(self.table.listing(t)), BlockStep::Redefine => match yes_no(t, false) { Some(true) => { self.redefine = true; self.step = BlockStep::Base; CmdResult::NeedPoint } Some(false) => CmdResult::Measurement(String::new()), None => CmdResult::ReportError("Invalid option keyword.".to_string()), }, BlockStep::Base => match up.as_str() { "A" | "ANNOTATIVE" => { self.step = BlockStep::Annotative; CmdResult::NeedPoint } "O" | "MODE" => { self.step = BlockStep::Mode; CmdResult::NeedPoint } _ => CmdResult::ReportError("Point or option keyword required.".to_string()), }, BlockStep::Annotative => match yes_no(t, true) { Some(true) => { self.annotative = true; self.step = BlockStep::Orient; CmdResult::NeedPoint } Some(false) => { self.annotative = false; self.match_orientation = false; self.step = BlockStep::Base; CmdResult::NeedPoint } None => CmdResult::ReportError("Invalid option keyword.".to_string()), }, BlockStep::Orient => match yes_no(t, false) { Some(orient) => { self.match_orientation = orient; self.step = BlockStep::Base; CmdResult::NeedPoint } None => CmdResult::ReportError("Invalid option keyword.".to_string()), }, BlockStep::Mode => { let mode = match up.as_str() { "" => Some(self.mode), "C" | "CONVERT" => Some(BlockObjectMode::Convert), "R" | "RETAIN" => Some(BlockObjectMode::Retain), "D" | "DELETE" => Some(BlockObjectMode::Delete), _ => None, }; match mode { Some(mode) => { self.mode = mode; self.step = BlockStep::Base; CmdResult::NeedPoint } None => CmdResult::ReportError("Invalid option keyword.".to_string()), } } BlockStep::Objects => return None, }) } fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.step { BlockStep::Base => { self.base = pt; self.after_base() } _ => CmdResult::NeedPoint, } } fn is_selection_gathering(&self) -> bool { matches!(self.step, BlockStep::Objects) } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { self.handles = handles; CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { match self.step { BlockStep::Name => CmdResult::ReportError("Invalid block name.".to_string()), BlockStep::ListPattern => CmdResult::Measurement(self.table.listing("*")), BlockStep::Redefine => CmdResult::Measurement(String::new()), BlockStep::Base => { CmdResult::ReportError("Point or option keyword required.".to_string()) } BlockStep::Annotative | BlockStep::Orient | BlockStep::Mode => self .on_text_input("") .unwrap_or(CmdResult::NeedPoint), BlockStep::Objects => { let handles = std::mem::take(&mut self.handles); self.create(handles) } } } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } } // ── -WBLOCK ─────────────────────────────────────────────────────────────── #[derive(Clone, Copy, PartialEq)] enum WblockStep { File, Replace, Source, Base, Mode, Objects, } /// What -WBLOCK writes. #[derive(Clone, Debug, PartialEq)] pub enum WblockSource { /// An existing block definition. Block(String), /// The whole drawing. Drawing, /// Objects with a base point (the conversion applied after writing). Objects { handles: Vec, base: DVec3, mode: BlockObjectMode, }, } /// -WBLOCK: `Enter name of output file:` → (replace?) → /// `Enter name of existing block or [= (block=output file)/* (whole drawing)] :` /// → `Specify insertion base point or [mOde]:` → `Select objects:`. pub struct DashWblockCommand { step: WblockStep, table: BlockTable, path: String, mode: BlockObjectMode, base: DVec3, preselected: Vec, handles: Vec, } impl DashWblockCommand { pub fn new(table: BlockTable, preselected: Vec) -> Self { Self { step: WblockStep::File, table, path: String::new(), mode: defaults().wblock_mode, base: DVec3::ZERO, preselected, handles: Vec::new(), } } pub fn settings_line(&self) -> String { current_settings_line(self.mode) } fn finish(&self, source: WblockSource) -> CmdResult { CmdResult::Dispatch(format!( "_WBLOCKWRITE {}", serde_json::json!({ "path": self.path, "source": match &source { WblockSource::Block(name) => serde_json::json!({"block": name}), WblockSource::Drawing => serde_json::json!({"drawing": true}), WblockSource::Objects { handles, base, mode } => serde_json::json!({ "handles": handles.iter().map(|h| h.value()).collect::>(), "base": [base.x, base.y, base.z], "mode": mode_code(*mode), }), } }) )) } fn after_base(&mut self) -> CmdResult { if self.preselected.is_empty() { self.step = WblockStep::Objects; CmdResult::NeedPoint } else { self.finish(WblockSource::Objects { handles: self.preselected.clone(), base: self.base, mode: self.mode, }) } } fn file_name_block(&self) -> String { std::path::Path::new(&self.path) .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_default() } } /// The output path -WBLOCK writes: `.dwg` is added when no extension is given. pub fn wblock_output_path(text: &str) -> String { let path = text.trim().trim_matches('"').to_string(); let lower = path.to_ascii_lowercase(); if lower.ends_with(".dwg") || lower.ends_with(".dxf") { path } else { format!("{path}.dwg") } } impl CadCommand for DashWblockCommand { fn name(&self) -> &'static str { "-WBLOCK" } fn prompt(&self) -> String { match self.step { WblockStep::File => "Enter name of output file:".to_string(), WblockStep::Replace => format!( "{} already exists, do you want to replace it? [Yes/No] :", self.path.replace('/', "\\") ), WblockStep::Source => "Enter name of existing block or\n[= (block=output file)/* (whole drawing)] :".to_string(), WblockStep::Base => "Specify insertion base point or [mOde]:".to_string(), WblockStep::Mode => format!( "Choose mode for selected object conversion [Convert to block/Retain/Delete] <{}>:", mode_name(self.mode) ), WblockStep::Objects => "Select objects:".to_string(), } } fn options(&self) -> Vec { let opts: &[(&str, &str)] = match self.step { WblockStep::Replace => &[("Yes", "Y"), ("No", "N")], WblockStep::Source => &[("=", "="), ("*", "*")], WblockStep::Base => &[("mOde", "O")], WblockStep::Mode => &[("Convert to block", "C"), ("Retain", "R"), ("Delete", "D")], _ => &[], }; opts.iter().map(|(l, k)| CmdOption::new(l, k)).collect() } fn wants_text_input(&self) -> bool { !matches!(self.step, WblockStep::Objects) } fn input_kind(&self) -> InputKind { match self.step { WblockStep::File | WblockStep::Source => InputKind::FreeText, WblockStep::Base | WblockStep::Objects => InputKind::Point, _ => InputKind::SingleToken, } } fn point_step_accepts_keywords(&self) -> bool { matches!(self.step, WblockStep::Base) } fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); let up = t.to_ascii_uppercase(); Some(match self.step { WblockStep::File => { if t.is_empty() { return Some(CmdResult::Cancel); } self.path = wblock_output_path(t); self.step = if std::path::Path::new(&self.path).exists() { WblockStep::Replace } else { WblockStep::Source }; CmdResult::NeedPoint } WblockStep::Replace => match yes_no(t, false) { Some(true) => { self.step = WblockStep::Source; CmdResult::NeedPoint } Some(false) => CmdResult::Measurement(String::new()), None => CmdResult::ReportError("Invalid option keyword.".to_string()), }, WblockStep::Source => match t { "" => { self.step = WblockStep::Base; CmdResult::NeedPoint } "*" => self.finish(WblockSource::Drawing), "=" => { let name = self.file_name_block(); match self.table.find_user(&name) { Some(block) => self.finish(WblockSource::Block(block)), None => CmdResult::ReportError(format!("Block \"{name}\" not found.")), } } name => match self.table.find_user(name) { Some(block) => self.finish(WblockSource::Block(block)), None => CmdResult::ReportError(format!("Block \"{name}\" not found.")), }, }, WblockStep::Base => match up.as_str() { "O" | "MODE" => { self.step = WblockStep::Mode; CmdResult::NeedPoint } _ => CmdResult::ReportError("Point or option keyword required.".to_string()), }, WblockStep::Mode => { let mode = match up.as_str() { "" => Some(self.mode), "C" | "CONVERT" => Some(BlockObjectMode::Convert), "R" | "RETAIN" => Some(BlockObjectMode::Retain), "D" | "DELETE" => Some(BlockObjectMode::Delete), _ => None, }; match mode { Some(mode) => { self.mode = mode; self.step = WblockStep::Base; CmdResult::NeedPoint } None => CmdResult::ReportError("Invalid option keyword.".to_string()), } } WblockStep::Objects => return None, }) } fn on_point(&mut self, pt: DVec3) -> CmdResult { if self.step == WblockStep::Base { self.base = pt; return self.after_base(); } CmdResult::NeedPoint } fn is_selection_gathering(&self) -> bool { matches!(self.step, WblockStep::Objects) } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { self.handles = handles; CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { match self.step { WblockStep::File => CmdResult::Cancel, WblockStep::Replace | WblockStep::Source | WblockStep::Mode => { self.on_text_input("").unwrap_or(CmdResult::NeedPoint) } WblockStep::Base => { CmdResult::ReportError("Point or option keyword required.".to_string()) } WblockStep::Objects => { let handles = std::mem::take(&mut self.handles); if handles.is_empty() { return CmdResult::Cancel; } self.finish(WblockSource::Objects { handles, base: self.base, mode: self.mode, }) } } } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } } // ── -BEDIT ──────────────────────────────────────────────────────────────── #[derive(Clone, Copy, PartialEq)] enum BeditStep { Name, ListPattern, } /// -BEDIT: `Enter block name or [?]:` opens the block editor on that /// definition, or on a new empty one for a new name. pub struct DashBeditCommand { step: BeditStep, table: BlockTable, } impl DashBeditCommand { pub fn new(table: BlockTable) -> Self { Self { step: BeditStep::Name, table, } } } impl DashBeditCommand { /// The user blocks matching `pattern`, one per line, or /// `No blocks found.` fn names(&self, pattern: &str) -> String { let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() }; let lines: Vec = self .table .user_names() .filter(|name| { pattern .split(',') .any(|p| crate::io::xref_model::wildcard_match(name, p.trim())) }) .map(|name| format!(" {name}")) .collect(); if lines.is_empty() { "No blocks found.".to_string() } else { lines.join(" ") } } } impl CadCommand for DashBeditCommand { fn name(&self) -> &'static str { "-BEDIT" } fn prompt(&self) -> String { match self.step { BeditStep::Name => "Enter block name or [?]:".to_string(), BeditStep::ListPattern => "Enter block(s) to list <*>:".to_string(), } } fn options(&self) -> Vec { match self.step { BeditStep::Name => vec![CmdOption::new("?", "?")], BeditStep::ListPattern => Vec::new(), } } fn wants_text_input(&self) -> bool { true } fn input_kind(&self) -> InputKind { InputKind::FreeText } fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); Some(match self.step { BeditStep::Name => { if t == "?" { self.step = BeditStep::ListPattern; return Some(CmdResult::NeedPoint); } if !valid_block_name(t) { return Some(CmdResult::ReportError("Invalid block name.".to_string())); } let name = self.table.find_user(t).unwrap_or_else(|| t.to_string()); CmdResult::Dispatch(format!("BEDIT_NEW:{name}")) } BeditStep::ListPattern => { self.step = BeditStep::Name; CmdResult::ReportMeasurement(self.names(t)) } }) } fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { match self.step { BeditStep::Name => CmdResult::Cancel, BeditStep::ListPattern => { self.step = BeditStep::Name; CmdResult::ReportMeasurement(self.names("*")) } } } } // ── ATTSYNC ─────────────────────────────────────────────────────────────── #[derive(Clone, Copy, PartialEq)] enum AttsyncStep { Option, Name, Pick, Confirm, } /// ATTSYNC: `Enter an option [?/Name/Select] :".to_string(), AttsyncStep::Name => "Enter name of block to sync or [?]:".to_string(), AttsyncStep::Pick => "Select a block:".to_string(), AttsyncStep::Confirm => format!("ATTSYNC block {}? [Yes/No] :", self.picked), } } fn options(&self) -> Vec { let opts: &[(&str, &str)] = match self.step { AttsyncStep::Option => &[("?", "?"), ("Name", "N"), ("Select", "S")], AttsyncStep::Name => &[("?", "?")], AttsyncStep::Confirm => &[("Yes", "Y"), ("No", "N")], AttsyncStep::Pick => &[], }; opts.iter().map(|(l, k)| CmdOption::new(l, k)).collect() } fn wants_text_input(&self) -> bool { !matches!(self.step, AttsyncStep::Pick) } fn input_kind(&self) -> InputKind { match self.step { AttsyncStep::Name => InputKind::FreeText, AttsyncStep::Pick => InputKind::Point, _ => InputKind::SingleToken, } } fn needs_entity_pick(&self) -> bool { matches!(self.step, AttsyncStep::Pick) } fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { match self.reference_blocks.get(&handle) { Some(name) => { if self .table .attributed .iter() .any(|n| n.eq_ignore_ascii_case(name)) { self.picked = name.clone(); self.step = AttsyncStep::Confirm; CmdResult::NeedPoint } else { CmdResult::Measurement(format!( "Block table contains no attributed block named {name}." )) } } None => CmdResult::ReportError("Error selecting entity.".to_string()), } } fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); let up = t.to_ascii_uppercase(); Some(match self.step { AttsyncStep::Option => match up.as_str() { "?" => CmdResult::ReportMeasurement(self.attributed_line()), "N" | "NAME" => { self.step = AttsyncStep::Name; CmdResult::NeedPoint } "" | "S" | "SELECT" => { self.step = AttsyncStep::Pick; CmdResult::NeedPoint } _ => CmdResult::ReportError("Invalid option keyword.".to_string()), }, AttsyncStep::Name => { if t == "?" { return Some(CmdResult::ReportMeasurement(self.attributed_line())); } if t.is_empty() { return Some(CmdResult::Cancel); } self.sync(t) } AttsyncStep::Confirm => match yes_no(t, true) { Some(true) => { let name = self.picked.clone(); self.sync(&name) } Some(false) => CmdResult::Cancel, None => CmdResult::ReportError("Invalid option keyword.".to_string()), }, AttsyncStep::Pick => return None, }) } fn on_point(&mut self, _pt: DVec3) -> CmdResult { if self.step == AttsyncStep::Pick { return CmdResult::ReportError("Error selecting entity.".to_string()); } CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { match self.step { AttsyncStep::Pick => CmdResult::Cancel, _ => self.on_text_input("").unwrap_or(CmdResult::Cancel), } } }