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/LICENSE +24 -0
- package/README.md +67 -0
- package/dist/Clipper.d.ts +114 -0
- package/dist/Clipper.d.ts.map +1 -0
- package/dist/Clipper.js +1134 -0
- package/dist/Clipper.js.map +1 -0
- package/dist/Core.d.ts +150 -0
- package/dist/Core.d.ts.map +1 -0
- package/dist/Core.js +645 -0
- package/dist/Core.js.map +1 -0
- package/dist/Engine.d.ts +337 -0
- package/dist/Engine.d.ts.map +1 -0
- package/dist/Engine.js +2972 -0
- package/dist/Engine.js.map +1 -0
- package/dist/Minkowski.d.ts +16 -0
- package/dist/Minkowski.d.ts.map +1 -0
- package/dist/Minkowski.js +131 -0
- package/dist/Minkowski.js.map +1 -0
- package/dist/Offset.d.ts +85 -0
- package/dist/Offset.d.ts.map +1 -0
- package/dist/Offset.js +649 -0
- package/dist/Offset.js.map +1 -0
- package/dist/RectClip.d.ts +80 -0
- package/dist/RectClip.d.ts.map +1 -0
- package/dist/RectClip.js +1009 -0
- package/dist/RectClip.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/Clipper.ts +1106 -0
- package/src/Core.ts +683 -0
- package/src/Engine.ts +3116 -0
- package/src/Minkowski.ts +153 -0
- package/src/Offset.ts +711 -0
- package/src/RectClip.ts +1028 -0
- package/src/index.ts +146 -0
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
|