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/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
+