1use glam::{Vec2, Vec3};
4use std::f32::consts::PI;
5
6#[derive(Clone, Debug)]
10pub struct ToroidalSpace {
11 pub width: f32,
12 pub height: f32,
13}
14
15impl ToroidalSpace {
16 pub fn new(width: f32, height: f32) -> Self {
17 Self { width, height }
18 }
19
20 pub fn wrap(&self, pos: Vec2) -> Vec2 {
22 Vec2::new(
23 ((pos.x % self.width) + self.width) % self.width,
24 ((pos.y % self.height) + self.height) % self.height,
25 )
26 }
27
28 pub fn distance(&self, a: Vec2, b: Vec2) -> f32 {
30 self.direction(a, b).length()
31 }
32
33 pub fn direction(&self, from: Vec2, to: Vec2) -> Vec2 {
35 let mut dx = to.x - from.x;
36 let mut dy = to.y - from.y;
37
38 if dx > self.width / 2.0 {
39 dx -= self.width;
40 } else if dx < -self.width / 2.0 {
41 dx += self.width;
42 }
43
44 if dy > self.height / 2.0 {
45 dy -= self.height;
46 } else if dy < -self.height / 2.0 {
47 dy += self.height;
48 }
49
50 Vec2::new(dx, dy)
51 }
52
53 pub fn line_of_sight(&self, from: Vec2, to: Vec2, obstacles: &[(Vec2, f32)]) -> bool {
57 let dir = self.direction(from, to);
58 let dist = dir.length();
59 if dist < 1e-8 {
60 return true;
61 }
62 let step_dir = dir / dist;
63 let steps = (dist * 4.0).ceil() as usize; for i in 0..=steps {
66 let t = i as f32 / steps as f32;
67 let sample = self.wrap(from + dir * t);
68
69 for &(obs_center, obs_radius) in obstacles {
70 let to_obs = self.direction(sample, obs_center);
71 if to_obs.length() < obs_radius {
72 return false;
73 }
74 }
75 }
76 true
77 }
78
79 pub fn neighbors(&self, pos: Vec2, radius: f32) -> Vec<Vec2> {
82 let mut result = Vec::new();
83 let wrapped = self.wrap(pos);
84
85 for dx in [-1.0_f32, 0.0, 1.0] {
87 for dy in [-1.0_f32, 0.0, 1.0] {
88 let candidate = Vec2::new(
89 wrapped.x + dx * self.width,
90 wrapped.y + dy * self.height,
91 );
92 result.push(candidate);
93 }
94 }
95 result
96 }
97}
98
99pub struct ToroidalRenderer {
103 pub space: ToroidalSpace,
104}
105
106impl ToroidalRenderer {
107 pub fn new(space: ToroidalSpace) -> Self {
108 Self { space }
109 }
110
111 pub fn visible_positions(&self, positions: &[Vec2], margin: f32) -> Vec<Vec2> {
115 let mut result = Vec::new();
116
117 for &pos in positions {
118 let wrapped = self.space.wrap(pos);
119 result.push(wrapped);
120
121 let near_left = wrapped.x < margin;
123 let near_right = wrapped.x > self.space.width - margin;
124 let near_bottom = wrapped.y < margin;
125 let near_top = wrapped.y > self.space.height - margin;
126
127 if near_left {
128 result.push(Vec2::new(wrapped.x + self.space.width, wrapped.y));
129 }
130 if near_right {
131 result.push(Vec2::new(wrapped.x - self.space.width, wrapped.y));
132 }
133 if near_bottom {
134 result.push(Vec2::new(wrapped.x, wrapped.y + self.space.height));
135 }
136 if near_top {
137 result.push(Vec2::new(wrapped.x, wrapped.y - self.space.height));
138 }
139
140 if near_left && near_bottom {
142 result.push(Vec2::new(wrapped.x + self.space.width, wrapped.y + self.space.height));
143 }
144 if near_left && near_top {
145 result.push(Vec2::new(wrapped.x + self.space.width, wrapped.y - self.space.height));
146 }
147 if near_right && near_bottom {
148 result.push(Vec2::new(wrapped.x - self.space.width, wrapped.y + self.space.height));
149 }
150 if near_right && near_top {
151 result.push(Vec2::new(wrapped.x - self.space.width, wrapped.y - self.space.height));
152 }
153 }
154
155 result
156 }
157}
158
159pub fn torus_surface(major_r: f32, minor_r: f32, u: f32, v: f32) -> Vec3 {
167 Vec3::new(
168 (major_r + minor_r * v.cos()) * u.cos(),
169 (major_r + minor_r * v.cos()) * u.sin(),
170 minor_r * v.sin(),
171 )
172}
173
174pub fn torus_mesh(major_r: f32, minor_r: f32, u_steps: usize, v_steps: usize) -> Vec<Vec3> {
176 let mut points = Vec::with_capacity(u_steps * v_steps);
177 for i in 0..u_steps {
178 let u = 2.0 * PI * i as f32 / u_steps as f32;
179 for j in 0..v_steps {
180 let v = 2.0 * PI * j as f32 / v_steps as f32;
181 points.push(torus_surface(major_r, minor_r, u, v));
182 }
183 }
184 points
185}
186
187pub fn torus_normal(major_r: f32, minor_r: f32, u: f32, v: f32) -> Vec3 {
189 let center_of_tube = Vec3::new(major_r * u.cos(), major_r * u.sin(), 0.0);
190 let point = torus_surface(major_r, minor_r, u, v);
191 (point - center_of_tube).normalize()
192}
193
194#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_wrap_basic() {
202 let space = ToroidalSpace::new(100.0, 100.0);
203 assert_eq!(space.wrap(Vec2::new(50.0, 50.0)), Vec2::new(50.0, 50.0));
204 }
205
206 #[test]
207 fn test_wrap_overflow() {
208 let space = ToroidalSpace::new(100.0, 100.0);
209 let w = space.wrap(Vec2::new(150.0, 250.0));
210 assert!((w.x - 50.0).abs() < 1e-4);
211 assert!((w.y - 50.0).abs() < 1e-4);
212 }
213
214 #[test]
215 fn test_wrap_negative() {
216 let space = ToroidalSpace::new(100.0, 100.0);
217 let w = space.wrap(Vec2::new(-10.0, -20.0));
218 assert!((w.x - 90.0).abs() < 1e-4);
219 assert!((w.y - 80.0).abs() < 1e-4);
220 }
221
222 #[test]
223 fn test_distance_no_wrap() {
224 let space = ToroidalSpace::new(100.0, 100.0);
225 let d = space.distance(Vec2::new(10.0, 10.0), Vec2::new(20.0, 10.0));
226 assert!((d - 10.0).abs() < 1e-4);
227 }
228
229 #[test]
230 fn test_distance_with_wrap() {
231 let space = ToroidalSpace::new(100.0, 100.0);
232 let d = space.distance(Vec2::new(5.0, 50.0), Vec2::new(95.0, 50.0));
234 assert!((d - 10.0).abs() < 1e-4, "Wrapped distance should be 10, got {}", d);
235 }
236
237 #[test]
238 fn test_distance_symmetry() {
239 let space = ToroidalSpace::new(100.0, 100.0);
240 let a = Vec2::new(15.0, 80.0);
241 let b = Vec2::new(90.0, 25.0);
242 let d1 = space.distance(a, b);
243 let d2 = space.distance(b, a);
244 assert!((d1 - d2).abs() < 1e-4);
245 }
246
247 #[test]
248 fn test_direction_wrap() {
249 let space = ToroidalSpace::new(100.0, 100.0);
250 let dir = space.direction(Vec2::new(95.0, 50.0), Vec2::new(5.0, 50.0));
251 assert!((dir.x - 10.0).abs() < 1e-4, "Direction x should be 10, got {}", dir.x);
252 }
253
254 #[test]
255 fn test_line_of_sight_clear() {
256 let space = ToroidalSpace::new(100.0, 100.0);
257 let clear = space.line_of_sight(
258 Vec2::new(10.0, 10.0),
259 Vec2::new(30.0, 10.0),
260 &[],
261 );
262 assert!(clear);
263 }
264
265 #[test]
266 fn test_line_of_sight_blocked() {
267 let space = ToroidalSpace::new(100.0, 100.0);
268 let blocked = space.line_of_sight(
269 Vec2::new(10.0, 10.0),
270 Vec2::new(30.0, 10.0),
271 &[(Vec2::new(20.0, 10.0), 2.0)],
272 );
273 assert!(!blocked);
274 }
275
276 #[test]
277 fn test_neighbors() {
278 let space = ToroidalSpace::new(100.0, 100.0);
279 let n = space.neighbors(Vec2::new(50.0, 50.0), 10.0);
280 assert_eq!(n.len(), 9); }
282
283 #[test]
284 fn test_torus_surface_on_torus() {
285 let major = 3.0;
286 let minor = 1.0;
287 for i in 0..20 {
288 let u = 2.0 * PI * i as f32 / 20.0;
289 for j in 0..20 {
290 let v = 2.0 * PI * j as f32 / 20.0;
291 let p = torus_surface(major, minor, u, v);
292 let xy_dist = (p.x * p.x + p.y * p.y).sqrt();
294 assert!(xy_dist >= major - minor - 1e-4);
295 assert!(xy_dist <= major + minor + 1e-4);
296 }
297 }
298 }
299
300 #[test]
301 fn test_torus_mesh_count() {
302 let mesh = torus_mesh(3.0, 1.0, 10, 10);
303 assert_eq!(mesh.len(), 100);
304 }
305
306 #[test]
307 fn test_torus_normal_unit() {
308 let n = torus_normal(3.0, 1.0, 0.5, 1.0);
309 assert!((n.length() - 1.0).abs() < 1e-4);
310 }
311
312 #[test]
313 fn test_visible_positions_no_edge() {
314 let space = ToroidalSpace::new(100.0, 100.0);
315 let renderer = ToroidalRenderer::new(space);
316 let positions = vec![Vec2::new(50.0, 50.0)];
317 let visible = renderer.visible_positions(&positions, 10.0);
318 assert_eq!(visible.len(), 1); }
320
321 #[test]
322 fn test_visible_positions_near_edge() {
323 let space = ToroidalSpace::new(100.0, 100.0);
324 let renderer = ToroidalRenderer::new(space);
325 let positions = vec![Vec2::new(5.0, 50.0)]; let visible = renderer.visible_positions(&positions, 10.0);
327 assert!(visible.len() > 1); }
329}