//! What a model looks like on screen. //! //! Finite elements: the faces nobody shares (the skin of the solid elements, and every shell), //! the element edges on those faces, line elements and nodes. Faces shared by two solid elements //! are inside the mesh and are left out, so a model of a million elements draws its surface, not //! its interior. Higher-order elements (tet10, hex20, tri6, quad8) draw through their corner //! nodes. Every element primitive keeps its node labels, so results (displacements, contours) //! can be drawn on it ([`DisplayMesh::displaced`]). //! //! Geometry: triangulated boundary surfaces (tessellated bodies, imported surfaces) added with //! [`DisplayMesh::add_surface`], drawn with the edges between their faces. //! //! The output is deterministic: elements come out in label order, edges sorted. use fieldcraft_model::{ElementKind, Model}; use crate::pick::{Hit, first_hit}; use crate::{Aabb3, Vec3}; /// One drawn triangle of an element face. #[derive(Clone, Copy, Debug, PartialEq)] pub struct DisplayTri { pub p: [Vec3; 3], /// The node labels at the corners. pub nodes: [u32; 3], /// The face's unit normal, pointing out of its element (a shell's own orientation). pub normal: Vec3, pub kind: ElementKind, /// The element it is a face of, and the face's number (from 1, as /// [`ElementKind::faces`] numbers them; a shell's is 1, its top side). pub element: u32, pub face: u8, } impl DisplayTri { /// The element face seen looking along `dir`: a shell's top side (1) from in front and its /// bottom side (2) from behind; a solid's face as it is. pub fn seen_face(&self, dir: Vec3) -> u8 { if self.kind.dimension() == 2 && self.normal.dot(dir) > 0.0 { 2 } else { self.face } } } /// An element edge. #[derive(Clone, Copy, Debug, PartialEq)] pub struct DisplayEdge { pub p: [Vec3; 2], pub nodes: [u32; 2], } /// A line element. #[derive(Clone, Copy, Debug, PartialEq)] pub struct DisplayLine { pub a: Vec3, pub b: Vec3, pub nodes: [u32; 2], pub kind: ElementKind, pub element: u32, } /// A node. #[derive(Clone, Copy, Debug, PartialEq)] pub struct DisplayNode { pub label: u32, pub p: Vec3, } /// A triangle of a geometry surface. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SurfaceTri { pub p: [Vec3; 3], pub normal: Vec3, /// Which surface (body) it belongs to: the order [`DisplayMesh::add_surface`] got them in. pub body: u32, /// The face of that surface it lies on (its `tri_face` tag; `u32::MAX` when it has none). pub face: u32, } /// Display data for a model and its geometry. #[derive(Clone, Debug, Default, PartialEq)] pub struct DisplayMesh { /// Visible element faces as triangles (quads are split in two). pub tris: Vec, /// Element edges on the visible faces, each once. pub edges: Vec, /// Line elements (rods, beams). pub lines: Vec, /// Every node, in label order. pub nodes: Vec, /// Geometry surface triangles. pub surface_tris: Vec, /// Geometry edges: where two faces meet, and open boundaries. pub surface_edges: Vec<[Vec3; 2]>, /// Geometry surfaces (bodies) added. pub bodies: u32, pub bounds: Aabb3, } /// The faces of an element as corner indices (0-based into its node list), each wound /// counter-clockwise seen from outside for a positively oriented element. A solid's faces come in /// the order [`ElementKind::faces`] numbers them (with the same corners); a shell draws its top /// side, face 1. fn faces(kind: ElementKind) -> &'static [&'static [usize]] { match kind { ElementKind::Rod2 | ElementKind::Rod3 | ElementKind::Beam2 => &[], ElementKind::Tri3 | ElementKind::Tri6 => &[&[0, 1, 2]], ElementKind::Quad4 | ElementKind::Quad8 => &[&[0, 1, 2, 3]], ElementKind::Tet4 | ElementKind::Tet10 => &[&[0, 2, 1], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]], ElementKind::Wedge6 => &[&[0, 2, 1], &[3, 4, 5], &[0, 1, 4, 3], &[1, 2, 5, 4], &[2, 0, 3, 5]], ElementKind::Hex8 | ElementKind::Hex20 => &[&[0, 3, 2, 1], &[4, 5, 6, 7], &[0, 1, 5, 4], &[1, 2, 6, 5], &[2, 3, 7, 6], &[3, 0, 4, 7]], } } /// Most corners of any element (hex). const MAX_CORNERS: usize = 8; /// A face's identity regardless of winding and starting corner: its sorted node labels, a /// triangle padded with `u32::MAX`. fn face_key(e: &[u32], face: &[usize]) -> Option<[u32; 4]> { let mut k = [u32::MAX; 4]; for (slot, &i) in k.iter_mut().zip(face) { *slot = *e.get(i)?; } k.sort_unstable(); Some(k) } /// Newell's normal of a polygon (robust for non-planar quads); `None` when degenerate. pub(crate) fn polygon_normal(p: &[Vec3]) -> Option { let mut n = Vec3::ZERO; for (i, a) in p.iter().enumerate() { let b = p.get((i + 1) % p.len()).copied().unwrap_or(*a); n = n + Vec3::new((a.y - b.y) * (a.z + b.z), (a.z - b.z) * (a.x + b.x), (a.x - b.x) * (a.y + b.y)); } n.normalized() } pub(crate) fn centre(p: &[Vec3]) -> Vec3 { p.iter().fold(Vec3::ZERO, |s, q| s + *q) * (1.0 / p.len().max(1) as f64) } impl DisplayMesh { pub fn from_model(model: &Model) -> 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 p = Vec3::from(n.xyz); out.nodes.push(DisplayNode { label, p }); out.bounds.add(p); } // Faces two solid elements share are inside the mesh: sort every solid face's key and // keep the ones that occur more than once. let mut keys: Vec<[u32; 4]> = model .elements() .values() .filter(|e| e.kind.dimension() == 3) .flat_map(|e| faces(e.kind).iter().filter_map(|f| face_key(&e.nodes, f))) .collect(); keys.sort_unstable(); let mut inner: Vec<[u32; 4]> = keys.windows(2).filter(|w| w.first() == w.get(1)).filter_map(|w| w.first().copied()).collect(); inner.dedup(); drop(keys); let mut edges: Vec<(u32, u32)> = Vec::new(); for (&label, e) in model.elements() { // Corners: one past the highest face index (line elements: their two ends). let corners = faces(e.kind).iter().flat_map(|f| f.iter()).max().map_or(2, |m| m + 1).min(MAX_CORNERS); let mut at = [Vec3::ZERO; MAX_CORNERS]; let mut found = 0; for (slot, &n) in at.iter_mut().zip(e.nodes.iter().take(corners)) { if let Some(p) = pos(n) { *slot = p; found += 1; } } if found < corners { continue; } let at = at.get(..corners).unwrap_or(&[]); if e.kind.dimension() == 1 { if let (Some(&a), Some(&b), Some(&na), Some(&nb)) = (at.first(), at.get(1), e.nodes.first(), e.nodes.get(1)) { out.lines.push(DisplayLine { a, b, nodes: [na, nb], kind: e.kind, element: label }); } continue; } let solid = e.kind.dimension() == 3; let element_centre = centre(at); for (number, f) in faces(e.kind).iter().enumerate() { let face = u8::try_from(number + 1).unwrap_or(u8::MAX); if solid && face_key(&e.nodes, f).is_some_and(|k| inner.binary_search(&k).is_ok()) { continue; } let p: Vec = f.iter().filter_map(|&i| at.get(i).copied()).collect(); let labels: Vec = f.iter().filter_map(|&i| e.nodes.get(i).copied()).collect(); let Some(mut normal) = polygon_normal(&p) else { continue }; if solid && normal.dot(centre(&p) - element_centre) < 0.0 { normal = -normal; } for k in 1..p.len().saturating_sub(1) { if let (Some(&a), Some(&b), Some(&c), Some(&la), Some(&lb), Some(&lc)) = (p.first(), p.get(k), p.get(k + 1), labels.first(), labels.get(k), labels.get(k + 1)) { out.tris.push(DisplayTri { p: [a, b, c], nodes: [la, lb, lc], normal, kind: e.kind, element: label, face }); } } for (j, &i) in f.iter().enumerate() { let i2 = f.get((j + 1) % f.len()).copied().unwrap_or(i); if let (Some(&a), Some(&b)) = (e.nodes.get(i), e.nodes.get(i2)) { edges.push((a.min(b), a.max(b))); } } } } edges.sort_unstable(); edges.dedup(); out.edges = edges.into_iter().filter_map(|(a, b)| Some(DisplayEdge { p: [pos(a)?, pos(b)?], nodes: [a, b] })).collect(); out } /// Add a triangulated geometry surface: `positions`, counter-clockwise `triangles` seen from /// outside, and the face each triangle lies on. Edges are drawn where triangles of different /// faces meet and along open boundaries; degenerate triangles and out-of-range indices are /// skipped. pub fn add_surface(&mut self, positions: &[[f64; 3]], triangles: &[[u32; 3]], tri_face: &[u32]) { let pos = |i: u32| positions.get(i as usize).map(|p| Vec3::from(*p)).filter(|p| p.is_finite()); // Tessellations may repeat a point for each triangle or face: copies with identical // coordinates are one vertex (the lowest index of the copies). let mut order: Vec = (0..u32::try_from(positions.len()).unwrap_or(u32::MAX)).collect(); let coords = |i: &u32| positions.get(*i as usize).copied().unwrap_or([f64::NAN; 3]); order.sort_by(|a, b| { let (p, q) = (coords(a), coords(b)); p[0].total_cmp(&q[0]).then(p[1].total_cmp(&q[1])).then(p[2].total_cmp(&q[2])).then(a.cmp(b)) }); let mut vertex: Vec = (0..order.len()).map(|i| i as u32).collect(); for run in order.chunk_by(|a, b| coords(a).map(f64::to_bits) == coords(b).map(f64::to_bits)) { let first = run.first().copied().unwrap_or(0); for &i in run { if let Some(v) = vertex.get_mut(i as usize) { *v = first; } } } let vertex = |i: u32| vertex.get(i as usize).copied().unwrap_or(i); // (low vertex, high vertex, face) per triangle side; a side alone, or sides on // different faces, make an edge to draw. let mut sides: Vec<(u32, u32, u32)> = Vec::with_capacity(triangles.len().saturating_mul(3)); for (t, &[i0, i1, i2]) in triangles.iter().enumerate() { let (Some(a), Some(b), Some(c)) = (pos(i0), pos(i1), pos(i2)) else { continue }; let Some(normal) = (b - a).cross(c - a).normalized() else { continue }; for p in [a, b, c] { self.bounds.add(p); } let face = tri_face.get(t).copied().unwrap_or(u32::MAX); self.surface_tris.push(SurfaceTri { p: [a, b, c], normal, body: self.bodies, face }); let [v0, v1, v2] = [vertex(i0), vertex(i1), vertex(i2)]; for (u, v) in [(v0, v1), (v1, v2), (v2, v0)] { sides.push((u.min(v), u.max(v), face)); } } sides.sort_unstable(); for run in sides.chunk_by(|a, b| a.0 == b.0 && a.1 == b.1) { let Some(&(u, v, face)) = run.first() else { continue }; if (run.len() == 1 || run.iter().any(|s| s.2 != face)) && let (Some(a), Some(b)) = (pos(u), pos(v)) { self.surface_edges.push([a, b]); } } self.bodies = self.bodies.saturating_add(1); } /// The colour of a geometry triangle: `single` when there is one body, else body `k`'s /// colour from `palette` (cycling), so the bodies of a design, such as the cells of a /// partitioned part, tell apart. pub fn body_color(&self, tri: &SurfaceTri, single: C, palette: &[C]) -> C { if self.bodies <= 1 || palette.is_empty() { return single; } palette.get(tri.body as usize % palette.len()).copied().unwrap_or(single) } /// Where node `label` is drawn, if it is (nodes are kept in label order). pub fn node_position(&self, label: u32) -> Option { self.nodes.binary_search_by_key(&label, |n| n.label).ok().and_then(|i| self.nodes.get(i)).map(|n| n.p) } /// The element face triangle ([`DisplayMesh::tris`]) a ray (`origin`, `dir`) hits first /// ([`pick::first_hit`]). pub fn hit_element_face(&self, origin: Vec3, dir: Vec3) -> Option { first_hit(origin, dir, self.tris.iter().map(|t| t.p)) } /// The geometry surface triangle ([`DisplayMesh::surface_tris`]) a ray hits first. pub fn hit_surface(&self, origin: Vec3, dir: Vec3) -> Option { first_hit(origin, dir, self.surface_tris.iter().map(|t| t.p)) } /// The node nearest to where a ray (`origin`, `dir`) first hits an element face ahead of /// its origin: the hit face's closest corner, and the hit point. Line elements and geometry /// are not hit. pub fn pick(&self, origin: Vec3, dir: Vec3) -> Option<(u32, Vec3)> { let h = self.hit_element_face(origin, dir)?; let t = self.tris.get(h.index)?; let hit = origin + dir * h.t; let (i, _) = t.p.iter().enumerate().min_by(|a, b| a.1.dist(hit).total_cmp(&b.1.dist(hit)))?; Some((*t.nodes.get(i)?, hit)) } /// The elements moved by `offset(label)` (no offset for a node it has none for): faces, /// edges, line elements and nodes move, face normals follow the deformation. Geometry /// surfaces are left out (they do not deform). pub fn displaced(&self, offset: impl Fn(u32) -> Option) -> DisplayMesh { let at = |p: Vec3, label: u32| p + offset(label).filter(|d| d.is_finite()).unwrap_or(Vec3::ZERO); let tris = self .tris .iter() .map(|t| { let p = [at(t.p[0], t.nodes[0]), at(t.p[1], t.nodes[1]), at(t.p[2], t.nodes[2])]; // The triangle's own normal, on the side its face's normal was. let before = (t.p[1] - t.p[0]).cross(t.p[2] - t.p[0]); let side = if before.dot(t.normal) < 0.0 { -1.0 } else { 1.0 }; let normal = ((p[1] - p[0]).cross(p[2] - p[0]) * side).normalized().unwrap_or(t.normal); DisplayTri { p, normal, ..*t } }) .collect(); let mut out = DisplayMesh { tris, edges: self.edges.iter().map(|e| DisplayEdge { p: [at(e.p[0], e.nodes[0]), at(e.p[1], e.nodes[1])], nodes: e.nodes }).collect(), lines: self.lines.iter().map(|l| DisplayLine { a: at(l.a, l.nodes[0]), b: at(l.b, l.nodes[1]), ..*l }).collect(), nodes: self.nodes.iter().map(|n| DisplayNode { label: n.label, p: at(n.p, n.label) }).collect(), ..Default::default() }; for n in &out.nodes { out.bounds.add(n.p); } out } } #[cfg(test)] mod tests { use super::*; /// `nx × ny × nz` unit hex8 cells. fn block(nx: u32, ny: u32, nz: u32) -> Model { let mut m = Model::default(); let id = |i: u32, j: u32, k: u32| 1 + i + j * (nx + 1) + k * (nx + 1) * (ny + 1); for k in 0..=nz { for j in 0..=ny { for i in 0..=nx { m.set_node(id(i, j, k), [i as f64, j as f64, k as f64]).unwrap(); } } } let mut label = 1; for k in 0..nz { for j in 0..ny { for i in 0..nx { let n = vec![ id(i, j, k), id(i + 1, j, k), id(i + 1, j + 1, k), id(i, j + 1, k), id(i, j, k + 1), id(i + 1, j, k + 1), id(i + 1, j + 1, k + 1), id(i, j + 1, k + 1), ]; m.set_element(label, ElementKind::Hex8, n).unwrap(); label += 1; } } } m } #[test] fn a_block_shows_its_skin_only() { let d = DisplayMesh::from_model(&block(3, 2, 2)); // Skin quads: 2·(3·2 + 3·2 + 2·2) = 32, two triangles each. assert_eq!(d.tris.len(), 64); assert_eq!(d.nodes.len(), 4 * 3 * 3); assert_eq!(d.bounds.max, Vec3::new(3.0, 2.0, 2.0)); // Every normal points out of the block. let c = d.bounds.center(); for t in &d.tris { let fc = (t.p[0] + t.p[1] + t.p[2]) * (1.0 / 3.0); assert!(t.normal.dot(fc - c) > 0.0, "{t:?}"); } // Euler on the skin: V - E + F = 2 (two nodes are inside). let skin_nodes = 4 * 3 * 3 - 2; assert_eq!(skin_nodes as i64 - d.edges.len() as i64 + 32, 2); } #[test] fn shells_lines_and_tets_draw() { let mut m = Model::default(); for (i, p) in [[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [1., 1., 0.]].into_iter().enumerate() { m.set_node(i as u32 + 1, p).unwrap(); } m.set_element(1, ElementKind::Tet4, vec![1, 2, 3, 4]).unwrap(); m.set_element(2, ElementKind::Tri3, vec![2, 5, 3]).unwrap(); m.set_element(3, ElementKind::Beam2, vec![4, 5]).unwrap(); let d = DisplayMesh::from_model(&m); assert_eq!(d.tris.len(), 5); assert_eq!(d.lines.len(), 1); assert_eq!(d.edges.len(), 6 + 2); let tet_normals: Vec = d.tris.iter().filter(|t| t.kind == ElementKind::Tet4).map(|t| t.normal).collect(); assert!(tet_normals.contains(&Vec3::new(0.0, 0.0, -1.0))); assert_eq!(d.lines[0].nodes, [4, 5]); assert!(d.tris.iter().all(|t| t.nodes.iter().zip(t.p).all(|(&l, p)| Vec3::from(m.node(l).unwrap().xyz) == p))); } #[test] fn surfaces_draw_their_face_boundaries() { // A unit square in two faces (one triangle each), plus a degenerate and an out-of-range // triangle, which are skipped. let p = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]]; let mut d = DisplayMesh::default(); d.add_surface(&p, &[[0, 1, 2], [0, 2, 3], [0, 0, 1], [0, 1, 9]], &[0, 1, 1, 1]); assert_eq!(d.surface_tris.len(), 2); // Four open sides and the diagonal between the two faces. assert_eq!(d.surface_edges.len(), 5); let mut one_face = DisplayMesh::default(); one_face.add_surface(&p, &[[0, 1, 2], [0, 2, 3]], &[0, 0]); assert_eq!(one_face.surface_edges.len(), 4); assert_eq!(one_face.bounds.max, Vec3::new(1.0, 1.0, 0.0)); // A triangle soup (each triangle with its own copies of the corners) is welded first. let soup = [p[0], p[1], p[2], p[0], p[2], p[3]]; let mut welded = DisplayMesh::default(); welded.add_surface(&soup, &[[0, 1, 2], [3, 4, 5]], &[0, 0]); assert_eq!(welded.surface_edges.len(), 4); let mut hostile = DisplayMesh::default(); let mut two = DisplayMesh::default(); two.add_surface(&p, &[[0, 1, 2]], &[0]); two.add_surface(&p, &[[0, 2, 3]], &[0]); assert_eq!(two.bodies, 2); let colors: Vec = two.surface_tris.iter().map(|t| two.body_color(t, 0u8, &[7, 8])).collect(); assert_eq!(colors, vec![7, 8]); assert_eq!(one_face.body_color(&one_face.surface_tris[0], 0u8, &[7, 8]), 0); hostile.add_surface(&[[f64::NAN, 0., 0.], [1., 0., 0.], [0., 1., 0.]], &[[0, 1, 2], [u32::MAX, 0, 1]], &[]); assert!(hostile.surface_tris.is_empty() && hostile.surface_edges.is_empty()); } #[test] fn displacing_moves_elements_by_node_and_keeps_normals_outward() { let d = DisplayMesh::from_model(&block(2, 1, 1)); // Stretch along x by 50 %: node labels carry the offset to every primitive. let m = block(2, 1, 1); let moved = d.displaced(|label| m.node(label).map(|n| Vec3::new(n.xyz[0] * 0.5, 0.0, 0.0))); assert_eq!(moved.tris.len(), d.tris.len()); assert_eq!(moved.bounds.max, Vec3::new(3.0, 1.0, 1.0)); assert!(moved.edges.iter().all(|e| e.p[0].x == 1.5 * m.node(e.nodes[0]).unwrap().xyz[0])); let c = moved.bounds.center(); for t in &moved.tris { let fc = (t.p[0] + t.p[1] + t.p[2]) * (1.0 / 3.0); assert!(t.normal.dot(fc - c) > 0.0, "{t:?}"); } // No offset, or a non-finite one, leaves a node where it is. let same = d.displaced(|label| (label == 1).then_some(Vec3::new(f64::NAN, 0.0, 0.0))); assert_eq!(same.tris, d.tris); } #[test] fn picking_finds_the_nearest_corner_of_the_face_hit_first() { let m = block(2, 1, 1); let d = DisplayMesh::from_model(&m); // From above, aimed near the corner (2, 1, 1): the top face is hit, not the bottom. let (label, hit) = d.pick(Vec3::new(1.9, 0.9, 10.0), Vec3::new(0.0, 0.0, -1.0)).unwrap(); assert_eq!(m.node(label).unwrap().xyz, [2.0, 1.0, 1.0]); assert!((hit.z - 1.0).abs() < 1e-12); // From below the bottom face wins. let (label, _) = d.pick(Vec3::new(0.1, 0.1, -10.0), Vec3::new(0.0, 0.0, 1.0)).unwrap(); assert_eq!(m.node(label).unwrap().xyz, [0.0, 0.0, 0.0]); assert_eq!(d.pick(Vec3::new(5.0, 5.0, 10.0), Vec3::new(0.0, 0.0, -1.0)), None); assert_eq!(d.pick(Vec3::new(f64::NAN, 0.0, 0.0), Vec3::new(0.0, 0.0, -1.0)), None); } /// A solid's drawn faces are numbered as the model numbers them, so a picked triangle names /// the element face a set or load refers to. #[test] fn drawn_faces_carry_the_models_face_numbers() { for kind in [ElementKind::Tet4, ElementKind::Tet10, ElementKind::Wedge6, ElementKind::Hex8, ElementKind::Hex20] { let drawn = faces(kind); assert_eq!(drawn.len(), kind.faces().len(), "{kind:?}"); for (d, m) in drawn.iter().zip(kind.faces()) { let corners = d.len(); let (mut a, mut b) = (d.to_vec(), m.get(..corners).unwrap().to_vec()); a.sort_unstable(); b.sort_unstable(); assert_eq!(a, b, "{kind:?}"); } } let m = block(2, 1, 1); let d = DisplayMesh::from_model(&m); for t in &d.tris { let face = m.face_nodes(fieldcraft_model::ElementFace { element: t.element, face: t.face }).unwrap(); assert!(t.nodes.iter().all(|n| face.contains(n)), "{t:?} {face:?}"); } // The ray from +x hits element 2's face 4 (corners 1, 2, 6, 5: x = 2). let h = d.hit_element_face(Vec3::new(10.0, 0.3, 0.6), Vec3::new(-1.0, 0.0, 0.0)).unwrap(); let t = d.tris[h.index]; assert_eq!((t.element, t.face, h.t), (2, 4, 8.0)); } #[test] fn empty_and_deterministic() { assert_eq!(DisplayMesh::from_model(&Model::default()), DisplayMesh::default()); let m = block(2, 2, 1); assert_eq!(DisplayMesh::from_model(&m), DisplayMesh::from_model(&m.clone())); } }