From 6e15ba03b6150d3d3c7d9db055eab49baf9b280f Mon Sep 17 00:00:00 2001 From: angusj Date: Tue, 16 May 2023 15:41:55 +1000 Subject: [PATCH] ClipperOffset added support for variable offsets (#511) further reduced extraneous vertices in solutions (#499) ClipperEngine Fixed a minor bug with polytrees (#520) --- CPP/CMakeLists.txt | 2 + .../include/clipper2/clipper.core.h | 2 +- .../include/clipper2/clipper.engine.h | 18 +- CPP/Clipper2Lib/include/clipper2/clipper.h | 47 +++-- .../include/clipper2/clipper.offset.h | 10 +- CPP/Clipper2Lib/src/clipper.engine.cpp | 23 ++- CPP/Clipper2Lib/src/clipper.offset.cpp | 184 +++++++++++------- .../VariableOffset/VariableOffset.cpp | 139 +++++++++++++ CPP/Tests/TestPolytreeHoles4.cpp | 26 +++ CSharp/Clipper2Lib/Clipper.Engine.cs | 28 +-- CSharp/Clipper2Lib/Clipper.Offset.cs | 184 +++++++++++------- Delphi/Clipper2Lib/Clipper.Engine.pas | 31 ++- Delphi/Clipper2Lib/Clipper.Offset.pas | 87 +++++++-- 13 files changed, 557 insertions(+), 224 deletions(-) create mode 100644 CPP/Examples/VariableOffset/VariableOffset.cpp create mode 100644 CPP/Tests/TestPolytreeHoles4.cpp diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt index b1e37afa..ad167700 100644 --- a/CPP/CMakeLists.txt +++ b/CPP/CMakeLists.txt @@ -137,6 +137,7 @@ if(CLIPPER2_EXAMPLES) UnionClipping RectClipping SimpleClipping + VariableOffset ) foreach(ex ${EXAMPLES}) @@ -190,6 +191,7 @@ endif() Tests/TestPolytreeHoles1.cpp Tests/TestPolytreeHoles2.cpp Tests/TestPolytreeHoles3.cpp + Tests/TestPolytreeHoles4.cpp Tests/TestPolytreeIntersection.cpp Tests/TestPolytreeUnion.cpp Tests/TestRandomPaths.cpp diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index e4442a68..9fd53d03 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -428,7 +428,7 @@ namespace Clipper2Lib } template - inline Path ScalePath(const Path& path, + inline Path ScalePath(const Path& path, double scale, int& error_code) { return ScalePath(path, scale, scale, error_code); diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h index 840d1f50..3c049fc5 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.engine.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.engine.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 May 2023 * +* Date : 15 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -270,7 +270,7 @@ namespace Clipper2Lib { bool ExecuteInternal(ClipType ct, FillRule ft, bool use_polytrees); void CleanCollinear(OutRec* outrec); bool CheckBounds(OutRec* outrec); - bool CheckSplitOwner(OutRec* outrec); + bool CheckSplitOwner(OutRec* outrec, OutRecList* splits); void RecursiveCheckOwners(OutRec* outrec, PolyPath* polypath); #ifdef USINGZ ZCallback64 zCallback_ = nullptr; @@ -347,7 +347,7 @@ namespace Clipper2Lib { const PolyPath64* operator [] (size_t index) const { - return childs_[index].get(); + return childs_[index].get(); //std::unique_ptr } const PolyPath64* Child(size_t index) const @@ -390,12 +390,12 @@ namespace Clipper2Lib { class PolyPathD : public PolyPath { private: PolyPathDList childs_; - double inv_scale_; + double scale_; PathD polygon_; public: explicit PolyPathD(PolyPathD* parent = nullptr) : PolyPath(parent) { - inv_scale_ = parent ? parent->inv_scale_ : 1.0; + scale_ = parent ? parent->scale_ : 1.0; } ~PolyPathD() { @@ -415,14 +415,14 @@ namespace Clipper2Lib { PolyPathDList::const_iterator begin() const { return childs_.cbegin(); } PolyPathDList::const_iterator end() const { return childs_.cend(); } - void SetInvScale(double value) { inv_scale_ = value; } - double InvScale() { return inv_scale_; } + void SetScale(double value) { scale_ = value; } + double Scale() { return scale_; } PolyPathD* AddChild(const Path64& path) override { int error_code = 0; auto p = std::make_unique(this); PolyPathD* result = childs_.emplace_back(std::move(p)).get(); - result->polygon_ = ScalePath(path, inv_scale_, error_code); + result->polygon_ = ScalePath(path, scale_, error_code); return result; } @@ -610,7 +610,7 @@ namespace Clipper2Lib { if (ExecuteInternal(clip_type, fill_rule, true)) { polytree.Clear(); - polytree.SetInvScale(invScale_); + polytree.SetScale(invScale_); open_paths.clear(); BuildTreeD(polytree, open_paths); } diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h index f406eaef..937e80f7 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 21 April 2023 * +* Date : 15 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This module provides a simple interface to the Clipper Library * @@ -161,40 +161,44 @@ namespace Clipper2Lib { return ScalePaths(solution, 1 / scale, error_code); } - inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + template + inline Path TranslatePath(const Path& path, T dx, T dy) { - Path64 result; + Path result; result.reserve(path.size()); std::transform(path.begin(), path.end(), back_inserter(result), - [dx, dy](const auto& pt) { return Point64(pt.x + dx, pt.y +dy); }); + [dx, dy](const auto& pt) { return Point(pt.x + dx, pt.y +dy); }); return result; } + inline Path64 TranslatePath(const Path64& path, int64_t dx, int64_t dy) + { + return TranslatePath(path, dx, dy); + } + inline PathD TranslatePath(const PathD& path, double dx, double dy) { - PathD result; - result.reserve(path.size()); - std::transform(path.begin(), path.end(), back_inserter(result), - [dx, dy](const auto& pt) { return PointD(pt.x + dx, pt.y + dy); }); - return result; + return TranslatePath(path, dx, dy); } - inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + template + inline Paths TranslatePaths(const Paths& paths, T dx, T dy) { - Paths64 result; + Paths result; result.reserve(paths.size()); std::transform(paths.begin(), paths.end(), back_inserter(result), [dx, dy](const auto& path) { return TranslatePath(path, dx, dy); }); return result; } + inline Paths64 TranslatePaths(const Paths64& paths, int64_t dx, int64_t dy) + { + return TranslatePaths(paths, dx, dy); + } + inline PathsD TranslatePaths(const PathsD& paths, double dx, double dy) { - PathsD result; - result.reserve(paths.size()); - std::transform(paths.begin(), paths.end(), back_inserter(result), - [dx, dy](const auto& path) { return TranslatePath(path, dx, dy); }); - return result; + return TranslatePaths(paths, dx, dy); } inline Paths64 ExecuteRectClip(const Rect64& rect, @@ -290,14 +294,9 @@ namespace Clipper2Lib { { // return false if this child isn't fully contained by its parent - // the following algorithm is a bit too crude, and doesn't account - // for rounding errors. A better algorithm is to return false when - // consecutive vertices are found outside the parent's polygon. - - //const Path64& path = pp.Polygon(); - //if (std::any_of(child->Polygon().cbegin(), child->Polygon().cend(), - // [path](const auto& pt) {return (PointInPolygon(pt, path) == - // PointInPolygonResult::IsOutside); })) return false; + // checking for a single vertex outside is a bit too crude since + // it doesn't account for rounding errors. It's better to check + // for consecutive vertices found outside the parent's polygon. int outsideCnt = 0; for (const Point64& pt : child->Polygon()) diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index f5d47e07..8835fb0f 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 22 March 2023 * +* Date : 15 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -24,6 +24,7 @@ enum class EndType {Polygon, Joined, Butt, Square, Round}; //Joined : offsets both sides of a path, with joined ends //Polygon: offsets only one side of a closed path +typedef std::function DeltaCallback64; class ClipperOffset { private: @@ -43,7 +44,6 @@ class ClipperOffset { int error_code_ = 0; double delta_ = 0.0; double group_delta_ = 0.0; - double abs_group_delta_ = 0.0; double temp_lim_ = 0.0; double steps_per_rad_ = 0.0; double step_sin_ = 0.0; @@ -62,6 +62,7 @@ class ClipperOffset { #ifdef USINGZ ZCallback64 zCallback64_ = nullptr; #endif + DeltaCallback64 deltaCallback64_ = nullptr; void DoSquare(Group& group, const Path64& path, size_t j, size_t k); void DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a); @@ -70,7 +71,7 @@ class ClipperOffset { void OffsetPolygon(Group& group, Path64& path); void OffsetOpenJoined(Group& group, Path64& path); void OffsetOpenPath(Group& group, Path64& path); - void OffsetPoint(Group& group, Path64& path, size_t j, size_t& k); + void OffsetPoint(Group& group, Path64& path, size_t j, size_t k); void DoGroupOffset(Group &group); void ExecuteInternal(double delta); public: @@ -91,6 +92,7 @@ class ClipperOffset { void Execute(double delta, Paths64& paths); void Execute(double delta, PolyTree64& polytree); + void Execute(DeltaCallback64 delta_cb, Paths64& paths); double MiterLimit() const { return miter_limit_; } void MiterLimit(double miter_limit) { miter_limit_ = miter_limit; } @@ -108,6 +110,8 @@ class ClipperOffset { #ifdef USINGZ void SetZCallback(ZCallback64 cb) { zCallback64_ = cb; } #endif + void SetDeltaCallback(DeltaCallback64 cb) { deltaCallback64_ = cb; } + }; } diff --git a/CPP/Clipper2Lib/src/clipper.engine.cpp b/CPP/Clipper2Lib/src/clipper.engine.cpp index 141c47eb..e29f2fe6 100644 --- a/CPP/Clipper2Lib/src/clipper.engine.cpp +++ b/CPP/Clipper2Lib/src/clipper.engine.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 May 2023 * +* Date : 16 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -1378,6 +1378,7 @@ namespace Clipper2Lib { result->owner = nullptr; result->polypath = nullptr; result->is_open = false; + result->splits = nullptr; return result; } @@ -2174,7 +2175,7 @@ namespace Clipper2Lib { if (or1 == or2) { - or2 = new OutRec(); + or2 = NewOutRec(); or2->pts = op1b; FixOutRecPts(or2); if (or1->pts->outrec == or2) @@ -2186,7 +2187,11 @@ namespace Clipper2Lib { if (using_polytree_) { if (Path1InsidePath2(or2->pts, or1->pts)) + { SetOwner(or2, or1); + if (!or1->splits) or1->splits = new OutRecList(); + or1->splits->push_back(or2); //(#520) + } else if (Path1InsidePath2(or1->pts, or2->pts)) SetOwner(or1, or2); else @@ -2198,8 +2203,6 @@ namespace Clipper2Lib { } else or2->owner = or1; - - outrec_list_.push_back(or2); } else { @@ -2826,14 +2829,14 @@ namespace Clipper2Lib { return true; } - bool ClipperBase::CheckSplitOwner(OutRec* outrec) + bool ClipperBase::CheckSplitOwner(OutRec* outrec, OutRecList* splits) { - for (auto s : *outrec->owner->splits) + for (auto s : *splits) { OutRec* split = GetRealOutRec(s); - if (split && split != outrec && - split != outrec->owner && CheckBounds(split) && - split->bounds.Contains(outrec->bounds) && + if (!split || split == outrec || split == outrec->owner) continue; + else if (split->splits && CheckSplitOwner(outrec, split->splits)) return true; + else if (CheckBounds(split) && split->bounds.Contains(outrec->bounds) && Path1InsidePath2(outrec->pts, split->pts)) { outrec->owner = split; //found in split @@ -2852,7 +2855,7 @@ namespace Clipper2Lib { while (outrec->owner) { - if (outrec->owner->splits && CheckSplitOwner(outrec)) break; + if (outrec->owner->splits && CheckSplitOwner(outrec, outrec->owner->splits)) break; if (outrec->owner->pts && CheckBounds(outrec->owner) && outrec->owner->bounds.Contains(outrec->bounds) && Path1InsidePath2(outrec->pts, outrec->owner->pts)) break; diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index a60cf05b..5baa466a 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 8 April 2023 * +* Date : 15 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -211,9 +211,11 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t PointD(-norms[k].y, norms[k].x), PointD(norms[j].y, -norms[j].x)); + double abs_delta = std::abs(group_delta_); + // 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); + ptQ = TranslatePoint(ptQ, abs_delta * vec.x, abs_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); @@ -260,6 +262,20 @@ void ClipperOffset::DoMiter(Group& group, const Path64& path, size_t j, size_t k void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k, double angle) { + if (deltaCallback64_) { + // when deltaCallback64_ is assigned, group_delta_ won't be constant, + // so we'll need to do the following calculations for *every* vertex. + double abs_delta = std::fabs(group_delta_); + double arcTol = (arc_tolerance_ > floating_point_tolerance ? + std::min(abs_delta, arc_tolerance_) : + std::log10(2 + abs_delta) * default_arc_tolerance); + double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI); + 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); + } + Point64 pt = path[j]; PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); @@ -287,7 +303,7 @@ void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k group.path.push_back(GetPerpendic(path[j], norms[j], group_delta_)); } -void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) +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) @@ -302,7 +318,16 @@ void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& 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 && (sin_a * group_delta_ < 0)) + if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, j, k); + if (std::fabs(group_delta_) <= floating_point_tolerance) + { + group.path.push_back(path[j]); + return; + } + + if (cos_a > 0.99) // 0.99 ~= 8.1 deg. + DoMiter(group, path, j, k, cos_a); + else if (cos_a > -0.99 && (sin_a * group_delta_ < 0)) { // is concave group.path.push_back(GetPerpendic(path[j], norms[k], group_delta_)); @@ -317,22 +342,16 @@ void ClipperOffset::OffsetPoint(Group& group, Path64& path, size_t j, size_t& k) if (cos_a > temp_lim_ - 1) DoMiter(group, path, j, k, cos_a); else DoSquare(group, path, j, k); } - else if (cos_a > 0.9998) - // almost straight - less than 1 degree (#424) - DoMiter(group, path, j, k, cos_a); - else if (cos_a > 0.99 || join_type_ == JoinType::Square) - //angle less than 8 degrees or squared joins + else if (join_type_ == JoinType::Square) DoSquare(group, path, j, k); else DoRound(group, path, j, k, std::atan2(sin_a, cos_a)); - - 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); + for (Path64::size_type j = 0, k = path.size() -1; j < path.size(); k = j, ++j) + OffsetPoint(group, path, j, k); group.paths_out.push_back(group.path); } @@ -354,34 +373,40 @@ void ClipperOffset::OffsetOpenJoined(Group& group, Path64& path) void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) { // do the line start cap - switch (end_type_) + if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0); + + if (std::fabs(group_delta_) <= floating_point_tolerance) + group.path.push_back(path[0]); + else { - case EndType::Butt: + 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)); + 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_)); + 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; + 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); + for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j) + OffsetPoint(group, path, j, k); // reverse normals for (size_t i = highI; i > 0; --i) @@ -389,31 +414,39 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) norms[0] = norms[highI]; // do the line end cap - switch (end_type_) + if (deltaCallback64_) + group_delta_ = deltaCallback64_(path, norms, highI, highI); + + if (std::fabs(group_delta_) <= floating_point_tolerance) + group.path.push_back(path[highI]); + else { - case EndType::Butt: + 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)); + 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_)); + 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; + 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); + for (size_t j = highI, k = 0; j > 0; k = j, --j) + OffsetPoint(group, path, j, k); group.paths_out.push_back(group.path); } @@ -439,10 +472,10 @@ void ClipperOffset::DoGroupOffset(Group& group) group.is_reversed = false; group_delta_ = std::abs(delta_) * 0.5; } - abs_group_delta_ = std::fabs(group_delta_); + double abs_delta = std::fabs(group_delta_); // do range checking - if (!IsSafeOffset(r, abs_group_delta_)) + if (!IsSafeOffset(r, abs_delta)) { DoError(range_error_i); error_code_ |= range_error_i; @@ -455,21 +488,22 @@ void ClipperOffset::DoGroupOffset(Group& group) //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); + if (!deltaCallback64_) { + // 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_delta, arc_tolerance_) : + std::log10(2 + abs_delta) * default_arc_tolerance); + + double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI); + 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 = @@ -491,7 +525,7 @@ void ClipperOffset::DoGroupOffset(Group& group) //single vertex so build a circle or square ... if (group.join_type == JoinType::Round) { - double radius = abs_group_delta_; + double radius = abs_delta; group.path = Ellipse(path[0], radius, radius); #ifdef USINGZ for (auto& p : group.path) p.z = path[0].z; @@ -499,7 +533,7 @@ void ClipperOffset::DoGroupOffset(Group& group) } else { - int d = (int)std::ceil(abs_group_delta_); + int d = (int)std::ceil(abs_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 @@ -568,6 +602,7 @@ void ClipperOffset::Execute(double delta, Paths64& paths) if (!solution.size()) return; paths = solution; + /**/ //clean up self-intersections ... Clipper64 c; c.PreserveCollinear = false; @@ -583,6 +618,7 @@ void ClipperOffset::Execute(double delta, Paths64& paths) c.Execute(ClipType::Union, FillRule::Negative, paths); else c.Execute(ClipType::Union, FillRule::Positive, paths); +/**/ } @@ -610,4 +646,10 @@ void ClipperOffset::Execute(double delta, PolyTree64& polytree) c.Execute(ClipType::Union, FillRule::Positive, polytree); } +void ClipperOffset::Execute(DeltaCallback64 delta_cb, Paths64& paths) +{ + deltaCallback64_ = delta_cb; + Execute(1.0, paths); +} + } // namespace diff --git a/CPP/Examples/VariableOffset/VariableOffset.cpp b/CPP/Examples/VariableOffset/VariableOffset.cpp new file mode 100644 index 00000000..1571c702 --- /dev/null +++ b/CPP/Examples/VariableOffset/VariableOffset.cpp @@ -0,0 +1,139 @@ +#include + +#include "clipper2/clipper.h" +#include "clipper2/clipper.core.h" +#include "../../Utils/clipper.svg.utils.h" +#include "../../Utils/CommonUtils.h" + +using namespace Clipper2Lib; + +void System(const std::string& filename) +{ +#ifdef _WIN32 + system(filename.c_str()); +#else + system(("firefox " + filename).c_str()); +#endif +} + +void test1() { + + int64_t const scale = 10; + double delta = 10 * scale; + + ClipperOffset co; + co.SetDeltaCallback([delta](const Path64& path, + const PathD& path_norms, size_t curr_idx, size_t prev_idx) + { + // gradually scale down the offset to a minimum of 25% of delta + double high = static_cast(path.size() - 1) * 1.25; + return (high - curr_idx) / high * delta; + }); + + Path64 ellipse = Ellipse(Rect64(0, 0, 200 * scale, 180 * scale)); + size_t el_size = ellipse.size() * 0.9; + ellipse.resize(el_size); + Paths64 subject = { ellipse }; + + co.AddPaths(subject, JoinType::Miter, EndType::Round); + Paths64 solution; + co.Execute(1.0, solution); + + SvgWriter svg; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, "c:\\temp\\tmp1.svg", 400, 400); + System("c:\\temp\\tmp1.svg"); +} + +void test2() { + + int64_t const scale = 10; + double delta = 10 * scale; + + ClipperOffset co; + co.SetDeltaCallback([delta](const Path64& path, + const PathD& path_norms, size_t curr_idx, size_t prev_idx) { + // calculate offset based on distance from the middle of the path + double mid_idx = static_cast(path.size()) / 2.0; + return delta * (1.0 - 0.70 * (std::fabs(curr_idx - mid_idx) / mid_idx)); + }); + + Path64 ellipse = Ellipse(Rect64(0, 0, 200 * scale, 180 * scale)); + size_t el_size = ellipse.size() * 0.9; + ellipse.resize(el_size); + Paths64 subject = { ellipse }; + + co.AddPaths(subject, JoinType::Miter, EndType::Round); + Paths64 solution; + co.Execute(1.0, solution); + + SvgWriter svg; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, "c:\\temp\\tmp2.svg", 400, 400); + System("c:\\temp\\tmp2.svg"); +} + +void test3() { + + double radius = 5000.0; + Paths64 subject = { Ellipse(Rect64(-radius, -radius, radius, radius), 200) }; + + ClipperOffset co; + co.AddPaths(subject, JoinType::Miter, EndType::Polygon); + + co.SetDeltaCallback([radius](const Path64& path, + const PathD& path_norms, size_t curr_idx, size_t prev_idx) { + // when multiplying the x & y of edge unit normal vectors, the value will be + // largest (0.5) when edges are at 45 deg. and least (-0.5) at negative 45 deg. + double delta = path_norms[curr_idx].y * path_norms[curr_idx].x; + return radius * 0.5 + radius * delta; + }); + + // solution + Paths64 solution; + co.Execute(1.0, solution); + + SvgWriter svg; + SvgAddSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, "c:\\temp\\tmp3.svg", 400, 400); + System("c:\\temp\\tmp3.svg"); +} + +void test4() { + + int64_t const scale = 100; + Paths64 solution; + Paths64 subject = { Ellipse(ScaleRect(Rect64(10, 10, 50, 50), scale)) }; + + ClipperOffset co; + co.AddPaths(subject, JoinType::Round, EndType::Round); + co.Execute( + [scale](const Path64& path, + const PathD& path_norms, size_t curr_idx, size_t prev_idx) { + //double vertex_sin_a = CrossProduct(path_norms[curr_idx], path_norms[prev_idx]); + //double vertex_cos_a = DotProduct(path_norms[curr_idx], path_norms[prev_idx]); + //double vertex_angle = std::atan2(vertex_sin_a, vertex_cos_a); + //double edge_angle = std::atan2(path_norms[curr_idx].y, path_norms[curr_idx].x); + double sin_edge = path_norms[curr_idx].y; + return Sqr(sin_edge) * 3 * scale; } + , solution); + + SvgWriter svg; + SvgAddOpenSubject(svg, subject, FillRule::NonZero); + SvgAddSolution(svg, solution, FillRule::NonZero, false); + SvgSaveToFile(svg, "c:\\temp\\tmp4.svg", 400, 400); + System("c:\\temp\\tmp4.svg"); +} + + +int main() { + + test1(); + test2(); + test3(); + test4(); + return 0; +} \ No newline at end of file diff --git a/CPP/Tests/TestPolytreeHoles4.cpp b/CPP/Tests/TestPolytreeHoles4.cpp new file mode 100644 index 00000000..290abddd --- /dev/null +++ b/CPP/Tests/TestPolytreeHoles4.cpp @@ -0,0 +1,26 @@ +#include +#include "clipper2/clipper.h" + +using namespace Clipper2Lib; + +TEST(Clipper2Tests, TestPolytreeHoles4) +{ + Paths64 subject; + PolyTree64 solution; + Clipper64 c; + subject.push_back(MakePath({ 50,500, 50,300, 100,300, 100,350, 150,350, + 150,250, 200,250, 200,450, 350,450, 350,200, 400,200, 400,225, 450,225, + 450,175, 400,175, 400,200, 350,200, 350,175, 200,175, 200,250, 150,250, + 150,200, 100,200, 100,300, 50,300, 50,125, 500,125, 500,500 })); + subject.push_back(MakePath({ 250,425, 250,375, 300,375, 300,425})); + + c.AddSubject(subject); + c.Execute(ClipType::Union, FillRule::NonZero, solution); + // Polytree root + // +- Polygon with 3 holes. + // +- Hole with 1 nested polygon. + // +-Polygon + // +- Hole + // +- Hole + EXPECT_TRUE(solution.Count() == 1 && solution[0]->Count() == 3); +} \ No newline at end of file diff --git a/CSharp/Clipper2Lib/Clipper.Engine.cs b/CSharp/Clipper2Lib/Clipper.Engine.cs index 9c887028..995ca382 100644 --- a/CSharp/Clipper2Lib/Clipper.Engine.cs +++ b/CSharp/Clipper2Lib/Clipper.Engine.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 1 May 2023 * +* Date : 14 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -2701,10 +2701,9 @@ private void ProcessHorzJoins() if (or1 == or2) { - or2 = new OutRec - { - pts = op1b - }; + or2 = NewOutRec(); + or2.pts = op1b; + FixOutRecPts(or2); if (or1.pts!.outrec == or2) { @@ -2715,7 +2714,11 @@ private void ProcessHorzJoins() if (_using_polytree) { if (Path1InsidePath2(or2.pts, or1.pts)) + { SetOwner(or2, or1); + or1.splits ??= new List(); + or1.splits.Add(or2.idx); // (#520) + } else if (Path1InsidePath2(or1.pts, or2.pts)) SetOwner(or1, or2); else @@ -2727,8 +2730,6 @@ private void ProcessHorzJoins() } else or2.owner = or1; - - _outrecList.Add(or2); } else { @@ -3009,14 +3010,14 @@ private bool CheckBounds(OutRec outrec) return true; } - private bool CheckSplitOwner(OutRec outrec) + private bool CheckSplitOwner(OutRec outrec, List? splits) { foreach (int i in outrec.owner!.splits!) { OutRec? split = GetRealOutRec(_outrecList[i]); - if (split != null && split != outrec && - split != outrec.owner && CheckBounds(split) && - split.bounds.Contains(outrec.bounds) && + if (split == null || split == outrec || split == outrec.owner) continue; + else if (split.splits != null && CheckSplitOwner(outrec, split.splits)) return true; + else if (CheckBounds(split) && split.bounds.Contains(outrec.bounds) && Path1InsidePath2(outrec.pts!, split.pts!)) { outrec.owner = split; //found in split @@ -3034,8 +3035,9 @@ private void RecursiveCheckOwners(OutRec outrec, PolyPathBase polypath) while (outrec.owner != null) { - if (outrec.owner.splits != null && CheckSplitOwner(outrec)) break; - if (outrec.owner.pts != null && CheckBounds(outrec.owner) && + if (outrec.owner.splits != null && + CheckSplitOwner(outrec, outrec.owner.splits)) break; + else if (outrec.owner.pts != null && CheckBounds(outrec.owner) && Path1InsidePath2(outrec.pts!, outrec.owner.pts!)) break; outrec.owner = outrec.owner.owner; } diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index ed6c3ceb..0716acc4 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 8 April 2023 * +* Date : 16 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -52,12 +52,12 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon } } + private static double Tolerance = 1.0E-12; private readonly List _groupList = new List(); private readonly PathD _normals = new PathD(); private readonly Paths64 _solution = new Paths64(); - private double _group_delta; //*0.5 for open paths; *-1.0 for negative areas + private double _groupDelta; //*0.5 for open paths; *-1.0 for negative areas private double _delta; - private double _abs_group_delta; private double _mitLimSqr; private double _stepsPerRad; private double _stepSin; @@ -69,6 +69,11 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon public double MiterLimit { get; set; } public bool PreserveCollinear { get; set; } public bool ReverseSolution { get; set; } + + public delegate double DeltaCallback64(Path64 path, + PathD path_norms, int currPt, int prevPt); + public ClipperOffset.DeltaCallback64? DeltaCallback { get; set; } + #if USINGZ public ClipperBase.ZCallback64? ZCallback { get; set; } #endif @@ -189,6 +194,11 @@ internal static PointD GetUnitNormal(Point64 pt1, Point64 pt2) return new PointD(dy, -dx); } + public void Execute(DeltaCallback64 deltaCallback, Paths64 solution) + { + DeltaCallback = deltaCallback; + Execute(1.0, solution); + } private static void GetBoundsAndLowestPolyIdx(Paths64 paths, out int index, out Rect64 rec) { @@ -297,8 +307,8 @@ private Point64 GetPerpendic(Point64 pt, PointD norm) return new Point64(pt.X + norm.x * _group_delta, pt.Y + norm.y * _group_delta, pt.Z); #else - return new Point64(pt.X + norm.x * _group_delta, - pt.Y + norm.y * _group_delta); + return new Point64(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta); #endif } @@ -309,8 +319,8 @@ private PointD GetPerpendicD(Point64 pt, PointD norm) return new PointD(pt.X + norm.x * _group_delta, pt.Y + norm.y * _group_delta, pt.Z); #else - return new PointD(pt.X + norm.x * _group_delta, - pt.Y + norm.y * _group_delta); + return new PointD(pt.X + norm.x * _groupDelta, + pt.Y + norm.y * _groupDelta); #endif } @@ -328,22 +338,23 @@ private void DoSquare(Group group, Path64 path, int j, int k) new PointD(-_normals[k].y, _normals[k].x), new PointD(_normals[j].y, -_normals[j].x)); } - + + double absDelta = Math.Abs(_groupDelta); // now offset the original vertex delta units along unit vector PointD ptQ = new PointD(path[j]); - ptQ = TranslatePoint(ptQ, _abs_group_delta * vec.x, _abs_group_delta * vec.y); + ptQ = TranslatePoint(ptQ, absDelta * vec.x, absDelta * 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); + PointD pt1 = TranslatePoint(ptQ, _groupDelta * vec.y, _groupDelta * -vec.x); + PointD pt2 = TranslatePoint(ptQ, _groupDelta * -vec.y, _groupDelta * vec.x); // get 2 vertices along one edge offset PointD pt3 = GetPerpendicD(path[k], _normals[k]); if (j == k) { PointD pt4 = new PointD( - pt3.x + vec.x * _group_delta, - pt3.y + vec.y * _group_delta); + pt3.x + vec.x * _groupDelta, + pt3.y + vec.y * _groupDelta); PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); #if USINGZ pt.z = ptQ.z; @@ -368,7 +379,7 @@ private void DoSquare(Group group, Path64 path, int j, int k) [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DoMiter(Group group, Path64 path, int j, int k, double cosA) { - double q = _group_delta / (cosA + 1); + double q = _groupDelta / (cosA + 1); #if USINGZ group.outPath.Add(new Point64( path[j].X + (_normals[k].x + _normals[j].x) * q, @@ -384,8 +395,23 @@ private void DoMiter(Group group, Path64 path, int j, int k, double cosA) [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DoRound(Group group, Path64 path, int j, int k, double angle) { + if (DeltaCallback != null) + { + // when DeltaCallback is assigned, _groupDelta won't be constant, + // so we'll need to do the following calculations for *every* vertex. + double absDelta = Math.Abs(_groupDelta); + double arcTol = ArcTolerance > 0.01 ? + ArcTolerance : + Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; + double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta); + _stepSin = Math.Sin((2 * Math.PI) / stepsPer360); + _stepCos = Math.Cos((2 * Math.PI) / stepsPer360); + if (_groupDelta < 0.0) _stepSin = -_stepSin; + _stepsPerRad = stepsPer360 / (2 * Math.PI); + } + Point64 pt = path[j]; - PointD offsetVec = new PointD(_normals[k].x * _group_delta, _normals[k].y * _group_delta); + PointD offsetVec = new PointD(_normals[k].x * _groupDelta, _normals[k].y * _groupDelta); if (j == k) offsetVec.Negate(); #if USINGZ group.outPath.Add(new Point64(pt.X + offsetVec.x, pt.Y + offsetVec.y, pt.Z)); @@ -433,8 +459,17 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) if (sinA > 1.0) sinA = 1.0; else if (sinA < -1.0) sinA = -1.0; + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, j, k); + if (Math.Abs(_groupDelta) < Tolerance) + { + group.outPath.Add(path[j]); + return; + } - if (cosA > -0.99 && (sinA * _group_delta < 0)) + if (cosA > 0.99) + DoMiter(group, path, j, k, cosA); + else if (cosA > -0.99 && (sinA * _groupDelta < 0)) { // is concave group.outPath.Add(GetPerpendic(path[j], _normals[k])); @@ -449,14 +484,11 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k) if (cosA > _mitLimSqr - 1) DoMiter(group, path, j, k, cosA); else DoSquare(group, path, j, k); } - else if (cosA > 0.9998) - // almost straight - less than 1 degree (#424) - DoMiter(group, path, j, k, cosA); - else if (cosA > 0.99 || _joinType == JoinType.Square) + else if (_joinType == JoinType.Square) //angle less than 8 degrees or a squared join DoSquare(group, path, j, k); else - DoRound(group, path, j, k, Math.Atan2(sinA, cosA)); + DoRound(group, path, j, k, Math.Atan2(sinA, cosA)); k = j; } @@ -485,29 +517,35 @@ private void OffsetOpenPath(Group group, Path64 path) group.outPath = new Path64(); int highI = path.Count - 1; + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, 0, 0); + // do the line start cap - switch (_endType) - { - case EndType.Butt: + if (Math.Abs(_groupDelta) < Tolerance) + group.outPath.Add(path[0]); + else + switch (_endType) + { + case EndType.Butt: #if USINGZ - group.outPath.Add(new Point64( - path[0].X - _normals[0].x * _group_delta, - path[0].Y - _normals[0].y * _group_delta, - path[0].Z)); + group.outPath.Add(new Point64( + path[0].X - _normals[0].x * _group_delta, + path[0].Y - _normals[0].y * _group_delta, + path[0].Z)); #else - group.outPath.Add(new Point64( - path[0].X - _normals[0].x * _group_delta, - path[0].Y - _normals[0].y * _group_delta)); + group.outPath.Add(new Point64( + path[0].X - _normals[0].x * _groupDelta, + path[0].Y - _normals[0].y * _groupDelta)); #endif - group.outPath.Add(GetPerpendic(path[0], _normals[0])); - break; - case EndType.Round: - DoRound(group, path, 0, 0, Math.PI); - break; - default: - DoSquare(group, path, 0, 0); - break; - } + group.outPath.Add(GetPerpendic(path[0], _normals[0])); + break; + case EndType.Round: + DoRound(group, path, 0, 0, Math.PI); + break; + default: + DoSquare(group, path, 0, 0); + break; + } // offset the left side going forward for (int i = 1, k = 0; i < highI; i++) @@ -518,29 +556,34 @@ private void OffsetOpenPath(Group group, Path64 path) _normals[i] = new PointD(-_normals[i - 1].x, -_normals[i - 1].y); _normals[0] = _normals[highI]; + if (DeltaCallback != null) + _groupDelta = DeltaCallback(path, _normals, highI, highI); // do the line end cap - switch (_endType) - { - case EndType.Butt: + if (Math.Abs(_groupDelta) < Tolerance) + group.outPath.Add(path[highI]); + else + switch (_endType) + { + case EndType.Butt: #if USINGZ - group.outPath.Add(new Point64( - path[highI].X - _normals[highI].x * _group_delta, - path[highI].Y - _normals[highI].y * _group_delta, - path[highI].Z)); + group.outPath.Add(new Point64( + path[highI].X - _normals[highI].x * _group_delta, + path[highI].Y - _normals[highI].y * _group_delta, + path[highI].Z)); #else - group.outPath.Add(new Point64( - path[highI].X - _normals[highI].x * _group_delta, - path[highI].Y - _normals[highI].y * _group_delta)); + group.outPath.Add(new Point64( + path[highI].X - _normals[highI].x * _groupDelta, + path[highI].Y - _normals[highI].y * _groupDelta)); #endif - group.outPath.Add(GetPerpendic(path[highI], _normals[highI])); - break; - case EndType.Round: - DoRound(group, path, highI, highI, Math.PI); - break; - default: - DoSquare(group, path, highI, highI); - break; - } + group.outPath.Add(GetPerpendic(path[highI], _normals[highI])); + break; + case EndType.Round: + DoRound(group, path, highI, highI, Math.PI); + break; + default: + DoSquare(group, path, highI, highI); + break; + } // offset the left side going back for (int i = highI, k = 0; i > 0; i--) @@ -561,32 +604,33 @@ private void DoGroupOffset(Group group) double area = Clipper.Area(group.inPaths[lowestIdx]); //if (area == 0) return; // this is probably unhelpful (#430) group.pathsReversed = (area < 0); - if (group.pathsReversed) _group_delta = -_delta; - else _group_delta = _delta; + if (group.pathsReversed) _groupDelta = -_delta; + else _groupDelta = _delta; } else { group.pathsReversed = false; - _group_delta = Math.Abs(_delta) * 0.5; + _groupDelta = Math.Abs(_delta) * 0.5; } - _abs_group_delta = Math.Abs(_group_delta); + double absDelta = Math.Abs(_groupDelta); _joinType = group.joinType; _endType = group.endType; - // calculate a sensible number of steps (for 360 deg for the given offset - if (group.joinType == JoinType.Round || group.endType == EndType.Round) + if (DeltaCallback == null && + (group.joinType == JoinType.Round || group.endType == EndType.Round)) { + // calculate a sensible number of steps (for 360 deg for the given offset // arcTol - when fArcTolerance 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 = ArcTolerance > 0.01 ? ArcTolerance : - Math.Log10(2 + _abs_group_delta) * InternalClipper.defaultArcTolerance; - double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / _abs_group_delta); + Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance; + double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta); _stepSin = Math.Sin((2 * Math.PI) / stepsPer360); _stepCos = Math.Cos((2 * Math.PI) / stepsPer360); - if (_group_delta < 0.0) _stepSin = -_stepSin; + if (_groupDelta < 0.0) _stepSin = -_stepSin; _stepsPerRad = stepsPer360 / (2 * Math.PI); } @@ -607,7 +651,7 @@ private void DoGroupOffset(Group group) // single vertex so build a circle or square ... if (group.endType == EndType.Round) { - double r = _abs_group_delta; + double r = absDelta; group.outPath = Clipper.Ellipse(path[0], r, r); #if USINGZ group.outPath = InternalClipper.SetZ(group.outPath, path[0].Z); @@ -615,7 +659,7 @@ private void DoGroupOffset(Group group) } else { - int d = (int) Math.Ceiling(_group_delta); + int d = (int) Math.Ceiling(_groupDelta); Rect64 r = new Rect64(path[0].X - d, path[0].Y - d, path[0].X - d, path[0].Y - d); group.outPath = r.AsPath(); diff --git a/Delphi/Clipper2Lib/Clipper.Engine.pas b/Delphi/Clipper2Lib/Clipper.Engine.pas index cd783e1c..620f9fed 100644 --- a/Delphi/Clipper2Lib/Clipper.Engine.pas +++ b/Delphi/Clipper2Lib/Clipper.Engine.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 1 May 2023 * +* Date : 15 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : This is the main polygon clipping module * @@ -269,7 +269,7 @@ TClipperBase = class procedure DoSplitOp(outrec: POutRec; splitOp: POutPt); procedure FixSelfIntersects(outrec: POutRec); function CheckBounds(outrec: POutRec): Boolean; - function CheckSplitOwner(outrec: POutRec): Boolean; + function CheckSplitOwner(outrec: POutRec; const splits: TOutRecArray): Boolean; procedure RecursiveCheckOwners(outrec: POutRec; polytree: TPolyPathBase); protected FUsingPolytree : Boolean; @@ -3022,7 +3022,10 @@ procedure TClipperBase.ProcessHorzJoins; if FUsingPolytree then begin if Path1InsidePath2(or2.pts, or1.pts) then - SetOwner(or2, or1) + begin + SetOwner(or2, or1); + AddSplit(or1, or2); //(#520) + end else if Path1InsidePath2(or1.pts, or2.pts) then SetOwner(or1, or2) else @@ -3694,16 +3697,25 @@ function TClipperBase.CheckBounds(outrec: POutRec): Boolean; end; //------------------------------------------------------------------------------ -function TClipperBase.CheckSplitOwner(outrec: POutRec): Boolean; +function TClipperBase.CheckSplitOwner(outrec: POutRec; const splits: TOutRecArray): Boolean; var i : integer; split : POutrec; begin - for i := 0 to High(outrec.owner.splits) do + Result := false; + for i := 0 to High(splits) do begin - split := GetRealOutRec(outrec.owner.splits[i]); - if Assigned(split) and (split <> outrec) and - (split <> outrec.owner) and CheckBounds(split) and + split := GetRealOutRec(splits[i]); + if not Assigned(split) or + (split = outrec) or (split = outrec.owner) then + Continue + else if Assigned(split.splits) and + CheckSplitOwner(outrec, split.splits) then + begin + Result := True; + Exit; + end + else if CheckBounds(split) and (split.bounds.Contains(outrec.bounds) and Path1InsidePath2(outrec.pts, split.pts)) then begin @@ -3712,7 +3724,6 @@ function TClipperBase.CheckSplitOwner(outrec: POutRec): Boolean; Exit; end; end; - Result := false; end; //------------------------------------------------------------------------------ @@ -3728,7 +3739,7 @@ procedure TClipperBase.RecursiveCheckOwners(outrec: POutRec; polytree: TPolyPath while Assigned(outrec.owner) do begin if Assigned(outrec.owner.splits) and - CheckSplitOwner(outrec) then Break; + CheckSplitOwner(outrec, outrec.owner.splits) then Break; if Assigned(outrec.owner.pts) and CheckBounds(outrec.owner) and (outrec.owner.bounds.Contains(outrec.bounds) and diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index d1d14e06..c2c43c3b 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 8 April 2023 * +* Date : 15 May 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -26,6 +26,10 @@ interface // etJoined : offsets both sides of a path, with joined ends // etPolygon: offsets only one side of a closed path + TDeltaCallback64 = function (const path: TPath64; + const path_norms: TPathD; currIdx, prevIdx: integer): double of object; + + TGroup = class paths : TPaths64; reversed : Boolean; @@ -38,7 +42,6 @@ TClipperOffset = class private fDelta : Double; fGroupDelta : Double; //*0.5 for open paths; *-1.0 for neg areas - fAbsGrpDelta : Double; fMinLenSqrd : double; fJoinType : TJoinType; fEndType : TEndType; @@ -57,6 +60,7 @@ TClipperOffset = class fSolution : TPaths64; fPreserveCollinear : Boolean; fReverseSolution : Boolean; + fDeltaCallback64 : TDeltaCallback64; {$IFDEF USINGZ} fZCallback64 : TZCallback64; procedure AddPoint(x,y: double; z: Int64); overload; @@ -89,6 +93,7 @@ TClipperOffset = class procedure Clear; procedure Execute(delta: Double; out solution: TPaths64); overload; procedure Execute(delta: Double; polytree: TPolyTree64); overload; + procedure Execute(DeltaCallback: TDeltaCallback64; out solution: TPaths64); overload; // MiterLimit: needed for mitered offsets (see offset_triginometry3.svg) property MiterLimit: Double read fMiterLimit write fMiterLimit; @@ -98,6 +103,8 @@ TClipperOffset = class read fPreserveCollinear write fPreserveCollinear; property ReverseSolution: Boolean read fReverseSolution write fReverseSolution; + property DeltaCallback: TDeltaCallback64 read + fDeltaCallback64 write fDeltaCallback64; {$IFDEF USINGZ} property ZCallback: TZCallback64 read fZCallback64 write fZCallback64; {$ENDIF} @@ -293,6 +300,7 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); var i,j, len, lowestIdx: Integer; r, stepsPer360, arcTol, area: Double; + absDelta: double; rec: TRect64; isJoined: Boolean; begin @@ -313,24 +321,25 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); group.reversed := false; fGroupDelta := Abs(fDelta) * 0.5; end; - fAbsGrpDelta := Abs(fGroupDelta); fJoinType := group.joinType; fEndType := group.endType; // calculate a sensible number of steps (for 360 deg for the given offset - if (group.joinType = jtRound) or (group.endType = etRound) then + if (not Assigned(fDeltaCallback64) and + (group.joinType = jtRound) or (group.endType = etRound)) then begin + absDelta := Abs(fGroupDelta); // arcTol - when fArcTolerance 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 if fArcTolerance > 0.01 then - arcTol := Min(fAbsGrpDelta, fArcTolerance) else - arcTol := Log10(2 + fAbsGrpDelta) * 0.25; // empirically derived + arcTol := Min(absDelta, fArcTolerance) else + arcTol := Log10(2 + absDelta) * 0.25; // empirically derived //http://www.angusj.com/clipper2/Docs/Trigonometry.htm - stepsPer360 := Pi / ArcCos(1 - arcTol / fAbsGrpDelta); - if (stepsPer360 > fAbsGrpDelta * Pi) then - stepsPer360 := fAbsGrpDelta * Pi; // avoid excessive precision + stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); + if (stepsPer360 > absDelta * Pi) then + stepsPer360 := absDelta * Pi; // avoid excessive precision fStepSin := sin(TwoPi/stepsPer360); fStepCos := cos(TwoPi/stepsPer360); if (fGroupDelta < 0.0) then fStepSin := -fStepSin; @@ -354,9 +363,10 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); if len = 1 then begin if fGroupDelta < 1 then Continue; + absDelta := Abs(fGroupDelta); if (group.endType = etRound) then begin - r := fAbsGrpDelta; + r := absDelta; with fInPath[0] do begin fOutPath := Path64(Ellipse(RectD(X-r, Y-r, X+r, Y+r))); @@ -367,7 +377,7 @@ procedure TClipperOffset.DoGroupOffset(group: TGroup); end; end else begin - j := Round(fGroupDelta); + j := Round(absDelta); with fInPath[0] do begin rec := Rect64(X -j, Y -j, X+j, Y+j); @@ -450,7 +460,14 @@ procedure TClipperOffset.OffsetOpenPath; begin highI := high(fInPath); - // do the line start cap + if Assigned(fDeltaCallback64) then + fGroupDelta := fDeltaCallback64(fInPath, fNorms, 0, 0); + + // do the line start cap + if Abs(fGroupDelta) < Tolerance then + begin + AddPoint(fInPath[0]); + end else case fEndType of etButt: begin @@ -484,6 +501,13 @@ procedure TClipperOffset.OffsetOpenPath; fNorms[0] := fNorms[highI]; // do the line end cap + + if Assigned(fDeltaCallback64) then + fGroupDelta := fDeltaCallback64(fInPath, fNorms, highI, highI); + if Abs(fGroupDelta) < Tolerance then + begin + AddPoint(fInPath[highI]); + end else case fEndType of etButt: begin @@ -584,6 +608,13 @@ procedure TClipperOffset.Execute(delta: Double; out solution: TPaths64); end; //------------------------------------------------------------------------------ +procedure TClipperOffset.Execute(DeltaCallback: TDeltaCallback64; out solution: TPaths64); +begin + fDeltaCallback64 := DeltaCallback; + Execute(1.0, solution); +end; +//------------------------------------------------------------------------------ + procedure TClipperOffset.Execute(delta: Double; polytree: TPolyTree64); var dummy: TPaths64; @@ -691,6 +722,7 @@ function ReflectPoint(const pt, pivot: TPointD): TPointD; procedure TClipperOffset.DoSquare(j, k: Integer); var vec, pt1,pt2,pt3,pt4, pt,ptQ : TPointD; + absDelta: double; begin if k = j then begin @@ -705,9 +737,10 @@ procedure TClipperOffset.DoSquare(j, k: Integer); PointD(fNorms[j].Y, -fNorms[j].X)); end; + absDelta := Abs(fGroupDelta); // now offset the original vertex delta units along unit vector ptQ := PointD(fInPath[j]); - ptQ := TranslatePoint(ptQ, fAbsGrpDelta * vec.X, fAbsGrpDelta * vec.Y); + ptQ := TranslatePoint(ptQ, absDelta * vec.X, absDelta * vec.Y); // get perpendicular vertices pt1 := TranslatePoint(ptQ, fGroupDelta * vec.Y, fGroupDelta * -vec.X); @@ -767,9 +800,29 @@ procedure TClipperOffset.DoMiter(j, k: Integer; cosA: Double); procedure TClipperOffset.DoRound(j, k: Integer; angle: double); var i, steps: Integer; + absDelta, arcTol, stepsPer360: double; pt: TPoint64; offDist: TPointD; begin + + if Assigned(fDeltaCallback64) then + begin + // when fDeltaCallback64 is assigned, fGroupDelta won't be constant, + // so we'll need to do the following calculations for *every* vertex. + absDelta := Abs(fGroupDelta); + if fArcTolerance > 0.01 then + arcTol := Min(absDelta, fArcTolerance) else + arcTol := Log10(2 + absDelta) * 0.25; // empirically derived + //http://www.angusj.com/clipper2/Docs/Trigonometry.htm + stepsPer360 := Pi / ArcCos(1 - arcTol / absDelta); + if (stepsPer360 > absDelta * Pi) then + stepsPer360 := absDelta * Pi; // avoid excessive precision + fStepSin := sin(TwoPi/stepsPer360); + fStepCos := cos(TwoPi/stepsPer360); + if (fGroupDelta < 0.0) then fStepSin := -fStepSin; + fStepsPerRad := stepsPer360 / TwoPi; + end; + // nb: angles may be negative but this will always be a convex join pt := fInPath[j]; offDist := ScalePoint(fNorms[k], fGroupDelta); @@ -814,6 +867,14 @@ procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer); if (sinA > 1.0) then sinA := 1.0 else if (sinA < -1.0) then sinA := -1.0; + if Assigned(fDeltaCallback64) then + fGroupDelta := fDeltaCallback64(fInPath, fNorms, j, k); + + if Abs(fGroupDelta) <= Tolerance then + begin + AddPoint(fInPath[j]); + Exit; + end; if (cosA > -0.99) and (sinA * fGroupDelta < 0) then begin