// Copyright 2015 the V8 project authors. All rights reserved.
|
// Use of this source code is governed by a BSD-style license that can be
|
// found in the LICENSE file.
|
|
import * as d3 from "d3"
|
import {layoutNodeGraph} from "./graph-layout.js"
|
import {MAX_RANK_SENTINEL} from "./constants.js"
|
import {GNode, nodeToStr, isNodeInitiallyVisible} from "./node.js"
|
import {NODE_INPUT_WIDTH, MINIMUM_NODE_OUTPUT_APPROACH} from "./node.js"
|
import {DEFAULT_NODE_BUBBLE_RADIUS} from "./node.js"
|
import {Edge, edgeToStr} from "./edge.js"
|
import {View, PhaseView} from "./view.js"
|
import {MySelection} from "./selection.js"
|
import {partial, alignUp} from "./util.js"
|
|
function nodeToStringKey(n) {
|
return "" + n.id;
|
}
|
|
interface GraphState {
|
showTypes: boolean;
|
selection: MySelection;
|
mouseDownNode: any;
|
justDragged: boolean,
|
justScaleTransGraph: boolean,
|
lastKeyDown: number,
|
hideDead: boolean
|
}
|
|
export class GraphView extends View implements PhaseView {
|
divElement: d3.Selection<any, any, any, any>;
|
svg: d3.Selection<any, any, any, any>;
|
showPhaseByName: (string) => void;
|
state: GraphState;
|
nodes: Array<GNode>;
|
edges: Array<any>;
|
selectionHandler: NodeSelectionHandler;
|
graphElement: d3.Selection<any, any, any, any>;
|
visibleNodes: d3.Selection<any, GNode, any, any>;
|
visibleEdges: d3.Selection<any, Edge, any, any>;
|
minGraphX: number;
|
maxGraphX: number;
|
minGraphY: number;
|
maxGraphY: number;
|
width: number;
|
height: number;
|
maxGraphNodeX: number;
|
drag: d3.DragBehavior<any, GNode, GNode>;
|
panZoom: d3.ZoomBehavior<SVGElement, any>;
|
nodeMap: Array<any>;
|
visibleBubbles: d3.Selection<any, any, any, any>;
|
transitionTimout: number;
|
|
createViewElement() {
|
const pane = document.createElement('div');
|
pane.setAttribute('id', "graph");
|
return pane;
|
}
|
|
constructor(id, broker, showPhaseByName: (string) => void) {
|
super(id);
|
var graph = this;
|
this.showPhaseByName = showPhaseByName;
|
this.divElement = d3.select(this.divNode);
|
const svg = this.divElement.append("svg").attr('version', '1.1')
|
.attr("width", "100%")
|
.attr("height", "100%");
|
svg.on("click", function (d) {
|
graph.selectionHandler.clear();
|
});
|
graph.svg = svg;
|
|
graph.nodes = [];
|
graph.edges = [];
|
|
graph.minGraphX = 0;
|
graph.maxGraphX = 1;
|
graph.minGraphY = 0;
|
graph.maxGraphY = 1;
|
|
graph.state = {
|
selection: null,
|
mouseDownNode: null,
|
justDragged: false,
|
justScaleTransGraph: false,
|
lastKeyDown: -1,
|
showTypes: false,
|
hideDead: false
|
};
|
|
this.selectionHandler = {
|
clear: function () {
|
graph.state.selection.clear();
|
broker.broadcastClear(this);
|
graph.updateGraphVisibility();
|
},
|
select: function (nodes, selected) {
|
let locations = [];
|
for (const node of nodes) {
|
if (node.sourcePosition) {
|
locations.push(node.sourcePosition);
|
}
|
if (node.origin && node.origin.bytecodePosition) {
|
locations.push({ bytecodePosition: node.origin.bytecodePosition });
|
}
|
}
|
graph.state.selection.select(nodes, selected);
|
broker.broadcastSourcePositionSelect(this, locations, selected);
|
graph.updateGraphVisibility();
|
},
|
brokeredNodeSelect: function (locations, selected) {
|
let selection = graph.nodes
|
.filter(function (n) {
|
return locations.has(nodeToStringKey(n))
|
&& (!graph.state.hideDead || n.isLive());
|
});
|
graph.state.selection.select(selection, selected);
|
// Update edge visibility based on selection.
|
graph.nodes.forEach((n) => {
|
if (graph.state.selection.isSelected(n)) n.visible = true;
|
});
|
graph.edges.forEach(function (e) {
|
e.visible = e.visible ||
|
(graph.state.selection.isSelected(e.source) && graph.state.selection.isSelected(e.target));
|
});
|
graph.updateGraphVisibility();
|
},
|
brokeredClear: function () {
|
graph.state.selection.clear();
|
graph.updateGraphVisibility();
|
}
|
};
|
broker.addNodeHandler(this.selectionHandler);
|
|
graph.state.selection = new MySelection(nodeToStringKey);
|
|
const defs = svg.append('svg:defs');
|
defs.append('svg:marker')
|
.attr('id', 'end-arrow')
|
.attr('viewBox', '0 -4 8 8')
|
.attr('refX', 2)
|
.attr('markerWidth', 2.5)
|
.attr('markerHeight', 2.5)
|
.attr('orient', 'auto')
|
.append('svg:path')
|
.attr('d', 'M0,-4L8,0L0,4');
|
|
this.graphElement = svg.append("g");
|
graph.visibleEdges = this.graphElement.append("g");
|
graph.visibleNodes = this.graphElement.append("g");
|
|
graph.drag = d3.drag<any, GNode, GNode>()
|
.on("drag", function (d) {
|
d.x += d3.event.dx;
|
d.y += d3.event.dy;
|
graph.updateGraphVisibility();
|
});
|
|
|
d3.select("#layout").on("click", partial(this.layoutAction, graph));
|
d3.select("#show-all").on("click", partial(this.showAllAction, graph));
|
d3.select("#toggle-hide-dead").on("click", partial(this.toggleHideDead, graph));
|
d3.select("#hide-unselected").on("click", partial(this.hideUnselectedAction, graph));
|
d3.select("#hide-selected").on("click", partial(this.hideSelectedAction, graph));
|
d3.select("#zoom-selection").on("click", partial(this.zoomSelectionAction, graph));
|
d3.select("#toggle-types").on("click", partial(this.toggleTypesAction, graph));
|
|
// listen for key events
|
d3.select(window).on("keydown", function (e) {
|
graph.svgKeyDown.call(graph);
|
}).on("keyup", function () {
|
graph.svgKeyUp.call(graph);
|
});
|
|
function zoomed() {
|
if (d3.event.shiftKey) return false;
|
graph.graphElement.attr("transform", d3.event.transform);
|
}
|
|
const zoomSvg = d3.zoom<SVGElement, any>()
|
.scaleExtent([0.2, 40])
|
.on("zoom", zoomed)
|
.on("start", function () {
|
if (d3.event.shiftKey) return;
|
d3.select('body').style("cursor", "move");
|
})
|
.on("end", function () {
|
d3.select('body').style("cursor", "auto");
|
});
|
|
svg.call(zoomSvg).on("dblclick.zoom", null);
|
|
graph.panZoom = zoomSvg;
|
|
}
|
|
|
static get selectedClass() {
|
return "selected";
|
}
|
static get rectClass() {
|
return "nodeStyle";
|
}
|
static get activeEditId() {
|
return "active-editing";
|
}
|
static get nodeRadius() {
|
return 50;
|
}
|
|
getNodeHeight(d): number {
|
if (this.state.showTypes) {
|
return d.normalheight + d.labelbbox.height;
|
} else {
|
return d.normalheight;
|
}
|
}
|
|
getEdgeFrontier(nodes, inEdges, edgeFilter) {
|
let frontier = new Set();
|
for (const n of nodes) {
|
var edges = inEdges ? n.inputs : n.outputs;
|
var edgeNumber = 0;
|
edges.forEach(function (edge) {
|
if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) {
|
frontier.add(edge);
|
}
|
++edgeNumber;
|
});
|
}
|
return frontier;
|
}
|
|
getNodeFrontier(nodes, inEdges, edgeFilter) {
|
let graph = this;
|
var frontier = new Set();
|
var newState = true;
|
var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter);
|
// Control key toggles edges rather than just turning them on
|
if (d3.event.ctrlKey) {
|
edgeFrontier.forEach(function (edge) {
|
if (edge.visible) {
|
newState = false;
|
}
|
});
|
}
|
edgeFrontier.forEach(function (edge) {
|
edge.visible = newState;
|
if (newState) {
|
var node = inEdges ? edge.source : edge.target;
|
node.visible = true;
|
frontier.add(node);
|
}
|
});
|
graph.updateGraphVisibility();
|
if (newState) {
|
return frontier;
|
} else {
|
return undefined;
|
}
|
}
|
|
initializeContent(data, rememberedSelection) {
|
this.createGraph(data, rememberedSelection);
|
if (rememberedSelection != null) {
|
this.attachSelection(rememberedSelection);
|
this.connectVisibleSelectedNodes();
|
this.viewSelection();
|
} else {
|
this.viewWholeGraph();
|
}
|
}
|
|
deleteContent() {
|
if (this.visibleNodes) {
|
this.nodes = [];
|
this.edges = [];
|
this.nodeMap = [];
|
this.updateGraphVisibility();
|
}
|
};
|
|
measureText(text) {
|
const textMeasure = document.getElementById('text-measure') as SVGTSpanElement;
|
textMeasure.textContent = text;
|
return {
|
width: textMeasure.getBBox().width,
|
height: textMeasure.getBBox().height,
|
};
|
}
|
|
createGraph(data, rememberedSelection) {
|
var g = this;
|
g.nodes = [];
|
g.nodeMap = [];
|
data.nodes.forEach(function (n, i) {
|
n.__proto__ = GNode.prototype;
|
n.visible = false;
|
n.x = 0;
|
n.y = 0;
|
if (typeof n.pos === "number") {
|
// Backwards compatibility.
|
n.sourcePosition = { scriptOffset: n.pos, inliningId: -1 };
|
}
|
n.rank = MAX_RANK_SENTINEL;
|
n.inputs = [];
|
n.outputs = [];
|
n.rpo = -1;
|
n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH;
|
n.cfg = n.control;
|
g.nodeMap[n.id] = n;
|
n.displayLabel = n.getDisplayLabel();
|
n.labelbbox = g.measureText(n.displayLabel);
|
n.typebbox = g.measureText(n.getDisplayType());
|
var innerwidth = Math.max(n.labelbbox.width, n.typebbox.width);
|
n.width = alignUp(innerwidth + NODE_INPUT_WIDTH * 2,
|
NODE_INPUT_WIDTH);
|
var innerheight = Math.max(n.labelbbox.height, n.typebbox.height);
|
n.normalheight = innerheight + 20;
|
g.nodes.push(n);
|
});
|
g.edges = [];
|
data.edges.forEach(function (e, i) {
|
var t = g.nodeMap[e.target];
|
var s = g.nodeMap[e.source];
|
var newEdge = new Edge(t, e.index, s, e.type);
|
t.inputs.push(newEdge);
|
s.outputs.push(newEdge);
|
g.edges.push(newEdge);
|
if (e.type == 'control') {
|
s.cfg = true;
|
}
|
});
|
g.nodes.forEach(function (n, i) {
|
n.visible = isNodeInitiallyVisible(n) && (!g.state.hideDead || n.isLive());
|
if (rememberedSelection != undefined) {
|
if (rememberedSelection.has(nodeToStringKey(n))) {
|
n.visible = true;
|
}
|
}
|
});
|
g.updateGraphVisibility();
|
g.layoutGraph();
|
g.updateGraphVisibility();
|
g.viewWholeGraph();
|
}
|
|
connectVisibleSelectedNodes() {
|
var graph = this;
|
for (const n of graph.state.selection) {
|
n.inputs.forEach(function (edge) {
|
if (edge.source.visible && edge.target.visible) {
|
edge.visible = true;
|
}
|
});
|
n.outputs.forEach(function (edge) {
|
if (edge.source.visible && edge.target.visible) {
|
edge.visible = true;
|
}
|
});
|
}
|
}
|
|
updateInputAndOutputBubbles() {
|
var g = this;
|
var s = g.visibleBubbles;
|
s.classed("filledBubbleStyle", function (c) {
|
var components = this.id.split(',');
|
if (components[0] == "ib") {
|
var edge = g.nodeMap[components[3]].inputs[components[2]];
|
return edge.isVisible();
|
} else {
|
return g.nodeMap[components[1]].areAnyOutputsVisible() == 2;
|
}
|
}).classed("halfFilledBubbleStyle", function (c) {
|
var components = this.id.split(',');
|
if (components[0] == "ib") {
|
var edge = g.nodeMap[components[3]].inputs[components[2]];
|
return false;
|
} else {
|
return g.nodeMap[components[1]].areAnyOutputsVisible() == 1;
|
}
|
}).classed("bubbleStyle", function (c) {
|
var components = this.id.split(',');
|
if (components[0] == "ib") {
|
var edge = g.nodeMap[components[3]].inputs[components[2]];
|
return !edge.isVisible();
|
} else {
|
return g.nodeMap[components[1]].areAnyOutputsVisible() == 0;
|
}
|
});
|
s.each(function (c) {
|
var components = this.id.split(',');
|
if (components[0] == "ob") {
|
var from = g.nodeMap[components[1]];
|
var x = from.getOutputX();
|
var y = g.getNodeHeight(from) + DEFAULT_NODE_BUBBLE_RADIUS;
|
var transform = "translate(" + x + "," + y + ")";
|
this.setAttribute('transform', transform);
|
}
|
});
|
}
|
|
attachSelection(s) {
|
const graph = this;
|
if (!(s instanceof Set)) return;
|
graph.selectionHandler.clear();
|
const selected = graph.nodes.filter((n) =>
|
s.has(graph.state.selection.stringKey(n)) && (!graph.state.hideDead || n.isLive()));
|
graph.selectionHandler.select(selected, true);
|
}
|
|
detachSelection() {
|
return this.state.selection.detachSelection();
|
}
|
|
selectAllNodes() {
|
var graph = this;
|
if (!d3.event.shiftKey) {
|
graph.state.selection.clear();
|
}
|
const allVisibleNodes = graph.nodes.filter((n) => n.visible);
|
graph.state.selection.select(allVisibleNodes, true);
|
graph.updateGraphVisibility();
|
}
|
|
layoutAction(graph) {
|
graph.updateGraphVisibility();
|
graph.layoutGraph();
|
graph.updateGraphVisibility();
|
graph.viewWholeGraph();
|
}
|
|
showAllAction(graph) {
|
graph.nodes.forEach(function (n) {
|
n.visible = !graph.state.hideDead || n.isLive();
|
});
|
graph.edges.forEach(function (e) {
|
e.visible = !graph.state.hideDead || (e.source.isLive() && e.target.isLive());
|
});
|
graph.updateGraphVisibility();
|
graph.viewWholeGraph();
|
}
|
|
toggleHideDead(graph) {
|
graph.state.hideDead = !graph.state.hideDead;
|
if (graph.state.hideDead) graph.hideDead();
|
var element = document.getElementById('toggle-hide-dead');
|
element.classList.toggle('button-input-toggled', graph.state.hideDead);
|
}
|
|
hideDead() {
|
const graph = this;
|
graph.nodes.filter(function (n) {
|
if (!n.isLive()) {
|
n.visible = false;
|
graph.state.selection.select([n], false);
|
}
|
})
|
graph.updateGraphVisibility();
|
}
|
|
hideUnselectedAction(graph) {
|
graph.nodes.forEach(function (n) {
|
if (!graph.state.selection.isSelected(n)) {
|
n.visible = false;
|
}
|
});
|
graph.updateGraphVisibility();
|
}
|
|
hideSelectedAction(graph) {
|
graph.nodes.forEach(function (n) {
|
if (graph.state.selection.isSelected(n)) {
|
n.visible = false;
|
}
|
});
|
graph.selectionHandler.clear();
|
}
|
|
zoomSelectionAction(graph) {
|
graph.viewSelection();
|
}
|
|
toggleTypesAction(graph) {
|
graph.toggleTypes();
|
}
|
|
searchInputAction(searchBar, e: KeyboardEvent) {
|
const graph = this;
|
if (e.keyCode == 13) {
|
graph.selectionHandler.clear();
|
var query = searchBar.value;
|
window.sessionStorage.setItem("lastSearch", query);
|
if (query.length == 0) return;
|
|
var reg = new RegExp(query);
|
var filterFunction = function (n) {
|
return (reg.exec(n.getDisplayLabel()) != null ||
|
(graph.state.showTypes && reg.exec(n.getDisplayType())) ||
|
(reg.exec(n.getTitle())) ||
|
reg.exec(n.opcode) != null);
|
};
|
|
const selection = graph.nodes.filter(
|
function (n, i) {
|
if ((e.ctrlKey || n.visible) && filterFunction(n)) {
|
if (e.ctrlKey) n.visible = true;
|
return true;
|
}
|
return false;
|
});
|
|
graph.selectionHandler.select(selection, true);
|
graph.connectVisibleSelectedNodes();
|
graph.updateGraphVisibility();
|
searchBar.blur();
|
graph.viewSelection();
|
}
|
e.stopPropagation();
|
}
|
|
svgKeyDown() {
|
var state = this.state;
|
var graph = this;
|
|
// Don't handle key press repetition
|
if (state.lastKeyDown !== -1) return;
|
|
var showSelectionFrontierNodes = function (inEdges, filter, select) {
|
var frontier = graph.getNodeFrontier(state.selection, inEdges, filter);
|
if (frontier != undefined && frontier.size) {
|
if (select) {
|
if (!d3.event.shiftKey) {
|
state.selection.clear();
|
}
|
state.selection.select(frontier, true);
|
}
|
graph.updateGraphVisibility();
|
}
|
allowRepetition = false;
|
}
|
|
var allowRepetition = true;
|
var eventHandled = true; // unless the below switch defaults
|
switch (d3.event.keyCode) {
|
case 49:
|
case 50:
|
case 51:
|
case 52:
|
case 53:
|
case 54:
|
case 55:
|
case 56:
|
case 57:
|
// '1'-'9'
|
showSelectionFrontierNodes(true,
|
(edge, index) => { return index == (d3.event.keyCode - 49); },
|
false);
|
break;
|
case 97:
|
case 98:
|
case 99:
|
case 100:
|
case 101:
|
case 102:
|
case 103:
|
case 104:
|
case 105:
|
// 'numpad 1'-'numpad 9'
|
showSelectionFrontierNodes(true,
|
(edge, index) => { return index == (d3.event.keyCode - 97); },
|
false);
|
break;
|
case 67:
|
// 'c'
|
showSelectionFrontierNodes(d3.event.altKey,
|
(edge, index) => { return edge.type == 'control'; },
|
true);
|
break;
|
case 69:
|
// 'e'
|
showSelectionFrontierNodes(d3.event.altKey,
|
(edge, index) => { return edge.type == 'effect'; },
|
true);
|
break;
|
case 79:
|
// 'o'
|
showSelectionFrontierNodes(false, undefined, false);
|
break;
|
case 73:
|
// 'i'
|
showSelectionFrontierNodes(true, undefined, false);
|
break;
|
case 65:
|
// 'a'
|
graph.selectAllNodes();
|
allowRepetition = false;
|
break;
|
case 38:
|
case 40: {
|
showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true);
|
break;
|
}
|
case 82:
|
// 'r'
|
if (!d3.event.ctrlKey) {
|
this.layoutAction(this);
|
} else {
|
eventHandled = false;
|
}
|
break;
|
case 83:
|
// 's'
|
graph.selectOrigins();
|
break;
|
case 191:
|
// '/'
|
document.getElementById("search-input").focus();
|
break;
|
default:
|
eventHandled = false;
|
break;
|
}
|
if (eventHandled) {
|
d3.event.preventDefault();
|
}
|
if (!allowRepetition) {
|
state.lastKeyDown = d3.event.keyCode;
|
}
|
}
|
|
svgKeyUp() {
|
this.state.lastKeyDown = -1
|
};
|
|
layoutGraph() {
|
layoutNodeGraph(this);
|
}
|
|
selectOrigins() {
|
const state = this.state;
|
const origins = [];
|
let phase = null;
|
for (const n of state.selection) {
|
if (n.origin) {
|
const node = this.nodeMap[n.origin.nodeId];
|
origins.push(node);
|
phase = n.origin.phase;
|
}
|
}
|
if (origins.length) {
|
state.selection.clear();
|
state.selection.select(origins, true);
|
if (phase) {
|
this.showPhaseByName(phase);
|
}
|
}
|
}
|
|
// call to propagate changes to graph
|
updateGraphVisibility() {
|
let graph = this;
|
let state = graph.state;
|
|
var filteredEdges = graph.edges.filter(function (e) {
|
return e.isVisible();
|
});
|
const selEdges = graph.visibleEdges.selectAll<SVGPathElement, Edge>("path").data(filteredEdges, edgeToStr);
|
|
// remove old links
|
selEdges.exit().remove();
|
|
// add new paths
|
selEdges.enter()
|
.append('path')
|
.style('marker-end', 'url(#end-arrow)')
|
.classed('hidden', function (e) {
|
return !e.isVisible();
|
})
|
.attr("id", function (edge) { return "e," + edge.stringID(); })
|
.on("click", function (edge) {
|
d3.event.stopPropagation();
|
if (!d3.event.shiftKey) {
|
graph.selectionHandler.clear();
|
}
|
graph.selectionHandler.select([edge.source, edge.target], true);
|
})
|
.attr("adjacentToHover", "false");
|
|
// Set the correct styles on all of the paths
|
selEdges.classed('value', function (e) {
|
return e.type == 'value' || e.type == 'context';
|
}).classed('control', function (e) {
|
return e.type == 'control';
|
}).classed('effect', function (e) {
|
return e.type == 'effect';
|
}).classed('frame-state', function (e) {
|
return e.type == 'frame-state';
|
}).attr('stroke-dasharray', function (e) {
|
if (e.type == 'frame-state') return "10,10";
|
return (e.type == 'effect') ? "5,5" : "";
|
});
|
|
// select existing nodes
|
const filteredNodes = graph.nodes.filter(n => n.visible);
|
const allNodes = graph.visibleNodes.selectAll<SVGGElement, GNode>("g");
|
const selNodes = allNodes.data(filteredNodes, nodeToStr);
|
|
// remove old nodes
|
selNodes.exit().remove();
|
|
// add new nodes
|
var newGs = selNodes.enter()
|
.append("g");
|
|
newGs.classed("turbonode", function (n) { return true; })
|
.classed("control", function (n) { return n.isControl(); })
|
.classed("live", function (n) { return n.isLive(); })
|
.classed("dead", function (n) { return !n.isLive(); })
|
.classed("javascript", function (n) { return n.isJavaScript(); })
|
.classed("input", function (n) { return n.isInput(); })
|
.classed("simplified", function (n) { return n.isSimplified(); })
|
.classed("machine", function (n) { return n.isMachine(); })
|
.on('mouseenter', function (node) {
|
const visibleEdges = graph.visibleEdges.selectAll<SVGPathElement, Edge>('path');
|
const adjInputEdges = visibleEdges.filter(e => { return e.target === node; });
|
const adjOutputEdges = visibleEdges.filter(e => { return e.source === node; });
|
adjInputEdges.attr('relToHover', "input");
|
adjOutputEdges.attr('relToHover', "output");
|
const adjInputNodes = adjInputEdges.data().map(e => e.source);
|
const visibleNodes = graph.visibleNodes.selectAll<SVGGElement, GNode>("g");
|
const input = visibleNodes.data<GNode>(adjInputNodes, nodeToStr)
|
.attr('relToHover', "input");
|
const adjOutputNodes = adjOutputEdges.data().map(e => e.target);
|
const output = visibleNodes.data<GNode>(adjOutputNodes, nodeToStr)
|
.attr('relToHover', "output");
|
graph.updateGraphVisibility();
|
})
|
.on('mouseleave', function (node) {
|
const visibleEdges = graph.visibleEdges.selectAll<SVGPathElement, Edge>('path');
|
const adjEdges = visibleEdges.filter(e => { return e.target === node || e.source === node; });
|
adjEdges.attr('relToHover', "none");
|
const adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source));
|
const visibleNodes = graph.visibleNodes.selectAll<SVGPathElement, GNode>("g");
|
const nodes = visibleNodes.data(adjNodes, nodeToStr)
|
.attr('relToHover', "none");
|
graph.updateGraphVisibility();
|
})
|
.on("click", (d) => {
|
if (!d3.event.shiftKey) graph.selectionHandler.clear();
|
graph.selectionHandler.select([d], undefined);
|
d3.event.stopPropagation();
|
})
|
.call(graph.drag)
|
|
newGs.append("rect")
|
.attr("rx", 10)
|
.attr("ry", 10)
|
.attr('width', function (d) {
|
return d.getTotalNodeWidth();
|
})
|
.attr('height', function (d) {
|
return graph.getNodeHeight(d);
|
})
|
|
function appendInputAndOutputBubbles(g, d) {
|
for (var i = 0; i < d.inputs.length; ++i) {
|
var x = d.getInputX(i);
|
var y = -DEFAULT_NODE_BUBBLE_RADIUS;
|
var s = g.append('circle')
|
.classed("filledBubbleStyle", function (c) {
|
return d.inputs[i].isVisible();
|
})
|
.classed("bubbleStyle", function (c) {
|
return !d.inputs[i].isVisible();
|
})
|
.attr("id", "ib," + d.inputs[i].stringID())
|
.attr("r", DEFAULT_NODE_BUBBLE_RADIUS)
|
.attr("transform", function (d) {
|
return "translate(" + x + "," + y + ")";
|
})
|
.on("click", function (d) {
|
var components = this.id.split(',');
|
var node = graph.nodeMap[components[3]];
|
var edge = node.inputs[components[2]];
|
var visible = !edge.isVisible();
|
node.setInputVisibility(components[2], visible);
|
d3.event.stopPropagation();
|
graph.updateGraphVisibility();
|
});
|
}
|
if (d.outputs.length != 0) {
|
var x = d.getOutputX();
|
var y = graph.getNodeHeight(d) + DEFAULT_NODE_BUBBLE_RADIUS;
|
var s = g.append('circle')
|
.classed("filledBubbleStyle", function (c) {
|
return d.areAnyOutputsVisible() == 2;
|
})
|
.classed("halFilledBubbleStyle", function (c) {
|
return d.areAnyOutputsVisible() == 1;
|
})
|
.classed("bubbleStyle", function (c) {
|
return d.areAnyOutputsVisible() == 0;
|
})
|
.attr("id", "ob," + d.id)
|
.attr("r", DEFAULT_NODE_BUBBLE_RADIUS)
|
.attr("transform", function (d) {
|
return "translate(" + x + "," + y + ")";
|
})
|
.on("click", function (d) {
|
d.setOutputVisibility(d.areAnyOutputsVisible() == 0);
|
d3.event.stopPropagation();
|
graph.updateGraphVisibility();
|
});
|
}
|
}
|
|
newGs.each(function (d) {
|
appendInputAndOutputBubbles(d3.select(this), d);
|
});
|
|
newGs.each(function (d) {
|
d3.select(this).append("text")
|
.classed("label", true)
|
.attr("text-anchor", "right")
|
.attr("dx", 5)
|
.attr("dy", 5)
|
.append('tspan')
|
.text(function (l) {
|
return d.getDisplayLabel();
|
})
|
.append("title")
|
.text(function (l) {
|
return d.getTitle();
|
})
|
if (d.type != undefined) {
|
d3.select(this).append("text")
|
.classed("label", true)
|
.classed("type", true)
|
.attr("text-anchor", "right")
|
.attr("dx", 5)
|
.attr("dy", d.labelbbox.height + 5)
|
.append('tspan')
|
.text(function (l) {
|
return d.getDisplayType();
|
})
|
.append("title")
|
.text(function (l) {
|
return d.getType();
|
})
|
}
|
});
|
|
const newAndOldNodes = newGs.merge(selNodes);
|
|
newAndOldNodes.select<SVGTextElement>('.type').each(function (d) {
|
this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden');
|
});
|
|
newAndOldNodes
|
.classed("selected", function (n) {
|
if (state.selection.isSelected(n)) return true;
|
return false;
|
})
|
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
|
.select('rect')
|
.attr('height', function (d) { return graph.getNodeHeight(d); });
|
|
graph.visibleBubbles = d3.selectAll('circle');
|
|
graph.updateInputAndOutputBubbles();
|
|
graph.maxGraphX = graph.maxGraphNodeX;
|
selEdges.attr("d", function (edge) {
|
return edge.generatePath(graph);
|
});
|
}
|
|
getSvgViewDimensions() {
|
return [this.container.clientWidth, this.container.clientHeight];
|
}
|
|
getSvgExtent(): [[number, number], [number, number]] {
|
return [[0, 0], [this.container.clientWidth, this.container.clientHeight]];
|
}
|
|
minScale() {
|
const graph = this;
|
const dimensions = this.getSvgViewDimensions();
|
const minXScale = dimensions[0] / (2 * graph.width);
|
const minYScale = dimensions[1] / (2 * graph.height);
|
const minScale = Math.min(minXScale, minYScale);
|
this.panZoom.scaleExtent([minScale, 40]);
|
return minScale;
|
}
|
|
onresize() {
|
const trans = d3.zoomTransform(this.svg.node());
|
const ctrans = this.panZoom.constrain()(trans, this.getSvgExtent(), this.panZoom.translateExtent())
|
this.panZoom.transform(this.svg, ctrans)
|
}
|
|
toggleTypes() {
|
var graph = this;
|
graph.state.showTypes = !graph.state.showTypes;
|
var element = document.getElementById('toggle-types');
|
element.classList.toggle('button-input-toggled', graph.state.showTypes);
|
graph.updateGraphVisibility();
|
}
|
|
viewSelection() {
|
var graph = this;
|
var minX, maxX, minY, maxY;
|
var hasSelection = false;
|
graph.visibleNodes.selectAll<SVGGElement, GNode>("g").each(function (n) {
|
if (graph.state.selection.isSelected(n)) {
|
hasSelection = true;
|
minX = minX ? Math.min(minX, n.x) : n.x;
|
maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) :
|
n.x + n.getTotalNodeWidth();
|
minY = minY ? Math.min(minY, n.y) : n.y;
|
maxY = maxY ? Math.max(maxY, n.y + graph.getNodeHeight(n)) :
|
n.y + graph.getNodeHeight(n);
|
}
|
});
|
if (hasSelection) {
|
graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60,
|
maxX + NODE_INPUT_WIDTH, maxY + 60,
|
true);
|
}
|
}
|
|
viewGraphRegion(minX, minY, maxX, maxY, transition) {
|
const [width, height] = this.getSvgViewDimensions();
|
const dx = maxX - minX;
|
const dy = maxY - minY;
|
const x = (minX + maxX) / 2;
|
const y = (minY + maxY) / 2;
|
const scale = Math.min(width / (1.1 * dx), height / (1.1 * dy));
|
const transform = d3.zoomIdentity.translate(1500, 100).scale(0.75);
|
this.svg
|
.transition().duration(300).call(this.panZoom.translateTo, x, y)
|
.transition().duration(300).call(this.panZoom.scaleTo, scale)
|
.transition().duration(300).call(this.panZoom.translateTo, x, y);
|
}
|
|
viewWholeGraph() {
|
this.panZoom.scaleTo(this.svg, 0);
|
this.panZoom.translateTo(this.svg, this.minGraphX + this.width / 2, this.minGraphY + this.height / 2)
|
}
|
}
|