1use nalgebra::{Point3, Vector3};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct VertexColor {
8 pub r: u8,
9 pub g: u8,
10 pub b: u8,
11}
12
13impl VertexColor {
14 #[inline]
16 pub fn new(r: u8, g: u8, b: u8) -> Self {
17 Self { r, g, b }
18 }
19
20 #[inline]
22 pub fn from_float(r: f32, g: f32, b: f32) -> Self {
23 Self {
24 r: (r.clamp(0.0, 1.0) * 255.0) as u8,
25 g: (g.clamp(0.0, 1.0) * 255.0) as u8,
26 b: (b.clamp(0.0, 1.0) * 255.0) as u8,
27 }
28 }
29
30 #[inline]
32 pub fn to_float(&self) -> (f32, f32, f32) {
33 (
34 self.r as f32 / 255.0,
35 self.g as f32 / 255.0,
36 self.b as f32 / 255.0,
37 )
38 }
39}
40
41#[derive(Debug, Clone)]
45pub struct Vertex {
46 pub position: Point3<f64>,
48
49 pub normal: Option<Vector3<f64>>,
51
52 pub color: Option<VertexColor>,
54
55 pub tag: Option<u32>,
57
58 pub offset: Option<f32>,
61}
62
63impl Vertex {
64 #[inline]
66 pub fn new(position: Point3<f64>) -> Self {
67 Self {
68 position,
69 normal: None,
70 color: None,
71 tag: None,
72 offset: None,
73 }
74 }
75
76 #[inline]
78 pub fn from_coords(x: f64, y: f64, z: f64) -> Self {
79 Self::new(Point3::new(x, y, z))
80 }
81
82 #[inline]
84 pub fn with_color(position: Point3<f64>, color: VertexColor) -> Self {
85 Self {
86 position,
87 normal: None,
88 color: Some(color),
89 tag: None,
90 offset: None,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct Mesh {
98 pub vertices: Vec<Vertex>,
100
101 pub faces: Vec<[u32; 3]>,
104}
105
106impl Mesh {
107 pub fn new() -> Self {
109 Self {
110 vertices: Vec::new(),
111 faces: Vec::new(),
112 }
113 }
114
115 pub fn with_capacity(vertex_count: usize, face_count: usize) -> Self {
117 Self {
118 vertices: Vec::with_capacity(vertex_count),
119 faces: Vec::with_capacity(face_count),
120 }
121 }
122
123 #[inline]
125 pub fn vertex_count(&self) -> usize {
126 self.vertices.len()
127 }
128
129 #[inline]
131 pub fn face_count(&self) -> usize {
132 self.faces.len()
133 }
134
135 #[inline]
137 pub fn is_empty(&self) -> bool {
138 self.vertices.is_empty() || self.faces.is_empty()
139 }
140
141 pub fn bounds(&self) -> Option<(Point3<f64>, Point3<f64>)> {
144 if self.vertices.is_empty() {
145 return None;
146 }
147
148 let mut min = self.vertices[0].position;
149 let mut max = self.vertices[0].position;
150
151 for vertex in &self.vertices[1..] {
152 let p = &vertex.position;
153 min.x = min.x.min(p.x);
154 min.y = min.y.min(p.y);
155 min.z = min.z.min(p.z);
156 max.x = max.x.max(p.x);
157 max.y = max.y.max(p.y);
158 max.z = max.z.max(p.z);
159 }
160
161 Some((min, max))
162 }
163
164 pub fn triangles(&self) -> impl Iterator<Item = Triangle> + '_ {
166 self.faces.iter().map(|&[i0, i1, i2]| Triangle {
167 v0: self.vertices[i0 as usize].position,
168 v1: self.vertices[i1 as usize].position,
169 v2: self.vertices[i2 as usize].position,
170 })
171 }
172
173 pub fn triangle(&self, face_idx: usize) -> Option<Triangle> {
175 self.faces.get(face_idx).map(|&[i0, i1, i2]| Triangle {
176 v0: self.vertices[i0 as usize].position,
177 v1: self.vertices[i1 as usize].position,
178 v2: self.vertices[i2 as usize].position,
179 })
180 }
181
182 pub fn rotate_x_90(&mut self) {
184 for vertex in &mut self.vertices {
185 let old_y = vertex.position.y;
186 let old_z = vertex.position.z;
187 vertex.position.y = -old_z;
188 vertex.position.z = old_y;
189
190 if let Some(ref mut normal) = vertex.normal {
191 let old_ny = normal.y;
192 let old_nz = normal.z;
193 normal.y = -old_nz;
194 normal.z = old_ny;
195 }
196 }
197 }
198
199 pub fn place_on_z_zero(&mut self) {
201 if let Some((min, _)) = self.bounds() {
202 let offset = -min.z;
203 for vertex in &mut self.vertices {
204 vertex.position.z += offset;
205 }
206 }
207 }
208
209 pub fn translate(&mut self, offset: Vector3<f64>) {
211 for vertex in &mut self.vertices {
212 vertex.position += offset;
213 }
214 }
215
216 pub fn scale(&mut self, factor: f64) {
218 for vertex in &mut self.vertices {
219 vertex.position.coords *= factor;
220 }
221 }
222
223 pub fn signed_volume(&self) -> f64 {
238 let mut volume = 0.0;
239
240 for &[i0, i1, i2] in &self.faces {
241 let v0 = &self.vertices[i0 as usize].position;
242 let v1 = &self.vertices[i1 as usize].position;
243 let v2 = &self.vertices[i2 as usize].position;
244
245 let cross = Vector3::new(
248 v1.y * v2.z - v1.z * v2.y,
249 v1.z * v2.x - v1.x * v2.z,
250 v1.x * v2.y - v1.y * v2.x,
251 );
252 volume += v0.x * cross.x + v0.y * cross.y + v0.z * cross.z;
253 }
254
255 volume / 6.0
256 }
257
258 #[inline]
267 pub fn volume(&self) -> f64 {
268 self.signed_volume().abs()
269 }
270
271 #[inline]
284 pub fn is_inside_out(&self) -> bool {
285 self.signed_volume() < 0.0
286 }
287
288 pub fn surface_area(&self) -> f64 {
292 self.triangles().map(|tri| tri.area()).sum()
293 }
294}
295
296impl Default for Mesh {
297 fn default() -> Self {
298 Self::new()
299 }
300}
301
302#[derive(Debug, Clone, Copy)]
307pub struct Triangle {
308 pub v0: Point3<f64>,
309 pub v1: Point3<f64>,
310 pub v2: Point3<f64>,
311}
312
313impl Triangle {
314 #[inline]
316 pub fn new(v0: Point3<f64>, v1: Point3<f64>, v2: Point3<f64>) -> Self {
317 Self { v0, v1, v2 }
318 }
319
320 #[inline]
323 pub fn normal_unnormalized(&self) -> Vector3<f64> {
324 let e1 = self.v1 - self.v0;
325 let e2 = self.v2 - self.v0;
326 e1.cross(&e2)
327 }
328
329 pub fn normal(&self) -> Option<Vector3<f64>> {
332 let n = self.normal_unnormalized();
333 let len_sq = n.norm_squared();
334 if len_sq > f64::EPSILON {
335 Some(n / len_sq.sqrt())
336 } else {
337 None
338 }
339 }
340
341 #[inline]
343 pub fn area(&self) -> f64 {
344 self.normal_unnormalized().norm() * 0.5
345 }
346
347 #[inline]
349 pub fn centroid(&self) -> Point3<f64> {
350 Point3::new(
351 (self.v0.x + self.v1.x + self.v2.x) / 3.0,
352 (self.v0.y + self.v1.y + self.v2.y) / 3.0,
353 (self.v0.z + self.v1.z + self.v2.z) / 3.0,
354 )
355 }
356
357 pub fn edges(&self) -> [(Point3<f64>, Point3<f64>); 3] {
359 [(self.v0, self.v1), (self.v1, self.v2), (self.v2, self.v0)]
360 }
361
362 #[inline]
365 pub fn edge_lengths(&self) -> [f64; 3] {
366 [
367 (self.v1 - self.v0).norm(),
368 (self.v2 - self.v1).norm(),
369 (self.v0 - self.v2).norm(),
370 ]
371 }
372
373 #[inline]
375 pub fn min_edge_length(&self) -> f64 {
376 let lengths = self.edge_lengths();
377 lengths[0].min(lengths[1]).min(lengths[2])
378 }
379
380 #[inline]
382 pub fn max_edge_length(&self) -> f64 {
383 let lengths = self.edge_lengths();
384 lengths[0].max(lengths[1]).max(lengths[2])
385 }
386
387 pub fn aspect_ratio(&self) -> f64 {
395 let area = self.area();
396 if area < f64::EPSILON {
397 return f64::INFINITY;
398 }
399
400 let max_edge = self.max_edge_length();
401
402 let shortest_altitude = 2.0 * area / max_edge;
405
406 if shortest_altitude < f64::EPSILON {
407 return f64::INFINITY;
408 }
409
410 max_edge / shortest_altitude
411 }
412
413 pub fn is_nearly_collinear(&self, epsilon: f64) -> bool {
418 let e1 = self.v1 - self.v0;
419 let e2 = self.v2 - self.v0;
420
421 let cross_magnitude = e1.cross(&e2).norm();
422 let edge_product = e1.norm() * e2.norm();
423
424 if edge_product < f64::EPSILON {
425 return true; }
427
428 cross_magnitude / edge_product < epsilon
431 }
432
433 pub fn is_degenerate(&self, epsilon: f64) -> bool {
435 self.area() < epsilon
436 }
437
438 pub fn is_degenerate_enhanced(
450 &self,
451 area_threshold: f64,
452 max_aspect_ratio: f64,
453 min_edge_length: f64,
454 ) -> bool {
455 if self.area() < area_threshold {
457 return true;
458 }
459
460 if self.aspect_ratio() > max_aspect_ratio {
462 return true;
463 }
464
465 if self.min_edge_length() < min_edge_length {
467 return true;
468 }
469
470 false
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 fn approx_eq(a: f64, b: f64) -> bool {
479 (a - b).abs() < 1e-10
480 }
481
482 #[test]
483 fn test_vertex_creation() {
484 let v = Vertex::from_coords(1.0, 2.0, 3.0);
485 assert!(approx_eq(v.position.x, 1.0));
486 assert!(approx_eq(v.position.y, 2.0));
487 assert!(approx_eq(v.position.z, 3.0));
488 assert!(v.normal.is_none());
489 assert!(v.tag.is_none());
490 assert!(v.offset.is_none());
491 }
492
493 #[test]
494 fn test_triangle_normal() {
495 let tri = Triangle::new(
496 Point3::new(0.0, 0.0, 0.0),
497 Point3::new(1.0, 0.0, 0.0),
498 Point3::new(0.0, 1.0, 0.0),
499 );
500
501 let normal = tri.normal().expect("non-degenerate triangle");
502 assert!(approx_eq(normal.x, 0.0));
503 assert!(approx_eq(normal.y, 0.0));
504 assert!(approx_eq(normal.z, 1.0));
505 }
506
507 #[test]
508 fn test_triangle_area() {
509 let tri = Triangle::new(
510 Point3::new(0.0, 0.0, 0.0),
511 Point3::new(1.0, 0.0, 0.0),
512 Point3::new(0.0, 1.0, 0.0),
513 );
514 assert!(approx_eq(tri.area(), 0.5));
515 }
516
517 #[test]
518 fn test_triangle_centroid() {
519 let tri = Triangle::new(
520 Point3::new(0.0, 0.0, 0.0),
521 Point3::new(3.0, 0.0, 0.0),
522 Point3::new(0.0, 3.0, 0.0),
523 );
524 let c = tri.centroid();
525 assert!(approx_eq(c.x, 1.0));
526 assert!(approx_eq(c.y, 1.0));
527 assert!(approx_eq(c.z, 0.0));
528 }
529
530 #[test]
531 fn test_degenerate_triangle_normal() {
532 let tri = Triangle::new(
533 Point3::new(0.0, 0.0, 0.0),
534 Point3::new(1.0, 0.0, 0.0),
535 Point3::new(2.0, 0.0, 0.0),
536 );
537 assert!(tri.normal().is_none());
538 }
539
540 #[test]
541 fn test_mesh_bounds() {
542 let mut mesh = Mesh::new();
543 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
544 mesh.vertices.push(Vertex::from_coords(10.0, 5.0, 3.0));
545 mesh.vertices.push(Vertex::from_coords(-2.0, 8.0, 1.0));
546
547 let (min, max) = mesh.bounds().expect("non-empty mesh");
548 assert!(approx_eq(min.x, -2.0));
549 assert!(approx_eq(min.y, 0.0));
550 assert!(approx_eq(min.z, 0.0));
551 assert!(approx_eq(max.x, 10.0));
552 assert!(approx_eq(max.y, 8.0));
553 assert!(approx_eq(max.z, 3.0));
554 }
555
556 #[test]
557 fn test_empty_mesh_bounds() {
558 let mesh = Mesh::new();
559 assert!(mesh.bounds().is_none());
560 }
561
562 #[test]
563 fn test_mesh_is_empty() {
564 let mesh = Mesh::new();
565 assert!(mesh.is_empty());
566
567 let mut mesh2 = Mesh::new();
568 mesh2.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
569 assert!(mesh2.is_empty()); mesh2.faces.push([0, 0, 0]);
572 assert!(!mesh2.is_empty());
573 }
574
575 #[test]
576 fn test_triangle_edge_lengths() {
577 let tri = Triangle::new(
579 Point3::new(0.0, 0.0, 0.0),
580 Point3::new(3.0, 0.0, 0.0),
581 Point3::new(0.0, 4.0, 0.0),
582 );
583 let lengths = tri.edge_lengths();
584 assert!(approx_eq(lengths[0], 3.0)); assert!(approx_eq(lengths[1], 5.0)); assert!(approx_eq(lengths[2], 4.0)); }
588
589 #[test]
590 fn test_triangle_min_max_edge_length() {
591 let tri = Triangle::new(
593 Point3::new(0.0, 0.0, 0.0),
594 Point3::new(3.0, 0.0, 0.0),
595 Point3::new(0.0, 4.0, 0.0),
596 );
597 assert!(approx_eq(tri.min_edge_length(), 3.0));
598 assert!(approx_eq(tri.max_edge_length(), 5.0));
599 }
600
601 #[test]
602 fn test_triangle_aspect_ratio_equilateral() {
603 let sqrt3 = 3.0_f64.sqrt();
605 let tri = Triangle::new(
606 Point3::new(0.0, 0.0, 0.0),
607 Point3::new(2.0, 0.0, 0.0),
608 Point3::new(1.0, sqrt3, 0.0),
609 );
610 let ar = tri.aspect_ratio();
612 assert!(
613 ar > 1.1 && ar < 1.2,
614 "Equilateral aspect ratio should be ~1.15, got {}",
615 ar
616 );
617 }
618
619 #[test]
620 fn test_triangle_aspect_ratio_thin() {
621 let tri = Triangle::new(
623 Point3::new(0.0, 0.0, 0.0),
624 Point3::new(100.0, 0.0, 0.0),
625 Point3::new(50.0, 0.1, 0.0),
626 );
627 let ar = tri.aspect_ratio();
628 assert!(
630 ar > 100.0,
631 "Thin triangle should have high aspect ratio, got {}",
632 ar
633 );
634 }
635
636 #[test]
637 fn test_triangle_aspect_ratio_degenerate() {
638 let tri = Triangle::new(
640 Point3::new(0.0, 0.0, 0.0),
641 Point3::new(1.0, 0.0, 0.0),
642 Point3::new(2.0, 0.0, 0.0),
643 );
644 assert!(tri.aspect_ratio().is_infinite());
645 }
646
647 #[test]
648 fn test_triangle_is_nearly_collinear() {
649 let tri_collinear = Triangle::new(
651 Point3::new(0.0, 0.0, 0.0),
652 Point3::new(1.0, 0.0, 0.0),
653 Point3::new(2.0, 0.0, 0.0),
654 );
655 assert!(tri_collinear.is_nearly_collinear(0.01));
656
657 let tri_nearly = Triangle::new(
659 Point3::new(0.0, 0.0, 0.0),
660 Point3::new(100.0, 0.0, 0.0),
661 Point3::new(50.0, 0.001, 0.0),
662 );
663 assert!(tri_nearly.is_nearly_collinear(0.001));
664
665 let tri_good = Triangle::new(
667 Point3::new(0.0, 0.0, 0.0),
668 Point3::new(1.0, 0.0, 0.0),
669 Point3::new(0.5, 1.0, 0.0),
670 );
671 assert!(!tri_good.is_nearly_collinear(0.01));
672 }
673
674 #[test]
675 fn test_triangle_is_degenerate_basic() {
676 let tri_zero_area = Triangle::new(
678 Point3::new(0.0, 0.0, 0.0),
679 Point3::new(1.0, 0.0, 0.0),
680 Point3::new(2.0, 0.0, 0.0),
681 );
682 assert!(tri_zero_area.is_degenerate(1e-9));
683
684 let tri_good = Triangle::new(
686 Point3::new(0.0, 0.0, 0.0),
687 Point3::new(1.0, 0.0, 0.0),
688 Point3::new(0.0, 1.0, 0.0),
689 );
690 assert!(!tri_good.is_degenerate(1e-9));
691 }
692
693 #[test]
694 fn test_triangle_is_degenerate_enhanced() {
695 let tri_good = Triangle::new(
697 Point3::new(0.0, 0.0, 0.0),
698 Point3::new(1.0, 0.0, 0.0),
699 Point3::new(0.5, 1.0, 0.0),
700 );
701 assert!(!tri_good.is_degenerate_enhanced(1e-9, 1000.0, 1e-9));
702
703 let tri_tiny = Triangle::new(
705 Point3::new(0.0, 0.0, 0.0),
706 Point3::new(1e-6, 0.0, 0.0),
707 Point3::new(0.0, 1e-6, 0.0),
708 );
709 assert!(tri_tiny.is_degenerate_enhanced(1e-9, 1000.0, 1e-9));
710
711 let tri_thin = Triangle::new(
713 Point3::new(0.0, 0.0, 0.0),
714 Point3::new(100.0, 0.0, 0.0),
715 Point3::new(50.0, 0.01, 0.0),
716 );
717 assert!(tri_thin.is_degenerate_enhanced(1e-9, 100.0, 1e-9));
718
719 let tri_short_edge = Triangle::new(
721 Point3::new(0.0, 0.0, 0.0),
722 Point3::new(1e-6, 0.0, 0.0),
723 Point3::new(0.5, 1.0, 0.0),
724 );
725 assert!(tri_short_edge.is_degenerate_enhanced(1e-12, 1000.0, 1e-5));
726 }
727
728 #[test]
729 fn test_triangle_edges() {
730 let tri = Triangle::new(
731 Point3::new(0.0, 0.0, 0.0),
732 Point3::new(1.0, 0.0, 0.0),
733 Point3::new(0.0, 1.0, 0.0),
734 );
735 let edges = tri.edges();
736
737 assert!(approx_eq(edges[0].0.x, 0.0) && approx_eq(edges[0].1.x, 1.0));
739 assert!(approx_eq(edges[1].0.x, 1.0) && approx_eq(edges[1].1.y, 1.0));
741 assert!(approx_eq(edges[2].0.y, 1.0) && approx_eq(edges[2].1.x, 0.0));
743 }
744
745 fn make_unit_cube() -> Mesh {
748 let mut mesh = Mesh::new();
749
750 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(1.0, 1.0, 0.0)); mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0)); mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 1.0)); mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 1.0)); mesh.vertices.push(Vertex::from_coords(1.0, 1.0, 1.0)); mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 1.0)); mesh.faces.push([0, 2, 1]);
764 mesh.faces.push([0, 3, 2]);
765
766 mesh.faces.push([4, 5, 6]);
768 mesh.faces.push([4, 6, 7]);
769
770 mesh.faces.push([0, 1, 5]);
772 mesh.faces.push([0, 5, 4]);
773
774 mesh.faces.push([3, 7, 6]);
776 mesh.faces.push([3, 6, 2]);
777
778 mesh.faces.push([0, 4, 7]);
780 mesh.faces.push([0, 7, 3]);
781
782 mesh.faces.push([1, 2, 6]);
784 mesh.faces.push([1, 6, 5]);
785
786 mesh
787 }
788
789 #[test]
790 fn test_signed_volume_unit_cube() {
791 let mesh = make_unit_cube();
792 let vol = mesh.signed_volume();
793 assert!(
795 (vol - 1.0).abs() < 1e-10,
796 "Unit cube signed volume should be 1.0, got {}",
797 vol
798 );
799 }
800
801 #[test]
802 fn test_volume_unit_cube() {
803 let mesh = make_unit_cube();
804 let vol = mesh.volume();
805 assert!(
806 (vol - 1.0).abs() < 1e-10,
807 "Unit cube volume should be 1.0, got {}",
808 vol
809 );
810 }
811
812 #[test]
813 fn test_signed_volume_scaled_cube() {
814 let mut mesh = make_unit_cube();
815 mesh.scale(2.0); let vol = mesh.signed_volume();
817 assert!(
819 (vol - 8.0).abs() < 1e-10,
820 "2x2x2 cube signed volume should be 8.0, got {}",
821 vol
822 );
823 }
824
825 #[test]
826 fn test_signed_volume_inverted_cube() {
827 let mut mesh = make_unit_cube();
828 for face in &mut mesh.faces {
830 face.swap(1, 2);
831 }
832 let vol = mesh.signed_volume();
833 assert!(
835 (vol + 1.0).abs() < 1e-10,
836 "Inverted cube signed volume should be -1.0, got {}",
837 vol
838 );
839 }
840
841 #[test]
842 fn test_is_inside_out_normal_cube() {
843 let mesh = make_unit_cube();
844 assert!(
845 !mesh.is_inside_out(),
846 "Normal cube should not be inside-out"
847 );
848 }
849
850 #[test]
851 fn test_is_inside_out_inverted_cube() {
852 let mut mesh = make_unit_cube();
853 for face in &mut mesh.faces {
855 face.swap(1, 2);
856 }
857 assert!(mesh.is_inside_out(), "Inverted cube should be inside-out");
858 }
859
860 #[test]
861 fn test_signed_volume_tetrahedron() {
862 let mut mesh = Mesh::new();
864 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::from_coords(0.5, 0.866025, 0.0)); mesh.vertices
868 .push(Vertex::from_coords(0.5, 0.288675, 0.816497)); mesh.faces.push([0, 2, 1]); mesh.faces.push([0, 1, 3]); mesh.faces.push([1, 2, 3]); mesh.faces.push([2, 0, 3]); let vol = mesh.signed_volume();
877 assert!(
880 vol > 0.1 && vol < 0.15,
881 "Tetrahedron volume should be ~0.1178, got {}",
882 vol
883 );
884 }
885
886 #[test]
887 fn test_signed_volume_translated_cube() {
888 let mut mesh = make_unit_cube();
889 mesh.translate(Vector3::new(10.0, 20.0, 30.0));
891 let vol = mesh.signed_volume();
892 assert!(
894 (vol - 1.0).abs() < 1e-10,
895 "Translated cube volume should still be 1.0, got {}",
896 vol
897 );
898 }
899
900 #[test]
901 fn test_signed_volume_empty_mesh() {
902 let mesh = Mesh::new();
903 let vol = mesh.signed_volume();
904 assert!(
905 vol.abs() < 1e-10,
906 "Empty mesh volume should be 0, got {}",
907 vol
908 );
909 }
910
911 #[test]
912 fn test_surface_area_unit_cube() {
913 let mesh = make_unit_cube();
914 let area = mesh.surface_area();
915 assert!(
917 (area - 6.0).abs() < 1e-10,
918 "Unit cube surface area should be 6.0, got {}",
919 area
920 );
921 }
922
923 #[test]
924 fn test_surface_area_single_triangle() {
925 let mut mesh = Mesh::new();
926 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
927 mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
928 mesh.vertices.push(Vertex::from_coords(0.0, 1.0, 0.0));
929 mesh.faces.push([0, 1, 2]);
930
931 let area = mesh.surface_area();
932 assert!(
934 (area - 0.5).abs() < 1e-10,
935 "Triangle area should be 0.5, got {}",
936 area
937 );
938 }
939
940 #[test]
941 fn test_surface_area_empty_mesh() {
942 let mesh = Mesh::new();
943 let area = mesh.surface_area();
944 assert!(
945 area.abs() < 1e-10,
946 "Empty mesh area should be 0, got {}",
947 area
948 );
949 }
950}