Skip to main content

mittens_engine/engine/graphics/
mesh.rs

1//! CPU-side procedural mesh generation.
2//!
3//! These meshes are intended as authoring / staging data.
4//! The renderer later uploads them into GPU buffers (vertex/index buffers)
5//! and returns a `MeshHandle` that can be referenced by ECS renderables.
6
7use vulkano::buffer::BufferContents;
8use vulkano::pipeline::graphics::vertex_input::Vertex;
9
10fn filled_polygon_2d(boundary: &[[f32; 2]]) -> CpuMesh {
11    let mut min_x = f32::INFINITY;
12    let mut max_x = f32::NEG_INFINITY;
13    let mut min_y = f32::INFINITY;
14    let mut max_y = f32::NEG_INFINITY;
15    for [x, y] in boundary {
16        min_x = min_x.min(*x);
17        max_x = max_x.max(*x);
18        min_y = min_y.min(*y);
19        max_y = max_y.max(*y);
20    }
21    let width = (max_x - min_x).max(1.0e-6);
22    let height = (max_y - min_y).max(1.0e-6);
23
24    let mut vertices = Vec::with_capacity(boundary.len() + 1);
25    vertices.push(CpuVertex {
26        pos: [0.0, 0.0, 0.0],
27        uv: [
28            ((0.0 - min_x) / width).clamp(0.0, 1.0),
29            (1.0 - (0.0 - min_y) / height).clamp(0.0, 1.0),
30        ],
31        normal: [0.0, 0.0, 1.0],
32    });
33    for [x, y] in boundary {
34        vertices.push(CpuVertex {
35            pos: [*x, *y, 0.0],
36            uv: [(*x - min_x) / width, 1.0 - (*y - min_y) / height],
37            normal: [0.0, 0.0, 1.0],
38        });
39    }
40
41    let mut indices = Vec::with_capacity(boundary.len() * 3);
42    for i in 0..boundary.len() {
43        let next = (i + 1) % boundary.len();
44        indices.extend_from_slice(&[0, (i + 1) as u32, (next + 1) as u32]);
45    }
46    CpuMesh::new(vertices, indices)
47}
48
49fn signed_area_2d(points: &[[f32; 2]]) -> f32 {
50    let mut area = 0.0;
51    for i in 0..points.len() {
52        let [x0, y0] = points[i];
53        let [x1, y1] = points[(i + 1) % points.len()];
54        area += x0 * y1 - x1 * y0;
55    }
56    area * 0.5
57}
58
59fn add2(a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
60    [a[0] + b[0], a[1] + b[1]]
61}
62
63fn sub2(a: [f32; 2], b: [f32; 2]) -> [f32; 2] {
64    [a[0] - b[0], a[1] - b[1]]
65}
66
67fn mul2(v: [f32; 2], s: f32) -> [f32; 2] {
68    [v[0] * s, v[1] * s]
69}
70
71fn len2(v: [f32; 2]) -> f32 {
72    (v[0] * v[0] + v[1] * v[1]).sqrt()
73}
74
75fn normalize2(v: [f32; 2]) -> [f32; 2] {
76    let len = len2(v).max(1.0e-6);
77    [v[0] / len, v[1] / len]
78}
79
80fn quadratic_bezier2(a: [f32; 2], b: [f32; 2], c: [f32; 2], t: f32) -> [f32; 2] {
81    let u = 1.0 - t;
82    add2(add2(mul2(a, u * u), mul2(b, 2.0 * u * t)), mul2(c, t * t))
83}
84
85fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
86    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
87}
88
89fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
90    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
91}
92
93fn mul3(v: [f32; 3], s: f32) -> [f32; 3] {
94    [v[0] * s, v[1] * s, v[2] * s]
95}
96
97fn len3(v: [f32; 3]) -> f32 {
98    (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
99}
100
101fn normalize3(v: [f32; 3]) -> [f32; 3] {
102    let len = len3(v).max(1.0e-6);
103    [v[0] / len, v[1] / len, v[2] / len]
104}
105
106fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
107    [
108        a[1] * b[2] - a[2] * b[1],
109        a[2] * b[0] - a[0] * b[2],
110        a[0] * b[1] - a[1] * b[0],
111    ]
112}
113
114fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
115    [
116        a[0] + (b[0] - a[0]) * t,
117        a[1] + (b[1] - a[1]) * t,
118        a[2] + (b[2] - a[2]) * t,
119    ]
120}
121
122fn append_rounded_corner_points(
123    boundary: &mut Vec<[f32; 2]>,
124    prev: [f32; 2],
125    curr: [f32; 2],
126    next: [f32; 2],
127    bevel_segments: u32,
128    trim_fraction: f32,
129) {
130    if bevel_segments == 0 {
131        boundary.push(curr);
132        return;
133    }
134
135    let to_prev = sub2(prev, curr);
136    let to_next = sub2(next, curr);
137    let trim = len2(to_prev).min(len2(to_next)) * trim_fraction.clamp(0.0, 0.49);
138    let start = add2(curr, mul2(normalize2(to_prev), trim));
139    let end = add2(curr, mul2(normalize2(to_next), trim));
140
141    for i in 0..=bevel_segments {
142        let t = i as f32 / bevel_segments as f32;
143        boundary.push(quadratic_bezier2(start, curr, end, t));
144    }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum PrimitiveTopology {
149    TriangleList,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum IndexFormat {
154    U16,
155    U32,
156}
157
158/// A minimal CPU vertex format for bring-up.
159///
160/// - `pos`: object-space / model-space position
161/// - `normal`: object-space normal (for lighting)
162/// - `uv`: optional 0..1 UV (useful for screen-space gradients)
163#[derive(BufferContents, Vertex, Debug, Clone, Copy, Default)]
164#[repr(C)]
165pub struct CpuVertex {
166    #[format(R32G32B32_SFLOAT)]
167    pub pos: [f32; 3],
168    #[format(R32G32_SFLOAT)]
169    pub uv: [f32; 2],
170    #[format(R32G32B32_SFLOAT)]
171    pub normal: [f32; 3],
172}
173
174/// CPU-side mesh data.
175///
176/// Contract:
177/// - `vertices` + `indices` fully define geometry.
178/// - `primitive_topology` is how indices are interpreted.
179/// - Upload step will pack `vertices` as tightly as possible into a GPU vertex buffer,
180///   and `indices` into a GPU index buffer.
181#[derive(Debug, Clone)]
182pub struct CpuMesh {
183    pub vertices: Vec<CpuVertex>,
184    pub indices_u32: Vec<u32>,
185
186    /// Optional skinning data (glTF: `JOINTS_0` / `WEIGHTS_0`).
187    ///
188    /// Contract (when present):
189    /// - `len == vertices.len()`
190    /// - `joints0[i]` corresponds to `weights0[i]`
191    pub joints0: Option<Vec<[u16; 4]>>,
192    pub weights0: Option<Vec<[f32; 4]>>,
193    pub primitive_topology: PrimitiveTopology,
194    pub index_format: IndexFormat,
195}
196
197impl CpuMesh {
198    pub fn new(vertices: Vec<CpuVertex>, indices_u32: Vec<u32>) -> Self {
199        Self {
200            vertices,
201            indices_u32,
202            joints0: None,
203            weights0: None,
204            primitive_topology: PrimitiveTopology::TriangleList,
205            index_format: IndexFormat::U32,
206        }
207    }
208
209    pub fn with_skinning(mut self, joints0: Vec<[u16; 4]>, weights0: Vec<[f32; 4]>) -> Self {
210        debug_assert_eq!(joints0.len(), self.vertices.len());
211        debug_assert_eq!(weights0.len(), self.vertices.len());
212        debug_assert_eq!(joints0.len(), weights0.len());
213        self.joints0 = Some(joints0);
214        self.weights0 = Some(weights0);
215        self
216    }
217
218    pub fn index_count(&self) -> u32 {
219        self.indices_u32.len() as u32
220    }
221
222    pub fn vertex_count(&self) -> u32 {
223        self.vertices.len() as u32
224    }
225
226    pub fn approximate_heap_bytes(&self) -> usize {
227        let mut bytes = self.vertices.capacity() * std::mem::size_of::<CpuVertex>();
228        bytes += self.indices_u32.capacity() * std::mem::size_of::<u32>();
229        if let Some(joints0) = &self.joints0 {
230            bytes += joints0.capacity() * std::mem::size_of::<[u16; 4]>();
231        }
232        if let Some(weights0) = &self.weights0 {
233            bytes += weights0.capacity() * std::mem::size_of::<[f32; 4]>();
234        }
235        bytes
236    }
237}
238
239/// Procedural mesh constructors.
240///
241/// Notes:
242/// - The shapes here are intentionally simple and low-poly.
243/// - Winding order:
244///   We return *counter-clockwise* triangles in object space for "front faces".
245pub struct MeshFactory;
246
247impl MeshFactory {
248    /// 2D equilateral triangle centered at origin.
249    pub fn triangle_2d() -> CpuMesh {
250        // Equilateral triangle of side length 1.0.
251        // Height h = sqrt(3)/2. Centered at origin using:
252        //  - top:    (0,  2h/3)
253        //  - bottom: (±0.5, -h/3)
254        let h = 0.866_025_4_f32;
255        let y_top = 2.0 * h / 3.0;
256        let y_bottom = -h / 3.0;
257        let y_span = y_top - y_bottom;
258
259        let vertices = vec![
260            CpuVertex {
261                pos: [-0.5, y_bottom, 0.0],
262                // For 2D primitives, we treat UV as normalized XY over the primitive's bounds.
263                uv: [0.0, 0.0],
264                normal: [0.0, 0.0, 1.0],
265            },
266            CpuVertex {
267                pos: [0.5, y_bottom, 0.0],
268                uv: [1.0, 0.0],
269                normal: [0.0, 0.0, 1.0],
270            },
271            CpuVertex {
272                pos: [0.0, y_top, 0.0],
273                uv: [0.5, (y_top - y_bottom) / y_span],
274                normal: [0.0, 0.0, 1.0],
275            },
276        ];
277
278        CpuMesh::new(vertices, vec![0, 1, 2])
279    }
280
281    /// 2D quad (square) as two triangles.
282    pub fn quad_2d() -> CpuMesh {
283        let vertices = vec![
284            CpuVertex {
285                pos: [-0.5, -0.5, 0.0],
286                // Texture convention: v=0 is TOP, v=1 is BOTTOM.
287                uv: [0.0, 1.0],
288                normal: [0.0, 0.0, 1.0],
289            },
290            CpuVertex {
291                pos: [0.5, -0.5, 0.0],
292                uv: [1.0, 1.0],
293                normal: [0.0, 0.0, 1.0],
294            },
295            CpuVertex {
296                pos: [0.5, 0.5, 0.0],
297                uv: [1.0, 0.0],
298                normal: [0.0, 0.0, 1.0],
299            },
300            CpuVertex {
301                pos: [-0.5, 0.5, 0.0],
302                uv: [0.0, 0.0],
303                normal: [0.0, 0.0, 1.0],
304            },
305        ];
306
307        // two triangles: (0,1,2) + (0,2,3)
308        CpuMesh::new(vertices, vec![0, 1, 2, 0, 2, 3])
309    }
310
311    /// Unit-ish cube centered at origin.
312    ///
313    /// This is a cube with per-face vertices (24 vertices, 12 triangles) so normals are flat.
314    pub fn cube() -> CpuMesh {
315        let p = 0.5_f32;
316
317        // 4 verts per face. UVs are placeholder; cube texturing isn't a priority yet.
318        let mut vertices: Vec<CpuVertex> = Vec::with_capacity(24);
319        let mut indices: Vec<u32> = Vec::with_capacity(36);
320
321        let mut push_face = |n: [f32; 3], a: [f32; 3], b: [f32; 3], c: [f32; 3], d: [f32; 3]| {
322            let base = vertices.len() as u32;
323            vertices.push(CpuVertex {
324                pos: a,
325                uv: [0.0, 0.0],
326                normal: n,
327            });
328            vertices.push(CpuVertex {
329                pos: b,
330                uv: [1.0, 0.0],
331                normal: n,
332            });
333            vertices.push(CpuVertex {
334                pos: c,
335                uv: [1.0, 1.0],
336                normal: n,
337            });
338            vertices.push(CpuVertex {
339                pos: d,
340                uv: [0.0, 1.0],
341                normal: n,
342            });
343            // CCW triangles as seen from outside.
344            indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
345        };
346
347        // -Z
348        push_face(
349            [0.0, 0.0, -1.0],
350            [-p, -p, -p],
351            [p, -p, -p],
352            [p, p, -p],
353            [-p, p, -p],
354        );
355        // +Z
356        push_face(
357            [0.0, 0.0, 1.0],
358            [-p, -p, p],
359            [p, -p, p],
360            [p, p, p],
361            [-p, p, p],
362        );
363        // -X
364        push_face(
365            [-1.0, 0.0, 0.0],
366            [-p, -p, -p],
367            [-p, -p, p],
368            [-p, p, p],
369            [-p, p, -p],
370        );
371        // +X
372        push_face(
373            [1.0, 0.0, 0.0],
374            [p, -p, -p],
375            [p, p, -p],
376            [p, p, p],
377            [p, -p, p],
378        );
379        // -Y
380        push_face(
381            [0.0, -1.0, 0.0],
382            [-p, -p, -p],
383            [p, -p, -p],
384            [p, -p, p],
385            [-p, -p, p],
386        );
387        // +Y
388        push_face(
389            [0.0, 1.0, 0.0],
390            [-p, p, -p],
391            [-p, p, p],
392            [p, p, p],
393            [p, p, -p],
394        );
395
396        CpuMesh::new(vertices, indices)
397    }
398
399    /// Unit wireframe box centered at the origin, represented by twelve solid edge prisms.
400    ///
401    /// `thickness` is relative to the unit box dimensions and is clamped to `(0, 1]`. Using
402    /// triangles instead of line primitives keeps the geometry compatible with the normal mesh
403    /// rendering path and gives the edges visible thickness from every view direction.
404    pub fn wireframe_box(thickness: f32) -> CpuMesh {
405        let thickness = thickness.clamp(1.0e-4, 1.0);
406        let edge_center = 0.5 - thickness * 0.5;
407        let cube = Self::cube();
408        let mut vertices = Vec::with_capacity(cube.vertices.len() * 12);
409        let mut indices = Vec::with_capacity(cube.indices_u32.len() * 12);
410
411        let mut append_prism = |center: [f32; 3], size: [f32; 3]| {
412            let base = vertices.len() as u32;
413            vertices.extend(cube.vertices.iter().map(|vertex| CpuVertex {
414                pos: [
415                    center[0] + vertex.pos[0] * size[0],
416                    center[1] + vertex.pos[1] * size[1],
417                    center[2] + vertex.pos[2] * size[2],
418                ],
419                uv: vertex.uv,
420                normal: vertex.normal,
421            }));
422            indices.extend(cube.indices_u32.iter().map(|index| base + index));
423        };
424
425        for a in [-edge_center, edge_center] {
426            for b in [-edge_center, edge_center] {
427                append_prism([0.0, a, b], [1.0, thickness, thickness]);
428                append_prism([a, 0.0, b], [thickness, 1.0, thickness]);
429                append_prism([a, b, 0.0], [thickness, thickness, 1.0]);
430            }
431        }
432
433        CpuMesh::new(vertices, indices)
434    }
435
436    /// Simple tetrahedron (4 vertices, 4 faces).
437    pub fn tetrahedron() -> CpuMesh {
438        // A regular tetrahedron-ish set of points.
439        // (Not perfectly regular, but stable and centered-ish.)
440        let vertices = vec![
441            CpuVertex {
442                pos: [0.0, 0.0, 0.6123724],
443                uv: [0.5, 1.0],
444                normal: [0.0, 0.0, 1.0],
445            },
446            CpuVertex {
447                pos: [-0.5, -0.2886751, -0.2041241],
448                uv: [0.0, 0.0],
449                normal: [-1.0, -1.0, -1.0],
450            },
451            CpuVertex {
452                pos: [0.5, -0.2886751, -0.2041241],
453                uv: [1.0, 0.0],
454                normal: [1.0, -1.0, -1.0],
455            },
456            CpuVertex {
457                pos: [0.0, 0.5773503, -0.2041241],
458                uv: [0.5, 0.5],
459                normal: [0.0, 1.0, -1.0],
460            },
461        ];
462
463        // 4 faces, CCW as seen from outside.
464        // NOTE: if these are wound the other way, the tetra renders “inside out”
465        // under back-face culling.
466        let indices = vec![
467            0, 1, 2, // base-ish
468            0, 3, 1, // side
469            0, 2, 3, // side
470            1, 3, 2, // bottom
471        ];
472
473        CpuMesh::new(vertices, indices)
474    }
475
476    /// Icosahedron with optional recursive tessellation and spherical blending.
477    ///
478    /// `tessellations` is the number of recursive 4-way triangle splits.
479    /// `sphericalness` blends from planar face subdivision (`0.0`) to an icosphere (`1.0`).
480    pub fn icosahedron(tessellations: u32, sphericalness: f32) -> CpuMesh {
481        let radius = 0.5_f32;
482        let sphericalness = sphericalness.clamp(0.0, 1.0);
483        let phi = (1.0 + 5.0_f32.sqrt()) * 0.5;
484
485        let base_positions = [
486            normalize3([-1.0, phi, 0.0]),
487            normalize3([1.0, phi, 0.0]),
488            normalize3([-1.0, -phi, 0.0]),
489            normalize3([1.0, -phi, 0.0]),
490            normalize3([0.0, -1.0, phi]),
491            normalize3([0.0, 1.0, phi]),
492            normalize3([0.0, -1.0, -phi]),
493            normalize3([0.0, 1.0, -phi]),
494            normalize3([phi, 0.0, -1.0]),
495            normalize3([phi, 0.0, 1.0]),
496            normalize3([-phi, 0.0, -1.0]),
497            normalize3([-phi, 0.0, 1.0]),
498        ]
499        .map(|p| mul3(p, radius));
500
501        let base_faces = [
502            [0, 11, 5],
503            [0, 5, 1],
504            [0, 1, 7],
505            [0, 7, 10],
506            [0, 10, 11],
507            [1, 5, 9],
508            [5, 11, 4],
509            [11, 10, 2],
510            [10, 7, 6],
511            [7, 1, 8],
512            [3, 9, 4],
513            [3, 4, 2],
514            [3, 2, 6],
515            [3, 6, 8],
516            [3, 8, 9],
517            [4, 9, 5],
518            [2, 4, 11],
519            [6, 2, 10],
520            [8, 6, 7],
521            [9, 8, 1],
522        ];
523
524        let mut triangles: Vec<[[f32; 3]; 3]> = base_faces
525            .iter()
526            .map(|face| {
527                [
528                    base_positions[face[0]],
529                    base_positions[face[1]],
530                    base_positions[face[2]],
531                ]
532            })
533            .collect();
534
535        for _ in 0..tessellations {
536            let mut next = Vec::with_capacity(triangles.len() * 4);
537            for [a, b, c] in triangles {
538                let ab = mul3(add3(a, b), 0.5);
539                let bc = mul3(add3(b, c), 0.5);
540                let ca = mul3(add3(c, a), 0.5);
541                next.push([a, ab, ca]);
542                next.push([ab, b, bc]);
543                next.push([ca, bc, c]);
544                next.push([ab, bc, ca]);
545            }
546            triangles = next;
547        }
548
549        let mut vertices = Vec::with_capacity(triangles.len() * 3);
550        let mut indices = Vec::with_capacity(triangles.len() * 3);
551
552        for triangle in triangles {
553            let [planar_a, planar_b, planar_c] = triangle;
554            let final_positions = [planar_a, planar_b, planar_c].map(|planar| {
555                let spherical = mul3(normalize3(planar), radius);
556                lerp3(planar, spherical, sphericalness)
557            });
558
559            let face_normal = normalize3(cross3(
560                sub3(final_positions[1], final_positions[0]),
561                sub3(final_positions[2], final_positions[0]),
562            ));
563
564            for pos in final_positions {
565                let spherical_normal = normalize3(pos);
566                let normal = normalize3(lerp3(face_normal, spherical_normal, sphericalness));
567                let u = 0.5 + normal[2].atan2(normal[0]) / std::f32::consts::TAU;
568                let v = 0.5 - normal[1].asin() / std::f32::consts::PI;
569                vertices.push(CpuVertex {
570                    pos,
571                    uv: [u, v],
572                    normal,
573                });
574                indices.push((vertices.len() - 1) as u32);
575            }
576        }
577
578        CpuMesh::new(vertices, indices)
579    }
580
581    /// UV sphere centered at origin.
582    ///
583    /// Radius is 0.5 to match the unit-ish cube extents.
584    pub fn sphere() -> CpuMesh {
585        let radius = 0.5_f32;
586        let rings: u32 = 16;
587        let segments: u32 = 32;
588
589        let mut vertices: Vec<CpuVertex> = Vec::new();
590        let mut indices: Vec<u32> = Vec::new();
591
592        // Create vertices.
593        // v in [0..rings], u in [0..segments]
594        for r in 0..=rings {
595            let v = r as f32 / rings as f32;
596            let theta = v * std::f32::consts::PI; // 0..pi
597            let (st, ct) = theta.sin_cos();
598
599            for s in 0..=segments {
600                let u = s as f32 / segments as f32;
601                let phi = u * std::f32::consts::TAU; // 0..2pi
602                let (sp, cp) = phi.sin_cos();
603
604                let x = cp * st;
605                let y = ct;
606                let z = sp * st;
607
608                vertices.push(CpuVertex {
609                    pos: [x * radius, y * radius, z * radius],
610                    uv: [u, 1.0 - v],
611                    normal: [x, y, z],
612                });
613            }
614        }
615
616        // Create indices.
617        let stride = segments + 1;
618        for r in 0..rings {
619            for s in 0..segments {
620                let i0 = r * stride + s;
621                let i1 = i0 + 1;
622                let i2 = (r + 1) * stride + s;
623                let i3 = i2 + 1;
624
625                // Two triangles per quad.
626                indices.extend_from_slice(&[i0, i2, i1]);
627                indices.extend_from_slice(&[i1, i2, i3]);
628            }
629        }
630
631        CpuMesh::new(vertices, indices)
632    }
633
634    /// Cone centered at origin, axis-aligned along +Z.
635    ///
636    /// Geometry:
637    /// - height = 1.0 (z in [-0.5, +0.5])
638    /// - base radius = 0.5 (at z = -0.5)
639    ///
640    /// `number_of_segments` controls radial tessellation.
641    pub fn cone(number_of_segments: u32) -> CpuMesh {
642        let segs = number_of_segments.max(3);
643        let radius = 0.5_f32;
644        let z_base = -0.5_f32;
645        let z_tip = 0.5_f32;
646
647        let tip = [0.0_f32, 0.0_f32, z_tip];
648        let base_center = [0.0_f32, 0.0_f32, z_base];
649
650        let mut vertices: Vec<CpuVertex> = Vec::new();
651        let mut indices: Vec<u32> = Vec::new();
652
653        fn vec3_sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
654            [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
655        }
656
657        fn vec3_cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
658            [
659                a[1] * b[2] - a[2] * b[1],
660                a[2] * b[0] - a[0] * b[2],
661                a[0] * b[1] - a[1] * b[0],
662            ]
663        }
664
665        fn vec3_len(v: [f32; 3]) -> f32 {
666            (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
667        }
668
669        fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
670            let len = vec3_len(v);
671            if len > 0.0 {
672                [v[0] / len, v[1] / len, v[2] / len]
673            } else {
674                [0.0, 0.0, 1.0]
675            }
676        }
677
678        // Side faces: flat-shaded (unique verts per triangle).
679        for i in 0..segs {
680            let a0 = (i as f32) / (segs as f32) * std::f32::consts::TAU;
681            let a1 = ((i + 1) as f32) / (segs as f32) * std::f32::consts::TAU;
682            let (s0, c0) = a0.sin_cos();
683            let (s1, c1) = a1.sin_cos();
684
685            let p0 = [c0 * radius, s0 * radius, z_base];
686            let p1 = [c1 * radius, s1 * radius, z_base];
687
688            // Normal from triangle (tip, p0, p1).
689            let e0 = vec3_sub(p0, tip);
690            let e1 = vec3_sub(p1, tip);
691            let n = vec3_normalize(vec3_cross(e0, e1));
692
693            let base = vertices.len() as u32;
694            vertices.push(CpuVertex {
695                pos: tip,
696                uv: [0.5, 0.0],
697                normal: n,
698            });
699            vertices.push(CpuVertex {
700                pos: p0,
701                uv: [0.0, 1.0],
702                normal: n,
703            });
704            vertices.push(CpuVertex {
705                pos: p1,
706                uv: [1.0, 1.0],
707                normal: n,
708            });
709            indices.extend_from_slice(&[base, base + 1, base + 2]);
710        }
711
712        // Base cap: triangles wound CCW when viewed from -Z (outside).
713        let n_base = [0.0_f32, 0.0_f32, -1.0_f32];
714        for i in 0..segs {
715            let a0 = (i as f32) / (segs as f32) * std::f32::consts::TAU;
716            let a1 = ((i + 1) as f32) / (segs as f32) * std::f32::consts::TAU;
717            let (s0, c0) = a0.sin_cos();
718            let (s1, c1) = a1.sin_cos();
719
720            let p0 = [c0 * radius, s0 * radius, z_base];
721            let p1 = [c1 * radius, s1 * radius, z_base];
722
723            let base = vertices.len() as u32;
724            vertices.push(CpuVertex {
725                pos: base_center,
726                uv: [0.5, 0.5],
727                normal: n_base,
728            });
729            vertices.push(CpuVertex {
730                pos: p1,
731                uv: [0.5 + p1[0], 0.5 - p1[1]],
732                normal: n_base,
733            });
734            vertices.push(CpuVertex {
735                pos: p0,
736                uv: [0.5 + p0[0], 0.5 - p0[1]],
737                normal: n_base,
738            });
739
740            indices.extend_from_slice(&[base, base + 1, base + 2]);
741        }
742
743        CpuMesh::new(vertices, indices)
744    }
745
746    /// 2D ring/annulus in the XY plane (normal +Z).
747    ///
748    /// `inner_radius` and `outer_radius` are in object-space units.
749    pub fn circle_2d(inner_radius: f32, outer_radius: f32, number_of_segments: u32) -> CpuMesh {
750        let segs = number_of_segments.max(3);
751        let inner = inner_radius.max(0.0);
752        let outer = outer_radius.max(inner + 1.0e-6);
753        let n = [0.0_f32, 0.0_f32, 1.0_f32];
754
755        let mut vertices: Vec<CpuVertex> = Vec::with_capacity((segs as usize) * 2);
756        let mut indices: Vec<u32> = Vec::with_capacity((segs as usize) * 6);
757
758        // Outer ring vertices.
759        for i in 0..segs {
760            let a = (i as f32) / (segs as f32) * std::f32::consts::TAU;
761            let (s, c) = a.sin_cos();
762            let x = c * outer;
763            let y = s * outer;
764            let uv = [0.5 + x / (2.0 * outer), 0.5 - y / (2.0 * outer)];
765            vertices.push(CpuVertex {
766                pos: [x, y, 0.0],
767                uv,
768                normal: n,
769            });
770        }
771        // Inner ring vertices.
772        for i in 0..segs {
773            let a = (i as f32) / (segs as f32) * std::f32::consts::TAU;
774            let (s, c) = a.sin_cos();
775            let x = c * inner;
776            let y = s * inner;
777            let uv = [0.5 + x / (2.0 * outer), 0.5 - y / (2.0 * outer)];
778            vertices.push(CpuVertex {
779                pos: [x, y, 0.0],
780                uv,
781                normal: n,
782            });
783        }
784
785        // Indices (two triangles per segment).
786        for i in 0..segs {
787            let next = (i + 1) % segs;
788            let outer_i = i;
789            let outer_n = next;
790            let inner_i = segs + i;
791            let inner_n = segs + next;
792
793            // Quad: outer_i -> outer_n -> inner_n -> inner_i
794            indices.extend_from_slice(&[outer_i, outer_n, inner_n]);
795            indices.extend_from_slice(&[outer_i, inner_n, inner_i]);
796        }
797
798        CpuMesh::new(vertices, indices)
799    }
800
801    /// 2D partial ring/annulus in the XY plane (normal +Z).
802    ///
803    /// `start_angle_radians` is the arc start angle in standard polar coordinates.
804    /// `sweep_angle_radians` is the angular span; negative values are accepted and
805    /// are normalized to the equivalent positive arc.
806    pub fn partial_annulus_2d(
807        inner_radius: f32,
808        outer_radius: f32,
809        start_angle_radians: f32,
810        sweep_angle_radians: f32,
811        number_of_segments: u32,
812    ) -> CpuMesh {
813        let mut start = start_angle_radians;
814        let mut sweep = sweep_angle_radians;
815        if sweep < 0.0 {
816            start += sweep;
817            sweep = -sweep;
818        }
819
820        if sweep >= std::f32::consts::TAU - 1.0e-6 {
821            return Self::circle_2d(inner_radius, outer_radius, number_of_segments);
822        }
823
824        let segs = number_of_segments.max(1);
825        let inner = inner_radius.max(0.0);
826        let outer = outer_radius.max(inner + 1.0e-6);
827        let n = [0.0_f32, 0.0_f32, 1.0_f32];
828
829        let ring_vertex_count = (segs as usize) + 1;
830        let mut vertices: Vec<CpuVertex> = Vec::with_capacity(ring_vertex_count * 2);
831        let mut indices: Vec<u32> = Vec::with_capacity((segs as usize) * 6);
832
833        for i in 0..=segs {
834            let t = i as f32 / segs as f32;
835            let a = start + sweep * t;
836            let (s, c) = a.sin_cos();
837            let x = c * outer;
838            let y = s * outer;
839            let uv = [0.5 + x / (2.0 * outer), 0.5 - y / (2.0 * outer)];
840            vertices.push(CpuVertex {
841                pos: [x, y, 0.0],
842                uv,
843                normal: n,
844            });
845        }
846
847        for i in 0..=segs {
848            let t = i as f32 / segs as f32;
849            let a = start + sweep * t;
850            let (s, c) = a.sin_cos();
851            let x = c * inner;
852            let y = s * inner;
853            let uv = [0.5 + x / (2.0 * outer), 0.5 - y / (2.0 * outer)];
854            vertices.push(CpuVertex {
855                pos: [x, y, 0.0],
856                uv,
857                normal: n,
858            });
859        }
860
861        for i in 0..segs {
862            let outer_i = i;
863            let outer_n = i + 1;
864            let inner_i = segs + 1 + i;
865            let inner_n = inner_i + 1;
866
867            indices.extend_from_slice(&[outer_i, outer_n, inner_n]);
868            indices.extend_from_slice(&[outer_i, inner_n, inner_i]);
869        }
870
871        CpuMesh::new(vertices, indices)
872    }
873
874    /// 2D filled star centered at the origin in the XY plane (normal +Z).
875    pub fn star(
876        points: u32,
877        inner_radius_fraction: f32,
878        outer_bevel_segments: u32,
879        inner_bevel_segments: u32,
880    ) -> CpuMesh {
881        let point_count = points.max(3);
882        let outer = 0.5_f32;
883        let inner = outer * inner_radius_fraction.clamp(0.01, 1.0);
884        let step = std::f32::consts::PI / point_count as f32;
885
886        let mut star_vertices: Vec<[f32; 2]> = Vec::with_capacity((point_count as usize) * 2);
887        for i in 0..point_count {
888            let outer_angle = i as f32 * 2.0 * step - std::f32::consts::FRAC_PI_2;
889            let inner_angle = outer_angle + step;
890            let (outer_s, outer_c) = outer_angle.sin_cos();
891            let (inner_s, inner_c) = inner_angle.sin_cos();
892            star_vertices.push([outer_c * outer, outer_s * outer]);
893            star_vertices.push([inner_c * inner, inner_s * inner]);
894        }
895
896        let mut boundary: Vec<[f32; 2]> = Vec::new();
897        for i in 0..star_vertices.len() {
898            let prev = star_vertices[(i + star_vertices.len() - 1) % star_vertices.len()];
899            let curr = star_vertices[i];
900            let next = star_vertices[(i + 1) % star_vertices.len()];
901            let bevel_segments = if i % 2 == 0 {
902                outer_bevel_segments
903            } else {
904                inner_bevel_segments
905            };
906            append_rounded_corner_points(&mut boundary, prev, curr, next, bevel_segments, 0.35);
907        }
908
909        if signed_area_2d(&boundary) < 0.0 {
910            boundary.reverse();
911        }
912        filled_polygon_2d(&boundary)
913    }
914
915    /// 2D filled heart centered near the origin in the XY plane (normal +Z).
916    pub fn heart(number_of_segments: u32) -> CpuMesh {
917        let segs = number_of_segments.max(12);
918        let mut boundary: Vec<[f32; 2]> = Vec::with_capacity(segs as usize);
919        for i in 0..segs {
920            let t = i as f32 / segs as f32 * std::f32::consts::TAU;
921            let x = 16.0 * t.sin().powi(3);
922            let y =
923                13.0 * t.cos() - 5.0 * (2.0 * t).cos() - 2.0 * (3.0 * t).cos() - (4.0 * t).cos();
924            boundary.push([x, y]);
925        }
926
927        let mut min_x = f32::INFINITY;
928        let mut max_x = f32::NEG_INFINITY;
929        let mut min_y = f32::INFINITY;
930        let mut max_y = f32::NEG_INFINITY;
931        for [x, y] in &boundary {
932            min_x = min_x.min(*x);
933            max_x = max_x.max(*x);
934            min_y = min_y.min(*y);
935            max_y = max_y.max(*y);
936        }
937        let center_x = (min_x + max_x) * 0.5;
938        let center_y = (min_y + max_y) * 0.5;
939        let scale = 1.0 / (max_x - min_x).max(max_y - min_y).max(1.0e-6);
940        for point in &mut boundary {
941            point[0] = (point[0] - center_x) * scale;
942            point[1] = (point[1] - center_y) * scale;
943        }
944
945        if signed_area_2d(&boundary) < 0.0 {
946            boundary.reverse();
947        }
948        filled_polygon_2d(&boundary)
949    }
950}
951
952#[cfg(test)]
953mod tests {
954    use super::MeshFactory;
955
956    fn radius2(point: [f32; 3]) -> f32 {
957        (point[0] * point[0] + point[1] * point[1]).sqrt()
958    }
959
960    fn radius3(point: [f32; 3]) -> f32 {
961        (point[0] * point[0] + point[1] * point[1] + point[2] * point[2]).sqrt()
962    }
963
964    #[test]
965    fn sharp_star_alternates_outer_and_inner_radii() {
966        let mesh = MeshFactory::star(5, 0.4, 0, 0);
967        let boundary = &mesh.vertices[1..];
968
969        assert_eq!(boundary.len(), 10);
970        for (index, vertex) in boundary.iter().enumerate() {
971            let expected = if index % 2 == 0 { 0.5 } else { 0.2 };
972            assert!((radius2(vertex.pos) - expected).abs() < 1.0e-4);
973        }
974    }
975
976    #[test]
977    fn beveled_star_generates_intermediate_radii_near_tips() {
978        let mesh = MeshFactory::star(5, 0.4, 3, 0);
979        let boundary = &mesh.vertices[1..];
980
981        assert!(boundary.iter().any(|vertex| {
982            let r = radius2(vertex.pos);
983            r > 0.2 + 1.0e-4 && r < 0.5 - 1.0e-4
984        }));
985    }
986
987    #[test]
988    fn icosahedron_tessellation_increases_triangle_count_by_four_per_level() {
989        let base = MeshFactory::icosahedron(0, 0.0);
990        let subdivided = MeshFactory::icosahedron(2, 0.0);
991
992        assert_eq!(base.indices_u32.len() / 3, 20);
993        assert_eq!(subdivided.indices_u32.len() / 3, 20 * 4 * 4);
994    }
995
996    #[test]
997    fn icosahedron_sphericalness_one_projects_vertices_to_radius() {
998        let mesh = MeshFactory::icosahedron(1, 1.0);
999
1000        for vertex in &mesh.vertices {
1001            assert!((radius3(vertex.pos) - 0.5).abs() < 1.0e-4);
1002        }
1003    }
1004}