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