mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-01 06:46:50 -06:00
- All 2D Draw-menu items: ARC variants, CIRCLE TTR/TTT, DIVIDE/MEASURE, REVCLOUD, WIPEOUT, 3DPOLY, MLINE, HELIX, SPLINE CV, CENTERMARK/CENTERLINE, XLINE and RECTANG options. - PEDIT, SPLINEDIT, LENGTHEN, ALIGN, BLEND, REVERSE, NCOPY, CHSPACE, FLATTEN, ARRAYPATH, TEXTEDIT; FILLET with arcs and polylines; TRIM ellipses and splines; grip editing commands. - Layouts, MVIEW/VPORTS, viewport scale/lock/layers, PAGESETUP, PLOT/EXPORTPDF with our own PDF writer; paper-space rendering through viewports. - GPU canvas (wgpu batches, CPU fallback); rstar spatial index for pick/select/osnap; cadcraft-cli perf harness. - CAL, FIND/replace, MASSPROP, MEASUREGEOM; TrueType outlines in cadcraft-fonts; contributors standard (xtask contributors, About window credits); docs/mcp.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
7.3 KiB
Rust
237 lines
7.3 KiB
Rust
//! The command registry. Ids are AutoCAD command names in lower case; menu paths follow
|
|
//! AutoCAD's menu bar so the catalog doubles as the parity metric.
|
|
|
|
mod annotate;
|
|
mod blocks;
|
|
mod draw;
|
|
mod draw2;
|
|
mod edit;
|
|
pub mod file;
|
|
mod gripcmds;
|
|
mod hatch;
|
|
mod inquiry;
|
|
mod layer;
|
|
mod layout;
|
|
mod modify;
|
|
mod modify2;
|
|
mod props;
|
|
mod settings;
|
|
mod utility;
|
|
mod view;
|
|
|
|
pub mod curves;
|
|
pub mod helpers;
|
|
pub mod machines;
|
|
|
|
use cadcraft_doc::Handle;
|
|
use cadcraft_geom::Vec2;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
use crate::{EngineError, Interactive, Result, Session};
|
|
|
|
pub type Run = fn(&mut Session, &Value) -> Result<Value>;
|
|
pub type Enabled = fn(&Session) -> std::result::Result<(), String>;
|
|
pub type Factory = fn(&Session) -> Result<Box<dyn Interactive>>;
|
|
|
|
pub struct CommandSpec {
|
|
pub id: &'static str,
|
|
pub label: &'static str,
|
|
/// Menu placement, e.g. `["Draw", "Circle", "Center, Radius"]`. Empty = not in menus.
|
|
pub menu: &'static [&'static str],
|
|
pub shortcut: Option<&'static str>,
|
|
pub aliases: &'static [&'static str],
|
|
pub params: &'static str,
|
|
pub enabled: Enabled,
|
|
pub run: Run,
|
|
pub undoable: bool,
|
|
pub interactive: Option<Factory>,
|
|
/// Can run inside another command with a leading apostrophe.
|
|
pub transparent: bool,
|
|
}
|
|
|
|
impl CommandSpec {
|
|
pub const fn new(id: &'static str, label: &'static str, run: Run) -> Self {
|
|
CommandSpec {
|
|
id,
|
|
label,
|
|
menu: &[],
|
|
shortcut: None,
|
|
aliases: &[],
|
|
params: "",
|
|
enabled: has_doc,
|
|
run,
|
|
undoable: true,
|
|
interactive: None,
|
|
transparent: false,
|
|
}
|
|
}
|
|
pub const fn menu(mut self, m: &'static [&'static str]) -> Self {
|
|
self.menu = m;
|
|
self
|
|
}
|
|
pub const fn key(mut self, k: &'static str) -> Self {
|
|
self.shortcut = Some(k);
|
|
self
|
|
}
|
|
pub const fn alias(mut self, a: &'static [&'static str]) -> Self {
|
|
self.aliases = a;
|
|
self
|
|
}
|
|
pub const fn params(mut self, p: &'static str) -> Self {
|
|
self.params = p;
|
|
self
|
|
}
|
|
pub const fn enabled(mut self, e: Enabled) -> Self {
|
|
self.enabled = e;
|
|
self
|
|
}
|
|
pub const fn noundo(mut self) -> Self {
|
|
self.undoable = false;
|
|
self
|
|
}
|
|
pub const fn interactive(mut self, f: Factory) -> Self {
|
|
self.interactive = Some(f);
|
|
self
|
|
}
|
|
pub const fn transparent(mut self) -> Self {
|
|
self.transparent = true;
|
|
self
|
|
}
|
|
pub fn info(&self, s: &Session) -> CommandInfo {
|
|
let e = (self.enabled)(s);
|
|
CommandInfo {
|
|
id: self.id,
|
|
label: self.label,
|
|
menu: self.menu.to_vec(),
|
|
shortcut: self.shortcut,
|
|
aliases: self.aliases.to_vec(),
|
|
params: self.params,
|
|
interactive: self.interactive.is_some(),
|
|
enabled: e.is_ok(),
|
|
disabled_reason: e.err(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CommandInfo {
|
|
pub id: &'static str,
|
|
pub label: &'static str,
|
|
pub menu: Vec<&'static str>,
|
|
pub shortcut: Option<&'static str>,
|
|
pub aliases: Vec<&'static str>,
|
|
pub params: &'static str,
|
|
pub interactive: bool,
|
|
pub enabled: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub disabled_reason: Option<String>,
|
|
}
|
|
|
|
pub fn always(_: &Session) -> std::result::Result<(), String> {
|
|
Ok(())
|
|
}
|
|
pub fn has_doc(s: &Session) -> std::result::Result<(), String> {
|
|
s.state().map(|_| ()).map_err(|_| "no drawing open".into())
|
|
}
|
|
pub fn has_selection(s: &Session) -> std::result::Result<(), String> {
|
|
has_doc(s)?;
|
|
if s.selection().is_empty() { Err("nothing selected".into()) } else { Ok(()) }
|
|
}
|
|
|
|
pub fn command_specs() -> &'static [CommandSpec] {
|
|
static SPECS: std::sync::OnceLock<Vec<CommandSpec>> = std::sync::OnceLock::new();
|
|
SPECS.get_or_init(|| {
|
|
let mut v = Vec::new();
|
|
v.extend(file::specs());
|
|
v.extend(edit::specs());
|
|
v.extend(view::specs());
|
|
v.extend(draw::specs());
|
|
v.extend(draw2::specs());
|
|
v.extend(annotate::specs());
|
|
v.extend(hatch::specs());
|
|
v.extend(blocks::specs());
|
|
v.extend(modify::specs());
|
|
v.extend(modify2::specs());
|
|
v.extend(gripcmds::specs());
|
|
v.extend(layer::specs());
|
|
v.extend(layout::specs());
|
|
v.extend(props::specs());
|
|
v.extend(inquiry::specs());
|
|
v.extend(settings::specs());
|
|
v.extend(utility::specs());
|
|
v
|
|
})
|
|
}
|
|
|
|
pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
|
|
let l = id.to_ascii_lowercase();
|
|
command_specs().iter().find(|c| c.id == l)
|
|
}
|
|
|
|
/// Resolve a typed name or alias (AutoCAD-style short names such as `L`, `C`, `PL`) to an id.
|
|
pub fn resolve_alias(name: &str) -> String {
|
|
let l = name.to_ascii_lowercase();
|
|
if find_command(&l).is_some() {
|
|
return l;
|
|
}
|
|
command_specs().iter().find(|c| c.aliases.iter().any(|a| a.eq_ignore_ascii_case(&l))).map(|c| c.id.to_string()).unwrap_or(l)
|
|
}
|
|
|
|
// ---------- param helpers ----------
|
|
|
|
pub(crate) fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
|
|
EngineError::BadParams { cmd: cmd.into(), msg: msg.into() }
|
|
}
|
|
pub(crate) fn f64_or(p: &Value, key: &str, default: f64) -> f64 {
|
|
p.get(key).and_then(Value::as_f64).filter(|v| v.is_finite()).unwrap_or(default)
|
|
}
|
|
pub(crate) fn f64_req(cmd: &str, p: &Value, key: &str) -> Result<f64> {
|
|
p.get(key).and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad(cmd, format!("`{key}` (number) is required")))
|
|
}
|
|
pub(crate) fn bool_or(p: &Value, key: &str, default: bool) -> bool {
|
|
p.get(key).and_then(Value::as_bool).unwrap_or(default)
|
|
}
|
|
pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> {
|
|
p.get(key).and_then(Value::as_str)
|
|
}
|
|
pub fn point_value(v: &Value) -> Option<Vec2> {
|
|
if let Some(a) = v.as_array() {
|
|
let x = a.first()?.as_f64()?;
|
|
let y = a.get(1)?.as_f64()?;
|
|
let p = Vec2::new(x, y);
|
|
return p.is_finite().then_some(p);
|
|
}
|
|
if let Some(s) = v.as_str() {
|
|
return crate::prompt::parse_point(s, Vec2::ZERO);
|
|
}
|
|
let x = v.get("x")?.as_f64()?;
|
|
let y = v.get("y")?.as_f64()?;
|
|
let p = Vec2::new(x, y);
|
|
p.is_finite().then_some(p)
|
|
}
|
|
pub(crate) fn point_param(p: &Value, key: &str) -> Option<Vec2> {
|
|
p.get(key).and_then(point_value)
|
|
}
|
|
pub(crate) fn point_req(cmd: &str, p: &Value, key: &str) -> Result<Vec2> {
|
|
point_param(p, key).ok_or_else(|| bad(cmd, format!("`{key}` ([x, y]) is required")))
|
|
}
|
|
pub(crate) fn points_param(p: &Value, key: &str) -> Option<Vec<Vec2>> {
|
|
p.get(key)?.as_array()?.iter().map(point_value).collect()
|
|
}
|
|
/// Handles from `handles` (hex strings or numbers) or the current selection.
|
|
pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<Handle>> {
|
|
if let Some(a) = p.get("handles").and_then(Value::as_array) {
|
|
let hs: Vec<Handle> = a.iter().filter_map(|v| v.as_str().and_then(Handle::parse_hex).or_else(|| v.as_u64().map(Handle))).collect();
|
|
return Ok(hs);
|
|
}
|
|
if let Some(h) = p.get("handle").and_then(|v| v.as_str().and_then(Handle::parse_hex).or_else(|| v.as_u64().map(Handle))) {
|
|
return Ok(vec![h]);
|
|
}
|
|
Ok(s.selection())
|
|
}
|
|
pub(crate) fn ok() -> Result<Value> {
|
|
Ok(Value::Null)
|
|
}
|