results: export probe and reaction results to CSV (results.export.csv, #92)

results.export.csv runs solve.probe or results.reactions and writes their reply as an RFC 4180
CSV: a row per node (node, x, y, z and the quantities the step has, columns it does not have
left out) or per set with the total and the applied load; numbers in shortest round-trip form,
so they read back bit for bit. A stale result is refused. The results panel has Export CSV…
beside Reaction sums and under the probed node: the save picker, or the form without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 11:07:46 -04:00
1 parent 5112855b83
commit d1557a87fe
8 files changed
+338 -10

No files matched your search

+29 -7
View File
@@ -255,6 +255,7 @@ fn probe(app: &ModelApp, plot: &Plot, painter: &egui::Painter, rect: Rect, t: &T
/// Every control runs a command or a view setting.
pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens) {
let mut run: Vec<(&'static str, Value)> = Vec::new();
let mut export: Option<Value> = None;
let steps: Vec<(String, bool)> = app.session.result_steps().map(|(n, c)| (n.to_string(), c)).collect();
egui::Grid::new("fc_results_grid").num_columns(2).spacing(vec2(8.0, 4.0)).show(ui, |ui| {
ui.label("Step");
@@ -385,26 +386,37 @@ pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens)
}
if matches!(plot.result.kind(), Kind::Static(_)) {
ui.separator();
if ui.button("Reaction sums").on_hover_text("Reaction force and moment per boundary-condition set (results.reactions)").clicked() {
run.push(("results.reactions", json!({"step": plot.view.step})));
}
ui.horizontal(|ui| {
if ui.button("Reaction sums").on_hover_text("Reaction force and moment per boundary-condition set (results.reactions)").clicked() {
run.push(("results.reactions", json!({"step": plot.view.step})));
}
if ui.button("Export CSV…").on_hover_text("Write the reaction sums to a CSV file (results.export.csv)").clicked() {
export = Some(json!({"what": "reactions", "step": plot.view.step}));
}
});
if let Some(r) = app.reactions.as_ref().filter(|r| r["step"] == plot.view.step.as_str()) {
reactions_table(ui, r, plot, t);
}
}
ui.separator();
probed(app, plot, ui, t);
if let Some(p) = probed(app, plot, ui, t) {
export = Some(p);
}
if let Some(p) = export {
app.export_csv(p);
}
for (id, params) in run {
// The reaction sums land in `app.reactions` (ModelApp::run_form).
let _ = app.run(id, params);
}
}
/// The probed node's values, one row each, or how to probe one.
fn probed(app: &ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens) {
/// The probed node's values, one row each, or how to probe one; the export's parameters when
/// its Export CSV… is clicked.
fn probed(app: &ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens) -> Option<Value> {
let Some(p) = app.probe.as_ref().filter(|p| p.step == plot.view.step && p.mode == plot.pick.mode && p.frame == plot.pick.frame) else {
ui.label(RichText::new("Click the model to probe a node").small().color(t.text_dim));
return;
return None;
};
ui.label(RichText::new(format!("Node {}", p.node)).strong());
egui::Grid::new("fc_results_probe").num_columns(2).spacing(vec2(8.0, 2.0)).show(ui, |ui| {
@@ -414,6 +426,16 @@ fn probed(app: &ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens) {
ui.end_row();
}
});
ui.button("Export CSV…").on_hover_text("Write the node's results to a CSV file (results.export.csv)").clicked().then(|| {
let mut params = json!({"what": "probe", "step": p.step, "nodes": [p.node]});
if matches!(plot.result.kind(), Kind::Frequency(_)) {
params["mode"] = json!(p.mode);
}
if let Some(f) = p.frame {
params["frame"] = json!(f);
}
params
})
}
/// An explicit step: a frame slider (the deformed shape and contours follow it) and the energy