UNPKG

@joint/core

Version:

JavaScript diagramming library

1,416 lines (1,368 loc) 297 kB
/*! 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