Skip to main content

exact_poly/
primitives.rs

1//! Fundamental integer geometry primitives.
2//! All coordinates are i64 (fixed-point). All intermediate computations use i128.
3//! ZERO floating point. ZERO epsilon. Exact integer arithmetic throughout.
4
5/// Orientation of three points.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Orientation {
8    CounterClockwise,
9    Clockwise,
10    Collinear,
11}
12
13/// 2D cross product of vectors (A→B) × (A→C).
14/// Equivalent to: (bx-ax)*(cy-ay) - (by-ay)*(cx-ax)
15/// Positive = CCW (left turn), Negative = CW (right turn), Zero = collinear.
16/// Casts to i128 before multiply — no overflow for coords up to MAX_WORLD=40_075_017_000_000.
17pub fn cross2d(ax: i64, ay: i64, bx: i64, by: i64, cx: i64, cy: i64) -> i128 {
18    let dx1 = (bx as i128) - (ax as i128);
19    let dy1 = (by as i128) - (ay as i128);
20    let dx2 = (cx as i128) - (ax as i128);
21    let dy2 = (cy as i128) - (ay as i128);
22
23    dx1 * dy2 - dy1 * dx2
24}
25
26/// Orientation of three points A, B, C.
27pub fn orientation(ax: i64, ay: i64, bx: i64, by: i64, cx: i64, cy: i64) -> Orientation {
28    match cross2d(ax, ay, bx, by, cx, cy).cmp(&0) {
29        std::cmp::Ordering::Greater => Orientation::CounterClockwise,
30        std::cmp::Ordering::Less => Orientation::Clockwise,
31        std::cmp::Ordering::Equal => Orientation::Collinear,
32    }
33}
34
35/// True if P is strictly left of line A→B (cross product > 0 for CCW polygon).
36pub fn is_left(ax: i64, ay: i64, bx: i64, by: i64, px: i64, py: i64) -> bool {
37    cross2d(ax, ay, bx, by, px, py) > 0
38}
39
40/// True if P is strictly left of or on line A→B.
41pub fn is_left_or_on(ax: i64, ay: i64, bx: i64, by: i64, px: i64, py: i64) -> bool {
42    cross2d(ax, ay, bx, by, px, py) >= 0
43}
44
45/// True if P is strictly right of line A→B.
46pub fn is_right(ax: i64, ay: i64, bx: i64, by: i64, px: i64, py: i64) -> bool {
47    cross2d(ax, ay, bx, by, px, py) < 0
48}
49
50/// True if P is strictly right of or on line A→B.
51pub fn is_right_or_on(ax: i64, ay: i64, bx: i64, by: i64, px: i64, py: i64) -> bool {
52    cross2d(ax, ay, bx, by, px, py) <= 0
53}
54
55/// True if P is collinear with A and B (cross product == 0).
56pub fn is_collinear_pts(ax: i64, ay: i64, bx: i64, by: i64, px: i64, py: i64) -> bool {
57    cross2d(ax, ay, bx, by, px, py) == 0
58}
59
60/// True if vertex `curr` is a reflex vertex in a CCW polygon.
61/// A reflex vertex has a CW turn (cross product < 0) in a CCW polygon.
62/// prev → curr → next should be a left turn for convex, right turn for reflex.
63pub fn is_reflex(
64    prev_x: i64,
65    prev_y: i64,
66    curr_x: i64,
67    curr_y: i64,
68    next_x: i64,
69    next_y: i64,
70) -> bool {
71    cross2d(prev_x, prev_y, curr_x, curr_y, next_x, next_y) < 0
72}
73
74/// Squared Euclidean distance between two points (using i128 to avoid overflow).
75/// Returns dx² + dy². Used for edge length validation (compare against MIN_EDGE_LENGTH_SQUARED).
76pub fn edge_squared_length(ax: i64, ay: i64, bx: i64, by: i64) -> u128 {
77    let dx = (bx as i128) - (ax as i128);
78    let dy = (by as i128) - (ay as i128);
79
80    (dx * dx + dy * dy) as u128
81}
82
83/// True if point P lies on segment A→B (collinear AND within the bounding box of A→B).
84pub fn point_on_segment(px: i64, py: i64, ax: i64, ay: i64, bx: i64, by: i64) -> bool {
85    if cross2d(ax, ay, bx, by, px, py) != 0 {
86        return false;
87    }
88
89    px >= ax.min(bx) && px <= ax.max(bx) && py >= ay.min(by) && py <= ay.max(by)
90}
91
92/// True if segments A1→A2 and B1→B2 properly intersect (cross each other, not collinear).
93/// Proper intersection: each segment straddles the other's line (both endpoints on opposite sides).
94/// Does NOT count T-junctions (endpoint on segment) as proper intersection.
95pub fn segments_properly_intersect(
96    a1x: i64,
97    a1y: i64,
98    a2x: i64,
99    a2y: i64,
100    b1x: i64,
101    b1y: i64,
102    b2x: i64,
103    b2y: i64,
104) -> bool {
105    let d1 = cross2d(b1x, b1y, b2x, b2y, a1x, a1y);
106    let d2 = cross2d(b1x, b1y, b2x, b2y, a2x, a2y);
107    let d3 = cross2d(a1x, a1y, a2x, a2y, b1x, b1y);
108    let d4 = cross2d(a1x, a1y, a2x, a2y, b2x, b2y);
109
110    ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))
111}
112
113/// True if segments A1→A2 and B1→B2 intersect (proper crossing OR endpoint on segment).
114pub fn segments_intersect(
115    a1x: i64,
116    a1y: i64,
117    a2x: i64,
118    a2y: i64,
119    b1x: i64,
120    b1y: i64,
121    b2x: i64,
122    b2y: i64,
123) -> bool {
124    if segments_properly_intersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) {
125        return true;
126    }
127
128    point_on_segment(b1x, b1y, a1x, a1y, a2x, a2y)
129        || point_on_segment(b2x, b2y, a1x, a1y, a2x, a2y)
130        || point_on_segment(a1x, a1y, b1x, b1y, b2x, b2y)
131        || point_on_segment(a2x, a2y, b1x, b1y, b2x, b2y)
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    const M: i64 = 1_000_000;
139    const MAX_WORLD: i64 = 40_075_017_000_000;
140
141    #[test]
142    fn cross2d_sign_and_collinearity_cases() {
143        assert!(cross2d(0, 0, M, 0, 0, M) > 0);
144        assert!(cross2d(0, 0, 0, M, M, 0) < 0);
145        assert_eq!(cross2d(0, 0, M, 0, 2 * M, 0), 0);
146    }
147
148    #[test]
149    fn cross2d_matches_expected_magnitude() {
150        assert_eq!(
151            cross2d(0, 0, 3 * M, 0, 0, 4 * M),
152            12 * (M as i128) * (M as i128)
153        );
154        assert_eq!(cross2d(-M, -M, M, -M, -M, M), 4 * (M as i128) * (M as i128));
155        assert_eq!(
156            cross2d(5 * M, 5 * M, 6 * M, 7 * M, 9 * M, 12 * M),
157            -(M as i128) * (M as i128)
158        );
159    }
160
161    #[test]
162    fn cross2d_handles_max_world_without_overflow() {
163        let result = cross2d(0, 0, MAX_WORLD, 0, 0, MAX_WORLD);
164        let expected = (MAX_WORLD as i128) * (MAX_WORLD as i128);
165        assert_eq!(result, expected);
166        assert!(cross2d(-MAX_WORLD, 0, MAX_WORLD, 0, 0, MAX_WORLD) > 0);
167        assert_eq!(
168            cross2d(0, 0, MAX_WORLD, MAX_WORLD, 2 * MAX_WORLD, 2 * MAX_WORLD),
169            0
170        );
171    }
172
173    #[test]
174    fn orientation_classifies_three_cases() {
175        assert_eq!(orientation(0, 0, M, 0, 0, M), Orientation::CounterClockwise);
176        assert_eq!(orientation(0, 0, 0, M, M, 0), Orientation::Clockwise);
177        assert_eq!(
178            orientation(0, 0, M, M, 2 * M, 2 * M),
179            Orientation::Collinear
180        );
181    }
182
183    #[test]
184    fn orientation_handles_shifted_and_negative_points() {
185        assert_eq!(
186            orientation(-M, -M, M, -M, 0, M),
187            Orientation::CounterClockwise
188        );
189        assert_eq!(orientation(-M, -M, 0, M, M, -M), Orientation::Clockwise);
190        assert_eq!(
191            orientation(-2 * M, -2 * M, -M, -M, 0, 0),
192            Orientation::Collinear
193        );
194    }
195
196    #[test]
197    fn orientation_handles_degenerate_repeated_points() {
198        assert_eq!(orientation(0, 0, 0, 0, M, M), Orientation::Collinear);
199        assert_eq!(orientation(0, 0, M, M, M, M), Orientation::Collinear);
200        assert_eq!(orientation(7, 11, 7, 11, 7, 11), Orientation::Collinear);
201    }
202
203    #[test]
204    fn side_predicates_classify_left_right_and_on() {
205        assert!(is_left(0, 0, M, 0, 0, M));
206        assert!(!is_left(0, 0, M, 0, M, 0));
207        assert!(!is_left(0, 0, M, 0, 0, -M));
208
209        assert!(is_left_or_on(0, 0, M, 0, 0, M));
210        assert!(is_left_or_on(0, 0, M, 0, M, 0));
211        assert!(!is_left_or_on(0, 0, M, 0, 0, -M));
212
213        assert!(is_right(0, 0, M, 0, 0, -M));
214        assert!(!is_right(0, 0, M, 0, M, 0));
215        assert!(!is_right(0, 0, M, 0, 0, M));
216
217        assert!(is_right_or_on(0, 0, M, 0, 0, -M));
218        assert!(is_right_or_on(0, 0, M, 0, M, 0));
219        assert!(!is_right_or_on(0, 0, M, 0, 0, M));
220
221        assert!(is_collinear_pts(0, 0, M, M, 2 * M, 2 * M));
222        assert!(is_collinear_pts(0, 0, 2 * M, 0, M, 0));
223        assert!(!is_collinear_pts(0, 0, M, 0, M, M));
224    }
225
226    #[test]
227    fn side_predicates_work_with_vertical_lines() {
228        assert!(is_left(0, 0, 0, M, -M, 0));
229        assert!(is_left_or_on(0, 0, 0, M, 0, M / 2));
230        assert!(is_right(0, 0, 0, M, M, 0));
231        assert!(is_right_or_on(0, 0, 0, M, 0, M / 2));
232        assert!(is_collinear_pts(0, 0, 0, 2 * M, 0, M));
233    }
234
235    #[test]
236    fn side_predicates_work_with_negative_coordinates() {
237        assert!(is_left(-M, -M, M, -M, 0, 0));
238        assert!(is_left_or_on(-M, -M, M, -M, -M, -M));
239        assert!(is_right(-M, -M, M, -M, 0, -2 * M));
240        assert!(is_right_or_on(-M, -M, M, -M, M, -M));
241        assert!(!is_collinear_pts(-M, -M, M, -M, 0, 0));
242    }
243
244    #[test]
245    fn is_reflex_distinguishes_concave_and_convex_vertices() {
246        assert!(is_reflex(0, 0, M, M, 2 * M, 0));
247        assert!(!is_reflex(0, 2 * M, 0, 0, 2 * M, 0));
248        assert!(!is_reflex(0, 0, M, 0, 2 * M, 0));
249    }
250
251    #[test]
252    fn is_reflex_handles_shifted_vertices() {
253        assert!(is_reflex(-2 * M, -2 * M, -M, -M, 0, -2 * M));
254        assert!(!is_reflex(-2 * M, -2 * M, -M, -2 * M, -M, -M));
255        assert!(!is_reflex(5 * M, 5 * M, 6 * M, 6 * M, 7 * M, 7 * M));
256    }
257
258    #[test]
259    fn is_reflex_uses_exact_orientation_semantics() {
260        assert_eq!(is_reflex(0, 0, M, 0, M, M), false);
261        assert_eq!(is_reflex(0, 0, M, 0, M, -M), true);
262        assert_eq!(is_reflex(0, 0, M, 0, 2 * M, 0), false);
263    }
264
265    #[test]
266    fn edge_squared_length_matches_known_distances() {
267        assert_eq!(
268            edge_squared_length(0, 0, 3 * M, 4 * M),
269            25 * (M as u128) * (M as u128)
270        );
271        assert_eq!(edge_squared_length(0, 0, M, 0), (M as u128) * (M as u128));
272        assert_eq!(
273            edge_squared_length(-M, -M, M, M),
274            8 * (M as u128) * (M as u128)
275        );
276    }
277
278    #[test]
279    fn edge_squared_length_is_symmetric_and_zero_for_same_point() {
280        let ab = edge_squared_length(-7, 11, 13, -17);
281        let ba = edge_squared_length(13, -17, -7, 11);
282        assert_eq!(ab, ba);
283        assert_eq!(edge_squared_length(5, 5, 5, 5), 0);
284        assert_eq!(
285            edge_squared_length(-M, 0, -M, 3 * M),
286            9 * (M as u128) * (M as u128)
287        );
288    }
289
290    #[test]
291    fn edge_squared_length_handles_max_world() {
292        let len_sq = edge_squared_length(0, 0, MAX_WORLD, MAX_WORLD);
293        assert_eq!(len_sq, 2 * (MAX_WORLD as u128) * (MAX_WORLD as u128));
294        assert!(len_sq > 0);
295        assert_eq!(
296            edge_squared_length(-MAX_WORLD, 0, MAX_WORLD, 0),
297            4 * (MAX_WORLD as u128) * (MAX_WORLD as u128)
298        );
299    }
300
301    #[test]
302    fn point_on_segment_handles_midpoint_endpoint_and_outside() {
303        assert!(point_on_segment(M, 0, 0, 0, 2 * M, 0));
304        assert!(point_on_segment(0, 0, 0, 0, 2 * M, 0));
305        assert!(!point_on_segment(3 * M, 0, 0, 0, 2 * M, 0));
306    }
307
308    #[test]
309    fn point_on_segment_handles_diagonal_and_vertical_segments() {
310        assert!(point_on_segment(M, M, 0, 0, 2 * M, 2 * M));
311        assert!(point_on_segment(0, M, 0, 0, 0, 2 * M));
312        assert!(!point_on_segment(M, M + 1, 0, 0, 2 * M, 2 * M));
313    }
314
315    #[test]
316    fn point_on_segment_handles_reversed_bounds() {
317        assert!(point_on_segment(M, 0, 2 * M, 0, 0, 0));
318        assert!(point_on_segment(0, M, 0, 2 * M, 0, 0));
319        assert!(!point_on_segment(-1, 0, 2 * M, 0, 0, 0));
320    }
321
322    #[test]
323    fn segments_properly_intersect_detects_crossings() {
324        assert!(segments_properly_intersect(
325            0,
326            0,
327            2 * M,
328            2 * M,
329            0,
330            2 * M,
331            2 * M,
332            0
333        ));
334        assert!(segments_properly_intersect(0, 0, 3 * M, 0, M, -M, M, M));
335        assert!(segments_properly_intersect(-M, -M, M, M, -M, M, M, -M));
336    }
337
338    #[test]
339    fn segments_properly_intersect_excludes_parallel_touching_and_collinear_cases() {
340        assert!(!segments_properly_intersect(0, 0, 2 * M, 0, 0, M, 2 * M, M));
341        assert!(!segments_properly_intersect(0, 0, 2 * M, 0, M, 0, M, 2 * M));
342        assert!(!segments_properly_intersect(0, 0, 2 * M, 0, M, 0, 3 * M, 0));
343    }
344
345    #[test]
346    fn segments_properly_intersect_excludes_separated_segments() {
347        assert!(!segments_properly_intersect(
348            0,
349            0,
350            M,
351            M,
352            2 * M,
353            2 * M,
354            3 * M,
355            3 * M
356        ));
357        assert!(!segments_properly_intersect(0, 0, 0, M, M, 0, M, M));
358        assert!(!segments_properly_intersect(
359            -2 * M,
360            0,
361            -M,
362            0,
363            M,
364            0,
365            2 * M,
366            0
367        ));
368    }
369
370    #[test]
371    fn segments_intersect_detects_proper_and_endpoint_intersections() {
372        assert!(segments_intersect(0, 0, 2 * M, 2 * M, 0, 2 * M, 2 * M, 0));
373        assert!(segments_intersect(0, 0, 2 * M, 0, M, 0, M, 2 * M));
374        assert!(segments_intersect(0, 0, 2 * M, 0, 2 * M, 0, 3 * M, 0));
375    }
376
377    #[test]
378    fn segments_intersect_detects_collinear_overlap_and_containment() {
379        assert!(segments_intersect(0, 0, 4 * M, 0, M, 0, 3 * M, 0));
380        assert!(segments_intersect(M, 0, 3 * M, 0, 0, 0, 4 * M, 0));
381        assert!(segments_intersect(0, 0, 0, 4 * M, 0, M, 0, 3 * M));
382    }
383
384    #[test]
385    fn segments_intersect_excludes_disjoint_segments() {
386        assert!(!segments_intersect(0, 0, M, 0, 2 * M, 0, 3 * M, 0));
387        assert!(!segments_intersect(0, 0, 0, M, M, 0, M, M));
388        assert!(!segments_intersect(
389            -3 * M,
390            -3 * M,
391            -2 * M,
392            -2 * M,
393            2 * M,
394            2 * M,
395            3 * M,
396            3 * M
397        ));
398    }
399}