forked from 1b-agent/opencadstudio-upstream
Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities. Refs #642
2535 lines
132 KiB
Rust
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(<.name))
|
|
&& !used_linetypes.contains(<.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 <_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,
|
|
}
|
|
}
|