@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 9547-3732 Québec inc. / perplex.gg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ <div style="display: flex;">
2
+ <h1>Bounce - Premium 3D Physics for the Web</h1>
3
+ </div>
4
+
5
+ Bounce provides fast and determinstic 3D physics for Typescript and Javascript projects. Bounce is written in pure Typsescript (no WebAssembly). See the official website at [https://perplex.gg/bounce](https://perplex.gg/bounce) for demos, API and more info.
6
+
7
+ ## Credits and Acknowledgments
8
+
9
+ Bounce was created by:
10
+ - Durai Ziyaee - 95% of the code, physics R&D
11
+ - Hamza Kubba - 5% of the code, funding, direction, project management, QA, website
12
+
13
+ Although Bounce is mostly original code, it is heavily inspired by and draws heavily from:
14
+ - Jolt
15
+ - Rapier
16
+ - Box2D
17
+ - Godot
18
+ - gl-matrix
19
+
20
+ ## Roadmap
21
+
22
+ - Release example code (ETA Oct-Nov 2025)
23
+ - Release the official character controller implementation (ETA Oct-Nov 2025)
24
+ - Create a dedicated documentation site (ETA Nov 2025)
25
+ - Fixing bugs. Please file an issue if you run into problems!
26
+ - Want to see something here? Feel free to submit an issue!
27
+
28
+ ## How to contribute
29
+
30
+ If you like this project and would like to support our work, please consider contributing code via pull requests, or donating via [open collective](https://opencollective.com/perplexgg). Contributions are greatly appreciated!
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@perplexdotgg/bounce",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "description": "Bounce",
6
+ "main": "./build/bounce.js",
7
+ "types": "./build/bounce.d.ts",
8
+ "scripts": {
9
+ "dev": "vite",
10
+ "build": "tsc && vite build",
11
+ "watch": "tsc --watch",
12
+ "test": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "monomorph": "^1.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^24.9.0",
19
+ "ts-node": "^10.9.2",
20
+ "tslib": "^2.8.1",
21
+ "typescript": "^5.9.3",
22
+ "vite": "^7.1.11",
23
+ "vite-plugin-conditional-compiler": "^0.3.1",
24
+ "vite-plugin-dts": "^4.5.4",
25
+ "vitest": "^3.2.4"
26
+ },
27
+ "type": "module",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://codeberg.org/perplexdotgg/bounce"
31
+ }
32
+ }
@@ -0,0 +1,437 @@
1
+ import { Mat3 } from "../math/mat3";
2
+ import { Vec3 } from "../math/vec3";
3
+ import { NumberValue } from "../math/NumberValue";
4
+ import { Plane } from "../shape/Plane";
5
+ import ConvexHull, {
6
+ ConvexHullFace,
7
+ ConvexHullNoConvex,
8
+ ConvexHullPoint,
9
+ ConvexHullWithConvex,
10
+ updateShape,
11
+ } from "../shape/ConvexHull";
12
+ import { ConvexHullBuilder3d, ConvexHullBuilderResult } from "./ConvexHullBuilder3d";
13
+ import { degreesToRadians } from "../math/scalar";
14
+
15
+ const covarianceCanonical = /*@__PURE__*/ Mat3.create();
16
+ covarianceCanonical.fillDiagonal(1 / 60);
17
+ covarianceCanonical.fillOffDiagonal(1 / 120);
18
+ const covarianceAccumulated = /*@__PURE__*/ Mat3.create();
19
+ const covarianceTransformed = /*@__PURE__*/ Mat3.create();
20
+ const transform = /*@__PURE__*/ Mat3.create();
21
+ const transposedTransform = /*@__PURE__*/ Mat3.create();
22
+ const v1 = /*@__PURE__*/ Vec3.create();
23
+ const v2 = /*@__PURE__*/ Vec3.create();
24
+ const v3 = /*@__PURE__*/ Vec3.create();
25
+ const zero = /*@__PURE__*/ Vec3.create({ x: 0, y: 0, z: 0 });
26
+ const p = /*@__PURE__*/ Vec3.create();
27
+ const faceCentroid = /*@__PURE__*/ Vec3.create();
28
+ const faceNormal = /*@__PURE__*/ Vec3.create();
29
+ const tempVector = /*@__PURE__*/ Vec3.create();
30
+ const normal1 = /*@__PURE__*/ Vec3.create();
31
+ const normal2 = /*@__PURE__*/ Vec3.create();
32
+ const crossNormal1Normal2 = /*@__PURE__*/ Vec3.create();
33
+ const normal3 = /*@__PURE__*/ Vec3.create();
34
+ const p1 = /*@__PURE__*/ Plane.create();
35
+ const p2 = /*@__PURE__*/ Plane.create();
36
+ let p3 = /*@__PURE__*/ Plane.create();
37
+ const offsetMask = /*@__PURE__*/ Vec3.create();
38
+ const crossN1N2 = /*@__PURE__*/ Vec3.create();
39
+ const n = /*@__PURE__*/ Mat3.create();
40
+ const adj_n = /*@__PURE__*/ Mat3.create();
41
+ const transformedOffset = /*@__PURE__*/ Vec3.create();
42
+
43
+ export class ConvexHullBuilder {
44
+ buildFromPoints(
45
+ vertices: Float32Array,
46
+ convexRadius: number = 0.02,
47
+ hullTolerance: number = 1e-3,
48
+ convexHullPool: typeof ConvexHull.Pool = new ConvexHull.Pool(),
49
+ facePool: typeof ConvexHullFace.Pool = new ConvexHullFace.Pool(),
50
+ numberValuePool: typeof NumberValue.Pool = new NumberValue.Pool(),
51
+ planePool: typeof Plane.Pool = new Plane.Pool(),
52
+ pointPool: typeof ConvexHullPoint.Pool = new ConvexHullPoint.Pool(),
53
+ shapeNoConvexPointsPool: typeof Vec3.Pool = new Vec3.Pool(),
54
+ ): ConvexHull {
55
+ const maxPoints = 1000;
56
+ const maxVertexIndices = 1000;
57
+
58
+ // get counts from item buffers and use as max counts
59
+ const hull = ConvexHull.create({
60
+ convexRadius,
61
+ faces: { pool: facePool },
62
+ vertexIdx: { pool: numberValuePool },
63
+ planes: { pool: planePool },
64
+ points: { pool: pointPool },
65
+ shapeNoConvexPoints: { pool: shapeNoConvexPointsPool },
66
+ }, convexHullPool) as ConvexHull;
67
+ hull.shapeNoConvex = new ConvexHullNoConvex(hull, hull.shapeNoConvexPoints);
68
+ hull.shapeWithConvex = new ConvexHullWithConvex(hull);
69
+
70
+ // Check convex radius
71
+ if (hull.convexRadius < 0.0) {
72
+ throw new Error("Invalid convex radius");
73
+ }
74
+
75
+ // set vertex data on polyhedron 3d
76
+ const polyhedron3d = new Vec3.Pool();
77
+ const polyhedron2d = new Vec3.Pool();
78
+ for (let i = 0; i < vertices.length; i += 3) {
79
+ Vec3.create({ x: vertices[i + 0], y: vertices[i + 1], z: vertices[i + 2] }, polyhedron3d);
80
+ Vec3.create({ x: vertices[i + 0], y: vertices[i + 1], z: vertices[i + 2] }, polyhedron2d);
81
+ }
82
+
83
+ // Build convex hull
84
+ const builder = new ConvexHullBuilder3d();
85
+ const result = builder.buildConvexHull(polyhedron3d, polyhedron2d, hullTolerance, maxPoints);
86
+ if (result !== ConvexHullBuilderResult.Success && result !== ConvexHullBuilderResult.MaxVerticesReached) {
87
+ throw new Error("Hull building failed");
88
+ }
89
+
90
+ const builder_faces = builder.faces;
91
+
92
+ // // Check the consistency of the resulting hull if we fully built it
93
+ // // TODO: revisit this error check because it seems to be failing even when the hull looks good visually
94
+ // if (result === ConvexHullBuilderResult.Success) {
95
+ // const [max_error_face, max_error_distance, max_error_idx, coplaner_distance] = builder.determineMaxError();
96
+ // if (max_error_distance > 4 * Math.max(coplaner_distance, hullTolerance)) {
97
+ // // Coplanar distance could be bigger than the allowed tolerance if the points are far apart
98
+ // throw new Error(
99
+ // `Hull building failed, point ${max_error_idx} had an error of ${max_error_distance} (relative to tolerance: ${
100
+ // max_error_distance / hullTolerance
101
+ // })`
102
+ // );
103
+ // }
104
+ // }
105
+
106
+ // Calculate center of mass and volume
107
+ hull.computedVolume = builder.getCenterOfMassAndVolume(hull.computedCenterOfMass);
108
+ // make center of mass zero if it is close to zero
109
+ if (hull.computedCenterOfMass.isCloseToZero(1e-6)) {
110
+ hull.computedCenterOfMass.zero();
111
+ }
112
+
113
+ // Calculate covariance matrix
114
+ // See:
115
+ // - Why the inertia tensor is the inertia tensor - Jonathan Blow (http://number-none.com/blow/inertia/deriving_i.html)
116
+ // - How to find the inertia tensor (or other mass properties) of a 3D solid body represented by a triangle mesh (Draft) - Jonathan Blow, Atman J Binstock (http://number-none.com/blow/inertia/bb_inertia.doc)
117
+
118
+ // compute canonical covariance matrix
119
+ // this is the covariance matrix for a unit tetrahedron
120
+ // with vertices (0, 0, 0), (1, 0, 0), (0, 1, 0) and (0, 0, 1)
121
+ // with density 1 kg/m^3
122
+ // because of symmetry, we only need to "calculate". diagonal = 1 / 60, off-diagonal = 1 / 120
123
+
124
+ // start covariance actual at zero as it will be used for accumulation
125
+ covarianceAccumulated.fill(0);
126
+
127
+ for (const face of builder_faces) {
128
+ // Fourth point of the tetrahedron is at the center of mass, we subtract it from the other points so we get a tetrahedron with one vertex at zero
129
+ // The first point on the face will be used to form a triangle fan
130
+ let edge = face.firstEdge;
131
+ v1.subtractVectors(polyhedron3d.array[edge!.startIndex], hull.computedCenterOfMass);
132
+
133
+ // Get the 2nd point
134
+ edge = edge!.nextEdge;
135
+ v2.subtractVectors(polyhedron3d.array[edge!.startIndex], hull.computedCenterOfMass);
136
+
137
+ // Loop over the triangle fan
138
+ for (edge = edge!.nextEdge; edge !== face.firstEdge; edge = edge!.nextEdge) {
139
+ v3.subtractVectors(polyhedron3d.array[edge!.startIndex], hull.computedCenterOfMass);
140
+
141
+ // Affine transform that transforms a unit tetrahedon (with vertices (0, 0, 0), (1, 0, 0), (0, 1, 0) and (0, 0, 1) to this tetrahedron
142
+ transform.setColumns(v1, v2, v3);
143
+
144
+ // Calculate covariance matrix for this tetrahedron
145
+ const det_a = transform.computeDeterminant();
146
+
147
+ // C_transformed = det_a * (A * C_canonical * A^T)
148
+ transposedTransform.transposeMatrix(transform);
149
+ covarianceTransformed.multiplyMat3s(transform, covarianceCanonical);
150
+ covarianceTransformed.multiplyMat3s(covarianceTransformed, transposedTransform);
151
+ covarianceTransformed.multiplyByScalar(det_a);
152
+
153
+ // Add it
154
+ covarianceAccumulated.addMatrices(covarianceAccumulated, covarianceTransformed);
155
+
156
+ // Prepare for next triangle
157
+ v2.copy(v3);
158
+ }
159
+ }
160
+
161
+ // Calculate inertia matrix assuming density is 1
162
+ hull.inertia.identity();
163
+ hull.inertia.multiplyByScalar(covarianceAccumulated.e0 + covarianceAccumulated.e4 + covarianceAccumulated.e8);
164
+ hull.inertia.subtractMatrices(hull.inertia, covarianceAccumulated);
165
+
166
+ // Convert polygons from the builder to our internal representation
167
+ const vertexMap = new Map<number, number>();
168
+
169
+ for (const builder_face of builder_faces) {
170
+ // Determine where the vertices go
171
+ if (hull.vertexIdx.length > maxVertexIndices) {
172
+ throw new Error(`vertex index array too large: (${hull.vertexIdx.length}), max allowed ${maxVertexIndices}`);
173
+ }
174
+ let firstVertex = hull.vertexIdx.length;
175
+ let numVertices = 0;
176
+
177
+ // Loop over vertices in face
178
+ let edge = builder_face.firstEdge;
179
+ do {
180
+ // Remap to new index, not all points in the original input set are required to form the hull
181
+ // TODO: is it okay to set new_idx to 0 here?
182
+ let newIdx: number = 0;
183
+ const originalIdx = edge!.startIndex;
184
+ const m = vertexMap.get(originalIdx);
185
+ if (m !== undefined) {
186
+ // Found, reuse
187
+ newIdx = m;
188
+ } else {
189
+ // This is a new point
190
+ // Make relative to center of mass
191
+
192
+ p.subtractVectors(polyhedron3d.array[originalIdx], hull.computedCenterOfMass);
193
+
194
+ // TODO: Update local bounds
195
+
196
+ // Add to point list
197
+ if (hull.points.length <= maxPoints) {
198
+ newIdx = hull.points.length;
199
+ hull.points.create({ position: p, numFaces: 0, faces: zero });
200
+ vertexMap.set(originalIdx, newIdx);
201
+ }
202
+ }
203
+
204
+ // Append to vertex list
205
+ if (hull.vertexIdx.length >= maxVertexIndices) {
206
+ throw new Error(`vertex index array too large: (${hull.vertexIdx.length}), max allowed ${maxVertexIndices}`);
207
+ }
208
+
209
+ // TODO: should newIdx be initialized to some value?
210
+ hull.vertexIdx.create({ value: newIdx });
211
+ numVertices++;
212
+
213
+ edge = edge!.nextEdge;
214
+ } while (edge !== builder_face.firstEdge);
215
+
216
+ // Add face
217
+ hull.faces.create({ firstVertex: firstVertex, numVertices: numVertices });
218
+
219
+ // Add plane
220
+ faceCentroid.fromArray(builder_face.centroid);
221
+ faceNormal.fromArray(builder_face.normal);
222
+ tempVector.subtractVectors(faceCentroid, hull.computedCenterOfMass);
223
+ faceNormal.normalize();
224
+
225
+ const plane = hull.planes.create(undefined);
226
+ plane.fromPointAndNormal(tempVector, faceNormal);
227
+ }
228
+
229
+ // Test if GetSupportFunction can support this many points
230
+ if (hull.points.length > maxPoints) {
231
+ throw new Error(`Internal error: Too many points in hull (${hull.points.length}), max allowed ${maxPoints}`);
232
+ }
233
+
234
+ for (let p = 0; p < hull.points.length; ++p) {
235
+ // For each point, find faces that use the point
236
+ let faces: number[] = [];
237
+
238
+ for (let f = 0; f < hull.faces.length; ++f) {
239
+ const face = hull.faces.getAtIndex(f)!;
240
+
241
+ for (let v = 0; v < face.numVertices; ++v) {
242
+ const vertexIndex = hull.vertexIdx.getAtIndex(face.firstVertex + v)!;
243
+ if (vertexIndex.value === p) {
244
+ faces.push(f);
245
+ break;
246
+ }
247
+ }
248
+ }
249
+
250
+ if (faces.length < 2) {
251
+ throw new Error("A point must be connected to 2 or more faces!");
252
+ }
253
+
254
+ // Find the 3 normals that form the largest tetrahedron
255
+ // The largest tetrahedron we can get is ((1, 0, 0) x (0, 1, 0)) . (0, 0, 1) = 1, if the volume is only 5% of that,
256
+ // the three vectors are too coplanar and we fall back to using only 2 plane normals
257
+ let biggest_volume = 0.05;
258
+ const best3 = [-1, -1, -1];
259
+
260
+ // When using 2 normals, we get the two with the biggest angle between them with a minimal difference of 1 degree
261
+ // otherwise we fall back to just using 1 plane normal
262
+ let smallest_dot = Math.cos(degreesToRadians(1.0));
263
+ const best2 = [-1, -1];
264
+
265
+ for (let face1 = 0; face1 < faces.length; ++face1) {
266
+ const plane1 = hull.planes.getAtIndex(faces[face1])!;
267
+ normal1.copy(plane1.normal);
268
+
269
+ for (let face2 = face1 + 1; face2 < faces.length; ++face2) {
270
+ const plane2 = hull.planes.getAtIndex(faces[face2])!;
271
+ normal2.copy(plane2.normal);
272
+
273
+ crossNormal1Normal2.crossVectors(normal1, normal2);
274
+
275
+ // Determine the 2 face normals that are most apart
276
+ const dot = normal1.dot(normal2);
277
+ if (dot < smallest_dot) {
278
+ smallest_dot = dot;
279
+ best2[0] = faces[face1];
280
+ best2[1] = faces[face2];
281
+ }
282
+
283
+ // Determine the 3 face normals that form the largest tetrahedron
284
+ for (let face3 = face2 + 1; face3 < faces.length; ++face3) {
285
+ const plane3 = hull.planes.getAtIndex(faces[face3])!;
286
+ normal3.copy(plane3.normal);
287
+
288
+ const volume = Math.abs(crossNormal1Normal2.dot(normal3));
289
+ if (volume > biggest_volume) {
290
+ biggest_volume = volume;
291
+ best3[0] = faces[face1];
292
+ best3[1] = faces[face2];
293
+ best3[2] = faces[face3];
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // If we didn't find 3 planes, use 2, if we didn't find 2 use 1
300
+ let pointFaces: [number, number, number];
301
+ if (best3[0] !== -1) {
302
+ faces = [best3[0], best3[1], best3[2]];
303
+ pointFaces = [best3[0], best3[1], best3[2]];
304
+ } else if (best2[0] !== -1) {
305
+ faces = [best2[0], best2[1]];
306
+ pointFaces = [best2[0], best2[1], -1];
307
+ } else {
308
+ faces = [faces[0], -1, -1];
309
+ pointFaces = [faces[0], -1, -1];
310
+ }
311
+
312
+ // Copy the faces to the points buffer
313
+ const point = hull.points.getAtIndex(p)!;
314
+ point.numFaces = faces.length;
315
+ point.faces.fromArray(pointFaces);
316
+ }
317
+
318
+ // If the convex radius is already zero, there's no point in further reducing it
319
+ if (hull.convexRadius > 0.0) {
320
+ // Find out how thin the hull is by walking over all planes and checking the thickness of the hull in that direction
321
+ let minSize = Number.MAX_VALUE;
322
+
323
+ for (let i = 0; i < hull.planes.length; i++) {
324
+ const plane = hull.planes.getAtIndex(i)!;
325
+
326
+ // Take the point that is furthest away from the plane as thickness of this hull
327
+ let maxDist = 0.0;
328
+
329
+ for (let j = 0; j < hull.points.length; j++) {
330
+ const point = hull.points.getAtIndex(j)!;
331
+
332
+ const dist = -plane.signedDistance(point.position); // Point is always behind plane, so we need to negate
333
+ if (dist > maxDist) {
334
+ maxDist = dist;
335
+ }
336
+ }
337
+ minSize = Math.min(minSize, maxDist);
338
+ }
339
+
340
+ // We need to fit in 2x the convex radius in min_size, so reduce the convex radius if it's bigger than that
341
+ hull.convexRadius = Math.min(hull.convexRadius, 0.5 * minSize);
342
+ }
343
+
344
+ // Now walk over all points and see if we have to further reduce the convex radius because of sharp edges
345
+ if (hull.convexRadius > 0.0) {
346
+ for (let i = 0; i < hull.points.length; i++) {
347
+ const point = hull.points.getAtIndex(i)!;
348
+
349
+ // If we have a single face, shifting back is easy and we don't need to reduce the convex radius
350
+ if (point.numFaces !== 1) {
351
+ // Get first two planes
352
+ p1.copy(hull.planes.getAtIndex(point.faces.x)!);
353
+ p2.copy(hull.planes.getAtIndex(point.faces.y)!);
354
+
355
+ if (point.numFaces === 3) {
356
+ // Get third plane
357
+ p3.copy(hull.planes.getAtIndex(point.faces.z)!);
358
+
359
+ // All 3 planes will be offset by the convex radius
360
+ offsetMask.replicate(1);
361
+ } else {
362
+ // Third plane has normal perpendicular to the other two planes and goes through the vertex position
363
+ if (point.numFaces !== 2) {
364
+ throw new Error("Invalid number of faces");
365
+ }
366
+
367
+ crossN1N2.crossVectors(p1.normal, p2.normal);
368
+ p3.fromPointAndNormal(point.position, crossN1N2);
369
+
370
+ // Only the first and 2nd plane will be offset, the 3rd plane is only there to guide the intersection point
371
+ offsetMask.set({ x: 1, y: 1, z: 0 });
372
+ }
373
+
374
+ // Plane equation: point . normal + constant = 0
375
+ // Offsetting the plane backwards with convex radius r: point . normal + constant + r = 0
376
+ // To find the intersection 'point' of 3 planes we solve:
377
+ // |n1x n1y n1z| |x| | r + c1 |
378
+ // |n2x n2y n2z| |y| = - | r + c2 | <=> n point = -r (1, 1, 1) - (c1, c2, c3)
379
+ // |n3x n3y n3z| |z| | r + c3 |
380
+ // Where point = (x, y, z), n1x is the x component of the first plane, c1 = plane constant of plane 1, etc.
381
+ // The relation between how much the intersection point shifts as a function of r is: -r * n^-1 (1, 1, 1) = r * offset
382
+ // Where offset = -n^-1 (1, 1, 1) or -n^-1 (1, 1, 0) in case only the first 2 planes are offset
383
+ // The error that is introduced by a convex radius r is: error = r * |offset| - r
384
+ // So the max convex radius given error is: r = error / (|offset| - 1)
385
+
386
+ // TODO: using a mat3 here instead of a mat4 because mat4.3x3 functions not implemented, as a result, not sure if this is correct
387
+
388
+ n.e0 = p1.normal.x;
389
+ n.e1 = p1.normal.y;
390
+ n.e2 = p1.normal.z;
391
+ n.e3 = p2.normal.x;
392
+ n.e4 = p2.normal.y;
393
+ n.e5 = p2.normal.z;
394
+ n.e6 = p3.normal.x;
395
+ n.e7 = p3.normal.y;
396
+ n.e8 = p3.normal.z;
397
+ n.transpose();
398
+
399
+ const det_n = n.computeDeterminant();
400
+ if (det_n === 0.0) {
401
+ // If the determinant is zero, the matrix is not invertible so no solution exists to move the point backwards and we have to choose a convex radius of zero
402
+ hull.convexRadius = 0.0;
403
+ break;
404
+ }
405
+
406
+ adj_n.adjointMatrix(n);
407
+
408
+ transformedOffset.transformVectorFromMat3(offsetMask, adj_n);
409
+ transformedOffset.scale(1 / det_n);
410
+ const offset = transformedOffset.length();
411
+ if (offset <= 1.0) {
412
+ throw new Error("Invalid offset");
413
+ }
414
+ const max_convex_radius = hullTolerance / (offset - 1.0);
415
+ hull.convexRadius = Math.min(hull.convexRadius, max_convex_radius);
416
+ }
417
+ }
418
+ }
419
+
420
+ // Calculate the inner radius by getting the minimum distance from the origin to the planes of the hull
421
+ hull.innerRadius = Number.MAX_VALUE;
422
+ for (let i = 0; i < hull.planes.length; i++) {
423
+ const plane = hull.planes.getAtIndex(i)!;
424
+ hull.innerRadius = Math.min(hull.innerRadius, -plane.constant);
425
+ }
426
+ // Clamp against zero, this should do nothing as the shape is centered around the
427
+ // center of mass but for flat convex hulls there may be numerical round off issues
428
+ hull.innerRadius = Math.max(0.0, hull.innerRadius);
429
+
430
+ for (let i = 0; i < hull.points.length; i++) {
431
+ hull.shapeNoConvexPoints.create();
432
+ }
433
+
434
+ updateShape(hull);
435
+ return hull;
436
+ }
437
+ }