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