1use tracing::{debug, info, warn};
6
7use mesh_repair::{Mesh, ThicknessMap, compute_vertex_normals};
8
9use super::rim::{generate_rim, generate_rim_for_sdf_shell};
10use super::validation::{ShellValidationResult, validate_shell};
11use crate::offset::extract::extract_isosurface;
12use crate::offset::grid::SdfGrid;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum WallGenerationMethod {
17 #[default]
23 Normal,
24
25 Sdf,
33}
34
35impl std::fmt::Display for WallGenerationMethod {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 WallGenerationMethod::Normal => write!(f, "normal"),
39 WallGenerationMethod::Sdf => write!(f, "sdf"),
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct ShellParams {
47 pub wall_thickness_mm: f64,
50 pub thickness_map: Option<ThicknessMap>,
54 pub min_thickness_mm: f64,
56 pub validate_after_generation: bool,
58 pub wall_generation_method: WallGenerationMethod,
60 pub sdf_voxel_size_mm: f64,
64 pub sdf_max_voxels: usize,
67}
68
69impl Default for ShellParams {
70 fn default() -> Self {
71 Self {
72 wall_thickness_mm: 2.5,
73 thickness_map: None,
74 min_thickness_mm: 1.5,
75 validate_after_generation: true,
76 wall_generation_method: WallGenerationMethod::Normal,
77 sdf_voxel_size_mm: 0.5,
78 sdf_max_voxels: 50_000_000,
79 }
80 }
81}
82
83impl ShellParams {
84 pub fn with_thickness_map(mut self, map: ThicknessMap) -> Self {
96 self.thickness_map = Some(map);
97 self
98 }
99
100 pub fn with_uniform_thickness(mut self, thickness: f64) -> Self {
105 self.thickness_map = Some(ThicknessMap::uniform(thickness));
106 self.wall_thickness_mm = thickness;
107 self
108 }
109
110 pub fn high_quality() -> Self {
114 Self {
115 wall_generation_method: WallGenerationMethod::Sdf,
116 sdf_voxel_size_mm: 0.3,
117 ..Default::default()
118 }
119 }
120
121 pub fn fast() -> Self {
125 Self {
126 wall_generation_method: WallGenerationMethod::Normal,
127 validate_after_generation: false,
128 ..Default::default()
129 }
130 }
131
132 pub fn get_vertex_thickness(&self, vertex_index: u32) -> f64 {
137 self.thickness_map
138 .as_ref()
139 .map(|m| m.get_vertex_thickness(vertex_index))
140 .unwrap_or(self.wall_thickness_mm)
141 }
142}
143
144#[derive(Debug)]
146pub struct ShellResult {
147 pub inner_vertex_count: usize,
149 pub outer_vertex_count: usize,
151 pub rim_face_count: usize,
153 pub total_face_count: usize,
155 pub boundary_size: usize,
157 pub validation: Option<ShellValidationResult>,
159 pub wall_method: WallGenerationMethod,
161 pub variable_thickness: bool,
163}
164
165pub fn generate_shell(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
177 let has_variable_thickness = params.thickness_map.is_some();
178
179 if has_variable_thickness {
180 info!(
181 "Generating shell with variable thickness (default={:.2}mm), method={}",
182 params.wall_thickness_mm, params.wall_generation_method
183 );
184 } else {
185 info!(
186 "Generating shell with thickness={:.2}mm, method={}",
187 params.wall_thickness_mm, params.wall_generation_method
188 );
189 }
190
191 match params.wall_generation_method {
192 WallGenerationMethod::Normal => generate_shell_normal(inner_shell, params),
193 WallGenerationMethod::Sdf => generate_shell_sdf(inner_shell, params),
194 }
195}
196
197fn generate_shell_normal(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
199 let n = inner_shell.vertices.len();
200 let mut shell = Mesh::new();
201
202 let mut inner_with_normals = inner_shell.clone();
204 compute_vertex_normals(&mut inner_with_normals);
205
206 for vertex in &inner_with_normals.vertices {
209 shell.vertices.push(vertex.clone());
211 }
212
213 for (i, vertex) in inner_with_normals.vertices.iter().enumerate() {
215 let thickness = params.get_vertex_thickness(i as u32);
217
218 let normal = vertex
220 .normal
221 .unwrap_or_else(|| nalgebra::Vector3::new(0.0, 0.0, 1.0));
222 let outer_pos = vertex.position + normal * thickness;
223
224 let mut outer_vertex = vertex.clone();
225 outer_vertex.position = outer_pos;
226 outer_vertex.normal = Some(normal);
228
229 shell.vertices.push(outer_vertex);
230 }
231
232 debug!("Generated {} inner + {} outer vertices", n, n);
233
234 for face in &inner_shell.faces {
236 shell.faces.push([face[0], face[2], face[1]]);
238 }
239
240 for face in &inner_shell.faces {
242 let n32 = n as u32;
243 shell
244 .faces
245 .push([face[0] + n32, face[1] + n32, face[2] + n32]);
246 }
247
248 let inner_face_count = inner_shell.faces.len();
249 debug!(
250 "Added {} inner + {} outer faces",
251 inner_face_count, inner_face_count
252 );
253
254 let (rim_faces, boundary_size) = generate_rim(&inner_with_normals, n);
256
257 let rim_face_count = rim_faces.len();
258 for face in rim_faces {
259 shell.faces.push(face);
260 }
261
262 info!(
263 "Shell generation complete: {} vertices, {} faces",
264 shell.vertices.len(),
265 shell.faces.len()
266 );
267
268 let validation = if params.validate_after_generation {
270 let validation_result = validate_shell(&shell);
271 if !validation_result.is_printable() {
272 warn!(
273 "Generated shell has {} validation issue(s)",
274 validation_result.issue_count()
275 );
276 }
277 Some(validation_result)
278 } else {
279 None
280 };
281
282 let result = ShellResult {
283 inner_vertex_count: n,
284 outer_vertex_count: n,
285 rim_face_count,
286 total_face_count: shell.faces.len(),
287 boundary_size,
288 validation,
289 wall_method: WallGenerationMethod::Normal,
290 variable_thickness: params.thickness_map.is_some(),
291 };
292
293 (shell, result)
294}
295
296fn generate_shell_sdf(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
302 let inner_vertex_count = inner_shell.vertices.len();
303
304 if params.thickness_map.is_some() {
306 warn!(
307 "Variable thickness (ThicknessMap) is not fully supported with SDF wall generation. \
308 Using uniform thickness={:.2}mm. Consider using WallGenerationMethod::Normal for variable thickness.",
309 params.wall_thickness_mm
310 );
311 }
312
313 let mut inner_with_normals = inner_shell.clone();
315 compute_vertex_normals(&mut inner_with_normals);
316
317 let padding = params.wall_thickness_mm + params.sdf_voxel_size_mm * 3.0;
319 let grid_result = SdfGrid::from_mesh_bounds(
320 &inner_with_normals,
321 params.sdf_voxel_size_mm,
322 padding,
323 params.sdf_max_voxels,
324 );
325
326 let mut grid = match grid_result {
327 Ok(g) => g,
328 Err(e) => {
329 warn!(
330 "SDF grid creation failed: {:?}, falling back to normal method",
331 e
332 );
333 return generate_shell_normal(inner_shell, params);
334 }
335 };
336
337 info!(
338 dims = ?grid.dims,
339 total_voxels = grid.total_voxels(),
340 "Created SDF grid for wall generation"
341 );
342
343 grid.compute_sdf(&inner_with_normals);
345
346 for val in &mut grid.values {
349 *val -= params.wall_thickness_mm as f32;
350 }
351
352 debug!("Applied wall thickness offset to SDF");
353
354 let outer_mesh = match extract_isosurface(&grid) {
356 Ok(m) => m,
357 Err(e) => {
358 warn!(
359 "Isosurface extraction failed: {:?}, falling back to normal method",
360 e
361 );
362 return generate_shell_normal(inner_shell, params);
363 }
364 };
365
366 let outer_vertex_count = outer_mesh.vertices.len();
367 debug!(
368 "Extracted outer surface: {} vertices, {} faces",
369 outer_vertex_count,
370 outer_mesh.faces.len()
371 );
372
373 let mut shell = Mesh::new();
375
376 for vertex in &inner_with_normals.vertices {
378 shell.vertices.push(vertex.clone());
379 }
380
381 let inner_count = inner_with_normals.vertices.len() as u32;
383 for vertex in &outer_mesh.vertices {
384 shell.vertices.push(vertex.clone());
385 }
386
387 for face in &inner_with_normals.faces {
389 shell.faces.push([face[0], face[2], face[1]]);
390 }
391
392 for face in &outer_mesh.faces {
394 shell.faces.push([
395 face[0] + inner_count,
396 face[1] + inner_count,
397 face[2] + inner_count,
398 ]);
399 }
400
401 let (rim_faces, boundary_size) =
403 generate_rim_for_sdf_shell(&inner_with_normals, &outer_mesh, inner_count as usize);
404
405 let rim_face_count = rim_faces.len();
406 for face in rim_faces {
407 shell.faces.push(face);
408 }
409
410 info!(
411 "SDF shell generation complete: {} vertices, {} faces (rim: {})",
412 shell.vertices.len(),
413 shell.faces.len(),
414 rim_face_count
415 );
416
417 let validation = if params.validate_after_generation {
419 let validation_result = validate_shell(&shell);
420 if !validation_result.is_printable() {
421 warn!(
422 "Generated shell has {} validation issue(s)",
423 validation_result.issue_count()
424 );
425 }
426 Some(validation_result)
427 } else {
428 None
429 };
430
431 let result = ShellResult {
432 inner_vertex_count,
433 outer_vertex_count,
434 rim_face_count,
435 total_face_count: shell.faces.len(),
436 boundary_size,
437 validation,
438 wall_method: WallGenerationMethod::Sdf,
439 variable_thickness: false, };
441
442 (shell, result)
443}
444
445pub fn generate_shell_no_validation(
449 inner_shell: &Mesh,
450 params: &ShellParams,
451) -> (Mesh, ShellResult) {
452 let mut params = params.clone();
453 params.validate_after_generation = false;
454 generate_shell(inner_shell, ¶ms)
455}
456
457pub fn generate_shell_with_progress(
491 inner_shell: &Mesh,
492 params: &ShellParams,
493 callback: Option<&mesh_repair::progress::ProgressCallback>,
494) -> (Mesh, ShellResult) {
495 use mesh_repair::progress::ProgressTracker;
496
497 let has_variable_thickness = params.thickness_map.is_some();
498
499 if has_variable_thickness {
500 info!(
501 "Generating shell with variable thickness (default={:.2}mm), method={}",
502 params.wall_thickness_mm, params.wall_generation_method
503 );
504 } else {
505 info!(
506 "Generating shell with thickness={:.2}mm, method={}",
507 params.wall_thickness_mm, params.wall_generation_method
508 );
509 }
510
511 let tracker = ProgressTracker::new(100);
513
514 tracker.set(5);
516 if !tracker.maybe_callback(callback, "Computing vertex normals".to_string()) {
517 return empty_shell_result(params);
518 }
519
520 let n = inner_shell.vertices.len();
521 let mut inner_with_normals = inner_shell.clone();
522 compute_vertex_normals(&mut inner_with_normals);
523
524 match params.wall_generation_method {
526 WallGenerationMethod::Normal => generate_shell_normal_with_progress(
527 inner_shell,
528 params,
529 &inner_with_normals,
530 n,
531 &tracker,
532 callback,
533 ),
534 WallGenerationMethod::Sdf => generate_shell_sdf_with_progress(
535 inner_shell,
536 params,
537 &inner_with_normals,
538 &tracker,
539 callback,
540 ),
541 }
542}
543
544fn empty_shell_result(params: &ShellParams) -> (Mesh, ShellResult) {
546 (
547 Mesh::new(),
548 ShellResult {
549 inner_vertex_count: 0,
550 outer_vertex_count: 0,
551 rim_face_count: 0,
552 total_face_count: 0,
553 boundary_size: 0,
554 validation: None,
555 wall_method: params.wall_generation_method,
556 variable_thickness: params.thickness_map.is_some(),
557 },
558 )
559}
560
561fn generate_shell_normal_with_progress(
563 inner_shell: &Mesh,
564 params: &ShellParams,
565 inner_with_normals: &Mesh,
566 n: usize,
567 tracker: &mesh_repair::progress::ProgressTracker,
568 callback: Option<&mesh_repair::progress::ProgressCallback>,
569) -> (Mesh, ShellResult) {
570 let mut shell = Mesh::new();
571
572 tracker.set(10);
574 if !tracker.maybe_callback(callback, "Copying inner vertices".to_string()) {
575 return empty_shell_result(params);
576 }
577
578 for vertex in &inner_with_normals.vertices {
579 shell.vertices.push(vertex.clone());
580 }
581
582 tracker.set(30);
584 if !tracker.maybe_callback(callback, "Generating outer surface vertices".to_string()) {
585 return empty_shell_result(params);
586 }
587
588 for (i, vertex) in inner_with_normals.vertices.iter().enumerate() {
589 let thickness = params.get_vertex_thickness(i as u32);
590 let normal = vertex
591 .normal
592 .unwrap_or_else(|| nalgebra::Vector3::new(0.0, 0.0, 1.0));
593 let outer_pos = vertex.position + normal * thickness;
594
595 let mut outer_vertex = vertex.clone();
596 outer_vertex.position = outer_pos;
597 outer_vertex.normal = Some(normal);
598
599 shell.vertices.push(outer_vertex);
600 }
601
602 debug!("Generated {} inner + {} outer vertices", n, n);
603
604 tracker.set(50);
606 if !tracker.maybe_callback(callback, "Creating inner and outer faces".to_string()) {
607 return empty_shell_result(params);
608 }
609
610 for face in &inner_shell.faces {
612 shell.faces.push([face[0], face[2], face[1]]);
613 }
614
615 for face in &inner_shell.faces {
617 let n32 = n as u32;
618 shell
619 .faces
620 .push([face[0] + n32, face[1] + n32, face[2] + n32]);
621 }
622
623 let inner_face_count = inner_shell.faces.len();
624 debug!(
625 "Added {} inner + {} outer faces",
626 inner_face_count, inner_face_count
627 );
628
629 tracker.set(70);
631 if !tracker.maybe_callback(callback, "Generating rim to connect boundaries".to_string()) {
632 return empty_shell_result(params);
633 }
634
635 let (rim_faces, boundary_size) = generate_rim(inner_with_normals, n);
636
637 let rim_face_count = rim_faces.len();
638 for face in rim_faces {
639 shell.faces.push(face);
640 }
641
642 info!(
643 "Shell generation complete: {} vertices, {} faces",
644 shell.vertices.len(),
645 shell.faces.len()
646 );
647
648 tracker.set(90);
650 let validation = if params.validate_after_generation {
651 if !tracker.maybe_callback(callback, "Validating shell".to_string()) {
652 return (
653 shell.clone(),
654 ShellResult {
655 inner_vertex_count: n,
656 outer_vertex_count: n,
657 rim_face_count,
658 total_face_count: shell.faces.len(),
659 boundary_size,
660 validation: None,
661 wall_method: WallGenerationMethod::Normal,
662 variable_thickness: params.thickness_map.is_some(),
663 },
664 );
665 }
666
667 let validation_result = validate_shell(&shell);
668 if !validation_result.is_printable() {
669 warn!(
670 "Generated shell has {} validation issue(s)",
671 validation_result.issue_count()
672 );
673 }
674 Some(validation_result)
675 } else {
676 None
677 };
678
679 tracker.set(100);
680 let _ = tracker.maybe_callback(callback, "Shell generation complete".to_string());
681
682 let result = ShellResult {
683 inner_vertex_count: n,
684 outer_vertex_count: n,
685 rim_face_count,
686 total_face_count: shell.faces.len(),
687 boundary_size,
688 validation,
689 wall_method: WallGenerationMethod::Normal,
690 variable_thickness: params.thickness_map.is_some(),
691 };
692
693 (shell, result)
694}
695
696fn generate_shell_sdf_with_progress(
698 inner_shell: &Mesh,
699 params: &ShellParams,
700 inner_with_normals: &Mesh,
701 tracker: &mesh_repair::progress::ProgressTracker,
702 callback: Option<&mesh_repair::progress::ProgressCallback>,
703) -> (Mesh, ShellResult) {
704 let inner_vertex_count = inner_with_normals.vertices.len();
705
706 if params.thickness_map.is_some() {
708 warn!(
709 "Variable thickness (ThicknessMap) is not fully supported with SDF wall generation. \
710 Using uniform thickness={:.2}mm. Consider using WallGenerationMethod::Normal for variable thickness.",
711 params.wall_thickness_mm
712 );
713 }
714
715 tracker.set(20);
717 if !tracker.maybe_callback(callback, "Creating SDF grid".to_string()) {
718 return empty_shell_result(params);
719 }
720
721 let padding = params.wall_thickness_mm + params.sdf_voxel_size_mm * 3.0;
722 let grid_result = SdfGrid::from_mesh_bounds(
723 inner_with_normals,
724 params.sdf_voxel_size_mm,
725 padding,
726 params.sdf_max_voxels,
727 );
728
729 let mut grid = match grid_result {
730 Ok(g) => g,
731 Err(e) => {
732 warn!(
733 "SDF grid creation failed: {:?}, falling back to normal method",
734 e
735 );
736 return generate_shell_normal(inner_shell, params);
737 }
738 };
739
740 info!(
741 dims = ?grid.dims,
742 total_voxels = grid.total_voxels(),
743 "Created SDF grid for wall generation"
744 );
745
746 tracker.set(40);
748 if !tracker.maybe_callback(callback, "Computing signed distance field".to_string()) {
749 return empty_shell_result(params);
750 }
751
752 grid.compute_sdf(inner_with_normals);
753
754 tracker.set(50);
756 if !tracker.maybe_callback(callback, "Applying wall thickness offset".to_string()) {
757 return empty_shell_result(params);
758 }
759
760 for val in &mut grid.values {
761 *val -= params.wall_thickness_mm as f32;
762 }
763
764 debug!("Applied wall thickness offset to SDF");
765
766 tracker.set(60);
768 if !tracker.maybe_callback(callback, "Extracting outer surface isosurface".to_string()) {
769 return empty_shell_result(params);
770 }
771
772 let outer_mesh = match extract_isosurface(&grid) {
773 Ok(m) => m,
774 Err(e) => {
775 warn!(
776 "Isosurface extraction failed: {:?}, falling back to normal method",
777 e
778 );
779 return generate_shell_normal(inner_shell, params);
780 }
781 };
782
783 let outer_vertex_count = outer_mesh.vertices.len();
784 debug!(
785 "Extracted outer surface: {} vertices, {} faces",
786 outer_vertex_count,
787 outer_mesh.faces.len()
788 );
789
790 tracker.set(70);
792 if !tracker.maybe_callback(callback, "Combining inner and outer surfaces".to_string()) {
793 return empty_shell_result(params);
794 }
795
796 let mut shell = Mesh::new();
797
798 for vertex in &inner_with_normals.vertices {
800 shell.vertices.push(vertex.clone());
801 }
802
803 let inner_count = inner_with_normals.vertices.len() as u32;
805 for vertex in &outer_mesh.vertices {
806 shell.vertices.push(vertex.clone());
807 }
808
809 for face in &inner_with_normals.faces {
811 shell.faces.push([face[0], face[2], face[1]]);
812 }
813
814 for face in &outer_mesh.faces {
816 shell.faces.push([
817 face[0] + inner_count,
818 face[1] + inner_count,
819 face[2] + inner_count,
820 ]);
821 }
822
823 tracker.set(80);
825 if !tracker.maybe_callback(callback, "Generating rim to connect boundaries".to_string()) {
826 return empty_shell_result(params);
827 }
828
829 let (rim_faces, boundary_size) =
830 generate_rim_for_sdf_shell(inner_with_normals, &outer_mesh, inner_count as usize);
831
832 let rim_face_count = rim_faces.len();
833 for face in rim_faces {
834 shell.faces.push(face);
835 }
836
837 info!(
838 "SDF shell generation complete: {} vertices, {} faces (rim: {})",
839 shell.vertices.len(),
840 shell.faces.len(),
841 rim_face_count
842 );
843
844 tracker.set(90);
846 let validation = if params.validate_after_generation {
847 if !tracker.maybe_callback(callback, "Validating shell".to_string()) {
848 return (
849 shell.clone(),
850 ShellResult {
851 inner_vertex_count,
852 outer_vertex_count,
853 rim_face_count,
854 total_face_count: shell.faces.len(),
855 boundary_size,
856 validation: None,
857 wall_method: WallGenerationMethod::Sdf,
858 variable_thickness: false,
859 },
860 );
861 }
862
863 let validation_result = validate_shell(&shell);
864 if !validation_result.is_printable() {
865 warn!(
866 "Generated shell has {} validation issue(s)",
867 validation_result.issue_count()
868 );
869 }
870 Some(validation_result)
871 } else {
872 None
873 };
874
875 tracker.set(100);
876 let _ = tracker.maybe_callback(callback, "Shell generation complete".to_string());
877
878 let result = ShellResult {
879 inner_vertex_count,
880 outer_vertex_count,
881 rim_face_count,
882 total_face_count: shell.faces.len(),
883 boundary_size,
884 validation,
885 wall_method: WallGenerationMethod::Sdf,
886 variable_thickness: false,
887 };
888
889 (shell, result)
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895 use mesh_repair::Vertex;
896
897 fn create_open_box() -> Mesh {
898 let mut mesh = Mesh::new();
900
901 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
903 mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
904 mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
905 mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
906 mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 10.0));
908 mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 10.0));
909 mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 10.0));
910 mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 10.0));
911
912 mesh.faces.push([0, 2, 1]);
914 mesh.faces.push([0, 3, 2]);
915 mesh.faces.push([0, 1, 5]);
917 mesh.faces.push([0, 5, 4]);
918 mesh.faces.push([2, 3, 7]);
920 mesh.faces.push([2, 7, 6]);
921 mesh.faces.push([0, 4, 7]);
923 mesh.faces.push([0, 7, 3]);
924 mesh.faces.push([1, 2, 6]);
926 mesh.faces.push([1, 6, 5]);
927 mesh
930 }
931
932 #[test]
933 fn test_shell_params_default() {
934 let params = ShellParams::default();
935 assert_eq!(params.wall_thickness_mm, 2.5);
936 assert_eq!(params.min_thickness_mm, 1.5);
937 assert!(params.validate_after_generation);
938 assert_eq!(params.wall_generation_method, WallGenerationMethod::Normal);
939 }
940
941 #[test]
942 fn test_shell_params_high_quality() {
943 let params = ShellParams::high_quality();
944 assert_eq!(params.wall_generation_method, WallGenerationMethod::Sdf);
945 assert!(params.sdf_voxel_size_mm < 0.5);
946 }
947
948 #[test]
949 fn test_shell_params_fast() {
950 let params = ShellParams::fast();
951 assert_eq!(params.wall_generation_method, WallGenerationMethod::Normal);
952 assert!(!params.validate_after_generation);
953 }
954
955 #[test]
956 fn test_wall_generation_method_display() {
957 assert_eq!(format!("{}", WallGenerationMethod::Normal), "normal");
958 assert_eq!(format!("{}", WallGenerationMethod::Sdf), "sdf");
959 }
960
961 #[test]
962 fn test_generate_shell_doubles_vertices() {
963 let inner = create_open_box();
964 let params = ShellParams::default();
965
966 let (shell, result) = generate_shell(&inner, ¶ms);
967
968 assert_eq!(shell.vertices.len(), inner.vertices.len() * 2);
970 assert_eq!(result.inner_vertex_count, inner.vertices.len());
971 assert_eq!(result.outer_vertex_count, inner.vertices.len());
972 assert_eq!(result.wall_method, WallGenerationMethod::Normal);
973 }
974
975 #[test]
976 fn test_shell_has_more_faces() {
977 let inner = create_open_box();
978 let params = ShellParams::default();
979
980 let (shell, result) = generate_shell(&inner, ¶ms);
981
982 assert!(shell.faces.len() > inner.faces.len() * 2);
984 assert!(result.rim_face_count > 0);
985 }
986
987 #[test]
988 fn test_generate_shell_sdf_method() {
989 let inner = create_open_box();
990 let params = ShellParams {
991 wall_generation_method: WallGenerationMethod::Sdf,
992 sdf_voxel_size_mm: 1.0, validate_after_generation: false,
994 ..Default::default()
995 };
996
997 let (shell, result) = generate_shell(&inner, ¶ms);
998
999 assert!(!shell.vertices.is_empty());
1001 assert!(!shell.faces.is_empty());
1002 assert_eq!(result.wall_method, WallGenerationMethod::Sdf);
1003
1004 assert_eq!(result.inner_vertex_count, inner.vertices.len());
1006
1007 assert!(result.outer_vertex_count > 0);
1009 }
1010
1011 #[test]
1012 fn test_sdf_produces_larger_outer_surface() {
1013 let inner = create_open_box();
1014 let wall_thickness = 2.0;
1015
1016 let params = ShellParams {
1017 wall_thickness_mm: wall_thickness,
1018 wall_generation_method: WallGenerationMethod::Sdf,
1019 sdf_voxel_size_mm: 0.5,
1020 validate_after_generation: false,
1021 ..Default::default()
1022 };
1023
1024 let (shell, _result) = generate_shell(&inner, ¶ms);
1025
1026 let inner_bounds = inner.bounds().unwrap();
1028 let shell_bounds = shell.bounds().unwrap();
1029
1030 let inner_extent = inner_bounds.1 - inner_bounds.0;
1032 let shell_extent = shell_bounds.1 - shell_bounds.0;
1033
1034 assert!(
1036 shell_extent.x > inner_extent.x,
1037 "Shell should be wider: {} vs {}",
1038 shell_extent.x,
1039 inner_extent.x
1040 );
1041 assert!(
1042 shell_extent.y > inner_extent.y,
1043 "Shell should be deeper: {} vs {}",
1044 shell_extent.y,
1045 inner_extent.y
1046 );
1047 }
1048
1049 #[test]
1050 fn test_variable_thickness_params() {
1051 let mut thickness_map = ThicknessMap::new(2.0);
1052 thickness_map.set_vertex_thickness(0, 3.0);
1053 thickness_map.set_vertex_thickness(1, 1.5);
1054
1055 let params = ShellParams::default().with_thickness_map(thickness_map);
1056
1057 assert!(params.thickness_map.is_some());
1058 assert_eq!(params.get_vertex_thickness(0), 3.0);
1059 assert_eq!(params.get_vertex_thickness(1), 1.5);
1060 assert_eq!(params.get_vertex_thickness(2), 2.0); }
1062
1063 #[test]
1064 fn test_variable_thickness_shell_generation() {
1065 let inner = create_open_box();
1066
1067 let mut thickness_map = ThicknessMap::new(2.0);
1069 for i in 0..4 {
1071 thickness_map.set_vertex_thickness(i, 1.0);
1072 }
1073 for i in 4..8 {
1075 thickness_map.set_vertex_thickness(i, 3.0);
1076 }
1077
1078 let params = ShellParams {
1079 wall_generation_method: WallGenerationMethod::Normal,
1080 validate_after_generation: false,
1081 ..ShellParams::default()
1082 }
1083 .with_thickness_map(thickness_map);
1084
1085 let (shell, result) = generate_shell(&inner, ¶ms);
1086
1087 assert!(result.variable_thickness);
1089
1090 assert_eq!(shell.vertices.len(), inner.vertices.len() * 2);
1092
1093 let inner_vertex_count = inner.vertices.len();
1095
1096 let inner_v0 = shell.vertices[0].position;
1098 let outer_v0 = shell.vertices[inner_vertex_count].position;
1099 let offset_0 = (outer_v0 - inner_v0).norm();
1100
1101 let inner_v4 = shell.vertices[4].position;
1103 let outer_v4 = shell.vertices[inner_vertex_count + 4].position;
1104 let offset_4 = (outer_v4 - inner_v4).norm();
1105
1106 assert!(
1107 offset_4 > offset_0,
1108 "Top vertices should have larger offset: {} vs {}",
1109 offset_4,
1110 offset_0
1111 );
1112
1113 assert!(
1115 offset_0 < 2.0,
1116 "Bottom offset should be around 1mm: {}",
1117 offset_0
1118 );
1119 assert!(
1120 offset_4 > 2.0,
1121 "Top offset should be around 3mm: {}",
1122 offset_4
1123 );
1124 }
1125
1126 #[test]
1127 fn test_uniform_thickness_via_map() {
1128 let inner = create_open_box();
1129
1130 let params = ShellParams::default().with_uniform_thickness(2.5);
1132
1133 let (_shell, result) = generate_shell(&inner, ¶ms);
1134
1135 assert!(result.variable_thickness); assert_eq!(params.wall_thickness_mm, 2.5);
1137 }
1138
1139 #[test]
1140 fn test_get_vertex_thickness_without_map() {
1141 let params = ShellParams {
1142 wall_thickness_mm: 3.0,
1143 ..Default::default()
1144 };
1145
1146 assert_eq!(params.get_vertex_thickness(0), 3.0);
1148 assert_eq!(params.get_vertex_thickness(100), 3.0);
1149 }
1150}