Skip to content

Commit

Permalink
Additional minor bugfix to ClipperOffset (#724 & Disc.#726)
Browse files Browse the repository at this point in the history
  • Loading branch information
AngusJohnson committed Nov 26, 2023
1 parent 906bc1d commit 88d0bc7
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 126 deletions.
21 changes: 6 additions & 15 deletions CPP/Clipper2Lib/src/clipper.offset.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 25 November 2023 *
* Date : 26 November 2023 *
* Website : http://www.angusj.com *
* Copyright : Angus Johnson 2010-2023 *
* Purpose : Path Offset (Inflate/Shrink) *
Expand Down Expand Up @@ -409,29 +409,20 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size
path_out.push_back(path[j]); // (#405)
path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_));
}
else if (cos_a > 0.999) // almost straight - less than 2.5 degree (#424, #526)
else if (cos_a > 0.999 && join_type_ != JoinType::Round)
{
// with ::Round, preserving near exact delta is more important than simpler paths
// See also Issues #424, #526 #482
if (join_type_ == JoinType::Round)
{
path_out.push_back(GetPerpendic(path[j], norms[k], group_delta_));
path_out.push_back(GetPerpendic(path[j], norms[j], group_delta_));
}
else
DoMiter(path, j, k, cos_a);
// almost straight - less than 2.5 degree (#424, #482, #526 & #724)
DoMiter(path, j, k, cos_a);
}
else if (join_type_ == JoinType::Miter)
{
// miter unless the angle is so acute the miter would exceeds ML
// miter unless the angle is sufficiently acute to exceed ML
if (cos_a > temp_lim_ - 1) DoMiter(path, j, k, cos_a);
else DoSquare(path, j, k);
}
else if (join_type_ == JoinType::Round)
DoRound(path, j, k, std::atan2(sin_a, cos_a));
else if (/*cos_a > 0.99 || */ join_type_ == JoinType::Bevel)
// cos_a > 0.99 here improves performance with extremely minor reduction in accuracy
// acos(0.99) == 8.1 deg. still a small angle but not as small as cos_a > 0.999 (see above)
else if ( join_type_ == JoinType::Bevel)
DoBevel(path, j, k);
else
DoSquare(path, j, k);
Expand Down
175 changes: 93 additions & 82 deletions CPP/Tests/TestOffsets.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include <gtest/gtest.h>
#include "clipper2/clipper.offset.h"
#include "ClipFileLoad.h"
#include <algorithm>
//#include "clipper.svg.utils.h"

using namespace Clipper2Lib;
TEST(Clipper2Tests, TestOffsets) {
Expand Down Expand Up @@ -42,7 +42,7 @@ TEST(Clipper2Tests, TestOffsets) {
}


Point64 MidPoint(const Point64& p1, const Point64& p2)
static Point64 MidPoint(const Point64& p1, const Point64& p2)
{
Point64 result;
result.x = (p1.x + p2.x) / 2;
Expand Down Expand Up @@ -424,66 +424,89 @@ TEST(Clipper2Tests, TestOffsets7) // (#593 & #715)
EXPECT_EQ(solution.size(), 0);
}


struct OffsetQual
{
double smallestDist;
double largestDist;
size_t idxSmallestIn; //index to first segment pt
size_t idxSmallestOut;
size_t idxLargestIn; //index to first segment pt
size_t idxLargestOut;
double standardDev;
PointD smallestInSub; // smallestInSub & smallestInSol are the points in subject and solution
PointD smallestInSol; // that define the place that most falls short of the expected offset
PointD largestInSub; // largestInSub & largestInSol are the points in subject and solution
PointD largestInSol; // that define the place that most exceeds the expected offset
};


template<typename T>
inline PointD GetClosestPointOnSegment(const PointD& offPt,
const Point<T>& seg1, const Point<T>& seg2)
{
if (seg1.x == seg2.x && seg1.y == seg2.y) return PointD(seg1);
double dx = static_cast<double>(seg2.x - seg1.x);
double dy = static_cast<double>(seg2.y - seg1.y);
double q = (
(offPt.x - static_cast<double>(seg1.x)) * dx +
(offPt.y - static_cast<double>(seg1.y)) * dy) /
(Sqr(dx) + Sqr(dy));
q = (q < 0) ? 0 : (q > 1) ? 1 : q;
return PointD(
static_cast<double>(seg1.x) + (q * dx),
static_cast<double>(seg1.y) + (q * dy));
}


template<typename T>
static OffsetQual GetOffsetQuality(const Path<T>& input, const Path<T>& output, const double desiredDist)
static OffsetQual GetOffsetQuality(const Path<T>& subject, const Path<T>& solution, const double delta)
{
if (!input.size() || !output.size()) return OffsetQual();
if (!subject.size() || !solution.size()) return OffsetQual();

double desiredDistSqr = desiredDist * desiredDist;
double desiredDistSqr = delta * delta;
double smallestSqr = desiredDistSqr, largestSqr = desiredDistSqr;
double deviationsSqr = 0;
size_t smallestInIdx = 0, largestInIdx = 0, smallestOutIdx = 0, largestOutIdx = 0;
size_t outIdx = 0;
for (const Point<T>& outPt : output)
{
double closestDistSqr = std::numeric_limits<double>::infinity();
OffsetQual oq;

size_t cpi = 0; //closest point index
Point<T> in_prev = input[input.size() - 1];
for (size_t i = 0; i < input.size(); ++i)
const size_t subVertexCount = 4; // 1 .. 100 :)
const double subVertexFrac = 1.0 / subVertexCount;
Point<T> outPrev = solution[solution.size() - 1];
for (const Point<T>& outPt : solution)
{
for (size_t i = 0; i < subVertexCount; ++i)
{
Point<T> cp = Clipper2Lib::GetClosestPointOnSegment(outPt, input[i], in_prev);
in_prev = input[i];
const double sqrDist = Clipper2Lib::DistanceSqr(cp, outPt);
if (sqrDist < closestDistSqr) { closestDistSqr = sqrDist; cpi = i; };
}
// divide each edge in solution into series of sub-vertices (solPt),
PointD solPt = PointD(
static_cast<double>(outPrev.x) + static_cast<double>(outPt.x - outPrev.x) * subVertexFrac * i,
static_cast<double>(outPrev.y) + static_cast<double>(outPt.y - outPrev.y) * subVertexFrac * i);

// now find the closest point in subject to each of these solPt.
PointD closestToSolPt;
double closestDistSqr = std::numeric_limits<double>::infinity();
Point<T> subPrev = subject[subject.size() - 1];
for (size_t i = 0; i < subject.size(); ++i)
{
PointD closestPt = ::GetClosestPointOnSegment(solPt, subject[i], subPrev);
subPrev = subject[i];
const double sqrDist = DistanceSqr(closestPt, solPt);
if (sqrDist < closestDistSqr) {
closestDistSqr = sqrDist;
closestToSolPt = closestPt;
};
}

if (closestDistSqr < smallestSqr)
{
smallestSqr = closestDistSqr;
smallestInIdx = cpi;
smallestOutIdx = outIdx;
}
if (closestDistSqr > largestSqr)
{
largestSqr = closestDistSqr;
largestInIdx = cpi;
largestOutIdx = outIdx;
// we've now found solPt's closest pt in subject (closestToSolPt).
// but how does the distance between these 2 points compare with delta
// ideally - Distance(closestToSolPt, solPt) == delta;

// see how this distance compares with every other solPt
if (closestDistSqr < smallestSqr) {
smallestSqr = closestDistSqr;
oq.smallestInSub = closestToSolPt;
oq.smallestInSol = solPt;
}
if (closestDistSqr > largestSqr) {
largestSqr = closestDistSqr;
oq.largestInSub = closestToSolPt;
oq.largestInSol = solPt;
}
}
// we now have smallestDistSqr between outPt and the input path
double offset_qual = std::sqrt(closestDistSqr) - desiredDist;
deviationsSqr += offset_qual * offset_qual;
++outIdx;
outPrev = outPt;
}
OffsetQual oq{};
oq.smallestDist = std::sqrt(smallestSqr);
oq.largestDist = std::sqrt(largestSqr);
oq.idxSmallestIn = smallestInIdx == 0 ? input.size() - 1 : smallestInIdx - 1;
oq.idxLargestIn = largestInIdx == 0 ? input.size() - 1 : largestInIdx - 1;
oq.idxSmallestOut = smallestOutIdx;
oq.idxLargestOut = largestOutIdx;
oq.standardDev = std::sqrt(deviationsSqr / input.size());
return oq;
}

Expand Down Expand Up @@ -571,40 +594,28 @@ TEST(Clipper2Tests, TestOffsets8) // (#724)
106736189, -44845834, 99439432, -47868251, 91759700, -49711991
}) };

Paths64 solution;
ClipperOffset c;
double offset = -50329979.0, arc_tol = 2500.0;

c.AddPaths(subject, JoinType::Round, EndType::Polygon);
c.ArcTolerance(arc_tol);
c.MiterLimit(2.0);
c.Execute(offset, solution);
double offset = -50329979.277800001, arc_tol = 5000;

Paths64 solution = InflatePaths(subject, offset, JoinType::Round, EndType::Polygon, 2, arc_tol);
OffsetQual oq = GetOffsetQuality(subject[0], solution[0], offset);
double smallestDist = Distance(oq.smallestInSub, oq.smallestInSol);
double largestDist = Distance(oq.largestInSub, oq.largestInSol);
const double rounding_tolerance = 1.0;
offset = std::abs(offset);
OffsetQual offset_qual = GetOffsetQuality(subject[0], solution[0], std::abs(offset));
/*
std::cout.imbue(std::locale(""));
std::cout << std::setprecision(2) << std::fixed << std::setfill(' ');
std::cout << "Max dist. short of specified offset : "
<< std::setw(12) << (offset - offset_qual.smallestDist) << std::endl;
std::cout << "Max dist. beyond specified offset : "
<< std::setw(12) << (offset_qual.largestDist - offset) << std::endl;
std::cout.imbue(std::locale("C")); //remove thousands separator
Point64 inPt = subject[0][offset_qual.idxSmallestIn];
Point64 outPt = solution[0][offset_qual.idxSmallestOut];
std::cout << "Distance less than delta" << std::endl;
std::cout << "Point in subject : " << inPt << std::endl;
std::cout << "Point in result : " << outPt << std::endl;
inPt = subject[0][offset_qual.idxLargestIn];
outPt = solution[0][offset_qual.idxLargestOut];
std::cout << "Distance greater than delta" << std::endl;
std::cout << "Point in subject : " << inPt << std::endl;
std::cout << "Point in result : " << outPt << std::endl;
std::cout << "StdDev of dist from specified offset : "
<< std::setw(12) << offset_qual.standardDev << std::endl << std::endl;
*/
EXPECT_LE(std::abs(offset) - offset_qual.smallestDist , arc_tol);

//std::cout << std::setprecision(0) << std::fixed;
//std::cout << "Expected delta : " << offset << std::endl;
//std::cout << "Smallest delta : " << smallestDist << " (" << smallestDist - offset << ")" << std::endl;
//std::cout << "Largest delta : " << largestDist << " (" << largestDist - offset << ")" << std::endl;
//std::cout << "Coords of smallest delta : " << oq.smallestInSub << " and " << oq.smallestInSol << std::endl;
//std::cout << "Coords of largest delta : " << oq.largestInSub << " and " << oq.largestInSol << std::endl;
//std::cout << std::endl;
//SvgWriter svg;
//SvgAddSubject(svg, subject, FillRule::NonZero);
//SvgAddSolution(svg, solution, FillRule::NonZero, false);
//std::string filename = "offset_test.svg";
//SvgSaveToFile(svg, filename, 800, 600, 10);

EXPECT_LE(offset - smallestDist - rounding_tolerance, arc_tol);
EXPECT_LE(largestDist - offset - rounding_tolerance, arc_tol);
}
21 changes: 6 additions & 15 deletions CSharp/Clipper2Lib/Clipper.Offset.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 25 November 2023 *
* Date : 26 November 2023 *
* Website : http://www.angusj.com *
* Copyright : Angus Johnson 2010-2023 *
* Purpose : Path Offset (Inflate/Shrink) *
Expand Down Expand Up @@ -586,29 +586,20 @@ private void OffsetPoint(Group group, Path64 path, int j, ref int k)
pathOut.Add(path[j]); // (#405)
pathOut.Add(GetPerpendic(path[j], _normals[j]));
}
else if (cosA > 0.999)
else if ((cosA > 0.999) && (_joinType != JoinType.Round))
{
// with ::Round, preserving near exact delta is more important than simpler paths
// See also Issues #424, #526 #482
if (_joinType == JoinType.Round)
{
pathOut.Add(GetPerpendic(path[j], _normals[k]));
pathOut.Add(GetPerpendic(path[j], _normals[j]));
}
else
DoMiter(group, path, j, k, cosA);
// almost straight - less than 2.5 degree (#424, #482, #526 & #724)
DoMiter(group, path, j, k, cosA);
}
else if (_joinType == JoinType.Miter)
{
// miter unless the angle is so acute the miter would exceeds ML
// miter unless the angle is sufficiently acute to exceed ML
if (cosA > _mitLimSqr - 1) DoMiter(group, path, j, k, cosA);
else DoSquare(path, j, k);
}
else if (_joinType == JoinType.Round)
DoRound(path, j, k, Math.Atan2(sinA, cosA));
else if (/*cosA > 0.99 ||*/ _joinType == JoinType.Bevel)
// cos_a > 0.99 here improves performance with extremely minor reduction in accuracy
// acos(0.99) == 8.1 deg. still a small angle but not as small as cos_a > 0.999 (see above)
else if (_joinType == JoinType.Bevel)
DoBevel(path, j, k);
else
DoSquare(path, j, k);
Expand Down
20 changes: 6 additions & 14 deletions Delphi/Clipper2Lib/Clipper.Offset.pas
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

(*******************************************************************************
* Author : Angus Johnson *
* Date : 25 November 2023 *
* Date : 26 November 2023 *
* Website : http://www.angusj.com *
* Copyright : Angus Johnson 2010-2023 *
* Purpose : Path Offset (Inflate/Shrink) *
Expand Down Expand Up @@ -1027,28 +1027,20 @@ procedure TClipperOffset.OffsetPoint(j: Integer; var k: integer);
AddPoint(fInPath[j]); // (#405)
AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta));
end
else if (cosA > 0.999) then
else if (cosA > 0.999) and (fJoinType <> jtRound) then
begin
// with ::Round, preserving near exact delta is more important
// than simpler paths (See also Issues #424, #526 #482)
if (fJoinType = jtRound) then
begin
AddPoint(GetPerpendic(fInPath[j], fNorms[k], fGroupDelta));
AddPoint(GetPerpendic(fInPath[j], fNorms[j], fGroupDelta));
end else
DoMiter(j, k, cosA)
// almost straight - less than 2.5 degree (#424, #482, #526 & #724)
DoMiter(j, k, cosA);
end
else if (fJoinType = jtMiter) then
begin
// miter unless the angle is so acute the miter would exceeds ML
// miter unless the angle is sufficiently acute to exceed ML
if (cosA > fTmpLimit -1) then DoMiter(j, k, cosA)
else DoSquare(j, k);
end
else if (fJoinType = jtRound) then
DoRound(j, k, ArcTan2(sinA, cosA))
else if {(cosA > 0.99) or} (fJoinType = jtBevel) then
// cos_a > 0.99 here improves performance with only very minor reductions in
// accuracy. acos(0.99) < ~8.1 deg. Not quite as small as cos_a > 0.999 above.
else if (fJoinType = jtBevel) then
DoBevel(j, k)
else
DoSquare(j, k);
Expand Down

0 comments on commit 88d0bc7

Please sign in to comment.