proof_engine/topology/
mobius.rs1use glam::Vec3;
4use std::f32::consts::PI;
5
6#[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 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 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 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 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 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 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
87pub struct MobiusNavigation {
91 pub strip: MobiusStrip,
92 pub u: f32,
94 pub v: f32,
96 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 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 while self.u >= 2.0 * PI {
119 self.u -= 2.0 * PI;
120 self.v = -self.v; 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 pub fn position(&self) -> Vec3 {
132 self.strip.parametric(self.u, self.v)
133 }
134
135 pub fn on_backside(&self) -> bool {
137 self.loops % 2 != 0
138 }
139
140 pub fn current_normal(&self) -> Vec3 {
142 self.strip.normal_at(self.u, self.v)
143 }
144
145 pub fn reset(&mut self) {
147 self.u = 0.0;
148 self.v = 0.0;
149 self.loops = 0;
150 }
151}
152
153#[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 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 let n_end = strip.normal_at(2.0 * PI - 0.001, 0.0);
182 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); }
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 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); 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); 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); assert!((nav.v - 1.0).abs() < 1e-4);
258 }
259}