@bornengine/engine 0.4.16

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,1908 @@
1
+ //! Jolt-backed physics world for Bloom Engine.
2
+ //!
3
+ //! Wraps `jolt_sys` behind f64 handle registries so Perry FFI can speak to
4
+ //! Jolt using only scalar values.
5
+ //!
6
+ //! Handle conventions:
7
+ //! - bloom handles are 1-based f64 indices into HandleRegistry
8
+ //! - bj_world, bj_shape, bj_body, bj_constraint are native Jolt handles
9
+ //! - Body/constraint handles are scoped to a single world internally
10
+ //!
11
+ //! Thread-safety: Jolt's contact listener runs on job threads; the queue it
12
+ //! fills is already mutex-protected inside `bloom_jolt.cpp`. Everything in
13
+ //! this file is single-threaded (matches Bloom's engine-main-thread model).
14
+
15
+ use crate::handles::HandleRegistry;
16
+ use crate::jolt_sys::*;
17
+
18
+ use std::sync::Once;
19
+
20
+ static GLOBAL_INIT: Once = Once::new();
21
+
22
+ /// Ensures `bj_global_init` is called exactly once per process.
23
+ pub fn ensure_jolt_initialised() {
24
+ GLOBAL_INIT.call_once(|| {
25
+ // SAFETY: called exactly once; bj_global_init is refcounted and idempotent.
26
+ let r = unsafe { bj_global_init() };
27
+ assert_eq!(r, BjResult::Ok, "Jolt global init failed");
28
+ });
29
+ }
30
+
31
+ // ============================================================
32
+ // Cached query results (multi-call read-back pattern)
33
+ // ============================================================
34
+
35
+ #[derive(Default)]
36
+ struct RayHitCache {
37
+ hits: Vec<BjRayHit>,
38
+ }
39
+
40
+ #[derive(Default)]
41
+ struct OverlapCache {
42
+ bodies: Vec<bj_body>,
43
+ }
44
+
45
+ #[derive(Default)]
46
+ struct ContactCache {
47
+ /// Drained from Jolt's queue at the start of each read cycle.
48
+ events: Vec<BjContact>,
49
+ }
50
+
51
+ // ============================================================
52
+ // JoltPhysics — owned by EngineState, indexed by f64 handles
53
+ // ============================================================
54
+
55
+ pub struct JoltPhysics {
56
+ /// Usually a single world; supporting a registry keeps the API symmetric.
57
+ worlds: HandleRegistry<bj_world>,
58
+ /// Shapes live across worlds (refcounted in Jolt). Stored as raw pointer values.
59
+ shapes: HandleRegistry<bj_shape>,
60
+ /// Bodies are world-scoped. We remember which world they belong to.
61
+ bodies: HandleRegistry<(bj_world, bj_body)>,
62
+ /// Constraints similarly scoped.
63
+ constraints: HandleRegistry<(bj_world, bj_constraint)>,
64
+ /// Character controllers — world-scoped like bodies/constraints.
65
+ characters: HandleRegistry<(bj_world, bj_character)>,
66
+ /// Wheeled vehicles — world-scoped; stores bj_vehicle + the chassis body
67
+ /// handle it mapped to (for TS-side getChassis()).
68
+ vehicles: HandleRegistry<(bj_world, bj_vehicle, f64 /*chassis body handle*/)>,
69
+
70
+ ray_hit_cache: RayHitCache,
71
+ overlap_cache: OverlapCache,
72
+ contact_cache: ContactCache,
73
+
74
+ /// Scratch streams for variable-size shape inputs (convex hull points,
75
+ /// mesh vertices/indices, heightfield samples). The TS layer pushes
76
+ /// values via `bloom_physics_scratch_push_*` then calls the shape factory.
77
+ scratch_f32: Vec<f32>,
78
+ scratch_u32: Vec<u32>,
79
+ /// Compound-shape builder state — cleared by compound_begin, extended
80
+ /// by compound_add_child, consumed by compound_end.
81
+ compound_children: Vec<(bj_shape, BjTransform)>,
82
+
83
+ /// Per-world fixed-timestep state, keyed by the raw bj_world handle.
84
+ /// Created lazily on the first step_fixed / set_fixed_timestep call;
85
+ /// worlds stepped only via the variable-rate `step()` never allocate
86
+ /// one.
87
+ step_states: std::collections::HashMap<u64, WorldStepState>,
88
+ }
89
+
90
+ /// Fixed-timestep accumulator + interpolation state for one world.
91
+ ///
92
+ /// Variable-dt stepping feeds frame hitches straight into the solver
93
+ /// (tunneling, constraint explosions); the accumulator decouples
94
+ /// simulation rate from render rate the standard way: clamp the incoming
95
+ /// frame dt, simulate N whole fixed steps, carry the remainder, and
96
+ /// expose `alpha = remainder / fixed_dt` so rendering can interpolate
97
+ /// between the last two physics states.
98
+ struct WorldStepState {
99
+ fixed_dt: f32,
100
+ /// Spiral-of-death guard: at most this many fixed steps per frame.
101
+ /// When the cap hits, the surplus backlog is dropped — the simulation
102
+ /// slows down instead of feeding ever-longer frames back into itself.
103
+ max_steps_per_frame: u32,
104
+ accumulator: f32,
105
+ /// remainder / fixed_dt after the latest step_fixed, in [0, 1).
106
+ alpha: f32,
107
+ /// When true, body position/rotation getters blend between the state
108
+ /// snapshot taken before the latest step batch and the current state.
109
+ /// Physics queries (raycast/overlap) always see the real simulation
110
+ /// state regardless.
111
+ interpolate: bool,
112
+ /// bj_body → (position, rotation) captured before the latest batch.
113
+ prev: std::collections::HashMap<u64, (BjVec3, BjQuat)>,
114
+ }
115
+
116
+ impl Default for WorldStepState {
117
+ fn default() -> Self {
118
+ Self {
119
+ fixed_dt: 1.0 / 60.0,
120
+ max_steps_per_frame: 4,
121
+ accumulator: 0.0,
122
+ alpha: 1.0,
123
+ interpolate: false,
124
+ prev: std::collections::HashMap::new(),
125
+ }
126
+ }
127
+ }
128
+
129
+ /// Hard ceiling on a single frame's contribution to the accumulator.
130
+ /// A debugger pause or OS hitch produces one slowed-down frame instead of
131
+ /// minutes of simulated catch-up.
132
+ const MAX_FRAME_DT: f32 = 0.25;
133
+
134
+ impl Default for JoltPhysics {
135
+ fn default() -> Self { Self::new() }
136
+ }
137
+
138
+ impl JoltPhysics {
139
+ pub fn new() -> Self {
140
+ ensure_jolt_initialised();
141
+ Self {
142
+ worlds: HandleRegistry::new(),
143
+ shapes: HandleRegistry::new(),
144
+ bodies: HandleRegistry::new(),
145
+ constraints: HandleRegistry::new(),
146
+ characters: HandleRegistry::new(),
147
+ vehicles: HandleRegistry::new(),
148
+ ray_hit_cache: RayHitCache::default(),
149
+ overlap_cache: OverlapCache::default(),
150
+ contact_cache: ContactCache::default(),
151
+ scratch_f32: Vec::with_capacity(1024),
152
+ scratch_u32: Vec::with_capacity(512),
153
+ compound_children: Vec::with_capacity(16),
154
+ step_states: std::collections::HashMap::new(),
155
+ }
156
+ }
157
+
158
+ // -------------------------------------------------------- Scratch buffer
159
+
160
+ pub fn scratch_reset(&mut self) {
161
+ self.scratch_f32.clear();
162
+ self.scratch_u32.clear();
163
+ }
164
+ pub fn scratch_push_f32(&mut self, v: f32) { self.scratch_f32.push(v); }
165
+ pub fn scratch_push_u32(&mut self, v: u32) { self.scratch_u32.push(v); }
166
+
167
+ pub fn shape_convex_hull_from_scratch(&mut self, num_points: u32, convex_radius: f32) -> f64 {
168
+ let need = (num_points * 3) as usize;
169
+ if self.scratch_f32.len() < need || num_points < 3 { return 0.0; }
170
+ let points: Vec<BjVec3> = (0..num_points as usize).map(|i| BjVec3 {
171
+ x: self.scratch_f32[i * 3],
172
+ y: self.scratch_f32[i * 3 + 1],
173
+ z: self.scratch_f32[i * 3 + 2],
174
+ }).collect();
175
+ self.create_convex_hull_shape(&points, convex_radius)
176
+ }
177
+
178
+ pub fn shape_mesh_from_scratch(&mut self, vertex_count: u32, triangle_count: u32) -> f64 {
179
+ let need_f = (vertex_count * 3) as usize;
180
+ let need_u = (triangle_count * 3) as usize;
181
+ if self.scratch_f32.len() < need_f || self.scratch_u32.len() < need_u { return 0.0; }
182
+ if vertex_count == 0 || triangle_count == 0 { return 0.0; }
183
+ let vertices: Vec<BjVec3> = (0..vertex_count as usize).map(|i| BjVec3 {
184
+ x: self.scratch_f32[i * 3],
185
+ y: self.scratch_f32[i * 3 + 1],
186
+ z: self.scratch_f32[i * 3 + 2],
187
+ }).collect();
188
+ let indices: Vec<u32> = self.scratch_u32[..need_u].to_vec();
189
+ self.create_mesh_shape(&vertices, &indices)
190
+ }
191
+
192
+ #[allow(clippy::too_many_arguments)]
193
+ pub fn shape_heightfield_from_scratch(
194
+ &mut self, sample_count: u32,
195
+ ox: f32, oy: f32, oz: f32, sx: f32, sy: f32, sz: f32, block_size: u32,
196
+ ) -> f64 {
197
+ let need = (sample_count * sample_count) as usize;
198
+ if self.scratch_f32.len() < need || sample_count < 2 { return 0.0; }
199
+ let samples = self.scratch_f32[..need].to_vec();
200
+ self.create_heightfield_shape(
201
+ &samples, sample_count,
202
+ BjVec3 { x: ox, y: oy, z: oz },
203
+ BjVec3 { x: sx, y: sy, z: sz },
204
+ block_size,
205
+ )
206
+ }
207
+
208
+ pub fn compound_begin(&mut self) {
209
+ self.compound_children.clear();
210
+ }
211
+ #[allow(clippy::too_many_arguments)]
212
+ pub fn compound_add_child(&mut self,
213
+ shape_h: f64,
214
+ px: f32, py: f32, pz: f32,
215
+ rx: f32, ry: f32, rz: f32, rw: f32,
216
+ ) {
217
+ if let Some(&shape) = self.shapes.get(shape_h) {
218
+ self.compound_children.push((shape, BjTransform {
219
+ position: BjVec3 { x: px, y: py, z: pz },
220
+ rotation: BjQuat { x: rx, y: ry, z: rz, w: rw },
221
+ }));
222
+ }
223
+ }
224
+ pub fn compound_end(&mut self) -> f64 {
225
+ if self.compound_children.is_empty() { return 0.0; }
226
+ let shapes: Vec<bj_shape> = self.compound_children.iter().map(|(s, _)| *s).collect();
227
+ let xforms: Vec<BjTransform> = self.compound_children.iter().map(|(_, x)| *x).collect();
228
+ let s = unsafe {
229
+ bj_shape_compound_static(shapes.as_ptr(), xforms.as_ptr(), shapes.len() as u32)
230
+ };
231
+ self.compound_children.clear();
232
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
233
+ }
234
+
235
+ // -------------------------------------------------------- World
236
+
237
+ pub fn create_world(
238
+ &mut self,
239
+ gx: f32, gy: f32, gz: f32,
240
+ max_bodies: u32, num_threads: u32,
241
+ ) -> f64 {
242
+ let desc = BjWorldDesc {
243
+ gravity: BjVec3 { x: gx, y: gy, z: gz },
244
+ max_bodies: if max_bodies > 0 { max_bodies } else { 65536 },
245
+ num_threads,
246
+ ..Default::default()
247
+ };
248
+ let world = unsafe { bj_world_create(&desc) };
249
+ if world == BJ_INVALID { return 0.0; }
250
+ self.worlds.alloc(world)
251
+ }
252
+
253
+ pub fn destroy_world(&mut self, handle: f64) {
254
+ if let Some(world) = self.worlds.free(handle) {
255
+ // Destroy bodies and constraints tied to this world first.
256
+ // (Iterate and collect to avoid borrow conflicts.)
257
+ let body_handles: Vec<f64> = self.bodies.iter()
258
+ .filter(|(_, (w, _))| *w == world)
259
+ .map(|(h, _)| h)
260
+ .collect();
261
+ for h in body_handles {
262
+ if let Some((w, b)) = self.bodies.free(h) {
263
+ unsafe { bj_body_destroy(w, b); }
264
+ }
265
+ }
266
+ let c_handles: Vec<f64> = self.constraints.iter()
267
+ .filter(|(_, (w, _))| *w == world)
268
+ .map(|(h, _)| h)
269
+ .collect();
270
+ for h in c_handles {
271
+ if let Some((w, c)) = self.constraints.free(h) {
272
+ unsafe { bj_constraint_destroy(w, c); }
273
+ }
274
+ }
275
+ self.step_states.remove(&world);
276
+ unsafe { bj_world_destroy(world); }
277
+ }
278
+ }
279
+
280
+ pub fn set_gravity(&self, world_h: f64, x: f32, y: f32, z: f32) {
281
+ if let Some(&world) = self.worlds.get(world_h) {
282
+ unsafe { bj_world_set_gravity(world, BjVec3 { x, y, z }); }
283
+ }
284
+ }
285
+
286
+ pub fn get_gravity_axis(&self, world_h: f64, axis: u32) -> f64 {
287
+ if let Some(&world) = self.worlds.get(world_h) {
288
+ let mut g = BjVec3::default();
289
+ unsafe { bj_world_get_gravity(world, &mut g); }
290
+ return match axis { 0 => g.x, 1 => g.y, 2 => g.z, _ => 0.0 } as f64;
291
+ }
292
+ 0.0
293
+ }
294
+
295
+ pub fn optimize_broadphase(&self, world_h: f64) {
296
+ if let Some(&world) = self.worlds.get(world_h) {
297
+ unsafe { bj_world_optimize_broadphase(world); }
298
+ }
299
+ }
300
+
301
+ pub fn step(&mut self, world_h: f64, dt: f32, collision_steps: u32) {
302
+ if let Some(&world) = self.worlds.get(world_h) {
303
+ unsafe { bj_world_step(world, dt, collision_steps.max(1)); }
304
+ self.drain_contacts(world);
305
+ }
306
+ }
307
+
308
+ /// Drain the shim's queued contact events into our cache so they
309
+ /// survive across queries. The shim queue accumulates across multiple
310
+ /// bj_world_step calls, so one drain after a fixed-step batch captures
311
+ /// the whole batch's events.
312
+ fn drain_contacts(&mut self, world: bj_world) {
313
+ let count = unsafe { bj_world_contact_count(world) };
314
+ if count > 0 {
315
+ self.contact_cache.events.resize(count as usize, unsafe { std::mem::zeroed() });
316
+ let drained = unsafe {
317
+ bj_world_pop_contacts(world, self.contact_cache.events.as_mut_ptr(), count)
318
+ };
319
+ self.contact_cache.events.truncate(drained as usize);
320
+ } else {
321
+ self.contact_cache.events.clear();
322
+ }
323
+ }
324
+
325
+ /// Advance the world by whole fixed steps, carrying the remainder.
326
+ /// Returns the interpolation alpha in [0, 1): how far the carried
327
+ /// remainder sits between the last simulated state and the next.
328
+ ///
329
+ /// This is the default stepping mode for the TS `physics.step()` API;
330
+ /// the variable-rate `step()` above remains as an explicit opt-out.
331
+ pub fn step_fixed(&mut self, world_h: f64, frame_dt: f32, collision_steps: u32) -> f32 {
332
+ let Some(&world) = self.worlds.get(world_h) else { return 1.0 };
333
+ let dt = if frame_dt.is_finite() { frame_dt.clamp(0.0, MAX_FRAME_DT) } else { 0.0 };
334
+
335
+ let st = self.step_states.entry(world).or_default();
336
+ st.accumulator += dt;
337
+ let fixed = st.fixed_dt;
338
+ let mut steps = (st.accumulator / fixed) as u32;
339
+ if steps > st.max_steps_per_frame {
340
+ steps = st.max_steps_per_frame;
341
+ // Drop the surplus backlog (keep at most one extra step's worth
342
+ // so alpha stays meaningful) — slow down, don't spiral.
343
+ st.accumulator = st.accumulator.min(fixed * (steps as f32 + 1.0));
344
+ }
345
+ let interpolate = st.interpolate;
346
+
347
+ if steps > 0 {
348
+ // Interpolation blends between the two most recent simulated
349
+ // states, so the snapshot is taken before the LAST step of the
350
+ // batch — snapshotting before the whole batch would render a
351
+ // backward jump on multi-step catch-up frames.
352
+ let mut snap = None;
353
+ for i in 0..steps {
354
+ if interpolate && i + 1 == steps {
355
+ snap = Some(self.snapshot_world_bodies(world));
356
+ }
357
+ unsafe { bj_world_step(world, fixed, collision_steps.max(1)); }
358
+ }
359
+ if let Some(s) = snap {
360
+ self.step_states.get_mut(&world).unwrap().prev = s;
361
+ }
362
+ self.drain_contacts(world);
363
+ }
364
+
365
+ let st = self.step_states.get_mut(&world).unwrap();
366
+ st.accumulator -= steps as f32 * fixed;
367
+ st.alpha = (st.accumulator / fixed).clamp(0.0, 1.0);
368
+ st.alpha
369
+ }
370
+
371
+ /// Configure the fixed step rate (`hz`, e.g. 60.0) and the per-frame
372
+ /// catch-up cap for a world. Values <= 0 keep the current setting.
373
+ pub fn set_fixed_timestep(&mut self, world_h: f64, hz: f32, max_steps: u32) {
374
+ let Some(&world) = self.worlds.get(world_h) else { return };
375
+ let st = self.step_states.entry(world).or_default();
376
+ if hz > 0.0 && hz.is_finite() {
377
+ st.fixed_dt = 1.0 / hz;
378
+ }
379
+ if max_steps > 0 {
380
+ st.max_steps_per_frame = max_steps;
381
+ }
382
+ }
383
+
384
+ /// Enable/disable render interpolation for a world's body transform
385
+ /// getters. Off by default (getters return the raw simulation state).
386
+ pub fn set_interpolation(&mut self, world_h: f64, on: bool) {
387
+ let Some(&world) = self.worlds.get(world_h) else { return };
388
+ let st = self.step_states.entry(world).or_default();
389
+ st.interpolate = on;
390
+ if !on {
391
+ st.prev.clear();
392
+ st.alpha = 1.0;
393
+ }
394
+ }
395
+
396
+ /// Interpolation alpha from the most recent step_fixed (1.0 when the
397
+ /// world has never been fixed-stepped).
398
+ pub fn step_alpha(&self, world_h: f64) -> f32 {
399
+ self.worlds
400
+ .get(world_h)
401
+ .and_then(|w| self.step_states.get(w))
402
+ .map(|st| st.alpha)
403
+ .unwrap_or(1.0)
404
+ }
405
+
406
+ /// Capture (position, rotation) of every body in `world`.
407
+ fn snapshot_world_bodies(&self, world: bj_world) -> std::collections::HashMap<u64, (BjVec3, BjQuat)> {
408
+ self.bodies
409
+ .iter()
410
+ .filter(|(_, &(w, _))| w == world)
411
+ .map(|(_, &(w, b))| {
412
+ let mut v = BjVec3 { x: 0.0, y: 0.0, z: 0.0 };
413
+ let mut q = BjQuat { x: 0.0, y: 0.0, z: 0.0, w: 1.0 };
414
+ unsafe {
415
+ bj_body_get_position(w, b, &mut v);
416
+ bj_body_get_rotation(w, b, &mut q);
417
+ }
418
+ (b, (v, q))
419
+ })
420
+ .collect()
421
+ }
422
+
423
+ /// Blend factor + previous state for a body, when its world has
424
+ /// interpolation enabled and a fresh snapshot. None → return raw state.
425
+ fn interp_prev(&self, world: bj_world, body: bj_body) -> Option<(f32, &(BjVec3, BjQuat))> {
426
+ let st = self.step_states.get(&world)?;
427
+ if !st.interpolate || st.alpha >= 1.0 {
428
+ return None;
429
+ }
430
+ st.prev.get(&body).map(|p| (st.alpha, p))
431
+ }
432
+
433
+ pub fn set_layer_collides(&self, world_h: f64, a: u32, b: u32, collides: bool) {
434
+ if let Some(&world) = self.worlds.get(world_h) {
435
+ unsafe { bj_world_set_layer_collides(world, a, b, collides as u8); }
436
+ }
437
+ }
438
+
439
+ pub fn get_layer_collides(&self, world_h: f64, a: u32, b: u32) -> bool {
440
+ if let Some(&world) = self.worlds.get(world_h) {
441
+ return unsafe { bj_world_get_layer_collides(world, a, b) } != 0;
442
+ }
443
+ false
444
+ }
445
+
446
+ pub fn body_count(&self, world_h: f64) -> u32 {
447
+ if let Some(&world) = self.worlds.get(world_h) {
448
+ return unsafe { bj_world_body_count(world) };
449
+ }
450
+ 0
451
+ }
452
+
453
+ pub fn active_body_count(&self, world_h: f64) -> u32 {
454
+ if let Some(&world) = self.worlds.get(world_h) {
455
+ return unsafe { bj_world_active_body_count(world) };
456
+ }
457
+ 0
458
+ }
459
+
460
+ // -------------------------------------------------------- Shapes
461
+
462
+ pub fn create_box_shape(&mut self, hx: f32, hy: f32, hz: f32, convex_radius: f32) -> f64 {
463
+ let s = unsafe { bj_shape_box(BjVec3 { x: hx, y: hy, z: hz }, convex_radius) };
464
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
465
+ }
466
+
467
+ pub fn create_sphere_shape(&mut self, radius: f32) -> f64 {
468
+ let s = unsafe { bj_shape_sphere(radius) };
469
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
470
+ }
471
+
472
+ pub fn create_capsule_shape(&mut self, half_height: f32, radius: f32) -> f64 {
473
+ let s = unsafe { bj_shape_capsule(half_height, radius) };
474
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
475
+ }
476
+
477
+ pub fn create_cylinder_shape(&mut self, half_height: f32, radius: f32, convex_radius: f32) -> f64 {
478
+ let s = unsafe { bj_shape_cylinder(half_height, radius, convex_radius) };
479
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
480
+ }
481
+
482
+ pub fn create_convex_hull_shape(&mut self, points: &[BjVec3], convex_radius: f32) -> f64 {
483
+ let s = unsafe {
484
+ bj_shape_convex_hull(points.as_ptr(), points.len() as u32, convex_radius)
485
+ };
486
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
487
+ }
488
+
489
+ pub fn create_mesh_shape(&mut self, vertices: &[BjVec3], indices: &[u32]) -> f64 {
490
+ debug_assert!(indices.len() % 3 == 0);
491
+ let s = unsafe {
492
+ bj_shape_mesh(
493
+ vertices.as_ptr(), vertices.len() as u32,
494
+ indices.as_ptr(), (indices.len() / 3) as u32,
495
+ )
496
+ };
497
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
498
+ }
499
+
500
+ pub fn create_heightfield_shape(
501
+ &mut self,
502
+ samples: &[f32], sample_count: u32,
503
+ offset: BjVec3, scale: BjVec3, block_size: u32,
504
+ ) -> f64 {
505
+ let s = unsafe {
506
+ bj_shape_heightfield(
507
+ samples.as_ptr(), sample_count, offset, scale, block_size,
508
+ )
509
+ };
510
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
511
+ }
512
+
513
+ pub fn create_scaled_shape(&mut self, base_h: f64, sx: f32, sy: f32, sz: f32) -> f64 {
514
+ let base = match self.shapes.get(base_h) { Some(&s) => s, None => return 0.0 };
515
+ let s = unsafe { bj_shape_scaled(base, BjVec3 { x: sx, y: sy, z: sz }) };
516
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
517
+ }
518
+
519
+ pub fn create_offset_com_shape(&mut self, base_h: f64, ox: f32, oy: f32, oz: f32) -> f64 {
520
+ let base = match self.shapes.get(base_h) { Some(&s) => s, None => return 0.0 };
521
+ let s = unsafe { bj_shape_offset_com(base, BjVec3 { x: ox, y: oy, z: oz }) };
522
+ if s == BJ_INVALID { 0.0 } else { self.shapes.alloc(s) }
523
+ }
524
+
525
+ pub fn release_shape(&mut self, handle: f64) {
526
+ if let Some(s) = self.shapes.free(handle) {
527
+ unsafe { bj_shape_release(s); }
528
+ }
529
+ }
530
+
531
+ pub fn shape_bounds_axis(&self, handle: f64, axis: u32) -> f64 {
532
+ if let Some(&s) = self.shapes.get(handle) {
533
+ let mut mn = BjVec3::default();
534
+ let mut mx = BjVec3::default();
535
+ unsafe { bj_shape_get_local_bounds(s, &mut mn, &mut mx); }
536
+ return match axis {
537
+ 0 => mn.x, 1 => mn.y, 2 => mn.z,
538
+ 3 => mx.x, 4 => mx.y, 5 => mx.z,
539
+ _ => 0.0,
540
+ } as f64;
541
+ }
542
+ 0.0
543
+ }
544
+
545
+ pub fn shape_volume(&self, handle: f64) -> f32 {
546
+ self.shapes.get(handle).map(|&s| unsafe { bj_shape_get_volume(s) }).unwrap_or(0.0)
547
+ }
548
+
549
+ // -------------------------------------------------------- Bodies
550
+
551
+ /// Full body descriptor — passed as flat scalars by the FFI wrapper.
552
+ #[allow(clippy::too_many_arguments)]
553
+ pub fn create_body(
554
+ &mut self,
555
+ world_h: f64, shape_h: f64,
556
+ motion_type: u32,
557
+ px: f32, py: f32, pz: f32,
558
+ rx: f32, ry: f32, rz: f32, rw: f32,
559
+ vx: f32, vy: f32, vz: f32,
560
+ wx: f32, wy: f32, wz: f32,
561
+ object_layer: u32,
562
+ is_sensor: bool, allow_sleeping: bool, use_ccd: bool, start_awake: bool,
563
+ friction: f32, restitution: f32,
564
+ lin_damp: f32, ang_damp: f32, gravity_factor: f32,
565
+ mass_override: f32,
566
+ ix: f32, iy: f32, iz: f32,
567
+ user_data: u64,
568
+ ) -> f64 {
569
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0.0 };
570
+ let shape = match self.shapes.get(shape_h) { Some(&s) => s, None => return 0.0 };
571
+
572
+ let desc = BjBodyDesc {
573
+ motion_type: match motion_type {
574
+ 0 => BjMotionType::Static,
575
+ 1 => BjMotionType::Kinematic,
576
+ _ => BjMotionType::Dynamic,
577
+ },
578
+ position: BjVec3 { x: px, y: py, z: pz },
579
+ rotation: BjQuat { x: rx, y: ry, z: rz, w: rw },
580
+ linear_velocity: BjVec3 { x: vx, y: vy, z: vz },
581
+ angular_velocity: BjVec3 { x: wx, y: wy, z: wz },
582
+ gravity_factor,
583
+ linear_damping: lin_damp,
584
+ angular_damping: ang_damp,
585
+ friction,
586
+ restitution,
587
+ mass_override,
588
+ inertia_diag_override: BjVec3 { x: ix, y: iy, z: iz },
589
+ object_layer,
590
+ is_sensor: is_sensor as u8,
591
+ allow_sleeping: allow_sleeping as u8,
592
+ use_ccd: use_ccd as u8,
593
+ start_awake: start_awake as u8,
594
+ user_data,
595
+ };
596
+ let body = unsafe { bj_body_create(world, shape, &desc) };
597
+ if body == BJ_INVALID { 0.0 } else { self.bodies.alloc((world, body)) }
598
+ }
599
+
600
+ pub fn destroy_body(&mut self, handle: f64) {
601
+ if let Some((world, body)) = self.bodies.free(handle) {
602
+ unsafe { bj_body_destroy(world, body); }
603
+ }
604
+ }
605
+
606
+ fn resolve_body(&self, h: f64) -> Option<(bj_world, bj_body)> {
607
+ self.bodies.get(h).copied()
608
+ }
609
+
610
+ pub fn body_activate(&self, h: f64) {
611
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_activate(w, b); } }
612
+ }
613
+ pub fn body_deactivate(&self, h: f64) {
614
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_deactivate(w, b); } }
615
+ }
616
+ pub fn body_is_active(&self, h: f64) -> bool {
617
+ self.resolve_body(h).map(|(w, b)| unsafe { bj_body_is_active(w, b) } != 0).unwrap_or(false)
618
+ }
619
+ pub fn body_is_valid(&self, h: f64) -> bool {
620
+ self.resolve_body(h).map(|(w, b)| unsafe { bj_body_is_valid(w, b) } != 0).unwrap_or(false)
621
+ }
622
+
623
+ /// Position for rendering. When the world has interpolation enabled
624
+ /// (set_interpolation), blends between the last two simulated states
625
+ /// using the step_fixed remainder; otherwise the raw simulation state.
626
+ pub fn body_get_position_axis(&self, h: f64, axis: u32) -> f64 {
627
+ if let Some((w, b)) = self.resolve_body(h) {
628
+ let mut v = BjVec3::default();
629
+ unsafe { bj_body_get_position(w, b, &mut v); }
630
+ if let Some((a, (pv, _))) = self.interp_prev(w, b) {
631
+ v = BjVec3 {
632
+ x: pv.x + (v.x - pv.x) * a,
633
+ y: pv.y + (v.y - pv.y) * a,
634
+ z: pv.z + (v.z - pv.z) * a,
635
+ };
636
+ }
637
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
638
+ }
639
+ 0.0
640
+ }
641
+ /// Rotation for rendering — see body_get_position_axis. Interpolation
642
+ /// is a normalized lerp with shortest-path sign handling (nlerp): for
643
+ /// the sub-17ms rotation deltas a 60 Hz step produces, nlerp is
644
+ /// visually identical to slerp and considerably cheaper.
645
+ pub fn body_get_rotation_axis(&self, h: f64, axis: u32) -> f64 {
646
+ if let Some((w, b)) = self.resolve_body(h) {
647
+ let mut q = BjQuat::default();
648
+ unsafe { bj_body_get_rotation(w, b, &mut q); }
649
+ if let Some((a, (_, pq))) = self.interp_prev(w, b) {
650
+ // shortest path: flip the previous quat if the hemispheres differ
651
+ let dot = pq.x * q.x + pq.y * q.y + pq.z * q.z + pq.w * q.w;
652
+ let s = if dot < 0.0 { -1.0 } else { 1.0 };
653
+ let mut r = BjQuat {
654
+ x: pq.x * s + (q.x - pq.x * s) * a,
655
+ y: pq.y * s + (q.y - pq.y * s) * a,
656
+ z: pq.z * s + (q.z - pq.z * s) * a,
657
+ w: pq.w * s + (q.w - pq.w * s) * a,
658
+ };
659
+ let len = (r.x * r.x + r.y * r.y + r.z * r.z + r.w * r.w).sqrt();
660
+ if len > 1e-6 {
661
+ r = BjQuat { x: r.x / len, y: r.y / len, z: r.z / len, w: r.w / len };
662
+ }
663
+ q = r;
664
+ }
665
+ return match axis { 0 => q.x, 1 => q.y, 2 => q.z, 3 => q.w, _ => 0.0 } as f64;
666
+ }
667
+ 0.0
668
+ }
669
+
670
+ pub fn body_set_position(&self, h: f64, x: f32, y: f32, z: f32, activate: bool) {
671
+ if let Some((w, b)) = self.resolve_body(h) {
672
+ unsafe { bj_body_set_position(w, b, BjVec3 { x, y, z }, if activate { BjActivation::Activate } else { BjActivation::DontActivate }); }
673
+ }
674
+ }
675
+ pub fn body_set_rotation(&self, h: f64, x: f32, y: f32, z: f32, ww: f32, activate: bool) {
676
+ if let Some((w, b)) = self.resolve_body(h) {
677
+ unsafe { bj_body_set_rotation(w, b, BjQuat { x, y, z, w: ww }, if activate { BjActivation::Activate } else { BjActivation::DontActivate }); }
678
+ }
679
+ }
680
+ #[allow(clippy::too_many_arguments)]
681
+ pub fn body_set_transform(&self, h: f64, px: f32, py: f32, pz: f32, rx: f32, ry: f32, rz: f32, rw: f32, activate: bool) {
682
+ if let Some((w, b)) = self.resolve_body(h) {
683
+ let x = BjTransform { position: BjVec3 { x: px, y: py, z: pz }, rotation: BjQuat { x: rx, y: ry, z: rz, w: rw } };
684
+ unsafe { bj_body_set_transform(w, b, &x, if activate { BjActivation::Activate } else { BjActivation::DontActivate }); }
685
+ }
686
+ }
687
+ #[allow(clippy::too_many_arguments)]
688
+ pub fn body_move_kinematic(&self, h: f64, px: f32, py: f32, pz: f32, rx: f32, ry: f32, rz: f32, rw: f32, dt: f32) {
689
+ if let Some((w, b)) = self.resolve_body(h) {
690
+ let target = BjTransform { position: BjVec3 { x: px, y: py, z: pz }, rotation: BjQuat { x: rx, y: ry, z: rz, w: rw } };
691
+ unsafe { bj_body_move_kinematic(w, b, &target, dt); }
692
+ }
693
+ }
694
+
695
+ pub fn body_get_linear_velocity_axis(&self, h: f64, axis: u32) -> f64 {
696
+ if let Some((w, b)) = self.resolve_body(h) {
697
+ let mut v = BjVec3::default();
698
+ unsafe { bj_body_get_linear_velocity(w, b, &mut v); }
699
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
700
+ }
701
+ 0.0
702
+ }
703
+ pub fn body_get_angular_velocity_axis(&self, h: f64, axis: u32) -> f64 {
704
+ if let Some((w, b)) = self.resolve_body(h) {
705
+ let mut v = BjVec3::default();
706
+ unsafe { bj_body_get_angular_velocity(w, b, &mut v); }
707
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
708
+ }
709
+ 0.0
710
+ }
711
+ pub fn body_get_point_velocity_axis(&self, h: f64, px: f32, py: f32, pz: f32, axis: u32) -> f64 {
712
+ if let Some((w, b)) = self.resolve_body(h) {
713
+ let mut v = BjVec3::default();
714
+ unsafe { bj_body_get_point_velocity(w, b, BjVec3 { x: px, y: py, z: pz }, &mut v); }
715
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
716
+ }
717
+ 0.0
718
+ }
719
+ pub fn body_set_linear_velocity(&self, h: f64, x: f32, y: f32, z: f32) {
720
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_linear_velocity(w, b, BjVec3 { x, y, z }); } }
721
+ }
722
+ pub fn body_set_angular_velocity(&self, h: f64, x: f32, y: f32, z: f32) {
723
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_angular_velocity(w, b, BjVec3 { x, y, z }); } }
724
+ }
725
+
726
+ pub fn body_add_force(&self, h: f64, x: f32, y: f32, z: f32) {
727
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_add_force(w, b, BjVec3 { x, y, z }); } }
728
+ }
729
+ pub fn body_add_impulse(&self, h: f64, x: f32, y: f32, z: f32) {
730
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_add_impulse(w, b, BjVec3 { x, y, z }); } }
731
+ }
732
+ pub fn body_add_torque(&self, h: f64, x: f32, y: f32, z: f32) {
733
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_add_torque(w, b, BjVec3 { x, y, z }); } }
734
+ }
735
+ pub fn body_add_angular_impulse(&self, h: f64, x: f32, y: f32, z: f32) {
736
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_add_angular_impulse(w, b, BjVec3 { x, y, z }); } }
737
+ }
738
+ #[allow(clippy::too_many_arguments)]
739
+ pub fn body_add_force_at(&self, h: f64, fx: f32, fy: f32, fz: f32, px: f32, py: f32, pz: f32) {
740
+ if let Some((w, b)) = self.resolve_body(h) {
741
+ unsafe { bj_body_add_force_at(w, b, BjVec3 { x: fx, y: fy, z: fz }, BjVec3 { x: px, y: py, z: pz }); }
742
+ }
743
+ }
744
+ #[allow(clippy::too_many_arguments)]
745
+ pub fn body_add_impulse_at(&self, h: f64, ix: f32, iy: f32, iz: f32, px: f32, py: f32, pz: f32) {
746
+ if let Some((w, b)) = self.resolve_body(h) {
747
+ unsafe { bj_body_add_impulse_at(w, b, BjVec3 { x: ix, y: iy, z: iz }, BjVec3 { x: px, y: py, z: pz }); }
748
+ }
749
+ }
750
+
751
+ pub fn body_set_friction(&self, h: f64, v: f32) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_friction(w, b, v); } } }
752
+ pub fn body_set_restitution(&self, h: f64, v: f32) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_restitution(w, b, v); } } }
753
+ pub fn body_set_linear_damping(&self, h: f64, v: f32) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_linear_damping(w, b, v); } } }
754
+ pub fn body_set_angular_damping(&self, h: f64, v: f32) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_angular_damping(w, b, v); } } }
755
+ pub fn body_set_gravity_factor(&self, h: f64, v: f32) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_gravity_factor(w, b, v); } } }
756
+ pub fn body_set_ccd(&self, h: f64, enabled: bool) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_ccd(w, b, enabled as u8); } } }
757
+ pub fn body_set_motion_type(&self, h: f64, t: u32, activate: bool) {
758
+ if let Some((w, b)) = self.resolve_body(h) {
759
+ let mt = match t { 0 => BjMotionType::Static, 1 => BjMotionType::Kinematic, _ => BjMotionType::Dynamic };
760
+ unsafe { bj_body_set_motion_type(w, b, mt, if activate { BjActivation::Activate } else { BjActivation::DontActivate }); }
761
+ }
762
+ }
763
+ pub fn body_set_object_layer(&self, h: f64, layer: u32) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_object_layer(w, b, layer); } } }
764
+ pub fn body_set_is_sensor(&self, h: f64, enabled: bool) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_is_sensor(w, b, enabled as u8); } } }
765
+ pub fn body_set_allow_sleeping(&self, h: f64, enabled: bool) { if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_allow_sleeping(w, b, enabled as u8); } } }
766
+ pub fn body_set_shape(&self, h: f64, shape_h: f64, update_mass: bool, activate: bool) {
767
+ if let (Some((w, b)), Some(&s)) = (self.resolve_body(h), self.shapes.get(shape_h)) {
768
+ unsafe { bj_body_set_shape(w, b, s, update_mass as u8, if activate { BjActivation::Activate } else { BjActivation::DontActivate }); }
769
+ }
770
+ }
771
+ pub fn body_lock_rotation_axes(&self, h: f64, x: bool, y: bool, z: bool) {
772
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_lock_rotation_axes(w, b, x as u8, y as u8, z as u8); } }
773
+ }
774
+ pub fn body_lock_translation_axes(&self, h: f64, x: bool, y: bool, z: bool) {
775
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_lock_translation_axes(w, b, x as u8, y as u8, z as u8); } }
776
+ }
777
+
778
+ pub fn body_get_mass(&self, h: f64) -> f32 { self.resolve_body(h).map(|(w, b)| unsafe { bj_body_get_mass(w, b) }).unwrap_or(0.0) }
779
+ pub fn body_get_friction(&self, h: f64) -> f32 { self.resolve_body(h).map(|(w, b)| unsafe { bj_body_get_friction(w, b) }).unwrap_or(0.0) }
780
+ pub fn body_get_restitution(&self, h: f64) -> f32 { self.resolve_body(h).map(|(w, b)| unsafe { bj_body_get_restitution(w, b) }).unwrap_or(0.0) }
781
+ pub fn body_get_object_layer(&self, h: f64) -> u32 { self.resolve_body(h).map(|(w, b)| unsafe { bj_body_get_object_layer(w, b) }).unwrap_or(0) }
782
+
783
+ pub fn body_set_user_data(&self, h: f64, user_data: u64) {
784
+ if let Some((w, b)) = self.resolve_body(h) { unsafe { bj_body_set_user_data(w, b, user_data); } }
785
+ }
786
+ pub fn body_get_user_data(&self, h: f64) -> u64 {
787
+ self.resolve_body(h).map(|(w, b)| unsafe { bj_body_get_user_data(w, b) }).unwrap_or(0)
788
+ }
789
+
790
+ // -------------------------------------------------------- Queries
791
+
792
+ /// Single-hit raycast. Stores the hit (if any) at cache index 0.
793
+ #[allow(clippy::too_many_arguments)]
794
+ pub fn raycast_closest(
795
+ &mut self, world_h: f64,
796
+ ox: f32, oy: f32, oz: f32,
797
+ dx: f32, dy: f32, dz: f32,
798
+ max_distance: f32, layer_mask: u32,
799
+ ) -> bool {
800
+ self.ray_hit_cache.hits.clear();
801
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return false };
802
+ let mut hit: BjRayHit = unsafe { std::mem::zeroed() };
803
+ let any = unsafe {
804
+ bj_query_raycast_closest(world,
805
+ BjVec3 { x: ox, y: oy, z: oz },
806
+ BjVec3 { x: dx, y: dy, z: dz },
807
+ max_distance, layer_mask, &mut hit)
808
+ };
809
+ if any != 0 {
810
+ self.ray_hit_cache.hits.push(hit);
811
+ true
812
+ } else { false }
813
+ }
814
+
815
+ /// Multi-hit raycast. Stores up to max_hits in the cache.
816
+ #[allow(clippy::too_many_arguments)]
817
+ pub fn raycast_all(
818
+ &mut self, world_h: f64,
819
+ ox: f32, oy: f32, oz: f32,
820
+ dx: f32, dy: f32, dz: f32,
821
+ max_distance: f32, layer_mask: u32, max_hits: u32,
822
+ ) -> u32 {
823
+ self.ray_hit_cache.hits.clear();
824
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0 };
825
+ self.ray_hit_cache.hits.resize(max_hits as usize, unsafe { std::mem::zeroed() });
826
+ let n = unsafe {
827
+ bj_query_raycast_all(world,
828
+ BjVec3 { x: ox, y: oy, z: oz },
829
+ BjVec3 { x: dx, y: dy, z: dz },
830
+ max_distance, layer_mask,
831
+ self.ray_hit_cache.hits.as_mut_ptr(), max_hits)
832
+ };
833
+ self.ray_hit_cache.hits.truncate(n as usize);
834
+ n
835
+ }
836
+
837
+ pub fn ray_hit_count(&self) -> u32 { self.ray_hit_cache.hits.len() as u32 }
838
+ pub fn ray_hit_body(&self, i: usize) -> f64 {
839
+ self.ray_hit_cache.hits.get(i)
840
+ .and_then(|h| self.bodies.iter().find(|(_, (_, b))| *b == h.body).map(|(hh, _)| hh))
841
+ .unwrap_or(0.0)
842
+ }
843
+ /// field: 0..5 = point.xyz, normal.xyz
844
+ pub fn ray_hit_axis(&self, i: usize, field: u32) -> f64 {
845
+ let h = match self.ray_hit_cache.hits.get(i) { Some(v) => v, None => return 0.0 };
846
+ (match field {
847
+ 0 => h.point.x, 1 => h.point.y, 2 => h.point.z,
848
+ 3 => h.normal.x, 4 => h.normal.y, 5 => h.normal.z,
849
+ _ => 0.0,
850
+ }) as f64
851
+ }
852
+ pub fn ray_hit_fraction(&self, i: usize) -> f32 {
853
+ self.ray_hit_cache.hits.get(i).map(|h| h.fraction).unwrap_or(0.0)
854
+ }
855
+ pub fn ray_hit_sub_shape(&self, i: usize) -> u32 {
856
+ self.ray_hit_cache.hits.get(i).map(|h| h.sub_shape_id).unwrap_or(0)
857
+ }
858
+
859
+ pub fn overlap_sphere(&mut self, world_h: f64, cx: f32, cy: f32, cz: f32, r: f32, layer_mask: u32, max_results: u32) -> u32 {
860
+ self.overlap_cache.bodies.clear();
861
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0 };
862
+ self.overlap_cache.bodies.resize(max_results as usize, BJ_INVALID);
863
+ let n = unsafe {
864
+ bj_query_overlap_sphere(world,
865
+ BjVec3 { x: cx, y: cy, z: cz }, r, layer_mask,
866
+ self.overlap_cache.bodies.as_mut_ptr(), max_results)
867
+ };
868
+ self.overlap_cache.bodies.truncate(n as usize);
869
+ n
870
+ }
871
+
872
+ pub fn overlap_point(&mut self, world_h: f64, px: f32, py: f32, pz: f32, layer_mask: u32, max_results: u32) -> u32 {
873
+ self.overlap_cache.bodies.clear();
874
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0 };
875
+ self.overlap_cache.bodies.resize(max_results as usize, BJ_INVALID);
876
+ let n = unsafe {
877
+ bj_query_overlap_point(world,
878
+ BjVec3 { x: px, y: py, z: pz }, layer_mask,
879
+ self.overlap_cache.bodies.as_mut_ptr(), max_results)
880
+ };
881
+ self.overlap_cache.bodies.truncate(n as usize);
882
+ n
883
+ }
884
+
885
+ #[allow(clippy::too_many_arguments)]
886
+ pub fn overlap_box(
887
+ &mut self, world_h: f64,
888
+ px: f32, py: f32, pz: f32, rx: f32, ry: f32, rz: f32, rw: f32,
889
+ hx: f32, hy: f32, hz: f32,
890
+ layer_mask: u32, max_results: u32,
891
+ ) -> u32 {
892
+ self.overlap_cache.bodies.clear();
893
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0 };
894
+ self.overlap_cache.bodies.resize(max_results as usize, BJ_INVALID);
895
+ let xform = BjTransform { position: BjVec3 { x: px, y: py, z: pz }, rotation: BjQuat { x: rx, y: ry, z: rz, w: rw } };
896
+ let n = unsafe {
897
+ bj_query_overlap_box(world, &xform, BjVec3 { x: hx, y: hy, z: hz }, layer_mask,
898
+ self.overlap_cache.bodies.as_mut_ptr(), max_results)
899
+ };
900
+ self.overlap_cache.bodies.truncate(n as usize);
901
+ n
902
+ }
903
+
904
+ pub fn overlap_body(&self, i: usize) -> f64 {
905
+ self.overlap_cache.bodies.get(i)
906
+ .and_then(|bj| self.bodies.iter().find(|(_, (_, b))| *b == *bj).map(|(h, _)| h))
907
+ .unwrap_or(0.0)
908
+ }
909
+
910
+ // -------------------------------------------------------- Constraints
911
+
912
+ fn make_anchors(&self, body_a_h: f64, body_b_h: f64, ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32, world_space: bool) -> Option<(bj_world, BjConstraintAnchors)> {
913
+ let (wa, ba) = self.resolve_body(body_a_h)?;
914
+ let bb = if body_b_h == 0.0 {
915
+ BJ_INVALID
916
+ } else {
917
+ let (wb, bb) = self.resolve_body(body_b_h)?;
918
+ if wb != wa { return None; }
919
+ bb
920
+ };
921
+ Some((wa, BjConstraintAnchors {
922
+ body_a: ba, body_b: bb,
923
+ anchor_a: BjVec3 { x: ax, y: ay, z: az },
924
+ anchor_b: BjVec3 { x: bx, y: by, z: bz },
925
+ use_world_space: world_space as u8,
926
+ }))
927
+ }
928
+
929
+ #[allow(clippy::too_many_arguments)]
930
+ pub fn constraint_fixed(&mut self, body_a: f64, body_b: f64, ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32, world_space: bool) -> f64 {
931
+ let (w, anchors) = match self.make_anchors(body_a, body_b, ax, ay, az, bx, by, bz, world_space) { Some(v) => v, None => return 0.0 };
932
+ let c = unsafe { bj_constraint_fixed(w, &anchors) };
933
+ if c == BJ_INVALID { 0.0 } else { self.constraints.alloc((w, c)) }
934
+ }
935
+
936
+ #[allow(clippy::too_many_arguments)]
937
+ pub fn constraint_point(&mut self, body_a: f64, body_b: f64, ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32, world_space: bool) -> f64 {
938
+ let (w, anchors) = match self.make_anchors(body_a, body_b, ax, ay, az, bx, by, bz, world_space) { Some(v) => v, None => return 0.0 };
939
+ let c = unsafe { bj_constraint_point(w, &anchors) };
940
+ if c == BJ_INVALID { 0.0 } else { self.constraints.alloc((w, c)) }
941
+ }
942
+
943
+ /// EN-025 — six-DOF, which is what a ragdoll joint actually is: translation
944
+ /// locked (a shoulder does not slide off the torso) and rotation limited (an
945
+ /// elbow does not bend backwards). An unlimited ball joint is cheaper and
946
+ /// gives you spaghetti; the limits are the whole difference between a corpse
947
+ /// and a novelty.
948
+ ///
949
+ /// `rot_limits` is [xmin, xmax, ymin, ymax, zmin, zmax] in radians.
950
+ #[allow(clippy::too_many_arguments)]
951
+ pub fn constraint_six_dof_locked_translation(
952
+ &mut self, body_a: f64, body_b: f64,
953
+ ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32,
954
+ rot_limits: [f32; 6], world_space: bool,
955
+ ) -> f64 {
956
+ let (w, anchors) = match self.make_anchors(body_a, body_b, ax, ay, az, bx, by, bz, world_space) {
957
+ Some(v) => v, None => return 0.0,
958
+ };
959
+ // min >= max means LOCKED (see the shim header), so this pins all three
960
+ // translation axes.
961
+ let trans: [f32; 6] = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
962
+ let c = unsafe {
963
+ bj_constraint_six_dof(w, &anchors, trans.as_ptr(), rot_limits.as_ptr())
964
+ };
965
+ if c == BJ_INVALID { 0.0 } else { self.constraints.alloc((w, c)) }
966
+ }
967
+
968
+ /// Body world transform as (position, quaternion) — the ragdoll needs the
969
+ /// full frame, and the existing getters hand back one axis at a time.
970
+ pub fn body_transform(&self, h: f64) -> Option<([f32; 3], [f32; 4])> {
971
+ let p = [
972
+ self.body_get_position_axis(h, 0) as f32,
973
+ self.body_get_position_axis(h, 1) as f32,
974
+ self.body_get_position_axis(h, 2) as f32,
975
+ ];
976
+ let q = [
977
+ self.body_get_rotation_axis(h, 0) as f32,
978
+ self.body_get_rotation_axis(h, 1) as f32,
979
+ self.body_get_rotation_axis(h, 2) as f32,
980
+ self.body_get_rotation_axis(h, 3) as f32,
981
+ ];
982
+ Some((p, q))
983
+ }
984
+
985
+ #[allow(clippy::too_many_arguments)]
986
+ pub fn constraint_hinge(
987
+ &mut self, body_a: f64, body_b: f64,
988
+ ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32,
989
+ axis_x: f32, axis_y: f32, axis_z: f32,
990
+ limit_min: f32, limit_max: f32, world_space: bool,
991
+ ) -> f64 {
992
+ let (w, anchors) = match self.make_anchors(body_a, body_b, ax, ay, az, bx, by, bz, world_space) { Some(v) => v, None => return 0.0 };
993
+ let c = unsafe {
994
+ bj_constraint_hinge(w, &anchors, BjVec3 { x: axis_x, y: axis_y, z: axis_z }, limit_min, limit_max)
995
+ };
996
+ if c == BJ_INVALID { 0.0 } else { self.constraints.alloc((w, c)) }
997
+ }
998
+
999
+ #[allow(clippy::too_many_arguments)]
1000
+ pub fn constraint_slider(
1001
+ &mut self, body_a: f64, body_b: f64,
1002
+ ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32,
1003
+ axis_x: f32, axis_y: f32, axis_z: f32,
1004
+ limit_min: f32, limit_max: f32, world_space: bool,
1005
+ ) -> f64 {
1006
+ let (w, anchors) = match self.make_anchors(body_a, body_b, ax, ay, az, bx, by, bz, world_space) { Some(v) => v, None => return 0.0 };
1007
+ let c = unsafe {
1008
+ bj_constraint_slider(w, &anchors, BjVec3 { x: axis_x, y: axis_y, z: axis_z }, limit_min, limit_max)
1009
+ };
1010
+ if c == BJ_INVALID { 0.0 } else { self.constraints.alloc((w, c)) }
1011
+ }
1012
+
1013
+ #[allow(clippy::too_many_arguments)]
1014
+ pub fn constraint_distance(
1015
+ &mut self, body_a: f64, body_b: f64,
1016
+ ax: f32, ay: f32, az: f32, bx: f32, by: f32, bz: f32,
1017
+ min_distance: f32, max_distance: f32, world_space: bool,
1018
+ ) -> f64 {
1019
+ let (w, anchors) = match self.make_anchors(body_a, body_b, ax, ay, az, bx, by, bz, world_space) { Some(v) => v, None => return 0.0 };
1020
+ let c = unsafe { bj_constraint_distance(w, &anchors, min_distance, max_distance) };
1021
+ if c == BJ_INVALID { 0.0 } else { self.constraints.alloc((w, c)) }
1022
+ }
1023
+
1024
+ pub fn constraint_destroy(&mut self, handle: f64) {
1025
+ if let Some((w, c)) = self.constraints.free(handle) {
1026
+ unsafe { bj_constraint_destroy(w, c); }
1027
+ }
1028
+ }
1029
+
1030
+ pub fn constraint_set_enabled(&self, handle: f64, enabled: bool) {
1031
+ if let Some(&(w, c)) = self.constraints.get(handle) {
1032
+ unsafe { bj_constraint_set_enabled(w, c, enabled as u8); }
1033
+ }
1034
+ }
1035
+
1036
+ // -------------------------------------------------------- Contacts
1037
+
1038
+ pub fn contact_count(&self) -> u32 { self.contact_cache.events.len() as u32 }
1039
+
1040
+ /// field: 0=event, 1=bodyA, 2=bodyB, 3..5=pointA.xyz, 6..8=pointB.xyz,
1041
+ /// 9..11=normal.xyz, 12=depth, 13=friction, 14=restitution
1042
+ pub fn contact_field(&self, i: usize, field: u32) -> f64 {
1043
+ let c = match self.contact_cache.events.get(i) { Some(v) => v, None => return 0.0 };
1044
+ let bj_to_bloom = |bj: bj_body| -> f64 {
1045
+ self.bodies.iter().find(|(_, (_, b))| *b == bj).map(|(h, _)| h).unwrap_or(0.0)
1046
+ };
1047
+ match field {
1048
+ 0 => c.event as u32 as f64,
1049
+ 1 => bj_to_bloom(c.body_a),
1050
+ 2 => bj_to_bloom(c.body_b),
1051
+ 3 => c.point_a.x as f64, 4 => c.point_a.y as f64, 5 => c.point_a.z as f64,
1052
+ 6 => c.point_b.x as f64, 7 => c.point_b.y as f64, 8 => c.point_b.z as f64,
1053
+ 9 => c.normal.x as f64, 10=> c.normal.y as f64, 11=> c.normal.z as f64,
1054
+ 12 => c.penetration_depth as f64,
1055
+ 13 => c.combined_friction as f64,
1056
+ 14 => c.combined_restitution as f64,
1057
+ _ => 0.0,
1058
+ }
1059
+ }
1060
+
1061
+ pub fn clear_contacts(&mut self) { self.contact_cache.events.clear(); }
1062
+
1063
+ // -------------------------------------------------------- Character controller
1064
+
1065
+ #[allow(clippy::too_many_arguments)]
1066
+ pub fn character_create(
1067
+ &mut self, world_h: f64, shape_h: f64,
1068
+ up_x: f32, up_y: f32, up_z: f32,
1069
+ max_slope_angle: f32, character_padding: f32,
1070
+ penetration_recovery_speed: f32, predictive_contact_distance: f32,
1071
+ max_strength: f32, mass: f32, object_layer: u32,
1072
+ px: f32, py: f32, pz: f32,
1073
+ rx: f32, ry: f32, rz: f32, rw: f32,
1074
+ ) -> f64 {
1075
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0.0 };
1076
+ let shape = match self.shapes.get(shape_h) { Some(&s) => s, None => return 0.0 };
1077
+ let desc = BjCharacterDesc {
1078
+ up: BjVec3 { x: up_x, y: up_y, z: up_z },
1079
+ max_slope_angle, character_padding, penetration_recovery_speed,
1080
+ predictive_contact_distance, max_strength, mass, object_layer,
1081
+ };
1082
+ let c = unsafe {
1083
+ bj_character_create(world, shape, &desc,
1084
+ BjVec3 { x: px, y: py, z: pz },
1085
+ BjQuat { x: rx, y: ry, z: rz, w: rw })
1086
+ };
1087
+ if c == BJ_INVALID { 0.0 } else { self.characters.alloc((world, c)) }
1088
+ }
1089
+
1090
+ pub fn character_destroy(&mut self, handle: f64) {
1091
+ if let Some((w, c)) = self.characters.free(handle) {
1092
+ unsafe { bj_character_destroy(w, c); }
1093
+ }
1094
+ }
1095
+
1096
+ fn resolve_character(&self, h: f64) -> Option<(bj_world, bj_character)> {
1097
+ self.characters.get(h).copied()
1098
+ }
1099
+
1100
+ pub fn character_update(&self, h: f64, dt: f32, gx: f32, gy: f32, gz: f32) {
1101
+ if let Some((w, c)) = self.resolve_character(h) {
1102
+ unsafe { bj_character_update(w, c, dt, BjVec3 { x: gx, y: gy, z: gz }); }
1103
+ }
1104
+ }
1105
+
1106
+ pub fn character_get_position_axis(&self, h: f64, axis: u32) -> f64 {
1107
+ if let Some((w, c)) = self.resolve_character(h) {
1108
+ let mut v = BjVec3::default();
1109
+ unsafe { bj_character_get_position(w, c, &mut v); }
1110
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
1111
+ }
1112
+ 0.0
1113
+ }
1114
+ pub fn character_get_rotation_axis(&self, h: f64, axis: u32) -> f64 {
1115
+ if let Some((w, c)) = self.resolve_character(h) {
1116
+ let mut q = BjQuat::default();
1117
+ unsafe { bj_character_get_rotation(w, c, &mut q); }
1118
+ return match axis { 0 => q.x, 1 => q.y, 2 => q.z, 3 => q.w, _ => 0.0 } as f64;
1119
+ }
1120
+ 0.0
1121
+ }
1122
+ pub fn character_set_position(&self, h: f64, x: f32, y: f32, z: f32) {
1123
+ if let Some((w, c)) = self.resolve_character(h) {
1124
+ unsafe { bj_character_set_position(w, c, BjVec3 { x, y, z }); }
1125
+ }
1126
+ }
1127
+ pub fn character_set_rotation(&self, h: f64, x: f32, y: f32, z: f32, ww: f32) {
1128
+ if let Some((w, c)) = self.resolve_character(h) {
1129
+ unsafe { bj_character_set_rotation(w, c, BjQuat { x, y, z, w: ww }); }
1130
+ }
1131
+ }
1132
+ pub fn character_get_linear_velocity_axis(&self, h: f64, axis: u32) -> f64 {
1133
+ if let Some((w, c)) = self.resolve_character(h) {
1134
+ let mut v = BjVec3::default();
1135
+ unsafe { bj_character_get_linear_velocity(w, c, &mut v); }
1136
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
1137
+ }
1138
+ 0.0
1139
+ }
1140
+ pub fn character_set_linear_velocity(&self, h: f64, x: f32, y: f32, z: f32) {
1141
+ if let Some((w, c)) = self.resolve_character(h) {
1142
+ unsafe { bj_character_set_linear_velocity(w, c, BjVec3 { x, y, z }); }
1143
+ }
1144
+ }
1145
+
1146
+ pub fn character_get_ground_state(&self, h: f64) -> u32 {
1147
+ if let Some((w, c)) = self.resolve_character(h) {
1148
+ return unsafe { bj_character_get_ground_state(w, c) } as u32;
1149
+ }
1150
+ BjGroundState::InAir as u32
1151
+ }
1152
+ pub fn character_get_ground_normal_axis(&self, h: f64, axis: u32) -> f64 {
1153
+ if let Some((w, c)) = self.resolve_character(h) {
1154
+ let mut v = BjVec3::default();
1155
+ unsafe { bj_character_get_ground_normal(w, c, &mut v); }
1156
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
1157
+ }
1158
+ 0.0
1159
+ }
1160
+ pub fn character_get_ground_position_axis(&self, h: f64, axis: u32) -> f64 {
1161
+ if let Some((w, c)) = self.resolve_character(h) {
1162
+ let mut v = BjVec3::default();
1163
+ unsafe { bj_character_get_ground_position(w, c, &mut v); }
1164
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
1165
+ }
1166
+ 0.0
1167
+ }
1168
+ pub fn character_get_ground_body(&self, h: f64) -> f64 {
1169
+ if let Some((w, c)) = self.resolve_character(h) {
1170
+ let bj = unsafe { bj_character_get_ground_body(w, c) };
1171
+ if bj == BJ_INVALID { return 0.0; }
1172
+ return self.bodies.iter()
1173
+ .find(|(_, (_, b))| *b == bj)
1174
+ .map(|(h, _)| h)
1175
+ .unwrap_or(0.0);
1176
+ }
1177
+ 0.0
1178
+ }
1179
+ pub fn character_set_shape(&self, h: f64, shape_h: f64) {
1180
+ if let (Some((w, c)), Some(&s)) = (self.resolve_character(h), self.shapes.get(shape_h)) {
1181
+ unsafe { bj_character_set_shape(w, c, s); }
1182
+ }
1183
+ }
1184
+
1185
+ // -------------------------------------------------------- Soft bodies
1186
+
1187
+ /// Create a soft body from scratch-buffered vertex+index streams.
1188
+ /// scratch_f32 layout: vertex_count * 4 floats (x, y, z, invMass per vertex).
1189
+ /// scratch_u32 layout: triangle_count * 3 indices.
1190
+ /// inv_mass == 0 pins the vertex.
1191
+ #[allow(clippy::too_many_arguments)]
1192
+ pub fn soft_body_create_from_scratch(
1193
+ &mut self, world_h: f64,
1194
+ vertex_count: u32, triangle_count: u32,
1195
+ px: f32, py: f32, pz: f32, rx: f32, ry: f32, rz: f32, rw: f32,
1196
+ object_layer: u32,
1197
+ edge_compliance: f32, gravity_factor: f32, linear_damping: f32, pressure: f32,
1198
+ ) -> f64 {
1199
+ let need_f = (vertex_count * 4) as usize;
1200
+ let need_u = (triangle_count * 3) as usize;
1201
+ if self.scratch_f32.len() < need_f || self.scratch_u32.len() < need_u {
1202
+ return 0.0;
1203
+ }
1204
+ if vertex_count < 3 || triangle_count == 0 { return 0.0; }
1205
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return 0.0 };
1206
+
1207
+ let body = unsafe {
1208
+ bj_soft_body_create(
1209
+ world,
1210
+ self.scratch_f32.as_ptr(), vertex_count,
1211
+ self.scratch_u32.as_ptr(), triangle_count,
1212
+ BjVec3 { x: px, y: py, z: pz },
1213
+ BjQuat { x: rx, y: ry, z: rz, w: rw },
1214
+ object_layer, edge_compliance, gravity_factor, linear_damping, pressure,
1215
+ )
1216
+ };
1217
+ if body == BJ_INVALID { 0.0 } else { self.bodies.alloc((world, body)) }
1218
+ }
1219
+
1220
+ pub fn soft_body_vertex_count(&self, body_h: f64) -> u32 {
1221
+ self.resolve_body(body_h)
1222
+ .map(|(w, b)| unsafe { bj_soft_body_vertex_count(w, b) })
1223
+ .unwrap_or(0)
1224
+ }
1225
+ pub fn soft_body_get_vertex_axis(&self, body_h: f64, idx: u32, axis: u32) -> f64 {
1226
+ if let Some((w, b)) = self.resolve_body(body_h) {
1227
+ let mut v = BjVec3::default();
1228
+ unsafe { bj_soft_body_get_vertex(w, b, idx, &mut v); }
1229
+ return match axis { 0 => v.x, 1 => v.y, 2 => v.z, _ => 0.0 } as f64;
1230
+ }
1231
+ 0.0
1232
+ }
1233
+ pub fn soft_body_set_vertex(&self, body_h: f64, idx: u32, x: f32, y: f32, z: f32) {
1234
+ if let Some((w, b)) = self.resolve_body(body_h) {
1235
+ unsafe { bj_soft_body_set_vertex(w, b, idx, BjVec3 { x, y, z }); }
1236
+ }
1237
+ }
1238
+ pub fn soft_body_set_vertex_inv_mass(&self, body_h: f64, idx: u32, inv_mass: f32) {
1239
+ if let Some((w, b)) = self.resolve_body(body_h) {
1240
+ unsafe { bj_soft_body_set_vertex_inv_mass(w, b, idx, inv_mass); }
1241
+ }
1242
+ }
1243
+
1244
+ // -------------------------------------------------------- Wheeled vehicles
1245
+
1246
+ #[allow(clippy::too_many_arguments)]
1247
+ pub fn vehicle_create(
1248
+ &mut self, world_h: f64, chassis_shape_h: f64,
1249
+ // Axes
1250
+ up_x: f32, up_y: f32, up_z: f32,
1251
+ fw_x: f32, fw_y: f32, fw_z: f32,
1252
+ // 4 wheel positions (flat array of 12 f32s: fl.xyz, fr.xyz, rl.xyz, rr.xyz)
1253
+ w0x: f32, w0y: f32, w0z: f32,
1254
+ w1x: f32, w1y: f32, w1z: f32,
1255
+ w2x: f32, w2y: f32, w2z: f32,
1256
+ w3x: f32, w3y: f32, w3z: f32,
1257
+ // Shared wheel + engine parameters
1258
+ wheel_radius: f32, wheel_width: f32,
1259
+ suspension_min: f32, suspension_max: f32,
1260
+ max_steer_angle: f32, max_brake_torque: f32, max_handbrake_torque: f32,
1261
+ engine_max_torque: f32, max_pitch_roll_angle: f32,
1262
+ object_layer: u32,
1263
+ // Pose
1264
+ px: f32, py: f32, pz: f32, rx: f32, ry: f32, rz: f32, rw: f32,
1265
+ ) -> (f64, f64) {
1266
+ // Returns (vehicle_handle, chassis_body_handle).
1267
+ let world = match self.worlds.get(world_h) { Some(&w) => w, None => return (0.0, 0.0) };
1268
+ let shape = match self.shapes.get(chassis_shape_h) { Some(&s) => s, None => return (0.0, 0.0) };
1269
+
1270
+ let desc = BjVehicleDesc {
1271
+ up: BjVec3 { x: up_x, y: up_y, z: up_z },
1272
+ forward: BjVec3 { x: fw_x, y: fw_y, z: fw_z },
1273
+ wheel_positions: [
1274
+ BjVec3 { x: w0x, y: w0y, z: w0z },
1275
+ BjVec3 { x: w1x, y: w1y, z: w1z },
1276
+ BjVec3 { x: w2x, y: w2y, z: w2z },
1277
+ BjVec3 { x: w3x, y: w3y, z: w3z },
1278
+ ],
1279
+ wheel_radius, wheel_width,
1280
+ suspension_min_length: suspension_min,
1281
+ suspension_max_length: suspension_max,
1282
+ max_steer_angle, max_brake_torque, max_handbrake_torque,
1283
+ engine_max_torque, max_pitch_roll_angle, object_layer,
1284
+ };
1285
+
1286
+ let vehicle = unsafe {
1287
+ bj_vehicle_create(world, shape, &desc,
1288
+ BjVec3 { x: px, y: py, z: pz },
1289
+ BjQuat { x: rx, y: ry, z: rz, w: rw })
1290
+ };
1291
+ if vehicle == BJ_INVALID { return (0.0, 0.0); }
1292
+
1293
+ // Register the chassis as a regular body so TS can query its transform
1294
+ // via normal body APIs.
1295
+ let chassis_bj = unsafe { bj_vehicle_get_chassis(world, vehicle) };
1296
+ let chassis_h = self.bodies.alloc((world, chassis_bj));
1297
+ let vh = self.vehicles.alloc((world, vehicle, chassis_h));
1298
+ (vh, chassis_h)
1299
+ }
1300
+
1301
+ pub fn vehicle_destroy(&mut self, handle: f64) {
1302
+ if let Some((w, v, chassis_h)) = self.vehicles.free(handle) {
1303
+ unsafe { bj_vehicle_destroy(w, v); }
1304
+ // Chassis was removed by vehicle_destroy; free its Rust handle too.
1305
+ let _ = self.bodies.free(chassis_h);
1306
+ }
1307
+ }
1308
+
1309
+ pub fn vehicle_get_chassis(&self, handle: f64) -> f64 {
1310
+ self.vehicles.get(handle).map(|&(_, _, h)| h).unwrap_or(0.0)
1311
+ }
1312
+
1313
+ fn resolve_vehicle(&self, h: f64) -> Option<(bj_world, bj_vehicle)> {
1314
+ self.vehicles.get(h).map(|&(w, v, _)| (w, v))
1315
+ }
1316
+
1317
+ pub fn vehicle_set_input(&self, handle: f64, forward: f32, right: f32, brake: f32, handbrake: f32) {
1318
+ if let Some((w, v)) = self.resolve_vehicle(handle) {
1319
+ unsafe { bj_vehicle_set_input(w, v, forward, right, brake, handbrake); }
1320
+ }
1321
+ }
1322
+
1323
+ pub fn vehicle_get_wheel_transform(&self, handle: f64, wheel_idx: u32, axis: u32) -> f32 {
1324
+ self.resolve_vehicle(handle)
1325
+ .map(|(w, v)| unsafe { bj_vehicle_get_wheel_transform(w, v, wheel_idx, axis) })
1326
+ .unwrap_or(0.0)
1327
+ }
1328
+
1329
+ pub fn vehicle_get_engine_rpm(&self, handle: f64) -> f32 {
1330
+ self.resolve_vehicle(handle)
1331
+ .map(|(w, v)| unsafe { bj_vehicle_get_engine_rpm(w, v) })
1332
+ .unwrap_or(0.0)
1333
+ }
1334
+
1335
+ pub fn vehicle_get_wheel_angular_velocity(&self, handle: f64, wheel_idx: u32) -> f32 {
1336
+ self.resolve_vehicle(handle)
1337
+ .map(|(w, v)| unsafe { bj_vehicle_get_wheel_angular_velocity(w, v, wheel_idx) })
1338
+ .unwrap_or(0.0)
1339
+ }
1340
+ }
1341
+
1342
+ // ============================================================================
1343
+ // FFI macro — generates the bloom_physics_* symbol set in the invoking crate.
1344
+ //
1345
+ // Usage (in a platform crate, e.g. native/macos/src/lib.rs):
1346
+ //
1347
+ // #[inline]
1348
+ // fn bloom_jolt_ffi_physics() -> impl std::ops::DerefMut<Target = bloom_shared::physics_jolt::JoltPhysics> {
1349
+ // engine_jolt_guard()
1350
+ // }
1351
+ // bloom_shared::define_physics_ffi!();
1352
+ //
1353
+ // The macro emits #[no_mangle] extern "C" functions, so each platform crate's
1354
+ // staticlib/cdylib independently re-exports the full Perry FFI surface.
1355
+ // ============================================================================
1356
+
1357
+ #[macro_export]
1358
+ macro_rules! define_physics_ffi {
1359
+ () => {
1360
+ // --- World ---
1361
+
1362
+ #[no_mangle] pub extern "C" fn bloom_physics_create_world(gx: f64, gy: f64, gz: f64, max_bodies: f64, num_threads: f64) -> f64 {
1363
+ bloom_jolt_ffi_physics().create_world(gx as f32, gy as f32, gz as f32, max_bodies as u32, num_threads as u32)
1364
+ }
1365
+ #[no_mangle] pub extern "C" fn bloom_physics_destroy_world(world: f64) { bloom_jolt_ffi_physics().destroy_world(world); }
1366
+ #[no_mangle] pub extern "C" fn bloom_physics_set_gravity(world: f64, gx: f64, gy: f64, gz: f64) {
1367
+ bloom_jolt_ffi_physics().set_gravity(world, gx as f32, gy as f32, gz as f32);
1368
+ }
1369
+ #[no_mangle] pub extern "C" fn bloom_physics_get_gravity(world: f64, axis: f64) -> f64 { bloom_jolt_ffi_physics().get_gravity_axis(world, axis as u32) }
1370
+ #[no_mangle] pub extern "C" fn bloom_physics_optimize_broadphase(world: f64) { bloom_jolt_ffi_physics().optimize_broadphase(world); }
1371
+ #[no_mangle] pub extern "C" fn bloom_physics_step(world: f64, dt: f64, collision_steps: f64) {
1372
+ bloom_jolt_ffi_physics().step(world, dt as f32, collision_steps as u32);
1373
+ }
1374
+ #[no_mangle] pub extern "C" fn bloom_physics_step_fixed(world: f64, dt: f64, collision_steps: f64) -> f64 {
1375
+ bloom_jolt_ffi_physics().step_fixed(world, dt as f32, collision_steps as u32) as f64
1376
+ }
1377
+ #[no_mangle] pub extern "C" fn bloom_physics_set_fixed_timestep(world: f64, hz: f64, max_steps: f64) {
1378
+ bloom_jolt_ffi_physics().set_fixed_timestep(world, hz as f32, max_steps as u32);
1379
+ }
1380
+ #[no_mangle] pub extern "C" fn bloom_physics_set_interpolation(world: f64, on: f64) {
1381
+ bloom_jolt_ffi_physics().set_interpolation(world, on != 0.0);
1382
+ }
1383
+ #[no_mangle] pub extern "C" fn bloom_physics_get_step_alpha(world: f64) -> f64 {
1384
+ bloom_jolt_ffi_physics().step_alpha(world) as f64
1385
+ }
1386
+ #[no_mangle] pub extern "C" fn bloom_physics_set_layer_collides(world: f64, a: f64, b: f64, collides: f64) {
1387
+ bloom_jolt_ffi_physics().set_layer_collides(world, a as u32, b as u32, collides != 0.0);
1388
+ }
1389
+ #[no_mangle] pub extern "C" fn bloom_physics_get_layer_collides(world: f64, a: f64, b: f64) -> f64 {
1390
+ if bloom_jolt_ffi_physics().get_layer_collides(world, a as u32, b as u32) { 1.0 } else { 0.0 }
1391
+ }
1392
+ #[no_mangle] pub extern "C" fn bloom_physics_body_count(world: f64) -> f64 { bloom_jolt_ffi_physics().body_count(world) as f64 }
1393
+ #[no_mangle] pub extern "C" fn bloom_physics_active_body_count(world: f64) -> f64 { bloom_jolt_ffi_physics().active_body_count(world) as f64 }
1394
+
1395
+ // --- Shapes ---
1396
+
1397
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_box(hx: f64, hy: f64, hz: f64, convex_radius: f64) -> f64 {
1398
+ bloom_jolt_ffi_physics().create_box_shape(hx as f32, hy as f32, hz as f32, convex_radius as f32)
1399
+ }
1400
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_sphere(r: f64) -> f64 { bloom_jolt_ffi_physics().create_sphere_shape(r as f32) }
1401
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_capsule(h: f64, r: f64) -> f64 { bloom_jolt_ffi_physics().create_capsule_shape(h as f32, r as f32) }
1402
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_cylinder(h: f64, r: f64, cr: f64) -> f64 {
1403
+ bloom_jolt_ffi_physics().create_cylinder_shape(h as f32, r as f32, cr as f32)
1404
+ }
1405
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_scaled(base: f64, sx: f64, sy: f64, sz: f64) -> f64 {
1406
+ bloom_jolt_ffi_physics().create_scaled_shape(base, sx as f32, sy as f32, sz as f32)
1407
+ }
1408
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_offset_com(base: f64, ox: f64, oy: f64, oz: f64) -> f64 {
1409
+ bloom_jolt_ffi_physics().create_offset_com_shape(base, ox as f32, oy as f32, oz as f32)
1410
+ }
1411
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_release(shape: f64) { bloom_jolt_ffi_physics().release_shape(shape); }
1412
+
1413
+ // Scratch streams for variable-size shape inputs.
1414
+ #[no_mangle] pub extern "C" fn bloom_physics_scratch_reset() { bloom_jolt_ffi_physics().scratch_reset(); }
1415
+ #[no_mangle] pub extern "C" fn bloom_physics_scratch_push_f32(v: f64) { bloom_jolt_ffi_physics().scratch_push_f32(v as f32); }
1416
+ #[no_mangle] pub extern "C" fn bloom_physics_scratch_push_u32(v: f64) { bloom_jolt_ffi_physics().scratch_push_u32(v as u32); }
1417
+
1418
+ // Complex shape factories — consume scratch streams populated by the caller.
1419
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_convex_hull(num_points: f64, convex_radius: f64) -> f64 {
1420
+ bloom_jolt_ffi_physics().shape_convex_hull_from_scratch(num_points as u32, convex_radius as f32)
1421
+ }
1422
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_mesh(vertex_count: f64, triangle_count: f64) -> f64 {
1423
+ bloom_jolt_ffi_physics().shape_mesh_from_scratch(vertex_count as u32, triangle_count as u32)
1424
+ }
1425
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_heightfield(
1426
+ sample_count: f64, ox: f64, oy: f64, oz: f64, sx: f64, sy: f64, sz: f64, block_size: f64,
1427
+ ) -> f64 {
1428
+ bloom_jolt_ffi_physics().shape_heightfield_from_scratch(
1429
+ sample_count as u32, ox as f32, oy as f32, oz as f32,
1430
+ sx as f32, sy as f32, sz as f32, block_size as u32,
1431
+ )
1432
+ }
1433
+
1434
+ // Compound shape builder — begin / add_child (repeat) / end.
1435
+ #[no_mangle] pub extern "C" fn bloom_physics_compound_begin() { bloom_jolt_ffi_physics().compound_begin(); }
1436
+ #[no_mangle] pub extern "C" fn bloom_physics_compound_add_child(
1437
+ shape: f64, px: f64, py: f64, pz: f64, rx: f64, ry: f64, rz: f64, rw: f64,
1438
+ ) {
1439
+ bloom_jolt_ffi_physics().compound_add_child(
1440
+ shape,
1441
+ px as f32, py as f32, pz as f32,
1442
+ rx as f32, ry as f32, rz as f32, rw as f32,
1443
+ );
1444
+ }
1445
+ #[no_mangle] pub extern "C" fn bloom_physics_compound_end() -> f64 {
1446
+ bloom_jolt_ffi_physics().compound_end()
1447
+ }
1448
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_bounds(shape: f64, axis: f64) -> f64 { bloom_jolt_ffi_physics().shape_bounds_axis(shape, axis as u32) }
1449
+ #[no_mangle] pub extern "C" fn bloom_physics_shape_volume(shape: f64) -> f64 { bloom_jolt_ffi_physics().shape_volume(shape) as f64 }
1450
+
1451
+ // --- Bodies ---
1452
+
1453
+ #[no_mangle] pub extern "C" fn bloom_physics_body_create(
1454
+ world: f64, shape: f64, motion_type: f64,
1455
+ px: f64, py: f64, pz: f64,
1456
+ rx: f64, ry: f64, rz: f64, rw: f64,
1457
+ layer: f64,
1458
+ ) -> f64 {
1459
+ bloom_jolt_ffi_physics().create_body(
1460
+ world, shape, motion_type as u32,
1461
+ px as f32, py as f32, pz as f32,
1462
+ rx as f32, ry as f32, rz as f32, rw as f32,
1463
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1464
+ layer as u32,
1465
+ false, true, false, true,
1466
+ 0.2, 0.0, 0.05, 0.05, 1.0,
1467
+ 0.0, 0.0, 0.0, 0.0,
1468
+ 0,
1469
+ )
1470
+ }
1471
+ #[no_mangle] pub extern "C" fn bloom_physics_body_destroy(body: f64) { bloom_jolt_ffi_physics().destroy_body(body); }
1472
+ #[no_mangle] pub extern "C" fn bloom_physics_body_activate(body: f64) { bloom_jolt_ffi_physics().body_activate(body); }
1473
+ #[no_mangle] pub extern "C" fn bloom_physics_body_deactivate(body: f64) { bloom_jolt_ffi_physics().body_deactivate(body); }
1474
+ #[no_mangle] pub extern "C" fn bloom_physics_body_is_active(body: f64) -> f64 { if bloom_jolt_ffi_physics().body_is_active(body) { 1.0 } else { 0.0 } }
1475
+ #[no_mangle] pub extern "C" fn bloom_physics_body_is_valid(body: f64) -> f64 { if bloom_jolt_ffi_physics().body_is_valid(body) { 1.0 } else { 0.0 } }
1476
+
1477
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_position(body: f64, axis: f64) -> f64 { bloom_jolt_ffi_physics().body_get_position_axis(body, axis as u32) }
1478
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_rotation(body: f64, axis: f64) -> f64 { bloom_jolt_ffi_physics().body_get_rotation_axis(body, axis as u32) }
1479
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_position(body: f64, x: f64, y: f64, z: f64, activate: f64) {
1480
+ bloom_jolt_ffi_physics().body_set_position(body, x as f32, y as f32, z as f32, activate != 0.0);
1481
+ }
1482
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_rotation(body: f64, x: f64, y: f64, z: f64, w: f64, activate: f64) {
1483
+ bloom_jolt_ffi_physics().body_set_rotation(body, x as f32, y as f32, z as f32, w as f32, activate != 0.0);
1484
+ }
1485
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_transform(
1486
+ body: f64, px: f64, py: f64, pz: f64, rx: f64, ry: f64, rz: f64, rw: f64, activate: f64,
1487
+ ) {
1488
+ bloom_jolt_ffi_physics().body_set_transform(body, px as f32, py as f32, pz as f32, rx as f32, ry as f32, rz as f32, rw as f32, activate != 0.0);
1489
+ }
1490
+ #[no_mangle] pub extern "C" fn bloom_physics_body_move_kinematic(
1491
+ body: f64, px: f64, py: f64, pz: f64, rx: f64, ry: f64, rz: f64, rw: f64, dt: f64,
1492
+ ) {
1493
+ bloom_jolt_ffi_physics().body_move_kinematic(body, px as f32, py as f32, pz as f32, rx as f32, ry as f32, rz as f32, rw as f32, dt as f32);
1494
+ }
1495
+
1496
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_linear_velocity(body: f64, axis: f64) -> f64 { bloom_jolt_ffi_physics().body_get_linear_velocity_axis(body, axis as u32) }
1497
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_angular_velocity(body: f64, axis: f64) -> f64 { bloom_jolt_ffi_physics().body_get_angular_velocity_axis(body, axis as u32) }
1498
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_point_velocity(body: f64, px: f64, py: f64, pz: f64, axis: f64) -> f64 {
1499
+ bloom_jolt_ffi_physics().body_get_point_velocity_axis(body, px as f32, py as f32, pz as f32, axis as u32)
1500
+ }
1501
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_linear_velocity(body: f64, x: f64, y: f64, z: f64) {
1502
+ bloom_jolt_ffi_physics().body_set_linear_velocity(body, x as f32, y as f32, z as f32);
1503
+ }
1504
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_angular_velocity(body: f64, x: f64, y: f64, z: f64) {
1505
+ bloom_jolt_ffi_physics().body_set_angular_velocity(body, x as f32, y as f32, z as f32);
1506
+ }
1507
+
1508
+ #[no_mangle] pub extern "C" fn bloom_physics_body_add_force(body: f64, x: f64, y: f64, z: f64) { bloom_jolt_ffi_physics().body_add_force(body, x as f32, y as f32, z as f32); }
1509
+ #[no_mangle] pub extern "C" fn bloom_physics_body_add_impulse(body: f64, x: f64, y: f64, z: f64) { bloom_jolt_ffi_physics().body_add_impulse(body, x as f32, y as f32, z as f32); }
1510
+ #[no_mangle] pub extern "C" fn bloom_physics_body_add_torque(body: f64, x: f64, y: f64, z: f64) { bloom_jolt_ffi_physics().body_add_torque(body, x as f32, y as f32, z as f32); }
1511
+ #[no_mangle] pub extern "C" fn bloom_physics_body_add_angular_impulse(body: f64, x: f64, y: f64, z: f64) { bloom_jolt_ffi_physics().body_add_angular_impulse(body, x as f32, y as f32, z as f32); }
1512
+ #[no_mangle] pub extern "C" fn bloom_physics_body_add_force_at(body: f64, fx: f64, fy: f64, fz: f64, px: f64, py: f64, pz: f64) {
1513
+ bloom_jolt_ffi_physics().body_add_force_at(body, fx as f32, fy as f32, fz as f32, px as f32, py as f32, pz as f32);
1514
+ }
1515
+ #[no_mangle] pub extern "C" fn bloom_physics_body_add_impulse_at(body: f64, ix: f64, iy: f64, iz: f64, px: f64, py: f64, pz: f64) {
1516
+ bloom_jolt_ffi_physics().body_add_impulse_at(body, ix as f32, iy as f32, iz as f32, px as f32, py as f32, pz as f32);
1517
+ }
1518
+
1519
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_friction(body: f64, v: f64) { bloom_jolt_ffi_physics().body_set_friction(body, v as f32); }
1520
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_restitution(body: f64, v: f64) { bloom_jolt_ffi_physics().body_set_restitution(body, v as f32); }
1521
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_linear_damping(body: f64, v: f64) { bloom_jolt_ffi_physics().body_set_linear_damping(body, v as f32); }
1522
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_angular_damping(body: f64, v: f64) { bloom_jolt_ffi_physics().body_set_angular_damping(body, v as f32); }
1523
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_gravity_factor(body: f64, v: f64) { bloom_jolt_ffi_physics().body_set_gravity_factor(body, v as f32); }
1524
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_ccd(body: f64, enabled: f64) { bloom_jolt_ffi_physics().body_set_ccd(body, enabled != 0.0); }
1525
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_motion_type(body: f64, t: f64, activate: f64) { bloom_jolt_ffi_physics().body_set_motion_type(body, t as u32, activate != 0.0); }
1526
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_object_layer(body: f64, layer: f64) { bloom_jolt_ffi_physics().body_set_object_layer(body, layer as u32); }
1527
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_is_sensor(body: f64, enabled: f64) { bloom_jolt_ffi_physics().body_set_is_sensor(body, enabled != 0.0); }
1528
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_allow_sleeping(body: f64, enabled: f64) { bloom_jolt_ffi_physics().body_set_allow_sleeping(body, enabled != 0.0); }
1529
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_shape(body: f64, shape: f64, update_mass: f64, activate: f64) {
1530
+ bloom_jolt_ffi_physics().body_set_shape(body, shape, update_mass != 0.0, activate != 0.0);
1531
+ }
1532
+ #[no_mangle] pub extern "C" fn bloom_physics_body_lock_rotation_axes(body: f64, x: f64, y: f64, z: f64) {
1533
+ bloom_jolt_ffi_physics().body_lock_rotation_axes(body, x != 0.0, y != 0.0, z != 0.0);
1534
+ }
1535
+ #[no_mangle] pub extern "C" fn bloom_physics_body_lock_translation_axes(body: f64, x: f64, y: f64, z: f64) {
1536
+ bloom_jolt_ffi_physics().body_lock_translation_axes(body, x != 0.0, y != 0.0, z != 0.0);
1537
+ }
1538
+
1539
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_mass(body: f64) -> f64 { bloom_jolt_ffi_physics().body_get_mass(body) as f64 }
1540
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_friction(body: f64) -> f64 { bloom_jolt_ffi_physics().body_get_friction(body) as f64 }
1541
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_restitution(body: f64) -> f64 { bloom_jolt_ffi_physics().body_get_restitution(body) as f64 }
1542
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_object_layer(body: f64) -> f64 { bloom_jolt_ffi_physics().body_get_object_layer(body) as f64 }
1543
+ #[no_mangle] pub extern "C" fn bloom_physics_body_set_user_data(body: f64, lo: f64, hi: f64) {
1544
+ let user = (lo as u64) | ((hi as u64) << 32);
1545
+ bloom_jolt_ffi_physics().body_set_user_data(body, user);
1546
+ }
1547
+ #[no_mangle] pub extern "C" fn bloom_physics_body_get_user_data(body: f64, part: f64) -> f64 {
1548
+ let u = bloom_jolt_ffi_physics().body_get_user_data(body);
1549
+ (if part as u32 == 1 { (u >> 32) as u32 } else { u as u32 }) as f64
1550
+ }
1551
+
1552
+ // --- Queries ---
1553
+
1554
+ #[no_mangle] pub extern "C" fn bloom_physics_raycast(
1555
+ world: f64, ox: f64, oy: f64, oz: f64,
1556
+ dx: f64, dy: f64, dz: f64, max_dist: f64, layer_mask: f64,
1557
+ ) -> f64 {
1558
+ if bloom_jolt_ffi_physics().raycast_closest(world, ox as f32, oy as f32, oz as f32, dx as f32, dy as f32, dz as f32, max_dist as f32, layer_mask as u32) { 1.0 } else { 0.0 }
1559
+ }
1560
+ #[no_mangle] pub extern "C" fn bloom_physics_raycast_all(
1561
+ world: f64, ox: f64, oy: f64, oz: f64,
1562
+ dx: f64, dy: f64, dz: f64, max_dist: f64, layer_mask: f64, max_hits: f64,
1563
+ ) -> f64 {
1564
+ bloom_jolt_ffi_physics().raycast_all(world, ox as f32, oy as f32, oz as f32, dx as f32, dy as f32, dz as f32, max_dist as f32, layer_mask as u32, max_hits as u32) as f64
1565
+ }
1566
+ #[no_mangle] pub extern "C" fn bloom_physics_ray_hit_count() -> f64 { bloom_jolt_ffi_physics().ray_hit_count() as f64 }
1567
+ #[no_mangle] pub extern "C" fn bloom_physics_ray_hit_body(i: f64) -> f64 { bloom_jolt_ffi_physics().ray_hit_body(i as usize) }
1568
+ #[no_mangle] pub extern "C" fn bloom_physics_ray_hit_axis(i: f64, field: f64) -> f64 { bloom_jolt_ffi_physics().ray_hit_axis(i as usize, field as u32) }
1569
+ #[no_mangle] pub extern "C" fn bloom_physics_ray_hit_fraction(i: f64) -> f64 { bloom_jolt_ffi_physics().ray_hit_fraction(i as usize) as f64 }
1570
+ #[no_mangle] pub extern "C" fn bloom_physics_ray_hit_sub_shape(i: f64) -> f64 { bloom_jolt_ffi_physics().ray_hit_sub_shape(i as usize) as f64 }
1571
+
1572
+ #[no_mangle] pub extern "C" fn bloom_physics_overlap_sphere(world: f64, cx: f64, cy: f64, cz: f64, r: f64, layer_mask: f64, max_results: f64) -> f64 {
1573
+ bloom_jolt_ffi_physics().overlap_sphere(world, cx as f32, cy as f32, cz as f32, r as f32, layer_mask as u32, max_results as u32) as f64
1574
+ }
1575
+ #[no_mangle] pub extern "C" fn bloom_physics_overlap_point(world: f64, px: f64, py: f64, pz: f64, layer_mask: f64, max_results: f64) -> f64 {
1576
+ bloom_jolt_ffi_physics().overlap_point(world, px as f32, py as f32, pz as f32, layer_mask as u32, max_results as u32) as f64
1577
+ }
1578
+ #[no_mangle] pub extern "C" fn bloom_physics_overlap_box(
1579
+ world: f64, px: f64, py: f64, pz: f64, rx: f64, ry: f64, rz: f64, rw: f64,
1580
+ hx: f64, hy: f64, hz: f64, layer_mask: f64, max_results: f64,
1581
+ ) -> f64 {
1582
+ bloom_jolt_ffi_physics().overlap_box(world, px as f32, py as f32, pz as f32, rx as f32, ry as f32, rz as f32, rw as f32, hx as f32, hy as f32, hz as f32, layer_mask as u32, max_results as u32) as f64
1583
+ }
1584
+ #[no_mangle] pub extern "C" fn bloom_physics_overlap_body(i: f64) -> f64 { bloom_jolt_ffi_physics().overlap_body(i as usize) }
1585
+
1586
+ // --- Constraints ---
1587
+
1588
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_fixed(
1589
+ body_a: f64, body_b: f64, ax: f64, ay: f64, az: f64, bx: f64, by: f64, bz: f64, world_space: f64,
1590
+ ) -> f64 {
1591
+ bloom_jolt_ffi_physics().constraint_fixed(body_a, body_b, ax as f32, ay as f32, az as f32, bx as f32, by as f32, bz as f32, world_space != 0.0)
1592
+ }
1593
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_point(
1594
+ body_a: f64, body_b: f64, ax: f64, ay: f64, az: f64, bx: f64, by: f64, bz: f64, world_space: f64,
1595
+ ) -> f64 {
1596
+ bloom_jolt_ffi_physics().constraint_point(body_a, body_b, ax as f32, ay as f32, az as f32, bx as f32, by as f32, bz as f32, world_space != 0.0)
1597
+ }
1598
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_hinge(
1599
+ body_a: f64, body_b: f64, ax: f64, ay: f64, az: f64, bx: f64, by: f64, bz: f64,
1600
+ axx: f64, axy: f64, axz: f64, lmin: f64, lmax: f64, world_space: f64,
1601
+ ) -> f64 {
1602
+ bloom_jolt_ffi_physics().constraint_hinge(
1603
+ body_a, body_b,
1604
+ ax as f32, ay as f32, az as f32, bx as f32, by as f32, bz as f32,
1605
+ axx as f32, axy as f32, axz as f32,
1606
+ lmin as f32, lmax as f32, world_space != 0.0,
1607
+ )
1608
+ }
1609
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_slider(
1610
+ body_a: f64, body_b: f64, ax: f64, ay: f64, az: f64, bx: f64, by: f64, bz: f64,
1611
+ axx: f64, axy: f64, axz: f64, lmin: f64, lmax: f64, world_space: f64,
1612
+ ) -> f64 {
1613
+ bloom_jolt_ffi_physics().constraint_slider(
1614
+ body_a, body_b,
1615
+ ax as f32, ay as f32, az as f32, bx as f32, by as f32, bz as f32,
1616
+ axx as f32, axy as f32, axz as f32,
1617
+ lmin as f32, lmax as f32, world_space != 0.0,
1618
+ )
1619
+ }
1620
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_distance(
1621
+ body_a: f64, body_b: f64, ax: f64, ay: f64, az: f64, bx: f64, by: f64, bz: f64,
1622
+ min_d: f64, max_d: f64, world_space: f64,
1623
+ ) -> f64 {
1624
+ bloom_jolt_ffi_physics().constraint_distance(body_a, body_b, ax as f32, ay as f32, az as f32, bx as f32, by as f32, bz as f32, min_d as f32, max_d as f32, world_space != 0.0)
1625
+ }
1626
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_destroy(c: f64) { bloom_jolt_ffi_physics().constraint_destroy(c); }
1627
+ #[no_mangle] pub extern "C" fn bloom_physics_constraint_set_enabled(c: f64, enabled: f64) { bloom_jolt_ffi_physics().constraint_set_enabled(c, enabled != 0.0); }
1628
+
1629
+ // --- Contact events ---
1630
+
1631
+ #[no_mangle] pub extern "C" fn bloom_physics_contact_count() -> f64 { bloom_jolt_ffi_physics().contact_count() as f64 }
1632
+ #[no_mangle] pub extern "C" fn bloom_physics_contact_field(i: f64, field: f64) -> f64 { bloom_jolt_ffi_physics().contact_field(i as usize, field as u32) }
1633
+ #[no_mangle] pub extern "C" fn bloom_physics_clear_contacts(world: f64) { let _ = world; bloom_jolt_ffi_physics().clear_contacts(); }
1634
+
1635
+ // --- Character controller (Tier 2) ---
1636
+
1637
+ #[no_mangle] pub extern "C" fn bloom_physics_character_create(
1638
+ world: f64, shape: f64,
1639
+ up_x: f64, up_y: f64, up_z: f64,
1640
+ max_slope_angle: f64, character_padding: f64,
1641
+ penetration_recovery_speed: f64, predictive_contact_distance: f64,
1642
+ max_strength: f64, mass: f64, object_layer: f64,
1643
+ px: f64, py: f64, pz: f64,
1644
+ rx: f64, ry: f64, rz: f64, rw: f64,
1645
+ ) -> f64 {
1646
+ bloom_jolt_ffi_physics().character_create(
1647
+ world, shape,
1648
+ up_x as f32, up_y as f32, up_z as f32,
1649
+ max_slope_angle as f32, character_padding as f32,
1650
+ penetration_recovery_speed as f32, predictive_contact_distance as f32,
1651
+ max_strength as f32, mass as f32, object_layer as u32,
1652
+ px as f32, py as f32, pz as f32,
1653
+ rx as f32, ry as f32, rz as f32, rw as f32,
1654
+ )
1655
+ }
1656
+ #[no_mangle] pub extern "C" fn bloom_physics_character_destroy(c: f64) { bloom_jolt_ffi_physics().character_destroy(c); }
1657
+ #[no_mangle] pub extern "C" fn bloom_physics_character_update(c: f64, dt: f64, gx: f64, gy: f64, gz: f64) {
1658
+ bloom_jolt_ffi_physics().character_update(c, dt as f32, gx as f32, gy as f32, gz as f32);
1659
+ }
1660
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_position(c: f64, axis: f64) -> f64 {
1661
+ bloom_jolt_ffi_physics().character_get_position_axis(c, axis as u32)
1662
+ }
1663
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_rotation(c: f64, axis: f64) -> f64 {
1664
+ bloom_jolt_ffi_physics().character_get_rotation_axis(c, axis as u32)
1665
+ }
1666
+ #[no_mangle] pub extern "C" fn bloom_physics_character_set_position(c: f64, x: f64, y: f64, z: f64) {
1667
+ bloom_jolt_ffi_physics().character_set_position(c, x as f32, y as f32, z as f32);
1668
+ }
1669
+ #[no_mangle] pub extern "C" fn bloom_physics_character_set_rotation(c: f64, x: f64, y: f64, z: f64, w: f64) {
1670
+ bloom_jolt_ffi_physics().character_set_rotation(c, x as f32, y as f32, z as f32, w as f32);
1671
+ }
1672
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_linear_velocity(c: f64, axis: f64) -> f64 {
1673
+ bloom_jolt_ffi_physics().character_get_linear_velocity_axis(c, axis as u32)
1674
+ }
1675
+ #[no_mangle] pub extern "C" fn bloom_physics_character_set_linear_velocity(c: f64, x: f64, y: f64, z: f64) {
1676
+ bloom_jolt_ffi_physics().character_set_linear_velocity(c, x as f32, y as f32, z as f32);
1677
+ }
1678
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_ground_state(c: f64) -> f64 {
1679
+ bloom_jolt_ffi_physics().character_get_ground_state(c) as f64
1680
+ }
1681
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_ground_normal(c: f64, axis: f64) -> f64 {
1682
+ bloom_jolt_ffi_physics().character_get_ground_normal_axis(c, axis as u32)
1683
+ }
1684
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_ground_position(c: f64, axis: f64) -> f64 {
1685
+ bloom_jolt_ffi_physics().character_get_ground_position_axis(c, axis as u32)
1686
+ }
1687
+ #[no_mangle] pub extern "C" fn bloom_physics_character_get_ground_body(c: f64) -> f64 {
1688
+ bloom_jolt_ffi_physics().character_get_ground_body(c)
1689
+ }
1690
+ #[no_mangle] pub extern "C" fn bloom_physics_character_set_shape(c: f64, shape: f64) {
1691
+ bloom_jolt_ffi_physics().character_set_shape(c, shape);
1692
+ }
1693
+
1694
+ // --- Soft bodies (Tier 2) ---
1695
+
1696
+ #[no_mangle] pub extern "C" fn bloom_physics_soft_body_create(
1697
+ world: f64, vertex_count: f64, triangle_count: f64,
1698
+ px: f64, py: f64, pz: f64, rx: f64, ry: f64, rz: f64, rw: f64,
1699
+ object_layer: f64,
1700
+ edge_compliance: f64, gravity_factor: f64, linear_damping: f64, pressure: f64,
1701
+ ) -> f64 {
1702
+ bloom_jolt_ffi_physics().soft_body_create_from_scratch(
1703
+ world, vertex_count as u32, triangle_count as u32,
1704
+ px as f32, py as f32, pz as f32,
1705
+ rx as f32, ry as f32, rz as f32, rw as f32,
1706
+ object_layer as u32,
1707
+ edge_compliance as f32, gravity_factor as f32,
1708
+ linear_damping as f32, pressure as f32,
1709
+ )
1710
+ }
1711
+ #[no_mangle] pub extern "C" fn bloom_physics_soft_body_vertex_count(body: f64) -> f64 {
1712
+ bloom_jolt_ffi_physics().soft_body_vertex_count(body) as f64
1713
+ }
1714
+ #[no_mangle] pub extern "C" fn bloom_physics_soft_body_get_vertex(body: f64, idx: f64, axis: f64) -> f64 {
1715
+ bloom_jolt_ffi_physics().soft_body_get_vertex_axis(body, idx as u32, axis as u32)
1716
+ }
1717
+ #[no_mangle] pub extern "C" fn bloom_physics_soft_body_set_vertex(body: f64, idx: f64, x: f64, y: f64, z: f64) {
1718
+ bloom_jolt_ffi_physics().soft_body_set_vertex(body, idx as u32, x as f32, y as f32, z as f32);
1719
+ }
1720
+ #[no_mangle] pub extern "C" fn bloom_physics_soft_body_set_vertex_inv_mass(body: f64, idx: f64, inv_mass: f64) {
1721
+ bloom_jolt_ffi_physics().soft_body_set_vertex_inv_mass(body, idx as u32, inv_mass as f32);
1722
+ }
1723
+
1724
+ // --- Wheeled vehicles (Tier 2) ---
1725
+ //
1726
+ // vehicle_create returns a packed f64 carrying BOTH the vehicle handle
1727
+ // and the chassis body handle: (vehicle << 20) | chassis_body. This keeps
1728
+ // the FFI scalar-only while letting TS unpack both. Handles stay <2^20
1729
+ // in practice for any realistic scene.
1730
+
1731
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_create(
1732
+ world: f64, chassis_shape: f64,
1733
+ up_x: f64, up_y: f64, up_z: f64,
1734
+ fw_x: f64, fw_y: f64, fw_z: f64,
1735
+ w0x: f64, w0y: f64, w0z: f64,
1736
+ w1x: f64, w1y: f64, w1z: f64,
1737
+ w2x: f64, w2y: f64, w2z: f64,
1738
+ w3x: f64, w3y: f64, w3z: f64,
1739
+ wheel_radius: f64, wheel_width: f64,
1740
+ suspension_min: f64, suspension_max: f64,
1741
+ max_steer_angle: f64, max_brake_torque: f64, max_handbrake_torque: f64,
1742
+ engine_max_torque: f64, max_pitch_roll_angle: f64,
1743
+ object_layer: f64,
1744
+ px: f64, py: f64, pz: f64, rx: f64, ry: f64, rz: f64, rw: f64,
1745
+ ) -> f64 {
1746
+ let (vh, _) = bloom_jolt_ffi_physics().vehicle_create(
1747
+ world, chassis_shape,
1748
+ up_x as f32, up_y as f32, up_z as f32,
1749
+ fw_x as f32, fw_y as f32, fw_z as f32,
1750
+ w0x as f32, w0y as f32, w0z as f32,
1751
+ w1x as f32, w1y as f32, w1z as f32,
1752
+ w2x as f32, w2y as f32, w2z as f32,
1753
+ w3x as f32, w3y as f32, w3z as f32,
1754
+ wheel_radius as f32, wheel_width as f32,
1755
+ suspension_min as f32, suspension_max as f32,
1756
+ max_steer_angle as f32, max_brake_torque as f32, max_handbrake_torque as f32,
1757
+ engine_max_torque as f32, max_pitch_roll_angle as f32,
1758
+ object_layer as u32,
1759
+ px as f32, py as f32, pz as f32,
1760
+ rx as f32, ry as f32, rz as f32, rw as f32,
1761
+ );
1762
+ vh
1763
+ }
1764
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_destroy(v: f64) { bloom_jolt_ffi_physics().vehicle_destroy(v); }
1765
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_get_chassis(v: f64) -> f64 { bloom_jolt_ffi_physics().vehicle_get_chassis(v) }
1766
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_set_input(v: f64, forward: f64, right: f64, brake: f64, handbrake: f64) {
1767
+ bloom_jolt_ffi_physics().vehicle_set_input(v, forward as f32, right as f32, brake as f32, handbrake as f32);
1768
+ }
1769
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_get_wheel_transform(v: f64, wheel_index: f64, axis: f64) -> f64 {
1770
+ bloom_jolt_ffi_physics().vehicle_get_wheel_transform(v, wheel_index as u32, axis as u32) as f64
1771
+ }
1772
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_get_engine_rpm(v: f64) -> f64 {
1773
+ bloom_jolt_ffi_physics().vehicle_get_engine_rpm(v) as f64
1774
+ }
1775
+ #[no_mangle] pub extern "C" fn bloom_physics_vehicle_get_wheel_angular_velocity(v: f64, wheel_index: f64) -> f64 {
1776
+ bloom_jolt_ffi_physics().vehicle_get_wheel_angular_velocity(v, wheel_index as u32) as f64
1777
+ }
1778
+ };
1779
+ }
1780
+
1781
+ // ---------------------------------------------------------------------------
1782
+ // Fixed-timestep stepping tests. Run with the same `jolt` feature gate the
1783
+ // jolt_sys smoke tests use (CI's shared-tests job exercises them on all
1784
+ // three host OSes).
1785
+ // ---------------------------------------------------------------------------
1786
+
1787
+ #[cfg(test)]
1788
+ mod step_tests {
1789
+ use super::*;
1790
+
1791
+ /// Dynamic sphere falling under default gravity in a fresh world.
1792
+ fn world_with_falling_sphere(p: &mut JoltPhysics) -> (f64, f64) {
1793
+ let world = p.create_world(0.0, -9.81, 0.0, 1024, 1);
1794
+ assert_ne!(world, 0.0);
1795
+ let shape = p.create_sphere_shape(0.5);
1796
+ assert_ne!(shape, 0.0);
1797
+ #[rustfmt::skip]
1798
+ let body = p.create_body(
1799
+ world, shape, 2 /* dynamic */,
1800
+ 0.0, 100.0, 0.0, // position
1801
+ 0.0, 0.0, 0.0, 1.0, // rotation
1802
+ 0.0, 0.0, 0.0, // linear velocity
1803
+ 0.0, 0.0, 0.0, // angular velocity
1804
+ 1, // moving layer
1805
+ false, false, false, true, // sensor/sleep/ccd/awake
1806
+ 0.5, 0.0, // friction, restitution
1807
+ 0.0, 0.0, 1.0, // damping, gravity factor
1808
+ 0.0, // mass override (0 = from shape)
1809
+ 0.0, 0.0, 0.0, // inertia override
1810
+ 0, // user data
1811
+ );
1812
+ assert_ne!(body, 0.0);
1813
+ (world, body)
1814
+ }
1815
+
1816
+ /// Irregular frame times through the accumulator land on the same
1817
+ /// trajectory as the equivalent number of manual fixed steps.
1818
+ #[test]
1819
+ fn fixed_accumulator_matches_manual_stepping() {
1820
+ let mut p = JoltPhysics::new();
1821
+ let (wa, ba) = world_with_falling_sphere(&mut p);
1822
+ let (wb, bb) = world_with_falling_sphere(&mut p);
1823
+
1824
+ // A: irregular frames summing to 12.5 fixed steps — the half-step
1825
+ // margin keeps the floor() robustly at 12 across f32 accumulation
1826
+ // rounding (an exact-boundary sum floors to 11 or 12 depending on
1827
+ // rounding direction, which is correct but untestable).
1828
+ let frames = [0.04, 0.05, 0.03, 0.06, 12.5 / 60.0 - 0.18];
1829
+ let mut alpha = 1.0;
1830
+ for dt in frames {
1831
+ alpha = p.step_fixed(wa, dt as f32, 1);
1832
+ }
1833
+ assert!((0.3..=0.7).contains(&alpha), "expected ~half-step remainder, alpha = {alpha}");
1834
+ // B: 12 manual steps at exactly 1/60.
1835
+ for _ in 0..12 {
1836
+ p.step(wb, 1.0 / 60.0, 1);
1837
+ }
1838
+
1839
+ let ya = p.body_get_position_axis(ba, 1);
1840
+ let yb = p.body_get_position_axis(bb, 1);
1841
+ // 0.2 s of free fall from rest ≈ ½·9.81·0.04 ≈ 0.196 m
1842
+ assert!(ya < 99.9, "sphere did not fall: y = {ya}");
1843
+ assert!(
1844
+ (ya - yb).abs() < 1e-4,
1845
+ "accumulator diverged from manual stepping: {ya} vs {yb}"
1846
+ );
1847
+
1848
+ p.destroy_world(wa);
1849
+ p.destroy_world(wb);
1850
+ }
1851
+
1852
+ /// A huge hitch frame consumes at most max_steps_per_frame steps and
1853
+ /// drops the backlog instead of simulating seconds of catch-up.
1854
+ #[test]
1855
+ fn hitch_frame_is_clamped_and_capped() {
1856
+ let mut p = JoltPhysics::new();
1857
+ let (w, b) = world_with_falling_sphere(&mut p);
1858
+
1859
+ p.step_fixed(w, 10.0, 1); // debugger-pause-sized frame
1860
+ let y = p.body_get_position_axis(b, 1);
1861
+ // 4 steps at 1/60 ≈ 67ms of fall from rest ≈ 2.2cm. If the clamp or
1862
+ // cap failed we'd have simulated 0.25–10s (0.3m–490m of fall).
1863
+ let fallen = 100.0 - y;
1864
+ assert!(fallen > 0.0, "no simulation happened");
1865
+ assert!(
1866
+ fallen < 0.1,
1867
+ "hitch frame simulated too much catch-up: fell {fallen}m"
1868
+ );
1869
+
1870
+ // The dropped backlog must not leak into later frames: a zero-dt
1871
+ // follow-up frame steps at most once.
1872
+ let y1 = p.body_get_position_axis(b, 1);
1873
+ p.step_fixed(w, 0.0, 1);
1874
+ let y2 = p.body_get_position_axis(b, 1);
1875
+ assert!(
1876
+ (y1 - y2).abs() < 0.01,
1877
+ "backlog leaked into the next frame: {y1} -> {y2}"
1878
+ );
1879
+
1880
+ p.destroy_world(w);
1881
+ }
1882
+
1883
+ /// With interpolation on, the position getter blends between the last
1884
+ /// two simulated states by the carried remainder.
1885
+ #[test]
1886
+ fn interpolation_blends_between_steps() {
1887
+ let mut p = JoltPhysics::new();
1888
+ let (w, b) = world_with_falling_sphere(&mut p);
1889
+ p.set_interpolation(w, true);
1890
+
1891
+ // One and a half fixed steps: alpha should be 0.5 and the reported
1892
+ // position should sit between the step-1 and step-2 states.
1893
+ let alpha = p.step_fixed(w, 1.5 / 60.0, 1);
1894
+ assert!((alpha - 0.5).abs() < 1e-3, "alpha = {alpha}");
1895
+
1896
+ let y_blended = p.body_get_position_axis(b, 1);
1897
+ p.set_interpolation(w, false);
1898
+ let y_raw = p.body_get_position_axis(b, 1);
1899
+ // The sphere is falling, so the blended (half-step-old) position
1900
+ // must be strictly above the raw current position.
1901
+ assert!(
1902
+ y_blended > y_raw,
1903
+ "interpolated y ({y_blended}) not above raw y ({y_raw})"
1904
+ );
1905
+
1906
+ p.destroy_world(w);
1907
+ }
1908
+ }