Skip to main content

polyscope_structures/volume_mesh/
mod.rs

1//! Volume mesh structure for tetrahedral and hexahedral meshes.
2//!
3//! # Overview
4//!
5//! `VolumeMesh` supports both tetrahedral (4 vertices) and hexahedral (8 vertices)
6//! cells. Mixed meshes are supported by using 8-index cells where unused indices
7//! are set to `u32::MAX`.
8//!
9//! # Interior/Exterior Faces
10//!
11//! Only exterior faces (not shared between cells) are rendered. This is determined
12//! by hashing sorted face vertex indices and counting occurrences.
13//!
14//! # Quantities
15//!
16//! Supported quantities:
17//! - `VolumeMeshVertexScalarQuantity` - scalar per vertex
18//! - `VolumeMeshCellScalarQuantity` - scalar per cell
19//! - `VolumeMeshVertexColorQuantity` - RGB color per vertex
20//! - `VolumeMeshCellColorQuantity` - RGB color per cell
21//! - `VolumeMeshVertexVectorQuantity` - vector per vertex
22//! - `VolumeMeshCellVectorQuantity` - vector per cell
23//!
24//! # Example
25//!
26//! ```rust,ignore
27//! use glam::Vec3;
28//! use polyscope_structures::VolumeMesh;
29//!
30//! // Create a single tetrahedron
31//! let vertices = vec![
32//!     Vec3::new(0.0, 0.0, 0.0),
33//!     Vec3::new(1.0, 0.0, 0.0),
34//!     Vec3::new(0.5, 1.0, 0.0),
35//!     Vec3::new(0.5, 0.5, 1.0),
36//! ];
37//! let tets = vec![[0, 1, 2, 3]];
38//! let mut mesh = VolumeMesh::new_tet_mesh("my_tet", vertices, tets);
39//!
40//! // Add a scalar quantity
41//! mesh.add_vertex_scalar_quantity("temperature", vec![0.0, 0.5, 1.0, 0.25]);
42//! ```
43
44mod color_quantity;
45mod scalar_quantity;
46pub mod slice_geometry;
47mod vector_quantity;
48
49pub use color_quantity::*;
50pub use scalar_quantity::*;
51pub use slice_geometry::{CellSliceResult, slice_hex, slice_tet};
52pub use vector_quantity::*;
53
54// Re-export SliceMeshData from this module
55
56use glam::{Mat4, Vec3, Vec4};
57use polyscope_core::pick::PickResult;
58use polyscope_core::quantity::Quantity;
59use polyscope_core::structure::{HasQuantities, RenderContext, Structure};
60use polyscope_render::{
61    MeshPickUniforms, MeshUniforms, SliceMeshRenderData, SurfaceMeshRenderData,
62};
63
64/// Cell type for volume meshes.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum VolumeCellType {
67    /// Tetrahedron (4 vertices)
68    Tet,
69    /// Hexahedron (8 vertices)
70    Hex,
71}
72
73/// A volume mesh structure (tetrahedral or hexahedral).
74///
75/// Cells are stored as arrays of 8 vertex indices. For tetrahedra,
76/// only the first 4 indices are used (indices 4-7 are set to `u32::MAX`).
77pub struct VolumeMesh {
78    name: String,
79
80    // Geometry
81    vertices: Vec<Vec3>,
82    cells: Vec<[u32; 8]>, // 8 indices per cell, unused slots hold u32::MAX
83
84    // Common structure fields
85    enabled: bool,
86    transform: Mat4,
87    quantities: Vec<Box<dyn Quantity>>,
88
89    // Visualization parameters
90    color: Vec4,
91    interior_color: Vec4,
92    edge_color: Vec4,
93    edge_width: f32,
94
95    // GPU resources (renders exterior faces)
96    render_data: Option<SurfaceMeshRenderData>,
97
98    // GPU picking resources
99    pick_uniform_buffer: Option<wgpu::Buffer>,
100    pick_bind_group: Option<wgpu::BindGroup>,
101    pick_cell_index_buffer: Option<wgpu::Buffer>,
102    global_start: u32,
103
104    // Slice mesh GPU resources (renders cross-section caps)
105    slice_render_data: Option<SliceMeshRenderData>,
106    /// Cached slice plane parameters for invalidation (origin, normal)
107    slice_plane_cache: Option<(Vec3, Vec3)>,
108    /// Cached cell culling plane parameters (origin, normal) for each enabled plane.
109    /// When Some, indicates `render_data` shows culled geometry.
110    culling_plane_cache: Option<Vec<(Vec3, Vec3)>>,
111}
112
113impl VolumeMesh {
114    /// Creates a new volume mesh from vertices and cell indices.
115    ///
116    /// # Arguments
117    /// * `name` - The name of the mesh
118    /// * `vertices` - Vertex positions
119    /// * `cells` - Cell indices, 8 per cell (unused indices should be `u32::MAX`)
120    pub fn new(name: impl Into<String>, vertices: Vec<Vec3>, cells: Vec<[u32; 8]>) -> Self {
121        let color = Vec4::new(0.25, 0.50, 0.75, 1.0);
122        // Interior color is a desaturated version
123        let interior_color = Vec4::new(0.45, 0.50, 0.55, 1.0);
124
125        Self {
126            name: name.into(),
127            vertices,
128            cells,
129            enabled: true,
130            transform: Mat4::IDENTITY,
131            quantities: Vec::new(),
132            color,
133            interior_color,
134            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
135            edge_width: 0.0,
136            render_data: None,
137            pick_uniform_buffer: None,
138            pick_bind_group: None,
139            pick_cell_index_buffer: None,
140            global_start: 0,
141            slice_render_data: None,
142            slice_plane_cache: None,
143            culling_plane_cache: None,
144        }
145    }
146
147    /// Creates a tetrahedral mesh.
148    pub fn new_tet_mesh(name: impl Into<String>, vertices: Vec<Vec3>, tets: Vec<[u32; 4]>) -> Self {
149        // Convert tets to 8-index cells
150        let cells: Vec<[u32; 8]> = tets
151            .into_iter()
152            .map(|t| {
153                [
154                    t[0],
155                    t[1],
156                    t[2],
157                    t[3],
158                    u32::MAX,
159                    u32::MAX,
160                    u32::MAX,
161                    u32::MAX,
162                ]
163            })
164            .collect();
165        Self::new(name, vertices, cells)
166    }
167
168    /// Creates a hexahedral mesh.
169    pub fn new_hex_mesh(
170        name: impl Into<String>,
171        vertices: Vec<Vec3>,
172        hexes: Vec<[u32; 8]>,
173    ) -> Self {
174        Self::new(name, vertices, hexes)
175    }
176
177    /// Returns the number of vertices.
178    #[must_use]
179    pub fn num_vertices(&self) -> usize {
180        self.vertices.len()
181    }
182
183    /// Returns the number of cells.
184    #[must_use]
185    pub fn num_cells(&self) -> usize {
186        self.cells.len()
187    }
188
189    /// Returns the cell type of the given cell.
190    #[must_use]
191    pub fn cell_type(&self, cell_idx: usize) -> VolumeCellType {
192        if self.cells[cell_idx][4] == u32::MAX {
193            VolumeCellType::Tet
194        } else {
195            VolumeCellType::Hex
196        }
197    }
198
199    /// Returns the vertices.
200    #[must_use]
201    pub fn vertices(&self) -> &[Vec3] {
202        &self.vertices
203    }
204
205    /// Returns the cells.
206    #[must_use]
207    pub fn cells(&self) -> &[[u32; 8]] {
208        &self.cells
209    }
210
211    /// Gets the base color.
212    #[must_use]
213    pub fn color(&self) -> Vec4 {
214        self.color
215    }
216
217    /// Sets the base color.
218    pub fn set_color(&mut self, color: Vec3) -> &mut Self {
219        self.color = color.extend(1.0);
220        self
221    }
222
223    /// Gets the interior color.
224    #[must_use]
225    pub fn interior_color(&self) -> Vec4 {
226        self.interior_color
227    }
228
229    /// Sets the interior color.
230    pub fn set_interior_color(&mut self, color: Vec3) -> &mut Self {
231        self.interior_color = color.extend(1.0);
232        self
233    }
234
235    /// Gets the edge color.
236    #[must_use]
237    pub fn edge_color(&self) -> Vec4 {
238        self.edge_color
239    }
240
241    /// Sets the edge color.
242    pub fn set_edge_color(&mut self, color: Vec3) -> &mut Self {
243        self.edge_color = color.extend(1.0);
244        self
245    }
246
247    /// Gets the edge width.
248    #[must_use]
249    pub fn edge_width(&self) -> f32 {
250        self.edge_width
251    }
252
253    /// Sets the edge width.
254    pub fn set_edge_width(&mut self, width: f32) -> &mut Self {
255        self.edge_width = width;
256        self
257    }
258
259    /// Decomposes all cells into tetrahedra.
260    /// Tets pass through unchanged, hexes are decomposed into 5 tets.
261    #[must_use]
262    pub fn decompose_to_tets(&self) -> Vec<[u32; 4]> {
263        let mut tets = Vec::new();
264
265        for cell in &self.cells {
266            if cell[4] == u32::MAX {
267                // Already a tet
268                tets.push([cell[0], cell[1], cell[2], cell[3]]);
269            } else {
270                // Hex - decompose using diagonal pattern (5 tets)
271                for tet_local in &HEX_TO_TET_PATTERN {
272                    let tet = [
273                        cell[tet_local[0]],
274                        cell[tet_local[1]],
275                        cell[tet_local[2]],
276                        cell[tet_local[3]],
277                    ];
278                    tets.push(tet);
279                }
280            }
281        }
282
283        tets
284    }
285
286    /// Returns the number of tetrahedra (including decomposed hexes).
287    #[must_use]
288    pub fn num_tets(&self) -> usize {
289        self.decompose_to_tets().len()
290    }
291
292    /// Computes face counts for interior/exterior detection.
293    fn compute_face_counts(&self) -> HashMap<[u32; 4], usize> {
294        let mut face_counts: HashMap<[u32; 4], usize> = HashMap::new();
295
296        for cell in &self.cells {
297            if cell[4] == u32::MAX {
298                // Tetrahedron
299                for [a, b, c] in TET_FACE_STENCIL {
300                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
301                    *face_counts.entry(key).or_insert(0) += 1;
302                }
303            } else {
304                // Hexahedron - each quad face uses same 4 vertices
305                for quad in HEX_FACE_STENCIL {
306                    // Get the 4 unique vertices of this quad face
307                    let v0 = cell[quad[0][0]];
308                    let v1 = cell[quad[0][1]];
309                    let v2 = cell[quad[0][2]];
310                    let v3 = cell[quad[1][2]]; // The fourth vertex
311                    let key = canonical_face_key(v0, v1, v2, Some(v3));
312                    *face_counts.entry(key).or_insert(0) += 1;
313                }
314            }
315        }
316
317        face_counts
318    }
319
320    /// Computes the centroid of a cell.
321    fn cell_centroid(&self, cell: &[u32; 8]) -> Vec3 {
322        if cell[4] == u32::MAX {
323            // Tetrahedron: average of 4 vertices
324            let sum = self.vertices[cell[0] as usize]
325                + self.vertices[cell[1] as usize]
326                + self.vertices[cell[2] as usize]
327                + self.vertices[cell[3] as usize];
328            sum / 4.0
329        } else {
330            // Hexahedron: average of 8 vertices
331            let sum = (0..8)
332                .map(|i| self.vertices[cell[i] as usize])
333                .fold(Vec3::ZERO, |a, b| a + b);
334            sum / 8.0
335        }
336    }
337
338    /// Tests if a cell should be visible based on slice planes.
339    /// Returns true if the cell's centroid is on the "kept" side of all planes.
340    fn is_cell_visible(&self, cell: &[u32; 8], planes: &[(Vec3, Vec3)]) -> bool {
341        if planes.is_empty() {
342            return true;
343        }
344        let centroid = self.cell_centroid(cell);
345        let centroid_world = (self.transform * centroid.extend(1.0)).truncate();
346        for (plane_origin, plane_normal) in planes {
347            let signed_dist = (centroid_world - *plane_origin).dot(*plane_normal);
348            // Keep cells on the positive side of the plane (same side as normal points)
349            if signed_dist < 0.0 {
350                return false;
351            }
352        }
353        true
354    }
355
356    /// Computes face counts for interior/exterior detection, only for visible cells.
357    fn compute_face_counts_with_culling(
358        &self,
359        planes: &[(Vec3, Vec3)],
360    ) -> HashMap<[u32; 4], usize> {
361        let mut face_counts: HashMap<[u32; 4], usize> = HashMap::new();
362
363        for cell in &self.cells {
364            // Skip cells culled by slice planes
365            if !self.is_cell_visible(cell, planes) {
366                continue;
367            }
368
369            if cell[4] == u32::MAX {
370                // Tetrahedron
371                for [a, b, c] in TET_FACE_STENCIL {
372                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
373                    *face_counts.entry(key).or_insert(0) += 1;
374                }
375            } else {
376                // Hexahedron
377                for quad in HEX_FACE_STENCIL {
378                    let v0 = cell[quad[0][0]];
379                    let v1 = cell[quad[0][1]];
380                    let v2 = cell[quad[0][2]];
381                    let v3 = cell[quad[1][2]];
382                    let key = canonical_face_key(v0, v1, v2, Some(v3));
383                    *face_counts.entry(key).or_insert(0) += 1;
384                }
385            }
386        }
387
388        face_counts
389    }
390
391    /// Generates triangulated exterior faces for rendering.
392    fn generate_render_geometry(&self) -> (Vec<Vec3>, Vec<[u32; 3]>) {
393        let face_counts = self.compute_face_counts();
394        let mut positions = Vec::new();
395        let mut faces = Vec::new();
396
397        for cell in &self.cells {
398            if cell[4] == u32::MAX {
399                // Tetrahedron
400                for [a, b, c] in TET_FACE_STENCIL {
401                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
402                    if face_counts[&key] == 1 {
403                        // Exterior face
404                        let base_idx = positions.len() as u32;
405                        positions.push(self.vertices[cell[a] as usize]);
406                        positions.push(self.vertices[cell[b] as usize]);
407                        positions.push(self.vertices[cell[c] as usize]);
408                        faces.push([base_idx, base_idx + 1, base_idx + 2]);
409                    }
410                }
411            } else {
412                // Hexahedron
413                for quad in HEX_FACE_STENCIL {
414                    let v0 = cell[quad[0][0]];
415                    let v1 = cell[quad[0][1]];
416                    let v2 = cell[quad[0][2]];
417                    let v3 = cell[quad[1][2]];
418                    let key = canonical_face_key(v0, v1, v2, Some(v3));
419                    if face_counts[&key] == 1 {
420                        // Exterior face - emit both triangles
421                        for [a, b, c] in quad {
422                            let base_idx = positions.len() as u32;
423                            positions.push(self.vertices[cell[a] as usize]);
424                            positions.push(self.vertices[cell[b] as usize]);
425                            positions.push(self.vertices[cell[c] as usize]);
426                            faces.push([base_idx, base_idx + 1, base_idx + 2]);
427                        }
428                    }
429                }
430            }
431        }
432
433        (positions, faces)
434    }
435
436    /// Generates triangulated exterior faces with cell culling based on slice planes.
437    /// Only cells whose centroid is on the positive side of all planes are rendered.
438    fn generate_render_geometry_with_culling(
439        &self,
440        planes: &[(Vec3, Vec3)],
441    ) -> (Vec<Vec3>, Vec<[u32; 3]>) {
442        // Compute face counts only for visible cells
443        let face_counts = self.compute_face_counts_with_culling(planes);
444        let mut positions = Vec::new();
445        let mut faces = Vec::new();
446
447        for cell in &self.cells {
448            // Skip cells culled by slice planes
449            if !self.is_cell_visible(cell, planes) {
450                continue;
451            }
452
453            if cell[4] == u32::MAX {
454                // Tetrahedron
455                for [a, b, c] in TET_FACE_STENCIL {
456                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
457                    // Render face if it's exterior among visible cells
458                    if face_counts.get(&key) == Some(&1) {
459                        let base_idx = positions.len() as u32;
460                        positions.push(self.vertices[cell[a] as usize]);
461                        positions.push(self.vertices[cell[b] as usize]);
462                        positions.push(self.vertices[cell[c] as usize]);
463                        faces.push([base_idx, base_idx + 1, base_idx + 2]);
464                    }
465                }
466            } else {
467                // Hexahedron
468                for quad in HEX_FACE_STENCIL {
469                    let v0 = cell[quad[0][0]];
470                    let v1 = cell[quad[0][1]];
471                    let v2 = cell[quad[0][2]];
472                    let v3 = cell[quad[1][2]];
473                    let key = canonical_face_key(v0, v1, v2, Some(v3));
474                    if face_counts.get(&key) == Some(&1) {
475                        for [a, b, c] in quad {
476                            let base_idx = positions.len() as u32;
477                            positions.push(self.vertices[cell[a] as usize]);
478                            positions.push(self.vertices[cell[b] as usize]);
479                            positions.push(self.vertices[cell[c] as usize]);
480                            faces.push([base_idx, base_idx + 1, base_idx + 2]);
481                        }
482                    }
483                }
484            }
485        }
486
487        (positions, faces)
488    }
489
490    /// Generates render geometry including any enabled quantity data.
491    #[must_use]
492    pub fn generate_render_geometry_with_quantities(&self) -> VolumeMeshRenderGeometry {
493        let face_counts = self.compute_face_counts();
494        let mut positions = Vec::new();
495        let mut faces = Vec::new();
496        let mut vertex_indices = Vec::new(); // Track original vertex indices
497        let mut cell_indices = Vec::new(); // Track which cell each face belongs to
498
499        // First pass: generate geometry and track indices
500        for (cell_idx, cell) in self.cells.iter().enumerate() {
501            if cell[4] == u32::MAX {
502                // Tetrahedron
503                for [a, b, c] in TET_FACE_STENCIL {
504                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
505                    if face_counts[&key] == 1 {
506                        let base_idx = positions.len() as u32;
507                        positions.push(self.vertices[cell[a] as usize]);
508                        positions.push(self.vertices[cell[b] as usize]);
509                        positions.push(self.vertices[cell[c] as usize]);
510                        vertex_indices.push(cell[a] as usize);
511                        vertex_indices.push(cell[b] as usize);
512                        vertex_indices.push(cell[c] as usize);
513                        cell_indices.push(cell_idx);
514                        cell_indices.push(cell_idx);
515                        cell_indices.push(cell_idx);
516                        faces.push([base_idx, base_idx + 1, base_idx + 2]);
517                    }
518                }
519            } else {
520                // Hexahedron
521                for quad in HEX_FACE_STENCIL {
522                    let v0 = cell[quad[0][0]];
523                    let v1 = cell[quad[0][1]];
524                    let v2 = cell[quad[0][2]];
525                    let v3 = cell[quad[1][2]];
526                    let key = canonical_face_key(v0, v1, v2, Some(v3));
527                    if face_counts[&key] == 1 {
528                        for [a, b, c] in quad {
529                            let base_idx = positions.len() as u32;
530                            positions.push(self.vertices[cell[a] as usize]);
531                            positions.push(self.vertices[cell[b] as usize]);
532                            positions.push(self.vertices[cell[c] as usize]);
533                            vertex_indices.push(cell[a] as usize);
534                            vertex_indices.push(cell[b] as usize);
535                            vertex_indices.push(cell[c] as usize);
536                            cell_indices.push(cell_idx);
537                            cell_indices.push(cell_idx);
538                            cell_indices.push(cell_idx);
539                            faces.push([base_idx, base_idx + 1, base_idx + 2]);
540                        }
541                    }
542                }
543            }
544        }
545
546        // Compute normals
547        let mut normals = vec![Vec3::ZERO; positions.len()];
548        for [a, b, c] in &faces {
549            let p0 = positions[*a as usize];
550            let p1 = positions[*b as usize];
551            let p2 = positions[*c as usize];
552            let normal = (p1 - p0).cross(p2 - p0).normalize_or_zero();
553            normals[*a as usize] = normal;
554            normals[*b as usize] = normal;
555            normals[*c as usize] = normal;
556        }
557
558        // Find enabled scalar quantity
559        let mut vertex_values = None;
560        let mut vertex_colors = None;
561
562        for q in &self.quantities {
563            if q.is_enabled() {
564                if let Some(scalar) = q.as_any().downcast_ref::<VolumeMeshVertexScalarQuantity>() {
565                    let values: Vec<f32> = vertex_indices
566                        .iter()
567                        .map(|&idx| scalar.values().get(idx).copied().unwrap_or(0.0))
568                        .collect();
569                    vertex_values = Some(values);
570                    break;
571                }
572                if let Some(color) = q.as_any().downcast_ref::<VolumeMeshVertexColorQuantity>() {
573                    let colors: Vec<Vec3> = vertex_indices
574                        .iter()
575                        .map(|&idx| {
576                            color
577                                .colors()
578                                .get(idx)
579                                .copied()
580                                .unwrap_or(Vec4::new(1.0, 1.0, 1.0, 1.0))
581                                .truncate()
582                        })
583                        .collect();
584                    vertex_colors = Some(colors);
585                    break;
586                }
587                if let Some(scalar) = q.as_any().downcast_ref::<VolumeMeshCellScalarQuantity>() {
588                    let values: Vec<f32> = cell_indices
589                        .iter()
590                        .map(|&idx| scalar.values().get(idx).copied().unwrap_or(0.0))
591                        .collect();
592                    vertex_values = Some(values);
593                    break;
594                }
595                if let Some(color) = q.as_any().downcast_ref::<VolumeMeshCellColorQuantity>() {
596                    let colors: Vec<Vec3> = cell_indices
597                        .iter()
598                        .map(|&idx| {
599                            color
600                                .colors()
601                                .get(idx)
602                                .copied()
603                                .unwrap_or(Vec4::new(1.0, 1.0, 1.0, 1.0))
604                                .truncate()
605                        })
606                        .collect();
607                    vertex_colors = Some(colors);
608                    break;
609                }
610            }
611        }
612
613        VolumeMeshRenderGeometry {
614            positions,
615            faces,
616            normals,
617            vertex_values,
618            vertex_colors,
619        }
620    }
621
622    /// Initializes GPU render data.
623    pub fn init_render_data(
624        &mut self,
625        device: &wgpu::Device,
626        bind_group_layout: &wgpu::BindGroupLayout,
627        camera_buffer: &wgpu::Buffer,
628    ) {
629        let (positions, triangles) = self.generate_render_geometry();
630
631        if triangles.is_empty() {
632            return;
633        }
634
635        // Compute per-vertex normals (for flat shading, each triangle vertex gets the face normal)
636        let mut normals = vec![Vec3::ZERO; positions.len()];
637        for [a, b, c] in &triangles {
638            let p0 = positions[*a as usize];
639            let p1 = positions[*b as usize];
640            let p2 = positions[*c as usize];
641            let normal = (p1 - p0).cross(p2 - p0).normalize_or_zero();
642            normals[*a as usize] = normal;
643            normals[*b as usize] = normal;
644            normals[*c as usize] = normal;
645        }
646
647        // For volume meshes, all edges are "real" (not internal triangulation edges)
648        // edge_is_real is per-triangle-vertex, 3 values per triangle
649        let edge_is_real: Vec<Vec3> = vec![Vec3::ONE; triangles.len() * 3];
650
651        let render_data = SurfaceMeshRenderData::new(
652            device,
653            bind_group_layout,
654            camera_buffer,
655            &positions,
656            &triangles,
657            &normals,
658            &edge_is_real,
659        );
660
661        self.render_data = Some(render_data);
662    }
663
664    /// Reinitializes render data with cell culling based on slice planes.
665    /// Cells whose centroid is on the negative side of any plane are hidden.
666    /// Uses caching to avoid regenerating geometry every frame.
667    pub fn update_render_data_with_culling(
668        &mut self,
669        device: &wgpu::Device,
670        bind_group_layout: &wgpu::BindGroupLayout,
671        camera_buffer: &wgpu::Buffer,
672        planes: &[(Vec3, Vec3)],
673    ) {
674        // Check if cache is still valid (plane hasn't moved)
675        let cache_valid = self.culling_plane_cache.as_ref().is_some_and(|cache| {
676            if cache.len() != planes.len() {
677                return false;
678            }
679            cache.iter().zip(planes.iter()).all(|((o, n), (po, pn))| {
680                (*o - *po).length_squared() < 1e-10 && (*n - *pn).length_squared() < 1e-10
681            })
682        });
683
684        if cache_valid && self.render_data.is_some() {
685            // Cached geometry is still valid
686            return;
687        }
688
689        let (positions, triangles) = self.generate_render_geometry_with_culling(planes);
690
691        if triangles.is_empty() {
692            self.render_data = None;
693            self.culling_plane_cache = Some(planes.to_vec());
694            return;
695        }
696
697        // Compute per-vertex normals
698        let mut normals = vec![Vec3::ZERO; positions.len()];
699        for [a, b, c] in &triangles {
700            let p0 = positions[*a as usize];
701            let p1 = positions[*b as usize];
702            let p2 = positions[*c as usize];
703            let normal = (p1 - p0).cross(p2 - p0).normalize_or_zero();
704            normals[*a as usize] = normal;
705            normals[*b as usize] = normal;
706            normals[*c as usize] = normal;
707        }
708
709        let edge_is_real: Vec<Vec3> = vec![Vec3::ONE; triangles.len() * 3];
710
711        let render_data = SurfaceMeshRenderData::new(
712            device,
713            bind_group_layout,
714            camera_buffer,
715            &positions,
716            &triangles,
717            &normals,
718            &edge_is_real,
719        );
720
721        self.render_data = Some(render_data);
722        self.culling_plane_cache = Some(planes.to_vec());
723    }
724
725    /// Resets render data to show all cells (no culling).
726    pub fn reset_render_data(
727        &mut self,
728        device: &wgpu::Device,
729        bind_group_layout: &wgpu::BindGroupLayout,
730        camera_buffer: &wgpu::Buffer,
731    ) {
732        self.culling_plane_cache = None;
733        self.init_render_data(device, bind_group_layout, camera_buffer);
734    }
735
736    /// Returns true if the mesh is currently showing culled geometry.
737    #[must_use]
738    pub fn is_culled(&self) -> bool {
739        self.culling_plane_cache.is_some()
740    }
741
742    /// Returns the render data if available.
743    #[must_use]
744    pub fn render_data(&self) -> Option<&SurfaceMeshRenderData> {
745        self.render_data.as_ref()
746    }
747
748    /// Generates triangulated exterior faces for picking.
749    /// Uses current slice plane culling if provided.
750    #[must_use]
751    pub fn pick_triangles(&self, planes: &[(Vec3, Vec3)]) -> (Vec<Vec3>, Vec<[u32; 3]>) {
752        if planes.is_empty() {
753            self.generate_render_geometry()
754        } else {
755            self.generate_render_geometry_with_culling(planes)
756        }
757    }
758
759    /// Generates cell index per triangle mapping for GPU picking.
760    ///
761    /// Returns one `u32` per triangle indicating which cell that triangle belongs to.
762    /// Matches the triangle ordering from `generate_render_geometry()`.
763    fn generate_cell_index_per_triangle(&self) -> Vec<u32> {
764        let face_counts = self.compute_face_counts();
765        let mut cell_indices = Vec::new();
766
767        for (cell_idx, cell) in self.cells.iter().enumerate() {
768            if cell[4] == u32::MAX {
769                // Tetrahedron
770                for [a, b, c] in TET_FACE_STENCIL {
771                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
772                    if face_counts[&key] == 1 {
773                        cell_indices.push(cell_idx as u32);
774                    }
775                }
776            } else {
777                // Hexahedron
778                for quad in HEX_FACE_STENCIL {
779                    let v0 = cell[quad[0][0]];
780                    let v1 = cell[quad[0][1]];
781                    let v2 = cell[quad[0][2]];
782                    let v3 = cell[quad[1][2]];
783                    let key = canonical_face_key(v0, v1, v2, Some(v3));
784                    if face_counts[&key] == 1 {
785                        // 2 triangles per quad face
786                        cell_indices.push(cell_idx as u32);
787                        cell_indices.push(cell_idx as u32);
788                    }
789                }
790            }
791        }
792
793        cell_indices
794    }
795
796    /// Generates cell index per triangle mapping with cell culling.
797    fn generate_cell_index_per_triangle_with_culling(&self, planes: &[(Vec3, Vec3)]) -> Vec<u32> {
798        let face_counts = self.compute_face_counts_with_culling(planes);
799        let mut cell_indices = Vec::new();
800
801        for (cell_idx, cell) in self.cells.iter().enumerate() {
802            if !self.is_cell_visible(cell, planes) {
803                continue;
804            }
805
806            if cell[4] == u32::MAX {
807                for [a, b, c] in TET_FACE_STENCIL {
808                    let key = canonical_face_key(cell[a], cell[b], cell[c], None);
809                    if face_counts.get(&key) == Some(&1) {
810                        cell_indices.push(cell_idx as u32);
811                    }
812                }
813            } else {
814                for quad in HEX_FACE_STENCIL {
815                    let v0 = cell[quad[0][0]];
816                    let v1 = cell[quad[0][1]];
817                    let v2 = cell[quad[0][2]];
818                    let v3 = cell[quad[1][2]];
819                    let key = canonical_face_key(v0, v1, v2, Some(v3));
820                    if face_counts.get(&key) == Some(&1) {
821                        cell_indices.push(cell_idx as u32);
822                        cell_indices.push(cell_idx as u32);
823                    }
824                }
825            }
826        }
827
828        cell_indices
829    }
830
831    /// Initializes GPU resources for pick rendering.
832    ///
833    /// Creates the pick uniform buffer, cell index mapping buffer, and bind group.
834    /// The cell index buffer maps each GPU triangle to its parent cell index.
835    pub fn init_pick_resources(
836        &mut self,
837        device: &wgpu::Device,
838        mesh_pick_bind_group_layout: &wgpu::BindGroupLayout,
839        camera_buffer: &wgpu::Buffer,
840        global_start: u32,
841    ) {
842        use wgpu::util::DeviceExt;
843
844        self.global_start = global_start;
845
846        let model = self.transform.to_cols_array_2d();
847        let pick_uniforms = MeshPickUniforms {
848            global_start,
849            _padding: [0.0; 3],
850            model,
851        };
852        let pick_uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
853            label: Some("volume mesh pick uniforms"),
854            contents: bytemuck::cast_slice(&[pick_uniforms]),
855            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
856        });
857
858        // Build cell index mapping: tri_index -> cell_index
859        let cell_index_data = if self.culling_plane_cache.is_some() {
860            self.generate_cell_index_per_triangle_with_culling(
861                self.culling_plane_cache.as_deref().unwrap_or(&[]),
862            )
863        } else {
864            self.generate_cell_index_per_triangle()
865        };
866
867        let pick_cell_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
868            label: Some("volume mesh pick cell indices"),
869            contents: bytemuck::cast_slice(&cell_index_data),
870            usage: wgpu::BufferUsages::STORAGE,
871        });
872
873        // Create pick bind group using render_data vertex buffer
874        if let Some(render_data) = &self.render_data {
875            let pick_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
876                label: Some("volume mesh pick bind group"),
877                layout: mesh_pick_bind_group_layout,
878                entries: &[
879                    wgpu::BindGroupEntry {
880                        binding: 0,
881                        resource: camera_buffer.as_entire_binding(),
882                    },
883                    wgpu::BindGroupEntry {
884                        binding: 1,
885                        resource: pick_uniform_buffer.as_entire_binding(),
886                    },
887                    wgpu::BindGroupEntry {
888                        binding: 2,
889                        resource: render_data.vertex_buffer.as_entire_binding(),
890                    },
891                    wgpu::BindGroupEntry {
892                        binding: 3,
893                        resource: pick_cell_index_buffer.as_entire_binding(),
894                    },
895                ],
896            });
897            self.pick_bind_group = Some(pick_bind_group);
898        }
899
900        self.pick_uniform_buffer = Some(pick_uniform_buffer);
901        self.pick_cell_index_buffer = Some(pick_cell_index_buffer);
902    }
903
904    /// Returns the pick bind group if initialized.
905    #[must_use]
906    pub fn pick_bind_group(&self) -> Option<&wgpu::BindGroup> {
907        self.pick_bind_group.as_ref()
908    }
909
910    /// Updates pick uniforms (model transform) when the structure is moved.
911    pub fn update_pick_uniforms(&self, queue: &wgpu::Queue) {
912        if let Some(buffer) = &self.pick_uniform_buffer {
913            let model = self.transform.to_cols_array_2d();
914            let pick_uniforms = MeshPickUniforms {
915                global_start: self.global_start,
916                _padding: [0.0; 3],
917                model,
918            };
919            queue.write_buffer(buffer, 0, bytemuck::cast_slice(&[pick_uniforms]));
920        }
921    }
922
923    /// Returns the total number of vertices in the rendered triangulation (for draw calls).
924    #[must_use]
925    pub fn num_render_vertices(&self) -> u32 {
926        self.render_data
927            .as_ref()
928            .map_or(0, SurfaceMeshRenderData::num_vertices)
929    }
930
931    /// Updates GPU buffers.
932    pub fn update_gpu_buffers(&self, queue: &wgpu::Queue) {
933        if let Some(ref rd) = self.render_data {
934            // Convert transform to array format for GPU
935            let model_matrix = self.transform.to_cols_array_2d();
936
937            let uniforms = MeshUniforms {
938                model_matrix,
939                shade_style: 0, // smooth
940                show_edges: u32::from(self.edge_width > 0.0),
941                edge_width: self.edge_width,
942                transparency: 0.0,
943                surface_color: self.color.to_array(),
944                edge_color: self.edge_color.to_array(),
945                backface_policy: 0,
946                slice_planes_enabled: 0,
947                ..Default::default()
948            };
949            rd.update_uniforms(queue, &uniforms);
950        }
951    }
952
953    /// Updates or creates slice mesh render data for a given slice plane.
954    ///
955    /// Returns `true` if the slice intersects this volume mesh.
956    pub fn update_slice_render_data(
957        &mut self,
958        device: &wgpu::Device,
959        queue: &wgpu::Queue,
960        bind_group_layout: &wgpu::BindGroupLayout,
961        camera_buffer: &wgpu::Buffer,
962        plane_origin: Vec3,
963        plane_normal: Vec3,
964    ) -> bool {
965        // Check if cache is still valid
966        let cache_valid = self.slice_plane_cache.is_some_and(|(o, n)| {
967            (o - plane_origin).length_squared() < 1e-10
968                && (n - plane_normal).length_squared() < 1e-10
969        });
970
971        if cache_valid {
972            if let Some(ref data) = self.slice_render_data {
973                return !data.is_empty();
974            }
975        }
976
977        // Generate new slice geometry
978        if let Some(slice_data) = self.generate_slice_geometry(plane_origin, plane_normal) {
979            if let Some(ref mut rd) = self.slice_render_data {
980                // Update existing render data
981                rd.update(
982                    device,
983                    queue,
984                    bind_group_layout,
985                    camera_buffer,
986                    &slice_data.vertices,
987                    &slice_data.normals,
988                    &slice_data.colors,
989                );
990            } else {
991                // Create new render data
992                self.slice_render_data = Some(SliceMeshRenderData::new(
993                    device,
994                    bind_group_layout,
995                    camera_buffer,
996                    &slice_data.vertices,
997                    &slice_data.normals,
998                    &slice_data.colors,
999                ));
1000            }
1001
1002            // Update uniforms with interior color
1003            if let Some(ref rd) = self.slice_render_data {
1004                rd.update_uniforms(queue, self.interior_color.truncate());
1005            }
1006
1007            self.slice_plane_cache = Some((plane_origin, plane_normal));
1008            true
1009        } else {
1010            // No intersection
1011            self.slice_render_data = None;
1012            self.slice_plane_cache = None;
1013            false
1014        }
1015    }
1016
1017    /// Returns the slice render data if available.
1018    #[must_use]
1019    pub fn slice_render_data(&self) -> Option<&SliceMeshRenderData> {
1020        self.slice_render_data.as_ref()
1021    }
1022
1023    /// Clears the slice render data cache.
1024    pub fn clear_slice_render_data(&mut self) {
1025        self.slice_render_data = None;
1026        self.slice_plane_cache = None;
1027    }
1028
1029    /// Builds the egui UI for this volume mesh.
1030    pub fn build_egui_ui(&mut self, ui: &mut egui::Ui) {
1031        // Info
1032        let num_tets = self.cells.iter().filter(|c| c[4] == u32::MAX).count();
1033        let num_hexes = self.num_cells() - num_tets;
1034        ui.label(format!(
1035            "{} verts, {} cells ({} tets, {} hexes)",
1036            self.num_vertices(),
1037            self.num_cells(),
1038            num_tets,
1039            num_hexes
1040        ));
1041
1042        // Color
1043        ui.horizontal(|ui| {
1044            ui.label("Color:");
1045            let mut color = [self.color.x, self.color.y, self.color.z];
1046            if ui.color_edit_button_rgb(&mut color).changed() {
1047                self.color = Vec4::new(color[0], color[1], color[2], self.color.w);
1048            }
1049        });
1050
1051        // Edge width
1052        ui.horizontal(|ui| {
1053            let mut show_edges = self.edge_width > 0.0;
1054            if ui.checkbox(&mut show_edges, "Edges").changed() {
1055                self.set_edge_width(if show_edges { 1.0 } else { 0.0 });
1056            }
1057            if show_edges {
1058                let mut width = self.edge_width;
1059                if ui
1060                    .add(
1061                        egui::DragValue::new(&mut width)
1062                            .speed(0.01)
1063                            .range(0.01..=5.0),
1064                    )
1065                    .changed()
1066                {
1067                    self.set_edge_width(width);
1068                }
1069            }
1070        });
1071    }
1072
1073    /// Adds a vertex scalar quantity.
1074    pub fn add_vertex_scalar_quantity(
1075        &mut self,
1076        name: impl Into<String>,
1077        values: Vec<f32>,
1078    ) -> &mut Self {
1079        let name = name.into();
1080        let quantity = VolumeMeshVertexScalarQuantity::new(name.clone(), self.name.clone(), values);
1081        self.add_quantity(Box::new(quantity));
1082        self
1083    }
1084
1085    /// Adds a cell scalar quantity.
1086    pub fn add_cell_scalar_quantity(
1087        &mut self,
1088        name: impl Into<String>,
1089        values: Vec<f32>,
1090    ) -> &mut Self {
1091        let name = name.into();
1092        let quantity = VolumeMeshCellScalarQuantity::new(name.clone(), self.name.clone(), values);
1093        self.add_quantity(Box::new(quantity));
1094        self
1095    }
1096
1097    /// Adds a vertex color quantity.
1098    pub fn add_vertex_color_quantity(
1099        &mut self,
1100        name: impl Into<String>,
1101        colors: Vec<Vec3>,
1102    ) -> &mut Self {
1103        let name = name.into();
1104        let quantity = VolumeMeshVertexColorQuantity::new(name.clone(), self.name.clone(), colors);
1105        self.add_quantity(Box::new(quantity));
1106        self
1107    }
1108
1109    /// Adds a cell color quantity.
1110    pub fn add_cell_color_quantity(
1111        &mut self,
1112        name: impl Into<String>,
1113        colors: Vec<Vec3>,
1114    ) -> &mut Self {
1115        let name = name.into();
1116        let quantity = VolumeMeshCellColorQuantity::new(name.clone(), self.name.clone(), colors);
1117        self.add_quantity(Box::new(quantity));
1118        self
1119    }
1120
1121    /// Adds a vertex vector quantity.
1122    pub fn add_vertex_vector_quantity(
1123        &mut self,
1124        name: impl Into<String>,
1125        vectors: Vec<Vec3>,
1126    ) -> &mut Self {
1127        let name = name.into();
1128        let quantity =
1129            VolumeMeshVertexVectorQuantity::new(name.clone(), self.name.clone(), vectors);
1130        self.add_quantity(Box::new(quantity));
1131        self
1132    }
1133
1134    /// Adds a cell vector quantity.
1135    pub fn add_cell_vector_quantity(
1136        &mut self,
1137        name: impl Into<String>,
1138        vectors: Vec<Vec3>,
1139    ) -> &mut Self {
1140        let name = name.into();
1141        let quantity = VolumeMeshCellVectorQuantity::new(name.clone(), self.name.clone(), vectors);
1142        self.add_quantity(Box::new(quantity));
1143        self
1144    }
1145
1146    /// Returns the active vertex color quantity, if any.
1147    fn active_vertex_color_quantity(&self) -> Option<&VolumeMeshVertexColorQuantity> {
1148        for q in &self.quantities {
1149            if q.is_enabled() {
1150                if let Some(vcq) = q.as_any().downcast_ref::<VolumeMeshVertexColorQuantity>() {
1151                    return Some(vcq);
1152                }
1153            }
1154        }
1155        None
1156    }
1157
1158    /// Generates mesh geometry for the cross-section created by a slice plane.
1159    ///
1160    /// This computes the intersection of all cells with the plane and triangulates
1161    /// the resulting polygons for rendering. If a vertex color quantity is enabled,
1162    /// colors are interpolated at slice points.
1163    ///
1164    /// # Arguments
1165    /// * `plane_origin` - A point on the slice plane
1166    /// * `plane_normal` - The plane normal (points toward kept geometry)
1167    ///
1168    /// # Returns
1169    /// `Some(SliceMeshData)` if the plane intersects the mesh, `None` otherwise.
1170    #[must_use]
1171    pub fn generate_slice_geometry(
1172        &self,
1173        plane_origin: Vec3,
1174        plane_normal: Vec3,
1175    ) -> Option<SliceMeshData> {
1176        let mut vertices = Vec::new();
1177        let mut normals = Vec::new();
1178        let mut colors = Vec::new();
1179
1180        // Get active vertex color quantity for interpolation (if any)
1181        let vertex_colors = self
1182            .active_vertex_color_quantity()
1183            .map(color_quantity::VolumeMeshVertexColorQuantity::colors);
1184
1185        for (cell_idx, cell) in self.cells.iter().enumerate() {
1186            let cell_type = self.cell_type(cell_idx);
1187
1188            let slice = match cell_type {
1189                VolumeCellType::Tet => slice_tet(
1190                    self.vertices[cell[0] as usize],
1191                    self.vertices[cell[1] as usize],
1192                    self.vertices[cell[2] as usize],
1193                    self.vertices[cell[3] as usize],
1194                    plane_origin,
1195                    plane_normal,
1196                ),
1197                VolumeCellType::Hex => {
1198                    let hex_verts: [Vec3; 8] =
1199                        std::array::from_fn(|i| self.vertices[cell[i] as usize]);
1200                    slice_hex(hex_verts, plane_origin, plane_normal)
1201                }
1202            };
1203
1204            if slice.has_intersection() {
1205                // Compute interpolated colors for each slice vertex
1206                let slice_colors: Vec<Vec4> = if let Some(vc) = vertex_colors {
1207                    slice
1208                        .interpolation
1209                        .iter()
1210                        .map(|&(a, b, t)| {
1211                            // Map local cell indices to global vertex indices
1212                            let va_idx = cell[a as usize] as usize;
1213                            let vb_idx = cell[b as usize] as usize;
1214                            // Interpolate colors
1215                            vc[va_idx].lerp(vc[vb_idx], t)
1216                        })
1217                        .collect()
1218                } else {
1219                    vec![self.interior_color; slice.vertices.len()]
1220                };
1221
1222                // Triangulate the polygon (fan from first vertex)
1223                for i in 1..slice.vertices.len() - 1 {
1224                    vertices.push(slice.vertices[0]);
1225                    vertices.push(slice.vertices[i]);
1226                    vertices.push(slice.vertices[i + 1]);
1227
1228                    // Normal is the slice plane normal
1229                    normals.push(plane_normal);
1230                    normals.push(plane_normal);
1231                    normals.push(plane_normal);
1232
1233                    // Interpolated colors (or interior_color if no quantity)
1234                    colors.push(slice_colors[0]);
1235                    colors.push(slice_colors[i]);
1236                    colors.push(slice_colors[i + 1]);
1237                }
1238            }
1239        }
1240
1241        if vertices.is_empty() {
1242            return None;
1243        }
1244
1245        Some(SliceMeshData {
1246            vertices,
1247            normals,
1248            colors,
1249        })
1250    }
1251}
1252
1253/// Data representing a slice mesh cross-section.
1254///
1255/// Contains triangulated geometry for rendering the cross-section
1256/// created by a slice plane intersecting a volume mesh.
1257#[derive(Debug, Clone)]
1258pub struct SliceMeshData {
1259    /// Vertex positions (3 per triangle)
1260    pub vertices: Vec<Vec3>,
1261    /// Vertex normals (3 per triangle, all pointing along plane normal)
1262    pub normals: Vec<Vec3>,
1263    /// Vertex colors (3 per triangle, from interior color or interpolated quantity)
1264    pub colors: Vec<Vec4>,
1265}
1266
1267impl SliceMeshData {
1268    /// Returns the number of triangles in the slice mesh.
1269    #[must_use]
1270    pub fn num_triangles(&self) -> usize {
1271        self.vertices.len() / 3
1272    }
1273
1274    /// Returns true if the slice mesh is empty.
1275    #[must_use]
1276    pub fn is_empty(&self) -> bool {
1277        self.vertices.is_empty()
1278    }
1279}
1280
1281impl Structure for VolumeMesh {
1282    fn as_any(&self) -> &dyn std::any::Any {
1283        self
1284    }
1285
1286    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1287        self
1288    }
1289
1290    fn name(&self) -> &str {
1291        &self.name
1292    }
1293
1294    fn type_name(&self) -> &'static str {
1295        "VolumeMesh"
1296    }
1297
1298    fn bounding_box(&self) -> Option<(Vec3, Vec3)> {
1299        if self.vertices.is_empty() {
1300            return None;
1301        }
1302
1303        let mut min = Vec3::splat(f32::MAX);
1304        let mut max = Vec3::splat(f32::MIN);
1305
1306        for &v in &self.vertices {
1307            min = min.min(v);
1308            max = max.max(v);
1309        }
1310
1311        Some((min, max))
1312    }
1313
1314    fn length_scale(&self) -> f32 {
1315        self.bounding_box()
1316            .map_or(1.0, |(min, max)| (max - min).length())
1317    }
1318
1319    fn transform(&self) -> Mat4 {
1320        self.transform
1321    }
1322
1323    fn set_transform(&mut self, transform: Mat4) {
1324        self.transform = transform;
1325        self.culling_plane_cache = None;
1326        // Invalidate pick resources since cell culling may change
1327        self.pick_bind_group = None;
1328        self.pick_cell_index_buffer = None;
1329    }
1330
1331    fn is_enabled(&self) -> bool {
1332        self.enabled
1333    }
1334
1335    fn set_enabled(&mut self, enabled: bool) {
1336        self.enabled = enabled;
1337    }
1338
1339    fn draw(&self, _ctx: &mut dyn RenderContext) {
1340        // Drawing is handled externally
1341    }
1342
1343    fn draw_pick(&self, _ctx: &mut dyn RenderContext) {
1344        // Picking not implemented
1345    }
1346
1347    fn build_ui(&mut self, _ui: &dyn std::any::Any) {
1348        // UI is built via build_egui_ui
1349    }
1350
1351    fn build_pick_ui(&self, _ui: &dyn std::any::Any, _pick: &PickResult) {
1352        // Pick UI not implemented
1353    }
1354
1355    fn clear_gpu_resources(&mut self) {
1356        self.render_data = None;
1357        self.pick_uniform_buffer = None;
1358        self.pick_bind_group = None;
1359        self.pick_cell_index_buffer = None;
1360        self.slice_render_data = None;
1361        self.slice_plane_cache = None;
1362        self.culling_plane_cache = None;
1363        for quantity in &mut self.quantities {
1364            quantity.clear_gpu_resources();
1365        }
1366    }
1367
1368    fn refresh(&mut self) {
1369        self.render_data = None;
1370        self.pick_uniform_buffer = None;
1371        self.pick_bind_group = None;
1372        self.pick_cell_index_buffer = None;
1373        self.slice_render_data = None;
1374        self.slice_plane_cache = None;
1375        self.culling_plane_cache = None;
1376        for quantity in &mut self.quantities {
1377            quantity.refresh();
1378        }
1379    }
1380}
1381
1382impl HasQuantities for VolumeMesh {
1383    fn add_quantity(&mut self, quantity: Box<dyn Quantity>) {
1384        self.quantities.push(quantity);
1385    }
1386
1387    fn get_quantity(&self, name: &str) -> Option<&dyn Quantity> {
1388        self.quantities
1389            .iter()
1390            .find(|q| q.name() == name)
1391            .map(std::convert::AsRef::as_ref)
1392    }
1393
1394    fn get_quantity_mut(&mut self, name: &str) -> Option<&mut Box<dyn Quantity>> {
1395        self.quantities.iter_mut().find(|q| q.name() == name)
1396    }
1397
1398    fn remove_quantity(&mut self, name: &str) -> Option<Box<dyn Quantity>> {
1399        let idx = self.quantities.iter().position(|q| q.name() == name)?;
1400        Some(self.quantities.remove(idx))
1401    }
1402
1403    fn quantities(&self) -> &[Box<dyn Quantity>] {
1404        &self.quantities
1405    }
1406}
1407
1408use std::collections::HashMap;
1409
1410/// Generates a canonical (sorted) face key for hashing.
1411/// For triangular faces, the fourth element is `u32::MAX`.
1412fn canonical_face_key(v0: u32, v1: u32, v2: u32, v3: Option<u32>) -> [u32; 4] {
1413    let mut key = [v0, v1, v2, v3.unwrap_or(u32::MAX)];
1414    key.sort_unstable();
1415    key
1416}
1417
1418/// Face stencil for tetrahedra: 4 triangular faces
1419const TET_FACE_STENCIL: [[usize; 3]; 4] = [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]];
1420
1421/// Face stencil for hexahedra: 6 quad faces (each as 2 triangles sharing diagonal)
1422const HEX_FACE_STENCIL: [[[usize; 3]; 2]; 6] = [
1423    [[2, 1, 0], [2, 0, 3]], // Bottom
1424    [[4, 0, 1], [4, 1, 5]], // Front
1425    [[5, 1, 2], [5, 2, 6]], // Right
1426    [[7, 3, 0], [7, 0, 4]], // Left
1427    [[6, 2, 3], [6, 3, 7]], // Back
1428    [[7, 4, 5], [7, 5, 6]], // Top
1429];
1430
1431/// Render geometry data with optional quantity values.
1432pub struct VolumeMeshRenderGeometry {
1433    pub positions: Vec<Vec3>,
1434    pub faces: Vec<[u32; 3]>,
1435    pub normals: Vec<Vec3>,
1436    /// Per-vertex scalar values for color mapping (from enabled vertex scalar quantity).
1437    pub vertex_values: Option<Vec<f32>>,
1438    /// Per-vertex colors (from enabled vertex color quantity).
1439    pub vertex_colors: Option<Vec<Vec3>>,
1440}
1441
1442/// Diagonal decomposition patterns (5 tets).
1443const HEX_TO_TET_PATTERN: [[usize; 4]; 5] = [
1444    [0, 1, 2, 5],
1445    [0, 2, 7, 5],
1446    [0, 2, 3, 7],
1447    [0, 5, 7, 4],
1448    [2, 7, 5, 6],
1449];
1450
1451#[cfg(test)]
1452mod tests {
1453    use super::*;
1454
1455    #[test]
1456    fn test_interior_face_detection() {
1457        // Two tets sharing a face
1458        let vertices = vec![
1459            Vec3::new(0.0, 0.0, 0.0),  // 0
1460            Vec3::new(1.0, 0.0, 0.0),  // 1
1461            Vec3::new(0.5, 1.0, 0.0),  // 2
1462            Vec3::new(0.5, 0.5, 1.0),  // 3 - apex of first tet
1463            Vec3::new(0.5, 0.5, -1.0), // 4 - apex of second tet
1464        ];
1465        // Two tets sharing face [0,1,2]
1466        let tets = vec![[0, 1, 2, 3], [0, 2, 1, 4]];
1467        let mesh = VolumeMesh::new_tet_mesh("test", vertices, tets);
1468
1469        // Should have 6 exterior faces (4 per tet - 1 shared = 3 per tet * 2 = 6)
1470        // Not 8 faces (4 per tet * 2 = 8)
1471        let (_, faces) = mesh.generate_render_geometry();
1472        assert_eq!(faces.len(), 6, "Should only render exterior faces");
1473    }
1474
1475    #[test]
1476    fn test_single_tet_all_exterior() {
1477        let vertices = vec![
1478            Vec3::new(0.0, 0.0, 0.0),
1479            Vec3::new(1.0, 0.0, 0.0),
1480            Vec3::new(0.5, 1.0, 0.0),
1481            Vec3::new(0.5, 0.5, 1.0),
1482        ];
1483        let tets = vec![[0, 1, 2, 3]];
1484        let mesh = VolumeMesh::new_tet_mesh("test", vertices, tets);
1485
1486        let (_, faces) = mesh.generate_render_geometry();
1487        assert_eq!(faces.len(), 4, "Single tet should have 4 exterior faces");
1488    }
1489
1490    #[test]
1491    fn test_single_hex_all_exterior() {
1492        let vertices = vec![
1493            Vec3::new(0.0, 0.0, 0.0),
1494            Vec3::new(1.0, 0.0, 0.0),
1495            Vec3::new(1.0, 1.0, 0.0),
1496            Vec3::new(0.0, 1.0, 0.0),
1497            Vec3::new(0.0, 0.0, 1.0),
1498            Vec3::new(1.0, 0.0, 1.0),
1499            Vec3::new(1.0, 1.0, 1.0),
1500            Vec3::new(0.0, 1.0, 1.0),
1501        ];
1502        let hexes = vec![[0, 1, 2, 3, 4, 5, 6, 7]];
1503        let mesh = VolumeMesh::new_hex_mesh("test", vertices, hexes);
1504
1505        let (_, faces) = mesh.generate_render_geometry();
1506        // 6 quad faces * 2 triangles each = 12 triangles
1507        assert_eq!(
1508            faces.len(),
1509            12,
1510            "Single hex should have 12 triangles (6 quads)"
1511        );
1512    }
1513
1514    #[test]
1515    fn test_hex_to_tet_decomposition() {
1516        let vertices = vec![
1517            Vec3::new(0.0, 0.0, 0.0),
1518            Vec3::new(1.0, 0.0, 0.0),
1519            Vec3::new(1.0, 1.0, 0.0),
1520            Vec3::new(0.0, 1.0, 0.0),
1521            Vec3::new(0.0, 0.0, 1.0),
1522            Vec3::new(1.0, 0.0, 1.0),
1523            Vec3::new(1.0, 1.0, 1.0),
1524            Vec3::new(0.0, 1.0, 1.0),
1525        ];
1526        let hexes = vec![[0, 1, 2, 3, 4, 5, 6, 7]];
1527        let mesh = VolumeMesh::new_hex_mesh("test", vertices, hexes);
1528
1529        let tets = mesh.decompose_to_tets();
1530        // A hex is decomposed into 5 tets
1531        assert_eq!(tets.len(), 5);
1532
1533        // Each tet should have 4 vertices
1534        for tet in &tets {
1535            assert!(tet[0] < 8);
1536            assert!(tet[1] < 8);
1537            assert!(tet[2] < 8);
1538            assert!(tet[3] < 8);
1539        }
1540    }
1541
1542    #[test]
1543    fn test_quantity_aware_geometry_generation() {
1544        let vertices = vec![
1545            Vec3::new(0.0, 0.0, 0.0),
1546            Vec3::new(1.0, 0.0, 0.0),
1547            Vec3::new(0.5, 1.0, 0.0),
1548            Vec3::new(0.5, 0.5, 1.0),
1549        ];
1550        let tets = vec![[0, 1, 2, 3]];
1551        let mut mesh = VolumeMesh::new_tet_mesh("test", vertices, tets);
1552
1553        // Add vertex scalar quantity
1554        mesh.add_vertex_scalar_quantity("temp", vec![0.0, 0.5, 1.0, 0.25]);
1555
1556        // Get the quantity and enable it
1557        if let Some(q) = mesh.get_quantity_mut("temp") {
1558            q.set_enabled(true);
1559        }
1560
1561        // Generate geometry should include scalar values for color mapping
1562        let render_data = mesh.generate_render_geometry_with_quantities();
1563        assert!(render_data.vertex_values.is_some());
1564        assert_eq!(
1565            render_data.vertex_values.as_ref().unwrap().len(),
1566            render_data.positions.len()
1567        );
1568    }
1569}