/
PolyfaceQuery.ts
1861 lines (1801 loc) · 88.6 KB
/
PolyfaceQuery.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 Polyface
*/
/* eslint-disable @typescript-eslint/naming-convention, no-empty */
import { BagOfCurves, CurveCollection } from "../curve/CurveCollection";
import { CurveLocationDetail } from "../curve/CurveLocationDetail";
import { CurveOps } from "../curve/CurveOps";
import { LinearCurvePrimitive } from "../curve/CurvePrimitive";
import { AnyChain } from "../curve/CurveTypes";
import { MultiChainCollector } from "../curve/internalContexts/MultiChainCollector";
import { LineSegment3d } from "../curve/LineSegment3d";
import { LineString3d } from "../curve/LineString3d";
import { Loop } from "../curve/Loop";
import { StrokeOptions } from "../curve/StrokeOptions";
import { Geometry } from "../Geometry";
import { Angle } from "../geometry3d/Angle";
import { BarycentricTriangle, TriangleLocationDetail } from "../geometry3d/BarycentricTriangle";
import { FrameBuilder } from "../geometry3d/FrameBuilder";
import { GrowableXYZArray } from "../geometry3d/GrowableXYZArray";
import { IndexedXYZCollection } from "../geometry3d/IndexedXYZCollection";
import { Plane3dByOriginAndUnitNormal } from "../geometry3d/Plane3dByOriginAndUnitNormal";
import { Point3dArrayCarrier } from "../geometry3d/Point3dArrayCarrier";
import { Point3d, Vector3d } from "../geometry3d/Point3dVector3d";
import { Point3dArray } from "../geometry3d/PointHelpers";
import { PolygonLocationDetail, PolygonOps } from "../geometry3d/PolygonOps";
import { Range3d } from "../geometry3d/Range";
import { Ray3d } from "../geometry3d/Ray3d";
import { Matrix4d } from "../geometry4d/Matrix4d";
import { MomentData } from "../geometry4d/MomentData";
import { UnionFindContext } from "../numerics/UnionFind";
import { ChainMergeContext } from "../topology/ChainMerge";
import { HalfEdge, HalfEdgeGraph, HalfEdgeMask } from "../topology/Graph";
import { HalfEdgeGraphFromIndexedLoopsContext } from "../topology/HalfEdgeGraphFromIndexedLoopsContext";
import { HalfEdgeGraphSearch, HalfEdgeMaskTester } from "../topology/HalfEdgeGraphSearch";
import { HalfEdgeGraphMerge } from "../topology/Merging";
import { SpacePolygonTriangulation } from "../topology/SpaceTriangulation";
import {
ConvexFacetLocationDetail, FacetIntersectOptions, FacetLocationDetail, NonConvexFacetLocationDetail, TriangularFacetLocationDetail,
} from "./FacetLocationDetail";
import { FacetOrientationFixup } from "./FacetOrientation";
import { IndexedEdgeMatcher, SortableEdge, SortableEdgeCluster } from "./IndexedEdgeMatcher";
import { IndexedPolyfaceSubsetVisitor } from "./IndexedPolyfaceVisitor";
import { BuildAverageNormalsContext } from "./multiclip/BuildAverageNormalsContext";
import { OffsetMeshContext } from "./multiclip/OffsetMeshContext";
import { Range2dSearchInterface } from "./multiclip/Range2dSearchInterface";
import { ClipSweptLineStringContext, EdgeClipData, SweepLineStringToFacetContext } from "./multiclip/SweepLineStringToFacetContext";
import { XYPointBuckets } from "./multiclip/XYPointBuckets";
import { IndexedPolyface, Polyface, PolyfaceVisitor } from "./Polyface";
import { PolyfaceBuilder } from "./PolyfaceBuilder";
import { RangeLengthData } from "./RangeLengthData";
/**
* Options carrier for sweeping linework onto meshes.
* * The create method initializes all options.
* @public
*/
export class SweepLineStringToFacetsOptions {
/** vector "towards the eye"
* * In the common case of sweeping to an XY (e.g. ground or DTM) mesh,
* use the positive Z vector as an up vector.
* * In general case, this is a vector from the mesh towards an eye at infinity.
*/
public vectorToEye: Vector3d;
/** true to collect edges from facets that face towards the eye */
public collectOnForwardFacets: boolean;
/** true to collect facets that are "on the side", i.e. their outward vector is perpendicular to vectorToEye. */
public collectOnSideFacets: boolean;
/** true to collect facets that face away from the eye */
public collectOnRearFacets: boolean;
/** (small) angle to use as tolerance for deciding if a facet is "on the side". Default (if given in degrees) is Geometry.smallAngleDegrees */
public sideAngle: Angle;
/** option to assemble lines into chains */
public assembleChains: boolean;
/** constructor -- captures fully-checked parameters from static create method.
*/
private constructor(vectorToEye: Vector3d, sideAngle: Angle, assembleChains: boolean, collectOnForwardFacets: boolean, collectOnSideFacets: boolean, collectOnRearFacets: boolean) {
this.vectorToEye = vectorToEye;
this.sideAngle = sideAngle;
this.assembleChains = assembleChains;
this.collectOnForwardFacets = collectOnForwardFacets;
this.collectOnSideFacets = collectOnSideFacets;
this.collectOnRearFacets = collectOnRearFacets;
}
/** Create an options structure.
* * Default vectorToEye is positive Z
* * Default sideAngle has radians value Geometry.smallAngleRadians
* * Default assembleChains is true
* * Default collectOnForwardFacets, collectOnSideFacets, collectOnRearFacets are all true.
*/
public static create(vectorToEye?: Vector3d, sideAngle?: Angle, assembleChains?: boolean,
collectOnForwardFacets?: boolean,
collectOnSideFacets?: boolean,
collectOnRearFacets?: boolean) {
return new SweepLineStringToFacetsOptions(
vectorToEye === undefined ? Vector3d.unitZ() : vectorToEye.clone(),
sideAngle === undefined ? Angle.createRadians(Geometry.smallAngleRadians) : sideAngle.clone(),
Geometry.resolveValue(assembleChains, true),
Geometry.resolveValue(collectOnForwardFacets, true),
Geometry.resolveValue(collectOnSideFacets, true),
Geometry.resolveValue(collectOnRearFacets, true),
);
}
/** Return true if all outputs are requested */
public get collectAll() { return this.collectOnForwardFacets === true && this.collectOnRearFacets === true && this.collectOnRearFacets === true; }
/** Decide if the instance flags accept this facet.
* * Facets whose facet normal have positive, zero, or negative dot product with the vectorToEye are forward, side, and rear.
* * Undefined facet normal returns false
*/
public collectFromThisFacetNormal(facetNormal: Vector3d | undefined): boolean {
if (facetNormal === undefined)
return false;
const theta = facetNormal.angleFromPerpendicular(this.vectorToEye);
if (theta.isMagnitudeLessThanOrEqual(this.sideAngle))
return this.collectOnSideFacets;
return facetNormal.dotProduct(this.vectorToEye) > 0 ? this.collectOnForwardFacets : this.collectOnRearFacets;
}
}
/**
* Options carrier for [[fillSimpleHoles]]
* @public
*/
export interface HoleFillOptions {
/** REJECT hole candidates if its boundary chain is longer than this limit. */
maxPerimeter?: number;
/** REJECT hole candidates if they have more than this number of edges */
maxEdgesAroundHole?: number;
/** REJECT hole candidates if their orientation is not COUNTERCLOCKWISE around this vector.
* * For instance, use an upward Z vector for a DTM whose facets face upward. This suppresses incorrectly treating the outer boundary as a hole.
*/
upVector?: Vector3d;
/** requests that all content from the original mesh be copied to the mesh with filled holes. */
includeOriginalMesh?: boolean;
}
/** Selective output options for PolyfaceQuery.cloneOffset:
* * undefined means the usual facets in the expected offset mesh.
* * if present as a json object, the various booleans select respective outputs.
* * @public
*/
export interface OffsetMeshSelectiveOutputOptions {
outputOffsetsFromFacesBeforeChamfers?: boolean;
outputOffsetsFromFaces?: boolean;
outputOffsetsFromEdges?: boolean;
outputOffsetsFromVertices?: boolean;
}
/**
* Options carrier for [[PolyfaceQuery.cloneOffset]].
* * Default options are strongly recommended.
* * The option most likely to be changed is chamferTurnAngle
* @public
*/
export class OffsetMeshOptions {
/** max angle between normals to be considered smooth */
public smoothSingleAngleBetweenNormals: Angle;
/** max accumulation of angle between normals to be considered smooth */
public smoothAccumulatedAngleBetweenNormals: Angle;
/** When crossing an edge, this turn angle (typically 120 degrees) triggers a chamfer */
public chamferAngleBetweenNormals: Angle;
/** optional control structure for selective output.
* * If undefined, output all expected offset facets.
*/
public outputSelector?: OffsetMeshSelectiveOutputOptions;
/** Constructor -- CAPTURE parameters ... */
private constructor(
smoothSingleAngleBetweenNormals: Angle = Angle.createDegrees(25),
smoothAccumulatedAngleBetweenNormals: Angle = Angle.createDegrees(60),
chamferTurnAngle: Angle = Angle.createDegrees(90)) {
this.smoothSingleAngleBetweenNormals = smoothSingleAngleBetweenNormals.clone();
this.smoothAccumulatedAngleBetweenNormals = smoothAccumulatedAngleBetweenNormals.clone();
this.chamferAngleBetweenNormals = chamferTurnAngle.clone();
}
/** construct and return an OffsetMeshOptions with given parameters.
* * Angles are forced to minimum values.
* * Clones of the angles are given to the constructor.
* @param smoothSingleRadiansBetweenNormals an angle larger than this (between facets) is considered a sharp edge
* @param smoothAccumulatedAngleBetweenNormals angles that sum to this much may be consolidated for average normal
* @param chamferTurnAngleBetweenNormals when facets meet with larger angle, a chamfer edge may be added if the angle between facet normals is larger than this.
*/
public static create(
smoothSingleAngleBetweenNormals: Angle = Angle.createDegrees(25),
smoothAccumulatedAngleBetweenNormals: Angle = Angle.createDegrees(60),
chamferTurnAngleBetweenNormals: Angle = Angle.createDegrees(120)) {
const mySmoothSingleRadiansBetweenNormals = smoothSingleAngleBetweenNormals.clone();
const mySmoothAccumulatedRadiansBetweenNormals = smoothAccumulatedAngleBetweenNormals.clone();
const myChamferTurnAngleBetweenNormals = chamferTurnAngleBetweenNormals.clone();
if (mySmoothSingleRadiansBetweenNormals.degrees < 1)
mySmoothAccumulatedRadiansBetweenNormals.setDegrees(1.0);
if (mySmoothAccumulatedRadiansBetweenNormals.degrees < 1.0)
mySmoothAccumulatedRadiansBetweenNormals.setDegrees(1.0);
if (mySmoothAccumulatedRadiansBetweenNormals.degrees < 15.0)
mySmoothAccumulatedRadiansBetweenNormals.setDegrees(15.0);
return new OffsetMeshOptions(mySmoothSingleRadiansBetweenNormals, mySmoothAccumulatedRadiansBetweenNormals, myChamferTurnAngleBetweenNormals);
}
}
/**
* Structure to return multiple results from volume between facets and plane
* @public
*/
export interface FacetProjectedVolumeSums {
/** Summed (signed) volume */
volume: number;
/** summed area moments for positive contributions */
positiveProjectedFacetAreaMoments?: MomentData;
/** summed area moments for negative contributions */
negativeProjectedFacetAreaMoments?: MomentData;
}
/**
* Enumeration of cases for retaining facets among duplicates
* @public
*/
export enum DuplicateFacetClusterSelector {
/** retain none of the duplicates */
SelectNone = 0,
/** retain any one member among duplicates */
SelectAny = 1,
/** retain all members among duplicates */
SelectAll = 2,
/** retain one from any cluster with an odd number of faces */
SelectOneByParity = 3,
}
/** PolyfaceQuery is a static class whose methods implement queries on a polyface or polyface visitor provided as a parameter to each method.
* @public
*/
export class PolyfaceQuery {
/** copy the points from a visitor into a Linestring3d in a Loop object */
public static visitorToLoop(visitor: PolyfaceVisitor) {
const ls = LineString3d.createPoints(visitor.point.getPoint3dArray());
return Loop.create(ls);
}
/** Create a linestring loop for each facet of the polyface. */
public static indexedPolyfaceToLoops(polyface: Polyface): BagOfCurves {
const result = BagOfCurves.create();
const visitor = polyface.createVisitor(1);
while (visitor.moveToNextFacet()) {
const loop = PolyfaceQuery.visitorToLoop(visitor);
result.tryAddChild(loop);
}
return result;
}
/** Return the sum of all facet areas.
* @param vectorToEye compute sum of *signed* facet areas projected to a view plane perpendicular to this vector
*/
public static sumFacetAreas(source: Polyface | PolyfaceVisitor | undefined, vectorToEye?: Vector3d): number {
let s = 0;
if (source !== undefined) {
if (source instanceof Polyface)
return PolyfaceQuery.sumFacetAreas(source.createVisitor(1), vectorToEye);
let unitVectorToEye: Vector3d | undefined;
if (vectorToEye !== undefined)
unitVectorToEye = vectorToEye.normalize();
source.reset();
while (source.moveToNextFacet()) {
const scaledNormal = PolygonOps.areaNormal(source.point.getPoint3dArray());
s += unitVectorToEye ? scaledNormal.dotProduct(unitVectorToEye) : scaledNormal.magnitude();
}
}
return s;
}
/** sum volumes of tetrahedra from origin to all facets.
* * if origin is omitted, the first point encountered (by the visitor) is used as origin.
* * If the mesh is closed, this sum is the volume.
* * If the mesh is not closed, this sum is the volume of a mesh with various additional facets
* from the origin to facets.
*/
public static sumTetrahedralVolumes(source: Polyface | PolyfaceVisitor, origin?: Point3d): number {
let s = 0;
if (source instanceof Polyface)
return PolyfaceQuery.sumTetrahedralVolumes(source.createVisitor(0), origin);
let myOrigin = origin;
const facetOrigin = Point3d.create();
const targetA = Point3d.create();
const targetB = Point3d.create();
source.reset();
while (source.moveToNextFacet()) {
if (myOrigin === undefined)
myOrigin = source.point.getPoint3dAtUncheckedPointIndex(0);
source.point.getPoint3dAtUncheckedPointIndex(0, facetOrigin);
for (let i = 1; i + 1 < source.point.length; i++) {
source.point.getPoint3dAtUncheckedPointIndex(i, targetA);
source.point.getPoint3dAtUncheckedPointIndex(i + 1, targetB);
s += myOrigin.tripleProductToPoints(facetOrigin, targetA, targetB);
}
}
return s / 6.0;
}
/** sum (signed) volumes between facets and a plane.
* Return a structure with multiple sums:
* * volume = the sum of (signed) volumes between facets and the plane.
* * positiveAreaMomentData, negativeProjectedFacetAreaMoments = moment data with centroid, area, and second moments with respect to the centroid.
*
*/
public static sumVolumeBetweenFacetsAndPlane(source: Polyface | PolyfaceVisitor, plane: Plane3dByOriginAndUnitNormal): FacetProjectedVolumeSums {
if (source instanceof Polyface)
return PolyfaceQuery.sumVolumeBetweenFacetsAndPlane(source.createVisitor(0), plane);
const facetOrigin = Point3d.create();
const targetA = Point3d.create();
const targetB = Point3d.create();
const triangleNormal = Vector3d.create();
const planeNormal = plane.getNormalRef();
let h0, hA, hB;
let signedVolumeSum = 0.0;
let signedTriangleArea;
let singleFacetArea;
const positiveAreaMomentSums = MomentData.create(undefined, true);
const negativeAreaMomentSums = MomentData.create(undefined, true);
const singleFacetProducts = Matrix4d.createZero();
const projectToPlane = plane.getProjectionToPlane();
source.reset();
// For each facet ..
// Form triangles from facet origin to each far edge.
// Sum signed area and volume contributions
// each "projectedArea" contribution is twice the area of a triangle.
// each volume contribution is 3 times the actual volume -- (1/3) of the altitude sums was the centroid altitude.
while (source.moveToNextFacet()) {
source.point.getPoint3dAtUncheckedPointIndex(0, facetOrigin);
h0 = plane.altitude(facetOrigin);
singleFacetArea = 0;
// within a single facets, the singleFacetArea sum is accumulated with signs of individual triangles.
// For a non-convex facet, this can be a mixture of positive and negative areas.
// The absoluteProjectedAreaSum contribution is forced positive after the sum for the facet.
for (let i = 1; i + 1 < source.point.length; i++) {
source.point.getPoint3dAtUncheckedPointIndex(i, targetA);
source.point.getPoint3dAtUncheckedPointIndex(i + 1, targetB);
facetOrigin.crossProductToPoints(targetA, targetB, triangleNormal);
hA = plane.altitude(targetA);
hB = plane.altitude(targetB);
signedTriangleArea = planeNormal.dotProduct(triangleNormal);
singleFacetArea += signedTriangleArea;
signedVolumeSum += signedTriangleArea * (h0 + hA + hB);
}
singleFacetProducts.setZero();
source.point.multiplyTransformInPlace(projectToPlane);
PolygonOps.addSecondMomentAreaProducts(source.point, facetOrigin, singleFacetProducts);
if (singleFacetArea > 0) {
positiveAreaMomentSums.accumulateProductsFromOrigin(facetOrigin, singleFacetProducts, 1.0);
} else {
negativeAreaMomentSums.accumulateProductsFromOrigin(facetOrigin, singleFacetProducts, 1.0);
}
}
positiveAreaMomentSums.shiftOriginAndSumsToCentroidOfSums();
negativeAreaMomentSums.shiftOriginAndSumsToCentroidOfSums();
const positiveAreaMoments = MomentData.inertiaProductsToPrincipalAxes(positiveAreaMomentSums.origin, positiveAreaMomentSums.sums);
const negativeAreaMoments = MomentData.inertiaProductsToPrincipalAxes(negativeAreaMomentSums.origin, negativeAreaMomentSums.sums);
return {
volume: signedVolumeSum / 6.0,
positiveProjectedFacetAreaMoments: positiveAreaMoments,
negativeProjectedFacetAreaMoments: negativeAreaMoments,
};
}
/** Return the inertia products [xx,xy,xz,xw, yw, etc] integrated over all all facets, as viewed from origin. */
public static sumFacetSecondAreaMomentProducts(source: Polyface | PolyfaceVisitor, origin: Point3d): Matrix4d {
if (source instanceof Polyface)
return PolyfaceQuery.sumFacetSecondAreaMomentProducts(source.createVisitor(0), origin);
const products = Matrix4d.createZero();
source.reset();
while (source.moveToNextFacet()) {
PolygonOps.addSecondMomentAreaProducts(source.point, origin, products);
}
return products;
}
/** Return the inertia products [xx,xy,xz,xw, yw, etc] integrated over all tetrahedral volumes from origin */
public static sumFacetSecondVolumeMomentProducts(source: Polyface | PolyfaceVisitor, origin: Point3d): Matrix4d {
if (source instanceof Polyface)
return PolyfaceQuery.sumFacetSecondVolumeMomentProducts(source.createVisitor(0), origin);
const products = Matrix4d.createZero();
source.reset();
while (source.moveToNextFacet()) {
PolygonOps.addSecondMomentVolumeProducts(source.point, origin, products);
}
return products;
}
/** Compute area moments for the mesh. In the returned MomentData:
* * origin is the centroid.
* * localToWorldMap has the origin and principal directions
* * radiiOfGyration radii for rotation around the x,y,z axes.
*/
public static computePrincipalAreaMoments(source: Polyface): MomentData | undefined {
const origin = source.data.getPoint(0);
if (!origin) return undefined;
const inertiaProducts = PolyfaceQuery.sumFacetSecondAreaMomentProducts(source, origin);
return MomentData.inertiaProductsToPrincipalAxes(origin, inertiaProducts);
}
/** Compute area moments for the mesh. In the returned MomentData:
* * origin is the centroid.
* * localToWorldMap has the origin and principal directions
* * radiiOfGyration radii for rotation around the x,y,z axes.
* * The result is only valid if the mesh is closed.
* * There is no test for closure. Use `PolyfaceQuery.isPolyfaceClosedByEdgePairing(polyface)` to test for closure.
*/
public static computePrincipalVolumeMoments(source: Polyface): MomentData | undefined {
const origin = source.data.getPoint(0);
if (!origin) return undefined;
const inertiaProducts = PolyfaceQuery.sumFacetSecondVolumeMomentProducts(source, origin);
return MomentData.inertiaProductsToPrincipalAxes(origin, inertiaProducts);
}
/** Determine whether all facets are convex.
* @param source mesh to examine
*/
public static areFacetsConvex(source: Polyface | PolyfaceVisitor): boolean {
if (source instanceof Polyface)
return this.areFacetsConvex(source.createVisitor(0));
source.setNumWrap(0);
source.reset();
while (source.moveToNextFacet()) {
if (source.pointCount > 3) {
if (!PolygonOps.isConvex(source.point))
return false;
}
}
return true;
}
/**
* Test for convex volume by dihedral angle tests on all edges.
* * This tests if all dihedral angles are positive.
* * In a closed solid, this is a strong test for overall convexity.
* * With `ignoreBoundaries` true, this may be a useful test when all the facets are in a single edge-connected component, such as a pyramid with no underside.
* * It is not a correct test if there are multiple, disjoint components.
* * Take the above-mentioned pyramid with no underside.
* * Within the same mesh, have a second pyramid placed to the side, still facing upward.
* * The angles will pass the dihedral convexity test, but the composite thing surely is not convex.
* @param source mesh to examine
* @param ignoreBoundaries if true, ignore simple boundary edges, i.e. allow unclosed meshes.
* @returns true if the mesh is closed and has all dihedral angles (angle across edge) positive
*/
public static isConvexByDihedralAngleCount(source: Polyface, ignoreBoundaries: boolean = false): boolean {
return this.dihedralAngleSummary(source, ignoreBoundaries) > 0;
}
/**
* Compute a number summarizing the dihedral angles in the mesh.
* @see [[isConvexByDihedralAngleCount]] for comments about ignoreBoundaries===true when there are multiple connected components.
* @param source mesh to examine
* @param ignoreBoundaries if true, ignore simple boundary edges, i.e. allow unclosed meshes.
* @returns a number summarizing the dihedral angles in the mesh.
* * Return 1 if all angles are positive or planar. The mesh is probably convex with outward normals.
* * Return -1 if all angles are negative or planar. The mesh is probably convex with inward normals.
* * Return 0 if
* * angles area mixed
* * any edge has other than 1 incident facet or more than 2 incident facets.
* * (but null edges are permitted -- These occur naturally at edges of quads at north or south pole)
*/
public static dihedralAngleSummary(source: Polyface, ignoreBoundaries: boolean = false): number {
const edges = new IndexedEdgeMatcher();
const visitor = source.createVisitor(1);
visitor.reset();
const centroidNormal: Ray3d[] = [];
let normalCounter = 0;
while (visitor.moveToNextFacet()) {
const numEdges = visitor.pointCount - 1;
const normal = PolygonOps.centroidAreaNormal(visitor.point);
if (normal === undefined)
return 0;
centroidNormal.push(normal);
for (let i = 0; i < numEdges; i++) {
edges.addEdge(visitor.clientPointIndex(i), visitor.clientPointIndex(i + 1), normalCounter);
}
normalCounter++;
}
const badClusters: SortableEdgeCluster[] = [];
const manifoldClusters: SortableEdgeCluster[] = [];
edges.sortAndCollectClusters(manifoldClusters,
ignoreBoundaries ? undefined : badClusters, undefined, badClusters);
if (badClusters.length > 0)
return 0;
let numPositive = 0;
let numPlanar = 0;
let numNegative = 0;
const edgeVector = Vector3d.create();
for (const cluster of manifoldClusters) {
const sideA = cluster[0];
const sideB = cluster[1];
if (sideA instanceof SortableEdge
&& sideB instanceof SortableEdge
&& source.data.point.vectorIndexIndex(sideA.vertexIndexA, sideA.vertexIndexB, edgeVector)) {
const dihedralAngle = centroidNormal[sideA.facetIndex].direction.signedAngleTo(
centroidNormal[sideB.facetIndex].direction, edgeVector);
if (dihedralAngle.isAlmostZero) numPlanar++;
else if (dihedralAngle.radians > 0.0) numPositive++;
else numNegative++;
}
}
if (numPositive > 0 && numNegative === 0)
return 1;
if (numNegative > 0 && numPositive === 0)
return -1;
// problem case: if all edges have zero dihedral angle, record it as convex.
if (numPlanar > 0 && numPositive === 0 && numNegative === 0)
return 1;
return 0;
}
/**
* Test if the facets in `source` occur in perfectly mated pairs, as is required for a closed manifold volume.
*/
public static isPolyfaceClosedByEdgePairing(source: Polyface): boolean {
return this.isPolyfaceManifold(source, false);
}
/** Test edges pairing in `source` mesh.
* * for `allowSimpleBoundaries === false` true return means this is a closed 2-manifold surface
* * for `allowSimpleBoundaries === true` true means this is a 2-manifold surface which may have boundary, but is still properly matched internally.
* * Any edge with 3 or more incident facets triggers `false` return.
* * Any edge with 2 incident facets in the same direction triggers a `false` return.
*/
public static isPolyfaceManifold(source: Polyface, allowSimpleBoundaries: boolean = false): boolean {
const edges = new IndexedEdgeMatcher();
const visitor = source.createVisitor(1);
visitor.reset();
while (visitor.moveToNextFacet()) {
const numEdges = visitor.pointCount - 1;
for (let i = 0; i < numEdges; i++) {
edges.addEdge(visitor.clientPointIndex(i), visitor.clientPointIndex(i + 1), visitor.currentReadIndex());
}
}
const badClusters: SortableEdgeCluster[] = [];
edges.sortAndCollectClusters(undefined, allowSimpleBoundaries ? undefined : badClusters, undefined, badClusters);
return badClusters.length === 0;
}
/**
* construct a CurveCollection containing boundary edges.
* * each edge is a LineSegment3d
* @param source polyface or visitor
* @param includeTypical true to in include typical boundary edges with a single incident facet
* @param includeMismatch true to include edges with more than 2 incident facets
* @param includeNull true to include edges with identical start and end vertex indices.
*/
public static boundaryEdges(source: Polyface | PolyfaceVisitor | undefined,
includeTypical: boolean = true, includeMismatch: boolean = true, includeNull: boolean = true): CurveCollection | undefined {
const result = new BagOfCurves();
const announceEdge = (pointA: Point3d, pointB: Point3d, _indexA: number, _indexB: number, _readIndex: number) => {
result.tryAddChild(LineSegment3d.create(pointA, pointB));
};
PolyfaceQuery.announceBoundaryEdges(source, announceEdge, includeTypical, includeMismatch, includeNull);
if (result.children.length === 0)
return undefined;
return result;
}
/**
* Collect boundary edges.
* * Return the edges as the simplest collection of chains of line segments.
* @param source facets
* @param includeTypical true to in include typical boundary edges with a single incident facet
* @param includeMismatch true to include edges with more than 2 incident facets
* @param includeNull true to include edges with identical start and end vertex indices.
*/
public static collectBoundaryEdges(source: Polyface | PolyfaceVisitor, includeTypical: boolean = true, includeMismatch: boolean = true, includeNull: boolean = true): AnyChain | undefined {
const collector = new MultiChainCollector(Geometry.smallMetricDistance, Geometry.smallMetricDistance);
PolyfaceQuery.announceBoundaryEdges(source, (ptA: Point3d, ptB: Point3d) => collector.captureCurve(LineSegment3d.create(ptA, ptB)), includeTypical, includeMismatch, includeNull);
return collector.grabResult(true);
}
/**
* Test if the facets in `source` occur in perfectly mated pairs, as is required for a closed manifold volume.
* If not, extract the boundary edges as lines.
* @param source polyface or visitor
* @param announceEdge function to be called with each boundary edge. The announcement is start and end points, start and end indices, and facet index.
* @param includeTypical true to announce typical boundary edges with a single incident facet
* @param includeMismatch true to announce edges with more than 2 incident facets
* @param includeNull true to announce edges with identical start and end vertex indices.
*/
public static announceBoundaryEdges(source: Polyface | PolyfaceVisitor | undefined,
announceEdge: (pointA: Point3d, pointB: Point3d, indexA: number, indexB: number, facetIndex: number) => void,
includeTypical: boolean = true, includeMismatch: boolean = true, includeNull: boolean = true): void {
if (source === undefined)
return undefined;
const edges = new IndexedEdgeMatcher();
const visitor = source instanceof Polyface ? source.createVisitor(1) : source;
visitor.setNumWrap(1);
visitor.reset();
while (visitor.moveToNextFacet()) {
const numEdges = visitor.pointCount - 1;
for (let i = 0; i < numEdges; i++) {
edges.addEdge(visitor.clientPointIndex(i), visitor.clientPointIndex(i + 1), visitor.currentReadIndex());
}
}
const bad1: SortableEdgeCluster[] = [];
const bad2: SortableEdgeCluster[] = [];
const bad0: SortableEdgeCluster[] = [];
edges.sortAndCollectClusters(undefined, bad1, bad0, bad2);
const badList = [];
if (includeTypical && bad1.length > 0)
badList.push(bad1);
if (includeMismatch && bad2.length > 0)
badList.push(bad2);
if (includeNull && bad0.length > 0)
badList.push(bad0);
if (badList.length === 0)
return undefined;
const sourcePolyface = visitor.clientPolyface()!;
const pointA = Point3d.create();
const pointB = Point3d.create();
for (const list of badList) {
for (const e of list) {
const e1 = e instanceof SortableEdge ? e : e[0];
const indexA = e1.vertexIndexA;
const indexB = e1.vertexIndexB;
if (sourcePolyface.data.getPoint(indexA, pointA) && sourcePolyface.data.getPoint(indexB, pointB))
announceEdge(pointA, pointB, indexA, indexB, e1.facetIndex);
}
}
}
/**
* Invoke the callback on each manifold edge whose adjacent facet normals form vectorToEye dot products with opposite sign.
* * The callback is not called on boundary edges.
* @param source facets
* @param announce callback function invoked on manifold silhouette edges
* @param vectorToEye normal of plane in which to compute silhouette edges
* @param sideAngle angular tolerance for perpendicularity test
*/
public static announceSilhouetteEdges(
source: Polyface | PolyfaceVisitor,
announce: (pointA: Point3d, pointB: Point3d, vertexIndexA: number, vertexIndexB: number, facetIndex: number) => void,
vectorToEye: Vector3d,
sideAngle: Angle = Angle.createSmallAngle(),
): void {
if (source instanceof Polyface)
return this.announceSilhouetteEdges(source.createVisitor(1), announce, vectorToEye, sideAngle);
const mesh = source.clientPolyface();
if (undefined === mesh)
return;
source.setNumWrap(1);
const allEdges = this.createIndexedEdges(source);
const manifoldEdges: SortableEdgeCluster[] = [];
allEdges.sortAndCollectClusters(manifoldEdges);
const sideAngleTol = sideAngle.radians < 0.0 ? -sideAngle.radians : sideAngle.radians;
const pointA = Point3d.create();
const pointB = Point3d.create();
const normal = Vector3d.create();
const analyzeFace = (iFacet: number): { isSideFace: boolean, perpAngle: number } => {
if (!PolyfaceQuery.computeFacetUnitNormal(source, iFacet, normal))
return { isSideFace: false, perpAngle: 0.0 };
const perpAngle = normal.radiansFromPerpendicular(vectorToEye);
const isSideFace = Math.abs(perpAngle) <= sideAngleTol;
return { isSideFace, perpAngle };
};
for (const pair of manifoldEdges) {
if (!Array.isArray(pair) || pair.length !== 2)
continue;
const indexA = pair[0].vertexIndexA;
const indexB = pair[0].vertexIndexB;
if (!mesh.data.getPoint(indexA, pointA) || !mesh.data.getPoint(indexB, pointB))
continue;
const face0 = analyzeFace(pair[0].facetIndex);
if (face0.isSideFace) {
announce(pointA, pointB, indexA, indexB, pair[0].facetIndex);
continue;
}
const face1 = analyzeFace(pair[1].facetIndex);
if (face1.isSideFace) {
announce(pointB, pointA, indexB, indexA, pair[1].facetIndex);
continue;
}
if (face0.perpAngle * face1.perpAngle < 0.0) { // normals straddle plane
announce(pointA, pointB, indexA, indexB, pair[0].facetIndex);
continue;
}
}
}
/**
* Collect manifold edges whose adjacent facet normals form vectorToEye dot products with opposite sign.
* * Does not return boundary edges.
* * Return the edges as chains of line segments.
* @param source facets
* @param vectorToEye normal of plane in which to compute silhouette edges
* @param sideAngle angular tolerance for perpendicularity test
*/
public static collectSilhouetteEdges(source: Polyface | PolyfaceVisitor, vectorToEye: Vector3d, sideAngle: Angle = Angle.createSmallAngle()): AnyChain | undefined {
const collector = new MultiChainCollector(Geometry.smallMetricDistance, Geometry.smallMetricDistance);
PolyfaceQuery.announceSilhouetteEdges(source, (ptA: Point3d, ptB: Point3d) => collector.captureCurve(LineSegment3d.create(ptA, ptB)), vectorToEye, sideAngle);
return collector.grabResult(true);
}
/** Find segments (within the linestring) which project to facets.
* * Announce each pair of linestring segment and on-facet segment through a callback.
* * Facets are ASSUMED to be convex and planar, and not overlap in the z direction.
*/
public static announceSweepLinestringToConvexPolyfaceXY(linestringPoints: GrowableXYZArray, polyface: Polyface,
announce: AnnounceDrapePanel): any {
const context = SweepLineStringToFacetContext.create(linestringPoints);
if (context) {
const visitor = polyface.createVisitor(0);
for (visitor.reset(); visitor.moveToNextFacet();) {
context.projectToPolygon(visitor.point, announce, polyface, visitor.currentReadIndex());
}
}
}
/** Execute context.projectToPolygon until its work estimates accumulate to workLimit */
private static async continueAnnounceSweepLinestringToConvexPolyfaceXY(
context: SweepLineStringToFacetContext, visitor: PolyfaceVisitor, announce: AnnounceDrapePanel): Promise<number> {
let workCount = 0;
while ((workCount < this.asyncWorkLimit) && visitor.moveToNextFacet()) {
workCount += context.projectToPolygon(visitor.point, announce, visitor.clientPolyface()!, visitor.currentReadIndex());
}
return workCount;
}
// amount of computation to do per step of async methods.
private static _asyncWorkLimit = 1.e06;
/** Set the limit on work during an async time blocks, and return the old value.
* * This should be a large number -- default is 1.0e6
* @internal
*/
public static setAsyncWorkLimit(value: number): number { const a = this._asyncWorkLimit; this._asyncWorkLimit = value; return a; }
/** Query the current limit on work during an async time block.
* @internal
*/
public static get asyncWorkLimit(): number { return this._asyncWorkLimit; }
/** Number of "await" steps executed in recent async calls.
* @internal
*/
public static awaitBlockCount = 0;
/** Find segments (within the linestring) which project to facets.
* * Announce each pair of linestring segment and on-facet segment through a callback.
* * Facets are ASSUMED to be convex and planar, and not overlap in the z direction.
* * REMARK: Although this is public, the usual use is via slightly higher level public methods, viz:
* * asyncSweepLinestringToFacetsXYReturnChains
* @internal
*/
public static async asyncAnnounceSweepLinestringToConvexPolyfaceXY(linestringPoints: GrowableXYZArray, polyface: Polyface,
announce: AnnounceDrapePanel): Promise<number> {
const context = SweepLineStringToFacetContext.create(linestringPoints);
this.awaitBlockCount = 0;
let workTotal = 0;
if (context) {
const visitor = polyface.createVisitor(0);
let workCount;
while (0 < (workCount = await Promise.resolve(PolyfaceQuery.continueAnnounceSweepLinestringToConvexPolyfaceXY(context, visitor, announce)))) {
workTotal += workCount;
this.awaitBlockCount++;
// GeometryCoreTestIO.consoleLog({ myWorkCount: workCount, myBlockCount: this.awaitBlockCount });
}
}
// GeometryCoreTestIO.consoleLog({ myWorkTotal: workTotal, myBlockCount: this.awaitBlockCount });
return workTotal;
}
/** Search the facets for facet subsets that are connected with at least vertex contact.
* * Return array of arrays of facet indices.
*/
public static partitionFacetIndicesByVertexConnectedComponent(polyface: Polyface | PolyfaceVisitor): number[][] {
if (polyface instanceof Polyface) {
return this.partitionFacetIndicesByVertexConnectedComponent(polyface.createVisitor(0));
}
// The polyface is really a visitor !!!
const context = new UnionFindContext(this.visitorClientPointCount(polyface));
for (polyface.reset(); polyface.moveToNextFacet();) {
const firstVertexIndexOnThisFacet = polyface.pointIndex[0];
for (const vertexIndex of polyface.pointIndex)
context.mergeSubsets(firstVertexIndexOnThisFacet, vertexIndex);
}
const roots = context.collectRootIndices();
const facetsInComponent: number[][] = [];
const numRoots = roots.length;
for (let i = 0; i < numRoots; i++) {
facetsInComponent.push([]);
}
for (polyface.reset(); polyface.moveToNextFacet();) {
const firstVertexIndexOnThisFacet = polyface.pointIndex[0];
const rootVertexForThisFacet = context.findRoot(firstVertexIndexOnThisFacet);
for (let rootIndex = 0; rootIndex < numRoots; rootIndex++) {
if (roots[rootIndex] === rootVertexForThisFacet) {
facetsInComponent[rootIndex].push(polyface.currentReadIndex());
break;
}
}
}
return facetsInComponent;
}
/**
* * Examine the normal orientation for each faces.
* * Separate to 3 partitions:
* * facets with normal in the positive direction of the vectorToEye (partition 0)
* * facets with normal in the negative direction of the vectorToEye (partition 1)
* * facets nearly perpendicular to the view vector (partition 2)
* * Return array of arrays of facet indices.
*/
public static partitionFacetIndicesByVisibilityVector(polyface: Polyface | PolyfaceVisitor, vectorToEye: Vector3d, sideAngleTolerance: Angle): number[][] {
if (polyface instanceof Polyface) {
return this.partitionFacetIndicesByVisibilityVector(polyface.createVisitor(0), vectorToEye, sideAngleTolerance);
}
const facetsInComponent: number[][] = [];
for (let i = 0; i < 3; i++) {
facetsInComponent.push([]);
}
const forwardComponent = facetsInComponent[0];
const rearComponent = facetsInComponent[1];
const sideComponent = facetsInComponent[2];
const radiansTol = Math.max(sideAngleTolerance.radians, 1.0e-8);
for (polyface.reset(); polyface.moveToNextFacet();) {
const areaNormal = PolygonOps.areaNormalGo(polyface.point);
const index = polyface.currentReadIndex();
if (areaNormal) {
const angle = areaNormal.angleFromPerpendicular(vectorToEye);
if (Math.abs(angle.radians) < radiansTol) {
sideComponent.push(index);
} else if (areaNormal.dotProduct(vectorToEye) < 0) {
rearComponent.push(index);
} else {
forwardComponent.push(index);
}
}
}
return facetsInComponent;
}
/**
* Return the boundary of facets that are facing the eye.
* @param polyface
* @param visibilitySubset selector among the visible facet sets extracted by partitionFacetIndicesByVisibilityVector
* * 0 ==> forward facing
* * 1 ==> rear facing
* * 2 ==> side facing
* @param vectorToEye
* @param sideAngleTolerance
*/
public static boundaryOfVisibleSubset(polyface: IndexedPolyface, visibilitySelect: 0 | 1 | 2, vectorToEye: Vector3d, sideAngleTolerance: Angle = Angle.createDegrees(1.0e-3)): CurveCollection | undefined {
const partitionedIndices = this.partitionFacetIndicesByVisibilityVector(polyface, vectorToEye, sideAngleTolerance);
if (partitionedIndices[visibilitySelect].length === 0)
return undefined;
const visitor = IndexedPolyfaceSubsetVisitor.createSubsetVisitor(polyface, partitionedIndices[visibilitySelect], 1);
return this.boundaryEdges(visitor, true, false, false);
}
/**
* Search for edges with only 1 incident facet.
* * chain them into loops
* * emit the loops to the announceLoop function
* @param mesh
*/
public static announceBoundaryChainsAsLineString3d(mesh: Polyface | PolyfaceVisitor,
announceLoop: (points: LineString3d) => void) {
const collector = new MultiChainCollector(Geometry.smallMetricDistance, 1000);
PolyfaceQuery.announceBoundaryEdges(mesh,
(pointA: Point3d, pointB: Point3d, _indexA: number, _indexB: number) => collector.captureCurve(LineSegment3d.create(pointA, pointB)),
true, false, false);
collector.announceChainsAsLineString3d(announceLoop);
}
/**
* Return a mesh with
* * clusters of adjacent, coplanar facets merged into larger facets.
* * other facets included unchanged.
* @param mesh existing mesh or visitor
* @param maxSmoothEdgeAngle maximum dihedral angle across an edge between facets deemed coplanar. If undefined, uses `Geometry.smallAngleRadians`.
* @returns
*/
public static cloneWithMaximalPlanarFacets(mesh: Polyface | PolyfaceVisitor, maxSmoothEdgeAngle?: Angle): IndexedPolyface | undefined {
if (mesh instanceof Polyface)
return this.cloneWithMaximalPlanarFacets(mesh.createVisitor(0), maxSmoothEdgeAngle);
const numFacets = PolyfaceQuery.visitorClientFacetCount(mesh);
const smoothEdges = PolyfaceQuery.collectEdgesByDihedralAngle(mesh, maxSmoothEdgeAngle);
const partitions = PolyfaceQuery.partitionFacetIndicesBySortableEdgeClusters(smoothEdges, numFacets);
const builder = PolyfaceBuilder.create();
const visitor = mesh;
const planarPartitions: number[][] = [];
for (const partition of partitions) {
if (partition.length === 1) {
if (visitor.moveToReadIndex(partition[0]))
builder.addFacetFromVisitor(visitor);
} else {
// This is a non-trivial set of contiguous coplanar facets
planarPartitions.push(partition);
}
}
const fragmentPolyfaces = PolyfaceQuery.clonePartitions(mesh, planarPartitions);
const gapTolerance = 1.0e-4;
const planarityTolerance = 1.0e-4;
for (const fragment of fragmentPolyfaces) {
const edges: LineSegment3d[] = [];
const edgeStrings: Point3d[][] = [];
PolyfaceQuery.announceBoundaryEdges(fragment,
(pointA: Point3d, pointB: Point3d, _indexA: number, _indexB: number) => {
edges.push(LineSegment3d.create(pointA, pointB));
edgeStrings.push([pointA.clone(), pointB.clone()]);
});
const chains = CurveOps.collectChains(edges, gapTolerance, planarityTolerance);
if (chains) {
const frameBuilder = new FrameBuilder();
frameBuilder.announce(chains);
const frame = frameBuilder.getValidatedFrame(false);
if (frame !== undefined) {
const inverseFrame = frame.inverse();
if (inverseFrame !== undefined) {
inverseFrame.multiplyPoint3dArrayArrayInPlace(edgeStrings);
const graph = HalfEdgeGraphMerge.formGraphFromChains(edgeStrings, true, HalfEdgeMask.BOUNDARY_EDGE);
if (graph) {
HalfEdgeGraphSearch.collectConnectedComponentsWithExteriorParityMasks(graph,
new HalfEdgeMaskTester(HalfEdgeMask.BOUNDARY_EDGE), HalfEdgeMask.EXTERIOR);
// this.purgeNullFaces(HalfEdgeMask.EXTERIOR);
const polyface1 = PolyfaceBuilder.graphToPolyface(graph);
builder.addIndexedPolyface(polyface1, false, frame);
}
}
}
}
}
return builder.claimPolyface(true);
}
/**
* Return a mesh with "some" holes filled in with new facets.
* * Candidate chains are computed by [[announceBoundaryChainsAsLineString3d]].
* * Unclosed chains are rejected.
* * Closed chains are triangulated and returned as a mesh.
* * The options structure enforces restrictions on how complicated the hole filling can be:
* * maxEdgesAroundHole -- holes with more edges are skipped
* * maxPerimeter -- holes with larger summed edge lengths are skipped.
* * upVector -- holes that do not have positive area along this view are skipped.
* * includeOriginalMesh -- includes the original mesh in the output mesh, so the composite mesh is a clone with holes filled
* @param mesh existing mesh
* @param options options controlling the hole fill.
* @param unfilledChains optional array to receive the points around holes that were not filled.
* @returns
*/
public static fillSimpleHoles(mesh: Polyface | PolyfaceVisitor, options: HoleFillOptions, unfilledChains?: LineString3d[]): IndexedPolyface | undefined {
if (mesh instanceof Polyface)
return this.fillSimpleHoles(mesh.createVisitor(0), options, unfilledChains);
const builder = PolyfaceBuilder.create();
const chains: LineString3d[] = [];
PolyfaceQuery.announceBoundaryChainsAsLineString3d(mesh,
(ls: LineString3d) => { ls.reverseInPlace(); chains.push(ls); });
for (const c of chains) {
const points = c.points;
let rejected = false;
if (!c.isPhysicallyClosed)
rejected = true;
else if (options.maxEdgesAroundHole !== undefined && points.length > options.maxEdgesAroundHole)
rejected = true;
else if (options.maxPerimeter !== undefined && Point3dArray.sumEdgeLengths(points, false) > options.maxPerimeter)
rejected = true;
else if (options.upVector !== undefined && PolygonOps.sumTriangleAreasPerpendicularToUpVector(points, options.upVector) <= 0.0)
rejected = true;
if (!rejected && SpacePolygonTriangulation.triangulateSimplestSpaceLoop(points,
(_loop: Point3d[], triangles: Point3d[][]) => {
for (const t of triangles)
builder.addPolygon(t);
},
)) {
} else {
rejected = true;
}
if (rejected && unfilledChains !== undefined)
unfilledChains.push(c); // yes, capture it -- this scope owns the chains and has no further use for it.
}
if (options.includeOriginalMesh !== undefined && options.includeOriginalMesh) {
for (mesh.reset(); mesh.moveToNextFacet();)
builder.addFacetFromVisitor(mesh);
}
return builder.claimPolyface(true);
}
/** Clone the facets in each partition to a separate polyface.
*
*/
public static clonePartitions(polyface: Polyface | PolyfaceVisitor, partitions: number[][]): Polyface[] {
if (polyface instanceof Polyface) {
return this.clonePartitions(polyface.createVisitor(0), partitions);
}
polyface.setNumWrap(0);
const polyfaces: Polyface[] = [];
const options = StrokeOptions.createForFacets();
options.needNormals = polyface.normal !== undefined;
options.needParams = polyface.param !== undefined;
options.needColors = polyface.color !== undefined;
options.needTwoSided = polyface.twoSided;
for (const partition of partitions) {
const builder = PolyfaceBuilder.create(options);
polyface.reset();
for (const facetIndex of partition) {
polyface.moveToReadIndex(facetIndex);
builder.addFacetFromVisitor(polyface);
}
polyfaces.push(builder.claimPolyface(true));
}
return polyfaces;
}
/** Clone facets that pass a filter function */
public static cloneFiltered(source: Polyface | PolyfaceVisitor, filter: (visitor: PolyfaceVisitor) => boolean): IndexedPolyface {