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