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"))]
14use micromath::F32Ext;
15
16#[derive(Debug, PartialEq, Clone)]
17pub enum RenderMode {
18 Points,
19 Lines,
20 Solid,
21 SolidLightDir(Vector3<f32>),
22 BlinnPhong {
23 light_dir: Vector3<f32>,
24 specular_intensity: f32,
25 shininess: f32,
26 },
27 GouraudLightDir(Vector3<f32>),
28 SectorBright(u8),
31 Textured,
38 TexturedGouraud(Vector3<f32>),
40 MatCap,
44 Toon(Vector3<f32>, u8),
46}
47#[derive(Debug, Default, Copy, Clone)]
48pub struct Geometry<'a> {
49 pub vertices: &'a [[f32; 3]],
50 pub faces: &'a [[usize; 3]],
51 pub colors: &'a [Rgb565],
52 pub lines: &'a [[usize; 2]],
53 pub normals: &'a [[f32; 3]],
54 pub vertex_normals: &'a [[f32; 3]],
57 pub uvs: &'a [[f32; 2]],
59 pub texture_id: Option<u32>,
61}
62
63impl Geometry<'_> {
64 fn check_validity(&self) -> bool {
65 if self.vertices.is_empty() {
66 error!("Vertices are empty");
67 return false;
68 }
69
70 for face in self.faces {
71 if face[0] >= self.vertices.len()
72 || face[1] >= self.vertices.len()
73 || face[2] >= self.vertices.len()
74 {
75 error!("Face vertices are out of bounds");
76 return false;
77 }
78 }
79
80 for line in self.lines {
81 if line[0] >= self.vertices.len() || line[1] >= self.vertices.len() {
82 error!("Line vertices are out of bounds");
83 return false;
84 }
85 }
86
87 if !self.colors.is_empty() && self.colors.len() != self.vertices.len() {
88 error!("Colors are not the same length as vertices");
89 return false;
90 }
91
92 if !self.uvs.is_empty() && self.uvs.len() != self.vertices.len() {
93 error!("UVs are not the same length as vertices");
94 return false;
95 }
96
97 if !self.vertex_normals.is_empty() && self.vertex_normals.len() != self.vertices.len() {
98 error!("Vertex normals are not the same length as vertices");
99 return false;
100 }
101
102 true
103 }
104
105 pub fn lines_from_faces<const N: usize>(faces: &[[usize; 3]]) -> Vec<(usize, usize), N> {
115 let mut set: FnvIndexSet<(usize, usize), N> = FnvIndexSet::new();
116 for face in faces {
117 for &(i1, i2) in &[(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
118 let edge = if i1 < i2 { (i1, i2) } else { (i2, i1) };
119 if set.insert(edge).is_err() {
120 error!(
121 "lines_from_faces: heapless Vec capacity exceeded (max {}). Some edges will not be rendered.",
122 N
123 );
124 break;
125 }
126 }
127 }
128 set.iter().copied().collect()
129 }
130}
131
132#[derive(Debug, Clone, Copy)]
142pub struct LODLevels {
143 pub high_distance: f32,
145 pub medium_distance: f32,
147 #[cfg(feature = "lod-crossfade")]
150 pub fade_margin: f32,
151}
152
153impl Default for LODLevels {
154 fn default() -> Self {
155 Self {
156 high_distance: 50.0,
157 medium_distance: 100.0,
158 #[cfg(feature = "lod-crossfade")]
159 fade_margin: 0.0,
160 }
161 }
162}
163
164#[cfg(feature = "lod-crossfade")]
166#[derive(Debug, Clone, Copy)]
167pub enum LodPick<'a> {
168 Single(&'a Geometry<'a>),
170 Crossfade {
172 near: &'a Geometry<'a>,
173 far: &'a Geometry<'a>,
174 t: f32,
175 },
176}
177
178pub struct K3dMesh<'a> {
180 pub similarity: Similarity3<f32>,
181 pub model_matrix: nalgebra::Matrix4<f32>,
182
183 pub color: Rgb565,
184 pub render_mode: RenderMode,
185 pub geometry: Geometry<'a>,
186
187 pub lod_medium: Option<Geometry<'a>>,
190 pub lod_low: Option<Geometry<'a>>,
191 pub lod_levels: LODLevels,
192 pub priority: u8,
193 pub outline_color: Option<Rgb565>,
194 pub outline_width: f32,
195 #[cfg(feature = "aabb-cull")]
198 pub aabb: Option<Aabb>,
199 #[cfg(feature = "render-layers")]
201 pub layers: RenderLayers,
202 #[cfg(feature = "lod-crossfade")]
205 pub(crate) lod_force: Cell<Option<u8>>,
206 #[cfg(feature = "lod-crossfade")]
208 pub(crate) draw_alpha: Cell<Option<u8>>,
209}
210
211impl<'a> K3dMesh<'a> {
212 pub fn new(geometry: Geometry) -> K3dMesh {
213 assert!(geometry.check_validity());
214 let sim = Similarity3::new(Vector3::new(0.0, 0.0, 0.0), nalgebra::zero(), 1.0);
215 K3dMesh {
216 model_matrix: sim.to_homogeneous(),
217 similarity: sim,
218 color: Rgb565::CSS_WHITE,
219 render_mode: RenderMode::Points,
220 geometry,
221 lod_medium: None,
222 lod_low: None,
223 lod_levels: LODLevels::default(),
224 priority: 128,
225 outline_color: None,
226 outline_width: 0.0,
227 #[cfg(feature = "aabb-cull")]
228 aabb: Aabb::enclosing(geometry.vertices.iter()),
229 #[cfg(feature = "render-layers")]
230 layers: RenderLayers::DEFAULT,
231 #[cfg(feature = "lod-crossfade")]
232 lod_force: Cell::new(None),
233 #[cfg(feature = "lod-crossfade")]
234 draw_alpha: Cell::new(None),
235 }
236 }
237
238 pub fn set_lod<'b>(
245 &mut self,
246 medium: Option<Geometry<'b>>,
247 low: Option<Geometry<'b>>,
248 levels: LODLevels,
249 ) where
250 'b: 'a,
251 {
252 if let Some(ref geom) = medium {
253 assert!(geom.check_validity());
254 }
255 if let Some(ref geom) = low {
256 assert!(geom.check_validity());
257 }
258 self.lod_medium = medium;
259 self.lod_low = low;
260 self.lod_levels = levels;
261 }
262
263 #[inline]
265 pub fn select_lod(&self, distance: f32) -> &Geometry<'_> {
266 #[cfg(feature = "lod-crossfade")]
267 {
268 if let Some(level) = self.lod_force.get() {
269 return self.geometry_for_lod_level(level);
270 }
271 match self.select_lod_pick(distance) {
272 LodPick::Single(g) => g,
273 LodPick::Crossfade { near, far, t } => {
274 if t < 0.5 {
275 near
276 } else {
277 far
278 }
279 }
280 }
281 }
282 #[cfg(not(feature = "lod-crossfade"))]
283 {
284 if distance < self.lod_levels.high_distance {
285 &self.geometry
286 } else if distance < self.lod_levels.medium_distance {
287 self.lod_medium.as_ref().unwrap_or(&self.geometry)
288 } else {
289 self.lod_low
290 .as_ref()
291 .unwrap_or(self.lod_medium.as_ref().unwrap_or(&self.geometry))
292 }
293 }
294 }
295
296 #[cfg(feature = "lod-crossfade")]
297 #[inline]
298 fn geometry_for_lod_level(&self, level: u8) -> &Geometry<'_> {
299 match level {
300 0 => &self.geometry,
301 1 => self.lod_medium.as_ref().unwrap_or(&self.geometry),
302 _ => self
303 .lod_low
304 .as_ref()
305 .unwrap_or(self.lod_medium.as_ref().unwrap_or(&self.geometry)),
306 }
307 }
308
309 #[cfg(feature = "lod-crossfade")]
311 #[inline]
312 pub(crate) fn lod_level_of(&self, geom: &Geometry<'_>) -> u8 {
313 if core::ptr::eq(geom as *const _, &self.geometry as *const _) {
314 0
315 } else if self
316 .lod_medium
317 .as_ref()
318 .is_some_and(|m| core::ptr::eq(geom as *const _, m as *const _))
319 {
320 1
321 } else {
322 2
323 }
324 }
325
326 #[cfg(feature = "lod-crossfade")]
328 #[inline]
329 pub fn select_lod_pick(&self, distance: f32) -> LodPick<'_> {
330 let high = &self.geometry;
331 let medium = self.lod_medium.as_ref().unwrap_or(high);
332 let low = self
333 .lod_low
334 .as_ref()
335 .unwrap_or(self.lod_medium.as_ref().unwrap_or(high));
336
337 let margin = self.lod_levels.fade_margin;
338 if margin <= 0.0 {
339 return if distance < self.lod_levels.high_distance {
340 LodPick::Single(high)
341 } else if distance < self.lod_levels.medium_distance {
342 LodPick::Single(medium)
343 } else {
344 LodPick::Single(low)
345 };
346 }
347
348 let h = self.lod_levels.high_distance;
349 let m = self.lod_levels.medium_distance;
350
351 if distance < h - margin {
352 LodPick::Single(high)
353 } else if distance < h + margin {
354 let t = ((distance - (h - margin)) / (2.0 * margin)).clamp(0.0, 1.0);
355 if core::ptr::eq(high as *const _, medium as *const _) {
356 LodPick::Single(high)
357 } else {
358 LodPick::Crossfade {
359 near: high,
360 far: medium,
361 t,
362 }
363 }
364 } else if distance < m - margin {
365 LodPick::Single(medium)
366 } else if distance < m + margin {
367 let t = ((distance - (m - margin)) / (2.0 * margin)).clamp(0.0, 1.0);
368 if core::ptr::eq(medium as *const _, low as *const _) {
369 LodPick::Single(medium)
370 } else {
371 LodPick::Crossfade {
372 near: medium,
373 far: low,
374 t,
375 }
376 }
377 } else {
378 LodPick::Single(low)
379 }
380 }
381
382 pub fn set_color(&mut self, color: Rgb565) {
383 self.color = color;
384 }
385
386 pub fn set_render_mode(&mut self, mode: RenderMode) {
387 self.render_mode = mode;
388 }
389
390 pub fn set_priority(&mut self, priority: u8) {
391 self.priority = priority;
392 }
393
394 #[cfg(feature = "render-layers")]
395 pub fn set_layers(&mut self, layers: RenderLayers) {
396 self.layers = layers;
397 }
398
399 #[cfg(feature = "aabb-cull")]
401 pub fn cache_aabb(&mut self) {
402 self.aabb = Aabb::enclosing(self.geometry.vertices.iter());
403 }
404
405 #[cfg(feature = "aabb-cull")]
407 pub fn set_aabb(&mut self, aabb: Aabb) {
408 self.aabb = Some(aabb);
409 }
410
411 #[cfg(feature = "aabb-cull")]
413 #[inline]
414 pub fn model_aabb(&self) -> Aabb {
415 self.aabb
416 .unwrap_or_else(|| Aabb::enclosing(self.geometry.vertices.iter()).unwrap_or(Aabb::ZERO))
417 }
418
419 pub fn set_position(&mut self, x: f32, y: f32, z: f32) {
420 self.similarity.isometry.translation.x = x;
421 self.similarity.isometry.translation.y = y;
422 self.similarity.isometry.translation.z = z;
423 self.update_model_matrix();
424 }
425
426 pub fn get_position(&self) -> Point3<f32> {
427 self.similarity.isometry.translation.vector.into()
428 }
429
430 pub fn set_attitude(&mut self, roll: f32, pitch: f32, yaw: f32) {
431 self.similarity.isometry.rotation = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
432 self.update_model_matrix();
433 }
434
435 pub fn set_rotation(&mut self, rotation: UnitQuaternion<f32>) {
437 self.similarity.isometry.rotation = rotation;
438 self.update_model_matrix();
439 }
440
441 pub fn set_target(&mut self, target: Point3<f32>) {
442 let view = Similarity3::look_at_rh(
443 &self.similarity.isometry.translation.vector.into(),
444 &target,
445 &Vector3::y(),
446 1.0,
447 );
448
449 self.similarity = view;
450 self.model_matrix = self.similarity.to_homogeneous();
451 }
452
453 pub fn set_scale(&mut self, s: f32) {
454 if s == 0.0 {
455 return;
456 }
457 self.similarity.set_scaling(s);
458 self.update_model_matrix();
459 }
460
461 fn update_model_matrix(&mut self) {
462 self.model_matrix = self.similarity.to_homogeneous();
463 }
464
465 #[inline]
471 pub fn compute_bounding_radius_sq(&self) -> f32 {
472 let scale = self.similarity.scaling();
473 let scale_sq = scale * scale;
474 #[cfg(feature = "aabb-cull")]
475 if let Some(aabb) = self.aabb {
476 let center_sq = aabb.center.norm_squared();
477 let r = aabb.radius();
478 let rad = r + center_sq.sqrt();
479 return rad * rad * scale_sq;
480 }
481 let mut max_dist_sq = 0.0f32;
482 for vertex in self.geometry.vertices {
483 let dist_sq = vertex[0] * vertex[0] + vertex[1] * vertex[1] + vertex[2] * vertex[2];
484 if dist_sq > max_dist_sq {
485 max_dist_sq = dist_sq;
486 }
487 }
488 max_dist_sq * scale_sq
489 }
490}
491
492pub fn compute_vertex_normals<const V: usize>(
502 vertices: &[[f32; 3]],
503 faces: &[[usize; 3]],
504 face_normals: &[[f32; 3]],
505) -> Vec<[f32; 3], V> {
506 let mut normals = Vec::<[f32; 3], V>::new();
507 for _ in 0..vertices.len() {
508 if normals.push([0.0, 0.0, 0.0]).is_err() {
509 break;
510 }
511 }
512
513 for (face, fn_arr) in faces.iter().zip(face_normals.iter()) {
514 for &vi in face {
515 if vi < normals.len() {
516 normals[vi][0] += fn_arr[0];
517 normals[vi][1] += fn_arr[1];
518 normals[vi][2] += fn_arr[2];
519 }
520 }
521 }
522
523 for n in normals.iter_mut() {
524 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
525 if len > 1e-10 {
526 n[0] /= len;
527 n[1] /= len;
528 n[2] /= len;
529 }
530 }
531
532 normals
533}
534
535#[cfg(test)]
536mod tests {
537 extern crate std;
538 use super::*;
539
540 #[test]
541 fn test_geometry_validation_valid() {
542 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
543 let faces = [[0, 1, 2]];
544
545 let geometry = Geometry {
546 vertices: &vertices,
547 faces: &faces,
548 colors: &[],
549 lines: &[],
550 normals: &[],
551 vertex_normals: &[],
552 uvs: &[],
553 texture_id: None,
554 };
555
556 assert!(geometry.check_validity());
557 }
558
559 #[test]
560 #[should_panic]
561 fn test_geometry_validation_invalid_face_index() {
562 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
563 let faces = [[0, 1, 5]]; let geometry = Geometry {
566 vertices: &vertices,
567 faces: &faces,
568 colors: &[],
569 lines: &[],
570 normals: &[],
571 vertex_normals: &[],
572 uvs: &[],
573 texture_id: None,
574 };
575
576 K3dMesh::new(geometry);
578 }
579
580 #[test]
581 #[should_panic]
582 fn test_geometry_validation_invalid_line_index() {
583 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
584 let lines = [[0, 10]]; let geometry = Geometry {
587 vertices: &vertices,
588 faces: &[],
589 colors: &[],
590 lines: &lines,
591 normals: &[],
592 vertex_normals: &[],
593 uvs: &[],
594 texture_id: None,
595 };
596
597 K3dMesh::new(geometry);
598 }
599
600 #[test]
601 #[should_panic]
602 fn test_geometry_validation_color_length_mismatch() {
603 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
604 let colors = [Rgb565::CSS_RED]; let geometry = Geometry {
607 vertices: &vertices,
608 faces: &[],
609 colors: &colors,
610 lines: &[],
611 normals: &[],
612 vertex_normals: &[],
613 uvs: &[],
614 texture_id: None,
615 };
616
617 K3dMesh::new(geometry);
618 }
619
620 #[test]
621 fn test_lines_from_faces_basic() {
622 let faces = [[0, 1, 2]];
623 let lines = Geometry::lines_from_faces::<16>(&faces);
624
625 assert_eq!(lines.len(), 3);
627
628 let expected_edges = [(0, 1), (0, 2), (1, 2)];
630 for edge in expected_edges.iter() {
631 assert!(lines.contains(edge));
632 }
633 }
634
635 #[test]
636 fn test_lines_from_faces_shared_edges() {
637 let faces = [[0, 1, 2], [0, 2, 3]];
638 let lines = Geometry::lines_from_faces::<16>(&faces);
639
640 assert_eq!(lines.len(), 5);
642 }
643
644 #[test]
645 fn test_lines_from_faces_capacity_limit() {
646 let faces = [[0, 1, 2], [3, 4, 5]];
647 let lines = Geometry::lines_from_faces::<16>(&faces);
649
650 assert_eq!(lines.len(), 6);
652 }
653
654 #[test]
655 fn test_mesh_creation() {
656 let vertices = [[0.0, 0.0, 0.0]];
657 let geometry = Geometry {
658 vertices: &vertices,
659 faces: &[],
660 colors: &[],
661 lines: &[],
662 normals: &[],
663 vertex_normals: &[],
664 uvs: &[],
665 texture_id: None,
666 };
667
668 let mesh = K3dMesh::new(geometry);
669 assert_eq!(mesh.color, Rgb565::CSS_WHITE);
670 assert_eq!(mesh.render_mode, RenderMode::Points);
671 assert_eq!(mesh.get_position(), Point3::new(0.0, 0.0, 0.0));
672 }
673
674 #[test]
675 fn test_mesh_set_color() {
676 let vertices = [[0.0, 0.0, 0.0]];
677 let geometry = Geometry {
678 vertices: &vertices,
679 faces: &[],
680 colors: &[],
681 lines: &[],
682 normals: &[],
683 vertex_normals: &[],
684 uvs: &[],
685 texture_id: None,
686 };
687
688 let mut mesh = K3dMesh::new(geometry);
689 mesh.set_color(Rgb565::CSS_RED);
690 assert_eq!(mesh.color, Rgb565::CSS_RED);
691 }
692
693 #[test]
694 fn test_mesh_set_position() {
695 let vertices = [[0.0, 0.0, 0.0]];
696 let geometry = Geometry {
697 vertices: &vertices,
698 faces: &[],
699 colors: &[],
700 lines: &[],
701 normals: &[],
702 vertex_normals: &[],
703 uvs: &[],
704 texture_id: None,
705 };
706
707 let mut mesh = K3dMesh::new(geometry);
708 mesh.set_position(5.0, 10.0, 15.0);
709 assert_eq!(mesh.get_position(), Point3::new(5.0, 10.0, 15.0));
710 }
711
712 #[test]
713 fn test_mesh_set_scale() {
714 let vertices = [[0.0, 0.0, 0.0]];
715 let geometry = Geometry {
716 vertices: &vertices,
717 faces: &[],
718 colors: &[],
719 lines: &[],
720 normals: &[],
721 vertex_normals: &[],
722 uvs: &[],
723 texture_id: None,
724 };
725
726 let mut mesh = K3dMesh::new(geometry);
727 mesh.set_scale(2.0);
728 assert!((mesh.similarity.scaling() - 2.0).abs() < 0.001);
729 }
730
731 #[test]
732 fn test_mesh_set_scale_zero_ignored() {
733 let vertices = [[0.0, 0.0, 0.0]];
734 let geometry = Geometry {
735 vertices: &vertices,
736 faces: &[],
737 colors: &[],
738 lines: &[],
739 normals: &[],
740 vertex_normals: &[],
741 uvs: &[],
742 texture_id: None,
743 };
744
745 let mut mesh = K3dMesh::new(geometry);
746 let original_scale = mesh.similarity.scaling();
747 mesh.set_scale(0.0);
748 assert_eq!(mesh.similarity.scaling(), original_scale);
750 }
751
752 #[test]
753 fn test_mesh_set_attitude() {
754 let vertices = [[0.0, 0.0, 0.0]];
755 let geometry = Geometry {
756 vertices: &vertices,
757 faces: &[],
758 colors: &[],
759 lines: &[],
760 normals: &[],
761 vertex_normals: &[],
762 uvs: &[],
763 texture_id: None,
764 };
765
766 let mut mesh = K3dMesh::new(geometry);
767 mesh.set_attitude(0.1, 0.2, 0.3);
768 assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
770 }
771
772 #[test]
773 fn test_mesh_set_target() {
774 let vertices = [[0.0, 0.0, 0.0]];
775 let geometry = Geometry {
776 vertices: &vertices,
777 faces: &[],
778 colors: &[],
779 lines: &[],
780 normals: &[],
781 vertex_normals: &[],
782 uvs: &[],
783 texture_id: None,
784 };
785
786 let mut mesh = K3dMesh::new(geometry);
787 mesh.set_position(5.0, 5.0, 5.0);
788 mesh.set_target(Point3::new(0.0, 0.0, 0.0));
789 assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
792 }
793
794 #[test]
795 fn test_mesh_render_mode_changes() {
796 let vertices = [[0.0, 0.0, 0.0]];
797 let geometry = Geometry {
798 vertices: &vertices,
799 faces: &[],
800 colors: &[],
801 lines: &[],
802 normals: &[],
803 vertex_normals: &[],
804 uvs: &[],
805 texture_id: None,
806 };
807
808 let mut mesh = K3dMesh::new(geometry);
809
810 mesh.set_render_mode(RenderMode::Lines);
811 assert_eq!(mesh.render_mode, RenderMode::Lines);
812
813 mesh.set_render_mode(RenderMode::Solid);
814 assert_eq!(mesh.render_mode, RenderMode::Solid);
815
816 mesh.set_render_mode(RenderMode::SolidLightDir(Vector3::new(0.0, 1.0, 0.0)));
817 assert!(matches!(mesh.render_mode, RenderMode::SolidLightDir(_)));
818 }
819
820 #[test]
821 fn test_compute_vertex_normals_single_triangle() {
822 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
823 let faces = [[0, 1, 2]];
824 let face_normals = [[0.0, 0.0, 1.0]];
825
826 let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
827 assert_eq!(vn.len(), 3);
828 for n in vn.iter() {
829 assert!((n[0] - 0.0).abs() < 1e-5);
830 assert!((n[1] - 0.0).abs() < 1e-5);
831 assert!((n[2] - 1.0).abs() < 1e-5);
832 }
833 }
834
835 #[test]
836 fn test_compute_vertex_normals_shared_edge() {
837 let vertices = [
839 [0.0, 0.0, 0.0],
840 [1.0, 0.0, 0.0],
841 [0.5, 0.0, 1.0],
842 [0.5, 1.0, 0.0],
843 ];
844 let faces = [[0, 1, 2], [0, 1, 3]];
845 let face_normals = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
846
847 let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
848 assert_eq!(vn.len(), 4);
849
850 let _expected_len = (0.5f32 * 0.5 + 0.5 * 0.5).sqrt(); for i in 0..2 {
853 let n = &vn[i];
854 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
855 assert!((len - 1.0).abs() < 1e-5, "Normal should be unit length");
856 assert!(
857 (n[1] - n[2]).abs() < 1e-5,
858 "Y and Z components should be equal for shared verts"
859 );
860 }
861
862 assert!((vn[2][2] - 1.0).abs() < 1e-5);
864 assert!((vn[3][1] - 1.0).abs() < 1e-5);
866 }
867
868 #[test]
869 fn test_geometry_validation_vertex_normals_length_mismatch() {
870 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
871 let vn = [[0.0, 0.0, 1.0]]; let geometry = Geometry {
874 vertices: &vertices,
875 faces: &[],
876 colors: &[],
877 lines: &[],
878 normals: &[],
879 vertex_normals: &vn,
880 uvs: &[],
881 texture_id: None,
882 };
883
884 assert!(!geometry.check_validity());
885 }
886
887 #[cfg(feature = "lod-crossfade")]
888 #[test]
889 fn test_lod_crossfade_pick() {
890 let high = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
891 let med = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
892 let faces = [[0usize, 1, 2]];
893 let mut mesh = K3dMesh::new(Geometry {
894 vertices: &high,
895 faces: &faces,
896 colors: &[],
897 lines: &[],
898 normals: &[],
899 vertex_normals: &[],
900 uvs: &[],
901 texture_id: None,
902 });
903 mesh.set_lod(
904 Some(Geometry {
905 vertices: &med,
906 faces: &faces,
907 colors: &[],
908 lines: &[],
909 normals: &[],
910 vertex_normals: &[],
911 uvs: &[],
912 texture_id: None,
913 }),
914 None,
915 LODLevels {
916 high_distance: 10.0,
917 medium_distance: 20.0,
918 fade_margin: 2.0,
919 },
920 );
921 assert!(matches!(mesh.select_lod_pick(5.0), LodPick::Single(_)));
922 assert!(matches!(
923 mesh.select_lod_pick(10.0),
924 LodPick::Crossfade { .. }
925 ));
926 }
927
928 #[cfg(feature = "aabb-cull")]
929 #[test]
930 fn test_mesh_aabb_cached_on_new() {
931 let vertices = [[-2.0, -1.0, 0.0], [2.0, 1.0, 0.0]];
932 let mesh = K3dMesh::new(Geometry {
933 vertices: &vertices,
934 faces: &[],
935 colors: &[],
936 lines: &[],
937 normals: &[],
938 vertex_normals: &[],
939 uvs: &[],
940 texture_id: None,
941 });
942 let aabb = mesh.aabb.expect("cached");
943 assert!((aabb.half_extents.x - 2.0).abs() < 1e-5);
944 assert!((aabb.half_extents.y - 1.0).abs() < 1e-5);
945 }
946}