forked from 1b-agent/fieldcraft-upstream
engine: result units, per-step staleness, step symbols, true-scale nonlinear results
- Quantity names the unit of every number the results show from the model's unit system (mm, MPa, N, N·mm, mJ, mW, °C or K); Field::title puts it in the legend title, and calls a mode shape normalised (#321). - A solved step's result stays current through edits that leave what it reads alone (Model::step_inputs: other steps and what only they use), and comes back current on undo (#318). - Session::step_glyphs: the symbols of what a solved step used, for drawing over its results; no load arrows on a frequency step's modes (#327). - Nonlinear static steps open deformed at true scale like explicit ones, and their automatic scale is 1 (auto_scale, shared by the viewport and the headless pictures) (#326). - results.reactions with an empty sets list sums the step's boundary-condition sets, as its form says (#328). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
a09e3c856b
commit
0966dfa2a6
9 files changed
+371
-42
No files matched your search
@@ -63,7 +63,7 @@ pub const FRAME: CommandSpec = CommandSpec {
|
||||
pub const REACTIONS: CommandSpec = CommandSpec {
|
||||
id: "results.reactions",
|
||||
label: "Reaction Sums",
|
||||
params: r#"{"step": "Load", "sets": ["root"], "about": [0, 0, 0]} — reaction force and moment (about the point) summed over each node set, and over the whole model; `sets` defaults to the sets the step's boundary conditions hold"#,
|
||||
params: r#"{"step": "Load", "sets": ["root"], "about": [0, 0, 0]} — reaction force and moment (about the point) summed over each node set, and over the whole model; `sets` (none or empty) defaults to the sets the step's boundary conditions hold"#,
|
||||
form: Some(super::forms::REACTIONS),
|
||||
kind: CommandKind::Session,
|
||||
requires: &[Requirement::Result],
|
||||
@@ -281,7 +281,8 @@ fn reactions(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"step `{step}` is not a static step: reactions are kept for static steps (explicit steps keep the constraint force history, solve.history)"
|
||||
)));
|
||||
};
|
||||
let sets: Vec<String> = match p.get("sets") {
|
||||
// No sets (or an empty list, a form with none ticked): the step's boundary-condition sets.
|
||||
let sets: Vec<String> = match p.get("sets").filter(|v| v.as_array().is_none_or(|a| !a.is_empty())) {
|
||||
Some(v) => {
|
||||
let list = v
|
||||
.as_array()
|
||||
@@ -369,6 +370,13 @@ mod tests {
|
||||
assert!(s.execute("results.contour", &json!({"field": "von_mises"})).is_err());
|
||||
s.execute("results.contour", &json!({"field": "displacement", "bands": 6})).unwrap();
|
||||
s.execute("results.animate", &json!({})).unwrap();
|
||||
// Changing the field keeps the deformation, its scale and the animation (#332).
|
||||
s.execute("results.deformed", &json!({"scale": 3})).unwrap();
|
||||
let r = s.execute("results.contour", &json!({"step": "Modes", "field": "displacement_z"})).unwrap();
|
||||
assert_eq!(
|
||||
(r["view"]["deformed"].as_bool(), r["view"]["scale"].as_f64(), r["view"]["animate"].as_bool()),
|
||||
(Some(true), Some(3.0), Some(true))
|
||||
);
|
||||
assert!(s.results_view.as_ref().is_some_and(|v| v.animate && v.bands == 6));
|
||||
s.execute("results.animate", &json!({"on": false})).unwrap();
|
||||
assert!(s.results_view.as_ref().is_some_and(|v| !v.animate));
|
||||
@@ -398,6 +406,8 @@ mod tests {
|
||||
let r = s.execute("results.reactions", &json!({"sets": ["root", "tip"], "about": [10, 0, 0]})).unwrap();
|
||||
assert!(r["sets"][0]["moment"][1].as_f64().unwrap().abs() < 1e-6, "{r}");
|
||||
assert_eq!(r["sets"][1]["force"][2].as_f64(), Some(0.0));
|
||||
let empty = s.execute("results.reactions", &json!({"sets": []})).unwrap();
|
||||
assert_eq!(empty["sets"][0]["set"], "root", "an empty list sums the step's boundary-condition sets: {empty}");
|
||||
assert!(s.execute("results.reactions", &json!({"step": "Modes"})).is_err());
|
||||
assert!(s.execute("results.reactions", &json!({"sets": ["nope"]})).is_err());
|
||||
}
|
||||
|
||||
@@ -1397,8 +1397,8 @@ pub(crate) mod tests {
|
||||
s.execute("model.step.create", &json!({"name": "Long", "procedure": long})).unwrap();
|
||||
assert!(s.execute("solve.explicit", &json!({"step": "Long"})).unwrap_err().to_string().contains("increments"));
|
||||
assert!(s.execute("model.set.delete", &json!({"name": "all_nodes"})).unwrap_err().to_string().contains("step `Impact`"));
|
||||
// Defining steps edited the model, so the impact result is stale.
|
||||
assert_eq!(s.solved_steps().count(), 0);
|
||||
// Defining other steps left what the impact step reads alone: its result is current.
|
||||
assert_eq!(s.solved_steps().collect::<Vec<_>>(), ["Impact"]);
|
||||
}
|
||||
|
||||
/// A mass on a spring and a dashpot, defined, solved and written through commands; editing
|
||||
|
||||
@@ -113,16 +113,31 @@ pub(crate) fn forget_missing(s: &mut Session) {
|
||||
impl Session {
|
||||
/// The analysis setup's symbols, without the definitions the view hides.
|
||||
pub fn glyphs(&self) -> fieldcraft_render::glyphs::Glyphs {
|
||||
self.glyphs_of(None)
|
||||
}
|
||||
|
||||
/// The symbols of what solved step `step` used, over its results: its own boundary
|
||||
/// conditions and loads (none for a frequency step, which has no loads) and its own initial
|
||||
/// velocities and rigid walls; the interactions as always. Without the view's hidden items.
|
||||
pub fn step_glyphs(&self, step: &str) -> fieldcraft_render::glyphs::Glyphs {
|
||||
self.glyphs_of(Some(step))
|
||||
}
|
||||
|
||||
fn glyphs_of(&self, step: Option<&str>) -> fieldcraft_render::glyphs::Glyphs {
|
||||
use fieldcraft_model::Procedure;
|
||||
use fieldcraft_render::glyphs::Definition;
|
||||
let step = step.map(|name| self.model.step(name));
|
||||
let frequency = matches!(step, Some(Some(s)) if matches!(s.procedure, Procedure::Frequency { .. }));
|
||||
fieldcraft_render::glyphs::Glyphs::from_model_hiding(&self.model, &|definition, name| {
|
||||
let kind = match definition {
|
||||
Definition::BoundaryCondition => ItemKind::BoundaryCondition,
|
||||
Definition::Load => ItemKind::Load,
|
||||
Definition::ReferencePoint => ItemKind::ReferencePoint,
|
||||
Definition::Coupling => ItemKind::Coupling,
|
||||
Definition::Contact | Definition::Tie | Definition::Step => return false,
|
||||
let (kind, used) = match definition {
|
||||
Definition::BoundaryCondition => (ItemKind::BoundaryCondition, step.map(|s| s.is_some_and(|s| s.boundary_conditions.contains(name)))),
|
||||
Definition::Load => (ItemKind::Load, step.map(|s| !frequency && s.is_some_and(|s| s.loads.contains(name)))),
|
||||
Definition::ReferencePoint => (ItemKind::ReferencePoint, None),
|
||||
Definition::Coupling => (ItemKind::Coupling, None),
|
||||
Definition::Step => return step.is_some_and(|s| s.is_none_or(|s| s.name != name)),
|
||||
Definition::Contact | Definition::Tie => return false,
|
||||
};
|
||||
self.view.is_hidden(kind, name)
|
||||
used == Some(false) || self.view.is_hidden(kind, name)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user