lib3mf-core 0.4.0

Parse and validate 3MF files for manufacturing workflows - production-ready with streaming parser and comprehensive validation
Documentation
use crate::error::Result;
use crate::model::{BooleanOperationType, Geometry, Model, Unit};
use crate::writer::displacement_writer::{write_displacement_2d, write_displacement_mesh};
use crate::writer::mesh_writer::write_mesh;
use crate::writer::slice_writer;
use crate::writer::volumetric_writer;
use crate::writer::xml_writer::XmlWriter;
use std::io::Write;

use std::collections::HashMap;

/// Formats a transformation matrix into the 3MF format (12 space-separated values in column-major order)
fn format_transform_matrix(mat: &glam::Mat4) -> String {
    format!(
        "{} {} {} {} {} {} {} {} {} {} {} {}",
        mat.x_axis.x,
        mat.x_axis.y,
        mat.x_axis.z,
        mat.y_axis.x,
        mat.y_axis.y,
        mat.y_axis.z,
        mat.z_axis.x,
        mat.z_axis.y,
        mat.z_axis.z,
        mat.w_axis.x,
        mat.w_axis.y,
        mat.w_axis.z
    )
}

impl Model {
    /// Serializes the model to XML, writing the `<model>` document to the given writer.
    pub fn write_xml<W: Write>(
        &self,
        writer: W,
        thumbnail_relationships: Option<&HashMap<String, String>>,
    ) -> Result<()> {
        let mut xml = XmlWriter::new(writer);
        xml.write_declaration()?;

        let mut root = xml
            .start_element("model")
            .attr("unit", self.unit_str())
            .attr("xml:lang", self.language.as_deref().unwrap_or("en-US"))
            .attr(
                "xmlns",
                "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
            )
            .attr(
                "xmlns:m",
                "http://schemas.microsoft.com/3dmanufacturing/material/2015/02",
            )
            .attr(
                "xmlns:p",
                "http://schemas.microsoft.com/3dmanufacturing/production/2015/06",
            )
            .attr(
                "xmlns:b",
                "http://schemas.3mf.io/3dmanufacturing/booleanoperations/2023/07",
            )
            .attr(
                "xmlns:d",
                "http://schemas.microsoft.com/3dmanufacturing/displacement/2024/01",
            )
            .attr(
                "xmlns:bl",
                "http://schemas.microsoft.com/3dmanufacturing/beamlattice/2017/02",
            )
            .attr(
                "xmlns:s",
                "http://schemas.microsoft.com/3dmanufacturing/slice/2015/07",
            )
            .attr(
                "xmlns:v",
                "http://schemas.microsoft.com/3dmanufacturing/volumetric/2018/11",
            );

        // Emit extra namespaces (e.g., BambuStudio vendor namespace)
        // We need to collect into a sorted vec for deterministic output
        let mut extra_ns: Vec<(&String, &String)> = self.extra_namespaces.iter().collect();
        extra_ns.sort_by_key(|(k, _)| k.as_str());
        let extra_ns_owned: Vec<(String, String)> = extra_ns
            .into_iter()
            .map(|(k, v)| (format!("xmlns:{}", k), v.clone()))
            .collect();
        for (attr_name, uri) in &extra_ns_owned {
            root = root.attr(attr_name, uri.as_str());
        }

        // Add typical namespaces if needed (e.g. production, slice) - strictly core for now
        root.write_start()?;

        // Metadata
        for (key, value) in &self.metadata {
            xml.start_element("metadata")
                .attr("name", key)
                .write_start()?;
            xml.write_text(value)?;
            xml.end_element("metadata")?;
        }

        // Resources
        xml.start_element("resources").write_start()?;

        // Write material resources first (colorgroups, basematerials, textures, etc.)
        for color_group in self.resources.iter_color_groups() {
            xml.start_element("colorgroup")
                .attr("id", &color_group.id.0.to_string())
                .write_start()?;
            for color in &color_group.colors {
                xml.start_element("color")
                    .attr("color", &color.to_hex())
                    .write_empty()?;
            }
            xml.end_element("colorgroup")?;
        }

        for base_materials in self.resources.iter_base_materials() {
            xml.start_element("m:basematerials")
                .attr("id", &base_materials.id.0.to_string())
                .write_start()?;
            for material in &base_materials.materials {
                xml.start_element("m:base")
                    .attr("name", &material.name)
                    .attr("displaycolor", &material.display_color.to_hex())
                    .write_empty()?;
            }
            xml.end_element("m:basematerials")?;
        }

        for texture_group in self.resources.iter_textures() {
            xml.start_element("m:texture2dgroup")
                .attr("id", &texture_group.id.0.to_string())
                .attr("texid", &texture_group.texture_id.0.to_string())
                .write_start()?;
            for coord in &texture_group.coords {
                xml.start_element("m:tex2coord")
                    .attr("u", &coord.u.to_string())
                    .attr("v", &coord.v.to_string())
                    .write_empty()?;
            }
            xml.end_element("m:texture2dgroup")?;
        }

        for composite in self.resources.iter_composite_materials() {
            xml.start_element("m:compositematerials")
                .attr("id", &composite.id.0.to_string())
                .attr("matid", &composite.base_material_id.0.to_string())
                .write_start()?;
            for comp in &composite.composites {
                xml.start_element("m:composite")
                    .attr(
                        "values",
                        &comp
                            .values
                            .iter()
                            .map(|v| v.to_string())
                            .collect::<Vec<_>>()
                            .join(" "),
                    )
                    .write_empty()?;
            }
            xml.end_element("m:compositematerials")?;
        }

        for multi_props in self.resources.iter_multi_properties() {
            xml.start_element("m:multiproperties")
                .attr("id", &multi_props.id.0.to_string())
                .attr(
                    "pids",
                    &multi_props
                        .pids
                        .iter()
                        .map(|id| id.0.to_string())
                        .collect::<Vec<_>>()
                        .join(" "),
                )
                .write_start()?;
            for multi in &multi_props.multis {
                xml.start_element("m:multi")
                    .attr(
                        "pindices",
                        &multi
                            .pindices
                            .iter()
                            .map(|idx: &u32| idx.to_string())
                            .collect::<Vec<_>>()
                            .join(" "),
                    )
                    .write_empty()?;
            }
            xml.end_element("m:multiproperties")?;
        }

        // Write displacement texture resources
        for displacement_2d in self.resources.iter_displacement_2d() {
            write_displacement_2d(&mut xml, displacement_2d)?;
        }

        // Write slice stack resources
        let slice_opts = slice_writer::SliceWriteOptions::default();
        for stack in self.resources.iter_slice_stacks() {
            slice_writer::write_slice_stack(&mut xml, stack, &slice_opts)?;
        }

        // Write volumetric stack resources
        for stack in self.resources.iter_volumetric_stacks() {
            volumetric_writer::write_volumetric_stack(&mut xml, stack)?;
        }

        // Write objects
        for obj in self.resources.iter_objects() {
            match &obj.geometry {
                Geometry::BooleanShape(bs) => {
                    // BooleanShape is written as a booleanshape resource (not an object)
                    let mut bool_elem = xml
                        .start_element("b:booleanshape")
                        .attr("id", &obj.id.0.to_string())
                        .attr("objectid", &bs.base_object_id.0.to_string());

                    if bs.base_transform != glam::Mat4::IDENTITY {
                        bool_elem = bool_elem
                            .attr("transform", &format_transform_matrix(&bs.base_transform));
                    }
                    if let Some(path) = &bs.base_path {
                        bool_elem = bool_elem.attr("p:path", path);
                    }

                    bool_elem.write_start()?;

                    // Write nested boolean operations
                    for op in &bs.operations {
                        let op_type_str = match op.operation_type {
                            BooleanOperationType::Union => "union",
                            BooleanOperationType::Difference => "difference",
                            BooleanOperationType::Intersection => "intersection",
                        };

                        let mut op_elem = xml
                            .start_element("b:boolean")
                            .attr("objectid", &op.object_id.0.to_string())
                            .attr("operation", op_type_str);

                        if op.transform != glam::Mat4::IDENTITY {
                            op_elem =
                                op_elem.attr("transform", &format_transform_matrix(&op.transform));
                        }
                        if let Some(path) = &op.path {
                            op_elem = op_elem.attr("p:path", path);
                        }

                        op_elem.write_empty()?;
                    }

                    xml.end_element("b:booleanshape")?;
                }
                _ => {
                    // Write as a regular object element
                    let mut obj_elem = xml
                        .start_element("object")
                        .attr("id", &obj.id.0.to_string())
                        .attr("type", &obj.object_type.to_string());

                    // Peek geometry to add extension-specific attributes BEFORE write_start()
                    if let Geometry::SliceStack(ssid) = &obj.geometry {
                        obj_elem = obj_elem.attr("slicestackid", &ssid.0.to_string());
                    }
                    // Forward-compatible for Phase 14 volumetric writer
                    if let Geometry::VolumetricStack(vsid) = &obj.geometry {
                        obj_elem = obj_elem.attr("volumetricstackid", &vsid.0.to_string());
                    }

                    if let Some(pid) = obj.pid {
                        obj_elem = obj_elem.attr("pid", &pid.0.to_string());
                    }
                    if let Some(pindex) = obj.pindex {
                        obj_elem = obj_elem.attr("pindex", &pindex.to_string());
                    }
                    if let Some(pid) = obj.part_number.as_ref() {
                        obj_elem = obj_elem.attr("partnumber", pid);
                    }
                    if let Some(uuid) = obj.uuid.as_ref() {
                        obj_elem = obj_elem.attr("p:UUID", &uuid.to_string());
                    }
                    if let Some(name) = obj.name.as_ref() {
                        obj_elem = obj_elem.attr("name", name);
                    }
                    if let Some(thumb_path) = obj.thumbnail.as_ref()
                        && let Some(rels) = thumbnail_relationships
                    {
                        let lookup_key = if thumb_path.starts_with('/') {
                            thumb_path.clone()
                        } else {
                            format!("/{}", thumb_path)
                        };

                        if let Some(rel_id) = rels.get(&lookup_key) {
                            obj_elem = obj_elem.attr("thumbnail", rel_id);
                        }
                    }

                    obj_elem.write_start()?;

                    match &obj.geometry {
                        Geometry::Mesh(mesh) => write_mesh(&mut xml, mesh)?,
                        Geometry::Components(comps) => {
                            xml.start_element("components").write_start()?;
                            for c in &comps.components {
                                let mut comp = xml
                                    .start_element("component")
                                    .attr("objectid", &c.object_id.0.to_string());

                                if let Some(path) = c.path.as_ref() {
                                    comp = comp.attr("p:path", path);
                                }
                                if let Some(uuid) = c.uuid.as_ref() {
                                    comp = comp.attr("p:UUID", &uuid.to_string());
                                }

                                if c.transform != glam::Mat4::IDENTITY {
                                    comp = comp
                                        .attr("transform", &format_transform_matrix(&c.transform));
                                }
                                comp.write_empty()?;
                            }
                            xml.end_element("components")?;
                        }
                        Geometry::SliceStack(_) => {
                            // No body content - slicestackid attribute already set above
                        }
                        Geometry::VolumetricStack(_) => {
                            // No body content - volumetricstackid attribute already set above
                        }
                        Geometry::BooleanShape(_) => {
                            unreachable!("BooleanShape handled in outer match")
                        }
                        Geometry::DisplacementMesh(mesh) => {
                            write_displacement_mesh(&mut xml, mesh)?;
                        }
                    }

                    xml.end_element("object")?;
                }
            }
        }
        xml.end_element("resources")?;

        // Build
        xml.start_element("build").write_start()?;
        for item in &self.build.items {
            let mut build_item = xml
                .start_element("item")
                .attr("objectid", &item.object_id.0.to_string());

            if item.transform != glam::Mat4::IDENTITY {
                build_item =
                    build_item.attr("transform", &format_transform_matrix(&item.transform));
            }
            if let Some(ref pn) = item.part_number {
                build_item = build_item.attr("partnumber", pn);
            }
            if let Some(printable) = item.printable {
                build_item = build_item.attr("printable", if printable { "1" } else { "0" });
            }
            build_item.write_empty()?;
        }
        xml.end_element("build")?;

        xml.end_element("model")?;
        Ok(())
    }

    fn unit_str(&self) -> &'static str {
        match self.unit {
            Unit::Micron => "micron",
            Unit::Millimeter => "millimeter",
            Unit::Centimeter => "centimeter",
            Unit::Inch => "inch",
            Unit::Foot => "foot",
            Unit::Meter => "meter",
        }
    }
}