mesh_loader/stl/
mod.rs

1//! [STL] (.stl) parser.
2//!
3//! [STL]: https://en.wikipedia.org/wiki/STL_(file_format)
4
5mod error;
6
7use std::{io, path::Path, str};
8
9use self::error::ErrorKind;
10use crate::{
11    utils::{
12        bytes::{memchr_naive_table, starts_with},
13        float,
14    },
15    Color4, Material, Mesh, Scene, Vec3,
16};
17
18/// Parses meshes from bytes of binary or ASCII STL.
19#[inline]
20pub fn from_slice(bytes: &[u8]) -> io::Result<Scene> {
21    from_slice_internal(bytes, None, false)
22}
23
24pub(crate) fn from_slice_internal(
25    bytes: &[u8],
26    path: Option<&Path>,
27    parse_color: bool,
28) -> io::Result<Scene> {
29    let mut meshes = Vec::with_capacity(1);
30    if is_ascii_stl(bytes) {
31        match read_ascii_stl(bytes, &mut meshes) {
32            Ok(()) => {
33                let materials = (0..meshes.len()).map(|_| Material::default()).collect();
34                return Ok(Scene { materials, meshes });
35            }
36            // If there is solid but no space or line break after solid or no
37            // facet normal, even valid ASCII text may be binary STL.
38            Err(
39                ErrorKind::NotAscii("solid", _)
40                | ErrorKind::ExpectedSpace("solid", _)
41                | ErrorKind::ExpectedNewline("solid", _)
42                | ErrorKind::Expected("facet", _),
43            ) if meshes.is_empty() => {}
44            Err(e) => return Err(e.into_io_error(bytes, path)),
45        }
46    }
47    match read_binary_header(bytes, parse_color) {
48        Ok(header) => {
49            let mesh = read_binary_triangles(&header);
50            let mut material = Material::default();
51            if header.reverse_color && mesh.colors[0].is_empty() {
52                let color = header.default_color;
53                material.color.diffuse = Some(color);
54                material.color.specular = Some(color);
55            }
56            meshes.push(mesh);
57            Ok(Scene {
58                materials: vec![material],
59                meshes,
60            })
61        }
62        Err(e) => Err(e.into_io_error(bytes, path)),
63    }
64}
65
66// An ASCII STL buffer will begin with "solid NAME", where NAME is optional.
67// Note: The "solid NAME" check is necessary, but not sufficient, to determine
68// if the buffer is ASCII; a binary header could also begin with "solid NAME".
69fn is_ascii_stl(mut bytes: &[u8]) -> bool {
70    // Use skip_spaces_and_lines_until_token instead of starts_with here
71    // because some ASCII STL files has space before solid.
72    // https://grep.app/search?q=%5E%20endsolid&regexp=true&case=true
73    let is_ascii = skip_spaces_and_lines_until_token(&mut bytes, b"solid");
74
75    if is_ascii {
76        // This check is now performed with a delay within read_ascii_stl.
77        // See the comment on ASCII check for stings after solid for more.
78        // // A lot of importers are write solid even if the file is binary.
79        // // So we have to check for ASCII-characters.
80        // if !bytes.is_ascii() {
81        //     is_ascii = false;
82        // }
83    }
84    is_ascii
85}
86
87// -----------------------------------------------------------------------------
88// Binary STL
89
90/*
91https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL
92
93UINT8[80]    – Header                 -     80 bytes
94UINT32       – Number of triangles    -      4 bytes
95
96foreach triangle                      - 50 bytes:
97    REAL32[3] – Normal vector             - 12 bytes
98    REAL32[3] – Vertex 1                  - 12 bytes
99    REAL32[3] – Vertex 2                  - 12 bytes
100    REAL32[3] – Vertex 3                  - 12 bytes
101    UINT16    – Attribute byte count      -  2 bytes
102end
103*/
104const HEADER_SIZE: usize = 80;
105const TRIANGLE_COUNT_SIZE: usize = 4;
106const TRIANGLE_START: usize = HEADER_SIZE + TRIANGLE_COUNT_SIZE;
107const TRIANGLE_SIZE: usize = 50;
108
109struct BinaryHeader<'a> {
110    default_color: Color4,
111    parse_color: bool,
112    reverse_color: bool,
113    triangle_bytes: &'a [u8],
114}
115
116fn read_binary_header(bytes: &[u8], parse_color: bool) -> Result<BinaryHeader<'_>, ErrorKind> {
117    if bytes.len() < TRIANGLE_START {
118        return Err(ErrorKind::TooSmall);
119    }
120
121    let header = &bytes[..HEADER_SIZE];
122    let triangle_bytes = &bytes[TRIANGLE_START..];
123
124    let extra_bytes = triangle_bytes.len() % TRIANGLE_SIZE;
125    if extra_bytes != 0 {
126        if extra_bytes == 1 && triangle_bytes.ends_with(b"\n")
127            || extra_bytes == 2 && triangle_bytes.ends_with(b"\r\n")
128        {
129            // Some buggy STL files have a newline after triangles...
130        } else {
131            return Err(ErrorKind::InvalidSize);
132        }
133    }
134
135    // Some STL files contain bogus count.
136    // So we calculate num_triangles based on the size of the input.
137    // let num_triangles = &bytes[HEADER_SIZE..TRIANGLE_START];
138    // let num_triangles = u32::from_le_bytes(num_triangles.try_into().unwrap());
139    // assert_eq!(triangle_bytes.len() / TRIANGLE_SIZE, num_triangles as usize);
140    let num_triangles = triangle_bytes.len() / TRIANGLE_SIZE;
141    let num_vertices = num_triangles * 3;
142    if u32::try_from(num_vertices).is_err() {
143        // face is [u32; 3], so num_vertices must not exceed u32::MAX.
144        return Err(ErrorKind::TooManyTriangles);
145    }
146
147    // Use the same default color (light gray) as assimp: https://github.com/assimp/assimp/blob/v5.3.1/code/AssetLib/STL/STLLoader.cpp#L183-L184
148    let mut default_color = [0.6, 0.6, 0.6, 0.6];
149    let mut reverse_color = false;
150    if parse_color {
151        // Handling colors in STL is not standardized. We use the same way as assimp.
152        // https://github.com/assimp/assimp/blob/v5.3.1/code/AssetLib/STL/STLLoader.cpp#L413-L431
153        let mut s = header;
154        let expect = b"COLOR=";
155        while s.len() >= expect.len() + 4 {
156            if token(&mut s, expect) {
157                const INV_BYTE: f32 = 1. / 255.;
158                reverse_color = true;
159                default_color = [
160                    s[0] as f32 * INV_BYTE,
161                    s[1] as f32 * INV_BYTE,
162                    s[2] as f32 * INV_BYTE,
163                    s[3] as f32 * INV_BYTE,
164                ];
165                break;
166            }
167            s = &s[1..];
168        }
169    }
170
171    Ok(BinaryHeader {
172        default_color,
173        parse_color,
174        reverse_color,
175        triangle_bytes,
176    })
177}
178
179fn read_binary_triangles(header: &BinaryHeader<'_>) -> Mesh {
180    let bytes = header.triangle_bytes;
181
182    let chunks = bytes.chunks_exact(TRIANGLE_SIZE);
183    let num_triangles = chunks.len();
184    let num_vertices = num_triangles * 3;
185    // Even if we allocate capacity with reserve_exact, the compiler does not
186    // seem to be able to remove the capacity check in push/extend_from_slice,
187    // so we first allocate zeros and then copy the actual data to it.
188    // If the size is relatively small, the fastest way here is to allocate Vec,
189    // write to it using unsafe ways, and finally call set_len.
190    // However, as the size increases, this way becomes equivalent performance
191    // (at least on x86_64 Linux & AArch64 macOS), and in some cases this way is
192    // finally 10% faster (at least on AArch64 macOS).
193    let mut mesh = Mesh {
194        vertices: vec![[0., 0., 0.]; num_vertices],
195        normals: vec![[0., 0., 0.]; num_vertices],
196        faces: vec![[0, 0, 0]; num_triangles],
197        ..Default::default()
198    };
199
200    let mut vertices_len = 0;
201    let has_color_mask = if header.parse_color { 1 << 15 } else { 0 };
202
203    for (((chunk, vertices), normals), face) in chunks
204        .zip(mesh.vertices.chunks_exact_mut(3))
205        .zip(mesh.normals.chunks_exact_mut(3))
206        .zip(&mut mesh.faces)
207    {
208        let triangle = read_binary_triangle(chunk);
209
210        vertices.clone_from_slice(&triangle.vertices);
211        normals.clone_from_slice(&[triangle.normal; 3]);
212        *face = [vertices_len, vertices_len + 1, vertices_len + 2];
213
214        // Handling colors in STL is not standardized. We use the same way as assimp.
215        // https://github.com/assimp/assimp/blob/v5.3.1/code/AssetLib/STL/STLLoader.cpp#L502-L529
216        if triangle.color & has_color_mask != 0 {
217            const INV_VAL: f32 = 1. / 31.;
218            if mesh.colors[0].is_empty() {
219                mesh.colors[0] = vec![header.default_color; num_vertices];
220            }
221            let a = 1.;
222            let color = if header.reverse_color {
223                let r = (triangle.color & 0x1F) as f32 * INV_VAL;
224                let g = ((triangle.color & (0x1F << 5)) >> 5) as f32 * INV_VAL;
225                let b = ((triangle.color & (0x1F << 10)) >> 10) as f32 * INV_VAL;
226                [r, g, b, a]
227            } else {
228                let b = (triangle.color & 0x1F) as f32 * INV_VAL;
229                let g = ((triangle.color & (0x1F << 5)) >> 5) as f32 * INV_VAL;
230                let r = ((triangle.color & (0x1F << 10)) >> 10) as f32 * INV_VAL;
231                [r, g, b, a]
232            };
233            mesh.colors[0][vertices_len as usize..vertices_len as usize + 3]
234                .copy_from_slice(&[color, color, color]);
235        }
236
237        vertices_len += 3;
238    }
239
240    mesh
241}
242
243#[inline]
244fn read_binary_triangle(mut buf: &[u8]) -> Triangle {
245    #[inline]
246    fn f32le(buf: &mut &[u8]) -> f32 {
247        let f = f32::from_le_bytes(buf[..4].try_into().unwrap());
248        *buf = &buf[4..];
249        f
250    }
251
252    let normal = [f32le(&mut buf), f32le(&mut buf), f32le(&mut buf)];
253    let vertex1 = [f32le(&mut buf), f32le(&mut buf), f32le(&mut buf)];
254    let vertex2 = [f32le(&mut buf), f32le(&mut buf), f32le(&mut buf)];
255    let vertex3 = [f32le(&mut buf), f32le(&mut buf), f32le(&mut buf)];
256    let color = u16::from_le_bytes(buf[..2].try_into().unwrap());
257    Triangle {
258        normal,
259        vertices: [vertex1, vertex2, vertex3],
260        color,
261    }
262}
263
264// -----------------------------------------------------------------------------
265// ASCII STL
266
267/*
268https://en.wikipedia.org/wiki/STL_(file_format)#ASCII_STL
269
270solid name
271
272facet normal ni nj nk
273  outer loop
274    vertex v1x v1y v1z
275    vertex v2x v2y v2z
276    vertex v3x v3y v3z
277  endloop
278endfacet
279
280endsolid name
281*/
282fn read_ascii_stl(mut s: &[u8], meshes: &mut Vec<Mesh>) -> Result<(), ErrorKind> {
283    loop {
284        let mut mesh = Mesh::default();
285
286        // solid [name]
287        let expected = "solid";
288        if !skip_spaces_and_lines_until_token(&mut s, expected.as_bytes()) {
289            if s.is_empty() {
290                // eof
291                if meshes.is_empty() {
292                    return Err(ErrorKind::Expected(expected, s.len()));
293                }
294                break;
295            }
296            return Err(ErrorKind::Expected(expected, s.len()));
297        }
298        if !skip_spaces(&mut s) {
299            return Err(ErrorKind::ExpectedSpace(expected, s.len()));
300        }
301        match memchr_naive_table(LINE, &TABLE, s) {
302            Some(n) => {
303                let mut name = &s[..n];
304                // The only strings we need to explicitly check for ASCII are the
305                // strings after solid and endsolid. Any other occurrence of
306                // a non-ASCII character elsewhere will result in the normal syntax
307                // error of simply not finding the expected character or whitespace.
308                if !name.is_ascii() {
309                    return Err(ErrorKind::NotAscii(expected, s.len()));
310                }
311                if let Some(n) = memchr_naive_table(SPACE, &TABLE, name) {
312                    // Ignore contents after the name.
313                    // https://en.wikipedia.org/wiki/STL_(file_format)#ASCII
314                    // > The remainder of the line is ignored and is sometimes used to
315                    // > store metadata (e.g., filename, author, modification date, etc).
316                    name = &name[..n];
317                }
318                let name = str::from_utf8(name).unwrap();
319                Mesh::set_name(&mut mesh, name);
320                s = &s[n + 1..];
321            }
322            None => return Err(ErrorKind::ExpectedNewline(expected, s.len())),
323        }
324
325        loop {
326            // facet normal <n1> <n2> <n3>
327            // Note: space in facet and normal can be multiple
328            // https://github.com/apache/commons-geometry/blob/fb537c8505644262f70fde6e4a0b109e06363340/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReaderTest.java#L124-L125
329            let expected = "facet";
330            if !skip_spaces_and_lines_until_token(&mut s, expected.as_bytes()) {
331                break;
332            }
333            if !skip_spaces(&mut s) {
334                return Err(ErrorKind::ExpectedSpace(expected, s.len()));
335            }
336            let expected = "normal";
337            if !token(&mut s, expected.as_bytes()) {
338                return Err(ErrorKind::Expected(expected, s.len()));
339            }
340            let mut normal = [0.; 3];
341            for normal in &mut normal {
342                if !skip_spaces(&mut s) {
343                    return Err(ErrorKind::ExpectedSpace(expected, s.len()));
344                }
345                match float::parse_partial::<f32>(s) {
346                    Some((f, n)) => {
347                        *normal = f;
348                        s = &s[n..];
349                    }
350                    None => return Err(ErrorKind::Float(s.len())),
351                }
352            }
353            if !skip_spaces_until_line(&mut s) {
354                return Err(ErrorKind::ExpectedNewline(expected, s.len()));
355            }
356
357            // outer loop
358            // Note: space in facet and normal can be multiple
359            // https://github.com/apache/commons-geometry/blob/fb537c8505644262f70fde6e4a0b109e06363340/commons-geometry-io-euclidean/src/test/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlFacetDefinitionReaderTest.java#L124-L125
360            let expected = "outer";
361            if !skip_spaces_and_lines_until_token(&mut s, expected.as_bytes()) {
362                return Err(ErrorKind::Expected(expected, s.len()));
363            }
364            if !skip_spaces(&mut s) {
365                return Err(ErrorKind::ExpectedSpace(expected, s.len()));
366            }
367            let expected = "loop";
368            if !token(&mut s, expected.as_bytes()) {
369                return Err(ErrorKind::Expected(expected, s.len()));
370            }
371            if !skip_spaces_until_line(&mut s) {
372                return Err(ErrorKind::ExpectedNewline(expected, s.len()));
373            }
374
375            // vertex <v1x> <v1y> <v1z>
376            // vertex <v2x> <v2y> <v2z>
377            // vertex <v3x> <v3y> <v3z>
378            let expected = "vertex";
379            let mut vertices = [[0.; 3]; 3];
380            for vertex in &mut vertices {
381                if !skip_spaces_and_lines_until_token(&mut s, expected.as_bytes()) {
382                    return Err(ErrorKind::Expected(expected, s.len()));
383                }
384                for vertex in vertex {
385                    if !skip_spaces(&mut s) {
386                        return Err(ErrorKind::ExpectedSpace(expected, s.len()));
387                    }
388                    match float::parse_partial::<f32>(s) {
389                        Some((f, n)) => {
390                            *vertex = f;
391                            s = &s[n..];
392                        }
393                        None => return Err(ErrorKind::Float(s.len())),
394                    }
395                }
396                if !skip_spaces_until_line(&mut s) {
397                    return Err(ErrorKind::ExpectedNewline(expected, s.len()));
398                }
399            }
400
401            // endloop
402            let expected = "endloop";
403            if !skip_spaces_and_lines_until_token(&mut s, expected.as_bytes()) {
404                return Err(ErrorKind::Expected(expected, s.len()));
405            }
406            if !skip_spaces_until_line(&mut s) {
407                return Err(ErrorKind::ExpectedNewline(expected, s.len()));
408            }
409
410            // endfacet
411            let expected = "endfacet";
412            if !skip_spaces_and_lines_until_token(&mut s, expected.as_bytes()) {
413                return Err(ErrorKind::Expected(expected, s.len()));
414            }
415            if !skip_spaces_until_line(&mut s) {
416                return Err(ErrorKind::ExpectedNewline(expected, s.len()));
417            }
418
419            Mesh::push_triangle(
420                &mut mesh,
421                Triangle {
422                    normal,
423                    vertices,
424                    color: 0,
425                },
426            );
427        }
428
429        // endsolid [name]
430        let expected = "endsolid";
431        if !token(&mut s, expected.as_bytes()) {
432            return Err(ErrorKind::Expected(expected, s.len()));
433        }
434        // Skip checking endsolid because some exporters have generated the wrong STL about endsolid.
435        // https://github.com/assimp/assimp/issues/3756
436        match memchr_naive_table(LINE, &TABLE, s) {
437            Some(n) => {
438                if !s[..n].is_ascii() {
439                    return Err(ErrorKind::NotAscii(expected, s.len())); // See the comment on ASCII check for stings after solid for more.
440                }
441                s = &s[n + 1..];
442            }
443            None => {
444                if !s.is_ascii() {
445                    return Err(ErrorKind::NotAscii(expected, s.len())); // See the comment on ASCII check for stings after solid for more.
446                }
447                s = &[];
448            }
449        }
450
451        meshes.push(mesh);
452    }
453
454    Ok(())
455}
456
457// -----------------------------------------------------------------------------
458// Helpers
459
460// [ \r\n\t]
461// Note: Unlike is_ascii_whitespace, FORM FEED ('\x0C') is not included.
462// https://en.wikipedia.org/wiki/STL_(file_format)#ASCII
463// > Whitespace (spaces, tabs, newlines) may be used anywhere in the file except within numbers or words.
464const WHITESPACE: u8 = SPACE | LINE;
465// [\r\n]
466const LINE: u8 = 1 << 0;
467// [ \t]
468const SPACE: u8 = 1 << 1;
469// [s]
470const S_: u8 = 1 << 2;
471// [e]
472const E_: u8 = 1 << 3;
473// [f]
474const F_: u8 = 1 << 4;
475// [o]
476const O_: u8 = 1 << 5;
477// [v]
478const V_: u8 = 1 << 6;
479
480static TABLE: [u8; 256] = {
481    const __: u8 = 0;
482    const LN: u8 = LINE;
483    const NL: u8 = SPACE;
484    [
485        //  _1  _2  _3  _4  _5  _6  _7  _8  _9  _A  _B  _C  _D  _E  _F
486        __, __, __, __, __, __, __, __, __, NL, LN, __, __, LN, __, __, // 0_
487        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 1_
488        NL, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2_
489        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 3_
490        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 4_
491        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 5_
492        __, __, __, __, __, E_, F_, __, __, __, __, __, __, __, __, O_, // 6_
493        __, __, __, S_, __, __, V_, __, __, __, __, __, __, __, __, __, // 7_
494        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8_
495        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9_
496        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A_
497        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B_
498        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C_
499        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D_
500        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E_
501        __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F_
502    ]
503};
504#[test]
505fn table() {
506    for b in u8::MIN..=u8::MAX {
507        match b {
508            b' ' | b'\t' => {
509                assert_eq!(TABLE[b as usize], SPACE, "{:?}({b:#X})", b as char);
510            }
511            b'\n' | b'\r' => {
512                assert_eq!(TABLE[b as usize], LINE, "{:?}({b:#X})", b as char);
513            }
514            b's' => {
515                assert_eq!(TABLE[b as usize], S_, "{:?}({b:#X})", b as char);
516            }
517            b'e' => {
518                assert_eq!(TABLE[b as usize], E_, "{:?}({b:#X})", b as char);
519            }
520            b'f' => {
521                assert_eq!(TABLE[b as usize], F_, "{:?}({b:#X})", b as char);
522            }
523            b'o' => {
524                assert_eq!(TABLE[b as usize], O_, "{:?}({b:#X})", b as char);
525            }
526            b'v' => {
527                assert_eq!(TABLE[b as usize], V_, "{:?}({b:#X})", b as char);
528            }
529            _ => assert_eq!(TABLE[b as usize], 0, "{:?}({b:#X})", b as char),
530        }
531    }
532}
533
534#[inline]
535fn skip_whitespace_until_byte(s: &mut &[u8], byte_mask: u8, whitespace_mask: u8) -> bool {
536    while let Some((&b, s_next)) = s.split_first() {
537        let b = TABLE[b as usize];
538        if b & byte_mask != 0 {
539            *s = s_next;
540            return true;
541        }
542        if b & whitespace_mask != 0 {
543            *s = s_next;
544            continue;
545        }
546        break;
547    }
548    false
549}
550
551#[inline]
552fn skip_spaces_until_line(s: &mut &[u8]) -> bool {
553    skip_whitespace_until_byte(s, LINE, SPACE)
554}
555
556#[inline]
557fn skip_spaces(s: &mut &[u8]) -> bool {
558    let start = *s;
559    while let Some((&b, s_next)) = s.split_first() {
560        let b = TABLE[b as usize];
561        if b & SPACE != 0 {
562            *s = s_next;
563            continue;
564        }
565        break;
566    }
567    start.len() != s.len()
568}
569
570#[inline]
571fn token(s: &mut &[u8], token: &'static [u8]) -> bool {
572    if starts_with(s, token) {
573        *s = &s[token.len()..];
574        true
575    } else {
576        false
577    }
578}
579
580#[inline(always)] // Ensure the code creating token_start_mask and check_start is inlined.
581fn skip_spaces_and_lines_until_token(s: &mut &[u8], token: &'static [u8]) -> bool {
582    let token_start_mask = TABLE[token[0] as usize];
583    debug_assert_ne!(token_start_mask, 0);
584    let check_start = match token.len() {
585        4 | 8 | 12 | 16 => 0,
586        _ => 1,
587    };
588    while let Some((&b, s_next)) = s.split_first() {
589        let b = TABLE[b as usize];
590        if b & token_start_mask != 0 {
591            if starts_with(&s[check_start..], &token[check_start..]) {
592                *s = &s[token.len()..];
593                return true;
594            }
595            break;
596        }
597        if b & WHITESPACE != 0 {
598            *s = s_next;
599            continue;
600        }
601        break;
602    }
603    false
604}
605
606struct Triangle {
607    normal: Vec3,
608    vertices: [Vec3; 3],
609    color: u16,
610}
611
612trait FromStl: Sized {
613    type Context;
614
615    /// Appends a triangle.
616    fn push_triangle(cx: &mut Self::Context, triangle: Triangle);
617
618    /// Sets the name.
619    fn set_name(cx: &mut Self::Context, name: &str);
620}
621
622impl FromStl for Mesh {
623    type Context = Self;
624
625    #[inline]
626    fn push_triangle(mesh: &mut Self::Context, triangle: Triangle) {
627        // With binary STL, reserve checks that the max length of cx.vertices
628        // will not be greater than u32::MAX.
629        // With ASCII STL, the max length of cx.vertices will not be too large,
630        // since much more bytes is required per triangle than for binary STL.
631        #[allow(clippy::cast_possible_truncation)]
632        let vertices_indices = [
633            mesh.vertices.len() as u32,
634            (mesh.vertices.len() + 1) as u32,
635            (mesh.vertices.len() + 2) as u32,
636        ];
637
638        mesh.vertices.extend_from_slice(&triangle.vertices);
639        mesh.normals.resize(mesh.normals.len() + 3, triangle.normal);
640        mesh.faces.push(vertices_indices);
641    }
642
643    fn set_name(mesh: &mut Self::Context, name: &str) {
644        mesh.name = name.to_owned();
645    }
646}