1use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
2use heapless::Vec;
3use heapless::index_set::FnvIndexSet;
4use log::error;
5use nalgebra::{Point3, Similarity3, UnitQuaternion, Vector3};
6
7#[cfg(not(feature = "std"))]
8use micromath::F32Ext;
9
10#[derive(Debug, PartialEq, Clone)]
11pub enum RenderMode {
12 Points,
13 Lines,
14 Solid,
15 SolidLightDir(Vector3<f32>),
16 BlinnPhong {
17 light_dir: Vector3<f32>,
18 specular_intensity: f32,
19 shininess: f32,
20 },
21 GouraudLightDir(Vector3<f32>),
22 SectorBright(u8),
25 Textured,
32}
33#[derive(Debug, Default, Copy, Clone)]
34pub struct Geometry<'a> {
35 pub vertices: &'a [[f32; 3]],
36 pub faces: &'a [[usize; 3]],
37 pub colors: &'a [Rgb565],
38 pub lines: &'a [[usize; 2]],
39 pub normals: &'a [[f32; 3]],
40 pub vertex_normals: &'a [[f32; 3]],
43 pub uvs: &'a [[f32; 2]],
45 pub texture_id: Option<u32>,
47}
48
49impl Geometry<'_> {
50 fn check_validity(&self) -> bool {
51 if self.vertices.is_empty() {
52 error!("Vertices are empty");
53 return false;
54 }
55
56 for face in self.faces {
57 if face[0] >= self.vertices.len()
58 || face[1] >= self.vertices.len()
59 || face[2] >= self.vertices.len()
60 {
61 error!("Face vertices are out of bounds");
62 return false;
63 }
64 }
65
66 for line in self.lines {
67 if line[0] >= self.vertices.len() || line[1] >= self.vertices.len() {
68 error!("Line vertices are out of bounds");
69 return false;
70 }
71 }
72
73 if !self.colors.is_empty() && self.colors.len() != self.vertices.len() {
74 error!("Colors are not the same length as vertices");
75 return false;
76 }
77
78 if !self.uvs.is_empty() && self.uvs.len() != self.vertices.len() {
79 error!("UVs are not the same length as vertices");
80 return false;
81 }
82
83 if !self.vertex_normals.is_empty() && self.vertex_normals.len() != self.vertices.len() {
84 error!("Vertex normals are not the same length as vertices");
85 return false;
86 }
87
88 true
89 }
90
91 pub fn lines_from_faces<const N: usize>(faces: &[[usize; 3]]) -> Vec<(usize, usize), N> {
101 let mut set: FnvIndexSet<(usize, usize), N> = FnvIndexSet::new();
102 for face in faces {
103 for &(i1, i2) in &[(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
104 let edge = if i1 < i2 { (i1, i2) } else { (i2, i1) };
105 if set.insert(edge).is_err() {
106 error!(
107 "lines_from_faces: heapless Vec capacity exceeded (max {}). Some edges will not be rendered.",
108 N
109 );
110 break;
111 }
112 }
113 }
114 set.iter().copied().collect()
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
125pub struct LODLevels {
126 pub high_distance: f32,
128 pub medium_distance: f32,
130}
131
132impl Default for LODLevels {
133 fn default() -> Self {
134 Self {
135 high_distance: 50.0,
136 medium_distance: 100.0,
137 }
138 }
139}
140
141pub struct K3dMesh<'a> {
143 pub similarity: Similarity3<f32>,
144 pub model_matrix: nalgebra::Matrix4<f32>,
145
146 pub color: Rgb565,
147 pub render_mode: RenderMode,
148 pub geometry: Geometry<'a>,
149
150 pub lod_medium: Option<Geometry<'a>>,
153 pub lod_low: Option<Geometry<'a>>,
154 pub lod_levels: LODLevels,
155 pub priority: u8,
156}
157
158impl<'a> K3dMesh<'a> {
159 pub fn new(geometry: Geometry) -> K3dMesh {
160 assert!(geometry.check_validity());
161 let sim = Similarity3::new(Vector3::new(0.0, 0.0, 0.0), nalgebra::zero(), 1.0);
162 K3dMesh {
163 model_matrix: sim.to_homogeneous(),
164 similarity: sim,
165 color: Rgb565::CSS_WHITE,
166 render_mode: RenderMode::Points,
167 geometry,
168 lod_medium: None,
169 lod_low: None,
170 lod_levels: LODLevels::default(),
171 priority: 128,
172 }
173 }
174
175 pub fn set_lod<'b>(
182 &mut self,
183 medium: Option<Geometry<'b>>,
184 low: Option<Geometry<'b>>,
185 levels: LODLevels,
186 ) where
187 'b: 'a,
188 {
189 if let Some(ref geom) = medium {
190 assert!(geom.check_validity());
191 }
192 if let Some(ref geom) = low {
193 assert!(geom.check_validity());
194 }
195 self.lod_medium = medium;
196 self.lod_low = low;
197 self.lod_levels = levels;
198 }
199
200 #[inline]
204 pub fn select_lod(&self, distance: f32) -> &Geometry<'_> {
205 if distance < self.lod_levels.high_distance {
206 &self.geometry
208 } else if distance < self.lod_levels.medium_distance {
209 self.lod_medium.as_ref().unwrap_or(&self.geometry)
211 } else {
212 self.lod_low
214 .as_ref()
215 .unwrap_or(self.lod_medium.as_ref().unwrap_or(&self.geometry))
216 }
217 }
218
219 pub fn set_color(&mut self, color: Rgb565) {
220 self.color = color;
221 }
222
223 pub fn set_render_mode(&mut self, mode: RenderMode) {
224 self.render_mode = mode;
225 }
226
227 pub fn set_priority(&mut self, priority: u8) {
228 self.priority = priority;
229 }
230
231 pub fn set_position(&mut self, x: f32, y: f32, z: f32) {
232 self.similarity.isometry.translation.x = x;
233 self.similarity.isometry.translation.y = y;
234 self.similarity.isometry.translation.z = z;
235 self.update_model_matrix();
236 }
237
238 pub fn get_position(&self) -> Point3<f32> {
239 self.similarity.isometry.translation.vector.into()
240 }
241
242 pub fn set_attitude(&mut self, roll: f32, pitch: f32, yaw: f32) {
243 self.similarity.isometry.rotation = UnitQuaternion::from_euler_angles(roll, pitch, yaw);
244 self.update_model_matrix();
245 }
246
247 pub fn set_rotation(&mut self, rotation: UnitQuaternion<f32>) {
249 self.similarity.isometry.rotation = rotation;
250 self.update_model_matrix();
251 }
252
253 pub fn set_target(&mut self, target: Point3<f32>) {
254 let view = Similarity3::look_at_rh(
255 &self.similarity.isometry.translation.vector.into(),
256 &target,
257 &Vector3::y(),
258 1.0,
259 );
260
261 self.similarity = view;
262 self.model_matrix = self.similarity.to_homogeneous();
263 }
264
265 pub fn set_scale(&mut self, s: f32) {
266 if s == 0.0 {
267 return;
268 }
269 self.similarity.set_scaling(s);
270 self.update_model_matrix();
271 }
272
273 fn update_model_matrix(&mut self) {
274 self.model_matrix = self.similarity.to_homogeneous();
275 }
276
277 #[inline]
281 pub fn compute_bounding_radius_sq(&self) -> f32 {
282 let mut max_dist_sq = 0.0f32;
283 for vertex in self.geometry.vertices {
284 let dist_sq = vertex[0] * vertex[0] + vertex[1] * vertex[1] + vertex[2] * vertex[2];
285 if dist_sq > max_dist_sq {
286 max_dist_sq = dist_sq;
287 }
288 }
289 let scale = self.similarity.scaling();
290 max_dist_sq * scale * scale
291 }
292}
293
294pub fn compute_vertex_normals<const V: usize>(
304 vertices: &[[f32; 3]],
305 faces: &[[usize; 3]],
306 face_normals: &[[f32; 3]],
307) -> Vec<[f32; 3], V> {
308 let mut normals = Vec::<[f32; 3], V>::new();
309 for _ in 0..vertices.len() {
310 if normals.push([0.0, 0.0, 0.0]).is_err() {
311 break;
312 }
313 }
314
315 for (face, fn_arr) in faces.iter().zip(face_normals.iter()) {
316 for &vi in face {
317 if vi < normals.len() {
318 normals[vi][0] += fn_arr[0];
319 normals[vi][1] += fn_arr[1];
320 normals[vi][2] += fn_arr[2];
321 }
322 }
323 }
324
325 for n in normals.iter_mut() {
326 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
327 if len > 1e-10 {
328 n[0] /= len;
329 n[1] /= len;
330 n[2] /= len;
331 }
332 }
333
334 normals
335}
336
337#[cfg(test)]
338mod tests {
339 extern crate std;
340 use super::*;
341
342 #[test]
343 fn test_geometry_validation_valid() {
344 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
345 let faces = [[0, 1, 2]];
346
347 let geometry = Geometry {
348 vertices: &vertices,
349 faces: &faces,
350 colors: &[],
351 lines: &[],
352 normals: &[],
353 vertex_normals: &[],
354 uvs: &[],
355 texture_id: None,
356 };
357
358 assert!(geometry.check_validity());
359 }
360
361 #[test]
362 #[should_panic]
363 fn test_geometry_validation_invalid_face_index() {
364 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
365 let faces = [[0, 1, 5]]; let geometry = Geometry {
368 vertices: &vertices,
369 faces: &faces,
370 colors: &[],
371 lines: &[],
372 normals: &[],
373 vertex_normals: &[],
374 uvs: &[],
375 texture_id: None,
376 };
377
378 K3dMesh::new(geometry);
380 }
381
382 #[test]
383 #[should_panic]
384 fn test_geometry_validation_invalid_line_index() {
385 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
386 let lines = [[0, 10]]; let geometry = Geometry {
389 vertices: &vertices,
390 faces: &[],
391 colors: &[],
392 lines: &lines,
393 normals: &[],
394 vertex_normals: &[],
395 uvs: &[],
396 texture_id: None,
397 };
398
399 K3dMesh::new(geometry);
400 }
401
402 #[test]
403 #[should_panic]
404 fn test_geometry_validation_color_length_mismatch() {
405 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
406 let colors = [Rgb565::CSS_RED]; let geometry = Geometry {
409 vertices: &vertices,
410 faces: &[],
411 colors: &colors,
412 lines: &[],
413 normals: &[],
414 vertex_normals: &[],
415 uvs: &[],
416 texture_id: None,
417 };
418
419 K3dMesh::new(geometry);
420 }
421
422 #[test]
423 fn test_lines_from_faces_basic() {
424 let faces = [[0, 1, 2]];
425 let lines = Geometry::lines_from_faces::<16>(&faces);
426
427 assert_eq!(lines.len(), 3);
429
430 let expected_edges = [(0, 1), (0, 2), (1, 2)];
432 for edge in expected_edges.iter() {
433 assert!(lines.contains(edge));
434 }
435 }
436
437 #[test]
438 fn test_lines_from_faces_shared_edges() {
439 let faces = [[0, 1, 2], [0, 2, 3]];
440 let lines = Geometry::lines_from_faces::<16>(&faces);
441
442 assert_eq!(lines.len(), 5);
444 }
445
446 #[test]
447 fn test_lines_from_faces_capacity_limit() {
448 let faces = [[0, 1, 2], [3, 4, 5]];
449 let lines = Geometry::lines_from_faces::<16>(&faces);
451
452 assert_eq!(lines.len(), 6);
454 }
455
456 #[test]
457 fn test_mesh_creation() {
458 let vertices = [[0.0, 0.0, 0.0]];
459 let geometry = Geometry {
460 vertices: &vertices,
461 faces: &[],
462 colors: &[],
463 lines: &[],
464 normals: &[],
465 vertex_normals: &[],
466 uvs: &[],
467 texture_id: None,
468 };
469
470 let mesh = K3dMesh::new(geometry);
471 assert_eq!(mesh.color, Rgb565::CSS_WHITE);
472 assert_eq!(mesh.render_mode, RenderMode::Points);
473 assert_eq!(mesh.get_position(), Point3::new(0.0, 0.0, 0.0));
474 }
475
476 #[test]
477 fn test_mesh_set_color() {
478 let vertices = [[0.0, 0.0, 0.0]];
479 let geometry = Geometry {
480 vertices: &vertices,
481 faces: &[],
482 colors: &[],
483 lines: &[],
484 normals: &[],
485 vertex_normals: &[],
486 uvs: &[],
487 texture_id: None,
488 };
489
490 let mut mesh = K3dMesh::new(geometry);
491 mesh.set_color(Rgb565::CSS_RED);
492 assert_eq!(mesh.color, Rgb565::CSS_RED);
493 }
494
495 #[test]
496 fn test_mesh_set_position() {
497 let vertices = [[0.0, 0.0, 0.0]];
498 let geometry = Geometry {
499 vertices: &vertices,
500 faces: &[],
501 colors: &[],
502 lines: &[],
503 normals: &[],
504 vertex_normals: &[],
505 uvs: &[],
506 texture_id: None,
507 };
508
509 let mut mesh = K3dMesh::new(geometry);
510 mesh.set_position(5.0, 10.0, 15.0);
511 assert_eq!(mesh.get_position(), Point3::new(5.0, 10.0, 15.0));
512 }
513
514 #[test]
515 fn test_mesh_set_scale() {
516 let vertices = [[0.0, 0.0, 0.0]];
517 let geometry = Geometry {
518 vertices: &vertices,
519 faces: &[],
520 colors: &[],
521 lines: &[],
522 normals: &[],
523 vertex_normals: &[],
524 uvs: &[],
525 texture_id: None,
526 };
527
528 let mut mesh = K3dMesh::new(geometry);
529 mesh.set_scale(2.0);
530 assert!((mesh.similarity.scaling() - 2.0).abs() < 0.001);
531 }
532
533 #[test]
534 fn test_mesh_set_scale_zero_ignored() {
535 let vertices = [[0.0, 0.0, 0.0]];
536 let geometry = Geometry {
537 vertices: &vertices,
538 faces: &[],
539 colors: &[],
540 lines: &[],
541 normals: &[],
542 vertex_normals: &[],
543 uvs: &[],
544 texture_id: None,
545 };
546
547 let mut mesh = K3dMesh::new(geometry);
548 let original_scale = mesh.similarity.scaling();
549 mesh.set_scale(0.0);
550 assert_eq!(mesh.similarity.scaling(), original_scale);
552 }
553
554 #[test]
555 fn test_mesh_set_attitude() {
556 let vertices = [[0.0, 0.0, 0.0]];
557 let geometry = Geometry {
558 vertices: &vertices,
559 faces: &[],
560 colors: &[],
561 lines: &[],
562 normals: &[],
563 vertex_normals: &[],
564 uvs: &[],
565 texture_id: None,
566 };
567
568 let mut mesh = K3dMesh::new(geometry);
569 mesh.set_attitude(0.1, 0.2, 0.3);
570 assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
572 }
573
574 #[test]
575 fn test_mesh_set_target() {
576 let vertices = [[0.0, 0.0, 0.0]];
577 let geometry = Geometry {
578 vertices: &vertices,
579 faces: &[],
580 colors: &[],
581 lines: &[],
582 normals: &[],
583 vertex_normals: &[],
584 uvs: &[],
585 texture_id: None,
586 };
587
588 let mut mesh = K3dMesh::new(geometry);
589 mesh.set_position(5.0, 5.0, 5.0);
590 mesh.set_target(Point3::new(0.0, 0.0, 0.0));
591 assert_ne!(mesh.model_matrix, nalgebra::Matrix4::identity());
594 }
595
596 #[test]
597 fn test_mesh_render_mode_changes() {
598 let vertices = [[0.0, 0.0, 0.0]];
599 let geometry = Geometry {
600 vertices: &vertices,
601 faces: &[],
602 colors: &[],
603 lines: &[],
604 normals: &[],
605 vertex_normals: &[],
606 uvs: &[],
607 texture_id: None,
608 };
609
610 let mut mesh = K3dMesh::new(geometry);
611
612 mesh.set_render_mode(RenderMode::Lines);
613 assert_eq!(mesh.render_mode, RenderMode::Lines);
614
615 mesh.set_render_mode(RenderMode::Solid);
616 assert_eq!(mesh.render_mode, RenderMode::Solid);
617
618 mesh.set_render_mode(RenderMode::SolidLightDir(Vector3::new(0.0, 1.0, 0.0)));
619 assert!(matches!(mesh.render_mode, RenderMode::SolidLightDir(_)));
620 }
621
622 #[test]
623 fn test_compute_vertex_normals_single_triangle() {
624 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
625 let faces = [[0, 1, 2]];
626 let face_normals = [[0.0, 0.0, 1.0]];
627
628 let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
629 assert_eq!(vn.len(), 3);
630 for n in vn.iter() {
631 assert!((n[0] - 0.0).abs() < 1e-5);
632 assert!((n[1] - 0.0).abs() < 1e-5);
633 assert!((n[2] - 1.0).abs() < 1e-5);
634 }
635 }
636
637 #[test]
638 fn test_compute_vertex_normals_shared_edge() {
639 let vertices = [
641 [0.0, 0.0, 0.0],
642 [1.0, 0.0, 0.0],
643 [0.5, 0.0, 1.0],
644 [0.5, 1.0, 0.0],
645 ];
646 let faces = [[0, 1, 2], [0, 1, 3]];
647 let face_normals = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
648
649 let vn = compute_vertex_normals::<8>(&vertices, &faces, &face_normals);
650 assert_eq!(vn.len(), 4);
651
652 let _expected_len = (0.5f32 * 0.5 + 0.5 * 0.5).sqrt(); for i in 0..2 {
655 let n = &vn[i];
656 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
657 assert!((len - 1.0).abs() < 1e-5, "Normal should be unit length");
658 assert!(
659 (n[1] - n[2]).abs() < 1e-5,
660 "Y and Z components should be equal for shared verts"
661 );
662 }
663
664 assert!((vn[2][2] - 1.0).abs() < 1e-5);
666 assert!((vn[3][1] - 1.0).abs() < 1e-5);
668 }
669
670 #[test]
671 fn test_geometry_validation_vertex_normals_length_mismatch() {
672 let vertices = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
673 let vn = [[0.0, 0.0, 1.0]]; let geometry = Geometry {
676 vertices: &vertices,
677 faces: &[],
678 colors: &[],
679 lines: &[],
680 normals: &[],
681 vertex_normals: &vn,
682 uvs: &[],
683 texture_id: None,
684 };
685
686 assert!(!geometry.check_validity());
687 }
688}