Files
OneCAD-Draft/src/app/commands/styleprops.rs
T
Hakan Seven 44efb8fd53 feat(frame): complete frame controls
Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities.

Refs #642
2026-08-20 23:37:02 +03:00

2535 lines
132 KiB
Rust

use super::*;
impl OpenCADStudio {
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"FRAMES0" => return self.dispatch_styleprops("SETVAR FRAME 0", i),
"FRAMES1" => return self.dispatch_styleprops("SETVAR FRAME 1", i),
"FRAMES2" => return self.dispatch_styleprops("SETVAR FRAME 2", i),
// COLOR <ByLayer|ByBlock|1-255|name> — 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 `<name> <value>` 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(crate::tf!("Current object colour: {}", describe(&c)).as_ref());
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::<i16>().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(crate::tf!("Object colour set to {}.", describe(&c)).as_ref());
}
None => {
self.command_line.push_error(
"Usage: COLOR <ByLayer|ByBlock|1-255|red|yellow|green|cyan|blue|magenta|white>",
);
}
}
}
// ── 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<String> = 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(crate::t!("No linetypes defined.").as_ref());
} else {
self.command_line
.push_output(crate::tf!("Linetypes: {}", ltypes.join(", ")).as_ref());
}
}
// 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(crate::t!("Usage: LINETYPE SET <name | ByLayer | ByBlock>").as_ref());
} 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(crate::tf!("Current linetype set to {nm}.").as_ref());
}
None => {
self.command_line.push_error(crate::tf!(
"LINETYPE: \"{name}\" is not loaded. Use LINETYPE LIST to see available linetypes."
).as_ref());
}
}
}
}
_ => {
self.command_line
.push_info(crate::t!("Usage: LINETYPE LIST | SET <name>").as_ref());
}
}
}
// ── PURGE unused definitions ──────────────────────────────────
// Bare PURGE prompts with the category options as clickable
// keywords; PURGE <sub> (typed, clicked or scripted) runs directly.
"PURGE" => {
use crate::command::KeywordCommand;
let c = KeywordCommand::new(
"PURGE",
"PURGE [All / Blocks / LAyers / LineTypes / STyles]:",
vec![
("All", "ALL", None),
("Blocks", "BLOCKS", None),
("LAyers", "LAYERS", None),
("LineTypes", "LINETYPES", None),
("STyles", "STYLES", None),
],
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("PURGE ") => {
let sub = cmd
.split_whitespace()
.nth(1)
.unwrap_or("ALL")
.to_uppercase();
let all = sub == "ALL" || sub.is_empty();
let do_layers = all || matches!(sub.as_str(), "LAYERS" | "LAYER" | "LA");
let do_styles = all || matches!(sub.as_str(), "TEXTSTYLES" | "STYLES" | "ST");
let do_lts = all || matches!(sub.as_str(), "LINETYPES" | "LT");
let do_blocks = all || matches!(sub.as_str(), "BLOCKS" | "BLOCK" | "B");
if !(do_layers || do_styles || do_lts || do_blocks) {
self.command_line.push_error(crate::tf!(
"PURGE: unknown option \"{sub}\" — use ALL, Blocks, LAyers, LineTypes or STyles."
).as_ref());
return Some(Task::none());
}
// Purging a block frees whatever only its members referenced
// (nested blocks, layers, linetypes, styles), so those become
// purgeable on the next pass — repeat until a pass removes
// nothing, so one PURGE does what re-running it used to.
let (mut n_layers, mut n_styles, mut n_lts, mut n_blocks) =
(0usize, 0usize, 0usize, 0usize);
let mut snapshot_pushed = false;
loop {
// Collect names in use (immutable borrows — own scope per pass).
let used_layers: rustc_hash::FxHashSet<String> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|e| {
let name = &e.common().layer;
if name.is_empty() {
None
} else {
Some(name.clone())
}
})
.collect();
// Text styles: entity references plus every dimension
// style's DIMTXSTY — a style only a dimstyle uses is not
// purgeable.
let used_text_styles: rustc_hash::FxHashSet<String> = 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()),
acadrust::EntityType::AttributeDefinition(a) => {
Some(a.text_style.clone())
}
acadrust::EntityType::AttributeEntity(a) => {
Some(a.text_style.clone())
}
_ => None,
})
.chain(
self.tabs[i]
.scene
.document
.dim_styles
.iter()
.map(|ds| ds.dimtxsty.clone()),
)
.filter(|s| !s.is_empty())
.collect();
// Linetypes: entity references plus every layer's own
// linetype — a ByLayer entity reaches it through the layer.
let used_linetypes: rustc_hash::FxHashSet<String> = 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())
}
})
.chain(
self.tabs[i]
.scene
.document
.layers
.iter()
.map(|l| l.line_type.clone()),
)
.filter(|s| !s.is_empty())
.collect();
// Which block definitions are LIVE — reachable from a real
// layout container by following every block reference. A
// block nothing reachable inserts is dead and purges, even a
// whole mutually-referencing subgraph (e.g. the dependent
// blocks a detached xref leaves behind: they insert one
// another but hang off no layout, so a flat "is it named by
// any INSERT" scan keeps them alive forever). Roots:
// • *Model_Space / *Paper_Space(N),
// • any block whose layout handle resolves to a real Layout
// object — a DANGLING layout handle, as orphaned xref
// blocks carry, does NOT count, so is_layout() alone can't
// shield the dead subgraph it heads,
// • dimension-style arrowhead blocks (referenced by handle).
// A block's out-edges: its members' INSERT names, dimension
// *D blocks, table *T / cell blocks, and MultiLeader content.
let live_blocks: rustc_hash::FxHashSet<String> = {
let doc = &self.tabs[i].scene.document;
let by_handle: rustc_hash::FxHashMap<acadrust::Handle, String> = doc
.block_records
.iter()
.map(|br| (br.handle, br.name.clone()))
.collect();
let is_real_layout = |br: &acadrust::BlockRecord| -> bool {
let up = br.name.to_ascii_uppercase();
up.starts_with("*MODEL_SPACE")
|| up.starts_with("*PAPER_SPACE")
|| matches!(
doc.objects.get(&br.layout),
Some(acadrust::objects::ObjectType::Layout(_))
)
};
let children = |name: &str| -> Vec<String> {
let mut out = Vec::new();
let Some(br) = doc.block_records.get(name) else {
return out;
};
for &h in &br.entity_handles {
match doc.get_entity(h) {
Some(acadrust::EntityType::Insert(ins))
if !ins.block_name.is_empty() =>
{
out.push(ins.block_name.clone());
}
Some(acadrust::EntityType::Dimension(d))
if !d.base().block_name.is_empty() =>
{
out.push(d.base().block_name.clone());
}
Some(acadrust::EntityType::Table(t)) => {
if let Some(bh) = t.block_record_handle {
if let Some(n) = by_handle.get(&bh) {
out.push(n.clone());
}
}
for row in &t.rows {
for cell in &row.cells {
for c in &cell.contents {
if let Some(bh) = c.block_handle {
if let Some(n) = by_handle.get(&bh) {
out.push(n.clone());
}
}
}
}
}
}
Some(acadrust::EntityType::MultiLeader(ml)) => {
if let Some(bh) = ml.block_content_handle {
if let Some(n) = by_handle.get(&bh) {
out.push(n.clone());
}
}
}
_ => {}
}
}
out
};
let mut live = rustc_hash::FxHashSet::default();
let mut stack: Vec<String> = doc
.block_records
.iter()
.filter(|&br| is_real_layout(br))
.map(|br| br.name.clone())
.collect();
for ds in doc.dim_styles.iter() {
for h in [ds.dimblk, ds.dimblk1, ds.dimblk2, ds.dimldrblk] {
if !h.is_null() {
if let Some(n) = by_handle.get(&h) {
stack.push(n.clone());
}
}
}
}
while let Some(n) = stack.pop() {
if !live.insert(n.clone()) {
continue;
}
for c in children(&n) {
if !live.contains(&c) {
stack.push(c);
}
}
}
live
};
// Build removal lists (still immutable)
let layer_remove: Vec<String> = if do_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<String> = if do_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<String> = if do_lts {
let standard = ["Continuous", "ByLayer", "ByBlock"];
self.tabs[i]
.scene
.document
.line_types
.iter()
.filter(|lt| {
!standard.iter().any(|s| s.eq_ignore_ascii_case(&lt.name))
&& !used_linetypes.contains(&lt.name)
})
.map(|lt| lt.name.clone())
.collect()
} else {
vec![]
};
// Blocks: everything the layout-reachability walk above did
// NOT mark live. That covers unreferenced named blocks,
// leftover anonymous blocks (*U hatch/unnamed, orphaned *D
// dimension, *T table, dynamic-block variants — AutoCAD drops
// these too), AND whole dead subgraphs a detached xref leaves
// behind. Live xrefs stay (an attached xref's blocks are
// reached from the layout that inserts them); the *Model_Space
// / *Paper_Space containers are never removed.
let block_remove: Vec<String> = if do_blocks {
self.tabs[i]
.scene
.document
.block_records
.iter()
.filter(|br| {
let up = br.name.to_ascii_uppercase();
// Never PURGE an xref *definition* — that is a
// detach, not a purge. (Orphaned dependent blocks
// a past detach left behind are is_xref=false and
// still fall to the reachability test.)
!br.flags.is_xref
&& !br.flags.is_xref_overlay
&& !up.starts_with("*MODEL_SPACE")
&& !up.starts_with("*PAPER_SPACE")
&& !live_blocks.contains(&br.name)
})
.map(|br| br.name.clone())
.collect()
} else {
vec![]
};
if layer_remove.len()
+ style_remove.len()
+ lt_remove.len()
+ block_remove.len()
== 0
{
break;
}
// Snapshot the pre-purge document once, before the first
// removal, so a single UNDO restores everything.
if !snapshot_pushed {
self.push_undo_snapshot(i, "PURGE");
snapshot_pushed = true;
}
// Apply removals (mutable)
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 &lt_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);
}
n_layers += layer_remove.len();
n_styles += style_remove.len();
n_lts += lt_remove.len();
n_blocks += block_remove.len();
}
// Draw-order cleanup. A SortEntitiesTable lists a block's
// entities in draw order; purging the block drops the entities
// and the block record, but this object lingers, still holding
// the (now dangling) handle of every deleted entity. On a
// detached-xref purge that is hundreds of thousands of stale
// handles — megabytes of dead weight the save re-emits, which is
// why an OCS-purged file stayed far larger than AutoCAD's.
// Drop every SortEntitiesTable whose owning block is gone
// (AutoCAD's "orphaned data").
let mut n_sortents = 0usize;
if do_blocks {
let live_blocks: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
.scene
.document
.block_records
.iter()
.map(|br| br.handle)
.collect();
let orphans: Vec<acadrust::Handle> = self.tabs[i]
.scene
.document
.objects
.iter()
.filter_map(|(h, o)| match o {
acadrust::objects::ObjectType::SortEntitiesTable(s)
if !live_blocks.contains(&s.block_owner_handle) =>
{
Some(*h)
}
_ => None,
})
.collect();
if !orphans.is_empty() {
// `snapshot_pushed` is this purge's last use — no
// re-assignment needed (a block purge already snapshotted;
// a snapshot here only matters when nothing else changed).
if !snapshot_pushed {
self.push_undo_snapshot(i, "PURGE");
}
for h in &orphans {
self.tabs[i].scene.document.objects.remove(h);
}
n_sortents = orphans.len();
}
}
let purged = n_layers + n_styles + n_lts + n_blocks;
if purged > 0 || n_sortents > 0 {
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<String> = Vec::new();
if n_layers > 0 {
parts.push(format!("{n_layers} layer(s)"));
}
if n_styles > 0 {
parts.push(format!("{n_styles} text style(s)"));
}
if n_lts > 0 {
parts.push(format!("{n_lts} linetype(s)"));
}
if n_blocks > 0 {
parts.push(format!("{n_blocks} block(s)"));
}
if n_sortents > 0 {
parts.push(format!("{n_sortents} stale draw-order table(s)"));
}
self.command_line.push_output(crate::tf!(
"PURGE: {} item(s) removed — {}.",
purged + n_sortents,
parts.join(", ")
).as_ref());
} else {
self.command_line.push_output(crate::t!("PURGE: nothing to purge.").as_ref());
}
}
// ── 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 <property> <value>
// 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 <prop> <val> (props: LAYER COLOR LINETYPE LTSCALE)",
);
} else {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
.push_error(crate::t!("CHPROP: no entities selected.").as_ref());
} else {
// Validate value early to give clear errors
let color_val: Option<acadrust::types::Color> = if prop == "COLOR" {
value
.parse::<i16>()
.ok()
.map(acadrust::types::Color::from_index)
} else {
None
};
let ltscale_val: Option<f64> = if prop == "LTSCALE" {
value.parse().ok()
} else {
None
};
let transparency_val: Option<acadrust::types::Transparency> =
if prop == "TRANSPARENCY" {
value
.parse::<f64>()
.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(crate::tf!(
"CHPROP: invalid value '{}' for {}.",
value, prop
).as_ref());
} 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(crate::tf!(
"CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop
).as_ref());
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(crate::tf!(
"CHPROP: {} entity/entities updated.",
changed
).as_ref());
}
}
}
}
}
// ── 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)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if handles.is_empty() {
self.command_line
.push_error(crate::t!("SETBYLAYER: select entities first.").as_ref());
} 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;
let changes: Vec<_> = handles
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.command_line.push_output(crate::tf!(
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
).as_ref());
}
}
// ── 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<u64> = 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()))
&& !self.tabs[i].scene.is_layer_locked(e.common().handle)
})
.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<Handle> = 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(crate::t!("OVERKILL: no duplicate objects found.").as_ref());
} 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(crate::tf!("OVERKILL: deleted {n} duplicate object(s).").as_ref());
}
}
// ── PICKADD / PICKDRAG — selection UX (#226, app settings) ───
// Bare form reports; `<name> 0|1` sets and persists. Defaults keep
// today's behaviour (PICKADD 1, PICKDRAG 0).
"PICKADD" | "PICKDRAG" => {
use crate::command::ValuePromptCommand;
let (name, prompt) = if cmd == "PICKADD" {
("PICKADD", "PICKADD 1 = click adds to selection (default), 0 = click replaces <Enter reports>:")
} else {
("PICKDRAG", "PICKDRAG 0 = press-drag lassoes (default), 1 = press-drag draws a rectangle <Enter reports>:")
};
let c = ValuePromptCommand::new(name, prompt);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("PICKADD ") || cmd.starts_with("PICKDRAG ") => {
let is_add = cmd.starts_with("PICKADD");
let arg = cmd.splitn(2, ' ').nth(1).unwrap_or("").trim();
if arg.is_empty() {
let v = if is_add {
u8::from(self.pick_add)
} else {
u8::from(self.pick_drag_rect)
};
self.command_line.push_output(crate::tf!(
"{} = {v}",
if is_add { "PICKADD" } else { "PICKDRAG" }
).as_ref());
} else {
match arg {
"0" | "1" => {
let on = arg == "1";
if is_add {
self.pick_add = on;
self.command_line.push_output(crate::tf!(
"PICKADD = {} ({})",
arg,
if on {
"click adds to selection"
} else {
"click replaces selection, Shift toggles"
}
).as_ref());
} else {
self.pick_drag_rect = on;
self.command_line.push_output(crate::tf!(
"PICKDRAG = {} ({})",
arg,
if on {
"press-drag draws a rectangle"
} else {
"press-drag lassoes"
}
).as_ref());
}
self.persist_settings_if_changed();
}
_ => self.command_line.push_error(crate::tf!(
"{}: expected 0 or 1.",
if is_add { "PICKADD" } else { "PICKDRAG" }
).as_ref()),
}
}
}
// ── SETVAR — read / write system variables ───────────────────
// SETVAR <name> → report the value
// SETVAR <name> <value> → 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"
| "ZOOMWHEEL"
| "ZOOMFACTOR"
| "CURSORSIZE"
| "PICKBOX"
| "CURSORTYPE"
| "SNAPANG"
| "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"
| "FRAME"
| "IMAGEFRAME"
| "PDFFRAME"
| "POINTCLOUDCLIPFRAME"
| "XCLIPFRAME"
| "WIPEOUTFRAME"
| "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 FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
let frame_kind = crate::scene::frame::kind_for_name(&name);
if name == "FRAME" || frame_kind.is_some() {
let current = frame_kind.map_or_else(
|| crate::scene::frame::master_mode(&self.tabs[i].scene.document),
|kind| crate::scene::frame::mode(&self.tabs[i].scene.document, kind),
);
match &value {
Some(value) => match value.parse::<i16>() {
Ok(mode @ 0..=2) => {
if current != mode {
let changed_kinds: Vec<_> = frame_kind.map_or_else(
|| {
crate::scene::frame::ALL_KINDS
.into_iter()
.filter(|kind| {
crate::scene::frame::mode(
&self.tabs[i].scene.document,
*kind,
) != mode
})
.collect()
},
|kind| vec![kind],
);
self.push_undo_snapshot(i, &name);
if let Some(kind) = frame_kind {
crate::scene::frame::set_mode(
&mut self.tabs[i].scene.document,
kind,
mode,
);
} else {
crate::scene::frame::set_master_mode(
&mut self.tabs[i].scene.document,
mode,
);
}
let changes: Vec<_> = self.tabs[i]
.scene
.document
.entities()
.filter_map(|entity| {
changed_kinds
.iter()
.any(|kind| {
crate::scene::frame::affected(entity, *kind)
})
.then_some((
entity.common().handle,
crate::scene::ChangeKind::Modified,
))
})
.collect();
if !changes.is_empty() {
self.tabs[i].scene.bump_entities(&changes);
}
self.tabs[i].dirty = true;
}
self.command_line
.push_output(&format!("{name} = {mode}"));
}
_ => self.command_line.push_error(
crate::tf!("SETVAR: {name} requires 0, 1, or 2.").as_ref(),
),
},
None => {
self.command_line.push_output(crate::tf!(
"Enter new value for {name} <{current}>:"
).as_ref());
self.pending_setvar = Some(name.clone());
}
}
return Some(self.finish_dispatch(cmd));
}
// 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::<f64>()
.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::<f64>()
.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::<f64>()
.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::<f64>()
.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::<i16>()
.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)),
},
"ZOOMWHEEL" => match &value {
Some(v) => match parse_bool(v) {
Some(reversed) => {
self.zoom_wheel_reversed = reversed;
Ok((
format!("ZOOMWHEEL = {}", reversed as i32),
true,
))
}
None => Err("SETVAR: 0 or 1 required.".into()),
},
None => Ok((
format!(
"ZOOMWHEEL = {}",
self.zoom_wheel_reversed as i32
),
false,
)),
},
"ZOOMFACTOR" => match &value {
Some(v) => match v.parse::<i32>() {
Ok(factor) if (3..=100).contains(&factor) => {
self.zoom_factor = factor;
Ok((format!("ZOOMFACTOR = {factor}"), true))
}
_ => Err("SETVAR: integer from 3 to 100 required.".into()),
},
None => {
Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false))
}
},
"CURSORSIZE" => match &value {
Some(v) => match v.parse::<i32>() {
Ok(size) if (1..=100).contains(&size) => {
self.cursor_size = size;
Ok((format!("CURSORSIZE = {size}"), true))
}
_ => Err("SETVAR: integer from 1 to 100 required.".into()),
},
None => Ok((format!("CURSORSIZE = {}", self.cursor_size), false)),
},
"PICKBOX" => match &value {
Some(v) => match v.parse::<i32>() {
Ok(size) if (0..=50).contains(&size) => {
self.pick_box = size;
Ok((format!("PICKBOX = {size}"), true))
}
_ => Err("SETVAR: integer from 0 to 50 required.".into()),
},
None => Ok((format!("PICKBOX = {}", self.pick_box), false)),
},
"CURSORTYPE" => match &value {
Some(v) => match v.as_str() {
"0" => {
self.cursor_type = crate::app::settings::CursorType::Crosshair;
Ok(("CURSORTYPE = 0".to_string(), true))
}
"1" => {
self.cursor_type = crate::app::settings::CursorType::Pointer;
Ok(("CURSORTYPE = 1".to_string(), true))
}
_ => Err("SETVAR: 0 or 1 required.".into()),
},
None => Ok((
format!(
"CURSORTYPE = {}",
i32::from(self.cursor_type == crate::app::settings::CursorType::Pointer)
),
false,
)),
},
"SNAPANG" => match &value {
Some(v) => match v.parse::<f32>() {
Ok(angle) if angle.is_finite() => {
self.snap_angle_deg = angle.rem_euclid(360.0);
Ok((format!("SNAPANG = {}", self.snap_angle_deg), true))
}
_ => Err("SETVAR: finite numeric value required.".into()),
},
None => Ok((format!("SNAPANG = {}", self.snap_angle_deg), 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::<i16>()
.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::<f64>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<f64>()
.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::<f64>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i32>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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::<i16>()
.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)),
},
"HALOGAP" => match &value {
Some(v) => v
.parse::<i16>()
.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::<f64>()
.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::<f64>()
.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 {
if matches!(
name.as_str(),
"ZOOMWHEEL"
| "ZOOMFACTOR"
| "CURSORSIZE"
| "PICKBOX"
| "CURSORTYPE"
| "SNAPANG"
) {
self.persist_settings_if_changed();
} else {
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(crate::tf!(
"Enter new value for {name} <{current}>:"
).as_ref());
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
.invalidate_text_geometry_dependencies();
}
// LTSCALE scales the dash pattern baked into every wire, and
// PDMODE / PDSIZE decide the point glyph built at tessellation
// time. Their own commands invalidate for exactly that reason;
// reaching the same variable through SETVAR has to do it too,
// or the drawing keeps rendering the old value until some
// unrelated edit happens to rebuild it.
if name == "LTSCALE" {
self.tabs[i].scene.bump_geometry();
}
if name == "PDMODE" || name == "PDSIZE" {
self.tabs[i].scene.invalidate_point_dependencies();
}
// 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<String> = FxHashSet::default();
let mut linetypes: FxHashSet<String> = FxHashSet::default();
let mut styles: FxHashSet<String> = FxHashSet::default();
let mut blocks: FxHashSet<String> = 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>| -> 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(crate::t!("FINDNONPURGEABLE: named objects in use (not purgeable):").as_ref());
self.command_line
.push_output(crate::tf!(" Layers: {}", fmt(layers)).as_ref());
self.command_line
.push_output(crate::tf!(" Linetypes: {}", fmt(linetypes)).as_ref());
self.command_line
.push_output(crate::tf!(" Text styles: {}", fmt(styles)).as_ref());
self.command_line
.push_output(crate::tf!(" Blocks: {}", fmt(blocks)).as_ref());
}
// ── 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<String> = BTreeSet::new();
let mut undefined_blocks: BTreeSet<String> = 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(crate::tf!("AUDIT: scanned {total} object(s).").as_ref());
if undefined_layers.is_empty() && undefined_blocks.is_empty() {
self.command_line.push_output(crate::t!("AUDIT: no issues found.").as_ref());
} else {
if !undefined_layers.is_empty() {
self.command_line.push_error(crate::tf!(
"AUDIT: reference(s) to undefined layer(s): {}",
undefined_layers.into_iter().collect::<Vec<_>>().join(", ")
).as_ref());
}
if !undefined_blocks.is_empty() {
self.command_line.push_error(crate::tf!(
"AUDIT: reference(s) to undefined block(s): {}",
undefined_blocks.into_iter().collect::<Vec<_>>().join(", ")
).as_ref());
}
self.command_line
.push_info(crate::t!("AUDIT: report only — no automatic repair performed.").as_ref());
}
}
// ── 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 <type> <old_name> <new_name>
// 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(
crate::t!("Usage: RENAME <type> <old> <new> (types: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW)").as_ref()
);
} else {
// Snapshot BEFORE mutating (undo must restore the old
// name); dropped again if nothing was renamed.
self.push_undo_snapshot(i, "RENAME");
let known = matches!(
type_str.as_str(),
"LAYER"
| "BLOCK"
| "STYLE"
| "TEXTSTYLE"
| "DIMSTYLE"
| "LINETYPE"
| "LT"
| "UCS"
| "VIEW"
);
let ok = if type_str == "BLOCK" {
// Re-keys the record, syncs the Block marker and every
// INSERT reference; refuses anonymous/xref sources and
// invalid/taken names.
self.tabs[i].scene.rename_block(&old_name, &new_name)
} else if type_str == "LAYER" {
self.tabs[i].rename_layer(&old_name, &new_name)
} else if known {
rename_symbol(
&mut self.tabs[i].scene.document,
&type_str,
&old_name,
&new_name,
)
} else {
false
};
if ok {
if type_str != "LAYER" {
self.tabs[i].scene.bump_geometry_no_blocks();
}
if type_str == "UCS" {
if let Some(active) = self.tabs[i].active_ucs.as_mut() {
if active.name.eq_ignore_ascii_case(&old_name) {
active.name = new_name.clone();
self.tabs[i].persist_active_ucs();
}
}
}
self.tabs[i].dirty = true;
if type_str == "LAYER" {
self.refresh_layer_panel();
}
self.command_line
.push_output(crate::tf!("RENAME: '{}' → '{}'.", old_name, new_name).as_ref());
} else {
self.discard_last_undo_entry(i);
if !known {
self.command_line.push_error(crate::tf!("RENAME: unknown type '{}'. Use LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW", type_str).as_ref());
} else if type_str == "BLOCK" {
self.command_line.push_error(crate::tf!(
"RENAME: could not rename block '{}' — missing/anonymous/xref, or '{}' is invalid or taken.",
old_name, new_name
).as_ref());
} else {
self.command_line.push_error(crate::tf!(
"RENAME: could not rename '{}' in {} — not found or protected, or '{}' is invalid or taken.",
old_name, type_str, new_name
).as_ref());
}
}
}
}
// ── 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(crate::tf!("CLAYER = \"{cur}\"").as_ref());
} 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(crate::tf!("CLAYER set to \"{name_arg}\"").as_ref());
} else {
self.command_line
.push_error(crate::tf!("CLAYER: layer '{}' not found.", name_arg).as_ref());
}
}
}
"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::<Vec<_>>().join(" ");
if name_arg.is_empty() {
let cur = &self.tabs[i].scene.document.header.current_dimstyle_name;
self.command_line
.push_output(crate::tf!("CDIMSTY = \"{cur}\"").as_ref());
} 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(crate::tf!("Active dim style set to \"{name_arg}\"").as_ref());
} else {
self.command_line
.push_error(crate::tf!("CDIMSTY: dim style '{}' not found.", name_arg).as_ref());
}
}
}
"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(crate::tf!("LTSCALE = {v:.4}").as_ref());
} else if let Ok(v) = val_str.parse::<f64>() {
if v > 0.0 {
self.push_undo_snapshot(i, "LTSCALE");
self.tabs[i].scene.document.header.linetype_scale = v;
// LTSCALE affects the cached linetype geometry of the whole drawing.
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("LTSCALE set to {v:.4}").as_ref());
} else {
self.command_line
.push_error(crate::t!("LTSCALE: value must be positive.").as_ref());
}
} else {
self.command_line.push_error(crate::t!("Usage: LTSCALE [value]").as_ref());
}
}
"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(crate::tf!("PDMODE = {v}").as_ref());
} else if let Ok(v) = val_str.parse::<i16>() {
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.invalidate_point_dependencies();
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!("PDMODE set to {v}").as_ref());
} 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(crate::tf!("TEXTEDITMODE = {v}").as_ref());
} 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(crate::tf!("TEXTEDITMODE set to {n}").as_ref());
} else {
self.command_line
.push_error(crate::t!("Requires 0 OR 1 OR MULTIPLE OR SINGLE").as_ref());
}
}
"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(crate::tf!("ISAVEBAK = {v}").as_ref());
}
"0" => {
self.backup_on_save = false;
self.persist_settings_if_changed();
self.command_line.push_output(crate::t!("ISAVEBAK set to 0").as_ref());
}
"1" => {
self.backup_on_save = true;
self.persist_settings_if_changed();
self.command_line.push_output(crate::t!("ISAVEBAK set to 1").as_ref());
}
_ => self.command_line.push_error(crate::t!("Requires 0 or 1").as_ref()),
}
}
"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(crate::tf!("FILEASSOC = {v}").as_ref());
}
"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(crate::tf!("FILEASSOC: registration failed: {e}").as_ref()),
}
}
"0" => {
self.file_assoc_enabled = false;
self.persist_settings_if_changed();
match crate::io::file_association::unregister_handler() {
Ok(()) => self
.command_line
.push_output(crate::t!("FILEASSOC set to 0 — unregistered as a file handler").as_ref()),
Err(e) => self
.command_line
.push_error(crate::tf!("FILEASSOC: unregister failed: {e}").as_ref()),
}
}
_ => self.command_line.push_error(crate::t!("Requires 0 or 1").as_ref()),
}
}
"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(crate::tf!("SAVETIME = {}", self.savetime_min).as_ref());
return Some(Task::none());
}
match arg.parse::<i32>() {
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(crate::t!("Requires a non-negative number of minutes (0 = off)").as_ref()),
}
}
"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(crate::tf!("PDSIZE = {v:.4}").as_ref());
} else if let Ok(v) = val_str.parse::<f64>() {
self.push_undo_snapshot(i, "PDSIZE");
self.tabs[i].scene.document.header.point_display_size = v;
self.tabs[i].scene.invalidate_point_dependencies();
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("PDSIZE set to {v:.4}").as_ref());
} 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<Option<bool>, ()> = 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(crate::t!("Usage: LWDISPLAY [ON|OFF]").as_ref()),
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(crate::tf!("LWDISPLAY {}", if v { "ON" } else { "OFF" }).as_ref());
}
Ok(None) => {
let v = self.tabs[i].scene.document.header.lineweight_display;
self.command_line
.push_output(crate::tf!("LWDISPLAY = {}", if v { "ON" } else { "OFF" }).as_ref());
}
}
}
"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(crate::tf!("CELTSCALE = {v:.4}").as_ref());
} else if let Ok(v) = val_str.parse::<f64>() {
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(crate::tf!("CELTSCALE set to {v:.4}").as_ref());
} else {
self.command_line
.push_error(crate::t!("CELTSCALE: value must be positive.").as_ref());
}
} else {
self.command_line.push_error(crate::t!("Usage: CELTSCALE [value]").as_ref());
}
}
// ── SCALETEXT — rescale selected Text/MText entities ─────────────────
// Usage: SCALETEXT <factor> e.g. SCALETEXT 2
// SCALETEXT H <height> 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 <height>' 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<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if selected_handles.is_empty() {
self.command_line
.push_error(crate::t!("SCALETEXT: select Text/MText entities first.").as_ref());
} 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::<f64>().ok()),
(Some(v), None) => (false, v.parse::<f64>().ok()),
_ => (false, None),
};
if let Some(val) = value {
if val <= 0.0 {
self.command_line
.push_error(crate::t!("SCALETEXT: value must be positive.").as_ref());
} 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(crate::tf!(
"SCALETEXT: scaled {count} text entity(ies)."
).as_ref());
} else {
self.command_line
.push_error(crate::t!("SCALETEXT: no Text/MText in selection.").as_ref());
}
}
} else {
self.command_line
.push_info(crate::t!("Usage: SCALETEXT <factor> or SCALETEXT H <height>").as_ref());
}
}
}
_ => return None,
}
Some(self.finish_dispatch(cmd))
}
}
/// RENAME for the remaining name-keyed symbol tables.
fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str) -> bool {
use acadrust::{EntityType, Table, TableEntry};
fn rekey<T: TableEntry>(table: &mut Table<T>, old: &str, new: &str) -> bool {
if !crate::scene::valid_block_name(new) {
return false;
}
table.rename(old, new.to_string()).is_ok()
}
match ty {
"STYLE" | "TEXTSTYLE" => {
if !rekey(&mut doc.text_styles, old, new) {
return false;
}
for e in doc.entities_mut() {
match e {
EntityType::Text(t) if t.style.eq_ignore_ascii_case(old) => {
t.style = new.to_string();
}
EntityType::MText(m) if m.style.eq_ignore_ascii_case(old) => {
m.style = new.to_string();
}
EntityType::AttributeDefinition(a)
if a.text_style.eq_ignore_ascii_case(old) =>
{
a.text_style = new.to_string();
}
EntityType::Insert(ins) => {
for a in &mut ins.attributes {
if a.text_style.eq_ignore_ascii_case(old) {
a.text_style = new.to_string();
}
}
}
_ => {}
}
}
if doc.header.current_text_style_name.eq_ignore_ascii_case(old) {
doc.header.current_text_style_name = new.to_string();
}
true
}
"DIMSTYLE" => {
if !rekey(&mut doc.dim_styles, old, new) {
return false;
}
for e in doc.entities_mut() {
match e {
EntityType::Dimension(d)
if d.base().style_name.eq_ignore_ascii_case(old) =>
{
d.base_mut().style_name = new.to_string();
}
EntityType::Leader(l) if l.dimension_style.eq_ignore_ascii_case(old) => {
l.dimension_style = new.to_string();
}
EntityType::Tolerance(t)
if t.dimension_style_name.eq_ignore_ascii_case(old) =>
{
t.dimension_style_name = new.to_string();
}
_ => {}
}
}
if doc.header.current_dimstyle_name.eq_ignore_ascii_case(old) {
doc.header.current_dimstyle_name = new.to_string();
}
true
}
"LINETYPE" | "LT" => {
// The three built-ins are fixed names every drawing relies on.
if ["BYLAYER", "BYBLOCK", "CONTINUOUS"]
.contains(&old.to_uppercase().as_str())
{
return false;
}
if !rekey(&mut doc.line_types, old, new) {
return false;
}
for e in doc.entities_mut() {
if e.common().linetype.eq_ignore_ascii_case(old) {
e.common_mut().linetype = new.to_string();
}
}
for l in doc.layers.iter_mut() {
if l.line_type.eq_ignore_ascii_case(old) {
l.line_type = new.to_string();
}
}
if doc.header.current_linetype_name.eq_ignore_ascii_case(old) {
doc.header.current_linetype_name = new.to_string();
}
true
}
"UCS" => {
if !rekey(&mut doc.ucss, old, new) {
return false;
}
if doc.header.model_space_ucs_name.eq_ignore_ascii_case(old) {
doc.header.model_space_ucs_name = new.to_string();
}
true
}
"VIEW" => rekey(&mut doc.views, old, new),
_ => false,
}
}