1use noise::{NoiseFn, Perlin};
27use rand::Rng;
28use sdl3::pixels::Color;
29use sdl3::render::{Canvas, FPoint};
30use sdl3::video::Window;
31
32#[derive(Debug, Clone, Copy)]
34pub struct WireParams {
35 pub sag: f32,
37 pub thickness: f32,
39 pub sway_speed: f32,
41 pub sway_amount: f32,
43 pub segment_density: f32,
45 pub color: Color,
47}
48
49impl Default for WireParams {
50 fn default() -> Self {
51 Self {
52 sag: 50.0,
53 thickness: 3.0,
54 sway_speed: 1.0,
55 sway_amount: 5.0,
56 segment_density: 2.0,
57 color: Color::RGB(180, 180, 180),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy)]
64pub struct WireLine {
65 pub start: FPoint,
66 pub end: FPoint,
67 pub params: WireParams,
68}
69
70impl WireLine {
71 pub fn new(x1: f32, y1: f32, x2: f32, y2: f32, params: WireParams) -> Self {
73 Self {
74 start: FPoint::new(x1, y1),
75 end: FPoint::new(x2, y2),
76 params,
77 }
78 }
79
80 pub fn horizontal_distance(&self) -> f32 {
82 (self.end.x - self.start.x).abs()
83 }
84
85 pub fn vertical_distance(&self) -> f32 {
87 self.end.y - self.start.y
88 }
89
90 fn catenary_y(&self, t: f32) -> f32 {
92 let dy = self.end.y - self.start.y;
93
94 let linear_y = self.start.y + t * dy;
96
97 let droop = 4.0 * t * (1.0 - t) * self.params.sag;
100
101 linear_y + droop
102 }
103}
104
105pub struct WireRenderer {
107 canvas: Canvas<Window>,
108 noise: Perlin,
109}
110
111impl WireRenderer {
112 pub fn new(canvas: Canvas<Window>) -> Self {
114 Self {
115 canvas,
116 noise: Perlin::new(rand::rng().random()),
117 }
118 }
119
120 pub fn canvas_mut(&mut self) -> &mut Canvas<Window> {
122 &mut self.canvas
123 }
124
125 pub fn present(&mut self) {
127 self.canvas.present();
128 }
129
130 pub fn render(&mut self, line: &WireLine, time: f32) -> Result<(), String> {
132 let horizontal_dist = line.horizontal_distance();
133 let num_segments = (horizontal_dist * line.params.segment_density) as usize;
134
135 if num_segments == 0 {
136 return Ok(());
137 }
138
139 let dx = line.end.x - line.start.x;
140
141 for i in 0..=num_segments {
142 let t = i as f32 / num_segments as f32;
143
144 let x = line.start.x + t * dx;
146 let base_y = line.catenary_y(t);
147
148 let sway_offset = self.noise.get([
150 x as f64 * 0.02,
151 (time * line.params.sway_speed) as f64,
152 0.0,
153 ]) as f32 * line.params.sway_amount;
154
155 let y = base_y + sway_offset;
156
157 for thickness_offset in 0..line.params.thickness as i32 {
159 let offset = thickness_offset as f32 - line.params.thickness * 0.5;
160
161 let py = y + offset;
163
164 let color_var = (self.noise.get([
166 x as f64 * 0.1,
167 py as f64 * 0.1,
168 1.0,
169 ]) * 20.0) as i32;
170
171 let r = (line.params.color.r as i32 + color_var).clamp(0, 255) as u8;
172 let g = (line.params.color.g as i32 + color_var).clamp(0, 255) as u8;
173 let b = (line.params.color.b as i32 + color_var).clamp(0, 255) as u8;
174
175 self.canvas.set_draw_color(Color::RGB(r, g, b));
176 self.canvas.draw_point(FPoint::new(x, py)).map_err(|e| e.to_string())?;
177 }
178 }
179
180 Ok(())
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_wire_line_creation() {
190 let line = WireLine::new(0.0, 0.0, 100.0, 0.0, WireParams::default());
191 assert_eq!(line.start.x, 0.0);
192 assert_eq!(line.start.y, 0.0);
193 assert_eq!(line.end.x, 100.0);
194 assert_eq!(line.end.y, 0.0);
195 }
196
197 #[test]
198 fn test_wire_line_horizontal_distance() {
199 let line = WireLine::new(0.0, 0.0, 100.0, 0.0, WireParams::default());
200 assert_eq!(line.horizontal_distance(), 100.0);
201 }
202
203 #[test]
204 fn test_catenary_y_midpoint() {
205 let line = WireLine::new(0.0, 0.0, 100.0, 0.0, WireParams {
206 sag: 50.0,
207 ..WireParams::default()
208 });
209
210 let y_mid = line.catenary_y(0.5);
212 assert!(y_mid > 0.0, "Wire should droop downward at midpoint");
213 }
214
215 #[test]
216 fn test_catenary_y_endpoints() {
217 let line = WireLine::new(0.0, 100.0, 100.0, 200.0, WireParams::default());
218
219 assert_eq!(line.catenary_y(0.0), 100.0);
221 assert_eq!(line.catenary_y(1.0), 200.0);
222 }
223}