jolt-ruby 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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitmodules +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +228 -0
  5. data/Rakefile +180 -0
  6. data/examples/character_virtual.rb +35 -0
  7. data/examples/falling_sphere.rb +25 -0
  8. data/examples/stagecraft_binding.rb +29 -0
  9. data/ext/jolt_ruby/CMakeLists.txt +42 -0
  10. data/ext/jolt_ruby/character_helper.cpp +44 -0
  11. data/ext/jolt_ruby/constraint_helper.cpp +154 -0
  12. data/ext/jolt_ruby/extconf.rb +56 -0
  13. data/ext/jolt_ruby/helper.cpp +225 -0
  14. data/ext/jolt_ruby/helper.h +103 -0
  15. data/ext/joltc/.github/FUNDING.yml +1 -0
  16. data/ext/joltc/.github/workflows/build.yml +298 -0
  17. data/ext/joltc/.gitignore +272 -0
  18. data/ext/joltc/CMakeLists.txt +431 -0
  19. data/ext/joltc/LICENSE +21 -0
  20. data/ext/joltc/README.md +10 -0
  21. data/ext/joltc/build/cmake_vs2026_arm64.bat +3 -0
  22. data/ext/joltc/build/cmake_vs2026_clang.bat +10 -0
  23. data/ext/joltc/build/cmake_vs2026_x64.bat +3 -0
  24. data/ext/joltc/build/cmake_vs2026_x64_double.bat +3 -0
  25. data/ext/joltc/include/joltc.h +3166 -0
  26. data/ext/joltc/samples/01_HelloWorld/main.cpp +170 -0
  27. data/ext/joltc/samples/CMakeLists.txt +35 -0
  28. data/ext/joltc/src/joltc.c +4 -0
  29. data/ext/joltc/src/joltc.cpp +11812 -0
  30. data/ext/joltc/src/joltc_assert.cpp +271 -0
  31. data/ext/joltc/tests/CMakeLists.txt +77 -0
  32. data/ext/joltc/tests/test_character.cpp +149 -0
  33. data/ext/joltc/tests/test_collision.cpp +146 -0
  34. data/ext/joltc/tests/test_constraints.cpp +353 -0
  35. data/ext/joltc/tests/test_core.cpp +94 -0
  36. data/ext/joltc/tests/test_math.cpp +585 -0
  37. data/ext/joltc/tests/test_physics_system.cpp +465 -0
  38. data/ext/joltc/tests/test_shapes.cpp +789 -0
  39. data/ext/joltc/tests/test_skeleton.cpp +370 -0
  40. data/ext/joltc/tests/test_vehicle.cpp +319 -0
  41. data/generator/generate.rb +237 -0
  42. data/generator/layout_probe.cpp +489 -0
  43. data/generator/verify_layout.rb +32 -0
  44. data/lib/jolt/body.rb +147 -0
  45. data/lib/jolt/body_collection.rb +115 -0
  46. data/lib/jolt/body_dynamics.rb +93 -0
  47. data/lib/jolt/character_virtual.rb +162 -0
  48. data/lib/jolt/constraint.rb +136 -0
  49. data/lib/jolt/constraint_collection.rb +123 -0
  50. data/lib/jolt/contact_events.rb +19 -0
  51. data/lib/jolt/conversions.rb +76 -0
  52. data/lib/jolt/errors.rb +12 -0
  53. data/lib/jolt/fixed_stepper.rb +37 -0
  54. data/lib/jolt/hit.rb +5 -0
  55. data/lib/jolt/layers.rb +182 -0
  56. data/lib/jolt/native/character_functions.rb +16 -0
  57. data/lib/jolt/native/constraint_functions.rb +27 -0
  58. data/lib/jolt/native/core_functions.rb +17 -0
  59. data/lib/jolt/native/generated.rb +1995 -0
  60. data/lib/jolt/native/platform.rb +51 -0
  61. data/lib/jolt/native/query_functions.rb +43 -0
  62. data/lib/jolt/native/types.rb +28 -0
  63. data/lib/jolt/native.rb +94 -0
  64. data/lib/jolt/shape.rb +90 -0
  65. data/lib/jolt/shape_builders.rb +155 -0
  66. data/lib/jolt/system.rb +238 -0
  67. data/lib/jolt/system_characters.rb +25 -0
  68. data/lib/jolt/system_constraints.rb +36 -0
  69. data/lib/jolt/system_contacts.rb +57 -0
  70. data/lib/jolt/system_queries.rb +111 -0
  71. data/lib/jolt/transform.rb +5 -0
  72. data/lib/jolt/version.rb +5 -0
  73. data/lib/jolt.rb +83 -0
  74. data/script/smoke_gem.rb +29 -0
  75. data/script/verify_release.rb +36 -0
  76. metadata +147 -0
@@ -0,0 +1,789 @@
1
+ // Copyright (c) Amer Koleci and Contributors.
2
+ // Licensed under the MIT License (MIT). See LICENSE in the repository root for more information.
3
+
4
+ #include <gtest/gtest.h>
5
+ #include <cmath>
6
+ #include "joltc.h"
7
+
8
+ class ShapesTest : public ::testing::Test {
9
+ protected:
10
+ void SetUp() override {
11
+ ASSERT_TRUE(JPH_Init());
12
+ }
13
+ void TearDown() override {
14
+ JPH_Shutdown();
15
+ }
16
+ };
17
+
18
+ // ============================================================================
19
+ // SphereShape Tests
20
+ // ============================================================================
21
+
22
+ TEST_F(ShapesTest, SphereShape_Create) {
23
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
24
+ ASSERT_NE(shape, nullptr);
25
+
26
+ float radius = JPH_SphereShape_GetRadius(shape);
27
+ EXPECT_FLOAT_EQ(radius, 1.0f);
28
+
29
+ JPH_Shape_Destroy((JPH_Shape*)shape);
30
+ }
31
+
32
+ TEST_F(ShapesTest, SphereShape_CreateWithSettings) {
33
+ JPH_SphereShapeSettings* settings = JPH_SphereShapeSettings_Create(2.0f);
34
+ ASSERT_NE(settings, nullptr);
35
+
36
+ float radius = JPH_SphereShapeSettings_GetRadius(settings);
37
+ EXPECT_FLOAT_EQ(radius, 2.0f);
38
+
39
+ JPH_SphereShape* shape = JPH_SphereShapeSettings_CreateShape(settings);
40
+ ASSERT_NE(shape, nullptr);
41
+
42
+ radius = JPH_SphereShape_GetRadius(shape);
43
+ EXPECT_FLOAT_EQ(radius, 2.0f);
44
+
45
+ JPH_Shape_Destroy((JPH_Shape*)shape);
46
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
47
+ }
48
+
49
+ TEST_F(ShapesTest, SphereShapeSettings_SetRadius) {
50
+ JPH_SphereShapeSettings* settings = JPH_SphereShapeSettings_Create(1.0f);
51
+ ASSERT_NE(settings, nullptr);
52
+
53
+ JPH_SphereShapeSettings_SetRadius(settings, 3.0f);
54
+ float radius = JPH_SphereShapeSettings_GetRadius(settings);
55
+ EXPECT_FLOAT_EQ(radius, 3.0f);
56
+
57
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
58
+ }
59
+
60
+ TEST_F(ShapesTest, SphereShape_GetType) {
61
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
62
+ ASSERT_NE(shape, nullptr);
63
+
64
+ JPH_ShapeType type = JPH_Shape_GetType((const JPH_Shape*)shape);
65
+ EXPECT_EQ(type, JPH_ShapeType_Convex);
66
+
67
+ JPH_ShapeSubType subType = JPH_Shape_GetSubType((const JPH_Shape*)shape);
68
+ EXPECT_EQ(subType, JPH_ShapeSubType_Sphere);
69
+
70
+ JPH_Shape_Destroy((JPH_Shape*)shape);
71
+ }
72
+
73
+ TEST_F(ShapesTest, SphereShape_GetVolume) {
74
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
75
+ ASSERT_NE(shape, nullptr);
76
+
77
+ float volume = JPH_Shape_GetVolume((const JPH_Shape*)shape);
78
+ // Volume of sphere = (4/3) * PI * r^3
79
+ float expectedVolume = (4.0f / 3.0f) * JPH_M_PI * 1.0f * 1.0f * 1.0f;
80
+ EXPECT_NEAR(volume, expectedVolume, 0.001f);
81
+
82
+ JPH_Shape_Destroy((JPH_Shape*)shape);
83
+ }
84
+
85
+ // ============================================================================
86
+ // BoxShape Tests
87
+ // ============================================================================
88
+
89
+ TEST_F(ShapesTest, BoxShape_Create) {
90
+ JPH_Vec3 halfExtent = {1.0f, 2.0f, 3.0f};
91
+ JPH_BoxShape* shape = JPH_BoxShape_Create(&halfExtent, JPH_DEFAULT_CONVEX_RADIUS);
92
+ ASSERT_NE(shape, nullptr);
93
+
94
+ JPH_Vec3 result;
95
+ JPH_BoxShape_GetHalfExtent(shape, &result);
96
+ EXPECT_FLOAT_EQ(result.x, 1.0f);
97
+ EXPECT_FLOAT_EQ(result.y, 2.0f);
98
+ EXPECT_FLOAT_EQ(result.z, 3.0f);
99
+
100
+ JPH_Shape_Destroy((JPH_Shape*)shape);
101
+ }
102
+
103
+ TEST_F(ShapesTest, BoxShape_CreateWithSettings) {
104
+ JPH_Vec3 halfExtent = {2.0f, 2.0f, 2.0f};
105
+ JPH_BoxShapeSettings* settings = JPH_BoxShapeSettings_Create(&halfExtent, JPH_DEFAULT_CONVEX_RADIUS);
106
+ ASSERT_NE(settings, nullptr);
107
+
108
+ JPH_BoxShape* shape = JPH_BoxShapeSettings_CreateShape(settings);
109
+ ASSERT_NE(shape, nullptr);
110
+
111
+ JPH_Vec3 result;
112
+ JPH_BoxShape_GetHalfExtent(shape, &result);
113
+ EXPECT_FLOAT_EQ(result.x, 2.0f);
114
+ EXPECT_FLOAT_EQ(result.y, 2.0f);
115
+ EXPECT_FLOAT_EQ(result.z, 2.0f);
116
+
117
+ JPH_Shape_Destroy((JPH_Shape*)shape);
118
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
119
+ }
120
+
121
+ TEST_F(ShapesTest, BoxShape_GetConvexRadius) {
122
+ JPH_Vec3 halfExtent = {1.0f, 1.0f, 1.0f};
123
+ JPH_BoxShape* shape = JPH_BoxShape_Create(&halfExtent, 0.1f);
124
+ ASSERT_NE(shape, nullptr);
125
+
126
+ float convexRadius = JPH_BoxShape_GetConvexRadius(shape);
127
+ EXPECT_FLOAT_EQ(convexRadius, 0.1f);
128
+
129
+ JPH_Shape_Destroy((JPH_Shape*)shape);
130
+ }
131
+
132
+ TEST_F(ShapesTest, BoxShape_GetType) {
133
+ JPH_Vec3 halfExtent = {1.0f, 1.0f, 1.0f};
134
+ JPH_BoxShape* shape = JPH_BoxShape_Create(&halfExtent, JPH_DEFAULT_CONVEX_RADIUS);
135
+ ASSERT_NE(shape, nullptr);
136
+
137
+ JPH_ShapeType type = JPH_Shape_GetType((const JPH_Shape*)shape);
138
+ EXPECT_EQ(type, JPH_ShapeType_Convex);
139
+
140
+ JPH_ShapeSubType subType = JPH_Shape_GetSubType((const JPH_Shape*)shape);
141
+ EXPECT_EQ(subType, JPH_ShapeSubType_Box);
142
+
143
+ JPH_Shape_Destroy((JPH_Shape*)shape);
144
+ }
145
+
146
+ // ============================================================================
147
+ // CapsuleShape Tests
148
+ // ============================================================================
149
+
150
+ TEST_F(ShapesTest, CapsuleShape_Create) {
151
+ JPH_CapsuleShape* shape = JPH_CapsuleShape_Create(1.0f, 0.5f);
152
+ ASSERT_NE(shape, nullptr);
153
+
154
+ float halfHeight = JPH_CapsuleShape_GetHalfHeightOfCylinder(shape);
155
+ float radius = JPH_CapsuleShape_GetRadius(shape);
156
+ EXPECT_FLOAT_EQ(halfHeight, 1.0f);
157
+ EXPECT_FLOAT_EQ(radius, 0.5f);
158
+
159
+ JPH_Shape_Destroy((JPH_Shape*)shape);
160
+ }
161
+
162
+ TEST_F(ShapesTest, CapsuleShape_CreateWithSettings) {
163
+ JPH_CapsuleShapeSettings* settings = JPH_CapsuleShapeSettings_Create(2.0f, 1.0f);
164
+ ASSERT_NE(settings, nullptr);
165
+
166
+ JPH_CapsuleShape* shape = JPH_CapsuleShapeSettings_CreateShape(settings);
167
+ ASSERT_NE(shape, nullptr);
168
+
169
+ float halfHeight = JPH_CapsuleShape_GetHalfHeightOfCylinder(shape);
170
+ float radius = JPH_CapsuleShape_GetRadius(shape);
171
+ EXPECT_FLOAT_EQ(halfHeight, 2.0f);
172
+ EXPECT_FLOAT_EQ(radius, 1.0f);
173
+
174
+ JPH_Shape_Destroy((JPH_Shape*)shape);
175
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
176
+ }
177
+
178
+ TEST_F(ShapesTest, CapsuleShape_GetType) {
179
+ JPH_CapsuleShape* shape = JPH_CapsuleShape_Create(1.0f, 0.5f);
180
+ ASSERT_NE(shape, nullptr);
181
+
182
+ JPH_ShapeSubType subType = JPH_Shape_GetSubType((const JPH_Shape*)shape);
183
+ EXPECT_EQ(subType, JPH_ShapeSubType_Capsule);
184
+
185
+ JPH_Shape_Destroy((JPH_Shape*)shape);
186
+ }
187
+
188
+ // ============================================================================
189
+ // CylinderShape Tests
190
+ // ============================================================================
191
+
192
+ TEST_F(ShapesTest, CylinderShape_Create) {
193
+ JPH_CylinderShape* shape = JPH_CylinderShape_Create(1.0f, 0.5f);
194
+ ASSERT_NE(shape, nullptr);
195
+
196
+ float halfHeight = JPH_CylinderShape_GetHalfHeight(shape);
197
+ float radius = JPH_CylinderShape_GetRadius(shape);
198
+ EXPECT_FLOAT_EQ(halfHeight, 1.0f);
199
+ EXPECT_FLOAT_EQ(radius, 0.5f);
200
+
201
+ JPH_Shape_Destroy((JPH_Shape*)shape);
202
+ }
203
+
204
+ TEST_F(ShapesTest, CylinderShape_CreateWithSettings) {
205
+ JPH_CylinderShapeSettings* settings = JPH_CylinderShapeSettings_Create(2.0f, 1.0f, JPH_DEFAULT_CONVEX_RADIUS);
206
+ ASSERT_NE(settings, nullptr);
207
+
208
+ JPH_CylinderShape* shape = JPH_CylinderShapeSettings_CreateShape(settings);
209
+ ASSERT_NE(shape, nullptr);
210
+
211
+ float halfHeight = JPH_CylinderShape_GetHalfHeight(shape);
212
+ float radius = JPH_CylinderShape_GetRadius(shape);
213
+ EXPECT_FLOAT_EQ(halfHeight, 2.0f);
214
+ EXPECT_FLOAT_EQ(radius, 1.0f);
215
+
216
+ JPH_Shape_Destroy((JPH_Shape*)shape);
217
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
218
+ }
219
+
220
+ TEST_F(ShapesTest, CylinderShape_GetType) {
221
+ JPH_CylinderShape* shape = JPH_CylinderShape_Create(1.0f, 0.5f);
222
+ ASSERT_NE(shape, nullptr);
223
+
224
+ JPH_ShapeSubType subType = JPH_Shape_GetSubType((const JPH_Shape*)shape);
225
+ EXPECT_EQ(subType, JPH_ShapeSubType_Cylinder);
226
+
227
+ JPH_Shape_Destroy((JPH_Shape*)shape);
228
+ }
229
+
230
+ // ============================================================================
231
+ // TriangleShape Tests
232
+ // ============================================================================
233
+
234
+ TEST_F(ShapesTest, TriangleShape_Create) {
235
+ JPH_Vec3 v1 = {0.0f, 0.0f, 0.0f};
236
+ JPH_Vec3 v2 = {1.0f, 0.0f, 0.0f};
237
+ JPH_Vec3 v3 = {0.0f, 1.0f, 0.0f};
238
+
239
+ JPH_TriangleShape* shape = JPH_TriangleShape_Create(&v1, &v2, &v3, 0.0f);
240
+ ASSERT_NE(shape, nullptr);
241
+
242
+ JPH_Vec3 result;
243
+ JPH_TriangleShape_GetVertex1(shape, &result);
244
+ EXPECT_FLOAT_EQ(result.x, 0.0f);
245
+ EXPECT_FLOAT_EQ(result.y, 0.0f);
246
+ EXPECT_FLOAT_EQ(result.z, 0.0f);
247
+
248
+ JPH_TriangleShape_GetVertex2(shape, &result);
249
+ EXPECT_FLOAT_EQ(result.x, 1.0f);
250
+ EXPECT_FLOAT_EQ(result.y, 0.0f);
251
+ EXPECT_FLOAT_EQ(result.z, 0.0f);
252
+
253
+ JPH_TriangleShape_GetVertex3(shape, &result);
254
+ EXPECT_FLOAT_EQ(result.x, 0.0f);
255
+ EXPECT_FLOAT_EQ(result.y, 1.0f);
256
+ EXPECT_FLOAT_EQ(result.z, 0.0f);
257
+
258
+ JPH_Shape_Destroy((JPH_Shape*)shape);
259
+ }
260
+
261
+ TEST_F(ShapesTest, TriangleShape_GetType) {
262
+ JPH_Vec3 v1 = {0.0f, 0.0f, 0.0f};
263
+ JPH_Vec3 v2 = {1.0f, 0.0f, 0.0f};
264
+ JPH_Vec3 v3 = {0.0f, 1.0f, 0.0f};
265
+
266
+ JPH_TriangleShape* shape = JPH_TriangleShape_Create(&v1, &v2, &v3, 0.0f);
267
+ ASSERT_NE(shape, nullptr);
268
+
269
+ JPH_ShapeSubType subType = JPH_Shape_GetSubType((const JPH_Shape*)shape);
270
+ EXPECT_EQ(subType, JPH_ShapeSubType_Triangle);
271
+
272
+ JPH_Shape_Destroy((JPH_Shape*)shape);
273
+ }
274
+
275
+ // ============================================================================
276
+ // TaperedCapsuleShape Tests
277
+ // ============================================================================
278
+
279
+ TEST_F(ShapesTest, TaperedCapsuleShape_Create) {
280
+ JPH_TaperedCapsuleShapeSettings* settings = JPH_TaperedCapsuleShapeSettings_Create(1.0f, 0.5f, 0.3f);
281
+ ASSERT_NE(settings, nullptr);
282
+
283
+ JPH_TaperedCapsuleShape* shape = JPH_TaperedCapsuleShapeSettings_CreateShape(settings);
284
+ ASSERT_NE(shape, nullptr);
285
+
286
+ float topRadius = JPH_TaperedCapsuleShape_GetTopRadius(shape);
287
+ float bottomRadius = JPH_TaperedCapsuleShape_GetBottomRadius(shape);
288
+ float halfHeight = JPH_TaperedCapsuleShape_GetHalfHeight(shape);
289
+
290
+ EXPECT_FLOAT_EQ(topRadius, 0.5f);
291
+ EXPECT_FLOAT_EQ(bottomRadius, 0.3f);
292
+ EXPECT_FLOAT_EQ(halfHeight, 1.0f);
293
+
294
+ JPH_Shape_Destroy((JPH_Shape*)shape);
295
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
296
+ }
297
+
298
+ TEST_F(ShapesTest, TaperedCapsuleShape_GetType) {
299
+ JPH_TaperedCapsuleShapeSettings* settings = JPH_TaperedCapsuleShapeSettings_Create(1.0f, 0.5f, 0.3f);
300
+ ASSERT_NE(settings, nullptr);
301
+
302
+ JPH_TaperedCapsuleShape* shape = JPH_TaperedCapsuleShapeSettings_CreateShape(settings);
303
+ ASSERT_NE(shape, nullptr);
304
+
305
+ JPH_ShapeSubType subType = JPH_Shape_GetSubType((const JPH_Shape*)shape);
306
+ EXPECT_EQ(subType, JPH_ShapeSubType_TaperedCapsule);
307
+
308
+ JPH_Shape_Destroy((JPH_Shape*)shape);
309
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
310
+ }
311
+
312
+ // ============================================================================
313
+ // ConvexHullShape Tests
314
+ // ============================================================================
315
+
316
+ TEST_F(ShapesTest, ConvexHullShape_Create) {
317
+ // Create a simple tetrahedron
318
+ JPH_Vec3 points[] = {
319
+ {0.0f, 0.0f, 0.0f},
320
+ {1.0f, 0.0f, 0.0f},
321
+ {0.5f, 1.0f, 0.0f},
322
+ {0.5f, 0.5f, 1.0f}
323
+ };
324
+
325
+ JPH_ConvexHullShapeSettings* settings = JPH_ConvexHullShapeSettings_Create(points, 4, JPH_DEFAULT_CONVEX_RADIUS);
326
+ ASSERT_NE(settings, nullptr);
327
+
328
+ JPH_ConvexHullShape* shape = JPH_ConvexHullShapeSettings_CreateShape(settings);
329
+ ASSERT_NE(shape, nullptr);
330
+
331
+ uint32_t numPoints = JPH_ConvexHullShape_GetNumPoints(shape);
332
+ EXPECT_EQ(numPoints, 4u);
333
+
334
+ uint32_t numFaces = JPH_ConvexHullShape_GetNumFaces(shape);
335
+ EXPECT_EQ(numFaces, 4u); // Tetrahedron has 4 faces
336
+
337
+ JPH_Shape_Destroy((JPH_Shape*)shape);
338
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
339
+ }
340
+
341
+ TEST_F(ShapesTest, ConvexHullShape_GetPoint) {
342
+ JPH_Vec3 points[] = {
343
+ {0.0f, 0.0f, 0.0f},
344
+ {1.0f, 0.0f, 0.0f},
345
+ {0.5f, 1.0f, 0.0f},
346
+ {0.5f, 0.5f, 1.0f}
347
+ };
348
+
349
+ JPH_ConvexHullShapeSettings* settings = JPH_ConvexHullShapeSettings_Create(points, 4, JPH_DEFAULT_CONVEX_RADIUS);
350
+ JPH_ConvexHullShape* shape = JPH_ConvexHullShapeSettings_CreateShape(settings);
351
+ ASSERT_NE(shape, nullptr);
352
+
353
+ JPH_Vec3 result;
354
+ JPH_ConvexHullShape_GetPoint(shape, 0, &result);
355
+ // Points may be reordered, so just check they're valid
356
+ EXPECT_FALSE(std::isnan(result.x));
357
+ EXPECT_FALSE(std::isnan(result.y));
358
+ EXPECT_FALSE(std::isnan(result.z));
359
+
360
+ JPH_Shape_Destroy((JPH_Shape*)shape);
361
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
362
+ }
363
+
364
+ // ============================================================================
365
+ // PlaneShape Tests
366
+ // ============================================================================
367
+
368
+ TEST_F(ShapesTest, PlaneShape_Create) {
369
+ JPH_Plane plane = {{0.0f, 1.0f, 0.0f}, 0.0f}; // XZ plane at origin
370
+ JPH_PlaneShape* shape = JPH_PlaneShape_Create(&plane, nullptr, 1000.0f);
371
+ ASSERT_NE(shape, nullptr);
372
+
373
+ JPH_Plane result;
374
+ JPH_PlaneShape_GetPlane(shape, &result);
375
+ EXPECT_FLOAT_EQ(result.normal.x, 0.0f);
376
+ EXPECT_FLOAT_EQ(result.normal.y, 1.0f);
377
+ EXPECT_FLOAT_EQ(result.normal.z, 0.0f);
378
+ EXPECT_FLOAT_EQ(result.distance, 0.0f);
379
+
380
+ float halfExtent = JPH_PlaneShape_GetHalfExtent(shape);
381
+ EXPECT_FLOAT_EQ(halfExtent, 1000.0f);
382
+
383
+ JPH_Shape_Destroy((JPH_Shape*)shape);
384
+ }
385
+
386
+ // ============================================================================
387
+ // Shape Common API Tests
388
+ // ============================================================================
389
+
390
+ TEST_F(ShapesTest, Shape_UserData) {
391
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
392
+ ASSERT_NE(shape, nullptr);
393
+
394
+ JPH_Shape_SetUserData((JPH_Shape*)shape, 12345);
395
+ uint64_t userData = JPH_Shape_GetUserData((const JPH_Shape*)shape);
396
+ EXPECT_EQ(userData, 12345u);
397
+
398
+ JPH_Shape_Destroy((JPH_Shape*)shape);
399
+ }
400
+
401
+ TEST_F(ShapesTest, Shape_GetLocalBounds) {
402
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
403
+ ASSERT_NE(shape, nullptr);
404
+
405
+ JPH_AABox bounds;
406
+ JPH_Shape_GetLocalBounds((const JPH_Shape*)shape, &bounds);
407
+
408
+ // Sphere of radius 1 should have bounds from (-1,-1,-1) to (1,1,1)
409
+ EXPECT_FLOAT_EQ(bounds.min.x, -1.0f);
410
+ EXPECT_FLOAT_EQ(bounds.min.y, -1.0f);
411
+ EXPECT_FLOAT_EQ(bounds.min.z, -1.0f);
412
+ EXPECT_FLOAT_EQ(bounds.max.x, 1.0f);
413
+ EXPECT_FLOAT_EQ(bounds.max.y, 1.0f);
414
+ EXPECT_FLOAT_EQ(bounds.max.z, 1.0f);
415
+
416
+ JPH_Shape_Destroy((JPH_Shape*)shape);
417
+ }
418
+
419
+ TEST_F(ShapesTest, Shape_GetCenterOfMass) {
420
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
421
+ ASSERT_NE(shape, nullptr);
422
+
423
+ JPH_Vec3 com;
424
+ JPH_Shape_GetCenterOfMass((const JPH_Shape*)shape, &com);
425
+
426
+ // Center of mass for sphere is at origin
427
+ EXPECT_FLOAT_EQ(com.x, 0.0f);
428
+ EXPECT_FLOAT_EQ(com.y, 0.0f);
429
+ EXPECT_FLOAT_EQ(com.z, 0.0f);
430
+
431
+ JPH_Shape_Destroy((JPH_Shape*)shape);
432
+ }
433
+
434
+ TEST_F(ShapesTest, Shape_GetInnerRadius) {
435
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
436
+ ASSERT_NE(shape, nullptr);
437
+
438
+ float innerRadius = JPH_Shape_GetInnerRadius((const JPH_Shape*)shape);
439
+ EXPECT_FLOAT_EQ(innerRadius, 1.0f);
440
+
441
+ JPH_Shape_Destroy((JPH_Shape*)shape);
442
+ }
443
+
444
+ TEST_F(ShapesTest, Shape_GetMassProperties) {
445
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
446
+ ASSERT_NE(shape, nullptr);
447
+
448
+ JPH_MassProperties massProps;
449
+ JPH_Shape_GetMassProperties((const JPH_Shape*)shape, &massProps);
450
+
451
+ // Mass should be positive (depends on density)
452
+ EXPECT_GT(massProps.mass, 0.0f);
453
+
454
+ JPH_Shape_Destroy((JPH_Shape*)shape);
455
+ }
456
+
457
+ TEST_F(ShapesTest, Shape_MustBeStatic) {
458
+ // Regular sphere doesn't need to be static
459
+ JPH_SphereShape* sphere = JPH_SphereShape_Create(1.0f);
460
+ EXPECT_FALSE(JPH_Shape_MustBeStatic((const JPH_Shape*)sphere));
461
+ JPH_Shape_Destroy((JPH_Shape*)sphere);
462
+
463
+ // Plane must be static
464
+ JPH_Plane plane = {{0.0f, 1.0f, 0.0f}, 0.0f};
465
+ JPH_PlaneShape* planeShape = JPH_PlaneShape_Create(&plane, nullptr, 1000.0f);
466
+ EXPECT_TRUE(JPH_Shape_MustBeStatic((const JPH_Shape*)planeShape));
467
+ JPH_Shape_Destroy((JPH_Shape*)planeShape);
468
+ }
469
+
470
+ // ============================================================================
471
+ // Convex Shape Density Tests
472
+ // ============================================================================
473
+
474
+ TEST_F(ShapesTest, ConvexShape_Density) {
475
+ JPH_SphereShapeSettings* settings = JPH_SphereShapeSettings_Create(1.0f);
476
+ ASSERT_NE(settings, nullptr);
477
+
478
+ // Get default density
479
+ float defaultDensity = JPH_ConvexShapeSettings_GetDensity((const JPH_ConvexShapeSettings*)settings);
480
+ EXPECT_GT(defaultDensity, 0.0f);
481
+
482
+ // Set new density
483
+ JPH_ConvexShapeSettings_SetDensity((JPH_ConvexShapeSettings*)settings, 2000.0f);
484
+ float newDensity = JPH_ConvexShapeSettings_GetDensity((const JPH_ConvexShapeSettings*)settings);
485
+ EXPECT_FLOAT_EQ(newDensity, 2000.0f);
486
+
487
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
488
+ }
489
+
490
+ TEST_F(ShapesTest, ConvexShape_SetDensity) {
491
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
492
+ ASSERT_NE(shape, nullptr);
493
+
494
+ // Get default density
495
+ float defaultDensity = JPH_ConvexShape_GetDensity((const JPH_ConvexShape*)shape);
496
+ EXPECT_GT(defaultDensity, 0.0f);
497
+
498
+ // Set new density
499
+ JPH_ConvexShape_SetDensity((JPH_ConvexShape*)shape, 5000.0f);
500
+ float newDensity = JPH_ConvexShape_GetDensity((const JPH_ConvexShape*)shape);
501
+ EXPECT_FLOAT_EQ(newDensity, 5000.0f);
502
+
503
+ JPH_Shape_Destroy((JPH_Shape*)shape);
504
+ }
505
+
506
+ // ============================================================================
507
+ // ShapeSettings UserData Tests
508
+ // ============================================================================
509
+
510
+ TEST_F(ShapesTest, ShapeSettings_UserData) {
511
+ JPH_SphereShapeSettings* settings = JPH_SphereShapeSettings_Create(1.0f);
512
+ ASSERT_NE(settings, nullptr);
513
+
514
+ JPH_ShapeSettings_SetUserData((JPH_ShapeSettings*)settings, 99999);
515
+ uint64_t userData = JPH_ShapeSettings_GetUserData((const JPH_ShapeSettings*)settings);
516
+ EXPECT_EQ(userData, 99999u);
517
+
518
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
519
+ }
520
+
521
+ // ============================================================================
522
+ // Decorated Shape Tests
523
+ // ============================================================================
524
+
525
+ TEST_F(ShapesTest, RotatedTranslatedShape_Create) {
526
+ JPH_SphereShape* innerShape = JPH_SphereShape_Create(1.0f);
527
+ ASSERT_NE(innerShape, nullptr);
528
+
529
+ JPH_Vec3 position = {1.0f, 2.0f, 3.0f};
530
+ JPH_Quat rotation = {0.0f, 0.0f, 0.0f, 1.0f}; // Identity
531
+
532
+ JPH_RotatedTranslatedShape* shape = JPH_RotatedTranslatedShape_Create(&position, &rotation, (const JPH_Shape*)innerShape);
533
+ ASSERT_NE(shape, nullptr);
534
+
535
+ JPH_Vec3 resultPos;
536
+ JPH_RotatedTranslatedShape_GetPosition(shape, &resultPos);
537
+ EXPECT_FLOAT_EQ(resultPos.x, 1.0f);
538
+ EXPECT_FLOAT_EQ(resultPos.y, 2.0f);
539
+ EXPECT_FLOAT_EQ(resultPos.z, 3.0f);
540
+
541
+ JPH_Quat resultRot;
542
+ JPH_RotatedTranslatedShape_GetRotation(shape, &resultRot);
543
+ EXPECT_FLOAT_EQ(resultRot.w, 1.0f);
544
+
545
+ // Get inner shape
546
+ const JPH_Shape* inner = JPH_DecoratedShape_GetInnerShape((const JPH_DecoratedShape*)shape);
547
+ EXPECT_EQ(inner, (const JPH_Shape*)innerShape);
548
+
549
+ JPH_Shape_Destroy((JPH_Shape*)shape);
550
+ // Note: innerShape is owned by the decorated shape and will be destroyed with it
551
+ }
552
+
553
+ TEST_F(ShapesTest, ScaledShape_Create) {
554
+ JPH_SphereShape* innerShape = JPH_SphereShape_Create(1.0f);
555
+ ASSERT_NE(innerShape, nullptr);
556
+
557
+ JPH_Vec3 scale = {2.0f, 2.0f, 2.0f};
558
+ JPH_ScaledShape* shape = JPH_ScaledShape_Create((const JPH_Shape*)innerShape, &scale);
559
+ ASSERT_NE(shape, nullptr);
560
+
561
+ JPH_Vec3 resultScale;
562
+ JPH_ScaledShape_GetScale(shape, &resultScale);
563
+ EXPECT_FLOAT_EQ(resultScale.x, 2.0f);
564
+ EXPECT_FLOAT_EQ(resultScale.y, 2.0f);
565
+ EXPECT_FLOAT_EQ(resultScale.z, 2.0f);
566
+
567
+ JPH_Shape_Destroy((JPH_Shape*)shape);
568
+ }
569
+
570
+ TEST_F(ShapesTest, OffsetCenterOfMassShape_Create) {
571
+ JPH_SphereShape* innerShape = JPH_SphereShape_Create(1.0f);
572
+ ASSERT_NE(innerShape, nullptr);
573
+
574
+ JPH_Vec3 offset = {0.0f, 1.0f, 0.0f};
575
+ JPH_OffsetCenterOfMassShape* shape = JPH_OffsetCenterOfMassShape_Create(&offset, (const JPH_Shape*)innerShape);
576
+ ASSERT_NE(shape, nullptr);
577
+
578
+ JPH_Vec3 resultOffset;
579
+ JPH_OffsetCenterOfMassShape_GetOffset(shape, &resultOffset);
580
+ EXPECT_FLOAT_EQ(resultOffset.x, 0.0f);
581
+ EXPECT_FLOAT_EQ(resultOffset.y, 1.0f);
582
+ EXPECT_FLOAT_EQ(resultOffset.z, 0.0f);
583
+
584
+ JPH_Shape_Destroy((JPH_Shape*)shape);
585
+ }
586
+
587
+ // ============================================================================
588
+ // Compound Shape Tests
589
+ // ============================================================================
590
+
591
+ TEST_F(ShapesTest, StaticCompoundShape_Create) {
592
+ JPH_StaticCompoundShapeSettings* settings = JPH_StaticCompoundShapeSettings_Create();
593
+ ASSERT_NE(settings, nullptr);
594
+
595
+ // Create two sphere shapes
596
+ JPH_SphereShape* sphere1 = JPH_SphereShape_Create(1.0f);
597
+ JPH_SphereShape* sphere2 = JPH_SphereShape_Create(0.5f);
598
+
599
+ JPH_Vec3 pos1 = {-1.0f, 0.0f, 0.0f};
600
+ JPH_Vec3 pos2 = {1.0f, 0.0f, 0.0f};
601
+ JPH_Quat rot = {0.0f, 0.0f, 0.0f, 1.0f};
602
+
603
+ JPH_CompoundShapeSettings_AddShape2((JPH_CompoundShapeSettings*)settings, &pos1, &rot, (const JPH_Shape*)sphere1, 0);
604
+ JPH_CompoundShapeSettings_AddShape2((JPH_CompoundShapeSettings*)settings, &pos2, &rot, (const JPH_Shape*)sphere2, 0);
605
+
606
+ JPH_StaticCompoundShape* shape = JPH_StaticCompoundShape_Create(settings);
607
+ ASSERT_NE(shape, nullptr);
608
+
609
+ uint32_t numSubShapes = JPH_CompoundShape_GetNumSubShapes((const JPH_CompoundShape*)shape);
610
+ EXPECT_EQ(numSubShapes, 2u);
611
+
612
+ JPH_Shape_Destroy((JPH_Shape*)shape);
613
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
614
+ }
615
+
616
+ TEST_F(ShapesTest, MutableCompoundShape_Create) {
617
+ JPH_MutableCompoundShapeSettings* settings = JPH_MutableCompoundShapeSettings_Create();
618
+ ASSERT_NE(settings, nullptr);
619
+
620
+ JPH_MutableCompoundShape* shape = JPH_MutableCompoundShape_Create(settings);
621
+ ASSERT_NE(shape, nullptr);
622
+
623
+ // Add a sphere
624
+ JPH_SphereShape* sphere = JPH_SphereShape_Create(1.0f);
625
+ JPH_Vec3 pos = {0.0f, 0.0f, 0.0f};
626
+ JPH_Quat rot = {0.0f, 0.0f, 0.0f, 1.0f};
627
+
628
+ uint32_t index = JPH_MutableCompoundShape_AddShape(shape, &pos, &rot, (const JPH_Shape*)sphere, 0, UINT32_MAX);
629
+ EXPECT_EQ(index, 0u);
630
+
631
+ uint32_t numSubShapes = JPH_CompoundShape_GetNumSubShapes((const JPH_CompoundShape*)shape);
632
+ EXPECT_EQ(numSubShapes, 1u);
633
+
634
+ // Remove the shape
635
+ JPH_MutableCompoundShape_RemoveShape(shape, 0);
636
+ numSubShapes = JPH_CompoundShape_GetNumSubShapes((const JPH_CompoundShape*)shape);
637
+ EXPECT_EQ(numSubShapes, 0u);
638
+
639
+ JPH_Shape_Destroy((JPH_Shape*)shape);
640
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
641
+ }
642
+
643
+ // ============================================================================
644
+ // EmptyShape Tests
645
+ // ============================================================================
646
+
647
+ TEST_F(ShapesTest, EmptyShape_Create) {
648
+ JPH_Vec3 centerOfMass = {0.0f, 0.0f, 0.0f};
649
+ JPH_EmptyShapeSettings* settings = JPH_EmptyShapeSettings_Create(&centerOfMass);
650
+ ASSERT_NE(settings, nullptr);
651
+
652
+ JPH_EmptyShape* shape = JPH_EmptyShapeSettings_CreateShape(settings);
653
+ ASSERT_NE(shape, nullptr);
654
+
655
+ // Empty shape should have zero volume
656
+ float volume = JPH_Shape_GetVolume((const JPH_Shape*)shape);
657
+ EXPECT_FLOAT_EQ(volume, 0.0f);
658
+
659
+ JPH_Shape_Destroy((JPH_Shape*)shape);
660
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
661
+ }
662
+
663
+ // ============================================================================
664
+ // Shape RayCast Tests
665
+ // ============================================================================
666
+
667
+ TEST_F(ShapesTest, Shape_CastRay) {
668
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
669
+ ASSERT_NE(shape, nullptr);
670
+
671
+ // Cast ray from outside the sphere toward its center
672
+ JPH_Vec3 origin = {-5.0f, 0.0f, 0.0f};
673
+ JPH_Vec3 direction = {10.0f, 0.0f, 0.0f}; // Direction, not normalized
674
+ JPH_RayCastResult hit;
675
+
676
+ bool hasHit = JPH_Shape_CastRay((const JPH_Shape*)shape, &origin, &direction, &hit);
677
+ EXPECT_TRUE(hasHit);
678
+
679
+ // The hit should be at fraction ~0.4 (when ray hits sphere at x=-1)
680
+ // fraction = (5-1)/10 = 0.4
681
+ EXPECT_NEAR(hit.fraction, 0.4f, 0.01f);
682
+
683
+ JPH_Shape_Destroy((JPH_Shape*)shape);
684
+ }
685
+
686
+ TEST_F(ShapesTest, Shape_CastRay_Miss) {
687
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
688
+ ASSERT_NE(shape, nullptr);
689
+
690
+ // Cast ray that misses the sphere
691
+ JPH_Vec3 origin = {0.0f, 5.0f, 0.0f};
692
+ JPH_Vec3 direction = {1.0f, 0.0f, 0.0f}; // Parallel to X axis, above sphere
693
+ JPH_RayCastResult hit;
694
+
695
+ bool hasHit = JPH_Shape_CastRay((const JPH_Shape*)shape, &origin, &direction, &hit);
696
+ EXPECT_FALSE(hasHit);
697
+
698
+ JPH_Shape_Destroy((JPH_Shape*)shape);
699
+ }
700
+
701
+ // ============================================================================
702
+ // Shape CollidePoint Tests
703
+ // ============================================================================
704
+
705
+ TEST_F(ShapesTest, Shape_CollidePoint_Inside) {
706
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
707
+ ASSERT_NE(shape, nullptr);
708
+
709
+ // Point inside sphere
710
+ JPH_Vec3 point = {0.0f, 0.0f, 0.0f};
711
+ bool isInside = JPH_Shape_CollidePoint((const JPH_Shape*)shape, &point, nullptr);
712
+ EXPECT_TRUE(isInside);
713
+
714
+ JPH_Shape_Destroy((JPH_Shape*)shape);
715
+ }
716
+
717
+ TEST_F(ShapesTest, Shape_CollidePoint_Outside) {
718
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
719
+ ASSERT_NE(shape, nullptr);
720
+
721
+ // Point outside sphere
722
+ JPH_Vec3 point = {5.0f, 0.0f, 0.0f};
723
+ bool isInside = JPH_Shape_CollidePoint((const JPH_Shape*)shape, &point, nullptr);
724
+ EXPECT_FALSE(isInside);
725
+
726
+ JPH_Shape_Destroy((JPH_Shape*)shape);
727
+ }
728
+
729
+ // ============================================================================
730
+ // TaperedCylinderShape Tests
731
+ // ============================================================================
732
+
733
+ TEST_F(ShapesTest, TaperedCylinderShape_Create) {
734
+ JPH_TaperedCylinderShapeSettings* settings = JPH_TaperedCylinderShapeSettings_Create(
735
+ 1.0f, // halfHeight
736
+ 0.5f, // topRadius
737
+ 1.0f, // bottomRadius
738
+ JPH_DEFAULT_CONVEX_RADIUS,
739
+ nullptr
740
+ );
741
+ ASSERT_NE(settings, nullptr);
742
+
743
+ JPH_TaperedCylinderShape* shape = JPH_TaperedCylinderShapeSettings_CreateShape(settings);
744
+ ASSERT_NE(shape, nullptr);
745
+
746
+ float topRadius = JPH_TaperedCylinderShape_GetTopRadius(shape);
747
+ float bottomRadius = JPH_TaperedCylinderShape_GetBottomRadius(shape);
748
+ float halfHeight = JPH_TaperedCylinderShape_GetHalfHeight(shape);
749
+
750
+ EXPECT_FLOAT_EQ(topRadius, 0.5f);
751
+ EXPECT_FLOAT_EQ(bottomRadius, 1.0f);
752
+ EXPECT_FLOAT_EQ(halfHeight, 1.0f);
753
+
754
+ JPH_Shape_Destroy((JPH_Shape*)shape);
755
+ JPH_ShapeSettings_Destroy((JPH_ShapeSettings*)settings);
756
+ }
757
+
758
+ // ============================================================================
759
+ // Shape Scale Validation Tests
760
+ // ============================================================================
761
+
762
+ TEST_F(ShapesTest, Shape_IsValidScale) {
763
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
764
+ ASSERT_NE(shape, nullptr);
765
+
766
+ // Uniform scale is valid for sphere
767
+ JPH_Vec3 uniformScale = {2.0f, 2.0f, 2.0f};
768
+ EXPECT_TRUE(JPH_Shape_IsValidScale((const JPH_Shape*)shape, &uniformScale));
769
+
770
+ // Non-uniform scale is NOT valid for sphere (spheres only support uniform scale)
771
+ JPH_Vec3 nonUniformScale = {2.0f, 1.0f, 1.0f};
772
+ EXPECT_FALSE(JPH_Shape_IsValidScale((const JPH_Shape*)shape, &nonUniformScale));
773
+
774
+ JPH_Shape_Destroy((JPH_Shape*)shape);
775
+ }
776
+
777
+ TEST_F(ShapesTest, Shape_MakeScaleValid) {
778
+ JPH_SphereShape* shape = JPH_SphereShape_Create(1.0f);
779
+ ASSERT_NE(shape, nullptr);
780
+
781
+ JPH_Vec3 nonUniformScale = {2.0f, 1.0f, 1.0f};
782
+ JPH_Vec3 result;
783
+ JPH_Shape_MakeScaleValid((const JPH_Shape*)shape, &nonUniformScale, &result);
784
+
785
+ // Result should be a valid (uniform) scale
786
+ EXPECT_TRUE(JPH_Shape_IsValidScale((const JPH_Shape*)shape, &result));
787
+
788
+ JPH_Shape_Destroy((JPH_Shape*)shape);
789
+ }