//! Parametric primitives (Layer ▸ New ▸ Cube/Sphere/Plane/Torus/Cone/Cylinder). //! //! Meshes are built in model space (+Y up, counter-clockwise front faces, like glTF), centred //! on the origin, in pixels, with normals, UVs (0..1 across each face/around each surface) and //! tangents. use std::f32::consts::{PI, TAU}; use serde::{Deserialize, Serialize}; use crate::{Material, Mesh, Model, Primitive}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum PrimitiveKind { #[default] Cube, Sphere, Plane, Torus, Cone, Cylinder, } impl PrimitiveKind { pub const ALL: [PrimitiveKind; 6] = [PrimitiveKind::Cube, PrimitiveKind::Sphere, PrimitiveKind::Plane, PrimitiveKind::Torus, PrimitiveKind::Cone, PrimitiveKind::Cylinder]; pub fn label(self) -> &'static str { match self { PrimitiveKind::Cube => "Cube", PrimitiveKind::Sphere => "Sphere", PrimitiveKind::Plane => "Plane", PrimitiveKind::Torus => "Torus", PrimitiveKind::Cone => "Cone", PrimitiveKind::Cylinder => "Cylinder", } } pub fn parse(s: &str) -> Option { PrimitiveKind::ALL.into_iter().find(|k| k.label().eq_ignore_ascii_case(s.trim())) } } /// Primitive dimensions (pixels) and tessellation. #[derive(Clone, Copy, Debug, PartialEq)] pub struct PrimParams { /// Width, height, depth (cube, plane: width × height). pub size: [f32; 3], /// Sphere/cone/cylinder radius; torus ring radius. pub radius: f32, /// Torus tube radius. pub tube: f32, /// Cone/cylinder height. pub height: f32, /// Segments around. pub segments: u32, /// Sphere rings / torus tube sides. pub rings: u32, } impl Default for PrimParams { fn default() -> Self { PrimParams { size: [300.0; 3], radius: 150.0, tube: 50.0, height: 300.0, segments: 48, rings: 24 } } } struct B { p: Primitive, } impl B { fn new() -> B { B { p: Primitive { material: Some(0), ..Default::default() } } } fn v(&mut self, pos: [f32; 3], n: [f32; 3], uv: [f32; 2]) -> u32 { self.p.positions.push(pos); self.p.normals.push(n); self.p.uvs.push(uv); (self.p.positions.len() - 1) as u32 } fn tri(&mut self, a: u32, b: u32, c: u32) { self.p.indices.extend([a, b, c]); } fn quad(&mut self, a: u32, b: u32, c: u32, d: u32) { // a-b-c-d counter-clockwise. self.tri(a, b, c); self.tri(a, c, d); } } /// The mesh of a primitive. pub fn mesh(kind: PrimitiveKind, pr: &PrimParams) -> Primitive { let seg = pr.segments.clamp(3, 512); let rings = pr.rings.clamp(2, 512); let mut b = B::new(); match kind { PrimitiveKind::Cube => { let [w, h, d] = pr.size.map(|v| v.abs() * 0.5); // (normal, u axis, v axis) per face; v runs down the texture. let faces: [([f32; 3], [f32; 3], [f32; 3]); 6] = [ ([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]), ([0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]), ([1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0]), ([-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0]), ([0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]), ([0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]), ]; let half = [w, h, d]; for (n, u, v) in faces { let c = |su: f32, sv: f32| -> [f32; 3] { [0, 1, 2].map(|k| (n[k] + u[k] * su + v[k] * sv) * half[k]) }; let i0 = b.v(c(-1.0, -1.0), n, [0.0, 0.0]); let i1 = b.v(c(1.0, -1.0), n, [1.0, 0.0]); let i2 = b.v(c(1.0, 1.0), n, [1.0, 1.0]); let i3 = b.v(c(-1.0, 1.0), n, [0.0, 1.0]); // u × v points along -n for these axes (v down): wind so the face is CCW from outside. b.quad(i0, i3, i2, i1); } } PrimitiveKind::Plane => { let (w, h) = (pr.size[0].abs() * 0.5, pr.size[1].abs() * 0.5); let n = [0.0, 0.0, 1.0]; let i0 = b.v([-w, h, 0.0], n, [0.0, 0.0]); let i1 = b.v([w, h, 0.0], n, [1.0, 0.0]); let i2 = b.v([w, -h, 0.0], n, [1.0, 1.0]); let i3 = b.v([-w, -h, 0.0], n, [0.0, 1.0]); b.quad(i0, i3, i2, i1); } PrimitiveKind::Sphere => { let r = pr.radius.abs(); for j in 0..=rings { let th = PI * j as f32 / rings as f32; // Exact poles and seam (welded positions). let (st, ct) = if j == 0 { (0.0, 1.0) } else if j == rings { (0.0, -1.0) } else { th.sin_cos() }; for i in 0..=seg { let ph = TAU * (i % seg) as f32 / seg as f32; let n = [st * ph.sin(), ct, st * ph.cos()]; b.v([n[0] * r, n[1] * r, n[2] * r], n, [i as f32 / seg as f32, j as f32 / rings as f32]); } } let row = seg + 1; for j in 0..rings { for i in 0..seg { let a = j * row + i; let (b0, c, d) = (a + row, a + row + 1, a + 1); if j != 0 { b.tri(a, b0, d); } if j != rings - 1 { b.tri(d, b0, c); } } } } PrimitiveKind::Torus => { let (rr, tr) = (pr.radius.abs(), pr.tube.abs()); for j in 0..=rings { let v = TAU * (j % rings) as f32 / rings as f32; for i in 0..=seg { let u = TAU * (i % seg) as f32 / seg as f32; // Ring in the XZ plane. let (cu, su, cv, sv) = (u.cos(), u.sin(), v.cos(), v.sin()); let n = [cv * su, sv, cv * cu]; let p = [(rr + tr * cv) * su, tr * sv, (rr + tr * cv) * cu]; b.v(p, n, [i as f32 / seg as f32, j as f32 / rings as f32]); } } let row = seg + 1; for j in 0..rings { for i in 0..seg { let a = j * row + i; b.quad(a, a + 1, a + row + 1, a + row); } } } PrimitiveKind::Cylinder | PrimitiveKind::Cone => { let r = pr.radius.abs(); let h = pr.height.abs() * 0.5; let top_r = if kind == PrimitiveKind::Cone { 0.0 } else { r }; // Side normal tilt for the cone (slope). let (ny, nk) = { let s = (r - top_r) / (2.0 * h).max(1e-6); let l = (1.0 + s * s).sqrt(); (s / l, 1.0 / l) }; let base = b.p.positions.len() as u32; for i in 0..=seg { let a = TAU * (i % seg) as f32 / seg as f32; let (s, c) = a.sin_cos(); let n = [s * nk, ny, c * nk]; let u = i as f32 / seg as f32; b.v([s * top_r, h, c * top_r], n, [u, 0.0]); b.v([s * r, -h, c * r], n, [u, 1.0]); } for i in 0..seg { let t0 = base + 2 * i; let (b0, t1, b1) = (t0 + 1, t0 + 2, t0 + 3); if kind == PrimitiveKind::Cone { b.tri(t0, b0, b1); } else { b.quad(t0, b0, b1, t1); } } let caps: &[(f32, f32)] = if kind == PrimitiveKind::Cone { &[(-1.0, r)] } else { &[(1.0, r), (-1.0, r)] }; for &(side, rad) in caps { let n = [0.0, side, 0.0]; let c = b.v([0.0, h * side, 0.0], n, [0.5, 0.5]); let first = b.p.positions.len() as u32; for i in 0..=seg { let a = TAU * (i % seg) as f32 / seg as f32; let (s, co) = a.sin_cos(); b.v([s * rad, h * side, co * rad], n, [0.5 + 0.5 * s, 0.5 - 0.5 * co * side]); } for i in 0..seg { let (p0, p1) = (first + i, first + i + 1); if side > 0.0 { b.tri(c, p0, p1) } else { b.tri(c, p1, p0) } } } } } b.p.compute_tangents(); b.p } /// A primitive as a one-mesh model with one material. pub fn model(kind: PrimitiveKind, pr: &PrimParams, material: Material) -> Model { let mut material = material; if kind == PrimitiveKind::Plane { material.double_sided = true; } Model::single(Mesh { name: kind.label().to_string(), primitives: vec![mesh(kind, pr)] }, vec![material]) }