use std::f64::consts::TAU;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rabbit {
pub body_cy: f64,
pub body_rx: f64,
pub body_ry: f64,
pub head_cx: f64,
pub head_cy: f64,
pub head_r: f64,
pub ear_w: f64,
pub ear_len: f64,
pub ear_lean: f64,
pub ear_gap: f64,
pub eye_dx: f64,
pub eye_dy: f64,
pub eye_r: f64,
pub segments: usize,
}
impl Default for Rabbit {
fn default() -> Self {
Self {
body_cy: -0.40,
body_rx: 0.44,
body_ry: 0.50,
head_cx: 0.05,
head_cy: 0.22,
head_r: 0.32,
ear_w: 0.105,
ear_len: 0.56,
ear_lean: 0.17,
ear_gap: 0.15,
eye_dx: 0.12,
eye_dy: 0.05,
eye_r: 0.05,
segments: 64,
}
}
}
pub type Loop = Vec<(f64, f64)>;
impl Rabbit {
fn ellipse(cx: f64, cy: f64, rx: f64, ry: f64, n: usize) -> Loop {
(0..n)
.map(|i| {
let t = i as f64 / n as f64 * TAU;
(cx + rx * t.cos(), cy + ry * t.sin())
})
.collect()
}
fn ear(&self, base_x: f64, lean: f64, n: usize) -> Loop {
let base_y = self.head_cy + self.head_r * 0.55; let tip_x = base_x + lean;
let tip_y = base_y + self.ear_len;
let w = self.ear_w;
let half = (n / 2).max(2);
let mut pts = Vec::with_capacity(half * 2 + 2);
for i in 0..=half {
let s = i as f64 / half as f64; let cx = base_x + (tip_x - base_x) * s;
let cy = base_y + (tip_y - base_y) * s;
let hw = w * (1.0 - 0.45 * s); let bow = (s * std::f64::consts::PI).sin() * 0.04 * lean.signum();
pts.push((cx - hw + bow, cy));
}
for i in (0..=half).rev() {
let s = i as f64 / half as f64;
let cx = base_x + (tip_x - base_x) * s;
let cy = base_y + (tip_y - base_y) * s;
let hw = w * (1.0 - 0.45 * s);
let bow = (s * std::f64::consts::PI).sin() * 0.04 * lean.signum();
pts.push((cx + hw + bow, cy));
}
pts
}
pub fn outline(&self) -> Vec<Loop> {
let n = self.segments.max(8);
let mut loops = Vec::new();
loops.push(Self::ellipse(0.0, self.body_cy, self.body_rx, self.body_ry, n));
loops.push(Self::ellipse(self.head_cx, self.head_cy, self.head_r, self.head_r, n));
let lx = self.head_cx - self.ear_gap;
let rx = self.head_cx + self.ear_gap;
loops.push(self.ear(lx, -self.ear_lean, n));
loops.push(self.ear(rx, self.ear_lean, n));
loops.push(self.eye());
loops
}
pub fn silhouette(&self) -> Vec<Loop> {
let mut all = self.outline();
all.pop(); all
}
pub fn eye(&self) -> Loop {
Self::ellipse(
self.head_cx + self.eye_dx,
self.head_cy + self.eye_dy,
self.eye_r,
self.eye_r,
(self.segments / 2).max(8),
)
}
}
pub fn rabbit_outline() -> Vec<Loop> {
Rabbit::default().outline()
}
#[derive(Clone, Debug, Default)]
pub struct RabbitMesh {
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub indices: Vec<u32>,
}
impl RabbitMesh {
pub fn vertex_count(&self) -> usize {
self.positions.len()
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
}
pub fn rabbit_mesh(depth: f32) -> RabbitMesh {
let r = Rabbit::default();
let mut mesh = RabbitMesh::default();
let hz = depth * 0.5;
for loop_pts in r.silhouette() {
let n = loop_pts.len();
if n < 3 {
continue;
}
let (mut cx, mut cy) = (0.0f64, 0.0f64);
for &(x, y) in &loop_pts {
cx += x;
cy += y;
}
cx /= n as f64;
cy /= n as f64;
let front_centre = mesh.positions.len() as u32;
mesh.positions.push([cx as f32, cy as f32, hz]);
mesh.normals.push([0.0, 0.0, 1.0]);
let front_rim0 = mesh.positions.len() as u32;
for &(x, y) in &loop_pts {
mesh.positions.push([x as f32, y as f32, hz]);
mesh.normals.push([0.0, 0.0, 1.0]);
}
for i in 0..n as u32 {
let a = front_rim0 + i;
let b = front_rim0 + (i + 1) % n as u32;
mesh.indices.extend_from_slice(&[front_centre, a, b]);
}
let back_centre = mesh.positions.len() as u32;
mesh.positions.push([cx as f32, cy as f32, -hz]);
mesh.normals.push([0.0, 0.0, -1.0]);
let back_rim0 = mesh.positions.len() as u32;
for &(x, y) in &loop_pts {
mesh.positions.push([x as f32, y as f32, -hz]);
mesh.normals.push([0.0, 0.0, -1.0]);
}
for i in 0..n as u32 {
let a = back_rim0 + i;
let b = back_rim0 + (i + 1) % n as u32;
mesh.indices.extend_from_slice(&[back_centre, b, a]);
}
let wall0 = mesh.positions.len() as u32;
for &(x, y) in &loop_pts {
let (mut nx, mut ny) = ((x - cx) as f32, (y - cy) as f32);
let len = (nx * nx + ny * ny).sqrt().max(1e-6);
nx /= len;
ny /= len;
mesh.positions.push([x as f32, y as f32, hz]);
mesh.normals.push([nx, ny, 0.0]);
mesh.positions.push([x as f32, y as f32, -hz]);
mesh.normals.push([nx, ny, 0.0]);
}
for i in 0..n as u32 {
let i0f = wall0 + i * 2;
let i0b = wall0 + i * 2 + 1;
let j = (i + 1) % n as u32;
let i1f = wall0 + j * 2;
let i1b = wall0 + j * 2 + 1;
mesh.indices.extend_from_slice(&[i0f, i0b, i1f]);
mesh.indices.extend_from_slice(&[i1f, i0b, i1b]);
}
}
mesh
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn outline_has_body_head_two_ears_and_an_eye() {
let loops = rabbit_outline();
assert_eq!(loops.len(), 5, "body + head + 2 ears + eye");
assert_eq!(Rabbit::default().silhouette().len(), 4, "silhouette drops the eye");
for (i, l) in loops.iter().enumerate() {
assert!(l.len() >= 8, "loop {i} has enough vertices: {}", l.len());
}
}
#[test]
fn geometry_is_deterministic() {
assert_eq!(rabbit_outline(), rabbit_outline());
let a = rabbit_mesh(0.3);
let b = rabbit_mesh(0.3);
assert_eq!(a.positions, b.positions);
assert_eq!(a.indices, b.indices);
}
#[test]
fn geometry_fits_design_box_and_ears_stand_up() {
let r = Rabbit::default();
let mut max_y = f64::MIN;
for l in r.outline() {
for (x, y) in l {
assert!((-1.0..=1.0).contains(&x), "x in box: {x}");
assert!((-1.0..=1.0).contains(&y), "y in box: {y}");
max_y = max_y.max(y);
}
}
let head_top = r.head_cy + r.head_r;
assert!(max_y > head_top + 0.3, "ears stand well above the head: {max_y} vs {head_top}");
}
#[test]
fn mesh_extrudes_with_depth_normals_and_triangles() {
let depth = 0.3f32;
let m = rabbit_mesh(depth);
assert!(m.vertex_count() > 100, "a real mesh, got {}", m.vertex_count());
assert_eq!(m.normals.len(), m.positions.len(), "one normal per vertex");
assert!(m.triangle_count() > 50, "front+back+walls tessellate to many tris");
assert_eq!(m.indices.len() % 3, 0, "indices are whole triangles");
let vc = m.vertex_count() as u32;
assert!(m.indices.iter().all(|&i| i < vc), "indices in range");
let (mut zmin, mut zmax) = (f32::MAX, f32::MIN);
for p in &m.positions {
zmin = zmin.min(p[2]);
zmax = zmax.max(p[2]);
}
assert!((zmax - depth * 0.5).abs() < 1e-5 && (zmin + depth * 0.5).abs() < 1e-5, "z spans the full depth");
for nml in &m.normals {
let l = (nml[0] * nml[0] + nml[1] * nml[1] + nml[2] * nml[2]).sqrt();
assert!((l - 1.0).abs() < 1e-4, "unit normal, got {l}");
}
}
}