1#[cfg(feature = "aabb-cull")]
2use crate::bounds::Aabb;
3#[cfg(feature = "render-layers")]
4use crate::render_layers::RenderLayers;
5#[cfg(feature = "lod-crossfade")]
6use core::cell::Cell;
7use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
8use heapless::Vec;
9use heapless::index_set::FnvIndexSet;
10use log::error;
11use nalgebra::{Point3, Similarity3, UnitQuaternion, Vector3};
12
13#[cfg(not(feature = "std"))]
14#[allow(unused_imports)]
15use micromath::F32Ext;
16
17#[derive(Debug, PartialEq, Clone)]
18pub enum RenderMode {
19 Points,
20 Lines,
21 Solid,
22 #[cfg(feature = "lighting")]
23 SolidLightDir(Vector3<f32>),
24 #[cfg(feature = "lighting")]
25 BlinnPhong {
26 light_dir: Vector3<f32>,
27 specular_intensity: f32,
28 shininess: f32,
29 },
30 #[cfg(feature = "lighting")]
31 GouraudLightDir(Vector3<f32>),
32 #[cfg(feature = "lighting")]
35 SectorBright(u8),
36 #[cfg(feature = "textured")]
43 Textured,
44 #[cfg(feature = "textured")]
46 TexturedGouraud(Vector3<f32>),
47 #[cfg(feature = "textured")]
51 MatCap,
52 #[cfg(feature = "lighting")]
54 Toon(Vector3<f32>, u8),
55}
56#[derive(Debug, Default, Copy, Clone)]
57pub struct Geometry<'a> {
58 pub vertices: &'a [[f32; 3]],
59 pub faces: &'a [[usize; 3]],
60 pub colors: &'a [Rgb565],
61 pub lines: &'a [[usize; 2]],
62 pub normals: &'a [[f32; 3]],
63 pub vertex_normals: &'a [[f32; 3]],
66 pub uvs: &'a [[f32; 2]],
68 pub texture_id: Option<u32>,
70}
71
72impl Geometry<'_> {
73 fn check_validity(&self) -> bool {
74 if self.vertices.is_empty() {
75 error!("Vertices are empty");
76 return false;
77 }
78
79 for face in self.faces {
80 if face[0] >= self.vertices.len()
81 || face[1] >= self.vertices.len()
82 || face[2] >= self.vertices.len()
83 {
84 error!("Face vertices are out of bounds");
85 return false;
86 }
87 }
88
89 for line in self.lines {
90 if line[0] >= self.vertices.len() || line[1] >= self.vertices.len() {
91 error!("Line vertices are out of bounds");
92 return false;
93 }
94 }
95
96 if !self.colors.is_empty() && self.colors.len() != self.vertices.len() {
97 error!("Colors are not the same length as vertices");
98 return false;
99 }
100
101 if !self.uvs.is_empty() && self.uvs.len() != self.vertices.len() {
102 error!("UVs are not the same length as vertices");
103 return false;
104 }
105
106 if !self.vertex_normals.is_empty() && self.vertex_normals.len() != self.vertices.len() {
107 error!("Vertex normals are not the same length as vertices");
108 return false;
109 }
110
111 true
112 }
113
114 pub fn lines_from_faces<const N: usize>(faces: &[[usize; 3]]) -> Vec<(usize, usize), N> {
124 let mut set: FnvIndexSet<(usize, usize), N> = FnvIndexSet::new();
125 for face in faces {
126 for &(i1, i2) in &[(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
127 let edge = if i1 < i2 { (i1, i2) } else { (i2, i1) };
128 if set.insert(edge).is_err() {
129 error!(
130 "lines_from_faces: heapless Vec capacity exceeded (max {}). Some edges will not be rendered.",
131 N
132 );
133 break;
134 }
135 }
136 }
137 set.iter().copied().collect()
138 }
139
140 #[inline]
142 pub fn calculate_face_normal(v0: &[f32; 3], v1: &[f32; 3], v2: &[f32; 3]) -> [f32; 3] {
143 let edge1 = Vector3::new(v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]);
144 let edge2 = Vector3::new(v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]);
145 let cross = edge1.cross(&edge2);
146 let norm_sq = cross.norm_squared();
147 if norm_sq > 1e-12 {
148 let inv_len = 1.0 / nalgebra::ComplexField::sqrt(norm_sq);
149 [cross.x * inv_len, cross.y * inv_len, cross.z * inv_len]
150 } else {
151 [0.0, 1.0, 0.0]
152 }
153 }
154
155 pub fn compute_face_normals_into(
160 vertices: &[[f32; 3]],
161 faces: &[[usize; 3]],
162 out_normals: &mut [[f32; 3]],
163 ) -> usize {
164 let count = faces.len().min(out_normals.len());
165 for (i, face) in faces.iter().take(count).enumerate() {
166 if face[0] < vertices.len() && face[1] < vertices.len() && face[2] < vertices.len() {
167 out_normals[i] = Self::calculate_face_normal(
168 &vertices[face[0]],
169 &vertices[face[1]],
170 &vertices[face[2]],
171 );
172 } else {
173 out_normals[i] = [0.0, 1.0, 0.0];
174 }
175 }
176 count
177 }
178
179 #[cfg(feature = "std")]
181 pub fn compute_face_normals(
182 vertices: &[[f32; 3]],
183 faces: &[[usize; 3]],
184 ) -> std::vec::Vec<[f32; 3]> {
185 let mut normals = std::vec::Vec::with_capacity(faces.len());
186 for face in faces {
187 if face[0] < vertices.len() && face[1] < vertices.len() && face[2] < vertices.len() {
188 normals.push(Self::calculate_face_normal(
189 &vertices[face[0]],
190 &vertices[face[1]],
191 &vertices[face[2]],
192 ));
193 } else {
194 normals.push([0.0, 1.0, 0.0]);
195 }
196 }
197 normals
198 }
199}
200
201#[derive(Debug, Clone, Copy)]
211pub struct LODLevels {
212 pub high_distance: f32,
214 pub medium_distance: f32,
216 #[cfg(feature = "lod-crossfade")]
219 pub fade_margin: f32,
220}
221
222impl Default for LODLevels {
223 fn default() -> Self {
224 Self {
225 high_distance: 50.0,
226 medium_distance: 100.0,
227 #[cfg(feature = "lod-crossfade")]
228 fade_margin: 0.0,
229 }
230 }
231}
232
233#[cfg(feature = "lod-crossfade")]
235#[derive(Debug, Clone, Copy)]
236pub enum LodPick<'a> {
237 Single(&'a Geometry<'a>),
239 Crossfade {
241 near: &'a Geometry<'a>,
242 far: &'a Geometry<'a>,
243 t: f32,
244 },
245}
246
247pub struct K3dMesh<'a> {
249 pub similarity: Similarity3<f32>,
250 pub model_matrix: nalgebra::Matrix4<f32>,
251
252 pub color: Rgb565,
253 pub render_mode: RenderMode,
254 pub geometry: Geometry<'a>,
255
256 pub lod_medium: Option<Geometry<'a>>,
259 pub lod_low: Option<Geometry<'a>>,
260 pub lod_levels: LODLevels,
261 pub priority: u8,
262 pub outline_color: Option<Rgb565>,
263 pub outline_width: f32,
264 #[cfg(feature = "aabb-cull")]
267 pub aabb: Option<Aabb>,
268 #[cfg(feature = "render-layers")]
270 pub layers: RenderLayers,
271 #[cfg(feature = "lod-crossfade")]
274 pub(crate) lod_force: Cell<Option<u8>>,
275 #[cfg(feature = "lod-crossfade")]
277 pub(crate) draw_alpha: Cell<Option<u8>>,
278}
279
280impl<'a> K3dMesh<'a> {
281 pub fn new(geometry: Geometry) -> K3dMesh {
282 assert!(geometry.check_validity());
283 let sim = Similarity3::new(Vector3::new(0.0, 0.0, 0.0), nalgebra::zero(), 1.0);
284 K3dMesh {
285 model_matrix: sim.to_homogeneous(),
286 similarity: sim,
287 color: Rgb565::CSS_WHITE,
288 render_mode: RenderMode::Points,
289 geometry,
290 lod_medium: None,
291 lod_low: None,
292 lod_levels: LODLevels::default(),
293 priority: 128,
294 outline_color: None,
295 outline_width: 0.0,
296 #[cfg(feature = "aabb-cull")]
297 aabb: Aabb::enclosing(geometry.vertices.iter()),
298 #[cfg(feature = "render-layers")]
299 layers: RenderLayers::DEFAULT,
300 #[cfg(feature = "lod-crossfade")]
301 lod_force: Cell::new(None),
302 #[cfg(feature = "lod-crossfade")]
303 draw_alpha: Cell::new(None),
304 }
305 }
306
307 pub fn set_lod<'b>(
314 &mut self,
315 medium: Option<Geometry<'b>>,
316 low: Option<Geometry<'b>>,
317 levels: LODLevels,
318 ) where
319 'b: 'a,
320 {
321 if let Some(ref geom) = medium {
322 assert!(geom.check_validity());
323 }
324 if let Some(ref geom) = low {
325 assert!(geom.check_validity());
326 }
327 self.lod_medium = medium;
328 self.lod_low = low;
329 self.lod_levels = levels;
330 }
331
332 #[inline]
334 pub fn select_lod(&self, distance: f32) -> &Geometry<'_> {
335 #[cfg(feature = "lod-crossfade")]
336 {
337 if let Some(level) = self.lod_force.get() {
338 return self.geometry_for_lod_level(level);
339 }
340 match self.select_lod_pick(distance) {
341 LodPick::Single(g) => g,
342 LodPick::Crossfade { near, far, t } => {
343 if t < 0.5 {
344 near
345 } else {
346 far
347 }
348 }
349 }
350 }
351 #[cfg(not(feature = "lod-crossfade"))]
352 {
353 if distance < self.lod_levels.high_distance {
354 &self.geometry
355 } else if distance < self.lod_levels.medium_distance {
356 self.lod_medium.as_ref().unwrap_or(&self.geometry)
357 } else {
358 self.lod_low
359 .as_ref()
360 .unwrap_or(self.lod_medium.as_ref().unwrap_or(&self.geometry))
361 }
362 }
363 }
364
365 #[cfg(feature = "lod-crossfade")]
366 #[inline]
367 fn geometry_for_lod_level(&self, level: u8) -> &Geometry<'_> {
368 match level {
369 0 => &self.geometry,
370 1 => self.lod_medium.as_ref().unwrap_or(&self.geometry),
371 _ => self
372 .lod_low
373 .as_ref()
374 .unwrap_or(self.lod_medium.as_ref().unwrap_or(&self.geometry)),
375 }
376 }
377
378 #[cfg(feature = "lod-crossfade")]
380 #[inline]
381 pub(crate) fn lod_level_of(&self, geom: &Geometry<'_>) -> u8 {
382 if core::ptr::eq(geom as *const _, &self.geometry as *const _) {
383 0
384 } else if self
385 .lod_medium
386 .as_ref()
387 .is_some_and(|m| core::ptr::eq(geom as *const _, m as *const _))
388 {
389 1
390 } else {
391 2
392 }
393 }
394
395 #[cfg(feature = "lod-crossfade")]
397 #[inline]
398 pub fn select_lod_pick(&self, distance: f32) -> LodPick<'_> {
399 let high = &self.geometry;
400 let medium = self.lod_medium.as_ref().unwrap_or(high);
401 let low = self
402 .lod_low
403 .as_ref()
404 .unwrap_or(self.lod_medium.as_ref().unwrap_or(high));
405
406 let margin = self.lod_levels.fade_margin;
407 if margin <= 0.0 {
408 return if distance < self.lod_levels.high_distance {
409 LodPick::Single(high)
410 } else if distance < self.lod_levels.medium_distance {
411 LodPick::Single(medium)
412 } else {
413 LodPick::Single(low)
414 };
415 }
416
417 let h = self.lod_levels.high_distance;
418 let m = self.lod_levels.medium_distance;
419
420 if distance < h - margin {
421 LodPick::Single(high)
422 } else if distance < h + margin {
423 let t = ((distance - (h - margin)) / (2.0 * margin)).clamp(0.0, 1.0);
424 if core::ptr::eq(high as *const _, medium as *const _) {
425 LodPick::Single(high)
426 } else {
427 LodPick::Crossfade {
428 near: high,
429 far: medium,
430 t,
431 }
432 }
433 } else if distance < m - margin {
434 LodPick::Single(medium)
435 } else if distance < m + margin {
436 let t = ((distance - (m - margin)) / (2.0 * margin)).clamp(0.0, 1.0);
437 if core::ptr::eq(medium as *const _, low as *const _) {
438 LodPick::Single(medium)
439 } else {
440 LodPick::Crossfade {
441 near: medium,
442 far: low,
443 t,
444 }
445 }
446 } else {
447 LodPick::Single(low)
448 }
449 }
450
451 pub fn set_color(&mut self, color: Rgb565) {
452 self.color = color;
453 }
454
455 pub fn set_render_mode(&mut self, mode: RenderMode) {
456 self.render_mode = mode;
457 }
458
459 pub fn set_priority(&mut self, priority: u8) {
460 self.priority = priority;
461 }
462
463 #[cfg(feature = "render-layers")]
464 pub fn set_layers(&mut self, layers: RenderLayers) {
465 self.layers = layers;
466 }
467
468 #[cfg(feature = "aabb-cull")]
470 pub fn cache_aabb(&mut self) {
471 self.aabb = Aabb::enclosing(self.geometry.vertices.iter());
472 }
473
474 #[cfg(feature = "aabb-cull")]
476 pub fn set_aabb(&mut self, aabb: Aabb) {
477 self.aabb = Some(aabb);
478 }
479
480 #[cfg(feature = "aabb-cull")]
482 #[inline]
483 pub fn model_aabb(&self) -> Aabb {
484 self.aabb
485 .unwrap_or_else(|| Aabb::enclosing(self.geometry.vertices.iter()).unwrap_or(Aabb::ZERO))
486 }
487
488 pub fn set_position(&mut self, x: f32, y: f32, z: f32) {
489 self.similarity.isometry.translation.x = x;
490 self.similarity.isometry.translation.y = y;
491 self.similarity.isometry.translation.z = z;
492 self.update_model_matrix();
493 }
494
495 pub fn get_position(&self) -> Point3<f32> {
496 self.similarity.isometry.translation.vector.into()
497 }
498
499 pub fn set_attitude(&mut self, roll: f32, pitch: f32, yaw: f32) {
500 self.similarity.isometry.rotation = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
501 self.update_model_matrix();
502 }
503
504 pub fn set_rotation(&mut self, rotation: UnitQuaternion<f32>) {
506 self.similarity.isometry.rotation = rotation;
507 self.update_model_matrix();
508 }
509
510 pub fn set_target(&mut self, target: Point3<f32>) {
511 let view = Similarity3::look_at_rh(
512 &self.similarity.isometry.translation.vector.into(),
513 &target,
514 &Vector3::y(),
515 1.0,
516 );
517
518 self.similarity = view;
519 self.model_matrix = self.similarity.to_homogeneous();
520 }
521
522 pub fn set_scale(&mut self, s: f32) {
523 if s == 0.0 {
524 return;
525 }
526 self.similarity.set_scaling(s);
527 self.update_model_matrix();
528 }
529
530 fn update_model_matrix(&mut self) {
531 self.model_matrix = self.similarity.to_homogeneous();
532 }
533
534 #[inline]
540 pub fn compute_bounding_radius_sq(&self) -> f32 {
541 let scale = self.similarity.scaling();
542 let scale_sq = scale * scale;
543 #[cfg(feature = "aabb-cull")]
544 if let Some(aabb) = self.aabb {
545 let center_sq = aabb.center.norm_squared();
546 let r = aabb.radius();
547 let rad = r + center_sq.sqrt();
548 return rad * rad * scale_sq;
549 }
550 let mut max_dist_sq = 0.0f32;
551 for vertex in self.geometry.vertices {
552 let dist_sq = vertex[0] * vertex[0] + vertex[1] * vertex[1] + vertex[2] * vertex[2];
553 if dist_sq > max_dist_sq {
554 max_dist_sq = dist_sq;
555 }
556 }
557 max_dist_sq * scale_sq
558 }
559}
560
561pub fn compute_vertex_normals<const V: usize>(
571 vertices: &[[f32; 3]],
572 faces: &[[usize; 3]],
573 face_normals: &[[f32; 3]],
574) -> Vec<[f32; 3], V> {
575 let mut normals = Vec::<[f32; 3], V>::new();
576 for _ in 0..vertices.len() {
577 if normals.push([0.0, 0.0, 0.0]).is_err() {
578 break;
579 }
580 }
581
582 for (face, fn_arr) in faces.iter().zip(face_normals.iter()) {
583 for &vi in face {
584 if vi < normals.len() {
585 normals[vi][0] += fn_arr[0];
586 normals[vi][1] += fn_arr[1];
587 normals[vi][2] += fn_arr[2];
588 }
589 }
590 }
591
592 for n in normals.iter_mut() {
593 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
594 if len > 1e-10 {
595 n[0] /= len;
596 n[1] /= len;
597 n[2] /= len;
598 }
599 }
600
601 normals
602}
603
604#[cfg(test)]
605mod tests {
606 extern crate std;
607 use super::*;
608
609 #[test]
610 fn test_geometry_validation_valid() {
611 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
612 let faces = [[0, 1, 2]];
613
614 let geometry = Geometry {
615 vertices: &vertices,
616 faces: &faces,
617 colors: &[],
618 lines: &[],
619 normals: &[],
620 vertex_normals: &[],
621 uvs: &[],
622 texture_id: None,
623 };
624
625 assert!(geometry.check_validity());
626 }
627
628 #[test]
629 #[should_panic]
630 fn test_geometry_validation_invalid_face_index() {
631 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
632 let faces = [[0, 1, 5]]; let geometry = Geometry {
635 vertices: &vertices,
636 faces: &faces,
637 colors: &[],
638 lines: &[],
639 normals: &[],
640 vertex_normals: &[],
641 uvs: &[],
642 texture_id: None,
643 };
644
645 K3dMesh::new(geometry);
647 }
648
649 #[test]
650 #[should_panic]
651 fn test_geometry_validation_invalid_line_index() {
652 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
653 let lines = [[0, 10]]; let geometry = Geometry {
656 vertices: &vertices,
657 faces: &[],
658 colors: &[],
659 lines: &lines,
660 normals: &[],
661 vertex_normals: &[],
662 uvs: &[],
663 texture_id: None,
664 };
665
666 K3dMesh::new(geometry);
667 }
668
669 #[test]
670 #[should_panic]
671 fn test_geometry_validation_color_length_mismatch() {
672 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
673 let colors = [Rgb565::CSS_RED]; let geometry = Geometry {
676 vertices: &vertices,
677 faces: &[],
678 colors: &colors,
679 lines: &[],
680 normals: &[],
681 vertex_normals: &[],
682 uvs: &[],
683 texture_id: None,
684 };
685
686 K3dMesh::new(geometry);
687 }
688
689 #[test]
690 fn test_lines_from_faces_basic() {
691 let faces = [[0, 1, 2]];
692 let lines = Geometry::lines_from_faces::<16>(&faces);
693
694 assert_eq!(lines.len(), 3);
696
697 let expected_edges = [(0, 1), (0, 2), (1, 2)];
699 for edge in expected_edges.iter() {
700 assert!(lines.contains(edge));
701 }
702 }
703
704 #[test]
705 fn test_lines_from_faces_shared_edges() {
706 let faces = [[0, 1, 2], [0, 2, 3]];
707 let lines = Geometry::lines_from_faces::<16>(&faces);
708
709 assert_eq!(lines.len(), 5);
711 }
712
713 #[test]
714 fn test_lines_from_faces_capacity_limit() {
715 let faces = [[0, 1, 2], [3, 4, 5]];
716 let lines = Geometry::lines_from_faces::<16>(&faces);
718
719 assert_eq!(lines.len(), 6);
721 }
722
723 #[test]
724 fn test_mesh_creation() {
725 let vertices = [[0.0, 0.0, 0.0]];
726 let geometry = Geometry {
727 vertices: &vertices,
728 faces: &[],
729 colors: &[],
730 lines: &[],
731 normals: &[],
732 vertex_normals: &[],
733 uvs: &[],
734 texture_id: None,
735 };
736
737 let mesh = K3dMesh::new(geometry);
738 assert_eq!(mesh.color, Rgb565::CSS_WHITE);
739 assert_eq!(mesh.render_mode, RenderMode::Points);
740 assert_eq!(mesh.get_position(), Point3::new(0.0, 0.0, 0.0));
741 }
742
743 #[test]
744 fn test_mesh_set_color() {
745 let vertices = [[0.0, 0.0, 0.0]];
746 let geometry = Geometry {
747 vertices: &vertices,
748 faces: &[],
749 colors: &[],
750 lines: &[],
751 normals: &[],
752 vertex_normals: &[],
753 uvs: &[],
754 texture_id: None,
755 };
756
757 let mut mesh = K3dMesh::new(geometry);
758 mesh.set_color(Rgb565::CSS_RED);
759 assert_eq!(mesh.color, Rgb565::CSS_RED);
760 }
761
762 #[test]
763 fn test_mesh_set_position() {
764 let vertices = [[0.0, 0.0, 0.0]];
765 let geometry = Geometry {
766 vertices: &vertices,
767 faces: &[],
768 colors: &[],
769 lines: &[],
770 normals: &[],
771 vertex_normals: &[],
772 uvs: &[],
773 texture_id: None,
774 };
775
776 let mut mesh = K3dMesh::new(geometry);
777 mesh.set_position(5.0, 10.0, 15.0);
778 assert_eq!(mesh.get_position(), Point3::new(5.0, 10.0, 15.0));
779 }
780
781 #[test]
782 fn test_mesh_set_scale() {
783 let vertices = [[0.0, 0.0, 0.0]];
784 let geometry = Geometry {
785 vertices: &vertices,
786 faces: &[],
787 colors: &[],
788 lines: &[],
789 normals: &[],
790 vertex_normals: &[],
791 uvs: &[],
792 texture_id: None,
793 };
794
795 let mut mesh = K3dMesh::new(geometry);
796 mesh.set_scale(2.0);
797 assert!((mesh.similarity.scaling() - 2.0).abs() < 0.001);
798 }
799
800 #[test]
801 fn test_mesh_set_scale_zero_ignored() {
802 let vertices = [[0.0, 0.0, 0.0]];
803 let geometry = Geometry {
804 vertices: &vertices,
805 faces: &[],
806 colors: &[],
807 lines: &[],
808 normals: &[],
809 vertex_normals: &[],
810 uvs: &[],
811 texture_id: None,
812 };
813
814 let mut mesh = K3dMesh::new(geometry);
815 let original_scale = mesh.similarity.scaling();
816 mesh.set_scale(0.0);
817 assert_eq!(mesh.similarity.scaling(), original_scale);
819 }
820
821 #[test]
822 fn test_mesh_set_attitude() {
823 let vertices = [[0.0, 0.0, 0.0]];
824 let geometry = Geometry {
825 vertices: &vertices,
826 faces: &[],
827 colors: &[],
828 lines: &[],
829 normals: &[],
830 vertex_normals: &[],
831 uvs: &[],
832 texture_id: None,
833 };
834
835 let mut mesh = K3dMesh::new(geometry);
836 mesh.set_attitude(0.1, 0.2, 0.3);
837 assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
839 }
840
841 #[test]
842 fn test_mesh_set_target() {
843 let vertices = [[0.0, 0.0, 0.0]];
844 let geometry = Geometry {
845 vertices: &vertices,
846 faces: &[],
847 colors: &[],
848 lines: &[],
849 normals: &[],
850 vertex_normals: &[],
851 uvs: &[],
852 texture_id: None,
853 };
854
855 let mut mesh = K3dMesh::new(geometry);
856 mesh.set_position(5.0, 5.0, 5.0);
857 mesh.set_target(Point3::new(0.0, 0.0, 0.0));
858 assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
861 }
862
863 #[test]
864 fn test_mesh_render_mode_changes() {
865 let vertices = [[0.0, 0.0, 0.0]];
866 let geometry = Geometry {
867 vertices: &vertices,
868 faces: &[],
869 colors: &[],
870 lines: &[],
871 normals: &[],
872 vertex_normals: &[],
873 uvs: &[],
874 texture_id: None,
875 };
876
877 let mut mesh = K3dMesh::new(geometry);
878
879 mesh.set_render_mode(RenderMode::Lines);
880 assert_eq!(mesh.render_mode, RenderMode::Lines);
881
882 mesh.set_render_mode(RenderMode::Solid);
883 assert_eq!(mesh.render_mode, RenderMode::Solid);
884
885 #[cfg(feature = "lighting")]
886 {
887 mesh.set_render_mode(RenderMode::SolidLightDir(Vector3::new(0.0, 1.0, 0.0)));
888 assert!(matches!(mesh.render_mode, RenderMode::SolidLightDir(_)));
889 }
890 }
891
892 #[test]
893 fn test_compute_vertex_normals_single_triangle() {
894 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
895 let faces = [[0, 1, 2]];
896 let face_normals = [[0.0, 0.0, 1.0]];
897
898 let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
899 assert_eq!(vn.len(), 3);
900 for n in vn.iter() {
901 assert!((n[0] - 0.0).abs() < 1e-5);
902 assert!((n[1] - 0.0).abs() < 1e-5);
903 assert!((n[2] - 1.0).abs() < 1e-5);
904 }
905 }
906
907 #[test]
908 fn test_compute_vertex_normals_shared_edge() {
909 let vertices = [
911 [0.0, 0.0, 0.0],
912 [1.0, 0.0, 0.0],
913 [0.5, 0.0, 1.0],
914 [0.5, 1.0, 0.0],
915 ];
916 let faces = [[0, 1, 2], [0, 1, 3]];
917 let face_normals = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
918
919 let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
920 assert_eq!(vn.len(), 4);
921
922 let _expected_len = (0.5f32 * 0.5 + 0.5 * 0.5).sqrt(); for i in 0..2 {
925 let n = &vn[i];
926 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
927 assert!((len - 1.0).abs() < 1e-5, "Normal should be unit length");
928 assert!(
929 (n[1] - n[2]).abs() < 1e-5,
930 "Y and Z components should be equal for shared verts"
931 );
932 }
933
934 assert!((vn[2][2] - 1.0).abs() < 1e-5);
936 assert!((vn[3][1] - 1.0).abs() < 1e-5);
938 }
939
940 #[test]
941 fn test_geometry_validation_vertex_normals_length_mismatch() {
942 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
943 let vn = [[0.0, 0.0, 1.0]]; let geometry = Geometry {
946 vertices: &vertices,
947 faces: &[],
948 colors: &[],
949 lines: &[],
950 normals: &[],
951 vertex_normals: &vn,
952 uvs: &[],
953 texture_id: None,
954 };
955
956 assert!(!geometry.check_validity());
957 }
958
959 #[cfg(feature = "lod-crossfade")]
960 #[test]
961 fn test_lod_crossfade_pick() {
962 let high = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
963 let med = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
964 let faces = [[0usize, 1, 2]];
965 let mut mesh = K3dMesh::new(Geometry {
966 vertices: &high,
967 faces: &faces,
968 colors: &[],
969 lines: &[],
970 normals: &[],
971 vertex_normals: &[],
972 uvs: &[],
973 texture_id: None,
974 });
975 mesh.set_lod(
976 Some(Geometry {
977 vertices: &med,
978 faces: &faces,
979 colors: &[],
980 lines: &[],
981 normals: &[],
982 vertex_normals: &[],
983 uvs: &[],
984 texture_id: None,
985 }),
986 None,
987 LODLevels {
988 high_distance: 10.0,
989 medium_distance: 20.0,
990 fade_margin: 2.0,
991 },
992 );
993 assert!(matches!(mesh.select_lod_pick(5.0), LodPick::Single(_)));
994 assert!(matches!(
995 mesh.select_lod_pick(10.0),
996 LodPick::Crossfade { .. }
997 ));
998 }
999
1000 #[cfg(feature = "aabb-cull")]
1001 #[test]
1002 fn test_mesh_aabb_cached_on_new() {
1003 let vertices = [[-2.0, -1.0, 0.0], [2.0, 1.0, 0.0]];
1004 let mesh = K3dMesh::new(Geometry {
1005 vertices: &vertices,
1006 faces: &[],
1007 colors: &[],
1008 lines: &[],
1009 normals: &[],
1010 vertex_normals: &[],
1011 uvs: &[],
1012 texture_id: None,
1013 });
1014 let aabb = mesh.aabb.expect("cached");
1015 assert!((aabb.half_extents.x - 2.0).abs() < 1e-5);
1016 assert!((aabb.half_extents.y - 1.0).abs() < 1e-5);
1017 }
1018
1019 #[test]
1020 fn test_compute_face_normals() {
1021 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1022 let faces = [[0usize, 1, 2]];
1023 let mut normals = [[0.0f32; 3]; 1];
1024
1025 let written = Geometry::compute_face_normals_into(&vertices, &faces, &mut normals);
1026 assert_eq!(written, 1);
1027 assert!((normals[0][0] - 0.0).abs() < 1e-5);
1029 assert!((normals[0][1] - 0.0).abs() < 1e-5);
1030 assert!((normals[0][2] - 1.0).abs() < 1e-5);
1031 }
1032}