diff --git a/crates/engine/src/cmd/solve.rs b/crates/engine/src/cmd/solve.rs index 7118a8b..e7be9e7 100644 --- a/crates/engine/src/cmd/solve.rs +++ b/crates/engine/src/cmd/solve.rs @@ -207,6 +207,19 @@ fn run_all(s: &mut Session, _: &Value) -> Result { Ok(json!({"steps": done})) } +/// A sum of nodal values with its round-off set to 0: a component below a billionth of the sum +/// of the terms' magnitudes is what cancelling left (a balanced load sums to ~1e-11, not 0). +fn settled(total: [f64; 3], terms: &[[f64; 3]]) -> [f64; 3] { + let mut out = total; + for (k, c) in out.iter_mut().enumerate() { + let scale: f64 = terms.iter().filter_map(|t| t.get(k)).map(|x| x.abs()).filter(|x| x.is_finite()).sum(); + if c.abs() <= 1e-9 * scale { + *c = 0.0; + } + } + out +} + fn largest(values: impl Iterator) -> Value { extreme(values, |v, b| v > b) } @@ -228,8 +241,8 @@ fn run_static(s: &mut Session, p: &Value) -> Result { "factor_entries": r.stats.factor_entries, "max_displacement": largest(r.nodes.iter().zip(&r.displacement).map(|(l, u)| (*l, crate::results::magnitude(*u)))), "max_von_mises": largest(r.nodes.iter().zip(&r.von_mises).map(|(l, v)| (*l, *v))), - "applied_total": r.applied_total, - "reaction_total": r.reaction_total, + "applied_total": settled(r.applied_total, &r.reaction), + "reaction_total": settled(r.reaction_total, &r.reaction), "reference_points": r.reference_points.iter().map(|p| json!({ "name": p.name, "label": p.label, @@ -272,8 +285,8 @@ fn run_nonlinear(s: &mut Session, p: &Value) -> Result { "max_displacement": largest(r.nodes.iter().zip(&r.displacement).map(|(l, u)| (*l, crate::results::magnitude(*u)))), "max_von_mises": largest(r.nodes.iter().zip(&r.von_mises).map(|(l, v)| (*l, *v))), "max_plastic_strain": largest(r.elements.iter().flat_map(|e| e.plastic_strain.iter().map(|v| (e.label, *v)))), - "applied_total": r.applied_total, - "reaction_total": r.reaction_total, + "applied_total": settled(r.applied_total, &r.reaction), + "reaction_total": settled(r.reaction_total, &r.reaction), "reference_points": r.reference_points.iter().map(|p| json!({ "name": p.name, "label": p.label, diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index e8bc47d..6595cc8 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -141,6 +141,11 @@ impl Session { }) } + /// The elements of the bodies the view hides: hiding a body hides its mesh and its results. + pub fn hidden_elements(&self) -> std::collections::BTreeSet { + self.view.hidden.iter().filter(|i| i.kind == ItemKind::Body).flat_map(|i| self.model.body_elements(&i.name)).collect() + } + /// Whether the view shows item `name` of `kind`. pub fn shows(&self, kind: ItemKind, name: &str) -> bool { !self.view.is_hidden(kind, name) diff --git a/crates/engine/src/results.rs b/crates/engine/src/results.rs index 223b085..e7957ab 100644 --- a/crates/engine/src/results.rs +++ b/crates/engine/src/results.rs @@ -621,6 +621,11 @@ pub(crate) fn inputs_fingerprint(model: &Model, step: &str) -> Option { Some(h.0.finish()) } +/// The principal values of a stress (xx, yy, zz, xy, xz, yz), largest first. +pub fn principal(t: &[f64; 6]) -> [f64; 3] { + fieldcraft_fem::principal(t) +} + pub fn magnitude(v: [f64; 3]) -> f64 { (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt() } diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index 9eecb8b..1b9a729 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -118,7 +118,7 @@ pub struct Shown { /// The model's display data with the open design's bodies and the imported surfaces. pub fn display(s: &Session) -> (DisplayMesh, Vec) { - let mut d = DisplayMesh::from_model(&s.model); + let mut d = DisplayMesh::from_model_hiding(&s.model, &s.hidden_elements()); let mut failed = Vec::new(); for body in s.design.iter().flat_map(|d| &d.bodies).filter(|b| s.shows(fieldcraft_io::ItemKind::Body, &b.name)) { let mesh = body.measure().and_then(|m| body.tessellate(display_tolerance(&Aabb3 { min: m.bbox_min.into(), max: m.bbox_max.into() }))); diff --git a/crates/render/src/display.rs b/crates/render/src/display.rs index 3f903c1..46439cc 100644 --- a/crates/render/src/display.rs +++ b/crates/render/src/display.rs @@ -175,9 +175,25 @@ pub(crate) fn centre(p: &[Vec3]) -> Vec3 { impl DisplayMesh { pub fn from_model(model: &Model) -> DisplayMesh { + DisplayMesh::from_model_hiding(model, &std::collections::BTreeSet::new()) + } + + /// [`DisplayMesh::from_model`] without the `hidden` elements (a hidden body's): their faces + /// are not drawn, a face they share with a shown element is drawn as its boundary, and nodes + /// only they use are left out (so nothing picks or frames them). + pub fn from_model_hiding(model: &Model, hidden: &std::collections::BTreeSet) -> DisplayMesh { let mut out = DisplayMesh::default(); let pos = |label: u32| model.node(label).map(|n| Vec3::from(n.xyz)); - for (&label, n) in model.nodes() { + let shown = |label: &u32| !hidden.contains(label); + // With elements hidden, the nodes only they use go too. + let hidden_nodes: std::collections::BTreeSet = if hidden.is_empty() { + Default::default() + } else { + let used: std::collections::BTreeSet = + model.elements().iter().filter(|(l, _)| shown(l)).flat_map(|(_, e)| e.nodes.iter().copied()).collect(); + hidden.iter().filter_map(|l| model.element(*l)).flat_map(|e| e.nodes.iter().copied()).filter(|n| !used.contains(n)).collect() + }; + for (&label, n) in model.nodes().iter().filter(|(l, _)| !hidden_nodes.contains(l)) { let p = Vec3::from(n.xyz); out.nodes.push(DisplayNode { label, p }); out.bounds.add(p); @@ -186,7 +202,9 @@ impl DisplayMesh { // keep the ones that occur more than once. let mut keys: Vec<[u32; 4]> = model .elements() - .values() + .iter() + .filter(|(l, _)| shown(l)) + .map(|(_, e)| e) .filter(|e| e.kind.dimension() == 3) .flat_map(|e| drawn_faces(e.kind).iter().filter_map(|f| face_key(&e.nodes, f))) .collect(); @@ -197,7 +215,7 @@ impl DisplayMesh { // Element edges on geometry edges (by their corner nodes): the mesh's feature lines. let classification = model.classification(); let mut features: Vec<(u32, u32)> = Vec::new(); - for (&element, tags) in classification.elements() { + for (&element, tags) in classification.elements().iter().filter(|(l, _)| shown(l)) { let Some(e) = model.element(element) else { continue }; for (&edge, &tag) in &tags.edges { if classification.entity(tag).is_some_and(|g| g.entity.dimension() == 1) @@ -212,7 +230,7 @@ impl DisplayMesh { features.dedup(); // (segment's nodes, the element edge's corners), each sorted. let mut edges: Vec<((u32, u32), (u32, u32))> = Vec::new(); - for (&label, e) in model.elements() { + for (&label, e) in model.elements().iter().filter(|(l, _)| shown(l)) { if e.kind.dimension() == 1 { if let (Some(&na), Some(&nb)) = (e.nodes.first(), e.nodes.get(1)) && let (Some(a), Some(b)) = (pos(na), pos(nb)) @@ -460,6 +478,17 @@ mod tests { assert_eq!(skin_nodes as i64 - d.edges.len() as i64 + 32, 2); } + /// Hidden elements (a hidden body's) are not drawn; the face they shared with a shown one + /// is its boundary now, and nodes only they used are gone (#365). + #[test] + fn hidden_elements_are_left_out() { + let m = block(2, 1, 1); + let d = DisplayMesh::from_model_hiding(&m, &[2].into_iter().collect()); + assert_eq!(d.tris.len(), 12, "one cube's six faces"); + assert_eq!(d.nodes.len(), 8); + assert_eq!(d.bounds.max, Vec3::new(1.0, 1.0, 1.0)); + } + #[test] fn shells_lines_and_tets_draw() { let mut m = Model::default();