forked from 1b-agent/opencadstudio-upstream
Bug:
SETVAR LUPREC and SETVAR AUPREC stored any parseable i16 verbatim, and
the header value flows through UnitContext into the per-call
formatters, which applied only a lower bound (.max(0)) with no upper
bound: format_length and format_angle rendered {:.*} with the raw
precision. SETVAR LUPREC 30000 therefore made format_length emit
30003-character strings on every properties and annotation formatting
call - a ~30 KB allocation per formatted value - while format_area in
the same file had long since capped itself with .min(15) and the units
dialog only ever offers 0-8 places. Negative values were also stored
verbatim.
Reproduce:
1. Command line: SETVAR LUPREC 30000 (or AUPREC 30000), then select
something so the properties palette formats lengths/angles.
2. Before: the header stores 30000 and each formatted value is ~30 KB
(the test measured 30003 chars for one length); -3 is stored as-is.
3. After: the setter stores 8 (0-8 range, echoed truthfully), -3
stores 0, 5 stores 5 - and even a file carrying LUPREC 30000 in its
header formats short output, because the formatters cap themselves.
Fix:
Two layers. The formatters get the cap format_area already uses:
format_signed_length and format_signed_angle now clamp with
.max(0).min(15), so every source - SETVAR, the units dialog, and
values carried in DWG/DXF files - produces bounded output on the hot
paths (15 matches the point past which f64 decimal output is
meaningless, which is why format_area was written that way). The
setters store x.clamp(0, 8) with the clamped value echoed back, the
same stay-inside-the-app's-own-UI-range rule SETVAR ISOLINES follows
against the Options slider. Architectural and fractional formats
ignore precision (fixed 1/64-inch resolution) and are unaffected.
Regression tests mirror the existing patterns: one forces hostile
precision through the formatters while saving and restoring the
thread-local unit context, one drives SETVAR through the command line
and checks the stored header values.
3714 lines
199 KiB
Rust
3714 lines
199 KiB
Rust
use super::*;
|
||
|
||
impl OpenCADStudio {
|
||
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||
match cmd {
|
||
"CETRANSPARENCY" => return self.dispatch_styleprops("SETVAR CETRANSPARENCY", i),
|
||
cmd if cmd.starts_with("CETRANSPARENCY ") => {
|
||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||
}
|
||
"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),
|
||
// The mixed state is shown, never chosen.
|
||
"FRAMES3" => return Some(Task::none()),
|
||
"UOSNAP0" => return self.dispatch_styleprops("SETVAR UOSNAP 0", i),
|
||
"UOSNAP1" => return self.dispatch_styleprops("SETVAR UOSNAP 1", 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 codec::types::Color;
|
||
let describe = |c: &Color| match c {
|
||
Color::ByLayer => "ByLayer".to_string(),
|
||
Color::ByBlock => "ByBlock".to_string(),
|
||
Color::Index(n) => crate::tf!("index {n}").into_owned(),
|
||
_ => "(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(
|
||
crate::t!("Usage: COLOR <ByLayer|ByBlock|1-255|red|yellow|green|cyan|blue|magenta|white>").as_ref(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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(), codec::types::Handle::NULL))
|
||
} else if name.eq_ignore_ascii_case("BYBLOCK") {
|
||
Some(("ByBlock".to_string(), codec::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 {
|
||
codec::EntityType::Text(t) => Some(t.style.clone()),
|
||
codec::EntityType::MText(t) => Some(t.style.clone()),
|
||
codec::EntityType::AttributeDefinition(a) => {
|
||
Some(a.text_style.clone())
|
||
}
|
||
codec::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();
|
||
|
||
// Live blocks are reachable from layouts and style roots
|
||
// through the shared graph. Dangling layouts do not count.
|
||
let live_blocks: rustc_hash::FxHashSet<String> = {
|
||
let doc = &self.tabs[i].scene.document;
|
||
let is_real_layout = |br: &codec::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(codec::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 {
|
||
let Some(entity) = doc.get_entity(h) else {
|
||
continue;
|
||
};
|
||
out.extend(
|
||
crate::scene::render_graph::entity_block_uses(
|
||
doc,
|
||
entity,
|
||
1.0,
|
||
)
|
||
.into_iter()
|
||
.filter(|block_use| !block_use.block.is_null())
|
||
.map(|block_use| block_use.insert.block_name),
|
||
);
|
||
}
|
||
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();
|
||
stack.extend(
|
||
crate::scene::render_graph::document_block_uses(doc)
|
||
.into_iter()
|
||
.map(|block_use| block_use.insert.block_name),
|
||
);
|
||
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![]
|
||
};
|
||
|
||
// Remove unreachable blocks but retain live xrefs and space containers.
|
||
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 dependencies from
|
||
// a past detach still use 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();
|
||
}
|
||
|
||
// Remove draw-order tables whose owning blocks are gone.
|
||
let mut n_sortents = 0usize;
|
||
if do_blocks {
|
||
let live_blocks: rustc_hash::FxHashSet<codec::Handle> = self.tabs[i]
|
||
.scene
|
||
.document
|
||
.block_records
|
||
.iter()
|
||
.map(|br| br.handle)
|
||
.collect();
|
||
let orphans: Vec<codec::Handle> = self.tabs[i]
|
||
.scene
|
||
.document
|
||
.objects
|
||
.iter()
|
||
.filter_map(|(h, o)| match o {
|
||
codec::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(crate::tf!("{n_layers} layer(s)").into_owned());
|
||
}
|
||
if n_styles > 0 {
|
||
parts.push(crate::tf!("{n_styles} text style(s)").into_owned());
|
||
}
|
||
if n_lts > 0 {
|
||
parts.push(crate::tf!("{n_lts} linetype(s)").into_owned());
|
||
}
|
||
if n_blocks > 0 {
|
||
parts.push(crate::tf!("{n_blocks} block(s)").into_owned());
|
||
}
|
||
if n_sortents > 0 {
|
||
parts.push(crate::tf!("{n_sortents} stale draw-order table(s)").into_owned());
|
||
}
|
||
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(
|
||
crate::t!("Usage: CHPROP <prop> <val> (props: LAYER COLOR LINETYPE LTSCALE)").as_ref(),
|
||
);
|
||
} 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<codec::types::Color> = if prop == "COLOR" {
|
||
value
|
||
.parse::<i16>()
|
||
.ok()
|
||
.map(codec::types::Color::from_index)
|
||
} else {
|
||
None
|
||
};
|
||
let ltscale_val: Option<f64> = if prop == "LTSCALE" {
|
||
value.parse().ok()
|
||
} else {
|
||
None
|
||
};
|
||
let transparency_val: Option<codec::types::Transparency> =
|
||
if prop == "TRANSPARENCY" {
|
||
value
|
||
.parse::<f64>()
|
||
.ok()
|
||
.map(codec::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 known = matches!(
|
||
prop.as_str(),
|
||
"LAYER" | "LINETYPE" | "LT" | "LTSCALE" | "COLOR" | "TRANSPARENCY"
|
||
);
|
||
if !known {
|
||
self.command_line.push_error(crate::tf!(
|
||
"CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop
|
||
).as_ref());
|
||
} else {
|
||
let mut changed = 0usize;
|
||
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||
if let Some(entity) =
|
||
app.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();
|
||
common.color_name = None;
|
||
common.color_book_handle = None;
|
||
changed += 1;
|
||
}
|
||
"TRANSPARENCY" => {
|
||
common.transparency = transparency_val.unwrap();
|
||
changed += 1;
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
});
|
||
// 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.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.
|
||
"SETBYLAYERMODE" => {
|
||
let command = crate::modules::draw::modify::setbylayer::ModeCommand;
|
||
self.command_line.push_info(&command.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||
}
|
||
value if value.starts_with("SETBYLAYERMODE ") => {
|
||
if let Ok(mode) = value.trim_start_matches("SETBYLAYERMODE ").trim().parse::<u8>() {
|
||
crate::modules::draw::modify::setbylayer::set_mode(mode);
|
||
} else { self.command_line.push_error("SETBYLAYERMODE requires an integer from 0 to 255."); }
|
||
}
|
||
"SETBYLAYER" | "-SETBYLAYER" => {
|
||
let command = crate::modules::draw::modify::setbylayer::SetByLayerCommand::new(
|
||
self.tabs[i].scene.selected.iter().copied().collect(),
|
||
);
|
||
self.command_line.push_info(&command.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||
}
|
||
cmd if cmd.starts_with("SETBYLAYER_APPLY ") => {
|
||
let flags: Vec<_> = cmd.split_whitespace().skip(1).collect();
|
||
if flags.len() != 2 || flags.iter().any(|flag| !matches!(*flag, "0" | "1")) {
|
||
return Some(Task::none());
|
||
}
|
||
let mask = crate::modules::draw::modify::setbylayer::mode();
|
||
let change_byblock = flags[0] == "1";
|
||
let include_blocks = flags[1] == "1";
|
||
let mut 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 include_blocks {
|
||
let mut visited: std::collections::HashSet<_> = handles.iter().copied().collect();
|
||
let mut index = 0;
|
||
while index < handles.len() {
|
||
let children = match self.tabs[i].scene.document.get_entity(handles[index]) {
|
||
Some(codec::EntityType::Insert(insert)) => self.tabs[i].scene.document
|
||
.block_records.get(&insert.block_name)
|
||
.map(|block| block.entity_handles.clone()).unwrap_or_default(),
|
||
_ => Vec::new(),
|
||
};
|
||
for child in children {
|
||
if visited.insert(child) && !self.tabs[i].scene.is_layer_locked(child) {
|
||
handles.push(child);
|
||
}
|
||
}
|
||
index += 1;
|
||
}
|
||
}
|
||
if handles.is_empty() {
|
||
self.command_line
|
||
.push_error(crate::t!("SETBYLAYER: select entities first.").as_ref());
|
||
} else {
|
||
let has_changes = handles.iter().any(|handle| {
|
||
self.tabs[i].scene.document.get_entity(*handle).is_some_and(|entity| {
|
||
let mut common = entity.common().clone();
|
||
crate::modules::draw::modify::setbylayer::apply_mask(
|
||
&mut common, mask, change_byblock,
|
||
)
|
||
})
|
||
});
|
||
if !has_changes {
|
||
self.command_line.push_output(
|
||
crate::t!("SETBYLAYER: no properties required changes.").as_ref(),
|
||
);
|
||
return Some(Task::none());
|
||
}
|
||
self.push_undo_snapshot(i, "SETBYLAYER");
|
||
let mut changed = Vec::new();
|
||
for handle in &handles {
|
||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) {
|
||
if crate::modules::draw::modify::setbylayer::apply_mask(
|
||
entity.common_mut(), mask, change_byblock,
|
||
) {
|
||
changed.push(*handle);
|
||
}
|
||
}
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
let changes: Vec<_> = changed
|
||
.iter()
|
||
.copied()
|
||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||
.collect();
|
||
self.tabs[i].scene.bump_entities(&changes);
|
||
self.refresh_properties();
|
||
self.command_line.push_output(crate::tf!(
|
||
"SETBYLAYER: reset {} entity/entities to ByLayer.",
|
||
changed.len()
|
||
).as_ref());
|
||
}
|
||
}
|
||
|
||
// OVERKILL gathers objects, exposes cleanup settings, then applies one undo group.
|
||
"OVERKILL" | "-OVERKILL" => {
|
||
let handles=self.tabs[i].scene.selected_entities().iter().map(|(h,_)|*h).collect();
|
||
let command=crate::modules::draw::modify::overkill::OverkillCommand::new(handles);
|
||
self.command_line.push_info(&command.prompt());
|
||
self.tabs[i].active_cmd=Some(Box::new(command));
|
||
}
|
||
value if value.starts_with("OVERKILL_APPLY ") => {
|
||
use crate::modules::draw::modify::overkill::{normalized,optimize};
|
||
let values:Vec<_>=value.split_whitespace().skip(1).collect();
|
||
if values.len()!=6 { return Some(self.finish_dispatch(cmd)); }
|
||
let tolerance=values[0].parse::<f64>().unwrap_or(1e-6);
|
||
let ignore=values[1].parse::<u16>().unwrap_or(0);
|
||
let optimize_plines=values[2]=="1";
|
||
let overlap=values[3]=="1";
|
||
let end_to_end=values[4]=="1";
|
||
let preserve_associative=values[5]=="1";
|
||
let mut candidates:Vec<_>=self.tabs[i].scene.selected_entities().into_iter()
|
||
.filter(|(h,e)|!self.tabs[i].scene.is_layer_locked(*h)
|
||
&& (!preserve_associative||e.common().reactors.is_empty()))
|
||
.map(|(h,e)|(h,e.clone())).collect();
|
||
candidates.sort_by_key(|(h,_)|h.value());
|
||
let mut changed=std::collections::HashSet::new();
|
||
if optimize_plines { for (h,e) in &mut candidates {
|
||
let before=e.clone(); optimize(e,tolerance); if *e!=before {changed.insert(*h);}
|
||
} }
|
||
let mut removed=std::collections::HashSet::new();
|
||
// Iterate to a fixed point: a bridge can join two previously disjoint intervals.
|
||
loop {
|
||
let mut progress=false;
|
||
for a in 0..candidates.len() {
|
||
if removed.contains(&candidates[a].0) {continue;}
|
||
for b in a+1..candidates.len() {
|
||
if removed.contains(&candidates[b].0) {continue;}
|
||
let left=normalized(&candidates[a].1,ignore);
|
||
let right=normalized(&candidates[b].1,ignore);
|
||
if left==right {
|
||
removed.insert(candidates[b].0);progress=true;continue;
|
||
}
|
||
if overlap {
|
||
let contained = |circle: &codec::entities::Circle, arc: &codec::entities::Arc| {
|
||
circle.common == arc.common && circle.thickness == arc.thickness
|
||
&& kernel::space::arc_union::circle_contains_arc(
|
||
[circle.center.x,circle.center.y,circle.center.z],
|
||
[circle.normal.x,circle.normal.y,circle.normal.z],circle.radius,
|
||
kernel::space::arc_union::CircularArc { center:[arc.center.x,arc.center.y,arc.center.z],
|
||
normal:[arc.normal.x,arc.normal.y,arc.normal.z],radius:arc.radius,start:arc.start_angle,end:arc.end_angle })
|
||
};
|
||
match (&left,&right) {
|
||
(codec::EntityType::Circle(circle),codec::EntityType::Arc(arc)) if contained(circle,arc) => {
|
||
removed.insert(candidates[b].0);progress=true;continue;
|
||
}
|
||
(codec::EntityType::Arc(arc),codec::EntityType::Circle(circle)) if contained(circle,arc) => {
|
||
removed.insert(candidates[a].0);progress=true;break;
|
||
}
|
||
_ => {},
|
||
}
|
||
}
|
||
if let (codec::EntityType::Arc(l),codec::EntityType::Arc(r))=(&left,&right) {
|
||
use kernel::space::arc_union::{CircularArc,ArcUnionKind,circular_arc_union};
|
||
if l.common!=r.common || l.thickness!=r.thickness {continue;}
|
||
let arc=|v:&codec::entities::Arc|CircularArc{center:[v.center.x,v.center.y,v.center.z],normal:[v.normal.x,v.normal.y,v.normal.z],radius:v.radius,start:v.start_angle,end:v.end_angle};
|
||
let Some(union)=circular_arc_union(arc(l),arc(r),tolerance) else {continue;};
|
||
let allowed=match union.kind {ArcUnionKind::Duplicate=>true,ArcUnionKind::Overlap=>overlap,ArcUnionKind::EndToEnd=>end_to_end};
|
||
if !allowed {continue;}
|
||
if let codec::EntityType::Arc(source)=&candidates[a].1 {
|
||
let replacement=if union.full_circle {
|
||
let mut circle=codec::entities::Circle::new();
|
||
circle.common=source.common.clone();circle.center=source.center.clone();circle.normal=source.normal.clone();circle.radius=source.radius;circle.thickness=source.thickness;
|
||
codec::EntityType::Circle(circle)
|
||
} else {
|
||
let mut arc=source.clone();arc.start_angle=union.start;arc.end_angle=union.end;codec::EntityType::Arc(arc)
|
||
};
|
||
candidates[a].1=replacement;
|
||
}
|
||
changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true;
|
||
continue;
|
||
}
|
||
let (codec::EntityType::Line(l),codec::EntityType::Line(r))=(&left,&right)
|
||
else {continue;};
|
||
if l.common!=r.common||l.thickness!=r.thickness||l.normal!=r.normal {continue;}
|
||
let point=|p:codec::types::Vector3|[p.x,p.y,p.z];
|
||
let Some(union)=kernel::space::line_union(
|
||
[point(l.start),point(l.end)],[point(r.start),point(r.end)],tolerance)
|
||
else {continue;};
|
||
let allowed=match union.kind {
|
||
kernel::space::LineUnionKind::Duplicate=>true,
|
||
kernel::space::LineUnionKind::Overlap=>overlap,
|
||
kernel::space::LineUnionKind::EndToEnd=>end_to_end,
|
||
};
|
||
if !allowed {continue;}
|
||
if let codec::EntityType::Line(line)=&mut candidates[a].1 {
|
||
line.start=codec::types::Vector3::new(union.start[0],union.start[1],union.start[2]);
|
||
line.end=codec::types::Vector3::new(union.end[0],union.end[1],union.end[2]);
|
||
}
|
||
changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true;
|
||
}
|
||
}
|
||
if !progress {break;}
|
||
}
|
||
if !changed.is_empty()||!removed.is_empty() {
|
||
self.push_undo_snapshot(i,"OVERKILL");
|
||
let updates:Vec<_>=candidates.into_iter().filter(|(h,_)|changed.contains(h)&&!removed.contains(h)).collect();
|
||
let updated_count = updates.len();
|
||
let mut changes = Vec::new();
|
||
for (handle, entity) in updates {
|
||
if let Some(target) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||
*target = entity;
|
||
changes.push((handle, crate::scene::ChangeKind::Modified));
|
||
}
|
||
}
|
||
self.tabs[i].scene.bump_entities(&changes);
|
||
let mut handles:Vec<_>=removed.iter().copied().collect();
|
||
handles.sort_by_key(|handle| handle.value());
|
||
self.tabs[i].scene.erase_entities(&handles);
|
||
self.tabs[i].dirty=true;self.refresh_properties();
|
||
self.command_line.push_output(&format!("OVERKILL: removed {} objects; updated {} objects.",removed.len(),updated_count));
|
||
} else {
|
||
self.command_line.push_output("OVERKILL: no objects required changes.");
|
||
}
|
||
}
|
||
// ── 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()),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── COMMANDLINEFADETIME — command-line history fade time ───
|
||
// How long history lines stay visible above the command window,
|
||
// in ms (0–60000, default 3000). Bare form prompts for a value
|
||
// (Enter reports current); 0 hides transient overlay lines.
|
||
"COMMANDLINEFADETIME" => {
|
||
use crate::command::ValuePromptCommand;
|
||
let c = ValuePromptCommand::new(
|
||
"COMMANDLINEFADETIME",
|
||
"COMMANDLINEFADETIME overlay fade time in ms (0-60000, 0 = hide) <Enter reports>:",
|
||
);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
|
||
// ── 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"
|
||
| "SHORTCUTMENU"
|
||
| "SHORTCUTMENUDURATION"
|
||
| "CURSORSIZE"
|
||
| "PICKBOX"
|
||
| "CURSORTYPE"
|
||
| "SNAPANG"
|
||
| "TEXTFILL"
|
||
| "CLIPROMPTLINES"
|
||
| "COMMANDLINEFADETIME"
|
||
| "ATTREQ"
|
||
| "ATTDIA"
|
||
| "DIMASSOC"
|
||
| "ANGBASE"
|
||
| "ANGDIR"
|
||
| "REGENMODE"
|
||
| "BLIPMODE"
|
||
| "SPLFRAME"
|
||
| "DELOBJ"
|
||
| "SOLIDHIST"
|
||
| "SHOWHIST"
|
||
| "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"
|
||
| "CMLSCALE"
|
||
| "CMLSTYLE"
|
||
| "TEXTQLTY"
|
||
| "SORTENTS"
|
||
| "FRAME"
|
||
| "IMAGEFRAME"
|
||
| "PDFFRAME"
|
||
| "DWFFRAME"
|
||
| "DGNFRAME"
|
||
| "PDFOSNAP"
|
||
| "DWFOSNAP"
|
||
| "DGNOSNAP"
|
||
| "UOSNAP"
|
||
| "PDFIMPORTMODE"
|
||
| "PDFIMPORTFILTER"
|
||
| "PDFIMPORTLAYERS"
|
||
| "PDFIMPORTIMAGEPATH"
|
||
| "XDWGFADECTL"
|
||
| "POINTCLOUDCLIPFRAME"
|
||
| "XCLIPFRAME"
|
||
| "WIPEOUTFRAME"
|
||
| "HALOGAP"
|
||
| "TRACEWID"
|
||
| "SKETCHINC"
|
||
| "SKPOLY"
|
||
| "SKTOLERANCE"
|
||
| "DONUTID"
|
||
| "DONUTOD"
|
||
| "CENTEREXE"
|
||
| "CENTERLAYER"
|
||
| "CENTERLTYPE"
|
||
| "CENTERLTSCALE"
|
||
| "CENTERLTYPEFILE"
|
||
| "CENTERCROSSSIZE"
|
||
| "CENTERCROSSGAP"
|
||
| "CENTERMARKEXE"
|
||
| "DIMCONTINUEMODE"
|
||
| "COLORTHEME"
|
||
| "SELECTIONAREA"
|
||
| "SELECTIONAREAOPACITY"
|
||
| "SELECTIONEFFECT"
|
||
| "SELECTIONEFFECTCOLOR"
|
||
| "WINDOWSAREACOLOR"
|
||
| "WINDOWAREACOLOR"
|
||
| "CROSSINGAREACOLOR"
|
||
| "SELECTIONPREVIEW"
|
||
| "GRIPSIZE"
|
||
| "GRIPCOLOR"
|
||
| "GRIPHOT"
|
||
| "GRIPHOVER"
|
||
| "GRIPOBJLIMIT"
|
||
| "CONSTRAINTSOLVEMODE"
|
||
| "CONSTRAINTINFER"
|
||
| "CONSTRAINTBARDISPLAY"
|
||
| "CONSTRAINTBARMODE"
|
||
| "CONSTRAINTNAMEFORMAT"
|
||
| "DYNCONSTRAINTDISPLAY"
|
||
| "CCONSTRAINTFORM"
|
||
) =>
|
||
{
|
||
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(&super::plotvars::setvar_listing());
|
||
self.command_line.push_info(
|
||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE CONSTRAINTNAMEFORMAT DYNCONSTRAINTDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||
);
|
||
} else {
|
||
if name == "XDWGFADECTL" {
|
||
let current = crate::scene::cache::block_cache::xref_fade_ctl();
|
||
if let Some(value) = &value {
|
||
match value.parse::<i32>().ok().filter(|v| (-90..=90).contains(v)) {
|
||
Some(fade) => self.set_xref_fade(fade),
|
||
None => self.command_line.push_error("Requires an integer between -90 and 90."),
|
||
}
|
||
} else {
|
||
self.command_line.push_output(&format!("Enter new value for XDWGFADECTL <{current}>:"));
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
if name == "CETRANSPARENCY" {
|
||
let current = self.tabs[i].scene.document.current_entity_transparency();
|
||
if let Some(value) = &value {
|
||
match crate::scene::creation_style::parse_current_transparency(value) {
|
||
Some(transparency) => {
|
||
if current != transparency {
|
||
self.push_undo_snapshot(i, &name);
|
||
if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) {
|
||
self.discard_last_undo_entry(i);
|
||
self.command_line.push_error("CETRANSPARENCY: drawing variable dictionary is invalid.");
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.refresh_properties();
|
||
}
|
||
self.command_line.push_output(&format!("CETRANSPARENCY = {}", crate::scene::creation_style::current_transparency_label(transparency)));
|
||
}
|
||
None => self.command_line.push_error("CETRANSPARENCY: expected ByLayer (-1), ByBlock (-2), or an integer from 0 to 90."),
|
||
}
|
||
} else {
|
||
self.command_line.push_output(&format!("Enter new value for CETRANSPARENCY <{}>:", crate::scene::creation_style::current_transparency_label(current)));
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
// Snapping to the geometry inside underlays (one switch for
|
||
// PDF underlays and underlays in general).
|
||
// The PDF Import Settings' options as bits.
|
||
if matches!(name.as_str(), "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS") {
|
||
use crate::modules::insert::pdf_import::{import_settings, set_import_settings, ImportLayers};
|
||
let mut settings = import_settings();
|
||
let (current, max) = match name.as_str() {
|
||
"PDFIMPORTMODE" => (settings.mode(), 31),
|
||
"PDFIMPORTFILTER" => (settings.filter(), 15),
|
||
_ => (settings.layers_value(), 2),
|
||
};
|
||
if let Some(value) = &value {
|
||
match value.parse::<i16>().ok().filter(|v| (0..=max).contains(v)) {
|
||
Some(v) => {
|
||
match name.as_str() {
|
||
"PDFIMPORTMODE" => settings.set_mode(v),
|
||
"PDFIMPORTFILTER" => settings.set_filter(v),
|
||
_ => {
|
||
settings.layers = [ImportLayers::Pdf, ImportLayers::Object, ImportLayers::Current][v as usize]
|
||
}
|
||
}
|
||
set_import_settings(settings);
|
||
}
|
||
None => self.command_line.push_error(&format!("Requires an integer between 0 and {max}.")),
|
||
}
|
||
} else {
|
||
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
// A folder name; "." clears it.
|
||
if name == "PDFIMPORTIMAGEPATH" {
|
||
use crate::modules::insert::pdf_import::{image_path, set_image_path};
|
||
match &value {
|
||
Some(value) => {
|
||
let value = value.trim().trim_matches('"');
|
||
set_image_path(if value == "." { String::new() } else { value.to_string() });
|
||
}
|
||
None => {
|
||
self.command_line.push_output(&format!(
|
||
"Enter new value for PDFIMPORTIMAGEPATH, or . for none <\"{}\">:",
|
||
image_path()
|
||
));
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
}
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
if matches!(name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP") {
|
||
use crate::scene::model::pdf_vector as pv;
|
||
use codec::entities::UnderlayType;
|
||
// UOSNAP sets every kind and reads 2 while they differ.
|
||
let kind = match name.as_str() {
|
||
"PDFOSNAP" => Some(UnderlayType::Pdf),
|
||
"DWFOSNAP" => Some(UnderlayType::Dwf),
|
||
"DGNOSNAP" => Some(UnderlayType::Dgn),
|
||
_ => None,
|
||
};
|
||
let current = kind.map_or_else(pv::uosnap, |kind| i16::from(pv::underlay_osnap(kind)));
|
||
if let Some(value) = &value {
|
||
match value.parse::<i16>().ok().filter(|value| (0..=1).contains(value)) {
|
||
Some(mode) => {
|
||
if current != mode {
|
||
match kind {
|
||
Some(kind) => pv::set_underlay_osnap(kind, mode == 1),
|
||
None => pv::set_uosnap(mode == 1),
|
||
}
|
||
self.tabs[i].scene.reseed_underlays();
|
||
self.sync_underlay_tab();
|
||
}
|
||
}
|
||
None => self.command_line.push_error("Requires 0 or 1 only"),
|
||
}
|
||
} else {
|
||
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
if matches!(name.as_str(), "SHOWHIST" | "SOLIDHIST") {
|
||
let current = if name == "SHOWHIST" {
|
||
self.tabs[i].scene.document.header.show_solid_history.clamp(0, 2)
|
||
} else {
|
||
i16::from(self.tabs[i].scene.document.header.record_solid_history)
|
||
};
|
||
if let Some(value) = &value {
|
||
let maximum = if name == "SHOWHIST" { 2 } else { 1 };
|
||
match value.parse::<i16>().ok().filter(|value| (0..=maximum).contains(value)) {
|
||
Some(mode) => {
|
||
if current != mode {
|
||
self.push_undo_snapshot(i, &name);
|
||
if name == "SHOWHIST" {
|
||
self.tabs[i].scene.document.header.show_solid_history = mode;
|
||
self.tabs[i].scene.bump_geometry();
|
||
} else {
|
||
self.tabs[i].scene.document.header.record_solid_history = mode != 0;
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.refresh_properties();
|
||
}
|
||
self.command_line.push_output(&crate::tf!("{name} = {mode}"));
|
||
}
|
||
None => self.command_line.push_error(&crate::tf!("{name}: expected an integer from 0 to {maximum}.")),
|
||
}
|
||
} else {
|
||
self.command_line.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
return Some(self.finish_dispatch(cmd));
|
||
}
|
||
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;
|
||
// IMAGEFRAME also lands in the profile.
|
||
self.save_config();
|
||
}
|
||
self.command_line
|
||
.push_output(crate::t!("Regenerating model.").as_ref());
|
||
}
|
||
// Out of range: say so and ask again.
|
||
_ => {
|
||
self.command_line.push_error(
|
||
crate::t!("Requires an integer between 0 and 2.").as_ref(),
|
||
);
|
||
self.command_line.push_output(
|
||
crate::tf!("Enter new value for {name} <{current}>:")
|
||
.as_ref(),
|
||
);
|
||
self.pending_setvar = Some(name.clone());
|
||
}
|
||
},
|
||
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));
|
||
}
|
||
if matches!(
|
||
name.as_str(),
|
||
"CENTEREXE"
|
||
| "CENTERLAYER"
|
||
| "CENTERLTYPE"
|
||
| "CENTERLTSCALE"
|
||
| "CENTERLTYPEFILE"
|
||
| "CENTERCROSSSIZE"
|
||
| "CENTERCROSSGAP"
|
||
| "CENTERMARKEXE"
|
||
) {
|
||
let settings = self.tabs[i].scene.centerline_settings();
|
||
let current = match name.as_str() {
|
||
"CENTEREXE" => settings.extension.to_string(),
|
||
"CENTERLAYER" => settings.layer,
|
||
"CENTERLTYPE" => settings.linetype,
|
||
"CENTERLTSCALE" => settings.linetype_scale.to_string(),
|
||
"CENTERLTYPEFILE" => settings.linetype_file,
|
||
"CENTERCROSSSIZE" => settings.cross_size,
|
||
"CENTERCROSSGAP" => settings.cross_gap,
|
||
"CENTERMARKEXE" => i16::from(settings.mark_extensions).to_string(),
|
||
_ => unreachable!(),
|
||
};
|
||
if let Some(value) = &value {
|
||
match self.tabs[i].scene.set_centerline_setting(&name, value) {
|
||
Ok(message) => {
|
||
self.tabs[i].dirty = true;
|
||
self.command_line.push_output(&message);
|
||
}
|
||
Err(error) => self.command_line.push_error(&error),
|
||
}
|
||
} else {
|
||
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));
|
||
}
|
||
if name == "DIMCONTINUEMODE" {
|
||
let current = self.dimension_continue_mode;
|
||
if let Some(value) = &value {
|
||
match value.parse::<i16>() {
|
||
Ok(mode @ 0..=1) => {
|
||
self.dimension_continue_mode = mode;
|
||
self.persist_settings_if_changed();
|
||
self.command_line
|
||
.push_output(&crate::tf!("DIMCONTINUEMODE = {mode}"));
|
||
}
|
||
_ => self.command_line.push_error(
|
||
crate::t!("SETVAR: DIMCONTINUEMODE requires 0 or 1.").as_ref(),
|
||
),
|
||
}
|
||
} else {
|
||
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));
|
||
}
|
||
if matches!(
|
||
name.as_str(),
|
||
"CONSTRAINTSOLVEMODE"
|
||
| "CONSTRAINTINFER"
|
||
| "CONSTRAINTBARDISPLAY"
|
||
| "CONSTRAINTBARMODE"
|
||
| "CONSTRAINTNAMEFORMAT"
|
||
| "DYNCONSTRAINTDISPLAY"
|
||
| "CCONSTRAINTFORM"
|
||
) {
|
||
let current = match name.as_str() {
|
||
"CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode),
|
||
"CONSTRAINTINFER" => i16::from(self.constraint_infer),
|
||
"CONSTRAINTBARDISPLAY" => self.constraint_bar_display,
|
||
"CONSTRAINTBARMODE" => self.constraint_bar_mode,
|
||
"CONSTRAINTNAMEFORMAT" => {
|
||
i16::from(self.tabs[i].scene.constraint_name_format)
|
||
}
|
||
"DYNCONSTRAINTDISPLAY" => {
|
||
i16::from(self.tabs[i].scene.dynamic_constraint_display)
|
||
}
|
||
"CCONSTRAINTFORM" => i16::from(self.constraint_form_annotational),
|
||
_ => unreachable!(),
|
||
};
|
||
let maximum = match name.as_str() {
|
||
"CONSTRAINTBARDISPLAY" => 3,
|
||
"CONSTRAINTBARMODE" => 4095,
|
||
"CONSTRAINTNAMEFORMAT" => 2,
|
||
_ => 1,
|
||
};
|
||
if let Some(value) = &value {
|
||
match value
|
||
.parse::<i16>()
|
||
.ok()
|
||
.filter(|value| (0..=maximum).contains(value))
|
||
{
|
||
Some(mode) => {
|
||
match name.as_str() {
|
||
"CONSTRAINTSOLVEMODE" => {
|
||
self.constraint_solve_mode = mode != 0
|
||
}
|
||
"CONSTRAINTINFER" => self.constraint_infer = mode != 0,
|
||
"CONSTRAINTBARDISPLAY" => {
|
||
self.constraint_bar_display = mode
|
||
}
|
||
"CONSTRAINTBARMODE" => self.constraint_bar_mode = mode,
|
||
"CONSTRAINTNAMEFORMAT" => {
|
||
self.tabs[i].scene.constraint_name_format = mode as u8;
|
||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||
self.tabs[i].dirty = true;
|
||
}
|
||
"DYNCONSTRAINTDISPLAY" => {
|
||
self.tabs[i].scene.dynamic_constraint_display =
|
||
mode != 0;
|
||
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
|
||
}
|
||
// The form new dimensional constraints take
|
||
// (0 dynamic, 1 annotational), as DCFORM sets.
|
||
"CCONSTRAINTFORM" => {
|
||
self.constraint_form_annotational = mode != 0
|
||
}
|
||
_ => unreachable!(),
|
||
}
|
||
self.persist_settings_if_changed();
|
||
self.command_line
|
||
.push_output(&crate::tf!("{name} = {mode}"));
|
||
}
|
||
None => self.command_line.push_error(&crate::tf!(
|
||
"{name}: expected an integer from 0 to {maximum}."
|
||
)),
|
||
}
|
||
} else {
|
||
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));
|
||
}
|
||
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
|
||
let current = if name == "DONUTID" {
|
||
crate::modules::draw::defaults::get_donut_inner_diameter()
|
||
} else {
|
||
crate::modules::draw::defaults::get_donut_outer_diameter()
|
||
};
|
||
if let Some(value) = &value {
|
||
let parsed = value
|
||
.trim()
|
||
.replace(',', ".")
|
||
.parse::<f64>()
|
||
.ok()
|
||
.filter(|number| number.is_finite());
|
||
let valid = parsed.filter(|number| {
|
||
if name == "DONUTID" {
|
||
*number >= 0.0
|
||
} else {
|
||
*number > 0.0
|
||
}
|
||
});
|
||
if let Some(number) = valid {
|
||
if name == "DONUTID" {
|
||
crate::modules::draw::defaults::set_donut_inner_diameter(number);
|
||
} else {
|
||
crate::modules::draw::defaults::set_donut_outer_diameter(number);
|
||
}
|
||
self.command_line
|
||
.push_output(&crate::tf!("{name} = {number}"));
|
||
} else {
|
||
self.command_line.push_error(crate::t!(if name == "DONUTID" {
|
||
"SETVAR: DONUTID requires a finite value greater than or equal to zero."
|
||
} else {
|
||
"SETVAR: DONUTOD requires a finite value greater than zero."
|
||
}).as_ref());
|
||
}
|
||
} else {
|
||
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 current_delete_objects = self.delete_objects;
|
||
let requested_delete_objects = (name == "DELOBJ")
|
||
.then(|| value.as_deref()?.parse::<i16>().ok())
|
||
.flatten()
|
||
.filter(|value| (0..=3).contains(value));
|
||
let outcome: Result<(String, bool), String> = {
|
||
let h = &mut self.tabs[i].scene.document.header;
|
||
match name.as_str() {
|
||
// The standalone LTSCALE and CELTSCALE commands
|
||
// require a positive value; reaching the same
|
||
// variable through SETVAR used to accept zero or a
|
||
// negative one, which degenerates every dash
|
||
// pattern in the drawing.
|
||
"LTSCALE" => match &value {
|
||
Some(v) => v
|
||
.parse::<f64>()
|
||
.ok()
|
||
.filter(|x| x.is_finite() && *x > 0.0)
|
||
.map(|x| {
|
||
h.linetype_scale = x;
|
||
(format!("LTSCALE = {x}"), true)
|
||
})
|
||
.ok_or_else(|| {
|
||
"SETVAR: positive numeric value required.".into()
|
||
}),
|
||
None => Ok((format!("LTSCALE = {}", h.linetype_scale), false)),
|
||
},
|
||
"CELTSCALE" => match &value {
|
||
Some(v) => v
|
||
.parse::<f64>()
|
||
.ok()
|
||
.filter(|x| x.is_finite() && *x > 0.0)
|
||
.map(|x| {
|
||
h.current_entity_linetype_scale = x;
|
||
(format!("CELTSCALE = {x}"), true)
|
||
})
|
||
.ok_or_else(|| {
|
||
"SETVAR: positive 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))
|
||
}
|
||
},
|
||
// Bit code of commercial solutions: 0 = no shortcut menus (right-click
|
||
// is Enter), 16 = time-sensitive; everything else is the
|
||
// regular shortcut menu. Reported as the defaults of commercial solutions
|
||
// (11 = default+edit+command menus, +16 when
|
||
// time-sensitive).
|
||
"SHORTCUTMENU" => {
|
||
use crate::app::settings::RightClickMode;
|
||
let code = |mode: RightClickMode| match mode {
|
||
RightClickMode::EnterFirst => 0,
|
||
RightClickMode::ShortcutMenu => 11,
|
||
RightClickMode::TimeSensitive => 27,
|
||
};
|
||
match &value {
|
||
Some(v) => match v.parse::<i32>() {
|
||
Ok(bits) if (0..=31).contains(&bits) => {
|
||
self.right_click_mode = if bits == 0 {
|
||
RightClickMode::EnterFirst
|
||
} else if bits & 16 != 0 {
|
||
RightClickMode::TimeSensitive
|
||
} else {
|
||
RightClickMode::ShortcutMenu
|
||
};
|
||
Ok((format!("SHORTCUTMENU = {}", code(self.right_click_mode)), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 to 31 required.".into()),
|
||
},
|
||
None => Ok((
|
||
format!("SHORTCUTMENU = {}", code(self.right_click_mode)),
|
||
false,
|
||
)),
|
||
}
|
||
}
|
||
"SHORTCUTMENUDURATION" => match &value {
|
||
Some(v) => match v.parse::<i32>() {
|
||
Ok(ms) if (100..=1000).contains(&ms) => {
|
||
self.right_click_hold_ms = ms;
|
||
Ok((format!("SHORTCUTMENUDURATION = {ms}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 100 to 1000 required.".into()),
|
||
},
|
||
None => Ok((
|
||
format!("SHORTCUTMENUDURATION = {}", self.right_click_hold_ms),
|
||
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,
|
||
)),
|
||
},
|
||
"COLORTHEME" => match &value {
|
||
Some(v) => match v.as_str() {
|
||
"0" => {
|
||
if self.ui_theme.name == "Custom" {
|
||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||
}
|
||
self.ui_theme.name = iced::Theme::Dark.to_string();
|
||
self.ui_theme.palette =
|
||
crate::app::config::UiThemePalette::from_iced(iced::Theme::Dark.seed());
|
||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||
self.active_theme = iced::Theme::Dark;
|
||
self.sync_model_space_theme(true);
|
||
Ok(("COLORTHEME = 0 (Dark)".to_string(), true))
|
||
}
|
||
"1" => {
|
||
if self.ui_theme.name == "Custom" {
|
||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||
}
|
||
self.ui_theme.name = iced::Theme::Light.to_string();
|
||
self.ui_theme.palette =
|
||
crate::app::config::UiThemePalette::from_iced(iced::Theme::Light.seed());
|
||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||
self.active_theme = iced::Theme::Light;
|
||
self.sync_model_space_theme(true);
|
||
Ok(("COLORTHEME = 1 (Light)".to_string(), true))
|
||
}
|
||
_ => Err("SETVAR: 0 (Dark) or 1 (Light) required.".into()),
|
||
},
|
||
None => {
|
||
let bg = self.ui_theme.palette.background;
|
||
let lum = 0.299 * (bg[0] as f32) + 0.587 * (bg[1] as f32) + 0.114 * (bg[2] as f32);
|
||
let code = if lum > 128.0 { 1 } else { 0 };
|
||
Ok((format!("COLORTHEME = {code} ({})", self.ui_theme.name), false))
|
||
}
|
||
},
|
||
"SELECTIONAREA" => match &value {
|
||
Some(v) => match v.as_str() {
|
||
"0" => {
|
||
self.model_space.selection_area = false;
|
||
self.sync_model_space_theme(false);
|
||
Ok(("SELECTIONAREA = 0".to_string(), true))
|
||
}
|
||
"1" => {
|
||
self.model_space.selection_area = true;
|
||
self.sync_model_space_theme(false);
|
||
Ok(("SELECTIONAREA = 1".to_string(), true))
|
||
}
|
||
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||
},
|
||
None => Ok((format!("SELECTIONAREA = {}", if self.model_space.selection_area { 1 } else { 0 }), false)),
|
||
},
|
||
"SELECTIONAREAOPACITY" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(op) if op <= 100 => {
|
||
self.model_space.selection_opacity = op;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("SELECTIONAREAOPACITY = {op}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 to 100 required.".into()),
|
||
},
|
||
None => Ok((format!("SELECTIONAREAOPACITY = {}", self.model_space.selection_opacity), false)),
|
||
},
|
||
"SELECTIONEFFECT" => match &value {
|
||
Some(v) => match v.as_str() {
|
||
"0" => {
|
||
self.model_space.selection_effect = false;
|
||
self.sync_model_space_theme(false);
|
||
Ok(("SELECTIONEFFECT = 0".to_string(), true))
|
||
}
|
||
"1" => {
|
||
self.model_space.selection_effect = true;
|
||
self.sync_model_space_theme(false);
|
||
Ok(("SELECTIONEFFECT = 1".to_string(), true))
|
||
}
|
||
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||
},
|
||
None => Ok((format!("SELECTIONEFFECT = {}", if self.model_space.selection_effect { 1 } else { 0 }), false)),
|
||
},
|
||
"SELECTIONEFFECTCOLOR" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(color) => {
|
||
self.model_space.selection_highlight_color = color;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("SELECTIONEFFECTCOLOR = {color}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||
},
|
||
None => Ok((format!("SELECTIONEFFECTCOLOR = {}", self.model_space.selection_highlight_color), false)),
|
||
},
|
||
"WINDOWSAREACOLOR" | "WINDOWAREACOLOR" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(color) => {
|
||
self.model_space.selection_window_color = color;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("WINDOWSAREACOLOR = {color}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||
},
|
||
None => Ok((format!("WINDOWSAREACOLOR = {}", self.model_space.selection_window_color), false)),
|
||
},
|
||
"CROSSINGAREACOLOR" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(color) => {
|
||
self.model_space.selection_crossing_color = color;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("CROSSINGAREACOLOR = {color}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||
},
|
||
None => Ok((format!("CROSSINGAREACOLOR = {}", self.model_space.selection_crossing_color), false)),
|
||
},
|
||
"SELECTIONPREVIEW" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(mode @ 0..=3) => {
|
||
self.model_space.selection_preview = mode;
|
||
Ok((format!("SELECTIONPREVIEW = {mode}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 to 3 required.".into()),
|
||
},
|
||
None => Ok((format!("SELECTIONPREVIEW = {}", self.model_space.selection_preview), false)),
|
||
},
|
||
"GRIPSIZE" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(size @ 1..=25) => {
|
||
self.model_space.grip_size = size;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("GRIPSIZE = {size}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 1 to 25 required.".into()),
|
||
},
|
||
None => Ok((format!("GRIPSIZE = {}", self.model_space.grip_size), false)),
|
||
},
|
||
"GRIPOBJLIMIT" => match &value {
|
||
Some(v) => match v.parse::<i32>() {
|
||
Ok(limit @ 0..=32767) => {
|
||
self.grip_object_limit = limit;
|
||
Ok((format!("GRIPOBJLIMIT = {limit}"), true))
|
||
}
|
||
_ => Err(
|
||
"SETVAR: integer from 0 to 32767 required (0 = no limit)."
|
||
.into(),
|
||
),
|
||
},
|
||
None => Ok((
|
||
format!("GRIPOBJLIMIT = {}", self.grip_object_limit),
|
||
false,
|
||
)),
|
||
},
|
||
"GRIPCOLOR" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(color) => {
|
||
self.model_space.grip_color = color;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("GRIPCOLOR = {color}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||
},
|
||
None => Ok((format!("GRIPCOLOR = {}", self.model_space.grip_color), false)),
|
||
},
|
||
"GRIPHOT" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(color) => {
|
||
self.model_space.grip_hot = color;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("GRIPHOT = {color}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||
},
|
||
None => Ok((format!("GRIPHOT = {}", self.model_space.grip_hot), false)),
|
||
},
|
||
"GRIPHOVER" => match &value {
|
||
Some(v) => match v.parse::<u8>() {
|
||
Ok(color) => {
|
||
self.model_space.grip_hover = color;
|
||
self.sync_model_space_theme(false);
|
||
Ok((format!("GRIPHOVER = {color}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||
},
|
||
None => Ok((format!("GRIPHOVER = {}", self.model_space.grip_hover), 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,
|
||
)),
|
||
},
|
||
"CLIPROMPTLINES" => match &value {
|
||
Some(v) => match v.parse::<i32>() {
|
||
Ok(n) if (0..=50).contains(&n) => {
|
||
self.cliprompt_lines = n;
|
||
self.command_line.set_cliprompt_lines(n as u8);
|
||
Ok((format!("CLIPROMPTLINES = {n}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 to 50 required.".into()),
|
||
},
|
||
None => Ok((
|
||
format!("CLIPROMPTLINES = {}", self.cliprompt_lines),
|
||
false,
|
||
)),
|
||
},
|
||
"COMMANDLINEFADETIME" => match &value {
|
||
Some(v) => match v.parse::<i32>() {
|
||
Ok(n) if (0..=60000).contains(&n) => {
|
||
self.commandline_fade_ms = n;
|
||
self.command_line.set_commandline_fade_ms(n as u32);
|
||
Ok((format!("COMMANDLINEFADETIME = {n}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer from 0 to 60000 required.".into()),
|
||
},
|
||
None => Ok((
|
||
format!("COMMANDLINEFADETIME = {}", self.commandline_fade_ms),
|
||
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) => v
|
||
.parse::<i16>()
|
||
.ok()
|
||
.filter(|value| (0..=3).contains(value))
|
||
.map(|value| {
|
||
(format!("DELOBJ = {value}"), true)
|
||
})
|
||
.ok_or_else(|| "SETVAR: integer from 0 to 3 required.".into()),
|
||
None => Ok((
|
||
format!("DELOBJ = {current_delete_objects}"),
|
||
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| {
|
||
let x = x.clamp(0, 8);
|
||
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| {
|
||
let x = x.clamp(0, 8);
|
||
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| {
|
||
// Stored values stay inside the range
|
||
// the Options slider can ask for; the
|
||
// consumption sites clamp again for
|
||
// file-sourced headers.
|
||
let x = x.clamp(
|
||
0,
|
||
crate::entities::solid3d::MAX_HEADER_ISOLINES,
|
||
);
|
||
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) => match v.parse::<i16>() {
|
||
Ok(x @ 0..=200) => {
|
||
h.surface_u_density = x;
|
||
Ok((format!("SURFU = {x}"), true))
|
||
}
|
||
Ok(_) => Err("SETVAR: value must be between 0 and 200.".into()),
|
||
Err(_) => Err("SETVAR: integer value required.".into()),
|
||
},
|
||
None => Ok((format!("SURFU = {}", h.surface_u_density), false)),
|
||
},
|
||
"SURFV" => match &value {
|
||
Some(v) => match v.parse::<i16>() {
|
||
Ok(x @ 0..=200) => {
|
||
h.surface_v_density = x;
|
||
Ok((format!("SURFV = {x}"), true))
|
||
}
|
||
Ok(_) => Err("SETVAR: value must be between 0 and 200.".into()),
|
||
Err(_) => Err("SETVAR: integer value required.".into()),
|
||
},
|
||
None => Ok((format!("SURFV = {}", h.surface_v_density), false)),
|
||
},
|
||
"SURFTYPE" => match &value {
|
||
Some(v) => match v.parse::<i16>() {
|
||
Ok(x @ (5 | 6 | 8)) => {
|
||
h.surface_type = x;
|
||
Ok((format!("SURFTYPE = {x}"), true))
|
||
}
|
||
Ok(_) => Err("SETVAR: value must be 5, 6, or 8.".into()),
|
||
Err(_) => 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) => match v.parse::<i16>() {
|
||
Ok(x @ 0..=2) => {
|
||
h.multiline_justification = x;
|
||
Ok((format!("CMLJUST = {x}"), true))
|
||
}
|
||
_ => Err("SETVAR: integer value from 0 to 2 required.".into()),
|
||
},
|
||
None => {
|
||
Ok((format!("CMLJUST = {}", h.multiline_justification), false))
|
||
}
|
||
},
|
||
"CMLSCALE" => match &value {
|
||
Some(v) => match v.parse::<f64>() {
|
||
Ok(x) if x.is_finite() => {
|
||
let changed = h.multiline_scale != x;
|
||
h.multiline_scale = x;
|
||
Ok((format!("CMLSCALE = {x}"), changed))
|
||
}
|
||
_ => Err("SETVAR: finite numeric value required.".into()),
|
||
},
|
||
None => Ok((format!("CMLSCALE = {}", h.multiline_scale), false)),
|
||
},
|
||
"CMLSTYLE" => match &value {
|
||
Some(_) => Err(
|
||
"SETVAR: CMLSTYLE is read-only here — use the MLSTYLE command."
|
||
.into(),
|
||
),
|
||
None => Ok((format!("CMLSTYLE = {}", h.multiline_style), 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) => match v.parse::<f64>() {
|
||
Ok(x) if x.is_finite() && x > 0.0 => {
|
||
let changed = h.sketch_increment != x;
|
||
h.sketch_increment = x;
|
||
Ok((format!("SKETCHINC = {x}"), changed))
|
||
}
|
||
_ => Err("SETVAR: positive numeric value required.".into()),
|
||
},
|
||
None => Ok((format!("SKETCHINC = {}", h.sketch_increment), false)),
|
||
},
|
||
"SKPOLY" => match &value {
|
||
Some(v) => match v.parse::<i16>() {
|
||
Ok(x @ 0..=2) => {
|
||
let changed = h.sketch_type != x;
|
||
h.sketch_type = x;
|
||
Ok((format!("SKPOLY = {x}"), changed))
|
||
}
|
||
_ => Err("SETVAR: integer value from 0 to 2 required.".into()),
|
||
},
|
||
None => Ok((format!("SKPOLY = {}", h.sketch_type), false)),
|
||
},
|
||
"SKTOLERANCE" => match &value {
|
||
Some(v) => match v.parse::<f64>() {
|
||
Ok(x) if x.is_finite() && (0.0..=1.0).contains(&x) => {
|
||
let changed = h.sketch_tolerance != x;
|
||
h.sketch_tolerance = x;
|
||
Ok((format!("SKTOLERANCE = {x}"), changed))
|
||
}
|
||
_ => Err("SETVAR: numeric value from 0 to 1 required.".into()),
|
||
},
|
||
None => Ok((
|
||
format!("SKTOLERANCE = {}", h.sketch_tolerance),
|
||
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(crate::tf!("SETVAR: unknown variable \"{name}\".").into_owned()),
|
||
}
|
||
};
|
||
match outcome {
|
||
Ok((msg, changed)) => {
|
||
if let Some(value) = requested_delete_objects {
|
||
self.delete_objects = value;
|
||
}
|
||
if changed {
|
||
if matches!(
|
||
name.as_str(),
|
||
"ZOOMWHEEL"
|
||
| "ZOOMFACTOR"
|
||
| "SHORTCUTMENU"
|
||
| "SHORTCUTMENUDURATION"
|
||
| "CURSORSIZE"
|
||
| "PICKBOX"
|
||
| "CURSORTYPE"
|
||
| "SNAPANG"
|
||
| "CLIPROMPTLINES"
|
||
| "COMMANDLINEFADETIME"
|
||
| "COLORTHEME"
|
||
| "SELECTIONAREA"
|
||
| "SELECTIONAREAOPACITY"
|
||
| "SELECTIONEFFECT"
|
||
| "SELECTIONEFFECTCOLOR"
|
||
| "WINDOWSAREACOLOR"
|
||
| "WINDOWAREACOLOR"
|
||
| "CROSSINGAREACOLOR"
|
||
| "SELECTIONPREVIEW"
|
||
| "GRIPSIZE"
|
||
| "GRIPCOLOR"
|
||
| "GRIPHOT"
|
||
| "GRIPHOVER"
|
||
| "GRIPOBJLIMIT"
|
||
| "DELOBJ"
|
||
// App-level, not part of the drawing:
|
||
// it lives in a process global and is
|
||
// snapshotted into the settings file.
|
||
// Without this it marked the drawing
|
||
// modified instead of persisting.
|
||
| "TEXTFILL"
|
||
) {
|
||
self.persist_settings_if_changed();
|
||
} else {
|
||
self.tabs[i].dirty = true;
|
||
}
|
||
if name == "FILLMODE" {
|
||
self.tabs[i].scene.bump_geometry();
|
||
}
|
||
if matches!(name.as_str(), "PSLTSCALE" | "PLIMCHECK") {
|
||
self.tabs[i].scene.persist_current_layout_state();
|
||
}
|
||
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.invalidate_text_everywhere();
|
||
}
|
||
// 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();
|
||
}
|
||
if matches!(name.as_str(), "LUNITS" | "LUPREC" | "AUNITS" | "AUPREC") {
|
||
crate::entities::common::set_unit_context(
|
||
crate::entities::common::UnitContext::from_header(
|
||
&self.tabs[i].scene.document.header,
|
||
),
|
||
);
|
||
}
|
||
// 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 {
|
||
codec::EntityType::Text(t) if !t.style.is_empty() => {
|
||
styles.insert(t.style.clone());
|
||
}
|
||
codec::EntityType::MText(t) if !t.style.is_empty() => {
|
||
styles.insert(t.style.clone());
|
||
}
|
||
codec::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 codec::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.set_current_layer_name(i, name_arg).is_ok() {
|
||
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 current = self.tabs[i].scene.document.header.linetype_scale;
|
||
|
||
self.command_line
|
||
.push_output(crate::tf!("LTSCALE = {current:.4}").as_ref());
|
||
|
||
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(
|
||
crate::t!("Usage: PDMODE [value] (0=dot 1=none 2=+ 3=x 4=tick; +32 circle, +64 square)").as_ref(),
|
||
);
|
||
}
|
||
}
|
||
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(
|
||
crate::t!("FILEASSOC set to 1 — registered as a .dwg/.dxf/.bak handler").as_ref(),
|
||
),
|
||
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()),
|
||
}
|
||
}
|
||
cmd if cmd == "SCRIPTCOMMANDS" || cmd.starts_with("SCRIPTCOMMANDS ") => {
|
||
// A user setting, deliberately outside the script's reach: it is refused by
|
||
// the script command runner, so only the command line can change it.
|
||
let arg = cmd.trim_start_matches("SCRIPTCOMMANDS").trim();
|
||
match arg {
|
||
"" => {}
|
||
"1" | "ON" | "YES" => self.script_commands = true,
|
||
"0" | "OFF" | "NO" => self.script_commands = false,
|
||
_ => {
|
||
self.command_line.push_error(
|
||
crate::t!("SCRIPTCOMMANDS takes 1 (scripts may run commands) or 0 (they may not)")
|
||
.as_ref(),
|
||
);
|
||
return Some(Task::none());
|
||
}
|
||
}
|
||
if !arg.is_empty() {
|
||
self.persist_settings_if_changed();
|
||
}
|
||
let state = if self.script_commands { 1 } else { 0 };
|
||
self.command_line
|
||
.push_output(crate::tf!("SCRIPTCOMMANDS = {state}").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 {
|
||
crate::tf!("SAVETIME set to {v} minute(s)").into_owned()
|
||
};
|
||
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, <0 = viewport percentage):",
|
||
);
|
||
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(
|
||
crate::t!("Usage: PDSIZE [value] (>0 absolute size, <0 percent of viewport, 0 default)").as_ref(),
|
||
);
|
||
}
|
||
}
|
||
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<codec::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 {
|
||
codec::EntityType::Text(t) => {
|
||
t.height =
|
||
if use_absolute { val } else { t.height * val };
|
||
count += 1;
|
||
}
|
||
codec::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 codec::CadDocument, ty: &str, old: &str, new: &str) -> bool {
|
||
use codec::{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,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use crate::app::OpenCADStudio;
|
||
|
||
fn fresh_app() -> OpenCADStudio {
|
||
let mut app = OpenCADStudio::new_for_test();
|
||
app.automation_op(r#"{"op":"new"}"#);
|
||
app
|
||
}
|
||
|
||
/// `SETVAR ISOLINES` stored any parseable `i16` verbatim. The header
|
||
/// value is the per-face isolate count handed to the tessellation
|
||
/// kernel (which loops `0..count`), so `ISOLINES 32767` made every
|
||
/// rebuild and per-cursor preview allocate without bound. The stored
|
||
/// value must stay inside the range the app's own UI can ask for
|
||
/// (the Options slider is 0..=64).
|
||
#[test]
|
||
fn setvar_isolines_stays_within_the_renderable_range() {
|
||
let mut app = fresh_app();
|
||
let i = app.active_tab;
|
||
|
||
let _ = app.run_command_line("SETVAR ISOLINES 32767");
|
||
assert_eq!(app.tabs[i].scene.document.header.isolines, 64);
|
||
|
||
let _ = app.run_command_line("SETVAR ISOLINES 8");
|
||
assert_eq!(app.tabs[i].scene.document.header.isolines, 8);
|
||
|
||
let _ = app.run_command_line("SETVAR ISOLINES -3");
|
||
assert_eq!(app.tabs[i].scene.document.header.isolines, 0);
|
||
}
|
||
|
||
/// `SETVAR LUPREC` / `AUPREC` stored any parseable `i16` verbatim. The
|
||
/// units dialog only ever offers 0-8 places, and the formatters read the
|
||
/// header on every properties/annotation call, so an unclamped value
|
||
/// (positive or negative) fed unbounded strings into those hot paths.
|
||
#[test]
|
||
fn setvar_precision_stays_within_the_range_the_units_dialog_offers() {
|
||
let mut app = fresh_app();
|
||
let i = app.active_tab;
|
||
|
||
let _ = app.run_command_line("SETVAR LUPREC 30000");
|
||
assert_eq!(app.tabs[i].scene.document.header.linear_unit_precision, 8);
|
||
|
||
let _ = app.run_command_line("SETVAR AUPREC 30000");
|
||
assert_eq!(app.tabs[i].scene.document.header.angular_unit_precision, 8);
|
||
|
||
let _ = app.run_command_line("SETVAR LUPREC -3");
|
||
assert_eq!(app.tabs[i].scene.document.header.linear_unit_precision, 0);
|
||
|
||
let _ = app.run_command_line("SETVAR LUPREC 5");
|
||
assert_eq!(app.tabs[i].scene.document.header.linear_unit_precision, 5);
|
||
}
|
||
|
||
#[test]
|
||
fn selection_commands_gather_instead_of_using_the_whole_drawing() {
|
||
for command in ["SETBYLAYER", "OVERKILL"] {
|
||
let mut app = fresh_app();
|
||
let _ = app.run_command_line(command);
|
||
assert!(
|
||
app.tabs[app.active_tab]
|
||
.active_cmd
|
||
.as_ref()
|
||
.is_some_and(|active| active.is_selection_gathering()),
|
||
"{command} should gather an explicit selection"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_cad_selection_and_model_space_sysvars() {
|
||
let mut app = fresh_app();
|
||
|
||
// SELECTIONAREA toggle
|
||
let _ = app.run_command_line("SETVAR SELECTIONAREA 0");
|
||
assert!(!app.model_space.selection_area);
|
||
let _ = app.run_command_line("SELECTIONAREA 1");
|
||
assert!(app.model_space.selection_area);
|
||
|
||
// SELECTIONAREAOPACITY
|
||
let _ = app.run_command_line("SETVAR SELECTIONAREAOPACITY 45");
|
||
assert_eq!(app.model_space.selection_opacity, 45);
|
||
let _ = app.run_command_line("SELECTIONAREAOPACITY 80");
|
||
assert_eq!(app.model_space.selection_opacity, 80);
|
||
|
||
// SELECTIONEFFECT
|
||
let _ = app.run_command_line("SETVAR SELECTIONEFFECT 0");
|
||
assert!(!app.model_space.selection_effect);
|
||
assert!(!app.tabs[app.active_tab].scene.selection_effect);
|
||
let _ = app.run_command_line("SELECTIONEFFECT 1");
|
||
assert!(app.model_space.selection_effect);
|
||
assert!(app.tabs[app.active_tab].scene.selection_effect);
|
||
|
||
// SELECTIONEFFECTCOLOR
|
||
let _ = app.run_command_line("SETVAR SELECTIONEFFECTCOLOR 4");
|
||
assert_eq!(app.model_space.selection_highlight_color, 4);
|
||
assert_eq!(
|
||
app.tabs[app.active_tab].scene.selection_color,
|
||
app.model_space.resolve_selection_color()
|
||
);
|
||
|
||
// WINDOWSAREACOLOR & CROSSINGAREACOLOR
|
||
let _ = app.run_command_line("SETVAR WINDOWSAREACOLOR 5");
|
||
assert_eq!(app.model_space.selection_window_color, 5);
|
||
let _ = app.run_command_line("WINDOWSAREACOLOR 1");
|
||
assert_eq!(app.model_space.selection_window_color, 1);
|
||
|
||
let _ = app.run_command_line("SETVAR CROSSINGAREACOLOR 3");
|
||
assert_eq!(app.model_space.selection_crossing_color, 3);
|
||
let _ = app.run_command_line("CROSSINGAREACOLOR 2");
|
||
assert_eq!(app.model_space.selection_crossing_color, 2);
|
||
|
||
// SELECTIONPREVIEW
|
||
let _ = app.run_command_line("SETVAR SELECTIONPREVIEW 2");
|
||
assert_eq!(app.model_space.selection_preview, 2);
|
||
|
||
let _ = app.run_command_line("SETVAR GRIPOBJLIMIT 0");
|
||
assert_eq!(app.grip_object_limit, 0);
|
||
let _ = app.run_command_line("GRIPOBJLIMIT 32767");
|
||
assert_eq!(app.grip_object_limit, 32767);
|
||
let _ = app.run_command_line("GRIPOBJLIMIT 32768");
|
||
assert_eq!(app.grip_object_limit, 32767);
|
||
}
|
||
|
||
#[test]
|
||
fn test_background_command_modes() {
|
||
let mut app = fresh_app();
|
||
let i = app.active_tab;
|
||
|
||
// BACKGROUND CLASSIC
|
||
let _ = app.run_command_line("BACKGROUND CLASSIC");
|
||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::ClassicDark);
|
||
assert_eq!(
|
||
app.tabs[i].scene.bg_color,
|
||
[33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0]
|
||
);
|
||
|
||
// BACKGROUND RGB custom
|
||
let _ = app.run_command_line("BACKGROUND 50 60 70");
|
||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::Custom);
|
||
assert_eq!(app.model_space.custom_bg, Some([50, 60, 70]));
|
||
assert!((app.tabs[i].scene.bg_color[0] - 50.0 / 255.0).abs() < 1e-4);
|
||
assert!((app.tabs[i].scene.bg_color[1] - 60.0 / 255.0).abs() < 1e-4);
|
||
assert!((app.tabs[i].scene.bg_color[2] - 70.0 / 255.0).abs() < 1e-4);
|
||
|
||
// BACKGROUND DEFAULT restores MatchTheme
|
||
let _ = app.run_command_line("BACKGROUND DEFAULT");
|
||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||
assert_eq!(app.model_space.custom_bg, None);
|
||
|
||
// BACKGROUND DESK
|
||
let _ = app.run_command_line("BACKGROUND DESK 80 85 90");
|
||
assert_eq!(app.model_space.custom_desk_bg, Some([80, 85, 90]));
|
||
let _ = app.run_command_line("BACKGROUND DESK DEFAULT");
|
||
assert_eq!(app.model_space.custom_desk_bg, None);
|
||
}
|
||
|
||
#[test]
|
||
fn test_reviewer_feedback_fixes() {
|
||
let mut app = fresh_app();
|
||
|
||
// An empty background restores MatchTheme.
|
||
let _ = app.update(crate::app::Message::ModelSpaceBgChanged("".to_string()));
|
||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||
assert_eq!(app.model_space.custom_bg, None);
|
||
|
||
// Display and selection defaults are restored separately.
|
||
app.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||
app.model_space.custom_bg = Some([10, 20, 30]);
|
||
app.model_space.selection_opacity = 99;
|
||
let _ = app.update(crate::app::Message::RestoreModelSpaceDisplayDefaults);
|
||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||
assert_eq!(app.model_space.selection_opacity, 99, "Selection visual must not be wiped by display restore");
|
||
|
||
let _ = app.update(crate::app::Message::RestoreSelectionVisualDefaults);
|
||
assert_eq!(app.model_space.selection_opacity, 12, "Selection restore resets selection visual defaults");
|
||
}
|
||
|
||
#[test]
|
||
fn test_grip_and_desk_surround_sysvars() {
|
||
let mut app = fresh_app();
|
||
|
||
// 1. Desk surround background message
|
||
let _ = app.update(crate::app::Message::DeskSpaceBgChanged("#1a2b3c".to_string()));
|
||
assert_eq!(app.model_space.custom_desk_bg, Some([0x1a, 0x2b, 0x3c]));
|
||
assert_eq!(app.desk_bg_input, "#1a2b3c");
|
||
let _ = app.update(crate::app::Message::DeskSpaceBgChanged("".to_string()));
|
||
assert_eq!(app.model_space.custom_desk_bg, None);
|
||
|
||
// 2. GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER via command line & SETVAR
|
||
let _ = app.run_command_line("GRIPSIZE 8");
|
||
assert_eq!(app.model_space.grip_size, 8);
|
||
let _ = app.run_command_line("GRIPCOLOR 4");
|
||
assert_eq!(app.model_space.grip_color, 4);
|
||
let _ = app.run_command_line("GRIPHOT 1");
|
||
assert_eq!(app.model_space.grip_hot, 1);
|
||
let _ = app.run_command_line("GRIPHOVER 2");
|
||
assert_eq!(app.model_space.grip_hover, 2);
|
||
|
||
// Direct messages
|
||
let _ = app.update(crate::app::Message::GripSizeChanged(12));
|
||
assert_eq!(app.model_space.grip_size, 12);
|
||
let _ = app.update(crate::app::Message::GripColorChanged(5));
|
||
assert_eq!(app.model_space.grip_color, 5);
|
||
|
||
// Restore defaults resets grips
|
||
let _ = app.update(crate::app::Message::RestoreSelectionVisualDefaults);
|
||
assert_eq!(app.model_space.grip_size, 5);
|
||
assert_eq!(app.model_space.grip_color, 0);
|
||
assert_eq!(app.model_space.grip_hot, 0);
|
||
assert_eq!(app.model_space.grip_hover, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn bare_commandlinefadetime_stays_active_until_value_or_cancel() {
|
||
let mut app = fresh_app();
|
||
let _ = app.run_command_line("COMMANDLINEFADETIME");
|
||
// Like RECTANGLE and PICKADD: bare form installs an active command
|
||
// whose pinned prompt stays until a value, Enter, or Esc.
|
||
assert!(
|
||
app.tabs[app.active_tab].active_cmd.is_some(),
|
||
"bare COMMANDLINEFADETIME must stay active"
|
||
);
|
||
// A value sets the variable and ends the command.
|
||
let _ = app.run_command_line("COMMANDLINEFADETIME 5000");
|
||
assert_eq!(app.commandline_fade_ms, 5000);
|
||
assert_eq!(app.command_line.commandline_fade_ms(), 5000);
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||
// Bare again, then Esc cancels without changing the value.
|
||
let _ = app.run_command_line("COMMANDLINEFADETIME");
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_some());
|
||
let _ = app.update(crate::app::Message::CommandEscape);
|
||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||
assert_eq!(app.commandline_fade_ms, 5000);
|
||
}
|
||
|
||
#[test]
|
||
fn delobj_accepts_values_zero_through_three() {
|
||
let mut app = fresh_app();
|
||
|
||
for value in 0..=3 {
|
||
let _ = app.run_command_line(&format!("SETVAR DELOBJ {value}"));
|
||
assert_eq!(app.delete_objects, value);
|
||
}
|
||
|
||
let _ = app.run_command_line("SETVAR DELOBJ 4");
|
||
assert_eq!(app.delete_objects, 3);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod scale_validation_tests {
|
||
use crate::app::OpenCADStudio;
|
||
|
||
/// A linetype scale of zero or less degenerates every dash pattern in the
|
||
/// drawing, which is why the standalone commands reject it. Reaching the
|
||
/// same variable through SETVAR used to accept it, so which door you came
|
||
/// through decided whether the drawing could be broken.
|
||
#[test]
|
||
fn both_doors_to_ltscale_refuse_a_non_positive_value() {
|
||
for name in ["LTSCALE", "CELTSCALE"] {
|
||
for entry in [format!("{name} -5"), format!("SETVAR {name} -5")] {
|
||
let mut app = OpenCADStudio::new_for_test();
|
||
app.automation_op(r#"{"op":"new"}"#);
|
||
let i = app.active_tab;
|
||
let before = if name == "LTSCALE" {
|
||
app.tabs[i].scene.document.header.linetype_scale
|
||
} else {
|
||
app.tabs[i].scene.document.header.current_entity_linetype_scale
|
||
};
|
||
let _ = app.run_command_line(&entry);
|
||
let after = if name == "LTSCALE" {
|
||
app.tabs[i].scene.document.header.linetype_scale
|
||
} else {
|
||
app.tabs[i].scene.document.header.current_entity_linetype_scale
|
||
};
|
||
assert_eq!(before, after, "{entry} must be refused");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl OpenCADStudio {
|
||
/// XDWGFADECTL: store it, show it on the ribbon and redraw every tab's
|
||
/// referenced drawings.
|
||
pub(in crate::app) fn set_xref_fade(&mut self, fade: i32) {
|
||
crate::scene::cache::block_cache::set_xref_fade_ctl(fade);
|
||
self.ribbon.xref_fade = crate::scene::cache::block_cache::xref_fade_ctl();
|
||
for tab in &mut self.tabs {
|
||
if !tab.is_start {
|
||
tab.scene.recolor_meshes();
|
||
tab.scene.bump_geometry();
|
||
}
|
||
}
|
||
}
|
||
}
|