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