use axiolid_core::{Point2, Tolerance};
use axiolid_overlay::{
offset_polygons, polygon_area, stroke_polyline, total_area, CapStyle, JoinStyle, OverlayError,
Polygon, Ring,
};
fn tolerance() -> Tolerance {
Tolerance::new(1e-9, 1e-9).expect("valid tolerance")
}
fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Polygon {
Polygon {
outer: Ring {
points: vec![
Point2::new(x0, y0),
Point2::new(x1, y0),
Point2::new(x1, y1),
Point2::new(x0, y1),
],
},
holes: Vec::new(),
}
}
fn square_with_hole() -> Polygon {
let mut polygon = rect(0.0, 0.0, 10.0, 10.0);
polygon.holes.push(Ring {
points: vec![
Point2::new(4.0, 4.0),
Point2::new(4.0, 6.0),
Point2::new(6.0, 6.0),
Point2::new(6.0, 4.0),
],
});
polygon
}
fn bevel() -> JoinStyle {
JoinStyle::Bevel
}
#[test]
fn zero_distance_is_the_identity() {
let input = vec![rect(0.0, 0.0, 4.0, 3.0)];
let result = offset_polygons(&input, 0.0, bevel(), tolerance()).expect("valid offset");
assert_eq!(result.polygons.len(), 1);
assert!((total_area(&result.polygons) - 12.0).abs() < 1e-9);
assert!(!result.evidence.collapsed);
}
#[test]
fn outset_grows_a_square_by_the_expected_area() {
let input = vec![rect(0.0, 0.0, 10.0, 10.0)];
let result = offset_polygons(&input, 1.0, bevel(), tolerance()).expect("valid offset");
assert_eq!(result.polygons.len(), 1);
let area = total_area(&result.polygons);
assert!(
(area - 142.0).abs() < 1e-6,
"expected 142 from the bevelled outset, got {area}"
);
}
#[test]
fn inset_shrinks_a_square_by_the_expected_area() {
let input = vec![rect(0.0, 0.0, 10.0, 10.0)];
let result = offset_polygons(&input, -1.0, bevel(), tolerance()).expect("valid offset");
assert_eq!(result.polygons.len(), 1);
let area = total_area(&result.polygons);
assert!(
(area - 64.0).abs() < 1e-6,
"expected 64 from the inset, got {area}"
);
}
#[test]
fn area_is_monotone_in_the_offset_distance() {
let input = vec![rect(0.0, 0.0, 10.0, 10.0)];
let distances = [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0];
let mut previous = f64::NEG_INFINITY;
for distance in distances {
let result = offset_polygons(&input, distance, bevel(), tolerance()).expect("valid offset");
let area = total_area(&result.polygons);
assert!(
area > previous,
"area must strictly increase with distance: {area} at {distance} did not exceed \
{previous}"
);
previous = area;
}
}
#[test]
fn an_inset_that_collapses_the_region_returns_empty_not_a_degenerate_ring() {
let input = vec![rect(0.0, 0.0, 10.0, 10.0)];
let result = offset_polygons(&input, -6.0, bevel(), tolerance()).expect("valid offset");
assert!(
result.polygons.is_empty(),
"an over-inset region must vanish, got {} polygons",
result.polygons.len()
);
assert!(
result.evidence.collapsed,
"collapse must be reported, otherwise the caller cannot distinguish it \
from an empty input"
);
}
#[test]
fn collapse_is_distinguishable_from_an_empty_input() {
let empty = offset_polygons(&[], -6.0, bevel(), tolerance()).expect("empty input is valid");
assert!(empty.polygons.is_empty());
assert!(
!empty.evidence.collapsed,
"an empty input did not collapse; nothing was there to collapse"
);
}
#[test]
fn outsetting_a_polygon_with_a_hole_shrinks_the_hole() {
let input = vec![square_with_hole()];
let before = polygon_area(&input[0]);
assert!((before - 96.0).abs() < 1e-9, "fixture area is {before}");
let result = offset_polygons(&input, 0.5, bevel(), tolerance()).expect("valid offset");
assert_eq!(result.polygons.len(), 1);
let after = &result.polygons[0];
assert_eq!(
after.holes.len(),
1,
"the hole must survive an outset of half its inradius"
);
let hole_before = 4.0;
let hole_after = axiolid_overlay::ring_area(&after.holes[0]);
assert!(
hole_after < hole_before,
"outsetting the region must SHRINK the hole: {hole_after} was not below {hole_before}"
);
}
#[test]
fn a_hole_can_be_closed_by_a_large_enough_outset() {
let input = vec![square_with_hole()];
let result = offset_polygons(&input, 1.5, bevel(), tolerance()).expect("valid offset");
assert_eq!(result.polygons.len(), 1);
assert_eq!(
result.polygons[0].holes.len(),
0,
"the hole must be gone, not present as a degenerate ring"
);
assert_eq!(result.evidence.output_holes, 0);
}
#[test]
fn a_non_finite_distance_is_refused() {
let input = vec![rect(0.0, 0.0, 4.0, 3.0)];
for distance in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(
offset_polygons(&input, distance, bevel(), tolerance()),
Err(OverlayError::InvalidOffsetDistance),
"distance {distance} must be refused"
);
}
}
#[test]
fn a_malformed_join_parameter_is_refused_separately_from_the_distance() {
let input = vec![rect(0.0, 0.0, 4.0, 3.0)];
assert_eq!(
offset_polygons(
&input,
1.0,
JoinStyle::Miter {
angle_limit: f64::NAN
},
tolerance()
),
Err(OverlayError::InvalidOffsetStyle)
);
assert_eq!(
offset_polygons(
&input,
1.0,
JoinStyle::Round {
max_segment_ratio: -1.0
},
tolerance()
),
Err(OverlayError::InvalidOffsetStyle)
);
}
#[test]
fn a_self_intersecting_polygon_is_refused() {
let bowtie = Polygon {
outer: Ring {
points: vec![
Point2::new(0.0, 0.0),
Point2::new(4.0, 4.0),
Point2::new(4.0, 0.0),
Point2::new(0.0, 4.0),
],
},
holes: Vec::new(),
};
assert_eq!(
offset_polygons(&[bowtie], 1.0, bevel(), tolerance()),
Err(OverlayError::SelfIntersection)
);
}
#[test]
fn a_stroked_segment_has_the_expected_area() {
let path = [Point2::new(0.0, 0.0), Point2::new(10.0, 0.0)];
let result = stroke_polyline(&path, 2.0, bevel(), CapStyle::Butt, false).expect("valid stroke");
assert_eq!(result.polygons.len(), 1);
let area = total_area(&result.polygons);
assert!(
(area - 20.0).abs() < 1e-6,
"expected 20 for a 10x2 stroke, got {area}"
);
}
#[test]
fn square_caps_extend_the_stroke_by_half_the_width_at_each_end() {
let path = [Point2::new(0.0, 0.0), Point2::new(10.0, 0.0)];
let butt = stroke_polyline(&path, 2.0, bevel(), CapStyle::Butt, false).expect("valid");
let square = stroke_polyline(&path, 2.0, bevel(), CapStyle::Square, false).expect("valid");
let butt_area = total_area(&butt.polygons);
let square_area = total_area(&square.polygons);
assert!(
(square_area - 24.0).abs() < 1e-6,
"expected 24 for square caps, got {square_area}"
);
assert!(
square_area > butt_area,
"square caps must cover more than butt caps"
);
}
#[test]
fn width_is_the_full_stroke_width_not_a_half_width() {
let path = [Point2::new(0.0, 0.0), Point2::new(10.0, 0.0)];
let result = stroke_polyline(&path, 1.0, bevel(), CapStyle::Butt, false).expect("valid");
let area = total_area(&result.polygons);
assert!(
(area - 10.0).abs() < 1e-6,
"width 1 over length 10 must sweep area 10, got {area}"
);
}
#[test]
fn a_self_intersecting_polyline_is_stroked_not_refused() {
let path = [
Point2::new(0.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(10.0, 0.0),
Point2::new(0.0, 10.0),
];
let result = stroke_polyline(&path, 1.0, bevel(), CapStyle::Butt, false)
.expect("a crossing path is strokeable");
assert!(
!result.polygons.is_empty(),
"the stroke must produce a region"
);
let area = total_area(&result.polygons);
let upper = 10.0 * 2f64.sqrt() + 10.0 + 10.0 * 2f64.sqrt();
assert!(
area < upper,
"overlap at the crossing must be unioned, not double-counted: {area} >= {upper}"
);
assert!(area > 0.0);
}
#[test]
fn a_closed_stroke_encloses_a_hole() {
let path = [
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(0.0, 10.0),
];
let result = stroke_polyline(&path, 1.0, bevel(), CapStyle::Butt, true).expect("valid");
assert_eq!(result.polygons.len(), 1);
assert_eq!(
result.polygons[0].holes.len(),
1,
"a closed stroke must leave the interior as a hole"
);
}
#[test]
fn a_degenerate_or_malformed_stroke_is_refused() {
let single = [Point2::new(0.0, 0.0)];
assert_eq!(
stroke_polyline(&single, 1.0, bevel(), CapStyle::Butt, false),
Err(OverlayError::RingTooShort)
);
let path = [Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)];
for width in [0.0, -1.0, f64::NAN] {
assert_eq!(
stroke_polyline(&path, width, bevel(), CapStyle::Butt, false),
Err(OverlayError::InvalidOffsetDistance),
"width {width} must be refused"
);
}
let non_finite = [Point2::new(0.0, 0.0), Point2::new(f64::NAN, 0.0)];
assert_eq!(
stroke_polyline(&non_finite, 1.0, bevel(), CapStyle::Butt, false),
Err(OverlayError::NonFinitePoint)
);
}
#[test]
fn round_trip_outset_then_inset_recovers_the_original_area() {
let input = vec![rect(0.0, 0.0, 20.0, 20.0)];
let original = total_area(&input);
let out = offset_polygons(&input, 2.0, bevel(), tolerance()).expect("outset");
let back = offset_polygons(&out.polygons, -2.0, bevel(), tolerance()).expect("inset");
let recovered = total_area(&back.polygons);
assert!(
(recovered - original).abs() <= 8.0,
"round trip lost more than the corner correction: {recovered} vs {original}"
);
assert!(
recovered <= original + 1e-6,
"a round trip must not grow the region: {recovered} > {original}"
);
}