Skip to main content

proof_engine/topology/
klein.rs

1// topology/klein.rs — Klein bottle: non-orientable surface with orientation-flipping wrapping
2
3use glam::{Vec2, Vec3};
4use std::f32::consts::PI;
5
6// ─── Klein Bottle Parametric Surface ───────────────────────────────────────
7
8/// A Klein bottle — a non-orientable closed surface with no boundary.
9/// Cannot be embedded in 3D without self-intersection.
10pub struct KleinBottle {
11    pub scale: f32,
12}
13
14impl KleinBottle {
15    pub fn new(scale: f32) -> Self {
16        Self { scale }
17    }
18
19    /// Parametric immersion of a Klein bottle in R^3 (figure-8 immersion).
20    /// u in [0, 2*PI), v in [0, 2*PI)
21    /// This immersion self-intersects but gives a valid visualization.
22    pub fn parametric(&self, u: f32, v: f32) -> Vec3 {
23        let s = self.scale;
24        let cos_u = u.cos();
25        let sin_u = u.sin();
26        let cos_v = v.cos();
27        let sin_v = v.sin();
28
29        // Figure-8 Klein bottle immersion
30        let r = 1.0;
31        let x = s * (r + cos_u * (v / 2.0).cos() - sin_u * (v / 2.0).sin().powi(2)) * cos_v;
32        // Simplified immersion (Bagel/figure-8 style):
33        let a = 2.0;
34        let x = s * ((a + cos_u * 0.5 * cos_v.cos()) * v.cos());
35        // Use the standard "Bottle" immersion instead:
36        let (x, y, z) = klein_bottle_immersion(u, v);
37        Vec3::new(x * s, y * s, z * s)
38    }
39
40    /// Generate a mesh of points on the Klein bottle surface.
41    pub fn generate_mesh(&self, u_steps: usize, v_steps: usize) -> Vec<Vec3> {
42        let mut points = Vec::with_capacity(u_steps * v_steps);
43        for i in 0..u_steps {
44            let u = 2.0 * PI * i as f32 / u_steps as f32;
45            for j in 0..v_steps {
46                let v = 2.0 * PI * j as f32 / v_steps as f32;
47                points.push(self.parametric(u, v));
48            }
49        }
50        points
51    }
52}
53
54/// Standard Klein bottle immersion in R^3.
55/// u, v in [0, 2*PI)
56fn klein_bottle_immersion(u: f32, v: f32) -> (f32, f32, f32) {
57    // "Figure-8" Klein bottle
58    let a = 3.0;
59    let cos_u = u.cos();
60    let sin_u = u.sin();
61    let cos_v = v.cos();
62    let sin_v = v.sin();
63    let half_v = v / 2.0;
64    let cos_hv = half_v.cos();
65    let sin_hv = half_v.sin();
66
67    let r = a + cos_hv * sin_u - sin_hv * (2.0 * u).sin();
68    let x = r * cos_v;
69    let y = r * sin_v;
70    let z = sin_hv * sin_u + cos_hv * (2.0 * u).sin();
71
72    (x, y, z)
73}
74
75// ─── Klein Wrapping (2D Game Space) ────────────────────────────────────────
76
77/// Wrap a position in a Klein bottle fundamental domain.
78/// Width axis wraps normally (like a torus).
79/// Height axis wraps with horizontal reflection (like a Klein bottle).
80/// Returns (wrapped_position, whether_orientation_is_flipped).
81pub fn klein_wrap(pos: Vec2, width: f32, height: f32) -> (Vec2, bool) {
82    // First normalize x
83    let mut x = ((pos.x % width) + width) % width;
84    let mut y = pos.y;
85    let mut flipped = false;
86
87    // Count how many times we've crossed the y boundary
88    let crossings = (pos.y / height).floor() as i32;
89    y = ((y % height) + height) % height;
90
91    // Each crossing flips orientation and mirrors x
92    if crossings.abs() % 2 != 0 {
93        x = width - x;
94        flipped = true;
95    }
96
97    (Vec2::new(x, y), flipped)
98}
99
100// ─── Klein Navigation ──────────────────────────────────────────────────────
101
102/// Handles movement on a Klein bottle where crossing the top/bottom boundary
103/// flips left and right.
104pub struct KleinNavigation {
105    pub width: f32,
106    pub height: f32,
107    pub position: Vec2,
108    pub flipped: bool,
109}
110
111impl KleinNavigation {
112    pub fn new(width: f32, height: f32, start: Vec2) -> Self {
113        let (pos, flipped) = klein_wrap(start, width, height);
114        Self {
115            width,
116            height,
117            position: pos,
118            flipped,
119        }
120    }
121
122    /// Move by a delta. When flipped, horizontal movement is reversed.
123    pub fn move_by(&mut self, delta: Vec2) {
124        let effective_dx = if self.flipped { -delta.x } else { delta.x };
125        let raw = Vec2::new(self.position.x + effective_dx, self.position.y + delta.y);
126        let (pos, flipped) = klein_wrap(raw, self.width, self.height);
127
128        // If the wrap flipped, toggle our flip state
129        let y_crossings = ((self.position.y + delta.y) / self.height).floor() as i32
130            - (self.position.y / self.height).floor() as i32;
131        if y_crossings.abs() % 2 != 0 {
132            self.flipped = !self.flipped;
133        }
134        self.position = pos;
135    }
136
137    /// Get the orientation at the current position (+1 or -1).
138    pub fn orientation(&self) -> f32 {
139        if self.flipped { -1.0 } else { 1.0 }
140    }
141}
142
143/// Get the orientation sign at a given position in the Klein bottle domain.
144/// +1.0 for normal orientation, -1.0 for flipped.
145pub fn orientation_at(pos: Vec2, height: f32) -> f32 {
146    let crossings = (pos.y / height).floor() as i32;
147    if crossings.abs() % 2 != 0 {
148        -1.0
149    } else {
150        1.0
151    }
152}
153
154// ─── Klein Renderer ────────────────────────────────────────────────────────
155
156/// Renders the fundamental domain of a Klein bottle with flip indicators.
157pub struct KleinRenderer {
158    pub width: f32,
159    pub height: f32,
160}
161
162impl KleinRenderer {
163    pub fn new(width: f32, height: f32) -> Self {
164        Self { width, height }
165    }
166
167    /// Generate boundary indicators showing which edges are identified and how.
168    /// Returns line segments with an associated flag: true = identified with flip.
169    pub fn boundary_indicators(&self) -> Vec<(Vec2, Vec2, bool)> {
170        vec![
171            // Left edge identified with right edge (no flip) — same direction
172            (Vec2::new(0.0, 0.0), Vec2::new(0.0, self.height), false),
173            (Vec2::new(self.width, 0.0), Vec2::new(self.width, self.height), false),
174            // Bottom edge identified with top edge (WITH flip) — reversed direction
175            (Vec2::new(0.0, 0.0), Vec2::new(self.width, 0.0), true),
176            (Vec2::new(0.0, self.height), Vec2::new(self.width, self.height), true),
177        ]
178    }
179
180    /// For entities near the top or bottom boundaries, compute ghost positions
181    /// that account for the Klein flip.
182    pub fn ghost_positions(&self, positions: &[Vec2], margin: f32) -> Vec<(Vec2, bool)> {
183        let mut ghosts = Vec::new();
184
185        for &pos in positions {
186            // Near bottom edge
187            if pos.y < margin {
188                // Ghost appears at top, but mirrored in x
189                let ghost = Vec2::new(self.width - pos.x, self.height + pos.y);
190                ghosts.push((ghost, true));
191            }
192            // Near top edge
193            if pos.y > self.height - margin {
194                let ghost = Vec2::new(self.width - pos.x, pos.y - self.height);
195                ghosts.push((ghost, true));
196            }
197            // Near left/right edges (no flip)
198            if pos.x < margin {
199                ghosts.push((Vec2::new(pos.x + self.width, pos.y), false));
200            }
201            if pos.x > self.width - margin {
202                ghosts.push((Vec2::new(pos.x - self.width, pos.y), false));
203            }
204        }
205
206        ghosts
207    }
208}
209
210// ─── Tests ─────────────────────────────────────────────────────────────────
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_klein_wrap_no_crossing() {
218        let (pos, flipped) = klein_wrap(Vec2::new(5.0, 5.0), 10.0, 10.0);
219        assert!((pos.x - 5.0).abs() < 1e-4);
220        assert!((pos.y - 5.0).abs() < 1e-4);
221        assert!(!flipped);
222    }
223
224    #[test]
225    fn test_klein_wrap_x_only() {
226        let (pos, flipped) = klein_wrap(Vec2::new(15.0, 5.0), 10.0, 10.0);
227        assert!((pos.x - 5.0).abs() < 1e-4);
228        assert!(!flipped); // x wrapping doesn't flip
229    }
230
231    #[test]
232    fn test_klein_wrap_y_flip() {
233        let (pos, flipped) = klein_wrap(Vec2::new(3.0, 15.0), 10.0, 10.0);
234        assert!((pos.x - 7.0).abs() < 1e-4, "x should flip: got {}", pos.x);
235        assert!((pos.y - 5.0).abs() < 1e-4);
236        assert!(flipped);
237    }
238
239    #[test]
240    fn test_klein_wrap_double_y_crossing() {
241        // Crossing y boundary twice = no flip
242        let (pos, flipped) = klein_wrap(Vec2::new(3.0, 25.0), 10.0, 10.0);
243        assert!((pos.x - 3.0).abs() < 1e-4);
244        assert!(!flipped);
245    }
246
247    #[test]
248    fn test_klein_navigation_simple_move() {
249        let mut nav = KleinNavigation::new(10.0, 10.0, Vec2::new(5.0, 5.0));
250        nav.move_by(Vec2::new(1.0, 0.0));
251        assert!((nav.position.x - 6.0).abs() < 1e-4);
252        assert!(!nav.flipped);
253    }
254
255    #[test]
256    fn test_klein_navigation_y_crossing() {
257        let mut nav = KleinNavigation::new(10.0, 10.0, Vec2::new(3.0, 9.0));
258        nav.move_by(Vec2::new(0.0, 2.0)); // crosses top boundary
259        assert!(nav.flipped, "Should be flipped after crossing y boundary");
260    }
261
262    #[test]
263    fn test_orientation_at() {
264        assert_eq!(orientation_at(Vec2::new(0.0, 5.0), 10.0), 1.0);
265        assert_eq!(orientation_at(Vec2::new(0.0, 15.0), 10.0), -1.0);
266        assert_eq!(orientation_at(Vec2::new(0.0, 25.0), 10.0), 1.0);
267    }
268
269    #[test]
270    fn test_klein_bottle_mesh() {
271        let kb = KleinBottle::new(1.0);
272        let mesh = kb.generate_mesh(10, 10);
273        assert_eq!(mesh.len(), 100);
274    }
275
276    #[test]
277    fn test_klein_renderer_boundaries() {
278        let renderer = KleinRenderer::new(10.0, 10.0);
279        let indicators = renderer.boundary_indicators();
280        assert_eq!(indicators.len(), 4);
281        // Top/bottom have flip
282        assert!(indicators[2].2);
283        assert!(indicators[3].2);
284        // Left/right don't
285        assert!(!indicators[0].2);
286        assert!(!indicators[1].2);
287    }
288
289    #[test]
290    fn test_klein_renderer_ghosts() {
291        let renderer = KleinRenderer::new(10.0, 10.0);
292        let positions = vec![Vec2::new(5.0, 1.0)]; // near bottom
293        let ghosts = renderer.ghost_positions(&positions, 2.0);
294        assert!(!ghosts.is_empty());
295        // Ghost should be at top with mirrored x
296        let (ghost_pos, is_flipped) = ghosts[0];
297        assert!(is_flipped);
298        assert!((ghost_pos.x - 5.0).abs() < 1e-4);
299    }
300}