1use anyhow::{bail, Context, Result};
4
5use super::*;
6
7pub const MAX_TRIANGLES: usize = 8_000_000;
9pub const MAX_POINTS: usize = 3_000_000;
11
12const MAGIC: &[u8; 8] = b"TCMESH01";
13const NEUTRAL: [u8; 3] = [176, 188, 200];
15
16#[derive(Clone, Debug, Default, PartialEq)]
18pub struct Mesh {
19 pub positions: Vec<f32>,
21 pub indices: Vec<u32>,
23 pub colors: Vec<u8>,
25 pub parts: u32,
27 pub fields: Vec<Field>,
29 pub faces: Option<(u32, u32)>,
32}
33
34pub const MAX_FIELDS: usize = 8;
36
37#[derive(Clone, Debug, Default, PartialEq)]
41pub struct Field {
42 pub name: String,
43 pub unit: String,
45 pub values: Vec<f32>,
46}
47
48impl Field {
49 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
67const 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
81pub 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 pub fn append(&mut self, mut other: Mesh) {
122 let offset = self.vertex_count() as u32;
123 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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.append(Mesh {
586 parts: 0,
587 ..triangle()
588 });
589 assert_eq!(a.parts, 3);
590 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 #[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 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 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}