mesh.quality: a one-line summary, shown in the status bar

Quality from the ribbon printed its whole JSON report into the status
bar, cut off after the first metric's histogram, so a user saw braces
and no answer. The report now carries a `summary` line ("30676 elements,
all valid; scaled Jacobian min 0.305, mean 0.963", with the invalid and
undetermined counts when there are any), and the status bar shows a
reply's summary instead of its JSON whenever it has one. The CLI and MCP
get the same line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 08:46:50 -04:00
1 parent e4e3c9a699
commit 4445be0b48
2 files changed
+54

No files matched your search

+39
View File
@@ -529,6 +529,7 @@ fn quality_report(s: &mut Session, p: &Value) -> Result<Value> {
})
.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<Value> {
}))
}
/// 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<Cow<'a, SurfaceMesh>> {
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() {
+15
View File
@@ -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());