/******************************************************************************* * Author : Angus Johnson * * Date : 15 October 2022 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2022 * * Purpose : Path Offset (Inflate/Shrink) * * License : http://www.boost.org/LICENSE_1_0.txt * *******************************************************************************/ #include #include "clipper.h" #include "clipper.offset.h" namespace Clipper2Lib { const double default_arc_tolerance = 0.25; const double floating_point_tolerance = 1e-12; //------------------------------------------------------------------------------ // Miscellaneous methods //------------------------------------------------------------------------------ Paths64::size_type GetLowestPolygonIdx(const Paths64& paths) { Paths64::size_type result = 0; Point64 lp = Point64(static_cast(0), std::numeric_limits::min()); for (Paths64::size_type i = 0 ; i < paths.size(); ++i) for (const Point64& p : paths[i]) { if (p.y < lp.y || (p.y == lp.y && p.x >= lp.x)) continue; result = i; lp = p; } return result; } PointD GetUnitNormal(const Point64& pt1, const Point64& pt2) { double dx, dy, inverse_hypot; if (pt1 == pt2) return PointD(0.0, 0.0); dx = static_cast(pt2.x - pt1.x); dy = static_cast(pt2.y - pt1.y); inverse_hypot = 1.0 / hypot(dx, dy); dx *= inverse_hypot; dy *= inverse_hypot; return PointD(dy, -dx); } inline bool AlmostZero(double value, double epsilon = 0.001) { return std::fabs(value) < epsilon; } inline double Hypot(double x, double y) { //see https://stackoverflow.com/a/32436148/359538 return std::sqrt(x * x + y * y); } inline PointD NormalizeVector(const PointD& vec) { double h = Hypot(vec.x, vec.y); if (AlmostZero(h)) return PointD(0,0); double inverseHypot = 1 / h; return PointD(vec.x * inverseHypot, vec.y * inverseHypot); } inline PointD GetAvgUnitVector(const PointD& vec1, const PointD& vec2) { return NormalizeVector(PointD(vec1.x + vec2.x, vec1.y + vec2.y)); } inline bool IsClosedPath(EndType et) { return et == EndType::Polygon || et == EndType::Joined; } inline Point64 GetPerpendic(const Point64& pt, const PointD& norm, double delta) { return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta); } inline PointD GetPerpendicD(const Point64& pt, const PointD& norm, double delta) { return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta); } //------------------------------------------------------------------------------ // ClipperOffset methods //------------------------------------------------------------------------------ void ClipperOffset::AddPath(const Path64& path, JoinType jt_, EndType et_) { Paths64 paths; paths.push_back(path); AddPaths(paths, jt_, et_); } void ClipperOffset::AddPaths(const Paths64 &paths, JoinType jt_, EndType et_) { if (paths.size() == 0) return; groups_.push_back(Group(paths, jt_, et_)); } void ClipperOffset::AddPath(const Clipper2Lib::PathD& path, JoinType jt_, EndType et_) { PathsD paths; paths.push_back(path); AddPaths(paths, jt_, et_); } void ClipperOffset::AddPaths(const PathsD& paths, JoinType jt_, EndType et_) { if (paths.size() == 0) return; groups_.push_back(Group(PathsDToPaths64(paths), jt_, et_)); } void ClipperOffset::BuildNormals(const Path64& path) { norms.clear(); norms.reserve(path.size()); if (path.size() == 0) return; Path64::const_iterator path_iter, path_last_iter = --path.cend(); for (path_iter = path.cbegin(); path_iter != path_last_iter; ++path_iter) norms.push_back(GetUnitNormal(*path_iter,*(path_iter +1))); norms.push_back(GetUnitNormal(*path_last_iter, *(path.cbegin()))); } inline PointD TranslatePoint(const PointD& pt, double dx, double dy) { return PointD(pt.x + dx, pt.y + dy); } inline PointD ReflectPoint(const PointD& pt, const PointD& pivot) { return PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); } PointD IntersectPoint(const PointD& pt1a, const PointD& pt1b, const PointD& pt2a, const PointD& pt2b) { if (pt1a.x == pt1b.x) //vertical { if (pt2a.x == pt2b.x) return PointD(0, 0); double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); double b2 = pt2a.y - m2 * pt2a.x; return PointD(pt1a.x, m2 * pt1a.x + b2); } else if (pt2a.x == pt2b.x) //vertical { double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); double b1 = pt1a.y - m1 * pt1a.x; return PointD(pt2a.x, m1 * pt2a.x + b1); } else { double m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x); double b1 = pt1a.y - m1 * pt1a.x; double m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x); double b2 = pt2a.y - m2 * pt2a.x; if (m1 == m2) return PointD(0, 0); double x = (b2 - b1) / (m1 - m2); return PointD(x, m1 * x + b1); } } void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t k) { PointD vec; if (j == k) vec = PointD(norms[0].y, -norms[0].x); else vec = GetAvgUnitVector( PointD(-norms[k].y, norms[k].x), PointD(norms[j].y, -norms[j].x)); // now offset the original vertex delta units along unit vector PointD ptQ = PointD(path[j]); ptQ = TranslatePoint(ptQ, abs_group_delta_ * vec.x, abs_group_delta_ * vec.y); // get perpendicular vertices PointD pt1 = TranslatePoint(ptQ, group_delta_ * vec.y, group_delta_ * -vec.x); PointD pt2 = TranslatePoint(ptQ, group_delta_ * -vec.y, group_delta_ * vec.x); // get 2 vertices along one edge offset PointD pt3 = GetPerpendicD(path[k], norms[k], group_delta_); if (j == k) { PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); //get the second intersect point through reflecion group.path_.push_back(Point64(ReflectPoint(pt, ptQ))); group.path_.push_back(Point64(pt)); } else { PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); group.path_.push_back(Point64(pt)); //get the second intersect point through reflecion group.path_.push_back(Point64(ReflectPoint(pt, ptQ))); } } void ClipperOffset::DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a) { double q = group_delta_ / (cos_a + 1); group.path_.push_back(Point64( path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q)); } void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle) { //even though angle may be negative this is a convex join Point64 pt = path[j]; int steps = static_cast(std::ceil(steps_per_rad_ * std::abs(angle))); double step_sin = std::sin(angle / steps); double step_cos = std::cos(angle / steps); PointD pt2 = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); if (j == k) pt2.Negate(); group.path_.push_back(Point64(pt.x + pt2.x, pt.y + pt2.y)); for (int i = 0; i < steps; i++) { pt2 = PointD(pt2.x * step_cos - step_sin * pt2.y, pt2.x * step_sin + pt2.y * step_cos); group.path_.push_back(Point64(pt.x + pt2.x, pt.y + pt2.y)); } group.path_.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) { // Let A = change in angle where edges join // A == 0: ie no change in angle (flat join) // A == PI: edges 'spike' // sin(A) < 0: right turning // cos(A) < 0: change in angle is more than 90 degree if (path[j] == path[k]) { k = j; return; } double sin_a = CrossProduct(norms[j], norms[k]); double cos_a = DotProduct(norms[j], norms[k]); if (sin_a > 1.0) sin_a = 1.0; else if (sin_a < -1.0) sin_a = -1.0; bool almostNoAngle = AlmostZero(sin_a) && cos_a > 0; // when there's almost no angle of deviation or it's concave if (almostNoAngle || (sin_a * group_delta_ < 0)) { Point64 p1 = Point64( path[j].x + norms[k].x * group_delta_, path[j].y + norms[k].y * group_delta_); Point64 p2 = Point64( path[j].x + norms[j].x * group_delta_, path[j].y + norms[j].y * group_delta_); group.path_.push_back(p1); if (p1 != p2) { // when concave add an extra vertex to ensure neat clipping if (!almostNoAngle) group.path_.push_back(path[j]); group.path_.push_back(p2); } } else // it's convex { if (join_type_ == JoinType::Round) DoRound(group, path, j, k, std::atan2(sin_a, cos_a)); else if (join_type_ == JoinType::Miter) { // miter unless the angle is so acute the miter would exceeds ML if (cos_a > temp_lim_ - 1) DoMiter(group, path, j, k, cos_a); else DoSquare(group, path, j, k); } // don't bother squaring angles that deviate < ~20 degrees because // squaring will be indistinguishable from mitering and just be a lot slower else if (cos_a > 0.9) DoMiter(group, path, j, k, cos_a); else DoSquare(group, path, j, k); } k = j; } void ClipperOffset::OffsetPolygon(Group& group, Path64& path) { group.path_.clear(); for (Path64::size_type i = 0, j = path.size() -1; i < path.size(); j = i, ++i) OffsetPoint(group, path, i, j); group.paths_out_.push_back(group.path_); } void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) { OffsetPolygon(group, path); std::reverse(path.begin(), path.end()); BuildNormals(path); OffsetPolygon(group, path); } void ClipperOffset::OffsetOpenPath(Group& group, Path64& path, EndType end_type) { group.path_.clear(); // do the line start cap switch (end_type) { case EndType::Butt: group.path_.push_back(Point64( path[0].x - norms[0].x * group_delta_, path[0].y - norms[0].y * group_delta_)); group.path_.push_back(GetPerpendic(path[0], norms[0], group_delta_)); break; case EndType::Round: DoRound(group, path, 0,0, PI); break; default: DoSquare(group, path, 0, 0); break; } size_t highI = path.size() - 1; // offset the left side going forward for (Path64::size_type i = 1, k = 0; i < highI; ++i) OffsetPoint(group, path, i, k); // reverse normals for (size_t i = highI; i > 0; --i) norms[i] = PointD(-norms[i - 1].x, -norms[i - 1].y); norms[0] = norms[highI]; // do the line end cap switch (end_type) { case EndType::Butt: group.path_.push_back(Point64( path[highI].x - norms[highI].x * group_delta_, path[highI].y - norms[highI].y * group_delta_)); group.path_.push_back(GetPerpendic(path[highI], norms[highI], group_delta_)); break; case EndType::Round: DoRound(group, path, highI, highI, PI); break; default: DoSquare(group, path, highI, highI); break; } for (size_t i = highI, k = 0; i > 0; --i) OffsetPoint(group, path, i, k); group.paths_out_.push_back(group.path_); } void ClipperOffset::DoGroupOffset(Group& group, double delta) { if (group.end_type_ != EndType::Polygon) delta = std::abs(delta) * 0.5; bool isClosedPaths = IsClosedPath(group.end_type_); if (isClosedPaths) { //the lowermost polygon must be an outer polygon. So we can use that as the //designated orientation for outer polygons (needed for tidy-up clipping) Paths64::size_type lowestIdx = GetLowestPolygonIdx(group.paths_in_); // nb: don't use the default orientation here ... double area = Area(group.paths_in_[lowestIdx]); if (area == 0) return; group.is_reversed_ = (area < 0); if (group.is_reversed_) delta = -delta; } else group.is_reversed_ = false; group_delta_ = delta; abs_group_delta_ = std::abs(group_delta_); join_type_ = group.join_type_; double arcTol = (arc_tolerance_ > floating_point_tolerance ? arc_tolerance_ : std::log10(2 + abs_group_delta_) * default_arc_tolerance); // empirically derived //calculate a sensible number of steps (for 360 deg for the given offset if (group.join_type_ == JoinType::Round || group.end_type_ == EndType::Round) { steps_per_rad_ = PI / std::acos(1 - arcTol / abs_group_delta_) / (PI *2); } bool is_closed_path = IsClosedPath(group.end_type_); Paths64::const_iterator path_iter; for(path_iter = group.paths_in_.cbegin(); path_iter != group.paths_in_.cend(); ++path_iter) { Path64 path = StripDuplicates(*path_iter, is_closed_path); Path64::size_type cnt = path.size(); if (cnt == 0) continue; if (cnt == 1) // single point - only valid with open paths { group.path_ = Path64(); //single vertex so build a circle or square ... if (group.join_type_ == JoinType::Round) { double radius = abs_group_delta_; group.path_ = Ellipse(path[0], radius, radius); } else { int d = (int)std::ceil(abs_group_delta_); Rect64 r = Rect64(path[0].x - d, path[0].y - d, path[0].x + d, path[0].y + d); group.path_ = r.AsPath(); } group.paths_out_.push_back(group.path_); } else { BuildNormals(path); if (group.end_type_ == EndType::Polygon) OffsetPolygon(group, path); else if (group.end_type_ == EndType::Joined) OffsetOpenJoined(group, path); else OffsetOpenPath(group, path, group.end_type_); } } if (!merge_groups_) { //clean up self-intersections ... Clipper64 c; c.PreserveCollinear = false; //the solution should retain the orientation of the input c.ReverseSolution = reverse_solution_ != group.is_reversed_; c.AddSubject(group.paths_out_); if (group.is_reversed_) c.Execute(ClipType::Union, FillRule::Negative, group.paths_out_); else c.Execute(ClipType::Union, FillRule::Positive, group.paths_out_); } solution.reserve(solution.size() + group.paths_out_.size()); copy(group.paths_out_.begin(), group.paths_out_.end(), back_inserter(solution)); group.paths_out_.clear(); } Paths64 ClipperOffset::Execute(double delta) { solution.clear(); if (std::abs(delta) < default_arc_tolerance) { for (const Group& group : groups_) { solution.reserve(solution.size() + group.paths_in_.size()); copy(group.paths_in_.begin(), group.paths_in_.end(), back_inserter(solution)); } return solution; } temp_lim_ = (miter_limit_ <= 1) ? 2.0 : 2.0 / (miter_limit_ * miter_limit_); std::vector::iterator groups_iter; for (groups_iter = groups_.begin(); groups_iter != groups_.end(); ++groups_iter) { DoGroupOffset(*groups_iter, delta); } if (merge_groups_ && groups_.size() > 0) { //clean up self-intersections ... Clipper64 c; c.PreserveCollinear = false; //the solution should retain the orientation of the input c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed_; c.AddSubject(solution); if (groups_[0].is_reversed_) c.Execute(ClipType::Union, FillRule::Negative, solution); else c.Execute(ClipType::Union, FillRule::Positive, solution); } return solution; } } // namespace