Skip to main content

proof_engine/topology/
toroidal.rs

1// topology/toroidal.rs — Toroidal (pac-man wrapping) space
2
3use glam::{Vec2, Vec3};
4use std::f32::consts::PI;
5
6// ─── Toroidal Space ────────────────────────────────────────────────────────
7
8/// A 2D toroidal space where coordinates wrap around (pac-man style).
9#[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    /// Wrap a position into the fundamental domain [0, width) x [0, height).
21    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    /// Shortest distance between two points considering toroidal wrapping.
29    pub fn distance(&self, a: Vec2, b: Vec2) -> f32 {
30        self.direction(a, b).length()
31    }
32
33    /// Shortest direction vector from `from` to `to`, considering wrapping.
34    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    /// Wrapping-aware line of sight check.
54    /// Steps along the shortest path from `from` to `to` and checks for obstacle intersection.
55    /// `obstacles` is a list of (center, radius) pairs representing circular obstacles.
56    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; // 4 checks per unit
64
65        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    /// Find all positions (including wrapped ghost copies) within `radius` of `pos`.
80    /// Returns copies that could be visible from another perspective.
81    pub fn neighbors(&self, pos: Vec2, radius: f32) -> Vec<Vec2> {
82        let mut result = Vec::new();
83        let wrapped = self.wrap(pos);
84
85        // Consider 3x3 copies of the fundamental domain
86        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
99// ─── Toroidal Renderer ─────────────────────────────────────────────────────
100
101/// Handles rendering objects near toroidal boundaries by duplicating them.
102pub struct ToroidalRenderer {
103    pub space: ToroidalSpace,
104}
105
106impl ToroidalRenderer {
107    pub fn new(space: ToroidalSpace) -> Self {
108        Self { space }
109    }
110
111    /// Given a list of entity positions, return all positions that should be rendered,
112    /// including ghost copies near boundaries.
113    /// `margin` is how far from the edge to start duplicating.
114    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            // Check proximity to each edge
122            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            // Corner copies
141            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
159// ─── Torus Surface ─────────────────────────────────────────────────────────
160
161/// Parametric surface of a torus embedded in 3D.
162/// `major_r` = distance from center of tube to center of torus
163/// `minor_r` = radius of the tube
164/// `u` = angle around the torus (0..2*PI)
165/// `v` = angle around the tube (0..2*PI)
166pub 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
174/// Generate a mesh of points on a torus surface.
175pub 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
187/// Normal vector at a point on the torus.
188pub 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// ─── Tests ─────────────────────────────────────────────────────────────────
195
196#[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        // 5 and 95 are 10 apart when wrapping
233        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); // 3x3 grid of copies
281    }
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                // Distance from z-axis to point projected onto xy should be major +/- minor
293                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); // center, no edge copies
319    }
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)]; // near left edge
326        let visible = renderer.visible_positions(&positions, 10.0);
327        assert!(visible.len() > 1); // original + at least one ghost
328    }
329}