Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 1 | (function (scope) { |
| 2 | var Class = function (param1, param2) { |
| 3 | |
| 4 | var extend, mixins, definition; |
| 5 | if (param2) { //two parameters passed, first is extends, second definition object |
| 6 | extend = Array.isArray(param1) ? param1[0] : param1; |
| 7 | mixins = Array.isArray(param1) ? param1.slice(1) : null; |
| 8 | definition = param2; |
| 9 | } else { //only one parameter passed => no extend, only definition |
| 10 | extend = null; |
| 11 | definition = param1; |
| 12 | } |
| 13 | |
| 14 | |
| 15 | var Definition = definition.hasOwnProperty("constructor") ? definition.constructor : function () { |
| 16 | }; |
| 17 | |
| 18 | Definition.prototype = Object.create(extend ? extend.prototype : null); |
| 19 | var propertiesObject = definition.propertiesObject ? definition.propertiesObject : {}; |
| 20 | if (mixins) { |
| 21 | var i, i2; |
| 22 | for (i in mixins) { |
| 23 | for (i2 in mixins[i].prototype) { |
| 24 | Definition.prototype[i2] = mixins[i].prototype[i2]; |
| 25 | } |
| 26 | for (var i2 in mixins[i].prototype.propertiesObject) { |
| 27 | propertiesObject[i2] = mixins[i].prototype.propertiesObject[i2]; |
| 28 | } |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | Definition.prototype.propertiesObject = propertiesObject; |
| 33 | |
| 34 | Object.defineProperties(Definition.prototype, propertiesObject); |
| 35 | |
| 36 | for (var key in definition) { |
| 37 | if (definition.hasOwnProperty(key)) { |
| 38 | Definition.prototype[key] = definition[key]; |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | Definition.prototype.constructor = Definition; |
| 43 | |
| 44 | return Definition; |
| 45 | }; |
| 46 | |
| 47 | |
| 48 | var Interface = function (properties) { |
| 49 | this.properties = properties; |
| 50 | }; |
| 51 | |
| 52 | var InterfaceException = function (message) { |
| 53 | this.name = "InterfaceException"; |
| 54 | this.message = message || ""; |
| 55 | }; |
| 56 | |
| 57 | InterfaceException.prototype = new Error(); |
| 58 | |
| 59 | Interface.prototype.implements = function (target) { |
| 60 | for (var i in this.properties) { |
| 61 | if (target[this.properties[i]] == undefined) { |
| 62 | throw new InterfaceException("Missing property " + this.properties[i]); |
| 63 | } |
| 64 | } |
| 65 | return true; |
| 66 | }; |
| 67 | |
| 68 | Interface.prototype.doesImplement = function (target) { |
| 69 | for (var i in this.properties) { |
| 70 | if (target[this.properties[i]] === undefined) { |
| 71 | return false; |
| 72 | } |
| 73 | } |
| 74 | return true; |
| 75 | }; |
| 76 | |
| 77 | var VectorMath = { |
| 78 | distance: function (vector1, vector2) { |
| 79 | return Math.sqrt(Math.pow(vector1.x - vector2.x, 2) + Math.pow(vector1.y - vector2.y, 2)); |
| 80 | } |
| 81 | }; |
| 82 | |
| 83 | var EventDispatcher = Class({ |
| 84 | constructor: function () { |
| 85 | this.events = {}; |
| 86 | }, |
| 87 | on: function (name, listener, context) { |
| 88 | this.events[name] = this.events[name] ? this.events[name] : []; |
| 89 | this.events[name].push({ |
| 90 | listener: listener, |
| 91 | context: context |
| 92 | }) |
| 93 | }, |
| 94 | once: function (name, listener, context) { |
| 95 | this.off(name, listener, context); |
| 96 | this.on(name, listener, context); |
| 97 | }, |
| 98 | off: function (name, listener, context) { |
| 99 | //no event with this name registered? => finish |
| 100 | if (!this.events[name]) { |
| 101 | return; |
| 102 | } |
| 103 | if (listener) { //searching only for certains listeners |
| 104 | for (var i in this.events[name]) { |
| 105 | if (this.events[name][i].listener === listener) { |
| 106 | if (!context || this.events[name][i].context === context) { |
| 107 | this.events[name].splice(i, 1); |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | } else { |
| 112 | delete this.events[name]; |
| 113 | } |
| 114 | }, |
| 115 | trigger: function (name) { |
| 116 | var listeners = this.events[name]; |
| 117 | |
| 118 | for (var i in listeners) { |
| 119 | listeners[i].listener.apply(listeners[i].context, Array.prototype.slice.call(arguments, 1)); |
| 120 | } |
| 121 | } |
| 122 | }); |
| 123 | |
| 124 | exports.CytoscapeEdgeEditation = Class({ |
| 125 | |
| 126 | init: function (cy, handleSize) { |
| 127 | this.DOUBLE_CLICK_INTERVAL = 300; |
| 128 | this.HANDLE_SIZE = handleSize ? handleSize : 5; |
| 129 | this.ARROW_END_ID = "ARROW_END_ID"; |
| 130 | |
| 131 | this._handles = {}; |
| 132 | this._dragging = false; |
| 133 | this._hover = null; |
| 134 | |
| 135 | |
| 136 | this._cy = cy; |
| 137 | this._$container = $(cy.container()); |
| 138 | |
| 139 | this._cy.on('mouseover tap', 'node', this._mouseOver.bind(this)); |
| 140 | this._cy.on('mouseout', 'node', this._mouseOut.bind(this)); |
| 141 | |
| 142 | this._$container.on('mouseout', function (e) { |
| 143 | if (this.permanentHandle) { |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | this._clear(); |
| 148 | }.bind(this)); |
| 149 | |
| 150 | this._$container.on('mouseover', function (e) { |
| 151 | if (this._hover) { |
| 152 | this._mouseOver({cyTarget: this._hover}); |
| 153 | } |
| 154 | }.bind(this)); |
| 155 | |
| 156 | this._cy.on("select", "node", this._redraw.bind(this)) |
| 157 | |
| 158 | this._cy.on("mousedown", "node", function () { |
| 159 | this._nodeClicked = true; |
| 160 | }.bind(this)); |
| 161 | |
| 162 | this._cy.on("mouseup", "node", function () { |
| 163 | this._nodeClicked = false; |
| 164 | }.bind(this)); |
| 165 | |
| 166 | this._cy.on("remove", "node", function () { |
| 167 | this._hover = false; |
| 168 | this._clear(); |
| 169 | }.bind(this)); |
| 170 | |
| 171 | this._cy.on('showhandle', function (cy, target) { |
| 172 | this.permanentHandle = true; |
| 173 | this._showHandles(target); |
| 174 | }.bind(this)); |
| 175 | |
| 176 | this._cy.on('hidehandles', this._hideHandles.bind(this)); |
| 177 | |
| 178 | this._cy.bind('zoom pan', this._redraw.bind(this)); |
| 179 | |
| 180 | |
| 181 | this._$canvas = $('<canvas></canvas>'); |
| 182 | this._$canvas.css("top", 0); |
| 183 | this._$canvas.on("mousedown", this._mouseDown.bind(this)); |
| 184 | this._$canvas.on("mousemove", this._mouseMove.bind(this)); |
| 185 | |
| 186 | this._ctx = this._$canvas[0].getContext('2d'); |
| 187 | this._$container.children("div").append(this._$canvas); |
| 188 | |
| 189 | $(window).bind('mouseup', this._mouseUp.bind(this)); |
| 190 | |
| 191 | /*$(window).bind('resize', this._resizeCanvas.bind(this)); |
| 192 | $(window).bind('resize', this._resizeCanvas.bind(this));*/ |
| 193 | |
| 194 | this._cy.on("resize", this._resizeCanvas.bind(this)); |
| 195 | |
| 196 | this._$container.bind('resize', function () { |
| 197 | this._resizeCanvas(); |
| 198 | }.bind(this)); |
| 199 | |
| 200 | this._resizeCanvas(); |
| 201 | |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 202 | |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 203 | |
| 204 | }, |
| 205 | registerHandle: function (handle) { |
| 206 | if (handle.nodeTypeNames) { |
| 207 | for (var i in handle.nodeTypeNames) { |
| 208 | var nodeTypeName = handle.nodeTypeNames[i]; |
| 209 | this._handles[nodeTypeName] = this._handles[nodeTypeName] || []; |
| 210 | this._handles[nodeTypeName].push(handle); |
| 211 | } |
| 212 | } else { |
| 213 | this._handles["*"] = this._handles["*"] || []; |
| 214 | this._handles["*"].push(handle); |
| 215 | } |
| 216 | |
| 217 | }, |
| 218 | _showHandles: function (target) { |
| 219 | var nodeTypeName = target.data().type; |
| 220 | if (nodeTypeName) { |
| 221 | |
| 222 | var handles = this._handles[nodeTypeName] ? this._handles[nodeTypeName] : this._handles["*"]; |
| 223 | |
| 224 | for (var i in handles) { |
| 225 | if (handles[i].type != null) { |
| 226 | this._drawHandle(handles[i], target); |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | }, |
| 232 | _clear: function () { |
| 233 | |
| 234 | var w = this._$container.width(); |
| 235 | var h = this._$container.height(); |
| 236 | this._ctx.clearRect(0, 0, w, h); |
| 237 | }, |
| 238 | _drawHandle: function (handle, target) { |
| 239 | |
| 240 | var position = this._getHandlePosition(handle, target); |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 241 | var handleSize = this.HANDLE_SIZE * this._cy.zoom(); |
Michael Lando | 39a4e0c | 2017-07-18 20:46:42 +0300 | [diff] [blame] | 242 | |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 243 | this._ctx.beginPath(); |
| 244 | |
| 245 | if (handle.imageUrl) { |
| 246 | var base_image = new Image(); |
| 247 | base_image.src = handle.imageUrl; |
Michael Lando | 39a4e0c | 2017-07-18 20:46:42 +0300 | [diff] [blame] | 248 | this._ctx.drawImage(base_image, position.x, position.y, handleSize, handleSize); |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 249 | } else { |
| 250 | this._ctx.arc(position.x, position.y, this.HANDLE_SIZE, 0, 2 * Math.PI, false); |
| 251 | this._ctx.fillStyle = handle.color; |
| 252 | this._ctx.strokeStyle = "white"; |
| 253 | this._ctx.lineWidth = 0; |
| 254 | this._ctx.fill(); |
| 255 | this._ctx.stroke(); |
| 256 | } |
| 257 | |
| 258 | }, |
| 259 | _drawArrow: function (fromNode, toPosition, handle) { |
| 260 | var toNode; |
| 261 | if (this._hover) { |
| 262 | toNode = this._hover; |
| 263 | } else { |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 264 | if (!this._arrowEnd) { |
| 265 | this._arrowEnd = this._cy.add({ |
| 266 | group: "nodes", |
| 267 | data: { |
| 268 | "id": this.ARROW_END_ID, |
| 269 | "position": { x: 150, y: 150 } |
| 270 | } |
| 271 | }); |
| 272 | |
| 273 | this._arrowEnd.css({ |
| 274 | "opacity": 0, |
| 275 | 'width': 0.0001, |
| 276 | 'height': 0.0001 |
| 277 | }); |
| 278 | } |
| 279 | |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 280 | this._arrowEnd.renderedPosition(toPosition); |
| 281 | toNode = this._arrowEnd; |
| 282 | } |
| 283 | |
| 284 | |
| 285 | if (this._edge) { |
| 286 | this._edge.remove(); |
| 287 | } |
| 288 | |
| 289 | this._edge = this._cy.add({ |
| 290 | group: "edges", |
| 291 | data: { |
| 292 | id: "edge", |
| 293 | source: fromNode.id(), |
| 294 | target: toNode.id(), |
| 295 | type: 'temporary-link' |
| 296 | }, |
| 297 | css: $.extend( |
| 298 | this._getEdgeCSSByHandle(handle), |
| 299 | {opacity: 0.5} |
| 300 | ) |
| 301 | }); |
| 302 | |
| 303 | }, |
| 304 | _clearArrow: function () { |
| 305 | if (this._edge) { |
| 306 | this._edge.remove(); |
| 307 | this._edge = null; |
| 308 | } |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 309 | |
| 310 | if (this._arrowEnd) { |
| 311 | this._arrowEnd.remove(); |
| 312 | this._arrowEnd = null; |
| 313 | } |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 314 | }, |
| 315 | _resizeCanvas: function () { |
| 316 | this._$canvas |
| 317 | .attr('height', this._$container.height()) |
| 318 | .attr('width', this._$container.width()) |
| 319 | .css({ |
| 320 | 'position': 'absolute', |
| 321 | 'z-index': '999' |
| 322 | }); |
| 323 | }, |
| 324 | _mouseDown: function (e) { |
| 325 | this._hit = this._hitTestHandles(e); |
| 326 | |
| 327 | if (this._hit) { |
| 328 | this._lastClick = Date.now(); |
| 329 | this._dragging = this._hover; |
| 330 | this._hover = null; |
| 331 | e.stopImmediatePropagation(); |
| 332 | } |
| 333 | }, |
| 334 | _hideHandles: function () { |
| 335 | this.permanentHandle = false; |
| 336 | this._clear(); |
| 337 | |
| 338 | if(this._hover){ |
| 339 | this._showHandles(this._hover); |
| 340 | } |
| 341 | }, |
| 342 | _mouseUp: function () { |
| 343 | if (this._hover) { |
| 344 | if (this._hit) { |
| 345 | //check if custom listener was passed, if so trigger it and do not add edge |
| 346 | var listeners = this._cy._private.listeners; |
| 347 | for (var i = 0; i < listeners.length; i++) { |
| 348 | if (listeners[i].type === 'addedgemouseup') { |
| 349 | this._cy.trigger('addedgemouseup', { |
| 350 | source: this._dragging, |
| 351 | target: this._hover, |
| 352 | edge: this._edge |
| 353 | }); |
| 354 | var that = this; |
| 355 | setTimeout(function () { |
| 356 | that._dragging = false; |
| 357 | that._clearArrow(); |
| 358 | that._hit = null; |
| 359 | }, 0); |
| 360 | |
| 361 | |
| 362 | return; |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | var edgeToRemove = this._checkSingleEdge(this._hit.handle, this._dragging); |
| 367 | if (edgeToRemove) { |
| 368 | this._cy.remove("#" + edgeToRemove.id()); |
| 369 | } |
| 370 | var edge = this._cy.add({ |
| 371 | data: { |
| 372 | source: this._dragging.id(), |
| 373 | target: this._hover.id(), |
| 374 | type: this._hit.handle.type |
| 375 | } |
| 376 | }); |
| 377 | this._initEdgeEvents(edge); |
| 378 | } |
| 379 | } |
| 380 | this._cy.trigger('handlemouseout', { |
| 381 | node: this._hover |
| 382 | }); |
| 383 | $("body").css("cursor", "inherit"); |
| 384 | this._dragging = false; |
| 385 | this._clearArrow(); |
| 386 | }, |
| 387 | _mouseMove: function (e) { |
| 388 | if (this._hover) { |
| 389 | if (!this._dragging) { |
| 390 | var hit = this._hitTestHandles(e); |
| 391 | if (hit) { |
| 392 | this._cy.trigger('handlemouseover', { |
| 393 | node: this._hover |
| 394 | }); |
| 395 | $("body").css("cursor", "pointer"); |
| 396 | |
| 397 | } else { |
| 398 | this._cy.trigger('handlemouseout', { |
| 399 | node: this._hover |
| 400 | }); |
| 401 | $("body").css("cursor", "inherit"); |
| 402 | } |
| 403 | } |
| 404 | } else { |
| 405 | $("body").css("cursor", "inherit"); |
| 406 | } |
| 407 | |
| 408 | if (this._dragging && this._hit.handle) { |
| 409 | this._drawArrow(this._dragging, this._getRelativePosition(e), this._hit.handle); |
| 410 | } |
| 411 | |
| 412 | if (this._nodeClicked) { |
| 413 | this._clear(); |
| 414 | } |
| 415 | }, |
| 416 | _mouseOver: function (e) { |
| 417 | |
| 418 | if (this._dragging) { |
| 419 | if ( (e.cyTarget.id() != this._dragging.id()) && e.cyTarget.data().allowConnection || this._hit.handle.allowLoop) { |
| 420 | this._hover = e.cyTarget; |
| 421 | } |
| 422 | } else { |
| 423 | this._hover = e.cyTarget; |
| 424 | this._showHandles(this._hover); |
| 425 | } |
| 426 | }, |
| 427 | _mouseOut: function (e) { |
| 428 | if(!this._dragging) { |
| 429 | if (this.permanentHandle) { |
| 430 | return; |
| 431 | } |
| 432 | |
| 433 | this._clear(); |
| 434 | } |
| 435 | this._hover = null; |
| 436 | }, |
| 437 | _removeEdge: function (edge) { |
| 438 | edge.off("mousedown"); |
| 439 | this._cy.remove("#" + edge.id()); |
| 440 | }, |
| 441 | _initEdgeEvents: function (edge) { |
| 442 | var self = this; |
| 443 | edge.on("mousedown", function () { |
| 444 | if (self.__lastClick && Date.now() - self.__lastClick < self.DOUBLE_CLICK_INTERVAL) { |
| 445 | self._removeEdge(this); |
| 446 | } |
| 447 | self.__lastClick = Date.now(); |
| 448 | }) |
| 449 | }, |
| 450 | _hitTestHandles: function (e) { |
| 451 | var mousePoisition = this._getRelativePosition(e); |
| 452 | |
| 453 | if (this._hover) { |
| 454 | var nodeTypeName = this._hover.data().type; |
| 455 | if (nodeTypeName) { |
| 456 | var handles = this._handles[nodeTypeName] ? this._handles[nodeTypeName] : this._handles["*"]; |
| 457 | |
| 458 | for (var i in handles) { |
| 459 | var handle = handles[i]; |
| 460 | |
| 461 | var position = this._getHandlePosition(handle, this._hover); |
| 462 | if (VectorMath.distance(position, mousePoisition) < this.HANDLE_SIZE) { |
| 463 | return { |
| 464 | handle: handle, |
| 465 | position: position |
| 466 | }; |
| 467 | } |
| 468 | } |
| 469 | } |
| 470 | } |
| 471 | }, |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 472 | _getHandlePosition: function (handle, target) { //returns the upper left point at which to begin drawing the handle |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 473 | var position = target.renderedPosition(); |
Michael Lando | 39a4e0c | 2017-07-18 20:46:42 +0300 | [diff] [blame] | 474 | var width = target.renderedWidth(); |
| 475 | var height = target.renderedHeight(); |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 476 | var renderedHandleSize = this.HANDLE_SIZE * this._cy.zoom(); //actual number of pixels that handle will use. |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 477 | var xpos = null; |
| 478 | var ypos = null; |
| 479 | |
| 480 | switch (handle.positionX) { |
| 481 | case "left": |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 482 | xpos = position.x - width / 2; |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 483 | break; |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 484 | case "right": //position.x is the exact center of the node. Need to add half the width to get to the right edge. Then, subtract renderedHandleSize to get handle position |
| 485 | xpos = position.x + width / 2 - renderedHandleSize; |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 486 | break; |
| 487 | case "center": |
| 488 | xpos = position.x; |
| 489 | break; |
| 490 | } |
| 491 | |
| 492 | switch (handle.positionY) { |
| 493 | case "top": |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 494 | ypos = position.y - height / 2; |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 495 | break; |
| 496 | case "center": |
| 497 | ypos = position.y; |
| 498 | break; |
| 499 | case "bottom": |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 500 | ypos = position.y + height / 2; |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 501 | break; |
| 502 | } |
| 503 | |
Michael Lando | 9db4052 | 2017-07-22 17:10:02 +0300 | [diff] [blame] | 504 | //Determine if handle will be too big and require offset to prevent it from covering too much of the node icon (in which case, move it over by 1/2 the renderedHandleSize, so half the handle overlaps). |
| 505 | //Need to use target.width(), which is the size of the node, unrelated to rendered size/zoom |
| 506 | var offsetX = (target.width() < 30) ? renderedHandleSize / 2 : 0; |
| 507 | var offsetY = (target.height() < 30) ? renderedHandleSize /2 : 0; |
| 508 | return {x: xpos + offsetX, y: ypos - offsetY}; |
Michael Lando | ed64b5e | 2017-06-09 03:19:04 +0300 | [diff] [blame] | 509 | }, |
| 510 | _getEdgeCSSByHandle: function (handle) { |
| 511 | var color = handle.lineColor ? handle.lineColor : handle.color; |
| 512 | return { |
| 513 | "line-color": color, |
| 514 | "target-arrow-color": color, |
| 515 | "line-style": handle.lineStyle? handle.lineStyle: 'solid', |
| 516 | "width": handle.width? handle.width : 3 |
| 517 | }; |
| 518 | }, |
| 519 | _getHandleByType: function (type) { |
| 520 | for (var i in this._handles) { |
| 521 | var byNodeType = this._handles[i]; |
| 522 | for (var i2 in byNodeType) { |
| 523 | var handle = byNodeType[i2]; |
| 524 | if (handle.type == type) { |
| 525 | return handle; |
| 526 | } |
| 527 | } |
| 528 | } |
| 529 | }, |
| 530 | _getRelativePosition: function (e) { |
| 531 | var containerPosition = this._$container.offset(); |
| 532 | return { |
| 533 | x: e.pageX - containerPosition.left, |
| 534 | y: e.pageY - containerPosition.top |
| 535 | } |
| 536 | }, |
| 537 | _checkSingleEdge: function (handle, node) { |
| 538 | |
| 539 | if (handle.noMultigraph) { |
| 540 | var edges = this._cy.edges("[source='" + this._hover.id() + "'][target='" + node.id() + "'],[source='" + node.id() + "'][target='" + this._hover.id() + "']"); |
| 541 | |
| 542 | for (var i = 0; i < edges.length; i++) { |
| 543 | return edges[i]; |
| 544 | } |
| 545 | } else { |
| 546 | |
| 547 | if (handle.single == false) { |
| 548 | return; |
| 549 | } |
| 550 | var edges = this._cy.edges("[source='" + node.id() + "']"); |
| 551 | |
| 552 | for (var i = 0; i < edges.length; i++) { |
| 553 | if (edges[i].data()["type"] == handle.type) { |
| 554 | return edges[i]; |
| 555 | } |
| 556 | } |
| 557 | } |
| 558 | }, |
| 559 | _redraw: function () { |
| 560 | this._clear(); |
| 561 | if (this._hover) { |
| 562 | this._showHandles(this._hover); |
| 563 | } |
| 564 | } |
| 565 | }); |
| 566 | |
| 567 | })(this); |
| 568 | |