Skip to main content

polyscope_structures/surface_mesh/
one_form_quantity.rs

1//! One-form quantities for surface meshes.
2//!
3//! A one-form assigns a scalar value to each edge of the mesh,
4//! representing integrated flux or circulation along the edge.
5//! It is rendered as vector arrows at edge midpoints.
6
7use glam::{Vec3, Vec4};
8use polyscope_core::quantity::{EdgeQuantity, Quantity, QuantityKind};
9use polyscope_render::{VectorRenderData, VectorUniforms};
10
11/// A one-form quantity on a surface mesh.
12///
13/// Stores one scalar value per edge with orientation conventions.
14/// Rendered as arrows at edge midpoints, where the arrow direction is
15/// along the edge and length is proportional to the value.
16pub struct MeshOneFormQuantity {
17    name: String,
18    structure_name: String,
19    values: Vec<f32>,        // One scalar per edge
20    orientations: Vec<bool>, // Edge orientation: true = default (low→high index)
21    enabled: bool,
22    length_scale: f32,
23    radius: f32,
24    color: Vec4,
25    render_data: Option<VectorRenderData>,
26}
27
28impl MeshOneFormQuantity {
29    /// Creates a new one-form quantity.
30    pub fn new(
31        name: impl Into<String>,
32        structure_name: impl Into<String>,
33        values: Vec<f32>,
34        orientations: Vec<bool>,
35    ) -> Self {
36        Self {
37            name: name.into(),
38            structure_name: structure_name.into(),
39            values,
40            orientations,
41            enabled: false,
42            length_scale: 1.0,
43            radius: 0.005,
44            color: Vec4::new(0.2, 0.7, 0.2, 1.0),
45            render_data: None,
46        }
47    }
48
49    /// Returns the per-edge scalar values.
50    #[must_use]
51    pub fn values(&self) -> &[f32] {
52        &self.values
53    }
54
55    /// Returns the edge orientation flags.
56    #[must_use]
57    pub fn orientations(&self) -> &[bool] {
58        &self.orientations
59    }
60
61    /// Gets the length scale.
62    #[must_use]
63    pub fn length_scale(&self) -> f32 {
64        self.length_scale
65    }
66
67    /// Sets the length scale.
68    pub fn set_length_scale(&mut self, scale: f32) -> &mut Self {
69        self.length_scale = scale;
70        self
71    }
72
73    /// Gets the radius.
74    #[must_use]
75    pub fn radius(&self) -> f32 {
76        self.radius
77    }
78
79    /// Sets the radius.
80    pub fn set_radius(&mut self, r: f32) -> &mut Self {
81        self.radius = r;
82        self
83    }
84
85    /// Gets the color.
86    #[must_use]
87    pub fn color(&self) -> Vec4 {
88        self.color
89    }
90
91    /// Sets the color.
92    pub fn set_color(&mut self, c: Vec3) -> &mut Self {
93        self.color = c.extend(1.0);
94        self
95    }
96
97    /// Convert edge scalars + orientations to vector field for rendering.
98    ///
99    /// Returns `(positions, vectors)` — one arrow per edge at the edge midpoint.
100    /// The vector direction is along the edge, scaled by the one-form value.
101    #[must_use]
102    pub fn compute_edge_vectors(
103        &self,
104        vertices: &[Vec3],
105        edges: &[(u32, u32)],
106    ) -> (Vec<Vec3>, Vec<Vec3>) {
107        let mut positions = Vec::with_capacity(self.values.len());
108        let mut vectors = Vec::with_capacity(self.values.len());
109
110        for (i, &(v0_idx, v1_idx)) in edges.iter().enumerate() {
111            if i >= self.values.len() {
112                break;
113            }
114
115            let v0 = vertices[v0_idx as usize];
116            let v1 = vertices[v1_idx as usize];
117
118            // Midpoint
119            let midpoint = (v0 + v1) * 0.5;
120
121            // Canonical direction: v0 → v1 (low → high index, since edges are sorted)
122            let mut direction = (v1 - v0).normalize_or_zero();
123
124            // Flip direction if orientation is false
125            if !self.orientations[i] {
126                direction = -direction;
127            }
128
129            let vector = direction * self.values[i] * self.length_scale;
130
131            positions.push(midpoint);
132            vectors.push(vector);
133        }
134
135        (positions, vectors)
136    }
137
138    /// Auto-scales length and radius based on the structure's bounding box diagonal.
139    pub fn auto_scale(
140        &mut self,
141        structure_length_scale: f32,
142        vertices: &[Vec3],
143        edges: &[(u32, u32)],
144    ) {
145        let (_positions, vecs) = self.compute_edge_vectors(vertices, edges);
146        let avg_length: f32 = if vecs.is_empty() {
147            1.0
148        } else {
149            let sum: f32 = vecs.iter().map(|v| v.length()).sum();
150            sum / vecs.len() as f32
151        };
152        if avg_length > 1e-8 {
153            self.length_scale = 0.02 * structure_length_scale / avg_length;
154        }
155        self.radius = 0.002 * structure_length_scale;
156    }
157
158    /// Initializes GPU resources for this vector quantity.
159    ///
160    /// Computes edge midpoint positions and direction vectors from the mesh,
161    /// then creates GPU buffers for arrow rendering.
162    pub fn init_gpu_resources(
163        &mut self,
164        device: &wgpu::Device,
165        bind_group_layout: &wgpu::BindGroupLayout,
166        camera_buffer: &wgpu::Buffer,
167        vertices: &[Vec3],
168        edges: &[(u32, u32)],
169    ) {
170        let (positions, vectors) = self.compute_edge_vectors(vertices, edges);
171        self.render_data = Some(VectorRenderData::new(
172            device,
173            bind_group_layout,
174            camera_buffer,
175            &positions,
176            &vectors,
177        ));
178    }
179
180    /// Returns the render data if initialized.
181    #[must_use]
182    pub fn render_data(&self) -> Option<&VectorRenderData> {
183        self.render_data.as_ref()
184    }
185
186    /// Updates GPU uniforms with the given model transform.
187    pub fn update_uniforms(&self, queue: &wgpu::Queue, model: &glam::Mat4) {
188        if let Some(render_data) = &self.render_data {
189            let uniforms = VectorUniforms {
190                model: model.to_cols_array(),
191                length_scale: self.length_scale,
192                radius: self.radius,
193                _padding: [0.0; 2],
194                color: self.color.to_array(),
195            };
196            render_data.update_uniforms(queue, &uniforms);
197        }
198    }
199
200    /// Builds the egui UI for this quantity.
201    pub fn build_egui_ui(&mut self, ui: &mut egui::Ui) -> bool {
202        let mut color = [self.color.x, self.color.y, self.color.z];
203        let changed = polyscope_ui::build_vector_quantity_ui(
204            ui,
205            &self.name,
206            &mut self.enabled,
207            &mut self.length_scale,
208            &mut self.radius,
209            &mut color,
210        );
211        if changed {
212            self.color = Vec4::new(color[0], color[1], color[2], self.color.w);
213        }
214        changed
215    }
216}
217
218impl Quantity for MeshOneFormQuantity {
219    fn as_any(&self) -> &dyn std::any::Any {
220        self
221    }
222    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
223        self
224    }
225    fn name(&self) -> &str {
226        &self.name
227    }
228    fn structure_name(&self) -> &str {
229        &self.structure_name
230    }
231    fn kind(&self) -> QuantityKind {
232        QuantityKind::Vector
233    }
234    fn is_enabled(&self) -> bool {
235        self.enabled
236    }
237    fn set_enabled(&mut self, enabled: bool) {
238        self.enabled = enabled;
239    }
240    fn build_ui(&mut self, _ui: &dyn std::any::Any) {}
241    fn refresh(&mut self) {}
242    fn clear_gpu_resources(&mut self) {
243        self.render_data = None;
244    }
245    fn data_size(&self) -> usize {
246        self.values.len()
247    }
248}
249
250impl EdgeQuantity for MeshOneFormQuantity {}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_one_form_creation() {
258        let values = vec![1.0, -0.5, 0.3];
259        let orientations = vec![true, true, false];
260        let q = MeshOneFormQuantity::new("flow", "mesh", values, orientations);
261
262        assert_eq!(q.name(), "flow");
263        assert_eq!(q.structure_name(), "mesh");
264        assert_eq!(q.data_size(), 3);
265        assert_eq!(q.kind(), QuantityKind::Vector);
266        assert!(!q.is_enabled());
267    }
268
269    #[test]
270    fn test_edge_vector_computation() {
271        let vertices = vec![
272            Vec3::new(0.0, 0.0, 0.0), // v0
273            Vec3::new(2.0, 0.0, 0.0), // v1
274            Vec3::new(1.0, 2.0, 0.0), // v2
275        ];
276        // Edges: (0,1), (0,2), (1,2) - sorted
277        let edges: Vec<(u32, u32)> = vec![(0, 1), (0, 2), (1, 2)];
278
279        // All orientations match canonical direction
280        let values = vec![1.0, 0.5, -0.5];
281        let orientations = vec![true, true, true];
282        let q = MeshOneFormQuantity::new("test", "mesh", values, orientations);
283
284        let (positions, vectors) = q.compute_edge_vectors(&vertices, &edges);
285
286        assert_eq!(positions.len(), 3);
287        assert_eq!(vectors.len(), 3);
288
289        // Edge (0,1): midpoint = (1, 0, 0), direction = +X, value = 1.0
290        assert!((positions[0] - Vec3::new(1.0, 0.0, 0.0)).length() < 1e-5);
291        assert!((vectors[0] - Vec3::new(1.0, 0.0, 0.0)).length() < 1e-5);
292
293        // Edge (0,2): midpoint = (0.5, 1, 0), value = 0.5
294        assert!((positions[1] - Vec3::new(0.5, 1.0, 0.0)).length() < 1e-5);
295    }
296
297    #[test]
298    fn test_edge_vector_orientation_flip() {
299        let vertices = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0)];
300        let edges: Vec<(u32, u32)> = vec![(0, 1)];
301
302        // Orientation = false means flip direction
303        let values = vec![1.0];
304        let orientations = vec![false];
305        let q = MeshOneFormQuantity::new("test", "mesh", values, orientations);
306
307        let (positions, vectors) = q.compute_edge_vectors(&vertices, &edges);
308
309        // Edge (0,1): canonical is +X, but orientation=false flips to -X
310        assert!((positions[0] - Vec3::new(1.0, 0.0, 0.0)).length() < 1e-5);
311        assert!((vectors[0] - Vec3::new(-1.0, 0.0, 0.0)).length() < 1e-5);
312    }
313
314    #[test]
315    fn test_edge_vector_negative_value() {
316        let vertices = vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0)];
317        let edges: Vec<(u32, u32)> = vec![(0, 1)];
318
319        // Negative value with default orientation
320        let values = vec![-1.0];
321        let orientations = vec![true];
322        let q = MeshOneFormQuantity::new("test", "mesh", values, orientations);
323
324        let (_positions, vectors) = q.compute_edge_vectors(&vertices, &edges);
325
326        // Negative value: direction is +X, value is -1.0, so vector points -X
327        assert!((vectors[0] - Vec3::new(-1.0, 0.0, 0.0)).length() < 1e-5);
328    }
329}