Skip to main content

lib3mf_core/writer/
model_writer.rs

1use crate::error::Result;
2use crate::model::{BooleanOperationType, Geometry, Model, Unit};
3use crate::writer::displacement_writer::{write_displacement_2d, write_displacement_mesh};
4use crate::writer::mesh_writer::write_mesh;
5use crate::writer::slice_writer;
6use crate::writer::volumetric_writer;
7use crate::writer::xml_writer::XmlWriter;
8use std::io::Write;
9
10use std::collections::HashMap;
11
12/// Formats a transformation matrix into the 3MF format (12 space-separated values in column-major order)
13fn format_transform_matrix(mat: &glam::Mat4) -> String {
14    format!(
15        "{} {} {} {} {} {} {} {} {} {} {} {}",
16        mat.x_axis.x,
17        mat.x_axis.y,
18        mat.x_axis.z,
19        mat.y_axis.x,
20        mat.y_axis.y,
21        mat.y_axis.z,
22        mat.z_axis.x,
23        mat.z_axis.y,
24        mat.z_axis.z,
25        mat.w_axis.x,
26        mat.w_axis.y,
27        mat.w_axis.z
28    )
29}
30
31impl Model {
32    /// Serializes the model to XML, writing the `<model>` document to the given writer.
33    pub fn write_xml<W: Write>(
34        &self,
35        writer: W,
36        thumbnail_relationships: Option<&HashMap<String, String>>,
37    ) -> Result<()> {
38        let mut xml = XmlWriter::new(writer);
39        xml.write_declaration()?;
40
41        let mut root = xml
42            .start_element("model")
43            .attr("unit", self.unit_str())
44            .attr("xml:lang", self.language.as_deref().unwrap_or("en-US"))
45            .attr(
46                "xmlns",
47                "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
48            )
49            .attr(
50                "xmlns:m",
51                "http://schemas.microsoft.com/3dmanufacturing/material/2015/02",
52            )
53            .attr(
54                "xmlns:p",
55                "http://schemas.microsoft.com/3dmanufacturing/production/2015/06",
56            )
57            .attr(
58                "xmlns:b",
59                "http://schemas.3mf.io/3dmanufacturing/booleanoperations/2023/07",
60            )
61            .attr(
62                "xmlns:d",
63                "http://schemas.microsoft.com/3dmanufacturing/displacement/2024/01",
64            )
65            .attr(
66                "xmlns:bl",
67                "http://schemas.microsoft.com/3dmanufacturing/beamlattice/2017/02",
68            )
69            .attr(
70                "xmlns:s",
71                "http://schemas.microsoft.com/3dmanufacturing/slice/2015/07",
72            )
73            .attr(
74                "xmlns:v",
75                "http://schemas.microsoft.com/3dmanufacturing/volumetric/2018/11",
76            );
77
78        // Emit extra namespaces (e.g., BambuStudio vendor namespace)
79        // We need to collect into a sorted vec for deterministic output
80        let mut extra_ns: Vec<(&String, &String)> = self.extra_namespaces.iter().collect();
81        extra_ns.sort_by_key(|(k, _)| k.as_str());
82        let extra_ns_owned: Vec<(String, String)> = extra_ns
83            .into_iter()
84            .map(|(k, v)| (format!("xmlns:{}", k), v.clone()))
85            .collect();
86        for (attr_name, uri) in &extra_ns_owned {
87            root = root.attr(attr_name, uri.as_str());
88        }
89
90        // Add typical namespaces if needed (e.g. production, slice) - strictly core for now
91        root.write_start()?;
92
93        // Metadata
94        for (key, value) in &self.metadata {
95            xml.start_element("metadata")
96                .attr("name", key)
97                .write_start()?;
98            xml.write_text(value)?;
99            xml.end_element("metadata")?;
100        }
101
102        // Resources
103        xml.start_element("resources").write_start()?;
104
105        // Write material resources first (colorgroups, basematerials, textures, etc.)
106        for color_group in self.resources.iter_color_groups() {
107            xml.start_element("colorgroup")
108                .attr("id", &color_group.id.0.to_string())
109                .write_start()?;
110            for color in &color_group.colors {
111                xml.start_element("color")
112                    .attr("color", &color.to_hex())
113                    .write_empty()?;
114            }
115            xml.end_element("colorgroup")?;
116        }
117
118        for base_materials in self.resources.iter_base_materials() {
119            xml.start_element("m:basematerials")
120                .attr("id", &base_materials.id.0.to_string())
121                .write_start()?;
122            for material in &base_materials.materials {
123                xml.start_element("m:base")
124                    .attr("name", &material.name)
125                    .attr("displaycolor", &material.display_color.to_hex())
126                    .write_empty()?;
127            }
128            xml.end_element("m:basematerials")?;
129        }
130
131        for texture_group in self.resources.iter_textures() {
132            xml.start_element("m:texture2dgroup")
133                .attr("id", &texture_group.id.0.to_string())
134                .attr("texid", &texture_group.texture_id.0.to_string())
135                .write_start()?;
136            for coord in &texture_group.coords {
137                xml.start_element("m:tex2coord")
138                    .attr("u", &coord.u.to_string())
139                    .attr("v", &coord.v.to_string())
140                    .write_empty()?;
141            }
142            xml.end_element("m:texture2dgroup")?;
143        }
144
145        for composite in self.resources.iter_composite_materials() {
146            xml.start_element("m:compositematerials")
147                .attr("id", &composite.id.0.to_string())
148                .attr("matid", &composite.base_material_id.0.to_string())
149                .write_start()?;
150            for comp in &composite.composites {
151                xml.start_element("m:composite")
152                    .attr(
153                        "values",
154                        &comp
155                            .values
156                            .iter()
157                            .map(|v| v.to_string())
158                            .collect::<Vec<_>>()
159                            .join(" "),
160                    )
161                    .write_empty()?;
162            }
163            xml.end_element("m:compositematerials")?;
164        }
165
166        for multi_props in self.resources.iter_multi_properties() {
167            xml.start_element("m:multiproperties")
168                .attr("id", &multi_props.id.0.to_string())
169                .attr(
170                    "pids",
171                    &multi_props
172                        .pids
173                        .iter()
174                        .map(|id| id.0.to_string())
175                        .collect::<Vec<_>>()
176                        .join(" "),
177                )
178                .write_start()?;
179            for multi in &multi_props.multis {
180                xml.start_element("m:multi")
181                    .attr(
182                        "pindices",
183                        &multi
184                            .pindices
185                            .iter()
186                            .map(|idx: &u32| idx.to_string())
187                            .collect::<Vec<_>>()
188                            .join(" "),
189                    )
190                    .write_empty()?;
191            }
192            xml.end_element("m:multiproperties")?;
193        }
194
195        // Write displacement texture resources
196        for displacement_2d in self.resources.iter_displacement_2d() {
197            write_displacement_2d(&mut xml, displacement_2d)?;
198        }
199
200        // Write slice stack resources
201        let slice_opts = slice_writer::SliceWriteOptions::default();
202        for stack in self.resources.iter_slice_stacks() {
203            slice_writer::write_slice_stack(&mut xml, stack, &slice_opts)?;
204        }
205
206        // Write volumetric stack resources
207        for stack in self.resources.iter_volumetric_stacks() {
208            volumetric_writer::write_volumetric_stack(&mut xml, stack)?;
209        }
210
211        // Write objects
212        for obj in self.resources.iter_objects() {
213            match &obj.geometry {
214                Geometry::BooleanShape(bs) => {
215                    // BooleanShape is written as a booleanshape resource (not an object)
216                    let mut bool_elem = xml
217                        .start_element("b:booleanshape")
218                        .attr("id", &obj.id.0.to_string())
219                        .attr("objectid", &bs.base_object_id.0.to_string());
220
221                    if bs.base_transform != glam::Mat4::IDENTITY {
222                        bool_elem = bool_elem
223                            .attr("transform", &format_transform_matrix(&bs.base_transform));
224                    }
225                    if let Some(path) = &bs.base_path {
226                        bool_elem = bool_elem.attr("p:path", path);
227                    }
228
229                    bool_elem.write_start()?;
230
231                    // Write nested boolean operations
232                    for op in &bs.operations {
233                        let op_type_str = match op.operation_type {
234                            BooleanOperationType::Union => "union",
235                            BooleanOperationType::Difference => "difference",
236                            BooleanOperationType::Intersection => "intersection",
237                        };
238
239                        let mut op_elem = xml
240                            .start_element("b:boolean")
241                            .attr("objectid", &op.object_id.0.to_string())
242                            .attr("operation", op_type_str);
243
244                        if op.transform != glam::Mat4::IDENTITY {
245                            op_elem =
246                                op_elem.attr("transform", &format_transform_matrix(&op.transform));
247                        }
248                        if let Some(path) = &op.path {
249                            op_elem = op_elem.attr("p:path", path);
250                        }
251
252                        op_elem.write_empty()?;
253                    }
254
255                    xml.end_element("b:booleanshape")?;
256                }
257                _ => {
258                    // Write as a regular object element
259                    let mut obj_elem = xml
260                        .start_element("object")
261                        .attr("id", &obj.id.0.to_string())
262                        .attr("type", &obj.object_type.to_string());
263
264                    // Peek geometry to add extension-specific attributes BEFORE write_start()
265                    if let Geometry::SliceStack(ssid) = &obj.geometry {
266                        obj_elem = obj_elem.attr("slicestackid", &ssid.0.to_string());
267                    }
268                    // Forward-compatible for Phase 14 volumetric writer
269                    if let Geometry::VolumetricStack(vsid) = &obj.geometry {
270                        obj_elem = obj_elem.attr("volumetricstackid", &vsid.0.to_string());
271                    }
272
273                    if let Some(pid) = obj.pid {
274                        obj_elem = obj_elem.attr("pid", &pid.0.to_string());
275                    }
276                    if let Some(pindex) = obj.pindex {
277                        obj_elem = obj_elem.attr("pindex", &pindex.to_string());
278                    }
279                    if let Some(pid) = obj.part_number.as_ref() {
280                        obj_elem = obj_elem.attr("partnumber", pid);
281                    }
282                    if let Some(uuid) = obj.uuid.as_ref() {
283                        obj_elem = obj_elem.attr("p:UUID", &uuid.to_string());
284                    }
285                    if let Some(name) = obj.name.as_ref() {
286                        obj_elem = obj_elem.attr("name", name);
287                    }
288                    if let Some(thumb_path) = obj.thumbnail.as_ref()
289                        && let Some(rels) = thumbnail_relationships
290                    {
291                        let lookup_key = if thumb_path.starts_with('/') {
292                            thumb_path.clone()
293                        } else {
294                            format!("/{}", thumb_path)
295                        };
296
297                        if let Some(rel_id) = rels.get(&lookup_key) {
298                            obj_elem = obj_elem.attr("thumbnail", rel_id);
299                        }
300                    }
301
302                    obj_elem.write_start()?;
303
304                    match &obj.geometry {
305                        Geometry::Mesh(mesh) => write_mesh(&mut xml, mesh)?,
306                        Geometry::Components(comps) => {
307                            xml.start_element("components").write_start()?;
308                            for c in &comps.components {
309                                let mut comp = xml
310                                    .start_element("component")
311                                    .attr("objectid", &c.object_id.0.to_string());
312
313                                if let Some(path) = c.path.as_ref() {
314                                    comp = comp.attr("p:path", path);
315                                }
316                                if let Some(uuid) = c.uuid.as_ref() {
317                                    comp = comp.attr("p:UUID", &uuid.to_string());
318                                }
319
320                                if c.transform != glam::Mat4::IDENTITY {
321                                    comp = comp
322                                        .attr("transform", &format_transform_matrix(&c.transform));
323                                }
324                                comp.write_empty()?;
325                            }
326                            xml.end_element("components")?;
327                        }
328                        Geometry::SliceStack(_) => {
329                            // No body content - slicestackid attribute already set above
330                        }
331                        Geometry::VolumetricStack(_) => {
332                            // No body content - volumetricstackid attribute already set above
333                        }
334                        Geometry::BooleanShape(_) => {
335                            unreachable!("BooleanShape handled in outer match")
336                        }
337                        Geometry::DisplacementMesh(mesh) => {
338                            write_displacement_mesh(&mut xml, mesh)?;
339                        }
340                    }
341
342                    xml.end_element("object")?;
343                }
344            }
345        }
346        xml.end_element("resources")?;
347
348        // Build
349        xml.start_element("build").write_start()?;
350        for item in &self.build.items {
351            let mut build_item = xml
352                .start_element("item")
353                .attr("objectid", &item.object_id.0.to_string());
354
355            if item.transform != glam::Mat4::IDENTITY {
356                build_item =
357                    build_item.attr("transform", &format_transform_matrix(&item.transform));
358            }
359            if let Some(ref pn) = item.part_number {
360                build_item = build_item.attr("partnumber", pn);
361            }
362            if let Some(printable) = item.printable {
363                build_item = build_item.attr("printable", if printable { "1" } else { "0" });
364            }
365            build_item.write_empty()?;
366        }
367        xml.end_element("build")?;
368
369        xml.end_element("model")?;
370        Ok(())
371    }
372
373    fn unit_str(&self) -> &'static str {
374        match self.unit {
375            Unit::Micron => "micron",
376            Unit::Millimeter => "millimeter",
377            Unit::Centimeter => "centimeter",
378            Unit::Inch => "inch",
379            Unit::Foot => "foot",
380            Unit::Meter => "meter",
381        }
382    }
383}