use super::clipper::{from_paths_d, to_path_d, PRECISION};
use crate::geometry::Point;
use clipper2_rust::{inflate_paths_d, EndType, JoinType, PathsD};
pub fn offset_polygon(rings: &[&[Point]], offset: f64, miter_limit: f64) -> Vec<Vec<Point>> {
if rings.is_empty() || rings[0].len() < 3 {
return Vec::new();
}
let outer_area = signed_area(rings[0]);
let outer_pos = outer_area >= 0.0;
let mut paths: PathsD = PathsD::new();
paths.push(to_path_d(rings[0], !outer_pos));
for hole in &rings[1..] {
if hole.len() < 3 {
continue;
}
let hole_pos = signed_area(hole) >= 0.0;
paths.push(to_path_d(hole, hole_pos));
}
let inflated = inflate_paths_d(
&paths,
offset,
JoinType::Miter,
EndType::Polygon,
miter_limit,
PRECISION,
0.0,
);
let mut out_rings = from_paths_d(&inflated);
out_rings.retain(|ring| ring.len() >= 3);
out_rings
}
fn signed_area(ring: &[Point]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut sum = 0.0;
for i in 0..n {
let j = (i + 1) % n;
sum += ring[i].x * ring[j].y - ring[j].x * ring[i].y;
}
sum * 0.5
}
#[cfg(test)]
mod tests {
use super::*;
fn pt(x: f64, y: f64) -> Point {
Point::new(x, y)
}
fn bbox(ring: &[Point]) -> (f64, f64, f64, f64) {
let mut x0 = f64::INFINITY;
let mut x1 = f64::NEG_INFINITY;
let mut y0 = f64::INFINITY;
let mut y1 = f64::NEG_INFINITY;
for p in ring {
x0 = x0.min(p.x);
x1 = x1.max(p.x);
y0 = y0.min(p.y);
y1 = y1.max(p.y);
}
(x0, y0, x1, y1)
}
#[test]
fn square_outward_offset() {
let sq = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0), pt(0.0, 1.0)];
let out = offset_polygon(&[&sq], 0.5, 4.0);
assert_eq!(out.len(), 1);
let (x0, y0, x1, y1) = bbox(&out[0]);
assert!((x0 - (-0.5)).abs() < 0.01);
assert!((y0 - (-0.5)).abs() < 0.01);
assert!((x1 - 1.5).abs() < 0.01);
assert!((y1 - 1.5).abs() < 0.01);
}
#[test]
fn square_inward_offset() {
let sq = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0), pt(0.0, 1.0)];
let out = offset_polygon(&[&sq], -0.25, 4.0);
assert_eq!(out.len(), 1);
let (x0, y0, x1, y1) = bbox(&out[0]);
assert!((x0 - 0.25).abs() < 0.01);
assert!((y0 - 0.25).abs() < 0.01);
assert!((x1 - 0.75).abs() < 0.01);
assert!((y1 - 0.75).abs() < 0.01);
}
#[test]
fn winding_normalised() {
let ccw = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0), pt(0.0, 1.0)];
let mut cw = ccw;
cw.reverse();
let from_ccw = offset_polygon(&[&ccw], 0.5, 4.0);
let from_cw = offset_polygon(&[&cw], 0.5, 4.0);
assert_eq!(from_ccw.len(), 1);
assert_eq!(from_cw.len(), 1);
let a = bbox(&from_ccw[0]);
let b = bbox(&from_cw[0]);
assert!((a.0 - b.0).abs() < 0.01);
assert!((a.1 - b.1).abs() < 0.01);
assert!((a.2 - b.2).abs() < 0.01);
assert!((a.3 - b.3).abs() < 0.01);
}
#[test]
fn square_with_hole_outward_offset_grows_outer_shrinks_hole() {
let outer = [pt(0.0, 0.0), pt(10.0, 0.0), pt(10.0, 10.0), pt(0.0, 10.0)];
let hole = [pt(3.0, 3.0), pt(7.0, 3.0), pt(7.0, 7.0), pt(3.0, 7.0)];
let rings: [&[Point]; 2] = [&outer, &hole];
let out = offset_polygon(&rings, 1.0, 4.0);
assert_eq!(out.len(), 2, "outer + hole");
let (outer_out, hole_out) = if area_bbox(&out[0]) > area_bbox(&out[1]) {
(&out[0], &out[1])
} else {
(&out[1], &out[0])
};
let (ox0, oy0, ox1, oy1) = bbox(outer_out);
assert!((ox0 - (-1.0)).abs() < 0.01);
assert!((oy0 - (-1.0)).abs() < 0.01);
assert!((ox1 - 11.0).abs() < 0.01);
assert!((oy1 - 11.0).abs() < 0.01);
let (hx0, hy0, hx1, hy1) = bbox(hole_out);
assert!((hx0 - 4.0).abs() < 0.01);
assert!((hy0 - 4.0).abs() < 0.01);
assert!((hx1 - 6.0).abs() < 0.01);
assert!((hy1 - 6.0).abs() < 0.01);
}
#[test]
fn inset_makes_outer_shrink_and_hole_grow_until_polygon_vanishes() {
let outer = [pt(0.0, 0.0), pt(10.0, 0.0), pt(10.0, 10.0), pt(0.0, 10.0)];
let hole = [pt(4.0, 4.0), pt(6.0, 4.0), pt(6.0, 6.0), pt(4.0, 6.0)];
let rings: [&[Point]; 2] = [&outer, &hole];
let out = offset_polygon(&rings, -2.0, 4.0);
assert!(out.is_empty(), "polygon collapses to zero area");
}
#[test]
fn inset_with_hole_still_inside_keeps_both_rings() {
let outer = [pt(0.0, 0.0), pt(10.0, 0.0), pt(10.0, 10.0), pt(0.0, 10.0)];
let hole = [pt(4.0, 4.0), pt(6.0, 4.0), pt(6.0, 6.0), pt(4.0, 6.0)];
let rings: [&[Point]; 2] = [&outer, &hole];
let out = offset_polygon(&rings, -1.0, 4.0);
assert_eq!(out.len(), 2);
}
fn area_bbox(ring: &[Point]) -> f64 {
let (x0, y0, x1, y1) = bbox(ring);
(x1 - x0) * (y1 - y0)
}
}