Files
OneCAD-Simulation/crates/render/src/display.rs
T
jyvikoandClaude Opus 5.5 91e8467e4f engine, render: hidden bodies hide their mesh and results; solve totals without round-off
- DisplayMesh::from_model_hiding leaves out a hidden body's elements (and the
  nodes only they use); Session::hidden_elements names them, so the viewport,
  probing and headless pictures skip hidden bodies (#365).
- A solve summary's applied and reaction totals set the round-off of
  cancelling sums to 0 (#319).
- results::principal for the probe's principal stresses (#329).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 09:36:54 -04:00

684 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 all their nodes:
//! a face is cut into triangles whose vertices are its corners and mid-side nodes, and each
//! edge is drawn as its two halves through its mid-side node, so the faces, the edges, the
//! feature lines and a highlight all share the same vertices and meet exactly at any zoom.
//! 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 }
}
}
/// A drawn segment of an element edge: the whole edge of a linear element, a half (corner to
/// mid-side node) of a quadratic one, so edges bend with the faces they bound.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DisplayEdge {
pub p: [Vec3; 2],
pub nodes: [u32; 2],
/// The corner nodes of the element edge it is (part of), lower first.
pub corners: [u32; 2],
/// The element edge lies on a geometry edge: a feature line of the mesh.
pub feature: bool,
}
/// 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,
}
/// A segment of a geometry surface's B-rep edge (a named design edge), for picking and marking.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SurfaceCurve {
pub a: Vec3,
pub b: Vec3,
/// The surface (body) it belongs to, as [`SurfaceTri::body`].
pub body: u32,
/// The edge's index in that body's tessellation.
pub edge: 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<DisplayTri>,
/// Element edges on the visible faces, each once.
pub edges: Vec<DisplayEdge>,
/// Line elements (rods, beams).
pub lines: Vec<DisplayLine>,
/// Every node, in label order.
pub nodes: Vec<DisplayNode>,
/// Geometry surface triangles.
pub surface_tris: Vec<SurfaceTri>,
/// Geometry edges: where two faces meet, and open boundaries.
pub surface_edges: Vec<[Vec3; 2]>,
/// The B-rep edges of the geometry surfaces that have them, segment by segment.
pub curves: Vec<SurfaceCurve>,
/// Geometry surfaces (bodies) added.
pub bodies: u32,
pub bounds: Aabb3,
}
/// The faces drawn of an element, as positions in its node list ([`ElementKind::faces`]: the
/// corners counter-clockwise seen from outside, then the mid-side nodes of quadratic kinds): a
/// solid's faces in their numbering, a shell's top side (face 1).
fn drawn_faces(kind: ElementKind) -> &'static [&'static [usize]] {
let all = kind.faces();
if kind.dimension() == 2 { all.get(..1).unwrap_or(&[]) } else { all }
}
/// The corners of a face listed with `n` nodes: a quadratic face (tri6, quad8) has as many
/// mid-side nodes as corners.
fn face_corner_count(n: usize) -> usize {
if n >= 6 { n / 2 } else { n }
}
/// Triangles (positions in the face's boundary ring) that fill a face whose boundary ring has
/// `n` points. A quadratic face's ring alternates corner and mid-side node, and every triangle's
/// vertices are nodes: one triangle cut off at each corner, and the polygon of the mid-side
/// nodes fanned, so the drawn face passes through every node and its sides are the drawn edges.
fn face_triangles(n: usize, quadratic: bool) -> Vec<[usize; 3]> {
if !quadratic {
return (1..n.saturating_sub(1)).map(|k| [0, k, k + 1]).collect();
}
let mut out: Vec<[usize; 3]> = (0..n).step_by(2).map(|c| [c, c + 1, (c + n - 1) % n]).collect();
let mids: Vec<usize> = (1..n).step_by(2).collect();
for k in 1..mids.len().saturating_sub(1) {
if let (Some(&a), Some(&b), Some(&c)) = (mids.first(), mids.get(k), mids.get(k + 1)) {
out.push([a, b, c]);
}
}
out
}
/// A face's identity regardless of winding and starting corner: its sorted corner 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.iter().take(face_corner_count(face.len()))) {
*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<Vec3> {
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 {
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<u32>) -> DisplayMesh {
let mut out = DisplayMesh::default();
let pos = |label: u32| model.node(label).map(|n| Vec3::from(n.xyz));
let shown = |label: &u32| !hidden.contains(label);
// With elements hidden, the nodes only they use go too.
let hidden_nodes: std::collections::BTreeSet<u32> = if hidden.is_empty() {
Default::default()
} else {
let used: std::collections::BTreeSet<u32> =
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);
}
// 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()
.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();
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);
// 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().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)
&& let Some(local) = e.kind.edges().get(usize::from(edge).wrapping_sub(1))
&& let (Some(&a), Some(&b)) = (local.first().and_then(|&i| e.nodes.get(i)), local.get(1).and_then(|&i| e.nodes.get(i)))
{
features.push((a.min(b), a.max(b)));
}
}
}
features.sort_unstable();
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().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))
{
out.lines.push(DisplayLine { a, b, nodes: [na, nb], kind: e.kind, element: label });
}
continue;
}
let Some(at) = e.nodes.iter().map(|&n| pos(n)).collect::<Option<Vec<Vec3>>>() else { continue };
if at.len() != e.kind.node_count() {
continue;
}
let solid = e.kind.dimension() == 3;
let element_centre = centre(at.get(..e.kind.corner_count()).unwrap_or(&at));
for (number, f) in drawn_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;
}
// The face's boundary: corner, mid-side node, corner, … (corners only when linear).
let corners = face_corner_count(f.len());
let ring: Vec<usize> = (0..corners).flat_map(|j| std::iter::once(j).chain((f.len() > corners).then_some(corners + j))).collect();
let Some(ring) =
ring.iter().map(|&k| f.get(k).and_then(|&i| Some((*at.get(i)?, *e.nodes.get(i)?)))).collect::<Option<Vec<(Vec3, u32)>>>()
else {
continue;
};
let p: Vec<Vec3> = ring.iter().map(|r| r.0).collect();
let Some(mut normal) = polygon_normal(&p) else { continue };
if solid && normal.dot(centre(&p) - element_centre) < 0.0 {
normal = -normal;
}
for [a, b, c] in face_triangles(ring.len(), corners < ring.len()) {
if let (Some(&(pa, la)), Some(&(pb, lb)), Some(&(pc, lc))) = (ring.get(a), ring.get(b), ring.get(c)) {
out.tris.push(DisplayTri { p: [pa, pb, pc], nodes: [la, lb, lc], normal, kind: e.kind, element: label, face });
}
}
let step = ring.len() / corners.max(1);
for (k, &(_, a)) in ring.iter().enumerate() {
let b = ring.get((k + 1) % ring.len()).map_or(a, |r| r.1);
let (c0, c1) = (ring.get(k - k % step).map_or(a, |r| r.1), ring.get((k - k % step + step) % ring.len()).map_or(b, |r| r.1));
edges.push(((a.min(b), a.max(b)), (c0.min(c1), c0.max(c1))));
}
}
}
edges.sort_unstable();
edges.dedup();
out.edges = edges
.into_iter()
.filter_map(|((a, b), corners)| {
let feature = features.binary_search(&corners).is_ok();
Some(DisplayEdge { p: [pos(a)?, pos(b)?], nodes: [a, b], corners: [corners.0, corners.1], feature })
})
.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<u32> = (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<u32> = (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);
}
/// Add the B-rep edges (polylines, by edge index) of the surface [`DisplayMesh::add_surface`]
/// adds next, so they are picked as that body's edges; non-finite points are skipped.
pub fn add_curves(&mut self, edges: &[Vec<[f64; 3]>]) {
let body = self.bodies;
for (e, points) in edges.iter().enumerate() {
let Ok(edge) = u32::try_from(e) else { break };
for w in points.windows(2) {
let (a, b) = (Vec3::from(w[0]), Vec3::from(w[1]));
if a.is_finite() && b.is_finite() {
self.curves.push(SurfaceCurve { a, b, body, edge });
}
}
}
}
/// 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<C: Copy>(&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<Vec3> {
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<Hit> {
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<Hit> {
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<Vec3>) -> 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])], ..*e }).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);
}
/// 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();
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<Vec3> = 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();
// Its B-rep edges: the diagonal (two segments) and a side; a non-finite point is skipped.
d.add_curves(&[vec![p[0], [0.5, 0.5, 0.0], p[2]], vec![p[0], p[1]], vec![[f64::NAN; 3], p[1]]]);
d.add_surface(&p, &[[0, 1, 2], [0, 2, 3], [0, 0, 1], [0, 1, 9]], &[0, 1, 1, 1]);
assert_eq!(d.curves.iter().map(|c| (c.body, c.edge)).collect::<Vec<_>>(), vec![(0, 0), (0, 0), (0, 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<u8> = 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() {
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));
}
/// `block(2, 1, 1)` as hex20: a mid-side node on every edge, the ones on the top face
/// raised by 0.1 (a curved top, as a mesher puts them on a curved surface), and element 1's
/// edge 5 (corners 5 → 6, along the top) on a geometry edge.
fn curved_hex20_block() -> Model {
let linear = block(2, 1, 1);
let mut m = Model::default();
for (&l, n) in linear.nodes() {
m.set_node(l, n.xyz).unwrap();
}
let mut mids = std::collections::BTreeMap::new();
let mut next = 100;
for (&label, e) in linear.elements() {
let mut nodes = e.nodes.clone();
for edge in ElementKind::Hex20.edges() {
let (a, b) = (e.nodes[edge[0]], e.nodes[edge[1]]);
let id = *mids.entry((a.min(b), a.max(b))).or_insert_with(|| {
let (p, q) = (linear.node(a).unwrap().xyz, linear.node(b).unwrap().xyz);
let mut mid = [0.5 * (p[0] + q[0]), 0.5 * (p[1] + q[1]), 0.5 * (p[2] + q[2])];
if p[2] == 1.0 && q[2] == 1.0 {
mid[2] += 0.1;
}
next += 1;
m.set_node(next, mid).unwrap();
next
});
nodes.push(id);
}
m.set_element(label, ElementKind::Hex20, nodes).unwrap();
}
let edge = fieldcraft_model::GeometryRef::new("B", fieldcraft_model::Entity::Edge("top".into())).unwrap();
let tag = m.geometry_tag(&edge).unwrap();
m.tag_element_edge(1, 5, tag).unwrap();
m
}
/// A curved hex20 face is drawn through its mid-side nodes, and its faces, edges and feature
/// lines share their vertices exactly (#260: lines met the faces nowhere when zoomed in).
#[test]
fn quadratic_faces_and_edges_share_their_vertices() {
let m = curved_hex20_block();
let d = DisplayMesh::from_model(&m);
let at = |l: u32| Vec3::from(m.node(l).unwrap().xyz);
// Every vertex drawn is a node, exactly where the node is.
for t in &d.tris {
assert!(t.nodes.iter().zip(t.p).all(|(&l, p)| at(l) == p), "{t:?}");
}
for e in &d.edges {
assert!(e.nodes.iter().zip(e.p).all(|(&l, p)| at(l) == p), "{e:?}");
}
// 10 skin faces, 6 triangles each; the 20 element edges in two halves each.
assert_eq!(d.tris.len(), 60);
assert_eq!(d.edges.len(), 40);
// The drawn edges are exactly the sides of the faces' outlines.
let mut sides: Vec<((u32, u8), [u32; 2])> = d
.tris
.iter()
.flat_map(|t| {
[0, 1, 2].map(|k| {
((t.element, t.face), {
let mut s = [t.nodes[k], t.nodes[(k + 1) % 3]];
s.sort_unstable();
s
})
})
})
.collect();
sides.sort_unstable();
let mut outline: Vec<[u32; 2]> = sides.chunk_by(|a, b| a == b).filter(|r| r.len() == 1).map(|r| r[0].1).collect();
outline.sort_unstable();
outline.dedup();
let mut drawn: Vec<[u32; 2]> = d.edges.iter().map(|e| e.nodes).collect();
drawn.sort_unstable();
assert_eq!(outline, drawn);
// A top edge bows up through its mid-side node.
assert!(d.edges.iter().any(|e| e.p.iter().any(|p| (p.z - 1.1).abs() < 1e-12)));
// The edge on the geometry edge is a feature line, both halves; the rest are not.
let feature: Vec<[u32; 2]> = d.edges.iter().filter(|e| e.feature).map(|e| e.corners).collect();
let e1 = &m.element(1).unwrap().nodes;
let top = [e1[4].min(e1[5]), e1[4].max(e1[5])];
assert_eq!(feature, vec![top, top]);
}
#[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()));
}
}