Skip to main content

box2d_rust/
hull.rs

1// Port of box2d-cpp-reference/src/hull.c, plus the b2Hull type and
2// B2_MAX_POLYGON_VERTICES from include/box2d/collision.h.
3//
4// quickhull:
5// - merges vertices based on the linear slop
6// - removes collinear points using the linear slop
7// - returns an empty hull if it fails
8//
9// SPDX-FileCopyrightText: 2023 Erin Catto
10// SPDX-License-Identifier: MIT
11
12// This is a line-by-line port of a quickhull that relies on explicit index
13// arithmetic — welding by comparing ps[i] against ps[j<i], swapping removed
14// points with ps[n-1], and walking modular triples (i, i+1, i+2) % count.
15// Rewriting these as iterators would obscure the correspondence to the C.
16#![allow(clippy::needless_range_loop)]
17
18use crate::constants::linear_slop;
19use crate::math_functions::{
20    aabb_center, cross, distance_squared, max, min, min_int, normalize, sub, Aabb, Vec2, VEC2_ZERO,
21};
22
23/// The maximum number of vertices on a convex polygon. Changing this affects
24/// performance even if you don't use more vertices. (B2_MAX_POLYGON_VERTICES)
25pub const MAX_POLYGON_VERTICES: usize = 8;
26
27/// A convex hull. Used to construct convex polygons. (b2Hull)
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct Hull {
30    /// The final points of the hull
31    pub points: [Vec2; MAX_POLYGON_VERTICES],
32    /// The number of points
33    pub count: i32,
34}
35
36impl Default for Hull {
37    fn default() -> Self {
38        Hull {
39            points: [VEC2_ZERO; MAX_POLYGON_VERTICES],
40            count: 0,
41        }
42    }
43}
44
45// quickhull recursion
46fn recurse_hull(p1: Vec2, p2: Vec2, ps: &[Vec2]) -> Hull {
47    let mut hull = Hull::default();
48
49    let count = ps.len();
50    if count == 0 {
51        return hull;
52    }
53
54    // create an edge vector pointing from p1 to p2
55    let e = normalize(sub(p2, p1));
56
57    // discard points left of e and find point furthest to the right of e
58    let mut right_points = [VEC2_ZERO; MAX_POLYGON_VERTICES];
59    let mut right_count = 0;
60
61    let mut best_index = 0;
62    let mut best_distance = cross(sub(ps[best_index], p1), e);
63    if best_distance > 0.0 {
64        right_points[right_count] = ps[best_index];
65        right_count += 1;
66    }
67
68    for i in 1..count {
69        let distance = cross(sub(ps[i], p1), e);
70        if distance > best_distance {
71            best_index = i;
72            best_distance = distance;
73        }
74
75        if distance > 0.0 {
76            right_points[right_count] = ps[i];
77            right_count += 1;
78        }
79    }
80
81    if best_distance < 2.0 * linear_slop() {
82        return hull;
83    }
84
85    let best_point = ps[best_index];
86
87    // compute hull to the right of p1-bestPoint
88    let hull1 = recurse_hull(p1, best_point, &right_points[..right_count]);
89
90    // compute hull to the right of bestPoint-p2
91    let hull2 = recurse_hull(best_point, p2, &right_points[..right_count]);
92
93    // stitch together hulls
94    for i in 0..hull1.count as usize {
95        hull.points[hull.count as usize] = hull1.points[i];
96        hull.count += 1;
97    }
98
99    hull.points[hull.count as usize] = best_point;
100    hull.count += 1;
101
102    for i in 0..hull2.count as usize {
103        hull.points[hull.count as usize] = hull2.points[i];
104        hull.count += 1;
105    }
106
107    debug_assert!(hull.count < MAX_POLYGON_VERTICES as i32);
108
109    hull
110}
111
112/// Compute the convex hull of a set of points. Returns an empty hull if it
113/// fails. (b2ComputeHull)
114///
115/// Some failure cases:
116/// - all points very close together
117/// - all points on a line
118/// - fewer than 3 points
119/// - more than [`MAX_POLYGON_VERTICES`] points
120pub fn compute_hull(points: &[Vec2]) -> Hull {
121    let mut hull = Hull::default();
122
123    let mut count = points.len() as i32;
124
125    if count < 3 || count > MAX_POLYGON_VERTICES as i32 {
126        // check your data
127        return hull;
128    }
129
130    count = min_int(count, MAX_POLYGON_VERTICES as i32);
131
132    let mut aabb = Aabb {
133        lower_bound: Vec2 {
134            x: f32::MAX,
135            y: f32::MAX,
136        },
137        upper_bound: Vec2 {
138            x: -f32::MAX,
139            y: -f32::MAX,
140        },
141    };
142
143    // Perform aggressive point welding. First point always remains.
144    // Also compute the bounding box for later.
145    let mut ps = [VEC2_ZERO; MAX_POLYGON_VERTICES];
146    let mut n = 0;
147    let slop = linear_slop();
148    let tol_sqr = 16.0 * slop * slop;
149    for i in 0..count as usize {
150        aabb.lower_bound = min(aabb.lower_bound, points[i]);
151        aabb.upper_bound = max(aabb.upper_bound, points[i]);
152
153        let vi = points[i];
154
155        let mut unique = true;
156        for j in 0..i {
157            let vj = points[j];
158
159            let dist_sqr = distance_squared(vi, vj);
160            if dist_sqr < tol_sqr {
161                unique = false;
162                break;
163            }
164        }
165
166        if unique {
167            ps[n] = vi;
168            n += 1;
169        }
170    }
171
172    if n < 3 {
173        // all points very close together, check your data and check your scale
174        return hull;
175    }
176
177    // Find an extreme point as the first point on the hull
178    let c = aabb_center(aabb);
179    let mut f1 = 0;
180    let mut dsq1 = distance_squared(c, ps[f1]);
181    for i in 1..n {
182        let dsq = distance_squared(c, ps[i]);
183        if dsq > dsq1 {
184            f1 = i;
185            dsq1 = dsq;
186        }
187    }
188
189    // remove p1 from working set
190    let p1 = ps[f1];
191    ps[f1] = ps[n - 1];
192    n -= 1;
193
194    let mut f2 = 0;
195    let mut dsq2 = distance_squared(p1, ps[f2]);
196    for i in 1..n {
197        let dsq = distance_squared(p1, ps[i]);
198        if dsq > dsq2 {
199            f2 = i;
200            dsq2 = dsq;
201        }
202    }
203
204    // remove p2 from working set
205    let p2 = ps[f2];
206    ps[f2] = ps[n - 1];
207    n -= 1;
208
209    // split the points into points that are left and right of the line p1-p2.
210    let mut right_points = [VEC2_ZERO; MAX_POLYGON_VERTICES - 2];
211    let mut right_count = 0;
212
213    let mut left_points = [VEC2_ZERO; MAX_POLYGON_VERTICES - 2];
214    let mut left_count = 0;
215
216    let e = normalize(sub(p2, p1));
217
218    for i in 0..n {
219        let d = cross(sub(ps[i], p1), e);
220
221        // slop used here to skip points that are very close to the line p1-p2
222        if d >= 2.0 * slop {
223            right_points[right_count] = ps[i];
224            right_count += 1;
225        } else if d <= -2.0 * slop {
226            left_points[left_count] = ps[i];
227            left_count += 1;
228        }
229    }
230
231    // compute hulls on right and left
232    let hull1 = recurse_hull(p1, p2, &right_points[..right_count]);
233    let hull2 = recurse_hull(p2, p1, &left_points[..left_count]);
234
235    if hull1.count == 0 && hull2.count == 0 {
236        // all points collinear
237        return hull;
238    }
239
240    // stitch hulls together, preserving CCW winding order
241    hull.points[hull.count as usize] = p1;
242    hull.count += 1;
243
244    for i in 0..hull1.count as usize {
245        hull.points[hull.count as usize] = hull1.points[i];
246        hull.count += 1;
247    }
248
249    hull.points[hull.count as usize] = p2;
250    hull.count += 1;
251
252    for i in 0..hull2.count as usize {
253        hull.points[hull.count as usize] = hull2.points[i];
254        hull.count += 1;
255    }
256
257    debug_assert!(hull.count <= MAX_POLYGON_VERTICES as i32);
258
259    // merge collinear
260    let mut searching = true;
261    while searching && hull.count > 2 {
262        searching = false;
263
264        for i in 0..hull.count as usize {
265            let i1 = i;
266            let i2 = (i + 1) % hull.count as usize;
267            let i3 = (i + 2) % hull.count as usize;
268
269            let s1 = hull.points[i1];
270            let s2 = hull.points[i2];
271            let s3 = hull.points[i3];
272
273            // unit edge vector for s1-s3
274            let r = normalize(sub(s3, s1));
275
276            let distance = cross(sub(s2, s1), r);
277            if distance <= 2.0 * slop {
278                // remove midpoint from hull
279                for j in i2..(hull.count as usize - 1) {
280                    hull.points[j] = hull.points[j + 1];
281                }
282                hull.count -= 1;
283
284                // continue searching for collinear points
285                searching = true;
286
287                break;
288            }
289        }
290    }
291
292    if hull.count < 3 {
293        // all points collinear, shouldn't be reached since this was validated above
294        hull.count = 0;
295    }
296
297    hull
298}
299
300/// Validate that a hull is convex, CCW, and has no collinear points.
301/// (b2ValidateHull)
302pub fn validate_hull(hull: &Hull) -> bool {
303    if hull.count < 3 || (MAX_POLYGON_VERTICES as i32) < hull.count {
304        return false;
305    }
306
307    let count = hull.count as usize;
308
309    // test that every point is behind every edge
310    for i in 0..count {
311        // create an edge vector
312        let i1 = i;
313        let i2 = if i < count - 1 { i1 + 1 } else { 0 };
314        let p = hull.points[i1];
315        let e = normalize(sub(hull.points[i2], p));
316
317        for (j, &point) in hull.points[..count].iter().enumerate() {
318            // skip points that subtend the current edge
319            if j == i1 || j == i2 {
320                continue;
321            }
322
323            let distance = cross(sub(point, p), e);
324            if distance >= 0.0 {
325                return false;
326            }
327        }
328    }
329
330    // test for collinear points
331    let slop = linear_slop();
332    for i in 0..count {
333        let i1 = i;
334        let i2 = (i + 1) % count;
335        let i3 = (i + 2) % count;
336
337        let p1 = hull.points[i1];
338        let p2 = hull.points[i2];
339        let p3 = hull.points[i3];
340
341        let e = normalize(sub(p3, p1));
342
343        let distance = cross(sub(p2, p1), e);
344        if distance <= slop {
345            // p1-p2-p3 are collinear
346            return false;
347        }
348    }
349
350    true
351}