//! What a command run from the UI answered, made readable: a one-line summary for the status bar //! ([`summary`]) and, when the reply holds more than that line, the whole reply in the context //! panel ([`show`]): Check Model as a list of its diagnostics with a way to each fix, the Step //! Table as a grid of steps, any other reply as labelled rows and tables. Programmatic callers //! (the control channel's `engine.execute`, MCP, scripts) still get the reply's JSON. use egui::{RichText, vec2}; use serde_json::{Map, Value}; use crate::browser::{self, Def}; use crate::frame::{Level, heading}; use crate::results::{components, count, number, with_unit}; use crate::theme::{Tokens, label_font}; use crate::{ModelApp, Tab}; use fieldcraft_engine::results::Quantity; use fieldcraft_model::UnitSystem; /// The reply shown in the context panel. #[derive(Clone, Debug, PartialEq)] pub struct Reply { pub command: String, pub label: String, pub params: Value, pub value: Value, } /// The solvers whose reply is a step's summary ([`solve_summary`]). const SOLVES: [&str; 6] = ["solve.static", "solve.nonlinear", "solve.heat", "solve.frequency", "solve.buckle", "solve.explicit"]; /// Replies that only read the model, refreshed when it changes; any other reply closes then. pub const LIVE: [&str; 2] = ["model.check", "model.activity"]; /// The status line's text (after the command's label) is cut to this many characters. const MAX_LINE: usize = 120; /// Table rows shown before "… n more". const MAX_ROWS: usize = 200; /// Table columns shown at most. const MAX_COLUMNS: usize = 8; /// Nesting shown at most; deeper values are compact text. const MAX_DEPTH: usize = 4; /// One line saying what `command` answered: the reply's own `summary`, a sentence for the /// commands with a well-known reply, otherwise its top-level numbers and words. Never JSON. pub fn summary(command: &str, v: &Value) -> String { summary_in(command, v, UnitSystem::Unspecified) } /// [`summary`] with the numbers' units in `units`. pub fn summary_in(command: &str, v: &Value, units: UnitSystem) -> String { if let Some(s) = v.get("summary").and_then(Value::as_str) { return one_line(s); } if SOLVES.contains(&command) { return one_line(&solve_line(v, units)); } let count = |key: &str| v.get(key).map_or(0, |x| x.as_array().map(Vec::len).or_else(|| x.as_object().map(Map::len)).unwrap_or(0)); let line = match command { "model.check" => { let (e, w) = (v["errors"].as_u64().unwrap_or(0), v["warnings"].as_u64().unwrap_or(0)); if e + w == 0 { "ready to solve, no issues".to_string() } else { format!("{}, {}", plural(e, "error"), plural(w, "warning")) } } "model.activity" => format!( "{}, {}, {}", plural(count("steps") as u64, "step"), plural(count("boundary_conditions") as u64, "boundary condition"), plural(count("loads") as u64, "load") ), "geometry.bodies" => match v.as_array().map(Vec::len) { Some(0) => "no bodies (open a design first)".to_string(), Some(n) => plural(n as u64, "body").replace("bodys", "bodies"), None => scalars(v), }, "model.info" => { let elements: u64 = v["elements"].as_object().into_iter().flatten().filter_map(|(_, n)| n.as_u64()).sum(); format!( "{}, {}, units {}", plural(v["nodes"].as_u64().unwrap_or(0), "node"), plural(elements, "element"), v["units"].as_str().unwrap_or("—") ) } "interaction.contact.find" => { format!("{} among {}", plural(count("touching") as u64, "touching pair"), plural(v["parts"].as_u64().unwrap_or(0), "part")) } "results.reactions" => match serde_json::from_value::>(v["total"]["force"].clone()) { Ok(f) => format!("total reaction force ({})", with_unit(components(&f, 0.0), Quantity::Force.unit(units).as_deref())), Err(_) => scalars(v), }, _ => scalars(v), }; if line.is_empty() { "done".into() } else { one_line(&line) } } /// The status line after `command` changed the model or the display, written for the person /// rather than from the command's label: "Showing Load deformed ×2", "Mode 2: 1637 Hz", "Added /// load Push". Commands without a sentence of their own say "