use super::*; impl OpenCADStudio { pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option> { match cmd { // COLOR — the colour applied to new // objects (CECOLOR). Bare COLOR reports the current value. "COLOR" | "COLOUR" | "CECOLOR" | "DDCOLOR" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "COLOR", "COLOR new object colour [ByLayer / ByBlock / 1-255 / red / yellow / green / cyan / blue / magenta / white]:", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } // BYLAYER is an immediate shortcut (sets ByLayer without prompting); // the ` ` forms and the bare-Enter getter route here too. cmd if cmd == "BYLAYER" || cmd.starts_with("COLOR ") || cmd.starts_with("COLOUR ") || cmd.starts_with("CECOLOR ") || cmd.starts_with("DDCOLOR ") => { use acadrust::types::Color; let describe = |c: &Color| match c { Color::ByLayer => "ByLayer".to_string(), Color::ByBlock => "ByBlock".to_string(), Color::Index(n) => format!("index {n}"), _ => "(custom)".to_string(), }; let arg = if cmd == "BYLAYER" { "BYLAYER".to_string() } else { cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase() }; let color = match arg.as_str() { "" => { let c = self.tabs[i].scene.document.header.current_entity_color; self.command_line .push_output(&format!("Current object colour: {}", describe(&c))); return Some(Task::none()); } "BYLAYER" => Some(Color::ByLayer), "BYBLOCK" => Some(Color::ByBlock), "RED" => Some(Color::Index(1)), "YELLOW" => Some(Color::Index(2)), "GREEN" => Some(Color::Index(3)), "CYAN" => Some(Color::Index(4)), "BLUE" => Some(Color::Index(5)), "MAGENTA" => Some(Color::Index(6)), "WHITE" => Some(Color::Index(7)), n => n.parse::().ok().map(Color::from_index), }; match color { Some(c) => { self.tabs[i].scene.document.header.current_entity_color = c; self.ribbon.active_color = c; self.tabs[i].dirty = true; self.command_line .push_output(&format!("Object colour set to {}.", describe(&c))); } None => { self.command_line.push_error( "Usage: COLOR ", ); } } } // ── LINETYPE management ─────────────────────────────────────── "LINETYPE" | "LT" => { use crate::command::KeywordCommand; let c = KeywordCommand::new( "LINETYPE", "LINETYPE [List / Set]:", vec![ ("List", "LIST", None), ("Set", "SET", Some("LINETYPE SET linetype name (ByLayer / ByBlock / …):")), ], ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("LINETYPE ") || cmd.starts_with("LT ") => { let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); let parts: Vec<&str> = raw_rest.split_whitespace().collect(); let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default(); match sub.as_str() { "" | "LIST" | "?" => { let ltypes: Vec = self.tabs[i] .scene .document .line_types .iter() .map(|lt| format!("{} ({})", lt.name, lt.description)) .collect(); if ltypes.is_empty() { self.command_line.push_output("No linetypes defined."); } else { self.command_line .push_output(&format!("Linetypes: {}", ltypes.join(", "))); } } // Set the current linetype applied to newly drawn entities. "SET" | "CURRENT" | "S" => { let name = parts.get(1).copied().unwrap_or(""); if name.is_empty() { self.command_line .push_info("Usage: LINETYPE SET "); } else { let canon = if name.eq_ignore_ascii_case("BYLAYER") { Some(("ByLayer".to_string(), acadrust::types::Handle::NULL)) } else if name.eq_ignore_ascii_case("BYBLOCK") { Some(("ByBlock".to_string(), acadrust::types::Handle::NULL)) } else { self.tabs[i] .scene .document .line_types .iter() .find(|lt| lt.name.eq_ignore_ascii_case(name)) .map(|lt| (lt.name.clone(), lt.handle)) }; match canon { Some((nm, handle)) => { let h = &mut self.tabs[i].scene.document.header; h.current_linetype_name = nm.clone(); h.current_linetype_handle = handle; self.tabs[i].dirty = true; self.command_line .push_output(&format!("Current linetype set to {nm}.")); } None => { self.command_line.push_error(&format!( "LINETYPE: \"{name}\" is not loaded. Use LINETYPE LIST to see available linetypes." )); } } } } _ => { self.command_line .push_info("Usage: LINETYPE LIST | SET "); } } } // ── PURGE unused definitions ────────────────────────────────── cmd if cmd == "PURGE" || cmd.starts_with("PURGE ") => { let sub = cmd .split_whitespace() .nth(1) .unwrap_or("ALL") .to_uppercase(); let all = sub == "ALL" || sub.is_empty(); // Collect names in use (immutable borrows — done in their own scope) let used_layers: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() .filter_map(|e| { let name = &e.common().layer; if name.is_empty() { None } else { Some(name.clone()) } }) .collect(); let used_text_styles: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() .filter_map(|e| match e { acadrust::EntityType::Text(t) => Some(t.style.clone()), acadrust::EntityType::MText(t) => Some(t.style.clone()), _ => None, }) .filter(|s| !s.is_empty()) .collect(); let used_linetypes: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() .filter_map(|e| { let lt = &e.common().linetype; if lt.is_empty() || lt == "ByLayer" || lt == "ByBlock" { None } else { Some(lt.clone()) } }) .collect(); let used_blocks: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() .filter_map(|e| match e { acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()), acadrust::EntityType::Dimension(d) => Some(d.base().block_name.clone()), _ => None, }) .filter(|s| !s.is_empty()) .collect(); // Build removal lists (still immutable) let layer_remove: Vec = if all || sub == "LAYERS" { self.tabs[i] .scene .document .layers .iter() .filter(|l| l.name != "0" && !used_layers.contains(&l.name)) .map(|l| l.name.clone()) .collect() } else { vec![] }; let style_remove: Vec = if all || sub == "TEXTSTYLES" || sub == "STYLES" { self.tabs[i] .scene .document .text_styles .iter() .filter(|s| s.name != "Standard" && !used_text_styles.contains(&s.name)) .map(|s| s.name.clone()) .collect() } else { vec![] }; let lt_remove: Vec = if all || sub == "LINETYPES" || sub == "LT" { let standard = ["Continuous", "ByLayer", "ByBlock"]; self.tabs[i] .scene .document .line_types .iter() .filter(|lt| { !standard.iter().any(|s| s.eq_ignore_ascii_case(<.name)) && !used_linetypes.contains(<.name) }) .map(|lt| lt.name.clone()) .collect() } else { vec![] }; // Blocks: definitions with no INSERT/Dimension reference. // Skip anonymous (*Model_Space/*Paper_Space/*U*/*D*…), layout, // and xref blocks — those are system- or reference-managed. let block_remove: Vec = if all || sub == "BLOCKS" { self.tabs[i] .scene .document .block_records .iter() .filter(|br| { !br.is_anonymous() && !br.is_layout() && !br.flags.is_xref && !br.flags.is_xref_overlay && !used_blocks.contains(&br.name) }) .map(|br| br.name.clone()) .collect() } else { vec![] }; // Apply removals (mutable) let purged = layer_remove.len() + style_remove.len() + lt_remove.len() + block_remove.len(); for name in &layer_remove { self.tabs[i].scene.document.layers.remove(name); } for name in &style_remove { self.tabs[i].scene.document.text_styles.remove(name); } for name in <_remove { self.tabs[i].scene.document.line_types.remove(name); } for name in &block_remove { // Drop the block definition's member entities (and the // BLOCK/ENDBLK delimiters) before the record so no orphaned // geometry survives in the document entity list. let handles: Vec<_> = if let Some(br) = self.tabs[i].scene.document.block_records.get(name) { let mut h = br.entity_handles.clone(); h.push(br.block_entity_handle); h.push(br.block_end_handle); h } else { Vec::new() }; for h in handles { self.tabs[i].scene.document.remove_entity(h); } self.tabs[i].scene.document.block_records.remove(name); } if purged > 0 { self.push_undo_snapshot(i, "PURGE"); self.tabs[i].dirty = true; // Rebuild the layer/style/linetype panel + ribbon caches so // the removed definitions disappear from the UI immediately. self.refresh_layer_panel(); // Per-type breakdown so the user sees exactly what went. let mut parts: Vec = Vec::new(); if !layer_remove.is_empty() { parts.push(format!("{} layer(s)", layer_remove.len())); } if !style_remove.is_empty() { parts.push(format!("{} text style(s)", style_remove.len())); } if !lt_remove.is_empty() { parts.push(format!("{} linetype(s)", lt_remove.len())); } if !block_remove.is_empty() { parts.push(format!("{} block(s)", block_remove.len())); } self.command_line.push_output(&format!( "PURGE: {} definition(s) removed — {}.", purged, parts.join(", ") )); } else { self.command_line.push_output("PURGE: nothing to purge."); } } // ── CHPROP — change entity properties from command line ─────── "CHPROP" => { use crate::command::SelectThenKeywordCommand; let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); let c = SelectThenKeywordCommand::new( "CHPROP", "CHPROP property [Layer / Color / Linetype / LtScale / Transparency]:", vec![ ("Layer", "LAYER", Some("CHPROP new layer name:")), ("Color", "COLOR", Some("CHPROP new colour (name / 1-255 / ByLayer):")), ("Linetype", "LINETYPE", Some("CHPROP new linetype name:")), ("LtScale", "LTSCALE", Some("CHPROP new linetype scale:")), ("Transparency", "TRANSPARENCY", Some("CHPROP transparency 0-90:")), ], has_sel, ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("CHPROP ") => { // Usage: CHPROP // Applies to currently selected entities. // Properties: LAYER, COLOR, LINETYPE, LTSCALE let parts: Vec<&str> = cmd.split_whitespace().collect(); let prop = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default(); let value = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if prop.is_empty() { self.command_line.push_info( "Usage: CHPROP (props: LAYER COLOR LINETYPE LTSCALE)", ); } else { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if handles.is_empty() { self.command_line .push_error("CHPROP: no entities selected."); } else { // Validate value early to give clear errors let color_val: Option = if prop == "COLOR" { value .parse::() .ok() .map(acadrust::types::Color::from_index) } else { None }; let ltscale_val: Option = if prop == "LTSCALE" { value.parse().ok() } else { None }; let transparency_val: Option = if prop == "TRANSPARENCY" { value .parse::() .ok() .map(acadrust::types::Transparency::from_percent) } else { None }; if (prop == "COLOR" && color_val.is_none()) || (prop == "LTSCALE" && ltscale_val.is_none()) || (prop == "TRANSPARENCY" && transparency_val.is_none()) { self.command_line.push_error(&format!( "CHPROP: invalid value '{}' for {}.", value, prop )); } else { let mut changed = 0usize; for handle in &handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) { let common = entity.common_mut(); match prop.as_str() { "LAYER" => { common.layer = value.clone(); changed += 1; } "LINETYPE" | "LT" => { common.linetype = value.clone(); changed += 1; } "LTSCALE" => { common.linetype_scale = ltscale_val.unwrap(); changed += 1; } "COLOR" => { common.color = color_val.unwrap(); changed += 1; } "TRANSPARENCY" => { common.transparency = transparency_val.unwrap(); changed += 1; } _ => { self.command_line.push_error(&format!( "CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop )); break; } } } } if changed > 0 { self.push_undo_snapshot(i, "CHPROP"); self.tabs[i].dirty = true; // Colour / linetype / ltscale / transparency / // layer are baked into the cached wire geometry — // re-tessellate the changed entities so they // repaint immediately (issue #231 class). self.invalidate_property_targets(i, &handles); self.command_line.push_output(&format!( "CHPROP: {} entity/entities updated.", changed )); } } } } } // ── SETBYLAYER — clear color/linetype/lineweight overrides ──── // Resets the selected entities' direct property overrides back to // ByLayer so they follow their layer again. "SETBYLAYER" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if handles.is_empty() { self.command_line .push_error("SETBYLAYER: select entities first."); } else { self.push_undo_snapshot(i, "SETBYLAYER"); let mut changed = 0usize; for handle in &handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) { let common = entity.common_mut(); common.color = acadrust::types::Color::ByLayer; common.linetype = "ByLayer".to_string(); common.line_weight = acadrust::types::LineWeight::ByLayer; changed += 1; } } self.tabs[i].dirty = true; self.tabs[i].scene.bump_geometry(); self.command_line.push_output(&format!( "SETBYLAYER: reset {changed} entity/entities to ByLayer." )); } } // ── OVERKILL — delete duplicate (identical) objects ────────── // Removes objects that are identical in geometry AND properties to // another object (compared with the handle ignored). Operates on // the current selection, or the whole drawing when nothing is // selected. Conservative: only exact duplicates are removed. "OVERKILL" => { use acadrust::Handle; let selected: std::collections::HashSet = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h.value()) .collect(); // Capture (handle, type-name, handle-normalized clone) for each // candidate while the document is borrowed immutably. let candidates: Vec<(Handle, String, acadrust::EntityType)> = self.tabs[i] .scene .document .entities() .filter(|e| { selected.is_empty() || selected.contains(&e.common().handle.value()) }) .map(|e| { let key = crate::entities::names::dxf_name(e).to_string(); let mut norm = e.clone(); norm.common_mut().handle = Handle::NULL; (e.common().handle, key, norm) }) .collect(); // Bucket by (type, layer) so only like objects are compared. let mut kept: Vec<(String, acadrust::EntityType)> = Vec::new(); let mut dups: Vec = Vec::new(); for (h, key, norm) in &candidates { let bucket = format!("{key}\u{0}{}", norm.common().layer); if kept.iter().any(|(b, e)| b == &bucket && e == norm) { dups.push(*h); } else { kept.push((bucket, norm.clone())); } } if dups.is_empty() { self.command_line .push_output("OVERKILL: no duplicate objects found."); } else { let n = dups.len(); self.push_undo_snapshot(i, "OVERKILL"); self.tabs[i].scene.erase_entities(&dups); self.tabs[i].dirty = true; self.refresh_properties(); self.command_line .push_output(&format!("OVERKILL: deleted {n} duplicate object(s).")); } } // ── SETVAR — read / write system variables ─────────────────── // SETVAR → report the value // SETVAR → set it // SETVAR ? → list supported variables // Numeric / boolean variables are settable; current-layer/linetype/ // style names are read-only here (use their own commands to change // them, which validate the name). // System variables typeable directly (e.g. `MIRRTEXT 1`) as well as // through SETVAR. (LTSCALE/PDMODE/PDSIZE have their own commands.) cmd if matches!( cmd.split_whitespace().next().unwrap_or(""), "MIRRTEXT" | "TEXTFILL" | "ATTREQ" | "ATTDIA" | "DIMASSOC" | "ANGBASE" | "ANGDIR" | "REGENMODE" | "BLIPMODE" | "SPLFRAME" | "DELOBJ" | "PLINEGEN" | "PSLTSCALE" | "DISPSILH" | "WORLDVIEW" | "LIMCHECK" | "DRAGMODE" | "LUNITS" | "LUPREC" | "AUNITS" | "AUPREC" | "THICKNESS" | "ELEVATION" | "INSUNITS" | "SPLINETYPE" | "ISOLINES" | "DIMASO" | "DIMSHO" | "QTEXTMODE" | "PLIMCHECK" | "VISRETAIN" | "USRTIMER" | "ATTMODE" | "COORDS" | "OSMODE" | "PICKSTYLE" | "SPLINESEGS" | "SURFU" | "SURFV" | "SURFTYPE" | "SHADEDGE" | "MAXACTVP" | "CMLJUST" | "TEXTQLTY" | "SORTENTS" | "XCLIPFRAME" | "HALOGAP" | "TRACEWID" | "SKETCHINC" ) => { return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); } "SETVAR" => { use crate::command::TwoValuePromptCommand; let c = TwoValuePromptCommand::new( "SETVAR", "SETVAR variable name:", "SETVAR new value (blank to read):", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("SETVAR ") => { let rest = cmd.strip_prefix("SETVAR").unwrap_or("").trim(); let mut it = rest.splitn(2, char::is_whitespace); let name = it.next().unwrap_or("").to_uppercase(); let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)", ); } else { // Parse a boolean given as 0/1 or ON/OFF. let parse_bool = |s: &str| match s.to_uppercase().as_str() { "1" | "ON" | "TRUE" => Some(true), "0" | "OFF" | "FALSE" => Some(false), _ => None, }; let outcome: Result<(String, bool), String> = { let h = &mut self.tabs[i].scene.document.header; match name.as_str() { "LTSCALE" => match &value { Some(v) => v .parse::() .map(|x| { h.linetype_scale = x; (format!("LTSCALE = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("LTSCALE = {}", h.linetype_scale), false)), }, "CELTSCALE" => match &value { Some(v) => v .parse::() .map(|x| { h.current_entity_linetype_scale = x; (format!("CELTSCALE = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok(( format!("CELTSCALE = {}", h.current_entity_linetype_scale), false, )), }, "PDSIZE" => match &value { Some(v) => v .parse::() .map(|x| { h.point_display_size = x; (format!("PDSIZE = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("PDSIZE = {}", h.point_display_size), false)), }, "TEXTSIZE" => match &value { Some(v) => v .parse::() .map(|x| { h.text_height = x; (format!("TEXTSIZE = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("TEXTSIZE = {}", h.text_height), false)), }, "PDMODE" => match &value { Some(v) => v .parse::() .map(|x| { h.point_display_mode = x; (format!("PDMODE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("PDMODE = {}", h.point_display_mode), false)), }, "ORTHOMODE" => match &value { Some(v) => parse_bool(v) .map(|b| { h.ortho_mode = b; (format!("ORTHOMODE = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("ORTHOMODE = {}", h.ortho_mode as i32), false)), }, "FILLMODE" => match &value { Some(v) => parse_bool(v) .map(|b| { h.fill_mode = b; (format!("FILLMODE = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("FILLMODE = {}", h.fill_mode as i32), false)), }, "MIRRTEXT" => match &value { Some(v) => parse_bool(v) .map(|b| { h.mirror_text = b; (format!("MIRRTEXT = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("MIRRTEXT = {}", h.mirror_text as i32), false)), }, // Global (not stored in the drawing): fill vs. hollow // TrueType text. The active tab re-tessellates below. "TEXTFILL" => match &value { Some(v) => parse_bool(v) .map(|b| { crate::scene::text::sdf_atlas::set_textfill(b); (format!("TEXTFILL = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!( "TEXTFILL = {}", crate::scene::text::sdf_atlas::textfill() as i32 ), false, )), }, "ATTREQ" => match &value { Some(v) => parse_bool(v) .map(|b| { h.attribute_request = b; (format!("ATTREQ = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => { Ok((format!("ATTREQ = {}", h.attribute_request as i32), false)) } }, "ATTDIA" => match &value { Some(v) => parse_bool(v) .map(|b| { h.attribute_dialog = b; (format!("ATTDIA = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => { Ok((format!("ATTDIA = {}", h.attribute_dialog as i32), false)) } }, "DIMASSOC" => match &value { Some(v) => v .parse::() .map(|x| { h.dimension_associativity = x; (format!("DIMASSOC = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => { Ok((format!("DIMASSOC = {}", h.dimension_associativity), false)) } }, "ANGBASE" => match &value { Some(v) => v .parse::() .map(|x| { h.angle_base = x; (format!("ANGBASE = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("ANGBASE = {}", h.angle_base), false)), }, "ANGDIR" => match &value { Some(v) => v .parse::() .map(|x| { h.angle_direction = x; (format!("ANGDIR = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("ANGDIR = {}", h.angle_direction), false)), }, "REGENMODE" => match &value { Some(v) => parse_bool(v) .map(|b| { h.regen_mode = b; (format!("REGENMODE = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("REGENMODE = {}", h.regen_mode as i32), false)), }, "BLIPMODE" => match &value { Some(v) => parse_bool(v) .map(|b| { h.blip_mode = b; (format!("BLIPMODE = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("BLIPMODE = {}", h.blip_mode as i32), false)), }, "SPLFRAME" => match &value { Some(v) => parse_bool(v) .map(|b| { h.spline_frame = b; (format!("SPLFRAME = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => { Ok((format!("SPLFRAME = {}", h.spline_frame as i32), false)) } }, "DELOBJ" => match &value { Some(v) => parse_bool(v) .map(|b| { h.delete_objects = b; (format!("DELOBJ = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => { Ok((format!("DELOBJ = {}", h.delete_objects as i32), false)) } }, "PLINEGEN" => match &value { Some(v) => parse_bool(v) .map(|b| { h.polyline_linetype_generation = b; (format!("PLINEGEN = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!("PLINEGEN = {}", h.polyline_linetype_generation as i32), false, )), }, "PSLTSCALE" => match &value { Some(v) => parse_bool(v) .map(|b| { h.paper_space_linetype_scaling = b; (format!("PSLTSCALE = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!( "PSLTSCALE = {}", h.paper_space_linetype_scaling as i32 ), false, )), }, "DISPSILH" => match &value { Some(v) => parse_bool(v) .map(|b| { h.display_silhouette = b; (format!("DISPSILH = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!("DISPSILH = {}", h.display_silhouette as i32), false, )), }, "WORLDVIEW" => match &value { Some(v) => parse_bool(v) .map(|b| { h.world_view = b; (format!("WORLDVIEW = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("WORLDVIEW = {}", h.world_view as i32), false)), }, "LIMCHECK" => match &value { Some(v) => parse_bool(v) .map(|b| { h.limit_check = b; (format!("LIMCHECK = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("LIMCHECK = {}", h.limit_check as i32), false)), }, "DRAGMODE" => match &value { Some(v) => v .parse::() .map(|x| { h.drag_mode = x; (format!("DRAGMODE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("DRAGMODE = {}", h.drag_mode), false)), }, "LUNITS" => match &value { Some(v) => v .parse::() .map(|x| { h.linear_unit_format = x; (format!("LUNITS = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("LUNITS = {}", h.linear_unit_format), false)), }, "LUPREC" => match &value { Some(v) => v .parse::() .map(|x| { h.linear_unit_precision = x; (format!("LUPREC = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => { Ok((format!("LUPREC = {}", h.linear_unit_precision), false)) } }, "AUNITS" => match &value { Some(v) => v .parse::() .map(|x| { h.angular_unit_format = x; (format!("AUNITS = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("AUNITS = {}", h.angular_unit_format), false)), }, "AUPREC" => match &value { Some(v) => v .parse::() .map(|x| { h.angular_unit_precision = x; (format!("AUPREC = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => { Ok((format!("AUPREC = {}", h.angular_unit_precision), false)) } }, "THICKNESS" => match &value { Some(v) => v .parse::() .map(|x| { h.thickness = x; (format!("THICKNESS = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("THICKNESS = {}", h.thickness), false)), }, "ELEVATION" => match &value { Some(v) => v .parse::() .map(|x| { h.elevation = x; (format!("ELEVATION = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("ELEVATION = {}", h.elevation), false)), }, "INSUNITS" => match &value { Some(v) => v .parse::() .map(|x| { h.insertion_units = x; (format!("INSUNITS = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("INSUNITS = {}", h.insertion_units), false)), }, "SPLINETYPE" => match &value { Some(v) => v .parse::() .map(|x| { h.spline_type = x; (format!("SPLINETYPE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SPLINETYPE = {}", h.spline_type), false)), }, "ISOLINES" => match &value { Some(v) => v .parse::() .map(|x| { h.isolines = x; (format!("ISOLINES = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("ISOLINES = {}", h.isolines), false)), }, "DIMASO" => match &value { Some(v) => parse_bool(v) .map(|b| { h.associate_dimensions = b; (format!("DIMASO = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!("DIMASO = {}", h.associate_dimensions as i32), false, )), }, "DIMSHO" => match &value { Some(v) => parse_bool(v) .map(|b| { h.update_dimensions_while_dragging = b; (format!("DIMSHO = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!( "DIMSHO = {}", h.update_dimensions_while_dragging as i32 ), false, )), }, "QTEXTMODE" => match &value { Some(v) => parse_bool(v) .map(|b| { h.quick_text_mode = b; (format!("QTEXTMODE = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => { Ok((format!("QTEXTMODE = {}", h.quick_text_mode as i32), false)) } }, "PLIMCHECK" => match &value { Some(v) => parse_bool(v) .map(|b| { h.paper_space_limit_check = b; (format!("PLIMCHECK = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!("PLIMCHECK = {}", h.paper_space_limit_check as i32), false, )), }, "VISRETAIN" => match &value { Some(v) => parse_bool(v) .map(|b| { h.retain_xref_visibility = b; (format!("VISRETAIN = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok(( format!("VISRETAIN = {}", h.retain_xref_visibility as i32), false, )), }, "USRTIMER" => match &value { Some(v) => parse_bool(v) .map(|b| { h.user_timer = b; (format!("USRTIMER = {}", b as i32), true) }) .ok_or_else(|| "SETVAR: 0 or 1 required.".into()), None => Ok((format!("USRTIMER = {}", h.user_timer as i32), false)), }, "ATTMODE" => match &value { Some(v) => v .parse::() .map(|x| { h.attribute_visibility = x; (format!("ATTMODE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => { Ok((format!("ATTMODE = {}", h.attribute_visibility), false)) } }, "COORDS" => match &value { Some(v) => v .parse::() .map(|x| { h.coords_mode = x; (format!("COORDS = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("COORDS = {}", h.coords_mode), false)), }, "OSMODE" => match &value { Some(v) => v .parse::() .map(|x| { h.object_snap_mode = x; (format!("OSMODE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("OSMODE = {}", h.object_snap_mode), false)), }, "PICKSTYLE" => match &value { Some(v) => v .parse::() .map(|x| { h.pick_style = x; (format!("PICKSTYLE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("PICKSTYLE = {}", h.pick_style), false)), }, "SPLINESEGS" => match &value { Some(v) => v .parse::() .map(|x| { h.spline_segments = x; (format!("SPLINESEGS = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SPLINESEGS = {}", h.spline_segments), false)), }, "SURFU" => match &value { Some(v) => v .parse::() .map(|x| { h.surface_u_density = x; (format!("SURFU = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SURFU = {}", h.surface_u_density), false)), }, "SURFV" => match &value { Some(v) => v .parse::() .map(|x| { h.surface_v_density = x; (format!("SURFV = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SURFV = {}", h.surface_v_density), false)), }, "SURFTYPE" => match &value { Some(v) => v .parse::() .map(|x| { h.surface_type = x; (format!("SURFTYPE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SURFTYPE = {}", h.surface_type), false)), }, "SHADEDGE" => match &value { Some(v) => v .parse::() .map(|x| { h.shade_edge = x; (format!("SHADEDGE = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SHADEDGE = {}", h.shade_edge), false)), }, "MAXACTVP" => match &value { Some(v) => v .parse::() .map(|x| { h.max_active_viewports = x; (format!("MAXACTVP = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => { Ok((format!("MAXACTVP = {}", h.max_active_viewports), false)) } }, "CMLJUST" => match &value { Some(v) => v .parse::() .map(|x| { h.multiline_justification = x; (format!("CMLJUST = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => { Ok((format!("CMLJUST = {}", h.multiline_justification), false)) } }, "TEXTQLTY" => match &value { Some(v) => v .parse::() .map(|x| { h.text_quality = x; (format!("TEXTQLTY = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("TEXTQLTY = {}", h.text_quality), false)), }, "SORTENTS" => match &value { Some(v) => v .parse::() .map(|x| { h.sort_entities = x; (format!("SORTENTS = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SORTENTS = {}", h.sort_entities), false)), }, "XCLIPFRAME" => match &value { Some(v) => v .parse::() .map(|x| { h.xclip_frame = x; (format!("XCLIPFRAME = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("XCLIPFRAME = {}", h.xclip_frame), false)), }, "HALOGAP" => match &value { Some(v) => v .parse::() .map(|x| { h.halo_gap = x; (format!("HALOGAP = {x}"), true) }) .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("HALOGAP = {}", h.halo_gap), false)), }, "TRACEWID" => match &value { Some(v) => v .parse::() .map(|x| { h.trace_width = x; (format!("TRACEWID = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("TRACEWID = {}", h.trace_width), false)), }, "SKETCHINC" => match &value { Some(v) => v .parse::() .map(|x| { h.sketch_increment = x; (format!("SKETCHINC = {x}"), true) }) .map_err(|_| "SETVAR: numeric value required.".into()), None => Ok((format!("SKETCHINC = {}", h.sketch_increment), false)), }, "CLAYER" => match &value { Some(_) => Err( "SETVAR: CLAYER is read-only here — use the CLAYER command." .into(), ), None => Ok((format!("CLAYER = {}", h.current_layer_name), false)), }, "CELTYPE" => match &value { Some(_) => { Err("SETVAR: CELTYPE is read-only here — use LINETYPE SET." .into()) } None => { Ok((format!("CELTYPE = {}", h.current_linetype_name), false)) } }, "TEXTSTYLE" => match &value { Some(_) => Err( "SETVAR: TEXTSTYLE is read-only here — use the STYLE command." .into(), ), None => Ok(( format!("TEXTSTYLE = {}", h.current_text_style_name), false, )), }, _ => Err(format!("SETVAR: unknown variable \"{name}\".")), } }; match outcome { Ok((msg, changed)) => { if changed { self.tabs[i].dirty = true; self.command_line.push_output(&msg); } else { // Queried with no value (`changed == false`): // prompt for a new value on the next line instead // of only echoing the current one. Enter keeps it. let current = msg.split('=').nth(1).map(str::trim).unwrap_or(""); self.command_line.push_output(&format!( "Enter new value for {name} <{current}>:" )); self.pending_setvar = Some(name.clone()); } } Err(e) => self.command_line.push_error(&e), } // TEXTFILL reset the glyph atlas; re-tessellate so text picks // up the re-baked filled / hollow tiles. if name == "TEXTFILL" { self.tabs[i].scene.bump_geometry(); } // ORTHOMODE / OSMODE set the header directly; mirror them into // the live Ortho / running OSNAP so the constraint + status // bar follow and the save-time stamp doesn't revert them. if name == "ORTHOMODE" { self.ortho_mode = self.tabs[i].scene.document.header.ortho_mode; if self.ortho_mode { self.polar_mode = false; } } if name == "OSMODE" { let (modes, en) = crate::app::settings::snaps_from_osmode( self.tabs[i].scene.document.header.object_snap_mode, ); self.snapper.enabled = modes.into_iter().collect(); self.snapper.snap_enabled = en; // The OSNAP set is app-level (no modern-DWG file slot). self.persist_settings_if_changed(); } } } // ── FINDNONPURGEABLE — list named objects still in use ─────── // Reports the layers, linetypes, text styles and blocks that are // referenced by objects, and therefore cannot be purged — so it is // clear why PURGE leaves them behind. Read-only. "FINDNONPURGEABLE" => { use rustc_hash::FxHashSet; let mut layers: FxHashSet = FxHashSet::default(); let mut linetypes: FxHashSet = FxHashSet::default(); let mut styles: FxHashSet = FxHashSet::default(); let mut blocks: FxHashSet = FxHashSet::default(); { let doc = &self.tabs[i].scene.document; for e in doc.entities() { let l = &e.common().layer; if !l.is_empty() { layers.insert(l.clone()); } let lt = &e.common().linetype; if !lt.is_empty() && lt != "ByLayer" && lt != "ByBlock" { linetypes.insert(lt.clone()); } match e { acadrust::EntityType::Text(t) if !t.style.is_empty() => { styles.insert(t.style.clone()); } acadrust::EntityType::MText(t) if !t.style.is_empty() => { styles.insert(t.style.clone()); } acadrust::EntityType::Insert(ins) => { blocks.insert(ins.block_name.clone()); } _ => {} } } } let fmt = |set: FxHashSet| -> String { if set.is_empty() { "(none)".to_string() } else { let mut v: Vec<_> = set.into_iter().collect(); v.sort(); v.join(", ") } }; self.command_line .push_output("FINDNONPURGEABLE: named objects in use (not purgeable):"); self.command_line .push_output(&format!(" Layers: {}", fmt(layers))); self.command_line .push_output(&format!(" Linetypes: {}", fmt(linetypes))); self.command_line .push_output(&format!(" Text styles: {}", fmt(styles))); self.command_line .push_output(&format!(" Blocks: {}", fmt(blocks))); } // ── AUDIT — report drawing-database integrity issues ───────── // Read-only scan: flags entities on undefined layers and block // references to undefined block definitions. Reports only; it does // not auto-repair (so it can never make the drawing worse). "AUDIT" => { use std::collections::BTreeSet; let mut undefined_layers: BTreeSet = BTreeSet::new(); let mut undefined_blocks: BTreeSet = BTreeSet::new(); let mut total = 0usize; { let doc = &self.tabs[i].scene.document; for e in doc.entities() { total += 1; let layer = &e.common().layer; if !layer.is_empty() && doc.layers.get(layer).is_none() { undefined_layers.insert(layer.clone()); } if let acadrust::EntityType::Insert(ins) = e { if doc.block_records.get(&ins.block_name).is_none() { undefined_blocks.insert(ins.block_name.clone()); } } } } self.command_line .push_output(&format!("AUDIT: scanned {total} object(s).")); if undefined_layers.is_empty() && undefined_blocks.is_empty() { self.command_line.push_output("AUDIT: no issues found."); } else { if !undefined_layers.is_empty() { self.command_line.push_error(&format!( "AUDIT: reference(s) to undefined layer(s): {}", undefined_layers.into_iter().collect::>().join(", ") )); } if !undefined_blocks.is_empty() { self.command_line.push_error(&format!( "AUDIT: reference(s) to undefined block(s): {}", undefined_blocks.into_iter().collect::>().join(", ") )); } self.command_line .push_info("AUDIT: report only — no automatic repair performed."); } } // ── RENAME table entries ────────────────────────────────────── // Bare RENAME prompts step by step (type → old → new); the argument // form below both handles a fully-typed line and is the target the // stepped front-end dispatches to. "RENAME" => { use crate::command::RenameCommand; let c = RenameCommand::new(); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("RENAME ") => { // Usage: RENAME // Types: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW let parts: Vec<&str> = cmd.split_whitespace().collect(); let type_str = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default(); let old_name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); let new_name = parts.get(3).map(|s| s.trim()).unwrap_or("").to_string(); if type_str.is_empty() || old_name.is_empty() || new_name.is_empty() { self.command_line.push_info( "Usage: RENAME (types: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW)" ); } else { let doc = &mut self.tabs[i].scene.document; let ok = match type_str.as_str() { "LAYER" => { if let Some(l) = doc.layers.get_mut(&old_name) { l.name = new_name.clone(); // Update entity references for e in doc.entities_mut() { if e.common().layer == old_name { e.common_mut().layer = new_name.clone(); } } true } else { false } } "STYLE" | "TEXTSTYLE" => { if let Some(s) = doc.text_styles.get_mut(&old_name) { s.name = new_name.clone(); true } else { false } } "DIMSTYLE" => { if let Some(s) = doc.dim_styles.get_mut(&old_name) { s.name = new_name.clone(); true } else { false } } "LINETYPE" | "LT" => { if let Some(lt) = doc.line_types.get_mut(&old_name) { lt.name = new_name.clone(); true } else { false } } "UCS" => { if let Some(u) = doc.ucss.get_mut(&old_name) { u.name = new_name.clone(); true } else { false } } "VIEW" => { if let Some(v) = doc.views.get_mut(&old_name) { v.name = new_name.clone(); true } else { false } } _ => { self.command_line.push_error(&format!("RENAME: unknown type '{}'. Use LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW", type_str)); false } }; if ok { self.push_undo_snapshot(i, "RENAME"); self.tabs[i].dirty = true; self.command_line .push_output(&format!("RENAME: '{}' → '{}'.", old_name, new_name)); } else if type_str != "BLOCK" { self.command_line.push_error(&format!( "RENAME: '{}' not found in {}.", old_name, type_str )); } } } // ── System variable getters/setters ────────────────────────────────── // CLAYER [name] — get or set current layer // TEXTSTYLE [name] — already handled above under STYLE SET // DIMSTYLE [name] — get or set active dim style // LTSCALE [val] — global linetype scale "CLAYER" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new("CLAYER", "CLAYER new current layer name:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("CLAYER ") => { let name_arg = cmd.trim_start_matches("CLAYER").trim(); if name_arg.is_empty() { let cur = &self.tabs[i].scene.document.header.current_layer_name; self.command_line .push_output(&format!("CLAYER = \"{cur}\"")); } else { if self.tabs[i].scene.document.layers.contains(name_arg) { self.tabs[i].scene.document.header.current_layer_name = name_arg.to_string(); self.tabs[i].dirty = true; self.command_line .push_output(&format!("CLAYER set to \"{name_arg}\"")); } else { self.command_line .push_error(&format!("CLAYER: layer '{}' not found.", name_arg)); } } } "CDIMSTY" | "DIMCURRENT" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new("CDIMSTY", "CDIMSTY new current dimension style name:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("CDIMSTY ") || cmd.starts_with("DIMCURRENT ") => { let name_arg = cmd.split_whitespace().skip(1).collect::>().join(" "); if name_arg.is_empty() { let cur = &self.tabs[i].scene.document.header.current_dimstyle_name; self.command_line .push_output(&format!("CDIMSTY = \"{cur}\"")); } else { if self.tabs[i].scene.document.dim_styles.contains(&name_arg) { self.tabs[i].scene.document.header.current_dimstyle_name = name_arg.clone(); self.tabs[i].dirty = true; self.command_line .push_output(&format!("Active dim style set to \"{name_arg}\"")); } else { self.command_line .push_error(&format!("CDIMSTY: dim style '{}' not found.", name_arg)); } } } "LTSCALE" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new("LTSCALE", "LTSCALE new global line-type scale:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("LTSCALE ") => { let val_str = cmd.trim_start_matches("LTSCALE").trim(); if val_str.is_empty() { let v = self.tabs[i].scene.document.header.linetype_scale; self.command_line.push_output(&format!("LTSCALE = {v:.4}")); } else if let Ok(v) = val_str.parse::() { if v > 0.0 { self.push_undo_snapshot(i, "LTSCALE"); self.tabs[i].scene.document.header.linetype_scale = v; self.tabs[i].dirty = true; self.command_line .push_output(&format!("LTSCALE set to {v:.4}")); } else { self.command_line .push_error("LTSCALE: value must be positive."); } } else { self.command_line.push_error("Usage: LTSCALE [value]"); } } "PDMODE" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "PDMODE", "PDMODE new value [0=dot 1=none 2=+ 3=x 4=tick; +32 circle +64 square]:", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("PDMODE ") => { let val_str = cmd.trim_start_matches("PDMODE").trim(); if val_str.is_empty() { let v = self.tabs[i].scene.document.header.point_display_mode; self.command_line.push_output(&format!("PDMODE = {v}")); } else if let Ok(v) = val_str.parse::() { self.push_undo_snapshot(i, "PDMODE"); self.tabs[i].scene.document.header.point_display_mode = v; // Point glyphs are built at tessellation time — rebuild them. self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; self.command_line.push_output(&format!("PDMODE set to {v}")); } else { self.command_line.push_error( "Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)", ); } } cmd if cmd.starts_with("TEXTEDITMODE ") => { let val_str = cmd.trim_start_matches("TEXTEDITMODE").trim().to_lowercase(); if val_str.is_empty() { let v = if self.texteditmode { 1 } else { 0 }; self.command_line .push_output(&format!("TEXTEDITMODE = {v}")); } else if let Some(v) = crate::modules::annotate::textedit::parse_texteditmode(&val_str) { self.texteditmode = v; let n = if v { 1 } else { 0 }; self.command_line .push_output(&format!("TEXTEDITMODE set to {n}")); } else { self.command_line .push_error("Requires 0 OR 1 OR MULTIPLE OR SINGLE"); } } "ISAVEBAK" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new("ISAVEBAK", "ISAVEBAK write a .bak on save? [1 / 0]:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("ISAVEBAK ") => { match cmd.trim_start_matches("ISAVEBAK").trim() { "" => { let v = if self.backup_on_save { 1 } else { 0 }; self.command_line.push_output(&format!("ISAVEBAK = {v}")); } "0" => { self.backup_on_save = false; self.persist_settings_if_changed(); self.command_line.push_output("ISAVEBAK set to 0"); } "1" => { self.backup_on_save = true; self.persist_settings_if_changed(); self.command_line.push_output("ISAVEBAK set to 1"); } _ => self.command_line.push_error("Requires 0 or 1"), } } "FILEASSOC" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "FILEASSOC", "FILEASSOC register as a .dwg/.dxf handler? [1 / 0]:", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("FILEASSOC ") => { match cmd.trim_start_matches("FILEASSOC").trim() { "" => { let v = if self.file_assoc_enabled { 1 } else { 0 }; self.command_line.push_output(&format!("FILEASSOC = {v}")); } "1" => { self.file_assoc_enabled = true; self.persist_settings_if_changed(); match crate::io::file_association::register_as_handler() { Ok(()) => self.command_line.push_output( "FILEASSOC set to 1 — registered as a .dwg/.dxf/.bak handler", ), Err(e) => self .command_line .push_error(&format!("FILEASSOC: registration failed: {e}")), } } "0" => { self.file_assoc_enabled = false; self.persist_settings_if_changed(); match crate::io::file_association::unregister_handler() { Ok(()) => self .command_line .push_output("FILEASSOC set to 0 — unregistered as a file handler"), Err(e) => self .command_line .push_error(&format!("FILEASSOC: unregister failed: {e}")), } } _ => self.command_line.push_error("Requires 0 or 1"), } } "SAVETIME" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "SAVETIME", "SAVETIME autosave interval in minutes (0 = off):", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("SAVETIME ") => { let arg = cmd.trim_start_matches("SAVETIME").trim(); if arg.is_empty() { self.command_line .push_output(&format!("SAVETIME = {}", self.savetime_min)); return Some(Task::none()); } match arg.parse::() { Ok(v) if v >= 0 => { self.savetime_min = v; self.persist_settings_if_changed(); let msg = if v == 0 { "SAVETIME set to 0 (autosave off)".to_string() } else { format!("SAVETIME set to {v} minute(s)") }; self.command_line.push_output(&msg); } _ => self .command_line .push_error("Requires a non-negative number of minutes (0 = off)"), } } "PDSIZE" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "PDSIZE", "PDSIZE new point size (0 = 5% of viewport, <0 = absolute):", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("PDSIZE ") => { let val_str = cmd.trim_start_matches("PDSIZE").trim(); if val_str.is_empty() { let v = self.tabs[i].scene.document.header.point_display_size; self.command_line.push_output(&format!("PDSIZE = {v:.4}")); } else if let Ok(v) = val_str.parse::() { self.push_undo_snapshot(i, "PDSIZE"); self.tabs[i].scene.document.header.point_display_size = v; self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; self.command_line .push_output(&format!("PDSIZE set to {v:.4}")); } else { self.command_line.push_error( "Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)", ); } } cmd if cmd == "DDPTYPE" => { // The dialog shows the magnitude; the sign (relative/absolute) // is driven by the radio buttons. A positive PDSIZE is absolute; // zero or negative is relative. let pdsize = self.tabs[i].scene.document.header.point_display_size; self.point_size_relative = pdsize <= 0.0; self.point_size_buf = format!("{}", pdsize.abs()); self.active_modal = Some(super::super::ModalKind::PointStyle); } "LWDISPLAY" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new("LWDISPLAY", "LWDISPLAY show lineweights? [ON / OFF]:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("LWDISPLAY ") => { let val_str = cmd.trim_start_matches("LWDISPLAY").trim(); let parsed: Result, ()> = match val_str.to_ascii_uppercase().as_str() { "" => Ok(None), "ON" | "1" | "TRUE" => Ok(Some(true)), "OFF" | "0" | "FALSE" => Ok(Some(false)), _ => Err(()), }; match parsed { Err(_) => self.command_line.push_error("Usage: LWDISPLAY [ON|OFF]"), Ok(Some(v)) => { self.push_undo_snapshot(i, "LWDISPLAY"); self.tabs[i].scene.document.header.lineweight_display = v; // No retessellate — the wire shader honours the flag via uniforms. self.tabs[i].dirty = true; self.command_line .push_output(&format!("LWDISPLAY {}", if v { "ON" } else { "OFF" })); } Ok(None) => { let v = self.tabs[i].scene.document.header.lineweight_display; self.command_line .push_output(&format!("LWDISPLAY = {}", if v { "ON" } else { "OFF" })); } } } "CELTSCALE" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "CELTSCALE", "CELTSCALE new current-object line-type scale:", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("CELTSCALE ") => { let val_str = cmd.trim_start_matches("CELTSCALE").trim(); if val_str.is_empty() { let v = self.tabs[i] .scene .document .header .current_entity_linetype_scale; self.command_line .push_output(&format!("CELTSCALE = {v:.4}")); } else if let Ok(v) = val_str.parse::() { if v > 0.0 { self.tabs[i] .scene .document .header .current_entity_linetype_scale = v; self.tabs[i].dirty = true; self.command_line .push_output(&format!("CELTSCALE set to {v:.4}")); } else { self.command_line .push_error("CELTSCALE: value must be positive."); } } else { self.command_line.push_error("Usage: CELTSCALE [value]"); } } // ── SCALETEXT — rescale selected Text/MText entities ───────────────── // Usage: SCALETEXT e.g. SCALETEXT 2 // SCALETEXT H set absolute height "SCALETEXT" => { use crate::command::SelectThenValueCommand; let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); let c = SelectThenValueCommand::new( "SCALETEXT", "SCALETEXT scale factor (or 'H ' for an absolute height):", has_sel, ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("SCALETEXT ") => { let rest = cmd.trim_start_matches("SCALETEXT").trim(); let parts: Vec<&str> = rest.split_whitespace().collect(); let selected_handles: Vec = self.tabs[i] .scene .selected_entities() .iter() .map(|(h, _)| *h) .collect(); if selected_handles.is_empty() { self.command_line .push_error("SCALETEXT: select Text/MText entities first."); } else { let (use_absolute, value) = match ( parts.first().map(|s| s.to_uppercase()).as_deref(), parts.get(1), ) { (Some("H"), Some(v)) => (true, v.parse::().ok()), (Some(v), None) => (false, v.parse::().ok()), _ => (false, None), }; if let Some(val) = value { if val <= 0.0 { self.command_line .push_error("SCALETEXT: value must be positive."); } else { self.push_undo_snapshot(i, "SCALETEXT"); let mut count = 0usize; for sh in &selected_handles { for entity in self.tabs[i].scene.document.entities_mut() { if entity.common().handle != *sh { continue; } match entity { acadrust::EntityType::Text(t) => { t.height = if use_absolute { val } else { t.height * val }; count += 1; } acadrust::EntityType::MText(t) => { t.height = if use_absolute { val } else { t.height * val }; count += 1; } _ => {} } break; } } if count > 0 { self.tabs[i].dirty = true; self.command_line.push_output(&format!( "SCALETEXT: scaled {count} text entity(ies)." )); } else { self.command_line .push_error("SCALETEXT: no Text/MText in selection."); } } } else { self.command_line .push_info("Usage: SCALETEXT or SCALETEXT H "); } } } _ => return None, } Some(self.finish_dispatch(cmd)) } }