//! The command registry. Every user-visible action is a command with an //! id, label, menu path, shortcut, parameter documentation, an `enabled` //! predicate and a `run` function. mod arrange; mod chop; mod chop_ext; pub mod color; mod compound; mod digitize; mod dimline; mod doc; mod edit; mod export_panel; pub(crate) mod file; mod indicator; mod joinsplit; mod library; pub(crate) mod measure; mod object; mod outline; pub mod picture; pub(crate) mod plot; mod prefs; mod print; mod region; mod sfactor; pub(crate) mod shape; mod spell; mod styles; pub(crate) mod tangent; pub(crate) use dimline::formatter as dimension_formatter; pub(crate) use region::circumcircle; pub use shape::STYLE_PARAMS; mod tool; mod transform; mod view; use crate::{Engine, EngineError}; use rustfig_geom::Point; use serde_json::Value; use std::sync::OnceLock; /// Result of running a command. pub type CmdResult = Result; /// A command definition. pub struct Command { /// Stable id, e.g. `edit.undo`. pub id: &'static str, /// Menu label. pub label: &'static str, /// Menu path (`"Edit"`, `"Object/Arrange"`), empty if not in a menu. pub menu: &'static str, /// Keyboard shortcut (`"Ctrl+Z"`), empty if none. `Ctrl` means Cmd on macOS. pub shortcut: &'static str, /// Parameter documentation (JSON-ish). pub params: &'static str, /// Whether the command changes the document (and is undoable). pub mutates: bool, pub enabled: fn(&Engine) -> bool, pub run: fn(&mut Engine, &Value) -> CmdResult, } pub(crate) fn always(_: &Engine) -> bool { true } pub(crate) fn has_objects(e: &Engine) -> bool { !e.doc().objects.is_empty() } /// All commands. pub fn all() -> &'static [Command] { static ALL: OnceLock> = OnceLock::new(); ALL.get_or_init(|| { let mut v = Vec::new(); v.extend(file::commands()); v.extend(digitize::commands()); v.extend(export_panel::commands()); v.extend(print::commands()); v.extend(edit::commands()); v.extend(view::commands()); v.extend(shape::commands()); v.extend(picture::commands()); v.extend(transform::commands()); v.extend(arrange::commands()); v.extend(object::commands()); v.extend(compound::commands()); v.extend(chop::commands()); v.extend(tangent::commands()); v.extend(sfactor::commands()); v.extend(measure::commands()); v.extend(dimline::commands()); v.extend(joinsplit::commands()); v.extend(library::commands()); v.extend(plot::commands()); v.extend(region::commands()); v.extend(doc::commands()); v.extend(color::commands()); v.extend(indicator::commands()); v.extend(tool::commands()); v.extend(prefs::commands()); v.extend(styles::commands()); v.extend(spell::commands()); v }) } /// Look a command up by id. pub fn find(id: &str) -> Option<&'static Command> { all().iter().find(|c| c.id == id) } // ---- parameter helpers ------------------------------------------------- pub(crate) fn num(p: &Value, key: &str) -> Result { opt_num(p, key)?.ok_or_else(|| EngineError::bad_params(format!("missing number parameter {key:?}"))) } pub(crate) fn opt_num(p: &Value, key: &str) -> Result, EngineError> { match p.get(key) { None | Some(Value::Null) => Ok(None), Some(v) => match v.as_f64() { Some(f) if f.is_finite() && f.abs() < 1.0e9 => Ok(Some(f)), _ => Err(EngineError::bad_params(format!( "parameter {key:?} must be a finite number" ))), }, } } pub(crate) fn num_or(p: &Value, key: &str, default: f64) -> Result { Ok(opt_num(p, key)?.unwrap_or(default)) } pub(crate) fn opt_str<'a>(p: &'a Value, key: &str) -> Result, EngineError> { match p.get(key) { None | Some(Value::Null) => Ok(None), Some(Value::String(s)) => Ok(Some(s.as_str())), Some(_) => Err(EngineError::bad_params(format!("parameter {key:?} must be a string"))), } } pub(crate) fn string(p: &Value, key: &str) -> Result { opt_str(p, key)? .map(str::to_string) .ok_or_else(|| EngineError::bad_params(format!("missing string parameter {key:?}"))) } pub(crate) fn opt_bool(p: &Value, key: &str) -> Result, EngineError> { match p.get(key) { None | Some(Value::Null) => Ok(None), Some(Value::Bool(b)) => Ok(Some(*b)), Some(_) => Err(EngineError::bad_params(format!( "parameter {key:?} must be true or false" ))), } } /// Largest point list accepted from parameters. pub const MAX_PARAM_POINTS: usize = 1_000_000; /// `[[x, y], ...]` or `[{"x":..,"y":..}, ...]`. pub(crate) fn points(p: &Value, key: &str) -> Result, EngineError> { let Some(Value::Array(a)) = p.get(key) else { return Err(EngineError::bad_params(format!("missing point list {key:?}"))); }; if a.len() > MAX_PARAM_POINTS { return Err(EngineError::bad_params("too many points")); } a.iter().map(point_value).collect() } pub(crate) fn point_value(v: &Value) -> Result { let (x, y) = match v { Value::Array(xy) => (xy.first().and_then(Value::as_f64), xy.get(1).and_then(Value::as_f64)), Value::Object(_) => (v.get("x").and_then(Value::as_f64), v.get("y").and_then(Value::as_f64)), _ => (None, None), }; match (x, y) { (Some(x), Some(y)) if x.is_finite() && y.is_finite() && x.abs() < 1.0e9 && y.abs() < 1.0e9 => { Ok(Point::new(x, y)) } _ => Err(EngineError::bad_params("points must be [x, y] pairs of finite numbers")), } } pub(crate) fn ids_value(ids: &[rustfig_model::ObjectId]) -> Value { Value::Array(ids.iter().map(|i| Value::from(i.0)).collect()) } #[cfg(test)] mod tests { use super::*; #[test] fn ids_are_unique_and_documented() { let mut seen = std::collections::BTreeSet::new(); for c in all() { assert!(seen.insert(c.id), "duplicate command id {}", c.id); assert!(!c.label.is_empty(), "{} has no label", c.id); assert!(c.id.contains('.'), "{} is not namespaced", c.id); } } #[test] fn shortcuts_are_unique() { let mut seen = std::collections::BTreeMap::new(); for c in all() { if !c.shortcut.is_empty() { if let Some(prev) = seen.insert(c.shortcut, c.id) { panic!("shortcut {} used by {} and {}", c.shortcut, prev, c.id); } } } } } .