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:
parent
e04b580a70
commit
0ca3954b5d
32 changed files with 2763 additions and 67 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue