8345041: IGV: Free Placement Mode in IGV Layout

Reviewed-by: chagedorn, epeter, rcastanedalo
This commit is contained in:
Tobias Holenstein 2025-01-07 14:30:05 +00:00
parent 8b22517cb0
commit e5f0c19084
12 changed files with 670 additions and 28 deletions

View File

@ -0,0 +1,384 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
package com.sun.hotspot.igv.hierarchicallayout;
import static com.sun.hotspot.igv.hierarchicallayout.LayoutNode.LAYOUT_NODE_DEGREE_COMPARATOR;
import com.sun.hotspot.igv.layout.Link;
import com.sun.hotspot.igv.layout.Vertex;
import java.awt.Point;
import java.util.*;
public class FreeInteractiveLayoutManager extends LayoutManager implements LayoutMover {
private boolean cutEdges = false;
private static final int LINE_OFFSET = 10;
private Map<Vertex, LayoutNode> layoutNodes;
private LayoutGraph prevGraph;
private final Random random = new Random(42);
// Constants for offsets and displacements
private static final int MAX_OFFSET_AROUND_NEIGHBOR = 200; // Max offset for random positioning around a neighbor
private static final int MAX_OFFSET_AROUND_ORIGIN = 200; // Max offset for random positioning around origin
private static final int DISPLACEMENT_RANGE_BARYCENTER = 100; // Displacement range for barycenter calculation
private static final int DISPLACEMENT_RANGE_SINGLE = 200;
// Create a comparator to sort nodes by the number of unassigned neighbors
private final Comparator<LayoutNode> LeastUnassignedNeighborsComparator = Comparator.comparingInt(node -> {
Vertex vertex = node.getVertex();
int unassignedNeighbors = 0;
for (Vertex neighborVertex : prevGraph.getNeighborVertices(vertex)) {
if (!layoutNodes.containsKey(neighborVertex)) {
unassignedNeighbors++;
}
}
return unassignedNeighbors;
});
public FreeInteractiveLayoutManager() {
this.cutEdges = false;
this.layoutNodes = new HashMap<>();
this.prevGraph = null;
}
@Override
public void moveLink(Point linkPos, int shiftX) {}
@Override
public void moveVertices(Set<? extends Vertex> movedVertices) {
for (Vertex v : movedVertices) {
moveVertex(v);
}
}
@Override
public void moveVertex(Vertex vertex) {
assert prevGraph.containsVertex(vertex);
LayoutNode layoutNode = layoutNodes.get(vertex);
layoutNode.setX(vertex.getPosition().x);
layoutNode.setY(vertex.getPosition().y);
for (Link link : prevGraph.getAllLinks(vertex)) {
setLinkControlPoints(link);
}
}
@Override
public boolean isFreeForm() {
return true;
}
public void setCutEdges(boolean enable) {
this.cutEdges = enable;
}
@Override
public void doLayout(LayoutGraph graph) {
prevGraph = graph;
if (layoutNodes.isEmpty()) {
HierarchicalLayoutManager manager = new HierarchicalLayoutManager();
manager.doLayout(graph);
for (LayoutNode node : graph.getLayoutNodes()) {
node.initSize();
layoutNodes.put(node.getVertex(), node);
}
graph.clearLayout();
} else {
// add new vertices to layoutNodes, x/y from barycenter
List<LayoutNode> newLayoutNodes = new ArrayList<>();
// Set up layout nodes for each vertex
for (Vertex vertex : prevGraph.getVertices()) {
if (!layoutNodes.containsKey(vertex)) {
LayoutNode addedNode = new LayoutNode(vertex);
addedNode.initSize();
newLayoutNodes.add(addedNode);
}
}
positionNewLayoutNodes(newLayoutNodes);
}
// Write back vertices
for (Vertex vertex : prevGraph.getVertices()) {
LayoutNode layoutNode = layoutNodes.get(vertex);
layoutNode.setVertex(vertex);
vertex.setPosition(new Point(layoutNode.getLeft(), layoutNode.getTop()));
}
// Write back links
for (Link link : prevGraph.getLinks()) {
setLinkControlPoints(link);
}
}
public void positionNewLayoutNodes(List<LayoutNode> newLayoutNodes) {
// First pass: Initial positioning based on unassigned neighbors
newLayoutNodes.sort(LeastUnassignedNeighborsComparator);
for (LayoutNode node : newLayoutNodes) {
Vertex vertex = node.getVertex();
// Gather assigned neighbors
List<LayoutNode> assignedNeighbors = new ArrayList<>();
for (Vertex neighborVertex : prevGraph.getNeighborVertices(vertex)) {
if (layoutNodes.containsKey(neighborVertex)) {
assignedNeighbors.add(layoutNodes.get(neighborVertex));
}
}
if (!assignedNeighbors.isEmpty()) {
if (assignedNeighbors.size() == 1) {
// Single neighbor: position around the neighbor
setPositionAroundSingleNode(node, assignedNeighbors.get(0), DISPLACEMENT_RANGE_SINGLE);
} else {
// Multiple neighbors: Calculate barycenter with displacement
calculateBarycenterWithDisplacement(node, assignedNeighbors, DISPLACEMENT_RANGE_BARYCENTER);
}
} else {
// No neighbors: Position randomly around (0, 0)
setRandomPositionAroundOrigin(node, random);
}
// Add the new node to the layout
layoutNodes.put(vertex, node);
}
// Second pass: Refine positions based on neighbor degree
newLayoutNodes.sort(LAYOUT_NODE_DEGREE_COMPARATOR.reversed());
// Collect all nodes (existing and new)
Collection<LayoutNode> allNodes = layoutNodes.values();
for (LayoutNode node : newLayoutNodes) {
Vertex vertex = node.getVertex();
// Gather assigned neighbors
List<LayoutNode> assignedNeighbors = new ArrayList<>();
for (Vertex neighborVertex : prevGraph.getNeighborVertices(vertex)) {
if (layoutNodes.containsKey(neighborVertex)) {
assignedNeighbors.add(layoutNodes.get(neighborVertex));
}
}
if (!assignedNeighbors.isEmpty()) {
// Refine position based on force-based method
applyForceBasedAdjustment(node, assignedNeighbors, allNodes);
}
// Ensure node's position remains updated in the layout
layoutNodes.put(vertex, node);
}
}
/**
* Applies a force-based adjustment to the position of a given layout node
* based on repulsive forces from all other nodes and attractive forces from its assigned neighbors.
* <p>
* This method simulates a physical system where nodes repel each other to maintain spacing
* and are pulled towards their neighbors to maintain connectivity. The forces are calculated
* using Coulomb's law for repulsion and Hooke's law for attraction. The system iterates for
* a fixed number of iterations to stabilize the position of the node.
*
* @param node The node whose position is being adjusted.
* @param assignedNeighbors A list of neighboring nodes that attract this node.
* @param allNodes A collection of all nodes in the layout, used for repulsive forces.
*/
private void applyForceBasedAdjustment(LayoutNode node, List<LayoutNode> assignedNeighbors, Collection<LayoutNode> allNodes) {
// Constants for force-based adjustment
final int ITERATIONS = 50; // Number of simulation iterations
final double REPULSION_CONSTANT = 1000; // Magnitude of repulsive forces (Coulomb's law)
final double SPRING_CONSTANT = 0.2; // Strength of attractive forces to neighbors (Hooke's law)
final double DAMPING = 0.8; // Factor to reduce displacement and ensure stability
final double IDEAL_LENGTH = 100; // Desired distance between a node and its neighbors
final double MAX_FORCE = 1000; // Upper limit for the magnitude of applied forces
final double CONVERGENCE_THRESHOLD = 0.01; // Force threshold for stopping early
double posX = node.getX();
double posY = node.getY();
double dx = 0, dy = 0; // Displacement
for (int i = 0; i < ITERATIONS; i++) {
double netForceX = 0;
double netForceY = 0;
// Repulsive forces from all other nodes
for (LayoutNode otherNode : allNodes) {
if (otherNode == node) continue; // Skip self
double deltaX = posX - otherNode.getX();
double deltaY = posY - otherNode.getY();
double distanceSquared = deltaX * deltaX + deltaY * deltaY;
double distance = Math.sqrt(distanceSquared);
// Avoid division by zero by introducing a minimum distance
if (distance < 1e-6) {
deltaX = random.nextDouble() * 0.1 - 0.05;
deltaY = random.nextDouble() * 0.1 - 0.05;
distanceSquared = deltaX * deltaX + deltaY * deltaY;
distance = Math.sqrt(distanceSquared);
}
// Repulsive force (Coulomb's law)
double repulsiveForce = REPULSION_CONSTANT / distanceSquared;
// Normalize force to prevent large displacements
if (repulsiveForce > MAX_FORCE) repulsiveForce = MAX_FORCE;
netForceX += (deltaX / distance) * repulsiveForce;
netForceY += (deltaY / distance) * repulsiveForce;
}
// Attractive forces to assigned neighbors
for (LayoutNode neighbor : assignedNeighbors) {
double deltaX = neighbor.getX() - posX;
double deltaY = neighbor.getY() - posY;
double distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < 1e-6) {
deltaX = random.nextDouble() * 0.1 - 0.05;
deltaY = random.nextDouble() * 0.1 - 0.05;
distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
// Attractive force (Hooke's law)
double displacement = distance - IDEAL_LENGTH;
double attractiveForce = SPRING_CONSTANT * displacement;
if (attractiveForce > MAX_FORCE) attractiveForce = MAX_FORCE;
netForceX += (deltaX / distance) * attractiveForce;
netForceY += (deltaY / distance) * attractiveForce;
}
// Apply damping and update displacement
dx = (dx + netForceX) * DAMPING;
dy = (dy + netForceY) * DAMPING;
// Scale displacement if it's too large
double displacementMagnitude = Math.sqrt(dx * dx + dy * dy);
if (displacementMagnitude > MAX_FORCE) {
dx *= MAX_FORCE / displacementMagnitude;
dy *= MAX_FORCE / displacementMagnitude;
}
// Update position
posX += dx;
posY += dy;
// Stop early if the net force is negligible
if (Math.abs(netForceX) < CONVERGENCE_THRESHOLD && Math.abs(netForceY) < CONVERGENCE_THRESHOLD) {
break;
}
// Validate position to avoid invalid or extreme values
if (Double.isNaN(posX) || Double.isInfinite(posX) || Double.isNaN(posY) || Double.isInfinite(posY)) {
posX = node.getX(); // Reset to original position
posY = node.getY();
break;
}
}
// Set final position
node.setX((int) Math.round(posX));
node.setY((int) Math.round(posY));
}
// Utility method: position around a given node
private void setPositionAroundSingleNode(LayoutNode node, LayoutNode neighbor, int displacement) {
boolean neighborIsPredecessor = prevGraph.isPredecessorVertex(node.getVertex(), neighbor.getVertex());
boolean neighborIsSuccessor = prevGraph.isSuccessorVertex(node.getVertex(), neighbor.getVertex());
int shiftY = 0;
if (neighborIsPredecessor) {
shiftY = displacement;
} else if (neighborIsSuccessor) {
shiftY = -displacement;
}
assert shiftY != 0;
int randomY = neighbor.getY() + random.nextInt(MAX_OFFSET_AROUND_NEIGHBOR + 1) + shiftY;
int randomX = neighbor.getX() + random.nextInt(MAX_OFFSET_AROUND_NEIGHBOR + 1);
node.setX(randomX);
node.setY(randomY);
}
// Utility method: Random position around origin
private void setRandomPositionAroundOrigin(LayoutNode node, Random random) {
int randomX = random.nextInt(MAX_OFFSET_AROUND_ORIGIN + 1);
int randomY = random.nextInt(MAX_OFFSET_AROUND_ORIGIN + 1);
node.setX(randomX);
node.setY(randomY);
}
// Utility method: Calculate barycenter with displacement
private void calculateBarycenterWithDisplacement(LayoutNode node, List<LayoutNode> neighbors, int displacementRange) {
double barycenterX = 0, barycenterY = 0;
for (LayoutNode neighbor : neighbors) {
barycenterX += neighbor.getX();
barycenterY += neighbor.getY();
}
barycenterX /= neighbors.size();
barycenterY /= neighbors.size();
// Add random displacement for slight separation
int displacementX = random.nextInt(displacementRange + 1);
int displacementY = random.nextInt(displacementRange + 1);
node.setX((int) barycenterX + displacementX);
node.setY((int) barycenterY + displacementY);
}
/**
* Sets control points for a given link based on its start and end layout nodes.
* <p>
* Calculates the start and end points, applies offsets for curvature, and updates
* the link's control points.
*
* @param link The link to process.
*/
private void setLinkControlPoints(Link link) {
if (link.getFrom().getVertex() == link.getTo().getVertex()) return; // Skip self-links
LayoutNode from = layoutNodes.get(link.getFrom().getVertex());
from.setVertex(link.getFrom().getVertex());
from.updateSize();
LayoutNode to = layoutNodes.get(link.getTo().getVertex());
to.setVertex(link.getTo().getVertex());
to.updateSize();
Point startPoint = new Point(from.getLeft() + link.getFrom().getRelativePosition().x, from.getBottom());
Point endPoint = new Point(to.getLeft() + link.getTo().getRelativePosition().x, to.getTop());
List<Point> controlPoints = new ArrayList<>();
controlPoints.add(startPoint);
controlPoints.add(new Point(startPoint.x, startPoint.y + LINE_OFFSET));
controlPoints.add(new Point(endPoint.x, endPoint.y - LINE_OFFSET));
controlPoints.add(endPoint);
link.setControlPoints(controlPoints);
}
}

View File

@ -53,6 +53,11 @@ public class HierarchicalLayoutManager extends LayoutManager implements LayoutMo
maxLayerLength = enable ? 10 : -1;
}
@Override
public boolean isFreeForm() {
return false;
}
@Override
public void doLayout(LayoutGraph layoutGraph) {
layoutGraph.initializeLayout();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -169,7 +169,7 @@ public class LayoutGraph {
* Retrieves a combined list of all nodes in the graph,
* including both layout nodes and dummy nodes.
*
* @return An unmodifiable list containing all nodes in the graph.
* @return An unmodifiable list containing all nodes in the graph
*/
public List<LayoutNode> getAllNodes() {
List<LayoutNode> allNodes = new ArrayList<>();
@ -447,6 +447,63 @@ public class LayoutGraph {
return outputLinks;
}
/**
* Checks if the given predecessorVertex is a direct predecessor of the specified vertex.
*
* @param vertex The vertex to check for predecessors.
* @param predecessorVertex The vertex to verify as a predecessor of the given vertex.
* @return true if predecessorVertex is a direct predecessor of vertex, false otherwise.
*/
public boolean isPredecessorVertex(Vertex vertex, Vertex predecessorVertex) {
for (Port inputPort : inputPorts.getOrDefault(vertex, Collections.emptySet())) {
for (Link inputLink : portLinks.getOrDefault(inputPort, Collections.emptySet())) {
Vertex fromVertex = inputLink.getFrom().getVertex();
if (fromVertex.equals(predecessorVertex)) {
return true;
}
}
}
return false;
}
/**
* Checks if the given successorVertex is a direct successor of the specified vertex.
*
* @param vertex The vertex to check for successors.
* @param successorVertex The vertex to verify as a successor of the given vertex.
* @return true if successorVertex is a direct successor of vertex, false otherwise.
*/
public boolean isSuccessorVertex(Vertex vertex, Vertex successorVertex) {
for (Port outputPort : outputPorts.getOrDefault(vertex, Collections.emptySet())) {
for (Link outputLink : portLinks.getOrDefault(outputPort, Collections.emptySet())) {
Vertex toVertex = outputLink.getTo().getVertex();
if (toVertex.equals(successorVertex)) {
return true;
}
}
}
return false;
}
public List<Vertex> getNeighborVertices(Vertex vertex) {
List<Vertex> neighborVertices = new ArrayList<>();
for (Port inputPort : inputPorts.getOrDefault(vertex, Collections.emptySet())) {
for (Link inputLink : portLinks.getOrDefault(inputPort, Collections.emptySet())) {
Vertex fromVertex = inputLink.getFrom().getVertex();
assert fromVertex != null;
neighborVertices.add(fromVertex);
}
}
for (Port outputPort : outputPorts.getOrDefault(vertex, Collections.emptySet())) {
for (Link outputLink : portLinks.getOrDefault(outputPort, Collections.emptySet())) {
Vertex toVertex = outputLink.getTo().getVertex();
assert toVertex != null;
neighborVertices.add(toVertex);
}
}
return neighborVertices;
}
public List<Link> getAllLinks(Vertex vertex) {
List<Link> allLinks = new ArrayList<>();

View File

@ -49,5 +49,7 @@ public interface LayoutMover {
* @param movedVertex The vertex to be moved.
*/
void moveVertex(Vertex movedVertex);
boolean isFreeForm();
}

View File

@ -107,12 +107,7 @@ public class LayoutNode {
this(null);
}
/**
* Initializes the size and margins of the node.
* If the node represents a real vertex, it uses the vertex's size.
* Dummy nodes use default dimensions.
*/
public void initSize() {
public void updateSize() {
if (vertex == null) {
height = DUMMY_HEIGHT;
width = DUMMY_WIDTH;
@ -121,6 +116,15 @@ public class LayoutNode {
height = size.height;
width = size.width;
}
}
/**
* Initializes the size and margins of the node.
* If the node represents a real vertex, it uses the vertex's size.
* Dummy nodes use default dimensions.
*/
public void initSize() {
updateSize();
topMargin = 0;
bottomMargin = 0;
leftMargin = 0;

View File

@ -37,6 +37,7 @@ public class Settings {
public static final int CLUSTERED_SEA_OF_NODES = 1;
public static final int CONTROL_FLOW_GRAPH = 2;
public static final int STABLE_SEA_OF_NODES = 3;
public static final int INTERACTIVE_FREE_NODES = 4;
}
public static final String NODE_TEXT = "nodeText";

View File

@ -89,6 +89,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
private final Map<OutputSlot, Set<LineWidget>> outputSlotToLineWidget = new HashMap<>();
private final Map<InputSlot, Set<LineWidget>> inputSlotToLineWidget = new HashMap<>();
private final FreeInteractiveLayoutManager freeInteractiveLayoutManager;
private final HierarchicalStableLayoutManager hierarchicalStableLayoutManager;
private HierarchicalLayoutManager seaLayoutManager;
private LayoutMover layoutMover;
@ -511,6 +512,7 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
}
});
freeInteractiveLayoutManager = new FreeInteractiveLayoutManager();
hierarchicalStableLayoutManager = new HierarchicalStableLayoutManager();
seaLayoutManager = new HierarchicalLayoutManager();
@ -643,6 +645,8 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
widget.bringToFront();
startLayerY = widget.getLocation().y;
hasMoved = false; // Reset the movement flag
if (layoutMover.isFreeForm()) return;
Set<Figure> selectedFigures = model.getSelectedFigures();
if (selectedFigures.size() == 1) {
Figure selectedFigure = selectedFigures.iterator().next();
@ -702,8 +706,12 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
hasMoved = true; // Mark that a movement occurred
int shiftX = location.x - widget.getLocation().x;
int shiftY = magnetToStartLayerY(widget, location);
int shiftY;
if (layoutMover.isFreeForm()) {
shiftY = location.y - widget.getLocation().y;
} else {
shiftY = magnetToStartLayerY(widget, location);
}
List<Figure> selectedFigures = new ArrayList<>( model.getSelectedFigures());
selectedFigures.sort(Comparator.comparingInt(f -> f.getPosition().x));
for (Figure figure : selectedFigures) {
@ -713,12 +721,15 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
if (inputSlotToLineWidget.containsKey(inputSlot)) {
for (LineWidget lw : inputSlotToLineWidget.get(inputSlot)) {
assert lw != null;
Point toPt = lw.getTo();
Point fromPt = lw.getFrom();
if (toPt != null && fromPt != null) {
int xTo = toPt.x + shiftX;
int yTo = toPt.y + shiftY;
lw.setTo(new Point(xTo, yTo));
Point toPt = lw.getTo();
if (toPt == null || fromPt == null) {
continue;
}
int xTo = toPt.x + shiftX;
int yTo = toPt.y + shiftY;
lw.setTo(new Point(xTo, yTo));
if (!layoutMover.isFreeForm()) {
lw.setFrom(new Point(fromPt.x + shiftX, fromPt.y));
LineWidget pred = lw.getPredecessor();
pred.setTo(new Point(pred.getTo().x + shiftX, pred.getTo().y));
@ -735,10 +746,13 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
assert lw != null;
Point fromPt = lw.getFrom();
Point toPt = lw.getTo();
if (toPt != null && fromPt != null) {
int xFrom = fromPt.x + shiftX;
int yFrom = fromPt.y + shiftY;
lw.setFrom(new Point(xFrom, yFrom));
if (toPt == null || fromPt == null) {
continue;
}
int xFrom = fromPt.x + shiftX;
int yFrom = fromPt.y + shiftY;
lw.setFrom(new Point(xFrom, yFrom));
if (!layoutMover.isFreeForm()) {
lw.setTo(new Point(toPt.x + shiftX, toPt.y));
for (LineWidget succ : lw.getSuccessors()) {
succ.setFrom(new Point(succ.getFrom().x + shiftX, succ.getFrom().y));
@ -753,10 +767,12 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
ActionFactory.createDefaultMoveProvider().setNewLocation(fw, newLocation);
}
FigureWidget fw = getWidget(selectedFigures.iterator().next());
pointerWidget.setVisible(true);
Point newLocation = new Point(fw.getLocation().x + shiftX -3, fw.getLocation().y + shiftY);
ActionFactory.createDefaultMoveProvider().setNewLocation(pointerWidget, newLocation);
if (selectedFigures.size() == 1 && !layoutMover.isFreeForm()) {
FigureWidget fw = getWidget(selectedFigures.iterator().next());
pointerWidget.setVisible(true);
Point newLocation = new Point(fw.getLocation().x + shiftX -3, fw.getLocation().y + shiftY);
ActionFactory.createDefaultMoveProvider().setNewLocation(pointerWidget, newLocation);
}
connectionLayer.revalidate();
connectionLayer.repaint();
}
@ -834,7 +850,9 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
Set<Figure> visibleFigures = getVisibleFigures();
Set<Connection> visibleConnections = getVisibleConnections();
if (getModel().getShowStableSea()) {
if (getModel().getShowFreeInteractive()) {
doFreeInteractiveLayout(visibleFigures, visibleConnections);
} else if (getModel().getShowStableSea()) {
doStableSeaLayout(visibleFigures, visibleConnections);
} else if (getModel().getShowSea()) {
doSeaLayout(visibleFigures, visibleConnections);
@ -904,6 +922,12 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
return w1.isVisible() && w2.isVisible();
}
private void doFreeInteractiveLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
layoutMover = freeInteractiveLayoutManager;
freeInteractiveLayoutManager.setCutEdges(model.getCutEdges());
freeInteractiveLayoutManager.doLayout(new LayoutGraph(visibleConnections, visibleFigures));
}
private void doStableSeaLayout(Set<Figure> visibleFigures, Set<Connection> visibleConnections) {
layoutMover = null;
boolean enable = model.getCutEdges();
@ -1108,6 +1132,52 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
}
}
private void processFreeForm(OutputSlot outputSlot, List<FigureConnection> connections) {
for (FigureConnection connection : connections) {
if (isVisibleFigureConnection(connection)) {
boolean isBold = false;
boolean isDashed = true;
boolean isVisible = true;
if (connection.getStyle() == Connection.ConnectionStyle.BOLD) {
isBold = true;
} else if (connection.getStyle() == Connection.ConnectionStyle.INVISIBLE) {
isVisible = false;
}
if (connection.getStyle() != Connection.ConnectionStyle.DASHED) {
isDashed = false;
}
List<Point> controlPoints = connection.getControlPoints();
if (controlPoints.size() <= 2) continue;
Point firstPoint = controlPoints.get(0); // First point
Point lastPoint = controlPoints.get(controlPoints.size() - 1); // Last point
List<FigureConnection> connectionList = new ArrayList<>(Collections.singleton(connection));
LineWidget line = new LineWidget(this, outputSlot, connectionList, firstPoint, lastPoint, null, isBold, isDashed);
line.setFromControlYOffset(50);
line.setToControlYOffset(-50);
line.setVisible(isVisible);
connectionLayer.addChild(line);
addObject(new ConnectionSet(connectionList), line);
line.getActions().addAction(hoverAction);
if (outputSlotToLineWidget.containsKey(outputSlot)) {
outputSlotToLineWidget.get(outputSlot).add(line);
} else {
outputSlotToLineWidget.put(outputSlot, new HashSet<>(Collections.singleton(line)));
}
InputSlot inputSlot = connection.getInputSlot();
if (inputSlotToLineWidget.containsKey(inputSlot)) {
inputSlotToLineWidget.get(inputSlot).add(line);
} else {
inputSlotToLineWidget.put(inputSlot, new HashSet<>(Collections.singleton(line)));
}
}
}
}
private void processBlockConnection(BlockConnection blockConnection) {
boolean isDashed = blockConnection.getStyle() == Connection.ConnectionStyle.DASHED;
boolean isBold = blockConnection.getStyle() == Connection.ConnectionStyle.BOLD;
@ -1281,7 +1351,11 @@ public class DiagramScene extends ObjectScene implements DiagramViewer, DoubleCl
for (Figure figure : getModel().getDiagram().getFigures()) {
for (OutputSlot outputSlot : figure.getOutputSlots()) {
List<FigureConnection> connectionList = new ArrayList<>(outputSlot.getConnections());
processOutputSlot(outputSlot, connectionList, 0, null, null);
if (layoutMover != null && layoutMover.isFreeForm()) {
processFreeForm(outputSlot, connectionList);
} else {
processOutputSlot(outputSlot, connectionList, 0, null, null);
}
}
}

View File

@ -63,6 +63,7 @@ public class DiagramViewModel extends RangeSliderModel implements ChangedListene
private final ChangedEvent<DiagramViewModel> selectedNodesChangedEvent = new ChangedEvent<>(this);
private final ChangedEvent<DiagramViewModel> hiddenNodesChangedEvent = new ChangedEvent<>(this);
private ChangedListener<InputGraph> titleChangedListener = g -> {};
private boolean showFreeInteractive;
private boolean showStableSea;
private boolean showSea;
private boolean showBlocks;
@ -104,6 +105,17 @@ public class DiagramViewModel extends RangeSliderModel implements ChangedListene
}
}
public boolean getShowFreeInteractive() {
return showFreeInteractive;
}
public void setShowFreeInteractive(boolean enable) {
showFreeInteractive = enable;
if (enable) {
diagramChangedEvent.fire();
}
}
public boolean getShowStableSea() {
return showStableSea;
}
@ -224,6 +236,7 @@ public class DiagramViewModel extends RangeSliderModel implements ChangedListene
globalSelection = GlobalSelectionAction.get(GlobalSelectionAction.class).isSelected();
cutEdges = CutEdgesAction.get(CutEdgesAction.class).isSelected();
showFreeInteractive = Settings.get().getInt(Settings.DEFAULT_VIEW, Settings.DEFAULT_VIEW_DEFAULT) == Settings.DefaultView.INTERACTIVE_FREE_NODES;
showStableSea = Settings.get().getInt(Settings.DEFAULT_VIEW, Settings.DEFAULT_VIEW_DEFAULT) == Settings.DefaultView.STABLE_SEA_OF_NODES;
showSea = Settings.get().getInt(Settings.DEFAULT_VIEW, Settings.DEFAULT_VIEW_DEFAULT) == Settings.DefaultView.SEA_OF_NODES;
showBlocks = Settings.get().getInt(Settings.DEFAULT_VIEW, Settings.DEFAULT_VIEW_DEFAULT) == Settings.DefaultView.CLUSTERED_SEA_OF_NODES;
@ -266,7 +279,7 @@ public class DiagramViewModel extends RangeSliderModel implements ChangedListene
for (String ignored : getPositions()) {
colors.add(Color.black);
}
if (nodes.size() >= 1) {
if (!nodes.isEmpty()) {
for (Integer id : nodes) {
if (id < 0) {
id = -id;

View File

@ -176,6 +176,11 @@ public final class EditorTopComponent extends TopComponent implements TopCompone
toolBar.addSeparator();
ButtonGroup layoutButtons = new ButtonGroup();
JToggleButton freeInteractiveLayoutButton = new JToggleButton(new EnableFreeLayoutAction(this));
freeInteractiveLayoutButton.setSelected(diagramViewModel.getShowFreeInteractive());
layoutButtons.add(freeInteractiveLayoutButton);
toolBar.add(freeInteractiveLayoutButton);
JToggleButton stableSeaLayoutButton = new JToggleButton(new EnableStableSeaLayoutAction(this));
stableSeaLayoutButton.setSelected(diagramViewModel.getShowStableSea());
layoutButtons.add(stableSeaLayoutButton);

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
package com.sun.hotspot.igv.view.actions;
import com.sun.hotspot.igv.view.EditorTopComponent;
import java.beans.PropertyChangeEvent;
public class EnableFreeLayoutAction extends EnableLayoutAction {
public EnableFreeLayoutAction(EditorTopComponent etc) {
super(etc);
}
@Override
protected String iconResource() {
return "com/sun/hotspot/igv/view/images/dynamic.png";
}
@Override
protected String getDescription() {
return "Show dynamic free layout";
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
editor.getModel().setShowFreeInteractive(this.isSelected());
}
}

View File

@ -30,14 +30,16 @@ import com.sun.hotspot.igv.graph.OutputSlot;
import com.sun.hotspot.igv.layout.Vertex;
import com.sun.hotspot.igv.util.StringUtils;
import com.sun.hotspot.igv.view.DiagramScene;
import com.sun.hotspot.igv.view.actions.CustomSelectAction;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Line2D;
import java.util.*;
import java.util.List;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import com.sun.hotspot.igv.view.actions.CustomSelectAction;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.PopupMenuProvider;
import org.netbeans.api.visual.action.SelectProvider;
@ -70,6 +72,8 @@ public class LineWidget extends Widget implements PopupMenuProvider {
private final boolean isBold;
private final boolean isDashed;
private boolean needToInitToolTipText = true;
private int fromControlYOffset;
private int toControlYOffset;
public LineWidget(DiagramScene scene, OutputSlot s, List<? extends Connection> connections, Point from, Point to, LineWidget predecessor, boolean isBold, boolean isDashed) {
super(scene);
@ -172,6 +176,16 @@ public class LineWidget extends Widget implements PopupMenuProvider {
computeClientArea();
}
public void setFromControlYOffset(int fromControlYOffset) {
this.fromControlYOffset = fromControlYOffset;
computeClientArea();
}
public void setToControlYOffset(int toControlYOffset) {
this.toControlYOffset = toControlYOffset;
computeClientArea();
}
public Point getFrom() {
return from;
}
@ -225,7 +239,41 @@ public class LineWidget extends Widget implements PopupMenuProvider {
g.setStroke(new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
}
g.drawLine(from.x, from.y, to.x, to.y);
// Define S-shaped curve with control points
if (fromControlYOffset != 0 && toControlYOffset != 0) {
if (from.y < to.y) { // non-reversed edges
if (Math.abs(from.x - to.x) > 10) {
CubicCurve2D.Float sShape = new CubicCurve2D.Float();
sShape.setCurve(from.x, from.y,
from.x, from.y + fromControlYOffset,
to.x, to.y + toControlYOffset,
to.x, to.y);
g.draw(sShape);
} else {
g.drawLine(from.x, from.y, to.x, to.y);
}
} else { // reverse edges
if (from.x - to.x > 0) {
CubicCurve2D.Float sShape = new CubicCurve2D.Float();
sShape.setCurve(from.x, from.y,
from.x - 150, from.y + fromControlYOffset,
to.x + 150, to.y + toControlYOffset,
to.x, to.y);
g.draw(sShape);
} else {
// add x offset
CubicCurve2D.Float sShape = new CubicCurve2D.Float();
sShape.setCurve(from.x, from.y,
from.x + 150, from.y + fromControlYOffset,
to.x - 150, to.y + toControlYOffset,
to.x, to.y);
g.draw(sShape);
}
}
} else {
// Fallback to straight line if control points are not set
g.drawLine(from.x, from.y, to.x, to.y);
}
boolean sameFrom = false;
boolean sameTo = successors.isEmpty();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB