1use glam::Vec2;
4use std::f32::consts::PI;
5
6pub struct PoincareDisk {
11 pub scale: f32,
13}
14
15impl PoincareDisk {
16 pub fn new(scale: f32) -> Self {
17 Self { scale }
18 }
19
20 pub fn to_disk(&self, world_pos: Vec2) -> Vec2 {
23 let p = world_pos / self.scale;
24 let r = p.length();
25 if r < 1e-8 {
26 return Vec2::ZERO;
27 }
28 let disk_r = (r / 2.0).tanh();
29 p.normalize() * disk_r
30 }
31
32 pub fn from_disk(&self, disk_pos: Vec2) -> Vec2 {
35 let r = disk_pos.length();
36 if r < 1e-8 {
37 return Vec2::ZERO;
38 }
39 if r >= 1.0 {
40 let clamped_r = 0.9999;
42 let world_r = 2.0 * atanh(clamped_r);
43 return disk_pos.normalize() * world_r * self.scale;
44 }
45 let world_r = 2.0 * atanh(r);
46 disk_pos.normalize() * world_r * self.scale
47 }
48
49 pub fn hyperbolic_distance(&self, a: Vec2, b: Vec2) -> f32 {
52 let a_sq = a.length_squared();
53 let b_sq = b.length_squared();
54 let diff_sq = (a - b).length_squared();
55
56 let denom = (1.0 - a_sq) * (1.0 - b_sq);
57 if denom <= 0.0 {
58 return f32::INFINITY;
59 }
60 let arg = 1.0 + 2.0 * diff_sq / denom;
61 acosh(arg)
62 }
63
64 pub fn geodesic(&self, a: Vec2, b: Vec2, steps: usize) -> Vec<Vec2> {
68 if steps < 2 {
69 return vec![a, b];
70 }
71
72 let mut result = Vec::with_capacity(steps);
74 let neg_a = -a;
76 let b_mapped = mobius_transform_impl(b, neg_a);
77
78 for i in 0..steps {
80 let t = i as f32 / (steps - 1) as f32;
81 let point_on_line = b_mapped * t;
82 result.push(mobius_transform_impl(point_on_line, a));
84 }
85 result
86 }
87
88 pub fn mobius_transform(&self, z: Vec2, a: Vec2) -> Vec2 {
92 mobius_transform_impl(z, a)
93 }
94}
95
96fn complex_mul(a: Vec2, b: Vec2) -> Vec2 {
98 Vec2::new(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x)
99}
100
101fn complex_conj(a: Vec2) -> Vec2 {
103 Vec2::new(a.x, -a.y)
104}
105
106fn complex_div(a: Vec2, b: Vec2) -> Vec2 {
108 let denom = b.x * b.x + b.y * b.y;
109 if denom < 1e-12 {
110 return Vec2::ZERO;
111 }
112 Vec2::new(
113 (a.x * b.x + a.y * b.y) / denom,
114 (a.y * b.x - a.x * b.y) / denom,
115 )
116}
117
118fn mobius_transform_impl(z: Vec2, a: Vec2) -> Vec2 {
120 let numerator = z + a;
121 let denominator = Vec2::new(1.0, 0.0) + complex_mul(complex_conj(a), z);
122 complex_div(numerator, denominator)
123}
124
125fn atanh(x: f32) -> f32 {
126 0.5 * ((1.0 + x) / (1.0 - x)).ln()
127}
128
129fn acosh(x: f32) -> f32 {
130 (x + (x * x - 1.0).sqrt()).ln()
131}
132
133pub struct KleinDisk;
138
139impl KleinDisk {
140 pub fn to_klein(&self, pos: Vec2) -> Vec2 {
143 let r = pos.length();
144 if r < 1e-8 {
145 return Vec2::ZERO;
146 }
147 let klein_r = (r / 2.0).tanh() * 2.0 / (1.0 + (r / 2.0).tanh().powi(2));
148 let klein_r = r.tanh();
150 pos.normalize() * klein_r
151 }
152
153 pub fn from_klein(&self, pos: Vec2) -> Vec2 {
155 let r = pos.length();
156 if r < 1e-8 {
157 return Vec2::ZERO;
158 }
159 if r >= 1.0 {
160 return pos.normalize() * 10.0; }
162 let world_r = atanh(r);
163 pos.normalize() * world_r
164 }
165
166 pub fn poincare_to_klein(pos: Vec2) -> Vec2 {
169 let r_sq = pos.length_squared();
170 pos * (2.0 / (1.0 + r_sq))
171 }
172
173 pub fn klein_to_poincare(pos: Vec2) -> Vec2 {
176 let r_sq = pos.length_squared();
177 if r_sq >= 1.0 {
178 return pos.normalize() * 0.9999;
179 }
180 let factor = 1.0 + (1.0 - r_sq).sqrt();
181 pos / factor
182 }
183}
184
185#[derive(Clone, Debug)]
189pub struct HyperbolicPolygon {
190 pub vertices: Vec<Vec2>,
191 pub center: Vec2,
192}
193
194pub struct HyperbolicTiling;
198
199impl HyperbolicTiling {
200 pub fn generate(p: usize, q: usize, depth: usize) -> Vec<HyperbolicPolygon> {
203 if p < 3 || q < 3 {
204 return vec![];
205 }
206 if (p - 2) * (q - 2) <= 4 {
208 return vec![]; }
210
211 let mut polygons = Vec::new();
212 let mut visited_centers: Vec<Vec2> = Vec::new();
213
214 let central_radius = central_polygon_radius(p, q);
216
217 let central = create_regular_polygon(Vec2::ZERO, central_radius, p, 0.0);
219 polygons.push(central.clone());
220 visited_centers.push(Vec2::ZERO);
221
222 if depth == 0 {
223 return polygons;
224 }
225
226 let mut frontier: Vec<HyperbolicPolygon> = vec![central];
228
229 for _layer in 0..depth {
230 let mut next_frontier = Vec::new();
231 for poly in &frontier {
232 let n = poly.vertices.len();
234 for i in 0..n {
235 let v1 = poly.vertices[i];
236 let v2 = poly.vertices[(i + 1) % n];
237 let edge_mid = (v1 + v2) * 0.5;
238
239 let neighbor_center = reflect_through_geodesic(poly.center, v1, v2);
241
242 if neighbor_center.length() >= 0.98 {
243 continue; }
245
246 let dominated = visited_centers.iter().any(|c| (*c - neighbor_center).length() < 0.01);
248 if dominated {
249 continue;
250 }
251
252 let new_poly = create_polygon_at(neighbor_center, central_radius, p, v1, v2);
255 visited_centers.push(neighbor_center);
256 polygons.push(new_poly.clone());
257 next_frontier.push(new_poly);
258 }
259 }
260 frontier = next_frontier;
261 }
262
263 polygons
264 }
265}
266
267fn central_polygon_radius(p: usize, q: usize) -> f32 {
269 let pi_p = PI / p as f32;
270 let pi_q = PI / q as f32;
271 let cosh_r = pi_q.cos() / pi_p.sin();
274 let hyp_r = acosh(cosh_r);
275 (hyp_r / 2.0).tanh()
277}
278
279fn create_regular_polygon(center: Vec2, radius: f32, p: usize, rotation: f32) -> HyperbolicPolygon {
281 let mut vertices = Vec::with_capacity(p);
282 for i in 0..p {
283 let angle = rotation + 2.0 * PI * i as f32 / p as f32;
284 let vertex_local = Vec2::new(angle.cos(), angle.sin()) * radius;
285 let vertex = mobius_transform_impl(vertex_local, center);
287 vertices.push(vertex);
288 }
289 HyperbolicPolygon { vertices, center }
290}
291
292fn create_polygon_at(center: Vec2, radius: f32, p: usize, v1: Vec2, v2: Vec2) -> HyperbolicPolygon {
294 let edge_mid = (v1 + v2) * 0.5;
296 let to_edge = edge_mid - center;
297 let base_angle = to_edge.y.atan2(to_edge.x);
298 let rotation = base_angle - PI / p as f32;
299
300 create_regular_polygon(center, radius, p, rotation)
301}
302
303fn reflect_through_geodesic(p: Vec2, a: Vec2, b: Vec2) -> Vec2 {
305 let neg_a = -a;
307 let b_m = mobius_transform_impl(b, neg_a);
308 let p_m = mobius_transform_impl(p, neg_a);
309
310 let angle = b_m.y.atan2(b_m.x);
313 let cos2 = (2.0 * angle).cos();
314 let sin2 = (2.0 * angle).sin();
315 let reflected = Vec2::new(
316 p_m.x * cos2 + p_m.y * sin2,
317 p_m.x * sin2 - p_m.y * cos2,
318 );
319
320 mobius_transform_impl(reflected, a)
322}
323
324pub struct HyperbolicGrid {
329 pub disk: PoincareDisk,
330 pub grid_lines: usize,
331}
332
333impl HyperbolicGrid {
334 pub fn new(scale: f32, grid_lines: usize) -> Self {
335 Self {
336 disk: PoincareDisk::new(scale),
337 grid_lines,
338 }
339 }
340
341 pub fn generate_grid(&self) -> Vec<(Vec2, Vec2)> {
344 let mut segments = Vec::new();
345 let n = self.grid_lines;
346 let step = 2.0 * self.disk.scale / n as f32;
347
348 for j in 0..=n {
350 let y = -self.disk.scale + j as f32 * step;
351 let mut prev = None;
352 for i in 0..=n * 4 {
353 let x = -self.disk.scale + i as f32 * step / 4.0;
354 let world = Vec2::new(x, y);
355 let disk = self.disk.to_disk(world);
356 if disk.length() < 0.99 {
357 if let Some(p) = prev {
358 segments.push((p, disk));
359 }
360 prev = Some(disk);
361 } else {
362 prev = None;
363 }
364 }
365 }
366
367 for i in 0..=n {
369 let x = -self.disk.scale + i as f32 * step;
370 let mut prev = None;
371 for j in 0..=n * 4 {
372 let y = -self.disk.scale + j as f32 * step / 4.0;
373 let world = Vec2::new(x, y);
374 let disk = self.disk.to_disk(world);
375 if disk.length() < 0.99 {
376 if let Some(p) = prev {
377 segments.push((p, disk));
378 }
379 prev = Some(disk);
380 } else {
381 prev = None;
382 }
383 }
384 }
385
386 segments
387 }
388
389 pub fn transform_entity(&self, world_pos: Vec2) -> Vec2 {
391 self.disk.to_disk(world_pos)
392 }
393
394 pub fn transform_entities(&self, positions: &[Vec2]) -> Vec<Vec2> {
396 positions.iter().map(|p| self.disk.to_disk(*p)).collect()
397 }
398}
399
400#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_poincare_roundtrip() {
408 let disk = PoincareDisk::new(10.0);
409 let world = Vec2::new(3.0, 4.0);
410 let d = disk.to_disk(world);
411 let back = disk.from_disk(d);
412 assert!((world - back).length() < 0.01, "Roundtrip failed: {:?} vs {:?}", world, back);
413 }
414
415 #[test]
416 fn test_poincare_origin() {
417 let disk = PoincareDisk::new(1.0);
418 let d = disk.to_disk(Vec2::ZERO);
419 assert!(d.length() < 1e-6);
420 }
421
422 #[test]
423 fn test_disk_points_inside_unit_circle() {
424 let disk = PoincareDisk::new(5.0);
425 for x in -10..=10 {
426 for y in -10..=10 {
427 let d = disk.to_disk(Vec2::new(x as f32, y as f32));
428 assert!(d.length() < 1.0 + 1e-6, "Point outside disk: {:?}", d);
429 }
430 }
431 }
432
433 #[test]
434 fn test_hyperbolic_distance_zero() {
435 let disk = PoincareDisk::new(1.0);
436 let a = Vec2::new(0.3, 0.2);
437 let d = disk.hyperbolic_distance(a, a);
438 assert!(d.abs() < 1e-4, "Self-distance should be ~0, got {}", d);
439 }
440
441 #[test]
442 fn test_hyperbolic_distance_symmetry() {
443 let disk = PoincareDisk::new(1.0);
444 let a = Vec2::new(0.3, 0.1);
445 let b = Vec2::new(-0.2, 0.4);
446 let d1 = disk.hyperbolic_distance(a, b);
447 let d2 = disk.hyperbolic_distance(b, a);
448 assert!((d1 - d2).abs() < 1e-4, "Distance not symmetric: {} vs {}", d1, d2);
449 }
450
451 #[test]
452 fn test_mobius_preserves_distance() {
453 let disk = PoincareDisk::new(1.0);
454 let a = Vec2::new(0.2, 0.1);
455 let b = Vec2::new(-0.1, 0.3);
456 let c = Vec2::new(0.15, -0.05);
457
458 let d_before = disk.hyperbolic_distance(a, b);
459 let a_t = disk.mobius_transform(a, c);
460 let b_t = disk.mobius_transform(b, c);
461 let d_after = disk.hyperbolic_distance(a_t, b_t);
462
463 assert!((d_before - d_after).abs() < 0.05,
464 "Mobius should preserve distance: {} vs {}", d_before, d_after);
465 }
466
467 #[test]
468 fn test_geodesic_endpoints() {
469 let disk = PoincareDisk::new(1.0);
470 let a = Vec2::new(0.2, 0.1);
471 let b = Vec2::new(-0.3, 0.2);
472 let path = disk.geodesic(a, b, 20);
473 assert!((path[0] - a).length() < 1e-4);
474 assert!((path[path.len() - 1] - b).length() < 0.05);
475 }
476
477 #[test]
478 fn test_klein_poincare_roundtrip() {
479 let p = Vec2::new(0.3, 0.2);
480 let k = KleinDisk::poincare_to_klein(p);
481 let back = KleinDisk::klein_to_poincare(k);
482 assert!((p - back).length() < 1e-4, "Klein-Poincare roundtrip failed");
483 }
484
485 #[test]
486 fn test_hyperbolic_tiling_basic() {
487 let polygons = HyperbolicTiling::generate(5, 4, 1);
489 assert!(!polygons.is_empty(), "Tiling should produce polygons");
490 for poly in &polygons {
491 assert_eq!(poly.vertices.len(), 5);
492 for v in &poly.vertices {
493 assert!(v.length() < 1.0 + 1e-3, "Vertex outside disk");
494 }
495 }
496 }
497
498 #[test]
499 fn test_hyperbolic_tiling_not_hyperbolic() {
500 let polygons = HyperbolicTiling::generate(4, 4, 2);
502 assert!(polygons.is_empty());
503 }
504
505 #[test]
506 fn test_hyperbolic_grid_generation() {
507 let grid = HyperbolicGrid::new(5.0, 4);
508 let segments = grid.generate_grid();
509 assert!(!segments.is_empty());
510 for (a, b) in &segments {
511 assert!(a.length() < 1.0);
512 assert!(b.length() < 1.0);
513 }
514 }
515
516 #[test]
517 fn test_complex_operations() {
518 let a = Vec2::new(1.0, 2.0);
519 let b = Vec2::new(3.0, 4.0);
520 let prod = complex_mul(a, b);
521 assert!((prod.x - (-5.0)).abs() < 1e-6);
523 assert!((prod.y - 10.0).abs() < 1e-6);
524 }
525}