diff --git a/crates/engine/src/cmd/mesh.rs b/crates/engine/src/cmd/mesh.rs index 3b25879..d97fab8 100644 --- a/crates/engine/src/cmd/mesh.rs +++ b/crates/engine/src/cmd/mesh.rs @@ -529,6 +529,7 @@ fn quality_report(s: &mut Session, p: &Value) -> Result { }) .collect(); Ok(json!({ + "summary": quality_summary(&r), "measured": r.measured, "skipped": skipped, "valid": r.valid, @@ -540,6 +541,29 @@ fn quality_report(s: &mut Session, p: &Value) -> Result { })) } +/// A quality report in a line, for the status bar and the CLI: the elements measured, how many +/// are invalid or undetermined, and the scaled Jacobian's minimum and mean. +fn quality_summary(r: &QualityReport) -> String { + if r.measured == 0 { + return "no elements to measure".into(); + } + let mut text = format!("{} elements", r.measured); + if r.invalid.is_empty() && r.undetermined.is_empty() { + text.push_str(", all valid"); + } + if !r.invalid.is_empty() { + text.push_str(&format!(", {} invalid", r.invalid.len())); + } + if !r.undetermined.is_empty() { + text.push_str(&format!(", {} undetermined", r.undetermined.len())); + } + let jacobian = r.metrics.iter().find(|m| m.metric == quality::Metric::ScaledJacobian && m.count > 0); + if let Some((min, mean)) = jacobian.and_then(|m| m.mean.map(|mean| (m.min, mean))) { + text.push_str(&format!("; scaled Jacobian min {min:.3}, mean {mean:.3}")); + } + text +} + /// The surface named by `body` (tessellated at `tolerance`) or `surface` (an imported index). fn surface_input<'a>(s: &'a Session, p: &Value) -> Result> { match (p.get("body"), p.get("surface")) { @@ -763,6 +787,7 @@ mod tests { // The certified quality report agrees. let q = s.execute("mesh.quality", &json!({})).unwrap(); assert_eq!(q["valid"].as_u64().unwrap() as usize, elements); + assert!(q["summary"].as_str().unwrap().starts_with(&format!("{elements} elements, all valid; scaled Jacobian min ")), "{q}"); // A second run appends after the existing labels. let again = s.execute("mesh.tet.generate", &json!({"body": "Body1", "tolerance": 0.5})).unwrap(); assert_eq!(again["first_element"].as_u64().unwrap() as usize, elements + 1); @@ -817,6 +842,20 @@ mod tests { assert!(e.starts_with("body `Box"), "{e}"); } + /// The quality report's one-line summary counts the inverted elements. + #[test] + fn quality_summary_counts_invalid_elements() { + let mut s = Session::default(); + assert_eq!(s.execute("mesh.quality", &json!({})).unwrap()["summary"], "no elements to measure"); + for (label, xyz) in [(1, [0, 0, 0]), (2, [1, 0, 0]), (3, [0, 1, 0]), (4, [0, 0, 1])] { + s.execute("mesh.node.add", &json!({"label": label, "xyz": xyz})).unwrap(); + } + s.execute("mesh.element.add", &json!({"label": 1, "kind": "tet4", "nodes": [1, 2, 3, 4]})).unwrap(); + s.execute("mesh.element.add", &json!({"label": 2, "kind": "tet4", "nodes": [1, 3, 2, 4]})).unwrap(); + let q = s.execute("mesh.quality", &json!({})).unwrap(); + assert!(q["summary"].as_str().unwrap().starts_with("2 elements, 1 invalid; scaled Jacobian min -"), "{q}"); + } + /// The session's caps are the defaults of `max_steiner` and `max_tets`; a parameter overrides them. #[test] fn tet_generate_honours_the_session_caps() { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 4c0a55b..c506f4a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -438,6 +438,8 @@ impl ModelApp { } match &r { Ok(v) if v.is_null() || changed || display => self.set_status(format!("{label} done"), false), + // A reply with a `summary` line shows it instead of its JSON. + Ok(v) if let Some(summary) = v.get("summary").and_then(Value::as_str) => self.set_status(format!("{label}: {summary}"), false), Ok(v) => self.set_status(format!("{label}: {v}"), false), Err(e) => self.set_status(format!("{label}: {e}"), true), } @@ -1227,6 +1229,19 @@ mod tests { assert_eq!(app.ui.tab, Tab::Properties); } + /// Mesh Quality reads as a sentence in the status bar, not as its JSON report. + #[test] + fn mesh_quality_reports_in_a_line() { + let mut app = ModelApp::new(Session::default()); + app.run("mesh.quality", json!({})).unwrap(); + assert_eq!(app.status.as_ref().map(|s| s.text.as_str()), Some("Mesh Quality: no elements to measure")); + app.run_script(&fieldcraft_engine::sample::script()).unwrap(); + app.run("mesh.quality", json!({})).unwrap(); + let status = app.status.clone().unwrap(); + assert!(status.text.starts_with("Mesh Quality: 226 elements, all valid; scaled Jacobian min "), "{}", status.text); + assert!(!status.error && !status.text.contains('{')); + } + #[test] fn starting_a_command_opens_its_form_and_running_it_closes_the_form() { let mut app = ModelApp::new(Session::default());