1use 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}