@forgeax/engine-physics-rapier2d 0.1.2
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.
- package/LICENSE +202 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +763 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rapier-physics-world-2d.d.ts +205 -0
- package/dist/rapier-physics-world-2d.d.ts.map +1 -0
- package/dist/wasm-loader.d.ts +9 -0
- package/dist/wasm-loader.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/index.ts +17 -0
- package/src/rapier-physics-world-2d.ts +1219 -0
- package/src/wasm-loader.ts +45 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import { defineSystem, FixedTime, componentDefinition, FixedUpdate } from '@forgeax/engine-ecs';
|
|
2
|
+
import { vec3, quat, mat4, vec2 } from '@forgeax/engine-math';
|
|
3
|
+
import { RigidBody, CharacterController, Collider, RIGID_BODY_TYPE_STATIC, rigidBodyTypeFromF32, CollidingEntities, PhysicsError, PHYSICS_ERROR_HINTS, colliderShapeFromF32, registerPhysicsComponents, PhysicsSet } from '@forgeax/engine-physics';
|
|
4
|
+
import { PhysicsError as PhysicsError$1 } from '@forgeax/engine-types';
|
|
5
|
+
|
|
6
|
+
// src/rapier-physics-world-2d.ts
|
|
7
|
+
var DEG_TO_RAD = Math.PI / 180;
|
|
8
|
+
function applyKccTuning(ctrl, cc) {
|
|
9
|
+
ctrl.setMaxSlopeClimbAngle(cc.maxSlopeClimbDeg * DEG_TO_RAD);
|
|
10
|
+
ctrl.setMinSlopeSlideAngle(cc.minSlopeSlideDeg * DEG_TO_RAD);
|
|
11
|
+
ctrl.setSlideEnabled(true);
|
|
12
|
+
if (cc.autoStepMaxHeight === 0) {
|
|
13
|
+
ctrl.disableAutostep();
|
|
14
|
+
} else {
|
|
15
|
+
ctrl.enableAutostep(cc.autoStepMaxHeight, cc.autoStepMinWidth, false);
|
|
16
|
+
}
|
|
17
|
+
if (cc.snapToGroundDist === 0) {
|
|
18
|
+
ctrl.disableSnapToGround();
|
|
19
|
+
} else {
|
|
20
|
+
ctrl.enableSnapToGround(cc.snapToGroundDist);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function rapierBodyTypeToString(rapier, bodyType) {
|
|
24
|
+
if (bodyType === rapier.RigidBodyType.Dynamic) return "dynamic";
|
|
25
|
+
if (bodyType === rapier.RigidBodyType.Fixed) return "static";
|
|
26
|
+
return "kinematic";
|
|
27
|
+
}
|
|
28
|
+
var RapierPhysicsWorld2D = class {
|
|
29
|
+
raw;
|
|
30
|
+
rapierModule;
|
|
31
|
+
/** Entity (raw number) -> PhysicsEntityRecord mapping. */
|
|
32
|
+
entityMap = /* @__PURE__ */ new Map();
|
|
33
|
+
/** Pending teleports: entity -> target position and rotation. */
|
|
34
|
+
pendingTeleports = /* @__PURE__ */ new Map();
|
|
35
|
+
eventQueue;
|
|
36
|
+
collisionPairs = /* @__PURE__ */ new Map();
|
|
37
|
+
pendingCollisionEvents = [];
|
|
38
|
+
collisionEventHistory = [];
|
|
39
|
+
currentGravity;
|
|
40
|
+
/**
|
|
41
|
+
* Lazily-built Rapier KinematicCharacterController per character entity
|
|
42
|
+
* (plan-strategy D-1/D-3, 2D variant). `moveAndSlide` creates one on first
|
|
43
|
+
* call; the `Collider.onRemove` hook clears it on despawn. Public so AC-12
|
|
44
|
+
* despawn tests can assert `kccCache.size === 0`.
|
|
45
|
+
*/
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: Rapier KinematicCharacterController from dynamic module
|
|
47
|
+
kccCache = /* @__PURE__ */ new Map();
|
|
48
|
+
kccOffsets = /* @__PURE__ */ new Map();
|
|
49
|
+
/**
|
|
50
|
+
* ECS World + components wired in by `registerPhysicsSystems2D`, so
|
|
51
|
+
* `moveAndSlide` can read CharacterController tuning and write Transform +
|
|
52
|
+
* grounded back. Undefined until systems are registered — the input-validation
|
|
53
|
+
* error paths fire before these are read, so direct `pw.moveAndSlide()` calls
|
|
54
|
+
* in error tests need no World.
|
|
55
|
+
*/
|
|
56
|
+
moveContext;
|
|
57
|
+
constructor(rapier) {
|
|
58
|
+
this.rapierModule = rapier;
|
|
59
|
+
this.raw = new rapier.World({ x: 0, y: -9.81 });
|
|
60
|
+
this.eventQueue = new rapier.EventQueue(true);
|
|
61
|
+
this.currentGravity = { x: 0, y: -9.81 };
|
|
62
|
+
}
|
|
63
|
+
// ─── PhysicsWorld2D interface ──────────────────────────────────────────
|
|
64
|
+
setGravity(gravity) {
|
|
65
|
+
const x = gravity[0] ?? 0;
|
|
66
|
+
const y = gravity[1] ?? 0;
|
|
67
|
+
this.raw.gravity = { x, y };
|
|
68
|
+
this.currentGravity = { x, y };
|
|
69
|
+
}
|
|
70
|
+
getGravity() {
|
|
71
|
+
const { x, y } = this.currentGravity;
|
|
72
|
+
return vec2.create(x, y);
|
|
73
|
+
}
|
|
74
|
+
raycast(origin, direction, maxDist, filterMask) {
|
|
75
|
+
const RAPIER = this.rapierModule;
|
|
76
|
+
const RayCtor = RAPIER.Ray;
|
|
77
|
+
const ray = new RayCtor(
|
|
78
|
+
{ x: origin[0] ?? 0, y: origin[1] ?? 0 },
|
|
79
|
+
{ x: direction[0] ?? 0, y: direction[1] ?? 0 }
|
|
80
|
+
);
|
|
81
|
+
const hit = this.raw.castRayAndGetNormal(
|
|
82
|
+
ray,
|
|
83
|
+
maxDist,
|
|
84
|
+
true,
|
|
85
|
+
void 0,
|
|
86
|
+
filterMask
|
|
87
|
+
);
|
|
88
|
+
if (hit === null) return void 0;
|
|
89
|
+
const point = ray.pointAt(hit.timeOfImpact);
|
|
90
|
+
const colliderParentBody = hit.collider.parent();
|
|
91
|
+
const entity = colliderParentBody !== null ? colliderParentBody.userData : 0;
|
|
92
|
+
return {
|
|
93
|
+
entity,
|
|
94
|
+
point: vec2.create(point.x, point.y),
|
|
95
|
+
normal: vec2.create(hit.normal.x, hit.normal.y),
|
|
96
|
+
timeOfImpact: hit.timeOfImpact
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
teleport(entity, position, rotation) {
|
|
100
|
+
this.pendingTeleports.set(entity, {
|
|
101
|
+
x: position[0] ?? 0,
|
|
102
|
+
y: position[1] ?? 0,
|
|
103
|
+
rotation
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
step(deltaTime) {
|
|
107
|
+
this.raw.step(this.eventQueue);
|
|
108
|
+
this.drainRapierCollisionEvents();
|
|
109
|
+
}
|
|
110
|
+
drainRapierCollisionEvents() {
|
|
111
|
+
this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
|
|
112
|
+
const entityA = this.colliderHandleToEntity(handle1);
|
|
113
|
+
const entityB = this.colliderHandleToEntity(handle2);
|
|
114
|
+
if (entityA === void 0 || entityB === void 0) return;
|
|
115
|
+
const changed = started ? this.addCollisionPair(entityA, entityB) : this.removeCollisionPair(entityA, entityB);
|
|
116
|
+
if (!changed) return;
|
|
117
|
+
this.pushCollisionEvent({
|
|
118
|
+
type: started ? "started" : "stopped",
|
|
119
|
+
entityA,
|
|
120
|
+
entityB
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
colliderHandleToEntity(colliderHandle) {
|
|
125
|
+
const collider = this.raw.getCollider(colliderHandle);
|
|
126
|
+
const body = collider?.parent();
|
|
127
|
+
return body?.userData;
|
|
128
|
+
}
|
|
129
|
+
addCollisionPair(entityA, entityB) {
|
|
130
|
+
let first = this.collisionPairs.get(entityA);
|
|
131
|
+
if (!first) {
|
|
132
|
+
first = /* @__PURE__ */ new Set();
|
|
133
|
+
this.collisionPairs.set(entityA, first);
|
|
134
|
+
}
|
|
135
|
+
if (first.has(entityB)) return false;
|
|
136
|
+
first.add(entityB);
|
|
137
|
+
let second = this.collisionPairs.get(entityB);
|
|
138
|
+
if (!second) {
|
|
139
|
+
second = /* @__PURE__ */ new Set();
|
|
140
|
+
this.collisionPairs.set(entityB, second);
|
|
141
|
+
}
|
|
142
|
+
second.add(entityA);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
removeCollisionPair(entityA, entityB) {
|
|
146
|
+
const first = this.collisionPairs.get(entityA);
|
|
147
|
+
const second = this.collisionPairs.get(entityB);
|
|
148
|
+
const firstChanged = first?.delete(entityB) === true;
|
|
149
|
+
const secondChanged = second?.delete(entityA) === true;
|
|
150
|
+
if (!first) this.collisionPairs.set(entityA, /* @__PURE__ */ new Set());
|
|
151
|
+
if (!second) this.collisionPairs.set(entityB, /* @__PURE__ */ new Set());
|
|
152
|
+
return firstChanged || secondChanged;
|
|
153
|
+
}
|
|
154
|
+
pushCollisionEvent(event) {
|
|
155
|
+
this.pendingCollisionEvents.push(event);
|
|
156
|
+
this.collisionEventHistory.push(event);
|
|
157
|
+
}
|
|
158
|
+
drainCollisionEvents() {
|
|
159
|
+
return this.pendingCollisionEvents.splice(0);
|
|
160
|
+
}
|
|
161
|
+
getCollisionPairs() {
|
|
162
|
+
return new Map([...this.collisionPairs].map(([entity, others]) => [entity, new Set(others)]));
|
|
163
|
+
}
|
|
164
|
+
getCollisionEventHistory() {
|
|
165
|
+
return [...this.collisionEventHistory];
|
|
166
|
+
}
|
|
167
|
+
getPendingTeleports() {
|
|
168
|
+
return [...this.pendingTeleports].map(([entity, target]) => [entity, { ...target }]);
|
|
169
|
+
}
|
|
170
|
+
getKinematicControllerStates() {
|
|
171
|
+
return [...this.kccOffsets].sort(([first], [second]) => first - second).map(([entity, offset]) => ({ entity, offset }));
|
|
172
|
+
}
|
|
173
|
+
dispose() {
|
|
174
|
+
if (typeof this.raw.free === "function") this.raw.free();
|
|
175
|
+
if (typeof this.eventQueue.free === "function") this.eventQueue.free();
|
|
176
|
+
this.kccCache.clear();
|
|
177
|
+
this.kccOffsets.clear();
|
|
178
|
+
}
|
|
179
|
+
writebackCollidingEntities(world, component = CollidingEntities) {
|
|
180
|
+
for (const [entity, others] of this.collisionPairs) {
|
|
181
|
+
const handle = entity;
|
|
182
|
+
if (world.get(handle, component).ok) {
|
|
183
|
+
world.set(handle, component, { entities: [...others] });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
getBodyCount() {
|
|
188
|
+
return this.entityMap.size;
|
|
189
|
+
}
|
|
190
|
+
hasBody(entity) {
|
|
191
|
+
return this.entityMap.has(entity);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Wire the ECS World + Transform / CharacterController components needed by
|
|
195
|
+
* `moveAndSlide` to read tuning and write back pose + grounded. Called once by
|
|
196
|
+
* `registerPhysicsSystems2D` (plan-strategy D-1/D-7).
|
|
197
|
+
*/
|
|
198
|
+
setMoveContext(world, transform, characterController) {
|
|
199
|
+
this.moveContext = { world, transform, characterController };
|
|
200
|
+
}
|
|
201
|
+
moveAndSlide(entity, desiredDelta) {
|
|
202
|
+
return this.computeMove(entity, desiredDelta);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Shared moveAndSlide core (plan-strategy D-1/D-2/D-4/D-6/D-7), 2D variant.
|
|
206
|
+
* Mirrors the 3D computeMove with Vec2 movement (x, y only — no z).
|
|
207
|
+
*
|
|
208
|
+
* The three Fail-Fast entry checks (body / collider / kinematic) throw
|
|
209
|
+
* structured PhysicsError before the World is read, so error-path tests can
|
|
210
|
+
* call this without registered systems.
|
|
211
|
+
*/
|
|
212
|
+
computeMove(entity, desiredDelta) {
|
|
213
|
+
const record = this.entityMap.get(entity);
|
|
214
|
+
if (!record) {
|
|
215
|
+
throw new PhysicsError({
|
|
216
|
+
code: "body-not-found",
|
|
217
|
+
expected: "a registered Rapier body for this entity",
|
|
218
|
+
hint: PHYSICS_ERROR_HINTS["body-not-found"],
|
|
219
|
+
detail: { code: "body-not-found", entity }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const body = this.raw.bodies.get(record.bodyHandle);
|
|
223
|
+
if (!body) {
|
|
224
|
+
throw new PhysicsError({
|
|
225
|
+
code: "body-not-found",
|
|
226
|
+
expected: "a registered Rapier body for this entity",
|
|
227
|
+
hint: PHYSICS_ERROR_HINTS["body-not-found"],
|
|
228
|
+
detail: { code: "body-not-found", entity }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (body.numColliders() === 0) {
|
|
232
|
+
throw new PhysicsError({
|
|
233
|
+
code: "collider-not-found",
|
|
234
|
+
expected: "a Collider attached to this entity body",
|
|
235
|
+
hint: PHYSICS_ERROR_HINTS["collider-not-found"],
|
|
236
|
+
detail: { code: "collider-not-found", entity }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const RAPIER = this.rapierModule;
|
|
240
|
+
if (body.bodyType() !== RAPIER.RigidBodyType.KinematicPositionBased) {
|
|
241
|
+
throw new PhysicsError({
|
|
242
|
+
code: "controller-requires-kinematic",
|
|
243
|
+
expected: "RigidBody.type === 'kinematic'",
|
|
244
|
+
hint: PHYSICS_ERROR_HINTS["controller-requires-kinematic"],
|
|
245
|
+
detail: {
|
|
246
|
+
code: "controller-requires-kinematic",
|
|
247
|
+
entity,
|
|
248
|
+
bodyType: rapierBodyTypeToString(RAPIER, body.bodyType())
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const collider = body.collider(0);
|
|
253
|
+
const cc = this.readCharacterController(entity);
|
|
254
|
+
const ctrl = this.ensureKcc(entity, cc.offset);
|
|
255
|
+
applyKccTuning(ctrl, cc);
|
|
256
|
+
const delta = { x: desiredDelta[0] ?? 0, y: desiredDelta[1] ?? 0 };
|
|
257
|
+
ctrl.computeColliderMovement(
|
|
258
|
+
collider,
|
|
259
|
+
delta,
|
|
260
|
+
void 0,
|
|
261
|
+
void 0,
|
|
262
|
+
// biome-ignore lint/suspicious/noExplicitAny: Rapier Collider in filter predicate
|
|
263
|
+
(other) => other.handle !== collider.handle
|
|
264
|
+
);
|
|
265
|
+
const movement = ctrl.computedMovement();
|
|
266
|
+
const grounded = ctrl.computedGrounded();
|
|
267
|
+
const t = body.translation();
|
|
268
|
+
const next = { x: t.x + movement.x, y: t.y + movement.y };
|
|
269
|
+
body.setNextKinematicTranslation(next);
|
|
270
|
+
body.setTranslation(next, true);
|
|
271
|
+
this.raw.propagateModifiedBodyPositionsToColliders();
|
|
272
|
+
const ctx = this.moveContext;
|
|
273
|
+
if (ctx) {
|
|
274
|
+
ctx.world.set(entity, ctx.transform, {
|
|
275
|
+
pos: [next.x, next.y, readTransformPosZ(ctx.world, entity, ctx.transform)]
|
|
276
|
+
});
|
|
277
|
+
ctx.world.set(entity, ctx.characterController, { grounded });
|
|
278
|
+
}
|
|
279
|
+
return vec2.create(movement.x, movement.y);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Read CharacterController tuning fields for an entity from the ECS World,
|
|
283
|
+
* falling back to schema defaults when the World is not wired (defensive;
|
|
284
|
+
* the kinematic check upstream means a valid character always has the World).
|
|
285
|
+
*/
|
|
286
|
+
readCharacterController(entity) {
|
|
287
|
+
const ctx = this.moveContext;
|
|
288
|
+
if (ctx) {
|
|
289
|
+
const r = ctx.world.get(entity, ctx.characterController);
|
|
290
|
+
if (r.ok) {
|
|
291
|
+
const v = r.value;
|
|
292
|
+
return {
|
|
293
|
+
offset: v.offset,
|
|
294
|
+
maxSlopeClimbDeg: v.maxSlopeClimbDeg,
|
|
295
|
+
minSlopeSlideDeg: v.minSlopeSlideDeg,
|
|
296
|
+
autoStepMaxHeight: v.autoStepMaxHeight,
|
|
297
|
+
autoStepMinWidth: v.autoStepMinWidth,
|
|
298
|
+
snapToGroundDist: v.snapToGroundDist
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return componentDefinition(CharacterController).defaults;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Lazily build a Rapier 2D KinematicCharacterController for `entity` (cached).
|
|
306
|
+
*/
|
|
307
|
+
// biome-ignore lint/suspicious/noExplicitAny: Rapier KinematicCharacterController from dynamic module
|
|
308
|
+
ensureKcc(entity, offset) {
|
|
309
|
+
const cached = this.kccCache.get(entity);
|
|
310
|
+
if (cached) return cached;
|
|
311
|
+
const ctrl = this.raw.createCharacterController(offset);
|
|
312
|
+
this.kccCache.set(entity, ctrl);
|
|
313
|
+
this.kccOffsets.set(entity, offset);
|
|
314
|
+
return ctrl;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Remove an entity's cached KCC and unregister it from the Rapier world
|
|
318
|
+
* (plan-strategy D-3). Idempotent — safe for entities that never moved.
|
|
319
|
+
*/
|
|
320
|
+
removeKccController(entity) {
|
|
321
|
+
const ctrl = this.kccCache.get(entity);
|
|
322
|
+
this.kccOffsets.delete(entity);
|
|
323
|
+
if (!ctrl) return;
|
|
324
|
+
this.raw.removeCharacterController(ctrl);
|
|
325
|
+
this.kccCache.delete(entity);
|
|
326
|
+
}
|
|
327
|
+
// ─── ECS->Rapier bridge (D-2, 2D variant) ────────────────────────────
|
|
328
|
+
/**
|
|
329
|
+
* Ensure a Rapier 2D body and collider exist for an ECS entity (idempotent).
|
|
330
|
+
*
|
|
331
|
+
* 2D variant of the M1 3D ensureBody: Vec2 {x,y} instead of Vec3 {x,y,z},
|
|
332
|
+
* Rapier2D ColliderDesc.{cuboid(hx,hy), ball(radius), capsule(halfHeight,radius)},
|
|
333
|
+
* scalar rotation from transform quat (extracted via atan2 for z-axis angle).
|
|
334
|
+
*
|
|
335
|
+
* Plan-strategy C-3 symmetry with M1, D-2 + D-5 2D adaptations.
|
|
336
|
+
*/
|
|
337
|
+
ensureBody(entity, transform, rigidBody, collider) {
|
|
338
|
+
if (this.entityMap.has(entity)) return;
|
|
339
|
+
const RAPIER = this.rapierModule;
|
|
340
|
+
const rbType = rigidBodyTypeFromF32(rigidBody.type);
|
|
341
|
+
let body;
|
|
342
|
+
switch (rbType) {
|
|
343
|
+
case "dynamic": {
|
|
344
|
+
const desc = RAPIER.RigidBodyDesc.dynamic().setTranslation(transform.position.x, transform.position.y).setRotation(transform.rotation).setLinearDamping(rigidBody.linearDamping).setAngularDamping(rigidBody.angularDamping).setGravityScale(rigidBody.gravityScale);
|
|
345
|
+
if (rigidBody.mass > 0) {
|
|
346
|
+
desc.setAdditionalMass(rigidBody.mass);
|
|
347
|
+
}
|
|
348
|
+
if (rigidBody.ccdEnabled) {
|
|
349
|
+
desc.setCcdEnabled(true);
|
|
350
|
+
}
|
|
351
|
+
body = this.raw.createRigidBody(desc);
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "static": {
|
|
355
|
+
const desc = RAPIER.RigidBodyDesc.fixed().setTranslation(transform.position.x, transform.position.y).setRotation(transform.rotation);
|
|
356
|
+
body = this.raw.createRigidBody(desc);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "kinematic": {
|
|
360
|
+
const desc = RAPIER.RigidBodyDesc.kinematicPositionBased().setTranslation(transform.position.x, transform.position.y).setRotation(transform.rotation);
|
|
361
|
+
if (rigidBody.ccdEnabled) {
|
|
362
|
+
desc.setCcdEnabled(true);
|
|
363
|
+
}
|
|
364
|
+
body = this.raw.createRigidBody(desc);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
body.userData = entity;
|
|
369
|
+
this.registerBody(entity, body.handle);
|
|
370
|
+
const scaleX = Math.abs(transform.scale.x);
|
|
371
|
+
const scaleY = Math.abs(transform.scale.y);
|
|
372
|
+
const cShape = colliderShapeFromF32(collider.shape);
|
|
373
|
+
switch (cShape) {
|
|
374
|
+
case "cuboid": {
|
|
375
|
+
const desc = RAPIER.ColliderDesc.cuboid(
|
|
376
|
+
collider.halfExtents[0] * scaleX,
|
|
377
|
+
collider.halfExtents[1] * scaleY
|
|
378
|
+
).setFriction(collider.friction).setRestitution(collider.restitution).setDensity(collider.density).setCollisionGroups(collider.collisionGroups).setSolverGroups(collider.solverGroups);
|
|
379
|
+
if (collider.isSensor) desc.setSensor(true);
|
|
380
|
+
this.raw.createCollider(desc, body);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "sphere": {
|
|
384
|
+
const desc = RAPIER.ColliderDesc.ball(collider.radius * Math.max(scaleX, scaleY)).setFriction(collider.friction).setRestitution(collider.restitution).setDensity(collider.density).setCollisionGroups(collider.collisionGroups).setSolverGroups(collider.solverGroups);
|
|
385
|
+
if (collider.isSensor) desc.setSensor(true);
|
|
386
|
+
this.raw.createCollider(desc, body);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case "capsule": {
|
|
390
|
+
const desc = RAPIER.ColliderDesc.capsule(
|
|
391
|
+
collider.halfHeight * scaleY,
|
|
392
|
+
collider.radius * scaleX
|
|
393
|
+
).setFriction(collider.friction).setRestitution(collider.restitution).setDensity(collider.density).setCollisionGroups(collider.collisionGroups).setSolverGroups(collider.solverGroups);
|
|
394
|
+
if (collider.isSensor) desc.setSensor(true);
|
|
395
|
+
this.raw.createCollider(desc, body);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/** Synchronize a static or kinematic Rapier body from its resolved 2D Transform pose. */
|
|
401
|
+
syncAuthoredPose(entity, transform, collider, bodyType) {
|
|
402
|
+
const record = this.entityMap.get(entity);
|
|
403
|
+
if (!record) return;
|
|
404
|
+
const body = this.raw.bodies.get(record.bodyHandle);
|
|
405
|
+
if (!body) return;
|
|
406
|
+
if (bodyType === "static") {
|
|
407
|
+
body.setTranslation(transform.position, true);
|
|
408
|
+
body.setRotation(transform.rotation, true);
|
|
409
|
+
} else {
|
|
410
|
+
body.setNextKinematicTranslation(transform.position);
|
|
411
|
+
body.setNextKinematicRotation(transform.rotation);
|
|
412
|
+
}
|
|
413
|
+
const rapierCollider = body.collider(0);
|
|
414
|
+
if (!rapierCollider) return;
|
|
415
|
+
const scaleX = Math.abs(transform.scale.x);
|
|
416
|
+
const scaleY = Math.abs(transform.scale.y);
|
|
417
|
+
switch (colliderShapeFromF32(collider.shape)) {
|
|
418
|
+
case "cuboid":
|
|
419
|
+
rapierCollider.setHalfExtents({
|
|
420
|
+
x: collider.halfExtents[0] * scaleX,
|
|
421
|
+
y: collider.halfExtents[1] * scaleY
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
case "sphere":
|
|
425
|
+
rapierCollider.setRadius(collider.radius * Math.max(scaleX, scaleY));
|
|
426
|
+
break;
|
|
427
|
+
case "capsule":
|
|
428
|
+
rapierCollider.setHalfHeight(collider.halfHeight * scaleY);
|
|
429
|
+
rapierCollider.setRadius(collider.radius * scaleX);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ─── ECS integration helpers ───────────────────────────────────────────
|
|
434
|
+
registerBody(entity, bodyHandle) {
|
|
435
|
+
this.entityMap.set(entity, { bodyHandle });
|
|
436
|
+
}
|
|
437
|
+
applyPendingTeleports() {
|
|
438
|
+
for (const [entity, target] of this.pendingTeleports) {
|
|
439
|
+
const record = this.entityMap.get(entity);
|
|
440
|
+
if (!record) continue;
|
|
441
|
+
const body = this.raw.bodies.get(record.bodyHandle);
|
|
442
|
+
if (!body) continue;
|
|
443
|
+
body.setTranslation({ x: target.x, y: target.y }, true);
|
|
444
|
+
body.setLinvel({ x: 0, y: 0 }, false);
|
|
445
|
+
body.setAngvel(0, false);
|
|
446
|
+
if (target.rotation !== void 0) {
|
|
447
|
+
body.setRotation(target.rotation, true);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
this.pendingTeleports.clear();
|
|
451
|
+
}
|
|
452
|
+
setKinematicPosition(entity, pos, rotation) {
|
|
453
|
+
const record = this.entityMap.get(entity);
|
|
454
|
+
if (!record) return;
|
|
455
|
+
const body = this.raw.bodies.get(record.bodyHandle);
|
|
456
|
+
if (!body) return;
|
|
457
|
+
body.setNextKinematicTranslation({ x: pos.x, y: pos.y });
|
|
458
|
+
if (rotation !== void 0) {
|
|
459
|
+
body.setNextKinematicRotation(rotation);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
writebackDynamicBodies() {
|
|
463
|
+
const results = [];
|
|
464
|
+
for (const [entity, record] of this.entityMap) {
|
|
465
|
+
const body = this.raw.bodies.get(record.bodyHandle);
|
|
466
|
+
if (!body) continue;
|
|
467
|
+
if (body.bodyType() !== this.rapierModule.RigidBodyType.Dynamic) continue;
|
|
468
|
+
const translation = body.translation();
|
|
469
|
+
const rotation = body.rotation();
|
|
470
|
+
results.push({
|
|
471
|
+
entity,
|
|
472
|
+
pos: { x: translation.x, y: translation.y },
|
|
473
|
+
rotation
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return results;
|
|
477
|
+
}
|
|
478
|
+
/** Remove backend rows whose Collider disappeared from the World query. */
|
|
479
|
+
pruneMissingEntities(active) {
|
|
480
|
+
for (const entity of this.entityMap.keys()) {
|
|
481
|
+
if (!active.has(entity)) this.removeEntity(entity);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
removeEntity(entity) {
|
|
485
|
+
const record = this.entityMap.get(entity);
|
|
486
|
+
if (!record) return;
|
|
487
|
+
const ownPairs = [...this.collisionPairs.get(entity) ?? []];
|
|
488
|
+
for (const other of ownPairs) {
|
|
489
|
+
if (this.removeCollisionPair(entity, other)) {
|
|
490
|
+
this.pushCollisionEvent({ type: "stopped", entityA: entity, entityB: other });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
this.removeKccController(entity);
|
|
494
|
+
this.raw.removeRigidBody({
|
|
495
|
+
handle: record.bodyHandle
|
|
496
|
+
});
|
|
497
|
+
this.entityMap.delete(entity);
|
|
498
|
+
this.collisionPairs.delete(entity);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function createRapier2DPhysicsWorld(rapier) {
|
|
502
|
+
return new RapierPhysicsWorld2D(rapier);
|
|
503
|
+
}
|
|
504
|
+
function readTransformPosZ(w, entity, transform) {
|
|
505
|
+
const result = w.get(entity, transform);
|
|
506
|
+
if (!result.ok) return 0;
|
|
507
|
+
const pos = result.value.pos;
|
|
508
|
+
return pos?.[2] ?? 0;
|
|
509
|
+
}
|
|
510
|
+
function hasResolvedWorldPose(world, base) {
|
|
511
|
+
if (!world) return false;
|
|
512
|
+
return world[base] !== 1 || world[base + 5] !== 1 || world[base + 10] !== 1 || world[base + 15] !== 1 || world[base + 1] !== 0 || world[base + 2] !== 0 || world[base + 4] !== 0 || world[base + 6] !== 0 || world[base + 8] !== 0 || world[base + 9] !== 0 || world[base + 12] !== 0 || world[base + 13] !== 0 || world[base + 14] !== 0;
|
|
513
|
+
}
|
|
514
|
+
var PHYSICS_DT_MAX = 0.1;
|
|
515
|
+
var poseScratchPosition2D = vec3.create();
|
|
516
|
+
var poseScratchRotation2D = quat.create();
|
|
517
|
+
var poseScratchScale2D = vec3.create();
|
|
518
|
+
var poseScratchWorld2D = new Float32Array(16);
|
|
519
|
+
var PHYSICS_SYNC_BACKEND_2D = "physicsSyncBackend2D";
|
|
520
|
+
var PHYSICS_STEP_SIMULATION_2D = "physicsStepSimulation2D";
|
|
521
|
+
var PHYSICS_WRITEBACK_2D = "physicsWriteback2D";
|
|
522
|
+
var PHYSICS_COLLISION_SYNC_2D = "physicsCollisionSync2D";
|
|
523
|
+
function resolveTransform(world) {
|
|
524
|
+
return world.components.resolve("Transform");
|
|
525
|
+
}
|
|
526
|
+
var PhysicsSyncBackend2D = defineSystem({
|
|
527
|
+
name: PHYSICS_SYNC_BACKEND_2D,
|
|
528
|
+
queries: [],
|
|
529
|
+
after: ["propagateTransformsFixed"],
|
|
530
|
+
fn: (world) => {
|
|
531
|
+
const transformComponent = resolveTransform(world);
|
|
532
|
+
if (transformComponent === void 0) return;
|
|
533
|
+
let pw;
|
|
534
|
+
try {
|
|
535
|
+
pw = world.getResource("PhysicsWorld");
|
|
536
|
+
} catch {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
pw.applyPendingTeleports();
|
|
540
|
+
const queryResult = world.query({
|
|
541
|
+
read: [Collider, transformComponent],
|
|
542
|
+
optional: [RigidBody, CharacterController]
|
|
543
|
+
});
|
|
544
|
+
if (!queryResult.ok) return;
|
|
545
|
+
const activeEntities = /* @__PURE__ */ new Set();
|
|
546
|
+
for (const queryRow of queryResult.value) {
|
|
547
|
+
const rowView = queryRow;
|
|
548
|
+
const colliderData = rowView.get(Collider);
|
|
549
|
+
const transformData = rowView.get(transformComponent);
|
|
550
|
+
const rigidBodyData = rowView.has(RigidBody) ? rowView.get(RigidBody) : void 0;
|
|
551
|
+
const hasCharacterController = rowView.has(CharacterController);
|
|
552
|
+
const rbType = rigidBodyData === void 0 ? void 0 : new Float32Array([rigidBodyData.type]);
|
|
553
|
+
const rbMass = rigidBodyData === void 0 ? void 0 : new Float32Array([rigidBodyData.mass]);
|
|
554
|
+
const rbLinDamp = rigidBodyData === void 0 ? void 0 : new Float32Array([rigidBodyData.linearDamping]);
|
|
555
|
+
const rbAngDamp = rigidBodyData === void 0 ? void 0 : new Float32Array([rigidBodyData.angularDamping]);
|
|
556
|
+
const rbGravScale = rigidBodyData === void 0 ? void 0 : new Float32Array([rigidBodyData.gravityScale]);
|
|
557
|
+
const rbCcd = rigidBodyData === void 0 ? void 0 : new Uint32Array([rigidBodyData.ccdEnabled]);
|
|
558
|
+
const cShape = new Uint32Array([colliderData.shape]);
|
|
559
|
+
const cHalfExtents = colliderData.halfExtents;
|
|
560
|
+
const cRadius = new Float32Array([colliderData.radius]);
|
|
561
|
+
const cHalfH = new Float32Array([colliderData.halfHeight]);
|
|
562
|
+
const cFric = new Float32Array([colliderData.friction]);
|
|
563
|
+
const cRest = new Float32Array([colliderData.restitution]);
|
|
564
|
+
const cDens = new Float32Array([colliderData.density]);
|
|
565
|
+
const cSensor = new Uint32Array([colliderData.isSensor]);
|
|
566
|
+
const cCGroups = new Uint32Array([colliderData.collisionGroups]);
|
|
567
|
+
const cSGroups = new Uint32Array([colliderData.solverGroups]);
|
|
568
|
+
const tfPos = transformData.pos;
|
|
569
|
+
const tfQuat = transformData.quat;
|
|
570
|
+
const tfScale = transformData.scale;
|
|
571
|
+
const tfWorld = transformData.world;
|
|
572
|
+
if (!cShape || !cHalfExtents || !cRadius || !cHalfH || !cFric || !cRest || !cDens || !cSensor || !cCGroups || !cSGroups || !tfPos || !tfQuat || !tfScale) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
{
|
|
576
|
+
const row = 0;
|
|
577
|
+
const entity = rowView.entity;
|
|
578
|
+
activeEntities.add(entity);
|
|
579
|
+
const localBase = row * 3;
|
|
580
|
+
const quatBase = row * 4;
|
|
581
|
+
const worldBase = row * 16;
|
|
582
|
+
const useWorldPose = hasResolvedWorldPose(tfWorld, worldBase);
|
|
583
|
+
if (useWorldPose && tfWorld) {
|
|
584
|
+
for (let lane = 0; lane < 16; lane++) {
|
|
585
|
+
poseScratchWorld2D[lane] = tfWorld[worldBase + lane] ?? 0;
|
|
586
|
+
}
|
|
587
|
+
mat4.decompose(
|
|
588
|
+
poseScratchPosition2D,
|
|
589
|
+
poseScratchRotation2D,
|
|
590
|
+
poseScratchScale2D,
|
|
591
|
+
poseScratchWorld2D
|
|
592
|
+
);
|
|
593
|
+
} else {
|
|
594
|
+
poseScratchPosition2D[0] = tfPos[localBase] ?? 0;
|
|
595
|
+
poseScratchPosition2D[1] = tfPos[localBase + 1] ?? 0;
|
|
596
|
+
poseScratchPosition2D[2] = tfPos[localBase + 2] ?? 0;
|
|
597
|
+
poseScratchRotation2D[0] = tfQuat[quatBase] ?? 0;
|
|
598
|
+
poseScratchRotation2D[1] = tfQuat[quatBase + 1] ?? 0;
|
|
599
|
+
poseScratchRotation2D[2] = tfQuat[quatBase + 2] ?? 0;
|
|
600
|
+
poseScratchRotation2D[3] = tfQuat[quatBase + 3] ?? 1;
|
|
601
|
+
poseScratchScale2D[0] = tfScale[localBase] ?? 1;
|
|
602
|
+
poseScratchScale2D[1] = tfScale[localBase + 1] ?? 1;
|
|
603
|
+
poseScratchScale2D[2] = tfScale[localBase + 2] ?? 1;
|
|
604
|
+
}
|
|
605
|
+
const transform = {
|
|
606
|
+
position: { x: poseScratchPosition2D[0] ?? 0, y: poseScratchPosition2D[1] ?? 0 },
|
|
607
|
+
rotation: 2 * Math.atan2(poseScratchRotation2D[2] ?? 0, poseScratchRotation2D[3] ?? 1),
|
|
608
|
+
scale: { x: poseScratchScale2D[0] ?? 1, y: poseScratchScale2D[1] ?? 1 }
|
|
609
|
+
};
|
|
610
|
+
const rigidBody = rbType ? {
|
|
611
|
+
type: rbType[row],
|
|
612
|
+
mass: rbMass?.[row] ?? 0,
|
|
613
|
+
linearDamping: rbLinDamp?.[row] ?? 0,
|
|
614
|
+
angularDamping: rbAngDamp?.[row] ?? 0,
|
|
615
|
+
gravityScale: rbGravScale?.[row] ?? 1,
|
|
616
|
+
ccdEnabled: rbCcd?.[row] ?? 0
|
|
617
|
+
} : {
|
|
618
|
+
type: RIGID_BODY_TYPE_STATIC,
|
|
619
|
+
mass: 0,
|
|
620
|
+
linearDamping: 0,
|
|
621
|
+
angularDamping: 0,
|
|
622
|
+
gravityScale: 1,
|
|
623
|
+
ccdEnabled: 0
|
|
624
|
+
};
|
|
625
|
+
const collider = {
|
|
626
|
+
shape: cShape[row],
|
|
627
|
+
halfExtents: [
|
|
628
|
+
cHalfExtents[row * 3],
|
|
629
|
+
cHalfExtents[row * 3 + 1],
|
|
630
|
+
cHalfExtents[row * 3 + 2]
|
|
631
|
+
],
|
|
632
|
+
radius: cRadius[row],
|
|
633
|
+
halfHeight: cHalfH[row],
|
|
634
|
+
friction: cFric[row],
|
|
635
|
+
restitution: cRest[row],
|
|
636
|
+
density: cDens[row],
|
|
637
|
+
isSensor: cSensor[row],
|
|
638
|
+
collisionGroups: cCGroups[row],
|
|
639
|
+
solverGroups: cSGroups[row]
|
|
640
|
+
};
|
|
641
|
+
pw.ensureBody(entity, transform, rigidBody, collider);
|
|
642
|
+
const rbTypeVal = rigidBodyTypeFromF32(rigidBody.type);
|
|
643
|
+
if (rbTypeVal === "static") {
|
|
644
|
+
pw.syncAuthoredPose(entity, transform, collider, "static");
|
|
645
|
+
} else if (rbTypeVal === "kinematic" && !hasCharacterController) {
|
|
646
|
+
pw.syncAuthoredPose(entity, transform, collider, "kinematic");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
pw.pruneMissingEntities(activeEntities);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
var PhysicsStepSimulation2D = defineSystem({
|
|
654
|
+
name: PHYSICS_STEP_SIMULATION_2D,
|
|
655
|
+
queries: [],
|
|
656
|
+
after: [PHYSICS_SYNC_BACKEND_2D],
|
|
657
|
+
fn: (world) => {
|
|
658
|
+
let pw;
|
|
659
|
+
try {
|
|
660
|
+
pw = world.getResource("PhysicsWorld");
|
|
661
|
+
} catch {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const dt = world.getResource(FixedTime).delta;
|
|
665
|
+
if (dt <= 0 || dt > PHYSICS_DT_MAX) return;
|
|
666
|
+
pw.step(dt);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
var PhysicsWriteback2D = defineSystem({
|
|
670
|
+
name: PHYSICS_WRITEBACK_2D,
|
|
671
|
+
queries: [],
|
|
672
|
+
after: [PHYSICS_STEP_SIMULATION_2D],
|
|
673
|
+
fn: (world) => {
|
|
674
|
+
const transformComponent = resolveTransform(world);
|
|
675
|
+
if (transformComponent === void 0) return;
|
|
676
|
+
let pw;
|
|
677
|
+
try {
|
|
678
|
+
pw = world.getResource("PhysicsWorld");
|
|
679
|
+
} catch {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const results = pw.writebackDynamicBodies();
|
|
683
|
+
for (const r of results) {
|
|
684
|
+
const entity = r.entity;
|
|
685
|
+
const outQuat = quat.create();
|
|
686
|
+
quat.fromAxisAngle(outQuat, [0, 0, 1], r.rotation);
|
|
687
|
+
world.set(entity, transformComponent, {
|
|
688
|
+
pos: [r.pos.x, r.pos.y, readTransformPosZ(world, entity, transformComponent)],
|
|
689
|
+
// Component order [x, y, z, w] (E6). `?? 0/1` narrows the
|
|
690
|
+
// noUncheckedIndexedAccess undefined out of the quat elements.
|
|
691
|
+
quat: [outQuat[0] ?? 0, outQuat[1] ?? 0, outQuat[2] ?? 0, outQuat[3] ?? 1]
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
var PhysicsCollisionSync2D = defineSystem({
|
|
697
|
+
name: PHYSICS_COLLISION_SYNC_2D,
|
|
698
|
+
queries: [],
|
|
699
|
+
after: [PHYSICS_WRITEBACK_2D],
|
|
700
|
+
fn: (world) => {
|
|
701
|
+
let pw;
|
|
702
|
+
try {
|
|
703
|
+
pw = world.getResource("PhysicsWorld");
|
|
704
|
+
} catch {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
pw.writebackCollidingEntities(world);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
function registerPhysicsSystems2D(world) {
|
|
711
|
+
const releaseComponents = registerPhysicsComponents(world);
|
|
712
|
+
const transformComponent = resolveTransform(world);
|
|
713
|
+
try {
|
|
714
|
+
const pw = world.getResource("PhysicsWorld");
|
|
715
|
+
if (transformComponent !== void 0) {
|
|
716
|
+
pw.setMoveContext(world, transformComponent, CharacterController);
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
world.addSystems(FixedUpdate, PhysicsSet, [
|
|
721
|
+
PhysicsSyncBackend2D,
|
|
722
|
+
PhysicsStepSimulation2D,
|
|
723
|
+
PhysicsWriteback2D,
|
|
724
|
+
PhysicsCollisionSync2D
|
|
725
|
+
]).unwrap();
|
|
726
|
+
return () => {
|
|
727
|
+
world.removeSystem(FixedUpdate, PHYSICS_COLLISION_SYNC_2D);
|
|
728
|
+
world.removeSystem(FixedUpdate, PHYSICS_WRITEBACK_2D);
|
|
729
|
+
world.removeSystem(FixedUpdate, PHYSICS_STEP_SIMULATION_2D);
|
|
730
|
+
world.removeSystem(FixedUpdate, PHYSICS_SYNC_BACKEND_2D);
|
|
731
|
+
releaseComponents();
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
var rapierInstance = null;
|
|
735
|
+
var loadingPromise = null;
|
|
736
|
+
async function loadRapier2D() {
|
|
737
|
+
if (rapierInstance !== null) return rapierInstance;
|
|
738
|
+
if (loadingPromise !== null) return loadingPromise;
|
|
739
|
+
loadingPromise = _doLoad();
|
|
740
|
+
return loadingPromise;
|
|
741
|
+
}
|
|
742
|
+
async function _doLoad() {
|
|
743
|
+
try {
|
|
744
|
+
const RAPIER = await import('@dimforge/rapier2d-compat');
|
|
745
|
+
await RAPIER.default.init();
|
|
746
|
+
rapierInstance = RAPIER.default;
|
|
747
|
+
loadingPromise = null;
|
|
748
|
+
return RAPIER.default;
|
|
749
|
+
} catch (cause) {
|
|
750
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
751
|
+
loadingPromise = null;
|
|
752
|
+
return new PhysicsError$1({
|
|
753
|
+
code: "wasm-load-failed",
|
|
754
|
+
expected: "successful dynamic import and init of @dimforge/rapier2d-compat",
|
|
755
|
+
hint: `dynamic import or init() failed: ${reason}. Check network, file path, and that @dimforge/rapier2d-compat is installed.`,
|
|
756
|
+
detail: { code: "wasm-load-failed", reason }
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export { PhysicsCollisionSync2D, PhysicsStepSimulation2D, PhysicsSyncBackend2D, PhysicsWriteback2D, RapierPhysicsWorld2D, createRapier2DPhysicsWorld, loadRapier2D, registerPhysicsSystems2D };
|
|
762
|
+
//# sourceMappingURL=index.mjs.map
|
|
763
|
+
//# sourceMappingURL=index.mjs.map
|