Skip to main content

exact_poly/
area.rs

1/// Exact twice-area for a ring, using i128 shoelace.
2///
3/// Returns `|2·signed_area|` as `u128`. For any ring whose vertices fit in
4/// `u64` (the on-chain domain used by `polygon.move::part_twice_area_fp2`),
5/// this function returns **exactly the same value** as the Move
6/// implementation — the positive/negative split in Move and the signed
7/// accumulation here are algebraically identical on non-negative inputs.
8///
9/// Unlike the previous `u128`-only port, this version also handles negative
10/// `i64` coordinates correctly. Casting a negative `i64` to `u128` is a
11/// sign-extension (e.g. `-91i64 as u128 == 2^128 - 91`), which silently
12/// corrupted every product in the shoelace sum. Using `i128` intermediates
13/// avoids that class of bug entirely while keeping Move parity for legal
14/// on-chain polygons.
15///
16/// Overflow safety: for `|x|, |y| ≤ MAX_WORLD ≈ 4·10^13 ≈ 2^45`, each
17/// product `x·y` is at most `2^90`, and with any realistic vertex count the
18/// accumulated sum stays far below `i128::MAX ≈ 1.7·10^38`.
19pub fn twice_area_fp2(ring: &[[i64; 2]]) -> u128 {
20    let n = ring.len();
21    if n < 3 {
22        return 0;
23    }
24
25    let mut signed: i128 = 0;
26    for i in 0..n {
27        let j = if i + 1 == n { 0 } else { i + 1 };
28        let xi = ring[i][0] as i128;
29        let yi = ring[i][1] as i128;
30        let xj = ring[j][0] as i128;
31        let yj = ring[j][1] as i128;
32        signed += xi * yj - xj * yi;
33    }
34
35    signed.unsigned_abs()
36}
37
38/// Lossy display conversion from fixed-point-squared to whole square meters.
39pub fn area_display(twice_area: u128, area_divisor: u128) -> u64 {
40    if area_divisor == 0 {
41        return twice_area as u64;
42    }
43    (twice_area / area_divisor) as u64
44}
45
46/// Exact area conservation check for part decompositions.
47pub fn areas_conserved(original: u128, parts: &[u128]) -> bool {
48    let sum: u128 = parts.iter().sum();
49    sum == original
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    const M: i64 = 1_000_000;
57
58    #[test]
59    fn square_10m_twice_area_matches_move() {
60        let ring = vec![[0, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
61        let area = twice_area_fp2(&ring);
62        assert_eq!(
63            area, 200_000_000_000_000_u128,
64            "square area mismatch: got {area}"
65        );
66    }
67
68    #[test]
69    fn triangle_twice_area_correct() {
70        let ring = vec![[0, 0], [10 * M, 0], [0, 10 * M]];
71        let area = twice_area_fp2(&ring);
72        assert_eq!(area, 100_000_000_000_000_u128);
73    }
74
75    #[test]
76    fn area_display_converts_correctly() {
77        let twice_area = 200_000_000_000_000_u128;
78        assert_eq!(
79            area_display(twice_area, crate::constants::AREA_DIVISOR),
80            100
81        );
82    }
83
84    #[test]
85    fn area_display_1m_x_1m_is_1_m2() {
86        let ring = vec![[0, 0], [M, 0], [M, M], [0, M]];
87        let twice_area = twice_area_fp2(&ring);
88        assert_eq!(twice_area, 2_000_000_000_000_u128);
89        assert_eq!(area_display(twice_area, crate::constants::AREA_DIVISOR), 1);
90    }
91
92    #[test]
93    fn twice_area_fp2_triangle_with_diagonal_edge() {
94        let ring = vec![[0, 0], [3, 0], [0, 4]];
95        assert_eq!(twice_area_fp2(&ring), 12);
96    }
97
98    #[test]
99    fn twice_area_fp2_l_shape_matches_expected() {
100        let ring = vec![[0, 0], [4, 0], [4, 2], [2, 2], [2, 4], [0, 4]];
101        assert_eq!(twice_area_fp2(&ring), 24);
102    }
103
104    #[test]
105    fn twice_area_fp2_degenerate_two_vertices_is_zero() {
106        let ring = vec![[0, 0], [10, 0]];
107        assert_eq!(twice_area_fp2(&ring), 0);
108    }
109
110    #[test]
111    fn area_display_scales_large_values() {
112        assert_eq!(
113            area_display(2_000_000_000_000_000_000, 2_000_000_000_000),
114            1_000_000
115        );
116    }
117
118    #[test]
119    fn area_display_zero_area_is_zero() {
120        assert_eq!(area_display(0, 1), 0);
121    }
122
123    #[test]
124    fn areas_conserved_accepts_exact_sum() {
125        assert!(areas_conserved(30, &[10, 20]));
126    }
127
128    #[test]
129    fn areas_conserved_rejects_overage() {
130        assert!(!areas_conserved(30, &[10, 21]));
131    }
132
133    #[test]
134    fn area_conservation_check() {
135        let square = vec![[0, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
136        let original = twice_area_fp2(&square);
137
138        let t1 = vec![[0, 0], [10 * M, 0], [10 * M, 10 * M]];
139        let t2 = vec![[0, 0], [10 * M, 10 * M], [0, 10 * M]];
140
141        let part_areas = vec![twice_area_fp2(&t1), twice_area_fp2(&t2)];
142
143        assert!(
144            areas_conserved(original, &part_areas),
145            "area not conserved: original={original}, sum={}",
146            part_areas.iter().sum::<u128>()
147        );
148    }
149
150    #[test]
151    fn twice_area_fp2_square_ring_matches() {
152        let ring = vec![[0i64, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
153        let area = twice_area_fp2(&ring);
154        assert_eq!(area, 200_000_000_000_000_u128);
155    }
156
157    #[test]
158    fn area_is_order_independent_for_ccw_and_cw() {
159        let ccw = vec![[0, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
160        let cw = vec![[0, 0], [0, 10 * M], [10 * M, 10 * M], [10 * M, 0]];
161        assert_eq!(twice_area_fp2(&ccw), twice_area_fp2(&cw));
162    }
163
164    #[test]
165    fn degenerate_ring_returns_zero() {
166        let ring = vec![[0, 0], [M, 0]];
167        assert_eq!(twice_area_fp2(&ring), 0);
168    }
169
170    #[test]
171    fn negative_coords_are_handled_correctly() {
172        // Triangle with negative coordinates. Before the i128 rewrite this
173        // returned garbage (~2^128) because `-x as u128` sign-extended into
174        // the high bits of every shoelace product.
175        let ring = vec![[-M, 0], [M, 0], [0, M]];
176        // Same triangle translated so all coords are non-negative.
177        let translated = vec![[0, 0], [2 * M, 0], [M, M]];
178        let expected = twice_area_fp2(&translated);
179        assert_eq!(twice_area_fp2(&ring), expected);
180        // 2·area of a (2M, M)-base-height triangle = 2·(½·2M·M) = 2·M²
181        assert_eq!(expected, 2 * (M as u128) * (M as u128));
182    }
183
184    #[test]
185    fn cw_and_ccw_negative_polygon_match() {
186        // Concave "crown" polygon from the decomposition bug report
187        // (https://.../issue): 5 vertices around the origin, drawn CW.
188        let cw = vec![
189            [-91 * M, -70 * M],
190            [-120 * M, 177 * M],
191            [M, 56 * M],
192            [110 * M, 210 * M],
193            [138 * M, -70 * M],
194        ];
195        let ccw: Vec<[i64; 2]> = cw.iter().rev().copied().collect();
196        let area_cw = twice_area_fp2(&cw);
197        let area_ccw = twice_area_fp2(&ccw);
198        assert_eq!(area_cw, area_ccw, "winding should not affect |2·area|");
199        // Shoelace on the raw (integer-meter) vertices gives |Σ| = 90_064,
200        // and the scale factor is M·M = 10^12 per product.
201        assert_eq!(area_cw, 90_064_u128 * (M as u128) * (M as u128));
202    }
203
204    #[test]
205    fn matches_move_on_u64_domain() {
206        // Mirror polygon.move::part_twice_area_fp2 — the u64 positive/negative
207        // split must agree with the i128 shoelace bit-for-bit on non-negative
208        // inputs. Pick a polygon that forces both positive and negative terms.
209        let ring = vec![
210            [0, 0],
211            [10 * M, 15 * M],
212            [30 * M, 5 * M],
213            [20 * M, 25 * M],
214            [5 * M, 20 * M],
215        ];
216        assert_eq!(twice_area_fp2(&ring), 525_u128 * (M as u128) * (M as u128));
217    }
218}