Skip to main content

proof_engine/topology/
mobius.rs

1// topology/mobius.rs — Mobius strip: one-sided non-orientable surface
2
3use glam::Vec3;
4use std::f32::consts::PI;
5
6// ─── Mobius Strip ──────────────────────────────────────────────────────────
7
8/// A Mobius strip parameterized by radius (center circle) and width (strip half-width).
9#[derive(Clone, Debug)]
10pub struct MobiusStrip {
11    pub radius: f32,
12    pub width: f32,
13}
14
15impl MobiusStrip {
16    pub fn new(radius: f32, width: f32) -> Self {
17        Self { radius, width }
18    }
19
20    /// Parametric surface of the Mobius strip.
21    /// u in [0, 2*PI) — around the strip
22    /// v in [-1, 1] — across the strip width
23    pub fn parametric(&self, u: f32, v: f32) -> Vec3 {
24        let half_u = u / 2.0;
25        let r = self.radius + self.width * v * half_u.cos();
26        Vec3::new(
27            r * u.cos(),
28            r * u.sin(),
29            self.width * v * half_u.sin(),
30        )
31    }
32
33    /// Position on the strip given:
34    /// t in [0, 1) — fraction around the strip
35    /// s in [-1, 1] — fraction across the strip width
36    pub fn position_on_strip(&self, t: f32, s: f32) -> Vec3 {
37        let u = 2.0 * PI * t;
38        self.parametric(u, s)
39    }
40
41    /// Surface normal at (u, v).
42    /// The normal flips sign after one full loop (u -> u + 2*PI),
43    /// demonstrating the one-sidedness of the Mobius strip.
44    pub fn normal_at(&self, u: f32, v: f32) -> Vec3 {
45        let eps = 1e-4;
46        let du = (self.parametric(u + eps, v) - self.parametric(u - eps, v)) / (2.0 * eps);
47        let dv = (self.parametric(u, v + eps) - self.parametric(u, v - eps)) / (2.0 * eps);
48        du.cross(dv).normalize()
49    }
50
51    /// Walk a given distance along the center of the strip.
52    /// Returns (position, on_backside).
53    /// After traveling the full circumference (2*PI*radius), you end up on the back side.
54    pub fn walk(&self, distance: f32) -> (Vec3, bool) {
55        let circumference = 2.0 * PI * self.radius;
56        let normalized = distance % (2.0 * circumference);
57        let on_backside = normalized >= circumference;
58        let u = (normalized % circumference) / circumference * 2.0 * PI;
59        let pos = self.parametric(u, 0.0);
60        (pos, on_backside)
61    }
62
63    /// Generate a mesh of the strip for rendering.
64    pub fn generate_mesh(&self, u_steps: usize, v_steps: usize) -> Vec<Vec3> {
65        let mut points = Vec::with_capacity(u_steps * v_steps);
66        for i in 0..u_steps {
67            let u = 2.0 * PI * i as f32 / u_steps as f32;
68            for j in 0..=v_steps {
69                let v = -1.0 + 2.0 * j as f32 / v_steps as f32;
70                points.push(self.parametric(u, v));
71            }
72        }
73        points
74    }
75
76    /// Generate the centerline of the strip.
77    pub fn centerline(&self, steps: usize) -> Vec<Vec3> {
78        (0..steps)
79            .map(|i| {
80                let u = 2.0 * PI * i as f32 / steps as f32;
81                self.parametric(u, 0.0)
82            })
83            .collect()
84    }
85}
86
87// ─── Mobius Navigation ─────────────────────────────────────────────────────
88
89/// Handles movement on a Mobius strip with automatic face switching.
90pub struct MobiusNavigation {
91    pub strip: MobiusStrip,
92    /// Parameter along the strip center, in [0, 2*PI)
93    pub u: f32,
94    /// Parameter across the strip, in [-1, 1]
95    pub v: f32,
96    /// How many times we have gone fully around
97    pub loops: u32,
98}
99
100impl MobiusNavigation {
101    pub fn new(strip: MobiusStrip) -> Self {
102        Self {
103            strip,
104            u: 0.0,
105            v: 0.0,
106            loops: 0,
107        }
108    }
109
110    /// Move along the strip (du) and across it (dv).
111    /// When crossing u = 2*PI, v is negated (you switch to the other side).
112    pub fn move_by(&mut self, du: f32, dv: f32) {
113        self.u += du;
114        self.v += dv;
115        self.v = self.v.clamp(-1.0, 1.0);
116
117        // Handle wrapping around
118        while self.u >= 2.0 * PI {
119            self.u -= 2.0 * PI;
120            self.v = -self.v; // flip across the strip
121            self.loops += 1;
122        }
123        while self.u < 0.0 {
124            self.u += 2.0 * PI;
125            self.v = -self.v;
126            self.loops += 1;
127        }
128    }
129
130    /// Get the current 3D position on the strip.
131    pub fn position(&self) -> Vec3 {
132        self.strip.parametric(self.u, self.v)
133    }
134
135    /// Check if currently on the "back side" (odd number of loops).
136    pub fn on_backside(&self) -> bool {
137        self.loops % 2 != 0
138    }
139
140    /// Get the surface normal at the current position.
141    pub fn current_normal(&self) -> Vec3 {
142        self.strip.normal_at(self.u, self.v)
143    }
144
145    /// Reset to starting position.
146    pub fn reset(&mut self) {
147        self.u = 0.0;
148        self.v = 0.0;
149        self.loops = 0;
150    }
151}
152
153// ─── Tests ─────────────────────────────────────────────────────────────────
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_parametric_center() {
161        let strip = MobiusStrip::new(2.0, 0.5);
162        let p = strip.parametric(0.0, 0.0);
163        // At u=0, v=0: x = radius, y = 0, z = 0
164        assert!((p.x - 2.0).abs() < 1e-4);
165        assert!(p.y.abs() < 1e-4);
166        assert!(p.z.abs() < 1e-4);
167    }
168
169    #[test]
170    fn test_position_on_strip() {
171        let strip = MobiusStrip::new(2.0, 0.5);
172        let p = strip.position_on_strip(0.0, 0.0);
173        assert!((p.x - 2.0).abs() < 1e-4);
174    }
175
176    #[test]
177    fn test_normal_flips_after_one_loop() {
178        let strip = MobiusStrip::new(2.0, 0.5);
179        let n_start = strip.normal_at(0.0, 0.0);
180        // Normal after going almost all the way around
181        let n_end = strip.normal_at(2.0 * PI - 0.001, 0.0);
182        // The normals should point in roughly opposite z-directions,
183        // demonstrating the Mobius twist.
184        // Due to the half-twist, the normal's sign in one component should flip.
185        let dot = n_start.dot(n_end);
186        assert!(dot < 0.0, "Normal should flip after full traversal: dot = {}", dot);
187    }
188
189    #[test]
190    fn test_walk_backside() {
191        let strip = MobiusStrip::new(1.0, 0.3);
192        let circumference = 2.0 * PI * strip.radius;
193
194        let (_, back1) = strip.walk(0.0);
195        assert!(!back1, "Start should not be on backside");
196
197        let (_, back2) = strip.walk(circumference + 0.1);
198        assert!(back2, "Should be on backside after one loop");
199
200        let (_, back3) = strip.walk(2.0 * circumference + 0.1);
201        assert!(!back3, "Should be back to front after two loops");
202    }
203
204    #[test]
205    fn test_mesh_generation() {
206        let strip = MobiusStrip::new(2.0, 0.5);
207        let mesh = strip.generate_mesh(20, 5);
208        assert_eq!(mesh.len(), 20 * 6); // 20 u_steps * (5+1) v_steps
209    }
210
211    #[test]
212    fn test_centerline() {
213        let strip = MobiusStrip::new(2.0, 0.5);
214        let line = strip.centerline(100);
215        assert_eq!(line.len(), 100);
216        // All centerline points should be at roughly distance `radius` from the z-axis
217        for p in &line {
218            let xy_dist = (p.x * p.x + p.y * p.y).sqrt();
219            assert!((xy_dist - 2.0).abs() < 1e-3, "Centerline off: xy_dist = {}", xy_dist);
220        }
221    }
222
223    #[test]
224    fn test_navigation_basic() {
225        let strip = MobiusStrip::new(2.0, 0.5);
226        let mut nav = MobiusNavigation::new(strip);
227        assert!(!nav.on_backside());
228        nav.move_by(1.0, 0.0);
229        assert!((nav.u - 1.0).abs() < 1e-4);
230    }
231
232    #[test]
233    fn test_navigation_loop_flips_v() {
234        let strip = MobiusStrip::new(2.0, 0.5);
235        let mut nav = MobiusNavigation::new(strip);
236        nav.v = 0.3;
237        nav.move_by(2.0 * PI, 0.0); // full loop
238        assert!((nav.v - (-0.3)).abs() < 1e-4, "v should be negated after full loop: {}", nav.v);
239        assert!(nav.on_backside());
240    }
241
242    #[test]
243    fn test_navigation_two_loops_restore() {
244        let strip = MobiusStrip::new(2.0, 0.5);
245        let mut nav = MobiusNavigation::new(strip);
246        nav.v = 0.5;
247        nav.move_by(4.0 * PI, 0.0); // two full loops
248        assert!((nav.v - 0.5).abs() < 1e-4, "v should restore after two loops");
249        assert!(!nav.on_backside());
250    }
251
252    #[test]
253    fn test_navigation_clamp_v() {
254        let strip = MobiusStrip::new(2.0, 0.5);
255        let mut nav = MobiusNavigation::new(strip);
256        nav.move_by(0.0, 5.0); // way beyond edge
257        assert!((nav.v - 1.0).abs() < 1e-4);
258    }
259}