use crate::core::{area, constants, Path64, Paths64, Point64};
use crate::offset::{ClipperOffset, EndType, JoinType};
fn make_square(size: i64) -> Path64 {
vec![
Point64::new(-size, -size),
Point64::new(size, -size),
Point64::new(size, size),
Point64::new(-size, size),
]
}
fn make_diamond(size: i64) -> Path64 {
vec![
Point64::new(0, -size),
Point64::new(size, 0),
Point64::new(0, size),
Point64::new(-size, 0),
]
}
fn make_triangle(size: i64) -> Path64 {
vec![
Point64::new(0, -size),
Point64::new(size, size),
Point64::new(-size, size),
]
}
fn make_open_line(length: i64) -> Path64 {
vec![Point64::new(0, 0), Point64::new(length, 0)]
}
fn make_l_path(size: i64) -> Path64 {
vec![
Point64::new(0, 0),
Point64::new(size, 0),
Point64::new(size, size),
]
}
#[test]
fn test_join_type_values() {
let types = [
JoinType::Square,
JoinType::Bevel,
JoinType::Round,
JoinType::Miter,
];
for i in 0..types.len() {
for j in (i + 1)..types.len() {
assert_ne!(types[i], types[j]);
}
}
}
#[test]
fn test_end_type_values() {
let types = [
EndType::Polygon,
EndType::Joined,
EndType::Butt,
EndType::Square,
EndType::Round,
];
for i in 0..types.len() {
for j in (i + 1)..types.len() {
assert_ne!(types[i], types[j]);
}
}
}
#[test]
fn test_clipper_offset_default_construction() {
let co = ClipperOffset::new_default();
assert_eq!(co.error_code(), 0);
assert_eq!(co.miter_limit(), 2.0);
assert_eq!(co.arc_tolerance(), 0.0);
assert!(!co.preserve_collinear());
assert!(!co.reverse_solution());
}
#[test]
fn test_clipper_offset_custom_construction() {
let co = ClipperOffset::new(3.0, 0.5, true, true);
assert_eq!(co.miter_limit(), 3.0);
assert_eq!(co.arc_tolerance(), 0.5);
assert!(co.preserve_collinear());
assert!(co.reverse_solution());
}
#[test]
fn test_clipper_offset_setters() {
let mut co = ClipperOffset::new_default();
co.set_miter_limit(5.0);
assert_eq!(co.miter_limit(), 5.0);
co.set_arc_tolerance(1.0);
assert_eq!(co.arc_tolerance(), 1.0);
co.set_preserve_collinear(true);
assert!(co.preserve_collinear());
co.set_reverse_solution(true);
assert!(co.reverse_solution());
}
#[test]
fn test_clipper_offset_clear() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
co.clear();
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(result.is_empty());
}
#[test]
fn test_offset_square_inflate_miter() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty(), "Result should not be empty");
let original_area = area(&square).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(
result_area > original_area,
"Inflated area {} should be > original area {}",
result_area,
original_area
);
}
#[test]
fn test_offset_square_shrink_miter() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(-10.0, &mut result);
assert!(
!result.is_empty(),
"Shrinking a square by 10 should produce a non-empty result"
);
let original_area = area(&square).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(
result_area < original_area,
"Shrunk area {} should be < original area {}",
result_area,
original_area
);
}
#[test]
fn test_offset_square_inflate_round() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Round, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty(), "Result should not be empty");
let original_area = area(&square).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > original_area);
}
#[test]
fn test_offset_square_inflate_bevel() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Bevel, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty(), "Result should not be empty");
let original_area = area(&square).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > original_area);
}
#[test]
fn test_offset_square_inflate_square_join() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Square, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty(), "Result should not be empty");
let original_area = area(&square).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > original_area);
}
#[test]
fn test_offset_triangle_inflate() {
let mut co = ClipperOffset::new_default();
let triangle = make_triangle(100);
co.add_path(&triangle, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
let original_area = area(&triangle).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > original_area);
}
#[test]
fn test_offset_diamond_inflate() {
let mut co = ClipperOffset::new_default();
let diamond = make_diamond(100);
co.add_path(&diamond, JoinType::Round, EndType::Polygon);
let mut result = Paths64::new();
co.execute(20.0, &mut result);
assert!(!result.is_empty());
let original_area = area(&diamond).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > original_area);
}
#[test]
fn test_offset_shrink_to_nothing() {
let mut co = ClipperOffset::new_default();
let square = make_square(10);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(-20.0, &mut result);
assert!(
result.is_empty(),
"Over-shrunk square should produce empty result"
);
}
#[test]
fn test_offset_insignificant_delta() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(0.1, &mut result);
assert_eq!(result.len(), 1, "Should return the original path");
assert_eq!(result[0].len(), square.len());
}
#[test]
fn test_offset_open_path_butt_end() {
let mut co = ClipperOffset::new_default();
let line = make_open_line(200);
co.add_path(&line, JoinType::Miter, EndType::Butt);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty(), "Open path offset should produce result");
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(
result_area > 3000.0,
"Area {} should be approximately 4000",
result_area
);
}
#[test]
fn test_offset_open_path_square_end() {
let mut co = ClipperOffset::new_default();
let line = make_open_line(200);
co.add_path(&line, JoinType::Miter, EndType::Square);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > 4000.0);
}
#[test]
fn test_offset_open_path_round_end() {
let mut co = ClipperOffset::new_default();
let line = make_open_line(200);
co.add_path(&line, JoinType::Round, EndType::Round);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > 3000.0);
}
#[test]
fn test_offset_open_path_joined_end() {
let mut co = ClipperOffset::new_default();
let line = make_open_line(200);
co.add_path(&line, JoinType::Miter, EndType::Joined);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
}
#[test]
fn test_offset_l_shape_miter() {
let mut co = ClipperOffset::new_default();
let l_path = make_l_path(100);
co.add_path(&l_path, JoinType::Miter, EndType::Butt);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
}
#[test]
fn test_offset_l_shape_round() {
let mut co = ClipperOffset::new_default();
let l_path = make_l_path(100);
co.add_path(&l_path, JoinType::Round, EndType::Round);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
}
#[test]
fn test_offset_single_point_round() {
let mut co = ClipperOffset::new_default();
let point = vec![Point64::new(100, 100)];
co.add_path(&point, JoinType::Round, EndType::Polygon);
let mut result = Paths64::new();
co.execute(20.0, &mut result);
assert!(
!result.is_empty(),
"Single point offset should produce a circle"
);
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
let expected_area = constants::PI * 20.0 * 20.0;
assert!(
(result_area - expected_area).abs() < expected_area * 0.15,
"Circle area {} should be approximately {}",
result_area,
expected_area
);
}
#[test]
fn test_offset_single_point_square() {
let mut co = ClipperOffset::new_default();
let point = vec![Point64::new(100, 100)];
co.add_path(&point, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(20.0, &mut result);
assert!(
!result.is_empty(),
"Single point offset should produce a square"
);
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
let expected_area = (2.0 * 20.0) * (2.0 * 20.0);
assert!(
(result_area - expected_area).abs() < expected_area * 0.15,
"Square area {} should be approximately {}",
result_area,
expected_area
);
}
#[test]
fn test_offset_single_point_small_delta() {
let mut co = ClipperOffset::new_default();
let point = vec![Point64::new(100, 100)];
co.add_path(&point, JoinType::Round, EndType::Polygon);
let mut result = Paths64::new();
co.execute(0.5, &mut result);
assert!(result.is_empty());
}
#[test]
fn test_offset_multiple_paths() {
let mut co = ClipperOffset::new_default();
let square1 = vec![
Point64::new(0, 0),
Point64::new(100, 0),
Point64::new(100, 100),
Point64::new(0, 100),
];
let square2 = vec![
Point64::new(200, 0),
Point64::new(300, 0),
Point64::new(300, 100),
Point64::new(200, 100),
];
co.add_paths(
&vec![square1.clone(), square2.clone()],
JoinType::Miter,
EndType::Polygon,
);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(
result.len() >= 2,
"Should produce at least 2 output paths for 2 non-overlapping inputs"
);
}
#[test]
fn test_offset_add_paths_empty() {
let mut co = ClipperOffset::new_default();
let empty: Paths64 = vec![];
co.add_paths(&empty, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(result.is_empty());
}
#[test]
fn test_offset_miter_limit() {
let mut co_low = ClipperOffset::new(1.0, 0.0, false, false);
let mut co_high = ClipperOffset::new(10.0, 0.0, false, false);
let triangle = make_triangle(100);
co_low.add_path(&triangle, JoinType::Miter, EndType::Polygon);
co_high.add_path(&triangle, JoinType::Miter, EndType::Polygon);
let mut result_low = Paths64::new();
let mut result_high = Paths64::new();
co_low.execute(10.0, &mut result_low);
co_high.execute(10.0, &mut result_high);
assert!(!result_low.is_empty());
assert!(!result_high.is_empty());
let area_low: f64 = result_low.iter().map(|p| area(p).abs()).sum();
let area_high: f64 = result_high.iter().map(|p| area(p).abs()).sum();
assert!(
area_high >= area_low,
"Higher miter limit should produce >= area: high={}, low={}",
area_high,
area_low
);
}
#[test]
fn test_offset_orientation_positive() {
let mut co = ClipperOffset::new_default();
let ccw_square = vec![
Point64::new(0, 0),
Point64::new(100, 0),
Point64::new(100, 100),
Point64::new(0, 100),
];
let original_area = area(&ccw_square);
co.add_path(&ccw_square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
let result_area = area(&result[0]);
assert!(
(original_area > 0.0) == (result_area > 0.0),
"Orientation should be preserved: original={}, result={}",
original_area,
result_area
);
}
#[test]
fn test_offset_orientation_negative() {
let mut co = ClipperOffset::new_default();
let cw_square = vec![
Point64::new(0, 100),
Point64::new(100, 100),
Point64::new(100, 0),
Point64::new(0, 0),
];
let original_area = area(&cw_square);
co.add_path(&cw_square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
let result_area = area(&result[0]);
assert!(
(original_area > 0.0) == (result_area > 0.0),
"Orientation should be preserved: original={}, result={}",
original_area,
result_area
);
}
#[test]
fn test_offset_two_point_polygon() {
let mut co = ClipperOffset::new_default();
let line = vec![Point64::new(0, 0), Point64::new(100, 0)];
co.add_path(&line, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
}
#[test]
fn test_offset_two_point_joined() {
let mut co = ClipperOffset::new_default();
let line = vec![Point64::new(0, 0), Point64::new(100, 0)];
co.add_path(&line, JoinType::Miter, EndType::Joined);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
}
#[test]
fn test_offset_arc_tolerance_small() {
let mut co_small = ClipperOffset::new(2.0, 0.5, false, false);
let mut co_large = ClipperOffset::new(2.0, 5.0, false, false);
let square = make_square(100);
co_small.add_path(&square, JoinType::Round, EndType::Polygon);
co_large.add_path(&square, JoinType::Round, EndType::Polygon);
let mut result_small = Paths64::new();
let mut result_large = Paths64::new();
co_small.execute(20.0, &mut result_small);
co_large.execute(20.0, &mut result_large);
assert!(!result_small.is_empty());
assert!(!result_large.is_empty());
let verts_small: usize = result_small.iter().map(|p| p.len()).sum();
let verts_large: usize = result_large.iter().map(|p| p.len()).sum();
assert!(
verts_small >= verts_large,
"Small arc tolerance should produce more vertices: {} vs {}",
verts_small,
verts_large
);
}
#[test]
fn test_offset_reverse_solution() {
let mut co_normal = ClipperOffset::new_default();
let mut co_reversed = ClipperOffset::new(2.0, 0.0, false, true);
let square = make_square(100);
co_normal.add_path(&square, JoinType::Miter, EndType::Polygon);
co_reversed.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result_normal = Paths64::new();
let mut result_reversed = Paths64::new();
co_normal.execute(10.0, &mut result_normal);
co_reversed.execute(10.0, &mut result_reversed);
assert!(!result_normal.is_empty());
assert!(!result_reversed.is_empty());
let area_normal = area(&result_normal[0]);
let area_reversed = area(&result_reversed[0]);
assert!(
area_normal * area_reversed < 0.0,
"Reversed solution should have opposite orientation: normal={}, reversed={}",
area_normal,
area_reversed
);
}
#[test]
fn test_offset_with_delta_callback() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
type DeltaCallback = Box<dyn Fn(&Path64, &crate::core::PathD, usize, usize) -> f64>;
let cb: DeltaCallback = Box::new(|_path, _norms, _j, _k| 10.0);
let mut result = Paths64::new();
co.execute_with_callback(cb, &mut result);
assert!(!result.is_empty());
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
let original_area = area(&square).abs();
assert!(result_area > original_area);
}
#[test]
fn test_offset_empty_path() {
let mut co = ClipperOffset::new_default();
let empty: Path64 = vec![];
co.add_path(&empty, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(result.is_empty());
}
#[test]
fn test_offset_no_groups() {
let mut co = ClipperOffset::new_default();
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(result.is_empty());
}
#[test]
fn test_offset_zero_delta() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(0.0, &mut result);
assert_eq!(result.len(), 1);
}
#[test]
fn test_offset_large_inflate() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(500.0, &mut result);
assert!(!result.is_empty());
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > 1_000_000.0);
}
#[test]
fn test_offset_concave_polygon() {
let mut co = ClipperOffset::new_default();
let l_shape = vec![
Point64::new(0, 0),
Point64::new(200, 0),
Point64::new(200, 100),
Point64::new(100, 100),
Point64::new(100, 200),
Point64::new(0, 200),
];
co.add_path(&l_shape, JoinType::Miter, EndType::Polygon);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(!result.is_empty());
let original_area = area(&l_shape).abs();
let result_area: f64 = result.iter().map(|p| area(p).abs()).sum();
assert!(result_area > original_area);
}
#[test]
fn test_offset_execute_tree() {
use crate::engine_public::PolyTree64;
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let mut tree = PolyTree64::new();
co.execute_tree(10.0, &mut tree);
assert!(
!tree.nodes.is_empty(),
"PolyTree should have at least one node"
);
}
#[test]
fn test_offset_preserve_collinear() {
let mut co_preserve = ClipperOffset::new(2.0, 0.0, true, false);
let mut co_no_preserve = ClipperOffset::new(2.0, 0.0, false, false);
let square_with_collinear = vec![
Point64::new(0, 0),
Point64::new(50, 0), Point64::new(100, 0),
Point64::new(100, 100),
Point64::new(0, 100),
];
co_preserve.add_path(&square_with_collinear, JoinType::Miter, EndType::Polygon);
co_no_preserve.add_path(&square_with_collinear, JoinType::Miter, EndType::Polygon);
let mut result_preserve = Paths64::new();
let mut result_no_preserve = Paths64::new();
co_preserve.execute(10.0, &mut result_preserve);
co_no_preserve.execute(10.0, &mut result_no_preserve);
assert!(!result_preserve.is_empty());
assert!(!result_no_preserve.is_empty());
}
#[test]
fn test_offset_mixed_groups() {
let mut co = ClipperOffset::new_default();
let square = make_square(100);
co.add_path(&square, JoinType::Miter, EndType::Polygon);
let line = make_open_line(200);
co.add_path(&line, JoinType::Round, EndType::Round);
let mut result = Paths64::new();
co.execute(10.0, &mut result);
assert!(
!result.is_empty(),
"Mixed groups should produce output paths"
);
}