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