Skip to main content

runmat_plot/plots/
patch.rs

1//! MATLAB-compatible polygon patch plot implementation.
2
3use crate::core::{AlphaMode, BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
4use crate::geometry::stroke3d::{tessellate_polyline, StrokeCap3D, StrokeStyle3D};
5use crate::plots::line::LineStyle;
6use glam::{Vec2, Vec3, Vec4};
7
8const TRIANGULATION_EPSILON: f32 = 1.0e-6;
9
10const POINTS_TO_PX: f32 = 96.0 / 72.0;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PatchFaceColorMode {
14    Color,
15    Flat,
16    None,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PatchEdgeColorMode {
21    Color,
22    None,
23}
24
25#[derive(Debug, Clone)]
26pub struct PatchPlot {
27    vertices: Vec<Vec3>,
28    faces: Vec<Vec<usize>>,
29    face_color: Vec4,
30    edge_color: Vec4,
31    face_color_mode: PatchFaceColorMode,
32    edge_color_mode: PatchEdgeColorMode,
33    face_alpha: f32,
34    edge_alpha: f32,
35    line_width: f32,
36    label: Option<String>,
37    visible: bool,
38    face_vertices: Option<Vec<Vertex>>,
39    face_indices: Option<Vec<u32>>,
40    edge_vertices: Option<Vec<Vertex>>,
41    bounds: Option<BoundingBox>,
42    force_3d: bool,
43    dirty: bool,
44}
45
46impl PatchPlot {
47    pub fn new(vertices: Vec<Vec3>, faces: Vec<Vec<usize>>) -> Result<Self, String> {
48        if vertices.is_empty() {
49            return Err("patch: Vertices must not be empty".to_string());
50        }
51        validate_finite_vertices(&vertices)?;
52        let faces = normalize_faces(faces);
53        if faces.is_empty() {
54            return Err("patch: Faces must contain at least one polygon".to_string());
55        }
56        validate_faces(&vertices, &faces)?;
57        Ok(Self {
58            vertices,
59            faces,
60            face_color: Vec4::new(0.0, 0.447, 0.741, 1.0),
61            edge_color: Vec4::new(0.0, 0.0, 0.0, 1.0),
62            face_color_mode: PatchFaceColorMode::Color,
63            edge_color_mode: PatchEdgeColorMode::Color,
64            face_alpha: 1.0,
65            edge_alpha: 1.0,
66            line_width: 0.5,
67            label: None,
68            visible: true,
69            face_vertices: None,
70            face_indices: None,
71            edge_vertices: None,
72            bounds: None,
73            force_3d: false,
74            dirty: true,
75        })
76    }
77
78    pub fn vertices(&self) -> &[Vec3] {
79        &self.vertices
80    }
81
82    pub fn faces(&self) -> &[Vec<usize>] {
83        &self.faces
84    }
85
86    pub fn face_color(&self) -> Vec4 {
87        self.face_color
88    }
89
90    pub fn edge_color(&self) -> Vec4 {
91        self.edge_color
92    }
93
94    pub fn face_color_mode(&self) -> PatchFaceColorMode {
95        self.face_color_mode
96    }
97
98    pub fn edge_color_mode(&self) -> PatchEdgeColorMode {
99        self.edge_color_mode
100    }
101
102    pub fn face_alpha(&self) -> f32 {
103        self.face_alpha
104    }
105
106    pub fn edge_alpha(&self) -> f32 {
107        self.edge_alpha
108    }
109
110    pub fn line_width(&self) -> f32 {
111        self.line_width
112    }
113
114    pub fn label(&self) -> Option<&str> {
115        self.label.as_deref()
116    }
117
118    pub fn is_visible(&self) -> bool {
119        self.visible
120    }
121
122    pub fn force_3d(&self) -> bool {
123        self.force_3d
124    }
125
126    pub fn set_force_3d(&mut self, force_3d: bool) {
127        self.force_3d = force_3d;
128        self.mark_dirty();
129    }
130
131    pub fn set_vertices(&mut self, vertices: Vec<Vec3>) -> Result<(), String> {
132        if vertices.is_empty() {
133            return Err("patch: Vertices must not be empty".to_string());
134        }
135        validate_finite_vertices(&vertices)?;
136        validate_faces(&vertices, &self.faces)?;
137        self.vertices = vertices;
138        self.mark_dirty();
139        Ok(())
140    }
141
142    pub fn set_faces(&mut self, faces: Vec<Vec<usize>>) -> Result<(), String> {
143        let faces = normalize_faces(faces);
144        if faces.is_empty() {
145            return Err("patch: Faces must contain at least one polygon".to_string());
146        }
147        validate_faces(&self.vertices, &faces)?;
148        self.faces = faces;
149        self.mark_dirty();
150        Ok(())
151    }
152
153    pub fn set_face_color(&mut self, color: Vec4) {
154        self.face_color = sanitize_color(color);
155        self.mark_dirty();
156    }
157
158    pub fn set_edge_color(&mut self, color: Vec4) {
159        self.edge_color = sanitize_color(color);
160        self.mark_dirty();
161    }
162
163    pub fn set_face_color_mode(&mut self, mode: PatchFaceColorMode) {
164        self.face_color_mode = mode;
165        self.mark_dirty();
166    }
167
168    pub fn set_edge_color_mode(&mut self, mode: PatchEdgeColorMode) {
169        self.edge_color_mode = mode;
170        self.mark_dirty();
171    }
172
173    pub fn set_face_alpha(&mut self, alpha: f32) {
174        self.face_alpha = sanitize_alpha(alpha);
175        self.mark_dirty();
176    }
177
178    pub fn set_edge_alpha(&mut self, alpha: f32) {
179        self.edge_alpha = sanitize_alpha(alpha);
180        self.mark_dirty();
181    }
182
183    pub fn set_line_width(&mut self, line_width: f32) {
184        self.line_width = line_width.max(0.0);
185        self.mark_dirty();
186    }
187
188    pub fn set_label(&mut self, label: Option<String>) {
189        self.label = label;
190        self.mark_dirty();
191    }
192
193    pub fn set_visible(&mut self, visible: bool) {
194        self.visible = visible;
195        self.mark_dirty();
196    }
197
198    pub fn mark_dirty(&mut self) {
199        self.dirty = true;
200        self.bounds = None;
201        self.face_vertices = None;
202        self.face_indices = None;
203        self.edge_vertices = None;
204    }
205
206    pub fn effective_face_color(&self) -> Vec4 {
207        let mut color = self.face_color;
208        color.w *= self.face_alpha.clamp(0.0, 1.0);
209        color
210    }
211
212    pub fn effective_edge_color(&self) -> Vec4 {
213        let mut color = self.edge_color;
214        color.w *= self.edge_alpha.clamp(0.0, 1.0);
215        color
216    }
217
218    fn generate_face_geometry(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
219        if self.dirty || self.face_vertices.is_none() || self.face_indices.is_none() {
220            let mut out_vertices = Vec::new();
221            let mut out_indices = Vec::new();
222            if self.face_color_mode != PatchFaceColorMode::None {
223                let color = self.effective_face_color();
224                for face in &self.faces {
225                    if face.len() < 3 {
226                        continue;
227                    }
228                    let base = out_vertices.len() as u32;
229                    for &idx in face {
230                        out_vertices.push(Vertex::new(self.vertices[idx], color));
231                    }
232                    triangulate_face(&self.vertices, face, base, &mut out_indices)
233                        .expect("validated patch face should triangulate");
234                }
235            }
236            self.face_vertices = Some(out_vertices);
237            self.face_indices = Some(out_indices);
238            self.dirty = false;
239        }
240        (
241            self.face_vertices.as_ref().unwrap(),
242            self.face_indices.as_ref().unwrap(),
243        )
244    }
245
246    fn generate_edge_vertices(&mut self) -> &Vec<Vertex> {
247        if self.dirty || self.edge_vertices.is_none() {
248            let mut out = Vec::new();
249            if self.edge_color_mode != PatchEdgeColorMode::None {
250                let color = self.effective_edge_color();
251                for face in &self.faces {
252                    if face.len() < 2 {
253                        continue;
254                    }
255                    for pos in 0..face.len() {
256                        let a = self.vertices[face[pos]];
257                        let b = self.vertices[face[(pos + 1) % face.len()]];
258                        out.push(Vertex::new(a, color));
259                        out.push(Vertex::new(b, color));
260                    }
261                }
262            }
263            self.edge_vertices = Some(out);
264        }
265        self.edge_vertices.as_ref().unwrap()
266    }
267
268    pub fn bounds(&mut self) -> BoundingBox {
269        if self.dirty || self.bounds.is_none() {
270            let points: Vec<Vec3> = self
271                .vertices
272                .iter()
273                .copied()
274                .filter(|point| point.is_finite())
275                .collect();
276            self.bounds = Some(if points.is_empty() {
277                BoundingBox::new(Vec3::ZERO, Vec3::ZERO)
278            } else {
279                BoundingBox::from_points(&points)
280            });
281        }
282        self.bounds.unwrap()
283    }
284
285    pub fn render_data(&mut self) -> RenderData {
286        let bounds = self.bounds();
287        let (vertices, indices) = {
288            let (vertices, indices) = self.generate_face_geometry();
289            (vertices.clone(), indices.clone())
290        };
291        let color = self.effective_face_color();
292        let vertex_count = vertices.len();
293        let index_count = indices.len();
294        RenderData {
295            pipeline_type: PipelineType::Triangles,
296            vertices,
297            indices: Some(indices.clone()),
298            gpu_vertices: None,
299            bounds: Some(bounds),
300            material: Material {
301                albedo: color,
302                alpha_mode: if color.w < 1.0 {
303                    AlphaMode::Blend
304                } else {
305                    AlphaMode::Opaque
306                },
307                double_sided: true,
308                ..Default::default()
309            },
310            draw_calls: vec![DrawCall {
311                vertex_offset: 0,
312                vertex_count,
313                index_offset: Some(0),
314                index_count: Some(index_count),
315                instance_count: 1,
316            }],
317            image: None,
318        }
319    }
320
321    pub fn edge_render_data(&mut self) -> Option<RenderData> {
322        self.edge_render_data_with_viewport(None)
323    }
324
325    pub fn edge_render_data_with_viewport(
326        &mut self,
327        viewport_px: Option<(u32, u32)>,
328    ) -> Option<RenderData> {
329        let bounds = self.bounds();
330        let line_width = self.line_width.max(0.0);
331        if line_width == 0.0 {
332            return None;
333        }
334
335        let color = self.effective_edge_color();
336        let width_px = (line_width.max(0.1) * POINTS_TO_PX).max(0.1);
337        if let Some(vp) = viewport_px.filter(|_| width_px > 1.0) {
338            let has_3d_content =
339                self.force_3d || self.vertices.iter().any(|point| point.z.abs() > 1e-6);
340            let data_per_px = if has_3d_content {
341                crate::core::data_units_per_px_3d(&bounds, vp)
342            } else {
343                crate::core::data_units_per_px(&bounds, vp)
344            };
345            let half_width_data = (width_px * 0.5) * data_per_px;
346            let style = StrokeStyle3D::new(half_width_data, LineStyle::Solid, StrokeCap3D::Butt);
347            let mut tri_vertices = Vec::new();
348            for face in &self.faces {
349                if face.len() < 2 {
350                    continue;
351                }
352                let mut polyline = Vec::with_capacity(face.len() + 1);
353                for &idx in face {
354                    polyline.push(self.vertices[idx]);
355                }
356                polyline.push(self.vertices[face[0]]);
357                tri_vertices.extend(tessellate_polyline(&polyline, color, style));
358            }
359            if !tri_vertices.is_empty() {
360                let indices = (0..tri_vertices.len() as u32).collect::<Vec<u32>>();
361                let index_count = indices.len();
362                let vertex_count = tri_vertices.len();
363                return Some(RenderData {
364                    pipeline_type: PipelineType::Triangles,
365                    vertices: tri_vertices,
366                    indices: Some(indices),
367                    gpu_vertices: None,
368                    bounds: Some(bounds),
369                    material: Material {
370                        albedo: color,
371                        roughness: width_px.max(0.5),
372                        alpha_mode: if color.w < 1.0 {
373                            AlphaMode::Blend
374                        } else {
375                            AlphaMode::Opaque
376                        },
377                        ..Default::default()
378                    },
379                    draw_calls: vec![DrawCall {
380                        vertex_offset: 0,
381                        vertex_count,
382                        index_offset: Some(0),
383                        index_count: Some(index_count),
384                        instance_count: 1,
385                    }],
386                    image: None,
387                });
388            }
389        }
390
391        let vertices = self.generate_edge_vertices().clone();
392        if vertices.is_empty() {
393            return None;
394        }
395        Some(RenderData {
396            pipeline_type: PipelineType::Lines,
397            vertices,
398            indices: None,
399            gpu_vertices: None,
400            bounds: Some(bounds),
401            material: Material {
402                albedo: color,
403                roughness: width_px.max(0.5),
404                alpha_mode: if color.w < 1.0 {
405                    AlphaMode::Blend
406                } else {
407                    AlphaMode::Opaque
408                },
409                ..Default::default()
410            },
411            draw_calls: vec![DrawCall {
412                vertex_offset: 0,
413                vertex_count: self.edge_vertices.as_ref().map(|v| v.len()).unwrap_or(0),
414                index_offset: None,
415                index_count: None,
416                instance_count: 1,
417            }],
418            image: None,
419        })
420    }
421
422    pub fn estimated_memory_usage(&self) -> usize {
423        self.face_vertices
424            .as_ref()
425            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
426            + self
427                .face_indices
428                .as_ref()
429                .map_or(0, |i| i.len() * std::mem::size_of::<u32>())
430            + self
431                .edge_vertices
432                .as_ref()
433                .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
434    }
435}
436
437fn sanitize_color(color: Vec4) -> Vec4 {
438    Vec4::new(
439        sanitize_color_component(color.x),
440        sanitize_color_component(color.y),
441        sanitize_color_component(color.z),
442        sanitize_color_component(color.w),
443    )
444}
445
446fn sanitize_color_component(value: f32) -> f32 {
447    if value.is_finite() {
448        value
449    } else {
450        0.0
451    }
452}
453
454fn sanitize_alpha(alpha: f32) -> f32 {
455    if alpha.is_finite() {
456        alpha.clamp(0.0, 1.0)
457    } else {
458        1.0
459    }
460}
461
462fn validate_finite_vertices(vertices: &[Vec3]) -> Result<(), String> {
463    if vertices
464        .iter()
465        .any(|v| !v.x.is_finite() || !v.y.is_finite() || !v.z.is_finite())
466    {
467        return Err(
468            "patch: Vertices must contain finite Vec3 coordinates before bounds/render_data"
469                .to_string(),
470        );
471    }
472    Ok(())
473}
474
475fn validate_faces(vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<(), String> {
476    for face in faces {
477        for &idx in face {
478            if idx >= vertices.len() {
479                return Err("patch: Faces index exceeds Vertices row count".to_string());
480            }
481        }
482        let mut indices = Vec::new();
483        triangulate_face(vertices, face, 0, &mut indices)?;
484    }
485    Ok(())
486}
487
488fn triangulate_face(
489    vertices: &[Vec3],
490    face: &[usize],
491    base: u32,
492    out_indices: &mut Vec<u32>,
493) -> Result<(), String> {
494    match face.len() {
495        0..=2 => Ok(()),
496        3 => {
497            out_indices.extend_from_slice(&[base, base + 1, base + 2]);
498            Ok(())
499        }
500        _ => {
501            let projected = project_face_to_2d(vertices, face)?;
502            ear_clip_projected_face(&projected, base, out_indices)
503        }
504    }
505}
506
507fn project_face_to_2d(vertices: &[Vec3], face: &[usize]) -> Result<Vec<Vec2>, String> {
508    let mut normal = Vec3::ZERO;
509    for pos in 0..face.len() {
510        let current = vertices[face[pos]];
511        let next = vertices[face[(pos + 1) % face.len()]];
512        normal.x += (current.y - next.y) * (current.z + next.z);
513        normal.y += (current.z - next.z) * (current.x + next.x);
514        normal.z += (current.x - next.x) * (current.y + next.y);
515    }
516
517    let abs = normal.abs();
518    if abs.max_element() <= TRIANGULATION_EPSILON {
519        return Err("patch: Face polygon must have non-zero area".to_string());
520    }
521
522    Ok(face
523        .iter()
524        .map(|&idx| {
525            let vertex = vertices[idx];
526            if abs.x >= abs.y && abs.x >= abs.z {
527                Vec2::new(vertex.y, vertex.z)
528            } else if abs.y >= abs.z {
529                Vec2::new(vertex.x, vertex.z)
530            } else {
531                Vec2::new(vertex.x, vertex.y)
532            }
533        })
534        .collect())
535}
536
537fn ear_clip_projected_face(
538    points: &[Vec2],
539    base: u32,
540    out_indices: &mut Vec<u32>,
541) -> Result<(), String> {
542    let signed_area = polygon_signed_area(points);
543    if signed_area.abs() <= TRIANGULATION_EPSILON {
544        return Err("patch: Face polygon must have non-zero area".to_string());
545    }
546    let ccw = signed_area > 0.0;
547    let mut polygon: Vec<usize> = (0..points.len()).collect();
548    let mut scan_start = 1;
549
550    while polygon.len() > 3 {
551        let len = polygon.len();
552        let mut ear_pos = None;
553        for step in 0..len {
554            let pos = (scan_start + step) % len;
555            if is_ear(points, &polygon, pos, ccw) {
556                ear_pos = Some(pos);
557                break;
558            }
559        }
560
561        let Some(pos) = ear_pos else {
562            return Err(
563                "patch: Face polygon could not be triangulated; faces must be simple polygons"
564                    .to_string(),
565            );
566        };
567        let len = polygon.len();
568        let prev = polygon[(pos + len - 1) % len];
569        let current = polygon[pos];
570        let next = polygon[(pos + 1) % len];
571        out_indices.extend_from_slice(&[
572            base + prev as u32,
573            base + current as u32,
574            base + next as u32,
575        ]);
576        polygon.remove(pos);
577        scan_start = pos.min(polygon.len() - 1);
578    }
579
580    out_indices.extend_from_slice(&[
581        base + polygon[0] as u32,
582        base + polygon[1] as u32,
583        base + polygon[2] as u32,
584    ]);
585    Ok(())
586}
587
588fn is_ear(points: &[Vec2], polygon: &[usize], pos: usize, ccw: bool) -> bool {
589    let len = polygon.len();
590    let prev = polygon[(pos + len - 1) % len];
591    let current = polygon[pos];
592    let next = polygon[(pos + 1) % len];
593    let a = points[prev];
594    let b = points[current];
595    let c = points[next];
596
597    if !is_convex(a, b, c, ccw) {
598        return false;
599    }
600
601    !polygon.iter().any(|&idx| {
602        idx != prev && idx != current && idx != next && point_in_triangle(points[idx], a, b, c, ccw)
603    })
604}
605
606fn is_convex(a: Vec2, b: Vec2, c: Vec2, ccw: bool) -> bool {
607    let cross = cross_2d(a, b, c);
608    if ccw {
609        cross > TRIANGULATION_EPSILON
610    } else {
611        cross < -TRIANGULATION_EPSILON
612    }
613}
614
615fn point_in_triangle(point: Vec2, a: Vec2, b: Vec2, c: Vec2, ccw: bool) -> bool {
616    let ab = cross_2d(a, b, point);
617    let bc = cross_2d(b, c, point);
618    let ca = cross_2d(c, a, point);
619    if ccw {
620        ab >= -TRIANGULATION_EPSILON && bc >= -TRIANGULATION_EPSILON && ca >= -TRIANGULATION_EPSILON
621    } else {
622        ab <= TRIANGULATION_EPSILON && bc <= TRIANGULATION_EPSILON && ca <= TRIANGULATION_EPSILON
623    }
624}
625
626fn cross_2d(a: Vec2, b: Vec2, c: Vec2) -> f32 {
627    let ab = b - a;
628    let ac = c - a;
629    ab.x * ac.y - ab.y * ac.x
630}
631
632fn polygon_signed_area(points: &[Vec2]) -> f32 {
633    let mut area = 0.0;
634    for pos in 0..points.len() {
635        let current = points[pos];
636        let next = points[(pos + 1) % points.len()];
637        area += current.x * next.y - next.x * current.y;
638    }
639    area * 0.5
640}
641
642fn normalize_faces(faces: Vec<Vec<usize>>) -> Vec<Vec<usize>> {
643    faces
644        .into_iter()
645        .filter_map(|mut face| {
646            face.dedup();
647            if face.len() > 1 && face.first() == face.last() {
648                face.pop();
649            }
650            if face.len() >= 3 {
651                Some(face)
652            } else {
653                None
654            }
655        })
656        .collect()
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662
663    #[test]
664    fn patch_triangulates_quad_and_closes_edges() {
665        let mut patch = PatchPlot::new(
666            vec![
667                Vec3::new(0.0, 0.0, 0.0),
668                Vec3::new(1.0, 0.0, 0.0),
669                Vec3::new(1.0, 1.0, 0.0),
670                Vec3::new(0.0, 1.0, 0.0),
671            ],
672            vec![vec![0, 1, 2, 3]],
673        )
674        .unwrap();
675        let face = patch.render_data();
676        assert_eq!(face.indices.as_ref().unwrap(), &[0, 1, 2, 0, 2, 3]);
677        let edge = patch.edge_render_data().unwrap();
678        assert_eq!(edge.vertices.len(), 8);
679    }
680
681    #[test]
682    fn patch_triangulates_concave_face_without_triangle_fan() {
683        let mut patch = PatchPlot::new(
684            vec![
685                Vec3::new(0.0, 0.0, 0.0),
686                Vec3::new(2.0, 0.0, 0.0),
687                Vec3::new(2.0, 1.0, 0.0),
688                Vec3::new(1.0, 0.4, 0.0),
689                Vec3::new(0.0, 1.0, 0.0),
690            ],
691            vec![vec![0, 1, 2, 3, 4]],
692        )
693        .unwrap();
694
695        let render = patch.render_data();
696        let indices = render.indices.as_ref().unwrap();
697        assert_eq!(indices.len(), 9);
698        assert_ne!(indices, &[0, 1, 2, 0, 2, 3, 0, 3, 4]);
699        assert_eq!(indices, &[1, 2, 3, 3, 4, 0, 0, 1, 3]);
700        assert!(
701            (triangle_area_sum(&render.vertices, indices) - polygon_area(&render.vertices)).abs()
702                < 1.0e-5
703        );
704    }
705
706    #[test]
707    fn patch_set_face_color_invalidates_cached_geometry() {
708        let mut patch = PatchPlot::new(
709            vec![
710                Vec3::new(0.0, 0.0, 0.0),
711                Vec3::new(1.0, 0.0, 0.0),
712                Vec3::new(0.0, 1.0, 0.0),
713            ],
714            vec![vec![0, 1, 2]],
715        )
716        .unwrap();
717        let initial = patch.render_data();
718        assert_eq!(initial.vertices[0].color, [0.0, 0.447, 0.741, 1.0]);
719
720        patch.set_face_color(Vec4::new(1.0, 0.0, 0.0, 1.0));
721        let updated = patch.render_data();
722        assert_eq!(updated.vertices[0].color, [1.0, 0.0, 0.0, 1.0]);
723    }
724
725    #[test]
726    fn patch_new_rejects_non_finite_vertices_before_render_data() {
727        let err = PatchPlot::new(
728            vec![
729                Vec3::new(0.0, 0.0, 0.0),
730                Vec3::new(f32::NAN, 0.0, 0.0),
731                Vec3::new(0.0, 1.0, f32::INFINITY),
732            ],
733            vec![vec![0, 1, 2]],
734        )
735        .expect_err("PatchPlot::new should reject non-finite Vec3 coordinates");
736        assert!(err.contains("finite Vec3 coordinates"));
737    }
738
739    #[test]
740    fn patch_style_setters_sanitize_non_finite_values() {
741        let mut patch = PatchPlot::new(
742            vec![
743                Vec3::new(0.0, 0.0, 0.0),
744                Vec3::new(1.0, 0.0, 0.0),
745                Vec3::new(0.0, 1.0, 0.0),
746            ],
747            vec![vec![0, 1, 2]],
748        )
749        .unwrap();
750
751        patch.set_face_color(Vec4::new(f32::NAN, 0.25, f32::INFINITY, 1.0));
752        patch.set_edge_color(Vec4::new(0.5, f32::NEG_INFINITY, 0.75, f32::NAN));
753        patch.set_face_alpha(f32::NAN);
754        patch.set_edge_alpha(f32::INFINITY);
755
756        assert_eq!(patch.face_color(), Vec4::new(0.0, 0.25, 0.0, 1.0));
757        assert_eq!(patch.edge_color(), Vec4::new(0.5, 0.0, 0.75, 0.0));
758        assert_eq!(patch.face_alpha(), 1.0);
759        assert_eq!(patch.edge_alpha(), 1.0);
760
761        let render = patch.render_data();
762        assert!(render.vertices[0]
763            .color
764            .iter()
765            .all(|component| component.is_finite()));
766        assert!(render.material.albedo.is_finite());
767    }
768
769    #[test]
770    fn patch_accepts_explicitly_closed_face() {
771        let patch = PatchPlot::new(
772            vec![
773                Vec3::new(0.0, 0.0, 0.0),
774                Vec3::new(1.0, 0.0, 0.0),
775                Vec3::new(0.0, 1.0, 0.0),
776            ],
777            vec![vec![0, 1, 2, 0]],
778        )
779        .unwrap();
780        assert_eq!(patch.faces(), &[vec![0, 1, 2]]);
781    }
782
783    fn triangle_area_sum(vertices: &[Vertex], indices: &[u32]) -> f32 {
784        indices
785            .chunks_exact(3)
786            .map(|tri| {
787                let a = Vec2::new(
788                    vertices[tri[0] as usize].position[0],
789                    vertices[tri[0] as usize].position[1],
790                );
791                let b = Vec2::new(
792                    vertices[tri[1] as usize].position[0],
793                    vertices[tri[1] as usize].position[1],
794                );
795                let c = Vec2::new(
796                    vertices[tri[2] as usize].position[0],
797                    vertices[tri[2] as usize].position[1],
798                );
799                cross_2d(a, b, c).abs() * 0.5
800            })
801            .sum()
802    }
803
804    fn polygon_area(vertices: &[Vertex]) -> f32 {
805        let points: Vec<Vec2> = vertices
806            .iter()
807            .map(|vertex| Vec2::new(vertex.position[0], vertex.position[1]))
808            .collect();
809        polygon_signed_area(&points).abs()
810    }
811}