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::xml_writer::XmlWriter;
6use std::io::Write;
7
8use std::collections::HashMap;
9
10fn format_transform_matrix(mat: &glam::Mat4) -> String {
12 format!(
13 "{} {} {} {} {} {} {} {} {} {} {} {}",
14 mat.x_axis.x,
15 mat.x_axis.y,
16 mat.x_axis.z,
17 mat.y_axis.x,
18 mat.y_axis.y,
19 mat.y_axis.z,
20 mat.z_axis.x,
21 mat.z_axis.y,
22 mat.z_axis.z,
23 mat.w_axis.x,
24 mat.w_axis.y,
25 mat.w_axis.z
26 )
27}
28
29impl Model {
30 pub fn write_xml<W: Write>(
31 &self,
32 writer: W,
33 thumbnail_relationships: Option<&HashMap<String, String>>,
34 ) -> Result<()> {
35 let mut xml = XmlWriter::new(writer);
36 xml.write_declaration()?;
37
38 let root = xml
39 .start_element("model")
40 .attr("unit", self.unit_str())
41 .attr("xml:lang", self.language.as_deref().unwrap_or("en-US"))
42 .attr(
43 "xmlns",
44 "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
45 )
46 .attr(
47 "xmlns:m",
48 "http://schemas.microsoft.com/3dmanufacturing/material/2015/02",
49 )
50 .attr(
51 "xmlns:p",
52 "http://schemas.microsoft.com/3dmanufacturing/production/2015/06",
53 )
54 .attr(
55 "xmlns:b",
56 "http://schemas.3mf.io/3dmanufacturing/booleanoperations/2023/07",
57 )
58 .attr(
59 "xmlns:d",
60 "http://schemas.microsoft.com/3dmanufacturing/displacement/2024/01",
61 );
62
63 root.write_start()?;
65
66 for (key, value) in &self.metadata {
68 xml.start_element("metadata")
69 .attr("name", key)
70 .write_start()?;
71 xml.write_text(value)?;
72 xml.end_element("metadata")?;
73 }
74
75 xml.start_element("resources").write_start()?;
77
78 for color_group in self.resources.iter_color_groups() {
80 xml.start_element("colorgroup")
81 .attr("id", &color_group.id.0.to_string())
82 .write_start()?;
83 for color in &color_group.colors {
84 xml.start_element("color")
85 .attr("color", &color.to_hex())
86 .write_empty()?;
87 }
88 xml.end_element("colorgroup")?;
89 }
90
91 for base_materials in self.resources.iter_base_materials() {
92 xml.start_element("m:basematerials")
93 .attr("id", &base_materials.id.0.to_string())
94 .write_start()?;
95 for material in &base_materials.materials {
96 xml.start_element("m:base")
97 .attr("name", &material.name)
98 .attr("displaycolor", &material.display_color.to_hex())
99 .write_empty()?;
100 }
101 xml.end_element("m:basematerials")?;
102 }
103
104 for texture_group in self.resources.iter_textures() {
105 xml.start_element("m:texture2dgroup")
106 .attr("id", &texture_group.id.0.to_string())
107 .attr("texid", &texture_group.texture_id.0.to_string())
108 .write_start()?;
109 for coord in &texture_group.coords {
110 xml.start_element("m:tex2coord")
111 .attr("u", &coord.u.to_string())
112 .attr("v", &coord.v.to_string())
113 .write_empty()?;
114 }
115 xml.end_element("m:texture2dgroup")?;
116 }
117
118 for composite in self.resources.iter_composite_materials() {
119 xml.start_element("m:compositematerials")
120 .attr("id", &composite.id.0.to_string())
121 .attr("matid", &composite.base_material_id.0.to_string())
122 .write_start()?;
123 for comp in &composite.composites {
124 xml.start_element("m:composite")
125 .attr(
126 "values",
127 &comp
128 .values
129 .iter()
130 .map(|v| v.to_string())
131 .collect::<Vec<_>>()
132 .join(" "),
133 )
134 .write_empty()?;
135 }
136 xml.end_element("m:compositematerials")?;
137 }
138
139 for multi_props in self.resources.iter_multi_properties() {
140 xml.start_element("m:multiproperties")
141 .attr("id", &multi_props.id.0.to_string())
142 .attr(
143 "pids",
144 &multi_props
145 .pids
146 .iter()
147 .map(|id| id.0.to_string())
148 .collect::<Vec<_>>()
149 .join(" "),
150 )
151 .write_start()?;
152 for multi in &multi_props.multis {
153 xml.start_element("m:multi")
154 .attr(
155 "pindices",
156 &multi
157 .pindices
158 .iter()
159 .map(|idx: &u32| idx.to_string())
160 .collect::<Vec<_>>()
161 .join(" "),
162 )
163 .write_empty()?;
164 }
165 xml.end_element("m:multiproperties")?;
166 }
167
168 for displacement_2d in self.resources.iter_displacement_2d() {
170 write_displacement_2d(&mut xml, displacement_2d)?;
171 }
172
173 for obj in self.resources.iter_objects() {
175 match &obj.geometry {
176 Geometry::BooleanShape(bs) => {
177 let mut bool_elem = xml
179 .start_element("b:booleanshape")
180 .attr("id", &obj.id.0.to_string())
181 .attr("objectid", &bs.base_object_id.0.to_string());
182
183 if bs.base_transform != glam::Mat4::IDENTITY {
184 bool_elem = bool_elem
185 .attr("transform", &format_transform_matrix(&bs.base_transform));
186 }
187 if let Some(path) = &bs.base_path {
188 bool_elem = bool_elem.attr("p:path", path);
189 }
190
191 bool_elem.write_start()?;
192
193 for op in &bs.operations {
195 let op_type_str = match op.operation_type {
196 BooleanOperationType::Union => "union",
197 BooleanOperationType::Difference => "difference",
198 BooleanOperationType::Intersection => "intersection",
199 };
200
201 let mut op_elem = xml
202 .start_element("b:boolean")
203 .attr("objectid", &op.object_id.0.to_string())
204 .attr("operation", op_type_str);
205
206 if op.transform != glam::Mat4::IDENTITY {
207 op_elem =
208 op_elem.attr("transform", &format_transform_matrix(&op.transform));
209 }
210 if let Some(path) = &op.path {
211 op_elem = op_elem.attr("p:path", path);
212 }
213
214 op_elem.write_empty()?;
215 }
216
217 xml.end_element("b:booleanshape")?;
218 }
219 _ => {
220 let mut obj_elem = xml
222 .start_element("object")
223 .attr("id", &obj.id.0.to_string())
224 .attr("type", &obj.object_type.to_string());
225
226 if let Some(pid) = obj.part_number.as_ref() {
227 obj_elem = obj_elem.attr("partnumber", pid);
228 }
229 if let Some(uuid) = obj.uuid.as_ref() {
230 obj_elem = obj_elem.attr("p:UUID", &uuid.to_string());
231 }
232 if let Some(name) = obj.name.as_ref() {
233 obj_elem = obj_elem.attr("name", name);
234 }
235 if let Some(thumb_path) = obj.thumbnail.as_ref()
236 && let Some(rels) = thumbnail_relationships
237 {
238 let lookup_key = if thumb_path.starts_with('/') {
240 thumb_path.clone()
241 } else {
242 format!("/{}", thumb_path)
243 };
244
245 if let Some(rel_id) = rels.get(&lookup_key) {
246 obj_elem = obj_elem.attr("thumbnail", rel_id);
247 }
248 }
249
250 obj_elem.write_start()?;
251
252 match &obj.geometry {
253 Geometry::Mesh(mesh) => write_mesh(&mut xml, mesh)?,
254 Geometry::Components(comps) => {
255 xml.start_element("components").write_start()?;
256 for c in &comps.components {
257 let mut comp = xml
258 .start_element("component")
259 .attr("objectid", &c.object_id.0.to_string());
260
261 if let Some(path) = c.path.as_ref() {
262 comp = comp.attr("p:path", path);
263 }
264 if let Some(uuid) = c.uuid.as_ref() {
265 comp = comp.attr("p:UUID", &uuid.to_string());
266 }
267
268 if c.transform != glam::Mat4::IDENTITY {
269 comp = comp
270 .attr("transform", &format_transform_matrix(&c.transform));
271 }
272 comp.write_empty()?;
273 }
274 xml.end_element("components")?;
275 }
276 Geometry::SliceStack(_id) => {
277 }
288 Geometry::VolumetricStack(_id) => {
289 }
291 Geometry::BooleanShape(_) => {
292 unreachable!("BooleanShape handled in outer match")
294 }
295 Geometry::DisplacementMesh(mesh) => {
296 write_displacement_mesh(&mut xml, mesh)?;
297 }
298 }
299
300 xml.end_element("object")?;
301 }
302 }
303 }
304 xml.end_element("resources")?;
305
306 xml.start_element("build").write_start()?;
308 for item in &self.build.items {
309 let mut build_item = xml
310 .start_element("item")
311 .attr("objectid", &item.object_id.0.to_string());
312
313 if item.transform != glam::Mat4::IDENTITY {
314 build_item =
315 build_item.attr("transform", &format_transform_matrix(&item.transform));
316 }
317 build_item.write_empty()?;
319 }
320 xml.end_element("build")?;
321
322 xml.end_element("model")?;
323 Ok(())
324 }
325
326 fn unit_str(&self) -> &'static str {
327 match self.unit {
328 Unit::Micron => "micron",
329 Unit::Millimeter => "millimeter",
330 Unit::Centimeter => "centimeter",
331 Unit::Inch => "inch",
332 Unit::Foot => "foot",
333 Unit::Meter => "meter",
334 }
335 }
336}