use crate::core::{
area, constants, cross_product_two_vectors, dot_product_two_vectors, ellipse_point64,
get_segment_intersect_pt_d, reflect_point, strip_duplicates_path, translate_point, Path64,
PathD, Paths64, Point64, PointD, Rect64,
};
use crate::engine::ClipType;
use crate::engine_public::{Clipper64, PolyTree64};
use crate::FillRule;
const FLOATING_POINT_TOLERANCE: f64 = 1e-12;
const ARC_CONST: f64 = 0.002;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JoinType {
Square,
Bevel,
Round,
Miter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EndType {
Polygon,
Joined,
Butt,
Square,
Round,
}
pub type DeltaCallback64 = Box<dyn Fn(&Path64, &PathD, usize, usize) -> f64>;
fn get_lowest_closed_path_info(paths: &Paths64) -> (Option<usize>, bool) {
let mut idx: Option<usize> = None;
let mut bot_pt = Point64::new(i64::MAX, i64::MIN);
let mut is_neg_area = false;
for (i, path_i) in paths.iter().enumerate() {
let mut a: f64 = f64::MAX;
for pt in path_i {
if (pt.y < bot_pt.y) || (pt.y == bot_pt.y && pt.x >= bot_pt.x) {
continue;
}
if a == f64::MAX {
a = area(path_i);
if a == 0.0 {
break; }
is_neg_area = a < 0.0;
}
idx = Some(i);
bot_pt.x = pt.x;
bot_pt.y = pt.y;
}
}
(idx, is_neg_area)
}
#[inline]
fn hypot_xy(x: f64, y: f64) -> f64 {
(x * x + y * y).sqrt()
}
#[inline]
fn get_unit_normal(pt1: &Point64, pt2: &Point64) -> PointD {
if pt1 == pt2 {
return PointD::new(0.0, 0.0);
}
let dx = (pt2.x - pt1.x) as f64;
let dy = (pt2.y - pt1.y) as f64;
let inverse_hypot = 1.0 / hypot_xy(dx, dy);
PointD::new(dy * inverse_hypot, -dx * inverse_hypot)
}
#[inline]
fn almost_zero(value: f64, epsilon: f64) -> bool {
value.abs() < epsilon
}
#[inline]
fn normalize_vector(vec: &PointD) -> PointD {
let h = hypot_xy(vec.x, vec.y);
if almost_zero(h, 0.001) {
return PointD::new(0.0, 0.0);
}
let inverse_hypot = 1.0 / h;
PointD::new(vec.x * inverse_hypot, vec.y * inverse_hypot)
}
#[inline]
fn get_avg_unit_vector(vec1: &PointD, vec2: &PointD) -> PointD {
normalize_vector(&PointD::new(vec1.x + vec2.x, vec1.y + vec2.y))
}
#[inline]
#[allow(dead_code)]
fn is_closed_path(et: EndType) -> bool {
et == EndType::Polygon || et == EndType::Joined
}
#[inline]
fn get_perpendic(pt: &Point64, norm: &PointD, delta: f64) -> Point64 {
Point64::new(
(pt.x as f64 + norm.x * delta).round() as i64,
(pt.y as f64 + norm.y * delta).round() as i64,
)
}
#[inline]
fn get_perpendic_d(pt: &Point64, norm: &PointD, delta: f64) -> PointD {
PointD::new(pt.x as f64 + norm.x * delta, pt.y as f64 + norm.y * delta)
}
#[inline]
fn negate_path(path: &mut PathD) {
for pt in path.iter_mut() {
pt.x = -pt.x;
pt.y = -pt.y;
}
}
#[inline]
fn point64_from_f(x: f64, y: f64) -> Point64 {
Point64::new(x.round() as i64, y.round() as i64)
}
struct Group {
paths_in: Paths64,
lowest_path_idx: Option<usize>,
is_reversed: bool,
join_type: JoinType,
end_type: EndType,
}
impl Group {
fn new(paths: &Paths64, join_type: JoinType, end_type: EndType) -> Self {
let mut paths_in = paths.clone();
let is_joined = end_type == EndType::Polygon || end_type == EndType::Joined;
for p in paths_in.iter_mut() {
strip_duplicates_path(p, is_joined);
}
let (lowest_path_idx, is_reversed) = if end_type == EndType::Polygon {
let (idx, is_neg_area) = get_lowest_closed_path_info(&paths_in);
let is_reversed = idx.is_some() && is_neg_area;
(idx, is_reversed)
} else {
(None, false)
};
Group {
paths_in,
lowest_path_idx,
is_reversed,
join_type,
end_type,
}
}
}
pub struct ClipperOffset {
error_code: i32,
delta: f64,
group_delta: f64,
temp_lim: f64,
steps_per_rad: f64,
step_sin: f64,
step_cos: f64,
norms: PathD,
path_out: Path64,
solution: Paths64,
groups: Vec<Group>,
join_type: JoinType,
end_type: EndType,
miter_limit: f64,
arc_tolerance: f64,
preserve_collinear: bool,
reverse_solution: bool,
delta_callback: Option<DeltaCallback64>,
}
impl ClipperOffset {
pub fn new(
miter_limit: f64,
arc_tolerance: f64,
preserve_collinear: bool,
reverse_solution: bool,
) -> Self {
ClipperOffset {
error_code: 0,
delta: 0.0,
group_delta: 0.0,
temp_lim: 0.0,
steps_per_rad: 0.0,
step_sin: 0.0,
step_cos: 0.0,
norms: PathD::new(),
path_out: Path64::new(),
solution: Paths64::new(),
groups: Vec::new(),
join_type: JoinType::Bevel,
end_type: EndType::Polygon,
miter_limit,
arc_tolerance,
preserve_collinear,
reverse_solution,
delta_callback: None,
}
}
pub fn new_default() -> Self {
Self::new(2.0, 0.0, false, false)
}
pub fn error_code(&self) -> i32 {
self.error_code
}
pub fn miter_limit(&self) -> f64 {
self.miter_limit
}
pub fn set_miter_limit(&mut self, miter_limit: f64) {
self.miter_limit = miter_limit;
}
pub fn arc_tolerance(&self) -> f64 {
self.arc_tolerance
}
pub fn set_arc_tolerance(&mut self, arc_tolerance: f64) {
self.arc_tolerance = arc_tolerance;
}
pub fn preserve_collinear(&self) -> bool {
self.preserve_collinear
}
pub fn set_preserve_collinear(&mut self, preserve_collinear: bool) {
self.preserve_collinear = preserve_collinear;
}
pub fn reverse_solution(&self) -> bool {
self.reverse_solution
}
pub fn set_reverse_solution(&mut self, reverse_solution: bool) {
self.reverse_solution = reverse_solution;
}
pub fn set_delta_callback(&mut self, cb: Option<DeltaCallback64>) {
self.delta_callback = cb;
}
pub fn add_path(&mut self, path: &Path64, jt: JoinType, et: EndType) {
self.groups.push(Group::new(&vec![path.clone()], jt, et));
}
pub fn add_paths(&mut self, paths: &Paths64, jt: JoinType, et: EndType) {
if paths.is_empty() {
return;
}
self.groups.push(Group::new(paths, jt, et));
}
pub fn clear(&mut self) {
self.groups.clear();
self.norms.clear();
}
pub fn execute(&mut self, delta: f64, paths: &mut Paths64) {
paths.clear();
self.solution.clear();
self.execute_internal(delta, None);
std::mem::swap(&mut self.solution, paths);
}
pub fn execute_tree(&mut self, delta: f64, polytree: &mut PolyTree64) {
polytree.clear();
self.solution.clear();
self.execute_internal(delta, Some(polytree));
self.solution.clear();
}
pub fn execute_with_callback(&mut self, delta_cb: DeltaCallback64, paths: &mut Paths64) {
self.delta_callback = Some(delta_cb);
self.execute(1.0, paths);
}
fn calc_solution_capacity(&self) -> usize {
let mut result = 0;
for g in &self.groups {
result += if g.end_type == EndType::Joined {
g.paths_in.len() * 2
} else {
g.paths_in.len()
};
}
result
}
fn check_reverse_orientation(&self) -> bool {
for g in &self.groups {
if g.end_type == EndType::Polygon {
return g.is_reversed;
}
}
false
}
fn execute_internal(&mut self, delta: f64, polytree: Option<&mut PolyTree64>) {
self.error_code = 0;
if self.groups.is_empty() {
return;
}
self.solution.reserve(self.calc_solution_capacity());
if delta.abs() < 0.5 {
let mut sol_size = 0;
for group in &self.groups {
sol_size += group.paths_in.len();
}
self.solution.reserve(sol_size);
for group in &self.groups {
self.solution.extend(group.paths_in.iter().cloned());
}
} else {
self.temp_lim = if self.miter_limit <= 1.0 {
2.0
} else {
2.0 / (self.miter_limit * self.miter_limit)
};
self.delta = delta;
for i in 0..self.groups.len() {
self.do_group_offset(i);
if self.error_code != 0 {
self.solution.clear();
}
}
}
if self.solution.is_empty() {
return;
}
let paths_reversed = self.check_reverse_orientation();
let mut c = Clipper64::new();
c.set_preserve_collinear(self.preserve_collinear);
c.set_reverse_solution(self.reverse_solution != paths_reversed);
c.add_subject(&self.solution);
let fill_rule = if paths_reversed {
FillRule::Negative
} else {
FillRule::Positive
};
if let Some(tree) = polytree {
let mut open_paths = Paths64::new();
c.execute_tree(ClipType::Union, fill_rule, tree, &mut open_paths);
} else {
c.execute(ClipType::Union, fill_rule, &mut self.solution, None);
}
}
fn build_normals(&mut self, path: &Path64) {
self.norms.clear();
if path.is_empty() {
return;
}
self.norms.reserve(path.len());
for i in 0..path.len() - 1 {
self.norms.push(get_unit_normal(&path[i], &path[i + 1]));
}
self.norms
.push(get_unit_normal(path.last().unwrap(), &path[0]));
}
fn do_bevel(&mut self, path: &Path64, j: usize, k: usize) {
let pt1: PointD;
let pt2: PointD;
if j == k {
let abs_delta = self.group_delta.abs();
pt1 = PointD::new(
path[j].x as f64 - abs_delta * self.norms[j].x,
path[j].y as f64 - abs_delta * self.norms[j].y,
);
pt2 = PointD::new(
path[j].x as f64 + abs_delta * self.norms[j].x,
path[j].y as f64 + abs_delta * self.norms[j].y,
);
} else {
pt1 = PointD::new(
path[j].x as f64 + self.group_delta * self.norms[k].x,
path[j].y as f64 + self.group_delta * self.norms[k].y,
);
pt2 = PointD::new(
path[j].x as f64 + self.group_delta * self.norms[j].x,
path[j].y as f64 + self.group_delta * self.norms[j].y,
);
}
self.path_out.push(point64_from_f(pt1.x, pt1.y));
self.path_out.push(point64_from_f(pt2.x, pt2.y));
}
fn do_square(&mut self, path: &Path64, j: usize, k: usize) {
let vec: PointD = if j == k {
PointD::new(self.norms[j].y, -self.norms[j].x)
} else {
get_avg_unit_vector(
&PointD::new(-self.norms[k].y, self.norms[k].x),
&PointD::new(self.norms[j].y, -self.norms[j].x),
)
};
let abs_delta = self.group_delta.abs();
let pt_q = PointD::new(path[j].x as f64, path[j].y as f64);
let pt_q = translate_point(&pt_q, abs_delta * vec.x, abs_delta * vec.y);
let pt1 = translate_point(&pt_q, self.group_delta * vec.y, self.group_delta * -vec.x);
let pt2 = translate_point(&pt_q, self.group_delta * -vec.y, self.group_delta * vec.x);
let pt3 = get_perpendic_d(&path[k], &self.norms[k], self.group_delta);
if j == k {
let pt4 = PointD::new(
pt3.x + vec.x * self.group_delta,
pt3.y + vec.y * self.group_delta,
);
let mut pt = pt_q;
get_segment_intersect_pt_d(pt1, pt2, pt3, pt4, &mut pt);
let reflected = reflect_point(&pt, &pt_q);
self.path_out.push(point64_from_f(reflected.x, reflected.y));
self.path_out.push(point64_from_f(pt.x, pt.y));
} else {
let pt4 = get_perpendic_d(&path[j], &self.norms[k], self.group_delta);
let mut pt = pt_q;
get_segment_intersect_pt_d(pt1, pt2, pt3, pt4, &mut pt);
self.path_out.push(point64_from_f(pt.x, pt.y));
let reflected = reflect_point(&pt, &pt_q);
self.path_out.push(point64_from_f(reflected.x, reflected.y));
}
}
fn do_miter(&mut self, path: &Path64, j: usize, k: usize, cos_a: f64) {
let q = self.group_delta / (cos_a + 1.0);
self.path_out.push(point64_from_f(
path[j].x as f64 + (self.norms[k].x + self.norms[j].x) * q,
path[j].y as f64 + (self.norms[k].y + self.norms[j].y) * q,
));
}
fn do_round(&mut self, path: &Path64, j: usize, k: usize, angle: f64) {
if self.delta_callback.is_some() {
let abs_delta = self.group_delta.abs();
let arc_tol = if self.arc_tolerance > FLOATING_POINT_TOLERANCE {
abs_delta.min(self.arc_tolerance)
} else {
abs_delta * ARC_CONST
};
let steps_per_360 =
(constants::PI / (1.0 - arc_tol / abs_delta).acos()).min(abs_delta * constants::PI);
self.step_sin = (2.0 * constants::PI / steps_per_360).sin();
self.step_cos = (2.0 * constants::PI / steps_per_360).cos();
if self.group_delta < 0.0 {
self.step_sin = -self.step_sin;
}
self.steps_per_rad = steps_per_360 / (2.0 * constants::PI);
}
let pt = path[j];
let mut offset_vec = PointD::new(
self.norms[k].x * self.group_delta,
self.norms[k].y * self.group_delta,
);
if j == k {
offset_vec = offset_vec.negate();
}
self.path_out.push(point64_from_f(
pt.x as f64 + offset_vec.x,
pt.y as f64 + offset_vec.y,
));
let steps = (self.steps_per_rad * angle.abs()).ceil() as i32; for _ in 1..steps {
offset_vec = PointD::new(
offset_vec.x * self.step_cos - self.step_sin * offset_vec.y,
offset_vec.x * self.step_sin + offset_vec.y * self.step_cos,
);
self.path_out.push(point64_from_f(
pt.x as f64 + offset_vec.x,
pt.y as f64 + offset_vec.y,
));
}
self.path_out
.push(get_perpendic(&path[j], &self.norms[j], self.group_delta));
}
fn offset_point(&mut self, group_idx: usize, path: &Path64, j: usize, k: usize) {
if path[j] == path[k] {
return;
}
let sin_a = cross_product_two_vectors(self.norms[j], self.norms[k]);
let cos_a = dot_product_two_vectors(self.norms[j], self.norms[k]);
let sin_a = sin_a.clamp(-1.0, 1.0);
if let Some(ref cb) = self.delta_callback {
self.group_delta = cb(path, &self.norms, j, k);
if self.groups[group_idx].is_reversed {
self.group_delta = -self.group_delta;
}
}
if self.group_delta.abs() <= FLOATING_POINT_TOLERANCE {
self.path_out.push(path[j]);
return;
}
if cos_a > -0.999 && (sin_a * self.group_delta < 0.0) {
self.path_out
.push(get_perpendic(&path[j], &self.norms[k], self.group_delta));
self.path_out.push(path[j]); self.path_out
.push(get_perpendic(&path[j], &self.norms[j], self.group_delta));
} else if cos_a > 0.999 && self.join_type != JoinType::Round {
self.do_miter(path, j, k, cos_a);
} else if self.join_type == JoinType::Miter {
if cos_a > self.temp_lim - 1.0 {
self.do_miter(path, j, k, cos_a);
} else {
self.do_square(path, j, k);
}
} else if self.join_type == JoinType::Round {
self.do_round(path, j, k, sin_a.atan2(cos_a));
} else if self.join_type == JoinType::Bevel {
self.do_bevel(path, j, k);
} else {
self.do_square(path, j, k);
}
}
fn offset_polygon(&mut self, group_idx: usize, path: &Path64) {
self.path_out.clear();
let len = path.len();
if len == 0 {
return;
}
let mut k = len - 1;
for j in 0..len {
self.offset_point(group_idx, path, j, k);
k = j;
}
let path_out = std::mem::take(&mut self.path_out);
self.solution.push(path_out);
}
fn offset_open_joined(&mut self, group_idx: usize, path: &Path64) {
self.offset_polygon(group_idx, path);
let mut reverse_path = path.clone();
reverse_path.reverse();
self.norms.reverse();
self.norms.push(self.norms[0]);
self.norms.remove(0);
negate_path(&mut self.norms);
self.offset_polygon(group_idx, &reverse_path);
}
fn offset_open_path(&mut self, group_idx: usize, path: &Path64) {
self.path_out.clear();
if let Some(ref cb) = self.delta_callback {
self.group_delta = cb(path, &self.norms, 0, 0);
}
if self.group_delta.abs() <= FLOATING_POINT_TOLERANCE {
self.path_out.push(path[0]);
} else {
match self.end_type {
EndType::Butt => self.do_bevel(path, 0, 0),
EndType::Round => self.do_round(path, 0, 0, constants::PI),
_ => self.do_square(path, 0, 0),
}
}
let high_i = path.len() - 1;
let mut k = 0;
for j in 1..high_i {
self.offset_point(group_idx, path, j, k);
k = j;
}
for i in (1..=high_i).rev() {
self.norms[i] = PointD::new(-self.norms[i - 1].x, -self.norms[i - 1].y);
}
self.norms[0] = self.norms[high_i];
if let Some(ref cb) = self.delta_callback {
self.group_delta = cb(path, &self.norms, high_i, high_i);
}
if self.group_delta.abs() <= FLOATING_POINT_TOLERANCE {
self.path_out.push(path[high_i]);
} else {
match self.end_type {
EndType::Butt => self.do_bevel(path, high_i, high_i),
EndType::Round => self.do_round(path, high_i, high_i, constants::PI),
_ => self.do_square(path, high_i, high_i),
}
}
let mut k = high_i;
for j in (1..high_i).rev() {
self.offset_point(group_idx, path, j, k);
k = j;
}
let path_out = std::mem::take(&mut self.path_out);
self.solution.push(path_out);
}
fn do_group_offset(&mut self, group_idx: usize) {
let group_end_type = self.groups[group_idx].end_type;
let group_join_type = self.groups[group_idx].join_type;
let group_is_reversed = self.groups[group_idx].is_reversed;
let group_lowest = self.groups[group_idx].lowest_path_idx;
if group_end_type == EndType::Polygon {
if group_lowest.is_none() {
self.delta = self.delta.abs();
}
self.group_delta = if group_is_reversed {
-self.delta
} else {
self.delta
};
} else {
self.group_delta = self.delta.abs();
}
let abs_delta = self.group_delta.abs();
self.join_type = group_join_type;
self.end_type = group_end_type;
if group_join_type == JoinType::Round || group_end_type == EndType::Round {
let arc_tol = if self.arc_tolerance > FLOATING_POINT_TOLERANCE {
abs_delta.min(self.arc_tolerance)
} else {
abs_delta * ARC_CONST
};
let steps_per_360 =
(constants::PI / (1.0 - arc_tol / abs_delta).acos()).min(abs_delta * constants::PI);
self.step_sin = (2.0 * constants::PI / steps_per_360).sin();
self.step_cos = (2.0 * constants::PI / steps_per_360).cos();
if self.group_delta < 0.0 {
self.step_sin = -self.step_sin;
}
self.steps_per_rad = steps_per_360 / (2.0 * constants::PI);
}
let paths_count = self.groups[group_idx].paths_in.len();
for path_idx in 0..paths_count {
let path = self.groups[group_idx].paths_in[path_idx].clone();
let path_len = path.len();
self.path_out.clear();
if path_len == 1 {
if self.delta_callback.is_some() {
let cb_result = if let Some(ref cb) = self.delta_callback {
cb(&path, &self.norms, 0, 0)
} else {
0.0
};
self.group_delta = cb_result;
if group_is_reversed {
self.group_delta = -self.group_delta;
}
}
if self.group_delta < 1.0 {
continue;
}
let pt = path[0];
let abs_delta_local = self.group_delta.abs();
if group_join_type == JoinType::Round {
let radius = abs_delta_local;
let steps = if self.steps_per_rad > 0.0 {
(self.steps_per_rad * 2.0 * constants::PI).ceil() as usize
} else {
0
};
self.path_out = ellipse_point64(pt, radius, radius, steps);
} else {
let d = abs_delta_local.ceil() as i64;
let r = Rect64::new(pt.x - d, pt.y - d, pt.x + d, pt.y + d);
self.path_out = r.as_path();
}
let path_out = std::mem::take(&mut self.path_out);
self.solution.push(path_out);
continue;
}
if path_len == 2 && group_end_type == EndType::Joined {
self.end_type = if group_join_type == JoinType::Round {
EndType::Round
} else {
EndType::Square
};
}
self.build_normals(&path);
if self.end_type == EndType::Polygon {
self.offset_polygon(group_idx, &path);
} else if self.end_type == EndType::Joined {
self.offset_open_joined(group_idx, &path);
} else {
self.offset_open_path(group_idx, &path);
}
}
}
}
#[cfg(test)]
#[path = "offset_tests.rs"]
mod tests;