Skip to main content

optic_render/asset/
msh.rs

1//! Mesh asset types — CPU-side geometry descriptions that can be shipped to the GPU.
2//!
3//! This module provides two mesh families:
4//!
5//! - [`Mesh3DFile`] — 3D geometry with positions, normals, UVs, colours, and indices.
6//! - [`Mesh2DFile`] — 2D geometry with positions, UVs, colours, indices, and a layer.
7//!
8//! Both support loading from standard file formats (OBJ, STL), procedural
9//! generation (cubes, spheres, cylinders, etc.), and binary caching.
10//!
11//! # Architecture
12//!
13//! `Mesh3DFile` / `Mesh2DFile` are *asset* types — pure data on the CPU.
14//! They are converted into GPU resources by calling [`ship`](Mesh3DFile::ship),
15//! which returns a [`MeshHandle`] owned by the GPU.
16//!
17//! ```ignore
18//! // On the CPU (asset phase)
19//! let file = Mesh3DFile::cube(2.0);
20//!
21//! // On the GPU (render phase)
22//! let mesh: Mesh3D = gpu.ship_mesh3d(&file);
23//! ```
24//!
25//! # Caching
26//!
27//! In debug builds, [`Mesh3DFile::from_disk`] always re-parses the source file
28//! and overwrites the cached `.omesh` binary. In release builds it loads from
29//! the cache directly for faster startup.
30
31use optic_core::consts::{OPTIC_CACHE_VERSION, OPTIC_MAGIC};
32use optic_core::{DrawMode, OpticError, OpticErrorKind, OpticResult};
33use cgmath::Vector2;
34use std::collections::HashMap;
35
36use crate::asset::attr::{ATTRInfo, ColATTR, CustomATTR, IndATTR, NrmATTR, Pos2DATTR, Pos3DATTR, UVMATTR};
37use crate::asset::attr::DataType;
38use crate::handles::mesh::{
39    create_index_buffer, create_mesh_buffer, fill_buffer, fill_index_buffer, set_attr_layout,
40    MeshHandle,
41};
42
43/// Internal OBJ parser state.
44///
45/// Distinguishes successfully-parsed triangle meshes from faces that could not
46/// be triangulated.
47enum OBJ {
48    Parsed {
49        pos_attr: Pos3DATTR,
50        col_attr: ColATTR,
51        uvm_attr: UVMATTR,
52        nrm_attr: NrmATTR,
53        ind_attr: IndATTR,
54    },
55    NonTriangle(String),
56}
57
58impl OBJ {
59    /// Parses a Wavefront OBJ source string.
60    ///
61    /// Supports `v`, `vt`, `vn`, and `f` with 3 vertices per face. Non-triangle
62    /// faces return [`OBJ::NonTriangle`] with the offending source line.
63    fn parse(src: &str) -> Self {
64        let mut pos_attr = Pos3DATTR::empty();
65        let mut col_attr = ColATTR::empty();
66        let mut uvm_attr = UVMATTR::empty();
67        let mut nrm_attr = NrmATTR::empty();
68        let mut ind_attr = IndATTR::empty();
69
70        let mut pos_data = Vec::new();
71        let mut uvm_data = Vec::new();
72        let mut nrm_data = Vec::new();
73        type Vert = Vec<usize>;
74        let mut verts: Vec<Vert> = Vec::new();
75        let mut unique_verts = HashMap::new();
76
77        for line in src.lines() {
78            let line = line.trim();
79            let words: Vec<&str> = line.split(' ').collect();
80            if words.is_empty() { continue; }
81            match words[0] {
82                "v" => pos_data.push(Self::parse_3(&words)),
83                "vt" => uvm_data.push(Self::parse_2(&words)),
84                "vn" => nrm_data.push(Self::parse_3(&words)),
85                "f" => {
86                    if words.len() != 4 {
87                        return OBJ::NonTriangle(line.to_string());
88                    }
89                    for word in &words[1..] {
90                        let tokens: Vec<&str> = word.split('/').collect();
91                        let vert = tokens.iter()
92                            .map(|s| s.parse::<usize>().unwrap_or(1).saturating_sub(1))
93                            .collect();
94                        verts.push(vert);
95                    }
96                }
97                _ => {}
98            }
99        }
100
101        let attr_count = verts.first().map_or(0, |v| v.len());
102        let pos_exists = attr_count > 0;
103        let uvm_exists = attr_count > 1;
104        let nrm_exists = attr_count > 2;
105
106        let def_uvm = [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]];
107        let def_col = [1.0, 1.0, 1.0, 1.0];
108        let def_nrm = [1.0, 1.0, 1.0];
109
110        for (i, vert) in verts.iter().enumerate() {
111            let key = (
112                pos_exists.then(|| vert[0]),
113                uvm_exists.then(|| vert[1]),
114                nrm_exists.then(|| vert[2]),
115            );
116
117            if let Some(&idx) = unique_verts.get(&key) {
118                ind_attr.push(idx as u32);
119            } else {
120                let v_local = i % 3;
121                let new = pos_attr.data.len();
122                unique_verts.insert(key, new);
123                pos_attr.push(if pos_exists { pos_data[vert[0]] } else { [0.0; 3] });
124                uvm_attr.push(if uvm_exists { uvm_data[vert[1]] } else { def_uvm[v_local] });
125                nrm_attr.push(if nrm_exists { nrm_data[vert[2]] } else { def_nrm });
126                col_attr.push(def_col);
127                ind_attr.push(new as u32);
128            }
129        }
130
131        OBJ::Parsed { pos_attr, col_attr, uvm_attr, nrm_attr, ind_attr }
132    }
133
134    /// Parses two float values from a space-separated word list (e.g. `vt 0.5 0.5`).
135    /// The Y coordinate is flipped (`1.0 - y`) to account for OpenGL's texture origin.
136    fn parse_2(words: &[&str]) -> [f32; 2] {
137        let x = words.get(1).and_then(|w| w.parse().ok()).unwrap_or(0.0);
138        let y = words.get(2).and_then(|w| w.parse().ok()).unwrap_or(0.0);
139        [x, 1.0 - y]
140    }
141
142    /// Parses three float values from a space-separated word list (e.g. `v 1.0 2.0 3.0`).
143    fn parse_3(words: &[&str]) -> [f32; 3] {
144        let x = words.get(1).and_then(|w| w.parse().ok()).unwrap_or(0.0);
145        let y = words.get(2).and_then(|w| w.parse().ok()).unwrap_or(0.0);
146        let z = words.get(3).and_then(|w| w.parse().ok()).unwrap_or(0.0);
147        [x, y, z]
148    }
149}
150
151// ── Mesh3DFile ───────────────────────────────────────────────────────────
152
153/// CPU-side 3D mesh geometry — positions, normals, UVs, colours, and indices.
154///
155/// `Mesh3DFile` is the primary asset type for 3D geometry. It stores vertex
156/// and index data in separate typed attribute arrays, ready for interleaving
157/// and GPU upload via [`ship`](Mesh3DFile::ship).
158///
159/// # Loading
160///
161/// | Method | Source |
162/// |---|---|
163/// | [`from_obj_src`](Mesh3DFile::from_obj_src) | Wavefront OBJ string (triangles only) |
164/// | [`from_stl_src`](Mesh3DFile::from_stl_src) | STL bytes (ASCII or binary) |
165/// | [`from_disk`](Mesh3DFile::from_disk) | Auto-detected via file extension (debug caches, release loads cache) |
166/// | [`cube`](Mesh3DFile::cube), [`cuboid`](Mesh3DFile::cuboid), etc. | Procedural generation |
167///
168/// # Generated primitives
169///
170/// | Function | Description |
171/// |---|---|
172/// | [`cube`](Mesh3DFile::cube) | Unit cube centred at origin |
173/// | [`cuboid`](Mesh3DFile::cuboid) | Box with per-axis extents |
174/// | [`sphere`](Mesh3DFile::sphere) | UV sphere with configurable stacks/sectors |
175/// | [`cylinder`](Mesh3DFile::cylinder) | Cylinder with optional end caps |
176/// | [`cone`](Mesh3DFile::cone) | Cone with optional base cap |
177/// | [`torus`](Mesh3DFile::torus) | Torus with configurable ring counts |
178/// | [`plane`](Mesh3DFile::plane) | Flat XZ quad |
179///
180/// # Custom attributes
181///
182/// Use [`attach_custom_attr`](Mesh3DFile::attach_custom_attr) to add
183/// per-vertex data beyond the built-in set (e.g. bone weights, AO values).
184///
185/// # Binary cache format
186///
187/// The `.omesh` cache uses a simple binary layout:
188///
189/// | Offset | Size | Field |
190/// |---|---|---|
191/// | 0 | 8 | Magic (`OPTIC_MAGIC`) |
192/// | 8 | 2 | Version (`OPTIC_CACHE_VERSION`) |
193/// | 10 | 1 | Flags (bit 0 = has normals, bit 1 = has UVs) |
194/// | 11 | 4 | Position data size (bytes) |
195/// | 15 | N | Position data (`f32 × 3` per vertex) |
196/// | ... | 4 | Normal data size (0 if absent) |
197/// | ... | N | Normal data (optional) |
198/// | ... | 4 | UV data size (0 if absent) |
199/// | ... | N | UV data (optional) |
200/// | ... | 4 | Colour data size |
201/// | ... | N | Colour data (`f32 × 4` per vertex) |
202/// | ... | 4 | Index data size |
203/// | ... | N | Index data (`u32` per index) |
204///
205/// # Example
206///
207/// ```ignore
208/// use optic_render::asset::Mesh3DFile;
209///
210/// // Load from OBJ
211/// let obj = "\
212///     v 0.0 0.0 0.0\n\
213///     v 1.0 0.0 0.0\n\
214///     v 0.0 1.0 0.0\n\
215///     f 1 2 3";
216/// let file = Mesh3DFile::from_obj_src(obj)?;
217///
218/// // Or generate procedurally
219/// let cube = Mesh3DFile::cube(2.0);
220/// ```
221pub struct Mesh3DFile {
222    pub pos_attr: Pos3DATTR,
223    pub col_attr: ColATTR,
224    pub uvm_attr: UVMATTR,
225    pub nrm_attr: NrmATTR,
226    pub ind_attr: IndATTR,
227    pub cus_attrs: Vec<CustomATTR>,
228}
229
230impl Mesh3DFile {
231    /// Creates an empty mesh with no vertices or indices.
232    pub fn empty() -> Self {
233        Self {
234            pos_attr: Pos3DATTR::empty(),
235            col_attr: ColATTR::empty(),
236            uvm_attr: UVMATTR::empty(),
237            nrm_attr: NrmATTR::empty(),
238            ind_attr: IndATTR::empty(),
239            cus_attrs: Vec::new(),
240        }
241    }
242
243    /// Parses a Wavefront OBJ source string into a triangle mesh.
244    ///
245    /// Supports `v`, `vt`, `vn`, and triangular `f` records. Non-triangle
246    /// faces produce an [`OpticError`].
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the OBJ contains a face with more than 3 vertices
251    /// (non-triangulated).
252    pub fn from_obj_src(src: &str) -> OpticResult<Self> {
253        match OBJ::parse(src) {
254            OBJ::NonTriangle(line) => Err(OpticError::new(
255                OpticErrorKind::Asset,
256                &format!("mesh not triangulated at: {line}"),
257            )),
258            OBJ::Parsed { pos_attr, col_attr, uvm_attr, nrm_attr, ind_attr } => {
259                Ok(Self { pos_attr, col_attr, uvm_attr, nrm_attr, ind_attr, cus_attrs: Vec::new() })
260            }
261        }
262    }
263
264    /// Parses an STL file (ASCII or binary) into a triangle mesh.
265    ///
266    /// Vertices are deduplicated by position + normal. UVs are left empty and
267    /// colours default to white.
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if the data is truncated or not valid UTF-8 (for ASCII STL).
272    pub fn from_stl_src(data: &[u8]) -> OpticResult<Self> {
273        let mut pos_attr = Pos3DATTR::empty();
274        let mut col_attr = ColATTR::empty();
275        let mut uvm_attr = UVMATTR::empty();
276        let mut nrm_attr = NrmATTR::empty();
277        let mut ind_attr = IndATTR::empty();
278
279        let def_col = [1.0, 1.0, 1.0, 1.0];
280        let def_uvm = [0.0, 0.0];
281        let mut unique_verts: HashMap<(u32, u32, u32, u32, u32, u32), u32> = HashMap::new();
282
283        let push_vert = |pos: [f32; 3], nrm: [f32; 3], unique: &mut HashMap<(u32, u32, u32, u32, u32, u32), u32>,
284                              pos_attr: &mut Pos3DATTR, nrm_attr: &mut NrmATTR,
285                              col_attr: &mut ColATTR, uvm_attr: &mut UVMATTR| -> u32 {
286            let key = (pos[0].to_bits(), pos[1].to_bits(), pos[2].to_bits(),
287                       nrm[0].to_bits(), nrm[1].to_bits(), nrm[2].to_bits());
288            if let Some(&idx) = unique.get(&key) {
289                idx
290            } else {
291                let idx = pos_attr.data.len() as u32;
292                unique.insert(key, idx);
293                pos_attr.push(pos);
294                nrm_attr.push(nrm);
295                col_attr.push(def_col);
296                uvm_attr.push(def_uvm);
297                idx
298            }
299        };
300
301        let is_ascii = data.len() >= 6 && &data[0..6] == b"solid ";
302
303        if is_ascii {
304            let text = std::str::from_utf8(data)
305                .map_err(|_| OpticError::new(OpticErrorKind::Asset, "STL file is not valid UTF-8"))?;
306            let mut nrm = [0.0f32; 3];
307            let mut tri_verts = Vec::new();
308
309            for line in text.lines() {
310                let line = line.trim();
311                if line.starts_with("facet normal") {
312                    let parts: Vec<&str> = line.split_whitespace().collect();
313                    if parts.len() >= 5 {
314                        nrm = [
315                            parts[2].parse().unwrap_or(0.0),
316                            parts[3].parse().unwrap_or(0.0),
317                            parts[4].parse().unwrap_or(0.0),
318                        ];
319                    }
320                    tri_verts.clear();
321                } else if line.starts_with("vertex") {
322                    let parts: Vec<&str> = line.split_whitespace().collect();
323                    if parts.len() >= 4 {
324                        tri_verts.push([
325                            parts[1].parse().unwrap_or(0.0),
326                            parts[2].parse().unwrap_or(0.0),
327                            parts[3].parse().unwrap_or(0.0),
328                        ]);
329                    }
330                } else if line.starts_with("endfacet") && tri_verts.len() == 3 {
331                    for v in &tri_verts {
332                        let idx = push_vert(*v, nrm, &mut unique_verts, &mut pos_attr, &mut nrm_attr, &mut col_attr, &mut uvm_attr);
333                        ind_attr.push(idx);
334                    }
335                }
336            }
337        } else {
338            if data.len() < 84 {
339                return Err(OpticError::new(OpticErrorKind::Asset, &format!("binary STL too short: {} bytes", data.len())));
340            }
341            let tri_count = u32::from_le_bytes([data[80], data[81], data[82], data[83]]) as usize;
342            if data.len() < 84 + tri_count * 50 {
343                return Err(OpticError::new(OpticErrorKind::Asset, &format!("binary STL truncated: expected {} triangles, got {} bytes", tri_count, data.len())));
344            }
345            for i in 0..tri_count {
346                let base = 84 + i * 50;
347                let nrm = [
348                    f32::from_le_bytes([data[base], data[base + 1], data[base + 2], data[base + 3]]),
349                    f32::from_le_bytes([data[base + 4], data[base + 5], data[base + 6], data[base + 7]]),
350                    f32::from_le_bytes([data[base + 8], data[base + 9], data[base + 10], data[base + 11]]),
351                ];
352                let verts = [
353                    [f32::from_le_bytes([data[base + 12], data[base + 13], data[base + 14], data[base + 15]]),
354                     f32::from_le_bytes([data[base + 16], data[base + 17], data[base + 18], data[base + 19]]),
355                     f32::from_le_bytes([data[base + 20], data[base + 21], data[base + 22], data[base + 23]])],
356                    [f32::from_le_bytes([data[base + 24], data[base + 25], data[base + 26], data[base + 27]]),
357                     f32::from_le_bytes([data[base + 28], data[base + 29], data[base + 30], data[base + 31]]),
358                     f32::from_le_bytes([data[base + 32], data[base + 33], data[base + 34], data[base + 35]])],
359                    [f32::from_le_bytes([data[base + 36], data[base + 37], data[base + 38], data[base + 39]]),
360                     f32::from_le_bytes([data[base + 40], data[base + 41], data[base + 42], data[base + 43]]),
361                     f32::from_le_bytes([data[base + 44], data[base + 45], data[base + 46], data[base + 47]])],
362                ];
363                for v in &verts {
364                    let idx = push_vert(*v, nrm, &mut unique_verts, &mut pos_attr, &mut nrm_attr, &mut col_attr, &mut uvm_attr);
365                    ind_attr.push(idx);
366                }
367            }
368        }
369
370        Ok(Self { pos_attr, col_attr, uvm_attr, nrm_attr, ind_attr, cus_attrs: Vec::new() })
371    }
372
373    /// Loads a mesh from disk, detecting format by extension.
374    ///
375    /// In **debug** builds: parses the source file and overwrites the cache.
376    /// In **release** builds: loads from the cached `.omesh` file directly for
377    /// faster startup.
378    ///
379    /// Supported formats: `.obj`, `.stl`.
380    #[cfg(debug_assertions)]
381    pub fn from_disk(path: &str) -> OpticResult<Self> {
382        let ext = optic_file::extension(path).unwrap_or_default();
383        let mesh = match ext.as_str() {
384            "obj" => {
385                let src = optic_file::read_string(path)?;
386                Self::from_obj_src(&src)?
387            }
388            "stl" => {
389                let data = optic_file::read_bytes(path)?;
390                Self::from_stl_src(&data)?
391            }
392            _ => return Err(OpticError::new(OpticErrorKind::Asset, &format!("unsupported mesh format: .{ext}"))),
393        };
394        let cache = optic_file::cached_path(path, "omesh");
395        mesh.save_cached(&cache)?;
396        Ok(mesh)
397    }
398
399    #[cfg(not(debug_assertions))]
400    pub fn from_disk(path: &str) -> OpticResult<Self> {
401        let cache = optic_file::cached_path(path, "omesh");
402        Self::from_cached(&cache)
403    }
404
405    /// Saves the mesh to a binary cache file (`.omesh` format).
406    pub fn save_cached(&self, path: &str) -> OpticResult<()> {
407        let has_normals = !self.nrm_attr.data.is_empty();
408        let has_uvs = !self.uvm_attr.data.is_empty();
409        let flags = (has_normals as u8) | ((has_uvs as u8) << 1);
410
411        let pos_bytes = self.pos_attr.data.len() * 12;
412        let nrm_bytes = self.nrm_attr.data.len() * 12;
413        let uvm_bytes = self.uvm_attr.data.len() * 8;
414        let col_bytes = self.col_attr.data.len() * 16;
415        let ind_bytes = self.ind_attr.data.len() * 4;
416
417        let size = 10 + 20 + pos_bytes + nrm_bytes + uvm_bytes + col_bytes + ind_bytes;
418        let mut data = Vec::with_capacity(size);
419        data.extend_from_slice(&OPTIC_MAGIC);
420        data.extend_from_slice(&OPTIC_CACHE_VERSION.to_le_bytes());
421        data.push(flags);
422
423        data.extend_from_slice(&(pos_bytes as u32).to_le_bytes());
424        for v in &self.pos_attr.data {
425            data.extend_from_slice(&v[0].to_le_bytes());
426            data.extend_from_slice(&v[1].to_le_bytes());
427            data.extend_from_slice(&v[2].to_le_bytes());
428        }
429
430        data.extend_from_slice(&(nrm_bytes as u32).to_le_bytes());
431        for v in &self.nrm_attr.data {
432            data.extend_from_slice(&v[0].to_le_bytes());
433            data.extend_from_slice(&v[1].to_le_bytes());
434            data.extend_from_slice(&v[2].to_le_bytes());
435        }
436
437        data.extend_from_slice(&(uvm_bytes as u32).to_le_bytes());
438        for v in &self.uvm_attr.data {
439            data.extend_from_slice(&v[0].to_le_bytes());
440            data.extend_from_slice(&v[1].to_le_bytes());
441        }
442
443        data.extend_from_slice(&(col_bytes as u32).to_le_bytes());
444        for v in &self.col_attr.data {
445            data.extend_from_slice(&v[0].to_le_bytes());
446            data.extend_from_slice(&v[1].to_le_bytes());
447            data.extend_from_slice(&v[2].to_le_bytes());
448            data.extend_from_slice(&v[3].to_le_bytes());
449        }
450
451        data.extend_from_slice(&(ind_bytes as u32).to_le_bytes());
452        for v in &self.ind_attr.data {
453            data.extend_from_slice(&v.to_le_bytes());
454        }
455
456        optic_file::write_bytes(path, &data)
457    }
458
459    #[cfg_attr(debug_assertions, allow(dead_code))]
460    fn from_cached(path: &str) -> OpticResult<Self> {
461        let data = optic_file::read_bytes(path)?;
462        if data.len() < 15 {
463            return Err(OpticError::new(OpticErrorKind::Asset, &format!("cached mesh too short: {path}")));
464        }
465        if data[0..8] != OPTIC_MAGIC {
466            return Err(OpticError::new(OpticErrorKind::Asset, &format!("not a valid Optic cache file (bad magic): {path}")));
467        }
468        let version = u16::from_le_bytes([data[8], data[9]]);
469        if version != OPTIC_CACHE_VERSION {
470            return Err(OpticError::new(OpticErrorKind::Asset, &format!(
471                "cache file version {version} is not supported (expected {OPTIC_CACHE_VERSION}): {path}"
472            )));
473        }
474
475        let mut off = 11usize;
476
477        let read_f32x3 = |off: &mut usize, data: &[u8]| -> [f32; 3] {
478            let x = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
479            let y = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
480            let z = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
481            [x, y, z]
482        };
483
484        let read_f32x2 = |off: &mut usize, data: &[u8]| -> [f32; 2] {
485            let x = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
486            let y = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
487            [x, y]
488        };
489
490        let read_f32x4 = |off: &mut usize, data: &[u8]| -> [f32; 4] {
491            let x = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
492            let y = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
493            let z = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
494            let w = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]); *off += 4;
495            [x, y, z, w]
496        };
497
498        let pos_size = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
499        off += 4;
500        if off + pos_size > data.len() {
501            return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached mesh (position): {path}")));
502        }
503        let vert_count = pos_size / 12;
504        let mut pos_attr = Pos3DATTR::empty();
505        for _ in 0..vert_count {
506            pos_attr.push(read_f32x3(&mut off, &data));
507        }
508
509        let nrm_size = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
510        off += 4;
511        let mut nrm_attr = NrmATTR::empty();
512        if nrm_size > 0 {
513            if off + nrm_size > data.len() {
514                return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached mesh (normals): {path}")));
515            }
516            let nrm_count = nrm_size / 12;
517            for _ in 0..nrm_count {
518                nrm_attr.push(read_f32x3(&mut off, &data));
519            }
520        }
521
522        let uvm_size = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
523        off += 4;
524        let mut uvm_attr = UVMATTR::empty();
525        if uvm_size > 0 {
526            if off + uvm_size > data.len() {
527                return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached mesh (UVs): {path}")));
528            }
529            let uvm_count = uvm_size / 8;
530            for _ in 0..uvm_count {
531                uvm_attr.push(read_f32x2(&mut off, &data));
532            }
533        }
534
535        let col_size = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
536        off += 4;
537        if off + col_size > data.len() {
538            return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached mesh (colors): {path}")));
539        }
540        let mut col_attr = ColATTR::empty();
541        let col_count = col_size / 16;
542        for _ in 0..col_count {
543            col_attr.push(read_f32x4(&mut off, &data));
544        }
545
546        let ind_size = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
547        off += 4;
548        if off + ind_size > data.len() {
549            return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached mesh (indices): {path}")));
550        }
551        let mut ind_attr = IndATTR::empty();
552        let ind_count = ind_size / 4;
553        for _ in 0..ind_count {
554            let idx = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
555            off += 4;
556            ind_attr.push(idx);
557        }
558
559        Ok(Self { pos_attr, col_attr, uvm_attr, nrm_attr, ind_attr, cus_attrs: Vec::new() })
560    }
561
562    /// Generates a unit cube centred at the origin with all six faces.
563    /// Generates a cube centred at the origin with equal side lengths.
564    ///
565    /// This is a convenient shorthand for [`cuboid(side, side, side)`](Self::cuboid).
566    /// Each face is a quad (2 triangles, 4 vertices) with correct normals and
567    /// per-face UVs covering `[0,1]²`.
568    ///
569    /// # Conventions
570    ///
571    /// - **Y-up** — the top face points toward +Y.
572    /// - **UVs** — each face covers the full `[0,1]²` texture atlas.
573    /// - **Normals** — per-face (flat, not smooth).
574    /// - **Colour** — all vertices white.
575    ///
576    /// # Example
577    ///
578    /// ```ignore
579    /// let cube = Mesh3DFile::cube(2.0);
580    /// let box_ = Mesh3DFile::cuboid(1.0, 2.0, 3.0);
581    /// ```
582    pub fn cube(side: f32) -> Self {
583        Self::cuboid(side, side, side)
584    }
585
586    /// Generates a rectangular box centred at the origin.
587    ///
588    /// Each face remains a quad with flat normals, full-cover UVs, and white
589    /// colour. Use [`cube`](Self::cube) for equal-sided boxes.
590    pub fn cuboid(w: f32, h: f32, d: f32) -> Self {
591        let mut mesh = Self::empty();
592        let hw = w * 0.5;
593        let hh = h * 0.5;
594        let hd = d * 0.5;
595        let faces: Vec<([f32; 3], [[f32; 3]; 4])> = vec![
596            ([0.0, 0.0, 1.0], [[-hw, -hh, hd], [hw, -hh, hd], [hw, hh, hd], [-hw, hh, hd]]),
597            ([0.0, 0.0, -1.0], [[hw, -hh, -hd], [-hw, -hh, -hd], [-hw, hh, -hd], [hw, hh, -hd]]),
598            ([0.0, 1.0, 0.0], [[-hw, hh, hd], [hw, hh, hd], [hw, hh, -hd], [-hw, hh, -hd]]),
599            ([0.0, -1.0, 0.0], [[-hw, -hh, -hd], [hw, -hh, -hd], [hw, -hh, hd], [-hw, -hh, hd]]),
600            ([1.0, 0.0, 0.0], [[hw, -hh, hd], [hw, -hh, -hd], [hw, hh, -hd], [hw, hh, hd]]),
601            ([-1.0, 0.0, 0.0], [[-hw, -hh, -hd], [-hw, -hh, hd], [-hw, hh, hd], [-hw, hh, -hd]]),
602        ];
603        for (nrm, verts) in &faces {
604            let base = mesh.pos_attr.data.len() as u32;
605            for v in verts {
606                mesh.pos_attr.push(*v);
607                mesh.nrm_attr.push(*nrm);
608            }
609            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
610            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
611            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
612            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
613            mesh.uvm_attr.push([0.0, 0.0]);
614            mesh.uvm_attr.push([1.0, 0.0]);
615            mesh.uvm_attr.push([1.0, 1.0]);
616            mesh.uvm_attr.push([0.0, 1.0]);
617            mesh.ind_attr.push(base);
618            mesh.ind_attr.push(base + 1);
619            mesh.ind_attr.push(base + 2);
620            mesh.ind_attr.push(base);
621            mesh.ind_attr.push(base + 2);
622            mesh.ind_attr.push(base + 3);
623        }
624        mesh
625    }
626
627    /// Generates a UV sphere centred at the origin.
628    ///
629    /// The sphere is constructed with a latitude/longitude grid and normals
630    /// that are exact for perfect shading. Positions and normals use the same
631    /// unit direction vector, so the sphere works correctly with dynamic
632    /// lighting.
633    ///
634    /// # Conventions
635    ///
636    /// - **Y-up** — the north pole is at `(0, radius, 0)`.
637    /// - **UVs** — `u` wraps around the equator, `v` goes from pole to pole.
638    /// - **Normals** — per-vertex smooth (interpolated across triangles).
639    ///
640    /// # Resolution
641    ///
642    /// `stacks` controls the number of latitude subdivisions, `sectors` controls
643    /// the number of longitude subdivisions. A sphere with `stacks = 16` and
644    /// `sectors = 32` is a good balance between quality and vertex count.
645    pub fn sphere(radius: f32, stacks: u32, sectors: u32) -> Self {
646        let mut mesh = Self::empty();
647        let pi = std::f32::consts::PI;
648        for i in 0..=stacks {
649            let phi = pi * i as f32 / stacks as f32;
650            for j in 0..=sectors {
651                let theta = std::f32::consts::TAU * j as f32 / sectors as f32;
652                let x = phi.sin() * theta.cos();
653                let y = phi.cos();
654                let z = phi.sin() * theta.sin();
655                mesh.pos_attr.push([radius * x, radius * y, radius * z]);
656                mesh.nrm_attr.push([x, y, z]);
657                mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
658                mesh.uvm_attr.push([j as f32 / sectors as f32, i as f32 / stacks as f32]);
659            }
660        }
661        for i in 0..stacks {
662            for j in 0..sectors {
663                let first = i * (sectors + 1) + j;
664                let second = first + sectors + 1;
665                mesh.ind_attr.push(first);
666                mesh.ind_attr.push(second);
667                mesh.ind_attr.push(first + 1);
668                mesh.ind_attr.push(second);
669                mesh.ind_attr.push(second + 1);
670                mesh.ind_attr.push(first + 1);
671            }
672        }
673        mesh
674    }
675
676    /// Generates a cylinder centred at the origin, oriented along Y.
677    ///
678    /// The cylinder body consists of a single quad strip around the circumference
679    /// with `segments` subdivisions. Optionally closes the top and bottom caps
680    /// with triangular fans.
681    ///
682    /// # Conventions
683    ///
684    /// - **Y-up** — the top face at `+height / 2`, bottom at `-height / 2`.
685    /// - **Normals** — per-vertex on the cylinder body (exact radial normal);
686    ///   flat for caps (pointing straight up/down).
687    /// - **UVs** — wraps around the body; caps cover a radial fan.
688    ///
689    /// # When to cap
690    ///
691    /// Set `cap = true` for a solid cylinder (e.g. a column). Set `cap = false`
692    /// for an open tube (e.g. a pipe segment).
693    pub fn cylinder(radius: f32, height: f32, segments: u32, cap: bool) -> Self {
694        let mut mesh = Self::empty();
695        let hh = height * 0.5;
696        for i in 0..=segments {
697            let a = std::f32::consts::TAU * i as f32 / segments as f32;
698            let (s, c) = a.sin_cos();
699            mesh.pos_attr.push([radius * c, hh, radius * s]);
700            mesh.nrm_attr.push([c, 0.0, s]);
701            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
702            mesh.uvm_attr.push([i as f32 / segments as f32, 1.0]);
703        }
704        for i in 0..=segments {
705            let a = std::f32::consts::TAU * i as f32 / segments as f32;
706            let (s, c) = a.sin_cos();
707            mesh.pos_attr.push([radius * c, -hh, radius * s]);
708            mesh.nrm_attr.push([c, 0.0, s]);
709            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
710            mesh.uvm_attr.push([i as f32 / segments as f32, 0.0]);
711        }
712        for i in 0..segments {
713            let t = i;
714            let b = segments + 1 + i;
715            mesh.ind_attr.push(t);
716            mesh.ind_attr.push(b);
717            mesh.ind_attr.push(t + 1);
718            mesh.ind_attr.push(b);
719            mesh.ind_attr.push(b + 1);
720            mesh.ind_attr.push(t + 1);
721        }
722        if cap {
723            let top_center = mesh.pos_attr.data.len() as u32;
724            mesh.pos_attr.push([0.0, hh, 0.0]);
725            mesh.nrm_attr.push([0.0, 1.0, 0.0]);
726            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
727            mesh.uvm_attr.push([0.5, 0.5]);
728            for i in 0..segments {
729                mesh.ind_attr.push(top_center);
730                mesh.ind_attr.push(i);
731                mesh.ind_attr.push(i + 1);
732            }
733            let bot_center = mesh.pos_attr.data.len() as u32;
734            mesh.pos_attr.push([0.0, -hh, 0.0]);
735            mesh.nrm_attr.push([0.0, -1.0, 0.0]);
736            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
737            mesh.uvm_attr.push([0.5, 0.5]);
738            for i in 0..segments {
739                let b = segments + 1 + i;
740                mesh.ind_attr.push(bot_center);
741                mesh.ind_attr.push(b + 1);
742                mesh.ind_attr.push(b);
743            }
744        }
745        mesh
746    }
747
748    /// Generates a cone centred at the origin, oriented along Y, with the apex
749    /// at +Y.
750    ///
751    /// The cone body is a single strip of triangles from apex to base ring.
752    /// Optionally closes the base with a triangular fan.
753    ///
754    /// # Conventions
755    ///
756    /// - **Apex** at `(0, +height / 2, 0)`, base at `(0, -height / 2, 0)`.
757    /// - **Normals** — per-vertex smooth normals that account for the cone's
758    ///   slope (the normal leans outward from the apex).
759    /// - **UVs** — the body maps the apex to the top of the UV range.
760    ///
761    /// # When to cap
762    ///
763    /// Set `cap = true` for a solid cone. Set `cap = false` for a cone-shaped
764    /// surface (e.g. a speaker horn).
765    pub fn cone(radius: f32, height: f32, segments: u32, cap: bool) -> Self {
766        let mut mesh = Self::empty();
767        let hh = height * 0.5;
768        mesh.pos_attr.push([0.0, hh, 0.0]);
769        mesh.nrm_attr.push([0.0, 1.0, 0.0]);
770        mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
771        mesh.uvm_attr.push([0.5, 1.0]);
772        for i in 0..=segments {
773            let a = std::f32::consts::TAU * i as f32 / segments as f32;
774            let (s, c) = a.sin_cos();
775            let nx = c;
776            let nz = s;
777            let ny = radius / height;
778            let len = (nx * nx + ny * ny + nz * nz).sqrt();
779            mesh.pos_attr.push([radius * c, -hh, radius * s]);
780            mesh.nrm_attr.push([nx / len, ny / len, nz / len]);
781            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
782            mesh.uvm_attr.push([i as f32 / segments as f32, 0.0]);
783        }
784        for i in 0..segments {
785            mesh.ind_attr.push(0);
786            mesh.ind_attr.push(i + 1);
787            mesh.ind_attr.push(i + 2);
788        }
789        if cap {
790            let center = mesh.pos_attr.data.len() as u32;
791            mesh.pos_attr.push([0.0, -hh, 0.0]);
792            mesh.nrm_attr.push([0.0, -1.0, 0.0]);
793            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
794            mesh.uvm_attr.push([0.5, 0.5]);
795            for i in 0..segments {
796                mesh.ind_attr.push(center);
797                mesh.ind_attr.push(center + 1 + i + 1);
798                mesh.ind_attr.push(center + 1 + i);
799            }
800        }
801        mesh
802    }
803
804    /// Generates a torus (donut shape) lying in the XZ plane.
805    ///
806    /// The torus is a ring swept around the Y axis. `major_radius` controls the
807    /// overall size of the ring and `minor_radius` controls the thickness of the
808    /// tube.
809    ///
810    /// # Conventions
811    ///
812    /// - **Y-up** — the torus lies flat in the XZ plane.
813    /// - **Normals** — per-vertex smooth (exact radial normal from the tube centre).
814    /// - **UVs** — `u` wraps around the major ring, `v` wraps around the minor ring.
815    /// - **Resolution** — `major_segments` around the ring, `minor_segments`
816    ///   around the tube. 32 × 16 gives a smooth result.
817    ///
818    /// # Use cases
819    ///
820    /// - Donuts, rings, tyres, rope coils, particle accelerator visualisations.
821    pub fn torus(major_radius: f32, minor_radius: f32, major_segments: u32, minor_segments: u32) -> Self {
822        let mut mesh = Self::empty();
823        for i in 0..=major_segments {
824            let u = std::f32::consts::TAU * i as f32 / major_segments as f32;
825            for j in 0..=minor_segments {
826                let v = std::f32::consts::TAU * j as f32 / minor_segments as f32;
827                let x = (major_radius + minor_radius * v.cos()) * u.cos();
828                let y = minor_radius * v.sin();
829                let z = (major_radius + minor_radius * v.cos()) * u.sin();
830                mesh.pos_attr.push([x, y, z]);
831                let nx = v.cos() * u.cos();
832                let ny = v.sin();
833                let nz = v.cos() * u.sin();
834                mesh.nrm_attr.push([nx, ny, nz]);
835                mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
836                mesh.uvm_attr.push([
837                    i as f32 / major_segments as f32,
838                    j as f32 / minor_segments as f32,
839                ]);
840            }
841        }
842        let stride = minor_segments + 1;
843        for i in 0..major_segments {
844            for j in 0..minor_segments {
845                let first = i * stride + j;
846                let second = first + stride;
847                mesh.ind_attr.push(first);
848                mesh.ind_attr.push(second);
849                mesh.ind_attr.push(first + 1);
850                mesh.ind_attr.push(second);
851                mesh.ind_attr.push(second + 1);
852                mesh.ind_attr.push(first + 1);
853            }
854        }
855        mesh
856    }
857
858    /// Generates a flat quad in the XZ plane (Y-up), centred at the origin.
859    ///
860    /// The plane has a normal of `(0, 1, 0)` and covers `width × depth` units.
861    /// UVs span `[0,1]²` across the full surface.
862    ///
863    /// # Use cases
864    ///
865    /// - Ground planes, floors, table-tops, decals.
866    /// - Combine with a translucent shader for shadow receivers.
867    ///
868    /// # Conventions
869    ///
870    /// - **Orientation** — faces +Y (up).
871    /// - **Windings** — counter-clockwise when viewed from above.
872    pub fn plane(width: f32, depth: f32) -> Self {
873        let mut mesh = Self::empty();
874        let hw = width * 0.5;
875        let hd = depth * 0.5;
876        mesh.pos_attr.push([-hw, 0.0, hd]);
877        mesh.pos_attr.push([hw, 0.0, hd]);
878        mesh.pos_attr.push([hw, 0.0, -hd]);
879        mesh.pos_attr.push([-hw, 0.0, -hd]);
880        for _ in 0..4 {
881            mesh.nrm_attr.push([0.0, 1.0, 0.0]);
882            mesh.col_attr.push([1.0, 1.0, 1.0, 1.0]);
883        }
884        mesh.uvm_attr.push([0.0, 0.0]);
885        mesh.uvm_attr.push([1.0, 0.0]);
886        mesh.uvm_attr.push([1.0, 1.0]);
887        mesh.uvm_attr.push([0.0, 1.0]);
888        mesh.ind_attr.push(0);
889        mesh.ind_attr.push(1);
890        mesh.ind_attr.push(2);
891        mesh.ind_attr.push(0);
892        mesh.ind_attr.push(2);
893        mesh.ind_attr.push(3);
894        mesh
895    }
896
897    /// Attaches a custom per-vertex attribute (e.g. bone weights, AO).
898    ///
899    /// The custom attribute must have the same number of elements as the
900    /// mesh's vertex count.
901    pub fn attach_custom_attr(&mut self, attr: CustomATTR) {
902        self.cus_attrs.push(attr);
903    }
904
905    /// Returns `true` if no built-in attributes are populated and no custom
906    /// attributes are attached.
907    pub fn has_no_attr(&self) -> bool {
908        self.starts_with_custom() && self.cus_attrs.is_empty()
909    }
910
911    /// Returns `true` if no built-in vertex attributes are populated.
912    ///
913    /// This does not check custom attributes — use [`has_no_attr`](Self::has_no_attr)
914    /// for a complete emptiness check.
915    pub fn starts_with_custom(&self) -> bool {
916        self.pos_attr.is_empty() && self.col_attr.is_empty()
917            && self.uvm_attr.is_empty() && self.nrm_attr.is_empty()
918    }
919
920    /// Interleaves vertex attributes and uploads to the GPU.
921    ///
922    /// Returns a [`MeshHandle`] ready for instanced or direct drawing.
923    /// This is the final step of the asset-to-GPU pipeline for 3D meshes.
924    pub fn ship(&self) -> MeshHandle {
925        create_mesh3d_handle(self)
926    }
927}
928
929/// CPU-side 2D mesh geometry — positions, UVs, colours, indices, layer, and centre.
930///
931/// `Mesh2DFile` is the 2D counterpart of [`Mesh3DFile`]. It adds:
932///
933/// - A `layer` field for draw-order sorting.
934/// - An `aspect` ratio and [`Center`] enumeration for positioning.
935///
936/// # Generated primitives
937///
938/// | Function | Description |
939/// |---|---|
940/// | [`quad`](Mesh2DFile::quad) | Textured quad sized to a canvas pixel area |
941/// | [`fullscreen_quad`](Mesh2DFile::fullscreen_quad) | NDC-space quad (-1..1) |
942/// | [`rect`](Mesh2DFile::rect) | Rectangle centred at origin |
943/// | [`circle`](Mesh2DFile::circle) | Approximated by a regular polygon |
944/// | [`polygon`](Mesh2DFile::polygon) | Regular polygon (same as circle) |
945/// | [`ring`](Mesh2DFile::ring) | Annulus (donut) shape |
946pub struct Mesh2DFile {
947    pub pos_attr: Pos2DATTR,
948    pub layer: u8,
949    pub aspect: f32,
950    pub col_attr: ColATTR,
951    pub uvm_attr: UVMATTR,
952    pub ind_attr: IndATTR,
953    pub cus_attrs: Vec<CustomATTR>,
954}
955
956/// Controls how 2D mesh positions are offset relative to their centre.
957///
958/// Used when calling [`Mesh2DFile::set_center`] or when constructing
959/// primitive meshes.
960pub enum Center {
961    TopLeft, TopRight, BottomLeft, BottomRight, Middle, Custom(f32, f32),
962}
963
964impl Center {
965    fn offset(&self) -> Vector2<f32> {
966        match self {
967            Center::TopLeft => Vector2::new(1.0, -1.0),
968            Center::TopRight => Vector2::new(-1.0, -1.0),
969            Center::BottomRight => Vector2::new(-1.0, 1.0),
970            Center::BottomLeft => Vector2::new(1.0, 1.0),
971            Center::Middle => Vector2::new(0.0, 0.0),
972            Center::Custom(x, y) => Vector2::new(-x, -y),
973        }
974    }
975}
976
977impl Mesh2DFile {
978    /// Creates an empty 2D mesh.
979    pub fn empty() -> Self {
980        Self {
981            pos_attr: Pos2DATTR::empty(),
982            layer: 0,
983            aspect: 1.0,
984            col_attr: ColATTR::empty(),
985            uvm_attr: UVMATTR::empty(),
986            ind_attr: IndATTR::empty(),
987            cus_attrs: Vec::new(),
988        }
989    }
990
991    fn offset_pos_by_center(&mut self, center: &Center) {
992        let offset = center.offset();
993        for pos in &mut self.pos_attr.data {
994            pos[0] += offset.x * self.aspect;
995            pos[1] += offset.y;
996        }
997    }
998
999    /// Replaces the position attribute.
1000    pub fn set_pos_attr(&mut self, attr: Pos2DATTR) { self.pos_attr = attr; }
1001
1002    /// Sets the draw layer (higher values render on top).
1003    pub fn set_layer(&mut self, layer: u8) { self.layer = layer; }
1004
1005    /// Offsets all vertex positions by the centre vector.
1006    pub fn set_center(&mut self, center: Center) { self.offset_pos_by_center(&center); }
1007
1008    /// Replaces the colour attribute.
1009    pub fn set_col_attr(&mut self, attr: ColATTR) { self.col_attr = attr; }
1010
1011    /// Replaces the UV attribute.
1012    pub fn set_uvm_attr(&mut self, attr: UVMATTR) { self.uvm_attr = attr; }
1013
1014    /// Replaces the index attribute.
1015    pub fn set_ind_attr(&mut self, attr: IndATTR) { self.ind_attr = attr; }
1016
1017    /// Creates a textured quad sized to fill the given canvas area.
1018    ///
1019    /// The quad's aspect ratio matches the canvas, and it is centred by default.
1020    /// Used for rendering sprites or UI elements where the quad should preserve
1021    /// the canvas's proportions in NDC space.
1022    ///
1023    /// # Conventions
1024    ///
1025    /// - **Position** — 2D in NDC-like space (aspect-ratio aware).
1026    /// - **UVs** — full `[0,1]²` coverage.
1027    /// - **Topology** — 2 triangles, 4 vertices.
1028    pub fn quad(size: &optic_core::Size2D) -> Self {
1029        let mut mesh = Self::empty();
1030        mesh.aspect = size.aspect_ratio();
1031        let x = mesh.aspect;
1032        let y = 1.0;
1033        mesh.set_pos_attr(Pos2DATTR::from_array(&[[-x, y], [x, y], [x, -y], [-x, -y]]));
1034        mesh.offset_pos_by_center(&Center::Middle);
1035        mesh.set_col_attr(ColATTR::from_array(&[[1.0, 1.0, 1.0, 1.0]; 4]));
1036        mesh.set_uvm_attr(UVMATTR::from_array(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]));
1037        mesh.set_ind_attr(IndATTR::from_array(&[0, 2, 1, 2, 0, 3]));
1038        mesh
1039    }
1040
1041    /// Creates a quad covering the entire NDC space (-1 to +1 on both axes).
1042    ///
1043    /// Useful for full-screen post-processing effects.
1044    pub fn fullscreen_quad() -> Self {
1045        let mut mesh = Self::empty();
1046        mesh.aspect = 1.0;
1047        mesh.set_pos_attr(Pos2DATTR::from_array(&[
1048            [-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0],
1049        ]));
1050        mesh.set_col_attr(ColATTR::from_array(&[[1.0, 1.0, 1.0, 1.0]; 4]));
1051        mesh.set_uvm_attr(UVMATTR::from_array(&[
1052            [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0],
1053        ]));
1054        mesh.set_ind_attr(IndATTR::from_array(&[0, 1, 2, 0, 2, 3]));
1055        mesh
1056    }
1057
1058    /// Attaches a custom per-vertex attribute.
1059    pub fn attach_custom_attr(&mut self, attr: CustomATTR) {
1060        self.cus_attrs.push(attr);
1061    }
1062
1063    /// Creates a circle approximated by a regular polygon.
1064    ///
1065    /// The first vertex is at the centre, creating a fan topology.
1066    pub fn circle(radius: f32, segments: u32) -> Self {
1067        let mut mesh = Self::empty();
1068        let mut pos = vec![[0.0f32, 0.0f32]];
1069        let mut uvm = vec![[0.5f32, 0.5f32]];
1070        for i in 0..segments {
1071            let a = std::f32::consts::TAU * i as f32 / segments as f32;
1072            pos.push([radius * a.cos(), radius * a.sin()]);
1073            uvm.push([0.5 + 0.5 * a.cos(), 0.5 + 0.5 * a.sin()]);
1074        }
1075        let mut ind = Vec::new();
1076        for i in 0..segments {
1077            ind.push(0);
1078            ind.push(i + 1);
1079            ind.push(if i + 1 < segments { i + 2 } else { 1 });
1080        }
1081        let vert_count = (segments + 1) as usize;
1082        mesh.set_pos_attr(Pos2DATTR::from(pos));
1083        mesh.set_col_attr(ColATTR::from(vec![[1.0f32; 4]; vert_count]));
1084        mesh.set_uvm_attr(UVMATTR::from(uvm));
1085        mesh.set_ind_attr(IndATTR::from(ind));
1086        mesh
1087    }
1088
1089    /// Creates a regular polygon (alias for [`circle`](Mesh2DFile::circle)).
1090    pub fn polygon(radius: f32, sides: u32) -> Self {
1091        Self::circle(radius, sides)
1092    }
1093
1094    /// Creates an annular (ring) shape with inner and outer radii.
1095    pub fn ring(inner_radius: f32, outer_radius: f32, segments: u32) -> Self {
1096        let mut mesh = Self::empty();
1097        let mut pos = Vec::new();
1098        let mut uvm = Vec::new();
1099        for i in 0..segments {
1100            let a = std::f32::consts::TAU * i as f32 / segments as f32;
1101            let (s, c) = a.sin_cos();
1102            pos.push([outer_radius * c, outer_radius * s]);
1103            uvm.push([0.5 + 0.5 * c, 0.5 + 0.5 * s]);
1104        }
1105        for i in 0..segments {
1106            let a = std::f32::consts::TAU * i as f32 / segments as f32;
1107            let (s, c) = a.sin_cos();
1108            pos.push([inner_radius * c, inner_radius * s]);
1109            uvm.push([0.5 + 0.5 * c * inner_radius / outer_radius, 0.5 + 0.5 * s * inner_radius / outer_radius]);
1110        }
1111        let mut ind = Vec::new();
1112        for i in 0..segments {
1113            let next = (i + 1) % segments;
1114            let o1 = i;
1115            let o2 = next;
1116            let i1 = segments + i;
1117            let i2 = segments + next;
1118            ind.push(o1);
1119            ind.push(o2);
1120            ind.push(i1);
1121            ind.push(i1);
1122            ind.push(o2);
1123            ind.push(i2);
1124        }
1125        let vert_count = (segments * 2) as usize;
1126        mesh.set_pos_attr(Pos2DATTR::from(pos));
1127        mesh.set_col_attr(ColATTR::from(vec![[1.0f32; 4]; vert_count]));
1128        mesh.set_uvm_attr(UVMATTR::from(uvm));
1129        mesh.set_ind_attr(IndATTR::from(ind));
1130        mesh
1131    }
1132
1133    /// Creates a rectangle centred at the origin with the given dimensions.
1134    ///
1135    /// This is a 2D quad in world-space units (not NDC). Use for billboards,
1136    /// 2D physics bodies, or UI elements that need precise world-space sizing.
1137    ///
1138    /// # Conventions
1139    ///
1140    /// - **Position** — 2D world-space, centred.
1141    /// - **UVs** — full `[0,1]²` coverage.
1142    /// - **Topology** — 2 triangles, 4 vertices.
1143    pub fn rect(width: f32, height: f32) -> Self {
1144        let mut mesh = Self::empty();
1145        let x = width * 0.5;
1146        let y = height * 0.5;
1147        mesh.set_pos_attr(Pos2DATTR::from_array(&[[-x, y], [x, y], [x, -y], [-x, -y]]));
1148        mesh.set_col_attr(ColATTR::from_array(&[[1.0, 1.0, 1.0, 1.0]; 4]));
1149        mesh.set_uvm_attr(UVMATTR::from_array(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]));
1150        mesh.set_ind_attr(IndATTR::from_array(&[0, 2, 1, 2, 0, 3]));
1151        mesh
1152    }
1153
1154    /// Returns `true` if no built-in vertex attributes are populated.
1155    pub fn starts_with_custom(&self) -> bool {
1156        self.pos_attr.is_empty() && self.col_attr.is_empty()
1157            && self.uvm_attr.is_empty()
1158    }
1159
1160    /// Interleaves vertex attributes and uploads to the GPU.
1161    pub fn ship(&self) -> MeshHandle {
1162        create_mesh2d_handle(self)
1163    }
1164}
1165
1166#[allow(dead_code)]
1167trait BufferExt {
1168    fn push_attr<T: DataType>(&mut self, attr: &[T]);
1169}
1170
1171impl BufferExt for Vec<u8> {
1172    fn push_attr<T: DataType>(&mut self, attr: &[T]) {
1173        for elem in attr {
1174            self.extend_from_slice(&elem.u8ify());
1175        }
1176    }
1177}
1178
1179#[allow(unused_mut)]
1180fn create_mesh3d_handle(mesh: &Mesh3DFile) -> MeshHandle {
1181    let (vao_id, buf_id) = create_mesh_buffer();
1182    let ind_id = create_index_buffer();
1183
1184    let mut stride = 0usize;
1185    let mut attrs: Vec<(&ATTRInfo, &dyn AsDataRef)> = Vec::new();
1186    let mut has_indices = false;
1187    let mut ind_count = 0;
1188    let mut ind_data: &[u32];
1189
1190    if !mesh.pos_attr.is_empty() {
1191        let info = &mesh.pos_attr.info;
1192        stride += info.elem_count * info.byte_count;
1193        attrs.push((info, &mesh.pos_attr.data as &dyn AsDataRef));
1194    }
1195    if !mesh.col_attr.is_empty() {
1196        let info = &mesh.col_attr.info;
1197        stride += info.elem_count * info.byte_count;
1198        attrs.push((info, &mesh.col_attr.data as &dyn AsDataRef));
1199    }
1200    if !mesh.uvm_attr.is_empty() {
1201        let info = &mesh.uvm_attr.info;
1202        stride += info.elem_count * info.byte_count;
1203        attrs.push((info, &mesh.uvm_attr.data as &dyn AsDataRef));
1204    }
1205    if !mesh.nrm_attr.is_empty() {
1206        let info = &mesh.nrm_attr.info;
1207        stride += info.elem_count * info.byte_count;
1208        attrs.push((info, &mesh.nrm_attr.data as &dyn AsDataRef));
1209    }
1210    for cus in &mesh.cus_attrs {
1211        stride += cus.info.elem_count * cus.info.byte_count;
1212        attrs.push((&cus.info, &cus.data as &dyn AsDataRef));
1213    }
1214
1215    let vert_count = if mesh.starts_with_custom() {
1216        let first = &mesh.cus_attrs[0];
1217        first.data.len() / (first.info.byte_count * first.info.elem_count)
1218    } else {
1219        mesh.pos_attr.data.len()
1220    } as u32;
1221
1222    let mut buffer: Vec<u8> = Vec::new();
1223    for i in 0..vert_count as usize {
1224        for &(info, data) in &attrs {
1225            let elem_size = info.elem_count * info.byte_count;
1226            let start = i * elem_size;
1227            let end = start + elem_size;
1228            buffer.extend_from_slice(&data.as_bytes()[start..end]);
1229        }
1230    }
1231
1232    crate::GL::bind_vao(vao_id);
1233    crate::GL::bind_buffer(buf_id);
1234    let mut attr_id = 0u32;
1235    let mut offset = 0usize;
1236    let mut layouts = Vec::new();
1237
1238    for &(info, _) in &attrs {
1239        set_attr_layout(info, attr_id, stride, offset);
1240        offset += info.elem_count * info.byte_count;
1241        layouts.push((info.clone(), attr_id));
1242        attr_id += 1;
1243    }
1244
1245    if !buffer.is_empty() {
1246        fill_buffer(buf_id, &buffer);
1247    }
1248
1249    if !mesh.ind_attr.is_empty() {
1250        has_indices = true;
1251        ind_data = &mesh.ind_attr.data;
1252        ind_count = ind_data.len() as u32;
1253        fill_index_buffer(ind_id, ind_data);
1254
1255    }
1256
1257    MeshHandle {
1258        layouts,
1259        draw_mode: DrawMode::Triangles,
1260        has_indices,
1261        vert_count,
1262        ind_count,
1263        vao_id,
1264        buf_id,
1265        ind_id,
1266        vert_stride: stride as u32,
1267        instance_buf_id: 0,
1268        instance_count: 0,
1269    }
1270}
1271
1272#[allow(unused_mut)]
1273fn create_mesh2d_handle(mesh: &Mesh2DFile) -> MeshHandle {
1274    let (vao_id, buf_id) = create_mesh_buffer();
1275    let ind_id = create_index_buffer();
1276
1277    let mut stride = 0usize;
1278    let mut attrs: Vec<(&ATTRInfo, &dyn AsDataRef)> = Vec::new();
1279    let mut has_indices = false;
1280    let mut ind_data: &[u32];
1281    let mut ind_count = 0;
1282
1283    if !mesh.pos_attr.is_empty() {
1284        let info = &mesh.pos_attr.info;
1285        stride += info.elem_count * info.byte_count;
1286        attrs.push((info, &mesh.pos_attr.data as &dyn AsDataRef));
1287    }
1288    if !mesh.col_attr.is_empty() {
1289        let info = &mesh.col_attr.info;
1290        stride += info.elem_count * info.byte_count;
1291        attrs.push((info, &mesh.col_attr.data as &dyn AsDataRef));
1292    }
1293    if !mesh.uvm_attr.is_empty() {
1294        let info = &mesh.uvm_attr.info;
1295        stride += info.elem_count * info.byte_count;
1296        attrs.push((info, &mesh.uvm_attr.data as &dyn AsDataRef));
1297    }
1298    for cus in &mesh.cus_attrs {
1299        stride += cus.info.elem_count * cus.info.byte_count;
1300        attrs.push((&cus.info, &cus.data as &dyn AsDataRef));
1301    }
1302
1303    let vert_count = if mesh.starts_with_custom() {
1304        let first = &mesh.cus_attrs[0];
1305        first.data.len() / (first.info.byte_count * first.info.elem_count)
1306    } else {
1307        mesh.pos_attr.data.len()
1308    } as u32;
1309
1310    let mut buffer: Vec<u8> = Vec::new();
1311    for i in 0..vert_count as usize {
1312        for &(info, data) in &attrs {
1313            let elem_size = info.elem_count * info.byte_count;
1314            let start = i * elem_size;
1315            let end = start + elem_size;
1316            buffer.extend_from_slice(&data.as_bytes()[start..end]);
1317        }
1318    }
1319
1320    crate::GL::bind_vao(vao_id);
1321    crate::GL::bind_buffer(buf_id);
1322    let mut attr_id = 0u32;
1323    let mut offset = 0usize;
1324    let mut layouts = Vec::new();
1325
1326    for &(info, _) in &attrs {
1327        set_attr_layout(info, attr_id, stride, offset);
1328        offset += info.elem_count * info.byte_count;
1329        layouts.push((info.clone(), attr_id));
1330        attr_id += 1;
1331    }
1332
1333    if !buffer.is_empty() {
1334        fill_buffer(buf_id, &buffer);
1335    }
1336
1337    if !mesh.ind_attr.is_empty() {
1338        has_indices = true;
1339        ind_data = &mesh.ind_attr.data;
1340        ind_count = ind_data.len() as u32;
1341        fill_index_buffer(ind_id, ind_data);
1342
1343    }
1344
1345    MeshHandle {
1346        layouts,
1347        draw_mode: DrawMode::Triangles,
1348        has_indices,
1349        vert_count,
1350        ind_count,
1351        vao_id,
1352        buf_id,
1353        ind_id,
1354        vert_stride: stride as u32,
1355        instance_buf_id: 0,
1356        instance_count: 0,
1357    }
1358}
1359
1360trait AsDataRef {
1361    fn as_bytes(&self) -> &[u8];
1362}
1363
1364impl AsDataRef for Vec<[f32; 3]> {
1365    fn as_bytes(&self) -> &[u8] {
1366        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 12) }
1367    }
1368}
1369
1370impl AsDataRef for Vec<[f32; 2]> {
1371    fn as_bytes(&self) -> &[u8] {
1372        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 8) }
1373    }
1374}
1375
1376impl AsDataRef for Vec<[f32; 4]> {
1377    fn as_bytes(&self) -> &[u8] {
1378        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 16) }
1379    }
1380}
1381
1382impl AsDataRef for Vec<u32> {
1383    fn as_bytes(&self) -> &[u8] {
1384        unsafe { std::slice::from_raw_parts(self.as_ptr() as *const u8, self.len() * 4) }
1385    }
1386}
1387
1388impl AsDataRef for Vec<u8> {
1389    fn as_bytes(&self) -> &[u8] {
1390        self
1391    }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396    use super::*;
1397    use optic_core::Size2D;
1398
1399    #[test]
1400    fn mesh3d_file_empty() {
1401        let m = Mesh3DFile::empty();
1402        assert!(m.pos_attr.is_empty());
1403        assert!(m.col_attr.is_empty());
1404        assert!(m.uvm_attr.is_empty());
1405        assert!(m.nrm_attr.is_empty());
1406        assert!(m.ind_attr.is_empty());
1407        assert!(m.cus_attrs.is_empty());
1408    }
1409
1410    #[test]
1411    fn mesh3d_file_starts_with_custom() {
1412        let m = Mesh3DFile::empty();
1413        assert!(m.starts_with_custom());
1414    }
1415
1416    #[test]
1417    fn mesh3d_file_has_no_attr_true() {
1418        let m = Mesh3DFile::empty();
1419        assert!(m.has_no_attr());
1420    }
1421
1422    #[test]
1423    fn mesh3d_file_has_no_attr_false_with_pos() {
1424        let mut m = Mesh3DFile::empty();
1425        m.pos_attr.push([1.0, 2.0, 3.0]);
1426        assert!(!m.has_no_attr());
1427    }
1428
1429    #[test]
1430    fn mesh3d_file_attach_custom() {
1431        let mut m = Mesh3DFile::empty();
1432        let attr = CustomATTR::empty::<u32>("bone_ids");
1433        m.attach_custom_attr(attr);
1434        assert_eq!(m.cus_attrs.len(), 1);
1435    }
1436
1437    #[test]
1438    fn mesh2d_file_empty() {
1439        let m = Mesh2DFile::empty();
1440        assert!(m.pos_attr.is_empty());
1441        assert!(m.col_attr.is_empty());
1442        assert!(m.uvm_attr.is_empty());
1443        assert!(m.ind_attr.is_empty());
1444        assert!(m.cus_attrs.is_empty());
1445        assert_eq!(m.layer, 0);
1446        assert!((m.aspect - 1.0).abs() < f32::EPSILON);
1447    }
1448
1449    #[test]
1450    fn mesh2d_file_starts_with_custom() {
1451        let m = Mesh2DFile::empty();
1452        assert!(m.starts_with_custom());
1453    }
1454
1455    #[test]
1456    fn mesh2d_quad() {
1457        let size = Size2D::from(100, 100);
1458        let m = Mesh2DFile::quad(&size);
1459        assert_eq!(m.pos_attr.data.len(), 4);
1460        assert_eq!(m.col_attr.data.len(), 4);
1461        assert_eq!(m.uvm_attr.data.len(), 4);
1462        assert_eq!(m.ind_attr.data.len(), 6);
1463    }
1464
1465    #[test]
1466    fn mesh2d_setters() {
1467        let mut m = Mesh2DFile::empty();
1468        m.set_pos_attr(Pos2DATTR::from_array(&[[0.0, 0.0], [1.0, 0.0]]));
1469        m.set_col_attr(ColATTR::from_array(&[[1.0; 4]; 2]));
1470        m.set_uvm_attr(UVMATTR::from_array(&[[0.0, 0.0], [1.0, 0.0]]));
1471        m.set_ind_attr(IndATTR::from_array(&[0, 1]));
1472        m.set_layer(5);
1473        assert_eq!(m.pos_attr.data.len(), 2);
1474        assert_eq!(m.layer, 5);
1475    }
1476
1477    #[test]
1478    fn mesh2d_set_center_topleft() {
1479        let mut m = Mesh2DFile::empty();
1480        m.set_pos_attr(Pos2DATTR::from_array(&[[0.0, 0.0]]));
1481        m.set_center(Center::TopLeft);
1482        assert!((m.pos_attr.data[0][0] - 1.0).abs() < f32::EPSILON);
1483        assert!((m.pos_attr.data[0][1] - (-1.0)).abs() < f32::EPSILON);
1484    }
1485
1486    #[test]
1487    fn mesh2d_set_center_custom() {
1488        let mut m = Mesh2DFile::empty();
1489        m.set_pos_attr(Pos2DATTR::from_array(&[[0.0, 0.0]]));
1490        m.set_center(Center::Custom(0.5, 0.5));
1491        assert!((m.pos_attr.data[0][0] - (-0.5)).abs() < f32::EPSILON);
1492        assert!((m.pos_attr.data[0][1] - (-0.5)).abs() < f32::EPSILON);
1493    }
1494
1495    #[test]
1496    fn mesh3d_from_obj_src() {
1497        let obj = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nf 1 2 3";
1498        let mesh = Mesh3DFile::from_obj_src(obj).unwrap();
1499        assert_eq!(mesh.pos_attr.data.len(), 3);
1500        assert_eq!(mesh.ind_attr.data.len(), 3);
1501    }
1502
1503    #[test]
1504    fn mesh3d_from_obj_src_non_triangle() {
1505        let obj = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nf 1 2 3 4";
1506        let result = Mesh3DFile::from_obj_src(obj);
1507        assert!(result.is_err());
1508    }
1509
1510    #[test]
1511    fn mesh3d_cached_roundtrip() {
1512        let obj = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nf 1 2 3";
1513        let mesh = Mesh3DFile::from_obj_src(obj).unwrap();
1514        let path = "/tmp/optic_test_mesh3d_cache.omesh";
1515        mesh.save_cached(path).unwrap();
1516        let loaded = Mesh3DFile::from_cached(path).unwrap();
1517        assert_eq!(loaded.pos_attr.data.len(), mesh.pos_attr.data.len());
1518        assert_eq!(loaded.ind_attr.data.len(), mesh.ind_attr.data.len());
1519        assert_eq!(loaded.pos_attr.data, mesh.pos_attr.data);
1520        assert_eq!(loaded.ind_attr.data, mesh.ind_attr.data);
1521        let _ = std::fs::remove_file(path);
1522    }
1523
1524    #[test]
1525    fn mesh3d_from_stl_ascii() {
1526        let stl = "solid cube\n\
1527            facet normal 0.0 0.0 1.0\n\
1528                outer loop\n\
1529                    vertex -1.0 -1.0 1.0\n\
1530                    vertex 1.0 -1.0 1.0\n\
1531                    vertex 1.0 1.0 1.0\n\
1532                endloop\n\
1533            endfacet\n\
1534            endsolid cube\n";
1535        let mesh = Mesh3DFile::from_stl_src(stl.as_bytes()).unwrap();
1536        assert_eq!(mesh.pos_attr.data.len(), 3);
1537        assert_eq!(mesh.ind_attr.data.len(), 3);
1538        assert_eq!(mesh.nrm_attr.data.len(), 3);
1539        assert!((mesh.nrm_attr.data[0][2] - 1.0).abs() < f32::EPSILON);
1540    }
1541
1542    #[test]
1543    fn obj_parse_simple_triangle() {
1544        let obj = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nf 1 2 3";
1545        match OBJ::parse(obj) {
1546            OBJ::Parsed { pos_attr, ind_attr, .. } => {
1547                assert_eq!(pos_attr.data.len(), 3);
1548                assert_eq!(ind_attr.data.len(), 3);
1549                assert_eq!(pos_attr.data[0], [0.0, 0.0, 0.0]);
1550                assert_eq!(pos_attr.data[1], [1.0, 0.0, 0.0]);
1551                assert_eq!(pos_attr.data[2], [0.0, 1.0, 0.0]);
1552            }
1553            OBJ::NonTriangle(_) => panic!("expected parsed triangle"),
1554        }
1555    }
1556
1557    #[test]
1558    fn obj_parse_with_uv_and_normals() {
1559        let obj = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nvt 0.0 0.0\nvt 1.0 0.0\nvt 0.0 1.0\nvn 0.0 0.0 1.0\nf 1/1/1 2/2/1 3/3/1";
1560        match OBJ::parse(obj) {
1561            OBJ::Parsed { pos_attr, uvm_attr, nrm_attr, ind_attr, .. } => {
1562                assert_eq!(pos_attr.data.len(), 3);
1563                assert_eq!(uvm_attr.data.len(), 3);
1564                assert!(!nrm_attr.data.is_empty());
1565                assert_eq!(ind_attr.data.len(), 3);
1566            }
1567            OBJ::NonTriangle(_) => panic!("expected parsed triangle"),
1568        }
1569    }
1570
1571    #[test]
1572    fn obj_parse_non_triangle() {
1573        let obj = "v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nv 1.0 1.0 0.0\nf 1 2 3 4";
1574        match OBJ::parse(obj) {
1575            OBJ::Parsed { .. } => panic!("expected non-triangle error"),
1576            OBJ::NonTriangle(line) => assert!(line.contains("4")),
1577        }
1578    }
1579
1580    #[test]
1581    fn obj_parse_empty() {
1582        match OBJ::parse("") {
1583            OBJ::Parsed { pos_attr, ind_attr, .. } => {
1584                assert!(pos_attr.is_empty());
1585                assert!(ind_attr.is_empty());
1586            }
1587            OBJ::NonTriangle(_) => panic!("expected empty parsed"),
1588        }
1589    }
1590
1591    #[test]
1592    fn obj_parse_parse_2() {
1593        let words = vec!["vt", "0.5", "0.5"];
1594        let result = OBJ::parse_2(&words);
1595        assert!((result[0] - 0.5).abs() < f32::EPSILON);
1596        assert!((result[1] - 0.5).abs() < f32::EPSILON);
1597    }
1598
1599    #[test]
1600    fn obj_parse_parse_3() {
1601        let words = vec!["v", "1.0", "2.0", "3.0"];
1602        let result = OBJ::parse_3(&words);
1603        assert_eq!(result, [1.0, 2.0, 3.0]);
1604    }
1605
1606    #[test]
1607    fn center_variants() {
1608        assert_eq!(Center::TopLeft.offset(), Vector2::new(1.0, -1.0));
1609        assert_eq!(Center::TopRight.offset(), Vector2::new(-1.0, -1.0));
1610        assert_eq!(Center::BottomRight.offset(), Vector2::new(-1.0, 1.0));
1611        assert_eq!(Center::BottomLeft.offset(), Vector2::new(1.0, 1.0));
1612        assert_eq!(Center::Middle.offset(), Vector2::new(0.0, 0.0));
1613    }
1614
1615    #[test]
1616    fn mesh2d_circle() {
1617        let m = Mesh2DFile::circle(1.0, 8);
1618        assert_eq!(m.pos_attr.data.len(), 9);
1619        assert_eq!(m.col_attr.data.len(), 9);
1620        assert_eq!(m.uvm_attr.data.len(), 9);
1621        assert_eq!(m.ind_attr.data.len(), 24);
1622    }
1623
1624    #[test]
1625    fn mesh2d_polygon() {
1626        let m = Mesh2DFile::polygon(1.0, 6);
1627        assert_eq!(m.pos_attr.data.len(), 7);
1628        assert_eq!(m.ind_attr.data.len(), 18);
1629    }
1630
1631    #[test]
1632    fn mesh2d_ring() {
1633        let m = Mesh2DFile::ring(0.5, 1.0, 8);
1634        assert_eq!(m.pos_attr.data.len(), 16);
1635        assert_eq!(m.ind_attr.data.len(), 48);
1636    }
1637
1638    #[test]
1639    fn mesh2d_rect() {
1640        let m = Mesh2DFile::rect(2.0, 3.0);
1641        assert_eq!(m.pos_attr.data.len(), 4);
1642        assert_eq!(m.ind_attr.data.len(), 6);
1643        assert!((m.pos_attr.data[0][0] - (-1.0)).abs() < f32::EPSILON);
1644        assert!((m.pos_attr.data[0][1] - 1.5).abs() < f32::EPSILON);
1645    }
1646
1647    #[test]
1648    fn mesh3d_cube() {
1649        let m = Mesh3DFile::cube(2.0);
1650        assert_eq!(m.pos_attr.data.len(), 24);
1651        assert_eq!(m.nrm_attr.data.len(), 24);
1652        assert_eq!(m.ind_attr.data.len(), 36);
1653    }
1654
1655    #[test]
1656    fn mesh3d_cuboid() {
1657        let m = Mesh3DFile::cuboid(1.0, 2.0, 3.0);
1658        assert_eq!(m.pos_attr.data.len(), 24);
1659        assert_eq!(m.ind_attr.data.len(), 36);
1660    }
1661
1662    #[test]
1663    fn mesh3d_sphere() {
1664        let m = Mesh3DFile::sphere(1.0, 8, 16);
1665        let verts = (8 + 1) * (16 + 1);
1666        assert_eq!(m.pos_attr.data.len(), verts);
1667        assert_eq!(m.nrm_attr.data.len(), verts);
1668        assert_eq!(m.ind_attr.data.len(), 8 * 16 * 6);
1669    }
1670
1671    #[test]
1672    fn mesh3d_cylinder_with_caps() {
1673        let m = Mesh3DFile::cylinder(0.5, 2.0, 16, true);
1674        let body_verts = (16 + 1) * 2;
1675        let cap_verts = 2;
1676        assert_eq!(m.pos_attr.data.len(), body_verts + cap_verts);
1677    }
1678
1679    #[test]
1680    fn mesh3d_cylinder_no_caps() {
1681        let m = Mesh3DFile::cylinder(0.5, 2.0, 16, false);
1682        assert_eq!(m.pos_attr.data.len(), (16 + 1) * 2);
1683    }
1684
1685    #[test]
1686    fn mesh3d_cone_with_cap() {
1687        let m = Mesh3DFile::cone(0.5, 2.0, 16, true);
1688        let body_verts = 1 + (16 + 1);
1689        let cap_verts = 1;
1690        assert_eq!(m.pos_attr.data.len(), body_verts + cap_verts);
1691    }
1692
1693    #[test]
1694    fn mesh3d_cone_no_cap() {
1695        let m = Mesh3DFile::cone(0.5, 2.0, 16, false);
1696        assert_eq!(m.pos_attr.data.len(), 1 + (16 + 1));
1697    }
1698
1699    #[test]
1700    fn mesh3d_torus() {
1701        let m = Mesh3DFile::torus(1.0, 0.3, 12, 8);
1702        let verts = (12 + 1) * (8 + 1);
1703        assert_eq!(m.pos_attr.data.len(), verts);
1704        assert_eq!(m.nrm_attr.data.len(), verts);
1705        assert_eq!(m.ind_attr.data.len(), 12 * 8 * 6);
1706    }
1707
1708    #[test]
1709    fn mesh3d_plane() {
1710        let m = Mesh3DFile::plane(2.0, 3.0);
1711        assert_eq!(m.pos_attr.data.len(), 4);
1712        assert_eq!(m.nrm_attr.data.len(), 4);
1713        assert_eq!(m.ind_attr.data.len(), 6);
1714        for nrm in &m.nrm_attr.data {
1715            assert!((nrm[1] - 1.0).abs() < f32::EPSILON);
1716        }
1717    }
1718}