Ben Murdoch | 61f157c | 2016-09-16 13:49:30 +0100 | [diff] [blame] | 1 | // Copyright 2015 the V8 project authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | "use strict"; |
| 6 | |
| 7 | class GraphView extends View { |
| 8 | constructor (d3, id, nodes, edges, broker) { |
| 9 | super(id, broker); |
| 10 | var graph = this; |
| 11 | |
| 12 | var svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%"); |
| 13 | graph.svg = svg; |
| 14 | |
| 15 | graph.nodes = nodes || []; |
| 16 | graph.edges = edges || []; |
| 17 | |
| 18 | graph.minGraphX = 0; |
| 19 | graph.maxGraphX = 1; |
| 20 | graph.minGraphY = 0; |
| 21 | graph.maxGraphY = 1; |
| 22 | |
| 23 | graph.state = { |
| 24 | selection: null, |
| 25 | mouseDownNode: null, |
| 26 | justDragged: false, |
| 27 | justScaleTransGraph: false, |
| 28 | lastKeyDown: -1, |
| 29 | showTypes: false |
| 30 | }; |
| 31 | |
| 32 | var selectionHandler = { |
| 33 | clear: function() { |
| 34 | broker.clear(selectionHandler); |
| 35 | }, |
| 36 | select: function(items, selected) { |
| 37 | var ranges = []; |
| 38 | for (var d of items) { |
| 39 | if (selected) { |
| 40 | d.classList.add("selected"); |
| 41 | } else { |
| 42 | d.classList.remove("selected"); |
| 43 | } |
| 44 | var data = d.__data__; |
| 45 | ranges.push([data.pos, data.pos + 1, data.id]); |
| 46 | } |
| 47 | broker.select(selectionHandler, ranges, selected); |
| 48 | }, |
| 49 | selectionDifference: function(span1, inclusive1, span2, inclusive2) { |
| 50 | // Should not be called |
| 51 | }, |
| 52 | brokeredSelect: function(ranges, selected) { |
| 53 | var test = [].entries().next(); |
| 54 | var selection = graph.nodes |
| 55 | .filter(function(n) { |
| 56 | var pos = n.pos; |
| 57 | for (var range of ranges) { |
| 58 | var start = range[0]; |
| 59 | var end = range[1]; |
| 60 | var id = range[2]; |
| 61 | if (end != undefined) { |
| 62 | if (pos >= start && pos < end) { |
| 63 | return true; |
| 64 | } |
| 65 | } else if (start != undefined) { |
| 66 | if (pos === start) { |
| 67 | return true; |
| 68 | } |
| 69 | } else { |
| 70 | if (n.id === id) { |
| 71 | return true; |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | return false; |
| 76 | }); |
| 77 | var newlySelected = new Set(); |
| 78 | selection.forEach(function(n) { |
| 79 | newlySelected.add(n); |
| 80 | if (!n.visible) { |
| 81 | n.visible = true; |
| 82 | } |
| 83 | }); |
| 84 | graph.updateGraphVisibility(); |
| 85 | graph.visibleNodes.each(function(n) { |
| 86 | if (newlySelected.has(n)) { |
| 87 | graph.state.selection.select(this, selected); |
| 88 | } |
| 89 | }); |
| 90 | graph.updateGraphVisibility(); |
| 91 | graph.viewSelection(); |
| 92 | }, |
| 93 | brokeredClear: function() { |
| 94 | graph.state.selection.clear(); |
| 95 | } |
| 96 | }; |
| 97 | broker.addSelectionHandler(selectionHandler); |
| 98 | |
| 99 | graph.state.selection = new Selection(selectionHandler); |
| 100 | |
| 101 | var defs = svg.append('svg:defs'); |
| 102 | defs.append('svg:marker') |
| 103 | .attr('id', 'end-arrow') |
| 104 | .attr('viewBox', '0 -4 8 8') |
| 105 | .attr('refX', 2) |
| 106 | .attr('markerWidth', 2.5) |
| 107 | .attr('markerHeight', 2.5) |
| 108 | .attr('orient', 'auto') |
| 109 | .append('svg:path') |
| 110 | .attr('d', 'M0,-4L8,0L0,4'); |
| 111 | |
| 112 | this.graphElement = svg.append("g"); |
| 113 | graph.visibleEdges = this.graphElement.append("g").selectAll("g"); |
| 114 | graph.visibleNodes = this.graphElement.append("g").selectAll("g"); |
| 115 | |
| 116 | graph.drag = d3.behavior.drag() |
| 117 | .origin(function(d){ |
| 118 | return {x: d.x, y: d.y}; |
| 119 | }) |
| 120 | .on("drag", function(args){ |
| 121 | graph.state.justDragged = true; |
| 122 | graph.dragmove.call(graph, args); |
| 123 | }) |
| 124 | |
| 125 | d3.select("#upload").on("click", function(){ |
| 126 | document.getElementById("hidden-file-upload").click(); |
| 127 | }); |
| 128 | |
| 129 | d3.select("#layout").on("click", function(){ |
| 130 | graph.updateGraphVisibility(); |
| 131 | graph.layoutGraph(); |
| 132 | graph.updateGraphVisibility(); |
| 133 | graph.viewWholeGraph(); |
| 134 | }); |
| 135 | |
| 136 | d3.select("#show-all").on("click", function(){ |
| 137 | graph.nodes.filter(function(n) { n.visible = true; }) |
| 138 | graph.edges.filter(function(e) { e.visible = true; }) |
| 139 | graph.updateGraphVisibility(); |
| 140 | graph.viewWholeGraph(); |
| 141 | }); |
| 142 | |
| 143 | d3.select("#hide-unselected").on("click", function() { |
| 144 | var unselected = graph.visibleNodes.filter(function(n) { |
| 145 | return !this.classList.contains("selected"); |
| 146 | }); |
| 147 | unselected.each(function(n) { |
| 148 | n.visible = false; |
| 149 | }); |
| 150 | graph.updateGraphVisibility(); |
| 151 | }); |
| 152 | |
| 153 | d3.select("#hide-selected").on("click", function() { |
| 154 | var selected = graph.visibleNodes.filter(function(n) { |
| 155 | return this.classList.contains("selected"); |
| 156 | }); |
| 157 | selected.each(function(n) { |
| 158 | n.visible = false; |
| 159 | }); |
| 160 | graph.state.selection.clear(); |
| 161 | graph.updateGraphVisibility(); |
| 162 | }); |
| 163 | |
| 164 | d3.select("#zoom-selection").on("click", function() { |
| 165 | graph.viewSelection(); |
| 166 | }); |
| 167 | |
| 168 | d3.select("#toggle-types").on("click", function() { |
| 169 | graph.toggleTypes(); |
| 170 | }); |
| 171 | |
| 172 | d3.select("#search-input").on("keydown", function() { |
| 173 | if (d3.event.keyCode == 13) { |
| 174 | graph.state.selection.clear(); |
| 175 | var reg = new RegExp(this.value); |
| 176 | var filterFunction = function(n) { |
| 177 | return (reg.exec(n.getDisplayLabel()) != null || |
| 178 | (graph.state.showTypes && reg.exec(n.getDisplayType())) || |
| 179 | reg.exec(n.opcode) != null); |
| 180 | }; |
| 181 | if (d3.event.ctrlKey) { |
| 182 | graph.nodes.forEach(function(n, i) { |
| 183 | if (filterFunction(n)) { |
| 184 | n.visible = true; |
| 185 | } |
| 186 | }); |
| 187 | graph.updateGraphVisibility(); |
| 188 | } |
| 189 | var selected = graph.visibleNodes.each(function(n) { |
| 190 | if (filterFunction(n)) { |
| 191 | graph.state.selection.select(this, true); |
| 192 | } |
| 193 | }); |
| 194 | graph.connectVisibleSelectedNodes(); |
| 195 | graph.updateGraphVisibility(); |
| 196 | this.blur(); |
| 197 | graph.viewSelection(); |
| 198 | } |
| 199 | d3.event.stopPropagation(); |
| 200 | }); |
| 201 | |
| 202 | // listen for key events |
| 203 | d3.select(window).on("keydown", function(e){ |
| 204 | graph.svgKeyDown.call(graph); |
| 205 | }) |
| 206 | .on("keyup", function(){ |
| 207 | graph.svgKeyUp.call(graph); |
| 208 | }); |
| 209 | svg.on("mousedown", function(d){graph.svgMouseDown.call(graph, d);}); |
| 210 | svg.on("mouseup", function(d){graph.svgMouseUp.call(graph, d);}); |
| 211 | |
| 212 | graph.dragSvg = d3.behavior.zoom() |
| 213 | .on("zoom", function(){ |
| 214 | if (d3.event.sourceEvent.shiftKey){ |
| 215 | return false; |
| 216 | } else{ |
| 217 | graph.zoomed.call(graph); |
| 218 | } |
| 219 | return true; |
| 220 | }) |
| 221 | .on("zoomstart", function(){ |
| 222 | if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move"); |
| 223 | }) |
| 224 | .on("zoomend", function(){ |
| 225 | d3.select('body').style("cursor", "auto"); |
| 226 | }); |
| 227 | |
| 228 | svg.call(graph.dragSvg).on("dblclick.zoom", null); |
| 229 | } |
| 230 | |
| 231 | static get selectedClass() { |
| 232 | return "selected"; |
| 233 | } |
| 234 | static get rectClass() { |
| 235 | return "nodeStyle"; |
| 236 | } |
| 237 | static get activeEditId() { |
| 238 | return "active-editing"; |
| 239 | } |
| 240 | static get nodeRadius() { |
| 241 | return 50; |
| 242 | } |
| 243 | |
| 244 | getNodeHeight(graph) { |
| 245 | if (this.state.showTypes) { |
| 246 | return DEFAULT_NODE_HEIGHT + TYPE_HEIGHT; |
| 247 | } else { |
| 248 | return DEFAULT_NODE_HEIGHT; |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | getEdgeFrontier(nodes, inEdges, edgeFilter) { |
| 253 | let frontier = new Set(); |
| 254 | nodes.forEach(function(element) { |
| 255 | var edges = inEdges ? element.__data__.inputs : element.__data__.outputs; |
| 256 | var edgeNumber = 0; |
| 257 | edges.forEach(function(edge) { |
| 258 | if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) { |
| 259 | frontier.add(edge); |
| 260 | } |
| 261 | ++edgeNumber; |
| 262 | }); |
| 263 | }); |
| 264 | return frontier; |
| 265 | } |
| 266 | |
| 267 | getNodeFrontier(nodes, inEdges, edgeFilter) { |
| 268 | let graph = this; |
| 269 | var frontier = new Set(); |
| 270 | var newState = true; |
| 271 | var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter); |
| 272 | // Control key toggles edges rather than just turning them on |
| 273 | if (d3.event.ctrlKey) { |
| 274 | edgeFrontier.forEach(function(edge) { |
| 275 | if (edge.visible) { |
| 276 | newState = false; |
| 277 | } |
| 278 | }); |
| 279 | } |
| 280 | edgeFrontier.forEach(function(edge) { |
| 281 | edge.visible = newState; |
| 282 | if (newState) { |
| 283 | var node = inEdges ? edge.source : edge.target; |
| 284 | node.visible = true; |
| 285 | frontier.add(node); |
| 286 | } |
| 287 | }); |
| 288 | graph.updateGraphVisibility(); |
| 289 | if (newState) { |
| 290 | return graph.visibleNodes.filter(function(n) { |
| 291 | return frontier.has(n); |
| 292 | }); |
| 293 | } else { |
| 294 | return undefined; |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | dragmove(d) { |
| 299 | var graph = this; |
| 300 | d.x += d3.event.dx; |
| 301 | d.y += d3.event.dy; |
| 302 | graph.updateGraphVisibility(); |
| 303 | } |
| 304 | |
| 305 | initializeContent(data, rememberedSelection) { |
| 306 | this.createGraph(data, rememberedSelection); |
| 307 | if (rememberedSelection != null) { |
| 308 | this.attachSelection(rememberedSelection); |
| 309 | this.connectVisibleSelectedNodes(); |
| 310 | } |
| 311 | this.updateGraphVisibility(); |
| 312 | } |
| 313 | |
| 314 | deleteContent() { |
| 315 | if (this.visibleNodes) { |
| 316 | this.nodes = []; |
| 317 | this.edges = []; |
| 318 | this.nodeMap = []; |
| 319 | this.updateGraphVisibility(); |
| 320 | } |
| 321 | }; |
| 322 | |
| 323 | createGraph(data, initiallyVisibileIds) { |
| 324 | var g = this; |
| 325 | g.nodes = data.nodes; |
| 326 | g.nodeMap = []; |
| 327 | var textMeasure = document.getElementById('text-measure'); |
| 328 | g.nodes.forEach(function(n, i){ |
| 329 | n.__proto__ = Node; |
| 330 | n.visible = false; |
| 331 | n.x = 0; |
| 332 | n.y = 0; |
| 333 | n.rank = MAX_RANK_SENTINEL; |
| 334 | n.inputs = []; |
| 335 | n.outputs = []; |
| 336 | n.rpo = -1; |
| 337 | n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; |
| 338 | n.cfg = n.control; |
| 339 | g.nodeMap[n.id] = n; |
| 340 | n.displayLabel = n.getDisplayLabel(); |
| 341 | textMeasure.textContent = n.getDisplayLabel(); |
| 342 | var width = textMeasure.getComputedTextLength(); |
| 343 | textMeasure.textContent = n.getDisplayType(); |
| 344 | width = Math.max(width, textMeasure.getComputedTextLength()); |
| 345 | n.width = Math.alignUp(width + NODE_INPUT_WIDTH * 2, |
| 346 | NODE_INPUT_WIDTH); |
| 347 | }); |
| 348 | g.edges = []; |
| 349 | data.edges.forEach(function(e, i){ |
| 350 | var t = g.nodeMap[e.target]; |
| 351 | var s = g.nodeMap[e.source]; |
| 352 | var newEdge = new Edge(t, e.index, s, e.type); |
| 353 | t.inputs.push(newEdge); |
| 354 | s.outputs.push(newEdge); |
| 355 | g.edges.push(newEdge); |
| 356 | if (e.type == 'control') { |
| 357 | s.cfg = true; |
| 358 | } |
| 359 | }); |
| 360 | g.nodes.forEach(function(n, i) { |
| 361 | n.visible = isNodeInitiallyVisible(n); |
| 362 | if (initiallyVisibileIds != undefined) { |
| 363 | if (initiallyVisibileIds.has(n.id)) { |
| 364 | n.visible = true; |
| 365 | } |
| 366 | } |
| 367 | }); |
| 368 | g.fitGraphViewToWindow(); |
| 369 | g.updateGraphVisibility(); |
| 370 | g.layoutGraph(); |
| 371 | g.updateGraphVisibility(); |
| 372 | g.viewWholeGraph(); |
| 373 | } |
| 374 | |
| 375 | connectVisibleSelectedNodes() { |
| 376 | var graph = this; |
| 377 | graph.state.selection.selection.forEach(function(element) { |
| 378 | var edgeNumber = 0; |
| 379 | element.__data__.inputs.forEach(function(edge) { |
| 380 | if (edge.source.visible && edge.target.visible) { |
| 381 | edge.visible = true; |
| 382 | } |
| 383 | }); |
| 384 | element.__data__.outputs.forEach(function(edge) { |
| 385 | if (edge.source.visible && edge.target.visible) { |
| 386 | edge.visible = true; |
| 387 | } |
| 388 | }); |
| 389 | }); |
| 390 | } |
| 391 | |
| 392 | updateInputAndOutputBubbles() { |
| 393 | var g = this; |
| 394 | var s = g.visibleBubbles; |
| 395 | s.classed("filledBubbleStyle", function(c) { |
| 396 | var components = this.id.split(','); |
| 397 | if (components[0] == "ib") { |
| 398 | var edge = g.nodeMap[components[3]].inputs[components[2]]; |
| 399 | return edge.isVisible(); |
| 400 | } else { |
| 401 | return g.nodeMap[components[1]].areAnyOutputsVisible() == 2; |
| 402 | } |
| 403 | }).classed("halfFilledBubbleStyle", function(c) { |
| 404 | var components = this.id.split(','); |
| 405 | if (components[0] == "ib") { |
| 406 | var edge = g.nodeMap[components[3]].inputs[components[2]]; |
| 407 | return false; |
| 408 | } else { |
| 409 | return g.nodeMap[components[1]].areAnyOutputsVisible() == 1; |
| 410 | } |
| 411 | }).classed("bubbleStyle", function(c) { |
| 412 | var components = this.id.split(','); |
| 413 | if (components[0] == "ib") { |
| 414 | var edge = g.nodeMap[components[3]].inputs[components[2]]; |
| 415 | return !edge.isVisible(); |
| 416 | } else { |
| 417 | return g.nodeMap[components[1]].areAnyOutputsVisible() == 0; |
| 418 | } |
| 419 | }); |
| 420 | s.each(function(c) { |
| 421 | var components = this.id.split(','); |
| 422 | if (components[0] == "ob") { |
| 423 | var from = g.nodeMap[components[1]]; |
| 424 | var x = from.getOutputX(); |
| 425 | var y = g.getNodeHeight() + DEFAULT_NODE_BUBBLE_RADIUS / 2 + 4; |
| 426 | var transform = "translate(" + x + "," + y + ")"; |
| 427 | this.setAttribute('transform', transform); |
| 428 | } |
| 429 | }); |
| 430 | } |
| 431 | |
| 432 | attachSelection(s) { |
| 433 | var graph = this; |
| 434 | if (s.size != 0) { |
| 435 | this.visibleNodes.each(function(n) { |
| 436 | if (s.has(this.__data__.id)) { |
| 437 | graph.state.selection.select(this, true); |
| 438 | } |
| 439 | }); |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | detachSelection() { |
| 444 | var selection = this.state.selection.detachSelection(); |
| 445 | var s = new Set(); |
| 446 | for (var i of selection) { |
| 447 | s.add(i.__data__.id); |
| 448 | }; |
| 449 | return s; |
| 450 | } |
| 451 | |
| 452 | pathMouseDown(path, d) { |
| 453 | d3.event.stopPropagation(); |
| 454 | this.state.selection.clear(); |
| 455 | this.state.selection.add(path); |
| 456 | }; |
| 457 | |
| 458 | nodeMouseDown(node, d) { |
| 459 | d3.event.stopPropagation(); |
| 460 | this.state.mouseDownNode = d; |
| 461 | } |
| 462 | |
| 463 | nodeMouseUp(d3node, d) { |
| 464 | var graph = this, |
| 465 | state = graph.state, |
| 466 | consts = graph.consts; |
| 467 | |
| 468 | var mouseDownNode = state.mouseDownNode; |
| 469 | |
| 470 | if (!mouseDownNode) return; |
| 471 | |
| 472 | if (mouseDownNode !== d){ |
| 473 | // we're in a different node: create new edge for mousedown edge and add to graph |
| 474 | var newEdge = {source: mouseDownNode, target: d}; |
| 475 | var filtRes = graph.visibleEdges.filter(function(d){ |
| 476 | if (d.source === newEdge.target && d.target === newEdge.source){ |
| 477 | graph.edges.splice(graph.edges.indexOf(d), 1); |
| 478 | } |
| 479 | return d.source === newEdge.source && d.target === newEdge.target; |
| 480 | }); |
| 481 | if (!filtRes[0].length){ |
| 482 | graph.edges.push(newEdge); |
| 483 | graph.updateGraphVisibility(); |
| 484 | } |
| 485 | } else{ |
| 486 | // we're in the same node |
| 487 | if (state.justDragged) { |
| 488 | // dragged, not clicked |
| 489 | state.justDragged = false; |
| 490 | } else{ |
| 491 | // clicked, not dragged |
| 492 | var extend = d3.event.shiftKey; |
| 493 | var selection = graph.state.selection; |
| 494 | if (!extend) { |
| 495 | selection.clear(); |
| 496 | } |
| 497 | selection.select(d3node[0][0], true); |
| 498 | } |
| 499 | } |
| 500 | } |
| 501 | |
| 502 | selectSourcePositions(start, end, selected) { |
| 503 | var graph = this; |
| 504 | var map = []; |
| 505 | var sel = graph.nodes.filter(function(n) { |
| 506 | var pos = (n.pos === undefined) |
| 507 | ? -1 |
| 508 | : n.getFunctionRelativeSourcePosition(graph); |
| 509 | if (pos >= start && pos < end) { |
| 510 | map[n.id] = true; |
| 511 | n.visible = true; |
| 512 | } |
| 513 | }); |
| 514 | graph.updateGraphVisibility(); |
| 515 | graph.visibleNodes.filter(function(n) { return map[n.id]; }) |
| 516 | .each(function(n) { |
| 517 | var selection = graph.state.selection; |
| 518 | selection.select(d3.select(this), selected); |
| 519 | }); |
| 520 | } |
| 521 | |
| 522 | selectAllNodes(inEdges, filter) { |
| 523 | var graph = this; |
| 524 | if (!d3.event.shiftKey) { |
| 525 | graph.state.selection.clear(); |
| 526 | } |
| 527 | graph.state.selection.select(graph.visibleNodes[0], true); |
| 528 | graph.updateGraphVisibility(); |
| 529 | } |
| 530 | |
| 531 | svgMouseDown() { |
| 532 | this.state.graphMouseDown = true; |
| 533 | } |
| 534 | |
| 535 | svgMouseUp() { |
| 536 | var graph = this, |
| 537 | state = graph.state; |
| 538 | if (state.justScaleTransGraph) { |
| 539 | // Dragged |
| 540 | state.justScaleTransGraph = false; |
| 541 | } else { |
| 542 | // Clicked |
| 543 | if (state.mouseDownNode == null) { |
| 544 | graph.state.selection.clear(); |
| 545 | } |
| 546 | } |
| 547 | state.mouseDownNode = null; |
| 548 | state.graphMouseDown = false; |
| 549 | } |
| 550 | |
| 551 | svgKeyDown() { |
| 552 | var state = this.state; |
| 553 | var graph = this; |
| 554 | |
| 555 | // Don't handle key press repetition |
| 556 | if(state.lastKeyDown !== -1) return; |
| 557 | |
| 558 | var showSelectionFrontierNodes = function(inEdges, filter, select) { |
| 559 | var frontier = graph.getNodeFrontier(state.selection.selection, inEdges, filter); |
| 560 | if (frontier != undefined) { |
| 561 | if (select) { |
| 562 | if (!d3.event.shiftKey) { |
| 563 | state.selection.clear(); |
| 564 | } |
| 565 | state.selection.select(frontier[0], true); |
| 566 | } |
| 567 | graph.updateGraphVisibility(); |
| 568 | } |
| 569 | allowRepetition = false; |
| 570 | } |
| 571 | |
| 572 | var allowRepetition = true; |
| 573 | var eventHandled = true; // unless the below switch defaults |
| 574 | switch(d3.event.keyCode) { |
| 575 | case 49: |
| 576 | case 50: |
| 577 | case 51: |
| 578 | case 52: |
| 579 | case 53: |
| 580 | case 54: |
| 581 | case 55: |
| 582 | case 56: |
| 583 | case 57: |
| 584 | // '1'-'9' |
| 585 | showSelectionFrontierNodes(true, |
| 586 | (edge, index) => { return index == (d3.event.keyCode - 49); }, |
| 587 | false); |
| 588 | break; |
| 589 | case 67: |
| 590 | // 'c' |
| 591 | showSelectionFrontierNodes(true, |
| 592 | (edge, index) => { return edge.type == 'control'; }, |
| 593 | false); |
| 594 | break; |
| 595 | case 69: |
| 596 | // 'e' |
| 597 | showSelectionFrontierNodes(true, |
| 598 | (edge, index) => { return edge.type == 'effect'; }, |
| 599 | false); |
| 600 | break; |
| 601 | case 79: |
| 602 | // 'o' |
| 603 | showSelectionFrontierNodes(false, undefined, false); |
| 604 | break; |
| 605 | case 73: |
| 606 | // 'i' |
| 607 | showSelectionFrontierNodes(true, undefined, false); |
| 608 | break; |
| 609 | case 65: |
| 610 | // 'a' |
| 611 | graph.selectAllNodes(); |
| 612 | allowRepetition = false; |
| 613 | break; |
| 614 | case 38: |
| 615 | case 40: { |
| 616 | showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true); |
| 617 | break; |
| 618 | } |
| 619 | default: |
| 620 | eventHandled = false; |
| 621 | break; |
| 622 | } |
| 623 | if (eventHandled) { |
| 624 | d3.event.preventDefault(); |
| 625 | } |
| 626 | if (!allowRepetition) { |
| 627 | state.lastKeyDown = d3.event.keyCode; |
| 628 | } |
| 629 | } |
| 630 | |
| 631 | svgKeyUp() { |
| 632 | this.state.lastKeyDown = -1 |
| 633 | }; |
| 634 | |
| 635 | layoutEdges() { |
| 636 | var graph = this; |
| 637 | graph.maxGraphX = graph.maxGraphNodeX; |
| 638 | this.visibleEdges.attr("d", function(edge){ |
| 639 | return edge.generatePath(graph); |
| 640 | }); |
| 641 | } |
| 642 | |
| 643 | layoutGraph() { |
| 644 | layoutNodeGraph(this); |
| 645 | } |
| 646 | |
| 647 | // call to propagate changes to graph |
| 648 | updateGraphVisibility() { |
| 649 | |
| 650 | var graph = this, |
| 651 | state = graph.state; |
| 652 | |
| 653 | var filteredEdges = graph.edges.filter(function(e) { return e.isVisible(); }); |
| 654 | var visibleEdges = graph.visibleEdges.data(filteredEdges, function(edge) { |
| 655 | return edge.stringID(); |
| 656 | }); |
| 657 | |
| 658 | // add new paths |
| 659 | visibleEdges.enter() |
| 660 | .append('path') |
| 661 | .style('marker-end','url(#end-arrow)') |
| 662 | .classed('hidden', function(e) { |
| 663 | return !e.isVisible(); |
| 664 | }) |
| 665 | .attr("id", function(edge){ return "e," + edge.stringID(); }) |
| 666 | .on("mousedown", function(d){ |
| 667 | graph.pathMouseDown.call(graph, d3.select(this), d); |
| 668 | }) |
| 669 | |
| 670 | // Set the correct styles on all of the paths |
| 671 | visibleEdges.classed('value', function(e) { |
| 672 | return e.type == 'value' || e.type == 'context'; |
| 673 | }).classed('control', function(e) { |
| 674 | return e.type == 'control'; |
| 675 | }).classed('effect', function(e) { |
| 676 | return e.type == 'effect'; |
| 677 | }).classed('frame-state', function(e) { |
| 678 | return e.type == 'frame-state'; |
| 679 | }).attr('stroke-dasharray', function(e) { |
| 680 | if (e.type == 'frame-state') return "10,10"; |
| 681 | return (e.type == 'effect') ? "5,5" : ""; |
| 682 | }); |
| 683 | |
| 684 | // remove old links |
| 685 | visibleEdges.exit().remove(); |
| 686 | |
| 687 | graph.visibleEdges = visibleEdges; |
| 688 | |
| 689 | // update existing nodes |
| 690 | var filteredNodes = graph.nodes.filter(function(n) { return n.visible; }); |
| 691 | graph.visibleNodes = graph.visibleNodes.data(filteredNodes, function(d) { |
| 692 | return d.id; |
| 693 | }); |
| 694 | graph.visibleNodes.attr("transform", function(n){ |
| 695 | return "translate(" + n.x + "," + n.y + ")"; |
| 696 | }).select('rect'). |
| 697 | attr(HEIGHT, function(d) { return graph.getNodeHeight(); }); |
| 698 | |
| 699 | // add new nodes |
| 700 | var newGs = graph.visibleNodes.enter() |
| 701 | .append("g"); |
| 702 | |
| 703 | newGs.classed("control", function(n) { return n.isControl(); }) |
| 704 | .classed("javascript", function(n) { return n.isJavaScript(); }) |
| 705 | .classed("input", function(n) { return n.isInput(); }) |
| 706 | .classed("simplified", function(n) { return n.isSimplified(); }) |
| 707 | .classed("machine", function(n) { return n.isMachine(); }) |
| 708 | .attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")";}) |
| 709 | .on("mousedown", function(d){ |
| 710 | graph.nodeMouseDown.call(graph, d3.select(this), d); |
| 711 | }) |
| 712 | .on("mouseup", function(d){ |
| 713 | graph.nodeMouseUp.call(graph, d3.select(this), d); |
| 714 | }) |
| 715 | .call(graph.drag); |
| 716 | |
| 717 | newGs.append("rect") |
| 718 | .attr("rx", 10) |
| 719 | .attr("ry", 10) |
| 720 | .attr(WIDTH, function(d) { return d.getTotalNodeWidth(); }) |
| 721 | .attr(HEIGHT, function(d) { return graph.getNodeHeight(); }) |
| 722 | |
| 723 | function appendInputAndOutputBubbles(g, d) { |
| 724 | for (var i = 0; i < d.inputs.length; ++i) { |
| 725 | var x = d.getInputX(i); |
| 726 | var y = -DEFAULT_NODE_BUBBLE_RADIUS / 2 - 4; |
| 727 | var s = g.append('circle') |
| 728 | .classed("filledBubbleStyle", function(c) { |
| 729 | return d.inputs[i].isVisible(); |
| 730 | } ) |
| 731 | .classed("bubbleStyle", function(c) { |
| 732 | return !d.inputs[i].isVisible(); |
| 733 | } ) |
| 734 | .attr("id", "ib," + d.inputs[i].stringID()) |
| 735 | .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) |
| 736 | .attr("transform", function(d) { |
| 737 | return "translate(" + x + "," + y + ")"; |
| 738 | }) |
| 739 | .on("mousedown", function(d){ |
| 740 | var components = this.id.split(','); |
| 741 | var node = graph.nodeMap[components[3]]; |
| 742 | var edge = node.inputs[components[2]]; |
| 743 | var visible = !edge.isVisible(); |
| 744 | node.setInputVisibility(components[2], visible); |
| 745 | d3.event.stopPropagation(); |
| 746 | graph.updateGraphVisibility(); |
| 747 | }); |
| 748 | } |
| 749 | if (d.outputs.length != 0) { |
| 750 | var x = d.getOutputX(); |
| 751 | var y = graph.getNodeHeight() + DEFAULT_NODE_BUBBLE_RADIUS / 2 + 4; |
| 752 | var s = g.append('circle') |
| 753 | .classed("filledBubbleStyle", function(c) { |
| 754 | return d.areAnyOutputsVisible() == 2; |
| 755 | } ) |
| 756 | .classed("halFilledBubbleStyle", function(c) { |
| 757 | return d.areAnyOutputsVisible() == 1; |
| 758 | } ) |
| 759 | .classed("bubbleStyle", function(c) { |
| 760 | return d.areAnyOutputsVisible() == 0; |
| 761 | } ) |
| 762 | .attr("id", "ob," + d.id) |
| 763 | .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) |
| 764 | .attr("transform", function(d) { |
| 765 | return "translate(" + x + "," + y + ")"; |
| 766 | }) |
| 767 | .on("mousedown", function(d) { |
| 768 | d.setOutputVisibility(d.areAnyOutputsVisible() == 0); |
| 769 | d3.event.stopPropagation(); |
| 770 | graph.updateGraphVisibility(); |
| 771 | }); |
| 772 | } |
| 773 | } |
| 774 | |
| 775 | newGs.each(function(d){ |
| 776 | appendInputAndOutputBubbles(d3.select(this), d); |
| 777 | }); |
| 778 | |
| 779 | newGs.each(function(d){ |
| 780 | d3.select(this).append("text") |
| 781 | .classed("label", true) |
| 782 | .attr("text-anchor","right") |
| 783 | .attr("dx", "5") |
| 784 | .attr("dy", DEFAULT_NODE_HEIGHT / 2 + 5) |
| 785 | .append('tspan') |
| 786 | .text(function(l) { |
| 787 | return d.getDisplayLabel(); |
| 788 | }) |
| 789 | .append("title") |
| 790 | .text(function(l) { |
| 791 | return d.getLabel(); |
| 792 | }) |
| 793 | if (d.type != undefined) { |
| 794 | d3.select(this).append("text") |
| 795 | .classed("label", true) |
| 796 | .classed("type", true) |
| 797 | .attr("text-anchor","right") |
| 798 | .attr("dx", "5") |
| 799 | .attr("dy", DEFAULT_NODE_HEIGHT / 2 + TYPE_HEIGHT + 5) |
| 800 | .append('tspan') |
| 801 | .text(function(l) { |
| 802 | return d.getDisplayType(); |
| 803 | }) |
| 804 | .append("title") |
| 805 | .text(function(l) { |
| 806 | return d.getType(); |
| 807 | }) |
| 808 | } |
| 809 | }); |
| 810 | |
| 811 | graph.visibleNodes.select('.type').each(function (d) { |
| 812 | this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden'); |
| 813 | }); |
| 814 | |
| 815 | // remove old nodes |
| 816 | graph.visibleNodes.exit().remove(); |
| 817 | |
| 818 | graph.visibleBubbles = d3.selectAll('circle'); |
| 819 | |
| 820 | graph.updateInputAndOutputBubbles(); |
| 821 | |
| 822 | graph.layoutEdges(); |
| 823 | |
| 824 | graph.svg.style.height = '100%'; |
| 825 | } |
| 826 | |
| 827 | getVisibleTranslation(translate, scale) { |
| 828 | var graph = this; |
| 829 | var height = (graph.maxGraphY - graph.minGraphY + 2 * GRAPH_MARGIN) * scale; |
| 830 | var width = (graph.maxGraphX - graph.minGraphX + 2 * GRAPH_MARGIN) * scale; |
| 831 | |
| 832 | var dimensions = this.getSvgViewDimensions(); |
| 833 | |
| 834 | var baseY = translate[1]; |
| 835 | var minY = (graph.minGraphY - GRAPH_MARGIN) * scale; |
| 836 | var maxY = (graph.maxGraphY + GRAPH_MARGIN) * scale; |
| 837 | |
| 838 | var adjustY = 0; |
| 839 | var adjustYCandidate = 0; |
| 840 | if ((maxY + baseY) < dimensions[1]) { |
| 841 | adjustYCandidate = dimensions[1] - (maxY + baseY); |
| 842 | if ((minY + baseY + adjustYCandidate) > 0) { |
| 843 | adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; |
| 844 | } else { |
| 845 | adjustY = adjustYCandidate; |
| 846 | } |
| 847 | } else if (-baseY < minY) { |
| 848 | adjustYCandidate = -(baseY + minY); |
| 849 | if ((maxY + baseY + adjustYCandidate) < dimensions[1]) { |
| 850 | adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; |
| 851 | } else { |
| 852 | adjustY = adjustYCandidate; |
| 853 | } |
| 854 | } |
| 855 | translate[1] += adjustY; |
| 856 | |
| 857 | var baseX = translate[0]; |
| 858 | var minX = (graph.minGraphX - GRAPH_MARGIN) * scale; |
| 859 | var maxX = (graph.maxGraphX + GRAPH_MARGIN) * scale; |
| 860 | |
| 861 | var adjustX = 0; |
| 862 | var adjustXCandidate = 0; |
| 863 | if ((maxX + baseX) < dimensions[0]) { |
| 864 | adjustXCandidate = dimensions[0] - (maxX + baseX); |
| 865 | if ((minX + baseX + adjustXCandidate) > 0) { |
| 866 | adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; |
| 867 | } else { |
| 868 | adjustX = adjustXCandidate; |
| 869 | } |
| 870 | } else if (-baseX < minX) { |
| 871 | adjustXCandidate = -(baseX + minX); |
| 872 | if ((maxX + baseX + adjustXCandidate) < dimensions[0]) { |
| 873 | adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; |
| 874 | } else { |
| 875 | adjustX = adjustXCandidate; |
| 876 | } |
| 877 | } |
| 878 | translate[0] += adjustX; |
| 879 | return translate; |
| 880 | } |
| 881 | |
| 882 | translateClipped(translate, scale, transition) { |
| 883 | var graph = this; |
| 884 | var graphNode = this.graphElement[0][0]; |
| 885 | var translate = this.getVisibleTranslation(translate, scale); |
| 886 | if (transition) { |
| 887 | graphNode.classList.add('visible-transition'); |
| 888 | clearTimeout(graph.transitionTimout); |
| 889 | graph.transitionTimout = setTimeout(function(){ |
| 890 | graphNode.classList.remove('visible-transition'); |
| 891 | }, 1000); |
| 892 | } |
| 893 | var translateString = "translate(" + translate[0] + "px," + translate[1] + "px) scale(" + scale + ")"; |
| 894 | graphNode.style.transform = translateString; |
| 895 | graph.dragSvg.translate(translate); |
| 896 | graph.dragSvg.scale(scale); |
| 897 | } |
| 898 | |
| 899 | zoomed(){ |
| 900 | this.state.justScaleTransGraph = true; |
| 901 | var scale = this.dragSvg.scale(); |
| 902 | this.translateClipped(d3.event.translate, scale); |
| 903 | } |
| 904 | |
| 905 | |
| 906 | getSvgViewDimensions() { |
| 907 | var canvasWidth = this.parentNode.clientWidth; |
| 908 | var documentElement = document.documentElement; |
| 909 | var canvasHeight = documentElement.clientHeight; |
| 910 | return [canvasWidth, canvasHeight]; |
| 911 | } |
| 912 | |
| 913 | |
| 914 | minScale() { |
| 915 | var graph = this; |
| 916 | var dimensions = this.getSvgViewDimensions(); |
| 917 | var width = graph.maxGraphX - graph.minGraphX; |
| 918 | var height = graph.maxGraphY - graph.minGraphY; |
| 919 | var minScale = dimensions[0] / (width + GRAPH_MARGIN * 2); |
| 920 | var minScaleYCandidate = dimensions[1] / (height + GRAPH_MARGIN * 2); |
| 921 | if (minScaleYCandidate < minScale) { |
| 922 | minScale = minScaleYCandidate; |
| 923 | } |
| 924 | this.dragSvg.scaleExtent([minScale, 1.5]); |
| 925 | return minScale; |
| 926 | } |
| 927 | |
| 928 | fitGraphViewToWindow() { |
| 929 | this.svg.attr("height", document.documentElement.clientHeight + "px"); |
| 930 | this.translateClipped(this.dragSvg.translate(), this.dragSvg.scale()); |
| 931 | } |
| 932 | |
| 933 | toggleTypes() { |
| 934 | var graph = this; |
| 935 | graph.state.showTypes = !graph.state.showTypes; |
| 936 | var element = document.getElementById('toggle-types'); |
| 937 | if (graph.state.showTypes) { |
| 938 | element.classList.add('button-input-toggled'); |
| 939 | } else { |
| 940 | element.classList.remove('button-input-toggled'); |
| 941 | } |
| 942 | graph.updateGraphVisibility(); |
| 943 | } |
| 944 | |
| 945 | viewSelection() { |
| 946 | var graph = this; |
| 947 | var minX, maxX, minY, maxY; |
| 948 | var hasSelection = false; |
| 949 | graph.visibleNodes.each(function(n) { |
| 950 | if (this.classList.contains("selected")) { |
| 951 | hasSelection = true; |
| 952 | minX = minX ? Math.min(minX, n.x) : n.x; |
| 953 | maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : |
| 954 | n.x + n.getTotalNodeWidth(); |
| 955 | minY = minY ? Math.min(minY, n.y) : n.y; |
| 956 | maxY = maxY ? Math.max(maxY, n.y + DEFAULT_NODE_HEIGHT) : |
| 957 | n.y + DEFAULT_NODE_HEIGHT; |
| 958 | } |
| 959 | }); |
| 960 | if (hasSelection) { |
| 961 | graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, |
| 962 | maxX + NODE_INPUT_WIDTH, maxY + 60, |
| 963 | true); |
| 964 | } |
| 965 | } |
| 966 | |
| 967 | viewGraphRegion(minX, minY, maxX, maxY, transition) { |
| 968 | var graph = this; |
| 969 | var dimensions = this.getSvgViewDimensions(); |
| 970 | var width = maxX - minX; |
| 971 | var height = maxY - minY; |
| 972 | var scale = Math.min(dimensions[0] / width, dimensions[1] / height); |
| 973 | scale = Math.min(1.5, scale); |
| 974 | scale = Math.max(graph.minScale(), scale); |
| 975 | var translation = [-minX*scale, -minY*scale]; |
| 976 | translation = graph.getVisibleTranslation(translation, scale); |
| 977 | graph.translateClipped(translation, scale, transition); |
| 978 | } |
| 979 | |
| 980 | viewWholeGraph() { |
| 981 | var graph = this; |
| 982 | var minScale = graph.minScale(); |
| 983 | var translation = [0, 0]; |
| 984 | translation = graph.getVisibleTranslation(translation, minScale); |
| 985 | graph.translateClipped(translation, minScale); |
| 986 | } |
| 987 | } |