From c27d11502ef73f09b76d066591f69234163ab015 Mon Sep 17 00:00:00 2001
From: yangfengzzz <yangfengzzz@hotmail.com>
Date: Sat, 28 Jan 2023 12:19:59 +0800
Subject: [PATCH] feat: add spherical joint

---
 .../core/src/physics/joint/SphericalJoint.ts  | 86 ++++++++++++++++
 packages/core/src/physics/joint/index.ts      |  1 +
 packages/design/src/physics/IPhysics.ts       |  8 +-
 .../src/physics/joints/ISphericalJoint.ts     | 24 +++++
 packages/design/src/physics/joints/index.ts   |  1 +
 packages/physics-lite/src/LitePhysics.ts      | 14 ++-
 packages/physics-physx/src/PhysXPhysics.ts    |  9 ++
 .../src/joint/PhysXSphericalJoint.ts          | 97 +++++++++++++++++++
 8 files changed, 236 insertions(+), 4 deletions(-)
 create mode 100644 packages/core/src/physics/joint/SphericalJoint.ts
 create mode 100644 packages/design/src/physics/joints/ISphericalJoint.ts
 create mode 100644 packages/physics-physx/src/joint/PhysXSphericalJoint.ts

diff --git a/packages/core/src/physics/joint/SphericalJoint.ts b/packages/core/src/physics/joint/SphericalJoint.ts
new file mode 100644
index 0000000000..9a0baca25c
--- /dev/null
+++ b/packages/core/src/physics/joint/SphericalJoint.ts
@@ -0,0 +1,86 @@
+import { Joint } from "./Joint";
+import { ISphericalJoint } from "@oasis-engine/design";
+import { Collider } from "../Collider";
+import { PhysicsManager } from "../PhysicsManager";
+
+/**
+ * A joint which behaves in a similar way to a ball and socket.
+ */
+export class SphericalJoint extends Joint {
+  private _yLimit = Math.PI / 2;
+  private _zLimit = Math.PI / 2;
+  private _contactDistance = -1;
+  private _stiffness = 0;
+  private _damping = 0;
+  private _enableSpring = false;
+
+  /** Whether enable spring limit */
+  get enableSpring(): boolean {
+    return this._enableSpring;
+  }
+
+  set enableSpring(value: boolean) {
+    this._enableSpring = value;
+    (<ISphericalJoint>this._nativeJoint).enableSpring(value);
+  }
+
+  /** The limit angle from the Y-axis of the constraint frame. */
+  get yLimit(): number {
+    return this._yLimit;
+  }
+
+  set yLimit(value: number) {
+    this._yLimit = value;
+    (<ISphericalJoint>this._nativeJoint).setYLimit(value);
+  }
+
+  /** The limit angle from the Z-axis of the constraint frame. */
+  get zLimit(): number {
+    return this._zLimit;
+  }
+
+  set zLimit(value: number) {
+    this._zLimit = value;
+    (<ISphericalJoint>this._nativeJoint).setZLimit(value);
+  }
+
+  /** Distance inside the limit value at which the limit will be considered to be active by the solver. */
+  get contactDistance(): number {
+    return this._contactDistance;
+  }
+
+  set contactDistance(value: number) {
+    this._contactDistance = value;
+    (<ISphericalJoint>this._nativeJoint).setContactDistance(value);
+  }
+
+  /** The spring forces used to reach the target position. */
+  get stiffness(): number {
+    return this._stiffness;
+  }
+
+  set stiffness(value: number) {
+    this._stiffness = value;
+    (<ISphericalJoint>this._nativeJoint).setStiffness(value);
+  }
+
+  /** The damper force uses to dampen the spring. */
+  get damping(): number {
+    return this._damping;
+  }
+
+  set damping(value: number) {
+    this._damping = value;
+    (<ISphericalJoint>this._nativeJoint).setDamping(value);
+  }
+
+  /**
+   * @override
+   * @internal
+   */
+  _onAwake() {
+    const collider = this._collider;
+    collider.collider = this.entity.getComponent(Collider);
+    this._nativeJoint = PhysicsManager._nativePhysics.createSphericalJoint(collider.collider._nativeCollider);
+  }
+}
diff --git a/packages/core/src/physics/joint/index.ts b/packages/core/src/physics/joint/index.ts
index 2ffd2e3cb2..b643d06dd5 100644
--- a/packages/core/src/physics/joint/index.ts
+++ b/packages/core/src/physics/joint/index.ts
@@ -2,6 +2,7 @@ export { Joint } from "./Joint";
 export { FixedJoint } from "./FixedJoint";
 export { HingeJoint } from "./HingeJoint";
 export { SpringJoint } from "./SpringJoint";
+export { SphericalJoint } from "./SphericalJoint";
 
 export { JointLimits } from "./JointLimits";
 export { JointMotor } from "./JointMotor";
diff --git a/packages/design/src/physics/IPhysics.ts b/packages/design/src/physics/IPhysics.ts
index ad90d00da2..ad0d08b17d 100644
--- a/packages/design/src/physics/IPhysics.ts
+++ b/packages/design/src/physics/IPhysics.ts
@@ -6,7 +6,7 @@ import { IStaticCollider } from "./IStaticCollider";
 import { Quaternion, Vector3 } from "@oasis-engine/math";
 import { ICollider } from "./ICollider";
 import { ICharacterController } from "./ICharacterController";
-import { IFixedJoint, IHingeJoint, ISpringJoint } from "./joints";
+import { IFixedJoint, IHingeJoint, ISpringJoint, ISphericalJoint } from "./joints";
 
 /**
  * The interface of physics creation.
@@ -119,4 +119,10 @@ export interface IPhysics {
    * @param collider - Affector of joint
    */
   createSpringJoint(collider: ICollider): ISpringJoint;
+
+  /**
+   * Create spherical joint
+   * @param collider - Affector of joint
+   */
+  createSphericalJoint(collider: ICollider): ISphericalJoint;
 }
diff --git a/packages/design/src/physics/joints/ISphericalJoint.ts b/packages/design/src/physics/joints/ISphericalJoint.ts
new file mode 100644
index 0000000000..9be2e17a95
--- /dev/null
+++ b/packages/design/src/physics/joints/ISphericalJoint.ts
@@ -0,0 +1,24 @@
+import { IJoint } from "./IJoint";
+
+/**
+ * A joint which behaves in a similar way to a ball and socket.
+ */
+export interface ISphericalJoint extends IJoint {
+  /** Whether enable spring limit */
+  enableSpring(value: boolean);
+
+  /** The limit angle from the Y-axis of the constraint frame. */
+  setYLimit(value: number);
+
+  /** The limit angle from the Z-axis of the constraint frame. */
+  setZLimit(value: number);
+
+  /** Distance inside the limit value at which the limit will be considered to be active by the solver. */
+  setContactDistance(value: number);
+
+  /** The spring forces used to reach the target position. */
+  setStiffness(value: number);
+
+  /** The damper force uses to dampen the spring. */
+  setDamping(value: number);
+}
diff --git a/packages/design/src/physics/joints/index.ts b/packages/design/src/physics/joints/index.ts
index 6a0b39f4e6..7f278b23ad 100644
--- a/packages/design/src/physics/joints/index.ts
+++ b/packages/design/src/physics/joints/index.ts
@@ -2,3 +2,4 @@ export type { IJoint } from "./IJoint";
 export type { IFixedJoint } from "./IFixedJoint";
 export type { IHingeJoint } from "./IHingeJoint";
 export type { ISpringJoint } from "./ISpringJoint";
+export type { ISphericalJoint } from "./ISphericalJoint";
diff --git a/packages/physics-lite/src/LitePhysics.ts b/packages/physics-lite/src/LitePhysics.ts
index 6cc92369b3..48b00e70ed 100644
--- a/packages/physics-lite/src/LitePhysics.ts
+++ b/packages/physics-lite/src/LitePhysics.ts
@@ -12,6 +12,7 @@ import {
   IFixedJoint,
   IHingeJoint,
   ISpringJoint,
+  ISphericalJoint
 } from "@oasis-engine/design";
 import { Quaternion, Vector3 } from "oasis-engine";
 import { LiteDynamicCollider } from "./LiteDynamicCollider";
@@ -121,20 +122,27 @@ export class LitePhysics {
    * {@inheritDoc IPhysics.createFixedJoint }
    */
   static createFixedJoint(collider: LiteCollider): IFixedJoint {
-    throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!";
+    throw "Physics-lite don't support FixedJoint. Use Physics-PhysX instead!";
   }
 
   /**
    * {@inheritDoc IPhysics.createHingeJoint }
    */
   static createHingeJoint(collider: LiteCollider): IHingeJoint {
-    throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!";
+    throw "Physics-lite don't support HingeJoint. Use Physics-PhysX instead!";
   }
 
   /**
    * {@inheritDoc IPhysics.createSpringJoint }
    */
   static createSpringJoint(collider: LiteCollider): ISpringJoint {
-    throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!";
+    throw "Physics-lite don't support SpringJoint. Use Physics-PhysX instead!";
+  }
+
+  /**
+   * {@inheritDoc IPhysics.createSphericalJoint }
+   */
+  static createSphericalJoint(collider: LiteCollider): ISphericalJoint {
+    throw "Physics-lite don't support SphericalJoint. Use Physics-PhysX instead!";
   }
 }
diff --git a/packages/physics-physx/src/PhysXPhysics.ts b/packages/physics-physx/src/PhysXPhysics.ts
index d5569dcebb..1456f00f33 100644
--- a/packages/physics-physx/src/PhysXPhysics.ts
+++ b/packages/physics-physx/src/PhysXPhysics.ts
@@ -10,6 +10,7 @@ import {
   IPhysicsMaterial,
   IPlaneColliderShape,
   ISphereColliderShape,
+  ISphericalJoint,
   ISpringJoint,
   IStaticCollider
 } from "@oasis-engine/design";
@@ -18,6 +19,7 @@ import { PhysXRuntimeMode } from "./enum/PhysXRuntimeMode";
 import { PhysXFixedJoint } from "./joint/PhysXFixedJoint";
 import { PhysXHingeJoint } from "./joint/PhysXHingeJoint";
 import { PhysXSpringJoint } from "./joint/PhysXSpringJoint";
+import { PhysXSphericalJoint } from "./joint/PhysXSphericalJoint";
 import { PhysXCharacterController } from "./PhysXCharacterController";
 import { PhysXCollider } from "./PhysXCollider";
 import { PhysXDynamicCollider } from "./PhysXDynamicCollider";
@@ -220,6 +222,13 @@ export class PhysXPhysics {
     return new PhysXSpringJoint(collider);
   }
 
+  /**
+   * {@inheritDoc IPhysics.createSpringJoint }
+   */
+  static createSphericalJoint(collider: PhysXCollider): ISphericalJoint {
+    return new PhysXSphericalJoint(collider);
+  }
+
   private static _init(physX: any): void {
     const version = physX.PX_PHYSICS_VERSION;
     const defaultErrorCallback = new physX.PxDefaultErrorCallback();
diff --git a/packages/physics-physx/src/joint/PhysXSphericalJoint.ts b/packages/physics-physx/src/joint/PhysXSphericalJoint.ts
new file mode 100644
index 0000000000..2844126f0e
--- /dev/null
+++ b/packages/physics-physx/src/joint/PhysXSphericalJoint.ts
@@ -0,0 +1,97 @@
+import { ISphericalJoint } from "@oasis-engine/design";
+import { PhysXJoint } from "./PhysXJoint";
+import { PhysXCollider } from "../PhysXCollider";
+import { PhysXPhysics } from "../PhysXPhysics";
+
+/**
+ * A joint which behaves in a similar way to a ball and socket.
+ */
+export class PhysXSphericalJoint extends PhysXJoint implements ISphericalJoint {
+  private _yLimit = Math.PI / 2;
+  private _zLimit = Math.PI / 2;
+  private _contactDistance = -1;
+  private _stiffness = 0;
+  private _damping = 0;
+  private _enableSpring = false;
+
+  constructor(collider: PhysXCollider) {
+    super();
+    this._collider = collider;
+    this._pxJoint = PhysXPhysics._pxPhysics.createSphericalJoint(
+      null,
+      PhysXJoint._defaultVec,
+      PhysXJoint._defaultQuat,
+      collider._pxActor,
+      PhysXJoint._defaultVec,
+      PhysXJoint._defaultQuat
+    );
+  }
+
+  /**
+   * {@inheritDoc ISphericalJoint.enableSpring }
+   */
+  enableSpring(value: boolean) {
+    if (this._enableSpring !== value) {
+      this._enableSpring = value;
+      this._setLimitCone();
+    }
+  }
+
+  /**
+   * {@inheritDoc ISphericalJoint.setYLimit }
+   */
+  setYLimit(value: number) {
+    if (this._yLimit !== value) {
+      this._yLimit = value;
+      this._setLimitCone();
+    }
+  }
+
+  /**
+   * {@inheritDoc ISphericalJoint.setZLimit }
+   */
+  setZLimit(value: number) {
+    if (this._zLimit !== value) {
+      this._zLimit = value;
+      this._setLimitCone();
+    }
+  }
+
+  /**
+   * {@inheritDoc ISphericalJoint.setContactDistance }
+   */
+  setContactDistance(value: number) {
+    if (this._contactDistance !== value) {
+      this._contactDistance = value;
+      this._setLimitCone();
+    }
+  }
+
+  /**
+   * {@inheritDoc ISphericalJoint.setStiffness }
+   */
+  setStiffness(value: number) {
+    if (this._stiffness !== value) {
+      this._stiffness = value;
+      this._setLimitCone();
+    }
+  }
+
+  /**
+   * {@inheritDoc ISphericalJoint.setDamping }
+   */
+  setDamping(value: number) {
+    if (this._damping !== value) {
+      this._damping = value;
+      this._setLimitCone();
+    }
+  }
+
+  private _setLimitCone() {
+    if (this._enableSpring) {
+      this._pxJoint.setSoftLimitCone(this._yLimit, this._zLimit, this._stiffness, this._damping);
+    } else {
+      this._pxJoint.setHardLimitCone(this._yLimit, this._zLimit, this._contactDistance);
+    }
+  }
+}