@forgeax/engine-physics-rapier3d 0.1.28 → 0.1.30

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/index.mjs CHANGED
@@ -214,7 +214,9 @@ var RapierPhysicsWorld3D = class {
214
214
  }
215
215
  step(deltaTime) {
216
216
  this.assertActive("step");
217
+ if (!Number.isFinite(deltaTime) || deltaTime <= 0 || deltaTime > PHYSICS_DT_MAX) return;
217
218
  try {
219
+ this.raw.timestep = deltaTime;
218
220
  this.processDerivedCandidates();
219
221
  if (this.derivedPoisonedEntities.size > 0) return;
220
222
  this.raw.step(this.eventQueue);
@@ -287,6 +289,8 @@ var RapierPhysicsWorld3D = class {
287
289
  recordContactObservation(observation, handleA, handleB) {
288
290
  let point;
289
291
  let normal;
292
+ let geometricPoint;
293
+ let geometricNormal;
290
294
  try {
291
295
  const colliderA = this.raw.getCollider(handleA);
292
296
  const colliderB = this.raw.getCollider(handleB);
@@ -295,6 +299,34 @@ var RapierPhysicsWorld3D = class {
295
299
  colliderA,
296
300
  colliderB,
297
301
  (manifold, flipped) => {
302
+ if (manifold.numSolverContacts() === 0 && geometricPoint === void 0 && manifold.numContacts() > 0) {
303
+ const first = flipped ? colliderB : colliderA;
304
+ const second = flipped ? colliderA : colliderB;
305
+ const types = this.rapierModule.ShapeType;
306
+ for (const [collider, local] of [
307
+ [first, manifold.localContactPoint1(0)],
308
+ [second, manifold.localContactPoint2(0)]
309
+ ]) {
310
+ if (local == null || ![types.Ball, types.Cuboid, types.Capsule].includes(collider.shapeType()))
311
+ continue;
312
+ const rotation = collider.rotation();
313
+ const rotated = quat.transformVec3(
314
+ vec3.create(),
315
+ [rotation.x, rotation.y, rotation.z, rotation.w],
316
+ [local.x, local.y, local.z]
317
+ );
318
+ const position = collider.translation();
319
+ geometricPoint = [
320
+ rotated[0] + position.x,
321
+ rotated[1] + position.y,
322
+ rotated[2] + position.z
323
+ ];
324
+ const n = manifold.normal();
325
+ const direction = flipped ? -1 : 1;
326
+ geometricNormal = [n.x * direction, n.y * direction, n.z * direction];
327
+ break;
328
+ }
329
+ }
298
330
  if (manifold.numSolverContacts?.() > 0) {
299
331
  const contact = manifold.solverContactPoint(0);
300
332
  const n = manifold.normal();
@@ -311,6 +343,10 @@ var RapierPhysicsWorld3D = class {
311
343
  }
312
344
  } catch {
313
345
  }
346
+ if (point === void 0 && geometricPoint !== void 0) {
347
+ point = geometricPoint;
348
+ normal = geometricNormal;
349
+ }
314
350
  this.derivedContacts.push({
315
351
  ...observation,
316
352
  ...point === void 0 ? {} : { point },
@@ -538,17 +574,72 @@ var RapierPhysicsWorld3D = class {
538
574
  }
539
575
  return admitted;
540
576
  }
577
+ admitDerivedShapeCandidates(candidates, commitGeometry) {
578
+ this.assertActive("admitDerivedShapeCandidates");
579
+ const records = [];
580
+ const sources = /* @__PURE__ */ new Map();
581
+ const constraints = /* @__PURE__ */ new Set();
582
+ const invalid = (reason) => new DerivedPhysicsError(
583
+ "derived-candidate-invalid",
584
+ "one bounded admission contains distinct prepared bodies and constraint updates",
585
+ "prepare one candidate per body and submit the complete replacement together",
586
+ { reason }
587
+ );
588
+ if (candidates.length === 0 || candidates.length > DERIVED_PHYSICS_LIMITS.maxCandidates)
589
+ return err(invalid("invalid batch size"));
590
+ for (const candidate of candidates) {
591
+ const record = this.derivedCandidates.get(candidate.candidateId);
592
+ if (record === void 0 || candidate.owner !== this.physicsOwner || candidate.generation !== this.backendGeneration || record.state !== "ready")
593
+ return err(invalid("batch member is not a current prepared candidate"));
594
+ if (sources.has(record.input.entity)) return err(invalid("duplicate body"));
595
+ for (const constraint of record.input.constraints ?? []) {
596
+ if (constraints.has(constraint.id)) return err(invalid("duplicate constraint update"));
597
+ constraints.add(constraint.id);
598
+ }
599
+ records.push(record);
600
+ sources.set(record.input.entity, {
601
+ sourceKey: record.input.sourceKey,
602
+ revision: record.input.revision
603
+ });
604
+ }
605
+ for (const candidate of candidates) {
606
+ const checked = this.admitDerivedShapeCandidateInternal(candidate, sources, true);
607
+ if (!checked.ok) {
608
+ for (const record of records) {
609
+ if (this.derivedCandidates.has(record.token.candidateId))
610
+ this.rejectPreparedCandidate(record, checked.error);
611
+ }
612
+ return checked;
613
+ }
614
+ }
615
+ const batch = Object.freeze({
616
+ records: Object.freeze(records),
617
+ ...commitGeometry === void 0 ? {} : { commitGeometry }
618
+ });
619
+ for (const record of records) record.batch = batch;
620
+ const queued = [];
621
+ for (const candidate of candidates) {
622
+ const admitted = this.admitDerivedShapeCandidateInternal(candidate, sources);
623
+ if (!admitted.ok) {
624
+ const first = records[0];
625
+ if (first !== void 0) this.rejectPreparedCandidate(first, admitted.error);
626
+ return admitted;
627
+ }
628
+ queued.push(admitted.value);
629
+ }
630
+ return ok(Object.freeze(queued));
631
+ }
541
632
  getDerivedAdmission(entity) {
542
- const record = this.activeDerivedAdmission;
543
- if (record === void 0 || entity !== void 0 && record.input.entity !== entity)
544
- return void 0;
633
+ const active = this.activeDerivedAdmission;
634
+ const record = entity === void 0 ? active : active?.batch?.records.find((item) => item.input.entity === entity) ?? (active?.input.entity === entity ? active : void 0);
635
+ if (record === void 0) return void 0;
545
636
  return {
546
637
  entity: record.input.entity,
547
638
  revision: record.input.revision,
548
639
  fixedStep: this.syncState?.world.getResource(FixedTime).tick ?? this.fixedStep + 1
549
640
  };
550
641
  }
551
- admitDerivedShapeCandidateInternal(candidate, sourceOverrides) {
642
+ admitDerivedShapeCandidateInternal(candidate, sourceOverrides, validateOnly = false) {
552
643
  this.assertActive("admitDerivedShapeCandidate");
553
644
  const record = this.derivedCandidates.get(candidate.candidateId);
554
645
  if (record === void 0 || candidate.owner !== this.physicsOwner || record.token.owner !== candidate.owner || candidate.generation !== this.backendGeneration) {
@@ -631,6 +722,7 @@ var RapierPhysicsWorld3D = class {
631
722
  this.rejectPreparedCandidate(record, stale);
632
723
  return err(stale);
633
724
  }
725
+ if (validateOnly) return ok(record.token);
634
726
  record.state = "queued";
635
727
  this.pendingDerivedCandidates.add(candidate.candidateId);
636
728
  const queued = Object.freeze({ ...record.token, state: "queued" });
@@ -692,10 +784,12 @@ var RapierPhysicsWorld3D = class {
692
784
  )
693
785
  );
694
786
  }
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);
787
+ for (const member of record.batch?.records ?? [record]) {
788
+ this.pendingDerivedCandidates.delete(member.token.candidateId);
789
+ for (const collider of member.nativeColliders) this.removeNativeCollider(collider.handle);
790
+ member.state = "cancelled";
791
+ this.releaseDerivedCandidate(member.token.candidateId);
792
+ }
699
793
  return ok(void 0);
700
794
  }
701
795
  /** Reject all in-flight derived work while preserving the last publication. */
@@ -742,12 +836,68 @@ var RapierPhysicsWorld3D = class {
742
836
  const com = body.worldCom();
743
837
  const linear = body.linvel();
744
838
  const angular = body.angvel();
839
+ const rotation = body.rotation();
745
840
  return Object.freeze({
746
841
  centerOfMass: [com.x, com.y, com.z],
747
842
  linearVelocity: [linear.x, linear.y, linear.z],
748
- angularVelocity: [angular.x, angular.y, angular.z]
843
+ angularVelocity: [angular.x, angular.y, angular.z],
844
+ rotation: [rotation.x, rotation.y, rotation.z, rotation.w]
749
845
  });
750
846
  }
847
+ applyDerivedImpulse(input) {
848
+ this.assertActive("applyDerivedImpulse");
849
+ const refuse = (code, expected) => err(
850
+ new DerivedPhysicsError(
851
+ code,
852
+ expected,
853
+ "read the committed dynamic body and submit a finite impulse outside pending admission",
854
+ { entity: input.entity }
855
+ )
856
+ );
857
+ if (this.recoveryBlocked())
858
+ return refuse("derived-recovery-invalid", "PhysicsWorld is healthy");
859
+ const record = this.derivedBodies.get(input.entity);
860
+ const body = this.bodyForEntity(input.entity);
861
+ if (record === void 0 || body === void 0)
862
+ return refuse("derived-body-not-found", "a committed derived body exists");
863
+ if (record.sourceKey !== input.sourceKey || record.revision !== input.revision)
864
+ return refuse(
865
+ "derived-candidate-stale",
866
+ "impulse identity matches the committed body revision"
867
+ );
868
+ if (this.derivedPublicationPending || [...this.pendingDerivedCandidates].some(
869
+ (id) => this.derivedCandidates.get(id)?.input.entity === input.entity
870
+ ))
871
+ return refuse("derived-candidate-pending", "the body has no unpublished native mutation");
872
+ if (rapierBodyTypeToString(this.rapierModule, body.bodyType()) !== "dynamic" || ![input.impulse, input.point].every(
873
+ (vector) => Array.isArray(vector) && vector.length === 3 && vector.every((value) => Number.isFinite(value) && Number.isFinite(Math.fround(value)))
874
+ ))
875
+ return refuse(
876
+ "derived-candidate-invalid",
877
+ "a dynamic body receives finite Float32 world vectors"
878
+ );
879
+ try {
880
+ body.applyImpulseAtPoint(
881
+ { x: input.impulse[0], y: input.impulse[1], z: input.impulse[2] },
882
+ { x: input.point[0], y: input.point[1], z: input.point[2] },
883
+ true
884
+ );
885
+ const linear = body.linvel(), angular = body.angvel();
886
+ if (![linear.x, linear.y, linear.z, angular.x, angular.y, angular.z].every(Number.isFinite))
887
+ throw new Error("native impulse produced non-finite motion");
888
+ return ok(void 0);
889
+ } catch (cause) {
890
+ this.derivedPoisonedEntities.add(input.entity);
891
+ return err(
892
+ new DerivedPhysicsError(
893
+ "derived-backend-failed",
894
+ "native impulse completes with finite motion",
895
+ "rebuild the World from a previously committed snapshot",
896
+ { entity: input.entity, reason: cause instanceof Error ? cause.message : String(cause) }
897
+ )
898
+ );
899
+ }
900
+ }
751
901
  getDerivedRecoveryState() {
752
902
  return this.recoveryBlocked() ? "rebuild-required" : "ready";
753
903
  }
@@ -844,16 +994,8 @@ var RapierPhysicsWorld3D = class {
844
994
  }
845
995
  preparedCandidates.push(prepared.value);
846
996
  }
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);
997
+ if (preparedCandidates.length === 0) return ok([]);
998
+ return this.admitDerivedShapeCandidates(preparedCandidates);
857
999
  }
858
1000
  createDerivedConstraint(input) {
859
1001
  return this.installDerivedConstraint(input, false);
@@ -969,8 +1111,11 @@ var RapierPhysicsWorld3D = class {
969
1111
  this.releaseDerivedCandidate(record.token.candidateId);
970
1112
  }
971
1113
  rejectPreparedCandidate(record, error) {
972
- for (const collider of record.nativeColliders) this.removeNativeCollider(collider.handle);
973
- this.rememberDerivedFailure(record, error, "old-state-retained");
1114
+ for (const member of record.batch?.records ?? [record]) {
1115
+ if (!this.derivedCandidates.has(member.token.candidateId)) continue;
1116
+ for (const collider of member.nativeColliders) this.removeNativeCollider(collider.handle);
1117
+ this.rememberDerivedFailure(member, error, "old-state-retained");
1118
+ }
974
1119
  }
975
1120
  processDerivedCandidates() {
976
1121
  if (this.pendingDerivedCandidates.size === 0) return;
@@ -986,329 +1131,358 @@ var RapierPhysicsWorld3D = class {
986
1131
  const visiting = /* @__PURE__ */ new Set();
987
1132
  const visited = /* @__PURE__ */ new Set();
988
1133
  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) {
1134
+ const group = record.batch?.records ?? [record];
1135
+ const key = group[0]?.token.candidateId ?? record.token.candidateId;
1136
+ if (visited.has(key)) return;
1137
+ if (visiting.has(key)) {
1015
1138
  this.rejectPreparedCandidate(
1016
1139
  record,
1017
1140
  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
- }
1141
+ "derived-constraint-invalid",
1142
+ "cyclic endpoint updates share one batch",
1143
+ "submit mutually dependent body replacements together",
1144
+ { candidateId: key }
1027
1145
  )
1028
1146
  );
1029
- continue;
1147
+ return;
1030
1148
  }
1031
- const admissionError = this.validateDerivedAdmission(record.input, pendingSources);
1032
- if (admissionError !== void 0) {
1033
- this.rejectPreparedCandidate(record, admissionError);
1034
- continue;
1149
+ visiting.add(key);
1150
+ for (const member of group) {
1151
+ for (const constraint of member.input.constraints ?? []) {
1152
+ for (const [entity, dependency] of [
1153
+ [constraint.bodyA, constraint.bodyASource],
1154
+ [constraint.bodyB, constraint.bodyBSource]
1155
+ ]) {
1156
+ const target = pendingByEntity.get(entity);
1157
+ if (target !== void 0 && !group.includes(target) && target.input.sourceKey === dependency.sourceKey && target.input.revision === dependency.revision)
1158
+ visit(target);
1159
+ }
1160
+ }
1035
1161
  }
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
- );
1162
+ visiting.delete(key);
1163
+ visited.add(key);
1164
+ ordered.push(group);
1165
+ };
1166
+ for (const record of pendingRecords) visit(record);
1167
+ for (const group of ordered) {
1168
+ if (this.derivedPoisonedEntities.size > 0) break;
1169
+ const first = group[0];
1170
+ if (first === void 0) continue;
1171
+ if (group.some(
1172
+ (record) => record.state !== "queued" || !this.pendingDerivedCandidates.has(record.token.candidateId)
1173
+ ))
1049
1174
  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(
1175
+ const sources = this.pendingDerivedSources();
1176
+ let rejected;
1177
+ for (const record of group) {
1178
+ const projected = sources.get(record.input.entity);
1179
+ if (projected !== void 0 && record.input.revision < projected.revision) {
1180
+ rejected = new DerivedPhysicsError(
1181
+ "derived-candidate-stale",
1182
+ "fixed-step admission publishes the final queued body revisions",
1183
+ "discard the older group and prepare a complete replacement",
1184
+ { entity: record.input.entity }
1185
+ );
1186
+ } else if (this.derivedPoisonedEntities.has(record.input.entity)) {
1187
+ rejected = new DerivedPhysicsError(
1188
+ "derived-recovery-invalid",
1189
+ "every batch body has recoverable native state",
1190
+ "rebuild the PhysicsWorld before retrying",
1191
+ { entity: record.input.entity }
1192
+ );
1193
+ } else if (this.bodyForEntity(record.input.entity) === void 0) {
1194
+ rejected = new DerivedPhysicsError(
1057
1195
  "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
- );
1196
+ "every batch body remains live through admission",
1197
+ "reconcile entities and prepare again",
1198
+ { entity: record.input.entity }
1199
+ );
1200
+ } else rejected = this.validateDerivedAdmission(record.input, sources);
1201
+ if (rejected !== void 0) break;
1202
+ }
1203
+ if (rejected !== void 0) {
1204
+ this.rejectPreparedCandidate(first, rejected);
1064
1205
  continue;
1065
1206
  }
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}`);
1207
+ const undos = [];
1208
+ for (const record of group) {
1209
+ const id = record.token.candidateId;
1210
+ const old = this.derivedBodies.get(record.input.entity);
1211
+ const body = this.bodyForEntity(record.input.entity);
1212
+ const oldBodyType = body.bodyType();
1213
+ const oldBodyEnabled = body.isEnabled();
1214
+ const oldVelocity = body.linvel();
1215
+ const oldAngularVelocity = body.angvel();
1216
+ const oldTranslation = body.translation();
1217
+ const oldRotation = body.rotation();
1218
+ const oldCom = body.worldCom();
1219
+ const oldMass = body.mass();
1220
+ const oldAutomaticAdditionalMass = this.entityMap.get(record.input.entity)?.automaticAdditionalMass ?? 0;
1221
+ const oldAutomaticRecordMass = this.entityMap.get(
1222
+ record.input.entity
1223
+ )?.automaticAdditionalMass;
1224
+ const oldSource = this.derivedBodySources.get(record.input.entity);
1225
+ const oldPublication = this.derivedPublications.get(record.input.entity);
1226
+ const oldDensities = this.bodyColliders(body).map((collider) => ({
1227
+ collider,
1228
+ density: typeof collider.density === "function" ? collider.density() : void 0,
1229
+ enabled: typeof collider.isEnabled === "function" ? collider.isEnabled() : true
1230
+ }));
1231
+ const oldConstraints = new Map(this.derivedConstraints);
1232
+ const stagedConstraints = /* @__PURE__ */ new Map();
1233
+ let geometryCommitUncertain = false;
1234
+ const rollback = () => {
1235
+ for (const staged of stagedConstraints.values())
1236
+ this.removeNativeConstraint(staged.handle);
1237
+ for (const current of this.derivedConstraints.values())
1238
+ this.removeNativeConstraint(current.handle);
1239
+ this.derivedConstraints.clear();
1240
+ if (old !== void 0) {
1241
+ for (const shape of old.shapes) {
1242
+ const collider = this.raw.getCollider(shape.colliderHandle);
1243
+ if (collider !== null && collider !== void 0) collider.setEnabled(true);
1244
+ }
1245
+ const retiredIndex = this.retiredDerivedBodies.indexOf(old);
1246
+ if (retiredIndex >= 0) this.retiredDerivedBodies.splice(retiredIndex, 1);
1247
+ this.derivedBodies.set(record.input.entity, old);
1248
+ this.derivedBodySources.set(record.input.entity, {
1249
+ sourceKey: old.sourceKey,
1250
+ revision: old.revision
1251
+ });
1121
1252
  }
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);
1253
+ for (const native of record.nativeColliders) {
1254
+ const collider = this.raw.getCollider(native.handle);
1180
1255
  if (collider !== null && collider !== void 0) collider.setEnabled(false);
1181
1256
  }
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 ?? []]
1257
+ for (const native of record.nativeColliders) this.removeNativeCollider(native.handle);
1258
+ for (const { collider, density, enabled } of oldDensities) {
1259
+ if (this.raw.getCollider(collider.handle) === null) continue;
1260
+ if (density !== void 0 && typeof collider.setDensity === "function")
1261
+ collider.setDensity(density);
1262
+ if (typeof collider.setEnabled === "function") collider.setEnabled(enabled);
1263
+ }
1264
+ let restored = true;
1265
+ try {
1266
+ body.setBodyType(oldBodyType, true);
1267
+ this.restoreCommittedMass(body, old, oldAutomaticAdditionalMass);
1268
+ body.setTranslation(oldTranslation, true);
1269
+ body.setRotation(oldRotation, true);
1270
+ body.setLinvel(oldVelocity, true);
1271
+ body.setAngvel(oldAngularVelocity, true);
1272
+ body.setEnabled(oldBodyEnabled);
1273
+ } catch {
1274
+ restored = false;
1275
+ }
1276
+ if (restored && !this.restoreNativeConstraints(oldConstraints)) restored = false;
1277
+ if (restored) {
1278
+ const currentMass = body.mass();
1279
+ const currentCom = body.worldCom();
1280
+ const currentVelocity = body.linvel();
1281
+ const currentAngularVelocity = body.angvel();
1282
+ const currentRotation = body.rotation();
1283
+ 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 && Math.abs(
1284
+ currentRotation.x * oldRotation.x + currentRotation.y * oldRotation.y + currentRotation.z * oldRotation.z + currentRotation.w * oldRotation.w
1285
+ ) >= 1 - 1e-6 && body.bodyType() === oldBodyType && body.isEnabled() === oldBodyEnabled;
1286
+ }
1287
+ if (old === void 0) this.derivedBodies.delete(record.input.entity);
1288
+ else this.derivedBodies.set(record.input.entity, old);
1289
+ if (oldSource === void 0) this.derivedBodySources.delete(record.input.entity);
1290
+ else this.derivedBodySources.set(record.input.entity, oldSource);
1291
+ if (oldPublication === void 0) this.derivedPublications.delete(record.input.entity);
1292
+ else this.derivedPublications.set(record.input.entity, oldPublication);
1293
+ const entityRecord = this.entityMap.get(record.input.entity);
1294
+ if (entityRecord !== void 0 && oldAutomaticRecordMass !== void 0)
1295
+ entityRecord.automaticAdditionalMass = oldAutomaticRecordMass;
1296
+ return restored;
1208
1297
  };
1209
- if (record.commitGeometry !== void 0) {
1210
- this.activeDerivedAdmission = record;
1298
+ const restore = (undo) => {
1211
1299
  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;
1300
+ return undo();
1301
+ } catch {
1302
+ return false;
1218
1303
  }
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);
1304
+ };
1305
+ try {
1306
+ for (const constraint of record.input.constraints ?? []) {
1307
+ const created = this.createNativeConstraint(constraint);
1308
+ if (!created.ok) throw created.error;
1309
+ stagedConstraints.set(constraint.id, {
1310
+ input: { ...constraint },
1311
+ handle: created.value.handle
1312
+ });
1242
1313
  }
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
1314
+ if (record.input.bodyType !== void 0) {
1315
+ const RAPIER = this.rapierModule;
1316
+ const bodyType = record.input.bodyType === "static" ? RAPIER.RigidBodyType.Fixed : record.input.bodyType === "kinematic" ? RAPIER.RigidBodyType.KinematicPositionBased : RAPIER.RigidBodyType.Dynamic;
1317
+ body.setBodyType(bodyType, true);
1318
+ }
1319
+ for (const native of record.nativeColliders) {
1320
+ const collider = this.raw.getCollider(native.handle);
1321
+ if (collider === null || collider === void 0)
1322
+ throw new Error("candidate collider disappeared");
1323
+ collider.setEnabled(true);
1324
+ }
1325
+ const nativeById = new Map(
1326
+ record.input.shapes.map((shape, index) => [
1327
+ shape.id,
1328
+ record.nativeColliders[index]?.handle
1329
+ ])
1330
+ );
1331
+ for (const seam of record.input.seams ?? []) {
1332
+ const firstHandle = nativeById.get(seam.shapeA);
1333
+ const secondHandle = nativeById.get(seam.shapeB);
1334
+ const first2 = firstHandle === void 0 ? void 0 : this.raw.getCollider(firstHandle);
1335
+ const second = secondHandle === void 0 ? void 0 : this.raw.getCollider(secondHandle);
1336
+ if (first2 === void 0 || second === void 0 || first2 === null || second === null) {
1337
+ throw new Error(`derived seam references missing shape ${seam.shapeA}`);
1338
+ }
1339
+ first2.combineVoxelStates(second, seam.offset[0], seam.offset[1], seam.offset[2]);
1340
+ }
1341
+ if (record.input.motion?.rotation !== void 0) {
1342
+ const [x, y, z, w] = record.input.motion.rotation;
1343
+ body.setRotation({ x, y, z, w }, true);
1344
+ }
1345
+ this.applyDerivedMass(
1346
+ body,
1347
+ record.input.massProperties,
1348
+ oldCom,
1349
+ record.input.velocityPolicy ?? "preserve",
1350
+ this.entityMap.get(record.input.entity)?.additionalMass ?? 0,
1351
+ record.input.entity,
1352
+ record.input.shapes,
1353
+ record.nativeColliders
1354
+ );
1355
+ if (record.input.motion !== void 0) {
1356
+ const currentCom = body.worldCom();
1357
+ const targetCom = record.input.motion.centerOfMass;
1358
+ const translation = body.translation();
1359
+ body.setTranslation(
1360
+ {
1361
+ x: translation.x + targetCom[0] - currentCom.x,
1362
+ y: translation.y + targetCom[1] - currentCom.y,
1363
+ z: translation.z + targetCom[2] - currentCom.z
1364
+ },
1365
+ true
1366
+ );
1367
+ body.setLinvel(
1368
+ {
1369
+ x: record.input.motion.linearVelocity[0],
1370
+ y: record.input.motion.linearVelocity[1],
1371
+ z: record.input.motion.linearVelocity[2]
1372
+ },
1373
+ true
1374
+ );
1375
+ body.setAngvel(
1376
+ {
1377
+ x: record.input.motion.angularVelocity[0],
1378
+ y: record.input.motion.angularVelocity[1],
1379
+ z: record.input.motion.angularVelocity[2]
1380
+ },
1381
+ true
1382
+ );
1383
+ }
1384
+ const committedSources = new Map(this.derivedBodySources);
1385
+ committedSources.set(record.input.entity, {
1386
+ sourceKey: record.input.sourceKey,
1387
+ revision: record.input.revision
1249
1388
  });
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
- {
1389
+ const replacementConstraintIds = new Set(
1390
+ (record.input.constraints ?? []).map((constraint) => constraint.id)
1391
+ );
1392
+ for (const [constraintId, current] of [...this.derivedConstraints]) {
1393
+ if (replacementConstraintIds.has(constraintId) || this.constraintDependenciesMatch(current.input, committedSources))
1394
+ continue;
1395
+ this.removeNativeConstraint(current.handle);
1396
+ this.derivedConstraints.delete(constraintId);
1397
+ }
1398
+ if (old !== void 0) {
1399
+ for (const shape of old.shapes) {
1400
+ const collider = this.raw.getCollider(shape.colliderHandle);
1401
+ if (collider !== null && collider !== void 0) collider.setEnabled(false);
1402
+ }
1403
+ this.retiredDerivedBodies.push(old);
1404
+ }
1405
+ for (const constraint of record.input.constraints ?? []) {
1406
+ const previous = this.derivedConstraints.get(constraint.id);
1407
+ if (previous !== void 0) this.removeNativeConstraint(previous.handle);
1408
+ const staged = stagedConstraints.get(constraint.id);
1409
+ if (staged !== void 0) this.derivedConstraints.set(constraint.id, staged);
1410
+ }
1411
+ const shapes = record.input.shapes.map(
1412
+ (shape, index) => ({
1413
+ input: shape,
1414
+ colliderHandle: record.nativeColliders[index]?.handle
1415
+ })
1416
+ );
1417
+ const committed = {
1296
1418
  entity: record.input.entity,
1419
+ sourceKey: record.input.sourceKey,
1420
+ generation: this.backendGeneration,
1421
+ revision: record.input.revision,
1422
+ bodyType: record.input.bodyType,
1423
+ velocityPolicy: record.input.velocityPolicy,
1297
1424
  candidateId: record.token.candidateId,
1298
- reason: cause instanceof Error ? cause.message : String(cause)
1425
+ shapes,
1426
+ seams: [...record.input.seams ?? []],
1427
+ massProperties: record.input.massProperties,
1428
+ constraints: [...record.input.constraints ?? []]
1429
+ };
1430
+ const commitGeometry = record === group[group.length - 1] ? record.batch?.commitGeometry ?? record.commitGeometry : void 0;
1431
+ if (commitGeometry !== void 0) {
1432
+ this.activeDerivedAdmission = record;
1433
+ try {
1434
+ geometryCommitUncertain = true;
1435
+ const geometry = commitGeometry();
1436
+ geometryCommitUncertain = false;
1437
+ if (!geometry.ok) throw geometry.error;
1438
+ } finally {
1439
+ this.activeDerivedAdmission = void 0;
1440
+ }
1441
+ delete record.commitGeometry;
1299
1442
  }
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);
1443
+ this.derivedBodies.set(record.input.entity, committed);
1444
+ const entityRecord = this.entityMap.get(record.input.entity);
1445
+ if (entityRecord !== void 0) {
1446
+ entityRecord.automaticAdditionalMass = record.input.massProperties?.mode === "explicit" ? 0 : entityRecord.additionalMass;
1447
+ }
1448
+ this.derivedBodySources.set(record.input.entity, {
1449
+ sourceKey: record.input.sourceKey,
1450
+ revision: record.input.revision
1451
+ });
1452
+ this.derivedFailures.delete(record.input.entity);
1453
+ record.state = "queued";
1454
+ this.pendingDerivedCandidates.delete(id);
1455
+ undos.push(rollback);
1456
+ } catch (cause) {
1457
+ let restored = restore(rollback);
1458
+ for (const undo of undos.reverse()) {
1459
+ if (!restore(undo)) restored = false;
1460
+ }
1461
+ const error = cause instanceof DerivedPhysicsError ? cause : new DerivedPhysicsError(
1462
+ "derived-backend-failed",
1463
+ "derived admission either commits completely or preserves the prior body state",
1464
+ "inspect the failure receipt and rebuild the PhysicsWorld if recovery is required",
1465
+ {
1466
+ entity: record.input.entity,
1467
+ candidateId: record.token.candidateId,
1468
+ reason: cause instanceof Error ? cause.message : String(cause)
1469
+ }
1470
+ );
1471
+ if (geometryCommitUncertain) restored = false;
1472
+ for (const member of group) {
1473
+ if (!restored) {
1474
+ this.derivedPoisonedEntities.add(member.input.entity);
1475
+ this.derivedBodies.delete(member.input.entity);
1476
+ this.derivedPublications.delete(member.input.entity);
1477
+ }
1478
+ this.rememberDerivedFailure(
1479
+ member,
1480
+ error,
1481
+ restored ? "old-state-retained" : "rebuild-required"
1482
+ );
1483
+ }
1484
+ break;
1306
1485
  }
1307
- this.rememberDerivedFailure(
1308
- record,
1309
- error,
1310
- restored ? "old-state-retained" : "rebuild-required"
1311
- );
1312
1486
  }
1313
1487
  }
1314
1488
  }
@@ -1328,6 +1502,7 @@ var RapierPhysicsWorld3D = class {
1328
1502
  })
1329
1503
  );
1330
1504
  candidate.state = "published";
1505
+ delete candidate.batch;
1331
1506
  }
1332
1507
  }
1333
1508
  retireDerivedBodies() {
@@ -1360,6 +1535,7 @@ var RapierPhysicsWorld3D = class {
1360
1535
  { x: frame[0], y: frame[1], z: frame[2], w: frame[3] },
1361
1536
  true
1362
1537
  );
1538
+ body.recomputeMassPropertiesFromColliders();
1363
1539
  } else {
1364
1540
  this.restoreAutomaticDensities(
1365
1541
  entity,
@@ -1404,6 +1580,7 @@ var RapierPhysicsWorld3D = class {
1404
1580
  { x: frame[0], y: frame[1], z: frame[2], w: frame[3] },
1405
1581
  true
1406
1582
  );
1583
+ body.recomputeMassPropertiesFromColliders();
1407
1584
  return;
1408
1585
  }
1409
1586
  this.restoreAutomaticMass(body, automaticAdditionalMass);
@@ -2189,6 +2366,7 @@ var RapierPhysicsWorld3D = class {
2189
2366
  );
2190
2367
  if (collider === void 0) {
2191
2368
  const record = this.entityMap.get(entity);
2369
+ this.restoreAutomaticMass(body, record?.additionalMass ?? 0);
2192
2370
  if (record !== void 0) record.automaticAdditionalMass = record.additionalMass;
2193
2371
  return;
2194
2372
  }
@@ -2346,6 +2524,19 @@ var RapierPhysicsWorld3D = class {
2346
2524
  removeEntity(entity) {
2347
2525
  const record = this.entityMap.get(entity);
2348
2526
  if (!record) return;
2527
+ for (const candidate of this.derivedCandidates.values()) {
2528
+ if (candidate.input.entity !== entity || candidate.batch === void 0 || candidate.state !== "ready" && candidate.state !== "queued")
2529
+ continue;
2530
+ this.rejectPreparedCandidate(
2531
+ candidate,
2532
+ new DerivedPhysicsError(
2533
+ "derived-body-not-found",
2534
+ "all grouped bodies remain live until admission",
2535
+ "prepare a new complete group after entity reconciliation",
2536
+ { entity }
2537
+ )
2538
+ );
2539
+ }
2349
2540
  const derived = this.derivedBodies.get(entity);
2350
2541
  if (derived !== void 0) {
2351
2542
  for (const shape of derived.shapes) this.removeNativeCollider(shape.colliderHandle);