use std::sync::Arc;
use crate::render3d::math::Vec3;
use crate::render3d::mesh::{Mesh, Vertex};
const PHI: f32 = 1.618_034;
fn polyhedron(faces: &[Vec<Vec3>]) -> Mesh {
let mut verts: Vec<Vertex> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
for face in faces {
let center = face.iter().fold(Vec3::ZERO, |a, &b| a + b) / face.len() as f32;
let mut normal = (face[1] - face[0])
.cross(face[2] - face[0])
.normalize_or_zero();
if normal.dot(center) < 0.0 {
normal = -normal;
}
let t = if normal.x.abs() < 0.9 {
Vec3::X
} else {
Vec3::Y
};
let t = (t - normal * normal.dot(t)).normalize_or_zero();
let bt = normal.cross(t);
let mut ordered = face.clone();
ordered.sort_by(|a, b| {
let angle = |p: &Vec3| (*p - center).dot(bt).atan2((*p - center).dot(t));
angle(a)
.partial_cmp(&angle(b))
.unwrap_or(std::cmp::Ordering::Equal)
});
let base = verts.len() as u32;
let n = ordered.len();
for (k, &p) in ordered.iter().enumerate() {
let a = k as f32 / n as f32 * std::f32::consts::TAU;
verts.push(Vertex::new(p, normal).with_uv(0.5 + 0.5 * a.cos(), 0.5 + 0.5 * a.sin()));
}
for k in 1..n as u32 - 1 {
indices.extend_from_slice(&[base, base + k, base + k + 1]);
}
}
Mesh::new(verts, indices)
}
fn fit(mut mesh: Mesh, radius: f32) -> Mesh {
let max = mesh
.vertices
.iter()
.map(|v| v.position.length())
.fold(0.0_f32, f32::max);
if max > 0.0 {
let s = radius / max;
for v in &mut mesh.vertices {
v.position *= s;
}
}
mesh
}
fn d4() -> Mesh {
let v = [
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(1.0, -1.0, -1.0),
Vec3::new(-1.0, 1.0, -1.0),
Vec3::new(-1.0, -1.0, 1.0),
];
let faces = vec![
vec![v[0], v[1], v[2]],
vec![v[0], v[1], v[3]],
vec![v[0], v[2], v[3]],
vec![v[1], v[2], v[3]],
];
fit(polyhedron(&faces), 1.0)
}
fn d6() -> Mesh {
let mut faces = Vec::new();
for axis in 0..3 {
for sign in [-1.0f32, 1.0] {
let mut face = Vec::new();
for a in [-1.0f32, 1.0] {
for b in [-1.0f32, 1.0] {
let mut p = [0.0f32; 3];
p[axis] = sign;
p[(axis + 1) % 3] = a;
p[(axis + 2) % 3] = b;
face.push(Vec3::new(p[0], p[1], p[2]));
}
}
faces.push(face);
}
}
fit(polyhedron(&faces), 1.0)
}
fn d8() -> Mesh {
let ax = [Vec3::X, Vec3::Y, Vec3::Z];
let mut faces = Vec::new();
for sx in [-1.0f32, 1.0] {
for sy in [-1.0f32, 1.0] {
for sz in [-1.0f32, 1.0] {
faces.push(vec![ax[0] * sx, ax[1] * sy, ax[2] * sz]);
}
}
}
fit(polyhedron(&faces), 1.0)
}
fn d10() -> Mesh {
let apex_t = Vec3::new(0.0, 0.0, 1.3);
let apex_b = Vec3::new(0.0, 0.0, -1.3);
let ring: Vec<Vec3> = (0..5)
.map(|i| {
let a = i as f32 / 5.0 * std::f32::consts::TAU;
Vec3::new(a.cos(), a.sin(), 0.0)
})
.collect();
let mut faces = Vec::new();
for i in 0..5 {
let j = (i + 1) % 5;
faces.push(vec![apex_t, ring[i], ring[j]]);
faces.push(vec![apex_b, ring[i], ring[j]]);
}
fit(polyhedron(&faces), 1.0)
}
fn ico_verts() -> [Vec3; 12] {
let p = PHI;
[
Vec3::new(-1.0, p, 0.0),
Vec3::new(1.0, p, 0.0),
Vec3::new(-1.0, -p, 0.0),
Vec3::new(1.0, -p, 0.0),
Vec3::new(0.0, -1.0, p),
Vec3::new(0.0, 1.0, p),
Vec3::new(0.0, -1.0, -p),
Vec3::new(0.0, 1.0, -p),
Vec3::new(p, 0.0, -1.0),
Vec3::new(p, 0.0, 1.0),
Vec3::new(-p, 0.0, -1.0),
Vec3::new(-p, 0.0, 1.0),
]
}
const ICO_FACES: [[usize; 3]; 20] = [
[0, 11, 5],
[0, 5, 1],
[0, 1, 7],
[0, 7, 10],
[0, 10, 11],
[1, 5, 9],
[5, 11, 4],
[11, 10, 2],
[10, 7, 6],
[7, 1, 8],
[3, 9, 4],
[3, 4, 2],
[3, 2, 6],
[3, 6, 8],
[3, 8, 9],
[4, 9, 5],
[2, 4, 11],
[6, 2, 10],
[8, 6, 7],
[9, 8, 1],
];
fn d20() -> Mesh {
let v = ico_verts();
let faces: Vec<Vec<Vec3>> = ICO_FACES
.iter()
.map(|f| vec![v[f[0]], v[f[1]], v[f[2]]])
.collect();
fit(polyhedron(&faces), 1.0)
}
fn d12() -> Mesh {
let v = ico_verts();
let centroids: Vec<Vec3> = ICO_FACES
.iter()
.map(|f| (v[f[0]] + v[f[1]] + v[f[2]]) / 3.0)
.collect();
let mut faces = Vec::new();
for vi in 0..12 {
let pent: Vec<Vec3> = ICO_FACES
.iter()
.enumerate()
.filter(|(_, f)| f.contains(&vi))
.map(|(fi, _)| centroids[fi])
.collect();
faces.push(pent);
}
fit(polyhedron(&faces), 1.0)
}
pub fn cup() -> Arc<Mesh> {
use std::sync::OnceLock;
static CACHE: OnceLock<Arc<Mesh>> = OnceLock::new();
CACHE.get_or_init(|| Arc::new(build_cup())).clone()
}
fn build_cup() -> Mesh {
const SEG: usize = 16;
let ring = |r: f32, y: f32| -> Vec<Vec3> {
(0..SEG)
.map(|i| {
let a = std::f32::consts::TAU * i as f32 / SEG as f32;
Vec3::new(a.cos() * r, y, a.sin() * r)
})
.collect()
};
let mut verts: Vec<Vertex> = Vec::new();
let mut idx: Vec<u32> = Vec::new();
let quad = |verts: &mut Vec<Vertex>, idx: &mut Vec<u32>, p: [Vec3; 4], n: Vec3| {
let base = verts.len() as u32;
for &pt in &p {
verts.push(Vertex::new(pt, n));
}
idx.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
idx.extend_from_slice(&[base, base + 2, base + 1, base, base + 3, base + 2]);
};
let profile = [
(0.44_f32, -0.60_f32), (0.50, -0.10),
(0.57, 0.44), (0.64, 0.52), (0.62, 0.60), ];
let rings: Vec<Vec<Vec3>> = profile.iter().map(|&(r, y)| ring(r, y)).collect();
for w in rings.windows(2) {
let (a, b) = (&w[0], &w[1]);
for i in 0..SEG {
let j = (i + 1) % SEG;
let mid = (a[i] + a[j] + b[j] + b[i]) / 4.0;
let out = Vec3::new(mid.x, 0.0, mid.z).normalize_or_zero();
quad(&mut verts, &mut idx, [a[i], a[j], b[j], b[i]], out);
}
}
let (mouth_r, rim_y) = (0.52_f32, 0.60_f32);
let mouth = ring(mouth_r, rim_y);
let lip_top = rings.last().unwrap().clone();
for i in 0..SEG {
let j = (i + 1) % SEG;
quad(
&mut verts,
&mut idx,
[lip_top[i], lip_top[j], mouth[j], mouth[i]],
Vec3::Y,
);
}
let inner_floor_y = -0.20_f32;
let inner_bot = ring(0.44, inner_floor_y);
for i in 0..SEG {
let j = (i + 1) % SEG;
let mid = (mouth[i] + mouth[j]) / 2.0;
let inward = -(Vec3::new(mid.x, 0.0, mid.z).normalize_or_zero() + Vec3::Y * 0.35)
.normalize_or_zero();
quad(
&mut verts,
&mut idx,
[mouth[i], mouth[j], inner_bot[j], inner_bot[i]],
inward,
);
}
let fan = |verts: &mut Vec<Vertex>, idx: &mut Vec<u32>, rim: &[Vec3], y: f32, n: Vec3| {
let base = verts.len() as u32;
verts.push(Vertex::new(Vec3::new(0.0, y, 0.0), n));
for &p in rim {
verts.push(Vertex::new(p, n));
}
for i in 0..SEG as u32 {
let j = (i + 1) % SEG as u32;
idx.extend_from_slice(&[base, base + 1 + i, base + 1 + j]);
idx.extend_from_slice(&[base, base + 1 + j, base + 1 + i]);
}
};
fan(
&mut verts,
&mut idx,
&inner_bot,
inner_floor_y,
Vec3::new(0.0, 0.5, 0.85).normalize(),
);
let foot = rings.first().unwrap().clone();
fan(&mut verts, &mut idx, &foot, -0.60, -Vec3::Y);
Mesh::new(verts, idx)
}
pub fn mesh_for(sides: u32) -> Arc<Mesh> {
use std::sync::OnceLock;
static CACHE: OnceLock<[(u32, Arc<Mesh>); 6]> = OnceLock::new();
let cache = CACHE.get_or_init(|| {
[
(4, Arc::new(d4())),
(6, Arc::new(d6())),
(8, Arc::new(d8())),
(10, Arc::new(d10())),
(12, Arc::new(d12())),
(20, Arc::new(d20())),
]
});
cache
.iter()
.find(|(s, _)| *s == sides)
.map(|(_, m)| m.clone())
.unwrap_or_else(|| cache[1].1.clone())
}
pub type FaceGeom = (Vec3, Vec3);
pub fn face_geometry(sides: u32) -> &'static [FaceGeom] {
use std::sync::OnceLock;
static CACHE: OnceLock<[(u32, Vec<FaceGeom>); 6]> = OnceLock::new();
let cache = CACHE.get_or_init(|| [4u32, 6, 8, 10, 12, 20].map(|s| (s, faces_of(&mesh_for(s)))));
cache
.iter()
.find(|(s, _)| *s == sides)
.map(|(_, f)| f.as_slice())
.unwrap_or_else(|| cache[1].1.as_slice())
}
fn faces_of(mesh: &Mesh) -> Vec<FaceGeom> {
let mut faces = Vec::new();
let mut i = 0;
while i < mesh.vertices.len() {
let normal = mesh.vertices[i].normal;
let start = i;
while i < mesh.vertices.len() && mesh.vertices[i].normal == normal {
i += 1;
}
let block = &mesh.vertices[start..i];
let centroid = block.iter().fold(Vec3::ZERO, |a, v| a + v.position) / block.len() as f32;
faces.push((centroid, normal));
}
faces
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_die_has_the_right_face_count() {
assert_eq!(mesh_for(4).triangle_count(), 4);
assert_eq!(mesh_for(6).triangle_count(), 12);
assert_eq!(mesh_for(8).triangle_count(), 8);
assert_eq!(mesh_for(10).triangle_count(), 10);
assert_eq!(mesh_for(12).triangle_count(), 36);
assert_eq!(mesh_for(20).triangle_count(), 20);
assert_eq!(mesh_for(100).triangle_count(), 12, "fallback is the cube");
}
#[test]
fn the_cup_is_an_open_hollow_tumbler() {
let cup = cup();
for v in &cup.vertices {
let radial = (v.position.x * v.position.x + v.position.z * v.position.z).sqrt();
if radial < 0.1 {
assert!(
v.position.y < 0.0,
"geometry near the axis at y={} — the mouth must stay open",
v.position.y
);
}
}
let radial_dot = |v: &Vertex| {
v.normal
.dot(Vec3::new(v.position.x, 0.0, v.position.z).normalize_or_zero())
};
assert!(
cup.vertices.iter().any(|v| radial_dot(v) > 0.5),
"no outward wall normals"
);
assert!(
cup.vertices.iter().any(|v| radial_dot(v) < -0.5),
"no inward bowl normals — the cup is not hollow"
);
let widest = cup
.vertices
.iter()
.max_by(|a, b| {
let r = |v: &&Vertex| v.position.x.hypot(v.position.z);
r(a).partial_cmp(&r(b)).unwrap()
})
.unwrap();
assert!(
widest.position.y > 0.3,
"the rolled lip must bulge near the mouth, not at y={}",
widest.position.y
);
}
#[test]
fn dice_are_unit_sized_and_outward_facing() {
for sides in [4, 6, 8, 10, 12, 20] {
let mesh = mesh_for(sides);
let max = mesh
.vertices
.iter()
.map(|v| v.position.length())
.fold(0.0_f32, f32::max);
assert!((max - 1.0).abs() < 1e-3, "d{sides} circumradius {max}");
for v in &mesh.vertices {
assert!(
v.normal.dot(v.position) > 0.0,
"d{sides} has an inward-facing normal"
);
}
}
}
#[test]
fn face_geometry_recovers_every_face() {
for (sides, faces) in [(4, 4), (6, 6), (8, 8), (10, 10), (12, 12), (20, 20)] {
let geo = face_geometry(sides);
assert_eq!(geo.len(), faces, "d{sides} face count");
for &(centroid, normal) in geo {
assert!(
(normal.length() - 1.0).abs() < 1e-3,
"d{sides} normal not unit"
);
assert!(
centroid.length() <= 1.0 + 1e-3,
"d{sides} centroid outside solid"
);
assert!(
centroid.dot(normal) > 0.0,
"d{sides} centroid on the inward side of its face"
);
}
}
assert_eq!(face_geometry(100).len(), 6, "fallback is the cube");
}
}