• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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