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
12fn 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>(
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 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 root.write_start()?;
92
93 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 xml.start_element("resources").write_start()?;
104
105 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 for displacement_2d in self.resources.iter_displacement_2d() {
197 write_displacement_2d(&mut xml, displacement_2d)?;
198 }
199
200 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 for stack in self.resources.iter_volumetric_stacks() {
208 volumetric_writer::write_volumetric_stack(&mut xml, stack)?;
209 }
210
211 for obj in self.resources.iter_objects() {
213 match &obj.geometry {
214 Geometry::BooleanShape(bs) => {
215 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 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 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 if let Geometry::SliceStack(ssid) = &obj.geometry {
266 obj_elem = obj_elem.attr("slicestackid", &ssid.0.to_string());
267 }
268 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 }
331 Geometry::VolumetricStack(_) => {
332 }
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 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}