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