ranim_items/mesh/
mod.rs

1//! Mesh-based items (Surface, Sphere, etc.)
2
3use ranim_core::{
4    Extract,
5    anchor::Aabb,
6    color::{AlphaColor, Srgb},
7    components::{PointVec, rgba::Rgba},
8    core_item::CoreItem,
9    glam::{DVec3, Mat4, Vec3},
10    traits::{
11        Alignable, Empty, FillColor, Interpolatable, Opacity, RotateTransform, ScaleTransform,
12        ShiftTransform,
13    },
14};
15
16mod sphere;
17mod surface;
18
19pub use sphere::*;
20pub use surface::*;
21
22/// A high-level mesh item with per-vertex data wrapped in PointVec for animation support.
23///
24/// This struct uses [`PointVec`] to wrap vertex data, enabling proper alignment
25/// and interpolation for animations. When extracted, it converts to the low-level
26/// [`ranim_core::core_item::mesh_item::MeshItem`] for rendering.
27#[derive(Debug, Clone, PartialEq)]
28pub struct MeshItem {
29    /// The vertices of the mesh
30    pub points: PointVec<Vec3>,
31    /// The triangle indices
32    pub triangle_indices: Vec<u32>,
33    /// The transform matrix
34    pub transform: Mat4,
35    /// Per-vertex colors
36    pub vertex_colors: PointVec<Rgba>,
37    /// Per-vertex normals for smooth shading.
38    /// All-zero (or empty) → shader falls back to flat shading via `dpdx`/`dpdy`.
39    pub vertex_normals: PointVec<Vec3>,
40}
41
42impl MeshItem {
43    /// Create a MeshItem from vertices only (no indices, suitable for point clouds).
44    pub fn from_vertices(points: Vec<Vec3>) -> Self {
45        let len = points.len();
46        Self {
47            points: points.into(),
48            triangle_indices: Vec::new(),
49            transform: Mat4::IDENTITY,
50            vertex_colors: vec![Rgba::default(); len].into(),
51            vertex_normals: vec![Vec3::ZERO; len].into(),
52        }
53    }
54
55    /// Create a MeshItem from vertices and triangle indices.
56    pub fn from_indexed_vertices(points: Vec<Vec3>, triangle_indices: Vec<u32>) -> Self {
57        let len = points.len();
58        Self {
59            points: points.into(),
60            triangle_indices,
61            transform: Mat4::IDENTITY,
62            vertex_colors: vec![Rgba::default(); len].into(),
63            vertex_normals: vec![Vec3::ZERO; len].into(),
64        }
65    }
66
67    /// Set the transform matrix.
68    pub fn with_transform(mut self, transform: Mat4) -> Self {
69        self.transform = transform;
70        self
71    }
72
73    /// Set all vertex colors to the same value.
74    pub fn with_color(mut self, color: AlphaColor<Srgb>) -> Self {
75        let rgba: Rgba = color.into();
76        self.vertex_colors = vec![rgba; self.points.len()].into();
77        self
78    }
79}
80
81impl From<MeshItem> for ranim_core::core_item::mesh_item::MeshItem {
82    fn from(value: MeshItem) -> Self {
83        Self {
84            points: value.points.iter().copied().collect(),
85            triangle_indices: value.triangle_indices,
86            transform: value.transform,
87            vertex_colors: value.vertex_colors.iter().copied().collect(),
88            vertex_normals: value.vertex_normals.iter().copied().collect(),
89        }
90    }
91}
92
93impl Extract for MeshItem {
94    type Target = CoreItem;
95    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
96        buf.push(CoreItem::MeshItem(self.clone().into()));
97    }
98}
99
100impl Alignable for MeshItem {
101    fn is_aligned(&self, other: &Self) -> bool {
102        self.points.is_aligned(&other.points)
103            && self.vertex_colors.is_aligned(&other.vertex_colors)
104            && self.vertex_normals.is_aligned(&other.vertex_normals)
105    }
106
107    fn align_with(&mut self, other: &mut Self) {
108        self.points.align_with(&mut other.points);
109        self.vertex_colors.align_with(&mut other.vertex_colors);
110        self.vertex_normals.align_with(&mut other.vertex_normals);
111    }
112}
113
114impl Interpolatable for MeshItem {
115    fn lerp(&self, target: &Self, t: f64) -> Self {
116        Self {
117            points: self.points.lerp(&target.points, t),
118            triangle_indices: if t < 0.5 {
119                self.triangle_indices.clone()
120            } else {
121                target.triangle_indices.clone()
122            },
123            transform: self.transform.lerp(&target.transform, t),
124            vertex_colors: self.vertex_colors.lerp(&target.vertex_colors, t),
125            vertex_normals: self.vertex_normals.lerp(&target.vertex_normals, t),
126        }
127    }
128}
129
130impl FillColor for MeshItem {
131    fn fill_color(&self) -> AlphaColor<Srgb> {
132        let Rgba(rgba) = self.vertex_colors.first().cloned().unwrap_or_default();
133        AlphaColor::new([rgba.x, rgba.y, rgba.z, rgba.w])
134    }
135
136    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
137        let rgba: Rgba = color.into();
138        self.vertex_colors.iter_mut().for_each(|c| *c = rgba);
139        self
140    }
141
142    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
143        self.vertex_colors.set_opacity(opacity);
144        self
145    }
146}
147
148impl Opacity for MeshItem {
149    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
150        self.vertex_colors.set_opacity(opacity);
151        self
152    }
153}
154
155impl Aabb for MeshItem {
156    fn aabb(&self) -> [DVec3; 2] {
157        if self.points.is_empty() {
158            return [DVec3::ZERO, DVec3::ZERO];
159        }
160
161        // Convert transform to DMat4 for calculations
162        let transform = self.transform.as_dmat4();
163
164        // TODO: do some optimize and caching
165        // Transform all points and compute bounds
166        let transformed_points: Vec<DVec3> = self
167            .points
168            .iter()
169            .map(|&p| transform.transform_point3(p.as_dvec3()))
170            .collect();
171
172        let mut min = transformed_points[0];
173        let mut max = transformed_points[0];
174
175        for &p in &transformed_points[1..] {
176            min = min.min(p);
177            max = max.max(p);
178        }
179
180        [min, max]
181    }
182}
183
184impl ShiftTransform for MeshItem {
185    fn shift(&mut self, offset: DVec3) -> &mut Self {
186        // Apply shift by modifying the transform matrix
187        let translation = Mat4::from_translation(offset.as_vec3());
188        self.transform = translation * self.transform;
189        self
190    }
191}
192
193impl RotateTransform for MeshItem {
194    fn rotate_on_axis(&mut self, axis: DVec3, angle: f64) -> &mut Self {
195        // Apply rotation by modifying the transform matrix
196        let rotation = Mat4::from_axis_angle(axis.as_vec3().normalize(), angle as f32);
197        self.transform = rotation * self.transform;
198        self
199    }
200}
201
202impl ScaleTransform for MeshItem {
203    fn scale(&mut self, scale: DVec3) -> &mut Self {
204        // Apply scale by modifying the transform matrix
205        let scale_mat = Mat4::from_scale(scale.as_vec3());
206        self.transform = scale_mat * self.transform;
207        self
208    }
209}
210
211impl Empty for MeshItem {
212    fn empty() -> Self {
213        Self {
214            points: Vec::new().into(),
215            triangle_indices: Vec::new(),
216            transform: Mat4::IDENTITY,
217            vertex_colors: Vec::new().into(),
218            vertex_normals: Vec::new().into(),
219        }
220    }
221}
222
223/// Compute smooth vertex normals from a triangle mesh.
224///
225/// Each face normal is weighted by the angle at the vertex before accumulation.
226/// The result is normalized per vertex. Degenerate triangles are skipped.
227pub fn compute_smooth_normals(points: &[DVec3], triangle_indices: &[u32]) -> Vec<DVec3> {
228    let mut normals = vec![DVec3::ZERO; points.len()];
229
230    for tri in triangle_indices.chunks_exact(3) {
231        let (i0, i1, i2) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
232        let (p0, p1, p2) = (points[i0], points[i1], points[i2]);
233
234        let e01 = p1 - p0;
235        let e02 = p2 - p0;
236        let face_normal = e01.cross(e02);
237
238        // Skip degenerate triangles
239        if face_normal.length_squared() < 1e-20 {
240            continue;
241        }
242
243        // Weight by angle at each vertex
244        let e10 = p0 - p1;
245        let e12 = p2 - p1;
246        let e20 = p0 - p2;
247        let e21 = p1 - p2;
248
249        let angle0 = angle_between(e01, e02);
250        let angle1 = angle_between(e10, e12);
251        let angle2 = angle_between(e20, e21);
252
253        normals[i0] += face_normal * angle0;
254        normals[i1] += face_normal * angle1;
255        normals[i2] += face_normal * angle2;
256    }
257
258    for n in &mut normals {
259        let len = n.length();
260        if len > 1e-10 {
261            *n /= len;
262        }
263    }
264
265    normals
266}
267
268/// Angle (in radians) between two vectors.
269fn angle_between(a: DVec3, b: DVec3) -> f64 {
270    let denom = a.length() * b.length();
271    if denom < 1e-20 {
272        return 0.0;
273    }
274    (a.dot(b) / denom).clamp(-1.0, 1.0).acos()
275}
276
277/// Generate triangle indices for a `nu × nv` grid of vertices (row-major layout).
278///
279/// Each quad `[i, j]` → 2 triangles: `[tl, bl, tr]` and `[tr, bl, br]`
280/// where `tl = i*nv + j`, `tr = i*nv + j+1`, `bl = (i+1)*nv + j`, `br = (i+1)*nv + j+1`.
281///
282/// Total index count = `6 * (nu - 1) * (nv - 1)`.
283pub fn generate_grid_indices(nu: u32, nv: u32) -> Vec<u32> {
284    let mut indices = Vec::with_capacity(6 * (nu as usize - 1) * (nv as usize - 1));
285    for i in 0..nu - 1 {
286        for j in 0..nv - 1 {
287            let tl = i * nv + j;
288            let tr = i * nv + j + 1;
289            let bl = (i + 1) * nv + j;
290            let br = (i + 1) * nv + j + 1;
291            // Triangle 1: tl, bl, tr
292            indices.push(tl);
293            indices.push(bl);
294            indices.push(tr);
295            // Triangle 2: tr, bl, br
296            indices.push(tr);
297            indices.push(bl);
298            indices.push(br);
299        }
300    }
301    indices
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use ranim_core::{
308        anchor::Aabb,
309        color::palette::css,
310        glam::{Mat4, Vec3},
311        traits::{Alignable, Empty, RotateTransform, ScaleTransform, ShiftTransform},
312    };
313
314    #[test]
315    fn test_generate_grid_indices_2x2() {
316        // 2×2 grid → 1 quad → 2 triangles → 6 indices
317        let indices = generate_grid_indices(2, 2);
318        assert_eq!(indices.len(), 6);
319        // Vertices: 0=tl, 1=tr, 2=bl, 3=br
320        assert_eq!(indices, vec![0, 2, 1, 1, 2, 3]);
321    }
322
323    #[test]
324    fn test_generate_grid_indices_3x3() {
325        // 3×3 grid → 4 quads → 8 triangles → 24 indices
326        let indices = generate_grid_indices(3, 3);
327        assert_eq!(indices.len(), 24);
328    }
329
330    #[test]
331    fn test_generate_grid_indices_count() {
332        let nu = 10;
333        let nv = 5;
334        let indices = generate_grid_indices(nu, nv);
335        assert_eq!(indices.len(), 6 * (nu as usize - 1) * (nv as usize - 1));
336    }
337
338    #[test]
339    fn test_mesh_item_alignable() {
340        let mut mesh1 = MeshItem::from_indexed_vertices(
341            vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)],
342            vec![0, 1, 2],
343        );
344
345        let mut mesh2 = MeshItem::from_indexed_vertices(
346            vec![
347                Vec3::new(0.0, 0.0, 0.0),
348                Vec3::new(1.0, 0.0, 0.0),
349                Vec3::new(0.0, 1.0, 0.0),
350                Vec3::new(1.0, 1.0, 0.0),
351            ],
352            vec![0, 1, 2, 1, 3, 2],
353        );
354
355        // Initially not aligned
356        assert!(!mesh1.is_aligned(&mesh2));
357
358        // Align them
359        mesh1.align_with(&mut mesh2);
360
361        // Now they should be aligned
362        assert!(mesh1.is_aligned(&mesh2));
363
364        // All vertex arrays should have same length
365        assert_eq!(mesh1.points.len(), 4);
366        assert_eq!(mesh2.points.len(), 4);
367        assert_eq!(mesh1.vertex_colors.len(), 4);
368        assert_eq!(mesh2.vertex_colors.len(), 4);
369        assert_eq!(mesh1.vertex_normals.len(), 4);
370        assert_eq!(mesh2.vertex_normals.len(), 4);
371
372        // mesh1's new points should be last point repeated (from PointVec::align_with)
373        assert_eq!(mesh1.points[2], Vec3::new(1.0, 0.0, 0.0));
374        assert_eq!(mesh1.points[3], Vec3::new(1.0, 0.0, 0.0));
375
376        // mesh2's points should remain unchanged (it was already longer)
377        assert_eq!(mesh2.points[0], Vec3::new(0.0, 0.0, 0.0));
378        assert_eq!(mesh2.points[3], Vec3::new(1.0, 1.0, 0.0));
379    }
380
381    #[test]
382    fn test_mesh_item_interpolate() {
383        use ranim_core::traits::Interpolatable;
384
385        let mut mesh1 = MeshItem::from_indexed_vertices(
386            vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)],
387            vec![0, 1, 2],
388        )
389        .with_color(css::RED.with_alpha(1.0));
390
391        let mut mesh2 = MeshItem::from_indexed_vertices(
392            vec![Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0)],
393            vec![0, 1, 3],
394        )
395        .with_color(css::GREEN.with_alpha(1.0))
396        .with_transform(Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0)));
397
398        // Align first
399        mesh1.align_with(&mut mesh2);
400
401        // Interpolate at t = 0.5
402        let interpolated = mesh1.lerp(&mesh2, 0.5);
403
404        // Points should be halfway between
405        assert_eq!(interpolated.points[0], Vec3::new(1.0, 0.0, 0.0));
406        assert_eq!(interpolated.points[1], Vec3::new(2.0, 0.0, 0.0));
407
408        // triangle_indices should be from mesh2 (since t >= 0.5)
409        assert_eq!(interpolated.triangle_indices, vec![0, 1, 3]);
410
411        // Transform should be interpolated
412        assert_eq!(
413            interpolated.transform,
414            Mat4::from_translation(Vec3::new(0.5, 0.0, 0.0))
415        );
416    }
417
418    #[test]
419    fn test_mesh_item_aabb() {
420        use ranim_core::glam::dvec3;
421
422        let mesh = MeshItem::from_indexed_vertices(
423            vec![
424                Vec3::new(-1.0, -1.0, -1.0),
425                Vec3::new(1.0, -1.0, -1.0),
426                Vec3::new(1.0, 1.0, -1.0),
427                Vec3::new(-1.0, 1.0, 1.0),
428            ],
429            vec![0, 1, 2],
430        );
431
432        let [min, max] = mesh.aabb();
433        assert_eq!(min, dvec3(-1.0, -1.0, -1.0));
434        assert_eq!(max, dvec3(1.0, 1.0, 1.0));
435    }
436
437    #[test]
438    fn test_mesh_item_shift() {
439        use ranim_core::glam::dvec3;
440
441        let mut mesh = MeshItem::from_indexed_vertices(
442            vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)],
443            vec![0, 1],
444        );
445
446        mesh.shift(dvec3(1.0, 2.0, 3.0));
447
448        // Check AABB after shift
449        let [min, _max] = mesh.aabb();
450        assert!((min.x - 1.0).abs() < 1e-5);
451        assert!((min.y - 2.0).abs() < 1e-5);
452        assert!((min.z - 3.0).abs() < 1e-5);
453    }
454
455    #[test]
456    fn test_mesh_item_scale() {
457        use ranim_core::glam::dvec3;
458
459        let mut mesh = MeshItem::from_indexed_vertices(
460            vec![Vec3::new(1.0, 1.0, 1.0), Vec3::new(2.0, 2.0, 2.0)],
461            vec![0, 1],
462        );
463
464        mesh.scale(dvec3(2.0, 2.0, 2.0));
465
466        // Check AABB after scale
467        let [min, max] = mesh.aabb();
468        assert!((min.x - 2.0).abs() < 1e-5);
469        assert!((max.x - 4.0).abs() < 1e-5);
470    }
471
472    #[test]
473    fn test_mesh_item_rotate() {
474        use ranim_core::glam::dvec3;
475        use std::f64::consts::PI;
476
477        let mut mesh = MeshItem::from_indexed_vertices(vec![Vec3::new(1.0, 0.0, 0.0)], vec![]);
478
479        // Rotate 90 degrees around Z axis
480        mesh.rotate_on_axis(dvec3(0.0, 0.0, 1.0), PI / 2.0);
481
482        let [min, _max] = mesh.aabb();
483        // After rotation, x should be ~0, y should be ~1
484        assert!(min.x.abs() < 1e-5);
485        assert!((min.y - 1.0).abs() < 1e-5);
486    }
487
488    #[test]
489    fn test_mesh_item_empty() {
490        let mesh = MeshItem::empty();
491        assert_eq!(mesh.points.len(), 0);
492        assert_eq!(mesh.triangle_indices.len(), 0);
493        assert_eq!(mesh.vertex_colors.len(), 0);
494        assert_eq!(mesh.vertex_normals.len(), 0);
495    }
496}