@joint/core
Version:
JavaScript diagramming library
376 lines (285 loc) • 10.2 kB
JavaScript
/*
Point is the most basic object consisting of x/y coordinate.
Possible instantiations are:
* `Point(10, 20)`
* `new Point(10, 20)`
* `Point('10 20')`
* `Point(Point(10, 20))`
*/
import { normalizeAngle, random, snapToGrid, toDeg, toRad } from './geometry.helpers.mjs';
import { bearing } from './line.bearing.mjs';
import { squaredLength } from './line.squaredLength.mjs';
import { length } from './line.length.mjs';
import { types } from './types.mjs';
const {
abs,
cos,
sin,
sqrt,
min,
max,
atan2,
round,
pow,
PI
} = Math;
export const Point = function(x, y) {
if (!(this instanceof Point)) {
return new Point(x, y);
}
if (typeof x === 'string') {
var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@');
x = parseFloat(xy[0]);
y = parseFloat(xy[1]);
} else if (Object(x) === x) {
y = x.y;
x = x.x;
}
this.x = x === undefined ? 0 : x;
this.y = y === undefined ? 0 : y;
};
// Alternative constructor, from polar coordinates.
// @param {number} Distance.
// @param {number} Angle in radians.
// @param {point} [optional] Origin.
Point.fromPolar = function(distance, angle, origin) {
origin = new Point(origin);
var x = abs(distance * cos(angle));
var y = abs(distance * sin(angle));
var deg = normalizeAngle(toDeg(angle));
if (deg < 90) {
y = -y;
} else if (deg < 180) {
x = -x;
y = -y;
} else if (deg < 270) {
x = -x;
}
return new Point(origin.x + x, origin.y + y);
};
// Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`.
Point.random = function(x1, x2, y1, y2) {
return new Point(random(x1, x2), random(y1, y2));
};
Point.prototype = {
type: types.Point,
chooseClosest: function(points) {
var n = points.length;
if (n === 1) return new Point(points[0]);
var closest = null;
var minSqrDistance = Infinity;
for (var i = 0; i < n; i++) {
var p = new Point(points[i]);
var sqrDistance = this.squaredDistance(p);
if (sqrDistance < minSqrDistance) {
closest = p;
minSqrDistance = sqrDistance;
}
}
return closest;
},
// If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`,
// otherwise return point itself.
// (see Squeak Smalltalk, Point>>adhereTo:)
adhereToRect: function(r) {
if (r.containsPoint(this)) {
return this;
}
this.x = min(max(this.x, r.x), r.x + r.width);
this.y = min(max(this.y, r.y), r.y + r.height);
return this;
},
// Compute the angle between vector from me to p1 and the vector from me to p2.
// ordering of points p1 and p2 is important!
// theta function's angle convention:
// returns angles between 0 and 180 when the angle is counterclockwise
// returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones
// returns NaN if any of the points p1, p2 is coincident with this point
angleBetween: function(p1, p2) {
var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1));
if (angleBetween < 0) {
angleBetween += 360; // correction to keep angleBetween between 0 and 360
}
return angleBetween;
},
// Return the bearing between me and the given point.
bearing: function(point) {
return bearing(this, point);
},
// Returns change in angle from my previous position (-dx, -dy) to my new position
// relative to ref point.
changeInAngle: function(dx, dy, ref) {
// Revert the translation and measure the change in angle around x-axis.
return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref);
},
clone: function() {
return new Point(this);
},
// Returns the cross product of this point relative to two other points
// this point is the common point
// point p1 lies on the first vector, point p2 lies on the second vector
// watch out for the ordering of points p1 and p2!
// positive result indicates a clockwise ("right") turn from first to second vector
// negative result indicates a counterclockwise ("left") turn from first to second vector
// zero indicates that the first and second vector are collinear
// note that the above directions are reversed from the usual answer on the Internet
// that is because we are in a left-handed coord system (because the y-axis points downward)
cross: function(p1, p2) {
return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN;
},
difference: function(dx, dy) {
if ((Object(dx) === dx)) {
dy = dx.y;
dx = dx.x;
}
return new Point(this.x - (dx || 0), this.y - (dy || 0));
},
// Returns distance between me and point `p`.
distance: function(p) {
return length(this, p);
},
// Returns the dot product of this point with given other point
dot: function(p) {
return p ? (this.x * p.x + this.y * p.y) : NaN;
},
equals: function(p) {
return !!p &&
this.x === p.x &&
this.y === p.y;
},
// Linear interpolation
lerp: function(p, t) {
var x = this.x;
var y = this.y;
return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y);
},
magnitude: function() {
return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01;
},
// Returns a manhattan (taxi-cab) distance between me and point `p`.
manhattanDistance: function(p) {
return abs(p.x - this.x) + abs(p.y - this.y);
},
// Move point on line starting from ref ending at me by
// distance distance.
move: function(ref, distance) {
var theta = toRad((new Point(ref)).theta(this));
var offset = this.offset(cos(theta) * distance, -sin(theta) * distance);
return offset;
},
// Scales x and y such that the distance between the point and the origin (0,0) is equal to the given length.
normalize: function(length) {
var scale = (length || 1) / this.magnitude();
return this.scale(scale, scale);
},
// Offset me by the specified amount.
offset: function(dx, dy) {
if ((Object(dx) === dx)) {
dy = dx.y;
dx = dx.x;
}
this.x += dx || 0;
this.y += dy || 0;
return this;
},
// Returns a point that is the reflection of me with
// the center of inversion in ref point.
reflection: function(ref) {
return (new Point(ref)).move(this, this.distance(ref));
},
// Rotate point by angle around origin.
// Angle is flipped because this is a left-handed coord system (y-axis points downward).
rotate: function(origin, angle) {
if (angle === 0) return this;
origin = origin || new Point(0, 0);
angle = toRad(normalizeAngle(-angle));
var cosAngle = cos(angle);
var sinAngle = sin(angle);
var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x;
var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y;
this.x = x;
this.y = y;
return this;
},
round: function(precision) {
let f = 1; // case 0
if (precision) {
switch (precision) {
case 1: f = 10; break;
case 2: f = 100; break;
case 3: f = 1000; break;
default: f = pow(10, precision); break;
}
}
this.x = round(this.x * f) / f;
this.y = round(this.y * f) / f;
return this;
},
// Scale point with origin.
scale: function(sx, sy, origin) {
origin = (origin && new Point(origin)) || new Point(0, 0);
this.x = origin.x + sx * (this.x - origin.x);
this.y = origin.y + sy * (this.y - origin.y);
return this;
},
snapToGrid: function(gx, gy) {
this.x = snapToGrid(this.x, gx);
this.y = snapToGrid(this.y, gy || gx);
return this;
},
squaredDistance: function(p) {
return squaredLength(this, p);
},
// Compute the angle between me and `p` and the x axis.
// (cartesian-to-polar coordinates conversion)
// Return theta angle in degrees.
theta: function(p) {
p = new Point(p);
// Invert the y-axis.
var y = -(p.y - this.y);
var x = p.x - this.x;
var rad = atan2(y, x); // defined for all 0 corner cases
// Correction for III. and IV. quadrant.
if (rad < 0) {
rad = 2 * PI + rad;
}
return 180 * rad / PI;
},
toJSON: function() {
return { x: this.x, y: this.y };
},
// Converts rectangular to polar coordinates.
// An origin can be specified, otherwise it's 0@0.
toPolar: function(o) {
o = (o && new Point(o)) || new Point(0, 0);
var x = this.x;
var y = this.y;
this.x = sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y)); // r
this.y = toRad(o.theta(new Point(x, y)));
return this;
},
toString: function() {
return this.x + '@' + this.y;
},
serialize: function() {
return this.x + ',' + this.y;
},
update: function(x, y) {
if ((Object(x) === x)) {
y = x.y;
x = x.x;
}
this.x = x || 0;
this.y = y || 0;
return this;
},
// Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p.
// Returns NaN if p is at 0,0.
vectorAngle: function(p) {
var zero = new Point(0, 0);
return zero.angleBetween(this, p);
}
};
Point.prototype.translate = Point.prototype.offset;
// For backwards compatibility:
export const point = Point;