Skip to main content

lib3mf_core/writer/
slice_writer.rs

1use crate::error::{Lib3mfError, Result};
2use crate::model::{Polygon, Segment, Slice, SliceRef, SliceStack, Vertex2D};
3use crate::writer::xml_writer::XmlWriter;
4use std::io::Write;
5
6/// Controls how slice data is written to the 3MF archive.
7///
8/// - `PreserveOriginal`: Writes inline slices as `<slice>` elements and slice
9///   references as `<sliceref>` elements, preserving the original structure
10///   as parsed. This is the only mode fully implemented in Phase 13.
11/// - `Inline`: Would convert all external slicerefs to inline slices.
12///   Not yet implemented.
13/// - `External`: Would move inline slices to external files with slicerefs.
14///   Not yet implemented.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum SliceMode {
17    /// Write slices inline as `<slice>` elements and references as `<sliceref>` elements (default).
18    #[default]
19    PreserveOriginal,
20    /// Convert all external slice references to inline `<slice>` elements (not yet implemented).
21    Inline,
22    /// Move inline slices to external files with `<sliceref>` elements (not yet implemented).
23    External,
24}
25
26/// Options controlling slice extension serialization behavior.
27pub struct SliceWriteOptions {
28    /// How slice data is laid out in the archive.
29    pub slice_mode: SliceMode,
30    /// When true, empty slice stacks cause an error instead of a warning.
31    pub strict: bool,
32}
33
34impl Default for SliceWriteOptions {
35    fn default() -> Self {
36        Self {
37            slice_mode: SliceMode::PreserveOriginal,
38            strict: false,
39        }
40    }
41}
42
43/// Writes a `<slicestack>` element with its child slices and slicerefs.
44///
45/// Element names are unqualified (no namespace prefix) because the slice
46/// namespace URI is declared on the root model element as `xmlns:s`.
47///
48/// # Degenerate handling
49///
50/// - Empty slice stacks (zero slices AND zero refs): emits a warning and writes
51///   an empty `<slicestack>` element. If `opts.strict` is true, returns an error.
52/// - Orphan vertices (vertices present but no polygons in a slice): skips the
53///   entire slice with a warning.
54/// - Degenerate polygons (zero segments): emits a warning but writes faithfully.
55pub fn write_slice_stack<W: Write>(
56    writer: &mut XmlWriter<W>,
57    stack: &SliceStack,
58    opts: &SliceWriteOptions,
59) -> Result<()> {
60    // Check slice mode first
61    match opts.slice_mode {
62        SliceMode::PreserveOriginal => {}
63        SliceMode::Inline => {
64            return Err(Lib3mfError::Validation(
65                "SliceMode::Inline is not yet implemented".to_string(),
66            ));
67        }
68        SliceMode::External => {
69            return Err(Lib3mfError::Validation(
70                "SliceMode::External is not yet implemented".to_string(),
71            ));
72        }
73    }
74
75    // Warn on empty slice stacks
76    if stack.slices.is_empty() && stack.refs.is_empty() {
77        eprintln!(
78            "Warning: slice stack id={} has no slices and no slicerefs",
79            stack.id.0
80        );
81        if opts.strict {
82            return Err(Lib3mfError::Validation(format!(
83                "Empty slice stack id={}",
84                stack.id.0
85            )));
86        }
87    }
88
89    writer
90        .start_element("slicestack")
91        .attr("id", &stack.id.0.to_string())
92        .attr("zbottom", &stack.z_bottom.to_string())
93        .write_start()?;
94
95    // Write inline slices first
96    for slice in &stack.slices {
97        write_slice(writer, slice)?;
98    }
99
100    // Write slicerefs after inline slices
101    for sref in &stack.refs {
102        write_sliceref(writer, sref)?;
103    }
104
105    writer.end_element("slicestack")?;
106    Ok(())
107}
108
109/// Writes a single `<slice>` element with its vertices, polygons, and segments.
110///
111/// Skips the entire slice (with a warning) if it has vertices but no polygons
112/// (orphan vertices).
113fn write_slice<W: Write>(writer: &mut XmlWriter<W>, slice: &Slice) -> Result<()> {
114    // Orphan vertices: skip the slice entirely
115    if !slice.vertices.is_empty() && slice.polygons.is_empty() {
116        eprintln!(
117            "Warning: slice ztop={} has {} vertices but no polygons, skipping",
118            slice.z_top,
119            slice.vertices.len()
120        );
121        return Ok(());
122    }
123
124    writer
125        .start_element("slice")
126        .attr("ztop", &slice.z_top.to_string())
127        .write_start()?;
128
129    // Write vertices section (only if there are vertices)
130    if !slice.vertices.is_empty() {
131        writer.start_element("vertices").write_start()?;
132        for v in &slice.vertices {
133            write_vertex(writer, v)?;
134        }
135        writer.end_element("vertices")?;
136    }
137
138    // Write polygons
139    for polygon in &slice.polygons {
140        write_polygon(writer, polygon)?;
141    }
142
143    writer.end_element("slice")?;
144    Ok(())
145}
146
147/// Writes a `<vertex>` element.
148fn write_vertex<W: Write>(writer: &mut XmlWriter<W>, v: &Vertex2D) -> Result<()> {
149    writer
150        .start_element("vertex")
151        .attr("x", &v.x.to_string())
152        .attr("y", &v.y.to_string())
153        .write_empty()?;
154    Ok(())
155}
156
157/// Writes a `<polygon>` element with its child segments.
158///
159/// Emits a warning for degenerate polygons (zero segments) but writes them
160/// faithfully rather than skipping.
161fn write_polygon<W: Write>(writer: &mut XmlWriter<W>, polygon: &Polygon) -> Result<()> {
162    if polygon.segments.is_empty() {
163        eprintln!(
164            "Warning: polygon start={} has zero segments (degenerate)",
165            polygon.start_segment
166        );
167    }
168
169    writer
170        .start_element("polygon")
171        .attr("start", &polygon.start_segment.to_string())
172        .write_start()?;
173
174    for segment in &polygon.segments {
175        write_segment(writer, segment)?;
176    }
177
178    writer.end_element("polygon")?;
179    Ok(())
180}
181
182/// Writes a `<segment>` element with required v2 and optional p1/p2/pid attributes.
183fn write_segment<W: Write>(writer: &mut XmlWriter<W>, segment: &Segment) -> Result<()> {
184    let mut elem = writer
185        .start_element("segment")
186        .attr("v2", &segment.v2.to_string());
187
188    if let Some(p1) = segment.p1 {
189        elem = elem.attr("p1", &p1.to_string());
190    }
191    if let Some(p2) = segment.p2 {
192        elem = elem.attr("p2", &p2.to_string());
193    }
194    if let Some(pid) = segment.pid {
195        elem = elem.attr("pid", &pid.0.to_string());
196    }
197
198    elem.write_empty()?;
199    Ok(())
200}
201
202/// Writes a `<sliceref>` element referencing an external slice stack.
203fn write_sliceref<W: Write>(writer: &mut XmlWriter<W>, sref: &SliceRef) -> Result<()> {
204    writer
205        .start_element("sliceref")
206        .attr("slicestackid", &sref.slice_stack_id.0.to_string())
207        .attr("slicepath", &sref.slice_path)
208        .write_empty()?;
209    Ok(())
210}