results.deformed: scale 0 draws the undeformed shape

A factor of 0 was refused, so a picture sequence swinging the deformation
out from the undeformed shape had to start at a small factor or switch
`show` off. 0 is now a factor like any other (the view stays deformed), and
the results panel no longer works around the refusal. The MCP show_results
docs say so, beside `deformed: false`.

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 00:15:50 -04:00
1 parent 89651b707c
commit 025c826dd4
4 files changed
+13 -8

No files matched your search

+8 -3
View File
@@ -11,7 +11,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
pub const DEFORMED: CommandSpec = CommandSpec {
id: "results.deformed",
label: "Deformed Shape",
params: r#"{"step": "Load", "show": true, "scale": "auto"} — shows a solved step's deformed shape (`show: false` the undeformed mesh); `scale` is a factor or "auto" (the largest displacement drawn a tenth of the model's size); `step` defaults to the step shown or the first solved one"#,
params: r#"{"step": "Load", "show": true, "scale": "auto"} — shows a solved step's deformed shape (`show: false` the undeformed mesh); `scale` is a factor (0 draws the undeformed shape, so a sequence from 0 swings it out) or "auto" (the largest displacement drawn a tenth of the model's size); `step` defaults to the step shown or the first solved one"#,
kind: CommandKind::Session,
run: deformed,
};
@@ -141,8 +141,9 @@ fn deformed(s: &mut Session, p: &Value) -> Result<Value> {
Some(Value::String(a)) if a == "auto" => Some(None),
Some(v) => Some(Some(
v.as_f64()
.filter(|x| x.is_finite() && *x > 0.0 && *x <= MAX_SCALE)
.ok_or_else(|| EngineError::Params(format!("`scale` must be \"auto\" or a factor above 0 and at most {MAX_SCALE}")))?,
.filter(|x| x.is_finite() && (0.0..=MAX_SCALE).contains(x))
.map(f64::abs)
.ok_or_else(|| EngineError::Params(format!("`scale` must be \"auto\" or a factor from 0 (undeformed) to {MAX_SCALE}")))?,
)),
};
let v = view_of(s, &step)?;
@@ -336,6 +337,9 @@ mod tests {
assert!(r["contour"]["range"]["max"]["value"].as_f64().unwrap() > 0.0, "{r}");
let r = s.execute("results.deformed", &json!({"scale": 2.5})).unwrap();
assert_eq!((r["view"]["deformed"].as_bool(), r["view"]["scale"].as_f64()), (Some(true), Some(2.5)));
// Scale 0: the undeformed shape, still the deformed view (an animation's first picture).
let r = s.execute("results.deformed", &json!({"scale": 0})).unwrap();
assert_eq!((r["view"]["deformed"].as_bool(), r["view"]["scale"].as_f64()), (Some(true), Some(0.0)));
s.execute("results.deformed", &json!({"scale": "auto", "show": false})).unwrap();
assert_eq!(s.results_view.as_ref().map(|v| (v.deformed, v.scale)), Some((false, None)));
// Modes switch to the frequency step (its own first look) and list the frequencies.
@@ -418,6 +422,7 @@ mod tests {
json!({"field": "von_mises", "bands": 0}),
json!({"field": "von_mises", "bands": 25}),
json!({"scale": -1}),
json!({"scale": -0.0001}),
json!({"scale": 1e300}),
json!({"scale": "big"}),
json!({"scale": f64::NAN}),
+2 -2
View File
@@ -210,7 +210,7 @@ pub fn tool_definitions() -> Vec<Value> {
"show_results",
"Show results",
"Choose what the results display shows and read it back: the step, the deformed shape (`deformed`, `scale`: \
a factor or \"auto\"), contours of a nodal field (`field`: displacement, displacement_x/y/z, von_mises, \
a factor, 0 for the undeformed shape, or \"auto\"), contours of a nodal field (`field`: displacement, displacement_x/y/z, von_mises, \
max/mid/min_principal, stress_xx/yy/zz/xy/xz/yz, or none; `bands` 2–24), a frequency step's mode or an \
explicit step's frame. The reply has the contour's range: its smallest and largest value and their nodes \
(field displacement gives the largest displacement). Only `step`: show that step.",
@@ -220,7 +220,7 @@ pub fn tool_definitions() -> Vec<Value> {
"field": string("Contour field, or \"none\""),
"bands": integer("Contour bands, 2 to 24"),
"deformed": {"type": "boolean", "description": "Draw the deformed shape"},
"scale": {"type": ["number", "string"], "description": "Deformation scale factor or \"auto\""},
"scale": {"type": ["number", "string"], "description": "Deformation scale factor (0: undeformed) or \"auto\""},
"mode": integer("Mode of a frequency step, from 1"),
"frame": integer("Field frame of an explicit step, from 0"),
"time": {"type": "number", "description": "Show the explicit step's frame nearest this time"},
+2 -2
View File
@@ -143,10 +143,10 @@ pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens)
}
let mut auto = plot.view.scale.is_none();
if ui.checkbox(&mut auto, "Auto").on_hover_text("Scale the largest displacement to a tenth of the model").changed() {
run.push(("results.deformed", json!({"scale": if auto { json!("auto") } else { json!(plot.scale.max(f64::MIN_POSITIVE)) }})));
run.push(("results.deformed", json!({"scale": if auto { json!("auto") } else { json!(plot.scale) }})));
}
let mut scale = plot.scale;
let drag = egui::DragValue::new(&mut scale).speed(plot.scale.abs().max(1e-6) * 0.01).range(1e-9..=1e12).prefix("× ");
let drag = egui::DragValue::new(&mut scale).speed(plot.scale.abs().max(1e-6) * 0.01).range(0.0..=1e12).prefix("× ");
if ui.add_enabled(plot.view.deformed, drag).changed() {
run.push(("results.deformed", json!({"scale": scale})));
}
+1 -1
View File
@@ -101,7 +101,7 @@ software on the computer.
| `write_deck` | `format` (abaqus, nastran, lsdyna), `path`, `steps?` | `deck.write`. |
| `mesh` | `method`, `params?` | Runs the mesher (`tet`: `mesh.tet.generate`; `planar`: `mesh.planar`; the hex and block meshers as they land) and replies with its result, `model.info` and `mesh.quality`. |
| `solve` | `analysis` (static, frequency, explicit, fluid; heat as it lands), `step?`, `options?` | `solve.<analysis>` with the step and the solver's caps and settings. |
| `show_results` | `step?`, `field?`, `bands?`, `deformed?`, `scale?`, `mode?`, `frame?`, `time?` | The `results.*` commands its arguments ask for; the reply has the contour range (smallest and largest value and their nodes: `field: "displacement"` gives the largest displacement), modes or frames. |
| `show_results` | `step?`, `field?`, `bands?`, `deformed?`, `scale?`, `mode?`, `frame?`, `time?` | The `results.*` commands its arguments ask for (`deformed: false` draws the undeformed mesh; `scale` is a factor, 0 for the undeformed shape so a sequence from 0 swings the deformation out, or `"auto"`); the reply has the contour range (smallest and largest value and their nodes: `field: "displacement"` gives the largest displacement), modes or frames. |
| `probe` | `step?`, `nodes?`, `elements?`, `points?`, `forces?`, `mode?`, `frame?` | `solve.probe`: stored results at nodes, elements (explicit) or points (fluid). |
| `reactions` | `step?`, `sets?`, `about?` | `results.reactions`: reaction force and moment per node set and in total. |
| `screenshot` | `view?`, `camera?`, `source?` (window, model), `width?`, `height?`, `path?` | A PNG image content block and what it shows. Headless: a CPU render framing the model, the geometry and the results display (deformed shape, contour bands). Bridged: the whole app window drawn offscreen (`ui.capture`, the default) or a model render with the app's camera (`ui.render`). `view` turns the camera to a standard view first; `camera` `{yaw, pitch, distance?}` to any orientation, in degrees (yaw 0 looks along +Y, −45 is the iso view's; pitch is the elevation, clamped to ±90) and model units (`ui.camera`; headless, the picture is framed unless `distance` is given). Give one of the two; the orientation is kept for the next picture, so a sequence of `camera` angles orbits smoothly. |