use crate::vpx::gameitem::plunger::{Plunger, PlungerType};
use crate::vpx::gameitem::primitive::VertexWrapper;
use crate::vpx::model::Vertex3dNoTex2;
use crate::vpx::obj::VpxFace;
use std::f32::consts::PI;
const N_LATHE_POINTS: usize = 16;
pub struct PlungerMeshes {
pub flat_rod: Option<(Vec<VertexWrapper>, Vec<VpxFace>)>,
pub rod: Option<(Vec<VertexWrapper>, Vec<VpxFace>)>,
pub spring: Option<(Vec<VertexWrapper>, Vec<VpxFace>)>,
pub ring: Option<(Vec<VertexWrapper>, Vec<VpxFace>)>,
pub tip: Option<(Vec<VertexWrapper>, Vec<VpxFace>)>,
}
#[derive(Debug, Clone)]
struct TipShapePoint {
y: f32,
r: f32,
}
fn parse_tip_shape(tip_shape: &str) -> Vec<TipShapePoint> {
let mut points = Vec::new();
for segment in tip_shape.split(';') {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
let parts: Vec<&str> = segment.split_whitespace().collect();
if parts.len() >= 2
&& let (Ok(y), Ok(r)) = (parts[0].parse::<f32>(), parts[1].parse::<f32>())
{
points.push(TipShapePoint { y, r: r * 0.5 });
}
}
points.sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal));
points
}
fn generate_lathe_ring(y: f32, center_z: f32, radius: f32) -> Vec<(f32, f32, f32)> {
let mut ring = Vec::with_capacity(N_LATHE_POINTS);
for i in 0..N_LATHE_POINTS {
let angle = (i as f32 / N_LATHE_POINTS as f32) * 2.0 * PI;
let x = radius * (-angle).sin();
let z = center_z + radius * (-angle).cos();
ring.push((x, y, z));
}
ring
}
fn generate_ring_normals(center_z: f32, ring: &[(f32, f32, f32)]) -> Vec<(f32, f32, f32)> {
ring.iter()
.map(|(x, _y, z)| {
let nx = *x; let nz = z - center_z;
let len = (nx * nx + nz * nz).sqrt();
if len > 0.0 {
(nx / len, 0.0, nz / len)
} else {
(1.0, 0.0, 0.0)
}
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn connect_rings(
vertices: &mut Vec<VertexWrapper>,
indices: &mut Vec<VpxFace>,
ring1: &[(f32, f32, f32)],
ring2: &[(f32, f32, f32)],
normals1: &[(f32, f32, f32)],
normals2: &[(f32, f32, f32)],
base_index: u16,
tv1: f32,
tv2: f32,
) -> u16 {
let n = ring1.len();
let step_u = 1.0 / n as f32;
for (i, ((x, y, z), (nx, ny, nz))) in ring1.iter().zip(normals1.iter()).enumerate() {
let mut u = 0.51 + i as f32 * step_u;
if u > 1.0 {
u -= 1.0;
}
vertices.push(VertexWrapper::new(
[0u8; 32],
Vertex3dNoTex2 {
x: *x,
y: *y,
z: *z,
nx: *nx,
ny: *ny,
nz: *nz,
tu: u,
tv: tv1,
},
));
}
for (i, ((x, y, z), (nx, ny, nz))) in ring2.iter().zip(normals2.iter()).enumerate() {
let mut u = 0.51 + i as f32 * step_u;
if u > 1.0 {
u -= 1.0;
}
vertices.push(VertexWrapper::new(
[0u8; 32],
Vertex3dNoTex2 {
x: *x,
y: *y,
z: *z,
nx: *nx,
ny: *ny,
nz: *nz,
tu: u,
tv: tv2,
},
));
}
for i in 0..n {
let i0 = base_index + i as u16;
let i1 = base_index + ((i + 1) % n) as u16;
let i2 = base_index + n as u16 + i as u16;
let i3 = base_index + n as u16 + ((i + 1) % n) as u16;
indices.push(VpxFace::new(i0 as i64, i2 as i64, i1 as i64));
indices.push(VpxFace::new(i1 as i64, i2 as i64, i3 as i64));
}
base_index + (2 * n) as u16
}
fn generate_flat_rod_mesh(plunger: &Plunger) -> (Vec<VertexWrapper>, Vec<VpxFace>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let rod_radius = plunger.width * plunger.rod_diam * 0.5;
let y_base = 0.0; let y_tip = -plunger.stroke;
let z_center = plunger.height * 0.5;
let ring_base = generate_lathe_ring(y_base, z_center, rod_radius);
let ring_tip = generate_lathe_ring(y_tip, z_center, rod_radius);
let normals_base = generate_ring_normals(z_center, &ring_base);
let normals_tip = generate_ring_normals(z_center, &ring_tip);
connect_rings(
&mut vertices,
&mut indices,
&ring_base,
&ring_tip,
&normals_base,
&normals_tip,
0,
1.0, 0.0, );
add_disc_cap(
&mut vertices,
&mut indices,
&ring_base,
false,
(0.0, 1.0, 0.0),
1.0, );
add_disc_cap(
&mut vertices,
&mut indices,
&ring_tip,
true,
(0.0, -1.0, 0.0),
0.0, );
(vertices, indices)
}
fn add_disc_cap(
vertices: &mut Vec<VertexWrapper>,
indices: &mut Vec<VpxFace>,
ring: &[(f32, f32, f32)],
flip: bool,
normal: (f32, f32, f32),
tv_base: f32,
) {
let base_index = vertices.len() as u16;
let n = ring.len();
let (cx, cy, cz) = ring.iter().fold((0.0, 0.0, 0.0), |acc, (x, y, z)| {
(acc.0 + x, acc.1 + y, acc.2 + z)
});
let (cx, cy, cz) = (cx / n as f32, cy / n as f32, cz / n as f32);
vertices.push(VertexWrapper::new(
[0u8; 32],
Vertex3dNoTex2 {
x: cx,
y: cy,
z: cz,
nx: normal.0,
ny: normal.1,
nz: normal.2,
tu: 0.5,
tv: tv_base,
},
));
for (i, (x, y, z)) in ring.iter().enumerate() {
let u = 0.5 + 0.5 * (2.0 * PI * i as f32 / n as f32).cos();
vertices.push(VertexWrapper::new(
[0u8; 32],
Vertex3dNoTex2 {
x: *x,
y: *y,
z: *z,
nx: normal.0,
ny: normal.1,
nz: normal.2,
tu: u,
tv: tv_base,
},
));
}
for i in 0..n {
let i0 = base_index; let i1 = base_index + 1 + i as u16;
let i2 = base_index + 1 + ((i + 1) % n) as u16;
if flip {
indices.push(VpxFace::new(i0 as i64, i2 as i64, i1 as i64));
} else {
indices.push(VpxFace::new(i0 as i64, i1 as i64, i2 as i64));
}
}
}
fn generate_rod_mesh(plunger: &Plunger) -> (Vec<VertexWrapper>, Vec<VpxFace>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let rod_radius = plunger.width * plunger.rod_diam * 0.5;
let tip_points = parse_tip_shape(&plunger.tip_shape);
let tip_length = if !tip_points.is_empty() {
tip_points.last().unwrap().y
} else {
0.0
};
let y_tip = -plunger.stroke;
let rody = -plunger.height + plunger.stroke;
let y_ring_top = y_tip + tip_length + plunger.ring_gap + plunger.ring_width;
let y_rod_start = rody; let y_rod_end = y_ring_top;
let z_center = plunger.height * 0.5;
let num_segments = 4;
let mut rings = Vec::new();
let mut normals_list = Vec::new();
for i in 0..=num_segments {
let t = i as f32 / num_segments as f32;
let y = y_rod_end + (y_rod_start - y_rod_end) * t;
let ring = generate_lathe_ring(y, z_center, rod_radius);
let normals = generate_ring_normals(z_center, &ring);
rings.push(ring);
normals_list.push(normals);
}
let mut base_idx = 0u16;
for i in 0..num_segments {
let t1 = 0.51 + (i as f32 / num_segments as f32) * 0.23;
let t2 = 0.51 + ((i + 1) as f32 / num_segments as f32) * 0.23;
base_idx = connect_rings(
&mut vertices,
&mut indices,
&rings[i],
&rings[i + 1],
&normals_list[i],
&normals_list[i + 1],
base_idx,
t1,
t2,
);
}
add_disc_cap(
&mut vertices,
&mut indices,
&rings[0],
false,
(0.0, 1.0, 0.0),
0.74, );
(vertices, indices)
}
fn generate_spring_mesh(plunger: &Plunger) -> (Vec<VertexWrapper>, Vec<VpxFace>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let half_width = plunger.width * 0.5;
let spring_radius = half_width * plunger.spring_diam;
let coil_radius = plunger.spring_gauge;
let tip_points = parse_tip_shape(&plunger.tip_shape);
let tip_length = if !tip_points.is_empty() {
tip_points.last().unwrap().y
} else {
0.0
};
let y_tip = -plunger.stroke; let rody = -plunger.height + plunger.stroke;
let y_ring_top = y_tip + tip_length + plunger.ring_gap + plunger.ring_width;
let y_spring_start = y_ring_top; let y_spring_end = rody;
let spring_length = y_spring_end - y_spring_start;
let z_center = plunger.height * 0.5;
let cx = 0.0;
let total_turns = plunger.spring_loops + plunger.spring_end_loops * 2.0;
let segments_per_turn = 24;
let total_segments = (total_turns * segments_per_turn as f32) as usize;
let n_end = (plunger.spring_end_loops * segments_per_turn as f32) as usize;
let n_main = total_segments.saturating_sub(n_end);
const SPRING_MIN_SPACING: f32 = 2.2;
let y_end = plunger.spring_end_loops * plunger.spring_gauge * SPRING_MIN_SPACING;
let dy_end = if n_end > 1 {
y_end / (n_end - 1) as f32
} else {
0.0
};
let dy_main = if n_main > 1 {
(spring_length - y_end) / (n_main - 1) as f32
} else {
spring_length
};
if total_segments < 2 {
return (vertices, indices);
}
let coil_segments = 8;
let mut helix_points = Vec::new();
let mut helix_tangents = Vec::new();
let mut helix_angles = Vec::new();
let mut y = y_spring_start;
let mut dy = dy_end;
for i in 0..=total_segments {
if i == n_end && n_main > 0 {
dy = dy_main;
}
let t = i as f32 / total_segments as f32;
let angle = t * total_turns * 2.0 * PI;
let hx = cx + spring_radius * angle.cos();
let hz = z_center + spring_radius * angle.sin();
helix_points.push((hx, y, hz));
helix_angles.push(angle);
let dx = -spring_radius * angle.sin() * (2.0 * PI / segments_per_turn as f32);
let dz = spring_radius * angle.cos() * (2.0 * PI / segments_per_turn as f32);
let len = (dx * dx + dy * dy + dz * dz).sqrt();
if len > 0.0 {
helix_tangents.push((dx / len, dy / len, dz / len));
} else {
helix_tangents.push((0.0, 1.0, 0.0));
}
if i < total_segments {
y += dy;
}
}
for i in 0..total_segments {
let (hx, hy, hz) = helix_points[i];
let (tx, ty, tz) = helix_tangents[i];
let helix_angle = helix_angles[i];
let up = if ty.abs() < 0.99 {
(0.0, 1.0, 0.0)
} else {
(1.0, 0.0, 0.0)
};
let bx = ty * up.2 - tz * up.1;
let by = tz * up.0 - tx * up.2;
let bz = tx * up.1 - ty * up.0;
let b_len = (bx * bx + by * by + bz * bz).sqrt();
let (bx, by, bz) = (bx / b_len, by / b_len, bz / b_len);
let nx = by * tz - bz * ty;
let ny = bz * tx - bx * tz;
let nz = bx * ty - by * tx;
let base_idx = vertices.len() as u16;
for j in 0..coil_segments {
let theta = (j as f32 / coil_segments as f32) * 2.0 * PI;
let cos_t = theta.cos();
let sin_t = theta.sin();
let px = hx + coil_radius * (nx * cos_t + bx * sin_t);
let py = hy + coil_radius * (ny * cos_t + by * sin_t);
let pz = hz + coil_radius * (nz * cos_t + bz * sin_t);
let vnx = nx * cos_t + bx * sin_t;
let vny = ny * cos_t + by * sin_t;
let vnz = nz * cos_t + bz * sin_t;
let u = (helix_angle.sin() + 1.0) * 0.5;
let v = 0.76 + (j as f32 / coil_segments as f32) * 0.22;
vertices.push(VertexWrapper::new(
[0u8; 32],
Vertex3dNoTex2 {
x: px,
y: py,
z: pz,
nx: vnx,
ny: vny,
nz: vnz,
tu: u,
tv: v,
},
));
}
if i < total_segments - 1 {
let next_base = base_idx + coil_segments as u16;
for j in 0..coil_segments {
let j0 = j as u16;
let j1 = ((j + 1) % coil_segments) as u16;
indices.push(VpxFace::new(
(base_idx + j0) as i64,
(base_idx + j1) as i64,
(next_base + j0) as i64,
));
indices.push(VpxFace::new(
(base_idx + j1) as i64,
(next_base + j1) as i64,
(next_base + j0) as i64,
));
}
}
}
(vertices, indices)
}
fn generate_ring_mesh(plunger: &Plunger) -> (Vec<VertexWrapper>, Vec<VpxFace>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let r_rod = plunger.width * plunger.rod_diam * 0.5;
let r_ring = plunger.width * plunger.ring_diam * 0.5;
let tip_points = parse_tip_shape(&plunger.tip_shape);
let tip_length = if !tip_points.is_empty() {
tip_points.last().unwrap().y
} else {
0.0
};
let y_tip_end = -plunger.stroke;
let y_ring_bottom = y_tip_end + tip_length + plunger.ring_gap;
let y_ring_top = y_ring_bottom + plunger.ring_width;
let z_center = plunger.height * 0.5;
let ring_bottom_inner = generate_lathe_ring(y_ring_bottom, z_center, r_rod);
let normals_bottom_inner = vec![(0.0, -1.0, 0.0); N_LATHE_POINTS];
let ring_bottom_outer = generate_lathe_ring(y_ring_bottom, z_center, r_ring);
let normals_bottom_outer = vec![(0.0, -1.0, 0.0); N_LATHE_POINTS];
let ring_side_bottom = generate_lathe_ring(y_ring_bottom, z_center, r_ring);
let normals_side_bottom = generate_ring_normals(z_center, &ring_side_bottom);
let ring_side_top = generate_lathe_ring(y_ring_top, z_center, r_ring);
let normals_side_top = generate_ring_normals(z_center, &ring_side_top);
let ring_top_outer = generate_lathe_ring(y_ring_top, z_center, r_ring);
let normals_top_outer = vec![(0.0, 1.0, 0.0); N_LATHE_POINTS];
let ring_top_inner = generate_lathe_ring(y_ring_top, z_center, r_rod);
let normals_top_inner = vec![(0.0, 1.0, 0.0); N_LATHE_POINTS];
let mut base_idx = 0u16;
base_idx = connect_rings(
&mut vertices,
&mut indices,
&ring_bottom_inner,
&ring_bottom_outer,
&normals_bottom_inner,
&normals_bottom_outer,
base_idx,
0.26,
0.33,
);
base_idx = connect_rings(
&mut vertices,
&mut indices,
&ring_side_bottom,
&ring_side_top,
&normals_side_bottom,
&normals_side_top,
base_idx,
0.33,
0.42,
);
connect_rings(
&mut vertices,
&mut indices,
&ring_top_outer,
&ring_top_inner,
&normals_top_outer,
&normals_top_inner,
base_idx,
0.42,
0.49,
);
(vertices, indices)
}
fn generate_tip_mesh(plunger: &Plunger) -> (Vec<VertexWrapper>, Vec<VpxFace>) {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let tip_points = parse_tip_shape(&plunger.tip_shape);
if tip_points.len() < 2 {
return (vertices, indices);
}
let y_tip_end = -plunger.stroke; let z_center = plunger.height * 0.5;
let tip_len = tip_points.last().map(|p| p.y).unwrap_or(1.0);
let mut rings = Vec::new();
let mut normals_list = Vec::new();
let mut tv_values = Vec::new();
for point in &tip_points {
let y = y_tip_end + point.y;
let radius = plunger.width * point.r;
let ring = generate_lathe_ring(y, z_center, radius);
let normals = generate_ring_normals(z_center, &ring);
rings.push(ring);
normals_list.push(normals);
let tv = 0.24 * point.y / tip_len;
tv_values.push(tv);
}
let mut base_idx = 0u16;
for i in 0..rings.len() - 1 {
base_idx = connect_rings(
&mut vertices,
&mut indices,
&rings[i],
&rings[i + 1],
&normals_list[i],
&normals_list[i + 1],
base_idx,
tv_values[i],
tv_values[i + 1],
);
}
(vertices, indices)
}
pub fn build_plunger_meshes(plunger: &Plunger) -> PlungerMeshes {
if !plunger.is_visible {
return PlungerMeshes {
flat_rod: None,
rod: None,
spring: None,
ring: None,
tip: None,
};
}
match plunger.plunger_type {
PlungerType::Unknown | PlungerType::Flat => {
PlungerMeshes {
flat_rod: Some(generate_flat_rod_mesh(plunger)),
rod: None,
spring: None,
ring: None,
tip: None,
}
}
PlungerType::Modern | PlungerType::Custom => {
PlungerMeshes {
flat_rod: None,
rod: Some(generate_rod_mesh(plunger)),
spring: Some(generate_spring_mesh(plunger)),
ring: Some(generate_ring_mesh(plunger)),
tip: Some(generate_tip_mesh(plunger)),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vpx::gameitem::vertex2d::Vertex2D;
#[test]
fn test_parse_tip_shape() {
let tip_shape = "0 .34; 2 .6; 3 .64; 5 .7; 7 .84; 8 .88; 9 .9; 11 .92; 14 .92; 39 .84";
let points = parse_tip_shape(tip_shape);
assert_eq!(points.len(), 10);
assert!((points[0].y - 0.0).abs() < 0.001);
assert!((points[0].r - 0.17).abs() < 0.001); assert!((points[9].y - 39.0).abs() < 0.001);
assert!((points[9].r - 0.42).abs() < 0.001); }
#[test]
fn test_parse_tip_shape_empty() {
let points = parse_tip_shape("");
assert!(points.is_empty());
}
#[test]
fn test_build_flat_plunger_meshes() {
let plunger = Plunger {
center: Vertex2D {
x: 500.0,
y: 1900.0,
},
width: 25.0,
height: 20.0,
z_adjust: 0.0,
stroke: 80.0,
plunger_type: PlungerType::Flat,
is_visible: true,
..Default::default()
};
let meshes = build_plunger_meshes(&plunger);
assert!(meshes.flat_rod.is_some());
assert!(meshes.rod.is_none());
assert!(meshes.spring.is_none());
assert!(meshes.ring.is_none());
assert!(meshes.tip.is_none());
let (vertices, indices) = meshes.flat_rod.unwrap();
assert!(!vertices.is_empty());
assert!(!indices.is_empty());
}
#[test]
fn test_build_modern_plunger_meshes() {
let plunger = Plunger {
center: Vertex2D {
x: 500.0,
y: 1900.0,
},
width: 25.0,
height: 20.0,
z_adjust: 0.0,
stroke: 80.0,
plunger_type: PlungerType::Modern,
is_visible: true,
..Default::default()
};
let meshes = build_plunger_meshes(&plunger);
assert!(meshes.flat_rod.is_none());
assert!(meshes.rod.is_some());
assert!(meshes.spring.is_some());
assert!(meshes.ring.is_some());
assert!(meshes.tip.is_some());
let (tip_vertices, _) = meshes.tip.as_ref().unwrap();
for vertex in tip_vertices {
let tv = vertex.vertex.tv;
assert!(
(0.0..=0.25).contains(&tv),
"Tip vertex TV {} is outside tip texture range 0.00-0.25",
tv
);
}
let (ring_vertices, _) = meshes.ring.as_ref().unwrap();
for vertex in ring_vertices {
let tv = vertex.vertex.tv;
assert!(
(0.25..=0.50).contains(&tv),
"Ring vertex TV {} is outside ring texture range 0.25-0.50",
tv
);
}
let (rod_vertices, _) = meshes.rod.as_ref().unwrap();
for vertex in rod_vertices {
let tv = vertex.vertex.tv;
assert!(
(0.50..=0.75).contains(&tv),
"Rod vertex TV {} is outside rod texture range 0.50-0.75",
tv
);
}
let (spring_vertices, _) = meshes.spring.as_ref().unwrap();
for vertex in spring_vertices {
let tv = vertex.vertex.tv;
assert!(
(0.75..=1.0).contains(&tv),
"Spring vertex TV {} is outside spring texture range 0.75-1.0",
tv
);
}
}
#[test]
fn test_invisible_plunger_no_meshes() {
let plunger = Plunger {
is_visible: false,
..Default::default()
};
let meshes = build_plunger_meshes(&plunger);
assert!(meshes.flat_rod.is_none());
assert!(meshes.rod.is_none());
assert!(meshes.spring.is_none());
assert!(meshes.ring.is_none());
assert!(meshes.tip.is_none());
}
#[test]
fn test_lathe_ring_angle_zero_at_top() {
let y = 100.0;
let center_z = 50.0;
let radius = 10.0;
let ring = generate_lathe_ring(y, center_z, radius);
let (x0, y0, z0) = ring[0];
assert!(
x0.abs() < 0.001,
"First vertex X {} should be at 0 (angle=0, sin(0)=0)",
x0,
);
assert!(
(z0 - (center_z + radius)).abs() < 0.001,
"First vertex Z {} should be at center_z + radius {} (angle=0, cos(0)=1, TOP)",
z0,
center_z + radius
);
assert!((y0 - y).abs() < 0.001, "Y should be y");
let max_z = ring
.iter()
.map(|(_, _, z)| *z)
.fold(f32::NEG_INFINITY, f32::max);
assert!(
(z0 - max_z).abs() < 0.001,
"First vertex Z {} should be maximum Z {} (at the TOP)",
z0,
max_z
);
}
#[test]
fn test_texture_mapping_tu_starts_at_051() {
let plunger = Plunger {
center: Vertex2D {
x: 500.0,
y: 1900.0,
},
width: 25.0,
height: 20.0,
stroke: 80.0,
plunger_type: PlungerType::Modern,
is_visible: true,
..Default::default()
};
let meshes = build_plunger_meshes(&plunger);
let (rod_vertices, _) = meshes.rod.as_ref().unwrap();
let mut found_top_vertex = false;
for vertex in rod_vertices {
let z = vertex.vertex.z;
let tu = vertex.vertex.tu;
let max_z = rod_vertices
.iter()
.map(|v| v.vertex.z)
.fold(f32::NEG_INFINITY, f32::max);
if (z - max_z).abs() < 0.1 {
assert!(
(tu - 0.51).abs() < 0.01,
"Top vertex (max Z={}) should have TU close to 0.51, got TU={}",
z,
tu
);
found_top_vertex = true;
break;
}
}
assert!(
found_top_vertex,
"Should find at least one vertex at the top with TU=0.51"
);
}
}