diff --git a/lib/examples/cube.rs b/lib/examples/cube.rs index 53a781b..02cb763 100644 --- a/lib/examples/cube.rs +++ b/lib/examples/cube.rs @@ -13,7 +13,8 @@ use glam::{Quat, Vec3}; use wsg_lib::AppHandler; use wsg_lib::app::AppBuilder; -use wsg_lib::resources::{Geometry, Texture}; +use wsg_lib::math::cube; +use wsg_lib::resources::Texture; use wsg_lib::utils::WsgError; /// Handler de démonstration : fait tourner le cube texturé dans `update`. @@ -22,68 +23,6 @@ struct Cube { angle: f32, } -/// Construit la `Geometry` d'un cube unitaire centré à l'origine (arête de 1), une normale et des -/// coordonnées UV par face. 24 sommets (4 par face) + 36 indices ; la couleur est absente (défaut -/// blanc opaque via `Geometry::to_vertices`). Depuis l'Étape 10, chaque face reçoit des UV [0,1]² pour -/// que la texture diffuse soit proprement projetée sur le cube. -fn cube_geometry() -> Geometry { - let s = 0.5; // demi-arête - // Chaque face : (normale sortante, 4 coins). Le culling est désactivé par défaut (PrimitiveState - // par défaut), donc l'ordre d'enroulement n'affecte pas la visibilité ; seules les normales comptent - // pour l'éclairage. - let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [ - ( - [0.0, 0.0, 1.0], - [[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]], - ), // +Z - ( - [0.0, 0.0, -1.0], - [[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]], - ), // -Z - ( - [1.0, 0.0, 0.0], - [[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]], - ), // +X - ( - [-1.0, 0.0, 0.0], - [[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]], - ), // -X - ( - [0.0, 1.0, 0.0], - [[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]], - ), // +Y - ( - [0.0, -1.0, 0.0], - [[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]], - ), // -Y - ]; - - let mut positions = Vec::with_capacity(24); - let mut normals = Vec::with_capacity(24); - let mut uvs = Vec::with_capacity(24); - // Mapping UV canonique d'un carré : BL(0,0) BR(1,0) TR(1,1) TL(0,1). - let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]; - for (normal, corners) in faces { - for (i, corner) in corners.iter().enumerate() { - positions.push(*corner); - normals.push(normal); - uvs.push(quad_uvs[i]); - } - } - - // 2 triangles par face, 36 indices. - let mut indices = Vec::with_capacity(36); - for face in 0..6u16 { - let b = face * 4; - indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]); - } - - Geometry::new(positions) - .with_normals(normals) - .with_uvs(uvs) - .with_indices(indices) -} - /// Génère un damier RGBA 8×8 (blanc/brique) *procédural*, sans asset sur disque, pour texturer le /// cube (Étape 10, D3/D4). Renvoyé en `Vec` brut RGBA8, chargeable via `Texture::from_rgba8`. fn checkerboard_rgba() -> Vec { @@ -120,7 +59,7 @@ impl AppHandler for Cube { .unwrap(); app.scene - .create_mesh("cube_mesh", cube_geometry(), Some("cube_material")) + .create_mesh("cube_mesh", cube(1.0), Some("cube_material")) .unwrap(); app.scene.add_entity("cube", "cube_mesh").unwrap(); diff --git a/lib/examples/spot_test.rs b/lib/examples/spot_test.rs index 50dd3c2..79e7ad1 100644 --- a/lib/examples/spot_test.rs +++ b/lib/examples/spot_test.rs @@ -12,7 +12,7 @@ use glam::{Quat, Vec3}; use wsg_lib::AppHandler; use wsg_lib::app::AppBuilder; -use wsg_lib::resources::Geometry; +use wsg_lib::math::cube; use wsg_lib::utils::WsgError; /// Handler de test : cube qui tourne lentement sur deux axes, éclairé **uniquement** par une spot. @@ -21,58 +21,6 @@ struct SpotTest { angle_y: f32, } -/// Cube unitaire centré à l'origine (mêmes 24 sommets / 36 indices que l'exemple `cube`). -fn cube_geometry() -> Geometry { - let s = 0.5; - let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [ - ( - [0.0, 0.0, 1.0], - [[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]], - ), // +Z - ( - [0.0, 0.0, -1.0], - [[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]], - ), // -Z - ( - [1.0, 0.0, 0.0], - [[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]], - ), // +X - ( - [-1.0, 0.0, 0.0], - [[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]], - ), // -X - ( - [0.0, 1.0, 0.0], - [[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]], - ), // +Y - ( - [0.0, -1.0, 0.0], - [[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]], - ), // -Y - ]; - - let mut positions = Vec::with_capacity(24); - let mut normals = Vec::with_capacity(24); - let mut uvs = Vec::with_capacity(24); - let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]; - for (normal, corners) in faces { - for (i, corner) in corners.iter().enumerate() { - positions.push(*corner); - normals.push(normal); - uvs.push(quad_uvs[i]); - } - } - let mut indices = Vec::with_capacity(36); - for face in 0..6u16 { - let b = face * 4; - indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]); - } - Geometry::new(positions) - .with_normals(normals) - .with_uvs(uvs) - .with_indices(indices) -} - impl AppHandler for SpotTest { fn setup(&mut self, app: &mut wsg_lib::App) { app.scene @@ -80,7 +28,7 @@ impl AppHandler for SpotTest { .unwrap(); app.scene.add_material_shader("mat", "standard").unwrap(); app.scene - .create_mesh("cube_mesh", cube_geometry(), Some("mat")) + .create_mesh("cube_mesh", cube(1.0), Some("mat")) .unwrap(); app.scene.add_entity("cube", "cube_mesh").unwrap(); diff --git a/lib/src/math/mod.rs b/lib/src/math/mod.rs index 39f3870..32c61bd 100644 --- a/lib/src/math/mod.rs +++ b/lib/src/math/mod.rs @@ -14,10 +14,13 @@ //! - `transform.rs`: Defines the `Transform` struct and its conversion to matrix form //! - `geometry.rs`: Defines the `Geometry` struct for mesh data storage //! - `camera.rs`: Defines the `Camera` struct and view/projection matrix calculations +//! - `primitives.rs`: Procedural mesh generators (cube, sphere, cylinder, cone, torus…) returning `Geometry` pub mod geometry; +pub mod primitives; pub mod transform; // Re-exports pub use geometry::{Geometry, GeometryError}; +pub use primitives::{cone, cube, cylinder, icosphere, plane, torus, uv_sphere}; pub use transform::Transform; diff --git a/lib/src/math/primitives.rs b/lib/src/math/primitives.rs new file mode 100644 index 0000000..c209925 --- /dev/null +++ b/lib/src/math/primitives.rs @@ -0,0 +1,527 @@ +//! # Primitives Module — Meshes géométriques prêts à l'emploi (Étape 15, ROADMAP 2.2) +//! +//! Générateurs de `Geometry` procédurales pour les formes 3D courantes, utilisables +//! directement dans WSGL sans import wgpu : `cube`, `plane`, `uv_sphere`, `icosphere`, +//! `cylinder`, `cone` (et `torus` en bonus). +//! +//! ## Conventions +//! - Axe **Y vers le haut**, origine centrée (sauf `plane`, ancré dans le plan XZ autour de 0). +//! - Normales **orientées vers l'extérieur** (pertinentes pour l'éclairage Phong, le culling +//! restant désactivé par défaut). +//! - UVs dans [0,1]², aussi continus que possible ; `uv_sphere`/`icosphere` projettent depuis +//! des coordonnées sphériques. +//! - Chaque générateur renvoie une `Geometry` **complète** (positions + normales + UVs + +//! indices, pas de couleurs → défaut blanc opaque via `Geometry::to_vertices`). +//! +//! ## Invariant +//! Toute géométrie produite passe `Geometry::validate()` sans erreur (vérifié par les tests). + +use crate::math::Geometry; +use glam::Vec3; +use std::collections::HashMap; + +/// Génére un cube centré à l'origine, d'arête `size`, avec une normale et des UVs par face. +/// 24 sommets (4 par face) + 36 indices. Reproduit exactement le `cube_geometry` historique des +/// exemples (Étape 5/10) pour assurer la non-régression. +pub fn cube(size: f32) -> Geometry { + let s = size * 0.5; // demi-arête + let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [ + ( + [0.0, 0.0, 1.0], + [[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]], + ), // +Z + ( + [0.0, 0.0, -1.0], + [[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]], + ), // -Z + ( + [1.0, 0.0, 0.0], + [[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]], + ), // +X + ( + [-1.0, 0.0, 0.0], + [[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]], + ), // -X + ( + [0.0, 1.0, 0.0], + [[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]], + ), // +Y + ( + [0.0, -1.0, 0.0], + [[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]], + ), // -Y + ]; + + let mut positions = Vec::with_capacity(24); + let mut normals = Vec::with_capacity(24); + let mut uvs = Vec::with_capacity(24); + let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]; + for (normal, corners) in faces { + for (i, corner) in corners.iter().enumerate() { + positions.push(*corner); + normals.push(normal); + uvs.push(quad_uvs[i]); + } + } + + let mut indices = Vec::with_capacity(36); + for face in 0..6u16 { + let b = face * 4; + indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]); + } + + Geometry::new(positions) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +/// Génére un plan horizontal dans le plan XZ (normale +Y), centré en (0, 0, 0), de dimensions +/// `width` × `depth`, subdivisé en `seg_x` × `seg_z` cellules. UVs étirées sur [0,1]². +pub fn plane(width: f32, depth: f32, seg_x: u32, seg_z: u32) -> Geometry { + let sx = seg_x.max(1); + let sz = seg_z.max(1); + let (mut positions, mut normals, mut uvs, mut indices) = ( + Vec::<[f32; 3]>::new(), + Vec::<[f32; 3]>::new(), + Vec::<[f32; 2]>::new(), + Vec::::new(), + ); + for z in 0..=sz { + let vz = z as f32 / sz as f32; + for x in 0..=sx { + let vx = x as f32 / sx as f32; + positions.push([(vx - 0.5) * width, 0.0, (vz - 0.5) * depth]); + normals.push([0.0, 1.0, 0.0]); + uvs.push([vx, vz]); + } + } + for z in 0..sz { + for x in 0..sx { + let a = z * (sx + 1) + x; + let b = a + 1; + let c = (z + 1) * (sx + 1) + x; + let d = c + 1; + indices + .extend_from_slice(&[a as u16, c as u16, b as u16, b as u16, c as u16, d as u16]); + } + } + Geometry::new(positions) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +/// Génére une sphère UV (latitude/longitude) de rayon `radius`, avec `sectors` segments autour et +/// `stacks` cercles verticaux. Normales lisses = position normalisée ; UVs sphériques. +pub fn uv_sphere(radius: f32, sectors: u32, stacks: u32) -> Geometry { + let si = sectors.max(3); + let st = stacks.max(3); + let (mut positions, mut normals, mut uvs, mut indices) = ( + Vec::<[f32; 3]>::new(), + Vec::<[f32; 3]>::new(), + Vec::<[f32; 2]>::new(), + Vec::::new(), + ); + for stack in 0..=st { + let v = stack as f32 / st as f32; + let phi = v * std::f32::consts::PI; + for sector in 0..=si { + let u = sector as f32 / si as f32; + let theta = u * 2.0 * std::f32::consts::PI; + let (sin_p, cos_p) = phi.sin_cos(); + let (sin_t, cos_t) = theta.sin_cos(); + let pos = Vec3::new( + radius * sin_p * cos_t, + radius * cos_p, + radius * sin_p * sin_t, + ); + positions.push(pos.to_array()); + normals.push(pos.normalize().to_array()); + uvs.push([u, v]); + } + } + for stack in 0..st { + for sector in 0..si { + let k1 = stack * (si + 1) + sector; + let k2 = k1 + si + 1; + let (k1, k2) = (k1 as u16, k2 as u16); + indices.extend_from_slice(&[k1, k2, k1 + 1, k1 + 1, k2, k2 + 1]); + } + } + Geometry::new(positions) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +/// Génére une icosphère (icosaèdre subdivisé) de rayon `radius`. `subdivisions = 0` donne un +/// icosaèdre (12 sommets / 20 faces / 60 indices) ; chaque subdivision raffine les faces en 4. +/// Normales lisses = direction de la position ; UVs sphériques (une couture est inévitable sans UV +/// atlas). +pub fn icosphere(radius: f32, subdivisions: u32) -> Geometry { + let t = (1.0 + 5.0_f32.sqrt()) * 0.5; + // 12 sommets unitaires (icosaèdre canonique). + let mut positions: Vec = [ + [-1.0, t, 0.0], + [1.0, t, 0.0], + [-1.0, -t, 0.0], + [1.0, -t, 0.0], + [0.0, -1.0, t], + [0.0, 1.0, t], + [0.0, -1.0, -t], + [0.0, 1.0, -t], + [t, 0.0, -1.0], + [t, 0.0, 1.0], + [-t, 0.0, -1.0], + [-t, 0.0, 1.0], + ] + .iter() + .map(|v| Vec3::from_array(*v).normalize()) + .collect(); + + let mut faces: Vec<[u32; 3]> = [ + [0, 11, 5], + [0, 5, 1], + [0, 1, 7], + [0, 7, 10], + [0, 10, 11], + [1, 5, 9], + [5, 11, 4], + [11, 10, 2], + [10, 7, 6], + [7, 1, 8], + [3, 9, 4], + [3, 4, 2], + [3, 2, 6], + [3, 6, 8], + [3, 8, 9], + [4, 9, 5], + [2, 4, 11], + [6, 2, 10], + [8, 6, 7], + [9, 8, 1], + ] + .into_iter() + .map(|[a, b, c]| [a, b, c]) + .collect(); + + for _ in 0..subdivisions { + let mut midpoint = HashMap::new(); + let old_faces = std::mem::take(&mut faces); + for [a, b, c] in old_faces { + let ab = subdiv_midpoint(&mut positions, &mut midpoint, a, b); + let bc = subdiv_midpoint(&mut positions, &mut midpoint, b, c); + let ca = subdiv_midpoint(&mut positions, &mut midpoint, c, a); + faces.push([a, ab, ca]); + faces.push([ab, b, bc]); + faces.push([ca, bc, c]); + faces.push([ab, bc, ca]); + } + } + + // Échelle au rayon + normales (direction unitaire) + UVs sphériques. + let mut normals = Vec::with_capacity(positions.len()); + let mut uvs = Vec::with_capacity(positions.len()); + for p in &positions { + let dir = p.normalize(); + normals.push(dir.to_array()); + uvs.push(spherical_uv(dir)); + } + let scaled: Vec<[f32; 3]> = positions.iter().map(|p| (*p * radius).to_array()).collect(); + + let mut indices = Vec::with_capacity(faces.len() * 3); + for [a, b, c] in &faces { + indices.extend_from_slice(&[*a as u16, *b as u16, *c as u16]); + } + Geometry::new(scaled) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +/// Crée (ou retrouve) le point milieu normalisé entre `a` et `b`, poussé sur la sphère unitaire. +fn subdiv_midpoint( + positions: &mut Vec, + cache: &mut HashMap<(u32, u32), u32>, + a: u32, + b: u32, +) -> u32 { + let key = if a < b { (a, b) } else { (b, a) }; + if let Some(&i) = cache.get(&key) { + return i; + } + let mid = (positions[a as usize] + positions[b as usize]).normalize(); + positions.push(mid); + let i = (positions.len() - 1) as u32; + cache.insert(key, i); + i +} + +/// UV sphérique à partir d'une direction unitaire, dans [0,1]². +fn spherical_uv(dir: Vec3) -> [f32; 2] { + let u = 0.5 + (dir.z.atan2(dir.x) / (2.0 * std::f32::consts::PI)); + let v = 0.5 - (dir.y.asin() / std::f32::consts::PI); + [u, v] +} + +/// Génére un cylindre de rayon `radius` et hauteur `height` (le long de Y, centré), avec `sectors` +/// segments. Parties : flanc (normales radiales lisses), couvercle supérieur (+Y), base inférieure +/// (-Y). UVs sur le flanc étirées [0,1]², anneaux concentriques fusionnés sur les caps. +pub fn cylinder(radius: f32, height: f32, sectors: u32) -> Geometry { + let si = sectors.max(3); + let h = height * 0.5; + let (mut positions, mut normals, mut uvs, mut indices) = ( + Vec::<[f32; 3]>::new(), + Vec::<[f32; 3]>::new(), + Vec::<[f32; 2]>::new(), + Vec::::new(), + ); + + // Flanc : colonnes radiales × 2 rangs (bas/haut). + let side_base = 0u16; + for row in 0..=1 { + let y = if row == 0 { -h } else { h }; + for s in 0..=si { + let u = s as f32 / si as f32; + let theta = u * 2.0 * std::f32::consts::PI; + let (sin_t, cos_t) = theta.sin_cos(); + let radial = Vec3::new(cos_t, 0.0, sin_t); + positions.push((radial * radius + Vec3::new(0.0, y, 0.0)).to_array()); + normals.push(radial.to_array()); + uvs.push([u, row as f32]); + } + } + for s in 0..si { + let a = side_base + s as u16; + let b = a + 1; + let c = side_base + (si as u16) + 1 + s as u16; + let d = c + 1; + indices.extend_from_slice(&[a, c, b, b, c, d]); + } + + // Caps : centre + anneau à chaque extrémité. + for (y, normal) in [(h, [0.0, 1.0, 0.0]), (-h, [0.0, -1.0, 0.0])] { + let center = positions.len() as u16; + positions.push([0.0, y, 0.0]); + normals.push(normal); + uvs.push([0.5, 0.5]); + let ring_start = positions.len() as u16; + for s in 0..=si { + let u = s as f32 / si as f32; + let theta = u * 2.0 * std::f32::consts::PI; + let (sin_t, cos_t) = theta.sin_cos(); + positions.push([radius * cos_t, y, radius * sin_t]); + normals.push(normal); + uvs.push([0.5 + 0.5 * cos_t, 0.5 + 0.5 * sin_t]); + } + for s in 0..si { + let a = ring_start + s as u16; + indices.extend_from_slice(&[center, a + 1, a]); + } + } + + Geometry::new(positions) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +/// Génére un cône de rayon `radius` et hauteur `height` (sommet en +h/2, base en -h/2), fermé par une +/// base, avec `sectors` segments. Normales latérales analytiques (inclinées vers l'extérieur) ; +/// normale de la base −Y. +pub fn cone(radius: f32, height: f32, sectors: u32) -> Geometry { + let si = sectors.max(3); + let h = height * 0.5; + let (mut positions, mut normals, mut uvs, mut indices) = ( + Vec::<[f32; 3]>::new(), + Vec::<[f32; 3]>::new(), + Vec::<[f32; 2]>::new(), + Vec::::new(), + ); + + // Éléments latéraux : sommet + anneau de base. + let apex = 0u16; + positions.push([0.0, h, 0.0]); + normals.push([0.0, 1.0, 0.0]); // sommet partagé ; normal proche +Y par défaut + uvs.push([0.5, 1.0]); + let base_start = 1u16; + for s in 0..=si { + let u = s as f32 / si as f32; + let theta = u * 2.0 * std::f32::consts::PI; + let (sin_t, cos_t) = theta.sin_cos(); + positions.push([radius * cos_t, -h, radius * sin_t]); + // Normale latérale : normalize(h·cosθ, r, h·sinθ). + let n = Vec3::new(h * cos_t, radius, h * sin_t).normalize(); + normals.push(n.to_array()); + uvs.push([u, 0.0]); + } + for s in 0..si { + indices.extend_from_slice(&[apex, base_start + s as u16 + 1, base_start + s as u16]); + } + + // Base fermée (cercle en -h/2, normale -Y). + let center = positions.len() as u16; + positions.push([0.0, -h, 0.0]); + normals.push([0.0, -1.0, 0.0]); + uvs.push([0.5, 0.5]); + let ring = positions.len() as u16; + for s in 0..=si { + let u = s as f32 / si as f32; + let theta = u * 2.0 * std::f32::consts::PI; + let (sin_t, cos_t) = theta.sin_cos(); + positions.push([radius * cos_t, -h, radius * sin_t]); + normals.push([0.0, -1.0, 0.0]); + uvs.push([0.5 + 0.5 * cos_t, 0.5 + 0.5 * sin_t]); + } + for s in 0..si { + let r = ring + s as u16; + indices.extend_from_slice(&[center, r, r + 1]); + } + + Geometry::new(positions) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +/// Génére un tore (anneau) de rayon majeur `major` (centre du tube) et rayon mineur `minor` +/// (rayon du tube), subdivisé en `major_segments` × `minor_segments`. Normales lisses (direction du +/// tube) ; UVs [0,1]² (couture le long du méridien et de l'équateur du tube). +pub fn torus(major: f32, minor: f32, major_segments: u32, minor_segments: u32) -> Geometry { + let mj = major_segments.max(3); + let mn = minor_segments.max(3); + let (mut positions, mut normals, mut uvs, mut indices) = ( + Vec::<[f32; 3]>::new(), + Vec::<[f32; 3]>::new(), + Vec::<[f32; 2]>::new(), + Vec::::new(), + ); + for i in 0..=mj { + let u = i as f32 / mj as f32; + let ua = u * 2.0 * std::f32::consts::PI; + let (sin_u, cos_u) = ua.sin_cos(); + for j in 0..=mn { + let v = j as f32 / mn as f32; + let va = v * 2.0 * std::f32::consts::PI; + let (sin_v, cos_v) = va.sin_cos(); + let ring = Vec3::new( + (major + minor * cos_v) * cos_u, + minor * sin_v, + (major + minor * cos_v) * sin_u, + ); + positions.push(ring.to_array()); + let n = Vec3::new(cos_v * cos_u, sin_v, cos_v * sin_u).normalize(); + normals.push(n.to_array()); + uvs.push([u, v]); + } + } + for i in 0..mj { + for j in 0..mn { + let a = i * (mn + 1) + j; + let b = a + 1; + let c = a + mn + 1; + let d = c + 1; + indices + .extend_from_slice(&[a as u16, c as u16, b as u16, b as u16, c as u16, d as u16]); + } + } + Geometry::new(positions) + .with_normals(normals) + .with_uvs(uvs) + .with_indices(indices) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_valid(geo: &Geometry) { + geo.validate().expect("generated geometry must validate"); + let positions = &geo.positions; + let normals = geo.normals.as_ref().expect("normals present"); + let uvs = geo.uvs.as_ref().expect("uvs present"); + let indices = geo.indices.as_ref().expect("indices present"); + assert_eq!(normals.len(), positions.len(), "normals/positions count"); + assert_eq!(uvs.len(), positions.len(), "uvs/positions count"); + for n in normals { + let len = Vec3::from_array(*n).length(); + assert!((len - 1.0).abs() < 1e-3, "unit normal, got {len}"); + } + for &i in indices { + assert!((i as usize) < positions.len(), "index {i} in bounds"); + } + } + + #[test] + fn cube_counts() { + let g = cube(1.0); + assert_eq!(g.positions.len(), 24); + assert_eq!(g.indices.as_ref().unwrap().len(), 36); + assert_valid(&g); + let g2 = cube(2.0); + assert_eq!( + g2.positions, + g.positions + .iter() + .map(|p| [p[0] * 2.0, p[1] * 2.0, p[2] * 2.0]) + .collect::>() + ); + } + + #[test] + fn plane_counts() { + let g = plane(2.0, 3.0, 1, 1); + assert_eq!(g.positions.len(), 4); + assert_eq!(g.indices.as_ref().unwrap().len(), 6); + assert_valid(&g); + assert!(g.positions.iter().all(|p| p[1] == 0.0)); + let g2 = plane(2.0, 3.0, 4, 5); + assert_eq!(g2.positions.len(), (4 + 1) * (5 + 1)); + assert_valid(&g2); + } + + #[test] + fn uv_sphere_counts_and_normals() { + let g = uv_sphere(1.0, 12, 8); + assert_eq!(g.positions.len(), (12 + 1) * (8 + 1)); + assert_valid(&g); + // Normales pointent vers l'extérieur (position/rayon). + for (p, n) in g.positions.iter().zip(g.normals.as_ref().unwrap()) { + let diff = (Vec3::from_array(*p) / 1.0 - Vec3::from_array(*n)).length(); + assert!(diff < 1e-4, "normal ~ position/radius, got diff {diff}"); + } + } + + #[test] + fn icosphere_grows_with_subdivision() { + let base = icosphere(1.0, 0); + assert_eq!(base.positions.len(), 12); + assert_eq!(base.indices.as_ref().unwrap().len(), 60); + assert_valid(&base); + let once = icosphere(1.0, 1); + assert!(once.positions.len() > base.positions.len()); + assert_valid(&once); + for (p, n) in once.positions.iter().zip(once.normals.as_ref().unwrap()) { + let r = Vec3::from_array(*p).length(); + assert!((r - 1.0).abs() < 1e-3, "on sphere radius, got {r}"); + let diff = (Vec3::from_array(*p).normalize() - Vec3::from_array(*n)).length(); + assert!(diff < 1e-4, "normal ~ direction, got {diff}"); + } + } + + #[test] + fn cylinder_and_cone_validate() { + assert_valid(&cylinder(0.5, 1.0, 16)); + assert_valid(&cone(0.5, 1.0, 16)); + let c = cylinder(0.5, 1.0, 8); + assert!(c.positions.iter().all(|p| p[1].abs() <= 0.5 + 1e-5)); + } + + #[test] + fn torus_validate() { + let g = torus(1.0, 0.25, 24, 12); + assert_valid(&g); + assert_eq!(g.positions.len(), (24 + 1) * (12 + 1)); + } +}