forked from 1b-agent/opencadstudio-upstream
Modern DWG (R2000+) has no file slot for OSMODE — reading a file left the header at 0, and adopt_header_sysvars wiped the user's running snap set on every open. Persist the set (as an $OSMODE-style bitmask) in settings.json, save it on every snap toggle, and only let a file override it when it actually carries a nonzero mask (R13/R14 DWG, DXF). Co-Authored-By: Claude Fable 5 <[email protected]>
1935 lines
99 KiB
Rust
1935 lines
99 KiB
Rust
use super::*;
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
|
match cmd {
|
|
// 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(&format!("Current object colour: {}", describe(&c)));
|
|
return Some(Task::none());
|
|
}
|
|
"BYLAYER" => Some(Color::ByLayer),
|
|
"BYBLOCK" => Some(Color::ByBlock),
|
|
"RED" => Some(Color::Index(1)),
|
|
"YELLOW" => Some(Color::Index(2)),
|
|
"GREEN" => Some(Color::Index(3)),
|
|
"CYAN" => Some(Color::Index(4)),
|
|
"BLUE" => Some(Color::Index(5)),
|
|
"MAGENTA" => Some(Color::Index(6)),
|
|
"WHITE" => Some(Color::Index(7)),
|
|
n => n.parse::<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(&format!("Object colour set to {}.", describe(&c)));
|
|
}
|
|
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("No linetypes defined.");
|
|
} else {
|
|
self.command_line
|
|
.push_output(&format!("Linetypes: {}", ltypes.join(", ")));
|
|
}
|
|
}
|
|
// Set the current linetype applied to newly drawn entities.
|
|
"SET" | "CURRENT" | "S" => {
|
|
let name = parts.get(1).copied().unwrap_or("");
|
|
if name.is_empty() {
|
|
self.command_line
|
|
.push_info("Usage: LINETYPE SET <name | ByLayer | ByBlock>");
|
|
} else {
|
|
let canon = if name.eq_ignore_ascii_case("BYLAYER") {
|
|
Some(("ByLayer".to_string(), acadrust::types::Handle::NULL))
|
|
} else if name.eq_ignore_ascii_case("BYBLOCK") {
|
|
Some(("ByBlock".to_string(), acadrust::types::Handle::NULL))
|
|
} else {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.line_types
|
|
.iter()
|
|
.find(|lt| lt.name.eq_ignore_ascii_case(name))
|
|
.map(|lt| (lt.name.clone(), lt.handle))
|
|
};
|
|
match canon {
|
|
Some((nm, handle)) => {
|
|
let h = &mut self.tabs[i].scene.document.header;
|
|
h.current_linetype_name = nm.clone();
|
|
h.current_linetype_handle = handle;
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("Current linetype set to {nm}."));
|
|
}
|
|
None => {
|
|
self.command_line.push_error(&format!(
|
|
"LINETYPE: \"{name}\" is not loaded. Use LINETYPE LIST to see available linetypes."
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
self.command_line
|
|
.push_info("Usage: LINETYPE LIST | SET <name>");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── PURGE unused definitions ──────────────────────────────────
|
|
cmd if cmd == "PURGE" || cmd.starts_with("PURGE ") => {
|
|
let sub = cmd
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.unwrap_or("ALL")
|
|
.to_uppercase();
|
|
let all = sub == "ALL" || sub.is_empty();
|
|
|
|
// Collect names in use (immutable borrows — done in their own scope)
|
|
let used_layers: rustc_hash::FxHashSet<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();
|
|
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()),
|
|
_ => None,
|
|
})
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
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())
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let used_blocks: rustc_hash::FxHashSet<String> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|e| match e {
|
|
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
|
acadrust::EntityType::Dimension(d) => Some(d.base().block_name.clone()),
|
|
_ => None,
|
|
})
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
|
|
// Build removal lists (still immutable)
|
|
let layer_remove: Vec<String> = if all || sub == "LAYERS" {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.layers
|
|
.iter()
|
|
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
|
|
.map(|l| l.name.clone())
|
|
.collect()
|
|
} else {
|
|
vec![]
|
|
};
|
|
let style_remove: Vec<String> = if all || sub == "TEXTSTYLES" || sub == "STYLES" {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.text_styles
|
|
.iter()
|
|
.filter(|s| s.name != "Standard" && !used_text_styles.contains(&s.name))
|
|
.map(|s| s.name.clone())
|
|
.collect()
|
|
} else {
|
|
vec![]
|
|
};
|
|
let lt_remove: Vec<String> = if all || sub == "LINETYPES" || sub == "LT" {
|
|
let standard = ["Continuous", "ByLayer", "ByBlock"];
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.line_types
|
|
.iter()
|
|
.filter(|lt| {
|
|
!standard.iter().any(|s| s.eq_ignore_ascii_case(<.name))
|
|
&& !used_linetypes.contains(<.name)
|
|
})
|
|
.map(|lt| lt.name.clone())
|
|
.collect()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
// Blocks: definitions with no INSERT/Dimension reference.
|
|
// Skip anonymous (*Model_Space/*Paper_Space/*U*/*D*…), layout,
|
|
// and xref blocks — those are system- or reference-managed.
|
|
let block_remove: Vec<String> = if all || sub == "BLOCKS" {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.iter()
|
|
.filter(|br| {
|
|
!br.is_anonymous()
|
|
&& !br.is_layout()
|
|
&& !br.flags.is_xref
|
|
&& !br.flags.is_xref_overlay
|
|
&& !used_blocks.contains(&br.name)
|
|
})
|
|
.map(|br| br.name.clone())
|
|
.collect()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
// Apply removals (mutable)
|
|
let purged = layer_remove.len()
|
|
+ style_remove.len()
|
|
+ lt_remove.len()
|
|
+ block_remove.len();
|
|
for name in &layer_remove {
|
|
self.tabs[i].scene.document.layers.remove(name);
|
|
}
|
|
for name in &style_remove {
|
|
self.tabs[i].scene.document.text_styles.remove(name);
|
|
}
|
|
for name in <_remove {
|
|
self.tabs[i].scene.document.line_types.remove(name);
|
|
}
|
|
for name in &block_remove {
|
|
// Drop the block definition's member entities (and the
|
|
// BLOCK/ENDBLK delimiters) before the record so no orphaned
|
|
// geometry survives in the document entity list.
|
|
let handles: Vec<_> =
|
|
if let Some(br) = self.tabs[i].scene.document.block_records.get(name) {
|
|
let mut h = br.entity_handles.clone();
|
|
h.push(br.block_entity_handle);
|
|
h.push(br.block_end_handle);
|
|
h
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
for h in handles {
|
|
self.tabs[i].scene.document.remove_entity(h);
|
|
}
|
|
self.tabs[i].scene.document.block_records.remove(name);
|
|
}
|
|
|
|
if purged > 0 {
|
|
self.push_undo_snapshot(i, "PURGE");
|
|
self.tabs[i].dirty = true;
|
|
// Rebuild the layer/style/linetype panel + ribbon caches so
|
|
// the removed definitions disappear from the UI immediately.
|
|
self.refresh_layer_panel();
|
|
// Per-type breakdown so the user sees exactly what went.
|
|
let mut parts: Vec<String> = Vec::new();
|
|
if !layer_remove.is_empty() {
|
|
parts.push(format!("{} layer(s)", layer_remove.len()));
|
|
}
|
|
if !style_remove.is_empty() {
|
|
parts.push(format!("{} text style(s)", style_remove.len()));
|
|
}
|
|
if !lt_remove.is_empty() {
|
|
parts.push(format!("{} linetype(s)", lt_remove.len()));
|
|
}
|
|
if !block_remove.is_empty() {
|
|
parts.push(format!("{} block(s)", block_remove.len()));
|
|
}
|
|
self.command_line.push_output(&format!(
|
|
"PURGE: {} definition(s) removed — {}.",
|
|
purged,
|
|
parts.join(", ")
|
|
));
|
|
} else {
|
|
self.command_line.push_output("PURGE: nothing to purge.");
|
|
}
|
|
}
|
|
|
|
// ── CHPROP — change entity properties from command line ───────
|
|
"CHPROP" => {
|
|
use crate::command::SelectThenKeywordCommand;
|
|
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
|
let c = SelectThenKeywordCommand::new(
|
|
"CHPROP",
|
|
"CHPROP property [Layer / Color / Linetype / LtScale / Transparency]:",
|
|
vec![
|
|
("Layer", "LAYER", Some("CHPROP new layer name:")),
|
|
("Color", "COLOR", Some("CHPROP new colour (name / 1-255 / ByLayer):")),
|
|
("Linetype", "LINETYPE", Some("CHPROP new linetype name:")),
|
|
("LtScale", "LTSCALE", Some("CHPROP new linetype scale:")),
|
|
("Transparency", "TRANSPARENCY", Some("CHPROP transparency 0-90:")),
|
|
],
|
|
has_sel,
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("CHPROP ") => {
|
|
// Usage: CHPROP <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)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
self.command_line
|
|
.push_error("CHPROP: no entities selected.");
|
|
} else {
|
|
// Validate value early to give clear errors
|
|
let color_val: Option<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(&format!(
|
|
"CHPROP: invalid value '{}' for {}.",
|
|
value, prop
|
|
));
|
|
} else {
|
|
let mut changed = 0usize;
|
|
for handle in &handles {
|
|
if let Some(entity) =
|
|
self.tabs[i].scene.document.get_entity_mut(*handle)
|
|
{
|
|
let common = entity.common_mut();
|
|
match prop.as_str() {
|
|
"LAYER" => {
|
|
common.layer = value.clone();
|
|
changed += 1;
|
|
}
|
|
"LINETYPE" | "LT" => {
|
|
common.linetype = value.clone();
|
|
changed += 1;
|
|
}
|
|
"LTSCALE" => {
|
|
common.linetype_scale = ltscale_val.unwrap();
|
|
changed += 1;
|
|
}
|
|
"COLOR" => {
|
|
common.color = color_val.unwrap();
|
|
changed += 1;
|
|
}
|
|
"TRANSPARENCY" => {
|
|
common.transparency = transparency_val.unwrap();
|
|
changed += 1;
|
|
}
|
|
_ => {
|
|
self.command_line.push_error(&format!(
|
|
"CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if changed > 0 {
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
self.tabs[i].dirty = true;
|
|
// Colour / linetype / ltscale / transparency /
|
|
// layer are baked into the cached wire geometry —
|
|
// re-tessellate the changed entities so they
|
|
// repaint immediately (issue #231 class).
|
|
self.invalidate_property_targets(i, &handles);
|
|
self.command_line.push_output(&format!(
|
|
"CHPROP: {} entity/entities updated.",
|
|
changed
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── SETBYLAYER — clear color/linetype/lineweight overrides ────
|
|
// Resets the selected entities' direct property overrides back to
|
|
// ByLayer so they follow their layer again.
|
|
"SETBYLAYER" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
self.command_line
|
|
.push_error("SETBYLAYER: select entities first.");
|
|
} else {
|
|
self.push_undo_snapshot(i, "SETBYLAYER");
|
|
let mut changed = 0usize;
|
|
for handle in &handles {
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) {
|
|
let common = entity.common_mut();
|
|
common.color = acadrust::types::Color::ByLayer;
|
|
common.linetype = "ByLayer".to_string();
|
|
common.line_weight = acadrust::types::LineWeight::ByLayer;
|
|
changed += 1;
|
|
}
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
self.tabs[i].scene.bump_geometry();
|
|
self.command_line.push_output(&format!(
|
|
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
|
|
));
|
|
}
|
|
}
|
|
|
|
// ── OVERKILL — delete duplicate (identical) objects ──────────
|
|
// Removes objects that are identical in geometry AND properties to
|
|
// another object (compared with the handle ignored). Operates on
|
|
// the current selection, or the whole drawing when nothing is
|
|
// selected. Conservative: only exact duplicates are removed.
|
|
"OVERKILL" => {
|
|
use acadrust::Handle;
|
|
let selected: std::collections::HashSet<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())
|
|
})
|
|
.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("OVERKILL: no duplicate objects found.");
|
|
} else {
|
|
let n = dups.len();
|
|
self.push_undo_snapshot(i, "OVERKILL");
|
|
self.tabs[i].scene.erase_entities(&dups);
|
|
self.tabs[i].dirty = true;
|
|
self.refresh_properties();
|
|
self.command_line
|
|
.push_output(&format!("OVERKILL: deleted {n} duplicate object(s)."));
|
|
}
|
|
}
|
|
|
|
// ── SETVAR — read / write system variables ───────────────────
|
|
// SETVAR <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"
|
|
| "TEXTFILL"
|
|
| "ATTREQ"
|
|
| "ATTDIA"
|
|
| "DIMASSOC"
|
|
| "ANGBASE"
|
|
| "ANGDIR"
|
|
| "REGENMODE"
|
|
| "BLIPMODE"
|
|
| "SPLFRAME"
|
|
| "DELOBJ"
|
|
| "PLINEGEN"
|
|
| "PSLTSCALE"
|
|
| "DISPSILH"
|
|
| "WORLDVIEW"
|
|
| "LIMCHECK"
|
|
| "DRAGMODE"
|
|
| "LUNITS"
|
|
| "LUPREC"
|
|
| "AUNITS"
|
|
| "AUPREC"
|
|
| "THICKNESS"
|
|
| "ELEVATION"
|
|
| "INSUNITS"
|
|
| "SPLINETYPE"
|
|
| "ISOLINES"
|
|
| "DIMASO"
|
|
| "DIMSHO"
|
|
| "QTEXTMODE"
|
|
| "PLIMCHECK"
|
|
| "VISRETAIN"
|
|
| "USRTIMER"
|
|
| "ATTMODE"
|
|
| "COORDS"
|
|
| "OSMODE"
|
|
| "PICKSTYLE"
|
|
| "SPLINESEGS"
|
|
| "SURFU"
|
|
| "SURFV"
|
|
| "SURFTYPE"
|
|
| "SHADEDGE"
|
|
| "MAXACTVP"
|
|
| "CMLJUST"
|
|
| "TEXTQLTY"
|
|
| "SORTENTS"
|
|
| "XCLIPFRAME"
|
|
| "HALOGAP"
|
|
| "TRACEWID"
|
|
| "SKETCHINC"
|
|
) =>
|
|
{
|
|
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
|
}
|
|
|
|
"SETVAR" => {
|
|
use crate::command::TwoValuePromptCommand;
|
|
let c = TwoValuePromptCommand::new(
|
|
"SETVAR",
|
|
"SETVAR variable name:",
|
|
"SETVAR new value (blank to read):",
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("SETVAR ") => {
|
|
let rest = cmd.strip_prefix("SETVAR").unwrap_or("").trim();
|
|
let mut it = rest.splitn(2, char::is_whitespace);
|
|
let name = it.next().unwrap_or("").to_uppercase();
|
|
let value = it.next().map(|s| s.trim().to_string());
|
|
if name.is_empty() || name == "?" {
|
|
self.command_line.push_info(
|
|
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
|
);
|
|
} else {
|
|
// Parse a boolean given as 0/1 or ON/OFF.
|
|
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
|
|
"1" | "ON" | "TRUE" => Some(true),
|
|
"0" | "OFF" | "FALSE" => Some(false),
|
|
_ => None,
|
|
};
|
|
let outcome: Result<(String, bool), String> = {
|
|
let h = &mut self.tabs[i].scene.document.header;
|
|
match name.as_str() {
|
|
"LTSCALE" => match &value {
|
|
Some(v) => v
|
|
.parse::<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)),
|
|
},
|
|
// 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)),
|
|
},
|
|
"XCLIPFRAME" => match &value {
|
|
Some(v) => v
|
|
.parse::<i16>()
|
|
.map(|x| {
|
|
h.xclip_frame = x;
|
|
(format!("XCLIPFRAME = {x}"), true)
|
|
})
|
|
.map_err(|_| "SETVAR: integer value required.".into()),
|
|
None => Ok((format!("XCLIPFRAME = {}", h.xclip_frame), false)),
|
|
},
|
|
"HALOGAP" => match &value {
|
|
Some(v) => v
|
|
.parse::<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 {
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(&msg);
|
|
} else {
|
|
// Queried with no value (`changed == false`):
|
|
// prompt for a new value on the next line instead
|
|
// of only echoing the current one. Enter keeps it.
|
|
let current = msg.split('=').nth(1).map(str::trim).unwrap_or("");
|
|
self.command_line.push_output(&format!(
|
|
"Enter new value for {name} <{current}>:"
|
|
));
|
|
self.pending_setvar = Some(name.clone());
|
|
}
|
|
}
|
|
Err(e) => self.command_line.push_error(&e),
|
|
}
|
|
// TEXTFILL reset the glyph atlas; re-tessellate so text picks
|
|
// up the re-baked filled / hollow tiles.
|
|
if name == "TEXTFILL" {
|
|
self.tabs[i].scene.bump_geometry();
|
|
}
|
|
// ORTHOMODE / OSMODE set the header directly; mirror them into
|
|
// the live Ortho / running OSNAP so the constraint + status
|
|
// bar follow and the save-time stamp doesn't revert them.
|
|
if name == "ORTHOMODE" {
|
|
self.ortho_mode = self.tabs[i].scene.document.header.ortho_mode;
|
|
if self.ortho_mode {
|
|
self.polar_mode = false;
|
|
}
|
|
}
|
|
if name == "OSMODE" {
|
|
let (modes, en) = crate::app::settings::snaps_from_osmode(
|
|
self.tabs[i].scene.document.header.object_snap_mode,
|
|
);
|
|
self.snapper.enabled = modes.into_iter().collect();
|
|
self.snapper.snap_enabled = en;
|
|
// The OSNAP set is app-level (no modern-DWG file slot).
|
|
self.persist_settings_if_changed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── FINDNONPURGEABLE — list named objects still in use ───────
|
|
// Reports the layers, linetypes, text styles and blocks that are
|
|
// referenced by objects, and therefore cannot be purged — so it is
|
|
// clear why PURGE leaves them behind. Read-only.
|
|
"FINDNONPURGEABLE" => {
|
|
use rustc_hash::FxHashSet;
|
|
let mut layers: FxHashSet<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("FINDNONPURGEABLE: named objects in use (not purgeable):");
|
|
self.command_line
|
|
.push_output(&format!(" Layers: {}", fmt(layers)));
|
|
self.command_line
|
|
.push_output(&format!(" Linetypes: {}", fmt(linetypes)));
|
|
self.command_line
|
|
.push_output(&format!(" Text styles: {}", fmt(styles)));
|
|
self.command_line
|
|
.push_output(&format!(" Blocks: {}", fmt(blocks)));
|
|
}
|
|
|
|
// ── AUDIT — report drawing-database integrity issues ─────────
|
|
// Read-only scan: flags entities on undefined layers and block
|
|
// references to undefined block definitions. Reports only; it does
|
|
// not auto-repair (so it can never make the drawing worse).
|
|
"AUDIT" => {
|
|
use std::collections::BTreeSet;
|
|
let mut undefined_layers: BTreeSet<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(&format!("AUDIT: scanned {total} object(s)."));
|
|
if undefined_layers.is_empty() && undefined_blocks.is_empty() {
|
|
self.command_line.push_output("AUDIT: no issues found.");
|
|
} else {
|
|
if !undefined_layers.is_empty() {
|
|
self.command_line.push_error(&format!(
|
|
"AUDIT: reference(s) to undefined layer(s): {}",
|
|
undefined_layers.into_iter().collect::<Vec<_>>().join(", ")
|
|
));
|
|
}
|
|
if !undefined_blocks.is_empty() {
|
|
self.command_line.push_error(&format!(
|
|
"AUDIT: reference(s) to undefined block(s): {}",
|
|
undefined_blocks.into_iter().collect::<Vec<_>>().join(", ")
|
|
));
|
|
}
|
|
self.command_line
|
|
.push_info("AUDIT: report only — no automatic repair performed.");
|
|
}
|
|
}
|
|
|
|
// ── RENAME table entries ──────────────────────────────────────
|
|
// Bare RENAME prompts step by step (type → old → new); the argument
|
|
// form below both handles a fully-typed line and is the target the
|
|
// stepped front-end dispatches to.
|
|
"RENAME" => {
|
|
use crate::command::RenameCommand;
|
|
let c = RenameCommand::new();
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("RENAME ") => {
|
|
// Usage: RENAME <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(
|
|
"Usage: RENAME <type> <old> <new> (types: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW)"
|
|
);
|
|
} else {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
let ok = match type_str.as_str() {
|
|
"LAYER" => {
|
|
if let Some(l) = doc.layers.get_mut(&old_name) {
|
|
l.name = new_name.clone();
|
|
// Update entity references
|
|
for e in doc.entities_mut() {
|
|
if e.common().layer == old_name {
|
|
e.common_mut().layer = new_name.clone();
|
|
}
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
"STYLE" | "TEXTSTYLE" => {
|
|
if let Some(s) = doc.text_styles.get_mut(&old_name) {
|
|
s.name = new_name.clone();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
"DIMSTYLE" => {
|
|
if let Some(s) = doc.dim_styles.get_mut(&old_name) {
|
|
s.name = new_name.clone();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
"LINETYPE" | "LT" => {
|
|
if let Some(lt) = doc.line_types.get_mut(&old_name) {
|
|
lt.name = new_name.clone();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
"UCS" => {
|
|
if let Some(u) = doc.ucss.get_mut(&old_name) {
|
|
u.name = new_name.clone();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
"VIEW" => {
|
|
if let Some(v) = doc.views.get_mut(&old_name) {
|
|
v.name = new_name.clone();
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
_ => {
|
|
self.command_line.push_error(&format!("RENAME: unknown type '{}'. Use LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW", type_str));
|
|
false
|
|
}
|
|
};
|
|
if ok {
|
|
self.push_undo_snapshot(i, "RENAME");
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("RENAME: '{}' → '{}'.", old_name, new_name));
|
|
} else if type_str != "BLOCK" {
|
|
self.command_line.push_error(&format!(
|
|
"RENAME: '{}' not found in {}.",
|
|
old_name, type_str
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── System variable getters/setters ──────────────────────────────────
|
|
// CLAYER [name] — get or set current layer
|
|
// TEXTSTYLE [name] — already handled above under STYLE SET
|
|
// DIMSTYLE [name] — get or set active dim style
|
|
// LTSCALE [val] — global linetype scale
|
|
"CLAYER" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new("CLAYER", "CLAYER new current layer name:");
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("CLAYER ") => {
|
|
let name_arg = cmd.trim_start_matches("CLAYER").trim();
|
|
if name_arg.is_empty() {
|
|
let cur = &self.tabs[i].scene.document.header.current_layer_name;
|
|
self.command_line
|
|
.push_output(&format!("CLAYER = \"{cur}\""));
|
|
} else {
|
|
if self.tabs[i].scene.document.layers.contains(name_arg) {
|
|
self.tabs[i].scene.document.header.current_layer_name =
|
|
name_arg.to_string();
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("CLAYER set to \"{name_arg}\""));
|
|
} else {
|
|
self.command_line
|
|
.push_error(&format!("CLAYER: layer '{}' not found.", name_arg));
|
|
}
|
|
}
|
|
}
|
|
"CDIMSTY" | "DIMCURRENT" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c =
|
|
ValuePromptCommand::new("CDIMSTY", "CDIMSTY new current dimension style name:");
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("CDIMSTY ") || cmd.starts_with("DIMCURRENT ") => {
|
|
let name_arg = cmd.split_whitespace().skip(1).collect::<Vec<_>>().join(" ");
|
|
if name_arg.is_empty() {
|
|
let cur = &self.tabs[i].scene.document.header.current_dimstyle_name;
|
|
self.command_line
|
|
.push_output(&format!("CDIMSTY = \"{cur}\""));
|
|
} else {
|
|
if self.tabs[i].scene.document.dim_styles.contains(&name_arg) {
|
|
self.tabs[i].scene.document.header.current_dimstyle_name = name_arg.clone();
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("Active dim style set to \"{name_arg}\""));
|
|
} else {
|
|
self.command_line
|
|
.push_error(&format!("CDIMSTY: dim style '{}' not found.", name_arg));
|
|
}
|
|
}
|
|
}
|
|
"LTSCALE" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new("LTSCALE", "LTSCALE new global line-type scale:");
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("LTSCALE ") => {
|
|
let val_str = cmd.trim_start_matches("LTSCALE").trim();
|
|
if val_str.is_empty() {
|
|
let v = self.tabs[i].scene.document.header.linetype_scale;
|
|
self.command_line.push_output(&format!("LTSCALE = {v:.4}"));
|
|
} else if let Ok(v) = val_str.parse::<f64>() {
|
|
if v > 0.0 {
|
|
self.push_undo_snapshot(i, "LTSCALE");
|
|
self.tabs[i].scene.document.header.linetype_scale = v;
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("LTSCALE set to {v:.4}"));
|
|
} else {
|
|
self.command_line
|
|
.push_error("LTSCALE: value must be positive.");
|
|
}
|
|
} else {
|
|
self.command_line.push_error("Usage: LTSCALE [value]");
|
|
}
|
|
}
|
|
"PDMODE" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new(
|
|
"PDMODE",
|
|
"PDMODE new value [0=dot 1=none 2=+ 3=x 4=tick; +32 circle +64 square]:",
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("PDMODE ") => {
|
|
let val_str = cmd.trim_start_matches("PDMODE").trim();
|
|
if val_str.is_empty() {
|
|
let v = self.tabs[i].scene.document.header.point_display_mode;
|
|
self.command_line.push_output(&format!("PDMODE = {v}"));
|
|
} else if let Ok(v) = val_str.parse::<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.bump_geometry();
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(&format!("PDMODE set to {v}"));
|
|
} else {
|
|
self.command_line.push_error(
|
|
"Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)",
|
|
);
|
|
}
|
|
}
|
|
cmd if cmd.starts_with("TEXTEDITMODE ") => {
|
|
let val_str = cmd.trim_start_matches("TEXTEDITMODE").trim().to_lowercase();
|
|
if val_str.is_empty() {
|
|
let v = if self.texteditmode { 1 } else { 0 };
|
|
self.command_line
|
|
.push_output(&format!("TEXTEDITMODE = {v}"));
|
|
} else if let Some(v) =
|
|
crate::modules::annotate::textedit::parse_texteditmode(&val_str)
|
|
{
|
|
self.texteditmode = v;
|
|
let n = if v { 1 } else { 0 };
|
|
self.command_line
|
|
.push_output(&format!("TEXTEDITMODE set to {n}"));
|
|
} else {
|
|
self.command_line
|
|
.push_error("Requires 0 OR 1 OR MULTIPLE OR SINGLE");
|
|
}
|
|
}
|
|
"ISAVEBAK" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c =
|
|
ValuePromptCommand::new("ISAVEBAK", "ISAVEBAK write a .bak on save? [1 / 0]:");
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("ISAVEBAK ") => {
|
|
match cmd.trim_start_matches("ISAVEBAK").trim() {
|
|
"" => {
|
|
let v = if self.backup_on_save { 1 } else { 0 };
|
|
self.command_line.push_output(&format!("ISAVEBAK = {v}"));
|
|
}
|
|
"0" => {
|
|
self.backup_on_save = false;
|
|
self.persist_settings_if_changed();
|
|
self.command_line.push_output("ISAVEBAK set to 0");
|
|
}
|
|
"1" => {
|
|
self.backup_on_save = true;
|
|
self.persist_settings_if_changed();
|
|
self.command_line.push_output("ISAVEBAK set to 1");
|
|
}
|
|
_ => self.command_line.push_error("Requires 0 or 1"),
|
|
}
|
|
}
|
|
"FILEASSOC" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new(
|
|
"FILEASSOC",
|
|
"FILEASSOC register as a .dwg/.dxf handler? [1 / 0]:",
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("FILEASSOC ") => {
|
|
match cmd.trim_start_matches("FILEASSOC").trim() {
|
|
"" => {
|
|
let v = if self.file_assoc_enabled { 1 } else { 0 };
|
|
self.command_line.push_output(&format!("FILEASSOC = {v}"));
|
|
}
|
|
"1" => {
|
|
self.file_assoc_enabled = true;
|
|
self.persist_settings_if_changed();
|
|
match crate::io::file_association::register_as_handler() {
|
|
Ok(()) => self.command_line.push_output(
|
|
"FILEASSOC set to 1 — registered as a .dwg/.dxf/.bak handler",
|
|
),
|
|
Err(e) => self
|
|
.command_line
|
|
.push_error(&format!("FILEASSOC: registration failed: {e}")),
|
|
}
|
|
}
|
|
"0" => {
|
|
self.file_assoc_enabled = false;
|
|
self.persist_settings_if_changed();
|
|
match crate::io::file_association::unregister_handler() {
|
|
Ok(()) => self
|
|
.command_line
|
|
.push_output("FILEASSOC set to 0 — unregistered as a file handler"),
|
|
Err(e) => self
|
|
.command_line
|
|
.push_error(&format!("FILEASSOC: unregister failed: {e}")),
|
|
}
|
|
}
|
|
_ => self.command_line.push_error("Requires 0 or 1"),
|
|
}
|
|
}
|
|
"SAVETIME" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new(
|
|
"SAVETIME",
|
|
"SAVETIME autosave interval in minutes (0 = off):",
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("SAVETIME ") => {
|
|
let arg = cmd.trim_start_matches("SAVETIME").trim();
|
|
if arg.is_empty() {
|
|
self.command_line
|
|
.push_output(&format!("SAVETIME = {}", self.savetime_min));
|
|
return Some(Task::none());
|
|
}
|
|
match arg.parse::<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("Requires a non-negative number of minutes (0 = off)"),
|
|
}
|
|
}
|
|
"PDSIZE" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new(
|
|
"PDSIZE",
|
|
"PDSIZE new point size (0 = 5% of viewport, <0 = absolute):",
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("PDSIZE ") => {
|
|
let val_str = cmd.trim_start_matches("PDSIZE").trim();
|
|
if val_str.is_empty() {
|
|
let v = self.tabs[i].scene.document.header.point_display_size;
|
|
self.command_line.push_output(&format!("PDSIZE = {v:.4}"));
|
|
} else if let Ok(v) = val_str.parse::<f64>() {
|
|
self.push_undo_snapshot(i, "PDSIZE");
|
|
self.tabs[i].scene.document.header.point_display_size = v;
|
|
self.tabs[i].scene.bump_geometry();
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("PDSIZE set to {v:.4}"));
|
|
} else {
|
|
self.command_line.push_error(
|
|
"Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)",
|
|
);
|
|
}
|
|
}
|
|
cmd if cmd == "DDPTYPE" => {
|
|
// The dialog shows the magnitude; the sign (relative/absolute)
|
|
// is driven by the radio buttons. A positive PDSIZE is absolute;
|
|
// zero or negative is relative.
|
|
let pdsize = self.tabs[i].scene.document.header.point_display_size;
|
|
self.point_size_relative = pdsize <= 0.0;
|
|
self.point_size_buf = format!("{}", pdsize.abs());
|
|
self.active_modal = Some(super::super::ModalKind::PointStyle);
|
|
}
|
|
"LWDISPLAY" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c =
|
|
ValuePromptCommand::new("LWDISPLAY", "LWDISPLAY show lineweights? [ON / OFF]:");
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("LWDISPLAY ") => {
|
|
let val_str = cmd.trim_start_matches("LWDISPLAY").trim();
|
|
let parsed: Result<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("Usage: LWDISPLAY [ON|OFF]"),
|
|
Ok(Some(v)) => {
|
|
self.push_undo_snapshot(i, "LWDISPLAY");
|
|
self.tabs[i].scene.document.header.lineweight_display = v;
|
|
// No retessellate — the wire shader honours the flag via uniforms.
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(&format!("LWDISPLAY {}", if v { "ON" } else { "OFF" }));
|
|
}
|
|
Ok(None) => {
|
|
let v = self.tabs[i].scene.document.header.lineweight_display;
|
|
self.command_line
|
|
.push_output(&format!("LWDISPLAY = {}", if v { "ON" } else { "OFF" }));
|
|
}
|
|
}
|
|
}
|
|
"CELTSCALE" => {
|
|
use crate::command::ValuePromptCommand;
|
|
let c = ValuePromptCommand::new(
|
|
"CELTSCALE",
|
|
"CELTSCALE new current-object line-type scale:",
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("CELTSCALE ") => {
|
|
let val_str = cmd.trim_start_matches("CELTSCALE").trim();
|
|
if val_str.is_empty() {
|
|
let v = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.header
|
|
.current_entity_linetype_scale;
|
|
self.command_line
|
|
.push_output(&format!("CELTSCALE = {v:.4}"));
|
|
} else if let Ok(v) = val_str.parse::<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(&format!("CELTSCALE set to {v:.4}"));
|
|
} else {
|
|
self.command_line
|
|
.push_error("CELTSCALE: value must be positive.");
|
|
}
|
|
} else {
|
|
self.command_line.push_error("Usage: CELTSCALE [value]");
|
|
}
|
|
}
|
|
|
|
// ── SCALETEXT — rescale selected Text/MText entities ─────────────────
|
|
// Usage: SCALETEXT <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)
|
|
.collect();
|
|
if selected_handles.is_empty() {
|
|
self.command_line
|
|
.push_error("SCALETEXT: select Text/MText entities first.");
|
|
} else {
|
|
let (use_absolute, value) = match (
|
|
parts.first().map(|s| s.to_uppercase()).as_deref(),
|
|
parts.get(1),
|
|
) {
|
|
(Some("H"), Some(v)) => (true, v.parse::<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("SCALETEXT: value must be positive.");
|
|
} else {
|
|
self.push_undo_snapshot(i, "SCALETEXT");
|
|
let mut count = 0usize;
|
|
for sh in &selected_handles {
|
|
for entity in self.tabs[i].scene.document.entities_mut() {
|
|
if entity.common().handle != *sh {
|
|
continue;
|
|
}
|
|
match entity {
|
|
acadrust::EntityType::Text(t) => {
|
|
t.height =
|
|
if use_absolute { val } else { t.height * val };
|
|
count += 1;
|
|
}
|
|
acadrust::EntityType::MText(t) => {
|
|
t.height =
|
|
if use_absolute { val } else { t.height * val };
|
|
count += 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if count > 0 {
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(&format!(
|
|
"SCALETEXT: scaled {count} text entity(ies)."
|
|
));
|
|
} else {
|
|
self.command_line
|
|
.push_error("SCALETEXT: no Text/MText in selection.");
|
|
}
|
|
}
|
|
} else {
|
|
self.command_line
|
|
.push_info("Usage: SCALETEXT <factor> or SCALETEXT H <height>");
|
|
}
|
|
}
|
|
}
|
|
|
|
_ => return None,
|
|
}
|
|
Some(self.finish_dispatch(cmd))
|
|
}
|
|
}
|