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>(
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 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 root.write_start()?;
91
92 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 xml.start_element("resources").write_start()?;
103
104 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 for displacement_2d in self.resources.iter_displacement_2d() {
196 write_displacement_2d(&mut xml, displacement_2d)?;
197 }
198
199 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 for stack in self.resources.iter_volumetric_stacks() {
207 volumetric_writer::write_volumetric_stack(&mut xml, stack)?;
208 }
209
210 for obj in self.resources.iter_objects() {
212 match &obj.geometry {
213 Geometry::BooleanShape(bs) => {
214 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 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 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 if let Geometry::SliceStack(ssid) = &obj.geometry {
265 obj_elem = obj_elem.attr("slicestackid", &ssid.0.to_string());
266 }
267 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 }
324 Geometry::VolumetricStack(_) => {
325 }
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 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}