diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h index 9601447f..383ae80f 100644 --- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 21 February 2023 * +* Date : 22 March 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Core Clipper Library structures and functions * @@ -337,69 +337,44 @@ namespace Clipper2Lib static const Rect64 MaxInvalidRect64 = Rect64( INT64_MAX, INT64_MAX, INT64_MIN, INT64_MIN); - static const RectD MaxInvalidRectD = RectD( MAX_DBL, MAX_DBL, -MAX_DBL, -MAX_DBL); - inline Rect64 GetBounds(const Path64& path) - { - Rect64 rec = MaxInvalidRect64; - for (const Point64& pt : path) - { - if (pt.x < rec.left) rec.left = pt.x; - if (pt.x > rec.right) rec.right = pt.x; - if (pt.y < rec.top) rec.top = pt.y; - if (pt.y > rec.bottom) rec.bottom = pt.y; - } - if (rec.left == INT64_MAX) return Rect64(); - return rec; - } - - inline Rect64 GetBounds(const Paths64& paths) - { - Rect64 rec = MaxInvalidRect64; - for (const Path64& path : paths) - for (const Point64& pt : path) - { - if (pt.x < rec.left) rec.left = pt.x; - if (pt.x > rec.right) rec.right = pt.x; - if (pt.y < rec.top) rec.top = pt.y; - if (pt.y > rec.bottom) rec.bottom = pt.y; - } - if (rec.left == INT64_MAX) return Rect64(); - return rec; - } - - inline RectD GetBounds(const PathD& path) + template + Rect GetBounds(const Path& path) { - RectD rec = MaxInvalidRectD; - for (const PointD& pt : path) + auto xmin = std::numeric_limits::max(); + auto ymin = std::numeric_limits::max(); + auto xmax = std::numeric_limits::lowest(); + auto ymax = std::numeric_limits::lowest(); + for (const auto& p : path) { - if (pt.x < rec.left) rec.left = pt.x; - if (pt.x > rec.right) rec.right = pt.x; - if (pt.y < rec.top) rec.top = pt.y; - if (pt.y > rec.bottom) rec.bottom = pt.y; + if (p.x < xmin) xmin = p.x; + if (p.x > xmax) xmax = p.x; + if (p.y < ymin) ymin = p.y; + if (p.y > ymax) ymax = p.y; } - if (rec.left == MAX_DBL) return RectD(); - return rec; + return Rect(xmin, ymin, xmax, ymax); } - inline RectD GetBounds(const PathsD& paths) - { - RectD rec = MaxInvalidRectD; - for (const PathD& path : paths) - for (const PointD& pt : path) + template + Rect GetBounds(const Paths& paths) + { + auto xmin = std::numeric_limits::max(); + auto ymin = std::numeric_limits::max(); + auto xmax = std::numeric_limits::lowest(); + auto ymax = std::numeric_limits::lowest(); + for (const Path& path : paths) + for (const Point& p : path) { - if (pt.x < rec.left) rec.left = pt.x; - if (pt.x > rec.right) rec.right = pt.x; - if (pt.y < rec.top) rec.top = pt.y; - if (pt.y > rec.bottom) rec.bottom = pt.y; + if (p.x < xmin) xmin = p.x; + if (p.x > xmax) xmax = p.x; + if (p.y < ymin) ymin = p.y; + if (p.y > ymax) ymax = p.y; } - if (rec.left == MAX_DBL) return RectD(); - return rec; + return Rect(xmin, ymin, xmax, ymax); } - template std::ostream& operator << (std::ostream& outstream, const Path& path) { diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.offset.h b/CPP/Clipper2Lib/include/clipper2/clipper.offset.h index 7aa201d4..f5d47e07 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 : 19 March 2023 * +* Date : 22 March 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -59,7 +59,7 @@ class ClipperOffset { bool preserve_collinear_ = false; bool reverse_solution_ = false; -#if USINGZ +#ifdef USINGZ ZCallback64 zCallback64_ = nullptr; #endif @@ -105,7 +105,7 @@ class ClipperOffset { bool ReverseSolution() const { return reverse_solution_; } void ReverseSolution(bool reverse_solution) {reverse_solution_ = reverse_solution;} -#if USINGZ +#ifdef USINGZ void SetZCallback(ZCallback64 cb) { zCallback64_ = cb; } #endif }; diff --git a/CPP/Clipper2Lib/src/clipper.offset.cpp b/CPP/Clipper2Lib/src/clipper.offset.cpp index a5f35717..78cd8237 100644 --- a/CPP/Clipper2Lib/src/clipper.offset.cpp +++ b/CPP/Clipper2Lib/src/clipper.offset.cpp @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 March 2023 * +* Date : 22 March 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -98,7 +98,7 @@ inline bool IsClosedPath(EndType et) inline Point64 GetPerpendic(const Point64& pt, const PointD& norm, double delta) { -#if USINGZ +#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); @@ -107,7 +107,7 @@ inline Point64 GetPerpendic(const Point64& pt, const PointD& norm, double delta) inline PointD GetPerpendicD(const Point64& pt, const PointD& norm, double delta) { -#if USINGZ +#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); @@ -120,7 +120,7 @@ inline void NegatePath(PathD& path) { pt.x = -pt.x; pt.y = -pt.y; -#if USINGZ +#ifdef USINGZ pt.z = pt.z; #endif } @@ -156,7 +156,7 @@ void ClipperOffset::BuildNormals(const Path64& path) inline PointD TranslatePoint(const PointD& pt, double dx, double dy) { -#if USINGZ +#ifdef USINGZ return PointD(pt.x + dx, pt.y + dy, pt.z); #else return PointD(pt.x + dx, pt.y + dy); @@ -165,7 +165,7 @@ inline PointD TranslatePoint(const PointD& pt, double dx, double dy) inline PointD ReflectPoint(const PointD& pt, const PointD& pivot) { -#if USINGZ +#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)); @@ -223,7 +223,7 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t { PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_); PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); -#if USINGZ +#ifdef USINGZ pt.z = ptQ.z; #endif //get the second intersect point through reflecion @@ -234,7 +234,7 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t { PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_); PointD pt = IntersectPoint(pt1, pt2, pt3, pt4); -#if USINGZ +#ifdef USINGZ pt.z = ptQ.z; #endif group.path.push_back(Point64(pt)); @@ -246,7 +246,7 @@ void ClipperOffset::DoSquare(Group& group, const Path64& path, size_t j, size_t void ClipperOffset::DoMiter(Group& group, const Path64& path, size_t j, size_t k, double cos_a) { double q = group_delta_ / (cos_a + 1); -#if USINGZ +#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, @@ -264,20 +264,19 @@ void ClipperOffset::DoRound(Group& group, const Path64& path, size_t j, size_t k PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_); if (j == k) offsetVec.Negate(); -#if USINGZ +#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 = std::max(2, static_cast( - std::ceil(steps_per_rad_ * std::abs(angle)))); // #448 + int steps = static_cast(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_); -#if USINGZ +#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)); @@ -364,7 +363,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) switch (end_type_) { case EndType::Butt: -#if USINGZ +#ifdef USINGZ group.path.push_back(Point64( path[0].x - norms[0].x * group_delta_, path[0].y - norms[0].y * group_delta_, @@ -399,7 +398,7 @@ void ClipperOffset::OffsetOpenPath(Group& group, Path64& path) switch (end_type_) { case EndType::Butt: -#if USINGZ +#ifdef USINGZ group.path.push_back(Point64( path[highI].x - norms[highI].x * group_delta_, path[highI].y - norms[highI].y * group_delta_, @@ -499,7 +498,7 @@ void ClipperOffset::DoGroupOffset(Group& group) { double radius = abs_group_delta_; group.path = Ellipse(path[0], radius, radius); -#if USINGZ +#ifdef USINGZ for (auto& p : group.path) p.z = path[0].z; #endif } @@ -508,7 +507,7 @@ void ClipperOffset::DoGroupOffset(Group& group) 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(); -#if USINGZ +#ifdef USINGZ for (auto& p : group.path) p.z = path[0].z; #endif } @@ -573,12 +572,13 @@ void ClipperOffset::Execute(double delta, Paths64& paths) 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; -#if USINGZ +#ifdef USINGZ if (zCallback64_) { c.SetZCallback(zCallback64_); } @@ -603,7 +603,7 @@ void ClipperOffset::Execute(double delta, PolyTree64& polytree) c.PreserveCollinear = false; //the solution should retain the orientation of the input c.ReverseSolution = reverse_solution_ != groups_[0].is_reversed; -#if USINGZ +#ifdef USINGZ if (zCallback64_) { c.SetZCallback(zCallback64_); } diff --git a/CPP/Tests/TestOffsets.cpp b/CPP/Tests/TestOffsets.cpp index 7e73c024..e197d153 100644 --- a/CPP/Tests/TestOffsets.cpp +++ b/CPP/Tests/TestOffsets.cpp @@ -5,36 +5,80 @@ TEST(Clipper2Tests, TestOffsets) { - std::ifstream ifs("Offsets.txt"); + std::ifstream ifs("Offsets.txt"); - for (int test_number = 1; test_number <= 2; ++test_number) + for (int test_number = 1; test_number <= 2; ++test_number) + { + Clipper2Lib::ClipperOffset co; + + Clipper2Lib::Paths64 subject, subject_open, clip; + Clipper2Lib::Paths64 solution, solution_open; + Clipper2Lib::ClipType ct = Clipper2Lib::ClipType::None; + Clipper2Lib::FillRule fr = Clipper2Lib::FillRule::NonZero; + int64_t stored_area = 0, stored_count = 0; + + ASSERT_TRUE(LoadTestNum(ifs, test_number, subject, subject_open, clip, stored_area, stored_count, ct, fr)); + + co.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); + Clipper2Lib::Paths64 outputs; + co.Execute(1, outputs); + + // is the sum total area of the solution is positive + const auto outer_is_positive = Clipper2Lib::Area(outputs) > 0; + + // there should be exactly one exterior path + const auto is_positive_func = Clipper2Lib::IsPositive; + const auto is_positive_count = std::count_if( + outputs.begin(), outputs.end(), is_positive_func); + const auto is_negative_count = + outputs.size() - is_positive_count; + if (outer_is_positive) + EXPECT_EQ(is_positive_count, 1); + else + EXPECT_EQ(is_negative_count, 1); + } +} + +Clipper2Lib::Point64 MidPoint(const Clipper2Lib::Point64& p1, const Clipper2Lib::Point64& p2) +{ + Clipper2Lib::Point64 result; + result.x = (p1.x + p2.x) / 2; + result.y = (p1.y + p2.y) / 2; + return result; +} + +TEST(Clipper2Tests, TestOffsets2) { // see #448 & #456 + + double scale = 10, delta = 10 * scale, arc_tol = 0.25 * scale; + + Clipper2Lib::Paths64 subject, solution; + Clipper2Lib::ClipperOffset c; + subject.push_back(Clipper2Lib::MakePath({ 50,50, 100,50, 100,150, 50,150, 0,100 })); + + int err; + subject = Clipper2Lib::ScalePaths(subject, scale, err); + + c.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); + c.ArcTolerance(arc_tol); + c.Execute(delta, solution); + + double min_dist = delta * 2, max_dist = 0; + for (auto subjPt : subject[0]) + { + Clipper2Lib::Point64 prevPt = solution[0][solution[0].size() - 1]; + for (auto pt : solution[0]) { - Clipper2Lib::ClipperOffset co; - - Clipper2Lib::Paths64 subject, subject_open, clip; - Clipper2Lib::Paths64 solution, solution_open; - Clipper2Lib::ClipType ct = Clipper2Lib::ClipType::None; - Clipper2Lib::FillRule fr = Clipper2Lib::FillRule::NonZero; - int64_t stored_area = 0, stored_count = 0; - - ASSERT_TRUE(LoadTestNum(ifs, test_number, subject, subject_open, clip, stored_area, stored_count, ct, fr)); - - co.AddPaths(subject, Clipper2Lib::JoinType::Round, Clipper2Lib::EndType::Polygon); - Clipper2Lib::Paths64 outputs; - co.Execute(1, outputs); - - // is the sum total area of the solution is positive - const auto outer_is_positive = Clipper2Lib::Area(outputs) > 0; - - // there should be exactly one exterior path - const auto is_positive_func = Clipper2Lib::IsPositive; - const auto is_positive_count = std::count_if( - outputs.begin(), outputs.end(), is_positive_func); - const auto is_negative_count = - outputs.size() - is_positive_count; - if (outer_is_positive) - EXPECT_EQ(is_positive_count, 1); - else - EXPECT_EQ(is_negative_count, 1); + Clipper2Lib::Point64 mp = MidPoint(prevPt, pt); + double d = Clipper2Lib::Distance(mp, subjPt); + if (d < delta * 2) + { + if (d < min_dist) min_dist = d; + if (d > max_dist) max_dist = d; + } + prevPt = pt; } + } + + EXPECT_GE(min_dist + 1, delta - arc_tol); // +1 for rounding errors + EXPECT_LE(solution[0].size(), 21); } \ No newline at end of file diff --git a/CSharp/Clipper2Lib/Clipper.Offset.cs b/CSharp/Clipper2Lib/Clipper.Offset.cs index 4ebcd166..a58b668a 100644 --- a/CSharp/Clipper2Lib/Clipper.Offset.cs +++ b/CSharp/Clipper2Lib/Clipper.Offset.cs @@ -1,6 +1,6 @@ /******************************************************************************* * Author : Angus Johnson * -* Date : 19 March 2023 * +* Date : 22 March 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -392,7 +392,7 @@ private void DoRound(Group group, Path64 path, int j, int k, double angle) #endif if (angle > -Math.PI + 0.01) // avoid 180deg concave { - int steps = Math.Max(2, (int) Math.Ceiling(_stepsPerRad * Math.Abs(angle))); + int steps = (int) Math.Ceiling(_stepsPerRad * Math.Abs(angle)); for (int i = 1; i < steps; i++) // ie 1 less than steps { offsetVec = new PointD(offsetVec.x * _stepCos - _stepSin * offsetVec.y, diff --git a/Delphi/Clipper2Lib/Clipper.Offset.pas b/Delphi/Clipper2Lib/Clipper.Offset.pas index 0e11459c..446b424c 100644 --- a/Delphi/Clipper2Lib/Clipper.Offset.pas +++ b/Delphi/Clipper2Lib/Clipper.Offset.pas @@ -2,7 +2,7 @@ (******************************************************************************* * Author : Angus Johnson * -* Date : 17 March 2023 * +* Date : 22 March 2023 * * Website : http://www.angusj.com * * Copyright : Angus Johnson 2010-2023 * * Purpose : Path Offset (Inflate/Shrink) * @@ -779,7 +779,7 @@ procedure TClipperOffset.DoRound(j, k: Integer; angle: double); {$ELSE} AddPoint(pt.X + offDist.X, pt.Y + offDist.Y); {$ENDIF} - steps := Max(2, Ceil(fStepsPerRad * abs(angle))); + steps := Ceil(fStepsPerRad * abs(angle)); for i := 2 to steps do begin offDist := PointD(offDist.X * fStepCos - fStepSin * offDist.Y,