diff options
Diffstat (limited to 'thirdparty/clipper2/src/clipper.offset.cpp')
-rw-r--r-- | thirdparty/clipper2/src/clipper.offset.cpp | 618 |
1 files changed, 618 insertions, 0 deletions
diff --git a/thirdparty/clipper2/src/clipper.offset.cpp b/thirdparty/clipper2/src/clipper.offset.cpp new file mode 100644 index 0000000000..78cd82376a --- /dev/null +++ b/thirdparty/clipper2/src/clipper.offset.cpp @@ -0,0 +1,618 @@ +/******************************************************************************* +* Author : Angus Johnson * +* Date : 22 March 2023 * +* Website : http://www.angusj.com * +* Copyright : Angus Johnson 2010-2023 * +* Purpose : Path Offset (Inflate/Shrink) * +* License : http://www.boost.org/LICENSE_1_0.txt * +*******************************************************************************/ + +#include <cmath> +#include "clipper2/clipper.h" +#include "clipper2/clipper.offset.h" + +namespace Clipper2Lib { + +const double default_arc_tolerance = 0.25; +const double floating_point_tolerance = 1e-12; + +//------------------------------------------------------------------------------ +// Miscellaneous methods +//------------------------------------------------------------------------------ + +void GetBoundsAndLowestPolyIdx(const Paths64& paths, Rect64& r, int & idx) +{ + idx = -1; + r = MaxInvalidRect64; + int64_t lpx = 0; + for (int i = 0; i < static_cast<int>(paths.size()); ++i) + for (const Point64& p : paths[i]) + { + if (p.y >= r.bottom) + { + if (p.y > r.bottom || p.x < lpx) + { + idx = i; + lpx = p.x; + r.bottom = p.y; + } + } + else if (p.y < r.top) r.top = p.y; + if (p.x > r.right) r.right = p.x; + else if (p.x < r.left) r.left = p.x; + } + //if (idx < 0) r = Rect64(0, 0, 0, 0); + //if (r.top == INT64_MIN) r.bottom = r.top; + //if (r.left == INT64_MIN) r.left = r.right; +} + +bool IsSafeOffset(const Rect64& r, double abs_delta) +{ + return r.left > min_coord + abs_delta && + r.right < max_coord - abs_delta && + r.top > min_coord + abs_delta && + r.bottom < max_coord - abs_delta; +} + +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<double>(pt2.x - pt1.x); + dy = static_cast<double>(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) +{ +#ifdef USINGZ + return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta, pt.z); +#else + return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta); +#endif +} + +inline PointD GetPerpendicD(const Point64& pt, const PointD& norm, double delta) +{ +#ifdef USINGZ + return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta, pt.z); +#else + return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta); +#endif +} + +inline void NegatePath(PathD& path) +{ + for (PointD& pt : path) + { + pt.x = -pt.x; + pt.y = -pt.y; +#ifdef USINGZ + pt.z = pt.z; +#endif + } +} + +//------------------------------------------------------------------------------ +// 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::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) +{ +#ifdef USINGZ + return PointD(pt.x + dx, pt.y + dy, pt.z); +#else + return PointD(pt.x + dx, pt.y + dy); +#endif +} + +inline PointD ReflectPoint(const PointD& pt, const PointD& pivot) +{ +#ifdef USINGZ + return PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y), pt.z); +#else + return PointD(pivot.x + (pivot.x - pt.x), pivot.y + (pivot.y - pt.y)); +#endif +} + +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); +#ifdef USINGZ + pt.z = ptQ.z; +#endif + //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); +#ifdef USINGZ + pt.z = ptQ.z; +#endif + 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); +#ifdef USINGZ + 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, + path[j].z)); +#else + 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)); +#endif +} + +void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle) +{ + Point64 pt = path[j]; + PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); + + if (j == k) offsetVec.Negate(); +#ifdef USINGZ + group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); +#else + group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); +#endif + if (angle > -PI + 0.01) // avoid 180deg concave + { + int steps = static_cast<int>(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456 + for (int i = 1; i < steps; ++i) // ie 1 less than steps + { + offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y, + offsetVec.x * step_sin_ + offsetVec.y * step_cos_); +#ifdef USINGZ + group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z)); +#else + group.path.push_back(Point64(pt.x + offsetVec.x, pt.y + offsetVec.y)); +#endif + + } + } + 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; + + if (cos_a > 0.99) // almost straight - less than 8 degrees + { + group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); + if (cos_a < 0.9998) // greater than 1 degree (#424) + group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); // (#418) + } + else if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) + { + // is concave + group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); + // this extra point is the only (simple) way to ensure that + // path reversals are fully cleaned with the trailing clipper + group.path.push_back(path[j]); // (#405) + group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); + } + else 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) +{ + 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()); + + //rebuild normals // BuildNormals(path); + std::reverse(norms.begin(), norms.end()); + norms.push_back(norms[0]); + norms.erase(norms.begin()); + NegatePath(norms); + + group.path.clear(); + OffsetPolygon(group, path); +} + +void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) +{ + // do the line start cap + switch (end_type_) + { + case EndType::Butt: +#ifdef USINGZ + group.path.push_back(Point64( + path[0].x - norms[0].x * group_delta_, + path[0].y - norms[0].y * group_delta_, + path[0].z)); +#else + group.path.push_back(Point64( + path[0].x - norms[0].x * group_delta_, + path[0].y - norms[0].y * group_delta_)); +#endif + 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: +#ifdef USINGZ + group.path.push_back(Point64( + path[highI].x - norms[highI].x * group_delta_, + path[highI].y - norms[highI].y * group_delta_, + path[highI].z)); +#else + group.path.push_back(Point64( + path[highI].x - norms[highI].x * group_delta_, + path[highI].y - norms[highI].y * group_delta_)); +#endif + 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) +{ + Rect64 r; + int idx = -1; + //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) + GetBoundsAndLowestPolyIdx(group.paths_in, r, idx); + if (idx < 0) return; + + if (group.end_type == EndType::Polygon) + { + double area = Area(group.paths_in[idx]); + //if (area == 0) return; // probably unhelpful (#430) + group.is_reversed = (area < 0); + if (group.is_reversed) group_delta_ = -delta_; + else group_delta_ = delta_; + } + else + { + group.is_reversed = false; + group_delta_ = std::abs(delta_) * 0.5; + } + abs_group_delta_ = std::fabs(group_delta_); + + // do range checking + if (!IsSafeOffset(r, abs_group_delta_)) + { + DoError(range_error_i); + error_code_ |= range_error_i; + return; + } + + join_type_ = group.join_type; + end_type_ = group.end_type; + + //calculate a sensible number of steps (for 360 deg for the given offset + if (group.join_type == JoinType::Round || group.end_type == EndType::Round) + { + // arcTol - when arc_tolerance_ is undefined (0), the amount of + // curve imprecision that's allowed is based on the size of the + // offset (delta). Obviously very large offsets will almost always + // require much less precision. See also offset_triginometry2.svg + double arcTol = (arc_tolerance_ > floating_point_tolerance ? + std::min(abs_group_delta_, arc_tolerance_) : + std::log10(2 + abs_group_delta_) * default_arc_tolerance); + double steps_per_360 = PI / std::acos(1 - arcTol / abs_group_delta_); + if (steps_per_360 > abs_group_delta_ * PI) + steps_per_360 = abs_group_delta_ * PI; //ie avoids excessive precision + + step_sin_ = std::sin(2 * PI / steps_per_360); + step_cos_ = std::cos(2 * PI / steps_per_360); + if (group_delta_ < 0.0) step_sin_ = -step_sin_; + steps_per_rad_ = steps_per_360 / (2 *PI); + } + + bool is_joined = + (end_type_ == EndType::Polygon) || + (end_type_ == EndType::Joined); + 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_joined); + Path64::size_type cnt = path.size(); + if (cnt == 0 || ((cnt < 3) && group.end_type == EndType::Polygon)) + continue; + + group.path.clear(); + if (cnt == 1) // single point - only valid with open paths + { + if (group_delta_ < 1) continue; + //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); +#ifdef USINGZ + for (auto& p : group.path) p.z = path[0].z; +#endif + } + else + { + int d = (int)std::ceil(abs_group_delta_); + r = Rect64(path[0].x - d, path[0].y - d, path[0].x + d, path[0].y + d); + group.path = r.AsPath(); +#ifdef USINGZ + for (auto& p : group.path) p.z = path[0].z; +#endif + } + group.paths_out.push_back(group.path); + } + else + { + if ((cnt == 2) && (group.end_type == EndType::Joined)) + { + if (group.join_type == JoinType::Round) + end_type_ = EndType::Round; + else + end_type_ = EndType::Square; + } + + BuildNormals(path); + if (end_type_ == EndType::Polygon) OffsetPolygon(group, path); + else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, path); + else OffsetOpenPath(group, path); + } + } + solution.reserve(solution.size() + group.paths_out.size()); + copy(group.paths_out.begin(), group.paths_out.end(), back_inserter(solution)); + group.paths_out.clear(); +} + +void ClipperOffset::ExecuteInternal(double delta) +{ + error_code_ = 0; + solution.clear(); + if (groups_.size() == 0) return; + + if (std::abs(delta) < 0.5) + { + 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)); + } + } + else + { + temp_lim_ = (miter_limit_ <= 1) ? + 2.0 : + 2.0 / (miter_limit_ * miter_limit_); + + delta_ = delta; + std::vector<Group>::iterator git; + for (git = groups_.begin(); git != groups_.end(); ++git) + { + DoGroupOffset(*git); + if (!error_code_) continue; // all OK + solution.clear(); + } + } +} + +void ClipperOffset::Execute(double delta, Paths64& paths) +{ + paths.clear(); + + ExecuteInternal(delta); + if (!solution.size()) return; + + paths = solution; + //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; +#ifdef USINGZ + if (zCallback64_) { + c.SetZCallback(zCallback64_); + } +#endif + c.AddSubject(solution); + if (groups_[0].is_reversed) + c.Execute(ClipType::Union, FillRule::Negative, paths); + else + c.Execute(ClipType::Union, FillRule::Positive, paths); +} + + +void ClipperOffset::Execute(double delta, PolyTree64& polytree) +{ + polytree.Clear(); + + ExecuteInternal(delta); + if (!solution.size()) return; + + //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; +#ifdef USINGZ + if (zCallback64_) { + c.SetZCallback(zCallback64_); + } +#endif + c.AddSubject(solution); + if (groups_[0].is_reversed) + c.Execute(ClipType::Union, FillRule::Negative, polytree); + else + c.Execute(ClipType::Union, FillRule::Positive, polytree); +} + +} // namespace |