1use crate::MathError;
8use crate::vec::Point2;
9
10const DEFAULT_MITER_LIMIT: f64 = 2.0;
13
14pub fn offset_polygon_2d(
32 vertices: &[Point2],
33 distance: f64,
34 tolerance: f64,
35) -> Result<Vec<Point2>, MathError> {
36 let n = vertices.len();
37 if n < 3 {
38 return Err(MathError::EmptyInput);
39 }
40
41 let miter_limit_dist = distance.abs() * DEFAULT_MITER_LIMIT;
42
43 let area2 = signed_area_2x(vertices);
48 if area2.abs() < tolerance {
49 return Err(MathError::EmptyInput);
50 }
51 let distance = if area2 < 0.0 { -distance } else { distance };
52
53 let mut result = Vec::with_capacity(n);
56
57 for i in 0..n {
58 let prev = (i + n - 1) % n;
59 let next = (i + 1) % n;
60
61 let (nx0, ny0) = edge_outward_normal(vertices[prev], vertices[i]);
63 let (nx1, ny1) = edge_outward_normal(vertices[i], vertices[next]);
65
66 let p0 = Point2::new(
70 vertices[i].x() + distance * nx0,
71 vertices[i].y() + distance * ny0,
72 );
73 let p1 = Point2::new(
74 vertices[i].x() + distance * nx1,
75 vertices[i].y() + distance * ny1,
76 );
77
78 let d0x = vertices[i].x() - vertices[prev].x();
79 let d0y = vertices[i].y() - vertices[prev].y();
80 let d1x = vertices[next].x() - vertices[i].x();
81 let d1y = vertices[next].y() - vertices[i].y();
82
83 let cross = d0x * d1y - d0y * d1x;
86
87 if cross.abs() < tolerance {
88 let avg_nx = (nx0 + nx1) * 0.5;
90 let avg_ny = (ny0 + ny1) * 0.5;
91 result.push(Point2::new(
92 vertices[i].x() + distance * avg_nx,
93 vertices[i].y() + distance * avg_ny,
94 ));
95 } else {
96 let dx = p1.x() - p0.x();
98 let dy = p1.y() - p0.y();
99 let t = (dx * d1y - dy * d1x) / cross;
100 let miter_pt = Point2::new(p0.x() + t * d0x, p0.y() + t * d0y);
101
102 let miter_dist = ((miter_pt.x() - vertices[i].x()).powi(2)
105 + (miter_pt.y() - vertices[i].y()).powi(2))
106 .sqrt();
107
108 if miter_dist > miter_limit_dist && miter_limit_dist > tolerance {
109 result.push(Point2::new(
111 (p0.x() + p1.x()) * 0.5,
112 (p0.y() + p1.y()) * 0.5,
113 ));
114 } else {
115 result.push(miter_pt);
116 }
117 }
118 }
119
120 remove_self_intersections(&mut result, tolerance);
122
123 Ok(result)
124}
125
126fn remove_self_intersections(polygon: &mut Vec<Point2>, tolerance: f64) {
129 if polygon.len() < 4 {
130 return;
131 }
132
133 let n = polygon.len();
134 let tol_sq = tolerance * tolerance;
135
136 let mut i = 0;
140 while i < polygon.len().saturating_sub(2) {
141 let n_cur = polygon.len();
142 let a1 = polygon[i];
143 let a2 = polygon[(i + 1) % n_cur];
144 let mut found = false;
145
146 let mut j = i + 2;
148 while j < n_cur {
149 if i == 0 && j == n_cur - 1 {
151 j += 1;
152 continue;
153 }
154
155 let b1 = polygon[j];
156 let b2 = polygon[(j + 1) % n_cur];
157
158 if let Some(_pt) = segment_intersection_2d(a1, a2, b1, b2, tol_sq) {
159 let loop_len = j - i - 1;
162 let other_len = n_cur - loop_len;
163
164 if loop_len <= other_len {
165 polygon.drain((i + 1)..j);
167 } else {
168 let kept: Vec<Point2> = polygon[i..=j].to_vec();
170 *polygon = kept;
171 }
172 found = true;
173 break;
174 }
175 j += 1;
176 }
177
178 if !found {
179 i += 1;
180 }
181 if polygon.len() < 3 || polygon.len() > n * 2 {
185 break;
186 }
187 }
188}
189
190fn segment_intersection_2d(
192 a1: Point2,
193 a2: Point2,
194 b1: Point2,
195 b2: Point2,
196 _tol_sq: f64,
197) -> Option<Point2> {
198 let dx_a = a2.x() - a1.x();
199 let dy_a = a2.y() - a1.y();
200 let dx_b = b2.x() - b1.x();
201 let dy_b = b2.y() - b1.y();
202
203 let denom = dx_a * dy_b - dy_a * dx_b;
204 if denom.abs() < 1e-15 {
205 return None; }
207
208 let dx_ab = b1.x() - a1.x();
209 let dy_ab = b1.y() - a1.y();
210 let t = (dx_ab * dy_b - dy_ab * dx_b) / denom;
211 let s = (dx_ab * dy_a - dy_ab * dx_a) / denom;
212
213 let eps = 1e-10;
215 if t > eps && t < 1.0 - eps && s > eps && s < 1.0 - eps {
216 Some(Point2::new(a1.x() + t * dx_a, a1.y() + t * dy_a))
217 } else {
218 None
219 }
220}
221
222fn signed_area_2x(vertices: &[Point2]) -> f64 {
226 let n = vertices.len();
227 let mut acc = 0.0;
228 for i in 0..n {
229 let j = (i + 1) % n;
230 acc += vertices[i].x() * vertices[j].y() - vertices[j].x() * vertices[i].y();
231 }
232 acc
233}
234
235fn edge_outward_normal(a: Point2, b: Point2) -> (f64, f64) {
240 let dx = b.x() - a.x();
241 let dy = b.y() - a.y();
242 let len = (dx * dx + dy * dy).sqrt();
243 if len < 1e-15 {
244 return (0.0, 0.0);
245 }
246 (dy / len, -dx / len)
248}
249
250#[cfg(test)]
251mod tests {
252 #![allow(clippy::unwrap_used)]
253
254 use super::*;
255
256 #[test]
257 fn offset_square_outward() {
258 let square = vec![
259 Point2::new(0.0, 0.0),
260 Point2::new(1.0, 0.0),
261 Point2::new(1.0, 1.0),
262 Point2::new(0.0, 1.0),
263 ];
264 let result = offset_polygon_2d(&square, 0.5, 1e-10).unwrap();
265 assert_eq!(result.len(), 4);
266 assert!((result[0].x() - (-0.5)).abs() < 1e-10);
268 assert!((result[0].y() - (-0.5)).abs() < 1e-10);
269 assert!((result[1].x() - 1.5).abs() < 1e-10);
270 assert!((result[1].y() - (-0.5)).abs() < 1e-10);
271 assert!((result[2].x() - 1.5).abs() < 1e-10);
272 assert!((result[2].y() - 1.5).abs() < 1e-10);
273 assert!((result[3].x() - (-0.5)).abs() < 1e-10);
274 assert!((result[3].y() - 1.5).abs() < 1e-10);
275 }
276
277 #[test]
278 fn offset_square_inward() {
279 let square = vec![
280 Point2::new(0.0, 0.0),
281 Point2::new(2.0, 0.0),
282 Point2::new(2.0, 2.0),
283 Point2::new(0.0, 2.0),
284 ];
285 let result = offset_polygon_2d(&square, -0.5, 1e-10).unwrap();
286 assert_eq!(result.len(), 4);
287 assert!((result[0].x() - 0.5).abs() < 1e-10);
288 assert!((result[0].y() - 0.5).abs() < 1e-10);
289 assert!((result[1].x() - 1.5).abs() < 1e-10);
290 assert!((result[1].y() - 0.5).abs() < 1e-10);
291 }
292
293 #[test]
294 fn offset_triangle() {
295 let tri = vec![
296 Point2::new(0.0, 0.0),
297 Point2::new(4.0, 0.0),
298 Point2::new(2.0, 3.0),
299 ];
300 let result = offset_polygon_2d(&tri, 0.1, 1e-10).unwrap();
301 assert_eq!(result.len(), 3);
302 let cx = (tri[0].x() + tri[1].x() + tri[2].x()) / 3.0;
304 let cy = (tri[0].y() + tri[1].y() + tri[2].y()) / 3.0;
305 for (orig, off) in tri.iter().zip(result.iter()) {
306 let d_orig = ((orig.x() - cx).powi(2) + (orig.y() - cy).powi(2)).sqrt();
307 let d_off = ((off.x() - cx).powi(2) + (off.y() - cy).powi(2)).sqrt();
308 assert!(
309 d_off > d_orig,
310 "offset vertex should be farther from centroid"
311 );
312 }
313 }
314
315 #[test]
316 fn too_few_vertices() {
317 let pts = vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)];
318 assert!(offset_polygon_2d(&pts, 0.1, 1e-10).is_err());
319 }
320
321 fn signed_area(poly: &[Point2]) -> f64 {
322 let n = poly.len();
323 let mut acc = 0.0;
324 for i in 0..n {
325 let j = (i + 1) % n;
326 acc += poly[i].x() * poly[j].y() - poly[j].x() * poly[i].y();
327 }
328 acc * 0.5
329 }
330
331 #[test]
332 fn offset_cw_square_inward() {
333 let cw_square = vec![
336 Point2::new(20.0, 0.0),
337 Point2::new(0.0, 0.0),
338 Point2::new(0.0, 20.0),
339 Point2::new(20.0, 20.0),
340 ];
341 let inward = offset_polygon_2d(&cw_square, -2.0, 1e-10).unwrap();
342 assert_eq!(inward.len(), 4);
343 assert!(
344 (signed_area(&inward).abs() - 256.0).abs() < 1e-6,
345 "CW square offset -2 should enclose area 256, got {}",
346 signed_area(&inward).abs()
347 );
348
349 let outward = offset_polygon_2d(&cw_square, 2.0, 1e-10).unwrap();
350 assert_eq!(outward.len(), 4);
351 assert!(
352 (signed_area(&outward).abs() - 576.0).abs() < 1e-6,
353 "CW square offset +2 should enclose area 576, got {}",
354 signed_area(&outward).abs()
355 );
356 }
357}