/* * __ .__ .__ ._____. * _/ |_ _______ __|__| ____ | | |__\_ |__ ______ * \ __\/ _ \ \/ / |/ ___\| | | || __ \ / ___/ * | | ( <_> > <| \ \___| |_| || \_\ \\___ \ * |__| \____/__/\_ \__|\___ >____/__||___ /____ > * \/ \/ \/ \/ * * Copyright (c) 2006-2011 Karsten Schmidt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * http://creativecommons.org/licenses/LGPL/2.1/ * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ package toxi.geom; import java.util.Random; import javax.xml.bind.annotation.XmlAttribute; import toxi.math.InterpolateStrategy; import toxi.math.MathUtils; import toxi.math.ScaleMap; /** * Comprehensive 2D vector class with additional basic intersection and * collision detection features. */ public class Vec2D implements Comparable, ReadonlyVec2D { /** * */ public static enum Axis { /** * */ X(Vec2D.X_AXIS), /** * */ Y(Vec2D.Y_AXIS); private final ReadonlyVec2D vector; private Axis(ReadonlyVec2D v) { this.vector = v; } /** * * @return */ public ReadonlyVec2D getVector() { return vector; } } /** * Defines positive X axis */ public static final ReadonlyVec2D X_AXIS = new Vec2D(1, 0); /** * Defines positive Y axis */ public static final ReadonlyVec2D Y_AXIS = new Vec2D(0, 1); ; /** Defines the zero vector. */ public static final ReadonlyVec2D ZERO = new Vec2D(); /** * Defines vector with both coords set to Float.MIN_VALUE. Useful for * bounding box operations. */ public static final ReadonlyVec2D MIN_VALUE = new Vec2D(Float.MIN_VALUE, Float.MIN_VALUE); /** * Defines vector with both coords set to Float.MAX_VALUE. Useful for * bounding box operations. */ public static final ReadonlyVec2D MAX_VALUE = new Vec2D(Float.MAX_VALUE, Float.MAX_VALUE); /** * */ public static final ReadonlyVec2D NEG_MAX_VALUE = new Vec2D( -Float.MAX_VALUE, -Float.MAX_VALUE); /** * Creates a new vector from the given angle in the XY plane. * * The resulting vector for theta=0 is equal to the positive X axis. * * @param theta * @return new vector pointing into the direction of the passed in angle */ public static final Vec2D fromTheta(float theta) { return new Vec2D((float) Math.cos(theta), (float) Math.sin(theta)); } /** * Constructs a new vector consisting of the largest components of both * vectors. * * @param b the b * @param a the a * * @return result as new vector */ public static final Vec2D max(ReadonlyVec2D a, ReadonlyVec2D b) { return new Vec2D(MathUtils.max(a.x(), b.x()), MathUtils.max(a.y(), b.y())); } /** * Constructs a new vector consisting of the smallest components of both * vectors. * * @param b comparing vector * @param a the a * * @return result as new vector */ public static final Vec2D min(ReadonlyVec2D a, ReadonlyVec2D b) { return new Vec2D(MathUtils.min(a.x(), b.x()), MathUtils.min(a.y(), b.y())); } /** * Static factory method. Creates a new random unit vector using the Random * implementation set as default for the {@link MathUtils} class. * * @return a new random normalized unit vector. */ public static final Vec2D randomVector() { return randomVector(MathUtils.RND); } /** * Static factory method. Creates a new random unit vector using the given * Random generator instance. I recommend to have a look at the * https://uncommons-maths.dev.java.net library for a good choice of * reliable and high quality random number generators. * * @param rnd * @return a new random normalized unit vector. */ public static final Vec2D randomVector(Random rnd) { Vec2D v = new Vec2D(rnd.nextFloat() * 2 - 1, rnd.nextFloat() * 2 - 1); return v.normalize(); } /** * X coordinate */ @XmlAttribute(required = true) public float x; /** * Y coordinate */ @XmlAttribute(required = true) public float y; /** * Creates a new zero vector */ public Vec2D() { x = y = 0; } /** * Creates a new vector with the given coordinates * * @param x * @param y */ public Vec2D(float x, float y) { this.x = x; this.y = y; } /** * * @param v */ public Vec2D(float[] v) { this.x = v[0]; this.y = v[1]; } /** * Creates a new vector with the coordinates of the given vector * * @param v vector to be copied */ public Vec2D(ReadonlyVec2D v) { this.x = v.x(); this.y = v.y(); } /** * * @return */ public final Vec2D abs() { x = MathUtils.abs(x); y = MathUtils.abs(y); return this; } @Override public final Vec2D add(float a, float b) { return new Vec2D(x + a, y + b); } @Override public Vec2D add(ReadonlyVec2D v) { return new Vec2D(x + v.x(), y + v.y()); } /** * * @param v * @return */ public final Vec2D add(Vec2D v) { return new Vec2D(x + v.x, y + v.y); } /** * Adds vector {a,b,c} and overrides coordinates with result. * * @param a X coordinate * @param b Y coordinate * @return itself */ public final Vec2D addSelf(float a, float b) { x += a; y += b; return this; } /** * Adds vector v and overrides coordinates with result. * * @param v vector to add * @return itself */ public final Vec2D addSelf(Vec2D v) { x += v.x; y += v.y; return this; } @Override public final float angleBetween(ReadonlyVec2D v) { return (float) Math.acos(dot(v)); } @Override public final float angleBetween(ReadonlyVec2D v, boolean forceNormalize) { float theta; if (forceNormalize) { theta = getNormalized().dot(v.getNormalized()); } else { theta = dot(v); } return (float) Math.acos(MathUtils.clipNormalized(theta)); } @Override public Vec3D bisect(Vec2D b) { Vec2D diff = this.sub(b); Vec2D sum = this.add(b); float dot = diff.dot(sum); return new Vec3D(diff.x, diff.y, -dot / 2); } /** * Sets all vector components to 0. * * @return itself */ public final Vec2D clear() { x = y = 0; return this; } @Override public int compareTo(ReadonlyVec2D o) { if (this.equals(o)) { return 0; } int result = (this.magSquared() < o.magSquared()) ? -1 : 1; return result; } /** * Constraints this vector to the perimeter of the given polygon. Unlike the * {@link #constrain(Rect)} version of this method, this version DOES NOT * check containment automatically. If you want to only constrain a point if * its (for example) outside the polygon, then check containment with * {@link Polygon2D#containsPoint(ReadonlyVec2D)} first before calling this * method. * * @param poly * @return itself */ public Vec2D constrain(Polygon2D poly) { float minD = Float.MAX_VALUE; Vec2D q = null; for (Line2D l : poly.getEdges()) { Vec2D c = l.closestPointTo(this); float d = c.distanceToSquared(this); if (d < minD) { q = c; minD = d; } } if (q != null) { x = q.x; y = q.y; } return this; } /** * Forcefully fits the vector in the given rectangle. * * @param r * @return itself */ public Vec2D constrain(Rect r) { x = MathUtils.clip(x, r.x, r.x + r.width); y = MathUtils.clip(y, r.y, r.y + r.height); return this; } /** * Forcefully fits the vector in the given rectangle defined by the points. * * @param min * @param max * @return itself */ public Vec2D constrain(Vec2D min, Vec2D max) { x = MathUtils.clip(x, min.x, max.x); y = MathUtils.clip(y, min.y, max.y); return this; } @Override public final Vec2D copy() { return new Vec2D(this); } @Override public float cross(ReadonlyVec2D v) { return (x * v.y()) - (y * v.x()); } @Override public final float distanceTo(ReadonlyVec2D v) { if (v != null) { float dx = x - v.x(); float dy = y - v.y(); return (float) Math.sqrt(dx * dx + dy * dy); } else { return Float.NaN; } } @Override public final float distanceToSquared(ReadonlyVec2D v) { if (v != null) { float dx = x - v.x(); float dy = y - v.y(); return dx * dx + dy * dy; } else { return Float.NaN; } } @Override public final float dot(ReadonlyVec2D v) { return x * v.x() + y * v.y(); } /** * Returns true if the Object v is of type ReadonlyVec2D and all of the data * members of v are equal to the corresponding data members in this vector. * * @param obj the object with which the comparison is made * @return true or false */ @Override public boolean equals(Object obj) { if (obj instanceof ReadonlyVec2D) { final ReadonlyVec2D other = (ReadonlyVec2D) obj; if (!((Float) x).equals(other.x())) { return false; } return ((Float) y).equals(other.y()); } return false; } /** * Returns a hash code value based on the data values in this object. Two * different Vec2D objects with identical data values (i.e., Vec2D.equals * returns true) will return the same hash code value. Two objects with * different data members may return the same hash value, although this is * not likely. * * @return the hash code value of this vector. */ @Override public int hashCode() { int hash = 7; hash = 97 * hash + Float.floatToIntBits(this.x); hash = 97 * hash + Float.floatToIntBits(this.y); return hash; } /** * Returns true if all of the data members of ReadonlyVec2D v are equal to * the corresponding data members in this vector. * * @param v the vector with which the comparison is made * @return true or false */ public boolean equals(ReadonlyVec2D v) { final ReadonlyVec2D other = (ReadonlyVec2D) v; if (!((Float) x).equals(other.x())) { return false; } return ((Float) y).equals(other.y()); } @Override public boolean equalsWithTolerance(ReadonlyVec2D v, float tolerance) { if (v instanceof ReadonlyVec2D){ float diff = x - v.x(); if (Float.isNaN(diff)) { return false; } if ((diff < 0 ? -diff : diff) > tolerance) { return false; } diff = y - v.y(); if (Float.isNaN(diff)) { return false; } return ((diff < 0 ? -diff : diff) < tolerance); } return false; } /** * Replaces the vector components with integer values of their current * values * * @return itself */ public final Vec2D floor() { x = MathUtils.floor(x); y = MathUtils.floor(y); return this; } /** * Replaces the vector components with the fractional part of their current * values * * @return itself */ public final Vec2D frac() { x -= MathUtils.floor(x); y -= MathUtils.floor(y); return this; } /** * * @return */ @Override public final Vec2D getAbs() { return new Vec2D(this).abs(); } @Override public Vec2D getCartesian() { return copy().toCartesian(); } /** * * @param id * @return */ @Override public float getComponent(Axis id) { switch (id) { case X: return x; case Y: return y; } return 0; } /** * * @param id * @return */ @Override public final float getComponent(int id) { switch (id) { case 0: return x; case 1: return y; } throw new IllegalArgumentException("index must be 0 or 1"); } /** * * @param poly * @return */ public final Vec2D getConstrained(Polygon2D poly) { return new Vec2D(this).constrain(poly); } @Override public final Vec2D getConstrained(Rect r) { return new Vec2D(this).constrain(r); } @Override public final Vec2D getFloored() { return new Vec2D(this).floor(); } @Override public final Vec2D getFrac() { return new Vec2D(this).frac(); } @Override public final Vec2D getInverted() { return new Vec2D(-x, -y); } @Override public final Vec2D getLimited(float lim) { if (magSquared() > lim * lim) { return getNormalizedTo(lim); } return new Vec2D(this); } @Override public Vec2D getMapped(ScaleMap map) { return new Vec2D((float) map.getClippedValueFor(x), (float) map.getClippedValueFor(y)); } @Override public final Vec2D getNormalized() { return new Vec2D(this).normalize(); } @Override public final Vec2D getNormalizedTo(float len) { return new Vec2D(this).normalizeTo(len); } /** * * @return */ @Override public final Vec2D getPerpendicular() { return new Vec2D(this).perpendicular(); } @Override public Vec2D getPolar() { return copy().toPolar(); } /** * * @return */ @Override public final Vec2D getReciprocal() { return copy().reciprocal(); } /** * * @param normal * @return */ @Override public final Vec2D getReflected(ReadonlyVec2D normal) { return copy().reflect(normal); } @Override public final Vec2D getRotated(float theta) { return new Vec2D(this).rotate(theta); } @Override public Vec2D getRoundedTo(float prec) { return copy().roundTo(prec); } @Override public Vec2D getSignum() { return new Vec2D(this).signum(); } @Override public final float heading() { return (float) Math.atan2(y, x); } @Override public Vec2D interpolateTo(ReadonlyVec2D v, float f) { return new Vec2D(x + (v.x() - x) * f, y + (v.y() - y) * f); } @Override public Vec2D interpolateTo(ReadonlyVec2D v, float f, InterpolateStrategy s) { return new Vec2D(s.interpolate(x, v.x(), f), s.interpolate(y, v.y(), f)); } /** * * @param v * @param f * @return */ public final Vec2D interpolateTo(Vec2D v, float f) { return new Vec2D(x + (v.x - x) * f, y + (v.y - y) * f); } /** * * @param v * @param f * @param s * @return */ public Vec2D interpolateTo(Vec2D v, float f, InterpolateStrategy s) { return new Vec2D(s.interpolate(x, v.x, f), s.interpolate(y, v.y, f)); } /** * Interpolates the vector towards the given target vector, using linear * interpolation * * @param v target vector * @param f interpolation factor (should be in the range 0..1) * @return itself, result overrides current vector */ public final Vec2D interpolateToSelf(ReadonlyVec2D v, float f) { x += (v.x() - x) * f; y += (v.y() - y) * f; return this; } /** * Interpolates the vector towards the given target vector, using the given * {@link InterpolateStrategy} * * @param v target vector * @param f interpolation factor (should be in the range 0..1) * @param s InterpolateStrategy instance * @return itself, result overrides current vector */ public Vec2D interpolateToSelf(ReadonlyVec2D v, float f, InterpolateStrategy s) { x = s.interpolate(x, v.x(), f); y = s.interpolate(y, v.y(), f); return this; } /** * Scales vector uniformly by factor -1 ( v = -v ), overrides coordinates * with result * * @return itself */ public final Vec2D invert() { x *= -1; y *= -1; return this; } @Override public boolean isInCircle(ReadonlyVec2D sO, float sR) { float d = sub(sO).magSquared(); return (d <= sR * sR); } /** * Simplified * @param r * @return */ @Override public boolean isInRectangle(Rect r) { if (x < r.x || x > r.x + r.width) { return false; } return (y >= r.y || y <= r.y + r.height); } @Override public boolean isInTriangle(Vec2D a, Vec2D b, Vec2D c) { Vec2D v1 = sub(a).normalize(); Vec2D v2 = sub(b).normalize(); Vec2D v3 = sub(c).normalize(); double total_angles = Math.acos(v1.dot(v2)); total_angles += Math.acos(v2.dot(v3)); total_angles += Math.acos(v3.dot(v1)); return (MathUtils.abs((float) total_angles - MathUtils.TWO_PI) <= 0.005f); } @Override public final boolean isMajorAxis(float tol) { float ax = MathUtils.abs(x); float ay = MathUtils.abs(y); float itol = 1 - tol; if (ax > itol) { return (ay < tol); } else if (ay > itol) { return (ax < tol); } return false; } @Override public final boolean isZeroVector() { return MathUtils.abs(x) < MathUtils.EPS && MathUtils.abs(y) < MathUtils.EPS; } /** * * @param j * @return */ public final Vec2D jitter(float j) { return jitter(j, j); } /** * Adds random jitter to the vector in the range -j ... +j using the default * {@link Random} generator of {@link MathUtils}. * * @param jx maximum x jitter * @param jy maximum y jitter * @return itself */ public final Vec2D jitter(float jx, float jy) { x += MathUtils.normalizedRandom() * jx; y += MathUtils.normalizedRandom() * jy; return this; } /** * * @param rnd * @param j * @return */ public final Vec2D jitter(Random rnd, float j) { return jitter(rnd, j, j); } /** * * @param rnd * @param jx * @param jy * @return */ public final Vec2D jitter(Random rnd, float jx, float jy) { x += MathUtils.normalizedRandom(rnd) * jx; y += MathUtils.normalizedRandom(rnd) * jy; return this; } /** * * @param rnd * @param jv * @return */ public final Vec2D jitter(Random rnd, Vec2D jv) { return jitter(rnd, jv.x, jv.y); } /** * * @param jv * @return */ public final Vec2D jitter(Vec2D jv) { return jitter(jv.x, jv.y); } /** * Limits the vector's magnitude to the length given * * @param lim new maximum magnitude * @return itself */ public final Vec2D limit(float lim) { if (magSquared() > lim * lim) { return normalize().scaleSelf(lim); } return this; } @Override public final float magnitude() { return (float) Math.sqrt(x * x + y * y); } @Override public final float magSquared() { return x * x + y * y; } @Override public final Vec2D max(ReadonlyVec2D v) { return new Vec2D(MathUtils.max(x, v.x()), MathUtils.max(y, v.y())); } /** * Adjusts the vector components to the maximum values of both vectors * * @param v * @return itself */ public final Vec2D maxSelf(ReadonlyVec2D v) { x = MathUtils.max(x, v.x()); y = MathUtils.max(y, v.y()); return this; } @Override public final Vec2D min(ReadonlyVec2D v) { return new Vec2D(MathUtils.min(x, v.x()), MathUtils.min(y, v.y())); } /** * Adjusts the vector components to the minimum values of both vectors * * @param v * @return itself */ public final Vec2D minSelf(ReadonlyVec2D v) { x = MathUtils.min(x, v.x()); y = MathUtils.min(y, v.y()); return this; } /** * Normalizes the vector so that its magnitude = 1 * * @return itself */ public final Vec2D normalize() { float mag = x * x + y * y; if (mag > 0) { mag = 1f / (float) Math.sqrt(mag); x *= mag; y *= mag; } return this; } /** * Normalizes the vector to the given length. * * @param len desired length * @return itself */ public final Vec2D normalizeTo(float len) { float mag = (float) Math.sqrt(x * x + y * y); if (mag > 0) { mag = len / mag; x *= mag; y *= mag; } return this; } /** * * @return */ public final Vec2D perpendicular() { float t = x; x = -y; y = t; return this; } /** * * @return */ public final float positiveHeading() { double dist = Math.sqrt(x * x + y * y); if (y >= 0) { return (float) Math.acos(x / dist); } else { return (float) (Math.acos(-x / dist) + MathUtils.PI); } } /** * * @return */ public final Vec2D reciprocal() { x = 1f / x; y = 1f / y; return this; } /** * * @param normal * @return */ public final Vec2D reflect(ReadonlyVec2D normal) { return set(normal.scale(this.dot(normal) * 2).subSelf(this)); } /** * Rotates the vector by the given angle around the Z axis. * * @param theta * @return itself */ public final Vec2D rotate(float theta) { float co = (float) Math.cos(theta); float si = (float) Math.sin(theta); float xx = co * x - si * y; y = si * x + co * y; x = xx; return this; } /** * * @param prec * @return */ public Vec2D roundTo(float prec) { x = MathUtils.roundTo(x, prec); y = MathUtils.roundTo(y, prec); return this; } @Override public final Vec2D scale(float s) { return new Vec2D(x * s, y * s); } @Override public final Vec2D scale(float a, float b) { return new Vec2D(x * a, y * b); } /** * * @param s * @return */ @Override public final Vec2D scale(ReadonlyVec2D s) { return s.copy().scaleSelf(this); } @Override public final Vec2D scale(Vec2D s) { return new Vec2D(x * s.x, y * s.y); } /** * Scales vector uniformly and overrides coordinates with result * * @param s scale factor * @return itself */ public final Vec2D scaleSelf(float s) { x *= s; y *= s; return this; } /** * Scales vector non-uniformly by vector {a,b,c} and overrides coordinates * with result * * @param a scale factor for X coordinate * @param b scale factor for Y coordinate * @return itself */ public final Vec2D scaleSelf(float a, float b) { x *= a; y *= b; return this; } /** * Scales vector non-uniformly by vector v and overrides coordinates with * result * * @param s scale vector * @return itself */ public final Vec2D scaleSelf(Vec2D s) { x *= s.x; y *= s.y; return this; } /** * Overrides coordinates with the given values * * @param x * @param y * @return itself */ public final Vec2D set(float x, float y) { this.x = x; this.y = y; return this; } /** * * @param v * @return */ public final Vec2D set(ReadonlyVec2D v) { x = v.x(); y = v.y(); return this; } /** * Overrides coordinates with the ones of the given vector * * @param v vector to be copied * @return itself */ public final Vec2D set(Vec2D v) { x = v.x; y = v.y; return this; } /** * * @param id * @param val * @return */ public final Vec2D setComponent(Axis id, float val) { switch (id) { case X: x = val; break; case Y: y = val; break; } return this; } /** * * @param id * @param val * @return */ public final Vec2D setComponent(int id, float val) { switch (id) { case 0: x = val; break; case 1: y = val; break; default: throw new IllegalArgumentException( "component id needs to be 0 or 1"); } return this; } /** * * @param x * @return */ public Vec2D setX(float x) { this.x = x; return this; } /** * * @param y * @return */ public Vec2D setY(float y) { this.y = y; return this; } /** * Replaces all vector components with the signum of their original values. * In other words if a components value was negative its new value will be * -1, if zero => 0, if positive => +1 * * @return itself */ public final Vec2D signum() { x = (x < 0 ? -1 : x == 0 ? 0 : 1); y = (y < 0 ? -1 : y == 0 ? 0 : 1); return this; } /** * Rounds the vector to the closest major axis. Assumes the vector is * normalized. * * @return itself */ public final Vec2D snapToAxis() { if (MathUtils.abs(x) < 0.5f) { x = 0; } else { x = x < 0 ? -1 : 1; y = 0; } if (MathUtils.abs(y) < 0.5f) { y = 0; } else { y = y < 0 ? -1 : 1; x = 0; } return this; } @Override public final Vec2D sub(float a, float b) { return new Vec2D(x - a, y - b); } /** * * @param v * @return */ @Override public final Vec2D sub(ReadonlyVec2D v) { return new Vec2D(x - v.x(), y - v.y()); } @Override public final Vec2D sub(Vec2D v) { return new Vec2D(x - v.x, y - v.y); } /** * Subtracts vector {a,b,c} and overrides coordinates with result. * * @param a X coordinate * @param b Y coordinate * @return itself */ public final Vec2D subSelf(float a, float b) { x -= a; y -= b; return this; } /** * Subtracts vector v and overrides coordinates with result. * * @param v vector to be subtracted * @return itself */ public final Vec2D subSelf(Vec2D v) { x -= v.x; y -= v.y; return this; } @Override public final Vec2D tangentNormalOfEllipse(Vec2D eO, Vec2D eR) { Vec2D p = this.sub(eO); float xr2 = eR.x * eR.x; float yr2 = eR.y * eR.y; return new Vec2D(p.x / xr2, p.y / yr2).normalize(); } @Override public final Vec3D to3DXY() { return new Vec3D(x, y, 0); } @Override public final Vec3D to3DXZ() { return new Vec3D(x, 0, y); } @Override public final Vec3D to3DYZ() { return new Vec3D(0, x, y); } /** * * @return */ @Override public float[] toArray() { return new float[]{ x, y }; } /** * * @return */ public final Vec2D toCartesian() { float xx = (float) (x * Math.cos(y)); y = (float) (x * Math.sin(y)); x = xx; return this; } /** * * @return */ public final Vec2D toPolar() { float r = (float) Math.sqrt(x * x + y * y); y = (float) Math.atan2(y, x); x = r; return this; } /** * * @return */ @Override public String toString() { StringBuilder sb = new StringBuilder(32); sb.append("{x:").append(x).append(", y:").append(y).append("}"); return sb.toString(); } /** * * @return */ @Override public final float x() { return x; } /** * * @return */ @Override public final float y() { return y; } }