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
10pub type Real = f32;
11
12/// Returns true if u is lexicographically <= v (s first, then t).
13#[inline]
14pub fn vert_leq(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> bool {
15    u_s < v_s || (u_s == v_s && u_t <= v_t)
16}
17
18/// Returns true if u == v (exact equality).
19#[inline]
20pub fn vert_eq(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> bool {
21    u_s == v_s && u_t == v_t
22}
23
24/// Returns true if u is lexicographically <= v with s and t transposed.
25#[inline]
26pub fn trans_leq(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> bool {
27    u_t < v_t || (u_t == v_t && u_s <= v_s)
28}
29
30/// Given three vertices u,v,w such that vert_leq(u,v) && vert_leq(v,w),
31/// evaluates the t-coord of edge uw at the s-coord of v.
32/// Returns v.t - (uw)(v.s), the signed distance from uw to v.
33/// If uw is vertical (passes through v), returns zero.
34/// The calculation is extremely accurate and stable.
35pub fn edge_eval(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
36    // debug_assert!(vert_leq(u_s, u_t, v_s, v_t) && vert_leq(v_s, v_t, w_s, w_t));
37    let gap_l = v_s - u_s;
38    let gap_r = w_s - v_s;
39    if gap_l + gap_r > 0.0 {
40        if gap_l < gap_r {
41            (v_t - u_t) + (u_t - w_t) * (gap_l / (gap_l + gap_r))
42        } else {
43            (v_t - w_t) + (w_t - u_t) * (gap_r / (gap_l + gap_r))
44        }
45    } else {
46        0.0
47    }
48}
49
50/// Returns a value whose sign matches edge_eval(u,v,w) but cheaper to compute.
51/// NOTE: In the C code, EdgeSign is #defined to call tesedgeEval (same as EdgeEval)
52/// to fix a numerical accuracy issue with nearly-zero x coordinates.
53#[inline]
54pub fn edge_sign(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
55    edge_eval(u_s, u_t, v_s, v_t, w_s, w_t)
56}
57
58/// Like edge_eval but with s and t transposed.
59pub fn trans_eval(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
60    // debug_assert!(trans_leq(u_s, u_t, v_s, v_t) && trans_leq(v_s, v_t, w_s, w_t));
61    let gap_l = v_t - u_t;
62    let gap_r = w_t - v_t;
63    if gap_l + gap_r > 0.0 {
64        if gap_l < gap_r {
65            (v_s - u_s) + (u_s - w_s) * (gap_l / (gap_l + gap_r))
66        } else {
67            (v_s - w_s) + (w_s - u_s) * (gap_r / (gap_l + gap_r))
68        }
69    } else {
70        0.0
71    }
72}
73
74/// Like edge_sign but with s and t transposed.
75pub fn trans_sign(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> Real {
76    // debug_assert!(trans_leq(u_s, u_t, v_s, v_t) && trans_leq(v_s, v_t, w_s, w_t));
77    let gap_l = v_t - u_t;
78    let gap_r = w_t - v_t;
79    if gap_l + gap_r > 0.0 {
80        (v_s - w_s) * gap_l + (v_s - u_s) * gap_r
81    } else {
82        0.0
83    }
84}
85
86/// Returns true if (u, v, w) are in CCW (counter-clockwise) order.
87#[inline]
88pub fn vert_ccw(u_s: Real, u_t: Real, v_s: Real, v_t: Real, w_s: Real, w_t: Real) -> bool {
89    u_s * (v_t - w_t) + v_s * (w_t - u_t) + w_s * (u_t - v_t) >= 0.0
90}
91
92/// L1 distance between two vertices.
93#[inline]
94pub fn vert_l1_dist(u_s: Real, u_t: Real, v_s: Real, v_t: Real) -> Real {
95    (u_s - v_s).abs() + (u_t - v_t).abs()
96}
97
98/// Numerically stable interpolation: returns (b*x + a*y) / (a + b),
99/// or (x + y) / 2 if a == b == 0. Requires a, b >= 0 and enforces this.
100/// Guarantees MIN(x,y) <= result <= MAX(x,y).
101#[inline]
102pub fn real_interpolate(mut a: Real, x: Real, mut b: Real, y: Real) -> Real {
103    if a < 0.0 {
104        a = 0.0;
105    }
106    if b < 0.0 {
107        b = 0.0;
108    }
109    if a <= b {
110        if b == 0.0 {
111            x / 2.0 + y / 2.0
112        } else {
113            x + (y - x) * (a / (a + b))
114        }
115    } else {
116        y + (x - y) * (b / (a + b))
117    }
118}
119
120/// Compute the intersection point of edges (o1,d1) and (o2,d2).
121/// Returns (s, t) of the intersection.
122/// The result is guaranteed to lie within the bounding rectangle of both edges.
123pub fn edge_intersect(
124    o1_s: Real,
125    o1_t: Real,
126    d1_s: Real,
127    d1_t: Real,
128    o2_s: Real,
129    o2_t: Real,
130    d2_s: Real,
131    d2_t: Real,
132) -> (Real, Real) {
133    // Compute s-coordinate of intersection using VertLeq ordering.
134    let v_s;
135    {
136        let (mut a_s, mut a_t) = (o1_s, o1_t);
137        let (mut b_s, mut b_t) = (d1_s, d1_t);
138        let (mut c_s, mut c_t) = (o2_s, o2_t);
139        let (mut d_s, mut d_t) = (d2_s, d2_t);
140
141        if !vert_leq(a_s, a_t, b_s, b_t) {
142            core::mem::swap(&mut a_s, &mut b_s);
143            core::mem::swap(&mut a_t, &mut b_t);
144        }
145        if !vert_leq(c_s, c_t, d_s, d_t) {
146            core::mem::swap(&mut c_s, &mut d_s);
147            core::mem::swap(&mut c_t, &mut d_t);
148        }
149        if !vert_leq(a_s, a_t, c_s, c_t) {
150            core::mem::swap(&mut a_s, &mut c_s);
151            core::mem::swap(&mut a_t, &mut c_t);
152            core::mem::swap(&mut b_s, &mut d_s);
153            core::mem::swap(&mut b_t, &mut d_t);
154        }
155
156        if !vert_leq(c_s, c_t, b_s, b_t) {
157            v_s = c_s / 2.0 + b_s / 2.0;
158        } else if vert_leq(b_s, b_t, d_s, d_t) {
159            let mut z1 = edge_eval(a_s, a_t, c_s, c_t, b_s, b_t);
160            let mut z2 = edge_eval(c_s, c_t, b_s, b_t, d_s, d_t);
161            if z1 + z2 < 0.0 {
162                z1 = -z1;
163                z2 = -z2;
164            }
165            v_s = real_interpolate(z1, c_s, z2, b_s);
166        } else {
167            let mut z1 = edge_sign(a_s, a_t, c_s, c_t, b_s, b_t);
168            let mut z2 = -edge_sign(a_s, a_t, d_s, d_t, b_s, b_t);
169            if z1 + z2 < 0.0 {
170                z1 = -z1;
171                z2 = -z2;
172            }
173            v_s = real_interpolate(z1, c_s, z2, d_s);
174        }
175    }
176
177    // Compute t-coordinate of intersection using TransLeq ordering.
178    let v_t;
179    {
180        let (mut a_s, mut a_t) = (o1_s, o1_t);
181        let (mut b_s, mut b_t) = (d1_s, d1_t);
182        let (mut c_s, mut c_t) = (o2_s, o2_t);
183        let (mut d_s, mut d_t) = (d2_s, d2_t);
184
185        if !trans_leq(a_s, a_t, b_s, b_t) {
186            core::mem::swap(&mut a_s, &mut b_s);
187            core::mem::swap(&mut a_t, &mut b_t);
188        }
189        if !trans_leq(c_s, c_t, d_s, d_t) {
190            core::mem::swap(&mut c_s, &mut d_s);
191            core::mem::swap(&mut c_t, &mut d_t);
192        }
193        if !trans_leq(a_s, a_t, c_s, c_t) {
194            core::mem::swap(&mut a_s, &mut c_s);
195            core::mem::swap(&mut a_t, &mut c_t);
196            core::mem::swap(&mut b_s, &mut d_s);
197            core::mem::swap(&mut b_t, &mut d_t);
198        }
199
200        if !trans_leq(c_s, c_t, b_s, b_t) {
201            v_t = c_t / 2.0 + b_t / 2.0;
202        } else if trans_leq(b_s, b_t, d_s, d_t) {
203            let mut z1 = trans_eval(a_s, a_t, c_s, c_t, b_s, b_t);
204            let mut z2 = trans_eval(c_s, c_t, b_s, b_t, d_s, d_t);
205            if z1 + z2 < 0.0 {
206                z1 = -z1;
207                z2 = -z2;
208            }
209            v_t = real_interpolate(z1, c_t, z2, b_t);
210        } else {
211            let mut z1 = trans_sign(a_s, a_t, c_s, c_t, b_s, b_t);
212            let mut z2 = -trans_sign(a_s, a_t, d_s, d_t, b_s, b_t);
213            if z1 + z2 < 0.0 {
214                z1 = -z1;
215                z2 = -z2;
216            }
217            v_t = real_interpolate(z1, c_t, z2, d_t);
218        }
219    }
220
221    (v_s, v_t)
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn vert_leq_basic() {
230        assert!(vert_leq(0.0, 0.0, 1.0, 0.0));
231        assert!(vert_leq(0.0, 0.0, 0.0, 1.0));
232        assert!(vert_leq(0.0, 0.0, 0.0, 0.0));
233        assert!(!vert_leq(1.0, 0.0, 0.0, 0.0));
234    }
235
236    #[test]
237    fn trans_leq_basic() {
238        assert!(trans_leq(0.0, 0.0, 0.0, 1.0));
239        assert!(trans_leq(0.0, 0.0, 1.0, 0.0));
240        assert!(!trans_leq(0.0, 1.0, 0.0, 0.0));
241    }
242
243    #[test]
244    fn edge_eval_horizontal() {
245        // u=(0,0), v=(0.5,1), w=(1,0) -- vertical midpoint of unit interval
246        // The t-value of the edge uw at s=0.5 is 0 (since u and w both have t=0).
247        // But v.t = 1, so signed distance from uw to v = 1 - 0 = 1.
248        let r = edge_eval(0.0, 0.0, 0.5, 1.0, 1.0, 0.0);
249        assert!((r - 1.0).abs() < 1e-6, "got {}", r);
250    }
251
252    #[test]
253    fn edge_eval_vertical_returns_zero() {
254        // When u.s == v.s == w.s (vertical), result must be 0.
255        let r = edge_eval(0.0, 0.0, 0.0, 0.5, 0.0, 1.0);
256        assert_eq!(r, 0.0);
257    }
258
259    #[test]
260    fn vert_ccw_basic() {
261        assert!(vert_ccw(0.0, 0.0, 1.0, 0.0, 0.5, 1.0));
262        assert!(!vert_ccw(0.0, 0.0, 0.5, 1.0, 1.0, 0.0));
263    }
264
265    #[test]
266    fn real_interpolate_midpoint() {
267        let r = real_interpolate(0.0, 0.0, 0.0, 1.0);
268        assert!((r - 0.5).abs() < 1e-6);
269    }
270
271    #[test]
272    fn real_interpolate_weighted() {
273        // a=1, x=0, b=1, y=2 → (1*0 + 1*2) / 2 = 1? No wait:
274        // (b*x + a*y) / (a+b) = (1*0 + 1*2) / 2 = 1
275        // But formula is: x + (y-x)*(a/(a+b)) = 0 + 2*(0.5) = 1 ✓
276        let r = real_interpolate(1.0, 0.0, 1.0, 2.0);
277        assert!((r - 1.0).abs() < 1e-6);
278    }
279
280    #[test]
281    fn edge_intersect_crossing() {
282        // Two edges crossing at (0.5, 0.5):
283        // Edge 1: (0,0) → (1,1)
284        // Edge 2: (0,1) → (1,0)
285        let (s, t) = edge_intersect(0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0);
286        assert!((s - 0.5).abs() < 1e-5, "s={}", s);
287        assert!((t - 0.5).abs() < 1e-5, "t={}", t);
288    }
289}