mesh_repair/
slice.rs

1//! Slicing and layer preview for 3D printing.
2//!
3//! This module provides tools for generating 2D slice previews of meshes,
4//! calculating per-layer statistics, and estimating print times.
5//!
6//! # Use Cases
7//!
8//! - Generate layer-by-layer preview of a 3D print
9//! - Calculate print time estimates
10//! - Analyze per-layer statistics (area, perimeter)
11//! - Validate slice-level printability
12//!
13//! # Example
14//!
15//! ```
16//! use mesh_repair::{Mesh, Vertex};
17//! use mesh_repair::slice::{slice_mesh, SliceParams};
18//!
19//! // Create a simple mesh
20//! let mut mesh = Mesh::new();
21//! mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
22//! mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
23//! mesh.vertices.push(Vertex::from_coords(5.0, 10.0, 0.0));
24//! mesh.vertices.push(Vertex::from_coords(5.0, 5.0, 10.0));
25//! mesh.faces.push([0, 1, 3]);
26//! mesh.faces.push([1, 2, 3]);
27//! mesh.faces.push([2, 0, 3]);
28//! mesh.faces.push([0, 2, 1]);
29//!
30//! // Generate slices
31//! let params = SliceParams::default();
32//! let result = slice_mesh(&mesh, &params);
33//!
34//! println!("Total layers: {}", result.layers.len());
35//! println!("Estimated print time: {:.1} minutes", result.estimated_print_time);
36//! ```
37
38use crate::Mesh;
39use crate::measure::{CrossSection, cross_section};
40use nalgebra::{Point3, Vector3};
41
42/// Parameters for slicing operations.
43#[derive(Debug, Clone)]
44pub struct SliceParams {
45    /// Layer height in mm.
46    pub layer_height: f64,
47
48    /// Print direction (typically Z-up).
49    pub direction: Vector3<f64>,
50
51    /// First layer height (often thicker for adhesion).
52    pub first_layer_height: f64,
53
54    /// Infill density (0.0-1.0).
55    pub infill_density: f64,
56
57    /// Number of perimeter shells.
58    pub perimeters: usize,
59
60    /// Perimeter width in mm (typically nozzle diameter).
61    pub perimeter_width: f64,
62
63    /// Print speed for perimeters in mm/s.
64    pub perimeter_speed: f64,
65
66    /// Print speed for infill in mm/s.
67    pub infill_speed: f64,
68
69    /// Travel speed in mm/s.
70    pub travel_speed: f64,
71
72    /// Extrusion width multiplier.
73    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    /// Parameters for high quality printing.
95    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    /// Parameters for fast draft printing.
108    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    /// Parameters for SLA/resin printing.
121    pub fn for_sla() -> Self {
122        Self {
123            layer_height: 0.05,
124            first_layer_height: 0.05,
125            infill_density: 1.0, // SLA typically prints solid
126            perimeters: 0,       // Not applicable for SLA
127            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/// Result of slicing operation.
137#[derive(Debug)]
138pub struct SliceResult {
139    /// Individual layers from bottom to top.
140    pub layers: Vec<Layer>,
141
142    /// Total height of the sliced object in mm.
143    pub total_height: f64,
144
145    /// Total number of layers.
146    pub layer_count: usize,
147
148    /// Estimated print time in minutes.
149    pub estimated_print_time: f64,
150
151    /// Estimated filament usage in mm.
152    pub estimated_filament_length: f64,
153
154    /// Estimated filament volume in mm³.
155    pub estimated_filament_volume: f64,
156
157    /// Layer with maximum area.
158    pub max_area_layer: usize,
159
160    /// Layer with maximum perimeter.
161    pub max_perimeter_layer: usize,
162
163    /// Slice parameters used.
164    pub params: SliceParams,
165}
166
167/// A single layer/slice of the mesh.
168#[derive(Debug, Clone)]
169pub struct Layer {
170    /// Layer index (0 = first layer).
171    pub index: usize,
172
173    /// Z height of this layer in mm.
174    pub z_height: f64,
175
176    /// Layer thickness in mm.
177    pub thickness: f64,
178
179    /// Cross-section contours for this layer.
180    pub contours: Vec<Contour>,
181
182    /// Total area of all contours in mm².
183    pub area: f64,
184
185    /// Total perimeter length in mm.
186    pub perimeter: f64,
187
188    /// Estimated print time for this layer in seconds.
189    pub print_time: f64,
190
191    /// Estimated filament length for this layer in mm.
192    pub filament_length: f64,
193
194    /// Number of separate islands (disconnected regions).
195    pub island_count: usize,
196
197    /// Bounding box of this layer (2D).
198    pub bounds: LayerBounds,
199}
200
201/// A contour (closed loop) in a layer.
202#[derive(Debug, Clone)]
203pub struct Contour {
204    /// Points defining the contour (closed loop).
205    pub points: Vec<Point3<f64>>,
206
207    /// Area enclosed by this contour.
208    pub area: f64,
209
210    /// Perimeter of this contour.
211    pub perimeter: f64,
212
213    /// Whether this is an outer contour (vs hole).
214    pub is_outer: bool,
215
216    /// Centroid of the contour.
217    pub centroid: Point3<f64>,
218}
219
220/// 2D bounding box for a layer.
221#[derive(Debug, Clone)]
222pub struct LayerBounds {
223    /// Minimum X coordinate.
224    pub min_x: f64,
225    /// Maximum X coordinate.
226    pub max_x: f64,
227    /// Minimum Y coordinate.
228    pub min_y: f64,
229    /// Maximum Y coordinate.
230    pub max_y: f64,
231}
232
233impl LayerBounds {
234    /// Width of the bounding box.
235    pub fn width(&self) -> f64 {
236        self.max_x - self.min_x
237    }
238
239    /// Height of the bounding box.
240    pub fn height(&self) -> f64 {
241        self.max_y - self.min_y
242    }
243
244    /// Center point of the bounding box.
245    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/// Layer statistics summary.
254#[derive(Debug, Clone)]
255pub struct LayerStats {
256    /// Minimum area across all layers.
257    pub min_area: f64,
258    /// Maximum area across all layers.
259    pub max_area: f64,
260    /// Average area.
261    pub avg_area: f64,
262    /// Minimum perimeter.
263    pub min_perimeter: f64,
264    /// Maximum perimeter.
265    pub max_perimeter: f64,
266    /// Average perimeter.
267    pub avg_perimeter: f64,
268    /// Maximum island count.
269    pub max_islands: usize,
270}
271
272// ============================================================================
273// Main slicing functions
274// ============================================================================
275
276/// Slice a mesh into layers for 3D printing preview.
277pub fn slice_mesh(mesh: &Mesh, params: &SliceParams) -> SliceResult {
278    // Find Z bounds
279    let (min_z, max_z) = find_z_bounds(mesh, &params.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    // Calculate layer heights
297    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    // Generate layers
309    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    // Calculate filament volume (assuming 1.75mm diameter)
336    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, // Convert to minutes
345        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
353/// Generate a slice preview at a specific height.
354pub fn slice_preview(mesh: &Mesh, z: f64, params: &SliceParams) -> Layer {
355    generate_layer(mesh, 0, z, params.layer_height, params)
356}
357
358/// Calculate layer statistics for all layers.
359pub 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
402// ============================================================================
403// Internal helper functions
404// ============================================================================
405
406fn 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    // Get cross-section at this Z height
432    let plane_point = Point3::new(0.0, 0.0, z);
433    let section = cross_section(mesh, plane_point, params.direction);
434
435    // Convert cross-section to contours
436    let contours = extract_contours(&section);
437    let island_count = contours.iter().filter(|c| c.is_outer).count();
438
439    // Calculate bounds
440    let bounds = calculate_layer_bounds(&contours);
441
442    // Calculate print time estimate
443    let print_time = estimate_layer_print_time(&contours, params);
444
445    // Calculate filament usage
446    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    // For simplicity, treat the entire cross-section as one contour
468    // In a full implementation, we would separate inner/outer contours
469    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        // Perimeter time
522        let perimeter_passes = params.perimeters as f64;
523        let perimeter_length = contour.perimeter * perimeter_passes;
524        time += perimeter_length / params.perimeter_speed;
525
526        // Infill time (simplified: assume infill is proportional to area)
527        if params.infill_density > 0.0 {
528            // Approximate infill path length based on area and density
529            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    // Add travel time (rough estimate: 10% of print time)
536    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        // Perimeter volume
550        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        // Infill volume
556        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    // Convert volume to filament length (assuming 1.75mm diameter)
565    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// ============================================================================
571// FDM Validation
572// ============================================================================
573
574/// Parameters for FDM (Fused Deposition Modeling) validation.
575#[derive(Debug, Clone)]
576pub struct FdmParams {
577    /// Nozzle diameter in mm.
578    pub nozzle_diameter: f64,
579
580    /// Minimum wall thickness (typically 2x nozzle diameter).
581    pub min_wall_thickness: f64,
582
583    /// Layer height in mm.
584    pub layer_height: f64,
585
586    /// Minimum feature size (typically nozzle diameter).
587    pub min_feature_size: f64,
588
589    /// Maximum overhang angle in degrees (0 = vertical, 90 = horizontal).
590    pub max_overhang_angle: f64,
591
592    /// Minimum gap between features.
593    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, // 2x nozzle
601            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    /// Parameters for a 0.4mm nozzle (most common).
611    pub fn nozzle_04() -> Self {
612        Self::default()
613    }
614
615    /// Parameters for a 0.6mm nozzle.
616    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    /// Parameters for a 0.25mm nozzle (fine detail).
628    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/// Result of FDM validation.
641#[derive(Debug, Clone)]
642pub struct FdmValidationResult {
643    /// Whether the mesh passes all FDM checks.
644    pub is_valid: bool,
645
646    /// Layers with thin walls below minimum.
647    pub thin_wall_layers: Vec<ThinWallIssue>,
648
649    /// Layers with features smaller than nozzle.
650    pub small_feature_layers: Vec<SmallFeatureIssue>,
651
652    /// Layers with gap issues.
653    pub gap_issues: Vec<GapIssue>,
654
655    /// Total number of issues found.
656    pub issue_count: usize,
657
658    /// Summary message.
659    pub summary: String,
660}
661
662impl FdmValidationResult {
663    /// Check if the mesh is printable (may have warnings but no critical issues).
664    pub fn is_printable(&self) -> bool {
665        self.thin_wall_layers.is_empty()
666    }
667}
668
669/// A thin wall issue at a specific layer.
670#[derive(Debug, Clone)]
671pub struct ThinWallIssue {
672    /// Layer index.
673    pub layer_index: usize,
674    /// Z height.
675    pub z_height: f64,
676    /// Minimum wall thickness found.
677    pub min_thickness: f64,
678    /// Required minimum thickness.
679    pub required_thickness: f64,
680    /// Approximate location (centroid of thin region).
681    pub location: (f64, f64),
682}
683
684/// A small feature issue at a specific layer.
685#[derive(Debug, Clone)]
686pub struct SmallFeatureIssue {
687    /// Layer index.
688    pub layer_index: usize,
689    /// Z height.
690    pub z_height: f64,
691    /// Feature size found.
692    pub feature_size: f64,
693    /// Minimum feature size allowed.
694    pub min_size: f64,
695}
696
697/// A gap issue between features.
698#[derive(Debug, Clone)]
699pub struct GapIssue {
700    /// Layer index.
701    pub layer_index: usize,
702    /// Z height.
703    pub z_height: f64,
704    /// Gap size found.
705    pub gap_size: f64,
706    /// Minimum gap allowed.
707    pub min_gap: f64,
708}
709
710/// Validate a mesh for FDM printing.
711pub 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(); // Gap detection is complex; simplified for now
722
723    for layer in &slice_result.layers {
724        // Check for thin walls by analyzing contour widths
725        for contour in &layer.contours {
726            // Approximate wall thickness using bounding box vs perimeter
727            // For a rectangular region: perimeter = 2*(w+h), area = w*h
728            // If we assume square-ish: perimeter ≈ 4*sqrt(area), so sqrt(area) ≈ perimeter/4
729            // Wall thickness is approximately area / (perimeter/2) for thin strips
730            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            // Check for small features (islands smaller than min feature size)
744            let feature_size = (contour.area).sqrt(); // Approximate feature dimension
745            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// ============================================================================
782// SLA Validation
783// ============================================================================
784
785/// Parameters for SLA (Stereolithography) validation.
786#[derive(Debug, Clone)]
787pub struct SlaParams {
788    /// XY resolution (pixel size) in mm.
789    pub xy_resolution: f64,
790
791    /// Layer height (Z resolution) in mm.
792    pub layer_height: f64,
793
794    /// Minimum wall thickness.
795    pub min_wall_thickness: f64,
796
797    /// Minimum feature size.
798    pub min_feature_size: f64,
799
800    /// Minimum hole diameter for drainage.
801    pub min_drain_hole: f64,
802
803    /// Maximum unsupported span in mm.
804    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    /// Parameters for high-detail resin printing.
822    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    /// Parameters for standard resin printing.
833    pub fn standard() -> Self {
834        Self::default()
835    }
836
837    /// Parameters for fast/draft resin printing.
838    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/// Result of SLA validation.
850#[derive(Debug, Clone)]
851pub struct SlaValidationResult {
852    /// Whether the mesh passes all SLA checks.
853    pub is_valid: bool,
854
855    /// Layers with thin walls.
856    pub thin_wall_layers: Vec<ThinWallIssue>,
857
858    /// Layers with small features.
859    pub small_feature_layers: Vec<SmallFeatureIssue>,
860
861    /// Whether mesh appears to be hollow and might need drain holes.
862    pub needs_drain_holes: bool,
863
864    /// Total number of issues found.
865    pub issue_count: usize,
866
867    /// Summary message.
868    pub summary: String,
869}
870
871impl SlaValidationResult {
872    /// Check if the mesh is printable.
873    pub fn is_printable(&self) -> bool {
874        self.thin_wall_layers.is_empty()
875    }
876}
877
878/// Validate a mesh for SLA printing.
879pub 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            // Check for thin walls
889            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            // Check for small features
903            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    // Check if mesh might need drain holes (hollow object detection)
916    // Simple heuristic: if there are internal contours (holes), might need drainage
917    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// ============================================================================
956// SVG Export for Visualization
957// ============================================================================
958
959/// Parameters for SVG export.
960#[derive(Debug, Clone)]
961pub struct SvgExportParams {
962    /// Width of the SVG in pixels.
963    pub width: u32,
964    /// Height of the SVG in pixels.
965    pub height: u32,
966    /// Padding around the content in pixels.
967    pub padding: u32,
968    /// Stroke width for contours.
969    pub stroke_width: f64,
970    /// Fill color for solid regions (CSS color string).
971    pub fill_color: String,
972    /// Stroke color for contours.
973    pub stroke_color: String,
974    /// Background color.
975    pub background_color: String,
976    /// Whether to show outer contours filled.
977    pub fill_outer: bool,
978    /// Whether to show hole contours.
979    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
998/// Export a single layer to SVG format.
999pub 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    // Calculate bounds and scale
1011    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, // SVG Y is inverted
1039        scale,
1040        -scale // Flip Y axis
1041    );
1042
1043    // Draw contours
1044    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            &params.fill_color
1065        } else if !contour.is_outer {
1066            &params.background_color // Holes cut out
1067        } 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    // Add layer info text
1084    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
1096/// Export all layers to a series of SVG files.
1097pub 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
1134// ============================================================================
1135// 3MF Slice Extension Export
1136// ============================================================================
1137
1138/// Export slices in 3MF slice extension format.
1139///
1140/// The 3MF slice extension allows pre-sliced data to be embedded in 3MF files,
1141/// which can be read by compatible slicers for faster processing.
1142pub 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    // Write content types
1160    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    // Write relationships
1172    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    // Write slice stack
1184    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            // Build vertices string
1224            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            // Build polygon indices (simple sequential for closed loop)
1232            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
1267// ============================================================================
1268// Mesh extension methods
1269// ============================================================================
1270
1271impl Mesh {
1272    /// Slice this mesh into layers for 3D printing.
1273    pub fn slice(&self) -> SliceResult {
1274        slice_mesh(self, &SliceParams::default())
1275    }
1276
1277    /// Slice with custom parameters.
1278    pub fn slice_with_params(&self, params: &SliceParams) -> SliceResult {
1279        slice_mesh(self, params)
1280    }
1281
1282    /// Generate a slice preview at a specific height.
1283    pub fn slice_preview(&self, z: f64) -> Layer {
1284        slice_preview(self, z, &SliceParams::default())
1285    }
1286
1287    /// Generate a slice preview with custom parameters.
1288    pub fn slice_preview_with_params(&self, z: f64, params: &SliceParams) -> Layer {
1289        slice_preview(self, z, params)
1290    }
1291
1292    /// Validate mesh for FDM printing.
1293    ///
1294    /// Checks for thin walls, small features, and other issues that would
1295    /// cause problems with FDM 3D printing.
1296    ///
1297    /// # Example
1298    /// ```
1299    /// use mesh_repair::{Mesh, Vertex};
1300    /// use mesh_repair::slice::FdmParams;
1301    ///
1302    /// let mut mesh = Mesh::new();
1303    /// mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
1304    /// mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
1305    /// mesh.vertices.push(Vertex::from_coords(5.0, 10.0, 0.0));
1306    /// mesh.vertices.push(Vertex::from_coords(5.0, 5.0, 10.0));
1307    /// mesh.faces.push([0, 1, 3]);
1308    /// mesh.faces.push([1, 2, 3]);
1309    /// mesh.faces.push([2, 0, 3]);
1310    /// mesh.faces.push([0, 2, 1]);
1311    ///
1312    /// let result = mesh.validate_for_fdm(&FdmParams::default());
1313    /// if result.is_printable() {
1314    ///     println!("Mesh is ready for FDM printing!");
1315    /// }
1316    /// ```
1317    pub fn validate_for_fdm(&self, params: &FdmParams) -> FdmValidationResult {
1318        validate_for_fdm(self, params)
1319    }
1320
1321    /// Validate mesh for SLA printing.
1322    ///
1323    /// Checks for thin walls, small features, and identifies if drain holes
1324    /// might be needed for hollow sections.
1325    pub fn validate_for_sla(&self, params: &SlaParams) -> SlaValidationResult {
1326        validate_for_sla(self, params)
1327    }
1328
1329    /// Export slices to SVG files for visualization.
1330    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        // Create a 10x10x10 cube
1348        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        // 12 triangles for 6 faces
1364        let faces = [
1365            // Bottom
1366            [0, 1, 2],
1367            [0, 2, 3],
1368            // Top
1369            [4, 6, 5],
1370            [4, 7, 6],
1371            // Front
1372            [0, 5, 1],
1373            [0, 4, 5],
1374            // Back
1375            [2, 7, 3],
1376            [2, 6, 7],
1377            // Left
1378            [0, 3, 7],
1379            [0, 7, 4],
1380            // Right
1381            [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        // 10mm height with 0.3mm first layer and 0.2mm layers
1417        // Should have multiple layers
1418        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        // At z=5, should intersect the cube
1428        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        // For a cube, all internal layers should have similar areas
1439        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        // Bounds should be reasonable for a 10x10 cube
1467        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, &params);
1484
1485        // A 10x10x10 cube should pass basic FDM validation
1486        // Wall thickness is 10mm which is well above minimum
1487        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(&params);
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, &params);
1512
1513        // A 10x10x10 cube should pass basic SLA validation
1514        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(&params);
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, &params);
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, &params);
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}