1use crate::polygon;
27
28const DEFAULT_MITER_LIMIT: f64 = 2.0;
30
31const EPS: f64 = 1e-10;
33
34pub fn offset_polygon(polygon: &[(f64, f64)], distance: f64) -> Vec<Vec<(f64, f64)>> {
62 offset_polygon_miter(polygon, distance, DEFAULT_MITER_LIMIT)
63}
64
65pub fn offset_polygon_miter(
81 polygon: &[(f64, f64)],
82 distance: f64,
83 miter_limit: f64,
84) -> Vec<Vec<(f64, f64)>> {
85 if polygon.len() < 3 {
86 return Vec::new();
87 }
88
89 if distance.abs() < EPS {
90 return vec![polygon.to_vec()];
91 }
92
93 let poly = polygon::ensure_ccw(polygon);
94 let n = poly.len();
95
96 let offset_edges: Vec<((f64, f64), (f64, f64))> = (0..n)
98 .map(|i| {
99 let j = (i + 1) % n;
100 offset_edge(poly[i], poly[j], distance)
101 })
102 .collect();
103
104 let mut raw_vertices: Vec<(f64, f64)> = Vec::new();
106
107 for i in 0..n {
108 let prev = if i == 0 { n - 1 } else { i - 1 };
109
110 let (ep0, ep1) = offset_edges[prev];
111 let (ec0, ec1) = offset_edges[i];
112
113 let dir_prev = (ep1.0 - ep0.0, ep1.1 - ep0.1);
114 let dir_curr = (ec1.0 - ec0.0, ec1.1 - ec0.1);
115
116 if let Some(pt) = line_intersection(ep0, dir_prev, ec0, dir_curr) {
117 let miter_dist_sq = (pt.0 - poly[i].0).powi(2) + (pt.1 - poly[i].1).powi(2);
120 let limit_sq = (miter_limit * distance.abs() * (1.0 + 1e-4)).powi(2);
121
122 if miter_dist_sq > limit_sq {
123 raw_vertices.push(ep1);
124 raw_vertices.push(ec0);
125 } else {
126 raw_vertices.push(pt);
127 }
128 } else {
129 raw_vertices.push(ep1);
130 }
131 }
132
133 let raw_vertices = remove_consecutive_duplicates(&raw_vertices);
134
135 if raw_vertices.len() < 3 {
136 return Vec::new();
137 }
138
139 let mut result = resolve_self_intersections(&raw_vertices);
140
141 if distance < 0.0 {
147 let abs_d = distance.abs();
148 let tol = abs_d * 0.1 + EPS; result.retain(|ring| {
151 ring.iter().all(|&v| {
153 let min_dist = min_distance_to_polygon_edges(&poly, v);
154 min_dist >= abs_d - tol
155 })
156 });
157 }
158
159 result
160}
161
162fn min_distance_to_polygon_edges(polygon: &[(f64, f64)], point: (f64, f64)) -> f64 {
164 let n = polygon.len();
165 let mut min_d = f64::MAX;
166
167 for i in 0..n {
168 let j = (i + 1) % n;
169 let d = point_to_segment_distance(point, polygon[i], polygon[j]);
170 if d < min_d {
171 min_d = d;
172 }
173 }
174
175 min_d
176}
177
178fn point_to_segment_distance(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
180 let dx = b.0 - a.0;
181 let dy = b.1 - a.1;
182 let len_sq = dx * dx + dy * dy;
183
184 if len_sq < EPS {
185 return ((p.0 - a.0).powi(2) + (p.1 - a.1).powi(2)).sqrt();
186 }
187
188 let t = ((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len_sq;
190 let t = t.clamp(0.0, 1.0);
191
192 let proj = (a.0 + t * dx, a.1 + t * dy);
193 ((p.0 - proj.0).powi(2) + (p.1 - proj.1).powi(2)).sqrt()
194}
195
196pub fn offset_edge(p0: (f64, f64), p1: (f64, f64), distance: f64) -> ((f64, f64), (f64, f64)) {
201 let dx = p1.0 - p0.0;
202 let dy = p1.1 - p0.1;
203 let len = (dx * dx + dy * dy).sqrt();
204
205 if len < EPS {
206 return (p0, p1);
207 }
208
209 let nx = dy / len * distance;
210 let ny = -dx / len * distance;
211
212 ((p0.0 + nx, p0.1 + ny), (p1.0 + nx, p1.1 + ny))
213}
214
215pub fn line_intersection(
224 p1: (f64, f64),
225 d1: (f64, f64),
226 p2: (f64, f64),
227 d2: (f64, f64),
228) -> Option<(f64, f64)> {
229 let cross = d1.0 * d2.1 - d1.1 * d2.0;
230
231 if cross.abs() < EPS {
232 return None;
233 }
234
235 let dp = (p2.0 - p1.0, p2.1 - p1.1);
236 let t = (dp.0 * d2.1 - dp.1 * d2.0) / cross;
237
238 Some((p1.0 + t * d1.0, p1.1 + t * d1.1))
239}
240
241fn segment_intersection(
242 a0: (f64, f64),
243 a1: (f64, f64),
244 b0: (f64, f64),
245 b1: (f64, f64),
246) -> Option<(f64, f64, (f64, f64))> {
247 let da = (a1.0 - a0.0, a1.1 - a0.1);
248 let db = (b1.0 - b0.0, b1.1 - b0.1);
249 let cross = da.0 * db.1 - da.1 * db.0;
250
251 if cross.abs() < EPS {
252 return None;
253 }
254
255 let dp = (b0.0 - a0.0, b0.1 - a0.1);
256 let t = (dp.0 * db.1 - dp.1 * db.0) / cross;
257 let u = (dp.0 * da.1 - dp.1 * da.0) / cross;
258
259 if t > EPS && t < 1.0 - EPS && u > EPS && u < 1.0 - EPS {
260 let pt = (a0.0 + t * da.0, a0.1 + t * da.1);
261 Some((t, u, pt))
262 } else {
263 None
264 }
265}
266
267fn remove_consecutive_duplicates(vertices: &[(f64, f64)]) -> Vec<(f64, f64)> {
268 if vertices.is_empty() {
269 return Vec::new();
270 }
271
272 let mut result = vec![vertices[0]];
273
274 for &v in &vertices[1..] {
275 let last = result
276 .last()
277 .expect("result is non-empty after initial push");
278 if (v.0 - last.0).abs() > EPS || (v.1 - last.1).abs() > EPS {
279 result.push(v);
280 }
281 }
282
283 if result.len() > 1 {
284 let first = result[0];
285 let last = result.last().expect("result has at least 2 elements");
286 if (first.0 - last.0).abs() < EPS && (first.1 - last.1).abs() < EPS {
287 result.pop();
288 }
289 }
290
291 result
292}
293
294fn resolve_self_intersections(vertices: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
295 let n = vertices.len();
296
297 let mut event_edge_i: Vec<usize> = Vec::new();
298 let mut event_edge_j: Vec<usize> = Vec::new();
299 let mut event_t_i: Vec<f64> = Vec::new();
300 let mut event_t_j: Vec<f64> = Vec::new();
301 let mut event_pts: Vec<(f64, f64)> = Vec::new();
302
303 for i in 0..n {
304 let i_next = (i + 1) % n;
305 for j in (i + 2)..n {
306 if i == 0 && j == n - 1 {
307 continue;
308 }
309 let j_next = (j + 1) % n;
310
311 if let Some((t, u, pt)) =
312 segment_intersection(vertices[i], vertices[i_next], vertices[j], vertices[j_next])
313 {
314 event_edge_i.push(i);
315 event_edge_j.push(j);
316 event_t_i.push(t);
317 event_t_j.push(u);
318 event_pts.push(pt);
319 }
320 }
321 }
322
323 if event_edge_i.is_empty() {
324 let sa = polygon::signed_area(vertices);
325 if sa > EPS {
326 return vec![vertices.to_vec()];
327 }
328 return Vec::new();
329 }
330
331 let num_events = event_edge_i.len();
332 let mut edge_splits: Vec<Vec<(f64, usize)>> = vec![Vec::new(); n];
333
334 for ei in 0..num_events {
335 edge_splits[event_edge_i[ei]].push((event_t_i[ei], ei));
336 edge_splits[event_edge_j[ei]].push((event_t_j[ei], ei));
337 }
338
339 for splits in &mut edge_splits {
340 splits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
341 }
342
343 let mut node_points: Vec<(f64, f64)> = Vec::new();
344 let mut node_is_isect: Vec<bool> = Vec::new();
345 let mut isect_node_on_i: Vec<usize> = vec![0; num_events];
346 let mut isect_node_on_j: Vec<usize> = vec![0; num_events];
347
348 for i in 0..n {
349 node_points.push(vertices[i]);
350 node_is_isect.push(false);
351
352 for &(_, ei) in &edge_splits[i] {
353 let node_idx = node_points.len();
354 node_points.push(event_pts[ei]);
355 node_is_isect.push(true);
356
357 if event_edge_i[ei] == i {
358 isect_node_on_i[ei] = node_idx;
359 } else {
360 isect_node_on_j[ei] = node_idx;
361 }
362 }
363 }
364
365 let total = node_points.len();
366 let forward: Vec<usize> = (0..total).map(|i| (i + 1) % total).collect();
367
368 let mut jump: Vec<Option<usize>> = vec![None; total];
369 for ei in 0..num_events {
370 let ni = isect_node_on_i[ei];
371 let nj = isect_node_on_j[ei];
372 jump[ni] = Some(forward[nj]);
373 jump[nj] = Some(forward[ni]);
374 }
375
376 let mut visited = vec![false; total];
377 let mut result_loops: Vec<Vec<(f64, f64)>> = Vec::new();
378
379 for start in 0..total {
380 if visited[start] || !node_is_isect[start] {
381 continue;
382 }
383
384 for use_jump_first in [true, false] {
385 let first_step = if use_jump_first {
386 match jump[start] {
387 Some(j) => j,
388 None => continue,
389 }
390 } else {
391 forward[start]
392 };
393
394 let mut loop_pts = vec![node_points[start]];
395 let mut curr = first_step;
396 let mut steps = 0;
397 let max_steps = total + 1;
398
399 while curr != start && steps < max_steps {
400 loop_pts.push(node_points[curr]);
401 curr = match jump[curr].take() {
402 Some(j) => j,
403 None => forward[curr],
404 };
405 steps += 1;
406 }
407
408 if curr == start && loop_pts.len() >= 3 {
409 let sa = polygon::signed_area(&loop_pts);
410
411 if sa > EPS {
412 for &lp in &loop_pts {
413 for (idx, &np) in node_points.iter().enumerate() {
414 if (lp.0 - np.0).abs() < EPS && (lp.1 - np.1).abs() < EPS {
415 visited[idx] = true;
416 }
417 }
418 }
419 result_loops.push(loop_pts);
420 }
421 }
422 }
423 }
424
425 if result_loops.is_empty() {
426 let sa = polygon::signed_area(vertices);
427 if sa > EPS {
428 return vec![vertices.to_vec()];
429 }
430 return Vec::new();
431 }
432
433 result_loops
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 fn approx_area(polygon: &[(f64, f64)]) -> f64 {
441 polygon::area(polygon)
442 }
443
444 #[test]
445 fn test_offset_edge_horizontal() {
446 let ((q0x, q0y), (q1x, q1y)) = offset_edge((0.0, 0.0), (10.0, 0.0), 1.0);
447 assert!((q0y - (-1.0)).abs() < EPS);
448 assert!((q1y - (-1.0)).abs() < EPS);
449 assert!((q0x - 0.0).abs() < EPS);
450 assert!((q1x - 10.0).abs() < EPS);
451 }
452
453 #[test]
454 fn test_offset_edge_vertical() {
455 let ((q0x, q0y), (q1x, q1y)) = offset_edge((0.0, 0.0), (0.0, 10.0), 1.0);
456 assert!((q0x - 1.0).abs() < EPS);
457 assert!((q1x - 1.0).abs() < EPS);
458 assert!((q0y - 0.0).abs() < EPS);
459 assert!((q1y - 10.0).abs() < EPS);
460 }
461
462 #[test]
463 fn test_offset_edge_zero_length() {
464 let (q0, q1) = offset_edge((5.0, 5.0), (5.0, 5.0), 1.0);
465 assert!((q0.0 - 5.0).abs() < EPS);
466 assert!((q0.1 - 5.0).abs() < EPS);
467 assert!((q1.0 - 5.0).abs() < EPS);
468 assert!((q1.1 - 5.0).abs() < EPS);
469 }
470
471 #[test]
472 fn test_line_intersection_perpendicular() {
473 let pt = line_intersection((0.0, 0.0), (1.0, 0.0), (5.0, -5.0), (0.0, 1.0));
474 assert!(pt.is_some());
475 let (x, y) = pt.expect("perpendicular lines must intersect");
476 assert!((x - 5.0).abs() < EPS);
477 assert!((y - 0.0).abs() < EPS);
478 }
479
480 #[test]
481 fn test_line_intersection_parallel() {
482 let pt = line_intersection((0.0, 0.0), (1.0, 0.0), (0.0, 5.0), (1.0, 0.0));
483 assert!(pt.is_none());
484 }
485
486 #[test]
487 fn test_line_intersection_diagonal() {
488 let pt = line_intersection((0.0, 0.0), (1.0, 1.0), (10.0, 0.0), (-1.0, 1.0));
489 let (x, y) = pt.expect("diagonal lines must intersect");
490 assert!((x - 5.0).abs() < EPS);
491 assert!((y - 5.0).abs() < EPS);
492 }
493
494 #[test]
495 fn test_square_offset_outward() {
496 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
497 let result = offset_polygon(&square, 1.0);
498 assert!(!result.is_empty());
499 let a = approx_area(&result[0]);
500 assert!((a - 144.0).abs() < 1.0, "got {a}");
501 }
502
503 #[test]
504 fn test_square_offset_inward() {
505 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
506 let result = offset_polygon(&square, -1.0);
507 assert!(!result.is_empty());
508 let a = approx_area(&result[0]);
509 assert!((a - 64.0).abs() < 1.0, "got {a}");
510 }
511
512 #[test]
513 fn test_square_offset_area_formula() {
514 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
515 let d = 2.0;
516 let result = offset_polygon(&square, d);
517 assert!(!result.is_empty());
518 let a = approx_area(&result[0]);
519 let expected = 100.0 + 40.0 * d + 4.0 * d * d;
520 assert!((a - expected).abs() < 1.0, "got {a}, expected {expected}");
521 }
522
523 #[test]
524 fn test_triangle_offset_outward() {
525 let tri = [(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)];
526 let result = offset_polygon(&tri, 0.5);
527 assert!(!result.is_empty());
528 let original_area = approx_area(&tri);
529 let offset_area = approx_area(&result[0]);
530 assert!(
531 offset_area > original_area,
532 "{offset_area} <= {original_area}"
533 );
534 }
535
536 #[test]
537 fn test_triangle_offset_inward() {
538 let tri = [(0.0, 0.0), (20.0, 0.0), (10.0, 17.32)];
539 let result = offset_polygon(&tri, -1.0);
540 assert!(!result.is_empty());
541 let original_area = approx_area(&tri);
542 let offset_area = approx_area(&result[0]);
543 assert!(
544 offset_area < original_area,
545 "{offset_area} >= {original_area}"
546 );
547 }
548
549 #[test]
550 fn test_convex_polygon_offset_preserves_convexity() {
551 let hexagon: Vec<(f64, f64)> = (0..6)
552 .map(|i| {
553 let angle = std::f64::consts::PI / 3.0 * i as f64;
554 (50.0 + 20.0 * angle.cos(), 50.0 + 20.0 * angle.sin())
555 })
556 .collect();
557 let result = offset_polygon(&hexagon, 2.0);
558 assert!(!result.is_empty());
559 assert!(polygon::is_convex(&result[0]));
560 }
561
562 #[test]
563 fn test_zero_distance_returns_original() {
564 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
565 let result = offset_polygon(&square, 0.0);
566 assert_eq!(result.len(), 1);
567 assert_eq!(result[0].len(), square.len());
568 for (orig, rp) in square.iter().zip(result[0].iter()) {
569 assert!((orig.0 - rp.0).abs() < EPS);
570 assert!((orig.1 - rp.1).abs() < EPS);
571 }
572 }
573
574 #[test]
575 fn test_large_negative_offset_collapses() {
576 let square = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
577 let result = offset_polygon(&square, -6.0);
578 assert!(result.is_empty(), "got {} rings", result.len());
579 }
580
581 #[test]
582 fn test_l_shape_offset_outward() {
583 let l_shape = [
584 (0.0, 0.0),
585 (20.0, 0.0),
586 (20.0, 10.0),
587 (10.0, 10.0),
588 (10.0, 20.0),
589 (0.0, 20.0),
590 ];
591 let result = offset_polygon(&l_shape, 1.0);
592 assert!(!result.is_empty());
593 let original_area = approx_area(&l_shape);
594 let total: f64 = result.iter().map(|r| approx_area(r)).sum();
595 assert!(total > original_area, "{total} <= {original_area}");
596 }
597
598 #[test]
599 fn test_l_shape_offset_inward() {
600 let l_shape = [
601 (0.0, 0.0),
602 (20.0, 0.0),
603 (20.0, 10.0),
604 (10.0, 10.0),
605 (10.0, 20.0),
606 (0.0, 20.0),
607 ];
608 let result = offset_polygon(&l_shape, -1.0);
609 assert!(!result.is_empty());
610 let original_area = approx_area(&l_shape);
611 let total: f64 = result.iter().map(|r| approx_area(r)).sum();
612 assert!(total < original_area, "{total} >= {original_area}");
613 }
614
615 #[test]
616 fn test_degenerate_polygon_too_few_vertices() {
617 assert!(offset_polygon(&[(0.0, 0.0), (10.0, 0.0)], 1.0).is_empty());
618 }
619
620 #[test]
621 fn test_empty_polygon() {
622 assert!(offset_polygon(&[], 1.0).is_empty());
623 }
624
625 #[test]
626 fn test_cw_polygon_normalized_to_ccw() {
627 let cw = [(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
628 let result = offset_polygon(&cw, 1.0);
629 assert!(!result.is_empty());
630 let a = approx_area(&result[0]);
631 assert!((a - 144.0).abs() < 1.0, "got {a}");
632 }
633
634 #[test]
635 fn test_small_polygon_small_offset() {
636 let tiny = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
637 let result = offset_polygon(&tiny, 0.1);
638 assert!(!result.is_empty());
639 let a = approx_area(&result[0]);
640 assert!((a - 1.44).abs() < 0.1, "got {a}");
641 }
642
643 #[test]
644 fn test_miter_limit_with_acute_triangle() {
645 let acute = [(0.0, 0.0), (100.0, 0.0), (50.0, 1.0)];
646 let result = offset_polygon_miter(´, 2.0, 1.5);
647 for ring in &result {
648 for &(x, y) in ring {
649 assert!(x.is_finite());
650 assert!(y.is_finite());
651 }
652 }
653 }
654
655 #[test]
656 fn test_regular_pentagon_offset() {
657 let pentagon: Vec<(f64, f64)> = (0..5)
658 .map(|i| {
659 let angle = std::f64::consts::TAU / 5.0 * i as f64 - std::f64::consts::FRAC_PI_2;
660 (50.0 + 30.0 * angle.cos(), 50.0 + 30.0 * angle.sin())
661 })
662 .collect();
663 let result = offset_polygon(&pentagon, 3.0);
664 assert!(!result.is_empty());
665 assert!(approx_area(&result[0]) > approx_area(&pentagon));
666 }
667
668 #[cfg(test)]
669 mod proptest_tests {
670 use super::*;
671 use proptest::prelude::*;
672
673 fn convex_polygon_strategy() -> impl Strategy<Value = Vec<(f64, f64)>> {
674 (5..=12u32, 10.0..100.0f64).prop_map(|(n, radius)| {
675 (0..n)
676 .map(|i| {
677 let angle = std::f64::consts::TAU / n as f64 * i as f64;
678 (50.0 + radius * angle.cos(), 50.0 + radius * angle.sin())
679 })
680 .collect()
681 })
682 }
683
684 proptest! {
685 #[test]
686 fn prop_outward_offset_increases_area(
687 poly in convex_polygon_strategy(),
688 d in 0.1..5.0f64,
689 ) {
690 let result = offset_polygon(&poly, d);
691 if !result.is_empty() {
692 let original = polygon::area(&poly);
693 let offset_a = polygon::area(&result[0]);
694 prop_assert!(offset_a >= original - 1e-6,
695 "original={}, offset={}", original, offset_a);
696 }
697 }
698
699 #[test]
700 fn prop_inward_offset_decreases_area(
701 poly in convex_polygon_strategy(),
702 d in 0.1..3.0f64,
703 ) {
704 let result = offset_polygon(&poly, -d);
705 if !result.is_empty() {
706 let original = polygon::area(&poly);
707 let offset_a = polygon::area(&result[0]);
708 prop_assert!(offset_a <= original + 1e-6,
709 "original={}, offset={}", original, offset_a);
710 }
711 }
712
713 #[test]
714 fn prop_offset_vertices_are_finite(
715 poly in convex_polygon_strategy(),
716 d in -10.0..10.0f64,
717 ) {
718 let result = offset_polygon(&poly, d);
719 for ring in &result {
720 for &(x, y) in ring {
721 prop_assert!(x.is_finite());
722 prop_assert!(y.is_finite());
723 }
724 }
725 }
726 }
727 }
728}