-
Notifications
You must be signed in to change notification settings - Fork 210
/
PolygonOps.ts
1709 lines (1659 loc) · 75.1 KB
/
PolygonOps.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module CartesianGeometry
*/
import { assert } from "@itwin/core-bentley";
import { CurveLocationDetailPair } from "../curve/CurveLocationDetail";
import { AxisOrder, Geometry, PlaneAltitudeEvaluator, PolygonLocation } from "../Geometry";
import { Matrix4d } from "../geometry4d/Matrix4d";
import { Point4d } from "../geometry4d/Point4d";
import { XYParitySearchContext } from "../topology/XYParitySearchContext";
import { FrameBuilder } from "./FrameBuilder";
import { GrowableXYZArray } from "./GrowableXYZArray";
import { IndexedReadWriteXYZCollection, IndexedXYZCollection } from "./IndexedXYZCollection";
import { Matrix3d } from "./Matrix3d";
import { Plane3dByOriginAndUnitNormal } from "./Plane3dByOriginAndUnitNormal";
import { Point2d, Vector2d } from "./Point2dVector2d";
import { Point3dArrayCarrier } from "./Point3dArrayCarrier";
import { Point3d, Vector3d } from "./Point3dVector3d";
import { PolylineOps } from "./PolylineOps";
import { Range1d, Range3d } from "./Range";
import { Ray3d } from "./Ray3d";
import { SortablePolygon } from "./SortablePolygon";
import { XAndY, XYAndZ } from "./XYZProps";
/**
* Carries data about a point in the plane of a polygon.
* @public
*/
export class PolygonLocationDetail {
/** The coordinates of the point p. */
public point: Point3d;
/** Application-specific number */
public a: number;
/** Application-specific vector */
public v: Vector3d;
/** A number that classifies the point's location with respect to the polygon. */
public code: PolygonLocation;
/** Index of the polygon vertex at the base of the edge closest to p. */
public closestEdgeIndex: number;
/** The parameter along the closest edge of the projection of p. */
public closestEdgeParam: number;
private constructor() {
this.point = new Point3d();
this.a = 0.0;
this.v = new Vector3d();
this.code = PolygonLocation.Unknown;
this.closestEdgeIndex = 0;
this.closestEdgeParam = 0.0;
}
/** Invalidate this detail. */
public invalidate() {
this.point.setZero();
this.a = 0.0;
this.v.setZero();
this.code = PolygonLocation.Unknown;
this.closestEdgeIndex = 0;
this.closestEdgeParam = 0.0;
}
/** Create an invalid detail.
* @param result optional pre-allocated object to fill and return
*/
public static create(result?: PolygonLocationDetail): PolygonLocationDetail {
if (undefined === result)
result = new PolygonLocationDetail();
else
result.invalidate();
return result;
}
/** Set the instance contents from the other detail.
* @param other detail to clone
*/
public copyContentsFrom(other: PolygonLocationDetail) {
this.point.setFrom(other.point);
this.a = other.a;
this.v.setFrom(other.v);
this.code = other.code;
this.closestEdgeIndex = other.closestEdgeIndex;
this.closestEdgeParam = other.closestEdgeParam;
}
/** Whether this detail is valid. */
public get isValid(): boolean {
return this.code !== PolygonLocation.Unknown;
}
/** Whether this instance specifies a location inside or on the polygon. */
public get isInsideOrOn(): boolean {
return this.code === PolygonLocation.InsidePolygon ||
this.code === PolygonLocation.OnPolygonVertex || this.code === PolygonLocation.OnPolygonEdgeInterior ||
this.code === PolygonLocation.InsidePolygonProjectsToVertex || this.code === PolygonLocation.InsidePolygonProjectsToEdgeInterior;
}
/**
* Set point, index, and fraction for an "at vertex" or "along edge" PolygonLocationDetail.
* * Point is not captured; its coordinates are copied.
*/
public static createAtVertexOrEdge(point: Point3d, index: number, fraction: number = 0): PolygonLocationDetail {
const detail = new PolygonLocationDetail();
detail.point.setFrom(point);
detail.closestEdgeIndex = index;
detail.closestEdgeParam = fraction;
fraction = Geometry.clamp(fraction, 0, 1);
detail.code = (fraction > 0 && fraction < 1) ? PolygonLocation.OnPolygonEdgeInterior : PolygonLocation.OnPolygonVertex;
return detail;
}
}
/**
* A pair of PolygonLocationDetail.
* @public
*/
export class PolygonLocationDetailPair {
/** The first of the two details. */
public detailA: PolygonLocationDetail;
/** The second of the two details. */
public detailB: PolygonLocationDetail;
/** Constructor, captures inputs */
private constructor(detailA?: PolygonLocationDetail, detailB?: PolygonLocationDetail) {
this.detailA = detailA ? detailA : PolygonLocationDetail.create();
this.detailB = detailB ? detailB : PolygonLocationDetail.create();
}
/** Create an instance by capturing inputs */
public static create(detailA: PolygonLocationDetail, detailB: PolygonLocationDetail, result?: PolygonLocationDetailPair): PolygonLocationDetailPair {
if (!result)
return new PolygonLocationDetailPair(detailA, detailB);
result.detailA = detailA;
result.detailB = detailB;
return result;
}
/** Make a deep copy of this PolygonLocationDetailPair */
public clone(result?: PolygonLocationDetailPair): PolygonLocationDetailPair {
result = result ? result : new PolygonLocationDetailPair();
result.detailA.copyContentsFrom(this.detailA);
result.detailB.copyContentsFrom(this.detailB);
return result;
}
/** Swap the details of A, B */
public swapDetails() {
const q = this.detailA;
this.detailA = this.detailB;
this.detailB = q;
}
}
/**
* Carrier for a loop extracted from clip operation, annotated for sorting
* @internal
*/
export class CutLoop {
/* All points of the loop */
public xyz: GrowableXYZArray;
/* ray within point of "on" edge */
public edge?: Ray3d;
public sortCoordinate0: number;
public sortCoordinate1: number;
public sortDelta: number;
public isNotch: boolean;
public constructor(xyz: GrowableXYZArray) {
this.xyz = xyz;
this.edge = undefined;
this.sortCoordinate0 = this.sortCoordinate1 = 0;
this.sortDelta = 0;
this.isNotch = false;
}
/**
* Create a `CutLoop` structure annotated with the vector from last point to first.
* @param xyz coordinates to capture
*/
public static createCaptureWithReturnEdge(xyz: GrowableXYZArray): CutLoop {
const result = new CutLoop(xyz);
if (xyz.length >= 2)
result.edge = Ray3d.createStartEnd(xyz.front()!, xyz.back()!);
return result;
}
/**
* Set up coordinates for sort steps:
* * Make `sortCoordinate0` and `sortCoordinate` the (algebraically sorted) start and end fractions along the ray
* * Make `sortDelta` the oriented difference of those two
* * Hence sorting on the coordinates puts loops in left-to-right order by the their edge vector leftmost point.
*/
public setSortCoordinates(ray: Ray3d) {
this.sortDelta = this.edge!.direction.dotProduct(ray.direction);
const a = ray.dotProductToPoint(this.edge!.origin);
if (this.sortDelta >= 0) {
this.sortCoordinate0 = a;
this.sortCoordinate1 = a + this.sortDelta;
} else {
this.sortCoordinate0 = a + this.sortDelta; // and sortDelta is negative !!!
this.sortCoordinate1 = a;
}
}
/** Return
* * 0 if other sort limits are not strictly contained in this.
* * 1 if other sort limits are strictly contained with same direction
* * -1 if other sort limits are strictly contained in opposite direction.
*/
public containsSortLimits(other: CutLoop): number {
if (other.sortCoordinate0 >= this.sortCoordinate1
|| other.sortCoordinate0 <= this.sortCoordinate0
|| other.sortCoordinate1 <= this.sortCoordinate0
|| other.sortCoordinate1 >= this.sortCoordinate1)
return 0;
return this.sortDelta * other.sortDelta > 0 ? 1 : -1;
}
/**
* * push coordinates from other onto this
* * reset this.sortCoordinate0 to other.sortCoordinate1
* @param other new coordinates
*/
public absorb(other: CutLoop) {
this.xyz.pushFromGrowableXYZArray(other.xyz);
this.sortCoordinate0 = other.sortCoordinate1;
}
/** Comparison function for system sort function applied to an array of CutLoop .... */
public static sortFunction(loopA: CutLoop, loopB: CutLoop): number {
const q = loopA.sortCoordinate0 - loopB.sortCoordinate0;
return q > 0 ? 1 : -1;
}
/** Return first point coordinates.
* * For type checking, assume array is not empty.
*/
public front(result?: Point3d): Point3d { return this.xyz.front(result)!; }
/** Return last point coordinates.
* * For type checking, assume array is not empty.
*/
public back(result?: Point3d): Point3d { return this.xyz.back(result)!; }
}
/**
* Context to hold an array of input loops and apply sort logic.
* * This is used when a non-convex face is clipped by a plane
* * Simple convex clip logic in this case generates double-back edges that need to be eliminated.
* * This class manages the elimination.
* * Usage pattern is:
* @internal
*/
export class CutLoopMergeContext {
/** Array (filled by user code) of loops being sorted. Contents are subject to being changed during sort. */
public inputLoops: CutLoop[];
/** Array (filled by sortAndMergeLoops) of reorganized loops. */
public outputLoops: CutLoop[];
// Initialize with empty loop arrays.
public constructor() {
this.inputLoops = [];
this.outputLoops = [];
}
/**
* * Search all start and end points for the one most distant from point0.
*/
private mostDistantPoint(point0: Point3d, workPoint: Point3d, resultPoint: Point3d) {
let dMax = -1.0;
resultPoint.setZero();
let d;
for (const loop of this.inputLoops) {
loop.front(workPoint);
d = workPoint.distanceSquared(point0);
if (d > dMax) {
dMax = d;
resultPoint.setFromPoint3d(workPoint);
}
loop.back(workPoint);
d = workPoint.distanceSquared(point0);
if (d > dMax) {
dMax = d;
resultPoint.setFromPoint3d(workPoint);
}
}
}
/**
* * Find a long (probably longest) edge through start and end points of inputs.
* * Setup sortCoordinate0 and sortCoordinate1 along that edge for each loop
* * sort all inputLoop members by sortCoordinate0.
*/
private sortInputs() {
if (this.inputLoops.length > 0 && this.inputLoops[0].xyz.length > 0) {
const point0 = this.inputLoops[0].xyz.front()!;
const workPoint = Point3d.create();
const point1 = Point3d.create();
// point0 could be in the middle. Find the most distant point ...
this.mostDistantPoint(point0, workPoint, point1);
// And again from point1 to get to the other extreme . .
this.mostDistantPoint(point1, workPoint, point0);
const sortRay = Ray3d.createStartEnd(point0, point1);
sortRay.direction.normalizeInPlace();
for (const loop of this.inputLoops)
loop.setSortCoordinates(sortRay);
this.inputLoops.sort((loopA, loopB) => CutLoop.sortFunction(loopA, loopB));
}
}
/**
* * sort all input loops by coordinate along the cut edge
* * sweep left to right, using start and end coordinates to decide if loops are outer or hole, and combine holes into their containing outer loops.
*/
public sortAndMergeLoops() {
this.sortInputs();
const inputs = this.inputLoops;
const outputs = this.outputLoops;
const stack = [];
outputs.length = 0;
for (const candidate of inputs) {
candidate.isNotch = false;
// candidate must be either (a) absorbed in to of stack or (b) pushed onto stack.
// If pushed, must have indication of natch state.
for (; stack.length > 0;) {
const topOfStack = stack[stack.length - 1];
const containment = topOfStack.containsSortLimits(candidate);
if (containment === 0) {
if (!topOfStack.isNotch)
outputs.push(topOfStack);
stack.pop();
continue; // a larger topOfStack may have appeared !
candidate.isNotch = false;
} else if (containment === 1) {
candidate.isNotch = false;
break;
} else {
topOfStack.absorb(candidate);
candidate.isNotch = true;
break;
}
}
stack.push(candidate);
}
// Anything on stack must be complete ...
for (const p of stack) {
if (!p.isNotch)
outputs.push(p);
}
}
}
/**
* Various static methods to perform computations on an array of points interpreted as a polygon.
* @public
*/
export class PolygonOps {
/** Sum areas of triangles from points[0] to each far edge.
* * Consider triangles from points[0] to each edge.
* * Sum the absolute areas (without regard to orientation) all these triangles.
* @returns sum of absolute triangle areas.
*/
public static sumTriangleAreas(points: Point3d[] | GrowableXYZArray): number {
let s = 0;
const n = points.length;
if (Array.isArray(points)) {
if (n >= 3) {
const origin = points[0];
const vector0 = origin.vectorTo(points[1]);
let vector1 = Vector3d.create();
// This will work with or without closure edge. If closure is given, the last vector is 000.
for (let i = 2; i < n; i++) {
vector1 = origin.vectorTo(points[i], vector1);
s += vector0.crossProductMagnitude(vector1);
vector0.setFrom(vector1);
}
}
return s * 0.5;
}
const crossVector = Vector3d.create();
for (let i = 2; i < n; i++) {
points.crossProductIndexIndexIndex(0, i - 1, i, crossVector);
s += crossVector.magnitude();
}
return s * 0.5;
}
/** Sum areas of triangles from points[0] to each far edge, as viewed with upVector pointing up.
* * Consider triangles from points[0] to each edge.
* * Sum the areas perpendicular to the upVector.
* * If the upVector is near-zero length, a simple z vector is used.
* @returns sum of triangle areas.
*/
public static sumTriangleAreasPerpendicularToUpVector(points: Point3d[] | GrowableXYZArray, upVector: Vector3d): number {
let scale = upVector.magnitude();
if (scale < Geometry.smallMetricDistance) {
upVector = Vector3d.create(0, 0, 1);
scale = 1.0;
}
let s = 0;
const n = points.length;
if (Array.isArray(points)) {
if (n >= 3) {
const origin = points[0];
const vector0 = origin.vectorTo(points[1]);
let vector1 = Vector3d.create();
// This will work with or without closure edge. If closure is given, the last vector is 000.
for (let i = 2; i < n; i++) {
vector1 = origin.vectorTo(points[i], vector1);
s += vector0.tripleProduct(vector1, upVector);
vector0.setFrom(vector1);
}
}
return s * 0.5 / scale;
}
const crossVector = Vector3d.create();
for (let i = 2; i < n; i++) {
points.crossProductIndexIndexIndex(0, i - 1, i, crossVector);
s += crossVector.dotProduct(upVector);
}
return s * 0.5 / scale;
}
/** Sum areas of triangles from points[0] to each far edge.
* * Consider triangles from points[0] to each edge.
* * Sum the signed areas of all these triangles. (An area can be negative at a concave corner.)
* @returns sum of signed triangle areas.
*/
public static sumTriangleAreasXY(points: Point3d[]): number {
let s = 0.0;
const n = points.length;
if (n >= 3) {
const origin = points[0];
const vector0 = origin.vectorTo(points[1]);
let vector1 = Vector3d.create();
// This will work with or without closure edge. If closure is given, the last vector is 000.
for (let i = 2; i < n; i++) {
vector1 = origin.vectorTo(points[i], vector1);
s += vector0.crossProductXY(vector1);
vector0.setFrom(vector1);
}
}
s *= 0.5;
return s;
}
/** These values are the integrated area moment products [xx,xy,xz, x]
* for a right triangle in the first quadrant at the origin -- (0,0),(1,0),(0,1)
*/
private static readonly _triangleMomentWeights = Matrix4d.createRowValues(2.0 / 24.0, 1.0 / 24.0, 0, 4.0 / 24.0, 1.0 / 24.0, 2.0 / 24.0, 0, 4.0 / 24.0, 0, 0, 0, 0, 4.0 / 24.0, 4.0 / 24.0, 0, 12.0 / 24.0);
/** These values are the integrated volume moment products [xx,xy,xz, x, yx,yy,yz,y, zx,zy,zz,z,x,y,z,1]
* for a tetrahedron in the first quadrant at the origin -- (0,00),(1,0,0),(0,1,0),(0,0,1)
*/
private static readonly _tetrahedralMomentWeights = Matrix4d.createRowValues(
1.0 / 60.0, 1.0 / 120, 1.0 / 120, 1.0 / 24.0,
1.0 / 120, 1.0 / 60.0, 1.0 / 120, 1.0 / 24.0,
1.0 / 120, 1.0 / 120, 1.0 / 60.0, 1.0 / 24.0,
1.0 / 24.0, 1.0 / 24.0, 1.0 / 24.0, 1.0 / 6.0);
// statics for shared reuse.
// many methods use these.
// only use them in "leaf" methods that are certain not to call other users . . .
private static _vector0 = Vector3d.create();
private static _vector1 = Vector3d.create();
private static _vector2 = Vector3d.create();
private static _vectorOrigin = Vector3d.create();
private static _normal = Vector3d.create();
private static _matrixA = Matrix4d.createIdentity();
private static _matrixB = Matrix4d.createIdentity();
private static _matrixC = Matrix4d.createIdentity();
/** return a vector which is perpendicular to the polygon and has magnitude equal to the polygon area. */
public static areaNormalGo(points: IndexedXYZCollection, result?: Vector3d): Vector3d | undefined {
if (!result)
result = new Vector3d();
else
result.setZero();
const n = points.length;
if (n === 3) {
points.crossProductIndexIndexIndex(0, 1, 2, result);
} else if (n > 3) {
// This will work with or without closure edge. If closure is given, the last vector is 000.
for (let i = 2; i < n; i++) {
points.accumulateCrossProductIndexIndexIndex(0, i - 1, i, result);
}
}
// ALL BRANCHES SUM FULL CROSS PRODUCTS AND EXPECT SCALE HERE
result.scaleInPlace(0.5);
return result.isZero ? undefined : result;
}
/** return a vector which is perpendicular to the polygon and has magnitude equal to the polygon area. */
public static areaNormal(points: Point3d[], result?: Vector3d): Vector3d {
if (!result)
result = Vector3d.create();
PolygonOps.areaNormalGo(new Point3dArrayCarrier(points), result);
return result;
}
/** return the area of the polygon.
* * This assumes the polygon is planar
* * This does NOT assume the polygon is on the xy plane.
*/
public static area(points: Point3d[]): number {
return PolygonOps.areaNormal(points).magnitude();
}
/** return the projected XY area of the polygon. */
public static areaXY(points: Point3d[] | IndexedXYZCollection): number {
let area = 0.0;
if (points instanceof IndexedXYZCollection) {
if (points.length > 2) {
const x0 = points.getXAtUncheckedPointIndex(0);
const y0 = points.getYAtUncheckedPointIndex(0);
let u1 = points.getXAtUncheckedPointIndex(1) - x0;
let v1 = points.getYAtUncheckedPointIndex(1) - y0;
let u2, v2;
for (let i = 2; i + 1 < points.length; i++, u1 = u2, v1 = v2) {
u2 = points.getXAtUncheckedPointIndex(i) - x0;
v2 = points.getYAtUncheckedPointIndex(i) - y0;
area += Geometry.crossProductXYXY(u1, v1, u2, v2);
}
}
} else {
for (let i = 1; i + 1 < points.length; i++)
area += points[0].crossProductToPointsXY(points[i], points[i + 1]);
}
return 0.5 * area;
}
/** Sum the areaXY () values for multiple polygons */
public static sumAreaXY(polygons: Point3d[][]): number {
let s = 0.0;
for (const p of polygons)
s += this.areaXY(p);
return s;
}
/**
* Return a Ray3d with (assuming the polygon is planar and not self-intersecting)
* * origin at the centroid of the (3D) polygon
* * normal is a unit vector perpendicular to the plane
* * 'a' member is the area.
* @param points
*/
public static centroidAreaNormal(points: IndexedXYZCollection | Point3d[]): Ray3d | undefined {
if (Array.isArray(points)) {
const carrier = new Point3dArrayCarrier(points);
return this.centroidAreaNormal(carrier);
}
const n = points.length;
if (n === 3) {
const normal = points.crossProductIndexIndexIndex(0, 1, 2)!;
const a = 0.5 * normal.magnitude();
const centroid = points.getPoint3dAtCheckedPointIndex(0)!;
points.accumulateScaledXYZ(1, 1.0, centroid);
points.accumulateScaledXYZ(2, 1.0, centroid);
centroid.scaleInPlace(1.0 / 3.0);
const result = Ray3d.createCapture(centroid, normal);
if (result.tryNormalizeInPlaceWithAreaWeight(a))
return result;
return undefined;
}
if (n >= 3) {
const areaNormal = Vector3d.createZero();
// This will work with or without closure edge. If closure is given, the last vector is 000.
for (let i = 2; i < n; i++) {
points.accumulateCrossProductIndexIndexIndex(0, i - 1, i, areaNormal);
}
areaNormal.normalizeInPlace();
const origin = points.getPoint3dAtCheckedPointIndex(0)!;
const vector0 = Vector3d.create();
const vector1 = Vector3d.create();
points.vectorXYAndZIndex(origin, 1, vector0);
let cross = Vector3d.create();
const centroidSum = Vector3d.createZero();
const normalSum = Vector3d.createZero();
let signedTriangleArea;
// This will work with or without closure edge. If closure is given, the last vector is 000.
for (let i = 2; i < n; i++) {
points.vectorXYAndZIndex(origin, i, vector1);
cross = vector0.crossProduct(vector1, cross);
signedTriangleArea = areaNormal.dotProduct(cross); // well, actually twice the area.
normalSum.addInPlace(cross); // this grows to twice the area
const b = signedTriangleArea / 6.0;
centroidSum.plus2Scaled(vector0, b, vector1, b, centroidSum);
vector0.setFrom(vector1);
}
const area = 0.5 * normalSum.magnitude();
const inverseArea = Geometry.conditionalDivideFraction(1, area);
if (inverseArea !== undefined) {
const result = Ray3d.createCapture(origin.plusScaled(centroidSum, inverseArea), normalSum);
result.tryNormalizeInPlaceWithAreaWeight(area);
return result;
}
}
return undefined;
}
// Has the potential to be combined with centroidAreaNormal for point3d array and Ray3d return listed above...
// Returns undefined if given point array less than 3 or if not safe to divide at any point
/**
* * Return (in caller-allocated centroid) the centroid of the xy polygon.
* * Return (as function value) the area
*/
public static centroidAndAreaXY(points: Point2d[], centroid: Point2d): number | undefined {
let area = 0.0;
centroid.set(0, 0);
if (points.length < 3)
return undefined;
const origin = points[0];
let vectorSum = Vector2d.create(0, 0); // == sum ((U+V)/3) * (U CROSS V)/2 -- but leave out divisions
let areaSum = 0.0; // == sum (U CROSS V) / 2 -- but leave out divisions
for (let i = 1; i + 1 < points.length; i++) {
const vector0 = origin.vectorTo(points[i]);
const vector1 = origin.vectorTo(points[i + 1]);
const tempArea = vector0.crossProduct(vector1);
vectorSum = vectorSum.plus(vector0.plus(vector1).scale(tempArea));
areaSum += tempArea;
}
area = areaSum * 0.5;
const a = Geometry.conditionalDivideFraction(1.0, 6.0 * area);
if (a === undefined) {
centroid.setFrom(origin);
return undefined;
}
centroid.setFrom(origin.plusScaled(vectorSum, a));
return area;
}
/**
* Return a unit normal to the plane of the polygon.
* @param points array of points around the polygon.
* @param result caller-allocated result vector.
* @return true if and only if result has unit length
*/
public static unitNormal(points: IndexedXYZCollection, result: Vector3d): boolean {
result.setZero();
let n = points.length;
if (n > 1 && points.getPoint3dAtUncheckedPointIndex(0).isExactEqual(points.getPoint3dAtUncheckedPointIndex(n - 1)))
--n; // ignore closure point
if (n === 3) {
points.crossProductIndexIndexIndex(0, 1, 2, result);
return result.normalizeInPlace();
}
if (n === 4) {
// cross product of diagonals is more stable than from single of the points . . .
points.vectorIndexIndex(0, 2, PolygonOps._vector0);
points.vectorIndexIndex(1, 3, PolygonOps._vector1);
PolygonOps._vector0.crossProduct(PolygonOps._vector1, result);
return result.normalizeInPlace();
}
// more than 4 points ... no shortcuts ...
PolygonOps.areaNormalGo(points, result);
return result.normalizeInPlace();
}
/** Accumulate to the matrix of area products of a polygon with respect to an origin.
* The polygon is assumed to be planar and non-self-intersecting.
*/
/** Accumulate to the matrix of area products of a polygon with respect to an origin.
* * The polygon is assumed to be planar and non-self-intersecting.
* * Accumulated values are integrals over triangles from point 0 of the polygon to other edges of the polygon.
* * Integral over each triangle is transformed to integrals from the given origin.
* @param points array of points around the polygon. Final closure point is not needed.
* @param origin origin for global accumulation.
* @param moments 4x4 matrix where products are accumulated.
*/
public static addSecondMomentAreaProducts(points: IndexedXYZCollection, origin: Point3d, moments: Matrix4d) {
this.addSecondMomentTransformedProducts(PolygonOps._triangleMomentWeights, points, origin, 2, moments);
}
/** Accumulate to the matrix of volume products of a polygon with respect to an origin.
* * The polygon is assumed to be planar and non-self-intersecting.
* * Accumulated values are integrals over tetrahedra from the origin to triangles on the polygon.
* @param points array of points around the polygon. Final closure point is not needed.
* @param origin origin for tetrahedra
* @param moments 4x4 matrix where products are accumulated.
*/
public static addSecondMomentVolumeProducts(points: IndexedXYZCollection, origin: Point3d, moments: Matrix4d) {
this.addSecondMomentTransformedProducts(PolygonOps._tetrahedralMomentWeights, points, origin, 3, moments);
}
/** Return the matrix of area products of a polygon with respect to an origin.
* The polygon is assumed to be planar and non-self-intersecting.
* * `frameType===2` has xy vectors in the plane of the polygon, plus a unit normal z. (Used for area integrals)
* * `frameType===3` has vectors from origin to 3 points in the triangle. (Used for volume integrals)
*/
private static addSecondMomentTransformedProducts(firstQuadrantMoments: Matrix4d, points: IndexedXYZCollection, origin: Point3d,
frameType: 2 | 3,
moments: Matrix4d) {
const unitNormal = PolygonOps._normal;
if (PolygonOps.unitNormal(points, unitNormal)) {
// The direction of the normal makes the various detJ values positive or negative so that non-convex polygons
// sum correctly.
const vector01 = PolygonOps._vector0;
const vector02 = PolygonOps._vector1;
const vector03 = PolygonOps._vector2;
const placement = PolygonOps._matrixA;
const matrixAB = PolygonOps._matrixB;
const matrixABC = PolygonOps._matrixC;
const vectorOrigin = points.vectorXYAndZIndex(origin, 0, PolygonOps._vectorOrigin)!;
const numPoints = points.length;
let detJ = 0;
for (let i2 = 2; i2 < numPoints; i2++) {
if (frameType === 2) {
points.vectorIndexIndex(0, i2 - 1, vector01);
points.vectorIndexIndex(0, i2, vector02);
detJ = unitNormal.tripleProduct(vector01, vector02);
placement.setOriginAndVectors(vectorOrigin, vector01, vector02, unitNormal);
placement.multiplyMatrixMatrix(firstQuadrantMoments, matrixAB);
matrixAB.multiplyMatrixMatrixTranspose(placement, matrixABC);
moments.addScaledInPlace(matrixABC, detJ);
} else if (frameType === 3) {
points.vectorXYAndZIndex(origin, 0, vector01);
points.vectorXYAndZIndex(origin, i2 - 1, vector02);
points.vectorXYAndZIndex(origin, i2, vector03);
detJ = vector01.tripleProduct(vector02, vector03);
placement.setOriginAndVectors(origin, vector01, vector02, vector03);
placement.multiplyMatrixMatrix(firstQuadrantMoments, matrixAB);
matrixAB.multiplyMatrixMatrixTranspose(placement, matrixABC);
moments.addScaledInPlace(matrixABC, detJ);
}
}
}
}
/** Test the direction of turn at the vertices of the polygon, ignoring z-coordinates.
* * For a polygon without self-intersections and successive colinear edges, this is a convexity and orientation test: all positive is convex and counterclockwise, all negative is convex and clockwise.
* * Beware that a polygon which turns through more than a full turn can cross itself and close, but is not convex.
* @returns 1 if all turns are to the left, -1 if all to the right, and 0 if there are any zero or reverse turns
*/
public static testXYPolygonTurningDirections(points: Point2d[] | Point3d[]): number {
// Reduce count by trailing duplicates; leaves iLast at final index
let numPoint = points.length;
let iLast = numPoint - 1;
while (iLast > 1 && points[iLast].x === points[0].x && points[iLast].y === points[0].y) {
numPoint = iLast--;
}
if (numPoint > 2) {
let vector0 = Point2d.create(points[iLast].x - points[iLast - 1].x, points[iLast].y - points[iLast - 1].y);
const vector1 = Point2d.create(points[0].x - points[iLast].x, points[0].y - points[iLast].y);
const baseArea = vector0.x * vector1.y - vector0.y * vector1.x;
// In a convex polygon, all successive-vector cross products will
// have the same sign as the base area, hence all products will be
// positive.
for (let i1 = 1; i1 < numPoint; i1++) {
vector0 = vector1.clone();
Point2d.create(points[i1].x - points[i1 - 1].x, points[i1].y - points[i1 - 1].y, vector1);
const currArea = vector0.x * vector1.y - vector0.y * vector1.x;
if (currArea * baseArea <= 0.0)
return 0;
}
// Fall out with all signs same as base area
return baseArea > 0.0 ? 1 : -1;
}
return 0;
}
/**
* Determine whether the polygon is convex.
* @param polygon vertices, closure point optional
* @returns whether the polygon is convex.
*/
public static isConvex(polygon: Point3d[] | IndexedXYZCollection): boolean {
if (!(polygon instanceof IndexedXYZCollection))
return this.isConvex(new Point3dArrayCarrier(polygon));
let n = polygon.length;
if (n > 1 && polygon.getPoint3dAtUncheckedPointIndex(0).isExactEqual(polygon.getPoint3dAtUncheckedPointIndex(n - 1)))
--n; // ignore closure point
const normal = Vector3d.create();
if (!this.unitNormal(polygon, normal))
return false;
let positiveArea = 0.0;
let negativeArea = 0.0;
const vecA = this._vector0;
let vecB = Vector3d.createStartEnd(polygon.getPoint3dAtUncheckedPointIndex(n - 1), polygon.getPoint3dAtUncheckedPointIndex(0), this._vector1);
for (let i = 1; i <= n; i++) {
// check turn through vertices i-1,i,i+1
vecA.setFromVector3d(vecB);
vecB = Vector3d.createStartEnd(polygon.getPoint3dAtUncheckedPointIndex(i - 1), polygon.getPoint3dAtUncheckedPointIndex(i % n), vecB);
const signedArea = normal.tripleProduct(vecA, vecB);
if (signedArea >= 0.0)
positiveArea += signedArea;
else
negativeArea += signedArea;
}
return Math.abs(negativeArea) < Geometry.smallMetricDistanceSquared * positiveArea;
}
/**
* Test if point (x,y) is IN, OUT or ON a polygon.
* @return (1) for in, (-1) for OUT, (0) for ON
* @param x x coordinate
* @param y y coordinate
* @param points array of xy coordinates.
*/
public static classifyPointInPolygon(x: number, y: number, points: XAndY[]): number | undefined {
const context = new XYParitySearchContext(x, y);
let i0 = 0;
const n = points.length;
let i1;
let iLast = -1;
// walk to an acceptable start index ...
for (i0 = 0; i0 < n; i0++) {
i1 = i0 + 1;
if (i1 >= n)
i1 = 0;
if (context.tryStartEdge(points[i0].x, points[i0].y, points[i1].x, points[i1].y)) {
iLast = i1;
break;
}
}
if (iLast < 0)
return undefined;
for (let i = 1; i <= n; i++) {
i1 = iLast + i;
if (i1 >= n)
i1 -= n;
if (!context.advance(points[i1].x, points[i1].y))
return context.classifyCounts();
}
return context.classifyCounts();
}
/**
* Test if point (x,y) is IN, OUT or ON a polygon.
* @return (1) for in, (-1) for OUT, (0) for ON
* @param x x coordinate
* @param y y coordinate
* @param points array of xy coordinates.
*/
public static classifyPointInPolygonXY(x: number, y: number, points: IndexedXYZCollection): number | undefined {
const context = new XYParitySearchContext(x, y);
let i0 = 0;
const n = points.length;
let i1;
let iLast = -1;
// walk to an acceptable start index ...
for (i0 = 0; i0 < n; i0++) {
i1 = i0 + 1;
if (i1 >= n)
i1 = 0;
if (context.tryStartEdge(points.getXAtUncheckedPointIndex(i0), points.getYAtUncheckedPointIndex(i0), points.getXAtUncheckedPointIndex(i1), points.getYAtUncheckedPointIndex(i1))) {
iLast = i1;
break;
}
}
if (iLast < 0)
return undefined;
for (let i = 1; i <= n; i++) {
i1 = iLast + i;
if (i1 >= n)
i1 -= n;
if (!context.advance(points.getXAtUncheckedPointIndex(i1), points.getYAtUncheckedPointIndex(i1)))
return context.classifyCounts();
}
return context.classifyCounts();
}
/**
* Reverse loops as necessary to make them all have CCW orientation for given outward normal.
* @param loops
* @param outwardNormal
* @return the number of loops reversed.
*/
public static orientLoopsCCWForOutwardNormalInPlace(loops: IndexedReadWriteXYZCollection | IndexedReadWriteXYZCollection[], outwardNormal: Vector3d): number {
if (loops instanceof IndexedXYZCollection)
return this.orientLoopsCCWForOutwardNormalInPlace([loops], outwardNormal);
const orientations: number[] = [];
const unitNormal = Vector3d.create();
// orient individually ... (no hole analysis)
let numReverse = 0;
for (const loop of loops) {
if (this.unitNormal(loop, unitNormal)) {
const q = unitNormal.dotProduct(outwardNormal);
orientations.push(q);
if (q <= 0.0)
loop.reverseInPlace();
numReverse++;
} else {
orientations.push(0.0);
}
}
return numReverse;
}
/**
* Reverse and reorder loops in the xy-plane for consistency and containment.
* @param loops multiple polygons in any order and orientation, z-coordinates ignored
* @returns array of arrays of polygons that capture the input pointers. In each first level array:
* * The first polygon is an outer loop, oriented counterclockwise.
* * Any subsequent polygons are holes of the outer loop, oriented clockwise.
* @see [[RegionOps.sortOuterAndHoleLoopsXY]]
*/
public static sortOuterAndHoleLoopsXY(loops: IndexedReadWriteXYZCollection[]): IndexedReadWriteXYZCollection[][] {
const loopAndArea: SortablePolygon[] = [];
for (const loop of loops) {
SortablePolygon.pushPolygon(loopAndArea, loop);
}
return SortablePolygon.sortAsArrayOfArrayOfPolygons(loopAndArea);
}
/**
* Exactly like `sortOuterAndHoleLoopsXY` but allows loops in any plane.
* @param loops multiple loops to sort and reverse.
* @param defaultNormal optional normal for the loops, if known
* @see [[sortOuterAndHoleLoopsXY]]
*/
public static sortOuterAndHoleLoops(loops: IndexedReadWriteXYZCollection[], defaultNormal: Vector3d | undefined): IndexedReadWriteXYZCollection[][] {
const localToWorld = FrameBuilder.createRightHandedFrame(defaultNormal, loops);
const worldToLocal = localToWorld?.inverse();
const xyLoops: GrowableXYZArray[] = [];
if (worldToLocal !== undefined) {
// transform into plane so we can ignore z in the sort
for (const loop of loops) {
const xyLoop = new GrowableXYZArray(loop.length);
for (const point of loop.points)
xyLoop.push(worldToLocal.multiplyPoint3d(point));
xyLoops.push(xyLoop);
}
}
const xySortedLoopsArray = PolygonOps.sortOuterAndHoleLoopsXY(xyLoops);
const sortedLoopsArray: GrowableXYZArray[][] = [];
if (localToWorld !== undefined) {
for (const xySortedLoops of xySortedLoopsArray) {
const sortedLoops: GrowableXYZArray[] = [];
for (const xySortedLoop of xySortedLoops) {
const sortedLoop = new GrowableXYZArray(xySortedLoop.length);
for (const point of xySortedLoop.points)
sortedLoop.push(localToWorld.multiplyPoint3d(point));
sortedLoops.push(sortedLoop);
}
sortedLoopsArray.push(sortedLoops);
}
}
return sortedLoopsArray;
}
/** Compute the closest point on the polygon boundary to the given point.
* * Compare to [[closestPoint]].
* @param polygon points of the polygon, closure point optional
* @param testPoint point p to project onto the polygon edges. Works best when p is in the plane of the polygon.
* @param tolerance optional distance tolerance to determine point-vertex and point-edge coincidence.
* @param result optional pre-allocated object to fill and return
* @returns details d of the closest point `d.point`:
* * `d.isValid()` returns true if and only if the polygon is nontrivial.
* * `d.edgeIndex` and `d.edgeParam` specify the location of the closest point.
* * `d.code` classifies the closest point as a vertex (`PolygonLocation.OnPolygonVertex`) or as a point on an edge (`PolygonLocation.OnPolygonEdgeInterior`).
* * `d.a` is the distance from testPoint to the closest point.
* * `d.v` can be used to classify p (if p and polygon are coplanar): if n is the polygon normal then `d.v.dotProduct(n)` is +/-/0 if and only if p is inside/outside/on the polygon.
*/
public static closestPointOnBoundary(polygon: Point3d[] | IndexedXYZCollection, testPoint: Point3d, tolerance: number = Geometry.smallMetricDistance, result?: PolygonLocationDetail): PolygonLocationDetail {
if (!(polygon instanceof IndexedXYZCollection))
return this.closestPointOnBoundary(new Point3dArrayCarrier(polygon), testPoint, tolerance, result);
const distTol2 = tolerance * tolerance;
let numPoints = polygon.length;
while (numPoints > 1) {
if (polygon.distanceSquaredIndexIndex(0, numPoints - 1)! > distTol2)
break;
--numPoints; // ignore closure point
}
result = PolygonLocationDetail.create(result);
if (0 === numPoints)
return result; // invalid
if (1 === numPoints) {
polygon.getPoint3dAtUncheckedPointIndex(0, result.point);
result.a = result.point.distance(testPoint);
result.v.setZero();
result.code = PolygonLocation.OnPolygonVertex;
result.closestEdgeIndex = 0;
result.closestEdgeParam = 0.0;
return result;
}
let iPrev = numPoints - 1;
let minDist2 = Geometry.largeCoordinateResult;
for (let iBase = 0; iBase < numPoints; ++iBase) {
let iNext = iBase + 1;
if (iNext === numPoints)
iNext = 0;
const uDotU = polygon.distanceSquaredIndexIndex(iBase, iNext)!;
if (uDotU <= distTol2)
continue; // ignore trivial polygon edge (keep iPrev)
const vDotV = polygon.distanceSquaredIndexXYAndZ(iBase, testPoint)!;
const uDotV = polygon.dotProductIndexIndexXYAndZ(iBase, iNext, testPoint)!;
const edgeParam = uDotV / uDotU; // param of projection of testPoint onto this edge
if (edgeParam <= 0.0) { // testPoint projects to/before edge start
const distToStart2 = vDotV;
if (distToStart2 <= distTol2) {
// testPoint is at edge start; we are done
polygon.getPoint3dAtUncheckedPointIndex(iBase, result.point);
result.a = Math.sqrt(distToStart2);
result.v.setZero();
result.code = PolygonLocation.OnPolygonVertex;
result.closestEdgeIndex = iBase;
result.closestEdgeParam = 0.0;
return result;
}
if (distToStart2 < minDist2) {
if (polygon.dotProductIndexIndexXYAndZ(iBase, iPrev, testPoint)! <= 0.0) {
// update candidate (to edge start) only if testPoint projected beyond previous edge end
polygon.getPoint3dAtUncheckedPointIndex(iBase, result.point);
result.a = Math.sqrt(distToStart2);
polygon.crossProductIndexIndexIndex(iBase, iPrev, iNext, result.v)!;
result.code = PolygonLocation.OnPolygonVertex;
result.closestEdgeIndex = iBase;
result.closestEdgeParam = 0.0;
minDist2 = distToStart2;
}
}
} else if (edgeParam <= 1.0) { // testPoint projects inside edge, or to edge end
const projDist2 = vDotV - edgeParam * edgeParam * uDotU;
if (projDist2 <= distTol2) {
// testPoint is on edge; we are done
const distToStart2 = vDotV;
if (edgeParam <= 0.5 && distToStart2 <= distTol2) {