How compare the orientation from one node to another - javafx

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.

Related

Preventing overlapping shapes while dragging on a Pane

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);
}

JavaFX - avoiding a retracing of common hexagon edges in a hexagonal map

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.

How to create a centerXProperty in a Rectangle to bind to?

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).

Prevent dragged circle from overlapping

As mentioned in the title, i have two Circle 's the first is draggable and the second is fixed, I would rotate (with the drag) the first one around the second without overlapping them but my Circle reacts oddly, I'm sure the error comes from the drag condition but I don't know how to solve it, that's why I need your help, here is a minimal and testable code :
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Collision extends Application{
private Pane root = new Pane();
private Scene scene;
private Circle CA = new Circle(20);
private Circle CB = new Circle(20);
private double xOffset = 0;
private double yOffset = 0;
#Override
public void start(Stage stage) throws Exception{
initCircles();
scene = new Scene(root,500,500);
stage.setScene(scene);
stage.show();
}
private void initCircles(){
CA.setCenterX(100);
CA.setCenterY(100);
CA.setFill(Color.rgb(255, 0, 0,0.2));
CA.setStroke(Color.BLACK);
CB.setCenterX(250);
CB.setCenterY(200);
CB.setFill(Color.rgb(255, 0, 0,0.2));
CB.setStroke(Color.BLACK);
CA.setOnMousePressed(evt->{
xOffset = CA.getCenterX() - evt.getSceneX();
yOffset = CA.getCenterY() - evt.getSceneY();
});
CA.setOnMouseDragged(evt->{
//get Scene coordinate from MouseEvent
drag(evt.getSceneX(),evt.getSceneY());
});
root.getChildren().addAll(CA,CB);
}
private void drag(double x, double y){
/* calculate the distance between
* the center of the first and the second circle
*/
double distance = Math.sqrt (Math.pow(CA.getCenterX() - CB.getCenterX(),2) + Math.pow(CA.getCenterY() - CB.getCenterY(),2));
if (!(distance < (CA.getRadius() + CB.getRadius()))){
CA.setCenterX(x + xOffset);
CA.setCenterY(y + yOffset);
}else{
/**************THE PROBLEM :Condition to drag************/
CA.setCenterX(CA.getCenterX() - (CB.getCenterX()-CA.getCenterX()));
CA.setCenterY(CA.getCenterY() - (CB.getCenterY()-CA.getCenterY()));
/*What condition must be established for the
* circle to behave correctly
*/
/********************************************************/
}
}
public static void main(String[] args) {
launch(args);
}
}
Here is a brief overview :
Note:
for my defense, i searched and found several subject close to mine but which have no precise or exact solution, among which:
-The circle remains blocked at the time of the collision
-Two circle that push each other
-JavaScript, Difficult to understand and convert to java
Thank you for your help !
Point2D can be interpreted as a 2D vector, and has useful methods for creating new vectors from it, etc. You can do:
private void drag(double x, double y){
// place drag wants to move circle to:
Point2D newCenter = new Point2D(x + xOffset, y+yOffset);
// center of fixed circle:
Point2D fixedCenter = new Point2D(CB.getCenterX(), CB.getCenterY());
// minimum distance between circles:
double minDistance = CA.getRadius() + CB.getRadius() ;
// if they overlap, adjust newCenter:
if (newCenter.distance(fixedCenter) < minDistance) {
// vector between fixedCenter and newCenter:
Point2D newDelta = newCenter.subtract(fixedCenter);
// adjust so that length of delta is distance between two centers:
Point2D adjustedDelta = newDelta.normalize().multiply(minDistance);
// move newCenter to match adjusted delta:
newCenter = fixedCenter.add(adjustedDelta);
}
CA.setCenterX(newCenter.getX());
CA.setCenterY(newCenter.getY());
}
Obviously, you could do all this without using Point2D and just doing the computation, but I think the API calls make the code easier to understand.

How can I animate a circle with PlayN?

This is a follow up to my last question:
How can I draw a circle to the screen with PlayN?
For my simple case, I want to programmatically create a single colored circle and move it across a 2-D plain (doesn't need to use box2d lib).
A real-world example would likely involve animating several circles. Two real-world examples for this case (sorry, I had to remove the links -- not enough karma!):
Browsmos for Chrome
Ants AI Challenge
It was suggested in response to my last question that I would want to use the ImmediateLayer class, so I am looking to understand how to properly incorporate this into my game loop.
Here's is my code sample:
public class SimpleCircleAnimation implements Game {
// Surface
private GroupLayer rootLayer;
private ImmediateLayer surface;
private Canvas canvas;
private Circle circle;
private CanvasImage circleImage;
#Override
public void init() {
// create root layer
rootLayer = graphics().rootLayer();
// a simple circle object
int circleX = 0; int circleY = 0;
int circleRadius = 20;
circle = new Circle(circleX, circleY, circleRadius);
// create an immediate layer and add to root layer
ImmediateLayer circleLayer = graphics().createImmediateLayer(new ImmediateLayer.Renderer() {
public void render (Surface surf) {
circleImage = graphics().createImage(circle.radius*2, circle.radius*2);
canvas = circleImage.canvas();
canvas.setFillColor(0xff0000eb);
canvas.fillCircle(circle.radius, circle.radius, circle.radius);
surf.drawImage(circleImage, circle.x, circle.y);
}
});
rootLayer.add(circleLayer);
}
#Override
public void paint(float alpha) {
}
#Override
public void update(float delta) {
// move circle
int newX = circle.x + 4; int newY = circle.y + 4;
circle.setPoint(newX, newY);
}
#Override
public int updateRate() {
return 25;
}
}
This successfully moves the circle diagonally down the screen from left to right. A couple questions:
Is this implemented properly?
In the case of multiple animated circles, is the idea with ImmediateLayer that you would create a circle image for each circle within the Renderer callback? Or would you perhaps create an Immediate Layer for each circle and add those to the root layer?
I would not use ImmediateLayer wiht render (Surface surf) adapter. Here u have, inside the render method creation of an image
circleImage = graphics().createImage(circle.radius*2, circle.radius*2);
just put this in the paint method
surf.drawImage(circleImage, circle.x, circle.y);
using the normal layer and u should be fine
Painting is done in paint method, and do not put calculations there
Update is for calculations, and physics oriented stuff
I discovered a detailed practical example of ImmediateLayer usage in the Cute Game source within the PlayN Samples:
CuteGame.java (code.google.com)

Resources