Skip to main content

ogeom_io/
threemf.rs

1//! 3MF: the mesh package, written with its own archive.
2//!
3//! A 3MF file is a ZIP holding three parts: the content-types declaration,
4//! a relationship pointing at the model, and the model itself: an XML
5//! document of meshes and the items that place them. The archive is written
6//! and read here, with no library behind it.
7//!
8//! It writes *stored* entries (no compression), which the ZIP format allows
9//! and every reader accepts: what writing a package needs is the container,
10//! not the codec. A file written this way is larger than one deflated.
11//!
12//! Reading is the other way round. Every package a slicer or a modelling
13//! tool writes is deflated, so [`read_3mf`] inflates, with the decoder half
14//! of DEFLATE kept in this crate, and reads the archive through its central
15//! directory, where the sizes are, since a streamed entry leaves them out
16//! of its local header.
17
18use ogeom_core::FastMap;
19use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
20use ogeom_math::{Point, Vector};
21use ogeom_topo::Triangulation;
22use std::fmt::Write as _;
23
24use crate::image::{Image, Wrap};
25use crate::mesh_formats::MeshColours;
26use crate::xml;
27
28/// One mesh in the package, with the name the item carries.
29#[derive(Debug, Clone)]
30pub struct Object<'a> {
31    /// The tessellation.
32    pub mesh: &'a Triangulation,
33    /// The name the object is given, if any.
34    pub name: Option<String>,
35}
36
37/// Write meshes as a 3MF package.
38///
39/// One object per mesh, one item per object, all in millimetres, the
40/// format's own default unit and this kernel's.
41#[must_use]
42pub fn write_3mf(objects: &[Object<'_>]) -> Vec<u8> {
43    let content_types = r#"<?xml version="1.0" encoding="UTF-8"?>
44<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
45  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
46  <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
47</Types>
48"#;
49    let relationships = r#"<?xml version="1.0" encoding="UTF-8"?>
50<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
51  <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
52</Relationships>
53"#;
54
55    let mut model = String::from(
56        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<model unit=\"millimeter\" \
57         xml:lang=\"en-US\" \
58         xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">\n  \
59         <resources>\n",
60    );
61    let mut items = String::new();
62    for (i, object) in objects.iter().enumerate() {
63        if object.mesh.triangles.is_empty() {
64            continue;
65        }
66        let id = i + 1;
67        match &object.name {
68            Some(name) => {
69                let _ = writeln!(
70                    model,
71                    "    <object id=\"{id}\" type=\"model\" name=\"{}\">",
72                    escaped(name)
73                );
74            }
75            None => {
76                let _ = writeln!(model, "    <object id=\"{id}\" type=\"model\">");
77            }
78        }
79        model.push_str("      <mesh>\n        <vertices>\n");
80        for p in &object.mesh.positions {
81            let _ = writeln!(
82                model,
83                "          <vertex x=\"{}\" y=\"{}\" z=\"{}\"/>",
84                p.x, p.y, p.z
85            );
86        }
87        model.push_str("        </vertices>\n        <triangles>\n");
88        for [a, b, c] in &object.mesh.triangles {
89            let _ = writeln!(
90                model,
91                "          <triangle v1=\"{a}\" v2=\"{b}\" v3=\"{c}\"/>"
92            );
93        }
94        model.push_str("        </triangles>\n      </mesh>\n    </object>\n");
95        let _ = writeln!(items, "    <item objectid=\"{id}\"/>");
96    }
97    model.push_str("  </resources>\n  <build>\n");
98    model.push_str(&items);
99    model.push_str("  </build>\n</model>\n");
100
101    archive(&[
102        ("[Content_Types].xml", content_types.as_bytes()),
103        ("_rels/.rels", relationships.as_bytes()),
104        ("3D/3dmodel.model", model.as_bytes()),
105    ])
106}
107
108/// The XML text escapes, which are the five the specification names.
109fn escaped(text: &str) -> String {
110    text.replace('&', "&amp;")
111        .replace('<', "&lt;")
112        .replace('>', "&gt;")
113        .replace('"', "&quot;")
114        .replace('\'', "&apos;")
115}
116
117/// A ZIP archive of stored (uncompressed) entries.
118///
119/// Local header, data, then a central directory and its end record. Every
120/// field the format requires and none it does not: no data descriptors (the
121/// sizes are known before writing), no ZIP64 (a 3MF over four gigabytes is
122/// a different problem), no extra fields.
123fn archive(entries: &[(&str, &[u8])]) -> Vec<u8> {
124    let mut out: Vec<u8> = Vec::new();
125    let mut directory: Vec<u8> = Vec::new();
126    let mut count = 0_u16;
127
128    for (name, data) in entries {
129        let offset = u32::try_from(out.len()).unwrap_or(u32::MAX);
130        let crc = crc32(data);
131        let size = u32::try_from(data.len()).unwrap_or(u32::MAX);
132        let name_bytes = name.as_bytes();
133        let name_len = u16::try_from(name_bytes.len()).unwrap_or(u16::MAX);
134
135        // Local file header.
136        out.extend_from_slice(&0x0403_4b50_u32.to_le_bytes());
137        out.extend_from_slice(&20_u16.to_le_bytes()); // version needed
138        out.extend_from_slice(&0_u16.to_le_bytes()); // flags
139        out.extend_from_slice(&0_u16.to_le_bytes()); // stored
140        out.extend_from_slice(&0_u16.to_le_bytes()); // time
141        out.extend_from_slice(&0_u16.to_le_bytes()); // date
142        out.extend_from_slice(&crc.to_le_bytes());
143        out.extend_from_slice(&size.to_le_bytes());
144        out.extend_from_slice(&size.to_le_bytes());
145        out.extend_from_slice(&name_len.to_le_bytes());
146        out.extend_from_slice(&0_u16.to_le_bytes()); // extra length
147        out.extend_from_slice(name_bytes);
148        out.extend_from_slice(data);
149
150        // Central directory entry.
151        directory.extend_from_slice(&0x0201_4b50_u32.to_le_bytes());
152        directory.extend_from_slice(&20_u16.to_le_bytes()); // version made by
153        directory.extend_from_slice(&20_u16.to_le_bytes()); // version needed
154        directory.extend_from_slice(&0_u16.to_le_bytes());
155        directory.extend_from_slice(&0_u16.to_le_bytes());
156        directory.extend_from_slice(&0_u16.to_le_bytes());
157        directory.extend_from_slice(&0_u16.to_le_bytes());
158        directory.extend_from_slice(&crc.to_le_bytes());
159        directory.extend_from_slice(&size.to_le_bytes());
160        directory.extend_from_slice(&size.to_le_bytes());
161        directory.extend_from_slice(&name_len.to_le_bytes());
162        directory.extend_from_slice(&0_u16.to_le_bytes()); // extra
163        directory.extend_from_slice(&0_u16.to_le_bytes()); // comment
164        directory.extend_from_slice(&0_u16.to_le_bytes()); // disk
165        directory.extend_from_slice(&0_u16.to_le_bytes()); // internal attrs
166        directory.extend_from_slice(&0_u32.to_le_bytes()); // external attrs
167        directory.extend_from_slice(&offset.to_le_bytes());
168        directory.extend_from_slice(name_bytes);
169        count += 1;
170    }
171
172    let directory_offset = u32::try_from(out.len()).unwrap_or(u32::MAX);
173    let directory_size = u32::try_from(directory.len()).unwrap_or(u32::MAX);
174    out.extend_from_slice(&directory);
175    // End of central directory.
176    out.extend_from_slice(&0x0605_4b50_u32.to_le_bytes());
177    out.extend_from_slice(&0_u16.to_le_bytes()); // this disk
178    out.extend_from_slice(&0_u16.to_le_bytes()); // directory's disk
179    out.extend_from_slice(&count.to_le_bytes());
180    out.extend_from_slice(&count.to_le_bytes());
181    out.extend_from_slice(&directory_size.to_le_bytes());
182    out.extend_from_slice(&directory_offset.to_le_bytes());
183    out.extend_from_slice(&0_u16.to_le_bytes()); // comment length
184    out
185}
186
187/// The ZIP checksum: CRC-32, reflected, polynomial `0xEDB8_8320`, a byte at
188/// a time through its table.
189pub(crate) fn crc32(data: &[u8]) -> u32 {
190    const TABLE: [u32; 256] = {
191        let mut table = [0_u32; 256];
192        let mut i = 0_u32;
193        while i < 256 {
194            let mut crc = i;
195            let mut k = 0;
196            while k < 8 {
197                crc = if crc & 1 != 0 {
198                    (crc >> 1) ^ 0xEDB8_8320
199                } else {
200                    crc >> 1
201                };
202                k += 1;
203            }
204            table[i as usize] = crc;
205            i += 1;
206        }
207        table
208    };
209    let mut crc = 0xFFFF_FFFF_u32;
210    for byte in data {
211        crc = (crc >> 8) ^ TABLE[((crc ^ u32::from(*byte)) & 0xFF) as usize];
212    }
213    !crc
214}
215
216// --- Reading -----------------------------------------------------------------
217
218/// One entry of the archive's central directory.
219struct Entry {
220    name: String,
221    method: u16,
222    flags: u16,
223    crc: u32,
224    compressed: usize,
225    size: usize,
226    header: usize,
227}
228
229fn u16_at(bytes: &[u8], at: usize) -> OgeomResult<u16> {
230    match at.checked_add(2).and_then(|end| bytes.get(at..end)) {
231        Some(b) => Ok(u16::from_le_bytes([b[0], b[1]])),
232        None => ogeom_bail!(Construction, "the archive is cut short"),
233    }
234}
235
236fn u32_at(bytes: &[u8], at: usize) -> OgeomResult<u32> {
237    match at.checked_add(4).and_then(|end| bytes.get(at..end)) {
238        Some(b) => Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])),
239        None => ogeom_bail!(Construction, "the archive is cut short"),
240    }
241}
242
243/// The central directory: every entry, with the sizes a streamed local
244/// header leaves as zero.
245fn directory(bytes: &[u8]) -> OgeomResult<Vec<Entry>> {
246    const END: u32 = 0x0605_4b50;
247    const LOCATOR: u32 = 0x0706_4b50;
248    const END64: u32 = 0x0606_4b50;
249    // The end record is the last thing in the file but for a comment of up
250    // to 65 535 bytes.
251    let floor = bytes.len().saturating_sub(22 + 0xFFFF);
252    let mut end = None;
253    let mut at = bytes.len().saturating_sub(22);
254    while at >= floor && bytes.len() >= 22 {
255        if u32_at(bytes, at)? == END {
256            end = Some(at);
257            break;
258        }
259        if at == 0 {
260            break;
261        }
262        at -= 1;
263    }
264    let Some(end) = end else {
265        ogeom_bail!(Construction, "these bytes are not a ZIP archive");
266    };
267    let zip64 = end >= 20 && u32_at(bytes, end - 20)? == LOCATOR;
268    // In a ZIP64 archive the classic record's disk numbers may be
269    // saturated like the rest of it; the ZIP64 record's are the ones read.
270    if !zip64 && (u16_at(bytes, end + 4)? != 0 || u16_at(bytes, end + 6)? != 0) {
271        ogeom_bail!(Construction, "the archive spans several disks");
272    }
273    let mut count = u64::from(u16_at(bytes, end + 10)?);
274    let mut offset = u64::from(u32_at(bytes, end + 16)?);
275    // A ZIP64 archive keeps the classic end record with its fields
276    // saturated, and the true ones in a ZIP64 end record its locator, just
277    // before the classic one, points to. Streaming writers emit it whatever
278    // the archive's size.
279    if zip64 {
280        let at = offset_in(bytes, u64_at(bytes, end - 20 + 8)?)?;
281        if u32_at(bytes, at)? != END64 {
282            ogeom_bail!(
283                Construction,
284                "the archive's ZIP64 locator points at no ZIP64 end record"
285            );
286        }
287        if u32_at(bytes, at + 16)? != 0 || u32_at(bytes, at + 20)? != 0 {
288            ogeom_bail!(Construction, "the archive spans several disks");
289        }
290        count = u64_at(bytes, at + 32)?;
291        offset = u64_at(bytes, at + 48)?;
292    } else if count == 0xFFFF || offset == 0xFFFF_FFFF {
293        ogeom_bail!(
294            Construction,
295            "the archive's end record is saturated and no ZIP64 record follows it"
296        );
297    }
298    let mut entries = Vec::new();
299    let mut at = offset_in(bytes, offset)?;
300    for _ in 0..count {
301        if u32_at(bytes, at)? != 0x0201_4b50 {
302            ogeom_bail!(Construction, "the archive's central directory is damaged");
303        }
304        let name_len = usize::from(u16_at(bytes, at + 28)?);
305        let extra_len = usize::from(u16_at(bytes, at + 30)?);
306        let comment_len = usize::from(u16_at(bytes, at + 32)?);
307        let Some(name) = bytes.get(at + 46..at + 46 + name_len) else {
308            ogeom_bail!(Construction, "the archive is cut short");
309        };
310        let name = String::from_utf8_lossy(name).into_owned();
311        let mut size = u64::from(u32_at(bytes, at + 24)?);
312        let mut compressed = u64::from(u32_at(bytes, at + 20)?);
313        let mut header = u64::from(u32_at(bytes, at + 42)?);
314        // A saturated size or offset is carried in the ZIP64 extra field,
315        // in the order uncompressed size, compressed size, header offset,
316        // each present only where its fixed field is saturated.
317        if [size, compressed, header].contains(&0xFFFF_FFFF) {
318            let Some(extra) = extra_field(bytes, at + 46 + name_len, extra_len, 0x0001) else {
319                ogeom_bail!(
320                    Construction,
321                    "the entry {name} has a saturated field and no ZIP64 extra"
322                );
323            };
324            let mut read = 0;
325            for field in [&mut size, &mut compressed, &mut header] {
326                if *field == 0xFFFF_FFFF {
327                    *field = u64_at(extra, read)?;
328                    read += 8;
329                }
330            }
331        }
332        entries.push(Entry {
333            name,
334            flags: u16_at(bytes, at + 8)?,
335            method: u16_at(bytes, at + 10)?,
336            crc: u32_at(bytes, at + 16)?,
337            compressed: offset_in(bytes, compressed)?,
338            size: to_usize(size)?,
339            header: offset_in(bytes, header)?,
340        });
341        at += 46 + name_len + extra_len + comment_len;
342    }
343    disjoint(&entries)?;
344    Ok(entries)
345}
346
347/// Refuse a directory whose entries share bytes. Each entry's local record
348/// is a 30-byte header, its name and extra field, then its data, and no two
349/// records of an archive overlap; reading only the directory, each record
350/// is taken as its header and data, which the whole record holds. Entries
351/// sharing bytes would let one compressed stream be inflated once per
352/// record naming it, so a small file could expand without bound; with the
353/// records apart, no package expands past the most its own bytes deflate
354/// from.
355fn disjoint(entries: &[Entry]) -> OgeomResult<()> {
356    let mut spans: Vec<(usize, usize, &str)> = entries
357        .iter()
358        .map(|e| {
359            let end = e.header.saturating_add(30).saturating_add(e.compressed);
360            (e.header, end, e.name.as_str())
361        })
362        .collect();
363    spans.sort_unstable_by_key(|&(start, _, _)| start);
364    for pair in spans.windows(2) {
365        let ((_, end, first), (start, _, second)) = (pair[0], pair[1]);
366        if start < end {
367            ogeom_bail!(
368                Construction,
369                "the entries {first} and {second} overlap in the archive"
370            );
371        }
372    }
373    Ok(())
374}
375
376fn u64_at(bytes: &[u8], at: usize) -> OgeomResult<u64> {
377    match at.checked_add(8).and_then(|end| bytes.get(at..end)) {
378        Some(b) => Ok(u64::from_le_bytes([
379            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
380        ])),
381        None => ogeom_bail!(Construction, "the archive is cut short"),
382    }
383}
384
385/// An offset or length within the archive, refused past its end: every
386/// such field names bytes the file must hold.
387fn offset_in(bytes: &[u8], value: u64) -> OgeomResult<usize> {
388    match usize::try_from(value) {
389        Ok(v) if v <= bytes.len() => Ok(v),
390        _ => ogeom_bail!(Construction, "the archive names an offset past its own end"),
391    }
392}
393
394fn to_usize(value: u64) -> OgeomResult<usize> {
395    match usize::try_from(value) {
396        Ok(v) => Ok(v),
397        Err(_) => ogeom_bail!(
398            Construction,
399            "the archive names an offset past this machine's reach"
400        ),
401    }
402}
403
404/// The data of the extra field with header `id` among the `len` bytes of
405/// extra fields starting at `at`.
406fn extra_field(bytes: &[u8], at: usize, len: usize, id: u16) -> Option<&[u8]> {
407    let fields = bytes.get(at..at + len)?;
408    let mut i = 0;
409    while i + 4 <= fields.len() {
410        let tag = u16::from_le_bytes([fields[i], fields[i + 1]]);
411        let size = usize::from(u16::from_le_bytes([fields[i + 2], fields[i + 3]]));
412        let data = fields.get(i + 4..i + 4 + size)?;
413        if tag == id {
414            return Some(data);
415        }
416        i += 4 + size;
417    }
418    None
419}
420
421/// An entry's bytes: stored ones as they are, deflated ones inflated, and
422/// either checked against the checksum the archive gives.
423fn contents(bytes: &[u8], entry: &Entry) -> OgeomResult<Vec<u8>> {
424    let name = &entry.name;
425    if entry.flags & 1 != 0 {
426        ogeom_bail!(Construction, "the entry {name} is encrypted");
427    }
428    if u32_at(bytes, entry.header)? != 0x0403_4b50 {
429        ogeom_bail!(
430            Construction,
431            "the entry {name} has no local header where the directory puts it"
432        );
433    }
434    let name_len = usize::from(u16_at(bytes, entry.header + 26)?);
435    let extra_len = usize::from(u16_at(bytes, entry.header + 28)?);
436    let data_at = entry.header + 30 + name_len + extra_len;
437    let Some(data) = data_at
438        .checked_add(entry.compressed)
439        .and_then(|end| bytes.get(data_at..end))
440    else {
441        ogeom_bail!(
442            Construction,
443            "the entry {name} runs past the end of the file"
444        );
445    };
446    let out = match entry.method {
447        0 => data.to_vec(),
448        8 => crate::inflate::inflate(data, entry.size)?,
449        method => ogeom_bail!(
450            Construction,
451            "the entry {name} is compressed with method {method}; only stored and deflated entries are read"
452        ),
453    };
454    if out.len() != entry.size || crc32(&out) != entry.crc {
455        ogeom_bail!(Construction, "the entry {name} does not match its checksum");
456    }
457    Ok(out)
458}
459
460/// Read every part of a package: the entry names and their bytes.
461///
462/// Stored and deflated entries are read, each checked against its
463/// checksum, from classic and ZIP64 archives alike; the central directory's
464/// sizes are the ones trusted, since a streamed entry's local header leaves
465/// them zero or saturated. Any other compression method, an encrypted
466/// entry, entries that overlap one another, or an archive spanning several
467/// disks is refused by name rather than half-read.
468///
469/// Every entry is held in memory at once, so the package is held to
470/// [`PackageLimits::default`]: [`read_package_within`] reads one under
471/// other limits, or [`PackageLimits::NONE`].
472///
473/// # Errors
474///
475/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
476/// bytes are not a ZIP archive, an entry is damaged, it is one of the
477/// refusals above, or it is past the default limits.
478pub fn read_package(bytes: &[u8]) -> OgeomResult<Vec<(String, Vec<u8>)>> {
479    read_package_within(bytes, PackageLimits::default())
480}
481
482/// How much of a package [`read_package_within`] may read: how many
483/// entries, directories aside, and how many bytes they inflate to in all.
484///
485/// Both are checked against the central directory before any entry is
486/// inflated. The sizes it gives are the ones each entry is then held to
487/// exactly, so a package within its limits never holds more.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub struct PackageLimits {
490    /// The most entries the package may have.
491    pub entries: usize,
492    /// The most bytes its entries may inflate to, together.
493    pub bytes: u64,
494}
495
496impl PackageLimits {
497    /// No limit past the archive's own: entries may not overlap, so a
498    /// package expands at most as far as its own bytes deflate from, about
499    /// a thousand times its size.
500    pub const NONE: Self = Self {
501        entries: usize::MAX,
502        bytes: u64::MAX,
503    };
504
505    /// 4096 entries and 4 GiB in all, what [`read_package`] holds a package
506    /// to. The packages in the corpus have at most 4 entries and inflate to
507    /// at most 315 457 bytes; these limits are a thousand times the one and
508    /// over thirteen thousand times the other.
509    pub const DEFAULT: Self = Self {
510        entries: 4096,
511        bytes: 4 << 30,
512    };
513}
514
515impl Default for PackageLimits {
516    /// [`PackageLimits::DEFAULT`].
517    fn default() -> Self {
518        Self::DEFAULT
519    }
520}
521
522/// [`read_package`], refusing a package past `limits` before reading any
523/// entry.
524///
525/// # Errors
526///
527/// As [`read_package`], and
528/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
529/// package has more entries or inflates to more bytes than `limits` allow.
530pub fn read_package_within(
531    bytes: &[u8],
532    limits: PackageLimits,
533) -> OgeomResult<Vec<(String, Vec<u8>)>> {
534    let entries = directory(bytes)?;
535    let files: Vec<&Entry> = entries.iter().filter(|e| !e.name.ends_with('/')).collect();
536    if files.len() > limits.entries {
537        ogeom_bail!(
538            Construction,
539            "the package has {} entries, past the {} allowed",
540            files.len(),
541            limits.entries
542        );
543    }
544    let total = files
545        .iter()
546        .map(|e| u64::try_from(e.size).unwrap_or(u64::MAX))
547        .fold(0_u64, u64::saturating_add);
548    if total > limits.bytes {
549        ogeom_bail!(
550            Construction,
551            "the package inflates to {total} bytes, past the {} allowed",
552            limits.bytes
553        );
554    }
555    files
556        .into_iter()
557        .map(|e| Ok((e.name.clone(), contents(bytes, e)?)))
558        .collect()
559}
560
561/// What a 3MF object is for, as its `type` attribute says.
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563pub enum ObjectType {
564    /// A part to be made: `model`, the default.
565    Model,
566    /// A support the designer modelled as solid: `solidsupport`.
567    SolidSupport,
568    /// Support structure, not part of the design: `support`.
569    Support,
570    /// An open surface, not a solid: `surface`.
571    Surface,
572    /// Anything else the file carries: `other`.
573    Other,
574}
575
576/// One object of a package, placed as its build item places it.
577#[derive(Debug, Clone)]
578pub struct ThreeMfObject {
579    /// The object's name, where it has one.
580    pub name: Option<String>,
581    /// What the object is for. Every type is read; the import's warnings
582    /// say when one is not a part.
583    pub object_type: ObjectType,
584    /// The mesh in millimetres, components flattened into it and the build
585    /// item's transform applied.
586    pub mesh: Triangulation,
587    /// The object's colour as the file writes it (sRGB, RGBA in `[0, 1]`),
588    /// where every triangle carries the same one.
589    pub colour: Option<[f64; 4]>,
590    /// What the object is for, as the metadata of the package names its
591    /// role; [`ObjectRole::Model`] where nothing does.
592    pub role: ObjectRole,
593    /// The colours of the mesh as the file writes them (sRGB, RGBA in
594    /// `[0, 1]`): from colour groups, base materials or textures sampled
595    /// at each corner's coordinates. Given where every triangle has one.
596    pub colours: Option<MeshColours>,
597    /// The volumes the mesh is made of, in order, each a run of its
598    /// triangles: one per mesh the object's components flatten, or per
599    /// triangle range the package's metadata divides a mesh into. Together
600    /// they cover every triangle of [`ThreeMfObject::mesh`].
601    pub volumes: Vec<ThreeMfVolume>,
602}
603
604/// What a volume is for, as the metadata a package's writer keeps names
605/// its role.
606///
607/// The core specification has no word for this. Tools that prepare a model
608/// for printing write it in their own vocabulary: as metadata of an object
609/// in the model (`<metadata name="…">`), or in configuration parts under
610/// `Metadata/` (`*.config`) that describe each object's volumes by an
611/// attribute or a `key`/`value` pair. Those are read by name, whatever
612/// namespace prefix the name carries: a `volume_type` or `subtype` of
613/// `ModelPart`/`normal_part`, `ParameterModifier`/`modifier_part`,
614/// `NegativeVolume`/`negative_part`, `SupportBlocker` or `SupportEnforcer`
615/// (case, underscores and hyphens aside); and the flags `modifier`,
616/// `infill_mesh` and `cutting_mesh` (a modifier), `anti_overhang_mesh` (a
617/// support blocker) and `support_mesh` (a support enforcer) set true.
618#[derive(Debug, Clone, PartialEq, Eq)]
619pub enum ObjectRole {
620    /// Part of what is to be made.
621    Model,
622    /// A volume that changes how the parts it overlaps are made, and is
623    /// not made itself.
624    Modifier,
625    /// A volume where no support is to be generated.
626    SupportBlocker,
627    /// A volume where support is to be generated.
628    SupportEnforcer,
629    /// A volume cut out of the parts it overlaps.
630    NegativeVolume,
631    /// A role the metadata names that is none of these, as written.
632    Other(String),
633}
634
635impl ObjectRole {
636    fn describe(&self) -> String {
637        match self {
638            Self::Model => "a part".to_owned(),
639            Self::Modifier => "a modifier".to_owned(),
640            Self::SupportBlocker => "a support blocker".to_owned(),
641            Self::SupportEnforcer => "a support enforcer".to_owned(),
642            Self::NegativeVolume => "a negative volume".to_owned(),
643            Self::Other(role) => format!("of role {role}"),
644        }
645    }
646}
647
648/// One volume of an object: a run of the object's triangles, and what it
649/// is for.
650#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct ThreeMfVolume {
652    /// The volume's name, from the metadata or else its mesh object.
653    pub name: Option<String>,
654    /// What the volume is for.
655    pub role: ObjectRole,
656    /// Its triangles, as indices into the object's mesh.
657    pub triangles: std::ops::Range<usize>,
658}
659
660/// What [`read_3mf`] found.
661#[derive(Debug, Clone)]
662pub struct ThreeMfImport {
663    /// One object per build item, in build order.
664    pub objects: Vec<ThreeMfObject>,
665    /// What was read with a caveat, or skipped: required extensions this
666    /// reader does not know, per-triangle colours dropped, objects that are
667    /// not parts.
668    pub warnings: Vec<String>,
669}
670
671const CORE: &str = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
672const MATERIAL: &str = "http://schemas.microsoft.com/3dmanufacturing/material/2015/02";
673const PRODUCTION: &str = "http://schemas.microsoft.com/3dmanufacturing/production/2015/06";
674const MODEL_RELATIONSHIP: &str = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
675/// The extensions whose required use changes nothing this reader returns:
676/// production (read; it is how multi-part packages reference their
677/// objects), materials (read as far as colour), and the slicers' own.
678const UNDERSTOOD: [&str; 2] = [PRODUCTION, MATERIAL];
679
680/// Read a 3MF package into placed meshes.
681///
682/// The start part is the one `_rels/.rels` names as the 3D model. Its
683/// objects are meshes or assemblies of components; a component, or a
684/// build item, may name an object in another model part of the package
685/// through the production extension's `path`, as slicers write every
686/// object to its own part. Each build item comes back as one mesh, its
687/// components flattened and every transform applied, scaled from the
688/// model's unit to millimetres. A transform that mirrors has its triangles
689/// rewound, so every mesh keeps its outward winding. Vertices the
690/// flattening brings together within the confusion tolerance are welded.
691///
692/// A uniform object colour is kept, from a base material or a colour
693/// group. Colours that vary across the triangles are read into
694/// [`ThreeMfObject::colours`] where every triangle has one: from colour
695/// groups and base materials, and from textures (PNG or JPEG parts of the
696/// package) sampled by the nearest texel at each corner's coordinates,
697/// tiled as the texture's `tilestyleu` and `tilestylev` say (`none` read as
698/// `clamp`). Composite materials and multi-properties are passed over with
699/// a warning, and so is a texture that cannot be decoded.
700///
701/// The roles of objects and volumes (modifiers, support blockers and
702/// enforcers, negative volumes) are read from the metadata of the package
703/// as [`ObjectRole`] lays out, and each object's [`ThreeMfObject::volumes`]
704/// says which of its triangles each volume is. A configuration part that
705/// cannot be read is a warning.
706///
707/// # Errors
708///
709/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
710/// archive cannot be read (see [`read_package`]), a model part is not
711/// well-formed, a reference names an object that is not there or contains
712/// itself, a triangle names a vertex that is not there, or the model
713/// requires the secure-content extension.
714pub fn read_3mf(bytes: &[u8], tol: Tolerances) -> OgeomResult<ThreeMfImport> {
715    let entries = directory(bytes)?;
716    let find = |path: &str| -> Option<&Entry> {
717        let wanted = part_key(path);
718        entries.iter().find(|e| part_key(&e.name) == wanted)
719    };
720    let mut warnings = Vec::new();
721    let start = match find("_rels/.rels") {
722        Some(rels) => {
723            let text = String::from_utf8_lossy(&contents(bytes, rels)?).into_owned();
724            start_part(&text)?
725        }
726        None => None,
727    }
728    .unwrap_or_else(|| "/3D/3dmodel.model".to_owned());
729
730    let mut parts: FastMap<String, Part> = FastMap::default();
731    let mut pending = vec![start.clone()];
732    while let Some(path) = pending.pop() {
733        let key = part_key(&path);
734        if parts.contains_key(&key) {
735            continue;
736        }
737        let Some(entry) = find(&path) else {
738            ogeom_bail!(Construction, "the package has no model part {path}");
739        };
740        let raw = contents(bytes, entry)?;
741        let Ok(text) = std::str::from_utf8(&raw) else {
742            ogeom_bail!(Construction, "the model part {path} is not UTF-8");
743        };
744        let part = parse_part(text, &path, &mut warnings)?;
745        pending.extend(part.referenced());
746        parts.insert(key, part);
747    }
748
749    // Textures, each decoded once; one that cannot be is a warning, and
750    // the triangles it colours go without.
751    let mut budget = crate::image::Budget::new();
752    for part in parts.values_mut() {
753        for def in part.textures.values_mut() {
754            let decoded = match find(&def.path) {
755                None => Err(format!("the package has no texture part {}", def.path)),
756                Some(entry) => contents(bytes, entry)
757                    .and_then(|raw| budget.decode(&raw))
758                    .map_err(|e| format!("the texture {} could not be decoded: {e}", def.path)),
759            };
760            match decoded {
761                Ok(image) => def.image = Some(image),
762                Err(warning) => warnings.push(warning),
763            }
764        }
765    }
766    // The configuration parts a writer keeps beside the model: read for the
767    // roles of the volumes, and passed over with a warning if malformed.
768    let mut config: FastMap<u32, ConfigObject> = FastMap::default();
769    for entry in &entries {
770        let key = part_key(&entry.name);
771        if !(key.starts_with("metadata/") && key.ends_with(".config")) {
772            continue;
773        }
774        let read = contents(bytes, entry).and_then(|raw| match std::str::from_utf8(&raw) {
775            Ok(text) => parse_config(text),
776            Err(_) => ogeom_bail!(Construction, "it is not UTF-8"),
777        });
778        match read {
779            Ok(objects) => {
780                for (id, object) in objects {
781                    let held = config.entry(id).or_default();
782                    held.pairs.extend(object.pairs);
783                    held.volumes.extend(object.volumes);
784                }
785            }
786            Err(e) => warnings.push(format!(
787                "the configuration part {} could not be read: {e}",
788                entry.name
789            )),
790        }
791    }
792
793    let context = Context {
794        parts: &parts,
795        start: part_key(&start),
796        config: &config,
797    };
798    let root = &parts[&part_key(&start)];
799    let scale = root.scale;
800    let mut objects = Vec::with_capacity(root.build.len());
801    for item in &root.build {
802        let path = item.path.clone().unwrap_or_else(|| start.clone());
803        let top = object(&parts, &path, item.object)?;
804        let outer = Tag {
805            name: None,
806            role: ObjectRole::Model,
807        };
808        let role = context.tag(&path, item.object, top, &outer).role;
809        let mut flat = Flat::default();
810        flatten(
811            &context,
812            &path,
813            item.object,
814            item.transform,
815            &mut flat,
816            0,
817            &outer,
818        )?;
819        let name = top.name.clone();
820        let label = name
821            .clone()
822            .unwrap_or_else(|| format!("object {}", item.object));
823        match top.object_type {
824            ObjectType::Model | ObjectType::SolidSupport => {}
825            ObjectType::Support => warnings.push(format!("{label} is a support structure")),
826            ObjectType::Surface => warnings.push(format!("{label} is a surface, not a solid")),
827            ObjectType::Other => warnings.push(format!("{label} is of type other, not a part")),
828        }
829        if role != ObjectRole::Model {
830            warnings.push(format!("{label} is {}, not a part", role.describe()));
831        }
832        if flat.skipped_properties {
833            warnings.push(format!(
834                "{label} carries composite materials or multiple properties; they are not read"
835            ));
836        }
837        if flat.undecoded {
838            warnings.push(format!(
839                "{label} is textured with an image that could not be decoded"
840            ));
841        }
842        let several = flat.colours.len() > 1;
843        let colour = match flat.colours.as_slice() {
844            [] => None,
845            [only] => *only,
846            _ => resolve_colour(parts.get(&part_key(&path)), top.pid, top.pindex),
847        };
848        if flat.degenerate > 0 {
849            warnings.push(format!(
850                "{label}: {} triangles repeat a vertex and were dropped",
851                flat.degenerate
852            ));
853        }
854        let Welded {
855            mesh,
856            corners,
857            volumes,
858        } = flat.into_mesh(scale, tol.confusion());
859        let coloured = corners.iter().filter(|c| c.is_some()).count();
860        let colours = match corners.into_iter().collect::<Option<Vec<_>>>() {
861            Some(corners) if !corners.is_empty() => Some(MeshColours::from_corners(
862                &mesh.triangles,
863                mesh.positions.len(),
864                corners,
865            )),
866            _ => {
867                if coloured > 0 || several {
868                    warnings.push(format!(
869                        "{label} carries several colours and not every triangle has one; the per-triangle colours are dropped"
870                    ));
871                }
872                None
873            }
874        };
875        for volume in &volumes {
876            if volume.role != ObjectRole::Model && volume.role != role {
877                let named = volume
878                    .name
879                    .as_ref()
880                    .map_or_else(String::new, |n| format!(" {n}"));
881                warnings.push(format!(
882                    "{label} holds the volume{named}, {}; its triangles are in the mesh",
883                    volume.role.describe()
884                ));
885            }
886        }
887        objects.push(ThreeMfObject {
888            name,
889            object_type: top.object_type,
890            mesh,
891            colour,
892            role,
893            colours,
894            volumes,
895        });
896    }
897    Ok(ThreeMfImport { objects, warnings })
898}
899
900/// A part name as the archive and every reference compare it: without the
901/// leading slash, percent escapes decoded, and case folded, as the package
902/// conventions make part names case-insensitive.
903fn part_key(path: &str) -> String {
904    let path = path.trim_start_matches('/');
905    let bytes = path.as_bytes();
906    let mut out = Vec::with_capacity(bytes.len());
907    let mut i = 0;
908    while i < bytes.len() {
909        if bytes[i] == b'%'
910            && let Some(hex) = path.get(i + 1..i + 3)
911            && let Ok(byte) = u8::from_str_radix(hex, 16)
912        {
913            out.push(byte);
914            i += 3;
915            continue;
916        }
917        out.push(bytes[i]);
918        i += 1;
919    }
920    String::from_utf8_lossy(&out).to_lowercase()
921}
922
923/// The target of the package's 3D-model relationship.
924fn start_part(rels: &str) -> OgeomResult<Option<String>> {
925    let mut reader = xml::Reader::new(rels);
926    while let Some(event) = reader.next()? {
927        if let xml::Event::Start(start) = event
928            && start.local == "Relationship"
929            && start.get("Type") == Some(MODEL_RELATIONSHIP)
930            && let Some(target) = start.get("Target")
931        {
932            return Ok(Some(target.to_owned()));
933        }
934    }
935    Ok(None)
936}
937
938/// A 3MF transform: the three rows the format writes, image of each axis,
939/// then the translation; a point is carried as a row vector.
940#[derive(Debug, Clone, Copy)]
941struct Affine {
942    axes: [Vector; 3],
943    translation: Vector,
944}
945
946impl Affine {
947    const IDENTITY: Self = Self {
948        axes: [Vector::X, Vector::Y, Vector::Z],
949        translation: Vector::ZERO,
950    };
951
952    fn parse(text: &str) -> OgeomResult<Self> {
953        let values: Vec<f64> = text
954            .split_ascii_whitespace()
955            .map(str::parse)
956            .collect::<Result<_, _>>()
957            .or_else(|_| ogeom_bail!(Construction, "the transform {text} is not twelve numbers"))?;
958        let [a, b, c, d, e, f, g, h, i, x, y, z] = values[..] else {
959            ogeom_bail!(Construction, "the transform {text} is not twelve numbers");
960        };
961        Ok(Self {
962            axes: [
963                Vector::new(a, b, c),
964                Vector::new(d, e, f),
965                Vector::new(g, h, i),
966            ],
967            translation: Vector::new(x, y, z),
968        })
969    }
970
971    fn vector(self, v: Vector) -> Vector {
972        self.axes[0] * v.x + self.axes[1] * v.y + self.axes[2] * v.z
973    }
974
975    fn point(self, p: Point) -> Point {
976        Point::ORIGIN + self.vector(p - Point::ORIGIN) + self.translation
977    }
978
979    /// `inner` first, then `self`.
980    fn after(self, inner: Self) -> Self {
981        Self {
982            axes: inner.axes.map(|a| self.vector(a)),
983            translation: self.vector(inner.translation) + self.translation,
984        }
985    }
986
987    fn mirrors(self) -> bool {
988        self.axes[0].dot(self.axes[1].cross(self.axes[2])) < 0.0
989    }
990}
991
992/// A component or a build item: an object, where it lives, and where it
993/// is placed.
994struct Reference {
995    path: Option<String>,
996    object: u32,
997    transform: Affine,
998}
999
1000enum Content {
1001    Mesh {
1002        positions: Vec<Point>,
1003        triangles: Vec<[u32; 3]>,
1004        /// Each triangle's property, `(group, index at each corner)`,
1005        /// where it names one.
1006        properties: Vec<Option<(u32, [u32; 3])>>,
1007    },
1008    Components(Vec<Reference>),
1009}
1010
1011struct ObjectDef {
1012    name: Option<String>,
1013    object_type: ObjectType,
1014    pid: Option<u32>,
1015    pindex: Option<u32>,
1016    content: Content,
1017    /// The `(name, value)` pairs of the object's metadata group.
1018    metadata: Vec<(String, String)>,
1019}
1020
1021/// A property group of a model part.
1022enum Group {
1023    /// Colours by index: a colour group, or base materials' display
1024    /// colours.
1025    Colours(Vec<[f64; 4]>),
1026    /// Texture coordinates by index into the texture of that id.
1027    Texture {
1028        texture: u32,
1029        coords: Vec<(f64, f64)>,
1030    },
1031    /// A group this reader does not read: composites, multi-properties.
1032    Unread,
1033}
1034
1035/// A `texture2d`: the image part it names, how it tiles, and the image
1036/// once decoded.
1037struct TextureDef {
1038    path: String,
1039    wrap: [Wrap; 2],
1040    image: Option<Image>,
1041}
1042
1043/// One model part: its objects, property groups and build.
1044struct Part {
1045    scale: f64,
1046    objects: FastMap<u32, ObjectDef>,
1047    /// Property groups by id.
1048    groups: FastMap<u32, Group>,
1049    textures: FastMap<u32, TextureDef>,
1050    build: Vec<Reference>,
1051}
1052
1053impl Part {
1054    /// The other parts this one's components and items name.
1055    fn referenced(&self) -> Vec<String> {
1056        let components = self.objects.values().flat_map(|o| match &o.content {
1057            Content::Components(c) => c.iter().collect::<Vec<_>>(),
1058            Content::Mesh { .. } => Vec::new(),
1059        });
1060        components
1061            .chain(&self.build)
1062            .filter_map(|r| r.path.clone())
1063            .collect()
1064    }
1065}
1066
1067fn number<T: std::str::FromStr>(start: &xml::Start<'_>, name: &str) -> OgeomResult<Option<T>> {
1068    match start.get(name) {
1069        None => Ok(None),
1070        Some(text) => match text.trim().parse() {
1071            Ok(v) => Ok(Some(v)),
1072            Err(_) => ogeom_bail!(
1073                Construction,
1074                "the {} attribute {name}=\"{text}\" is not a number",
1075                start.local
1076            ),
1077        },
1078    }
1079}
1080
1081fn required<T: std::str::FromStr>(start: &xml::Start<'_>, name: &str) -> OgeomResult<T> {
1082    match number(start, name)? {
1083        Some(v) => Ok(v),
1084        None => ogeom_bail!(Construction, "a {} has no {name}", start.local),
1085    }
1086}
1087
1088fn parse_part(text: &str, path: &str, warnings: &mut Vec<String>) -> OgeomResult<Part> {
1089    let mut reader = xml::Reader::new(text);
1090    let mut part = Part {
1091        scale: 1.0,
1092        objects: FastMap::default(),
1093        groups: FastMap::default(),
1094        textures: FastMap::default(),
1095        build: Vec::new(),
1096    };
1097    // Where the reader is: the element names from the model down.
1098    let mut stack: Vec<(bool, String)> = Vec::new();
1099    let mut current: Option<(u32, ObjectDef)> = None;
1100    let mut group: Option<(u32, Group)> = None;
1101    while let Some(event) = reader.next()? {
1102        let start = match event {
1103            xml::Event::End => {
1104                if let Some((core, name)) = stack.pop() {
1105                    match (core, name.as_str()) {
1106                        (true, "object") => {
1107                            if let Some((id, def)) = current.take() {
1108                                part.objects.insert(id, def);
1109                            }
1110                        }
1111                        (_, "basematerials" | "colorgroup" | "texture2dgroup") => {
1112                            if let Some((id, g)) = group.take() {
1113                                part.groups.insert(id, g);
1114                            }
1115                        }
1116                        _ => {}
1117                    }
1118                }
1119                continue;
1120            }
1121            xml::Event::Start(start) => start,
1122        };
1123        let ns = start.namespace.as_ref();
1124        let core = ns == CORE;
1125        match (ns, start.local, stack.len()) {
1126            (CORE, "model", 0) => {
1127                part.scale = unit_scale(start.get("unit").unwrap_or("millimeter"))?;
1128                for prefix in start
1129                    .get("requiredextensions")
1130                    .unwrap_or("")
1131                    .split_whitespace()
1132                {
1133                    let uri = namespace_of(text, prefix);
1134                    if uri.contains("securecontent") {
1135                        ogeom_bail!(
1136                            Construction,
1137                            "the model {path} requires the secure-content extension; its meshes are encrypted"
1138                        );
1139                    }
1140                    if !UNDERSTOOD.contains(&uri.as_str()) {
1141                        warnings.push(format!(
1142                            "the model {path} requires the extension {uri}, which is not read"
1143                        ));
1144                    }
1145                }
1146            }
1147            (_, _, 0) => ogeom_bail!(Construction, "the part {path} is not a 3MF model"),
1148            (CORE, "object", _) => {
1149                let id = required(&start, "id")?;
1150                let object_type = match start.get("type").unwrap_or("model") {
1151                    "model" => ObjectType::Model,
1152                    "solidsupport" => ObjectType::SolidSupport,
1153                    "support" => ObjectType::Support,
1154                    "surface" => ObjectType::Surface,
1155                    _ => ObjectType::Other,
1156                };
1157                current = Some((
1158                    id,
1159                    ObjectDef {
1160                        name: start.get("name").map(str::to_owned),
1161                        object_type,
1162                        pid: number(&start, "pid")?,
1163                        pindex: number(&start, "pindex")?,
1164                        content: Content::Components(Vec::new()),
1165                        metadata: Vec::new(),
1166                    },
1167                ));
1168            }
1169            (CORE, "mesh", _) => {
1170                if let Some((_, def)) = current.as_mut() {
1171                    def.content = Content::Mesh {
1172                        positions: Vec::new(),
1173                        triangles: Vec::new(),
1174                        properties: Vec::new(),
1175                    };
1176                }
1177            }
1178            (CORE, "vertex", _) => {
1179                if let Some((_, def)) = current.as_mut()
1180                    && let Content::Mesh { positions, .. } = &mut def.content
1181                {
1182                    positions.push(Point::new(
1183                        required(&start, "x")?,
1184                        required(&start, "y")?,
1185                        required(&start, "z")?,
1186                    ));
1187                }
1188            }
1189            (CORE, "triangle", _) => {
1190                if let Some((_, def)) = current.as_mut() {
1191                    let pid = number(&start, "pid")?.or(def.pid);
1192                    let first = number(&start, "p1")?;
1193                    // The second and third corners take the first's property
1194                    // unless they name their own.
1195                    let corner = |name: &str, p1: u32| {
1196                        first
1197                            .and(start.get(name))
1198                            .and_then(|t| t.trim().parse().ok())
1199                            .unwrap_or(p1)
1200                    };
1201                    let index = first
1202                        .or(def.pindex)
1203                        .map(|p1| [p1, corner("p2", p1), corner("p3", p1)]);
1204                    if let Content::Mesh {
1205                        triangles,
1206                        properties,
1207                        ..
1208                    } = &mut def.content
1209                    {
1210                        triangles.push([
1211                            required(&start, "v1")?,
1212                            required(&start, "v2")?,
1213                            required(&start, "v3")?,
1214                        ]);
1215                        properties.push(pid.zip(index));
1216                    }
1217                }
1218            }
1219            (CORE, "component", _) => {
1220                let reference = reference(&start)?;
1221                if let Some((_, def)) = current.as_mut()
1222                    && let Content::Components(list) = &mut def.content
1223                {
1224                    list.push(reference);
1225                }
1226            }
1227            (CORE, "item", _) => part.build.push(reference(&start)?),
1228            (CORE, "basematerials", _) | (MATERIAL, "colorgroup", _) => {
1229                group = Some((required(&start, "id")?, Group::Colours(Vec::new())));
1230            }
1231            (CORE, "base", _) => {
1232                if let Some((_, Group::Colours(colours))) = group.as_mut() {
1233                    colours.push(parse_colour(
1234                        start.get("displaycolor").unwrap_or("#FFFFFF"),
1235                    )?);
1236                }
1237            }
1238            (MATERIAL, "color", _) => {
1239                if let Some((_, Group::Colours(colours))) = group.as_mut() {
1240                    colours.push(parse_colour(start.get("color").unwrap_or("#FFFFFF"))?);
1241                }
1242            }
1243            (MATERIAL, "texture2d", _) => {
1244                let wrap = |name: &str| match start.get(name).unwrap_or("wrap") {
1245                    "mirror" => Wrap::Mirror,
1246                    "clamp" | "none" => Wrap::Clamp,
1247                    _ => Wrap::Repeat,
1248                };
1249                if let Some(id) = number::<u32>(&start, "id")? {
1250                    part.textures.insert(
1251                        id,
1252                        TextureDef {
1253                            path: start.get("path").unwrap_or("").to_owned(),
1254                            wrap: [wrap("tilestyleu"), wrap("tilestylev")],
1255                            image: None,
1256                        },
1257                    );
1258                }
1259            }
1260            // A texture group that does not hold together is one this
1261            // reader does not read, not a reason to refuse the package.
1262            (MATERIAL, "texture2dgroup", _) => {
1263                if let Some(id) = number::<u32>(&start, "id")? {
1264                    let texture = start.get("texid").and_then(|t| t.trim().parse().ok());
1265                    group = Some((
1266                        id,
1267                        texture.map_or(Group::Unread, |texture| Group::Texture {
1268                            texture,
1269                            coords: Vec::new(),
1270                        }),
1271                    ));
1272                }
1273            }
1274            (MATERIAL, "tex2coord", _) => {
1275                if let Some((_, g @ Group::Texture { .. })) = group.as_mut() {
1276                    let at = |name: &str| {
1277                        start
1278                            .get(name)
1279                            .and_then(|t| t.trim().parse::<f64>().ok())
1280                            .filter(|x| x.is_finite())
1281                    };
1282                    match (at("u"), at("v"), &mut *g) {
1283                        (Some(u), Some(v), Group::Texture { coords, .. }) => coords.push((u, v)),
1284                        _ => *g = Group::Unread,
1285                    }
1286                }
1287            }
1288            (MATERIAL, _, _) => {
1289                // Textures, composites and multi-properties: their ids are
1290                // known, so a triangle naming one is reported, not failed.
1291                if let Some(id) = number::<u32>(&start, "id")? {
1292                    part.groups.insert(id, Group::Unread);
1293                }
1294                if !start.empty {
1295                    reader.skip()?;
1296                    continue;
1297                }
1298            }
1299            (CORE, "metadatagroup", _)
1300                if current.is_some() && stack.last().is_some_and(|(c, n)| *c && n == "object") => {}
1301            (CORE, "metadata", _)
1302                if current.is_some()
1303                    && stack
1304                        .last()
1305                        .is_some_and(|(c, n)| *c && n == "metadatagroup") =>
1306            {
1307                let name = start.get("name").unwrap_or("").to_owned();
1308                let value = reader.text().into_owned();
1309                if let Some((_, def)) = current.as_mut() {
1310                    def.metadata.push((name, value));
1311                }
1312                if !start.empty {
1313                    reader.skip()?;
1314                    continue;
1315                }
1316            }
1317            (CORE, "metadata" | "metadatagroup", _) if !start.empty => {
1318                reader.skip()?;
1319                continue;
1320            }
1321            _ => {}
1322        }
1323        // An empty element still ends, and its end pops it.
1324        stack.push((core, start.local.to_owned()));
1325    }
1326    Ok(part)
1327}
1328
1329fn reference(start: &xml::Start<'_>) -> OgeomResult<Reference> {
1330    Ok(Reference {
1331        path: start.get_in(PRODUCTION, "path").map(str::to_owned),
1332        object: required(start, "objectid")?,
1333        transform: match start.get("transform") {
1334            Some(text) => Affine::parse(text)?,
1335            None => Affine::IDENTITY,
1336        },
1337    })
1338}
1339
1340/// The URI a prefix is declared as on the model element, for naming a
1341/// required extension.
1342fn namespace_of(text: &str, prefix: &str) -> String {
1343    let declaration = format!("xmlns:{prefix}=");
1344    text.find(&declaration)
1345        .and_then(|i| {
1346            let rest = &text[i + declaration.len()..];
1347            let quote = rest.chars().next()?;
1348            let rest = &rest[1..];
1349            rest.find(quote).map(|end| rest[..end].to_owned())
1350        })
1351        .unwrap_or_else(|| prefix.to_owned())
1352}
1353
1354fn unit_scale(unit: &str) -> OgeomResult<f64> {
1355    Ok(match unit {
1356        "micron" => 1e-3,
1357        "millimeter" => 1.0,
1358        "centimeter" => 10.0,
1359        "inch" => 25.4,
1360        "foot" => 304.8,
1361        "meter" => 1000.0,
1362        _ => ogeom_bail!(Construction, "the model unit {unit} is not one 3MF defines"),
1363    })
1364}
1365
1366/// `#RRGGBB` or `#RRGGBBAA`, to RGBA in `[0, 1]`.
1367fn parse_colour(text: &str) -> OgeomResult<[f64; 4]> {
1368    let hex = text.trim().trim_start_matches('#');
1369    let channel = |i: usize| -> Option<f64> {
1370        let byte = u8::from_str_radix(hex.get(i..i + 2)?, 16).ok()?;
1371        Some(f64::from(byte) / 255.0)
1372    };
1373    match (hex.len(), channel(0), channel(2), channel(4)) {
1374        (6, Some(r), Some(g), Some(b)) => Ok([r, g, b, 1.0]),
1375        (8, Some(r), Some(g), Some(b)) => Ok([r, g, b, channel(6).unwrap_or(1.0)]),
1376        _ => ogeom_bail!(
1377            Construction,
1378            "the colour {text} is not #RRGGBB or #RRGGBBAA"
1379        ),
1380    }
1381}
1382
1383fn object<'p>(parts: &'p FastMap<String, Part>, path: &str, id: u32) -> OgeomResult<&'p ObjectDef> {
1384    match parts.get(&part_key(path)).and_then(|p| p.objects.get(&id)) {
1385        Some(def) => Ok(def),
1386        None => ogeom_bail!(Construction, "the model {path} has no object {id}"),
1387    }
1388}
1389
1390/// A colour a property names: `None` for none, or for a group this reader
1391/// does not read.
1392fn resolve_colour(part: Option<&Part>, pid: Option<u32>, index: Option<u32>) -> Option<[f64; 4]> {
1393    match part?.groups.get(&pid?)? {
1394        Group::Colours(colours) => colours.get(index? as usize).copied(),
1395        _ => None,
1396    }
1397}
1398
1399/// The meshes of one build item, gathered as they flatten.
1400#[derive(Default)]
1401struct Flat {
1402    positions: Vec<Point>,
1403    triangles: Vec<[u32; 3]>,
1404    /// Every distinct colour a triangle's first corner carries, `None`
1405    /// among them for a triangle that carries none.
1406    colours: Vec<Option<[f64; 4]>>,
1407    /// Each triangle's colour at its three corners, where it has one.
1408    corners: Vec<Option<[[f64; 4]; 3]>>,
1409    /// Each triangle's volume, an index into `volumes`.
1410    volume_of: Vec<usize>,
1411    volumes: Vec<(Option<String>, ObjectRole)>,
1412    skipped_properties: bool,
1413    /// Textures a triangle names that could not be decoded.
1414    undecoded: bool,
1415    degenerate: usize,
1416}
1417
1418/// Components nest; past this depth a reference is taken to contain itself.
1419const DEPTH: usize = 64;
1420
1421/// What the package's configuration parts say of one object of the start
1422/// part: its own `(key, value)` pairs, and its volumes'.
1423#[derive(Default)]
1424struct ConfigObject {
1425    pairs: Vec<(String, String)>,
1426    volumes: Vec<ConfigVolume>,
1427}
1428
1429/// A volume a configuration part describes: a range of the object's mesh
1430/// triangles, first and last inclusive, or a component's object id.
1431#[derive(Default)]
1432struct ConfigVolume {
1433    triangles: Option<(usize, usize)>,
1434    object: Option<u32>,
1435    pairs: Vec<(String, String)>,
1436}
1437
1438/// The role and name a volume carries down from what encloses it.
1439#[derive(Clone)]
1440struct Tag {
1441    name: Option<String>,
1442    role: ObjectRole,
1443}
1444
1445impl Tag {
1446    /// This tag, overridden by what `pairs` say.
1447    fn with(&self, pairs: &[(String, String)]) -> Self {
1448        Self {
1449            name: value_of(pairs, "name")
1450                .map(str::to_owned)
1451                .or_else(|| self.name.clone()),
1452            role: role_of(pairs).unwrap_or_else(|| self.role.clone()),
1453        }
1454    }
1455}
1456
1457/// A key without its namespace prefix, lower-cased.
1458fn bare_key(key: &str) -> String {
1459    key.rsplit(':').next().unwrap_or(key).to_ascii_lowercase()
1460}
1461
1462fn value_of<'a>(pairs: &'a [(String, String)], key: &str) -> Option<&'a str> {
1463    pairs
1464        .iter()
1465        .find(|(k, _)| bare_key(k) == key)
1466        .map(|(_, v)| v.as_str())
1467}
1468
1469/// The role `(key, value)` pairs name, by the vocabulary [`ObjectRole`]
1470/// lists: a type first, then a flag.
1471fn role_of(pairs: &[(String, String)]) -> Option<ObjectRole> {
1472    let squashed = |v: &str| {
1473        v.chars()
1474            .filter(|c| !matches!(c, '_' | '-' | ' '))
1475            .collect::<String>()
1476            .to_ascii_lowercase()
1477    };
1478    for key in ["volume_type", "subtype"] {
1479        if let Some(value) = value_of(pairs, key) {
1480            return Some(match squashed(value).as_str() {
1481                "modelpart" | "normalpart" | "model" | "part" => ObjectRole::Model,
1482                "parametermodifier" | "modifierpart" | "modifier" => ObjectRole::Modifier,
1483                "negativevolume" | "negativepart" => ObjectRole::NegativeVolume,
1484                "supportblocker" => ObjectRole::SupportBlocker,
1485                "supportenforcer" => ObjectRole::SupportEnforcer,
1486                _ => ObjectRole::Other(value.to_owned()),
1487            });
1488        }
1489    }
1490    let set = |key: &str| {
1491        value_of(pairs, key)
1492            .is_some_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
1493    };
1494    if set("modifier") || set("infill_mesh") || set("cutting_mesh") {
1495        return Some(ObjectRole::Modifier);
1496    }
1497    if set("anti_overhang_mesh") {
1498        return Some(ObjectRole::SupportBlocker);
1499    }
1500    if set("support_mesh") {
1501        return Some(ObjectRole::SupportEnforcer);
1502    }
1503    None
1504}
1505
1506/// Read a configuration part: its `object` elements by id, each with the
1507/// `key`/`value` metadata it holds and the `volume` or `part` elements in
1508/// it, whose own attributes count as pairs too.
1509fn parse_config(text: &str) -> OgeomResult<FastMap<u32, ConfigObject>> {
1510    let mut reader = xml::Reader::new(text);
1511    let mut objects: FastMap<u32, ConfigObject> = FastMap::default();
1512    let mut depth = 0_usize;
1513    // The open object, the depth it opened at, and its open volume.
1514    let mut object: Option<(u32, usize, ConfigObject)> = None;
1515    let mut volume: Option<(usize, ConfigVolume)> = None;
1516    while let Some(event) = reader.next()? {
1517        let start = match event {
1518            xml::Event::End => {
1519                if volume.as_ref().is_some_and(|(d, _)| *d == depth)
1520                    && let (Some((_, v)), Some((_, _, o))) = (volume.take(), object.as_mut())
1521                {
1522                    o.volumes.push(v);
1523                }
1524                if object.as_ref().is_some_and(|(_, d, _)| *d == depth)
1525                    && let Some((id, _, o)) = object.take()
1526                {
1527                    let held = objects.entry(id).or_default();
1528                    held.pairs.extend(o.pairs);
1529                    held.volumes.extend(o.volumes);
1530                }
1531                depth = depth.saturating_sub(1);
1532                continue;
1533            }
1534            xml::Event::Start(start) => start,
1535        };
1536        depth += 1;
1537        let pairs = |start: &xml::Start<'_>| -> Vec<(String, String)> {
1538            start
1539                .attributes
1540                .iter()
1541                .map(|(_, k, v)| ((*k).to_owned(), v.clone().into_owned()))
1542                .collect()
1543        };
1544        match start.local {
1545            "object" if object.is_none() && depth == 2 => {
1546                if let Some(id) = number::<u32>(&start, "id")? {
1547                    object = Some((id, depth, ConfigObject::default()));
1548                }
1549            }
1550            "volume" | "part" if object.is_some() && volume.is_none() => {
1551                let first = number::<usize>(&start, "firstid")?;
1552                let last = number::<usize>(&start, "lastid")?;
1553                volume = Some((
1554                    depth,
1555                    ConfigVolume {
1556                        triangles: first.zip(last),
1557                        object: number(&start, "id")?,
1558                        pairs: pairs(&start),
1559                    },
1560                ));
1561            }
1562            "metadata" => {
1563                let key = start.get("key").or_else(|| start.get("name"));
1564                let value = match start.get("value") {
1565                    Some(v) => v.to_owned(),
1566                    None => reader.text().into_owned(),
1567                };
1568                if let Some(key) = key {
1569                    let pair = (key.to_owned(), value);
1570                    match (volume.as_mut(), object.as_mut()) {
1571                        (Some((_, v)), _) => v.pairs.push(pair),
1572                        (None, Some((_, _, o))) => o.pairs.push(pair),
1573                        (None, None) => {}
1574                    }
1575                }
1576            }
1577            _ => {}
1578        }
1579    }
1580    Ok(objects)
1581}
1582
1583/// What flattening reads besides the parts: the package's configuration of
1584/// the start part's objects.
1585struct Context<'p> {
1586    parts: &'p FastMap<String, Part>,
1587    start: String,
1588    config: &'p FastMap<u32, ConfigObject>,
1589}
1590
1591impl Context<'_> {
1592    /// The configuration of object `id` of the part at `path`: only the
1593    /// start part's objects are configured.
1594    fn config(&self, path: &str, id: u32) -> Option<&ConfigObject> {
1595        (part_key(path) == self.start)
1596            .then(|| self.config.get(&id))
1597            .flatten()
1598    }
1599
1600    /// The tag of the object `id` at `path`, under `inherited`.
1601    fn tag(&self, path: &str, id: u32, def: &ObjectDef, inherited: &Tag) -> Tag {
1602        let mut tag = inherited.clone();
1603        if let Some(config) = self.config(path, id) {
1604            tag.role = role_of(&config.pairs).unwrap_or(tag.role);
1605        }
1606        if let Some(role) = role_of(&def.metadata) {
1607            tag.role = role;
1608        }
1609        tag
1610    }
1611}
1612
1613/// The colour of a triangle at its corners, from the group its property
1614/// names: `Ok(None)` for no colour, `Err(())` for a group or texture that
1615/// is not read.
1616fn corner_colours(
1617    part: Option<&Part>,
1618    pid: u32,
1619    index: [u32; 3],
1620) -> Result<Option<[[f64; 4]; 3]>, bool> {
1621    let Some(part) = part else {
1622        return Ok(None);
1623    };
1624    match part.groups.get(&pid) {
1625        None => Ok(None),
1626        Some(Group::Unread) => Err(false),
1627        Some(Group::Colours(colours)) => Ok(index
1628            .iter()
1629            .map(|&i| colours.get(i as usize).copied())
1630            .collect::<Option<Vec<_>>>()
1631            .map(|c| [c[0], c[1], c[2]])),
1632        Some(Group::Texture { texture, coords }) => {
1633            let Some(def) = part.textures.get(texture) else {
1634                return Ok(None);
1635            };
1636            let Some(image) = def.image.as_ref() else {
1637                return Err(true);
1638            };
1639            Ok(index
1640                .iter()
1641                .map(|&i| {
1642                    coords
1643                        .get(i as usize)
1644                        .map(|&(u, v)| image.sample(u, v, def.wrap))
1645                })
1646                .collect::<Option<Vec<_>>>()
1647                .map(|c| [c[0], c[1], c[2]]))
1648        }
1649    }
1650}
1651
1652fn flatten(
1653    context: &Context<'_>,
1654    path: &str,
1655    id: u32,
1656    placed: Affine,
1657    flat: &mut Flat,
1658    depth: usize,
1659    inherited: &Tag,
1660) -> OgeomResult<()> {
1661    if depth > DEPTH {
1662        ogeom_bail!(Construction, "the object {id} in {path} contains itself");
1663    }
1664    let parts = context.parts;
1665    let def = object(parts, path, id)?;
1666    let part = parts.get(&part_key(path));
1667    let tag = context.tag(path, id, def, inherited);
1668    let config = context.config(path, id);
1669    match &def.content {
1670        Content::Components(components) => {
1671            for c in components {
1672                let inner = c.path.as_deref().unwrap_or(path);
1673                let child = config
1674                    .and_then(|cfg| cfg.volumes.iter().find(|v| v.object == Some(c.object)))
1675                    .map_or_else(|| tag.clone(), |v| tag.with(&v.pairs));
1676                flatten(
1677                    context,
1678                    inner,
1679                    c.object,
1680                    placed.after(c.transform),
1681                    flat,
1682                    depth + 1,
1683                    &child,
1684                )?;
1685            }
1686        }
1687        Content::Mesh {
1688            positions,
1689            triangles,
1690            properties,
1691        } => {
1692            let base = u32::try_from(flat.positions.len()).unwrap_or(u32::MAX);
1693            let count = positions.len();
1694            flat.positions
1695                .extend(positions.iter().map(|p| placed.point(*p)));
1696            let mirrored = placed.mirrors();
1697            let ranges: Vec<(usize, usize, Tag)> = config
1698                .map(|cfg| {
1699                    cfg.volumes
1700                        .iter()
1701                        .filter_map(|v| v.triangles.map(|(a, b)| (a, b, tag.with(&v.pairs))))
1702                        .collect()
1703                })
1704                .unwrap_or_default();
1705            // The volume the last triangle went to, by range, in this mesh.
1706            let mut open: Option<Option<usize>> = None;
1707            for (k, (triangle, property)) in triangles.iter().zip(properties).enumerate() {
1708                if triangle.iter().any(|&v| v as usize >= count) {
1709                    ogeom_bail!(
1710                        Construction,
1711                        "a triangle of object {id} in {path} names a vertex past the {count} it has"
1712                    );
1713                }
1714                let [a, b, c] = *triangle;
1715                if a == b || b == c || c == a {
1716                    flat.degenerate += 1;
1717                    continue;
1718                }
1719                let triangle = if mirrored { [a, c, b] } else { [a, b, c] };
1720                flat.triangles.push(triangle.map(|v| base + v));
1721                let range = ranges.iter().position(|(a, b, _)| (*a..=*b).contains(&k));
1722                if open != Some(range) {
1723                    let volume = range.map_or(&tag, |r| &ranges[r].2);
1724                    flat.volumes.push((
1725                        volume.name.clone().or_else(|| def.name.clone()),
1726                        volume.role.clone(),
1727                    ));
1728                    open = Some(range);
1729                }
1730                flat.volume_of.push(flat.volumes.len() - 1);
1731                let colour = match property {
1732                    Some((pid, index)) => match part.and_then(|p| p.groups.get(pid)) {
1733                        Some(Group::Colours(group)) => group.get(index[0] as usize).copied(),
1734                        Some(Group::Unread) => {
1735                            flat.skipped_properties = true;
1736                            None
1737                        }
1738                        _ => None,
1739                    },
1740                    None => None,
1741                };
1742                if !flat.colours.contains(&colour) {
1743                    flat.colours.push(colour);
1744                }
1745                let corners = match property {
1746                    Some((pid, index)) => match corner_colours(part, *pid, *index) {
1747                        Ok(c) => c,
1748                        Err(undecoded) => {
1749                            flat.undecoded |= undecoded;
1750                            None
1751                        }
1752                    },
1753                    None => None,
1754                };
1755                // A mirrored triangle is rewound; its corners go with it.
1756                flat.corners
1757                    .push(corners.map(|[p, q, r]| if mirrored { [p, r, q] } else { [p, q, r] }));
1758            }
1759        }
1760    }
1761    Ok(())
1762}
1763
1764/// A welded build item: the mesh, each kept triangle's corner colours, and
1765/// its volumes as runs of the kept triangles.
1766struct Welded {
1767    mesh: Triangulation,
1768    corners: Vec<Option<[[f64; 4]; 3]>>,
1769    volumes: Vec<ThreeMfVolume>,
1770}
1771
1772impl Flat {
1773    /// The mesh, scaled to millimetres, coincident vertices welded, and a
1774    /// normal at each vertex from the triangles around it.
1775    fn into_mesh(self, scale: f64, weld: f64) -> Welded {
1776        let positions: Vec<Point> = self
1777            .positions
1778            .iter()
1779            .map(|p| Point::ORIGIN + (*p - Point::ORIGIN) * scale)
1780            .collect();
1781        // Weld on a grid of the weld distance, looking in the neighbouring
1782        // cells too, so two points either side of a cell wall still meet.
1783        // A cast saturates, so a coordinate past `i64`'s cells shares the
1784        // last one and is still compared by distance.
1785        #[allow(clippy::cast_possible_truncation, reason = "saturating")]
1786        let cell = |p: Point| {
1787            (
1788                (p.x / weld).floor() as i64,
1789                (p.y / weld).floor() as i64,
1790                (p.z / weld).floor() as i64,
1791            )
1792        };
1793        let mut grid: FastMap<(i64, i64, i64), Vec<u32>> = FastMap::default();
1794        let mut kept: Vec<Point> = Vec::with_capacity(positions.len());
1795        let mut remap = Vec::with_capacity(positions.len());
1796        for p in &positions {
1797            let (x, y, z) = cell(*p);
1798            let mut found = None;
1799            'search: for dx in -1..=1 {
1800                for dy in -1..=1 {
1801                    for dz in -1..=1 {
1802                        if let Some(list) = grid.get(&(
1803                            x.saturating_add(dx),
1804                            y.saturating_add(dy),
1805                            z.saturating_add(dz),
1806                        )) && let Some(&k) = list
1807                            .iter()
1808                            .find(|&&k| kept[k as usize].distance(*p) <= weld)
1809                        {
1810                            found = Some(k);
1811                            break 'search;
1812                        }
1813                    }
1814                }
1815            }
1816            let index = found.unwrap_or_else(|| {
1817                let k = u32::try_from(kept.len()).unwrap_or(u32::MAX);
1818                kept.push(*p);
1819                grid.entry((x, y, z)).or_default().push(k);
1820                k
1821            });
1822            remap.push(index);
1823        }
1824        let mut triangles: Vec<[u32; 3]> = Vec::with_capacity(self.triangles.len());
1825        let mut corners = Vec::with_capacity(self.triangles.len());
1826        let mut volume_of = Vec::with_capacity(self.triangles.len());
1827        for (k, t) in self.triangles.iter().enumerate() {
1828            let [a, b, c] = t.map(|v| remap[v as usize]);
1829            if a != b && b != c && c != a {
1830                triangles.push([a, b, c]);
1831                corners.push(self.corners[k]);
1832                volume_of.push(self.volume_of[k]);
1833            }
1834        }
1835        let mut cursor = 0;
1836        let volumes = self
1837            .volumes
1838            .into_iter()
1839            .enumerate()
1840            .map(|(v, (name, role))| {
1841                let start = cursor;
1842                while volume_of.get(cursor) == Some(&v) {
1843                    cursor += 1;
1844                }
1845                ThreeMfVolume {
1846                    name,
1847                    role,
1848                    triangles: start..cursor,
1849                }
1850            })
1851            .collect();
1852        let mut normals = vec![Vector::ZERO; kept.len()];
1853        for triangle in &triangles {
1854            let [a, b, c] = triangle.map(|i| kept[i as usize]);
1855            let face = (b - a).cross(c - a);
1856            for &i in triangle {
1857                normals[i as usize] += face;
1858            }
1859        }
1860        let normals = normals
1861            .into_iter()
1862            .map(|n| {
1863                let m = n.magnitude();
1864                if m > 0.0 { n / m } else { Vector::Z }
1865            })
1866            .collect();
1867        Welded {
1868            mesh: Triangulation {
1869                parameters: vec![(0.0, 0.0); kept.len()],
1870                positions: kept,
1871                normals,
1872                triangles,
1873                deflection_met: true,
1874            },
1875            corners,
1876            volumes,
1877        }
1878    }
1879}
1880
1881#[cfg(test)]
1882mod tests {
1883    #![allow(
1884        clippy::unwrap_used,
1885        clippy::cast_possible_truncation,
1886        reason = "test code"
1887    )]
1888    use super::*;
1889
1890    const T: Tolerances = Tolerances::millimetres();
1891
1892    const TYPES: &str = r#"<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>"#;
1893    const RELS: &str = r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="r0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>"#;
1894
1895    /// The vertices and twelve outward triangles of a unit cube at `x`,
1896    /// each triangle's extra attributes from `property(t)`.
1897    fn cube(x: f64, property: impl Fn(usize) -> String) -> String {
1898        let mut out = String::from("<mesh><vertices>");
1899        for k in 0..8 {
1900            let (dx, dy, dz) = (k & 1, (k >> 1) & 1, (k >> 2) & 1);
1901            out.push_str(&format!(
1902                r#"<vertex x="{}" y="{dy}" z="{dz}"/>"#,
1903                x + f64::from(dx)
1904            ));
1905        }
1906        out.push_str("</vertices><triangles>");
1907        let faces = [
1908            [0, 2, 3, 1],
1909            [4, 5, 7, 6],
1910            [0, 1, 5, 4],
1911            [2, 6, 7, 3],
1912            [0, 4, 6, 2],
1913            [1, 3, 7, 5],
1914        ];
1915        for (f, [a, b, c, d]) in faces.iter().enumerate() {
1916            for (k, [p, q, r]) in [[a, b, c], [a, c, d]].iter().enumerate() {
1917                out.push_str(&format!(
1918                    r#"<triangle v1="{p}" v2="{q}" v3="{r}" {}/>"#,
1919                    property(2 * f + k)
1920                ));
1921            }
1922        }
1923        out.push_str("</triangles></mesh>");
1924        out
1925    }
1926
1927    fn model(resources: &str, build: &str) -> String {
1928        format!(
1929            r#"<?xml version="1.0"?><model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02" xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06"><resources>{resources}</resources><build>{build}</build></model>"#
1930        )
1931    }
1932
1933    fn package(parts: &[(&str, &[u8])]) -> Vec<u8> {
1934        let mut all: Vec<(&str, &[u8])> = vec![
1935            ("[Content_Types].xml", TYPES.as_bytes()),
1936            ("_rels/.rels", RELS.as_bytes()),
1937        ];
1938        all.extend_from_slice(parts);
1939        archive(&all)
1940    }
1941
1942    /// One mesh of two cubes, the second a modifier by the triangle range
1943    /// the configuration part gives it.
1944    #[test]
1945    fn a_modifier_volume_reports_its_role_by_triangle_range() {
1946        let mut mesh = cube(0.0, |_| String::new());
1947        let second = cube(5.0, |_| String::new());
1948        // Merge the second cube's vertices and triangles into the first.
1949        let (v2, t2) = second
1950            .trim_start_matches("<mesh><vertices>")
1951            .trim_end_matches("</triangles></mesh>")
1952            .split_once("</vertices><triangles>")
1953            .unwrap();
1954        let t2 = (0..8).rev().fold(t2.to_owned(), |t, k| {
1955            t.replace(&format!("\"{k}\""), &format!("\"#{}\"", k + 8))
1956        });
1957        let t2 = t2.replace("\"#", "\"");
1958        mesh = mesh.replace("</vertices>", &format!("{v2}</vertices>"));
1959        mesh = mesh.replace("</triangles>", &format!("{t2}</triangles>"));
1960        let model = model(
1961            &format!(r#"<object id="1" type="model" name="bracket">{mesh}</object>"#),
1962            r#"<item objectid="1"/>"#,
1963        );
1964        let config = r#"<?xml version="1.0"?><config><object id="1"><metadata type="object" key="name" value="bracket"/>
1965<volume firstid="0" lastid="11"><metadata type="volume" key="name" value="body"/><metadata type="volume" key="volume_type" value="ModelPart"/></volume>
1966<volume firstid="12" lastid="23"><metadata type="volume" key="name" value="denser"/><metadata type="volume" key="volume_type" value="ParameterModifier"/></volume>
1967</object></config>"#;
1968        let bytes = package(&[
1969            ("3D/3dmodel.model", model.as_bytes()),
1970            ("Metadata/model.config", config.as_bytes()),
1971        ]);
1972        let import = read_3mf(&bytes, T).unwrap();
1973        let object = &import.objects[0];
1974        assert_eq!(object.role, ObjectRole::Model);
1975        assert_eq!(
1976            object.volumes,
1977            vec![
1978                ThreeMfVolume {
1979                    name: Some("body".into()),
1980                    role: ObjectRole::Model,
1981                    triangles: 0..12,
1982                },
1983                ThreeMfVolume {
1984                    name: Some("denser".into()),
1985                    role: ObjectRole::Modifier,
1986                    triangles: 12..24,
1987                },
1988            ]
1989        );
1990        assert!(
1991            object.mesh.triangles[12..]
1992                .iter()
1993                .flatten()
1994                .all(|&v| object.mesh.positions[v as usize].x >= 5.0)
1995        );
1996        assert!(
1997            import.warnings.iter().any(|w| w.contains("a modifier")),
1998            "{:?}",
1999            import.warnings
2000        );
2001        // Without the configuration, the same mesh is one volume, a part.
2002        let plain = package(&[("3D/3dmodel.model", model.as_bytes())]);
2003        let import = read_3mf(&plain, T).unwrap();
2004        assert_eq!(import.objects[0].volumes.len(), 1);
2005        assert_eq!(import.objects[0].volumes[0].triangles, 0..24);
2006        assert!(import.warnings.is_empty(), "{:?}", import.warnings);
2007    }
2008
2009    /// An assembly whose components live in a part of their own, the
2010    /// configuration naming each component by its object id and the
2011    /// subtype attribute giving its role; and an object whose own metadata
2012    /// makes it a support enforcer.
2013    #[test]
2014    fn component_and_object_roles_read_by_name() {
2015        let parts = model(
2016            &format!(
2017                r#"<object id="2" type="model">{}</object><object id="3" type="model">{}</object>"#,
2018                cube(0.0, |_| String::new()),
2019                cube(0.25, |_| String::new()),
2020            ),
2021            "",
2022        );
2023        let root = model(
2024            &format!(
2025                r#"<object id="1" type="model" name="assembly"><components><component p:path="/3D/Objects/parts.model" objectid="2"/><component p:path="/3D/Objects/parts.model" objectid="3"/></components></object>
2026<object id="4" type="model" name="tower"><metadatagroup><metadata name="tool:support_mesh" preserve="1">True</metadata></metadatagroup>{}</object>"#,
2027                cube(9.0, |_| String::new())
2028            ),
2029            r#"<item objectid="1"/><item objectid="4"/>"#,
2030        );
2031        let config = r#"<?xml version="1.0"?><config><object id="1"><metadata key="name" value="assembly"/>
2032<part id="2" subtype="normal_part"><metadata key="name" value="housing"/></part>
2033<part id="3" subtype="negative_part"><metadata key="name" value="bore"/></part></object>
2034<plate><metadata key="plater_id" value="1"/></plate></config>"#;
2035        let bytes = package(&[
2036            ("3D/3dmodel.model", root.as_bytes()),
2037            ("3D/Objects/parts.model", parts.as_bytes()),
2038            ("Metadata/model_settings.config", config.as_bytes()),
2039        ]);
2040        let import = read_3mf(&bytes, T).unwrap();
2041        let (assembly, tower) = (&import.objects[0], &import.objects[1]);
2042        let roles: Vec<_> = assembly
2043            .volumes
2044            .iter()
2045            .map(|v| (v.name.as_deref(), v.role.clone(), v.triangles.clone()))
2046            .collect();
2047        assert_eq!(
2048            roles,
2049            vec![
2050                (Some("housing"), ObjectRole::Model, 0..12),
2051                (Some("bore"), ObjectRole::NegativeVolume, 12..24),
2052            ]
2053        );
2054        assert_eq!(tower.role, ObjectRole::SupportEnforcer);
2055        assert_eq!(tower.volumes[0].role, ObjectRole::SupportEnforcer);
2056        assert!(
2057            import
2058                .warnings
2059                .iter()
2060                .any(|w| w.contains("tower is a support enforcer"))
2061        );
2062        // An unknown role is kept as written.
2063        assert_eq!(
2064            role_of(&[("subtype".into(), "fuzzy_skin_part".into())]),
2065            Some(ObjectRole::Other("fuzzy_skin_part".into()))
2066        );
2067        assert_eq!(
2068            role_of(&[("modifier".into(), "1".into())]),
2069            Some(ObjectRole::Modifier)
2070        );
2071        assert_eq!(role_of(&[("name".into(), "x".into())]), None);
2072    }
2073
2074    /// A colour group named at each corner, and a texture group sampled at
2075    /// each corner, come back as the colours the file gives.
2076    #[test]
2077    fn colour_groups_and_textures_read_as_mesh_colours() {
2078        let texels = [[255, 0, 0, 255], [0, 0, 255, 255]];
2079        let png = crate::image::encode_png(2, 1, &texels);
2080        let resources = format!(
2081            r##"<m:colorgroup id="5"><m:color color="#FF0000"/><m:color color="#00FF0080"/></m:colorgroup>
2082<m:texture2d id="6" path="/3D/Textures/two.png" contenttype="image/png" tilestyleu="clamp"/>
2083<m:texture2dgroup id="7" texid="6"><m:tex2coord u="0.25" v="0.5"/><m:tex2coord u="0.75" v="0.5"/></m:texture2dgroup>
2084<object id="1" type="model" name="painted">{}</object><object id="2" type="model" name="textured">{}</object>"##,
2085            cube(0.0, |t| format!(r#"pid="5" p1="{}" p2="1" p3="0""#, t % 2)),
2086            cube(3.0, |t| format!(r#"pid="7" p1="{}""#, t / 6)),
2087        );
2088        let model = model(&resources, r#"<item objectid="1"/><item objectid="2"/>"#);
2089        let bytes = package(&[
2090            ("3D/3dmodel.model", model.as_bytes()),
2091            ("3D/Textures/two.png", &png),
2092        ]);
2093        let import = read_3mf(&bytes, T).unwrap();
2094        assert!(import.warnings.is_empty(), "{:?}", import.warnings);
2095        let red = [1.0, 0.0, 0.0, 1.0];
2096        let green = [0.0, 1.0, 0.0, 128.0 / 255.0];
2097        let painted = &import.objects[0];
2098        let Some(MeshColours::PerCorner(corners)) = &painted.colours else {
2099            panic!("{:?}", painted.colours);
2100        };
2101        assert_eq!(corners[0], [red, green, red]);
2102        assert_eq!(corners[1], [green, green, red]);
2103        let textured = &import.objects[1];
2104        let Some(MeshColours::PerTriangle(faces)) = &textured.colours else {
2105            panic!("{:?}", textured.colours);
2106        };
2107        assert!(faces[..6].iter().all(|c| *c == red));
2108        assert!(faces[6..].iter().all(|c| *c == [0.0, 0.0, 1.0, 1.0]));
2109        assert_eq!(textured.colour, None);
2110
2111        // The texture part missing, the mesh still reads, uncoloured, and
2112        // says why.
2113        let bytes = package(&[("3D/3dmodel.model", model.as_bytes())]);
2114        let import = read_3mf(&bytes, T).unwrap();
2115        assert!(import.objects[1].colours.is_none());
2116        assert!(
2117            import
2118                .warnings
2119                .iter()
2120                .any(|w| w.contains("no texture part"))
2121        );
2122    }
2123
2124    /// One stored entry of `data`, named by `copies` central directory
2125    /// records that all point at its one local record.
2126    fn overlapping(data: &[u8], copies: u16) -> Vec<u8> {
2127        let one = archive(&[("p0000", data)]);
2128        let end = one.len() - 22;
2129        let at = usize::try_from(u32_at(&one, end + 16).unwrap()).unwrap();
2130        let record = &one[at..end];
2131        let mut out = one[..at].to_vec();
2132        for k in 0..copies {
2133            let mut copy = record.to_vec();
2134            copy[46..51].copy_from_slice(format!("p{k:04}").as_bytes());
2135            out.extend_from_slice(&copy);
2136        }
2137        let size = u32::try_from(out.len() - at).unwrap();
2138        out.extend_from_slice(&one[end..end + 8]);
2139        out.extend_from_slice(&copies.to_le_bytes());
2140        out.extend_from_slice(&copies.to_le_bytes());
2141        out.extend_from_slice(&size.to_le_bytes());
2142        out.extend_from_slice(&u32::try_from(at).unwrap().to_le_bytes());
2143        out.extend_from_slice(&0_u16.to_le_bytes());
2144        out
2145    }
2146
2147    /// A thousand directory records over one 64 KiB entry would read as
2148    /// 64 MiB from a file of 114 KiB. Entries share no bytes in a ZIP
2149    /// archive, so a directory whose records overlap is refused before any
2150    /// entry is read, by `read_package` and `read_3mf` alike.
2151    #[test]
2152    fn entries_overlapping_one_another_are_refused() {
2153        let data = vec![b'x'; 1 << 16];
2154        let bytes = overlapping(&data, 1000);
2155        assert!(bytes.len() < 120_000, "{}", bytes.len());
2156        let message = read_package(&bytes).unwrap_err().to_string();
2157        assert!(message.contains("overlap"), "{message}");
2158        assert!(read_3mf(&bytes, T).is_err());
2159        // One record is an ordinary archive.
2160        let parts = read_package(&overlapping(&data, 1)).unwrap();
2161        assert_eq!(parts.len(), 1);
2162        assert_eq!(parts[0].1, data);
2163    }
2164
2165    /// The limits are read against the central directory's sizes, which
2166    /// each entry's inflation is held to exactly: three entries of 10, 20
2167    /// and 30 bytes pass a limit of three entries and 60 bytes, and fail
2168    /// one entry or one byte less.
2169    #[test]
2170    fn a_package_read_is_held_to_its_limits() {
2171        let bytes = archive(&[
2172            ("a", &[1; 10][..]),
2173            ("dir/", &[][..]),
2174            ("dir/b", &[2; 20][..]),
2175            ("c", &[3; 30][..]),
2176        ]);
2177        let exact = PackageLimits {
2178            entries: 3,
2179            bytes: 60,
2180        };
2181        assert_eq!(read_package_within(&bytes, exact).unwrap().len(), 3);
2182        assert_eq!(read_package(&bytes).unwrap().len(), 3);
2183        let fewer = PackageLimits {
2184            entries: 2,
2185            ..exact
2186        };
2187        let message = read_package_within(&bytes, fewer).unwrap_err().to_string();
2188        assert!(message.contains("3 entries"), "{message}");
2189        let smaller = PackageLimits { bytes: 59, ..exact };
2190        let message = read_package_within(&bytes, smaller)
2191            .unwrap_err()
2192            .to_string();
2193        assert!(message.contains("60 bytes"), "{message}");
2194    }
2195
2196    /// Bits packed least significant first, as DEFLATE reads them.
2197    #[derive(Default)]
2198    struct BitWriter {
2199        out: Vec<u8>,
2200        held: u64,
2201        count: u32,
2202    }
2203
2204    impl BitWriter {
2205        fn put(&mut self, value: u64, bits: u32) {
2206            self.held |= value << self.count;
2207            self.count += bits;
2208            while self.count >= 8 {
2209                self.out.push((self.held & 0xFF) as u8);
2210                self.held >>= 8;
2211                self.count -= 8;
2212            }
2213        }
2214
2215        /// A Huffman code, written most significant bit first.
2216        fn code(&mut self, code: u64, bits: u32) {
2217            self.put(code.reverse_bits() >> (64 - bits), bits);
2218        }
2219
2220        fn finish(mut self) -> Vec<u8> {
2221            if self.count > 0 {
2222                self.out.push((self.held & 0xFF) as u8);
2223            }
2224            self.out
2225        }
2226    }
2227
2228    /// A DEFLATE stream of `1 + 258 * repeats` zero bytes in one dynamic
2229    /// block whose codes are a bit or two long: a zero literal, then a
2230    /// 258-byte copy at distance 1 in two bits, the most DEFLATE expands.
2231    fn zeros_bomb(repeats: usize) -> Vec<u8> {
2232        let mut w = BitWriter::default();
2233        w.put(1, 1); // last block
2234        w.put(2, 2); // dynamic codes
2235        w.put(29, 5); // 286 literal/length codes
2236        w.put(0, 5); // 1 distance code
2237        w.put(14, 4); // 18 code-length code lengths
2238        // In CODE_LENGTH_ORDER, 18 comes third, 2 sixteenth and 1
2239        // eighteenth: 18 takes code `0`, 1 takes `10` and 2 takes `11`.
2240        for slot in 0..18 {
2241            let length = match slot {
2242                2 => 1,
2243                15 | 17 => 2,
2244                _ => 0,
2245            };
2246            w.put(length, 3);
2247        }
2248        let zeros = |w: &mut BitWriter, mut run: u64| {
2249            while run > 0 {
2250                let take = run.min(138);
2251                w.code(0, 1);
2252                w.put(take - 11, 7);
2253                run -= take;
2254            }
2255        };
2256        // Literal 0 and end-of-block in two bits, length 285 in one; one
2257        // distance code, distance 1, in one.
2258        w.code(0b11, 2); // literal 0: length 2
2259        zeros(&mut w, 255);
2260        w.code(0b11, 2); // end of block: length 2
2261        zeros(&mut w, 28);
2262        w.code(0b10, 2); // length 285: length 1
2263        w.code(0b10, 2); // distance 0: length 1
2264        // Canonical codes: 285 is `0`, literal 0 is `10`, end of block `11`.
2265        w.code(0b10, 2);
2266        for _ in 0..repeats {
2267            w.code(0, 1);
2268            w.code(0, 1);
2269        }
2270        w.code(0b11, 2);
2271        w.finish()
2272    }
2273
2274    /// A ZIP archive of deflated entries: name, stream and inflated size.
2275    /// Each checksum is written as zero: a reader refusing the archive by
2276    /// its sizes never inflates far enough to check one.
2277    fn deflated_archive(entries: &[(&str, &[u8], u32)]) -> Vec<u8> {
2278        let mut out: Vec<u8> = Vec::new();
2279        let mut directory: Vec<u8> = Vec::new();
2280        let field = |v: &mut Vec<u8>, bytes: &[u8]| v.extend_from_slice(bytes);
2281        for (name, stream, size) in entries {
2282            let offset = u32::try_from(out.len()).unwrap();
2283            let compressed = u32::try_from(stream.len()).unwrap();
2284            let name_len = u16::try_from(name.len()).unwrap();
2285            field(&mut out, &0x0403_4b50_u32.to_le_bytes());
2286            field(&mut out, &[20, 0, 0, 0, 8, 0, 0, 0, 0, 0]);
2287            field(&mut out, &0_u32.to_le_bytes());
2288            field(&mut out, &compressed.to_le_bytes());
2289            field(&mut out, &size.to_le_bytes());
2290            field(&mut out, &name_len.to_le_bytes());
2291            field(&mut out, &0_u16.to_le_bytes());
2292            field(&mut out, name.as_bytes());
2293            field(&mut out, stream);
2294            field(&mut directory, &0x0201_4b50_u32.to_le_bytes());
2295            field(&mut directory, &[20, 0, 20, 0, 0, 0, 8, 0, 0, 0, 0, 0]);
2296            field(&mut directory, &0_u32.to_le_bytes());
2297            field(&mut directory, &compressed.to_le_bytes());
2298            field(&mut directory, &size.to_le_bytes());
2299            field(&mut directory, &name_len.to_le_bytes());
2300            field(&mut directory, &[0; 12]);
2301            field(&mut directory, &offset.to_le_bytes());
2302            field(&mut directory, name.as_bytes());
2303        }
2304        let count = u16::try_from(entries.len()).unwrap();
2305        let at = u32::try_from(out.len()).unwrap();
2306        let size = u32::try_from(directory.len()).unwrap();
2307        out.extend_from_slice(&directory);
2308        field(&mut out, &0x0605_4b50_u32.to_le_bytes());
2309        field(&mut out, &[0, 0, 0, 0]);
2310        field(&mut out, &count.to_le_bytes());
2311        field(&mut out, &count.to_le_bytes());
2312        field(&mut out, &size.to_le_bytes());
2313        field(&mut out, &at.to_le_bytes());
2314        field(&mut out, &0_u16.to_le_bytes());
2315        out
2316    }
2317
2318    /// Three entries of 1.5 GiB of zeros each, deflated to about 1.5 MiB
2319    /// apiece, apart in the archive: 4.5 GiB from under 5 MiB. Without a
2320    /// limit it would be inflated entry by entry and held; the default
2321    /// refuses it from the directory alone.
2322    #[test]
2323    fn the_default_limits_refuse_a_deflate_bomb() {
2324        // The stream is real: a small one inflates to the zeros it says.
2325        let small = zeros_bomb(1000);
2326        let zeros = crate::inflate::inflate(&small, 1 + 258 * 1000).unwrap();
2327        assert!(zeros.iter().all(|&b| b == 0));
2328
2329        let repeats = (3 << 29) / 258;
2330        let size = u32::try_from(1 + 258 * repeats).unwrap();
2331        let stream = zeros_bomb(repeats);
2332        assert!(size as usize > 1000 * stream.len(), "{}", stream.len());
2333        let bytes = deflated_archive(&[
2334            ("a", &stream, size),
2335            ("b", &stream, size),
2336            ("c", &stream, size),
2337        ]);
2338        assert!(bytes.len() < 5 << 20, "{}", bytes.len());
2339        let message = read_package(&bytes).unwrap_err().to_string();
2340        assert!(message.contains("past the 4294967296 allowed"), "{message}");
2341
2342        // Past the entry count alone, empty entries are refused too.
2343        let names: Vec<String> = (0..4097).map(|k| format!("e{k:04}")).collect();
2344        let many: Vec<(&str, &[u8])> = names.iter().map(|n| (n.as_str(), &b""[..])).collect();
2345        let message = read_package(&archive(&many)).unwrap_err().to_string();
2346        assert!(message.contains("4097 entries"), "{message}");
2347        assert_eq!(read_package(&archive(&many[..4096])).unwrap().len(), 4096);
2348    }
2349
2350    /// Every 3MF package in the corpus reads within the default limits.
2351    #[test]
2352    fn the_corpus_reads_within_the_default_limits() {
2353        let corpus = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/corpus");
2354        let mut read = 0;
2355        for entry in std::fs::read_dir(corpus).unwrap() {
2356            let path = entry.unwrap().path();
2357            if path.extension().is_some_and(|e| e == "3mf") {
2358                let bytes = std::fs::read(&path).unwrap();
2359                assert!(read_package(&bytes).is_ok(), "{}", path.display());
2360                assert!(read_3mf(&bytes, T).is_ok(), "{}", path.display());
2361                read += 1;
2362            }
2363        }
2364        assert!(read >= 4, "{read}");
2365    }
2366
2367    /// Cut short or damaged anywhere, a package is refused or read, never
2368    /// a panic; a malformed configuration part is only a warning.
2369    #[test]
2370    fn hostile_packages_never_panic() {
2371        let model = model(
2372            &format!(
2373                r#"<object id="1" type="model">{}</object>"#,
2374                cube(0.0, |_| String::new())
2375            ),
2376            r#"<item objectid="1"/>"#,
2377        );
2378        let bytes = package(&[
2379            ("3D/3dmodel.model", model.as_bytes()),
2380            (
2381                "Metadata/a.config",
2382                b"<config><object id=\"x\"><volume firstid=\"q\"/>",
2383            ),
2384        ]);
2385        let import = read_3mf(&bytes, T).unwrap();
2386        assert!(
2387            import
2388                .warnings
2389                .iter()
2390                .any(|w| w.contains("configuration part"))
2391        );
2392        assert_eq!(import.objects[0].mesh.triangles.len(), 12);
2393        for cut in 0..bytes.len() {
2394            assert!(read_3mf(&bytes[..cut], T).is_err(), "cut at {cut}");
2395        }
2396        let mut seed = 0x9e37_79b9_u32;
2397        for _ in 0..2000 {
2398            let mut noisy = bytes.clone();
2399            seed ^= seed << 13;
2400            seed ^= seed >> 17;
2401            seed ^= seed << 5;
2402            let at = seed as usize % noisy.len();
2403            noisy[at] ^= (seed >> 8) as u8 | 1;
2404            let _ = read_3mf(&noisy, T);
2405        }
2406        for garbage in [&b""[..], b"PK\x05\x06", b"\x89PNG\r\n\x1a\n"] {
2407            assert!(read_3mf(garbage, T).is_err());
2408        }
2409    }
2410}