Skip to main content

runmat_plot/plots/
mesh.rs

1//! Indexed triangle mesh plot primitive.
2
3use crate::core::{AlphaMode, BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
4use glam::{Vec3, Vec4};
5use std::borrow::Cow;
6use std::collections::{BTreeMap, BTreeSet};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct MeshTriangleRange {
10    pub start: u32,
11    pub count: u32,
12}
13
14impl MeshTriangleRange {
15    pub fn new(start: u32, count: u32) -> Self {
16        Self { start, count }
17    }
18
19    pub fn end_exclusive(&self) -> Option<u32> {
20        self.start.checked_add(self.count)
21    }
22
23    pub fn contains(&self, triangle_index: u32) -> bool {
24        self.end_exclusive()
25            .is_some_and(|end| triangle_index >= self.start && triangle_index < end)
26    }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct MeshRegion {
31    pub region_id: String,
32    pub label: Option<String>,
33    pub tag: Option<String>,
34    pub triangle_ranges: Vec<MeshTriangleRange>,
35}
36
37impl MeshRegion {
38    pub fn new(
39        region_id: impl Into<String>,
40        label: Option<String>,
41        tag: Option<String>,
42        triangle_ranges: Vec<MeshTriangleRange>,
43    ) -> Self {
44        Self {
45            region_id: region_id.into(),
46            label,
47            tag,
48            triangle_ranges,
49        }
50    }
51
52    pub fn contains_triangle(&self, triangle_index: u32) -> bool {
53        self.triangle_ranges
54            .iter()
55            .any(|range| range.contains(triangle_index))
56    }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum MeshFieldLocation {
61    Vertex,
62    Triangle,
63}
64
65impl MeshFieldLocation {
66    pub fn as_str(self) -> &'static str {
67        match self {
68            Self::Vertex => "vertex",
69            Self::Triangle => "triangle",
70        }
71    }
72
73    pub fn parse(value: &str) -> Option<Self> {
74        match value {
75            "vertex" | "vertices" | "node" | "nodes" => Some(Self::Vertex),
76            "triangle" | "triangles" | "face" | "faces" | "element" | "elements" => {
77                Some(Self::Triangle)
78            }
79            _ => None,
80        }
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum MeshEdgeMode {
86    #[default]
87    All,
88    Feature,
89    None,
90}
91
92impl MeshEdgeMode {
93    pub fn as_str(self) -> &'static str {
94        match self {
95            Self::All => "all",
96            Self::Feature => "feature",
97            Self::None => "none",
98        }
99    }
100
101    pub fn parse(value: &str) -> Option<Self> {
102        match value {
103            "all" | "triangle" | "triangles" => Some(Self::All),
104            "feature" | "features" | "boundary" | "boundaries" | "region" | "regions" => {
105                Some(Self::Feature)
106            }
107            "none" | "off" => Some(Self::None),
108            _ => None,
109        }
110    }
111}
112
113#[derive(Debug, Clone, PartialEq)]
114pub struct MeshScalarField {
115    pub field_id: String,
116    pub label: Option<String>,
117    pub location: MeshFieldLocation,
118    pub values: Vec<f32>,
119    pub color_limits: Option<[f32; 2]>,
120    pub colormap: String,
121    pub alpha: f32,
122}
123
124impl MeshScalarField {
125    pub fn new(field_id: impl Into<String>, location: MeshFieldLocation, values: Vec<f32>) -> Self {
126        Self {
127            field_id: field_id.into(),
128            label: None,
129            location,
130            values,
131            color_limits: None,
132            colormap: "viridis".to_string(),
133            alpha: 1.0,
134        }
135    }
136}
137
138#[derive(Debug, Clone, PartialEq)]
139pub struct MeshVectorField {
140    pub field_id: String,
141    pub label: Option<String>,
142    pub location: MeshFieldLocation,
143    pub vectors: Vec<Vec3>,
144    pub scale: f32,
145    pub stride: usize,
146    pub color: Vec4,
147}
148
149impl MeshVectorField {
150    pub fn new(
151        field_id: impl Into<String>,
152        location: MeshFieldLocation,
153        vectors: Vec<Vec3>,
154    ) -> Self {
155        Self {
156            field_id: field_id.into(),
157            label: None,
158            location,
159            vectors,
160            scale: 1.0,
161            stride: 1,
162            color: Vec4::new(0.95, 0.86, 0.28, 1.0),
163        }
164    }
165}
166
167#[derive(Debug, Clone, PartialEq)]
168pub struct MeshDeformation {
169    pub field_id: String,
170    pub label: Option<String>,
171    pub displacements: Vec<Vec3>,
172    pub scale: f32,
173}
174
175impl MeshDeformation {
176    pub fn new(field_id: impl Into<String>, displacements: Vec<Vec3>) -> Self {
177        Self {
178            field_id: field_id.into(),
179            label: None,
180            displacements,
181            scale: 1.0,
182        }
183    }
184}
185
186#[derive(Debug, Clone)]
187pub struct MeshPlot {
188    mesh_id: Option<String>,
189    vertices: Vec<Vec3>,
190    triangles: Vec<[u32; 3]>,
191    face_color: Vec4,
192    edge_color: Vec4,
193    face_alpha: f32,
194    edge_alpha: f32,
195    edge_width: f32,
196    edge_mode: MeshEdgeMode,
197    feature_edge_groups: Option<Vec<u64>>,
198    vertex_colors: Option<Vec<Vec4>>,
199    triangle_colors: Option<Vec<Vec4>>,
200    label: Option<String>,
201    regions: Vec<MeshRegion>,
202    highlighted_region_id: Option<String>,
203    highlight_color: Vec4,
204    scalar_field: Option<MeshScalarField>,
205    vector_field: Option<MeshVectorField>,
206    deformation: Option<MeshDeformation>,
207    visible: bool,
208    bounds: Option<BoundingBox>,
209    face_vertices: Option<Vec<Vertex>>,
210    face_indices: Option<Vec<u32>>,
211    edge_vertices: Option<Vec<Vertex>>,
212    vector_vertices: Option<Vec<Vertex>>,
213    dirty: bool,
214}
215
216impl MeshPlot {
217    pub fn new(vertices: Vec<Vec3>, triangles: Vec<[u32; 3]>) -> Result<Self, String> {
218        validate_mesh(&vertices, &triangles)?;
219        Ok(Self {
220            mesh_id: None,
221            vertices,
222            triangles,
223            face_color: Vec4::new(0.18, 0.48, 0.86, 1.0),
224            edge_color: Vec4::new(0.78, 0.86, 0.96, 1.0),
225            face_alpha: 1.0,
226            edge_alpha: 0.65,
227            edge_width: 0.5,
228            edge_mode: MeshEdgeMode::All,
229            feature_edge_groups: None,
230            vertex_colors: None,
231            triangle_colors: None,
232            label: None,
233            regions: Vec::new(),
234            highlighted_region_id: None,
235            highlight_color: Vec4::new(0.98, 0.78, 0.22, 1.0),
236            scalar_field: None,
237            vector_field: None,
238            deformation: None,
239            visible: true,
240            bounds: None,
241            face_vertices: None,
242            face_indices: None,
243            edge_vertices: None,
244            vector_vertices: None,
245            dirty: true,
246        })
247    }
248
249    pub fn mesh_id(&self) -> Option<&str> {
250        self.mesh_id.as_deref()
251    }
252
253    pub fn vertices(&self) -> &[Vec3] {
254        &self.vertices
255    }
256
257    pub fn triangles(&self) -> &[[u32; 3]] {
258        &self.triangles
259    }
260
261    pub fn face_color(&self) -> Vec4 {
262        self.face_color
263    }
264
265    pub fn edge_color(&self) -> Vec4 {
266        self.edge_color
267    }
268
269    pub fn face_alpha(&self) -> f32 {
270        self.face_alpha
271    }
272
273    pub fn edge_alpha(&self) -> f32 {
274        self.edge_alpha
275    }
276
277    pub fn edge_width(&self) -> f32 {
278        self.edge_width
279    }
280
281    pub fn edge_mode(&self) -> MeshEdgeMode {
282        self.edge_mode
283    }
284
285    pub fn feature_edge_groups(&self) -> Option<&[u64]> {
286        self.feature_edge_groups.as_deref()
287    }
288
289    pub fn triangle_colors(&self) -> Option<&[Vec4]> {
290        self.triangle_colors.as_deref()
291    }
292
293    pub fn vertex_colors(&self) -> Option<&[Vec4]> {
294        self.vertex_colors.as_deref()
295    }
296
297    pub fn label(&self) -> Option<&str> {
298        self.label.as_deref()
299    }
300
301    pub fn regions(&self) -> &[MeshRegion] {
302        &self.regions
303    }
304
305    pub fn highlighted_region_id(&self) -> Option<&str> {
306        self.highlighted_region_id.as_deref()
307    }
308
309    pub fn highlight_color(&self) -> Vec4 {
310        self.highlight_color
311    }
312
313    pub fn scalar_field(&self) -> Option<&MeshScalarField> {
314        self.scalar_field.as_ref()
315    }
316
317    pub fn vector_field(&self) -> Option<&MeshVectorField> {
318        self.vector_field.as_ref()
319    }
320
321    pub fn deformation(&self) -> Option<&MeshDeformation> {
322        self.deformation.as_ref()
323    }
324
325    pub fn is_visible(&self) -> bool {
326        self.visible
327    }
328
329    pub fn set_mesh_id(&mut self, mesh_id: Option<String>) {
330        self.mesh_id = mesh_id;
331    }
332
333    pub fn set_vertices(&mut self, vertices: Vec<Vec3>) -> Result<(), String> {
334        validate_mesh(&vertices, &self.triangles)?;
335        self.vertices = vertices;
336        self.clear_invalid_vertex_metadata();
337        self.mark_dirty();
338        Ok(())
339    }
340
341    pub fn set_triangles(&mut self, triangles: Vec<[u32; 3]>) -> Result<(), String> {
342        validate_mesh(&self.vertices, &triangles)?;
343        self.triangles = triangles;
344        self.clear_invalid_triangle_metadata();
345        self.mark_dirty();
346        Ok(())
347    }
348
349    pub fn set_face_color(&mut self, color: Vec4) {
350        self.face_color = sanitize_color(color);
351        self.mark_dirty();
352    }
353
354    pub fn set_edge_color(&mut self, color: Vec4) {
355        self.edge_color = sanitize_color(color);
356        self.mark_dirty();
357    }
358
359    pub fn set_face_alpha(&mut self, alpha: f32) {
360        self.face_alpha = sanitize_alpha(alpha);
361        self.mark_dirty();
362    }
363
364    pub fn set_edge_alpha(&mut self, alpha: f32) {
365        self.edge_alpha = sanitize_alpha(alpha);
366        self.mark_dirty();
367    }
368
369    pub fn set_edge_width(&mut self, edge_width: f32) {
370        self.edge_width = if edge_width.is_finite() {
371            edge_width.max(0.0)
372        } else {
373            0.0
374        };
375        self.mark_dirty();
376    }
377
378    pub fn set_edge_mode(&mut self, edge_mode: MeshEdgeMode) {
379        self.edge_mode = edge_mode;
380        self.mark_dirty();
381    }
382
383    pub fn set_feature_edge_groups(&mut self, groups: Option<Vec<u64>>) -> Result<(), String> {
384        if let Some(groups) = groups.as_ref() {
385            if groups.len() != self.triangles.len() {
386                return Err(format!(
387                    "mesh feature edge groups: group count must match triangle count ({})",
388                    self.triangles.len()
389                ));
390            }
391        }
392        self.feature_edge_groups = groups;
393        self.mark_dirty();
394        Ok(())
395    }
396
397    pub fn set_vertex_colors(&mut self, colors: Option<Vec<Vec4>>) -> Result<(), String> {
398        if let Some(colors) = colors.as_ref() {
399            if colors.len() != self.vertices.len() {
400                return Err(format!(
401                    "mesh vertex colors: color count must match vertex count ({})",
402                    self.vertices.len()
403                ));
404            }
405        }
406        self.vertex_colors = colors.map(|colors| colors.into_iter().map(sanitize_color).collect());
407        self.mark_dirty();
408        Ok(())
409    }
410
411    pub fn set_triangle_colors(&mut self, colors: Option<Vec<Vec4>>) -> Result<(), String> {
412        if let Some(colors) = colors.as_ref() {
413            if colors.len() != self.triangles.len() {
414                return Err(format!(
415                    "mesh triangle colors: color count must match triangle count ({})",
416                    self.triangles.len()
417                ));
418            }
419        }
420        self.triangle_colors =
421            colors.map(|colors| colors.into_iter().map(sanitize_color).collect());
422        self.mark_dirty();
423        Ok(())
424    }
425
426    pub fn set_label(&mut self, label: Option<String>) {
427        self.label = label;
428    }
429
430    pub fn set_regions(&mut self, regions: Vec<MeshRegion>) {
431        self.regions = regions;
432        self.mark_dirty();
433    }
434
435    pub fn set_highlighted_region_id(&mut self, region_id: Option<String>) {
436        self.highlighted_region_id = region_id;
437        self.mark_dirty();
438    }
439
440    pub fn set_highlight_color(&mut self, color: Vec4) {
441        self.highlight_color = sanitize_color(color);
442        self.mark_dirty();
443    }
444
445    pub fn set_scalar_field(&mut self, field: Option<MeshScalarField>) -> Result<(), String> {
446        if let Some(field) = field.as_ref() {
447            validate_scalar_field(field, self.vertices.len(), self.triangles.len())?;
448        }
449        self.scalar_field = field.map(sanitize_scalar_field);
450        self.mark_dirty();
451        Ok(())
452    }
453
454    pub fn set_vector_field(&mut self, field: Option<MeshVectorField>) -> Result<(), String> {
455        if let Some(field) = field.as_ref() {
456            validate_vector_field(field, self.vertices.len(), self.triangles.len())?;
457        }
458        self.vector_field = field.map(sanitize_vector_field);
459        self.mark_dirty();
460        Ok(())
461    }
462
463    pub fn set_deformation(&mut self, deformation: Option<MeshDeformation>) -> Result<(), String> {
464        if let Some(deformation) = deformation.as_ref() {
465            validate_deformation(deformation, self.vertices.len())?;
466        }
467        self.deformation = deformation.map(sanitize_deformation);
468        self.mark_dirty();
469        Ok(())
470    }
471
472    pub fn region_for_triangle(&self, triangle_index: u32) -> Option<&MeshRegion> {
473        self.regions
474            .iter()
475            .find(|region| region.contains_triangle(triangle_index))
476    }
477
478    pub fn set_visible(&mut self, visible: bool) {
479        self.visible = visible;
480    }
481
482    pub fn mark_dirty(&mut self) {
483        self.dirty = true;
484        self.bounds = None;
485        self.face_vertices = None;
486        self.face_indices = None;
487        self.edge_vertices = None;
488        self.vector_vertices = None;
489    }
490
491    pub fn effective_face_color(&self) -> Vec4 {
492        let mut color = self.face_color;
493        color.w *= self.face_alpha.clamp(0.0, 1.0);
494        color
495    }
496
497    pub fn effective_edge_color(&self) -> Vec4 {
498        let mut color = self.edge_color;
499        color.w *= self.edge_alpha.clamp(0.0, 1.0);
500        color
501    }
502
503    pub fn bounds(&mut self) -> BoundingBox {
504        if self.dirty || self.bounds.is_none() {
505            let vertices = self.effective_vertices();
506            self.bounds = Some(BoundingBox::from_points(&vertices));
507        }
508        self.bounds.unwrap()
509    }
510
511    pub fn render_data(&mut self) -> RenderData {
512        let bounds = self.bounds();
513        let (vertices, indices) = {
514            let (vertices, indices) = self.generate_face_geometry();
515            (vertices.clone(), indices.clone())
516        };
517        let color = self.effective_face_color();
518        let vertex_count = vertices.len();
519        let index_count = indices.len();
520        RenderData {
521            pipeline_type: PipelineType::Triangles,
522            vertices,
523            indices: Some(indices),
524            gpu_vertices: None,
525            bounds: Some(bounds),
526            material: Material {
527                albedo: color,
528                alpha_mode: if color.w < 1.0 {
529                    AlphaMode::Blend
530                } else {
531                    AlphaMode::Opaque
532                },
533                double_sided: true,
534                ..Default::default()
535            },
536            draw_calls: vec![DrawCall {
537                vertex_offset: 0,
538                vertex_count,
539                index_offset: Some(0),
540                index_count: Some(index_count),
541                instance_count: 1,
542            }],
543            image: None,
544        }
545    }
546
547    pub fn edge_render_data(&mut self) -> Option<RenderData> {
548        let bounds = self.bounds();
549        if self.edge_width <= 0.0
550            || self.edge_alpha <= 0.0
551            || matches!(self.edge_mode, MeshEdgeMode::None)
552        {
553            return None;
554        }
555        if self.dirty || self.edge_vertices.is_none() {
556            let color = self.effective_edge_color();
557            let vertices_source = self.effective_vertices();
558            let edges = self.renderable_edges();
559            let mut vertices = Vec::with_capacity(edges.len() * 2);
560            for (a, b) in edges {
561                vertices.push(Vertex::new(vertices_source[a as usize], color));
562                vertices.push(Vertex::new(vertices_source[b as usize], color));
563            }
564            self.edge_vertices = Some(vertices);
565        }
566        let vertices = self.edge_vertices.as_ref()?.clone();
567        if vertices.is_empty() {
568            return None;
569        }
570        let vertex_count = vertices.len();
571        Some(RenderData {
572            pipeline_type: PipelineType::Lines,
573            vertices,
574            indices: None,
575            gpu_vertices: None,
576            bounds: Some(bounds),
577            material: Material {
578                albedo: self.effective_edge_color(),
579                roughness: self.edge_width.max(0.1),
580                alpha_mode: if self.edge_alpha < 1.0 {
581                    AlphaMode::Blend
582                } else {
583                    AlphaMode::Opaque
584                },
585                ..Default::default()
586            },
587            draw_calls: vec![DrawCall {
588                vertex_offset: 0,
589                vertex_count,
590                index_offset: None,
591                index_count: None,
592                instance_count: 1,
593            }],
594            image: None,
595        })
596    }
597
598    pub fn vector_render_data(&mut self) -> Option<RenderData> {
599        let bounds = self.bounds();
600        if self.dirty || self.vector_vertices.is_none() {
601            let vertices_source = self.effective_vertices();
602            let field = self.vector_field.as_ref()?;
603            let color = sanitize_color(field.color);
604            let stride = field.stride.max(1);
605            let mut vertices = Vec::new();
606            match field.location {
607                MeshFieldLocation::Vertex => {
608                    for (index, vector) in field.vectors.iter().enumerate().step_by(stride) {
609                        if vector.length_squared() <= f32::EPSILON {
610                            continue;
611                        }
612                        let start = vertices_source[index];
613                        vertices.push(Vertex::new(start, color));
614                        vertices.push(Vertex::new(start + *vector * field.scale, color));
615                    }
616                }
617                MeshFieldLocation::Triangle => {
618                    for (index, vector) in field.vectors.iter().enumerate().step_by(stride) {
619                        if vector.length_squared() <= f32::EPSILON {
620                            continue;
621                        }
622                        let triangle = self.triangles[index];
623                        let start = (vertices_source[triangle[0] as usize]
624                            + vertices_source[triangle[1] as usize]
625                            + vertices_source[triangle[2] as usize])
626                            / 3.0;
627                        vertices.push(Vertex::new(start, color));
628                        vertices.push(Vertex::new(start + *vector * field.scale, color));
629                    }
630                }
631            }
632            self.vector_vertices = Some(vertices);
633        }
634        let vertices = self.vector_vertices.as_ref()?.clone();
635        if vertices.is_empty() {
636            return None;
637        }
638        let vertex_count = vertices.len();
639        Some(RenderData {
640            pipeline_type: PipelineType::Lines,
641            vertices,
642            indices: None,
643            gpu_vertices: None,
644            bounds: Some(bounds),
645            material: Material {
646                albedo: self
647                    .vector_field
648                    .as_ref()
649                    .map(|field| sanitize_color(field.color))
650                    .unwrap_or(Vec4::ONE),
651                alpha_mode: if self
652                    .vector_field
653                    .as_ref()
654                    .map(|field| field.color.w < 1.0)
655                    .unwrap_or(false)
656                {
657                    AlphaMode::Blend
658                } else {
659                    AlphaMode::Opaque
660                },
661                ..Default::default()
662            },
663            draw_calls: vec![DrawCall {
664                vertex_offset: 0,
665                vertex_count,
666                index_offset: None,
667                index_count: None,
668                instance_count: 1,
669            }],
670            image: None,
671        })
672    }
673
674    pub fn estimated_memory_usage(&self) -> usize {
675        self.vertices.len() * std::mem::size_of::<Vec3>()
676            + self.triangles.len() * std::mem::size_of::<[u32; 3]>()
677            + self
678                .face_vertices
679                .as_ref()
680                .map_or(0, |vertices| vertices.len() * std::mem::size_of::<Vertex>())
681            + self
682                .face_indices
683                .as_ref()
684                .map_or(0, |indices| indices.len() * std::mem::size_of::<u32>())
685            + self
686                .edge_vertices
687                .as_ref()
688                .map_or(0, |vertices| vertices.len() * std::mem::size_of::<Vertex>())
689            + self
690                .feature_edge_groups
691                .as_ref()
692                .map_or(0, |groups| groups.len() * std::mem::size_of::<u64>())
693            + self
694                .vertex_colors
695                .as_ref()
696                .map_or(0, |colors| colors.len() * std::mem::size_of::<Vec4>())
697            + self
698                .triangle_colors
699                .as_ref()
700                .map_or(0, |colors| colors.len() * std::mem::size_of::<Vec4>())
701            + self
702                .vector_vertices
703                .as_ref()
704                .map_or(0, |vertices| vertices.len() * std::mem::size_of::<Vertex>())
705            + self
706                .scalar_field
707                .as_ref()
708                .map_or(0, |field| field.values.len() * std::mem::size_of::<f32>())
709            + self
710                .vector_field
711                .as_ref()
712                .map_or(0, |field| field.vectors.len() * std::mem::size_of::<Vec3>())
713            + self.deformation.as_ref().map_or(0, |field| {
714                field.displacements.len() * std::mem::size_of::<Vec3>()
715            })
716    }
717
718    fn generate_face_geometry(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
719        if self.dirty || self.face_vertices.is_none() || self.face_indices.is_none() {
720            let color = self.effective_face_color();
721            let vertices_source = self.effective_vertices();
722            let normals = vertex_normals(&vertices_source, &self.triangles);
723            let scalar_limits = self.scalar_field.as_ref().and_then(scalar_limits);
724            let (vertices, indices) = if self.highlighted_region_id.is_some()
725                || self
726                    .scalar_field
727                    .as_ref()
728                    .is_some_and(|field| field.location == MeshFieldLocation::Triangle)
729                || self.triangle_colors.is_some()
730            {
731                let highlight_color = self.highlight_color;
732                let highlighted_region_id = self.highlighted_region_id.as_deref();
733                let highlighted_region = self
734                    .regions
735                    .iter()
736                    .find(|region| Some(region.region_id.as_str()) == highlighted_region_id);
737                let mut vertices = Vec::with_capacity(self.triangles.len() * 3);
738                let mut indices = Vec::with_capacity(self.triangles.len() * 3);
739                for (triangle_index, triangle) in self.triangles.iter().enumerate() {
740                    let region_highlighted = if highlighted_region
741                        .is_some_and(|region| region.contains_triangle(triangle_index as u32))
742                    {
743                        Some(highlight_color)
744                    } else {
745                        None
746                    };
747                    for vertex_id in triangle {
748                        let source_index = *vertex_id as usize;
749                        let target_index = vertices.len() as u32;
750                        let vertex_color = region_highlighted.unwrap_or_else(|| {
751                            self.scalar_color(source_index, triangle_index, scalar_limits)
752                                .or_else(|| self.triangle_color(triangle_index))
753                                .or_else(|| self.vertex_color(source_index))
754                                .unwrap_or(color)
755                        });
756                        vertices.push(Vertex {
757                            position: vertices_source[source_index].to_array(),
758                            color: vertex_color.to_array(),
759                            normal: normals[source_index].to_array(),
760                            tex_coords: [0.0, 0.0],
761                        });
762                        indices.push(target_index);
763                    }
764                }
765                (vertices, indices)
766            } else {
767                let vertices = vertices_source
768                    .iter()
769                    .zip(normals.iter())
770                    .enumerate()
771                    .map(|(source_index, (&position, &normal))| {
772                        let vertex_color = self
773                            .scalar_color(source_index, 0, scalar_limits)
774                            .or_else(|| self.vertex_color(source_index))
775                            .unwrap_or(color);
776                        Vertex {
777                            position: position.to_array(),
778                            color: vertex_color.to_array(),
779                            normal: normal.to_array(),
780                            tex_coords: [0.0, 0.0],
781                        }
782                    })
783                    .collect();
784                let indices = self
785                    .triangles
786                    .iter()
787                    .flat_map(|triangle| triangle.iter().copied())
788                    .collect();
789                (vertices, indices)
790            };
791            self.face_vertices = Some(vertices);
792            self.face_indices = Some(indices);
793            self.dirty = false;
794        }
795        (
796            self.face_vertices.as_ref().unwrap(),
797            self.face_indices.as_ref().unwrap(),
798        )
799    }
800
801    fn effective_vertices(&self) -> Cow<'_, [Vec3]> {
802        if let Some(deformation) = self.deformation.as_ref() {
803            Cow::Owned(
804                self.vertices
805                    .iter()
806                    .zip(deformation.displacements.iter())
807                    .map(|(&position, &displacement)| position + displacement * deformation.scale)
808                    .collect(),
809            )
810        } else {
811            Cow::Borrowed(&self.vertices)
812        }
813    }
814
815    fn scalar_color(
816        &self,
817        vertex_index: usize,
818        triangle_index: usize,
819        limits: Option<[f32; 2]>,
820    ) -> Option<Vec4> {
821        let field = self.scalar_field.as_ref()?;
822        let value = match field.location {
823            MeshFieldLocation::Vertex => *field.values.get(vertex_index)?,
824            MeshFieldLocation::Triangle => *field.values.get(triangle_index)?,
825        };
826        if !value.is_finite() {
827            return None;
828        }
829        let [min, max] = limits?;
830        let t = if max > min {
831            ((value - min) / (max - min)).clamp(0.0, 1.0)
832        } else {
833            0.5
834        };
835        Some(colormap_color(
836            &field.colormap,
837            t,
838            field.alpha * self.face_alpha,
839        ))
840    }
841
842    fn triangle_color(&self, triangle_index: usize) -> Option<Vec4> {
843        let mut color = *self.triangle_colors.as_ref()?.get(triangle_index)?;
844        color.w *= self.face_alpha.clamp(0.0, 1.0);
845        Some(color)
846    }
847
848    fn vertex_color(&self, vertex_index: usize) -> Option<Vec4> {
849        let mut color = *self.vertex_colors.as_ref()?.get(vertex_index)?;
850        color.w *= self.face_alpha.clamp(0.0, 1.0);
851        Some(color)
852    }
853
854    fn renderable_edges(&self) -> Vec<(u32, u32)> {
855        match self.edge_mode {
856            MeshEdgeMode::All => self.all_triangle_edges(),
857            MeshEdgeMode::Feature => self.feature_edges(),
858            MeshEdgeMode::None => Vec::new(),
859        }
860    }
861
862    fn all_triangle_edges(&self) -> Vec<(u32, u32)> {
863        let mut edges = BTreeSet::<(u32, u32)>::new();
864        for &[a, b, c] in &self.triangles {
865            edges.insert(normalized_edge(a, b));
866            edges.insert(normalized_edge(b, c));
867            edges.insert(normalized_edge(c, a));
868        }
869        edges.into_iter().collect()
870    }
871
872    fn feature_edges(&self) -> Vec<(u32, u32)> {
873        let groups = self.feature_edge_groups.as_deref();
874        let mut edges = BTreeMap::<(u32, u32), FeatureEdgeAccumulator>::new();
875        for (triangle_index, &[a, b, c]) in self.triangles.iter().enumerate() {
876            let group = groups
877                .and_then(|groups| groups.get(triangle_index).copied())
878                .unwrap_or(0);
879            accumulate_feature_edge(&mut edges, a, b, group);
880            accumulate_feature_edge(&mut edges, b, c, group);
881            accumulate_feature_edge(&mut edges, c, a, group);
882        }
883        edges
884            .into_iter()
885            .filter_map(|(edge, accumulator)| {
886                (accumulator.count != 2 || accumulator.crosses_group).then_some(edge)
887            })
888            .collect()
889    }
890
891    fn clear_invalid_triangle_metadata(&mut self) {
892        if self
893            .feature_edge_groups
894            .as_ref()
895            .is_some_and(|groups| groups.len() != self.triangles.len())
896        {
897            self.feature_edge_groups = None;
898        }
899        if self
900            .triangle_colors
901            .as_ref()
902            .is_some_and(|colors| colors.len() != self.triangles.len())
903        {
904            self.triangle_colors = None;
905        }
906    }
907
908    fn clear_invalid_vertex_metadata(&mut self) {
909        if self
910            .vertex_colors
911            .as_ref()
912            .is_some_and(|colors| colors.len() != self.vertices.len())
913        {
914            self.vertex_colors = None;
915        }
916    }
917}
918
919fn validate_mesh(vertices: &[Vec3], triangles: &[[u32; 3]]) -> Result<(), String> {
920    if vertices.is_empty() {
921        return Err("mesh: vertices must not be empty".to_string());
922    }
923    if triangles.is_empty() {
924        return Err("mesh: triangles must not be empty".to_string());
925    }
926    if vertices
927        .iter()
928        .any(|vertex| !vertex.x.is_finite() || !vertex.y.is_finite() || !vertex.z.is_finite())
929    {
930        return Err("mesh: vertices must contain finite coordinates".to_string());
931    }
932    let vertex_count = vertices.len();
933    for triangle in triangles {
934        if triangle.iter().any(|index| *index as usize >= vertex_count) {
935            return Err("mesh: triangle index exceeds vertex count".to_string());
936        }
937    }
938    Ok(())
939}
940
941fn validate_scalar_field(
942    field: &MeshScalarField,
943    vertex_count: usize,
944    triangle_count: usize,
945) -> Result<(), String> {
946    if field.field_id.trim().is_empty() {
947        return Err("mesh scalar field: field_id must not be empty".to_string());
948    }
949    validate_field_len(
950        "mesh scalar field",
951        field.location,
952        field.values.len(),
953        vertex_count,
954        triangle_count,
955    )?;
956    if field
957        .color_limits
958        .is_some_and(|[min, max]| !min.is_finite() || !max.is_finite() || max < min)
959    {
960        return Err("mesh scalar field: color_limits must be finite and ordered".to_string());
961    }
962    Ok(())
963}
964
965fn validate_vector_field(
966    field: &MeshVectorField,
967    vertex_count: usize,
968    triangle_count: usize,
969) -> Result<(), String> {
970    if field.field_id.trim().is_empty() {
971        return Err("mesh vector field: field_id must not be empty".to_string());
972    }
973    validate_field_len(
974        "mesh vector field",
975        field.location,
976        field.vectors.len(),
977        vertex_count,
978        triangle_count,
979    )?;
980    if field
981        .vectors
982        .iter()
983        .any(|vector| !vector.x.is_finite() || !vector.y.is_finite() || !vector.z.is_finite())
984    {
985        return Err("mesh vector field: vectors must contain finite components".to_string());
986    }
987    Ok(())
988}
989
990fn validate_deformation(deformation: &MeshDeformation, vertex_count: usize) -> Result<(), String> {
991    if deformation.field_id.trim().is_empty() {
992        return Err("mesh deformation: field_id must not be empty".to_string());
993    }
994    if deformation.displacements.len() != vertex_count {
995        return Err("mesh deformation: displacements must match vertex count".to_string());
996    }
997    if deformation.displacements.iter().any(|displacement| {
998        !displacement.x.is_finite() || !displacement.y.is_finite() || !displacement.z.is_finite()
999    }) {
1000        return Err("mesh deformation: displacements must contain finite components".to_string());
1001    }
1002    Ok(())
1003}
1004
1005fn validate_field_len(
1006    label: &str,
1007    location: MeshFieldLocation,
1008    actual: usize,
1009    vertex_count: usize,
1010    triangle_count: usize,
1011) -> Result<(), String> {
1012    let expected = match location {
1013        MeshFieldLocation::Vertex => vertex_count,
1014        MeshFieldLocation::Triangle => triangle_count,
1015    };
1016    if actual != expected {
1017        return Err(format!(
1018            "{label}: value count must match {} count ({expected})",
1019            location.as_str()
1020        ));
1021    }
1022    Ok(())
1023}
1024
1025fn sanitize_scalar_field(mut field: MeshScalarField) -> MeshScalarField {
1026    field.alpha = sanitize_alpha(field.alpha);
1027    if field.colormap.trim().is_empty() {
1028        field.colormap = "viridis".to_string();
1029    }
1030    field
1031}
1032
1033fn sanitize_vector_field(mut field: MeshVectorField) -> MeshVectorField {
1034    field.scale = if field.scale.is_finite() {
1035        field.scale
1036    } else {
1037        1.0
1038    };
1039    field.stride = field.stride.max(1);
1040    field.color = sanitize_color(field.color);
1041    field
1042}
1043
1044fn sanitize_deformation(mut deformation: MeshDeformation) -> MeshDeformation {
1045    deformation.scale = if deformation.scale.is_finite() {
1046        deformation.scale
1047    } else {
1048        1.0
1049    };
1050    deformation
1051}
1052
1053fn scalar_limits(field: &MeshScalarField) -> Option<[f32; 2]> {
1054    if let Some([min, max]) = field.color_limits {
1055        if min.is_finite() && max.is_finite() && max >= min {
1056            return Some([min, max]);
1057        }
1058    }
1059    let mut min = f32::INFINITY;
1060    let mut max = f32::NEG_INFINITY;
1061    for value in field
1062        .values
1063        .iter()
1064        .copied()
1065        .filter(|value| value.is_finite())
1066    {
1067        min = min.min(value);
1068        max = max.max(value);
1069    }
1070    if min.is_finite() && max.is_finite() {
1071        Some([min, max])
1072    } else {
1073        None
1074    }
1075}
1076
1077fn colormap_color(name: &str, t: f32, alpha: f32) -> Vec4 {
1078    let t = t.clamp(0.0, 1.0);
1079    let alpha = sanitize_alpha(alpha);
1080    match name {
1081        "thermal" | "heat" => Vec4::new(t, 0.22 + 0.5 * t, 1.0 - t, alpha),
1082        "blue_red" | "blue-red" | "diverging" => {
1083            if t < 0.5 {
1084                let local = t * 2.0;
1085                Vec4::new(0.14 + 0.56 * local, 0.34 + 0.36 * local, 0.86, alpha)
1086            } else {
1087                let local = (t - 0.5) * 2.0;
1088                Vec4::new(
1089                    0.70 + 0.25 * local,
1090                    0.70 - 0.46 * local,
1091                    0.86 - 0.66 * local,
1092                    alpha,
1093                )
1094            }
1095        }
1096        _ => {
1097            let r = (0.28 + 0.65 * t).clamp(0.0, 1.0);
1098            let g = (0.08 + 0.85 * (1.0 - (t - 0.5).abs() * 2.0)).clamp(0.0, 1.0);
1099            let b = (0.55 + 0.35 * (1.0 - t)).clamp(0.0, 1.0);
1100            Vec4::new(r, g, b, alpha)
1101        }
1102    }
1103}
1104
1105fn vertex_normals(vertices: &[Vec3], triangles: &[[u32; 3]]) -> Vec<Vec3> {
1106    let mut normals = vec![Vec3::ZERO; vertices.len()];
1107    for &[a, b, c] in triangles {
1108        let ia = a as usize;
1109        let ib = b as usize;
1110        let ic = c as usize;
1111        let edge_ab = vertices[ib] - vertices[ia];
1112        let edge_ac = vertices[ic] - vertices[ia];
1113        let normal = edge_ab.cross(edge_ac);
1114        if normal.length_squared() > f32::EPSILON {
1115            normals[ia] += normal;
1116            normals[ib] += normal;
1117            normals[ic] += normal;
1118        }
1119    }
1120    normals
1121        .into_iter()
1122        .map(|normal| {
1123            if normal.length_squared() > f32::EPSILON {
1124                normal.normalize()
1125            } else {
1126                Vec3::Z
1127            }
1128        })
1129        .collect()
1130}
1131
1132#[derive(Debug, Clone, Copy)]
1133struct FeatureEdgeAccumulator {
1134    first_group: u64,
1135    count: u8,
1136    crosses_group: bool,
1137}
1138
1139fn accumulate_feature_edge(
1140    edges: &mut BTreeMap<(u32, u32), FeatureEdgeAccumulator>,
1141    a: u32,
1142    b: u32,
1143    group: u64,
1144) {
1145    let entry = edges
1146        .entry(normalized_edge(a, b))
1147        .or_insert(FeatureEdgeAccumulator {
1148            first_group: group,
1149            count: 0,
1150            crosses_group: false,
1151        });
1152    if entry.first_group != group {
1153        entry.crosses_group = true;
1154    }
1155    entry.count = entry.count.saturating_add(1);
1156}
1157
1158fn normalized_edge(a: u32, b: u32) -> (u32, u32) {
1159    if a <= b {
1160        (a, b)
1161    } else {
1162        (b, a)
1163    }
1164}
1165
1166fn sanitize_color(color: Vec4) -> Vec4 {
1167    Vec4::new(
1168        sanitize_color_component(color.x),
1169        sanitize_color_component(color.y),
1170        sanitize_color_component(color.z),
1171        sanitize_color_component(color.w),
1172    )
1173}
1174
1175fn sanitize_color_component(value: f32) -> f32 {
1176    if value.is_finite() {
1177        value
1178    } else {
1179        0.0
1180    }
1181}
1182
1183fn sanitize_alpha(alpha: f32) -> f32 {
1184    if alpha.is_finite() {
1185        alpha.clamp(0.0, 1.0)
1186    } else {
1187        1.0
1188    }
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193    use super::*;
1194
1195    fn triangle_mesh() -> MeshPlot {
1196        MeshPlot::new(
1197            vec![
1198                Vec3::new(0.0, 0.0, 0.0),
1199                Vec3::new(1.0, 0.0, 0.0),
1200                Vec3::new(0.0, 1.0, 0.0),
1201            ],
1202            vec![[0, 1, 2]],
1203        )
1204        .expect("mesh should be valid")
1205    }
1206
1207    fn square_mesh() -> MeshPlot {
1208        MeshPlot::new(
1209            vec![
1210                Vec3::new(0.0, 0.0, 0.0),
1211                Vec3::new(1.0, 0.0, 0.0),
1212                Vec3::new(1.0, 1.0, 0.0),
1213                Vec3::new(0.0, 1.0, 0.0),
1214            ],
1215            vec![[0, 1, 2], [0, 2, 3]],
1216        )
1217        .expect("mesh should be valid")
1218    }
1219
1220    #[test]
1221    fn scalar_field_colors_mesh_vertices() {
1222        let mut mesh = triangle_mesh();
1223        let mut field = MeshScalarField::new(
1224            "fea.structural.von_mises",
1225            MeshFieldLocation::Vertex,
1226            vec![0.0, 0.5, 1.0],
1227        );
1228        field.color_limits = Some([0.0, 1.0]);
1229        field.alpha = 0.75;
1230        mesh.set_scalar_field(Some(field))
1231            .expect("scalar field should be accepted");
1232
1233        let render = mesh.render_data();
1234        assert_eq!(render.vertices.len(), 3);
1235        assert_ne!(render.vertices[0].color, render.vertices[2].color);
1236        assert!((render.vertices[0].color[3] - 0.75).abs() < f32::EPSILON);
1237    }
1238
1239    #[test]
1240    fn deformation_updates_render_positions_and_bounds() {
1241        let mut mesh = triangle_mesh();
1242        let mut deformation = MeshDeformation::new(
1243            "fea.structural.displacement",
1244            vec![Vec3::ZERO, Vec3::new(0.0, 0.0, 2.0), Vec3::ZERO],
1245        );
1246        deformation.scale = 0.5;
1247        mesh.set_deformation(Some(deformation))
1248            .expect("deformation should be accepted");
1249
1250        let bounds = mesh.bounds();
1251        assert_eq!(bounds.max.z, 1.0);
1252        let render = mesh.render_data();
1253        assert!(render
1254            .vertices
1255            .iter()
1256            .any(|vertex| (vertex.position[2] - 1.0).abs() < f32::EPSILON));
1257    }
1258
1259    #[test]
1260    fn vector_field_generates_line_glyphs() {
1261        let mut mesh = triangle_mesh();
1262        let mut field = MeshVectorField::new(
1263            "fea.em.flux_density",
1264            MeshFieldLocation::Vertex,
1265            vec![Vec3::X, Vec3::ZERO, Vec3::Y],
1266        );
1267        field.scale = 0.25;
1268        mesh.set_vector_field(Some(field))
1269            .expect("vector field should be accepted");
1270
1271        let render = mesh.vector_render_data().expect("vector glyphs");
1272        assert_eq!(render.pipeline_type, PipelineType::Lines);
1273        assert_eq!(render.vertices.len(), 4);
1274    }
1275
1276    #[test]
1277    fn feature_edge_mode_suppresses_internal_edges_with_same_group() {
1278        let mut mesh = square_mesh();
1279        mesh.set_edge_mode(MeshEdgeMode::Feature);
1280        mesh.set_feature_edge_groups(Some(vec![7, 7]))
1281            .expect("feature groups should be accepted");
1282
1283        let render = mesh.edge_render_data().expect("feature edges");
1284
1285        assert_eq!(render.pipeline_type, PipelineType::Lines);
1286        assert_eq!(render.vertices.len(), 8);
1287    }
1288
1289    #[test]
1290    fn feature_edge_mode_keeps_edges_between_groups() {
1291        let mut mesh = square_mesh();
1292        mesh.set_edge_mode(MeshEdgeMode::Feature);
1293        mesh.set_feature_edge_groups(Some(vec![7, 9]))
1294            .expect("feature groups should be accepted");
1295
1296        let render = mesh.edge_render_data().expect("feature edges");
1297
1298        assert_eq!(render.pipeline_type, PipelineType::Lines);
1299        assert_eq!(render.vertices.len(), 10);
1300    }
1301
1302    #[test]
1303    fn triangle_colors_generate_per_face_vertices() {
1304        let mut mesh = square_mesh();
1305        mesh.set_triangle_colors(Some(vec![
1306            Vec4::new(1.0, 0.0, 0.0, 1.0),
1307            Vec4::new(0.0, 0.0, 1.0, 1.0),
1308        ]))
1309        .expect("triangle colors should be accepted");
1310
1311        let render = mesh.render_data();
1312
1313        assert_eq!(render.vertices.len(), 6);
1314        assert_eq!(render.vertices[0].color, [1.0, 0.0, 0.0, 1.0]);
1315        assert_eq!(render.vertices[3].color, [0.0, 0.0, 1.0, 1.0]);
1316    }
1317
1318    #[test]
1319    fn vertex_colors_preserve_indexed_mesh_geometry() {
1320        let mut mesh = square_mesh();
1321        mesh.set_vertex_colors(Some(vec![
1322            Vec4::new(1.0, 0.0, 0.0, 1.0),
1323            Vec4::new(0.0, 1.0, 0.0, 1.0),
1324            Vec4::new(0.0, 0.0, 1.0, 1.0),
1325            Vec4::new(1.0, 1.0, 0.0, 1.0),
1326        ]))
1327        .expect("vertex colors should be accepted");
1328
1329        let render = mesh.render_data();
1330
1331        assert_eq!(render.vertices.len(), 4);
1332        assert_eq!(render.indices.as_ref().unwrap().len(), 6);
1333        assert_eq!(render.vertices[2].color, [0.0, 0.0, 1.0, 1.0]);
1334    }
1335}