413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
// A Javascript 2D vector library
|
|
// conventions :
|
|
// method that returns a float value do not modify the vector
|
|
// method that implement operators return a new vector with the
|
|
// modifications without modifying the calling vector or the parameters.
|
|
//
|
|
// v3 = v1.add(v2); // v3 is set to v1 + v2, v1, v2 are not modified
|
|
//
|
|
// methods that take a single vector as a parameter are usually also
|
|
// available with q '_xy' suffix.
|
|
// Those method takes two floats representing the x,y coordinates of
|
|
// the vector parameter and allow you to avoid to needlessly create a
|
|
// vector object :
|
|
//
|
|
// v2 = v1.add(new Vec2(3,4));
|
|
// v2 = v1.add_xy(3,4); //equivalent to previous line
|
|
//
|
|
// angles are in radians by default but method that takes angle as
|
|
// parameters or return angle values usually have a
|
|
// variant with a '_deg' suffix that works in degrees
|
|
//
|
|
|
|
// The 2D vector object
|
|
function Vec2 (x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
|
|
// Multiply a number expressed in radiant by rad2deg to convert it in
|
|
// degrees
|
|
var rad2deg = 57.29577951308232;
|
|
// Multiply a number expressed in degrees by deg2rad to convert it to
|
|
// radiant
|
|
var deg2rad = 0.017453292519943295;
|
|
// The numerical precision used to compare vector equality
|
|
var epsilon = 0.0000001;
|
|
|
|
// This static method creates a new vector from polar coordinates with the
|
|
// angle expressed in degrees
|
|
Vec2.new_polar_deg = function (len, angle) {
|
|
var v = new Vec2(len, 0);
|
|
return v.rotate_deg(angle);
|
|
};
|
|
// This static method creates a new vector from polar coordinates with the
|
|
// angle expressed in radians
|
|
Vec2.new_polar = function (len, angle) {
|
|
var v = new Vec2(len, 0);
|
|
v.rotate(angle);
|
|
return v;
|
|
};
|
|
// Returns the length or modulus or magnitude of the vector
|
|
Vec2.prototype.len = function () {
|
|
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
};
|
|
// Returns the squared length of the vector,
|
|
// this method is much faster than len()
|
|
Vec2.prototype.len_sq = function () {
|
|
return this.x * this.x + this.y * this.y;
|
|
};
|
|
// Return the distance between this vector and the vector v
|
|
Vec2.prototype.dist = function (v) {
|
|
var dx = this.x - v.x;
|
|
var dy = this.y - v.y;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
};
|
|
// Return the distance between this vector and
|
|
// the vector of coordinates (x,y)
|
|
Vec2.prototype.dist_xy = function (x, y) {
|
|
var dx = this.x - x;
|
|
var dy = this.y - y;
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
};
|
|
// Return the squared distance between this vector and
|
|
// the vector and the vector v
|
|
Vec2.prototype.dist_sq = function (v) {
|
|
var dx = this.x - v.x;
|
|
var dy = this.y - v.y;
|
|
return dx * dx + dy * dy;
|
|
};
|
|
// Return the squared distance between this vector and
|
|
// the vector of coordinates (x,y)
|
|
Vec2.prototype.dist_sq_xy = function (x, y) {
|
|
var dx = this.x - x;
|
|
var dy = this.y - y;
|
|
return dx * dx + dy * dy;
|
|
};
|
|
// Return the dot product between this vector and the vector v
|
|
Vec2.prototype.dot = function (v) {
|
|
return this.x * v.x + this.y * v.y;
|
|
};
|
|
// Return the dot product between this vector and
|
|
// the vector of coordinate (x,y)
|
|
Vec2.prototype.dot_xy = function (x, y) {
|
|
return this.x * x + this.y * y;
|
|
};
|
|
// Return a new vector with the same coordinates as this
|
|
Vec2.prototype.clone = function () {
|
|
return new Vec2(this.x, this.y);
|
|
};
|
|
// Return the sum of this and vector v as a new vector
|
|
Vec2.prototype.add = function (v) {
|
|
return new Vec2(this.x + v.x, this.y + v.y);
|
|
};
|
|
// Return the sum of this and vector (x, y) as a new vector
|
|
Vec2.prototype.add_xy = function (x, y) {
|
|
return new Vec2(this.x + x, this.y + y);
|
|
};
|
|
// Returns (this - v) as a new vector where v is a
|
|
// vector and - is the vector subtraction
|
|
Vec2.prototype.sub = function (v) {
|
|
return new Vec2(this.x - v.x, this.y - v.y);
|
|
};
|
|
// Returns (this - (x, y)) as a new vector where - is vector subtraction
|
|
Vec2.prototype.sub_xy = function (x, y) {
|
|
return new Vec2(this.x - x, this.y - y);
|
|
};
|
|
// Return (this * v) as a new vector where v is a
|
|
// vector and * is the by component product
|
|
Vec2.prototype.mult = function (v) {
|
|
return new Vec2(this.x * v.x, this.y * v.y);
|
|
};
|
|
// Return (this * (x,y)) as a new vector where * is the
|
|
// by component product
|
|
Vec2.prototype.mult_xy = function (x, y) {
|
|
return new Vec2(this.x * x, this.y * y);
|
|
};
|
|
// Return this scaled by float f as a new fector
|
|
Vec2.prototype.scale = function (f) {
|
|
return new Vec2(this.x * f, this.y * f);
|
|
};
|
|
// Return the negation of this vector
|
|
// eslint-disable-next-line no-unused-vars
|
|
Vec2.prototype.neg = function (f) {
|
|
return new Vec2( -this.x, -this.y);
|
|
};
|
|
// Return this vector normalized as a new vector
|
|
Vec2.prototype.normalize = function () {
|
|
var len = this.len();
|
|
if (len === 0) {
|
|
return new Vec2(0, 1);
|
|
} else if (len !== 1) {
|
|
return this.scale(1.0 / len);
|
|
}
|
|
return new Vec2(this.x, this.y);
|
|
};
|
|
// Return a new vector with the same direction as this
|
|
// vector of length float l. (negative values of l will invert direction)
|
|
Vec2.prototype.set_len = function (l) {
|
|
return this.normalize().scale(l);
|
|
};
|
|
// Return the projection of this onto the vector v as a new vector
|
|
Vec2.prototype.project = function (v) {
|
|
return v.set_len(this.dot(v));
|
|
};
|
|
// Return a string representation of this vector
|
|
Vec2.prototype.toString = function () {
|
|
var str = "";
|
|
str += "[";
|
|
str += this.x;
|
|
str += ",";
|
|
str += this.y;
|
|
str += "]";
|
|
return str;
|
|
};
|
|
// Return this vector counterclockwise rotated by rad radians as a
|
|
// new vector
|
|
Vec2.prototype.rotate = function (rad) {
|
|
var c = Math.cos(rad);
|
|
var s = Math.sin(rad);
|
|
var px = this.x * c - this.y * s;
|
|
var py = this.x * s + this.y * c;
|
|
return new Vec2(px, py);
|
|
};
|
|
// Return this vector counterclockwise rotated by deg degrees as a
|
|
// new vector
|
|
Vec2.prototype.rotate_deg = function (deg) {
|
|
return this.rotate(deg * deg2rad);
|
|
};
|
|
// Linearly interpolate this vector towards the vector v by
|
|
// float factor alpha.
|
|
// alpha == 0 : does nothing
|
|
// alpha == 1 : sets this to v
|
|
Vec2.prototype.lerp = function (v, alpha) {
|
|
var inv_alpha = 1 - alpha;
|
|
return new Vec2(
|
|
this.x * inv_alpha + v.x * alpha,
|
|
this.y * inv_alpha + v.y * alpha);
|
|
};
|
|
// Returns the angle between this vector and the vector (1,0) in radians
|
|
Vec2.prototype.angle = function () {
|
|
return Math.atan2(this.y, this.x);
|
|
};
|
|
// Returns the angle between this vector and the vector (1,0) in degrees
|
|
Vec2.prototype.angle_deg = function () {
|
|
return Math.atan2(this.y, this.x) * rad2deg;
|
|
};
|
|
// Returns true if this vector is equal to the vector v, with a
|
|
// tolerance defined by the epsilon module constant
|
|
Vec2.prototype.equals = function (v) {
|
|
if (Math.abs(this.x-v.x) > epsilon) {
|
|
return false;
|
|
} else if (Math.abs(this.y-v.y) > epsilon) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
// Returns true if this vector is equal to the vector (x,y) with a
|
|
// tolerance defined by the epsilon module constant
|
|
Vec2.prototype.equals_xy = function (x, y) {
|
|
if (Math.abs(this.x-x) > epsilon) {
|
|
return false;
|
|
} else if (Math.abs(this.y-y) > epsilon) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// A Bounding Shapes Library
|
|
|
|
// A Bounding Ellipse
|
|
// cx,cy : center of the ellipse
|
|
// rx,ry : radius of the ellipse
|
|
function BEllipse (cx, cy, rx, ry) {
|
|
this.type = 'ellipse';
|
|
// Minimum x coordinate contained in the ellipse
|
|
this.x = cx-rx;
|
|
// Minimum y coordinate contained in the ellipse
|
|
this.y = cy-ry;
|
|
// Width of the ellipse on the x axis
|
|
this.sx = 2*rx;
|
|
// Width of the ellipse on the y axis
|
|
this.sy = 2*ry;
|
|
// Half of the ellipse width on the x axis
|
|
this.hx = rx;
|
|
// Half of the ellipse width on the y axis
|
|
this.hy = ry;
|
|
// The x coordinate of the ellipse center
|
|
this.cx = cx;
|
|
// The y coordinate of the ellipse center
|
|
this.cy = cy;
|
|
// Maximum x coordinate contained in the ellipse
|
|
this.mx = cx + rx;
|
|
// Maximum x coordinate contained in the ellipse
|
|
this.my = cy + ry;
|
|
}
|
|
|
|
// Returns an unordered list of vector defining the positions of the
|
|
// intersections between the ellipse's
|
|
// boundary and a line segment defined by the start and end vectors a,b
|
|
BEllipse.prototype.collide_segment = function (a, b) {
|
|
// http://paulbourke.net/geometry/sphereline/
|
|
var collisions = [];
|
|
|
|
// We do not compute the intersection in this case. TODO ?
|
|
if (a.equals(b)) {
|
|
return collisions;
|
|
}
|
|
|
|
// Make all computations in a space where the ellipse is a circle
|
|
// centered on zero
|
|
var c = new Vec2(this.cx, this.cy);
|
|
var _a = a.sub(c).mult_xy(1 / this.hx, 1 / this.hy);
|
|
var _b = b.sub(c).mult_xy(1 / this.hx, 1 / this.hy);
|
|
|
|
// Both points inside the ellipse
|
|
if (_a.len_sq() < 1 && _b.len_sq() < 1) {
|
|
return collisions;
|
|
}
|
|
|
|
// Compute the roots of the intersection
|
|
var ab = _b.sub(_a);
|
|
var A = ab.x * ab.x + ab.y * ab.y;
|
|
var B = 2 * ( ab.x * _a.x + ab.y * _a.y);
|
|
var C = _a.x * _a.x + _a.y * _a.y - 1;
|
|
var u = B * B - 4 * A * C;
|
|
|
|
if (u < 0) {
|
|
return collisions;
|
|
}
|
|
|
|
u = Math.sqrt(u);
|
|
var u1 = (-B + u) / (2 * A);
|
|
var u2 = (-B - u) / (2 * A);
|
|
|
|
// eslint-disable-next-line init-declarations
|
|
var pos;
|
|
if (u1 >= 0 && u1 <= 1) {
|
|
pos = _a.add(ab.scale(u1));
|
|
collisions.push(pos);
|
|
}
|
|
if (u1 !== u2 && u2 >= 0 && u2 <= 1) {
|
|
pos = _a.add(ab.scale(u2));
|
|
collisions.push(pos);
|
|
}
|
|
for (var i = 0; i < collisions.length; i++) {
|
|
collisions[i] = collisions[i].mult_xy(this.hx, this.hy);
|
|
collisions[i] = collisions[i].add_xy(this.cx, this.cy);
|
|
}
|
|
return collisions;
|
|
};
|
|
|
|
// A bounding rectangle
|
|
// x,y the minimum coordinate contained in the rectangle
|
|
// sx,sy the size of the rectangle along the x,y axis
|
|
function BRect (x, y, sx, sy) {
|
|
this.type = 'rect';
|
|
// Minimum x coordinate contained in the rectangle
|
|
this.x = x;
|
|
// Minimum y coordinate contained in the rectangle
|
|
this.y = y;
|
|
// Width of the rectangle on the x axis
|
|
this.sx = sx;
|
|
// Width of the rectangle on the y axis
|
|
this.sy = sy;
|
|
// Half of the rectangle width on the x axis
|
|
this.hx = sx / 2;
|
|
// Half of the rectangle width on the y axis
|
|
this.hy = sy / 2;
|
|
// The x coordinate of the rectangle center
|
|
this.cx = x + this.hx;
|
|
// The y coordinate of the rectangle center
|
|
this.cy = y + this.hy;
|
|
// Maximum x coordinate contained in the rectangle
|
|
this.mx = x + sx;
|
|
// Maximum x coordinate contained in the rectangle
|
|
this.my = y + sy;
|
|
}
|
|
|
|
// Static method creating a new bounding rectangle of
|
|
// size (sx,sy) centered on (cx,cy)
|
|
BRect.new_centered = function (cx, cy, sx, sy) {
|
|
return new BRect(cx - sx / 2, cy - sy / 2, sx, sy);
|
|
};
|
|
// Intersect line a,b with line c,d, returns null if no intersection
|
|
function line_intersect (a, b, c, d) {
|
|
// http://paulbourke.net/geometry/lineline2d/
|
|
var f = (d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y);
|
|
if (f === 0) {
|
|
return null;
|
|
}
|
|
f = 1 / f;
|
|
var fab = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) * f;
|
|
if (fab < 0 || fab > 1) {
|
|
return null;
|
|
}
|
|
var fcd = ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) * f;
|
|
if (fcd < 0 || fcd > 1) {
|
|
return null;
|
|
}
|
|
return new Vec2(a.x + fab * (b.x - a.x), a.y + fab * (b.y - a.y));
|
|
}
|
|
|
|
// Returns an unordered list of vector defining the positions of the
|
|
// intersections between the ellipse's
|
|
// boundary and a line segment defined by the start and end vectors a,b
|
|
|
|
BRect.prototype.collide_segment = function (a, b) {
|
|
var collisions = [];
|
|
var corners = [
|
|
new Vec2(this.x, this.y), new Vec2(this.x, this.my),
|
|
new Vec2(this.mx, this.my), new Vec2(this.mx, this.y),
|
|
];
|
|
var pos = line_intersect(a, b, corners[0], corners[1]);
|
|
if (pos) {
|
|
collisions.push(pos);
|
|
}
|
|
pos = line_intersect(a, b, corners[1], corners[2]);
|
|
if (pos) {
|
|
collisions.push(pos);
|
|
}
|
|
pos = line_intersect(a, b, corners[2], corners[3]);
|
|
if (pos) {
|
|
collisions.push(pos);
|
|
}
|
|
pos = line_intersect(a, b, corners[3], corners[0]);
|
|
if (pos) {
|
|
collisions.push(pos);
|
|
}
|
|
return collisions;
|
|
};
|
|
|
|
// Returns true if the rectangle contains the position defined by the
|
|
// vector 'vec'
|
|
BRect.prototype.contains_vec = function (vec) {
|
|
return (
|
|
vec.x >= this.x && vec.x <= this.mx &&
|
|
vec.y >= this.y && vec.y <= this.my);
|
|
};
|
|
// Returns true if the rectangle contains the position (x,y)
|
|
BRect.prototype.contains_xy = function (x, y) {
|
|
return (
|
|
x >= this.x && x <= this.mx &&
|
|
y >= this.y && y <= this.my);
|
|
};
|
|
// Returns true if the ellipse contains the position defined by the
|
|
// vector 'vec'
|
|
BEllipse.prototype.contains_vec = function (v) {
|
|
var _v = v.mult_xy(this.hx, this.hy);
|
|
return _v.len_sq() <= 1;
|
|
};
|
|
// Returns true if the ellipse contains the position (x, y)
|
|
BEllipse.prototype.contains_xy = function (x, y) {
|
|
return this.contains(new Vec2(x, y));
|
|
};
|
|
|
|
export {
|
|
BEllipse,
|
|
Vec2,
|
|
BRect,
|
|
};
|