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}