feat(commands): integrate standard CAD commands and fix behavior gaps

Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).

New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
  (DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
  integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
  FINDNONPURGEABLE

Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
  (correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
  interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
  text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count

Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-28 19:27:35 +03:00
commit 0ca3954b5d
32 changed files with 2763 additions and 67 deletions

View file

@ -3,6 +3,44 @@ use super::*;
impl OpenCADStudio {
pub(super) fn dispatch_blocks(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
// ── BASE — drawing insertion base point ───────────────────────
// Bare BASE picks a point interactively; BASE <x> <y> [z] sets it
// directly. The base point is stored per active space (model/paper).
cmd if cmd == "BASE" || cmd.starts_with("BASE ") => {
let rest = cmd.strip_prefix("BASE").unwrap_or("").trim();
if rest.is_empty() {
use crate::modules::insert::base_point::BaseCommand;
let c = BaseCommand::new();
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
} else {
let nums: Vec<f64> = rest
.split(|ch| ch == ' ' || ch == ',')
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse::<f64>().ok())
.collect();
if nums.len() >= 2 {
let z = nums.get(2).copied().unwrap_or(0.0);
let pt = acadrust::types::Vector3::new(nums[0], nums[1], z);
let is_paper = self.tabs[i].scene.current_layout != "Model";
self.push_undo_snapshot(i, "BASE");
if is_paper {
self.tabs[i].scene.document.header.paper_space_insertion_base = pt;
} else {
self.tabs[i].scene.document.header.model_space_insertion_base = pt;
}
self.tabs[i].dirty = true;
let space = if is_paper { "paper space" } else { "model space" };
self.command_line.push_output(&format!(
"Base point ({}, {}, {}) set for {space}.",
nums[0], nums[1], z
));
} else {
self.command_line.push_error("Usage: BASE <x> <y> [z]");
}
}
}
"COPYCLIP" | "CC" => {
let handles: Vec<_> = self.tabs[i]
.scene
@ -154,7 +192,7 @@ impl OpenCADStudio {
}
}
"BLOCK" => {
"BLOCK" | "BMAKE" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()

View file

@ -17,21 +17,21 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"DIMLINEAR" => {
"DIMLINEAR" | "DLI" => {
use crate::modules::annotate::linear_dim::LinearDimensionCommand;
let new_cmd = LinearDimensionCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"DIMRADIUS" => {
"DIMRADIUS" | "DRA" => {
use crate::modules::annotate::radius_dim::RadiusDimensionCommand;
let new_cmd = RadiusDimensionCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"DIMANGULAR" => {
"DIMANGULAR" | "DAN" => {
use crate::modules::annotate::angular_dim::AngularDimensionCommand;
let new_cmd = AngularDimensionCommand::new();
self.command_line.push_info(&new_cmd.prompt());

View file

@ -146,6 +146,34 @@ impl OpenCADStudio {
return Some(Task::done(Message::ToggleLayoutTabs));
}
// ── Drafting aids — same toggles the status-bar pills drive, also
// reachable by name from the command line. ─────────────────────────
// GRID — show / hide the reference grid.
"GRID" => {
return Some(Task::done(Message::ToggleGrid));
}
// SNAP — toggle cursor snapping to the grid.
"SNAP" => {
return Some(Task::done(Message::ToggleGridSnap));
}
// POLAR — toggle polar tracking.
"POLAR" => {
return Some(Task::done(Message::TogglePolar));
}
// DSETTINGS / OSNAP — open the drafting-settings (object snap) popup.
"DSETTINGS" | "OSNAP" => {
return Some(Task::done(Message::ToggleSnapPopup));
}
// ── CLEANSCREEN — collapse the surrounding panels for a full canvas ──
"CLEANSCREEN" => {
return Some(Task::done(Message::ToggleCleanScreen));
}
// ── QUICKPROPERTIES — toggle the floating quick-properties readout ───
"QUICKPROPERTIES" => {
return Some(Task::done(Message::ToggleQuickProperties));
}
// ── TOOLPALETTES — not yet implemented ───────────────────────────────
"TOOLPALETTES" | "TP" => {
self.command_line
@ -328,7 +356,7 @@ impl OpenCADStudio {
}
// ── Plot / Page Setup ──────────────────────────────────────────
"PLOT" | "EXPORT" => {
"PLOT" | "EXPORT" | "EXPORTPDF" => {
return Some(Task::done(Message::PlotExport));
}
// PRINT — send current layout to the system default printer.
@ -487,6 +515,22 @@ impl OpenCADStudio {
return Some(Task::done(Message::PageSetupOpen));
}
}
// ── Recognized commands whose full implementation is pending ─────────
// These verbs are surfaced by the ribbon / menus but their feature is
// still being built. Acknowledge them with an honest status so the
// button responds instead of reporting an unknown command; each is
// replaced by its real handler as the feature lands.
"ADCENTER" | "CONTENTBROWSER" | "BLOCKPALETTE" | "DATALINK"
| "LANDXMLIMPORT" | "POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS"
| "UNDERLAYLAYERS" | "ADJUST" | "ANNOSCALE" | "OBJECTSCALE"
| "SCALELISTEDIT" | "UOSNAP" | "ATTSYNC"
| "ATTMAN" | "HIDDENLINE" | "XRAY" | "OPTIONS"
| "COLOR" | "BYLAYER" | "CUILOAD" | "CUIIMPORT" | "CUIEXPORT" => {
self.command_line
.push_info(&format!("{cmd}: not yet implemented."));
}
_ => return None,
}
Some(self.finish_dispatch(cmd))

View file

@ -52,12 +52,21 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
// ATTEDIT — list or edit attribute values on selected Insert entities.
// Command-line attribute editing on selected Insert entities. Bare
// ATTEDIT and the ATE alias launch the interactive editor instead
// (see the ATTEDIT arm in the inquiry family); the dash form is the
// command-line entry point.
// Usage:
// ATTEDIT — list all attributes on selected Insert(s)
// ATTEDIT <tag> <v> — set the value of attribute <tag> to <v>
cmd if cmd == "ATTEDIT" || cmd.starts_with("ATTEDIT ") => {
let rest = cmd.trim_start_matches("ATTEDIT").trim();
// -ATTEDIT — list all attributes on selected Insert(s)
// ATTEDIT <tag> <v> — quick-set attribute <tag> to <v>
cmd if cmd.starts_with("ATTEDIT ")
|| cmd == "-ATTEDIT"
|| cmd.starts_with("-ATTEDIT ") =>
{
let rest = cmd
.trim_start_matches("-ATTEDIT")
.trim_start_matches("ATTEDIT")
.trim();
let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect();
let selected_handles: Vec<acadrust::Handle> = self.tabs[i]
.scene
@ -340,6 +349,64 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"3DPOLY" => {
use crate::modules::draw::draw::poly3d::Poly3dCommand;
let new_cmd = Poly3dCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
// 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is
// currently the shaded-display toggle (token collision tracked).
"SO" | "SOLID2D" => {
use crate::modules::draw::draw::solid2d::Solid2dCommand;
let new_cmd = Solid2dCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"HELIX" => {
use crate::modules::draw::draw::helix::HelixCommand;
let new_cmd = HelixCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"TRACE" => {
use crate::modules::draw::draw::trace::TraceCommand;
let new_cmd = TraceCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"DIMCENTER" | "DCE" | "CENTERMARK" => {
use crate::modules::draw::draw::dimcenter::DimCenterCommand;
let new_cmd = DimCenterCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"SKETCH" => {
use crate::modules::draw::draw::sketch::SketchCommand;
let new_cmd = SketchCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"REVERSE" => {
use crate::modules::draw::modify::reverse::ReverseCommand;
let new_cmd = ReverseCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"MEASUREGEOM" | "MEA" => {
use crate::modules::draw::inquiry::measuregeom::MeasureGeomCommand;
let new_cmd = MeasureGeomCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
// ── Modify commands ────────────────────────────────────────────
"MOVE" | "M" => {
let handles: Vec<_> = self.tabs[i]

View file

@ -7,6 +7,19 @@ impl OpenCADStudio {
"OPEN" => return Some(Task::done(Message::OpenFile)),
"SAVE" | "QSAVE" => return Some(Task::done(Message::SaveFile)),
"SAVEAS" => return Some(Task::done(Message::SaveAs)),
// UNDO <n> — step back n operations at once; bare UNDO / U is one step.
cmd if cmd.starts_with("UNDO ") => {
let arg = cmd["UNDO ".len()..].trim();
match arg.parse::<usize>() {
Ok(0) => return Some(Task::none()),
Ok(n) => return Some(Task::done(Message::UndoMany(n))),
Err(_) => {
self.command_line
.push_error("Usage: UNDO [number of steps]");
return Some(Task::none());
}
}
}
"UNDO" | "U" => return Some(Task::done(Message::Undo)),
"REDO" => return Some(Task::done(Message::Redo)),
"CLEAR" | "CLR" => return Some(Task::done(Message::ClearScene)),
@ -101,6 +114,44 @@ impl OpenCADStudio {
"PERSP" => return Some(Task::done(Message::SetProjection(false))),
"LAYERS" | "LA" => return Some(Task::done(Message::ToggleLayers)),
// SCRIPT <path> — run a command script: each non-blank, non-comment
// line is fed through the same command path the `--script` startup
// flag uses, so the behaviour matches headless automation exactly.
cmd if cmd == "SCRIPT"
|| cmd == "SCR"
|| cmd.starts_with("SCRIPT ")
|| cmd.starts_with("SCR ") =>
{
let path = cmd.split_once(' ').map(|(_, r)| r.trim().to_string());
match path {
Some(p) if !p.is_empty() => match std::fs::read_to_string(&p) {
Ok(text) => {
let cmds: Vec<Task<Message>> = text
.lines()
.map(str::trim)
.filter(|l| {
!l.is_empty() && !l.starts_with('#') && !l.starts_with(';')
})
.map(|l| Task::done(Message::Command(l.to_string())))
.collect();
self.command_line.push_output(&format!(
"SCRIPT: running {} command(s) from {p}.",
cmds.len()
));
return Some(Task::batch(cmds));
}
Err(e) => {
self.command_line
.push_error(&format!("SCRIPT: cannot read {p}: {e}"));
}
},
_ => {
self.command_line
.push_info("Usage: SCRIPT <path to .scr file>");
}
}
}
_ => return None,
}
Some(self.finish_dispatch(cmd))

View file

@ -84,6 +84,58 @@ impl OpenCADStudio {
}
}
// DBLIST — list data for every entity in the drawing (LIST over the
// whole database rather than the current selection).
"DBLIST" => {
// Format every entity first so the immutable document borrow is
// released before writing to the command line.
let lines: Vec<String> = self.tabs[i]
.scene
.document
.entities()
.map(|entity| {
let type_name = crate::entities::names::dxf_name(entity);
let common = entity.common();
let color_str = common
.color
.index()
.map(|c| c.to_string())
.unwrap_or_else(|| "ByLayer".to_string());
let linetype =
if common.linetype.is_empty() || common.linetype == "ByLayer" {
"ByLayer".to_string()
} else {
common.linetype.clone()
};
let details = entity_list_details(entity);
format!(
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
common.handle.value(),
common.layer,
color_str,
linetype,
if details.is_empty() {
String::new()
} else {
format!("\n {details}")
}
)
})
.collect();
if lines.is_empty() {
self.command_line.push_info("DBLIST: drawing has no entities.");
} else {
let count = lines.len();
for l in lines {
self.command_line.push_output(&l);
}
self.command_line.push_output(&format!(
"DBLIST: {count} entit{}.",
if count == 1 { "y" } else { "ies" }
));
}
}
// ── Break / Join ─────────────────────────────────────────────────
"JOIN" | "J" => {
use crate::modules::draw::modify::join::JoinCommand;
@ -120,7 +172,9 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
}
"ATTEDIT" | "ATE" | "-ATTEDIT" => {
// Bare ATTEDIT (and the ATE alias) launch the interactive attribute
// editor; the command-line -ATTEDIT form is handled in the draw family.
"ATTEDIT" | "ATE" => {
use crate::modules::draw::modify::attedit::AtteditCommand;
let cmd_obj = AtteditCommand::new();
self.command_line.push_info(&cmd_obj.prompt());

View file

@ -200,6 +200,21 @@ impl OpenCADStudio {
// excluded.
inventory::submit!(crate::command::CommandRegistration {
names: &[
// Drafting-aid / display toggles + customization entry points wired in the
// dispatch families (no interactive command module of their own).
"ALIASEDIT", "CLEANSCREEN", "CUI", "DSETTINGS", "GRID", "OSNAP", "POLAR",
"QUICKPROPERTIES", "SNAP",
// Viewport-arrangement shortcuts (delegate to VPORTS configurations).
"CASCADE", "HORIZONTAL", "VERTICAL", "VPJOIN",
// Standard aliases for existing commands.
"BMAKE", "EXPORTPDF", "DDIM",
// Inquiry: list the whole drawing database.
"DBLIST",
// Reset selected entities' overrides to follow their layer.
"SETBYLAYER",
// Remove duplicate objects; set drawing base point; audit integrity;
// read/write system variables.
"OVERKILL", "BASE", "AUDIT", "SETVAR", "SCRIPT", "SCR", "FINDNONPURGEABLE",
"3DORBIT", "3O", "ABOUT", "ATTDISP", "ATTEXT", "BACKGROUND", "CDIMSTY", "CELTSCALE",
"CHANGELOG", "CHPROP", "CLAYER", "CLEAR", "CLR", "COLORSCHEME", "COUNT", "DATAEXTRACTION",
"DE", "DESELALL", "DESELECT", "DIMSTYLE", "DONATE", "DRAWORDER", "DWGPROP", "DWGPROPS",

View file

@ -28,8 +28,46 @@ impl OpenCADStudio {
.push_output(&format!("Linetypes: {}", ltypes.join(", ")));
}
}
// Set the current linetype applied to newly drawn entities.
"SET" | "CURRENT" | "S" => {
let name = parts.get(1).copied().unwrap_or("");
if name.is_empty() {
self.command_line
.push_info("Usage: LINETYPE SET <name | ByLayer | ByBlock>");
} else {
let canon = if name.eq_ignore_ascii_case("BYLAYER") {
Some(("ByLayer".to_string(), acadrust::types::Handle::NULL))
} else if name.eq_ignore_ascii_case("BYBLOCK") {
Some(("ByBlock".to_string(), acadrust::types::Handle::NULL))
} else {
self.tabs[i]
.scene
.document
.line_types
.iter()
.find(|lt| lt.name.eq_ignore_ascii_case(name))
.map(|lt| (lt.name.clone(), lt.handle))
};
match canon {
Some((nm, handle)) => {
let h = &mut self.tabs[i].scene.document.header;
h.current_linetype_name = nm.clone();
h.current_linetype_handle = handle;
self.tabs[i].dirty = true;
self.command_line
.push_output(&format!("Current linetype set to {nm}."));
}
None => {
self.command_line.push_error(&format!(
"LINETYPE: \"{name}\" is not loaded. Use LINETYPE LIST to see available linetypes."
));
}
}
}
}
_ => {
self.command_line.push_info("Usage: LINETYPE LIST");
self.command_line
.push_info("Usage: LINETYPE LIST | SET <name>");
}
}
}
@ -252,6 +290,274 @@ impl OpenCADStudio {
}
}
// ── SETBYLAYER — clear color/linetype/lineweight overrides ────
// Resets the selected entities' direct property overrides back to
// ByLayer so they follow their layer again.
"SETBYLAYER" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
if handles.is_empty() {
self.command_line
.push_error("SETBYLAYER: select entities first.");
} else {
self.push_undo_snapshot(i, "SETBYLAYER");
let mut changed = 0usize;
for handle in &handles {
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) {
let common = entity.common_mut();
common.color = acadrust::types::Color::ByLayer;
common.linetype = "ByLayer".to_string();
common.line_weight = acadrust::types::LineWeight::ByLayer;
changed += 1;
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
self.command_line.push_output(&format!(
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
));
}
}
// ── OVERKILL — delete duplicate (identical) objects ──────────
// Removes objects that are identical in geometry AND properties to
// another object (compared with the handle ignored). Operates on
// the current selection, or the whole drawing when nothing is
// selected. Conservative: only exact duplicates are removed.
"OVERKILL" => {
use acadrust::Handle;
let selected: std::collections::HashSet<u64> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h.value())
.collect();
// Capture (handle, type-name, handle-normalized clone) for each
// candidate while the document is borrowed immutably.
let candidates: Vec<(Handle, String, acadrust::EntityType)> = self.tabs[i]
.scene
.document
.entities()
.filter(|e| selected.is_empty() || selected.contains(&e.common().handle.value()))
.map(|e| {
let key = crate::entities::names::dxf_name(e).to_string();
let mut norm = e.clone();
norm.common_mut().handle = Handle::NULL;
(e.common().handle, key, norm)
})
.collect();
// Bucket by (type, layer) so only like objects are compared.
let mut kept: Vec<(String, acadrust::EntityType)> = Vec::new();
let mut dups: Vec<Handle> = Vec::new();
for (h, key, norm) in &candidates {
let bucket = format!("{key}\u{0}{}", norm.common().layer);
if kept.iter().any(|(b, e)| b == &bucket && e == norm) {
dups.push(*h);
} else {
kept.push((bucket, norm.clone()));
}
}
if dups.is_empty() {
self.command_line
.push_output("OVERKILL: no duplicate objects found.");
} else {
let n = dups.len();
self.push_undo_snapshot(i, "OVERKILL");
self.tabs[i].scene.erase_entities(&dups);
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(&format!("OVERKILL: deleted {n} duplicate object(s)."));
}
}
// ── SETVAR — read / write system variables ───────────────────
// SETVAR <name> → report the value
// SETVAR <name> <value> → set it
// SETVAR ? → list supported variables
// Numeric / boolean variables are settable; current-layer/linetype/
// style names are read-only here (use their own commands to change
// them, which validate the name).
cmd if cmd == "SETVAR" || cmd.starts_with("SETVAR ") => {
let rest = cmd.strip_prefix("SETVAR").unwrap_or("").trim();
let mut it = rest.splitn(2, char::is_whitespace);
let name = it.next().unwrap_or("").to_uppercase();
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
// Parse a boolean given as 0/1 or ON/OFF.
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
"1" | "ON" | "TRUE" => Some(true),
"0" | "OFF" | "FALSE" => Some(false),
_ => None,
};
let outcome: Result<(String, bool), String> = {
let h = &mut self.tabs[i].scene.document.header;
match name.as_str() {
"LTSCALE" => match &value {
Some(v) => v.parse::<f64>().map(|x| { h.linetype_scale = x; (format!("LTSCALE = {x}"), true) }).map_err(|_| "SETVAR: numeric value required.".into()),
None => Ok((format!("LTSCALE = {}", h.linetype_scale), false)),
},
"CELTSCALE" => match &value {
Some(v) => v.parse::<f64>().map(|x| { h.current_entity_linetype_scale = x; (format!("CELTSCALE = {x}"), true) }).map_err(|_| "SETVAR: numeric value required.".into()),
None => Ok((format!("CELTSCALE = {}", h.current_entity_linetype_scale), false)),
},
"PDSIZE" => match &value {
Some(v) => v.parse::<f64>().map(|x| { h.point_display_size = x; (format!("PDSIZE = {x}"), true) }).map_err(|_| "SETVAR: numeric value required.".into()),
None => Ok((format!("PDSIZE = {}", h.point_display_size), false)),
},
"TEXTSIZE" => match &value {
Some(v) => v.parse::<f64>().map(|x| { h.text_height = x; (format!("TEXTSIZE = {x}"), true) }).map_err(|_| "SETVAR: numeric value required.".into()),
None => Ok((format!("TEXTSIZE = {}", h.text_height), false)),
},
"PDMODE" => match &value {
Some(v) => v.parse::<i16>().map(|x| { h.point_display_mode = x; (format!("PDMODE = {x}"), true) }).map_err(|_| "SETVAR: integer value required.".into()),
None => Ok((format!("PDMODE = {}", h.point_display_mode), false)),
},
"ORTHOMODE" => match &value {
Some(v) => parse_bool(v).map(|b| { h.ortho_mode = b; (format!("ORTHOMODE = {}", b as i32), true) }).ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
None => Ok((format!("ORTHOMODE = {}", h.ortho_mode as i32), false)),
},
"FILLMODE" => match &value {
Some(v) => parse_bool(v).map(|b| { h.fill_mode = b; (format!("FILLMODE = {}", b as i32), true) }).ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
None => Ok((format!("FILLMODE = {}", h.fill_mode as i32), false)),
},
"CLAYER" => match &value {
Some(_) => Err("SETVAR: CLAYER is read-only here — use the CLAYER command.".into()),
None => Ok((format!("CLAYER = {}", h.current_layer_name), false)),
},
"CELTYPE" => match &value {
Some(_) => Err("SETVAR: CELTYPE is read-only here — use LINETYPE SET.".into()),
None => Ok((format!("CELTYPE = {}", h.current_linetype_name), false)),
},
"TEXTSTYLE" => match &value {
Some(_) => Err("SETVAR: TEXTSTYLE is read-only here — use the STYLE command.".into()),
None => Ok((format!("TEXTSTYLE = {}", h.current_text_style_name), false)),
},
_ => Err(format!("SETVAR: unknown variable \"{name}\".")),
}
};
match outcome {
Ok((msg, changed)) => {
if changed {
self.tabs[i].dirty = true;
}
self.command_line.push_output(&msg);
}
Err(e) => self.command_line.push_error(&e),
}
}
}
// ── FINDNONPURGEABLE — list named objects still in use ───────
// Reports the layers, linetypes, text styles and blocks that are
// referenced by objects, and therefore cannot be purged — so it is
// clear why PURGE leaves them behind. Read-only.
"FINDNONPURGEABLE" => {
use rustc_hash::FxHashSet;
let mut layers: FxHashSet<String> = FxHashSet::default();
let mut linetypes: FxHashSet<String> = FxHashSet::default();
let mut styles: FxHashSet<String> = FxHashSet::default();
let mut blocks: FxHashSet<String> = FxHashSet::default();
{
let doc = &self.tabs[i].scene.document;
for e in doc.entities() {
let l = &e.common().layer;
if !l.is_empty() {
layers.insert(l.clone());
}
let lt = &e.common().linetype;
if !lt.is_empty() && lt != "ByLayer" && lt != "ByBlock" {
linetypes.insert(lt.clone());
}
match e {
acadrust::EntityType::Text(t) if !t.style.is_empty() => {
styles.insert(t.style.clone());
}
acadrust::EntityType::MText(t) if !t.style.is_empty() => {
styles.insert(t.style.clone());
}
acadrust::EntityType::Insert(ins) => {
blocks.insert(ins.block_name.clone());
}
_ => {}
}
}
}
let fmt = |set: FxHashSet<String>| -> String {
if set.is_empty() {
"(none)".to_string()
} else {
let mut v: Vec<_> = set.into_iter().collect();
v.sort();
v.join(", ")
}
};
self.command_line
.push_output("FINDNONPURGEABLE: named objects in use (not purgeable):");
self.command_line
.push_output(&format!(" Layers: {}", fmt(layers)));
self.command_line
.push_output(&format!(" Linetypes: {}", fmt(linetypes)));
self.command_line
.push_output(&format!(" Text styles: {}", fmt(styles)));
self.command_line
.push_output(&format!(" Blocks: {}", fmt(blocks)));
}
// ── AUDIT — report drawing-database integrity issues ─────────
// Read-only scan: flags entities on undefined layers and block
// references to undefined block definitions. Reports only; it does
// not auto-repair (so it can never make the drawing worse).
"AUDIT" => {
use std::collections::BTreeSet;
let mut undefined_layers: BTreeSet<String> = BTreeSet::new();
let mut undefined_blocks: BTreeSet<String> = BTreeSet::new();
let mut total = 0usize;
{
let doc = &self.tabs[i].scene.document;
for e in doc.entities() {
total += 1;
let layer = &e.common().layer;
if !layer.is_empty() && doc.layers.get(layer).is_none() {
undefined_layers.insert(layer.clone());
}
if let acadrust::EntityType::Insert(ins) = e {
if doc.block_records.get(&ins.block_name).is_none() {
undefined_blocks.insert(ins.block_name.clone());
}
}
}
}
self.command_line
.push_output(&format!("AUDIT: scanned {total} object(s)."));
if undefined_layers.is_empty() && undefined_blocks.is_empty() {
self.command_line.push_output("AUDIT: no issues found.");
} else {
if !undefined_layers.is_empty() {
self.command_line.push_error(&format!(
"AUDIT: reference(s) to undefined layer(s): {}",
undefined_layers.into_iter().collect::<Vec<_>>().join(", ")
));
}
if !undefined_blocks.is_empty() {
self.command_line.push_error(&format!(
"AUDIT: reference(s) to undefined block(s): {}",
undefined_blocks.into_iter().collect::<Vec<_>>().join(", ")
));
}
self.command_line
.push_info("AUDIT: report only — no automatic repair performed.");
}
}
// ── RENAME table entries ──────────────────────────────────────
cmd if cmd == "RENAME" || cmd.starts_with("RENAME ") => {
// Usage: RENAME <type> <old_name> <new_name>

View file

@ -237,6 +237,13 @@ impl OpenCADStudio {
self.command_line.push_info("Opening release notes...");
}
// ── CUI / ALIASEDIT — customization entry points ───────────────
// Command-alias and key-binding editing lives in the keyboard
// shortcuts panel, so route the customization verbs there.
"CUI" | "ALIASEDIT" => {
return Some(Task::done(Message::ShortcutsPanelOpen));
}
// ── Keyboard Shortcuts panel ──────────────────────────────────
cmd if cmd == "SHORTCUTS" || cmd.starts_with("SHORTCUTS ") => {
let raw_rest = cmd.trim_start_matches("SHORTCUTS").trim();
@ -357,6 +364,15 @@ impl OpenCADStudio {
return Some(Task::done(Message::PspaceCommand));
}
// ── Viewport arrangement shortcuts ────────────────────────────
// Tile the model viewports into preset splits. Each delegates to the
// matching VPORTS configuration so the Model/paper handling stays in
// one place.
"HORIZONTAL" => return self.dispatch_view("VPORTS 2H", i),
"VERTICAL" => return self.dispatch_view("VPORTS 2V", i),
"VPJOIN" => return self.dispatch_view("VPORTS SINGLE", i),
"CASCADE" => return self.dispatch_view("VPORTS 4", i),
// ── VPORTS — list or create preset viewport configurations ────
cmd if cmd == "VPORTS" || cmd.starts_with("VPORTS ") => {
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();