clipper2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +165 -0
- data/examples/larb_transform.rb +17 -0
- data/examples/rugl_mesh.rb +33 -0
- data/examples/svg_boolean.rb +28 -0
- data/ext/clipper2/CMakeLists.txt +27 -0
- data/ext/clipper2/clipper2_native.cpp +2 -0
- data/ext/clipper2/extconf.rb +27 -0
- data/ext/clipper2/generate_bindings.rb +73 -0
- data/ext/clipper2/memory_check.cpp +48 -0
- data/ext/clipper2/vendor/LICENSE +23 -0
- data/ext/clipper2/vendor/UPSTREAM.md +11 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.core.h +1159 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.engine.h +635 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.export.h +851 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.h +796 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.minkowski.h +117 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.offset.h +125 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.rectclip.h +80 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.triangulation.h +27 -0
- data/ext/clipper2/vendor/include/clipper2/clipper.version.h +6 -0
- data/ext/clipper2/vendor/src/clipper.engine.cpp +3152 -0
- data/ext/clipper2/vendor/src/clipper.offset.cpp +661 -0
- data/ext/clipper2/vendor/src/clipper.rectclip.cpp +1027 -0
- data/ext/clipper2/vendor/src/clipper.triangulation.cpp +1221 -0
- data/lib/clipper2/api.rb +45 -0
- data/lib/clipper2/c_paths64.rb +80 -0
- data/lib/clipper2/earcut/engine.rb +191 -0
- data/lib/clipper2/earcut/geometry.rb +100 -0
- data/lib/clipper2/earcut/node.rb +18 -0
- data/lib/clipper2/earcut/ring.rb +94 -0
- data/lib/clipper2/earcut.rb +18 -0
- data/lib/clipper2/errors.rb +19 -0
- data/lib/clipper2/generated.rb +62 -0
- data/lib/clipper2/native.rb +38 -0
- data/lib/clipper2/native_memory.rb +64 -0
- data/lib/clipper2/operations.rb +206 -0
- data/lib/clipper2/path.rb +171 -0
- data/lib/clipper2/path_simplifier.rb +61 -0
- data/lib/clipper2/paths.rb +130 -0
- data/lib/clipper2/quantizer.rb +48 -0
- data/lib/clipper2/triangulation.rb +103 -0
- data/lib/clipper2/version.rb +5 -0
- data/lib/clipper2.rb +34 -0
- data/licenses/CLIPPER2-BSL-1.0.txt +23 -0
- data/licenses/EARCUT-ISC.txt +15 -0
- metadata +113 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Author : Angus Johnson *
|
|
3
|
+
* Date : 11 October 2025 *
|
|
4
|
+
* Website : https://www.angusj.com *
|
|
5
|
+
* Copyright : Angus Johnson 2010-2025 *
|
|
6
|
+
* Purpose : Path Offset (Inflate/Shrink) *
|
|
7
|
+
* License : https://www.boost.org/LICENSE_1_0.txt *
|
|
8
|
+
*******************************************************************************/
|
|
9
|
+
|
|
10
|
+
#include "clipper2/clipper.h"
|
|
11
|
+
#include "clipper2/clipper.offset.h"
|
|
12
|
+
|
|
13
|
+
namespace Clipper2Lib {
|
|
14
|
+
|
|
15
|
+
const double floating_point_tolerance = 1e-12;
|
|
16
|
+
|
|
17
|
+
// Clipper2 approximates arcs by using series of relatively short straight
|
|
18
|
+
//line segments. And logically, shorter line segments will produce better arc
|
|
19
|
+
// approximations. But very short segments can degrade performance, usually
|
|
20
|
+
// with little or no discernable improvement in curve quality. Very short
|
|
21
|
+
// segments can even detract from curve quality, due to the effects of integer
|
|
22
|
+
// rounding. Since there isn't an optimal number of line segments for any given
|
|
23
|
+
// arc radius (that perfectly balances curve approximation with performance),
|
|
24
|
+
// arc tolerance is user defined. Nevertheless, when the user doesn't define
|
|
25
|
+
// an arc tolerance (ie leaves alone the 0 default value), the calculated
|
|
26
|
+
// default arc tolerance (offset_radius / 500) generally produces good (smooth)
|
|
27
|
+
// arc approximations without producing excessively small segment lengths.
|
|
28
|
+
// See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
|
|
29
|
+
const double arc_const = 0.002; // <-- 1/500
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
//------------------------------------------------------------------------------
|
|
33
|
+
// Miscellaneous methods
|
|
34
|
+
//------------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
void GetLowestClosedPathInfo(const Paths64& paths, std::optional<size_t>& idx, bool& is_neg_area)
|
|
37
|
+
{
|
|
38
|
+
idx.reset();
|
|
39
|
+
Point64 botPt = Point64(INT64_MAX, INT64_MIN);
|
|
40
|
+
for (size_t i = 0; i < paths.size(); ++i)
|
|
41
|
+
{
|
|
42
|
+
double a = MAX_DBL;
|
|
43
|
+
for (const Point64& pt : paths[i])
|
|
44
|
+
{
|
|
45
|
+
if ((pt.y < botPt.y) ||
|
|
46
|
+
((pt.y == botPt.y) && (pt.x >= botPt.x))) continue;
|
|
47
|
+
if (a == MAX_DBL)
|
|
48
|
+
{
|
|
49
|
+
a = Area(paths[i]);
|
|
50
|
+
if (a == 0) break; // invalid closed path, so break from inner loop
|
|
51
|
+
is_neg_area = a < 0;
|
|
52
|
+
}
|
|
53
|
+
idx = i;
|
|
54
|
+
botPt.x = pt.x;
|
|
55
|
+
botPt.y = pt.y;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
inline double Hypot(double x, double y)
|
|
61
|
+
{
|
|
62
|
+
// given that this is an internal function, and given the x and y parameters
|
|
63
|
+
// will always be coordinate values (or the difference between coordinate values),
|
|
64
|
+
// x and y should always be within INT64_MIN to INT64_MAX. Consequently,
|
|
65
|
+
// there should be no risk that the following computation will overflow
|
|
66
|
+
// see https://stackoverflow.com/a/32436148/359538
|
|
67
|
+
return std::sqrt(x * x + y * y);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static PointD GetUnitNormal(const Point64& pt1, const Point64& pt2)
|
|
71
|
+
{
|
|
72
|
+
if (pt1 == pt2) return PointD(0.0, 0.0);
|
|
73
|
+
double dx = static_cast<double>(pt2.x - pt1.x);
|
|
74
|
+
double dy = static_cast<double>(pt2.y - pt1.y);
|
|
75
|
+
double inverse_hypot = 1.0 / Hypot(dx, dy);
|
|
76
|
+
dx *= inverse_hypot;
|
|
77
|
+
dy *= inverse_hypot;
|
|
78
|
+
return PointD(dy, -dx);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
inline bool AlmostZero(double value, double epsilon = 0.001)
|
|
82
|
+
{
|
|
83
|
+
return std::fabs(value) < epsilon;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
inline PointD NormalizeVector(const PointD& vec)
|
|
87
|
+
{
|
|
88
|
+
double h = Hypot(vec.x, vec.y);
|
|
89
|
+
if (AlmostZero(h)) return PointD(0,0);
|
|
90
|
+
double inverseHypot = 1 / h;
|
|
91
|
+
return PointD(vec.x * inverseHypot, vec.y * inverseHypot);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
inline PointD GetAvgUnitVector(const PointD& vec1, const PointD& vec2)
|
|
95
|
+
{
|
|
96
|
+
return NormalizeVector(PointD(vec1.x + vec2.x, vec1.y + vec2.y));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
inline bool IsClosedPath(EndType et)
|
|
100
|
+
{
|
|
101
|
+
return et == EndType::Polygon || et == EndType::Joined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static inline Point64 GetPerpendic(const Point64& pt, const PointD& norm, double delta)
|
|
105
|
+
{
|
|
106
|
+
#ifdef USINGZ
|
|
107
|
+
return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta, pt.z);
|
|
108
|
+
#else
|
|
109
|
+
return Point64(pt.x + norm.x * delta, pt.y + norm.y * delta);
|
|
110
|
+
#endif
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
inline PointD GetPerpendicD(const Point64& pt, const PointD& norm, double delta)
|
|
114
|
+
{
|
|
115
|
+
#ifdef USINGZ
|
|
116
|
+
return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta, pt.z);
|
|
117
|
+
#else
|
|
118
|
+
return PointD(pt.x + norm.x * delta, pt.y + norm.y * delta);
|
|
119
|
+
#endif
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
inline void NegatePath(PathD& path)
|
|
123
|
+
{
|
|
124
|
+
for (PointD& pt : path)
|
|
125
|
+
{
|
|
126
|
+
pt.x = -pt.x;
|
|
127
|
+
pt.y = -pt.y;
|
|
128
|
+
#ifdef USINGZ
|
|
129
|
+
pt.z = pt.z;
|
|
130
|
+
#endif
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
//------------------------------------------------------------------------------
|
|
136
|
+
// ClipperOffset::Group methods
|
|
137
|
+
//------------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType _end_type):
|
|
140
|
+
paths_in(_paths), join_type(_join_type), end_type(_end_type)
|
|
141
|
+
{
|
|
142
|
+
bool is_joined =
|
|
143
|
+
(end_type == EndType::Polygon) ||
|
|
144
|
+
(end_type == EndType::Joined);
|
|
145
|
+
for (Path64& p: paths_in)
|
|
146
|
+
StripDuplicates(p, is_joined);
|
|
147
|
+
|
|
148
|
+
if (end_type == EndType::Polygon)
|
|
149
|
+
{
|
|
150
|
+
bool is_neg_area;
|
|
151
|
+
GetLowestClosedPathInfo(paths_in, lowest_path_idx, is_neg_area);
|
|
152
|
+
// the lowermost path must be an outer path, so if its orientation is negative,
|
|
153
|
+
// then flag the whole group is 'reversed' (will negate delta etc.)
|
|
154
|
+
// as this is much more efficient than reversing every path.
|
|
155
|
+
is_reversed = lowest_path_idx.has_value() && is_neg_area;
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
{
|
|
159
|
+
lowest_path_idx.reset();
|
|
160
|
+
is_reversed = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//------------------------------------------------------------------------------
|
|
165
|
+
// ClipperOffset methods
|
|
166
|
+
//------------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
void ClipperOffset::AddPath(const Path64& path, JoinType jt_, EndType et_)
|
|
169
|
+
{
|
|
170
|
+
groups_.emplace_back(Paths64(1, path), jt_, et_);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void ClipperOffset::AddPaths(const Paths64 &paths, JoinType jt_, EndType et_)
|
|
174
|
+
{
|
|
175
|
+
if (paths.size() == 0) return;
|
|
176
|
+
groups_.emplace_back(paths, jt_, et_);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void ClipperOffset::BuildNormals(const Path64& path)
|
|
180
|
+
{
|
|
181
|
+
norms.clear();
|
|
182
|
+
norms.reserve(path.size());
|
|
183
|
+
if (path.size() == 0) return;
|
|
184
|
+
Path64::const_iterator path_iter, path_stop_iter = --path.cend();
|
|
185
|
+
for (path_iter = path.cbegin(); path_iter != path_stop_iter; ++path_iter)
|
|
186
|
+
norms.emplace_back(GetUnitNormal(*path_iter,*(path_iter +1)));
|
|
187
|
+
norms.emplace_back(GetUnitNormal(*path_stop_iter, *(path.cbegin())));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void ClipperOffset::DoBevel(const Path64& path, size_t j, size_t k)
|
|
191
|
+
{
|
|
192
|
+
PointD pt1, pt2;
|
|
193
|
+
if (j == k)
|
|
194
|
+
{
|
|
195
|
+
double abs_delta = std::abs(group_delta_);
|
|
196
|
+
#ifdef USINGZ
|
|
197
|
+
pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y, path[j].z);
|
|
198
|
+
pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y, path[j].z);
|
|
199
|
+
#else
|
|
200
|
+
pt1 = PointD(path[j].x - abs_delta * norms[j].x, path[j].y - abs_delta * norms[j].y);
|
|
201
|
+
pt2 = PointD(path[j].x + abs_delta * norms[j].x, path[j].y + abs_delta * norms[j].y);
|
|
202
|
+
#endif
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
{
|
|
206
|
+
#ifdef USINGZ
|
|
207
|
+
pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y, path[j].z);
|
|
208
|
+
pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y, path[j].z);
|
|
209
|
+
#else
|
|
210
|
+
pt1 = PointD(path[j].x + group_delta_ * norms[k].x, path[j].y + group_delta_ * norms[k].y);
|
|
211
|
+
pt2 = PointD(path[j].x + group_delta_ * norms[j].x, path[j].y + group_delta_ * norms[j].y);
|
|
212
|
+
#endif
|
|
213
|
+
}
|
|
214
|
+
path_out.emplace_back(pt1);
|
|
215
|
+
path_out.emplace_back(pt2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k)
|
|
219
|
+
{
|
|
220
|
+
PointD vec;
|
|
221
|
+
if (j == k)
|
|
222
|
+
vec = PointD(norms[j].y, -norms[j].x);
|
|
223
|
+
else
|
|
224
|
+
vec = GetAvgUnitVector(
|
|
225
|
+
PointD(-norms[k].y, norms[k].x),
|
|
226
|
+
PointD(norms[j].y, -norms[j].x));
|
|
227
|
+
|
|
228
|
+
double abs_delta = std::abs(group_delta_);
|
|
229
|
+
|
|
230
|
+
// now offset the original vertex delta units along unit vector
|
|
231
|
+
PointD ptQ = PointD(path[j]);
|
|
232
|
+
ptQ = TranslatePoint(ptQ, abs_delta * vec.x, abs_delta * vec.y);
|
|
233
|
+
// get perpendicular vertices
|
|
234
|
+
PointD pt1 = TranslatePoint(ptQ, group_delta_ * vec.y, group_delta_ * -vec.x);
|
|
235
|
+
PointD pt2 = TranslatePoint(ptQ, group_delta_ * -vec.y, group_delta_ * vec.x);
|
|
236
|
+
// get 2 vertices along one edge offset
|
|
237
|
+
PointD pt3 = GetPerpendicD(path[k], norms[k], group_delta_);
|
|
238
|
+
if (j == k)
|
|
239
|
+
{
|
|
240
|
+
PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_);
|
|
241
|
+
PointD pt = ptQ;
|
|
242
|
+
GetLineIntersectPt(pt1, pt2, pt3, pt4, pt);
|
|
243
|
+
//get the second intersect point through reflecion
|
|
244
|
+
path_out.emplace_back(ReflectPoint(pt, ptQ));
|
|
245
|
+
path_out.emplace_back(pt);
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
{
|
|
249
|
+
PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_);
|
|
250
|
+
PointD pt = ptQ;
|
|
251
|
+
GetLineIntersectPt(pt1, pt2, pt3, pt4, pt);
|
|
252
|
+
path_out.emplace_back(pt);
|
|
253
|
+
//get the second intersect point through reflecion
|
|
254
|
+
path_out.emplace_back(ReflectPoint(pt, ptQ));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void ClipperOffset::DoMiter(const Path64& path, size_t j, size_t k, double cos_a)
|
|
259
|
+
{
|
|
260
|
+
double q = group_delta_ / (cos_a + 1);
|
|
261
|
+
#ifdef USINGZ
|
|
262
|
+
path_out.emplace_back(
|
|
263
|
+
path[j].x + (norms[k].x + norms[j].x) * q,
|
|
264
|
+
path[j].y + (norms[k].y + norms[j].y) * q,
|
|
265
|
+
path[j].z);
|
|
266
|
+
#else
|
|
267
|
+
path_out.emplace_back(
|
|
268
|
+
path[j].x + (norms[k].x + norms[j].x) * q,
|
|
269
|
+
path[j].y + (norms[k].y + norms[j].y) * q);
|
|
270
|
+
#endif
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle)
|
|
274
|
+
{
|
|
275
|
+
if (deltaCallback64_) {
|
|
276
|
+
// when deltaCallback64_ is assigned, group_delta_ won't be constant,
|
|
277
|
+
// so we'll need to do the following calculations for *every* vertex.
|
|
278
|
+
double abs_delta = std::fabs(group_delta_);
|
|
279
|
+
double arcTol = (arc_tolerance_ > floating_point_tolerance ?
|
|
280
|
+
std::min(abs_delta, arc_tolerance_) : abs_delta * arc_const);
|
|
281
|
+
double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI);
|
|
282
|
+
step_sin_ = std::sin(2 * PI / steps_per_360);
|
|
283
|
+
step_cos_ = std::cos(2 * PI / steps_per_360);
|
|
284
|
+
if (group_delta_ < 0.0) step_sin_ = -step_sin_;
|
|
285
|
+
steps_per_rad_ = steps_per_360 / (2 * PI);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Point64 pt = path[j];
|
|
289
|
+
PointD offsetVec = PointD(norms[k].x * group_delta_, norms[k].y * group_delta_);
|
|
290
|
+
|
|
291
|
+
if (j == k) offsetVec.Negate();
|
|
292
|
+
#ifdef USINGZ
|
|
293
|
+
path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z);
|
|
294
|
+
#else
|
|
295
|
+
path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y);
|
|
296
|
+
#endif
|
|
297
|
+
int steps = static_cast<int>(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456
|
|
298
|
+
for (int i = 1; i < steps; ++i) // ie 1 less than steps
|
|
299
|
+
{
|
|
300
|
+
offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y,
|
|
301
|
+
offsetVec.x * step_sin_ + offsetVec.y * step_cos_);
|
|
302
|
+
#ifdef USINGZ
|
|
303
|
+
path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y, pt.z);
|
|
304
|
+
#else
|
|
305
|
+
path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y);
|
|
306
|
+
#endif
|
|
307
|
+
}
|
|
308
|
+
path_out.emplace_back(GetPerpendic(path[j], norms[j], group_delta_));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size_t k)
|
|
312
|
+
{
|
|
313
|
+
// Let A = change in angle where edges join
|
|
314
|
+
// A == 0: ie no change in angle (flat join)
|
|
315
|
+
// A == PI: edges 'spike'
|
|
316
|
+
// sin(A) < 0: right turning
|
|
317
|
+
// cos(A) < 0: change in angle is more than 90 degree
|
|
318
|
+
|
|
319
|
+
if (path[j] == path[k]) return;
|
|
320
|
+
|
|
321
|
+
double sin_a = CrossProduct(norms[j], norms[k]);
|
|
322
|
+
double cos_a = DotProduct(norms[j], norms[k]);
|
|
323
|
+
if (sin_a > 1.0) sin_a = 1.0;
|
|
324
|
+
else if (sin_a < -1.0) sin_a = -1.0;
|
|
325
|
+
|
|
326
|
+
if (deltaCallback64_) {
|
|
327
|
+
group_delta_ = deltaCallback64_(path, norms, j, k);
|
|
328
|
+
if (group.is_reversed) group_delta_ = -group_delta_;
|
|
329
|
+
}
|
|
330
|
+
if (std::fabs(group_delta_) <= floating_point_tolerance)
|
|
331
|
+
{
|
|
332
|
+
path_out.emplace_back(path[j]);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593)
|
|
337
|
+
{
|
|
338
|
+
// is concave
|
|
339
|
+
// by far the simplest way to construct concave joins, especially those joining very
|
|
340
|
+
// short segments, is to insert 3 points that produce negative regions. These regions
|
|
341
|
+
// will be removed later by the finishing union operation. This is also the best way
|
|
342
|
+
// to ensure that path reversals (ie over-shrunk paths) are removed.
|
|
343
|
+
#ifdef USINGZ
|
|
344
|
+
path_out.emplace_back(GetPerpendic(path[j], norms[k], group_delta_), path[j].z);
|
|
345
|
+
path_out.emplace_back(path[j]); // (#405, #873, #916)
|
|
346
|
+
path_out.emplace_back(GetPerpendic(path[j], norms[j], group_delta_), path[j].z);
|
|
347
|
+
#else
|
|
348
|
+
path_out.emplace_back(GetPerpendic(path[j], norms[k], group_delta_));
|
|
349
|
+
path_out.emplace_back(path[j]); // (#405, #873, #916)
|
|
350
|
+
path_out.emplace_back(GetPerpendic(path[j], norms[j], group_delta_));
|
|
351
|
+
#endif
|
|
352
|
+
}
|
|
353
|
+
else if (cos_a > 0.999 && join_type_ != JoinType::Round)
|
|
354
|
+
{
|
|
355
|
+
// almost straight - less than 2.5 degree (#424, #482, #526 & #724)
|
|
356
|
+
DoMiter(path, j, k, cos_a);
|
|
357
|
+
}
|
|
358
|
+
else if (join_type_ == JoinType::Miter)
|
|
359
|
+
{
|
|
360
|
+
// miter unless the angle is sufficiently acute to exceed ML
|
|
361
|
+
if (cos_a > temp_lim_ - 1) DoMiter(path, j, k, cos_a);
|
|
362
|
+
else DoSquare(path, j, k);
|
|
363
|
+
}
|
|
364
|
+
else if (join_type_ == JoinType::Round)
|
|
365
|
+
DoRound(path, j, k, std::atan2(sin_a, cos_a));
|
|
366
|
+
else if ( join_type_ == JoinType::Bevel)
|
|
367
|
+
DoBevel(path, j, k);
|
|
368
|
+
else
|
|
369
|
+
DoSquare(path, j, k);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
void ClipperOffset::OffsetPolygon(Group& group, const Path64& path)
|
|
373
|
+
{
|
|
374
|
+
path_out.clear();
|
|
375
|
+
for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j)
|
|
376
|
+
OffsetPoint(group, path, j, k);
|
|
377
|
+
solution->emplace_back(path_out);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path)
|
|
381
|
+
{
|
|
382
|
+
OffsetPolygon(group, path);
|
|
383
|
+
Path64 reverse_path(path);
|
|
384
|
+
std::reverse(reverse_path.begin(), reverse_path.end());
|
|
385
|
+
|
|
386
|
+
//rebuild normals
|
|
387
|
+
std::reverse(norms.begin(), norms.end());
|
|
388
|
+
norms.emplace_back(norms[0]);
|
|
389
|
+
norms.erase(norms.begin());
|
|
390
|
+
NegatePath(norms);
|
|
391
|
+
|
|
392
|
+
OffsetPolygon(group, reverse_path);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
void ClipperOffset::OffsetOpenPath(Group& group, const Path64& path)
|
|
396
|
+
{
|
|
397
|
+
// do the line start cap
|
|
398
|
+
if (deltaCallback64_) group_delta_ = deltaCallback64_(path, norms, 0, 0);
|
|
399
|
+
|
|
400
|
+
if (std::fabs(group_delta_) <= floating_point_tolerance)
|
|
401
|
+
path_out.emplace_back(path[0]);
|
|
402
|
+
else
|
|
403
|
+
{
|
|
404
|
+
switch (end_type_)
|
|
405
|
+
{
|
|
406
|
+
case EndType::Butt:
|
|
407
|
+
DoBevel(path, 0, 0);
|
|
408
|
+
break;
|
|
409
|
+
case EndType::Round:
|
|
410
|
+
DoRound(path, 0, 0, PI);
|
|
411
|
+
break;
|
|
412
|
+
default:
|
|
413
|
+
DoSquare(path, 0, 0);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
size_t highI = path.size() - 1;
|
|
419
|
+
// offset the left side going forward
|
|
420
|
+
for (Path64::size_type j = 1, k = 0; j < highI; k = j, ++j)
|
|
421
|
+
OffsetPoint(group, path, j, k);
|
|
422
|
+
|
|
423
|
+
// reverse normals
|
|
424
|
+
for (size_t i = highI; i > 0; --i)
|
|
425
|
+
norms[i] = PointD(-norms[i - 1].x, -norms[i - 1].y);
|
|
426
|
+
norms[0] = norms[highI];
|
|
427
|
+
|
|
428
|
+
// do the line end cap
|
|
429
|
+
if (deltaCallback64_)
|
|
430
|
+
group_delta_ = deltaCallback64_(path, norms, highI, highI);
|
|
431
|
+
|
|
432
|
+
if (std::fabs(group_delta_) <= floating_point_tolerance)
|
|
433
|
+
path_out.emplace_back(path[highI]);
|
|
434
|
+
else
|
|
435
|
+
{
|
|
436
|
+
switch (end_type_)
|
|
437
|
+
{
|
|
438
|
+
case EndType::Butt:
|
|
439
|
+
DoBevel(path, highI, highI);
|
|
440
|
+
break;
|
|
441
|
+
case EndType::Round:
|
|
442
|
+
DoRound(path, highI, highI, PI);
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
DoSquare(path, highI, highI);
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (size_t j = highI -1, k = highI; j > 0; k = j, --j)
|
|
451
|
+
OffsetPoint(group, path, j, k);
|
|
452
|
+
solution->emplace_back(path_out);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
void ClipperOffset::DoGroupOffset(Group& group)
|
|
456
|
+
{
|
|
457
|
+
if (group.end_type == EndType::Polygon)
|
|
458
|
+
{
|
|
459
|
+
// a straight path (2 points) can now also be 'polygon' offset
|
|
460
|
+
// where the ends will be treated as (180 deg.) joins
|
|
461
|
+
if (!group.lowest_path_idx.has_value()) delta_ = std::abs(delta_);
|
|
462
|
+
group_delta_ = (group.is_reversed) ? -delta_ : delta_;
|
|
463
|
+
}
|
|
464
|
+
else
|
|
465
|
+
group_delta_ = std::abs(delta_);// *0.5;
|
|
466
|
+
|
|
467
|
+
double abs_delta = std::fabs(group_delta_);
|
|
468
|
+
join_type_ = group.join_type;
|
|
469
|
+
end_type_ = group.end_type;
|
|
470
|
+
|
|
471
|
+
if (group.join_type == JoinType::Round || group.end_type == EndType::Round)
|
|
472
|
+
{
|
|
473
|
+
// calculate the number of steps required to approximate a circle
|
|
474
|
+
// (see https://www.angusj.com/clipper2/Docs/Trigonometry.htm)
|
|
475
|
+
// arcTol - when arc_tolerance_ is undefined (0) then curve imprecision
|
|
476
|
+
// will be relative to the size of the offset (delta). Obviously very
|
|
477
|
+
//large offsets will almost always require much less precision.
|
|
478
|
+
double arcTol = (arc_tolerance_ > floating_point_tolerance) ?
|
|
479
|
+
std::min(abs_delta, arc_tolerance_) : abs_delta * arc_const;
|
|
480
|
+
|
|
481
|
+
double steps_per_360 = std::min(PI / std::acos(1 - arcTol / abs_delta), abs_delta * PI);
|
|
482
|
+
step_sin_ = std::sin(2 * PI / steps_per_360);
|
|
483
|
+
step_cos_ = std::cos(2 * PI / steps_per_360);
|
|
484
|
+
if (group_delta_ < 0.0) step_sin_ = -step_sin_;
|
|
485
|
+
steps_per_rad_ = steps_per_360 / (2 * PI);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
//double min_area = PI * Sqr(group_delta_);
|
|
489
|
+
Paths64::const_iterator path_in_it = group.paths_in.cbegin();
|
|
490
|
+
for ( ; path_in_it != group.paths_in.cend(); ++path_in_it)
|
|
491
|
+
{
|
|
492
|
+
Path64::size_type pathLen = path_in_it->size();
|
|
493
|
+
path_out.clear();
|
|
494
|
+
|
|
495
|
+
if (pathLen == 1) // single point
|
|
496
|
+
{
|
|
497
|
+
if (deltaCallback64_)
|
|
498
|
+
{
|
|
499
|
+
group_delta_ = deltaCallback64_(*path_in_it, norms, 0, 0);
|
|
500
|
+
if (group.is_reversed) group_delta_ = -group_delta_;
|
|
501
|
+
abs_delta = std::fabs(group_delta_);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (group_delta_ < 1) continue;
|
|
505
|
+
const Point64& pt = (*path_in_it)[0];
|
|
506
|
+
//single vertex so build a circle or square ...
|
|
507
|
+
if (group.join_type == JoinType::Round)
|
|
508
|
+
{
|
|
509
|
+
double radius = abs_delta;
|
|
510
|
+
size_t steps = steps_per_rad_ > 0 ? static_cast<size_t>(std::ceil(steps_per_rad_ * 2 * PI)) : 0; //#617
|
|
511
|
+
path_out = Ellipse(pt, radius, radius, steps);
|
|
512
|
+
#ifdef USINGZ
|
|
513
|
+
for (auto& p : path_out) p.z = pt.z;
|
|
514
|
+
#endif
|
|
515
|
+
}
|
|
516
|
+
else
|
|
517
|
+
{
|
|
518
|
+
int d = (int)std::ceil(abs_delta);
|
|
519
|
+
Rect64 r = Rect64(pt.x - d, pt.y - d, pt.x + d, pt.y + d);
|
|
520
|
+
path_out = r.AsPath();
|
|
521
|
+
#ifdef USINGZ
|
|
522
|
+
for (auto& p : path_out) p.z = pt.z;
|
|
523
|
+
#endif
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
solution->emplace_back(path_out);
|
|
527
|
+
continue;
|
|
528
|
+
} // end of offsetting a single point
|
|
529
|
+
|
|
530
|
+
if ((pathLen == 2) && (group.end_type == EndType::Joined))
|
|
531
|
+
end_type_ = (group.join_type == JoinType::Round) ?
|
|
532
|
+
EndType::Round :
|
|
533
|
+
EndType::Square;
|
|
534
|
+
|
|
535
|
+
BuildNormals(*path_in_it);
|
|
536
|
+
if (end_type_ == EndType::Polygon) OffsetPolygon(group, *path_in_it);
|
|
537
|
+
else if (end_type_ == EndType::Joined) OffsetOpenJoined(group, *path_in_it);
|
|
538
|
+
else OffsetOpenPath(group, *path_in_it);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#ifdef USINGZ
|
|
543
|
+
void ClipperOffset::ZCB(const Point64& bot1, const Point64& top1,
|
|
544
|
+
const Point64& bot2, const Point64& top2, Point64& ip)
|
|
545
|
+
{
|
|
546
|
+
if (bot1.z && ((bot1.z == bot2.z) || (bot1.z == top2.z))) ip.z = bot1.z;
|
|
547
|
+
else if (bot2.z && (bot2.z == top1.z)) ip.z = bot2.z;
|
|
548
|
+
else if (top1.z && (top1.z == top2.z)) ip.z = top1.z;
|
|
549
|
+
else if (zCallback64_) zCallback64_(bot1, top1, bot2, top2, ip);
|
|
550
|
+
}
|
|
551
|
+
#endif
|
|
552
|
+
|
|
553
|
+
size_t ClipperOffset::CalcSolutionCapacity()
|
|
554
|
+
{
|
|
555
|
+
size_t result = 0;
|
|
556
|
+
for (const Group& g : groups_)
|
|
557
|
+
result += (g.end_type == EndType::Joined) ? g.paths_in.size() * 2 : g.paths_in.size();
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
bool ClipperOffset::CheckReverseOrientation()
|
|
562
|
+
{
|
|
563
|
+
// nb: this assumes there's consistency in orientation between groups
|
|
564
|
+
bool is_reversed_orientation = false;
|
|
565
|
+
for (const Group& g : groups_)
|
|
566
|
+
if (g.end_type == EndType::Polygon)
|
|
567
|
+
{
|
|
568
|
+
is_reversed_orientation = g.is_reversed;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
return is_reversed_orientation;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
void ClipperOffset::ExecuteInternal(double delta)
|
|
575
|
+
{
|
|
576
|
+
error_code_ = 0;
|
|
577
|
+
if (groups_.size() == 0) return;
|
|
578
|
+
solution->reserve(CalcSolutionCapacity());
|
|
579
|
+
|
|
580
|
+
if (std::abs(delta) < 0.5) // ie: offset is insignificant
|
|
581
|
+
{
|
|
582
|
+
Paths64::size_type sol_size = 0;
|
|
583
|
+
for (const Group& group : groups_) sol_size += group.paths_in.size();
|
|
584
|
+
solution->reserve(sol_size);
|
|
585
|
+
for (const Group& group : groups_)
|
|
586
|
+
copy(group.paths_in.begin(), group.paths_in.end(), back_inserter(*solution));
|
|
587
|
+
}
|
|
588
|
+
else
|
|
589
|
+
{
|
|
590
|
+
|
|
591
|
+
temp_lim_ = (miter_limit_ <= 1) ?
|
|
592
|
+
2.0 :
|
|
593
|
+
2.0 / (miter_limit_ * miter_limit_);
|
|
594
|
+
|
|
595
|
+
delta_ = delta;
|
|
596
|
+
std::vector<Group>::iterator git;
|
|
597
|
+
for (git = groups_.begin(); git != groups_.end(); ++git)
|
|
598
|
+
{
|
|
599
|
+
DoGroupOffset(*git);
|
|
600
|
+
if (!error_code_) continue; // all OK
|
|
601
|
+
solution->clear();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!solution->size()) return;
|
|
606
|
+
|
|
607
|
+
bool paths_reversed = CheckReverseOrientation();
|
|
608
|
+
//clean up self-intersections ...
|
|
609
|
+
Clipper64 c;
|
|
610
|
+
c.PreserveCollinear(preserve_collinear_);
|
|
611
|
+
//the solution should retain the orientation of the input
|
|
612
|
+
c.ReverseSolution(reverse_solution_ != paths_reversed);
|
|
613
|
+
#ifdef USINGZ
|
|
614
|
+
auto fp = std::bind(&ClipperOffset::ZCB, this, std::placeholders::_1,
|
|
615
|
+
std::placeholders::_2, std::placeholders::_3,
|
|
616
|
+
std::placeholders::_4, std::placeholders::_5);
|
|
617
|
+
c.SetZCallback(fp);
|
|
618
|
+
#endif
|
|
619
|
+
c.AddSubject(*solution);
|
|
620
|
+
if (solution_tree)
|
|
621
|
+
{
|
|
622
|
+
if (paths_reversed)
|
|
623
|
+
c.Execute(ClipType::Union, FillRule::Negative, *solution_tree);
|
|
624
|
+
else
|
|
625
|
+
c.Execute(ClipType::Union, FillRule::Positive, *solution_tree);
|
|
626
|
+
}
|
|
627
|
+
else
|
|
628
|
+
{
|
|
629
|
+
if (paths_reversed)
|
|
630
|
+
c.Execute(ClipType::Union, FillRule::Negative, *solution);
|
|
631
|
+
else
|
|
632
|
+
c.Execute(ClipType::Union, FillRule::Positive, *solution);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
void ClipperOffset::Execute(double delta, Paths64& paths64)
|
|
637
|
+
{
|
|
638
|
+
paths64.clear();
|
|
639
|
+
solution = &paths64;
|
|
640
|
+
solution_tree = nullptr;
|
|
641
|
+
ExecuteInternal(delta);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
void ClipperOffset::Execute(double delta, PolyTree64& polytree)
|
|
646
|
+
{
|
|
647
|
+
polytree.Clear();
|
|
648
|
+
solution_tree = &polytree;
|
|
649
|
+
solution = new Paths64();
|
|
650
|
+
ExecuteInternal(delta);
|
|
651
|
+
delete solution;
|
|
652
|
+
solution = nullptr;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
void ClipperOffset::Execute(DeltaCallback64 delta_cb, Paths64& paths)
|
|
656
|
+
{
|
|
657
|
+
deltaCallback64_ = delta_cb;
|
|
658
|
+
Execute(1.0, paths);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
} // namespace
|