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.
@@ -0,0 +1,1028 @@
1
+ /*******************************************************************************
2
+ * Author : Angus Johnson *
3
+ * Date : 11 October 2025 *
4
+ * Website : https://www.angusj.com *
5
+ * Copyright : Angus Johnson 2010-2025 *
6
+ * Purpose : FAST rectangular clipping *
7
+ * License : https://www.boost.org/LICENSE_1_0.txt *
8
+ *******************************************************************************/
9
+
10
+ import {
11
+ Point64, PointD, Path64, PathD, Paths64, PathsD, Rect64, RectD,
12
+ PointInPolygonResult, InternalClipper, Point64Utils, Rect64Utils, RectDUtils
13
+ } from './Core';
14
+
15
+ export class OutPt2 {
16
+ public next: OutPt2 | null = null;
17
+ public prev: OutPt2 | null = null;
18
+ public pt: Point64;
19
+ public ownerIdx: number = 0;
20
+ public edge: (OutPt2 | null)[] | null = null;
21
+
22
+ constructor(pt: Point64) {
23
+ this.pt = pt;
24
+ }
25
+ }
26
+
27
+ enum Location {
28
+ left = 0,
29
+ top = 1,
30
+ right = 2,
31
+ bottom = 3,
32
+ inside = 4
33
+ }
34
+
35
+ export class RectClip64 {
36
+
37
+ protected readonly rect: Rect64;
38
+ protected readonly mp: Point64;
39
+ protected readonly rectPath: Path64;
40
+ protected pathBounds: Rect64 = { left: 0, top: 0, right: 0, bottom: 0 };
41
+ protected results: (OutPt2 | null)[] = [];
42
+ protected edges: (OutPt2 | null)[][] = [];
43
+ protected currIdx: number = -1;
44
+
45
+ constructor(rect: Rect64) {
46
+ this.currIdx = -1;
47
+ this.rect = rect;
48
+ this.mp = Rect64Utils.midPoint(rect);
49
+ this.rectPath = Rect64Utils.asPath(this.rect);
50
+ this.results = [];
51
+ this.edges = [];
52
+ for (let i = 0; i < 8; i++) {
53
+ this.edges[i] = [];
54
+ }
55
+ }
56
+
57
+ protected add(pt: Point64, startingNewPath: boolean = false): OutPt2 {
58
+ // this method is only called by InternalExecute.
59
+ // Later splitting and rejoining won't create additional op's,
60
+ // though they will change the (non-storage) fResults count.
61
+ let currIdx = this.results.length;
62
+ let result: OutPt2;
63
+
64
+ if ((currIdx === 0) || startingNewPath) {
65
+ result = new OutPt2(pt);
66
+ this.results.push(result);
67
+ result.ownerIdx = currIdx;
68
+ result.prev = result;
69
+ result.next = result;
70
+ } else {
71
+ currIdx--;
72
+ const prevOp = this.results[currIdx];
73
+ if (prevOp && Point64Utils.equals(prevOp.pt, pt)) return prevOp;
74
+
75
+ result = new OutPt2(pt);
76
+ result.ownerIdx = currIdx;
77
+ result.next = prevOp!.next;
78
+ prevOp!.next!.prev = result;
79
+ prevOp!.next = result;
80
+ result.prev = prevOp!;
81
+ this.results[currIdx] = result;
82
+ }
83
+ return result;
84
+ }
85
+
86
+ private static path1ContainsPath2(path1: Path64, path2: Path64): boolean {
87
+ // nb: occasionally, due to rounding, path1 may
88
+ // appear (momentarily) inside or outside path2.
89
+ let ioCount = 0;
90
+ for (const pt of path2) {
91
+ const pip = InternalClipper.pointInPolygon(pt, path1);
92
+ switch (pip) {
93
+ case PointInPolygonResult.IsInside:
94
+ ioCount--;
95
+ break;
96
+ case PointInPolygonResult.IsOutside:
97
+ ioCount++;
98
+ break;
99
+ }
100
+ if (Math.abs(ioCount) > 1) break;
101
+ }
102
+ return ioCount <= 0;
103
+ }
104
+
105
+ private static isClockwise(
106
+ prev: Location,
107
+ curr: Location,
108
+ prevPt: Point64,
109
+ currPt: Point64,
110
+ rectMidPoint: Point64
111
+ ): boolean {
112
+ if (RectClip64.areOpposites(prev, curr)) {
113
+ return InternalClipper.crossProduct(prevPt, rectMidPoint, currPt) < 0;
114
+ }
115
+ return RectClip64.headingClockwise(prev, curr);
116
+ }
117
+
118
+ private static areOpposites(prev: Location, curr: Location): boolean {
119
+ return Math.abs(prev - curr) === 2;
120
+ }
121
+
122
+ private static headingClockwise(prev: Location, curr: Location): boolean {
123
+ return (prev + 1) % 4 === curr;
124
+ }
125
+
126
+ private static getAdjacentLocation(loc: Location, isClockwise: boolean): Location {
127
+ const delta = isClockwise ? 1 : 3;
128
+ return (loc + delta) % 4;
129
+ }
130
+
131
+ private static unlinkOp(op: OutPt2): OutPt2 | null {
132
+ if (op.next === op) return null;
133
+ op.prev!.next = op.next;
134
+ op.next!.prev = op.prev;
135
+ return op.next;
136
+ }
137
+
138
+ private static unlinkOpBack(op: OutPt2): OutPt2 | null {
139
+ if (op.next === op) return null;
140
+ op.prev!.next = op.next;
141
+ op.next!.prev = op.prev;
142
+ return op.prev;
143
+ }
144
+
145
+ private static getEdgesForPt(pt: Point64, rec: Rect64): number {
146
+ let result = 0;
147
+ if (pt.x === rec.left) result = 1;
148
+ else if (pt.x === rec.right) result = 4;
149
+ if (pt.y === rec.top) result += 2;
150
+ else if (pt.y === rec.bottom) result += 8;
151
+ return result;
152
+ }
153
+
154
+ private static isHeadingClockwise(pt1: Point64, pt2: Point64, edgeIdx: number): boolean {
155
+ switch (edgeIdx) {
156
+ case 0: return pt2.y < pt1.y;
157
+ case 1: return pt2.x > pt1.x;
158
+ case 2: return pt2.y > pt1.y;
159
+ default: return pt2.x < pt1.x;
160
+ }
161
+ }
162
+
163
+ private static hasHorzOverlap(left1: Point64, right1: Point64, left2: Point64, right2: Point64): boolean {
164
+ return (left1.x < right2.x) && (right1.x > left2.x);
165
+ }
166
+
167
+ private static hasVertOverlap(top1: Point64, bottom1: Point64, top2: Point64, bottom2: Point64): boolean {
168
+ return (top1.y < bottom2.y) && (bottom1.y > top2.y);
169
+ }
170
+
171
+ private static addToEdge(edge: (OutPt2 | null)[], op: OutPt2): void {
172
+ if (op.edge !== null) return;
173
+ op.edge = edge;
174
+ edge.push(op);
175
+ }
176
+
177
+ private static uncoupleEdge(op: OutPt2): void {
178
+ if (op.edge === null) return;
179
+ for (let i = 0; i < op.edge.length; i++) {
180
+ const op2 = op.edge[i];
181
+ if (op2 === op) {
182
+ op.edge[i] = null;
183
+ break;
184
+ }
185
+ }
186
+ op.edge = null;
187
+ }
188
+
189
+ private static setNewOwner(op: OutPt2, newIdx: number): void {
190
+ op.ownerIdx = newIdx;
191
+ let op2 = op.next!;
192
+ while (op2 !== op) {
193
+ op2.ownerIdx = newIdx;
194
+ op2 = op2.next!;
195
+ }
196
+ }
197
+
198
+ private addCorner(prev: Location, curr: Location): void {
199
+ this.add(RectClip64.headingClockwise(prev, curr) ?
200
+ this.rectPath[prev] : this.rectPath[curr]);
201
+ }
202
+
203
+ private addCornerWithDirection(loc: Location, isClockwise: boolean): Location {
204
+ if (isClockwise) {
205
+ this.add(this.rectPath[loc]);
206
+ return RectClip64.getAdjacentLocation(loc, true);
207
+ } else {
208
+ const newLoc = RectClip64.getAdjacentLocation(loc, false);
209
+ this.add(this.rectPath[newLoc]);
210
+ return newLoc;
211
+ }
212
+ }
213
+
214
+ protected static getLocation(rec: Rect64, pt: Point64): { location: Location; isOnRect: boolean } {
215
+ if (pt.x === rec.left && pt.y >= rec.top && pt.y <= rec.bottom) {
216
+ return { location: Location.left, isOnRect: true };
217
+ }
218
+ if (pt.x === rec.right && pt.y >= rec.top && pt.y <= rec.bottom) {
219
+ return { location: Location.right, isOnRect: true };
220
+ }
221
+ if (pt.y === rec.top && pt.x >= rec.left && pt.x <= rec.right) {
222
+ return { location: Location.top, isOnRect: true };
223
+ }
224
+ if (pt.y === rec.bottom && pt.x >= rec.left && pt.x <= rec.right) {
225
+ return { location: Location.bottom, isOnRect: true };
226
+ }
227
+
228
+ let location: Location;
229
+ if (pt.x < rec.left) location = Location.left;
230
+ else if (pt.x > rec.right) location = Location.right;
231
+ else if (pt.y < rec.top) location = Location.top;
232
+ else if (pt.y > rec.bottom) location = Location.bottom;
233
+ else location = Location.inside;
234
+
235
+ return { location, isOnRect: false };
236
+ }
237
+
238
+ private static isHorizontal(pt1: Point64, pt2: Point64): boolean {
239
+ return pt1.y === pt2.y;
240
+ }
241
+
242
+ private static getSegmentIntersection(p1: Point64, p2: Point64, p3: Point64, p4: Point64): { intersects: boolean; point: Point64 } {
243
+ const res1 = InternalClipper.crossProduct(p1, p3, p4);
244
+ const res2 = InternalClipper.crossProduct(p2, p3, p4);
245
+
246
+ if (res1 === 0) {
247
+ const ip = p1;
248
+ if (res2 === 0) return { intersects: false, point: ip }; // segments are collinear
249
+ if (Point64Utils.equals(p1, p3) || Point64Utils.equals(p1, p4)) return { intersects: true, point: ip };
250
+ if (RectClip64.isHorizontal(p3, p4)) {
251
+ return { intersects: (p1.x > p3.x) === (p1.x < p4.x), point: ip };
252
+ }
253
+ return { intersects: (p1.y > p3.y) === (p1.y < p4.y), point: ip };
254
+ }
255
+
256
+ if (res2 === 0) {
257
+ const ip = p2;
258
+ if (Point64Utils.equals(p2, p3) || Point64Utils.equals(p2, p4)) return { intersects: true, point: ip };
259
+ if (RectClip64.isHorizontal(p3, p4)) {
260
+ return { intersects: (p2.x > p3.x) === (p2.x < p4.x), point: ip };
261
+ }
262
+ return { intersects: (p2.y > p3.y) === (p2.y < p4.y), point: ip };
263
+ }
264
+
265
+ if ((res1 > 0) === (res2 > 0)) {
266
+ return { intersects: false, point: { x: 0, y: 0 } };
267
+ }
268
+
269
+ const res3 = InternalClipper.crossProduct(p3, p1, p2);
270
+ const res4 = InternalClipper.crossProduct(p4, p1, p2);
271
+
272
+ if (res3 === 0) {
273
+ const ip = p3;
274
+ if (Point64Utils.equals(p3, p1) || Point64Utils.equals(p3, p2)) return { intersects: true, point: ip };
275
+ if (RectClip64.isHorizontal(p1, p2)) {
276
+ return { intersects: (p3.x > p1.x) === (p3.x < p2.x), point: ip };
277
+ }
278
+ return { intersects: (p3.y > p1.y) === (p3.y < p2.y), point: ip };
279
+ }
280
+
281
+ if (res4 === 0) {
282
+ const ip = p4;
283
+ if (Point64Utils.equals(p4, p1) || Point64Utils.equals(p4, p2)) return { intersects: true, point: ip };
284
+ if (RectClip64.isHorizontal(p1, p2)) {
285
+ return { intersects: (p4.x > p1.x) === (p4.x < p2.x), point: ip };
286
+ }
287
+ return { intersects: (p4.y > p1.y) === (p4.y < p2.y), point: ip };
288
+ }
289
+
290
+ if ((res3 > 0) === (res4 > 0)) {
291
+ return { intersects: false, point: { x: 0, y: 0 } };
292
+ }
293
+
294
+ // segments must intersect to get here
295
+ return InternalClipper.getLineIntersectPt(p1, p2, p3, p4);
296
+ }
297
+
298
+ protected static getIntersection(
299
+ rectPath: Path64,
300
+ p: Point64,
301
+ p2: Point64,
302
+ loc: Location
303
+ ): { intersects: boolean; point: Point64; newLocation: Location } {
304
+ // gets the pt of intersection between rectPath and segment(p, p2) that's closest to 'p'
305
+ // when result == false, loc will remain unchanged
306
+ let ip: Point64 = { x: 0, y: 0 };
307
+ let newLocation = loc;
308
+
309
+ switch (loc) {
310
+ case Location.left:
311
+ {
312
+ const result1 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[3]);
313
+ if (result1.intersects) {
314
+ ip = result1.point;
315
+ return { intersects: true, point: ip, newLocation };
316
+ }
317
+ if (p.y < rectPath[0].y) {
318
+ const result2 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[1]);
319
+ if (result2.intersects) {
320
+ newLocation = Location.top;
321
+ return { intersects: true, point: result2.point, newLocation };
322
+ }
323
+ }
324
+ const result3 = RectClip64.getSegmentIntersection(p, p2, rectPath[2], rectPath[3]);
325
+ if (result3.intersects) {
326
+ newLocation = Location.bottom;
327
+ return { intersects: true, point: result3.point, newLocation };
328
+ }
329
+ return { intersects: false, point: ip, newLocation };
330
+ }
331
+
332
+ case Location.right:
333
+ {
334
+ const result1 = RectClip64.getSegmentIntersection(p, p2, rectPath[1], rectPath[2]);
335
+ if (result1.intersects) {
336
+ return { intersects: true, point: result1.point, newLocation };
337
+ }
338
+ if (p.y < rectPath[0].y) {
339
+ const result2 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[1]);
340
+ if (result2.intersects) {
341
+ newLocation = Location.top;
342
+ return { intersects: true, point: result2.point, newLocation };
343
+ }
344
+ }
345
+ const result3 = RectClip64.getSegmentIntersection(p, p2, rectPath[2], rectPath[3]);
346
+ if (result3.intersects) {
347
+ newLocation = Location.bottom;
348
+ return { intersects: true, point: result3.point, newLocation };
349
+ }
350
+ return { intersects: false, point: ip, newLocation };
351
+ }
352
+
353
+ case Location.top:
354
+ {
355
+ const result1 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[1]);
356
+ if (result1.intersects) {
357
+ return { intersects: true, point: result1.point, newLocation };
358
+ }
359
+ if (p.x < rectPath[0].x) {
360
+ const result2 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[3]);
361
+ if (result2.intersects) {
362
+ newLocation = Location.left;
363
+ return { intersects: true, point: result2.point, newLocation };
364
+ }
365
+ }
366
+ if (p.x <= rectPath[1].x) {
367
+ return { intersects: false, point: ip, newLocation };
368
+ }
369
+ const result3 = RectClip64.getSegmentIntersection(p, p2, rectPath[1], rectPath[2]);
370
+ if (result3.intersects) {
371
+ newLocation = Location.right;
372
+ return { intersects: true, point: result3.point, newLocation };
373
+ }
374
+ return { intersects: false, point: ip, newLocation };
375
+ }
376
+
377
+ case Location.bottom:
378
+ {
379
+ const result1 = RectClip64.getSegmentIntersection(p, p2, rectPath[2], rectPath[3]);
380
+ if (result1.intersects) {
381
+ return { intersects: true, point: result1.point, newLocation };
382
+ }
383
+ if (p.x < rectPath[3].x) {
384
+ const result2 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[3]);
385
+ if (result2.intersects) {
386
+ newLocation = Location.left;
387
+ return { intersects: true, point: result2.point, newLocation };
388
+ }
389
+ }
390
+ if (p.x <= rectPath[2].x) {
391
+ return { intersects: false, point: ip, newLocation };
392
+ }
393
+ const result3 = RectClip64.getSegmentIntersection(p, p2, rectPath[1], rectPath[2]);
394
+ if (result3.intersects) {
395
+ newLocation = Location.right;
396
+ return { intersects: true, point: result3.point, newLocation };
397
+ }
398
+ return { intersects: false, point: ip, newLocation };
399
+ }
400
+
401
+ default:
402
+ {
403
+ const result1 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[3]);
404
+ if (result1.intersects) {
405
+ newLocation = Location.left;
406
+ return { intersects: true, point: result1.point, newLocation };
407
+ }
408
+ const result2 = RectClip64.getSegmentIntersection(p, p2, rectPath[0], rectPath[1]);
409
+ if (result2.intersects) {
410
+ newLocation = Location.top;
411
+ return { intersects: true, point: result2.point, newLocation };
412
+ }
413
+ const result3 = RectClip64.getSegmentIntersection(p, p2, rectPath[1], rectPath[2]);
414
+ if (result3.intersects) {
415
+ newLocation = Location.right;
416
+ return { intersects: true, point: result3.point, newLocation };
417
+ }
418
+ const result4 = RectClip64.getSegmentIntersection(p, p2, rectPath[2], rectPath[3]);
419
+ if (result4.intersects) {
420
+ newLocation = Location.bottom;
421
+ return { intersects: true, point: result4.point, newLocation };
422
+ }
423
+ return { intersects: false, point: ip, newLocation };
424
+ }
425
+ }
426
+ }
427
+
428
+ protected getNextLocation(path: Path64, loc: Location, i: number, highI: number): { location: Location; index: number } {
429
+ let newI = i;
430
+ let newLoc = loc;
431
+
432
+ switch (loc) {
433
+ case Location.left:
434
+ while (newI <= highI && path[newI].x <= this.rect.left) newI++;
435
+ if (newI > highI) break;
436
+ if (path[newI].x >= this.rect.right) newLoc = Location.right;
437
+ else if (path[newI].y <= this.rect.top) newLoc = Location.top;
438
+ else if (path[newI].y >= this.rect.bottom) newLoc = Location.bottom;
439
+ else newLoc = Location.inside;
440
+ break;
441
+
442
+ case Location.top:
443
+ while (newI <= highI && path[newI].y <= this.rect.top) newI++;
444
+ if (newI > highI) break;
445
+ if (path[newI].y >= this.rect.bottom) newLoc = Location.bottom;
446
+ else if (path[newI].x <= this.rect.left) newLoc = Location.left;
447
+ else if (path[newI].x >= this.rect.right) newLoc = Location.right;
448
+ else newLoc = Location.inside;
449
+ break;
450
+
451
+ case Location.right:
452
+ while (newI <= highI && path[newI].x >= this.rect.right) newI++;
453
+ if (newI > highI) break;
454
+ if (path[newI].x <= this.rect.left) newLoc = Location.left;
455
+ else if (path[newI].y <= this.rect.top) newLoc = Location.top;
456
+ else if (path[newI].y >= this.rect.bottom) newLoc = Location.bottom;
457
+ else newLoc = Location.inside;
458
+ break;
459
+
460
+ case Location.bottom:
461
+ while (newI <= highI && path[newI].y >= this.rect.bottom) newI++;
462
+ if (newI > highI) break;
463
+ if (path[newI].y <= this.rect.top) newLoc = Location.top;
464
+ else if (path[newI].x <= this.rect.left) newLoc = Location.left;
465
+ else if (path[newI].x >= this.rect.right) newLoc = Location.right;
466
+ else newLoc = Location.inside;
467
+ break;
468
+
469
+ case Location.inside:
470
+ while (newI <= highI) {
471
+ if (path[newI].x < this.rect.left) newLoc = Location.left;
472
+ else if (path[newI].x > this.rect.right) newLoc = Location.right;
473
+ else if (path[newI].y > this.rect.bottom) newLoc = Location.bottom;
474
+ else if (path[newI].y < this.rect.top) newLoc = Location.top;
475
+ else {
476
+ this.add(path[newI]);
477
+ newI++;
478
+ continue;
479
+ }
480
+ break;
481
+ }
482
+ break;
483
+ }
484
+
485
+ return { location: newLoc, index: newI };
486
+ }
487
+
488
+ private static startLocsAreClockwise(startLocs: Location[]): boolean {
489
+ let result = 0;
490
+ for (let i = 1; i < startLocs.length; i++) {
491
+ const d = startLocs[i] - startLocs[i - 1];
492
+ switch (d) {
493
+ case -1: result -= 1; break;
494
+ case 1: result += 1; break;
495
+ case -3: result += 1; break;
496
+ case 3: result -= 1; break;
497
+ }
498
+ }
499
+ return result > 0;
500
+ }
501
+
502
+ private executeInternal(path: Path64): void {
503
+ if (path.length < 3 || Rect64Utils.isEmpty(this.rect)) return;
504
+
505
+ const startLocs: Location[] = [];
506
+ let firstCross: Location = Location.inside;
507
+ let crossingLoc: Location = firstCross;
508
+ let prev: Location = firstCross;
509
+
510
+ const highI = path.length - 1;
511
+ const lastLocResult = RectClip64.getLocation(this.rect, path[highI]);
512
+ let loc = lastLocResult.location;
513
+
514
+ if (lastLocResult.isOnRect) {
515
+ let i = highI - 1;
516
+ while (i >= 0) {
517
+ const prevLocResult = RectClip64.getLocation(this.rect, path[i]);
518
+ if (!prevLocResult.isOnRect) {
519
+ prev = prevLocResult.location;
520
+ break;
521
+ }
522
+ i--;
523
+ }
524
+ if (i < 0) {
525
+ for (const pt of path) {
526
+ this.add(pt);
527
+ }
528
+ return;
529
+ }
530
+ if (prev === Location.inside) loc = Location.inside;
531
+ }
532
+ const startingLoc = loc;
533
+
534
+ ///////////////////////////////////////////////////
535
+ let i = 0;
536
+ while (i <= highI) {
537
+ prev = loc;
538
+ const prevCrossLoc: Location = crossingLoc;
539
+
540
+ const nextLocResult = this.getNextLocation(path, loc, i, highI);
541
+ loc = nextLocResult.location;
542
+ i = nextLocResult.index;
543
+
544
+ if (i > highI) break;
545
+
546
+ const prevPt = (i === 0) ? path[highI] : path[i - 1];
547
+ crossingLoc = loc;
548
+
549
+ const intersectionResult = RectClip64.getIntersection(this.rectPath, path[i], prevPt, crossingLoc);
550
+ if (!intersectionResult.intersects) {
551
+ // ie remaining outside
552
+ if (prevCrossLoc === Location.inside) {
553
+ const isClockw = RectClip64.isClockwise(prev, loc, prevPt, path[i], this.mp);
554
+ do {
555
+ startLocs.push(prev);
556
+ prev = RectClip64.getAdjacentLocation(prev, isClockw);
557
+ } while (prev !== loc);
558
+ crossingLoc = prevCrossLoc; // still not crossed
559
+ } else if (prev !== Location.inside && prev !== loc) {
560
+ const isClockw = RectClip64.isClockwise(prev, loc, prevPt, path[i], this.mp);
561
+ do {
562
+ prev = this.addCornerWithDirection(prev, isClockw);
563
+ } while (prev !== loc);
564
+ }
565
+ ++i;
566
+ continue;
567
+ }
568
+
569
+ const ip = intersectionResult.point;
570
+ crossingLoc = intersectionResult.newLocation;
571
+
572
+ ////////////////////////////////////////////////////
573
+ // we must be crossing the rect boundary to get here
574
+ ////////////////////////////////////////////////////
575
+
576
+ if (loc === Location.inside) { // path must be entering rect
577
+ if (firstCross === Location.inside) {
578
+ firstCross = crossingLoc;
579
+ startLocs.push(prev);
580
+ } else if (prev !== crossingLoc) {
581
+ const isClockw = RectClip64.isClockwise(prev, crossingLoc, prevPt, path[i], this.mp);
582
+ do {
583
+ prev = this.addCornerWithDirection(prev, isClockw);
584
+ } while (prev !== crossingLoc);
585
+ }
586
+ } else if (prev !== Location.inside) {
587
+ // passing right through rect. 'ip' here will be the second
588
+ // intersect pt but we'll also need the first intersect pt (ip2)
589
+ loc = prev;
590
+ const intersection2Result = RectClip64.getIntersection(this.rectPath, prevPt, path[i], loc);
591
+ const ip2 = intersection2Result.point;
592
+
593
+ if (prevCrossLoc !== Location.inside && prevCrossLoc !== loc) { //#597
594
+ this.addCorner(prevCrossLoc, loc);
595
+ }
596
+
597
+ if (firstCross === Location.inside) {
598
+ firstCross = loc;
599
+ startLocs.push(prev);
600
+ }
601
+
602
+ loc = crossingLoc;
603
+ this.add(ip2);
604
+ if (Point64Utils.equals(ip, ip2)) {
605
+ // it's very likely that path[i] is on rect
606
+ const pathLocResult = RectClip64.getLocation(this.rect, path[i]);
607
+ loc = pathLocResult.location;
608
+ this.addCorner(crossingLoc, loc);
609
+ crossingLoc = loc;
610
+ continue;
611
+ }
612
+ } else { // path must be exiting rect
613
+ loc = crossingLoc;
614
+ if (firstCross === Location.inside) {
615
+ firstCross = crossingLoc;
616
+ }
617
+ }
618
+
619
+ this.add(ip);
620
+ } //while i <= highI
621
+ ///////////////////////////////////////////////////
622
+
623
+ if (firstCross === Location.inside) {
624
+ // path never intersects
625
+ if (startingLoc === Location.inside) return;
626
+ if (!Rect64Utils.containsRect(this.pathBounds, this.rect) ||
627
+ !RectClip64.path1ContainsPath2(path, this.rectPath)) return;
628
+
629
+ const startLocsClockwise = RectClip64.startLocsAreClockwise(startLocs);
630
+ for (let j = 0; j < 4; j++) {
631
+ const k = startLocsClockwise ? j : 3 - j; // ie reverse result path
632
+ this.add(this.rectPath[k]);
633
+ RectClip64.addToEdge(this.edges[k * 2], this.results[0]!);
634
+ }
635
+ } else if (loc !== Location.inside &&
636
+ (loc !== firstCross || startLocs.length > 2)) {
637
+
638
+ if (startLocs.length > 0) {
639
+ prev = loc;
640
+ for (const loc2 of startLocs) {
641
+ if (prev === loc2) continue;
642
+ prev = this.addCornerWithDirection(prev, RectClip64.headingClockwise(prev, loc2));
643
+ prev = loc2;
644
+ }
645
+ loc = prev;
646
+ }
647
+ if (loc !== firstCross) {
648
+ this.addCornerWithDirection(loc, RectClip64.headingClockwise(loc, firstCross));
649
+ }
650
+ }
651
+ }
652
+
653
+ public execute(paths: Paths64): Paths64 {
654
+ const result: Paths64 = [];
655
+ if (Rect64Utils.isEmpty(this.rect)) return result;
656
+
657
+ for (const path of paths) {
658
+ if (path.length < 3) continue;
659
+ this.pathBounds = InternalClipper.getBounds(path);
660
+ if (!Rect64Utils.intersects(this.rect, this.pathBounds)) {
661
+ continue; // the path must be completely outside rect
662
+ }
663
+ if (Rect64Utils.containsRect(this.rect, this.pathBounds)) {
664
+ // the path must be completely inside rect
665
+ result.push(path);
666
+ continue;
667
+ }
668
+
669
+ this.executeInternal(path);
670
+ this.checkEdges();
671
+ for (let i = 0; i < 4; ++i) {
672
+ this.tidyEdgePair(i, this.edges[i * 2], this.edges[i * 2 + 1]);
673
+ }
674
+
675
+ for (const op of this.results) {
676
+ const tmp = this.getPath(op);
677
+ if (tmp.length > 0) result.push(tmp);
678
+ }
679
+
680
+ //clean up after every loop
681
+ this.results.length = 0;
682
+ for (let i = 0; i < 8; i++) {
683
+ this.edges[i].length = 0;
684
+ }
685
+ }
686
+ return result;
687
+ }
688
+
689
+ private checkEdges(): void {
690
+ for (let i = 0; i < this.results.length; i++) {
691
+ let op = this.results[i];
692
+ let op2 = op;
693
+ if (op === null) continue;
694
+
695
+ do {
696
+ if (InternalClipper.isCollinear(op2!.prev!.pt, op2!.pt, op2!.next!.pt)) {
697
+ if (op2 === op) {
698
+ op2 = RectClip64.unlinkOpBack(op2!);
699
+ if (op2 === null) break;
700
+ op = op2.prev;
701
+ } else {
702
+ op2 = RectClip64.unlinkOpBack(op2!);
703
+ if (op2 === null) break;
704
+ }
705
+ } else {
706
+ op2 = op2!.next;
707
+ }
708
+ } while (op2 !== op);
709
+
710
+ if (op2 === null) {
711
+ this.results[i] = null;
712
+ continue;
713
+ }
714
+ this.results[i] = op2; // safety first
715
+
716
+ let edgeSet1 = RectClip64.getEdgesForPt(op!.prev!.pt, this.rect);
717
+ op2 = op!;
718
+ do {
719
+ const edgeSet2 = RectClip64.getEdgesForPt(op2!.pt, this.rect);
720
+ if (edgeSet2 !== 0 && op2!.edge === null) {
721
+ const combinedSet = (edgeSet1 & edgeSet2);
722
+ for (let j = 0; j < 4; ++j) {
723
+ if ((combinedSet & (1 << j)) === 0) continue;
724
+ if (RectClip64.isHeadingClockwise(op2!.prev!.pt, op2!.pt, j)) {
725
+ RectClip64.addToEdge(this.edges[j * 2], op2!);
726
+ } else {
727
+ RectClip64.addToEdge(this.edges[j * 2 + 1], op2!);
728
+ }
729
+ }
730
+ }
731
+ edgeSet1 = edgeSet2;
732
+ op2 = op2!.next;
733
+ } while (op2 !== op);
734
+ }
735
+ }
736
+
737
+ private tidyEdgePair(idx: number, cw: (OutPt2 | null)[], ccw: (OutPt2 | null)[]): void {
738
+ if (ccw.length === 0) return;
739
+ const isHorz = ((idx === 1) || (idx === 3));
740
+ const cwIsTowardLarger = ((idx === 1) || (idx === 2));
741
+ let i = 0, j = 0;
742
+
743
+ while (i < cw.length) {
744
+ let p1 = cw[i];
745
+ if (p1 === null || p1.next === p1.prev) {
746
+ cw[i++] = null;
747
+ j = 0;
748
+ continue;
749
+ }
750
+
751
+ const jLim = ccw.length;
752
+ while (j < jLim && (ccw[j] === null || ccw[j]!.next === ccw[j]!.prev)) ++j;
753
+
754
+ if (j === jLim) {
755
+ ++i;
756
+ j = 0;
757
+ continue;
758
+ }
759
+
760
+ let p2: OutPt2 | null;
761
+ let p1a: OutPt2 | null;
762
+ let p2a: OutPt2 | null;
763
+
764
+ if (cwIsTowardLarger) {
765
+ // p1 >>>> p1a;
766
+ // p2 <<<< p2a;
767
+ p1 = cw[i]!.prev!;
768
+ p1a = cw[i];
769
+ p2 = ccw[j];
770
+ p2a = ccw[j]!.prev!;
771
+ } else {
772
+ // p1 <<<< p1a;
773
+ // p2 >>>> p2a;
774
+ p1 = cw[i];
775
+ p1a = cw[i]!.prev!;
776
+ p2 = ccw[j]!.prev!;
777
+ p2a = ccw[j];
778
+ }
779
+
780
+ if ((isHorz && !RectClip64.hasHorzOverlap(p1!.pt, p1a!.pt, p2!.pt, p2a!.pt)) ||
781
+ (!isHorz && !RectClip64.hasVertOverlap(p1!.pt, p1a!.pt, p2!.pt, p2a!.pt))) {
782
+ ++j;
783
+ continue;
784
+ }
785
+
786
+ // to get here we're either splitting or rejoining
787
+ const isRejoining = cw[i]!.ownerIdx !== ccw[j]!.ownerIdx;
788
+
789
+ if (isRejoining) {
790
+ this.results[p2!.ownerIdx] = null;
791
+ RectClip64.setNewOwner(p2!, p1!.ownerIdx);
792
+ }
793
+
794
+ // do the split or re-join
795
+ if (cwIsTowardLarger) {
796
+ // p1 >> | >> p1a;
797
+ // p2 << | << p2a;
798
+ p1!.next = p2;
799
+ p2!.prev = p1;
800
+ p1a!.prev = p2a;
801
+ p2a!.next = p1a;
802
+ } else {
803
+ // p1 << | << p1a;
804
+ // p2 >> | >> p2a;
805
+ p1!.prev = p2;
806
+ p2!.next = p1;
807
+ p1a!.next = p2a;
808
+ p2a!.prev = p1a;
809
+ }
810
+
811
+ if (!isRejoining) {
812
+ const newIdx = this.results.length;
813
+ this.results.push(p1a);
814
+ RectClip64.setNewOwner(p1a!, newIdx);
815
+ }
816
+
817
+ let op: OutPt2 | null;
818
+ let op2: OutPt2 | null;
819
+ if (cwIsTowardLarger) {
820
+ op = p2;
821
+ op2 = p1a;
822
+ } else {
823
+ op = p1;
824
+ op2 = p2a;
825
+ }
826
+ this.results[op!.ownerIdx] = op;
827
+ this.results[op2!.ownerIdx] = op2;
828
+
829
+ // and now lots of work to get ready for the next loop
830
+ let opIsLarger: boolean, op2IsLarger: boolean;
831
+ if (isHorz) { // X
832
+ opIsLarger = op!.pt.x > op!.prev!.pt.x;
833
+ op2IsLarger = op2!.pt.x > op2!.prev!.pt.x;
834
+ } else { // Y
835
+ opIsLarger = op!.pt.y > op!.prev!.pt.y;
836
+ op2IsLarger = op2!.pt.y > op2!.prev!.pt.y;
837
+ }
838
+
839
+ if ((op!.next === op!.prev) || Point64Utils.equals(op!.pt, op!.prev!.pt)) {
840
+ if (op2IsLarger === cwIsTowardLarger) {
841
+ cw[i] = op2;
842
+ ccw[j++] = null;
843
+ } else {
844
+ ccw[j] = op2;
845
+ cw[i++] = null;
846
+ }
847
+ } else if ((op2!.next === op2!.prev) || Point64Utils.equals(op2!.pt, op2!.prev!.pt)) {
848
+ if (opIsLarger === cwIsTowardLarger) {
849
+ cw[i] = op;
850
+ ccw[j++] = null;
851
+ } else {
852
+ ccw[j] = op;
853
+ cw[i++] = null;
854
+ }
855
+ } else if (opIsLarger === op2IsLarger) {
856
+ if (opIsLarger === cwIsTowardLarger) {
857
+ cw[i] = op;
858
+ RectClip64.uncoupleEdge(op2!);
859
+ RectClip64.addToEdge(cw, op2!);
860
+ ccw[j++] = null;
861
+ } else {
862
+ cw[i++] = null;
863
+ ccw[j] = op2;
864
+ RectClip64.uncoupleEdge(op!);
865
+ RectClip64.addToEdge(ccw, op!);
866
+ j = 0;
867
+ }
868
+ } else {
869
+ if (opIsLarger === cwIsTowardLarger) {
870
+ cw[i] = op;
871
+ } else {
872
+ ccw[j] = op;
873
+ }
874
+ if (op2IsLarger === cwIsTowardLarger) {
875
+ cw[i] = op2;
876
+ } else {
877
+ ccw[j] = op2;
878
+ }
879
+ }
880
+ }
881
+ }
882
+
883
+ private getPath(op: OutPt2 | null): Path64 {
884
+ const result: Path64 = [];
885
+ if (op === null || op.prev === op.next) return result;
886
+
887
+ let op2 = op.next;
888
+ while (op2 !== null && op2 !== op) {
889
+ if (InternalClipper.isCollinear(op2.prev!.pt, op2.pt, op2.next!.pt)) {
890
+ op = op2.prev;
891
+ op2 = RectClip64.unlinkOp(op2);
892
+ } else {
893
+ op2 = op2.next;
894
+ }
895
+ }
896
+ if (op2 === null) return [];
897
+
898
+ result.push(op!.pt);
899
+ op2 = op!.next;
900
+ while (op2 !== op) {
901
+ result.push(op2!.pt);
902
+ op2 = op2!.next;
903
+ }
904
+ return result;
905
+ }
906
+ }
907
+
908
+ export class RectClipLines64 extends RectClip64 {
909
+ constructor(rect: Rect64) {
910
+ super(rect);
911
+ }
912
+
913
+ public execute(paths: Paths64): Paths64 {
914
+ const result: Paths64 = [];
915
+ if (Rect64Utils.isEmpty(this.rect)) return result;
916
+
917
+ for (const path of paths) {
918
+ if (path.length < 2) continue;
919
+ this.pathBounds = InternalClipper.getBounds(path);
920
+ if (!Rect64Utils.intersects(this.rect, this.pathBounds)) {
921
+ continue; // the path must be completely outside rect
922
+ }
923
+ // Apart from that, we can't be sure whether the path
924
+ // is completely outside or completed inside or intersects
925
+ // rect, simply by comparing path bounds with rect.
926
+ this.executeInternalLines(path);
927
+
928
+ for (const op of this.results) {
929
+ const tmp = this.getPathLines(op);
930
+ if (tmp.length > 0) result.push(tmp);
931
+ }
932
+
933
+ //clean up after every loop
934
+ this.results.length = 0;
935
+ for (let i = 0; i < 8; i++) {
936
+ this.edges[i].length = 0;
937
+ }
938
+ }
939
+ return result;
940
+ }
941
+
942
+ private getPathLines(op: OutPt2 | null): Path64 {
943
+ const result: Path64 = [];
944
+ if (op === null || op === op.next) return result;
945
+
946
+ op = op.next; // starting at path beginning
947
+ result.push(op!.pt);
948
+ let op2 = op!.next!;
949
+ while (op2 !== op) {
950
+ result.push(op2.pt);
951
+ op2 = op2.next!;
952
+ }
953
+ return result;
954
+ }
955
+
956
+ private executeInternalLines(path: Path64): void {
957
+ this.results.length = 0;
958
+ if (path.length < 2 || Rect64Utils.isEmpty(this.rect)) return;
959
+
960
+ let prev = Location.inside;
961
+ let i = 1;
962
+ const highI = path.length - 1;
963
+
964
+ const firstLocResult = RectClip64.getLocation(this.rect, path[0]);
965
+ let loc = firstLocResult.location;
966
+
967
+ if (firstLocResult.isOnRect) {
968
+ while (i <= highI) {
969
+ const prevLocResult = RectClip64.getLocation(this.rect, path[i]);
970
+ if (!prevLocResult.isOnRect) {
971
+ prev = prevLocResult.location;
972
+ break;
973
+ }
974
+ i++;
975
+ }
976
+ if (i > highI) {
977
+ for (const pt of path) {
978
+ this.add(pt);
979
+ }
980
+ return;
981
+ }
982
+ if (prev === Location.inside) loc = Location.inside;
983
+ i = 1;
984
+ }
985
+ if (loc === Location.inside) this.add(path[0]);
986
+
987
+ ///////////////////////////////////////////////////
988
+ while (i <= highI) {
989
+ prev = loc;
990
+ const nextLocResult = this.getNextLocation(path, loc, i, highI);
991
+ loc = nextLocResult.location;
992
+ i = nextLocResult.index;
993
+
994
+ if (i > highI) break;
995
+ const prevPt = path[i - 1];
996
+
997
+ let crossingLoc = loc;
998
+ const intersectionResult = RectClip64.getIntersection(this.rectPath, path[i], prevPt, crossingLoc);
999
+ if (!intersectionResult.intersects) {
1000
+ // ie remaining outside (& crossingLoc still == loc)
1001
+ ++i;
1002
+ continue;
1003
+ }
1004
+
1005
+ const ip = intersectionResult.point;
1006
+ crossingLoc = intersectionResult.newLocation;
1007
+
1008
+ ////////////////////////////////////////////////////
1009
+ // we must be crossing the rect boundary to get here
1010
+ ////////////////////////////////////////////////////
1011
+
1012
+ if (loc === Location.inside) { // path must be entering rect
1013
+ this.add(ip, true);
1014
+ } else if (prev !== Location.inside) {
1015
+ // passing right through rect. 'ip' here will be the second
1016
+ // intersect pt but we'll also need the first intersect pt (ip2)
1017
+ crossingLoc = prev;
1018
+ const intersection2Result = RectClip64.getIntersection(this.rectPath, prevPt, path[i], crossingLoc);
1019
+ const ip2 = intersection2Result.point;
1020
+ this.add(ip2, true);
1021
+ this.add(ip);
1022
+ } else { // path must be exiting rect
1023
+ this.add(ip);
1024
+ }
1025
+ } //while i <= highI
1026
+ ///////////////////////////////////////////////////
1027
+ }
1028
+ }