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 5import * as d3 from "d3"; 6import { layoutNodeGraph } from "../src/graph-layout"; 7import { GNode, nodeToStr } from "../src/node"; 8import { NODE_INPUT_WIDTH } from "../src/node"; 9import { DEFAULT_NODE_BUBBLE_RADIUS } from "../src/node"; 10import { Edge, edgeToStr } from "../src/edge"; 11import { PhaseView } from "../src/view"; 12import { MySelection } from "../src/selection"; 13import { partial } from "../src/util"; 14import { NodeSelectionHandler, ClearableHandler } from "./selection-handler"; 15import { Graph } from "./graph"; 16import { SelectionBroker } from "./selection-broker"; 17 18function nodeToStringKey(n: GNode) { 19 return "" + n.id; 20} 21 22interface GraphState { 23 showTypes: boolean; 24 selection: MySelection; 25 mouseDownNode: any; 26 justDragged: boolean; 27 justScaleTransGraph: boolean; 28 hideDead: boolean; 29} 30 31export class GraphView extends PhaseView { 32 divElement: d3.Selection<any, any, any, any>; 33 svg: d3.Selection<any, any, any, any>; 34 showPhaseByName: (p: string, s: Set<any>) => void; 35 state: GraphState; 36 selectionHandler: NodeSelectionHandler & ClearableHandler; 37 graphElement: d3.Selection<any, any, any, any>; 38 visibleNodes: d3.Selection<any, GNode, any, any>; 39 visibleEdges: d3.Selection<any, Edge, any, any>; 40 drag: d3.DragBehavior<any, GNode, GNode>; 41 panZoom: d3.ZoomBehavior<SVGElement, any>; 42 visibleBubbles: d3.Selection<any, any, any, any>; 43 transitionTimout: number; 44 graph: Graph; 45 broker: SelectionBroker; 46 phaseName: string; 47 toolbox: HTMLElement; 48 49 createViewElement() { 50 const pane = document.createElement('div'); 51 pane.setAttribute('id', "graph"); 52 return pane; 53 } 54 55 constructor(idOrContainer: string | HTMLElement, broker: SelectionBroker, 56 showPhaseByName: (s: string) => void, toolbox: HTMLElement) { 57 super(idOrContainer); 58 const view = this; 59 this.broker = broker; 60 this.showPhaseByName = showPhaseByName; 61 this.divElement = d3.select(this.divNode); 62 this.phaseName = ""; 63 this.toolbox = toolbox; 64 const svg = this.divElement.append("svg") 65 .attr('version', '2.0') 66 .attr("width", "100%") 67 .attr("height", "100%"); 68 svg.on("click", function (d) { 69 view.selectionHandler.clear(); 70 }); 71 // Listen for key events. Note that the focus handler seems 72 // to be important even if it does nothing. 73 svg 74 .on("focus", e => { }) 75 .on("keydown", e => { view.svgKeyDown(); }); 76 77 view.svg = svg; 78 79 this.state = { 80 selection: null, 81 mouseDownNode: null, 82 justDragged: false, 83 justScaleTransGraph: false, 84 showTypes: false, 85 hideDead: false 86 }; 87 88 this.selectionHandler = { 89 clear: function () { 90 view.state.selection.clear(); 91 broker.broadcastClear(this); 92 view.updateGraphVisibility(); 93 }, 94 select: function (nodes: Array<GNode>, selected: boolean) { 95 const locations = []; 96 for (const node of nodes) { 97 if (node.nodeLabel.sourcePosition) { 98 locations.push(node.nodeLabel.sourcePosition); 99 } 100 if (node.nodeLabel.origin && node.nodeLabel.origin.bytecodePosition) { 101 locations.push({ bytecodePosition: node.nodeLabel.origin.bytecodePosition }); 102 } 103 } 104 view.state.selection.select(nodes, selected); 105 broker.broadcastSourcePositionSelect(this, locations, selected); 106 view.updateGraphVisibility(); 107 }, 108 brokeredNodeSelect: function (locations, selected: boolean) { 109 if (!view.graph) return; 110 const selection = view.graph.nodes(n => { 111 return locations.has(nodeToStringKey(n)) 112 && (!view.state.hideDead || n.isLive()); 113 }); 114 view.state.selection.select(selection, selected); 115 // Update edge visibility based on selection. 116 for (const n of view.graph.nodes()) { 117 if (view.state.selection.isSelected(n)) { 118 n.visible = true; 119 n.inputs.forEach(e => { 120 e.visible = e.visible || view.state.selection.isSelected(e.source); 121 }); 122 n.outputs.forEach(e => { 123 e.visible = e.visible || view.state.selection.isSelected(e.target); 124 }); 125 } 126 } 127 view.updateGraphVisibility(); 128 }, 129 brokeredClear: function () { 130 view.state.selection.clear(); 131 view.updateGraphVisibility(); 132 } 133 }; 134 135 view.state.selection = new MySelection(nodeToStringKey); 136 137 const defs = svg.append('svg:defs'); 138 defs.append('svg:marker') 139 .attr('id', 'end-arrow') 140 .attr('viewBox', '0 -4 8 8') 141 .attr('refX', 2) 142 .attr('markerWidth', 2.5) 143 .attr('markerHeight', 2.5) 144 .attr('orient', 'auto') 145 .append('svg:path') 146 .attr('d', 'M0,-4L8,0L0,4'); 147 148 this.graphElement = svg.append("g"); 149 view.visibleEdges = this.graphElement.append("g"); 150 view.visibleNodes = this.graphElement.append("g"); 151 152 view.drag = d3.drag<any, GNode, GNode>() 153 .on("drag", function (d) { 154 d.x += d3.event.dx; 155 d.y += d3.event.dy; 156 view.updateGraphVisibility(); 157 }); 158 159 function zoomed() { 160 if (d3.event.shiftKey) return false; 161 view.graphElement.attr("transform", d3.event.transform); 162 return true; 163 } 164 165 const zoomSvg = d3.zoom<SVGElement, any>() 166 .scaleExtent([0.2, 40]) 167 .on("zoom", zoomed) 168 .on("start", function () { 169 if (d3.event.shiftKey) return; 170 d3.select('body').style("cursor", "move"); 171 }) 172 .on("end", function () { 173 d3.select('body').style("cursor", "auto"); 174 }); 175 176 svg.call(zoomSvg).on("dblclick.zoom", null); 177 178 view.panZoom = zoomSvg; 179 180 } 181 182 getEdgeFrontier(nodes: Iterable<GNode>, inEdges: boolean, 183 edgeFilter: (e: Edge, i: number) => boolean) { 184 const frontier: Set<Edge> = new Set(); 185 for (const n of nodes) { 186 const edges = inEdges ? n.inputs : n.outputs; 187 let edgeNumber = 0; 188 edges.forEach((edge: Edge) => { 189 if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) { 190 frontier.add(edge); 191 } 192 ++edgeNumber; 193 }); 194 } 195 return frontier; 196 } 197 198 getNodeFrontier(nodes: Iterable<GNode>, inEdges: boolean, 199 edgeFilter: (e: Edge, i: number) => boolean) { 200 const view = this; 201 const frontier: Set<GNode> = new Set(); 202 let newState = true; 203 const edgeFrontier = view.getEdgeFrontier(nodes, inEdges, edgeFilter); 204 // Control key toggles edges rather than just turning them on 205 if (d3.event.ctrlKey) { 206 edgeFrontier.forEach(function (edge: Edge) { 207 if (edge.visible) { 208 newState = false; 209 } 210 }); 211 } 212 edgeFrontier.forEach(function (edge: Edge) { 213 edge.visible = newState; 214 if (newState) { 215 const node = inEdges ? edge.source : edge.target; 216 node.visible = true; 217 frontier.add(node); 218 } 219 }); 220 view.updateGraphVisibility(); 221 if (newState) { 222 return frontier; 223 } else { 224 return undefined; 225 } 226 } 227 228 initializeContent(data, rememberedSelection) { 229 this.show(); 230 function createImgInput(id: string, title: string, onClick): HTMLElement { 231 const input = document.createElement("input"); 232 input.setAttribute("id", id); 233 input.setAttribute("type", "image"); 234 input.setAttribute("title", title); 235 input.setAttribute("src", `img/${id}-icon.png`); 236 input.className = "button-input graph-toolbox-item"; 237 input.addEventListener("click", onClick); 238 return input; 239 } 240 this.toolbox.appendChild(createImgInput("layout", "layout graph", 241 partial(this.layoutAction, this))); 242 this.toolbox.appendChild(createImgInput("show-all", "show all nodes", 243 partial(this.showAllAction, this))); 244 this.toolbox.appendChild(createImgInput("show-control", "show only control nodes", 245 partial(this.showControlAction, this))); 246 this.toolbox.appendChild(createImgInput("toggle-hide-dead", "toggle hide dead nodes", 247 partial(this.toggleHideDead, this))); 248 this.toolbox.appendChild(createImgInput("hide-unselected", "hide unselected", 249 partial(this.hideUnselectedAction, this))); 250 this.toolbox.appendChild(createImgInput("hide-selected", "hide selected", 251 partial(this.hideSelectedAction, this))); 252 this.toolbox.appendChild(createImgInput("zoom-selection", "zoom selection", 253 partial(this.zoomSelectionAction, this))); 254 this.toolbox.appendChild(createImgInput("toggle-types", "toggle types", 255 partial(this.toggleTypesAction, this))); 256 257 this.phaseName = data.name; 258 this.createGraph(data.data, rememberedSelection); 259 this.broker.addNodeHandler(this.selectionHandler); 260 261 if (rememberedSelection != null && rememberedSelection.size > 0) { 262 this.attachSelection(rememberedSelection); 263 this.connectVisibleSelectedNodes(); 264 this.viewSelection(); 265 } else { 266 this.viewWholeGraph(); 267 } 268 } 269 270 deleteContent() { 271 for (const item of this.toolbox.querySelectorAll(".graph-toolbox-item")) { 272 item.parentElement.removeChild(item); 273 } 274 275 for (const n of this.graph.nodes()) { 276 n.visible = false; 277 } 278 this.graph.forEachEdge((e: Edge) => { 279 e.visible = false; 280 }); 281 this.updateGraphVisibility(); 282 } 283 284 public hide(): void { 285 super.hide(); 286 this.deleteContent(); 287 } 288 289 createGraph(data, rememberedSelection) { 290 this.graph = new Graph(data); 291 292 this.showControlAction(this); 293 294 if (rememberedSelection != undefined) { 295 for (const n of this.graph.nodes()) { 296 n.visible = n.visible || rememberedSelection.has(nodeToStringKey(n)); 297 } 298 } 299 300 this.graph.forEachEdge(e => e.visible = e.source.visible && e.target.visible); 301 302 this.layoutGraph(); 303 this.updateGraphVisibility(); 304 } 305 306 connectVisibleSelectedNodes() { 307 const view = this; 308 for (const n of view.state.selection) { 309 n.inputs.forEach(function (edge: Edge) { 310 if (edge.source.visible && edge.target.visible) { 311 edge.visible = true; 312 } 313 }); 314 n.outputs.forEach(function (edge: Edge) { 315 if (edge.source.visible && edge.target.visible) { 316 edge.visible = true; 317 } 318 }); 319 } 320 } 321 322 updateInputAndOutputBubbles() { 323 const view = this; 324 const g = this.graph; 325 const s = this.visibleBubbles; 326 s.classed("filledBubbleStyle", function (c) { 327 const components = this.id.split(','); 328 if (components[0] == "ib") { 329 const edge = g.nodeMap[components[3]].inputs[components[2]]; 330 return edge.isVisible(); 331 } else { 332 return g.nodeMap[components[1]].areAnyOutputsVisible() == 2; 333 } 334 }).classed("halfFilledBubbleStyle", function (c) { 335 const components = this.id.split(','); 336 if (components[0] == "ib") { 337 return false; 338 } else { 339 return g.nodeMap[components[1]].areAnyOutputsVisible() == 1; 340 } 341 }).classed("bubbleStyle", function (c) { 342 const components = this.id.split(','); 343 if (components[0] == "ib") { 344 const edge = g.nodeMap[components[3]].inputs[components[2]]; 345 return !edge.isVisible(); 346 } else { 347 return g.nodeMap[components[1]].areAnyOutputsVisible() == 0; 348 } 349 }); 350 s.each(function (c) { 351 const components = this.id.split(','); 352 if (components[0] == "ob") { 353 const from = g.nodeMap[components[1]]; 354 const x = from.getOutputX(); 355 const y = from.getNodeHeight(view.state.showTypes) + DEFAULT_NODE_BUBBLE_RADIUS; 356 const transform = "translate(" + x + "," + y + ")"; 357 this.setAttribute('transform', transform); 358 } 359 }); 360 } 361 362 attachSelection(s) { 363 if (!(s instanceof Set)) return; 364 this.selectionHandler.clear(); 365 const selected = [...this.graph.nodes(n => 366 s.has(this.state.selection.stringKey(n)) && (!this.state.hideDead || n.isLive()))]; 367 this.selectionHandler.select(selected, true); 368 } 369 370 detachSelection() { 371 return this.state.selection.detachSelection(); 372 } 373 374 selectAllNodes() { 375 if (!d3.event.shiftKey) { 376 this.state.selection.clear(); 377 } 378 const allVisibleNodes = [...this.graph.nodes(n => n.visible)]; 379 this.state.selection.select(allVisibleNodes, true); 380 this.updateGraphVisibility(); 381 } 382 383 layoutAction(graph: GraphView) { 384 graph.layoutGraph(); 385 graph.updateGraphVisibility(); 386 graph.viewWholeGraph(); 387 graph.focusOnSvg(); 388 } 389 390 showAllAction(view: GraphView) { 391 for (const n of view.graph.nodes()) { 392 n.visible = !view.state.hideDead || n.isLive(); 393 } 394 view.graph.forEachEdge((e: Edge) => { 395 e.visible = e.source.visible || e.target.visible; 396 }); 397 view.updateGraphVisibility(); 398 view.viewWholeGraph(); 399 view.focusOnSvg(); 400 } 401 402 showControlAction(view: GraphView) { 403 for (const n of view.graph.nodes()) { 404 n.visible = n.cfg && (!view.state.hideDead || n.isLive()); 405 } 406 view.graph.forEachEdge((e: Edge) => { 407 e.visible = e.type == 'control' && e.source.visible && e.target.visible; 408 }); 409 view.updateGraphVisibility(); 410 view.viewWholeGraph(); 411 view.focusOnSvg(); 412 } 413 414 toggleHideDead(view: GraphView) { 415 view.state.hideDead = !view.state.hideDead; 416 if (view.state.hideDead) { 417 view.hideDead(); 418 } else { 419 view.showDead(); 420 } 421 const element = document.getElementById('toggle-hide-dead'); 422 element.classList.toggle('button-input-toggled', view.state.hideDead); 423 view.focusOnSvg(); 424 } 425 426 hideDead() { 427 for (const n of this.graph.nodes()) { 428 if (!n.isLive()) { 429 n.visible = false; 430 this.state.selection.select([n], false); 431 } 432 } 433 this.updateGraphVisibility(); 434 } 435 436 showDead() { 437 for (const n of this.graph.nodes()) { 438 if (!n.isLive()) { 439 n.visible = true; 440 } 441 } 442 this.updateGraphVisibility(); 443 } 444 445 hideUnselectedAction(view: GraphView) { 446 for (const n of view.graph.nodes()) { 447 if (!view.state.selection.isSelected(n)) { 448 n.visible = false; 449 } 450 } 451 view.updateGraphVisibility(); 452 view.focusOnSvg(); 453 } 454 455 hideSelectedAction(view: GraphView) { 456 for (const n of view.graph.nodes()) { 457 if (view.state.selection.isSelected(n)) { 458 n.visible = false; 459 } 460 } 461 view.selectionHandler.clear(); 462 view.focusOnSvg(); 463 } 464 465 zoomSelectionAction(view: GraphView) { 466 view.viewSelection(); 467 view.focusOnSvg(); 468 } 469 470 toggleTypesAction(view: GraphView) { 471 view.toggleTypes(); 472 view.focusOnSvg(); 473 } 474 475 searchInputAction(searchBar: HTMLInputElement, e: KeyboardEvent, onlyVisible: boolean) { 476 if (e.keyCode == 13) { 477 this.selectionHandler.clear(); 478 const query = searchBar.value; 479 window.sessionStorage.setItem("lastSearch", query); 480 if (query.length == 0) return; 481 482 const reg = new RegExp(query); 483 const filterFunction = (n: GNode) => { 484 return (reg.exec(n.getDisplayLabel()) != null || 485 (this.state.showTypes && reg.exec(n.getDisplayType())) || 486 (reg.exec(n.getTitle())) || 487 reg.exec(n.nodeLabel.opcode) != null); 488 }; 489 490 const selection = [...this.graph.nodes(n => { 491 if ((e.ctrlKey || n.visible || !onlyVisible) && filterFunction(n)) { 492 if (e.ctrlKey || !onlyVisible) n.visible = true; 493 return true; 494 } 495 return false; 496 })]; 497 498 this.selectionHandler.select(selection, true); 499 this.connectVisibleSelectedNodes(); 500 this.updateGraphVisibility(); 501 searchBar.blur(); 502 this.viewSelection(); 503 this.focusOnSvg(); 504 } 505 e.stopPropagation(); 506 } 507 508 focusOnSvg() { 509 (document.getElementById("graph").childNodes[0] as HTMLElement).focus(); 510 } 511 512 svgKeyDown() { 513 const view = this; 514 const state = this.state; 515 516 const showSelectionFrontierNodes = (inEdges: boolean, filter: (e: Edge, i: number) => boolean, doSelect: boolean) => { 517 const frontier = view.getNodeFrontier(state.selection, inEdges, filter); 518 if (frontier != undefined && frontier.size) { 519 if (doSelect) { 520 if (!d3.event.shiftKey) { 521 state.selection.clear(); 522 } 523 state.selection.select([...frontier], true); 524 } 525 view.updateGraphVisibility(); 526 } 527 }; 528 529 let eventHandled = true; // unless the below switch defaults 530 switch (d3.event.keyCode) { 531 case 49: 532 case 50: 533 case 51: 534 case 52: 535 case 53: 536 case 54: 537 case 55: 538 case 56: 539 case 57: 540 // '1'-'9' 541 showSelectionFrontierNodes(true, 542 (edge: Edge, index: number) => index == (d3.event.keyCode - 49), 543 !d3.event.ctrlKey); 544 break; 545 case 97: 546 case 98: 547 case 99: 548 case 100: 549 case 101: 550 case 102: 551 case 103: 552 case 104: 553 case 105: 554 // 'numpad 1'-'numpad 9' 555 showSelectionFrontierNodes(true, 556 (edge, index) => index == (d3.event.keyCode - 97), 557 !d3.event.ctrlKey); 558 break; 559 case 67: 560 // 'c' 561 showSelectionFrontierNodes(d3.event.altKey, 562 (edge, index) => edge.type == 'control', 563 true); 564 break; 565 case 69: 566 // 'e' 567 showSelectionFrontierNodes(d3.event.altKey, 568 (edge, index) => edge.type == 'effect', 569 true); 570 break; 571 case 79: 572 // 'o' 573 showSelectionFrontierNodes(false, undefined, false); 574 break; 575 case 73: 576 // 'i' 577 if (!d3.event.ctrlKey && !d3.event.shiftKey) { 578 showSelectionFrontierNodes(true, undefined, false); 579 } else { 580 eventHandled = false; 581 } 582 break; 583 case 65: 584 // 'a' 585 view.selectAllNodes(); 586 break; 587 case 38: 588 // UP 589 case 40: { 590 // DOWN 591 showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true); 592 break; 593 } 594 case 82: 595 // 'r' 596 if (!d3.event.ctrlKey && !d3.event.shiftKey) { 597 this.layoutAction(this); 598 } else { 599 eventHandled = false; 600 } 601 break; 602 case 80: 603 // 'p' 604 view.selectOrigins(); 605 break; 606 default: 607 eventHandled = false; 608 break; 609 case 83: 610 // 's' 611 if (!d3.event.ctrlKey && !d3.event.shiftKey) { 612 this.hideSelectedAction(this); 613 } else { 614 eventHandled = false; 615 } 616 break; 617 case 85: 618 // 'u' 619 if (!d3.event.ctrlKey && !d3.event.shiftKey) { 620 this.hideUnselectedAction(this); 621 } else { 622 eventHandled = false; 623 } 624 break; 625 } 626 if (eventHandled) { 627 d3.event.preventDefault(); 628 } 629 } 630 631 layoutGraph() { 632 console.time("layoutGraph"); 633 layoutNodeGraph(this.graph, this.state.showTypes); 634 const extent = this.graph.redetermineGraphBoundingBox(this.state.showTypes); 635 this.panZoom.translateExtent(extent); 636 this.minScale(); 637 console.timeEnd("layoutGraph"); 638 } 639 640 selectOrigins() { 641 const state = this.state; 642 const origins = []; 643 let phase = this.phaseName; 644 const selection = new Set<any>(); 645 for (const n of state.selection) { 646 const origin = n.nodeLabel.origin; 647 if (origin) { 648 phase = origin.phase; 649 const node = this.graph.nodeMap[origin.nodeId]; 650 if (phase === this.phaseName && node) { 651 origins.push(node); 652 } else { 653 selection.add(`${origin.nodeId}`); 654 } 655 } 656 } 657 // Only go through phase reselection if we actually need 658 // to display another phase. 659 if (selection.size > 0 && phase !== this.phaseName) { 660 this.showPhaseByName(phase, selection); 661 } else if (origins.length > 0) { 662 this.selectionHandler.clear(); 663 this.selectionHandler.select(origins, true); 664 } 665 } 666 667 // call to propagate changes to graph 668 updateGraphVisibility() { 669 const view = this; 670 const graph = this.graph; 671 const state = this.state; 672 if (!graph) return; 673 674 const filteredEdges = [...graph.filteredEdges(function (e) { 675 return e.source.visible && e.target.visible; 676 })]; 677 const selEdges = view.visibleEdges.selectAll<SVGPathElement, Edge>("path").data(filteredEdges, edgeToStr); 678 679 // remove old links 680 selEdges.exit().remove(); 681 682 // add new paths 683 const newEdges = selEdges.enter() 684 .append('path'); 685 686 newEdges.style('marker-end', 'url(#end-arrow)') 687 .attr("id", function (edge) { return "e," + edge.stringID(); }) 688 .on("click", function (edge) { 689 d3.event.stopPropagation(); 690 if (!d3.event.shiftKey) { 691 view.selectionHandler.clear(); 692 } 693 view.selectionHandler.select([edge.source, edge.target], true); 694 }) 695 .attr("adjacentToHover", "false") 696 .classed('value', function (e) { 697 return e.type == 'value' || e.type == 'context'; 698 }).classed('control', function (e) { 699 return e.type == 'control'; 700 }).classed('effect', function (e) { 701 return e.type == 'effect'; 702 }).classed('frame-state', function (e) { 703 return e.type == 'frame-state'; 704 }).attr('stroke-dasharray', function (e) { 705 if (e.type == 'frame-state') return "10,10"; 706 return (e.type == 'effect') ? "5,5" : ""; 707 }); 708 709 const newAndOldEdges = newEdges.merge(selEdges); 710 711 newAndOldEdges.classed('hidden', e => !e.isVisible()); 712 713 // select existing nodes 714 const filteredNodes = [...graph.nodes(n => n.visible)]; 715 const allNodes = view.visibleNodes.selectAll<SVGGElement, GNode>("g"); 716 const selNodes = allNodes.data(filteredNodes, nodeToStr); 717 718 // remove old nodes 719 selNodes.exit().remove(); 720 721 // add new nodes 722 const newGs = selNodes.enter() 723 .append("g"); 724 725 newGs.classed("turbonode", function (n) { return true; }) 726 .classed("control", function (n) { return n.isControl(); }) 727 .classed("live", function (n) { return n.isLive(); }) 728 .classed("dead", function (n) { return !n.isLive(); }) 729 .classed("javascript", function (n) { return n.isJavaScript(); }) 730 .classed("input", function (n) { return n.isInput(); }) 731 .classed("simplified", function (n) { return n.isSimplified(); }) 732 .classed("machine", function (n) { return n.isMachine(); }) 733 .on('mouseenter', function (node) { 734 const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, Edge>('path'); 735 const adjInputEdges = visibleEdges.filter(e => e.target === node); 736 const adjOutputEdges = visibleEdges.filter(e => e.source === node); 737 adjInputEdges.attr('relToHover', "input"); 738 adjOutputEdges.attr('relToHover', "output"); 739 const adjInputNodes = adjInputEdges.data().map(e => e.source); 740 const visibleNodes = view.visibleNodes.selectAll<SVGGElement, GNode>("g"); 741 visibleNodes.data<GNode>(adjInputNodes, nodeToStr).attr('relToHover', "input"); 742 const adjOutputNodes = adjOutputEdges.data().map(e => e.target); 743 visibleNodes.data<GNode>(adjOutputNodes, nodeToStr).attr('relToHover', "output"); 744 view.updateGraphVisibility(); 745 }) 746 .on('mouseleave', function (node) { 747 const visibleEdges = view.visibleEdges.selectAll<SVGPathElement, Edge>('path'); 748 const adjEdges = visibleEdges.filter(e => e.target === node || e.source === node); 749 adjEdges.attr('relToHover', "none"); 750 const adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source)); 751 const visibleNodes = view.visibleNodes.selectAll<SVGPathElement, GNode>("g"); 752 visibleNodes.data(adjNodes, nodeToStr).attr('relToHover', "none"); 753 view.updateGraphVisibility(); 754 }) 755 .on("click", d => { 756 if (!d3.event.shiftKey) view.selectionHandler.clear(); 757 view.selectionHandler.select([d], undefined); 758 d3.event.stopPropagation(); 759 }) 760 .call(view.drag); 761 762 newGs.append("rect") 763 .attr("rx", 10) 764 .attr("ry", 10) 765 .attr('width', function (d) { 766 return d.getTotalNodeWidth(); 767 }) 768 .attr('height', function (d) { 769 return d.getNodeHeight(view.state.showTypes); 770 }); 771 772 function appendInputAndOutputBubbles(g, d) { 773 for (let i = 0; i < d.inputs.length; ++i) { 774 const x = d.getInputX(i); 775 const y = -DEFAULT_NODE_BUBBLE_RADIUS; 776 g.append('circle') 777 .classed("filledBubbleStyle", function (c) { 778 return d.inputs[i].isVisible(); 779 }) 780 .classed("bubbleStyle", function (c) { 781 return !d.inputs[i].isVisible(); 782 }) 783 .attr("id", "ib," + d.inputs[i].stringID()) 784 .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) 785 .attr("transform", function (d) { 786 return "translate(" + x + "," + y + ")"; 787 }) 788 .on("click", function (this: SVGCircleElement, d) { 789 const components = this.id.split(','); 790 const node = graph.nodeMap[components[3]]; 791 const edge = node.inputs[components[2]]; 792 const visible = !edge.isVisible(); 793 node.setInputVisibility(components[2], visible); 794 d3.event.stopPropagation(); 795 view.updateGraphVisibility(); 796 }); 797 } 798 if (d.outputs.length != 0) { 799 const x = d.getOutputX(); 800 const y = d.getNodeHeight(view.state.showTypes) + DEFAULT_NODE_BUBBLE_RADIUS; 801 g.append('circle') 802 .classed("filledBubbleStyle", function (c) { 803 return d.areAnyOutputsVisible() == 2; 804 }) 805 .classed("halFilledBubbleStyle", function (c) { 806 return d.areAnyOutputsVisible() == 1; 807 }) 808 .classed("bubbleStyle", function (c) { 809 return d.areAnyOutputsVisible() == 0; 810 }) 811 .attr("id", "ob," + d.id) 812 .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) 813 .attr("transform", function (d) { 814 return "translate(" + x + "," + y + ")"; 815 }) 816 .on("click", function (d) { 817 d.setOutputVisibility(d.areAnyOutputsVisible() == 0); 818 d3.event.stopPropagation(); 819 view.updateGraphVisibility(); 820 }); 821 } 822 } 823 824 newGs.each(function (d) { 825 appendInputAndOutputBubbles(d3.select(this), d); 826 }); 827 828 newGs.each(function (d) { 829 d3.select(this).append("text") 830 .classed("label", true) 831 .attr("text-anchor", "right") 832 .attr("dx", 5) 833 .attr("dy", 5) 834 .append('tspan') 835 .text(function (l) { 836 return d.getDisplayLabel(); 837 }) 838 .append("title") 839 .text(function (l) { 840 return d.getTitle(); 841 }); 842 if (d.nodeLabel.type != undefined) { 843 d3.select(this).append("text") 844 .classed("label", true) 845 .classed("type", true) 846 .attr("text-anchor", "right") 847 .attr("dx", 5) 848 .attr("dy", d.labelbbox.height + 5) 849 .append('tspan') 850 .text(function (l) { 851 return d.getDisplayType(); 852 }) 853 .append("title") 854 .text(function (l) { 855 return d.getType(); 856 }); 857 } 858 }); 859 860 const newAndOldNodes = newGs.merge(selNodes); 861 862 newAndOldNodes.select<SVGTextElement>('.type').each(function (d) { 863 this.setAttribute('visibility', view.state.showTypes ? 'visible' : 'hidden'); 864 }); 865 866 newAndOldNodes 867 .classed("selected", function (n) { 868 if (state.selection.isSelected(n)) return true; 869 return false; 870 }) 871 .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }) 872 .select('rect') 873 .attr('height', function (d) { return d.getNodeHeight(view.state.showTypes); }); 874 875 view.visibleBubbles = d3.selectAll('circle'); 876 877 view.updateInputAndOutputBubbles(); 878 879 graph.maxGraphX = graph.maxGraphNodeX; 880 newAndOldEdges.attr("d", function (edge) { 881 return edge.generatePath(graph, view.state.showTypes); 882 }); 883 } 884 885 getSvgViewDimensions() { 886 return [this.container.clientWidth, this.container.clientHeight]; 887 } 888 889 getSvgExtent(): [[number, number], [number, number]] { 890 return [[0, 0], [this.container.clientWidth, this.container.clientHeight]]; 891 } 892 893 minScale() { 894 const dimensions = this.getSvgViewDimensions(); 895 const minXScale = dimensions[0] / (2 * this.graph.width); 896 const minYScale = dimensions[1] / (2 * this.graph.height); 897 const minScale = Math.min(minXScale, minYScale); 898 this.panZoom.scaleExtent([minScale, 40]); 899 return minScale; 900 } 901 902 onresize() { 903 const trans = d3.zoomTransform(this.svg.node()); 904 const ctrans = this.panZoom.constrain()(trans, this.getSvgExtent(), this.panZoom.translateExtent()); 905 this.panZoom.transform(this.svg, ctrans); 906 } 907 908 toggleTypes() { 909 const view = this; 910 view.state.showTypes = !view.state.showTypes; 911 const element = document.getElementById('toggle-types'); 912 element.classList.toggle('button-input-toggled', view.state.showTypes); 913 view.updateGraphVisibility(); 914 } 915 916 viewSelection() { 917 const view = this; 918 let minX; 919 let maxX; 920 let minY; 921 let maxY; 922 let hasSelection = false; 923 view.visibleNodes.selectAll<SVGGElement, GNode>("g").each(function (n) { 924 if (view.state.selection.isSelected(n)) { 925 hasSelection = true; 926 minX = minX ? Math.min(minX, n.x) : n.x; 927 maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : 928 n.x + n.getTotalNodeWidth(); 929 minY = minY ? Math.min(minY, n.y) : n.y; 930 maxY = maxY ? Math.max(maxY, n.y + n.getNodeHeight(view.state.showTypes)) : 931 n.y + n.getNodeHeight(view.state.showTypes); 932 } 933 }); 934 if (hasSelection) { 935 view.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, 936 maxX + NODE_INPUT_WIDTH, maxY + 60); 937 } 938 } 939 940 viewGraphRegion(minX, minY, maxX, maxY) { 941 const [width, height] = this.getSvgViewDimensions(); 942 const dx = maxX - minX; 943 const dy = maxY - minY; 944 const x = (minX + maxX) / 2; 945 const y = (minY + maxY) / 2; 946 const scale = Math.min(width / dx, height / dy) * 0.9; 947 this.svg 948 .transition().duration(120).call(this.panZoom.scaleTo, scale) 949 .transition().duration(120).call(this.panZoom.translateTo, x, y); 950 } 951 952 viewWholeGraph() { 953 this.panZoom.scaleTo(this.svg, 0); 954 this.panZoom.translateTo(this.svg, 955 this.graph.minGraphX + this.graph.width / 2, 956 this.graph.minGraphY + this.graph.height / 2); 957 } 958} 959