@joint/core
Version:
JavaScript diagramming library
1,416 lines (1,368 loc) • 297 kB
JavaScript
/*! JointJS v4.1.3 (2025-02-04) - JavaScript diagramming library
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.V = factory());
}(this, function () { 'use strict';
// Declare shorthands to the most used math functions.
const {
round,
floor,
PI
} = Math;
const scale = {
// Return the `value` from the `domain` interval scaled to the `range` interval.
linear: function (domain, range, value) {
var domainSpan = domain[1] - domain[0];
var rangeSpan = range[1] - range[0];
return (value - domain[0]) / domainSpan * rangeSpan + range[0] || 0;
}
};
const normalizeAngle = function (angle) {
return angle % 360 + (angle < 0 ? 360 : 0);
};
const snapToGrid = function (value, gridSize) {
return gridSize * round(value / gridSize);
};
const toDeg = function (rad) {
return 180 * rad / PI % 360;
};
const toRad = function (deg, over360) {
over360 = over360 || false;
deg = over360 ? deg : deg % 360;
return deg * PI / 180;
};
// Return a random integer from the interval [min,max], inclusive.
const random = function (min, max) {
if (max === undefined) {
// use first argument as max, min is 0
max = min === undefined ? 1 : min;
min = 0;
} else if (max < min) {
// switch max and min
const temp = min;
min = max;
max = temp;
}
return floor(Math.random() * (max - min + 1) + min);
};
// @return the bearing (cardinal direction) of the line. For example N, W, or SE.
const {
cos,
sin,
atan2
} = Math;
const bearing = function (p, q) {
var lat1 = toRad(p.y);
var lat2 = toRad(q.y);
var lon1 = p.x;
var lon2 = q.x;
var dLon = toRad(lon2 - lon1);
var y = sin(dLon) * cos(lat2);
var x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
var brng = toDeg(atan2(y, x));
var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N'];
var index = brng - 22.5;
if (index < 0) index += 360;
index = parseInt(index / 45);
return bearings[index];
};
// @return {integer} length without sqrt
// @note for applications where the exact length is not necessary (e.g. compare only)
const squaredLength = function (start, end) {
var x0 = start.x;
var y0 = start.y;
var x1 = end.x;
var y1 = end.y;
return (x0 -= x1) * x0 + (y0 -= y1) * y0;
};
const length = function (start, end) {
return Math.sqrt(squaredLength(start, end));
};
const types = {
Point: 1,
Line: 2,
Ellipse: 3,
Rect: 4,
Polyline: 5,
Polygon: 6,
Curve: 7,
Path: 8
};
/*
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))`
*/
const {
abs,
cos: cos$1,
sin: sin$1,
sqrt,
min,
max,
atan2: atan2$1,
round: round$1,
pow,
PI: PI$1
} = Math;
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$1(angle));
var y = abs(distance * sin$1(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$1(theta) * distance, -sin$1(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$1(angle);
var sinAngle = sin$1(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$1(this.x * f) / f;
this.y = round$1(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$1(y, x); // defined for all 0 corner cases
// Correction for III. and IV. quadrant.
if (rad < 0) {
rad = 2 * PI$1 + rad;
}
return 180 * rad / PI$1;
},
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:
const point = Point;
const {
max: max$1,
min: min$1
} = Math;
const Line = function (p1, p2) {
if (!(this instanceof Line)) {
return new Line(p1, p2);
}
if (p1 instanceof Line) {
return new Line(p1.start, p1.end);
}
this.start = new Point(p1);
this.end = new Point(p2);
};
Line.prototype = {
type: types.Line,
// @returns the angle of incline of the line.
angle: function () {
var horizontalPoint = new Point(this.start.x + 1, this.start.y);
return this.start.angleBetween(this.end, horizontalPoint);
},
bbox: function () {
var left = min$1(this.start.x, this.end.x);
var top = min$1(this.start.y, this.end.y);
var right = max$1(this.start.x, this.end.x);
var bottom = max$1(this.start.y, this.end.y);
return new Rect(left, top, right - left, bottom - top);
},
// @return the bearing (cardinal direction) of the line. For example N, W, or SE.
// @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N.
bearing: function () {
return bearing(this.start, this.end);
},
clone: function () {
return new Line(this.start, this.end);
},
// @return {point} the closest point on the line to point `p`
closestPoint: function (p) {
return this.pointAt(this.closestPointNormalizedLength(p));
},
closestPointLength: function (p) {
return this.closestPointNormalizedLength(p) * this.length();
},
// @return {number} the normalized length of the closest point on the line to point `p`
closestPointNormalizedLength: function (p) {
var product = this.vector().dot(new Line(this.start, p).vector());
var cpNormalizedLength = min$1(1, max$1(0, product / this.squaredLength()));
// cpNormalizedLength returns `NaN` if this line has zero length
// we can work with that - if `NaN`, return 0
if (cpNormalizedLength !== cpNormalizedLength) return 0; // condition evaluates to `true` if and only if cpNormalizedLength is `NaN`
// (`NaN` is the only value that is not equal to itself)
return cpNormalizedLength;
},
closestPointTangent: function (p) {
return this.tangentAt(this.closestPointNormalizedLength(p));
},
// Returns `true` if the point lies on the line.
containsPoint: function (p) {
var start = this.start;
var end = this.end;
if (start.cross(p, end) !== 0) return false;
// else: cross product of 0 indicates that this line and the vector to `p` are collinear
var length = this.length();
if (new Line(start, p).length() > length) return false;
if (new Line(p, end).length() > length) return false;
// else: `p` lies between start and end of the line
return true;
},
// Divides the line into two at requested `ratio` between 0 and 1.
divideAt: function (ratio) {
var dividerPoint = this.pointAt(ratio);
// return array with two lines
return [new Line(this.start, dividerPoint), new Line(dividerPoint, this.end)];
},
// Divides the line into two at requested `length`.
divideAtLength: function (length) {
var dividerPoint = this.pointAtLength(length);
// return array with two new lines
return [new Line(this.start, dividerPoint), new Line(dividerPoint, this.end)];
},
equals: function (l) {
return !!l && this.start.x === l.start.x && this.start.y === l.start.y && this.end.x === l.end.x && this.end.y === l.end.y;
},
// @return {point} Point where I'm intersecting a line.
// @return [point] Points where I'm intersecting a rectangle.
// @see Squeak Smalltalk, LineSegment>>intersectionWith:
intersect: function (shape, opt) {
if (shape && shape.intersectionWithLine) {
var intersection = shape.intersectionWithLine(this, opt);
// Backwards compatibility
if (intersection && shape instanceof Line) {
intersection = intersection[0];
}
return intersection;
}
return null;
},
intersectionWithLine: function (line) {
var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y);
var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y);
var det = pt1Dir.x * pt2Dir.y - pt1Dir.y * pt2Dir.x;
var deltaPt = new Point(line.start.x - this.start.x, line.start.y - this.start.y);
var alpha = deltaPt.x * pt2Dir.y - deltaPt.y * pt2Dir.x;
var beta = deltaPt.x * pt1Dir.y - deltaPt.y * pt1Dir.x;
if (det === 0 || alpha * det < 0 || beta * det < 0) {
// No intersection found.
return null;
}
if (det > 0) {
if (alpha > det || beta > det) {
return null;
}
} else {
if (alpha < det || beta < det) {
return null;
}
}
return [new Point(this.start.x + alpha * pt1Dir.x / det, this.start.y + alpha * pt1Dir.y / det)];
},
isDifferentiable: function () {
return !this.start.equals(this.end);
},
// @return {double} length of the line
length: function () {
return length(this.start, this.end);
},
// @return {point} my midpoint
midpoint: function () {
return new Point((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2);
},
parallel: function (distance) {
const l = this.clone();
if (!this.isDifferentiable()) return l;
const {
start,
end
} = l;
const eRef = start.clone().rotate(end, 270);
const sRef = end.clone().rotate(start, 90);
start.move(sRef, distance);
end.move(eRef, distance);
return l;
},
// @return {point} my point at 't' <0,1>
pointAt: function (t) {
var start = this.start;
var end = this.end;
if (t <= 0) return start.clone();
if (t >= 1) return end.clone();
return start.lerp(end, t);
},
pointAtLength: function (length) {
var start = this.start;
var end = this.end;
var fromStart = true;
if (length < 0) {
fromStart = false; // negative lengths mean start calculation from end point
length = -length; // absolute value
}
var lineLength = this.length();
if (length >= lineLength) return fromStart ? end.clone() : start.clone();
return this.pointAt((fromStart ? length : lineLength - length) / lineLength);
},
// @return {number} the offset of the point `p` from the line. + if the point `p` is on the right side of the line, - if on the left and 0 if on the line.
pointOffset: function (p) {
// Find the sign of the determinant of vectors (start,end), where p is the query point.
p = new Point(p);
var start = this.start;
var end = this.end;
var determinant = (end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x);
return determinant / this.length();
},
rotate: function (origin, angle) {
this.start.rotate(origin, angle);
this.end.rotate(origin, angle);
return this;
},
round: function (precision) {
this.start.round(precision);
this.end.round(precision);
return this;
},
scale: function (sx, sy, origin) {
this.start.scale(sx, sy, origin);
this.end.scale(sx, sy, origin);
return this;
},
// @return {number} scale the line so that it has the requested length
setLength: function (length) {
var currentLength = this.length();
if (!currentLength) return this;
var scaleFactor = length / currentLength;
return this.scale(scaleFactor, scaleFactor, this.start);
},
// @return {integer} length without sqrt
// @note for applications where the exact length is not necessary (e.g. compare only)
squaredLength: function () {
return squaredLength(this.start, this.end);
},
tangentAt: function (t) {
if (!this.isDifferentiable()) return null;
var start = this.start;
var end = this.end;
var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1
var tangentLine = new Line(start, end);
tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested
return tangentLine;
},
tangentAtLength: function (length) {
if (!this.isDifferentiable()) return null;
var start = this.start;
var end = this.end;
var tangentStart = this.pointAtLength(length);
var tangentLine = new Line(start, end);
tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested
return tangentLine;
},
toString: function () {
return this.start.toString() + ' ' + this.end.toString();
},
serialize: function () {
return this.start.serialize() + ' ' + this.end.serialize();
},
translate: function (tx, ty) {
this.start.translate(tx, ty);
this.end.translate(tx, ty);
return this;
},
// @return vector {point} of the line
vector: function () {
return new Point(this.end.x - this.start.x, this.end.y - this.start.y);
}
};
// For backwards compatibility:
Line.prototype.intersection = Line.prototype.intersect;
// For backwards compatibility:
const line = Line;
const {
sqrt: sqrt$1,
round: round$2,
pow: pow$1
} = Math;
const Ellipse = function (c, a, b) {
if (!(this instanceof Ellipse)) {
return new Ellipse(c, a, b);
}
if (c instanceof Ellipse) {
return new Ellipse(new Point(c.x, c.y), c.a, c.b);
}
c = new Point(c);
this.x = c.x;
this.y = c.y;
this.a = a;
this.b = b;
};
Ellipse.fromRect = function (rect) {
rect = new Rect(rect);
return new Ellipse(rect.center(), rect.width / 2, rect.height / 2);
};
Ellipse.prototype = {
type: types.Ellipse,
bbox: function () {
return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b);
},
/**
* @returns {g.Point}
*/
center: function () {
return new Point(this.x, this.y);
},
clone: function () {
return new Ellipse(this);
},
/**
* @param {g.Point} p
* @returns {boolean}
*/
containsPoint: function (p) {
return this.normalizedDistance(p) <= 1;
},
equals: function (ellipse) {
return !!ellipse && ellipse.x === this.x && ellipse.y === this.y && ellipse.a === this.a && ellipse.b === this.b;
},
// inflate by dx and dy
// @param dx {delta_x} representing additional size to x
// @param dy {delta_y} representing additional size to y -
// dy param is not required -> in that case y is sized by dx
inflate: function (dx, dy) {
if (dx === undefined) {
dx = 0;
}
if (dy === undefined) {
dy = dx;
}
this.a += 2 * dx;
this.b += 2 * dy;
return this;
},
intersectionWithLine: function (line) {
var intersections = [];
var a1 = line.start;
var a2 = line.end;
var rx = this.a;
var ry = this.b;
var dir = line.vector();
var diff = a1.difference(new Point(this));
var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry));
var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry));
var a = dir.dot(mDir);
var b = dir.dot(mDiff);
var c = diff.dot(mDiff) - 1.0;
var d = b * b - a * c;
if (d < 0) {
return null;
} else if (d > 0) {
var root = sqrt$1(d);
var ta = (-b - root) / a;
var tb = (-b + root) / a;
if ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) {
// if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside
return null;
} else {
if (0 <= ta && ta <= 1) intersections.push(a1.lerp(a2, ta));
if (0 <= tb && tb <= 1) intersections.push(a1.lerp(a2, tb));
}
} else {
var t = -b / a;
if (0 <= t && t <= 1) {
intersections.push(a1.lerp(a2, t));
} else {
// outside
return null;
}
}
return intersections;
},
// Find point on me where line from my center to
// point p intersects my boundary.
// @param {number} angle If angle is specified, intersection with rotated ellipse is computed.
intersectionWithLineFromCenterToPoint: function (p, angle) {
p = new Point(p);
if (angle) p.rotate(new Point(this.x, this.y), angle);
var dx = p.x - this.x;
var dy = p.y - this.y;
var result;
if (dx === 0) {
result = this.bbox().pointNearestToPoint(p);
if (angle) return result.rotate(new Point(this.x, this.y), -angle);
return result;
}
var m = dy / dx;
var mSquared = m * m;
var aSquared = this.a * this.a;
var bSquared = this.b * this.b;
var x = sqrt$1(1 / (1 / aSquared + mSquared / bSquared));
x = dx < 0 ? -x : x;
var y = m * x;
result = new Point(this.x + x, this.y + y);
if (angle) return result.rotate(new Point(this.x, this.y), -angle);
return result;
},
/**
* @param {g.Point} point
* @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside
*/
normalizedDistance: function (point) {
var x0 = point.x;
var y0 = point.y;
var a = this.a;
var b = this.b;
var x = this.x;
var y = this.y;
return (x0 - x) * (x0 - x) / (a * a) + (y0 - y) * (y0 - y) / (b * b);
},
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$1(10, precision);
break;
}
}
this.x = round$2(this.x * f) / f;
this.y = round$2(this.y * f) / f;
this.a = round$2(this.a * f) / f;
this.b = round$2(this.b * f) / f;
return this;
},
/** Compute angle between tangent and x axis
* @param {g.Point} p Point of tangency, it has to be on ellipse boundaries.
* @returns {number} angle between tangent and x axis
*/
tangentTheta: function (p) {
var refPointDelta = 30;
var x0 = p.x;
var y0 = p.y;
var a = this.a;
var b = this.b;
var center = this.bbox().center();
var m = center.x;
var n = center.y;
var q1 = x0 > center.x + a / 2;
var q3 = x0 < center.x - a / 2;
var y, x;
if (q1 || q3) {
y = x0 > center.x ? y0 - refPointDelta : y0 + refPointDelta;
x = a * a / (x0 - m) - a * a * (y0 - n) * (y - n) / (b * b * (x0 - m)) + m;
} else {
x = y0 > center.y ? x0 + refPointDelta : x0 - refPointDelta;
y = b * b / (y0 - n) - b * b * (x0 - m) * (x - m) / (a * a * (y0 - n)) + n;
}
return new Point(x, y).theta(p);
},
toString: function () {
return new Point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b;
}
};
// For backwards compatibility:
const ellipse = Ellipse;
const {
abs: abs$1,
cos: cos$2,
sin: sin$2,
min: min$2,
max: max$2,
round: round$3,
pow: pow$2
} = Math;
const Rect = function (x, y, w, h) {
if (!(this instanceof Rect)) {
return new Rect(x, y, w, h);
}
if (Object(x) === x) {
y = x.y;
w = x.width;
h = x.height;
x = x.x;
}
this.x = x === undefined ? 0 : x;
this.y = y === undefined ? 0 : y;
this.width = w === undefined ? 0 : w;
this.height = h === undefined ? 0 : h;
};
Rect.fromEllipse = function (e) {
e = new Ellipse(e);
return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b);
};
Rect.fromPointUnion = function () {
if (arguments.length === 0) return null;
const p = new Point();
let minX, minY, maxX, maxY;
minX = minY = Infinity;
maxX = maxY = -Infinity;
for (let i = 0; i < arguments.length; i++) {
p.update(i < 0 || arguments.length <= i ? undefined : arguments[i]);
const x = p.x;
const y = p.y;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
};
Rect.fromRectUnion = function () {
if (arguments.length === 0) return null;
const r = new Rect();
let minX, minY, maxX, maxY;
minX = minY = Infinity;
maxX = maxY = -Infinity;
for (let i = 0; i < arguments.length; i++) {
r.update(i < 0 || arguments.length <= i ? undefined : arguments[i]);
const x = r.x;
const y = r.y;
const mX = x + r.width;
const mY = y + r.height;
if (x < minX) minX = x;
if (mX > maxX) maxX = mX;
if (y < minY) minY = y;
if (mY > maxY) maxY = mY;
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
};
Rect.prototype = {
type: types.Rect,
// Find my bounding box when I'm rotated with the center of rotation in the center of me.
// @return r {rectangle} representing a bounding box
bbox: function (angle) {
return this.clone().rotateAroundCenter(angle);
},
rotateAroundCenter: function (angle) {
if (!angle) return this;
const {
width,
height
} = this;
const theta = toRad(angle);
const st = abs$1(sin$2(theta));
const ct = abs$1(cos$2(theta));
const w = width * ct + height * st;
const h = width * st + height * ct;
this.x += (width - w) / 2;
this.y += (height - h) / 2;
this.width = w;
this.height = h;
return this;
},
bottomLeft: function () {
return new Point(this.x, this.y + this.height);
},
bottomLine: function () {
return new Line(this.bottomLeft(), this.bottomRight());
},
bottomMiddle: function () {
return new Point(this.x + this.width / 2, this.y + this.height);
},
center: function () {
return new Point(this.x + this.width / 2, this.y + this.height / 2);
},
clone: function () {
return new Rect(this);
},
// @return {bool} true if point p is inside me.
// @param {bool} strict If true, the point has to be strictly inside (not on the border).
containsPoint: function (p, opt) {
let x, y;
if (!p || typeof p === 'string') {
// Backwards compatibility: if the point is not provided,
// the point is considered to be the origin [0, 0].
({
x,
y
} = new Point(p));
} else {
// Do not create a new Point object if the point is already a Point-like object.
({
x = 0,
y = 0
} = p);
}
return opt && opt.strict ? x > this.x && x < this.x + this.width && y > this.y && y < this.y + this.height : x >= this.x && x <= this.x + this.width && y >= this.y && y <= this.y + this.height;
},
// @return {bool} true if rectangle `r` is inside me.
containsRect: function (r) {
var r0 = new Rect(this).normalize();
var r1 = new Rect(r).normalize();
var w0 = r0.width;
var h0 = r0.height;
var w1 = r1.width;
var h1 = r1.height;
if (!w0 || !h0 || !w1 || !h1) {
// At least one of the dimensions is 0
return false;
}
var x0 = r0.x;
var y0 = r0.y;
var x1 = r1.x;
var y1 = r1.y;
w1 += x1;
w0 += x0;
h1 += y1;
h0 += y0;
return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0;
},
corner: function () {
return new Point(this.x + this.width, this.y + this.height);
},
// @return {boolean} true if rectangles are equal.
equals: function (r) {
var mr = new Rect(this).normalize();
var nr = new Rect(r).normalize();
return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height;
},
// inflate by dx and dy, recompute origin [x, y]
// @param dx {delta_x} representing additional size to x
// @param dy {delta_y} representing additional size to y -
// dy param is not required -> in that case y is sized by dx
inflate: function (dx, dy) {
if (dx === undefined) {
dx = 0;
}
if (dy === undefined) {
dy = dx;
}
this.x -= dx;
this.y -= dy;
this.width += 2 * dx;
this.height += 2 * dy;
return this;
},
// @return {rect} if rectangles intersect, {null} if not.
intersect: function (r) {
var myOrigin = this.origin();
var myCorner = this.corner();
var rOrigin = r.origin();
var rCorner = r.corner();
// No intersection found
if (rCorner.x <= myOrigin.x || rCorner.y <= myOrigin.y || rOrigin.x >= myCorner.x || rOrigin.y >= myCorner.y) return null;
var x = max$2(myOrigin.x, rOrigin.x);
var y = max$2(myOrigin.y, rOrigin.y);
return new Rect(x, y, min$2(myCorner.x, rCorner.x) - x, min$2(myCorner.y, rCorner.y) - y);
},
intersectionWithLine: function (line) {
var r = this;
var rectLines = [r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine()];
var points = [];
var dedupeArr = [];
var pt, i;
var n = rectLines.length;
for (i = 0; i < n; i++) {
pt = line.intersect(rectLines[i]);
if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) {
points.push(pt);
dedupeArr.push(pt.toString());
}
}
return points.length > 0 ? points : null;
},
// Find point on my boundary where line starting
// from my center ending in point p intersects me.
// @param {number} angle If angle is specified, intersection with rotated rectangle is computed.
intersectionWithLineFromCenterToPoint: function (p, angle) {
p = new Point(p);
var center = new Point(this.x + this.width / 2, this.y + this.height / 2);
var result;
if (angle) p.rotate(center, angle);
// (clockwise, starting from the top side)
var sides = [this.topLine(), this.rightLine(), this.bottomLine(), this.leftLine()];
var connector = new Line(center, p);
for (var i = sides.length - 1; i >= 0; --i) {
var intersection = sides[i].intersection(connector);
if (intersection !== null) {
result = intersection;
break;
}
}
if (result && angle) result.rotate(center, -angle);
return result;
},
leftLine: function () {
return new Line(this.topLeft(), this.bottomLeft());
},
leftMiddle: function () {
return new Point(this.x, this.y + this.height / 2);
},
maxRectScaleToFit: function (rect, origin) {
rect = new Rect(rect);
origin || (origin = rect.center());
var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4;
var ox = origin.x;
var oy = origin.y;
// Here we find the maximal possible scale for all corner points (for x and y axis) of the rectangle,
// so when the scale is applied the point is still inside the rectangle.
sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity;
// Top Left
var p1 = rect.topLeft();
if (p1.x < ox) {
sx1 = (this.x - ox) / (p1.x - ox);
}
if (p1.y < oy) {
sy1 = (this.y - oy) / (p1.y - oy);
}
// Bottom Right
var p2 = rect.bottomRight();
if (p2.x > ox) {
sx2 = (this.x + this.width - ox) / (p2.x - ox);
}
if (p2.y > oy) {
sy2 = (this.y + this.height - oy) / (p2.y - oy);
}
// Top Right
var p3 = rect.topRight();
if (p3.x > ox) {
sx3 = (this.x + this.width - ox) / (p3.x - ox);
}
if (p3.y < oy) {
sy3 = (this.y - oy) / (p3.y - oy);
}
// Bottom Left
var p4 = rect.bottomLeft();
if (p4.x < ox) {
sx4 = (this.x - ox) / (p4.x - ox);
}
if (p4.y > oy) {
sy4 = (this.y + this.height - oy) / (p4.y - oy);
}
return {
sx: min$2(sx1, sx2, sx3, sx4),
sy: min$2(sy1, sy2, sy3, sy4)
};
},
maxRectUniformScaleToFit: function (rect, origin) {
var scale = this.maxRectScaleToFit(rect, origin);
return min$2(scale.sx, scale.sy);
},
// Move and expand me.
// @param r {rectangle} representing deltas
moveAndExpand: function (r) {
this.x += r.x || 0;
this.y += r.y || 0;
this.width += r.width || 0;
this.height += r.height || 0;
return this;
},
// Normalize the rectangle; i.e., make it so that it has a non-negative width and height.
// If width < 0 the function swaps the left and right corners,
// and it swaps the top and bottom corners if height < 0
// like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized
normalize: function () {
var newx = this.x;
var newy = this.y;
var newwidth = this.width;
var newheight = this.height;
if (this.width < 0) {
newx = this.x + this.width;
newwidth = -this.width;
}
if (this.height < 0) {
newy = this.y + this.height;
newheight = -this.height;
}
this.x = newx;
this.y = newy;
this.width = newwidth;
this.height = newheight;
return this;
},
// Offset me by the specified amount.
offset: function (dx, dy) {
// pretend that this is a point and call offset()
// rewrites x and y according to dx and dy
return Point.prototype.offset.call(this, dx, dy);
},
origin: function () {
return new Point(this.x, this.y);
},
// @return {point} a point on my boundary nearest to the given point.
// @see Squeak Smalltalk, Rectangle>>pointNearestTo:
pointNearestToPoint: function (point) {
point = new Point(point);
if (this.containsPoint(point)) {
var side = this.sideNearestToPoint(point);
switch (side) {
case 'right':
return new Point(this.x + this.width, point.y);
case 'left':
return new Point(this.x, point.y);
case 'bottom':
return new Point(point.x, this.y + this.height);
case 'top':
return new Point(point.x, this.y);
}
}
return point.adhereToRect(this);
},
rightLine: function () {
return new Line(this.topRight(), this.bottomRight());
},
rightMiddle: function () {
return new Point(this.x + this.width, this.y + this.height / 2);
},
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$2(10, precision);
break;
}
}
this.x = round$3(this.x * f) / f;
this.y = round$3(this.y * f) / f;
this.width = round$3(this.width * f) / f;
this.height = round$3(this.height * f) / f;
return this;
},
// Scale rectangle with origin.
scale: function (sx, sy, origin) {
origin = this.origin().scale(sx, sy, origin);
this.x = origin.x;
this.y = origin.y;
this.width *= sx;
this.height *= sy;
return this;
},
// @return {string} (left|right|top|bottom) side which is nearest to point
// @see Squeak Smalltalk, Rectangle>>sideNearestTo:
sideNearestToPoint: function (point) {
point = new Point(point);
var distToLeft = point.x - this.x;
var distToRight = this.x + this.width - point.x;
var distToTop = point.y - this.y;
var distToBottom = this.y + this.height - point.y;
var closest = distToLeft;
var side = 'left';
if (distToRight < closest) {
closest = distToRight;
side = 'right';
}
if (distToTop < closest) {
closest = distToTop;
side = 'top';
}
if (distToBottom < closest) {
// closest = distToBottom;
side = 'bottom';
}
return side;
},
snapToGrid: function (gx, gy) {
var origin = this.origin().snapToGrid(gx, gy);
var corner = this.corner().snapToGrid(gx, gy);
this.x = origin.x;
this.y = origin.y;
this.width = corner.x - origin.x;
this.height = corner.y - origin.y;
return this;
},
toJSON: function () {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
};
},
topLine: function () {
return new Line(this.topLeft(), this.topRight());
},
topMiddle: function () {
return new Point(this.x + this.width / 2, this.y);
},
topRight: function () {
return new Point(this.x + this.width, this.y);
},
toString: function () {
return this.origin().toString() + ' ' + this.corner().toString();
},
// @return {rect} representing the union of both rectangles.
union: function (rect) {
return Rect.fromRectUnion(this, rect);
},
update: function (x, y, w, h) {
if (Object(x) === x) {
y = x.y;
w = x.width;
h = x.height;
x = x.x;
}
this.x = x || 0;
this.y = y || 0;
this.width = w || 0;
this.height = h || 0;
return this;
}
};
Rect.prototype.bottomRight = Rect.prototype.corner;
Rect.prototype.topLeft = Rect.prototype.origin;
Rect.prototype.translate = Rect.prototype.offset;
// For backwards compatibility:
const rect = Rect;
function parsePoints(svgString) {
// Step 1: Discard surrounding spaces
const trimmedString = svgString.trim();
if (trimmedString === '') return [];
const points = [];
// Step 2: Split at commas (+ their surrounding spaces) or at multiple spaces
// ReDoS mitigation: Have an anchor at the beginning of each alternation
// Note: This doesn't simplify double (or more) commas - causes empty coords
// This regex is used by `split()`, so it doesn't need to use /g
const coords = trimmedString.split(/\b\s*,\s*|,\s*|\s+/);
const numCoords = coords.length;
for (let i = 0; i < numCoords; i += 2) {
// Step 3: Convert each coord to number
// Note: If the coord cannot be converted to a number, it will be `NaN`
// Note: If the coord is empty ("", e.g. from ",," input), it will be `0`
// Note: If we end up with an odd number of coords, the last point's second coord will be `NaN`
points.push({
x: +coords[i],
y: +coords[i + 1]
});
}
return points;
}
function clonePoints(points) {
const numPoints = points.length;
if (numPoints === 0) return [];
const newPoints = [];
for (let i = 0; i < numPoints; i++) {
const point = points[i].clone();
newPoints.push(point);
}
return newPoints;
}
// Returns a convex-hull polyline from this polyline.
// Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan).
// Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise.
// Minimal polyline is found (only vertices of the hull are reported, no collinear points).
function convexHull(points) {
const {
abs
} = Math;
var i;
var n;
var numPoints = points.length;
if (numPoints === 0) return []; // if points array is empty
// step 1: find the starting point - point with the lowest y (if equality, highest x)
var startPoint;
for (i = 0; i < numPoints; i++) {
if (startPoint === undefined) {
// if this is the first point we see, set it as start point
startPoint = points[i];
} else if (points[i].y < startPoint.y) {
// start point should have lowest y from all points
startPoint = points[i];
} else if (points[i].y === startPoint.y && points[i].x > startPoint.x) {
// if two points have the lowest y, choose the one that has highest x
// there are no points to the right of startPoint - no ambiguity about theta 0
// if there are several coincident start point candidates, first one is reported
startPoint = points[i];
}
}
// step 2: sort the list of points
// sorting by angle between line from startPoint to point and the x-axis (theta)
// step 2a: create the point records = [point, originalIndex, angle]
var sortedPointRecords = [];
for (i = 0; i < numPoints; i++) {
var angle = startPoint.theta(points[i]);
if (angle === 0) {
angle = 360; // give highest angle to start point
// the start point will end up at end of sorted list
// the start point will end up at beginning of hull points list
}
var entry = [points[i], i, angle];
sortedPointRecords.push(entry);
}
// step 2b: sort the list in place
sortedPointRecords.sort(function (record1, record2) {
// returning a negative number here sorts record1 before record2
// if first angle is smaller than second, first angle should come before second
var sortOutput = record1[2] - record2[2]; // negative if first angle smaller
if (sortOutput === 0) {
// if the two angles are equal, sort by originalIndex
sortOutput = record2[1] - record1[1]; // negative if first index larger
// coincident points will be sorted in reverse-numerical order
// so the coincident points with lower original index will be considered first
}
return sortOutput;
});
// step 2c: duplicate start record from the top of the stack to the bottom of the stack
if (sortedPointRecords.length > 2) {
var startPointRecord = sortedPointRecords[sortedPointRecords.length - 1];
sortedPointRecords.unshift(startPointRecord);
}
// step 3a: go through sorted points in order and find those with right turns
// we want to get our results in clockwise order
var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull
var hullPointRecords = []; // stack of records with right turns - hull point candidates
var currentPointRecord;
var currentPoint;
var lastHullPointRecord;
var lastHullPoint;
var secondLastHullPointRecord;
var secondLastHullPoint;
while (sortedPointRecords.length !== 0