1use crate::Mesh;
39use crate::measure::{CrossSection, cross_section};
40use nalgebra::{Point3, Vector3};
41
42#[derive(Debug, Clone)]
44pub struct SliceParams {
45 pub layer_height: f64,
47
48 pub direction: Vector3<f64>,
50
51 pub first_layer_height: f64,
53
54 pub infill_density: f64,
56
57 pub perimeters: usize,
59
60 pub perimeter_width: f64,
62
63 pub perimeter_speed: f64,
65
66 pub infill_speed: f64,
68
69 pub travel_speed: f64,
71
72 pub extrusion_multiplier: f64,
74}
75
76impl Default for SliceParams {
77 fn default() -> Self {
78 Self {
79 layer_height: 0.2,
80 direction: Vector3::z(),
81 first_layer_height: 0.3,
82 infill_density: 0.2,
83 perimeters: 2,
84 perimeter_width: 0.4,
85 perimeter_speed: 40.0,
86 infill_speed: 60.0,
87 travel_speed: 150.0,
88 extrusion_multiplier: 1.0,
89 }
90 }
91}
92
93impl SliceParams {
94 pub fn high_quality() -> Self {
96 Self {
97 layer_height: 0.1,
98 first_layer_height: 0.2,
99 infill_density: 0.3,
100 perimeters: 3,
101 perimeter_speed: 30.0,
102 infill_speed: 40.0,
103 ..Default::default()
104 }
105 }
106
107 pub fn draft() -> Self {
109 Self {
110 layer_height: 0.3,
111 first_layer_height: 0.35,
112 infill_density: 0.1,
113 perimeters: 2,
114 perimeter_speed: 60.0,
115 infill_speed: 80.0,
116 ..Default::default()
117 }
118 }
119
120 pub fn for_sla() -> Self {
122 Self {
123 layer_height: 0.05,
124 first_layer_height: 0.05,
125 infill_density: 1.0, perimeters: 0, perimeter_width: 0.0,
128 perimeter_speed: 0.0,
129 infill_speed: 0.0,
130 travel_speed: 0.0,
131 ..Default::default()
132 }
133 }
134}
135
136#[derive(Debug)]
138pub struct SliceResult {
139 pub layers: Vec<Layer>,
141
142 pub total_height: f64,
144
145 pub layer_count: usize,
147
148 pub estimated_print_time: f64,
150
151 pub estimated_filament_length: f64,
153
154 pub estimated_filament_volume: f64,
156
157 pub max_area_layer: usize,
159
160 pub max_perimeter_layer: usize,
162
163 pub params: SliceParams,
165}
166
167#[derive(Debug, Clone)]
169pub struct Layer {
170 pub index: usize,
172
173 pub z_height: f64,
175
176 pub thickness: f64,
178
179 pub contours: Vec<Contour>,
181
182 pub area: f64,
184
185 pub perimeter: f64,
187
188 pub print_time: f64,
190
191 pub filament_length: f64,
193
194 pub island_count: usize,
196
197 pub bounds: LayerBounds,
199}
200
201#[derive(Debug, Clone)]
203pub struct Contour {
204 pub points: Vec<Point3<f64>>,
206
207 pub area: f64,
209
210 pub perimeter: f64,
212
213 pub is_outer: bool,
215
216 pub centroid: Point3<f64>,
218}
219
220#[derive(Debug, Clone)]
222pub struct LayerBounds {
223 pub min_x: f64,
225 pub max_x: f64,
227 pub min_y: f64,
229 pub max_y: f64,
231}
232
233impl LayerBounds {
234 pub fn width(&self) -> f64 {
236 self.max_x - self.min_x
237 }
238
239 pub fn height(&self) -> f64 {
241 self.max_y - self.min_y
242 }
243
244 pub fn center(&self) -> (f64, f64) {
246 (
247 (self.min_x + self.max_x) / 2.0,
248 (self.min_y + self.max_y) / 2.0,
249 )
250 }
251}
252
253#[derive(Debug, Clone)]
255pub struct LayerStats {
256 pub min_area: f64,
258 pub max_area: f64,
260 pub avg_area: f64,
262 pub min_perimeter: f64,
264 pub max_perimeter: f64,
266 pub avg_perimeter: f64,
268 pub max_islands: usize,
270}
271
272pub fn slice_mesh(mesh: &Mesh, params: &SliceParams) -> SliceResult {
278 let (min_z, max_z) = find_z_bounds(mesh, ¶ms.direction);
280 let total_height = max_z - min_z;
281
282 if total_height <= 0.0 || mesh.vertices.is_empty() {
283 return SliceResult {
284 layers: Vec::new(),
285 total_height: 0.0,
286 layer_count: 0,
287 estimated_print_time: 0.0,
288 estimated_filament_length: 0.0,
289 estimated_filament_volume: 0.0,
290 max_area_layer: 0,
291 max_perimeter_layer: 0,
292 params: params.clone(),
293 };
294 }
295
296 let mut z_heights = Vec::new();
298 let mut current_z = min_z + params.first_layer_height;
299 z_heights.push((current_z, params.first_layer_height));
300
301 while current_z < max_z {
302 current_z += params.layer_height;
303 if current_z <= max_z {
304 z_heights.push((current_z, params.layer_height));
305 }
306 }
307
308 let mut layers = Vec::with_capacity(z_heights.len());
310 let mut max_area = 0.0;
311 let mut max_area_layer = 0;
312 let mut max_perimeter = 0.0;
313 let mut max_perimeter_layer = 0;
314 let mut total_print_time = 0.0;
315 let mut total_filament = 0.0;
316
317 for (index, (z, thickness)) in z_heights.iter().enumerate() {
318 let layer = generate_layer(mesh, index, *z, *thickness, params);
319
320 if layer.area > max_area {
321 max_area = layer.area;
322 max_area_layer = index;
323 }
324 if layer.perimeter > max_perimeter {
325 max_perimeter = layer.perimeter;
326 max_perimeter_layer = index;
327 }
328
329 total_print_time += layer.print_time;
330 total_filament += layer.filament_length;
331
332 layers.push(layer);
333 }
334
335 let filament_diameter: f64 = 1.75;
337 let filament_area = std::f64::consts::PI * (filament_diameter / 2.0).powi(2);
338 let filament_volume = total_filament * filament_area;
339
340 SliceResult {
341 layers,
342 total_height,
343 layer_count: z_heights.len(),
344 estimated_print_time: total_print_time / 60.0, estimated_filament_length: total_filament,
346 estimated_filament_volume: filament_volume,
347 max_area_layer,
348 max_perimeter_layer,
349 params: params.clone(),
350 }
351}
352
353pub fn slice_preview(mesh: &Mesh, z: f64, params: &SliceParams) -> Layer {
355 generate_layer(mesh, 0, z, params.layer_height, params)
356}
357
358pub fn calculate_layer_stats(result: &SliceResult) -> LayerStats {
360 if result.layers.is_empty() {
361 return LayerStats {
362 min_area: 0.0,
363 max_area: 0.0,
364 avg_area: 0.0,
365 min_perimeter: 0.0,
366 max_perimeter: 0.0,
367 avg_perimeter: 0.0,
368 max_islands: 0,
369 };
370 }
371
372 let mut min_area: f64 = f64::INFINITY;
373 let mut max_area: f64 = 0.0;
374 let mut sum_area: f64 = 0.0;
375 let mut min_perimeter: f64 = f64::INFINITY;
376 let mut max_perimeter: f64 = 0.0;
377 let mut sum_perimeter = 0.0;
378 let mut max_islands = 0;
379
380 for layer in &result.layers {
381 min_area = min_area.min(layer.area);
382 max_area = max_area.max(layer.area);
383 sum_area += layer.area;
384 min_perimeter = min_perimeter.min(layer.perimeter);
385 max_perimeter = max_perimeter.max(layer.perimeter);
386 sum_perimeter += layer.perimeter;
387 max_islands = max_islands.max(layer.island_count);
388 }
389
390 let n = result.layers.len() as f64;
391 LayerStats {
392 min_area,
393 max_area,
394 avg_area: sum_area / n,
395 min_perimeter,
396 max_perimeter,
397 avg_perimeter: sum_perimeter / n,
398 max_islands,
399 }
400}
401
402fn find_z_bounds(mesh: &Mesh, direction: &Vector3<f64>) -> (f64, f64) {
407 if mesh.vertices.is_empty() {
408 return (0.0, 0.0);
409 }
410
411 let dir = direction.normalize();
412 let mut min_z = f64::INFINITY;
413 let mut max_z = f64::NEG_INFINITY;
414
415 for v in &mesh.vertices {
416 let z = v.position.coords.dot(&dir);
417 min_z = min_z.min(z);
418 max_z = max_z.max(z);
419 }
420
421 (min_z, max_z)
422}
423
424fn generate_layer(
425 mesh: &Mesh,
426 index: usize,
427 z: f64,
428 thickness: f64,
429 params: &SliceParams,
430) -> Layer {
431 let plane_point = Point3::new(0.0, 0.0, z);
433 let section = cross_section(mesh, plane_point, params.direction);
434
435 let contours = extract_contours(§ion);
437 let island_count = contours.iter().filter(|c| c.is_outer).count();
438
439 let bounds = calculate_layer_bounds(&contours);
441
442 let print_time = estimate_layer_print_time(&contours, params);
444
445 let filament_length = estimate_filament_usage(&contours, thickness, params);
447
448 Layer {
449 index,
450 z_height: z,
451 thickness,
452 area: section.area,
453 perimeter: section.perimeter,
454 contours,
455 print_time,
456 filament_length,
457 island_count,
458 bounds,
459 }
460}
461
462fn extract_contours(section: &CrossSection) -> Vec<Contour> {
463 if section.points.is_empty() {
464 return Vec::new();
465 }
466
467 let perimeter = section.perimeter;
470 let area = section.area;
471
472 vec![Contour {
473 points: section.points.clone(),
474 area,
475 perimeter,
476 is_outer: true,
477 centroid: section.centroid,
478 }]
479}
480
481fn calculate_layer_bounds(contours: &[Contour]) -> LayerBounds {
482 if contours.is_empty() {
483 return LayerBounds {
484 min_x: 0.0,
485 max_x: 0.0,
486 min_y: 0.0,
487 max_y: 0.0,
488 };
489 }
490
491 let mut min_x = f64::INFINITY;
492 let mut max_x = f64::NEG_INFINITY;
493 let mut min_y = f64::INFINITY;
494 let mut max_y = f64::NEG_INFINITY;
495
496 for contour in contours {
497 for p in &contour.points {
498 min_x = min_x.min(p.x);
499 max_x = max_x.max(p.x);
500 min_y = min_y.min(p.y);
501 max_y = max_y.max(p.y);
502 }
503 }
504
505 LayerBounds {
506 min_x,
507 max_x,
508 min_y,
509 max_y,
510 }
511}
512
513fn estimate_layer_print_time(contours: &[Contour], params: &SliceParams) -> f64 {
514 if contours.is_empty() {
515 return 0.0;
516 }
517
518 let mut time = 0.0;
519
520 for contour in contours {
521 let perimeter_passes = params.perimeters as f64;
523 let perimeter_length = contour.perimeter * perimeter_passes;
524 time += perimeter_length / params.perimeter_speed;
525
526 if params.infill_density > 0.0 {
528 let infill_spacing = params.perimeter_width / params.infill_density;
530 let infill_length = contour.area / infill_spacing;
531 time += infill_length / params.infill_speed;
532 }
533 }
534
535 time *= 1.1;
537
538 time
539}
540
541fn estimate_filament_usage(contours: &[Contour], layer_height: f64, params: &SliceParams) -> f64 {
542 if contours.is_empty() {
543 return 0.0;
544 }
545
546 let mut volume = 0.0;
547
548 for contour in contours {
549 let perimeter_passes = params.perimeters as f64;
551 let perimeter_length = contour.perimeter * perimeter_passes;
552 let perimeter_cross_section = params.perimeter_width * layer_height;
553 volume += perimeter_length * perimeter_cross_section;
554
555 if params.infill_density > 0.0 {
557 let infill_volume = contour.area * layer_height * params.infill_density;
558 volume += infill_volume;
559 }
560 }
561
562 volume *= params.extrusion_multiplier;
563
564 let filament_diameter: f64 = 1.75;
566 let filament_area = std::f64::consts::PI * (filament_diameter / 2.0).powi(2);
567 volume / filament_area
568}
569
570#[derive(Debug, Clone)]
576pub struct FdmParams {
577 pub nozzle_diameter: f64,
579
580 pub min_wall_thickness: f64,
582
583 pub layer_height: f64,
585
586 pub min_feature_size: f64,
588
589 pub max_overhang_angle: f64,
591
592 pub min_gap: f64,
594}
595
596impl Default for FdmParams {
597 fn default() -> Self {
598 Self {
599 nozzle_diameter: 0.4,
600 min_wall_thickness: 0.8, layer_height: 0.2,
602 min_feature_size: 0.4,
603 max_overhang_angle: 45.0,
604 min_gap: 0.4,
605 }
606 }
607}
608
609impl FdmParams {
610 pub fn nozzle_04() -> Self {
612 Self::default()
613 }
614
615 pub fn nozzle_06() -> Self {
617 Self {
618 nozzle_diameter: 0.6,
619 min_wall_thickness: 1.2,
620 layer_height: 0.3,
621 min_feature_size: 0.6,
622 min_gap: 0.6,
623 ..Default::default()
624 }
625 }
626
627 pub fn nozzle_025() -> Self {
629 Self {
630 nozzle_diameter: 0.25,
631 min_wall_thickness: 0.5,
632 layer_height: 0.1,
633 min_feature_size: 0.25,
634 min_gap: 0.25,
635 ..Default::default()
636 }
637 }
638}
639
640#[derive(Debug, Clone)]
642pub struct FdmValidationResult {
643 pub is_valid: bool,
645
646 pub thin_wall_layers: Vec<ThinWallIssue>,
648
649 pub small_feature_layers: Vec<SmallFeatureIssue>,
651
652 pub gap_issues: Vec<GapIssue>,
654
655 pub issue_count: usize,
657
658 pub summary: String,
660}
661
662impl FdmValidationResult {
663 pub fn is_printable(&self) -> bool {
665 self.thin_wall_layers.is_empty()
666 }
667}
668
669#[derive(Debug, Clone)]
671pub struct ThinWallIssue {
672 pub layer_index: usize,
674 pub z_height: f64,
676 pub min_thickness: f64,
678 pub required_thickness: f64,
680 pub location: (f64, f64),
682}
683
684#[derive(Debug, Clone)]
686pub struct SmallFeatureIssue {
687 pub layer_index: usize,
689 pub z_height: f64,
691 pub feature_size: f64,
693 pub min_size: f64,
695}
696
697#[derive(Debug, Clone)]
699pub struct GapIssue {
700 pub layer_index: usize,
702 pub z_height: f64,
704 pub gap_size: f64,
706 pub min_gap: f64,
708}
709
710pub fn validate_for_fdm(mesh: &Mesh, params: &FdmParams) -> FdmValidationResult {
712 let slice_params = SliceParams {
713 layer_height: params.layer_height,
714 ..Default::default()
715 };
716
717 let slice_result = slice_mesh(mesh, &slice_params);
718
719 let mut thin_wall_layers = Vec::new();
720 let mut small_feature_layers = Vec::new();
721 let gap_issues = Vec::new(); for layer in &slice_result.layers {
724 for contour in &layer.contours {
726 if contour.perimeter > 0.0 {
731 let approx_thickness = 2.0 * contour.area / contour.perimeter;
732 if approx_thickness < params.min_wall_thickness && approx_thickness > 0.0 {
733 thin_wall_layers.push(ThinWallIssue {
734 layer_index: layer.index,
735 z_height: layer.z_height,
736 min_thickness: approx_thickness,
737 required_thickness: params.min_wall_thickness,
738 location: (contour.centroid.x, contour.centroid.y),
739 });
740 }
741 }
742
743 let feature_size = (contour.area).sqrt(); if feature_size < params.min_feature_size && feature_size > 0.0 {
746 small_feature_layers.push(SmallFeatureIssue {
747 layer_index: layer.index,
748 z_height: layer.z_height,
749 feature_size,
750 min_size: params.min_feature_size,
751 });
752 }
753 }
754 }
755
756 let issue_count = thin_wall_layers.len() + small_feature_layers.len() + gap_issues.len();
757 let is_valid = issue_count == 0;
758
759 let summary = if is_valid {
760 "Mesh passes all FDM validation checks.".to_string()
761 } else {
762 format!(
763 "Found {} issues: {} thin walls, {} small features, {} gaps",
764 issue_count,
765 thin_wall_layers.len(),
766 small_feature_layers.len(),
767 gap_issues.len()
768 )
769 };
770
771 FdmValidationResult {
772 is_valid,
773 thin_wall_layers,
774 small_feature_layers,
775 gap_issues,
776 issue_count,
777 summary,
778 }
779}
780
781#[derive(Debug, Clone)]
787pub struct SlaParams {
788 pub xy_resolution: f64,
790
791 pub layer_height: f64,
793
794 pub min_wall_thickness: f64,
796
797 pub min_feature_size: f64,
799
800 pub min_drain_hole: f64,
802
803 pub max_unsupported_span: f64,
805}
806
807impl Default for SlaParams {
808 fn default() -> Self {
809 Self {
810 xy_resolution: 0.05,
811 layer_height: 0.05,
812 min_wall_thickness: 0.4,
813 min_feature_size: 0.2,
814 min_drain_hole: 2.0,
815 max_unsupported_span: 5.0,
816 }
817 }
818}
819
820impl SlaParams {
821 pub fn high_detail() -> Self {
823 Self {
824 xy_resolution: 0.025,
825 layer_height: 0.025,
826 min_wall_thickness: 0.3,
827 min_feature_size: 0.15,
828 ..Default::default()
829 }
830 }
831
832 pub fn standard() -> Self {
834 Self::default()
835 }
836
837 pub fn draft() -> Self {
839 Self {
840 xy_resolution: 0.1,
841 layer_height: 0.1,
842 min_wall_thickness: 0.6,
843 min_feature_size: 0.4,
844 ..Default::default()
845 }
846 }
847}
848
849#[derive(Debug, Clone)]
851pub struct SlaValidationResult {
852 pub is_valid: bool,
854
855 pub thin_wall_layers: Vec<ThinWallIssue>,
857
858 pub small_feature_layers: Vec<SmallFeatureIssue>,
860
861 pub needs_drain_holes: bool,
863
864 pub issue_count: usize,
866
867 pub summary: String,
869}
870
871impl SlaValidationResult {
872 pub fn is_printable(&self) -> bool {
874 self.thin_wall_layers.is_empty()
875 }
876}
877
878pub fn validate_for_sla(mesh: &Mesh, params: &SlaParams) -> SlaValidationResult {
880 let slice_params = SliceParams::for_sla();
881 let slice_result = slice_mesh(mesh, &slice_params);
882
883 let mut thin_wall_layers = Vec::new();
884 let mut small_feature_layers = Vec::new();
885
886 for layer in &slice_result.layers {
887 for contour in &layer.contours {
888 if contour.perimeter > 0.0 {
890 let approx_thickness = 2.0 * contour.area / contour.perimeter;
891 if approx_thickness < params.min_wall_thickness && approx_thickness > 0.0 {
892 thin_wall_layers.push(ThinWallIssue {
893 layer_index: layer.index,
894 z_height: layer.z_height,
895 min_thickness: approx_thickness,
896 required_thickness: params.min_wall_thickness,
897 location: (contour.centroid.x, contour.centroid.y),
898 });
899 }
900 }
901
902 let feature_size = (contour.area).sqrt();
904 if feature_size < params.min_feature_size && feature_size > 0.0 {
905 small_feature_layers.push(SmallFeatureIssue {
906 layer_index: layer.index,
907 z_height: layer.z_height,
908 feature_size,
909 min_size: params.min_feature_size,
910 });
911 }
912 }
913 }
914
915 let needs_drain_holes = slice_result
918 .layers
919 .iter()
920 .any(|l| l.contours.iter().any(|c| !c.is_outer));
921
922 let issue_count = thin_wall_layers.len() + small_feature_layers.len();
923 let is_valid = issue_count == 0;
924
925 let summary = if is_valid {
926 if needs_drain_holes {
927 "Mesh passes SLA checks but may need drain holes for hollow sections.".to_string()
928 } else {
929 "Mesh passes all SLA validation checks.".to_string()
930 }
931 } else {
932 format!(
933 "Found {} issues: {} thin walls, {} small features{}",
934 issue_count,
935 thin_wall_layers.len(),
936 small_feature_layers.len(),
937 if needs_drain_holes {
938 " (also needs drain holes)"
939 } else {
940 ""
941 }
942 )
943 };
944
945 SlaValidationResult {
946 is_valid,
947 thin_wall_layers,
948 small_feature_layers,
949 needs_drain_holes,
950 issue_count,
951 summary,
952 }
953}
954
955#[derive(Debug, Clone)]
961pub struct SvgExportParams {
962 pub width: u32,
964 pub height: u32,
966 pub padding: u32,
968 pub stroke_width: f64,
970 pub fill_color: String,
972 pub stroke_color: String,
974 pub background_color: String,
976 pub fill_outer: bool,
978 pub show_holes: bool,
980}
981
982impl Default for SvgExportParams {
983 fn default() -> Self {
984 Self {
985 width: 800,
986 height: 600,
987 padding: 20,
988 stroke_width: 1.0,
989 fill_color: "#4a90d9".to_string(),
990 stroke_color: "#2d5986".to_string(),
991 background_color: "#f5f5f5".to_string(),
992 fill_outer: true,
993 show_holes: true,
994 }
995 }
996}
997
998pub fn export_layer_svg(layer: &Layer, params: &SvgExportParams) -> String {
1000 if layer.contours.is_empty() {
1001 return format!(
1002 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"0 0 {} {}\">\n\
1003 <rect width=\"100%\" height=\"100%\" fill=\"{}\"/>\n\
1004 <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" fill=\"#999\">Empty layer</text>\n\
1005</svg>",
1006 params.width, params.height, params.width, params.height, params.background_color
1007 );
1008 }
1009
1010 let bounds = &layer.bounds;
1012 let content_width = bounds.width();
1013 let content_height = bounds.height();
1014
1015 let available_width = params.width as f64 - 2.0 * params.padding as f64;
1016 let available_height = params.height as f64 - 2.0 * params.padding as f64;
1017
1018 let scale = if content_width > 0.0 && content_height > 0.0 {
1019 (available_width / content_width).min(available_height / content_height)
1020 } else {
1021 1.0
1022 };
1023
1024 let offset_x = params.padding as f64 + (available_width - content_width * scale) / 2.0;
1025 let offset_y = params.padding as f64 + (available_height - content_height * scale) / 2.0;
1026
1027 let mut svg = format!(
1028 r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">
1029 <rect width="100%" height="100%" fill="{}"/>
1030 <g transform="translate({:.2},{:.2}) scale({:.6},{:.6})">
1031"#,
1032 params.width,
1033 params.height,
1034 params.width,
1035 params.height,
1036 params.background_color,
1037 offset_x - bounds.min_x * scale,
1038 offset_y + (bounds.max_y) * scale, scale,
1040 -scale );
1042
1043 for contour in &layer.contours {
1045 if contour.points.is_empty() {
1046 continue;
1047 }
1048
1049 if !contour.is_outer && !params.show_holes {
1050 continue;
1051 }
1052
1053 let mut path = String::new();
1054 for (i, point) in contour.points.iter().enumerate() {
1055 if i == 0 {
1056 path.push_str(&format!("M {:.4} {:.4}", point.x, point.y));
1057 } else {
1058 path.push_str(&format!(" L {:.4} {:.4}", point.x, point.y));
1059 }
1060 }
1061 path.push_str(" Z");
1062
1063 let fill = if params.fill_outer && contour.is_outer {
1064 ¶ms.fill_color
1065 } else if !contour.is_outer {
1066 ¶ms.background_color } else {
1068 "none"
1069 };
1070
1071 svg.push_str(&format!(
1072 r#" <path d="{}" fill="{}" stroke="{}" stroke-width="{:.2}"/>
1073"#,
1074 path,
1075 fill,
1076 params.stroke_color,
1077 params.stroke_width / scale
1078 ));
1079 }
1080
1081 svg.push_str(" </g>\n");
1082
1083 svg.push_str(&format!(
1085 " <text x=\"10\" y=\"20\" font-family=\"monospace\" font-size=\"12\" fill=\"#666\">\n\
1086 Layer {}: Z={:.2}mm, Area={:.1}mm², Perimeter={:.1}mm\n\
1087 </text>\n",
1088 layer.index, layer.z_height, layer.area, layer.perimeter
1089 ));
1090
1091 svg.push_str("</svg>");
1092
1093 svg
1094}
1095
1096pub fn export_slices_svg(
1098 result: &SliceResult,
1099 output_dir: &std::path::Path,
1100 params: &SvgExportParams,
1101) -> crate::MeshResult<Vec<std::path::PathBuf>> {
1102 use std::fs;
1103 use std::io::Write;
1104
1105 fs::create_dir_all(output_dir).map_err(|e| crate::MeshError::IoWrite {
1106 path: output_dir.to_path_buf(),
1107 source: e,
1108 })?;
1109
1110 let mut paths = Vec::with_capacity(result.layers.len());
1111
1112 for layer in &result.layers {
1113 let svg = export_layer_svg(layer, params);
1114 let filename = format!("layer_{:04}.svg", layer.index);
1115 let path = output_dir.join(&filename);
1116
1117 let mut file = fs::File::create(&path).map_err(|e| crate::MeshError::IoWrite {
1118 path: path.clone(),
1119 source: e,
1120 })?;
1121
1122 file.write_all(svg.as_bytes())
1123 .map_err(|e| crate::MeshError::IoWrite {
1124 path: path.clone(),
1125 source: e,
1126 })?;
1127
1128 paths.push(path);
1129 }
1130
1131 Ok(paths)
1132}
1133
1134pub fn export_3mf_slices(
1143 result: &SliceResult,
1144 output_path: &std::path::Path,
1145) -> crate::MeshResult<()> {
1146 use std::fs::File;
1147 use std::io::Write;
1148 use zip::ZipWriter;
1149 use zip::write::SimpleFileOptions;
1150
1151 let file = File::create(output_path).map_err(|e| crate::MeshError::IoWrite {
1152 path: output_path.to_path_buf(),
1153 source: e,
1154 })?;
1155
1156 let mut zip = ZipWriter::new(file);
1157 let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
1158
1159 zip.start_file("[Content_Types].xml", options)
1161 .map_err(|e| crate::MeshError::IoWrite {
1162 path: output_path.to_path_buf(),
1163 source: std::io::Error::other(e.to_string()),
1164 })?;
1165 zip.write_all(SLICE_CONTENT_TYPES_XML.as_bytes())
1166 .map_err(|e| crate::MeshError::IoWrite {
1167 path: output_path.to_path_buf(),
1168 source: e,
1169 })?;
1170
1171 zip.start_file("_rels/.rels", options)
1173 .map_err(|e| crate::MeshError::IoWrite {
1174 path: output_path.to_path_buf(),
1175 source: std::io::Error::other(e.to_string()),
1176 })?;
1177 zip.write_all(SLICE_RELS_XML.as_bytes())
1178 .map_err(|e| crate::MeshError::IoWrite {
1179 path: output_path.to_path_buf(),
1180 source: e,
1181 })?;
1182
1183 zip.start_file("2D/2dmodel.model", options)
1185 .map_err(|e| crate::MeshError::IoWrite {
1186 path: output_path.to_path_buf(),
1187 source: std::io::Error::other(e.to_string()),
1188 })?;
1189
1190 let slice_xml = generate_slice_stack_xml(result);
1191 zip.write_all(slice_xml.as_bytes())
1192 .map_err(|e| crate::MeshError::IoWrite {
1193 path: output_path.to_path_buf(),
1194 source: e,
1195 })?;
1196
1197 zip.finish().map_err(|e| crate::MeshError::IoWrite {
1198 path: output_path.to_path_buf(),
1199 source: std::io::Error::other(e.to_string()),
1200 })?;
1201
1202 Ok(())
1203}
1204
1205fn generate_slice_stack_xml(result: &SliceResult) -> String {
1206 let mut xml = String::with_capacity(result.layers.len() * 500);
1207
1208 xml.push_str(
1209 r#"<?xml version="1.0" encoding="UTF-8"?>
1210<slicestack xmlns="http://schemas.microsoft.com/3dmanufacturing/slice/2015/07"
1211 zbottom="0">
1212"#,
1213 );
1214
1215 for layer in &result.layers {
1216 xml.push_str(&format!(" <slice ztop=\"{:.6}\">\n", layer.z_height));
1217
1218 for (contour_idx, contour) in layer.contours.iter().enumerate() {
1219 if contour.points.is_empty() {
1220 continue;
1221 }
1222
1223 let vertices: String = contour
1225 .points
1226 .iter()
1227 .map(|p| format!("{:.4} {:.4}", p.x, p.y))
1228 .collect::<Vec<_>>()
1229 .join(" ");
1230
1231 let indices: String = (0..contour.points.len())
1233 .map(|i| i.to_string())
1234 .collect::<Vec<_>>()
1235 .join(" ");
1236
1237 xml.push_str(&format!(
1238 " <vertices id=\"{}\">{}</vertices>\n",
1239 contour_idx, vertices
1240 ));
1241 xml.push_str(&format!(
1242 " <polygon startv=\"0\">{}</polygon>\n",
1243 indices
1244 ));
1245 }
1246
1247 xml.push_str(" </slice>\n");
1248 }
1249
1250 xml.push_str("</slicestack>\n");
1251 xml
1252}
1253
1254const SLICE_CONTENT_TYPES_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1255<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
1256 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
1257 <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
1258</Types>
1259"#;
1260
1261const SLICE_RELS_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1262<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
1263 <Relationship Target="/2D/2dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/slice"/>
1264</Relationships>
1265"#;
1266
1267impl Mesh {
1272 pub fn slice(&self) -> SliceResult {
1274 slice_mesh(self, &SliceParams::default())
1275 }
1276
1277 pub fn slice_with_params(&self, params: &SliceParams) -> SliceResult {
1279 slice_mesh(self, params)
1280 }
1281
1282 pub fn slice_preview(&self, z: f64) -> Layer {
1284 slice_preview(self, z, &SliceParams::default())
1285 }
1286
1287 pub fn slice_preview_with_params(&self, z: f64, params: &SliceParams) -> Layer {
1289 slice_preview(self, z, params)
1290 }
1291
1292 pub fn validate_for_fdm(&self, params: &FdmParams) -> FdmValidationResult {
1318 validate_for_fdm(self, params)
1319 }
1320
1321 pub fn validate_for_sla(&self, params: &SlaParams) -> SlaValidationResult {
1326 validate_for_sla(self, params)
1327 }
1328
1329 pub fn export_slices_svg(
1331 &self,
1332 output_dir: &std::path::Path,
1333 params: &SvgExportParams,
1334 ) -> crate::MeshResult<Vec<std::path::PathBuf>> {
1335 let result = self.slice();
1336 export_slices_svg(&result, output_dir, params)
1337 }
1338}
1339
1340#[cfg(test)]
1341mod tests {
1342 use super::*;
1343 use crate::Vertex;
1344
1345 fn create_test_cube() -> Mesh {
1346 let mut mesh = Mesh::new();
1347 let vertices = [
1349 (0.0, 0.0, 0.0),
1350 (10.0, 0.0, 0.0),
1351 (10.0, 10.0, 0.0),
1352 (0.0, 10.0, 0.0),
1353 (0.0, 0.0, 10.0),
1354 (10.0, 0.0, 10.0),
1355 (10.0, 10.0, 10.0),
1356 (0.0, 10.0, 10.0),
1357 ];
1358
1359 for (x, y, z) in vertices {
1360 mesh.vertices.push(Vertex::from_coords(x, y, z));
1361 }
1362
1363 let faces = [
1365 [0, 1, 2],
1367 [0, 2, 3],
1368 [4, 6, 5],
1370 [4, 7, 6],
1371 [0, 5, 1],
1373 [0, 4, 5],
1374 [2, 7, 3],
1376 [2, 6, 7],
1377 [0, 3, 7],
1379 [0, 7, 4],
1380 [1, 5, 6],
1382 [1, 6, 2],
1383 ];
1384
1385 for f in faces {
1386 mesh.faces.push(f);
1387 }
1388
1389 mesh
1390 }
1391
1392 #[test]
1393 fn test_slice_params_default() {
1394 let params = SliceParams::default();
1395 assert!((params.layer_height - 0.2).abs() < 0.001);
1396 assert_eq!(params.perimeters, 2);
1397 }
1398
1399 #[test]
1400 fn test_slice_params_presets() {
1401 let hq = SliceParams::high_quality();
1402 assert!(hq.layer_height < 0.2);
1403
1404 let draft = SliceParams::draft();
1405 assert!(draft.layer_height > 0.2);
1406
1407 let sla = SliceParams::for_sla();
1408 assert!((sla.infill_density - 1.0).abs() < 0.001);
1409 }
1410
1411 #[test]
1412 fn test_slice_mesh() {
1413 let mesh = create_test_cube();
1414 let result = slice_mesh(&mesh, &SliceParams::default());
1415
1416 assert!(result.layer_count > 0);
1419 assert!((result.total_height - 10.0).abs() < 0.1);
1420 }
1421
1422 #[test]
1423 fn test_slice_preview() {
1424 let mesh = create_test_cube();
1425 let layer = slice_preview(&mesh, 5.0, &SliceParams::default());
1426
1427 assert!(layer.area > 0.0);
1429 assert!(layer.perimeter > 0.0);
1430 }
1431
1432 #[test]
1433 fn test_layer_stats() {
1434 let mesh = create_test_cube();
1435 let result = slice_mesh(&mesh, &SliceParams::default());
1436 let stats = calculate_layer_stats(&result);
1437
1438 assert!(stats.max_area >= stats.min_area);
1440 assert!(stats.avg_area > 0.0);
1441 }
1442
1443 #[test]
1444 fn test_mesh_slice_method() {
1445 let mesh = create_test_cube();
1446 let result = mesh.slice();
1447
1448 assert!(result.layer_count > 0);
1449 assert!(result.estimated_print_time >= 0.0);
1450 }
1451
1452 #[test]
1453 fn test_empty_mesh_slice() {
1454 let mesh = Mesh::new();
1455 let result = slice_mesh(&mesh, &SliceParams::default());
1456
1457 assert_eq!(result.layer_count, 0);
1458 assert!((result.total_height - 0.0).abs() < 0.001);
1459 }
1460
1461 #[test]
1462 fn test_layer_bounds() {
1463 let mesh = create_test_cube();
1464 let layer = slice_preview(&mesh, 5.0, &SliceParams::default());
1465
1466 assert!(layer.bounds.width() <= 10.5);
1468 assert!(layer.bounds.height() <= 10.5);
1469 }
1470
1471 #[test]
1472 fn test_fdm_params_default() {
1473 let params = FdmParams::default();
1474 assert!((params.nozzle_diameter - 0.4).abs() < 0.001);
1475 assert!((params.min_wall_thickness - 0.8).abs() < 0.001);
1476 assert!((params.layer_height - 0.2).abs() < 0.001);
1477 }
1478
1479 #[test]
1480 fn test_fdm_validation_cube() {
1481 let mesh = create_test_cube();
1482 let params = FdmParams::default();
1483 let result = validate_for_fdm(&mesh, ¶ms);
1484
1485 assert!(result.thin_wall_layers.is_empty() || result.is_valid);
1488 }
1489
1490 #[test]
1491 fn test_fdm_validation_mesh_method() {
1492 let mesh = create_test_cube();
1493 let params = FdmParams::default();
1494 let result = mesh.validate_for_fdm(¶ms);
1495
1496 assert!(!result.summary.is_empty());
1497 }
1498
1499 #[test]
1500 fn test_sla_params_default() {
1501 let params = SlaParams::default();
1502 assert!((params.xy_resolution - 0.05).abs() < 0.001);
1503 assert!((params.layer_height - 0.05).abs() < 0.001);
1504 assert!((params.min_drain_hole - 2.0).abs() < 0.001);
1505 }
1506
1507 #[test]
1508 fn test_sla_validation_cube() {
1509 let mesh = create_test_cube();
1510 let params = SlaParams::default();
1511 let result = validate_for_sla(&mesh, ¶ms);
1512
1513 assert!(!result.summary.is_empty());
1515 }
1516
1517 #[test]
1518 fn test_sla_validation_mesh_method() {
1519 let mesh = create_test_cube();
1520 let params = SlaParams::default();
1521 let result = mesh.validate_for_sla(¶ms);
1522
1523 assert!(!result.summary.is_empty());
1524 }
1525
1526 #[test]
1527 fn test_svg_export_params_default() {
1528 let params = SvgExportParams::default();
1529 assert_eq!(params.width, 800);
1530 assert_eq!(params.height, 600);
1531 assert_eq!(params.padding, 20);
1532 assert_eq!(params.fill_color, "#4a90d9");
1533 assert_eq!(params.stroke_color, "#2d5986");
1534 assert_eq!(params.background_color, "#f5f5f5");
1535 assert!(params.fill_outer);
1536 assert!(params.show_holes);
1537 }
1538
1539 #[test]
1540 fn test_export_layer_svg_empty() {
1541 let layer = Layer {
1542 index: 0,
1543 z_height: 0.0,
1544 thickness: 0.2,
1545 contours: vec![],
1546 area: 0.0,
1547 perimeter: 0.0,
1548 print_time: 0.0,
1549 filament_length: 0.0,
1550 island_count: 0,
1551 bounds: LayerBounds {
1552 min_x: 0.0,
1553 max_x: 0.0,
1554 min_y: 0.0,
1555 max_y: 0.0,
1556 },
1557 };
1558 let params = SvgExportParams::default();
1559 let svg = export_layer_svg(&layer, ¶ms);
1560
1561 assert!(svg.contains("<svg"));
1562 assert!(svg.contains("Empty layer"));
1563 assert!(svg.contains("</svg>"));
1564 }
1565
1566 #[test]
1567 fn test_export_layer_svg_with_contour() {
1568 let mesh = create_test_cube();
1569 let layer = slice_preview(&mesh, 5.0, &SliceParams::default());
1570 let params = SvgExportParams::default();
1571 let svg = export_layer_svg(&layer, ¶ms);
1572
1573 assert!(svg.contains("<svg"));
1574 assert!(svg.contains("</svg>"));
1575 assert!(svg.contains("<path"));
1576 }
1577
1578 #[test]
1579 fn test_3mf_slice_xml() {
1580 let mesh = create_test_cube();
1581 let result = slice_mesh(&mesh, &SliceParams::default());
1582 let xml = generate_slice_stack_xml(&result);
1583
1584 assert!(xml.contains("<?xml"));
1585 assert!(xml.contains("slicestack"));
1586 assert!(xml.contains("<slice"));
1587 }
1588}