clipper2-ts 1.5.4-3.9a869ba

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.
package/dist/Offset.js ADDED
@@ -0,0 +1,649 @@
1
+ "use strict";
2
+ /*******************************************************************************
3
+ * Author : Angus Johnson *
4
+ * Date : 11 October 2025 *
5
+ * Website : https://www.angusj.com *
6
+ * Copyright : Angus Johnson 2010-2025 *
7
+ * Purpose : Path Offset (Inflate/Shrink) *
8
+ * License : https://www.boost.org/LICENSE_1_0.txt *
9
+ *******************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ClipperOffset = exports.EndType = exports.JoinType = void 0;
12
+ const Core_1 = require("./Core");
13
+ const Engine_1 = require("./Engine");
14
+ var JoinType;
15
+ (function (JoinType) {
16
+ JoinType[JoinType["Miter"] = 0] = "Miter";
17
+ JoinType[JoinType["Square"] = 1] = "Square";
18
+ JoinType[JoinType["Bevel"] = 2] = "Bevel";
19
+ JoinType[JoinType["Round"] = 3] = "Round";
20
+ })(JoinType || (exports.JoinType = JoinType = {}));
21
+ var EndType;
22
+ (function (EndType) {
23
+ EndType[EndType["Polygon"] = 0] = "Polygon";
24
+ EndType[EndType["Joined"] = 1] = "Joined";
25
+ EndType[EndType["Butt"] = 2] = "Butt";
26
+ EndType[EndType["Square"] = 3] = "Square";
27
+ EndType[EndType["Round"] = 4] = "Round";
28
+ })(EndType || (exports.EndType = EndType = {}));
29
+ class Group {
30
+ constructor(paths, joinType, endType = EndType.Polygon) {
31
+ this.joinType = joinType;
32
+ this.endType = endType;
33
+ const isJoined = (endType === EndType.Polygon) || (endType === EndType.Joined);
34
+ this.inPaths = [];
35
+ for (const path of paths) {
36
+ this.inPaths.push(ClipperOffset.stripDuplicates(path, isJoined));
37
+ }
38
+ if (endType === EndType.Polygon) {
39
+ const lowestInfo = ClipperOffset.getLowestPathInfo(this.inPaths);
40
+ this.lowestPathIdx = lowestInfo.idx;
41
+ // the lowermost path must be an outer path, so if its orientation is negative,
42
+ // then flag that the whole group is 'reversed' (will negate delta etc.)
43
+ // as this is much more efficient than reversing every path.
44
+ this.pathsReversed = (this.lowestPathIdx >= 0) && lowestInfo.isNegArea;
45
+ }
46
+ else {
47
+ this.lowestPathIdx = -1;
48
+ this.pathsReversed = false;
49
+ }
50
+ }
51
+ }
52
+ class ClipperOffset {
53
+ constructor(miterLimit = 2.0, arcTolerance = 0.0, preserveCollinear = false, reverseSolution = false) {
54
+ this.groupList = [];
55
+ this.pathOut = [];
56
+ this.normals = [];
57
+ this.solution = [];
58
+ this.solutionTree = null;
59
+ this.groupDelta = 0; //*0.5 for open paths; *-1.0 for negative areas
60
+ this.delta = 0;
61
+ this.mitLimSqr = 0;
62
+ this.stepsPerRad = 0;
63
+ this.stepSin = 0;
64
+ this.stepCos = 0;
65
+ this.joinType = JoinType.Bevel;
66
+ this.endType = EndType.Polygon;
67
+ this.arcTolerance = 0;
68
+ this.mergeGroups = true;
69
+ this.miterLimit = 2.0;
70
+ this.preserveCollinear = false;
71
+ this.reverseSolution = false;
72
+ this.deltaCallback = null;
73
+ this.miterLimit = miterLimit;
74
+ this.arcTolerance = arcTolerance;
75
+ this.mergeGroups = true;
76
+ this.preserveCollinear = preserveCollinear;
77
+ this.reverseSolution = reverseSolution;
78
+ }
79
+ clear() {
80
+ this.groupList.length = 0;
81
+ }
82
+ addPath(path, joinType, endType) {
83
+ if (path.length === 0)
84
+ return;
85
+ const pp = [path];
86
+ this.addPaths(pp, joinType, endType);
87
+ }
88
+ addPaths(paths, joinType, endType) {
89
+ if (paths.length === 0)
90
+ return;
91
+ this.groupList.push(new Group(paths, joinType, endType));
92
+ }
93
+ calcSolutionCapacity() {
94
+ let result = 0;
95
+ for (const g of this.groupList) {
96
+ result += (g.endType === EndType.Joined) ? g.inPaths.length * 2 : g.inPaths.length;
97
+ }
98
+ return result;
99
+ }
100
+ checkPathsReversed() {
101
+ let result = false;
102
+ for (const g of this.groupList) {
103
+ if (g.endType === EndType.Polygon) {
104
+ result = g.pathsReversed;
105
+ break;
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+ executeInternal(delta) {
111
+ if (this.groupList.length === 0)
112
+ return;
113
+ // make sure the offset delta is significant
114
+ if (Math.abs(delta) < 0.5) {
115
+ for (const group of this.groupList) {
116
+ for (const path of group.inPaths) {
117
+ this.solution.push(path);
118
+ }
119
+ }
120
+ return;
121
+ }
122
+ this.delta = delta;
123
+ this.mitLimSqr = (this.miterLimit <= 1 ?
124
+ 2.0 : 2.0 / ClipperOffset.sqr(this.miterLimit));
125
+ for (const group of this.groupList) {
126
+ this.doGroupOffset(group);
127
+ }
128
+ if (this.groupList.length === 0)
129
+ return;
130
+ const pathsReversed = this.checkPathsReversed();
131
+ const fillRule = pathsReversed ? Core_1.FillRule.Negative : Core_1.FillRule.Positive;
132
+ // clean up self-intersections ...
133
+ const c = new Engine_1.Clipper64();
134
+ c.preserveCollinear = this.preserveCollinear;
135
+ c.reverseSolution = this.reverseSolution !== pathsReversed;
136
+ c.addSubject(this.solution);
137
+ if (this.solutionTree !== null) {
138
+ c.execute(Core_1.ClipType.Union, fillRule, this.solutionTree);
139
+ }
140
+ else {
141
+ c.execute(Core_1.ClipType.Union, fillRule, this.solution);
142
+ }
143
+ }
144
+ execute(delta, solutionOrTree) {
145
+ if (Array.isArray(solutionOrTree)) {
146
+ // Paths64 version
147
+ const solution = solutionOrTree;
148
+ solution.length = 0;
149
+ this.solution = solution;
150
+ this.executeInternal(delta);
151
+ }
152
+ else {
153
+ // PolyTree64 version
154
+ const solutionTree = solutionOrTree;
155
+ solutionTree.clear();
156
+ this.solutionTree = solutionTree;
157
+ this.solution = [];
158
+ this.executeInternal(delta);
159
+ }
160
+ }
161
+ executeWithCallback(deltaCallback, solution) {
162
+ this.deltaCallback = deltaCallback;
163
+ this.execute(1.0, solution);
164
+ }
165
+ static getUnitNormal(pt1, pt2) {
166
+ const dx = (pt2.x - pt1.x);
167
+ const dy = (pt2.y - pt1.y);
168
+ if ((dx === 0) && (dy === 0))
169
+ return { x: 0, y: 0 };
170
+ const f = 1.0 / Math.sqrt(dx * dx + dy * dy);
171
+ return {
172
+ x: dy * f,
173
+ y: -dx * f
174
+ };
175
+ }
176
+ static getLowestPathInfo(paths) {
177
+ let idx = -1;
178
+ let isNegArea = false;
179
+ let botPt = { x: Number.MAX_SAFE_INTEGER, y: Number.MIN_SAFE_INTEGER };
180
+ for (let i = 0; i < paths.length; ++i) {
181
+ let a = Number.MAX_VALUE;
182
+ for (const pt of paths[i]) {
183
+ if ((pt.y < botPt.y) || ((pt.y === botPt.y) && (pt.x >= botPt.x)))
184
+ continue;
185
+ if (a === Number.MAX_VALUE) {
186
+ a = ClipperOffset.area(paths[i]);
187
+ if (a === 0)
188
+ break; // invalid closed path so break from inner loop
189
+ isNegArea = a < 0;
190
+ }
191
+ idx = i;
192
+ botPt.x = pt.x;
193
+ botPt.y = pt.y;
194
+ }
195
+ }
196
+ return { idx, isNegArea };
197
+ }
198
+ static translatePoint(pt, dx, dy) {
199
+ return { x: pt.x + dx, y: pt.y + dy };
200
+ }
201
+ static reflectPoint(pt, pivot) {
202
+ return { x: pivot.x + (pivot.x - pt.x), y: pivot.y + (pivot.y - pt.y) };
203
+ }
204
+ static almostZero(value, epsilon = 0.001) {
205
+ return Math.abs(value) < epsilon;
206
+ }
207
+ static hypotenuse(x, y) {
208
+ return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
209
+ }
210
+ static normalizeVector(vec) {
211
+ const h = ClipperOffset.hypotenuse(vec.x, vec.y);
212
+ if (ClipperOffset.almostZero(h))
213
+ return { x: 0, y: 0 };
214
+ const inverseHypot = 1 / h;
215
+ return { x: vec.x * inverseHypot, y: vec.y * inverseHypot };
216
+ }
217
+ static getAvgUnitVector(vec1, vec2) {
218
+ return ClipperOffset.normalizeVector({ x: vec1.x + vec2.x, y: vec1.y + vec2.y });
219
+ }
220
+ static intersectPoint(pt1a, pt1b, pt2a, pt2b) {
221
+ if (Core_1.InternalClipper.isAlmostZero(pt1a.x - pt1b.x)) { // vertical
222
+ if (Core_1.InternalClipper.isAlmostZero(pt2a.x - pt2b.x))
223
+ return { x: 0, y: 0 };
224
+ const m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x);
225
+ const b2 = pt2a.y - m2 * pt2a.x;
226
+ return { x: pt1a.x, y: m2 * pt1a.x + b2 };
227
+ }
228
+ if (Core_1.InternalClipper.isAlmostZero(pt2a.x - pt2b.x)) { // vertical
229
+ const m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x);
230
+ const b1 = pt1a.y - m1 * pt1a.x;
231
+ return { x: pt2a.x, y: m1 * pt2a.x + b1 };
232
+ }
233
+ else {
234
+ const m1 = (pt1b.y - pt1a.y) / (pt1b.x - pt1a.x);
235
+ const b1 = pt1a.y - m1 * pt1a.x;
236
+ const m2 = (pt2b.y - pt2a.y) / (pt2b.x - pt2a.x);
237
+ const b2 = pt2a.y - m2 * pt2a.x;
238
+ if (Core_1.InternalClipper.isAlmostZero(m1 - m2))
239
+ return { x: 0, y: 0 };
240
+ const x = (b2 - b1) / (m1 - m2);
241
+ return { x: x, y: m1 * x + b1 };
242
+ }
243
+ }
244
+ getPerpendic(pt, norm) {
245
+ return {
246
+ x: Math.round(pt.x + norm.x * this.groupDelta),
247
+ y: Math.round(pt.y + norm.y * this.groupDelta)
248
+ };
249
+ }
250
+ getPerpendicD(pt, norm) {
251
+ return {
252
+ x: pt.x + norm.x * this.groupDelta,
253
+ y: pt.y + norm.y * this.groupDelta
254
+ };
255
+ }
256
+ doBevel(path, j, k) {
257
+ let pt1, pt2;
258
+ if (j === k) {
259
+ const absDelta = Math.abs(this.groupDelta);
260
+ pt1 = {
261
+ x: Math.round(path[j].x - absDelta * this.normals[j].x),
262
+ y: Math.round(path[j].y - absDelta * this.normals[j].y)
263
+ };
264
+ pt2 = {
265
+ x: Math.round(path[j].x + absDelta * this.normals[j].x),
266
+ y: Math.round(path[j].y + absDelta * this.normals[j].y)
267
+ };
268
+ }
269
+ else {
270
+ pt1 = {
271
+ x: Math.round(path[j].x + this.groupDelta * this.normals[k].x),
272
+ y: Math.round(path[j].y + this.groupDelta * this.normals[k].y)
273
+ };
274
+ pt2 = {
275
+ x: Math.round(path[j].x + this.groupDelta * this.normals[j].x),
276
+ y: Math.round(path[j].y + this.groupDelta * this.normals[j].y)
277
+ };
278
+ }
279
+ this.pathOut.push(pt1);
280
+ this.pathOut.push(pt2);
281
+ }
282
+ doSquare(path, j, k) {
283
+ let vec;
284
+ if (j === k) {
285
+ vec = { x: this.normals[j].y, y: -this.normals[j].x };
286
+ }
287
+ else {
288
+ vec = ClipperOffset.getAvgUnitVector({ x: -this.normals[k].y, y: this.normals[k].x }, { x: this.normals[j].y, y: -this.normals[j].x });
289
+ }
290
+ const absDelta = Math.abs(this.groupDelta);
291
+ // now offset the original vertex delta units along unit vector
292
+ let ptQ = { x: path[j].x, y: path[j].y };
293
+ ptQ = ClipperOffset.translatePoint(ptQ, absDelta * vec.x, absDelta * vec.y);
294
+ // get perpendicular vertices
295
+ const pt1 = ClipperOffset.translatePoint(ptQ, this.groupDelta * vec.y, this.groupDelta * -vec.x);
296
+ const pt2 = ClipperOffset.translatePoint(ptQ, this.groupDelta * -vec.y, this.groupDelta * vec.x);
297
+ // get 2 vertices along one edge offset
298
+ const pt3 = this.getPerpendicD(path[k], this.normals[k]);
299
+ if (j === k) {
300
+ const pt4 = {
301
+ x: pt3.x + vec.x * this.groupDelta,
302
+ y: pt3.y + vec.y * this.groupDelta
303
+ };
304
+ const pt = ClipperOffset.intersectPoint(pt1, pt2, pt3, pt4);
305
+ //get the second intersect point through reflecion
306
+ this.pathOut.push(Core_1.Point64Utils.fromPointD(ClipperOffset.reflectPoint(pt, ptQ)));
307
+ this.pathOut.push(Core_1.Point64Utils.fromPointD(pt));
308
+ }
309
+ else {
310
+ const pt4 = this.getPerpendicD(path[j], this.normals[k]);
311
+ const pt = ClipperOffset.intersectPoint(pt1, pt2, pt3, pt4);
312
+ this.pathOut.push(Core_1.Point64Utils.fromPointD(pt));
313
+ //get the second intersect point through reflecion
314
+ this.pathOut.push(Core_1.Point64Utils.fromPointD(ClipperOffset.reflectPoint(pt, ptQ)));
315
+ }
316
+ }
317
+ doMiter(path, j, k, cosA) {
318
+ const q = this.groupDelta / (cosA + 1);
319
+ this.pathOut.push({
320
+ x: Math.round(path[j].x + (this.normals[k].x + this.normals[j].x) * q),
321
+ y: Math.round(path[j].y + (this.normals[k].y + this.normals[j].y) * q)
322
+ });
323
+ }
324
+ doRound(path, j, k, angle) {
325
+ if (this.deltaCallback !== null) {
326
+ // when deltaCallback is assigned, groupDelta won't be constant,
327
+ // so we'll need to do the following calculations for *every* vertex.
328
+ const absDelta = Math.abs(this.groupDelta);
329
+ const arcTol = this.arcTolerance > 0.01 ? this.arcTolerance : absDelta * ClipperOffset.arc_const;
330
+ const stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
331
+ this.stepSin = Math.sin((2 * Math.PI) / stepsPer360);
332
+ this.stepCos = Math.cos((2 * Math.PI) / stepsPer360);
333
+ if (this.groupDelta < 0.0)
334
+ this.stepSin = -this.stepSin;
335
+ this.stepsPerRad = stepsPer360 / (2 * Math.PI);
336
+ }
337
+ const pt = path[j];
338
+ let offsetVec = { x: this.normals[k].x * this.groupDelta, y: this.normals[k].y * this.groupDelta };
339
+ if (j === k)
340
+ Core_1.PointDUtils.negate(offsetVec);
341
+ this.pathOut.push({
342
+ x: Math.round(pt.x + offsetVec.x),
343
+ y: Math.round(pt.y + offsetVec.y)
344
+ });
345
+ const steps = Math.ceil(this.stepsPerRad * Math.abs(angle));
346
+ for (let i = 1; i < steps; i++) { // ie 1 less than steps
347
+ offsetVec = {
348
+ x: offsetVec.x * this.stepCos - this.stepSin * offsetVec.y,
349
+ y: offsetVec.x * this.stepSin + offsetVec.y * this.stepCos
350
+ };
351
+ this.pathOut.push({
352
+ x: Math.round(pt.x + offsetVec.x),
353
+ y: Math.round(pt.y + offsetVec.y)
354
+ });
355
+ }
356
+ this.pathOut.push(this.getPerpendic(path[j], this.normals[j]));
357
+ }
358
+ buildNormals(path) {
359
+ const cnt = path.length;
360
+ this.normals.length = 0;
361
+ if (cnt === 0)
362
+ return;
363
+ for (let i = 0; i < cnt - 1; i++) {
364
+ this.normals.push(ClipperOffset.getUnitNormal(path[i], path[i + 1]));
365
+ }
366
+ this.normals.push(ClipperOffset.getUnitNormal(path[cnt - 1], path[0]));
367
+ }
368
+ offsetPoint(group, path, j, k) {
369
+ if (Core_1.Point64Utils.equals(path[j], path[k]))
370
+ return;
371
+ // Let A = change in angle where edges join
372
+ // A == 0: ie no change in angle (flat join)
373
+ // A == PI: edges 'spike'
374
+ // sin(A) < 0: right turning
375
+ // cos(A) < 0: change in angle is more than 90 degree
376
+ let sinA = Core_1.InternalClipper.crossProductD(this.normals[j], this.normals[k]);
377
+ const cosA = Core_1.InternalClipper.dotProductD(this.normals[j], this.normals[k]);
378
+ if (sinA > 1.0)
379
+ sinA = 1.0;
380
+ else if (sinA < -1.0)
381
+ sinA = -1.0;
382
+ if (this.deltaCallback !== null) {
383
+ this.groupDelta = this.deltaCallback(path, this.normals, j, k);
384
+ if (group.pathsReversed)
385
+ this.groupDelta = -this.groupDelta;
386
+ }
387
+ if (Math.abs(this.groupDelta) < ClipperOffset.Tolerance) {
388
+ this.pathOut.push(path[j]);
389
+ return;
390
+ }
391
+ if (cosA > -0.999 && (sinA * this.groupDelta < 0)) { // test for concavity first (#593)
392
+ // is concave
393
+ // by far the simplest way to construct concave joins, especially those joining very
394
+ // short segments, is to insert 3 points that produce negative regions. These regions
395
+ // will be removed later by the finishing union operation. This is also the best way
396
+ // to ensure that path reversals (ie over-shrunk paths) are removed.
397
+ this.pathOut.push(this.getPerpendic(path[j], this.normals[k]));
398
+ this.pathOut.push(path[j]); // (#405, #873, #916)
399
+ this.pathOut.push(this.getPerpendic(path[j], this.normals[j]));
400
+ }
401
+ else if ((cosA > 0.999) && (this.joinType !== JoinType.Round)) {
402
+ // almost straight - less than 2.5 degree (#424, #482, #526 & #724)
403
+ this.doMiter(path, j, k, cosA);
404
+ }
405
+ else {
406
+ switch (this.joinType) {
407
+ // miter unless the angle is sufficiently acute to exceed ML
408
+ case JoinType.Miter:
409
+ if (cosA > this.mitLimSqr - 1) {
410
+ this.doMiter(path, j, k, cosA);
411
+ }
412
+ else {
413
+ this.doSquare(path, j, k);
414
+ }
415
+ break;
416
+ case JoinType.Round:
417
+ this.doRound(path, j, k, Math.atan2(sinA, cosA));
418
+ break;
419
+ case JoinType.Bevel:
420
+ this.doBevel(path, j, k);
421
+ break;
422
+ default:
423
+ this.doSquare(path, j, k);
424
+ break;
425
+ }
426
+ }
427
+ }
428
+ offsetPolygon(group, path) {
429
+ this.pathOut = [];
430
+ const cnt = path.length;
431
+ let prev = cnt - 1;
432
+ for (let i = 0; i < cnt; i++) {
433
+ this.offsetPoint(group, path, i, prev);
434
+ prev = i;
435
+ }
436
+ this.solution.push([...this.pathOut]);
437
+ }
438
+ offsetOpenJoined(group, path) {
439
+ this.offsetPolygon(group, path);
440
+ const reversePath = [...path].reverse();
441
+ this.buildNormals(reversePath);
442
+ this.offsetPolygon(group, reversePath);
443
+ }
444
+ offsetOpenPath(group, path) {
445
+ this.pathOut = [];
446
+ const highI = path.length - 1;
447
+ if (this.deltaCallback !== null) {
448
+ this.groupDelta = this.deltaCallback(path, this.normals, 0, 0);
449
+ }
450
+ // do the line start cap
451
+ if (Math.abs(this.groupDelta) < ClipperOffset.Tolerance) {
452
+ this.pathOut.push(path[0]);
453
+ }
454
+ else {
455
+ switch (this.endType) {
456
+ case EndType.Butt:
457
+ this.doBevel(path, 0, 0);
458
+ break;
459
+ case EndType.Round:
460
+ this.doRound(path, 0, 0, Math.PI);
461
+ break;
462
+ default:
463
+ this.doSquare(path, 0, 0);
464
+ break;
465
+ }
466
+ }
467
+ // offset the left side going forward
468
+ for (let i = 1, k = 0; i < highI; i++) {
469
+ this.offsetPoint(group, path, i, k);
470
+ k = i;
471
+ }
472
+ // reverse normals ...
473
+ for (let i = highI; i > 0; i--) {
474
+ this.normals[i] = { x: -this.normals[i - 1].x, y: -this.normals[i - 1].y };
475
+ }
476
+ this.normals[0] = this.normals[highI];
477
+ if (this.deltaCallback !== null) {
478
+ this.groupDelta = this.deltaCallback(path, this.normals, highI, highI);
479
+ }
480
+ // do the line end cap
481
+ if (Math.abs(this.groupDelta) < ClipperOffset.Tolerance) {
482
+ this.pathOut.push(path[highI]);
483
+ }
484
+ else {
485
+ switch (this.endType) {
486
+ case EndType.Butt:
487
+ this.doBevel(path, highI, highI);
488
+ break;
489
+ case EndType.Round:
490
+ this.doRound(path, highI, highI, Math.PI);
491
+ break;
492
+ default:
493
+ this.doSquare(path, highI, highI);
494
+ break;
495
+ }
496
+ }
497
+ // offset the left side going back
498
+ for (let i = highI - 1, k = highI; i > 0; i--) {
499
+ this.offsetPoint(group, path, i, k);
500
+ k = i;
501
+ }
502
+ this.solution.push([...this.pathOut]);
503
+ }
504
+ doGroupOffset(group) {
505
+ if (group.endType === EndType.Polygon) {
506
+ // a straight path (2 points) can now also be 'polygon' offset
507
+ // where the ends will be treated as (180 deg.) joins
508
+ if (group.lowestPathIdx < 0)
509
+ this.delta = Math.abs(this.delta);
510
+ this.groupDelta = group.pathsReversed ? -this.delta : this.delta;
511
+ }
512
+ else {
513
+ this.groupDelta = Math.abs(this.delta);
514
+ }
515
+ const absDelta = Math.abs(this.groupDelta);
516
+ this.joinType = group.joinType;
517
+ this.endType = group.endType;
518
+ if (group.joinType === JoinType.Round || group.endType === EndType.Round) {
519
+ const arcTol = this.arcTolerance > 0.01 ? this.arcTolerance : absDelta * ClipperOffset.arc_const;
520
+ const stepsPer360 = Math.PI / Math.acos(1 - arcTol / absDelta);
521
+ this.stepSin = Math.sin((2 * Math.PI) / stepsPer360);
522
+ this.stepCos = Math.cos((2 * Math.PI) / stepsPer360);
523
+ if (this.groupDelta < 0.0)
524
+ this.stepSin = -this.stepSin;
525
+ this.stepsPerRad = stepsPer360 / (2 * Math.PI);
526
+ }
527
+ for (const pathIn of group.inPaths) {
528
+ this.pathOut = [];
529
+ const cnt = pathIn.length;
530
+ if (cnt === 1) {
531
+ // single point
532
+ const pt = pathIn[0];
533
+ if (this.deltaCallback !== null) {
534
+ this.groupDelta = this.deltaCallback(pathIn, this.normals, 0, 0);
535
+ if (group.pathsReversed)
536
+ this.groupDelta = -this.groupDelta;
537
+ }
538
+ // single vertex so build a circle or square ...
539
+ if (group.endType === EndType.Round) {
540
+ const steps = Math.ceil(this.stepsPerRad * 2 * Math.PI);
541
+ this.pathOut = ClipperOffset.ellipse(pt, Math.abs(this.groupDelta), Math.abs(this.groupDelta), steps);
542
+ }
543
+ else {
544
+ const d = Math.ceil(Math.abs(this.groupDelta));
545
+ const r = { left: pt.x - d, top: pt.y - d, right: pt.x + d, bottom: pt.y + d };
546
+ this.pathOut = [
547
+ { x: r.left, y: r.top },
548
+ { x: r.right, y: r.top },
549
+ { x: r.right, y: r.bottom },
550
+ { x: r.left, y: r.bottom }
551
+ ];
552
+ }
553
+ this.solution.push([...this.pathOut]);
554
+ continue; // end of offsetting a single point
555
+ }
556
+ if (cnt === 2 && group.endType === EndType.Joined) {
557
+ this.endType = (group.joinType === JoinType.Round) ?
558
+ EndType.Round :
559
+ EndType.Square;
560
+ }
561
+ this.buildNormals(pathIn);
562
+ switch (this.endType) {
563
+ case EndType.Polygon:
564
+ this.offsetPolygon(group, pathIn);
565
+ break;
566
+ case EndType.Joined:
567
+ this.offsetOpenJoined(group, pathIn);
568
+ break;
569
+ default:
570
+ this.offsetOpenPath(group, pathIn);
571
+ break;
572
+ }
573
+ }
574
+ }
575
+ static stripDuplicates(path, isClosedPath) {
576
+ const cnt = path.length;
577
+ const result = [];
578
+ if (cnt === 0)
579
+ return result;
580
+ let lastPt = path[0];
581
+ result.push(lastPt);
582
+ for (let i = 1; i < cnt; i++) {
583
+ if (!Core_1.Point64Utils.equals(lastPt, path[i])) {
584
+ lastPt = path[i];
585
+ result.push(lastPt);
586
+ }
587
+ }
588
+ if (isClosedPath && Core_1.Point64Utils.equals(lastPt, result[0])) {
589
+ result.pop();
590
+ }
591
+ return result;
592
+ }
593
+ static area(path) {
594
+ // https://en.wikipedia.org/wiki/Shoelace_formula
595
+ let a = 0.0;
596
+ const cnt = path.length;
597
+ if (cnt < 3)
598
+ return 0.0;
599
+ let prevPt = path[cnt - 1];
600
+ for (const pt of path) {
601
+ a += (prevPt.y + pt.y) * (prevPt.x - pt.x);
602
+ prevPt = pt;
603
+ }
604
+ return a * 0.5;
605
+ }
606
+ static sqr(val) {
607
+ return val * val;
608
+ }
609
+ static ellipse(center, radiusX, radiusY = 0, steps = 0) {
610
+ if (radiusX <= 0)
611
+ return [];
612
+ if (radiusY <= 0)
613
+ radiusY = radiusX;
614
+ if (steps <= 2) {
615
+ steps = Math.ceil(Math.PI * Math.sqrt((radiusX + radiusY) / 2));
616
+ }
617
+ const si = Math.sin(2 * Math.PI / steps);
618
+ const co = Math.cos(2 * Math.PI / steps);
619
+ let dx = co;
620
+ let dy = si;
621
+ const result = [{ x: Math.round(center.x + radiusX), y: center.y }];
622
+ for (let i = 1; i < steps; ++i) {
623
+ result.push({
624
+ x: Math.round(center.x + radiusX * dx),
625
+ y: Math.round(center.y + radiusY * dy)
626
+ });
627
+ const x = dx * co - dy * si;
628
+ dy = dy * co + dx * si;
629
+ dx = x;
630
+ }
631
+ return result;
632
+ }
633
+ }
634
+ exports.ClipperOffset = ClipperOffset;
635
+ ClipperOffset.Tolerance = 1.0E-12;
636
+ // Clipper2 approximates arcs by using series of relatively short straight
637
+ //line segments. And logically, shorter line segments will produce better arc
638
+ // approximations. But very short segments can degrade performance, usually
639
+ // with little or no discernable improvement in curve quality. Very short
640
+ // segments can even detract from curve quality, due to the effects of integer
641
+ // rounding. Since there isn't an optimal number of line segments for any given
642
+ // arc radius (that perfectly balances curve approximation with performance),
643
+ // arc tolerance is user defined. Nevertheless, when the user doesn't define
644
+ // an arc tolerance (ie leaves alone the 0 default value), the calculated
645
+ // default arc tolerance (offset_radius / 500) generally produces good (smooth)
646
+ // arc approximations without producing excessively small segment lengths.
647
+ // See also: https://www.angusj.com/clipper2/Docs/Trigonometry.htm
648
+ ClipperOffset.arc_const = 0.002; // <-- 1/500
649
+ //# sourceMappingURL=Offset.js.map