Skip to main content

geometry_overlay/
surface_point.rs

1//! OVL6.T3 — `point_on_surface`.
2//!
3//! Mirrors `boost/geometry/algorithms/point_on_surface.hpp`: pick a
4//! point **guaranteed to lie in the interior** of an areal geometry.
5//! Boost uses a horizontal sweep at a representative height and takes
6//! the midpoint of the widest interior span the scanline cuts; the port
7//! does the same.
8//!
9//! Unlike the centroid, this point is always inside the polygon even for
10//! non-convex or holed shapes — which is exactly why overlay,
11//! labelling, and `relate` need it.
12
13use alloc::vec::Vec;
14
15use geometry_coords::CoordinateScalar;
16use geometry_trait::{Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait};
17
18/// A point guaranteed to lie in the interior of `polygon`, or `None`
19/// for a degenerate (zero-area) polygon.
20///
21/// Sweeps a horizontal line at the average of the exterior ring's
22/// minimum and maximum y, collects the x-values where it crosses the
23/// boundary (exterior and holes), and returns the midpoint of the
24/// widest gap between consecutive crossings that lies in the interior.
25///
26/// Mirrors `boost::geometry::point_on_surface`
27/// (`algorithms/point_on_surface.hpp`).
28///
29/// # Examples
30///
31/// ```
32/// use geometry_cs::Cartesian;
33/// use geometry_model::{polygon, Point2D, Polygon};
34/// use geometry_overlay::surface_point::point_on_surface;
35/// use geometry_trait::Point as _;
36///
37/// type P = Point2D<f64, Cartesian>;
38/// let pg: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)]];
39/// let p = point_on_surface(&pg).unwrap();
40/// // The representative point is inside the square.
41/// assert!(p.get::<0>() > 0.0 && p.get::<0>() < 4.0);
42/// assert!(p.get::<1>() > 0.0 && p.get::<1>() < 4.0);
43/// ```
44#[inline]
45#[must_use]
46pub fn point_on_surface<G, P>(polygon: &G) -> Option<P>
47where
48    G: PolygonTrait<Point = P>,
49    P: PointMut + Default + Copy,
50    P::Scalar: CoordinateScalar,
51{
52    let outer: Vec<P> = polygon.exterior().points().copied().collect();
53    if outer.len() < 3 {
54        return None;
55    }
56
57    // Representative sweep height: the average of the exterior's y-range.
58    let mut ymin = outer[0].get::<1>();
59    let mut ymax = ymin;
60    for p in &outer {
61        let y = p.get::<1>();
62        if y < ymin {
63            ymin = y;
64        }
65        if y > ymax {
66            ymax = y;
67        }
68    }
69    let two = P::Scalar::ONE + P::Scalar::ONE;
70    let sweep_y = (ymin + ymax) / two;
71
72    // Collect x-crossings of the sweep line with every ring.
73    let mut xs: Vec<P::Scalar> = Vec::new();
74    collect_crossings(&outer, sweep_y, &mut xs);
75    for hole in polygon.interiors() {
76        let hpts: Vec<P> = hole.points().copied().collect();
77        collect_crossings(&hpts, sweep_y, &mut xs);
78    }
79
80    if xs.len() < 2 {
81        return None;
82    }
83    xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
84
85    // The interior spans are the odd gaps (between crossing 0-1, 2-3, …).
86    // Take the midpoint of the widest such span.
87    let mut best: Option<(P::Scalar, P::Scalar)> = None; // (width, mid_x)
88    let mut i = 0;
89    while i + 1 < xs.len() {
90        let lo = xs[i];
91        let hi = xs[i + 1];
92        let width = hi - lo;
93        let mid = (lo + hi) / two;
94        match best {
95            Some((bw, _)) if bw >= width => {}
96            _ => best = Some((width, mid)),
97        }
98        i += 2;
99    }
100
101    let (_, mid_x) = best?;
102    let mut p = P::default();
103    p.set::<0>(mid_x);
104    p.set::<1>(sweep_y);
105    Some(p)
106}
107
108/// Append the x-coordinates where the horizontal line `y = sweep_y`
109/// crosses the edges of the vertex ring `pts`.
110fn collect_crossings<P>(pts: &[P], sweep_y: P::Scalar, out: &mut Vec<P::Scalar>)
111where
112    P: Point,
113    P::Scalar: CoordinateScalar,
114{
115    let n = pts.len();
116    if n < 2 {
117        return;
118    }
119    for k in 0..n {
120        let a = &pts[k];
121        let b = &pts[(k + 1) % n];
122        let ay = a.get::<1>();
123        let by = b.get::<1>();
124        // Half-open crossing test to avoid double-counting a shared
125        // vertex: the edge crosses the sweep if exactly one endpoint is
126        // strictly above it.
127        if (ay > sweep_y) != (by > sweep_y) {
128            let ax = a.get::<0>();
129            let bx = b.get::<0>();
130            let t = (sweep_y - ay) / (by - ay);
131            out.push(ax + t * (bx - ax));
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    //! OVL6.T3 done-when: the returned point is inside the polygon.
139    //! Mirrors `test/algorithms/point_on_surface.cpp`.
140
141    use super::point_on_surface;
142    use geometry_algorithm::within;
143    use geometry_cs::Cartesian;
144    use geometry_model::{Point2D, Polygon, polygon};
145
146    type P = Point2D<f64, Cartesian>;
147
148    #[test]
149    fn inside_a_square() {
150        let pg: Polygon<P> = polygon![[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0), (0.0, 0.0)]];
151        let p = point_on_surface(&pg).unwrap();
152        assert!(within(&p, &pg));
153    }
154
155    #[test]
156    fn inside_an_l_shape() {
157        // Non-convex L: the centroid could fall outside, but the sweep
158        // point must be inside.
159        let pg: Polygon<P> = polygon![[
160            (0.0, 0.0),
161            (6.0, 0.0),
162            (6.0, 2.0),
163            (2.0, 2.0),
164            (2.0, 6.0),
165            (0.0, 6.0),
166            (0.0, 0.0)
167        ]];
168        let p = point_on_surface(&pg).unwrap();
169        assert!(within(&p, &pg));
170    }
171
172    #[test]
173    fn avoids_a_hole() {
174        // A big square with a big central hole; the representative point
175        // must land in the ring of material, not the hole.
176        let pg: Polygon<P> = polygon![
177            [
178                (0.0, 0.0),
179                (10.0, 0.0),
180                (10.0, 10.0),
181                (0.0, 10.0),
182                (0.0, 0.0)
183            ],
184            [(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0), (3.0, 3.0)]
185        ];
186        let p = point_on_surface(&pg).unwrap();
187        assert!(within(&p, &pg));
188    }
189}