UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

1,150 lines (1,149 loc) 35.9 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug } from "../../../core/debug.js"; import { Mat4 } from "../../../core/math/mat4.js"; import { Vec2 } from "../../../core/math/vec2.js"; import { Vec3 } from "../../../core/math/vec3.js"; import { GraphNode } from "../../../scene/graph-node.js"; import { Component } from "../component.js"; import { BODYTYPE_DYNAMIC } from "../rigid-body/constants.js"; import { JOINTTYPE_6DOF, JOINTTYPE_BALL, JOINTTYPE_FIXED, JOINTTYPE_HINGE, JOINTTYPE_SLIDER, MOTION_LOCKED } from "./constants.js"; const _mat = new Mat4(); const _jointMat = new Mat4(); const _frameMatA = new Mat4(); const _frameMatB = new Mat4(); const _vec3 = new Vec3(); const _vec3b = new Vec3(); const _jointTypes = /* @__PURE__ */ new Set([JOINTTYPE_FIXED, JOINTTYPE_BALL, JOINTTYPE_HINGE, JOINTTYPE_SLIDER, JOINTTYPE_6DOF]); const _breakSeparation = 0.2; function setVec2(target, value) { const x = value instanceof Vec2 ? value.x : value[0]; const y = value instanceof Vec2 ? value.y : value[1]; if (target.x !== x || target.y !== y) { target.set(x, y); return true; } return false; } function setVec3(target, value) { const x = value instanceof Vec3 ? value.x : value[0]; const y = value instanceof Vec3 ? value.y : value[1]; const z = value instanceof Vec3 ? value.z : value[2]; if (target.x !== x || target.y !== y || target.z !== z) { target.set(x, y, z); return true; } return false; } class JointComponent extends Component { constructor() { super(...arguments); /** * The physics backend joint, when created. * * @type {PhysicsJoint|null} * @private */ __publicField(this, "_joint", null); /** @private */ __publicField(this, "_type", JOINTTYPE_FIXED); /** * @type {Entity|null} * @private */ __publicField(this, "_entityA", null); /** * @type {Entity|null} * @private */ __publicField(this, "_entityB", null); /** @private */ __publicField(this, "_enableCollision", false); /** @private */ __publicField(this, "_breakImpulse", Infinity); /** @private */ __publicField(this, "_broken", false); /** @private */ __publicField(this, "_initialized", false); // hinge, slider and ball /** @private */ __publicField(this, "_enableLimits", false); /** @private */ __publicField(this, "_limits", new Vec2(-45, 45)); /** @private */ __publicField(this, "_motorSpeed", 0); /** @private */ __publicField(this, "_maxMotorForce", 0); /** @private */ __publicField(this, "_swingLimitY", 45); /** @private */ __publicField(this, "_swingLimitZ", 45); /** @private */ __publicField(this, "_twistLimit", 20); // 6dof /** @private */ __publicField(this, "_linearMotionX", MOTION_LOCKED); /** @private */ __publicField(this, "_linearMotionY", MOTION_LOCKED); /** @private */ __publicField(this, "_linearMotionZ", MOTION_LOCKED); /** @private */ __publicField(this, "_linearLimitsX", new Vec2()); /** @private */ __publicField(this, "_linearLimitsY", new Vec2()); /** @private */ __publicField(this, "_linearLimitsZ", new Vec2()); /** @private */ __publicField(this, "_linearStiffness", new Vec3()); /** @private */ __publicField(this, "_linearDamping", new Vec3(1, 1, 1)); /** @private */ __publicField(this, "_linearEquilibrium", new Vec3()); /** @private */ __publicField(this, "_angularMotionX", MOTION_LOCKED); /** @private */ __publicField(this, "_angularMotionY", MOTION_LOCKED); /** @private */ __publicField(this, "_angularMotionZ", MOTION_LOCKED); /** @private */ __publicField(this, "_angularLimitsX", new Vec2()); /** @private */ __publicField(this, "_angularLimitsY", new Vec2()); /** @private */ __publicField(this, "_angularLimitsZ", new Vec2()); /** @private */ __publicField(this, "_angularStiffness", new Vec3()); /** @private */ __publicField(this, "_angularDamping", new Vec3(1, 1, 1)); /** @private */ __publicField(this, "_angularEquilibrium", new Vec3()); // event bookkeeping /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtEntityADestroy", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtEntityBDestroy", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtSimEnabledA", null); /** * @type {EventHandle|null} * @private */ __publicField(this, "_evtSimEnabledB", null); /** * @type {EventHandle[]} * @private */ __publicField(this, "_evtBodyTeardown", []); // joint frame anchors captured at constraint creation, used for break detection on ammo // builds that expose no constraint state /** @private */ __publicField(this, "_anchorA", new Vec3()); /** @private */ __publicField(this, "_anchorB", new Vec3()); /** @private */ __publicField(this, "_axisA", new Vec3()); } /** * Sets the type of joint. Can be: * * - {@link JOINTTYPE_FIXED}: rigidly locks the bodies together. * - {@link JOINTTYPE_BALL}: ball and socket - free rotation about the anchor point, with * optional swing and twist limits. * - {@link JOINTTYPE_HINGE}: rotation about the joint's X axis, with optional limits and * motor. * - {@link JOINTTYPE_SLIDER}: translation along the joint's X axis, with optional limits and * motor. * - {@link JOINTTYPE_6DOF}: each linear and angular axis is independently locked, limited or * free, with optional springs. * * Defaults to {@link JOINTTYPE_FIXED}. * * @type {JOINTTYPE_FIXED|JOINTTYPE_BALL|JOINTTYPE_HINGE|JOINTTYPE_SLIDER|JOINTTYPE_6DOF} */ set type(type) { if (this._type !== type) { if (!_jointTypes.has(type)) { Debug.warn(`JointComponent: invalid joint type '${type}'`); return; } this._destroyConstraint(); this._broken = false; this._type = type; this._tryCreateConstraint(); } } /** * Gets the type of joint. * * @type {JOINTTYPE_FIXED|JOINTTYPE_BALL|JOINTTYPE_HINGE|JOINTTYPE_SLIDER|JOINTTYPE_6DOF} */ get type() { return this._type; } /** * Sets the first entity constrained by this joint. The entity must have a * {@link RigidBodyComponent}. Can be set to an {@link Entity} or the GUID of an entity. The * constraint is created once both constrained entities have rigid bodies in the simulation. * * @type {Entity|string|null} */ set entityA(arg) { this._setJointEntity("_entityA", "_evtEntityADestroy", arg); } /** * Gets the first entity constrained by this joint. * * @type {Entity|null} */ get entityA() { return this._entityA; } /** * Sets the second entity constrained by this joint. The entity must have a * {@link RigidBodyComponent}. Can be set to an {@link Entity} or the GUID of an entity. If * null, {@link entityA} is constrained to a fixed point in world space instead. Defaults to * null. * * @type {Entity|string|null} */ set entityB(arg) { this._setJointEntity("_entityB", "_evtEntityBDestroy", arg); } /** * Gets the second entity constrained by this joint. * * @type {Entity|null} */ get entityB() { return this._entityB; } /** * Sets whether the two constrained bodies can collide with each other. Defaults to false. * * @type {boolean} */ set enableCollision(enableCollision) { if (this._enableCollision !== enableCollision) { this._destroyConstraint(); this._enableCollision = enableCollision; this._tryCreateConstraint(); } } /** * Gets whether the two constrained bodies can collide with each other. * * @type {boolean} */ get enableCollision() { return this._enableCollision; } /** * Sets the impulse threshold, in Newton seconds, above which the joint breaks. A broken joint * no longer constrains its bodies, fires the 'break' event and has {@link isBroken} set to * true. As a rule of thumb, a steady force breaks the joint when force × simulation timestep * exceeds this value. Defaults to Infinity (unbreakable). * * @type {number} */ set breakImpulse(impulse) { if (this._breakImpulse !== impulse) { this._breakImpulse = impulse; const joint = this._joint; if (joint) { joint.setBreakImpulse(impulse); if (Number.isFinite(impulse)) { this.system._breakable.add(this); } else { this.system._breakable.delete(this); } } } } /** * Gets the impulse threshold above which the joint breaks. * * @type {number} */ get breakImpulse() { return this._breakImpulse; } /** * The underlying Ammo (Bullet) constraint, or null if it has not been created or has broken. * An unsupported escape hatch for native functionality the component does not yet expose - it * is deliberately kept off the public, backend-agnostic API surface, mirroring * {@link RigidBodyComponent#body}. * * @type {object|null} * @ignore */ get constraint() { return this._joint ? this._joint.nativeJoint : null; } /** * Gets whether the joint has broken as a result of the applied impulse exceeding * {@link breakImpulse}. A broken joint is re-armed by calling {@link refreshFrames}, toggling * {@link Component#enabled} or changing {@link type}, {@link entityA} or {@link entityB}. * * @type {boolean} */ get isBroken() { return this._broken; } /** * Sets whether the joint's limits are enabled. For hinge joints, this limits rotation about * the joint's X axis to {@link limits}. For slider joints, this limits travel along the * joint's X axis to {@link limits}. For ball joints, this limits rotation to * {@link swingLimitY}, {@link swingLimitZ} and {@link twistLimit}. Not used by fixed and 6dof * joints. Defaults to false. * * @type {boolean} */ set enableLimits(arg) { if (this._enableLimits !== arg) { this._enableLimits = arg; this._updateLimits(); } } /** * Gets whether the joint's limits are enabled. * * @type {boolean} */ get enableLimits() { return this._enableLimits; } /** * Sets the lower and upper limit of the joint's primary degree of freedom. For hinge joints, * these are angles of rotation about the joint's X axis, in degrees; for slider joints, * distances along the joint's X axis, in meters. Only used by hinge and slider joints, when * {@link enableLimits} is true. (Ball joints limit motion with {@link swingLimitY}, * {@link swingLimitZ} and {@link twistLimit}; 6dof joints use {@link linearLimitsX} and * {@link angularLimitsX} etc.) Defaults to `[-45, 45]`. * * @type {Vec2} */ set limits(arg) { if (setVec2(this._limits, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of the joint's primary degree of freedom. * * @type {Vec2} */ get limits() { return this._limits; } /** * Sets the target speed the motor drives towards. For hinge joints, this is an angular speed * in degrees per second; for slider joints, a linear speed in meters per second. The motor is * active only while {@link maxMotorForce} is greater than 0; with a target speed of 0 and a * positive force the motor acts as a brake, holding the joint at rest. Only used by hinge and * slider joints. Defaults to 0. * * @type {number} */ set motorSpeed(arg) { if (this._motorSpeed !== arg) { this._motorSpeed = arg; this._updateMotor(); } } /** * Gets the target speed the motor drives towards. * * @type {number} */ get motorSpeed() { return this._motorSpeed; } /** * Sets the maximum force the motor can apply to reach {@link motorSpeed}. For hinge joints, * this is a torque in Newton meters; for slider joints, a force in Newtons. The motor is * disabled while this is 0, so set it greater than 0 to engage the motor. Only used by hinge * and slider joints. Defaults to 0. * * @type {number} */ set maxMotorForce(arg) { if (this._maxMotorForce !== arg) { this._maxMotorForce = arg; this._updateMotor(); } } /** * Gets the maximum force the joint's motor can apply. * * @type {number} */ get maxMotorForce() { return this._maxMotorForce; } /** * Sets the maximum swing of a ball joint towards its Y axis, in degrees, measured as a * half-angle either side of the joint's X axis. Only used by ball joints when * {@link enableLimits} is true. Defaults to 45. * * @type {number} */ set swingLimitY(arg) { if (this._swingLimitY !== arg) { this._swingLimitY = arg; this._updateLimits(); } } /** * Gets the maximum swing of a ball joint towards its Y axis. * * @type {number} */ get swingLimitY() { return this._swingLimitY; } /** * Sets the maximum swing of a ball joint towards its Z axis, in degrees, measured as a * half-angle either side of the joint's X axis. Only used by ball joints when * {@link enableLimits} is true. Defaults to 45. * * @type {number} */ set swingLimitZ(arg) { if (this._swingLimitZ !== arg) { this._swingLimitZ = arg; this._updateLimits(); } } /** * Gets the maximum swing of a ball joint towards its Z axis. * * @type {number} */ get swingLimitZ() { return this._swingLimitZ; } /** * Sets the maximum twist of a ball joint about its X axis, in degrees, measured as a * half-angle either side of the rest orientation. Only used by ball joints when * {@link enableLimits} is true. Defaults to 20. * * @type {number} */ set twistLimit(arg) { if (this._twistLimit !== arg) { this._twistLimit = arg; this._updateLimits(); } } /** * Gets the maximum twist of a ball joint about its X axis. * * @type {number} */ get twistLimit() { return this._twistLimit; } /** * Sets the type of motion allowed for translation along the joint's X axis. Can be * {@link MOTION_LOCKED}, {@link MOTION_LIMITED} or {@link MOTION_FREE}. Only used by 6dof * joints. Defaults to {@link MOTION_LOCKED}. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ set linearMotionX(arg) { if (this._linearMotionX !== arg) { this._linearMotionX = arg; this._updateLimits(); } } /** * Gets the type of motion allowed for translation along the joint's X axis. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ get linearMotionX() { return this._linearMotionX; } /** * Sets the type of motion allowed for translation along the joint's Y axis. Can be * {@link MOTION_LOCKED}, {@link MOTION_LIMITED} or {@link MOTION_FREE}. Only used by 6dof * joints. Defaults to {@link MOTION_LOCKED}. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ set linearMotionY(arg) { if (this._linearMotionY !== arg) { this._linearMotionY = arg; this._updateLimits(); } } /** * Gets the type of motion allowed for translation along the joint's Y axis. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ get linearMotionY() { return this._linearMotionY; } /** * Sets the type of motion allowed for translation along the joint's Z axis. Can be * {@link MOTION_LOCKED}, {@link MOTION_LIMITED} or {@link MOTION_FREE}. Only used by 6dof * joints. Defaults to {@link MOTION_LOCKED}. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ set linearMotionZ(arg) { if (this._linearMotionZ !== arg) { this._linearMotionZ = arg; this._updateLimits(); } } /** * Gets the type of motion allowed for translation along the joint's Z axis. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ get linearMotionZ() { return this._linearMotionZ; } /** * Sets the lower and upper limit of translation along the joint's X axis, in meters. Only * used by 6dof joints when {@link linearMotionX} is {@link MOTION_LIMITED}. Defaults to * `[0, 0]`. * * @type {Vec2} */ set linearLimitsX(arg) { if (setVec2(this._linearLimitsX, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of translation along the joint's X axis. * * @type {Vec2} */ get linearLimitsX() { return this._linearLimitsX; } /** * Sets the lower and upper limit of translation along the joint's Y axis, in meters. Only * used by 6dof joints when {@link linearMotionY} is {@link MOTION_LIMITED}. Defaults to * `[0, 0]`. * * @type {Vec2} */ set linearLimitsY(arg) { if (setVec2(this._linearLimitsY, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of translation along the joint's Y axis. * * @type {Vec2} */ get linearLimitsY() { return this._linearLimitsY; } /** * Sets the lower and upper limit of translation along the joint's Z axis, in meters. Only * used by 6dof joints when {@link linearMotionZ} is {@link MOTION_LIMITED}. Defaults to * `[0, 0]`. * * @type {Vec2} */ set linearLimitsZ(arg) { if (setVec2(this._linearLimitsZ, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of translation along the joint's Z axis. * * @type {Vec2} */ get linearLimitsZ() { return this._linearLimitsZ; } /** * Sets the stiffness of the springs acting on translation along the joint's X, Y and Z axes. * A spring acts on an axis when its stiffness component is greater than 0. Only used by 6dof * joints. Defaults to `[0, 0, 0]` (no springs). * * @type {Vec3} */ set linearStiffness(arg) { if (setVec3(this._linearStiffness, arg)) { this._updateSpring(); } } /** * Gets the stiffness of the springs acting on translation along the joint's X, Y and Z axes. * * @type {Vec3} */ get linearStiffness() { return this._linearStiffness; } /** * Sets the damping of the springs acting on translation along the joint's X, Y and Z axes. * Only used by 6dof joints. Defaults to `[1, 1, 1]`. * * @type {Vec3} */ set linearDamping(arg) { if (setVec3(this._linearDamping, arg)) { this._updateSpring(); } } /** * Gets the damping of the springs acting on translation along the joint's X, Y and Z axes. * * @type {Vec3} */ get linearDamping() { return this._linearDamping; } /** * Sets the rest positions of the springs acting on translation along the joint's X, Y and Z * axes, in meters. Only used by 6dof joints. Defaults to `[0, 0, 0]`. * * @type {Vec3} */ set linearEquilibrium(arg) { if (setVec3(this._linearEquilibrium, arg)) { this._updateSpring(); } } /** * Gets the rest positions of the springs acting on translation along the joint's X, Y and Z * axes. * * @type {Vec3} */ get linearEquilibrium() { return this._linearEquilibrium; } /** * Sets the type of motion allowed for rotation about the joint's X axis. Can be * {@link MOTION_LOCKED}, {@link MOTION_LIMITED} or {@link MOTION_FREE}. Only used by 6dof * joints. Defaults to {@link MOTION_LOCKED}. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ set angularMotionX(arg) { if (this._angularMotionX !== arg) { this._angularMotionX = arg; this._updateLimits(); } } /** * Gets the type of motion allowed for rotation about the joint's X axis. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ get angularMotionX() { return this._angularMotionX; } /** * Sets the type of motion allowed for rotation about the joint's Y axis. Can be * {@link MOTION_LOCKED}, {@link MOTION_LIMITED} or {@link MOTION_FREE}. Only used by 6dof * joints. Defaults to {@link MOTION_LOCKED}. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ set angularMotionY(arg) { if (this._angularMotionY !== arg) { this._angularMotionY = arg; this._updateLimits(); } } /** * Gets the type of motion allowed for rotation about the joint's Y axis. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ get angularMotionY() { return this._angularMotionY; } /** * Sets the type of motion allowed for rotation about the joint's Z axis. Can be * {@link MOTION_LOCKED}, {@link MOTION_LIMITED} or {@link MOTION_FREE}. Only used by 6dof * joints. Defaults to {@link MOTION_LOCKED}. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ set angularMotionZ(arg) { if (this._angularMotionZ !== arg) { this._angularMotionZ = arg; this._updateLimits(); } } /** * Gets the type of motion allowed for rotation about the joint's Z axis. * * @type {MOTION_FREE|MOTION_LIMITED|MOTION_LOCKED} */ get angularMotionZ() { return this._angularMotionZ; } /** * Sets the lower and upper limit of rotation about the joint's X axis, in degrees. For * stability, keep the limits within -180 and 180 degrees. Only used by 6dof joints when * {@link angularMotionX} is {@link MOTION_LIMITED}. Defaults to `[0, 0]`. * * @type {Vec2} */ set angularLimitsX(arg) { if (setVec2(this._angularLimitsX, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of rotation about the joint's X axis. * * @type {Vec2} */ get angularLimitsX() { return this._angularLimitsX; } /** * Sets the lower and upper limit of rotation about the joint's Y axis, in degrees. For * stability, keep the limits within -90 and 90 degrees. Only used by 6dof joints when * {@link angularMotionY} is {@link MOTION_LIMITED}. Defaults to `[0, 0]`. * * @type {Vec2} */ set angularLimitsY(arg) { if (setVec2(this._angularLimitsY, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of rotation about the joint's Y axis. * * @type {Vec2} */ get angularLimitsY() { return this._angularLimitsY; } /** * Sets the lower and upper limit of rotation about the joint's Z axis, in degrees. For * stability, keep the limits within -180 and 180 degrees. Only used by 6dof joints when * {@link angularMotionZ} is {@link MOTION_LIMITED}. Defaults to `[0, 0]`. * * @type {Vec2} */ set angularLimitsZ(arg) { if (setVec2(this._angularLimitsZ, arg)) { this._updateLimits(); } } /** * Gets the lower and upper limit of rotation about the joint's Z axis. * * @type {Vec2} */ get angularLimitsZ() { return this._angularLimitsZ; } /** * Sets the stiffness of the springs acting on rotation about the joint's X, Y and Z axes. A * spring acts on an axis when its stiffness component is greater than 0. Only used by 6dof * joints. Defaults to `[0, 0, 0]` (no springs). * * @type {Vec3} */ set angularStiffness(arg) { if (setVec3(this._angularStiffness, arg)) { this._updateSpring(); } } /** * Gets the stiffness of the springs acting on rotation about the joint's X, Y and Z axes. * * @type {Vec3} */ get angularStiffness() { return this._angularStiffness; } /** * Sets the damping of the springs acting on rotation about the joint's X, Y and Z axes. Only * used by 6dof joints. Defaults to `[1, 1, 1]`. * * @type {Vec3} */ set angularDamping(arg) { if (setVec3(this._angularDamping, arg)) { this._updateSpring(); } } /** * Gets the damping of the springs acting on rotation about the joint's X, Y and Z axes. * * @type {Vec3} */ get angularDamping() { return this._angularDamping; } /** * Sets the rest angles of the springs acting on rotation about the joint's X, Y and Z axes, * in degrees. Only used by 6dof joints. Defaults to `[0, 0, 0]`. * * @type {Vec3} */ set angularEquilibrium(arg) { if (setVec3(this._angularEquilibrium, arg)) { this._updateSpring(); } } /** * Gets the rest angles of the springs acting on rotation about the joint's X, Y and Z axes. * * @type {Vec3} */ get angularEquilibrium() { return this._angularEquilibrium; } /** * Destroys and recreates the underlying constraint, re-capturing the joint frames from the * current world transforms of the joint entity, {@link entityA} and {@link entityB}. Call * this after moving the joint entity to re-anchor the joint, or to re-attach a joint that has * broken. */ refreshFrames() { this._destroyConstraint(); this._broken = false; this._tryCreateConstraint(); } /** * Resolves an entity reference, accepting an Entity instance or a GUID string, and tracks the * destruction of the referenced entity. * * @param {'_entityA'|'_entityB'} prop - The backing field to assign. * @param {'_evtEntityADestroy'|'_evtEntityBDestroy'} evtProp - The destroy event handle field. * @param {Entity|string|null} arg - The entity, entity GUID or null. * @private */ _setJointEntity(prop, evtProp, arg) { let entity; if (arg instanceof GraphNode) { entity = arg; } else if (typeof arg === "string") { entity = this.system.app.getEntityFromIndex(arg) ?? null; } else { entity = null; } if (this[prop] === entity) { return; } this._destroyConstraint(); this[evtProp]?.off(); this[evtProp] = null; this[prop] = entity; if (entity) { this[evtProp] = entity.once("destroy", () => { this[evtProp] = null; this[prop] = null; this._destroyConstraint(); }); } Debug.call(() => { if (this._entityA && this._entityA === this._entityB) { Debug.warn("JointComponent: entityA and entityB must be different entities.", this); } }); this._broken = false; this._tryCreateConstraint(); } /** * @param {Entity} entity - The entity to check. * @returns {boolean} True if the entity has a rigid body that is present in the simulation. * @private */ _isBodyReady(entity) { const rigidbody = entity.rigidbody; return !!(rigidbody && rigidbody._body && rigidbody._simulationEnabled); } /** @private */ _subscribeBodyAvailable() { if (!this._evtSimEnabledA && this._entityA?.rigidbody) { this._evtSimEnabledA = this._entityA.rigidbody.once("simulationenabled", this._onBodyAvailable, this); } if (!this._evtSimEnabledB && this._entityB?.rigidbody) { this._evtSimEnabledB = this._entityB.rigidbody.once("simulationenabled", this._onBodyAvailable, this); } } /** @private */ _clearBodyAvailableSubscriptions() { this._evtSimEnabledA?.off(); this._evtSimEnabledA = null; this._evtSimEnabledB?.off(); this._evtSimEnabledB = null; } /** @private */ _onBodyAvailable() { this._clearBodyAvailableSubscriptions(); this._tryCreateConstraint(); } /** @private */ _onBodyLost() { this._destroyConstraint(); this._tryCreateConstraint(); } /** * Creates the constraint if the component is in a state where one should exist and both * constrained bodies are present in the simulation. Otherwise, the component registers with * the system to retry when the bodies become available, which is also where this is called * from. * * @ignore */ _tryCreateConstraint() { const system = this.system; if (this._joint || !this._initialized || !this.enabled || !this.entity.enabled || this._broken || !this._entityA || this._entityA === this._entityB) { system._pending.delete(this); this._clearBodyAvailableSubscriptions(); return; } if (!this._isBodyReady(this._entityA) || this._entityB && !this._isBodyReady(this._entityB)) { system._pending.add(this); this._subscribeBodyAvailable(); return; } system._pending.delete(this); this._clearBodyAvailableSubscriptions(); this._createConstraint(); } /** * Computes the joint frame in the local space of the given body entity, as a scale-free * matrix with X as the primary joint axis. The frame's local origin and X axis are also * written to anchor and axis, for use by break detection. * * @param {Entity|null} bodyEntity - The entity whose local space the frame is expressed in, * or null for the world-pinning fixed body, whose transform is identity. * @param {Vec3} anchor - Receives the frame origin in the body's local space. * @param {Vec3} axis - Receives the frame X axis in the body's local space. * @param {Mat4} frame - Receives the joint frame. * @private */ _createFrame(bodyEntity, anchor, axis, frame) { _jointMat.setTRS(this.entity.getPosition(), this.entity.getRotation(), Vec3.ONE); if (bodyEntity) { _mat.setTRS(bodyEntity.getPosition(), bodyEntity.getRotation(), Vec3.ONE); _mat.invert(); _mat.mul(_jointMat); } else { _mat.copy(_jointMat); } _mat.getTranslation(_vec3); anchor.copy(_vec3); _mat.getX(axis).normalize(); frame.copy(_mat); } /** @private */ _createConstraint() { const entityA = this._entityA; const entityB = this._entityB; const rigidbodyA = entityA.rigidbody; const rigidbodyB = entityB ? entityB.rigidbody : null; Debug.call(() => { if (rigidbodyA.type !== BODYTYPE_DYNAMIC && (!rigidbodyB || rigidbodyB.type !== BODYTYPE_DYNAMIC)) { Debug.warn("JointComponent: neither constrained body is dynamic so the joint will have no effect.", this); } }); this._createFrame(entityA, this._anchorA, this._axisA, _frameMatA); this._createFrame(entityB, this._anchorB, _vec3b, _frameMatB); const world = this.system.app.systems.rigidbody.physicsWorld; this._joint = world.createJoint({ type: this._type, bodyA: rigidbodyA._body, bodyB: rigidbodyB ? rigidbodyB._body : null, frameA: _frameMatA, frameB: _frameMatB, enableCollision: this._enableCollision, settings: this }); if (Number.isFinite(this._breakImpulse)) { this.system._breakable.add(this); } this._evtBodyTeardown.push( rigidbodyA.on("simulationdisabled", this._onBodyLost, this), rigidbodyA.on("beforeremove", this._onBodyLost, this) ); if (rigidbodyB) { this._evtBodyTeardown.push( rigidbodyB.on("simulationdisabled", this._onBodyLost, this), rigidbodyB.on("beforeremove", this._onBodyLost, this) ); } } /** @private */ _destroyConstraint() { this.system._pending.delete(this); this._clearBodyAvailableSubscriptions(); const joint = this._joint; if (joint) { this.system._breakable.delete(this); for (let i = 0; i < this._evtBodyTeardown.length; i++) { this._evtBodyTeardown[i].off(); } this._evtBodyTeardown.length = 0; this.system.app.systems.rigidbody.physicsWorld.destroyJoint(joint); this._joint = null; } } /** * Tests whether the joint frame anchors of the two bodies have drifted apart, which can only * happen once bullet has internally disabled a broken constraint. Used for break detection * on ammo builds that expose no constraint state. The slide axis of slider joints is ignored * and 6dof joints, whose axes may all be free, are not detectable this way. * * @returns {boolean} True if the anchors have separated. * @private */ _isAnchorSeparated() { if (this._type === JOINTTYPE_6DOF) { return false; } const entityA = this._entityA; _mat.setTRS(entityA.getPosition(), entityA.getRotation(), Vec3.ONE); _mat.transformPoint(this._anchorA, _vec3); const entityB = this._entityB; if (entityB) { _jointMat.setTRS(entityB.getPosition(), entityB.getRotation(), Vec3.ONE); _jointMat.transformPoint(this._anchorB, _vec3b); } else { _vec3b.copy(this._anchorB); } _vec3b.sub(_vec3); if (this._type === JOINTTYPE_SLIDER) { _mat.transformVector(this._axisA, _vec3); const slide = _vec3b.dot(_vec3); _vec3b.sub(_vec3.mulScalar(slide)); } return _vec3b.lengthSq() > _breakSeparation * _breakSeparation; } /** * Tests whether the constraint has broken during the last simulation step and if so, fires * the break event and destroys the constraint. Called by the system after each physics step * for joints with a finite break impulse. * * @ignore */ _checkBroken() { const joint = this._joint; if (!joint) { return; } let broken = joint.isBroken(); if (broken === null) { broken = this._isAnchorSeparated(); } if (broken) { this._broken = true; this._destroyConstraint(); this.fire("break"); } } /** * Wakes the constrained bodies so that a change to the constraint takes immediate effect on * a sleeping simulation island. * * @private */ _activateBodies() { this._entityA?.rigidbody?.activate(); this._entityB?.rigidbody?.activate(); } /** @private */ _updateLimits() { if (this._joint && this._joint.updateLimits(this)) { this._activateBodies(); } } /** @private */ _updateMotor() { if (this._joint && this._joint.updateMotor(this)) { this._activateBodies(); } } /** @private */ _updateSpring() { if (this._joint && this._joint.updateSpring(this)) { this._activateBodies(); } } onEnable() { this._broken = false; this._tryCreateConstraint(); } onDisable() { this._destroyConstraint(); } onBeforeRemove() { this._destroyConstraint(); this._evtEntityADestroy?.off(); this._evtEntityADestroy = null; this._evtEntityBDestroy?.off(); this._evtEntityBDestroy = null; } /** * Remaps entity references to their duplicated counterparts when the joint is cloned as part * of an entity subtree. References to entities outside the duplicated subtree are preserved. * * @param {JointComponent} oldJoint - The joint component being duplicated. * @param {Object<string, Entity>} duplicatedIdsMap - A map of original entity GUIDs to cloned * entities. * @ignore */ resolveDuplicatedEntityReferenceProperties(oldJoint, duplicatedIdsMap) { if (oldJoint.entityA) { const newEntityA = duplicatedIdsMap[oldJoint.entityA.guid]; if (newEntityA) { this.entityA = newEntityA; } } if (oldJoint.entityB) { const newEntityB = duplicatedIdsMap[oldJoint.entityB.guid]; if (newEntityB) { this.entityB = newEntityB; } } } } /** * Fired when the applied impulse on the joint exceeds {@link breakImpulse} and the constraint * breaks. The broken joint no longer constrains its bodies and {@link isBroken} becomes true. * Call {@link refreshFrames} to re-attach it. Note that on ammo builds that expose no * constraint state, breakage of 6dof joints cannot be detected, so this event does not fire * for them - other joint types are unaffected. * * @event * @example * entity.joint.on('break', () => { * console.log('The joint broke'); * }); */ __publicField(JointComponent, "EVENT_BREAK", "break"); export { JointComponent };