UNPKG

jointjs

Version:

JavaScript diagramming library

1,670 lines (1,258 loc) 299 kB
/*! JointJS v3.4.4 (2021-09-27) - 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. var round = Math.round; var floor = Math.floor; var PI = Math.PI; var 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; } }; var normalizeAngle = function(angle) { return (angle % 360) + (angle < 0 ? 360 : 0); }; var snapToGrid = function(value, gridSize) { return gridSize * round(value / gridSize); }; var toDeg = function(rad) { return (180 * rad / PI) % 360; }; var 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. var 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 var 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. var cos = Math.cos; var sin = Math.sin; var atan2 = Math.atan2; var 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) var 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; }; var length = function(start, end) { return Math.sqrt(squaredLength(start, end)); }; /* 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))` */ var abs = Math.abs; var cos$1 = Math.cos; var sin$1 = Math.sin; var sqrt = Math.sqrt; var min = Math.min; var max = Math.max; var atan2$1 = Math.atan2; var round$1 = Math.round; var pow = Math.pow; var PI$1 = Math.PI; var 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 = { 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) { var 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: var point = Point; var max$1 = Math.max; var min$1 = Math.min; var 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 = { // @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) { var l = this.clone(); if (!this.isDifferentiable()) { return l; } var start = l.start; var end = l.end; var eRef = start.clone().rotate(end, 270); var 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: var line = Line; var sqrt$1 = Math.sqrt; var round$2 = Math.round; var pow$1 = Math.pow; var 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 = { 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) { var 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: var ellipse = Ellipse; var abs$1 = Math.abs; var cos$2 = Math.cos; var sin$2 = Math.sin; var min$2 = Math.min; var max$2 = Math.max; var round$3 = Math.round; var pow$2 = Math.pow; var 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() { var points = [], len = arguments.length; while ( len-- ) points[ len ] = arguments[ len ]; if (points.length === 0) { return null; } var p = new Point(); var minX, minY, maxX, maxY; minX = minY = Infinity; maxX = maxY = -Infinity; for (var i = 0; i < points.length; i++) { p.update(points[i]); var x = p.x; var 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() { var rects = [], len = arguments.length; while ( len-- ) rects[ len ] = arguments[ len ]; if (rects.length === 0) { return null; } var r = new Rect(); var minX, minY, maxX, maxY; minX = minY = Infinity; maxX = maxY = -Infinity; for (var i = 0; i < rects.length; i++) { r.update(rects[i]); var x = r.x; var y = r.y; var mX = x + r.width; var 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 = { // 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) { if (!angle) { return this.clone(); } var theta = toRad(angle); var st = abs$1(sin$2(theta)); var ct = abs$1(cos$2(theta)); var w = this.width * ct + this.height * st; var h = this.width * st + this.height * ct; return new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); }, 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. containsPoint: function(p) { p = new Point(p); return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.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) { var 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: var rect = Rect; var abs$2 = Math.abs; var Polyline = function(points) { if (!(this instanceof Polyline)) { return new Polyline(points); } if (typeof points === 'string') { return new Polyline.parse(points); } this.points = (Array.isArray(points) ? points.map(Point) : []); }; Polyline.parse = function(svgString) { svgString = svgString.trim(); if (svgString === '') { return new Polyline(); } var points = []; var coords = svgString.split(/\s*,\s*|\s+/); var n = coords.length; for (var i = 0; i < n; i += 2) { points.push({ x: +coords[i], y: +coords[i + 1] }); } return new Polyline(points); }; Polyline.prototype = { bbox: function() { var x1 = Infinity; var x2 = -Infinity; var y1 = Infinity; var y2 = -Infinity; var points = this.points; var numPoints = points.length; if (numPoints === 0) { return null; } // if points array is empty for (var i = 0; i < numPoints; i++) { var point = points[i]; var x = point.x; var y = point.y; if (x < x1) { x1 = x; } if (x > x2) { x2 = x; } if (y < y1) { y1 = y; } if (y > y2) { y2 = y; } } return new Rect(x1, y1, x2 - x1, y2 - y1); }, clone: function() { var points = this.points; var numPoints = points.length; if (numPoints === 0) {