All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dyn4j.dynamics.joint.PulleyJoint Maven / Gradle / Ivy

There is a newer version: 5.0.2
Show newest version
/*
 * Copyright (c) 2010-2022 William Bittle  http://www.dyn4j.org/
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted 
 * provided that the following conditions are met:
 * 
 *   * Redistributions of source code must retain the above copyright notice, this list of conditions 
 *     and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice, this list of conditions 
 *     and the following disclaimer in the documentation and/or other materials provided with the 
 *     distribution.
 *   * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or 
 *     promote products derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.dyn4j.dynamics.joint;

import org.dyn4j.DataContainer;
import org.dyn4j.Epsilon;
import org.dyn4j.Ownable;
import org.dyn4j.dynamics.PhysicsBody;
import org.dyn4j.dynamics.Settings;
import org.dyn4j.dynamics.TimeStep;
import org.dyn4j.exception.ArgumentNullException;
import org.dyn4j.exception.ValueOutOfRangeException;
import org.dyn4j.geometry.Mass;
import org.dyn4j.geometry.Shiftable;
import org.dyn4j.geometry.Transform;
import org.dyn4j.geometry.Vector2;

/**
 * Implementation of a pulley joint.
 * 

* A pulley joint joins two bodies in a pulley system with a fixed length zero * mass rope. The bodies are allowed to rotate freely. The bodies are allowed * to translate freely up to the total length of the "rope." *

* The length of the "rope" connecting the two bodies is computed by distance * from the pulley anchors to the body anchors including the ratio (if any) * when the joint is created. The length can be changed dynamically by calling * the {@link #setLength(double)} method. *

* The pulley anchor points represent the "hanging" points for the respective * bodies and can be any world space point. *

* This joint can also model a block-and-tackle system by setting the ratio * using the {@link #setRatio(double)} method. A value of 1.0 indicates no * ratio. For all values of the ratio, the length of the "rope" stays constant. * The ratio applies only when computing the impulse between the two bodies. * If the ratio is between 0 and 1 exclusive, the second body exhibits the * effect of having 1/x more mass. If the ration is greater than 1, the first * body exhibits the effect of having x more mass. *

* By default this joint acts very similar to two {@link DistanceJoint}s in * that the bodies are forced to be their respective rope-distance away from * the pulley anchors (i.e. not behaving like a rope). To have the bodies * behave as if connected by flexible rope pass in true to the * {@link #setSlackEnabled(boolean)} method. * @author William Bittle * @version 5.0.0 * @since 2.1.0 * @see Documentation * @see Pulley Constraint * @param the {@link PhysicsBody} type */ public class PulleyJoint extends AbstractPairedBodyJoint implements PairedBodyJoint, Joint, Shiftable, DataContainer, Ownable { /** The world space pulley anchor point for the first {@link PhysicsBody} */ protected final Vector2 pulleyAnchor1; /** The world space pulley anchor point for the second {@link PhysicsBody} */ protected final Vector2 pulleyAnchor2; /** The local anchor point on the first {@link PhysicsBody} */ protected final Vector2 localAnchor1; /** The local anchor point on the second {@link PhysicsBody} */ protected final Vector2 localAnchor2; /** The pulley ratio for modeling a block-and-tackle */ protected double ratio; /** True if slack in the rope is enabled */ protected boolean slackEnabled; // current state /** The state of the limit (only used for slack) */ private boolean overLength; /** The total length of the pulley system */ private double length; /** The normal from the first pulley anchor to the first {@link PhysicsBody} anchor */ private Vector2 n1; /** The normal from the second pulley anchor to the second {@link PhysicsBody} anchor */ private Vector2 n2; /** The effective mass of the two body system (Kinv = J * Minv * Jtrans) */ private double invK; // output /** The accumulated impulse from the previous time step */ private double impulse; /** * Minimal constructor. *

* Creates a pulley joint between the two given {@link PhysicsBody}s using the given anchor points. * @param body1 the first {@link PhysicsBody} * @param body2 the second {@link PhysicsBody} * @param pulleyAnchor1 the first pulley anchor point * @param pulleyAnchor2 the second pulley anchor point * @param bodyAnchor1 the first {@link PhysicsBody}'s anchor point * @param bodyAnchor2 the second {@link PhysicsBody}'s anchor point * @throws NullPointerException if body1, body2, pulleyAnchor1, pulleyAnchor2, bodyAnchor1, or bodyAnchor2 is null * @throws IllegalArgumentException if body1 == body2 */ public PulleyJoint(T body1, T body2, Vector2 pulleyAnchor1, Vector2 pulleyAnchor2, Vector2 bodyAnchor1, Vector2 bodyAnchor2) { super(body1, body2); // verify the pulley anchor points are not null if (pulleyAnchor1 == null) throw new ArgumentNullException("pulleyAnchor1"); if (pulleyAnchor2 == null) throw new ArgumentNullException("pulleyAnchor2"); // verify the body anchor points are not null if (bodyAnchor1 == null) throw new ArgumentNullException("bodyAnchor1"); if (bodyAnchor2 == null) throw new ArgumentNullException("bodyAnchor2"); // set the pulley anchor points this.pulleyAnchor1 = pulleyAnchor1.copy(); this.pulleyAnchor2 = pulleyAnchor2.copy(); // get the local anchor points this.localAnchor1 = body1.getLocalPoint(bodyAnchor1); this.localAnchor2 = body2.getLocalPoint(bodyAnchor2); // default the ratio and minimum length this.ratio = 1.0; // compute the lengths double length1 = bodyAnchor1.distance(pulleyAnchor1); double length2 = bodyAnchor2.distance(pulleyAnchor2); // compute the lengths // length = l1 + ratio * l2 this.length = length1 + length2; // initialize the other fields this.impulse = 0.0; // initialize the slack parameters this.slackEnabled = false; this.overLength = false; } /* (non-Javadoc) * @see org.dyn4j.dynamics.joint.Joint#toString() */ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("PulleyJoint[").append(super.toString()) .append("|PulleyAnchor1=").append(this.pulleyAnchor1) .append("|PulleyAnchor2=").append(this.pulleyAnchor2) .append("|Anchor1=").append(this.getAnchor1()) .append("|Anchor2=").append(this.getAnchor2()) .append("|Ratio=").append(this.ratio) .append("|Length=").append(this.length) .append("|SlackEnabled=").append(this.slackEnabled) .append("]"); return sb.toString(); } /* (non-Javadoc) * @see org.dyn4j.dynamics.joint.Joint#initializeConstraints(org.dyn4j.dynamics.TimeStep, org.dyn4j.dynamics.Settings) */ @Override public void initializeConstraints(TimeStep step, Settings settings) { double linearTolerance = settings.getLinearTolerance(); Transform t1 = this.body1.getTransform(); Transform t2 = this.body2.getTransform(); Mass m1 = this.body1.getMass(); Mass m2 = this.body2.getMass(); double invM1 = m1.getInverseMass(); double invM2 = m2.getInverseMass(); double invI1 = m1.getInverseInertia(); double invI2 = m2.getInverseInertia(); // put the body anchors in world space Vector2 r1 = t1.getTransformedR(this.body1.getLocalCenter().to(this.localAnchor1)); Vector2 r2 = t2.getTransformedR(this.body2.getLocalCenter().to(this.localAnchor2)); Vector2 p1 = r1.sum(this.body1.getWorldCenter()); Vector2 p2 = r2.sum(this.body2.getWorldCenter()); Vector2 s1 = this.pulleyAnchor1; Vector2 s2 = this.pulleyAnchor2; // compute the axes this.n1 = s1.to(p1); this.n2 = s2.to(p2); // get the lengths double l1 = this.n1.normalize(); double l2 = this.n2.normalize(); // get the current total length double l = l1 + l2; // check if we need to solve the constraint if (l > this.length || !this.slackEnabled) { this.overLength = true; // check for near zero length if (l1 <= 10.0 * linearTolerance) { // zero out the axis this.n1.zero(); } // check for near zero length if (l2 <= 10.0 * linearTolerance) { // zero out the axis this.n2.zero(); } // compute the inverse effective masses (K matrix, in this case its a scalar) for the constraints double r1CrossN1 = r1.cross(this.n1); double r2CrossN2 = r2.cross(this.n2); double pm1 = invM1 + invI1 * r1CrossN1 * r1CrossN1; double pm2 = invM2 + invI2 * r2CrossN2 * r2CrossN2; this.invK = pm1 + this.ratio * this.ratio * pm2; // make sure we can invert it if (this.invK > Epsilon.E) { this.invK = 1.0 / this.invK; } else { this.invK = 0.0; } if (settings.isWarmStartingEnabled()) { // warm start the constraints taking // variable time steps into account double dtRatio = step.getDeltaTimeRatio(); this.impulse *= dtRatio; // compute the impulse along the axes Vector2 J1 = this.n1.product(-this.impulse); Vector2 J2 = this.n2.product(-this.ratio * this.impulse); // apply the impulse this.body1.getLinearVelocity().add(J1.product(invM1)); this.body1.setAngularVelocity(this.body1.getAngularVelocity() + invI1 * r1.cross(J1)); this.body2.getLinearVelocity().add(J2.product(invM2)); this.body2.setAngularVelocity(this.body2.getAngularVelocity() + invI2 * r2.cross(J2)); } else { this.impulse = 0.0; } } else { // clear the impulse and don't solve anything this.impulse = 0; this.overLength = false; } } /* (non-Javadoc) * @see org.dyn4j.dynamics.joint.Joint#solveVelocityConstraints(org.dyn4j.dynamics.TimeStep, org.dyn4j.dynamics.Settings) */ @Override public void solveVelocityConstraints(TimeStep step, Settings settings) { if (this.overLength || !this.slackEnabled) { Transform t1 = this.body1.getTransform(); Transform t2 = this.body2.getTransform(); Mass m1 = this.body1.getMass(); Mass m2 = this.body2.getMass(); double invM1 = m1.getInverseMass(); double invM2 = m2.getInverseMass(); double invI1 = m1.getInverseInertia(); double invI2 = m2.getInverseInertia(); // compute r1 and r2 Vector2 r1 = t1.getTransformedR(this.body1.getLocalCenter().to(this.localAnchor1)); Vector2 r2 = t2.getTransformedR(this.body2.getLocalCenter().to(this.localAnchor2)); // compute the relative velocity Vector2 v1 = this.body1.getLinearVelocity().sum(r1.cross(this.body1.getAngularVelocity())); Vector2 v2 = this.body2.getLinearVelocity().sum(r2.cross(this.body2.getAngularVelocity())); // compute Jv + b double C = -this.n1.dot(v1) - this.ratio * this.n2.dot(v2); // compute the impulse double impulse = this.invK * (-C); this.impulse += impulse; // compute the impulse along each axis Vector2 J1 = this.n1.product(-impulse); Vector2 J2 = this.n2.product(-impulse * this.ratio); // apply the impulse this.body1.getLinearVelocity().add(J1.product(invM1)); this.body1.setAngularVelocity(this.body1.getAngularVelocity() + invI1 * r1.cross(J1)); this.body2.getLinearVelocity().add(J2.product(invM2)); this.body2.setAngularVelocity(this.body2.getAngularVelocity() + invI2 * r2.cross(J2)); } } /* (non-Javadoc) * @see org.dyn4j.dynamics.joint.Joint#solvePositionConstraints(org.dyn4j.dynamics.TimeStep, org.dyn4j.dynamics.Settings) */ @Override public boolean solvePositionConstraints(TimeStep step, Settings settings) { if (this.overLength || !this.slackEnabled) { double linearTolerance = settings.getLinearTolerance(); Transform t1 = this.body1.getTransform(); Transform t2 = this.body2.getTransform(); Mass m1 = this.body1.getMass(); Mass m2 = this.body2.getMass(); double invM1 = m1.getInverseMass(); double invM2 = m2.getInverseMass(); double invI1 = m1.getInverseInertia(); double invI2 = m2.getInverseInertia(); // put the body anchors in world space Vector2 r1 = t1.getTransformedR(this.body1.getLocalCenter().to(this.localAnchor1)); Vector2 r2 = t2.getTransformedR(this.body2.getLocalCenter().to(this.localAnchor2)); Vector2 p1 = r1.sum(this.body1.getWorldCenter()); Vector2 p2 = r2.sum(this.body2.getWorldCenter()); Vector2 s1 = this.pulleyAnchor1; Vector2 s2 = this.pulleyAnchor2; // compute the axes this.n1 = s1.to(p1); this.n2 = s2.to(p2); // normalize and save the length double l1 = this.n1.normalize(); double l2 = this.n2.normalize(); // make sure the length is not near zero if (l1 <= 10.0 * linearTolerance) { this.n1.zero(); } // make sure the length is not near zero if (l2 <= 10.0 * linearTolerance) { this.n2.zero(); } double linearError = 0.0; // recompute K double r1CrossN1 = r1.cross(this.n1); double r2CrossN2 = r2.cross(this.n2); double pm1 = invM1 + invI1 * r1CrossN1 * r1CrossN1; double pm2 = invM2 + invI2 * r2CrossN2 * r2CrossN2; this.invK = pm1 + pm2; // make sure we can invert it if (this.invK > Epsilon.E) { this.invK = 1.0 / this.invK; } else { this.invK = 0.0; } // compute the constraint error double C = this.length - l1 - l2; linearError = Math.abs(C); // clamp the error double impulse = -this.invK * C; // compute the impulse along the axes Vector2 J1 = this.n1.product(-impulse); Vector2 J2 = this.n2.product(-impulse); // apply the impulse this.body1.translate(J1.x * invM1, J1.y * invM1); this.body1.rotateAboutCenter(r1.cross(J1) * invI1); this.body2.translate(J2.x * invM2, J2.y * invM2); this.body2.rotateAboutCenter(r2.cross(J2) * invI2); return linearError < linearTolerance; } return true; } /** * Returns the world space anchor point on the first body. * @return {@link Vector2} */ public Vector2 getAnchor1() { return body1.getWorldPoint(this.localAnchor1); } /** * Returns the world space anchor point on the second body. * @return {@link Vector2} */ public Vector2 getAnchor2() { return body2.getWorldPoint(this.localAnchor2); } /* (non-Javadoc) * @see org.dyn4j.dynamics.joint.Joint#getReactionForce(double) */ @Override public Vector2 getReactionForce(double invdt) { return this.n2.product(this.impulse * invdt); } /** * {@inheritDoc} *

* Not applicable to this joint. * Always returns zero. */ @Override public double getReactionTorque(double invdt) { return 0.0; } /* (non-Javadoc) * @see org.dyn4j.geometry.Shiftable#shift(org.dyn4j.geometry.Vector2) */ @Override public void shift(Vector2 shift) { // we must move the world space pulley anchors this.pulleyAnchor1.add(shift); this.pulleyAnchor2.add(shift); } /** * Returns the pulley anchor point for the first {@link PhysicsBody} * in world coordinates. * @return {@link Vector2} */ public Vector2 getPulleyAnchor1() { return this.pulleyAnchor1; } /** * Returns the pulley anchor point for the second {@link PhysicsBody} * in world coordinates. * @return {@link Vector2} */ public Vector2 getPulleyAnchor2() { return this.pulleyAnchor2; } /** * Returns the target total length of the pulley "rope." *

* NOTE: this is the target length with the ratio taken into account. * @since 3.0.1 * @return double * @see #setLength(double) */ public double getLength() { return this.length; } /** * Returns the current length of the pulley "rope." *

* This should always be close to, or equal to, the {@link #getLength()}. * @return double * @since 4.2.0 */ public double getCurrentLength() { Vector2 a1 = this.body1.getWorldPoint(this.localAnchor1); Vector2 a2 = this.body2.getWorldPoint(this.localAnchor2); double l1 = this.pulleyAnchor1.distance(a1); double l2 = this.pulleyAnchor2.distance(a2); return l1 + l2; } /** * Sets the total length of the pulley "rope." *

* Typically this is computed when the joint is created by adding the distance from the * first body anchor to the first pulley anchor with the distance from the second body anchor * to the second pulley anchor. * @param length the length; must be greater than or equal to zero * @since 3.2.1 * @throws IllegalArgumentException if length is less than zero */ public void setLength(double length) { if (length < 0.0) throw new ValueOutOfRangeException("length", length, ValueOutOfRangeException.MUST_BE_GREATER_THAN_OR_EQUAL_TO, 0.0); if (this.length != length) { this.length = length; // wake up both bodies this.body1.setAtRest(false); this.body2.setAtRest(false); } } /** * Returns the current length from the first pulley anchor point to the * anchor point on the first {@link PhysicsBody}. *

* This is used, in conjunction with length2, to compute the total length * when the ratio is changed. * @return double * @since 4.2.0 */ public double getCurrentLength1() { // get the body anchor point in world space Vector2 ba = this.body1.getWorldPoint(this.localAnchor1); return this.pulleyAnchor1.distance(ba); } /** * Returns the current length from the second pulley anchor point to the * anchor point on the second {@link PhysicsBody}. *

* This is used, in conjunction with length1, to compute the total length * when the ratio is changed. * @return double * @since 4.2.0 */ public double getCurrentLength2() { // get the body anchor point in world space Vector2 ba = this.body2.getWorldPoint(this.localAnchor2); return this.pulleyAnchor2.distance(ba); } /** * Returns the pulley ratio. * @return double */ public double getRatio() { return this.ratio; } /** * Sets the pulley ratio. *

* The ratio value is used to simulate a block-and-tackle. A ratio of 1.0 is the default * and indicates that the pulley is not a block-and-tackle. *

* A ratio in the range (0, 1) indicates that the second body weighs more. A ratio in the * range of (1, ∞] indicates that the first body weighs more. * @param ratio the ratio; must be greater than zero * @throws IllegalArgumentException if ratio is less than or equal to zero */ public void setRatio(double ratio) { if (ratio <= 0.0) throw new ValueOutOfRangeException("ratio", ratio, ValueOutOfRangeException.MUST_BE_GREATER_THAN, 0.0); // make sure the ratio changed if (ratio != this.ratio) { // set the new ratio this.ratio = ratio; // wake up both bodies this.body1.setAtRest(false); this.body2.setAtRest(false); } } /** * Returns true if slack in the rope is enabled. * @return boolean * @since 3.1.6 */ public boolean isSlackEnabled() { return this.slackEnabled; } /** * Toggles the slack in the rope. *

* If slack is not enabled the rope length is fixed to the total length of the rope, acting like the {@link DistanceJoint}. * @param flag true to enable slack * @since 3.1.6 */ public void setSlackEnabled(boolean flag) { // make sure the ratio changed if (this.slackEnabled != flag) { // set the new ratio this.slackEnabled = flag; // wake up both bodies this.body1.setAtRest(false); this.body2.setAtRest(false); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy