fbccad5c66
BUG= R=danno Review-Url: https://codereview.chromium.org/2171543004 Cr-Commit-Position: refs/heads/master@{#38045}
998 lines
29 KiB
JavaScript
998 lines
29 KiB
JavaScript
// 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.
|
|
|
|
"use strict";
|
|
|
|
class GraphView extends View {
|
|
constructor (d3, id, nodes, edges, broker) {
|
|
super(id, broker);
|
|
var graph = this;
|
|
|
|
var svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%");
|
|
graph.svg = svg;
|
|
|
|
graph.nodes = nodes || [];
|
|
graph.edges = 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
|
|
};
|
|
|
|
var selectionHandler = {
|
|
clear: function() {
|
|
broker.clear(selectionHandler);
|
|
},
|
|
select: function(items, selected) {
|
|
var ranges = [];
|
|
for (var d of items) {
|
|
if (selected) {
|
|
d.classList.add("selected");
|
|
} else {
|
|
d.classList.remove("selected");
|
|
}
|
|
var data = d.__data__;
|
|
ranges.push([data.pos, data.pos + 1, data.id]);
|
|
}
|
|
broker.select(selectionHandler, ranges, selected);
|
|
},
|
|
selectionDifference: function(span1, inclusive1, span2, inclusive2) {
|
|
// Should not be called
|
|
},
|
|
brokeredSelect: function(ranges, selected) {
|
|
var test = [].entries().next();
|
|
var selection = graph.nodes
|
|
.filter(function(n) {
|
|
var pos = n.pos;
|
|
for (var range of ranges) {
|
|
var start = range[0];
|
|
var end = range[1];
|
|
var id = range[2];
|
|
if (end != undefined) {
|
|
if (pos >= start && pos < end) {
|
|
return true;
|
|
}
|
|
} else if (start != undefined) {
|
|
if (pos === start) {
|
|
return true;
|
|
}
|
|
} else {
|
|
if (n.id === id) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
var newlySelected = new Set();
|
|
selection.forEach(function(n) {
|
|
newlySelected.add(n);
|
|
if (!n.visible) {
|
|
n.visible = true;
|
|
}
|
|
});
|
|
graph.updateGraphVisibility();
|
|
graph.visibleNodes.each(function(n) {
|
|
if (newlySelected.has(n)) {
|
|
graph.state.selection.select(this, selected);
|
|
}
|
|
});
|
|
graph.updateGraphVisibility();
|
|
graph.viewSelection();
|
|
},
|
|
brokeredClear: function() {
|
|
graph.state.selection.clear();
|
|
}
|
|
};
|
|
broker.addSelectionHandler(selectionHandler);
|
|
|
|
graph.state.selection = new Selection(selectionHandler);
|
|
|
|
var 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").selectAll("g");
|
|
graph.visibleNodes = this.graphElement.append("g").selectAll("g");
|
|
|
|
graph.drag = d3.behavior.drag()
|
|
.origin(function(d){
|
|
return {x: d.x, y: d.y};
|
|
})
|
|
.on("drag", function(args){
|
|
graph.state.justDragged = true;
|
|
graph.dragmove.call(graph, args);
|
|
})
|
|
|
|
d3.select("#upload").on("click", partial(this.uploadAction, graph));
|
|
d3.select("#layout").on("click", partial(this.layoutAction, graph));
|
|
d3.select("#show-all").on("click", partial(this.showAllAction, 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));
|
|
d3.select("#search-input").on("keydown", partial(this.searchInputAction, graph));
|
|
|
|
// listen for key events
|
|
d3.select(window).on("keydown", function(e){
|
|
graph.svgKeyDown.call(graph);
|
|
})
|
|
.on("keyup", function(){
|
|
graph.svgKeyUp.call(graph);
|
|
});
|
|
svg.on("mousedown", function(d){graph.svgMouseDown.call(graph, d);});
|
|
svg.on("mouseup", function(d){graph.svgMouseUp.call(graph, d);});
|
|
|
|
graph.dragSvg = d3.behavior.zoom()
|
|
.on("zoom", function(){
|
|
if (d3.event.sourceEvent.shiftKey){
|
|
return false;
|
|
} else{
|
|
graph.zoomed.call(graph);
|
|
}
|
|
return true;
|
|
})
|
|
.on("zoomstart", function(){
|
|
if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move");
|
|
})
|
|
.on("zoomend", function(){
|
|
d3.select('body').style("cursor", "auto");
|
|
});
|
|
|
|
svg.call(graph.dragSvg).on("dblclick.zoom", null);
|
|
}
|
|
|
|
static get selectedClass() {
|
|
return "selected";
|
|
}
|
|
static get rectClass() {
|
|
return "nodeStyle";
|
|
}
|
|
static get activeEditId() {
|
|
return "active-editing";
|
|
}
|
|
static get nodeRadius() {
|
|
return 50;
|
|
}
|
|
|
|
getNodeHeight(graph) {
|
|
if (this.state.showTypes) {
|
|
return DEFAULT_NODE_HEIGHT + TYPE_HEIGHT;
|
|
} else {
|
|
return DEFAULT_NODE_HEIGHT;
|
|
}
|
|
}
|
|
|
|
getEdgeFrontier(nodes, inEdges, edgeFilter) {
|
|
let frontier = new Set();
|
|
nodes.forEach(function(element) {
|
|
var edges = inEdges ? element.__data__.inputs : element.__data__.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 graph.visibleNodes.filter(function(n) {
|
|
return frontier.has(n);
|
|
});
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
dragmove(d) {
|
|
var graph = this;
|
|
d.x += d3.event.dx;
|
|
d.y += d3.event.dy;
|
|
graph.updateGraphVisibility();
|
|
}
|
|
|
|
initializeContent(data, rememberedSelection) {
|
|
this.createGraph(data, rememberedSelection);
|
|
if (rememberedSelection != null) {
|
|
this.attachSelection(rememberedSelection);
|
|
this.connectVisibleSelectedNodes();
|
|
}
|
|
this.updateGraphVisibility();
|
|
}
|
|
|
|
deleteContent() {
|
|
if (this.visibleNodes) {
|
|
this.nodes = [];
|
|
this.edges = [];
|
|
this.nodeMap = [];
|
|
this.updateGraphVisibility();
|
|
}
|
|
};
|
|
|
|
createGraph(data, initiallyVisibileIds) {
|
|
var g = this;
|
|
g.nodes = data.nodes;
|
|
g.nodeMap = [];
|
|
var textMeasure = document.getElementById('text-measure');
|
|
g.nodes.forEach(function(n, i){
|
|
n.__proto__ = Node;
|
|
n.visible = false;
|
|
n.x = 0;
|
|
n.y = 0;
|
|
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();
|
|
textMeasure.textContent = n.getDisplayLabel();
|
|
var width = textMeasure.getComputedTextLength();
|
|
textMeasure.textContent = n.getDisplayType();
|
|
width = Math.max(width, textMeasure.getComputedTextLength());
|
|
n.width = Math.alignUp(width + NODE_INPUT_WIDTH * 2,
|
|
NODE_INPUT_WIDTH);
|
|
});
|
|
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);
|
|
if (initiallyVisibileIds != undefined) {
|
|
if (initiallyVisibileIds.has(n.id)) {
|
|
n.visible = true;
|
|
}
|
|
}
|
|
});
|
|
g.fitGraphViewToWindow();
|
|
g.updateGraphVisibility();
|
|
g.layoutGraph();
|
|
g.updateGraphVisibility();
|
|
g.viewWholeGraph();
|
|
}
|
|
|
|
connectVisibleSelectedNodes() {
|
|
var graph = this;
|
|
graph.state.selection.selection.forEach(function(element) {
|
|
var edgeNumber = 0;
|
|
element.__data__.inputs.forEach(function(edge) {
|
|
if (edge.source.visible && edge.target.visible) {
|
|
edge.visible = true;
|
|
}
|
|
});
|
|
element.__data__.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() + DEFAULT_NODE_BUBBLE_RADIUS / 2 + 4;
|
|
var transform = "translate(" + x + "," + y + ")";
|
|
this.setAttribute('transform', transform);
|
|
}
|
|
});
|
|
}
|
|
|
|
attachSelection(s) {
|
|
var graph = this;
|
|
if (s.size != 0) {
|
|
this.visibleNodes.each(function(n) {
|
|
if (s.has(this.__data__.id)) {
|
|
graph.state.selection.select(this, true);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
detachSelection() {
|
|
var selection = this.state.selection.detachSelection();
|
|
var s = new Set();
|
|
for (var i of selection) {
|
|
s.add(i.__data__.id);
|
|
};
|
|
return s;
|
|
}
|
|
|
|
pathMouseDown(path, d) {
|
|
d3.event.stopPropagation();
|
|
this.state.selection.clear();
|
|
this.state.selection.add(path);
|
|
};
|
|
|
|
nodeMouseDown(node, d) {
|
|
d3.event.stopPropagation();
|
|
this.state.mouseDownNode = d;
|
|
}
|
|
|
|
nodeMouseUp(d3node, d) {
|
|
var graph = this,
|
|
state = graph.state,
|
|
consts = graph.consts;
|
|
|
|
var mouseDownNode = state.mouseDownNode;
|
|
|
|
if (!mouseDownNode) return;
|
|
|
|
if (state.justDragged) {
|
|
// dragged, not clicked
|
|
redetermineGraphBoundingBox(graph);
|
|
state.justDragged = false;
|
|
} else{
|
|
// clicked, not dragged
|
|
var extend = d3.event.shiftKey;
|
|
var selection = graph.state.selection;
|
|
if (!extend) {
|
|
selection.clear();
|
|
}
|
|
selection.select(d3node[0][0], true);
|
|
}
|
|
}
|
|
|
|
selectSourcePositions(start, end, selected) {
|
|
var graph = this;
|
|
var map = [];
|
|
var sel = graph.nodes.filter(function(n) {
|
|
var pos = (n.pos === undefined)
|
|
? -1
|
|
: n.getFunctionRelativeSourcePosition(graph);
|
|
if (pos >= start && pos < end) {
|
|
map[n.id] = true;
|
|
n.visible = true;
|
|
}
|
|
});
|
|
graph.updateGraphVisibility();
|
|
graph.visibleNodes.filter(function(n) { return map[n.id]; })
|
|
.each(function(n) {
|
|
var selection = graph.state.selection;
|
|
selection.select(d3.select(this), selected);
|
|
});
|
|
}
|
|
|
|
selectAllNodes(inEdges, filter) {
|
|
var graph = this;
|
|
if (!d3.event.shiftKey) {
|
|
graph.state.selection.clear();
|
|
}
|
|
graph.state.selection.select(graph.visibleNodes[0], true);
|
|
graph.updateGraphVisibility();
|
|
}
|
|
|
|
uploadAction(graph) {
|
|
document.getElementById("hidden-file-upload").click();
|
|
}
|
|
|
|
layoutAction(graph) {
|
|
graph.updateGraphVisibility();
|
|
graph.layoutGraph();
|
|
graph.updateGraphVisibility();
|
|
graph.viewWholeGraph();
|
|
}
|
|
|
|
showAllAction(graph) {
|
|
graph.nodes.filter(function(n) { n.visible = true; })
|
|
graph.edges.filter(function(e) { e.visible = true; })
|
|
graph.updateGraphVisibility();
|
|
graph.viewWholeGraph();
|
|
}
|
|
|
|
hideUnselectedAction(graph) {
|
|
var unselected = graph.visibleNodes.filter(function(n) {
|
|
return !this.classList.contains("selected");
|
|
});
|
|
unselected.each(function(n) {
|
|
n.visible = false;
|
|
});
|
|
graph.updateGraphVisibility();
|
|
}
|
|
|
|
hideSelectedAction(graph) {
|
|
var selected = graph.visibleNodes.filter(function(n) {
|
|
return this.classList.contains("selected");
|
|
});
|
|
selected.each(function(n) {
|
|
n.visible = false;
|
|
});
|
|
graph.state.selection.clear();
|
|
graph.updateGraphVisibility();
|
|
}
|
|
|
|
zoomSelectionAction(graph) {
|
|
graph.viewSelection();
|
|
}
|
|
|
|
toggleTypesAction(graph) {
|
|
graph.toggleTypes();
|
|
}
|
|
|
|
searchInputAction(graph) {
|
|
if (d3.event.keyCode == 13) {
|
|
graph.state.selection.clear();
|
|
var query = this.value;
|
|
window.sessionStorage.setItem("lastSearch", query);
|
|
|
|
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.opcode) != null);
|
|
};
|
|
if (d3.event.ctrlKey) {
|
|
graph.nodes.forEach(function(n, i) {
|
|
if (filterFunction(n)) {
|
|
n.visible = true;
|
|
}
|
|
});
|
|
graph.updateGraphVisibility();
|
|
}
|
|
var selected = graph.visibleNodes.each(function(n) {
|
|
if (filterFunction(n)) {
|
|
graph.state.selection.select(this, true);
|
|
}
|
|
});
|
|
graph.connectVisibleSelectedNodes();
|
|
graph.updateGraphVisibility();
|
|
this.blur();
|
|
graph.viewSelection();
|
|
}
|
|
d3.event.stopPropagation();
|
|
}
|
|
|
|
svgMouseDown() {
|
|
this.state.graphMouseDown = true;
|
|
}
|
|
|
|
svgMouseUp() {
|
|
var graph = this,
|
|
state = graph.state;
|
|
if (state.justScaleTransGraph) {
|
|
// Dragged
|
|
state.justScaleTransGraph = false;
|
|
} else {
|
|
// Clicked
|
|
if (state.mouseDownNode == null) {
|
|
graph.state.selection.clear();
|
|
}
|
|
}
|
|
state.mouseDownNode = null;
|
|
state.graphMouseDown = false;
|
|
}
|
|
|
|
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.selection, inEdges, filter);
|
|
if (frontier != undefined) {
|
|
if (select) {
|
|
if (!d3.event.shiftKey) {
|
|
state.selection.clear();
|
|
}
|
|
state.selection.select(frontier[0], 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 67:
|
|
// 'c'
|
|
showSelectionFrontierNodes(true,
|
|
(edge, index) => { return edge.type == 'control'; },
|
|
false);
|
|
break;
|
|
case 69:
|
|
// 'e'
|
|
showSelectionFrontierNodes(true,
|
|
(edge, index) => { return edge.type == 'effect'; },
|
|
false);
|
|
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 191:
|
|
// '/'
|
|
document.getElementById("search-input").focus();
|
|
document.getElementById("search-input").select();
|
|
break;
|
|
default:
|
|
eventHandled = false;
|
|
break;
|
|
}
|
|
if (eventHandled) {
|
|
d3.event.preventDefault();
|
|
}
|
|
if (!allowRepetition) {
|
|
state.lastKeyDown = d3.event.keyCode;
|
|
}
|
|
}
|
|
|
|
svgKeyUp() {
|
|
this.state.lastKeyDown = -1
|
|
};
|
|
|
|
layoutEdges() {
|
|
var graph = this;
|
|
graph.maxGraphX = graph.maxGraphNodeX;
|
|
this.visibleEdges.attr("d", function(edge){
|
|
return edge.generatePath(graph);
|
|
});
|
|
}
|
|
|
|
layoutGraph() {
|
|
layoutNodeGraph(this);
|
|
}
|
|
|
|
// call to propagate changes to graph
|
|
updateGraphVisibility() {
|
|
|
|
var graph = this,
|
|
state = graph.state;
|
|
|
|
var filteredEdges = graph.edges.filter(function(e) { return e.isVisible(); });
|
|
var visibleEdges = graph.visibleEdges.data(filteredEdges, function(edge) {
|
|
return edge.stringID();
|
|
});
|
|
|
|
// add new paths
|
|
visibleEdges.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("mousedown", function(d){
|
|
graph.pathMouseDown.call(graph, d3.select(this), d);
|
|
})
|
|
|
|
// Set the correct styles on all of the paths
|
|
visibleEdges.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" : "";
|
|
});
|
|
|
|
// remove old links
|
|
visibleEdges.exit().remove();
|
|
|
|
graph.visibleEdges = visibleEdges;
|
|
|
|
// update existing nodes
|
|
var filteredNodes = graph.nodes.filter(function(n) { return n.visible; });
|
|
graph.visibleNodes = graph.visibleNodes.data(filteredNodes, function(d) {
|
|
return d.id;
|
|
});
|
|
graph.visibleNodes.attr("transform", function(n){
|
|
return "translate(" + n.x + "," + n.y + ")";
|
|
}).select('rect').
|
|
attr(HEIGHT, function(d) { return graph.getNodeHeight(); });
|
|
|
|
// add new nodes
|
|
var newGs = graph.visibleNodes.enter()
|
|
.append("g");
|
|
|
|
newGs.classed("control", function(n) { return n.isControl(); })
|
|
.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(); })
|
|
.attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")";})
|
|
.on("mousedown", function(d){
|
|
graph.nodeMouseDown.call(graph, d3.select(this), d);
|
|
})
|
|
.on("mouseup", function(d){
|
|
graph.nodeMouseUp.call(graph, d3.select(this), d);
|
|
})
|
|
.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(); })
|
|
|
|
function appendInputAndOutputBubbles(g, d) {
|
|
for (var i = 0; i < d.inputs.length; ++i) {
|
|
var x = d.getInputX(i);
|
|
var y = -DEFAULT_NODE_BUBBLE_RADIUS / 2 - 4;
|
|
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("mousedown", 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() + DEFAULT_NODE_BUBBLE_RADIUS / 2 + 4;
|
|
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("mousedown", 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", DEFAULT_NODE_HEIGHT / 2 + 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", DEFAULT_NODE_HEIGHT / 2 + TYPE_HEIGHT + 5)
|
|
.append('tspan')
|
|
.text(function(l) {
|
|
return d.getDisplayType();
|
|
})
|
|
.append("title")
|
|
.text(function(l) {
|
|
return d.getType();
|
|
})
|
|
}
|
|
});
|
|
|
|
graph.visibleNodes.select('.type').each(function (d) {
|
|
this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden');
|
|
});
|
|
|
|
// remove old nodes
|
|
graph.visibleNodes.exit().remove();
|
|
|
|
graph.visibleBubbles = d3.selectAll('circle');
|
|
|
|
graph.updateInputAndOutputBubbles();
|
|
|
|
graph.layoutEdges();
|
|
|
|
graph.svg.style.height = '100%';
|
|
}
|
|
|
|
getVisibleTranslation(translate, scale) {
|
|
var graph = this;
|
|
var height = (graph.maxGraphY - graph.minGraphY + 2 * GRAPH_MARGIN) * scale;
|
|
var width = (graph.maxGraphX - graph.minGraphX + 2 * GRAPH_MARGIN) * scale;
|
|
|
|
var dimensions = this.getSvgViewDimensions();
|
|
|
|
var baseY = translate[1];
|
|
var minY = (graph.minGraphY - GRAPH_MARGIN) * scale;
|
|
var maxY = (graph.maxGraphY + GRAPH_MARGIN) * scale;
|
|
|
|
var adjustY = 0;
|
|
var adjustYCandidate = 0;
|
|
if ((maxY + baseY) < dimensions[1]) {
|
|
adjustYCandidate = dimensions[1] - (maxY + baseY);
|
|
if ((minY + baseY + adjustYCandidate) > 0) {
|
|
adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY;
|
|
} else {
|
|
adjustY = adjustYCandidate;
|
|
}
|
|
} else if (-baseY < minY) {
|
|
adjustYCandidate = -(baseY + minY);
|
|
if ((maxY + baseY + adjustYCandidate) < dimensions[1]) {
|
|
adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY;
|
|
} else {
|
|
adjustY = adjustYCandidate;
|
|
}
|
|
}
|
|
translate[1] += adjustY;
|
|
|
|
var baseX = translate[0];
|
|
var minX = (graph.minGraphX - GRAPH_MARGIN) * scale;
|
|
var maxX = (graph.maxGraphX + GRAPH_MARGIN) * scale;
|
|
|
|
var adjustX = 0;
|
|
var adjustXCandidate = 0;
|
|
if ((maxX + baseX) < dimensions[0]) {
|
|
adjustXCandidate = dimensions[0] - (maxX + baseX);
|
|
if ((minX + baseX + adjustXCandidate) > 0) {
|
|
adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX;
|
|
} else {
|
|
adjustX = adjustXCandidate;
|
|
}
|
|
} else if (-baseX < minX) {
|
|
adjustXCandidate = -(baseX + minX);
|
|
if ((maxX + baseX + adjustXCandidate) < dimensions[0]) {
|
|
adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX;
|
|
} else {
|
|
adjustX = adjustXCandidate;
|
|
}
|
|
}
|
|
translate[0] += adjustX;
|
|
return translate;
|
|
}
|
|
|
|
translateClipped(translate, scale, transition) {
|
|
var graph = this;
|
|
var graphNode = this.graphElement[0][0];
|
|
var translate = this.getVisibleTranslation(translate, scale);
|
|
if (transition) {
|
|
graphNode.classList.add('visible-transition');
|
|
clearTimeout(graph.transitionTimout);
|
|
graph.transitionTimout = setTimeout(function(){
|
|
graphNode.classList.remove('visible-transition');
|
|
}, 1000);
|
|
}
|
|
var translateString = "translate(" + translate[0] + "px," + translate[1] + "px) scale(" + scale + ")";
|
|
graphNode.style.transform = translateString;
|
|
graph.dragSvg.translate(translate);
|
|
graph.dragSvg.scale(scale);
|
|
}
|
|
|
|
zoomed(){
|
|
this.state.justScaleTransGraph = true;
|
|
var scale = this.dragSvg.scale();
|
|
this.translateClipped(d3.event.translate, scale);
|
|
}
|
|
|
|
|
|
getSvgViewDimensions() {
|
|
var canvasWidth = this.parentNode.clientWidth;
|
|
var documentElement = document.documentElement;
|
|
var canvasHeight = documentElement.clientHeight;
|
|
return [canvasWidth, canvasHeight];
|
|
}
|
|
|
|
|
|
minScale() {
|
|
var graph = this;
|
|
var dimensions = this.getSvgViewDimensions();
|
|
var width = graph.maxGraphX - graph.minGraphX;
|
|
var height = graph.maxGraphY - graph.minGraphY;
|
|
var minScale = dimensions[0] / (width + GRAPH_MARGIN * 2);
|
|
var minScaleYCandidate = dimensions[1] / (height + GRAPH_MARGIN * 2);
|
|
if (minScaleYCandidate < minScale) {
|
|
minScale = minScaleYCandidate;
|
|
}
|
|
this.dragSvg.scaleExtent([minScale, 1.5]);
|
|
return minScale;
|
|
}
|
|
|
|
fitGraphViewToWindow() {
|
|
this.svg.attr("height", document.documentElement.clientHeight + "px");
|
|
this.translateClipped(this.dragSvg.translate(), this.dragSvg.scale());
|
|
}
|
|
|
|
toggleTypes() {
|
|
var graph = this;
|
|
graph.state.showTypes = !graph.state.showTypes;
|
|
var element = document.getElementById('toggle-types');
|
|
if (graph.state.showTypes) {
|
|
element.classList.add('button-input-toggled');
|
|
} else {
|
|
element.classList.remove('button-input-toggled');
|
|
}
|
|
graph.updateGraphVisibility();
|
|
}
|
|
|
|
viewSelection() {
|
|
var graph = this;
|
|
var minX, maxX, minY, maxY;
|
|
var hasSelection = false;
|
|
graph.visibleNodes.each(function(n) {
|
|
if (this.classList.contains("selected")) {
|
|
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 + DEFAULT_NODE_HEIGHT) :
|
|
n.y + DEFAULT_NODE_HEIGHT;
|
|
}
|
|
});
|
|
if (hasSelection) {
|
|
graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60,
|
|
maxX + NODE_INPUT_WIDTH, maxY + 60,
|
|
true);
|
|
}
|
|
}
|
|
|
|
viewGraphRegion(minX, minY, maxX, maxY, transition) {
|
|
var graph = this;
|
|
var dimensions = this.getSvgViewDimensions();
|
|
var width = maxX - minX;
|
|
var height = maxY - minY;
|
|
var scale = Math.min(dimensions[0] / width, dimensions[1] / height);
|
|
scale = Math.min(1.5, scale);
|
|
scale = Math.max(graph.minScale(), scale);
|
|
var translation = [-minX*scale, -minY*scale];
|
|
translation = graph.getVisibleTranslation(translation, scale);
|
|
graph.translateClipped(translation, scale, transition);
|
|
}
|
|
|
|
viewWholeGraph() {
|
|
var graph = this;
|
|
var minScale = graph.minScale();
|
|
var translation = [0, 0];
|
|
translation = graph.getVisibleTranslation(translation, minScale);
|
|
graph.translateClipped(translation, minScale);
|
|
}
|
|
}
|