Skip to main content

viewport_lib/quantities/
one_forms.rs

1//! Whitney one-form reconstruction and conversion to [`GlyphItem`]s.
2//!
3//! A *one-form* assigns a scalar value to each directed edge of a triangle mesh.
4//! The value represents the integral of a covector field along that edge.
5//!
6//! # Edge ordering convention
7//!
8//! For triangle `t` with vertex indices `(v0, v1, v2)` from the index buffer:
9//!
10//! - `edge_values[3 * t + 0]` : value on edge `v0 -> v1`
11//! - `edge_values[3 * t + 1]` : value on edge `v1 -> v2`
12//! - `edge_values[3 * t + 2]` : value on edge `v2 -> v0`
13//!
14//! # Reconstruction formula
15//!
16//! The reconstructed vector field at face centroid `c` of triangle `(p0, p1, p2)`
17//! is the Hodge dual of the discrete one-form (Whitney reconstruction):
18//!
19//! ```text
20//! F = (w01 · R(e01) + w12 · R(e12) + w20 · R(e20)) / (2 · area)
21//! ```
22//!
23//! where `eij = pj − pi`, `R(v) = n × v` (90° rotation in the face plane),
24//! and `area` is the signed triangle area (`|n_raw| / 2`).
25
26use crate::GlyphItem;
27
28/// Convert a scalar-per-directed-edge one-form to a [`GlyphItem`] via Whitney
29/// reconstruction.
30///
31/// Returns one arrow per triangle placed at the face centroid, pointing in the
32/// direction of the reconstructed vector field.
33///
34/// # Arguments
35///
36/// * `positions`    : vertex positions in world/local space
37/// * `indices`      : triangle index list (every 3 indices form one triangle)
38/// * `edge_values`  : one scalar per directed edge, in triangle-local order
39///                    (see [module-level docs](self) for the convention).
40///                    Length must be `3 × num_triangles`.
41/// * `scale`        : global arrow scale (see [`GlyphItem::scale`])
42///
43/// Triangles whose `edge_values` slice is shorter than expected are skipped.
44pub fn edge_one_form_to_glyphs(
45    positions: &[[f32; 3]],
46    indices: &[u32],
47    edge_values: &[f32],
48    scale: f32,
49) -> GlyphItem {
50    let num_tris = indices.len() / 3;
51    let n = num_tris.min(edge_values.len() / 3);
52
53    let mut glyph_positions = Vec::with_capacity(n);
54    let mut glyph_vectors = Vec::with_capacity(n);
55
56    for tri in 0..n {
57        let i0 = indices[3 * tri] as usize;
58        let i1 = indices[3 * tri + 1] as usize;
59        let i2 = indices[3 * tri + 2] as usize;
60
61        if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
62            continue;
63        }
64
65        let p0 = glam::Vec3::from(positions[i0]);
66        let p1 = glam::Vec3::from(positions[i1]);
67        let p2 = glam::Vec3::from(positions[i2]);
68
69        let e01 = p1 - p0;
70        let e12 = p2 - p1;
71        let e20 = p0 - p2;
72
73        // Face normal (unnormalised; length = 2 * area).
74        let n_raw = e01.cross(-e20); // (p1-p0) × (p2-p0)
75        let area2 = n_raw.length();
76
77        if area2 < 1e-12 {
78            continue; // degenerate triangle
79        }
80
81        let face_normal = n_raw / area2; // normalised
82
83        let w01 = edge_values[3 * tri];
84        let w12 = edge_values[3 * tri + 1];
85        let w20 = edge_values[3 * tri + 2];
86
87        // R(v) = face_normal × v  (rotates v by 90° within the face plane)
88        let f = (w01 * face_normal.cross(e01)
89            + w12 * face_normal.cross(e12)
90            + w20 * face_normal.cross(e20))
91            / area2; // divide by 2*area, but area2 = 2*area
92
93        let centroid = (p0 + p1 + p2) / 3.0;
94
95        glyph_positions.push(centroid.to_array());
96        glyph_vectors.push(f.to_array());
97    }
98
99    let mut item = GlyphItem::default();
100    item.positions = glyph_positions;
101    item.vectors = glyph_vectors;
102    item.scale = scale;
103    item
104}