forked from 1b-agent/fieldcraft-upstream
Results display (M4) and offscreen full-UI capture (#28)
* ui.capture: the whole window drawn offscreen and rasterised on the CPU ui.screenshot falls back to it when no frame is presented (hidden window, locked screen), so agents can look at panels and the toolbar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Draw the open design and imported surfaces in the viewport DisplayMesh keeps node labels on every element primitive (for results), gains add_surface (geometry faces and their boundaries, coincident points welded) and displaced (elements moved by nodal offsets). Bodies are tessellated at a thousandth of their size; meshed models show the geometry as boundaries only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * results.* commands: deformed shape, contours, mode shapes, animation, reaction sums The results display is session state (ResultsView), set by commands that are not edits. Nodal fields (displacement, von Mises, principal and component stresses; principal stresses in closed form after Smith 1961) and reaction force and moment sums per set. Results kept from an earlier revision stay viewable, marked stale; solve.probe says when a result is stale. The Results tab's display and reaction entries are ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Results display: deformed shape, contours with legend, mode shapes and animation, probe, reaction sums The viewport draws the session's results view: elements moved by their nodes' displacements or a mode shape (automatic or given scale), faces split exactly along contour band boundaries into flat-coloured pieces (same on GPU and CPU), a legend with levels and extremes, a results panel whose controls run the results.* commands, a click probing the nearest node (solve.probe), and stale results marked. Solving from the form shows the result on the Results tab. sample::cantilever is a solvable model for tests and demos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Simplify the results display (review) One Contour in the plot; the results scene reuses scene::build; a solve shows its result through the engine (ResultsView::for_result) and the UI raises the Results tab when a view appears; the plot's scale and contour values are kept across animation frames; design bodies are tessellated once per design; the first content is framed where the display is rebuilt; field_values in one match; probe reuses results::displacements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Results display for explicit steps: field frames, energy histories The results view works on the engine's step results (static, frequency, explicit): ResultsView.frame and results.frame {frame | time} pick an explicit field frame; the deformed shape (true scale) and contours (element stresses averaged to the nodes, velocity) follow it; animation runs through the frames. The Results panel has a frame slider and plots the kinetic, internal, hourglass and external energies with their values at the frame. sample::impact is a steel bar flying onto a rigid wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs/features.md after the rebase Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
30 files changed
+2989
-86
No files matched your search
@@ -0,0 +1,176 @@
|
||||
//! Banded contour plots: a range of nodal values cut into equal bands, each drawn in one colour.
|
||||
//!
|
||||
//! A triangle with values at its corners is split exactly along the band boundaries (the
|
||||
//! values vary linearly across it), so every piece lies in one band and the band edges are
|
||||
//! sharp iso-lines on any renderer, with no shader support: the GPU viewport and the CPU
|
||||
//! rasterizer draw the same flat-coloured pieces.
|
||||
|
||||
use crate::{Rgb, Vec3};
|
||||
|
||||
/// Equal bands from `min` to `max`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Bands {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
impl Bands {
|
||||
/// `count` bands over `[min, max]`; a range of one value (or none) is one band around it.
|
||||
pub fn new(min: f64, max: f64, count: u32) -> Bands {
|
||||
let count = count.max(1);
|
||||
if !(min.is_finite() && max.is_finite()) {
|
||||
return Bands { min: 0.0, max: 0.0, count };
|
||||
}
|
||||
Bands { min: min.min(max), max: max.max(min), count }
|
||||
}
|
||||
|
||||
/// The band a value lies in (values outside the range go to the end bands); `None` for a
|
||||
/// value that is not a number.
|
||||
pub fn band(&self, v: f64) -> Option<u32> {
|
||||
if v.is_nan() {
|
||||
return None;
|
||||
}
|
||||
let span = self.max - self.min;
|
||||
if !(span > 0.0) {
|
||||
return Some(self.count / 2);
|
||||
}
|
||||
let k = ((v - self.min) / span * f64::from(self.count)).floor();
|
||||
Some(k.clamp(0.0, f64::from(self.count - 1)) as u32)
|
||||
}
|
||||
|
||||
/// The value between band `k − 1` and band `k` (`level(0)` is `min`, `level(count)` `max`).
|
||||
pub fn level(&self, k: u32) -> f64 {
|
||||
self.min + (self.max - self.min) * f64::from(k) / f64::from(self.count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a triangle (corners `p`, values `v`) into pieces that each lie in one band, and hand
|
||||
/// each piece (a convex polygon of 3 to 5 corners) to `out` with its band. A triangle with a
|
||||
/// value that is not a number is handed whole, with no band.
|
||||
pub fn split_triangle(p: [Vec3; 3], v: [f64; 3], bands: &Bands, out: &mut impl FnMut(&[Vec3], Option<u32>)) {
|
||||
let [Some(a), Some(b), Some(c)] = v.map(|x| bands.band(x)) else {
|
||||
out(&p, None);
|
||||
return;
|
||||
};
|
||||
let (lo, hi) = (a.min(b).min(c), a.max(b).max(c));
|
||||
if lo == hi {
|
||||
out(&p, Some(lo));
|
||||
return;
|
||||
}
|
||||
let poly: Vec<(Vec3, f64)> = p.into_iter().zip(v).collect();
|
||||
for k in lo..=hi {
|
||||
// The end bands also hold the values beyond the range.
|
||||
let below = if k == 0 { f64::NEG_INFINITY } else { bands.level(k) };
|
||||
let above = if k + 1 == bands.count { f64::INFINITY } else { bands.level(k + 1) };
|
||||
let piece = clip(&clip(&poly, below, true), above, false);
|
||||
if piece.len() >= 3 {
|
||||
let pts: Vec<Vec3> = piece.iter().map(|(q, _)| *q).collect();
|
||||
out(&pts, Some(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The part of a convex polygon where the value is at least (`keep_above`) or at most `level`
|
||||
/// (Sutherland–Hodgman against one level).
|
||||
fn clip(poly: &[(Vec3, f64)], level: f64, keep_above: bool) -> Vec<(Vec3, f64)> {
|
||||
if level.is_infinite() {
|
||||
return poly.to_vec();
|
||||
}
|
||||
let inside = |v: f64| if keep_above { v >= level } else { v <= level };
|
||||
let mut out = Vec::with_capacity(poly.len() + 1);
|
||||
for (i, &(a, va)) in poly.iter().enumerate() {
|
||||
let Some(&(b, vb)) = poly.get((i + 1) % poly.len()) else { continue };
|
||||
if inside(va) {
|
||||
out.push((a, va));
|
||||
}
|
||||
if inside(va) != inside(vb) {
|
||||
let t = (level - va) / (vb - va);
|
||||
out.push((a + (b - a) * t, level));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The colour of band `k` of `count`: the colour scale `stops` (low to high) sampled at the
|
||||
/// band's middle.
|
||||
pub fn band_color(stops: &[Rgb], k: u32, count: u32) -> Rgb {
|
||||
let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return Rgb(128, 128, 128) };
|
||||
if stops.len() == 1 {
|
||||
return *first;
|
||||
}
|
||||
let count = count.max(1);
|
||||
let t = (f64::from(k.min(count - 1)) + 0.5) / f64::from(count) * (stops.len() - 1) as f64;
|
||||
let i = (t.floor() as usize).min(stops.len() - 2);
|
||||
let f = t - i as f64;
|
||||
let (Some(a), Some(b)) = (stops.get(i), stops.get(i + 1)) else { return *last };
|
||||
let mix = |x: u8, y: u8| (f64::from(x) + (f64::from(y) - f64::from(x)) * f).round().clamp(0.0, 255.0) as u8;
|
||||
Rgb(mix(a.0, b.0), mix(a.1, b.1), mix(a.2, b.2))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn area(p: &[Vec3]) -> f64 {
|
||||
let mut n = Vec3::ZERO;
|
||||
for i in 1..p.len() - 1 {
|
||||
n = n + (p[i] - p[0]).cross(p[i + 1] - p[0]);
|
||||
}
|
||||
n.len() * 0.5
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bands_and_levels() {
|
||||
let b = Bands::new(0.0, 10.0, 5);
|
||||
assert_eq!(
|
||||
(b.band(0.0), b.band(1.99), b.band(2.0), b.band(10.0), b.band(-5.0), b.band(99.0)),
|
||||
(Some(0), Some(0), Some(1), Some(4), Some(0), Some(4))
|
||||
);
|
||||
assert_eq!((b.level(0), b.level(5)), (0.0, 10.0));
|
||||
assert_eq!(b.band(f64::NAN), None);
|
||||
// One value: one band in the middle; reversed and non-finite ranges are made safe.
|
||||
assert_eq!(Bands::new(3.0, 3.0, 6).band(3.0), Some(3));
|
||||
assert_eq!(Bands::new(5.0, 1.0, 4), Bands::new(1.0, 5.0, 4));
|
||||
assert_eq!(Bands::new(f64::NAN, 1.0, 0).count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_triangle_splits_along_the_levels_and_keeps_its_area() {
|
||||
let p = [Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0), Vec3::new(0.0, 10.0, 0.0)];
|
||||
// The value is x: pieces are strips between x = 0, 2, 4, …
|
||||
let b = Bands::new(0.0, 10.0, 5);
|
||||
let mut pieces = Vec::new();
|
||||
split_triangle(p, [0.0, 10.0, 0.0], &b, &mut |q, k| pieces.push((q.to_vec(), k)));
|
||||
assert_eq!(pieces.iter().map(|(_, k)| k.unwrap()).collect::<Vec<_>>(), vec![0, 1, 2, 3, 4]);
|
||||
let total: f64 = pieces.iter().map(|(q, _)| area(q)).sum();
|
||||
assert!((total - 50.0).abs() < 1e-9, "{total}");
|
||||
for (q, k) in &pieces {
|
||||
let k = k.unwrap() as f64;
|
||||
assert!(q.iter().all(|c| c.x >= 2.0 * k - 1e-9 && c.x <= 2.0 * k + 2.0 + 1e-9), "{q:?} in band {k}");
|
||||
}
|
||||
// A band-uniform triangle stays whole; a NaN value hands it over with no band.
|
||||
let mut n = 0;
|
||||
split_triangle(p, [0.5, 1.0, 1.5], &b, &mut |q, k| {
|
||||
n += 1;
|
||||
assert_eq!((q.len(), k), (3, Some(0)));
|
||||
});
|
||||
split_triangle(p, [0.5, f64::NAN, 1.5], &b, &mut |_, k| {
|
||||
n += 1;
|
||||
assert_eq!(k, None);
|
||||
});
|
||||
assert_eq!(n, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn band_colours_run_through_the_stops() {
|
||||
let stops = [Rgb(0, 0, 255), Rgb(0, 255, 0), Rgb(255, 0, 0)];
|
||||
assert_eq!(band_color(&stops, 0, 2), Rgb(0, 128, 128));
|
||||
assert_eq!(band_color(&stops, 1, 2), Rgb(128, 128, 0));
|
||||
let first = band_color(&stops, 0, 12);
|
||||
let last = band_color(&stops, 11, 12);
|
||||
assert!(first.2 > 200 && last.0 > 200, "{first:?} {last:?}");
|
||||
assert_eq!(band_color(&[], 0, 3), Rgb(128, 128, 128));
|
||||
assert_eq!(band_color(&stops, 99, 3), band_color(&stops, 2, 3));
|
||||
}
|
||||
}
|
||||
+225
-19
@@ -1,43 +1,77 @@
|
||||
//! What a finite element model looks like on screen: the faces nobody shares (the skin of the
|
||||
//! solid elements, and every shell), the element edges on those faces, line elements and nodes.
|
||||
//! What a model looks like on screen.
|
||||
//!
|
||||
//! 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. The output is deterministic: faces come out in
|
||||
//! element label order, edges sorted.
|
||||
//! 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 modelcraft_model::{ElementKind, Model};
|
||||
|
||||
use crate::{Aabb3, Vec3};
|
||||
|
||||
/// One drawn triangle.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Display data for a model.
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Display data for a model and its geometry.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct DisplayMesh {
|
||||
/// Visible faces as triangles (quads are split in two).
|
||||
/// 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<[Vec3; 2]>,
|
||||
pub edges: Vec<DisplayEdge>,
|
||||
/// Line elements (rods, beams).
|
||||
pub lines: Vec<DisplayLine>,
|
||||
/// Every node.
|
||||
pub nodes: Vec<Vec3>,
|
||||
/// 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]>,
|
||||
pub bounds: Aabb3,
|
||||
}
|
||||
|
||||
@@ -86,9 +120,9 @@ 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 n in model.nodes().values() {
|
||||
for (&label, n) in model.nodes() {
|
||||
let p = Vec3::from(n.xyz);
|
||||
out.nodes.push(p);
|
||||
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
|
||||
@@ -120,8 +154,8 @@ impl DisplayMesh {
|
||||
}
|
||||
let at = at.get(..corners).unwrap_or(&[]);
|
||||
if e.kind.dimension() == 1 {
|
||||
if let (Some(&a), Some(&b)) = (at.first(), at.get(1)) {
|
||||
out.lines.push(DisplayLine { a, b, kind: e.kind });
|
||||
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 });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -132,13 +166,16 @@ impl DisplayMesh {
|
||||
continue;
|
||||
}
|
||||
let p: Vec<Vec3> = f.iter().filter_map(|&i| at.get(i).copied()).collect();
|
||||
let labels: Vec<u32> = 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)) = (p.first(), p.get(k), p.get(k + 1)) {
|
||||
out.tris.push(DisplayTri { p: [a, b, c], normal, kind: e.kind });
|
||||
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 });
|
||||
}
|
||||
}
|
||||
for (j, &i) in f.iter().enumerate() {
|
||||
@@ -151,7 +188,116 @@ impl DisplayMesh {
|
||||
}
|
||||
edges.sort_unstable();
|
||||
edges.dedup();
|
||||
out.edges = edges.into_iter().filter_map(|(a, b)| Some([pos(a)?, pos(b)?])).collect();
|
||||
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<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);
|
||||
}
|
||||
self.surface_tris.push(SurfaceTri { p: [a, b, c], normal });
|
||||
let face = tri_face.get(t).copied().unwrap_or(u32::MAX);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 mut best: Option<(f64, &DisplayTri)> = None;
|
||||
for t in &self.tris {
|
||||
// Möller–Trumbore, both sides.
|
||||
let (e1, e2) = (t.p[1] - t.p[0], t.p[2] - t.p[0]);
|
||||
let h = dir.cross(e2);
|
||||
let det = e1.dot(h);
|
||||
if !(det.abs() > 1e-300) {
|
||||
continue;
|
||||
}
|
||||
let s = origin - t.p[0];
|
||||
let u = s.dot(h) / det;
|
||||
let q = s.cross(e1);
|
||||
let v = dir.dot(q) / det;
|
||||
let d = e2.dot(q) / det;
|
||||
if u >= 0.0 && v >= 0.0 && u + v <= 1.0 && d >= 0.0 && d.is_finite() && best.is_none_or(|(b, _)| d < b) {
|
||||
best = Some((d, t));
|
||||
}
|
||||
}
|
||||
let (d, t) = best?;
|
||||
let hit = origin + dir * d;
|
||||
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])], 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
|
||||
}
|
||||
}
|
||||
@@ -226,6 +372,66 @@ mod tests {
|
||||
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();
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod camera;
|
||||
pub mod contour;
|
||||
mod display;
|
||||
mod raster;
|
||||
mod vec;
|
||||
|
||||
pub use camera::{Camera, Mat4, StandardView};
|
||||
pub use display::{DisplayLine, DisplayMesh, DisplayTri};
|
||||
pub use display::{DisplayEdge, DisplayLine, DisplayMesh, DisplayNode, DisplayTri, SurfaceTri};
|
||||
pub use raster::{
|
||||
Canvas, MAX_DIM, MAX_PIXELS, MAX_PRIMITIVES, RenderError, Rgb, Scene, SceneLine, ScenePoint, SceneTri, fit_size, png, render, render_png, shade,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user