@perplexdotgg/bounce 1.0.0

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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/package.json +32 -0
  4. package/src/builders/ConvexHullBuilder.ts +437 -0
  5. package/src/builders/ConvexHullBuilder2d.ts +344 -0
  6. package/src/builders/ConvexHullBuilder3d.ts +1689 -0
  7. package/src/builders/HeightMapBuilder.ts +414 -0
  8. package/src/builders/TriangleMeshBuilder.ts +92 -0
  9. package/src/collision/CastShapesModule.ts +184 -0
  10. package/src/collision/CollideShapesModule.ts +152 -0
  11. package/src/collision/HeightMapCaster.ts +38 -0
  12. package/src/collision/HeightMapCollider.ts +33 -0
  13. package/src/collision/TriangleCaster.ts +249 -0
  14. package/src/collision/TriangleCollider.ts +308 -0
  15. package/src/collision/TriangleCollider2.ts +379 -0
  16. package/src/collision/activeEdge.ts +146 -0
  17. package/src/collision/cast/cast.ts +139 -0
  18. package/src/collision/cast/castCompoundVsCompound.ts +59 -0
  19. package/src/collision/cast/castCompoundVsConvex.ts +116 -0
  20. package/src/collision/cast/castConvexVsCompound.ts +123 -0
  21. package/src/collision/cast/castConvexVsConvex.ts +213 -0
  22. package/src/collision/cast/castConvexVsHeightMap.ts +73 -0
  23. package/src/collision/cast/castConvexVsTriangleMesh.ts +56 -0
  24. package/src/collision/cast/castRayVsCompound.ts +44 -0
  25. package/src/collision/cast/castRayVsConvex.ts +45 -0
  26. package/src/collision/cast/castRayVsHeightMap.ts +58 -0
  27. package/src/collision/cast/castRayVsTriangleMesh.ts +58 -0
  28. package/src/collision/closestPoints/closestPoints.ts +23 -0
  29. package/src/collision/closestPoints/computeBarycentricCoordinates2d.ts +32 -0
  30. package/src/collision/closestPoints/computeBarycentricCoordinates3d.ts +81 -0
  31. package/src/collision/closestPoints/computeClosestPointOnLine.ts +30 -0
  32. package/src/collision/closestPoints/computeClosestPointOnTetrahedron.ts +96 -0
  33. package/src/collision/closestPoints/computeClosestPointOnTriangle.ts +195 -0
  34. package/src/collision/closestPoints/isOriginOutsideOfPlane.ts +25 -0
  35. package/src/collision/closestPoints/isOriginOutsideOfTrianglePlanes.ts +72 -0
  36. package/src/collision/collide/collide.ts +146 -0
  37. package/src/collision/collide/collideCompoundVsCompound.ts +60 -0
  38. package/src/collision/collide/collideCompoundVsConvex.ts +59 -0
  39. package/src/collision/collide/collideCompoundVsHeightMap.ts +73 -0
  40. package/src/collision/collide/collideCompoundVsTriangleMesh.ts +56 -0
  41. package/src/collision/collide/collideConvexVsCompound.ts +57 -0
  42. package/src/collision/collide/collideConvexVsConvex.ts +225 -0
  43. package/src/collision/collide/collideConvexVsConvexImp.ts +236 -0
  44. package/src/collision/collide/collideConvexVsHeightMap.ts +53 -0
  45. package/src/collision/collide/collideConvexVsTriangleMesh.ts +58 -0
  46. package/src/collision/collide/collideHeightMapVsCompound.ts +69 -0
  47. package/src/collision/collide/collideHeightMapVsConvex.ts +53 -0
  48. package/src/collision/collide/collideSphereVsSphere.ts +81 -0
  49. package/src/collision/collide/collideTriangleMeshVsCompound.ts +58 -0
  50. package/src/collision/collide/collideTriangleMeshVsConvex.ts +58 -0
  51. package/src/collision/epa/EpaConvexHullBuilder.ts +397 -0
  52. package/src/collision/epa/StaticArray.ts +154 -0
  53. package/src/collision/epa/TriangleFactory.ts +32 -0
  54. package/src/collision/epa/arrays.ts +99 -0
  55. package/src/collision/epa/binaryHeap.ts +82 -0
  56. package/src/collision/epa/structs.ts +227 -0
  57. package/src/collision/gjk/GjkModule.ts +864 -0
  58. package/src/collision/gjk/PenetrationDepthModule.ts +493 -0
  59. package/src/collision/gjk/SupportPoints.ts +50 -0
  60. package/src/collision/imp/MinkowskiDifference.ts +36 -0
  61. package/src/collision/imp/computeExploredDistanceLowerUpperBound.ts +40 -0
  62. package/src/collision/imp/finalizeImpResult.ts +69 -0
  63. package/src/collision/imp/findContactImp.ts +196 -0
  64. package/src/collision/imp/imp.ts +28 -0
  65. package/src/collision/imp/incrementalMinimumDistanceExploreDirection.ts +207 -0
  66. package/src/collision/mpr/findPortal.ts +152 -0
  67. package/src/collision/mpr/mpr.ts +29 -0
  68. package/src/collision/mpr/updatePortal.ts +52 -0
  69. package/src/constraints/BaseConstraint.ts +50 -0
  70. package/src/constraints/ConstraintOptions.ts +22 -0
  71. package/src/constraints/ConstraintSolver.ts +119 -0
  72. package/src/constraints/DistanceConstraint.ts +229 -0
  73. package/src/constraints/FixedConstraint.ts +203 -0
  74. package/src/constraints/HingeConstraint.ts +460 -0
  75. package/src/constraints/PointConstraint.ts +108 -0
  76. package/src/constraints/components/AngleComponent.ts +226 -0
  77. package/src/constraints/components/AxisComponent.ts +263 -0
  78. package/src/constraints/components/HingeComponent.ts +215 -0
  79. package/src/constraints/components/Motor.ts +36 -0
  80. package/src/constraints/components/PointConstraintComponent.ts +179 -0
  81. package/src/constraints/components/RotationEulerComponent.ts +139 -0
  82. package/src/constraints/components/Spring.ts +30 -0
  83. package/src/constraints/components/SpringComponent.ts +71 -0
  84. package/src/constraints/types.ts +6 -0
  85. package/src/helpers.ts +147 -0
  86. package/src/index.ts +50 -0
  87. package/src/math/BasicTransform.ts +19 -0
  88. package/src/math/NumberValue.ts +13 -0
  89. package/src/math/isometry.ts +64 -0
  90. package/src/math/mat3.ts +529 -0
  91. package/src/math/mat4.ts +588 -0
  92. package/src/math/quat.ts +193 -0
  93. package/src/math/scalar.ts +81 -0
  94. package/src/math/tensor.ts +17 -0
  95. package/src/math/vec3.ts +589 -0
  96. package/src/math/vec4.ts +10 -0
  97. package/src/physics/Body.ts +581 -0
  98. package/src/physics/CollisionFilter.ts +52 -0
  99. package/src/physics/SleepModule.ts +163 -0
  100. package/src/physics/broadphase/BodyPairsModule.ts +363 -0
  101. package/src/physics/broadphase/BvhModule.ts +237 -0
  102. package/src/physics/broadphase/BvhTree.ts +803 -0
  103. package/src/physics/broadphase/ConstraintPairsModule.ts +385 -0
  104. package/src/physics/broadphase/TriangleMeshBvhTree.ts +379 -0
  105. package/src/physics/manifold/ContactManifold.ts +227 -0
  106. package/src/physics/manifold/ContactManifoldModule.ts +623 -0
  107. package/src/physics/manifold/Face.ts +119 -0
  108. package/src/physics/manifold/ManifoldCache.ts +116 -0
  109. package/src/physics/manifold/clipping/clipPolyVsEdge.ts +131 -0
  110. package/src/physics/manifold/clipping/clipPolyVsPlane.ts +73 -0
  111. package/src/physics/manifold/clipping/clipPolyVsPoly.ts +72 -0
  112. package/src/physics/narrowphase/CollideBodiesModule.ts +755 -0
  113. package/src/physics/solver/ContactConstraintModule.ts +659 -0
  114. package/src/physics/solver/ManifoldConstraint.ts +420 -0
  115. package/src/physics/solver/estimateCollisionResponse.ts +146 -0
  116. package/src/shape/Aabb.ts +400 -0
  117. package/src/shape/Box.ts +231 -0
  118. package/src/shape/Capsule.ts +332 -0
  119. package/src/shape/CompoundShape.ts +288 -0
  120. package/src/shape/Convex.ts +130 -0
  121. package/src/shape/ConvexHull.ts +423 -0
  122. package/src/shape/Cylinder.ts +313 -0
  123. package/src/shape/HeightMap.ts +511 -0
  124. package/src/shape/Line.ts +14 -0
  125. package/src/shape/Plane.ts +116 -0
  126. package/src/shape/Ray.ts +81 -0
  127. package/src/shape/Segment.ts +25 -0
  128. package/src/shape/Shape.ts +77 -0
  129. package/src/shape/Sphere.ts +181 -0
  130. package/src/shape/TransformedShape.ts +51 -0
  131. package/src/shape/Triangle.ts +122 -0
  132. package/src/shape/TriangleMesh.ts +186 -0
  133. package/src/types.ts +1 -0
  134. package/src/world.ts +1335 -0
  135. package/tests/BodyPairsModule.test.ts +71 -0
  136. package/tests/BvhTree.test.ts +406 -0
  137. package/tests/test.md +642 -0
  138. package/tests/vec3.test.ts +12 -0
  139. package/tsconfig.json +20 -0
  140. package/vite.config.js +40 -0
@@ -0,0 +1,1689 @@
1
+ import { PoolClass } from "monomorph";
2
+ import { ClosestPointResult } from "../collision/closestPoints/closestPoints";
3
+ import { Vec3 } from "../math/vec3";
4
+ import { degreesToRadians, squared } from "../math/scalar";
5
+ import { ConvexHullBuilder2d, ConvexHullBuilder2dResult } from "./ConvexHullBuilder2d";
6
+ import { computeClosestPointOnLine } from "../collision/closestPoints/computeClosestPointOnLine";
7
+
8
+ // TODO: this may need to change, not sure if value is too large
9
+ const floatEpsilon = 1e-5;
10
+
11
+ const minTriangleAreaSquared = 1e-12;
12
+
13
+ type Tuple3 = [number, number, number];
14
+
15
+ const calculateNormalAndCentroidy0 = /*@__PURE__*/ Vec3.create();
16
+ const calculateNormalAndCentroidy1 = /*@__PURE__*/ Vec3.create();
17
+ const calculateNormalAndCentroidy2 = /*@__PURE__*/ Vec3.create();
18
+ const calculateNormalAndCentroide0 = /*@__PURE__*/ Vec3.create();
19
+ const calculateNormalAndCentroide1 = /*@__PURE__*/ Vec3.create();
20
+ const calculateNormalAndCentroide2 = /*@__PURE__*/ Vec3.create();
21
+ const calculateNormalAndCentroidshortestEdgeNormal = /*@__PURE__*/ Vec3.create();
22
+ const calculateNormalAndCentroidcentroid = /*@__PURE__*/ Vec3.create();
23
+ const calculateNormalAndCentroidnormal = /*@__PURE__*/ Vec3.create();
24
+
25
+ const isFacingcentroid = /*@__PURE__*/ Vec3.create();
26
+ const isFacingnormal = /*@__PURE__*/ Vec3.create();
27
+ const isFacingvectorFromCentroid = /*@__PURE__*/ Vec3.create();
28
+
29
+ const getFaceForPointcentroid = /*@__PURE__*/ Vec3.create();
30
+ const getFaceForPointnormal = /*@__PURE__*/ Vec3.create();
31
+ const getFaceForPointtempVector = /*@__PURE__*/ Vec3.create();
32
+
33
+ const getDistanceToEdgeSquaredp1 = /*@__PURE__*/ Vec3.create();
34
+ const getDistanceToEdgeSquaredp2 = /*@__PURE__*/ Vec3.create();
35
+ const getDistanceToEdgeSquaredvector12 = /*@__PURE__*/ Vec3.create();
36
+ const getDistanceToEdgeSquaredtempVector = /*@__PURE__*/ Vec3.create();
37
+ const getDistanceToEdgeSquarednormal = /*@__PURE__*/ Vec3.create();
38
+ const getDistanceToEdgeSquaredp1ToPoint = /*@__PURE__*/ Vec3.create();
39
+ const getDistanceToEdgeSquaredclosest = /*@__PURE__*/ ClosestPointResult.create();
40
+ const getDistanceToEdgeSquaredpointToP1 = /*@__PURE__*/ Vec3.create();
41
+ const getDistanceToEdgeSquaredpointToP2 = /*@__PURE__*/ Vec3.create();
42
+
43
+ const assignPointToFacepoint = /*@__PURE__*/ Vec3.create();
44
+
45
+ const determineCoplanarDistancecurrentVertex = /*@__PURE__*/ Vec3.create();
46
+ const determineCoplanarDistancemaxComponents = /*@__PURE__*/ Vec3.create();
47
+
48
+ const buildConvexHullvertexA = /*@__PURE__*/ Vec3.create();
49
+ const buildConvexHullvertexB = /*@__PURE__*/ Vec3.create();
50
+ const buildConvexHullvertexC = /*@__PURE__*/ Vec3.create();
51
+ const buildConvexHullvertexD = /*@__PURE__*/ Vec3.create();
52
+ const buildConvexHullvectorCA = /*@__PURE__*/ Vec3.create();
53
+ const buildConvexHullvectorCB = /*@__PURE__*/ Vec3.create();
54
+ const buildConvexHullvectorAB = /*@__PURE__*/ Vec3.create();
55
+ const buildConvexHullvectorAC = /*@__PURE__*/ Vec3.create();
56
+ const buildConvexHullcrossCA_CB = /*@__PURE__*/ Vec3.create();
57
+ const buildConvexHullinitialPlaneNormal = /*@__PURE__*/ Vec3.create();
58
+ const buildConvexHullinitialPlaneCentroid = /*@__PURE__*/ Vec3.create();
59
+ const buildConvexHulltempVector = /*@__PURE__*/ Vec3.create();
60
+ const buildConvexHullbase1 = /*@__PURE__*/ Vec3.create();
61
+ const buildConvexHullbase2 = /*@__PURE__*/ Vec3.create();
62
+
63
+ const addPointposition = /*@__PURE__*/ Vec3.create();
64
+
65
+ const mergeDegenerateFacenormal = /*@__PURE__*/ Vec3.create();
66
+ const mergeDegenerateFacep1 = /*@__PURE__*/ Vec3.create();
67
+ const mergeDegenerateFacep2 = /*@__PURE__*/ Vec3.create();
68
+
69
+ const mergeCoplanarOrConcaveFacesdeltaCentroid = /*@__PURE__*/ Vec3.create();
70
+ const mergeCoplanarOrConcaveFacesinFaceNormal = /*@__PURE__*/ Vec3.create();
71
+ const mergeCoplanarOrConcaveFacesinFaceCentroid = /*@__PURE__*/ Vec3.create();
72
+ const mergeCoplanarOrConcaveFacesotherFaceNormal = /*@__PURE__*/ Vec3.create();
73
+ const mergeCoplanarOrConcaveFacesotherFaceCentroid = /*@__PURE__*/ Vec3.create();
74
+
75
+ const getCenterOfMassAndVolumevertexA = /*@__PURE__*/ Vec3.create();
76
+ const getCenterOfMassAndVolumevertexB = /*@__PURE__*/ Vec3.create();
77
+ const getCenterOfMassAndVolumevertexC = /*@__PURE__*/ Vec3.create();
78
+ const getCenterOfMassAndVolumevertexD = /*@__PURE__*/ Vec3.create();
79
+ const getCenterOfMassAndVolumevectorDA = /*@__PURE__*/ Vec3.create();
80
+ const getCenterOfMassAndVolumevectorDB = /*@__PURE__*/ Vec3.create();
81
+ const getCenterOfMassAndVolumevectorDC = /*@__PURE__*/ Vec3.create();
82
+ const getCenterOfMassAndVolumecrossDB_DC = /*@__PURE__*/ Vec3.create();
83
+ const getCenterOfMassAndVolumecentroid = /*@__PURE__*/ Vec3.create();
84
+ const getCenterOfMassAndVolumecenterOfMassTetrahedron = /*@__PURE__*/ Vec3.create();
85
+
86
+ const determineMaxError = /*@__PURE__*/ Vec3.create();
87
+ const determineMaxErrornormal = /*@__PURE__*/ Vec3.create();
88
+ const determineMaxErrorcentroid = /*@__PURE__*/ Vec3.create();
89
+ const determineMaxErrortempVector = /*@__PURE__*/ Vec3.create();
90
+
91
+ const faceNormal = /*@__PURE__*/ Vec3.create();
92
+ const neighborNormal = /*@__PURE__*/ Vec3.create();
93
+
94
+ export const enum ConvexHullBuilderResult {
95
+ Success, ///< Hull building finished successfully
96
+ MaxVerticesReached, ///< Hull building finished successfully, but the desired accuracy was not reached because the max vertices limit was reached
97
+ TooFewPoints, ///< Too few points to create a hull
98
+ TooFewFaces, ///< Too few faces in the created hull (signifies precision errors during building)
99
+ Degenerate, ///< Degenerate hull detected
100
+ }
101
+
102
+ class ConvexHullBuilderCoplanar {
103
+ positionIndex: number;
104
+ distanceSquared: number;
105
+
106
+ constructor(positionIndex: number, distanceSquared: number) {
107
+ this.positionIndex = positionIndex;
108
+ this.distanceSquared = distanceSquared;
109
+ }
110
+ }
111
+
112
+ class ConvexHullBuilderEdge {
113
+ face: ConvexHullBuilderFace;
114
+ nextEdge?: ConvexHullBuilderEdge;
115
+ neighbourEdge?: ConvexHullBuilderEdge;
116
+ startIndex: number;
117
+ // isFirst: boolean = false;
118
+
119
+ constructor(face: ConvexHullBuilderFace, startIndex: number) {
120
+ this.face = face;
121
+ this.startIndex = startIndex;
122
+ }
123
+
124
+ getPreviousEdge() {
125
+ let prevEdge: ConvexHullBuilderEdge | undefined = this;
126
+ while (prevEdge?.nextEdge && prevEdge.nextEdge !== this) {
127
+ prevEdge = prevEdge.nextEdge;
128
+ }
129
+ return prevEdge;
130
+ }
131
+ }
132
+
133
+ class ConvexHullBuilderFullEdge {
134
+ startIndex: number;
135
+ endIndex: number;
136
+ neighbourEdge: ConvexHullBuilderEdge;
137
+
138
+ constructor(startIndex: number, endIndex: number, neighbourEdge: ConvexHullBuilderEdge) {
139
+ this.startIndex = startIndex;
140
+ this.endIndex = endIndex;
141
+ this.neighbourEdge = neighbourEdge;
142
+ }
143
+ }
144
+
145
+ export class ConvexHullBuilderFace {
146
+ normal: Tuple3;
147
+ centroid: Tuple3;
148
+ conflictList: number[];
149
+ firstEdge?: ConvexHullBuilderEdge;
150
+ furthestPointDistanceSquared: number = 0;
151
+ removed: boolean = false;
152
+
153
+ indices: Tuple3;
154
+
155
+ constructor() {
156
+ this.normal = [0, 0, 0];
157
+ this.centroid = [0, 0, 0];
158
+ this.conflictList = [];
159
+ this.indices = [0, 0, 0];
160
+ }
161
+
162
+ copy(face: ConvexHullBuilderFace) {
163
+ this.normal[0] = face.normal[0];
164
+ this.normal[1] = face.normal[1];
165
+ this.normal[2] = face.normal[2];
166
+ this.centroid[0] = face.centroid[0];
167
+ this.centroid[1] = face.centroid[1];
168
+ this.centroid[2] = face.centroid[2];
169
+ this.conflictList = face.conflictList.slice();
170
+ this.firstEdge = face.firstEdge;
171
+ this.furthestPointDistanceSquared = face.furthestPointDistanceSquared;
172
+ this.removed = face.removed;
173
+ this.indices[0] = face.indices[0];
174
+ this.indices[1] = face.indices[1];
175
+ this.indices[2] = face.indices[2];
176
+ }
177
+
178
+ initialize(index0: number, index1: number, index2: number, polyhedron: PoolClass<Vec3>) {
179
+ if (index0 === index1 || index0 === index2 || index1 === index2) {
180
+ throw new Error("Indices must be unique");
181
+ }
182
+
183
+ // Create 3 edges
184
+ const edge0 = new ConvexHullBuilderEdge(this, index0);
185
+ const edge1 = new ConvexHullBuilderEdge(this, index1);
186
+ const edge2 = new ConvexHullBuilderEdge(this, index2);
187
+
188
+ // Link edges
189
+ edge0.nextEdge = edge1;
190
+ edge1.nextEdge = edge2;
191
+ edge2.nextEdge = edge0;
192
+ this.firstEdge = edge0;
193
+
194
+ this.calculateNormalAndCentroid(polyhedron);
195
+
196
+ this.indices[0] = index0;
197
+ this.indices[1] = index1;
198
+ this.indices[2] = index2;
199
+ }
200
+
201
+ calculateNormalAndCentroid(polyhedron: PoolClass<Vec3>) {
202
+ // Get point that we use to construct a triangle fan
203
+ let edge = this.firstEdge!;
204
+ calculateNormalAndCentroidy0.copy(polyhedron.array[edge.startIndex]);
205
+
206
+ // Get the 2nd point
207
+ edge = edge.nextEdge!;
208
+ calculateNormalAndCentroidy1.copy(polyhedron.array[edge.startIndex]);
209
+
210
+ // Start accumulating the centroid
211
+ // centroid.zero();
212
+ calculateNormalAndCentroidcentroid.addVectors(calculateNormalAndCentroidy0, calculateNormalAndCentroidy1);
213
+ let n = 2;
214
+
215
+ // Start accumulating the normal
216
+ calculateNormalAndCentroidnormal.zero();
217
+
218
+ // Loop over remaining edges accumulating normals in a triangle fan fashion
219
+ for (edge = edge.nextEdge!; edge !== this.firstEdge; edge = edge.nextEdge!) {
220
+ // Get the 3rd point
221
+ calculateNormalAndCentroidy2.copy(polyhedron.array[edge.startIndex]);
222
+
223
+ // Calculate edges (counter clockwise)
224
+ calculateNormalAndCentroide0.subtractVectors(calculateNormalAndCentroidy1, calculateNormalAndCentroidy0);
225
+ calculateNormalAndCentroide1.subtractVectors(calculateNormalAndCentroidy2, calculateNormalAndCentroidy1);
226
+ calculateNormalAndCentroide2.subtractVectors(calculateNormalAndCentroidy0, calculateNormalAndCentroidy2);
227
+
228
+ // The best normal is calculated by using the two shortest edges
229
+ // See: https://box2d.org/posts/2014/01/troublesome-triangle/
230
+ // The difference in normals is most pronounced when one edge is much smaller than the others (in which case the others must have roughly the same length).
231
+ // Therefore we can suffice by just picking the shortest from 2 edges and use that with the 3rd edge to calculate the normal.
232
+
233
+ if (
234
+ calculateNormalAndCentroide1.dot(calculateNormalAndCentroide1) <
235
+ calculateNormalAndCentroide2.dot(calculateNormalAndCentroide2)
236
+ ) {
237
+ calculateNormalAndCentroidshortestEdgeNormal.crossVectors(
238
+ calculateNormalAndCentroide0,
239
+ calculateNormalAndCentroide1
240
+ );
241
+ } else {
242
+ calculateNormalAndCentroidshortestEdgeNormal.crossVectors(
243
+ calculateNormalAndCentroide2,
244
+ calculateNormalAndCentroide0
245
+ );
246
+ }
247
+
248
+ // Accumulate normal
249
+ calculateNormalAndCentroidnormal.addVectors(
250
+ calculateNormalAndCentroidnormal,
251
+ calculateNormalAndCentroidshortestEdgeNormal
252
+ );
253
+
254
+ // Accumulate centroid
255
+ calculateNormalAndCentroidcentroid.addVectors(calculateNormalAndCentroidcentroid, calculateNormalAndCentroidy2);
256
+ n++;
257
+
258
+ // Update y1 for next triangle
259
+ calculateNormalAndCentroidy1.copy(calculateNormalAndCentroidy2);
260
+ }
261
+
262
+ // Finalize normal, TODO: this is not done in the reference implementation
263
+ calculateNormalAndCentroidnormal.normalizeVector(calculateNormalAndCentroidnormal);
264
+
265
+ // Finalize centroid
266
+ calculateNormalAndCentroidcentroid.scaleVector(calculateNormalAndCentroidcentroid, 1 / n);
267
+
268
+ // update centroid and normal data
269
+ this.centroid[0] = calculateNormalAndCentroidcentroid.x;
270
+ this.centroid[1] = calculateNormalAndCentroidcentroid.y;
271
+ this.centroid[2] = calculateNormalAndCentroidcentroid.z;
272
+ this.normal[0] = calculateNormalAndCentroidnormal.x;
273
+ this.normal[1] = calculateNormalAndCentroidnormal.y;
274
+ this.normal[2] = calculateNormalAndCentroidnormal.z;
275
+ }
276
+
277
+ isFacing(point: Vec3) {
278
+ isFacingcentroid.x = this.centroid[0];
279
+ isFacingcentroid.y = this.centroid[1];
280
+ isFacingcentroid.z = this.centroid[2];
281
+ isFacingnormal.x = this.normal[0];
282
+ isFacingnormal.y = this.normal[1];
283
+ isFacingnormal.z = this.normal[2];
284
+
285
+ isFacingvectorFromCentroid.subtractVectors(point, isFacingcentroid);
286
+ return isFacingnormal.dot(isFacingvectorFromCentroid) > 0;
287
+ }
288
+ }
289
+
290
+ export class ConvexHullBuilder3d {
291
+ polyhedron?: PoolClass<Vec3>;
292
+ coplanarList: ConvexHullBuilderCoplanar[];
293
+ faces: ConvexHullBuilderFace[];
294
+
295
+ constructor() {
296
+ this.coplanarList = [];
297
+ this.faces = [];
298
+ }
299
+
300
+ freeFaces() {
301
+ this.faces.length = 0;
302
+ }
303
+
304
+ getFaceForPoint(point: Vec3, faces: ConvexHullBuilderFace[]): [ConvexHullBuilderFace | undefined, number] {
305
+ let outFace = undefined;
306
+ let outDistanceSquared = 0.0;
307
+
308
+ for (const face of faces) {
309
+ if (!face.removed) {
310
+ // Determine distance to face
311
+ getFaceForPointcentroid.x = face.centroid[0];
312
+ getFaceForPointcentroid.y = face.centroid[1];
313
+ getFaceForPointcentroid.z = face.centroid[2];
314
+ getFaceForPointnormal.x = face.normal[0];
315
+ getFaceForPointnormal.y = face.normal[1];
316
+ getFaceForPointnormal.z = face.normal[2];
317
+ getFaceForPointtempVector.subtractVectors(point, getFaceForPointcentroid);
318
+ const dot = getFaceForPointnormal.dot(getFaceForPointtempVector);
319
+ if (dot > 0.0) {
320
+ const distanceSquared = (dot * dot) / getFaceForPointnormal.squaredLength();
321
+ if (distanceSquared > outDistanceSquared) {
322
+ outFace = face;
323
+ outDistanceSquared = distanceSquared;
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ return [outFace, outDistanceSquared];
330
+ }
331
+
332
+ getDistanceToEdgeSquared(point: Vec3, face: ConvexHullBuilderFace): number {
333
+ let allInside = true;
334
+ let edgeDistanceSquared = Infinity;
335
+
336
+ // Test if it is inside the edges of the polygon
337
+ let edge = face.firstEdge!;
338
+
339
+ getDistanceToEdgeSquaredp1.copy(this.polyhedron!.array[edge.getPreviousEdge().startIndex]);
340
+
341
+ do {
342
+ getDistanceToEdgeSquaredp2.copy(this.polyhedron!.array[edge.startIndex]);
343
+
344
+ getDistanceToEdgeSquarednormal.x = face.normal[0];
345
+ getDistanceToEdgeSquarednormal.y = face.normal[1];
346
+ getDistanceToEdgeSquarednormal.z = face.normal[2];
347
+
348
+ getDistanceToEdgeSquaredvector12.subtractVectors(getDistanceToEdgeSquaredp2, getDistanceToEdgeSquaredp1);
349
+ getDistanceToEdgeSquaredp1ToPoint.subtractVectors(point, getDistanceToEdgeSquaredp1);
350
+ getDistanceToEdgeSquaredtempVector.crossVectors(
351
+ getDistanceToEdgeSquaredvector12,
352
+ getDistanceToEdgeSquaredp1ToPoint
353
+ );
354
+ if (getDistanceToEdgeSquaredtempVector.dot(getDistanceToEdgeSquarednormal) < 0.0) {
355
+ // It is outside
356
+ allInside = false;
357
+
358
+ // Measure distance to this edge
359
+ getDistanceToEdgeSquaredpointToP1.subtractVectors(getDistanceToEdgeSquaredp1, point);
360
+ getDistanceToEdgeSquaredpointToP2.subtractVectors(getDistanceToEdgeSquaredp2, point);
361
+ computeClosestPointOnLine(
362
+ getDistanceToEdgeSquaredclosest,
363
+ getDistanceToEdgeSquaredpointToP1,
364
+ getDistanceToEdgeSquaredpointToP2
365
+ );
366
+ edgeDistanceSquared = Math.min(edgeDistanceSquared, getDistanceToEdgeSquaredclosest.point.squaredLength());
367
+ }
368
+ getDistanceToEdgeSquaredp1.copy(getDistanceToEdgeSquaredp2);
369
+ edge = edge.nextEdge!;
370
+ } while (edge !== face.firstEdge);
371
+
372
+ return allInside ? 0.0 : edgeDistanceSquared;
373
+ }
374
+
375
+ assignPointToFace(
376
+ polyhedron: PoolClass<Vec3>,
377
+ index: number,
378
+ faces: ConvexHullBuilderFace[],
379
+ toleranceSquared: number
380
+ ) {
381
+ assignPointToFacepoint.copy(polyhedron.array[index]);
382
+
383
+ // Find the face for which the point is furthest away
384
+ const [bestFace, bestDistanceSquared] = this.getFaceForPoint(assignPointToFacepoint, faces);
385
+
386
+ if (bestFace) {
387
+ // Check if this point is within the tolerance margin to the plane
388
+ if (bestDistanceSquared <= toleranceSquared) {
389
+ // Check distance to edges
390
+ const distanceToEdgeSquared = this.getDistanceToEdgeSquared(assignPointToFacepoint, bestFace);
391
+ if (distanceToEdgeSquared > toleranceSquared) {
392
+ // Point is outside of the face and too far away to discard
393
+ this.coplanarList.push(new ConvexHullBuilderCoplanar(index, distanceToEdgeSquared));
394
+ }
395
+ } else {
396
+ // This point is in front of the face, add it to the conflict list
397
+ if (bestDistanceSquared > bestFace.furthestPointDistanceSquared) {
398
+ // This point is futher away than any others, update the distance and add point as last point
399
+ bestFace.furthestPointDistanceSquared = bestDistanceSquared;
400
+ bestFace.conflictList.push(index);
401
+ } else {
402
+ // Not the furthest point, add it as the before last point
403
+ bestFace.conflictList.push(bestFace.conflictList[bestFace.conflictList.length - 1]);
404
+ bestFace.conflictList[bestFace.conflictList.length - 2] = index;
405
+ }
406
+
407
+ return true;
408
+ }
409
+ }
410
+
411
+ return false;
412
+ }
413
+
414
+ determineCoplanarDistance() {
415
+ // Formula as per: Implementing Quickhull - Dirk Gregorius.
416
+
417
+ determineCoplanarDistancemaxComponents.zero();
418
+
419
+ for (let i = 0; i < this.polyhedron!.length; ++i) {
420
+ determineCoplanarDistancecurrentVertex.copy(this.polyhedron!.array[i]);
421
+ determineCoplanarDistancemaxComponents.x = Math.max(
422
+ determineCoplanarDistancemaxComponents.x,
423
+ Math.abs(determineCoplanarDistancecurrentVertex.x)
424
+ );
425
+ determineCoplanarDistancemaxComponents.y = Math.max(
426
+ determineCoplanarDistancemaxComponents.y,
427
+ Math.abs(determineCoplanarDistancecurrentVertex.y)
428
+ );
429
+ determineCoplanarDistancemaxComponents.z = Math.max(
430
+ determineCoplanarDistancemaxComponents.z,
431
+ Math.abs(determineCoplanarDistancecurrentVertex.z)
432
+ );
433
+ }
434
+
435
+ return (
436
+ 3.0 *
437
+ floatEpsilon *
438
+ (determineCoplanarDistancemaxComponents.x +
439
+ determineCoplanarDistancemaxComponents.y +
440
+ determineCoplanarDistancemaxComponents.z)
441
+ );
442
+ }
443
+
444
+ getNumberOfVerticesUsed() {
445
+ const usedVertices = new Set<number>();
446
+ for (const face of this.faces) {
447
+ let edge = face.firstEdge!;
448
+ do {
449
+ usedVertices.add(edge.startIndex);
450
+ edge = edge.nextEdge!;
451
+ } while (edge !== face.firstEdge);
452
+ }
453
+ return usedVertices.size;
454
+ }
455
+
456
+ // TODO this is unused/untested, i may have broken it - Hamza
457
+ containsFace(inIndices: number[]): boolean {
458
+ for (const face of this.faces) {
459
+ let edge = face.firstEdge;
460
+ let index = inIndices.indexOf(edge!.startIndex);
461
+ // let startIndex = inIndices.indexOf(edge!.startIndex);
462
+ if (index !== inIndices[inIndices.length - 1]) {
463
+ let matches = 0;
464
+ // let currentIndex = startIndex;
465
+
466
+ do {
467
+ // Check if index matches
468
+ if (index !== edge!.startIndex) {
469
+ break;
470
+ }
471
+
472
+ // Increment number of matches
473
+ matches++;
474
+
475
+ index++;
476
+
477
+ if (index === inIndices[inIndices.length - 1]) {
478
+ index = inIndices[0];
479
+ }
480
+
481
+ // Next index in list of inIndices
482
+ // currentIndex = (currentIndex + 1) % inIndices.length;
483
+
484
+ // Next edge
485
+ edge = edge!.nextEdge;
486
+ } while (edge !== face.firstEdge);
487
+
488
+ if (matches === inIndices.length) {
489
+ return true;
490
+ }
491
+ }
492
+ }
493
+
494
+ return false;
495
+ }
496
+
497
+ buildConvexHull(
498
+ polyhedron: PoolClass<Vec3>,
499
+ positions2d: PoolClass<Vec3>,
500
+ tolerance: number,
501
+ maxVertexCount: number,
502
+ maxAngleBetweenAdjacentFacesDegrees: number = 1
503
+ ): ConvexHullBuilderResult {
504
+ this.polyhedron = polyhedron;
505
+
506
+ // TODO: cleanup if needed
507
+ // Free the faces possibly left over from an earlier hull
508
+ this.freeFaces();
509
+
510
+ // Test that we have at least 3 points
511
+ if (polyhedron.length < 3) {
512
+ throw new Error("Need at least 3 points to make a hull");
513
+ return ConvexHullBuilderResult.TooFewPoints;
514
+ }
515
+
516
+ // Determine a suitable tolerance for detecting that points are coplanar
517
+ const coplanarToleranceSquared = squared(this.determineCoplanarDistance());
518
+
519
+ // Increase desired tolerance if accuracy doesn't allow it
520
+ const toleranceSquared = Math.max(coplanarToleranceSquared, squared(tolerance));
521
+
522
+ // Find point furthest from the origin
523
+ let index1 = -1;
524
+ let maxDistanceSquared = -1.0;
525
+ for (let i = 0; i < polyhedron.length; ++i) {
526
+ buildConvexHullvertexA.copy(polyhedron.array[i]);
527
+ let distanceSquared = buildConvexHullvertexA.squaredLength();
528
+ if (distanceSquared > maxDistanceSquared) {
529
+ maxDistanceSquared = distanceSquared;
530
+ index1 = i;
531
+ }
532
+ }
533
+
534
+ if (index1 < 0) {
535
+ throw new Error("No vertex found");
536
+ }
537
+
538
+ buildConvexHullvertexA.copy(polyhedron.array[index1]);
539
+
540
+ // Find point that is furthest away from this point
541
+ let index2 = -1;
542
+ maxDistanceSquared = -1.0;
543
+ for (let i = 0; i < polyhedron.length; ++i) {
544
+ if (i === index1) {
545
+ continue;
546
+ }
547
+
548
+ buildConvexHullvertexB.copy(polyhedron.array[i]);
549
+ let distanceSquared = buildConvexHullvertexA.squaredDistance(buildConvexHullvertexB);
550
+ if (distanceSquared > maxDistanceSquared) {
551
+ maxDistanceSquared = distanceSquared;
552
+ index2 = i;
553
+ }
554
+ }
555
+
556
+ if (index2 < 0) {
557
+ throw new Error("No vertex found");
558
+ }
559
+
560
+ buildConvexHullvertexB.copy(polyhedron.array[index2]);
561
+
562
+ // Find point that forms the biggest triangle
563
+ let index3 = -1;
564
+ let bestTriangleAreaSquared = -1.0;
565
+ for (let i = 0; i < polyhedron.length; ++i) {
566
+ if (i === index1 || i === index2) {
567
+ continue;
568
+ }
569
+
570
+ buildConvexHullvertexC.copy(polyhedron.array[i]);
571
+ buildConvexHullvectorCA.subtractVectors(buildConvexHullvertexA, buildConvexHullvertexC);
572
+ buildConvexHullvectorCB.subtractVectors(buildConvexHullvertexB, buildConvexHullvertexC);
573
+ buildConvexHullcrossCA_CB.crossVectors(buildConvexHullvectorCA, buildConvexHullvectorCB);
574
+ const triangleAreaSquared = buildConvexHullcrossCA_CB.squaredLength();
575
+
576
+ if (triangleAreaSquared > bestTriangleAreaSquared) {
577
+ bestTriangleAreaSquared = triangleAreaSquared;
578
+ index3 = i;
579
+ }
580
+ }
581
+
582
+ if (index3 < 0) {
583
+ throw new Error("No vertex found");
584
+ }
585
+
586
+ buildConvexHullvertexC.copy(polyhedron.array[index3]);
587
+
588
+ if (bestTriangleAreaSquared < minTriangleAreaSquared) {
589
+ throw new Error("Could not find a suitable initial triangle because its area was too small");
590
+ return ConvexHullBuilderResult.Degenerate;
591
+ }
592
+
593
+ // Check if we have only 3 vertices
594
+ if (polyhedron.length === 3) {
595
+ // Create two triangles (back to back)
596
+ const triangle1 = this.createTriangle(index1, index2, index3);
597
+ const triangle2 = this.createTriangle(index1, index3, index2);
598
+
599
+ // Link faces edges
600
+ this.linkFace(triangle1.firstEdge!, triangle2.firstEdge!.nextEdge!.nextEdge!);
601
+ this.linkFace(triangle1.firstEdge!.nextEdge!, triangle2.firstEdge!.nextEdge!);
602
+ this.linkFace(triangle1.firstEdge!.nextEdge!.nextEdge!, triangle2.firstEdge!);
603
+
604
+ return ConvexHullBuilderResult.Success;
605
+ }
606
+
607
+ // Find point that forms the biggest tetrahedron
608
+ buildConvexHullvectorAB.subtractVectors(buildConvexHullvertexB, buildConvexHullvertexA);
609
+ buildConvexHullvectorAC.subtractVectors(buildConvexHullvertexC, buildConvexHullvertexA);
610
+
611
+ buildConvexHullinitialPlaneNormal.crossVectors(buildConvexHullvectorAB, buildConvexHullvectorAC);
612
+ buildConvexHullinitialPlaneNormal.normalizeVector(buildConvexHullinitialPlaneNormal);
613
+
614
+ buildConvexHullinitialPlaneCentroid.addVectors(buildConvexHullvertexA, buildConvexHullvertexB);
615
+ buildConvexHullinitialPlaneCentroid.addVectors(buildConvexHullinitialPlaneCentroid, buildConvexHullvertexC);
616
+ buildConvexHullinitialPlaneCentroid.scaleVector(buildConvexHullinitialPlaneCentroid, 1 / 3);
617
+
618
+ let index4 = -1;
619
+ let maxDistance = 0.0;
620
+ for (let i = 0; i < polyhedron.length; ++i) {
621
+ if (i === index1 || i === index2 || i === index3) {
622
+ continue;
623
+ }
624
+
625
+ buildConvexHullvertexD.copy(polyhedron.array[i]);
626
+ buildConvexHulltempVector.subtractVectors(buildConvexHullvertexD, buildConvexHullinitialPlaneCentroid);
627
+ const distance = buildConvexHulltempVector.dot(buildConvexHullinitialPlaneNormal);
628
+ if (Math.abs(distance) > Math.abs(maxDistance)) {
629
+ maxDistance = distance;
630
+ index4 = i;
631
+ }
632
+ }
633
+
634
+ if (index4 < 0) {
635
+ throw new Error("No vertex found");
636
+ }
637
+
638
+ buildConvexHullvertexD.copy(polyhedron.array[index4]);
639
+
640
+ // Check if the hull is coplanar
641
+ if (squared(maxDistance) <= 25.0 * coplanarToleranceSquared) {
642
+ // First project all points in 2D space
643
+ buildConvexHullbase1.computeNormalizedPerpendicular(buildConvexHullinitialPlaneNormal);
644
+ buildConvexHullbase2.crossVectors(buildConvexHullinitialPlaneNormal, buildConvexHullbase1);
645
+
646
+ // const positions2d = this.world.allocate(Polyhedron, {
647
+ // maxCount: 1,
648
+ // extraSize: 3 * polyhedron.vertices.length,
649
+ // });
650
+ // positions2d.add();
651
+ // const offset2d = positions2d.fullOffset;
652
+
653
+ for (let i = 0; i < polyhedron.length; ++i) {
654
+ buildConvexHulltempVector.copy(polyhedron.array[i]);
655
+ positions2d.array[i].x = buildConvexHullbase1.dot(buildConvexHulltempVector); // x
656
+ positions2d.array[i].y = buildConvexHullbase2.dot(buildConvexHulltempVector); // y
657
+ positions2d.array[i].z = 0.0; // z
658
+ // positions2d.structsBufferFloat64[offset2d + i * 3 + 0] = base1.dot(tempVector);
659
+ // positions2d.structsBufferFloat64[offset2d + i * 3 + 1] = base2.dot(tempVector);
660
+ // positions2d.structsBufferFloat64[offset2d + i * 3 + 2] = 0.0;
661
+ }
662
+
663
+ // Build hull
664
+ const edges2d: number[] = [];
665
+ const builder2d = new ConvexHullBuilder2d();
666
+ const result = builder2d.buildConvexHull(edges2d, index1, index2, index3, positions2d, tolerance, maxVertexCount);
667
+
668
+ // Create faces (back to back)
669
+ const face1 = this.createFace();
670
+ const face2 = this.createFace();
671
+
672
+ // Create edges for face 1
673
+ const edgesF1: ConvexHullBuilderEdge[] = [];
674
+ for (const startIndex of edges2d) {
675
+ const edge = new ConvexHullBuilderEdge(face1, startIndex);
676
+ if (edgesF1.length === 0) {
677
+ face1.firstEdge = edge;
678
+ } else {
679
+ edgesF1[edgesF1.length - 1].nextEdge = edge;
680
+ }
681
+ edgesF1.push(edge);
682
+ }
683
+ edgesF1[edgesF1.length - 1].nextEdge = face1.firstEdge;
684
+
685
+ // Create edges for face 2
686
+ const edgesF2: ConvexHullBuilderEdge[] = [];
687
+ for (let i = edges2d.length - 1; i >= 0; --i) {
688
+ const edge = new ConvexHullBuilderEdge(face2, edges2d[i]);
689
+ if (edgesF2.length === 0) {
690
+ face2.firstEdge = edge;
691
+ } else {
692
+ edgesF2[edgesF2.length - 1].nextEdge = edge;
693
+ }
694
+ edgesF2.push(edge);
695
+ }
696
+ edgesF2[edgesF2.length - 1].nextEdge = face2.firstEdge;
697
+
698
+ // Link edges
699
+ for (let i = 0; i < edges2d.length; ++i) {
700
+ this.linkFace(edgesF1[i], edgesF2[(2 * edges2d.length - 2 - i) % edges2d.length]);
701
+ }
702
+
703
+ // Calculate the plane for both faces
704
+ face1.calculateNormalAndCentroid(polyhedron);
705
+ face2.normal[0] = -face1.normal[0];
706
+ face2.normal[1] = -face1.normal[1];
707
+ face2.normal[2] = -face1.normal[2];
708
+ face2.centroid[0] = face1.centroid[0];
709
+ face2.centroid[1] = face1.centroid[1];
710
+ face2.centroid[2] = face1.centroid[2];
711
+
712
+ return result === ConvexHullBuilder2dResult.MaxVerticesReached
713
+ ? ConvexHullBuilderResult.MaxVerticesReached
714
+ : ConvexHullBuilderResult.Success;
715
+ }
716
+
717
+ // Ensure the planes are facing outwards
718
+ if (maxDistance < 0.0) {
719
+ let temp = index2;
720
+ index2 = index3;
721
+ index3 = temp;
722
+ }
723
+
724
+ // Create tetrahedron
725
+ const face1 = this.createTriangle(index1, index2, index4);
726
+ const face2 = this.createTriangle(index2, index3, index4);
727
+ const face3 = this.createTriangle(index3, index1, index4);
728
+ const face4 = this.createTriangle(index1, index3, index2);
729
+
730
+ // Link face edges
731
+ this.linkFace(face1.firstEdge!, face4.firstEdge!.nextEdge!.nextEdge!);
732
+ this.linkFace(face1.firstEdge!.nextEdge!, face2.firstEdge!.nextEdge!.nextEdge!);
733
+ this.linkFace(face1.firstEdge!.nextEdge!.nextEdge!, face3.firstEdge!.nextEdge!);
734
+ this.linkFace(face2.firstEdge!, face4.firstEdge!.nextEdge!);
735
+ this.linkFace(face2.firstEdge!.nextEdge!, face3.firstEdge!.nextEdge!.nextEdge!);
736
+ this.linkFace(face3.firstEdge!, face4.firstEdge!);
737
+
738
+ // Build the initial conflict lists
739
+ const faces: ConvexHullBuilderFace[] = [face1, face2, face3, face4];
740
+ for (let index = 0; index < polyhedron.length; ++index) {
741
+ if (index !== index1 && index !== index2 && index !== index3 && index !== index4) {
742
+ this.assignPointToFace(polyhedron, index, faces, toleranceSquared);
743
+ }
744
+ }
745
+
746
+ // Overestimate of the actual amount of vertices we use, for limiting the amount of vertices in the hull
747
+ let numVerticesUsed = 4;
748
+
749
+ // Loop through the remainder of the points and add them
750
+ while (true) {
751
+ // Find the face with the furthest point on it
752
+ let faceWithFurthestPoint: ConvexHullBuilderFace | undefined = undefined;
753
+ let furthestDistanceSquared = 0.0;
754
+ for (const face of this.faces) {
755
+ if (face.furthestPointDistanceSquared > furthestDistanceSquared) {
756
+ furthestDistanceSquared = face.furthestPointDistanceSquared;
757
+ faceWithFurthestPoint = face;
758
+ }
759
+ }
760
+
761
+ let furthestPointIndex: number | undefined = undefined;
762
+ if (faceWithFurthestPoint) {
763
+ // Take the furthest point
764
+ furthestPointIndex = faceWithFurthestPoint.conflictList.pop();
765
+ } else if (this.coplanarList.length !== 0) {
766
+ // Try to assign points to faces (this also recalculates the distance to the hull for the coplanar vertices)
767
+ let coplanarList: ConvexHullBuilderCoplanar[] = [];
768
+ // TODO: is this equivalent to the swap?
769
+ [this.coplanarList, coplanarList] = [coplanarList, this.coplanarList];
770
+ // this does a deep copy (not fully recursive, but enough for our purposes)... i believe this is equivalent to the swap here
771
+ // coplanarList = this.coplanarList.map(coplanar => ({ ...coplanar }));
772
+
773
+ let added = false;
774
+ for (const coplanarObject of coplanarList) {
775
+ added =
776
+ this.assignPointToFace(polyhedron, coplanarObject.positionIndex, this.faces, toleranceSquared) || added;
777
+ }
778
+
779
+ // If we were able to assign a point, loop again to pick it up
780
+ if (added) {
781
+ continue;
782
+ }
783
+
784
+ // If the coplanar list is empty, there are no points left and we're done
785
+ if (this.coplanarList.length === 0) {
786
+ break;
787
+ }
788
+
789
+ do {
790
+ // Find the vertex that is furthest from the hull
791
+ let bestIndex = 0;
792
+ let bestDistanceSquared = this.coplanarList[0].distanceSquared;
793
+ for (let i = 1; i < this.coplanarList.length; ++i) {
794
+ const coplanarObject = this.coplanarList[i];
795
+ if (coplanarObject.distanceSquared > bestDistanceSquared) {
796
+ bestIndex = i;
797
+ bestDistanceSquared = coplanarObject.distanceSquared;
798
+ }
799
+ }
800
+
801
+ // Swap it to the end
802
+ const temp = this.coplanarList[bestIndex];
803
+ this.coplanarList[bestIndex] = this.coplanarList[this.coplanarList.length - 1];
804
+ this.coplanarList[this.coplanarList.length - 1] = temp;
805
+
806
+ // Remove it
807
+ furthestPointIndex = this.coplanarList.pop()?.positionIndex;
808
+
809
+ // Find the face for which the point is furthest away
810
+ if (furthestPointIndex) {
811
+ buildConvexHulltempVector.copy(polyhedron.array[furthestPointIndex]);
812
+ [faceWithFurthestPoint, bestDistanceSquared] = this.getFaceForPoint(buildConvexHulltempVector, this.faces);
813
+ }
814
+ } while (this.coplanarList.length !== 0 && !faceWithFurthestPoint);
815
+
816
+ if (!faceWithFurthestPoint) {
817
+ break;
818
+ }
819
+ } else {
820
+ // If there are no more vertices, we're done
821
+ break;
822
+ }
823
+
824
+ // Check if we have a limit on the max vertices that we should produce
825
+ if (numVerticesUsed >= maxVertexCount) {
826
+ // Count the actual amount of used vertices (we did not take the removal of any vertices into account)
827
+ numVerticesUsed = this.getNumberOfVerticesUsed();
828
+
829
+ // Check if there are too many
830
+ if (numVerticesUsed >= maxVertexCount) {
831
+ return ConvexHullBuilderResult.MaxVerticesReached;
832
+ }
833
+ }
834
+
835
+ // We're about to add another vertex
836
+ ++numVerticesUsed;
837
+
838
+ // Add the point to the hull
839
+ const newFaces: ConvexHullBuilderFace[] = [];
840
+ this.addPoint(faceWithFurthestPoint, furthestPointIndex!, coplanarToleranceSquared, newFaces);
841
+
842
+ // Redistribute points on conflict lists belonging to removed faces
843
+ for (const face of this.faces) {
844
+ if (face.removed) {
845
+ for (const index of face.conflictList) {
846
+ this.assignPointToFace(polyhedron, index, newFaces, toleranceSquared);
847
+ }
848
+ }
849
+ }
850
+
851
+ // Permanently delete faces that we removed in AddPoint()
852
+ this.garbageCollectFaces();
853
+ }
854
+
855
+ // this.mergeSimilarFaces(maxAngleBetweenAdjacentFacesDegrees);
856
+
857
+ // Check if we are left with a hull. It is possible that hull building fails if the points are nearly coplanar.
858
+ if (this.faces.length < 2) {
859
+ throw new Error("Too few faces in hull");
860
+ return ConvexHullBuilderResult.TooFewFaces;
861
+ }
862
+
863
+ return ConvexHullBuilderResult.Success;
864
+ }
865
+
866
+ mergeSimilarFaces(maxAngleBetweenAdjacentFaces: number) {
867
+ const threshold = Math.cos(degreesToRadians(maxAngleBetweenAdjacentFaces));
868
+
869
+ for (const face of this.faces) {
870
+ if (face.removed) continue;
871
+ let edge = face.firstEdge!;
872
+ do {
873
+ const neighbor = edge.neighbourEdge?.face;
874
+ if (!neighbor || neighbor.removed) {
875
+ edge = edge.nextEdge!;
876
+ continue;
877
+ }
878
+
879
+ faceNormal.fromArray(face.normal);
880
+ neighborNormal.fromArray(neighbor.normal);
881
+
882
+ if (faceNormal.dot(neighborNormal) >= threshold) {
883
+ this.mergeFaces(edge);
884
+ }
885
+
886
+ edge = edge.nextEdge!;
887
+ } while (edge !== face.firstEdge);
888
+ }
889
+
890
+ this.garbageCollectFaces();
891
+ }
892
+
893
+ addPoint(
894
+ inFacingFace: ConvexHullBuilderFace,
895
+ inIndex: number,
896
+ inCoplanarToleranceSquared: number,
897
+ outNewFaces: ConvexHullBuilderFace[]
898
+ ): void {
899
+ // Get position
900
+ addPointposition.copy(this.polyhedron!.array[inIndex]);
901
+
902
+ this.validateFaces();
903
+
904
+ // Find edge of convex hull of faces that are not facing the new vertex
905
+ const edges: ConvexHullBuilderFullEdge[] = [];
906
+ this.findEdge(inFacingFace, addPointposition, edges);
907
+ if (edges.length < 3) {
908
+ // debugger;
909
+ throw new Error("Not enough edges found");
910
+ }
911
+
912
+ // Create new faces
913
+ outNewFaces.length = 0;
914
+ for (const edge of edges) {
915
+ if (edge.startIndex === edge.endIndex) {
916
+ throw new Error("Start and end index are the same");
917
+ }
918
+
919
+ const face = this.createTriangle(edge.startIndex, edge.endIndex, inIndex);
920
+ outNewFaces.push(face);
921
+ }
922
+
923
+ // Link edges
924
+ for (let i = 0; i < outNewFaces.length; ++i) {
925
+ // debugger;
926
+ this.linkFace(outNewFaces[i].firstEdge!, edges[i].neighbourEdge!);
927
+ this.linkFace(
928
+ outNewFaces[i].firstEdge!.nextEdge!,
929
+ outNewFaces[(i + 1) % outNewFaces.length].firstEdge!.nextEdge!.nextEdge!
930
+ );
931
+ }
932
+
933
+ // Loop on faces that were modified until nothing needs to be checked anymore
934
+ const affectedFaces: ConvexHullBuilderFace[] = [...outNewFaces];
935
+ while (affectedFaces.length !== 0) {
936
+ // Take the next face
937
+ const face = affectedFaces.pop()!;
938
+
939
+ if (face.removed) {
940
+ continue;
941
+ }
942
+
943
+ // Merge with neighbour if this is a degenerate face
944
+ this.mergeDegenerateFace(face, affectedFaces);
945
+
946
+ // Merge with coplanar neighbours (or when the neighbour forms a concave edge)
947
+ if (face.removed) {
948
+ continue;
949
+ }
950
+ this.mergeCoplanarOrConcaveFaces(face, inCoplanarToleranceSquared, affectedFaces);
951
+ }
952
+
953
+ this.validateFaces();
954
+ }
955
+
956
+ garbageCollectFaces(): void {
957
+ for (let i = this.faces.length - 1; i >= 0; --i) {
958
+ const face = this.faces[i];
959
+ if (face.removed) {
960
+ this.freeFace(face);
961
+ this.faces.splice(i, 1);
962
+ }
963
+ }
964
+ }
965
+
966
+ createFace(): ConvexHullBuilderFace {
967
+ // Call provider to create face
968
+ const face = new ConvexHullBuilderFace();
969
+
970
+ // Add to list
971
+ this.faces.push(face);
972
+ return face;
973
+ }
974
+
975
+ createTriangle(index0: number, index1: number, index2: number) {
976
+ const face = this.createFace();
977
+ face.initialize(index0, index1, index2, this.polyhedron!);
978
+ return face;
979
+ }
980
+
981
+ freeFace(inFace: ConvexHullBuilderFace): void {
982
+ // #v-ifdef DEV
983
+ if (!inFace.removed) {
984
+ throw new Error("Face should be removed");
985
+ }
986
+ // #v-endif
987
+
988
+ // Make sure that this face is not connected
989
+ let e = inFace.firstEdge;
990
+ if (e !== undefined) {
991
+ do {
992
+ // #v-ifdef DEV
993
+ if (e!.neighbourEdge !== undefined) {
994
+ throw new Error("Face should not have neighbours");
995
+ }
996
+ // #v-endif
997
+ e = e!.nextEdge;
998
+ } while (e !== inFace.firstEdge);
999
+ }
1000
+
1001
+ // Free the face
1002
+ // delete inFace; TODO: may not need to delete anything
1003
+ }
1004
+
1005
+ linkFace(edge1: ConvexHullBuilderEdge, edge2: ConvexHullBuilderEdge) {
1006
+ // Check not connected yet
1007
+ // #v-ifdef DEV
1008
+ if (edge1.neighbourEdge !== undefined) {
1009
+ throw new Error("Edge 1 already connected");
1010
+ }
1011
+ // #v-endif
1012
+
1013
+ // #v-ifdef DEV
1014
+ if (edge2.neighbourEdge !== undefined) {
1015
+ throw new Error("Edge 2 already connected");
1016
+ }
1017
+ // #v-endif
1018
+
1019
+ // #v-ifdef DEV
1020
+ if (edge1.face === edge2.face) {
1021
+ throw new Error("Faces are the same");
1022
+ }
1023
+ // #v-endif
1024
+
1025
+ // // Check not connected yet
1026
+
1027
+ // #v-ifdef DEV
1028
+ // Check faces are different
1029
+ if (edge1.face === edge2.face) {
1030
+ throw new Error("Faces are the same");
1031
+ }
1032
+ // #v-endif
1033
+
1034
+ // #v-ifdef DEV
1035
+ // Check vertices match
1036
+ if (edge1.startIndex !== edge2.nextEdge!.startIndex) {
1037
+ throw new Error("Vertices do not match");
1038
+ }
1039
+ // #v-endif
1040
+
1041
+ // #v-ifdef DEV
1042
+ if (edge2.startIndex !== edge1.nextEdge!.startIndex) {
1043
+ throw new Error("Vertices do not match");
1044
+ }
1045
+ // #v-endif
1046
+
1047
+ // Link up
1048
+ edge1.neighbourEdge = edge2;
1049
+ edge2.neighbourEdge = edge1;
1050
+ }
1051
+
1052
+ unlinkFace(inFace: ConvexHullBuilderFace): void {
1053
+ // Unlink from neighbours
1054
+ let edge = inFace.firstEdge;
1055
+ do {
1056
+ if (edge!.neighbourEdge) {
1057
+ // #v-ifdef DEV
1058
+ // Validate that neighbour points to us
1059
+ if (edge!.neighbourEdge.neighbourEdge !== edge) {
1060
+ throw new Error("Neighbour does not point to us");
1061
+ }
1062
+ // #v-endif
1063
+
1064
+ // Unlink
1065
+ edge!.neighbourEdge.neighbourEdge = undefined;
1066
+ edge!.neighbourEdge = undefined;
1067
+ }
1068
+
1069
+ edge = edge!.nextEdge;
1070
+ } while (edge !== inFace.firstEdge);
1071
+ }
1072
+
1073
+ findEdge(inFacingFace: ConvexHullBuilderFace, inVertex: Vec3, outEdges: ConvexHullBuilderFullEdge[]): void {
1074
+ // #v-ifdef DEV
1075
+ // Assert that we were given an empty array
1076
+ if (outEdges.length !== 0) {
1077
+ throw new Error("outEdges must be empty");
1078
+ }
1079
+ // #v-endif
1080
+
1081
+ // #v-ifdef DEV
1082
+ // Should start with a facing face
1083
+ if (!inFacingFace.isFacing(inVertex)) {
1084
+ throw new Error("inFacingFace must be facing inVertex");
1085
+ }
1086
+ // #v-endif
1087
+
1088
+ // Flag as removed
1089
+ inFacingFace.removed = true;
1090
+
1091
+ // Instead of recursing, we build our own stack with the information we need
1092
+ interface StackEntry {
1093
+ firstEdge: ConvexHullBuilderEdge;
1094
+ currentEdge: ConvexHullBuilderEdge;
1095
+ visited: boolean;
1096
+ }
1097
+
1098
+ const stack: StackEntry[] = [];
1099
+ let currentStackPosition = 0;
1100
+
1101
+ // Start with the face / edge provided
1102
+ stack[currentStackPosition] = {
1103
+ firstEdge: inFacingFace.firstEdge!,
1104
+ currentEdge: inFacingFace.firstEdge!,
1105
+ visited: false,
1106
+ };
1107
+
1108
+ let iteration = -1;
1109
+
1110
+ while (true) {
1111
+ iteration++;
1112
+ const currentEntry = stack[currentStackPosition];
1113
+
1114
+ // Next edge
1115
+ const edge = currentEntry.currentEdge;
1116
+ currentEntry.currentEdge = edge.nextEdge!;
1117
+
1118
+ // If we're back at the first edge we've completed the face and we're done
1119
+ if (edge === currentEntry.firstEdge && currentEntry.visited) {
1120
+ // This face needs to be removed, unlink it now, caller will free
1121
+ this.unlinkFace(edge.face);
1122
+
1123
+ // Pop from stack
1124
+ if (--currentStackPosition < 0) {
1125
+ break;
1126
+ }
1127
+ } else {
1128
+ currentEntry.visited = true;
1129
+ // Visit neighbour face
1130
+ const neighbourEdge = edge.neighbourEdge;
1131
+ if (neighbourEdge) {
1132
+ const neighbourFace = neighbourEdge.face;
1133
+ if (!neighbourFace.removed) {
1134
+ // Check if vertex is on the front side of this face
1135
+ if (neighbourFace.isFacing(inVertex)) {
1136
+ // Vertex on front, this face needs to be removed
1137
+ neighbourFace.removed = true;
1138
+
1139
+ // Add element to the stack of elements to visit
1140
+ currentStackPosition++;
1141
+ stack[currentStackPosition] = {
1142
+ firstEdge: neighbourEdge,
1143
+ currentEdge: neighbourEdge.nextEdge!, // We don't need to test this edge again since we came from it
1144
+ visited: false,
1145
+ };
1146
+ } else {
1147
+ // Vertex behind, keep edge
1148
+ const full = new ConvexHullBuilderFullEdge(edge.startIndex, neighbourEdge.startIndex, neighbourEdge);
1149
+ outEdges.push(full);
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ mergeFaces(inEdge: ConvexHullBuilderEdge): void {
1158
+ // Get the face
1159
+ const face = inEdge.face;
1160
+
1161
+ // Find the previous and next edge
1162
+ const nextEdge = inEdge.nextEdge;
1163
+ const prevEdge = inEdge.getPreviousEdge();
1164
+
1165
+ // Get the other face
1166
+ const otherEdge = inEdge.neighbourEdge;
1167
+ const otherFace = otherEdge!.face;
1168
+
1169
+ // #v-ifdef DEV
1170
+ // Check if attempting to merge with self
1171
+ if (face === otherFace) {
1172
+ throw new Error("Attempting to merge with self");
1173
+ }
1174
+ // #v-endif
1175
+
1176
+ // Loop over the edges of the other face and make them belong to inFace
1177
+ let edge = otherEdge!.nextEdge;
1178
+ prevEdge!.nextEdge = edge;
1179
+ while (true) {
1180
+ edge!.face = face;
1181
+ if (edge!.nextEdge === otherEdge) {
1182
+ // Terminate when we are back at otherEdge
1183
+ edge!.nextEdge = nextEdge;
1184
+ break;
1185
+ }
1186
+ edge = edge!.nextEdge;
1187
+ }
1188
+
1189
+ // If the first edge happens to be inEdge we need to fix it because this edge is no longer part of the face.
1190
+ // Note that we replace it with the first edge of the merged face so that if the MergeFace function is called
1191
+ // from a loop that loops around the face that it will still terminate after visiting all edges once.
1192
+ if (face.firstEdge === inEdge) {
1193
+ face.firstEdge = prevEdge!.nextEdge;
1194
+ }
1195
+
1196
+ // Free the edges
1197
+ // delete inEdge; TODO: may not need to delete anything
1198
+ // delete otherEdge; TODO: may not need to delete anything
1199
+
1200
+ // Mark the other face as removed
1201
+ otherFace.firstEdge = undefined;
1202
+ otherFace.removed = true;
1203
+
1204
+ // Recalculate plane
1205
+ face.calculateNormalAndCentroid(this.polyhedron!);
1206
+
1207
+ // Merge conflict lists
1208
+ if (face.furthestPointDistanceSquared > otherFace.furthestPointDistanceSquared) {
1209
+ // This face has a point that's further away, make sure it remains the last one as we add the other points to this faces list
1210
+ face.conflictList.splice(face.conflictList.length - 1, 0, ...otherFace.conflictList);
1211
+ } else {
1212
+ // The other face has a point that's furthest away, add that list at the end.
1213
+ face.conflictList.splice(face.conflictList.length, 0, ...otherFace.conflictList);
1214
+ face.furthestPointDistanceSquared = otherFace.furthestPointDistanceSquared;
1215
+ }
1216
+ otherFace.conflictList.length = 0;
1217
+ }
1218
+
1219
+ mergeDegenerateFace(inFace: ConvexHullBuilderFace, ioAffectedFaces: ConvexHullBuilderFace[]): void {
1220
+ mergeDegenerateFacenormal.x = inFace.normal[0];
1221
+ mergeDegenerateFacenormal.y = inFace.normal[1];
1222
+ mergeDegenerateFacenormal.z = inFace.normal[2];
1223
+
1224
+ // Check area of face
1225
+ if (mergeDegenerateFacenormal.squaredLength() < minTriangleAreaSquared) {
1226
+ // Find longest edge, since this face is a sliver this should keep the face convex
1227
+ let maxLengthSquared = 0.0;
1228
+ let longestEdge: ConvexHullBuilderEdge | undefined = undefined;
1229
+ let edge = inFace.firstEdge;
1230
+ mergeDegenerateFacep1.copy(this.polyhedron!.array[edge!.startIndex]);
1231
+ do {
1232
+ const nextEdge = edge!.nextEdge;
1233
+ mergeDegenerateFacep2.copy(this.polyhedron!.array[nextEdge!.startIndex]);
1234
+ const lengthSquared = mergeDegenerateFacep1.squaredDistance(mergeDegenerateFacep2);
1235
+ if (lengthSquared >= maxLengthSquared) {
1236
+ maxLengthSquared = lengthSquared;
1237
+ longestEdge = edge;
1238
+ }
1239
+ mergeDegenerateFacep1.copy(mergeDegenerateFacep2);
1240
+ edge = nextEdge;
1241
+ } while (edge !== inFace.firstEdge);
1242
+
1243
+ // Merge with face on longest edge
1244
+ this.mergeFaces(longestEdge!);
1245
+
1246
+ // Remove any invalid edges
1247
+ this.removeInvalidEdges(inFace, ioAffectedFaces);
1248
+ }
1249
+ }
1250
+
1251
+ mergeCoplanarOrConcaveFaces(
1252
+ inFace: ConvexHullBuilderFace,
1253
+ inCoplanarToleranceSquared: number,
1254
+ ioAffectedFaces: ConvexHullBuilderFace[]
1255
+ ): void {
1256
+ mergeCoplanarOrConcaveFacesinFaceNormal.x = inFace.normal[0];
1257
+ mergeCoplanarOrConcaveFacesinFaceNormal.y = inFace.normal[1];
1258
+ mergeCoplanarOrConcaveFacesinFaceNormal.z = inFace.normal[2];
1259
+ mergeCoplanarOrConcaveFacesinFaceCentroid.x = inFace.centroid[0];
1260
+ mergeCoplanarOrConcaveFacesinFaceCentroid.y = inFace.centroid[1];
1261
+ mergeCoplanarOrConcaveFacesinFaceCentroid.z = inFace.centroid[2];
1262
+
1263
+ let merged = false;
1264
+
1265
+ let edge = inFace.firstEdge;
1266
+ do {
1267
+ // Store next edge since this edge can be removed
1268
+ const nextEdge = edge!.nextEdge;
1269
+
1270
+ // Test if centroid of one face is above plane of the other face by inCoplanarToleranceSq.
1271
+ // If so we need to merge other face into inFace.
1272
+ const otherFace = edge!.neighbourEdge!.face;
1273
+ mergeCoplanarOrConcaveFacesotherFaceNormal.x = otherFace.normal[0];
1274
+ mergeCoplanarOrConcaveFacesotherFaceNormal.y = otherFace.normal[1];
1275
+ mergeCoplanarOrConcaveFacesotherFaceNormal.z = otherFace.normal[2];
1276
+ mergeCoplanarOrConcaveFacesotherFaceCentroid.x = otherFace.centroid[0];
1277
+ mergeCoplanarOrConcaveFacesotherFaceCentroid.y = otherFace.centroid[1];
1278
+ mergeCoplanarOrConcaveFacesotherFaceCentroid.z = otherFace.centroid[2];
1279
+
1280
+ mergeCoplanarOrConcaveFacesdeltaCentroid.subtractVectors(
1281
+ mergeCoplanarOrConcaveFacesotherFaceCentroid,
1282
+ mergeCoplanarOrConcaveFacesinFaceCentroid
1283
+ );
1284
+ const distanceOtherFaceCentroid = mergeCoplanarOrConcaveFacesinFaceNormal.dot(
1285
+ mergeCoplanarOrConcaveFacesdeltaCentroid
1286
+ );
1287
+ const signedDistanceOtherFaceCentroidSquared = Math.abs(distanceOtherFaceCentroid) * distanceOtherFaceCentroid;
1288
+ const distanceFaceCentroid = -mergeCoplanarOrConcaveFacesotherFaceNormal.dot(
1289
+ mergeCoplanarOrConcaveFacesdeltaCentroid
1290
+ );
1291
+ const signedDistanceFaceCentroidSquared = Math.abs(distanceFaceCentroid) * distanceFaceCentroid;
1292
+ const inFaceNormalLengthSquared = mergeCoplanarOrConcaveFacesinFaceNormal.squaredLength();
1293
+ const otherFaceNormalLengthSquared = mergeCoplanarOrConcaveFacesotherFaceNormal.squaredLength();
1294
+ if (
1295
+ (signedDistanceOtherFaceCentroidSquared > -inCoplanarToleranceSquared * inFaceNormalLengthSquared ||
1296
+ signedDistanceFaceCentroidSquared > -inCoplanarToleranceSquared * otherFaceNormalLengthSquared) &&
1297
+ mergeCoplanarOrConcaveFacesinFaceNormal.dot(mergeCoplanarOrConcaveFacesotherFaceNormal) > 0.0
1298
+ ) {
1299
+ // Never merge faces that are back to back
1300
+ this.mergeFaces(edge!);
1301
+ merged = true;
1302
+ }
1303
+
1304
+ edge = nextEdge;
1305
+ } while (edge !== inFace.firstEdge);
1306
+
1307
+ if (merged) {
1308
+ this.removeInvalidEdges(inFace, ioAffectedFaces);
1309
+ }
1310
+ }
1311
+
1312
+ markAffected(inFace: ConvexHullBuilderFace, ioAffectedFaces: ConvexHullBuilderFace[]): void {
1313
+ if (!ioAffectedFaces.includes(inFace)) {
1314
+ ioAffectedFaces.push(inFace);
1315
+ }
1316
+ }
1317
+
1318
+ removeInvalidEdges(inFace: ConvexHullBuilderFace, ioAffectedFaces: ConvexHullBuilderFace[]): void {
1319
+ // This marks that the plane needs to be recalculated (we delay this until the end of the function since we don't use the plane and we want to avoid calculating it multiple times)
1320
+ let recalculatePlane = false;
1321
+
1322
+ // We keep going through this loop until no more edges were removed
1323
+ let removed: boolean;
1324
+
1325
+ do {
1326
+ removed = false;
1327
+
1328
+ // Loop over all edges in this face
1329
+ let edge = inFace.firstEdge;
1330
+ let neighbourFace = edge!.neighbourEdge!.face;
1331
+
1332
+ do {
1333
+ let nextEdge = edge!.nextEdge;
1334
+ let nextNeighbourFace = nextEdge!.neighbourEdge!.face;
1335
+
1336
+ if (neighbourFace === inFace) {
1337
+ // We only remove 1 edge at a time, check if this edge's next edge is our neighbour.
1338
+ // If this check fails, we will continue to scan along the edge until we find an edge where this is the case.
1339
+ if (edge?.neighbourEdge === nextEdge) {
1340
+ // This edge leads back to the starting point, this means the edge is interior and needs to be removed
1341
+
1342
+ // Remove edge
1343
+ const prevEdge = edge!.getPreviousEdge();
1344
+ prevEdge!.nextEdge = nextEdge!.nextEdge;
1345
+ if (inFace.firstEdge === edge || inFace.firstEdge === nextEdge) {
1346
+ inFace.firstEdge = prevEdge;
1347
+ }
1348
+ // delete edge; TODO: may not need to delete anything
1349
+ // delete nextEdge; TODO: may not need to delete anything
1350
+
1351
+ // Check if inFace now has only 2 edges left
1352
+ if (this.removeTwoEdgeFace(inFace, ioAffectedFaces)) {
1353
+ return; // Bail if face no longer exists
1354
+ }
1355
+
1356
+ // Restart the loop
1357
+ recalculatePlane = true;
1358
+ removed = true;
1359
+ break;
1360
+ }
1361
+ } else if (neighbourFace === nextNeighbourFace) {
1362
+ // There are two edges that connect to the same face, we will remove the second one
1363
+
1364
+ // First merge the neighbours edges
1365
+ const neighbourEdge = nextEdge!.neighbourEdge;
1366
+ const nextNeighbourEdge = neighbourEdge!.nextEdge;
1367
+ if (neighbourFace.firstEdge === nextNeighbourEdge) {
1368
+ neighbourFace.firstEdge = neighbourEdge;
1369
+ }
1370
+ neighbourEdge!.nextEdge = nextNeighbourEdge!.nextEdge;
1371
+ neighbourEdge!.neighbourEdge = edge;
1372
+ // delete nextNeighbourEdge; TODO: may not need to delete anything
1373
+
1374
+ // Then merge my own edges
1375
+ if (inFace.firstEdge === nextEdge) {
1376
+ inFace.firstEdge = edge;
1377
+ }
1378
+ edge!.nextEdge = nextEdge!.nextEdge;
1379
+ edge!.neighbourEdge = neighbourEdge;
1380
+ // delete nextEdge; TODO: may not need to delete anything
1381
+
1382
+ // Check if neighbour has only 2 edges left
1383
+ if (!this.removeTwoEdgeFace(neighbourFace, ioAffectedFaces)) {
1384
+ // No, we need to recalculate its plane
1385
+ neighbourFace.calculateNormalAndCentroid(this.polyhedron!);
1386
+
1387
+ // Mark neighbour face as affected
1388
+ this.markAffected(neighbourFace, ioAffectedFaces);
1389
+ }
1390
+
1391
+ // Check if inFace now has only 2 edges left
1392
+ if (this.removeTwoEdgeFace(inFace, ioAffectedFaces)) {
1393
+ return; // Bail if face no longer exists
1394
+ }
1395
+
1396
+ // Restart loop
1397
+ recalculatePlane = true;
1398
+ removed = true;
1399
+ break;
1400
+ }
1401
+
1402
+ // This edge is ok, go to the next edge
1403
+ edge = nextEdge;
1404
+ neighbourFace = nextNeighbourFace;
1405
+ } while (edge !== inFace.firstEdge);
1406
+ } while (removed);
1407
+
1408
+ // Recalculate plane?
1409
+ if (recalculatePlane) {
1410
+ inFace.calculateNormalAndCentroid(this.polyhedron!);
1411
+ }
1412
+ }
1413
+
1414
+ removeTwoEdgeFace(inFace: ConvexHullBuilderFace, ioAffectedFaces: ConvexHullBuilderFace[]): boolean {
1415
+ // Check if this face contains only 2 edges
1416
+ let edge = inFace.firstEdge;
1417
+ let nextEdge = edge!.nextEdge;
1418
+ // #v-ifdef DEV
1419
+ if (edge === nextEdge) {
1420
+ throw new Error("1 edge faces should not exist");
1421
+ }
1422
+ // #v-endif
1423
+
1424
+ if (nextEdge!.nextEdge === edge) {
1425
+ // Schedule both neighbours for re-checking
1426
+ const neighbourEdge = edge!.neighbourEdge;
1427
+ const neighbourFace = neighbourEdge!.face;
1428
+ const nextNeighbourEdge = nextEdge!.neighbourEdge;
1429
+ const nextNeighbourFace = nextNeighbourEdge!.face;
1430
+ this.markAffected(neighbourFace, ioAffectedFaces);
1431
+ this.markAffected(nextNeighbourFace, ioAffectedFaces);
1432
+
1433
+ // Link my neighbours to each other
1434
+ neighbourEdge!.neighbourEdge = nextNeighbourEdge;
1435
+ nextNeighbourEdge!.neighbourEdge = neighbourEdge;
1436
+
1437
+ // Unlink my edges
1438
+ edge!.neighbourEdge = undefined;
1439
+ nextEdge!.neighbourEdge = undefined;
1440
+
1441
+ // Mark this face as removed
1442
+ inFace.removed = true;
1443
+
1444
+ return true;
1445
+ }
1446
+
1447
+ return false;
1448
+ }
1449
+
1450
+ validateFace(inFace: ConvexHullBuilderFace): void {
1451
+ if (inFace.removed) {
1452
+ let edge = inFace.firstEdge;
1453
+ if (edge !== undefined) {
1454
+ do {
1455
+ // #v-ifdef DEV
1456
+ if (edge!.neighbourEdge !== undefined) {
1457
+ throw new Error("Removed face should not have neighbours");
1458
+ }
1459
+ // #v-endif
1460
+ edge = edge!.nextEdge;
1461
+ } while (edge !== inFace.firstEdge);
1462
+ }
1463
+ } else {
1464
+ let edgeCount = 0;
1465
+
1466
+ let edge = inFace.firstEdge;
1467
+ do {
1468
+ // Count edge
1469
+ ++edgeCount;
1470
+
1471
+ // Validate that adjacent faces are all different
1472
+ if (this.faces.length > 2) {
1473
+ for (let otherEdge = edge!.nextEdge; otherEdge !== inFace.firstEdge; otherEdge = otherEdge!.nextEdge) {
1474
+ // #v-ifdef DEV
1475
+ if (edge!.neighbourEdge!.face === otherEdge!.neighbourEdge!.face) {
1476
+ throw new Error("Adjacent faces should be different");
1477
+ }
1478
+ // #v-endif
1479
+ }
1480
+
1481
+ // #v-ifdef DEV
1482
+ // Assert that the face is correct
1483
+ if (edge!.face !== inFace) {
1484
+ throw new Error("Face is not correct");
1485
+ }
1486
+ // #v-endif
1487
+
1488
+ const nbEdge = edge!.neighbourEdge;
1489
+ // #v-ifdef DEV
1490
+ // Assert that we have a neighbour
1491
+ if (nbEdge === undefined) {
1492
+ throw new Error("Edge should have a neighbour");
1493
+ }
1494
+ // #v-endif
1495
+
1496
+ // #v-ifdef DEV
1497
+ // Assert that our neighbours edge points to us
1498
+ if (nbEdge.neighbourEdge !== edge) {
1499
+ throw new Error("Neighbour should point to us");
1500
+ }
1501
+ // #v-endif
1502
+
1503
+ // #v-ifdef DEV
1504
+ // Assert that it belongs to a different face
1505
+ if (nbEdge.face === inFace) {
1506
+ throw new Error("Neighbour should belong to a different face");
1507
+ }
1508
+ // #v-endif
1509
+
1510
+ // #v-ifdef DEV
1511
+ // Assert that the next edge of the neighbour points to the same vertex as this edge's vertex
1512
+ if (nbEdge.nextEdge!.startIndex !== edge!.startIndex) {
1513
+ throw new Error("Neighbour's next edge should point to the same vertex");
1514
+ }
1515
+
1516
+ // #v-endif
1517
+ // #v-ifdef DEV
1518
+ // Assert that my next edge points to the same vertex as my neighbours vertex
1519
+ if (edge!.nextEdge!.startIndex !== nbEdge.startIndex) {
1520
+ throw new Error("My next edge should point to the same vertex as my neighbours vertex");
1521
+ }
1522
+ // #v-endif
1523
+ }
1524
+
1525
+ edge = edge!.nextEdge;
1526
+ } while (edge !== inFace.firstEdge);
1527
+
1528
+ // #v-ifdef DEV
1529
+ // Assert that we have 3 or more edges
1530
+ if (edgeCount < 3) {
1531
+ throw new Error("Face should have 3 or more edges");
1532
+ }
1533
+ // #v-endif
1534
+ }
1535
+ }
1536
+
1537
+ validateFaces(): void {
1538
+ for (const face of this.faces) {
1539
+ this.validateFace(face);
1540
+ }
1541
+ }
1542
+
1543
+ getCenterOfMassAndVolume(outCenterOfMass: Vec3): number {
1544
+ // Fourth point is the average of all face centroids
1545
+ getCenterOfMassAndVolumevertexD.zero();
1546
+ for (const face of this.faces) {
1547
+ getCenterOfMassAndVolumevertexD.x += face.centroid[0];
1548
+ getCenterOfMassAndVolumevertexD.y += face.centroid[1];
1549
+ getCenterOfMassAndVolumevertexD.z += face.centroid[2];
1550
+ }
1551
+
1552
+ getCenterOfMassAndVolumevertexD.scaleVector(getCenterOfMassAndVolumevertexD, 1.0 / this.faces.length);
1553
+
1554
+ // Calculate mass and center of mass of this convex hull by summing all tetrahedrons
1555
+ let outVolume = 0.0;
1556
+ outCenterOfMass.zero();
1557
+ for (const face of this.faces) {
1558
+ // Get the first vertex that we'll use to create a triangle fan
1559
+ let edge = face.firstEdge;
1560
+ getCenterOfMassAndVolumevertexA.copy(this.polyhedron!.array[edge!.startIndex]);
1561
+
1562
+ // Get the second vertex
1563
+ edge = edge!.nextEdge;
1564
+ getCenterOfMassAndVolumevertexB.copy(this.polyhedron!.array[edge!.startIndex]);
1565
+
1566
+ for (edge = edge!.nextEdge; edge !== face.firstEdge; edge = edge!.nextEdge) {
1567
+ // Fetch the last point of the triangle
1568
+ getCenterOfMassAndVolumevertexC.copy(this.polyhedron!.array[edge!.startIndex]);
1569
+
1570
+ // Calculate center of mass and mass of this tetrahedron,
1571
+ // see: https://en.wikipedia.org/wiki/Tetrahedron#Volume
1572
+ getCenterOfMassAndVolumevectorDA.subtractVectors(
1573
+ getCenterOfMassAndVolumevertexA,
1574
+ getCenterOfMassAndVolumevertexD
1575
+ );
1576
+ getCenterOfMassAndVolumevectorDB.subtractVectors(
1577
+ getCenterOfMassAndVolumevertexB,
1578
+ getCenterOfMassAndVolumevertexD
1579
+ );
1580
+ getCenterOfMassAndVolumevectorDC.subtractVectors(
1581
+ getCenterOfMassAndVolumevertexC,
1582
+ getCenterOfMassAndVolumevertexD
1583
+ );
1584
+ getCenterOfMassAndVolumecrossDB_DC.crossVectors(
1585
+ getCenterOfMassAndVolumevectorDB,
1586
+ getCenterOfMassAndVolumevectorDC
1587
+ );
1588
+ const volume_tetrahedron = getCenterOfMassAndVolumevectorDA.dot(getCenterOfMassAndVolumecrossDB_DC); // Needs to be divided by 6, postpone this until the end of the loop
1589
+ getCenterOfMassAndVolumecenterOfMassTetrahedron.addVectors(
1590
+ getCenterOfMassAndVolumevertexA,
1591
+ getCenterOfMassAndVolumevertexB
1592
+ );
1593
+ getCenterOfMassAndVolumecenterOfMassTetrahedron.addVectors(
1594
+ getCenterOfMassAndVolumecenterOfMassTetrahedron,
1595
+ getCenterOfMassAndVolumevertexC
1596
+ );
1597
+ getCenterOfMassAndVolumecenterOfMassTetrahedron.addVectors(
1598
+ getCenterOfMassAndVolumecenterOfMassTetrahedron,
1599
+ getCenterOfMassAndVolumevertexD
1600
+ ); // Needs to be divided by 4, postpone this until the end of the loop
1601
+
1602
+ // Accumulate results
1603
+ outVolume += volume_tetrahedron;
1604
+ outCenterOfMass.addScaledToVector(
1605
+ outCenterOfMass,
1606
+ getCenterOfMassAndVolumecenterOfMassTetrahedron,
1607
+ volume_tetrahedron
1608
+ );
1609
+
1610
+ // Update v2 for next triangle
1611
+ getCenterOfMassAndVolumevertexB.copy(getCenterOfMassAndVolumevertexC);
1612
+ }
1613
+ }
1614
+
1615
+ // Calculate center of mass, fall back to average point in case there is no volume (everything is on a plane in this case)
1616
+ if (outVolume > floatEpsilon) {
1617
+ outCenterOfMass.scaleVector(outCenterOfMass, 1.0 / (4.0 * outVolume));
1618
+ } else {
1619
+ outCenterOfMass.copy(getCenterOfMassAndVolumevertexD);
1620
+ }
1621
+
1622
+ outVolume /= 6.0;
1623
+
1624
+ return outVolume;
1625
+ }
1626
+
1627
+ determineMaxError(): [ConvexHullBuilderFace | undefined, number, number, number] {
1628
+ const outCoplanarDistance = this.determineCoplanarDistance();
1629
+
1630
+ // This measures the distance from a polygon to the furthest point outside of the hull
1631
+ let maxError = 0.0;
1632
+ let maxErrorFace: ConvexHullBuilderFace | undefined = undefined;
1633
+ let maxErrorPoint = -1;
1634
+
1635
+ for (let i = 0; i < this.polyhedron!.length; ++i) {
1636
+ determineMaxError.copy(this.polyhedron!.array[i]);
1637
+
1638
+ // This measures the closest edge from all faces to point v
1639
+ // Note that we take the min of all faces since there may be multiple near coplanar faces so if we were to test this per face
1640
+ // we may find that a point is outside of a polygon and mark it as an error, while it is actually inside a nearly coplanar
1641
+ // polygon.
1642
+ let minEdgeDistanceSquared = Infinity;
1643
+ let minEdgeDistanceFace: ConvexHullBuilderFace | undefined = undefined;
1644
+
1645
+ for (const face of this.faces) {
1646
+ // Check if point is on or in front of plane
1647
+ determineMaxErrornormal.x = face.normal[0];
1648
+ determineMaxErrornormal.y = face.normal[1];
1649
+ determineMaxErrornormal.z = face.normal[2];
1650
+ determineMaxErrorcentroid.x = face.centroid[0];
1651
+ determineMaxErrorcentroid.y = face.centroid[1];
1652
+ determineMaxErrorcentroid.z = face.centroid[2];
1653
+ const normalLength = determineMaxErrornormal.length();
1654
+ // #v-ifdef DEV
1655
+ if (normalLength <= 0.0) {
1656
+ throw new Error(`Normal length should be greater than 0, got ${normalLength}`);
1657
+ }
1658
+ // #v-endif
1659
+ determineMaxErrortempVector.subtractVectors(determineMaxError, determineMaxErrorcentroid);
1660
+ const planeDistance = determineMaxError.dot(determineMaxErrornormal) / normalLength;
1661
+ if (planeDistance > -outCoplanarDistance) {
1662
+ // Check distance to the edges of this face
1663
+ const edgeDistanceSquared = this.getDistanceToEdgeSquared(determineMaxError, face);
1664
+ if (edgeDistanceSquared < minEdgeDistanceSquared) {
1665
+ minEdgeDistanceSquared = edgeDistanceSquared;
1666
+ minEdgeDistanceFace = face;
1667
+ }
1668
+
1669
+ // If the point is inside the polygon and the point is in front of the plane, measure the distance
1670
+ if (edgeDistanceSquared === 0.0 && planeDistance > maxError) {
1671
+ maxError = planeDistance;
1672
+ maxErrorFace = face;
1673
+ maxErrorPoint = i;
1674
+ }
1675
+ }
1676
+ }
1677
+
1678
+ // If the minimum distance to an edge is further than our current max error, we use that as max error
1679
+ const minEdgeDistance = Math.sqrt(minEdgeDistanceSquared);
1680
+ if (minEdgeDistanceFace !== undefined && minEdgeDistance > maxError) {
1681
+ maxError = minEdgeDistance;
1682
+ maxErrorFace = minEdgeDistanceFace;
1683
+ maxErrorPoint = i;
1684
+ }
1685
+ }
1686
+
1687
+ return [maxErrorFace, maxError, maxErrorPoint, outCoplanarDistance];
1688
+ }
1689
+ }