#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct RibbonConfig {
pub width: f32,
pub segments: u32,
pub twist: f32,
pub taper: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct RibbonPoint {
pub position: [f32; 3],
pub up: [f32; 3],
pub width_scale: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct RibbonMesh {
pub vertices: Vec<[f32; 3]>,
pub uvs: Vec<[f32; 2]>,
pub indices: Vec<u32>,
pub point_count: usize,
}
#[allow(dead_code)]
pub fn default_ribbon_config() -> RibbonConfig {
RibbonConfig {
width: 1.0,
segments: 16,
twist: 0.0,
taper: false,
}
}
#[allow(dead_code)]
pub fn new_ribbon_point(pos: [f32; 3], up: [f32; 3]) -> RibbonPoint {
RibbonPoint {
position: pos,
up,
width_scale: 1.0,
}
}
fn normalize3(v: [f32; 3]) -> [f32; 3] {
let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
if len < 1e-9 {
[0.0, 1.0, 0.0]
} else {
[v[0] / len, v[1] / len, v[2] / len]
}
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
fn dist3(a: [f32; 3], b: [f32; 3]) -> f32 {
let d = sub3(a, b);
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
}
#[allow(dead_code)]
pub fn ribbon_length(points: &[RibbonPoint]) -> f32 {
if points.len() < 2 {
return 0.0;
}
points
.windows(2)
.map(|w| dist3(w[1].position, w[0].position))
.sum()
}
#[allow(dead_code)]
pub fn build_ribbon(points: &[RibbonPoint], cfg: &RibbonConfig) -> RibbonMesh {
let n = points.len();
if n < 2 {
return RibbonMesh {
vertices: Vec::new(),
uvs: Vec::new(),
indices: Vec::new(),
point_count: n,
};
}
let total_len = ribbon_length(points);
let half = cfg.width * 0.5;
let mut vertices: Vec<[f32; 3]> = Vec::with_capacity(n * 2);
let mut uvs: Vec<[f32; 2]> = Vec::with_capacity(n * 2);
let mut indices: Vec<u32> = Vec::with_capacity((n - 1) * 6);
let mut arc = 0.0_f32;
for (i, pt) in points.iter().enumerate() {
let fwd = if i + 1 < n {
normalize3(sub3(points[i + 1].position, pt.position))
} else {
normalize3(sub3(pt.position, points[i - 1].position))
};
let up = normalize3(pt.up);
let right_raw = cross3(fwd, up);
let right = if right_raw[0] * right_raw[0]
+ right_raw[1] * right_raw[1]
+ right_raw[2] * right_raw[2]
< 1e-12
{
[1.0, 0.0, 0.0]
} else {
normalize3(right_raw)
};
let t_frac = if total_len > 1e-9 { arc / total_len } else { 0.0 };
let twist_angle = cfg.twist * t_frac;
let cos_t = twist_angle.cos();
let sin_t = twist_angle.sin();
let up2 = normalize3(cross3(right, fwd));
let tr = add3(scale3(right, cos_t), scale3(up2, sin_t));
let taper_scale = if cfg.taper {
let tt = t_frac * 2.0;
if tt < 1.0 {
tt
} else {
2.0 - tt
}
} else {
1.0
};
let w = half * pt.width_scale * taper_scale;
let v_coord = if total_len > 1e-9 { arc / total_len } else { 0.0 };
let left_pos = sub3(pt.position, scale3(tr, w));
vertices.push(left_pos);
uvs.push([0.0, v_coord]);
let right_pos = add3(pt.position, scale3(tr, w));
vertices.push(right_pos);
uvs.push([1.0, v_coord]);
if i + 1 < n {
arc += dist3(points[i + 1].position, pt.position);
}
if i + 1 < n {
let base = (i * 2) as u32;
indices.push(base);
indices.push(base + 1);
indices.push(base + 3);
indices.push(base);
indices.push(base + 3);
indices.push(base + 2);
}
}
RibbonMesh {
vertices,
uvs,
indices,
point_count: n,
}
}
#[allow(dead_code)]
pub fn ribbon_vertex_count(mesh: &RibbonMesh) -> usize {
mesh.vertices.len()
}
#[allow(dead_code)]
pub fn ribbon_index_count(mesh: &RibbonMesh) -> usize {
mesh.indices.len()
}
#[allow(dead_code)]
pub fn ribbon_to_json(mesh: &RibbonMesh) -> String {
format!(
"{{\"point_count\":{},\"vertex_count\":{},\"index_count\":{}}}",
mesh.point_count,
mesh.vertices.len(),
mesh.indices.len()
)
}
#[allow(dead_code)]
pub fn flip_ribbon_normals(mesh: &mut RibbonMesh) {
for uv in mesh.uvs.iter_mut() {
uv[1] = 1.0 - uv[1];
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_straight_points(n: usize) -> Vec<RibbonPoint> {
(0..n)
.map(|i| new_ribbon_point([i as f32, 0.0, 0.0], [0.0, 1.0, 0.0]))
.collect()
}
#[test]
fn test_default_config() {
let cfg = default_ribbon_config();
assert!((cfg.width - 1.0).abs() < 1e-6);
assert_eq!(cfg.segments, 16);
assert!(!cfg.taper);
}
#[test]
fn test_new_ribbon_point() {
let p = new_ribbon_point([1.0, 2.0, 3.0], [0.0, 1.0, 0.0]);
assert_eq!(p.position, [1.0, 2.0, 3.0]);
assert!((p.width_scale - 1.0).abs() < 1e-6);
}
#[test]
fn test_ribbon_length_zero() {
let pts = vec![new_ribbon_point([0.0, 0.0, 0.0], [0.0, 1.0, 0.0])];
assert!((ribbon_length(&pts)).abs() < 1e-6);
}
#[test]
fn test_ribbon_length_straight() {
let pts = make_straight_points(5);
let len = ribbon_length(&pts);
assert!((len - 4.0).abs() < 1e-5);
}
#[test]
fn test_build_ribbon_empty() {
let cfg = default_ribbon_config();
let mesh = build_ribbon(&[], &cfg);
assert_eq!(mesh.vertices.len(), 0);
assert_eq!(mesh.indices.len(), 0);
}
#[test]
fn test_build_ribbon_single_point() {
let pts = vec![new_ribbon_point([0.0, 0.0, 0.0], [0.0, 1.0, 0.0])];
let cfg = default_ribbon_config();
let mesh = build_ribbon(&pts, &cfg);
assert_eq!(mesh.vertices.len(), 0);
assert_eq!(mesh.indices.len(), 0);
}
#[test]
fn test_build_ribbon_two_points() {
let pts = make_straight_points(2);
let cfg = default_ribbon_config();
let mesh = build_ribbon(&pts, &cfg);
assert_eq!(mesh.vertices.len(), 4);
assert_eq!(mesh.indices.len(), 6);
assert_eq!(mesh.point_count, 2);
}
#[test]
fn test_build_ribbon_counts() {
let pts = make_straight_points(5);
let cfg = default_ribbon_config();
let mesh = build_ribbon(&pts, &cfg);
assert_eq!(ribbon_vertex_count(&mesh), 10);
assert_eq!(ribbon_index_count(&mesh), 24);
}
#[test]
fn test_build_ribbon_taper() {
let pts = make_straight_points(4);
let mut cfg = default_ribbon_config();
cfg.taper = true;
let mesh = build_ribbon(&pts, &cfg);
assert_eq!(mesh.vertices.len(), 8);
}
#[test]
fn test_build_ribbon_twist() {
let pts = make_straight_points(4);
let mut cfg = default_ribbon_config();
cfg.twist = std::f32::consts::PI;
let mesh = build_ribbon(&pts, &cfg);
assert_eq!(mesh.vertices.len(), 8);
}
#[test]
fn test_flip_ribbon_normals() {
let pts = make_straight_points(3);
let cfg = default_ribbon_config();
let mut mesh = build_ribbon(&pts, &cfg);
flip_ribbon_normals(&mut mesh);
for uv in &mesh.uvs {
assert!(uv[1] >= 0.0 && uv[1] <= 1.0);
}
}
#[test]
fn test_ribbon_to_json() {
let pts = make_straight_points(3);
let cfg = default_ribbon_config();
let mesh = build_ribbon(&pts, &cfg);
let json = ribbon_to_json(&mesh);
assert!(json.contains("vertex_count"));
assert!(json.contains("index_count"));
}
#[test]
fn test_ribbon_uv_range() {
let pts = make_straight_points(5);
let cfg = default_ribbon_config();
let mesh = build_ribbon(&pts, &cfg);
for uv in &mesh.uvs {
assert!(uv[0] >= 0.0 && uv[0] <= 1.0);
assert!(uv[1] >= 0.0 && uv[1] <= 1.0 + 1e-6);
}
}
#[test]
fn test_ribbon_indices_in_range() {
let pts = make_straight_points(6);
let cfg = default_ribbon_config();
let mesh = build_ribbon(&pts, &cfg);
let vcount = mesh.vertices.len() as u32;
for &idx in &mesh.indices {
assert!(idx < vcount);
}
}
}