//! The command registry. Each area has its own module; [`ALL`] lists every command. mod block; mod cells; mod contact; pub(crate) mod datum; pub(crate) mod deck; pub(crate) mod define; mod edit; pub(crate) mod file; pub(crate) mod forms; mod geometry; mod hex; pub(crate) mod mesh; pub use datum::edit_params as datum_edit_params; pub use mesh::analytic_surfaces; mod mirror; mod model; pub(crate) mod octree; pub mod replace; mod results; pub(crate) mod seeds; pub(crate) mod solve; mod symmetry; mod tags; pub(crate) mod view; use crate::schema::ProblemKind; use fieldcraft_model::MAX_LABEL; use serde::Deserialize; use serde_json::{Value, json}; use fieldcraft_io::roots::Access; use crate::{CommandSpec, EngineError, Result, Session}; pub static ALL: &[CommandSpec] = &[ model::INFO, geometry::OPEN, geometry::BODIES, geometry::TESSELLATE, geometry::IMPORT_MESH, datum::DATUM_PLANE, datum::DATUM_AXIS, datum::DATUM_POINT, datum::DATUM_EDIT, datum::DATUM_AXIS_EDIT, datum::DATUM_POINT_EDIT, datum::DATUM_DELETE, view::HIDE, view::SHOW, view::ISOLATE, view::SHOW_ALL, datum::PARTITION, datum::PARTITION_DELETE, mesh::NODE_ADD, mesh::NODE_REMOVE, mesh::ELEMENT_ADD, mesh::ELEMENT_REMOVE, mesh::QUALITY, mesh::DELAUNAY_POINTS, mesh::SURFACE_CHECK, mesh::PLANAR, mesh::TET_GENERATE, octree::HEX_OCTREE, block::BLOCK, hex::HEX_GENERATE, hex::HEX_SWEEP, hex::HEX_MAPPED, hex::HEX_BLOCKS, cells::CELLS, seeds::SEED_EDGE, seeds::SEED_CLEAR, seeds::SEED_LIST, mirror::MIRROR, tags::TAG, tags::TAG_CLEAR, model::UNITS, model::CHECK, model::SET_RESOLVE, define::SET_CREATE, define::SET_EDIT, define::SET_DELETE, define::MATERIAL_CREATE, define::MATERIAL_EDIT, define::MATERIAL_DELETE, define::MATERIAL_PLASTICITY, define::MATERIAL_DAMAGE, define::SECTION_CREATE, define::SECTION_EDIT, define::SECTION_DELETE, define::REFERENCE_POINT_CREATE, define::REFERENCE_POINT_EDIT, define::REFERENCE_POINT_DELETE, define::COUPLING_CREATE, define::COUPLING_EDIT, define::COUPLING_DELETE, contact::PAIR, contact::GENERAL, contact::EDIT, contact::DELETE, contact::FIND, contact::TIE, contact::TIE_EDIT, contact::TIE_DELETE, define::MASS_CREATE, define::MASS_EDIT, define::MASS_DELETE, define::SPRING_CREATE, define::SPRING_EDIT, define::SPRING_DELETE, define::BOUNDARY_CONDITION_CREATE, define::BOUNDARY_CONDITION_EDIT, define::BOUNDARY_CONDITION_DELETE, symmetry::SYMMETRY, define::LOAD_CREATE, define::LOAD_EDIT, define::LOAD_DELETE, define::FLUID_DOMAIN_CREATE, define::FLUID_DOMAIN_EDIT, define::FLUID_DOMAIN_DELETE, define::FLUID_BOUNDARY, define::STEP_CREATE, define::STEP_EDIT, define::STEP_DELETE, define::STEP_ACTIVATE, define::ACTIVITY, file::OPEN, file::SAVE, file::SAVE_AS, file::RECOVERY_LIST, file::RECOVER, file::RECOVERY_DISCARD, file::SAMPLE, edit::UNDO, edit::REDO, deck::WRITE, solve::RUN, solve::STATIC, solve::NONLINEAR, solve::FREQUENCY, solve::BUCKLE, solve::HEAT, solve::EXPLICIT, solve::HISTORY, solve::FLUID, solve::PROBE, results::DEFORMED, results::CONTOUR, results::MODES, results::ANIMATE, results::FRAME, results::REACTIONS, results::CLEAR, ]; /// The problem with required parameter `key`: [`ProblemKind::Required`] when it is left out, /// else `kind`. pub(crate) fn wrong(p: &Value, key: &str, kind: ProblemKind) -> EngineError { EngineError::param(key, if p.get(key).is_none_or(Value::is_null) { ProblemKind::Required } else { kind }) } /// A required label parameter (1..=u32::MAX; the model checks its own range). pub(crate) fn label(p: &Value, key: &str) -> Result { p.get(key) .and_then(Value::as_u64) .and_then(|v| u32::try_from(v).ok()) .ok_or_else(|| wrong(p, key, ProblemKind::NotAWholeNumber { min: 1, max: i64::from(u32::MAX) })) } /// The first node and element labels after the model's largest, for `nodes` new nodes and /// `elements` new elements, all within [`MAX_LABEL`]. pub(crate) fn fresh_labels(model: &fieldcraft_model::Model, nodes: usize, elements: usize) -> Result<(u32, u32)> { let first = |last: Option<&u32>, count: usize| { let first = last.map_or(Some(1), |l| l.checked_add(1))?; (u64::from(first) + count as u64 <= u64::from(MAX_LABEL) + 1).then_some(first) }; match (first(model.nodes().keys().next_back(), nodes), first(model.elements().keys().next_back(), elements)) { (Some(n), Some(e)) => Ok((n, e)), _ => Err(EngineError::Params(format!("the mesh needs labels beyond {MAX_LABEL}"))), } } /// A required `[x, y, z]` parameter. pub(crate) fn xyz(p: &Value, key: &str) -> Result<[f64; 3]> { p.get(key).and_then(xyz_value).ok_or_else(|| wrong(p, key, ProblemKind::NotAVector { len: 3 })) } /// A JSON `[x, y, z]`. fn xyz_value(v: &Value) -> Option<[f64; 3]> { match v.as_array()?.as_slice() { [x, y, z] => Some([x.as_f64()?, y.as_f64()?, z.as_f64()?]), _ => None, } } /// A required list of `[x, y, z]` points, at most `max_len` long. pub(crate) fn points(p: &Value, key: &str, max_len: usize) -> Result> { let raw = p.get(key).and_then(Value::as_array).filter(|a| a.len() <= max_len); let raw = raw.ok_or_else(|| wrong(p, key, ProblemKind::NotAList { max: max_len, what: "[x, y, z]" }))?; raw.iter() .enumerate() .map(|(i, v)| xyz_value(v).ok_or_else(|| EngineError::invalid(key, format!("must be a list of [x, y, z] (entry {i} is not)")))) .collect() } /// A required list of labels (each in 1..=MAX_LABEL), at most `max_len` long. pub(crate) fn labels(p: &Value, key: &str, max_len: usize) -> Result> { let raw = p .get(key) .and_then(Value::as_array) .filter(|a| a.len() <= max_len) .ok_or_else(|| wrong(p, key, ProblemKind::NotAList { max: max_len, what: "labels" }))?; raw.iter() .map(|v| v.as_u64().and_then(|n| u32::try_from(n).ok()).filter(|n| (1..=MAX_LABEL).contains(n))) .collect::>>() .ok_or_else(|| EngineError::invalid(key, format!("must be labels in 1..={MAX_LABEL}"))) } /// A required path parameter, as given. fn path_param<'a>(p: &'a Value, key: &str) -> Result<&'a str> { p.get(key) .and_then(Value::as_str) .filter(|s| !s.trim().is_empty() && s.len() <= fieldcraft_io::MAX_PATH_LEN && !s.contains('\0')) .ok_or_else(|| wrong(p, key, ProblemKind::NotAPath)) } /// A required path parameter naming a file (or folder) the command reads or writes: the path /// to use, checked against the session's [`Session::file_access`] (as given while unconfined; /// resolved, links followed, when confined). Every command that touches a file named by a /// parameter takes its path from here. pub(crate) fn file_param(s: &Session, p: &Value, key: &str, access: Access) -> Result { s.checked_path(path_param(p, key)?, access) } /// An optional number parameter. pub(crate) fn optional_number(p: &Value, key: &str, default: f64) -> Result { match p.get(key) { None => Ok(default), Some(v) => v.as_f64().ok_or_else(|| EngineError::param(key, ProblemKind::NotANumber)), } } /// The `name` parameter. pub(crate) fn name(p: &Value) -> Result<&str> { p.get("name").and_then(Value::as_str).ok_or_else(|| wrong(p, "name", ProblemKind::NotAName)) } /// An optional parameter read as `T`'s JSON form; `None` when it is absent. pub(crate) fn opt<'a, T: Deserialize<'a>>(p: &'a Value, key: &str) -> Result> { p.get(key).map(|v| T::deserialize(v).map_err(|e| EngineError::invalid(key, format!("is not valid: {e}")))).transpose() } /// A required parameter read as `T`'s JSON form. pub(crate) fn req<'a, T: Deserialize<'a>>(p: &'a Value, key: &str) -> Result { opt(p, key)?.ok_or_else(|| EngineError::param(key, ProblemKind::Required)) } /// Entries listed in a report: the first ones, and how many there are. pub(crate) const LISTED: usize = 100; pub(crate) fn listed(v: &[T]) -> Value { json!({"count": v.len(), "labels": v.get(..LISTED).unwrap_or(v)}) }