#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ChainLinkParams {
pub outer_radius: f32,
pub wire_radius: f32,
pub major_segments: usize,
pub minor_segments: usize,
pub link_count: usize,
}
impl Default for ChainLinkParams {
fn default() -> Self {
Self {
outer_radius: 0.1,
wire_radius: 0.02,
major_segments: 16,
minor_segments: 8,
link_count: 5,
}
}
}
#[derive(Debug, Clone)]
pub struct LinkMesh {
pub positions: Vec<[f32; 3]>,
pub normals: Vec<[f32; 3]>,
pub indices: Vec<u32>,
}
impl LinkMesh {
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn vertex_count(&self) -> usize {
self.positions.len()
}
}
pub fn build_link(params: &ChainLinkParams) -> LinkMesh {
let maj = params.major_segments.max(4);
let min = params.minor_segments.max(3);
let r_maj = params.outer_radius - params.wire_radius;
let r_min = params.wire_radius;
let mut positions = Vec::new();
let mut normals = Vec::new();
for i in 0..maj {
let phi = 2.0 * std::f32::consts::PI * i as f32 / maj as f32;
let (sp, cp) = phi.sin_cos();
for j in 0..min {
let theta = 2.0 * std::f32::consts::PI * j as f32 / min as f32;
let (st, ct) = theta.sin_cos();
let x = (r_maj + r_min * ct) * cp;
let y = r_min * st;
let z = (r_maj + r_min * ct) * sp;
positions.push([x, y, z]);
normals.push([ct * cp, st, ct * sp]);
}
}
let mut indices = Vec::new();
for i in 0..maj {
for j in 0..min {
let a = (i * min + j) as u32;
let b = (i * min + (j + 1) % min) as u32;
let c = (((i + 1) % maj) * min + j) as u32;
let d = (((i + 1) % maj) * min + (j + 1) % min) as u32;
indices.extend_from_slice(&[a, b, c, b, d, c]);
}
}
LinkMesh {
positions,
normals,
indices,
}
}
pub fn build_chain(params: &ChainLinkParams) -> Vec<LinkMesh> {
let step = params.outer_radius * 2.0 - params.wire_radius;
(0..params.link_count)
.map(|i| {
let mut link = build_link(params);
let offset_y = i as f32 * step;
let rot = if i % 2 == 0 {
0.0f32
} else {
std::f32::consts::FRAC_PI_2
};
for p in &mut link.positions {
let x = p[0] * rot.cos() - p[2] * rot.sin();
let z = p[0] * rot.sin() + p[2] * rot.cos();
p[0] = x;
p[1] += offset_y;
p[2] = z;
}
link
})
.collect()
}
pub fn triangles_per_link(maj: usize, min: usize) -> usize {
maj * min * 2
}
pub fn validate_link_params(p: &ChainLinkParams) -> bool {
p.outer_radius > p.wire_radius
&& p.wire_radius > 0.0
&& p.major_segments >= 4
&& p.minor_segments >= 3
&& p.link_count >= 1
}
pub fn chain_triangle_count(params: &ChainLinkParams) -> usize {
params.link_count * triangles_per_link(params.major_segments, params.minor_segments)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn link_triangle_count() {
let link = build_link(&ChainLinkParams::default());
assert_eq!(link.triangle_count(), 256);
}
#[test]
fn link_vertex_count() {
let link = build_link(&ChainLinkParams::default());
assert_eq!(link.vertex_count(), 128);
}
#[test]
fn chain_link_count() {
let chain = build_chain(&ChainLinkParams::default());
assert_eq!(chain.len(), 5);
}
#[test]
fn validate_params_ok() {
assert!(validate_link_params(&ChainLinkParams::default()));
}
#[test]
fn validate_bad_radii() {
let p = ChainLinkParams {
outer_radius: 0.01,
wire_radius: 0.05,
..ChainLinkParams::default()
};
assert!(!validate_link_params(&p));
}
#[test]
fn triangles_per_link_formula() {
assert_eq!(triangles_per_link(16, 8), 256);
}
#[test]
fn chain_total_triangles() {
assert_eq!(chain_triangle_count(&ChainLinkParams::default()), 1280);
}
#[test]
fn link_indices_in_bounds() {
let link = build_link(&ChainLinkParams::default());
let n = link.positions.len() as u32;
assert!(link.indices.iter().all(|&i| i < n));
}
#[test]
fn normals_count_matches() {
let link = build_link(&ChainLinkParams::default());
assert_eq!(link.normals.len(), link.positions.len());
}
}