Skip to main content

oxiphysics_io/
mesh_export.rs

1#![allow(clippy::manual_strip)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Mesh export and import utilities.
6//!
7//! Provides:
8//! - `MeshData`: vertex positions, face indices, normals, UVs, material IDs.
9//! - `ObjExporter`: write Wavefront OBJ + MTL files.
10//! - `StlExporter`: binary and ASCII STL export with normals.
11//! - `PlyExporter`: PLY header + binary/ASCII body.
12//! - `GltfExporter`: glTF 2.0 JSON + binary buffer.
13//! - `MeshImporter`: parse OBJ, binary/ASCII STL, and PLY.
14//! - `MeshSimplification`: quadric error metric edge-collapse simplification.
15
16#![allow(dead_code)]
17
18use std::collections::HashMap;
19use std::fmt::Write as FmtWrite;
20
21// ============================================================================
22// MeshData
23// ============================================================================
24
25/// Container for mesh geometry and per-vertex attributes.
26#[derive(Debug, Clone, Default)]
27pub struct MeshData {
28    /// Vertex positions as `[f64; 3]` triples.
29    pub vertices: Vec<[f64; 3]>,
30    /// Triangle face indices: each entry is `[v0, v1, v2]`.
31    pub faces: Vec<[usize; 3]>,
32    /// Per-vertex normals (may be empty).
33    pub normals: Vec<[f64; 3]>,
34    /// Per-vertex UV coordinates (may be empty).
35    pub uvs: Vec<[f64; 2]>,
36    /// Per-face material ID (may be empty).
37    pub material_ids: Vec<usize>,
38}
39
40impl MeshData {
41    /// Create an empty `MeshData`.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Add a vertex and return its index.
47    pub fn add_vertex(&mut self, pos: [f64; 3]) -> usize {
48        let idx = self.vertices.len();
49        self.vertices.push(pos);
50        idx
51    }
52
53    /// Add a triangular face.
54    pub fn add_face(&mut self, v0: usize, v1: usize, v2: usize) {
55        self.faces.push([v0, v1, v2]);
56    }
57
58    /// Add a triangular face with material ID.
59    pub fn add_face_with_material(&mut self, v0: usize, v1: usize, v2: usize, mat: usize) {
60        self.faces.push([v0, v1, v2]);
61        self.material_ids.push(mat);
62    }
63
64    /// Compute per-face normals and store as per-vertex normals (flat shading).
65    pub fn compute_flat_normals(&mut self) {
66        self.normals = vec![[0.0; 3]; self.vertices.len()];
67        for face in &self.faces {
68            let v0 = self.vertices[face[0]];
69            let v1 = self.vertices[face[1]];
70            let v2 = self.vertices[face[2]];
71            let n = triangle_normal_f64(v0, v1, v2);
72            for &vi in face {
73                self.normals[vi] = n;
74            }
75        }
76    }
77
78    /// Compute smooth per-vertex normals by averaging face normals.
79    pub fn compute_smooth_normals(&mut self) {
80        let mut acc = vec![[0.0f64; 3]; self.vertices.len()];
81        let mut count = vec![0u32; self.vertices.len()];
82        for face in &self.faces {
83            let v0 = self.vertices[face[0]];
84            let v1 = self.vertices[face[1]];
85            let v2 = self.vertices[face[2]];
86            let n = triangle_normal_f64(v0, v1, v2);
87            for &vi in face {
88                acc[vi][0] += n[0];
89                acc[vi][1] += n[1];
90                acc[vi][2] += n[2];
91                count[vi] += 1;
92            }
93        }
94        self.normals = acc
95            .iter()
96            .zip(count.iter())
97            .map(|(a, &c)| {
98                if c == 0 {
99                    [0.0; 3]
100                } else {
101                    let inv = 1.0 / c as f64;
102                    normalize3_f64([a[0] * inv, a[1] * inv, a[2] * inv])
103                }
104            })
105            .collect();
106    }
107
108    /// Return the axis-aligned bounding box of all vertices as `(min, max)`.
109    pub fn bounding_box(&self) -> ([f64; 3], [f64; 3]) {
110        let mut mn = [f64::INFINITY; 3];
111        let mut mx = [f64::NEG_INFINITY; 3];
112        for &v in &self.vertices {
113            for i in 0..3 {
114                mn[i] = mn[i].min(v[i]);
115                mx[i] = mx[i].max(v[i]);
116            }
117        }
118        (mn, mx)
119    }
120
121    /// Number of triangles in the mesh.
122    pub fn num_triangles(&self) -> usize {
123        self.faces.len()
124    }
125}
126
127// ============================================================================
128// Geometry helpers
129// ============================================================================
130
131/// Compute the unit normal of a triangle given three vertices (f64).
132fn triangle_normal_f64(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
133    let a = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
134    let b = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
135    let n = [
136        a[1] * b[2] - a[2] * b[1],
137        a[2] * b[0] - a[0] * b[2],
138        a[0] * b[1] - a[1] * b[0],
139    ];
140    normalize3_f64(n)
141}
142
143/// Normalise a 3-D f64 vector; returns zero vector if length < eps.
144fn normalize3_f64(v: [f64; 3]) -> [f64; 3] {
145    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
146    if l < 1e-12 {
147        [0.0; 3]
148    } else {
149        [v[0] / l, v[1] / l, v[2] / l]
150    }
151}
152
153// ============================================================================
154// Material
155// ============================================================================
156
157/// Simple Phong/PBR material description used by exporters.
158#[derive(Debug, Clone)]
159pub struct Material {
160    /// Material name.
161    pub name: String,
162    /// Diffuse / base colour (RGB in \[0,1\]).
163    pub diffuse: [f64; 3],
164    /// Specular colour (RGB in \[0,1\]).
165    pub specular: [f64; 3],
166    /// Shininess / roughness exponent.
167    pub shininess: f64,
168    /// Opacity (1.0 = fully opaque).
169    pub opacity: f64,
170}
171
172impl Default for Material {
173    fn default() -> Self {
174        Self {
175            name: "default".to_string(),
176            diffuse: [0.8, 0.8, 0.8],
177            specular: [0.2, 0.2, 0.2],
178            shininess: 32.0,
179            opacity: 1.0,
180        }
181    }
182}
183
184impl Material {
185    /// Create a new material with the given name and default parameters.
186    pub fn new(name: impl Into<String>) -> Self {
187        Self {
188            name: name.into(),
189            ..Default::default()
190        }
191    }
192}
193
194// ============================================================================
195// ObjExporter
196// ============================================================================
197
198/// Wavefront OBJ + MTL exporter.
199#[derive(Debug, Default)]
200pub struct ObjExporter {
201    /// Materials to embed in the `.mtl` file.
202    pub materials: Vec<Material>,
203}
204
205impl ObjExporter {
206    /// Create a new `ObjExporter`.
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Add a material.
212    pub fn add_material(&mut self, mat: Material) {
213        self.materials.push(mat);
214    }
215
216    /// Serialise `mesh` to OBJ format and return the string.
217    ///
218    /// If `mtl_name` is provided a `mtllib` directive is emitted.
219    pub fn export_obj(&self, mesh: &MeshData, mtl_name: Option<&str>) -> String {
220        let mut out = String::new();
221        let _ = writeln!(out, "# OxiPhysics OBJ export");
222        if let Some(name) = mtl_name {
223            let _ = writeln!(out, "mtllib {name}");
224        }
225
226        // Vertices
227        for &v in &mesh.vertices {
228            let _ = writeln!(out, "v {} {} {}", v[0], v[1], v[2]);
229        }
230
231        // Normals
232        for &n in &mesh.normals {
233            let _ = writeln!(out, "vn {} {} {}", n[0], n[1], n[2]);
234        }
235
236        // UVs
237        for &uv in &mesh.uvs {
238            let _ = writeln!(out, "vt {} {}", uv[0], uv[1]);
239        }
240
241        // Group faces by material
242        let has_mats = !mesh.material_ids.is_empty() && !self.materials.is_empty();
243        let mut current_mat: Option<usize> = None;
244
245        for (fi, &face) in mesh.faces.iter().enumerate() {
246            if has_mats {
247                let mat_id = *mesh.material_ids.get(fi).unwrap_or(&0);
248                if current_mat != Some(mat_id) {
249                    if let Some(mat) = self.materials.get(mat_id) {
250                        let _ = writeln!(out, "usemtl {}", mat.name);
251                    }
252                    current_mat = Some(mat_id);
253                }
254            }
255            // OBJ uses 1-based indices
256            let v0 = face[0] + 1;
257            let v1 = face[1] + 1;
258            let v2 = face[2] + 1;
259            if !mesh.normals.is_empty() && !mesh.uvs.is_empty() {
260                let _ = writeln!(out, "f {v0}/{v0}/{v0} {v1}/{v1}/{v1} {v2}/{v2}/{v2}");
261            } else if !mesh.normals.is_empty() {
262                let _ = writeln!(out, "f {v0}//{v0} {v1}//{v1} {v2}//{v2}");
263            } else if !mesh.uvs.is_empty() {
264                let _ = writeln!(out, "f {v0}/{v0} {v1}/{v1} {v2}/{v2}");
265            } else {
266                let _ = writeln!(out, "f {v0} {v1} {v2}");
267            }
268        }
269        out
270    }
271
272    /// Serialise the material list to MTL format.
273    pub fn export_mtl(&self) -> String {
274        let mut out = String::new();
275        let _ = writeln!(out, "# OxiPhysics MTL export");
276        for mat in &self.materials {
277            let _ = writeln!(out, "newmtl {}", mat.name);
278            writeln!(
279                out,
280                "Kd {} {} {}",
281                mat.diffuse[0], mat.diffuse[1], mat.diffuse[2]
282            )
283            .expect("operation should succeed");
284            writeln!(
285                out,
286                "Ks {} {} {}",
287                mat.specular[0], mat.specular[1], mat.specular[2]
288            )
289            .expect("operation should succeed");
290            let _ = writeln!(out, "Ns {}", mat.shininess);
291            let _ = writeln!(out, "d {}", mat.opacity);
292        }
293        out
294    }
295}
296
297// ============================================================================
298// StlExporter
299// ============================================================================
300
301/// Binary and ASCII STL exporter.
302#[derive(Debug, Default)]
303pub struct StlExporter;
304
305impl StlExporter {
306    /// Create a new `StlExporter`.
307    pub fn new() -> Self {
308        Self
309    }
310
311    /// Export `mesh` to binary STL bytes.
312    ///
313    /// Layout: 80-byte header, 4-byte count, 50 bytes per triangle.
314    pub fn export_binary(&self, mesh: &MeshData, solid_name: &str) -> Vec<u8> {
315        let n = mesh.faces.len();
316        let mut buf = Vec::with_capacity(84 + n * 50);
317
318        // 80-byte header
319        let mut header = [0u8; 80];
320        let name_bytes = solid_name.as_bytes();
321        let copy_len = name_bytes.len().min(80);
322        header[..copy_len].copy_from_slice(&name_bytes[..copy_len]);
323        buf.extend_from_slice(&header);
324
325        // Triangle count
326        buf.extend_from_slice(&(n as u32).to_le_bytes());
327
328        for &face in &mesh.faces {
329            let v0 = mesh.vertices[face[0]];
330            let v1 = mesh.vertices[face[1]];
331            let v2 = mesh.vertices[face[2]];
332
333            // Per-triangle normal from geometry (or from normals array)
334            let n_vec = if !mesh.normals.is_empty() {
335                mesh.normals[face[0]]
336            } else {
337                triangle_normal_f64(v0, v1, v2)
338            };
339
340            for &c in &n_vec {
341                buf.extend_from_slice(&(c as f32).to_le_bytes());
342            }
343            for &v in &[v0, v1, v2] {
344                for &c in &v {
345                    buf.extend_from_slice(&(c as f32).to_le_bytes());
346                }
347            }
348            buf.extend_from_slice(&0u16.to_le_bytes()); // attribute
349        }
350        buf
351    }
352
353    /// Export `mesh` to ASCII STL string.
354    pub fn export_ascii(&self, mesh: &MeshData, solid_name: &str) -> String {
355        let mut out = String::new();
356        let _ = writeln!(out, "solid {solid_name}");
357        for &face in &mesh.faces {
358            let v0 = mesh.vertices[face[0]];
359            let v1 = mesh.vertices[face[1]];
360            let v2 = mesh.vertices[face[2]];
361            let n_vec = triangle_normal_f64(v0, v1, v2);
362            let _ = writeln!(out, "  facet normal {} {} {}", n_vec[0], n_vec[1], n_vec[2]);
363            let _ = writeln!(out, "    outer loop");
364            let _ = writeln!(out, "      vertex {} {} {}", v0[0], v0[1], v0[2]);
365            let _ = writeln!(out, "      vertex {} {} {}", v1[0], v1[1], v1[2]);
366            let _ = writeln!(out, "      vertex {} {} {}", v2[0], v2[1], v2[2]);
367            let _ = writeln!(out, "    endloop");
368            let _ = writeln!(out, "  endfacet");
369        }
370        let _ = writeln!(out, "endsolid {solid_name}");
371        out
372    }
373
374    /// Compute a simple CRC32 checksum of `data`.
375    pub fn crc32(data: &[u8]) -> u32 {
376        let mut crc: u32 = 0xFFFF_FFFF;
377        for &byte in data {
378            crc ^= byte as u32;
379            for _ in 0..8 {
380                if crc & 1 != 0 {
381                    crc = (crc >> 1) ^ 0xEDB8_8320;
382                } else {
383                    crc >>= 1;
384                }
385            }
386        }
387        crc ^ 0xFFFF_FFFF
388    }
389}
390
391// ============================================================================
392// PlyExporter
393// ============================================================================
394
395/// PLY (Polygon File Format) exporter supporting binary and ASCII modes.
396#[derive(Debug, Default)]
397pub struct PlyExporter {
398    /// Write in binary little-endian format when `true`; ASCII when `false`.
399    pub binary: bool,
400}
401
402impl PlyExporter {
403    /// Create a new `PlyExporter`.
404    pub fn new(binary: bool) -> Self {
405        Self { binary }
406    }
407
408    /// Build the PLY header string.
409    fn build_header(&self, mesh: &MeshData) -> String {
410        let mut h = String::new();
411        let _ = writeln!(h, "ply");
412        if self.binary {
413            let _ = writeln!(h, "format binary_little_endian 1.0");
414        } else {
415            let _ = writeln!(h, "format ascii 1.0");
416        }
417        let _ = writeln!(h, "comment OxiPhysics PLY export");
418        let _ = writeln!(h, "element vertex {}", mesh.vertices.len());
419        let _ = writeln!(h, "property float x");
420        let _ = writeln!(h, "property float y");
421        let _ = writeln!(h, "property float z");
422        if !mesh.normals.is_empty() {
423            let _ = writeln!(h, "property float nx");
424            let _ = writeln!(h, "property float ny");
425            let _ = writeln!(h, "property float nz");
426        }
427        if !mesh.uvs.is_empty() {
428            let _ = writeln!(h, "property float s");
429            let _ = writeln!(h, "property float t");
430        }
431        let _ = writeln!(h, "element face {}", mesh.faces.len());
432        let _ = writeln!(h, "property list uchar int vertex_indices");
433        let _ = writeln!(h, "end_header");
434        h
435    }
436
437    /// Export `mesh` to ASCII PLY bytes.
438    pub fn export_ascii_bytes(&self, mesh: &MeshData) -> Vec<u8> {
439        let header = self.build_header(mesh);
440        let mut out = header;
441        for (i, &v) in mesh.vertices.iter().enumerate() {
442            let mut line = format!("{} {} {}", v[0] as f32, v[1] as f32, v[2] as f32);
443            if !mesh.normals.is_empty() {
444                let n = mesh.normals[i];
445                line.push_str(&format!(" {} {} {}", n[0] as f32, n[1] as f32, n[2] as f32));
446            }
447            if !mesh.uvs.is_empty() {
448                let uv = mesh.uvs[i];
449                line.push_str(&format!(" {} {}", uv[0] as f32, uv[1] as f32));
450            }
451            let _ = writeln!(out, "{line}");
452        }
453        for &face in &mesh.faces {
454            let _ = writeln!(out, "3 {} {} {}", face[0], face[1], face[2]);
455        }
456        out.into_bytes()
457    }
458
459    /// Export `mesh` to binary PLY bytes (little-endian).
460    pub fn export_binary_bytes(&self, mesh: &MeshData) -> Vec<u8> {
461        let header = self.build_header(mesh);
462        let mut buf = header.into_bytes();
463
464        for (i, &v) in mesh.vertices.iter().enumerate() {
465            for &c in &v {
466                buf.extend_from_slice(&(c as f32).to_le_bytes());
467            }
468            if !mesh.normals.is_empty() {
469                let n = mesh.normals[i];
470                for &c in &n {
471                    buf.extend_from_slice(&(c as f32).to_le_bytes());
472                }
473            }
474            if !mesh.uvs.is_empty() {
475                let uv = mesh.uvs[i];
476                for &c in &uv {
477                    buf.extend_from_slice(&(c as f32).to_le_bytes());
478                }
479            }
480        }
481        for &face in &mesh.faces {
482            buf.push(3u8); // list count
483            for &vi in &face {
484                buf.extend_from_slice(&(vi as i32).to_le_bytes());
485            }
486        }
487        buf
488    }
489
490    /// Export using the mode selected at construction time.
491    pub fn export(&self, mesh: &MeshData) -> Vec<u8> {
492        if self.binary {
493            self.export_binary_bytes(mesh)
494        } else {
495            self.export_ascii_bytes(mesh)
496        }
497    }
498}
499
500// ============================================================================
501// GltfExporter
502// ============================================================================
503
504/// A minimal glTF 2.0 exporter producing JSON + binary buffer.
505#[derive(Debug, Default)]
506pub struct GltfExporter {
507    /// Scene name embedded in the JSON.
508    pub scene_name: String,
509}
510
511impl GltfExporter {
512    /// Create a new `GltfExporter` with the given scene name.
513    pub fn new(scene_name: impl Into<String>) -> Self {
514        Self {
515            scene_name: scene_name.into(),
516        }
517    }
518
519    /// Export `mesh` to a `(json, binary_buffer)` pair.
520    ///
521    /// The binary buffer holds interleaved position data; the JSON references
522    /// it via a buffer view and accessor.
523    pub fn export(&self, mesh: &MeshData) -> (String, Vec<u8>) {
524        // Build binary buffer: float32 positions
525        let mut bin: Vec<u8> = Vec::new();
526        for &v in &mesh.vertices {
527            for &c in &v {
528                bin.extend_from_slice(&(c as f32).to_le_bytes());
529            }
530        }
531        // Face indices as uint32
532        let indices_offset = bin.len();
533        for &face in &mesh.faces {
534            for &vi in &face {
535                bin.extend_from_slice(&(vi as u32).to_le_bytes());
536            }
537        }
538
539        let vertex_count = mesh.vertices.len();
540        let index_count = mesh.faces.len() * 3;
541        let positions_byte_len = vertex_count * 3 * 4;
542        let indices_byte_len = index_count * 4;
543        let total_byte_len = bin.len();
544
545        // Compute bounding box for accessor min/max
546        let (bb_min, bb_max) = mesh.bounding_box();
547
548        let json = format!(
549            r#"{{
550  "asset": {{"version": "2.0", "generator": "OxiPhysics"}},
551  "scene": 0,
552  "scenes": [{{"name": "{}", "nodes": [0]}}],
553  "nodes": [{{"mesh": 0}}],
554  "meshes": [{{
555    "name": "mesh0",
556    "primitives": [{{
557      "attributes": {{"POSITION": 0}},
558      "indices": 1,
559      "mode": 4
560    }}]
561  }}],
562  "accessors": [
563    {{
564      "bufferView": 0,
565      "byteOffset": 0,
566      "componentType": 5126,
567      "count": {vertex_count},
568      "type": "VEC3",
569      "min": [{}, {}, {}],
570      "max": [{}, {}, {}]
571    }},
572    {{
573      "bufferView": 1,
574      "byteOffset": 0,
575      "componentType": 5125,
576      "count": {index_count},
577      "type": "SCALAR"
578    }}
579  ],
580  "bufferViews": [
581    {{"buffer": 0, "byteOffset": 0, "byteLength": {positions_byte_len}, "target": 34962}},
582    {{"buffer": 0, "byteOffset": {indices_offset}, "byteLength": {indices_byte_len}, "target": 34963}}
583  ],
584  "buffers": [{{"byteLength": {total_byte_len}}}]
585}}"#,
586            self.scene_name,
587            bb_min[0] as f32,
588            bb_min[1] as f32,
589            bb_min[2] as f32,
590            bb_max[0] as f32,
591            bb_max[1] as f32,
592            bb_max[2] as f32,
593        );
594
595        (json, bin)
596    }
597}
598
599// ============================================================================
600// MeshImporter
601// ============================================================================
602
603/// Mesh importer supporting OBJ, binary STL, ASCII STL, and PLY.
604#[derive(Debug, Default)]
605pub struct MeshImporter;
606
607impl MeshImporter {
608    /// Create a new `MeshImporter`.
609    pub fn new() -> Self {
610        Self
611    }
612
613    /// Parse a Wavefront OBJ string into a `MeshData`.
614    ///
615    /// Supports `v`, `vn`, `vt`, and `f` directives.
616    pub fn parse_obj(&self, src: &str) -> MeshData {
617        let mut mesh = MeshData::new();
618        let mut obj_normals: Vec<[f64; 3]> = Vec::new();
619        let mut obj_uvs: Vec<[f64; 2]> = Vec::new();
620
621        for line in src.lines() {
622            let line = line.trim();
623            if line.starts_with("v ") {
624                let vals: Vec<f64> = line[2..]
625                    .split_whitespace()
626                    .filter_map(|s| s.parse().ok())
627                    .collect();
628                if vals.len() >= 3 {
629                    mesh.add_vertex([vals[0], vals[1], vals[2]]);
630                }
631            } else if line.starts_with("vn ") {
632                let vals: Vec<f64> = line[3..]
633                    .split_whitespace()
634                    .filter_map(|s| s.parse().ok())
635                    .collect();
636                if vals.len() >= 3 {
637                    obj_normals.push([vals[0], vals[1], vals[2]]);
638                }
639            } else if line.starts_with("vt ") {
640                let vals: Vec<f64> = line[3..]
641                    .split_whitespace()
642                    .filter_map(|s| s.parse().ok())
643                    .collect();
644                if vals.len() >= 2 {
645                    obj_uvs.push([vals[0], vals[1]]);
646                }
647            } else if line.starts_with("f ") {
648                let parts: Vec<&str> = line[2..].split_whitespace().collect();
649                if parts.len() >= 3 {
650                    // Parse each token as vi/vt/vn (1-based)
651                    let parse_index = |tok: &str| -> usize {
652                        let s = tok.split('/').next().unwrap_or("1");
653                        s.parse::<usize>().unwrap_or(1).saturating_sub(1)
654                    };
655                    let i0 = parse_index(parts[0]);
656                    let i1 = parse_index(parts[1]);
657                    let i2 = parse_index(parts[2]);
658                    mesh.add_face(i0, i1, i2);
659                    // Fan triangulation for quads / ngons
660                    for k in 3..parts.len() {
661                        let ik = parse_index(parts[k]);
662                        mesh.add_face(i0, parse_index(parts[k - 1]), ik);
663                    }
664                }
665            }
666        }
667
668        // Transfer normals if counts match
669        if !obj_normals.is_empty() && obj_normals.len() == mesh.vertices.len() {
670            mesh.normals = obj_normals;
671        }
672        if !obj_uvs.is_empty() && obj_uvs.len() == mesh.vertices.len() {
673            mesh.uvs = obj_uvs;
674        }
675
676        mesh
677    }
678
679    /// Parse a binary STL byte slice into a `MeshData`.
680    ///
681    /// Expects the standard 80-byte header + 4-byte count + 50 bytes per triangle.
682    pub fn parse_stl_binary(&self, data: &[u8]) -> Option<MeshData> {
683        if data.len() < 84 {
684            return None;
685        }
686        let count = u32::from_le_bytes(data[80..84].try_into().ok()?) as usize;
687        let expected_len = 84 + count * 50;
688        if data.len() < expected_len {
689            return None;
690        }
691
692        let mut mesh = MeshData::new();
693        let mut offset = 84usize;
694        for _ in 0..count {
695            let read_f32 = |buf: &[u8], off: usize| -> f32 {
696                f32::from_le_bytes(
697                    buf[off..off + 4]
698                        .try_into()
699                        .expect("slice length must match"),
700                )
701            };
702            let nx = read_f32(data, offset) as f64;
703            let ny = read_f32(data, offset + 4) as f64;
704            let nz = read_f32(data, offset + 8) as f64;
705            let v0 = [
706                read_f32(data, offset + 12) as f64,
707                read_f32(data, offset + 16) as f64,
708                read_f32(data, offset + 20) as f64,
709            ];
710            let v1 = [
711                read_f32(data, offset + 24) as f64,
712                read_f32(data, offset + 28) as f64,
713                read_f32(data, offset + 32) as f64,
714            ];
715            let v2 = [
716                read_f32(data, offset + 36) as f64,
717                read_f32(data, offset + 40) as f64,
718                read_f32(data, offset + 44) as f64,
719            ];
720            let base = mesh.vertices.len();
721            mesh.add_vertex(v0);
722            mesh.add_vertex(v1);
723            mesh.add_vertex(v2);
724            mesh.add_face(base, base + 1, base + 2);
725            mesh.normals.push([nx, ny, nz]);
726            mesh.normals.push([nx, ny, nz]);
727            mesh.normals.push([nx, ny, nz]);
728            offset += 50;
729        }
730        Some(mesh)
731    }
732
733    /// Parse an ASCII STL string into a `MeshData`.
734    pub fn parse_stl_ascii(&self, src: &str) -> MeshData {
735        let mut mesh = MeshData::new();
736        let mut current_normal = [0.0f64; 3];
737        let mut verts_in_loop: Vec<[f64; 3]> = Vec::new();
738
739        for line in src.lines() {
740            let line = line.trim();
741            if line.starts_with("facet normal") {
742                let vals: Vec<f64> = line
743                    .split_whitespace()
744                    .skip(2)
745                    .filter_map(|s| s.parse().ok())
746                    .collect();
747                if vals.len() >= 3 {
748                    current_normal = [vals[0], vals[1], vals[2]];
749                }
750            } else if line.starts_with("vertex") {
751                let vals: Vec<f64> = line
752                    .split_whitespace()
753                    .skip(1)
754                    .filter_map(|s| s.parse().ok())
755                    .collect();
756                if vals.len() >= 3 {
757                    verts_in_loop.push([vals[0], vals[1], vals[2]]);
758                }
759            } else if line.starts_with("endfacet") {
760                if verts_in_loop.len() == 3 {
761                    let base = mesh.vertices.len();
762                    for &v in &verts_in_loop {
763                        mesh.add_vertex(v);
764                        mesh.normals.push(current_normal);
765                    }
766                    mesh.add_face(base, base + 1, base + 2);
767                }
768                verts_in_loop.clear();
769            }
770        }
771        mesh
772    }
773
774    /// Parse an ASCII PLY string into a `MeshData`.
775    pub fn parse_ply_ascii(&self, src: &str) -> Option<MeshData> {
776        let mut lines = src.lines();
777        let mut vertex_count = 0usize;
778        let mut face_count = 0usize;
779        let mut has_normal = false;
780        let mut has_uv = false;
781        // Parse header
782        for line in lines.by_ref() {
783            let line = line.trim();
784            if line == "end_header" {
785                break;
786            }
787            if line.starts_with("element vertex") {
788                vertex_count = line
789                    .split_whitespace()
790                    .last()
791                    .and_then(|s| s.parse().ok())
792                    .unwrap_or(0);
793            } else if line.starts_with("element face") {
794                face_count = line
795                    .split_whitespace()
796                    .last()
797                    .and_then(|s| s.parse().ok())
798                    .unwrap_or(0);
799            } else if line.contains("property float nx") || line.contains("property float nz") {
800                has_normal = true;
801            } else if line.contains("property float s") || line.contains("property float t") {
802                has_uv = true;
803            }
804        }
805
806        let mut mesh = MeshData::new();
807
808        // Read vertices
809        for _ in 0..vertex_count {
810            let line = lines.next()?.trim().to_string();
811            let vals: Vec<f64> = line
812                .split_whitespace()
813                .filter_map(|s| s.parse().ok())
814                .collect();
815            if vals.len() < 3 {
816                return None;
817            }
818            mesh.add_vertex([vals[0], vals[1], vals[2]]);
819            if has_normal && vals.len() >= 6 {
820                mesh.normals.push([vals[3], vals[4], vals[5]]);
821            }
822            let uv_offset = if has_normal { 6 } else { 3 };
823            if has_uv && vals.len() >= uv_offset + 2 {
824                mesh.uvs.push([vals[uv_offset], vals[uv_offset + 1]]);
825            }
826        }
827
828        // Read faces
829        for _ in 0..face_count {
830            let line = lines.next()?.trim().to_string();
831            let vals: Vec<usize> = line
832                .split_whitespace()
833                .filter_map(|s| s.parse().ok())
834                .collect();
835            if vals.is_empty() {
836                continue;
837            }
838            let n = vals[0];
839            if n >= 3 && vals.len() > n {
840                mesh.add_face(vals[1], vals[2], vals[3]);
841                for k in 3..n {
842                    mesh.add_face(vals[1], vals[k], vals[k + 1]);
843                }
844            }
845        }
846
847        Some(mesh)
848    }
849}
850
851// ============================================================================
852// MeshSimplification
853// ============================================================================
854
855/// Quadric error metric (QEM) entry for a vertex.
856#[derive(Debug, Clone, Default)]
857struct Quadric {
858    /// Upper-triangular 4x4 symmetric matrix stored as 10 f64 coefficients.
859    q: [f64; 10],
860}
861
862impl Quadric {
863    fn add(&mut self, other: &Quadric) {
864        for i in 0..10 {
865            self.q[i] += other.q[i];
866        }
867    }
868
869    /// Accumulate the plane `(a,b,c,d)` into this quadric.
870    fn add_plane(&mut self, a: f64, b: f64, c: f64, d: f64) {
871        self.q[0] += a * a;
872        self.q[1] += a * b;
873        self.q[2] += a * c;
874        self.q[3] += a * d;
875        self.q[4] += b * b;
876        self.q[5] += b * c;
877        self.q[6] += b * d;
878        self.q[7] += c * c;
879        self.q[8] += c * d;
880        self.q[9] += d * d;
881    }
882
883    /// Evaluate the quadric error at vertex `v`.
884    fn error_at(&self, v: [f64; 3]) -> f64 {
885        let [x, y, z] = v;
886        let q = &self.q;
887        x * x * q[0]
888            + 2.0 * x * y * q[1]
889            + 2.0 * x * z * q[2]
890            + 2.0 * x * q[3]
891            + y * y * q[4]
892            + 2.0 * y * z * q[5]
893            + 2.0 * y * q[6]
894            + z * z * q[7]
895            + 2.0 * z * q[8]
896            + q[9]
897    }
898}
899
900/// Edge collapse candidate.
901#[derive(Debug, Clone)]
902struct EdgeCollapse {
903    /// First vertex index.
904    v0: usize,
905    /// Second vertex index.
906    v1: usize,
907    /// Optimal position for the merged vertex.
908    target: [f64; 3],
909    /// Quadric error at `target`.
910    error: f64,
911}
912
913/// Mesh simplification via quadric error metrics (QEM) and edge collapse.
914#[derive(Debug, Default)]
915pub struct MeshSimplification;
916
917impl MeshSimplification {
918    /// Create a new `MeshSimplification`.
919    pub fn new() -> Self {
920        Self
921    }
922
923    /// Simplify `mesh` to at most `target_triangles` triangles.
924    ///
925    /// Uses the QEM algorithm: computes per-vertex quadrics from incident
926    /// planes, then greedily collapses the cheapest edges.
927    pub fn simplify(&self, mesh: &MeshData, target_triangles: usize) -> MeshData {
928        if mesh.faces.len() <= target_triangles {
929            return mesh.clone();
930        }
931
932        let n = mesh.vertices.len();
933        let mut quadrics: Vec<Quadric> = vec![Quadric::default(); n];
934
935        // Accumulate quadrics from face planes
936        for &face in &mesh.faces {
937            let v0 = mesh.vertices[face[0]];
938            let v1 = mesh.vertices[face[1]];
939            let v2 = mesh.vertices[face[2]];
940            let nrm = triangle_normal_f64(v0, v1, v2);
941            let d = -(nrm[0] * v0[0] + nrm[1] * v0[1] + nrm[2] * v0[2]);
942            for &vi in &face {
943                quadrics[vi].add_plane(nrm[0], nrm[1], nrm[2], d);
944            }
945        }
946
947        // Build edge set (unique edges from faces)
948        let mut edge_set: HashMap<(usize, usize), ()> = HashMap::new();
949        for &face in &mesh.faces {
950            for k in 0..3 {
951                let a = face[k];
952                let b = face[(k + 1) % 3];
953                let key = if a < b { (a, b) } else { (b, a) };
954                edge_set.insert(key, ());
955            }
956        }
957
958        // Compute collapse candidates
959        let mut candidates: Vec<EdgeCollapse> = edge_set
960            .keys()
961            .map(|&(v0, v1)| {
962                let mid = [
963                    (mesh.vertices[v0][0] + mesh.vertices[v1][0]) * 0.5,
964                    (mesh.vertices[v0][1] + mesh.vertices[v1][1]) * 0.5,
965                    (mesh.vertices[v0][2] + mesh.vertices[v1][2]) * 0.5,
966                ];
967                let mut combined = quadrics[v0].clone();
968                combined.add(&quadrics[v1]);
969                let error = combined.error_at(mid);
970                EdgeCollapse {
971                    v0,
972                    v1,
973                    target: mid,
974                    error,
975                }
976            })
977            .collect();
978
979        candidates.sort_by(|a, b| {
980            a.error
981                .partial_cmp(&b.error)
982                .unwrap_or(std::cmp::Ordering::Equal)
983        });
984
985        // Greedy collapse
986        let mut vertices = mesh.vertices.clone();
987        let mut faces = mesh.faces.clone();
988        let mut redirect: Vec<usize> = (0..n).collect();
989        let mut removed_count = 0;
990
991        let to_remove = mesh.faces.len() - target_triangles;
992
993        for collapse in &candidates {
994            if removed_count >= to_remove {
995                break;
996            }
997            let v0 = follow_redirect(&redirect, collapse.v0);
998            let v1 = follow_redirect(&redirect, collapse.v1);
999            if v0 == v1 {
1000                continue;
1001            }
1002
1003            // Merge v1 into v0
1004            vertices[v0] = collapse.target;
1005            redirect[v1] = v0;
1006
1007            // Remap faces, remove degenerate triangles
1008            let prev_len = faces.len();
1009            faces.retain(|f| {
1010                let a = follow_redirect(&redirect, f[0]);
1011                let b = follow_redirect(&redirect, f[1]);
1012                let c = follow_redirect(&redirect, f[2]);
1013                a != b && b != c && a != c
1014            });
1015            // Remap remaining
1016            for f in faces.iter_mut() {
1017                f[0] = follow_redirect(&redirect, f[0]);
1018                f[1] = follow_redirect(&redirect, f[1]);
1019                f[2] = follow_redirect(&redirect, f[2]);
1020            }
1021
1022            removed_count += prev_len - faces.len();
1023        }
1024
1025        // Build output mesh with only referenced vertices
1026        let mut new_mesh = MeshData::new();
1027        let mut new_index: HashMap<usize, usize> = HashMap::new();
1028        for f in &faces {
1029            let mut new_face = [0usize; 3];
1030            for (k, &vi) in f.iter().enumerate() {
1031                let entry_count = new_index.len();
1032                let new_vi = *new_index.entry(vi).or_insert(entry_count);
1033                if new_vi == new_index.len() - 1 {
1034                    new_mesh.add_vertex(vertices[vi]);
1035                }
1036                new_face[k] = new_vi;
1037            }
1038            new_mesh.add_face(new_face[0], new_face[1], new_face[2]);
1039        }
1040        new_mesh
1041    }
1042}
1043
1044/// Follow the redirect chain to find the canonical representative vertex.
1045fn follow_redirect(redirect: &[usize], mut v: usize) -> usize {
1046    while redirect[v] != v {
1047        v = redirect[v];
1048    }
1049    v
1050}
1051
1052// ============================================================================
1053// Tests
1054// ============================================================================
1055
1056#[cfg(test)]
1057mod tests {
1058    use super::*;
1059
1060    // Helper: simple tetrahedron MeshData
1061    fn tetra() -> MeshData {
1062        let mut m = MeshData::new();
1063        m.add_vertex([0.0, 0.0, 0.0]);
1064        m.add_vertex([1.0, 0.0, 0.0]);
1065        m.add_vertex([0.0, 1.0, 0.0]);
1066        m.add_vertex([0.0, 0.0, 1.0]);
1067        m.add_face(0, 1, 2);
1068        m.add_face(0, 1, 3);
1069        m.add_face(0, 2, 3);
1070        m.add_face(1, 2, 3);
1071        m
1072    }
1073
1074    // Helper: unit square (two triangles)
1075    fn quad() -> MeshData {
1076        let mut m = MeshData::new();
1077        m.add_vertex([0.0, 0.0, 0.0]);
1078        m.add_vertex([1.0, 0.0, 0.0]);
1079        m.add_vertex([1.0, 1.0, 0.0]);
1080        m.add_vertex([0.0, 1.0, 0.0]);
1081        m.add_face(0, 1, 2);
1082        m.add_face(0, 2, 3);
1083        m
1084    }
1085
1086    // -- MeshData tests -------------------------------------------------------
1087
1088    #[test]
1089    fn test_mesh_add_vertex() {
1090        let mut m = MeshData::new();
1091        let idx = m.add_vertex([1.0, 2.0, 3.0]);
1092        assert_eq!(idx, 0);
1093        assert_eq!(m.vertices.len(), 1);
1094    }
1095
1096    #[test]
1097    fn test_mesh_add_face() {
1098        let m = tetra();
1099        assert_eq!(m.num_triangles(), 4);
1100    }
1101
1102    #[test]
1103    fn test_mesh_bounding_box() {
1104        let m = tetra();
1105        let (mn, mx) = m.bounding_box();
1106        assert!((mn[0] - 0.0).abs() < 1e-10);
1107        assert!((mx[0] - 1.0).abs() < 1e-10);
1108    }
1109
1110    #[test]
1111    fn test_mesh_compute_flat_normals() {
1112        let mut m = quad();
1113        m.compute_flat_normals();
1114        assert_eq!(m.normals.len(), m.vertices.len());
1115        // All normals should point in Z direction
1116        for n in &m.normals {
1117            assert!((n[2].abs() - 1.0).abs() < 1e-5);
1118        }
1119    }
1120
1121    #[test]
1122    fn test_mesh_compute_smooth_normals() {
1123        let mut m = quad();
1124        m.compute_smooth_normals();
1125        assert_eq!(m.normals.len(), m.vertices.len());
1126    }
1127
1128    #[test]
1129    fn test_mesh_add_face_with_material() {
1130        let mut m = MeshData::new();
1131        m.add_vertex([0.0; 3]);
1132        m.add_vertex([1.0, 0.0, 0.0]);
1133        m.add_vertex([0.0, 1.0, 0.0]);
1134        m.add_face_with_material(0, 1, 2, 3);
1135        assert_eq!(m.material_ids[0], 3);
1136    }
1137
1138    // -- ObjExporter tests ----------------------------------------------------
1139
1140    #[test]
1141    fn test_obj_export_basic() {
1142        let exporter = ObjExporter::new();
1143        let m = quad();
1144        let obj = exporter.export_obj(&m, None);
1145        assert!(obj.contains("v 0 0 0"));
1146        assert!(obj.contains("f 1 2 3"));
1147    }
1148
1149    #[test]
1150    fn test_obj_export_with_mtllib() {
1151        let exporter = ObjExporter::new();
1152        let m = quad();
1153        let obj = exporter.export_obj(&m, Some("scene.mtl"));
1154        assert!(obj.contains("mtllib scene.mtl"));
1155    }
1156
1157    #[test]
1158    fn test_obj_export_normals() {
1159        let mut exporter = ObjExporter::new();
1160        exporter.add_material(Material::new("mat0"));
1161        let mut m = quad();
1162        m.compute_flat_normals();
1163        let obj = exporter.export_obj(&m, None);
1164        assert!(obj.contains("vn "));
1165    }
1166
1167    #[test]
1168    fn test_mtl_export() {
1169        let mut exporter = ObjExporter::new();
1170        exporter.add_material(Material::new("red"));
1171        let mtl = exporter.export_mtl();
1172        assert!(mtl.contains("newmtl red"));
1173        assert!(mtl.contains("Kd "));
1174    }
1175
1176    #[test]
1177    fn test_obj_usemtl_directive() {
1178        let mut exporter = ObjExporter::new();
1179        exporter.add_material(Material::new("mat0"));
1180        let mut m = quad();
1181        m.material_ids = vec![0, 0];
1182        let obj = exporter.export_obj(&m, None);
1183        assert!(obj.contains("usemtl mat0"));
1184    }
1185
1186    // -- StlExporter tests ----------------------------------------------------
1187
1188    #[test]
1189    fn test_stl_binary_header() {
1190        let exp = StlExporter::new();
1191        let m = quad();
1192        let bytes = exp.export_binary(&m, "test");
1193        assert!(bytes.len() >= 84);
1194        // First 80 bytes are header
1195        assert_eq!(&bytes[..4], b"test");
1196    }
1197
1198    #[test]
1199    fn test_stl_binary_triangle_count() {
1200        let exp = StlExporter::new();
1201        let m = quad();
1202        let bytes = exp.export_binary(&m, "q");
1203        let count = u32::from_le_bytes(bytes[80..84].try_into().unwrap());
1204        assert_eq!(count, 2);
1205    }
1206
1207    #[test]
1208    fn test_stl_binary_size() {
1209        let exp = StlExporter::new();
1210        let m = quad();
1211        let bytes = exp.export_binary(&m, "q");
1212        assert_eq!(bytes.len(), 84 + 2 * 50);
1213    }
1214
1215    #[test]
1216    fn test_stl_ascii_contains_solid() {
1217        let exp = StlExporter::new();
1218        let m = quad();
1219        let s = exp.export_ascii(&m, "mymesh");
1220        assert!(s.starts_with("solid mymesh"));
1221        assert!(s.contains("endsolid mymesh"));
1222    }
1223
1224    #[test]
1225    fn test_stl_ascii_facet_count() {
1226        let exp = StlExporter::new();
1227        let m = quad();
1228        let s = exp.export_ascii(&m, "q");
1229        let count = s.matches("facet normal").count();
1230        assert_eq!(count, 2);
1231    }
1232
1233    #[test]
1234    fn test_stl_crc32_known() {
1235        // CRC32 of empty slice should be 0x00000000
1236        let crc = StlExporter::crc32(&[]);
1237        assert_eq!(crc, 0x0000_0000);
1238    }
1239
1240    // -- PlyExporter tests ----------------------------------------------------
1241
1242    #[test]
1243    fn test_ply_ascii_header() {
1244        let exp = PlyExporter::new(false);
1245        let m = quad();
1246        let bytes = exp.export_ascii_bytes(&m);
1247        let s = String::from_utf8_lossy(&bytes);
1248        assert!(s.contains("ply"));
1249        assert!(s.contains("format ascii"));
1250        assert!(s.contains("element vertex 4"));
1251        assert!(s.contains("element face 2"));
1252    }
1253
1254    #[test]
1255    fn test_ply_binary_header() {
1256        let exp = PlyExporter::new(true);
1257        let m = quad();
1258        let bytes = exp.export_binary_bytes(&m);
1259        let header_end = bytes
1260            .windows(11)
1261            .position(|w| w == b"end_header\n")
1262            .unwrap()
1263            + 11;
1264        let header = String::from_utf8_lossy(&bytes[..header_end]);
1265        assert!(header.contains("format binary_little_endian"));
1266    }
1267
1268    #[test]
1269    fn test_ply_export_dispatch() {
1270        let exp_ascii = PlyExporter::new(false);
1271        let exp_bin = PlyExporter::new(true);
1272        let m = quad();
1273        let a = exp_ascii.export(&m);
1274        let b = exp_bin.export(&m);
1275        assert!(!a.is_empty());
1276        assert!(!b.is_empty());
1277    }
1278
1279    #[test]
1280    fn test_ply_ascii_with_normals() {
1281        let mut m = quad();
1282        m.compute_flat_normals();
1283        let exp = PlyExporter::new(false);
1284        let bytes = exp.export_ascii_bytes(&m);
1285        let s = String::from_utf8_lossy(&bytes);
1286        assert!(s.contains("property float nx"));
1287    }
1288
1289    // -- GltfExporter tests ---------------------------------------------------
1290
1291    #[test]
1292    fn test_gltf_json_structure() {
1293        let exp = GltfExporter::new("test_scene");
1294        let m = quad();
1295        let (json, _bin) = exp.export(&m);
1296        assert!(json.contains("\"version\": \"2.0\""));
1297        assert!(json.contains("test_scene"));
1298        assert!(json.contains("POSITION"));
1299    }
1300
1301    #[test]
1302    fn test_gltf_binary_buffer_size() {
1303        let exp = GltfExporter::new("s");
1304        let m = quad(); // 4 verts, 2 faces
1305        let (_json, bin) = exp.export(&m);
1306        // 4 * 3 * 4 bytes for positions + 2 * 3 * 4 bytes for indices
1307        let expected = 4 * 3 * 4 + 2 * 3 * 4;
1308        assert_eq!(bin.len(), expected);
1309    }
1310
1311    #[test]
1312    fn test_gltf_accessor_count() {
1313        let exp = GltfExporter::new("s");
1314        let m = quad();
1315        let (json, _) = exp.export(&m);
1316        assert!(json.contains("\"count\": 4")); // vertex count
1317        assert!(json.contains("\"count\": 6")); // index count
1318    }
1319
1320    // -- MeshImporter OBJ tests -----------------------------------------------
1321
1322    #[test]
1323    fn test_import_obj_basic() {
1324        let src = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
1325        let imp = MeshImporter::new();
1326        let m = imp.parse_obj(src);
1327        assert_eq!(m.vertices.len(), 3);
1328        assert_eq!(m.faces.len(), 1);
1329    }
1330
1331    #[test]
1332    fn test_import_obj_normals() {
1333        let src = "v 0 0 0\nv 1 0 0\nv 0 1 0\nvn 0 0 1\nvn 0 0 1\nvn 0 0 1\nf 1//1 2//2 3//3\n";
1334        let imp = MeshImporter::new();
1335        let m = imp.parse_obj(src);
1336        assert_eq!(m.vertices.len(), 3);
1337        // Normals are only stored when count matches vertices
1338        assert_eq!(m.normals.len(), 3);
1339    }
1340
1341    #[test]
1342    fn test_import_obj_quad_fan() {
1343        let src = "v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\n";
1344        let imp = MeshImporter::new();
1345        let m = imp.parse_obj(src);
1346        assert_eq!(m.faces.len(), 2); // quad -> 2 tris
1347    }
1348
1349    #[test]
1350    fn test_import_stl_binary_roundtrip() {
1351        let exp = StlExporter::new();
1352        let m = quad();
1353        let bytes = exp.export_binary(&m, "test");
1354        let imp = MeshImporter::new();
1355        let m2 = imp.parse_stl_binary(&bytes).unwrap();
1356        assert_eq!(m2.faces.len(), 2);
1357    }
1358
1359    #[test]
1360    fn test_import_stl_ascii_roundtrip() {
1361        let exp = StlExporter::new();
1362        let m = quad();
1363        let s = exp.export_ascii(&m, "q");
1364        let imp = MeshImporter::new();
1365        let m2 = imp.parse_stl_ascii(&s);
1366        assert_eq!(m2.faces.len(), 2);
1367    }
1368
1369    #[test]
1370    fn test_import_stl_binary_too_short() {
1371        let imp = MeshImporter::new();
1372        let result = imp.parse_stl_binary(&[0u8; 10]);
1373        assert!(result.is_none());
1374    }
1375
1376    #[test]
1377    fn test_import_ply_ascii_roundtrip() {
1378        let exp = PlyExporter::new(false);
1379        let m = quad();
1380        let bytes = exp.export_ascii_bytes(&m);
1381        let s = String::from_utf8(bytes).unwrap();
1382        let imp = MeshImporter::new();
1383        let m2 = imp.parse_ply_ascii(&s).unwrap();
1384        assert_eq!(m2.vertices.len(), 4);
1385        assert_eq!(m2.faces.len(), 2);
1386    }
1387
1388    #[test]
1389    fn test_import_ply_ascii_with_normals() {
1390        let exp = PlyExporter::new(false);
1391        let mut m = quad();
1392        m.compute_flat_normals();
1393        let bytes = exp.export_ascii_bytes(&m);
1394        let s = String::from_utf8(bytes).unwrap();
1395        let imp = MeshImporter::new();
1396        let m2 = imp.parse_ply_ascii(&s).unwrap();
1397        assert_eq!(m2.normals.len(), 4);
1398    }
1399
1400    // -- MeshSimplification tests ---------------------------------------------
1401
1402    #[test]
1403    fn test_simplify_already_simple() {
1404        let s = MeshSimplification::new();
1405        let m = quad();
1406        let out = s.simplify(&m, 10);
1407        assert_eq!(out.faces.len(), m.faces.len());
1408    }
1409
1410    #[test]
1411    fn test_simplify_reduces_triangles() {
1412        let s = MeshSimplification::new();
1413        let m = tetra();
1414        let out = s.simplify(&m, 2);
1415        assert!(out.faces.len() <= 4);
1416    }
1417
1418    #[test]
1419    fn test_simplify_empty_mesh() {
1420        let s = MeshSimplification::new();
1421        let m = MeshData::new();
1422        let out = s.simplify(&m, 100);
1423        assert_eq!(out.faces.len(), 0);
1424    }
1425
1426    #[test]
1427    fn test_simplify_single_face() {
1428        let s = MeshSimplification::new();
1429        let mut m = MeshData::new();
1430        m.add_vertex([0.0; 3]);
1431        m.add_vertex([1.0, 0.0, 0.0]);
1432        m.add_vertex([0.0, 1.0, 0.0]);
1433        m.add_face(0, 1, 2);
1434        let out = s.simplify(&m, 1);
1435        assert_eq!(out.faces.len(), 1);
1436    }
1437
1438    // -- Material tests -------------------------------------------------------
1439
1440    #[test]
1441    fn test_material_default() {
1442        let mat = Material::default();
1443        assert_eq!(mat.name, "default");
1444        assert!((mat.opacity - 1.0).abs() < 1e-10);
1445    }
1446
1447    #[test]
1448    fn test_material_new() {
1449        let mat = Material::new("chrome");
1450        assert_eq!(mat.name, "chrome");
1451    }
1452
1453    // -- Geometry helper tests -----------------------------------------------
1454
1455    #[test]
1456    fn test_triangle_normal_z() {
1457        let n = triangle_normal_f64([0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1458        assert!((n[2] - 1.0).abs() < 1e-10);
1459    }
1460
1461    #[test]
1462    fn test_normalize3_f64_unit() {
1463        let n = normalize3_f64([3.0, 0.0, 0.0]);
1464        assert!((n[0] - 1.0).abs() < 1e-10);
1465    }
1466
1467    #[test]
1468    fn test_normalize3_f64_zero() {
1469        let n = normalize3_f64([0.0; 3]);
1470        assert_eq!(n, [0.0; 3]);
1471    }
1472}