Created
September 24, 2013 05:41
-
-
Save davidjgraph/6680789 to your computer and use it in GitHub Desktop.
23.09.2013 Swimlane layout fix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* $Id: mxSwimlaneLayout.js,v 1.3 2013/09/23 14:11:22 david Exp $ | |
* Copyright (c) 2005-2012, JGraph Ltd | |
*/ | |
/** | |
* Class: mxSwimlaneLayout | |
* | |
* A hierarchical layout algorithm. | |
* | |
* Constructor: mxSwimlaneLayout | |
* | |
* Constructs a new hierarchical layout algorithm. | |
* | |
* Arguments: | |
* | |
* graph - Reference to the enclosing <mxGraph>. | |
* orientation - Optional constant that defines the orientation of this | |
* layout. | |
* deterministic - Optional boolean that specifies if this layout should be | |
* deterministic. Default is true. | |
*/ | |
function mxSwimlaneLayout(graph, orientation, deterministic) | |
{ | |
mxGraphLayout.call(this, graph); | |
this.orientation = (orientation != null) ? orientation : mxConstants.DIRECTION_NORTH; | |
this.deterministic = (deterministic != null) ? deterministic : true; | |
}; | |
/** | |
* Extends mxGraphLayout. | |
*/ | |
mxSwimlaneLayout.prototype = new mxGraphLayout(); | |
mxSwimlaneLayout.prototype.constructor = mxSwimlaneLayout; | |
/** | |
* Variable: roots | |
* | |
* Holds the array of <mxCell> that this layout contains. | |
*/ | |
mxSwimlaneLayout.prototype.roots = null; | |
/** | |
* Variable: swimlanes | |
* | |
* Holds the array of <mxCell> of the ordered swimlanes to lay out | |
*/ | |
mxSwimlaneLayout.prototype.swimlanes = null; | |
/** | |
* Variable: dummyVertices | |
* | |
* Holds an array of <mxCell> of dummy vertices inserted during the layout | |
* to pad out empty swimlanes | |
*/ | |
mxSwimlaneLayout.prototype.dummyVertices = null; | |
/** | |
* Variable: dummyVertexWidth | |
* | |
* The cell width of any dummy vertices inserted | |
*/ | |
mxSwimlaneLayout.prototype.dummyVertexWidth = 50; | |
/** | |
* Variable: resizeParent | |
* | |
* Specifies if the parent should be resized after the layout so that it | |
* contains all the child cells. Default is false. See also <parentBorder>. | |
*/ | |
mxSwimlaneLayout.prototype.resizeParent = false; | |
/** | |
* Variable: moveParent | |
* | |
* Specifies if the parent should be moved if <resizeParent> is enabled. | |
* Default is false. | |
*/ | |
mxSwimlaneLayout.prototype.moveParent = false; | |
/** | |
* Variable: parentBorder | |
* | |
* The border to be added around the children if the parent is to be | |
* resized using <resizeParent>. Default is 0. | |
*/ | |
mxSwimlaneLayout.prototype.parentBorder = 30; | |
/** | |
* Variable: intraCellSpacing | |
* | |
* The spacing buffer added between cells on the same layer. Default is 30. | |
*/ | |
mxSwimlaneLayout.prototype.intraCellSpacing = 30; | |
/** | |
* Variable: interRankCellSpacing | |
* | |
* The spacing buffer added between cell on adjacent layers. Default is 50. | |
*/ | |
mxSwimlaneLayout.prototype.interRankCellSpacing = 100; | |
/** | |
* Variable: interHierarchySpacing | |
* | |
* The spacing buffer between unconnected hierarchies. Default is 60. | |
*/ | |
mxSwimlaneLayout.prototype.interHierarchySpacing = 60; | |
/** | |
* Variable: parallelEdgeSpacing | |
* | |
* The distance between each parallel edge on each ranks for long edges | |
*/ | |
mxSwimlaneLayout.prototype.parallelEdgeSpacing = 10; | |
/** | |
* Variable: orientation | |
* | |
* The position of the root node(s) relative to the laid out graph in. | |
* Default is <mxConstants.DIRECTION_NORTH>. | |
*/ | |
mxSwimlaneLayout.prototype.orientation = mxConstants.DIRECTION_NORTH; | |
/** | |
* Variable: fineTuning | |
* | |
* Whether or not to perform local optimisations and iterate multiple times | |
* through the algorithm. Default is true. | |
*/ | |
mxSwimlaneLayout.prototype.fineTuning = true; | |
/** | |
* | |
* Variable: tightenToSource | |
* | |
* Whether or not to tighten the assigned ranks of vertices up towards | |
* the source cells. | |
*/ | |
mxSwimlaneLayout.prototype.tightenToSource = true; | |
/** | |
* Variable: disableEdgeStyle | |
* | |
* Specifies if the STYLE_NOEDGESTYLE flag should be set on edges that are | |
* modified by the result. Default is true. | |
*/ | |
mxSwimlaneLayout.prototype.disableEdgeStyle = true; | |
/** | |
* Variable: traverseAncestors | |
* | |
* Whether or not to drill into child cells and layout in reverse | |
* group order. This also cause the layout to navigate edges whose | |
* terminal vertices * have different parents but are in the same | |
* ancestry chain | |
*/ | |
mxSwimlaneLayout.prototype.traverseAncestors = true; | |
/** | |
* Variable: model | |
* | |
* The internal <mxSwimlaneModel> formed of the layout. | |
*/ | |
mxSwimlaneLayout.prototype.model = null; | |
/** | |
* Variable: edgesSet | |
* | |
* A cache of edges whose source terminal is the key | |
*/ | |
mxSwimlaneLayout.prototype.edgesCache = null; | |
/** | |
* Function: getModel | |
* | |
* Returns the internal <mxSwimlaneModel> for this layout algorithm. | |
*/ | |
mxSwimlaneLayout.prototype.getModel = function() | |
{ | |
return this.model; | |
}; | |
/** | |
* Function: execute | |
* | |
* Executes the layout for the children of the specified parent. | |
* | |
* Parameters: | |
* | |
* parent - Parent <mxCell> that contains the children to be laid out. | |
* swimlanes - Ordered array of swimlanes to be laid out | |
*/ | |
mxSwimlaneLayout.prototype.execute = function(parent, swimlanes) | |
{ | |
this.parent = parent; | |
var model = this.graph.model; | |
this.edgesCache = new Object(); | |
// If the roots are set and the parent is set, only | |
// use the roots that are some dependent of the that | |
// parent. | |
// If just the root are set, use them as-is | |
// If just the parent is set use it's immediate | |
// children as the initial set | |
if (swimlanes == null || swimlanes.length < 1) | |
{ | |
// TODO indicate the problem | |
return; | |
} | |
if (parent == null) | |
{ | |
parent = model.getParent(swimlanes[0]); | |
} | |
this.swimlanes = swimlanes; | |
this.dummyVertices = []; | |
// Check the swimlanes all have vertices | |
// in them | |
for (var i = 0; i < swimlanes.length; i++) | |
{ | |
var children = this.graph.getChildCells(swimlanes[i]); | |
if (children == null || children.length == 0) | |
{ | |
var vertex = this.graph.insertVertex(swimlanes[i], null, null, 0, 0, this.dummyVertexWidth, 0); | |
this.dummyVertices.push(vertex); | |
} | |
} | |
model.beginUpdate(); | |
try | |
{ | |
this.run(parent); | |
if (this.resizeParent && | |
!this.graph.isCellCollapsed(parent)) | |
{ | |
this.updateGroupBounds(); | |
} | |
this.graph.removeCells(this.dummyVertices); | |
} | |
finally | |
{ | |
model.endUpdate(); | |
} | |
}; | |
/** | |
* Function: updateGroupBounds | |
* | |
* Updates the bounds of the given array of groups so that it includes | |
* all child vertices. | |
* | |
*/ | |
mxSwimlaneLayout.prototype.updateGroupBounds = function() | |
{ | |
// Get all vertices and edge in the layout | |
var cells = []; | |
var model = this.model; | |
for (var key in model.edgeMapper) | |
{ | |
var edge = model.edgeMapper[key]; | |
for (var i = 0; i < edge.edges.length; i++) | |
{ | |
cells.push(edge.edges[i]); | |
} | |
} | |
var layoutBounds = this.graph.getBoundingBoxFromGeometry(cells, true); | |
var childBounds = []; | |
for (var i = 0; i < this.swimlanes.length; i++) | |
{ | |
var lane = this.swimlanes[i]; | |
var geo = this.graph.getCellGeometry(lane); | |
if (geo != null) | |
{ | |
var children = this.graph.getChildCells(lane); | |
var size = (this.graph.isSwimlane(lane)) ? | |
this.graph.getStartSize(lane) : new mxRectangle(); | |
var bounds = this.graph.getBoundingBoxFromGeometry(children); | |
childBounds[i] = bounds; | |
var childrenY = bounds.y + geo.y - size.height - this.parentBorder; | |
var maxChildrenY = bounds.y + geo.y + bounds.height; | |
if (layoutBounds == null) | |
{ | |
layoutBounds = new mxRectangle(0, childrenY, 0, maxChildrenY - childrenY); | |
} | |
else | |
{ | |
layoutBounds.y = Math.min(layoutBounds.y, childrenY); | |
var maxY = Math.max(layoutBounds.y + layoutBounds.height, maxChildrenY); | |
layoutBounds.height = maxY - layoutBounds.y; | |
} | |
} | |
} | |
for (var i = 0; i < this.swimlanes.length; i++) | |
{ | |
var lane = this.swimlanes[i]; | |
var geo = this.graph.getCellGeometry(lane); | |
if (geo != null) | |
{ | |
var children = this.graph.getChildCells(lane); | |
var size = (this.graph.isSwimlane(lane)) ? | |
this.graph.getStartSize(lane) : new mxRectangle(); | |
var newGeo = geo.clone(); | |
var leftGroupBorder = (i == 0) ? this.parentBorder : this.interRankCellSpacing/2; | |
newGeo.x += childBounds[i].x - size.width - leftGroupBorder; | |
newGeo.y = newGeo.y + layoutBounds.y - geo.y - this.parentBorder; | |
newGeo.width = childBounds[i].width + size.width + this.interRankCellSpacing/2 + leftGroupBorder; | |
newGeo.height = layoutBounds.height + size.height + 2 * this.parentBorder; | |
this.graph.model.setGeometry(lane, newGeo); | |
this.graph.moveCells(children, -childBounds[i].x + size.width + leftGroupBorder, | |
geo.y - layoutBounds.y + this.parentBorder); | |
} | |
} | |
}; | |
/** | |
* Function: findRoots | |
* | |
* Returns all visible children in the given parent which do not have | |
* incoming edges. If the result is empty then the children with the | |
* maximum difference between incoming and outgoing edges are returned. | |
* This takes into account edges that are being promoted to the given | |
* root due to invisible children or collapsed cells. | |
* | |
* Parameters: | |
* | |
* parent - <mxCell> whose children should be checked. | |
* vertices - array of vertices to limit search to | |
*/ | |
mxSwimlaneLayout.prototype.findRoots = function(parent, vertices) | |
{ | |
var roots = []; | |
if (parent != null && vertices != null) | |
{ | |
var model = this.graph.model; | |
var best = null; | |
var maxDiff = -100000; | |
for (var i in vertices) | |
{ | |
var cell = vertices[i]; | |
if (cell != null && model.isVertex(cell) && this.graph.isCellVisible(cell) && model.isAncestor(parent, cell)) | |
{ | |
var conns = this.getEdges(cell); | |
var fanOut = 0; | |
var fanIn = 0; | |
for (var k = 0; k < conns.length; k++) | |
{ | |
var src = this.getVisibleTerminal(conns[k], true); | |
if (src == cell) | |
{ | |
// Only count connection within this swimlane | |
var other = this.getVisibleTerminal(conns[k], false); | |
if (model.isAncestor(parent, other)) | |
{ | |
fanOut++; | |
} | |
} | |
else if (model.isAncestor(parent, src)) | |
{ | |
fanIn++; | |
} | |
} | |
if (fanIn == 0 && fanOut > 0) | |
{ | |
roots.push(cell); | |
} | |
var diff = fanOut - fanIn; | |
if (diff > maxDiff) | |
{ | |
maxDiff = diff; | |
best = cell; | |
} | |
} | |
} | |
if (roots.length == 0 && best != null) | |
{ | |
roots.push(best); | |
} | |
} | |
return roots; | |
}; | |
/** | |
* Function: getEdges | |
* | |
* Returns the connected edges for the given cell. | |
* | |
* Parameters: | |
* | |
* cell - <mxCell> whose edges should be returned. | |
*/ | |
mxSwimlaneLayout.prototype.getEdges = function(cell) | |
{ | |
var cellID = mxCellPath.create(cell); | |
if (this.edgesCache[cellID] != null) | |
{ | |
return this.edgesCache[cellID]; | |
} | |
var model = this.graph.model; | |
var edges = []; | |
var isCollapsed = this.graph.isCellCollapsed(cell); | |
var childCount = model.getChildCount(cell); | |
for (var i = 0; i < childCount; i++) | |
{ | |
var child = model.getChildAt(cell, i); | |
if (this.isPort(child)) | |
{ | |
edges = edges.concat(model.getEdges(child, true, true)); | |
} | |
else if (isCollapsed || !this.graph.isCellVisible(child)) | |
{ | |
edges = edges.concat(model.getEdges(child, true, true)); | |
} | |
} | |
edges = edges.concat(model.getEdges(cell, true, true)); | |
var result = []; | |
for (var i = 0; i < edges.length; i++) | |
{ | |
var source = this.getVisibleTerminal(edges[i], true); | |
var target = this.getVisibleTerminal(edges[i], false); | |
if ((source == target) || ((source != target) && ((target == cell && (this.parent == null || this.graph.isValidAncestor(source, this.parent, this.traverseAncestors))) || | |
(source == cell && (this.parent == null || | |
this.graph.isValidAncestor(target, this.parent, this.traverseAncestors)))))) | |
{ | |
result.push(edges[i]); | |
} | |
} | |
this.edgesCache[cellID] = result; | |
return result; | |
}; | |
/** | |
* Function: getVisibleTerminal | |
* | |
* Helper function to return visible terminal for edge allowing for ports | |
* | |
* Parameters: | |
* | |
* edge - <mxCell> whose edges should be returned. | |
* source - Boolean that specifies whether the source or target terminal is to be returned | |
*/ | |
mxSwimlaneLayout.prototype.getVisibleTerminal = function(edge, source) | |
{ | |
var state = this.graph.view.getState(edge); | |
var terminal = (state != null) ? state.getVisibleTerminal(source) : this.graph.view.getVisibleTerminal(edge, source); | |
if (this.isPort(terminal)) | |
{ | |
terminal = this.graph.model.getParent(terminal); | |
} | |
return terminal; | |
}; | |
/** | |
* Function: run | |
* | |
* The API method used to exercise the layout upon the graph description | |
* and produce a separate description of the vertex position and edge | |
* routing changes made. It runs each stage of the layout that has been | |
* created. | |
*/ | |
mxSwimlaneLayout.prototype.run = function(parent) | |
{ | |
// Separate out unconnected hierarchies | |
var hierarchyVertices = []; | |
var allVertexSet = []; | |
if (this.swimlanes != null && this.swimlanes.length > 0 && parent != null) | |
{ | |
var filledVertexSet = Object(); | |
for (var i = 0; i < this.swimlanes.length; i++) | |
{ | |
this.filterDescendants(this.swimlanes[i], filledVertexSet); | |
} | |
this.roots = []; | |
var filledVertexSetEmpty = true; | |
// Poor man's isSetEmpty | |
for (var key in filledVertexSet) | |
{ | |
if (filledVertexSet[key] != null) | |
{ | |
filledVertexSetEmpty = false; | |
break; | |
} | |
} | |
// Only test for candidates in each swimlane in order | |
var laneCounter = 0; | |
while (!filledVertexSetEmpty && laneCounter < this.swimlanes.length) | |
{ | |
var candidateRoots = this.findRoots(this.swimlanes[laneCounter], filledVertexSet); | |
if (candidateRoots.length == 0) | |
{ | |
laneCounter++; | |
continue; | |
} | |
// If the candidate root is an unconnected group cell, remove it from | |
// the layout. We may need a custom set that holds such groups and forces | |
// them to be processed for resizing and/or moving. | |
for (var i = 0; i < candidateRoots.length; i++) | |
{ | |
var vertexSet = Object(); | |
hierarchyVertices.push(vertexSet); | |
this.traverse(candidateRoots[i], true, null, allVertexSet, vertexSet, | |
hierarchyVertices, filledVertexSet, laneCounter); | |
} | |
for (var i = 0; i < candidateRoots.length; i++) | |
{ | |
this.roots.push(candidateRoots[i]); | |
} | |
filledVertexSetEmpty = true; | |
// Poor man's isSetEmpty | |
for (var key in filledVertexSet) | |
{ | |
if (filledVertexSet[key] != null) | |
{ | |
filledVertexSetEmpty = false; | |
break; | |
} | |
} | |
} | |
} | |
else | |
{ | |
// Find vertex set as directed traversal from roots | |
for (var i = 0; i < this.roots.length; i++) | |
{ | |
var vertexSet = Object(); | |
hierarchyVertices.push(vertexSet); | |
this.traverse(this.roots[i], true, null, allVertexSet, vertexSet, | |
hierarchyVertices, null); | |
} | |
} | |
var tmp = []; | |
for (var key in allVertexSet) | |
{ | |
tmp.push(allVertexSet[key]); | |
} | |
this.model = new mxSwimlaneModel(this, tmp, this.roots, | |
parent, this.tightenToSource); | |
this.cycleStage(parent); | |
this.layeringStage(); | |
this.crossingStage(parent); | |
initialX = this.placementStage(0, parent); | |
}; | |
/** | |
* Function: filterDescendants | |
* | |
* Creates an array of descendant cells | |
*/ | |
mxSwimlaneLayout.prototype.filterDescendants = function(cell, result) | |
{ | |
var model = this.graph.model; | |
if (model.isVertex(cell) && cell != this.parent && model.getParent(cell) != this.parent && this.graph.isCellVisible(cell)) | |
{ | |
result[mxCellPath.create(cell)] = cell; | |
} | |
if (this.traverseAncestors || cell == this.parent | |
&& this.graph.isCellVisible(cell)) | |
{ | |
var childCount = model.getChildCount(cell); | |
for (var i = 0; i < childCount; i++) | |
{ | |
var child = model.getChildAt(cell, i); | |
// Ignore ports in the layout vertex list, they are dealt with | |
// in the traversal mechanisms | |
if (!this.isPort(child)) | |
{ | |
this.filterDescendants(child, result); | |
} | |
} | |
} | |
}; | |
/** | |
* Function: isPort | |
* | |
* Returns true if the given cell is a "port", that is, when connecting to | |
* it, its parent is the connecting vertex in terms of graph traversal | |
* | |
* Parameters: | |
* | |
* cell - <mxCell> that represents the port. | |
*/ | |
mxSwimlaneLayout.prototype.isPort = function(cell) | |
{ | |
if (cell.geometry.relative) | |
{ | |
return true; | |
} | |
return false; | |
}; | |
/** | |
* Function: getEdgesBetween | |
* | |
* Returns the edges between the given source and target. This takes into | |
* account collapsed and invisible cells and ports. | |
* | |
* Parameters: | |
* | |
* source - | |
* target - | |
* directed - | |
*/ | |
mxSwimlaneLayout.prototype.getEdgesBetween = function(source, target, directed) | |
{ | |
directed = (directed != null) ? directed : false; | |
var edges = this.getEdges(source); | |
var result = []; | |
// Checks if the edge is connected to the correct | |
// cell and returns the first match | |
for (var i = 0; i < edges.length; i++) | |
{ | |
var src = this.getVisibleTerminal(edges[i], true); | |
var trg = this.getVisibleTerminal(edges[i], false); | |
if ((src == source && trg == target) || (!directed && src == target && trg == source)) | |
{ | |
result.push(edges[i]); | |
} | |
} | |
return result; | |
}; | |
/** | |
* Traverses the (directed) graph invoking the given function for each | |
* visited vertex and edge. The function is invoked with the current vertex | |
* and the incoming edge as a parameter. This implementation makes sure | |
* each vertex is only visited once. The function may return false if the | |
* traversal should stop at the given vertex. | |
* | |
* Parameters: | |
* | |
* vertex - <mxCell> that represents the vertex where the traversal starts. | |
* directed - boolean indicating if edges should only be traversed | |
* from source to target. Default is true. | |
* edge - Optional <mxCell> that represents the incoming edge. This is | |
* null for the first step of the traversal. | |
* allVertices - Array of cell paths for the visited cells. | |
* swimlaneIndex - the laid out order index of the swimlane vertex is contained in | |
*/ | |
mxSwimlaneLayout.prototype.traverse = function(vertex, directed, edge, allVertices, currentComp, | |
hierarchyVertices, filledVertexSet, swimlaneIndex) | |
{ | |
if (vertex != null && allVertices != null) | |
{ | |
// Has this vertex been seen before in any traversal | |
// And if the filled vertex set is populated, only | |
// process vertices in that it contains | |
var vertexID = mxCellPath.create(vertex); | |
if ((allVertices[vertexID] == null) | |
&& (filledVertexSet == null ? true : filledVertexSet[vertexID] != null)) | |
{ | |
if (currentComp[vertexID] == null) | |
{ | |
currentComp[vertexID] = vertex; | |
} | |
if (allVertices[vertexID] == null) | |
{ | |
allVertices[vertexID] = vertex; | |
} | |
if (filledVertexSet !== null) | |
{ | |
delete filledVertexSet[vertexID]; | |
} | |
var edges = this.getEdges(vertex); | |
var model = this.graph.model; | |
for (var i = 0; i < edges.length; i++) | |
{ | |
var otherVertex = this.getVisibleTerminal(edges[i], true); | |
var isSource = otherVertex == vertex; | |
if (isSource) | |
{ | |
otherVertex = this.getVisibleTerminal(edges[i], false); | |
} | |
var otherIndex = 0; | |
// Get the swimlane index of the other terminal | |
for (var otherIndex = 0; otherIndex < this.swimlanes.length; otherIndex++) | |
{ | |
if (model.isAncestor(this.swimlanes[otherIndex], otherVertex)) | |
{ | |
break; | |
} | |
} | |
if (otherIndex >= this.swimlanes.length) | |
{ | |
continue; | |
} | |
// Traverse if the other vertex is within the same swimlane as | |
// as the current vertex, or if the swimlane index of the other | |
// vertex is greater than that of this vertex | |
if ((otherIndex > swimlaneIndex) || | |
((!directed || isSource) && otherIndex == swimlaneIndex)) | |
{ | |
currentComp = this.traverse(otherVertex, directed, edges[i], allVertices, | |
currentComp, hierarchyVertices, | |
filledVertexSet, otherIndex); | |
} | |
} | |
} | |
else | |
{ | |
if (currentComp[vertexID] == null) | |
{ | |
// We've seen this vertex before, but not in the current component | |
// This component and the one it's in need to be merged | |
for (var i = 0; i < hierarchyVertices.length; i++) | |
{ | |
var comp = hierarchyVertices[i]; | |
if (comp[vertexID] != null) | |
{ | |
for (var key in currentComp) | |
{ | |
comp[key] = currentComp[key]; | |
} | |
// Remove the current component from the hierarchy set | |
hierarchyVertices.pop(); | |
return comp; | |
} | |
} | |
} | |
} | |
} | |
return currentComp; | |
}; | |
/** | |
* Function: cycleStage | |
* | |
* Executes the cycle stage using mxMinimumCycleRemover. | |
*/ | |
mxSwimlaneLayout.prototype.cycleStage = function(parent) | |
{ | |
var cycleStage = new mxSwimlaneOrdering(this); | |
cycleStage.execute(parent); | |
}; | |
/** | |
* Function: layeringStage | |
* | |
* Implements first stage of a Sugiyama layout. | |
*/ | |
mxSwimlaneLayout.prototype.layeringStage = function() | |
{ | |
this.model.initialRank(); | |
this.model.fixRanks(); | |
}; | |
/** | |
* Function: crossingStage | |
* | |
* Executes the crossing stage using mxMedianHybridCrossingReduction. | |
*/ | |
mxSwimlaneLayout.prototype.crossingStage = function(parent) | |
{ | |
var crossingStage = new mxMedianHybridCrossingReduction(this); | |
crossingStage.execute(parent); | |
}; | |
/** | |
* Function: placementStage | |
* | |
* Executes the placement stage using mxCoordinateAssignment. | |
*/ | |
mxSwimlaneLayout.prototype.placementStage = function(initialX, parent) | |
{ | |
var placementStage = new mxCoordinateAssignment(this, this.intraCellSpacing, | |
this.interRankCellSpacing, this.orientation, initialX, | |
this.parallelEdgeSpacing); | |
placementStage.fineTuning = this.fineTuning; | |
placementStage.execute(parent); | |
return placementStage.limitX + this.interHierarchySpacing; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment