Skip to main content

viewport_lib/geometry/
primitives.rs

1use crate::resources::MeshData;
2
3/// Unit cube (side length 1, centered at the origin).
4///
5/// `size` scales all three axes uniformly.
6pub fn cube(size: f32) -> MeshData {
7    let h = size / 2.0;
8
9    // 6 faces × 4 vertices each = 24 vertices
10    #[rustfmt::skip]
11    let positions: Vec<[f32; 3]> = vec![
12        // +Z
13        [-h, -h,  h], [ h, -h,  h], [ h,  h,  h], [-h,  h,  h],
14        // -Z
15        [ h, -h, -h], [-h, -h, -h], [-h,  h, -h], [ h,  h, -h],
16        // +Y
17        [-h,  h,  h], [ h,  h,  h], [ h,  h, -h], [-h,  h, -h],
18        // -Y
19        [-h, -h, -h], [ h, -h, -h], [ h, -h,  h], [-h, -h,  h],
20        // +X
21        [ h, -h,  h], [ h, -h, -h], [ h,  h, -h], [ h,  h,  h],
22        // -X
23        [-h, -h, -h], [-h, -h,  h], [-h,  h,  h], [-h,  h, -h],
24    ];
25
26    // Build per-face flat normals
27    let face_normals: [[f32; 3]; 6] = [
28        [ 0.0,  0.0,  1.0],
29        [ 0.0,  0.0, -1.0],
30        [ 0.0,  1.0,  0.0],
31        [ 0.0, -1.0,  0.0],
32        [ 1.0,  0.0,  0.0],
33        [-1.0,  0.0,  0.0],
34    ];
35    let normals: Vec<[f32; 3]> = face_normals
36        .iter()
37        .flat_map(|n| std::iter::repeat(*n).take(4))
38        .collect();
39
40    // 6 faces × 2 triangles × 3 indices
41    let indices: Vec<u32> = (0..6u32)
42        .flat_map(|f| {
43            let b = f * 4;
44            [b, b + 1, b + 2, b, b + 2, b + 3]
45        })
46        .collect();
47
48    MeshData { positions, normals, indices, ..MeshData::default() }
49}
50
51/// UV sphere centered at the origin.
52///
53/// `radius` — sphere radius.
54/// `sectors` — longitude subdivisions (minimum 3).
55/// `stacks` — latitude subdivisions (minimum 2).
56pub fn sphere(radius: f32, sectors: u32, stacks: u32) -> MeshData {
57    let sectors = sectors.max(3);
58    let stacks = stacks.max(2);
59
60    let mut positions: Vec<[f32; 3]> = Vec::new();
61    let mut normals: Vec<[f32; 3]> = Vec::new();
62    let mut indices: Vec<u32> = Vec::new();
63
64    let sector_step = 2.0 * std::f32::consts::PI / sectors as f32;
65    let stack_step = std::f32::consts::PI / stacks as f32;
66
67    for i in 0..=stacks {
68        let stack_angle = std::f32::consts::FRAC_PI_2 - i as f32 * stack_step;
69        let xy = radius * stack_angle.cos();
70        let z = radius * stack_angle.sin();
71
72        for j in 0..=sectors {
73            let sector_angle = j as f32 * sector_step;
74            let x = xy * sector_angle.cos();
75            let y = xy * sector_angle.sin();
76            positions.push([x, y, z]);
77            normals.push([x / radius, y / radius, z / radius]);
78        }
79    }
80
81    for i in 0..stacks {
82        let k1 = i * (sectors + 1);
83        let k2 = k1 + sectors + 1;
84        for j in 0..sectors {
85            if i != 0 {
86                indices.push(k1 + j);
87                indices.push(k2 + j);
88                indices.push(k1 + j + 1);
89            }
90            if i != stacks - 1 {
91                indices.push(k1 + j + 1);
92                indices.push(k2 + j);
93                indices.push(k2 + j + 1);
94            }
95        }
96    }
97
98    MeshData { positions, normals, indices, ..MeshData::default() }
99}
100
101/// Flat XZ plane centered at the origin.
102///
103/// `width` — extent along X. `depth` — extent along Z.
104pub fn plane(width: f32, depth: f32) -> MeshData {
105    let hw = width / 2.0;
106    let hd = depth / 2.0;
107
108    let positions = vec![
109        [-hw, 0.0, -hd],
110        [ hw, 0.0, -hd],
111        [ hw, 0.0,  hd],
112        [-hw, 0.0,  hd],
113    ];
114    let normals = vec![[0.0, 1.0, 0.0]; 4];
115    let indices = vec![0, 2, 1, 0, 3, 2];
116
117    MeshData { positions, normals, indices, ..MeshData::default() }
118}
119
120/// Cylinder centered at the origin, axis along Y.
121///
122/// `radius` — circle radius. `height` — total height. `sectors` — circumference subdivisions (minimum 3).
123pub fn cylinder(radius: f32, height: f32, sectors: u32) -> MeshData {
124    let sectors = sectors.max(3);
125    let half_h = height / 2.0;
126    let step = 2.0 * std::f32::consts::PI / sectors as f32;
127
128    let mut positions: Vec<[f32; 3]> = Vec::new();
129    let mut normals: Vec<[f32; 3]> = Vec::new();
130    let mut indices: Vec<u32> = Vec::new();
131
132    // Side vertices: two rings (bottom then top)
133    for &y in &[-half_h, half_h] {
134        for j in 0..sectors {
135            let angle = j as f32 * step;
136            let x = radius * angle.cos();
137            let z = radius * angle.sin();
138            positions.push([x, y, z]);
139            normals.push([angle.cos(), 0.0, angle.sin()]);
140        }
141    }
142
143    // Side faces
144    for j in 0..sectors {
145        let b = j;
146        let next = (j + 1) % sectors;
147        let t = j + sectors;
148        let t_next = next + sectors;
149        indices.extend_from_slice(&[b, t_next, next, b, t, t_next]);
150    }
151
152    // Cap centers
153    let bottom_center = positions.len() as u32;
154    positions.push([0.0, -half_h, 0.0]);
155    normals.push([0.0, -1.0, 0.0]);
156
157    let top_center = positions.len() as u32;
158    positions.push([0.0, half_h, 0.0]);
159    normals.push([0.0, 1.0, 0.0]);
160
161    // Cap rim vertices (separate so normals point up/down)
162    let bottom_rim_start = positions.len() as u32;
163    for j in 0..sectors {
164        let angle = j as f32 * step;
165        positions.push([radius * angle.cos(), -half_h, radius * angle.sin()]);
166        normals.push([0.0, -1.0, 0.0]);
167    }
168
169    let top_rim_start = positions.len() as u32;
170    for j in 0..sectors {
171        let angle = j as f32 * step;
172        positions.push([radius * angle.cos(), half_h, radius * angle.sin()]);
173        normals.push([0.0, 1.0, 0.0]);
174    }
175
176    // Cap faces
177    for j in 0..sectors as u32 {
178        let next = (j + 1) % sectors as u32;
179        // Bottom
180        indices.extend_from_slice(&[bottom_center, bottom_rim_start + j, bottom_rim_start + next]);
181        // Top
182        indices.extend_from_slice(&[top_center, top_rim_start + next, top_rim_start + j]);
183    }
184
185    MeshData { positions, normals, indices, ..MeshData::default() }
186}
187
188/// Non-uniform box (cuboid) centered at the origin.
189///
190/// `width` — X extent. `height` — Y extent. `depth` — Z extent.
191pub fn cuboid(width: f32, height: f32, depth: f32) -> MeshData {
192    let hw = width / 2.0;
193    let hh = height / 2.0;
194    let hd = depth / 2.0;
195
196    #[rustfmt::skip]
197    let positions: Vec<[f32; 3]> = vec![
198        // +Z
199        [-hw, -hh,  hd], [ hw, -hh,  hd], [ hw,  hh,  hd], [-hw,  hh,  hd],
200        // -Z
201        [ hw, -hh, -hd], [-hw, -hh, -hd], [-hw,  hh, -hd], [ hw,  hh, -hd],
202        // +Y
203        [-hw,  hh,  hd], [ hw,  hh,  hd], [ hw,  hh, -hd], [-hw,  hh, -hd],
204        // -Y
205        [-hw, -hh, -hd], [ hw, -hh, -hd], [ hw, -hh,  hd], [-hw, -hh,  hd],
206        // +X
207        [ hw, -hh,  hd], [ hw, -hh, -hd], [ hw,  hh, -hd], [ hw,  hh,  hd],
208        // -X
209        [-hw, -hh, -hd], [-hw, -hh,  hd], [-hw,  hh,  hd], [-hw,  hh, -hd],
210    ];
211
212    let face_normals: [[f32; 3]; 6] = [
213        [ 0.0,  0.0,  1.0],
214        [ 0.0,  0.0, -1.0],
215        [ 0.0,  1.0,  0.0],
216        [ 0.0, -1.0,  0.0],
217        [ 1.0,  0.0,  0.0],
218        [-1.0,  0.0,  0.0],
219    ];
220    let normals: Vec<[f32; 3]> = face_normals
221        .iter()
222        .flat_map(|n| std::iter::repeat(*n).take(4))
223        .collect();
224
225    let indices: Vec<u32> = (0..6u32)
226        .flat_map(|f| {
227            let b = f * 4;
228            [b, b + 1, b + 2, b, b + 2, b + 3]
229        })
230        .collect();
231
232    MeshData { positions, normals, indices, ..MeshData::default() }
233}
234
235/// Cone with tip at +Y and base at −Y, centered at the origin.
236///
237/// `radius` — base radius. `height` — total height. `sectors` — circumference subdivisions (minimum 3).
238pub fn cone(radius: f32, height: f32, sectors: u32) -> MeshData {
239    let sectors = sectors.max(3);
240    let half_h = height / 2.0;
241    let step = 2.0 * std::f32::consts::PI / sectors as f32;
242
243    // Side normal components: outward radial and upward Y.
244    let hyp = (radius * radius + height * height).sqrt();
245    let ny = radius / hyp;
246    let nr = height / hyp;
247
248    let mut positions: Vec<[f32; 3]> = Vec::new();
249    let mut normals: Vec<[f32; 3]> = Vec::new();
250    let mut indices: Vec<u32> = Vec::new();
251
252    // Side faces — one duplicated tip vertex per sector so each has the bisector normal.
253    for j in 0..sectors {
254        let a0 = j as f32 * step;
255        let a1 = (j + 1) as f32 * step;
256        let amid = (a0 + a1) * 0.5;
257        let base = positions.len() as u32;
258
259        positions.push([0.0, half_h, 0.0]);
260        normals.push([nr * amid.cos(), ny, nr * amid.sin()]);
261
262        positions.push([radius * a0.cos(), -half_h, radius * a0.sin()]);
263        normals.push([nr * a0.cos(), ny, nr * a0.sin()]);
264
265        positions.push([radius * a1.cos(), -half_h, radius * a1.sin()]);
266        normals.push([nr * a1.cos(), ny, nr * a1.sin()]);
267
268        indices.extend_from_slice(&[base, base + 2, base + 1]);
269    }
270
271    // Bottom cap
272    let bottom_center = positions.len() as u32;
273    positions.push([0.0, -half_h, 0.0]);
274    normals.push([0.0, -1.0, 0.0]);
275
276    let rim_start = positions.len() as u32;
277    for j in 0..sectors {
278        let a = j as f32 * step;
279        positions.push([radius * a.cos(), -half_h, radius * a.sin()]);
280        normals.push([0.0, -1.0, 0.0]);
281    }
282    for j in 0..sectors as u32 {
283        let next = (j + 1) % sectors as u32;
284        indices.extend_from_slice(&[bottom_center, rim_start + j, rim_start + next]);
285    }
286
287    MeshData { positions, normals, indices, ..MeshData::default() }
288}
289
290/// Capsule (cylinder body with hemispherical caps) centered at the origin, axis along Y.
291///
292/// `radius` — sphere cap radius. `height` — total height (clamped so body ≥ 0).
293/// `sectors` — longitude subdivisions (minimum 3). `stacks` — latitude subdivisions (minimum 2).
294pub fn capsule(radius: f32, height: f32, sectors: u32, stacks: u32) -> MeshData {
295    let sectors = sectors.max(3);
296    let stacks = stacks.max(2);
297    let body_height = (height - 2.0 * radius).max(0.0);
298    let half_body = body_height / 2.0;
299    let hemi_stacks = (stacks / 2).max(1);
300    let cols = sectors + 1;
301
302    let mut positions: Vec<[f32; 3]> = Vec::new();
303    let mut normals: Vec<[f32; 3]> = Vec::new();
304    let mut indices: Vec<u32> = Vec::new();
305
306    // Top hemisphere (tip at i=0, equator at i=hemi_stacks, offset center +half_body)
307    for i in 0..=hemi_stacks {
308        let phi = std::f32::consts::FRAC_PI_2 * (1.0 - i as f32 / hemi_stacks as f32);
309        let sin_phi = phi.sin();
310        let cos_phi = phi.cos();
311        for j in 0..=sectors {
312            let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
313            let nx = cos_phi * theta.cos();
314            let nz = cos_phi * theta.sin();
315            positions.push([radius * nx, half_body + radius * sin_phi, radius * nz]);
316            normals.push([nx, sin_phi, nz]);
317        }
318    }
319
320    // Bottom hemisphere (equator at i=0, tip at i=hemi_stacks, offset center −half_body)
321    let bottom_off = (hemi_stacks + 1) as u32;
322    for i in 0..=hemi_stacks {
323        let phi = -std::f32::consts::FRAC_PI_2 * i as f32 / hemi_stacks as f32;
324        let sin_phi = phi.sin();
325        let cos_phi = phi.cos();
326        for j in 0..=sectors {
327            let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
328            let nx = cos_phi * theta.cos();
329            let nz = cos_phi * theta.sin();
330            positions.push([radius * nx, -half_body + radius * sin_phi, radius * nz]);
331            normals.push([nx, sin_phi, nz]);
332        }
333    }
334
335    // Top hemisphere quads (skip degenerate upper triangle at pole)
336    for i in 0..hemi_stacks {
337        let k1 = i * cols;
338        let k2 = k1 + cols;
339        for j in 0..sectors {
340            if i != 0 {
341                indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
342            }
343            indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
344        }
345    }
346
347    // Body strip connecting the two equators
348    if body_height > 1e-6 {
349        let k1 = hemi_stacks * cols;
350        let k2 = bottom_off * cols;
351        for j in 0..sectors {
352            indices.extend_from_slice(&[
353                k1 + j, k1 + j + 1, k2 + j,
354                k1 + j + 1, k2 + j + 1, k2 + j,
355            ]);
356        }
357    }
358
359    // Bottom hemisphere quads (skip degenerate lower triangle at pole)
360    for i in 0..hemi_stacks {
361        let k1 = (bottom_off + i) * cols;
362        let k2 = k1 + cols;
363        for j in 0..sectors {
364            indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
365            if i != hemi_stacks - 1 {
366                indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
367            }
368        }
369    }
370
371    MeshData { positions, normals, indices, ..MeshData::default() }
372}
373
374/// Torus centered at the origin, lying in the XZ plane.
375///
376/// `major_radius` — distance from the torus centre to the tube centre.
377/// `minor_radius` — radius of the tube.
378/// `sectors` — segments around the tube (minimum 3).
379/// `stacks` — segments around the torus ring (minimum 3).
380pub fn torus(major_radius: f32, minor_radius: f32, sectors: u32, stacks: u32) -> MeshData {
381    let sectors = sectors.max(3);
382    let stacks = stacks.max(3);
383
384    let mut positions: Vec<[f32; 3]> = Vec::new();
385    let mut normals: Vec<[f32; 3]> = Vec::new();
386    let mut indices: Vec<u32> = Vec::new();
387
388    for i in 0..=stacks {
389        let phi = i as f32 * std::f32::consts::TAU / stacks as f32;
390        let cos_phi = phi.cos();
391        let sin_phi = phi.sin();
392        let cx = major_radius * cos_phi;
393        let cz = major_radius * sin_phi;
394
395        for j in 0..=sectors {
396            let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
397            let cos_theta = theta.cos();
398            let sin_theta = theta.sin();
399
400            let nx = cos_phi * cos_theta;
401            let ny = sin_theta;
402            let nz = sin_phi * cos_theta;
403
404            positions.push([cx + minor_radius * nx, minor_radius * ny, cz + minor_radius * nz]);
405            normals.push([nx, ny, nz]);
406        }
407    }
408
409    let cols = sectors + 1;
410    for i in 0..stacks {
411        let k1 = i * cols;
412        let k2 = k1 + cols;
413        for j in 0..sectors {
414            indices.extend_from_slice(&[
415                k1 + j, k1 + j + 1, k2 + j,
416                k1 + j + 1, k2 + j + 1, k2 + j,
417            ]);
418        }
419    }
420
421    MeshData { positions, normals, indices, ..MeshData::default() }
422}
423
424/// Icosphere centered at the origin (better tessellation than UV sphere; no pole pinching).
425///
426/// `radius` — sphere radius. `subdivisions` — refinement level (0 = raw icosahedron, 20 faces).
427pub fn icosphere(radius: f32, subdivisions: u32) -> MeshData {
428    let phi = (1.0 + 5.0f32.sqrt()) / 2.0;
429    let norm = (1.0 + phi * phi).sqrt();
430    let a = 1.0 / norm;
431    let b = phi / norm;
432
433    let mut verts: Vec<[f32; 3]> = vec![
434        [-a,  b,  0.0], [ a,  b,  0.0], [-a, -b,  0.0], [ a, -b,  0.0],
435        [ 0.0, -a,  b], [ 0.0,  a,  b], [ 0.0, -a, -b], [ 0.0,  a, -b],
436        [ b,  0.0, -a], [ b,  0.0,  a], [-b,  0.0, -a], [-b,  0.0,  a],
437    ];
438    let mut faces: Vec<[u32; 3]> = vec![
439        [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
440        [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
441        [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
442        [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1],
443    ];
444
445    for _ in 0..subdivisions {
446        let mut new_faces: Vec<[u32; 3]> = Vec::with_capacity(faces.len() * 4);
447        let mut cache: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
448        for &[va, vb, vc] in &faces {
449            let mab = ico_midpoint(&mut verts, &mut cache, va, vb);
450            let mbc = ico_midpoint(&mut verts, &mut cache, vb, vc);
451            let mca = ico_midpoint(&mut verts, &mut cache, vc, va);
452            new_faces.push([va, mab, mca]);
453            new_faces.push([vb, mbc, mab]);
454            new_faces.push([vc, mca, mbc]);
455            new_faces.push([mab, mbc, mca]);
456        }
457        faces = new_faces;
458    }
459
460    let normals: Vec<[f32; 3]> = verts.clone();
461    let positions: Vec<[f32; 3]> = verts.iter().map(|v| [v[0] * radius, v[1] * radius, v[2] * radius]).collect();
462    let indices: Vec<u32> = faces.iter().flat_map(|f| f.iter().copied()).collect();
463
464    MeshData { positions, normals, indices, ..MeshData::default() }
465}
466
467fn ico_midpoint(
468    verts: &mut Vec<[f32; 3]>,
469    cache: &mut std::collections::HashMap<u64, u32>,
470    a: u32,
471    b: u32,
472) -> u32 {
473    let key = if a < b { (a as u64) << 32 | b as u64 } else { (b as u64) << 32 | a as u64 };
474    if let Some(&idx) = cache.get(&key) {
475        return idx;
476    }
477    let va = verts[a as usize];
478    let vb = verts[b as usize];
479    let mx = (va[0] + vb[0]) * 0.5;
480    let my = (va[1] + vb[1]) * 0.5;
481    let mz = (va[2] + vb[2]) * 0.5;
482    let len = (mx * mx + my * my + mz * mz).sqrt();
483    let idx = verts.len() as u32;
484    verts.push([mx / len, my / len, mz / len]);
485    cache.insert(key, idx);
486    idx
487}
488
489/// Arrow along +Y, centered at the origin (total length 1).
490///
491/// `shaft_radius` — cylinder shaft radius.
492/// `head_radius` — cone head base radius.
493/// `head_fraction` — fraction of total length occupied by the cone head (clamped to 0.1–0.9).
494/// `sectors` — circumference subdivisions (minimum 3).
495pub fn arrow(shaft_radius: f32, head_radius: f32, head_fraction: f32, sectors: u32) -> MeshData {
496    let sectors = sectors.max(3);
497    let head_fraction = head_fraction.clamp(0.1, 0.9);
498    let step = std::f32::consts::TAU / sectors as f32;
499
500    let shaft_bot: f32 = -0.5;
501    let shaft_top: f32 = 0.5 - head_fraction;
502    let head_bot: f32 = shaft_top;
503    let head_top: f32 = 0.5;
504    let head_h = head_fraction;
505
506    let mut positions: Vec<[f32; 3]> = Vec::new();
507    let mut normals: Vec<[f32; 3]> = Vec::new();
508    let mut indices: Vec<u32> = Vec::new();
509
510    // Shaft side rings (bottom then top)
511    for &y in &[shaft_bot, shaft_top] {
512        for j in 0..sectors {
513            let a = j as f32 * step;
514            positions.push([shaft_radius * a.cos(), y, shaft_radius * a.sin()]);
515            normals.push([a.cos(), 0.0, a.sin()]);
516        }
517    }
518    for j in 0..sectors {
519        let next = (j + 1) % sectors;
520        let t = j + sectors;
521        let t_next = next + sectors;
522        indices.extend_from_slice(&[j, t_next, next, j, t, t_next]);
523    }
524
525    // Shaft bottom cap
526    let sb_center = positions.len() as u32;
527    positions.push([0.0, shaft_bot, 0.0]);
528    normals.push([0.0, -1.0, 0.0]);
529    let sb_rim = positions.len() as u32;
530    for j in 0..sectors {
531        let a = j as f32 * step;
532        positions.push([shaft_radius * a.cos(), shaft_bot, shaft_radius * a.sin()]);
533        normals.push([0.0, -1.0, 0.0]);
534    }
535    for j in 0..sectors as u32 {
536        let next = (j + 1) % sectors as u32;
537        indices.extend_from_slice(&[sb_center, sb_rim + j, sb_rim + next]);
538    }
539
540    // Cone head side (one duplicated tip per sector)
541    let cone_hyp = (head_radius * head_radius + head_h * head_h).sqrt();
542    let cny = head_radius / cone_hyp;
543    let cnr = head_h / cone_hyp;
544    for j in 0..sectors {
545        let a0 = j as f32 * step;
546        let a1 = (j + 1) as f32 * step;
547        let amid = (a0 + a1) * 0.5;
548        let base = positions.len() as u32;
549
550        positions.push([0.0, head_top, 0.0]);
551        normals.push([cnr * amid.cos(), cny, cnr * amid.sin()]);
552
553        positions.push([head_radius * a0.cos(), head_bot, head_radius * a0.sin()]);
554        normals.push([cnr * a0.cos(), cny, cnr * a0.sin()]);
555
556        positions.push([head_radius * a1.cos(), head_bot, head_radius * a1.sin()]);
557        normals.push([cnr * a1.cos(), cny, cnr * a1.sin()]);
558
559        indices.extend_from_slice(&[base, base + 2, base + 1]);
560    }
561
562    // Cone base cap
563    let hb_center = positions.len() as u32;
564    positions.push([0.0, head_bot, 0.0]);
565    normals.push([0.0, -1.0, 0.0]);
566    let hb_rim = positions.len() as u32;
567    for j in 0..sectors {
568        let a = j as f32 * step;
569        positions.push([head_radius * a.cos(), head_bot, head_radius * a.sin()]);
570        normals.push([0.0, -1.0, 0.0]);
571    }
572    for j in 0..sectors as u32 {
573        let next = (j + 1) % sectors as u32;
574        indices.extend_from_slice(&[hb_center, hb_rim + j, hb_rim + next]);
575    }
576
577    MeshData { positions, normals, indices, ..MeshData::default() }
578}
579
580/// Flat disk in the XZ plane, centered at the origin, normal pointing +Y.
581///
582/// `radius` — disk radius. `sectors` — circumference subdivisions (minimum 3).
583pub fn disk(radius: f32, sectors: u32) -> MeshData {
584    let sectors = sectors.max(3);
585    let step = std::f32::consts::TAU / sectors as f32;
586
587    let mut positions: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0]];
588    let mut normals: Vec<[f32; 3]> = vec![[0.0, 1.0, 0.0]];
589    let mut indices: Vec<u32> = Vec::new();
590
591    for j in 0..sectors {
592        let a = j as f32 * step;
593        positions.push([radius * a.cos(), 0.0, radius * a.sin()]);
594        normals.push([0.0, 1.0, 0.0]);
595    }
596
597    for j in 0..sectors as u32 {
598        let next = (j + 1) % sectors as u32;
599        indices.extend_from_slice(&[0, next + 1, j + 1]);
600    }
601
602    MeshData { positions, normals, indices, ..MeshData::default() }
603}
604
605/// Camera frustum mesh for visualization.
606///
607/// The camera sits at the origin looking along −Z.
608/// `fov_y` — vertical field of view in radians. `aspect` — width / height.
609/// `near`, `far` — clip plane distances (positive values).
610pub fn frustum(fov_y: f32, aspect: f32, near: f32, far: f32) -> MeshData {
611    let half_h_n = near * (fov_y * 0.5).tan();
612    let half_w_n = half_h_n * aspect;
613    let half_h_f = far * (fov_y * 0.5).tan();
614    let half_w_f = half_h_f * aspect;
615
616    // 8 corners
617    let nbl = [-half_w_n, -half_h_n, -near];
618    let nbr = [ half_w_n, -half_h_n, -near];
619    let ntr = [ half_w_n,  half_h_n, -near];
620    let ntl = [-half_w_n,  half_h_n, -near];
621    let fbl = [-half_w_f, -half_h_f, -far];
622    let fbr = [ half_w_f, -half_h_f, -far];
623    let ftr = [ half_w_f,  half_h_f, -far];
624    let ftl = [-half_w_f,  half_h_f, -far];
625
626    // 6 faces as (v0, v1, v2, v3); normal from (v1-v0) × (v3-v0)
627    let face_quads: [[[f32; 3]; 4]; 6] = [
628        [ntl, ntr, nbr, nbl], // near
629        [fbl, fbr, ftr, ftl], // far
630        [ntl, ftl, ftr, ntr], // top
631        [nbr, fbr, fbl, nbl], // bottom
632        [ntr, ftr, fbr, nbr], // right
633        [nbl, fbl, ftl, ntl], // left
634    ];
635
636    let mut positions: Vec<[f32; 3]> = Vec::new();
637    let mut normals: Vec<[f32; 3]> = Vec::new();
638    let mut indices: Vec<u32> = Vec::new();
639
640    for quad in &face_quads {
641        let [v0, v1, _, v3] = quad;
642        let e1 = [v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]];
643        let e2 = [v3[0]-v0[0], v3[1]-v0[1], v3[2]-v0[2]];
644        let nr = [
645            e1[1]*e2[2] - e1[2]*e2[1],
646            e1[2]*e2[0] - e1[0]*e2[2],
647            e1[0]*e2[1] - e1[1]*e2[0],
648        ];
649        let len = (nr[0]*nr[0] + nr[1]*nr[1] + nr[2]*nr[2]).sqrt();
650        let n = if len > 0.0 { [nr[0]/len, nr[1]/len, nr[2]/len] } else { [0.0, 0.0, 1.0] };
651
652        let base = positions.len() as u32;
653        for v in quad {
654            positions.push(*v);
655            normals.push(n);
656        }
657        indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
658    }
659
660    MeshData { positions, normals, indices, ..MeshData::default() }
661}
662
663/// Hemisphere (upper half of a UV sphere) centered at the origin, dome facing +Y.
664///
665/// `radius` — sphere radius.
666/// `sectors` — longitude subdivisions (minimum 3). `stacks` — latitude subdivisions (minimum 1).
667pub fn hemisphere(radius: f32, sectors: u32, stacks: u32) -> MeshData {
668    let sectors = sectors.max(3);
669    let stacks = stacks.max(1);
670
671    let mut positions: Vec<[f32; 3]> = Vec::new();
672    let mut normals: Vec<[f32; 3]> = Vec::new();
673    let mut indices: Vec<u32> = Vec::new();
674
675    for i in 0..=stacks {
676        let phi = std::f32::consts::FRAC_PI_2 * (1.0 - i as f32 / stacks as f32);
677        let sin_phi = phi.sin();
678        let cos_phi = phi.cos();
679        for j in 0..=sectors {
680            let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
681            let nx = cos_phi * theta.cos();
682            let nz = cos_phi * theta.sin();
683            positions.push([radius * nx, radius * sin_phi, radius * nz]);
684            normals.push([nx, sin_phi, nz]);
685        }
686    }
687
688    let cols = sectors + 1;
689    for i in 0..stacks {
690        let k1 = i * cols;
691        let k2 = k1 + cols;
692        for j in 0..sectors {
693            if i != 0 {
694                indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
695            }
696            indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
697        }
698    }
699
700    // Equator disk cap (faces −Y)
701    let center = positions.len() as u32;
702    positions.push([0.0, 0.0, 0.0]);
703    normals.push([0.0, -1.0, 0.0]);
704    let rim_start = positions.len() as u32;
705    for j in 0..sectors {
706        let theta = j as f32 * std::f32::consts::TAU / sectors as f32;
707        positions.push([radius * theta.cos(), 0.0, radius * theta.sin()]);
708        normals.push([0.0, -1.0, 0.0]);
709    }
710    for j in 0..sectors as u32 {
711        let next = (j + 1) % sectors as u32;
712        indices.extend_from_slice(&[center, rim_start + j, rim_start + next]);
713    }
714
715    MeshData { positions, normals, indices, ..MeshData::default() }
716}
717
718/// Flat ring (annulus) in the XZ plane, centered at the origin, normal pointing +Y.
719///
720/// `inner_radius` — inner edge. `outer_radius` — outer edge. `sectors` — circumference subdivisions (minimum 3).
721pub fn ring(inner_radius: f32, outer_radius: f32, sectors: u32) -> MeshData {
722    let sectors = sectors.max(3);
723    let step = std::f32::consts::TAU / sectors as f32;
724
725    let mut positions: Vec<[f32; 3]> = Vec::new();
726    let mut normals: Vec<[f32; 3]> = Vec::new();
727    let mut indices: Vec<u32> = Vec::new();
728
729    // Interleaved inner/outer pairs: [inner_0, outer_0, inner_1, outer_1, ...]
730    for j in 0..=sectors {
731        let a = j as f32 * step;
732        let cos_a = a.cos();
733        let sin_a = a.sin();
734        positions.push([inner_radius * cos_a, 0.0, inner_radius * sin_a]);
735        normals.push([0.0, 1.0, 0.0]);
736        positions.push([outer_radius * cos_a, 0.0, outer_radius * sin_a]);
737        normals.push([0.0, 1.0, 0.0]);
738    }
739
740    for j in 0..sectors as u32 {
741        let i0 = j * 2;
742        let o0 = i0 + 1;
743        let i1 = i0 + 2;
744        let o1 = i0 + 3;
745        indices.extend_from_slice(&[i0, i1, o0, i1, o1, o0]);
746    }
747
748    MeshData { positions, normals, indices, ..MeshData::default() }
749}
750
751/// Ellipsoid centered at the origin.
752///
753/// `rx`, `ry`, `rz` — semi-axes along X, Y, Z.
754/// `sectors` — longitude subdivisions (minimum 3). `stacks` — latitude subdivisions (minimum 2).
755pub fn ellipsoid(rx: f32, ry: f32, rz: f32, sectors: u32, stacks: u32) -> MeshData {
756    let sectors = sectors.max(3);
757    let stacks = stacks.max(2);
758
759    let mut positions: Vec<[f32; 3]> = Vec::new();
760    let mut normals: Vec<[f32; 3]> = Vec::new();
761    let mut indices: Vec<u32> = Vec::new();
762
763    let sector_step = std::f32::consts::TAU / sectors as f32;
764    let stack_step = std::f32::consts::PI / stacks as f32;
765
766    for i in 0..=stacks {
767        let stack_angle = std::f32::consts::FRAC_PI_2 - i as f32 * stack_step;
768        let cos_sa = stack_angle.cos();
769        let sin_sa = stack_angle.sin();
770
771        for j in 0..=sectors {
772            let sector_angle = j as f32 * sector_step;
773            let cos_se = sector_angle.cos();
774            let sin_se = sector_angle.sin();
775
776            let x = rx * cos_sa * cos_se;
777            let y = ry * sin_sa;
778            let z = rz * cos_sa * sin_se;
779            positions.push([x, y, z]);
780
781            // Gradient of the implicit ellipsoid equation gives outward normal direction.
782            let nx = x / (rx * rx);
783            let ny = y / (ry * ry);
784            let nz = z / (rz * rz);
785            let len = (nx*nx + ny*ny + nz*nz).sqrt();
786            normals.push(if len > 0.0 { [nx/len, ny/len, nz/len] } else { [0.0, 1.0, 0.0] });
787        }
788    }
789
790    for i in 0..stacks {
791        let k1 = i * (sectors + 1);
792        let k2 = k1 + sectors + 1;
793        for j in 0..sectors {
794            if i != 0 {
795                indices.extend_from_slice(&[k1 + j, k1 + j + 1, k2 + j]);
796            }
797            if i != stacks - 1 {
798                indices.extend_from_slice(&[k1 + j + 1, k2 + j + 1, k2 + j]);
799            }
800        }
801    }
802
803    MeshData { positions, normals, indices, ..MeshData::default() }
804}
805
806/// Helical spring centered at the origin, axis along Y.
807///
808/// `radius` — distance from Y axis to tube centre.
809/// `coil_radius` — cross-section tube radius.
810/// `turns` — number of complete coil turns.
811/// `sectors` — tube cross-section subdivisions (minimum 3).
812pub fn spring(radius: f32, coil_radius: f32, turns: f32, sectors: u32) -> MeshData {
813    let sectors = sectors.max(3);
814    let pitch = 2.5 * coil_radius; // height per turn; keeps coils from overlapping
815    let height = turns * pitch;
816    let n_segs = (turns * 16.0).ceil() as u32;
817    let total_t = std::f32::consts::TAU * turns;
818
819    let mut positions: Vec<[f32; 3]> = Vec::new();
820    let mut normals: Vec<[f32; 3]> = Vec::new();
821    let mut indices: Vec<u32> = Vec::new();
822
823    for seg in 0..=n_segs {
824        let t = seg as f32 / n_segs as f32 * total_t;
825
826        let cx = radius * t.cos();
827        let cy = t / std::f32::consts::TAU * pitch - height * 0.5;
828        let cz = radius * t.sin();
829
830        // Helix tangent
831        let dtx = -radius * t.sin();
832        let dty = pitch / std::f32::consts::TAU;
833        let dtz = radius * t.cos();
834        let dt_len = (dtx*dtx + dty*dty + dtz*dtz).sqrt();
835        let (tx, ty, tz) = (dtx / dt_len, dty / dt_len, dtz / dt_len);
836
837        // Principal normal: inward radial toward helix axis
838        let pnx = -t.cos();
839        let pny = 0.0f32;
840        let pnz = -t.sin();
841
842        // Binormal = T × principal_normal
843        let bx = ty * pnz - tz * pny;
844        let by = tz * pnx - tx * pnz;
845        let bz = tx * pny - ty * pnx;
846
847        for sec in 0..=sectors {
848            let phi = sec as f32 * std::f32::consts::TAU / sectors as f32;
849            let cp = phi.cos();
850            let sp = phi.sin();
851
852            let on_x = cp * pnx + sp * bx;
853            let on_y = cp * pny + sp * by;
854            let on_z = cp * pnz + sp * bz;
855
856            positions.push([cx + coil_radius * on_x, cy + coil_radius * on_y, cz + coil_radius * on_z]);
857            normals.push([on_x, on_y, on_z]);
858        }
859    }
860
861    let cols = sectors + 1;
862    for seg in 0..n_segs {
863        let k1 = seg * cols;
864        let k2 = k1 + cols;
865        for sec in 0..sectors {
866            indices.extend_from_slice(&[
867                k1 + sec, k1 + sec + 1, k2 + sec,
868                k1 + sec + 1, k2 + sec + 1, k2 + sec,
869            ]);
870        }
871    }
872
873    MeshData { positions, normals, indices, ..MeshData::default() }
874}
875
876/// Subdivided plane in the XZ plane, centered at the origin, normal pointing +Y.
877///
878/// `width` — X extent. `depth` — Z extent.
879/// `cols` — column subdivisions (minimum 1). `rows` — row subdivisions (minimum 1).
880pub fn grid_plane(width: f32, depth: f32, cols: u32, rows: u32) -> MeshData {
881    let cols = cols.max(1);
882    let rows = rows.max(1);
883    let hw = width * 0.5;
884    let hd = depth * 0.5;
885
886    let mut positions: Vec<[f32; 3]> = Vec::new();
887    let mut normals: Vec<[f32; 3]> = Vec::new();
888    let mut indices: Vec<u32> = Vec::new();
889
890    for row in 0..=rows {
891        let z = -hd + row as f32 / rows as f32 * depth;
892        for col in 0..=cols {
893            let x = -hw + col as f32 / cols as f32 * width;
894            positions.push([x, 0.0, z]);
895            normals.push([0.0, 1.0, 0.0]);
896        }
897    }
898
899    let v_cols = cols + 1;
900    for row in 0..rows {
901        for col in 0..cols {
902            let tl = row * v_cols + col;
903            let tr = tl + 1;
904            let bl = tl + v_cols;
905            let br = bl + 1;
906            indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
907        }
908    }
909
910    MeshData { positions, normals, indices, ..MeshData::default() }
911}
912
913#[cfg(test)]
914mod tests {
915    use super::*;
916
917    fn assert_triangle_winding_matches_normals(mesh: &MeshData) {
918        for tri in mesh.indices.chunks_exact(3) {
919            let ia = tri[0] as usize;
920            let ib = tri[1] as usize;
921            let ic = tri[2] as usize;
922
923            let a = glam::Vec3::from_array(mesh.positions[ia]);
924            let b = glam::Vec3::from_array(mesh.positions[ib]);
925            let c = glam::Vec3::from_array(mesh.positions[ic]);
926            let face_normal = (b - a).cross(c - a);
927            if face_normal.length_squared() <= 1e-12 {
928                continue;
929            }
930
931            let avg_vertex_normal =
932                glam::Vec3::from_array(mesh.normals[ia])
933                + glam::Vec3::from_array(mesh.normals[ib])
934                + glam::Vec3::from_array(mesh.normals[ic]);
935
936            assert!(
937                face_normal.dot(avg_vertex_normal) > 0.0,
938                "triangle winding does not match vertex normals: {tri:?}"
939            );
940        }
941    }
942
943    #[test]
944    fn generated_primitives_have_consistent_outward_winding() {
945        let meshes = [
946            ("cube", cube(1.0)),
947            ("sphere", sphere(1.0, 24, 12)),
948            ("plane", plane(1.0, 1.0)),
949            ("cylinder", cylinder(1.0, 2.0, 24)),
950            ("cuboid", cuboid(1.0, 1.5, 2.0)),
951            ("cone", cone(1.0, 2.0, 24)),
952            ("capsule", capsule(1.0, 3.0, 24, 12)),
953            ("torus", torus(2.0, 0.5, 24, 24)),
954            ("icosphere", icosphere(1.0, 2)),
955            ("arrow", arrow(0.2, 0.4, 0.3, 24)),
956            ("disk", disk(1.0, 24)),
957            ("frustum", frustum(std::f32::consts::FRAC_PI_4, 1.5, 0.1, 2.0)),
958            ("hemisphere", hemisphere(1.0, 24, 12)),
959            ("ring", ring(0.5, 1.0, 24)),
960            ("ellipsoid", ellipsoid(1.0, 0.75, 1.25, 24, 12)),
961            ("spring", spring(2.0, 0.25, 3.0, 16)),
962            ("grid_plane", grid_plane(1.0, 1.0, 4, 4)),
963        ];
964
965        for (name, mesh) in &meshes {
966            eprintln!("checking {name}");
967            assert_triangle_winding_matches_normals(mesh);
968        }
969    }
970}