Skip to main content

oxiphysics_geometry/computational_geometry/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::types::{
7    ArtGalleryResult, ConvexFace3D, ConvexHull3D, DelaunayTri, Line2D, Point2, VoronoiCell2D,
8};
9
10/// A point in 3D space represented as a plain array.
11pub type Point3 = [f64; 3];
12/// Subtract two 3D points.
13pub fn sub3(a: Point3, b: Point3) -> Point3 {
14    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
15}
16/// Add two 3D vectors.
17pub fn add3(a: Point3, b: Point3) -> Point3 {
18    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
19}
20/// Scale a 3D vector.
21pub fn scale3(a: Point3, t: f64) -> Point3 {
22    [a[0] * t, a[1] * t, a[2] * t]
23}
24/// Dot product of two 3D vectors.
25pub fn dot3(a: Point3, b: Point3) -> f64 {
26    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
27}
28/// Cross product of two 3D vectors.
29pub fn cross3(a: Point3, b: Point3) -> Point3 {
30    [
31        a[1] * b[2] - a[2] * b[1],
32        a[2] * b[0] - a[0] * b[2],
33        a[0] * b[1] - a[1] * b[0],
34    ]
35}
36/// Squared magnitude of a 3D vector.
37pub fn mag3_sq(a: Point3) -> f64 {
38    dot3(a, a)
39}
40/// Magnitude of a 3D vector.
41pub fn mag3(a: Point3) -> f64 {
42    mag3_sq(a).sqrt()
43}
44/// Normalize a 3D vector; returns the zero vector if the magnitude is too small.
45pub fn normalize3(a: Point3) -> Point3 {
46    let m = mag3(a);
47    if m < 1e-15 {
48        [0.0; 3]
49    } else {
50        scale3(a, 1.0 / m)
51    }
52}
53/// Index of a half-edge in a `HalfEdgeMesh`.
54pub type HalfEdgeId = usize;
55/// Index of a vertex in a `HalfEdgeMesh`.
56pub type VertexId = usize;
57/// Index of a face in a `HalfEdgeMesh`.
58pub type FaceId = usize;
59/// Compute the signed volume of a tetrahedron formed by three points and the origin.
60pub(super) fn signed_tet_volume(a: Point3, b: Point3, c: Point3) -> f64 {
61    dot3(a, cross3(b, c)) / 6.0
62}
63/// Check if a point `p` is above (in the direction of the outward normal) a face defined
64/// by vertices `a`, `b`, `c` (counter-clockwise order from outside).
65pub(super) fn above_face(a: Point3, b: Point3, c: Point3, p: Point3) -> bool {
66    let normal = cross3(sub3(b, a), sub3(c, a));
67    dot3(normal, sub3(p, a)) > 1e-12
68}
69/// Build the initial tetrahedron from the first four non-coplanar points.
70pub(super) fn initial_tetrahedron(pts: &[Point3]) -> Option<ConvexHull3D> {
71    if pts.len() < 4 {
72        return None;
73    }
74    let p0 = pts[0];
75    let mut p1_idx = None;
76    for i in 1..pts.len() {
77        if mag3(sub3(pts[i], p0)) > 1e-10 {
78            p1_idx = Some(i);
79            break;
80        }
81    }
82    let p1_idx = p1_idx?;
83    let p1 = pts[p1_idx];
84    let mut p2_idx = None;
85    for i in (p1_idx + 1)..pts.len() {
86        let v = cross3(sub3(p1, p0), sub3(pts[i], p0));
87        if mag3(v) > 1e-10 {
88            p2_idx = Some(i);
89            break;
90        }
91    }
92    let p2_idx = p2_idx?;
93    let p2 = pts[p2_idx];
94    let mut p3_idx = None;
95    let normal = cross3(sub3(p1, p0), sub3(p2, p0));
96    for i in (p2_idx + 1)..pts.len() {
97        if dot3(normal, sub3(pts[i], p0)).abs() > 1e-10 {
98            p3_idx = Some(i);
99            break;
100        }
101    }
102    let p3_idx = p3_idx?;
103    let p3 = pts[p3_idx];
104    let verts = vec![p0, p1, p2, p3];
105    let cx = (p0[0] + p1[0] + p2[0] + p3[0]) / 4.0;
106    let cy = (p0[1] + p1[1] + p2[1] + p3[1]) / 4.0;
107    let cz = (p0[2] + p1[2] + p2[2] + p3[2]) / 4.0;
108    let center: Point3 = [cx, cy, cz];
109    let face_triples: [[usize; 3]; 4] = [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
110    let mut faces = Vec::new();
111    for tri in &face_triples {
112        let a = verts[tri[0]];
113        let b = verts[tri[1]];
114        let c = verts[tri[2]];
115        let mut normal = normalize3(cross3(sub3(b, a), sub3(c, a)));
116        if dot3(normal, sub3(a, center)) < 0.0 {
117            normal = scale3(normal, -1.0);
118            faces.push(ConvexFace3D {
119                verts: [tri[0], tri[2], tri[1]],
120                normal,
121            });
122        } else {
123            faces.push(ConvexFace3D {
124                verts: *tri,
125                normal,
126            });
127        }
128    }
129    Some(ConvexHull3D {
130        vertices: verts,
131        faces,
132    })
133}
134/// Compute the 3D convex hull of a set of points using the incremental algorithm.
135///
136/// Returns `None` if fewer than 4 non-coplanar points are provided.
137///
138/// # Algorithm
139/// 1. Build an initial tetrahedron from 4 non-coplanar seed points.
140/// 2. For each remaining point, find all faces visible from it.
141/// 3. Remove visible faces, identify the horizon edge loop, and connect horizon
142///    edges to the new point to form new faces.
143///
144/// Time complexity: O(n²) worst case; O(n log n) expected for random points.
145pub fn convex_hull_3d(points: &[Point3]) -> Option<ConvexHull3D> {
146    if points.len() < 4 {
147        return None;
148    }
149    let mut hull = initial_tetrahedron(points)?;
150    let skip_eps = 1e-10;
151    let mut remaining: Vec<Point3> = Vec::new();
152    'outer: for &p in points.iter() {
153        for v in &hull.vertices {
154            if mag3(sub3(p, *v)) < skip_eps {
155                continue 'outer;
156            }
157        }
158        remaining.push(p);
159    }
160    for &pt in &remaining {
161        let visible: Vec<bool> = hull
162            .faces
163            .iter()
164            .map(|f| {
165                let a = hull.vertices[f.verts[0]];
166                above_face(a, hull.vertices[f.verts[1]], hull.vertices[f.verts[2]], pt)
167            })
168            .collect();
169        let any_visible = visible.iter().any(|&v| v);
170        if !any_visible {
171            continue;
172        }
173        let mut horizon: Vec<(usize, usize)> = Vec::new();
174        for (fi, face) in hull.faces.iter().enumerate() {
175            if !visible[fi] {
176                continue;
177            }
178            let tri = face.verts;
179            let edges = [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])];
180            for (ea, eb) in edges {
181                let on_horizon = hull.faces.iter().enumerate().any(|(fj, g)| {
182                    if visible[fj] {
183                        return false;
184                    }
185                    let t = g.verts;
186                    [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])]
187                        .iter()
188                        .any(|&(ga, gb)| ga == eb && gb == ea)
189                });
190                if on_horizon {
191                    horizon.push((ea, eb));
192                }
193            }
194        }
195        let new_vid = hull.vertices.len();
196        hull.vertices.push(pt);
197        let n_v = hull.vertices.len() as f64;
198        let cx: f64 = hull.vertices.iter().map(|v| v[0]).sum::<f64>() / n_v;
199        let cy: f64 = hull.vertices.iter().map(|v| v[1]).sum::<f64>() / n_v;
200        let cz: f64 = hull.vertices.iter().map(|v| v[2]).sum::<f64>() / n_v;
201        let center = [cx, cy, cz];
202        let new_faces: Vec<ConvexFace3D> = hull
203            .faces
204            .iter()
205            .enumerate()
206            .filter(|(fi, _)| !visible[*fi])
207            .map(|(_, f)| f.clone())
208            .collect();
209        hull.faces = new_faces;
210        for (ha, hb) in horizon {
211            let a = hull.vertices[ha];
212            let b = hull.vertices[hb];
213            let mut normal = normalize3(cross3(sub3(b, a), sub3(pt, a)));
214            let verts = if dot3(normal, sub3(a, center)) < 0.0 {
215                normal = scale3(normal, -1.0);
216                [ha, new_vid, hb]
217            } else {
218                [ha, hb, new_vid]
219            };
220            hull.faces.push(ConvexFace3D { verts, normal });
221        }
222    }
223    Some(hull)
224}
225/// Clip a polygon against an infinite half-plane defined by the directed edge from
226/// `edge_a` to `edge_b` (the inside is to the left).
227///
228/// Returns the clipped polygon vertices.
229pub fn clip_polygon_by_edge(polygon: &[Point2], edge_a: Point2, edge_b: Point2) -> Vec<Point2> {
230    if polygon.is_empty() {
231        return vec![];
232    }
233    let n = polygon.len();
234    let mut output = Vec::with_capacity(n + 1);
235    for i in 0..n {
236        let cur = polygon[i];
237        let next = polygon[(i + 1) % n];
238        let d_cur = Point2::cross2(edge_a, edge_b, cur);
239        let d_next = Point2::cross2(edge_a, edge_b, next);
240        if d_cur >= 0.0 {
241            output.push(cur);
242            if d_next < 0.0 {
243                output.push(edge_intersect(cur, next, edge_a, edge_b));
244            }
245        } else {
246            if d_next >= 0.0 {
247                output.push(edge_intersect(cur, next, edge_a, edge_b));
248            }
249        }
250    }
251    output
252}
253/// Compute the intersection of segment (p, q) with the line through (a, b).
254pub(super) fn edge_intersect(p: Point2, q: Point2, a: Point2, b: Point2) -> Point2 {
255    let pq = q.sub(p);
256    let ab = b.sub(a);
257    let ap = a.sub(p);
258    let denom = pq.cross(ab);
259    if denom.abs() < 1e-15 {
260        return p;
261    }
262    let t = ap.cross(ab) / denom;
263    p.add(pq.scale(t))
264}
265/// Clip a subject polygon against a convex clipping polygon using the
266/// Sutherland-Hodgman algorithm.
267///
268/// Both polygons must be given as counter-clockwise vertex lists.
269/// Returns the vertices of the clipped (intersection) polygon, which is
270/// convex if the clipping polygon is convex.
271pub fn sutherland_hodgman(subject: &[Point2], clip: &[Point2]) -> Vec<Point2> {
272    if subject.is_empty() || clip.is_empty() {
273        return vec![];
274    }
275    let mut output = subject.to_vec();
276    let n = clip.len();
277    for i in 0..n {
278        if output.is_empty() {
279            break;
280        }
281        let a = clip[i];
282        let b = clip[(i + 1) % n];
283        output = clip_polygon_by_edge(&output, a, b);
284    }
285    output
286}
287/// Compute the area of a simple polygon (positive for CCW).
288pub fn polygon_area(poly: &[Point2]) -> f64 {
289    let n = poly.len();
290    if n < 3 {
291        return 0.0;
292    }
293    let mut area = 0.0f64;
294    for i in 0..n {
295        let j = (i + 1) % n;
296        area += poly[i].x * poly[j].y;
297        area -= poly[j].x * poly[i].y;
298    }
299    area / 2.0
300}
301/// Compute the Minkowski sum of two convex polygons P and Q.
302///
303/// Both polygons must be given as vertices in counter-clockwise order.
304/// The result is the convex polygon P ⊕ Q.
305///
306/// # Algorithm
307/// Rotating calipers / edge-angle merge:
308/// 1. Rotate both polygons so the bottom-most (then left-most) vertex is first.
309/// 2. Merge the edge direction sequences of P and Q in angular order.
310/// 3. Walk the merged sequence to build the sum polygon.
311pub fn minkowski_sum(p: &[Point2], q: &[Point2]) -> Vec<Point2> {
312    if p.is_empty() || q.is_empty() {
313        return vec![];
314    }
315    let start_p = bottom_vertex(p);
316    let start_q = bottom_vertex(q);
317    let n = p.len();
318    let m = q.len();
319    let p_rot: Vec<Point2> = (0..n).map(|i| p[(start_p + i) % n]).collect();
320    let q_rot: Vec<Point2> = (0..m).map(|i| q[(start_q + i) % m]).collect();
321    let edges_p: Vec<Point2> = (0..n).map(|i| p_rot[(i + 1) % n].sub(p_rot[i])).collect();
322    let edges_q: Vec<Point2> = (0..m).map(|i| q_rot[(i + 1) % m].sub(q_rot[i])).collect();
323    let mut result = Vec::with_capacity(n + m);
324    let mut cur = p_rot[0].add(q_rot[0]);
325    result.push(cur);
326    let mut i = 0;
327    let mut j = 0;
328    while i < n || j < m {
329        let ep = if i < n {
330            edges_p[i]
331        } else {
332            Point2::new(1e18, 1e18)
333        };
334        let eq = if j < m {
335            edges_q[j]
336        } else {
337            Point2::new(1e18, 1e18)
338        };
339        let cross = ep.cross(eq);
340        let next_edge = if cross > 0.0 || j >= m {
341            i += 1;
342            ep
343        } else if cross < 0.0 || i >= n {
344            j += 1;
345            eq
346        } else {
347            i += 1;
348            j += 1;
349            ep.add(eq)
350        };
351        if i <= n || j <= m {
352            cur = cur.add(next_edge);
353            result.push(cur);
354        }
355    }
356    if result.len() > 1 {
357        let last = *result.last().expect("collection should not be empty");
358        let first = result[0];
359        if last.dist_sq(first) < 1e-20 {
360            result.pop();
361        }
362    }
363    result
364}
365/// Find the index of the bottom-most (then left-most) vertex.
366pub(super) fn bottom_vertex(poly: &[Point2]) -> usize {
367    let mut best = 0;
368    for i in 1..poly.len() {
369        let b = poly[best];
370        let c = poly[i];
371        if c.y < b.y || (c.y == b.y && c.x < b.x) {
372            best = i;
373        }
374    }
375    best
376}
377/// Intersection point of two lines (if they are not parallel).
378pub fn line_intersect_2d(l1: &Line2D, l2: &Line2D) -> Option<Point2> {
379    let det = l1.a * l2.b - l2.a * l1.b;
380    if det.abs() < 1e-15 {
381        return None;
382    }
383    let x = (l1.c * l2.b - l2.c * l1.b) / det;
384    let y = (l1.a * l2.c - l2.a * l1.c) / det;
385    Some(Point2::new(x, y))
386}
387/// Test whether the open segment (p, q) is visible (does not cross any blocking edge).
388///
389/// Endpoint coincidences are permitted (two vertices of the same polygon can see
390/// each other if they are adjacent, handled by the caller).
391pub(super) fn segment_visible(p: Point2, q: Point2, blocking: &[(Point2, Point2)]) -> bool {
392    for &(a, b) in blocking {
393        if segments_properly_intersect(p, q, a, b) {
394            return false;
395        }
396    }
397    true
398}
399/// Test whether two open segments properly intersect (not at shared endpoints).
400pub(super) fn segments_properly_intersect(p: Point2, q: Point2, a: Point2, b: Point2) -> bool {
401    let eps = 1e-12;
402    if p.dist_sq(a) < eps || p.dist_sq(b) < eps || q.dist_sq(a) < eps || q.dist_sq(b) < eps {
403        return false;
404    }
405    let d1 = Point2::cross2(a, b, p);
406    let d2 = Point2::cross2(a, b, q);
407    let d3 = Point2::cross2(p, q, a);
408    let d4 = Point2::cross2(p, q, b);
409    if d1 * d2 < 0.0 && d3 * d4 < 0.0 {
410        return true;
411    }
412    false
413}
414/// Greedy vertex-guard approximation for the art gallery problem.
415///
416/// Given a simple polygon (vertices in CCW order), iteratively places a guard
417/// at the vertex that covers the maximum number of currently uncovered vertices,
418/// until all vertices are covered or `max_guards` is reached.
419///
420/// This is a set-cover greedy approximation with ratio O(log n).
421///
422/// Returns a [`ArtGalleryResult`] describing the guards and coverage.
423pub fn art_gallery_greedy(poly: &[Point2], max_guards: usize) -> ArtGalleryResult {
424    let n = poly.len();
425    if n == 0 {
426        return ArtGalleryResult {
427            guards: vec![],
428            covered: vec![],
429        };
430    }
431    let edges: Vec<(Point2, Point2)> = (0..n).map(|i| (poly[i], poly[(i + 1) % n])).collect();
432    let vis: Vec<Vec<bool>> = (0..n)
433        .map(|i| {
434            (0..n)
435                .map(|j| {
436                    if i == j {
437                        true
438                    } else {
439                        segment_visible(poly[i], poly[j], &edges)
440                    }
441                })
442                .collect()
443        })
444        .collect();
445    let mut covered = vec![false; n];
446    let mut guards = Vec::new();
447    for _ in 0..max_guards {
448        if covered.iter().all(|&c| c) {
449            break;
450        }
451        let best = (0..n)
452            .max_by_key(|&i| (0..n).filter(|&j| !covered[j] && vis[i][j]).count())
453            .expect("operation should succeed");
454        guards.push(best);
455        for j in 0..n {
456            if vis[best][j] {
457                covered[j] = true;
458            }
459        }
460    }
461    ArtGalleryResult { guards, covered }
462}
463/// Check if every vertex of a polygon is covered by the given set of guards.
464///
465/// Returns `true` if full coverage is achieved.
466pub fn check_full_coverage(poly: &[Point2], guards: &[usize]) -> bool {
467    let n = poly.len();
468    let edges: Vec<(Point2, Point2)> = (0..n).map(|i| (poly[i], poly[(i + 1) % n])).collect();
469    for j in 0..n {
470        let seen = guards
471            .iter()
472            .any(|&g| g == j || segment_visible(poly[g], poly[j], &edges));
473        if !seen {
474            return false;
475        }
476    }
477    true
478}
479/// Circumcircle of three 2D points. Returns (center, radius²) or None if degenerate.
480pub fn circumcircle_2d(pa: Point2, pb: Point2, pc: Point2) -> Option<(Point2, f64)> {
481    let ax = pb.x - pa.x;
482    let ay = pb.y - pa.y;
483    let bx = pc.x - pa.x;
484    let by = pc.y - pa.y;
485    let d = 2.0 * (ax * by - ay * bx);
486    if d.abs() < 1e-12 {
487        return None;
488    }
489    let ux = (by * (ax * ax + ay * ay) - ay * (bx * bx + by * by)) / d;
490    let uy = (ax * (bx * bx + by * by) - bx * (ax * ax + ay * ay)) / d;
491    let cx = pa.x + ux;
492    let cy = pa.y + uy;
493    let center = Point2::new(cx, cy);
494    let r2 = center.dist_sq(pa);
495    Some((center, r2))
496}
497/// Test if point `p` lies inside the circumcircle of triangle (a, b, c).
498pub fn in_circumcircle_2d(pa: Point2, pb: Point2, pc: Point2, p: Point2) -> bool {
499    match circumcircle_2d(pa, pb, pc) {
500        Some((center, r2)) => center.dist_sq(p) < r2 - 1e-12,
501        None => false,
502    }
503}
504/// Bowyer-Watson incremental Delaunay triangulation for 2D points.
505///
506/// Returns the list of [`DelaunayTri`] triangles (indices into `points`).
507/// Points are assumed to be in general position (no four co-circular).
508pub fn delaunay_2d(points: &[Point2]) -> Vec<DelaunayTri> {
509    let n = points.len();
510    if n < 3 {
511        return vec![];
512    }
513    let min_x = points.iter().map(|p| p.x).fold(f64::INFINITY, f64::min);
514    let max_x = points.iter().map(|p| p.x).fold(f64::NEG_INFINITY, f64::max);
515    let min_y = points.iter().map(|p| p.y).fold(f64::INFINITY, f64::min);
516    let max_y = points.iter().map(|p| p.y).fold(f64::NEG_INFINITY, f64::max);
517    let dx = max_x - min_x;
518    let dy = max_y - min_y;
519    let delta = dx.max(dy) * 3.0 + 1.0;
520    let mx = (min_x + max_x) / 2.0;
521    let my = (min_y + max_y) / 2.0;
522    let mut all_pts = points.to_vec();
523    all_pts.push(Point2::new(mx - 20.0 * delta, my - delta));
524    all_pts.push(Point2::new(mx, my + 20.0 * delta));
525    all_pts.push(Point2::new(mx + 20.0 * delta, my - delta));
526    let s0 = n;
527    let s1 = n + 1;
528    let s2 = n + 2;
529    let mut triangles: Vec<DelaunayTri> = vec![DelaunayTri {
530        a: s0,
531        b: s1,
532        c: s2,
533    }];
534    for i in 0..n {
535        let pt = all_pts[i];
536        let (bad, good): (Vec<_>, Vec<_>) = triangles
537            .into_iter()
538            .partition(|t| in_circumcircle_2d(all_pts[t.a], all_pts[t.b], all_pts[t.c], pt));
539        let mut boundary: Vec<(usize, usize)> = Vec::new();
540        for t in &bad {
541            let edges = [(t.a, t.b), (t.b, t.c), (t.c, t.a)];
542            for (ea, eb) in edges {
543                let shared = bad.iter().any(|u| {
544                    u != t
545                        && ([(u.a, u.b), (u.b, u.c), (u.c, u.a)]
546                            .iter()
547                            .any(|&(ua, ub)| ua == eb && ub == ea))
548                });
549                if !shared {
550                    boundary.push((ea, eb));
551                }
552            }
553        }
554        triangles = good;
555        for (ea, eb) in boundary {
556            triangles.push(DelaunayTri { a: ea, b: eb, c: i });
557        }
558    }
559    triangles.retain(|t| t.a < n && t.b < n && t.c < n);
560    triangles
561}
562/// Compute the dual Voronoi diagram from a Delaunay triangulation.
563///
564/// For each site point, collects all Delaunay triangles incident to that site
565/// and returns their circumcenters as the Voronoi polygon vertices.
566pub fn voronoi_from_delaunay(points: &[Point2], tris: &[DelaunayTri]) -> Vec<VoronoiCell2D> {
567    let n = points.len();
568    let mut cells: Vec<VoronoiCell2D> = (0..n)
569        .map(|i| VoronoiCell2D {
570            site: i,
571            circumcenters: Vec::new(),
572        })
573        .collect();
574    for t in tris {
575        if let Some((center, _)) = circumcircle_2d(points[t.a], points[t.b], points[t.c]) {
576            cells[t.a].circumcenters.push(center);
577            cells[t.b].circumcenters.push(center);
578            cells[t.c].circumcenters.push(center);
579        }
580    }
581    for cell in &mut cells {
582        let site = points[cell.site];
583        cell.circumcenters.sort_by(|a, b| {
584            let ta = (a.y - site.y).atan2(a.x - site.x);
585            let tb = (b.y - site.y).atan2(b.x - site.x);
586            ta.partial_cmp(&tb).unwrap_or(std::cmp::Ordering::Equal)
587        });
588    }
589    cells
590}
591/// Test if three 2D points are in counter-clockwise orientation.
592pub fn ccw(a: Point2, b: Point2, c: Point2) -> bool {
593    Point2::cross2(a, b, c) > 0.0
594}
595/// Test if three 2D points are collinear (within tolerance).
596pub fn collinear(a: Point2, b: Point2, c: Point2) -> bool {
597    Point2::cross2(a, b, c).abs() < 1e-10
598}
599/// Test if point `p` lies inside the triangle (a, b, c) using barycentric coordinates.
600///
601/// Returns `true` if strictly inside, `false` on boundary or outside.
602pub fn point_in_triangle(p: Point2, a: Point2, b: Point2, c: Point2) -> bool {
603    let d1 = Point2::cross2(p, a, b);
604    let d2 = Point2::cross2(p, b, c);
605    let d3 = Point2::cross2(p, c, a);
606    let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
607    let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
608    !(has_neg && has_pos)
609}
610/// Test if point `p` lies inside a convex polygon (given in CCW order).
611pub fn point_in_convex_polygon(p: Point2, poly: &[Point2]) -> bool {
612    let n = poly.len();
613    if n < 3 {
614        return false;
615    }
616    for i in 0..n {
617        let a = poly[i];
618        let b = poly[(i + 1) % n];
619        if Point2::cross2(a, b, p) < 0.0 {
620            return false;
621        }
622    }
623    true
624}
625/// Compute the 2D convex hull of a point set (Graham scan).
626///
627/// Returns vertices in CCW order.
628pub fn convex_hull_2d(points: &[Point2]) -> Vec<Point2> {
629    let mut pts = points.to_vec();
630    if pts.len() < 3 {
631        return pts;
632    }
633    pts.sort_by(|a, b| {
634        a.x.partial_cmp(&b.x)
635            .expect("operation should succeed")
636            .then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
637    });
638    pts.dedup_by(|a, b| a.dist_sq(*b) < 1e-20);
639    let n = pts.len();
640    let mut hull: Vec<Point2> = Vec::with_capacity(2 * n);
641    for &p in &pts {
642        while hull.len() >= 2 {
643            let a = hull[hull.len() - 2];
644            let b = hull[hull.len() - 1];
645            if Point2::cross2(a, b, p) <= 0.0 {
646                hull.pop();
647            } else {
648                break;
649            }
650        }
651        hull.push(p);
652    }
653    let lower_len = hull.len();
654    for &p in pts.iter().rev() {
655        while hull.len() > lower_len {
656            let a = hull[hull.len() - 2];
657            let b = hull[hull.len() - 1];
658            if Point2::cross2(a, b, p) <= 0.0 {
659                hull.pop();
660            } else {
661                break;
662            }
663        }
664        hull.push(p);
665    }
666    hull.pop();
667    hull
668}