cad-editor/src/app/commands/mod.rs

178 lines
7.6 KiB
Rust
Raw Normal View History

use super::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::scene::Scene;
use iced::Task;
use std::path::PathBuf;
mod blocks;
mod dim;
mod display;
mod draw;
mod fileops;
mod inquiry;
mod layerprops;
mod layers;
mod styleprops;
mod view;
// `DrawOrderRefCommand` lives in the `view` family file but is referenced by
// path (`commands::DrawOrderRefCommand`) from `update.rs`, so re-export it at
// the module root to keep that path valid.
pub(crate) use view::DrawOrderRefCommand;
impl OpenCADStudio {
/// First `"{prefix}{n}"` (n ≥ 1) not already used by a block record in the
/// active drawing. Used to auto-name a paste-as-block definition.
fn unique_block_name(&self, prefix: &str) -> String {
let i = self.active_tab;
let mut n = 1;
loop {
let name = format!("{prefix}{n}");
if self.tabs[i].scene.document.block_records.get(&name).is_none() {
return name;
}
n += 1;
}
}
pub(super) fn dispatch_command(&mut self, cmd: &str) -> Task<Message> {
let i = self.active_tab;
// Starting a command closes any open ribbon dropdown (e.g. a style
// combo left open) so it does not stay stuck behind the new tool.
self.ribbon.close_dropdown();
// Cancel any running command before starting a new one.
if self.tabs[i].active_cmd.is_some() {
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
}
// Starting any command leaves interactive PAN mode (the PAN arm below
// re-enables it).
self.tabs[i].pan_mode = false;
// Reset the last committed point so the first click of the new command
// is not constrained by ortho/polar relative to a previous command's endpoint.
self.last_point = None;
// A fresh command starts at the polar/cartesian default — clear
// any `,`-driven reshape from a previous command (#35).
self.dyn_user_reshaped = false;
if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") {
let path = PathBuf::from(path_str);
let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
return Task::done(Message::OpenPathPicked(Some((path, size))));
}
// The Start (welcome) tab has no drawing to act on, so a drawing
// command would silently do nothing. Allow only the commands that
// make sense there (create / open a document, or quit) and tell the
// user otherwise instead of running a no-op. See #96.
if self.tabs[i].is_start
&& !matches!(
cmd,
"NEW" | "OPEN" | "EXIT" | "QUIT" | "REPORT" | "CHANGELOG" | "ABOUT"
| "PLUGINS" | "PLUGINMANAGER" | "DONATE" | "WEBVERSION"
)
{
self.command_line
.push_info("No drawing open. Use NEW or OPEN to start a drawing.");
return Task::none();
}
if crate::plugin::try_dispatch(self, i, cmd) {
// try_dispatch returns true for both finished commands and interactive
// commands that it just installed. If no command is now active, the
// tool was a one-shot and we must turn the ribbon highlight off here —
// normally apply_cmd_result does that, but plugin dispatch can return
// without producing a CmdResult.
if self.tabs[i].active_cmd.is_none() {
self.ribbon.deactivate_tool();
}
return Task::none();
}
// Command families are dispatched in source order; the first family
// whose `match` arm matches `cmd` handles it. Each handler returns
// `Some(task)` when it handled the command (either an early-returning
// arm or a state-mutating arm that falls through to `finish_dispatch`),
// or `None` to fall through to the next family — exactly as a single
// sequential `match` over all arms would behave.
if let Some(t) = self.dispatch_fileops(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_layers(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_blocks(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_draw(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_dim(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_inquiry(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_view(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_layerprops(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_styleprops(cmd, i) {
return t;
}
if let Some(t) = self.dispatch_display(cmd, i) {
return t;
}
// No family matched.
self.command_line
.push_error(&format!("Unknown command: {cmd}"));
self.finish_dispatch(cmd)
}
/// Shared tail run after a `dispatch_*` family handler whose matched arm
/// did not early-return. Focuses the command line whenever a command just
/// became active.
fn finish_dispatch(&mut self, cmd: &str) -> Task<Message> {
let i = self.active_tab;
if self.tabs[i].active_cmd.is_some() {
self.tabs[i].last_cmd = Some(cmd.to_string());
self.focus_cmd_input()
} else {
Task::none()
}
}
}
// ── Autocomplete registry — one-shot commands ──────────────────────────────
// These commands dispatch a single action (file ops, view, layer/style
// managers, undo/redo, …) rather than installing an interactive `CadCommand`,
// so they have no module of their own to register from. They are surfaced for
// command-line autocomplete here. Internal dispatch tokens that the user never
// types (REFEDIT_BEGIN, REFCLOSE_SAVE, REFCLOSE_DISCARD) are intentionally
// excluded.
inventory::submit!(crate::command::CommandRegistration {
names: &[
"3DORBIT", "3O", "ABOUT", "ATTDISP", "ATTEXT", "BACKGROUND", "CDIMSTY", "CELTSCALE",
"CHANGELOG", "CHPROP", "CLAYER", "CLEAR", "CLR", "COLORSCHEME", "COUNT", "DATAEXTRACTION",
"DE", "DESELALL", "DESELECT", "DIMSTYLE", "DONATE", "DRAWORDER", "DWGPROP", "DWGPROPS",
"EATTEXT", "EXIT", "EXPORT", "EXPORTSTEP", "EXPORTSTL", "FILETAB", "FIND", "FLATTEN",
"HELP", "HIDEOBJECTS", "IM", "IMAGE", "IMAGEATTACH", "IMPORTOBJ", "ISOLATEOBJECTS", "LA",
"LAYER", "LAYERS", "LAYISO", "LAYON", "LAYOUTMANAGER", "LAYOUTPANEL", "LAYOUTTAB", "LAYTHW",
"LAYUNISO", "LI", "LINETYPE", "LIST", "LTSCALE", "LWDISPLAY", "MASSPROP", "MLEADERSTYLE",
"MLSTYLE", "MS", "MSPACE", "NAVVCUBE", "NEW", "OBJIMPORT", "OPEN", "ORTHO",
"P", "PAN", "PAGESETUP", "PERF", "PERSP", "PLOT", "PLOTSTYLE", "PLOTSTYLEEDITOR",
"PLOTSTYLEPANEL", "PR",
"PRINT", "PROPERTIES", "PROPS", "PSPACE", "PURGE", "QS", "QSAVE", "QSELECT",
"QUIT", "REDO", "REDRAW", "REDRWALL", "REGEN", "REGENALL", "RENAME", "REPORT",
"SA", "SAVE", "SAVEAS", "SCALETEXT", "SELECTALL", "SELECTSIMILAR", "SELSIM", "SHEETSET",
"SHORTCUTS", "SOLID", "SSM", "STEPOUT", "STLOUT", "STPOUT", "STYLE", "STYLESMANAGER",
"TABLESTYLE", "TOOLPALETTES", "TP", "TS", "U", "UCS", "UCSICON", "UNDERLAY",
"UNDO", "UNISOLATEOBJECTS", "USERI", "USERR", "VIEW", "VPORTS", "VS", "VW",
"WB", "WBLOCK", "WEBVERSION", "WIREFRAME", "XA", "XATTACH", "XDATA", "XR",
"XREF", "XRELOAD", "ZOOM", "ZS",
]
});