2016-05-19 08:17:29 +00:00
|
|
|
// 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);
|
|
|
|
})
|
|
|
|
|
2016-07-25 14:23:45 +00:00
|
|
|
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));
|
2016-05-19 08:17:29 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-22 10:11:02 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-19 08:17:29 +00:00
|
|
|
dragmove(d) {
|
|
|
|
var graph = this;
|
|
|
|
d.x += d3.event.dx;
|
|
|
|
d.y += d3.event.dy;
|
|
|
|
graph.updateGraphVisibility();
|
|
|
|
}
|
|
|
|
|
|
|
|
initializeContent(data, rememberedSelection) {
|
2016-06-13 13:21:23 +00:00
|
|
|
this.createGraph(data, rememberedSelection);
|
2016-05-19 08:17:29 +00:00
|
|
|
if (rememberedSelection != null) {
|
|
|
|
this.attachSelection(rememberedSelection);
|
2016-06-13 13:21:23 +00:00
|
|
|
this.connectVisibleSelectedNodes();
|
2016-05-19 08:17:29 +00:00
|
|
|
}
|
|
|
|
this.updateGraphVisibility();
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteContent() {
|
|
|
|
if (this.visibleNodes) {
|
|
|
|
this.nodes = [];
|
|
|
|
this.edges = [];
|
|
|
|
this.nodeMap = [];
|
|
|
|
this.updateGraphVisibility();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-06-13 13:21:23 +00:00
|
|
|
createGraph(data, initiallyVisibileIds) {
|
2016-05-19 08:17:29 +00:00
|
|
|
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);
|
2016-06-13 13:21:23 +00:00
|
|
|
if (initiallyVisibileIds != undefined) {
|
|
|
|
if (initiallyVisibileIds.has(n.id)) {
|
|
|
|
n.visible = true;
|
|
|
|
}
|
|
|
|
}
|
2016-05-19 08:17:29 +00:00
|
|
|
});
|
|
|
|
g.fitGraphViewToWindow();
|
|
|
|
g.updateGraphVisibility();
|
|
|
|
g.layoutGraph();
|
|
|
|
g.updateGraphVisibility();
|
|
|
|
g.viewWholeGraph();
|
|
|
|
}
|
|
|
|
|
2016-06-13 13:21:23 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-05-19 08:17:29 +00:00
|
|
|
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();
|
2016-06-13 13:21:23 +00:00
|
|
|
var s = new Set();
|
2016-05-19 08:17:29 +00:00
|
|
|
for (var i of selection) {
|
2016-06-13 13:21:23 +00:00
|
|
|
s.add(i.__data__.id);
|
2016-05-19 08:17:29 +00:00
|
|
|
};
|
2016-06-13 13:21:23 +00:00
|
|
|
return s;
|
2016-05-19 08:17:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2016-07-25 09:49:52 +00:00
|
|
|
if (state.justDragged) {
|
|
|
|
// dragged, not clicked
|
2016-07-25 14:19:25 +00:00
|
|
|
redetermineGraphBoundingBox(graph);
|
2016-07-25 09:49:52 +00:00
|
|
|
state.justDragged = false;
|
2016-05-19 08:17:29 +00:00
|
|
|
} else{
|
2016-07-25 09:49:52 +00:00
|
|
|
// clicked, not dragged
|
|
|
|
var extend = d3.event.shiftKey;
|
|
|
|
var selection = graph.state.selection;
|
|
|
|
if (!extend) {
|
|
|
|
selection.clear();
|
2016-05-19 08:17:29 +00:00
|
|
|
}
|
2016-07-25 09:49:52 +00:00
|
|
|
selection.select(d3node[0][0], true);
|
2016-05-19 08:17:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-06-13 13:21:23 +00:00
|
|
|
selectAllNodes(inEdges, filter) {
|
|
|
|
var graph = this;
|
|
|
|
if (!d3.event.shiftKey) {
|
|
|
|
graph.state.selection.clear();
|
|
|
|
}
|
2016-06-22 10:11:02 +00:00
|
|
|
graph.state.selection.select(graph.visibleNodes[0], true);
|
2016-06-13 13:21:23 +00:00
|
|
|
graph.updateGraphVisibility();
|
|
|
|
}
|
|
|
|
|
2016-07-25 14:23:45 +00:00
|
|
|
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 reg = new RegExp(this.value);
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2016-05-19 08:17:29 +00:00
|
|
|
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;
|
|
|
|
|
2016-06-22 10:11:02 +00:00
|
|
|
var showSelectionFrontierNodes = function(inEdges, filter, select) {
|
|
|
|
var frontier = graph.getNodeFrontier(state.selection.selection, inEdges, filter);
|
2016-06-13 13:21:23 +00:00
|
|
|
if (frontier != undefined) {
|
2016-06-22 10:11:02 +00:00
|
|
|
if (select) {
|
2016-06-13 13:21:23 +00:00
|
|
|
if (!d3.event.shiftKey) {
|
|
|
|
state.selection.clear();
|
|
|
|
}
|
2016-06-22 10:11:02 +00:00
|
|
|
state.selection.select(frontier[0], true);
|
2016-06-13 13:21:23 +00:00
|
|
|
}
|
|
|
|
graph.updateGraphVisibility();
|
|
|
|
}
|
|
|
|
allowRepetition = false;
|
2016-05-19 08:17:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var allowRepetition = true;
|
2016-06-13 16:33:41 +00:00
|
|
|
var eventHandled = true; // unless the below switch defaults
|
2016-05-19 08:17:29 +00:00
|
|
|
switch(d3.event.keyCode) {
|
2016-06-13 13:21:23 +00:00
|
|
|
case 49:
|
|
|
|
case 50:
|
|
|
|
case 51:
|
|
|
|
case 52:
|
|
|
|
case 53:
|
|
|
|
case 54:
|
|
|
|
case 55:
|
|
|
|
case 56:
|
|
|
|
case 57:
|
|
|
|
// '1'-'9'
|
2016-06-22 10:11:02 +00:00
|
|
|
showSelectionFrontierNodes(true,
|
|
|
|
(edge, index) => { return index == (d3.event.keyCode - 49); },
|
|
|
|
false);
|
2016-06-13 13:21:23 +00:00
|
|
|
break;
|
|
|
|
case 67:
|
|
|
|
// 'c'
|
2016-06-22 10:11:02 +00:00
|
|
|
showSelectionFrontierNodes(true,
|
|
|
|
(edge, index) => { return edge.type == 'control'; },
|
|
|
|
false);
|
2016-06-13 13:21:23 +00:00
|
|
|
break;
|
|
|
|
case 69:
|
|
|
|
// 'e'
|
2016-06-22 10:11:02 +00:00
|
|
|
showSelectionFrontierNodes(true,
|
|
|
|
(edge, index) => { return edge.type == 'effect'; },
|
|
|
|
false);
|
2016-06-13 13:21:23 +00:00
|
|
|
break;
|
|
|
|
case 79:
|
|
|
|
// 'o'
|
2016-06-22 10:11:02 +00:00
|
|
|
showSelectionFrontierNodes(false, undefined, false);
|
2016-06-13 13:21:23 +00:00
|
|
|
break;
|
|
|
|
case 73:
|
|
|
|
// 'i'
|
2016-06-22 10:11:02 +00:00
|
|
|
showSelectionFrontierNodes(true, undefined, false);
|
2016-06-13 13:21:23 +00:00
|
|
|
break;
|
|
|
|
case 65:
|
|
|
|
// 'a'
|
|
|
|
graph.selectAllNodes();
|
|
|
|
allowRepetition = false;
|
|
|
|
break;
|
2016-05-19 08:17:29 +00:00
|
|
|
case 38:
|
|
|
|
case 40: {
|
2016-06-22 10:11:02 +00:00
|
|
|
showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true);
|
2016-05-19 08:17:29 +00:00
|
|
|
break;
|
|
|
|
}
|
2016-06-13 16:33:41 +00:00
|
|
|
default:
|
|
|
|
eventHandled = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (eventHandled) {
|
|
|
|
d3.event.preventDefault();
|
2016-05-19 08:17:29 +00:00
|
|
|
}
|
|
|
|
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) {
|
[turbolizer] Show a label with a shorter parameter for some opcodes.
With this patch, every node in turbo-*.json has an opcode, a title, and
a label. The label field is new; the opcode and title were already
there. The title is for the mouseover text. The label is what will be
displayed in the graph view, unless it's too long, in which case only
the opcode will be displayed. (This is similar to the preexisting
behaviour of putting titles in labels, except that the titles were
rarely short enough to fit in a label.)
With this patch, the labels generated are in practice the same as the
titles we had before, except for LoadField and StoreField, which will be
rendered as LoadField[[+432]] and StoreField[[+432]] (if 432 was the
offset).
This diff adds an overloadable method
virtual void Operator1<T>::PrintParameter(ostream&, PrintVerbosity)
for each type T to Operator1. Its default implementation just uses
operator<<(ostream&, T const&) and adds square brackets around it, but
it is overridden for FieldAccess to print "[+432]" in the example case.
BUG=
R=jarin,danno
Review-Url: https://codereview.chromium.org/2093013002
Cr-Commit-Position: refs/heads/master@{#37795}
2016-07-15 12:05:54 +00:00
|
|
|
return d.getTitle();
|
2016-05-19 08:17:29 +00:00
|
|
|
})
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|