Skip to content

Commit 4ab4da0

Browse files
committed
Clipper.Offset - changed arc_tolerance default
1 parent 7598be9 commit 4ab4da0

7 files changed

Lines changed: 153 additions & 111 deletions

File tree

CPP/Clipper2Lib/include/clipper2/clipper.offset.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*******************************************************************************
22
* Author : Angus Johnson *
3-
* Date : 24 March 2024 *
3+
* Date : 22 January 2025 *
44
* Website : http://www.angusj.com *
5-
* Copyright : Angus Johnson 2010-2024 *
5+
* Copyright : Angus Johnson 2010-2025 *
66
* Purpose : Path Offset (Inflate/Shrink) *
77
* License : http://www.boost.org/LICENSE_1_0.txt *
88
*******************************************************************************/
@@ -96,7 +96,7 @@ class ClipperOffset {
9696
void AddPaths(const Paths64& paths, JoinType jt_, EndType et_);
9797
void Clear() { groups_.clear(); norms.clear(); };
9898

99-
void Execute(double delta, Paths64& paths);
99+
void Execute(double delta, Paths64& sols_64);
100100
void Execute(double delta, PolyTree64& polytree);
101101
void Execute(DeltaCallback64 delta_cb, Paths64& paths);
102102

CPP/Clipper2Lib/src/clipper.offset.cpp

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*******************************************************************************
22
* Author : Angus Johnson *
3-
* Date : 22 November 2024 *
3+
* Date : 22 January 2025 *
44
* Website : http://www.angusj.com *
5-
* Copyright : Angus Johnson 2010-2024 *
5+
* Copyright : Angus Johnson 2010-2025 *
66
* Purpose : Path Offset (Inflate/Shrink) *
77
* License : http://www.boost.org/LICENSE_1_0.txt *
88
*******************************************************************************/
@@ -13,9 +13,23 @@
1313

1414
namespace Clipper2Lib {
1515

16-
const double default_arc_tolerance = 0.25;
1716
const double floating_point_tolerance = 1e-12;
1817

18+
// Clipper2 approximates arcs by using series of relatively short straight
19+
//line segments. And logically, shorter line segments will produce better arc
20+
// approximations. But very short segments can degrade performance, usually
21+
// with little or no discernable improvement in curve quality. Very short
22+
// segments can even detract from curve quality, due to the effects of integer
23+
// rounding. Since there isn't an optimal number of line segments for any given
24+
// arc radius (that perfectly balances curve approximation with performance),
25+
// arc tolerance is user defined. Nevertheless, when the user doesn't define
26+
// an arc tolerance (ie leaves alone the 0 default value), the calculated
27+
// default arc tolerance (offset_radius / 500) generally produces good (smooth)
28+
// arc approximations without producing excessively small segment lengths.
29+
// See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
30+
const double arc_const = 0.002; // <-- 1/500
31+
32+
1933
//------------------------------------------------------------------------------
2034
// Miscellaneous methods
2135
//------------------------------------------------------------------------------
@@ -50,11 +64,10 @@ inline double Hypot(double x, double y)
5064

5165
static PointD GetUnitNormal(const Point64& pt1, const Point64& pt2)
5266
{
53-
double dx, dy, inverse_hypot;
5467
if (pt1 == pt2) return PointD(0.0, 0.0);
55-
dx = static_cast<double>(pt2.x - pt1.x);
56-
dy = static_cast<double>(pt2.y - pt1.y);
57-
inverse_hypot = 1.0 / Hypot(dx, dy);
68+
double dx = static_cast<double>(pt2.x - pt1.x);
69+
double dy = static_cast<double>(pt2.y - pt1.y);
70+
double inverse_hypot = 1.0 / Hypot(dx, dy);
5871
dx *= inverse_hypot;
5972
dy *= inverse_hypot;
6073
return PointD(dy, -dx);
@@ -260,8 +273,7 @@ void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle
260273
// so we'll need to do the following calculations for *every* vertex.
261274
double abs_delta = std::fabs(group_delta_);
262275
double arcTol = (arc_tolerance_ > floating_point_tolerance ?
263-
std::min(abs_delta, arc_tolerance_) :
264-
std::log10(2 + abs_delta) * default_arc_tolerance);
276+
std::min(abs_delta, arc_tolerance_) : abs_delta * arc_const);
265277
double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI);
266278
step_sin_ = std::sin(2 * PI / steps_per_360);
267279
step_cos_ = std::cos(2 * PI / steps_per_360);
@@ -459,9 +471,8 @@ void ClipperOffset::DoGroupOffset(Group& group)
459471
// arcTol - when arc_tolerance_ is undefined (0) then curve imprecision
460472
// will be relative to the size of the offset (delta). Obviously very
461473
//large offsets will almost always require much less precision.
462-
double arcTol = (arc_tolerance_ > floating_point_tolerance ?
463-
std::min(abs_delta, arc_tolerance_) :
464-
std::log10(2 + abs_delta) * default_arc_tolerance);
474+
double arcTol = (arc_tolerance_ > floating_point_tolerance) ?
475+
std::min(abs_delta, arc_tolerance_) : abs_delta * arc_const;
465476

466477
double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI);
467478
step_sin_ = std::sin(2 * PI / steps_per_360);
@@ -590,7 +601,6 @@ void ClipperOffset::ExecuteInternal(double delta)
590601
if (!solution->size()) return;
591602

592603
bool paths_reversed = CheckReverseOrientation();
593-
594604
//clean up self-intersections ...
595605
Clipper64 c;
596606
c.PreserveCollinear(false);
@@ -617,13 +627,12 @@ void ClipperOffset::ExecuteInternal(double delta)
617627
else
618628
c.Execute(ClipType::Union, FillRule::Positive, *solution);
619629
}
620-
621630
}
622631

623-
void ClipperOffset::Execute(double delta, Paths64& paths)
632+
void ClipperOffset::Execute(double delta, Paths64& paths64)
624633
{
625-
paths.clear();
626-
solution = &paths;
634+
paths64.clear();
635+
solution = &paths64;
627636
solution_tree = nullptr;
628637
ExecuteInternal(delta);
629638
}

CPP/Examples/Inflate/Inflate.cpp

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,71 +20,83 @@ int main(int argc, char* argv[])
2020
//std::getchar();
2121
}
2222

23-
void DoSimpleShapes()
23+
void DoSimpleShapes()
2424
{
25-
Paths64 op1, op2;
25+
// OPEN_PATHS SVG:
26+
27+
PathsD op1, op2;
2628
FillRule fr2 = FillRule::EvenOdd;
2729
SvgWriter svg2;
28-
29-
op1.push_back(MakePath({ 80,60, 20,20, 180,20, 180,70, 25,150, 20,180, 180,180 }));
30+
op1.push_back(MakePathD({ 80,60, 20,20, 180,20, 180,70, 25,150, 20,180, 180,180 }));
3031
op2 = InflatePaths(op1, 15, JoinType::Miter, EndType::Square, 3);
3132
SvgAddOpenSubject(svg2, op1, fr2, false);
32-
SvgAddSolution(svg2, TransformPaths<double, int64_t>(op2), fr2, false);
33+
SvgAddSolution(svg2, op2, fr2, false);
3334
SvgAddCaption(svg2, "Miter Joins; Square Ends", 20, 210);
34-
35-
op1 = TranslatePaths<int64_t>(op1, 210, 0);
35+
op1 = TranslatePaths<double>(op1, 210, 0);
3636
op2 = InflatePaths(op1, 15, JoinType::Square, EndType::Square);
3737
SvgAddOpenSubject(svg2, op1, fr2, false);
38-
SvgAddSolution(svg2, TransformPaths<double, int64_t>(op2), fr2, false);
38+
SvgAddSolution(svg2, op2, fr2, false);
3939
SvgAddCaption(svg2, "Square Joins; Square Ends", 230, 210);
40-
41-
op1 = TranslatePaths<int64_t>(op1, 210, 0);
40+
op1 = TranslatePaths<double>(op1, 210, 0);
4241
op2 = InflatePaths(op1, 15, JoinType::Bevel, EndType::Butt, 3);
4342
SvgAddOpenSubject(svg2, op1, fr2, false);
44-
SvgAddSolution(svg2, TransformPaths<double, int64_t>(op2), fr2, false);
43+
SvgAddSolution(svg2, op2, fr2, false);
4544
SvgAddCaption(svg2, "Bevel Joins; Butt Ends", 440, 210);
46-
47-
op1 = TranslatePaths<int64_t>(op1, 210, 0);
45+
op1 = TranslatePaths<double>(op1, 210, 0);
4846
op2 = InflatePaths(op1, 15, JoinType::Round, EndType::Round);
4947
SvgAddOpenSubject(svg2, op1, fr2, false);
50-
SvgAddSolution(svg2, TransformPaths<double, int64_t>(op2), fr2, false);
48+
SvgAddSolution(svg2, op2, fr2, false);
5149
SvgAddCaption(svg2, "Round Joins; Round Ends", 650, 210);
52-
5350
SvgSaveToFile(svg2, "open_paths.svg", 800, 600, 20);
5451
System("open_paths.svg");
5552

56-
//triangle offset - with large miter
57-
Paths64 p, pp;
58-
p.push_back(MakePath({ 30, 150, 60, 350, 0, 350 }));
59-
pp.insert(pp.end(), p.begin(), p.end());
53+
// POLYGON JOINTYPES SVG:
6054

55+
// 1. triangle offset - with large miter
56+
int err, scale = 100;
57+
PathsD p, solution;
58+
p.push_back(MakePathD({ 30,150, 60,350, 0,350 }));
59+
solution.insert(solution.end(), p.begin(), p.end());
6160
for (int i = 0; i < 5; ++i)
6261
{
63-
//note the relatively large miter limit set here (10)
64-
p = InflatePaths(p, 5,
65-
JoinType::Miter, EndType::Polygon, 10);
66-
pp.insert(pp.end(), p.begin(), p.end());
62+
p = InflatePaths(p, 5, JoinType::Miter, EndType::Polygon, 10);
63+
solution.insert(solution.end(), p.begin(), p.end());
6764
}
65+
66+
// 2. open rectangles offset bevelled, squared & rounded ...
6867

69-
//rectangle offset - both squared and rounded
7068
p.clear();
71-
p.push_back(MakePath({ 100,30, 340,30, 340,230, 100,230 }));
72-
pp.insert(pp.end(), p.begin(), p.end());
73-
//nb: using the ClipperOffest class directly here to control
74-
//different join types within the same offset operation
75-
ClipperOffset co;
76-
co.AddPaths(p, JoinType::Miter, EndType::Joined);
77-
p = TranslatePaths<int64_t>(p, 120, 100);
78-
pp.insert(pp.end(), p.begin(), p.end());
79-
co.AddPaths(p, JoinType::Round, EndType::Joined);
80-
co.Execute(10, p);
81-
pp.insert(pp.end(), p.begin(), p.end());
82-
83-
FillRule fr3 = FillRule::EvenOdd;
69+
p.push_back(MakePathD({ 100,30, 340,30, 340,230, 100,230 }));
70+
p.push_back(TranslatePath<double>(p[0], 60, 50));
71+
p.push_back(TranslatePath<double>(p[1], 60, 50));
72+
8473
SvgWriter svg;
85-
SvgAddSolution(svg, TransformPaths<double, int64_t>(pp), fr3, false);
86-
SvgSaveToFile(svg, "solution_off.svg", 800, 600, 20);
87-
System("solution_off.svg");
74+
SvgAddOpenSubject(svg, p);
75+
SvgAddCaption(svg, "Bevelled", 100, 15);
76+
SvgAddCaption(svg, "Squared", 160, 65);
77+
SvgAddCaption(svg, "Rounded", 220, 115);
78+
79+
// nb: we must use the ClipperOffest class directly if we want to
80+
// perform different join types within the same offset operation
81+
// ClipperOffset only supports int64_t coords so, if we want better than unit
82+
// precision, we have to scale manually. (InflatePaths does this scaling internally)
83+
ClipperOffset co;
84+
p = ScalePaths<double, double>(p, scale, err);
85+
// AddPaths - paths must be int64_t paths
86+
co.AddPath(TransformPath<int64_t, double>(p[0]), JoinType::Bevel, EndType::Joined);
87+
co.AddPath(TransformPath<int64_t, double>(p[1]), JoinType::Square, EndType::Joined);
88+
co.AddPath(TransformPath<int64_t, double>(p[2]), JoinType::Round, EndType::Joined);
89+
Paths64 sol64;
90+
co.Execute(scale * 10, sol64); // ClipperOffset solutions must be int64_t
91+
92+
// de-scale and append to solution ...
93+
p = ScalePaths<double, int64_t>(sol64, 1.0 / scale, err);
94+
solution.insert(solution.end(), p.begin(), p.end());
95+
96+
string filename = "polygon_jointypes.svg";
97+
SvgAddSolution(svg, solution, FillRule::EvenOdd, false);
98+
SvgSaveToFile(svg, filename, 800, 600, 20);
99+
System(filename);
88100
}
89101

90102
void DoRabbit()

CSharp/Clipper2Lib.Examples/InflateDemo/Main.cs

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,42 +26,48 @@ public static void Main()
2626
public static void DoSimpleShapes()
2727
{
2828
SvgWriter svg = new();
29-
ClipperOffset co = new();
3029

31-
//triangle offset - with large miter
32-
Paths64 p0 = new() { Clipper.MakePath(new [] { 30,150, 60,350, 0,350 }) };
33-
Paths64 p = new();
30+
//TRIANGLE OFFSET - WITH LARGE MITER
31+
32+
PathsD pp = new() { Clipper.MakePath(new double[] { 30,150, 60,350, 0,350 }) };
33+
PathsD solution = new();
3434
for (int i = 0; i < 5; ++i)
3535
{
3636
//nb: the last parameter here (10) greatly increases miter limit
37-
p0 = Clipper.InflatePaths(p0, 5, JoinType.Miter, EndType.Polygon, 10);
38-
p.AddRange(p0);
37+
pp = Clipper.InflatePaths(pp, 5, JoinType.Miter, EndType.Polygon, 10);
38+
solution.AddRange(pp);
3939
}
40-
SvgUtils.AddSolution(svg, p, false);
41-
p.Clear();
40+
SvgUtils.AddSolution(svg, solution, false);
4241

43-
//rectangle offset - both squared and rounded
44-
//nb: using the ClipperOffest class directly here to control
45-
//different join types within the same offset operation
46-
p.Add(Clipper.MakePath(new [] { 100,0, 340,0, 340,200, 100,200, 100, 0 }));
47-
SvgUtils.AddOpenSubject(svg, p);
48-
co.AddPaths(p, JoinType.Bevel, EndType.Joined);
42+
// RECTANGLE OFFSET - BEVEL, SQUARED AND ROUNDED
4943

50-
p = Clipper.TranslatePaths(p, 60, 50);
51-
SvgUtils.AddOpenSubject(svg, p);
52-
co.AddPaths(p, JoinType.Square, EndType.Joined);
53-
p = Clipper.TranslatePaths(p, 60, 50);
54-
SvgUtils.AddOpenSubject(svg, p);
55-
co.AddPaths(p, JoinType.Round, EndType.Joined);
44+
solution.Clear();
45+
solution.Add(Clipper.MakePath(new double[] { 100, 0, 340, 0, 340, 200, 100, 200 }));
46+
solution.Add(Clipper.TranslatePath(solution[0], 60, 50));
47+
solution.Add(Clipper.TranslatePath(solution[1], 60, 50));
48+
SvgUtils.AddOpenSubject(svg, solution);
5649

57-
co.Execute(10, p);
50+
// nb: rather than using InflatePaths(), we have to use the
51+
// ClipperOffest class directly because we want to perform
52+
// different join types in a single offset operation
53+
ClipperOffset co = new();
54+
// because ClipperOffset only accepts Int64 paths, scale them
55+
// so the de-scaled offset result will have greater precision
56+
double scale = 100;
57+
Paths64 pp64 = Clipper.ScalePaths64(solution, scale);
58+
co.AddPath(pp64[0], JoinType.Bevel, EndType.Joined);
59+
co.AddPath(pp64[1], JoinType.Square, EndType.Joined);
60+
co.AddPath(pp64[2], JoinType.Round, EndType.Joined);
61+
co.Execute(10 * scale, pp64);
62+
// now de-scale the offset solution
63+
solution = Clipper.ScalePathsD(pp64, 1 / scale);
5864

5965
const string filename = "../../../inflate.svg";
60-
SvgUtils.AddSolution(svg, p, false);
61-
SvgUtils.AddCaption(svg, "Beveled join", 100, -27);
62-
SvgUtils.AddCaption(svg, "Squared join", 160, 23);
63-
SvgUtils.AddCaption(svg, "Rounded join", 220, 73);
64-
SvgUtils.SaveToFile(svg, filename, FillRule.EvenOdd, 800, 600, 20);
66+
SvgUtils.AddSolution(svg, solution, false);
67+
SvgUtils.AddCaption(svg, "Beveled join", 100, -17);
68+
SvgUtils.AddCaption(svg, "Squared join", 160, 33);
69+
SvgUtils.AddCaption(svg, "Rounded join", 220, 83);
70+
SvgUtils.SaveToFile(svg, filename, FillRule.EvenOdd, 800, 600, 40);
6571
ClipperFileIO.OpenFileWithDefaultApp(filename);
6672
}
6773

CSharp/Clipper2Lib/Clipper.Core.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*******************************************************************************
22
* Author : Angus Johnson *
3-
* Date : 10 October 2024 *
3+
* Date : 22 January 2025 *
44
* Website : http://www.angusj.com *
5-
* Copyright : Angus Johnson 2010-2024 *
5+
* Copyright : Angus Johnson 2010-2025 *
66
* Purpose : Core structures and functions for the Clipper Library *
77
* License : http://www.boost.org/LICENSE_1_0.txt *
88
*******************************************************************************/
@@ -523,7 +523,6 @@ public static class InternalClipper
523523
internal const double min_coord = -MaxCoord;
524524
internal const long Invalid64 = MaxInt64;
525525

526-
internal const double defaultArcTolerance = 0.25;
527526
internal const double floatingPointTolerance = 1E-12;
528527
internal const double defaultMinimumEdgeLength = 0.1;
529528

CSharp/Clipper2Lib/Clipper.Offset.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*******************************************************************************
22
* Author : Angus Johnson *
3-
* Date : 22 November 2024 *
3+
* Date : 22 January 2025 *
44
* Website : http://www.angusj.com *
5-
* Copyright : Angus Johnson 2010-2024 *
5+
* Copyright : Angus Johnson 2010-2025 *
66
* Purpose : Path Offset (Inflate/Shrink) *
77
* License : http://www.boost.org/LICENSE_1_0.txt *
88
*******************************************************************************/
@@ -69,6 +69,20 @@ public Group(Paths64 paths, JoinType joinType, EndType endType = EndType.Polygon
6969

7070
private const double Tolerance = 1.0E-12;
7171

72+
// Clipper2 approximates arcs by using series of relatively short straight
73+
//line segments. And logically, shorter line segments will produce better arc
74+
// approximations. But very short segments can degrade performance, usually
75+
// with little or no discernable improvement in curve quality. Very short
76+
// segments can even detract from curve quality, due to the effects of integer
77+
// rounding. Since there isn't an optimal number of line segments for any given
78+
// arc radius (that perfectly balances curve approximation with performance),
79+
// arc tolerance is user defined. Nevertheless, when the user doesn't define
80+
// an arc tolerance (ie leaves alone the 0 default value), the calculated
81+
// default arc tolerance (offset_radius / 500) generally produces good (smooth)
82+
// arc approximations without producing excessively small segment lengths.
83+
// See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
84+
const double arc_const = 0.002; // <-- 1/500
85+
7286
private readonly List<Group> _groupList = new List<Group>();
7387
private Path64 pathOut = new Path64();
7488
private readonly PathD _normals = new PathD();
@@ -474,9 +488,7 @@ private void DoRound(Path64 path, int j, int k, double angle)
474488
// when DeltaCallback is assigned, _groupDelta won't be constant,
475489
// so we'll need to do the following calculations for *every* vertex.
476490
double absDelta = Math.Abs(_groupDelta);
477-
double arcTol = ArcTolerance > 0.01 ?
478-
ArcTolerance :
479-
Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance;
491+
double arcTol = ArcTolerance > 0.01 ? ArcTolerance : absDelta * arc_const;
480492
double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta);
481493
_stepSin = Math.Sin((2 * Math.PI) / stepsPer360);
482494
_stepCos = Math.Cos((2 * Math.PI) / stepsPer360);
@@ -680,14 +692,7 @@ private void DoGroupOffset(Group group)
680692

681693
if (group.joinType == JoinType.Round || group.endType == EndType.Round)
682694
{
683-
// calculate the number of steps required to approximate a circle
684-
// (see http://www.angusj.com/clipper2/Docs/Trigonometry.htm)
685-
// arcTol - when arc_tolerance_ is undefined (0) then curve imprecision
686-
// will be relative to the size of the offset (delta). Obviously very
687-
//large offsets will almost always require much less precision.
688-
double arcTol = ArcTolerance > 0.01 ?
689-
ArcTolerance :
690-
Math.Log10(2 + absDelta) * InternalClipper.defaultArcTolerance;
695+
double arcTol = ArcTolerance > 0.01 ? ArcTolerance : absDelta * arc_const;
691696
double stepsPer360 = Math.PI / Math.Acos(1 - arcTol / absDelta);
692697
_stepSin = Math.Sin((2 * Math.PI) / stepsPer360);
693698
_stepCos = Math.Cos((2 * Math.PI) / stepsPer360);

0 commit comments

Comments
 (0)