@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,1519 @@
1
+ // ============================================================================
2
+ // jolt_bridge.js — web-side implementation of the bloom_physics_* FFI surface.
3
+ //
4
+ // The native Rust/Jolt backend is replaced on the web target by JoltPhysics.js
5
+ // (https://github.com/jrouwe/JoltPhysics.js). This module exports one function
6
+ // per `bloom_physics_*` entry so `bloom_web.wasm` can forward calls verbatim.
7
+ //
8
+ // Lifecycle:
9
+ // 1. Before Perry WASM boots, the host page awaits `initJolt()` below, which
10
+ // loads the Jolt WASM module and wires allocators + types.
11
+ // 2. Once ready, all bridge calls operate on live Jolt objects.
12
+ // 3. If a bridge call arrives before init (shouldn't happen if the host page
13
+ // sequences correctly), the bridge logs once and returns 0 / a no-op.
14
+ //
15
+ // Handle model mirrors the native Rust side (HandleRegistry<f64>):
16
+ // - 1-based numeric handles ("slots") issued sequentially per resource type.
17
+ // - 0 == INVALID.
18
+ // - Separate maps for worlds / shapes / bodies / constraints.
19
+ // ============================================================================
20
+
21
+ let JoltModule = null;
22
+
23
+ const state = {
24
+ worlds: new Map(), // handle → { system, bodyInterface, temp, jobs, contacts, rayHits, overlaps }
25
+ shapes: new Map(), // handle → Jolt Shape
26
+ bodies: new Map(), // handle → { world: number, bodyId: number }
27
+ constraints: new Map(), // handle → { world: number, constraint: Jolt.Constraint }
28
+ characters: new Map(), // handle → { world: number, character: Jolt.CharacterVirtual, layer: number }
29
+ nextWorld: 1, nextShape: 1, nextBody: 1, nextConstraint: 1, nextCharacter: 1,
30
+ // Most-recent query results (drained on read)
31
+ rayHits: [], // array of { body, point:[x,y,z], normal:[x,y,z], fraction, subShapeId }
32
+ overlapBodies: [], // array of body handles
33
+ contacts: [], // array of contact objects
34
+ // Scratch streams for variable-size shape inputs.
35
+ scratchF32: [],
36
+ scratchU32: [],
37
+ // Compound-shape builder state (cleared by compoundBegin).
38
+ compoundChildren: [],
39
+ };
40
+
41
+ function warnUninit(fnName) {
42
+ if (!JoltModule) {
43
+ if (!warnUninit._warned) {
44
+ console.warn('[jolt_bridge] Jolt not initialised; bloom_physics_* calls are no-ops until initJolt() resolves');
45
+ warnUninit._warned = true;
46
+ }
47
+ return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Initialisation — must be awaited before Perry calls any bloom_physics_* fn.
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * @param {Object} joltFactory - the default export of jolt-physics (a function
58
+ * returning a Promise<Jolt>). Accepting it via parameter lets the host page
59
+ * pick how to load it (CDN script tag, bundler, etc.).
60
+ */
61
+ export async function initJolt(joltFactory) {
62
+ if (JoltModule) return JoltModule;
63
+ if (typeof joltFactory !== 'function') {
64
+ throw new Error('initJolt requires a joltFactory function (Jolt.default or window.Jolt)');
65
+ }
66
+ JoltModule = await joltFactory();
67
+ return JoltModule;
68
+ }
69
+
70
+ export function isJoltReady() { return !!JoltModule; }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function vec3(x, y, z) { return new JoltModule.Vec3(x, y, z); }
77
+ function rvec3(x, y, z) { return new JoltModule.RVec3(x, y, z); }
78
+ function quat(x, y, z, w) { return new JoltModule.Quat(x, y, z, w); }
79
+
80
+ const OBJECT_LAYER_NON_MOVING = 0;
81
+ const OBJECT_LAYER_MOVING = 1;
82
+ const BP_LAYER_NON_MOVING = 0;
83
+ const BP_LAYER_MOVING = 1;
84
+
85
+ function buildSettings(J) {
86
+ // Object-layer pair filter — 16 layers, mirrors the native C++ shim.
87
+ const pairFilter = new J.ObjectLayerPairFilterTable(16);
88
+ // Default: all layers collide except non-moving↔non-moving.
89
+ for (let a = 0; a < 16; a++) {
90
+ for (let b = 0; b < 16; b++) {
91
+ if (a === OBJECT_LAYER_NON_MOVING && b === OBJECT_LAYER_NON_MOVING) continue;
92
+ pairFilter.EnableCollision(a, b);
93
+ }
94
+ }
95
+ // Broadphase-layer interface — 2 broadphase buckets.
96
+ const bpInterface = new J.BroadPhaseLayerInterfaceTable(16, 2);
97
+ bpInterface.MapObjectToBroadPhaseLayer(OBJECT_LAYER_NON_MOVING, new J.BroadPhaseLayer(BP_LAYER_NON_MOVING));
98
+ for (let i = 1; i < 16; i++) {
99
+ bpInterface.MapObjectToBroadPhaseLayer(i, new J.BroadPhaseLayer(BP_LAYER_MOVING));
100
+ }
101
+ const objectVsBp = new J.ObjectVsBroadPhaseLayerFilterTable(bpInterface, 2, pairFilter, 16);
102
+
103
+ const settings = new J.JoltSettings();
104
+ settings.mObjectLayerPairFilter = pairFilter;
105
+ settings.mBroadPhaseLayerInterface = bpInterface;
106
+ settings.mObjectVsBroadPhaseLayerFilter= objectVsBp;
107
+ settings.mMaxBodies = 65536;
108
+ settings.mMaxBodyPairs = 65536;
109
+ settings.mMaxContactConstraints = 10240;
110
+ // The filter tables ride along: builds without
111
+ // system.GetDefaultBroadPhaseLayerFilter() need them to construct the
112
+ // per-layer query filters (see queryFilters).
113
+ return { settings, pairFilter, objectVsBp };
114
+ }
115
+
116
+ function motionTypeFrom(t) {
117
+ const J = JoltModule;
118
+ switch (t | 0) {
119
+ case 0: return J.EMotionType_Static;
120
+ case 1: return J.EMotionType_Kinematic;
121
+ default: return J.EMotionType_Dynamic;
122
+ }
123
+ }
124
+
125
+ function activationFrom(flag) {
126
+ const J = JoltModule;
127
+ return flag !== 0 ? J.EActivation_Activate : J.EActivation_DontActivate;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // World
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export function createWorld(gx, gy, gz, maxBodies, numThreads) {
135
+ if (warnUninit('createWorld')) return 0;
136
+ const J = JoltModule;
137
+ const { settings, pairFilter, objectVsBp } = buildSettings(J);
138
+ if ((maxBodies | 0) > 0) settings.mMaxBodies = maxBodies | 0;
139
+ const jolt = new J.JoltInterface(settings);
140
+ const system = jolt.GetPhysicsSystem();
141
+ system.SetGravity(vec3(gx, gy, gz));
142
+
143
+ const handle = state.nextWorld++;
144
+
145
+ // Contact listener — bridges Jolt contact callbacks into the state.contacts queue.
146
+ // JoltPhysics.js provides ContactListenerJS with virtual methods exposed as JS fields.
147
+ const listener = new J.ContactListenerJS();
148
+ const pushContact = (event, body1, body2, manifold, settings_out) => {
149
+ const bodyIdA = body1.GetID();
150
+ const bodyIdB = body2.GetID();
151
+ const bodyAh = bodyIdToHandle(handle, bodyIdA);
152
+ const bodyBh = bodyIdToHandle(handle, bodyIdB);
153
+ let pointA = [0, 0, 0], pointB = [0, 0, 0], normal = [0, 1, 0], depth = 0;
154
+ if (manifold) {
155
+ const base = manifold.mBaseOffset;
156
+ const n = manifold.mWorldSpaceNormal;
157
+ normal = [n.GetX(), n.GetY(), n.GetZ()];
158
+ depth = manifold.mPenetrationDepth;
159
+ if (manifold.mRelativeContactPointsOn1.size() > 0) {
160
+ const p = manifold.mRelativeContactPointsOn1.at(0);
161
+ pointA = [base.GetX() + p.GetX(), base.GetY() + p.GetY(), base.GetZ() + p.GetZ()];
162
+ }
163
+ if (manifold.mRelativeContactPointsOn2.size() > 0) {
164
+ const p = manifold.mRelativeContactPointsOn2.at(0);
165
+ pointB = [base.GetX() + p.GetX(), base.GetY() + p.GetY(), base.GetZ() + p.GetZ()];
166
+ }
167
+ }
168
+ let friction = 0, restitution = 0;
169
+ if (settings_out) {
170
+ friction = settings_out.mCombinedFriction ?? 0;
171
+ restitution = settings_out.mCombinedRestitution ?? 0;
172
+ }
173
+ state.contacts.push({
174
+ event, bodyA: bodyAh, bodyB: bodyBh,
175
+ pointA, pointB, normal,
176
+ penetrationDepth: depth,
177
+ combinedFriction: friction,
178
+ combinedRestitution: restitution,
179
+ });
180
+ };
181
+ listener.OnContactValidate = () => J.ValidateResult_AcceptAllContactsForThisBodyPair;
182
+ listener.OnContactAdded = (b1, b2, m, s) => pushContact(0, b1, b2, m, s);
183
+ listener.OnContactPersisted = (b1, b2, m, s) => pushContact(1, b1, b2, m, s);
184
+ listener.OnContactRemoved = (pair) => {
185
+ const b1 = pair.GetBody1ID();
186
+ const b2 = pair.GetBody2ID();
187
+ state.contacts.push({
188
+ event: 2,
189
+ bodyA: bodyIdToHandle(handle, b1),
190
+ bodyB: bodyIdToHandle(handle, b2),
191
+ pointA: [0, 0, 0], pointB: [0, 0, 0], normal: [0, 1, 0],
192
+ penetrationDepth: 0, combinedFriction: 0, combinedRestitution: 0,
193
+ });
194
+ };
195
+ system.SetContactListener(listener);
196
+
197
+ state.worlds.set(handle, {
198
+ jolt,
199
+ system,
200
+ bodyInterface: system.GetBodyInterface(),
201
+ settings,
202
+ pairFilter, // for DefaultObjectLayerFilter construction
203
+ objectVsBp, // for DefaultBroadPhaseLayerFilter construction
204
+ listener, // hold ref so GC doesn't collect
205
+ });
206
+ return handle;
207
+ }
208
+
209
+ function bodyIdToHandle(worldH, bodyId) {
210
+ for (const [h, b] of state.bodies) {
211
+ if (b.world === worldH && b.bodyId.GetIndexAndSequenceNumber() === bodyId.GetIndexAndSequenceNumber()) {
212
+ return h;
213
+ }
214
+ }
215
+ return 0;
216
+ }
217
+
218
+ export function destroyWorld(h) {
219
+ const w = state.worlds.get(h);
220
+ if (!w) return;
221
+ // Remove any bodies/constraints tied to this world first.
222
+ for (const [bh, b] of state.bodies) {
223
+ if (b.world === h) { destroyBodyInternal(w, b); state.bodies.delete(bh); }
224
+ }
225
+ for (const [ch, c] of state.constraints) {
226
+ if (c.world === h) { w.system.RemoveConstraint(c.constraint); state.constraints.delete(ch); }
227
+ }
228
+ JoltModule.destroy(w.jolt);
229
+ state.worlds.delete(h);
230
+ }
231
+
232
+ export function setGravity(h, gx, gy, gz) {
233
+ const w = state.worlds.get(h); if (!w) return;
234
+ w.system.SetGravity(vec3(gx, gy, gz));
235
+ }
236
+
237
+ export function getGravity(h, axis) {
238
+ const w = state.worlds.get(h); if (!w) return 0;
239
+ const g = w.system.GetGravity();
240
+ if (axis === 0) return g.GetX();
241
+ if (axis === 1) return g.GetY();
242
+ if (axis === 2) return g.GetZ();
243
+ return 0;
244
+ }
245
+
246
+ export function optimizeBroadphase(h) {
247
+ const w = state.worlds.get(h); if (!w) return;
248
+ w.system.OptimizeBroadPhase();
249
+ }
250
+
251
+ export function step(h, dt, collisionSteps) {
252
+ const w = state.worlds.get(h); if (!w) return;
253
+ const steps = Math.max(1, collisionSteps | 0);
254
+ w.jolt.Step(dt, steps);
255
+ }
256
+
257
+ // --- fixed-timestep stepping (mirrors physics_jolt.rs WorldStepState) ------
258
+ // Semantics must match the native implementation exactly: clamp the frame
259
+ // dt (0.25s), simulate whole fixed steps with a catch-up cap, carry the
260
+ // remainder, snapshot before the LAST step of a batch for interpolation,
261
+ // and return alpha = remainder / fixedDt.
262
+
263
+ function stepState(w) {
264
+ if (!w.stepState) {
265
+ w.stepState = { fixedDt: 1 / 60, maxSteps: 4, acc: 0, alpha: 1, interpolate: false, prev: new Map() };
266
+ }
267
+ return w.stepState;
268
+ }
269
+
270
+ function snapshotWorldBodies(worldHandle, st) {
271
+ st.prev.clear();
272
+ for (const [bh, b] of state.bodies) {
273
+ if (b.world !== worldHandle) continue;
274
+ const bi = resolveBodyInterface(b); if (!bi) continue;
275
+ const p = bi.GetPosition(b.bodyId), q = bi.GetRotation(b.bodyId);
276
+ st.prev.set(bh, [p.GetX(), p.GetY(), p.GetZ(), q.GetX(), q.GetY(), q.GetZ(), q.GetW()]);
277
+ }
278
+ }
279
+
280
+ export function stepFixed(h, frameDt, collisionSteps) {
281
+ const w = state.worlds.get(h); if (!w) return 1;
282
+ const st = stepState(w);
283
+ const dt = Number.isFinite(frameDt) ? Math.min(Math.max(frameDt, 0), 0.25) : 0;
284
+ st.acc += dt;
285
+ let steps = Math.floor(st.acc / st.fixedDt);
286
+ if (steps > st.maxSteps) {
287
+ steps = st.maxSteps;
288
+ st.acc = Math.min(st.acc, st.fixedDt * (steps + 1));
289
+ }
290
+ if (steps > 0) {
291
+ const cs = Math.max(1, collisionSteps | 0);
292
+ for (let i = 0; i < steps; i++) {
293
+ if (st.interpolate && i + 1 === steps) snapshotWorldBodies(h, st);
294
+ w.jolt.Step(st.fixedDt, cs);
295
+ }
296
+ }
297
+ st.acc -= steps * st.fixedDt;
298
+ st.alpha = Math.min(Math.max(st.acc / st.fixedDt, 0), 1);
299
+ return st.alpha;
300
+ }
301
+
302
+ export function setFixedTimestep(h, hz, maxSteps) {
303
+ const w = state.worlds.get(h); if (!w) return;
304
+ const st = stepState(w);
305
+ if (hz > 0 && Number.isFinite(hz)) st.fixedDt = 1 / hz;
306
+ if (maxSteps > 0) st.maxSteps = maxSteps | 0;
307
+ }
308
+
309
+ export function setInterpolation(h, on) {
310
+ const w = state.worlds.get(h); if (!w) return;
311
+ const st = stepState(w);
312
+ st.interpolate = !!on;
313
+ if (!st.interpolate) { st.prev.clear(); st.alpha = 1; }
314
+ }
315
+
316
+ export function getStepAlpha(h) {
317
+ const w = state.worlds.get(h);
318
+ return w && w.stepState ? w.stepState.alpha : 1;
319
+ }
320
+
321
+ /** Interpolation blend for a body's getters, or null for raw state. */
322
+ function interpPrev(b, bh) {
323
+ const w = state.worlds.get(b.world);
324
+ const st = w && w.stepState;
325
+ if (!st || !st.interpolate || st.alpha >= 1) return null;
326
+ const prev = st.prev.get(bh);
327
+ return prev ? { a: st.alpha, prev } : null;
328
+ }
329
+
330
+ export function setLayerCollides(h, a, b, collides) {
331
+ const w = state.worlds.get(h); if (!w) return;
332
+ // JoltSettings filter was already baked; runtime mutation isn't supported via
333
+ // the JS bindings. This is a no-op for now; layer setup must happen before
334
+ // createWorld in a future API tweak.
335
+ void a; void b; void collides;
336
+ }
337
+ export function getLayerCollides(h, a, b) {
338
+ void h; void a; void b;
339
+ return 1; // default permissive
340
+ }
341
+
342
+ export function bodyCount(h) { const w = state.worlds.get(h); return w ? w.system.GetNumBodies() : 0; }
343
+ export function activeBodyCount(h) { const w = state.worlds.get(h); return w ? w.system.GetNumActiveBodies() : 0; }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Shapes
347
+ // ---------------------------------------------------------------------------
348
+
349
+ function registerShape(shape) {
350
+ if (!shape) return 0;
351
+ const h = state.nextShape++;
352
+ state.shapes.set(h, shape);
353
+ return h;
354
+ }
355
+
356
+ export function shapeBox(hx, hy, hz, convexRadius) {
357
+ if (warnUninit('shapeBox')) return 0;
358
+ const settings = new JoltModule.BoxShapeSettings(vec3(hx, hy, hz), convexRadius);
359
+ const result = settings.Create();
360
+ return result.IsValid() ? registerShape(result.Get()) : 0;
361
+ }
362
+ export function shapeSphere(r) {
363
+ if (warnUninit('shapeSphere')) return 0;
364
+ const settings = new JoltModule.SphereShapeSettings(r);
365
+ const result = settings.Create();
366
+ return result.IsValid() ? registerShape(result.Get()) : 0;
367
+ }
368
+ export function shapeCapsule(h, r) {
369
+ if (warnUninit('shapeCapsule')) return 0;
370
+ const settings = new JoltModule.CapsuleShapeSettings(h, r);
371
+ const result = settings.Create();
372
+ return result.IsValid() ? registerShape(result.Get()) : 0;
373
+ }
374
+ export function shapeCylinder(h, r, cr) {
375
+ if (warnUninit('shapeCylinder')) return 0;
376
+ const settings = new JoltModule.CylinderShapeSettings(h, r, cr);
377
+ const result = settings.Create();
378
+ return result.IsValid() ? registerShape(result.Get()) : 0;
379
+ }
380
+ export function shapeScaled(base, sx, sy, sz) {
381
+ if (warnUninit('shapeScaled')) return 0;
382
+ const inner = state.shapes.get(base); if (!inner) return 0;
383
+ const settings = new JoltModule.ScaledShapeSettings(inner, vec3(sx, sy, sz));
384
+ const result = settings.Create();
385
+ return result.IsValid() ? registerShape(result.Get()) : 0;
386
+ }
387
+ export function shapeOffsetCom(base, ox, oy, oz) {
388
+ if (warnUninit('shapeOffsetCom')) return 0;
389
+ const inner = state.shapes.get(base); if (!inner) return 0;
390
+ const settings = new JoltModule.OffsetCenterOfMassShapeSettings(vec3(ox, oy, oz), inner);
391
+ const result = settings.Create();
392
+ return result.IsValid() ? registerShape(result.Get()) : 0;
393
+ }
394
+ export function shapeRelease(h) {
395
+ const s = state.shapes.get(h); if (!s) return;
396
+ state.shapes.delete(h);
397
+ // JoltPhysics.js shapes are refcounted internally; letting GC handle it is fine.
398
+ }
399
+ export function shapeBounds(h, axis) {
400
+ const s = state.shapes.get(h); if (!s) return 0;
401
+ const box = s.GetLocalBounds();
402
+ switch (axis | 0) {
403
+ case 0: return box.mMin.GetX();
404
+ case 1: return box.mMin.GetY();
405
+ case 2: return box.mMin.GetZ();
406
+ case 3: return box.mMax.GetX();
407
+ case 4: return box.mMax.GetY();
408
+ case 5: return box.mMax.GetZ();
409
+ default: return 0;
410
+ }
411
+ }
412
+ export function shapeVolume(h) {
413
+ const s = state.shapes.get(h); if (!s) return 0;
414
+ return s.GetVolume ? s.GetVolume() : 0;
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Bodies
419
+ // ---------------------------------------------------------------------------
420
+
421
+ function destroyBodyInternal(w, b) {
422
+ if (w.bodyInterface.IsAdded(b.bodyId)) w.bodyInterface.RemoveBody(b.bodyId);
423
+ w.bodyInterface.DestroyBody(b.bodyId);
424
+ }
425
+
426
+ function resolveBody(h) { return state.bodies.get(h); }
427
+ function resolveBodyInterface(b) {
428
+ const w = state.worlds.get(b.world); return w ? w.bodyInterface : null;
429
+ }
430
+
431
+ export function bodyCreate(worldH, shapeH, motionType, px, py, pz, rx, ry, rz, rw, layer) {
432
+ if (warnUninit('bodyCreate')) return 0;
433
+ const w = state.worlds.get(worldH); if (!w) return 0;
434
+ const s = state.shapes.get(shapeH); if (!s) return 0;
435
+ const J = JoltModule;
436
+ const settings = new J.BodyCreationSettings(
437
+ s,
438
+ rvec3(px, py, pz),
439
+ quat(rx, ry, rz, rw),
440
+ motionTypeFrom(motionType),
441
+ (layer | 0),
442
+ );
443
+ const bodyId = w.bodyInterface.CreateAndAddBody(settings, J.EActivation_Activate);
444
+ J.destroy(settings);
445
+ const h = state.nextBody++;
446
+ state.bodies.set(h, { world: worldH, bodyId });
447
+ return h;
448
+ }
449
+
450
+ export function bodyDestroy(h) {
451
+ const b = resolveBody(h); if (!b) return;
452
+ const w = state.worlds.get(b.world); if (w) destroyBodyInternal(w, b);
453
+ state.bodies.delete(h);
454
+ }
455
+
456
+ export function bodyActivate(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.ActivateBody(b.bodyId); }
457
+ export function bodyDeactivate(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.DeactivateBody(b.bodyId); }
458
+ export function bodyIsActive(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); return bi && bi.IsActive(b.bodyId) ? 1 : 0; }
459
+ export function bodyIsValid(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); return bi && bi.IsAdded(b.bodyId) ? 1 : 0; }
460
+
461
+ export function bodyGetPosition(h, axis) {
462
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return 0;
463
+ const p = bi.GetPosition(b.bodyId);
464
+ let x = p.GetX(), y = p.GetY(), z = p.GetZ();
465
+ const ip = interpPrev(b, h);
466
+ if (ip) {
467
+ const [px, py, pz] = ip.prev;
468
+ x = px + (x - px) * ip.a; y = py + (y - py) * ip.a; z = pz + (z - pz) * ip.a;
469
+ }
470
+ if (axis === 0) return x; if (axis === 1) return y; if (axis === 2) return z;
471
+ return 0;
472
+ }
473
+ export function bodyGetRotation(h, axis) {
474
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return 0;
475
+ const q = bi.GetRotation(b.bodyId);
476
+ let x = q.GetX(), y = q.GetY(), z = q.GetZ(), w = q.GetW();
477
+ const ip = interpPrev(b, h);
478
+ if (ip) {
479
+ // nlerp with shortest-path sign handling — matches the native getter
480
+ let [, , , px, py, pz, pw] = ip.prev;
481
+ const dot = px * x + py * y + pz * z + pw * w;
482
+ const s = dot < 0 ? -1 : 1;
483
+ px *= s; py *= s; pz *= s; pw *= s;
484
+ const a = ip.a;
485
+ x = px + (x - px) * a; y = py + (y - py) * a; z = pz + (z - pz) * a; w = pw + (w - pw) * a;
486
+ const len = Math.hypot(x, y, z, w);
487
+ if (len > 1e-6) { x /= len; y /= len; z /= len; w /= len; }
488
+ }
489
+ if (axis === 0) return x; if (axis === 1) return y;
490
+ if (axis === 2) return z; if (axis === 3) return w;
491
+ return 0;
492
+ }
493
+ export function bodySetPosition(h, x, y, z, activate) {
494
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
495
+ bi.SetPosition(b.bodyId, rvec3(x, y, z), activationFrom(activate));
496
+ }
497
+ export function bodySetRotation(h, x, y, z, w, activate) {
498
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
499
+ bi.SetRotation(b.bodyId, quat(x, y, z, w), activationFrom(activate));
500
+ }
501
+ export function bodySetTransform(h, px, py, pz, rx, ry, rz, rw, activate) {
502
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
503
+ bi.SetPositionAndRotation(b.bodyId, rvec3(px, py, pz), quat(rx, ry, rz, rw), activationFrom(activate));
504
+ }
505
+ export function bodyMoveKinematic(h, px, py, pz, rx, ry, rz, rw, dt) {
506
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
507
+ bi.MoveKinematic(b.bodyId, rvec3(px, py, pz), quat(rx, ry, rz, rw), dt);
508
+ }
509
+
510
+ export function bodyGetLinearVelocity(h, axis) {
511
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return 0;
512
+ const v = bi.GetLinearVelocity(b.bodyId);
513
+ if (axis === 0) return v.GetX(); if (axis === 1) return v.GetY(); if (axis === 2) return v.GetZ();
514
+ return 0;
515
+ }
516
+ export function bodyGetAngularVelocity(h, axis) {
517
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return 0;
518
+ const v = bi.GetAngularVelocity(b.bodyId);
519
+ if (axis === 0) return v.GetX(); if (axis === 1) return v.GetY(); if (axis === 2) return v.GetZ();
520
+ return 0;
521
+ }
522
+ export function bodyGetPointVelocity(h, px, py, pz, axis) {
523
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return 0;
524
+ const v = bi.GetPointVelocity(b.bodyId, rvec3(px, py, pz));
525
+ if (axis === 0) return v.GetX(); if (axis === 1) return v.GetY(); if (axis === 2) return v.GetZ();
526
+ return 0;
527
+ }
528
+ export function bodySetLinearVelocity(h, x, y, z) {
529
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
530
+ bi.SetLinearVelocity(b.bodyId, vec3(x, y, z));
531
+ }
532
+ export function bodySetAngularVelocity(h, x, y, z) {
533
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
534
+ bi.SetAngularVelocity(b.bodyId, vec3(x, y, z));
535
+ }
536
+
537
+ export function bodyAddForce(h, x, y, z) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.AddForce(b.bodyId, vec3(x, y, z)); }
538
+ export function bodyAddImpulse(h, x, y, z) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.AddImpulse(b.bodyId, vec3(x, y, z)); }
539
+ export function bodyAddTorque(h, x, y, z) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.AddTorque(b.bodyId, vec3(x, y, z)); }
540
+ export function bodyAddAngularImpulse(h, x, y, z) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.AddAngularImpulse(b.bodyId, vec3(x, y, z)); }
541
+ export function bodyAddForceAt(h, fx, fy, fz, px, py, pz) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.AddForceAt ? bi.AddForceAt(b.bodyId, vec3(fx, fy, fz), rvec3(px, py, pz)) : bi.AddForce(b.bodyId, vec3(fx, fy, fz), rvec3(px, py, pz)); }
542
+ export function bodyAddImpulseAt(h, ix, iy, iz, px, py, pz) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.AddImpulseAt ? bi.AddImpulseAt(b.bodyId, vec3(ix, iy, iz), rvec3(px, py, pz)) : bi.AddImpulse(b.bodyId, vec3(ix, iy, iz), rvec3(px, py, pz)); }
543
+
544
+ export function bodySetFriction(h, v) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.SetFriction(b.bodyId, v); }
545
+ export function bodySetRestitution(h, v) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi) bi.SetRestitution(b.bodyId, v); }
546
+ export function bodySetLinearDamping(h, v) {
547
+ const b = resolveBody(h); if (!b) return;
548
+ const w = state.worlds.get(b.world); if (!w) return;
549
+ const J = JoltModule;
550
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), b.bodyId);
551
+ if (lock.SucceededAndIsInBroadPhase()) {
552
+ const mp = lock.GetBody().GetMotionPropertiesUnchecked();
553
+ if (mp) mp.SetLinearDamping(v);
554
+ }
555
+ lock.ReleaseLock();
556
+ }
557
+ export function bodySetAngularDamping(h, v) {
558
+ const b = resolveBody(h); if (!b) return;
559
+ const w = state.worlds.get(b.world); if (!w) return;
560
+ const J = JoltModule;
561
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), b.bodyId);
562
+ if (lock.SucceededAndIsInBroadPhase()) {
563
+ const mp = lock.GetBody().GetMotionPropertiesUnchecked();
564
+ if (mp) mp.SetAngularDamping(v);
565
+ }
566
+ lock.ReleaseLock();
567
+ }
568
+ export function bodySetGravityFactor(h, v) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi && bi.SetGravityFactor) bi.SetGravityFactor(b.bodyId, v); }
569
+ export function bodySetCcd(h, enabled) {
570
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
571
+ const J = JoltModule;
572
+ bi.SetMotionQuality(b.bodyId, enabled ? J.EMotionQuality_LinearCast : J.EMotionQuality_Discrete);
573
+ }
574
+ export function bodySetMotionType(h, t, activate) {
575
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
576
+ bi.SetMotionType(b.bodyId, motionTypeFrom(t), activationFrom(activate));
577
+ }
578
+ export function bodySetObjectLayer(h, layer) {
579
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (bi && bi.SetObjectLayer) bi.SetObjectLayer(b.bodyId, layer | 0);
580
+ }
581
+ export function bodySetIsSensor(h, enabled) {
582
+ const b = resolveBody(h); if (!b) return;
583
+ const w = state.worlds.get(b.world); if (!w) return;
584
+ const J = JoltModule;
585
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), b.bodyId);
586
+ if (lock.SucceededAndIsInBroadPhase()) lock.GetBody().SetIsSensor(enabled !== 0);
587
+ lock.ReleaseLock();
588
+ }
589
+ export function bodySetAllowSleeping(h, enabled) {
590
+ const b = resolveBody(h); if (!b) return;
591
+ const w = state.worlds.get(b.world); if (!w) return;
592
+ const J = JoltModule;
593
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), b.bodyId);
594
+ if (lock.SucceededAndIsInBroadPhase()) lock.GetBody().SetAllowSleeping(enabled !== 0);
595
+ lock.ReleaseLock();
596
+ }
597
+ export function bodySetShape(h, shapeH, updateMass, activate) {
598
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return;
599
+ const s = state.shapes.get(shapeH); if (!s) return;
600
+ bi.SetShape(b.bodyId, s, updateMass !== 0, activationFrom(activate));
601
+ }
602
+ export function bodyLockRotationAxes(h, x, y, z) {
603
+ void h; void x; void y; void z; // stub — requires recreating the body on JS side
604
+ }
605
+ export function bodyLockTranslationAxes(h, x, y, z) {
606
+ void h; void x; void y; void z;
607
+ }
608
+
609
+ export function bodyGetMass(h) {
610
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi) return 0;
611
+ const inv = bi.GetInverseMass ? bi.GetInverseMass(b.bodyId) : 0;
612
+ return inv > 0 ? 1 / inv : 0;
613
+ }
614
+ export function bodyGetFriction(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); return bi ? bi.GetFriction(b.bodyId) : 0; }
615
+ export function bodyGetRestitution(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); return bi ? bi.GetRestitution(b.bodyId) : 0; }
616
+ export function bodyGetObjectLayer(h) { const b = resolveBody(h); const bi = b && resolveBodyInterface(b); return bi && bi.GetObjectLayer ? bi.GetObjectLayer(b.bodyId) : 0; }
617
+ export function bodySetUserData(h, lo, hi) {
618
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi || !bi.SetUserData) return;
619
+ // Pack lo (uint32) + hi (uint32) into uint64. Whether a u64 crosses
620
+ // JoltPhysics.js as BigInt or Number depends on how the build was linked
621
+ // (-sWASM_BIGINT or not) — the CDN 1.0.0 build wants Number and THROWS on
622
+ // BigInt, so probe once and remember. Number packing is exact here: call
623
+ // sites keep hi = 0, far below 2^53. Most call sites use just `lo`.
624
+ try {
625
+ bi.SetUserData(b.bodyId, BigInt(lo >>> 0) | (BigInt(hi >>> 0) << 32n));
626
+ } catch {
627
+ bi.SetUserData(b.bodyId, (hi >>> 0) * 0x100000000 + (lo >>> 0));
628
+ }
629
+ }
630
+ export function bodyGetUserData(h, part) {
631
+ const b = resolveBody(h); const bi = b && resolveBodyInterface(b); if (!bi || !bi.GetUserData) return 0;
632
+ const u = bi.GetUserData(b.bodyId);
633
+ if (typeof u === 'bigint') {
634
+ return part === 1 ? Number(u >> 32n) : Number(u & 0xFFFFFFFFn);
635
+ }
636
+ return Number(u);
637
+ }
638
+
639
+ // ---------------------------------------------------------------------------
640
+ // Queries (Tier 1 — closest-hit only; all-hits/overlap = stubs for Tier 2)
641
+ // ---------------------------------------------------------------------------
642
+
643
+ export function raycast(worldH, ox, oy, oz, dx, dy, dz, maxDist, layerMask) {
644
+ state.rayHits.length = 0;
645
+ const w = state.worlds.get(worldH); if (!w) return 0;
646
+ const J = JoltModule;
647
+ const dir = vec3(dx, dy, dz);
648
+ if (dir.LengthSq() === 0) return 0;
649
+ const scaled = dir.Mul ? dir.Mul(maxDist / Math.sqrt(dir.LengthSq())) : dir;
650
+ const ray = new J.RRayCast(rvec3(ox, oy, oz), scaled);
651
+ // Layer 1 (MOVING) collides with every layer in the pair table, so its
652
+ // Default filters are the pass-all query. Two npm-build gotchas here:
653
+ // the abstract BroadPhaseLayerFilter/ObjectLayerFilter bases trap with a
654
+ // null vtable, and the closest-hit `CastRay(ray, RayCastResult, ...)`
655
+ // overload does not exist — only the collector form from the official
656
+ // examples is bound.
657
+ const qf = queryFilters(w, worldH, 1);
658
+ const rcSettings = new J.RayCastSettings();
659
+ const collector = new J.CastRayClosestHitCollisionCollector();
660
+ w.system.GetNarrowPhaseQuery().CastRay(ray, rcSettings, collector, qf.bp, qf.obj, qf.body, qf.shape);
661
+ const hit = collector.HadHit();
662
+ void layerMask; // layer filtering is handled by ObjectLayerFilter; Tier 2 adds mask support
663
+ if (hit) {
664
+ // Resolve body handle from BodyID.
665
+ const result = collector.mHit;
666
+ const bodyId = result.mBodyID;
667
+ let bodyHandle = 0;
668
+ for (const [h, b] of state.bodies) { if (b.world === worldH && b.bodyId.GetIndexAndSequenceNumber() === bodyId.GetIndexAndSequenceNumber()) { bodyHandle = h; break; } }
669
+ const fraction = result.mFraction;
670
+ state.rayHits.push({
671
+ body: bodyHandle,
672
+ point: [ox + (dx * maxDist) * fraction, oy + (dy * maxDist) * fraction, oz + (dz * maxDist) * fraction],
673
+ normal: [0, 1, 0], // TODO: world-space surface normal — needs a BodyLock
674
+ fraction,
675
+ subShapeId: 0,
676
+ });
677
+ J.destroy(collector); J.destroy(rcSettings); J.destroy(ray);
678
+ return 1;
679
+ }
680
+ J.destroy(collector); J.destroy(rcSettings); J.destroy(ray);
681
+ return 0;
682
+ }
683
+ export function raycastAll(worldH, ox, oy, oz, dx, dy, dz, maxDist, layerMask, maxHits) {
684
+ state.rayHits.length = 0;
685
+ void layerMask;
686
+ const w = state.worlds.get(worldH); if (!w) return 0;
687
+ const J = JoltModule;
688
+ const dir = vec3(dx, dy, dz);
689
+ if (dir.LengthSq() === 0) return 0;
690
+ const scaled = vec3(dx * maxDist, dy * maxDist, dz * maxDist);
691
+ const ray = new J.RRayCast(rvec3(ox, oy, oz), scaled);
692
+ const settings = new J.RayCastSettings();
693
+ const collector = new J.CastRayAllHitCollisionCollectorJS();
694
+ const qf = queryFilters(w, worldH, 1);
695
+ w.system.GetNarrowPhaseQuery().CastRay(
696
+ ray, settings, collector,
697
+ qf.bp, qf.obj, qf.body, qf.shape,
698
+ );
699
+ collector.Sort();
700
+ const count = Math.min(maxHits | 0, collector.mHits.size());
701
+ for (let i = 0; i < count; i++) {
702
+ const hit = collector.mHits.at(i);
703
+ const frac = hit.mFraction;
704
+ let bodyHandle = 0;
705
+ for (const [h, b] of state.bodies) {
706
+ if (b.world === worldH && b.bodyId.GetIndexAndSequenceNumber() === hit.mBodyID.GetIndexAndSequenceNumber()) {
707
+ bodyHandle = h; break;
708
+ }
709
+ }
710
+ state.rayHits.push({
711
+ body: bodyHandle,
712
+ point: [ox + dx * maxDist * frac, oy + dy * maxDist * frac, oz + dz * maxDist * frac],
713
+ normal: [0, 1, 0], // world-space normal requires a BodyLock; skipped for now
714
+ fraction: frac,
715
+ subShapeId: hit.mSubShapeID2 ? hit.mSubShapeID2.GetValue() : 0,
716
+ });
717
+ }
718
+ return state.rayHits.length;
719
+ }
720
+ export function rayHitCount() { return state.rayHits.length; }
721
+ export function rayHitBody(i) { return state.rayHits[i|0]?.body ?? 0; }
722
+ export function rayHitAxis(i, f) {
723
+ const h = state.rayHits[i|0]; if (!h) return 0;
724
+ return f < 3 ? h.point[f|0] : h.normal[(f|0)-3];
725
+ }
726
+ export function rayHitFraction(i) { return state.rayHits[i|0]?.fraction ?? 0; }
727
+ export function rayHitSubShape(i) { return state.rayHits[i|0]?.subShapeId ?? 0; }
728
+
729
+ function collectOverlapBodies(w, shapeCollider, maxResults) {
730
+ // JoltPhysics.js exposes broadphase query collectors via *_JS suffixed classes.
731
+ const J = JoltModule;
732
+ const collector = new J.CollideShapeBodyCollectorJS();
733
+ shapeCollider(collector);
734
+ const count = Math.min(maxResults | 0, collector.mHits.size());
735
+ const out = [];
736
+ for (let i = 0; i < count; i++) {
737
+ const bodyId = collector.mHits.at(i);
738
+ for (const [h, b] of state.bodies) {
739
+ if (b.world === w && b.bodyId.GetIndexAndSequenceNumber() === bodyId.GetIndexAndSequenceNumber()) {
740
+ out.push(h); break;
741
+ }
742
+ }
743
+ }
744
+ return out;
745
+ }
746
+
747
+ export function overlapSphere(worldH, cx, cy, cz, r, layerMask, maxResults) {
748
+ state.overlapBodies.length = 0;
749
+ void layerMask;
750
+ const w = state.worlds.get(worldH); if (!w) return 0;
751
+ const J = JoltModule;
752
+ state.overlapBodies = collectOverlapBodies(worldH, collector => {
753
+ w.system.GetBroadPhaseQuery().CollideSphere(
754
+ vec3(cx, cy, cz), r, collector,
755
+ queryFilters(w, worldH, 1).bp, queryFilters(w, worldH, 1).obj,
756
+ );
757
+ }, maxResults);
758
+ return state.overlapBodies.length;
759
+ }
760
+ export function overlapPoint(worldH, px, py, pz, layerMask, maxResults) {
761
+ state.overlapBodies.length = 0;
762
+ void layerMask;
763
+ const w = state.worlds.get(worldH); if (!w) return 0;
764
+ const J = JoltModule;
765
+ state.overlapBodies = collectOverlapBodies(worldH, collector => {
766
+ w.system.GetBroadPhaseQuery().CollidePoint(
767
+ vec3(px, py, pz), collector,
768
+ queryFilters(w, worldH, 1).bp, queryFilters(w, worldH, 1).obj,
769
+ );
770
+ }, maxResults);
771
+ return state.overlapBodies.length;
772
+ }
773
+ export function overlapBox(worldH, px, py, pz, rx, ry, rz, rw, hx, hy, hz, layerMask, maxResults) {
774
+ state.overlapBodies.length = 0;
775
+ void layerMask;
776
+ const w = state.worlds.get(worldH); if (!w) return 0;
777
+ const J = JoltModule;
778
+ // Conservative AABB around the rotated box (matches native C++ shim behavior).
779
+ const q = quat(rx, ry, rz, rw);
780
+ const rotMat = q.GetRotation ? q.GetRotation() : null;
781
+ // Jolt.js may not expose Quat.GetRotation; fall back to axis-aligned extents.
782
+ const ex = Math.abs(hx) + Math.abs(hy) * 0.001 + Math.abs(hz) * 0.001;
783
+ const ey = Math.abs(hy) + Math.abs(hx) * 0.001 + Math.abs(hz) * 0.001;
784
+ const ez = Math.abs(hz) + Math.abs(hx) * 0.001 + Math.abs(hy) * 0.001;
785
+ const box = new J.AABox(vec3(px - ex, py - ey, pz - ez), vec3(px + ex, py + ey, pz + ez));
786
+ state.overlapBodies = collectOverlapBodies(worldH, collector => {
787
+ w.system.GetBroadPhaseQuery().CollideAABox(
788
+ box, collector,
789
+ queryFilters(w, worldH, 1).bp, queryFilters(w, worldH, 1).obj,
790
+ );
791
+ }, maxResults);
792
+ void rotMat;
793
+ return state.overlapBodies.length;
794
+ }
795
+ export function overlapBody(i) { return state.overlapBodies[i|0] ?? 0; }
796
+
797
+ // ---------------------------------------------------------------------------
798
+ // Constraints
799
+ // ---------------------------------------------------------------------------
800
+
801
+ function resolveConstraintBodies(bodyAh, bodyBh) {
802
+ const J = JoltModule;
803
+ const ba = state.bodies.get(bodyAh); if (!ba) return null;
804
+ const w = state.worlds.get(ba.world); if (!w) return null;
805
+ // Retrieve the actual Body refs via the lock interface.
806
+ const lockA = new J.BodyLockRead(w.system.GetBodyLockInterface(), ba.bodyId);
807
+ if (!lockA.SucceededAndIsInBroadPhase()) { lockA.ReleaseLock(); return null; }
808
+ const body1 = lockA.GetBody();
809
+ let body2 = J.Body.prototype.sFixedToWorld ?? null;
810
+ let lockB = null;
811
+ if (bodyBh !== 0) {
812
+ const bb = state.bodies.get(bodyBh); if (!bb) { lockA.ReleaseLock(); return null; }
813
+ lockB = new J.BodyLockRead(w.system.GetBodyLockInterface(), bb.bodyId);
814
+ if (!lockB.SucceededAndIsInBroadPhase()) { lockA.ReleaseLock(); lockB.ReleaseLock(); return null; }
815
+ body2 = lockB.GetBody();
816
+ }
817
+ return {
818
+ w, body1, body2,
819
+ release: () => { lockA.ReleaseLock(); if (lockB) lockB.ReleaseLock(); },
820
+ };
821
+ }
822
+
823
+ function registerConstraint(worldId, constraint) {
824
+ state.worlds.get(worldId).system.AddConstraint(constraint);
825
+ const h = state.nextConstraint++;
826
+ state.constraints.set(h, { world: worldId, constraint });
827
+ return h;
828
+ }
829
+
830
+ export function constraintFixed(bodyA, bodyB, ax, ay, az, bx, by, bz, worldSpace) {
831
+ if (warnUninit('constraintFixed')) return 0;
832
+ const ctx = resolveConstraintBodies(bodyA, bodyB); if (!ctx) return 0;
833
+ const J = JoltModule;
834
+ const settings = new J.FixedConstraintSettings();
835
+ settings.mSpace = worldSpace ? J.EConstraintSpace_WorldSpace : J.EConstraintSpace_LocalToBodyCOM;
836
+ settings.mPoint1 = rvec3(ax, ay, az);
837
+ settings.mPoint2 = rvec3(bx, by, bz);
838
+ settings.mAutoDetectPoint = false;
839
+ const c = settings.Create(ctx.body1, ctx.body2);
840
+ ctx.release();
841
+ return registerConstraint(ctx.w === state.worlds.get(state.bodies.get(bodyA).world) ? state.bodies.get(bodyA).world : 0, c);
842
+ }
843
+
844
+ export function constraintPoint(bodyA, bodyB, ax, ay, az, bx, by, bz, worldSpace) {
845
+ if (warnUninit('constraintPoint')) return 0;
846
+ const ctx = resolveConstraintBodies(bodyA, bodyB); if (!ctx) return 0;
847
+ const J = JoltModule;
848
+ const settings = new J.PointConstraintSettings();
849
+ settings.mSpace = worldSpace ? J.EConstraintSpace_WorldSpace : J.EConstraintSpace_LocalToBodyCOM;
850
+ settings.mPoint1 = rvec3(ax, ay, az);
851
+ settings.mPoint2 = rvec3(bx, by, bz);
852
+ const c = settings.Create(ctx.body1, ctx.body2);
853
+ ctx.release();
854
+ return registerConstraint(state.bodies.get(bodyA).world, c);
855
+ }
856
+
857
+ export function constraintHinge(bodyA, bodyB, ax, ay, az, bx, by, bz, axX, axY, axZ, lmin, lmax, worldSpace) {
858
+ if (warnUninit('constraintHinge')) return 0;
859
+ const ctx = resolveConstraintBodies(bodyA, bodyB); if (!ctx) return 0;
860
+ const J = JoltModule;
861
+ const settings = new J.HingeConstraintSettings();
862
+ settings.mSpace = worldSpace ? J.EConstraintSpace_WorldSpace : J.EConstraintSpace_LocalToBodyCOM;
863
+ settings.mPoint1 = rvec3(ax, ay, az);
864
+ settings.mPoint2 = rvec3(bx, by, bz);
865
+ const axis = vec3(axX, axY, axZ);
866
+ settings.mHingeAxis1 = axis;
867
+ settings.mHingeAxis2 = axis;
868
+ const perp = axis.GetNormalizedPerpendicular ? axis.GetNormalizedPerpendicular() : vec3(1, 0, 0);
869
+ settings.mNormalAxis1 = perp;
870
+ settings.mNormalAxis2 = perp;
871
+ if (lmin < lmax) { settings.mLimitsMin = lmin; settings.mLimitsMax = lmax; }
872
+ const c = settings.Create(ctx.body1, ctx.body2);
873
+ ctx.release();
874
+ return registerConstraint(state.bodies.get(bodyA).world, c);
875
+ }
876
+
877
+ export function constraintSlider(bodyA, bodyB, ax, ay, az, bx, by, bz, axX, axY, axZ, lmin, lmax, worldSpace) {
878
+ if (warnUninit('constraintSlider')) return 0;
879
+ const ctx = resolveConstraintBodies(bodyA, bodyB); if (!ctx) return 0;
880
+ const J = JoltModule;
881
+ const settings = new J.SliderConstraintSettings();
882
+ settings.mSpace = worldSpace ? J.EConstraintSpace_WorldSpace : J.EConstraintSpace_LocalToBodyCOM;
883
+ settings.mPoint1 = rvec3(ax, ay, az);
884
+ settings.mPoint2 = rvec3(bx, by, bz);
885
+ // Normalize axis.
886
+ const len = Math.hypot(axX, axY, axZ) || 1;
887
+ const axis = vec3(axX / len, axY / len, axZ / len);
888
+ settings.mSliderAxis1 = axis;
889
+ settings.mSliderAxis2 = axis;
890
+ const perp = axis.GetNormalizedPerpendicular ? axis.GetNormalizedPerpendicular() : vec3(1, 0, 0);
891
+ settings.mNormalAxis1 = perp;
892
+ settings.mNormalAxis2 = perp;
893
+ if (lmin < lmax) { settings.mLimitsMin = lmin; settings.mLimitsMax = lmax; }
894
+ const c = settings.Create(ctx.body1, ctx.body2);
895
+ ctx.release();
896
+ return registerConstraint(state.bodies.get(bodyA).world, c);
897
+ }
898
+
899
+ export function constraintDistance(bodyA, bodyB, ax, ay, az, bx, by, bz, minD, maxD, worldSpace) {
900
+ if (warnUninit('constraintDistance')) return 0;
901
+ const ctx = resolveConstraintBodies(bodyA, bodyB); if (!ctx) return 0;
902
+ const J = JoltModule;
903
+ const settings = new J.DistanceConstraintSettings();
904
+ settings.mSpace = worldSpace ? J.EConstraintSpace_WorldSpace : J.EConstraintSpace_LocalToBodyCOM;
905
+ settings.mPoint1 = rvec3(ax, ay, az);
906
+ settings.mPoint2 = rvec3(bx, by, bz);
907
+ settings.mMinDistance = minD;
908
+ settings.mMaxDistance = maxD;
909
+ const c = settings.Create(ctx.body1, ctx.body2);
910
+ ctx.release();
911
+ return registerConstraint(state.bodies.get(bodyA).world, c);
912
+ }
913
+
914
+ // EN-063 (web ragdolls): six-DOF with all three translation axes locked and
915
+ // per-axis rotation limits — the articulation the engine's ragdoll builder
916
+ // uses. Mirrors the native C++ shim's convention: a rotation axis whose
917
+ // min >= max is LOCKED, otherwise limited to [min, max]. Wrapped in
918
+ // try/catch so a JoltPhysics.js build without the SixDOF surface degrades
919
+ // to "no ragdolls" instead of killing the frame.
920
+ export function constraintSixDofLockedTranslation(
921
+ bodyA, bodyB, ax, ay, az, bx, by, bz,
922
+ rxMin, rxMax, ryMin, ryMax, rzMin, rzMax, worldSpace,
923
+ ) {
924
+ if (warnUninit('constraintSixDofLockedTranslation')) return 0;
925
+ const ctx = resolveConstraintBodies(bodyA, bodyB); if (!ctx) return 0;
926
+ const J = JoltModule;
927
+ try {
928
+ const settings = new J.SixDOFConstraintSettings();
929
+ settings.mSpace = worldSpace ? J.EConstraintSpace_WorldSpace : J.EConstraintSpace_LocalToBodyCOM;
930
+ settings.mPosition1 = rvec3(ax, ay, az);
931
+ settings.mPosition2 = rvec3(bx, by, bz);
932
+ const AX = [
933
+ [J.SixDOFConstraintSettings_EAxis_TranslationX, 1, -1],
934
+ [J.SixDOFConstraintSettings_EAxis_TranslationY, 1, -1],
935
+ [J.SixDOFConstraintSettings_EAxis_TranslationZ, 1, -1],
936
+ [J.SixDOFConstraintSettings_EAxis_RotationX, rxMin, rxMax],
937
+ [J.SixDOFConstraintSettings_EAxis_RotationY, ryMin, ryMax],
938
+ [J.SixDOFConstraintSettings_EAxis_RotationZ, rzMin, rzMax],
939
+ ];
940
+ for (const [axis, lo, hi] of AX) {
941
+ if (lo >= hi) settings.MakeFixedAxis(axis);
942
+ else settings.SetLimitedAxis(axis, lo, hi);
943
+ }
944
+ const c = settings.Create(ctx.body1, ctx.body2);
945
+ ctx.release();
946
+ return registerConstraint(state.bodies.get(bodyA).world, c);
947
+ } catch (e) {
948
+ ctx.release();
949
+ console.warn('[jolt_bridge] SixDOF constraint unavailable:', e);
950
+ return 0;
951
+ }
952
+ }
953
+
954
+ export function constraintDestroy(h) {
955
+ const c = state.constraints.get(h); if (!c) return;
956
+ const w = state.worlds.get(c.world); if (w) w.system.RemoveConstraint(c.constraint);
957
+ state.constraints.delete(h);
958
+ }
959
+ export function constraintSetEnabled(h, enabled) {
960
+ const c = state.constraints.get(h); if (!c) return;
961
+ if (c.constraint.SetEnabled) c.constraint.SetEnabled(enabled !== 0);
962
+ }
963
+
964
+ // ---------------------------------------------------------------------------
965
+ // Contact events (Tier 2 — listener wiring not yet ported to JS)
966
+ // ---------------------------------------------------------------------------
967
+
968
+ export function contactCount() { return state.contacts.length; }
969
+ export function contactField(i, field) {
970
+ const c = state.contacts[i|0]; if (!c) return 0;
971
+ switch (field|0) {
972
+ case 0: return c.event;
973
+ case 1: return c.bodyA;
974
+ case 2: return c.bodyB;
975
+ case 3: return c.pointA[0]; case 4: return c.pointA[1]; case 5: return c.pointA[2];
976
+ case 6: return c.pointB[0]; case 7: return c.pointB[1]; case 8: return c.pointB[2];
977
+ case 9: return c.normal[0]; case 10: return c.normal[1]; case 11: return c.normal[2];
978
+ case 12: return c.penetrationDepth;
979
+ case 13: return c.combinedFriction;
980
+ case 14: return c.combinedRestitution;
981
+ default: return 0;
982
+ }
983
+ }
984
+ export function clearContacts(worldH) { void worldH; state.contacts.length = 0; }
985
+
986
+ // ============================================================================
987
+ // Phase 5 additions — complex shapes + character controller
988
+ // ============================================================================
989
+
990
+ // --- Scratch streams ---
991
+ export function scratchReset() { state.scratchF32.length = 0; state.scratchU32.length = 0; }
992
+ export function scratchPushF32(v) { state.scratchF32.push(v); }
993
+ export function scratchPushU32(v) { state.scratchU32.push(v >>> 0); }
994
+
995
+ // --- Complex shape factories ---
996
+
997
+ export function shapeConvexHull(numPoints, convexRadius) {
998
+ if (warnUninit('shapeConvexHull')) return 0;
999
+ const J = JoltModule;
1000
+ if (state.scratchF32.length < numPoints * 3 || numPoints < 3) return 0;
1001
+ const settings = new J.ConvexHullShapeSettings();
1002
+ // JoltPhysics.js exposes mPoints as a JS-friendly Array<Vec3>.
1003
+ for (let i = 0; i < numPoints; i++) {
1004
+ const p = vec3(state.scratchF32[i*3], state.scratchF32[i*3+1], state.scratchF32[i*3+2]);
1005
+ settings.mPoints.push_back(p);
1006
+ }
1007
+ settings.mConvexRadius = convexRadius;
1008
+ const result = settings.Create();
1009
+ return result.IsValid() ? registerShape(result.Get()) : 0;
1010
+ }
1011
+
1012
+ export function shapeMesh(vertexCount, triangleCount) {
1013
+ if (warnUninit('shapeMesh')) return 0;
1014
+ const J = JoltModule;
1015
+ if (state.scratchF32.length < vertexCount * 3 || state.scratchU32.length < triangleCount * 3) return 0;
1016
+ if (vertexCount === 0 || triangleCount === 0) return 0;
1017
+ const settings = new J.MeshShapeSettings();
1018
+ for (let i = 0; i < vertexCount; i++) {
1019
+ settings.mTriangleVertices.push_back(new J.Float3(
1020
+ state.scratchF32[i*3], state.scratchF32[i*3+1], state.scratchF32[i*3+2]
1021
+ ));
1022
+ }
1023
+ for (let t = 0; t < triangleCount; t++) {
1024
+ const idx = new J.IndexedTriangle(
1025
+ state.scratchU32[t*3], state.scratchU32[t*3+1], state.scratchU32[t*3+2], 0
1026
+ );
1027
+ settings.mIndexedTriangles.push_back(idx);
1028
+ }
1029
+ settings.Sanitize();
1030
+ const result = settings.Create();
1031
+ return result.IsValid() ? registerShape(result.Get()) : 0;
1032
+ }
1033
+
1034
+ export function shapeHeightfield(sampleCount, ox, oy, oz, sx, sy, sz, blockSize) {
1035
+ if (warnUninit('shapeHeightfield')) return 0;
1036
+ const J = JoltModule;
1037
+ const need = sampleCount * sampleCount;
1038
+ if (state.scratchF32.length < need || sampleCount < 2) return 0;
1039
+ // The convenience constructor taking a JS Float32Array does not exist in
1040
+ // the npm/CDN JoltPhysics.js builds (Create() returns invalid — the
1041
+ // samples never reach the shape). Canonical pattern from the official
1042
+ // examples: default-construct the settings, size mHeightSamples, and
1043
+ // write the floats straight into the emscripten heap.
1044
+ const settings = new J.HeightFieldShapeSettings();
1045
+ settings.mOffset = vec3(ox, oy, oz);
1046
+ settings.mScale = vec3(sx, sy, sz);
1047
+ settings.mSampleCount = sampleCount;
1048
+ settings.mBlockSize = blockSize || 4;
1049
+ settings.mHeightSamples.resize(need);
1050
+ const heap = new Float32Array(
1051
+ J.HEAPF32.buffer,
1052
+ J.getPointer(settings.mHeightSamples.data()),
1053
+ need,
1054
+ );
1055
+ for (let i = 0; i < need; i++) heap[i] = state.scratchF32[i];
1056
+ const result = settings.Create();
1057
+ return result.IsValid() ? registerShape(result.Get()) : 0;
1058
+ }
1059
+
1060
+ // --- Compound builder ---
1061
+
1062
+ export function compoundBegin() { state.compoundChildren.length = 0; }
1063
+ export function compoundAddChild(shape, px, py, pz, rx, ry, rz, rw) {
1064
+ const s = state.shapes.get(shape); if (!s) return;
1065
+ state.compoundChildren.push({ shape: s, px, py, pz, rx, ry, rz, rw });
1066
+ }
1067
+ export function compoundEnd() {
1068
+ if (warnUninit('compoundEnd')) return 0;
1069
+ const J = JoltModule;
1070
+ if (state.compoundChildren.length === 0) return 0;
1071
+ const settings = new J.StaticCompoundShapeSettings();
1072
+ for (const c of state.compoundChildren) {
1073
+ settings.AddShape(vec3(c.px, c.py, c.pz), quat(c.rx, c.ry, c.rz, c.rw), c.shape, 0);
1074
+ }
1075
+ state.compoundChildren.length = 0;
1076
+ const result = settings.Create();
1077
+ return result.IsValid() ? registerShape(result.Get()) : 0;
1078
+ }
1079
+
1080
+ // --- Character controller (CharacterVirtual) ---
1081
+
1082
+ // Query-filter compatibility + caching. Two JoltPhysics.js API generations
1083
+ // exist: some builds expose `system.GetDefaultBroadPhaseLayerFilter(layer)`,
1084
+ // the npm/CDN 1.0.0 build only has the constructor forms
1085
+ // (`new Jolt.DefaultBroadPhaseLayerFilter(system.GetObjectVsBroadPhaseLayerFilter(), layer)`).
1086
+ // Cached per (world, layer) — the character path runs every frame, and
1087
+ // constructing emscripten objects per call is a real leak.
1088
+ const queryFilterCache = new Map(); // "worldH:layer" → { bp, obj, body, shape }
1089
+ function queryFilters(w, worldH, layer) {
1090
+ const key = worldH + ':' + (layer | 0);
1091
+ let f = queryFilterCache.get(key);
1092
+ if (f) return f;
1093
+ const J = JoltModule;
1094
+ let bp, obj;
1095
+ if (typeof w.system.GetDefaultBroadPhaseLayerFilter === 'function') {
1096
+ bp = w.system.GetDefaultBroadPhaseLayerFilter(layer);
1097
+ obj = w.system.GetDefaultLayerFilter(layer);
1098
+ } else {
1099
+ // npm/CDN builds: the canonical form from the JoltPhysics.js examples —
1100
+ // the filter getters live on the JoltInterface, and constructing the
1101
+ // Default* filters from the raw tables instead trips a null virtual
1102
+ // inside ExtendedUpdate.
1103
+ bp = new J.DefaultBroadPhaseLayerFilter(w.jolt.GetObjectVsBroadPhaseLayerFilter(), layer);
1104
+ obj = new J.DefaultObjectLayerFilter(w.jolt.GetObjectLayerPairFilter(), layer);
1105
+ }
1106
+ f = { bp, obj, body: new J.BodyFilter(), shape: new J.ShapeFilter() };
1107
+ queryFilterCache.set(key, f);
1108
+ return f;
1109
+ }
1110
+
1111
+ export function characterCreate(
1112
+ worldH, shapeH,
1113
+ upX, upY, upZ,
1114
+ maxSlopeAngle, characterPadding,
1115
+ penetrationRecoverySpeed, predictiveContactDistance,
1116
+ maxStrength, mass, objectLayer,
1117
+ px, py, pz, rx, ry, rz, rw,
1118
+ ) {
1119
+ if (warnUninit('characterCreate')) return 0;
1120
+ const w = state.worlds.get(worldH); if (!w) return 0;
1121
+ const shape = state.shapes.get(shapeH); if (!shape) return 0;
1122
+ const J = JoltModule;
1123
+ const settings = new J.CharacterVirtualSettings();
1124
+ settings.mShape = shape;
1125
+ settings.mUp = vec3(upX, upY, upZ);
1126
+ settings.mMaxSlopeAngle = maxSlopeAngle;
1127
+ settings.mCharacterPadding = characterPadding;
1128
+ settings.mPenetrationRecoverySpeed = penetrationRecoverySpeed;
1129
+ settings.mPredictiveContactDistance = predictiveContactDistance;
1130
+ settings.mMaxStrength = maxStrength;
1131
+ settings.mMass = mass;
1132
+ // Two constructor generations exist. -sWASM_BIGINT builds take
1133
+ // (settings, pos, rot, userData: u64-as-BigInt, system); the npm/CDN
1134
+ // builds bind FOUR args (settings, pos, rot, system). Crucially, do NOT
1135
+ // fall back to a 5-arg call with a Number userData: emscripten dispatches
1136
+ // by declared arity, so on a 4-arg build the `0` lands in the SYSTEM slot
1137
+ // and the character is constructed around a null PhysicsSystem — nothing
1138
+ // fails until ExtendedUpdate dies with "null function". The BigInt
1139
+ // TypeError is the reliable discriminator between the generations.
1140
+ let character;
1141
+ try {
1142
+ character = new J.CharacterVirtual(
1143
+ settings, rvec3(px, py, pz), quat(rx, ry, rz, rw), 0n, w.system
1144
+ );
1145
+ } catch {
1146
+ character = new J.CharacterVirtual(
1147
+ settings, rvec3(px, py, pz), quat(rx, ry, rz, rw), w.system
1148
+ );
1149
+ }
1150
+ const h = state.nextCharacter++;
1151
+ state.characters.set(h, { world: worldH, character, layer: objectLayer | 0 });
1152
+ return h;
1153
+ }
1154
+
1155
+ export function characterDestroy(h) {
1156
+ const e = state.characters.get(h); if (!e) return;
1157
+ JoltModule.destroy(e.character);
1158
+ state.characters.delete(h);
1159
+ }
1160
+
1161
+ export function characterUpdate(h, dt, gx, gy, gz) {
1162
+ const e = state.characters.get(h); if (!e) return;
1163
+ const w = state.worlds.get(e.world); if (!w) return;
1164
+ const J = JoltModule;
1165
+ // Integrate gravity into velocity (match native C shim behaviour).
1166
+ const v = e.character.GetLinearVelocity();
1167
+ const newV = vec3(v.GetX() + gx * dt, v.GetY() + gy * dt, v.GetZ() + gz * dt);
1168
+ e.character.SetLinearVelocity(newV);
1169
+ const settings = new J.ExtendedUpdateSettings();
1170
+ const f = queryFilters(w, e.world, e.layer);
1171
+ e.character.ExtendedUpdate(
1172
+ dt, vec3(gx, gy, gz),
1173
+ settings,
1174
+ f.bp, f.obj, f.body, f.shape,
1175
+ w.jolt.GetTempAllocator()
1176
+ );
1177
+ J.destroy(settings);
1178
+ }
1179
+
1180
+ export function characterGetPosition(h, axis) {
1181
+ const e = state.characters.get(h); if (!e) return 0;
1182
+ const p = e.character.GetPosition();
1183
+ if (axis === 0) return p.GetX(); if (axis === 1) return p.GetY(); if (axis === 2) return p.GetZ();
1184
+ return 0;
1185
+ }
1186
+ export function characterGetRotation(h, axis) {
1187
+ const e = state.characters.get(h); if (!e) return axis === 3 ? 1 : 0;
1188
+ const q = e.character.GetRotation();
1189
+ if (axis === 0) return q.GetX(); if (axis === 1) return q.GetY();
1190
+ if (axis === 2) return q.GetZ(); if (axis === 3) return q.GetW();
1191
+ return 0;
1192
+ }
1193
+ export function characterSetPosition(h, x, y, z) {
1194
+ const e = state.characters.get(h); if (!e) return;
1195
+ e.character.SetPosition(rvec3(x, y, z));
1196
+ }
1197
+ export function characterSetRotation(h, x, y, z, w) {
1198
+ const e = state.characters.get(h); if (!e) return;
1199
+ e.character.SetRotation(quat(x, y, z, w));
1200
+ }
1201
+ export function characterGetLinearVelocity(h, axis) {
1202
+ const e = state.characters.get(h); if (!e) return 0;
1203
+ const v = e.character.GetLinearVelocity();
1204
+ if (axis === 0) return v.GetX(); if (axis === 1) return v.GetY(); if (axis === 2) return v.GetZ();
1205
+ return 0;
1206
+ }
1207
+ export function characterSetLinearVelocity(h, x, y, z) {
1208
+ const e = state.characters.get(h); if (!e) return;
1209
+ e.character.SetLinearVelocity(vec3(x, y, z));
1210
+ }
1211
+ export function characterGetGroundState(h) {
1212
+ const e = state.characters.get(h); if (!e) return 3;
1213
+ const J = JoltModule;
1214
+ const s = e.character.GetGroundState();
1215
+ // Map Jolt enum → bj_ground_state.
1216
+ if (s === J.EGroundState_OnGround) return 0;
1217
+ if (s === J.EGroundState_OnSteepGround) return 1;
1218
+ if (s === J.EGroundState_NotSupported) return 2;
1219
+ return 3;
1220
+ }
1221
+ export function characterGetGroundNormal(h, axis) {
1222
+ const e = state.characters.get(h); if (!e) return axis === 1 ? 1 : 0;
1223
+ const n = e.character.GetGroundNormal();
1224
+ if (axis === 0) return n.GetX(); if (axis === 1) return n.GetY(); if (axis === 2) return n.GetZ();
1225
+ return 0;
1226
+ }
1227
+ export function characterGetGroundPosition(h, axis) {
1228
+ const e = state.characters.get(h); if (!e) return 0;
1229
+ const p = e.character.GetGroundPosition();
1230
+ if (axis === 0) return p.GetX(); if (axis === 1) return p.GetY(); if (axis === 2) return p.GetZ();
1231
+ return 0;
1232
+ }
1233
+ export function characterGetGroundBody(h) {
1234
+ const e = state.characters.get(h); if (!e) return 0;
1235
+ const bodyId = e.character.GetGroundBodyID();
1236
+ for (const [bh, b] of state.bodies) {
1237
+ if (b.world === e.world && b.bodyId.GetIndexAndSequenceNumber() === bodyId.GetIndexAndSequenceNumber()) {
1238
+ return bh;
1239
+ }
1240
+ }
1241
+ return 0;
1242
+ }
1243
+ export function characterSetShape(h, shapeH) {
1244
+ const e = state.characters.get(h); if (!e) return;
1245
+ const w = state.worlds.get(e.world); if (!w) return;
1246
+ const s = state.shapes.get(shapeH); if (!s) return;
1247
+ const J = JoltModule;
1248
+ const f = queryFilters(w, e.world, e.layer);
1249
+ e.character.SetShape(
1250
+ s, Number.MAX_VALUE,
1251
+ f.bp, f.obj, f.body, f.shape,
1252
+ w.jolt.GetTempAllocator()
1253
+ );
1254
+ }
1255
+
1256
+ // ---------------------------------------------------------------------------
1257
+ // Soft bodies (Tier 2 — cloth / rope / jelly)
1258
+ // ---------------------------------------------------------------------------
1259
+
1260
+ export function softBodyCreate(
1261
+ worldH, vertexCount, triangleCount,
1262
+ px, py, pz, rx, ry, rz, rw,
1263
+ objectLayer, edgeCompliance, gravityFactor, linearDamping, pressure,
1264
+ ) {
1265
+ if (warnUninit('softBodyCreate')) return 0;
1266
+ const w = state.worlds.get(worldH); if (!w) return 0;
1267
+ const needF = vertexCount * 4, needU = triangleCount * 3;
1268
+ if (state.scratchF32.length < needF || state.scratchU32.length < needU) return 0;
1269
+ if (vertexCount < 3 || triangleCount === 0) return 0;
1270
+
1271
+ const J = JoltModule;
1272
+ const shared = new J.SoftBodySharedSettings();
1273
+ for (let i = 0; i < vertexCount; i++) {
1274
+ const v = new J.SoftBodySharedSettingsVertex();
1275
+ v.mPosition = new J.Float3(state.scratchF32[i*4], state.scratchF32[i*4+1], state.scratchF32[i*4+2]);
1276
+ v.mVelocity = new J.Float3(0, 0, 0);
1277
+ v.mInvMass = state.scratchF32[i*4+3];
1278
+ shared.mVertices.push_back(v);
1279
+ }
1280
+ for (let t = 0; t < triangleCount; t++) {
1281
+ const f = new J.SoftBodySharedSettingsFace(
1282
+ state.scratchU32[t*3], state.scratchU32[t*3+1], state.scratchU32[t*3+2], 0
1283
+ );
1284
+ shared.AddFace(f);
1285
+ }
1286
+ const attrs = new J.SoftBodySharedSettingsVertexAttributes();
1287
+ attrs.mCompliance = edgeCompliance;
1288
+ attrs.mShearCompliance = edgeCompliance;
1289
+ attrs.mBendCompliance = edgeCompliance;
1290
+ shared.CreateConstraints(attrs, 1, J.EBendType_Distance ?? 0);
1291
+ shared.Optimize();
1292
+
1293
+ const bcs = new J.SoftBodyCreationSettings(shared, rvec3(px, py, pz), quat(rx, ry, rz, rw), objectLayer | 0);
1294
+ bcs.mGravityFactor = gravityFactor;
1295
+ bcs.mLinearDamping = linearDamping;
1296
+ bcs.mPressure = pressure;
1297
+ bcs.mUpdatePosition = true;
1298
+
1299
+ const bodyId = w.bodyInterface.CreateAndAddSoftBody(bcs, J.EActivation_Activate);
1300
+ const h = state.nextBody++;
1301
+ state.bodies.set(h, { world: worldH, bodyId, isSoftBody: true });
1302
+ return h;
1303
+ }
1304
+
1305
+ function softMotionProperties(h) {
1306
+ const b = state.bodies.get(h); if (!b) return null;
1307
+ const w = state.worlds.get(b.world); if (!w) return null;
1308
+ const J = JoltModule;
1309
+ // Use read-locking to safely access SoftBodyMotionProperties.
1310
+ const lock = new J.BodyLockRead(w.system.GetBodyLockInterface(), b.bodyId);
1311
+ if (!lock.SucceededAndIsInBroadPhase()) { lock.ReleaseLock(); return null; }
1312
+ const body = lock.GetBody();
1313
+ if (!body.IsSoftBody()) { lock.ReleaseLock(); return null; }
1314
+ const mp = J.castObject ? J.castObject(body.GetMotionPropertiesUnchecked(), J.SoftBodyMotionProperties)
1315
+ : body.GetMotionPropertiesUnchecked();
1316
+ return { lock, body, mp };
1317
+ }
1318
+
1319
+ export function softBodyVertexCount(h) {
1320
+ const ctx = softMotionProperties(h); if (!ctx) return 0;
1321
+ const n = ctx.mp.GetVertices ? ctx.mp.GetVertices().size() : 0;
1322
+ ctx.lock.ReleaseLock();
1323
+ return n;
1324
+ }
1325
+ export function softBodyGetVertex(h, idx, axis) {
1326
+ const ctx = softMotionProperties(h); if (!ctx) return 0;
1327
+ const verts = ctx.mp.GetVertices ? ctx.mp.GetVertices() : null;
1328
+ if (!verts || idx >= verts.size()) { ctx.lock.ReleaseLock(); return 0; }
1329
+ const v = verts.at(idx);
1330
+ const local = v.mPosition; // Vec3 in body-local space
1331
+ const xform = ctx.body.GetWorldTransform();
1332
+ const worldPos = xform.Multiply3x4(local);
1333
+ ctx.lock.ReleaseLock();
1334
+ if (axis === 0) return worldPos.GetX();
1335
+ if (axis === 1) return worldPos.GetY();
1336
+ if (axis === 2) return worldPos.GetZ();
1337
+ return 0;
1338
+ }
1339
+ export function softBodySetVertex(h, idx, x, y, z) {
1340
+ const b = state.bodies.get(h); if (!b) return;
1341
+ const w = state.worlds.get(b.world); if (!w) return;
1342
+ const J = JoltModule;
1343
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), b.bodyId);
1344
+ if (lock.SucceededAndIsInBroadPhase()) {
1345
+ const body = lock.GetBody();
1346
+ if (body.IsSoftBody()) {
1347
+ const mp = J.castObject ? J.castObject(body.GetMotionPropertiesUnchecked(), J.SoftBodyMotionProperties)
1348
+ : body.GetMotionPropertiesUnchecked();
1349
+ const verts = mp.GetVertices();
1350
+ if (idx < verts.size()) {
1351
+ const xform = body.GetWorldTransform();
1352
+ const inv = xform.Inversed();
1353
+ const local = inv.Multiply3x4(vec3(x, y, z));
1354
+ verts.at(idx).mPosition = local;
1355
+ }
1356
+ }
1357
+ }
1358
+ lock.ReleaseLock();
1359
+ }
1360
+ export function softBodySetVertexInvMass(h, idx, invMass) {
1361
+ const b = state.bodies.get(h); if (!b) return;
1362
+ const w = state.worlds.get(b.world); if (!w) return;
1363
+ const J = JoltModule;
1364
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), b.bodyId);
1365
+ if (lock.SucceededAndIsInBroadPhase()) {
1366
+ const body = lock.GetBody();
1367
+ if (body.IsSoftBody()) {
1368
+ const mp = J.castObject ? J.castObject(body.GetMotionPropertiesUnchecked(), J.SoftBodyMotionProperties)
1369
+ : body.GetMotionPropertiesUnchecked();
1370
+ const verts = mp.GetVertices();
1371
+ if (idx < verts.size()) verts.at(idx).mInvMass = invMass;
1372
+ }
1373
+ }
1374
+ lock.ReleaseLock();
1375
+ }
1376
+
1377
+ // ---------------------------------------------------------------------------
1378
+ // Wheeled vehicles (Tier 2 — 4-wheel car)
1379
+ // ---------------------------------------------------------------------------
1380
+
1381
+ export function vehicleCreate(
1382
+ worldH, chassisShapeH,
1383
+ upX, upY, upZ, fwX, fwY, fwZ,
1384
+ w0x, w0y, w0z, w1x, w1y, w1z, w2x, w2y, w2z, w3x, w3y, w3z,
1385
+ wheelRadius, wheelWidth, suspensionMin, suspensionMax,
1386
+ maxSteerAngle, maxBrakeTorque, maxHandbrakeTorque,
1387
+ engineMaxTorque, maxPitchRollAngle, objectLayer,
1388
+ px, py, pz, rx, ry, rz, rw,
1389
+ ) {
1390
+ if (warnUninit('vehicleCreate')) return 0;
1391
+ const w = state.worlds.get(worldH); if (!w) return 0;
1392
+ const shape = state.shapes.get(chassisShapeH); if (!shape) return 0;
1393
+ const J = JoltModule;
1394
+
1395
+ // Chassis body.
1396
+ const chassisSettings = new J.BodyCreationSettings(
1397
+ shape, rvec3(px, py, pz), quat(rx, ry, rz, rw),
1398
+ J.EMotionType_Dynamic, objectLayer | 0,
1399
+ );
1400
+ chassisSettings.mOverrideMassProperties = J.EOverrideMassProperties_CalculateInertia;
1401
+ chassisSettings.mMassPropertiesOverride.mMass = 1500.0;
1402
+ const chassisId = w.bodyInterface.CreateAndAddBody(chassisSettings, J.EActivation_Activate);
1403
+ const chassisHandle = state.nextBody++;
1404
+ state.bodies.set(chassisHandle, { world: worldH, bodyId: chassisId });
1405
+
1406
+ // VehicleConstraintSettings.
1407
+ const vcs = new J.VehicleConstraintSettings();
1408
+ vcs.mUp = vec3(upX, upY, upZ);
1409
+ vcs.mForward = vec3(fwX, fwY, fwZ);
1410
+ vcs.mMaxPitchRollAngle = maxPitchRollAngle;
1411
+
1412
+ // 4 wheels — FL/FR steer, RL/RR drive + handbrake.
1413
+ const wheelPositions = [
1414
+ [w0x, w0y, w0z], [w1x, w1y, w1z], [w2x, w2y, w2z], [w3x, w3y, w3z],
1415
+ ];
1416
+ for (let i = 0; i < 4; i++) {
1417
+ const wheel = new J.WheelSettingsWV();
1418
+ wheel.mPosition = vec3(wheelPositions[i][0], wheelPositions[i][1], wheelPositions[i][2]);
1419
+ wheel.mRadius = wheelRadius;
1420
+ wheel.mWidth = wheelWidth;
1421
+ wheel.mSuspensionMinLength = suspensionMin;
1422
+ wheel.mSuspensionMaxLength = suspensionMax;
1423
+ wheel.mMaxSteerAngle = (i < 2) ? maxSteerAngle : 0.0;
1424
+ wheel.mMaxBrakeTorque = maxBrakeTorque;
1425
+ wheel.mMaxHandBrakeTorque = (i >= 2) ? maxHandbrakeTorque : 0.0;
1426
+ vcs.mWheels.push_back(wheel);
1427
+ }
1428
+
1429
+ // WheeledVehicleController with a single rear-axle differential.
1430
+ const controller = new J.WheeledVehicleControllerSettings();
1431
+ controller.mEngine.mMaxTorque = engineMaxTorque;
1432
+ const diff = new J.VehicleDifferentialSettings();
1433
+ diff.mLeftWheel = 2;
1434
+ diff.mRightWheel = 3;
1435
+ controller.mDifferentials.push_back(diff);
1436
+ vcs.mController = controller;
1437
+
1438
+ // Lock chassis to construct the constraint.
1439
+ const lock = new J.BodyLockWrite(w.system.GetBodyLockInterface(), chassisId);
1440
+ if (!lock.SucceededAndIsInBroadPhase()) { lock.ReleaseLock(); return 0; }
1441
+ const constraint = new J.VehicleConstraint(lock.GetBody(), vcs);
1442
+ lock.ReleaseLock();
1443
+
1444
+ const tester = new J.VehicleCollisionTesterRay(OBJECT_LAYER_NON_MOVING, vec3(upX, upY, upZ));
1445
+ constraint.SetVehicleCollisionTester(tester);
1446
+ w.system.AddConstraint(constraint);
1447
+ w.system.AddStepListener(constraint);
1448
+
1449
+ const vh = state.nextConstraint++;
1450
+ state.constraints.set(vh, {
1451
+ world: worldH, constraint, tester,
1452
+ isVehicle: true, chassisHandle, chassisId,
1453
+ });
1454
+ return vh;
1455
+ }
1456
+
1457
+ export function vehicleDestroy(h) {
1458
+ const v = state.constraints.get(h); if (!v || !v.isVehicle) return;
1459
+ const w = state.worlds.get(v.world); if (!w) return;
1460
+ w.system.RemoveStepListener(v.constraint);
1461
+ w.system.RemoveConstraint(v.constraint);
1462
+ if (w.bodyInterface.IsAdded(v.chassisId)) w.bodyInterface.RemoveBody(v.chassisId);
1463
+ w.bodyInterface.DestroyBody(v.chassisId);
1464
+ state.bodies.delete(v.chassisHandle);
1465
+ state.constraints.delete(h);
1466
+ }
1467
+
1468
+ export function vehicleGetChassis(h) {
1469
+ const v = state.constraints.get(h); return (v && v.isVehicle) ? v.chassisHandle : 0;
1470
+ }
1471
+
1472
+ export function vehicleSetInput(h, forward, right, brake, handbrake) {
1473
+ const v = state.constraints.get(h); if (!v || !v.isVehicle) return;
1474
+ const J = JoltModule;
1475
+ const ctrl = J.castObject ? J.castObject(v.constraint.GetController(), J.WheeledVehicleController)
1476
+ : v.constraint.GetController();
1477
+ ctrl.SetDriverInput(forward, right, brake, handbrake);
1478
+ if (forward !== 0 || brake !== 0 || handbrake !== 0 || right !== 0) {
1479
+ const w = state.worlds.get(v.world);
1480
+ if (w) w.bodyInterface.ActivateBody(v.chassisId);
1481
+ }
1482
+ }
1483
+
1484
+ export function vehicleGetWheelTransform(h, wheelIndex, axis) {
1485
+ const v = state.constraints.get(h); if (!v || !v.isVehicle) return 0;
1486
+ const J = JoltModule;
1487
+ const xform = v.constraint.GetWheelWorldTransform(wheelIndex, J.Vec3.prototype.sAxisY(), J.Vec3.prototype.sAxisX());
1488
+ if (axis < 3) {
1489
+ const p = xform.GetTranslation();
1490
+ if (axis === 0) return p.GetX();
1491
+ if (axis === 1) return p.GetY();
1492
+ return p.GetZ();
1493
+ }
1494
+ if (axis < 7) {
1495
+ const q = xform.GetQuaternion();
1496
+ if (axis === 3) return q.GetX();
1497
+ if (axis === 4) return q.GetY();
1498
+ if (axis === 5) return q.GetZ();
1499
+ return q.GetW();
1500
+ }
1501
+ return 0;
1502
+ }
1503
+
1504
+ export function vehicleGetEngineRpm(h) {
1505
+ const v = state.constraints.get(h); if (!v || !v.isVehicle) return 0;
1506
+ const J = JoltModule;
1507
+ const ctrl = J.castObject ? J.castObject(v.constraint.GetController(), J.WheeledVehicleController)
1508
+ : v.constraint.GetController();
1509
+ return ctrl.GetEngine().GetCurrentRPM();
1510
+ }
1511
+
1512
+ export function vehicleGetWheelAngularVelocity(h, wheelIndex) {
1513
+ const v = state.constraints.get(h); if (!v || !v.isVehicle) return 0;
1514
+ const J = JoltModule;
1515
+ const wheel = v.constraint.GetWheel(wheelIndex);
1516
+ if (!wheel) return 0;
1517
+ const wv = J.castObject ? J.castObject(wheel, J.WheelWV) : wheel;
1518
+ return wv.GetAngularVelocity ? wv.GetAngularVelocity() : 0;
1519
+ }