ui: selecting the Mesh node shows the commands and parameters that made the mesh (#343, #371)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 10:17:28 -04:00
1 parent f9c1c3cb24
commit 3a023f3e25
5 files changed
+123 -4

No files matched your search

+11 -1
View File
@@ -899,8 +899,18 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
);
if r.eye {
app.ui.show_mesh = !app.ui.show_mesh;
} else if r.fold || r.clicked {
} else if r.fold {
acts.push(Act::Fold("mesh".into()));
} else if r.clicked {
// A click shows how the mesh was made (#343) and opens the node; again, deselects it.
if app.selected_mesh {
app.selected_mesh = false;
} else {
app.select_mesh();
}
if !open(app, "mesh") {
acts.push(Act::Fold("mesh".into()));
}
}
if open(app, "mesh") {
let r = draw_row(
+60
View File
@@ -21,6 +21,7 @@ pub fn has_context(app: &mut ModelApp) -> bool {
|| app.refused.is_some()
|| app.selected.is_some()
|| app.selected_seed.is_some()
|| app.selected_mesh
|| app.reply.is_some()
|| app.plot().is_some()
}
@@ -59,6 +60,10 @@ pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
seed_card(app, ui, &t, edge);
ui.add_space(10.0);
}
if app.selected_mesh {
mesh_card(app, ui, &t);
ui.add_space(10.0);
}
if let Some(plot) = app.plot() {
let mut close = false;
heading(ui, &t, Level::Title, |ui| {
@@ -370,6 +375,61 @@ fn seed_card(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, edge: fieldcraft
}
}
/// The mesh: its size, then each command that made it with its parameters (in its form's
/// words), with Edit… opening that command's form filled in (#343).
fn mesh_card(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
let key = app.session.revision();
let made = match &app.mesh_made {
Some((k, made)) if *k == key => made.clone(),
_ => {
let model = &app.session.model;
let made: Vec<(String, Value, Vec<(String, String)>)> = app
.mesh_history()
.into_iter()
.map(|(c, p)| {
let rows = p.as_object().map(|o| crate::describe::command_rows(model, &c, o)).unwrap_or_default();
(c, p, rows)
})
.collect();
let made = std::sync::Arc::new(made);
app.mesh_made = Some((key, made.clone()));
made
}
};
let mut deselect = false;
heading(ui, t, Level::Title, |ui| {
ui.label(RichText::new("MESH").font(label_font(ui.ctx(), 12.0)));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
deselect = ui.small_button("×").on_hover_text("Deselect").clicked();
});
});
ui.add_space(4.0);
let (nodes, elements) = (app.session.model.nodes().len(), app.session.model.elements().len());
let size = [("Nodes".to_string(), crate::describe::quantity(nodes as f64)), ("Elements".to_string(), crate::describe::quantity(elements as f64))];
property_grid(ui, t, "fc_mesh_size", &size);
let mut edit = None;
if made.is_empty() && elements > 0 {
ui.add_space(4.0);
ui.label(RichText::new("Not made by a mesher in this model (imported or built by hand)").size(11.5).color(t.text_dim));
}
for (i, (command, params, rows)) in made.iter().enumerate() {
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(RichText::new(crate::command_label(command)).strong());
ui.label(RichText::new(command.as_str()).size(11.0).color(t.text_dim));
});
property_grid(ui, t, &format!("fc_mesh_made_{i}"), rows);
if ui.button("Edit…").on_hover_text(format!("{command}: its form with these parameters")).clicked() {
edit = Some((command.clone(), params.clone()));
}
}
if deselect {
app.selected_mesh = false;
} else if let Some((command, params)) = edit {
app.open_form(&command, params);
}
}
/// Rows of (label, text) as a two-column grid.
fn property_grid(ui: &mut egui::Ui, t: &Tokens, id: &str, rows: &[(String, String)]) {
egui::Grid::new(id).num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
+8 -3
View File
@@ -53,19 +53,24 @@ pub fn rows(model: &Model, kind: Def, name: &str) -> Option<Vec<(String, String)
if kind == Def::Set {
return Some(set_rows(model, name, obj));
}
let form = fieldcraft_engine::find_command(&kind.command("create")).and_then(|spec| spec.form);
Some(command_rows(model, &kind.command("create"), obj))
}
/// Parameters of `command` as (label, text) rows in its form's words and units; fields its form
/// does not show stay readable by their key.
pub fn command_rows(model: &Model, command: &str, obj: &Map<String, Value>) -> Vec<(String, String)> {
let form = fieldcraft_engine::find_command(command).and_then(|spec| spec.form);
let mut out = Vec::new();
let mut used = Vec::new();
if let Some(fields) = form {
object(fields, obj, model.units(), &mut out, &mut used, 0);
}
// What the form does not show stays readable.
for (k, field) in obj {
if k != "name" && !used.iter().any(|u| u == k) {
out.push((sentence(k), crate::context::value_text(field)));
}
}
Some(out)
out
}
/// The seed on `edge` as (label, text) rows: the edge, then `mesh.seed.edge`'s fields.
+5
View File
@@ -1606,6 +1606,11 @@ mod tests {
app.wait_loaded();
assert!(app.dialog.is_none(), "{:?}", app.status);
assert_eq!(app.session.model.element_counts(), counts, "replaced, not stacked");
// The Mesh node shows the run that made it, once though it ran twice (#343).
let made = app.mesh_history();
assert_eq!(made.len(), 1, "{made:?}");
assert_eq!(made[0].0, "mesh.tet.generate");
assert_eq!(made[0].1["order"], "linear");
// A plate with a hole is not one mapped block: the form stays with the reason.
app.start("mesh.hex.mapped").unwrap();
+39
View File
@@ -451,6 +451,9 @@ pub struct ModelApp {
/// The edge seed selected in the browser (by its edge), its properties on the right; never
/// together with [`ModelApp::selected`].
pub selected_seed: Option<fieldcraft_model::GeometryRef>,
/// The Mesh node is selected in the browser: how the current mesh was made shows on the
/// right ([`ModelApp::mesh_history`]); never together with a definition or a seed.
pub selected_mesh: bool,
/// What is picked in the viewport.
pub pick: PickState,
/// Set while [`ModelApp::capture`] draws the window offscreen: no pointer there is the
@@ -468,6 +471,8 @@ pub struct ModelApp {
element_counts: Option<(u64, Vec<(fieldcraft_model::ElementKind, usize)>)>,
/// The selected definition's property rows and what they were read for.
properties: Option<((u64, browser::Def, String), std::sync::Arc<Vec<(String, String)>>)>,
/// [`ModelApp::mesh_history`] with each command's rows, by model revision.
mesh_made: Option<(u64, std::sync::Arc<Vec<(String, Value, Vec<(String, String)>)>>)>,
/// The search's query and its results.
search: Option<(String, std::sync::Arc<Vec<palette::Hit>>)>,
}
@@ -536,6 +541,7 @@ impl ModelApp {
tree_open: ["mesh", "results"].into_iter().map(str::to_string).collect(),
selected: None,
selected_seed: None,
selected_mesh: false,
pick: PickState::default(),
capturing: false,
nav: NavMode::default(),
@@ -545,6 +551,7 @@ impl ModelApp {
element_counts: None,
search: None,
properties: None,
mesh_made: None,
}
}
@@ -1372,14 +1379,45 @@ impl ModelApp {
pub fn select_definition(&mut self, selected: Option<(browser::Def, String)>) {
if selected.is_some() {
self.selected_seed = None;
self.selected_mesh = false;
}
self.selected = selected;
}
/// Select the Mesh node in the browser: how the current mesh was made, on the right.
pub fn select_mesh(&mut self) {
self.selected = None;
self.selected_seed = None;
self.selected_mesh = true;
}
/// The meshing commands that made the current mesh with their parameters, oldest first: from
/// the journal, the latest run for each body that still has elements (a remesh replaces an
/// earlier run, a mirror adds its image). Empty for a mesh no mesher made (imported, built
/// node by node).
pub fn mesh_history(&self) -> Vec<(String, Value)> {
let mut covered: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut out = Vec::new();
for (command, params) in self.session.journal.iter().rev() {
let Some((_, targets)) = fieldcraft_engine::MESHERS.iter().find(|(m, _)| m == command) else { continue };
let Ok(bodies) = targets(&self.session, params) else { continue };
let fresh: Vec<String> =
bodies.into_iter().filter(|b| !covered.contains(b) && !self.session.model.body_mesh(b).elements.is_empty()).collect();
if fresh.is_empty() {
continue;
}
covered.extend(fresh);
out.push((command.clone(), params.clone()));
}
out.reverse();
out
}
/// Select the seed on `edge` in the browser: its properties on the right, its edge picked in
/// the viewport.
pub fn select_seed(&mut self, edge: fieldcraft_model::GeometryRef) {
self.selected = None;
self.selected_mesh = false;
self.select([selection::Item::Geometry(edge.clone())], selection::Mode::Replace);
self.selected_seed = Some(edge);
}
@@ -1538,6 +1576,7 @@ impl ModelApp {
if self.framed_document.is_some() {
self.selected = None;
self.selected_seed = None;
self.selected_mesh = false;
self.reply = None;
// Picks are by name, and the names of another document are other things.
if !self.pick.selection.is_empty() {