Skip to main content

teamcraft/mesh/
model.rs

1//! The mesh itself: [`Mesh`], scalar [`Field`]s, the colour map and the `TCMESH01` wire format the viewer draws.
2
3use anyhow::{bail, Context, Result};
4
5use super::*;
6
7/// Largest triangle count that is previewed.
8pub const MAX_TRIANGLES: usize = 8_000_000;
9/// Point clouds with more points are thinned out evenly.
10pub const MAX_POINTS: usize = 3_000_000;
11
12const MAGIC: &[u8; 8] = b"TCMESH01";
13/// Colour of parts without own colour when they are merged with coloured ones.
14const NEUTRAL: [u8; 3] = [176, 188, 200];
15
16/// Triangles (or points when `indices` is empty) in model coordinates.
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct Mesh {
19    /// x, y, z per vertex.
20    pub positions: Vec<f32>,
21    /// Three vertex indices per triangle; empty for a point cloud.
22    pub indices: Vec<u32>,
23    /// r, g, b per vertex – empty, or three bytes for every vertex.
24    pub colors: Vec<u8>,
25    /// Number of bodies / objects (for the status line of the viewer).
26    pub parts: u32,
27    /// Scalar result fields, one value per vertex (simulation results).
28    pub fields: Vec<Field>,
29    /// A partial preview: (faces shown, faces in the file) – STEP files
30    /// whose B-rep the kernel could tessellate only in part.
31    pub faces: Option<(u32, u32)>,
32}
33
34/// Fields a mesh carries at most (the viewer offers them in a selector).
35pub const MAX_FIELDS: usize = 8;
36
37/// A scalar result field on the vertices of a [`Mesh`]: a measured value,
38/// the magnitude of a vector or the von Mises stress of a tensor. Values
39/// that are not finite mark vertices without a value (drawn neutral).
40#[derive(Clone, Debug, Default, PartialEq)]
41pub struct Field {
42    pub name: String,
43    /// Unit if the file names one, otherwise empty.
44    pub unit: String,
45    pub values: Vec<f32>,
46}
47
48impl Field {
49    /// Smallest and largest finite value (`(0, 0)` when there is none).
50    pub fn range(&self) -> (f32, f32) {
51        let mut lo = f32::INFINITY;
52        let mut hi = f32::NEG_INFINITY;
53        for &v in &self.values {
54            if v.is_finite() {
55                lo = lo.min(v);
56                hi = hi.max(v);
57            }
58        }
59        if lo > hi {
60            (0.0, 0.0)
61        } else {
62            (lo, hi)
63        }
64    }
65}
66
67/// Stops of the colour map (perceptually uniform, after matplotlib's
68/// "viridis", CC0). `shared/js/viewer.js` has the same table.
69const COLOR_MAP: [[u8; 3]; 9] = [
70    [68, 1, 84],
71    [71, 45, 123],
72    [59, 82, 139],
73    [44, 114, 142],
74    [33, 145, 140],
75    [40, 174, 128],
76    [94, 201, 98],
77    [173, 220, 48],
78    [253, 231, 37],
79];
80
81/// Colour of `t` in `0..=1` on the colour map (NaN = neutral grey).
82pub fn color_map(t: f32) -> [u8; 3] {
83    if !t.is_finite() {
84        return NEUTRAL;
85    }
86    let x = t.clamp(0.0, 1.0) * (COLOR_MAP.len() - 1) as f32;
87    let i = (x.floor() as usize).min(COLOR_MAP.len() - 2);
88    let f = x - i as f32;
89    let (Some(a), Some(b)) = (COLOR_MAP.get(i), COLOR_MAP.get(i + 1)) else {
90        return NEUTRAL;
91    };
92    let mut rgb = [0u8; 3];
93    for ((out, &a), &b) in rgb.iter_mut().zip(a).zip(b) {
94        *out = (a as f32 + (b as f32 - a as f32) * f).round() as u8;
95    }
96    rgb
97}
98
99impl Mesh {
100    pub fn vertex_count(&self) -> usize {
101        self.positions.len() / 3
102    }
103
104    pub fn triangle_count(&self) -> usize {
105        self.indices.len() / 3
106    }
107
108    pub fn is_point_cloud(&self) -> bool {
109        self.indices.is_empty()
110    }
111
112    pub(crate) fn vertex(&mut self, p: [f64; 3]) -> u32 {
113        let index = self.vertex_count() as u32;
114        self.positions
115            .extend([p[0] as f32, p[1] as f32, p[2] as f32]);
116        index
117    }
118
119    /// Appends `other` (indices are shifted; colours are filled in so that
120    /// either every vertex or none has a colour).
121    pub fn append(&mut self, mut other: Mesh) {
122        let offset = self.vertex_count() as u32;
123        // fields: those both meshes have (by name) survive
124        if self.positions.is_empty() {
125            self.fields = std::mem::take(&mut other.fields);
126        } else {
127            let mut theirs = std::mem::take(&mut other.fields);
128            self.fields
129                .retain_mut(|f| match theirs.iter_mut().find(|o| o.name == f.name) {
130                    Some(o) => {
131                        f.values.append(&mut o.values);
132                        true
133                    }
134                    None => false,
135                });
136        }
137        if self.colors.is_empty() != other.colors.is_empty() {
138            if self.colors.is_empty() {
139                self.colors = NEUTRAL.repeat(self.vertex_count());
140            } else {
141                other.colors = NEUTRAL.repeat(other.vertex_count());
142            }
143        }
144        self.positions.append(&mut other.positions);
145        self.indices
146            .extend(other.indices.iter().map(|i| i + offset));
147        self.colors.append(&mut other.colors);
148        self.parts += other.parts.max(1);
149    }
150
151    /// Appends the geometry of `other` with every vertex moved by `place`:
152    /// what [`Mesh::append`] does with a placed copy of `other`, without
153    /// making that copy first (an assembly places the same part many times).
154    /// Indices are shifted and colours filled in the same way; result fields
155    /// and the partial-preview note belong to one file and are not carried
156    /// over.
157    pub fn append_placed(&mut self, other: &Mesh, place: impl Fn([f64; 3]) -> [f64; 3]) {
158        let offset = self.vertex_count() as u32;
159        self.fields.clear();
160        if self.colors.is_empty() && !other.colors.is_empty() {
161            self.colors = NEUTRAL.repeat(self.vertex_count());
162        }
163        if !self.colors.is_empty() || !other.colors.is_empty() {
164            if other.colors.is_empty() {
165                self.colors
166                    .extend_from_slice(&NEUTRAL.repeat(other.vertex_count()));
167            } else {
168                self.colors.extend_from_slice(&other.colors);
169            }
170        }
171        // copied in one piece, then moved where they are
172        let first = self.positions.len();
173        self.positions.extend_from_slice(&other.positions);
174        if let Some(placed) = self.positions.get_mut(first..) {
175            for v in placed.chunks_exact_mut(3) {
176                if let [x, y, z] = v {
177                    let q = place([f64::from(*x), f64::from(*y), f64::from(*z)]);
178                    *x = q[0] as f32;
179                    *y = q[1] as f32;
180                    *z = q[2] as f32;
181                }
182            }
183        }
184        self.indices
185            .extend(other.indices.iter().map(|i| i + offset));
186        self.parts += other.parts.max(1);
187    }
188
189    /// Validates the mesh: finite coordinates, indices in range, limits.
190    pub fn check(&self) -> Result<()> {
191        if self.positions.is_empty() {
192            bail!("The file contains no geometry.");
193        }
194        if self.positions.len() % 3 != 0 || self.indices.len() % 3 != 0 {
195            bail!("Internal error: incomplete vertices or triangles.");
196        }
197        if !self.colors.is_empty() && self.colors.len() != self.positions.len() {
198            bail!("Internal error: colours do not match the vertices.");
199        }
200        if self.fields.len() > MAX_FIELDS
201            || self
202                .fields
203                .iter()
204                .any(|f| f.values.len() != self.vertex_count())
205        {
206            bail!("Internal error: result fields do not match the vertices.");
207        }
208        if self.triangle_count() > MAX_TRIANGLES {
209            bail!(
210                "The model has {} triangles – more than the preview shows ({}).",
211                self.triangle_count(),
212                MAX_TRIANGLES
213            );
214        }
215        if self.is_point_cloud() && self.vertex_count() > MAX_POINTS {
216            bail!("Internal error: point cloud not thinned out.");
217        }
218        if self.positions.iter().any(|v| !v.is_finite()) {
219            bail!("The file contains invalid coordinates (NaN or infinite).");
220        }
221        let n = self.vertex_count() as u32;
222        if self.indices.iter().any(|&i| i >= n) {
223            bail!("Internal error: triangle index out of range.");
224        }
225        Ok(())
226    }
227
228    /// Bounding box `[min x, min y, min z, max x, max y, max z]`.
229    pub fn bounds(&self) -> [f32; 6] {
230        let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
231        for p in self.positions.chunks_exact(3) {
232            for ((lo, hi), &c) in lo.iter_mut().zip(hi.iter_mut()).zip(p) {
233                *lo = lo.min(c);
234                *hi = hi.max(c);
235            }
236        }
237        let [x0, y0, z0] = lo;
238        let [x1, y1, z1] = hi;
239        [x0, y0, z0, x1, y1, z1]
240    }
241
242    /// The binary form read by `shared/js/viewer.js`:
243    ///
244    /// ```text
245    /// 0   "TCMESH01"
246    /// 8   u32 vertices   12  u32 indices   16  u32 parts
247    /// 20  u32 flags (1 = colours, 2 = fields, 4 = partial preview)
248    /// 24  f32 × 6 bounding box
249    /// 48  f32 × 3·vertices, u32 × indices, u8 × 3·vertices (colours, padded to 4 bytes)
250    ///     only with flag 2, after that:
251    ///     u32 field count, then per field
252    ///     u32 n, n bytes name (UTF-8), u32 m, m bytes unit, padding to 4 bytes,
253    ///     f32 min, f32 max, f32 × vertices (NaN = no value)
254    /// with flag 4, the last 8 bytes of the file: u32 faces shown, u32 faces in the file
255    /// ```
256    ///
257    /// Viewers that do not know flag 2 ignore the field section; meshes
258    /// with fields also carry the first field as colours for them. The face
259    /// count sits at the very end, so viewers that do not know flag 4 – and
260    /// the field section in between – are not affected.
261    pub fn encode(&self) -> Vec<u8> {
262        let field_bytes: usize = self
263            .fields
264            .iter()
265            .map(|f| 20 + f.name.len() + f.unit.len() + f.values.len() * 4)
266            .sum();
267        let mut out = Vec::with_capacity(
268            56 + self.positions.len() * 4
269                + self.indices.len() * 4
270                + self.colors.len()
271                + field_bytes
272                + 4,
273        );
274        out.extend_from_slice(MAGIC);
275        let flags = u32::from(!self.colors.is_empty())
276            | (u32::from(!self.fields.is_empty()) << 1)
277            | (u32::from(self.faces.is_some()) << 2);
278        for v in [
279            self.vertex_count() as u32,
280            self.indices.len() as u32,
281            self.parts.max(1),
282            flags,
283        ] {
284            out.extend_from_slice(&v.to_le_bytes());
285        }
286        for v in self.bounds() {
287            out.extend_from_slice(&v.to_le_bytes());
288        }
289        for v in &self.positions {
290            out.extend_from_slice(&v.to_le_bytes());
291        }
292        for v in &self.indices {
293            out.extend_from_slice(&v.to_le_bytes());
294        }
295        out.extend_from_slice(&self.colors);
296        while out.len() % 4 != 0 {
297            out.push(0);
298        }
299        if !self.fields.is_empty() {
300            out.extend_from_slice(&(self.fields.len() as u32).to_le_bytes());
301            for f in &self.fields {
302                for s in [&f.name, &f.unit] {
303                    out.extend_from_slice(&(s.len() as u32).to_le_bytes());
304                    out.extend_from_slice(s.as_bytes());
305                }
306                while out.len() % 4 != 0 {
307                    out.push(0);
308                }
309                let (lo, hi) = f.range();
310                out.extend_from_slice(&lo.to_le_bytes());
311                out.extend_from_slice(&hi.to_le_bytes());
312                for v in &f.values {
313                    out.extend_from_slice(&v.to_le_bytes());
314                }
315            }
316        }
317        // last, so that viewers that do not know flag 4 are not affected
318        if let Some((shown, total)) = self.faces {
319            out.extend_from_slice(&shown.to_le_bytes());
320            out.extend_from_slice(&total.to_le_bytes());
321        }
322        out
323    }
324
325    /// Sets the colours from the first field (for viewers that do not know
326    /// fields); vertices without a value are neutral.
327    pub fn bake_colors(&mut self) {
328        let Some(field) = self.fields.first() else {
329            return;
330        };
331        let (lo, hi) = field.range();
332        let span = if hi > lo { hi - lo } else { 1.0 };
333        self.colors = field
334            .values
335            .iter()
336            .flat_map(|&v| {
337                color_map(if v.is_finite() {
338                    (v - lo) / span
339                } else {
340                    f32::NAN
341                })
342            })
343            .collect();
344    }
345
346    /// Reads [`Mesh::encode`] output (tests, tools). Every size is checked,
347    /// so a damaged cache file is an error, not a panic.
348    pub fn decode(data: &[u8]) -> Result<Mesh> {
349        if data.len() < 48 || data.get(..8) != Some(MAGIC.as_slice()) {
350            bail!("not a TeamCraft mesh");
351        }
352        let u = |at: usize| -> Result<usize> {
353            Ok(u32_le(data, at).context("truncated mesh")? as usize)
354        };
355        let (vertices, indices, parts, flags) = (u(8)?, u(12)?, u(16)?, u(20)?);
356        let colors = if flags & 1 == 1 { vertices * 3 } else { 0 };
357        let need = 48 + vertices * 12 + indices * 4 + colors;
358        if data.len() < need {
359            bail!("truncated mesh");
360        }
361        let positions = quads(range(data, 48, 48 + vertices * 12)?)
362            .map(f32::from_le_bytes)
363            .collect();
364        let at = 48 + vertices * 12;
365        let index_values = quads(range(data, at, at + indices * 4)?)
366            .map(u32::from_le_bytes)
367            .collect();
368        let at = at + indices * 4;
369        // end of the body: after the colours (padded) and the field section
370        let mut end = (at + colors).div_ceil(4) * 4;
371        let mut fields = Vec::new();
372        if flags & 2 == 2 {
373            let mut at = end;
374            let count = u(at)?;
375            at += 4;
376            if count > MAX_FIELDS {
377                bail!("too many fields");
378            }
379            for _ in 0..count {
380                let mut text = || -> Result<String> {
381                    let n = u(at)?;
382                    let s = data.get(at + 4..at + 4 + n).context("truncated mesh")?;
383                    at += 4 + n;
384                    Ok(String::from_utf8_lossy(s).into_owned())
385                };
386                let (name, unit) = (text()?, text()?);
387                at = at.div_ceil(4) * 4 + 8;
388                let values_end = vertices
389                    .checked_mul(4)
390                    .and_then(|n| n.checked_add(at))
391                    .context("truncated mesh")?;
392                let values = quads(data.get(at..values_end).context("truncated mesh")?)
393                    .map(f32::from_le_bytes)
394                    .collect();
395                at = values_end;
396                fields.push(Field { name, unit, values });
397            }
398            end = at;
399        }
400        // flag 4: the face count in the last 8 bytes, after everything else
401        let faces = if flags & 4 == 4 {
402            if data.len() < end + 8 {
403                bail!("truncated mesh");
404            }
405            let tail = data.len() - 8;
406            Some((u(tail)? as u32, u(tail + 4)? as u32))
407        } else {
408            None
409        };
410        Ok(Mesh {
411            positions,
412            indices: index_values,
413            colors: range(data, at, at + colors)?.to_vec(),
414            parts: parts as u32,
415            fields,
416            faces,
417        })
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::mesh::test_support::cube_stl_ascii;
425
426    fn cube() -> Mesh {
427        crate::mesh::read("stl", cube_stl_ascii().as_bytes()).unwrap()
428    }
429
430    #[test]
431    fn encode_round_trip_and_limits() {
432        let mut mesh = cube();
433        mesh.colors = vec![9; mesh.positions.len()];
434        let bytes = mesh.encode();
435        assert_eq!(bytes.len() % 4, 0);
436        assert_eq!(Mesh::decode(&bytes).unwrap(), mesh);
437        assert!(Mesh::decode(&bytes[..40]).is_err());
438        let mut a = Mesh::default();
439        a.append(Mesh {
440            positions: vec![0., 0., 0.],
441            ..Mesh::default()
442        });
443        a.append(Mesh {
444            positions: vec![1., 1., 1.],
445            colors: vec![1, 2, 3],
446            ..Mesh::default()
447        });
448        assert_eq!(a.colors, vec![176, 188, 200, 1, 2, 3]);
449        assert!(Mesh {
450            positions: vec![f32::NAN, 0., 0.],
451            ..Mesh::default()
452        }
453        .check()
454        .is_err());
455        assert!(Mesh::default().check().is_err());
456    }
457
458    #[test]
459    fn partial_previews_carry_their_face_count() {
460        let mut mesh = cube();
461        mesh.faces = Some((5, 6));
462        let bytes = mesh.encode();
463        assert_eq!(bytes.len() % 4, 0);
464        assert_eq!(u32::from_le_bytes(bytes[20..24].try_into().unwrap()), 4);
465        assert_eq!(Mesh::decode(&bytes).unwrap(), mesh);
466        // a complete preview has no face count
467        mesh.faces = None;
468        let complete = mesh.encode();
469        assert_eq!(complete.len() + 8, bytes.len());
470        assert_eq!(Mesh::decode(&complete).unwrap().faces, None);
471        // a damaged face count is an error, not a panic
472        assert!(Mesh::decode(&bytes[..bytes.len() - 4]).is_err());
473    }
474
475    fn flags(bytes: &[u8]) -> u32 {
476        u32::from_le_bytes(bytes[20..24].try_into().unwrap())
477    }
478
479    #[test]
480    fn fields_and_face_counts_combine() {
481        let mut mesh = cube();
482        let n = mesh.vertex_count();
483        mesh.fields = vec![
484            Field {
485                name: "Displacement".into(),
486                unit: "mm".into(),
487                values: (0..n).map(|i| i as f32 * 0.5).collect(),
488            },
489            Field {
490                name: "S".into(),
491                unit: String::new(),
492                values: (0..n)
493                    .map(|i| if i == 0 { f32::NAN } else { i as f32 })
494                    .collect(),
495            },
496        ];
497        mesh.bake_colors();
498
499        // plain mesh: no extension flags
500        let plain = Mesh {
501            fields: Vec::new(),
502            colors: Vec::new(),
503            ..mesh.clone()
504        };
505        let plain_bytes = plain.encode();
506        assert_eq!(flags(&plain_bytes), 0);
507        assert_eq!(Mesh::decode(&plain_bytes).unwrap(), plain);
508
509        // fields only (flag 2, with the baked colours: flag 1)
510        let fields_only = mesh.encode();
511        assert_eq!(flags(&fields_only), 1 | 2);
512        let decoded = Mesh::decode(&fields_only).unwrap();
513        assert_eq!(decoded.faces, None);
514        assert_eq!(decoded.fields.len(), 2);
515        assert_eq!(decoded.fields[0], mesh.fields[0]);
516        assert!(decoded.fields[1].values[0].is_nan());
517        assert_eq!(decoded.fields[1].values[1..], mesh.fields[1].values[1..]);
518
519        // face counts only (flag 4)
520        let faces_only = Mesh {
521            fields: Vec::new(),
522            faces: Some((3, 7)),
523            ..mesh.clone()
524        };
525        let faces_bytes = faces_only.encode();
526        assert_eq!(flags(&faces_bytes), 1 | 4);
527        assert_eq!(Mesh::decode(&faces_bytes).unwrap(), faces_only);
528
529        // both: the field section first, the face count in the last 8 bytes
530        mesh.faces = Some((5, 6));
531        let both = mesh.encode();
532        assert_eq!(both.len() % 4, 0);
533        assert_eq!(flags(&both), 1 | 2 | 4);
534        assert_eq!(both.len(), fields_only.len() + 8);
535        // apart from the flags word, the body is the fields-only encoding
536        assert_eq!(both[..20], fields_only[..20]);
537        assert_eq!(both[24..fields_only.len()], fields_only[24..]);
538        assert_eq!(both[both.len() - 8..both.len() - 4], 5u32.to_le_bytes());
539        assert_eq!(both[both.len() - 4..], 6u32.to_le_bytes());
540        let decoded = Mesh::decode(&both).unwrap();
541        assert_eq!(decoded.faces, Some((5, 6)));
542        assert_eq!(decoded.fields.len(), 2);
543        assert_eq!(decoded.fields[0], mesh.fields[0]);
544        assert_eq!(decoded.colors, mesh.colors);
545        // the face count must not overlap the field section
546        assert!(Mesh::decode(&both[..both.len() - 4]).is_err());
547        let mut short = fields_only.clone();
548        short[20] |= 4;
549        short.extend_from_slice(&[0; 4]);
550        assert!(Mesh::decode(&short).is_err());
551    }
552
553    #[test]
554    fn the_colour_map_runs_from_the_first_to_the_last_stop() {
555        assert_eq!(color_map(0.0), COLOR_MAP[0]);
556        assert_eq!(color_map(1.0), COLOR_MAP[8]);
557        assert_eq!(color_map(0.5), COLOR_MAP[4], "the middle stop");
558        // values outside 0..=1 are clamped, non-finite ones are neutral grey
559        assert_eq!(color_map(-3.0), COLOR_MAP[0]);
560        assert_eq!(color_map(7.0), COLOR_MAP[8]);
561        for t in [f32::NAN, f32::INFINITY] {
562            assert_eq!(color_map(t), NEUTRAL, "{t}");
563        }
564        // half way between two stops is half way in every channel
565        let half = color_map(1.0 / 16.0);
566        for c in 0..3 {
567            let mid = (COLOR_MAP[0][c] as f32 + COLOR_MAP[1][c] as f32) / 2.0;
568            assert!((half[c] as f32 - mid).abs() <= 0.5, "{half:?}");
569        }
570    }
571
572    #[test]
573    fn append_shifts_indices_and_counts_parts() {
574        let triangle = || Mesh {
575            positions: vec![0., 0., 0., 1., 0., 0., 0., 1., 0.],
576            indices: vec![0, 1, 2],
577            parts: 1,
578            ..Mesh::default()
579        };
580        let mut a = triangle();
581        a.append(triangle());
582        assert_eq!(a.indices, [0, 1, 2, 3, 4, 5]);
583        assert_eq!((a.vertex_count(), a.parts), (6, 2));
584        // a part count of 0 still counts as one part
585        a.append(Mesh {
586            parts: 0,
587            ..triangle()
588        });
589        assert_eq!(a.parts, 3);
590        // fields survive only when both sides have them
591        let with_field = |name: &str| Mesh {
592            fields: vec![Field {
593                name: name.into(),
594                unit: String::new(),
595                values: vec![1.0; 3],
596            }],
597            ..triangle()
598        };
599        let mut b = with_field("T");
600        b.append(with_field("T"));
601        assert_eq!(b.fields[0].values.len(), 6);
602        let mut c = with_field("T");
603        c.append(with_field("S"));
604        assert!(c.fields.is_empty());
605        assert!(b.check().is_ok() && c.check().is_ok());
606    }
607
608    /// `append_placed` must give exactly what the copy it replaces gave: a
609    /// clone of the part without fields, moved, then appended.
610    #[test]
611    fn append_placed_equals_appending_a_moved_copy() {
612        let place = |p: [f64; 3]| [p[1] + 10.0, -p[0] + 0.5, p[2] * 2.0 - 3.0];
613        fn plain() -> Mesh {
614            Mesh {
615                positions: vec![0., 0., 0., 1., 0., 0., 0., 1., 0., 0.25, 0.5, 4.],
616                indices: vec![0, 1, 2, 1, 2, 3],
617                parts: 0,
618                ..Mesh::default()
619            }
620        }
621        fn coloured() -> Mesh {
622            Mesh {
623                colors: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
624                parts: 3,
625                ..plain()
626            }
627        }
628        fn with_field() -> Mesh {
629            Mesh {
630                fields: vec![Field {
631                    name: "T".into(),
632                    unit: "K".into(),
633                    values: vec![1.0; 4],
634                }],
635                faces: Some((1, 2)),
636                ..coloured()
637            }
638        }
639        let by_copy = |out: &mut Mesh, part: &Mesh| {
640            let mut placed = part.clone();
641            placed.fields.clear();
642            placed.faces = None;
643            for p in placed.positions.chunks_exact_mut(3) {
644                let q = place([f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]);
645                p.copy_from_slice(&[q[0] as f32, q[1] as f32, q[2] as f32]);
646            }
647            out.append(placed);
648        };
649        // every order of parts with and without colours, starting empty
650        let parts = [plain(), coloured(), with_field()];
651        for first in 0..3 {
652            for second in 0..3 {
653                for third in 0..3 {
654                    let (mut direct, mut copied) = (Mesh::default(), Mesh::default());
655                    for i in [first, second, third] {
656                        direct.append_placed(&parts[i], place);
657                        by_copy(&mut copied, &parts[i]);
658                    }
659                    assert_eq!(direct, copied, "{first} {second} {third}");
660                    assert!(direct.check().is_ok());
661                    assert_eq!(direct.vertex_count(), 12);
662                    assert_eq!(direct.indices[6..], [4, 5, 6, 5, 6, 7, 8, 9, 10, 9, 10, 11]);
663                }
664            }
665        }
666        // the part itself is untouched, and a mesh with fields loses them
667        assert_eq!(parts[2], with_field());
668        let mut target = with_field();
669        target.append_placed(&parts[0], place);
670        assert!(target.fields.is_empty());
671        assert_eq!(target.parts, 4);
672    }
673
674    #[test]
675    fn check_refuses_broken_meshes() {
676        let msg = |m: Mesh| m.check().unwrap_err().to_string();
677        assert!(msg(Mesh::default()).contains("no geometry"));
678        assert!(msg(Mesh {
679            positions: vec![0.0; 9],
680            indices: vec![0, 1, 3],
681            ..Mesh::default()
682        })
683        .contains("index out of range"));
684        assert!(msg(Mesh {
685            positions: vec![0.0; 9],
686            colors: vec![1; 3],
687            ..Mesh::default()
688        })
689        .contains("colours"));
690        assert!(msg(Mesh {
691            positions: vec![0.0; 9],
692            fields: vec![Field {
693                name: "T".into(),
694                unit: String::new(),
695                values: vec![1.0; 2],
696            }],
697            ..Mesh::default()
698        })
699        .contains("result fields"));
700        assert!(msg(Mesh {
701            positions: vec![0.0, f32::INFINITY, 0.0],
702            ..Mesh::default()
703        })
704        .contains("invalid coordinates"));
705    }
706
707    #[test]
708    fn bounds_of_a_mesh() {
709        assert_eq!(cube().bounds(), [0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
710        let one_point = Mesh {
711            positions: vec![2.0, -3.0, 4.0],
712            ..Mesh::default()
713        };
714        assert_eq!(one_point.bounds(), [2.0, -3.0, 4.0, 2.0, -3.0, 4.0]);
715    }
716}