1use crate::container_10d::mesh_section::{
33 decode_mesh_section, encode_mesh_section, encoded_len, MeshSectionError,
34};
35use crate::gpu_context::OperationalMode;
36use crate::render::assets::Mesh;
37use crate::specialized_libs::computational_geometry::{
38 decimate_qem, DecimateError, DecimateOptions, DecimateReport, Point3,
39};
40
41#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum LodChainError {
48 DecimateFailed { level: usize, cause: DecimateError },
50 EncodeFailed {
52 level: usize,
53 cause: MeshSectionError,
54 },
55 DecodeFailed {
57 level: usize,
58 cause: MeshSectionError,
59 },
60 BufferTooSmall { needed: usize, have: usize },
62 NoLodLevels,
64 EmptySourceMesh,
66}
67
68impl core::fmt::Display for LodChainError {
69 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70 match self {
71 Self::DecimateFailed { level, cause } => {
72 write!(
73 f,
74 "lod_chain: decimation failed at level {level}: {cause:?}"
75 )
76 }
77 Self::EncodeFailed { level, cause } => {
78 write!(f, "lod_chain: encode failed at level {level}: {cause}")
79 }
80 Self::DecodeFailed { level, cause } => {
81 write!(f, "lod_chain: decode failed at level {level}: {cause}")
82 }
83 Self::BufferTooSmall { needed, have } => {
84 write!(f, "lod_chain: buffer too small, need {needed}, have {have}")
85 }
86 Self::NoLodLevels => write!(f, "lod_chain: no LOD levels requested"),
87 Self::EmptySourceMesh => write!(f, "lod_chain: source mesh is empty"),
88 }
89 }
90}
91
92impl std::error::Error for LodChainError {}
93
94pub const MAX_LOD_LEVELS: usize = 8;
100
101#[derive(Debug, Clone, Copy)]
104pub struct LodChainOptions {
105 pub level_count: usize,
107 pub fraction: f64,
111}
112
113impl Default for LodChainOptions {
114 fn default() -> Self {
115 Self {
116 level_count: 3,
117 fraction: 0.5,
118 }
119 }
120}
121
122impl LodChainOptions {
123 pub fn new(level_count: usize, fraction: f64) -> Self {
125 Self {
126 level_count: level_count.clamp(1, MAX_LOD_LEVELS),
127 fraction: fraction.clamp(0.01, 1.0),
128 }
129 }
130
131 pub fn default_3_tier() -> Self {
133 Self {
134 level_count: 3,
135 fraction: 0.5,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy)]
142pub struct LodLevelReport {
143 pub level: usize,
145 pub vertices: usize,
147 pub triangles: usize,
149 pub encoded_bytes: usize,
151 pub decimate_report: Option<DecimateReport>,
153}
154
155#[derive(Debug, Clone)]
157pub struct LodChainReport {
158 pub levels: Vec<LodLevelReport>,
159 pub total_bytes: usize,
161}
162
163pub fn build_lod_chain(
175 source: &Mesh,
176 options: LodChainOptions,
177 out_buffer: &mut [u8],
178) -> Result<LodChainReport, LodChainError> {
179 if source.positions.is_empty() || source.triangles.is_empty() {
180 return Err(LodChainError::EmptySourceMesh);
181 }
182 if options.level_count == 0 {
183 return Err(LodChainError::NoLodLevels);
184 }
185
186 let mut levels: Vec<LodLevelReport> = Vec::with_capacity(options.level_count);
187 let mut offset = 0usize;
188
189 let lod0_bytes = encoded_len(source.positions.len(), source.triangles.len());
191 if offset + lod0_bytes > out_buffer.len() {
192 return Err(LodChainError::BufferTooSmall {
193 needed: offset + lod0_bytes,
194 have: out_buffer.len(),
195 });
196 }
197 let written = encode_mesh_section(source, &mut out_buffer[offset..])
198 .map_err(|e| LodChainError::EncodeFailed { level: 0, cause: e })?;
199 levels.push(LodLevelReport {
200 level: 0,
201 vertices: source.positions.len(),
202 triangles: source.triangles.len(),
203 encoded_bytes: written,
204 decimate_report: None,
205 });
206 offset += written;
207
208 let mut current_verts_f64: Vec<Point3> = source
210 .positions
211 .iter()
212 .map(|p| Point3::new(p[0] as f64, p[1] as f64, p[2] as f64))
213 .collect();
214 let mut current_tris: Vec<[u32; 3]> = source.triangles.clone();
215
216 for level in 1..options.level_count {
217 let target_faces = (current_tris.len() as f64 * options.fraction) as usize;
218 if target_faces < 4 {
219 break;
221 }
222
223 let decim_opts = DecimateOptions::to_faces(target_faces);
224 let mut out_v = vec![Point3::default(); current_verts_f64.len()];
225 let mut out_t = vec![[0u32; 3]; current_tris.len()];
226
227 let report = decimate_qem(
228 ¤t_verts_f64,
229 ¤t_tris,
230 decim_opts,
231 &mut out_v,
232 &mut out_t,
233 )
234 .map_err(|e| LodChainError::DecimateFailed { level, cause: e })?;
235
236 let live_v = report.vertices;
238 let live_t = report.faces;
239 current_verts_f64 = out_v[..live_v].to_vec();
240 current_tris = out_t[..live_t].to_vec();
241
242 let f32_positions: Vec<[f32; 3]> = current_verts_f64
244 .iter()
245 .map(|p| [p.x as f32, p.y as f32, p.z as f32])
246 .collect();
247
248 let lod_mesh = Mesh {
250 positions: f32_positions.clone(),
251 triangles: current_tris.clone(),
252 min: [
253 f32_positions
254 .iter()
255 .map(|p| p[0])
256 .fold(f32::INFINITY, f32::min),
257 f32_positions
258 .iter()
259 .map(|p| p[1])
260 .fold(f32::INFINITY, f32::min),
261 f32_positions
262 .iter()
263 .map(|p| p[2])
264 .fold(f32::INFINITY, f32::min),
265 ],
266 max: [
267 f32_positions
268 .iter()
269 .map(|p| p[0])
270 .fold(f32::NEG_INFINITY, f32::max),
271 f32_positions
272 .iter()
273 .map(|p| p[1])
274 .fold(f32::NEG_INFINITY, f32::max),
275 f32_positions
276 .iter()
277 .map(|p| p[2])
278 .fold(f32::NEG_INFINITY, f32::max),
279 ],
280 };
281
282 let lod_bytes = encoded_len(live_v, live_t);
283 if offset + lod_bytes > out_buffer.len() {
284 return Err(LodChainError::BufferTooSmall {
285 needed: offset + lod_bytes,
286 have: out_buffer.len(),
287 });
288 }
289 let written = encode_mesh_section(&lod_mesh, &mut out_buffer[offset..])
290 .map_err(|e| LodChainError::EncodeFailed { level, cause: e })?;
291 levels.push(LodLevelReport {
292 level,
293 vertices: live_v,
294 triangles: live_t,
295 encoded_bytes: written,
296 decimate_report: Some(report),
297 });
298 offset += written;
299 }
300
301 Ok(LodChainReport {
302 levels,
303 total_bytes: offset,
304 })
305}
306
307pub fn required_lod_buffer_size(
311 source_vertices: usize,
312 source_triangles: usize,
313 options: LodChainOptions,
314) -> usize {
315 let mut total = 0usize;
316 let mut verts = source_vertices;
317 let mut tris = source_triangles;
318
319 for level in 0..options.level_count {
320 total += encoded_len(verts, tris);
321 if level > 0 {
322 let target = (tris as f64 * options.fraction) as usize;
323 if target < 4 {
324 break;
325 }
326 verts = (verts as f64 * options.fraction) as usize;
328 tris = target;
329 }
330 }
331 total
332}
333
334#[inline]
346pub fn select_lod(mode: OperationalMode, level_count: usize) -> usize {
347 if level_count == 0 {
348 return 0;
349 }
350 let preferred = match mode {
351 OperationalMode::Full => 0,
352 OperationalMode::Eco => 1,
353 OperationalMode::Reserve => 2,
354 };
355 preferred.min(level_count - 1)
356}
357
358pub fn parse_lod_level(
367 buffer: &[u8],
368 level_offsets: &[usize],
369 level: usize,
370) -> Result<Mesh, LodChainError> {
371 if level >= level_offsets.len() {
372 return Err(LodChainError::DecodeFailed {
373 level,
374 cause: MeshSectionError::PayloadTooShort { got: 0, need: 40 },
375 });
376 }
377 let offset = level_offsets[level];
378 let end = if level + 1 < level_offsets.len() {
379 level_offsets[level + 1]
380 } else {
381 buffer.len()
382 };
383 let section_bytes = &buffer[offset..end];
384 decode_mesh_section(section_bytes).map_err(|e| LodChainError::DecodeFailed { level, cause: e })
385}
386
387pub fn level_offsets(report: &LodChainReport) -> Vec<usize> {
389 let mut offsets = Vec::with_capacity(report.levels.len());
390 let mut acc = 0;
391 for lvl in &report.levels {
392 offsets.push(acc);
393 acc += lvl.encoded_bytes;
394 }
395 offsets
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
405pub enum LodViewDisposition {
406 Render3dWithLod { lod: usize },
408 Render2d,
410 Collapsed2D,
413 WithheldUnattested,
415 RefusedRightsBounded,
417}
418
419#[allow(clippy::too_many_arguments)]
424pub fn plan_view_with_lod(
425 view: &crate::render::authoring::QappView,
426 standpoint: &crate::render::authoring::RenderStandpoint,
427 mode: OperationalMode,
428 attestations: &[crate::NQuin],
429 gov_norms: &[crate::NQuin],
430 now_unix: u32,
431 lod_level_count: usize,
432) -> LodViewDisposition {
433 use crate::render::authoring::{has_attestation, Sensitivity, ViewKind};
434
435 if view.requires_attestation && !has_attestation(view, attestations) {
437 return LodViewDisposition::WithheldUnattested;
438 }
439
440 if matches!(view.sensitivity, Sensitivity::RightsBounded)
442 && !crate::render::authoring::rights_render_permitted(
443 standpoint,
444 view.manifold,
445 gov_norms,
446 now_unix,
447 )
448 {
449 return LodViewDisposition::RefusedRightsBounded;
450 }
451
452 match view.kind {
454 ViewKind::Pane2D => LodViewDisposition::Render2d,
455 ViewKind::Scene3D => {
456 if mode.supports_3d() {
457 LodViewDisposition::Render3dWithLod {
458 lod: select_lod(mode, lod_level_count),
459 }
460 } else if lod_level_count > 1 {
461 LodViewDisposition::Render3dWithLod {
463 lod: select_lod(mode, lod_level_count),
464 }
465 } else {
466 LodViewDisposition::Collapsed2D
468 }
469 }
470 }
471}
472
473pub fn lod_chain_hash(bytes: &[u8]) -> u64 {
479 let mut hash: u64 = 0xcbf29ce484222325;
480 for &b in bytes {
481 hash ^= b as u64;
482 hash = hash.wrapping_mul(0x100000001b3);
483 }
484 hash
485}
486
487#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::q_hash;
495 use crate::render::authoring::{
496 attestation_quin, plan_view, QappView, RenderStandpoint, Sensitivity, ViewDisposition,
497 ViewKind,
498 };
499
500 fn unit_cube_mesh() -> Mesh {
501 let positions = vec![
502 [0.0f32, 0.0, 0.0],
503 [1.0, 0.0, 0.0],
504 [1.0, 1.0, 0.0],
505 [0.0, 1.0, 0.0],
506 [0.0, 0.0, 1.0],
507 [1.0, 0.0, 1.0],
508 [1.0, 1.0, 1.0],
509 [0.0, 1.0, 1.0],
510 ];
511 let triangles = vec![
512 [0, 3, 2],
513 [0, 2, 1],
514 [4, 5, 6],
515 [4, 6, 7],
516 [0, 1, 5],
517 [0, 5, 4],
518 [3, 7, 6],
519 [3, 6, 2],
520 [0, 4, 7],
521 [0, 7, 3],
522 [1, 2, 6],
523 [1, 6, 5],
524 ];
525 Mesh {
526 positions,
527 triangles,
528 min: [0.0, 0.0, 0.0],
529 max: [1.0, 1.0, 1.0],
530 }
531 }
532
533 fn subdivided_cube_mesh(subdivs: usize) -> Mesh {
534 let s = subdivs as f32;
535 let mut positions = Vec::new();
536 for i in 0..=subdivs {
537 for j in 0..=subdivs {
538 for k in 0..=subdivs {
539 positions.push([i as f32 / s, j as f32 / s, k as f32 / s]);
540 }
541 }
542 }
543 let mut triangles = Vec::new();
545 let idx = |i: usize, j: usize, k: usize| -> u32 {
546 (i * (subdivs + 1) * (subdivs + 1) + j * (subdivs + 1) + k) as u32
547 };
548 for i in 0..subdivs {
550 for j in 0..subdivs {
551 triangles.push([idx(i, j, 0), idx(i, j + 1, 0), idx(i + 1, j + 1, 0)]);
552 triangles.push([idx(i, j, 0), idx(i + 1, j + 1, 0), idx(i + 1, j, 0)]);
553 }
554 }
555 for i in 0..subdivs {
557 for j in 0..subdivs {
558 triangles.push([
559 idx(i, j, subdivs),
560 idx(i + 1, j + 1, subdivs),
561 idx(i, j + 1, subdivs),
562 ]);
563 triangles.push([
564 idx(i, j, subdivs),
565 idx(i + 1, j, subdivs),
566 idx(i + 1, j + 1, subdivs),
567 ]);
568 }
569 }
570 for i in 0..subdivs {
572 for k in 0..subdivs {
573 triangles.push([idx(i, 0, k), idx(i + 1, 0, k + 1), idx(i, 0, k + 1)]);
574 triangles.push([idx(i, 0, k), idx(i + 1, 0, k), idx(i + 1, 0, k + 1)]);
575 }
576 }
577 for i in 0..subdivs {
579 for k in 0..subdivs {
580 triangles.push([
581 idx(i, subdivs, k),
582 idx(i, subdivs, k + 1),
583 idx(i + 1, subdivs, k + 1),
584 ]);
585 triangles.push([
586 idx(i, subdivs, k),
587 idx(i + 1, subdivs, k + 1),
588 idx(i + 1, subdivs, k),
589 ]);
590 }
591 }
592 for j in 0..subdivs {
594 for k in 0..subdivs {
595 triangles.push([idx(0, j, k), idx(0, j, k + 1), idx(0, j + 1, k + 1)]);
596 triangles.push([idx(0, j, k), idx(0, j + 1, k + 1), idx(0, j + 1, k)]);
597 }
598 }
599 for j in 0..subdivs {
601 for k in 0..subdivs {
602 triangles.push([
603 idx(subdivs, j, k),
604 idx(subdivs, j + 1, k + 1),
605 idx(subdivs, j, k + 1),
606 ]);
607 triangles.push([
608 idx(subdivs, j, k),
609 idx(subdivs, j + 1, k),
610 idx(subdivs, j + 1, k + 1),
611 ]);
612 }
613 }
614 let min = [0.0f32; 3];
615 let max = [1.0f32; 3];
616 Mesh {
617 positions,
618 triangles,
619 min,
620 max,
621 }
622 }
623
624 #[test]
625 fn build_lod_chain_3_levels() {
626 let mesh = subdivided_cube_mesh(4); let options = LodChainOptions::default_3_tier();
628 let buf_size =
629 required_lod_buffer_size(mesh.positions.len(), mesh.triangles.len(), options);
630 let mut buf = vec![0u8; buf_size];
631 let report = build_lod_chain(&mesh, options, &mut buf).unwrap();
632
633 assert!(
634 report.levels.len() >= 2,
635 "should produce at least 2 LOD levels"
636 );
637 assert_eq!(report.levels[0].level, 0);
638 assert_eq!(report.levels[0].vertices, mesh.positions.len());
639 assert_eq!(report.levels[0].triangles, mesh.triangles.len());
640 assert_eq!(
641 report.total_bytes,
642 report.levels.iter().map(|l| l.encoded_bytes).sum::<usize>()
643 );
644 }
645
646 #[test]
647 fn lod_chain_hash_stable() {
648 let mesh = subdivided_cube_mesh(3);
649 let options = LodChainOptions::default_3_tier();
650 let buf_size =
651 required_lod_buffer_size(mesh.positions.len(), mesh.triangles.len(), options);
652
653 let mut buf1 = vec![0u8; buf_size];
654 let mut buf2 = vec![0u8; buf_size];
655 let report1 = build_lod_chain(&mesh, options, &mut buf1).unwrap();
656 let report2 = build_lod_chain(&mesh, options, &mut buf2).unwrap();
657
658 assert_eq!(report1.total_bytes, report2.total_bytes);
659 let h1 = lod_chain_hash(&buf1[..report1.total_bytes]);
660 let h2 = lod_chain_hash(&buf2[..report2.total_bytes]);
661 assert_eq!(
662 h1, h2,
663 "LOD chain bytes must be hash-stable across two encodes"
664 );
665 }
666
667 #[test]
668 fn lod_chain_round_trip() {
669 let mesh = subdivided_cube_mesh(3);
670 let options = LodChainOptions::default_3_tier();
671 let buf_size =
672 required_lod_buffer_size(mesh.positions.len(), mesh.triangles.len(), options);
673 let mut buf = vec![0u8; buf_size];
674 let report = build_lod_chain(&mesh, options, &mut buf).unwrap();
675
676 let offsets = level_offsets(&report);
677
678 let lod0 = parse_lod_level(&buf, &offsets, 0).unwrap();
680 assert_eq!(lod0.positions.len(), mesh.positions.len());
681 assert_eq!(lod0.triangles.len(), mesh.triangles.len());
682
683 for (i, lvl) in report.levels.iter().enumerate() {
685 let decoded = parse_lod_level(&buf, &offsets, i).unwrap();
686 assert_eq!(
687 decoded.positions.len(),
688 lvl.vertices,
689 "LOD {i} vertex count mismatch"
690 );
691 assert_eq!(
692 decoded.triangles.len(),
693 lvl.triangles,
694 "LOD {i} triangle count mismatch"
695 );
696 }
697 }
698
699 #[test]
700 fn select_lod_by_mode() {
701 assert_eq!(select_lod(OperationalMode::Full, 3), 0);
702 assert_eq!(select_lod(OperationalMode::Eco, 3), 1);
703 assert_eq!(select_lod(OperationalMode::Reserve, 3), 2);
704 assert_eq!(select_lod(OperationalMode::Reserve, 2), 1);
706 assert_eq!(select_lod(OperationalMode::Reserve, 1), 0);
708 }
709
710 #[test]
711 fn lod_chain_decreasing_triangle_counts() {
712 let mesh = subdivided_cube_mesh(5);
713 let options = LodChainOptions::default_3_tier();
714 let buf_size =
715 required_lod_buffer_size(mesh.positions.len(), mesh.triangles.len(), options);
716 let mut buf = vec![0u8; buf_size];
717 let report = build_lod_chain(&mesh, options, &mut buf).unwrap();
718
719 for i in 1..report.levels.len() {
720 assert!(
721 report.levels[i].triangles <= report.levels[i - 1].triangles,
722 "LOD {i} should have ≤ triangles than LOD {}",
723 i - 1
724 );
725 }
726 }
727
728 #[test]
729 fn plan_view_with_lod_selects_coarser_on_eco() {
730 let m = q_hash("urn:qualia:manifold:demo");
731 let view = QappView::public(m, ViewKind::Scene3D);
732 let standpoint = RenderStandpoint {
733 id: q_hash("urn:qualia:standpoint:owner"),
734 shared_civic: false,
735 };
736
737 assert_eq!(
739 plan_view_with_lod(&view, &standpoint, OperationalMode::Full, &[], &[], 100, 3),
740 LodViewDisposition::Render3dWithLod { lod: 0 }
741 );
742 assert_eq!(
744 plan_view_with_lod(&view, &standpoint, OperationalMode::Eco, &[], &[], 100, 3),
745 LodViewDisposition::Render3dWithLod { lod: 1 }
746 );
747 assert_eq!(
749 plan_view_with_lod(
750 &view,
751 &standpoint,
752 OperationalMode::Reserve,
753 &[],
754 &[],
755 100,
756 3
757 ),
758 LodViewDisposition::Render3dWithLod { lod: 2 }
759 );
760 }
761
762 #[test]
763 fn plan_view_with_lod_collapses_when_no_coarser_lod() {
764 let m = q_hash("urn:qualia:manifold:demo");
765 let view = QappView::public(m, ViewKind::Scene3D);
766 let standpoint = RenderStandpoint {
767 id: q_hash("urn:qualia:standpoint:owner"),
768 shared_civic: false,
769 };
770
771 assert_eq!(
773 plan_view_with_lod(&view, &standpoint, OperationalMode::Eco, &[], &[], 100, 1),
774 LodViewDisposition::Collapsed2D
775 );
776 }
777
778 #[test]
779 fn plan_view_with_lod_2d_pane_ignores_lod() {
780 let m = q_hash("urn:qualia:manifold:demo");
781 let view = QappView::public(m, ViewKind::Pane2D);
782 let standpoint = RenderStandpoint {
783 id: q_hash("urn:qualia:standpoint:owner"),
784 shared_civic: false,
785 };
786
787 assert_eq!(
788 plan_view_with_lod(&view, &standpoint, OperationalMode::Full, &[], &[], 100, 3),
789 LodViewDisposition::Render2d
790 );
791 assert_eq!(
792 plan_view_with_lod(&view, &standpoint, OperationalMode::Eco, &[], &[], 100, 3),
793 LodViewDisposition::Render2d
794 );
795 }
796
797 #[test]
798 fn plan_view_with_lod_attestation_gate() {
799 let m = q_hash("urn:qualia:manifold:demo");
800 let view = QappView {
801 manifold: m,
802 kind: ViewKind::Scene3D,
803 sensitivity: Sensitivity::Public,
804 requires_attestation: true,
805 };
806 let standpoint = RenderStandpoint {
807 id: q_hash("urn:qualia:standpoint:owner"),
808 shared_civic: false,
809 };
810
811 assert_eq!(
813 plan_view_with_lod(&view, &standpoint, OperationalMode::Full, &[], &[], 100, 3),
814 LodViewDisposition::WithheldUnattested
815 );
816
817 let att = attestation_quin(
819 q_hash("did:example:auditor"),
820 m,
821 q_hash("urn:qualia:frame:app"),
822 );
823 assert_eq!(
824 plan_view_with_lod(
825 &view,
826 &standpoint,
827 OperationalMode::Full,
828 &[att],
829 &[],
830 100,
831 3
832 ),
833 LodViewDisposition::Render3dWithLod { lod: 0 }
834 );
835 }
836
837 #[test]
838 fn plan_view_with_lod_rights_bounded() {
839 let m = q_hash("urn:qualia:manifold:demo");
840 let view = QappView {
841 manifold: m,
842 kind: ViewKind::Scene3D,
843 sensitivity: Sensitivity::RightsBounded,
844 requires_attestation: false,
845 };
846 let civic = RenderStandpoint {
847 id: q_hash("urn:qualia:standpoint:civic"),
848 shared_civic: true,
849 };
850
851 assert_eq!(
853 plan_view_with_lod(&view, &civic, OperationalMode::Full, &[], &[], 100, 3),
854 LodViewDisposition::RefusedRightsBounded
855 );
856 }
857
858 #[test]
859 fn existing_authoring_tests_stay_green() {
860 let m = q_hash("urn:qualia:manifold:demo");
862 let view = QappView::public(m, ViewKind::Scene3D);
863 let standpoint = RenderStandpoint {
864 id: q_hash("urn:qualia:standpoint:owner"),
865 shared_civic: false,
866 };
867
868 assert_eq!(
870 plan_view(&view, &standpoint, OperationalMode::Full, &[], &[], 100),
871 ViewDisposition::Render(ViewKind::Scene3D)
872 );
873 assert_eq!(
875 plan_view(&view, &standpoint, OperationalMode::Eco, &[], &[], 100),
876 ViewDisposition::Collapsed2D
877 );
878 }
879
880 #[test]
881 fn empty_source_mesh_errors() {
882 let mesh = Mesh {
883 positions: vec![],
884 triangles: vec![],
885 min: [0.0; 3],
886 max: [0.0; 3],
887 };
888 let options = LodChainOptions::default_3_tier();
889 let mut buf = vec![0u8; 1024];
890 assert!(matches!(
891 build_lod_chain(&mesh, options, &mut buf),
892 Err(LodChainError::EmptySourceMesh)
893 ));
894 }
895
896 #[test]
897 fn buffer_too_small_errors() {
898 let mesh = unit_cube_mesh();
899 let options = LodChainOptions::default_3_tier();
900 let mut buf = vec![0u8; 10]; assert!(matches!(
902 build_lod_chain(&mesh, options, &mut buf),
903 Err(LodChainError::BufferTooSmall { .. })
904 ));
905 }
906
907 #[test]
908 fn single_level_lod_chain() {
909 let mesh = unit_cube_mesh();
910 let options = LodChainOptions::new(1, 0.5);
911 let buf_size =
912 required_lod_buffer_size(mesh.positions.len(), mesh.triangles.len(), options);
913 let mut buf = vec![0u8; buf_size];
914 let report = build_lod_chain(&mesh, options, &mut buf).unwrap();
915 assert_eq!(report.levels.len(), 1);
916 assert_eq!(report.levels[0].vertices, 8);
917 assert_eq!(report.levels[0].triangles, 12);
918 }
919}