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/src/Engine.ts
ADDED
|
@@ -0,0 +1,3116 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
* Author : Angus Johnson *
|
|
3
|
+
* Date : 11 October 2025 *
|
|
4
|
+
* Website : https://www.angusj.com *
|
|
5
|
+
* Copyright : Angus Johnson 2010-2025 *
|
|
6
|
+
* Purpose : This is the main polygon clipping module *
|
|
7
|
+
* License : https://www.boost.org/LICENSE_1_0.txt *
|
|
8
|
+
*******************************************************************************/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Point64, PointD, Path64, PathD, Paths64, PathsD, Rect64, RectD,
|
|
12
|
+
ClipType, PathType, FillRule, PointInPolygonResult,
|
|
13
|
+
InternalClipper, Point64Utils, Rect64Utils
|
|
14
|
+
} from './Core';
|
|
15
|
+
|
|
16
|
+
// Vertex: a pre-clipping data structure. It is used to separate polygons
|
|
17
|
+
// into ascending and descending 'bounds' (or sides) that start at local
|
|
18
|
+
// minima and ascend to a local maxima, before descending again.
|
|
19
|
+
export enum VertexFlags {
|
|
20
|
+
None = 0,
|
|
21
|
+
OpenStart = 1,
|
|
22
|
+
OpenEnd = 2,
|
|
23
|
+
LocalMax = 4,
|
|
24
|
+
LocalMin = 8
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// C# keeps scanlines in a sorted list; here we use a heap to avoid O(n) splices.
|
|
28
|
+
class ScanlineHeap {
|
|
29
|
+
private readonly data: number[] = [];
|
|
30
|
+
|
|
31
|
+
push(value: number): void {
|
|
32
|
+
this.data.push(value);
|
|
33
|
+
this.siftUp(this.data.length - 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pop(): number | null {
|
|
37
|
+
if (this.data.length === 0) return null;
|
|
38
|
+
const max = this.data[0];
|
|
39
|
+
const last = this.data.pop()!;
|
|
40
|
+
if (this.data.length > 0) {
|
|
41
|
+
this.data[0] = last;
|
|
42
|
+
this.siftDown(0);
|
|
43
|
+
}
|
|
44
|
+
return max;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clear(): void {
|
|
48
|
+
this.data.length = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private siftUp(index: number): void {
|
|
52
|
+
while (index > 0) {
|
|
53
|
+
const parent = (index - 1) >> 1;
|
|
54
|
+
if (this.data[parent] >= this.data[index]) break;
|
|
55
|
+
[this.data[parent], this.data[index]] = [this.data[index], this.data[parent]];
|
|
56
|
+
index = parent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private siftDown(index: number): void {
|
|
61
|
+
const length = this.data.length;
|
|
62
|
+
while (true) {
|
|
63
|
+
let largest = index;
|
|
64
|
+
const left = (index << 1) + 1;
|
|
65
|
+
const right = left + 1;
|
|
66
|
+
|
|
67
|
+
if (left < length && this.data[left] > this.data[largest]) largest = left;
|
|
68
|
+
if (right < length && this.data[right] > this.data[largest]) largest = right;
|
|
69
|
+
if (largest === index) break;
|
|
70
|
+
[this.data[index], this.data[largest]] = [this.data[largest], this.data[index]];
|
|
71
|
+
index = largest;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class Vertex {
|
|
77
|
+
public readonly pt: Point64;
|
|
78
|
+
public next: Vertex | null = null;
|
|
79
|
+
public prev: Vertex | null = null;
|
|
80
|
+
public flags: VertexFlags;
|
|
81
|
+
|
|
82
|
+
constructor(pt: Point64, flags: VertexFlags, prev: Vertex | null) {
|
|
83
|
+
this.pt = pt;
|
|
84
|
+
this.flags = flags;
|
|
85
|
+
this.prev = prev;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class LocalMinima {
|
|
90
|
+
public readonly vertex: Vertex;
|
|
91
|
+
public readonly polytype: PathType;
|
|
92
|
+
public readonly isOpen: boolean;
|
|
93
|
+
|
|
94
|
+
constructor(vertex: Vertex, polytype: PathType, isOpen: boolean = false) {
|
|
95
|
+
this.vertex = vertex;
|
|
96
|
+
this.polytype = polytype;
|
|
97
|
+
this.isOpen = isOpen;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
equals(other: LocalMinima | null): boolean {
|
|
101
|
+
return other !== null && this.vertex === other.vertex;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// deprecated: kept for backward compatibility, use new LocalMinima() directly
|
|
106
|
+
// (no longer used internally for performance)
|
|
107
|
+
export function createLocalMinima(vertex: Vertex, polytype: PathType, isOpen: boolean = false): LocalMinima {
|
|
108
|
+
return new LocalMinima(vertex, polytype, isOpen);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// IntersectNode: a structure representing 2 intersecting edges.
|
|
112
|
+
// Intersections must be sorted so they are processed from the largest
|
|
113
|
+
// Y coordinates to the smallest while keeping edges adjacent.
|
|
114
|
+
export interface IntersectNode {
|
|
115
|
+
readonly pt: Point64;
|
|
116
|
+
readonly edge1: Active;
|
|
117
|
+
readonly edge2: Active;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createIntersectNode(pt: Point64, edge1: Active, edge2: Active): IntersectNode {
|
|
121
|
+
// Create a copy of pt to avoid reference sharing (C# uses struct which copies by value)
|
|
122
|
+
return { pt: { x: pt.x, y: pt.y }, edge1, edge2 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// OutPt: vertex data structure for clipping solutions
|
|
126
|
+
export class OutPt {
|
|
127
|
+
private static _nextId = 1;
|
|
128
|
+
public readonly _debugId: number;
|
|
129
|
+
public pt: Point64;
|
|
130
|
+
public next: OutPt | null;
|
|
131
|
+
public prev: OutPt;
|
|
132
|
+
public outrec: OutRec;
|
|
133
|
+
public horz: HorzSegment | null;
|
|
134
|
+
|
|
135
|
+
constructor(pt: Point64, outrec: OutRec) {
|
|
136
|
+
this._debugId = OutPt._nextId++;
|
|
137
|
+
this.pt = pt;
|
|
138
|
+
this.outrec = outrec;
|
|
139
|
+
this.next = this;
|
|
140
|
+
this.prev = this;
|
|
141
|
+
this.horz = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export enum JoinWith { None, Left, Right }
|
|
146
|
+
export enum HorzPosition { Bottom, Middle, Top }
|
|
147
|
+
|
|
148
|
+
// OutRec: path data structure for clipping solutions
|
|
149
|
+
export class OutRec {
|
|
150
|
+
private static _nextId = 1;
|
|
151
|
+
public readonly _debugId: number;
|
|
152
|
+
public idx: number = 0;
|
|
153
|
+
public owner: OutRec | null = null;
|
|
154
|
+
public frontEdge: Active | null = null;
|
|
155
|
+
public backEdge: Active | null = null;
|
|
156
|
+
public pts: OutPt | null = null;
|
|
157
|
+
public polypath: PolyPathBase | null = null;
|
|
158
|
+
public bounds: Rect64 = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
159
|
+
public path: Path64 = [];
|
|
160
|
+
public isOpen: boolean = false;
|
|
161
|
+
public splits: number[] | null = null;
|
|
162
|
+
public recursiveSplit: OutRec | null = null;
|
|
163
|
+
|
|
164
|
+
constructor() {
|
|
165
|
+
this._debugId = OutRec._nextId++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class HorzSegment {
|
|
170
|
+
public leftOp: OutPt | null;
|
|
171
|
+
public rightOp: OutPt | null;
|
|
172
|
+
public leftToRight: boolean;
|
|
173
|
+
|
|
174
|
+
constructor(op: OutPt) {
|
|
175
|
+
this.leftOp = op;
|
|
176
|
+
this.rightOp = null;
|
|
177
|
+
this.leftToRight = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class HorzJoin {
|
|
182
|
+
public op1: OutPt | null;
|
|
183
|
+
public op2: OutPt | null;
|
|
184
|
+
|
|
185
|
+
constructor(ltor: OutPt, rtol: OutPt) {
|
|
186
|
+
this.op1 = ltor;
|
|
187
|
+
this.op2 = rtol;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
///////////////////////////////////////////////////////////////////
|
|
192
|
+
// Important: UP and DOWN here are premised on Y-axis positive down
|
|
193
|
+
// displays, which is the orientation used in Clipper's development.
|
|
194
|
+
///////////////////////////////////////////////////////////////////
|
|
195
|
+
|
|
196
|
+
export class Active {
|
|
197
|
+
public bot: Point64 = { x: 0, y: 0 };
|
|
198
|
+
public top: Point64 = { x: 0, y: 0 };
|
|
199
|
+
public curX: number = 0; // current (updated at every new scanline) - keep as number but ensure integer precision
|
|
200
|
+
public dx: number = 0;
|
|
201
|
+
public windDx: number = 0; // 1 or -1 depending on winding direction
|
|
202
|
+
public windCount: number = 0;
|
|
203
|
+
public windCount2: number = 0; // winding count of the opposite polytype
|
|
204
|
+
public outrec: OutRec | null = null;
|
|
205
|
+
|
|
206
|
+
// AEL: 'active edge list' (Vatti's AET - active edge table)
|
|
207
|
+
// a linked list of all edges (from left to right) that are present
|
|
208
|
+
// (or 'active') within the current scanbeam (a horizontal 'beam' that
|
|
209
|
+
// sweeps from bottom to top over the paths in the clipping operation).
|
|
210
|
+
public prevInAEL: Active | null = null;
|
|
211
|
+
public nextInAEL: Active | null = null;
|
|
212
|
+
|
|
213
|
+
// SEL: 'sorted edge list' (Vatti's ST - sorted table)
|
|
214
|
+
// linked list used when sorting edges into their new positions at the
|
|
215
|
+
// top of scanbeams, but also (re)used to process horizontals.
|
|
216
|
+
public prevInSEL: Active | null = null;
|
|
217
|
+
public nextInSEL: Active | null = null;
|
|
218
|
+
public jump: Active | null = null;
|
|
219
|
+
public vertexTop: Vertex | null = null;
|
|
220
|
+
public localMin: LocalMinima | null = null; // the bottom of an edge 'bound' (also Vatti)
|
|
221
|
+
public isLeftBound: boolean = false;
|
|
222
|
+
public joinWith: JoinWith = JoinWith.None;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export namespace ClipperEngine {
|
|
226
|
+
export function addLocMin(vert: Vertex, polytype: PathType, isOpen: boolean, minimaList: LocalMinima[]): void {
|
|
227
|
+
// make sure the vertex is added only once ...
|
|
228
|
+
if ((vert.flags & VertexFlags.LocalMin) !== VertexFlags.None) return;
|
|
229
|
+
vert.flags |= VertexFlags.LocalMin;
|
|
230
|
+
|
|
231
|
+
const lm = new LocalMinima(vert, polytype, isOpen);
|
|
232
|
+
minimaList.push(lm);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function addPathsToVertexList(
|
|
236
|
+
paths: Paths64,
|
|
237
|
+
polytype: PathType,
|
|
238
|
+
isOpen: boolean,
|
|
239
|
+
minimaList: LocalMinima[],
|
|
240
|
+
vertexList: Vertex[]
|
|
241
|
+
): void {
|
|
242
|
+
for (const path of paths) {
|
|
243
|
+
let v0: Vertex | null = null;
|
|
244
|
+
let prevV: Vertex | null = null;
|
|
245
|
+
|
|
246
|
+
for (const pt of path) {
|
|
247
|
+
if (v0 === null) {
|
|
248
|
+
v0 = new Vertex(pt, VertexFlags.None, null);
|
|
249
|
+
vertexList.push(v0);
|
|
250
|
+
prevV = v0;
|
|
251
|
+
} else if (!(prevV!.pt.x === pt.x && prevV!.pt.y === pt.y)) { // ie skips duplicates
|
|
252
|
+
const currV: Vertex = new Vertex(pt, VertexFlags.None, prevV);
|
|
253
|
+
vertexList.push(currV);
|
|
254
|
+
prevV!.next = currV;
|
|
255
|
+
prevV = currV;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (prevV?.prev === null) continue;
|
|
260
|
+
if (!isOpen && prevV!.pt.x === v0!.pt.x && prevV!.pt.y === v0!.pt.y) prevV = prevV!.prev;
|
|
261
|
+
prevV!.next = v0;
|
|
262
|
+
v0!.prev = prevV;
|
|
263
|
+
if (!isOpen && prevV!.next === prevV) continue;
|
|
264
|
+
|
|
265
|
+
// OK, we have a valid path
|
|
266
|
+
let goingUp: boolean;
|
|
267
|
+
if (isOpen) {
|
|
268
|
+
let currV = v0!.next;
|
|
269
|
+
while (currV !== v0 && currV!.pt.y === v0!.pt.y)
|
|
270
|
+
currV = currV!.next;
|
|
271
|
+
goingUp = currV!.pt.y <= v0!.pt.y;
|
|
272
|
+
if (goingUp) {
|
|
273
|
+
v0!.flags = VertexFlags.OpenStart;
|
|
274
|
+
addLocMin(v0!, polytype, true, minimaList);
|
|
275
|
+
} else {
|
|
276
|
+
v0!.flags = VertexFlags.OpenStart | VertexFlags.LocalMax;
|
|
277
|
+
}
|
|
278
|
+
} else { // closed path
|
|
279
|
+
prevV = v0!.prev;
|
|
280
|
+
while (prevV !== v0 && prevV!.pt.y === v0!.pt.y)
|
|
281
|
+
prevV = prevV!.prev;
|
|
282
|
+
if (prevV === v0)
|
|
283
|
+
continue; // only open paths can be completely flat
|
|
284
|
+
goingUp = prevV!.pt.y > v0!.pt.y;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const goingUp0 = goingUp;
|
|
288
|
+
prevV = v0;
|
|
289
|
+
let currV = v0!.next;
|
|
290
|
+
while (currV !== v0) {
|
|
291
|
+
if (currV!.pt.y > prevV!.pt.y && goingUp) {
|
|
292
|
+
prevV!.flags |= VertexFlags.LocalMax;
|
|
293
|
+
goingUp = false;
|
|
294
|
+
} else if (currV!.pt.y < prevV!.pt.y && !goingUp) {
|
|
295
|
+
goingUp = true;
|
|
296
|
+
addLocMin(prevV!, polytype, isOpen, minimaList);
|
|
297
|
+
}
|
|
298
|
+
prevV = currV;
|
|
299
|
+
currV = currV!.next;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isOpen) {
|
|
303
|
+
prevV!.flags |= VertexFlags.OpenEnd;
|
|
304
|
+
if (goingUp)
|
|
305
|
+
prevV!.flags |= VertexFlags.LocalMax;
|
|
306
|
+
else
|
|
307
|
+
addLocMin(prevV!, polytype, isOpen, minimaList);
|
|
308
|
+
} else if (goingUp !== goingUp0) {
|
|
309
|
+
if (goingUp0) addLocMin(prevV!, polytype, false, minimaList);
|
|
310
|
+
else prevV!.flags |= VertexFlags.LocalMax;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export class ReuseableDataContainer64 {
|
|
317
|
+
private readonly minimaList: LocalMinima[];
|
|
318
|
+
private readonly vertexList: Vertex[];
|
|
319
|
+
|
|
320
|
+
constructor() {
|
|
321
|
+
this.minimaList = [];
|
|
322
|
+
this.vertexList = [];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
public clear(): void {
|
|
326
|
+
this.minimaList.length = 0;
|
|
327
|
+
this.vertexList.length = 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
public addPaths(paths: Paths64, pt: PathType, isOpen: boolean): void {
|
|
331
|
+
ClipperEngine.addPathsToVertexList(paths, pt, isOpen, this.minimaList, this.vertexList);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export abstract class PolyPathBase {
|
|
336
|
+
protected parent: PolyPathBase | null;
|
|
337
|
+
protected children: PolyPathBase[] = [];
|
|
338
|
+
|
|
339
|
+
constructor(parent: PolyPathBase | null = null) {
|
|
340
|
+
this.parent = parent;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public get isHole(): boolean {
|
|
344
|
+
return this.getIsHole();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private getLevel(): number {
|
|
348
|
+
let result = 0;
|
|
349
|
+
let pp = this.parent;
|
|
350
|
+
while (pp !== null) {
|
|
351
|
+
++result;
|
|
352
|
+
pp = pp.parent;
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public get level(): number {
|
|
358
|
+
return this.getLevel();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private getIsHole(): boolean {
|
|
362
|
+
const lvl = this.getLevel();
|
|
363
|
+
return lvl !== 0 && (lvl & 1) === 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public get count(): number {
|
|
367
|
+
return this.children.length;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public abstract addChild(p: Path64): PolyPathBase;
|
|
371
|
+
|
|
372
|
+
public clear(): void {
|
|
373
|
+
this.children.length = 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
protected toStringInternal(idx: number, level: number): string {
|
|
377
|
+
let result = "";
|
|
378
|
+
const padding = " ".repeat(level);
|
|
379
|
+
const plural = this.children.length === 1 ? "" : "s";
|
|
380
|
+
|
|
381
|
+
if ((level & 1) === 0) {
|
|
382
|
+
result += `${padding}+- hole (${idx}) contains ${this.children.length} nested polygon${plural}.\n`;
|
|
383
|
+
} else {
|
|
384
|
+
result += `${padding}+- polygon (${idx}) contains ${this.children.length} hole${plural}.\n`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < this.count; i++) {
|
|
388
|
+
if (this.children[i].count > 0) {
|
|
389
|
+
result += this.children[i].toStringInternal(i, level + 1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
public toString(): string {
|
|
396
|
+
if (this.level > 0) return ""; // only accept tree root
|
|
397
|
+
const plural = this.children.length === 1 ? "" : "s";
|
|
398
|
+
let result = `Polytree with ${this.children.length} polygon${plural}.\n`;
|
|
399
|
+
for (let i = 0; i < this.count; i++) {
|
|
400
|
+
if (this.children[i].count > 0) {
|
|
401
|
+
result += this.children[i].toStringInternal(i, 1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return result + '\n';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export class PolyPath64 extends PolyPathBase {
|
|
409
|
+
public polygon: Path64 | null = null; // polytree root's polygon == null
|
|
410
|
+
|
|
411
|
+
constructor(parent: PolyPathBase | null = null) {
|
|
412
|
+
super(parent);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
public get poly(): Path64 | null {
|
|
416
|
+
return this.polygon;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
public addChild(p: Path64): PolyPathBase {
|
|
420
|
+
const newChild = new PolyPath64(this);
|
|
421
|
+
newChild.polygon = p;
|
|
422
|
+
this.children.push(newChild);
|
|
423
|
+
return newChild;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
public child(index: number): PolyPath64 {
|
|
427
|
+
if (index < 0 || index >= this.children.length) {
|
|
428
|
+
throw new Error("Index out of range");
|
|
429
|
+
}
|
|
430
|
+
return this.children[index] as PolyPath64;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public area(): number {
|
|
434
|
+
let result = this.polygon === null ? 0 : Clipper.area(this.polygon);
|
|
435
|
+
for (const child of this.children) {
|
|
436
|
+
result += (child as PolyPath64).area();
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export class PolyPathD extends PolyPathBase {
|
|
443
|
+
public scale: number = 1.0;
|
|
444
|
+
private polygon: PathD | null = null;
|
|
445
|
+
|
|
446
|
+
constructor(parent: PolyPathBase | null = null) {
|
|
447
|
+
super(parent);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public get poly(): PathD | null {
|
|
451
|
+
return this.polygon;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public addChild(p: Path64): PolyPathBase {
|
|
455
|
+
const newChild = new PolyPathD(this);
|
|
456
|
+
newChild.scale = this.scale;
|
|
457
|
+
newChild.polygon = Clipper.scalePathD(p, 1 / this.scale);
|
|
458
|
+
this.children.push(newChild);
|
|
459
|
+
return newChild;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public addChildD(p: PathD): PolyPathBase {
|
|
463
|
+
const newChild = new PolyPathD(this);
|
|
464
|
+
newChild.scale = this.scale;
|
|
465
|
+
newChild.polygon = p;
|
|
466
|
+
this.children.push(newChild);
|
|
467
|
+
return newChild;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
public child(index: number): PolyPathD {
|
|
471
|
+
if (index < 0 || index >= this.children.length) {
|
|
472
|
+
throw new Error("Index out of range");
|
|
473
|
+
}
|
|
474
|
+
return this.children[index] as PolyPathD;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
public area(): number {
|
|
478
|
+
let result = this.polygon === null ? 0 : Clipper.areaD(this.polygon);
|
|
479
|
+
for (const child of this.children) {
|
|
480
|
+
result += (child as PolyPathD).area();
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export class PolyTree64 extends PolyPath64 {}
|
|
487
|
+
|
|
488
|
+
export class PolyTreeD extends PolyPathD {
|
|
489
|
+
public get scaleValue(): number {
|
|
490
|
+
return this.scale;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export class ClipperBase {
|
|
495
|
+
protected cliptype: ClipType = ClipType.NoClip;
|
|
496
|
+
protected fillrule: FillRule = FillRule.EvenOdd;
|
|
497
|
+
protected actives: Active | null = null;
|
|
498
|
+
protected sel: Active | null = null;
|
|
499
|
+
protected readonly minimaList: LocalMinima[] = [];
|
|
500
|
+
protected readonly intersectList: IntersectNode[] = [];
|
|
501
|
+
protected readonly vertexList: Vertex[] = [];
|
|
502
|
+
protected readonly outrecList: OutRec[] = [];
|
|
503
|
+
protected readonly scanlineHeap = new ScanlineHeap();
|
|
504
|
+
protected readonly scanlineSet = new Set<number>();
|
|
505
|
+
protected readonly horzSegList: HorzSegment[] = [];
|
|
506
|
+
protected readonly horzJoinList: HorzJoin[] = [];
|
|
507
|
+
protected currentLocMin: number = 0;
|
|
508
|
+
protected currentBotY: number = 0;
|
|
509
|
+
protected isSortedMinimaList: boolean = false;
|
|
510
|
+
protected hasOpenPaths: boolean = false;
|
|
511
|
+
protected usingPolytree: boolean = false;
|
|
512
|
+
protected succeeded: boolean = false;
|
|
513
|
+
|
|
514
|
+
public preserveCollinear: boolean = true;
|
|
515
|
+
public reverseSolution: boolean = false;
|
|
516
|
+
|
|
517
|
+
constructor() {}
|
|
518
|
+
|
|
519
|
+
// Helper functions
|
|
520
|
+
private static isOdd(val: number): boolean {
|
|
521
|
+
return (val & 1) !== 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private static isHotEdge(ae: Active): boolean {
|
|
525
|
+
return ae.outrec != null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private static isOpen(ae: Active): boolean {
|
|
529
|
+
return ae.localMin!.isOpen;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private static isOpenEnd(ae: Active): boolean {
|
|
533
|
+
return ae.localMin!.isOpen && ClipperBase.isOpenEndVertex(ae.vertexTop!);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private static isOpenEndVertex(v: Vertex): boolean {
|
|
537
|
+
return (v.flags & (VertexFlags.OpenStart | VertexFlags.OpenEnd)) !== VertexFlags.None;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private static getPrevHotEdge(ae: Active): Active | null {
|
|
541
|
+
let prev = ae.prevInAEL;
|
|
542
|
+
while (prev !== null && (ClipperBase.isOpen(prev) || !ClipperBase.isHotEdge(prev))) {
|
|
543
|
+
prev = prev.prevInAEL;
|
|
544
|
+
}
|
|
545
|
+
return prev;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private static isFront(ae: Active): boolean {
|
|
549
|
+
return ae === ae.outrec!.frontEdge;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/*******************************************************************************
|
|
553
|
+
* Dx: 0(90deg) *
|
|
554
|
+
* | *
|
|
555
|
+
* +inf (180deg) <--- o ---> -inf (0deg) *
|
|
556
|
+
*******************************************************************************/
|
|
557
|
+
|
|
558
|
+
private static getDx(pt1: Point64, pt2: Point64): number {
|
|
559
|
+
const dy = pt2.y - pt1.y;
|
|
560
|
+
if (dy !== 0) {
|
|
561
|
+
return (pt2.x - pt1.x) / dy;
|
|
562
|
+
}
|
|
563
|
+
return pt2.x > pt1.x ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private static topX(ae: Active, currentY: number): number {
|
|
567
|
+
if ((currentY === ae.top.y) || (ae.top.x === ae.bot.x)) return ae.top.x;
|
|
568
|
+
if (currentY === ae.bot.y) return ae.bot.x;
|
|
569
|
+
|
|
570
|
+
// use MidpointRounding.ToEven in order to explicitly match the nearbyint behaviour on the C++ side
|
|
571
|
+
return InternalClipper.roundToEven(ae.bot.x + ae.dx * (currentY - ae.bot.y));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private static isHorizontal(ae: Active): boolean {
|
|
575
|
+
return ae.top.y === ae.bot.y;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private static isHeadingRightHorz(ae: Active): boolean {
|
|
579
|
+
return ae.dx === Number.NEGATIVE_INFINITY;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private static isHeadingLeftHorz(ae: Active): boolean {
|
|
583
|
+
return ae.dx === Number.POSITIVE_INFINITY;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private static swapActives(ae1: Active, ae2: Active): [Active, Active] {
|
|
587
|
+
return [ae2, ae1];
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private static getPolyType(ae: Active): PathType {
|
|
591
|
+
return ae.localMin!.polytype;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private static isSamePolyType(ae1: Active, ae2: Active): boolean {
|
|
595
|
+
return ae1.localMin!.polytype === ae2.localMin!.polytype;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private static setDx(ae: Active): void {
|
|
599
|
+
ae.dx = ClipperBase.getDx(ae.bot, ae.top);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private static nextVertex(ae: Active): Vertex {
|
|
603
|
+
return ae.windDx > 0 ? ae.vertexTop!.next! : ae.vertexTop!.prev!;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private static prevPrevVertex(ae: Active): Vertex {
|
|
607
|
+
return ae.windDx > 0 ? ae.vertexTop!.prev!.prev! : ae.vertexTop!.next!.next!;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private static isMaxima(vertex: Vertex): boolean;
|
|
611
|
+
private static isMaxima(ae: Active): boolean;
|
|
612
|
+
private static isMaxima(vertexOrAe: Vertex | Active): boolean {
|
|
613
|
+
if ('flags' in vertexOrAe) {
|
|
614
|
+
// It's a Vertex
|
|
615
|
+
return (vertexOrAe.flags & VertexFlags.LocalMax) !== VertexFlags.None;
|
|
616
|
+
} else {
|
|
617
|
+
// It's an Active
|
|
618
|
+
return ClipperBase.isMaxima(vertexOrAe.vertexTop!);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private static getMaximaPair(ae: Active): Active | null {
|
|
623
|
+
let ae2 = ae.nextInAEL;
|
|
624
|
+
while (ae2 !== null) {
|
|
625
|
+
if (ae2.vertexTop === ae.vertexTop) return ae2; // Found!
|
|
626
|
+
ae2 = ae2.nextInAEL;
|
|
627
|
+
}
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// optimization (not in C# reference): fast bounding box overlap check for segment intersection
|
|
632
|
+
private boundingBoxesOverlap(p1: Point64, p2: Point64, p3: Point64, p4: Point64): boolean {
|
|
633
|
+
// segment 1: p1-p2, segment 2: p3-p4
|
|
634
|
+
const min1x = Math.min(p1.x, p2.x);
|
|
635
|
+
const max1x = Math.max(p1.x, p2.x);
|
|
636
|
+
const min1y = Math.min(p1.y, p2.y);
|
|
637
|
+
const max1y = Math.max(p1.y, p2.y);
|
|
638
|
+
|
|
639
|
+
const min2x = Math.min(p3.x, p4.x);
|
|
640
|
+
const max2x = Math.max(p3.x, p4.x);
|
|
641
|
+
const min2y = Math.min(p3.y, p4.y);
|
|
642
|
+
const max2y = Math.max(p3.y, p4.y);
|
|
643
|
+
|
|
644
|
+
return !(max1x < min2x || max2x < min1x || max1y < min2y || max2y < min1y);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
protected clearSolutionOnly(): void {
|
|
648
|
+
while (this.actives !== null) this.deleteFromAEL(this.actives);
|
|
649
|
+
this.scanlineHeap.clear();
|
|
650
|
+
this.scanlineSet.clear();
|
|
651
|
+
this.disposeIntersectNodes();
|
|
652
|
+
this.outrecList.length = 0;
|
|
653
|
+
this.horzSegList.length = 0;
|
|
654
|
+
this.horzJoinList.length = 0;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
public clear(): void {
|
|
658
|
+
this.clearSolutionOnly();
|
|
659
|
+
this.minimaList.length = 0;
|
|
660
|
+
this.vertexList.length = 0;
|
|
661
|
+
this.currentLocMin = 0;
|
|
662
|
+
this.isSortedMinimaList = false;
|
|
663
|
+
this.hasOpenPaths = false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
protected reset(): void {
|
|
667
|
+
if (!this.isSortedMinimaList) {
|
|
668
|
+
this.minimaList.sort((a, b) => b.vertex.pt.y - a.vertex.pt.y);
|
|
669
|
+
this.isSortedMinimaList = true;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
this.scanlineHeap.clear();
|
|
673
|
+
this.scanlineSet.clear();
|
|
674
|
+
for (let i = this.minimaList.length - 1; i >= 0; i--) {
|
|
675
|
+
this.insertScanline(this.minimaList[i].vertex.pt.y);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
this.currentBotY = 0;
|
|
679
|
+
this.currentLocMin = 0;
|
|
680
|
+
this.actives = null;
|
|
681
|
+
this.sel = null;
|
|
682
|
+
this.succeeded = true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private insertScanline(y: number): void {
|
|
686
|
+
if (this.scanlineSet.has(y)) return;
|
|
687
|
+
this.scanlineSet.add(y);
|
|
688
|
+
this.scanlineHeap.push(y);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
private popScanline(): { success: boolean; y: number } {
|
|
692
|
+
const y = this.scanlineHeap.pop();
|
|
693
|
+
if (y === null) return { success: false, y: 0 };
|
|
694
|
+
this.scanlineSet.delete(y);
|
|
695
|
+
return { success: true, y };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private hasLocMinAtY(y: number): boolean {
|
|
699
|
+
return this.currentLocMin < this.minimaList.length &&
|
|
700
|
+
this.minimaList[this.currentLocMin].vertex.pt.y === y;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private popLocalMinima(): LocalMinima {
|
|
704
|
+
return this.minimaList[this.currentLocMin++];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
protected addPath(path: Path64, polytype: PathType, isOpen: boolean = false): void {
|
|
708
|
+
const tmp: Paths64 = [path];
|
|
709
|
+
this.addPaths(tmp, polytype, isOpen);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
protected addPaths(paths: Paths64, polytype: PathType, isOpen: boolean = false): void {
|
|
713
|
+
if (isOpen) this.hasOpenPaths = true;
|
|
714
|
+
this.isSortedMinimaList = false;
|
|
715
|
+
ClipperEngine.addPathsToVertexList(paths, polytype, isOpen, this.minimaList, this.vertexList);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
protected addReuseableData(reuseableData: ReuseableDataContainer64): void {
|
|
719
|
+
if (reuseableData['minimaList'].length === 0) return;
|
|
720
|
+
// nb: reuseableData will continue to own the vertices, so it's important
|
|
721
|
+
// that the reuseableData object isn't destroyed before the Clipper object
|
|
722
|
+
// that's using the data.
|
|
723
|
+
this.isSortedMinimaList = false;
|
|
724
|
+
for (const lm of reuseableData['minimaList']) {
|
|
725
|
+
this.minimaList.push(new LocalMinima(lm.vertex, lm.polytype, lm.isOpen));
|
|
726
|
+
if (lm.isOpen) this.hasOpenPaths = true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private deleteFromAEL(ae: Active): void {
|
|
731
|
+
const prev = ae.prevInAEL;
|
|
732
|
+
const next = ae.nextInAEL;
|
|
733
|
+
if (prev === null && next === null && (ae !== this.actives)) return; // already deleted
|
|
734
|
+
if (prev !== null) {
|
|
735
|
+
prev.nextInAEL = next;
|
|
736
|
+
} else {
|
|
737
|
+
this.actives = next;
|
|
738
|
+
}
|
|
739
|
+
if (next !== null) next.prevInAEL = prev;
|
|
740
|
+
// delete ae;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
public getBounds(): Rect64 {
|
|
744
|
+
const bounds: Rect64 = {
|
|
745
|
+
left: Number.MAX_SAFE_INTEGER,
|
|
746
|
+
top: Number.MAX_SAFE_INTEGER,
|
|
747
|
+
right: Number.MIN_SAFE_INTEGER,
|
|
748
|
+
bottom: Number.MIN_SAFE_INTEGER
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
for (const t of this.vertexList) {
|
|
752
|
+
let v = t;
|
|
753
|
+
do {
|
|
754
|
+
if (v.pt.x < bounds.left) bounds.left = v.pt.x;
|
|
755
|
+
if (v.pt.x > bounds.right) bounds.right = v.pt.x;
|
|
756
|
+
if (v.pt.y < bounds.top) bounds.top = v.pt.y;
|
|
757
|
+
if (v.pt.y > bounds.bottom) bounds.bottom = v.pt.y;
|
|
758
|
+
v = v.next!;
|
|
759
|
+
} while (v !== t);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return Rect64Utils.isEmpty(bounds) ? { left: 0, top: 0, right: 0, bottom: 0 } : bounds;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
protected executeInternal(ct: ClipType, fillRule: FillRule): void {
|
|
766
|
+
if (ct === ClipType.NoClip) return;
|
|
767
|
+
this.fillrule = fillRule;
|
|
768
|
+
this.cliptype = ct;
|
|
769
|
+
this.reset();
|
|
770
|
+
|
|
771
|
+
const scanResult = this.popScanline();
|
|
772
|
+
if (!scanResult.success) return;
|
|
773
|
+
let y = scanResult.y;
|
|
774
|
+
|
|
775
|
+
while (this.succeeded) {
|
|
776
|
+
this.insertLocalMinimaIntoAEL(y);
|
|
777
|
+
let ae: Active | null;
|
|
778
|
+
while ((ae = this.popHorz()) !== null) this.doHorizontal(ae);
|
|
779
|
+
if (this.horzSegList.length > 0) {
|
|
780
|
+
this.convertHorzSegsToJoins();
|
|
781
|
+
this.horzSegList.length = 0;
|
|
782
|
+
}
|
|
783
|
+
this.currentBotY = y; // bottom of scanbeam
|
|
784
|
+
const nextScanResult = this.popScanline();
|
|
785
|
+
if (!nextScanResult.success) break; // y new top of scanbeam
|
|
786
|
+
y = nextScanResult.y;
|
|
787
|
+
this.doIntersections(y);
|
|
788
|
+
this.doTopOfScanbeam(y);
|
|
789
|
+
while ((ae = this.popHorz()) !== null) this.doHorizontal(ae);
|
|
790
|
+
}
|
|
791
|
+
if (this.succeeded) this.processHorzJoins();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private insertLocalMinimaIntoAEL(botY: number): void {
|
|
795
|
+
// Add any local minima (if any) at BotY ...
|
|
796
|
+
// NB horizontal local minima edges should contain locMin.vertex.prev
|
|
797
|
+
while (this.hasLocMinAtY(botY)) {
|
|
798
|
+
const localMinima = this.popLocalMinima();
|
|
799
|
+
let leftBound: Active | null;
|
|
800
|
+
|
|
801
|
+
if ((localMinima.vertex.flags & VertexFlags.OpenStart) !== VertexFlags.None) {
|
|
802
|
+
leftBound = null;
|
|
803
|
+
} else {
|
|
804
|
+
leftBound = new Active();
|
|
805
|
+
leftBound.bot = { x: localMinima.vertex.pt.x, y: localMinima.vertex.pt.y }; // Create copy
|
|
806
|
+
leftBound.curX = localMinima.vertex.pt.x;
|
|
807
|
+
leftBound.windDx = -1;
|
|
808
|
+
leftBound.vertexTop = localMinima.vertex.prev;
|
|
809
|
+
leftBound.top = { x: localMinima.vertex.prev!.pt.x, y: localMinima.vertex.prev!.pt.y }; // Create copy
|
|
810
|
+
leftBound.outrec = null;
|
|
811
|
+
leftBound.localMin = localMinima;
|
|
812
|
+
ClipperBase.setDx(leftBound);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let rightBound: Active | null;
|
|
816
|
+
if ((localMinima.vertex.flags & VertexFlags.OpenEnd) !== VertexFlags.None) {
|
|
817
|
+
rightBound = null;
|
|
818
|
+
} else {
|
|
819
|
+
rightBound = new Active();
|
|
820
|
+
rightBound.bot = { x: localMinima.vertex.pt.x, y: localMinima.vertex.pt.y }; // Create copy
|
|
821
|
+
rightBound.curX = localMinima.vertex.pt.x;
|
|
822
|
+
rightBound.windDx = 1;
|
|
823
|
+
rightBound.vertexTop = localMinima.vertex.next; // i.e. ascending
|
|
824
|
+
rightBound.top = { x: localMinima.vertex.next!.pt.x, y: localMinima.vertex.next!.pt.y }; // Create copy
|
|
825
|
+
rightBound.outrec = null;
|
|
826
|
+
rightBound.localMin = localMinima;
|
|
827
|
+
ClipperBase.setDx(rightBound);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Currently LeftB is just the descending bound and RightB is the ascending.
|
|
831
|
+
// Now if the LeftB isn't on the left of RightB then we need swap them.
|
|
832
|
+
if (leftBound !== null && rightBound !== null) {
|
|
833
|
+
if (ClipperBase.isHorizontal(leftBound)) {
|
|
834
|
+
if (ClipperBase.isHeadingRightHorz(leftBound)) [leftBound, rightBound] = ClipperBase.swapActives(leftBound, rightBound);
|
|
835
|
+
} else if (ClipperBase.isHorizontal(rightBound)) {
|
|
836
|
+
if (ClipperBase.isHeadingLeftHorz(rightBound)) [leftBound, rightBound] = ClipperBase.swapActives(leftBound, rightBound);
|
|
837
|
+
} else if (leftBound.dx < rightBound.dx) {
|
|
838
|
+
[leftBound, rightBound] = ClipperBase.swapActives(leftBound, rightBound);
|
|
839
|
+
}
|
|
840
|
+
} else if (leftBound === null) {
|
|
841
|
+
leftBound = rightBound;
|
|
842
|
+
rightBound = null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
let contributing: boolean;
|
|
846
|
+
leftBound!.isLeftBound = true;
|
|
847
|
+
this.insertLeftEdge(leftBound!);
|
|
848
|
+
|
|
849
|
+
if (ClipperBase.isOpen(leftBound!)) {
|
|
850
|
+
this.setWindCountForOpenPathEdge(leftBound!);
|
|
851
|
+
contributing = this.isContributingOpen(leftBound!);
|
|
852
|
+
} else {
|
|
853
|
+
this.setWindCountForClosedPathEdge(leftBound!);
|
|
854
|
+
contributing = this.isContributingClosed(leftBound!);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (rightBound !== null) {
|
|
858
|
+
rightBound.windCount = leftBound!.windCount;
|
|
859
|
+
rightBound.windCount2 = leftBound!.windCount2;
|
|
860
|
+
this.insertRightEdge(leftBound!, rightBound);
|
|
861
|
+
|
|
862
|
+
if (contributing) {
|
|
863
|
+
this.addLocalMinPoly(leftBound!, rightBound, leftBound!.bot, true);
|
|
864
|
+
if (!ClipperBase.isHorizontal(leftBound!)) {
|
|
865
|
+
this.checkJoinLeft(leftBound!, leftBound!.bot);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
while (rightBound.nextInAEL !== null &&
|
|
870
|
+
this.isValidAelOrder(rightBound.nextInAEL, rightBound)) {
|
|
871
|
+
this.intersectEdges(rightBound, rightBound.nextInAEL, rightBound.bot);
|
|
872
|
+
this.swapPositionsInAEL(rightBound, rightBound.nextInAEL);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (ClipperBase.isHorizontal(rightBound)) {
|
|
876
|
+
this.pushHorz(rightBound);
|
|
877
|
+
} else {
|
|
878
|
+
this.checkJoinRight(rightBound, rightBound.bot);
|
|
879
|
+
this.insertScanline(rightBound.top.y);
|
|
880
|
+
}
|
|
881
|
+
} else if (contributing) {
|
|
882
|
+
this.startOpenPath(leftBound!, leftBound!.bot);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (ClipperBase.isHorizontal(leftBound!)) {
|
|
886
|
+
this.pushHorz(leftBound!);
|
|
887
|
+
} else {
|
|
888
|
+
this.insertScanline(leftBound!.top.y);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private pushHorz(ae: Active): void {
|
|
894
|
+
ae.nextInSEL = this.sel;
|
|
895
|
+
this.sel = ae;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private popHorz(): Active | null {
|
|
899
|
+
const ae = this.sel;
|
|
900
|
+
if (ae === null) return null;
|
|
901
|
+
this.sel = this.sel!.nextInSEL;
|
|
902
|
+
return ae;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private doHorizontal(horz: Active): void {
|
|
906
|
+
const horzIsOpen = ClipperBase.isOpen(horz);
|
|
907
|
+
const y = horz.bot.y;
|
|
908
|
+
|
|
909
|
+
const vertexMax = horzIsOpen ?
|
|
910
|
+
this.getCurrYMaximaVertexOpen(horz) :
|
|
911
|
+
this.getCurrYMaximaVertex(horz);
|
|
912
|
+
|
|
913
|
+
const { isLeftToRight, leftX, rightX } = this.resetHorzDirection(horz, vertexMax);
|
|
914
|
+
let leftX2 = leftX;
|
|
915
|
+
let rightX2 = rightX;
|
|
916
|
+
|
|
917
|
+
if (ClipperBase.isHotEdge(horz)) {
|
|
918
|
+
const op = this.addOutPt(horz, { x: horz.curX, y });
|
|
919
|
+
this.addToHorzSegList(op);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
while (true) {
|
|
923
|
+
// loops through consec. horizontal edges (if open)
|
|
924
|
+
let ae = isLeftToRight ? horz.nextInAEL : horz.prevInAEL;
|
|
925
|
+
|
|
926
|
+
while (ae !== null) {
|
|
927
|
+
if (ae.vertexTop === vertexMax) {
|
|
928
|
+
// do this first!!
|
|
929
|
+
if (ClipperBase.isHotEdge(horz) && this.isJoined(ae)) this.split(ae, ae.top);
|
|
930
|
+
|
|
931
|
+
if (ClipperBase.isHotEdge(horz)) {
|
|
932
|
+
while (horz.vertexTop !== vertexMax) {
|
|
933
|
+
this.addOutPt(horz, horz.top);
|
|
934
|
+
this.updateEdgeIntoAEL(horz);
|
|
935
|
+
}
|
|
936
|
+
if (isLeftToRight) {
|
|
937
|
+
this.addLocalMaxPoly(horz, ae, horz.top);
|
|
938
|
+
} else {
|
|
939
|
+
this.addLocalMaxPoly(ae, horz, horz.top);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
this.deleteFromAEL(ae);
|
|
943
|
+
this.deleteFromAEL(horz);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// if horzEdge is a maxima, keep going until we reach
|
|
948
|
+
// its maxima pair, otherwise check for break conditions
|
|
949
|
+
if (vertexMax !== horz.vertexTop || ClipperBase.isOpenEnd(horz)) {
|
|
950
|
+
// otherwise stop when 'ae' is beyond the end of the horizontal line
|
|
951
|
+
if ((isLeftToRight && ae.curX > rightX2) ||
|
|
952
|
+
(!isLeftToRight && ae.curX < leftX2)) break;
|
|
953
|
+
|
|
954
|
+
if (ae.curX === horz.top.x && !ClipperBase.isHorizontal(ae)) {
|
|
955
|
+
const pt = ClipperBase.nextVertex(horz).pt;
|
|
956
|
+
|
|
957
|
+
// to maximize the possibility of putting open edges into
|
|
958
|
+
// solutions, we'll only break if it's past HorzEdge's end
|
|
959
|
+
if (ClipperBase.isOpen(ae) && !ClipperBase.isSamePolyType(ae, horz) && !ClipperBase.isHotEdge(ae)) {
|
|
960
|
+
if ((isLeftToRight && (ClipperBase.topX(ae, pt.y) > pt.x)) ||
|
|
961
|
+
(!isLeftToRight && (ClipperBase.topX(ae, pt.y) < pt.x))) break;
|
|
962
|
+
}
|
|
963
|
+
// otherwise for edges at horzEdge's end, only stop when horzEdge's
|
|
964
|
+
// outslope is greater than e's slope when heading right or when
|
|
965
|
+
// horzEdge's outslope is less than e's slope when heading left.
|
|
966
|
+
else if ((isLeftToRight && (ClipperBase.topX(ae, pt.y) >= pt.x)) ||
|
|
967
|
+
(!isLeftToRight && (ClipperBase.topX(ae, pt.y) <= pt.x))) break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const pt = { x: ae.curX, y };
|
|
972
|
+
|
|
973
|
+
if (isLeftToRight) {
|
|
974
|
+
this.intersectEdges(horz, ae, pt);
|
|
975
|
+
this.swapPositionsInAEL(horz, ae);
|
|
976
|
+
this.checkJoinLeft(ae, pt);
|
|
977
|
+
horz.curX = ae.curX;
|
|
978
|
+
ae = horz.nextInAEL;
|
|
979
|
+
} else {
|
|
980
|
+
this.intersectEdges(ae, horz, pt);
|
|
981
|
+
this.swapPositionsInAEL(ae, horz);
|
|
982
|
+
this.checkJoinRight(ae, pt);
|
|
983
|
+
horz.curX = ae.curX;
|
|
984
|
+
ae = horz.prevInAEL;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (ClipperBase.isHotEdge(horz)) {
|
|
988
|
+
this.addToHorzSegList(this.getLastOp(horz));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// check if we've finished looping
|
|
993
|
+
// through consecutive horizontals
|
|
994
|
+
if (horzIsOpen && ClipperBase.isOpenEnd(horz)) { // ie open at top
|
|
995
|
+
if (ClipperBase.isHotEdge(horz)) {
|
|
996
|
+
this.addOutPt(horz, horz.top);
|
|
997
|
+
if (ClipperBase.isFront(horz)) {
|
|
998
|
+
horz.outrec!.frontEdge = null;
|
|
999
|
+
} else {
|
|
1000
|
+
horz.outrec!.backEdge = null;
|
|
1001
|
+
}
|
|
1002
|
+
horz.outrec = null;
|
|
1003
|
+
}
|
|
1004
|
+
this.deleteFromAEL(horz);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (ClipperBase.nextVertex(horz).pt.y !== horz.top.y) {
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
//still more horizontals in bound to process ...
|
|
1012
|
+
if (ClipperBase.isHotEdge(horz)) {
|
|
1013
|
+
this.addOutPt(horz, horz.top);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
this.updateEdgeIntoAEL(horz);
|
|
1017
|
+
|
|
1018
|
+
const resetResult = this.resetHorzDirection(horz, vertexMax);
|
|
1019
|
+
leftX2 = resetResult.leftX;
|
|
1020
|
+
rightX2 = resetResult.rightX;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (ClipperBase.isHotEdge(horz)) {
|
|
1024
|
+
const op = this.addOutPt(horz, horz.top);
|
|
1025
|
+
this.addToHorzSegList(op);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
this.updateEdgeIntoAEL(horz); // this is the end of an intermediate horiz.
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private convertHorzSegsToJoins(): void {
|
|
1032
|
+
let k = 0;
|
|
1033
|
+
for (const hs of this.horzSegList) {
|
|
1034
|
+
if (this.updateHorzSegment(hs)) k++;
|
|
1035
|
+
}
|
|
1036
|
+
if (k < 2) return;
|
|
1037
|
+
|
|
1038
|
+
this.horzSegList.sort((a, b) => this.horzSegSort(a, b));
|
|
1039
|
+
|
|
1040
|
+
for (let i = 0; i < k - 1; i++) {
|
|
1041
|
+
const hs1 = this.horzSegList[i];
|
|
1042
|
+
// for each HorzSegment, find others that overlap
|
|
1043
|
+
for (let j = i + 1; j < k; j++) {
|
|
1044
|
+
const hs2 = this.horzSegList[j];
|
|
1045
|
+
if ((hs2.leftOp!.pt.x >= hs1.rightOp!.pt.x) ||
|
|
1046
|
+
(hs2.leftToRight === hs1.leftToRight) ||
|
|
1047
|
+
(hs2.rightOp!.pt.x <= hs1.leftOp!.pt.x)) continue;
|
|
1048
|
+
const currY = hs1.leftOp!.pt.y;
|
|
1049
|
+
if (hs1.leftToRight) {
|
|
1050
|
+
while (hs1.leftOp!.next!.pt.y === currY &&
|
|
1051
|
+
hs1.leftOp!.next!.pt.x <= hs2.leftOp!.pt.x)
|
|
1052
|
+
hs1.leftOp = hs1.leftOp!.next;
|
|
1053
|
+
while (hs2.leftOp!.prev.pt.y === currY &&
|
|
1054
|
+
hs2.leftOp!.prev.pt.x <= hs1.leftOp!.pt.x)
|
|
1055
|
+
hs2.leftOp = hs2.leftOp!.prev;
|
|
1056
|
+
const join = new HorzJoin(
|
|
1057
|
+
this.duplicateOp(hs1.leftOp!, true),
|
|
1058
|
+
this.duplicateOp(hs2.leftOp!, false));
|
|
1059
|
+
this.horzJoinList.push(join);
|
|
1060
|
+
} else {
|
|
1061
|
+
while (hs1.leftOp!.prev.pt.y === currY &&
|
|
1062
|
+
hs1.leftOp!.prev.pt.x <= hs2.leftOp!.pt.x)
|
|
1063
|
+
hs1.leftOp = hs1.leftOp!.prev;
|
|
1064
|
+
while (hs2.leftOp!.next!.pt.y === currY &&
|
|
1065
|
+
hs2.leftOp!.next!.pt.x <= hs1.leftOp!.pt.x)
|
|
1066
|
+
hs2.leftOp = hs2.leftOp!.next;
|
|
1067
|
+
const join = new HorzJoin(
|
|
1068
|
+
this.duplicateOp(hs2.leftOp!, true),
|
|
1069
|
+
this.duplicateOp(hs1.leftOp!, false));
|
|
1070
|
+
this.horzJoinList.push(join);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
private updateHorzSegment(hs: HorzSegment): boolean {
|
|
1077
|
+
const op = hs.leftOp!;
|
|
1078
|
+
const outrec = this.getRealOutRec(op.outrec)!;
|
|
1079
|
+
const outrecHasEdges = outrec.frontEdge !== null;
|
|
1080
|
+
const currY = op.pt.y;
|
|
1081
|
+
let opP = op;
|
|
1082
|
+
let opN = op;
|
|
1083
|
+
|
|
1084
|
+
if (outrecHasEdges) {
|
|
1085
|
+
const opA = outrec.pts!;
|
|
1086
|
+
const opZ = opA.next!;
|
|
1087
|
+
while (opP !== opZ && opP.prev.pt.y === currY)
|
|
1088
|
+
opP = opP.prev;
|
|
1089
|
+
while (opN !== opA && opN.next!.pt.y === currY)
|
|
1090
|
+
opN = opN.next!;
|
|
1091
|
+
} else {
|
|
1092
|
+
while (opP.prev !== opN && opP.prev.pt.y === currY)
|
|
1093
|
+
opP = opP.prev;
|
|
1094
|
+
while (opN.next !== opP && opN.next!.pt.y === currY)
|
|
1095
|
+
opN = opN.next!;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const result = this.setHorzSegHeadingForward(hs, opP, opN) && hs.leftOp!.horz === null;
|
|
1099
|
+
|
|
1100
|
+
if (result) {
|
|
1101
|
+
hs.leftOp!.horz = hs;
|
|
1102
|
+
} else {
|
|
1103
|
+
hs.rightOp = null; // (for sorting)
|
|
1104
|
+
}
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private setHorzSegHeadingForward(hs: HorzSegment, opP: OutPt, opN: OutPt): boolean {
|
|
1109
|
+
if (opP.pt.x === opN.pt.x) return false;
|
|
1110
|
+
if (opP.pt.x < opN.pt.x) {
|
|
1111
|
+
hs.leftOp = opP;
|
|
1112
|
+
hs.rightOp = opN;
|
|
1113
|
+
hs.leftToRight = true;
|
|
1114
|
+
} else {
|
|
1115
|
+
hs.leftOp = opN;
|
|
1116
|
+
hs.rightOp = opP;
|
|
1117
|
+
hs.leftToRight = false;
|
|
1118
|
+
}
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private horzSegSort(hs1: HorzSegment, hs2: HorzSegment): number {
|
|
1123
|
+
if (hs1.rightOp === null) {
|
|
1124
|
+
return hs2.rightOp === null ? 0 : 1;
|
|
1125
|
+
}
|
|
1126
|
+
if (hs2.rightOp === null) return -1;
|
|
1127
|
+
return hs1.leftOp!.pt.x - hs2.leftOp!.pt.x;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
protected duplicateOp(op: OutPt, insertAfter: boolean): OutPt {
|
|
1131
|
+
const result = new OutPt(op.pt, op.outrec);
|
|
1132
|
+
if (insertAfter) {
|
|
1133
|
+
result.next = op.next;
|
|
1134
|
+
result.next!.prev = result;
|
|
1135
|
+
result.prev = op;
|
|
1136
|
+
op.next = result;
|
|
1137
|
+
} else {
|
|
1138
|
+
result.prev = op.prev;
|
|
1139
|
+
result.prev.next = result;
|
|
1140
|
+
result.next = op;
|
|
1141
|
+
op.prev = result;
|
|
1142
|
+
}
|
|
1143
|
+
return result;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
protected getRealOutRec(outRec: OutRec | null): OutRec | null {
|
|
1147
|
+
while (outRec !== null && outRec.pts === null) {
|
|
1148
|
+
outRec = outRec.owner;
|
|
1149
|
+
}
|
|
1150
|
+
return outRec;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private doIntersections(y: number): void {
|
|
1154
|
+
if (this.buildIntersectList(y)) {
|
|
1155
|
+
this.processIntersectList();
|
|
1156
|
+
this.disposeIntersectNodes();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private doTopOfScanbeam(y: number): void {
|
|
1161
|
+
this.sel = null; // sel is reused to flag horizontals (see PushHorz below)
|
|
1162
|
+
let ae = this.actives;
|
|
1163
|
+
while (ae !== null) {
|
|
1164
|
+
// NB 'ae' will never be horizontal here
|
|
1165
|
+
if (ae.top.y === y) {
|
|
1166
|
+
ae.curX = ae.top.x;
|
|
1167
|
+
if (ClipperBase.isMaxima(ae)) {
|
|
1168
|
+
ae = this.doMaxima(ae); // TOP OF BOUND (MAXIMA)
|
|
1169
|
+
continue;
|
|
1170
|
+
} else {
|
|
1171
|
+
// INTERMEDIATE VERTEX ...
|
|
1172
|
+
if (ClipperBase.isHotEdge(ae)) this.addOutPt(ae, ae.top);
|
|
1173
|
+
this.updateEdgeIntoAEL(ae);
|
|
1174
|
+
if (ClipperBase.isHorizontal(ae)) {
|
|
1175
|
+
this.pushHorz(ae); // horizontals are processed later
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} else { // i.e. not the top of the edge
|
|
1179
|
+
ae.curX = ClipperBase.topX(ae, y); // TopX already returns correctly rounded integer
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
ae = ae.nextInAEL;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private processHorzJoins(): void {
|
|
1187
|
+
for (const j of this.horzJoinList) {
|
|
1188
|
+
const or1 = this.getRealOutRec(j.op1!.outrec)!;
|
|
1189
|
+
const or2 = this.getRealOutRec(j.op2!.outrec)!;
|
|
1190
|
+
|
|
1191
|
+
const op1b = j.op1!.next!;
|
|
1192
|
+
const op2b = j.op2!.prev;
|
|
1193
|
+
j.op1!.next = j.op2;
|
|
1194
|
+
j.op2!.prev = j.op1!;
|
|
1195
|
+
op1b.prev = op2b;
|
|
1196
|
+
op2b.next = op1b;
|
|
1197
|
+
|
|
1198
|
+
if (or1 === or2) { // 'join' is really a split
|
|
1199
|
+
const or2New = this.newOutRec();
|
|
1200
|
+
or2New.pts = op1b;
|
|
1201
|
+
this.fixOutRecPts(or2New);
|
|
1202
|
+
|
|
1203
|
+
//if or1->pts has moved to or2 then update or1->pts!!
|
|
1204
|
+
if (or1.pts!.outrec === or2New) {
|
|
1205
|
+
or1.pts = j.op1;
|
|
1206
|
+
or1.pts!.outrec = or1;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (this.usingPolytree) {
|
|
1210
|
+
if (this.path1InsidePath2(or1.pts!, or2New.pts!)) {
|
|
1211
|
+
//swap or1's & or2's pts
|
|
1212
|
+
[or2New.pts, or1.pts] = [or1.pts, or2New.pts];
|
|
1213
|
+
this.fixOutRecPts(or1);
|
|
1214
|
+
this.fixOutRecPts(or2New);
|
|
1215
|
+
//or2 is now inside or1
|
|
1216
|
+
or2New.owner = or1;
|
|
1217
|
+
} else if (this.path1InsidePath2(or2New.pts!, or1.pts!)) {
|
|
1218
|
+
or2New.owner = or1;
|
|
1219
|
+
} else {
|
|
1220
|
+
or2New.owner = or1.owner;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (or1.splits === null) or1.splits = [];
|
|
1224
|
+
or1.splits.push(or2New.idx);
|
|
1225
|
+
} else {
|
|
1226
|
+
or2New.owner = or1;
|
|
1227
|
+
}
|
|
1228
|
+
} else {
|
|
1229
|
+
or2.pts = null;
|
|
1230
|
+
if (this.usingPolytree) {
|
|
1231
|
+
this.setOwner(or2, or1);
|
|
1232
|
+
this.moveSplits(or2, or1);
|
|
1233
|
+
} else {
|
|
1234
|
+
or2.owner = or1;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
private fixOutRecPts(outrec: OutRec): void {
|
|
1241
|
+
let op = outrec.pts!;
|
|
1242
|
+
do {
|
|
1243
|
+
op.outrec = outrec;
|
|
1244
|
+
op = op.next!;
|
|
1245
|
+
} while (op !== outrec.pts);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
protected path1InsidePath2(op1: OutPt, op2: OutPt): boolean {
|
|
1249
|
+
// we need to make some accommodation for rounding errors
|
|
1250
|
+
// so we won't jump if the first vertex is found outside
|
|
1251
|
+
let pip = PointInPolygonResult.IsOn;
|
|
1252
|
+
let op = op1;
|
|
1253
|
+
do {
|
|
1254
|
+
switch (this.pointInOpPolygon(op.pt, op2)) {
|
|
1255
|
+
case PointInPolygonResult.IsOutside:
|
|
1256
|
+
if (pip === PointInPolygonResult.IsOutside) return false;
|
|
1257
|
+
pip = PointInPolygonResult.IsOutside;
|
|
1258
|
+
break;
|
|
1259
|
+
case PointInPolygonResult.IsInside:
|
|
1260
|
+
if (pip === PointInPolygonResult.IsInside) return true;
|
|
1261
|
+
pip = PointInPolygonResult.IsInside;
|
|
1262
|
+
break;
|
|
1263
|
+
default:
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
op = op.next!;
|
|
1267
|
+
} while (op !== op1);
|
|
1268
|
+
// result is unclear, so try again using cleaned paths
|
|
1269
|
+
return InternalClipper.path2ContainsPath1(this.getCleanPath(op1), this.getCleanPath(op2)); // (#973)
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private pointInOpPolygon(pt: Point64, op: OutPt): PointInPolygonResult {
|
|
1273
|
+
if (op === op.next || op.prev === op.next) {
|
|
1274
|
+
return PointInPolygonResult.IsOutside;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
let op2 = op;
|
|
1278
|
+
do {
|
|
1279
|
+
if (op.pt.y !== pt.y) break;
|
|
1280
|
+
op = op.next!;
|
|
1281
|
+
} while (op !== op2);
|
|
1282
|
+
if (op.pt.y === pt.y) // not a proper polygon
|
|
1283
|
+
return PointInPolygonResult.IsOutside;
|
|
1284
|
+
|
|
1285
|
+
// must be above or below to get here
|
|
1286
|
+
let isAbove = op.pt.y < pt.y;
|
|
1287
|
+
const startingAbove = isAbove;
|
|
1288
|
+
let val = 0;
|
|
1289
|
+
|
|
1290
|
+
op2 = op.next!;
|
|
1291
|
+
while (op2 !== op) {
|
|
1292
|
+
if (isAbove) {
|
|
1293
|
+
while (op2 !== op && op2.pt.y < pt.y) op2 = op2.next!;
|
|
1294
|
+
} else {
|
|
1295
|
+
while (op2 !== op && op2.pt.y > pt.y) op2 = op2.next!;
|
|
1296
|
+
}
|
|
1297
|
+
if (op2 === op) break;
|
|
1298
|
+
|
|
1299
|
+
// must have touched or crossed the pt.Y horizontal
|
|
1300
|
+
// and this must happen an even number of times
|
|
1301
|
+
|
|
1302
|
+
if (op2.pt.y === pt.y) { // touching the horizontal
|
|
1303
|
+
if (op2.pt.x === pt.x || (op2.pt.y === op2.prev.pt.y &&
|
|
1304
|
+
(pt.x < op2.prev.pt.x) !== (pt.x < op2.pt.x)))
|
|
1305
|
+
return PointInPolygonResult.IsOn;
|
|
1306
|
+
op2 = op2.next!;
|
|
1307
|
+
if (op2 === op) break;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (op2.pt.x <= pt.x || op2.prev.pt.x <= pt.x) {
|
|
1312
|
+
if ((op2.prev.pt.x < pt.x && op2.pt.x < pt.x)) {
|
|
1313
|
+
val = 1 - val; // toggle val
|
|
1314
|
+
} else {
|
|
1315
|
+
const d = InternalClipper.crossProduct(op2.prev.pt, op2.pt, pt);
|
|
1316
|
+
if (d === 0) return PointInPolygonResult.IsOn;
|
|
1317
|
+
if ((d < 0) === isAbove) val = 1 - val;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
isAbove = !isAbove;
|
|
1321
|
+
op2 = op2.next!;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (isAbove === startingAbove) return val === 0 ? PointInPolygonResult.IsOutside : PointInPolygonResult.IsInside;
|
|
1325
|
+
{
|
|
1326
|
+
const d = InternalClipper.crossProduct(op2.prev.pt, op2.pt, pt);
|
|
1327
|
+
if (d === 0) return PointInPolygonResult.IsOn;
|
|
1328
|
+
if ((d < 0) === isAbove) val = 1 - val;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return val === 0 ? PointInPolygonResult.IsOutside : PointInPolygonResult.IsInside;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
private getCleanPath(op: OutPt): Path64 {
|
|
1335
|
+
const result: Path64 = [];
|
|
1336
|
+
let op2 = op;
|
|
1337
|
+
while (op2.next !== op &&
|
|
1338
|
+
((op2.pt.x === op2.next!.pt.x && op2.pt.x === op2.prev.pt.x) ||
|
|
1339
|
+
(op2.pt.y === op2.next!.pt.y && op2.pt.y === op2.prev.pt.y))) op2 = op2.next!;
|
|
1340
|
+
result.push(op2.pt);
|
|
1341
|
+
let prevOp = op2;
|
|
1342
|
+
op2 = op2.next!;
|
|
1343
|
+
while (op2 !== op) {
|
|
1344
|
+
if ((op2.pt.x !== op2.next!.pt.x || op2.pt.x !== prevOp.pt.x) &&
|
|
1345
|
+
(op2.pt.y !== op2.next!.pt.y || op2.pt.y !== prevOp.pt.y)) {
|
|
1346
|
+
result.push(op2.pt);
|
|
1347
|
+
prevOp = op2;
|
|
1348
|
+
}
|
|
1349
|
+
op2 = op2.next!;
|
|
1350
|
+
}
|
|
1351
|
+
return result;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private moveSplits(fromOr: OutRec, toOr: OutRec): void {
|
|
1355
|
+
if (fromOr.splits === null) return;
|
|
1356
|
+
if (toOr.splits === null) toOr.splits = [];
|
|
1357
|
+
for (const i of fromOr.splits) {
|
|
1358
|
+
if (i !== toOr.idx) {
|
|
1359
|
+
toOr.splits.push(i);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
fromOr.splits = null;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private buildIntersectList(topY: number): boolean {
|
|
1366
|
+
if (this.actives?.nextInAEL === null) return false;
|
|
1367
|
+
|
|
1368
|
+
// Calculate edge positions at the top of the current scanbeam, and from this
|
|
1369
|
+
// we will determine the intersections required to reach these new positions.
|
|
1370
|
+
this.adjustCurrXAndCopyToSEL(topY);
|
|
1371
|
+
|
|
1372
|
+
// Find all edge intersections in the current scanbeam using a stable merge
|
|
1373
|
+
// sort that ensures only adjacent edges are intersecting. Intersect info is
|
|
1374
|
+
// stored in intersectList ready to be processed in ProcessIntersectList.
|
|
1375
|
+
// Re merge sorts see https://stackoverflow.com/a/46319131/359538
|
|
1376
|
+
|
|
1377
|
+
let left = this.sel;
|
|
1378
|
+
|
|
1379
|
+
while (left !== null && left.jump !== null) {
|
|
1380
|
+
let prevBase: Active | null = null;
|
|
1381
|
+
while (left !== null && left.jump !== null) {
|
|
1382
|
+
let currBase = left;
|
|
1383
|
+
let right: Active | null = left.jump;
|
|
1384
|
+
let lEnd: Active | null = right;
|
|
1385
|
+
const rEnd: Active | null = right?.jump || null;
|
|
1386
|
+
left.jump = rEnd;
|
|
1387
|
+
|
|
1388
|
+
while (left !== lEnd && right !== rEnd) {
|
|
1389
|
+
if (right!.curX < left!.curX) {
|
|
1390
|
+
let tmp = right!.prevInSEL!;
|
|
1391
|
+
while (true) {
|
|
1392
|
+
this.addNewIntersectNode(tmp, right!, topY);
|
|
1393
|
+
if (tmp === left) break;
|
|
1394
|
+
tmp = tmp.prevInSEL!;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
tmp = right!;
|
|
1398
|
+
right = this.extractFromSEL(tmp);
|
|
1399
|
+
lEnd = right; // Update lEnd - this is the critical fix!
|
|
1400
|
+
if (left !== null) this.insert1Before2InSEL(tmp, left);
|
|
1401
|
+
if (left !== currBase) continue;
|
|
1402
|
+
currBase = tmp;
|
|
1403
|
+
currBase.jump = rEnd;
|
|
1404
|
+
if (prevBase === null) {
|
|
1405
|
+
this.sel = currBase;
|
|
1406
|
+
} else {
|
|
1407
|
+
prevBase.jump = currBase;
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
left = left!.nextInSEL;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
prevBase = currBase;
|
|
1415
|
+
left = rEnd;
|
|
1416
|
+
}
|
|
1417
|
+
left = this.sel;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return this.intersectList.length > 0;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
private processIntersectList(): void {
|
|
1424
|
+
// We now have a list of intersections required so that edges will be
|
|
1425
|
+
// correctly positioned at the top of the scanbeam. However, it's important
|
|
1426
|
+
// that edge intersections are processed from the bottom up, but it's also
|
|
1427
|
+
// crucial that intersections only occur between adjacent edges.
|
|
1428
|
+
|
|
1429
|
+
// First we do a quicksort so intersections proceed in a bottom up order ...
|
|
1430
|
+
this.intersectList.sort((a, b) => {
|
|
1431
|
+
if (a.pt.y !== b.pt.y) return (a.pt.y > b.pt.y) ? -1 : 1;
|
|
1432
|
+
if (a.pt.x !== b.pt.x) return (a.pt.x < b.pt.x) ? -1 : 1;
|
|
1433
|
+
// Tiebreaker: when points are identical, sort by edge1's curX position
|
|
1434
|
+
// This provides deterministic ordering matching C# IntroSort behavior
|
|
1435
|
+
if (a.edge1.curX !== b.edge1.curX) return (a.edge1.curX < b.edge1.curX) ? -1 : 1;
|
|
1436
|
+
// Final tiebreaker: edge2's curX
|
|
1437
|
+
return (a.edge2.curX < b.edge2.curX) ? -1 : (a.edge2.curX > b.edge2.curX) ? 1 : 0;
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// Now as we process these intersections, we must sometimes adjust the order
|
|
1441
|
+
// to ensure that intersecting edges are always adjacent ...
|
|
1442
|
+
for (let i = 0; i < this.intersectList.length; ++i) {
|
|
1443
|
+
if (!this.edgesAdjacentInAEL(this.intersectList[i])) {
|
|
1444
|
+
let j = i + 1;
|
|
1445
|
+
while (!this.edgesAdjacentInAEL(this.intersectList[j])) j++;
|
|
1446
|
+
// swap
|
|
1447
|
+
[this.intersectList[j], this.intersectList[i]] = [this.intersectList[i], this.intersectList[j]];
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const node = this.intersectList[i];
|
|
1451
|
+
this.intersectEdges(node.edge1, node.edge2, node.pt);
|
|
1452
|
+
this.swapPositionsInAEL(node.edge1, node.edge2);
|
|
1453
|
+
|
|
1454
|
+
node.edge1.curX = node.pt.x;
|
|
1455
|
+
node.edge2.curX = node.pt.x;
|
|
1456
|
+
this.checkJoinLeft(node.edge2, node.pt, true);
|
|
1457
|
+
this.checkJoinRight(node.edge1, node.pt, true);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
private edgesAdjacentInAEL(inode: IntersectNode): boolean {
|
|
1462
|
+
return (inode.edge1.nextInAEL === inode.edge2) || (inode.edge1.prevInAEL === inode.edge2);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
private adjustCurrXAndCopyToSEL(topY: number): void {
|
|
1466
|
+
let ae = this.actives;
|
|
1467
|
+
this.sel = ae;
|
|
1468
|
+
while (ae !== null) {
|
|
1469
|
+
ae.prevInSEL = ae.prevInAEL;
|
|
1470
|
+
ae.nextInSEL = ae.nextInAEL;
|
|
1471
|
+
ae.jump = ae.nextInSEL;
|
|
1472
|
+
// it is safe to ignore 'joined' edges here because
|
|
1473
|
+
// if necessary they will be split in IntersectEdges()
|
|
1474
|
+
ae.curX = ClipperBase.topX(ae, topY);
|
|
1475
|
+
// NB don't update ae.curr.Y yet (see AddNewIntersectNode)
|
|
1476
|
+
ae = ae.nextInAEL;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private doMaxima(ae: Active): Active | null {
|
|
1481
|
+
const prevE = ae.prevInAEL;
|
|
1482
|
+
let nextE = ae.nextInAEL;
|
|
1483
|
+
|
|
1484
|
+
if (ClipperBase.isOpenEnd(ae)) {
|
|
1485
|
+
if (ClipperBase.isHotEdge(ae)) this.addOutPt(ae, ae.top);
|
|
1486
|
+
if (ClipperBase.isHorizontal(ae)) return nextE;
|
|
1487
|
+
if (ClipperBase.isHotEdge(ae)) {
|
|
1488
|
+
if (ClipperBase.isFront(ae)) {
|
|
1489
|
+
ae.outrec!.frontEdge = null;
|
|
1490
|
+
} else {
|
|
1491
|
+
ae.outrec!.backEdge = null;
|
|
1492
|
+
}
|
|
1493
|
+
ae.outrec = null;
|
|
1494
|
+
}
|
|
1495
|
+
this.deleteFromAEL(ae);
|
|
1496
|
+
return nextE;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const maxPair = ClipperBase.getMaximaPair(ae);
|
|
1500
|
+
if (maxPair === null) return nextE; // eMaxPair is horizontal
|
|
1501
|
+
|
|
1502
|
+
if (this.isJoined(ae)) this.split(ae, ae.top);
|
|
1503
|
+
if (this.isJoined(maxPair)) this.split(maxPair, maxPair.top);
|
|
1504
|
+
|
|
1505
|
+
// only non-horizontal maxima here.
|
|
1506
|
+
// process any edges between maxima pair ...
|
|
1507
|
+
while (nextE !== maxPair) {
|
|
1508
|
+
this.intersectEdges(ae, nextE!, ae.top);
|
|
1509
|
+
this.swapPositionsInAEL(ae, nextE!);
|
|
1510
|
+
nextE = ae.nextInAEL;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (ClipperBase.isOpen(ae)) {
|
|
1514
|
+
if (ClipperBase.isHotEdge(ae)) {
|
|
1515
|
+
this.addLocalMaxPoly(ae, maxPair, ae.top);
|
|
1516
|
+
}
|
|
1517
|
+
this.deleteFromAEL(maxPair);
|
|
1518
|
+
this.deleteFromAEL(ae);
|
|
1519
|
+
return (prevE !== null ? prevE.nextInAEL : this.actives);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// here ae.nextInAel == ENext == EMaxPair ...
|
|
1523
|
+
if (ClipperBase.isHotEdge(ae)) {
|
|
1524
|
+
this.addLocalMaxPoly(ae, maxPair, ae.top);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
this.deleteFromAEL(ae);
|
|
1528
|
+
this.deleteFromAEL(maxPair);
|
|
1529
|
+
return (prevE !== null ? prevE.nextInAEL : this.actives);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private updateEdgeIntoAEL(ae: Active): void {
|
|
1533
|
+
ae.bot = { x: ae.top.x, y: ae.top.y }; // Create copy
|
|
1534
|
+
ae.vertexTop = ClipperBase.nextVertex(ae);
|
|
1535
|
+
ae.top = { x: ae.vertexTop!.pt.x, y: ae.vertexTop!.pt.y }; // Create copy
|
|
1536
|
+
ae.curX = ae.bot.x;
|
|
1537
|
+
ClipperBase.setDx(ae);
|
|
1538
|
+
|
|
1539
|
+
if (this.isJoined(ae)) this.split(ae, ae.bot);
|
|
1540
|
+
|
|
1541
|
+
if (ClipperBase.isHorizontal(ae)) {
|
|
1542
|
+
if (!ClipperBase.isOpen(ae)) this.trimHorz(ae, this.preserveCollinear);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
this.insertScanline(ae.top.y);
|
|
1546
|
+
|
|
1547
|
+
this.checkJoinLeft(ae, ae.bot);
|
|
1548
|
+
this.checkJoinRight(ae, ae.bot, true); // (#500)
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
private trimHorz(horzEdge: Active, preserveCollinear: boolean): void {
|
|
1552
|
+
let wasTrimmed = false;
|
|
1553
|
+
let pt = ClipperBase.nextVertex(horzEdge).pt;
|
|
1554
|
+
|
|
1555
|
+
while (pt.y === horzEdge.top.y) {
|
|
1556
|
+
// always trim 180 deg. spikes (in closed paths)
|
|
1557
|
+
// but otherwise break if preserveCollinear = true
|
|
1558
|
+
if (preserveCollinear &&
|
|
1559
|
+
(pt.x < horzEdge.top.x) !== (horzEdge.bot.x < horzEdge.top.x)) {
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
horzEdge.vertexTop = ClipperBase.nextVertex(horzEdge);
|
|
1564
|
+
horzEdge.top = pt;
|
|
1565
|
+
wasTrimmed = true;
|
|
1566
|
+
if (ClipperBase.isMaxima(horzEdge)) break;
|
|
1567
|
+
pt = ClipperBase.nextVertex(horzEdge).pt;
|
|
1568
|
+
}
|
|
1569
|
+
if (wasTrimmed) ClipperBase.setDx(horzEdge); // +/-infinity
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
private addToHorzSegList(op: OutPt): void {
|
|
1573
|
+
if (op.outrec.isOpen) return;
|
|
1574
|
+
this.horzSegList.push(new HorzSegment(op));
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
private addNewIntersectNode(ae1: Active, ae2: Active, topY: number): void {
|
|
1578
|
+
const intersectResult = InternalClipper.getLineIntersectPt(ae1.bot, ae1.top, ae2.bot, ae2.top);
|
|
1579
|
+
let ip: Point64;
|
|
1580
|
+
|
|
1581
|
+
if (!intersectResult.intersects) {
|
|
1582
|
+
ip = { x: ae1.curX, y: topY }; // parallel edges
|
|
1583
|
+
} else {
|
|
1584
|
+
ip = intersectResult.point;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (ip.y > this.currentBotY || ip.y < topY) {
|
|
1588
|
+
const absDx1 = Math.abs(ae1.dx);
|
|
1589
|
+
const absDx2 = Math.abs(ae2.dx);
|
|
1590
|
+
|
|
1591
|
+
if (absDx1 > 100 && absDx2 > 100) {
|
|
1592
|
+
if (absDx1 > absDx2) {
|
|
1593
|
+
ip = InternalClipper.getClosestPtOnSegment(ip, ae1.bot, ae1.top);
|
|
1594
|
+
} else {
|
|
1595
|
+
ip = InternalClipper.getClosestPtOnSegment(ip, ae2.bot, ae2.top);
|
|
1596
|
+
}
|
|
1597
|
+
} else if (absDx1 > 100) {
|
|
1598
|
+
ip = InternalClipper.getClosestPtOnSegment(ip, ae1.bot, ae1.top);
|
|
1599
|
+
} else if (absDx2 > 100) {
|
|
1600
|
+
ip = InternalClipper.getClosestPtOnSegment(ip, ae2.bot, ae2.top);
|
|
1601
|
+
} else {
|
|
1602
|
+
if (ip.y < topY) ip.y = topY;
|
|
1603
|
+
else ip.y = this.currentBotY;
|
|
1604
|
+
if (absDx1 < absDx2) ip.x = ClipperBase.topX(ae1, ip.y);
|
|
1605
|
+
else ip.x = ClipperBase.topX(ae2, ip.y);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const node = createIntersectNode(ip, ae1, ae2);
|
|
1610
|
+
this.intersectList.push(node);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private extractFromSEL(ae: Active): Active | null {
|
|
1614
|
+
const res = ae.nextInSEL;
|
|
1615
|
+
if (res !== null) {
|
|
1616
|
+
res.prevInSEL = ae.prevInSEL;
|
|
1617
|
+
}
|
|
1618
|
+
ae.prevInSEL!.nextInSEL = res;
|
|
1619
|
+
return res;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
private insert1Before2InSEL(ae1: Active, ae2: Active): void {
|
|
1623
|
+
ae1.prevInSEL = ae2.prevInSEL;
|
|
1624
|
+
if (ae1.prevInSEL !== null) {
|
|
1625
|
+
ae1.prevInSEL.nextInSEL = ae1;
|
|
1626
|
+
}
|
|
1627
|
+
ae1.nextInSEL = ae2;
|
|
1628
|
+
ae2.prevInSEL = ae1;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private getCurrYMaximaVertexOpen(ae: Active): Vertex | null {
|
|
1632
|
+
let result = ae.vertexTop;
|
|
1633
|
+
if (ae.windDx > 0) {
|
|
1634
|
+
while (result!.next!.pt.y === result!.pt.y &&
|
|
1635
|
+
((result!.flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) === VertexFlags.None))
|
|
1636
|
+
result = result!.next;
|
|
1637
|
+
} else {
|
|
1638
|
+
while (result!.prev!.pt.y === result!.pt.y &&
|
|
1639
|
+
((result!.flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) === VertexFlags.None))
|
|
1640
|
+
result = result!.prev;
|
|
1641
|
+
}
|
|
1642
|
+
if (!ClipperBase.isMaxima(result!)) result = null; // not a maxima
|
|
1643
|
+
return result;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
private getCurrYMaximaVertex(ae: Active): Vertex | null {
|
|
1647
|
+
let result = ae.vertexTop;
|
|
1648
|
+
if (ae.windDx > 0) {
|
|
1649
|
+
while (result!.next!.pt.y === result!.pt.y) result = result!.next;
|
|
1650
|
+
} else {
|
|
1651
|
+
while (result!.prev!.pt.y === result!.pt.y) result = result!.prev;
|
|
1652
|
+
}
|
|
1653
|
+
if (!ClipperBase.isMaxima(result!)) result = null; // not a maxima
|
|
1654
|
+
return result;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
private resetHorzDirection(horz: Active, vertexMax: Vertex | null): { isLeftToRight: boolean; leftX: number; rightX: number } {
|
|
1658
|
+
if (horz.bot.x === horz.top.x) {
|
|
1659
|
+
// the horizontal edge is going nowhere ...
|
|
1660
|
+
const leftX = horz.curX;
|
|
1661
|
+
const rightX = horz.curX;
|
|
1662
|
+
let ae = horz.nextInAEL;
|
|
1663
|
+
while (ae !== null && ae.vertexTop !== vertexMax)
|
|
1664
|
+
ae = ae.nextInAEL;
|
|
1665
|
+
return { isLeftToRight: ae !== null, leftX, rightX };
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (horz.curX < horz.top.x) {
|
|
1669
|
+
return { isLeftToRight: true, leftX: horz.curX, rightX: horz.top.x };
|
|
1670
|
+
} else {
|
|
1671
|
+
return { isLeftToRight: false, leftX: horz.top.x, rightX: horz.curX };
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
private getLastOp(hotEdge: Active): OutPt {
|
|
1676
|
+
const outrec = hotEdge.outrec!;
|
|
1677
|
+
return (hotEdge === outrec.frontEdge) ?
|
|
1678
|
+
outrec.pts! : outrec.pts!.next!;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
private insertLeftEdge(ae: Active): void {
|
|
1682
|
+
if (this.actives === null) {
|
|
1683
|
+
ae.prevInAEL = null;
|
|
1684
|
+
ae.nextInAEL = null;
|
|
1685
|
+
this.actives = ae;
|
|
1686
|
+
} else if (!this.isValidAelOrder(this.actives, ae)) {
|
|
1687
|
+
ae.prevInAEL = null;
|
|
1688
|
+
ae.nextInAEL = this.actives;
|
|
1689
|
+
this.actives.prevInAEL = ae;
|
|
1690
|
+
this.actives = ae;
|
|
1691
|
+
} else {
|
|
1692
|
+
let ae2 = this.actives;
|
|
1693
|
+
while (ae2.nextInAEL !== null && this.isValidAelOrder(ae2.nextInAEL, ae)) {
|
|
1694
|
+
ae2 = ae2.nextInAEL;
|
|
1695
|
+
}
|
|
1696
|
+
//don't separate joined edges
|
|
1697
|
+
if (ae2.joinWith === JoinWith.Right) ae2 = ae2.nextInAEL!;
|
|
1698
|
+
ae.nextInAEL = ae2.nextInAEL;
|
|
1699
|
+
if (ae2.nextInAEL !== null) ae2.nextInAEL.prevInAEL = ae;
|
|
1700
|
+
ae.prevInAEL = ae2;
|
|
1701
|
+
ae2.nextInAEL = ae;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
private insertRightEdge(ae1: Active, ae2: Active): void {
|
|
1706
|
+
ae2.nextInAEL = ae1.nextInAEL;
|
|
1707
|
+
if (ae1.nextInAEL !== null) ae1.nextInAEL.prevInAEL = ae2;
|
|
1708
|
+
ae2.prevInAEL = ae1;
|
|
1709
|
+
ae1.nextInAEL = ae2;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
private setWindCountForOpenPathEdge(ae: Active): void {
|
|
1713
|
+
let ae2 = this.actives;
|
|
1714
|
+
if (this.fillrule === FillRule.EvenOdd) {
|
|
1715
|
+
let cnt1 = 0, cnt2 = 0;
|
|
1716
|
+
while (ae2 !== ae) {
|
|
1717
|
+
if (ClipperBase.getPolyType(ae2!) === PathType.Clip) {
|
|
1718
|
+
cnt2++;
|
|
1719
|
+
} else if (!ClipperBase.isOpen(ae2!)) {
|
|
1720
|
+
cnt1++;
|
|
1721
|
+
}
|
|
1722
|
+
ae2 = ae2!.nextInAEL;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
ae.windCount = (ClipperBase.isOdd(cnt1) ? 1 : 0);
|
|
1726
|
+
ae.windCount2 = (ClipperBase.isOdd(cnt2) ? 1 : 0);
|
|
1727
|
+
} else {
|
|
1728
|
+
while (ae2 !== ae) {
|
|
1729
|
+
if (ClipperBase.getPolyType(ae2!) === PathType.Clip) {
|
|
1730
|
+
ae.windCount2 += ae2!.windDx;
|
|
1731
|
+
} else if (!ClipperBase.isOpen(ae2!)) {
|
|
1732
|
+
ae.windCount += ae2!.windDx;
|
|
1733
|
+
}
|
|
1734
|
+
ae2 = ae2!.nextInAEL;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private setWindCountForClosedPathEdge(ae: Active): void {
|
|
1740
|
+
// Wind counts refer to polygon regions not edges, so here an edge's WindCnt
|
|
1741
|
+
// indicates the higher of the wind counts for the two regions touching the
|
|
1742
|
+
// edge. (nb: Adjacent regions can only ever have their wind counts differ by
|
|
1743
|
+
// one. Also, open paths have no meaningful wind directions or counts.)
|
|
1744
|
+
|
|
1745
|
+
let ae2 = ae.prevInAEL;
|
|
1746
|
+
// find the nearest closed path edge of the same PolyType in AEL (heading left)
|
|
1747
|
+
const pt = ClipperBase.getPolyType(ae);
|
|
1748
|
+
while (ae2 !== null && (ClipperBase.getPolyType(ae2) !== pt || ClipperBase.isOpen(ae2))) ae2 = ae2.prevInAEL;
|
|
1749
|
+
|
|
1750
|
+
if (ae2 === null) {
|
|
1751
|
+
ae.windCount = ae.windDx;
|
|
1752
|
+
ae2 = this.actives;
|
|
1753
|
+
} else if (this.fillrule === FillRule.EvenOdd) {
|
|
1754
|
+
ae.windCount = ae.windDx;
|
|
1755
|
+
ae.windCount2 = ae2.windCount2;
|
|
1756
|
+
ae2 = ae2.nextInAEL;
|
|
1757
|
+
} else {
|
|
1758
|
+
// NonZero, positive, or negative filling here ...
|
|
1759
|
+
// when e2's WindCnt is in the SAME direction as its WindDx,
|
|
1760
|
+
// then polygon will fill on the right of 'e2' (and 'e' will be inside)
|
|
1761
|
+
// nb: neither e2.WindCnt nor e2.WindDx should ever be 0.
|
|
1762
|
+
if (ae2.windCount * ae2.windDx < 0) {
|
|
1763
|
+
// opposite directions so 'ae' is outside 'ae2' ...
|
|
1764
|
+
if (Math.abs(ae2.windCount) > 1) {
|
|
1765
|
+
// outside prev poly but still inside another.
|
|
1766
|
+
if (ae2.windDx * ae.windDx < 0) {
|
|
1767
|
+
// reversing direction so use the same WC
|
|
1768
|
+
ae.windCount = ae2.windCount;
|
|
1769
|
+
} else {
|
|
1770
|
+
// otherwise keep 'reducing' the WC by 1 (i.e. towards 0) ...
|
|
1771
|
+
ae.windCount = ae2.windCount + ae.windDx;
|
|
1772
|
+
}
|
|
1773
|
+
} else {
|
|
1774
|
+
// now outside all polys of same polytype so set own WC ...
|
|
1775
|
+
ae.windCount = (ClipperBase.isOpen(ae) ? 1 : ae.windDx);
|
|
1776
|
+
}
|
|
1777
|
+
} else {
|
|
1778
|
+
//'ae' must be inside 'ae2'
|
|
1779
|
+
if (ae2.windDx * ae.windDx < 0) {
|
|
1780
|
+
// reversing direction so use the same WC
|
|
1781
|
+
ae.windCount = ae2.windCount;
|
|
1782
|
+
} else {
|
|
1783
|
+
// otherwise keep 'increasing' the WC by 1 (i.e. away from 0) ...
|
|
1784
|
+
ae.windCount = ae2.windCount + ae.windDx;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
ae.windCount2 = ae2.windCount2;
|
|
1789
|
+
ae2 = ae2.nextInAEL; // i.e. get ready to calc WindCnt2
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// update windCount2 ...
|
|
1793
|
+
if (this.fillrule === FillRule.EvenOdd) {
|
|
1794
|
+
while (ae2 !== ae) {
|
|
1795
|
+
if (ClipperBase.getPolyType(ae2!) !== pt && !ClipperBase.isOpen(ae2!)) {
|
|
1796
|
+
ae.windCount2 = (ae.windCount2 === 0 ? 1 : 0);
|
|
1797
|
+
}
|
|
1798
|
+
ae2 = ae2!.nextInAEL;
|
|
1799
|
+
}
|
|
1800
|
+
} else {
|
|
1801
|
+
while (ae2 !== ae) {
|
|
1802
|
+
if (ClipperBase.getPolyType(ae2!) !== pt && !ClipperBase.isOpen(ae2!)) {
|
|
1803
|
+
ae.windCount2 += ae2!.windDx;
|
|
1804
|
+
}
|
|
1805
|
+
ae2 = ae2!.nextInAEL;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
private isContributingOpen(ae: Active): boolean {
|
|
1811
|
+
let isInClip: boolean, isInSubj: boolean;
|
|
1812
|
+
switch (this.fillrule) {
|
|
1813
|
+
case FillRule.Positive:
|
|
1814
|
+
isInSubj = ae.windCount > 0;
|
|
1815
|
+
isInClip = ae.windCount2 > 0;
|
|
1816
|
+
break;
|
|
1817
|
+
case FillRule.Negative:
|
|
1818
|
+
isInSubj = ae.windCount < 0;
|
|
1819
|
+
isInClip = ae.windCount2 < 0;
|
|
1820
|
+
break;
|
|
1821
|
+
default:
|
|
1822
|
+
isInSubj = ae.windCount !== 0;
|
|
1823
|
+
isInClip = ae.windCount2 !== 0;
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
switch (this.cliptype) {
|
|
1828
|
+
case ClipType.Intersection: return isInClip;
|
|
1829
|
+
case ClipType.Union: return !isInSubj && !isInClip;
|
|
1830
|
+
default: return !isInClip;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
private isContributingClosed(ae: Active): boolean {
|
|
1835
|
+
switch (this.fillrule) {
|
|
1836
|
+
case FillRule.Positive:
|
|
1837
|
+
if (ae.windCount !== 1) return false;
|
|
1838
|
+
break;
|
|
1839
|
+
case FillRule.Negative:
|
|
1840
|
+
if (ae.windCount !== -1) return false;
|
|
1841
|
+
break;
|
|
1842
|
+
case FillRule.NonZero:
|
|
1843
|
+
if (Math.abs(ae.windCount) !== 1) return false;
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
switch (this.cliptype) {
|
|
1848
|
+
case ClipType.Intersection:
|
|
1849
|
+
return this.fillrule === FillRule.Positive ? ae.windCount2 > 0 :
|
|
1850
|
+
this.fillrule === FillRule.Negative ? ae.windCount2 < 0 :
|
|
1851
|
+
ae.windCount2 !== 0;
|
|
1852
|
+
|
|
1853
|
+
case ClipType.Union:
|
|
1854
|
+
return this.fillrule === FillRule.Positive ? ae.windCount2 <= 0 :
|
|
1855
|
+
this.fillrule === FillRule.Negative ? ae.windCount2 >= 0 :
|
|
1856
|
+
ae.windCount2 === 0;
|
|
1857
|
+
|
|
1858
|
+
case ClipType.Difference:
|
|
1859
|
+
const result = this.fillrule === FillRule.Positive ? (ae.windCount2 <= 0) :
|
|
1860
|
+
this.fillrule === FillRule.Negative ? (ae.windCount2 >= 0) :
|
|
1861
|
+
(ae.windCount2 === 0);
|
|
1862
|
+
return (ClipperBase.getPolyType(ae) === PathType.Subject) ? result : !result;
|
|
1863
|
+
|
|
1864
|
+
case ClipType.Xor:
|
|
1865
|
+
return true; // XOr is always contributing unless open
|
|
1866
|
+
|
|
1867
|
+
default:
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
private addLocalMinPoly(ae1: Active, ae2: Active, pt: Point64, isNew: boolean = false): OutPt {
|
|
1873
|
+
const outrec = this.newOutRec();
|
|
1874
|
+
ae1.outrec = outrec;
|
|
1875
|
+
ae2.outrec = outrec;
|
|
1876
|
+
|
|
1877
|
+
if (ClipperBase.isOpen(ae1)) {
|
|
1878
|
+
outrec.owner = null;
|
|
1879
|
+
outrec.isOpen = true;
|
|
1880
|
+
if (ae1.windDx > 0) {
|
|
1881
|
+
this.setSides(outrec, ae1, ae2);
|
|
1882
|
+
} else {
|
|
1883
|
+
this.setSides(outrec, ae2, ae1);
|
|
1884
|
+
}
|
|
1885
|
+
} else {
|
|
1886
|
+
outrec.isOpen = false;
|
|
1887
|
+
const prevHotEdge = ClipperBase.getPrevHotEdge(ae1);
|
|
1888
|
+
// e.windDx is the winding direction of the **input** paths
|
|
1889
|
+
// and unrelated to the winding direction of output polygons.
|
|
1890
|
+
// Output orientation is determined by e.outrec.frontE which is
|
|
1891
|
+
// the ascending edge (see AddLocalMinPoly).
|
|
1892
|
+
if (prevHotEdge !== null) {
|
|
1893
|
+
if (this.usingPolytree) {
|
|
1894
|
+
this.setOwner(outrec, prevHotEdge.outrec!);
|
|
1895
|
+
}
|
|
1896
|
+
outrec.owner = prevHotEdge.outrec;
|
|
1897
|
+
if (this.outrecIsAscending(prevHotEdge) === isNew) {
|
|
1898
|
+
this.setSides(outrec, ae2, ae1);
|
|
1899
|
+
} else {
|
|
1900
|
+
this.setSides(outrec, ae1, ae2);
|
|
1901
|
+
}
|
|
1902
|
+
} else {
|
|
1903
|
+
outrec.owner = null;
|
|
1904
|
+
if (isNew) {
|
|
1905
|
+
this.setSides(outrec, ae1, ae2);
|
|
1906
|
+
} else {
|
|
1907
|
+
this.setSides(outrec, ae2, ae1);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const op = new OutPt(pt, outrec);
|
|
1913
|
+
outrec.pts = op;
|
|
1914
|
+
return op;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
private outrecIsAscending(hotEdge: Active): boolean {
|
|
1918
|
+
return hotEdge === hotEdge.outrec!.frontEdge;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
protected newOutRec(): OutRec {
|
|
1922
|
+
const result = new OutRec();
|
|
1923
|
+
result.idx = this.outrecList.length;
|
|
1924
|
+
this.outrecList.push(result);
|
|
1925
|
+
return result;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
private startOpenPath(ae: Active, pt: Point64): OutPt {
|
|
1929
|
+
const outrec = this.newOutRec();
|
|
1930
|
+
outrec.isOpen = true;
|
|
1931
|
+
if (ae.windDx > 0) {
|
|
1932
|
+
outrec.frontEdge = ae;
|
|
1933
|
+
outrec.backEdge = null;
|
|
1934
|
+
} else {
|
|
1935
|
+
outrec.frontEdge = null;
|
|
1936
|
+
outrec.backEdge = ae;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
ae.outrec = outrec;
|
|
1940
|
+
const op = new OutPt(pt, outrec);
|
|
1941
|
+
outrec.pts = op;
|
|
1942
|
+
return op;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
private checkJoinLeft(ae: Active, pt: Point64, checkCurrX: boolean = false): void {
|
|
1946
|
+
const prev = ae.prevInAEL;
|
|
1947
|
+
if (prev === null ||
|
|
1948
|
+
!ClipperBase.isHotEdge(ae) || !ClipperBase.isHotEdge(prev) ||
|
|
1949
|
+
ClipperBase.isHorizontal(ae) || ClipperBase.isHorizontal(prev) ||
|
|
1950
|
+
ClipperBase.isOpen(ae) || ClipperBase.isOpen(prev)) return;
|
|
1951
|
+
if ((pt.y < ae.top.y + 2 || pt.y < prev.top.y + 2) && // avoid trivial joins
|
|
1952
|
+
((ae.bot.y > pt.y) || (prev.bot.y > pt.y))) return; // (#490)
|
|
1953
|
+
|
|
1954
|
+
if (checkCurrX) {
|
|
1955
|
+
if (this.perpendicDistFromLineSqrd(pt, prev.bot, prev.top) > 0.25) return;
|
|
1956
|
+
} else if (ae.curX !== prev.curX) return;
|
|
1957
|
+
if (!InternalClipper.isCollinear(ae.top, pt, prev.top)) return;
|
|
1958
|
+
|
|
1959
|
+
if (ae.outrec!.idx === prev.outrec!.idx) {
|
|
1960
|
+
this.addLocalMaxPoly(prev, ae, pt);
|
|
1961
|
+
} else if (ae.outrec!.idx < prev.outrec!.idx) {
|
|
1962
|
+
this.joinOutrecPaths(ae, prev);
|
|
1963
|
+
} else {
|
|
1964
|
+
this.joinOutrecPaths(prev, ae);
|
|
1965
|
+
}
|
|
1966
|
+
prev.joinWith = JoinWith.Right;
|
|
1967
|
+
ae.joinWith = JoinWith.Left;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
private checkJoinRight(ae: Active, pt: Point64, checkCurrX: boolean = false): void {
|
|
1971
|
+
const next = ae.nextInAEL;
|
|
1972
|
+
if (next === null ||
|
|
1973
|
+
!ClipperBase.isHotEdge(ae) || !ClipperBase.isHotEdge(next) ||
|
|
1974
|
+
ClipperBase.isHorizontal(ae) || ClipperBase.isHorizontal(next) ||
|
|
1975
|
+
ClipperBase.isOpen(ae) || ClipperBase.isOpen(next)) return;
|
|
1976
|
+
if ((pt.y < ae.top.y + 2 || pt.y < next.top.y + 2) && // avoid trivial joins
|
|
1977
|
+
((ae.bot.y > pt.y) || (next.bot.y > pt.y))) return; // (#490)
|
|
1978
|
+
|
|
1979
|
+
if (checkCurrX) {
|
|
1980
|
+
if (this.perpendicDistFromLineSqrd(pt, next.bot, next.top) > 0.25) return;
|
|
1981
|
+
} else if (ae.curX !== next.curX) return;
|
|
1982
|
+
if (!InternalClipper.isCollinear(ae.top, pt, next.top)) return;
|
|
1983
|
+
|
|
1984
|
+
if (ae.outrec!.idx === next.outrec!.idx) {
|
|
1985
|
+
this.addLocalMaxPoly(ae, next, pt);
|
|
1986
|
+
} else if (ae.outrec!.idx < next.outrec!.idx) {
|
|
1987
|
+
this.joinOutrecPaths(ae, next);
|
|
1988
|
+
} else {
|
|
1989
|
+
this.joinOutrecPaths(next, ae);
|
|
1990
|
+
}
|
|
1991
|
+
ae.joinWith = JoinWith.Right;
|
|
1992
|
+
next.joinWith = JoinWith.Left;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
private perpendicDistFromLineSqrd(pt: Point64, line1: Point64, line2: Point64): number {
|
|
1996
|
+
const a = pt.x - line1.x;
|
|
1997
|
+
const b = pt.y - line1.y;
|
|
1998
|
+
const c = line2.x - line1.x;
|
|
1999
|
+
const d = line2.y - line1.y;
|
|
2000
|
+
if (c === 0 && d === 0) return 0;
|
|
2001
|
+
return ((a * d - c * b) * (a * d - c * b)) / (c * c + d * d);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
private intersectEdges(ae1: Active, ae2: Active, pt: Point64): void {
|
|
2006
|
+
let resultOp: OutPt | null = null;
|
|
2007
|
+
// MANAGE OPEN PATH INTERSECTIONS SEPARATELY ...
|
|
2008
|
+
if (this.hasOpenPaths && (ClipperBase.isOpen(ae1) || ClipperBase.isOpen(ae2))) {
|
|
2009
|
+
if (ClipperBase.isOpen(ae1) && ClipperBase.isOpen(ae2)) return;
|
|
2010
|
+
// the following line avoids duplicating quite a bit of code
|
|
2011
|
+
if (ClipperBase.isOpen(ae2)) [ae1, ae2] = ClipperBase.swapActives(ae1, ae2);
|
|
2012
|
+
if (this.isJoined(ae2)) this.split(ae2, pt); // needed for safety
|
|
2013
|
+
|
|
2014
|
+
if (this.cliptype === ClipType.Union) {
|
|
2015
|
+
if (!ClipperBase.isHotEdge(ae2)) return;
|
|
2016
|
+
} else if (ae2.localMin!.polytype === PathType.Subject) return;
|
|
2017
|
+
|
|
2018
|
+
switch (this.fillrule) {
|
|
2019
|
+
case FillRule.Positive:
|
|
2020
|
+
if (ae2.windCount !== 1) return;
|
|
2021
|
+
break;
|
|
2022
|
+
case FillRule.Negative:
|
|
2023
|
+
if (ae2.windCount !== -1) return;
|
|
2024
|
+
break;
|
|
2025
|
+
default:
|
|
2026
|
+
if (Math.abs(ae2.windCount) !== 1) return;
|
|
2027
|
+
break;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// toggle contribution ...
|
|
2031
|
+
if (ClipperBase.isHotEdge(ae1)) {
|
|
2032
|
+
resultOp = this.addOutPt(ae1, pt);
|
|
2033
|
+
if (ClipperBase.isFront(ae1)) {
|
|
2034
|
+
ae1.outrec!.frontEdge = null;
|
|
2035
|
+
} else {
|
|
2036
|
+
ae1.outrec!.backEdge = null;
|
|
2037
|
+
}
|
|
2038
|
+
ae1.outrec = null;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// horizontal edges can pass under open paths at a LocMins
|
|
2042
|
+
else if (pt.x === ae1.localMin!.vertex.pt.x && pt.y === ae1.localMin!.vertex.pt.y &&
|
|
2043
|
+
!ClipperBase.isOpenEndVertex(ae1.localMin!.vertex)) {
|
|
2044
|
+
// find the other side of the LocMin and
|
|
2045
|
+
// if it's 'hot' join up with it ...
|
|
2046
|
+
const ae3 = this.findEdgeWithMatchingLocMin(ae1);
|
|
2047
|
+
if (ae3 !== null && ClipperBase.isHotEdge(ae3)) {
|
|
2048
|
+
ae1.outrec = ae3.outrec;
|
|
2049
|
+
if (ae1.windDx > 0) {
|
|
2050
|
+
this.setSides(ae3.outrec!, ae1, ae3);
|
|
2051
|
+
} else {
|
|
2052
|
+
this.setSides(ae3.outrec!, ae3, ae1);
|
|
2053
|
+
}
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
resultOp = this.startOpenPath(ae1, pt);
|
|
2058
|
+
} else {
|
|
2059
|
+
resultOp = this.startOpenPath(ae1, pt);
|
|
2060
|
+
}
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// MANAGING CLOSED PATHS FROM HERE ON
|
|
2065
|
+
if (this.isJoined(ae1)) this.split(ae1, pt);
|
|
2066
|
+
if (this.isJoined(ae2)) this.split(ae2, pt);
|
|
2067
|
+
|
|
2068
|
+
// UPDATE WINDING COUNTS...
|
|
2069
|
+
let oldE1WindCount: number, oldE2WindCount: number;
|
|
2070
|
+
if (ae1.localMin!.polytype === ae2.localMin!.polytype) {
|
|
2071
|
+
if (this.fillrule === FillRule.EvenOdd) {
|
|
2072
|
+
oldE1WindCount = ae1.windCount;
|
|
2073
|
+
ae1.windCount = ae2.windCount;
|
|
2074
|
+
ae2.windCount = oldE1WindCount;
|
|
2075
|
+
} else {
|
|
2076
|
+
if (ae1.windCount + ae2.windDx === 0) {
|
|
2077
|
+
ae1.windCount = -ae1.windCount;
|
|
2078
|
+
} else {
|
|
2079
|
+
ae1.windCount += ae2.windDx;
|
|
2080
|
+
}
|
|
2081
|
+
if (ae2.windCount - ae1.windDx === 0) {
|
|
2082
|
+
ae2.windCount = -ae2.windCount;
|
|
2083
|
+
} else {
|
|
2084
|
+
ae2.windCount -= ae1.windDx;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
} else {
|
|
2088
|
+
if (this.fillrule !== FillRule.EvenOdd) {
|
|
2089
|
+
ae1.windCount2 += ae2.windDx;
|
|
2090
|
+
} else {
|
|
2091
|
+
ae1.windCount2 = (ae1.windCount2 === 0 ? 1 : 0);
|
|
2092
|
+
}
|
|
2093
|
+
if (this.fillrule !== FillRule.EvenOdd) {
|
|
2094
|
+
ae2.windCount2 -= ae1.windDx;
|
|
2095
|
+
} else {
|
|
2096
|
+
ae2.windCount2 = (ae2.windCount2 === 0 ? 1 : 0);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
switch (this.fillrule) {
|
|
2101
|
+
case FillRule.Positive:
|
|
2102
|
+
oldE1WindCount = ae1.windCount;
|
|
2103
|
+
oldE2WindCount = ae2.windCount;
|
|
2104
|
+
break;
|
|
2105
|
+
case FillRule.Negative:
|
|
2106
|
+
oldE1WindCount = -ae1.windCount;
|
|
2107
|
+
oldE2WindCount = -ae2.windCount;
|
|
2108
|
+
break;
|
|
2109
|
+
default:
|
|
2110
|
+
oldE1WindCount = Math.abs(ae1.windCount);
|
|
2111
|
+
oldE2WindCount = Math.abs(ae2.windCount);
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const e1WindCountIs0or1 = oldE1WindCount === 0 || oldE1WindCount === 1;
|
|
2116
|
+
const e2WindCountIs0or1 = oldE2WindCount === 0 || oldE2WindCount === 1;
|
|
2117
|
+
|
|
2118
|
+
if ((!ClipperBase.isHotEdge(ae1) && !e1WindCountIs0or1) ||
|
|
2119
|
+
(!ClipperBase.isHotEdge(ae2) && !e2WindCountIs0or1)) return;
|
|
2120
|
+
|
|
2121
|
+
// NOW PROCESS THE INTERSECTION ...
|
|
2122
|
+
|
|
2123
|
+
// if both edges are 'hot' ...
|
|
2124
|
+
if (ClipperBase.isHotEdge(ae1) && ClipperBase.isHotEdge(ae2)) {
|
|
2125
|
+
if ((oldE1WindCount !== 0 && oldE1WindCount !== 1) || (oldE2WindCount !== 0 && oldE2WindCount !== 1) ||
|
|
2126
|
+
(ae1.localMin!.polytype !== ae2.localMin!.polytype && this.cliptype !== ClipType.Xor)) {
|
|
2127
|
+
resultOp = this.addLocalMaxPoly(ae1, ae2, pt);
|
|
2128
|
+
} else if (ClipperBase.isFront(ae1) || (ae1.outrec === ae2.outrec)) {
|
|
2129
|
+
// this 'else if' condition isn't strictly needed but
|
|
2130
|
+
// it's sensible to split polygons that only touch at
|
|
2131
|
+
// a common vertex (not at common edges).
|
|
2132
|
+
resultOp = this.addLocalMaxPoly(ae1, ae2, pt);
|
|
2133
|
+
// C# non-USINGZ version calls AddLocalMinPoly here (without the max poly call)
|
|
2134
|
+
this.addLocalMinPoly(ae1, ae2, pt);
|
|
2135
|
+
} else {
|
|
2136
|
+
// can't treat as maxima & minima
|
|
2137
|
+
resultOp = this.addOutPt(ae1, pt);
|
|
2138
|
+
this.addOutPt(ae2, pt);
|
|
2139
|
+
this.swapOutrecs(ae1, ae2);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// if one or other edge is 'hot' ...
|
|
2144
|
+
else if (ClipperBase.isHotEdge(ae1)) {
|
|
2145
|
+
resultOp = this.addOutPt(ae1, pt);
|
|
2146
|
+
this.swapOutrecs(ae1, ae2);
|
|
2147
|
+
} else if (ClipperBase.isHotEdge(ae2)) {
|
|
2148
|
+
resultOp = this.addOutPt(ae2, pt);
|
|
2149
|
+
this.swapOutrecs(ae1, ae2);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// neither edge is 'hot'
|
|
2153
|
+
else {
|
|
2154
|
+
let e1Wc2: number, e2Wc2: number;
|
|
2155
|
+
switch (this.fillrule) {
|
|
2156
|
+
case FillRule.Positive:
|
|
2157
|
+
e1Wc2 = ae1.windCount2;
|
|
2158
|
+
e2Wc2 = ae2.windCount2;
|
|
2159
|
+
break;
|
|
2160
|
+
case FillRule.Negative:
|
|
2161
|
+
e1Wc2 = -ae1.windCount2;
|
|
2162
|
+
e2Wc2 = -ae2.windCount2;
|
|
2163
|
+
break;
|
|
2164
|
+
default:
|
|
2165
|
+
e1Wc2 = Math.abs(ae1.windCount2);
|
|
2166
|
+
e2Wc2 = Math.abs(ae2.windCount2);
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (!ClipperBase.isSamePolyType(ae1, ae2)) {
|
|
2171
|
+
resultOp = this.addLocalMinPoly(ae1, ae2, pt);
|
|
2172
|
+
} else if (oldE1WindCount === 1 && oldE2WindCount === 1) {
|
|
2173
|
+
resultOp = null;
|
|
2174
|
+
switch (this.cliptype) {
|
|
2175
|
+
case ClipType.Union:
|
|
2176
|
+
if (e1Wc2 > 0 && e2Wc2 > 0) return;
|
|
2177
|
+
resultOp = this.addLocalMinPoly(ae1, ae2, pt);
|
|
2178
|
+
break;
|
|
2179
|
+
|
|
2180
|
+
case ClipType.Difference:
|
|
2181
|
+
if (((ClipperBase.getPolyType(ae1) === PathType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) ||
|
|
2182
|
+
((ClipperBase.getPolyType(ae1) === PathType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) {
|
|
2183
|
+
resultOp = this.addLocalMinPoly(ae1, ae2, pt);
|
|
2184
|
+
}
|
|
2185
|
+
break;
|
|
2186
|
+
|
|
2187
|
+
case ClipType.Xor:
|
|
2188
|
+
resultOp = this.addLocalMinPoly(ae1, ae2, pt);
|
|
2189
|
+
break;
|
|
2190
|
+
|
|
2191
|
+
default: // ClipType.Intersection:
|
|
2192
|
+
if (e1Wc2 <= 0 || e2Wc2 <= 0) return;
|
|
2193
|
+
resultOp = this.addLocalMinPoly(ae1, ae2, pt);
|
|
2194
|
+
break;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
private swapPositionsInAEL(ae1: Active, ae2: Active): void {
|
|
2201
|
+
// preconditon: ae1 must be immediately to the left of ae2
|
|
2202
|
+
const next = ae2.nextInAEL;
|
|
2203
|
+
if (next !== null) next.prevInAEL = ae1;
|
|
2204
|
+
const prev = ae1.prevInAEL;
|
|
2205
|
+
if (prev !== null) prev.nextInAEL = ae2;
|
|
2206
|
+
ae2.prevInAEL = prev;
|
|
2207
|
+
ae2.nextInAEL = ae1;
|
|
2208
|
+
ae1.prevInAEL = ae2;
|
|
2209
|
+
ae1.nextInAEL = next;
|
|
2210
|
+
if (ae2.prevInAEL === null) this.actives = ae2;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
private isValidAelOrder(resident: Active, newcomer: Active): boolean {
|
|
2214
|
+
if (newcomer.curX !== resident.curX) {
|
|
2215
|
+
return newcomer.curX > resident.curX;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// get the turning direction a1.top, a2.bot, a2.top
|
|
2219
|
+
const d = InternalClipper.crossProduct(resident.top, newcomer.bot, newcomer.top);
|
|
2220
|
+
if (d !== 0) return d < 0;
|
|
2221
|
+
|
|
2222
|
+
// edges must be collinear to get here
|
|
2223
|
+
|
|
2224
|
+
// for starting open paths, place them according to
|
|
2225
|
+
// the direction they're about to turn
|
|
2226
|
+
if (!ClipperBase.isMaxima(resident) && (resident.top.y > newcomer.top.y)) {
|
|
2227
|
+
return InternalClipper.crossProduct(newcomer.bot,
|
|
2228
|
+
resident.top, ClipperBase.nextVertex(resident).pt) <= 0;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
if (!ClipperBase.isMaxima(newcomer) && (newcomer.top.y > resident.top.y)) {
|
|
2232
|
+
return InternalClipper.crossProduct(newcomer.bot,
|
|
2233
|
+
newcomer.top, ClipperBase.nextVertex(newcomer).pt) >= 0;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
const y = newcomer.bot.y;
|
|
2237
|
+
const newcomerIsLeft = newcomer.isLeftBound;
|
|
2238
|
+
|
|
2239
|
+
if (resident.bot.y !== y || resident.localMin!.vertex.pt.y !== y) {
|
|
2240
|
+
return newcomer.isLeftBound;
|
|
2241
|
+
}
|
|
2242
|
+
// resident must also have just been inserted
|
|
2243
|
+
if (resident.isLeftBound !== newcomerIsLeft) {
|
|
2244
|
+
return newcomerIsLeft;
|
|
2245
|
+
}
|
|
2246
|
+
if (InternalClipper.isCollinear(ClipperBase.prevPrevVertex(resident).pt,
|
|
2247
|
+
resident.bot, resident.top)) return true;
|
|
2248
|
+
// compare turning direction of the alternate bound
|
|
2249
|
+
return (InternalClipper.crossProduct(ClipperBase.prevPrevVertex(resident).pt,
|
|
2250
|
+
newcomer.bot, ClipperBase.prevPrevVertex(newcomer).pt) > 0) === newcomerIsLeft;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
private isJoined(e: Active): boolean {
|
|
2254
|
+
return e.joinWith !== JoinWith.None;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
private split(e: Active, currPt: Point64): void {
|
|
2258
|
+
if (e.joinWith === JoinWith.Right) {
|
|
2259
|
+
e.joinWith = JoinWith.None;
|
|
2260
|
+
e.nextInAEL!.joinWith = JoinWith.None;
|
|
2261
|
+
this.addLocalMinPoly(e, e.nextInAEL!, currPt, true);
|
|
2262
|
+
} else {
|
|
2263
|
+
e.joinWith = JoinWith.None;
|
|
2264
|
+
e.prevInAEL!.joinWith = JoinWith.None;
|
|
2265
|
+
this.addLocalMinPoly(e.prevInAEL!, e, currPt, true);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
private setSides(outrec: OutRec, startEdge: Active, endEdge: Active): void {
|
|
2270
|
+
outrec.frontEdge = startEdge;
|
|
2271
|
+
outrec.backEdge = endEdge;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
private findEdgeWithMatchingLocMin(e: Active): Active | null {
|
|
2275
|
+
let result = e.nextInAEL;
|
|
2276
|
+
while (result !== null) {
|
|
2277
|
+
if (result.localMin?.equals(e.localMin)) return result;
|
|
2278
|
+
if (!ClipperBase.isHorizontal(result) && !(e.bot.x === result.bot.x && e.bot.y === result.bot.y)) result = null;
|
|
2279
|
+
else result = result.nextInAEL;
|
|
2280
|
+
}
|
|
2281
|
+
result = e.prevInAEL;
|
|
2282
|
+
while (result !== null) {
|
|
2283
|
+
if (result.localMin?.equals(e.localMin)) return result;
|
|
2284
|
+
if (!ClipperBase.isHorizontal(result) && !(e.bot.x === result.bot.x && e.bot.y === result.bot.y)) return null;
|
|
2285
|
+
result = result.prevInAEL;
|
|
2286
|
+
}
|
|
2287
|
+
return result;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
private addOutPt(ae: Active, pt: Point64): OutPt {
|
|
2291
|
+
// Outrec.OutPts: a circular doubly-linked-list of POutPt where ...
|
|
2292
|
+
// opFront[.Prev]* ~~~> opBack & opBack == opFront.Next
|
|
2293
|
+
const outrec = ae.outrec!;
|
|
2294
|
+
const toFront = ClipperBase.isFront(ae);
|
|
2295
|
+
const opFront = outrec.pts!;
|
|
2296
|
+
const opBack = opFront.next!;
|
|
2297
|
+
|
|
2298
|
+
if (toFront && pt.x === opFront.pt.x && pt.y === opFront.pt.y) {
|
|
2299
|
+
return opFront;
|
|
2300
|
+
} else if (!toFront && pt.x === opBack.pt.x && pt.y === opBack.pt.y) {
|
|
2301
|
+
return opBack;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
const newOp = new OutPt(pt, outrec);
|
|
2305
|
+
opBack.prev = newOp;
|
|
2306
|
+
newOp.prev = opFront;
|
|
2307
|
+
newOp.next = opBack;
|
|
2308
|
+
opFront.next = newOp;
|
|
2309
|
+
if (toFront) outrec.pts = newOp;
|
|
2310
|
+
return newOp;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
private addLocalMaxPoly(ae1: Active, ae2: Active, pt: Point64): OutPt | null {
|
|
2314
|
+
if (this.isJoined(ae1)) this.split(ae1, pt);
|
|
2315
|
+
if (this.isJoined(ae2)) this.split(ae2, pt);
|
|
2316
|
+
|
|
2317
|
+
if (ClipperBase.isFront(ae1) === ClipperBase.isFront(ae2)) {
|
|
2318
|
+
if (ClipperBase.isOpenEnd(ae1)) {
|
|
2319
|
+
this.swapFrontBackSides(ae1.outrec!);
|
|
2320
|
+
} else if (ClipperBase.isOpenEnd(ae2)) {
|
|
2321
|
+
this.swapFrontBackSides(ae2.outrec!);
|
|
2322
|
+
} else {
|
|
2323
|
+
this.succeeded = false;
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
const result = this.addOutPt(ae1, pt);
|
|
2329
|
+
if (ae1.outrec === ae2.outrec) {
|
|
2330
|
+
const outrec = ae1.outrec!;
|
|
2331
|
+
outrec.pts = result;
|
|
2332
|
+
|
|
2333
|
+
if (this.usingPolytree) {
|
|
2334
|
+
const e = ClipperBase.getPrevHotEdge(ae1);
|
|
2335
|
+
if (e === null) {
|
|
2336
|
+
outrec.owner = null;
|
|
2337
|
+
} else {
|
|
2338
|
+
this.setOwner(outrec, e.outrec!);
|
|
2339
|
+
}
|
|
2340
|
+
// nb: outRec.owner here is likely NOT the real
|
|
2341
|
+
// owner but this will be fixed in DeepCheckOwner()
|
|
2342
|
+
}
|
|
2343
|
+
this.uncoupleOutRec(ae1);
|
|
2344
|
+
}
|
|
2345
|
+
// and to preserve the winding orientation of outrec ...
|
|
2346
|
+
else if (ClipperBase.isOpen(ae1)) {
|
|
2347
|
+
if (ae1.windDx < 0) {
|
|
2348
|
+
this.joinOutrecPaths(ae1, ae2);
|
|
2349
|
+
} else {
|
|
2350
|
+
this.joinOutrecPaths(ae2, ae1);
|
|
2351
|
+
}
|
|
2352
|
+
} else if (ae1.outrec!.idx < ae2.outrec!.idx) {
|
|
2353
|
+
this.joinOutrecPaths(ae1, ae2);
|
|
2354
|
+
} else {
|
|
2355
|
+
this.joinOutrecPaths(ae2, ae1);
|
|
2356
|
+
}
|
|
2357
|
+
return result;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
private swapFrontBackSides(outrec: OutRec): void {
|
|
2361
|
+
// while this proc. is needed for open paths
|
|
2362
|
+
// it's almost never needed for closed paths
|
|
2363
|
+
const ae2 = outrec.frontEdge!;
|
|
2364
|
+
outrec.frontEdge = outrec.backEdge;
|
|
2365
|
+
outrec.backEdge = ae2;
|
|
2366
|
+
outrec.pts = outrec.pts!.next;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
private setOwner(outrec: OutRec, newOwner: OutRec): void {
|
|
2370
|
+
//precondition1: new_owner is never null
|
|
2371
|
+
while (newOwner.owner !== null && newOwner.owner.pts === null) {
|
|
2372
|
+
newOwner.owner = newOwner.owner.owner;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
//make sure that outrec isn't an owner of newOwner
|
|
2376
|
+
let tmp: OutRec | null = newOwner;
|
|
2377
|
+
while (tmp !== null && tmp !== outrec) {
|
|
2378
|
+
tmp = tmp.owner;
|
|
2379
|
+
}
|
|
2380
|
+
if (tmp !== null) {
|
|
2381
|
+
newOwner.owner = outrec.owner;
|
|
2382
|
+
}
|
|
2383
|
+
outrec.owner = newOwner;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
private uncoupleOutRec(ae: Active): void {
|
|
2387
|
+
const outrec = ae.outrec;
|
|
2388
|
+
if (outrec === null) return;
|
|
2389
|
+
outrec.frontEdge!.outrec = null;
|
|
2390
|
+
outrec.backEdge!.outrec = null;
|
|
2391
|
+
outrec.frontEdge = null;
|
|
2392
|
+
outrec.backEdge = null;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
private joinOutrecPaths(ae1: Active, ae2: Active): void {
|
|
2396
|
+
// join ae2 outrec path onto ae1 outrec path and then delete ae2 outrec path
|
|
2397
|
+
// pointers. (NB Only very rarely do the joining ends share the same coords.)
|
|
2398
|
+
const p1Start = ae1.outrec!.pts!;
|
|
2399
|
+
const p2Start = ae2.outrec!.pts!;
|
|
2400
|
+
const p1End = p1Start.next!;
|
|
2401
|
+
const p2End = p2Start.next!;
|
|
2402
|
+
if (ClipperBase.isFront(ae1)) {
|
|
2403
|
+
p2End.prev = p1Start;
|
|
2404
|
+
p1Start.next = p2End;
|
|
2405
|
+
p2Start.next = p1End;
|
|
2406
|
+
p1End.prev = p2Start;
|
|
2407
|
+
ae1.outrec!.pts = p2Start;
|
|
2408
|
+
// nb: if IsOpen(e1) then e1 & e2 must be a 'maximaPair'
|
|
2409
|
+
ae1.outrec!.frontEdge = ae2.outrec!.frontEdge;
|
|
2410
|
+
if (ae1.outrec!.frontEdge !== null) {
|
|
2411
|
+
ae1.outrec!.frontEdge!.outrec = ae1.outrec;
|
|
2412
|
+
}
|
|
2413
|
+
} else {
|
|
2414
|
+
p1End.prev = p2Start;
|
|
2415
|
+
p2Start.next = p1End;
|
|
2416
|
+
p1Start.next = p2End;
|
|
2417
|
+
p2End.prev = p1Start;
|
|
2418
|
+
|
|
2419
|
+
ae1.outrec!.backEdge = ae2.outrec!.backEdge;
|
|
2420
|
+
if (ae1.outrec!.backEdge !== null) {
|
|
2421
|
+
ae1.outrec!.backEdge!.outrec = ae1.outrec;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// after joining, the ae2.OutRec must contains no vertices ...
|
|
2426
|
+
ae2.outrec!.frontEdge = null;
|
|
2427
|
+
ae2.outrec!.backEdge = null;
|
|
2428
|
+
ae2.outrec!.pts = null;
|
|
2429
|
+
this.setOwner(ae2.outrec!, ae1.outrec!);
|
|
2430
|
+
|
|
2431
|
+
if (ClipperBase.isOpenEnd(ae1)) {
|
|
2432
|
+
ae2.outrec!.pts = ae1.outrec!.pts;
|
|
2433
|
+
ae1.outrec!.pts = null;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// and ae1 and ae2 are maxima and are about to be dropped from the Actives list.
|
|
2437
|
+
ae1.outrec = null;
|
|
2438
|
+
ae2.outrec = null;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
private swapOutrecs(ae1: Active, ae2: Active): void {
|
|
2442
|
+
const or1 = ae1.outrec; // at least one edge has
|
|
2443
|
+
const or2 = ae2.outrec; // an assigned outrec
|
|
2444
|
+
if (or1 === or2) {
|
|
2445
|
+
const ae = or1!.frontEdge;
|
|
2446
|
+
or1!.frontEdge = or1!.backEdge;
|
|
2447
|
+
or1!.backEdge = ae;
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
if (or1 !== null) {
|
|
2452
|
+
if (ae1 === or1.frontEdge) {
|
|
2453
|
+
or1.frontEdge = ae2;
|
|
2454
|
+
} else {
|
|
2455
|
+
or1.backEdge = ae2;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
if (or2 !== null) {
|
|
2460
|
+
if (ae2 === or2.frontEdge) {
|
|
2461
|
+
or2.frontEdge = ae1;
|
|
2462
|
+
} else {
|
|
2463
|
+
or2.backEdge = ae1;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
ae1.outrec = or2;
|
|
2468
|
+
ae2.outrec = or1;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
private disposeIntersectNodes(): void {
|
|
2472
|
+
this.intersectList.length = 0;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
private static ptsReallyClose(pt1: Point64, pt2: Point64): boolean {
|
|
2476
|
+
return (Math.abs(pt1.x - pt2.x) < 2) && (Math.abs(pt1.y - pt2.y) < 2);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
private static isVerySmallTriangle(op: OutPt): boolean {
|
|
2480
|
+
return op.next!.next === op.prev &&
|
|
2481
|
+
(ClipperBase.ptsReallyClose(op.prev.pt, op.next!.pt) ||
|
|
2482
|
+
ClipperBase.ptsReallyClose(op.pt, op.next!.pt) ||
|
|
2483
|
+
ClipperBase.ptsReallyClose(op.pt, op.prev.pt));
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
protected static buildPath(op: OutPt | null, reverse: boolean, isOpen: boolean, path: Path64): boolean {
|
|
2487
|
+
if (op === null || op.next === op || (!isOpen && op.next === op.prev)) return false;
|
|
2488
|
+
path.length = 0;
|
|
2489
|
+
|
|
2490
|
+
let lastPt: Point64;
|
|
2491
|
+
let op2: OutPt;
|
|
2492
|
+
|
|
2493
|
+
if (reverse) {
|
|
2494
|
+
lastPt = op.pt;
|
|
2495
|
+
op2 = op.prev;
|
|
2496
|
+
} else {
|
|
2497
|
+
op = op.next!;
|
|
2498
|
+
lastPt = op.pt;
|
|
2499
|
+
op2 = op.next!;
|
|
2500
|
+
}
|
|
2501
|
+
path.push(lastPt);
|
|
2502
|
+
|
|
2503
|
+
while (op2 !== op) {
|
|
2504
|
+
if (!(op2.pt.x === lastPt.x && op2.pt.y === lastPt.y)) {
|
|
2505
|
+
lastPt = op2.pt;
|
|
2506
|
+
path.push(lastPt);
|
|
2507
|
+
}
|
|
2508
|
+
if (reverse) {
|
|
2509
|
+
op2 = op2.prev;
|
|
2510
|
+
} else {
|
|
2511
|
+
op2 = op2.next!;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
return path.length !== 3 || isOpen || !ClipperBase.isVerySmallTriangle(op2);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
protected buildPaths(solutionClosed: Paths64, solutionOpen: Paths64): boolean {
|
|
2519
|
+
solutionClosed.length = 0;
|
|
2520
|
+
solutionOpen.length = 0;
|
|
2521
|
+
|
|
2522
|
+
let i = 0;
|
|
2523
|
+
// outrecList.length is not static here because
|
|
2524
|
+
// CleanCollinear can indirectly add additional OutRec
|
|
2525
|
+
while (i < this.outrecList.length) {
|
|
2526
|
+
const outrec = this.outrecList[i++];
|
|
2527
|
+
if (outrec.pts === null) continue;
|
|
2528
|
+
|
|
2529
|
+
const path: Path64 = [];
|
|
2530
|
+
if (outrec.isOpen) {
|
|
2531
|
+
if (ClipperBase.buildPath(outrec.pts, this.reverseSolution, true, path)) {
|
|
2532
|
+
solutionOpen.push(path);
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
this.cleanCollinear(outrec);
|
|
2536
|
+
// closed paths should always return a Positive orientation
|
|
2537
|
+
// except when ReverseSolution == true
|
|
2538
|
+
if (ClipperBase.buildPath(outrec.pts, this.reverseSolution, false, path)) {
|
|
2539
|
+
solutionClosed.push(path);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return true;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
protected buildTree(polytree: PolyPathBase, solutionOpen: Paths64): void {
|
|
2547
|
+
polytree.clear();
|
|
2548
|
+
solutionOpen.length = 0;
|
|
2549
|
+
|
|
2550
|
+
let i = 0;
|
|
2551
|
+
// outrecList.length is not static here because
|
|
2552
|
+
// checkBounds below can indirectly add additional
|
|
2553
|
+
// OutRec (via FixOutRecPts & CleanCollinear)
|
|
2554
|
+
while (i < this.outrecList.length) {
|
|
2555
|
+
const outrec = this.outrecList[i++];
|
|
2556
|
+
if (outrec.pts === null) continue;
|
|
2557
|
+
|
|
2558
|
+
if (outrec.isOpen) {
|
|
2559
|
+
const openPath: Path64 = [];
|
|
2560
|
+
if (ClipperBase.buildPath(outrec.pts, this.reverseSolution, true, openPath)) {
|
|
2561
|
+
solutionOpen.push(openPath);
|
|
2562
|
+
}
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (this.checkBounds(outrec)) {
|
|
2567
|
+
this.recursiveCheckOwners(outrec, polytree);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
protected checkBounds(outrec: OutRec): boolean {
|
|
2573
|
+
if (outrec.pts === null) return false;
|
|
2574
|
+
if (!Rect64Utils.isEmpty(outrec.bounds)) return true;
|
|
2575
|
+
this.cleanCollinear(outrec);
|
|
2576
|
+
if (outrec.pts === null ||
|
|
2577
|
+
!ClipperBase.buildPath(outrec.pts, this.reverseSolution, false, outrec.path)) {
|
|
2578
|
+
return false;
|
|
2579
|
+
}
|
|
2580
|
+
outrec.bounds = InternalClipper.getBounds(outrec.path);
|
|
2581
|
+
return true;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
protected recursiveCheckOwners(outrec: OutRec, polypath: PolyPathBase): void {
|
|
2585
|
+
// pre-condition: outrec will have valid bounds
|
|
2586
|
+
// post-condition: if a valid path, outrec will have a polypath
|
|
2587
|
+
|
|
2588
|
+
if (outrec.polypath !== null || Rect64Utils.isEmpty(outrec.bounds)) return;
|
|
2589
|
+
|
|
2590
|
+
while (outrec.owner !== null) {
|
|
2591
|
+
if (outrec.owner.splits !== null &&
|
|
2592
|
+
this.checkSplitOwner(outrec, outrec.owner.splits)) break;
|
|
2593
|
+
if (outrec.owner.pts !== null && this.checkBounds(outrec.owner) &&
|
|
2594
|
+
this.path1InsidePath2(outrec.pts!, outrec.owner.pts!)) break;
|
|
2595
|
+
outrec.owner = outrec.owner.owner;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
if (outrec.owner !== null) {
|
|
2599
|
+
if (outrec.owner.polypath === null) {
|
|
2600
|
+
this.recursiveCheckOwners(outrec.owner, polypath);
|
|
2601
|
+
}
|
|
2602
|
+
outrec.polypath = outrec.owner.polypath!.addChild(outrec.path);
|
|
2603
|
+
} else {
|
|
2604
|
+
outrec.polypath = polypath.addChild(outrec.path);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
protected cleanCollinear(outrec: OutRec | null): void {
|
|
2609
|
+
outrec = this.getRealOutRec(outrec);
|
|
2610
|
+
|
|
2611
|
+
if (outrec === null || outrec.isOpen) return;
|
|
2612
|
+
|
|
2613
|
+
if (!this.isValidClosedPath(outrec.pts)) {
|
|
2614
|
+
outrec.pts = null;
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
let startOp = outrec.pts!;
|
|
2619
|
+
let op2: OutPt | null = startOp;
|
|
2620
|
+
|
|
2621
|
+
while (true) {
|
|
2622
|
+
// NB if preserveCollinear == true, then only remove 180 deg. spikes
|
|
2623
|
+
if (op2 !== null && InternalClipper.isCollinear(op2.prev.pt, op2.pt, op2.next!.pt) &&
|
|
2624
|
+
((op2.pt.x === op2.prev.pt.x && op2.pt.y === op2.prev.pt.y) ||
|
|
2625
|
+
(op2.pt.x === op2.next!.pt.x && op2.pt.y === op2.next!.pt.y) ||
|
|
2626
|
+
!this.preserveCollinear ||
|
|
2627
|
+
InternalClipper.dotProduct(op2.prev.pt, op2.pt, op2.next!.pt) < 0)) {
|
|
2628
|
+
|
|
2629
|
+
if (op2 === outrec.pts) {
|
|
2630
|
+
outrec.pts = op2.prev;
|
|
2631
|
+
}
|
|
2632
|
+
op2 = this.disposeOutPt(op2!);
|
|
2633
|
+
if (!this.isValidClosedPath(op2)) {
|
|
2634
|
+
outrec.pts = null;
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
startOp = op2!;
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
if (op2 === null) break;
|
|
2641
|
+
op2 = op2.next!;
|
|
2642
|
+
if (op2 === startOp) break;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
this.fixSelfIntersects(outrec);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
private isValidClosedPath(op: OutPt | null): boolean {
|
|
2649
|
+
return op !== null && op.next !== op &&
|
|
2650
|
+
(op.next !== op.prev || !ClipperBase.isVerySmallTriangle(op));
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
private disposeOutPt(op: OutPt): OutPt | null {
|
|
2654
|
+
const result = (op.next === op ? null : op.next);
|
|
2655
|
+
op.prev.next = op.next;
|
|
2656
|
+
op.next!.prev = op.prev;
|
|
2657
|
+
return result;
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
private fixSelfIntersects(outrec: OutRec): void {
|
|
2661
|
+
let op2 = outrec.pts!;
|
|
2662
|
+
if (op2.prev === op2.next!.next) {
|
|
2663
|
+
return; // because triangles can't self-intersect
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
while (true) {
|
|
2667
|
+
if (op2.next && op2.next.next &&
|
|
2668
|
+
// optimization (not in C# reference): bbox check before segsIntersect
|
|
2669
|
+
this.boundingBoxesOverlap(op2.prev.pt, op2.pt, op2.next.pt, op2.next.next.pt) && // TEST: Bbox only
|
|
2670
|
+
InternalClipper.segsIntersect(op2.prev.pt, op2.pt, op2.next.pt, op2.next.next.pt)) {
|
|
2671
|
+
if (op2.next.next.next &&
|
|
2672
|
+
// optimization (not in C# reference): bbox check before segsIntersect
|
|
2673
|
+
this.boundingBoxesOverlap(op2.prev.pt, op2.pt, op2.next.next.pt, op2.next.next.next.pt) && // TEST: Bbox only
|
|
2674
|
+
InternalClipper.segsIntersect(op2.prev.pt, op2.pt, op2.next.next.pt, op2.next.next.next.pt)) {
|
|
2675
|
+
// adjacent intersections (ie a micro self-intersection)
|
|
2676
|
+
op2 = this.duplicateOp(op2, false);
|
|
2677
|
+
op2.pt = op2.next!.next!.next!.pt;
|
|
2678
|
+
op2 = op2.next!;
|
|
2679
|
+
} else {
|
|
2680
|
+
if (op2 === outrec.pts || op2.next === outrec.pts) {
|
|
2681
|
+
outrec.pts = outrec.pts!.prev;
|
|
2682
|
+
}
|
|
2683
|
+
this.doSplitOp(outrec, op2);
|
|
2684
|
+
if (outrec.pts === null) return;
|
|
2685
|
+
op2 = outrec.pts;
|
|
2686
|
+
// triangles can't self-intersect
|
|
2687
|
+
if (op2.prev === op2.next!.next) break;
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
op2 = op2.next!;
|
|
2693
|
+
if (op2 === outrec.pts) break;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
private doSplitOp(outrec: OutRec, splitOp: OutPt): void {
|
|
2698
|
+
// splitOp.prev <=> splitOp &&
|
|
2699
|
+
// splitOp.next <=> splitOp.next.next are intersecting
|
|
2700
|
+
const prevOp = splitOp.prev;
|
|
2701
|
+
const nextNextOp = splitOp.next!.next!;
|
|
2702
|
+
outrec.pts = prevOp;
|
|
2703
|
+
|
|
2704
|
+
const intersectResult = InternalClipper.getLineIntersectPt(
|
|
2705
|
+
prevOp.pt, splitOp.pt, splitOp.next!.pt, nextNextOp.pt);
|
|
2706
|
+
|
|
2707
|
+
const ip = intersectResult.point;
|
|
2708
|
+
|
|
2709
|
+
const area1 = ClipperBase.areaOutPt(prevOp);
|
|
2710
|
+
const absArea1 = Math.abs(area1);
|
|
2711
|
+
|
|
2712
|
+
if (absArea1 < 2) {
|
|
2713
|
+
outrec.pts = null;
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
const area2 = this.areaTriangle(ip, splitOp.pt, splitOp.next!.pt);
|
|
2718
|
+
const absArea2 = Math.abs(area2);
|
|
2719
|
+
|
|
2720
|
+
// de-link splitOp and splitOp.next from the path
|
|
2721
|
+
// while inserting the intersection point
|
|
2722
|
+
if ((ip.x === prevOp.pt.x && ip.y === prevOp.pt.y) || (ip.x === nextNextOp.pt.x && ip.y === nextNextOp.pt.y)) {
|
|
2723
|
+
nextNextOp.prev = prevOp;
|
|
2724
|
+
prevOp.next = nextNextOp;
|
|
2725
|
+
} else {
|
|
2726
|
+
const newOp2 = new OutPt(ip, outrec);
|
|
2727
|
+
newOp2.prev = prevOp;
|
|
2728
|
+
newOp2.next = nextNextOp;
|
|
2729
|
+
nextNextOp.prev = newOp2;
|
|
2730
|
+
prevOp.next = newOp2;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
if (!(absArea2 > 1) ||
|
|
2734
|
+
(!(absArea2 > absArea1) &&
|
|
2735
|
+
((area2 > 0) !== (area1 > 0)))) return;
|
|
2736
|
+
|
|
2737
|
+
const newOutRec = this.newOutRec();
|
|
2738
|
+
newOutRec.owner = outrec.owner;
|
|
2739
|
+
splitOp.outrec = newOutRec;
|
|
2740
|
+
splitOp.next!.outrec = newOutRec;
|
|
2741
|
+
|
|
2742
|
+
const newOp = new OutPt(ip, newOutRec);
|
|
2743
|
+
newOp.prev = splitOp.next!;
|
|
2744
|
+
newOp.next = splitOp;
|
|
2745
|
+
newOutRec.pts = newOp;
|
|
2746
|
+
splitOp.prev = newOp;
|
|
2747
|
+
splitOp.next!.next = newOp;
|
|
2748
|
+
|
|
2749
|
+
if (!this.usingPolytree) return;
|
|
2750
|
+
|
|
2751
|
+
if (this.path1InsidePath2(prevOp, newOp)) {
|
|
2752
|
+
if (newOutRec.splits === null) newOutRec.splits = [];
|
|
2753
|
+
newOutRec.splits.push(outrec.idx);
|
|
2754
|
+
} else {
|
|
2755
|
+
if (outrec.splits === null) outrec.splits = [];
|
|
2756
|
+
outrec.splits.push(newOutRec.idx);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
private static areaOutPt(op: OutPt): number {
|
|
2761
|
+
// https://en.wikipedia.org/wiki/Shoelace_formula
|
|
2762
|
+
let area = 0.0;
|
|
2763
|
+
let op2 = op;
|
|
2764
|
+
do {
|
|
2765
|
+
area += (op2.prev.pt.y + op2.pt.y) * (op2.prev.pt.x - op2.pt.x);
|
|
2766
|
+
op2 = op2.next!;
|
|
2767
|
+
} while (op2 !== op);
|
|
2768
|
+
return area * 0.5;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
private areaTriangle(pt1: Point64, pt2: Point64, pt3: Point64): number {
|
|
2772
|
+
return ((pt3.y + pt1.y) * (pt3.x - pt1.x) +
|
|
2773
|
+
(pt1.y + pt2.y) * (pt1.x - pt2.x) +
|
|
2774
|
+
(pt2.y + pt3.y) * (pt2.x - pt3.x));
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
private isValidOwner(outRec: OutRec | null, testOwner: OutRec | null): boolean {
|
|
2778
|
+
while (testOwner !== null && testOwner !== outRec) {
|
|
2779
|
+
testOwner = testOwner.owner;
|
|
2780
|
+
}
|
|
2781
|
+
return testOwner === null;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
private containsRect(rect: Rect64, rec: Rect64): boolean {
|
|
2785
|
+
return rec.left >= rect.left && rec.right <= rect.right &&
|
|
2786
|
+
rec.top >= rect.top && rec.bottom <= rect.bottom;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
private checkSplitOwner(outrec: OutRec, splits: number[]): boolean {
|
|
2790
|
+
// nb: use indexing (not an iterator) in case 'splits' is modified inside this loop (#1029)
|
|
2791
|
+
for (let i = 0; i < splits.length; i++) {
|
|
2792
|
+
let split: OutRec | null = this.outrecList[splits[i]];
|
|
2793
|
+
if (split.pts === null && split.splits !== null &&
|
|
2794
|
+
this.checkSplitOwner(outrec, split.splits)) return true; // #942
|
|
2795
|
+
split = this.getRealOutRec(split);
|
|
2796
|
+
if (split === null || split === outrec || split.recursiveSplit === outrec) continue;
|
|
2797
|
+
split.recursiveSplit = outrec; // #599
|
|
2798
|
+
|
|
2799
|
+
if (split.splits !== null && this.checkSplitOwner(outrec, split.splits)) return true;
|
|
2800
|
+
|
|
2801
|
+
if (!this.checkBounds(split) ||
|
|
2802
|
+
!this.containsRect(split.bounds, outrec.bounds) ||
|
|
2803
|
+
!this.path1InsidePath2(outrec.pts!, split.pts!)) continue;
|
|
2804
|
+
|
|
2805
|
+
if (!this.isValidOwner(outrec, split)) { // split is owned by outrec (#957)
|
|
2806
|
+
split.owner = outrec.owner;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
outrec.owner = split; // found in split
|
|
2810
|
+
return true;
|
|
2811
|
+
}
|
|
2812
|
+
return false;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
export class Clipper64 extends ClipperBase {
|
|
2817
|
+
public addPath(path: Path64, polytype: PathType, isOpen: boolean = false): void {
|
|
2818
|
+
super.addPath(path, polytype, isOpen);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
public addReuseableData(reuseableData: ReuseableDataContainer64): void {
|
|
2822
|
+
super.addReuseableData(reuseableData);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
public addPaths(paths: Paths64, polytype: PathType, isOpen: boolean = false): void {
|
|
2826
|
+
super.addPaths(paths, polytype, isOpen);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
public addSubject(paths: Paths64): void {
|
|
2830
|
+
this.addPaths(paths, PathType.Subject);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
public addOpenSubject(paths: Paths64): void {
|
|
2834
|
+
this.addPaths(paths, PathType.Subject, true);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
public addClip(paths: Paths64): void {
|
|
2838
|
+
this.addPaths(paths, PathType.Clip);
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
public execute(clipType: ClipType, fillRule: FillRule, solutionClosed: Paths64, solutionOpen?: Paths64): boolean;
|
|
2842
|
+
public execute(clipType: ClipType, fillRule: FillRule, polytree: PolyTree64, openPaths?: Paths64): boolean;
|
|
2843
|
+
public execute(clipType: ClipType, fillRule: FillRule, solutionOrTree: Paths64 | PolyTree64, openPathsOrSolutionOpen?: Paths64): boolean {
|
|
2844
|
+
if (Array.isArray(solutionOrTree)) {
|
|
2845
|
+
// Paths64 version
|
|
2846
|
+
const solutionClosed = solutionOrTree;
|
|
2847
|
+
const solutionOpen = openPathsOrSolutionOpen;
|
|
2848
|
+
|
|
2849
|
+
solutionClosed.length = 0;
|
|
2850
|
+
if (solutionOpen) solutionOpen.length = 0;
|
|
2851
|
+
|
|
2852
|
+
try {
|
|
2853
|
+
this.executeInternal(clipType, fillRule);
|
|
2854
|
+
this.buildPaths(solutionClosed, solutionOpen || []);
|
|
2855
|
+
} catch {
|
|
2856
|
+
this.succeeded = false;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
this.clearSolutionOnly();
|
|
2860
|
+
return this.succeeded;
|
|
2861
|
+
} else {
|
|
2862
|
+
// PolyTree64 version
|
|
2863
|
+
const polytree = solutionOrTree;
|
|
2864
|
+
const openPaths = openPathsOrSolutionOpen;
|
|
2865
|
+
|
|
2866
|
+
polytree.clear();
|
|
2867
|
+
if (openPaths) openPaths.length = 0;
|
|
2868
|
+
this.usingPolytree = true;
|
|
2869
|
+
|
|
2870
|
+
try {
|
|
2871
|
+
this.executeInternal(clipType, fillRule);
|
|
2872
|
+
this.buildTree(polytree, openPaths || []);
|
|
2873
|
+
} catch {
|
|
2874
|
+
this.succeeded = false;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
this.clearSolutionOnly();
|
|
2878
|
+
return this.succeeded;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
export class ClipperD extends ClipperBase {
|
|
2884
|
+
private readonly scale: number;
|
|
2885
|
+
private readonly invScale: number;
|
|
2886
|
+
|
|
2887
|
+
constructor(roundingDecimalPrecision: number = 2) {
|
|
2888
|
+
super();
|
|
2889
|
+
InternalClipper.checkPrecision(roundingDecimalPrecision);
|
|
2890
|
+
this.scale = Math.pow(10, roundingDecimalPrecision);
|
|
2891
|
+
this.invScale = 1 / this.scale;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
private scalePathDFromInt(path: Path64, scale: number): PathD {
|
|
2895
|
+
const result: PathD = [];
|
|
2896
|
+
for (const pt of path) {
|
|
2897
|
+
result.push({
|
|
2898
|
+
x: pt.x * scale,
|
|
2899
|
+
y: pt.y * scale
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
return result;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
public buildPathsD(solutionClosed: PathsD, solutionOpen: PathsD): boolean {
|
|
2906
|
+
solutionClosed.length = 0;
|
|
2907
|
+
solutionOpen.length = 0;
|
|
2908
|
+
|
|
2909
|
+
let i = 0;
|
|
2910
|
+
// outrecList.length is not static here because
|
|
2911
|
+
// CleanCollinear can indirectly add additional OutRec
|
|
2912
|
+
while (i < this.outrecList.length) {
|
|
2913
|
+
const outrec = this.outrecList[i++];
|
|
2914
|
+
if (outrec.pts === null) continue;
|
|
2915
|
+
|
|
2916
|
+
const path: Path64 = [];
|
|
2917
|
+
if (outrec.isOpen) {
|
|
2918
|
+
if (ClipperBase.buildPath(outrec.pts, this.reverseSolution, true, path)) {
|
|
2919
|
+
solutionOpen.push(this.scalePathDFromInt(path, this.invScale));
|
|
2920
|
+
}
|
|
2921
|
+
} else {
|
|
2922
|
+
this.cleanCollinear(outrec);
|
|
2923
|
+
// closed paths should always return a Positive orientation
|
|
2924
|
+
// except when ReverseSolution == true
|
|
2925
|
+
if (ClipperBase.buildPath(outrec.pts, this.reverseSolution, false, path)) {
|
|
2926
|
+
solutionClosed.push(this.scalePathDFromInt(path, this.invScale));
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
return true;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
public buildTreeD(polytree: PolyPathBase, solutionOpen: PathsD): void {
|
|
2934
|
+
polytree.clear();
|
|
2935
|
+
solutionOpen.length = 0;
|
|
2936
|
+
|
|
2937
|
+
let i = 0;
|
|
2938
|
+
// outrecList.length is not static here because
|
|
2939
|
+
// BuildPathD below can indirectly add additional OutRec
|
|
2940
|
+
while (i < this.outrecList.length) {
|
|
2941
|
+
const outrec = this.outrecList[i++];
|
|
2942
|
+
if (outrec.pts === null) continue;
|
|
2943
|
+
|
|
2944
|
+
if (outrec.isOpen) {
|
|
2945
|
+
const openPath: Path64 = [];
|
|
2946
|
+
if (ClipperBase.buildPath(outrec.pts, this.reverseSolution, true, openPath)) {
|
|
2947
|
+
solutionOpen.push(this.scalePathDFromInt(openPath, this.invScale));
|
|
2948
|
+
}
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
if (this.checkBounds(outrec)) {
|
|
2953
|
+
this.recursiveCheckOwners(outrec, polytree);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
public addPath(path: PathD, polytype: PathType, isOpen: boolean = false): void {
|
|
2959
|
+
super.addPath(Clipper.scalePath64(path, this.scale), polytype, isOpen);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
public addPaths(paths: PathsD, polytype: PathType, isOpen: boolean = false): void {
|
|
2963
|
+
super.addPaths(Clipper.scalePaths64(paths, this.scale), polytype, isOpen);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
public addSubject(path: PathD): void {
|
|
2967
|
+
this.addPath(path, PathType.Subject);
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
public addOpenSubject(path: PathD): void {
|
|
2971
|
+
this.addPath(path, PathType.Subject, true);
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
public addClip(path: PathD): void {
|
|
2975
|
+
this.addPath(path, PathType.Clip);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
public addSubjectPaths(paths: PathsD): void {
|
|
2979
|
+
this.addPaths(paths, PathType.Subject);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
public addOpenSubjectPaths(paths: PathsD): void {
|
|
2983
|
+
this.addPaths(paths, PathType.Subject, true);
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
public addClipPaths(paths: PathsD): void {
|
|
2987
|
+
this.addPaths(paths, PathType.Clip);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
public execute(clipType: ClipType, fillRule: FillRule, solutionClosed: PathsD, solutionOpen?: PathsD): boolean;
|
|
2991
|
+
public execute(clipType: ClipType, fillRule: FillRule, polytree: PolyTreeD, openPaths?: PathsD): boolean;
|
|
2992
|
+
public execute(clipType: ClipType, fillRule: FillRule, solutionOrTree: PathsD | PolyTreeD, openPathsOrSolutionOpen?: PathsD): boolean {
|
|
2993
|
+
if (Array.isArray(solutionOrTree)) {
|
|
2994
|
+
// PathsD version - match C# implementation exactly
|
|
2995
|
+
const solutionClosed = solutionOrTree;
|
|
2996
|
+
const solutionOpen = openPathsOrSolutionOpen;
|
|
2997
|
+
|
|
2998
|
+
// Use Paths64 internally like C# does
|
|
2999
|
+
const solClosed64: Paths64 = [];
|
|
3000
|
+
const solOpen64: Paths64 = [];
|
|
3001
|
+
|
|
3002
|
+
solutionClosed.length = 0;
|
|
3003
|
+
if (solutionOpen) solutionOpen.length = 0;
|
|
3004
|
+
|
|
3005
|
+
let success = true;
|
|
3006
|
+
try {
|
|
3007
|
+
this.executeInternal(clipType, fillRule);
|
|
3008
|
+
// Call regular buildPaths which includes cleanCollinear and fixSelfIntersects
|
|
3009
|
+
this.buildPaths(solClosed64, solOpen64);
|
|
3010
|
+
} catch {
|
|
3011
|
+
success = false;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
this.clearSolutionOnly();
|
|
3015
|
+
if (!success) return false;
|
|
3016
|
+
|
|
3017
|
+
// Convert Paths64 to PathsD
|
|
3018
|
+
for (const path of solClosed64) {
|
|
3019
|
+
solutionClosed.push(this.scalePathDFromInt(path, this.invScale));
|
|
3020
|
+
}
|
|
3021
|
+
if (solutionOpen) {
|
|
3022
|
+
for (const path of solOpen64) {
|
|
3023
|
+
solutionOpen.push(this.scalePathDFromInt(path, this.invScale));
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
return true;
|
|
3028
|
+
} else {
|
|
3029
|
+
// PolyTreeD version
|
|
3030
|
+
const polytree = solutionOrTree;
|
|
3031
|
+
const openPaths = openPathsOrSolutionOpen;
|
|
3032
|
+
|
|
3033
|
+
polytree.clear();
|
|
3034
|
+
if (openPaths) openPaths.length = 0;
|
|
3035
|
+
this.usingPolytree = true;
|
|
3036
|
+
polytree.scale = this.scale;
|
|
3037
|
+
|
|
3038
|
+
let success = true;
|
|
3039
|
+
try {
|
|
3040
|
+
this.executeInternal(clipType, fillRule);
|
|
3041
|
+
this.buildTreeD(polytree, openPaths || []);
|
|
3042
|
+
} catch {
|
|
3043
|
+
success = false;
|
|
3044
|
+
}
|
|
3045
|
+
this.clearSolutionOnly();
|
|
3046
|
+
return success;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// Forward declaration for Clipper class
|
|
3052
|
+
export namespace Clipper {
|
|
3053
|
+
export function area(path: Path64): number {
|
|
3054
|
+
// https://en.wikipedia.org/wiki/Shoelace_formula
|
|
3055
|
+
let a = 0.0;
|
|
3056
|
+
const cnt = path.length;
|
|
3057
|
+
if (cnt < 3) return 0.0;
|
|
3058
|
+
let prevPt = path[cnt - 1];
|
|
3059
|
+
for (const pt of path) {
|
|
3060
|
+
a += (prevPt.y + pt.y) * (prevPt.x - pt.x);
|
|
3061
|
+
prevPt = pt;
|
|
3062
|
+
}
|
|
3063
|
+
return a * 0.5;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
export function areaD(path: PathD): number {
|
|
3067
|
+
let a = 0.0;
|
|
3068
|
+
const cnt = path.length;
|
|
3069
|
+
if (cnt < 3) return 0.0;
|
|
3070
|
+
let prevPt = path[cnt - 1];
|
|
3071
|
+
for (const pt of path) {
|
|
3072
|
+
a += (prevPt.y + pt.y) * (prevPt.x - pt.x);
|
|
3073
|
+
prevPt = pt;
|
|
3074
|
+
}
|
|
3075
|
+
return a * 0.5;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
export function scalePath64(path: PathD, scale: number): Path64 {
|
|
3079
|
+
const result: Path64 = [];
|
|
3080
|
+
for (const pt of path) {
|
|
3081
|
+
result.push({
|
|
3082
|
+
x: Math.round(pt.x * scale),
|
|
3083
|
+
y: Math.round(pt.y * scale)
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
return result;
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
export function scalePaths64(paths: PathsD, scale: number): Paths64 {
|
|
3090
|
+
const result: Paths64 = [];
|
|
3091
|
+
for (const path of paths) {
|
|
3092
|
+
result.push(scalePath64(path, scale));
|
|
3093
|
+
}
|
|
3094
|
+
return result;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
export function scalePathD(path: Path64, scale: number): PathD {
|
|
3098
|
+
const result: PathD = [];
|
|
3099
|
+
for (const pt of path) {
|
|
3100
|
+
result.push({
|
|
3101
|
+
x: pt.x * scale,
|
|
3102
|
+
y: pt.y * scale
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
return result;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
export function scalePathsD(paths: Paths64, scale: number): PathsD {
|
|
3109
|
+
const result: PathsD = [];
|
|
3110
|
+
for (const path of paths) {
|
|
3111
|
+
result.push(scalePathD(path, scale));
|
|
3112
|
+
}
|
|
3113
|
+
return result;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
|