Skip to main content

proof_engine/topology/
projective.rs

1// topology/projective.rs — Real projective plane RP^2
2
3use glam::Vec2;
4
5// ─── Projective Plane ──────────────────────────────────────────────────────
6
7/// The real projective plane RP^2, modeled as a square with antipodal
8/// boundary identification. Crossing any edge puts you on the opposite edge
9/// with both coordinates inverted (antipodal identification).
10pub struct ProjectivePlane {
11    pub size: f32,
12}
13
14impl ProjectivePlane {
15    pub fn new(size: f32) -> Self {
16        Self { size }
17    }
18
19    /// Wrap a position into the fundamental domain [0, size) x [0, size)
20    /// with antipodal boundary identification.
21    pub fn projective_wrap(&self, pos: Vec2) -> Vec2 {
22        projective_wrap(pos, self.size)
23    }
24}
25
26/// Wrap a position in the projective plane's fundamental domain.
27/// The square [0, size) x [0, size) with opposite edges identified with a flip.
28pub fn projective_wrap(pos: Vec2, size: f32) -> Vec2 {
29    let mut x = pos.x;
30    let mut y = pos.y;
31
32    // Normalize into [0, 2*size) x [0, 2*size) first (the orientation double cover is a torus)
33    let double = 2.0 * size;
34    x = ((x % double) + double) % double;
35    y = ((y % double) + double) % double;
36
37    // If in the second copy, apply antipodal identification
38    if x >= size && y >= size {
39        x -= size;
40        y -= size;
41    } else if x >= size {
42        // wrap: (x, y) ~ (x - size, y + size) if y + size < 2*size
43        x -= size;
44        y += size;
45        if y >= double {
46            y -= double;
47        }
48        // re-check
49        if x >= size || y >= size {
50            x = ((x % size) + size) % size;
51            y = ((y % size) + size) % size;
52        }
53    } else if y >= size {
54        y -= size;
55        x += size;
56        if x >= double {
57            x -= double;
58        }
59        if x >= size || y >= size {
60            x = ((x % size) + size) % size;
61            y = ((y % size) + size) % size;
62        }
63    }
64
65    // Final clamp
66    x = ((x % size) + size) % size;
67    y = ((y % size) + size) % size;
68
69    Vec2::new(x, y)
70}
71
72/// Convert 2D Euclidean coordinates to homogeneous projective coordinates.
73/// (x, y) -> [x, y, 1]
74pub fn homogeneous_coords(x: f32, y: f32) -> [f32; 3] {
75    [x, y, 1.0]
76}
77
78/// Compute the projective line through two points (in homogeneous coordinates).
79/// The line is the cross product of the two points.
80pub fn projective_line(p1: [f32; 3], p2: [f32; 3]) -> [f32; 3] {
81    cross3(p1, p2)
82}
83
84/// Compute the intersection of two projective lines.
85/// In projective geometry, any two distinct lines always intersect (possibly at infinity).
86/// Returns the intersection as an affine point if w != 0, None if the lines are identical.
87pub fn line_intersection(l1: [f32; 3], l2: [f32; 3]) -> Option<Vec2> {
88    let p = cross3(l1, l2);
89    if p[2].abs() < 1e-10 {
90        // Point at infinity (parallel lines in Euclidean sense) or identical lines
91        if p[0].abs() < 1e-10 && p[1].abs() < 1e-10 {
92            return None; // identical lines
93        }
94        // Point at infinity — still a valid projective point but no finite affine representation.
95        // Return a large but finite point in the direction of intersection.
96        let scale = 1e6;
97        let len = (p[0] * p[0] + p[1] * p[1]).sqrt();
98        return Some(Vec2::new(p[0] / len * scale, p[1] / len * scale));
99    }
100    Some(Vec2::new(p[0] / p[2], p[1] / p[2]))
101}
102
103/// Cross product of two 3-vectors (used for homogeneous coordinate operations).
104fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
105    [
106        a[1] * b[2] - a[2] * b[1],
107        a[2] * b[0] - a[0] * b[2],
108        a[0] * b[1] - a[1] * b[0],
109    ]
110}
111
112/// Compute the cross-ratio of four collinear points (a projective invariant).
113/// Given four points on a projective line, the cross-ratio is:
114/// (AC * BD) / (BC * AD)
115/// where AC = distance from A to C, etc.
116/// Points are given as 1D projective coordinates [value, 1] or [1, 0] for infinity.
117pub fn cross_ratio(a: f32, b: f32, c: f32, d: f32) -> f32 {
118    let ac = c - a;
119    let bd = d - b;
120    let bc = c - b;
121    let ad = d - a;
122
123    if bc.abs() < 1e-10 || ad.abs() < 1e-10 {
124        return f32::INFINITY;
125    }
126
127    (ac * bd) / (bc * ad)
128}
129
130/// Cross-ratio of four points given as homogeneous 1D coordinates [x, w].
131pub fn cross_ratio_homogeneous(a: [f32; 2], b: [f32; 2], c: [f32; 2], d: [f32; 2]) -> f32 {
132    // (A,C) = a[0]*c[1] - a[1]*c[0], etc.
133    let ac = a[0] * c[1] - a[1] * c[0];
134    let bd = b[0] * d[1] - b[1] * d[0];
135    let bc = b[0] * c[1] - b[1] * c[0];
136    let ad = a[0] * d[1] - a[1] * d[0];
137
138    if bc.abs() < 1e-10 || ad.abs() < 1e-10 {
139        return f32::INFINITY;
140    }
141
142    (ac * bd) / (bc * ad)
143}
144
145// ─── Tests ─────────────────────────────────────────────────────────────────
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_projective_wrap_inside() {
153        let p = projective_wrap(Vec2::new(3.0, 4.0), 10.0);
154        assert!((p.x - 3.0).abs() < 1e-4);
155        assert!((p.y - 4.0).abs() < 1e-4);
156    }
157
158    #[test]
159    fn test_projective_wrap_result_in_domain() {
160        for ix in -20..20 {
161            for iy in -20..20 {
162                let pos = Vec2::new(ix as f32 * 3.7, iy as f32 * 2.3);
163                let w = projective_wrap(pos, 10.0);
164                assert!(w.x >= -1e-4 && w.x < 10.0 + 1e-4, "x out of range: {}", w.x);
165                assert!(w.y >= -1e-4 && w.y < 10.0 + 1e-4, "y out of range: {}", w.y);
166            }
167        }
168    }
169
170    #[test]
171    fn test_homogeneous_coords() {
172        let h = homogeneous_coords(3.0, 4.0);
173        assert_eq!(h, [3.0, 4.0, 1.0]);
174    }
175
176    #[test]
177    fn test_projective_line_through_points() {
178        let p1 = homogeneous_coords(0.0, 0.0);
179        let p2 = homogeneous_coords(1.0, 1.0);
180        let line = projective_line(p1, p2);
181        // Line y - x = 0 => [1, -1, 0] (up to scale)
182        // Check that both points satisfy the line equation a*x + b*y + c*w = 0
183        let dot1 = line[0] * p1[0] + line[1] * p1[1] + line[2] * p1[2];
184        let dot2 = line[0] * p2[0] + line[1] * p2[1] + line[2] * p2[2];
185        assert!(dot1.abs() < 1e-6);
186        assert!(dot2.abs() < 1e-6);
187    }
188
189    #[test]
190    fn test_line_intersection() {
191        // Line x = 1: [-1, 0, 1] => actually [1, 0, -1] via standard form
192        // Let's use two lines defined by points
193        let l1 = projective_line(homogeneous_coords(0.0, 0.0), homogeneous_coords(1.0, 0.0));
194        let l2 = projective_line(homogeneous_coords(0.5, -1.0), homogeneous_coords(0.5, 1.0));
195        let inter = line_intersection(l1, l2);
196        assert!(inter.is_some());
197        let p = inter.unwrap();
198        assert!((p.x - 0.5).abs() < 1e-3, "x = {}", p.x);
199        assert!(p.y.abs() < 1e-3, "y = {}", p.y);
200    }
201
202    #[test]
203    fn test_line_intersection_parallel() {
204        // Two parallel lines in Euclidean geometry still meet in projective geometry (at infinity)
205        let l1 = [0.0_f32, 1.0, -1.0]; // y = 1
206        let l2 = [0.0_f32, 1.0, -2.0]; // y = 2
207        let inter = line_intersection(l1, l2);
208        // Should return Some (point at infinity along x-axis)
209        assert!(inter.is_some());
210    }
211
212    #[test]
213    fn test_cross_ratio_basic() {
214        // Cross-ratio of (0, 1, 2, 3)
215        let cr = cross_ratio(0.0, 1.0, 2.0, 3.0);
216        // (2-0)(3-1) / ((2-1)(3-0)) = 2*2 / (1*3) = 4/3
217        assert!((cr - 4.0 / 3.0).abs() < 1e-4, "Cross ratio = {}", cr);
218    }
219
220    #[test]
221    fn test_cross_ratio_projective_invariant() {
222        // Cross-ratio should be invariant under projective transformation
223        // Apply Mobius transform: f(x) = (2x + 1) / (x + 1)
224        let transform = |x: f32| (2.0 * x + 1.0) / (x + 1.0);
225
226        let a = 0.0_f32;
227        let b = 1.0;
228        let c = 2.0;
229        let d = 3.0;
230
231        let cr1 = cross_ratio(a, b, c, d);
232        let cr2 = cross_ratio(transform(a), transform(b), transform(c), transform(d));
233        assert!((cr1 - cr2).abs() < 1e-3, "Cross-ratio not invariant: {} vs {}", cr1, cr2);
234    }
235
236    #[test]
237    fn test_cross_ratio_homogeneous() {
238        let cr1 = cross_ratio(0.0, 1.0, 2.0, 3.0);
239        let cr2 = cross_ratio_homogeneous([0.0, 1.0], [1.0, 1.0], [2.0, 1.0], [3.0, 1.0]);
240        assert!((cr1 - cr2).abs() < 1e-4);
241    }
242}