1use std::collections::HashMap;
9use std::io::Read;
10
11use super::math::{self, M4, V3};
12use super::{Material, Mesh, Model, ParseError, Result};
13
14const MAX_PART: u64 = 1 << 30;
16const MAX_DEPTH: usize = 32;
18const MAX_INSTANCES: usize = 100_000;
20
21pub fn parse(bytes: &[u8]) -> Result<Model> {
22 let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).map_err(|e| ParseError(format!("not a 3MF (zip) file: {e}")))?;
23 let name = find_model_part(&mut zip).ok_or_else(|| ParseError::new("the 3MF has no 3D model part"))?;
24 let data = read_part(&mut zip, &name).ok_or_else(|| ParseError(format!("cannot read {name} in the 3MF")))?;
25 let doc = parse_document(&String::from_utf8_lossy(&data));
26 doc.flatten()
27}
28
29fn entry_name(zip: &mut zip::ZipArchive<std::io::Cursor<&[u8]>>, wanted: &str) -> Option<String> {
30 let wanted = wanted.trim_start_matches('/');
31 (0..zip.len()).find_map(|i| {
32 let f = zip.by_index_raw(i).ok()?;
33 let full = String::from_utf8_lossy(f.name_raw()).into_owned();
34 (full.trim_start_matches('/').eq_ignore_ascii_case(wanted) && !f.is_dir()).then_some(full)
35 })
36}
37
38fn read_part(zip: &mut zip::ZipArchive<std::io::Cursor<&[u8]>>, name: &str) -> Option<Vec<u8>> {
39 let real = entry_name(zip, name)?;
40 let f = zip.by_name(&real).ok()?;
41 let mut out = Vec::new();
42 f.take(MAX_PART).read_to_end(&mut out).ok()?;
43 Some(out)
44}
45
46fn find_model_part(zip: &mut zip::ZipArchive<std::io::Cursor<&[u8]>>) -> Option<String> {
47 if let Some(rels) = read_part(zip, "_rels/.rels") {
48 let mut target = None;
49 scan(&String::from_utf8_lossy(&rels), &mut |t| {
50 if !t.close && t.name == "Relationship" && target.is_none() && t.attr("Type").is_some_and(|ty| ty.ends_with("/3dmodel")) {
51 target = t.attr("Target").map(|s| s.trim_start_matches('/').to_string());
52 }
53 });
54 if let Some(t) = target
55 && entry_name(zip, &t).is_some()
56 {
57 return Some(t);
58 }
59 }
60 if entry_name(zip, "3D/3dmodel.model").is_some() {
61 return Some("3D/3dmodel.model".into());
62 }
63 (0..zip.len()).find_map(|i| {
64 let f = zip.by_index_raw(i).ok()?;
65 let full = String::from_utf8_lossy(f.name_raw()).into_owned();
66 (!f.is_dir() && full.to_ascii_lowercase().ends_with(".model")).then_some(full)
67 })
68}
69
70struct Tag {
73 name: String,
75 close: bool,
78 attrs: Vec<(String, String)>,
79}
80
81impl Tag {
82 fn attr(&self, name: &str) -> Option<&str> {
83 self.attrs.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
84 }
85
86 fn num(&self, name: &str) -> Option<f32> {
87 self.attr(name)?.trim().parse().ok()
88 }
89
90 fn id(&self, name: &str) -> Option<u32> {
91 self.attr(name)?.trim().parse().ok()
92 }
93}
94
95fn local(name: &str) -> &str {
96 name.rsplit(':').next().unwrap_or(name)
97}
98
99fn unescape(s: &str) -> String {
100 if !s.contains('&') {
101 return s.to_string();
102 }
103 s.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("&", "&")
104}
105
106fn scan(s: &str, f: &mut dyn FnMut(Tag)) {
109 let b = s.as_bytes();
110 let mut i = 0;
111 while let Some(off) = s[i..].find('<') {
112 i += off;
113 let rest = &s[i..];
114 if rest.starts_with("<!--") {
115 match rest.find("-->") {
116 Some(e) => i += e + 3,
117 None => return,
118 }
119 continue;
120 }
121 if rest.starts_with("<![CDATA[") {
122 match rest.find("]]>") {
123 Some(e) => i += e + 3,
124 None => return,
125 }
126 continue;
127 }
128 if rest.starts_with("<?") || rest.starts_with("<!") {
129 match rest.find('>') {
130 Some(e) => i += e + 1,
131 None => return,
132 }
133 continue;
134 }
135 i += 1;
136 let close = b.get(i) == Some(&b'/');
137 if close {
138 i += 1;
139 }
140 let start = i;
141 while i < b.len() && !b[i].is_ascii_whitespace() && b[i] != b'/' && b[i] != b'>' {
142 i += 1;
143 }
144 let name = local(&s[start..i]).to_string();
145 let mut attrs = Vec::new();
146 let mut selfclose = false;
147 loop {
148 while i < b.len() && b[i].is_ascii_whitespace() {
149 i += 1;
150 }
151 match b.get(i) {
152 None => return,
153 Some(b'>') => {
154 i += 1;
155 break;
156 }
157 Some(b'/') => {
158 selfclose = true;
159 i += 1;
160 }
161 Some(_) => {
162 let ks = i;
163 while i < b.len() && !b[i].is_ascii_whitespace() && !matches!(b[i], b'=' | b'>' | b'/') {
164 i += 1;
165 }
166 let key = local(&s[ks..i]).to_string();
167 while i < b.len() && b[i].is_ascii_whitespace() {
168 i += 1;
169 }
170 if b.get(i) != Some(&b'=') {
171 if i == ks {
172 i += 1;
173 }
174 continue;
175 }
176 i += 1;
177 while i < b.len() && b[i].is_ascii_whitespace() {
178 i += 1;
179 }
180 let val = match b.get(i) {
181 Some(&q) if q == b'"' || q == b'\'' => {
182 let vs = i + 1;
183 match s[vs..].find(q as char) {
184 Some(e) => {
185 i = vs + e + 1;
186 unescape(&s[vs..vs + e])
187 }
188 None => return,
189 }
190 }
191 _ => {
192 let vs = i;
193 while i < b.len() && !b[i].is_ascii_whitespace() && b[i] != b'>' {
194 i += 1;
195 }
196 unescape(&s[vs..i])
197 }
198 };
199 if !key.is_empty() {
200 attrs.push((key, val));
201 }
202 }
203 }
204 }
205 if close {
206 f(Tag { name, close: true, attrs: Vec::new() });
207 } else {
208 f(Tag { name: name.clone(), close: false, attrs });
209 if selfclose {
210 f(Tag { name, close: true, attrs: Vec::new() });
211 }
212 }
213 }
214}
215
216type Triangle = ([u32; 3], Option<(u32, usize)>);
220
221#[derive(Default)]
222struct Object {
223 name: String,
224 pid: Option<u32>,
225 pindex: usize,
226 positions: Vec<V3>,
227 triangles: Vec<Triangle>,
229 components: Vec<(u32, M4)>,
230}
231
232#[derive(Default)]
233struct Document {
234 objects: HashMap<u32, Object>,
235 order: Vec<u32>,
236 groups: HashMap<u32, Vec<Material>>,
238 build: Vec<(u32, M4)>,
239 has_build: bool,
240}
241
242const IDENTITY: M4 = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
243
244fn parse_transform(s: Option<&str>) -> M4 {
247 let Some(s) = s else { return IDENTITY };
248 let nums: Vec<f32> = s.split_whitespace().filter_map(|t| t.parse().ok()).collect();
249 if nums.len() != 12 || nums.iter().any(|n| !n.is_finite()) {
250 return IDENTITY;
251 }
252 let mut m = IDENTITY;
253 for (c, col) in m.iter_mut().enumerate() {
254 for (r, cell) in col.iter_mut().take(3).enumerate() {
255 *cell = nums[c * 3 + r];
256 }
257 }
258 m
259}
260
261fn parse_color(s: &str) -> Option<(V3, f32)> {
263 let h = s.trim().strip_prefix('#')?;
264 if !(h.len() == 6 || h.len() == 8) || !h.is_ascii() {
265 return None;
266 }
267 let byte = |i: usize| u8::from_str_radix(&h[i..i + 2], 16).ok().map(|v| f32::from(v) / 255.0);
268 let a = if h.len() == 8 { byte(6)? } else { 1.0 };
269 Some(([byte(0)?, byte(2)?, byte(4)?], a))
270}
271
272fn color_material(name: Option<&str>, spec: &str) -> Option<Material> {
273 let (color, opacity) = parse_color(spec)?;
274 let name = match name {
275 Some(n) if !n.is_empty() => n.to_string(),
276 _ => format!("#{}", spec.trim().trim_start_matches('#').get(..6).unwrap_or("").to_ascii_uppercase()),
277 };
278 Some(Material { name, color, opacity })
279}
280
281fn parse_document(xml: &str) -> Document {
282 let mut doc = Document::default();
283 let mut cur: Option<(u32, Object)> = None;
284 let mut group: Option<(u32, Vec<Material>)> = None;
285 let mut in_build = false;
286 scan(xml, &mut |t| {
287 if t.close {
288 match t.name.as_str() {
289 "object" => {
290 if let Some((id, o)) = cur.take()
291 && doc.objects.insert(id, o).is_none()
292 {
293 doc.order.push(id);
294 }
295 }
296 "basematerials" | "colorgroup" => {
297 if let Some((id, g)) = group.take() {
298 doc.groups.insert(id, g);
299 }
300 }
301 "build" => in_build = false,
302 _ => {}
303 }
304 return;
305 }
306 match t.name.as_str() {
307 "object" => {
308 if let Some(id) = t.id("id") {
309 cur = Some((
310 id,
311 Object {
312 name: t.attr("name").unwrap_or("").to_string(),
313 pid: t.id("pid"),
314 pindex: t.id("pindex").unwrap_or(0) as usize,
315 ..Object::default()
316 },
317 ));
318 }
319 }
320 "vertex" => {
321 if let Some((_, o)) = cur.as_mut() {
322 let p = [t.num("x").unwrap_or(0.0), t.num("y").unwrap_or(0.0), t.num("z").unwrap_or(0.0)];
323 o.positions.push(if p.iter().all(|c| c.is_finite()) { p } else { [0.0; 3] });
324 }
325 }
326 "triangle" => {
327 if let Some((_, o)) = cur.as_mut()
328 && let (Some(a), Some(b), Some(c)) = (t.id("v1"), t.id("v2"), t.id("v3"))
329 {
330 let p1 = t.id("p1").map(|p| p as usize);
331 let prop = match t.id("pid") {
332 Some(pid) => Some((pid, p1.unwrap_or(0))),
333 None => o.pid.map(|pid| (pid, p1.unwrap_or(o.pindex))),
334 };
335 o.triangles.push(([a, b, c], prop));
336 }
337 }
338 "component" => {
339 if let (Some((_, o)), Some(id)) = (cur.as_mut(), t.id("objectid")) {
340 o.components.push((id, parse_transform(t.attr("transform"))));
341 }
342 }
343 "basematerials" | "colorgroup" => {
344 if let Some(id) = t.id("id") {
345 group = Some((id, Vec::new()));
346 }
347 }
348 "base" => {
349 if let Some((_, g)) = group.as_mut() {
350 g.push(t.attr("displaycolor").and_then(|c| color_material(t.attr("name"), c)).unwrap_or(Material { name: String::new(), color: super::DEFAULT_COLOR, opacity: 1.0 }));
352 }
353 }
354 "color" => {
355 if let Some((_, g)) = group.as_mut() {
356 g.push(t.attr("color").and_then(|c| color_material(None, c)).unwrap_or(Material { name: String::new(), color: super::DEFAULT_COLOR, opacity: 1.0 }));
357 }
358 }
359 "build" => {
360 in_build = true;
361 doc.has_build = true;
362 }
363 "item" if in_build => {
364 if let Some(id) = t.id("objectid") {
365 doc.build.push((id, parse_transform(t.attr("transform"))));
366 }
367 }
368 _ => {}
369 }
370 });
371 doc
372}
373
374impl Document {
375 fn material(&self, prop: Option<(u32, usize)>) -> Option<&Material> {
378 let (pid, idx) = prop?;
379 let m = self.groups.get(&pid)?.get(idx)?;
380 (m.color != super::DEFAULT_COLOR || !m.name.is_empty()).then_some(m)
381 }
382
383 fn flatten(&self) -> Result<Model> {
384 let mut meshes = Vec::new();
385 let mut instances = 0usize;
386 if self.has_build && !self.build.is_empty() {
387 for &(id, m) in &self.build {
388 self.instantiate(id, &m, 0, &mut Vec::new(), &mut instances, &mut meshes);
389 }
390 } else {
391 let used: std::collections::HashSet<u32> = self.objects.values().flat_map(|o| o.components.iter().map(|c| c.0)).collect();
392 for id in self.order.iter().filter(|id| !used.contains(id)) {
393 self.instantiate(*id, &IDENTITY, 0, &mut Vec::new(), &mut instances, &mut meshes);
394 }
395 }
396 Model { meshes }.finish()
397 }
398
399 fn instantiate(&self, id: u32, m: &M4, depth: usize, stack: &mut Vec<u32>, instances: &mut usize, out: &mut Vec<Mesh>) {
400 if depth > MAX_DEPTH || stack.contains(&id) || *instances >= MAX_INSTANCES {
401 return;
402 }
403 let Some(o) = self.objects.get(&id) else { return };
404 *instances += 1;
405 self.emit_mesh(o, m, out);
406 stack.push(id);
407 for (child, cm) in &o.components {
408 self.instantiate(*child, &math::mul(m, cm), depth + 1, stack, instances, out);
409 }
410 stack.pop();
411 }
412
413 fn emit_mesh(&self, o: &Object, m: &M4, out: &mut Vec<Mesh>) {
415 if o.triangles.is_empty() {
416 return;
417 }
418 struct Bucket {
419 material: Option<Material>,
420 remap: HashMap<u32, u32>,
421 mesh: Mesh,
422 }
423 let mut buckets: Vec<Bucket> = Vec::new();
424 let mut by_key: HashMap<Option<(String, [u32; 3], u32)>, usize> = HashMap::new();
425 for (tri, prop) in &o.triangles {
426 if tri.iter().any(|&v| v as usize >= o.positions.len()) {
427 continue;
428 }
429 let mat = self.material(*prop);
430 let key = mat.map(|x| (x.name.clone(), x.color.map(f32::to_bits), x.opacity.to_bits()));
431 let bi = *by_key.entry(key).or_insert_with(|| {
432 buckets.push(Bucket {
433 material: mat.cloned(),
434 remap: HashMap::new(),
435 mesh: Mesh { name: o.name.clone(), ..Mesh::default() },
436 });
437 buckets.len() - 1
438 });
439 let b = &mut buckets[bi];
440 for &v in tri {
441 let next = b.mesh.positions.len() as u32;
442 let ni = *b.remap.entry(v).or_insert_with(|| {
443 b.mesh.positions.push(math::transform_point(m, o.positions[v as usize]));
444 next
445 });
446 b.mesh.indices.push(ni);
447 }
448 }
449 out.extend(buckets.into_iter().map(|mut b| {
450 b.mesh.material = b.material;
451 b.mesh
452 }));
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use std::io::Write;
460 use zip::write::SimpleFileOptions;
461
462 fn make_zip(files: &[(&str, &str)]) -> Vec<u8> {
463 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
464 for (name, body) in files {
465 w.start_file(*name, SimpleFileOptions::default()).unwrap();
466 w.write_all(body.as_bytes()).unwrap();
467 }
468 w.finish().unwrap().into_inner()
469 }
470
471 fn model_zip(xml: &str) -> Vec<u8> {
472 make_zip(&[("3D/3dmodel.model", xml)])
473 }
474
475 const TRI: &str = r#"<vertices><vertex x="0" y="0" z="0"/><vertex x="1" y="0" z="0"/><vertex x="0" y="1" z="0"/></vertices>"#;
476
477 fn doc(resources: &str, build: &str) -> String {
478 format!(r#"<?xml version="1.0"?><model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"><resources>{resources}</resources><build>{build}</build></model>"#)
479 }
480
481 #[test]
482 fn single_triangle() {
483 let xml = doc(&format!(r#"<object id="1" name="tri"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh></object>"#), r#"<item objectid="1"/>"#);
484 let m = parse(&model_zip(&xml)).unwrap();
485 assert_eq!(m.meshes.len(), 1);
486 assert_eq!(m.meshes[0].name, "tri");
487 assert_eq!(m.meshes[0].indices, vec![0, 1, 2]);
488 assert_eq!(m.meshes[0].positions[1], [1.0, 0.0, 0.0]);
489 assert!(m.meshes[0].material.is_none());
490 }
491
492 #[test]
493 fn item_transform_translates() {
494 let xml = doc(&format!(r#"<object id="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh></object>"#), r#"<item objectid="1" transform="1 0 0 0 1 0 0 0 1 10 20 30"/>"#);
495 let m = parse(&model_zip(&xml)).unwrap();
496 assert_eq!(m.meshes[0].positions[0], [10.0, 20.0, 30.0]);
497 assert_eq!(m.meshes[0].positions[1], [11.0, 20.0, 30.0]);
498 }
499
500 #[test]
501 fn object_base_material() {
502 let xml = doc(
503 &format!(r##"<basematerials id="5"><base name="Red" displaycolor="#FF0000"/><base name="Blue" displaycolor="#0000FF80"/></basematerials><object id="1" pid="5" pindex="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh></object>"##),
504 r#"<item objectid="1"/>"#,
505 );
506 let m = parse(&model_zip(&xml)).unwrap();
507 let mat = m.meshes[0].material.clone().unwrap();
508 assert_eq!(mat.name, "Blue");
509 assert_eq!(mat.color, [0.0, 0.0, 1.0]);
510 assert!((mat.opacity - 128.0 / 255.0).abs() < 1e-6);
511 }
512
513 #[test]
514 fn per_triangle_colors_split() {
515 let xml = doc(
516 &format!(
517 r##"<colorgroup id="2"><color color="#00FF00"/><color color="#112233"/></colorgroup><object id="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2" pid="2" p1="0"/><triangle v1="2" v2="1" v3="0" pid="2" p1="1"/><triangle v1="0" v2="2" v3="1"/></triangles></mesh></object>"##
518 ),
519 r#"<item objectid="1"/>"#,
520 );
521 let m = parse(&model_zip(&xml)).unwrap();
522 assert_eq!(m.meshes.len(), 3);
523 assert_eq!(m.meshes[0].material.as_ref().unwrap().name, "#00FF00");
524 assert_eq!(m.meshes[1].material.as_ref().unwrap().color, [17.0 / 255.0, 34.0 / 255.0, 51.0 / 255.0]);
525 assert!(m.meshes[2].material.is_none());
526 }
527
528 #[test]
529 fn components_twice() {
530 let xml = doc(
531 &format!(
532 r#"<object id="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh></object><object id="2"><components><component objectid="1"/><component objectid="1" transform="1 0 0 0 1 0 0 0 1 5 0 0"/></components></object>"#
533 ),
534 r#"<item objectid="2" transform="1 0 0 0 1 0 0 0 1 0 0 100"/>"#,
535 );
536 let m = parse(&model_zip(&xml)).unwrap();
537 assert_eq!(m.meshes.len(), 2);
538 assert_eq!(m.meshes[0].positions[0], [0.0, 0.0, 100.0]);
539 assert_eq!(m.meshes[1].positions[0], [5.0, 0.0, 100.0]);
540 }
541
542 #[test]
543 fn no_build_uses_unreferenced_objects_and_survives_cycles() {
544 let xml = doc(
545 &format!(r#"<object id="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh><components><component objectid="2"/></components></object><object id="2"><components><component objectid="1"/></components></object>"#),
546 "",
547 );
548 assert!(parse(&model_zip(&xml)).is_err());
550 let xml = doc(&format!(r#"<object id="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh></object>"#), "");
551 assert_eq!(parse(&model_zip(&xml)).unwrap().meshes.len(), 1);
552 }
553
554 #[test]
555 fn model_found_through_rels() {
556 let xml = doc(&format!(r#"<object id="1"><mesh>{TRI}<triangles><triangle v1="0" v2="1" v3="2"/></triangles></mesh></object>"#), r#"<item objectid="1"/>"#);
557 let rels = r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/Other/part.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>"#;
558 let z = make_zip(&[("_rels/.rels", rels), ("Other/part.model", &xml), ("3D/3dmodel.model", "<model/>")]);
559 assert_eq!(parse(&z).unwrap().triangle_count(), 1);
560 }
561
562 #[test]
563 fn namespaced_document() {
564 let xml = format!(
565 r#"<?xml version='1.0'?><!-- c --><m:model xmlns:m='urn:x'><m:resources><m:object id='1' name='a & b'><m:mesh>{}<m:triangles><m:triangle v1='0' v2='1' v3='2'/></m:triangles></m:mesh></m:object></m:resources><m:build><m:item objectid='1'/></m:build></m:model>"#,
566 TRI.replace("<vertex", "<m:vertex").replace("</vertices", "</m:vertices").replace("<vertices", "<m:vertices")
567 );
568 let m = parse(&model_zip(&xml)).unwrap();
569 assert_eq!(m.meshes[0].name, "a & b");
570 assert_eq!(m.triangle_count(), 1);
571 }
572
573 #[test]
574 fn not_a_zip() {
575 assert!(parse(b"hello, this is not a zip").is_err());
576 }
577
578 #[test]
579 fn missing_model() {
580 let z = make_zip(&[("readme.txt", "hi")]);
581 let e = parse(&z).unwrap_err();
582 assert!(e.0.contains("no 3D model"), "{e}");
583 }
584}