private void addHexagonsToScreen() {
for(Hex hex : map.getMap()) {
Point[] corners = screen.polygonCorners(hex);
int i = 0;
Double[] points = new Double[12];
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
this.root.getChildren().add(drawHexagon(points));
}
}
private Polygon drawHexagon(Double[] points) {
Polygon polygon = new Polygon();
polygon.getPoints().addAll(points);
polygon.setStroke(Color.BLACK);
polygon.setFill(Color.TRANSPARENT);
return polygon;
}
This is how I define a point.
public class Point {
private final double x, y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
/**
* #return the screen x-coordinate.
*/
public double getX() {
return x;
}
/**
* #return the screen y-coordinate.
*/
public double getY() {
return y;
} }
This is currently how I am drawing my hexagons on a Pane object. As you can see in the picture below, I'm fairly certain that most of the lines are actually being retraced because each edge (except the outer edges) are obviously common to two hexagons. In order to solve this, I instead calculated all the points at once then passed that into the Polygon() function as a double [] with the intention of drawing just one polygon rather than many.
private void addHexagonsToScreen() {
int i = 0;
Double[] points = new Double[map.getMap().size() * 12];
for(Hex hex : map.getMap()) {
Point[] corners = screen.polygonCorners(hex);
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
}
this.root.getChildren().add(drawHexagon(points));
}
This resulted in the second picture. I think the solution requires that I specify "only connect each point to its closest other point", but I am unsure of how to do this. Is there a simple solution to my problem?
The reason the hex map is off in the top left corner is because I'm specifying the origin of my hex map to be 0,0 and calculating a pixel location as such. Obviously I have done something wrong in converting my 0,0 hexagon location to correspond with the center of the Pane and growing Y+ in the up direction and X+ in the right direction.
Related
I've looked at similar questions but they all are concerned with collision detection rather than preventing overlap. I've gotten most of it to work with the below code:
private final EventHandler<MouseEvent> onPress = mouseEvent -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : getAllShapes()) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
However, the problem is, once there is a tiniest bit of overlap, the Shape is no longer draggable at all. Meaning, if I drag a shape to another, once they become essentially tangent, neither of them are draggable anymore. What I want to happen is just that, for example, if you try to drag a circle onto another, the circle won't follow the mouse position as long as the future position of the drag will cause an overlap.
I can't figure out exactly how to accomplish this.
EDIT: Minimum Reproducible Example:
Main.java
public class Main extends Application {
static Circle circle1 = new DraggableCircle(100, 200);
static Circle circle2 = new DraggableCircle(200, 300);
static Circle[] circleList = new Circle[]{circle1, circle2};
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World");
Pane pane = new Pane();
primaryStage.setScene(new Scene(pane, 300, 275));
primaryStage.show();
pane.getChildren().addAll(circle1, circle2);
}
public static void main(String[] args) {
launch(args);
}
}
DraggableCircle.java
public class DraggableCircle extends Circle {
private double xDrag, yDrag;
public DraggableCircle(double x, double y) {
super(x, y, 30);
this.setFill(Color.WHITE);
this.setStroke(Color.BLACK);
this.setStrokeWidth(1.5);
this.addEventHandler(MouseEvent.MOUSE_PRESSED, onPress);
this.addEventHandler(MouseEvent.MOUSE_DRAGGED, onDrag);
}
private final EventHandler<MouseEvent> onPress = (mouseEvent) -> {
xDrag = this.getCenterX() - mouseEvent.getX();
yDrag = this.getCenterY() - mouseEvent.getY();
};
private final EventHandler<MouseEvent> onDrag = mouseEvent -> {
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
return;
}
}
}
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
};
}
This also has an issue where dragging too quickly causes a noticeable overlap between the circles, before the drag detection ends.
A simple (imperfect) solution
The following algorithm will allow a node to continue to be dragged after an intersection has occurred:
Record the current draggable shape position.
Set the new position.
Check the intersection.
If an intersection is detected, reset the position to the original position.
An implementation replaces the drag handler in the supplied minimal example code from the question.
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Shape shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
this.setCenterX(priorCenterX);
this.setCenterY(priorCenterY);
return;
}
}
}
};
This handler does work better than what you had, it does at least allow you to continue dragging after the intersection.
But, yes, if you drag quickly it will leave a visible space between nodes when it has detected that the drag operation would cause an intersection, which isn't ideal.
Also, the additional requirement you added in your comment about having the dragged shape glide along a border would require a more sophisticated solution.
Other potential solutions
I don't offer code for these more sophisticated solutions here.
One potential brute force solution is to interpolate the prior center with the new center and then, in a loop, slowly move the dragged object along the interpolated line until an intersection is detected, then just back it out to the last interpolated value to prevent the intersection. You can do this by calculating and applying a normalized (1 unit distance) movement vector. That might fix space between intersected nodes.
Similarly to get the gliding, on the intersection, you could just update either the interpolated x or y value rather than both.
There may be more sophisticated methods with geometry math applied, especially if you know shape geometry along with movement vectors and surface normals.
+1 for #jewelsea answer.
On top of #jewelsea answer, I would like to provide a fix for the "space between nodes" issue.
So you might have already observed that when you drag fast, it will not cover each and every pixel in the drag path. It varies with the speed of the drag. So when you decide to move it to the previous recorded point, we will do a quick math, to see if there is any gap between the two nodes, if yes:
We will do a math "to determine a point along a line which is at distance d" and move the drag circle to that point. Here..
start point of line is : previous recorded point
end point of line is : the intersected shape center
d is : the gap between the two shapes.
So the updated code to the #jewelsea answer is as below:
private final EventHandler<MouseEvent> onDrag = (mouseEvent) -> {
double priorCenterX = getCenterX();
double priorCenterY = getCenterY();
this.setCenterX(mouseEvent.getX() + xDrag);
this.setCenterY(mouseEvent.getY() + yDrag);
for (Circle shape : Main.circleList) {
if (!this.equals(shape)) {
Shape intersect = Shape.intersect(shape, this);
if (intersect.getLayoutBounds().getWidth() > 0) {
Point2D cx = new Point2D(priorCenterX, priorCenterY);
Point2D px = new Point2D(shape.getCenterX(), shape.getCenterY());
double d = cx.distance(px);
if (d > getRadius() + shape.getRadius()) {
cx = pointAtDistance(cx, px, d - getRadius() - shape.getRadius());
}
this.setCenterX(cx.getX());
this.setCenterY(cx.getY());
return;
}
}
}
};
private Point2D pointAtDistance(Point2D p1, Point2D p2, double distance) {
double lineLength = p1.distance(p2);
double t = distance / lineLength;
double dx = ((1 - t) * p1.getX()) + (t * p2.getX());
double dy = ((1 - t) * p1.getY()) + (t * p2.getY());
return new Point2D(dx, dy);
}
Currently, I have a working implementation of an undirected graph where vertices are represented as StackPanes and edges are represented as Lines. The StackPanes are draggable and when dragged the lines move accordingly.
This is what it looks like so far.
However, I am really struggling with implementing directed graphs. Undirected graphs only really need 1 line, but with directed graphs, you will need 2 lines when there is an edge from A to B and an edge from B to A.
I want something like this when there are 2 edges between a vertex:
This is how I binded the undirected lines between 2 vertices (ie A Line between 2 StackPanes):
Line line = new Line();
line.setStroke(Color.BLACK);
line.setFill(null);
line.setStrokeWidth(2);
line.startXProperty().bind(vertexClickedOn.layoutXProperty().add(vertexClickedOn.translateXProperty()).add(vertexClickedOn.widthProperty().divide(2)));
line.startYProperty().bind(vertexClickedOn.layoutYProperty().add(vertexClickedOn.translateYProperty()).add(vertexClickedOn.heightProperty().divide(2)));
line.endXProperty().bind(vertexTo.layoutXProperty().add(vertexTo.translateXProperty()).add(vertexTo.widthProperty().divide(2)));
line.endYProperty().bind(vertexTo.layoutYProperty().add(vertexTo.translateYProperty()).add(vertexTo.heightProperty().divide(2)));
VertexClickedOn and VertexTo are both StackPanes, in graph terms, VertexClickedOn is where the edge starts and VertexTo is where the edge ends
I've been stuck on this for a while now any help will be greatly appreciated.
Thank you.
To start with, lets discuss your requirement in terms of vectors.
You have a line (joining the centers of two circles).
You want to place a node(arrow) at partical point on a line.
And this point is always located at a distance of (totalLineLength - circleRadius) for end arrow and a distance of circleRadius for start arrow.
Finally for directed lines, you want to translate this line up or down based on direction.
So once you have the line start and end points, using little Math you can get the point on a line at a certain distance. To keep the arrow direction correctly you can rotate the arrow based on the line slope.
As the code is a bit verbose because of calculations, please find below a working demo of what I mentioned above.
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class PaneLayoutDemo extends Application {
double sceneX, sceneY, layoutX, layoutY;
#Override
public void start(Stage stage) throws Exception {
StackPane root = new StackPane();
root.setPadding(new Insets(20));
Pane pane = new Pane();
root.getChildren().add(pane);
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
StackPane dotA = getDot("green", "A");
StackPane dotB = getDot("red", "B");
StackPane dotC = getDot("yellow", "C");
StackPane dotD = getDot("pink", "D");
StackPane dotE = getDot("silver", "E");
buildSingleDirectionalLine(dotA, dotB, pane, true, true); // A <--> B
buildSingleDirectionalLine(dotB, dotC, pane, true, true); // B <--> C
buildSingleDirectionalLine(dotC, dotD, pane, true, false); // C --> D
// D <===> E
buildBiDirectionalLine(true, dotD, dotE, pane);
buildBiDirectionalLine(false, dotD, dotE, pane);
pane.getChildren().addAll(dotA, dotB, dotC, dotD, dotE);
}
/**
* Builds the single directional line with pointing arrows at each end.
* #param startDot Pane for considering start point
* #param endDot Pane for considering end point
* #param parent Parent container
* #param hasEndArrow Specifies whether to show arrow towards end
* #param hasStartArrow Specifies whether to show arrow towards start
*/
private void buildSingleDirectionalLine(StackPane startDot, StackPane endDot, Pane parent, boolean hasEndArrow, boolean hasStartArrow) {
Line line = getLine(startDot, endDot);
StackPane arrowAB = getArrow(true, line, startDot, endDot);
if (!hasEndArrow) {
arrowAB.setOpacity(0);
}
StackPane arrowBA = getArrow(false, line, startDot, endDot);
if (!hasStartArrow) {
arrowBA.setOpacity(0);
}
StackPane weightAB = getWeight(line);
parent.getChildren().addAll(line, weightAB, arrowBA, arrowAB);
}
/**
* Builds the bi directional line with pointing arrow at specified end.
* #param isEnd Specifies whether the line is towards end or not. If false then the line is towards start.
* #param startDot Pane for considering start point
* #param endDot Pane for considering end point
* #param parent Parent container
*/
private void buildBiDirectionalLine(boolean isEnd, StackPane startDot, StackPane endDot, Pane parent) {
Line virtualCenterLine = getLine(startDot, endDot);
virtualCenterLine.setOpacity(0);
StackPane centerLineArrowAB = getArrow(true, virtualCenterLine, startDot, endDot);
centerLineArrowAB.setOpacity(0);
StackPane centerLineArrowBA = getArrow(false, virtualCenterLine, startDot, endDot);
centerLineArrowBA.setOpacity(0);
Line directedLine = new Line();
directedLine.setStroke(Color.RED);
directedLine.setStrokeWidth(2);
double diff = isEnd ? -centerLineArrowAB.getPrefWidth() / 2 : centerLineArrowAB.getPrefWidth() / 2;
final ChangeListener<Number> listener = (obs, old, newVal) -> {
Rotate r = new Rotate();
r.setPivotX(virtualCenterLine.getStartX());
r.setPivotY(virtualCenterLine.getStartY());
r.setAngle(centerLineArrowAB.getRotate());
Point2D point = r.transform(new Point2D(virtualCenterLine.getStartX(), virtualCenterLine.getStartY() + diff));
directedLine.setStartX(point.getX());
directedLine.setStartY(point.getY());
Rotate r2 = new Rotate();
r2.setPivotX(virtualCenterLine.getEndX());
r2.setPivotY(virtualCenterLine.getEndY());
r2.setAngle(centerLineArrowBA.getRotate());
Point2D point2 = r2.transform(new Point2D(virtualCenterLine.getEndX(), virtualCenterLine.getEndY() - diff));
directedLine.setEndX(point2.getX());
directedLine.setEndY(point2.getY());
};
centerLineArrowAB.rotateProperty().addListener(listener);
centerLineArrowBA.rotateProperty().addListener(listener);
virtualCenterLine.startXProperty().addListener(listener);
virtualCenterLine.startYProperty().addListener(listener);
virtualCenterLine.endXProperty().addListener(listener);
virtualCenterLine.endYProperty().addListener(listener);
StackPane mainArrow = getArrow(isEnd, directedLine, startDot, endDot);
parent.getChildren().addAll(virtualCenterLine, centerLineArrowAB, centerLineArrowBA, directedLine, mainArrow);
}
/**
* Builds a line between the provided start and end panes center point.
*
* #param startDot Pane for considering start point
* #param endDot Pane for considering end point
* #return Line joining the layout center points of the provided panes.
*/
private Line getLine(StackPane startDot, StackPane endDot) {
Line line = new Line();
line.setStroke(Color.BLUE);
line.setStrokeWidth(2);
line.startXProperty().bind(startDot.layoutXProperty().add(startDot.translateXProperty()).add(startDot.widthProperty().divide(2)));
line.startYProperty().bind(startDot.layoutYProperty().add(startDot.translateYProperty()).add(startDot.heightProperty().divide(2)));
line.endXProperty().bind(endDot.layoutXProperty().add(endDot.translateXProperty()).add(endDot.widthProperty().divide(2)));
line.endYProperty().bind(endDot.layoutYProperty().add(endDot.translateYProperty()).add(endDot.heightProperty().divide(2)));
return line;
}
/**
* Builds an arrow on the provided line pointing towards the specified pane.
*
* #param toLineEnd Specifies whether the arrow to point towards end pane or start pane.
* #param line Line joining the layout center points of the provided panes.
* #param startDot Pane which is considered as start point of line
* #param endDot Pane which is considered as end point of line
* #return Arrow towards the specified pane.
*/
private StackPane getArrow(boolean toLineEnd, Line line, StackPane startDot, StackPane endDot) {
double size = 12; // Arrow size
StackPane arrow = new StackPane();
arrow.setStyle("-fx-background-color:#333333;-fx-border-width:1px;-fx-border-color:black;-fx-shape: \"M0,-4L4,0L0,4Z\"");//
arrow.setPrefSize(size, size);
arrow.setMaxSize(size, size);
arrow.setMinSize(size, size);
// Determining the arrow visibility unless there is enough space between dots.
DoubleBinding xDiff = line.endXProperty().subtract(line.startXProperty());
DoubleBinding yDiff = line.endYProperty().subtract(line.startYProperty());
BooleanBinding visible = (xDiff.lessThanOrEqualTo(size).and(xDiff.greaterThanOrEqualTo(-size)).and(yDiff.greaterThanOrEqualTo(-size)).and(yDiff.lessThanOrEqualTo(size))).not();
arrow.visibleProperty().bind(visible);
// Determining the x point on the line which is at a certain distance.
DoubleBinding tX = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (line.getEndX() - line.getStartX()) * (line.getEndX() - line.getStartX());
double yDiffSqu = (line.getEndY() - line.getStartY()) * (line.getEndY() - line.getStartY());
double lineLength = Math.sqrt(xDiffSqu + yDiffSqu);
double dt;
if (toLineEnd) {
// When determining the point towards end, the required distance is total length minus (radius + arrow half width)
dt = lineLength - (endDot.getWidth() / 2) - (arrow.getWidth() / 2);
} else {
// When determining the point towards start, the required distance is just (radius + arrow half width)
dt = (startDot.getWidth() / 2) + (arrow.getWidth() / 2);
}
double t = dt / lineLength;
double dx = ((1 - t) * line.getStartX()) + (t * line.getEndX());
return dx;
}, line.startXProperty(), line.endXProperty(), line.startYProperty(), line.endYProperty());
// Determining the y point on the line which is at a certain distance.
DoubleBinding tY = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (line.getEndX() - line.getStartX()) * (line.getEndX() - line.getStartX());
double yDiffSqu = (line.getEndY() - line.getStartY()) * (line.getEndY() - line.getStartY());
double lineLength = Math.sqrt(xDiffSqu + yDiffSqu);
double dt;
if (toLineEnd) {
dt = lineLength - (endDot.getHeight() / 2) - (arrow.getHeight() / 2);
} else {
dt = (startDot.getHeight() / 2) + (arrow.getHeight() / 2);
}
double t = dt / lineLength;
double dy = ((1 - t) * line.getStartY()) + (t * line.getEndY());
return dy;
}, line.startXProperty(), line.endXProperty(), line.startYProperty(), line.endYProperty());
arrow.layoutXProperty().bind(tX.subtract(arrow.widthProperty().divide(2)));
arrow.layoutYProperty().bind(tY.subtract(arrow.heightProperty().divide(2)));
DoubleBinding endArrowAngle = Bindings.createDoubleBinding(() -> {
double stX = toLineEnd ? line.getStartX() : line.getEndX();
double stY = toLineEnd ? line.getStartY() : line.getEndY();
double enX = toLineEnd ? line.getEndX() : line.getStartX();
double enY = toLineEnd ? line.getEndY() : line.getStartY();
double angle = Math.toDegrees(Math.atan2(enY - stY, enX - stX));
if (angle < 0) {
angle += 360;
}
return angle;
}, line.startXProperty(), line.endXProperty(), line.startYProperty(), line.endYProperty());
arrow.rotateProperty().bind(endArrowAngle);
return arrow;
}
/**
* Builds a pane at the center of the provided line.
*
* #param line Line on which the pane need to be set.
* #return Pane located at the center of the provided line.
*/
private StackPane getWeight(Line line) {
double size = 20;
StackPane weight = new StackPane();
weight.setStyle("-fx-background-color:grey;-fx-border-width:1px;-fx-border-color:black;");
weight.setPrefSize(size, size);
weight.setMaxSize(size, size);
weight.setMinSize(size, size);
DoubleBinding wgtSqrHalfWidth = weight.widthProperty().divide(2);
DoubleBinding wgtSqrHalfHeight = weight.heightProperty().divide(2);
DoubleBinding lineXHalfLength = line.endXProperty().subtract(line.startXProperty()).divide(2);
DoubleBinding lineYHalfLength = line.endYProperty().subtract(line.startYProperty()).divide(2);
weight.layoutXProperty().bind(line.startXProperty().add(lineXHalfLength.subtract(wgtSqrHalfWidth)));
weight.layoutYProperty().bind(line.startYProperty().add(lineYHalfLength.subtract(wgtSqrHalfHeight)));
return weight;
}
/**
* Builds a pane consisting of circle with the provided specifications.
*
* #param color Color of the circle
* #param text Text inside the circle
* #return Draggable pane consisting a circle.
*/
private StackPane getDot(String color, String text) {
double radius = 50;
double paneSize = 2 * radius;
StackPane dotPane = new StackPane();
Circle dot = new Circle();
dot.setRadius(radius);
dot.setStyle("-fx-fill:" + color + ";-fx-stroke-width:2px;-fx-stroke:black;");
Label txt = new Label(text);
txt.setStyle("-fx-font-size:18px;-fx-font-weight:bold;");
dotPane.getChildren().addAll(dot, txt);
dotPane.setPrefSize(paneSize, paneSize);
dotPane.setMaxSize(paneSize, paneSize);
dotPane.setMinSize(paneSize, paneSize);
dotPane.setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
layoutX = dotPane.getLayoutX();
layoutY = dotPane.getLayoutY();
});
EventHandler<MouseEvent> dotOnMouseDraggedEventHandler = e -> {
// Offset of drag
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
// Taking parent bounds
Bounds parentBounds = dotPane.getParent().getLayoutBounds();
// Drag node bounds
double currPaneLayoutX = dotPane.getLayoutX();
double currPaneWidth = dotPane.getWidth();
double currPaneLayoutY = dotPane.getLayoutY();
double currPaneHeight = dotPane.getHeight();
if ((currPaneLayoutX + offsetX < parentBounds.getWidth() - currPaneWidth) && (currPaneLayoutX + offsetX > -1)) {
// If the dragNode bounds is within the parent bounds, then you can set the offset value.
dotPane.setTranslateX(offsetX);
} else if (currPaneLayoutX + offsetX < 0) {
// If the sum of your offset and current layout position is negative, then you ALWAYS update your translate to negative layout value
// which makes the final layout position to 0 in mouse released event.
dotPane.setTranslateX(-currPaneLayoutX);
} else {
// If your dragNode bounds are outside parent bounds,ALWAYS setting the translate value that fits your node at end.
dotPane.setTranslateX(parentBounds.getWidth() - currPaneLayoutX - currPaneWidth);
}
if ((currPaneLayoutY + offsetY < parentBounds.getHeight() - currPaneHeight) && (currPaneLayoutY + offsetY > -1)) {
dotPane.setTranslateY(offsetY);
} else if (currPaneLayoutY + offsetY < 0) {
dotPane.setTranslateY(-currPaneLayoutY);
} else {
dotPane.setTranslateY(parentBounds.getHeight() - currPaneLayoutY - currPaneHeight);
}
};
dotPane.setOnMouseDragged(dotOnMouseDraggedEventHandler);
dotPane.setOnMouseReleased(e -> {
// Updating the new layout positions
dotPane.setLayoutX(layoutX + dotPane.getTranslateX());
dotPane.setLayoutY(layoutY + dotPane.getTranslateY());
// Resetting the translate positions
dotPane.setTranslateX(0);
dotPane.setTranslateY(0);
});
return dotPane;
}
public static void main(String[] args) {
Application.launch(args);
}
}
I am developing a program for video annotation. Objects seen in a video may be marked as objects of interest and if they are interacting, the user may draw a line between two annotations. On the visible level, bject annotations are basically transparent Rectangles and relations between them are Lines. At this point it is easy to compute the center of a rectangle, but I am not able to bind the start and end of a Line to the center of the corresponding Rectangle. I have tried the following approaches:
Creating two DoubleBindings inside the rectangle class that computes the center x and y:
private DoubleBinding centerXBinding = new DoubleBinding() {
#Override
protected double computeValue() {
return getX() + getWidth() / 2;
}
};
and then bound it to the newly created Line:
`currentRelation.startXProperty().bind(startShape.centerXBinding());`
in the controller…
The result is ok at first, the line start and end points are exactly where I want to haven them, but when a Rectangle gets dragged to another position, the line end does not move anywhere!
Does anyone see the problem?
UPDATE:
The movement of a Rectangle is done by computing an offset and updating the translation values like translateX:
public class MyRectangle extends Rectangle {
private double orgSceneX;
private double orgSceneY;
private double orgTranslateX;
private double orgTranslateY;
private void initEventHandling() {
this.setOnMousePressed(mousePress -> {
if (mousePress.getButton() == MouseButton.PRIMARY) {
orgSceneX = mousePress.getSceneX();
orgSceneY = mousePress.getSceneY();
orgTranslateX = ((MyRectangle) mousePress.getSource()).getTranslateX();
orgTranslateY = ((MyRectangle) mousePress.getSource()).getTranslateY();
mousePress.consume();
} else if (mousePress.getButton() == MouseButton.SECONDARY) {
System.out.println(LOG_TAG + ": right mouse button PRESS on " + this.getId() + ", event not consumed");
}
});
this.setOnMouseDragged(mouseDrag -> {
if (mouseDrag.getButton() == MouseButton.PRIMARY) {
double offsetX = mouseDrag.getSceneX() - orgSceneX;
double offsetY = mouseDrag.getSceneY() - orgSceneY;
double updateTranslateX = orgTranslateX + offsetX;
double updateTranslateY = orgTranslateY + offsetY;
this.setTranslateX(updateTranslateX);
this.setTranslateY(updateTranslateY);
mouseDrag.consume();
}
});
}
}
Your binding needs to invalidate when either the xProperty or widthProperty are invalidated (so that anything bound to it knows to recompute). You can do this by calling the bind method in the custom binding's constructor:
private DoubleBinding centerXBinding = new DoubleBinding() {
{
bind(xProperty(), widthProperty());
}
#Override
protected double computeValue() {
return getX() + getWidth() / 2;
}
};
Note you can also do
private DoubleBinding centerXBinding = xProperty().add(widthProperty().divide(2));
The choice between the two is really just a matter of which style you prefer.
Binding to the x and width properties assumes, obviously, that you are moving the rectangle by changing one or both of those properties. If you are moving the rectangle by some other means (e.g. by changing one of its translateX or translateY properties, or by altering its list of transformations), then you need to observe the boundsInParentProperty instead:
private DoubleBinding centerXBinding = new DoubleBinding() {
{
bind(boundsInParentProperty());
}
#Override
protected double computeValue() {
Bounds bounds = getBoundsInParent();
return (bounds.getMinX() + bounds.getMaxX()) / 2 ;
}
}
This binding will give the x-coordinate of the center of the rectangle in the parent's coordinate system (which is usually the coordinate system you want).
I am trying to get a node's point to connect to another node's point depending on the orientation of the first node. The problem is, I am having trouble thinking of the way a node call tell whether it is left, right , north or south given its X and Y positions and Width and Height.
Here is an image of the problem:
I have tried comparing the X and Y values, for example, if (child.Y > parent.Y) -> snap to southPoint, but it is too vague and will snap to north or south if it is placed right or left. Any idea on more specific conditions I can use to get the correct orientation? Thank you
You could do it by defining anchor points on your box:
public class UMLClassBox {
private Point2D north;
private Point2D east;
private Point2D south;
private Point2D west;
public List<Point> getAnchorPoints() {
...
}
}
As a second step you then can have a method to find the two nodes that are closest:
public Point2D[] findClosestNodes(UMLClassBox box1, UMLClassBox box2) {
Point2D[] closest = new Point2D[2];
double shortestDistance = Double.MAX_VALUE;
for (Point2D anchorBox1 : box1.getAnchorPoints()) {
for (Point2D anchorBox2 : box2.getAnchorPoints()) {
double distance = anchorBox1.distance(anchorBox2);
if (distance < shortestDistance) {
shortestDistance = distance;
closest[0] = anchorBox1;
closest[1] = anchorBox2;
}
}
}
return closest;
}
While there is a chance that two anchor points pairs have the same distance, this should not matter in this case because either of those would be equally valid.
I want to detect if I click inside a diamond.
The only thing I have are the coordinates of the click (x,y), the center of the diamond (x,y) and the width/height of the diamond.
I found this, but the problem is different.
pixel coordinates on diamond
You can formulate a distance measure based upon the l(1) norm within which points of fixed distance from some center point form an axially aligned diamond with vertices equidistant from the center.
In this case you will need to apply a suitable affine transformation to place your diamond into a canonical form centered at the origin with the vertices of the diamond placed on the coordinate axes equidistant from the origin; call this distance r. Depending upon the form of the original diamond, this may require translation (if the diamond is not centered on the origin), rotation (if the diagonals of the diamond are not axially aligned) and scaling (if the diagonals are not of equal length) operations which form the basis of the affine transformation you will apply. You then apply this same affine transformation to your mouse click and sum the absolute value of each component of the resulting point; call this sum d. If r > d then the point lies interior to the diamond. If d > r the point lies exterior to the diamond, and if r = d the point lies on an edge of the diamond.
The answer that you linked actually contains everything you need: You can do the "Direct point position check" to detect whether a point is inside the diamond.
I assume that the diamonds can not be rotated or so, otherwise, the question would have been horribly imprecise.
Here is an MCVE, implemented in Java/Swing as an example:
The relevant part is actually the Diamond#contains method at the bottom, which consists of the 4 lines of code taken from the other answer....
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class DiamondClickTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
List<Diamond> diamonds = new ArrayList<Diamond>();
diamonds.add(new Diamond("A", new Point(100,100), 180, 140));
diamonds.add(new Diamond("B", new Point(300,100), 110, 160));
diamonds.add(new Diamond("C", new Point(100,300), 110, 180));
diamonds.add(new Diamond("D", new Point(300,300), 130, 150));
DiamondClickTestPanel p = new DiamondClickTestPanel(diamonds);
f.getContentPane().add(p);
f.setSize(400,430);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class DiamondClickTestPanel extends JPanel implements MouseMotionListener
{
private List<Diamond> diamonds;
private Diamond highlighedDiamond = null;
DiamondClickTestPanel(List<Diamond> diamonds)
{
this.diamonds = diamonds;
addMouseMotionListener(this);
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
Graphics2D g = (Graphics2D)gr;
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (Diamond diamond : diamonds)
{
draw(g, diamond);
}
}
private void draw(Graphics2D g, Diamond diamond)
{
Point2D c = diamond.getCenter();
double x0 = c.getX() + diamond.getWidth() * 0.5;
double y0 = c.getY();
double x1 = c.getX();
double y1 = c.getY() - diamond.getHeight() * 0.5;
double x2 = c.getX() - diamond.getWidth() * 0.5;
double y2 = c.getY();
double x3 = c.getX();
double y3 = c.getY() + diamond.getHeight() * 0.5;
Path2D p = new Path2D.Double();
p.moveTo(x0, y0);
p.lineTo(x1, y1);
p.lineTo(x2, y2);
p.lineTo(x3, y3);
p.closePath();
if (diamond == highlighedDiamond)
{
g.setColor(Color.RED);
g.fill(p);
}
g.setColor(Color.BLACK);
g.draw(p);
g.drawString(diamond.getName(), (int)c.getX()-4, (int)c.getY()+8);
}
#Override
public void mouseDragged(MouseEvent e)
{
}
#Override
public void mouseMoved(MouseEvent e)
{
double x = e.getX();
double y = e.getY();
highlighedDiamond = null;
for (Diamond diamond : diamonds)
{
if (diamond.contains(x, y))
{
highlighedDiamond = diamond;
}
}
repaint();
}
}
class Diamond
{
private String name;
private Point2D center;
private double width;
private double height;
Diamond(String name, Point2D center, double width, double height)
{
this.name = name;
this.center = center;
this.width = width;
this.height = height;
}
String getName()
{
return name;
}
Point2D getCenter()
{
return center;
}
double getWidth()
{
return width;
}
double getHeight()
{
return height;
}
boolean contains(double x, double y)
{
double dx = Math.abs(x - center.getX());
double dy = Math.abs(y - center.getY());
double d = dx / width + dy / height;
return d <= 0.5;
}
}