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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +165 -0
  4. data/examples/larb_transform.rb +17 -0
  5. data/examples/rugl_mesh.rb +33 -0
  6. data/examples/svg_boolean.rb +28 -0
  7. data/ext/clipper2/CMakeLists.txt +27 -0
  8. data/ext/clipper2/clipper2_native.cpp +2 -0
  9. data/ext/clipper2/extconf.rb +27 -0
  10. data/ext/clipper2/generate_bindings.rb +73 -0
  11. data/ext/clipper2/memory_check.cpp +48 -0
  12. data/ext/clipper2/vendor/LICENSE +23 -0
  13. data/ext/clipper2/vendor/UPSTREAM.md +11 -0
  14. data/ext/clipper2/vendor/include/clipper2/clipper.core.h +1159 -0
  15. data/ext/clipper2/vendor/include/clipper2/clipper.engine.h +635 -0
  16. data/ext/clipper2/vendor/include/clipper2/clipper.export.h +851 -0
  17. data/ext/clipper2/vendor/include/clipper2/clipper.h +796 -0
  18. data/ext/clipper2/vendor/include/clipper2/clipper.minkowski.h +117 -0
  19. data/ext/clipper2/vendor/include/clipper2/clipper.offset.h +125 -0
  20. data/ext/clipper2/vendor/include/clipper2/clipper.rectclip.h +80 -0
  21. data/ext/clipper2/vendor/include/clipper2/clipper.triangulation.h +27 -0
  22. data/ext/clipper2/vendor/include/clipper2/clipper.version.h +6 -0
  23. data/ext/clipper2/vendor/src/clipper.engine.cpp +3152 -0
  24. data/ext/clipper2/vendor/src/clipper.offset.cpp +661 -0
  25. data/ext/clipper2/vendor/src/clipper.rectclip.cpp +1027 -0
  26. data/ext/clipper2/vendor/src/clipper.triangulation.cpp +1221 -0
  27. data/lib/clipper2/api.rb +45 -0
  28. data/lib/clipper2/c_paths64.rb +80 -0
  29. data/lib/clipper2/earcut/engine.rb +191 -0
  30. data/lib/clipper2/earcut/geometry.rb +100 -0
  31. data/lib/clipper2/earcut/node.rb +18 -0
  32. data/lib/clipper2/earcut/ring.rb +94 -0
  33. data/lib/clipper2/earcut.rb +18 -0
  34. data/lib/clipper2/errors.rb +19 -0
  35. data/lib/clipper2/generated.rb +62 -0
  36. data/lib/clipper2/native.rb +38 -0
  37. data/lib/clipper2/native_memory.rb +64 -0
  38. data/lib/clipper2/operations.rb +206 -0
  39. data/lib/clipper2/path.rb +171 -0
  40. data/lib/clipper2/path_simplifier.rb +61 -0
  41. data/lib/clipper2/paths.rb +130 -0
  42. data/lib/clipper2/quantizer.rb +48 -0
  43. data/lib/clipper2/triangulation.rb +103 -0
  44. data/lib/clipper2/version.rb +5 -0
  45. data/lib/clipper2.rb +34 -0
  46. data/licenses/CLIPPER2-BSL-1.0.txt +23 -0
  47. data/licenses/EARCUT-ISC.txt +15 -0
  48. 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