@forgeax/engine-physics-rapier3d 0.1.26 → 0.1.28
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/dist/__tests__/derived-physics.unit.test.d.ts +2 -0
- package/dist/__tests__/derived-physics.unit.test.d.ts.map +1 -0
- package/dist/index.mjs +1598 -27
- package/dist/index.mjs.map +1 -1
- package/dist/rapier-physics-world-3d.d.ts +98 -4
- package/dist/rapier-physics-world-3d.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/derived-physics.unit.test.ts +779 -0
- package/src/__tests__/physics-rapier3d.unit.test.ts +66 -1
- package/src/rapier-physics-world-3d.ts +2013 -50
package/dist/index.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { defineSystem, FixedTime, componentDefinition, Disabled, FixedUpdate } f
|
|
|
2
2
|
import { componentId } from '@forgeax/engine-ecs/internal';
|
|
3
3
|
import { readStructuralEvidence } from '@forgeax/engine-ecs/projection';
|
|
4
4
|
import { vec3, quat, mat4 } from '@forgeax/engine-math';
|
|
5
|
-
import { CollidingEntities, PhysicsError, PHYSICS_ERROR_HINTS, CharacterController, rigidBodyTypeFromF32, RigidBody, Collider, colliderShapeFromF32, RIGID_BODY_TYPE_STATIC, registerPhysicsComponents, PhysicsSet } from '@forgeax/engine-physics';
|
|
5
|
+
import { CollidingEntities, DerivedPhysicsError, cloneDerivedPhysicsInput, validateMassProperties, DERIVED_PHYSICS_LIMITS, estimateDerivedPhysicsInputBytes, preserveCenterOfMassVelocity, PhysicsError, PHYSICS_ERROR_HINTS, CharacterController, rigidBodyTypeFromF32, RigidBody, Collider, colliderShapeFromF32, RIGID_BODY_TYPE_STATIC, registerPhysicsComponents, PhysicsSet } from '@forgeax/engine-physics';
|
|
6
6
|
import { ChildOf } from '@forgeax/engine-scene';
|
|
7
|
-
import { PhysicsError as PhysicsError$1 } from '@forgeax/engine-types';
|
|
7
|
+
import { err, ok, PhysicsError as PhysicsError$1 } from '@forgeax/engine-types';
|
|
8
8
|
|
|
9
9
|
// src/rapier-physics-world-3d.ts
|
|
10
10
|
function createPhysicsChangeQueries(world, components) {
|
|
@@ -55,6 +55,41 @@ function rapierBodyTypeToString(rapier, bodyType) {
|
|
|
55
55
|
if (bodyType === rapier.RigidBodyType.Fixed) return "static";
|
|
56
56
|
return "kinematic";
|
|
57
57
|
}
|
|
58
|
+
function validateConstraintInput(input) {
|
|
59
|
+
const finiteVector = (value) => value.length === 3 && value.every(Number.isFinite);
|
|
60
|
+
if (typeof input.id !== "string" || input.id.trim().length === 0 || !Number.isInteger(input.revision) || input.revision < 0 || !Number.isInteger(input.bodyA) || !Number.isInteger(input.bodyB) || input.bodyA === input.bodyB || !finiteVector(input.anchorA) || !finiteVector(input.anchorB)) {
|
|
61
|
+
return err(
|
|
62
|
+
new DerivedPhysicsError(
|
|
63
|
+
"derived-constraint-invalid",
|
|
64
|
+
"constraint identity, revision, endpoint bodies, and anchors are valid",
|
|
65
|
+
"supply two distinct live bodies and finite local anchors",
|
|
66
|
+
{ constraintId: input.id }
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (input.kind === "spring") {
|
|
71
|
+
if (!Number.isFinite(input.restLength) || input.restLength < 0 || !Number.isFinite(input.stiffness) || input.stiffness < 0 || !Number.isFinite(input.damping) || input.damping < 0) {
|
|
72
|
+
return err(
|
|
73
|
+
new DerivedPhysicsError(
|
|
74
|
+
"derived-constraint-invalid",
|
|
75
|
+
"spring rest length, stiffness, and damping are finite and non-negative",
|
|
76
|
+
"repair spring tuning before native creation",
|
|
77
|
+
{ constraintId: input.id }
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} else if (!finiteVector(input.axis) || Math.hypot(input.axis[0], input.axis[1], input.axis[2]) < 1e-6 || input.limits !== void 0 && (!Number.isFinite(input.limits[0]) || !Number.isFinite(input.limits[1]) || input.limits[0] > input.limits[1])) {
|
|
82
|
+
return err(
|
|
83
|
+
new DerivedPhysicsError(
|
|
84
|
+
"derived-constraint-invalid",
|
|
85
|
+
"hinge axis is non-zero and optional limits are ordered finite values",
|
|
86
|
+
"normalize the hinge axis and set minLimit <= maxLimit",
|
|
87
|
+
{ constraintId: input.id }
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return ok(input);
|
|
92
|
+
}
|
|
58
93
|
var RapierPhysicsWorld3D = class {
|
|
59
94
|
/** Rapier 3D World instance owning all bodies, colliders, and pipeline. */
|
|
60
95
|
raw;
|
|
@@ -75,6 +110,26 @@ var RapierPhysicsWorld3D = class {
|
|
|
75
110
|
collisionPairs = /* @__PURE__ */ new Map();
|
|
76
111
|
pendingCollisionEvents = [];
|
|
77
112
|
collisionEventHistory = [];
|
|
113
|
+
/** One backend owns every derived shape; these maps are not a second world. */
|
|
114
|
+
derivedBodies = /* @__PURE__ */ new Map();
|
|
115
|
+
derivedCandidates = /* @__PURE__ */ new Map();
|
|
116
|
+
pendingDerivedCandidates = /* @__PURE__ */ new Set();
|
|
117
|
+
retiredDerivedBodies = [];
|
|
118
|
+
derivedColliderToShape = /* @__PURE__ */ new Map();
|
|
119
|
+
derivedConstraints = /* @__PURE__ */ new Map();
|
|
120
|
+
derivedBodySources = /* @__PURE__ */ new Map();
|
|
121
|
+
derivedPublications = /* @__PURE__ */ new Map();
|
|
122
|
+
derivedFailures = /* @__PURE__ */ new Map();
|
|
123
|
+
derivedContacts = [];
|
|
124
|
+
derivedPoisonedEntities = /* @__PURE__ */ new Set();
|
|
125
|
+
physicsOwner = {};
|
|
126
|
+
candidateSequence = 0;
|
|
127
|
+
derivedCandidateBytes = 0;
|
|
128
|
+
backendGeneration = 1;
|
|
129
|
+
fixedStep = 0;
|
|
130
|
+
derivedPublicationPending = false;
|
|
131
|
+
worldIdentity;
|
|
132
|
+
activeDerivedAdmission;
|
|
78
133
|
currentGravity;
|
|
79
134
|
/**
|
|
80
135
|
* Lazily-built Rapier KinematicCharacterController per character entity
|
|
@@ -117,6 +172,14 @@ var RapierPhysicsWorld3D = class {
|
|
|
117
172
|
}
|
|
118
173
|
raycast(origin, direction, maxDist, filterMask) {
|
|
119
174
|
this.assertActive("raycast");
|
|
175
|
+
if (this.recoveryBlocked()) {
|
|
176
|
+
throw new DerivedPhysicsError(
|
|
177
|
+
"derived-recovery-invalid",
|
|
178
|
+
"raycasts observe a complete healthy physics state",
|
|
179
|
+
"rebuild the PhysicsWorld before querying after an unrecoverable admission",
|
|
180
|
+
{}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
120
183
|
const RAPIER = this.rapierModule;
|
|
121
184
|
const RayCtor = RAPIER.Ray;
|
|
122
185
|
const ray = new RayCtor(
|
|
@@ -151,8 +214,37 @@ var RapierPhysicsWorld3D = class {
|
|
|
151
214
|
}
|
|
152
215
|
step(deltaTime) {
|
|
153
216
|
this.assertActive("step");
|
|
154
|
-
|
|
155
|
-
|
|
217
|
+
try {
|
|
218
|
+
this.processDerivedCandidates();
|
|
219
|
+
if (this.derivedPoisonedEntities.size > 0) return;
|
|
220
|
+
this.raw.step(this.eventQueue);
|
|
221
|
+
this.fixedStep = this.syncState?.world.getResource(FixedTime).tick ?? this.fixedStep + 1;
|
|
222
|
+
this.drainRapierCollisionEvents();
|
|
223
|
+
this.derivedPublicationPending = true;
|
|
224
|
+
if (this.syncState === void 0) this.finalizeDerivedFixedStep();
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
const error = new DerivedPhysicsError(
|
|
227
|
+
"derived-backend-failed",
|
|
228
|
+
"native fixed-step execution and publication complete together",
|
|
229
|
+
"rebuild the World and PhysicsWorld from the last committed snapshot",
|
|
230
|
+
{ reason: cause instanceof Error ? cause.message : String(cause) }
|
|
231
|
+
);
|
|
232
|
+
for (const entity of this.entityMap.keys()) this.derivedPoisonedEntities.add(entity);
|
|
233
|
+
this.derivedPublicationPending = false;
|
|
234
|
+
this.derivedPublications.clear();
|
|
235
|
+
for (const record of this.derivedCandidates.values()) {
|
|
236
|
+
this.rememberDerivedFailure(record, error, "rebuild-required");
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Publish only after the fixed-step ECS writeback/contact boundary. */
|
|
242
|
+
finalizeDerivedFixedStep() {
|
|
243
|
+
if (!this.derivedPublicationPending) return;
|
|
244
|
+
this.derivedPublicationPending = false;
|
|
245
|
+
if (this.derivedPoisonedEntities.size > 0) return;
|
|
246
|
+
this.publishDerivedCandidates();
|
|
247
|
+
this.retireDerivedBodies();
|
|
156
248
|
}
|
|
157
249
|
/**
|
|
158
250
|
* Drain the Rapier event queue into `collisionPairs`. Each event names two
|
|
@@ -166,15 +258,67 @@ var RapierPhysicsWorld3D = class {
|
|
|
166
258
|
const a = this.colliderHandleToEntity(handle1);
|
|
167
259
|
const b = this.colliderHandleToEntity(handle2);
|
|
168
260
|
if (a === void 0 || b === void 0) return;
|
|
261
|
+
const shapeA = this.derivedColliderToShape.get(handle1);
|
|
262
|
+
const shapeB = this.derivedColliderToShape.get(handle2);
|
|
263
|
+
this.recordContactObservation(
|
|
264
|
+
{
|
|
265
|
+
phase: started ? "started" : "stopped",
|
|
266
|
+
fixedStep: this.fixedStep,
|
|
267
|
+
entityA: a,
|
|
268
|
+
entityB: b,
|
|
269
|
+
...shapeA === void 0 ? {} : { shapeA: shapeA.id },
|
|
270
|
+
...shapeB === void 0 ? {} : { shapeB: shapeB.id }
|
|
271
|
+
},
|
|
272
|
+
handle1,
|
|
273
|
+
handle2
|
|
274
|
+
);
|
|
169
275
|
const changed = started ? this.addPair(a, b) : this.removePair(a, b);
|
|
170
276
|
if (!changed) return;
|
|
171
277
|
this.pushCollisionEvent({
|
|
172
278
|
type: started ? "started" : "stopped",
|
|
173
279
|
entityA: a,
|
|
174
|
-
entityB: b
|
|
280
|
+
entityB: b,
|
|
281
|
+
fixedStep: this.fixedStep,
|
|
282
|
+
...shapeA === void 0 ? {} : { shapeA: shapeA.id },
|
|
283
|
+
...shapeB === void 0 ? {} : { shapeB: shapeB.id }
|
|
175
284
|
});
|
|
176
285
|
});
|
|
177
286
|
}
|
|
287
|
+
recordContactObservation(observation, handleA, handleB) {
|
|
288
|
+
let point;
|
|
289
|
+
let normal;
|
|
290
|
+
try {
|
|
291
|
+
const colliderA = this.raw.getCollider(handleA);
|
|
292
|
+
const colliderB = this.raw.getCollider(handleB);
|
|
293
|
+
if (colliderA !== null && colliderB !== null) {
|
|
294
|
+
this.raw.contactPair(
|
|
295
|
+
colliderA,
|
|
296
|
+
colliderB,
|
|
297
|
+
(manifold, flipped) => {
|
|
298
|
+
if (manifold.numSolverContacts?.() > 0) {
|
|
299
|
+
const contact = manifold.solverContactPoint(0);
|
|
300
|
+
const n = manifold.normal();
|
|
301
|
+
if (contact !== null && contact !== void 0) {
|
|
302
|
+
point = [contact.x, contact.y, contact.z];
|
|
303
|
+
}
|
|
304
|
+
if (n !== null && n !== void 0) {
|
|
305
|
+
const direction = flipped ? -1 : 1;
|
|
306
|
+
normal = [n.x * direction, n.y * direction, n.z * direction];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
this.derivedContacts.push({
|
|
315
|
+
...observation,
|
|
316
|
+
...point === void 0 ? {} : { point },
|
|
317
|
+
...normal === void 0 ? {} : { normal }
|
|
318
|
+
});
|
|
319
|
+
if (this.derivedContacts.length > 256)
|
|
320
|
+
this.derivedContacts.splice(0, this.derivedContacts.length - 256);
|
|
321
|
+
}
|
|
178
322
|
/** Resolve a Rapier collider handle to its owning ECS entity, or undefined. */
|
|
179
323
|
colliderHandleToEntity(colliderHandle) {
|
|
180
324
|
const collider = this.raw.getCollider(colliderHandle);
|
|
@@ -205,7 +349,14 @@ var RapierPhysicsWorld3D = class {
|
|
|
205
349
|
return removedA || removedB;
|
|
206
350
|
}
|
|
207
351
|
pushCollisionEvent(event) {
|
|
208
|
-
const ordered = event.entityA <= event.entityB ? event : {
|
|
352
|
+
const ordered = event.entityA <= event.entityB ? event : {
|
|
353
|
+
type: event.type,
|
|
354
|
+
entityA: event.entityB,
|
|
355
|
+
entityB: event.entityA,
|
|
356
|
+
...event.fixedStep === void 0 ? {} : { fixedStep: event.fixedStep },
|
|
357
|
+
...event.shapeB === void 0 ? {} : { shapeA: event.shapeB },
|
|
358
|
+
...event.shapeA === void 0 ? {} : { shapeB: event.shapeA }
|
|
359
|
+
};
|
|
209
360
|
this.pendingCollisionEvents.push(ordered);
|
|
210
361
|
this.collisionEventHistory.push(ordered);
|
|
211
362
|
}
|
|
@@ -232,6 +383,1276 @@ var RapierPhysicsWorld3D = class {
|
|
|
232
383
|
getCollisionEventHistory() {
|
|
233
384
|
return [...this.collisionEventHistory];
|
|
234
385
|
}
|
|
386
|
+
/** Detached fixed-step contact facts; no Rapier manifolds or handles escape. */
|
|
387
|
+
getContactObservations() {
|
|
388
|
+
if (this.recoveryBlocked()) return [];
|
|
389
|
+
return this.derivedContacts.map((contact) => ({
|
|
390
|
+
...contact,
|
|
391
|
+
...contact.point === void 0 ? {} : { point: [...contact.point] },
|
|
392
|
+
...contact.normal === void 0 ? {} : { normal: [...contact.normal] }
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
/** Prepare disabled native Voxels for one entity without changing queries. */
|
|
396
|
+
prepareDerivedShapeCandidate(input) {
|
|
397
|
+
this.assertActive("prepareDerivedShapeCandidate");
|
|
398
|
+
if (input.worldIdentity !== void 0 && input.worldIdentity !== this.worldIdentity) {
|
|
399
|
+
return err(
|
|
400
|
+
new DerivedPhysicsError(
|
|
401
|
+
"derived-world-mismatch",
|
|
402
|
+
"candidate belongs to the ECS World bound to this PhysicsWorld",
|
|
403
|
+
"submit the candidate to the PhysicsWorld that owns its entity",
|
|
404
|
+
{ entity: input.entity }
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!this.entityMap.has(input.entity)) {
|
|
409
|
+
return err(
|
|
410
|
+
new DerivedPhysicsError(
|
|
411
|
+
"derived-body-not-found",
|
|
412
|
+
"candidate entity has a committed body in this PhysicsWorld",
|
|
413
|
+
"wait for physics reconciliation before preparing derived shapes",
|
|
414
|
+
{ entity: input.entity }
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (this.derivedPoisonedEntities.has(input.entity)) {
|
|
419
|
+
return err(
|
|
420
|
+
new DerivedPhysicsError(
|
|
421
|
+
"derived-recovery-invalid",
|
|
422
|
+
"the entity has a recoverable committed native PhysicsWorld state",
|
|
423
|
+
"rebuild the PhysicsWorld from the last portable snapshot before retrying",
|
|
424
|
+
{ entity: input.entity }
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const active = this.derivedBodies.get(input.entity);
|
|
429
|
+
if (active !== void 0 && input.revision <= active.revision) {
|
|
430
|
+
return err(
|
|
431
|
+
new DerivedPhysicsError(
|
|
432
|
+
"derived-candidate-stale",
|
|
433
|
+
"candidate revision is newer than the committed derived shape revision",
|
|
434
|
+
"read the latest publication and advance the consumer revision",
|
|
435
|
+
{ entity: input.entity, actual: input.revision, expected: `>${active.revision}` }
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
for (const pendingId of this.pendingDerivedCandidates) {
|
|
440
|
+
const pending = this.derivedCandidates.get(pendingId);
|
|
441
|
+
if (pending?.input.entity === input.entity) {
|
|
442
|
+
return err(
|
|
443
|
+
new DerivedPhysicsError(
|
|
444
|
+
"derived-candidate-pending",
|
|
445
|
+
"one body has at most one queued derived-shape candidate",
|
|
446
|
+
"cancel or let the current candidate publish before preparing another",
|
|
447
|
+
{ entity: input.entity, candidateId: pendingId }
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const copied = cloneDerivedPhysicsInput(input);
|
|
453
|
+
if (!copied.ok) return copied;
|
|
454
|
+
const mass = validateMassProperties(copied.value.massProperties);
|
|
455
|
+
if (!mass.ok) return mass;
|
|
456
|
+
const body = this.bodyForEntity(input.entity);
|
|
457
|
+
if (body === void 0) {
|
|
458
|
+
return err(
|
|
459
|
+
new DerivedPhysicsError(
|
|
460
|
+
"derived-body-not-found",
|
|
461
|
+
"candidate entity resolves to a live native body",
|
|
462
|
+
"wait for the next ECS physics sync",
|
|
463
|
+
{ entity: input.entity }
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (this.derivedCandidates.size >= DERIVED_PHYSICS_LIMITS.maxCandidates) {
|
|
468
|
+
return err(
|
|
469
|
+
new DerivedPhysicsError(
|
|
470
|
+
"derived-candidate-budget-exceeded",
|
|
471
|
+
`this PhysicsWorld keeps at most ${DERIVED_PHYSICS_LIMITS.maxCandidates} candidates`,
|
|
472
|
+
"cancel or publish an existing candidate before preparing another",
|
|
473
|
+
{ entity: input.entity, actual: this.derivedCandidates.size }
|
|
474
|
+
)
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const candidateInput = Object.freeze({
|
|
478
|
+
...copied.value,
|
|
479
|
+
...mass.value === void 0 ? {} : { massProperties: mass.value }
|
|
480
|
+
});
|
|
481
|
+
const privateInput = cloneDerivedPhysicsInput(candidateInput);
|
|
482
|
+
if (!privateInput.ok) return privateInput;
|
|
483
|
+
const publicInput = cloneDerivedPhysicsInput(privateInput.value);
|
|
484
|
+
if (!publicInput.ok) return publicInput;
|
|
485
|
+
const nativeColliders = [];
|
|
486
|
+
try {
|
|
487
|
+
for (const shape of privateInput.value.shapes) {
|
|
488
|
+
const collider = this.createDerivedCollider(body, shape);
|
|
489
|
+
nativeColliders.push({ handle: collider.handle });
|
|
490
|
+
}
|
|
491
|
+
} catch (cause) {
|
|
492
|
+
for (const collider of nativeColliders) this.removeNativeCollider(collider.handle);
|
|
493
|
+
return err(
|
|
494
|
+
new DerivedPhysicsError(
|
|
495
|
+
"derived-backend-failed",
|
|
496
|
+
"Rapier can create every candidate voxel collider while it remains disabled",
|
|
497
|
+
"reduce the candidate or rebuild the PhysicsWorld after a native failure",
|
|
498
|
+
{ entity: input.entity, reason: cause instanceof Error ? cause.message : String(cause) }
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const candidateBytes = estimateDerivedPhysicsInputBytes(privateInput.value);
|
|
503
|
+
if (this.derivedCandidateBytes + candidateBytes > DERIVED_PHYSICS_LIMITS.maxCandidateBytes) {
|
|
504
|
+
for (const collider of nativeColliders) this.removeNativeCollider(collider.handle);
|
|
505
|
+
return err(
|
|
506
|
+
new DerivedPhysicsError(
|
|
507
|
+
"derived-candidate-budget-exceeded",
|
|
508
|
+
`staged candidate bytes remain within ${DERIVED_PHYSICS_LIMITS.maxCandidateBytes}`,
|
|
509
|
+
"cancel or retire an in-flight candidate before retrying",
|
|
510
|
+
{ entity: input.entity, actual: this.derivedCandidateBytes + candidateBytes }
|
|
511
|
+
)
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const candidateId = `derived:${this.backendGeneration}:${input.entity}:${++this.candidateSequence}:${input.revision}`;
|
|
515
|
+
const token = Object.freeze({
|
|
516
|
+
candidateId,
|
|
517
|
+
generation: this.backendGeneration,
|
|
518
|
+
owner: this.physicsOwner,
|
|
519
|
+
input: publicInput.value,
|
|
520
|
+
state: "ready"
|
|
521
|
+
});
|
|
522
|
+
this.derivedCandidates.set(candidateId, {
|
|
523
|
+
token,
|
|
524
|
+
nativeColliders,
|
|
525
|
+
input: privateInput.value,
|
|
526
|
+
bytes: candidateBytes,
|
|
527
|
+
state: "ready"
|
|
528
|
+
});
|
|
529
|
+
this.derivedCandidateBytes += candidateBytes;
|
|
530
|
+
return ok(token);
|
|
531
|
+
}
|
|
532
|
+
/** Queue a prepared candidate for the next call to `step()`. */
|
|
533
|
+
admitDerivedShapeCandidate(candidate, commitGeometry) {
|
|
534
|
+
const admitted = this.admitDerivedShapeCandidateInternal(candidate);
|
|
535
|
+
if (admitted.ok && commitGeometry !== void 0) {
|
|
536
|
+
const record = this.derivedCandidates.get(candidate.candidateId);
|
|
537
|
+
if (record !== void 0) record.commitGeometry = commitGeometry;
|
|
538
|
+
}
|
|
539
|
+
return admitted;
|
|
540
|
+
}
|
|
541
|
+
getDerivedAdmission(entity) {
|
|
542
|
+
const record = this.activeDerivedAdmission;
|
|
543
|
+
if (record === void 0 || entity !== void 0 && record.input.entity !== entity)
|
|
544
|
+
return void 0;
|
|
545
|
+
return {
|
|
546
|
+
entity: record.input.entity,
|
|
547
|
+
revision: record.input.revision,
|
|
548
|
+
fixedStep: this.syncState?.world.getResource(FixedTime).tick ?? this.fixedStep + 1
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
admitDerivedShapeCandidateInternal(candidate, sourceOverrides) {
|
|
552
|
+
this.assertActive("admitDerivedShapeCandidate");
|
|
553
|
+
const record = this.derivedCandidates.get(candidate.candidateId);
|
|
554
|
+
if (record === void 0 || candidate.owner !== this.physicsOwner || record.token.owner !== candidate.owner || candidate.generation !== this.backendGeneration) {
|
|
555
|
+
return err(
|
|
556
|
+
new DerivedPhysicsError(
|
|
557
|
+
"derived-candidate-not-found",
|
|
558
|
+
"candidate belongs to the current PhysicsWorld generation",
|
|
559
|
+
"discard stale candidate credentials and prepare from committed input again",
|
|
560
|
+
{ candidateId: candidate.candidateId }
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
if (this.derivedPoisonedEntities.has(record?.input.entity ?? -1)) {
|
|
565
|
+
return err(
|
|
566
|
+
new DerivedPhysicsError(
|
|
567
|
+
"derived-recovery-invalid",
|
|
568
|
+
"the candidate entity is stopped after an unrecoverable native admission failure",
|
|
569
|
+
"rebuild the PhysicsWorld from its portable snapshot before retrying",
|
|
570
|
+
{ entity: record?.input.entity, candidateId: candidate.candidateId }
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (record.state === "cancelled" || record.state === "invalidated") {
|
|
575
|
+
return err(
|
|
576
|
+
new DerivedPhysicsError(
|
|
577
|
+
"derived-candidate-cancelled",
|
|
578
|
+
"candidate has not been cancelled or invalidated",
|
|
579
|
+
"prepare a new candidate from the latest committed revision",
|
|
580
|
+
{ candidateId: candidate.candidateId }
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (record.state !== "ready") {
|
|
585
|
+
return err(
|
|
586
|
+
new DerivedPhysicsError(
|
|
587
|
+
"derived-candidate-pending",
|
|
588
|
+
"a prepared candidate is admitted at most once",
|
|
589
|
+
"retain the returned queued receipt and wait for fixed-step publication",
|
|
590
|
+
{ candidateId: candidate.candidateId }
|
|
591
|
+
)
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const pendingSources = this.pendingDerivedSources();
|
|
595
|
+
if (sourceOverrides !== void 0) {
|
|
596
|
+
for (const [entity, source] of sourceOverrides) pendingSources.set(entity, source);
|
|
597
|
+
}
|
|
598
|
+
pendingSources.set(record.input.entity, {
|
|
599
|
+
sourceKey: record.input.sourceKey,
|
|
600
|
+
revision: record.input.revision
|
|
601
|
+
});
|
|
602
|
+
const admissionError = this.validateDerivedAdmission(record.input, pendingSources);
|
|
603
|
+
if (admissionError !== void 0) {
|
|
604
|
+
this.rejectPreparedCandidate(record, admissionError);
|
|
605
|
+
return err(admissionError);
|
|
606
|
+
}
|
|
607
|
+
const active = this.derivedBodies.get(record.input.entity);
|
|
608
|
+
if (active !== void 0 && record.input.revision <= active.revision) {
|
|
609
|
+
const stale = new DerivedPhysicsError(
|
|
610
|
+
"derived-candidate-stale",
|
|
611
|
+
"candidate revision is newer than the committed shape revision",
|
|
612
|
+
"advance the consumer revision before admission",
|
|
613
|
+
{ entity: record.input.entity, candidateId: candidate.candidateId }
|
|
614
|
+
);
|
|
615
|
+
this.rejectPreparedCandidate(record, stale);
|
|
616
|
+
return err(stale);
|
|
617
|
+
}
|
|
618
|
+
const newestPendingRevision = this.newestPendingRevision(record.input.entity);
|
|
619
|
+
if (newestPendingRevision !== void 0 && record.input.revision <= newestPendingRevision) {
|
|
620
|
+
const stale = new DerivedPhysicsError(
|
|
621
|
+
"derived-candidate-stale",
|
|
622
|
+
"candidate revision advances every already queued revision for the body",
|
|
623
|
+
"admit only the newest body revision at a fixed-step boundary",
|
|
624
|
+
{
|
|
625
|
+
entity: record.input.entity,
|
|
626
|
+
candidateId: candidate.candidateId,
|
|
627
|
+
expected: `>${newestPendingRevision}`,
|
|
628
|
+
actual: record.input.revision
|
|
629
|
+
}
|
|
630
|
+
);
|
|
631
|
+
this.rejectPreparedCandidate(record, stale);
|
|
632
|
+
return err(stale);
|
|
633
|
+
}
|
|
634
|
+
record.state = "queued";
|
|
635
|
+
this.pendingDerivedCandidates.add(candidate.candidateId);
|
|
636
|
+
const queued = Object.freeze({ ...record.token, state: "queued" });
|
|
637
|
+
record.token = queued;
|
|
638
|
+
let changed = true;
|
|
639
|
+
while (changed) {
|
|
640
|
+
changed = false;
|
|
641
|
+
const projectedSources = new Map(sourceOverrides ?? []);
|
|
642
|
+
for (const [entity, source] of this.pendingDerivedSources()) {
|
|
643
|
+
const current = projectedSources.get(entity);
|
|
644
|
+
if (current === void 0 || source.revision > current.revision)
|
|
645
|
+
projectedSources.set(entity, source);
|
|
646
|
+
}
|
|
647
|
+
for (const queuedId of [...this.pendingDerivedCandidates]) {
|
|
648
|
+
const queuedRecord = this.derivedCandidates.get(queuedId);
|
|
649
|
+
if (queuedRecord?.state !== "queued") continue;
|
|
650
|
+
const projected = projectedSources.get(queuedRecord.input.entity);
|
|
651
|
+
const staleRevision = projected !== void 0 && queuedRecord.input.revision < projected.revision;
|
|
652
|
+
const dependencyError = staleRevision ? new DerivedPhysicsError(
|
|
653
|
+
"derived-candidate-stale",
|
|
654
|
+
"queued candidates publish only the final revision submitted for an entity",
|
|
655
|
+
"discard the older queued candidate and submit one complete revision",
|
|
656
|
+
{
|
|
657
|
+
entity: queuedRecord.input.entity,
|
|
658
|
+
candidateId: queuedRecord.token.candidateId,
|
|
659
|
+
expected: `>=${projected.revision}`,
|
|
660
|
+
actual: queuedRecord.input.revision
|
|
661
|
+
}
|
|
662
|
+
) : this.validateDerivedAdmission(queuedRecord.input, projectedSources);
|
|
663
|
+
if (dependencyError === void 0) continue;
|
|
664
|
+
this.rejectPreparedCandidate(queuedRecord, dependencyError);
|
|
665
|
+
changed = true;
|
|
666
|
+
if (queuedId === candidate.candidateId) return err(dependencyError);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return ok(queued);
|
|
670
|
+
}
|
|
671
|
+
/** Cancel candidate-native resources; the committed state remains untouched. */
|
|
672
|
+
cancelDerivedShapeCandidate(candidate) {
|
|
673
|
+
this.assertActive("cancelDerivedShapeCandidate");
|
|
674
|
+
const record = this.derivedCandidates.get(candidate.candidateId);
|
|
675
|
+
if (record === void 0 || candidate.owner !== this.physicsOwner) {
|
|
676
|
+
return err(
|
|
677
|
+
new DerivedPhysicsError(
|
|
678
|
+
"derived-candidate-not-found",
|
|
679
|
+
"candidate belongs to the current PhysicsWorld",
|
|
680
|
+
"ignore already-retired credentials and prepare again when needed",
|
|
681
|
+
{ candidateId: candidate.candidateId }
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if (record.state === "published") {
|
|
686
|
+
return err(
|
|
687
|
+
new DerivedPhysicsError(
|
|
688
|
+
"derived-candidate-cancelled",
|
|
689
|
+
"a published candidate remains the committed result until replaced",
|
|
690
|
+
"submit a newer candidate instead of cancelling committed state",
|
|
691
|
+
{ candidateId: candidate.candidateId }
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
this.pendingDerivedCandidates.delete(candidate.candidateId);
|
|
696
|
+
for (const collider of record.nativeColliders) this.removeNativeCollider(collider.handle);
|
|
697
|
+
record.state = "cancelled";
|
|
698
|
+
this.releaseDerivedCandidate(candidate.candidateId);
|
|
699
|
+
return ok(void 0);
|
|
700
|
+
}
|
|
701
|
+
/** Reject all in-flight derived work while preserving the last publication. */
|
|
702
|
+
invalidateDerivedShapeCandidates(reason = "consumer-invalidated") {
|
|
703
|
+
const committedCandidateIds = new Set(
|
|
704
|
+
[...this.derivedBodies.values()].map((body) => body.candidateId)
|
|
705
|
+
);
|
|
706
|
+
for (const [id, record] of this.derivedCandidates) {
|
|
707
|
+
if (record.state === "published" || committedCandidateIds.has(id)) continue;
|
|
708
|
+
record.state = "invalidated";
|
|
709
|
+
for (const collider of record.nativeColliders) this.removeNativeCollider(collider.handle);
|
|
710
|
+
this.releaseDerivedCandidate(id);
|
|
711
|
+
}
|
|
712
|
+
this.pendingDerivedCandidates.clear();
|
|
713
|
+
}
|
|
714
|
+
getDerivedPublication(entity) {
|
|
715
|
+
this.assertActive("getDerivedPublication");
|
|
716
|
+
if (this.recoveryBlocked()) return void 0;
|
|
717
|
+
const publication = this.derivedPublications.get(entity);
|
|
718
|
+
return publication === void 0 ? void 0 : { ...publication, shapeIds: [...publication.shapeIds] };
|
|
719
|
+
}
|
|
720
|
+
getDerivedFailure(entity) {
|
|
721
|
+
const failure = this.derivedFailures.get(entity);
|
|
722
|
+
return failure === void 0 ? void 0 : { ...failure };
|
|
723
|
+
}
|
|
724
|
+
getDerivedBodyType(entity) {
|
|
725
|
+
this.assertActive("getDerivedBodyType");
|
|
726
|
+
if (this.recoveryBlocked()) return void 0;
|
|
727
|
+
const body = this.bodyForEntity(entity);
|
|
728
|
+
if (body === void 0) return void 0;
|
|
729
|
+
return rapierBodyTypeToString(this.rapierModule, body.bodyType());
|
|
730
|
+
}
|
|
731
|
+
getDerivedBodyMass(entity) {
|
|
732
|
+
this.assertActive("getDerivedBodyMass");
|
|
733
|
+
if (this.recoveryBlocked()) return void 0;
|
|
734
|
+
const body = this.bodyForEntity(entity);
|
|
735
|
+
return body === void 0 ? void 0 : body.mass();
|
|
736
|
+
}
|
|
737
|
+
getDerivedMotion(entity) {
|
|
738
|
+
this.assertActive("getDerivedMotion");
|
|
739
|
+
if (this.recoveryBlocked() || !this.derivedBodies.has(entity)) return void 0;
|
|
740
|
+
const body = this.bodyForEntity(entity);
|
|
741
|
+
if (body === void 0) return void 0;
|
|
742
|
+
const com = body.worldCom();
|
|
743
|
+
const linear = body.linvel();
|
|
744
|
+
const angular = body.angvel();
|
|
745
|
+
return Object.freeze({
|
|
746
|
+
centerOfMass: [com.x, com.y, com.z],
|
|
747
|
+
linearVelocity: [linear.x, linear.y, linear.z],
|
|
748
|
+
angularVelocity: [angular.x, angular.y, angular.z]
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
getDerivedRecoveryState() {
|
|
752
|
+
return this.recoveryBlocked() ? "rebuild-required" : "ready";
|
|
753
|
+
}
|
|
754
|
+
getDerivedShapes(entity) {
|
|
755
|
+
this.assertActive("getDerivedShapes");
|
|
756
|
+
if (this.recoveryBlocked()) return [];
|
|
757
|
+
const body = this.derivedBodies.get(entity);
|
|
758
|
+
if (body === void 0) return [];
|
|
759
|
+
return body.shapes.map((shape) => ({
|
|
760
|
+
id: shape.input.id,
|
|
761
|
+
revision: shape.input.revision,
|
|
762
|
+
entity,
|
|
763
|
+
voxelSize: [...shape.input.voxelSize],
|
|
764
|
+
origin: [...shape.input.origin],
|
|
765
|
+
rotation: [...shape.input.rotation],
|
|
766
|
+
generation: body.generation
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
captureDerivedPhysicsState() {
|
|
770
|
+
this.assertActive("captureDerivedPhysicsState");
|
|
771
|
+
return Object.freeze({
|
|
772
|
+
generation: this.backendGeneration,
|
|
773
|
+
fixedStep: this.fixedStep,
|
|
774
|
+
bodies: Object.freeze(
|
|
775
|
+
[...this.derivedBodies.values()].map((body) => ({
|
|
776
|
+
entity: body.entity,
|
|
777
|
+
revision: body.revision,
|
|
778
|
+
sourceKey: body.sourceKey,
|
|
779
|
+
...body.bodyType === void 0 ? {} : { bodyType: body.bodyType },
|
|
780
|
+
...body.velocityPolicy === void 0 ? {} : { velocityPolicy: body.velocityPolicy },
|
|
781
|
+
shapes: Object.freeze(
|
|
782
|
+
body.shapes.map((shape) => ({
|
|
783
|
+
...shape.input,
|
|
784
|
+
cells: new Int32Array(shape.input.cells),
|
|
785
|
+
voxelSize: [...shape.input.voxelSize],
|
|
786
|
+
origin: [...shape.input.origin],
|
|
787
|
+
rotation: [...shape.input.rotation]
|
|
788
|
+
}))
|
|
789
|
+
),
|
|
790
|
+
seams: Object.freeze(
|
|
791
|
+
body.seams.map((seam) => ({ ...seam, offset: [...seam.offset] }))
|
|
792
|
+
),
|
|
793
|
+
...body.massProperties === void 0 ? {} : { massProperties: body.massProperties },
|
|
794
|
+
...(() => {
|
|
795
|
+
const motion = this.getDerivedMotion(body.entity);
|
|
796
|
+
return motion === void 0 ? {} : { motion };
|
|
797
|
+
})(),
|
|
798
|
+
constraints: Object.freeze(this.constraintsForBody(body.entity))
|
|
799
|
+
}))
|
|
800
|
+
)
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
restoreDerivedPhysicsState(snapshot) {
|
|
804
|
+
this.assertActive("restoreDerivedPhysicsState");
|
|
805
|
+
const snapshotSources = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const body of snapshot.bodies) {
|
|
807
|
+
if (snapshotSources.has(body.entity)) {
|
|
808
|
+
return err(
|
|
809
|
+
new DerivedPhysicsError(
|
|
810
|
+
"derived-candidate-invalid",
|
|
811
|
+
"a portable snapshot contains one committed body row per entity",
|
|
812
|
+
"capture the snapshot from one PhysicsWorld without duplicate entities",
|
|
813
|
+
{ entity: body.entity }
|
|
814
|
+
)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
snapshotSources.set(body.entity, {
|
|
818
|
+
sourceKey: body.sourceKey,
|
|
819
|
+
revision: body.revision
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const preparedCandidates = [];
|
|
823
|
+
const restoredConstraintIds = /* @__PURE__ */ new Set();
|
|
824
|
+
for (const body of snapshot.bodies) {
|
|
825
|
+
const prepared = this.prepareDerivedShapeCandidate({
|
|
826
|
+
entity: body.entity,
|
|
827
|
+
revision: body.revision,
|
|
828
|
+
sourceKey: body.sourceKey,
|
|
829
|
+
shapes: body.shapes,
|
|
830
|
+
...body.seams === void 0 || body.seams.length === 0 ? {} : { seams: body.seams },
|
|
831
|
+
...body.bodyType === void 0 ? {} : { bodyType: body.bodyType },
|
|
832
|
+
...body.velocityPolicy === void 0 ? {} : { velocityPolicy: body.velocityPolicy },
|
|
833
|
+
...body.motion === void 0 ? {} : { motion: body.motion },
|
|
834
|
+
...body.massProperties === void 0 ? {} : { massProperties: body.massProperties },
|
|
835
|
+
constraints: body.constraints.filter((constraint) => {
|
|
836
|
+
if (restoredConstraintIds.has(constraint.id)) return false;
|
|
837
|
+
restoredConstraintIds.add(constraint.id);
|
|
838
|
+
return true;
|
|
839
|
+
})
|
|
840
|
+
});
|
|
841
|
+
if (!prepared.ok) {
|
|
842
|
+
for (const candidate of preparedCandidates) this.cancelDerivedShapeCandidate(candidate);
|
|
843
|
+
return prepared;
|
|
844
|
+
}
|
|
845
|
+
preparedCandidates.push(prepared.value);
|
|
846
|
+
}
|
|
847
|
+
const candidates = [];
|
|
848
|
+
for (const prepared of preparedCandidates) {
|
|
849
|
+
const admitted = this.admitDerivedShapeCandidateInternal(prepared, snapshotSources);
|
|
850
|
+
if (!admitted.ok) {
|
|
851
|
+
for (const candidate of preparedCandidates) this.cancelDerivedShapeCandidate(candidate);
|
|
852
|
+
return admitted;
|
|
853
|
+
}
|
|
854
|
+
candidates.push(admitted.value);
|
|
855
|
+
}
|
|
856
|
+
return ok(candidates);
|
|
857
|
+
}
|
|
858
|
+
createDerivedConstraint(input) {
|
|
859
|
+
return this.installDerivedConstraint(input, false);
|
|
860
|
+
}
|
|
861
|
+
updateDerivedConstraint(input) {
|
|
862
|
+
return this.installDerivedConstraint(input, true);
|
|
863
|
+
}
|
|
864
|
+
removeDerivedConstraint(id) {
|
|
865
|
+
this.assertActive("removeDerivedConstraint");
|
|
866
|
+
const existing = this.derivedConstraints.get(id);
|
|
867
|
+
if (existing === void 0) {
|
|
868
|
+
return err(
|
|
869
|
+
new DerivedPhysicsError(
|
|
870
|
+
"derived-constraint-not-found",
|
|
871
|
+
"constraint identity is currently committed",
|
|
872
|
+
"ignore repeated cleanup or reconcile the owning constraint set",
|
|
873
|
+
{ constraintId: id }
|
|
874
|
+
)
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
this.removeNativeConstraint(existing.handle);
|
|
878
|
+
this.derivedConstraints.delete(id);
|
|
879
|
+
return ok(void 0);
|
|
880
|
+
}
|
|
881
|
+
createDerivedCollider(body, shape) {
|
|
882
|
+
const RAPIER = this.rapierModule;
|
|
883
|
+
const desc = RAPIER.ColliderDesc.voxels(
|
|
884
|
+
shape.cells instanceof Int32Array ? new Int32Array(shape.cells) : new Int32Array(shape.cells.flat()),
|
|
885
|
+
{ x: shape.voxelSize[0], y: shape.voxelSize[1], z: shape.voxelSize[2] }
|
|
886
|
+
).setTranslation(shape.origin?.[0] ?? 0, shape.origin?.[1] ?? 0, shape.origin?.[2] ?? 0).setRotation({
|
|
887
|
+
x: shape.rotation?.[0] ?? 0,
|
|
888
|
+
y: shape.rotation?.[1] ?? 0,
|
|
889
|
+
z: shape.rotation?.[2] ?? 0,
|
|
890
|
+
w: shape.rotation?.[3] ?? 1
|
|
891
|
+
}).setFriction(shape.friction ?? 0.5).setRestitution(shape.restitution ?? 0).setDensity(0).setCollisionGroups(shape.collisionGroups ?? 4294967295).setSolverGroups(shape.solverGroups ?? 4294967295).setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS).setActiveCollisionTypes(RAPIER.ActiveCollisionTypes.ALL).setEnabled(false);
|
|
892
|
+
if (shape.isSensor === true) desc.setSensor(true);
|
|
893
|
+
const collider = this.raw.createCollider(desc, body);
|
|
894
|
+
this.derivedColliderToShape.set(collider.handle, { entity: body.userData, id: shape.id });
|
|
895
|
+
return collider;
|
|
896
|
+
}
|
|
897
|
+
removeNativeCollider(handle) {
|
|
898
|
+
this.derivedColliderToShape.delete(handle);
|
|
899
|
+
try {
|
|
900
|
+
const collider = this.raw.getCollider(handle);
|
|
901
|
+
if (collider !== null && collider !== void 0) {
|
|
902
|
+
this.raw.removeCollider(collider, false);
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
createNativeConstraint(input) {
|
|
908
|
+
let native;
|
|
909
|
+
try {
|
|
910
|
+
const RAPIER = this.rapierModule;
|
|
911
|
+
const bodyA = this.bodyForEntity(input.bodyA);
|
|
912
|
+
const bodyB = this.bodyForEntity(input.bodyB);
|
|
913
|
+
if (bodyA === void 0 || bodyB === void 0) {
|
|
914
|
+
return err(
|
|
915
|
+
new DerivedPhysicsError(
|
|
916
|
+
"derived-body-not-found",
|
|
917
|
+
"both constraint endpoints have committed bodies in this PhysicsWorld",
|
|
918
|
+
"reconcile both entities before creating the constraint",
|
|
919
|
+
{ constraintId: input.id }
|
|
920
|
+
)
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const anchorA = { x: input.anchorA[0], y: input.anchorA[1], z: input.anchorA[2] };
|
|
924
|
+
const anchorB = { x: input.anchorB[0], y: input.anchorB[1], z: input.anchorB[2] };
|
|
925
|
+
const jointData = input.kind === "spring" ? RAPIER.JointData.spring(
|
|
926
|
+
input.restLength,
|
|
927
|
+
input.stiffness,
|
|
928
|
+
input.damping,
|
|
929
|
+
anchorA,
|
|
930
|
+
anchorB
|
|
931
|
+
) : RAPIER.JointData.revolute(anchorA, anchorB, {
|
|
932
|
+
x: input.axis[0],
|
|
933
|
+
y: input.axis[1],
|
|
934
|
+
z: input.axis[2]
|
|
935
|
+
});
|
|
936
|
+
native = this.raw.createImpulseJoint(jointData, bodyA, bodyB, true);
|
|
937
|
+
if (input.kind === "hinge" && input.limits !== void 0)
|
|
938
|
+
native.setLimits(input.limits[0], input.limits[1]);
|
|
939
|
+
return ok({ handle: native.handle });
|
|
940
|
+
} catch (cause) {
|
|
941
|
+
if (native !== void 0) this.removeNativeConstraint(native.handle);
|
|
942
|
+
return err(
|
|
943
|
+
new DerivedPhysicsError(
|
|
944
|
+
"derived-backend-failed",
|
|
945
|
+
"the selected Rapier joint can be created for both endpoint bodies",
|
|
946
|
+
"repair endpoint state or use a supported spring/hinge input",
|
|
947
|
+
{
|
|
948
|
+
constraintId: input.id,
|
|
949
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
950
|
+
}
|
|
951
|
+
)
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
rememberDerivedFailure(record, error, recovery) {
|
|
956
|
+
record.state = "failed";
|
|
957
|
+
this.pendingDerivedCandidates.delete(record.token.candidateId);
|
|
958
|
+
this.derivedFailures.set(
|
|
959
|
+
record.input.entity,
|
|
960
|
+
Object.freeze({
|
|
961
|
+
candidateId: record.token.candidateId,
|
|
962
|
+
entity: record.input.entity,
|
|
963
|
+
revision: record.input.revision,
|
|
964
|
+
fixedStep: this.fixedStep,
|
|
965
|
+
error,
|
|
966
|
+
recovery
|
|
967
|
+
})
|
|
968
|
+
);
|
|
969
|
+
this.releaseDerivedCandidate(record.token.candidateId);
|
|
970
|
+
}
|
|
971
|
+
rejectPreparedCandidate(record, error) {
|
|
972
|
+
for (const collider of record.nativeColliders) this.removeNativeCollider(collider.handle);
|
|
973
|
+
this.rememberDerivedFailure(record, error, "old-state-retained");
|
|
974
|
+
}
|
|
975
|
+
processDerivedCandidates() {
|
|
976
|
+
if (this.pendingDerivedCandidates.size === 0) return;
|
|
977
|
+
const pendingRecords = [...this.pendingDerivedCandidates].map((id) => this.derivedCandidates.get(id)).filter((record) => record?.state === "queued");
|
|
978
|
+
const pendingByEntity = /* @__PURE__ */ new Map();
|
|
979
|
+
for (const record of pendingRecords) {
|
|
980
|
+
const current = pendingByEntity.get(record.input.entity);
|
|
981
|
+
if (current === void 0 || record.input.revision > current.input.revision) {
|
|
982
|
+
pendingByEntity.set(record.input.entity, record);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const ordered = [];
|
|
986
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
987
|
+
const visited = /* @__PURE__ */ new Set();
|
|
988
|
+
const visit = (record) => {
|
|
989
|
+
if (visited.has(record.token.candidateId)) return;
|
|
990
|
+
if (visiting.has(record.token.candidateId)) return;
|
|
991
|
+
visiting.add(record.token.candidateId);
|
|
992
|
+
for (const constraint of record.input.constraints ?? []) {
|
|
993
|
+
for (const endpoint of [
|
|
994
|
+
[constraint.bodyA, constraint.bodyASource],
|
|
995
|
+
[constraint.bodyB, constraint.bodyBSource]
|
|
996
|
+
]) {
|
|
997
|
+
const dependency = endpoint[1];
|
|
998
|
+
const target = pendingByEntity.get(endpoint[0]);
|
|
999
|
+
if (target !== void 0 && target.input.sourceKey === dependency.sourceKey && target.input.revision === dependency.revision) {
|
|
1000
|
+
visit(target);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
visiting.delete(record.token.candidateId);
|
|
1005
|
+
visited.add(record.token.candidateId);
|
|
1006
|
+
ordered.push(record);
|
|
1007
|
+
};
|
|
1008
|
+
for (const record of pendingRecords) visit(record);
|
|
1009
|
+
for (const record of ordered) {
|
|
1010
|
+
const id = record.token.candidateId;
|
|
1011
|
+
if (!this.pendingDerivedCandidates.has(id) || record.state !== "queued") continue;
|
|
1012
|
+
const pendingSources = this.pendingDerivedSources();
|
|
1013
|
+
const projected = pendingSources.get(record.input.entity);
|
|
1014
|
+
if (projected !== void 0 && record.input.revision < projected.revision) {
|
|
1015
|
+
this.rejectPreparedCandidate(
|
|
1016
|
+
record,
|
|
1017
|
+
new DerivedPhysicsError(
|
|
1018
|
+
"derived-candidate-stale",
|
|
1019
|
+
"fixed-step admission publishes only the newest queued body revision",
|
|
1020
|
+
"discard the older queued candidate and submit the latest complete input",
|
|
1021
|
+
{
|
|
1022
|
+
entity: record.input.entity,
|
|
1023
|
+
candidateId: record.token.candidateId,
|
|
1024
|
+
expected: `>=${projected.revision}`,
|
|
1025
|
+
actual: record.input.revision
|
|
1026
|
+
}
|
|
1027
|
+
)
|
|
1028
|
+
);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const admissionError = this.validateDerivedAdmission(record.input, pendingSources);
|
|
1032
|
+
if (admissionError !== void 0) {
|
|
1033
|
+
this.rejectPreparedCandidate(record, admissionError);
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const old = this.derivedBodies.get(record.input.entity);
|
|
1037
|
+
if (this.derivedPoisonedEntities.has(record.input.entity)) {
|
|
1038
|
+
for (const collider of record.nativeColliders) this.removeNativeCollider(collider.handle);
|
|
1039
|
+
this.rememberDerivedFailure(
|
|
1040
|
+
record,
|
|
1041
|
+
new DerivedPhysicsError(
|
|
1042
|
+
"derived-recovery-invalid",
|
|
1043
|
+
"the entity is stopped after an unrecoverable native admission failure",
|
|
1044
|
+
"rebuild the PhysicsWorld from the last portable snapshot before retrying",
|
|
1045
|
+
{ entity: record.input.entity, candidateId: record.token.candidateId }
|
|
1046
|
+
),
|
|
1047
|
+
"rebuild-required"
|
|
1048
|
+
);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const body = this.bodyForEntity(record.input.entity);
|
|
1052
|
+
if (body === void 0) {
|
|
1053
|
+
for (const collider of record.nativeColliders) this.removeNativeCollider(collider.handle);
|
|
1054
|
+
this.rememberDerivedFailure(
|
|
1055
|
+
record,
|
|
1056
|
+
new DerivedPhysicsError(
|
|
1057
|
+
"derived-body-not-found",
|
|
1058
|
+
"candidate entity remains a live native body at fixed-step admission",
|
|
1059
|
+
"reconcile the entity and submit a fresh candidate",
|
|
1060
|
+
{ entity: record.input.entity, candidateId: record.token.candidateId }
|
|
1061
|
+
),
|
|
1062
|
+
"old-state-retained"
|
|
1063
|
+
);
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
const oldBodyType = body.bodyType();
|
|
1067
|
+
const oldBodyEnabled = body.isEnabled();
|
|
1068
|
+
const oldVelocity = body.linvel();
|
|
1069
|
+
const oldAngularVelocity = body.angvel();
|
|
1070
|
+
const oldTranslation = body.translation();
|
|
1071
|
+
const oldRotation = body.rotation();
|
|
1072
|
+
const oldCom = body.worldCom();
|
|
1073
|
+
const oldMass = body.mass();
|
|
1074
|
+
const oldAutomaticAdditionalMass = this.entityMap.get(record.input.entity)?.automaticAdditionalMass ?? 0;
|
|
1075
|
+
const oldAutomaticRecordMass = this.entityMap.get(
|
|
1076
|
+
record.input.entity
|
|
1077
|
+
)?.automaticAdditionalMass;
|
|
1078
|
+
const oldSource = this.derivedBodySources.get(record.input.entity);
|
|
1079
|
+
const oldPublication = this.derivedPublications.get(record.input.entity);
|
|
1080
|
+
const oldDensities = this.bodyColliders(body).map((collider) => ({
|
|
1081
|
+
collider,
|
|
1082
|
+
density: typeof collider.density === "function" ? collider.density() : void 0,
|
|
1083
|
+
enabled: typeof collider.isEnabled === "function" ? collider.isEnabled() : true
|
|
1084
|
+
}));
|
|
1085
|
+
const oldConstraints = new Map(this.derivedConstraints);
|
|
1086
|
+
const stagedConstraints = /* @__PURE__ */ new Map();
|
|
1087
|
+
let geometryCommitUncertain = false;
|
|
1088
|
+
try {
|
|
1089
|
+
for (const constraint of record.input.constraints ?? []) {
|
|
1090
|
+
const created = this.createNativeConstraint(constraint);
|
|
1091
|
+
if (!created.ok) throw created.error;
|
|
1092
|
+
stagedConstraints.set(constraint.id, {
|
|
1093
|
+
input: { ...constraint },
|
|
1094
|
+
handle: created.value.handle
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (record.input.bodyType !== void 0) {
|
|
1098
|
+
const RAPIER = this.rapierModule;
|
|
1099
|
+
const bodyType = record.input.bodyType === "static" ? RAPIER.RigidBodyType.Fixed : record.input.bodyType === "kinematic" ? RAPIER.RigidBodyType.KinematicPositionBased : RAPIER.RigidBodyType.Dynamic;
|
|
1100
|
+
body.setBodyType(bodyType, true);
|
|
1101
|
+
}
|
|
1102
|
+
for (const native of record.nativeColliders) {
|
|
1103
|
+
const collider = this.raw.getCollider(native.handle);
|
|
1104
|
+
if (collider === null || collider === void 0)
|
|
1105
|
+
throw new Error("candidate collider disappeared");
|
|
1106
|
+
collider.setEnabled(true);
|
|
1107
|
+
}
|
|
1108
|
+
const nativeById = new Map(
|
|
1109
|
+
record.input.shapes.map((shape, index) => [
|
|
1110
|
+
shape.id,
|
|
1111
|
+
record.nativeColliders[index]?.handle
|
|
1112
|
+
])
|
|
1113
|
+
);
|
|
1114
|
+
for (const seam of record.input.seams ?? []) {
|
|
1115
|
+
const firstHandle = nativeById.get(seam.shapeA);
|
|
1116
|
+
const secondHandle = nativeById.get(seam.shapeB);
|
|
1117
|
+
const first = firstHandle === void 0 ? void 0 : this.raw.getCollider(firstHandle);
|
|
1118
|
+
const second = secondHandle === void 0 ? void 0 : this.raw.getCollider(secondHandle);
|
|
1119
|
+
if (first === void 0 || second === void 0 || first === null || second === null) {
|
|
1120
|
+
throw new Error(`derived seam references missing shape ${seam.shapeA}`);
|
|
1121
|
+
}
|
|
1122
|
+
first.combineVoxelStates(second, seam.offset[0], seam.offset[1], seam.offset[2]);
|
|
1123
|
+
}
|
|
1124
|
+
this.applyDerivedMass(
|
|
1125
|
+
body,
|
|
1126
|
+
record.input.massProperties,
|
|
1127
|
+
oldCom,
|
|
1128
|
+
record.input.velocityPolicy ?? "preserve",
|
|
1129
|
+
this.entityMap.get(record.input.entity)?.additionalMass ?? 0,
|
|
1130
|
+
record.input.entity,
|
|
1131
|
+
record.input.shapes,
|
|
1132
|
+
record.nativeColliders
|
|
1133
|
+
);
|
|
1134
|
+
if (record.input.motion !== void 0) {
|
|
1135
|
+
const currentCom = body.worldCom();
|
|
1136
|
+
const targetCom = record.input.motion.centerOfMass;
|
|
1137
|
+
const translation = body.translation();
|
|
1138
|
+
body.setTranslation(
|
|
1139
|
+
{
|
|
1140
|
+
x: translation.x + targetCom[0] - currentCom.x,
|
|
1141
|
+
y: translation.y + targetCom[1] - currentCom.y,
|
|
1142
|
+
z: translation.z + targetCom[2] - currentCom.z
|
|
1143
|
+
},
|
|
1144
|
+
true
|
|
1145
|
+
);
|
|
1146
|
+
body.setLinvel(
|
|
1147
|
+
{
|
|
1148
|
+
x: record.input.motion.linearVelocity[0],
|
|
1149
|
+
y: record.input.motion.linearVelocity[1],
|
|
1150
|
+
z: record.input.motion.linearVelocity[2]
|
|
1151
|
+
},
|
|
1152
|
+
true
|
|
1153
|
+
);
|
|
1154
|
+
body.setAngvel(
|
|
1155
|
+
{
|
|
1156
|
+
x: record.input.motion.angularVelocity[0],
|
|
1157
|
+
y: record.input.motion.angularVelocity[1],
|
|
1158
|
+
z: record.input.motion.angularVelocity[2]
|
|
1159
|
+
},
|
|
1160
|
+
true
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
const committedSources = new Map(this.derivedBodySources);
|
|
1164
|
+
committedSources.set(record.input.entity, {
|
|
1165
|
+
sourceKey: record.input.sourceKey,
|
|
1166
|
+
revision: record.input.revision
|
|
1167
|
+
});
|
|
1168
|
+
const replacementConstraintIds = new Set(
|
|
1169
|
+
(record.input.constraints ?? []).map((constraint) => constraint.id)
|
|
1170
|
+
);
|
|
1171
|
+
for (const [constraintId, current] of [...this.derivedConstraints]) {
|
|
1172
|
+
if (replacementConstraintIds.has(constraintId) || this.constraintDependenciesMatch(current.input, committedSources))
|
|
1173
|
+
continue;
|
|
1174
|
+
this.removeNativeConstraint(current.handle);
|
|
1175
|
+
this.derivedConstraints.delete(constraintId);
|
|
1176
|
+
}
|
|
1177
|
+
if (old !== void 0) {
|
|
1178
|
+
for (const shape of old.shapes) {
|
|
1179
|
+
const collider = this.raw.getCollider(shape.colliderHandle);
|
|
1180
|
+
if (collider !== null && collider !== void 0) collider.setEnabled(false);
|
|
1181
|
+
}
|
|
1182
|
+
this.retiredDerivedBodies.push(old);
|
|
1183
|
+
}
|
|
1184
|
+
for (const constraint of record.input.constraints ?? []) {
|
|
1185
|
+
const previous = this.derivedConstraints.get(constraint.id);
|
|
1186
|
+
if (previous !== void 0) this.removeNativeConstraint(previous.handle);
|
|
1187
|
+
const staged = stagedConstraints.get(constraint.id);
|
|
1188
|
+
if (staged !== void 0) this.derivedConstraints.set(constraint.id, staged);
|
|
1189
|
+
}
|
|
1190
|
+
const shapes = record.input.shapes.map(
|
|
1191
|
+
(shape, index) => ({
|
|
1192
|
+
input: shape,
|
|
1193
|
+
colliderHandle: record.nativeColliders[index]?.handle
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
const committed = {
|
|
1197
|
+
entity: record.input.entity,
|
|
1198
|
+
sourceKey: record.input.sourceKey,
|
|
1199
|
+
generation: this.backendGeneration,
|
|
1200
|
+
revision: record.input.revision,
|
|
1201
|
+
bodyType: record.input.bodyType,
|
|
1202
|
+
velocityPolicy: record.input.velocityPolicy,
|
|
1203
|
+
candidateId: record.token.candidateId,
|
|
1204
|
+
shapes,
|
|
1205
|
+
seams: [...record.input.seams ?? []],
|
|
1206
|
+
massProperties: record.input.massProperties,
|
|
1207
|
+
constraints: [...record.input.constraints ?? []]
|
|
1208
|
+
};
|
|
1209
|
+
if (record.commitGeometry !== void 0) {
|
|
1210
|
+
this.activeDerivedAdmission = record;
|
|
1211
|
+
try {
|
|
1212
|
+
geometryCommitUncertain = true;
|
|
1213
|
+
const geometry = record.commitGeometry();
|
|
1214
|
+
geometryCommitUncertain = false;
|
|
1215
|
+
if (!geometry.ok) throw geometry.error;
|
|
1216
|
+
} finally {
|
|
1217
|
+
this.activeDerivedAdmission = void 0;
|
|
1218
|
+
}
|
|
1219
|
+
delete record.commitGeometry;
|
|
1220
|
+
}
|
|
1221
|
+
this.derivedBodies.set(record.input.entity, committed);
|
|
1222
|
+
const entityRecord = this.entityMap.get(record.input.entity);
|
|
1223
|
+
if (entityRecord !== void 0) {
|
|
1224
|
+
entityRecord.automaticAdditionalMass = record.input.massProperties?.mode === "explicit" ? 0 : entityRecord.additionalMass;
|
|
1225
|
+
}
|
|
1226
|
+
this.derivedBodySources.set(record.input.entity, {
|
|
1227
|
+
sourceKey: record.input.sourceKey,
|
|
1228
|
+
revision: record.input.revision
|
|
1229
|
+
});
|
|
1230
|
+
this.derivedFailures.delete(record.input.entity);
|
|
1231
|
+
record.state = "queued";
|
|
1232
|
+
this.pendingDerivedCandidates.delete(id);
|
|
1233
|
+
} catch (cause) {
|
|
1234
|
+
for (const staged of stagedConstraints.values()) this.removeNativeConstraint(staged.handle);
|
|
1235
|
+
for (const current of this.derivedConstraints.values())
|
|
1236
|
+
this.removeNativeConstraint(current.handle);
|
|
1237
|
+
this.derivedConstraints.clear();
|
|
1238
|
+
if (old !== void 0) {
|
|
1239
|
+
for (const shape of old.shapes) {
|
|
1240
|
+
const collider = this.raw.getCollider(shape.colliderHandle);
|
|
1241
|
+
if (collider !== null && collider !== void 0) collider.setEnabled(true);
|
|
1242
|
+
}
|
|
1243
|
+
const retiredIndex = this.retiredDerivedBodies.indexOf(old);
|
|
1244
|
+
if (retiredIndex >= 0) this.retiredDerivedBodies.splice(retiredIndex, 1);
|
|
1245
|
+
this.derivedBodies.set(record.input.entity, old);
|
|
1246
|
+
this.derivedBodySources.set(record.input.entity, {
|
|
1247
|
+
sourceKey: old.sourceKey,
|
|
1248
|
+
revision: old.revision
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
for (const native of record.nativeColliders) {
|
|
1252
|
+
const collider = this.raw.getCollider(native.handle);
|
|
1253
|
+
if (collider !== null && collider !== void 0) collider.setEnabled(false);
|
|
1254
|
+
}
|
|
1255
|
+
for (const native of record.nativeColliders) this.removeNativeCollider(native.handle);
|
|
1256
|
+
for (const { collider, density, enabled } of oldDensities) {
|
|
1257
|
+
if (this.raw.getCollider(collider.handle) === null) continue;
|
|
1258
|
+
if (density !== void 0 && typeof collider.setDensity === "function")
|
|
1259
|
+
collider.setDensity(density);
|
|
1260
|
+
if (typeof collider.setEnabled === "function") collider.setEnabled(enabled);
|
|
1261
|
+
}
|
|
1262
|
+
let restored = true;
|
|
1263
|
+
try {
|
|
1264
|
+
body.setBodyType(oldBodyType, true);
|
|
1265
|
+
this.restoreCommittedMass(body, old, oldAutomaticAdditionalMass);
|
|
1266
|
+
body.setTranslation(oldTranslation, true);
|
|
1267
|
+
body.setRotation(oldRotation, true);
|
|
1268
|
+
body.setLinvel(oldVelocity, true);
|
|
1269
|
+
body.setAngvel(oldAngularVelocity, true);
|
|
1270
|
+
body.setEnabled(oldBodyEnabled);
|
|
1271
|
+
} catch {
|
|
1272
|
+
restored = false;
|
|
1273
|
+
}
|
|
1274
|
+
if (restored && !this.restoreNativeConstraints(oldConstraints)) restored = false;
|
|
1275
|
+
if (restored) {
|
|
1276
|
+
const currentMass = body.mass();
|
|
1277
|
+
const currentCom = body.worldCom();
|
|
1278
|
+
const currentVelocity = body.linvel();
|
|
1279
|
+
const currentAngularVelocity = body.angvel();
|
|
1280
|
+
restored = Number.isFinite(currentMass) && Math.abs(currentMass - oldMass) <= 1e-6 * Math.max(1, Math.abs(oldMass)) && Math.abs(currentCom.x - oldCom.x) <= 1e-6 && Math.abs(currentCom.y - oldCom.y) <= 1e-6 && Math.abs(currentCom.z - oldCom.z) <= 1e-6 && Math.abs(currentVelocity.x - oldVelocity.x) <= 1e-6 && Math.abs(currentVelocity.y - oldVelocity.y) <= 1e-6 && Math.abs(currentVelocity.z - oldVelocity.z) <= 1e-6 && Math.abs(currentAngularVelocity.x - oldAngularVelocity.x) <= 1e-6 && Math.abs(currentAngularVelocity.y - oldAngularVelocity.y) <= 1e-6 && Math.abs(currentAngularVelocity.z - oldAngularVelocity.z) <= 1e-6 && body.bodyType() === oldBodyType && body.isEnabled() === oldBodyEnabled;
|
|
1281
|
+
}
|
|
1282
|
+
if (old === void 0) this.derivedBodies.delete(record.input.entity);
|
|
1283
|
+
else this.derivedBodies.set(record.input.entity, old);
|
|
1284
|
+
if (oldSource === void 0) this.derivedBodySources.delete(record.input.entity);
|
|
1285
|
+
else this.derivedBodySources.set(record.input.entity, oldSource);
|
|
1286
|
+
if (oldPublication === void 0) this.derivedPublications.delete(record.input.entity);
|
|
1287
|
+
else this.derivedPublications.set(record.input.entity, oldPublication);
|
|
1288
|
+
const entityRecord = this.entityMap.get(record.input.entity);
|
|
1289
|
+
if (entityRecord !== void 0 && oldAutomaticRecordMass !== void 0)
|
|
1290
|
+
entityRecord.automaticAdditionalMass = oldAutomaticRecordMass;
|
|
1291
|
+
const error = cause instanceof DerivedPhysicsError ? cause : new DerivedPhysicsError(
|
|
1292
|
+
"derived-backend-failed",
|
|
1293
|
+
"derived admission either commits completely or preserves the prior body state",
|
|
1294
|
+
"inspect the failure receipt and rebuild the PhysicsWorld if recovery is required",
|
|
1295
|
+
{
|
|
1296
|
+
entity: record.input.entity,
|
|
1297
|
+
candidateId: record.token.candidateId,
|
|
1298
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
1299
|
+
}
|
|
1300
|
+
);
|
|
1301
|
+
if (geometryCommitUncertain) restored = false;
|
|
1302
|
+
if (!restored) {
|
|
1303
|
+
this.derivedPoisonedEntities.add(record.input.entity);
|
|
1304
|
+
this.derivedBodies.delete(record.input.entity);
|
|
1305
|
+
this.derivedPublications.delete(record.input.entity);
|
|
1306
|
+
}
|
|
1307
|
+
this.rememberDerivedFailure(
|
|
1308
|
+
record,
|
|
1309
|
+
error,
|
|
1310
|
+
restored ? "old-state-retained" : "rebuild-required"
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
publishDerivedCandidates() {
|
|
1316
|
+
for (const body of this.derivedBodies.values()) {
|
|
1317
|
+
const candidate = this.derivedCandidates.get(body.candidateId);
|
|
1318
|
+
if (candidate?.state !== "queued") continue;
|
|
1319
|
+
this.derivedPublications.set(
|
|
1320
|
+
body.entity,
|
|
1321
|
+
Object.freeze({
|
|
1322
|
+
candidateId: body.candidateId,
|
|
1323
|
+
entity: body.entity,
|
|
1324
|
+
revision: body.revision,
|
|
1325
|
+
fixedStep: this.fixedStep,
|
|
1326
|
+
shapeIds: Object.freeze(body.shapes.map((shape) => shape.input.id)),
|
|
1327
|
+
generation: body.generation
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
candidate.state = "published";
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
retireDerivedBodies() {
|
|
1334
|
+
for (const body of this.retiredDerivedBodies.splice(0)) {
|
|
1335
|
+
for (const shape of body.shapes) this.removeNativeCollider(shape.colliderHandle);
|
|
1336
|
+
this.releaseDerivedCandidate(body.candidateId);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
applyDerivedMass(body, properties, previousWorldCom, velocityPolicy, authoredAdditionalMass, entity, candidateShapes, candidateColliders) {
|
|
1340
|
+
const oldVelocity = body.linvel();
|
|
1341
|
+
const oldAngularVelocity = body.angvel();
|
|
1342
|
+
if (properties?.mode === "explicit") {
|
|
1343
|
+
this.rememberAuthoredDensity(entity, body);
|
|
1344
|
+
for (const collider of this.bodyColliders(body)) {
|
|
1345
|
+
if (typeof collider.setDensity === "function") collider.setDensity(0);
|
|
1346
|
+
}
|
|
1347
|
+
const frame = properties.principalInertiaLocalFrame ?? [0, 0, 0, 1];
|
|
1348
|
+
body.setAdditionalMassProperties(
|
|
1349
|
+
properties.mass,
|
|
1350
|
+
{
|
|
1351
|
+
x: properties.centerOfMass[0],
|
|
1352
|
+
y: properties.centerOfMass[1],
|
|
1353
|
+
z: properties.centerOfMass[2]
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
x: properties.principalInertia[0],
|
|
1357
|
+
y: properties.principalInertia[1],
|
|
1358
|
+
z: properties.principalInertia[2]
|
|
1359
|
+
},
|
|
1360
|
+
{ x: frame[0], y: frame[1], z: frame[2], w: frame[3] },
|
|
1361
|
+
true
|
|
1362
|
+
);
|
|
1363
|
+
} else {
|
|
1364
|
+
this.restoreAutomaticDensities(
|
|
1365
|
+
entity,
|
|
1366
|
+
body,
|
|
1367
|
+
properties?.mode === "automatic" ? properties.density : void 0,
|
|
1368
|
+
candidateShapes,
|
|
1369
|
+
candidateColliders
|
|
1370
|
+
);
|
|
1371
|
+
this.restoreAutomaticMass(body, authoredAdditionalMass);
|
|
1372
|
+
}
|
|
1373
|
+
if (velocityPolicy === "reset") {
|
|
1374
|
+
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
|
1375
|
+
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const nextWorldCom = body.worldCom();
|
|
1379
|
+
const next = preserveCenterOfMassVelocity(
|
|
1380
|
+
[oldVelocity.x, oldVelocity.y, oldVelocity.z],
|
|
1381
|
+
[oldAngularVelocity.x, oldAngularVelocity.y, oldAngularVelocity.z],
|
|
1382
|
+
[previousWorldCom.x, previousWorldCom.y, previousWorldCom.z],
|
|
1383
|
+
[nextWorldCom.x, nextWorldCom.y, nextWorldCom.z]
|
|
1384
|
+
);
|
|
1385
|
+
body.setLinvel({ x: next[0], y: next[1], z: next[2] }, true);
|
|
1386
|
+
body.setAngvel(oldAngularVelocity, true);
|
|
1387
|
+
}
|
|
1388
|
+
/** Restore the complete committed mass policy after a failed admission. */
|
|
1389
|
+
restoreCommittedMass(body, previous, automaticAdditionalMass) {
|
|
1390
|
+
if (previous?.massProperties?.mode === "explicit") {
|
|
1391
|
+
const frame = previous.massProperties.principalInertiaLocalFrame ?? [0, 0, 0, 1];
|
|
1392
|
+
body.setAdditionalMassProperties(
|
|
1393
|
+
previous.massProperties.mass,
|
|
1394
|
+
{
|
|
1395
|
+
x: previous.massProperties.centerOfMass[0],
|
|
1396
|
+
y: previous.massProperties.centerOfMass[1],
|
|
1397
|
+
z: previous.massProperties.centerOfMass[2]
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
x: previous.massProperties.principalInertia[0],
|
|
1401
|
+
y: previous.massProperties.principalInertia[1],
|
|
1402
|
+
z: previous.massProperties.principalInertia[2]
|
|
1403
|
+
},
|
|
1404
|
+
{ x: frame[0], y: frame[1], z: frame[2], w: frame[3] },
|
|
1405
|
+
true
|
|
1406
|
+
);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
this.restoreAutomaticMass(body, automaticAdditionalMass);
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Rapier keeps `setAdditionalMassProperties` as native state even after a
|
|
1413
|
+
* collider recompute. Clear that override explicitly before recomputing so
|
|
1414
|
+
* automatic candidates and rollback really return to the authored policy.
|
|
1415
|
+
*/
|
|
1416
|
+
restoreAutomaticMass(body, additionalMass) {
|
|
1417
|
+
body.setAdditionalMass(Math.max(0, additionalMass), true);
|
|
1418
|
+
body.recomputeMassPropertiesFromColliders();
|
|
1419
|
+
}
|
|
1420
|
+
rememberAuthoredDensity(entity, body) {
|
|
1421
|
+
const record = this.entityMap.get(entity);
|
|
1422
|
+
if (record === void 0 || record.authoredDensity !== void 0) return;
|
|
1423
|
+
const authored = this.bodyColliders(body).find(
|
|
1424
|
+
(collider) => !this.derivedColliderToShape.has(collider.handle)
|
|
1425
|
+
);
|
|
1426
|
+
const density = authored !== void 0 && typeof authored.density === "function" ? authored.density() : void 0;
|
|
1427
|
+
if (density !== void 0 && Number.isFinite(density) && density >= 0)
|
|
1428
|
+
record.authoredDensity = density;
|
|
1429
|
+
}
|
|
1430
|
+
restoreAutomaticDensities(entity, body, overrideDensity, candidateShapes, candidateColliders) {
|
|
1431
|
+
const record = this.entityMap.get(entity);
|
|
1432
|
+
const candidateDensityByHandle = /* @__PURE__ */ new Map();
|
|
1433
|
+
for (const [index, collider] of candidateColliders.entries()) {
|
|
1434
|
+
const shape = candidateShapes[index];
|
|
1435
|
+
if (shape !== void 0) candidateDensityByHandle.set(collider.handle, shape.density ?? 1);
|
|
1436
|
+
}
|
|
1437
|
+
const authoredDensity = record?.authoredDensity ?? 1;
|
|
1438
|
+
for (const collider of this.bodyColliders(body)) {
|
|
1439
|
+
const candidateDensity = candidateDensityByHandle.get(collider.handle);
|
|
1440
|
+
const density = candidateDensity !== void 0 ? overrideDensity ?? candidateDensity : this.derivedColliderToShape.has(collider.handle) ? 0 : overrideDensity ?? authoredDensity;
|
|
1441
|
+
if (typeof collider.setDensity === "function") collider.setDensity(density);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
restoreNativeConstraints(previous) {
|
|
1445
|
+
for (const [constraintId, record] of previous) {
|
|
1446
|
+
const recreated = this.createNativeConstraint(record.input);
|
|
1447
|
+
if (!recreated.ok) {
|
|
1448
|
+
for (const current of this.derivedConstraints.values())
|
|
1449
|
+
this.removeNativeConstraint(current.handle);
|
|
1450
|
+
this.derivedConstraints.clear();
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
this.derivedConstraints.set(constraintId, {
|
|
1454
|
+
input: { ...record.input },
|
|
1455
|
+
handle: recreated.value.handle
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
bodyColliders(body) {
|
|
1461
|
+
const result = [];
|
|
1462
|
+
for (let index = 0; index < body.numColliders(); index += 1) {
|
|
1463
|
+
const collider = body.collider(index);
|
|
1464
|
+
if (collider !== null && collider !== void 0) result.push(collider);
|
|
1465
|
+
}
|
|
1466
|
+
return result;
|
|
1467
|
+
}
|
|
1468
|
+
releaseDerivedCandidate(candidateId) {
|
|
1469
|
+
const record = this.derivedCandidates.get(candidateId);
|
|
1470
|
+
if (record === void 0) return;
|
|
1471
|
+
this.derivedCandidateBytes = Math.max(0, this.derivedCandidateBytes - record.bytes);
|
|
1472
|
+
this.derivedCandidates.delete(candidateId);
|
|
1473
|
+
}
|
|
1474
|
+
sourceForEntity(entity) {
|
|
1475
|
+
return this.derivedBodySources.get(entity) ?? {
|
|
1476
|
+
sourceKey: `entity:${entity}`,
|
|
1477
|
+
revision: 0
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Project the final source revision of every queued body. Constraint
|
|
1482
|
+
* dependencies are checked against this projection, not against whichever
|
|
1483
|
+
* queued candidate happens to be processed first.
|
|
1484
|
+
*/
|
|
1485
|
+
pendingDerivedSources() {
|
|
1486
|
+
const sources = /* @__PURE__ */ new Map();
|
|
1487
|
+
for (const candidateId of this.pendingDerivedCandidates) {
|
|
1488
|
+
const record = this.derivedCandidates.get(candidateId);
|
|
1489
|
+
if (record === void 0 || record.state !== "queued") continue;
|
|
1490
|
+
const current = sources.get(record.input.entity);
|
|
1491
|
+
if (current === void 0 || record.input.revision > current.revision) {
|
|
1492
|
+
sources.set(record.input.entity, {
|
|
1493
|
+
sourceKey: record.input.sourceKey,
|
|
1494
|
+
revision: record.input.revision
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return sources;
|
|
1499
|
+
}
|
|
1500
|
+
newestPendingRevision(entity) {
|
|
1501
|
+
return this.pendingDerivedSources().get(entity)?.revision;
|
|
1502
|
+
}
|
|
1503
|
+
recoveryBlocked() {
|
|
1504
|
+
return this.derivedPoisonedEntities.size > 0 || this.syncState?.world.execution.health === "poisoned";
|
|
1505
|
+
}
|
|
1506
|
+
validateConstraintDependencies(input, candidate, sourceOverrides) {
|
|
1507
|
+
const endpoints = [
|
|
1508
|
+
[input.bodyA, input.bodyASource],
|
|
1509
|
+
[input.bodyB, input.bodyBSource]
|
|
1510
|
+
];
|
|
1511
|
+
for (const [entity, dependency] of endpoints) {
|
|
1512
|
+
if (this.derivedPoisonedEntities.has(entity)) {
|
|
1513
|
+
return new DerivedPhysicsError(
|
|
1514
|
+
"derived-recovery-invalid",
|
|
1515
|
+
"constraint endpoints belong to a healthy PhysicsWorld state",
|
|
1516
|
+
"rebuild the PhysicsWorld before recreating constraints",
|
|
1517
|
+
{ constraintId: input.id, entity }
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
if (!this.entityMap.has(entity)) {
|
|
1521
|
+
return new DerivedPhysicsError(
|
|
1522
|
+
"derived-body-not-found",
|
|
1523
|
+
"both constraint endpoint entities have committed bodies",
|
|
1524
|
+
"reconcile both endpoint entities before creating or migrating a constraint",
|
|
1525
|
+
{ constraintId: input.id, entity }
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
const expected = candidate !== void 0 && entity === candidate.entity ? { sourceKey: candidate.sourceKey, revision: candidate.revision } : sourceOverrides?.get(entity) ?? this.sourceForEntity(entity);
|
|
1529
|
+
if (dependency.sourceKey !== expected.sourceKey || dependency.revision !== expected.revision) {
|
|
1530
|
+
return new DerivedPhysicsError(
|
|
1531
|
+
"derived-constraint-stale",
|
|
1532
|
+
"constraint endpoint sourceKey and revision match the committed endpoint",
|
|
1533
|
+
"refresh both endpoint dependencies and retry the complete candidate",
|
|
1534
|
+
{
|
|
1535
|
+
constraintId: input.id,
|
|
1536
|
+
entity,
|
|
1537
|
+
expected: `${expected.sourceKey}@${expected.revision}`,
|
|
1538
|
+
actual: `${dependency.sourceKey}@${dependency.revision}`
|
|
1539
|
+
}
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return void 0;
|
|
1544
|
+
}
|
|
1545
|
+
constraintDependenciesMatch(input, sourceOverrides) {
|
|
1546
|
+
for (const [entity, dependency] of [
|
|
1547
|
+
[input.bodyA, input.bodyASource],
|
|
1548
|
+
[input.bodyB, input.bodyBSource]
|
|
1549
|
+
]) {
|
|
1550
|
+
if (!this.entityMap.has(entity)) return false;
|
|
1551
|
+
const expected = sourceOverrides.get(entity) ?? this.sourceForEntity(entity);
|
|
1552
|
+
if (dependency.sourceKey !== expected.sourceKey || dependency.revision !== expected.revision)
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
validateDerivedAdmission(input, sourceOverrides) {
|
|
1558
|
+
if (input.bodyType !== void 0 && input.bodyType !== "static" && input.bodyType !== "dynamic" && input.bodyType !== "kinematic") {
|
|
1559
|
+
return new DerivedPhysicsError(
|
|
1560
|
+
"derived-candidate-invalid",
|
|
1561
|
+
"candidate bodyType is one of static, dynamic, or kinematic",
|
|
1562
|
+
"repair the motion type before admission",
|
|
1563
|
+
{ entity: input.entity, actual: input.bodyType }
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1567
|
+
for (const constraint of input.constraints ?? []) {
|
|
1568
|
+
const validation = validateConstraintInput(constraint);
|
|
1569
|
+
if (!validation.ok) return validation.error;
|
|
1570
|
+
if (seen.has(constraint.id)) {
|
|
1571
|
+
return new DerivedPhysicsError(
|
|
1572
|
+
"derived-constraint-invalid",
|
|
1573
|
+
"candidate contains one constraint update per identity",
|
|
1574
|
+
"merge duplicate updates before admission",
|
|
1575
|
+
{ entity: input.entity, constraintId: constraint.id }
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
seen.add(constraint.id);
|
|
1579
|
+
const dependencyError = this.validateConstraintDependencies(
|
|
1580
|
+
constraint,
|
|
1581
|
+
input,
|
|
1582
|
+
sourceOverrides
|
|
1583
|
+
);
|
|
1584
|
+
if (dependencyError !== void 0) return dependencyError;
|
|
1585
|
+
const existing = this.derivedConstraints.get(constraint.id);
|
|
1586
|
+
if (existing !== void 0 && constraint.revision <= existing.input.revision) {
|
|
1587
|
+
return new DerivedPhysicsError(
|
|
1588
|
+
"derived-constraint-stale",
|
|
1589
|
+
"migrated constraint revision advances the committed revision",
|
|
1590
|
+
"submit both endpoint dependencies and a newer constraint revision",
|
|
1591
|
+
{
|
|
1592
|
+
entity: input.entity,
|
|
1593
|
+
constraintId: constraint.id,
|
|
1594
|
+
expected: `>${existing.input.revision}`,
|
|
1595
|
+
actual: constraint.revision
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return void 0;
|
|
1601
|
+
}
|
|
1602
|
+
constraintsForBody(entity) {
|
|
1603
|
+
return [...this.derivedConstraints.values()].filter(
|
|
1604
|
+
(constraint) => constraint.input.bodyA === entity || constraint.input.bodyB === entity
|
|
1605
|
+
).map((constraint) => ({ ...constraint.input }));
|
|
1606
|
+
}
|
|
1607
|
+
installDerivedConstraint(input, updating) {
|
|
1608
|
+
this.assertActive(updating ? "updateDerivedConstraint" : "createDerivedConstraint");
|
|
1609
|
+
const validation = validateConstraintInput(input);
|
|
1610
|
+
if (!validation.ok) return validation;
|
|
1611
|
+
const dependencyError = this.validateConstraintDependencies(
|
|
1612
|
+
input,
|
|
1613
|
+
void 0,
|
|
1614
|
+
this.pendingDerivedSources()
|
|
1615
|
+
);
|
|
1616
|
+
if (dependencyError !== void 0) return err(dependencyError);
|
|
1617
|
+
const existing = this.derivedConstraints.get(input.id);
|
|
1618
|
+
if (existing !== void 0 && !updating) {
|
|
1619
|
+
return err(
|
|
1620
|
+
new DerivedPhysicsError(
|
|
1621
|
+
"derived-constraint-stale",
|
|
1622
|
+
"constraint identity is not already committed when creating it",
|
|
1623
|
+
"call updateDerivedConstraint with a newer revision",
|
|
1624
|
+
{ constraintId: input.id }
|
|
1625
|
+
)
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
if (existing !== void 0 && input.revision <= existing.input.revision) {
|
|
1629
|
+
return err(
|
|
1630
|
+
new DerivedPhysicsError(
|
|
1631
|
+
"derived-constraint-stale",
|
|
1632
|
+
"constraint revision advances monotonically",
|
|
1633
|
+
"submit a newer constraint revision",
|
|
1634
|
+
{
|
|
1635
|
+
constraintId: input.id,
|
|
1636
|
+
actual: input.revision,
|
|
1637
|
+
expected: `>${existing.input.revision}`
|
|
1638
|
+
}
|
|
1639
|
+
)
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
const native = this.createNativeConstraint(input);
|
|
1643
|
+
if (!native.ok) return native;
|
|
1644
|
+
if (existing !== void 0) this.removeNativeConstraint(existing.handle);
|
|
1645
|
+
this.derivedConstraints.set(input.id, { input: { ...input }, handle: native.value.handle });
|
|
1646
|
+
return ok({ id: input.id, revision: input.revision });
|
|
1647
|
+
}
|
|
1648
|
+
removeNativeConstraint(handle) {
|
|
1649
|
+
try {
|
|
1650
|
+
const joint = this.raw.getImpulseJoint(handle);
|
|
1651
|
+
if (joint !== null && joint !== void 0)
|
|
1652
|
+
this.raw.removeImpulseJoint(joint, true);
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
235
1656
|
getPendingTeleports() {
|
|
236
1657
|
return [...this.pendingTeleports].map(([entity, target]) => [entity, { ...target }]);
|
|
237
1658
|
}
|
|
@@ -240,6 +1661,22 @@ var RapierPhysicsWorld3D = class {
|
|
|
240
1661
|
}
|
|
241
1662
|
dispose() {
|
|
242
1663
|
if (this.disposed) return;
|
|
1664
|
+
this.assertActive("dispose");
|
|
1665
|
+
this.invalidateDerivedShapeCandidates("physics-dispose");
|
|
1666
|
+
this.derivedCandidates.clear();
|
|
1667
|
+
this.pendingDerivedCandidates.clear();
|
|
1668
|
+
this.derivedBodies.clear();
|
|
1669
|
+
this.derivedBodySources.clear();
|
|
1670
|
+
this.derivedPublications.clear();
|
|
1671
|
+
this.derivedFailures.clear();
|
|
1672
|
+
this.derivedConstraints.clear();
|
|
1673
|
+
this.derivedContacts.length = 0;
|
|
1674
|
+
this.derivedPublicationPending = false;
|
|
1675
|
+
this.derivedPoisonedEntities.clear();
|
|
1676
|
+
this.derivedColliderToShape.clear();
|
|
1677
|
+
this.retiredDerivedBodies.length = 0;
|
|
1678
|
+
this.derivedCandidateBytes = 0;
|
|
1679
|
+
this.backendGeneration += 1;
|
|
243
1680
|
this.syncState = void 0;
|
|
244
1681
|
this.moveContext = void 0;
|
|
245
1682
|
if (typeof this.raw.free === "function") this.raw.free();
|
|
@@ -267,17 +1704,27 @@ var RapierPhysicsWorld3D = class {
|
|
|
267
1704
|
setMoveContext(world, transform, characterController) {
|
|
268
1705
|
this.assertActive("setMoveContext");
|
|
269
1706
|
this.moveContext = { world, transform, characterController };
|
|
1707
|
+
this.worldIdentity = world;
|
|
270
1708
|
}
|
|
271
1709
|
/** Release the persistent ECS readers owned by one system registration. */
|
|
272
1710
|
clearEcsContext(world) {
|
|
273
1711
|
if (this.syncState?.world === world) this.syncState = void 0;
|
|
274
1712
|
if (this.moveContext?.world === world) this.moveContext = void 0;
|
|
1713
|
+
if (this.worldIdentity === world) this.worldIdentity = void 0;
|
|
275
1714
|
}
|
|
276
1715
|
moveAndSlide(entity, desiredDelta) {
|
|
277
1716
|
this.assertActive("moveAndSlide");
|
|
278
1717
|
return this.computeMove(entity, desiredDelta);
|
|
279
1718
|
}
|
|
280
1719
|
assertActive(operation) {
|
|
1720
|
+
if (this.activeDerivedAdmission !== void 0) {
|
|
1721
|
+
throw new DerivedPhysicsError(
|
|
1722
|
+
"derived-candidate-pending",
|
|
1723
|
+
"physics queries and mutations observe only complete fixed-step states",
|
|
1724
|
+
"finish the paired geometry commit before querying or mutating physics",
|
|
1725
|
+
{ entity: this.activeDerivedAdmission.input.entity, reason: operation }
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
281
1728
|
if (this.disposed) {
|
|
282
1729
|
throw new Error(`RapierPhysicsWorld3D.${operation} cannot run on a disposed instance`);
|
|
283
1730
|
}
|
|
@@ -426,18 +1873,20 @@ var RapierPhysicsWorld3D = class {
|
|
|
426
1873
|
fullReconcilePhysicsState(state) {
|
|
427
1874
|
const descriptors = [];
|
|
428
1875
|
const transformlessStatic = /* @__PURE__ */ new Set();
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
1876
|
+
const committedDerived = this.derivedBodies.size > 0 ? this.captureDerivedPhysicsState() : void 0;
|
|
1877
|
+
for (const query of state.queries)
|
|
1878
|
+
for (const row of query) {
|
|
1879
|
+
if (!row.has(state.transformComponent)) {
|
|
1880
|
+
if (physicsRowIsStatic(row)) transformlessStatic.add(row.entity);
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
const descriptor = readPhysicsSyncDescriptor(
|
|
1884
|
+
row,
|
|
1885
|
+
state.transformComponent,
|
|
1886
|
+
state.globalTransformComponent
|
|
1887
|
+
);
|
|
1888
|
+
if (descriptor !== void 0) descriptors.push(descriptor);
|
|
433
1889
|
}
|
|
434
|
-
const descriptor = readPhysicsSyncDescriptor(
|
|
435
|
-
row,
|
|
436
|
-
state.transformComponent,
|
|
437
|
-
state.globalTransformComponent
|
|
438
|
-
);
|
|
439
|
-
if (descriptor !== void 0) descriptors.push(descriptor);
|
|
440
|
-
}
|
|
441
1890
|
this.resetForFullReconcile(transformlessStatic);
|
|
442
1891
|
for (const descriptor of descriptors) {
|
|
443
1892
|
this.ensureBody(
|
|
@@ -447,13 +1896,28 @@ var RapierPhysicsWorld3D = class {
|
|
|
447
1896
|
descriptor.collider
|
|
448
1897
|
);
|
|
449
1898
|
}
|
|
1899
|
+
if (committedDerived !== void 0) {
|
|
1900
|
+
const restorableBodies = committedDerived.bodies.filter(
|
|
1901
|
+
(body) => this.entityMap.has(body.entity) && !this.derivedBodies.has(body.entity)
|
|
1902
|
+
);
|
|
1903
|
+
if (restorableBodies.length > 0) {
|
|
1904
|
+
const restorable = Object.freeze({
|
|
1905
|
+
...committedDerived,
|
|
1906
|
+
bodies: Object.freeze(restorableBodies)
|
|
1907
|
+
});
|
|
1908
|
+
const restored = this.restoreDerivedPhysicsState(restorable);
|
|
1909
|
+
if (!restored.ok) {
|
|
1910
|
+
this.invalidateDerivedShapeCandidates("full-reconcile-restore-failed");
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
450
1914
|
drainPhysicsChangeQueries(state.changeQueries);
|
|
451
1915
|
state.structuralCursor = readStructuralEvidence(state.world, state.structuralCursor).cursor;
|
|
452
1916
|
state.structureEpoch = state.world.getStructureEpoch();
|
|
453
1917
|
state.initialized = true;
|
|
454
1918
|
}
|
|
455
1919
|
reconcilePhysicsDelta(state, entity, delta) {
|
|
456
|
-
const row = state.query.at(entity);
|
|
1920
|
+
const row = state.queries.map((query) => query.at(entity)).find((entry) => entry !== void 0);
|
|
457
1921
|
if (row === void 0) {
|
|
458
1922
|
this.removeEntity(entity);
|
|
459
1923
|
return;
|
|
@@ -476,7 +1940,11 @@ var RapierPhysicsWorld3D = class {
|
|
|
476
1940
|
return;
|
|
477
1941
|
}
|
|
478
1942
|
if (this.hasBody(entity) && (delta.colliderChanged || delta.rigidBodyChanged)) {
|
|
479
|
-
this.
|
|
1943
|
+
if (this.derivedBodies.has(entity)) {
|
|
1944
|
+
this.syncDerivedCompatibleEcsMutation(entity, descriptor);
|
|
1945
|
+
} else {
|
|
1946
|
+
this.removeEntity(entity);
|
|
1947
|
+
}
|
|
480
1948
|
}
|
|
481
1949
|
if (!this.hasBody(entity)) {
|
|
482
1950
|
this.ensureBody(entity, descriptor.transform, descriptor.rigidBody, descriptor.collider);
|
|
@@ -503,9 +1971,51 @@ var RapierPhysicsWorld3D = class {
|
|
|
503
1971
|
this.syncAuthoredPose(entity, descriptor.transform, descriptor.collider, "kinematic");
|
|
504
1972
|
}
|
|
505
1973
|
}
|
|
1974
|
+
syncDerivedCompatibleEcsMutation(entity, descriptor) {
|
|
1975
|
+
const body = this.bodyForEntity(entity);
|
|
1976
|
+
if (body === void 0) return;
|
|
1977
|
+
const bodyType = rigidBodyTypeFromF32(descriptor.rigidBody.type);
|
|
1978
|
+
const RAPIER = this.rapierModule;
|
|
1979
|
+
for (const collider of this.bodyColliders(body)) {
|
|
1980
|
+
if (!this.derivedColliderToShape.has(collider.handle)) {
|
|
1981
|
+
this.raw.removeCollider(collider, true);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const committed = this.derivedBodies.get(entity);
|
|
1985
|
+
const record = this.entityMap.get(entity);
|
|
1986
|
+
if (record !== void 0) record.authoredDensity = descriptor.collider?.density;
|
|
1987
|
+
if (descriptor.collider !== void 0) {
|
|
1988
|
+
this.createAuthoredCollider(body, descriptor.transform, {
|
|
1989
|
+
...descriptor.collider,
|
|
1990
|
+
density: committed?.massProperties?.mode === "explicit" ? 0 : descriptor.collider.density
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (bodyType === "static") {
|
|
1994
|
+
body.setBodyType(RAPIER.RigidBodyType.Fixed, true);
|
|
1995
|
+
this.syncAuthoredPose(entity, descriptor.transform, descriptor.collider, "static");
|
|
1996
|
+
} else if (bodyType === "kinematic") {
|
|
1997
|
+
body.setBodyType(RAPIER.RigidBodyType.KinematicPositionBased, true);
|
|
1998
|
+
this.syncAuthoredPose(entity, descriptor.transform, descriptor.collider, "kinematic");
|
|
1999
|
+
} else {
|
|
2000
|
+
body.setBodyType(RAPIER.RigidBodyType.Dynamic, true);
|
|
2001
|
+
if (record !== void 0) record.additionalMass = Math.max(0, descriptor.rigidBody.mass);
|
|
2002
|
+
if (committed?.massProperties?.mode !== "explicit") {
|
|
2003
|
+
this.restoreAutomaticMass(body, record?.additionalMass ?? 0);
|
|
2004
|
+
if (record !== void 0) record.automaticAdditionalMass = record.additionalMass;
|
|
2005
|
+
}
|
|
2006
|
+
body.setGravityScale(descriptor.rigidBody.gravityScale, true);
|
|
2007
|
+
body.setLinearDamping(descriptor.rigidBody.linearDamping);
|
|
2008
|
+
body.setAngularDamping(descriptor.rigidBody.angularDamping);
|
|
2009
|
+
}
|
|
2010
|
+
body.enableCcd(Boolean(descriptor.rigidBody.ccdEnabled));
|
|
2011
|
+
if (committed?.massProperties?.mode === "explicit") {
|
|
2012
|
+
this.restoreCommittedMass(body, committed, 0);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
506
2015
|
/** @internal ECS system bridge; consumers should register PhysicsSyncBackend. */
|
|
507
2016
|
_syncFromEcs(world, transformComponent, globalTransformComponent = world.components.resolve("GlobalTransform")) {
|
|
508
2017
|
this.assertActive("syncFromEcs");
|
|
2018
|
+
this.worldIdentity = world;
|
|
509
2019
|
if (globalTransformComponent === void 0) return;
|
|
510
2020
|
let state = this.syncState;
|
|
511
2021
|
if (state === void 0 || state.world !== world || state.transformComponent !== transformComponent || state.globalTransformComponent !== globalTransformComponent) {
|
|
@@ -520,11 +2030,17 @@ var RapierPhysicsWorld3D = class {
|
|
|
520
2030
|
]
|
|
521
2031
|
});
|
|
522
2032
|
if (!queryResult.ok) return;
|
|
2033
|
+
const bodyQuery = world.query({
|
|
2034
|
+
read: [RigidBody],
|
|
2035
|
+
without: [Collider],
|
|
2036
|
+
optional: [transformComponent, globalTransformComponent, CharacterController, ChildOf]
|
|
2037
|
+
});
|
|
2038
|
+
if (!bodyQuery.ok) throw bodyQuery.error;
|
|
523
2039
|
state = {
|
|
524
2040
|
world,
|
|
525
2041
|
transformComponent,
|
|
526
2042
|
globalTransformComponent,
|
|
527
|
-
|
|
2043
|
+
queries: [queryResult.value, bodyQuery.value],
|
|
528
2044
|
changeQueries: createPhysicsChangeQueries(world, [
|
|
529
2045
|
transformComponent,
|
|
530
2046
|
globalTransformComponent,
|
|
@@ -665,7 +2181,21 @@ var RapierPhysicsWorld3D = class {
|
|
|
665
2181
|
}
|
|
666
2182
|
}
|
|
667
2183
|
body.userData = entity;
|
|
668
|
-
this.registerBody(
|
|
2184
|
+
this.registerBody(
|
|
2185
|
+
entity,
|
|
2186
|
+
body.handle,
|
|
2187
|
+
rbType === "dynamic" ? Math.max(0, rigidBody.mass) : 0,
|
|
2188
|
+
collider?.density
|
|
2189
|
+
);
|
|
2190
|
+
if (collider === void 0) {
|
|
2191
|
+
const record = this.entityMap.get(entity);
|
|
2192
|
+
if (record !== void 0) record.automaticAdditionalMass = record.additionalMass;
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
this.createAuthoredCollider(body, transform, collider);
|
|
2196
|
+
}
|
|
2197
|
+
createAuthoredCollider(body, transform, collider) {
|
|
2198
|
+
const RAPIER = this.rapierModule;
|
|
669
2199
|
const scaleX = Math.abs(transform.scale.x);
|
|
670
2200
|
const scaleY = Math.abs(transform.scale.y);
|
|
671
2201
|
const scaleZ = Math.abs(transform.scale.z);
|
|
@@ -719,7 +2249,10 @@ var RapierPhysicsWorld3D = class {
|
|
|
719
2249
|
body.setNextKinematicTranslation(transform.position);
|
|
720
2250
|
body.setNextKinematicRotation(transform.rotation);
|
|
721
2251
|
}
|
|
722
|
-
|
|
2252
|
+
if (collider === void 0) return;
|
|
2253
|
+
const rapierCollider = this.bodyColliders(body).find(
|
|
2254
|
+
(shape) => !this.derivedColliderToShape.has(shape.handle)
|
|
2255
|
+
);
|
|
723
2256
|
if (!rapierCollider) return;
|
|
724
2257
|
const scaleX = Math.abs(transform.scale.x);
|
|
725
2258
|
const scaleY = Math.abs(transform.scale.y);
|
|
@@ -745,8 +2278,23 @@ var RapierPhysicsWorld3D = class {
|
|
|
745
2278
|
/**
|
|
746
2279
|
* Register an ECS entity with its Rapier body handle.
|
|
747
2280
|
*/
|
|
748
|
-
registerBody(entity, bodyHandle) {
|
|
749
|
-
this.entityMap.set(entity, {
|
|
2281
|
+
registerBody(entity, bodyHandle, additionalMass = 0, authoredDensity) {
|
|
2282
|
+
this.entityMap.set(entity, {
|
|
2283
|
+
bodyHandle,
|
|
2284
|
+
additionalMass: Math.max(0, additionalMass),
|
|
2285
|
+
// `ensureBody` registers before its authored collider is attached.
|
|
2286
|
+
// Rapier recomputes the body from that collider and clears the
|
|
2287
|
+
// descriptor-only additional mass, so the native baseline is zero.
|
|
2288
|
+
// A later automatic derived admission records the actual additional
|
|
2289
|
+
// contribution after it has been applied. Keeping this separate from
|
|
2290
|
+
// `additionalMass` lets rollback restore native state rather than an
|
|
2291
|
+
// authored value that Rapier has not applied yet.
|
|
2292
|
+
automaticAdditionalMass: 0,
|
|
2293
|
+
authoredDensity: authoredDensity !== void 0 && Number.isFinite(authoredDensity) && authoredDensity >= 0 ? authoredDensity : void 0
|
|
2294
|
+
});
|
|
2295
|
+
if (!this.derivedBodySources.has(entity)) {
|
|
2296
|
+
this.derivedBodySources.set(entity, { sourceKey: `entity:${entity}`, revision: 0 });
|
|
2297
|
+
}
|
|
750
2298
|
}
|
|
751
2299
|
/**
|
|
752
2300
|
* Apply all pending teleports to their respective bodies.
|
|
@@ -798,6 +2346,26 @@ var RapierPhysicsWorld3D = class {
|
|
|
798
2346
|
removeEntity(entity) {
|
|
799
2347
|
const record = this.entityMap.get(entity);
|
|
800
2348
|
if (!record) return;
|
|
2349
|
+
const derived = this.derivedBodies.get(entity);
|
|
2350
|
+
if (derived !== void 0) {
|
|
2351
|
+
for (const shape of derived.shapes) this.removeNativeCollider(shape.colliderHandle);
|
|
2352
|
+
this.derivedBodies.delete(entity);
|
|
2353
|
+
this.derivedPublications.delete(entity);
|
|
2354
|
+
}
|
|
2355
|
+
this.derivedFailures.delete(entity);
|
|
2356
|
+
this.derivedPoisonedEntities.delete(entity);
|
|
2357
|
+
for (const [id, constraint] of this.derivedConstraints) {
|
|
2358
|
+
if (constraint.input.bodyA === entity || constraint.input.bodyB === entity) {
|
|
2359
|
+
this.removeNativeConstraint(constraint.handle);
|
|
2360
|
+
this.derivedConstraints.delete(id);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
for (const [id, candidate] of this.derivedCandidates) {
|
|
2364
|
+
if (candidate.input.entity !== entity) continue;
|
|
2365
|
+
for (const collider of candidate.nativeColliders) this.removeNativeCollider(collider.handle);
|
|
2366
|
+
this.pendingDerivedCandidates.delete(id);
|
|
2367
|
+
this.releaseDerivedCandidate(id);
|
|
2368
|
+
}
|
|
801
2369
|
const ownPairs = [...this.collisionPairs.get(entity) ?? []];
|
|
802
2370
|
for (const other of ownPairs) {
|
|
803
2371
|
if (this.removePair(entity, other)) {
|
|
@@ -807,6 +2375,7 @@ var RapierPhysicsWorld3D = class {
|
|
|
807
2375
|
this.removeKccController(entity);
|
|
808
2376
|
this.raw.removeRigidBody({ handle: record.bodyHandle });
|
|
809
2377
|
this.entityMap.delete(entity);
|
|
2378
|
+
this.derivedBodySources.delete(entity);
|
|
810
2379
|
const own = this.collisionPairs.get(entity);
|
|
811
2380
|
if (own) {
|
|
812
2381
|
for (const other of own) this.collisionPairs.get(other)?.delete(entity);
|
|
@@ -833,8 +2402,9 @@ function physicsRowIsStatic(row) {
|
|
|
833
2402
|
function readPhysicsSyncDescriptor(row, transformComponent, globalTransformComponent) {
|
|
834
2403
|
const transformData = row.get(transformComponent);
|
|
835
2404
|
const globalTransformData = row.get(globalTransformComponent);
|
|
836
|
-
const colliderData = row.get(Collider);
|
|
837
|
-
if (transformData === void 0 || colliderData === void 0
|
|
2405
|
+
const colliderData = row.has(Collider) ? row.get(Collider) : void 0;
|
|
2406
|
+
if (transformData === void 0 || colliderData === void 0 && !row.has(RigidBody))
|
|
2407
|
+
return void 0;
|
|
838
2408
|
const useWorldPose = row.has(ChildOf) && hasReadableWorldPose(globalTransformData?.world);
|
|
839
2409
|
if (useWorldPose) {
|
|
840
2410
|
poseScratchWorld.set(globalTransformData.world);
|
|
@@ -888,7 +2458,7 @@ function readPhysicsSyncDescriptor(row, transformComponent, globalTransformCompo
|
|
|
888
2458
|
gravityScale: rigidBodyData.gravityScale,
|
|
889
2459
|
ccdEnabled: Number(rigidBodyData.ccdEnabled)
|
|
890
2460
|
},
|
|
891
|
-
collider: {
|
|
2461
|
+
collider: colliderData === void 0 ? void 0 : {
|
|
892
2462
|
shape: colliderData.shape,
|
|
893
2463
|
halfExtents: [
|
|
894
2464
|
colliderData.halfExtents[0] ?? 0,
|
|
@@ -984,6 +2554,7 @@ var PhysicsCollisionSync = defineSystem({
|
|
|
984
2554
|
return;
|
|
985
2555
|
}
|
|
986
2556
|
pw.writebackCollidingEntities(world, CollidingEntities);
|
|
2557
|
+
pw.finalizeDerivedFixedStep();
|
|
987
2558
|
}
|
|
988
2559
|
});
|
|
989
2560
|
function registerPhysicsSystems(world) {
|