/*! JointJS v0.9.7 - JavaScript diagramming library 2016-04-20 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/. */ // Geometry library. // (c) 2011-2015 client IO var g = (function() { // Declare shorthands to the most used math functions. var math = Math; var abs = math.abs; var cos = math.cos; var sin = math.sin; var sqrt = math.sqrt; var mmin = math.min; var mmax = math.max; var atan = math.atan; var atan2 = math.atan2; var acos = math.acos; var round = math.round; var floor = math.floor; var PI = math.PI; var random = math.random; 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; }; var snapToGrid = function(val, gridSize) { return gridSize * Math.round(val / gridSize); }; var normalizeAngle = function(angle) { return (angle % 360) + (angle < 0 ? 360 : 0); }; // Point // ----- // 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))` function point(x, y) { if (!(this instanceof point)) return new point(x, y); var xy; if (y === undefined && Object(x) !== x) { xy = x.split(x.indexOf('@') === -1 ? ' ' : '@'); this.x = parseInt(xy[0], 10); this.y = parseInt(xy[1], 10); } else if (Object(x) === x) { this.x = x.x; this.y = x.y; } else { this.x = x; this.y = y; } } point.prototype = { toString: function() { return this.x + '@' + this.y; }, // 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 = mmin(mmax(this.x, r.x), r.x + r.width); this.y = mmin(mmax(this.y, r.y), r.y + r.height); return this; }, // 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 = point(p); // Invert the y-axis. var y = -(p.y - this.y); var x = p.x - this.x; // Makes sure that the comparison with zero takes rounding errors into account. var PRECISION = 10; // Note that `atan2` is not defined for `x`, `y` both equal zero. var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x); // Correction for III. and IV. quadrant. if (rad < 0) { rad = 2 * PI + rad; } return 180 * rad / PI; }, // Returns distance between me and point `p`. distance: function(p) { return line(this, p).length(); }, // 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); }, // Offset me by the specified amount. offset: function(dx, dy) { this.x += dx || 0; this.y += dy || 0; return this; }, magnitude: function() { return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01; }, update: function(x, y) { this.x = x || 0; this.y = y || 0; return this; }, round: function(decimals) { this.x = decimals ? this.x.toFixed(decimals) : round(this.x); this.y = decimals ? this.y.toFixed(decimals) : round(this.y); return this; }, // Scale the line segment between (0,0) and me to have a length of len. normalize: function(len) { var s = (len || 1) / this.magnitude(); this.x = s * this.x; this.y = s * this.y; return this; }, difference: function(p) { return point(this.x - p.x, this.y - p.y); }, // Return the bearing between me and point `p`. bearing: function(p) { return line(this, p).bearing(); }, // Converts rectangular to polar coordinates. // An origin can be specified, otherwise it's 0@0. toPolar: function(o) { o = (o && point(o)) || 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(point(x, y))); return this; }, // Rotate point by angle around origin o. rotate: function(o, angle) { angle = (angle + 360) % 360; this.toPolar(o); this.y += toRad(angle); var p = point.fromPolar(this.x, this.y, o); this.x = p.x; this.y = p.y; return this; }, // Move point on line starting from ref ending at me by // distance distance. move: function(ref, distance) { var theta = toRad(point(ref).theta(this)); return this.offset(cos(theta) * distance, -sin(theta) * distance); }, // Scale point with origin at point `o`. scale: function(sx, sy, o) { o = (o && point(o)) || point(0, 0); this.x = o.x + sx * (this.x - o.x); this.y = o.y + sy * (this.y - o.y); return this; }, // 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 point(this).offset(-dx, -dy).theta(ref) - this.theta(ref); }, equals: function(p) { return this.x === p.x && this.y === p.y; }, snapToGrid: function(gx, gy) { this.x = snapToGrid(this.x, gx); this.y = snapToGrid(this.y, gy || gx); return this; }, // Returns a point that is the reflection of me with // the center of inversion in ref point. reflection: function(ref) { return point(ref).move(this, this.distance(ref)); }, clone: function() { return point(this); }, toJSON: function() { return { x: this.x, y: this.y }; } }; // Alternative constructor, from polar coordinates. // @param {number} r Distance. // @param {number} angle Angle in radians. // @param {point} [optional] o Origin. point.fromPolar = function(r, angle, o) { o = (o && point(o)) || point(0, 0); var x = abs(r * cos(angle)); var y = abs(r * sin(angle)); var deg = normalizeAngle(toDeg(angle)); if (deg < 90) { y = -y; } else if (deg < 180) { x = -x; y = -y; } else if (deg < 270) { x = -x; } return point(o.x + x, o.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 point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1)); }; // Line. // ----- function line(p1, p2) { if (!(this instanceof line)) return new line(p1, p2); this.start = point(p1); this.end = point(p2); } line.prototype = { toString: function() { return this.start.toString() + ' ' + this.end.toString(); }, // @return {double} length of the line length: function() { return sqrt(this.squaredLength()); }, // @return {integer} length without sqrt // @note for applications where the exact length is not necessary (e.g. compare only) squaredLength: function() { var x0 = this.start.x; var y0 = this.start.y; var x1 = this.end.x; var y1 = this.end.y; return (x0 -= x1) * x0 + (y0 -= y1) * y0; }, // @return {point} my midpoint midpoint: function() { return point((this.start.x + this.end.x) / 2, (this.start.y + this.end.y) / 2); }, // @return {point} Point where I'm intersecting l. // @see Squeak Smalltalk, LineSegment>>intersectionWith: intersection: function(l) { var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y); var pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y); var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x); var deltaPt = point(l.start.x - this.start.x, l.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 point(this.start.x + (alpha * pt1Dir.x / det), this.start.y + (alpha * pt1Dir.y / det)); }, // @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() { var lat1 = toRad(this.start.y); var lat2 = toRad(this.end.y); var lon1 = this.start.x; var lon2 = this.end.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 {point} my point at 't' <0,1> pointAt: function(t) { var x = (1 - t) * this.start.x + t * this.end.x; var y = (1 - t) * this.start.y + t * this.end.y; return point(x, y); }, // @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. return ((this.end.x - this.start.x) * (p.y - this.start.y) - (this.end.y - this.start.y) * (p.x - this.start.x)) / 2; }, clone: function() { return line(this); } }; // Rectangle. // ---------- function rect(x, y, w, h) { if (!(this instanceof rect)) return new rect(x, y, w, h); if (y === undefined) { y = x.y; w = x.width; h = x.height; x = x.x; } this.x = x; this.y = y; this.width = w; this.height = h; } rect.prototype = { toString: function() { return this.origin().toString() + ' ' + this.corner().toString(); }, // @return {boolean} true if rectangles are equal. equals: function(r) { var mr = g.rect(this).normalize(); var nr = g.rect(r).normalize(); return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height; }, origin: function() { return point(this.x, this.y); }, corner: function() { return point(this.x + this.width, this.y + this.height); }, topRight: function() { return point(this.x + this.width, this.y); }, bottomLeft: function() { return point(this.x, this.y + this.height); }, center: function() { return point(this.x + this.width / 2, this.y + this.height / 2); }, // @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 = Math.max(myOrigin.x, rOrigin.x); var y = Math.max(myOrigin.y, rOrigin.y); return rect(x, y, Math.min(myCorner.x, rCorner.x) - x, Math.min(myCorner.y, rCorner.y) - y); }, // @return {rect} representing the union of both rectangles. union: function(r) { var myOrigin = this.origin(); var myCorner = this.corner(); var rOrigin = r.origin(); var rCorner = r.corner(); var originX = Math.min(myOrigin.x, rOrigin.x); var originY = Math.min(myOrigin.y, rOrigin.y); var cornerX = Math.max(myCorner.x, rCorner.x); var cornerY = Math.max(myCorner.y, rCorner.y); return rect(originX, originY, cornerX - originX, cornerY - originY); }, // @return {string} (left|right|top|bottom) side which is nearest to point // @see Squeak Smalltalk, Rectangle>>sideNearestTo: sideNearestToPoint: function(p) { p = point(p); var distToLeft = p.x - this.x; var distToRight = (this.x + this.width) - p.x; var distToTop = p.y - this.y; var distToBottom = (this.y + this.height) - p.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; }, // @return {bool} true if point p is insight me containsPoint: function(p) { p = point(p); if (p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height) { return true; } return false; }, // @return {bool} true if rectangle `r` is inside me. containsRect: function(r) { var r0 = rect(this).normalize(); var r1 = 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; }, // @return {point} a point on my boundary nearest to p // @see Squeak Smalltalk, Rectangle>>pointNearestTo: pointNearestToPoint: function(p) { p = point(p); if (this.containsPoint(p)) { var side = this.sideNearestToPoint(p); switch (side){ case 'right': return point(this.x + this.width, p.y); case 'left': return point(this.x, p.y); case 'bottom': return point(p.x, this.y + this.height); case 'top': return point(p.x, this.y); } } return p.adhereToRect(this); }, // 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 = point(p); var center = 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 = [ line(this.origin(), this.topRight()), line(this.topRight(), this.corner()), line(this.corner(), this.bottomLeft()), line(this.bottomLeft(), this.origin()) ]; var connector = 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; }, // 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; }, round: function(decimals) { this.x = decimals ? this.x.toFixed(decimals) : round(this.x); this.y = decimals ? this.y.toFixed(decimals) : round(this.y); this.width = decimals ? this.width.toFixed(decimals) : round(this.width); this.height = decimals ? this.height.toFixed(decimals) : round(this.height); 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; }, // 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) { var theta = toRad(angle || 0); var st = abs(sin(theta)); var ct = abs(cos(theta)); var w = this.width * ct + this.height * st; var h = this.width * st + this.height * ct; return rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h); }, // Scale rectangle with origin at point `o` scale: function(sx, sy, o) { var origin = this.origin().scale(sx, sy, o); this.x = origin.x; this.y = origin.y; this.width *= sx; this.height *= sy; return this; }, 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; }, clone: function() { return rect(this); }, toJSON: function() { return { x: this.x, y: this.y, width: this.width, height: this.height }; } }; // Ellipse. // -------- function ellipse(c, a, b) { if (!(this instanceof ellipse)) return new ellipse(c, a, b); c = point(c); this.x = c.x; this.y = c.y; this.a = a; this.b = b; } ellipse.prototype = { toString: function() { return point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b; }, bbox: function() { return rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b); }, // 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 = point(p); if (angle) p.rotate(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(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 / aSquared) + (mSquared / bSquared))); x = dx < 0 ? -x : x; var y = m * x; result = point(this.x + x, this.y + y); if (angle) return result.rotate(point(this.x, this.y), -angle); return result; }, clone: function() { return ellipse(this); } }; // Bezier curve. // ------------- var bezier = { // Cubic Bezier curve path through points. // Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx). // @param {array} points Array of points through which the smooth line will go. // @return {array} SVG Path commands as an array curveThroughPoints: function(points) { var controlPoints = this.getCurveControlPoints(points); var path = ['M', points[0].x, points[0].y]; for (var i = 0; i < controlPoints[0].length; i++) { path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i + 1].x, points[i + 1].y); } return path; }, // Get open-ended Bezier Spline Control Points. // @param knots Input Knot Bezier spline points (At least two points!). // @param firstControlPoints Output First Control points. Array of knots.length - 1 length. // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length. getCurveControlPoints: function(knots) { var firstControlPoints = []; var secondControlPoints = []; var n = knots.length - 1; var i; // Special case: Bezier curve should be a straight line. if (n == 1) { // 3P1 = 2P0 + P3 firstControlPoints[0] = point((2 * knots[0].x + knots[1].x) / 3, (2 * knots[0].y + knots[1].y) / 3); // P2 = 2P1 – P0 secondControlPoints[0] = point(2 * firstControlPoints[0].x - knots[0].x, 2 * firstControlPoints[0].y - knots[0].y); return [firstControlPoints, secondControlPoints]; } // Calculate first Bezier control points. // Right hand side vector. var rhs = []; // Set right hand side X values. for (i = 1; i < n - 1; i++) { rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x; } rhs[0] = knots[0].x + 2 * knots[1].x; rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0; // Get first control points X-values. var x = this.getFirstControlPoints(rhs); // Set right hand side Y values. for (i = 1; i < n - 1; ++i) { rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y; } rhs[0] = knots[0].y + 2 * knots[1].y; rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0; // Get first control points Y-values. var y = this.getFirstControlPoints(rhs); // Fill output arrays. for (i = 0; i < n; i++) { // First control point. firstControlPoints.push(point(x[i], y[i])); // Second control point. if (i < n - 1) { secondControlPoints.push(point(2 * knots [i + 1].x - x[i + 1], 2 * knots[i + 1].y - y[i + 1])); } else { secondControlPoints.push(point((knots[n].x + x[n - 1]) / 2, (knots[n].y + y[n - 1]) / 2)); } } return [firstControlPoints, secondControlPoints]; }, // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points. // @param rhs Right hand side vector. // @return Solution vector. getFirstControlPoints: function(rhs) { var n = rhs.length; // `x` is a solution vector. var x = []; var tmp = []; var b = 2.0; x[0] = rhs[0] / b; // Decomposition and forward substitution. for (var i = 1; i < n; i++) { tmp[i] = 1 / b; b = (i < n - 1 ? 4.0 : 3.5) - tmp[i]; x[i] = (rhs[i] - x[i - 1]) / b; } for (i = 1; i < n; i++) { // Backsubstitution. x[n - i - 1] -= tmp[n - i] * x[n - i]; } return x; }, // Solves an inversion problem -- Given the (x, y) coordinates of a point which lies on // a parametric curve x = x(t)/w(t), y = y(t)/w(t), find the parameter value t // which corresponds to that point. // @param control points (start, control start, control end, end) // @return a function accepts a point and returns t. getInversionSolver: function(p0, p1, p2, p3) { var pts = arguments; function l(i, j) { // calculates a determinant 3x3 // [p.x p.y 1] // [pi.x pi.y 1] // [pj.x pj.y 1] var pi = pts[i]; var pj = pts[j]; return function(p) { var w = (i % 3 ? 3 : 1) * (j % 3 ? 3 : 1); var lij = p.x * (pi.y - pj.y) + p.y * (pj.x - pi.x) + pi.x * pj.y - pi.y * pj.x; return w * lij; }; } return function solveInversion(p) { var ct = 3 * l(2, 3)(p1); var c1 = l(1, 3)(p0) / ct; var c2 = -l(2, 3)(p0) / ct; var la = c1 * l(3, 1)(p) + c2 * (l(3, 0)(p) + l(2, 1)(p)) + l(2, 0)(p); var lb = c1 * l(3, 0)(p) + c2 * l(2, 0)(p) + l(1, 0)(p); return lb / (lb - la); }; }, // Divide a Bezier curve into two at point defined by value 't' <0,1>. // Using deCasteljau algorithm. http://math.stackexchange.com/a/317867 // @param control points (start, control start, control end, end) // @return a function accepts t and returns 2 curves each defined by 4 control points. getCurveDivider: function(p0, p1, p2, p3) { return function divideCurve(t) { var l = line(p0, p1).pointAt(t); var m = line(p1, p2).pointAt(t); var n = line(p2, p3).pointAt(t); var p = line(l, m).pointAt(t); var q = line(m, n).pointAt(t); var r = line(p, q).pointAt(t); return [{ p0: p0, p1: l, p2: p, p3: r }, { p0: r, p1: q, p2: n, p3: p3 }]; }; } }; // Scale. 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; } }; return { toDeg: toDeg, toRad: toRad, snapToGrid: snapToGrid, normalizeAngle: normalizeAngle, point: point, line: line, rect: rect, ellipse: ellipse, bezier: bezier, scale: scale }; })(); // Vectorizer. // ----------- // A tiny library for making your life easier when dealing with SVG. // The only Vectorizer dependency is the Geometry library. // Copyright © 2012 - 2015 client IO (http://client.io) var V; var Vectorizer; V = Vectorizer = (function() { 'use strict'; var hasSvg = typeof window === 'object' && !!( window.SVGAngle || document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#BasicStructure', '1.1') ); // SVG support is required. if (!hasSvg) { // Return a function that throws an error when it is used. return function() { throw new Error('SVG is required to use Vectorizer.'); }; } // XML namespaces. var ns = { xmlns: 'http://www.w3.org/2000/svg', xlink: 'http://www.w3.org/1999/xlink' }; var SVGversion = '1.1'; var V = function(el, attrs, children) { // This allows using V() without the new keyword. if (!(this instanceof V)) { return V.apply(Object.create(V.prototype), arguments); } if (!el) return; if (V.isV(el)) { el = el.node; } attrs = attrs || {}; if (V.isString(el)) { if (el.toLowerCase() === 'svg') { // Create a new SVG canvas. el = V.createSvgDocument(); } else if (el[0] === '<') { // Create element from an SVG string. // Allows constructs of type: `document.appendChild(V('').node)`. var svgDoc = V.createSvgDocument(el); // Note that `V()` might also return an array should the SVG string passed as // the first argument contain more than one root element. if (svgDoc.childNodes.length > 1) { // Map child nodes to `V`s. var arrayOfVels = []; var i, len; for (i = 0, len = svgDoc.childNodes.length; i < len; i++) { var childNode = svgDoc.childNodes[i]; arrayOfVels.push(new V(document.importNode(childNode, true))); } return arrayOfVels; } el = document.importNode(svgDoc.firstChild, true); } else { el = document.createElementNS(ns.xmlns, el); } } this.node = el; if (!this.node.id) { this.node.id = V.uniqueId(); } this.setAttributes(attrs); if (children) { this.append(children); } return this; }; /** * @param {SVGGElement} toElem * @returns {SVGMatrix} */ V.prototype.getTransformToElement = function(toElem) { return toElem.getScreenCTM().inverse().multiply(this.node.getScreenCTM()); }; /** * @param {SVGMatrix} matrix * @returns {V, SVGMatrix} Setter / Getter */ V.prototype.transform = function(matrix) { if (V.isUndefined(matrix)) { return (this.node.parentNode) ? this.getTransformToElement(this.node.parentNode) : this.node.getScreenCTM(); } var svgTransform = V.createSVGTransform(matrix); this.node.transform.baseVal.appendItem(svgTransform); return this; }; V.prototype.translate = function(tx, ty, opt) { opt = opt || {}; ty = ty || 0; var transformAttr = this.attr('transform') || ''; var transform = V.parseTransformString(transformAttr); // Is it a getter? if (V.isUndefined(tx)) { return transform.translate; } transformAttr = transformAttr.replace(/translate\([^\)]*\)/g, '').trim(); var newTx = opt.absolute ? tx : transform.translate.tx + tx; var newTy = opt.absolute ? ty : transform.translate.ty + ty; var newTranslate = 'translate(' + newTx + ',' + newTy + ')'; // Note that `translate()` is always the first transformation. This is // usually the desired case. this.attr('transform', (newTranslate + ' ' + transformAttr).trim()); return this; }; V.prototype.rotate = function(angle, cx, cy, opt) { opt = opt || {}; var transformAttr = this.attr('transform') || ''; var transform = V.parseTransformString(transformAttr); // Is it a getter? if (V.isUndefined(angle)) { return transform.rotate; } transformAttr = transformAttr.replace(/rotate\([^\)]*\)/g, '').trim(); angle %= 360; var newAngle = opt.absolute ? angle : transform.rotate.angle + angle; var newOrigin = (cx !== undefined && cy !== undefined) ? ',' + cx + ',' + cy : ''; var newRotate = 'rotate(' + newAngle + newOrigin + ')'; this.attr('transform', (transformAttr + ' ' + newRotate).trim()); return this; }; // Note that `scale` as the only transformation does not combine with previous values. V.prototype.scale = function(sx, sy) { sy = V.isUndefined(sy) ? sx : sy; var transformAttr = this.attr('transform') || ''; var transform = V.parseTransformString(transformAttr); // Is it a getter? if (V.isUndefined(sx)) { return transform.scale; } transformAttr = transformAttr.replace(/scale\([^\)]*\)/g, '').trim(); var newScale = 'scale(' + sx + ',' + sy + ')'; this.attr('transform', (transformAttr + ' ' + newScale).trim()); return this; }; // Get SVGRect that contains coordinates and dimension of the real bounding box, // i.e. after transformations are applied. // If `target` is specified, bounding box will be computed relatively to `target` element. V.prototype.bbox = function(withoutTransformations, target) { // If the element is not in the live DOM, it does not have a bounding box defined and // so fall back to 'zero' dimension element. if (!this.node.ownerSVGElement) return { x: 0, y: 0, width: 0, height: 0 }; var box; try { box = this.node.getBBox(); // We are creating a new object as the standard says that you can't // modify the attributes of a bbox. box = { x: box.x, y: box.y, width: box.width, height: box.height }; } catch (e) { // Fallback for IE. box = { x: this.node.clientLeft, y: this.node.clientTop, width: this.node.clientWidth, height: this.node.clientHeight }; } if (withoutTransformations) { return box; } var matrix = this.getTransformToElement(target || this.node.ownerSVGElement); return V.transformRect(box, matrix); }; V.prototype.text = function(content, opt) { // Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm). // IE would otherwise collapse all spaces into one. content = V.sanitizeText(content); opt = opt || {}; var lines = content.split('\n'); var i = 0; var tspan; // `alignment-baseline` does not work in Firefox. // Setting `dominant-baseline` on the `` element doesn't work in IE9. // In order to have the 0,0 coordinate of the `` element (or the first ``) // in the top left corner we translate the `` element by `0.8em`. // See `http://www.w3.org/Graphics/SVG/WG/wiki/How_to_determine_dominant_baseline`. // See also `http://apike.ca/prog_svg_text_style.html`. var y = this.attr('y'); if (!y) { this.attr('y', '0.8em'); } // An empty text gets rendered into the DOM in webkit-based browsers. // In order to unify this behaviour across all browsers // we rather hide the text element when it's empty. this.attr('display', content ? null : 'none'); // Preserve spaces. In other words, we do not want consecutive spaces to get collapsed to one. this.node.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); // Easy way to erase all `` children; this.node.textContent = ''; var textNode = this.node; if (opt.textPath) { // Wrap the text in the SVG element that points // to a path defined by `opt.textPath` inside the internal `` element. var defs = this.find('defs'); if (defs.length === 0) { defs = V('defs'); this.append(defs); } // If `opt.textPath` is a plain string, consider it to be directly the // SVG path data for the text to go along (this is a shortcut). // Otherwise if it is an object and contains the `d` property, then this is our path. var d = Object(opt.textPath) === opt.textPath ? opt.textPath.d : opt.textPath; if (d) { var path = V('path', { d: d }); defs.append(path); } var textPath = V('textPath'); // Set attributes on the ``. The most important one // is the `xlink:href` that points to our newly created `` element in ``. // Note that we also allow the following construct: // `t.text('my text', { textPath: { 'xlink:href': '#my-other-path' } })`. // In other words, one can completely skip the auto-creation of the path // and use any other arbitrary path that is in the document. if (!opt.textPath['xlink:href'] && path) { textPath.attr('xlink:href', '#' + path.node.id); } if (Object(opt.textPath) === opt.textPath) { textPath.attr(opt.textPath); } this.append(textPath); // Now all the ``s will be inside the ``. textNode = textPath.node; } var offset = 0; for (var i = 0; i < lines.length; i++) { var line = lines[i]; // Shift all the but first by one line (`1em`) var lineHeight = opt.lineHeight || '1em'; if (opt.lineHeight === 'auto') { lineHeight = '1.5em'; } var vLine = V('tspan', { dy: (i == 0 ? '0em' : lineHeight), x: this.attr('x') || 0 }); vLine.addClass('v-line'); if (line) { if (opt.annotations) { // Get the line height based on the biggest font size in the annotations for this line. var maxFontSize = 0; // Find the *compacted* annotations for this line. var lineAnnotations = V.annotateString(lines[i], V.isArray(opt.annotations) ? opt.annotations : [opt.annotations], { offset: -offset, includeAnnotationIndices: opt.includeAnnotationIndices }); for (var j = 0; j < lineAnnotations.length; j++) { var annotation = lineAnnotations[j]; if (V.isObject(annotation)) { var fontSize = parseInt(annotation.attrs['font-size'], 10); if (fontSize && fontSize > maxFontSize) { maxFontSize = fontSize; } tspan = V('tspan', annotation.attrs); if (opt.includeAnnotationIndices) { // If `opt.includeAnnotationIndices` is `true`, // set the list of indices of all the applied annotations // in the `annotations` attribute. This list is a comma // separated list of indices. tspan.attr('annotations', annotation.annotations); } if (annotation.attrs['class']) { tspan.addClass(annotation.attrs['class']); } tspan.node.textContent = annotation.t; } else { tspan = document.createTextNode(annotation || ' '); } vLine.append(tspan); } if (opt.lineHeight === 'auto' && maxFontSize && i !== 0) { vLine.attr('dy', (maxFontSize * 1.2) + 'px'); } } else { vLine.node.textContent = line; } } else { // Make sure the textContent is never empty. If it is, add a dummy // character and make it invisible, making the following lines correctly // relatively positioned. `dy=1em` won't work with empty lines otherwise. vLine.addClass('v-empty-line'); vLine.node.style.opacity = 0; vLine.node.textContent = '-'; } V(textNode).append(vLine); offset += line.length + 1; // + 1 = newline character. } return this; }; V.prototype.attr = function(name, value) { if (V.isUndefined(name)) { // Return all attributes. var attributes = this.node.attributes; var attrs = {}; for (var i = 0; i < attributes.length; i++) { attrs[attributes[i].nodeName] = attributes[i].nodeValue; } return attrs; } if (V.isString(name) && V.isUndefined(value)) { return this.node.getAttribute(name); } if (typeof name === 'object') { for (var attrName in name) { if (name.hasOwnProperty(attrName)) { V.setAttribute(this.node, attrName, name[attrName]); } } } else { V.setAttribute(this.node, name, value); } return this; }; V.prototype.remove = function() { if (this.node.parentNode) { this.node.parentNode.removeChild(this.node); } return this; }; V.prototype.empty = function() { while (this.node.firstChild) { this.node.removeChild(this.node.firstChild); } return this; }; V.prototype.setAttributes = function(attrs) { var key; for (key in attrs) { V.setAttribute(this.node, key, attrs[key]); } return this; }; V.prototype.append = function(els) { if (!V.isArray(els)) { els = [els]; } var i, len, el; for (i = 0, len = els.length; i < len; i++) { el = els[i]; this.node.appendChild(V.isV(el) ? el.node : (el.nodeName && el || el[0])); } return this; }; V.prototype.prepend = function(el) { this.node.insertBefore(V.isV(el) ? el.node : el, this.node.firstChild); return this; }; V.prototype.svg = function() { return this.node instanceof window.SVGSVGElement ? this : V(this.node.ownerSVGElement); }; V.prototype.defs = function() { var defs = this.svg().node.getElementsByTagName('defs'); return (defs && defs.length) ? V(defs[0]) : undefined; }; V.prototype.clone = function() { var clone = V(this.node.cloneNode(true/* deep */)); // Note that clone inherits also ID. Therefore, we need to change it here. clone.node.id = V.uniqueId(); return clone; }; V.prototype.findOne = function(selector) { var found = this.node.querySelector(selector); return found ? V(found) : undefined; }; V.prototype.find = function(selector) { var vels = []; var nodes = this.node.querySelectorAll(selector); if (nodes) { // Map DOM elements to `V`s. for (var i = 0; i < nodes.length; i++) { vels.push(V(nodes[i])); } } return vels; }; // Find an index of an element inside its container. V.prototype.index = function() { var index = 0; var node = this.node.previousSibling; while (node) { // nodeType 1 for ELEMENT_NODE if (node.nodeType === 1) index++; node = node.previousSibling; } return index; }; V.prototype.findParentByClass = function(className, terminator) { var ownerSVGElement = this.node.ownerSVGElement; var node = this.node.parentNode; while (node && node !== terminator && node !== ownerSVGElement) { var vel = V(node); if (vel.hasClass(className)) { return vel; } node = node.parentNode; } return null; }; // Convert global point into the coordinate space of this element. V.prototype.toLocalPoint = function(x, y) { var svg = this.svg().node; var p = svg.createSVGPoint(); p.x = x; p.y = y; try { var globalPoint = p.matrixTransform(svg.getScreenCTM().inverse()); var globalToLocalMatrix = this.getTransformToElement(svg).inverse(); } catch (e) { // IE9 throws an exception in odd cases. (`Unexpected call to method or property access`) // We have to make do with the original coordianates. return p; } return globalPoint.matrixTransform(globalToLocalMatrix); }; V.prototype.translateCenterToPoint = function(p) { var bbox = this.bbox(); var center = g.rect(bbox).center(); this.translate(p.x - center.x, p.y - center.y); }; // Efficiently auto-orient an element. This basically implements the orient=auto attribute // of markers. The easiest way of understanding on what this does is to imagine the element is an // arrowhead. Calling this method on the arrowhead makes it point to the `position` point while // being auto-oriented (properly rotated) towards the `reference` point. // `target` is the element relative to which the transformations are applied. Usually a viewport. V.prototype.translateAndAutoOrient = function(position, reference, target) { // Clean-up previously set transformations except the scale. If we didn't clean up the // previous transformations then they'd add up with the old ones. Scale is an exception as // it doesn't add up, consider: `this.scale(2).scale(2).scale(2)`. The result is that the // element is scaled by the factor 2, not 8. var s = this.scale(); this.attr('transform', ''); this.scale(s.sx, s.sy); var svg = this.svg().node; var bbox = this.bbox(false, target); // 1. Translate to origin. var translateToOrigin = svg.createSVGTransform(); translateToOrigin.setTranslate(-bbox.x - bbox.width / 2, -bbox.y - bbox.height / 2); // 2. Rotate around origin. var rotateAroundOrigin = svg.createSVGTransform(); var angle = g.point(position).changeInAngle(position.x - reference.x, position.y - reference.y, reference); rotateAroundOrigin.setRotate(angle, 0, 0); // 3. Translate to the `position` + the offset (half my width) towards the `reference` point. var translateFinal = svg.createSVGTransform(); var finalPosition = g.point(position).move(reference, bbox.width / 2); translateFinal.setTranslate(position.x + (position.x - finalPosition.x), position.y + (position.y - finalPosition.y)); // 4. Apply transformations. var ctm = this.getTransformToElement(target); var transform = svg.createSVGTransform(); transform.setMatrix( translateFinal.matrix.multiply( rotateAroundOrigin.matrix.multiply( translateToOrigin.matrix.multiply( ctm))) ); // Instead of directly setting the `matrix()` transform on the element, first, decompose // the matrix into separate transforms. This allows us to use normal Vectorizer methods // as they don't work on matrices. An example of this is to retrieve a scale of an element. // this.node.transform.baseVal.initialize(transform); var decomposition = V.decomposeMatrix(transform.matrix); this.translate(decomposition.translateX, decomposition.translateY); this.rotate(decomposition.rotation); // Note that scale has been already applied, hence the following line stays commented. (it's here just for reference). //this.scale(decomposition.scaleX, decomposition.scaleY); return this; }; V.prototype.animateAlongPath = function(attrs, path) { var animateMotion = V('animateMotion', attrs); var mpath = V('mpath', { 'xlink:href': '#' + V(path).node.id }); animateMotion.append(mpath); this.append(animateMotion); try { animateMotion.node.beginElement(); } catch (e) { // Fallback for IE 9. // Run the animation programatically if FakeSmile (`http://leunen.me/fakesmile/`) present if (document.documentElement.getAttribute('smiling') === 'fake') { // Register the animation. (See `https://answers.launchpad.net/smil/+question/203333`) var animation = animateMotion.node; animation.animators = []; var animationID = animation.getAttribute('id'); if (animationID) id2anim[animationID] = animation; var targets = getTargets(animation); for (var i = 0, len = targets.length; i < len; i++) { var target = targets[i]; var animator = new Animator(animation, target, i); animators.push(animator); animation.animators[i] = animator; animator.register(); } } } }; V.prototype.hasClass = function(className) { return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.node.getAttribute('class')); }; V.prototype.addClass = function(className) { if (!this.hasClass(className)) { var prevClasses = this.node.getAttribute('class') || ''; this.node.setAttribute('class', (prevClasses + ' ' + className).trim()); } return this; }; V.prototype.removeClass = function(className) { if (this.hasClass(className)) { var newClasses = this.node.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2'); this.node.setAttribute('class', newClasses); } return this; }; V.prototype.toggleClass = function(className, toAdd) { var toRemove = V.isUndefined(toAdd) ? this.hasClass(className) : !toAdd; if (toRemove) { this.removeClass(className); } else { this.addClass(className); } return this; }; // Interpolate path by discrete points. The precision of the sampling // is controlled by `interval`. In other words, `sample()` will generate // a point on the path starting at the beginning of the path going to the end // every `interval` pixels. // The sampler can be very useful for e.g. finding intersection between two // paths (finding the two closest points from two samples). V.prototype.sample = function(interval) { interval = interval || 1; var node = this.node; var length = node.getTotalLength(); var samples = []; var distance = 0; var sample; while (distance < length) { sample = node.getPointAtLength(distance); samples.push({ x: sample.x, y: sample.y, distance: distance }); distance += interval; } return samples; }; V.prototype.convertToPath = function() { var path = V('path'); path.attr(this.attr()); var d = this.convertToPathData(); if (d) { path.attr('d', d); } return path; }; V.prototype.convertToPathData = function() { var tagName = this.node.tagName.toUpperCase(); switch (tagName) { case 'PATH': return this.attr('d'); case 'LINE': return V.convertLineToPathData(this.node); case 'POLYGON': return V.convertPolygonToPathData(this.node); case 'POLYLINE': return V.convertPolylineToPathData(this.node); case 'ELLIPSE': return V.convertEllipseToPathData(this.node); case 'CIRCLE': return V.convertCircleToPathData(this.node); case 'RECT': return V.convertRectToPathData(this.node); } throw new Error(tagName + ' cannot be converted to PATH.'); }; // Find the intersection of a line starting in the center // of the SVG `node` ending in the point `ref`. // `target` is an SVG element to which `node`s transformations are relative to. // In JointJS, `target` is the `paper.viewport` SVG group element. // Note that `ref` point must be in the coordinate system of the `target` for this function to work properly. // Returns a point in the `target` coordinte system (the same system as `ref` is in) if // an intersection is found. Returns `undefined` otherwise. V.prototype.findIntersection = function(ref, target) { var svg = this.svg().node; target = target || svg; var bbox = g.rect(this.bbox(false, target)); var center = bbox.center(); if (!bbox.intersectionWithLineFromCenterToPoint(ref)) return undefined; var spot; var tagName = this.node.localName.toUpperCase(); // Little speed up optimalization for `` element. We do not do conversion // to path element and sampling but directly calculate the intersection through // a transformed geometrical rectangle. if (tagName === 'RECT') { var gRect = g.rect( parseFloat(this.attr('x') || 0), parseFloat(this.attr('y') || 0), parseFloat(this.attr('width')), parseFloat(this.attr('height')) ); // Get the rect transformation matrix with regards to the SVG document. var rectMatrix = this.getTransformToElement(target); // Decompose the matrix to find the rotation angle. var rectMatrixComponents = V.decomposeMatrix(rectMatrix); // Now we want to rotate the rectangle back so that we // can use `intersectionWithLineFromCenterToPoint()` passing the angle as the second argument. var resetRotation = svg.createSVGTransform(); resetRotation.setRotate(-rectMatrixComponents.rotation, center.x, center.y); var rect = V.transformRect(gRect, resetRotation.matrix.multiply(rectMatrix)); spot = g.rect(rect).intersectionWithLineFromCenterToPoint(ref, rectMatrixComponents.rotation); } else if (tagName === 'PATH' || tagName === 'POLYGON' || tagName === 'POLYLINE' || tagName === 'CIRCLE' || tagName === 'ELLIPSE') { var pathNode = (tagName === 'PATH') ? this : this.convertToPath(); var samples = pathNode.sample(); var minDistance = Infinity; var closestSamples = []; var i, sample, gp, centerDistance, refDistance, distance; for (i = 0; i < samples.length; i++) { sample = samples[i]; // Convert the sample point in the local coordinate system to the global coordinate system. gp = V.createSVGPoint(sample.x, sample.y); gp = gp.matrixTransform(this.getTransformToElement(target)); sample = g.point(gp); centerDistance = sample.distance(center); // Penalize a higher distance to the reference point by 10%. // This gives better results. This is due to // inaccuracies introduced by rounding errors and getPointAtLength() returns. refDistance = sample.distance(ref) * 1.1; distance = centerDistance + refDistance; if (distance < minDistance) { minDistance = distance; closestSamples = [{ sample: sample, refDistance: refDistance }]; } else if (distance < minDistance + 1) { closestSamples.push({ sample: sample, refDistance: refDistance }); } } closestSamples.sort(function(a, b) { return a.refDistance - b.refDistance; }); if (closestSamples[0]) { spot = closestSamples[0].sample; } } return spot; }; // Create an SVG document element. // If `content` is passed, it will be used as the SVG content of the `` root element. V.createSvgDocument = function(content) { var svg = '' + (content || '') + ''; var xml = V.parseXML(svg, { async: false }); return xml.documentElement; }; V.idCounter = 0; // A function returning a unique identifier for this client session with every call. V.uniqueId = function() { var id = ++V.idCounter + ''; return 'v-' + id; }; // Replace all spaces with the Unicode No-break space (http://www.fileformat.info/info/unicode/char/a0/index.htm). // IE would otherwise collapse all spaces into one. This is used in the text() method but it is // also exposed so that the programmer can use it in case he needs to. This is useful e.g. in tests // when you want to compare the actual DOM text content without having to add the unicode character in // the place of all spaces. V.sanitizeText = function(text) { return (text || '').replace(/ /g, '\u00A0'); }; V.isUndefined = function(value) { return typeof value === 'undefined'; }; V.isString = function(value) { return typeof value === 'string'; }; V.isObject = function(value) { return value && (typeof value === 'object'); }; V.isArray = Array.isArray; V.parseXML = function(data, opt) { opt = opt || {}; var xml; try { var parser = new DOMParser(); if (!V.isUndefined(opt.async)) { parser.async = opt.async; } xml = parser.parseFromString(data, 'text/xml'); } catch (error) { xml = undefined; } if (!xml || xml.getElementsByTagName('parsererror').length) { throw new Error('Invalid XML: ' + data); } return xml; }; V.setAttribute = function(el, name, value) { if (name.indexOf(':') > -1) { // Attribute names can be namespaced. E.g. `image` elements // have a `xlink:href` attribute to set the source of the image. var combinedKey = name.split(':'); el.setAttributeNS(ns[combinedKey[0]], combinedKey[1], value); } else if (name === 'id') { el.id = value; } else { el.setAttribute(name, value); } }; V.parseTransformString = function(transform) { var translate, rotate, scale; if (transform) { var separator = /[ ,]+/; var translateMatch = transform.match(/translate\((.*)\)/); if (translateMatch) { translate = translateMatch[1].split(separator); } var rotateMatch = transform.match(/rotate\((.*)\)/); if (rotateMatch) { rotate = rotateMatch[1].split(separator); } var scaleMatch = transform.match(/scale\((.*)\)/); if (scaleMatch) { scale = scaleMatch[1].split(separator); } } var sx = (scale && scale[0]) ? parseFloat(scale[0]) : 1; return { translate: { tx: (translate && translate[0]) ? parseInt(translate[0], 10) : 0, ty: (translate && translate[1]) ? parseInt(translate[1], 10) : 0 }, rotate: { angle: (rotate && rotate[0]) ? parseInt(rotate[0], 10) : 0, cx: (rotate && rotate[1]) ? parseInt(rotate[1], 10) : undefined, cy: (rotate && rotate[2]) ? parseInt(rotate[2], 10) : undefined }, scale: { sx: sx, sy: (scale && scale[1]) ? parseFloat(scale[1]) : sx } }; }; V.deltaTransformPoint = function(matrix, point) { var dx = point.x * matrix.a + point.y * matrix.c + 0; var dy = point.x * matrix.b + point.y * matrix.d + 0; return { x: dx, y: dy }; }; V.decomposeMatrix = function(matrix) { // @see https://gist.github.com/2052247 // calculate delta transform point var px = V.deltaTransformPoint(matrix, { x: 0, y: 1 }); var py = V.deltaTransformPoint(matrix, { x: 1, y: 0 }); // calculate skew var skewX = ((180 / Math.PI) * Math.atan2(px.y, px.x) - 90); var skewY = ((180 / Math.PI) * Math.atan2(py.y, py.x)); return { translateX: matrix.e, translateY: matrix.f, scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b), scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d), skewX: skewX, skewY: skewY, rotation: skewX // rotation is the same as skew x }; }; V.isV = function(object) { return object instanceof V; }; // For backwards compatibility: V.isVElement = V.isV; var svgDocument = V('svg').node; V.createSVGMatrix = function(matrix) { var svgMatrix = svgDocument.createSVGMatrix(); for (var component in matrix) { svgMatrix[component] = matrix[component]; } return svgMatrix; }; V.createSVGTransform = function(matrix) { if (!V.isUndefined(matrix)) { if (!(matrix instanceof SVGMatrix)) { matrix = V.createSVGMatrix(matrix); } return svgDocument.createSVGTransformFromMatrix(matrix); } return svgDocument.createSVGTransform(); }; V.createSVGPoint = function(x, y) { var p = svgDocument.createSVGPoint(); p.x = x; p.y = y; return p; }; V.transformRect = function(r, matrix) { var p = svgDocument.createSVGPoint(); p.x = r.x; p.y = r.y; var corner1 = p.matrixTransform(matrix); p.x = r.x + r.width; p.y = r.y; var corner2 = p.matrixTransform(matrix); p.x = r.x + r.width; p.y = r.y + r.height; var corner3 = p.matrixTransform(matrix); p.x = r.x; p.y = r.y + r.height; var corner4 = p.matrixTransform(matrix); var minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x); var maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x); var minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y); var maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }; V.transformPoint = function(p, matrix) { return V.createSVGPoint(p.x, p.y).matrixTransform(matrix); }; // Convert a style represented as string (e.g. `'fill="blue"; stroke="red"'`) to // an object (`{ fill: 'blue', stroke: 'red' }`). V.styleToObject = function(styleString) { var ret = {}; var styles = styleString.split(';'); for (var i = 0; i < styles.length; i++) { var style = styles[i]; var pair = style.split('='); ret[pair[0].trim()] = pair[1].trim(); } return ret; }; // Inspired by d3.js https://github.com/mbostock/d3/blob/master/src/svg/arc.js V.createSlicePathData = function(innerRadius, outerRadius, startAngle, endAngle) { var svgArcMax = 2 * Math.PI - 1e-6; var r0 = innerRadius; var r1 = outerRadius; var a0 = startAngle; var a1 = endAngle; var da = (a1 < a0 && (da = a0, a0 = a1, a1 = da), a1 - a0); var df = da < Math.PI ? '0' : '1'; var c0 = Math.cos(a0); var s0 = Math.sin(a0); var c1 = Math.cos(a1); var s1 = Math.sin(a1); return (da >= svgArcMax) ? (r0 ? 'M0,' + r1 + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + 'M0,' + r0 + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + (-r0) + 'A' + r0 + ',' + r0 + ' 0 1,0 0,' + r0 + 'Z' : 'M0,' + r1 + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + (-r1) + 'A' + r1 + ',' + r1 + ' 0 1,1 0,' + r1 + 'Z') : (r0 ? 'M' + r1 * c0 + ',' + r1 * s0 + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + 'L' + r0 * c1 + ',' + r0 * s1 + 'A' + r0 + ',' + r0 + ' 0 ' + df + ',0 ' + r0 * c0 + ',' + r0 * s0 + 'Z' : 'M' + r1 * c0 + ',' + r1 * s0 + 'A' + r1 + ',' + r1 + ' 0 ' + df + ',1 ' + r1 * c1 + ',' + r1 * s1 + 'L0,0' + 'Z'); }; // Merge attributes from object `b` with attributes in object `a`. // Note that this modifies the object `a`. // Also important to note that attributes are merged but CSS classes are concatenated. V.mergeAttrs = function(a, b) { for (var attr in b) { if (attr === 'class') { // Concatenate classes. a[attr] = a[attr] ? a[attr] + ' ' + b[attr] : b[attr]; } else if (attr === 'style') { // `style` attribute can be an object. if (V.isObject(a[attr]) && V.isObject(b[attr])) { // `style` stored in `a` is an object. a[attr] = V.mergeAttrs(a[attr], b[attr]); } else if (V.isObject(a[attr])) { // `style` in `a` is an object but it's a string in `b`. // Convert the style represented as a string to an object in `b`. a[attr] = V.mergeAttrs(a[attr], V.styleToObject(b[attr])); } else if (V.isObject(b[attr])) { // `style` in `a` is a string, in `b` it's an object. a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), b[attr]); } else { // Both styles are strings. a[attr] = V.mergeAttrs(V.styleToObject(a[attr]), V.styleToObject(b[attr])); } } else { a[attr] = b[attr]; } } return a; }; V.annotateString = function(t, annotations, opt) { annotations = annotations || []; opt = opt || {}; var offset = opt.offset || 0; var compacted = []; var batch; var ret = []; var item; var prev; for (var i = 0; i < t.length; i++) { item = ret[i] = t[i]; for (var j = 0; j < annotations.length; j++) { var annotation = annotations[j]; var start = annotation.start + offset; var end = annotation.end + offset; if (i >= start && i < end) { // Annotation applies. if (V.isObject(item)) { // There is more than one annotation to be applied => Merge attributes. item.attrs = V.mergeAttrs(V.mergeAttrs({}, item.attrs), annotation.attrs); } else { item = ret[i] = { t: t[i], attrs: annotation.attrs }; } if (opt.includeAnnotationIndices) { (item.annotations || (item.annotations = [])).push(j); } } } prev = ret[i - 1]; if (!prev) { batch = item; } else if (V.isObject(item) && V.isObject(prev)) { // Both previous item and the current one are annotations. If the attributes // didn't change, merge the text. if (JSON.stringify(item.attrs) === JSON.stringify(prev.attrs)) { batch.t += item.t; } else { compacted.push(batch); batch = item; } } else if (V.isObject(item)) { // Previous item was a string, current item is an annotation. compacted.push(batch); batch = item; } else if (V.isObject(prev)) { // Previous item was an annotation, current item is a string. compacted.push(batch); batch = item; } else { // Both previous and current item are strings. batch = (batch || '') + item; } } if (batch) { compacted.push(batch); } return compacted; }; V.findAnnotationsAtIndex = function(annotations, index) { var found = []; if (annotations) { annotations.forEach(function(annotation) { if (annotation.start < index && index <= annotation.end) { found.push(annotation); } }); } return found; }; V.findAnnotationsBetweenIndexes = function(annotations, start, end) { var found = []; if (annotations) { annotations.forEach(function(annotation) { if ((start >= annotation.start && start < annotation.end) || (end > annotation.start && end <= annotation.end) || (annotation.start >= start && annotation.end < end)) { found.push(annotation); } }); } return found; }; // Shift all the text annotations after character `index` by `offset` positions. V.shiftAnnotations = function(annotations, index, offset) { if (annotations) { annotations.forEach(function(annotation) { if (annotation.start < index && annotation.end >= index) { annotation.end += offset; } else if (annotation.start >= index) { annotation.start += offset; annotation.end += offset; } }); } return annotations; }; V.convertLineToPathData = function(line) { line = V(line); var d = [ 'M', line.attr('x1'), line.attr('y1'), 'L', line.attr('x2'), line.attr('y2') ].join(' '); return d; }; V.convertPolygonToPathData = function(polygon) { polygon = V(polygon); var points = V.getPointsFromSvgNode(polygon.node); if (!(points.length > 0)) return null; return V.svgPointsToPath(points); }; V.convertPolylineToPathData = function(polyline) { var points = V.getPointsFromSvgNode(polyline.node); if (!(points.length > 0)) return null; return V.svgPointsToPath(points); }; V.svgPointsToPath = function(points) { var i; for (i = 0; i < points.length; i++) { points[i] = points[i].x + ' ' + points[i].y; } return 'M ' + points.join(' L') + ' Z'; }; V.getPointsFromSvgNode = function(node) { var points = []; var i; for (i = 0; i < node.points.numberOfItems; i++) { points.push(node.points.getItem(i)); } return points; }; V.KAPPA = 0.5522847498307935; V.convertCircleToPathData = function(circle) { circle = V(circle); var cx = parseFloat(circle.attr('cx')) || 0; var cy = parseFloat(circle.attr('cy')) || 0; var r = parseFloat(circle.attr('r')); var cd = r * V.KAPPA; // Control distance. var d = [ 'M', cx, cy - r, // Move to the first point. 'C', cx + cd, cy - r, cx + r, cy - cd, cx + r, cy, // I. Quadrant. 'C', cx + r, cy + cd, cx + cd, cy + r, cx, cy + r, // II. Quadrant. 'C', cx - cd, cy + r, cx - r, cy + cd, cx - r, cy, // III. Quadrant. 'C', cx - r, cy - cd, cx - cd, cy - r, cx, cy - r, // IV. Quadrant. 'Z' ].join(' '); return d; }; V.convertEllipseToPathData = function(ellipse) { ellipse = V(ellipse); var cx = parseFloat(ellipse.attr('cx')) || 0; var cy = parseFloat(ellipse.attr('cy')) || 0; var rx = parseFloat(ellipse.attr('rx')); var ry = parseFloat(ellipse.attr('ry')) || rx; var cdx = rx * V.KAPPA; // Control distance x. var cdy = ry * V.KAPPA; // Control distance y. var d = [ 'M', cx, cy - ry, // Move to the first point. 'C', cx + cdx, cy - ry, cx + rx, cy - cdy, cx + rx, cy, // I. Quadrant. 'C', cx + rx, cy + cdy, cx + cdx, cy + ry, cx, cy + ry, // II. Quadrant. 'C', cx - cdx, cy + ry, cx - rx, cy + cdy, cx - rx, cy, // III. Quadrant. 'C', cx - rx, cy - cdy, cx - cdx, cy - ry, cx, cy - ry, // IV. Quadrant. 'Z' ].join(' '); return d; }; V.convertRectToPathData = function(rect) { rect = V(rect); var x = parseFloat(rect.attr('x')) || 0; var y = parseFloat(rect.attr('y')) || 0; var width = parseFloat(rect.attr('width')) || 0; var height = parseFloat(rect.attr('height')) || 0; var rx = parseFloat(rect.attr('rx')) || 0; var ry = parseFloat(rect.attr('ry')) || 0; var bbox = g.rect(x, y, width, height); var d; if (!rx && !ry) { d = [ 'M', bbox.origin().x, bbox.origin().y, 'H', bbox.corner().x, 'V', bbox.corner().y, 'H', bbox.origin().x, 'V', bbox.origin().y, 'Z' ].join(' '); } else { var r = x + width; var b = y + height; d = [ 'M', x + rx, y, 'L', r - rx, y, 'Q', r, y, r, y + ry, 'L', r, y + height - ry, 'Q', r, b, r - rx, b, 'L', x + rx, b, 'Q', x, b, x, b - rx, 'L', x, y + ry, 'Q', x, y, x + rx, y, 'Z' ].join(' '); } return d; }; // Convert a rectangle to SVG path commands. `r` is an object of the form: // `{ x: [number], y: [number], width: [number], height: [number], top-ry: [number], top-ry: [number], bottom-rx: [number], bottom-ry: [number] }`, // where `x, y, width, height` are the usual rectangle attributes and [top-/bottom-]rx/ry allows for // specifying radius of the rectangle for all its sides (as opposed to the built-in SVG rectangle // that has only `rx` and `ry` attributes). V.rectToPath = function(r) { var topRx = r.rx || r['top-rx'] || 0; var bottomRx = r.rx || r['bottom-rx'] || 0; var topRy = r.ry || r['top-ry'] || 0; var bottomRy = r.ry || r['bottom-ry'] || 0; return [ 'M', r.x, r.y + topRy, 'v', r.height - topRy - bottomRy, 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, bottomRy, 'h', r.width - 2 * bottomRx, 'a', bottomRx, bottomRy, 0, 0, 0, bottomRx, -bottomRy, 'v', -(r.height - bottomRy - topRy), 'a', topRx, topRy, 0, 0, 0, -topRx, -topRy, 'h', -(r.width - 2 * topRx), 'a', topRx, topRy, 0, 0, 0, -topRx, topRy ].join(' '); }; return V; })(); // JointJS library. // (c) 2011-2015 client IO // Global namespace. var joint = { version: '0.9.7', // `joint.dia` namespace. dia: {}, // `joint.ui` namespace. ui: {}, // `joint.layout` namespace. layout: {}, // `joint.shapes` namespace. shapes: {}, // `joint.format` namespace. format: {}, // `joint.connectors` namespace. connectors: {}, // `joint.highlighters` namespace. highlighters: {}, // `joint.routers` namespace. routers: {}, // `joint.mvc` namespace. mvc: { views: {} }, setTheme: function(theme, opt) { opt = opt || {}; _.invoke(joint.mvc.views, 'setTheme', theme, opt); // Update the default theme on the view prototype. joint.mvc.View.prototype.options.theme = theme; }, // `joint.env` namespace. env: { _results: {}, _tests: { svgforeignobject: function() { return !!document.createElementNS && /SVGForeignObject/.test(({}).toString.call(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'))); } }, addTest: function(name, fn) { return joint.env._tests[name] = fn; }, test: function(name) { var fn = joint.env._tests[name]; if (!fn) { throw new Error('Test not defined ("' + name + '"). Use `joint.env.addTest(name, fn) to add a new test.`'); } var result = joint.env._results[name]; if (typeof result !== 'undefined') { return result; } try { result = fn(); } catch (error) { result = false; } // Cache the test result. joint.env._results[name] = result; return result; } }, util: { // Return a simple hash code from a string. See http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/. hashCode: function(str) { var hash = 0; if (str.length == 0) return hash; for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i); hash = ((hash << 5) - hash) + c; hash = hash & hash; // Convert to 32bit integer } return hash; }, getByPath: function(obj, path, delim) { delim = delim || '/'; var keys = path.split(delim); var key; while (keys.length) { key = keys.shift(); if (Object(obj) === obj && key in obj) { obj = obj[key]; } else { return undefined; } } return obj; }, setByPath: function(obj, path, value, delim) { delim = delim || '/'; var keys = path.split(delim); var diver = obj; var i = 0; if (path.indexOf(delim) > -1) { for (var len = keys.length; i < len - 1; i++) { // diver creates an empty object if there is no nested object under such a key. // This means that one can populate an empty nested object with setByPath(). diver = diver[keys[i]] || (diver[keys[i]] = {}); } diver[keys[len - 1]] = value; } else { obj[path] = value; } return obj; }, unsetByPath: function(obj, path, delim) { delim = delim || '/'; // index of the last delimiter var i = path.lastIndexOf(delim); if (i > -1) { // unsetting a nested attribute var parent = joint.util.getByPath(obj, path.substr(0, i), delim); if (parent) { delete parent[path.slice(i + 1)]; } } else { // unsetting a primitive attribute delete obj[path]; } return obj; }, flattenObject: function(obj, delim, stop) { delim = delim || '/'; var ret = {}; for (var key in obj) { if (!obj.hasOwnProperty(key)) continue; var shouldGoDeeper = typeof obj[key] === 'object'; if (shouldGoDeeper && stop && stop(obj[key])) { shouldGoDeeper = false; } if (shouldGoDeeper) { var flatObject = this.flattenObject(obj[key], delim, stop); for (var flatKey in flatObject) { if (!flatObject.hasOwnProperty(flatKey)) continue; ret[key + delim + flatKey] = flatObject[flatKey]; } } else { ret[key] = obj[key]; } } return ret; }, uuid: function() { // credit: http://stackoverflow.com/posts/2117523/revisions return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16|0; var v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); }, // Generate global unique id for obj and store it as a property of the object. guid: function(obj) { this.guid.id = this.guid.id || 1; obj.id = (obj.id === undefined ? 'j_' + this.guid.id++ : obj.id); return obj.id; }, // Copy all the properties to the first argument from the following arguments. // All the properties will be overwritten by the properties from the following // arguments. Inherited properties are ignored. mixin: function() { var target = arguments[0]; for (var i = 1, l = arguments.length; i < l; i++) { var extension = arguments[i]; // Only functions and objects can be mixined. if ((Object(extension) !== extension) && !_.isFunction(extension) && (extension === null || extension === undefined)) { continue; } _.each(extension, function(copy, key) { if (this.mixin.deep && (Object(copy) === copy)) { if (!target[key]) { target[key] = _.isArray(copy) ? [] : {}; } this.mixin(target[key], copy); return; } if (target[key] !== copy) { if (!this.mixin.supplement || !target.hasOwnProperty(key)) { target[key] = copy; } } }, this); } return target; }, // Copy all properties to the first argument from the following // arguments only in case if they don't exists in the first argument. // All the function propererties in the first argument will get // additional property base pointing to the extenders same named // property function's call method. supplement: function() { this.mixin.supplement = true; var ret = this.mixin.apply(this, arguments); this.mixin.supplement = false; return ret; }, // Same as `mixin()` but deep version. deepMixin: function() { this.mixin.deep = true; var ret = this.mixin.apply(this, arguments); this.mixin.deep = false; return ret; }, // Same as `supplement()` but deep version. deepSupplement: function() { this.mixin.deep = this.mixin.supplement = true; var ret = this.mixin.apply(this, arguments); this.mixin.deep = this.mixin.supplement = false; return ret; }, normalizeEvent: function(evt) { var touchEvt = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0]; if (touchEvt) { for (var property in evt) { // copy all the properties from the input event that are not // defined on the touch event (functions included). if (touchEvt[property] === undefined) { touchEvt[property] = evt[property]; } } return touchEvt; } return evt; }, nextFrame:(function() { var raf; if (typeof window !== 'undefined') { raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame; } if (!raf) { var lastTime = 0; raf = function(callback) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } return function(callback, context) { return context ? raf(_.bind(callback, context)) : raf(callback); }; })(), cancelFrame: (function() { var caf; var client = typeof window != 'undefined'; if (client) { caf = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.msCancelAnimationFrame || window.msCancelRequestAnimationFrame || window.oCancelAnimationFrame || window.oCancelRequestAnimationFrame || window.mozCancelAnimationFrame || window.mozCancelRequestAnimationFrame; } caf = caf || clearTimeout; return client ? _.bind(caf, window) : caf; })(), shapePerimeterConnectionPoint: function(linkView, view, magnet, reference) { var bbox; var spot; if (!magnet) { // There is no magnet, try to make the best guess what is the // wrapping SVG element. This is because we want this "smart" // connection points to work out of the box without the // programmer to put magnet marks to any of the subelements. // For example, we want the functoin to work on basic.Path elements // without any special treatment of such elements. // The code below guesses the wrapping element based on // one simple assumption. The wrapping elemnet is the // first child of the scalable group if such a group exists // or the first child of the rotatable group if not. // This makese sense because usually the wrapping element // is below any other sub element in the shapes. var scalable = view.$('.scalable')[0]; var rotatable = view.$('.rotatable')[0]; if (scalable && scalable.firstChild) { magnet = scalable.firstChild; } else if (rotatable && rotatable.firstChild) { magnet = rotatable.firstChild; } } if (magnet) { spot = V(magnet).findIntersection(reference, linkView.paper.viewport); if (!spot) { bbox = g.rect(V(magnet).bbox(false, linkView.paper.viewport)); } } else { bbox = view.model.getBBox(); spot = bbox.intersectionWithLineFromCenterToPoint(reference); } return spot || bbox.center(); }, breakText: function(text, size, styles, opt) { opt = opt || {}; var width = size.width; var height = size.height; var svgDocument = opt.svgDocument || V('svg').node; var textElement = V('').attr(styles || {}).node; var textSpan = textElement.firstChild; var textNode = document.createTextNode(''); // Prevent flickering textElement.style.opacity = 0; // Prevent FF from throwing an uncaught exception when `getBBox()` // called on element that is not in the render tree (is not measurable). // .getComputedTextLength() returns always 0 in this case. // Note that the `textElement` resp. `textSpan` can become hidden // when it's appended to the DOM and a `display: none` CSS stylesheet // rule gets applied. textElement.style.display = 'block'; textSpan.style.display = 'block'; textSpan.appendChild(textNode); svgDocument.appendChild(textElement); if (!opt.svgDocument) { document.body.appendChild(svgDocument); } var words = text.split(' '); var full = []; var lines = []; var p; for (var i = 0, l = 0, len = words.length; i < len; i++) { var word = words[i]; textNode.data = lines[l] ? lines[l] + ' ' + word : word; if (textSpan.getComputedTextLength() <= width) { // the current line fits lines[l] = textNode.data; if (p) { // We were partitioning. Put rest of the word onto next line full[l++] = true; // cancel partitioning p = 0; } } else { if (!lines[l] || p) { var partition = !!p; p = word.length - 1; if (partition || !p) { // word has only one character. if (!p) { if (!lines[l]) { // we won't fit this text within our rect lines = []; break; } // partitioning didn't help on the non-empty line // try again, but this time start with a new line // cancel partitions created words.splice(i, 2, word + words[i + 1]); // adjust word length len--; full[l++] = true; i--; continue; } // move last letter to the beginning of the next word words[i] = word.substring(0, p); words[i + 1] = word.substring(p) + words[i + 1]; } else { // We initiate partitioning // split the long word into two words words.splice(i, 1, word.substring(0, p), word.substring(p)); // adjust words length len++; if (l && !full[l - 1]) { // if the previous line is not full, try to fit max part of // the current word there l--; } } i--; continue; } l++; i--; } // if size.height is defined we have to check whether the height of the entire // text exceeds the rect height if (typeof height !== 'undefined') { // get line height as text height / 0.8 (as text height is approx. 0.8em // and line height is 1em. See vectorizer.text()) var lh = lh || textElement.getBBox().height * 1.25; if (lh * lines.length > height) { // remove overflowing lines lines.splice(Math.floor(height / lh)); break; } } } if (opt.svgDocument) { // svg document was provided, remove the text element only svgDocument.removeChild(textElement); } else { // clean svg document document.body.removeChild(svgDocument); } return lines.join('\n'); }, imageToDataUri: function(url, callback) { if (!url || url.substr(0, 'data:'.length) === 'data:') { // No need to convert to data uri if it is already in data uri. // This not only convenient but desired. For example, // IE throws a security error if data:image/svg+xml is used to render // an image to the canvas and an attempt is made to read out data uri. // Now if our image is already in data uri, there is no need to render it to the canvas // and so we can bypass this error. // Keep the async nature of the function. return setTimeout(function() { callback(null, url); }, 0); } var canvas = document.createElement('canvas'); var img = document.createElement('img'); img.onload = function() { var ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); try { // Guess the type of the image from the url suffix. var suffix = (url.split('.').pop()) || 'png'; // A little correction for JPEGs. There is no image/jpg mime type but image/jpeg. var type = 'image/' + (suffix === 'jpg') ? 'jpeg' : suffix; var dataUri = canvas.toDataURL(type); } catch (e) { if (/\.svg$/.test(url)) { // IE throws a security error if we try to render an SVG into the canvas. // Luckily for us, we don't need canvas at all to convert // SVG to data uri. We can just use AJAX to load the SVG string // and construct the data uri ourselves. var xhr = window.XMLHttpRequest ? new XMLHttpRequest : new ActiveXObject('Microsoft.XMLHTTP'); xhr.open('GET', url, false); xhr.send(null); var svg = xhr.responseText; return callback(null, 'data:image/svg+xml,' + encodeURIComponent(svg)); } console.error(img.src, 'fails to convert', e); } callback(null, dataUri); }; img.ononerror = function() { callback(new Error('Failed to load image.')); }; img.src = url; }, getElementBBox: function(el) { var $el = $(el); var offset = $el.offset(); var bbox; if (el.ownerSVGElement) { // Use Vectorizer to get the dimensions of the element if it is an SVG element. bbox = V(el).bbox(); // getBoundingClientRect() used in jQuery.fn.offset() takes into account `stroke-width` // in Firefox only. So clientRect width/height and getBBox width/height in FF don't match. // To unify this across all browsers we add the `stroke-width` (left & top) back to // the calculated offset. var crect = el.getBoundingClientRect(); var strokeWidthX = (crect.width - bbox.width) / 2; var strokeWidthY = (crect.height - bbox.height) / 2; // The `bbox()` returns coordinates relative to the SVG viewport, therefore, use the // ones returned from the `offset()` method that are relative to the document. bbox.x = offset.left + strokeWidthX; bbox.y = offset.top + strokeWidthY; } else { bbox = { x: offset.left, y: offset.top, width: $el.outerWidth(), height: $el.outerHeight() }; } return bbox; }, // Highly inspired by the jquery.sortElements plugin by Padolsey. // See http://james.padolsey.com/javascript/sorting-elements-with-jquery/. sortElements: function(elements, comparator) { var $elements = $(elements); var placements = $elements.map(function() { var sortElement = this; var parentNode = sortElement.parentNode; // Since the element itself will change position, we have // to have some way of storing it's original position in // the DOM. The easiest way is to have a 'flag' node: var nextSibling = parentNode.insertBefore(document.createTextNode(''), sortElement.nextSibling); return function() { if (parentNode === this) { throw new Error('You can\'t sort elements if any one is a descendant of another.'); } // Insert before flag: parentNode.insertBefore(this, nextSibling); // Remove flag: parentNode.removeChild(nextSibling); }; }); return Array.prototype.sort.call($elements, comparator).each(function(i) { placements[i].call(this); }); }, // Sets attributes on the given element and its descendants based on the selector. // `attrs` object: { [SELECTOR1]: { attrs1 }, [SELECTOR2]: { attrs2}, ... } e.g. { 'input': { color : 'red' }} setAttributesBySelector: function(element, attrs) { var $element = $(element); _.each(attrs, function(attrs, selector) { var $elements = $element.find(selector).addBack().filter(selector); // Make a special case for setting classes. // We do not want to overwrite any existing class. if (_.has(attrs, 'class')) { $elements.addClass(attrs['class']); attrs = _.omit(attrs, 'class'); } $elements.attr(attrs); }); }, // Return a new object with all for sides (top, bottom, left and right) in it. // Value of each side is taken from the given argument (either number or object). // Default value for a side is 0. // Examples: // joint.util.normalizeSides(5) --> { top: 5, left: 5, right: 5, bottom: 5 } // joint.util.normalizeSides({ left: 5 }) --> { top: 0, left: 5, right: 0, bottom: 0 } normalizeSides: function(box) { if (Object(box) !== box) { box = box || 0; return { top: box, bottom: box, left: box, right: box }; } return { top: box.top || 0, bottom: box.bottom || 0, left: box.left || 0, right: box.right || 0 }; }, timing: { linear: function(t) { return t; }, quad: function(t) { return t * t; }, cubic: function(t) { return t * t * t; }, inout: function(t) { if (t <= 0) return 0; if (t >= 1) return 1; var t2 = t * t; var t3 = t2 * t; return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); }, exponential: function(t) { return Math.pow(2, 10 * (t - 1)); }, bounce: function(t) { for (var a = 0, b = 1; 1; a += b, b /= 2) { if (t >= (7 - 4 * a) / 11) { var q = (11 - 6 * a - 11 * t) / 4; return -q * q + b * b; } } }, reverse: function(f) { return function(t) { return 1 - f(1 - t); }; }, reflect: function(f) { return function(t) { return .5 * (t < .5 ? f(2 * t) : (2 - f(2 - 2 * t))); }; }, clamp: function(f, n, x) { n = n || 0; x = x || 1; return function(t) { var r = f(t); return r < n ? n : r > x ? x : r; }; }, back: function(s) { if (!s) s = 1.70158; return function(t) { return t * t * ((s + 1) * t - s); }; }, elastic: function(x) { if (!x) x = 1.5; return function(t) { return Math.pow(2, 10 * (t - 1)) * Math.cos(20 * Math.PI * x / 3 * t); }; } }, interpolate: { number: function(a, b) { var d = b - a; return function(t) { return a + d * t; }; }, object: function(a, b) { var s = _.keys(a); return function(t) { var i, p; var r = {}; for (i = s.length - 1; i != -1; i--) { p = s[i]; r[p] = a[p] + (b[p] - a[p]) * t; } return r; }; }, hexColor: function(a, b) { var ca = parseInt(a.slice(1), 16); var cb = parseInt(b.slice(1), 16); var ra = ca & 0x0000ff; var rd = (cb & 0x0000ff) - ra; var ga = ca & 0x00ff00; var gd = (cb & 0x00ff00) - ga; var ba = ca & 0xff0000; var bd = (cb & 0xff0000) - ba; return function(t) { var r = (ra + rd * t) & 0x000000ff; var g = (ga + gd * t) & 0x0000ff00; var b = (ba + bd * t) & 0x00ff0000; return '#' + (1 << 24 | r | g | b ).toString(16).slice(1); }; }, unit: function(a, b) { var r = /(-?[0-9]*.[0-9]*)(px|em|cm|mm|in|pt|pc|%)/; var ma = r.exec(a); var mb = r.exec(b); var p = mb[1].indexOf('.'); var f = p > 0 ? mb[1].length - p - 1 : 0; a = +ma[1]; var d = +mb[1] - a; var u = ma[2]; return function(t) { return (a + d * t).toFixed(f) + u; }; } }, // SVG filters. filter: { // `color` ... outline color // `width`... outline width // `opacity` ... outline opacity // `margin` ... gap between outline and the element outline: function(args) { var tpl = ''; var margin = _.isFinite(args.margin) ? args.margin : 2; var width = _.isFinite(args.width) ? args.width : 1; return joint.util.template(tpl)({ color: args.color || 'blue', opacity: _.isFinite(args.opacity) ? args.opacity : 1, outerRadius: margin + width, innerRadius: margin }); }, // `color` ... color // `width`... width // `blur` ... blur // `opacity` ... opacity highlight: function(args) { var tpl = ''; return joint.util.template(tpl)({ color: args.color || 'red', width: _.isFinite(args.width) ? args.width : 1, blur: _.isFinite(args.blur) ? args.blur : 0, opacity: _.isFinite(args.opacity) ? args.opacity : 1 }); }, // `x` ... horizontal blur // `y` ... vertical blur (optional) blur: function(args) { var x = _.isFinite(args.x) ? args.x : 2; return joint.util.template('')({ stdDeviation: _.isFinite(args.y) ? [x, args.y] : x }); }, // `dx` ... horizontal shift // `dy` ... vertical shift // `blur` ... blur // `color` ... color // `opacity` ... opacity dropShadow: function(args) { var tpl = 'SVGFEDropShadowElement' in window ? '' : ''; return joint.util.template(tpl)({ dx: args.dx || 0, dy: args.dy || 0, opacity: _.isFinite(args.opacity) ? args.opacity : 1, color: args.color || 'black', blur: _.isFinite(args.blur) ? args.blur : 4 }); }, // `amount` ... the proportion of the conversion. A value of 1 is completely grayscale. A value of 0 leaves the input unchanged. grayscale: function(args) { var amount = _.isFinite(args.amount) ? args.amount : 1; return joint.util.template('')({ a: 0.2126 + 0.7874 * (1 - amount), b: 0.7152 - 0.7152 * (1 - amount), c: 0.0722 - 0.0722 * (1 - amount), d: 0.2126 - 0.2126 * (1 - amount), e: 0.7152 + 0.2848 * (1 - amount), f: 0.0722 - 0.0722 * (1 - amount), g: 0.2126 - 0.2126 * (1 - amount), h: 0.0722 + 0.9278 * (1 - amount) }); }, // `amount` ... the proportion of the conversion. A value of 1 is completely sepia. A value of 0 leaves the input unchanged. sepia: function(args) { var amount = _.isFinite(args.amount) ? args.amount : 1; return joint.util.template('')({ a: 0.393 + 0.607 * (1 - amount), b: 0.769 - 0.769 * (1 - amount), c: 0.189 - 0.189 * (1 - amount), d: 0.349 - 0.349 * (1 - amount), e: 0.686 + 0.314 * (1 - amount), f: 0.168 - 0.168 * (1 - amount), g: 0.272 - 0.272 * (1 - amount), h: 0.534 - 0.534 * (1 - amount), i: 0.131 + 0.869 * (1 - amount) }); }, // `amount` ... the proportion of the conversion. A value of 0 is completely un-saturated. A value of 1 leaves the input unchanged. saturate: function(args) { var amount = _.isFinite(args.amount) ? args.amount : 1; return joint.util.template('')({ amount: 1 - amount }); }, // `angle` ... the number of degrees around the color circle the input samples will be adjusted. hueRotate: function(args) { return joint.util.template('')({ angle: args.angle || 0 }); }, // `amount` ... the proportion of the conversion. A value of 1 is completely inverted. A value of 0 leaves the input unchanged. invert: function(args) { var amount = _.isFinite(args.amount) ? args.amount : 1; return joint.util.template('')({ amount: amount, amount2: 1 - amount }); }, // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. brightness: function(args) { return joint.util.template('')({ amount: _.isFinite(args.amount) ? args.amount : 1 }); }, // `amount` ... proportion of the conversion. A value of 0 will create an image that is completely black. A value of 1 leaves the input unchanged. contrast: function(args) { var amount = _.isFinite(args.amount) ? args.amount : 1; return joint.util.template('')({ amount: amount, amount2: .5 - amount / 2 }); } }, format: { // Formatting numbers via the Python Format Specification Mini-language. // See http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. // Heavilly inspired by the D3.js library implementation. number: function(specifier, value, locale) { locale = locale || { currency: ['$', ''], decimal: '.', thousands: ',', grouping: [3] }; // See Python format specification mini-language: http://docs.python.org/release/3.1.3/library/string.html#format-specification-mini-language. // [[fill]align][sign][symbol][0][width][,][.precision][type] var re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; var match = re.exec(specifier); var fill = match[1] || ' '; var align = match[2] || '>'; var sign = match[3] || ''; var symbol = match[4] || ''; var zfill = match[5]; var width = +match[6]; var comma = match[7]; var precision = match[8]; var type = match[9]; var scale = 1; var prefix = ''; var suffix = ''; var integer = false; if (precision) precision = +precision.substring(1); if (zfill || fill === '0' && align === '=') { zfill = fill = '0'; align = '='; if (comma) width -= Math.floor((width - 1) / 4); } switch (type) { case 'n': comma = true; type = 'g'; break; case '%': scale = 100; suffix = '%'; type = 'f'; break; case 'p': scale = 100; suffix = '%'; type = 'r'; break; case 'b': case 'o': case 'x': case 'X': if (symbol === '#') prefix = '0' + type.toLowerCase(); case 'c': case 'd': integer = true; precision = 0; break; case 's': scale = -1; type = 'r'; break; } if (symbol === '$') { prefix = locale.currency[0]; suffix = locale.currency[1]; } // If no precision is specified for `'r'`, fallback to general notation. if (type == 'r' && !precision) type = 'g'; // Ensure that the requested precision is in the supported range. if (precision != null) { if (type == 'g') precision = Math.max(1, Math.min(21, precision)); else if (type == 'e' || type == 'f') precision = Math.max(0, Math.min(20, precision)); } var zcomma = zfill && comma; // Return the empty string for floats formatted as ints. if (integer && (value % 1)) return ''; // Convert negative to positive, and record the sign prefix. var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, '-') : sign; var fullSuffix = suffix; // Apply the scale, computing it from the value's exponent for si format. // Preserve the existing suffix, if any, such as the currency symbol. if (scale < 0) { var unit = this.prefix(value, precision); value = unit.scale(value); fullSuffix = unit.symbol + suffix; } else { value *= scale; } // Convert to the desired precision. value = this.convert(type, value, precision); // Break the value into the integer part (before) and decimal part (after). var i = value.lastIndexOf('.'); var before = i < 0 ? value : value.substring(0, i); var after = i < 0 ? '' : locale.decimal + value.substring(i + 1); function formatGroup(value) { var i = value.length; var t = []; var j = 0; var g = locale.grouping[0]; while (i > 0 && g > 0) { t.push(value.substring(i -= g, i + g)); g = locale.grouping[j = (j + 1) % locale.grouping.length]; } return t.reverse().join(locale.thousands); } // If the fill character is not `'0'`, grouping is applied before padding. if (!zfill && comma && locale.grouping) { before = formatGroup(before); } var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length); var padding = length < width ? new Array(length = width - length + 1).join(fill) : ''; // If the fill character is `'0'`, grouping is applied after padding. if (zcomma) before = formatGroup(padding + before); // Apply prefix. negative += prefix; // Rejoin integer and decimal parts. value = before + after; return (align === '<' ? negative + value + padding : align === '>' ? padding + negative + value : align === '^' ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix; }, // Formatting string via the Python Format string. // See https://docs.python.org/2/library/string.html#format-string-syntax) string: function(formatString, value) { var fieldDelimiterIndex; var fieldDelimiter = '{'; var endPlaceholder = false; var formattedStringArray = []; while ((fieldDelimiterIndex = formatString.indexOf(fieldDelimiter)) !== -1) { var pieceFormatedString, formatSpec, fieldName; pieceFormatedString = formatString.slice(0, fieldDelimiterIndex); if (endPlaceholder) { formatSpec = pieceFormatedString.split(':'); fieldName = formatSpec.shift().split('.'); pieceFormatedString = value; for (var i = 0; i < fieldName.length; i++) pieceFormatedString = pieceFormatedString[fieldName[i]]; if (formatSpec.length) pieceFormatedString = this.number(formatSpec, pieceFormatedString); } formattedStringArray.push(pieceFormatedString); formatString = formatString.slice(fieldDelimiterIndex + 1); fieldDelimiter = (endPlaceholder = !endPlaceholder) ? '}' : '{'; } formattedStringArray.push(formatString); return formattedStringArray.join(''); }, convert: function(type, value, precision) { switch (type) { case 'b': return value.toString(2); case 'c': return String.fromCharCode(value); case 'o': return value.toString(8); case 'x': return value.toString(16); case 'X': return value.toString(16).toUpperCase(); case 'g': return value.toPrecision(precision); case 'e': return value.toExponential(precision); case 'f': return value.toFixed(precision); case 'r': return (value = this.round(value, this.precision(value, precision))).toFixed(Math.max(0, Math.min(20, this.precision(value * (1 + 1e-15), precision)))); default: return value + ''; } }, round: function(value, precision) { return precision ? Math.round(value * (precision = Math.pow(10, precision))) / precision : Math.round(value); }, precision: function(value, precision) { return precision - (value ? Math.ceil(Math.log(value) / Math.LN10) : 1); }, prefix: function(value, precision) { var prefixes = _.map(['y', 'z', 'a', 'f', 'p', 'n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'], function(d, i) { var k = Math.pow(10, Math.abs(8 - i) * 3); return { scale: i > 8 ? function(d) { return d / k; } : function(d) { return d * k; }, symbol: d }; }); var i = 0; if (value) { if (value < 0) value *= -1; if (precision) value = this.round(value, this.precision(value, precision)); i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); i = Math.max(-24, Math.min(24, Math.floor((i <= 0 ? i + 1 : i - 1) / 3) * 3)); } return prefixes[8 + i / 3]; } }, /* Pre-compile the HTML to be used as a template. */ template: function(html) { /* Must support the variation in templating syntax found here: https://lodash.com/docs#template */ var regex = /<%= ([^ ]+) %>|\$\{ ?([^\{\} ]+) ?\}|\{\{([^\{\} ]+)\}\}/g; return function(data) { return html.replace(regex, function(match) { var args = Array.prototype.slice.call(arguments); var attr = _.find(args.slice(1, 4), function(_attr) { return !!_attr; }); var attrArray = attr.split('.'); var value = data[attrArray.shift()]; while (!_.isUndefined(value) && attrArray.length) { value = value[attrArray.shift()]; } return !_.isUndefined(value) ? value : ''; }); }; }, /** * @param {Element=} el Element, which content is intent to display in full-screen mode, 'document.body' is default. */ toggleFullScreen: function(el) { el = el || document.body; function prefixedResult(el, prop) { var prefixes = ['webkit', 'moz', 'ms', 'o', '']; for (var i = 0; i < prefixes.length; i++) { var prefix = prefixes[i]; var propName = prefix ? (prefix + prop) : (prop.substr(0, 1).toLowerCase() + prop.substr(1)); if (!_.isUndefined(el[propName])) { return _.isFunction(el[propName]) ? el[propName]() : el[propName]; } } } if (prefixedResult(document, 'FullScreen') || prefixedResult(document, 'IsFullScreen')) { prefixedResult(document, 'CancelFullScreen'); } else { prefixedResult(el, 'RequestFullScreen'); } }, wrapWith: function(object, methods, wrapper) { if (_.isString(wrapper)) { if (!joint.util.wrappers[wrapper]) { throw new Error('Unknown wrapper: "' + wrapper + '"'); } wrapper = joint.util.wrappers[wrapper]; } if (!_.isFunction(wrapper)) { throw new Error('Wrapper must be a function.'); } _.each(methods, function(method) { object[method] = wrapper(object[method]); }); }, wrappers: { /* Prepares a function with the following usage: fn([cell, cell, cell], opt); fn([cell, cell, cell]); fn(cell, cell, cell, opt); fn(cell, cell, cell); */ cells: function(fn) { return function() { var args = Array.prototype.slice.call(arguments); var cells = args.length > 0 && _.first(args) || []; var opt = args.length > 1 && _.last(args) || {}; if (!_.isArray(cells)) { if (opt instanceof joint.dia.Cell) { cells = args; opt = {}; } else { cells = _.initial(args); } } return fn.call(this, cells, opt); }; } } } }; // JointJS library. // (c) 2011-2015 client IO joint.mvc.View = Backbone.View.extend({ options: { theme: 'default' }, theme: null, themeClassNamePrefix: 'joint-theme-', requireSetThemeOverride: false, constructor: function(options) { Backbone.View.call(this, options); }, initialize: function(options) { this.requireSetThemeOverride = options && !!options.theme; this.options = _.extend({}, joint.mvc.View.prototype.options || {}, this.options || {}, options || {}); _.bindAll(this, 'setTheme', 'onSetTheme', 'remove', 'onRemove'); joint.mvc.views[this.cid] = this; this.setTheme(this.options.theme); this._ensureElClassName(); this.init(); }, _ensureElClassName: function() { var className = _.result(this, 'className'); this.$el.addClass(className); }, init: function() { // Intentionally empty. // This method is meant to be overriden. }, setTheme: function(theme, opt) { opt = opt || {}; // Theme is already set, override is required, and override has not been set. // Don't set the theme. if (this.theme && this.requireSetThemeOverride && !opt.override) return; this.onSetTheme(this.theme/* oldTheme */, theme/* newTheme */); if (this.theme) { this.$el.removeClass(this.themeClassNamePrefix + this.theme); } this.$el.addClass(this.themeClassNamePrefix + theme); this.theme = theme; return this; }, onSetTheme: function(oldTheme, newTheme) { // Intentionally empty. // This method is meant to be overriden. }, remove: function() { this.onRemove(); joint.mvc.views[this.cid] = null; Backbone.View.prototype.remove.apply(this, arguments); return this; }, onRemove: function() { // Intentionally empty. // This method is meant to be overriden. } }); // JointJS, the JavaScript diagramming library. // (c) 2011-2015 client IO joint.dia.GraphCells = Backbone.Collection.extend({ cellNamespace: joint.shapes, initialize: function(models, opt) { // Set the optional namespace where all model classes are defined. if (opt.cellNamespace) { this.cellNamespace = opt.cellNamespace; } this.graph = opt.graph; }, model: function(attrs, options) { var collection = options.collection; var namespace = collection.cellNamespace; // Find the model class in the namespace or use the default one. var ModelClass = (attrs.type === 'link') ? joint.dia.Link : joint.util.getByPath(namespace, attrs.type, '.') || joint.dia.Element; var cell = new ModelClass(attrs, options); // Add a reference to the graph. It is necessary to do this here because this is the earliest place // where a new model is created from a plain JS object. For other objects, see `joint.dia.Graph>>_prepareCell()`. cell.graph = collection.graph; return cell; }, // `comparator` makes it easy to sort cells based on their `z` index. comparator: function(model) { return model.get('z') || 0; } }); joint.dia.Graph = Backbone.Model.extend({ _batches: {}, initialize: function(attrs, opt) { opt = opt || {}; // Passing `cellModel` function in the options object to graph allows for // setting models based on attribute objects. This is especially handy // when processing JSON graphs that are in a different than JointJS format. var cells = new joint.dia.GraphCells([], { model: opt.cellModel, cellNamespace: opt.cellNamespace, graph: this }); Backbone.Model.prototype.set.call(this, 'cells', cells); // Make all the events fired in the `cells` collection available. // to the outside world. cells.on('all', this.trigger, this); // Backbone automatically doesn't trigger re-sort if models attributes are changed later when // they're already in the collection. Therefore, we're triggering sort manually here. this.on('change:z', this._sortOnChangeZ, this); this.on('batch:stop', this._onBatchStop, this); // `joint.dia.Graph` keeps an internal data structure (an adjacency list) // for fast graph queries. All changes that affect the structure of the graph // must be reflected in the `al` object. This object provides fast answers to // questions such as "what are the neighbours of this node" or "what // are the sibling links of this link". // Outgoing edges per node. Note that we use a hash-table for the list // of outgoing edges for a faster lookup. // [node ID] -> Object [edge] -> true this._out = {}; // Ingoing edges per node. // [node ID] -> Object [edge] -> true this._in = {}; // `_nodes` is useful for quick lookup of all the elements in the graph, without // having to go through the whole cells array. // [node ID] -> true this._nodes = {}; // `_edges` is useful for quick lookup of all the links in the graph, without // having to go through the whole cells array. // [edge ID] -> true this._edges = {}; cells.on('add', this._restructureOnAdd, this); cells.on('remove', this._restructureOnRemove, this); cells.on('reset', this._restructureOnReset, this); cells.on('change:source', this._restructureOnChangeSource, this); cells.on('change:target', this._restructureOnChangeTarget, this); cells.on('remove', this._removeCell, this); }, _sortOnChangeZ: function() { if (!this.hasActiveBatch('to-front') && !this.hasActiveBatch('to-back')) { this.get('cells').sort(); } }, _onBatchStop: function(data) { var batchName = data && data.batchName; if ((batchName === 'to-front' || batchName === 'to-back') && !this.hasActiveBatch(batchName)) { this.get('cells').sort(); } }, _restructureOnAdd: function(cell) { if (cell.isLink()) { this._edges[cell.id] = true; var source = cell.get('source'); var target = cell.get('target'); if (source.id) { (this._out[source.id] || (this._out[source.id] = {}))[cell.id] = true; } if (target.id) { (this._in[target.id] || (this._in[target.id] = {}))[cell.id] = true; } } else { this._nodes[cell.id] = true; } }, _restructureOnRemove: function(cell) { if (cell.isLink()) { delete this._edges[cell.id]; var source = cell.get('source'); var target = cell.get('target'); if (source.id && this._out[source.id] && this._out[source.id][cell.id]) { delete this._out[source.id][cell.id]; } if (target.id && this._in[target.id] && this._in[target.id][cell.id]) { delete this._in[target.id][cell.id]; } } else { delete this._nodes[cell.id]; } }, _restructureOnReset: function(cells) { // Normalize into an array of cells. The original `cells` is GraphCells Backbone collection. cells = cells.models; this._out = {}; this._in = {}; this._nodes = {}; this._edges = {}; _.each(cells, this._restructureOnAdd, this); }, _restructureOnChangeSource: function(link) { var prevSource = link.previous('source'); if (prevSource.id && this._out[prevSource.id]) { delete this._out[prevSource.id][link.id]; } var source = link.get('source'); if (source.id) { (this._out[source.id] || (this._out[source.id] = {}))[link.id] = true; } }, _restructureOnChangeTarget: function(link) { var prevTarget = link.previous('target'); if (prevTarget.id && this._in[prevTarget.id]) { delete this._in[prevTarget.id][link.id]; } var target = link.get('target'); if (target.id) { (this._in[target.id] || (this._in[target.id] = {}))[link.id] = true; } }, // Return all outbound edges for the node. Return value is an object // of the form: [edge] -> true getOutboundEdges: function(node) { return (this._out && this._out[node]) || {}; }, // Return all inbound edges for the node. Return value is an object // of the form: [edge] -> true getInboundEdges: function(node) { return (this._in && this._in[node]) || {}; }, toJSON: function() { // Backbone does not recursively call `toJSON()` on attributes that are themselves models/collections. // It just clones the attributes. Therefore, we must call `toJSON()` on the cells collection explicitely. var json = Backbone.Model.prototype.toJSON.apply(this, arguments); json.cells = this.get('cells').toJSON(); return json; }, fromJSON: function(json, opt) { if (!json.cells) { throw new Error('Graph JSON must contain cells array.'); } return this.set(json, opt); }, set: function(key, val, opt) { var attrs; // Handle both `key`, value and {key: value} style arguments. if (typeof key === 'object') { attrs = key; opt = val; } else { (attrs = {})[key] = val; } // Make sure that `cells` attribute is handled separately via resetCells(). if (attrs.hasOwnProperty('cells')) { this.resetCells(attrs.cells, opt); attrs = _.omit(attrs, 'cells'); } // The rest of the attributes are applied via original set method. return Backbone.Model.prototype.set.call(this, attrs, opt); }, clear: function(opt) { opt = _.extend({}, opt, { clear: true }); var collection = this.get('cells'); if (collection.length === 0) return this; this.startBatch('clear', opt); // The elements come after the links. var cells = collection.sortBy(function(cell) { return cell.isLink() ? 1 : 2; }); do { // Remove all the cells one by one. // Note that all the links are removed first, so it's // safe to remove the elements without removing the connected // links first. cells.shift().remove(opt); } while (cells.length > 0); this.stopBatch('clear'); return this; }, _prepareCell: function(cell) { var attrs; if (cell instanceof Backbone.Model) { attrs = cell.attributes; cell.graph = this; } else { // In case we're dealing with a plain JS object, we have to set the reference // to the `graph` right after the actual model is created. This happens in the `model()` function // of `joint.dia.GraphCells`. attrs = cell; } if (!_.isString(attrs.type)) { throw new TypeError('dia.Graph: cell type must be a string.'); } return cell; }, maxZIndex: function() { var lastCell = this.get('cells').last(); return lastCell ? (lastCell.get('z') || 0) : 0; }, addCell: function(cell, options) { if (_.isArray(cell)) { return this.addCells(cell, options); } if (cell instanceof Backbone.Model) { if (!cell.has('z')) { cell.set('z', this.maxZIndex() + 1); } } else if (_.isUndefined(cell.z)) { cell.z = this.maxZIndex() + 1; } this.get('cells').add(this._prepareCell(cell), options || {}); return this; }, addCells: function(cells, opt) { if (cells.length) { opt.position = cells.length; this.startBatch('add'); _.each(cells, function(cell) { opt.position--; this.addCell(cell, opt); }, this); this.stopBatch('add'); } return this; }, // When adding a lot of cells, it is much more efficient to // reset the entire cells collection in one go. // Useful for bulk operations and optimizations. resetCells: function(cells, opt) { this.get('cells').reset(_.map(cells, this._prepareCell, this), opt); return this; }, removeCells: function(cells, opt) { if (cells.length) { this.startBatch('remove'); _.invoke(cells, 'remove'); this.stopBatch('remove'); } return this; }, _removeCell: function(cell, collection, options) { options = options || {}; if (!options.clear) { // Applications might provide a `disconnectLinks` option set to `true` in order to // disconnect links when a cell is removed rather then removing them. The default // is to remove all the associated links. if (options.disconnectLinks) { this.disconnectLinks(cell, options); } else { this.removeLinks(cell, options); } } // Silently remove the cell from the cells collection. Silently, because // `joint.dia.Cell.prototype.remove` already triggers the `remove` event which is // then propagated to the graph model. If we didn't remove the cell silently, two `remove` events // would be triggered on the graph model. this.get('cells').remove(cell, { silent: true }); delete cell.graph; }, // Get a cell by `id`. getCell: function(id) { return this.get('cells').get(id); }, getCells: function() { return this.get('cells').toArray(); }, getElements: function() { return _.map(this._nodes, function(exists, node) { return this.getCell(node); }, this); }, getLinks: function() { return _.map(this._edges, function(exists, edge) { return this.getCell(edge); }, this); }, getFirstCell: function() { return this.get('cells').first(); }, getLastCell: function() { return this.get('cells').last(); }, // Get all inbound and outbound links connected to the cell `model`. getConnectedLinks: function(model, opt) { opt = opt || {}; var inbound = opt.inbound; var outbound = opt.outbound; if (_.isUndefined(inbound) && _.isUndefined(outbound)) { inbound = outbound = true; } // The final array of connected link models. var links = []; // Connected edges. This hash table ([edge] -> true) serves only // for a quick lookup to check if we already added a link. var edges = {}; if (outbound) { _.each(this.getOutboundEdges(model.id), function(exists, edge) { if (!edges[edge]) { links.push(this.getCell(edge)); edges[edge] = true; } }, this); } if (inbound) { _.each(this.getInboundEdges(model.id), function(exists, edge) { // Skip links that were already added. Those must be self-loop links // because they are both inbound and outbond edges of the same element. if (!edges[edge]) { links.push(this.getCell(edge)); edges[edge] = true; } }, this); } // If 'deep' option is 'true', return all the links that are connected to any of the descendent cells // and are not descendents themselves. if (opt.deep) { var embeddedCells = model.getEmbeddedCells({ deep: true }); // In the first round, we collect all the embedded edges so that we can exclude // them from the final result. var embeddedEdges = {}; _.each(embeddedCells, function(cell) { if (cell.isLink()) { embeddedEdges[cell.id] = true; } }); _.each(embeddedCells, function(cell) { if (cell.isLink()) return; if (outbound) { _.each(this.getOutboundEdges(cell.id), function(exists, edge) { if (!edges[edge] && !embeddedEdges[edge]) { links.push(this.getCell(edge)); edges[edge] = true; } }, this); } if (inbound) { _.each(this.getInboundEdges(cell.id), function(exists, edge) { if (!edges[edge] && !embeddedEdges[edge]) { links.push(this.getCell(edge)); edges[edge] = true; } }, this); } }, this); } return links; }, getNeighbors: function(model, opt) { opt = opt || {}; var inbound = opt.inbound; var outbound = opt.outbound; if (_.isUndefined(inbound) && _.isUndefined(outbound)) { inbound = outbound = true; } var neighbors = _.transform(this.getConnectedLinks(model, opt), function(res, link) { var source = link.get('source'); var target = link.get('target'); var loop = link.hasLoop(opt); // Discard if it is a point, or if the neighbor was already added. if (inbound && _.has(source, 'id') && !res[source.id]) { var sourceElement = this.getCell(source.id); if (loop || (sourceElement && sourceElement !== model && (!opt.deep || !sourceElement.isEmbeddedIn(model)))) { res[source.id] = sourceElement; } } // Discard if it is a point, or if the neighbor was already added. if (outbound && _.has(target, 'id') && !res[target.id]) { var targetElement = this.getCell(target.id); if (loop || (targetElement && targetElement !== model && (!opt.deep || !targetElement.isEmbeddedIn(model)))) { res[target.id] = targetElement; } } }, {}, this); return _.values(neighbors); }, getCommonAncestor: function(/* cells */) { var cellsAncestors = _.map(arguments, function(cell) { var ancestors = []; var parentId = cell.get('parent'); while (parentId) { ancestors.push(parentId); parentId = this.getCell(parentId).get('parent'); } return ancestors; }, this); cellsAncestors = _.sortBy(cellsAncestors, 'length'); var commonAncestor = _.find(cellsAncestors.shift(), function(ancestor) { return _.every(cellsAncestors, function(cellAncestors) { return _.contains(cellAncestors, ancestor); }); }); return this.getCell(commonAncestor); }, // Find the whole branch starting at `element`. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. getSuccessors: function(element, opt) { opt = opt || {}; var res = []; // Modify the options so that it includes the `outbound` neighbors only. In other words, search forwards. this.search(element, function(el) { if (el !== element) { res.push(el); } }, _.extend({}, opt, { outbound: true })); return res; }, // Clone `cells` returning an object that maps the original cell ID to the clone. The number // of clones is exactly the same as the `cells.length`. // This function simply clones all the `cells`. However, it also reconstructs // all the `source/target` and `parent/embed` references within the `cells`. // This is the main difference from the `cell.clone()` method. The // `cell.clone()` method works on one single cell only. // For example, for a graph: `A --- L ---> B`, `cloneCells([A, L, B])` // returns `[A2, L2, B2]` resulting to a graph: `A2 --- L2 ---> B2`, i.e. // the source and target of the link `L2` is changed to point to `A2` and `B2`. cloneCells: function(cells) { cells = _.unique(cells); // A map of the form [original cell ID] -> [clone] helping // us to reconstruct references for source/target and parent/embeds. // This is also the returned value. var cloneMap = _.transform(cells, function(map, cell) { map[cell.id] = cell.clone(); }, {}); _.each(cells, function(cell) { var clone = cloneMap[cell.id]; // assert(clone exists) if (clone.isLink()) { var source = clone.get('source'); var target = clone.get('target'); if (source.id && cloneMap[source.id]) { // Source points to an element and the element is among the clones. // => Update the source of the cloned link. clone.prop('source/id', cloneMap[source.id].id); } if (target.id && cloneMap[target.id]) { // Target points to an element and the element is among the clones. // => Update the target of the cloned link. clone.prop('target/id', cloneMap[target.id].id); } } // Find the parent of the original cell var parent = cell.get('parent'); if (parent && cloneMap[parent]) { clone.set('parent', cloneMap[parent].id); } // Find the embeds of the original cell var embeds = _.reduce(cell.get('embeds'), function(newEmbeds, embed) { // Embedded cells that are not being cloned can not be carried // over with other embedded cells. if (cloneMap[embed]) { newEmbeds.push(cloneMap[embed].id); } return newEmbeds; }, []); if (!_.isEmpty(embeds)) { clone.set('embeds', embeds); } }); return cloneMap; }, // Clone the whole subgraph (including all the connected links whose source/target is in the subgraph). // If `opt.deep` is `true`, also take into account all the embedded cells of all the subgraph cells. // Return a map of the form: [original cell ID] -> [clone]. cloneSubgraph: function(cells, opt) { var subgraph = this.getSubgraph(cells, opt); return this.cloneCells(subgraph); }, // Return `cells` and all the connected links that connect cells in the `cells` array. // If `opt.deep` is `true`, return all the cells including all their embedded cells // and all the links that connect any of the returned cells. // For example, for a single shallow element, the result is that very same element. // For two elements connected with a link: `A --- L ---> B`, the result for // `getSubgraph([A, B])` is `[A, L, B]`. The same goes for `getSubgraph([L])`, the result is again `[A, L, B]`. getSubgraph: function(cells, opt) { opt = opt || {}; var subgraph = []; // `cellMap` is used for a quick lookup of existance of a cell in the `cells` array. var cellMap = {}; var elements = []; var links = []; _.each(cells, function(cell) { if (!cellMap[cell.id]) { subgraph.push(cell); cellMap[cell.id] = cell; if (cell.isLink()) { links.push(cell); } else { elements.push(cell); } } if (opt.deep) { var embeds = cell.getEmbeddedCells({ deep: true }); _.each(embeds, function(embed) { if (!cellMap[embed.id]) { subgraph.push(embed); cellMap[embed.id] = embed; if (embed.isLink()) { links.push(embed); } else { elements.push(embed); } } }); } }); _.each(links, function(link) { // For links, return their source & target (if they are elements - not points). var source = link.get('source'); var target = link.get('target'); if (source.id && !cellMap[source.id]) { var sourceElement = this.getCell(source.id); subgraph.push(sourceElement); cellMap[sourceElement.id] = sourceElement; elements.push(sourceElement); } if (target.id && !cellMap[target.id]) { var targetElement = this.getCell(target.id); subgraph.push(this.getCell(target.id)); cellMap[targetElement.id] = targetElement; elements.push(targetElement); } }, this); _.each(elements, function(element) { // For elements, include their connected links if their source/target is in the subgraph; var links = this.getConnectedLinks(element, opt); _.each(links, function(link) { var source = link.get('source'); var target = link.get('target'); if (!cellMap[link.id] && source.id && cellMap[source.id] && target.id && cellMap[target.id]) { subgraph.push(link); cellMap[link.id] = link; } }); }, this); return subgraph; }, // Find all the predecessors of `element`. This is a reverse operation of `getSuccessors()`. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.breadthFirst` is `true`, use the Breadth-first search algorithm, otherwise use Depth-first search. getPredecessors: function(element, opt) { opt = opt || {}; var res = []; // Modify the options so that it includes the `inbound` neighbors only. In other words, search backwards. this.search(element, function(el) { if (el !== element) { res.push(el); } }, _.extend({}, opt, { inbound: true })); return res; }, // Perform search on the graph. // If `opt.breadthFirst` is `true`, use the Breadth-first Search algorithm, otherwise use Depth-first search. // By setting `opt.inbound` to `true`, you can reverse the direction of the search. // If `opt.deep` is `true`, take into account embedded elements too. // `iteratee` is a function of the form `function(element) {}`. // If `iteratee` explicitely returns `false`, the searching stops. search: function(element, iteratee, opt) { opt = opt || {}; if (opt.breadthFirst) { this.bfs(element, iteratee, opt); } else { this.dfs(element, iteratee, opt); } }, // Breadth-first search. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). // `iteratee` is a function of the form `function(element, distance) {}`. // where `element` is the currently visited element and `distance` is the distance of that element // from the root `element` passed the `bfs()`, i.e. the element we started the search from. // Note that the `distance` is not the shortest or longest distance, it is simply the number of levels // crossed till we visited the `element` for the first time. It is especially useful for tree graphs. // If `iteratee` explicitely returns `false`, the searching stops. bfs: function(element, iteratee, opt) { opt = opt || {}; var visited = {}; var distance = {}; var queue = []; queue.push(element); distance[element.id] = 0; while (queue.length > 0) { var next = queue.shift(); if (!visited[next.id]) { visited[next.id] = true; if (iteratee(next, distance[next.id]) === false) return; _.each(this.getNeighbors(next, opt), function(neighbor) { distance[neighbor.id] = distance[next.id] + 1; queue.push(neighbor); }); } } }, // Depth-first search. // If `opt.deep` is `true`, take into account embedded elements too. // If `opt.inbound` is `true`, reverse the search direction (it's like reversing all the link directions). // `iteratee` is a function of the form `function(element, distance) {}`. // If `iteratee` explicitely returns `false`, the search stops. dfs: function(element, iteratee, opt, _visited, _distance) { opt = opt || {}; var visited = _visited || {}; var distance = _distance || 0; if (iteratee(element, distance) === false) return; visited[element.id] = true; _.each(this.getNeighbors(element, opt), function(neighbor) { if (!visited[neighbor.id]) { this.dfs(neighbor, iteratee, opt, visited, distance + 1); } }, this); }, // Get all the roots of the graph. Time complexity: O(|V|). getSources: function() { var sources = []; _.each(this._nodes, function(exists, node) { if (!this._in[node] || _.isEmpty(this._in[node])) { sources.push(this.getCell(node)); } }, this); return sources; }, // Get all the leafs of the graph. Time complexity: O(|V|). getSinks: function() { var sinks = []; _.each(this._nodes, function(exists, node) { if (!this._out[node] || _.isEmpty(this._out[node])) { sinks.push(this.getCell(node)); } }, this); return sinks; }, // Return `true` if `element` is a root. Time complexity: O(1). isSource: function(element) { return !this._in[element.id] || _.isEmpty(this._in[element.id]); }, // Return `true` if `element` is a leaf. Time complexity: O(1). isSink: function(element) { return !this._out[element.id] || _.isEmpty(this._out[element.id]); }, // Return `true` is `elementB` is a successor of `elementA`. Return `false` otherwise. isSuccessor: function(elementA, elementB) { var isSuccessor = false; this.search(elementA, function(element) { if (element === elementB && element !== elementA) { isSuccessor = true; return false; } }, { outbound: true }); return isSuccessor; }, // Return `true` is `elementB` is a predecessor of `elementA`. Return `false` otherwise. isPredecessor: function(elementA, elementB) { var isPredecessor = false; this.search(elementA, function(element) { if (element === elementB && element !== elementA) { isPredecessor = true; return false; } }, { inbound: true }); return isPredecessor; }, // Return `true` is `elementB` is a neighbor of `elementA`. Return `false` otherwise. // `opt.deep` controls whether to take into account embedded elements as well. See `getNeighbors()` // for more details. // If `opt.outbound` is set to `true`, return `true` only if `elementB` is a successor neighbor. // Similarly, if `opt.inbound` is set to `true`, return `true` only if `elementB` is a predecessor neighbor. isNeighbor: function(elementA, elementB, opt) { opt = opt || {}; var inbound = opt.inbound; var outbound = opt.outbound; if (_.isUndefined(inbound) && _.isUndefined(outbound)) { inbound = outbound = true; } var isNeighbor = false; _.each(this.getConnectedLinks(elementA, opt), function(link) { var source = link.get('source'); var target = link.get('target'); var loop = link.hasLoop(opt); // Discard if it is a point. if (inbound && _.has(source, 'id') && source.id === elementB.id) { isNeighbor = true; return false; } // Discard if it is a point, or if the neighbor was already added. if (outbound && _.has(target, 'id') && target.id === elementB.id) { isNeighbor = true; return false; } }); return isNeighbor; }, // Disconnect links connected to the cell `model`. disconnectLinks: function(model, options) { _.each(this.getConnectedLinks(model), function(link) { link.set(link.get('source').id === model.id ? 'source' : 'target', g.point(0, 0), options); }); }, // Remove links connected to the cell `model` completely. removeLinks: function(model, options) { _.invoke(this.getConnectedLinks(model), 'remove', options); }, // Find all elements at given point findModelsFromPoint: function(p) { return _.filter(this.getElements(), function(el) { return el.getBBox().containsPoint(p); }); }, // Find all elements in given area findModelsInArea: function(rect, opt) { opt = _.defaults(opt || {}, { strict: false }); var method = opt.strict ? 'containsRect' : 'intersect'; return _.filter(this.getElements(), function(el) { return rect[method](el.getBBox()); }); }, // Find all elements under the given element. findModelsUnderElement: function(element, opt) { opt = _.defaults(opt || {}, { searchBy: 'bbox' }); var bbox = element.getBBox(); var elements = (opt.searchBy == 'bbox') ? this.findModelsInArea(bbox) : this.findModelsFromPoint(bbox[opt.searchBy]()); // don't account element itself or any of its descendents return _.reject(elements, function(el) { return element.id == el.id || el.isEmbeddedIn(element); }); }, // Return bounding box of all elements. getBBox: function(cells, opt) { return this.getCellsBBox(cells || this.getElements(), opt); }, // Return the bounding box of all cells in array provided. // Links are being ignored. getCellsBBox: function(cells, opt) { return _.reduce(cells, function(memo, cell) { if (cell.isLink()) return memo; if (memo) { return memo.union(cell.getBBox(opt)); } else { return cell.getBBox(opt); } }, null); }, translate: function(dx, dy, opt) { // Don't translate cells that are embedded in any other cell. var cells = _.reject(this.getCells(), function(cell) { return cell.isEmbedded(); }); _.invoke(cells, 'translate', dx, dy, opt); }, resize: function(width, height, opt) { return this.resizeCells(width, height, this.getCells(), opt); }, resizeCells: function(width, height, cells, opt) { // `getBBox` method returns `null` if no elements provided. // i.e. cells can be an array of links var bbox = this.getCellsBBox(cells); if (bbox) { var sx = Math.max(width / bbox.width, 0); var sy = Math.max(height / bbox.height, 0); _.invoke(cells, 'scale', sx, sy, bbox.origin(), opt); } return this; }, startBatch: function(name, data) { data = data || {}; this._batches[name] = (this._batches[name] || 0) + 1; return this.trigger('batch:start', _.extend({}, data, { batchName: name })); }, stopBatch: function(name, data) { data = data || {}; this._batches[name] = (this._batches[name] || 0) - 1; return this.trigger('batch:stop', _.extend({}, data, { batchName: name })); }, hasActiveBatch: function(name) { if (name) { return this._batches[name]; } else { return _.any(this._batches, function(batches) { return batches > 0; }); } } }); joint.util.wrapWith(joint.dia.Graph.prototype, ['resetCells', 'addCells', 'removeCells'], 'cells'); // JointJS. // (c) 2011-2015 client IO // joint.dia.Cell base model. // -------------------------- joint.dia.Cell = Backbone.Model.extend({ // This is the same as Backbone.Model with the only difference that is uses _.merge // instead of just _.extend. The reason is that we want to mixin attributes set in upper classes. constructor: function(attributes, options) { var defaults; var attrs = attributes || {}; this.cid = _.uniqueId('c'); this.attributes = {}; if (options && options.collection) this.collection = options.collection; if (options && options.parse) attrs = this.parse(attrs, options) || {}; if (defaults = _.result(this, 'defaults')) { // // Replaced the call to _.defaults with _.merge. attrs = _.merge({}, defaults, attrs); // } this.set(attrs, options); this.changed = {}; this.initialize.apply(this, arguments); }, translate: function(dx, dy, opt) { throw new Error('Must define a translate() method.'); }, toJSON: function() { var defaultAttrs = this.constructor.prototype.defaults.attrs || {}; var attrs = this.attributes.attrs; var finalAttrs = {}; // Loop through all the attributes and // omit the default attributes as they are implicitly reconstructable by the cell 'type'. _.each(attrs, function(attr, selector) { var defaultAttr = defaultAttrs[selector]; _.each(attr, function(value, name) { // attr is mainly flat though it might have one more level (consider the `style` attribute). // Check if the `value` is object and if yes, go one level deep. if (_.isObject(value) && !_.isArray(value)) { _.each(value, function(value2, name2) { if (!defaultAttr || !defaultAttr[name] || !_.isEqual(defaultAttr[name][name2], value2)) { finalAttrs[selector] = finalAttrs[selector] || {}; (finalAttrs[selector][name] || (finalAttrs[selector][name] = {}))[name2] = value2; } }); } else if (!defaultAttr || !_.isEqual(defaultAttr[name], value)) { // `value` is not an object, default attribute for such a selector does not exist // or it is different than the attribute value set on the model. finalAttrs[selector] = finalAttrs[selector] || {}; finalAttrs[selector][name] = value; } }); }); var attributes = _.cloneDeep(_.omit(this.attributes, 'attrs')); //var attributes = JSON.parse(JSON.stringify(_.omit(this.attributes, 'attrs'))); attributes.attrs = finalAttrs; return attributes; }, initialize: function(options) { if (!options || !options.id) { this.set('id', joint.util.uuid(), { silent: true }); } this._transitionIds = {}; // Collect ports defined in `attrs` and keep collecting whenever `attrs` object changes. this.processPorts(); this.on('change:attrs', this.processPorts, this); }, processPorts: function() { // Whenever `attrs` changes, we extract ports from the `attrs` object and store it // in a more accessible way. Also, if any port got removed and there were links that had `target`/`source` // set to that port, we remove those links as well (to follow the same behaviour as // with a removed element). var previousPorts = this.ports; // Collect ports from the `attrs` object. var ports = {}; _.each(this.get('attrs'), function(attrs, selector) { if (attrs && attrs.port) { // `port` can either be directly an `id` or an object containing an `id` (and potentially other data). if (!_.isUndefined(attrs.port.id)) { ports[attrs.port.id] = attrs.port; } else { ports[attrs.port] = { id: attrs.port }; } } }); // Collect ports that have been removed (compared to the previous ports) - if any. // Use hash table for quick lookup. var removedPorts = {}; _.each(previousPorts, function(port, id) { if (!ports[id]) removedPorts[id] = true; }); // Remove all the incoming/outgoing links that have source/target port set to any of the removed ports. if (this.graph && !_.isEmpty(removedPorts)) { var inboundLinks = this.graph.getConnectedLinks(this, { inbound: true }); _.each(inboundLinks, function(link) { if (removedPorts[link.get('target').port]) link.remove(); }); var outboundLinks = this.graph.getConnectedLinks(this, { outbound: true }); _.each(outboundLinks, function(link) { if (removedPorts[link.get('source').port]) link.remove(); }); } // Update the `ports` object. this.ports = ports; }, remove: function(opt) { opt = opt || {}; // Store the graph in a variable because `this.graph` won't' be accessbile after `this.trigger('remove', ...)` down below. var graph = this.graph; if (graph) { graph.startBatch('remove'); } // First, unembed this cell from its parent cell if there is one. var parentCellId = this.get('parent'); if (parentCellId) { var parentCell = graph && graph.getCell(parentCellId); parentCell.unembed(this); } _.invoke(this.getEmbeddedCells(), 'remove', opt); this.trigger('remove', this, this.collection, opt); if (graph) { graph.stopBatch('remove'); } return this; }, toFront: function(opt) { if (this.graph) { opt = opt || {}; var z = (this.graph.getLastCell().get('z') || 0) + 1; this.startBatch('to-front').set('z', z, opt); if (opt.deep) { var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); _.each(cells, function(cell) { cell.set('z', ++z, opt); }); } this.stopBatch('to-front'); } return this; }, toBack: function(opt) { if (this.graph) { opt = opt || {}; var z = (this.graph.getFirstCell().get('z') || 0) - 1; this.startBatch('to-back'); if (opt.deep) { var cells = this.getEmbeddedCells({ deep: true, breadthFirst: true }); _.eachRight(cells, function(cell) { cell.set('z', z--, opt); }); } this.set('z', z, opt).stopBatch('to-back'); } return this; }, embed: function(cell, opt) { if (this === cell || this.isEmbeddedIn(cell)) { throw new Error('Recursive embedding not allowed.'); } else { this.startBatch('embed'); var embeds = _.clone(this.get('embeds') || []); // We keep all element ids after link ids. embeds[cell.isLink() ? 'unshift' : 'push'](cell.id); cell.set('parent', this.id, opt); this.set('embeds', _.uniq(embeds), opt); this.stopBatch('embed'); } return this; }, unembed: function(cell, opt) { this.startBatch('unembed'); cell.unset('parent', opt); this.set('embeds', _.without(this.get('embeds'), cell.id), opt); this.stopBatch('unembed'); return this; }, // Return an array of ancestor cells. // The array is ordered from the parent of the cell // to the most distant ancestor. getAncestors: function() { var ancestors = []; var parentId = this.get('parent'); if (!this.graph) { return ancestors; } while (parentId !== undefined) { var parent = this.graph.getCell(parentId); if (parent !== undefined) { ancestors.push(parent); parentId = parent.get('parent'); } else { break; } } return ancestors; }, getEmbeddedCells: function(opt) { opt = opt || {}; // Cell models can only be retrieved when this element is part of a collection. // There is no way this element knows about other cells otherwise. // This also means that calling e.g. `translate()` on an element with embeds before // adding it to a graph does not translate its embeds. if (this.graph) { var cells; if (opt.deep) { if (opt.breadthFirst) { // breadthFirst algorithm cells = []; var queue = this.getEmbeddedCells(); while (queue.length > 0) { var parent = queue.shift(); cells.push(parent); queue.push.apply(queue, parent.getEmbeddedCells()); } } else { // depthFirst algorithm cells = this.getEmbeddedCells(); _.each(cells, function(cell) { cells.push.apply(cells, cell.getEmbeddedCells(opt)); }); } } else { cells = _.map(this.get('embeds'), this.graph.getCell, this.graph); } return cells; } return []; }, isEmbeddedIn: function(cell, opt) { var cellId = _.isString(cell) ? cell : cell.id; var parentId = this.get('parent'); opt = _.defaults({ deep: true }, opt); // See getEmbeddedCells(). if (this.graph && opt.deep) { while (parentId) { if (parentId === cellId) { return true; } parentId = this.graph.getCell(parentId).get('parent'); } return false; } else { // When this cell is not part of a collection check // at least whether it's a direct child of given cell. return parentId === cellId; } }, // Whether or not the cell is embedded in any other cell. isEmbedded: function() { return !!this.get('parent'); }, // Isolated cloning. Isolated cloning has two versions: shallow and deep (pass `{ deep: true }` in `opt`). // Shallow cloning simply clones the cell and returns a new cell with different ID. // Deep cloning clones the cell and all its embedded cells recursively. clone: function(opt) { opt = opt || {}; if (!opt.deep) { // Shallow cloning. var clone = Backbone.Model.prototype.clone.apply(this, arguments); // We don't want the clone to have the same ID as the original. clone.set('id', joint.util.uuid()); // A shallow cloned element does not carry over the original embeds. clone.unset('embeds'); // And can not be embedded in any cell // as the clone is not part of the graph. clone.unset('parent'); return clone; } else { // Deep cloning. // For a deep clone, simply call `graph.cloneCells()` with the cell and all its embedded cells. return _.values(joint.dia.Graph.prototype.cloneCells.call(null, [this].concat(this.getEmbeddedCells({ deep: true })))); } }, // A convenient way to set nested properties. // This method merges the properties you'd like to set with the ones // stored in the cell and makes sure change events are properly triggered. // You can either set a nested property with one object // or use a property path. // The most simple use case is: // `cell.prop('name/first', 'John')` or // `cell.prop({ name: { first: 'John' } })`. // Nested arrays are supported too: // `cell.prop('series/0/data/0/degree', 50)` or // `cell.prop({ series: [ { data: [ { degree: 50 } ] } ] })`. prop: function(props, value, opt) { var delim = '/'; if (_.isString(props)) { // Get/set an attribute by a special path syntax that delimits // nested objects by the colon character. if (arguments.length > 1) { var path = props; var pathArray = path.split('/'); var property = pathArray[0]; // Remove the top-level property from the array of properties. pathArray.shift(); opt = opt || {}; opt.propertyPath = path; opt.propertyValue = value; if (pathArray.length === 0) { // Property is not nested. We can simply use `set()`. return this.set(property, value, opt); } var update = {}; // Initialize the nested object. Subobjects are either arrays or objects. // An empty array is created if the sub-key is an integer. Otherwise, an empty object is created. // Note that this imposes a limitation on object keys one can use with Inspector. // Pure integer keys will cause issues and are therefore not allowed. var initializer = update; var prevProperty = property; _.each(pathArray, function(key) { initializer = initializer[prevProperty] = (_.isFinite(Number(key)) ? [] : {}); prevProperty = key; }); // Fill update with the `value` on `path`. update = joint.util.setByPath(update, path, value, '/'); var baseAttributes = _.merge({}, this.attributes); // if rewrite mode enabled, we replace value referenced by path with // the new one (we don't merge). opt.rewrite && joint.util.unsetByPath(baseAttributes, path, '/'); // Merge update with the model attributes. var attributes = _.merge(baseAttributes, update); // Finally, set the property to the updated attributes. return this.set(property, attributes[property], opt); } else { return joint.util.getByPath(this.attributes, props, delim); } } return this.set(_.merge({}, this.attributes, props), value); }, // A convient way to unset nested properties removeProp: function(path, opt) { // Once a property is removed from the `attrs` attribute // the cellView will recognize a `dirty` flag and rerender itself // in order to remove the attribute from SVG element. opt = opt || {}; opt.dirty = true; var pathArray = path.split('/'); if (pathArray.length === 1) { // A top level property return this.unset(path, opt); } // A nested property var property = pathArray[0]; var nestedPath = pathArray.slice(1).join('/'); var propertyValue = _.merge({}, this.get(property)); joint.util.unsetByPath(propertyValue, nestedPath, '/'); return this.set(property, propertyValue, opt); }, // A convenient way to set nested attributes. attr: function(attrs, value, opt) { var args = Array.prototype.slice.call(arguments); if (_.isString(attrs)) { // Get/set an attribute by a special path syntax that delimits // nested objects by the colon character. args[0] = 'attrs/' + attrs; } else { args[0] = { 'attrs' : attrs }; } return this.prop.apply(this, args); }, // A convenient way to unset nested attributes removeAttr: function(path, opt) { if (_.isArray(path)) { _.each(path, function(p) { this.removeAttr(p, opt); }, this); return this; } return this.removeProp('attrs/' + path, opt); }, transition: function(path, value, opt, delim) { delim = delim || '/'; var defaults = { duration: 100, delay: 10, timingFunction: joint.util.timing.linear, valueFunction: joint.util.interpolate.number }; opt = _.extend(defaults, opt); var firstFrameTime = 0; var interpolatingFunction; var setter = _.bind(function(runtime) { var id, progress, propertyValue, status; firstFrameTime = firstFrameTime || runtime; runtime -= firstFrameTime; progress = runtime / opt.duration; if (progress < 1) { this._transitionIds[path] = id = joint.util.nextFrame(setter); } else { progress = 1; delete this._transitionIds[path]; } propertyValue = interpolatingFunction(opt.timingFunction(progress)); opt.transitionId = id; this.prop(path, propertyValue, opt); if (!id) this.trigger('transition:end', this, path); }, this); var initiator = _.bind(function(callback) { this.stopTransitions(path); interpolatingFunction = opt.valueFunction(joint.util.getByPath(this.attributes, path, delim), value); this._transitionIds[path] = joint.util.nextFrame(callback); this.trigger('transition:start', this, path); }, this); return _.delay(initiator, opt.delay, setter); }, getTransitions: function() { return _.keys(this._transitionIds); }, stopTransitions: function(path, delim) { delim = delim || '/'; var pathArray = path && path.split(delim); _(this._transitionIds).keys().filter(pathArray && function(key) { return _.isEqual(pathArray, key.split(delim).slice(0, pathArray.length)); }).each(function(key) { joint.util.cancelFrame(this._transitionIds[key]); delete this._transitionIds[key]; this.trigger('transition:end', this, key); }, this); return this; }, // A shorcut making it easy to create constructs like the following: // `var el = (new joint.shapes.basic.Rect).addTo(graph)`. addTo: function(graph, opt) { graph.addCell(this, opt); return this; }, // A shortcut for an equivalent call: `paper.findViewByModel(cell)` // making it easy to create constructs like the following: // `cell.findView(paper).highlight()` findView: function(paper) { return paper.findViewByModel(this); }, isElement: function() { return false; }, isLink: function() { return false; }, startBatch: function(name, opt) { if (this.graph) { this.graph.startBatch(name, _.extend({}, opt, { cell: this })); } return this; }, stopBatch: function(name, opt) { if (this.graph) { this.graph.stopBatch(name, _.extend({}, opt, { cell: this })); } return this; } }); // joint.dia.CellView base view and controller. // -------------------------------------------- // This is the base view and controller for `joint.dia.ElementView` and `joint.dia.LinkView`. joint.dia.CellView = joint.mvc.View.extend({ tagName: 'g', attributes: function() { return { 'model-id': this.model.id }; }, constructor: function(options) { // Make sure a global unique id is assigned to this view. Store this id also to the properties object. // The global unique id makes sure that the same view can be rendered on e.g. different machines and // still be associated to the same object among all those clients. This is necessary for real-time // collaboration mechanism. options.id = options.id || joint.util.guid(this); joint.mvc.View.call(this, options); }, init: function() { _.bindAll(this, 'remove', 'update'); // Store reference to this to the DOM element so that the view is accessible through the DOM tree. this.$el.data('view', this); this.listenTo(this.model, 'change:attrs', this.onChangeAttrs); }, onChangeAttrs: function(cell, attrs, opt) { if (opt.dirty) { // dirty flag could be set when a model attribute was removed and it needs to be cleared // also from the DOM element. See cell.removeAttr(). return this.render(); } return this.update(cell, attrs, opt); }, // Return `true` if cell link is allowed to perform a certain UI `feature`. // Example: `can('vertexMove')`, `can('labelMove')`. can: function(feature) { var interactive = _.isFunction(this.options.interactive) ? this.options.interactive(this) : this.options.interactive; return (_.isObject(interactive) && interactive[feature] !== false) || (_.isBoolean(interactive) && interactive !== false); }, // Override the Backbone `_ensureElement()` method in order to create a `` node that wraps // all the nodes of the Cell view. _ensureElement: function() { var el; if (!this.el) { var attrs = _.extend({ id: this.id }, _.result(this, 'attributes')); if (this.className) attrs['class'] = _.result(this, 'className'); el = V(_.result(this, 'tagName'), attrs).node; } else { el = _.result(this, 'el'); } this.setElement(el, false); }, // Utilize an alternative DOM manipulation API by // adding an element reference wrapped in Vectorizer. _setElement: function(el) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); this.el = this.$el[0]; this.vel = V(this.el); }, findBySelector: function(selector) { // These are either descendants of `this.$el` of `this.$el` itself. // `.` is a special selector used to select the wrapping `` element. var $selected = selector === '.' ? this.$el : this.$el.find(selector); return $selected; }, notify: function(eventName) { if (this.paper) { var args = Array.prototype.slice.call(arguments, 1); // Trigger the event on both the element itself and also on the paper. this.trigger.apply(this, [eventName].concat(args)); // Paper event handlers receive the view object as the first argument. this.paper.trigger.apply(this.paper, [eventName, this].concat(args)); } }, getStrokeBBox: function(el) { // Return a bounding box rectangle that takes into account stroke. // Note that this is a naive and ad-hoc implementation that does not // works only in certain cases and should be replaced as soon as browsers will // start supporting the getStrokeBBox() SVG method. // @TODO any better solution is very welcome! var isMagnet = !!el; el = el || this.el; var bbox = V(el).bbox(false, this.paper.viewport); var strokeWidth; if (isMagnet) { strokeWidth = V(el).attr('stroke-width'); } else { strokeWidth = this.model.attr('rect/stroke-width') || this.model.attr('circle/stroke-width') || this.model.attr('ellipse/stroke-width') || this.model.attr('path/stroke-width'); } strokeWidth = parseFloat(strokeWidth) || 0; return g.rect(bbox).moveAndExpand({ x: -strokeWidth / 2, y: -strokeWidth / 2, width: strokeWidth, height: strokeWidth }); }, getBBox: function() { return g.rect(this.vel.bbox()); }, highlight: function(el, opt) { el = !el ? this.el : this.$(el)[0] || this.el; // set partial flag if the highlighted element is not the entire view. opt = opt || {}; opt.partial = el != this.el; this.notify('cell:highlight', el, opt); return this; }, unhighlight: function(el, opt) { el = !el ? this.el : this.$(el)[0] || this.el; opt = opt || {}; opt.partial = el != this.el; this.notify('cell:unhighlight', el, opt); return this; }, // Find the closest element that has the `magnet` attribute set to `true`. If there was not such // an element found, return the root element of the cell view. findMagnet: function(el) { var $el = this.$(el); if ($el.length === 0 || $el[0] === this.el) { // If the overall cell has set `magnet === false`, then return `undefined` to // announce there is no magnet found for this cell. // This is especially useful to set on cells that have 'ports'. In this case, // only the ports have set `magnet === true` and the overall element has `magnet === false`. var attrs = this.model.get('attrs') || {}; if (attrs['.'] && attrs['.']['magnet'] === false) { return undefined; } return this.el; } if ($el.attr('magnet')) { return $el[0]; } return this.findMagnet($el.parent()); }, // `selector` is a CSS selector or `'.'`. `filter` must be in the special JointJS filter format: // `{ name: , args: { , ... }`. // An example is: `{ filter: { name: 'blur', args: { radius: 5 } } }`. applyFilter: function(selector, filter) { var $selected = _.isString(selector) ? this.findBySelector(selector) : $(selector); // Generate a hash code from the stringified filter definition. This gives us // a unique filter ID for different definitions. var filterId = filter.name + this.paper.svg.id + joint.util.hashCode(JSON.stringify(filter)); // If the filter already exists in the document, // we're done and we can just use it (reference it using `url()`). // If not, create one. if (!this.paper.svg.getElementById(filterId)) { var filterSVGString = joint.util.filter[filter.name] && joint.util.filter[filter.name](filter.args || {}); if (!filterSVGString) { throw new Error('Non-existing filter ' + filter.name); } var filterElement = V(filterSVGString); // Set the filter area to be 3x the bounding box of the cell // and center the filter around the cell. filterElement.attr({ filterUnits: 'objectBoundingBox', x: -1, y: -1, width: 3, height: 3 }); if (filter.attrs) filterElement.attr(filter.attrs); filterElement.node.id = filterId; V(this.paper.svg).defs().append(filterElement); } $selected.each(function() { V(this).attr('filter', 'url(#' + filterId + ')'); }); }, // `selector` is a CSS selector or `'.'`. `attr` is either a `'fill'` or `'stroke'`. // `gradient` must be in the special JointJS gradient format: // `{ type: , stops: [ { offset: , color: }, ... ]`. // An example is: `{ fill: { type: 'linearGradient', stops: [ { offset: '10%', color: 'green' }, { offset: '50%', color: 'blue' } ] } }`. applyGradient: function(selector, attr, gradient) { var $selected = _.isString(selector) ? this.findBySelector(selector) : $(selector); // Generate a hash code from the stringified filter definition. This gives us // a unique filter ID for different definitions. var gradientId = gradient.type + this.paper.svg.id + joint.util.hashCode(JSON.stringify(gradient)); // If the gradient already exists in the document, // we're done and we can just use it (reference it using `url()`). // If not, create one. if (!this.paper.svg.getElementById(gradientId)) { var gradientSVGString = [ '<' + gradient.type + '>', _.map(gradient.stops, function(stop) { return ''; }).join(''), '' ].join(''); var gradientElement = V(gradientSVGString); if (gradient.attrs) { gradientElement.attr(gradient.attrs); } gradientElement.node.id = gradientId; V(this.paper.svg).defs().append(gradientElement); } $selected.each(function() { V(this).attr(attr, 'url(#' + gradientId + ')'); }); }, // Construct a unique selector for the `el` element within this view. // `prevSelector` is being collected through the recursive call. // No value for `prevSelector` is expected when using this method. getSelector: function(el, prevSelector) { if (el === this.el) { return prevSelector; } var nthChild = V(el).index() + 1; var selector = el.tagName + ':nth-child(' + nthChild + ')'; if (prevSelector) { selector += ' > ' + prevSelector; } return this.getSelector(el.parentNode, selector); }, // Interaction. The controller part. // --------------------------------- // Interaction is handled by the paper and delegated to the view in interest. // `x` & `y` parameters passed to these functions represent the coordinates already snapped to the paper grid. // If necessary, real coordinates can be obtained from the `evt` event object. // These functions are supposed to be overriden by the views that inherit from `joint.dia.Cell`, // i.e. `joint.dia.Element` and `joint.dia.Link`. pointerdblclick: function(evt, x, y) { this.notify('cell:pointerdblclick', evt, x, y); }, pointerclick: function(evt, x, y) { this.notify('cell:pointerclick', evt, x, y); }, pointerdown: function(evt, x, y) { if (this.model.graph) { this.model.startBatch('pointer'); this._graph = this.model.graph; } this.notify('cell:pointerdown', evt, x, y); }, pointermove: function(evt, x, y) { this.notify('cell:pointermove', evt, x, y); }, pointerup: function(evt, x, y) { this.notify('cell:pointerup', evt, x, y); if (this._graph) { // we don't want to trigger event on model as model doesn't // need to be member of collection anymore (remove) this._graph.stopBatch('pointer', { cell: this.model }); delete this._graph; } }, mouseover: function(evt) { this.notify('cell:mouseover', evt); }, mouseout: function(evt) { this.notify('cell:mouseout', evt); }, mousewheel: function(evt, x, y, delta) { this.notify('cell:mousewheel', evt, x, y, delta); }, contextmenu: function(evt, x, y) { this.notify('cell:contextmenu', evt, x, y); }, onSetTheme: function(oldTheme, newTheme) { if (oldTheme) { this.vel.removeClass(this.themeClassNamePrefix + oldTheme); } this.vel.addClass(this.themeClassNamePrefix + newTheme); } }); // JointJS library. // (c) 2011-2015 client IO // joint.dia.Element base model. // ----------------------------- joint.dia.Element = joint.dia.Cell.extend({ defaults: { position: { x: 0, y: 0 }, size: { width: 1, height: 1 }, angle: 0 }, isElement: function() { return true; }, position: function(x, y, opt) { var isSetter = _.isNumber(y); opt = (isSetter ? opt : x) || {}; // option `parentRelative` for setting the position relative to the element's parent. if (opt.parentRelative) { // Getting the parent's position requires the collection. // Cell.get('parent') helds cell id only. if (!this.graph) throw new Error('Element must be part of a graph.'); var parent = this.graph.getCell(this.get('parent')); var parentPosition = parent && !parent.isLink() ? parent.get('position') : { x: 0, y: 0 }; } if (isSetter) { if (opt.parentRelative) { x += parentPosition.x; y += parentPosition.y; } return this.set('position', { x: x, y: y }, opt); } else { // Getter returns a geometry point. var elementPosition = g.point(this.get('position')); return opt.parentRelative ? elementPosition.difference(parentPosition) : elementPosition; } }, translate: function(tx, ty, opt) { tx = tx || 0; ty = ty || 0; if (tx === 0 && ty === 0) { // Like nothing has happened. return this; } opt = opt || {}; // Pass the initiator of the translation. opt.translateBy = opt.translateBy || this.id; var position = this.get('position') || { x: 0, y: 0 }; if (opt.restrictedArea && opt.translateBy === this.id) { // We are restricting the translation for the element itself only. We get // the bounding box of the element including all its embeds. // All embeds have to be translated the exact same way as the element. var bbox = this.getBBox({ deep: true }); var ra = opt.restrictedArea; //- - - - - - - - - - - - -> ra.x + ra.width // - - - -> position.x | // -> bbox.x // ▓▓▓▓▓▓▓ | // ░░░░░░░▓▓▓▓▓▓▓ // ░░░░░░░░░ | // ▓▓▓▓▓▓▓▓░░░░░░░ // ▓▓▓▓▓▓▓▓ | // <-dx-> | restricted area right border // <-width-> | ░ translated element // <- - bbox.width - -> ▓ embedded element var dx = position.x - bbox.x; var dy = position.y - bbox.y; // Find the maximal/minimal coordinates that the element can be translated // while complies the restrictions. var x = Math.max(ra.x + dx, Math.min(ra.x + ra.width + dx - bbox.width, position.x + tx)); var y = Math.max(ra.y + dy, Math.min(ra.y + ra.height + dy - bbox.height, position.y + ty)); // recalculate the translation taking the resctrictions into account. tx = x - position.x; ty = y - position.y; } var translatedPosition = { x: position.x + tx, y: position.y + ty }; // To find out by how much an element was translated in event 'change:position' handlers. opt.tx = tx; opt.ty = ty; if (opt.transition) { if (!_.isObject(opt.transition)) opt.transition = {}; this.transition('position', translatedPosition, _.extend({}, opt.transition, { valueFunction: joint.util.interpolate.object })); } else { this.set('position', translatedPosition, opt); // Recursively call `translate()` on all the embeds cells. _.invoke(this.getEmbeddedCells(), 'translate', tx, ty, opt); } return this; }, resize: function(width, height, opt) { opt = opt || {}; this.startBatch('resize', opt); if (opt.direction) { var currentSize = this.get('size'); switch (opt.direction) { case 'left': case 'right': // Don't change height when resizing horizontally. height = currentSize.height; break; case 'top': case 'bottom': // Don't change width when resizing vertically. width = currentSize.width; break; } // Get the angle and clamp its value between 0 and 360 degrees. var angle = g.normalizeAngle(this.get('angle') || 0); var quadrant = { 'top-right': 0, 'right': 0, 'top-left': 1, 'top': 1, 'bottom-left': 2, 'left': 2, 'bottom-right': 3, 'bottom': 3 }[opt.direction]; if (opt.absolute) { // We are taking the element's rotation into account quadrant += Math.floor((angle + 45) / 90); quadrant %= 4; } // This is a rectangle in size of the unrotated element. var bbox = this.getBBox(); // Pick the corner point on the element, which meant to stay on its place before and // after the rotation. var fixedPoint = bbox[['bottomLeft', 'corner', 'topRight', 'origin'][quadrant]](); // Find an image of the previous indent point. This is the position, where is the // point actually located on the screen. var imageFixedPoint = g.point(fixedPoint).rotate(bbox.center(), -angle); // Every point on the element rotates around a circle with the centre of rotation // in the middle of the element while the whole element is being rotated. That means // that the distance from a point in the corner of the element (supposed its always rect) to // the center of the element doesn't change during the rotation and therefore it equals // to a distance on unrotated element. // We can find the distance as DISTANCE = (ELEMENTWIDTH/2)^2 + (ELEMENTHEIGHT/2)^2)^0.5. var radius = Math.sqrt((width * width) + (height * height)) / 2; // Now we are looking for an angle between x-axis and the line starting at image of fixed point // and ending at the center of the element. We call this angle `alpha`. // The image of a fixed point is located in n-th quadrant. For each quadrant passed // going anti-clockwise we have to add 90 degrees. Note that the first quadrant has index 0. // // 3 | 2 // --c-- Quadrant positions around the element's center `c` // 0 | 1 // var alpha = quadrant * Math.PI / 2; // Add an angle between the beginning of the current quadrant (line parallel with x-axis or y-axis // going through the center of the element) and line crossing the indent of the fixed point and the center // of the element. This is the angle we need but on the unrotated element. alpha += Math.atan(quadrant % 2 == 0 ? height / width : width / height); // Lastly we have to deduct the original angle the element was rotated by and that's it. alpha -= g.toRad(angle); // With this angle and distance we can easily calculate the centre of the unrotated element. // Note that fromPolar constructor accepts an angle in radians. var center = g.point.fromPolar(radius, alpha, imageFixedPoint); // The top left corner on the unrotated element has to be half a width on the left // and half a height to the top from the center. This will be the origin of rectangle // we were looking for. var origin = g.point(center).offset( width / -2, height / -2); // Resize the element (before re-positioning it). this.set('size', { width: width, height: height }, opt); // Finally, re-position the element. this.position(origin.x, origin.y, opt); } else { // Resize the element. this.set('size', { width: width, height: height }, opt); } this.stopBatch('resize', opt); return this; }, scale: function(sx, sy, origin, opt) { var scaledBBox = this.getBBox().scale(sx, sy, origin); this.startBatch('scale', opt); this.position(scaledBBox.x, scaledBBox.y, opt); this.resize(scaledBBox.width, scaledBBox.height, opt); this.stopBatch('scale'); return this; }, fitEmbeds: function(opt) { opt = opt || {}; // Getting the children's size and position requires the collection. // Cell.get('embdes') helds an array of cell ids only. if (!this.graph) throw new Error('Element must be part of a graph.'); var embeddedCells = this.getEmbeddedCells(); if (embeddedCells.length > 0) { this.startBatch('fit-embeds', opt); if (opt.deep) { // Recursively apply fitEmbeds on all embeds first. _.invoke(embeddedCells, 'fitEmbeds', opt); } // Compute cell's size and position based on the children bbox // and given padding. var bbox = this.graph.getCellsBBox(embeddedCells); var padding = joint.util.normalizeSides(opt.padding); // Apply padding computed above to the bbox. bbox.moveAndExpand({ x: - padding.left, y: - padding.top, width: padding.right + padding.left, height: padding.bottom + padding.top }); // Set new element dimensions finally. this.set({ position: { x: bbox.x, y: bbox.y }, size: { width: bbox.width, height: bbox.height } }, opt); this.stopBatch('fit-embeds'); } return this; }, // Rotate element by `angle` degrees, optionally around `origin` point. // If `origin` is not provided, it is considered to be the center of the element. // If `absolute` is `true`, the `angle` is considered is abslute, i.e. it is not // the difference from the previous angle. rotate: function(angle, absolute, origin) { if (origin) { var center = this.getBBox().center(); var size = this.get('size'); var position = this.get('position'); center.rotate(origin, this.get('angle') - angle); var dx = center.x - size.width / 2 - position.x; var dy = center.y - size.height / 2 - position.y; this.startBatch('rotate', { angle: angle, absolute: absolute, origin: origin }); this.translate(dx, dy); this.rotate(angle, absolute); this.stopBatch('rotate'); } else { this.set('angle', absolute ? angle : (this.get('angle') + angle) % 360); } return this; }, getBBox: function(opt) { opt = opt || {}; if (opt.deep && this.graph) { // Get all the embedded elements using breadth first algorithm, // that doesn't use recursion. var elements = this.getEmbeddedCells({ deep: true, breadthFirst: true }); // Add the model itself. elements.push(this); return this.graph.getCellsBBox(elements); } var position = this.get('position'); var size = this.get('size'); return g.rect(position.x, position.y, size.width, size.height); } }); // joint.dia.Element base view and controller. // ------------------------------------------- joint.dia.ElementView = joint.dia.CellView.extend({ SPECIAL_ATTRIBUTES: [ 'style', 'text', 'html', 'ref-x', 'ref-y', 'ref-dx', 'ref-dy', 'ref-width', 'ref-height', 'ref', 'x-alignment', 'y-alignment', 'port' ], className: function() { return 'element ' + this.model.get('type').replace(/\./g, ' '); }, initialize: function() { _.bindAll(this, 'translate', 'resize', 'rotate'); joint.dia.CellView.prototype.initialize.apply(this, arguments); this.listenTo(this.model, 'change:position', this.translate); this.listenTo(this.model, 'change:size', this.resize); this.listenTo(this.model, 'change:angle', this.rotate); }, // Default is to process the `attrs` object and set attributes on subelements based on the selectors. update: function(cell, renderingOnlyAttrs) { var allAttrs = this.model.get('attrs'); var rotatable = this.rotatableNode; if (rotatable) { var rotation = rotatable.attr('transform'); rotatable.attr('transform', ''); } var relativelyPositioned = []; var nodesBySelector = {}; _.each(renderingOnlyAttrs || allAttrs, function(attrs, selector) { // Elements that should be updated. var $selected = this.findBySelector(selector); // No element matched by the `selector` was found. We're done then. if ($selected.length === 0) return; nodesBySelector[selector] = $selected; // Special attributes are treated by JointJS, not by SVG. var specialAttributes = this.SPECIAL_ATTRIBUTES.slice(); // If the `filter` attribute is an object, it is in the special JointJS filter format and so // it becomes a special attribute and is treated separately. if (_.isObject(attrs.filter)) { specialAttributes.push('filter'); this.applyFilter($selected, attrs.filter); } // If the `fill` or `stroke` attribute is an object, it is in the special JointJS gradient format and so // it becomes a special attribute and is treated separately. if (_.isObject(attrs.fill)) { specialAttributes.push('fill'); this.applyGradient($selected, 'fill', attrs.fill); } if (_.isObject(attrs.stroke)) { specialAttributes.push('stroke'); this.applyGradient($selected, 'stroke', attrs.stroke); } // Make special case for `text` attribute. So that we can set text content of the `` element // via the `attrs` object as well. // Note that it's important to set text before applying the rest of the final attributes. // Vectorizer `text()` method sets on the element its own attributes and it has to be possible // to rewrite them, if needed. (i.e display: 'none') if (!_.isUndefined(attrs.text)) { $selected.each(function() { V(this).text(attrs.text + '', { lineHeight: attrs.lineHeight, textPath: attrs.textPath, annotations: attrs.annotations }); }); specialAttributes.push('lineHeight', 'textPath', 'annotations'); } // Set regular attributes on the `$selected` subelement. Note that we cannot use the jQuery attr() // method as some of the attributes might be namespaced (e.g. xlink:href) which fails with jQuery attr(). var finalAttributes = _.omit(attrs, specialAttributes); $selected.each(function() { V(this).attr(finalAttributes); }); // `port` attribute contains the `id` of the port that the underlying magnet represents. if (attrs.port) { $selected.attr('port', _.isUndefined(attrs.port.id) ? attrs.port : attrs.port.id); } // `style` attribute is special in the sense that it sets the CSS style of the subelement. if (attrs.style) { $selected.css(attrs.style); } if (!_.isUndefined(attrs.html)) { $selected.each(function() { $(this).html(attrs.html + ''); }); } // Special `ref-x` and `ref-y` attributes make it possible to set both absolute or // relative positioning of subelements. if (!_.isUndefined(attrs['ref-x']) || !_.isUndefined(attrs['ref-y']) || !_.isUndefined(attrs['ref-dx']) || !_.isUndefined(attrs['ref-dy']) || !_.isUndefined(attrs['x-alignment']) || !_.isUndefined(attrs['y-alignment']) || !_.isUndefined(attrs['ref-width']) || !_.isUndefined(attrs['ref-height']) ) { _.each($selected, function(el, index, list) { var $el = $(el); // copy original list selector to the element $el.selector = list.selector; relativelyPositioned.push($el); }); } }, this); // We don't want the sub elements to affect the bounding box of the root element when // positioning the sub elements relatively to the bounding box. //_.invoke(relativelyPositioned, 'hide'); //_.invoke(relativelyPositioned, 'show'); // Note that we're using the bounding box without transformation because we are already inside // a transformed coordinate system. var size = this.model.get('size'); var bbox = { x: 0, y: 0, width: size.width, height: size.height }; renderingOnlyAttrs = renderingOnlyAttrs || {}; _.each(relativelyPositioned, function($el) { // if there was a special attribute affecting the position amongst renderingOnlyAttributes // we have to merge it with rest of the element's attributes as they are necessary // to update the position relatively (i.e `ref`) var renderingOnlyElAttrs = renderingOnlyAttrs[$el.selector]; var elAttrs = renderingOnlyElAttrs ? _.merge({}, allAttrs[$el.selector], renderingOnlyElAttrs) : allAttrs[$el.selector]; this.positionRelative(V($el[0]), bbox, elAttrs, nodesBySelector); }, this); if (rotatable) { rotatable.attr('transform', rotation || ''); } }, positionRelative: function(vel, bbox, attributes, nodesBySelector) { var ref = attributes['ref']; var refDx = parseFloat(attributes['ref-dx']); var refDy = parseFloat(attributes['ref-dy']); var yAlignment = attributes['y-alignment']; var xAlignment = attributes['x-alignment']; // 'ref-y', 'ref-x', 'ref-width', 'ref-height' can be defined // by value or by percentage e.g 4, 0.5, '200%'. var refY = attributes['ref-y']; var refYPercentage = _.isString(refY) && refY.slice(-1) === '%'; refY = parseFloat(refY); if (refYPercentage) { refY /= 100; } var refX = attributes['ref-x']; var refXPercentage = _.isString(refX) && refX.slice(-1) === '%'; refX = parseFloat(refX); if (refXPercentage) { refX /= 100; } var refWidth = attributes['ref-width']; var refWidthPercentage = _.isString(refWidth) && refWidth.slice(-1) === '%'; refWidth = parseFloat(refWidth); if (refWidthPercentage) { refWidth /= 100; } var refHeight = attributes['ref-height']; var refHeightPercentage = _.isString(refHeight) && refHeight.slice(-1) === '%'; refHeight = parseFloat(refHeight); if (refHeightPercentage) { refHeight /= 100; } // Check if the node is a descendant of the scalable group. var scalable = vel.findParentByClass('scalable', this.el); // `ref` is the selector of the reference element. If no `ref` is passed, reference // element is the root element. if (ref) { var vref; if (nodesBySelector && nodesBySelector[ref]) { // First we check if the same selector has been already used. vref = V(nodesBySelector[ref][0]); } else { // Other wise we find the ref ourselves. vref = ref === '.' ? this.vel : this.vel.findOne(ref); } if (!vref) { throw new Error('dia.ElementView: reference does not exists.'); } // Get the bounding box of the reference element relative to the root `` element. bbox = vref.bbox(false, this.el); } // Remove the previous translate() from the transform attribute and translate the element // relative to the root bounding box following the `ref-x` and `ref-y` attributes. if (vel.attr('transform')) { vel.attr('transform', vel.attr('transform').replace(/translate\([^)]*\)/g, '').trim() || ''); } // 'ref-width'/'ref-height' defines the width/height of the subelement relatively to // the reference element size // val in 0..1 ref-width = 0.75 sets the width to 75% of the ref. el. width // val < 0 || val > 1 ref-height = -20 sets the height to the the ref. el. height shorter by 20 if (isFinite(refWidth)) { if (refWidthPercentage || refWidth >= 0 && refWidth <= 1) { vel.attr('width', refWidth * bbox.width); } else { vel.attr('width', Math.max(refWidth + bbox.width, 0)); } } if (isFinite(refHeight)) { if (refHeightPercentage || refHeight >= 0 && refHeight <= 1) { vel.attr('height', refHeight * bbox.height); } else { vel.attr('height', Math.max(refHeight + bbox.height, 0)); } } // The final translation of the subelement. var tx = 0; var ty = 0; var scale; // `ref-dx` and `ref-dy` define the offset of the subelement relative to the right and/or bottom // coordinate of the reference element. if (isFinite(refDx)) { if (scalable) { // Compensate for the scale grid in case the elemnt is in the scalable group. scale = scale || scalable.scale(); tx = bbox.x + bbox.width + refDx / scale.sx; } else { tx = bbox.x + bbox.width + refDx; } } if (isFinite(refDy)) { if (scalable) { // Compensate for the scale grid in case the elemnt is in the scalable group. scale = scale || scalable.scale(); ty = bbox.y + bbox.height + refDy / scale.sy; } else { ty = bbox.y + bbox.height + refDy; } } // if `refX` is in [0, 1] then `refX` is a fraction of bounding box width // if `refX` is < 0 then `refX`'s absolute values is the right coordinate of the bounding box // otherwise, `refX` is the left coordinate of the bounding box // Analogical rules apply for `refY`. if (isFinite(refX)) { if (refXPercentage || refX > 0 && refX < 1) { tx = bbox.x + bbox.width * refX; } else if (scalable) { // Compensate for the scale grid in case the elemnt is in the scalable group. scale = scale || scalable.scale(); tx = bbox.x + refX / scale.sx; } else { tx = bbox.x + refX; } } if (isFinite(refY)) { if (refYPercentage || refY > 0 && refY < 1) { ty = bbox.y + bbox.height * refY; } else if (scalable) { // Compensate for the scale grid in case the elemnt is in the scalable group. scale = scale || scalable.scale(); ty = bbox.y + refY / scale.sy; } else { ty = bbox.y + refY; } } if (!_.isUndefined(yAlignment) || !_.isUndefined(xAlignment)) { var velBBox = vel.bbox(false, this.paper.viewport); // `y-alignment` when set to `middle` causes centering of the subelement around its new y coordinate. // `y-alignment` when set to `bottom` uses the y coordinate as referenced to the bottom of the bbox. if (yAlignment === 'middle') { ty -= velBBox.height / 2; } else if (yAlignment === 'bottom') { ty -= velBBox.height; } else if (isFinite(yAlignment)) { ty += (yAlignment > -1 && yAlignment < 1) ? velBBox.height * yAlignment : yAlignment; } // `x-alignment` when set to `middle` causes centering of the subelement around its new x coordinate. // `x-alignment` when set to `right` uses the x coordinate as referenced to the right of the bbox. if (xAlignment === 'middle') { tx -= velBBox.width / 2; } else if (xAlignment === 'right') { tx -= velBBox.width; } else if (isFinite(xAlignment)) { tx += (xAlignment > -1 && xAlignment < 1) ? velBBox.width * xAlignment : xAlignment; } } vel.translate(tx, ty); }, // `prototype.markup` is rendered by default. Set the `markup` attribute on the model if the // default markup is not desirable. renderMarkup: function() { var markup = this.model.get('markup') || this.model.markup; if (markup) { var nodes = V(markup); this.vel.append(nodes); } else { throw new Error('properties.markup is missing while the default render() implementation is used.'); } }, render: function() { this.$el.empty(); this.renderMarkup(); this.rotatableNode = this.vel.findOne('.rotatable'); this.scalableNode = this.vel.findOne('.scalable'); this.update(); this.resize(); this.rotate(); this.translate(); return this; }, // Scale the whole `` group. Note the difference between `scale()` and `resize()` here. // `resize()` doesn't scale the whole `` group but rather adjusts the `box.sx`/`box.sy` only. // `update()` is then responsible for scaling only those elements that have the `follow-scale` // attribute set to `true`. This is desirable in elements that have e.g. a `` subelement // that is not supposed to be scaled together with a surrounding `` element that IS supposed // be be scaled. scale: function(sx, sy) { // TODO: take into account the origin coordinates `ox` and `oy`. this.vel.scale(sx, sy); }, resize: function() { var size = this.model.get('size') || { width: 1, height: 1 }; var angle = this.model.get('angle') || 0; var scalable = this.scalableNode; if (!scalable) { // If there is no scalable elements, than there is nothing to resize. return; } var scalableBbox = scalable.bbox(true); // Make sure `scalableBbox.width` and `scalableBbox.height` are not zero which can happen if the element does not have any content. By making // the width/height 1, we prevent HTML errors of the type `scale(Infinity, Infinity)`. scalable.attr('transform', 'scale(' + (size.width / (scalableBbox.width || 1)) + ',' + (size.height / (scalableBbox.height || 1)) + ')'); // Now the interesting part. The goal is to be able to store the object geometry via just `x`, `y`, `angle`, `width` and `height` // Order of transformations is significant but we want to reconstruct the object always in the order: // resize(), rotate(), translate() no matter of how the object was transformed. For that to work, // we must adjust the `x` and `y` coordinates of the object whenever we resize it (because the origin of the // rotation changes). The new `x` and `y` coordinates are computed by canceling the previous rotation // around the center of the resized object (which is a different origin then the origin of the previous rotation) // and getting the top-left corner of the resulting object. Then we clean up the rotation back to what it originally was. // Cancel the rotation but now around a different origin, which is the center of the scaled object. var rotatable = this.rotatableNode; var rotation = rotatable && rotatable.attr('transform'); if (rotation && rotation !== 'null') { rotatable.attr('transform', rotation + ' rotate(' + (-angle) + ',' + (size.width / 2) + ',' + (size.height / 2) + ')'); var rotatableBbox = scalable.bbox(false, this.paper.viewport); // Store new x, y and perform rotate() again against the new rotation origin. this.model.set('position', { x: rotatableBbox.x, y: rotatableBbox.y }); this.rotate(); } // Update must always be called on non-rotated element. Otherwise, relative positioning // would work with wrong (rotated) bounding boxes. this.update(); }, translate: function(model, changes, opt) { var position = this.model.get('position') || { x: 0, y: 0 }; this.vel.attr('transform', 'translate(' + position.x + ',' + position.y + ')'); }, rotate: function() { var rotatable = this.rotatableNode; if (!rotatable) { // If there is no rotatable elements, then there is nothing to rotate. return; } var angle = this.model.get('angle') || 0; var size = this.model.get('size') || { width: 1, height: 1 }; var ox = size.width / 2; var oy = size.height / 2; rotatable.attr('transform', 'rotate(' + angle + ',' + ox + ',' + oy + ')'); }, getBBox: function(opt) { if (opt && opt.useModelGeometry) { var noTransformationBBox = this.model.getBBox().bbox(this.model.get('angle')); var transformationMatrix = this.paper.viewport.getCTM(); return g.rect(V.transformRect(noTransformationBBox, transformationMatrix)); } return joint.dia.CellView.prototype.getBBox.apply(this, arguments); }, // Embedding mode methods // ---------------------- prepareEmbedding: function(opt) { opt = opt || {}; var model = opt.model || this.model; var paper = opt.paper || this.paper; model.startBatch('to-front', opt); // Bring the model to the front with all his embeds. model.toFront({ deep: true, ui: true }); // Move to front also all the inbound and outbound links that are connected // to any of the element descendant. If we bring to front only embedded elements, // links connected to them would stay in the background. _.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'toFront', { ui: true }); model.stopBatch('to-front'); // Before we start looking for suitable parent we remove the current one. var parentId = model.get('parent'); parentId && paper.model.getCell(parentId).unembed(model, { ui: true }); }, processEmbedding: function(opt) { opt = opt || {}; var model = opt.model || this.model; var paper = opt.paper || this.paper; var paperOptions = paper.options; var candidates = paper.model.findModelsUnderElement(model, { searchBy: paperOptions.findParentBy }); if (paperOptions.frontParentOnly) { // pick the element with the highest `z` index candidates = candidates.slice(-1); } var newCandidateView = null; var prevCandidateView = this._candidateEmbedView; // iterate over all candidates starting from the last one (has the highest z-index). for (var i = candidates.length - 1; i >= 0; i--) { var candidate = candidates[i]; if (prevCandidateView && prevCandidateView.model.id == candidate.id) { // candidate remains the same newCandidateView = prevCandidateView; break; } else { var view = candidate.findView(paper); if (paperOptions.validateEmbedding.call(paper, this, view)) { // flip to the new candidate newCandidateView = view; break; } } } if (newCandidateView && newCandidateView != prevCandidateView) { // A new candidate view found. Highlight the new one. prevCandidateView && prevCandidateView.unhighlight(null, { embedding: true }); this._candidateEmbedView = newCandidateView.highlight(null, { embedding: true }); } if (!newCandidateView && prevCandidateView) { // No candidate view found. Unhighlight the previous candidate. prevCandidateView.unhighlight(null, { embedding: true }); delete this._candidateEmbedView; } }, finalizeEmbedding: function(opt) { opt = opt || {}; var candidateView = this._candidateEmbedView; var model = opt.model || this.model; var paper = opt.paper || this.paper; if (candidateView) { // We finished embedding. Candidate view is chosen to become the parent of the model. candidateView.model.embed(model, { ui: true }); candidateView.unhighlight(null, { embedding: true }); delete this._candidateEmbedView; } _.invoke(paper.model.getConnectedLinks(model, { deep: true }), 'reparent', { ui: true }); }, // Interaction. The controller part. // --------------------------------- pointerdown: function(evt, x, y) { var paper = this.paper; if ( evt.target.getAttribute('magnet') && this.can('addLinkFromMagnet') && paper.options.validateMagnet.call(paper, this, evt.target) ) { this.model.startBatch('add-link'); var link = paper.getDefaultLink(this, evt.target); link.set({ source: { id: this.model.id, selector: this.getSelector(evt.target), port: evt.target.getAttribute('port') }, target: { x: x, y: y } }); paper.model.addCell(link); var linkView = this._linkView = paper.findViewByModel(link); linkView.pointerdown(evt, x, y); linkView.startArrowheadMove('target', { whenNotAllowed: 'remove' }); } else { this._dx = x; this._dy = y; this.restrictedArea = paper.getRestrictedArea(this); joint.dia.CellView.prototype.pointerdown.apply(this, arguments); this.notify('element:pointerdown', evt, x, y); } }, pointermove: function(evt, x, y) { if (this._linkView) { // let the linkview deal with this event this._linkView.pointermove(evt, x, y); } else { var grid = this.paper.options.gridSize; if (this.can('elementMove')) { var position = this.model.get('position'); // Make sure the new element's position always snaps to the current grid after // translate as the previous one could be calculated with a different grid size. var tx = g.snapToGrid(position.x, grid) - position.x + g.snapToGrid(x - this._dx, grid); var ty = g.snapToGrid(position.y, grid) - position.y + g.snapToGrid(y - this._dy, grid); this.model.translate(tx, ty, { restrictedArea: this.restrictedArea, ui: true }); if (this.paper.options.embeddingMode) { if (!this._inProcessOfEmbedding) { // Prepare the element for embedding only if the pointer moves. // We don't want to do unnecessary action with the element // if an user only clicks/dblclicks on it. this.prepareEmbedding(); this._inProcessOfEmbedding = true; } this.processEmbedding(); } } this._dx = g.snapToGrid(x, grid); this._dy = g.snapToGrid(y, grid); joint.dia.CellView.prototype.pointermove.apply(this, arguments); this.notify('element:pointermove', evt, x, y); } }, pointerup: function(evt, x, y) { if (this._linkView) { // Let the linkview deal with this event. this._linkView.pointerup(evt, x, y); this._linkView = null; this.model.stopBatch('add-link'); } else { if (this._inProcessOfEmbedding) { this.finalizeEmbedding(); this._inProcessOfEmbedding = false; } this.notify('element:pointerup', evt, x, y); joint.dia.CellView.prototype.pointerup.apply(this, arguments); } } }); // JointJS diagramming library. // (c) 2011-2015 client IO // joint.dia.Link base model. // -------------------------- joint.dia.Link = joint.dia.Cell.extend({ // The default markup for links. markup: [ '', '', '', '', '', '', '', '' ].join(''), labelMarkup: [ '', '', '', '' ].join(''), toolMarkup: [ '', '', '', '', 'Remove link.', '', '', '', '', 'Link options.', '', '' ].join(''), // The default markup for showing/removing vertices. These elements are the children of the .marker-vertices element (see `this.markup`). // Only .marker-vertex and .marker-vertex-remove element have special meaning. The former is used for // dragging vertices (changin their position). The latter is used for removing vertices. vertexMarkup: [ '', '', '', '', 'Remove vertex.', '', '' ].join(''), arrowheadMarkup: [ '', '', '' ].join(''), defaults: { type: 'link', source: {}, target: {} }, isLink: function() { return true; }, disconnect: function() { return this.set({ source: g.point(0, 0), target: g.point(0, 0) }); }, // A convenient way to set labels. Currently set values will be mixined with `value` if used as a setter. label: function(idx, value) { idx = idx || 0; var labels = this.get('labels') || []; // Is it a getter? if (arguments.length === 0 || arguments.length === 1) { return labels[idx]; } var newValue = _.merge({}, labels[idx], value); var newLabels = labels.slice(); newLabels[idx] = newValue; return this.set({ labels: newLabels }); }, translate: function(tx, ty, opt) { // enrich the option object opt = opt || {}; opt.translateBy = opt.translateBy || this.id; opt.tx = tx; opt.ty = ty; return this.applyToPoints(function(p) { return { x: (p.x || 0) + tx, y: (p.y || 0) + ty }; }, opt); }, scale: function(sx, sy, origin, opt) { return this.applyToPoints(function(p) { return g.point(p).scale(sx, sy, origin).toJSON(); }, opt); }, applyToPoints: function(fn, opt) { if (!_.isFunction(fn)) { throw new TypeError('dia.Link: applyToPoints expects its first parameter to be a function.'); } var attrs = {}; var source = this.get('source'); if (!source.id) { attrs.source = fn(source); } var target = this.get('target'); if (!target.id) { attrs.target = fn(target); } var vertices = this.get('vertices'); if (vertices && vertices.length > 0) { attrs.vertices = _.map(vertices, fn); } return this.set(attrs, opt); }, reparent: function(opt) { var newParent; if (this.graph) { var source = this.graph.getCell(this.get('source').id); var target = this.graph.getCell(this.get('target').id); var prevParent = this.graph.getCell(this.get('parent')); if (source && target) { newParent = this.graph.getCommonAncestor(source, target); } if (prevParent && (!newParent || newParent.id !== prevParent.id)) { // Unembed the link if source and target has no common ancestor // or common ancestor changed prevParent.unembed(this, opt); } if (newParent) { newParent.embed(this, opt); } } return newParent; }, hasLoop: function(opt) { opt = opt || {}; var sourceId = this.get('source').id; var targetId = this.get('target').id; if (!sourceId || !targetId) { // Link "pinned" to the paper does not have a loop. return false; } var loop = sourceId === targetId; // Note that there in the deep mode a link can have a loop, // even if it connects only a parent and its embed. // A loop "target equals source" is valid in both shallow and deep mode. if (!loop && opt.deep && this.graph) { var sourceElement = this.graph.getCell(sourceId); var targetElement = this.graph.getCell(targetId); loop = sourceElement.isEmbeddedIn(targetElement) || targetElement.isEmbeddedIn(sourceElement); } return loop; }, getSourceElement: function() { var source = this.get('source'); return (source && source.id && this.graph && this.graph.getCell(source.id)) || null; }, getTargetElement: function() { var target = this.get('target'); return (target && target.id && this.graph && this.graph.getCell(target.id)) || null; }, // Returns the common ancestor for the source element, // target element and the link itself. getRelationshipAncestor: function() { var connectionAncestor; if (this.graph) { var cells = _.compact([ this, this.getSourceElement(), // null if source is a point this.getTargetElement() // null if target is a point ]); connectionAncestor = this.graph.getCommonAncestor.apply(this.graph, cells); } return connectionAncestor || null; }, // Is source, target and the link itself embedded in a given element? isRelationshipEmbeddedIn: function(element) { var elementId = _.isString(element) ? element : element.id; var ancestor = this.getRelationshipAncestor(); return !!ancestor && (ancestor.id === elementId || ancestor.isEmbeddedIn(elementId)); } }); // joint.dia.Link base view and controller. // ---------------------------------------- joint.dia.LinkView = joint.dia.CellView.extend({ className: function() { return _.unique(this.model.get('type').split('.').concat('link')).join(' '); }, options: { shortLinkLength: 100, doubleLinkTools: false, longLinkLength: 160, linkToolsOffset: 40, doubleLinkToolsOffset: 60, sampleInterval: 50 }, _z: null, initialize: function(options) { joint.dia.CellView.prototype.initialize.apply(this, arguments); // create methods in prototype, so they can be accessed from any instance and // don't need to be create over and over if (typeof this.constructor.prototype.watchSource !== 'function') { this.constructor.prototype.watchSource = this.createWatcher('source'); this.constructor.prototype.watchTarget = this.createWatcher('target'); } // `_.labelCache` is a mapping of indexes of labels in the `this.get('labels')` array to // `` nodes wrapped by Vectorizer. This allows for quick access to the // nodes in `updateLabelPosition()` in order to update the label positions. this._labelCache = {}; // keeps markers bboxes and positions again for quicker access this._markerCache = {}; // bind events this.startListening(); }, startListening: function() { var model = this.model; this.listenTo(model, 'change:markup', this.render); this.listenTo(model, 'change:smooth change:manhattan change:router change:connector', this.update); this.listenTo(model, 'change:toolMarkup', this.onToolsChange); this.listenTo(model, 'change:labels change:labelMarkup', this.onLabelsChange); this.listenTo(model, 'change:vertices change:vertexMarkup', this.onVerticesChange); this.listenTo(model, 'change:source', this.onSourceChange); this.listenTo(model, 'change:target', this.onTargetChange); }, onSourceChange: function(cell, source, opt) { // Start watching the new source model. this.watchSource(cell, source); // This handler is called when the source attribute is changed. // This can happen either when someone reconnects the link (or moves arrowhead), // or when an embedded link is translated by its ancestor. // 1. Always do update. // 2. Do update only if the opposite end ('target') is also a point. if (!opt.translateBy || !this.model.get('target').id) { opt.updateConnectionOnly = true; this.update(this.model, null, opt); } }, onTargetChange: function(cell, target, opt) { // Start watching the new target model. this.watchTarget(cell, target); // See `onSourceChange` method. if (!opt.translateBy) { opt.updateConnectionOnly = true; this.update(this.model, null, opt); } }, onVerticesChange: function(cell, changed, opt) { this.renderVertexMarkers(); // If the vertices have been changed by a translation we do update only if the link was // the only link that was translated. If the link was translated via another element which the link // is embedded in, this element will be translated as well and that triggers an update. // Note that all embeds in a model are sorted - first comes links, then elements. if (!opt.translateBy || opt.translateBy === this.model.id) { // Vertices were changed (not as a reaction on translate) // or link.translate() was called or opt.updateConnectionOnly = true; this.update(cell, null, opt); } }, onToolsChange: function() { this.renderTools().updateToolsPosition(); }, onLabelsChange: function() { this.renderLabels().updateLabelPositions(); }, // Rendering //---------- render: function() { this.$el.empty(); // A special markup can be given in the `properties.markup` property. This might be handy // if e.g. arrowhead markers should be `` elements or any other element than ``s. // `.connection`, `.connection-wrap`, `.marker-source` and `.marker-target` selectors // of elements with special meaning though. Therefore, those classes should be preserved in any // special markup passed in `properties.markup`. var model = this.model; var children = V(model.get('markup') || model.markup); // custom markup may contain only one children if (!_.isArray(children)) children = [children]; // Cache all children elements for quicker access. this._V = {}; // vectorized markup; _.each(children, function(child) { var c = child.attr('class'); c && (this._V[$.camelCase(c)] = child); }, this); // Only the connection path is mandatory if (!this._V.connection) throw new Error('link: no connection path in the markup'); // partial rendering this.renderTools(); this.renderVertexMarkers(); this.renderArrowheadMarkers(); this.vel.append(children); // rendering labels has to be run after the link is appended to DOM tree. (otherwise bbox // returns zero values) this.renderLabels(); // start watching the ends of the link for changes this.watchSource(model, model.get('source')) .watchTarget(model, model.get('target')) .update(); return this; }, renderLabels: function() { if (!this._V.labels) return this; this._labelCache = {}; var $labels = $(this._V.labels.node).empty(); var labels = this.model.get('labels') || []; if (!labels.length) return this; var labelTemplate = joint.util.template(this.model.get('labelMarkup') || this.model.labelMarkup); // This is a prepared instance of a vectorized SVGDOM node for the label element resulting from // compilation of the labelTemplate. The purpose is that all labels will just `clone()` this // node to create a duplicate. var labelNodeInstance = V(labelTemplate()); var canLabelMove = this.can('labelMove'); _.each(labels, function(label, idx) { var labelNode = labelNodeInstance.clone().node; V(labelNode).attr('label-idx', idx); if (canLabelMove) { V(labelNode).attr('cursor', 'move'); } // Cache label nodes so that the `updateLabels()` can just update the label node positions. this._labelCache[idx] = V(labelNode); var $text = $(labelNode).find('text'); var $rect = $(labelNode).find('rect'); // Text attributes with the default `text-anchor` and font-size set. var textAttributes = _.extend({ 'text-anchor': 'middle', 'font-size': 14 }, joint.util.getByPath(label, 'attrs/text', '/')); $text.attr(_.omit(textAttributes, 'text')); if (!_.isUndefined(textAttributes.text)) { V($text[0]).text(textAttributes.text + '', { annotations: textAttributes.annotations }); } // Note that we first need to append the `` element to the DOM in order to // get its bounding box. $labels.append(labelNode); // `y-alignment` - center the text element around its y coordinate. var textBbox = V($text[0]).bbox(true, $labels[0]); V($text[0]).translate(0, -textBbox.height / 2); // Add default values. var rectAttributes = _.extend({ fill: 'white', rx: 3, ry: 3 }, joint.util.getByPath(label, 'attrs/rect', '/')); $rect.attr(_.extend(rectAttributes, { x: textBbox.x, y: textBbox.y - textBbox.height / 2, // Take into account the y-alignment translation. width: textBbox.width, height: textBbox.height })); }, this); return this; }, renderTools: function() { if (!this._V.linkTools) return this; // Tools are a group of clickable elements that manipulate the whole link. // A good example of this is the remove tool that removes the whole link. // Tools appear after hovering the link close to the `source` element/point of the link // but are offset a bit so that they don't cover the `marker-arrowhead`. var $tools = $(this._V.linkTools.node).empty(); var toolTemplate = joint.util.template(this.model.get('toolMarkup') || this.model.toolMarkup); var tool = V(toolTemplate()); $tools.append(tool.node); // Cache the tool node so that the `updateToolsPosition()` can update the tool position quickly. this._toolCache = tool; // If `doubleLinkTools` is enabled, we render copy of the tools on the other side of the // link as well but only if the link is longer than `longLinkLength`. if (this.options.doubleLinkTools) { var tool2; if (this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup) { toolTemplate = joint.util.template(this.model.get('doubleToolMarkup') || this.model.doubleToolMarkup); tool2 = V(toolTemplate()); } else { tool2 = tool.clone(); } $tools.append(tool2.node); this._tool2Cache = tool2; } return this; }, renderVertexMarkers: function() { if (!this._V.markerVertices) return this; var $markerVertices = $(this._V.markerVertices.node).empty(); // A special markup can be given in the `properties.vertexMarkup` property. This might be handy // if default styling (elements) are not desired. This makes it possible to use any // SVG elements for .marker-vertex and .marker-vertex-remove tools. var markupTemplate = joint.util.template(this.model.get('vertexMarkup') || this.model.vertexMarkup); _.each(this.model.get('vertices'), function(vertex, idx) { $markerVertices.append(V(markupTemplate(_.extend({ idx: idx }, vertex))).node); }); return this; }, renderArrowheadMarkers: function() { // Custom markups might not have arrowhead markers. Therefore, jump of this function immediately if that's the case. if (!this._V.markerArrowheads) return this; var $markerArrowheads = $(this._V.markerArrowheads.node); $markerArrowheads.empty(); // A special markup can be given in the `properties.vertexMarkup` property. This might be handy // if default styling (elements) are not desired. This makes it possible to use any // SVG elements for .marker-vertex and .marker-vertex-remove tools. var markupTemplate = joint.util.template(this.model.get('arrowheadMarkup') || this.model.arrowheadMarkup); this._V.sourceArrowhead = V(markupTemplate({ end: 'source' })); this._V.targetArrowhead = V(markupTemplate({ end: 'target' })); $markerArrowheads.append(this._V.sourceArrowhead.node, this._V.targetArrowhead.node); return this; }, // Updating //--------- // Default is to process the `attrs` object and set attributes on subelements based on the selectors. update: function(model, attributes, opt) { opt = opt || {}; if (!opt.updateConnectionOnly) { // update SVG attributes defined by 'attrs/'. this.updateAttributes(); } // update the link path, label position etc. this.updateConnection(opt); this.updateLabelPositions(); this.updateToolsPosition(); this.updateArrowheadMarkers(); // Local perpendicular flag (as opposed to one defined on paper). // Could be enabled inside a connector/router. It's valid only // during the update execution. this.options.perpendicular = null; // Mark that postponed update has been already executed. this.updatePostponed = false; return this; }, updateConnection: function(opt) { opt = opt || {}; var model = this.model; var route; if (opt.translateBy && model.isRelationshipEmbeddedIn(opt.translateBy)) { // The link is being translated by an ancestor that will // shift source point, target point and all vertices // by an equal distance. var tx = opt.tx || 0; var ty = opt.ty || 0; route = this.route = _.map(this.route, function(point) { // translate point by point by delta translation return g.point(point).offset(tx, ty); }); // translate source and target connection and marker points. this._translateConnectionPoints(tx, ty); } else { // Necessary path finding route = this.route = this.findRoute(model.get('vertices') || [], opt); // finds all the connection points taking new vertices into account this._findConnectionPoints(route); } var pathData = this.getPathData(route); // The markup needs to contain a `.connection` this._V.connection.attr('d', pathData); this._V.connectionWrap && this._V.connectionWrap.attr('d', pathData); this._translateAndAutoOrientArrows(this._V.markerSource, this._V.markerTarget); }, updateAttributes: function() { // Update attributes. _.each(this.model.get('attrs'), function(attrs, selector) { var processedAttributes = []; // If the `fill` or `stroke` attribute is an object, it is in the special JointJS gradient format and so // it becomes a special attribute and is treated separately. if (_.isObject(attrs.fill)) { this.applyGradient(selector, 'fill', attrs.fill); processedAttributes.push('fill'); } if (_.isObject(attrs.stroke)) { this.applyGradient(selector, 'stroke', attrs.stroke); processedAttributes.push('stroke'); } // If the `filter` attribute is an object, it is in the special JointJS filter format and so // it becomes a special attribute and is treated separately. if (_.isObject(attrs.filter)) { this.applyFilter(selector, attrs.filter); processedAttributes.push('filter'); } // remove processed special attributes from attrs if (processedAttributes.length > 0) { processedAttributes.unshift(attrs); attrs = _.omit.apply(_, processedAttributes); } this.findBySelector(selector).attr(attrs); }, this); }, _findConnectionPoints: function(vertices) { // cache source and target points var sourcePoint, targetPoint, sourceMarkerPoint, targetMarkerPoint; var firstVertex = _.first(vertices); sourcePoint = this.getConnectionPoint( 'source', this.model.get('source'), firstVertex || this.model.get('target') ).round(); var lastVertex = _.last(vertices); targetPoint = this.getConnectionPoint( 'target', this.model.get('target'), lastVertex || sourcePoint ).round(); // Move the source point by the width of the marker taking into account // its scale around x-axis. Note that scale is the only transform that // makes sense to be set in `.marker-source` attributes object // as all other transforms (translate/rotate) will be replaced // by the `translateAndAutoOrient()` function. var cache = this._markerCache; if (this._V.markerSource) { cache.sourceBBox = cache.sourceBBox || this._V.markerSource.bbox(true); sourceMarkerPoint = g.point(sourcePoint).move( firstVertex || targetPoint, cache.sourceBBox.width * this._V.markerSource.scale().sx * -1 ).round(); } if (this._V.markerTarget) { cache.targetBBox = cache.targetBBox || this._V.markerTarget.bbox(true); targetMarkerPoint = g.point(targetPoint).move( lastVertex || sourcePoint, cache.targetBBox.width * this._V.markerTarget.scale().sx * -1 ).round(); } // if there was no markup for the marker, use the connection point. cache.sourcePoint = sourceMarkerPoint || sourcePoint; cache.targetPoint = targetMarkerPoint || targetPoint; // make connection points public this.sourcePoint = sourcePoint; this.targetPoint = targetPoint; }, _translateConnectionPoints: function(tx, ty) { var cache = this._markerCache; cache.sourcePoint.offset(tx, ty); cache.targetPoint.offset(tx, ty); this.sourcePoint.offset(tx, ty); this.targetPoint.offset(tx, ty); }, updateLabelPositions: function() { if (!this._V.labels) return this; // This method assumes all the label nodes are stored in the `this._labelCache` hash table // by their indexes in the `this.get('labels')` array. This is done in the `renderLabels()` method. var labels = this.model.get('labels') || []; if (!labels.length) return this; var connectionElement = this._V.connection.node; var connectionLength = connectionElement.getTotalLength(); // Firefox returns connectionLength=NaN in odd cases (for bezier curves). // In that case we won't update labels at all. if (!_.isNaN(connectionLength)) { var samples; _.each(labels, function(label, idx) { var position = label.position; var distance = _.isObject(position) ? position.distance : position; var offset = _.isObject(position) ? position.offset : { x: 0, y: 0 }; if (!_.isNaN(distance)) { distance = (distance > connectionLength) ? connectionLength : distance; // sanity check distance = (distance < 0) ? connectionLength + distance : distance; distance = (distance > 1) ? distance : connectionLength * distance; } else { distance = connectionLength / 2; } var labelCoordinates = connectionElement.getPointAtLength(distance); if (_.isObject(offset)) { // Just offset the label by the x,y provided in the offset object. labelCoordinates = g.point(labelCoordinates).offset(offset.x, offset.y); } else if (_.isNumber(offset)) { if (!samples) { samples = this._samples || this._V.connection.sample(this.options.sampleInterval); } // Offset the label by the amount provided in `offset` to an either // side of the link. // 1. Find the closest sample & its left and right neighbours. var minSqDistance = Infinity; var closestSample; var closestSampleIndex; var p; var sqDistance; for (var i = 0, len = samples.length; i < len; i++) { p = samples[i]; sqDistance = g.line(p, labelCoordinates).squaredLength(); if (sqDistance < minSqDistance) { minSqDistance = sqDistance; closestSample = p; closestSampleIndex = i; } } var prevSample = samples[closestSampleIndex - 1]; var nextSample = samples[closestSampleIndex + 1]; // 2. Offset the label on the perpendicular line between // the current label coordinate ("at `distance`") and // the next sample. var angle = 0; if (nextSample) { angle = g.point(labelCoordinates).theta(nextSample); } else if (prevSample) { angle = g.point(prevSample).theta(labelCoordinates); } labelCoordinates = g.point(labelCoordinates).offset(offset).rotate(labelCoordinates, angle - 90); } this._labelCache[idx].attr('transform', 'translate(' + labelCoordinates.x + ', ' + labelCoordinates.y + ')'); }, this); } return this; }, updateToolsPosition: function() { if (!this._V.linkTools) return this; // Move the tools a bit to the target position but don't cover the `sourceArrowhead` marker. // Note that the offset is hardcoded here. The offset should be always // more than the `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking // this up all the time would be slow. var scale = ''; var offset = this.options.linkToolsOffset; var connectionLength = this.getConnectionLength(); // Firefox returns connectionLength=NaN in odd cases (for bezier curves). // In that case we won't update tools position at all. if (!_.isNaN(connectionLength)) { // If the link is too short, make the tools half the size and the offset twice as low. if (connectionLength < this.options.shortLinkLength) { scale = 'scale(.5)'; offset /= 2; } var toolPosition = this.getPointAtLength(offset); this._toolCache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); if (this.options.doubleLinkTools && connectionLength >= this.options.longLinkLength) { var doubleLinkToolsOffset = this.options.doubleLinkToolsOffset || offset; toolPosition = this.getPointAtLength(connectionLength - doubleLinkToolsOffset); this._tool2Cache.attr('transform', 'translate(' + toolPosition.x + ', ' + toolPosition.y + ') ' + scale); this._tool2Cache.attr('visibility', 'visible'); } else if (this.options.doubleLinkTools) { this._tool2Cache.attr('visibility', 'hidden'); } } return this; }, updateArrowheadMarkers: function() { if (!this._V.markerArrowheads) return this; // getting bbox of an element with `display="none"` in IE9 ends up with access violation if ($.css(this._V.markerArrowheads.node, 'display') === 'none') return this; var sx = this.getConnectionLength() < this.options.shortLinkLength ? .5 : 1; this._V.sourceArrowhead.scale(sx); this._V.targetArrowhead.scale(sx); this._translateAndAutoOrientArrows(this._V.sourceArrowhead, this._V.targetArrowhead); return this; }, // Returns a function observing changes on an end of the link. If a change happens and new end is a new model, // it stops listening on the previous one and starts listening to the new one. createWatcher: function(endType) { // create handler for specific end type (source|target). var onModelChange = _.partial(this.onEndModelChange, endType); function watchEndModel(link, end) { end = end || {}; var endModel = null; var previousEnd = link.previous(endType) || {}; if (previousEnd.id) { this.stopListening(this.paper.getModelById(previousEnd.id), 'change', onModelChange); } if (end.id) { // If the observed model changes, it caches a new bbox and do the link update. endModel = this.paper.getModelById(end.id); this.listenTo(endModel, 'change', onModelChange); } onModelChange.call(this, endModel, { cacheOnly: true }); return this; } return watchEndModel; }, onEndModelChange: function(endType, endModel, opt) { var doUpdate = !opt.cacheOnly; var model = this.model; var end = model.get(endType) || {}; if (endModel) { var selector = this.constructor.makeSelector(end); var oppositeEndType = endType == 'source' ? 'target' : 'source'; var oppositeEnd = model.get(oppositeEndType) || {}; var oppositeSelector = oppositeEnd.id && this.constructor.makeSelector(oppositeEnd); // Caching end models bounding boxes. // If `opt.handleBy` equals the client-side ID of this link view and it is a loop link, then we already cached // the bounding boxes in the previous turn (e.g. for loop link, the change:source event is followed // by change:target and so on change:source, we already chached the bounding boxes of - the same - element). if (opt.handleBy === this.cid && selector == oppositeSelector) { // Source and target elements are identical. We're dealing with a loop link. We are handling `change` event for the // second time now. There is no need to calculate bbox and find magnet element again. // It was calculated already for opposite link end. this[endType + 'BBox'] = this[oppositeEndType + 'BBox']; this[endType + 'View'] = this[oppositeEndType + 'View']; this[endType + 'Magnet'] = this[oppositeEndType + 'Magnet']; } else if (opt.translateBy) { // `opt.translateBy` optimizes the way we calculate bounding box of the source/target element. // If `opt.translateBy` is an ID of the element that was originally translated. This allows us // to just offset the cached bounding box by the translation instead of calculating the bounding // box from scratch on every translate. var bbox = this[endType + 'BBox']; bbox.x += opt.tx; bbox.y += opt.ty; } else { // The slowest path, source/target could have been rotated or resized or any attribute // that affects the bounding box of the view might have been changed. var view = this.paper.findViewByModel(end.id); var magnetElement = view.el.querySelector(selector); this[endType + 'BBox'] = view.getStrokeBBox(magnetElement); this[endType + 'View'] = view; this[endType + 'Magnet'] = magnetElement; } if (opt.handleBy === this.cid && opt.translateBy && model.isEmbeddedIn(endModel) && !_.isEmpty(model.get('vertices'))) { // Loop link whose element was translated and that has vertices (that need to be translated with // the parent in which my element is embedded). // If the link is embedded, has a loop and vertices and the end model // has been translated, do not update yet. There are vertices still to be updated (change:vertices // event will come in the next turn). doUpdate = false; } if (!this.updatePostponed && oppositeEnd.id) { // The update was not postponed (that can happen e.g. on the first change event) and the opposite // end is a model (opposite end is the opposite end of the link we're just updating, e.g. if // we're reacting on change:source event, the oppositeEnd is the target model). var oppositeEndModel = this.paper.getModelById(oppositeEnd.id); // Passing `handleBy` flag via event option. // Note that if we are listening to the same model for event 'change' twice. // The same event will be handled by this method also twice. if (end.id === oppositeEnd.id) { // We're dealing with a loop link. Tell the handlers in the next turn that they should update // the link instead of me. (We know for sure there will be a next turn because // loop links react on at least two events: change on the source model followed by a change on // the target model). opt.handleBy = this.cid; } if (opt.handleBy === this.cid || (opt.translateBy && oppositeEndModel.isEmbeddedIn(opt.translateBy))) { // Here are two options: // - Source and target are connected to the same model (not necessarily the same port). // - Both end models are translated by the same ancestor. We know that opposite end // model will be translated in the next turn as well. // In both situations there will be more changes on the model that trigger an // update. So there is no need to update the linkView yet. this.updatePostponed = true; doUpdate = false; } } } else { // the link end is a point ~ rect 1x1 this[endType + 'BBox'] = g.rect(end.x || 0, end.y || 0, 1, 1); this[endType + 'View'] = this[endType + 'Magnet'] = null; } if (doUpdate) { opt.updateConnectionOnly = true; this.update(model, null, opt); } }, _translateAndAutoOrientArrows: function(sourceArrow, targetArrow) { // Make the markers "point" to their sticky points being auto-oriented towards // `targetPosition`/`sourcePosition`. And do so only if there is a markup for them. if (sourceArrow) { sourceArrow.translateAndAutoOrient( this.sourcePoint, _.first(this.route) || this.targetPoint, this.paper.viewport ); } if (targetArrow) { targetArrow.translateAndAutoOrient( this.targetPoint, _.last(this.route) || this.sourcePoint, this.paper.viewport ); } }, removeVertex: function(idx) { var vertices = _.clone(this.model.get('vertices')); if (vertices && vertices.length) { vertices.splice(idx, 1); this.model.set('vertices', vertices, { ui: true }); } return this; }, // This method ads a new vertex to the `vertices` array of `.connection`. This method // uses a heuristic to find the index at which the new `vertex` should be placed at assuming // the new vertex is somewhere on the path. addVertex: function(vertex) { // As it is very hard to find a correct index of the newly created vertex, // a little heuristics is taking place here. // The heuristics checks if length of the newly created // path is lot more than length of the old path. If this is the case, // new vertex was probably put into a wrong index. // Try to put it into another index and repeat the heuristics again. var vertices = (this.model.get('vertices') || []).slice(); // Store the original vertices for a later revert if needed. var originalVertices = vertices.slice(); // A `` element used to compute the length of the path during heuristics. var path = this._V.connection.node.cloneNode(false); // Length of the original path. var originalPathLength = path.getTotalLength(); // Current path length. var pathLength; // Tolerance determines the highest possible difference between the length // of the old and new path. The number has been chosen heuristically. var pathLengthTolerance = 20; // Total number of vertices including source and target points. var idx = vertices.length + 1; // Loop through all possible indexes and check if the difference between // path lengths changes significantly. If not, the found index is // most probably the right one. while (idx--) { vertices.splice(idx, 0, vertex); V(path).attr('d', this.getPathData(this.findRoute(vertices))); pathLength = path.getTotalLength(); // Check if the path lengths changed significantly. if (pathLength - originalPathLength > pathLengthTolerance) { // Revert vertices to the original array. The path length has changed too much // so that the index was not found yet. vertices = originalVertices.slice(); } else { break; } } if (idx === -1) { // If no suitable index was found for such a vertex, make the vertex the first one. idx = 0; vertices.splice(idx, 0, vertex); } this.model.set('vertices', vertices, { ui: true }); return idx; }, // Send a token (an SVG element, usually a circle) along the connection path. // Example: `paper.findViewByModel(link).sendToken(V('circle', { r: 7, fill: 'green' }).node)` // `duration` is optional and is a time in milliseconds that the token travels from the source to the target of the link. Default is `1000`. // `callback` is optional and is a function to be called once the token reaches the target. sendToken: function(token, duration, callback) { duration = duration || 1000; V(this.paper.viewport).append(token); V(token).animateAlongPath({ dur: duration + 'ms', repeatCount: 1 }, this._V.connection.node); _.delay(function() { V(token).remove(); callback && callback(); }, duration); }, findRoute: function(oldVertices) { var namespace = joint.routers; var router = this.model.get('router'); var defaultRouter = this.paper.options.defaultRouter; if (!router) { if (this.model.get('manhattan')) { // backwards compability router = { name: 'orthogonal' }; } else if (defaultRouter) { router = defaultRouter; } else { return oldVertices; } } var args = router.args || {}; var routerFn = _.isFunction(router) ? router : namespace[router.name]; if (!_.isFunction(routerFn)) { throw new Error('unknown router: "' + router.name + '"'); } var newVertices = routerFn.call(this, oldVertices || [], args, this); return newVertices; }, // Return the `d` attribute value of the `` element representing the link // between `source` and `target`. getPathData: function(vertices) { var namespace = joint.connectors; var connector = this.model.get('connector'); var defaultConnector = this.paper.options.defaultConnector; if (!connector) { // backwards compability if (this.model.get('smooth')) { connector = { name: 'smooth' }; } else { connector = defaultConnector || {}; } } var connectorFn = _.isFunction(connector) ? connector : namespace[connector.name]; var args = connector.args || {}; if (!_.isFunction(connectorFn)) { throw new Error('unknown connector: "' + connector.name + '"'); } var pathData = connectorFn.call( this, this._markerCache.sourcePoint, // Note that the value is translated by the size this._markerCache.targetPoint, // of the marker. (We'r not using this.sourcePoint) vertices || (this.model.get('vertices') || {}), args, // options this ); return pathData; }, // Find a point that is the start of the connection. // If `selectorOrPoint` is a point, then we're done and that point is the start of the connection. // If the `selectorOrPoint` is an element however, we need to know a reference point (or element) // that the link leads to in order to determine the start of the connection on the original element. getConnectionPoint: function(end, selectorOrPoint, referenceSelectorOrPoint) { var spot; // If the `selectorOrPoint` (or `referenceSelectorOrPoint`) is `undefined`, the `source`/`target` of the link model is `undefined`. // We want to allow this however so that one can create links such as `var link = new joint.dia.Link` and // set the `source`/`target` later. _.isEmpty(selectorOrPoint) && (selectorOrPoint = { x: 0, y: 0 }); _.isEmpty(referenceSelectorOrPoint) && (referenceSelectorOrPoint = { x: 0, y: 0 }); if (!selectorOrPoint.id) { // If the source is a point, we don't need a reference point to find the sticky point of connection. spot = g.point(selectorOrPoint); } else { // If the source is an element, we need to find a point on the element boundary that is closest // to the reference point (or reference element). // Get the bounding box of the spot relative to the paper viewport. This is necessary // in order to follow paper viewport transformations (scale/rotate). // `_sourceBbox` (`_targetBbox`) comes from `_sourceBboxUpdate` (`_sourceBboxUpdate`) // method, it exists since first render and are automatically updated var spotBbox = end === 'source' ? this.sourceBBox : this.targetBBox; var reference; if (!referenceSelectorOrPoint.id) { // Reference was passed as a point, therefore, we're ready to find the sticky point of connection on the source element. reference = g.point(referenceSelectorOrPoint); } else { // Reference was passed as an element, therefore we need to find a point on the reference // element boundary closest to the source element. // Get the bounding box of the spot relative to the paper viewport. This is necessary // in order to follow paper viewport transformations (scale/rotate). var referenceBbox = end === 'source' ? this.targetBBox : this.sourceBBox; reference = g.rect(referenceBbox).intersectionWithLineFromCenterToPoint(g.rect(spotBbox).center()); reference = reference || g.rect(referenceBbox).center(); } // If `perpendicularLinks` flag is set on the paper and there are vertices // on the link, then try to find a connection point that makes the link perpendicular // even though the link won't point to the center of the targeted object. if (this.paper.options.perpendicularLinks || this.options.perpendicular) { var horizontalLineRect = g.rect(0, reference.y, this.paper.options.width, 1); var verticalLineRect = g.rect(reference.x, 0, 1, this.paper.options.height); var nearestSide; if (horizontalLineRect.intersect(g.rect(spotBbox))) { nearestSide = g.rect(spotBbox).sideNearestToPoint(reference); switch (nearestSide) { case 'left': spot = g.point(spotBbox.x, reference.y); break; case 'right': spot = g.point(spotBbox.x + spotBbox.width, reference.y); break; default: spot = g.rect(spotBbox).center(); break; } } else if (verticalLineRect.intersect(g.rect(spotBbox))) { nearestSide = g.rect(spotBbox).sideNearestToPoint(reference); switch (nearestSide) { case 'top': spot = g.point(reference.x, spotBbox.y); break; case 'bottom': spot = g.point(reference.x, spotBbox.y + spotBbox.height); break; default: spot = g.rect(spotBbox).center(); break; } } else { // If there is no intersection horizontally or vertically with the object bounding box, // then we fall back to the regular situation finding straight line (not perpendicular) // between the object and the reference point. spot = g.rect(spotBbox).intersectionWithLineFromCenterToPoint(reference); spot = spot || g.rect(spotBbox).center(); } } else if (this.paper.options.linkConnectionPoint) { var view = end === 'target' ? this.targetView : this.sourceView; var magnet = end === 'target' ? this.targetMagnet : this.sourceMagnet; spot = this.paper.options.linkConnectionPoint(this, view, magnet, reference); } else { spot = g.rect(spotBbox).intersectionWithLineFromCenterToPoint(reference); spot = spot || g.rect(spotBbox).center(); } } return spot; }, // Public API // ---------- getConnectionLength: function() { return this._V.connection.node.getTotalLength(); }, getPointAtLength: function(length) { return this._V.connection.node.getPointAtLength(length); }, // Interaction. The controller part. // --------------------------------- _beforeArrowheadMove: function() { this._z = this.model.get('z'); this.model.toFront(); // Let the pointer propagate throught the link view elements so that // the `evt.target` is another element under the pointer, not the link itself. this.el.style.pointerEvents = 'none'; if (this.paper.options.markAvailable) { this._markAvailableMagnets(); } }, _afterArrowheadMove: function() { if (!_.isNull(this._z)) { this.model.set('z', this._z, { ui: true }); this._z = null; } // Put `pointer-events` back to its original value. See `startArrowheadMove()` for explanation. // Value `auto` doesn't work in IE9. We force to use `visiblePainted` instead. // See `https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events`. this.el.style.pointerEvents = 'visiblePainted'; if (this.paper.options.markAvailable) { this._unmarkAvailableMagnets(); } }, _createValidateConnectionArgs: function(arrowhead) { // It makes sure the arguments for validateConnection have the following form: // (source view, source magnet, target view, target magnet and link view) var args = []; args[4] = arrowhead; args[5] = this; var oppositeArrowhead; var i = 0; var j = 0; if (arrowhead === 'source') { i = 2; oppositeArrowhead = 'target'; } else { j = 2; oppositeArrowhead = 'source'; } var end = this.model.get(oppositeArrowhead); if (end.id) { args[i] = this.paper.findViewByModel(end.id); args[i + 1] = end.selector && args[i].el.querySelector(end.selector); } function validateConnectionArgs(cellView, magnet) { args[j] = cellView; args[j + 1] = cellView.el === magnet ? undefined : magnet; return args; } return validateConnectionArgs; }, _markAvailableMagnets: function() { function isMagnetAvailable(view, magnet) { var paper = view.paper; var validate = paper.options.validateConnection; return validate.apply(paper, this._validateConnectionArgs(view, magnet)); } var paper = this.paper; var elements = paper.model.getElements(); this._marked = {}; _.chain(elements).map(paper.findViewByModel, paper).each(function(view) { var magnets = Array.prototype.slice.call(view.el.querySelectorAll('[magnet]')); if (view.el.getAttribute('magnet') !== 'false') { // Element wrapping group is also a magnet magnets.push(view.el); } var availableMagnets = _.filter(magnets, _.partial(isMagnetAvailable, view), this); if (availableMagnets.length > 0) { // highlight all available magnets _.each(availableMagnets, _.partial(view.highlight, _, { magnetAvailability: true }), view); // highlight the entire view view.highlight(null, { elementAvailability: true }); this._marked[view.model.id] = availableMagnets; } }, this).value(); }, _unmarkAvailableMagnets: function() { _.each(this._marked, function(markedMagnets, id) { var view = this.paper.findViewByModel(id); if (view) { _.each(markedMagnets, _.partial(view.unhighlight, _, { magnetAvailability: true }), view); view.unhighlight(null, { elementAvailability: true }); } }, this); this._marked = null; }, startArrowheadMove: function(end, opt) { opt = _.defaults(opt || {}, { whenNotAllowed: 'revert' }); // Allow to delegate events from an another view to this linkView in order to trigger arrowhead // move without need to click on the actual arrowhead dom element. this._action = 'arrowhead-move'; this._whenNotAllowed = opt.whenNotAllowed; this._arrowhead = end; this._initialEnd = _.clone(this.model.get(end)) || { x: 0, y: 0 }; this._validateConnectionArgs = this._createValidateConnectionArgs(this._arrowhead); this._beforeArrowheadMove(); }, pointerdown: function(evt, x, y) { joint.dia.CellView.prototype.pointerdown.apply(this, arguments); this.notify('link:pointerdown', evt, x, y); this._dx = x; this._dy = y; // if are simulating pointerdown on a link during a magnet click, skip link interactions if (evt.target.getAttribute('magnet') != null) return; var className = evt.target.getAttribute('class'); var parentClassName = evt.target.parentNode.getAttribute('class'); var labelNode; if (parentClassName === 'label') { className = parentClassName; labelNode = evt.target.parentNode; } else { labelNode = evt.target; } switch (className) { case 'marker-vertex': if (this.can('vertexMove')) { this._action = 'vertex-move'; this._vertexIdx = evt.target.getAttribute('idx'); } break; case 'marker-vertex-remove': case 'marker-vertex-remove-area': if (this.can('vertexRemove')) { this.removeVertex(evt.target.getAttribute('idx')); } break; case 'marker-arrowhead': if (this.can('arrowheadMove')) { this.startArrowheadMove(evt.target.getAttribute('end')); } break; case 'label': if (this.can('labelMove')) { this._action = 'label-move'; this._labelIdx = parseInt(V(labelNode).attr('label-idx'), 10); // Precalculate samples so that we don't have to do that // over and over again while dragging the label. this._samples = this._V.connection.sample(1); this._linkLength = this._V.connection.node.getTotalLength(); } break; default: var targetParentEvent = evt.target.parentNode.getAttribute('event'); if (targetParentEvent) { if (this.can('useLinkTools')) { // `remove` event is built-in. Other custom events are triggered on the paper. if (targetParentEvent === 'remove') { this.model.remove(); } else { this.notify(targetParentEvent, evt, x, y); } } } else { if (this.can('vertexAdd')) { // Store the index at which the new vertex has just been placed. // We'll be update the very same vertex position in `pointermove()`. this._vertexIdx = this.addVertex({ x: x, y: y }); this._action = 'vertex-move'; } } } }, pointermove: function(evt, x, y) { switch (this._action) { case 'vertex-move': var vertices = _.clone(this.model.get('vertices')); vertices[this._vertexIdx] = { x: x, y: y }; this.model.set('vertices', vertices, { ui: true }); break; case 'label-move': var dragPoint = { x: x, y: y }; var label = this.model.get('labels')[this._labelIdx]; var samples = this._samples; var minSqDistance = Infinity; var closestSample; var closestSampleIndex; var p; var sqDistance; for (var i = 0, len = samples.length; i < len; i++) { p = samples[i]; sqDistance = g.line(p, dragPoint).squaredLength(); if (sqDistance < minSqDistance) { minSqDistance = sqDistance; closestSample = p; closestSampleIndex = i; } } var prevSample = samples[closestSampleIndex - 1]; var nextSample = samples[closestSampleIndex + 1]; var closestSampleDistance = g.point(closestSample).distance(dragPoint); var offset = 0; if (prevSample && nextSample) { offset = g.line(prevSample, nextSample).pointOffset(dragPoint); } else if (prevSample) { offset = g.line(prevSample, closestSample).pointOffset(dragPoint); } else if (nextSample) { offset = g.line(closestSample, nextSample).pointOffset(dragPoint); } this.model.label(this._labelIdx, { position: { distance: closestSample.distance / this._linkLength, offset: offset } }); break; case 'arrowhead-move': if (this.paper.options.snapLinks) { // checking view in close area of the pointer var r = this.paper.options.snapLinks.radius || 50; var viewsInArea = this.paper.findViewsInArea({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); if (this._closestView) { this._closestView.unhighlight(this._closestEnd.selector, { connecting: true, snapping: true }); } this._closestView = this._closestEnd = null; var distance; var minDistance = Number.MAX_VALUE; var pointer = g.point(x, y); _.each(viewsInArea, function(view) { // skip connecting to the element in case '.': { magnet: false } attribute present if (view.el.getAttribute('magnet') !== 'false') { // find distance from the center of the model to pointer coordinates distance = view.model.getBBox().center().distance(pointer); // the connection is looked up in a circle area by `distance < r` if (distance < r && distance < minDistance) { if (this.paper.options.validateConnection.apply( this.paper, this._validateConnectionArgs(view, null) )) { minDistance = distance; this._closestView = view; this._closestEnd = { id: view.model.id }; } } } view.$('[magnet]').each(_.bind(function(index, magnet) { var bbox = V(magnet).bbox(false, this.paper.viewport); distance = pointer.distance({ x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 }); if (distance < r && distance < minDistance) { if (this.paper.options.validateConnection.apply( this.paper, this._validateConnectionArgs(view, magnet) )) { minDistance = distance; this._closestView = view; this._closestEnd = { id: view.model.id, selector: view.getSelector(magnet), port: magnet.getAttribute('port') }; } } }, this)); }, this); if (this._closestView) { this._closestView.highlight(this._closestEnd.selector, { connecting: true, snapping: true }); } this.model.set(this._arrowhead, this._closestEnd || { x: x, y: y }, { ui: true }); } else { // checking views right under the pointer // Touchmove event's target is not reflecting the element under the coordinates as mousemove does. // It holds the element when a touchstart triggered. var target = (evt.type === 'mousemove') ? evt.target : document.elementFromPoint(evt.clientX, evt.clientY); if (this._targetEvent !== target) { // Unhighlight the previous view under pointer if there was one. if (this._magnetUnderPointer) { this._viewUnderPointer.unhighlight(this._magnetUnderPointer, { connecting: true }); } this._viewUnderPointer = this.paper.findView(target); if (this._viewUnderPointer) { // If we found a view that is under the pointer, we need to find the closest // magnet based on the real target element of the event. this._magnetUnderPointer = this._viewUnderPointer.findMagnet(target); if (this._magnetUnderPointer && this.paper.options.validateConnection.apply( this.paper, this._validateConnectionArgs(this._viewUnderPointer, this._magnetUnderPointer) )) { // If there was no magnet found, do not highlight anything and assume there // is no view under pointer we're interested in reconnecting to. // This can only happen if the overall element has the attribute `'.': { magnet: false }`. if (this._magnetUnderPointer) { this._viewUnderPointer.highlight(this._magnetUnderPointer, { connecting: true }); } } else { // This type of connection is not valid. Disregard this magnet. this._magnetUnderPointer = null; } } else { // Make sure we'll unset previous magnet. this._magnetUnderPointer = null; } } this._targetEvent = target; this.model.set(this._arrowhead, { x: x, y: y }, { ui: true }); } break; } this._dx = x; this._dy = y; joint.dia.CellView.prototype.pointermove.apply(this, arguments); this.notify('link:pointermove', evt, x, y); }, pointerup: function(evt, x, y) { if (this._action === 'label-move') { this._samples = null; } else if (this._action === 'arrowhead-move') { var paperOptions = this.paper.options; var arrowhead = this._arrowhead; if (paperOptions.snapLinks) { // Finish off link snapping. // Everything except view unhighlighting was already done on pointermove. if (this._closestView) { this._closestView.unhighlight(this._closestEnd.selector, { connecting: true, snapping: true }); } this._closestView = this._closestEnd = null; } else { var viewUnderPointer = this._viewUnderPointer; var magnetUnderPointer = this._magnetUnderPointer; this._viewUnderPointer = null; this._magnetUnderPointer = null; if (magnetUnderPointer) { viewUnderPointer.unhighlight(magnetUnderPointer, { connecting: true }); // Find a unique `selector` of the element under pointer that is a magnet. If the // `this._magnetUnderPointer` is the root element of the `this._viewUnderPointer` itself, // the returned `selector` will be `undefined`. That means we can directly pass it to the // `source`/`target` attribute of the link model below. var selector = viewUnderPointer.getSelector(magnetUnderPointer); var port = magnetUnderPointer.getAttribute('port'); var arrowheadValue = { id: viewUnderPointer.model.id }; if (selector != null) arrowheadValue.port = port; if (port != null) arrowheadValue.selector = selector; this.model.set(arrowhead, arrowheadValue, { ui: true }); } } // If the changed link is not allowed, revert to its previous state. if (!this.paper.linkAllowed(this)) { switch (this._whenNotAllowed) { case 'remove': this.model.remove(); break; case 'revert': default: this.model.set(arrowhead, this._initialEnd, { ui: true }); break; } } // Reparent the link if embedding is enabled if (paperOptions.embeddingMode && this.model.reparent()) { // Make sure we don't reverse to the original 'z' index (see afterArrowheadMove()). this._z = null; } this._afterArrowheadMove(); } this._action = null; this._whenNotAllowed = null; this.notify('link:pointerup', evt, x, y); joint.dia.CellView.prototype.pointerup.apply(this, arguments); } }, { makeSelector: function(end) { var selector = '[model-id="' + end.id + '"]'; // `port` has a higher precendence over `selector`. This is because the selector to the magnet // might change while the name of the port can stay the same. if (end.port) { selector += ' [port="' + end.port + '"]'; } else if (end.selector) { selector += ' ' + end.selector; } return selector; } }); // JointJS library. // (c) 2011-2015 client IO joint.dia.Paper = joint.mvc.View.extend({ className: 'paper', options: { width: 800, height: 600, origin: { x: 0, y: 0 }, // x,y coordinates in top-left corner gridSize: 1, /* Whether or not to draw the grid lines on the paper's DOM element. e.g drawGrid: true, drawGrid: { color: 'red', thickness: 2 } */ drawGrid: false, perpendicularLinks: false, elementView: joint.dia.ElementView, linkView: joint.dia.LinkView, snapLinks: false, // false, true, { radius: value } // When set to FALSE, an element may not have more than 1 link with the same source and target element. multiLinks: true, // For adding custom guard logic. guard: function(evt, view) { // FALSE means the event isn't guarded. return false; }, highlighting: { 'default': { name: 'stroke', options: { padding: 3 } }, magnetAvailability: { name: 'addClass', options: { className: 'available-magnet' } }, elementAvailability: { name: 'addClass', options: { className: 'available-cell' } } }, // Prevent the default context menu from being displayed. preventContextMenu: true, // Restrict the translation of elements by given bounding box. // Option accepts a boolean: // true - the translation is restricted to the paper area // false - no restrictions // A method: // restrictTranslate: function(elementView) { // var parentId = elementView.model.get('parent'); // return parentId && this.model.getCell(parentId).getBBox(); // }, // Or a bounding box: // restrictTranslate: { x: 10, y: 10, width: 790, height: 590 } restrictTranslate: false, // Marks all available magnets with 'available-magnet' class name and all available cells with // 'available-cell' class name. Marks them when dragging a link is started and unmark // when the dragging is stopped. markAvailable: false, // Defines what link model is added to the graph after an user clicks on an active magnet. // Value could be the Backbone.model or a function returning the Backbone.model // defaultLink: function(elementView, magnet) { return condition ? new customLink1() : new customLink2() } defaultLink: new joint.dia.Link, // A connector that is used by links with no connector defined on the model. // e.g. { name: 'rounded', args: { radius: 5 }} or a function defaultConnector: { name: 'normal' }, // A router that is used by links with no router defined on the model. // e.g. { name: 'oneSide', args: { padding: 10 }} or a function defaultRouter: { name: 'normal' }, /* CONNECTING */ // Check whether to add a new link to the graph when user clicks on an a magnet. validateMagnet: function(cellView, magnet) { return magnet.getAttribute('magnet') !== 'passive'; }, // Check whether to allow or disallow the link connection while an arrowhead end (source/target) // being changed. validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { return (end === 'target' ? cellViewT : cellViewS) instanceof joint.dia.ElementView; }, /* EMBEDDING */ // Enables embedding. Reparents the dragged element with elements under it and makes sure that // all links and elements are visible taken the level of embedding into account. embeddingMode: false, // Check whether to allow or disallow the element embedding while an element being translated. validateEmbedding: function(childView, parentView) { // by default all elements can be in relation child-parent return true; }, // Determines the way how a cell finds a suitable parent when it's dragged over the paper. // The cell with the highest z-index (visually on the top) will be choosen. findParentBy: 'bbox', // 'bbox'|'center'|'origin'|'corner'|'topRight'|'bottomLeft' // If enabled only the element on the very front is taken into account for the embedding. // If disabled the elements under the dragged view are tested one by one // (from front to back) until a valid parent found. frontParentOnly: true, // Interactive flags. See online docs for the complete list of interactive flags. interactive: { labelMove: false }, // When set to true the links can be pinned to the paper. // i.e. link source/target can be a point e.g. link.get('source') ==> { x: 100, y: 100 }; linkPinning: true, // Allowed number of mousemove events after which the pointerclick event will be still triggered. clickThreshold: 0, // The namespace, where all the cell views are defined. cellViewNamespace: joint.shapes, // The namespace, where all the cell views are defined. highlighterNamespace: joint.highlighters }, events: { 'mousedown': 'pointerdown', 'dblclick': 'mousedblclick', 'click': 'mouseclick', 'touchstart': 'pointerdown', 'touchend': 'mouseclick', 'touchmove': 'pointermove', 'mousemove': 'pointermove', 'mouseover .element': 'cellMouseover', 'mouseover .link': 'cellMouseover', 'mouseout .element': 'cellMouseout', 'mouseout .link': 'cellMouseout', 'contextmenu': 'contextmenu', 'mousewheel': 'mousewheel', 'DOMMouseScroll': 'mousewheel' }, _highlights: [], init: function() { _.bindAll(this, 'pointerup'); this.model = this.options.model || new joint.dia.Graph; // This is a fix for the case where two papers share the same options. // Changing origin.x for one paper would change the value of origin.x for the other. // This prevents that behavior. this.options.origin = _.clone(this.options.origin); this.options.defaultConnector = _.clone(this.options.defaultConnector); // Return default highlighting options into the user specified options. _.defaults(this.options.highlighting, this.constructor.prototype.options.highlighting); this.options.highlighting = _.cloneDeep(this.options.highlighting); this.svg = V('svg').node; this.viewport = V('g').addClass('viewport').node; this.defs = V('defs').node; // Append `` element to the SVG document. This is useful for filters and gradients. V(this.svg).append([this.viewport, this.defs]); this.$el.append(this.svg); this.model.on('add', this.onCellAdded, this); this.model.on('remove', this.removeView, this); this.model.on('reset', this.resetViews, this); this.model.on('sort', this._onSort, this); this.model.on('batch:stop', this._onBatchStop, this); this.setOrigin(); this.setDimensions(); $(document).on('mouseup touchend', this.pointerup); // Hold the value when mouse has been moved: when mouse moved, no click event will be triggered. this._mousemoved = 0; // Hash of all cell views. this._views = {}; this.on('cell:highlight', this.onCellHighlight, this); this.on('cell:unhighlight', this.onCellUnhighlight, this); }, _onSort: function() { if (!this.model.hasActiveBatch('add')) { this.sortViews(); } }, _onBatchStop: function(data) { var name = data && data.batchName; if (name === 'add' && !this.model.hasActiveBatch('add')) { this.sortViews(); } }, onRemove: function() { //clean up all DOM elements/views to prevent memory leaks this.removeViews(); $(document).off('mouseup touchend', this.pointerup); }, setDimensions: function(width, height) { width = this.options.width = width || this.options.width; height = this.options.height = height || this.options.height; V(this.svg).attr({ width: width, height: height }); this.trigger('resize', width, height); }, setOrigin: function(ox, oy) { this.options.origin.x = ox || 0; this.options.origin.y = oy || 0; V(this.viewport).translate(ox, oy, { absolute: true }); this.trigger('translate', ox, oy); if (this.options.drawGrid) { this.drawGrid(); } }, // Expand/shrink the paper to fit the content. Snap the width/height to the grid // defined in `gridWidth`, `gridHeight`. `padding` adds to the resulting width/height of the paper. // When options { fitNegative: true } it also translates the viewport in order to make all // the content visible. fitToContent: function(gridWidth, gridHeight, padding, opt) { // alternatively function(opt) if (_.isObject(gridWidth)) { // first parameter is an option object opt = gridWidth; gridWidth = opt.gridWidth || 1; gridHeight = opt.gridHeight || 1; padding = opt.padding || 0; } else { opt = opt || {}; gridWidth = gridWidth || 1; gridHeight = gridHeight || 1; padding = padding || 0; } padding = joint.util.normalizeSides(padding); // Calculate the paper size to accomodate all the graph's elements. var bbox = V(this.viewport).bbox(true, this.svg); var currentScale = V(this.viewport).scale(); bbox.x *= currentScale.sx; bbox.y *= currentScale.sy; bbox.width *= currentScale.sx; bbox.height *= currentScale.sy; var calcWidth = Math.max(Math.ceil((bbox.width + bbox.x) / gridWidth), 1) * gridWidth; var calcHeight = Math.max(Math.ceil((bbox.height + bbox.y) / gridHeight), 1) * gridHeight; var tx = 0; var ty = 0; if ((opt.allowNewOrigin == 'negative' && bbox.x < 0) || (opt.allowNewOrigin == 'positive' && bbox.x >= 0) || opt.allowNewOrigin == 'any') { tx = Math.ceil(-bbox.x / gridWidth) * gridWidth; tx += padding.left; calcWidth += tx; } if ((opt.allowNewOrigin == 'negative' && bbox.y < 0) || (opt.allowNewOrigin == 'positive' && bbox.y >= 0) || opt.allowNewOrigin == 'any') { ty = Math.ceil(-bbox.y / gridHeight) * gridHeight; ty += padding.top; calcHeight += ty; } calcWidth += padding.right; calcHeight += padding.bottom; // Make sure the resulting width and height are greater than minimum. calcWidth = Math.max(calcWidth, opt.minWidth || 0); calcHeight = Math.max(calcHeight, opt.minHeight || 0); // Make sure the resulting width and height are lesser than maximum. calcWidth = Math.min(calcWidth, opt.maxWidth || Number.MAX_VALUE); calcHeight = Math.min(calcHeight, opt.maxHeight || Number.MAX_VALUE); var dimensionChange = calcWidth != this.options.width || calcHeight != this.options.height; var originChange = tx != this.options.origin.x || ty != this.options.origin.y; // Change the dimensions only if there is a size discrepency or an origin change if (originChange) { this.setOrigin(tx, ty); } if (dimensionChange) { this.setDimensions(calcWidth, calcHeight); } }, scaleContentToFit: function(opt) { var contentBBox = this.getContentBBox(); if (!contentBBox.width || !contentBBox.height) return; opt = opt || {}; _.defaults(opt, { padding: 0, preserveAspectRatio: true, scaleGrid: null, minScale: 0, maxScale: Number.MAX_VALUE //minScaleX //minScaleY //maxScaleX //maxScaleY //fittingBBox }); var padding = opt.padding; var minScaleX = opt.minScaleX || opt.minScale; var maxScaleX = opt.maxScaleX || opt.maxScale; var minScaleY = opt.minScaleY || opt.minScale; var maxScaleY = opt.maxScaleY || opt.maxScale; var fittingBBox = opt.fittingBBox || ({ x: this.options.origin.x, y: this.options.origin.y, width: this.options.width, height: this.options.height }); fittingBBox = g.rect(fittingBBox).moveAndExpand({ x: padding, y: padding, width: -2 * padding, height: -2 * padding }); var currentScale = V(this.viewport).scale(); var newSx = fittingBBox.width / contentBBox.width * currentScale.sx; var newSy = fittingBBox.height / contentBBox.height * currentScale.sy; if (opt.preserveAspectRatio) { newSx = newSy = Math.min(newSx, newSy); } // snap scale to a grid if (opt.scaleGrid) { var gridSize = opt.scaleGrid; newSx = gridSize * Math.floor(newSx / gridSize); newSy = gridSize * Math.floor(newSy / gridSize); } // scale min/max boundaries newSx = Math.min(maxScaleX, Math.max(minScaleX, newSx)); newSy = Math.min(maxScaleY, Math.max(minScaleY, newSy)); this.scale(newSx, newSy); var contentTranslation = this.getContentBBox(); var newOx = fittingBBox.x - contentTranslation.x; var newOy = fittingBBox.y - contentTranslation.y; this.setOrigin(newOx, newOy); }, getContentBBox: function() { var crect = this.viewport.getBoundingClientRect(); // Using Screen CTM was the only way to get the real viewport bounding box working in both // Google Chrome and Firefox. var screenCTM = this.viewport.getScreenCTM(); // for non-default origin we need to take the viewport translation into account var viewportCTM = this.viewport.getCTM(); return g.rect({ x: crect.left - screenCTM.e + viewportCTM.e, y: crect.top - screenCTM.f + viewportCTM.f, width: crect.width, height: crect.height }); }, // Returns a geometry rectangle represeting the entire // paper area (coordinates from the left paper border to the right one // and the top border to the bottom one). getArea: function() { var transformationMatrix = this.viewport.getCTM().inverse(); var noTransformationBBox = { x: 0, y: 0, width: this.options.width, height: this.options.height }; return g.rect(V.transformRect(noTransformationBBox, transformationMatrix)); }, getRestrictedArea: function() { var restrictedArea; if (_.isFunction(this.options.restrictTranslate)) { // A method returning a bounding box restrictedArea = this.options.restrictTranslate.apply(this, arguments); } else if (this.options.restrictTranslate === true) { // The paper area restrictedArea = this.getArea(); } else { // Either false or a bounding box restrictedArea = this.options.restrictTranslate || null; } return restrictedArea; }, createViewForModel: function(cell) { // A class taken from the paper options. var optionalViewClass; // A default basic class (either dia.ElementView or dia.LinkView) var defaultViewClass; // A special class defined for this model in the corresponding namespace. // e.g. joint.shapes.basic.Rect searches for joint.shapes.basic.RectView var namespace = this.options.cellViewNamespace; var type = cell.get('type') + 'View'; var namespaceViewClass = joint.util.getByPath(namespace, type, '.'); if (cell.isLink()) { optionalViewClass = this.options.linkView; defaultViewClass = joint.dia.LinkView; } else { optionalViewClass = this.options.elementView; defaultViewClass = joint.dia.ElementView; } // a) the paper options view is a class (deprecated) // 1. search the namespace for a view // 2. if no view was found, use view from the paper options // b) the paper options view is a function // 1. call the function from the paper options // 2. if no view was return, search the namespace for a view // 3. if no view was found, use the default var ViewClass = (optionalViewClass.prototype instanceof Backbone.View) ? namespaceViewClass || optionalViewClass : optionalViewClass.call(this, cell) || namespaceViewClass || defaultViewClass; return new ViewClass({ model: cell, interactive: this.options.interactive }); }, onCellAdded: function(cell, graph, opt) { if (this.options.async && opt.async !== false && _.isNumber(opt.position)) { this._asyncCells = this._asyncCells || []; this._asyncCells.push(cell); if (opt.position == 0) { if (this._frameId) throw new Error('another asynchronous rendering in progress'); this.asyncRenderViews(this._asyncCells, opt); delete this._asyncCells; } } else { this.renderView(cell); } }, removeView: function(cell) { var view = this._views[cell.id]; if (view) { view.remove(); delete this._views[cell.id]; } return view; }, renderView: function(cell) { var view = this._views[cell.id] = this.createViewForModel(cell); V(this.viewport).append(view.el); view.paper = this; view.render(); // This is the only way to prevent image dragging in Firefox that works. // Setting -moz-user-select: none, draggable="false" attribute or user-drag: none didn't help. $(view.el).find('image').on('dragstart', function() { return false; }); return view; }, beforeRenderViews: function(cells) { // Make sure links are always added AFTER elements. // They wouldn't find their sources/targets in the DOM otherwise. cells.sort(function(a) { return a instanceof joint.dia.Link ? 1 : -1; }); return cells; }, afterRenderViews: function() { this.sortViews(); }, resetViews: function(cellsCollection, opt) { // clearing views removes any event listeners this.removeViews(); var cells = cellsCollection.models.slice(); // `beforeRenderViews()` can return changed cells array (e.g sorted). cells = this.beforeRenderViews(cells, opt) || cells; if (this._frameId) { joint.util.cancelFrame(this._frameId); delete this._frameId; } if (this.options.async) { this.asyncRenderViews(cells, opt); // Sort the cells once all elements rendered (see asyncRenderViews()). } else { _.each(cells, this.renderView, this); // Sort the cells in the DOM manually as we might have changed the order they // were added to the DOM (see above). this.sortViews(); } }, removeViews: function() { _.invoke(this._views, 'remove'); this._views = {}; }, asyncBatchAdded: _.noop, asyncRenderViews: function(cells, opt) { if (this._frameId) { var batchSize = (this.options.async && this.options.async.batchSize) || 50; var batchCells = cells.splice(0, batchSize); var collection = this.model.get('cells'); _.each(batchCells, function(cell) { // The cell has to be part of the graph collection. // There is a chance in asynchronous rendering // that a cell was removed before it's rendered to the paper. if (cell.collection === collection) this.renderView(cell); }, this); this.asyncBatchAdded(); } if (!cells.length) { // No cells left to render. delete this._frameId; this.afterRenderViews(opt); this.trigger('render:done', opt); } else { // Schedule a next batch to render. this._frameId = joint.util.nextFrame(function() { this.asyncRenderViews(cells, opt); }, this); } }, sortViews: function() { // Run insertion sort algorithm in order to efficiently sort DOM elements according to their // associated model `z` attribute. var $cells = $(this.viewport).children('[model-id]'); var cells = this.model.get('cells'); joint.util.sortElements($cells, function(a, b) { var cellA = cells.get($(a).attr('model-id')); var cellB = cells.get($(b).attr('model-id')); return (cellA.get('z') || 0) > (cellB.get('z') || 0) ? 1 : -1; }); }, scale: function(sx, sy, ox, oy) { sy = sy || sx; if (_.isUndefined(ox)) { ox = 0; oy = 0; } // Remove previous transform so that the new scale is not affected by previous scales, especially // the old translate() does not affect the new translate if an origin is specified. V(this.viewport).attr('transform', ''); var oldTx = this.options.origin.x; var oldTy = this.options.origin.y; // TODO: V.scale() doesn't support setting scale origin. #Fix if (ox || oy || oldTx || oldTy) { var newTx = oldTx - ox * (sx - 1); var newTy = oldTy - oy * (sy - 1); this.setOrigin(newTx, newTy); } V(this.viewport).scale(sx, sy); this.trigger('scale', sx, sy, ox, oy); if (this.options.drawGrid) { this.drawGrid(); } return this; }, rotate: function(deg, ox, oy) { // If the origin is not set explicitely, rotate around the center. Note that // we must use the plain bounding box (`this.el.getBBox()` instead of the one that gives us // the real bounding box (`bbox()`) including transformations). if (_.isUndefined(ox)) { var bbox = this.viewport.getBBox(); ox = bbox.width / 2; oy = bbox.height / 2; } V(this.viewport).rotate(deg, ox, oy); }, // Find the first view climbing up the DOM tree starting at element `el`. Note that `el` can also // be a selector or a jQuery object. findView: function($el) { var el = _.isString($el) ? this.viewport.querySelector($el) : $el instanceof $ ? $el[0] : $el; while (el && el !== this.el && el !== document) { var id = el.getAttribute('model-id'); if (id) return this._views[id]; el = el.parentNode; } return undefined; }, // Find a view for a model `cell`. `cell` can also be a string representing a model `id`. findViewByModel: function(cell) { var id = _.isString(cell) ? cell : cell.id; return this._views[id]; }, // Find all views at given point findViewsFromPoint: function(p) { p = g.point(p); var views = _.map(this.model.getElements(), this.findViewByModel, this); return _.filter(views, function(view) { return view && g.rect(view.vel.bbox(false, this.viewport)).containsPoint(p); }, this); }, // Find all views in given area findViewsInArea: function(rect, opt) { opt = _.defaults(opt || {}, { strict: false }); rect = g.rect(rect); var views = _.map(this.model.getElements(), this.findViewByModel, this); var method = opt.strict ? 'containsRect' : 'intersect'; return _.filter(views, function(view) { return view && rect[method](g.rect(view.vel.bbox(false, this.viewport))); }, this); }, getModelById: function(id) { return this.model.getCell(id); }, snapToGrid: function(p) { // Convert global coordinates to the local ones of the `viewport`. Otherwise, // improper transformation would be applied when the viewport gets transformed (scaled/rotated). var localPoint = V(this.viewport).toLocalPoint(p.x, p.y); return { x: g.snapToGrid(localPoint.x, this.options.gridSize), y: g.snapToGrid(localPoint.y, this.options.gridSize) }; }, // Transform client coordinates to the paper local coordinates. // Useful when you have a mouse event object and you'd like to get coordinates // inside the paper that correspond to `evt.clientX` and `evt.clientY` point. // Exmaple: var paperPoint = paper.clientToLocalPoint({ x: evt.clientX, y: evt.clientY }); clientToLocalPoint: function(p) { p = g.point(p); // This is a hack for Firefox! If there wasn't a fake (non-visible) rectangle covering the // whole SVG area, `$(paper.svg).offset()` used below won't work. var fakeRect = V('rect', { width: this.options.width, height: this.options.height, x: 0, y: 0, opacity: 0 }); V(this.svg).prepend(fakeRect); var paperOffset = $(this.svg).offset(); // Clean up the fake rectangle once we have the offset of the SVG document. fakeRect.remove(); var scrollTop = document.body.scrollTop || document.documentElement.scrollTop; var scrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; p.offset(scrollLeft - paperOffset.left, scrollTop - paperOffset.top); // Transform point into the viewport coordinate system. return V.transformPoint(p, this.viewport.getCTM().inverse()); }, linkAllowed: function(linkViewOrModel) { var link; if (linkViewOrModel instanceof joint.dia.Link) { link = linkViewOrModel; } else if (linkViewOrModel instanceof joint.dia.LinkView) { link = linkViewOrModel.model; } else { throw new Error('Must provide link model or view.'); } if (!this.options.multiLinks) { // Do not allow multiple links to have the same source and target. var source = link.get('source'); var target = link.get('target'); if (source.id && target.id) { var sourceModel = link.getSourceElement(); if (sourceModel) { var connectedLinks = this.model.getConnectedLinks(sourceModel, { outbound: true, inbound: false }); var numSameLinks = _.filter(connectedLinks, function(_link) { var _source = _link.get('source'); var _target = _link.get('target'); return _source && _source.id === source.id && (!_source.port || (_source.port === source.port)) && _target && _target.id === target.id && (!_target.port || (_target.port === target.port)); }).length; if (numSameLinks > 1) { return false; } } } } if ( !this.options.linkPinning && ( !_.has(link.get('source'), 'id') || !_.has(link.get('target'), 'id') ) ) { // Link pinning is not allowed and the link is not connected to the target. return false; } return true; }, getDefaultLink: function(cellView, magnet) { return _.isFunction(this.options.defaultLink) // default link is a function producing link model ? this.options.defaultLink.call(this, cellView, magnet) // default link is the Backbone model : this.options.defaultLink.clone(); }, // Cell highlighting // ----------------- resolveHighlighter: function(opt) { opt = opt || {}; var highlighterDef = opt.highlighter; var paperOpt = this.options; /* Expecting opt.highlighter to have the following structure: { name: 'highlighter-name', options: { some: 'value' } } */ if (_.isUndefined(highlighterDef)) { // check for built-in types var type = _.chain(opt) .pick('embedding', 'connecting', 'magnetAvailability', 'elementAvailability') .keys().first().value(); highlighterDef = (type && paperOpt.highlighting[type]) || paperOpt.highlighting['default']; } // Do nothing if opt.highlighter is falsey. // This allows the case to not highlight cell(s) in certain cases. // For example, if you want to NOT highlight when embedding elements. if (!highlighterDef) return false; var name = highlighterDef.name; var highlighter = paperOpt.highlighterNamespace[name]; // Highlighter validation if (!highlighter) { throw new Error('Unknown highlighter ("' + name + '")'); } if (typeof highlighter.highlight !== 'function') { throw new Error('Highlighter ("' + name + '") is missing required highlight() method'); } if (typeof highlighter.unhighlight !== 'function') { throw new Error('Highlighter ("' + name + '") is missing required unhighlight() method'); } return { highlighter: highlighter, options: highlighterDef.options, name: name }; }, onCellHighlight: function(cellView, magnetEl, opt) { opt = this.resolveHighlighter(opt); if (!opt) return; var key = opt.name + magnetEl.id + JSON.stringify(opt.options); if (!this._highlights[key]) { var highlighter = opt.highlighter; highlighter.highlight(cellView, magnetEl, _.clone(opt.options)); this._highlights[key] = { cellView: cellView, magnetEl: magnetEl, opt: opt.options, highlighter: highlighter }; } }, onCellUnhighlight: function(cellView, magnetEl, opt) { opt = this.resolveHighlighter(opt); if (!opt) return; var key = opt.name + magnetEl.id + JSON.stringify(opt.options); var highlight = this._highlights[key]; if (highlight) { // Use the cellView and magnetEl that were used by the highlighter.highlight() method. highlight.highlighter.unhighlight(highlight.cellView, highlight.magnetEl, highlight.opt); this._highlights[key] = null; } }, // Interaction. // ------------ mousedblclick: function(evt) { evt.preventDefault(); evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { view.pointerdblclick(evt, localPoint.x, localPoint.y); } else { this.trigger('blank:pointerdblclick', evt, localPoint.x, localPoint.y); } }, mouseclick: function(evt) { // Trigger event when mouse not moved. if (this._mousemoved <= this.options.clickThreshold) { evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { view.pointerclick(evt, localPoint.x, localPoint.y); } else { this.trigger('blank:pointerclick', evt, localPoint.x, localPoint.y); } } }, // Guard guards the event received. If the event is not interesting, guard returns `true`. // Otherwise, it return `false`. guard: function(evt, view) { if (this.options.guard && this.options.guard(evt, view)) { return true; } if (view && view.model && (view.model instanceof joint.dia.Cell)) { return false; } else if (this.svg === evt.target || this.el === evt.target || $.contains(this.svg, evt.target)) { return false; } return true; // Event guarded. Paper should not react on it in any way. }, contextmenu: function(evt) { evt = joint.util.normalizeEvent(evt); if (this.options.preventContextMenu) { evt.preventDefault(); } var view = this.findView(evt.target); if (this.guard(evt, view)) return; var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { view.contextmenu(evt, localPoint.x, localPoint.y); } else { this.trigger('blank:contextmenu', evt, localPoint.x, localPoint.y); } }, pointerdown: function(evt) { evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; evt.preventDefault(); this._mousemoved = 0; var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (view) { this.sourceView = view; view.pointerdown(evt, localPoint.x, localPoint.y); } else { this.trigger('blank:pointerdown', evt, localPoint.x, localPoint.y); } }, pointermove: function(evt) { evt.preventDefault(); evt = joint.util.normalizeEvent(evt); if (this.sourceView) { // Mouse moved counter. this._mousemoved++; var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); this.sourceView.pointermove(evt, localPoint.x, localPoint.y); } }, pointerup: function(evt) { evt = joint.util.normalizeEvent(evt); var localPoint = this.snapToGrid({ x: evt.clientX, y: evt.clientY }); if (this.sourceView) { this.sourceView.pointerup(evt, localPoint.x, localPoint.y); //"delete sourceView" occasionally throws an error in chrome (illegal access exception) this.sourceView = null; } else { this.trigger('blank:pointerup', evt, localPoint.x, localPoint.y); } }, mousewheel: function(evt) { evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (this.guard(evt, view)) return; var originalEvent = evt.originalEvent; var localPoint = this.snapToGrid({ x: originalEvent.clientX, y: originalEvent.clientY }); var delta = Math.max(-1, Math.min(1, (originalEvent.wheelDelta || -originalEvent.detail))); if (view) { view.mousewheel(evt, localPoint.x, localPoint.y, delta); } else { this.trigger('blank:mousewheel', evt, localPoint.x, localPoint.y, delta); } }, cellMouseover: function(evt) { evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (view) { if (this.guard(evt, view)) return; view.mouseover(evt); } }, cellMouseout: function(evt) { evt = joint.util.normalizeEvent(evt); var view = this.findView(evt.target); if (view) { if (this.guard(evt, view)) return; view.mouseout(evt); } }, setGridSize: function(gridSize) { this.options.gridSize = gridSize; if (this.options.drawGrid) { this.drawGrid(); } return this; }, drawGrid: function(opt) { opt = opt || {}; _.defaults(opt, this.options.drawGrid, { color: '#aaa', thickness: 1 }); var gridSize = this.options.gridSize; if (gridSize <= 1) { this.el.style.backgroundImage = 'none'; return; } var currentScale = V(this.viewport).scale(); var scaleX = currentScale.sx; var scaleY = currentScale.sy; var originX = this.options.origin.x; var originY = this.options.origin.y; var gridX = gridSize * scaleX; var gridY = gridSize * scaleY; var canvas = document.createElement('canvas'); canvas.width = gridX; canvas.height = gridY; gridX = originX >= 0 ? originX % gridX : gridX + originX % gridX; gridY = originY >= 0 ? originY % gridY : gridY + originY % gridY; var context = canvas.getContext('2d'); context.beginPath(); context.rect(gridX, gridY, opt.thickness * scaleX, opt.thickness * scaleY); context.fillStyle = opt.color; context.fill(); var backgroundImage = canvas.toDataURL('image/png'); this.el.style.backgroundImage = 'url("' + backgroundImage + '")'; } }); // JointJS library. // (c) 2011-2013 client IO joint.shapes.basic = {}; joint.shapes.basic.Generic = joint.dia.Element.extend({ defaults: joint.util.deepSupplement({ type: 'basic.Generic', attrs: { '.': { fill: '#ffffff', stroke: 'none' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.basic.Rect = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Rect', attrs: { 'rect': { fill: '#ffffff', stroke: '#000000', width: 100, height: 60 }, 'text': { fill: '#000000', text: '', 'font-size': 14, 'ref-x': .5, 'ref-y': .5, 'text-anchor': 'middle', 'y-alignment': 'middle', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.TextView = joint.dia.ElementView.extend({ initialize: function() { joint.dia.ElementView.prototype.initialize.apply(this, arguments); // The element view is not automatically rescaled to fit the model size // when the attribute 'attrs' is changed. this.listenTo(this.model, 'change:attrs', this.resize); } }); joint.shapes.basic.Text = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Text', attrs: { 'text': { 'font-size': 18, fill: '#000000' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Circle = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Circle', size: { width: 60, height: 60 }, attrs: { 'circle': { fill: '#ffffff', stroke: '#000000', r: 30, cx: 30, cy: 30 }, 'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', fill: '#000000', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Ellipse = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Ellipse', size: { width: 60, height: 40 }, attrs: { 'ellipse': { fill: '#ffffff', stroke: '#000000', rx: 30, ry: 20, cx: 30, cy: 20 }, 'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', fill: '#000000', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Polygon = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Polygon', size: { width: 60, height: 40 }, attrs: { 'polygon': { fill: '#ffffff', stroke: '#000000' }, 'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, 'y-alignment': 'middle', fill: '#000000', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Polyline = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Polyline', size: { width: 60, height: 40 }, attrs: { 'polyline': { fill: '#ffffff', stroke: '#000000' }, 'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, 'y-alignment': 'middle', fill: '#000000', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Image = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Image', attrs: { 'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref-x': .5, 'ref-dy': 20, 'y-alignment': 'middle', fill: '#000000', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Path = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'basic.Path', size: { width: 60, height: 60 }, attrs: { 'path': { fill: '#ffffff', stroke: '#000000' }, 'text': { 'font-size': 14, text: '', 'text-anchor': 'middle', 'ref': 'path', 'ref-x': .5, 'ref-dy': 10, fill: '#000000', 'font-family': 'Arial, helvetica, sans-serif' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.basic.Rhombus = joint.shapes.basic.Path.extend({ defaults: joint.util.deepSupplement({ type: 'basic.Rhombus', attrs: { 'path': { d: 'M 30 0 L 60 30 30 60 0 30 z' }, 'text': { 'ref-y': .5, 'y-alignment': 'middle' } } }, joint.shapes.basic.Path.prototype.defaults) }); // PortsModelInterface is a common interface for shapes that have ports. This interface makes it easy // to create new shapes with ports functionality. It is assumed that the new shapes have // `inPorts` and `outPorts` array properties. Only these properties should be used to set ports. // In other words, using this interface, it is no longer recommended to set ports directly through the // `attrs` object. // Usage: // joint.shapes.custom.MyElementWithPorts = joint.shapes.basic.Path.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, { // getPortAttrs: function(portName, index, total, selector, type) { // var attrs = {}; // var portClass = 'port' + index; // var portSelector = selector + '>.' + portClass; // var portTextSelector = portSelector + '>text'; // var portBodySelector = portSelector + '>.port-body'; // // attrs[portTextSelector] = { text: portName }; // attrs[portBodySelector] = { port: { id: portName || _.uniqueId(type) , type: type } }; // attrs[portSelector] = { ref: 'rect', 'ref-y': (index + 0.5) * (1 / total) }; // // if (selector === '.outPorts') { attrs[portSelector]['ref-dx'] = 0; } // // return attrs; // } //})); joint.shapes.basic.PortsModelInterface = { initialize: function() { this.updatePortsAttrs(); this.on('change:inPorts change:outPorts', this.updatePortsAttrs, this); // Call the `initialize()` of the parent. this.constructor.__super__.constructor.__super__.initialize.apply(this, arguments); }, updatePortsAttrs: function(eventName) { if (this._portSelectors) { var newAttrs = _.omit(this.get('attrs'), this._portSelectors); this.set('attrs', newAttrs, { silent: true }); } // This holds keys to the `attrs` object for all the port specific attribute that // we set in this method. This is necessary in order to remove previously set // attributes for previous ports. this._portSelectors = []; var attrs = {}; _.each(this.get('inPorts'), function(portName, index, ports) { var portAttributes = this.getPortAttrs(portName, index, ports.length, '.inPorts', 'in'); this._portSelectors = this._portSelectors.concat(_.keys(portAttributes)); _.extend(attrs, portAttributes); }, this); _.each(this.get('outPorts'), function(portName, index, ports) { var portAttributes = this.getPortAttrs(portName, index, ports.length, '.outPorts', 'out'); this._portSelectors = this._portSelectors.concat(_.keys(portAttributes)); _.extend(attrs, portAttributes); }, this); // Silently set `attrs` on the cell so that noone knows the attrs have changed. This makes sure // that, for example, command manager does not register `change:attrs` command but only // the important `change:inPorts`/`change:outPorts` command. this.attr(attrs, { silent: true }); // Manually call the `processPorts()` method that is normally called on `change:attrs` (that we just made silent). this.processPorts(); // Let the outside world (mainly the `ModelView`) know that we're done configuring the `attrs` object. this.trigger('process:ports'); }, getPortSelector: function(name) { var selector = '.inPorts'; var index = this.get('inPorts').indexOf(name); if (index < 0) { selector = '.outPorts'; index = this.get('outPorts').indexOf(name); if (index < 0) throw new Error("getPortSelector(): Port doesn't exist."); } return selector + '>g:nth-child(' + (index + 1) + ')>.port-body'; } }; joint.shapes.basic.PortsViewInterface = { initialize: function() { // `Model` emits the `process:ports` whenever it's done configuring the `attrs` object for ports. this.listenTo(this.model, 'process:ports', this.update); joint.dia.ElementView.prototype.initialize.apply(this, arguments); }, update: function() { // First render ports so that `attrs` can be applied to those newly created DOM elements // in `ElementView.prototype.update()`. this.renderPorts(); joint.dia.ElementView.prototype.update.apply(this, arguments); }, renderPorts: function() { var $inPorts = this.$('.inPorts').empty(); var $outPorts = this.$('.outPorts').empty(); var portTemplate = joint.util.template(this.model.portMarkup); _.each(_.filter(this.model.ports, function(p) { return p.type === 'in'; }), function(port, index) { $inPorts.append(V(portTemplate({ id: index, port: port })).node); }); _.each(_.filter(this.model.ports, function(p) { return p.type === 'out'; }), function(port, index) { $outPorts.append(V(portTemplate({ id: index, port: port })).node); }); } }; joint.shapes.basic.TextBlock = joint.shapes.basic.Generic.extend({ markup: [ '', '', joint.env.test('svgforeignobject') ? '
' : '', '' ].join(''), defaults: joint.util.deepSupplement({ type: 'basic.TextBlock', // see joint.css for more element styles attrs: { rect: { fill: '#ffffff', stroke: '#000000', width: 80, height: 100 }, text: { fill: '#000000', 'font-size': 14, 'font-family': 'Arial, helvetica, sans-serif' }, '.content': { text: '', ref: 'rect', 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', 'x-alignment': 'middle' } }, content: '' }, joint.shapes.basic.Generic.prototype.defaults), initialize: function() { this.listenTo(this, 'change:size', this.updateSize); this.listenTo(this, 'change:content', this.updateContent); this.updateSize(this, this.get('size')); this.updateContent(this, this.get('content')); joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments); }, updateSize: function(cell, size) { // Selector `foreignObject' doesn't work accross all browsers, we'r using class selector instead. // We have to clone size as we don't want attributes.div.style to be same object as attributes.size. this.attr({ '.fobj': _.clone(size), div: { style: _.clone(size) } }); }, updateContent: function(cell, content) { if (joint.env.test('svgforeignobject')) { // Content element is a
element. this.attr({ '.content': { html: content } }); } else { // Content element is a element. // SVG elements don't have innerHTML attribute. this.attr({ '.content': { text: content } }); } }, // Here for backwards compatibility: setForeignObjectSize: function() { this.updateSize.apply(this, arguments); }, // Here for backwards compatibility: setDivContent: function() { this.updateContent.apply(this, arguments); } }); // TextBlockView implements the fallback for IE when no foreignObject exists and // the text needs to be manually broken. joint.shapes.basic.TextBlockView = joint.dia.ElementView.extend({ initialize: function() { joint.dia.ElementView.prototype.initialize.apply(this, arguments); // Keep this for backwards compatibility: this.noSVGForeignObjectElement = !joint.env.test('svgforeignobject'); if (!joint.env.test('svgforeignobject')) { this.listenTo(this.model, 'change:content', function(cell) { // avoiding pass of extra paramters this.updateContent(cell); }); } }, update: function(cell, renderingOnlyAttrs) { if (joint.env.test('svgforeignobject')) { var model = this.model; // Update everything but the content first. var noTextAttrs = _.omit(renderingOnlyAttrs || model.get('attrs'), '.content'); joint.dia.ElementView.prototype.update.call(this, model, noTextAttrs); if (!renderingOnlyAttrs || _.has(renderingOnlyAttrs, '.content')) { // Update the content itself. this.updateContent(model, renderingOnlyAttrs); } } else { joint.dia.ElementView.prototype.update.call(this, model, renderingOnlyAttrs); } }, updateContent: function(cell, renderingOnlyAttrs) { // Create copy of the text attributes var textAttrs = _.merge({}, (renderingOnlyAttrs || cell.get('attrs'))['.content']); textAttrs = _.omit(textAttrs, 'text'); // Break the content to fit the element size taking into account the attributes // set on the model. var text = joint.util.breakText(cell.get('content'), cell.get('size'), textAttrs, { // measuring sandbox svg document svgDocument: this.paper.svg }); // Create a new attrs with same structure as the model attrs { text: { *textAttributes* }} var attrs = joint.util.setByPath({}, '.content', textAttrs, '/'); // Replace text attribute with the one we just processed. attrs['.content'].text = text; // Update the view using renderingOnlyAttributes parameter. joint.dia.ElementView.prototype.update.call(this, cell, attrs); } }); joint.routers.manhattan = (function(g, _) { 'use strict'; var config = { // size of the step to find a route step: 10, // use of the perpendicular linkView option to connect center of element with first vertex perpendicular: true, // should be source or target not to be consider as an obstacle excludeEnds: [], // 'source', 'target' // should be any element with a certain type not to be consider as an obstacle excludeTypes: ['basic.Text'], // if number of route finding loops exceed the maximum, stops searching and returns // fallback route maximumLoops: 2000, // possible starting directions from an element startDirections: ['left', 'right', 'top', 'bottom'], // possible ending directions to an element endDirections: ['left', 'right', 'top', 'bottom'], // specify directions above directionMap: { right: { x: 1, y: 0 }, bottom: { x: 0, y: 1 }, left: { x: -1, y: 0 }, top: { x: 0, y: -1 } }, // maximum change of the direction maxAllowedDirectionChange: 90, // padding applied on the element bounding boxes paddingBox: function() { var step = this.step; return { x: -step, y: -step, width: 2 * step, height: 2 * step }; }, // an array of directions to find next points on the route directions: function() { var step = this.step; return [ { offsetX: step , offsetY: 0 , cost: step }, { offsetX: 0 , offsetY: step , cost: step }, { offsetX: -step , offsetY: 0 , cost: step }, { offsetX: 0 , offsetY: -step , cost: step } ]; }, // a penalty received for direction change penalties: function() { return { 0: 0, 45: this.step / 2, 90: this.step / 2 }; }, // a simple route used in situations, when main routing method fails // (exceed loops, inaccessible). fallbackRoute: function(from, to, opts) { // Find an orthogonal route ignoring obstacles. var point = ((opts.previousDirAngle || 0) % 180 === 0) ? g.point(from.x, to.y) : g.point(to.x, from.y); return [point, to]; }, // if a function is provided, it's used to route the link while dragging an end // i.e. function(from, to, opts) { return []; } draggingRoute: null }; // Map of obstacles // Helper structure to identify whether a point lies in an obstacle. function ObstacleMap(opt) { this.map = {}; this.options = opt; // tells how to divide the paper when creating the elements map this.mapGridSize = 100; } ObstacleMap.prototype.build = function(graph, link) { var opt = this.options; // source or target element could be excluded from set of obstacles var excludedEnds = _.chain(opt.excludeEnds) .map(link.get, link) .pluck('id') .map(graph.getCell, graph).value(); // Exclude any embedded elements from the source and the target element. var excludedAncestors = []; var source = graph.getCell(link.get('source').id); if (source) { excludedAncestors = _.union(excludedAncestors, _.map(source.getAncestors(), 'id')); }; var target = graph.getCell(link.get('target').id); if (target) { excludedAncestors = _.union(excludedAncestors, _.map(target.getAncestors(), 'id')); } // builds a map of all elements for quicker obstacle queries (i.e. is a point contained // in any obstacle?) (a simplified grid search) // The paper is divided to smaller cells, where each of them holds an information which // elements belong to it. When we query whether a point is in an obstacle we don't need // to go through all obstacles, we check only those in a particular cell. var mapGridSize = this.mapGridSize; _.chain(graph.getElements()) // remove source and target element if required .difference(excludedEnds) // remove all elements whose type is listed in excludedTypes array .reject(function(element) { // reject any element which is an ancestor of either source or target return _.contains(opt.excludeTypes, element.get('type')) || _.contains(excludedAncestors, element.id); }) // change elements (models) to their bounding boxes .invoke('getBBox') // expand their boxes by specific padding .invoke('moveAndExpand', opt.paddingBox) // build the map .foldl(function(map, bbox) { var origin = bbox.origin().snapToGrid(mapGridSize); var corner = bbox.corner().snapToGrid(mapGridSize); for (var x = origin.x; x <= corner.x; x += mapGridSize) { for (var y = origin.y; y <= corner.y; y += mapGridSize) { var gridKey = x + '@' + y; map[gridKey] = map[gridKey] || []; map[gridKey].push(bbox); } } return map; }, this.map).value(); return this; }; ObstacleMap.prototype.isPointAccessible = function(point) { var mapKey = point.clone().snapToGrid(this.mapGridSize).toString(); return _.every(this.map[mapKey], function(obstacle) { return !obstacle.containsPoint(point); }); }; // Sorted Set // Set of items sorted by given value. function SortedSet() { this.items = []; this.hash = {}; this.values = {}; this.OPEN = 1; this.CLOSE = 2; } SortedSet.prototype.add = function(item, value) { if (this.hash[item]) { // item removal this.items.splice(this.items.indexOf(item), 1); } else { this.hash[item] = this.OPEN; } this.values[item] = value; var index = _.sortedIndex(this.items, item, function(i) { return this.values[i]; }, this); this.items.splice(index, 0, item); }; SortedSet.prototype.remove = function(item) { this.hash[item] = this.CLOSE; }; SortedSet.prototype.isOpen = function(item) { return this.hash[item] === this.OPEN; }; SortedSet.prototype.isClose = function(item) { return this.hash[item] === this.CLOSE; }; SortedSet.prototype.isEmpty = function() { return this.items.length === 0; }; SortedSet.prototype.pop = function() { var item = this.items.shift(); this.remove(item); return item; }; // reconstructs a route by concating points with their parents function reconstructRoute(parents, point) { var route = []; var prevDiff = { x: 0, y: 0 }; var current = point; var parent; while ((parent = parents[current])) { var diff = parent.difference(current); if (!diff.equals(prevDiff)) { route.unshift(current); prevDiff = diff; } current = parent; } route.unshift(current); return route; } // find points around the rectangle taking given directions in the account function getRectPoints(bbox, directionList, opt) { var step = opt.step; var center = bbox.center(); var startPoints = _.chain(opt.directionMap).pick(directionList).map(function(direction) { var x = direction.x * bbox.width / 2; var y = direction.y * bbox.height / 2; var point = center.clone().offset(x, y); if (bbox.containsPoint(point)) { point.offset(direction.x * step, direction.y * step); } return point.snapToGrid(step); }).value(); return startPoints; }; // returns a direction index from start point to end point function getDirectionAngle(start, end, dirLen) { var q = 360 / dirLen; return Math.floor(g.normalizeAngle(start.theta(end) + q / 2) / q) * q; } function getDirectionChange(angle1, angle2) { var dirChange = Math.abs(angle1 - angle2); return dirChange > 180 ? 360 - dirChange : dirChange; } // heurestic method to determine the distance between two points function estimateCost(from, endPoints) { var min = Infinity; for (var i = 0, len = endPoints.length; i < len; i++) { var cost = from.manhattanDistance(endPoints[i]); if (cost < min) min = cost; }; return min; } // finds the route between to points/rectangles implementing A* alghoritm function findRoute(start, end, map, opt) { var step = opt.step; var startPoints, endPoints; var startCenter, endCenter; // set of points we start pathfinding from if (start instanceof g.rect) { startPoints = getRectPoints(start, opt.startDirections, opt); startCenter = start.center(); } else { startCenter = start.clone().snapToGrid(step); startPoints = [startCenter]; } // set of points we want the pathfinding to finish at if (end instanceof g.rect) { endPoints = getRectPoints(end, opt.endDirections, opt); endCenter = end.center(); } else { endCenter = end.clone().snapToGrid(step); endPoints = [endCenter]; } // take into account only accessible end points startPoints = _.filter(startPoints, map.isPointAccessible, map); endPoints = _.filter(endPoints, map.isPointAccessible, map); // Check if there is a accessible end point. // We would have to use a fallback route otherwise. if (startPoints.length > 0 && endPoints.length > 0) { // The set of tentative points to be evaluated, initially containing the start points. var openSet = new SortedSet(); // Keeps reference to a point that is immediate predecessor of given element. var parents = {}; // Cost from start to a point along best known path. var costs = {}; _.each(startPoints, function(point) { var key = point.toString(); openSet.add(key, estimateCost(point, endPoints)); costs[key] = 0; }); // directions var dir, dirChange; var dirs = opt.directions; var dirLen = dirs.length; var loopsRemain = opt.maximumLoops; var endPointsKeys = _.invoke(endPoints, 'toString'); // main route finding loop while (!openSet.isEmpty() && loopsRemain > 0) { // remove current from the open list var currentKey = openSet.pop(); var currentPoint = g.point(currentKey); var currentDist = costs[currentKey]; var previousDirAngle = currentDirAngle; var currentDirAngle = parents[currentKey] ? getDirectionAngle(parents[currentKey], currentPoint, dirLen) : opt.previousDirAngle != null ? opt.previousDirAngle : getDirectionAngle(startCenter, currentPoint, dirLen); // Check if we reached any endpoint if (endPointsKeys.indexOf(currentKey) >= 0) { // We don't want to allow route to enter the end point in opposite direction. dirChange = getDirectionChange(currentDirAngle, getDirectionAngle(currentPoint, endCenter, dirLen)); if (currentPoint.equals(endCenter) || dirChange < 180) { opt.previousDirAngle = currentDirAngle; return reconstructRoute(parents, currentPoint); } } // Go over all possible directions and find neighbors. for (var i = 0; i < dirLen; i++) { dir = dirs[i]; dirChange = getDirectionChange(currentDirAngle, dir.angle); // if the direction changed rapidly don't use this point if (dirChange > opt.maxAllowedDirectionChange) { continue; } var neighborPoint = currentPoint.clone().offset(dir.offsetX, dir.offsetY); var neighborKey = neighborPoint.toString(); // Closed points from the openSet were already evaluated. if (openSet.isClose(neighborKey) || !map.isPointAccessible(neighborPoint)) { continue; } // The current direction is ok to proccess. var costFromStart = currentDist + dir.cost + opt.penalties[dirChange]; if (!openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey]) { // neighbor point has not been processed yet or the cost of the path // from start is lesser than previously calcluated. parents[neighborKey] = currentPoint; costs[neighborKey] = costFromStart; openSet.add(neighborKey, costFromStart + estimateCost(neighborPoint, endPoints)); }; }; loopsRemain--; } } // no route found ('to' point wasn't either accessible or finding route took // way to much calculations) return opt.fallbackRoute(startCenter, endCenter, opt); } // resolve some of the options function resolveOptions(opt) { opt.directions = _.result(opt, 'directions'); opt.penalties = _.result(opt, 'penalties'); opt.paddingBox = _.result(opt, 'paddingBox'); _.each(opt.directions, function(direction) { var point1 = new g.point(0, 0); var point2 = new g.point(direction.offsetX, direction.offsetY); var angle = g.normalizeAngle(point1.theta(point2)); direction.angle = angle; }); } // initiation of the route finding function router(vertices, opt) { resolveOptions(opt); // enable/disable linkView perpendicular option this.options.perpendicular = !!opt.perpendicular; // expand boxes by specific padding var sourceBBox = g.rect(this.sourceBBox).moveAndExpand(opt.paddingBox); var targetBBox = g.rect(this.targetBBox).moveAndExpand(opt.paddingBox); // pathfinding var map = (new ObstacleMap(opt)).build(this.paper.model, this.model); var oldVertices = _.map(vertices, g.point); var newVertices = []; var tailPoint = sourceBBox.center().snapToGrid(opt.step); // find a route by concating all partial routes (routes need to go through the vertices) // startElement -> vertex[1] -> ... -> vertex[n] -> endElement for (var i = 0, len = oldVertices.length; i <= len; i++) { var partialRoute = null; var from = to || sourceBBox; var to = oldVertices[i]; if (!to) { to = targetBBox; // 'to' is not a vertex. If the target is a point (i.e. it's not an element), we // might use dragging route instead of main routing method if that is enabled. var endingAtPoint = !this.model.get('source').id || !this.model.get('target').id; if (endingAtPoint && _.isFunction(opt.draggingRoute)) { // Make sure we passing points only (not rects). var dragFrom = from instanceof g.rect ? from.center() : from; partialRoute = opt.draggingRoute(dragFrom, to.origin(), opt); } } // if partial route has not been calculated yet use the main routing method to find one partialRoute = partialRoute || findRoute(from, to, map, opt); var leadPoint = _.first(partialRoute); if (leadPoint && leadPoint.equals(tailPoint)) { // remove the first point if the previous partial route had the same point as last partialRoute.shift(); } tailPoint = _.last(partialRoute) || tailPoint; Array.prototype.push.apply(newVertices, partialRoute); }; return newVertices; } // public function return function(vertices, opt, linkView) { return router.call(linkView, vertices, _.extend({}, config, opt)); }; })(g, _); joint.routers.metro = (function() { if (!_.isFunction(joint.routers.manhattan)) { throw('Metro requires the manhattan router.'); } var config = { // cost of a diagonal step (calculated if not defined). diagonalCost: null, // an array of directions to find next points on the route directions: function() { var step = this.step; var diagonalCost = this.diagonalCost || Math.ceil(Math.sqrt(step * step << 1)); return [ { offsetX: step , offsetY: 0 , cost: step }, { offsetX: step , offsetY: step , cost: diagonalCost }, { offsetX: 0 , offsetY: step , cost: step }, { offsetX: -step , offsetY: step , cost: diagonalCost }, { offsetX: -step , offsetY: 0 , cost: step }, { offsetX: -step , offsetY: -step , cost: diagonalCost }, { offsetX: 0 , offsetY: -step , cost: step }, { offsetX: step , offsetY: -step , cost: diagonalCost } ]; }, maxAllowedDirectionChange: 45, // a simple route used in situations, when main routing method fails // (exceed loops, inaccessible). fallbackRoute: function(from, to, opts) { // Find a route which breaks by 45 degrees ignoring all obstacles. var theta = from.theta(to); var a = { x: to.x, y: from.y }; var b = { x: from.x, y: to.y }; if (theta % 180 > 90) { var t = a; a = b; b = t; } var p1 = (theta % 90) < 45 ? a : b; var l1 = g.line(from, p1); var alpha = 90 * Math.ceil(theta / 90); var p2 = g.point.fromPolar(l1.squaredLength(), g.toRad(alpha + 135), p1); var l2 = g.line(to, p2); var point = l1.intersection(l2); return point ? [point.round(), to] : [to]; } }; // public function return function(vertices, opts, linkView) { return joint.routers.manhattan(vertices, _.extend({}, config, opts), linkView); }; })(); // Does not make any changes to vertices. // Returns the arguments that are passed to it, unchanged. joint.routers.normal = function(vertices, opt, linkView) { return vertices; }; // Routes the link always to/from a certain side // // Arguments: // padding ... gap between the element and the first vertex. :: Default 40. // side ... 'left' | 'right' | 'top' | 'bottom' :: Default 'bottom'. // joint.routers.oneSide = function(vertices, opt, linkView) { var side = opt.side || 'bottom'; var padding = opt.padding || 40; // LinkView contains cached source an target bboxes. // Note that those are Geometry rectangle objects. var sourceBBox = linkView.sourceBBox; var targetBBox = linkView.targetBBox; var sourcePoint = sourceBBox.center(); var targetPoint = targetBBox.center(); var coordinate, coordinateValue, dimension, direction; switch (side) { case 'bottom': direction = 1; coordinate = 'y'; dimension = 'height'; break; case 'top': direction = -1; coordinate = 'y'; dimension = 'height'; break; case 'left': direction = -1; coordinate = 'x'; dimension = 'width'; break; case 'right': direction = 1; coordinate = 'x'; dimension = 'width'; break; default: throw new Error('Router: invalid side'); } // move the points from the center of the element to outside of it. sourcePoint[coordinate] += direction * (sourceBBox[dimension] / 2 + padding); targetPoint[coordinate] += direction * (targetBBox[dimension] / 2 + padding); // make link orthogonal (at least the first and last vertex). if (direction * (sourcePoint[coordinate] - targetPoint[coordinate]) > 0) { targetPoint[coordinate] = sourcePoint[coordinate]; } else { sourcePoint[coordinate] = targetPoint[coordinate]; } return [sourcePoint].concat(vertices, targetPoint); }; joint.routers.orthogonal = (function() { // bearing -> opposite bearing var opposite = { N: 'S', S: 'N', E: 'W', W: 'E' }; // bearing -> radians var radians = { N: -Math.PI / 2 * 3, S: -Math.PI / 2, E: 0, W: Math.PI }; // HELPERS // // simple bearing method (calculates only orthogonal cardinals) function bearing(from, to) { if (from.x == to.x) return from.y > to.y ? 'N' : 'S'; if (from.y == to.y) return from.x > to.x ? 'W' : 'E'; return null; } // returns either width or height of a bbox based on the given bearing function boxSize(bbox, brng) { return bbox[brng == 'W' || brng == 'E' ? 'width' : 'height']; } // expands a box by specific value function expand(bbox, val) { return g.rect(bbox).moveAndExpand({ x: -val, y: -val, width: 2 * val, height: 2 * val }); } // transform point to a rect function pointBox(p) { return g.rect(p.x, p.y, 0, 0); } // returns a minimal rect which covers the given boxes function boundary(bbox1, bbox2) { var x1 = Math.min(bbox1.x, bbox2.x); var y1 = Math.min(bbox1.y, bbox2.y); var x2 = Math.max(bbox1.x + bbox1.width, bbox2.x + bbox2.width); var y2 = Math.max(bbox1.y + bbox1.height, bbox2.y + bbox2.height); return g.rect(x1, y1, x2 - x1, y2 - y1); } // returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained // in the given box function freeJoin(p1, p2, bbox) { var p = g.point(p1.x, p2.y); if (bbox.containsPoint(p)) p = g.point(p2.x, p1.y); // kept for reference // if (bbox.containsPoint(p)) p = null; return p; } // PARTIAL ROUTERS // function vertexVertex(from, to, brng) { var p1 = g.point(from.x, to.y); var p2 = g.point(to.x, from.y); var d1 = bearing(from, p1); var d2 = bearing(from, p2); var xBrng = opposite[brng]; var p = (d1 == brng || (d1 != xBrng && (d2 == xBrng || d2 != brng))) ? p1 : p2; return { points: [p], direction: bearing(p, to) }; } function elementVertex(from, to, fromBBox) { var p = freeJoin(from, to, fromBBox); return { points: [p], direction: bearing(p, to) }; } function vertexElement(from, to, toBBox, brng) { var route = {}; var pts = [g.point(from.x, to.y), g.point(to.x, from.y)]; var freePts = _.filter(pts, function(pt) { return !toBBox.containsPoint(pt); }); var freeBrngPts = _.filter(freePts, function(pt) { return bearing(pt, from) != brng; }); var p; if (freeBrngPts.length > 0) { // try to pick a point which bears the same direction as the previous segment p = _.filter(freeBrngPts, function(pt) { return bearing(from, pt) == brng; }).pop(); p = p || freeBrngPts[0]; route.points = [p]; route.direction = bearing(p, to); } else { // Here we found only points which are either contained in the element or they would create // a link segment going in opposite direction from the previous one. // We take the point inside element and move it outside the element in the direction the // route is going. Now we can join this point with the current end (using freeJoin). p = _.difference(pts, freePts)[0]; var p2 = g.point(to).move(p, -boxSize(toBBox, brng) / 2); var p1 = freeJoin(p2, from, toBBox); route.points = [p1, p2]; route.direction = bearing(p2, to); } return route; } function elementElement(from, to, fromBBox, toBBox) { var route = elementVertex(to, from, toBBox); var p1 = route.points[0]; if (fromBBox.containsPoint(p1)) { route = elementVertex(from, to, fromBBox); var p2 = route.points[0]; if (toBBox.containsPoint(p2)) { var fromBorder = g.point(from).move(p2, -boxSize(fromBBox, bearing(from, p2)) / 2); var toBorder = g.point(to).move(p1, -boxSize(toBBox, bearing(to, p1)) / 2); var mid = g.line(fromBorder, toBorder).midpoint(); var startRoute = elementVertex(from, mid, fromBBox); var endRoute = vertexVertex(mid, to, startRoute.direction); route.points = [startRoute.points[0], endRoute.points[0]]; route.direction = endRoute.direction; } } return route; } // Finds route for situations where one of end is inside the other. // Typically the route is conduct outside the outer element first and // let go back to the inner element. function insideElement(from, to, fromBBox, toBBox, brng) { var route = {}; var bndry = expand(boundary(fromBBox, toBBox), 1); // start from the point which is closer to the boundary var reversed = bndry.center().distance(to) > bndry.center().distance(from); var start = reversed ? to : from; var end = reversed ? from : to; var p1, p2, p3; if (brng) { // Points on circle with radius equals 'W + H` are always outside the rectangle // with width W and height H if the center of that circle is the center of that rectangle. p1 = g.point.fromPolar(bndry.width + bndry.height, radians[brng], start); p1 = bndry.pointNearestToPoint(p1).move(p1, -1); } else { p1 = bndry.pointNearestToPoint(start).move(start, 1); } p2 = freeJoin(p1, end, bndry); if (p1.round().equals(p2.round())) { p2 = g.point.fromPolar(bndry.width + bndry.height, g.toRad(p1.theta(start)) + Math.PI / 2, end); p2 = bndry.pointNearestToPoint(p2).move(end, 1).round(); p3 = freeJoin(p1, p2, bndry); route.points = reversed ? [p2, p3, p1] : [p1, p3, p2]; } else { route.points = reversed ? [p2, p1] : [p1, p2]; } route.direction = reversed ? bearing(p1, to) : bearing(p2, to); return route; } // MAIN ROUTER // // Return points that one needs to draw a connection through in order to have a orthogonal link // routing from source to target going through `vertices`. function findOrthogonalRoute(vertices, opt, linkView) { var padding = opt.elementPadding || 20; var orthogonalVertices = []; var sourceBBox = expand(linkView.sourceBBox, padding); var targetBBox = expand(linkView.targetBBox, padding); vertices = _.map(vertices, g.point); vertices.unshift(sourceBBox.center()); vertices.push(targetBBox.center()); var brng; for (var i = 0, max = vertices.length - 1; i < max; i++) { var route = null; var from = vertices[i]; var to = vertices[i + 1]; var isOrthogonal = !!bearing(from, to); if (i == 0) { if (i + 1 == max) { // route source -> target // Expand one of elements by 1px so we detect also situations when they // are positioned one next other with no gap between. if (sourceBBox.intersect(expand(targetBBox, 1))) { route = insideElement(from, to, sourceBBox, targetBBox); } else if (!isOrthogonal) { route = elementElement(from, to, sourceBBox, targetBBox); } } else { // route source -> vertex if (sourceBBox.containsPoint(to)) { route = insideElement(from, to, sourceBBox, expand(pointBox(to), padding)); } else if (!isOrthogonal) { route = elementVertex(from, to, sourceBBox); } } } else if (i + 1 == max) { // route vertex -> target var orthogonalLoop = isOrthogonal && bearing(to, from) == brng; if (targetBBox.containsPoint(from) || orthogonalLoop) { route = insideElement(from, to, expand(pointBox(from), padding), targetBBox, brng); } else if (!isOrthogonal) { route = vertexElement(from, to, targetBBox, brng); } } else if (!isOrthogonal) { // route vertex -> vertex route = vertexVertex(from, to, brng); } if (route) { Array.prototype.push.apply(orthogonalVertices, route.points); brng = route.direction; } else { // orthogonal route and not looped brng = bearing(from, to); } if (i + 1 < max) { orthogonalVertices.push(to); } } return orthogonalVertices; }; return findOrthogonalRoute; })(); joint.connectors.normal = function(sourcePoint, targetPoint, vertices) { // Construct the `d` attribute of the `` element. var d = ['M', sourcePoint.x, sourcePoint.y]; _.each(vertices, function(vertex) { d.push(vertex.x, vertex.y); }); d.push(targetPoint.x, targetPoint.y); return d.join(' '); }; joint.connectors.rounded = function(sourcePoint, targetPoint, vertices, opts) { opts = opts || {}; var offset = opts.radius || 10; var c1, c2, d1, d2, prev, next; // Construct the `d` attribute of the `` element. var d = ['M', sourcePoint.x, sourcePoint.y]; _.each(vertices, function(vertex, index) { // the closest vertices prev = vertices[index - 1] || sourcePoint; next = vertices[index + 1] || targetPoint; // a half distance to the closest vertex d1 = d2 || g.point(vertex).distance(prev) / 2; d2 = g.point(vertex).distance(next) / 2; // control points c1 = g.point(vertex).move(prev, -Math.min(offset, d1)).round(); c2 = g.point(vertex).move(next, -Math.min(offset, d2)).round(); d.push(c1.x, c1.y, 'S', vertex.x, vertex.y, c2.x, c2.y, 'L'); }); d.push(targetPoint.x, targetPoint.y); return d.join(' '); }; joint.connectors.smooth = function(sourcePoint, targetPoint, vertices) { var d; if (vertices.length) { d = g.bezier.curveThroughPoints([sourcePoint].concat(vertices).concat([targetPoint])); } else { // if we have no vertices use a default cubic bezier curve, cubic bezier requires // two control points. The two control points are both defined with X as mid way // between the source and target points. SourceControlPoint Y is equal to sourcePoint Y // and targetControlPointY being equal to targetPointY. Handle situation were // sourcePointX is greater or less then targetPointX. var controlPointX = (sourcePoint.x < targetPoint.x) ? targetPoint.x - ((targetPoint.x - sourcePoint.x) / 2) : sourcePoint.x - ((sourcePoint.x - targetPoint.x) / 2); d = [ 'M', sourcePoint.x, sourcePoint.y, 'C', controlPointX, sourcePoint.y, controlPointX, targetPoint.y, targetPoint.x, targetPoint.y ]; } return d.join(' '); }; joint.connectors.jumpover = (function(_, g) { // default size of jump if not specified in options var JUMP_SIZE = 5; // available jump types var JUMP_TYPES = ['arc', 'gap', 'cubic']; // takes care of math. error for case when jump is too close to end of line var CLOSE_PROXIMITY_PADDING = 1; // list of connector types not to jump over. var IGNORED_CONNECTORS = ['smooth']; /** * Transform start/end and vertices into series of lines * @param {g.point} sourcePoint start point * @param {g.point} targetPoint end point * @param {g.point[]} vertices optional list of vertices * @return {g.line[]} [description] */ function createLines(sourcePoint, targetPoint, vertices) { // make a flattened array of all points var points = [].concat(sourcePoint, vertices, targetPoint); return points.reduce(function(resultLines, point, idx) { // if there is a next point, make a line with it var nextPoint = points[idx + 1]; if (nextPoint != null) { resultLines[idx] = g.line(point, nextPoint); } return resultLines; }, []); } function setupUpdating(jumpOverLinkView) { var updateList = jumpOverLinkView.paper._jumpOverUpdateList; // first time setup for this paper if (updateList == null) { updateList = jumpOverLinkView.paper._jumpOverUpdateList = []; jumpOverLinkView.paper.on('cell:pointerup', updateJumpOver); jumpOverLinkView.paper.model.on('reset', function() { updateList = []; }); } // add this link to a list so it can be updated when some other link is updated if (updateList.indexOf(jumpOverLinkView) < 0) { updateList.push(jumpOverLinkView); // watch for change of connector type or removal of link itself // to remove the link from a list of jump over connectors jumpOverLinkView.listenToOnce(jumpOverLinkView.model, 'change:connector remove', function() { updateList.splice(updateList.indexOf(jumpOverLinkView), 1); }); } } /** * Handler for a batch:stop event to force * update of all registered links with jump over connector * @param {object} batchEvent optional object with info about batch */ function updateJumpOver() { var updateList = this._jumpOverUpdateList; for (var i = 0; i < updateList.length; i++) { updateList[i].update(); } } /** * Utility function to collect all intersection poinst of a single * line against group of other lines. * @param {g.line} line where to find points * @param {g.line[]} crossCheckLines lines to cross * @return {g.point[]} list of intersection points */ function findLineIntersections(line, crossCheckLines) { return _(crossCheckLines).map(function(crossCheckLine) { return line.intersection(crossCheckLine); }).compact().value(); } /** * Sorting function for list of points by their distance. * @param {g.point} p1 first point * @param {g.point} p2 second point * @return {number} squared distance between points */ function sortPoints(p1, p2) { return g.line(p1, p2).squaredLength(); } /** * Split input line into multiple based on intersection points. * @param {g.line} line input line to split * @param {g.point[]} intersections poinst where to split the line * @param {number} jumpSize the size of jump arc (length empty spot on a line) * @return {g.line[]} list of lines being split */ function createJumps(line, intersections, jumpSize) { return intersections.reduce(function(resultLines, point, idx) { // skipping points that were merged with the previous line // to make bigger arc over multiple lines that are close to each other if (point.skip === true) { return resultLines; } // always grab the last line from buffer and modify it var lastLine = resultLines.pop() || line; // calculate start and end of jump by moving by a given size of jump var jumpStart = g.point(point).move(lastLine.start, -(jumpSize)); var jumpEnd = g.point(point).move(lastLine.start, +(jumpSize)); // now try to look at the next intersection point var nextPoint = intersections[idx + 1]; if (nextPoint != null) { var distance = jumpEnd.distance(nextPoint); if (distance <= jumpSize) { // next point is close enough, move the jump end by this // difference and mark the next point to be skipped jumpEnd = nextPoint.move(lastLine.start, distance); nextPoint.skip = true; } } else { // this block is inside of `else` as an optimization so the distance is // not calculated when we know there are no other intersection points var endDistance = jumpStart.distance(lastLine.end); // if the end is too close to possible jump, draw remaining line instead of a jump if (endDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) { resultLines.push(lastLine); return resultLines; } } var startDistance = jumpEnd.distance(lastLine.start); if (startDistance < jumpSize * 2 + CLOSE_PROXIMITY_PADDING) { // if the start of line is too close to jump, draw that line instead of a jump resultLines.push(lastLine); return resultLines; } // finally create a jump line var jumpLine = g.line(jumpStart, jumpEnd); // it's just simple line but with a `isJump` property jumpLine.isJump = true; resultLines.push( g.line(lastLine.start, jumpStart), jumpLine, g.line(jumpEnd, lastLine.end) ); return resultLines; }, []); } /** * Assemble `D` attribute of a SVG path by iterating given lines. * @param {g.line[]} lines source lines to use * @param {number} jumpSize the size of jump arc (length empty spot on a line) * @return {string} */ function buildPath(lines, jumpSize, jumpType) { // first move to the start of a first line var start = ['M', lines[0].start.x, lines[0].start.y]; // make a paths from lines var paths = _(lines).map(function(line) { if (line.isJump) { var diff; if (jumpType === 'arc') { diff = line.start.difference(line.end); // determine rotation of arc based on difference between points var xAxisRotate = Number(diff.x < 0 && diff.y < 0); // for a jump line we create an arc instead return ['A', jumpSize, jumpSize, 0, 0, xAxisRotate, line.end.x, line.end.y]; } else if (jumpType === 'gap') { return ['M', line.end.x, line.end.y]; } else if (jumpType === 'cubic') { diff = line.start.difference(line.end); var angle = line.start.theta(line.end); var xOffset = jumpSize * 0.6; var yOffset = jumpSize * 1.35; // determine rotation of curve based on difference between points if (diff.x < 0 && diff.y < 0) { yOffset *= -1; } var controlStartPoint = g.point(line.start.x + xOffset, line.start.y + yOffset).rotate(line.start, angle); var controlEndPoint = g.point(line.end.x - xOffset, line.end.y + yOffset).rotate(line.end, angle); // create a cubic bezier curve return ['C', controlStartPoint.x, controlStartPoint.y, controlEndPoint.x, controlEndPoint.y, line.end.x, line.end.y]; } } return ['L', line.end.x, line.end.y]; }).flatten().value(); return [].concat(start, paths).join(' '); } /** * Actual connector function that will be run on every update. * @param {g.point} sourcePoint start point of this link * @param {g.point} targetPoint end point of this link * @param {g.point[]} vertices of this link * @param {object} opts options * @property {number} size optional size of a jump arc * @return {string} created `D` attribute of SVG path */ return function(sourcePoint, targetPoint, vertices, opts) { // eslint-disable-line max-params setupUpdating(this); var jumpSize = opts.size || JUMP_SIZE; var jumpType = opts.jump && ('' + opts.jump).toLowerCase(); var ignoreConnectors = opts.ignoreConnectors || IGNORED_CONNECTORS; // grab the first jump type as a default if specified one is invalid if (JUMP_TYPES.indexOf(jumpType) === -1) { jumpType = JUMP_TYPES[0]; } var paper = this.paper; var graph = paper.model; var allLinks = graph.getLinks(); // there is just one link, draw it directly if (allLinks.length === 1) { return buildPath( createLines(sourcePoint, targetPoint, vertices), jumpSize, jumpType ); } var thisModel = this.model; var thisIndex = allLinks.indexOf(thisModel); var defaultConnector = paper.options.defaultConnector || {}; // not all links are meant to be jumped over. var links = allLinks.filter(function(link, idx) { var connector = link.get('connector') || defaultConnector; // avoid jumping over links with connector type listed in `ignored connectors`. if (_.contains(ignoreConnectors, connector.name)) { return false; } // filter out links that are above this one and have the same connector type // otherwise there would double hoops for each intersection if (idx > thisIndex) { return connector.name !== 'jumpover'; } return true; }); // find views for all links var linkViews = links.map(function(link) { return paper.findViewByModel(link); }); // create lines for this link var thisLines = createLines( sourcePoint, targetPoint, vertices ); // create lines for all other links var linkLines = linkViews.map(function(linkView) { if (linkView == null) { return []; } if (linkView === this) { return thisLines; } return createLines( linkView.sourcePoint, linkView.targetPoint, linkView.route ); }, this); // transform lines for this link by splitting with jump lines at // points of intersection with other links var jumpingLines = thisLines.reduce(function(resultLines, thisLine) { // iterate all links and grab the intersections with this line // these are then sorted by distance so the line can be split more easily var intersections = _(links).map(function(link, i) { // don't intersection with itself if (link === thisModel) { return null; } return findLineIntersections(thisLine, linkLines[i]); }).flatten().compact().sortBy(_.partial(sortPoints, thisLine.start)).value(); if (intersections.length > 0) { // split the line based on found intersection points resultLines.push.apply(resultLines, createJumps(thisLine, intersections, jumpSize)); } else { // without any intersection the line goes uninterrupted resultLines.push(thisLine); } return resultLines; }, []); return buildPath(jumpingLines, jumpSize, jumpType); }; }(_, g)); joint.highlighters.addClass = { className: 'highlighted', highlight: function(cellView, magnetEl, opt) { var className = opt.className || this.className; V(magnetEl).addClass(className); }, unhighlight: function(cellView, magnetEl, opt) { var className = opt.className || this.className; V(magnetEl).removeClass(className); } }; joint.highlighters.opacity = { highlight: function(cellView, magnetEl, opt) { V(magnetEl).addClass('joint-highlight-opacity'); }, unhighlight: function(cellView, magnetEl, opt) { V(magnetEl).removeClass('joint-highlight-opacity'); } }; joint.highlighters.stroke = { defaultOptions: { padding: 3, rx: 0, ry: 0 }, _views: {}, highlight: function(cellView, magnetEl, opt) { // Only highlight once. if (this._views[magnetEl.id]) return; opt = _.defaults(opt || {}, this.defaultOptions); var magnetVel = V(magnetEl); var magnetBBox = magnetVel.bbox(true/* without transforms */); try { var pathData = magnetVel.convertToPathData(); } catch (error) { // Failed to get path data from magnet element. // Draw a rectangle around the entire cell view instead. pathData = V.rectToPath(_.extend({}, opt, magnetBBox)); } var highlightVel = V('path').attr({ d: pathData, 'class': 'joint-highlight-stroke', 'pointer-events': 'none' }); highlightVel.transform(cellView.el.getCTM().inverse()); highlightVel.transform(magnetEl.getCTM()); var padding = opt.padding; if (padding) { // Add padding to the highlight element. var cx = magnetBBox.x + (magnetBBox.width / 2); var cy = magnetBBox.y + (magnetBBox.height / 2); var sx = (magnetBBox.width + padding) / magnetBBox.width; var sy = (magnetBBox.height + padding) / magnetBBox.height; highlightVel.transform({ a: sx, b: 0, c: 0, d: sy, e: cx - sx * cx, f: cy - sy * cy }); } // This will handle the joint-theme-* class name for us. var highlightView = this._views[magnetEl.id] = new joint.mvc.View({ // This is necessary because we're passing in a vectorizer element (not jQuery). el: highlightVel.node, $el: highlightVel }); // Remove the highlight view when the cell is removed from the graph. highlightView.listenTo(cellView.model, 'remove', highlightView.remove); cellView.vel.append(highlightVel); }, unhighlight: function(cellView, magnetEl, opt) { opt = _.defaults(opt || {}, this.defaultOptions); if (this._views[magnetEl.id]) { this._views[magnetEl.id].remove(); this._views[magnetEl.id] = null; } } }; // JointJS library. // (c) 2011-2013 client IO joint.shapes.erd = {}; joint.shapes.erd.Entity = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'erd.Entity', size: { width: 150, height: 60 }, attrs: { '.outer': { fill: '#2ECC71', stroke: '#27AE60', 'stroke-width': 2, points: '100,0 100,60 0,60 0,0' }, '.inner': { fill: '#2ECC71', stroke: '#27AE60', 'stroke-width': 2, points: '95,5 95,55 5,55 5,5', display: 'none' }, text: { text: 'Entity', 'font-family': 'Arial', 'font-size': 14, ref: '.outer', 'ref-x': .5, 'ref-y': .5, 'x-alignment': 'middle', 'y-alignment': 'middle' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.erd.WeakEntity = joint.shapes.erd.Entity.extend({ defaults: joint.util.deepSupplement({ type: 'erd.WeakEntity', attrs: { '.inner' : { display: 'auto' }, text: { text: 'Weak Entity' } } }, joint.shapes.erd.Entity.prototype.defaults) }); joint.shapes.erd.Relationship = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'erd.Relationship', size: { width: 80, height: 80 }, attrs: { '.outer': { fill: '#3498DB', stroke: '#2980B9', 'stroke-width': 2, points: '40,0 80,40 40,80 0,40' }, '.inner': { fill: '#3498DB', stroke: '#2980B9', 'stroke-width': 2, points: '40,5 75,40 40,75 5,40', display: 'none' }, text: { text: 'Relationship', 'font-family': 'Arial', 'font-size': 12, ref: '.', 'ref-x': .5, 'ref-y': .5, 'x-alignment': 'middle', 'y-alignment': 'middle' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.erd.IdentifyingRelationship = joint.shapes.erd.Relationship.extend({ defaults: joint.util.deepSupplement({ type: 'erd.IdentifyingRelationship', attrs: { '.inner': { display: 'auto' }, text: { text: 'Identifying' } } }, joint.shapes.erd.Relationship.prototype.defaults) }); joint.shapes.erd.Attribute = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'erd.Attribute', size: { width: 100, height: 50 }, attrs: { 'ellipse': { transform: 'translate(50, 25)' }, '.outer': { stroke: '#D35400', 'stroke-width': 2, cx: 0, cy: 0, rx: 50, ry: 25, fill: '#E67E22' }, '.inner': { stroke: '#D35400', 'stroke-width': 2, cx: 0, cy: 0, rx: 45, ry: 20, fill: '#E67E22', display: 'none' }, text: { 'font-family': 'Arial', 'font-size': 14, ref: '.', 'ref-x': .5, 'ref-y': .5, 'x-alignment': 'middle', 'y-alignment': 'middle' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.erd.Multivalued = joint.shapes.erd.Attribute.extend({ defaults: joint.util.deepSupplement({ type: 'erd.Multivalued', attrs: { '.inner': { display: 'block' }, text: { text: 'multivalued' } } }, joint.shapes.erd.Attribute.prototype.defaults) }); joint.shapes.erd.Derived = joint.shapes.erd.Attribute.extend({ defaults: joint.util.deepSupplement({ type: 'erd.Derived', attrs: { '.outer': { 'stroke-dasharray': '3,5' }, text: { text: 'derived' } } }, joint.shapes.erd.Attribute.prototype.defaults) }); joint.shapes.erd.Key = joint.shapes.erd.Attribute.extend({ defaults: joint.util.deepSupplement({ type: 'erd.Key', attrs: { ellipse: { 'stroke-width': 4 }, text: { text: 'key', 'font-weight': '800', 'text-decoration': 'underline' } } }, joint.shapes.erd.Attribute.prototype.defaults) }); joint.shapes.erd.Normal = joint.shapes.erd.Attribute.extend({ defaults: joint.util.deepSupplement({ type: 'erd.Normal', attrs: { text: { text: 'Normal' }} }, joint.shapes.erd.Attribute.prototype.defaults) }); joint.shapes.erd.ISA = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'erd.ISA', size: { width: 100, height: 50 }, attrs: { polygon: { points: '0,0 50,50 100,0', fill: '#F1C40F', stroke: '#F39C12', 'stroke-width': 2 }, text: { text: 'ISA', 'font-size': 18, ref: 'polygon', 'ref-x': .5, 'ref-y': .3, 'x-alignment': 'middle', 'y-alignment': 'middle' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.erd.Line = joint.dia.Link.extend({ defaults: { type: 'erd.Line' }, cardinality: function(value) { this.set('labels', [{ position: -20, attrs: { text: { dy: -8, text: value }}}]); } }); joint.shapes.fsa = {}; joint.shapes.fsa.State = joint.shapes.basic.Circle.extend({ defaults: joint.util.deepSupplement({ type: 'fsa.State', attrs: { circle: { 'stroke-width': 3 }, text: { 'font-weight': '800' } } }, joint.shapes.basic.Circle.prototype.defaults) }); joint.shapes.fsa.StartState = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'fsa.StartState', size: { width: 20, height: 20 }, attrs: { circle: { transform: 'translate(10, 10)', r: 10, fill: '#000000' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.fsa.EndState = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'fsa.EndState', size: { width: 20, height: 20 }, attrs: { '.outer': { transform: 'translate(10, 10)', r: 10, fill: '#ffffff', stroke: '#000000' }, '.inner': { transform: 'translate(10, 10)', r: 6, fill: '#000000' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.fsa.Arrow = joint.dia.Link.extend({ defaults: joint.util.deepSupplement({ type: 'fsa.Arrow', attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' }}, smooth: true }, joint.dia.Link.prototype.defaults) }); // JointJS library. // (c) 2011-2013 client IO joint.shapes.org = {}; joint.shapes.org.Member = joint.dia.Element.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'org.Member', size: { width: 180, height: 70 }, attrs: { rect: { width: 170, height: 60 }, '.card': { fill: '#FFFFFF', stroke: '#000000', 'stroke-width': 2, 'pointer-events': 'visiblePainted', rx: 10, ry: 10 }, image: { width: 48, height: 48, ref: '.card', 'ref-x': 10, 'ref-y': 5 }, '.rank': { 'text-decoration': 'underline', ref: '.card', 'ref-x': 0.9, 'ref-y': 0.2, 'font-family': 'Courier New', 'font-size': 14, 'text-anchor': 'end' }, '.name': { 'font-weight': '800', ref: '.card', 'ref-x': 0.9, 'ref-y': 0.6, 'font-family': 'Courier New', 'font-size': 14, 'text-anchor': 'end' } } }, joint.dia.Element.prototype.defaults) }); joint.shapes.org.Arrow = joint.dia.Link.extend({ defaults: { type: 'org.Arrow', source: { selector: '.card' }, target: { selector: '.card' }, attrs: { '.connection': { stroke: '#585858', 'stroke-width': 3 }}, z: -1 } }); // JointJS library. // (c) 2011-2013 client IO joint.shapes.chess = {}; joint.shapes.chess.KingWhite = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.KingWhite', size: { width: 42, height: 38 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.KingBlack = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.KingBlack', size: { width: 42, height: 38 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.QueenWhite = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.QueenWhite', size: { width: 42, height: 38 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.QueenBlack = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.QueenBlack', size: { width: 42, height: 38 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.RookWhite = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.RookWhite', size: { width: 32, height: 34 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.RookBlack = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.RookBlack', size: { width: 32, height: 34 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.BishopWhite = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.BishopWhite', size: { width: 38, height: 38 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.BishopBlack = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.BishopBlack', size: { width: 38, height: 38 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.KnightWhite = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.KnightWhite', size: { width: 38, height: 37 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.KnightBlack = joint.shapes.basic.Generic.extend({ markup: ' ', defaults: joint.util.deepSupplement({ type: 'chess.KnightBlack', size: { width: 38, height: 37 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.PawnWhite = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'chess.PawnWhite', size: { width: 28, height: 33 } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.chess.PawnBlack = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'chess.PawnBlack', size: { width: 28, height: 33 } }, joint.shapes.basic.Generic.prototype.defaults) }); // JointJS library. // (c) 2011-2013 client IO joint.shapes.pn = {}; joint.shapes.pn.Place = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'pn.Place', size: { width: 50, height: 50 }, attrs: { '.root': { r: 25, fill: '#ffffff', stroke: '#000000', transform: 'translate(25, 25)' }, '.label': { 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': -20, ref: '.root', fill: '#000000', 'font-size': 12 }, '.tokens > circle': { fill: '#000000', r: 5 }, '.tokens.one > circle': { transform: 'translate(25, 25)' }, '.tokens.two > circle:nth-child(1)': { transform: 'translate(19, 25)' }, '.tokens.two > circle:nth-child(2)': { transform: 'translate(31, 25)' }, '.tokens.three > circle:nth-child(1)': { transform: 'translate(18, 29)' }, '.tokens.three > circle:nth-child(2)': { transform: 'translate(25, 19)' }, '.tokens.three > circle:nth-child(3)': { transform: 'translate(32, 29)' }, '.tokens.alot > text': { transform: 'translate(25, 18)', 'text-anchor': 'middle', fill: '#000000' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.pn.PlaceView = joint.dia.ElementView.extend({ initialize: function() { joint.dia.ElementView.prototype.initialize.apply(this, arguments); this.model.on('change:tokens', function() { this.renderTokens(); this.update(); }, this); }, render: function() { joint.dia.ElementView.prototype.render.apply(this, arguments); this.renderTokens(); this.update(); }, renderTokens: function() { var $tokens = this.$('.tokens').empty(); $tokens[0].className.baseVal = 'tokens'; var tokens = this.model.get('tokens'); if (!tokens) return; switch (tokens) { case 1: $tokens[0].className.baseVal += ' one'; $tokens.append(V('').node); break; case 2: $tokens[0].className.baseVal += ' two'; $tokens.append(V('').node, V('').node); break; case 3: $tokens[0].className.baseVal += ' three'; $tokens.append(V('').node, V('').node, V('').node); break; default: $tokens[0].className.baseVal += ' alot'; $tokens.append(V('').text(tokens + '' ).node); break; } } }); joint.shapes.pn.Transition = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'pn.Transition', size: { width: 12, height: 50 }, attrs: { 'rect': { width: 12, height: 50, fill: '#000000', stroke: '#000000' }, '.label': { 'text-anchor': 'middle', 'ref-x': .5, 'ref-y': -20, ref: 'rect', fill: '#000000', 'font-size': 12 } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.pn.Link = joint.dia.Link.extend({ defaults: joint.util.deepSupplement({ type: 'pn.Link', attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' }} }, joint.dia.Link.prototype.defaults) }); // JointJS library. // (c) 2011-2013 client IO joint.shapes.devs = {}; joint.shapes.devs.Model = joint.shapes.basic.Generic.extend(_.extend({}, joint.shapes.basic.PortsModelInterface, { markup: '', portMarkup: '', defaults: joint.util.deepSupplement({ type: 'devs.Model', size: { width: 1, height: 1 }, inPorts: [], outPorts: [], attrs: { '.': { magnet: false }, '.body': { width: 150, height: 250, stroke: '#000000' }, '.port-body': { r: 10, magnet: true, stroke: '#000000' }, text: { 'pointer-events': 'none' }, '.label': { text: 'Model', 'ref-x': .5, 'ref-y': 10, ref: '.body', 'text-anchor': 'middle', fill: '#000000' }, '.inPorts .port-label': { x:-15, dy: 4, 'text-anchor': 'end', fill: '#000000' }, '.outPorts .port-label':{ x: 15, dy: 4, fill: '#000000' } } }, joint.shapes.basic.Generic.prototype.defaults), getPortAttrs: function(portName, index, total, selector, type) { var attrs = {}; var portClass = 'port' + index; var portSelector = selector + '>.' + portClass; var portLabelSelector = portSelector + '>.port-label'; var portBodySelector = portSelector + '>.port-body'; attrs[portLabelSelector] = { text: portName }; attrs[portBodySelector] = { port: { id: portName || _.uniqueId(type) , type: type } }; attrs[portSelector] = { ref: '.body', 'ref-y': (index + 0.5) * (1 / total) }; if (selector === '.outPorts') { attrs[portSelector]['ref-dx'] = 0; } return attrs; } })); joint.shapes.devs.Atomic = joint.shapes.devs.Model.extend({ defaults: joint.util.deepSupplement({ type: 'devs.Atomic', size: { width: 80, height: 80 }, attrs: { '.body': { fill: 'salmon' }, '.label': { text: 'Atomic' }, '.inPorts .port-body': { fill: 'PaleGreen' }, '.outPorts .port-body': { fill: 'Tomato' } } }, joint.shapes.devs.Model.prototype.defaults) }); joint.shapes.devs.Coupled = joint.shapes.devs.Model.extend({ defaults: joint.util.deepSupplement({ type: 'devs.Coupled', size: { width: 200, height: 300 }, attrs: { '.body': { fill: 'seaGreen' }, '.label': { text: 'Coupled' }, '.inPorts .port-body': { fill: 'PaleGreen' }, '.outPorts .port-body': { fill: 'Tomato' } } }, joint.shapes.devs.Model.prototype.defaults) }); joint.shapes.devs.Link = joint.dia.Link.extend({ defaults: { type: 'devs.Link', attrs: { '.connection' : { 'stroke-width' : 2 }} } }); joint.shapes.devs.ModelView = joint.dia.ElementView.extend(joint.shapes.basic.PortsViewInterface); joint.shapes.devs.AtomicView = joint.shapes.devs.ModelView; joint.shapes.devs.CoupledView = joint.shapes.devs.ModelView; joint.shapes.uml = {}; joint.shapes.uml.Class = joint.shapes.basic.Generic.extend({ markup: [ '', '', '', '', '', '' ].join(''), defaults: joint.util.deepSupplement({ type: 'uml.Class', attrs: { rect: { 'width': 200 }, '.uml-class-name-rect': { 'stroke': 'black', 'stroke-width': 2, 'fill': '#3498db' }, '.uml-class-attrs-rect': { 'stroke': 'black', 'stroke-width': 2, 'fill': '#2980b9' }, '.uml-class-methods-rect': { 'stroke': 'black', 'stroke-width': 2, 'fill': '#2980b9' }, '.uml-class-name-text': { 'ref': '.uml-class-name-rect', 'ref-y': .5, 'ref-x': .5, 'text-anchor': 'middle', 'y-alignment': 'middle', 'font-weight': 'bold', 'fill': 'black', 'font-size': 12, 'font-family': 'Times New Roman' }, '.uml-class-attrs-text': { 'ref': '.uml-class-attrs-rect', 'ref-y': 5, 'ref-x': 5, 'fill': 'black', 'font-size': 12, 'font-family': 'Times New Roman' }, '.uml-class-methods-text': { 'ref': '.uml-class-methods-rect', 'ref-y': 5, 'ref-x': 5, 'fill': 'black', 'font-size': 12, 'font-family': 'Times New Roman' } }, name: [], attributes: [], methods: [] }, joint.shapes.basic.Generic.prototype.defaults), initialize: function() { this.on('change:name change:attributes change:methods', function() { this.updateRectangles(); this.trigger('uml-update'); }, this); this.updateRectangles(); joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments); }, getClassName: function() { return this.get('name'); }, updateRectangles: function() { var attrs = this.get('attrs'); var rects = [ { type: 'name', text: this.getClassName() }, { type: 'attrs', text: this.get('attributes') }, { type: 'methods', text: this.get('methods') } ]; var offsetY = 0; _.each(rects, function(rect) { var lines = _.isArray(rect.text) ? rect.text : [rect.text]; var rectHeight = lines.length * 20 + 20; attrs['.uml-class-' + rect.type + '-text'].text = lines.join('\n'); attrs['.uml-class-' + rect.type + '-rect'].height = rectHeight; attrs['.uml-class-' + rect.type + '-rect'].transform = 'translate(0,' + offsetY + ')'; offsetY += rectHeight; }); } }); joint.shapes.uml.ClassView = joint.dia.ElementView.extend({ initialize: function() { joint.dia.ElementView.prototype.initialize.apply(this, arguments); this.listenTo(this.model, 'uml-update', function() { this.update(); this.resize(); }); } }); joint.shapes.uml.Abstract = joint.shapes.uml.Class.extend({ defaults: joint.util.deepSupplement({ type: 'uml.Abstract', attrs: { '.uml-class-name-rect': { fill : '#e74c3c' }, '.uml-class-attrs-rect': { fill : '#c0392b' }, '.uml-class-methods-rect': { fill : '#c0392b' } } }, joint.shapes.uml.Class.prototype.defaults), getClassName: function() { return ['<>', this.get('name')]; } }); joint.shapes.uml.AbstractView = joint.shapes.uml.ClassView; joint.shapes.uml.Interface = joint.shapes.uml.Class.extend({ defaults: joint.util.deepSupplement({ type: 'uml.Interface', attrs: { '.uml-class-name-rect': { fill : '#f1c40f' }, '.uml-class-attrs-rect': { fill : '#f39c12' }, '.uml-class-methods-rect': { fill : '#f39c12' } } }, joint.shapes.uml.Class.prototype.defaults), getClassName: function() { return ['<>', this.get('name')]; } }); joint.shapes.uml.InterfaceView = joint.shapes.uml.ClassView; joint.shapes.uml.Generalization = joint.dia.Link.extend({ defaults: { type: 'uml.Generalization', attrs: { '.marker-target': { d: 'M 20 0 L 0 10 L 20 20 z', fill: 'white' }} } }); joint.shapes.uml.Implementation = joint.dia.Link.extend({ defaults: { type: 'uml.Implementation', attrs: { '.marker-target': { d: 'M 20 0 L 0 10 L 20 20 z', fill: 'white' }, '.connection': { 'stroke-dasharray': '3,3' } } } }); joint.shapes.uml.Aggregation = joint.dia.Link.extend({ defaults: { type: 'uml.Aggregation', attrs: { '.marker-target': { d: 'M 40 10 L 20 20 L 0 10 L 20 0 z', fill: 'white' }} } }); joint.shapes.uml.Composition = joint.dia.Link.extend({ defaults: { type: 'uml.Composition', attrs: { '.marker-target': { d: 'M 40 10 L 20 20 L 0 10 L 20 0 z', fill: 'black' }} } }); joint.shapes.uml.Association = joint.dia.Link.extend({ defaults: { type: 'uml.Association' } }); // Statechart joint.shapes.uml.State = joint.shapes.basic.Generic.extend({ markup: [ '', '', '', '', '', '', '', '' ].join(''), defaults: joint.util.deepSupplement({ type: 'uml.State', attrs: { '.uml-state-body': { 'width': 200, 'height': 200, 'rx': 10, 'ry': 10, 'fill': '#ecf0f1', 'stroke': '#bdc3c7', 'stroke-width': 3 }, '.uml-state-separator': { 'stroke': '#bdc3c7', 'stroke-width': 2 }, '.uml-state-name': { 'ref': '.uml-state-body', 'ref-x': .5, 'ref-y': 5, 'text-anchor': 'middle', 'fill': '#000000', 'font-family': 'Courier New', 'font-size': 14 }, '.uml-state-events': { 'ref': '.uml-state-separator', 'ref-x': 5, 'ref-y': 5, 'fill': '#000000', 'font-family': 'Courier New', 'font-size': 14 } }, name: 'State', events: [] }, joint.shapes.basic.Generic.prototype.defaults), initialize: function() { this.on({ 'change:name': this.updateName, 'change:events': this.updateEvents, 'change:size': this.updatePath }, this); this.updateName(); this.updateEvents(); this.updatePath(); joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments); }, updateName: function() { this.attr('.uml-state-name/text', this.get('name')); }, updateEvents: function() { this.attr('.uml-state-events/text', this.get('events').join('\n')); }, updatePath: function() { var d = 'M 0 20 L ' + this.get('size').width + ' 20'; // We are using `silent: true` here because updatePath() is meant to be called // on resize and there's no need to to update the element twice (`change:size` // triggers also an update). this.attr('.uml-state-separator/d', d, { silent: true }); } }); joint.shapes.uml.StartState = joint.shapes.basic.Circle.extend({ defaults: joint.util.deepSupplement({ type: 'uml.StartState', attrs: { circle: { 'fill': '#34495e', 'stroke': '#2c3e50', 'stroke-width': 2, 'rx': 1 }} }, joint.shapes.basic.Circle.prototype.defaults) }); joint.shapes.uml.EndState = joint.shapes.basic.Generic.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'uml.EndState', size: { width: 20, height: 20 }, attrs: { 'circle.outer': { transform: 'translate(10, 10)', r: 10, fill: '#ffffff', stroke: '#2c3e50' }, 'circle.inner': { transform: 'translate(10, 10)', r: 6, fill: '#34495e' } } }, joint.shapes.basic.Generic.prototype.defaults) }); joint.shapes.uml.Transition = joint.dia.Link.extend({ defaults: { type: 'uml.Transition', attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', fill: '#34495e', stroke: '#2c3e50' }, '.connection': { stroke: '#2c3e50' } } } }); // JointJS library. // (c) 2011-2013 client IO joint.shapes.logic = {}; joint.shapes.logic.Gate = joint.shapes.basic.Generic.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Gate', size: { width: 80, height: 40 }, attrs: { '.': { magnet: false }, '.body': { width: 100, height: 50 }, circle: { r: 7, stroke: 'black', fill: 'transparent', 'stroke-width': 2 } } }, joint.shapes.basic.Generic.prototype.defaults), operation: function() { return true; } }); joint.shapes.logic.IO = joint.shapes.logic.Gate.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'logic.IO', size: { width: 60, height: 30 }, attrs: { '.body': { fill: 'white', stroke: 'black', 'stroke-width': 2 }, '.wire': { ref: '.body', 'ref-y': .5, stroke: 'black' }, text: { fill: 'black', ref: '.body', 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', 'text-anchor': 'middle', 'font-weight': 'bold', 'font-variant': 'small-caps', 'text-transform': 'capitalize', 'font-size': '14px' } } }, joint.shapes.logic.Gate.prototype.defaults) }); joint.shapes.logic.Input = joint.shapes.logic.IO.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Input', attrs: { '.wire': { 'ref-dx': 0, d: 'M 0 0 L 23 0' }, circle: { ref: '.body', 'ref-dx': 30, 'ref-y': 0.5, magnet: true, 'class': 'output', port: 'out' }, text: { text: 'input' } } }, joint.shapes.logic.IO.prototype.defaults) }); joint.shapes.logic.Output = joint.shapes.logic.IO.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Output', attrs: { '.wire': { 'ref-x': 0, d: 'M 0 0 L -23 0' }, circle: { ref: '.body', 'ref-x': -30, 'ref-y': 0.5, magnet: 'passive', 'class': 'input', port: 'in' }, text: { text: 'output' } } }, joint.shapes.logic.IO.prototype.defaults) }); joint.shapes.logic.Gate11 = joint.shapes.logic.Gate.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'logic.Gate11', attrs: { '.input': { ref: '.body', 'ref-x': -2, 'ref-y': 0.5, magnet: 'passive', port: 'in' }, '.output': { ref: '.body', 'ref-dx': 2, 'ref-y': 0.5, magnet: true, port: 'out' } } }, joint.shapes.logic.Gate.prototype.defaults) }); joint.shapes.logic.Gate21 = joint.shapes.logic.Gate.extend({ markup: '', defaults: joint.util.deepSupplement({ type: 'logic.Gate21', attrs: { '.input1': { ref: '.body', 'ref-x': -2, 'ref-y': 0.3, magnet: 'passive', port: 'in1' }, '.input2': { ref: '.body', 'ref-x': -2, 'ref-y': 0.7, magnet: 'passive', port: 'in2' }, '.output': { ref: '.body', 'ref-dx': 2, 'ref-y': 0.5, magnet: true, port: 'out' } } }, joint.shapes.logic.Gate.prototype.defaults) }); joint.shapes.logic.Repeater = joint.shapes.logic.Gate11.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Repeater', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PVCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjgiCiAgICAgaW5rc2NhcGU6Y3g9Ijg0LjY4NTM1MiIKICAgICBpbmtzY2FwZTpjeT0iMTUuMjg4NjI4IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzIuMTU2OTEsMjUgTCA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAyOS4wNDM0NzgsMjUgTCA1LjA0MzQ3ODEsMjUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSAyOC45Njg3NSwyLjU5Mzc1IEwgMjguOTY4NzUsNSBMIDI4Ljk2ODc1LDQ1IEwgMjguOTY4NzUsNDcuNDA2MjUgTCAzMS4xMjUsNDYuMzQzNzUgTCA3Mi4xNTYyNSwyNi4zNDM3NSBMIDcyLjE1NjI1LDIzLjY1NjI1IEwgMzEuMTI1LDMuNjU2MjUgTCAyOC45Njg3NSwyLjU5Mzc1IHogTSAzMS45Njg3NSw3LjQwNjI1IEwgNjguMDkzNzUsMjUgTCAzMS45Njg3NSw0Mi41OTM3NSBMIDMxLjk2ODc1LDcuNDA2MjUgeiIKICAgICAgIGlkPSJwYXRoMjYzOCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjY2NjYyIgLz4KICA8L2c+Cjwvc3ZnPgo=' }} }, joint.shapes.logic.Gate11.prototype.defaults), operation: function(input) { return input; } }); joint.shapes.logic.Not = joint.shapes.logic.Gate11.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Not', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PVCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjgiCiAgICAgaW5rc2NhcGU6Y3g9Ijg0LjY4NTM1MiIKICAgICBpbmtzY2FwZTpjeT0iMTUuMjg4NjI4IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzkuMTU2OTEsMjUgTCA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAyOS4wNDM0NzgsMjUgTCA1LjA0MzQ3ODEsMjUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSAyOC45Njg3NSwyLjU5Mzc1IEwgMjguOTY4NzUsNSBMIDI4Ljk2ODc1LDQ1IEwgMjguOTY4NzUsNDcuNDA2MjUgTCAzMS4xMjUsNDYuMzQzNzUgTCA3Mi4xNTYyNSwyNi4zNDM3NSBMIDcyLjE1NjI1LDIzLjY1NjI1IEwgMzEuMTI1LDMuNjU2MjUgTCAyOC45Njg3NSwyLjU5Mzc1IHogTSAzMS45Njg3NSw3LjQwNjI1IEwgNjguMDkzNzUsMjUgTCAzMS45Njg3NSw0Mi41OTM3NSBMIDMxLjk2ODc1LDcuNDA2MjUgeiIKICAgICAgIGlkPSJwYXRoMjYzOCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2NjY2NjY2NjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDI2NzEiCiAgICAgICBzb2RpcG9kaTpjeD0iNzYiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA4MCwyNSBBIDQsNCAwIDEgMSA3MiwyNSBBIDQsNCAwIDEgMSA4MCwyNSB6IgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEsMCkiIC8+CiAgPC9nPgo8L3N2Zz4K' }} }, joint.shapes.logic.Gate11.prototype.defaults), operation: function(input) { return !input; } }); joint.shapes.logic.Or = joint.shapes.logic.Gate21.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Or', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik9SIEFOU0kuc3ZnIgogICBpbmtzY2FwZTpvdXRwdXRfZXh0ZW5zaW9uPSJvcmcuaW5rc2NhcGUub3V0cHV0LnN2Zy5pbmtzY2FwZSI+CiAgPGRlZnMKICAgICBpZD0iZGVmczQiPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjUwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjI1IDogMTAgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjcxNCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iMSA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMC41IDogMC4zMzMzMzMzMyA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyODA2IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyODE5IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjM3Mi4wNDcyNCA6IDM1MC43ODczOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI3NDQuMDk0NDggOiA1MjYuMTgxMDkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzc3IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49Ijc1IDogNDAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iMTUwIDogNjAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDYwIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTMyNzUiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNTAgOiAzMy4zMzMzMzMgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iMTAwIDogNTAgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDUwIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTU1MzMiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzIgOiAyMS4zMzMzMzMgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNjQgOiAzMiA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMzIgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjU1NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDE2LjY2NjY2NyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDI1IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAyNSA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iNCIKICAgICBpbmtzY2FwZTpjeD0iMTEzLjAwMDM5IgogICAgIGlua3NjYXBlOmN5PSIxMi44OTM3MzEiCiAgICAgaW5rc2NhcGU6ZG9jdW1lbnQtdW5pdHM9InB4IgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImcyNTYwIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTpncmlkLWJib3g9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1wb2ludHM9InRydWUiCiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAwMDAiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxMzk5IgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9Ijg3NCIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMzciCiAgICAgaW5rc2NhcGU6d2luZG93LXk9Ii00IgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNzAsMjUgYyAyMCwwIDI1LDAgMjUsMCIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMSwxNSA1LDE1IgogICAgICAgaWQ9InBhdGgzMDYxIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzIsMzUgNSwzNSIKICAgICAgIGlkPSJwYXRoMzk0NCIgLz4KICAgIDxnCiAgICAgICBpZD0iZzI1NjAiCiAgICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2LjUsLTM5LjUpIj4KICAgICAgPHBhdGgKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjQwNjI1LDQ0LjUgTCAtMC40MDYyNSw0Ni45Mzc1IEMgLTAuNDA2MjUsNDYuOTM3NSA1LjI1LDUzLjkzNzU0OSA1LjI1LDY0LjUgQyA1LjI1LDc1LjA2MjQ1MSAtMC40MDYyNSw4Mi4wNjI1IC0wLjQwNjI1LDgyLjA2MjUgTCAtMi40MDYyNSw4NC41IEwgMC43NSw4NC41IEwgMTQuNzUsODQuNSBDIDE3LjE1ODA3Niw4NC41MDAwMDEgMjIuNDM5Njk5LDg0LjUyNDUxNCAyOC4zNzUsODIuMDkzNzUgQyAzNC4zMTAzMDEsNzkuNjYyOTg2IDQwLjkxMTUzNiw3NC43NTA0ODQgNDYuMDYyNSw2NS4yMTg3NSBMIDQ0Ljc1LDY0LjUgTCA0Ni4wNjI1LDYzLjc4MTI1IEMgMzUuNzU5Mzg3LDQ0LjcxNTU5IDE5LjUwNjU3NCw0NC41IDE0Ljc1LDQ0LjUgTCAwLjc1LDQ0LjUgTCAtMi40MDYyNSw0NC41IHogTSAzLjQ2ODc1LDQ3LjUgTCAxNC43NSw0Ny41IEMgMTkuNDM0MTczLDQ3LjUgMzMuMDM2ODUsNDcuMzY5NzkzIDQyLjcxODc1LDY0LjUgQyAzNy45NTE5NjQsNzIuOTI5MDc1IDMyLjE5NzQ2OSw3Ny4xODM5MSAyNyw3OS4zMTI1IEMgMjEuNjM5MzM5LDgxLjUwNzkyNCAxNy4xNTgwNzUsODEuNTAwMDAxIDE0Ljc1LDgxLjUgTCAzLjUsODEuNSBDIDUuMzczNTg4NCw3OC4zOTE1NjYgOC4yNSw3Mi40NTA2NSA4LjI1LDY0LjUgQyA4LjI1LDU2LjUyNjY0NiA1LjM0MTQ2ODYsNTAuNTk5ODE1IDMuNDY4NzUsNDcuNSB6IgogICAgICAgICBpZD0icGF0aDQ5NzMiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NzY2NjY3NjY2NjY2NjY2NzY2NzYyIgLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo=' }} }, joint.shapes.logic.Gate21.prototype.defaults), operation: function(input1, input2) { return input1 || input2; } }); joint.shapes.logic.And = joint.shapes.logic.Gate21.extend({ defaults: joint.util.deepSupplement({ type: 'logic.And', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IkFORCBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgPC9kZWZzPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSI4IgogICAgIGlua3NjYXBlOmN4PSI1Ni42OTgzNDgiCiAgICAgaW5rc2NhcGU6Y3k9IjI1LjMyNjg5OSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDcwLDI1IGMgMjAsMCAyNSwwIDI1LDAiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEsMTUgNSwxNSIKICAgICAgIGlkPSJwYXRoMzA2MSIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjk5OTk5OTg4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMyLDM1IDUsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTptZWRpdW07Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDt0ZXh0LWluZGVudDowO3RleHQtYWxpZ246c3RhcnQ7dGV4dC1kZWNvcmF0aW9uOm5vbmU7bGluZS1oZWlnaHQ6bm9ybWFsO2xldHRlci1zcGFjaW5nOm5vcm1hbDt3b3JkLXNwYWNpbmc6bm9ybWFsO3RleHQtdHJhbnNmb3JtOm5vbmU7ZGlyZWN0aW9uOmx0cjtibG9jay1wcm9ncmVzc2lvbjp0Yjt3cml0aW5nLW1vZGU6bHItdGI7dGV4dC1hbmNob3I6c3RhcnQ7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDozO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGU7Zm9udC1mYW1pbHk6Qml0c3RyZWFtIFZlcmEgU2FuczstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOkJpdHN0cmVhbSBWZXJhIFNhbnMiCiAgICAgICBkPSJNIDMwLDUgTCAzMCw2LjQyODU3MTQgTCAzMCw0My41NzE0MjkgTCAzMCw0NSBMIDMxLjQyODU3MSw0NSBMIDUwLjQ3NjE5LDQ1IEMgNjEuNzQ0MDk4LDQ1IDcwLjQ3NjE5LDM1Ljk5OTk1NSA3MC40NzYxOSwyNSBDIDcwLjQ3NjE5LDE0LjAwMDA0NSA2MS43NDQwOTksNS4wMDAwMDAyIDUwLjQ3NjE5LDUgQyA1MC40NzYxOSw1IDUwLjQ3NjE5LDUgMzEuNDI4NTcxLDUgTCAzMCw1IHogTSAzMi44NTcxNDMsNy44NTcxNDI5IEMgNDAuODM0MjY0LDcuODU3MTQyOSA0NS45MTgzNjgsNy44NTcxNDI5IDQ4LjA5NTIzOCw3Ljg1NzE0MjkgQyA0OS4yODU3MTQsNy44NTcxNDI5IDQ5Ljg4MDk1Miw3Ljg1NzE0MjkgNTAuMTc4NTcxLDcuODU3MTQyOSBDIDUwLjMyNzM4MSw3Ljg1NzE0MjkgNTAuNDA5MjI3LDcuODU3MTQyOSA1MC40NDY0MjksNy44NTcxNDI5IEMgNTAuNDY1MDI5LDcuODU3MTQyOSA1MC40NzE1NDMsNy44NTcxNDI5IDUwLjQ3NjE5LDcuODU3MTQyOSBDIDYwLjIzNjg1Myw3Ljg1NzE0MyA2Ny4xNDI4NTcsMTUuNDk3MDk4IDY3LjE0Mjg1NywyNSBDIDY3LjE0Mjg1NywzNC41MDI5MDIgNTkuNzYwNjYyLDQyLjE0Mjg1NyA1MCw0Mi4xNDI4NTcgTCAzMi44NTcxNDMsNDIuMTQyODU3IEwgMzIuODU3MTQzLDcuODU3MTQyOSB6IgogICAgICAgaWQ9InBhdGgyODg0IgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NjY2NzY2NjY3Nzc3NzY2NjIiAvPgogIDwvZz4KPC9zdmc+Cg==' }} }, joint.shapes.logic.Gate21.prototype.defaults), operation: function(input1, input2) { return input1 && input2; } }); joint.shapes.logic.Nor = joint.shapes.logic.Gate21.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Nor', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5PUiBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjEiCiAgICAgaW5rc2NhcGU6Y3g9Ijc4LjY3NzY0NCIKICAgICBpbmtzY2FwZTpjeT0iMjIuMTAyMzQ0IgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjM3IgogICAgIGlua3NjYXBlOndpbmRvdy15PSItNCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDc5LDI1IEMgOTksMjUgOTUsMjUgOTUsMjUiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEsMTUgNSwxNSIKICAgICAgIGlkPSJwYXRoMzA2MSIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjk5OTk5OTg4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMyLDM1IDUsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8ZwogICAgICAgaWQ9ImcyNTYwIgogICAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNi41LC0zOS41KSI+CiAgICAgIDxwYXRoCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi40MDYyNSw0NC41IEwgLTAuNDA2MjUsNDYuOTM3NSBDIC0wLjQwNjI1LDQ2LjkzNzUgNS4yNSw1My45Mzc1NDkgNS4yNSw2NC41IEMgNS4yNSw3NS4wNjI0NTEgLTAuNDA2MjUsODIuMDYyNSAtMC40MDYyNSw4Mi4wNjI1IEwgLTIuNDA2MjUsODQuNSBMIDAuNzUsODQuNSBMIDE0Ljc1LDg0LjUgQyAxNy4xNTgwNzYsODQuNTAwMDAxIDIyLjQzOTY5OSw4NC41MjQ1MTQgMjguMzc1LDgyLjA5Mzc1IEMgMzQuMzEwMzAxLDc5LjY2Mjk4NiA0MC45MTE1MzYsNzQuNzUwNDg0IDQ2LjA2MjUsNjUuMjE4NzUgTCA0NC43NSw2NC41IEwgNDYuMDYyNSw2My43ODEyNSBDIDM1Ljc1OTM4Nyw0NC43MTU1OSAxOS41MDY1NzQsNDQuNSAxNC43NSw0NC41IEwgMC43NSw0NC41IEwgLTIuNDA2MjUsNDQuNSB6IE0gMy40Njg3NSw0Ny41IEwgMTQuNzUsNDcuNSBDIDE5LjQzNDE3Myw0Ny41IDMzLjAzNjg1LDQ3LjM2OTc5MyA0Mi43MTg3NSw2NC41IEMgMzcuOTUxOTY0LDcyLjkyOTA3NSAzMi4xOTc0NjksNzcuMTgzOTEgMjcsNzkuMzEyNSBDIDIxLjYzOTMzOSw4MS41MDc5MjQgMTcuMTU4MDc1LDgxLjUwMDAwMSAxNC43NSw4MS41IEwgMy41LDgxLjUgQyA1LjM3MzU4ODQsNzguMzkxNTY2IDguMjUsNzIuNDUwNjUgOC4yNSw2NC41IEMgOC4yNSw1Ni41MjY2NDYgNS4zNDE0Njg2LDUwLjU5OTgxNSAzLjQ2ODc1LDQ3LjUgeiIKICAgICAgICAgaWQ9InBhdGg0OTczIgogICAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjc2NjY2NzY2NjY2NjY2Njc2Njc2MiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIHNvZGlwb2RpOnR5cGU9ImFyYyIKICAgICAgICAgc3R5bGU9ImZpbGw6bm9uZTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWpvaW46bWl0ZXI7bWFya2VyOm5vbmU7c3Ryb2tlLW9wYWNpdHk6MTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgICBpZD0icGF0aDI2MDQiCiAgICAgICAgIHNvZGlwb2RpOmN4PSI3NSIKICAgICAgICAgc29kaXBvZGk6Y3k9IjI1IgogICAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgICAgc29kaXBvZGk6cnk9IjQiCiAgICAgICAgIGQ9Ik0gNzksMjUgQSA0LDQgMCAxIDEgNzEsMjUgQSA0LDQgMCAxIDEgNzksMjUgeiIKICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTI2LjUsMzkuNSkiIC8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K' }} }, joint.shapes.logic.Gate21.prototype.defaults), operation: function(input1, input2) { return !(input1 || input2); } }); joint.shapes.logic.Nand = joint.shapes.logic.Gate21.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Nand', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik5BTkQgQU5TSS5zdmciCiAgIGlua3NjYXBlOm91dHB1dF9leHRlbnNpb249Im9yZy5pbmtzY2FwZS5vdXRwdXQuc3ZnLmlua3NjYXBlIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCI+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxMCA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzE0IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIwLjUgOiAwLjMzMzMzMzMzIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MDYiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MTkiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzcyLjA0NzI0IDogMzUwLjc4NzM5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9Ijc0NC4wOTQ0OCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTI2LjE4MTA5IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3NzciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNzUgOiA0MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxNTAgOiA2MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNjAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMzI3NSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI1MCA6IDMzLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxMDAgOiA1MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlNTUzMyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzMiA6IDIxLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI2NCA6IDMyIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAzMiA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9ImJhc2UiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6em9vbT0iMTYiCiAgICAgaW5rc2NhcGU6Y3g9Ijc4LjI4MzMwNyIKICAgICBpbmtzY2FwZTpjeT0iMTYuNDQyODQzIgogICAgIGlua3NjYXBlOmRvY3VtZW50LXVuaXRzPSJweCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiCiAgICAgc2hvd2dyaWQ9InRydWUiCiAgICAgaW5rc2NhcGU6Z3JpZC1iYm94PSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtcG9pbnRzPSJ0cnVlIgogICAgIGdyaWR0b2xlcmFuY2U9IjEwMDAwIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTM5OSIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI4NzQiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjMzIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIwIgogICAgIGlua3NjYXBlOnNuYXAtYmJveD0idHJ1ZSI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgaWQ9IkdyaWRGcm9tUHJlMDQ2U2V0dGluZ3MiCiAgICAgICB0eXBlPSJ4eWdyaWQiCiAgICAgICBvcmlnaW54PSIwcHgiCiAgICAgICBvcmlnaW55PSIwcHgiCiAgICAgICBzcGFjaW5neD0iMXB4IgogICAgICAgc3BhY2luZ3k9IjFweCIKICAgICAgIGNvbG9yPSIjMDAwMGZmIgogICAgICAgZW1wY29sb3I9IiMwMDAwZmYiCiAgICAgICBvcGFjaXR5PSIwLjIiCiAgICAgICBlbXBvcGFjaXR5PSIwLjQiCiAgICAgICBlbXBzcGFjaW5nPSI1IgogICAgICAgdmlzaWJsZT0idHJ1ZSIKICAgICAgIGVuYWJsZWQ9InRydWUiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhNyI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gNzksMjUgQyA5MS44LDI1IDk1LDI1IDk1LDI1IgogICAgICAgaWQ9InBhdGgzMDU5IgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDMxLDE1IDUsMTUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk4ODtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMiwzNSA1LDM1IgogICAgICAgaWQ9InBhdGgzOTQ0IiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmb250LXNpemU6bWVkaXVtO2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7dGV4dC1pbmRlbnQ6MDt0ZXh0LWFsaWduOnN0YXJ0O3RleHQtZGVjb3JhdGlvbjpub25lO2xpbmUtaGVpZ2h0Om5vcm1hbDtsZXR0ZXItc3BhY2luZzpub3JtYWw7d29yZC1zcGFjaW5nOm5vcm1hbDt0ZXh0LXRyYW5zZm9ybTpub25lO2RpcmVjdGlvbjpsdHI7YmxvY2stcHJvZ3Jlc3Npb246dGI7d3JpdGluZy1tb2RlOmxyLXRiO3RleHQtYW5jaG9yOnN0YXJ0O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MzttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlO2ZvbnQtZmFtaWx5OkJpdHN0cmVhbSBWZXJhIFNhbnM7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpCaXRzdHJlYW0gVmVyYSBTYW5zIgogICAgICAgZD0iTSAzMCw1IEwgMzAsNi40Mjg1NzE0IEwgMzAsNDMuNTcxNDI5IEwgMzAsNDUgTCAzMS40Mjg1NzEsNDUgTCA1MC40NzYxOSw0NSBDIDYxLjc0NDA5OCw0NSA3MC40NzYxOSwzNS45OTk5NTUgNzAuNDc2MTksMjUgQyA3MC40NzYxOSwxNC4wMDAwNDUgNjEuNzQ0MDk5LDUuMDAwMDAwMiA1MC40NzYxOSw1IEMgNTAuNDc2MTksNSA1MC40NzYxOSw1IDMxLjQyODU3MSw1IEwgMzAsNSB6IE0gMzIuODU3MTQzLDcuODU3MTQyOSBDIDQwLjgzNDI2NCw3Ljg1NzE0MjkgNDUuOTE4MzY4LDcuODU3MTQyOSA0OC4wOTUyMzgsNy44NTcxNDI5IEMgNDkuMjg1NzE0LDcuODU3MTQyOSA0OS44ODA5NTIsNy44NTcxNDI5IDUwLjE3ODU3MSw3Ljg1NzE0MjkgQyA1MC4zMjczODEsNy44NTcxNDI5IDUwLjQwOTIyNyw3Ljg1NzE0MjkgNTAuNDQ2NDI5LDcuODU3MTQyOSBDIDUwLjQ2NTAyOSw3Ljg1NzE0MjkgNTAuNDcxNTQzLDcuODU3MTQyOSA1MC40NzYxOSw3Ljg1NzE0MjkgQyA2MC4yMzY4NTMsNy44NTcxNDMgNjcuMTQyODU3LDE1LjQ5NzA5OCA2Ny4xNDI4NTcsMjUgQyA2Ny4xNDI4NTcsMzQuNTAyOTAyIDU5Ljc2MDY2Miw0Mi4xNDI4NTcgNTAsNDIuMTQyODU3IEwgMzIuODU3MTQzLDQyLjE0Mjg1NyBMIDMyLjg1NzE0Myw3Ljg1NzE0MjkgeiIKICAgICAgIGlkPSJwYXRoMjg4NCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2Njc2NjY2Nzc3Nzc2NjYyIgLz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDQwMDgiCiAgICAgICBzb2RpcG9kaTpjeD0iNzUiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA3OSwyNSBBIDQsNCAwIDEgMSA3MSwyNSBBIDQsNCAwIDEgMSA3OSwyNSB6IiAvPgogIDwvZz4KPC9zdmc+Cg==' }} }, joint.shapes.logic.Gate21.prototype.defaults), operation: function(input1, input2) { return !(input1 && input2); } }); joint.shapes.logic.Xor = joint.shapes.logic.Gate21.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Xor', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IlhPUiBBTlNJLnN2ZyIKICAgaW5rc2NhcGU6b3V0cHV0X2V4dGVuc2lvbj0ib3JnLmlua3NjYXBlLm91dHB1dC5zdmcuaW5rc2NhcGUiPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0Ij4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSI1MCA6IDE1IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIyNSA6IDEwIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3MTQiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEgOiAwLjUgOiAxIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjAuNSA6IDAuMzMzMzMzMzMgOiAxIgogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgwNiIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjgxOSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzNzIuMDQ3MjQgOiAzNTAuNzg3MzkgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNzQ0LjA5NDQ4IDogNTI2LjE4MTA5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MjYuMTgxMDkgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMjc3NyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI3NSA6IDQwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjE1MCA6IDYwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA2MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUzMjc1IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjUwIDogMzMuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjEwMCA6IDUwIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiA1MCA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmU1NTMzIgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjMyIDogMjEuMzMzMzMzIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjY0IDogMzIgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDMyIDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI1NTciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxNi42NjY2NjcgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAyNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMjUgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICA8L2RlZnM+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlkPSJiYXNlIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnpvb209IjUuNjU2ODU0MiIKICAgICBpbmtzY2FwZTpjeD0iMjUuOTM4MTE2IgogICAgIGlua3NjYXBlOmN5PSIxNy4yMzAwNSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDcwLDI1IGMgMjAsMCAyNSwwIDI1LDAiCiAgICAgICBpZD0icGF0aDMwNTkiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5ODg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzAuMzg1NzE3LDE1IEwgNC45OTk5OTk4LDE1IgogICAgICAgaWQ9InBhdGgzMDYxIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjEuOTk5OTk5NzY7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Ik0gMzEuMzYyMDkxLDM1IEwgNC45OTk5OTk4LDM1IgogICAgICAgaWQ9InBhdGgzOTQ0IiAvPgogICAgPGcKICAgICAgIGlkPSJnMjU2MCIKICAgICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYuNSwtMzkuNSkiPgogICAgICA8cGF0aAogICAgICAgICBpZD0icGF0aDM1MTYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi4yNSw4MS41MDAwMDUgQyAtMy44NDczNzQsODQuMTQ0NDA1IC00LjUsODQuNTAwMDA1IC00LjUsODQuNTAwMDA1IEwgLTguMTU2MjUsODQuNTAwMDA1IEwgLTYuMTU2MjUsODIuMDYyNTA1IEMgLTYuMTU2MjUsODIuMDYyNTA1IC0wLjUsNzUuMDYyNDUxIC0wLjUsNjQuNSBDIC0wLjUsNTMuOTM3NTQ5IC02LjE1NjI1LDQ2LjkzNzUgLTYuMTU2MjUsNDYuOTM3NSBMIC04LjE1NjI1LDQ0LjUgTCAtNC41LDQ0LjUgQyAtMy43MTg3NSw0NS40Mzc1IC0zLjA3ODEyNSw0Ni4xNTYyNSAtMi4yODEyNSw0Ny41IEMgLTAuNDA4NTMxLDUwLjU5OTgxNSAyLjUsNTYuNTI2NjQ2IDIuNSw2NC41IEMgMi41LDcyLjQ1MDY1IC0wLjM5NjY5Nyw3OC4zNzk0MjUgLTIuMjUsODEuNTAwMDA1IHoiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY3NjY2Njc2MiIC8+CiAgICAgIDxwYXRoCiAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjM7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgICAgZD0iTSAtMi40MDYyNSw0NC41IEwgLTAuNDA2MjUsNDYuOTM3NSBDIC0wLjQwNjI1LDQ2LjkzNzUgNS4yNSw1My45Mzc1NDkgNS4yNSw2NC41IEMgNS4yNSw3NS4wNjI0NTEgLTAuNDA2MjUsODIuMDYyNSAtMC40MDYyNSw4Mi4wNjI1IEwgLTIuNDA2MjUsODQuNSBMIDAuNzUsODQuNSBMIDE0Ljc1LDg0LjUgQyAxNy4xNTgwNzYsODQuNTAwMDAxIDIyLjQzOTY5OSw4NC41MjQ1MTQgMjguMzc1LDgyLjA5Mzc1IEMgMzQuMzEwMzAxLDc5LjY2Mjk4NiA0MC45MTE1MzYsNzQuNzUwNDg0IDQ2LjA2MjUsNjUuMjE4NzUgTCA0NC43NSw2NC41IEwgNDYuMDYyNSw2My43ODEyNSBDIDM1Ljc1OTM4Nyw0NC43MTU1OSAxOS41MDY1NzQsNDQuNSAxNC43NSw0NC41IEwgMC43NSw0NC41IEwgLTIuNDA2MjUsNDQuNSB6IE0gMy40Njg3NSw0Ny41IEwgMTQuNzUsNDcuNSBDIDE5LjQzNDE3Myw0Ny41IDMzLjAzNjg1LDQ3LjM2OTc5MyA0Mi43MTg3NSw2NC41IEMgMzcuOTUxOTY0LDcyLjkyOTA3NSAzMi4xOTc0NjksNzcuMTgzOTEgMjcsNzkuMzEyNSBDIDIxLjYzOTMzOSw4MS41MDc5MjQgMTcuMTU4MDc1LDgxLjUwMDAwMSAxNC43NSw4MS41IEwgMy41LDgxLjUgQyA1LjM3MzU4ODQsNzguMzkxNTY2IDguMjUsNzIuNDUwNjUgOC4yNSw2NC41IEMgOC4yNSw1Ni41MjY2NDYgNS4zNDE0Njg2LDUwLjU5OTgxNSAzLjQ2ODc1LDQ3LjUgeiIKICAgICAgICAgaWQ9InBhdGg0OTczIgogICAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNjc2NjY2NzY2NjY2NjY2Njc2Njc2MiIC8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K' }} }, joint.shapes.logic.Gate21.prototype.defaults), operation: function(input1, input2) { return (!input1 || input2) && (input1 || !input2); } }); joint.shapes.logic.Xnor = joint.shapes.logic.Gate21.extend({ defaults: joint.util.deepSupplement({ type: 'logic.Xnor', attrs: { image: { 'xlink:href': 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgo8c3ZnCiAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1sbnM6Y2M9Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL25zIyIKICAgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHdpZHRoPSIxMDAiCiAgIGhlaWdodD0iNTAiCiAgIGlkPSJzdmcyIgogICBzb2RpcG9kaTp2ZXJzaW9uPSIwLjMyIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjQ2IgogICB2ZXJzaW9uPSIxLjAiCiAgIHNvZGlwb2RpOmRvY25hbWU9IlhOT1IgQU5TSS5zdmciCiAgIGlua3NjYXBlOm91dHB1dF9leHRlbnNpb249Im9yZy5pbmtzY2FwZS5vdXRwdXQuc3ZnLmlua3NjYXBlIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzNCI+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogMTUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfej0iNTAgOiAxNSA6IDEiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMjUgOiAxMCA6IDEiCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNzE0IiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDAuNSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxIDogMC41IDogMSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIwLjUgOiAwLjMzMzMzMzMzIDogMSIKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MDYiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI4MTkiCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iMzcyLjA0NzI0IDogMzUwLjc4NzM5IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9Ijc0NC4wOTQ0OCA6IDUyNi4xODEwOSA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTI2LjE4MTA5IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgICA8aW5rc2NhcGU6cGVyc3BlY3RpdmUKICAgICAgIGlkPSJwZXJzcGVjdGl2ZTI3NzciCiAgICAgICBpbmtzY2FwZTpwZXJzcDNkLW9yaWdpbj0iNzUgOiA0MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxNTAgOiA2MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNjAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlMzI3NSIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSI1MCA6IDMzLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSIxMDAgOiA1MCA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF95PSIwIDogMTAwMCA6IDAiCiAgICAgICBpbmtzY2FwZTp2cF94PSIwIDogNTAgOiAxIgogICAgICAgc29kaXBvZGk6dHlwZT0iaW5rc2NhcGU6cGVyc3AzZCIgLz4KICAgIDxpbmtzY2FwZTpwZXJzcGVjdGl2ZQogICAgICAgaWQ9InBlcnNwZWN0aXZlNTUzMyIKICAgICAgIGlua3NjYXBlOnBlcnNwM2Qtb3JpZ2luPSIzMiA6IDIxLjMzMzMzMyA6IDEiCiAgICAgICBpbmtzY2FwZTp2cF96PSI2NCA6IDMyIDogMSIKICAgICAgIGlua3NjYXBlOnZwX3k9IjAgOiAxMDAwIDogMCIKICAgICAgIGlua3NjYXBlOnZwX3g9IjAgOiAzMiA6IDEiCiAgICAgICBzb2RpcG9kaTp0eXBlPSJpbmtzY2FwZTpwZXJzcDNkIiAvPgogICAgPGlua3NjYXBlOnBlcnNwZWN0aXZlCiAgICAgICBpZD0icGVyc3BlY3RpdmUyNTU3IgogICAgICAgaW5rc2NhcGU6cGVyc3AzZC1vcmlnaW49IjI1IDogMTYuNjY2NjY3IDogMSIKICAgICAgIGlua3NjYXBlOnZwX3o9IjUwIDogMjUgOiAxIgogICAgICAgaW5rc2NhcGU6dnBfeT0iMCA6IDEwMDAgOiAwIgogICAgICAgaW5rc2NhcGU6dnBfeD0iMCA6IDI1IDogMSIKICAgICAgIHNvZGlwb2RpOnR5cGU9Imlua3NjYXBlOnBlcnNwM2QiIC8+CiAgPC9kZWZzPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSI0IgogICAgIGlua3NjYXBlOmN4PSI5NS43MjM2NiIKICAgICBpbmtzY2FwZTpjeT0iLTI2Ljc3NTAyMyIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJ0cnVlIgogICAgIGlua3NjYXBlOmdyaWQtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTpncmlkLXBvaW50cz0idHJ1ZSIKICAgICBncmlkdG9sZXJhbmNlPSIxMDAwMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzOTkiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iODc0IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIzMyIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTpzbmFwLWJib3g9InRydWUiPgogICAgPGlua3NjYXBlOmdyaWQKICAgICAgIGlkPSJHcmlkRnJvbVByZTA0NlNldHRpbmdzIgogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgb3JpZ2lueD0iMHB4IgogICAgICAgb3JpZ2lueT0iMHB4IgogICAgICAgc3BhY2luZ3g9IjFweCIKICAgICAgIHNwYWNpbmd5PSIxcHgiCiAgICAgICBjb2xvcj0iIzAwMDBmZiIKICAgICAgIGVtcGNvbG9yPSIjMDAwMGZmIgogICAgICAgb3BhY2l0eT0iMC4yIgogICAgICAgZW1wb3BhY2l0eT0iMC40IgogICAgICAgZW1wc3BhY2luZz0iNSIKICAgICAgIHZpc2libGU9InRydWUiCiAgICAgICBlbmFibGVkPSJ0cnVlIiAvPgogIDwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTciPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxnCiAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgaW5rc2NhcGU6Z3JvdXBtb2RlPSJsYXllciIKICAgICBpZD0ibGF5ZXIxIj4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyLjAwMDAwMDI0O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJNIDc4LjMzMzMzMiwyNSBDIDkxLjY2NjY2NiwyNSA5NSwyNSA5NSwyNSIKICAgICAgIGlkPSJwYXRoMzA1OSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk4ODtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMC4zODU3MTcsMTUgTCA0Ljk5OTk5OTgsMTUiCiAgICAgICBpZD0icGF0aDMwNjEiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS45OTk5OTk3NjtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgZD0iTSAzMS4zNjIwOTEsMzUgTCA0Ljk5OTk5OTgsMzUiCiAgICAgICBpZD0icGF0aDM5NDQiIC8+CiAgICA8ZwogICAgICAgaWQ9ImcyNTYwIgogICAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiCiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNi41LC0zOS41KSI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMzUxNiIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjI1LDgxLjUwMDAwNSBDIC0zLjg0NzM3NCw4NC4xNDQ0MDUgLTQuNSw4NC41MDAwMDUgLTQuNSw4NC41MDAwMDUgTCAtOC4xNTYyNSw4NC41MDAwMDUgTCAtNi4xNTYyNSw4Mi4wNjI1MDUgQyAtNi4xNTYyNSw4Mi4wNjI1MDUgLTAuNSw3NS4wNjI0NTEgLTAuNSw2NC41IEMgLTAuNSw1My45Mzc1NDkgLTYuMTU2MjUsNDYuOTM3NSAtNi4xNTYyNSw0Ni45Mzc1IEwgLTguMTU2MjUsNDQuNSBMIC00LjUsNDQuNSBDIC0zLjcxODc1LDQ1LjQzNzUgLTMuMDc4MTI1LDQ2LjE1NjI1IC0yLjI4MTI1LDQ3LjUgQyAtMC40MDg1MzEsNTAuNTk5ODE1IDIuNSw1Ni41MjY2NDYgMi41LDY0LjUgQyAyLjUsNzIuNDUwNjUgLTAuMzk2Njk3LDc4LjM3OTQyNSAtMi4yNSw4MS41MDAwMDUgeiIKICAgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Njc2NjY2NzYyIgLz4KICAgICAgPHBhdGgKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MztzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2Utb3BhY2l0eToxIgogICAgICAgICBkPSJNIC0yLjQwNjI1LDQ0LjUgTCAtMC40MDYyNSw0Ni45Mzc1IEMgLTAuNDA2MjUsNDYuOTM3NSA1LjI1LDUzLjkzNzU0OSA1LjI1LDY0LjUgQyA1LjI1LDc1LjA2MjQ1MSAtMC40MDYyNSw4Mi4wNjI1IC0wLjQwNjI1LDgyLjA2MjUgTCAtMi40MDYyNSw4NC41IEwgMC43NSw4NC41IEwgMTQuNzUsODQuNSBDIDE3LjE1ODA3Niw4NC41MDAwMDEgMjIuNDM5Njk5LDg0LjUyNDUxNCAyOC4zNzUsODIuMDkzNzUgQyAzNC4zMTAzMDEsNzkuNjYyOTg2IDQwLjkxMTUzNiw3NC43NTA0ODQgNDYuMDYyNSw2NS4yMTg3NSBMIDQ0Ljc1LDY0LjUgTCA0Ni4wNjI1LDYzLjc4MTI1IEMgMzUuNzU5Mzg3LDQ0LjcxNTU5IDE5LjUwNjU3NCw0NC41IDE0Ljc1LDQ0LjUgTCAwLjc1LDQ0LjUgTCAtMi40MDYyNSw0NC41IHogTSAzLjQ2ODc1LDQ3LjUgTCAxNC43NSw0Ny41IEMgMTkuNDM0MTczLDQ3LjUgMzMuMDM2ODUsNDcuMzY5NzkzIDQyLjcxODc1LDY0LjUgQyAzNy45NTE5NjQsNzIuOTI5MDc1IDMyLjE5NzQ2OSw3Ny4xODM5MSAyNyw3OS4zMTI1IEMgMjEuNjM5MzM5LDgxLjUwNzkyNCAxNy4xNTgwNzUsODEuNTAwMDAxIDE0Ljc1LDgxLjUgTCAzLjUsODEuNSBDIDUuMzczNTg4NCw3OC4zOTE1NjYgOC4yNSw3Mi40NTA2NSA4LjI1LDY0LjUgQyA4LjI1LDU2LjUyNjY0NiA1LjM0MTQ2ODYsNTAuNTk5ODE1IDMuNDY4NzUsNDcuNSB6IgogICAgICAgICBpZD0icGF0aDQ5NzMiCiAgICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NzY2NjY3NjY2NjY2NjY2NzY2NzYyIgLz4KICAgIDwvZz4KICAgIDxwYXRoCiAgICAgICBzb2RpcG9kaTp0eXBlPSJhcmMiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDozO3N0cm9rZS1saW5lam9pbjptaXRlcjttYXJrZXI6bm9uZTtzdHJva2Utb3BhY2l0eToxO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBpZD0icGF0aDM1NTEiCiAgICAgICBzb2RpcG9kaTpjeD0iNzUiCiAgICAgICBzb2RpcG9kaTpjeT0iMjUiCiAgICAgICBzb2RpcG9kaTpyeD0iNCIKICAgICAgIHNvZGlwb2RpOnJ5PSI0IgogICAgICAgZD0iTSA3OSwyNSBBIDQsNCAwIDEgMSA3MSwyNSBBIDQsNCAwIDEgMSA3OSwyNSB6IiAvPgogIDwvZz4KPC9zdmc+Cg==' }} }, joint.shapes.logic.Gate21.prototype.defaults), operation: function(input1, input2) { return (!input1 || !input2) && (input1 || input2); } }); joint.shapes.logic.Wire = joint.dia.Link.extend({ arrowheadMarkup: [ '', '', '' ].join(''), vertexMarkup: [ '', '', '', '', '', 'Remove vertex.', '', '', '' ].join(''), defaults: joint.util.deepSupplement({ type: 'logic.Wire', attrs: { '.connection': { 'stroke-width': 2 }, '.marker-vertex': { r: 7 } }, router: { name: 'orthogonal' }, connector: { name: 'rounded', args: { radius: 10 }} }, joint.dia.Link.prototype.defaults) }); if (typeof exports === 'object') { var graphlib = require('graphlib'); var dagre = require('dagre'); } // In the browser, these variables are set to undefined because of JavaScript hoisting. // In that case, should grab them from the window object. graphlib = graphlib || (typeof window !== 'undefined' && window.graphlib); dagre = dagre || (typeof window !== 'undefined' && window.dagre); joint.layout.DirectedGraph = { layout: function(graphOrCells, opt) { var graph; if (graphOrCells instanceof joint.dia.Graph) { graph = graphOrCells; } else { graph = (new joint.dia.Graph()).resetCells(graphOrCells); } // This is not needed anymore. graphOrCells = null; opt = _.defaults(opt || {}, { resizeClusters: true, clusterPadding: 10 }); // create a graphlib.Graph that represents the joint.dia.Graph var glGraph = graph.toGraphLib({ directed: true, // We are about to use edge naming feature. multigraph: true, // We are able to layout graphs with embeds. compound: true, setNodeLabel: function(element) { return { width: element.get('size').width, height: element.get('size').height, rank: element.get('rank') }; }, setEdgeLabel: function(link) { return { minLen: link.get('minLen') || 1 }; }, setEdgeName: function(link) { // Graphlib edges have no ids. We use edge name property // to store and retrieve ids instead. return link.id; } }); var glLabel = {}; // Dagre layout accepts options as lower case. // Direction for rank nodes. Can be TB, BT, LR, or RL if (opt.rankDir) glLabel.rankdir = opt.rankDir; // Alignment for rank nodes. Can be UL, UR, DL, or DR if (opt.align) glLabel.align = opt.align; // Number of pixels that separate nodes horizontally in the layout. if (opt.nodeSep) glLabel.nodesep = opt.nodeSep; // Number of pixels that separate edges horizontally in the layout. if (opt.edgeSep) glLabel.edgesep = opt.edgeSep; // Number of pixels between each rank in the layout. if (opt.rankSep) glLabel.ranksep = opt.rankSep; // Number of pixels to use as a margin around the left and right of the graph. if (opt.marginX) glLabel.marginx = opt.marginX; // Number of pixels to use as a margin around the top and bottom of the graph. if (opt.marginY) glLabel.marginy = opt.marginY; // Set the option object for the graph label. glGraph.setGraph(glLabel); // Executes the layout. dagre.layout(glGraph, { debugTiming: !!opt.debugTiming }); // Wrap all graph changes into a batch. graph.startBatch('layout'); // Update the graph. graph.fromGraphLib(glGraph, { importNode: function(v, gl) { var element = this.getCell(v); var glNode = gl.node(v); if (opt.setPosition) { opt.setPosition(element, glNode); } else { element.set('position', { x: glNode.x - glNode.width / 2, y: glNode.y - glNode.height / 2 }); } }, importEdge: function(edgeObj, gl) { var link = this.getCell(edgeObj.name); var glEdge = gl.edge(edgeObj); var points = glEdge.points || []; if (opt.setLinkVertices) { if (opt.setVertices) { opt.setVertices(link, points); } else { // Remove the first and last point from points array. // Those are source/target element connection points // ie. they lies on the edge of connected elements. link.set('vertices', points.slice(1, points.length - 1)); } } } }); if (opt.resizeClusters) { // Resize and reposition cluster elements (parents of other elements) // to fit their children. // 1. filter clusters only // 2. map id on cells // 3. sort cells by their depth (the deepest first) // 4. resize cell to fit their direct children only. _.chain(glGraph.nodes()) .filter(function(v) { return glGraph.children(v).length > 0; }) .map(graph.getCell, graph) .sortBy(function(cluster) { return -cluster.getAncestors().length; }) .invoke('fitEmbeds', { padding: opt.clusterPadding }) .value(); } graph.stopBatch('layout'); // Return an object with height and width of the graph. return glGraph.graph(); }, fromGraphLib: function(glGraph, opt) { opt = opt || {}; var importNode = opt.importNode || _.noop; var importEdge = opt.importEdge || _.noop; var graph = this instanceof joint.dia.Graph ? this : new joint.dia.Graph; // Import all nodes. glGraph.nodes().forEach(function(node) { importNode.call(graph, node, glGraph, graph, opt); }); // Import all edges. glGraph.edges().forEach(function(edge) { importEdge.call(graph, edge, glGraph, graph, opt); }); return graph; }, // Create new graphlib graph from existing JointJS graph. toGraphLib: function(graph, opt) { opt = opt || {}; var glGraphType = _.pick(opt, 'directed', 'compound', 'multigraph'); var glGraph = new graphlib.Graph(glGraphType); var setNodeLabel = opt.setNodeLabel || _.noop; var setEdgeLabel = opt.setEdgeLabel || _.noop; var setEdgeName = opt.setEdgeName || _.noop; graph.get('cells').each(function(cell) { if (cell.isLink()) { var source = cell.get('source'); var target = cell.get('target'); // Links that end at a point are ignored. if (!source.id || !target.id) return; // Note that if we are creating a multigraph we can name the edges. If // we try to name edges on a non-multigraph an exception is thrown. glGraph.setEdge(source.id, target.id, setEdgeLabel(cell), setEdgeName(cell)); } else { glGraph.setNode(cell.id, setNodeLabel(cell)); // For the compound graphs we have to take embeds into account. if (glGraph.isCompound() && cell.has('parent')) { glGraph.setParent(cell.id, cell.get('parent')); } } }); return glGraph; } }; joint.dia.Graph.prototype.toGraphLib = function(opt) { return joint.layout.DirectedGraph.toGraphLib(this, opt); }; joint.dia.Graph.prototype.fromGraphLib = function(glGraph, opt) { return joint.layout.DirectedGraph.fromGraphLib.call(this, glGraph, opt); };