Skip to main content

teamcraft/mesh/
rhino.rs

1//! Rhino models (`.3dm`, versions 1 to 8).
2//!
3//! The format is the openNURBS archive (documented by McNeel's openNURBS
4//! sources, which were read but not copied): a 32-byte header, then nested
5//! chunks – a `u32` type code and a length (32 bit, 64 bit from version 50).
6//! Short chunks (type bit `0x8000_0000`) carry their value in the length;
7//! chunks with type bit `0x8000` end in a CRC-32. The object table holds one
8//! record per object: the class UUID, its data and its attributes.
9//!
10//! What is drawn is what Rhino stored for display:
11//!
12//! * mesh objects;
13//! * the render meshes of polysurfaces (one per B-rep face, saved after the
14//!   exact geometry unless the file was saved small);
15//! * the mesh cache of extrusions (Rhino 6 and later);
16//! * SubD objects, which never store a mesh: their control net is refined
17//!   by Catmull-Clark subdivision (creases and corners kept) and pushed to
18//!   the limit surface;
19//! * block instances: the objects of the block definition, transformed
20//!   (nested blocks up to [`MAX_DEPTH`]);
21//! * the quantised meshes of Rhino 1 files.
22//!
23//! Hidden objects, objects on hidden layers and the objects of block
24//! definitions (drawn only through their instances) are left out.
25//! Coordinates are converted from the model unit to millimetres.
26//!
27//! Everything is bounded: chunk lengths must fit their parent, counts are
28//! checked against the bytes left before anything is allocated, the
29//! triangles of the whole scene (blocks expanded) are counted against
30//! [`MAX_TRIANGLES`] before they are copied.
31
32use std::collections::{HashMap, HashSet};
33use std::io::Read;
34
35use anyhow::{anyhow, bail, Result};
36
37use super::{check_triangles, Mesh, MAX_TRIANGLES};
38
39/// The start of the 32-byte header of every `.3dm` file.
40pub const MAGIC: &[u8] = b"3D Geometry File Format ";
41/// openNURBS skips up to 32 MiB of foreign data (OLE wrappers) before the header.
42const MAX_HEADER_OFFSET: usize = 32 * 1024 * 1024;
43/// Nesting of block instances.
44pub const MAX_DEPTH: usize = 8;
45/// Quads one SubD may be subdivided into.
46const MAX_SUBD_FACES: usize = 200_000;
47/// Subdivision steps of a SubD (each one quadruples the faces).
48const MAX_SUBD_LEVELS: usize = 4;
49
50const TCODE_SHORT: u32 = 0x8000_0000;
51const TCODE_CRC: u32 = 0x8000;
52const TCODE_ENDOFFILE: u32 = 0x0000_7FFF;
53const TCODE_ENDOFTABLE: u32 = 0xFFFF_FFFF;
54const TCODE_PROPERTIES_TABLE: u32 = 0x1000_0014;
55const TCODE_SETTINGS_TABLE: u32 = 0x1000_0015;
56const TCODE_LAYER_TABLE: u32 = 0x1000_0011;
57const TCODE_OBJECT_TABLE: u32 = 0x1000_0013;
58const TCODE_IDEF_TABLE: u32 = 0x1000_0021;
59const TCODE_OPENNURBS_VERSION: u32 = 0xA000_0026;
60const TCODE_COMPRESSED_PREVIEW: u32 = 0x2000_8025;
61const TCODE_UNITS: u32 = 0x2000_8031;
62const TCODE_LAYER_RECORD: u32 = 0x2000_8050;
63const TCODE_IDEF_RECORD: u32 = 0x2000_8076;
64const TCODE_OBJECT_RECORD: u32 = 0x2000_8070;
65const TCODE_OBJECT_TYPE: u32 = 0x8200_0071;
66const TCODE_OBJECT_ATTRIBUTES: u32 = 0x0200_8072;
67const TCODE_OBJECT_END: u32 = 0x8200_007F;
68const TCODE_CLASS: u32 = 0x0002_7FFA;
69const TCODE_CLASS_UUID: u32 = 0x0002_FFFB;
70const TCODE_CLASS_DATA: u32 = 0x0002_FFFC;
71const TCODE_CLASS_END: u32 = 0x8002_7FFF;
72const TCODE_ANONYMOUS: u32 = 0x4000_8000;
73const TCODE_V1_MESH: u32 = 0x0010_0015;
74const TCODE_V1_MESH_GEOMETRY: u32 = 0x0010_0017;
75
76/// A class UUID as it is stored: `u32`, `u16`, `u16` little endian, 8 bytes.
77const fn uuid(d1: u32, d2: u16, d3: u16, d4: [u8; 8]) -> [u8; 16] {
78    let a = d1.to_le_bytes();
79    let b = d2.to_le_bytes();
80    let c = d3.to_le_bytes();
81    [
82        a[0], a[1], a[2], a[3], b[0], b[1], c[0], c[1], d4[0], d4[1], d4[2], d4[3], d4[4], d4[5],
83        d4[6], d4[7],
84    ]
85}
86
87const MESH: [u8; 16] = uuid(
88    0x4ED7_D4E4,
89    0xE947,
90    0x11d3,
91    [0xBF, 0xE5, 0x00, 0x10, 0x83, 0x01, 0x22, 0xF0],
92);
93/// ON_Brep and the legacy classes openNURBS reads as one: old trimmed
94/// surface, old B-rep and `TL_Brep` (every Rhino 4 polysurface).
95const BREPS: [[u8; 16]; 4] = [
96    uuid(
97        0x60B5_DBC5,
98        0xE660,
99        0x11d3,
100        [0xBF, 0xE4, 0x00, 0x10, 0x83, 0x01, 0x22, 0xF0],
101    ),
102    uuid(
103        0x0705_FDEF,
104        0x3E2A,
105        0x11d4,
106        [0x80, 0x0E, 0x00, 0x10, 0x83, 0x01, 0x22, 0xF0],
107    ),
108    uuid(
109        0x2D4C_FEDB,
110        0x3E2A,
111        0x11d4,
112        [0x80, 0x0E, 0x00, 0x10, 0x83, 0x01, 0x22, 0xF0],
113    ),
114    uuid(
115        0xF06F_C243,
116        0xA32A,
117        0x4608,
118        [0x9D, 0xD8, 0xA7, 0xD2, 0xC4, 0xCE, 0x2A, 0x36],
119    ),
120];
121const EXTRUSION: [u8; 16] = uuid(
122    0x36F5_3175,
123    0x72B8,
124    0x4d47,
125    [0xBF, 0x1F, 0xB4, 0xE6, 0xFC, 0x24, 0xF4, 0xB9],
126);
127const SUBD: [u8; 16] = uuid(
128    0xF09B_A4D9,
129    0x455B,
130    0x42C3,
131    [0xBA, 0x3B, 0xE6, 0xCC, 0xAC, 0xEF, 0x85, 0x3B],
132);
133const INSTANCE_REF: [u8; 16] = uuid(
134    0xF9CF_B638,
135    0xB9D4,
136    0x4340,
137    [0x87, 0xE3, 0xC5, 0x6E, 0x78, 0x65, 0xD9, 0x6A],
138);
139
140/// Millimetres per model unit (ON::LengthUnitSystem); no unit counts as mm.
141fn millimetres_per_unit(unit: u32) -> f64 {
142    match unit {
143        1 => 1e-3,
144        3 => 10.0,
145        4 => 1000.0,
146        5 => 1e6,
147        6 => 2.54e-5,
148        7 => 0.0254,
149        8 => 25.4,
150        9 => 304.8,
151        10 => 1_609_344.0,
152        12 => 1e-7,
153        13 => 1e-6,
154        14 => 100.0,
155        15 => 1e4,
156        16 => 1e5,
157        19 => 914.4,
158        20 => 1_852_000.0,
159        _ => 1.0,
160    }
161}
162
163// ---------------------------------------------------------------------------
164// archive cursor
165// ---------------------------------------------------------------------------
166
167fn short_file(at: usize) -> anyhow::Error {
168    anyhow!("The Rhino file is damaged or ends early (byte {at}).")
169}
170
171/// A bounded read position inside one chunk.
172#[derive(Clone, Copy)]
173struct Cur<'a> {
174    data: &'a [u8],
175    at: usize,
176    end: usize,
177    /// 64-bit chunk lengths (version 50 and later).
178    big: bool,
179    /// Version 1: no 32-bit CRCs.
180    v1: bool,
181}
182
183struct Chunk<'a> {
184    tcode: u32,
185    value: i64,
186    body: Option<Cur<'a>>,
187}
188
189impl<'a> Cur<'a> {
190    fn left(&self) -> usize {
191        self.end.saturating_sub(self.at)
192    }
193
194    fn bytes(&mut self, n: usize) -> Result<&'a [u8]> {
195        let end = self
196            .at
197            .checked_add(n)
198            .filter(|&e| e <= self.end)
199            .ok_or_else(|| short_file(self.at))?;
200        let s = self
201            .data
202            .get(self.at..end)
203            .ok_or_else(|| short_file(self.at))?;
204        self.at = end;
205        Ok(s)
206    }
207
208    fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
209        let at = self.at;
210        <[u8; N]>::try_from(self.bytes(N)?).map_err(|_| short_file(at))
211    }
212
213    fn skip(&mut self, n: usize) -> Result<()> {
214        self.bytes(n).map(|_| ())
215    }
216
217    /// Skips `count` items of `size` bytes (counts from the file are checked).
218    fn skip_items(&mut self, count: usize, size: usize) -> Result<()> {
219        let n = count.checked_mul(size).ok_or_else(|| short_file(self.at))?;
220        self.skip(n)
221    }
222
223    fn u8(&mut self) -> Result<u8> {
224        self.array::<1>().map(|[b]| b)
225    }
226    fn u16(&mut self) -> Result<u16> {
227        self.array().map(u16::from_le_bytes)
228    }
229    fn u32(&mut self) -> Result<u32> {
230        self.array().map(u32::from_le_bytes)
231    }
232    fn i32(&mut self) -> Result<i32> {
233        self.array().map(i32::from_le_bytes)
234    }
235    fn f64(&mut self) -> Result<f64> {
236        self.array().map(f64::from_le_bytes)
237    }
238    fn f64s<const N: usize>(&mut self) -> Result<[f64; N]> {
239        let mut out = [0.0; N];
240        for v in &mut out {
241            *v = self.f64()?;
242        }
243        Ok(out)
244    }
245    fn uuid(&mut self) -> Result<[u8; 16]> {
246        self.array()
247    }
248    /// A count that must fit into the bytes left (`size` bytes per item).
249    fn count(&mut self, size: usize) -> Result<usize> {
250        let at = self.at;
251        let n = self.u32()? as usize;
252        if n.saturating_mul(size.max(1)) > self.left() {
253            bail!("The Rhino file is damaged: a count at byte {at} exceeds the data.");
254        }
255        Ok(n)
256    }
257    /// `Read3dmChunkVersion`: major and minor version in one byte.
258    fn version(&mut self) -> Result<(u8, u8)> {
259        let v = self.u8()?;
260        Ok((v >> 4, v & 0x0F))
261    }
262    /// The two `i32` that start a versioned chunk (`BeginRead3dmChunk`).
263    fn version2(&mut self) -> Result<(i32, i32)> {
264        Ok((self.i32()?, self.i32()?))
265    }
266    /// Skips an `ON_wString` (length with terminator, UTF-16).
267    fn skip_string(&mut self) -> Result<()> {
268        let n = self.count(2)?;
269        self.skip_items(n, 2)
270    }
271
272    fn chunk(&mut self) -> Result<Chunk<'a>> {
273        let tcode = self.u32()?;
274        let short = tcode & TCODE_SHORT != 0;
275        let value = if self.big {
276            i64::from_le_bytes(self.array()?)
277        } else if short
278            && tcode != TCODE_OPENNURBS_VERSION
279            && tcode != TCODE_OBJECT_TYPE
280            && tcode != TCODE_ENDOFTABLE
281        {
282            i64::from(self.i32()?)
283        } else {
284            i64::from(self.u32()?)
285        };
286        if short {
287            return Ok(Chunk {
288                tcode,
289                value,
290                body: None,
291            });
292        }
293        let len = usize::try_from(value)
294            .ok()
295            .filter(|&n| n <= self.left())
296            .ok_or_else(|| {
297                anyhow!(
298                    "The Rhino file is damaged: a chunk at byte {} is too long.",
299                    self.at
300                )
301            })?;
302        let crc = if tcode & TCODE_CRC != 0 && !self.v1 {
303            4
304        } else {
305            0
306        };
307        if len < crc {
308            bail!(
309                "The Rhino file is damaged: a chunk at byte {} is too short.",
310                self.at
311            );
312        }
313        let body = Cur {
314            at: self.at,
315            end: self.at + len - crc,
316            ..*self
317        };
318        self.at += len;
319        Ok(Chunk {
320            tcode,
321            value,
322            body: Some(body),
323        })
324    }
325
326    /// The next chunk, `None` at the end of the data or after an end marker.
327    fn next(&mut self) -> Result<Option<Chunk<'a>>> {
328        if self.left() == 0 {
329            return Ok(None);
330        }
331        let c = self.chunk()?;
332        Ok(Some(c))
333    }
334
335    /// The body of the next chunk, which must be a long chunk.
336    fn body(&mut self) -> Result<Cur<'a>> {
337        let at = self.at;
338        self.chunk()?.body.ok_or_else(|| {
339            anyhow!("The Rhino file is damaged: unexpected short chunk at byte {at}.")
340        })
341    }
342}
343
344/// A class-tagged object (`ReadObject`): its class UUID and its data.
345fn read_object<'a>(cur: &mut Cur<'a>) -> Result<([u8; 16], Option<Cur<'a>>)> {
346    let at = cur.at;
347    let c = cur.chunk()?;
348    let (TCODE_CLASS, Some(mut class)) = (c.tcode, c.body) else {
349        bail!("The Rhino file is damaged: no object at byte {at}.");
350    };
351    let mut id = [0; 16];
352    let mut data = None;
353    while let Some(c) = class.next()? {
354        match (c.tcode, c.body) {
355            (TCODE_CLASS_UUID, Some(mut b)) => id = b.uuid()?,
356            (TCODE_CLASS_DATA, Some(b)) => data = Some(b),
357            (TCODE_CLASS_END, _) => break,
358            _ => {}
359        }
360    }
361    Ok((id, data))
362}
363
364// ---------------------------------------------------------------------------
365// model
366// ---------------------------------------------------------------------------
367
368/// Triangles in model units.
369#[derive(Default)]
370struct Part {
371    positions: Vec<f32>,
372    indices: Vec<u32>,
373}
374
375impl Part {
376    fn triangles(&self) -> usize {
377        self.indices.len() / 3
378    }
379}
380
381enum Geometry {
382    Parts(Vec<Part>),
383    Instance {
384        definition: [u8; 16],
385        xform: [[f64; 4]; 4],
386    },
387}
388
389struct Object {
390    id: Option<[u8; 16]>,
391    layer: i32,
392    /// ON::object_mode: 1 hidden, 3 part of a block definition.
393    mode: u8,
394    visible: bool,
395    geometry: Geometry,
396}
397
398struct Model {
399    version: u32,
400    opennurbs: u64,
401    unit: u32,
402    hidden_layers: Vec<bool>,
403    definitions: HashMap<[u8; 16], Vec<[u8; 16]>>,
404    objects: Vec<Object>,
405    /// Triangles read so far (before blocks are expanded).
406    triangles: usize,
407}
408
409/// The archive version and a cursor behind the 32-byte header.
410fn open(data: &[u8]) -> Result<(u32, Cur<'_>)> {
411    let start = data
412        .get(..data.len().min(MAX_HEADER_OFFSET + MAGIC.len()))
413        .and_then(|head| head.windows(MAGIC.len()).position(|w| w == MAGIC))
414        .ok_or_else(|| anyhow!("This is not a Rhino model (.3dm header missing)."))?;
415    let digits = data
416        .get(start + MAGIC.len()..start + 32)
417        .ok_or_else(|| short_file(start))?;
418    let mut version: u32 = 0;
419    for &d in digits.iter().skip_while(|&&d| d == b' ') {
420        // openNURBS reads an 'X' in the last place as 2 (early beta files)
421        let d = if d == b'X' { b'2' } else { d };
422        if !d.is_ascii_digit() {
423            bail!("This is not a Rhino model (invalid version).");
424        }
425        version = version
426            .saturating_mul(10)
427            .saturating_add(u32::from(d - b'0'));
428    }
429    if version == 0 {
430        bail!("This is not a Rhino model (invalid version).");
431    }
432    let cur = Cur {
433        data,
434        at: start + 32,
435        end: data.len(),
436        big: version >= 50,
437        v1: version == 1,
438    };
439    Ok((version, cur))
440}
441
442/// Reads a Rhino model into one preview mesh in millimetres.
443pub fn read(data: &[u8]) -> Result<Mesh> {
444    let (version, mut cur) = open(data)?;
445    let mut model = Model {
446        version,
447        opennurbs: 0,
448        unit: 2,
449        hidden_layers: Vec::new(),
450        definitions: HashMap::new(),
451        objects: Vec::new(),
452        triangles: 0,
453    };
454    while let Some(c) = cur.next()? {
455        let Some(body) = c.body else {
456            continue;
457        };
458        match c.tcode {
459            TCODE_PROPERTIES_TABLE => model.properties(body)?,
460            TCODE_SETTINGS_TABLE => model.settings(body)?,
461            TCODE_LAYER_TABLE => model.layers(body)?,
462            TCODE_IDEF_TABLE => model.definitions(body)?,
463            TCODE_OBJECT_TABLE => model.objects(body)?,
464            TCODE_V1_MESH if version == 1 => {
465                // a damaged legacy mesh costs that mesh, not the model
466                if let Ok(part) = v1_mesh(body) {
467                    model.add_triangles(part.triangles())?;
468                    model.objects.push(Object {
469                        id: None,
470                        layer: 0,
471                        mode: 0,
472                        visible: true,
473                        geometry: Geometry::Parts(vec![part]),
474                    });
475                }
476            }
477            TCODE_ENDOFFILE => break,
478            _ => {}
479        }
480    }
481    model.scene()
482}
483
484impl Model {
485    fn add_triangles(&mut self, n: usize) -> Result<()> {
486        self.triangles = self.triangles.saturating_add(n);
487        check_triangles(self.triangles)
488    }
489
490    fn properties(&mut self, mut table: Cur<'_>) -> Result<()> {
491        while let Some(c) = table.next()? {
492            match c.tcode {
493                TCODE_OPENNURBS_VERSION => self.opennurbs = u64::try_from(c.value).unwrap_or(0),
494                TCODE_ENDOFTABLE => break,
495                _ => {}
496            }
497        }
498        Ok(())
499    }
500
501    fn settings(&mut self, mut table: Cur<'_>) -> Result<()> {
502        while let Some(c) = table.next()? {
503            match (c.tcode, c.body) {
504                (TCODE_UNITS, Some(mut b)) => {
505                    let version = b.i32()?;
506                    if (100..200).contains(&version) {
507                        self.unit = b.u32()?;
508                    }
509                }
510                (TCODE_ENDOFTABLE, _) => break,
511                _ => {}
512            }
513        }
514        Ok(())
515    }
516
517    fn layers(&mut self, mut table: Cur<'_>) -> Result<()> {
518        while let Some(c) = table.next()? {
519            match (c.tcode, c.body) {
520                (TCODE_LAYER_RECORD, Some(mut record)) => {
521                    // the layer index is its place in the table
522                    let hidden = read_object(&mut record)
523                        .ok()
524                        .and_then(|(_, data)| data)
525                        .and_then(|mut d| {
526                            let (major, _) = d.version().ok()?;
527                            (major == 1).then(|| d.i32().ok()).flatten()
528                        })
529                        .is_some_and(|mode| mode == 1);
530                    self.hidden_layers.push(hidden);
531                }
532                (TCODE_ENDOFTABLE, _) => break,
533                _ => {}
534            }
535        }
536        Ok(())
537    }
538
539    fn definitions(&mut self, mut table: Cur<'_>) -> Result<()> {
540        while let Some(c) = table.next()? {
541            match (c.tcode, c.body) {
542                (TCODE_IDEF_RECORD, Some(mut record)) => {
543                    let Ok((_, Some(data))) = read_object(&mut record) else {
544                        continue;
545                    };
546                    if let Ok((id, objects)) = definition(data, self.version) {
547                        self.definitions.insert(id, objects);
548                    }
549                }
550                (TCODE_ENDOFTABLE, _) => break,
551                _ => {}
552            }
553        }
554        Ok(())
555    }
556
557    fn objects(&mut self, mut table: Cur<'_>) -> Result<()> {
558        while let Some(c) = table.next()? {
559            match (c.tcode, c.body) {
560                (TCODE_OBJECT_RECORD, Some(record)) => {
561                    // a damaged object costs that object, not the model
562                    match self.object(record) {
563                        Ok(Some(o)) => self.objects.push(o),
564                        Ok(None) => {}
565                        Err(e) if self.triangles > MAX_TRIANGLES => return Err(e),
566                        Err(_) => {}
567                    }
568                }
569                (TCODE_ENDOFTABLE, _) => break,
570                _ => {}
571            }
572        }
573        Ok(())
574    }
575
576    fn object(&mut self, mut record: Cur<'_>) -> Result<Option<Object>> {
577        let mut geometry = None;
578        let mut attributes = None;
579        while record.left() > 0 {
580            let at = record.at;
581            let c = record.chunk()?;
582            match (c.tcode, c.body) {
583                (TCODE_OBJECT_TYPE, _) => {}
584                (TCODE_CLASS, Some(_)) => {
585                    record.at = at;
586                    let (class, data) = read_object(&mut record)?;
587                    if let Some(data) = data {
588                        geometry = self.geometry(class, data)?;
589                    }
590                }
591                (TCODE_OBJECT_ATTRIBUTES, Some(b)) => {
592                    attributes = Some(read_attributes(b, self.version, self.opennurbs)?);
593                }
594                (TCODE_OBJECT_END, _) => break,
595                _ => {}
596            }
597        }
598        let Some(geometry) = geometry else {
599            return Ok(None);
600        };
601        let a = attributes.unwrap_or_default();
602        Ok(Some(Object {
603            id: a.id,
604            layer: a.layer,
605            mode: a.mode,
606            visible: a.visible,
607            geometry,
608        }))
609    }
610
611    fn geometry(&mut self, class: [u8; 16], mut data: Cur<'_>) -> Result<Option<Geometry>> {
612        let parts = if class == MESH {
613            vec![mesh(data)?]
614        } else if BREPS.contains(&class) {
615            brep_meshes(data)?
616        } else if class == EXTRUSION {
617            extrusion_mesh(data)?.into_iter().collect()
618        } else if class == SUBD {
619            subd::mesh(data, self.version)?.into_iter().collect()
620        } else if class == INSTANCE_REF {
621            let (major, _) = data.version()?;
622            if major != 1 {
623                return Ok(None);
624            }
625            let definition = data.uuid()?;
626            let m: [f64; 16] = data.f64s()?;
627            let row = |r: usize| -> [f64; 4] {
628                let mut out = [0.0; 4];
629                for (k, v) in out.iter_mut().enumerate() {
630                    *v = m.get(r * 4 + k).copied().unwrap_or(0.0);
631                }
632                out
633            };
634            return Ok(Some(Geometry::Instance {
635                definition,
636                xform: [row(0), row(1), row(2), row(3)],
637            }));
638        } else {
639            return Ok(None);
640        };
641        let n = parts.iter().map(Part::triangles).sum();
642        self.add_triangles(n)?;
643        Ok((!parts.is_empty()).then_some(Geometry::Parts(parts)))
644    }
645
646    fn hidden(&self, o: &Object) -> bool {
647        o.mode == 1
648            || !o.visible
649            || usize::try_from(o.layer)
650                .ok()
651                .and_then(|l| self.hidden_layers.get(l))
652                .copied()
653                .unwrap_or(false)
654    }
655
656    /// Visible objects, blocks expanded, in millimetres.
657    fn scene(&self) -> Result<Mesh> {
658        let by_id: HashMap<[u8; 16], &Object> = self
659            .objects
660            .iter()
661            .filter_map(|o| o.id.map(|id| (id, o)))
662            .collect();
663        let in_definition: HashSet<[u8; 16]> =
664            self.definitions.values().flatten().copied().collect();
665        let top: Vec<&Object> = self
666            .objects
667            .iter()
668            .filter(|o| o.mode != 3 && !o.id.is_some_and(|id| in_definition.contains(&id)))
669            .filter(|o| !self.hidden(o))
670            .collect();
671        // count first: blocks can multiply the triangles
672        let mut total = 0usize;
673        for o in &top {
674            total = total.saturating_add(self.count(o, &by_id, 0));
675            check_triangles(total)?;
676        }
677        if total == 0 {
678            bail!(
679                "The Rhino model stores no meshes to show (polysurfaces saved without render \
680                 meshes, e.g. with \"Save Small\", curves or points only)."
681            );
682        }
683        let scale = millimetres_per_unit(self.unit);
684        let mut out = Mesh::default();
685        let identity = scaling(scale);
686        for o in top {
687            self.emit(o, &by_id, &identity, 0, &mut out);
688        }
689        Ok(out)
690    }
691
692    fn count(&self, o: &Object, by_id: &HashMap<[u8; 16], &Object>, depth: usize) -> usize {
693        match &o.geometry {
694            Geometry::Parts(parts) => parts.iter().map(Part::triangles).sum(),
695            Geometry::Instance { definition, .. } => {
696                if depth >= MAX_DEPTH {
697                    return 0;
698                }
699                self.members(definition, by_id)
700                    .map(|c| self.count(c, by_id, depth + 1))
701                    .fold(0usize, usize::saturating_add)
702            }
703        }
704    }
705
706    fn members<'s>(
707        &'s self,
708        definition: &[u8; 16],
709        by_id: &'s HashMap<[u8; 16], &'s Object>,
710    ) -> impl Iterator<Item = &'s Object> + 's {
711        self.definitions
712            .get(definition)
713            .into_iter()
714            .flatten()
715            .filter_map(|id| by_id.get(id).copied())
716            .filter(|o| !self.hidden(o))
717    }
718
719    fn emit(
720        &self,
721        o: &Object,
722        by_id: &HashMap<[u8; 16], &Object>,
723        xform: &[[f64; 4]; 4],
724        depth: usize,
725        out: &mut Mesh,
726    ) {
727        match &o.geometry {
728            Geometry::Parts(parts) => {
729                let mut mesh = Mesh {
730                    parts: 1,
731                    ..Mesh::default()
732                };
733                for part in parts {
734                    let base = mesh.vertex_count() as u32;
735                    for p in part.positions.chunks_exact(3) {
736                        if let [x, y, z] = *p {
737                            mesh.positions.extend(apply(xform, [x, y, z]));
738                        }
739                    }
740                    mesh.indices.extend(part.indices.iter().map(|i| i + base));
741                }
742                out.append(weld(mesh));
743            }
744            Geometry::Instance {
745                definition,
746                xform: local,
747            } => {
748                if depth >= MAX_DEPTH {
749                    return;
750                }
751                let child = multiply(xform, local);
752                for c in self.members(definition, by_id) {
753                    self.emit(c, by_id, &child, depth + 1, out);
754                }
755            }
756        }
757    }
758}
759
760fn scaling(s: f64) -> [[f64; 4]; 4] {
761    [
762        [s, 0.0, 0.0, 0.0],
763        [0.0, s, 0.0, 0.0],
764        [0.0, 0.0, s, 0.0],
765        [0.0, 0.0, 0.0, 1.0],
766    ]
767}
768
769/// `a · b` (column-vector transforms: `b` is applied first).
770fn multiply(a: &[[f64; 4]; 4], b: &[[f64; 4]; 4]) -> [[f64; 4]; 4] {
771    let mut out = [[0.0; 4]; 4];
772    for (i, row) in out.iter_mut().enumerate() {
773        for (j, v) in row.iter_mut().enumerate() {
774            *v = (0..4)
775                .map(|k| {
776                    a.get(i).and_then(|r| r.get(k)).copied().unwrap_or(0.0)
777                        * b.get(k).and_then(|r| r.get(j)).copied().unwrap_or(0.0)
778                })
779                .sum();
780        }
781    }
782    out
783}
784
785/// `M · (x, y, z, 1)` with the projective division of openNURBS.
786fn apply(m: &[[f64; 4]; 4], [x, y, z]: [f32; 3]) -> [f32; 3] {
787    let p = [f64::from(x), f64::from(y), f64::from(z), 1.0];
788    let row = |r: &[f64; 4]| r.iter().zip(p).map(|(a, b)| a * b).sum::<f64>();
789    let [r0, r1, r2, r3] = m;
790    let w = row(r3);
791    let w = if w == 0.0 || !w.is_finite() { 1.0 } else { w };
792    [
793        (row(r0) / w) as f32,
794        (row(r1) / w) as f32,
795        (row(r2) / w) as f32,
796    ]
797}
798
799/// Merges vertices that coincide within 1 µm (the faces of a polysurface
800/// are meshed separately; their edges share positions).
801fn weld(mesh: Mesh) -> Mesh {
802    let mut map: HashMap<[i64; 3], u32> = HashMap::with_capacity(mesh.vertex_count());
803    let mut remap = Vec::with_capacity(mesh.vertex_count());
804    let mut out = Mesh {
805        parts: mesh.parts,
806        ..Mesh::default()
807    };
808    for p in mesh.positions.chunks_exact(3) {
809        let key = [0, 1, 2].map(|k| {
810            p.get(k)
811                .map_or(0, |&c| (f64::from(c) * 1000.0).round() as i64)
812        });
813        let next = out.vertex_count() as u32;
814        let index = *map.entry(key).or_insert_with(|| {
815            out.positions.extend_from_slice(p);
816            next
817        });
818        remap.push(index);
819    }
820    for t in mesh.indices.chunks_exact(3) {
821        let [a, b, c] = [0, 1, 2].map(|k| {
822            t.get(k)
823                .and_then(|&i| remap.get(i as usize))
824                .copied()
825                .unwrap_or(0)
826        });
827        if a != b && b != c && a != c {
828            out.indices.extend([a, b, c]);
829        }
830    }
831    out
832}
833
834// ---------------------------------------------------------------------------
835// attributes, block definitions
836// ---------------------------------------------------------------------------
837
838struct Attributes {
839    id: Option<[u8; 16]>,
840    layer: i32,
841    mode: u8,
842    visible: bool,
843}
844
845impl Default for Attributes {
846    fn default() -> Self {
847        Attributes {
848            id: None,
849            layer: 0,
850            mode: 0,
851            visible: true,
852        }
853    }
854}
855
856/// `ON_3dmObjectAttributes`: id, layer, mode and visibility.
857fn read_attributes(mut c: Cur<'_>, version: u32, opennurbs: u64) -> Result<Attributes> {
858    let (_, minor) = c.version()?;
859    let mut a = Attributes {
860        id: Some(c.uuid()?),
861        layer: c.i32()?,
862        ..Attributes::default()
863    };
864    if version >= 5 && opennurbs >= 200_712_190 {
865        // a list of (item id, value) pairs up to item 0; later items are not needed
866        loop {
867            match c.u8()? {
868                0 => break,
869                1 | 2 => c.skip_string()?,
870                3 | 4 | 6 | 7 | 10 | 22 => c.skip(4)?,
871                5 => {
872                    c.body()?;
873                }
874                8 => c.skip(8)?,
875                9 | 13..=17 | 19 => c.skip(1)?,
876                11 => a.visible = c.u8()? != 0,
877                12 => a.mode = c.u8()? & 0x0F,
878                18 => {
879                    let n = c.count(4)?;
880                    c.skip_items(n, 4)?;
881                }
882                20 => c.skip(16)?,
883                21 => {
884                    let n = c.count(32)?;
885                    c.skip_items(n, 32)?;
886                }
887                _ => break,
888            }
889        }
890        return Ok(a);
891    }
892    // material, colour, obsolete line style (2 + 2 + 8 + 8), wire density
893    c.skip(4 + 4 + 20 + 4)?;
894    a.mode = c.u8()? & 0x0F;
895    a.visible = a.mode != 1;
896    c.skip(3)?;
897    c.skip_string()?;
898    c.skip_string()?;
899    if minor >= 1 {
900        let n = c.count(4)?;
901        c.skip_items(n, 4)?;
902        if minor >= 2 {
903            a.visible = c.u8()? != 0;
904        }
905    }
906    Ok(a)
907}
908
909/// `ON_InstanceDefinition`: its id and the ids of its objects.
910fn definition(mut c: Cur<'_>, version: u32) -> Result<([u8; 16], Vec<[u8; 16]>)> {
911    let ids = |c: &mut Cur<'_>| -> Result<Vec<[u8; 16]>> {
912        let n = c.count(16)?;
913        (0..n).map(|_| c.uuid()).collect()
914    };
915    if version >= 60 {
916        let mut probe = c;
917        let first = probe.chunk()?;
918        if let (TCODE_ANONYMOUS, Some(mut a)) = (first.tcode, first.body) {
919            a.version2()?;
920            let mut attributes = a.body()?; // model component attributes
921            attributes.version2()?;
922            let bits = attributes.u32()?;
923            if bits & 1 == 0 {
924                bail!("block definition without id");
925            }
926            let id = attributes.uuid()?;
927            a.skip(4)?; // definition type
928            a.body()?; // unit system
929            a.skip_string()?;
930            a.skip_string()?;
931            a.skip_string()?;
932            a.skip(48)?; // bounding box
933            let objects = if a.u8()? != 0 {
934                ids(&mut a)?
935            } else {
936                Vec::new()
937            };
938            return Ok((id, objects));
939        }
940    }
941    c.version()?;
942    let id = c.uuid()?;
943    Ok((id, ids(&mut c)?))
944}
945
946// ---------------------------------------------------------------------------
947// meshes
948// ---------------------------------------------------------------------------
949
950/// `ReadCompressedBuffer` of exactly `size` bytes: CRC-32, method (0 raw,
951/// 1 zlib in an anonymous chunk), data.
952fn compressed(c: &mut Cur<'_>, size: usize) -> Result<Vec<u8>> {
953    if size == 0 {
954        return Ok(Vec::new());
955    }
956    let crc = c.u32()?;
957    let out = match c.u8()? {
958        0 => c.bytes(size)?.to_vec(),
959        1 => {
960            let chunk = c.chunk()?;
961            let (TCODE_ANONYMOUS, Some(mut z)) = (chunk.tcode, chunk.body) else {
962                bail!("The Rhino file is damaged: compressed data expected.");
963            };
964            let input = z.bytes(z.left())?;
965            // a zlib stream inflates to at most ~1032 times its size
966            if size / 1032 > input.len() {
967                bail!("The Rhino file is damaged: compressed data too short.");
968            }
969            let mut out = Vec::with_capacity(size);
970            flate2::read::ZlibDecoder::new(input)
971                .take(size as u64 + 1)
972                .read_to_end(&mut out)
973                .map_err(|e| anyhow!("The Rhino file is damaged: {e}."))?;
974            out
975        }
976        m => bail!("The Rhino file is damaged: unknown compression {m}."),
977    };
978    if out.len() != size {
979        bail!("The Rhino file is damaged: compressed data has the wrong size.");
980    }
981    let mut check = flate2::Crc::new();
982    check.update(&out);
983    if check.sum() != crc {
984        bail!("The Rhino file is damaged: checksum error in compressed data.");
985    }
986    Ok(out)
987}
988
989fn floats(bytes: &[u8]) -> Vec<f32> {
990    super::quads(bytes).map(f32::from_le_bytes).collect()
991}
992
993/// Quads and triangles (`d == c`) → triangles; faces with an index outside
994/// the vertices or with non-finite vertices are dropped.
995fn part(positions: Vec<f32>, faces: impl Iterator<Item = [u32; 4]>) -> Part {
996    let n = (positions.len() / 3) as u32;
997    let finite = |i: u32| {
998        positions
999            .get(i as usize * 3..i as usize * 3 + 3)
1000            .is_some_and(|p| p.iter().all(|v| v.is_finite()))
1001    };
1002    let mut indices = Vec::new();
1003    for [a, b, c, d] in faces {
1004        if a >= n || b >= n || c >= n || d >= n || ![a, b, c, d].into_iter().all(finite) {
1005            continue;
1006        }
1007        indices.extend([a, b, c]);
1008        if c != d {
1009            indices.extend([a, c, d]);
1010        }
1011    }
1012    Part { positions, indices }
1013}
1014
1015/// `ON_Mesh::Read` (class data).
1016fn mesh(mut c: Cur<'_>) -> Result<Part> {
1017    let (major, _) = c.version()?;
1018    if major != 1 && major != 3 {
1019        bail!("unsupported mesh version {major}");
1020    }
1021    let vcount = c.u32()? as usize;
1022    let fcount = c.u32()? as usize;
1023    if vcount > 4 * MAX_TRIANGLES || fcount > MAX_TRIANGLES {
1024        bail!("The Rhino file is damaged: too many mesh vertices or faces.");
1025    }
1026    // texture and surface domains, scale, boxes (6 + 6 + 4 floats), closed flag
1027    c.skip(8 * 8 + 2 * 8 + 16 * 4 + 4)?;
1028    if c.u8()? != 0 {
1029        c.body()?; // mesh parameters
1030    }
1031    for _ in 0..4 {
1032        if c.u8()? != 0 {
1033            c.body()?; // curvature statistics
1034        }
1035    }
1036    let index_size = c.i32()?;
1037    let index_size = match index_size {
1038        1 | 2 | 4 => index_size as usize,
1039        _ if fcount == 0 => 4,
1040        other => bail!("unsupported face index size {other}"),
1041    };
1042    if fcount.saturating_mul(4 * index_size) > c.left() {
1043        bail!("The Rhino file is damaged: too many mesh faces.");
1044    }
1045    let face_bytes = c.bytes(fcount * 4 * index_size)?;
1046    let index = |i: usize| -> u32 {
1047        let at = i * index_size;
1048        match index_size {
1049            1 => face_bytes.get(at).map_or(u32::MAX, |&b| u32::from(b)),
1050            2 => face_bytes
1051                .get(at..at + 2)
1052                .and_then(|s| <[u8; 2]>::try_from(s).ok())
1053                .map_or(u32::MAX, |b| u32::from(u16::from_le_bytes(b))),
1054            _ => face_bytes
1055                .get(at..at + 4)
1056                .and_then(|s| <[u8; 4]>::try_from(s).ok())
1057                .map_or(u32::MAX, u32::from_le_bytes),
1058        }
1059    };
1060    let positions = if major == 1 {
1061        let n = c.count(12)?;
1062        floats(c.bytes(n * 12)?)
1063    } else if vcount > 0 {
1064        let size = c.u32()? as usize;
1065        if size != 0 && size != vcount.saturating_mul(12) {
1066            bail!("The Rhino file is damaged: vertex buffer has the wrong size.");
1067        }
1068        floats(&compressed(&mut c, size)?)
1069    } else {
1070        Vec::new()
1071    };
1072    let faces = (0..fcount).map(|f| [0, 1, 2, 3].map(|k| index(f * 4 + k)));
1073    Ok(part(positions, faces))
1074}
1075
1076/// The render meshes of an `ON_Brep` (version 3.1 and later), one per face.
1077fn brep_meshes(mut c: Cur<'_>) -> Result<Vec<Part>> {
1078    let (major, minor) = c.version()?;
1079    if major != 3 || minor < 1 {
1080        return Ok(Vec::new());
1081    }
1082    // 2d curves, 3d curves, surfaces, vertices, edges, trims, loops, faces
1083    for _ in 0..8 {
1084        c.body()?;
1085    }
1086    c.skip(48)?; // bounding box
1087    let chunk = c.chunk()?;
1088    let (TCODE_ANONYMOUS, Some(mut meshes)) = (chunk.tcode, chunk.body) else {
1089        bail!("The Rhino file is damaged: B-rep render meshes expected.");
1090    };
1091    let mut out = Vec::new();
1092    while meshes.left() > 0 {
1093        if meshes.u8()? == 0 {
1094            continue;
1095        }
1096        let (class, data) = read_object(&mut meshes)?;
1097        if let (true, Some(data)) = (class == MESH, data) {
1098            out.push(mesh(data)?);
1099        }
1100    }
1101    Ok(out)
1102}
1103
1104/// The render mesh of an `ON_Extrusion` (mesh cache, chunk version 1.3+).
1105fn extrusion_mesh(mut c: Cur<'_>) -> Result<Option<Part>> {
1106    let mut e = c.body()?;
1107    let (major, minor) = e.version2()?;
1108    if major != 1 || minor < 3 {
1109        return Ok(None);
1110    }
1111    read_object(&mut e)?; // profile
1112                          // path, t, up, have N0/N1, N0, N1, path domain, transposed, profile count, caps
1113    e.skip(6 * 8 + 2 * 8 + 3 * 8 + 2 + 6 * 8 + 2 * 8 + 1 + 4 + 2)?;
1114    let mut cache = e.body()?;
1115    cache.version2()?;
1116    while cache.u8()? == 1 {
1117        let mut item = cache.body()?;
1118        item.version2()?;
1119        item.uuid()?;
1120        let (class, data) = read_object(&mut item)?;
1121        if let (true, Some(data)) = (class == MESH, data) {
1122            // the render mesh comes first
1123            return Ok(Some(mesh(data)?));
1124        }
1125    }
1126    Ok(None)
1127}
1128
1129/// A Rhino 1 mesh: points quantised to 16 bits inside the bounding box.
1130fn v1_mesh(mut c: Cur<'_>) -> Result<Part> {
1131    let chunk = c.chunk()?;
1132    let (TCODE_V1_MESH_GEOMETRY, Some(mut g)) = (chunk.tcode, chunk.body) else {
1133        bail!("not a Rhino 1 mesh");
1134    };
1135    let points = g.count(6)?;
1136    let faces = g.count(8)?;
1137    g.skip(8)?; // normals and texture flags
1138    let min: [f64; 3] = g.f64s()?;
1139    let max: [f64; 3] = g.f64s()?;
1140    let mut positions = Vec::with_capacity(points * 3);
1141    for _ in 0..points {
1142        for (lo, hi) in min.iter().zip(max) {
1143            let q = f64::from(g.u16()?);
1144            positions.push((lo + (hi - lo) * q / 65535.0) as f32);
1145        }
1146    }
1147    let wide = points >= 65535;
1148    let mut list = Vec::with_capacity(faces);
1149    for _ in 0..faces {
1150        let mut f = [0u32; 4];
1151        for v in &mut f {
1152            *v = if wide { g.u32()? } else { u32::from(g.u16()?) };
1153        }
1154        list.push(f);
1155    }
1156    Ok(part(positions, list.into_iter()))
1157}
1158
1159// ---------------------------------------------------------------------------
1160// preview image
1161// ---------------------------------------------------------------------------
1162
1163/// The preview bitmap Rhino stores in the properties table, as a DIB
1164/// (BITMAPINFOHEADER + pixels), for files without a 3D preview.
1165pub fn preview_dib(data: &[u8]) -> Option<Vec<u8>> {
1166    let (_, mut cur) = open(data).ok()?;
1167    while let Some(c) = cur.next().ok()? {
1168        if c.tcode == TCODE_PROPERTIES_TABLE {
1169            let mut table = c.body?;
1170            while let Some(c) = table.next().ok()? {
1171                if let (TCODE_COMPRESSED_PREVIEW, Some(mut b)) = (c.tcode, c.body) {
1172                    return dib(&mut b).ok();
1173                }
1174            }
1175            return None;
1176        }
1177    }
1178    None
1179}
1180
1181fn dib(b: &mut Cur<'_>) -> Result<Vec<u8>> {
1182    let mut header = Vec::with_capacity(40);
1183    let _size = b.u32()?;
1184    header.extend(40u32.to_le_bytes());
1185    header.extend(b.bytes(8)?); // width, height
1186    header.extend(b.bytes(4)?); // planes, bit count
1187    header.extend(b.bytes(24)?); // compression … important colours
1188    let pixels = b.u32()? as usize;
1189    if pixels > 64 * 1024 * 1024 {
1190        bail!("preview too large");
1191    }
1192    let mut out = header;
1193    out.extend(compressed(b, pixels)?);
1194    Ok(out)
1195}
1196
1197// ---------------------------------------------------------------------------
1198// SubD
1199// ---------------------------------------------------------------------------
1200
1201mod subd {
1202    //! `ON_SubD`: the level-0 control net and its Catmull-Clark refinement.
1203
1204    use std::collections::HashMap;
1205
1206    use anyhow::{bail, Result};
1207
1208    use super::{Cur, Part, MAX_SUBD_FACES, MAX_SUBD_LEVELS};
1209
1210    /// End of the optional additions of an element (Rhino 7+).
1211    const END: u8 = 255;
1212    const ANONYMOUS: u8 = 254;
1213    const CREASE: u8 = 2;
1214    const CORNER: u8 = 3;
1215
1216    /// The size byte of an optional field: 0 absent, `valid` present, 255 end.
1217    fn addition(c: &mut Cur<'_>, valid: u8) -> Result<u8> {
1218        let sz = c.u8()?;
1219        if sz != 0 && sz != valid && sz != END {
1220            bail!("The Rhino file is damaged: bad SubD data.");
1221        }
1222        Ok(sz)
1223    }
1224
1225    /// Skips the rest of the additions up to the end mark.
1226    fn finish(c: &mut Cur<'_>) -> Result<()> {
1227        loop {
1228            match c.u8()? {
1229                END => return Ok(()),
1230                ANONYMOUS => {
1231                    c.body()?;
1232                }
1233                n => c.skip(usize::from(n))?,
1234            }
1235        }
1236    }
1237
1238    /// The common part of vertices, edges and faces.
1239    fn base(c: &mut Cur<'_>, v7: bool) -> Result<u32> {
1240        let archive_id = c.u32()?;
1241        c.skip(4 + 2)?; // id, level
1242        if !v7 {
1243            if c.u8()? != 0 {
1244                c.skip(24)?;
1245            }
1246            if c.u8()? != 0 {
1247                c.skip(24)?;
1248            }
1249            return Ok(archive_id);
1250        }
1251        for (valid, size) in [(24, 24), (4, 4), (5, 5)] {
1252            match addition(c, valid)? {
1253                END => return Ok(archive_id),
1254                0 => {}
1255                _ => c.skip(size)?,
1256            }
1257        }
1258        finish(c)?;
1259        Ok(archive_id)
1260    }
1261
1262    /// A list of element references (`u16` count, then id + flags).
1263    fn list(c: &mut Cur<'_>) -> Result<Vec<(u32, u8)>> {
1264        let n = usize::from(c.u16()?);
1265        if n.saturating_mul(5) > c.left() {
1266            bail!("The Rhino file is damaged: bad SubD list.");
1267        }
1268        (0..n).map(|_| Ok((c.u32()?, c.u8()?))).collect()
1269    }
1270
1271    fn element_end(c: &mut Cur<'_>, v7: bool) -> Result<()> {
1272        if v7 {
1273            finish(c)
1274        } else if c.u8()? != 0 {
1275            bail!("The Rhino file is damaged: bad SubD element.")
1276        } else {
1277            Ok(())
1278        }
1279    }
1280
1281    struct Net {
1282        points: Vec<[f64; 3]>,
1283        corner: Vec<bool>,
1284        creases: Vec<(u32, u32)>,
1285        faces: Vec<Vec<u32>>,
1286    }
1287
1288    fn read(mut c: Cur<'_>, version: u32) -> Result<Option<Net>> {
1289        let v7 = version >= 70;
1290        if c.u8()? != 1 {
1291            return Ok(None);
1292        }
1293        let mut s = c.body()?;
1294        if s.version2()?.0 != 1 {
1295            return Ok(None);
1296        }
1297        let levels = s.u32()?;
1298        s.skip(12 + 48)?;
1299        if levels == 0 {
1300            return Ok(None);
1301        }
1302        let mut l = s.body()?;
1303        l.version2()?;
1304        l.skip(2 + 3 + 48)?;
1305        let [p0, p1, p2, p3] = [l.u32()?, l.u32()?, l.u32()?, l.u32()?];
1306        if !(p0 <= p1 && p1 <= p2 && p2 <= p3) || (p3 - p0) as usize > l.left() / 16 {
1307            bail!("The Rhino file is damaged: bad SubD counts.");
1308        }
1309        let mut vertex = HashMap::new();
1310        let mut net = Net {
1311            points: Vec::new(),
1312            corner: Vec::new(),
1313            creases: Vec::new(),
1314            faces: Vec::new(),
1315        };
1316        for _ in p0..p1 {
1317            let id = base(&mut l, v7)?;
1318            let tag = l.u8()?;
1319            let p: [f64; 3] = l.f64s()?;
1320            l.skip(4)?; // edge and face counts
1321            if l.u8()? != 0 {
1322                // saved limit points: 12 doubles and a face reference each
1323                let n = l.count(101)?;
1324                l.skip_items(n, 101)?;
1325            }
1326            list(&mut l)?;
1327            list(&mut l)?;
1328            element_end(&mut l, v7)?;
1329            if !p.iter().all(|v| v.is_finite()) {
1330                bail!("The Rhino file is damaged: SubD point not finite.");
1331            }
1332            vertex.insert(id, net.points.len() as u32);
1333            net.points.push(p);
1334            net.corner.push(tag == CORNER);
1335        }
1336        let mut edges = HashMap::new();
1337        for _ in p1..p2 {
1338            let id = base(&mut l, v7)?;
1339            let tag = l.u8()?;
1340            l.skip(2 + 16 + 8)?; // face count, sector coefficients, sharpness
1341            let ends = list(&mut l)?;
1342            list(&mut l)?;
1343            if v7 && version >= 80 {
1344                match l.u8()? {
1345                    END => {}
1346                    8 => {
1347                        l.skip(8)?;
1348                        finish(&mut l)?;
1349                    }
1350                    _ => bail!("The Rhino file is damaged: bad SubD edge."),
1351                }
1352            } else {
1353                element_end(&mut l, v7)?;
1354            }
1355            let end = |k: usize| ends.get(k).and_then(|(v, _)| vertex.get(v)).copied();
1356            if let (Some(a), Some(b)) = (end(0), end(1)) {
1357                edges.insert(id, (a, b));
1358                if tag == CREASE {
1359                    net.creases.push((a, b));
1360                }
1361            }
1362        }
1363        for _ in p2..p3 {
1364            base(&mut l, v7)?;
1365            l.skip(4 + 4 + 2)?; // level-zero face id, parent, edge count
1366            let loop_edges = list(&mut l)?;
1367            if v7 {
1368                // packing (34), material channel, colour, pack id (4 each), texture points
1369                let mut ended = false;
1370                for (i, valid) in [34u8, 4, 4, 4, 4].into_iter().enumerate() {
1371                    match addition(&mut l, valid)? {
1372                        END => {
1373                            ended = true;
1374                            break;
1375                        }
1376                        0 => {}
1377                        _ if i < 4 => l.skip(usize::from(valid))?,
1378                        _ => {
1379                            let tens = l.u32()? as usize;
1380                            for _ in 0..tens {
1381                                if l.u8()? != 240 {
1382                                    bail!("The Rhino file is damaged: bad SubD texture.");
1383                                }
1384                                l.skip(240)?;
1385                            }
1386                            let rest = loop_edges.len() % 10;
1387                            if rest > 0 {
1388                                if usize::from(l.u8()?) != rest * 24 {
1389                                    bail!("The Rhino file is damaged: bad SubD texture.");
1390                                }
1391                                l.skip(rest * 24)?;
1392                            }
1393                        }
1394                    }
1395                }
1396                if !ended {
1397                    finish(&mut l)?;
1398                }
1399            } else {
1400                element_end(&mut l, v7)?;
1401            }
1402            let corners: Option<Vec<u32>> = loop_edges
1403                .iter()
1404                .map(|(e, flags)| {
1405                    edges
1406                        .get(e)
1407                        .map(|&(a, b)| if flags & 1 == 0 { a } else { b })
1408                })
1409                .collect();
1410            if let Some(f) = corners.filter(|f| f.len() >= 3) {
1411                net.faces.push(f);
1412            }
1413        }
1414        Ok(Some(net))
1415    }
1416
1417    fn key(a: u32, b: u32) -> (u32, u32) {
1418        (a.min(b), a.max(b))
1419    }
1420
1421    fn add(a: [f64; 3], b: [f64; 3], s: f64) -> [f64; 3] {
1422        [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s]
1423    }
1424
1425    fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
1426        [a[0] * s, a[1] * s, a[2] * s]
1427    }
1428
1429    fn point(points: &[[f64; 3]], i: u32) -> [f64; 3] {
1430        points.get(i as usize).copied().unwrap_or([0.0; 3])
1431    }
1432
1433    /// Edges of the net with their faces; boundary and crease edges are sharp.
1434    struct Topology {
1435        edge_faces: HashMap<(u32, u32), Vec<usize>>,
1436        vertex_edges: Vec<Vec<(u32, u32)>>,
1437        vertex_faces: Vec<Vec<usize>>,
1438    }
1439
1440    fn topology(n: usize, faces: &[Vec<u32>]) -> Topology {
1441        let mut t = Topology {
1442            edge_faces: HashMap::new(),
1443            vertex_edges: vec![Vec::new(); n],
1444            vertex_faces: vec![Vec::new(); n],
1445        };
1446        for (fi, f) in faces.iter().enumerate() {
1447            for (i, &a) in f.iter().enumerate() {
1448                let b = f.get((i + 1) % f.len()).copied().unwrap_or(a);
1449                t.edge_faces.entry(key(a, b)).or_default().push(fi);
1450                if let Some(vf) = t.vertex_faces.get_mut(a as usize) {
1451                    vf.push(fi);
1452                }
1453            }
1454        }
1455        for &(a, b) in t.edge_faces.keys() {
1456            for v in [a, b] {
1457                if let Some(ve) = t.vertex_edges.get_mut(v as usize) {
1458                    ve.push((a, b));
1459                }
1460            }
1461        }
1462        t
1463    }
1464
1465    fn other(e: (u32, u32), v: u32) -> u32 {
1466        if e.0 == v {
1467            e.1
1468        } else {
1469            e.0
1470        }
1471    }
1472
1473    struct Level {
1474        points: Vec<[f64; 3]>,
1475        corner: Vec<bool>,
1476        creases: std::collections::HashSet<(u32, u32)>,
1477        faces: Vec<Vec<u32>>,
1478    }
1479
1480    /// One Catmull-Clark step (creases: Hoppe et al. rules).
1481    fn subdivide(l: &Level) -> Level {
1482        let n = l.points.len();
1483        let t = topology(n, &l.faces);
1484        let sharp = |e: &(u32, u32)| {
1485            t.edge_faces.get(e).is_none_or(|f| f.len() != 2) || l.creases.contains(e)
1486        };
1487        let face_points: Vec<[f64; 3]> = l
1488            .faces
1489            .iter()
1490            .map(|f| {
1491                let s = f
1492                    .iter()
1493                    .fold([0.0; 3], |acc, &v| add(acc, point(&l.points, v), 1.0));
1494                scale(s, 1.0 / f.len() as f64)
1495            })
1496            .collect();
1497        let face_point = |fi: usize| face_points.get(fi).copied().unwrap_or([0.0; 3]);
1498        let mut edge_index = HashMap::with_capacity(t.edge_faces.len());
1499        let mut edge_points = Vec::with_capacity(t.edge_faces.len());
1500        for (e, faces) in &t.edge_faces {
1501            let mid = scale(add(point(&l.points, e.0), point(&l.points, e.1), 1.0), 0.5);
1502            let p = match (sharp(e), faces.as_slice()) {
1503                (false, [f, g]) => {
1504                    scale(add(add(mid, face_point(*f), 0.5), face_point(*g), 0.5), 0.5)
1505                }
1506                _ => mid,
1507            };
1508            edge_index.insert(*e, (n + edge_points.len()) as u32);
1509            edge_points.push(p);
1510        }
1511        let mut points = Vec::with_capacity(n + edge_points.len() + face_points.len());
1512        for v in 0..n {
1513            let p = point(&l.points, v as u32);
1514            let edges = t.vertex_edges.get(v).map(Vec::as_slice).unwrap_or(&[]);
1515            let sharp_edges: Vec<&(u32, u32)> = edges.iter().filter(|e| sharp(e)).collect();
1516            let corner = l.corner.get(v).copied().unwrap_or(false);
1517            let q = if corner || sharp_edges.len() > 2 || edges.is_empty() {
1518                p
1519            } else if let [a, b] = sharp_edges.as_slice() {
1520                let s = add(
1521                    point(&l.points, other(**a, v as u32)),
1522                    point(&l.points, other(**b, v as u32)),
1523                    1.0,
1524                );
1525                add(scale(p, 0.75), s, 0.125)
1526            } else {
1527                let k = edges.len() as f64;
1528                let faces = t.vertex_faces.get(v).map(Vec::as_slice).unwrap_or(&[]);
1529                let f = scale(
1530                    faces
1531                        .iter()
1532                        .fold([0.0; 3], |acc, &fi| add(acc, face_point(fi), 1.0)),
1533                    1.0 / faces.len().max(1) as f64,
1534                );
1535                let r = scale(
1536                    edges.iter().fold([0.0; 3], |acc, e| {
1537                        add(
1538                            acc,
1539                            add(point(&l.points, e.0), point(&l.points, e.1), 1.0),
1540                            0.5,
1541                        )
1542                    }),
1543                    1.0 / k,
1544                );
1545                scale(add(add(f, r, 2.0), p, k - 3.0), 1.0 / k)
1546            };
1547            points.push(q);
1548        }
1549        points.extend(edge_points);
1550        let first_face = points.len() as u32;
1551        points.extend(face_points.iter().copied());
1552        let mut faces = Vec::new();
1553        let mut creases = std::collections::HashSet::new();
1554        for (fi, f) in l.faces.iter().enumerate() {
1555            let m = f.len();
1556            for (i, &b) in f.iter().enumerate() {
1557                let a = f.get((i + m - 1) % m).copied().unwrap_or(b);
1558                let c = f.get((i + 1) % m).copied().unwrap_or(b);
1559                let (Some(&next), Some(&prev)) =
1560                    (edge_index.get(&key(b, c)), edge_index.get(&key(a, b)))
1561                else {
1562                    continue;
1563                };
1564                faces.push(vec![b, next, first_face + fi as u32, prev]);
1565            }
1566        }
1567        for (e, &mid) in &edge_index {
1568            if sharp(e) {
1569                creases.insert(key(e.0, mid));
1570                creases.insert(key(mid, e.1));
1571            }
1572        }
1573        let mut corner = l.corner.clone();
1574        corner.resize(points.len(), false);
1575        Level {
1576            points,
1577            corner,
1578            creases,
1579            faces,
1580        }
1581    }
1582
1583    /// Moves the vertices of a quad net onto the limit surface.
1584    fn limit(l: &Level) -> Vec<[f64; 3]> {
1585        let t = topology(l.points.len(), &l.faces);
1586        let sharp = |e: &(u32, u32)| {
1587            t.edge_faces.get(e).is_none_or(|f| f.len() != 2) || l.creases.contains(e)
1588        };
1589        (0..l.points.len())
1590            .map(|v| {
1591                let p = point(&l.points, v as u32);
1592                let edges = t.vertex_edges.get(v).map(Vec::as_slice).unwrap_or(&[]);
1593                let sharp_edges: Vec<&(u32, u32)> = edges.iter().filter(|e| sharp(e)).collect();
1594                if l.corner.get(v).copied().unwrap_or(false)
1595                    || sharp_edges.len() > 2
1596                    || edges.is_empty()
1597                {
1598                    return p;
1599                }
1600                if let [a, b] = sharp_edges.as_slice() {
1601                    let s = add(
1602                        point(&l.points, other(**a, v as u32)),
1603                        point(&l.points, other(**b, v as u32)),
1604                        1.0,
1605                    );
1606                    return scale(add(scale(p, 4.0), s, 1.0), 1.0 / 6.0);
1607                }
1608                let n = edges.len() as f64;
1609                let e = edges.iter().fold([0.0; 3], |acc, e| {
1610                    add(acc, point(&l.points, other(*e, v as u32)), 1.0)
1611                });
1612                let faces = t.vertex_faces.get(v).map(Vec::as_slice).unwrap_or(&[]);
1613                let f = faces.iter().fold([0.0; 3], |acc, &fi| {
1614                    let face = l.faces.get(fi).map(Vec::as_slice).unwrap_or(&[]);
1615                    let opposite = face
1616                        .iter()
1617                        .position(|&x| x as usize == v)
1618                        .and_then(|i| face.get((i + 2) % face.len().max(1)))
1619                        .copied()
1620                        .unwrap_or(v as u32);
1621                    add(acc, point(&l.points, opposite), 1.0)
1622                });
1623                scale(
1624                    add(add(scale(p, n * n), e, 4.0), f, 1.0),
1625                    1.0 / (n * (n + 5.0)),
1626                )
1627            })
1628            .collect()
1629    }
1630
1631    /// The SubD as a refined quad mesh (two triangles per quad).
1632    pub(super) fn mesh(data: Cur<'_>, version: u32) -> Result<Option<Part>> {
1633        let Some(net) = read(data, version)? else {
1634            return Ok(None);
1635        };
1636        if net.faces.is_empty() {
1637            return Ok(None);
1638        }
1639        let mut level = Level {
1640            points: net.points,
1641            corner: net.corner,
1642            creases: net.creases.iter().map(|&(a, b)| key(a, b)).collect(),
1643            faces: net.faces,
1644        };
1645        let mut corners: usize = level.faces.iter().map(Vec::len).sum();
1646        let mut steps = 0;
1647        while steps == 0 || (steps < MAX_SUBD_LEVELS && corners.saturating_mul(4) <= MAX_SUBD_FACES)
1648        {
1649            if corners > MAX_SUBD_FACES {
1650                bail!("The SubD has too many faces for the preview.");
1651            }
1652            level = subdivide(&level);
1653            corners = level.faces.len() * 4;
1654            steps += 1;
1655        }
1656        let points = limit(&level);
1657        let mut positions = Vec::with_capacity(points.len() * 3);
1658        for p in &points {
1659            positions.extend(p.map(|v| v as f32));
1660        }
1661        let mut indices = Vec::with_capacity(level.faces.len() * 6);
1662        for f in &level.faces {
1663            if let [a, b, c, d] = f.as_slice() {
1664                indices.extend([*a, *b, *c, *a, *c, *d]);
1665            }
1666        }
1667        Ok(Some(Part { positions, indices }))
1668    }
1669
1670    #[cfg(test)]
1671    mod tests {
1672        use super::*;
1673
1674        /// A 10 mm cube with smooth edges, `steps` times subdivided, on the limit surface.
1675        fn smooth_cube(steps: usize, creased: bool) -> Vec<[f64; 3]> {
1676            let mut points = Vec::new();
1677            for i in 0..8u32 {
1678                points.push([
1679                    f64::from(i & 1) * 10.0,
1680                    f64::from((i >> 1) & 1) * 10.0,
1681                    f64::from((i >> 2) & 1) * 10.0,
1682                ]);
1683            }
1684            let faces = vec![
1685                vec![0, 2, 3, 1],
1686                vec![4, 5, 7, 6],
1687                vec![0, 1, 5, 4],
1688                vec![2, 6, 7, 3],
1689                vec![0, 4, 6, 2],
1690                vec![1, 3, 7, 5],
1691            ];
1692            let mut creases = std::collections::HashSet::new();
1693            if creased {
1694                // the four edges around the top face
1695                for (a, b) in [(4, 5), (5, 7), (7, 6), (6, 4)] {
1696                    creases.insert(key(a, b));
1697                }
1698            }
1699            let mut level = Level {
1700                points,
1701                corner: vec![false; 8],
1702                creases,
1703                faces,
1704            };
1705            for _ in 0..steps {
1706                level = subdivide(&level);
1707            }
1708            limit(&level)
1709        }
1710
1711        fn z_range(points: &[[f64; 3]]) -> (f64, f64) {
1712            points.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| {
1713                (lo.min(p[2]), hi.max(p[2]))
1714            })
1715        }
1716
1717        #[test]
1718        fn the_limit_surface_does_not_depend_on_the_steps() {
1719            let (lo1, hi1) = z_range(&smooth_cube(1, false));
1720            let (lo3, hi3) = z_range(&smooth_cube(3, false));
1721            // centre of a face of the Catmull-Clark limit of a cube: 5 ± 340/81
1722            assert!((hi1 - (5.0 + 340.0 / 81.0)).abs() < 1e-9, "{hi1}");
1723            assert!((lo1 - (5.0 - 340.0 / 81.0)).abs() < 1e-9, "{lo1}");
1724            assert!((hi3 - hi1).abs() < 1e-9 && (lo3 - lo1).abs() < 1e-9);
1725        }
1726
1727        #[test]
1728        fn creases_keep_their_edges() {
1729            let (_, hi) = z_range(&smooth_cube(3, true));
1730            // the creased square stays in the plane z = 10 and pulls the top flat
1731            assert!((hi - 10.0).abs() < 1e-9, "{hi}");
1732        }
1733    }
1734}
1735
1736#[cfg(test)]
1737pub(crate) mod tests {
1738    use super::*;
1739
1740    #[test]
1741    fn uuids_are_stored_little_endian() {
1742        assert_eq!(
1743            MESH,
1744            [
1745                0xE4, 0xD4, 0xD7, 0x4E, 0x47, 0xE9, 0xD3, 0x11, 0xBF, 0xE5, 0x00, 0x10, 0x83, 0x01,
1746                0x22, 0xF0
1747            ]
1748        );
1749    }
1750
1751    #[test]
1752    fn units_become_millimetres() {
1753        assert_eq!(millimetres_per_unit(2), 1.0);
1754        assert_eq!(millimetres_per_unit(4), 1000.0);
1755        assert_eq!(millimetres_per_unit(8), 25.4);
1756        assert_eq!(millimetres_per_unit(0), 1.0);
1757    }
1758
1759    #[test]
1760    fn transforms_compose_as_column_vectors() {
1761        let mut shift = scaling(1.0);
1762        shift[0][3] = 10.0;
1763        let twice = multiply(&scaling(2.0), &shift);
1764        // the shift is applied first, then everything is scaled
1765        assert_eq!(apply(&twice, [1.0, 0.0, 0.0]), [22.0, 0.0, 0.0]);
1766    }
1767
1768    #[test]
1769    fn not_a_rhino_file() {
1770        assert!(read(b"solid cube\nendsolid").is_err());
1771        assert!(read(b"3D Geometry File Format     abc").is_err());
1772    }
1773
1774    #[test]
1775    fn a_header_alone_is_an_empty_model() {
1776        let err = read(b"3D Geometry File Format       50").unwrap_err();
1777        assert!(err.to_string().contains("no meshes"), "{err}");
1778    }
1779
1780    // --- a small version-50 archive written by the test -------------------
1781
1782    fn chunk(tcode: u32, body: &[u8]) -> Vec<u8> {
1783        let mut out = tcode.to_le_bytes().to_vec();
1784        let crc = if tcode & TCODE_CRC != 0 { 4 } else { 0 };
1785        out.extend(((body.len() + crc) as i64).to_le_bytes());
1786        out.extend(body);
1787        out.extend(vec![0; crc]); // the reader does not check chunk CRCs
1788        out
1789    }
1790
1791    fn short(tcode: u32, value: i64) -> Vec<u8> {
1792        let mut out = tcode.to_le_bytes().to_vec();
1793        out.extend(value.to_le_bytes());
1794        out
1795    }
1796
1797    fn object(class: [u8; 16], data: &[u8]) -> Vec<u8> {
1798        let mut body = chunk(TCODE_CLASS_UUID, &class);
1799        body.extend(chunk(TCODE_CLASS_DATA, data));
1800        body.extend(short(TCODE_CLASS_END, 0));
1801        chunk(TCODE_CLASS, &body)
1802    }
1803
1804    fn table(tcode: u32, records: &[Vec<u8>]) -> Vec<u8> {
1805        let mut body = records.concat();
1806        body.extend(short(TCODE_ENDOFTABLE, 0));
1807        chunk(tcode, &body)
1808    }
1809
1810    /// An uncompressed (version 1.0) mesh: one triangle.
1811    fn triangle() -> Vec<u8> {
1812        let mut d = vec![0x10];
1813        d.extend(3i32.to_le_bytes());
1814        d.extend(1i32.to_le_bytes());
1815        d.extend(vec![0; 8 * 8 + 2 * 8 + 16 * 4 + 4]);
1816        d.extend([0, 0, 0, 0, 0]);
1817        d.extend(4i32.to_le_bytes());
1818        for i in [0u32, 1, 2, 2] {
1819            d.extend(i.to_le_bytes());
1820        }
1821        d.extend(3i32.to_le_bytes());
1822        for v in [0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0] {
1823            d.extend(v.to_le_bytes());
1824        }
1825        d
1826    }
1827
1828    fn instance(definition: [u8; 16], [x, y, z]: [f64; 3]) -> Vec<u8> {
1829        let mut d = vec![0x10];
1830        d.extend(definition);
1831        let m = [
1832            1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, y, 0.0, 0.0, 1.0, z, 0.0, 0.0, 0.0, 1.0,
1833        ];
1834        for v in m {
1835            d.extend(f64::to_le_bytes(v));
1836        }
1837        d.extend(vec![0; 48]); // bounding box
1838        d
1839    }
1840
1841    fn record(class: [u8; 16], data: &[u8], id: u8, mode: u8) -> Vec<u8> {
1842        let mut attributes = vec![0x20];
1843        attributes.extend([id; 16]);
1844        attributes.extend(0i32.to_le_bytes());
1845        attributes.extend([12, mode, 0]);
1846        let mut body = short(TCODE_OBJECT_TYPE, 0x20);
1847        body.extend(object(class, data));
1848        body.extend(chunk(TCODE_OBJECT_ATTRIBUTES, &attributes));
1849        body.extend(short(TCODE_OBJECT_END, 0));
1850        chunk(TCODE_OBJECT_RECORD, &body)
1851    }
1852
1853    fn definition(id: u8, members: &[u8]) -> Vec<u8> {
1854        let mut d = vec![0x10];
1855        d.extend([id; 16]);
1856        d.extend((members.len() as i32).to_le_bytes());
1857        for &m in members {
1858            d.extend([m; 16]);
1859        }
1860        chunk(TCODE_IDEF_RECORD, &object([0; 16], &d))
1861    }
1862
1863    /// An 8 × 8 preview bitmap (24 bit), stored raw.
1864    fn preview() -> Vec<u8> {
1865        let mut d = Vec::new();
1866        for v in [40i32, 8, 8] {
1867            d.extend(v.to_le_bytes());
1868        }
1869        d.extend(1i16.to_le_bytes());
1870        d.extend(24i16.to_le_bytes());
1871        d.extend([0u8; 24]);
1872        let pixels = [200u8; 8 * 24];
1873        d.extend((pixels.len() as u32).to_le_bytes());
1874        let mut crc = flate2::Crc::new();
1875        crc.update(&pixels);
1876        d.extend(crc.sum().to_le_bytes());
1877        d.push(0);
1878        d.extend(pixels);
1879        chunk(TCODE_COMPRESSED_PREVIEW, &d)
1880    }
1881
1882    /// Metres; definition 1 = a triangle, definition 2 = definition 1
1883    /// raised by 5 m; drawn: definition 2 at x + 10 m and definition 1 at
1884    /// the origin. A hidden triangle and the definition's own triangle are
1885    /// not drawn.
1886    pub(crate) fn model() -> Vec<u8> {
1887        let mut file = b"3D Geometry File Format       50".to_vec();
1888        file.extend(table(
1889            TCODE_PROPERTIES_TABLE,
1890            &[short(TCODE_OPENNURBS_VERSION, 201_209_145), preview()],
1891        ));
1892        let mut units = 102i32.to_le_bytes().to_vec();
1893        units.extend(4u32.to_le_bytes());
1894        file.extend(table(TCODE_SETTINGS_TABLE, &[chunk(TCODE_UNITS, &units)]));
1895        file.extend(table(
1896            TCODE_IDEF_TABLE,
1897            &[definition(0xD1, &[0xA1]), definition(0xD2, &[0xA2])],
1898        ));
1899        file.extend(table(
1900            TCODE_OBJECT_TABLE,
1901            &[
1902                record(MESH, &triangle(), 0xA1, 3),
1903                record(
1904                    INSTANCE_REF,
1905                    &instance([0xD1; 16], [0.0, 0.0, 5.0]),
1906                    0xA2,
1907                    3,
1908                ),
1909                record(
1910                    INSTANCE_REF,
1911                    &instance([0xD2; 16], [10.0, 0.0, 0.0]),
1912                    0xB1,
1913                    0,
1914                ),
1915                record(INSTANCE_REF, &instance([0xD1; 16], [0.0; 3]), 0xB2, 0),
1916                record(MESH, &triangle(), 0xB3, 1),
1917            ],
1918        ));
1919        file.extend(chunk(TCODE_ENDOFFILE, &[0; 8]));
1920        file
1921    }
1922
1923    #[test]
1924    fn blocks_are_placed_hidden_objects_left_out() {
1925        let mesh = read(&model()).unwrap();
1926        mesh.check().unwrap();
1927        assert_eq!(mesh.triangle_count(), 2);
1928        assert_eq!(mesh.parts, 2);
1929        assert_eq!(mesh.bounds(), [0.0, 0.0, 0.0, 11_000.0, 1000.0, 5000.0]);
1930    }
1931
1932    #[test]
1933    fn the_stored_preview_bitmap_is_found() {
1934        let dib = preview_dib(&model()).unwrap();
1935        assert_eq!(dib.len(), 40 + 8 * 24);
1936        assert_eq!(dib.get(..4), Some(&40u32.to_le_bytes()[..]));
1937        assert!(preview_dib(b"3D Geometry File Format       50").is_none());
1938    }
1939
1940    #[test]
1941    fn damaged_models_are_errors_not_panics() {
1942        let data = model();
1943        for cut in 0..data.len() {
1944            let _ = read(data.get(..cut).unwrap_or_default());
1945        }
1946        for at in 32..data.len() {
1947            for v in [0x00, 0xFF, 0x7F] {
1948                let mut copy = data.clone();
1949                if let Some(b) = copy.get_mut(at) {
1950                    *b = v;
1951                }
1952                let _ = read(&copy);
1953                let _ = preview_dib(&copy);
1954            }
1955        }
1956    }
1957
1958    #[test]
1959    fn welding_joins_coincident_corners() {
1960        let mesh = Mesh {
1961            positions: vec![
1962                0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, // first triangle
1963                1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, // second, sharing an edge
1964            ],
1965            indices: vec![0, 1, 2, 3, 4, 5],
1966            parts: 1,
1967            ..Mesh::default()
1968        };
1969        let w = weld(mesh);
1970        assert_eq!(w.vertex_count(), 4);
1971        assert_eq!(w.triangle_count(), 2);
1972    }
1973}