planck-js
Version:
2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development
564 lines (479 loc) • 18.7 kB
text/typescript
/*
* Planck.js
*
* Copyright (c) Erin Catto, Ali Shakiba
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { options } from "../../util/options";
import { SettingsInternal as Settings } from "../../Settings";
import { } from "../../common/Math";
import { Vec2 } from "../../common/Vec2";
import { Rot } from "../../common/Rot";
import { Joint, JointOpt, JointDef } from "../Joint";
import { Body } from "../Body";
import { RevoluteJoint } from "./RevoluteJoint";
import { PrismaticJoint } from "./PrismaticJoint";
import { TimeStep } from "../Solver";
/** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT;
/** @internal */ const _CONSTRUCTOR_FACTORY = typeof CONSTRUCTOR_FACTORY === "undefined" ? false : CONSTRUCTOR_FACTORY;
/**
* Gear joint definition.
*/
export interface GearJointOpt extends JointOpt {
/**
* The gear ratio. See {@link GearJoint} for explanation.
*/
ratio?: number;
}
/**
* Gear joint definition.
*/
export interface GearJointDef extends JointDef, GearJointOpt {
/**
* The first revolute/prismatic joint attached to the gear joint.
*/
joint1: RevoluteJoint | PrismaticJoint;
/**
* The second prismatic/revolute joint attached to the gear joint.
*/
joint2: RevoluteJoint | PrismaticJoint;
}
/** @internal */ const DEFAULTS = {
ratio : 1.0
};
declare module "./GearJoint" {
/** @hidden @deprecated Use new keyword. */
// @ts-expect-error
function GearJoint(def: GearJointDef): GearJoint;
/** @hidden @deprecated Use new keyword. */
// @ts-expect-error
function GearJoint(def: GearJointOpt, bodyA: Body, bodyB: Body, joint1: RevoluteJoint | PrismaticJoint, joint2: RevoluteJoint | PrismaticJoint, ratio?: number): GearJoint;
}
/**
* A gear joint is used to connect two joints together. Either joint can be a
* revolute or prismatic joint. You specify a gear ratio to bind the motions
* together: coordinate1 + ratio * coordinate2 = constant
*
* The ratio can be negative or positive. If one joint is a revolute joint and
* the other joint is a prismatic joint, then the ratio will have units of
* length or units of 1/length. Warning: You have to manually destroy the gear
* joint if joint1 or joint2 is destroyed.
*
* This definition requires two existing revolute or prismatic joints (any
* combination will work).
*/
// @ts-expect-error
export class GearJoint extends Joint {
static TYPE = "gear-joint" as const;
/** @internal */ m_type: "gear-joint";
/** @internal */ m_joint1: RevoluteJoint | PrismaticJoint;
/** @internal */ m_joint2: RevoluteJoint | PrismaticJoint;
/** @internal */ m_type1: "revolute-joint" | "prismatic-joint";
/** @internal */ m_type2: "revolute-joint" | "prismatic-joint";
/** @internal */ m_bodyC: Body;
/** @internal */ m_localAnchorC: Vec2;
/** @internal */ m_localAnchorA: Vec2;
/** @internal */ m_referenceAngleA: number;
/** @internal */ m_localAxisC: Vec2;
/** @internal */ m_bodyD: Body;
/** @internal */ m_localAnchorD: Vec2;
/** @internal */ m_localAnchorB: Vec2;
/** @internal */ m_referenceAngleB: number;
/** @internal */ m_localAxisD: Vec2;
/** @internal */ m_ratio: number;
/** @internal */ m_constant: number;
/** @internal */ m_impulse: number;
// Solver temp
/** @internal */ m_lcA: Vec2;
/** @internal */ m_lcB: Vec2;
/** @internal */ m_lcC: Vec2;
/** @internal */ m_lcD: Vec2;
/** @internal */ m_mA: number;
/** @internal */ m_mB: number;
/** @internal */ m_mC: number;
/** @internal */ m_mD: number;
/** @internal */ m_iA: number;
/** @internal */ m_iB: number;
/** @internal */ m_iC: number;
/** @internal */ m_iD: number;
/** @internal */ m_JvAC: Vec2;
/** @internal */ m_JvBD: Vec2;
/** @internal */ m_JwA: number;
/** @internal */ m_JwB: number;
/** @internal */ m_JwC: number;
/** @internal */ m_JwD: number;
/** @internal */ m_mass: number;
constructor(def: GearJointDef);
constructor(def: GearJointOpt, bodyA: Body, bodyB: Body, joint1: RevoluteJoint | PrismaticJoint, joint2: RevoluteJoint | PrismaticJoint, ratio?: number);
constructor(def: GearJointDef, bodyA?: Body, bodyB?: Body, joint1?: RevoluteJoint | PrismaticJoint, joint2?: RevoluteJoint | PrismaticJoint, ratio?: number) {
// @ts-ignore
if (_CONSTRUCTOR_FACTORY && !(this instanceof GearJoint)) {
return new GearJoint(def, bodyA, bodyB, joint1, joint2, ratio);
}
def = options(def, DEFAULTS);
super(def, bodyA, bodyB);
bodyA = this.m_bodyA;
bodyB = this.m_bodyB;
this.m_type = GearJoint.TYPE;
if (_ASSERT) console.assert(joint1.m_type === RevoluteJoint.TYPE || joint1.m_type === PrismaticJoint.TYPE);
if (_ASSERT) console.assert(joint2.m_type === RevoluteJoint.TYPE || joint2.m_type === PrismaticJoint.TYPE);
this.m_joint1 = joint1 ? joint1 : def.joint1;
this.m_joint2 = joint2 ? joint2 : def.joint2;
this.m_ratio = Number.isFinite(ratio) ? ratio : def.ratio;
this.m_type1 = this.m_joint1.getType() as "revolute-joint" | "prismatic-joint";
this.m_type2 = this.m_joint2.getType() as "revolute-joint" | "prismatic-joint";
// joint1 connects body A to body C
// joint2 connects body B to body D
let coordinateA: number;
let coordinateB: number;
// TODO_ERIN there might be some problem with the joint edges in Joint.
this.m_bodyC = this.m_joint1.getBodyA();
this.m_bodyA = this.m_joint1.getBodyB();
// Get geometry of joint1
const xfA = this.m_bodyA.m_xf;
const aA = this.m_bodyA.m_sweep.a;
const xfC = this.m_bodyC.m_xf;
const aC = this.m_bodyC.m_sweep.a;
if (this.m_type1 === RevoluteJoint.TYPE) {
const revolute = this.m_joint1 as RevoluteJoint;
this.m_localAnchorC = revolute.m_localAnchorA;
this.m_localAnchorA = revolute.m_localAnchorB;
this.m_referenceAngleA = revolute.m_referenceAngle;
this.m_localAxisC = Vec2.zero();
coordinateA = aA - aC - this.m_referenceAngleA;
} else {
const prismatic = this.m_joint1 as PrismaticJoint;
this.m_localAnchorC = prismatic.m_localAnchorA;
this.m_localAnchorA = prismatic.m_localAnchorB;
this.m_referenceAngleA = prismatic.m_referenceAngle;
this.m_localAxisC = prismatic.m_localXAxisA;
const pC = this.m_localAnchorC;
const pA = Rot.mulTVec2(xfC.q, Vec2.add(Rot.mulVec2(xfA.q, this.m_localAnchorA), Vec2.sub(xfA.p, xfC.p)));
coordinateA = Vec2.dot(pA, this.m_localAxisC) - Vec2.dot(pC, this.m_localAxisC);
}
this.m_bodyD = this.m_joint2.getBodyA();
this.m_bodyB = this.m_joint2.getBodyB();
// Get geometry of joint2
const xfB = this.m_bodyB.m_xf;
const aB = this.m_bodyB.m_sweep.a;
const xfD = this.m_bodyD.m_xf;
const aD = this.m_bodyD.m_sweep.a;
if (this.m_type2 === RevoluteJoint.TYPE) {
const revolute = this.m_joint2 as RevoluteJoint;
this.m_localAnchorD = revolute.m_localAnchorA;
this.m_localAnchorB = revolute.m_localAnchorB;
this.m_referenceAngleB = revolute.m_referenceAngle;
this.m_localAxisD = Vec2.zero();
coordinateB = aB - aD - this.m_referenceAngleB;
} else {
const prismatic = this.m_joint2 as PrismaticJoint;
this.m_localAnchorD = prismatic.m_localAnchorA;
this.m_localAnchorB = prismatic.m_localAnchorB;
this.m_referenceAngleB = prismatic.m_referenceAngle;
this.m_localAxisD = prismatic.m_localXAxisA;
const pD = this.m_localAnchorD;
const pB = Rot.mulTVec2(xfD.q, Vec2.add(Rot.mulVec2(xfB.q, this.m_localAnchorB), Vec2.sub(xfB.p, xfD.p)));
coordinateB = Vec2.dot(pB, this.m_localAxisD) - Vec2.dot(pD, this.m_localAxisD);
}
this.m_constant = coordinateA + this.m_ratio * coordinateB;
this.m_impulse = 0.0;
// Gear Joint:
// C0 = (coordinate1 + ratio * coordinate2)_initial
// C = (coordinate1 + ratio * coordinate2) - C0 = 0
// J = [J1 ratio * J2]
// K = J * invM * JT
// = J1 * invM1 * J1T + ratio * ratio * J2 * invM2 * J2T
//
// Revolute:
// coordinate = rotation
// Cdot = angularVelocity
// J = [0 0 1]
// K = J * invM * JT = invI
//
// Prismatic:
// coordinate = dot(p - pg, ug)
// Cdot = dot(v + cross(w, r), ug)
// J = [ug cross(r, ug)]
// K = J * invM * JT = invMass + invI * cross(r, ug)^2
}
/** @internal */
_serialize(): object {
return {
type: this.m_type,
bodyA: this.m_bodyA,
bodyB: this.m_bodyB,
collideConnected: this.m_collideConnected,
joint1: this.m_joint1,
joint2: this.m_joint2,
ratio: this.m_ratio,
// _constant: this.m_constant,
};
}
/** @internal */
static _deserialize(data: any, world: any, restore: any): GearJoint {
data = {...data};
data.bodyA = restore(Body, data.bodyA, world);
data.bodyB = restore(Body, data.bodyB, world);
data.joint1 = restore(Joint, data.joint1, world);
data.joint2 = restore(Joint, data.joint2, world);
const joint = new GearJoint(data);
// if (data._constant) joint.m_constant = data._constant;
return joint;
}
/** @hidden */
_reset(def: Partial<GearJointDef>): void {
// todo: implement other fields
if (Number.isFinite(def.ratio)) {
this.m_ratio = def.ratio;
}
}
/**
* Get the first joint.
*/
getJoint1(): Joint {
return this.m_joint1;
}
/**
* Get the second joint.
*/
getJoint2(): Joint {
return this.m_joint2;
}
/**
* Set the gear ratio.
*/
setRatio(ratio: number): void {
if (_ASSERT) console.assert(Number.isFinite(ratio));
this.m_ratio = ratio;
}
/**
* Get the gear ratio.
*/
getRatio(): number {
return this.m_ratio;
}
/**
* Get the anchor point on bodyA in world coordinates.
*/
getAnchorA(): Vec2 {
return this.m_bodyA.getWorldPoint(this.m_localAnchorA);
}
/**
* Get the anchor point on bodyB in world coordinates.
*/
getAnchorB(): Vec2 {
return this.m_bodyB.getWorldPoint(this.m_localAnchorB);
}
/**
* Get the reaction force on bodyB at the joint anchor in Newtons.
*/
getReactionForce(inv_dt: number): Vec2 {
return Vec2.mulNumVec2(this.m_impulse, this.m_JvAC).mul(inv_dt);
}
/**
* Get the reaction torque on bodyB in N*m.
*/
getReactionTorque(inv_dt: number): number {
const L = this.m_impulse * this.m_JwA;
return inv_dt * L;
}
initVelocityConstraints(step: TimeStep): void {
this.m_lcA = this.m_bodyA.m_sweep.localCenter;
this.m_lcB = this.m_bodyB.m_sweep.localCenter;
this.m_lcC = this.m_bodyC.m_sweep.localCenter;
this.m_lcD = this.m_bodyD.m_sweep.localCenter;
this.m_mA = this.m_bodyA.m_invMass;
this.m_mB = this.m_bodyB.m_invMass;
this.m_mC = this.m_bodyC.m_invMass;
this.m_mD = this.m_bodyD.m_invMass;
this.m_iA = this.m_bodyA.m_invI;
this.m_iB = this.m_bodyB.m_invI;
this.m_iC = this.m_bodyC.m_invI;
this.m_iD = this.m_bodyD.m_invI;
const aA = this.m_bodyA.c_position.a;
const vA = this.m_bodyA.c_velocity.v;
let wA = this.m_bodyA.c_velocity.w;
const aB = this.m_bodyB.c_position.a;
const vB = this.m_bodyB.c_velocity.v;
let wB = this.m_bodyB.c_velocity.w;
const aC = this.m_bodyC.c_position.a;
const vC = this.m_bodyC.c_velocity.v;
let wC = this.m_bodyC.c_velocity.w;
const aD = this.m_bodyD.c_position.a;
const vD = this.m_bodyD.c_velocity.v;
let wD = this.m_bodyD.c_velocity.w;
const qA = Rot.neo(aA);
const qB = Rot.neo(aB);
const qC = Rot.neo(aC);
const qD = Rot.neo(aD);
this.m_mass = 0.0;
if (this.m_type1 == RevoluteJoint.TYPE) {
this.m_JvAC = Vec2.zero();
this.m_JwA = 1.0;
this.m_JwC = 1.0;
this.m_mass += this.m_iA + this.m_iC;
} else {
const u = Rot.mulVec2(qC, this.m_localAxisC);
const rC = Rot.mulSub(qC, this.m_localAnchorC, this.m_lcC);
const rA = Rot.mulSub(qA, this.m_localAnchorA, this.m_lcA);
this.m_JvAC = u;
this.m_JwC = Vec2.crossVec2Vec2(rC, u);
this.m_JwA = Vec2.crossVec2Vec2(rA, u);
this.m_mass += this.m_mC + this.m_mA + this.m_iC * this.m_JwC * this.m_JwC + this.m_iA * this.m_JwA * this.m_JwA;
}
if (this.m_type2 == RevoluteJoint.TYPE) {
this.m_JvBD = Vec2.zero();
this.m_JwB = this.m_ratio;
this.m_JwD = this.m_ratio;
this.m_mass += this.m_ratio * this.m_ratio * (this.m_iB + this.m_iD);
} else {
const u = Rot.mulVec2(qD, this.m_localAxisD);
const rD = Rot.mulSub(qD, this.m_localAnchorD, this.m_lcD);
const rB = Rot.mulSub(qB, this.m_localAnchorB, this.m_lcB);
this.m_JvBD = Vec2.mulNumVec2(this.m_ratio, u);
this.m_JwD = this.m_ratio * Vec2.crossVec2Vec2(rD, u);
this.m_JwB = this.m_ratio * Vec2.crossVec2Vec2(rB, u);
this.m_mass += this.m_ratio * this.m_ratio * (this.m_mD + this.m_mB) + this.m_iD * this.m_JwD * this.m_JwD + this.m_iB * this.m_JwB * this.m_JwB;
}
// Compute effective mass.
this.m_mass = this.m_mass > 0.0 ? 1.0 / this.m_mass : 0.0;
if (step.warmStarting) {
vA.addMul(this.m_mA * this.m_impulse, this.m_JvAC);
wA += this.m_iA * this.m_impulse * this.m_JwA;
vB.addMul(this.m_mB * this.m_impulse, this.m_JvBD);
wB += this.m_iB * this.m_impulse * this.m_JwB;
vC.subMul(this.m_mC * this.m_impulse, this.m_JvAC);
wC -= this.m_iC * this.m_impulse * this.m_JwC;
vD.subMul(this.m_mD * this.m_impulse, this.m_JvBD);
wD -= this.m_iD * this.m_impulse * this.m_JwD;
} else {
this.m_impulse = 0.0;
}
this.m_bodyA.c_velocity.v.setVec2(vA);
this.m_bodyA.c_velocity.w = wA;
this.m_bodyB.c_velocity.v.setVec2(vB);
this.m_bodyB.c_velocity.w = wB;
this.m_bodyC.c_velocity.v.setVec2(vC);
this.m_bodyC.c_velocity.w = wC;
this.m_bodyD.c_velocity.v.setVec2(vD);
this.m_bodyD.c_velocity.w = wD;
}
solveVelocityConstraints(step: TimeStep): void {
const vA = this.m_bodyA.c_velocity.v;
let wA = this.m_bodyA.c_velocity.w;
const vB = this.m_bodyB.c_velocity.v;
let wB = this.m_bodyB.c_velocity.w;
const vC = this.m_bodyC.c_velocity.v;
let wC = this.m_bodyC.c_velocity.w;
const vD = this.m_bodyD.c_velocity.v;
let wD = this.m_bodyD.c_velocity.w;
let Cdot = Vec2.dot(this.m_JvAC, vA) - Vec2.dot(this.m_JvAC, vC) + Vec2.dot(this.m_JvBD, vB) - Vec2.dot(this.m_JvBD, vD);
Cdot += (this.m_JwA * wA - this.m_JwC * wC) + (this.m_JwB * wB - this.m_JwD * wD);
const impulse = -this.m_mass * Cdot;
this.m_impulse += impulse;
vA.addMul(this.m_mA * impulse, this.m_JvAC);
wA += this.m_iA * impulse * this.m_JwA;
vB.addMul(this.m_mB * impulse, this.m_JvBD);
wB += this.m_iB * impulse * this.m_JwB;
vC.subMul(this.m_mC * impulse, this.m_JvAC);
wC -= this.m_iC * impulse * this.m_JwC;
vD.subMul(this.m_mD * impulse, this.m_JvBD);
wD -= this.m_iD * impulse * this.m_JwD;
this.m_bodyA.c_velocity.v.setVec2(vA);
this.m_bodyA.c_velocity.w = wA;
this.m_bodyB.c_velocity.v.setVec2(vB);
this.m_bodyB.c_velocity.w = wB;
this.m_bodyC.c_velocity.v.setVec2(vC);
this.m_bodyC.c_velocity.w = wC;
this.m_bodyD.c_velocity.v.setVec2(vD);
this.m_bodyD.c_velocity.w = wD;
}
/**
* This returns true if the position errors are within tolerance.
*/
solvePositionConstraints(step: TimeStep): boolean {
const cA = this.m_bodyA.c_position.c;
let aA = this.m_bodyA.c_position.a;
const cB = this.m_bodyB.c_position.c;
let aB = this.m_bodyB.c_position.a;
const cC = this.m_bodyC.c_position.c;
let aC = this.m_bodyC.c_position.a;
const cD = this.m_bodyD.c_position.c;
let aD = this.m_bodyD.c_position.a;
const qA = Rot.neo(aA);
const qB = Rot.neo(aB);
const qC = Rot.neo(aC);
const qD = Rot.neo(aD);
const linearError = 0.0;
let coordinateA: number;
let coordinateB: number;
let JvAC: Vec2;
let JvBD: Vec2;
let JwA: number;
let JwB: number;
let JwC: number;
let JwD: number;
let mass = 0.0;
if (this.m_type1 == RevoluteJoint.TYPE) {
JvAC = Vec2.zero();
JwA = 1.0;
JwC = 1.0;
mass += this.m_iA + this.m_iC;
coordinateA = aA - aC - this.m_referenceAngleA;
} else {
const u = Rot.mulVec2(qC, this.m_localAxisC);
const rC = Rot.mulSub(qC, this.m_localAnchorC, this.m_lcC);
const rA = Rot.mulSub(qA, this.m_localAnchorA, this.m_lcA);
JvAC = u;
JwC = Vec2.crossVec2Vec2(rC, u);
JwA = Vec2.crossVec2Vec2(rA, u);
mass += this.m_mC + this.m_mA + this.m_iC * JwC * JwC + this.m_iA * JwA * JwA;
const pC = Vec2.sub(this.m_localAnchorC, this.m_lcC);
const pA = Rot.mulTVec2(qC, Vec2.add(rA, Vec2.sub(cA, cC)));
coordinateA = Vec2.dot(Vec2.sub(pA, pC), this.m_localAxisC);
}
if (this.m_type2 == RevoluteJoint.TYPE) {
JvBD = Vec2.zero();
JwB = this.m_ratio;
JwD = this.m_ratio;
mass += this.m_ratio * this.m_ratio * (this.m_iB + this.m_iD);
coordinateB = aB - aD - this.m_referenceAngleB;
} else {
const u = Rot.mulVec2(qD, this.m_localAxisD);
const rD = Rot.mulSub(qD, this.m_localAnchorD, this.m_lcD);
const rB = Rot.mulSub(qB, this.m_localAnchorB, this.m_lcB);
JvBD = Vec2.mulNumVec2(this.m_ratio, u);
JwD = this.m_ratio * Vec2.crossVec2Vec2(rD, u);
JwB = this.m_ratio * Vec2.crossVec2Vec2(rB, u);
mass += this.m_ratio * this.m_ratio * (this.m_mD + this.m_mB) + this.m_iD * JwD * JwD + this.m_iB * JwB * JwB;
const pD = Vec2.sub(this.m_localAnchorD, this.m_lcD);
const pB = Rot.mulTVec2(qD, Vec2.add(rB, Vec2.sub(cB, cD)));
coordinateB = Vec2.dot(pB, this.m_localAxisD) - Vec2.dot(pD, this.m_localAxisD);
}
const C = (coordinateA + this.m_ratio * coordinateB) - this.m_constant;
let impulse = 0.0;
if (mass > 0.0) {
impulse = -C / mass;
}
cA.addMul(this.m_mA * impulse, JvAC);
aA += this.m_iA * impulse * JwA;
cB.addMul(this.m_mB * impulse, JvBD);
aB += this.m_iB * impulse * JwB;
cC.subMul(this.m_mC * impulse, JvAC);
aC -= this.m_iC * impulse * JwC;
cD.subMul(this.m_mD * impulse, JvBD);
aD -= this.m_iD * impulse * JwD;
this.m_bodyA.c_position.c.setVec2(cA);
this.m_bodyA.c_position.a = aA;
this.m_bodyB.c_position.c.setVec2(cB);
this.m_bodyB.c_position.a = aB;
this.m_bodyC.c_position.c.setVec2(cC);
this.m_bodyC.c_position.a = aC;
this.m_bodyD.c_position.c.setVec2(cD);
this.m_bodyD.c_position.a = aD;
// TODO_ERIN not implemented
return linearError < Settings.linearSlop;
}
}