Skip to main content

tess2_rust/
geom.rs

1// Copyright 2025 Lars Brubaker
2// License: SGI Free Software License B (MIT-compatible)
3//
4// Port of libtess2 geom.c/h
5//
6// Pure geometric functions operating on vertex (s, t) coordinates.
7// These are exact translations of the C functions with identical floating-point
8// behavior to ensure mathematical equivalence with the original library.
9
10/// Internal coordinate type used throughout the sweep and all geometric
11/// predicates.
12///
13/// Pinned to **`f64`** — matching the C# agg-sharp `Tesselator` reference and
14/// modern libtess2 builds that opt for double precision.  The sweep's
15/// stability hinges on edge-sign tests not flipping under small coordinate
16/// perturbations (e.g. rotating integer input to floating coordinates):
17/// `f32`'s ~7 decimal digits of precision is not enough to keep
18/// near-collinear predicates on the same side across rotations.  With `f64`
19/// the 15-digit margin absorbs the rounding noise and the topology stays
20/// rotation-invariant — which is the whole point of this library.
21pub type Real = f64;
22
23/// Returns true if u is lexicographically <= v (s first, then t).
24#[inline]
25pub fn vert_leq(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> bool {
26    u_s < v_s || (u_s == v_s && u_t <= v_t)
27}
28
29/// Returns true if u == v (exact equality).
30#[inline]
31pub fn vert_eq(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> bool {
32    u_s == v_s && u_t == v_t
33}
34
35/// Returns true if u is lexicographically <= v with s and t transposed.
36#[inline]
37pub fn trans_leq(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> bool {
38    u_t < v_t || (u_t == v_t && u_s <= v_s)
39}
40
41/// Given three vertices u,v,w such that vert_leq(u,v) && vert_leq(v,w),
42/// evaluates the t-coord of edge uw at the s-coord of v.
43/// Returns v.t - (uw)(v.s), the signed distance from uw to v.
44/// If uw is vertical (passes through v), returns zero.
45/// The calculation is extremely accurate and stable.
46pub fn edge_eval(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
47    // debug_assert!(vert_leq(u_s, u_t, v_s, v_t) && vert_leq(v_s, v_t, w_s, w_t));
48    let gap_l = v_s - u_s;
49    let gap_r = w_s - v_s;
50    if gap_l + gap_r > 0.0 {
51        if gap_l < gap_r {
52            (v_t - u_t) + (u_t - w_t) * (gap_l / (gap_l + gap_r))
53        } else {
54            (v_t - w_t) + (w_t - u_t) * (gap_r / (gap_l + gap_r))
55        }
56    } else {
57        0.0
58    }
59}
60
61/// Returns a value whose **sign** matches `edge_eval(u,v,w)`, computed with a
62/// cheaper division-free cross-product formula.
63///
64/// Direct port of libtess2's `tesedgeSign` (`geom.c`).  The point of having
65/// this as a separate routine from `edge_eval` is numerical stability: the
66/// division inside `edge_eval` (`gap_l / (gap_l + gap_r)`) rounds slightly
67/// differently depending on floating-point representation, which flips the
68/// sign on nearly-collinear inputs.  Rotating a polygon changes coordinate
69/// representations just enough for that flipping to give DIFFERENT
70/// topological decisions during the sweep — the classic robustness failure
71/// libtess2 was written to avoid.
72///
73/// The cross-product formula below preserves the same sign as
74/// `edge_eval(u,v,w)` (they differ only by a positive scalar `gap_l+gap_r`),
75/// but avoids division entirely, so its sign is stable under the small
76/// coordinate perturbations that rotations introduce.
77#[inline]
78pub fn edge_sign(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
79    // debug_assert!(vert_leq(u_s, u_t, v_s, v_t) && vert_leq(v_s, v_t, w_s, w_t));
80    let gap_l = v_s - u_s;
81    let gap_r = w_s - v_s;
82    if gap_l + gap_r > 0.0 {
83        (v_t - w_t) * gap_l + (v_t - u_t) * gap_r
84    } else {
85        // Vertical line uvw — collinear on s axis, sign ambiguous.
86        0.0
87    }
88}
89
90/// Like edge_eval but with s and t transposed.
91pub fn trans_eval(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
92    // debug_assert!(trans_leq(u_s, u_t, v_s, v_t) && trans_leq(v_s, v_t, w_s, w_t));
93    let gap_l = v_t - u_t;
94    let gap_r = w_t - v_t;
95    if gap_l + gap_r > 0.0 {
96        if gap_l < gap_r {
97            (v_s - u_s) + (u_s - w_s) * (gap_l / (gap_l + gap_r))
98        } else {
99            (v_s - w_s) + (w_s - u_s) * (gap_r / (gap_l + gap_r))
100        }
101    } else {
102        0.0
103    }
104}
105
106/// Like edge_sign but with s and t transposed.
107pub fn trans_sign(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
108    // debug_assert!(trans_leq(u_s, u_t, v_s, v_t) && trans_leq(v_s, v_t, w_s, w_t));
109    let gap_l = v_t - u_t;
110    let gap_r = w_t - v_t;
111    if gap_l + gap_r > 0.0 {
112        (v_s - w_s) * gap_l + (v_s - u_s) * gap_r
113    } else {
114        0.0
115    }
116}
117
118/// Returns true if (u, v, w) are in CCW (counter-clockwise) order.
119#[inline]
120pub fn vert_ccw(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> bool {
121    u_s * (v_t - w_t) + v_s * (w_t - u_t) + w_s * (u_t - v_t) >= 0.0
122}
123
124/// L1 distance between two vertices.
125#[inline]
126pub fn vert_l1_dist(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> Real {
127    (u_s - v_s).abs() + (u_t - v_t).abs()
128}
129
130/// Numerically stable interpolation: returns (b*x + a*y) / (a + b),
131/// or (x + y) / 2 if a == b == 0. Requires a, b >= 0 and enforces this.
132/// Guarantees MIN(x,y) <= result <= MAX(x,y).
133#[inline]
134pub fn real_interpolate(mut a: Real, x: Real, mut b: Real, y: Real) -> Real {
135    if a < 0.0 {
136        a = 0.0;
137    }
138    if b < 0.0 {
139        b = 0.0;
140    }
141    if a <= b {
142        if b == 0.0 {
143            x / 2.0 + y / 2.0
144        } else {
145            x + (y - x) * (a / (a + b))
146        }
147    } else {
148        y + (x - y) * (b / (a + b))
149    }
150}
151
152/// Compute the intersection point of edges (o1,d1) and (o2,d2).
153/// Returns (s, t) of the intersection.
154/// The result is guaranteed to lie within the bounding rectangle of both edges.
155pub fn edge_intersect(
156    o1_s: Real,
157    o1_t: Real,
158    d1_s: Real,
159    d1_t: Real,
160    o2_s: Real,
161    o2_t: Real,
162    d2_s: Real,
163    d2_t: Real,
164) -> (Real, Real) {
165    // Compute s-coordinate of intersection using VertLeq ordering.
166    let v_s;
167    {
168        let (mut a_s, mut a_t) = (o1_s, o1_t);
169        let (mut b_s, mut b_t) = (d1_s, d1_t);
170        let (mut c_s, mut c_t) = (o2_s, o2_t);
171        let (mut d_s, mut d_t) = (d2_s, d2_t);
172
173        if !vert_leq(a_s, a_t, b_s, b_t) {
174            core::mem::swap(&mut a_s, &mut b_s);
175            core::mem::swap(&mut a_t, &mut b_t);
176        }
177        if !vert_leq(c_s, c_t, d_s, d_t) {
178            core::mem::swap(&mut c_s, &mut d_s);
179            core::mem::swap(&mut c_t, &mut d_t);
180        }
181        if !vert_leq(a_s, a_t, c_s, c_t) {
182            core::mem::swap(&mut a_s, &mut c_s);
183            core::mem::swap(&mut a_t, &mut c_t);
184            core::mem::swap(&mut b_s, &mut d_s);
185            core::mem::swap(&mut b_t, &mut d_t);
186        }
187
188        if !vert_leq(c_s, c_t, b_s, b_t) {
189            v_s = c_s / 2.0 + b_s / 2.0;
190        } else if vert_leq(b_s, b_t, d_s, d_t) {
191            let mut z1 = edge_eval(a_s, a_t, c_s, c_t, b_s, b_t);
192            let mut z2 = edge_eval(c_s, c_t, b_s, b_t, d_s, d_t);
193            if z1 + z2 < 0.0 {
194                z1 = -z1;
195                z2 = -z2;
196            }
197            v_s = real_interpolate(z1, c_s, z2, b_s);
198        } else {
199            let mut z1 = edge_sign(a_s, a_t, c_s, c_t, b_s, b_t);
200            let mut z2 = -edge_sign(a_s, a_t, d_s, d_t, b_s, b_t);
201            if z1 + z2 < 0.0 {
202                z1 = -z1;
203                z2 = -z2;
204            }
205            v_s = real_interpolate(z1, c_s, z2, d_s);
206        }
207    }
208
209    // Compute t-coordinate of intersection using TransLeq ordering.
210    let v_t;
211    {
212        let (mut a_s, mut a_t) = (o1_s, o1_t);
213        let (mut b_s, mut b_t) = (d1_s, d1_t);
214        let (mut c_s, mut c_t) = (o2_s, o2_t);
215        let (mut d_s, mut d_t) = (d2_s, d2_t);
216
217        if !trans_leq(a_s, a_t, b_s, b_t) {
218            core::mem::swap(&mut a_s, &mut b_s);
219            core::mem::swap(&mut a_t, &mut b_t);
220        }
221        if !trans_leq(c_s, c_t, d_s, d_t) {
222            core::mem::swap(&mut c_s, &mut d_s);
223            core::mem::swap(&mut c_t, &mut d_t);
224        }
225        if !trans_leq(a_s, a_t, c_s, c_t) {
226            core::mem::swap(&mut a_s, &mut c_s);
227            core::mem::swap(&mut a_t, &mut c_t);
228            core::mem::swap(&mut b_s, &mut d_s);
229            core::mem::swap(&mut b_t, &mut d_t);
230        }
231
232        if !trans_leq(c_s, c_t, b_s, b_t) {
233            v_t = c_t / 2.0 + b_t / 2.0;
234        } else if trans_leq(b_s, b_t, d_s, d_t) {
235            let mut z1 = trans_eval(a_s, a_t, c_s, c_t, b_s, b_t);
236            let mut z2 = trans_eval(c_s, c_t, b_s, b_t, d_s, d_t);
237            if z1 + z2 < 0.0 {
238                z1 = -z1;
239                z2 = -z2;
240            }
241            v_t = real_interpolate(z1, c_t, z2, b_t);
242        } else {
243            let mut z1 = trans_sign(a_s, a_t, c_s, c_t, b_s, b_t);
244            let mut z2 = -trans_sign(a_s, a_t, d_s, d_t, b_s, b_t);
245            if z1 + z2 < 0.0 {
246                z1 = -z1;
247                z2 = -z2;
248            }
249            v_t = real_interpolate(z1, c_t, z2, d_t);
250        }
251    }
252
253    (v_s, v_t)
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn vert_leq_basic() {
262        assert!(vert_leq(0.0, 0.0, 1.0, 0.0));
263        assert!(vert_leq(0.0, 0.0, 0.0, 1.0));
264        assert!(vert_leq(0.0, 0.0, 0.0, 0.0));
265        assert!(!vert_leq(1.0, 0.0, 0.0, 0.0));
266    }
267
268    #[test]
269    fn trans_leq_basic() {
270        assert!(trans_leq(0.0, 0.0, 0.0, 1.0));
271        assert!(trans_leq(0.0, 0.0, 1.0, 0.0));
272        assert!(!trans_leq(0.0, 1.0, 0.0, 0.0));
273    }
274
275    #[test]
276    fn edge_eval_horizontal() {
277        // u=(0,0), v=(0.5,1), w=(1,0) -- vertical midpoint of unit interval
278        // The t-value of the edge uw at s=0.5 is 0 (since u and w both have t=0).
279        // But v.t = 1, so signed distance from uw to v = 1 - 0 = 1.
280        let r = edge_eval(0.0, 0.0, 0.5, 1.0, 1.0, 0.0);
281        assert!((r - 1.0).abs() < 1e-6, "got {}", r);
282    }
283
284    #[test]
285    fn edge_eval_vertical_returns_zero() {
286        // When u.s == v.s == w.s (vertical), result must be 0.
287        let r = edge_eval(0.0, 0.0, 0.0, 0.5, 0.0, 1.0);
288        assert_eq!(r, 0.0);
289    }
290
291    #[test]
292    fn vert_ccw_basic() {
293        assert!(vert_ccw(0.0, 0.0, 1.0, 0.0, 0.5, 1.0));
294        assert!(!vert_ccw(0.0, 0.0, 0.5, 1.0, 1.0, 0.0));
295    }
296
297    #[test]
298    fn real_interpolate_midpoint() {
299        let r = real_interpolate(0.0, 0.0, 0.0, 1.0);
300        assert!((r - 0.5).abs() < 1e-6);
301    }
302
303    #[test]
304    fn real_interpolate_weighted() {
305        // a=1, x=0, b=1, y=2 → (1*0 + 1*2) / 2 = 1? No wait:
306        // (b*x + a*y) / (a+b) = (1*0 + 1*2) / 2 = 1
307        // But formula is: x + (y-x)*(a/(a+b)) = 0 + 2*(0.5) = 1 ✓
308        let r = real_interpolate(1.0, 0.0, 1.0, 2.0);
309        assert!((r - 1.0).abs() < 1e-6);
310    }
311
312    #[test]
313    fn edge_intersect_crossing() {
314        // Two edges crossing at (0.5, 0.5):
315        // Edge 1: (0,0) → (1,1)
316        // Edge 2: (0,1) → (1,0)
317        let (s, t) = edge_intersect(0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0);
318        assert!((s - 0.5).abs() < 1e-5, "s={}", s);
319        assert!((t - 0.5).abs() < 1e-5, "t={}", t);
320    }
321}