I am trying to build a hex grid by following along with Red Blob Games. The hex grid axial coordinate system runs from -radius < x < radius and -radius < y < radius. Red Blob defines a function that converts the int axial coordinates into a double pixel coordinate.
public Point tileToPixel(Tile tile) {
Hex hex = (Hex) tile;
double x = (orientation.getF0() * hex.getX() + orientation.getF1() * hex.getY()) * size.getX();
double y = (orientation.getF2() * hex.getX() + orientation.getF3() * hex.getY()) * size.getY();
return new Point(x, y);
}
Point is equivalent to Point2D; I decided to write my own.
orientation is an enum that defines the points of the hexagon based on a flat-top or point-top hexagon orientation.
POINT_TOP_ORIENTATION ( Math.sqrt(3.0), Math.sqrt(3.0)/2.0, 0.0, 3.0/2.0,
Math.sqrt(3.0)/3.0, -1.0/3.0, 0.0, 2.0/3.0,
0.5),
FLAT_TOP_ORIENTATION ( 3.0/2.0, 0.0, Math.sqrt(3.0)/2.0, Math.sqrt(3.0),
2.0/3.0, 0.0, -1.0/3.0, Math.sqrt(3.0)/3.0,
0.0);
private HexOrientation(double f0, double f1, double f2, double f3,
double b0, double b1, double b2, double b3,
double startAngle) {
this.f0 = f0;
this.f1 = f1;
this.f2 = f2;
this.f3 = f3;
this.b0 = b0;
this.b1 = b1;
this.b2 = b2;
this.b3 = b3;
this.startAngle = startAngle;
}
The problem becomes that the axial coordinate is converted to a pixel coordinate double that includes negative values and assumes a normal Cartesian format (positive Y up, positive X right) with the origin being (0,0) at the center.
I am using Scene Builder to construct my display. Here is my extremely basic set-up:
You can see that I have the AnchorPane as root, a BorderPane, and a Pane located in the center of the BorderPane where I intend my Hexagons to be displayed.
My Main class is defined:
public class Main extends Application {
#Override
public void start(Stage stage) {
try {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/Display.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
stage.setTitle("Grid Display");
stage.setScene(scene);
stage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
My FXML class is defined:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>
<AnchorPane xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.ScreenController">
<children>
<BorderPane layoutX="-8.0" layoutY="-69.0" prefHeight="297.0" prefWidth="610.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<center>
<Pane fx:id="nodePane" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
</center>
</BorderPane>
</children>
</AnchorPane>
Lastly, my controller class, ScreenController is defined:
public class ScreenController {
#FXML
private Pane nodePane;
#FXML
protected void initialize() {
Grid grid = new Grid(new HexScreen(HexOrientation.POINT_TOP_ORIENTATION, new Point (10,10)), new HexMap(MapShape.HEXAGON));
for(Tile tile : grid.getMap().getMap()) {
int i = 0;
Double[] points = new Double[12];
Point[] corners = grid.getScreen().polygonCorners(tile);
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
Polygon polygon = drawTile(points);
if (tile.getX() == 0 && tile.getY() == 0) {
polygon.setFill(Color.BLACK);
}
this.nodePane.getChildren().add(polygon);
}
}
private Polygon drawTile(Double[] points) {
Polygon polygon = new Polygon();
polygon.getPoints().addAll(points);
polygon.setStroke(Color.BLACK);
polygon.setFill(Color.TRANSPARENT);
return polygon;
}
}
The output of this is:
How can I set the origin of the nodePane to be in the center of the Pane rather than the top left? I would rather not recalculate the pixel coordinates via the tileToPixel() because this function is located in a project that I import as a JAR into the GridDisplay project where the actual JavaFX display is occurring. I intend to be able to use that JAR as a library independent of how I am building the GUI and thus need "universal" tile to pixel calculations and not JavaFX specific (if that makes sense).
Therefore, I think the most appropriate place to change the pixel coordinates is in the ScreenController. So far I have tried:
for(Tile tile : grid.getMap().getMap()) {
int i = 0;
Double[] points = new Double[12];
Point[] corners = grid.getScreen().polygonCorners(tile);
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
Polygon polygon = drawTile(points);
if (tile.getX() == 0 && tile.getY() == 0) {
polygon.setFill(Color.BLACK);
}
polygon.setTranslateX(nodePane.getWidth()/2);
polygon.setTranslateY(nodePane.getHeight()/2);
this.nodePane.getChildren().add(polygon);
}
The output of this looks exactly the same as the above image. I have also substituted nodePane.getWidth() and nodePane.getHeight() for:
polygon.setTranslateX(nodePane.getPrefWidth()/2);
polygon.setTranslateY(nodePane.getPrefHeight()/2);
This moves the origin slightly but not how I imagine it should look. For reference, the PrefWidth and PrefHeight for my AnchorPane, BorderPane, and Pane are all set at USE_COMPUTED_SIZE.
Finally, even if the above solution at centered my origin at the center of the Pane, I don't believe it would work if a user resized the window.
Thank you for reading my lengthy post, please let me know if you require any additional information.
#Sedrick thank you for your help.
Here is my solution.
public class ScreenController {
#FXML
private StackPane nodePane;
#FXML
protected void initialize() {
Grid grid = new Grid(new HexScreen(HexOrientation.POINT_TOP_ORIENTATION, new Point (10,10)), new HexMap(MapShape.HEXAGON));
Group group = new Group();
for(Tile tile : grid.getMap().getMap()) {
int i = 0;
Double[] points = new Double[12];
Point[] corners = grid.getScreen().polygonCorners(tile);
for(Point point : corners) {
points[i] = point.getX();
points[i+1] = point.getY();
i += 2;
}
Polygon polygon = drawTile(points);
if (tile.getX() == 0 && tile.getY() == 0) {
polygon.setFill(Color.BLACK);
}
group.getChildren().add(polygon);
}
this.nodePane.getChildren().add(group);
}
private Polygon drawTile(Double[] points) {
Polygon polygon = new Polygon();
polygon.getPoints().addAll(points);
polygon.setStroke(Color.BLACK);
polygon.setFill(Color.TRANSPARENT);
//tileHandler.hoverHandler(polygon, Color.TRANSPARENT, Color.RED);
return polygon;
}
}
I first put all the polygons (hexs) into a Group object. Then added the Group to a StackPane (instead of a Pane), as suggested above. This solution did not work if I used a Pane instead of a Group. Here is the resultant output:
I still need to mirror the grid across the X axis (because -Y is still up) and preferably allow for dynamic resizing of the grid with window resizing so the full grid is always in view.
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);
}
I'm trying to draw a curve between two nodes, which reside in different parent container. The container can be arbitrary deeply nested. I am able to transform the local coordinates into the coordinate space of the common ancestor, however the node bounds are not available, at the time the line is drawn.
In my application I create my nodes in a background thread and add them later to the scene. Therefore the computation of the line coordinates should just be triggered, when the two nodes are fully layouted (they should have a height and width).
I tried to solve the issue with a property change listener, but I cannot find a property which changes enough frequently.
Is there an event I can listen to, which gets triggered if the needed properties are set?
Thank you in advance!
I added a simplified example, which should show the problem. The line is correctly drawn, as soon as you click the "redraw"-Button.
Application Class:
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Showcase");
Scene scene = new Scene(root, 300, 300);
primaryStage.setScene(scene);
primaryStage.setResizable(false);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
sample.fxml
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.layout.*?>
<AnchorPane fx:controller="sample.Controller" xmlns:fx="http://javafx.com/fxml">
<ToolBar AnchorPane.rightAnchor="0" AnchorPane.leftAnchor="0" prefHeight="50">
<Button onAction="#redrawEdge" text="Redraw Line"/>
</ToolBar>
<AnchorPane fx:id="contentPane" AnchorPane.topAnchor="50"
AnchorPane.leftAnchor="0"
AnchorPane.rightAnchor="0"/>
</AnchorPane>
Controller:
public class Controller {
private final Rectangle node1 = new Rectangle();
private final Ellipse node2 = new Ellipse();
private final HBox parent = new HBox();
private final HBox parent2 = new HBox();
private final VBox parentParent = new VBox();
private final Line edge = new Line();
#FXML
private AnchorPane contentPane;
#FXML
public void initialize() {
// not updated as expected
node1.boundsInParentProperty().addListener((o) -> drawLine());
node2.boundsInParentProperty().addListener((o) -> drawLine());
node1.setHeight(20);
node1.setWidth(20);
node1.setFill(Color.BURLYWOOD);
node2.setRadiusX(20);
node2.setRadiusY(20);
node2.setFill(Color.DEEPSKYBLUE);
parent.setStyle("-fx-border-color: #DC143C");
parent.setPadding(new Insets(10));
parent.getChildren().add(node1);
parent2.setStyle("-fx-border-color: #5CD3A9");
parent2.setPadding(new Insets(10));
parent2.getChildren().add(node2);
parentParent.setStyle("-fx-border-color: #0336FF");
parentParent.setLayoutX(200);
parentParent.setLayoutY(50);
parentParent.setPadding(new Insets(10));
parentParent.getChildren().add(parent);
contentPane.setStyle("-fx-border-color: #4B0082;");
contentPane.setPadding(new Insets(10));
contentPane.getChildren().addAll(parentParent, parent2, edge);
}
#FXML
public void redrawEdge(ActionEvent actionEvent) {
drawLine();
}
private void drawLine() {
Bounds n1InCommonAncestor = getRelativeBounds(node1, contentPane);
Bounds n2InCommonAncestor = getRelativeBounds(node2, contentPane);
Point2D n1Center = getCenter(n1InCommonAncestor);
Point2D n2Center = getCenter(n2InCommonAncestor);
Point2D startIntersection = findIntersectionPoint(n1InCommonAncestor,
n1Center, n2Center);
Point2D endIntersection = findIntersectionPoint(n2InCommonAncestor,
n2Center, n1Center);
edge.setStartX(startIntersection.getX());
edge.setStartY(startIntersection.getY());
edge.setEndX(endIntersection.getX());
edge.setEndY(endIntersection.getY());
}
private Bounds getRelativeBounds(Node node, Node relativeTo) {
Bounds nodeBoundsInScene = node.localToScene(node.getBoundsInLocal());
return relativeTo.sceneToLocal(nodeBoundsInScene);
}
private Point2D getCenter(Bounds bounds) {
return new Point2D(bounds.getMinX() + bounds.getWidth() / 2,
bounds.getMinY() + bounds.getHeight() / 2);
}
private Point2D findIntersectionPoint(Bounds nodeBounds,
Point2D inside, Point2D outside) {
Point2D middle = outside.midpoint(inside);
double deltaX = outside.getX() - inside.getX();
double deltaY = outside.getY() - inside.getY();
if (Math.hypot(deltaX, deltaY) < 1.) {
return middle;
} else {
if (nodeBounds.contains(middle)) {
return findIntersectionPoint(nodeBounds, middle, outside);
} else {
return findIntersectionPoint(nodeBounds, inside, middle);
}
}
}
}
Creating the following should work, though you may need to consider consequences on performance in some circumstances:
Node node = ... ;
ObjectBinding<Bounds> boundsInScene = new ObjectBinding<Bounds>() {
{
bind(node.boundsInLocalProperty(), node.localToSceneTransformProperty());
}
#Override
protected Bounds computeValue() {
return node.localToScene(node.getBoundsInLocal());
}
};
You could also do something similar for the relative bounds:
Node node1 = ... ;
Node node2 = ... ;
ObjectBinding<Bounds> relativeBounds = new ObjectBinding<Bounds>() {
{
bind(
node1.boundsInLocalProperty(),
node1.localToSceneTransformProperty(),
node2.boundsInLocalProperty(),
node2.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));
}
};
Adding listeners to these, and redrawing when they change, should give you the control you need over when to redraw.
I am trying to created a custom pane, which scales it's content to the available space of the pane.
I created a demo application, which splits the Stage with a SplitPane. Each split contains one AutoScalePane (see FMXL). I would expect the AutoScalePane to shrink/grow its content according to the available space (please play with the split bar)
The content of the AutoScalePane is grouped in a Group, which should be scaled, as the AutoScalePane boundaries change.
Even though, I receive the correct bounds and can compute the right zoom ratio (check debug log), the Circle nodes are not scaled..
I assume that I made a mistake in the layoutChildren() method, but I can't see an obvious issue.
It would be great if somebody with more JavaFX experience could help me :)
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("AutoScalePane Test");
primaryStage.setScene(new Scene(root, 700, 200));
primaryStage.show();
}
}
View Controller:
public class Controller {
#FXML
public AutoScalePane scalePaneLeft;
#FXML
public AutoScalePane scalePaneRight;
#FXML
public void initialize() {
fillLeftContent();
fillRightContent();
}
private void fillLeftContent() {
Circle circle1 = new Circle(100, 300, 10);
Circle circle2 = new Circle(150, 300, 10);
Circle circle3 = new Circle(200, 300, 10);
Circle circle4 = new Circle(250, 300, 10);
scalePaneLeft.addChildren(new Node[] {circle1, circle2, circle3,
circle4});
}
private void fillRightContent() {
Circle circle1 = new Circle(100, 200, 20);
Circle circle2 = new Circle(150, 200, 20);
Circle circle3 = new Circle(200, 200, 20);
Circle circle4 = new Circle(250, 200, 20);
scalePaneRight.addChildren(new Node[] {circle1, circle2, circle3,
circle4});
}
}
FXML View:
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import sample.AutoScalePane?>
<AnchorPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml">
<SplitPane dividerPositions="0.3" orientation="HORIZONTAL" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0"
AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" style="-fx-background-color: #2c5069;">
<AutoScalePane fx:id="scalePaneLeft"
style="-fx-background-color: #943736;"/>
<AutoScalePane fx:id="scalePaneRight"
style="-fx-background-color: #d27452;"/>
</SplitPane>
</AnchorPane>
Auto-scale Pane:
/**
* Auto-scales its content according to the available space of the Pane.
* The content is always centered
*
*/
public class AutoScalePane extends Pane {
private Group content = new Group();
private Scale zoom = new Scale(1, 1);
public AutoScalePane() {
layoutBoundsProperty().addListener((o) -> {
autoScale();
});
content.scaleXProperty().bind(zoom.xProperty());
content.scaleYProperty().bind(zoom.yProperty());
getChildren().add(content);
}
/**
* Adds nodes to the AutoScalePane
*
* #param children nodes
*/
public void addChildren(Node... children) {
content.getChildren().addAll(children);
requestLayout();
}
private void autoScale() {
if (getHeight() > 0
&& getWidth() > 0
&& content.getBoundsInParent().getWidth() > 0
&& content.getBoundsInParent().getHeight() > 0) {
// scale
double scaleX = getWidth() / content.getBoundsInParent().getWidth();
double scaleY = getHeight() / content.getBoundsInParent()
.getHeight();
System.out.println("*************** DEBUG ****************");
System.out.println("Pane Width: " + getWidth());
System.out.println("Content Bounds Width: " + content
.getBoundsInParent()
.getWidth());
System.out.println("Pane Height: " + getHeight());
System.out.println("Content Bounds Height: " + content
.getBoundsInParent()
.getHeight());
System.out.println("ScaleX: " + scaleX);
System.out.println("ScaleY: " + scaleY);
double zoomFactor = Math.min(scaleX, scaleY);
zoom.setX(zoomFactor);
zoom.setY(zoomFactor);
requestLayout();
}
}
#Override
protected void layoutChildren() {
final double paneWidth = getWidth();
final double paneHeight = getHeight();
final double insetTop = getInsets().getTop();
final double insetRight = getInsets().getRight();
final double insetLeft = getInsets().getLeft();
final double insertBottom = getInsets().getBottom();
final double contentWidth = (paneWidth - insetLeft - insetRight) *
zoom.getX();
final double contentHeight = (paneHeight - insetTop - insertBottom) *
zoom.getY();
layoutInArea(content, 0, 0, contentWidth, contentHeight,
getBaselineOffset(), HPos.CENTER, VPos.CENTER);
}
}
layoutChildren is invoked on a change of the size of the node. You don't need to register a listener if you adjust the scale from the layoutChildren method.
As for the zoom: You never really modify the scale properties. You don't update the Scale anywhere but in this snippet:
double zoomFactor = Math.min(zoom.getX(), zoom.getY());
zoom.setX(zoomFactor);
zoom.setY(zoomFactor);
so zoom.getX() and zoom.getY() always return 1 which is equal to the initial scale factor.
Note that you can apply the Scale matrix to the transforms of the content node directly, but this wouldn't use the center as a pivot point of the zoom.
BTW: By extending Region instead of Pane you restrict the access to the children list to protected which prevents users from modifying it.
public class AutoScalePane extends Region {
private final Group content = new Group();
public AutoScalePane() {
content.setManaged(false); // avoid constraining the size by content
getChildren().add(content);
}
/**
* Adds nodes to the AutoScalePane
*
* #param children nodes
*/
public void addChildren(Node... children) {
content.getChildren().addAll(children);
requestLayout();
}
#Override
protected void layoutChildren() {
final Bounds groupBounds = content.getBoundsInLocal();
final double paneWidth = getWidth();
final double paneHeight = getHeight();
final double insetTop = getInsets().getTop();
final double insetRight = getInsets().getRight();
final double insetLeft = getInsets().getLeft();
final double insertBottom = getInsets().getBottom();
final double contentWidth = (paneWidth - insetLeft - insetRight);
final double contentHeight = (paneHeight - insetTop - insertBottom);
// zoom
double factorX = contentWidth / groupBounds.getWidth();
double factorY = contentHeight / groupBounds.getHeight();
double factor = Math.min(factorX, factorY);
content.setScaleX(factor);
content.setScaleY(factor);
layoutInArea(content, insetLeft, insetTop, contentWidth, contentHeight,
getBaselineOffset(), HPos.CENTER, VPos.CENTER);
}
}
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.
I am currently writing a program in JavaFX, which visualises a graph in 3D. The user should be able to translate and rotate it.
As the common rule for movement in 3D is, to move the camera instead of the whole graph, I followed this rule.
I created a StackPane, which contains two Panes (bottomPane and topPane). The bottomPane contains the SubScene, in which the graph is displayed and which uses a PerspectiveCamera. In the topPane I put a Group containing the Labels for specific nodes.
I did this setup to prevent labels being hidden by the graph, when their corresponding nodes are behind other nodes or edges.
To get the positioning of the labels right, I bind a function to the translateX/Y/Z properties of the camera, which updates the positions of the labels accordingly (rearrangeLabels()).
This function calculates the screen coordinates of the nodes and then calculates these back to the coordinates of the group containing the labels.
This works perfectly for translation (e.g. moving the graph up/down/left/right). It also works, if I rotate the camera only around the x-axis OR the y-axis.
But when I want to rotate in both axis the same time (e.g. diagonal) the labels are not moving accordingly.
I tried a lot of different approaches and came to the conclusion, that this might be a bug in JavaFX, or more precise in either the localToScreen() or screenToLocal() function.
I attached a demo program, which depicts the mentioned problem.
Just press Start and use your primary mouse button to rotate the camera.
So my question is, does anyone of you know, if this is a known bug? And if yes, is there a workaround? Or am I doing something wrong?
EDIT: I narrowed the error down to being the rotation around the Z-axis. When rotating ONLY around the z-axis, the weird behaviour happens, whereas rotating around either the X-axis or the Y-axis alone, everything moves as expected.
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class RotateCamWithLabel extends Application{
Sphere node1;
Sphere node2;
Cylinder edge;
Label nodeLabel1;
Label nodeLabel2;
Group labelGroup;
Group graphGroup;
Pane topPane;
Pane bottomPane;
PerspectiveCamera cam;
double oldPosX;
double oldPosY;
Transform camTransform = new Rotate();
#Override
public void start(Stage primaryStage) throws Exception {
// Setting up the root of the whole Scene
topPane = new Pane();
topPane.setPickOnBounds(false);
bottomPane = new Pane();
StackPane root = new StackPane();
root.getChildren().addAll(bottomPane, topPane);
Button startButton = new Button("Start");
startButton.setOnAction(event -> {
setUpSubSceneAndCam();
addLabels();
});
topPane.getChildren().add(startButton);
// Starting the Scene
Scene scene = new Scene(root, 700, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
private void setUpSubSceneAndCam() {
createGraph();
SubScene subScene = new SubScene(graphGroup, 700, 500, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.LIGHTGRAY);
cam = new PerspectiveCamera(true);
cam.setTranslateZ(-1000);
cam.setNearClip(0.1);
cam.setFarClip(Double.MAX_VALUE);
subScene.setCamera(cam);
bottomPane.getChildren().add(subScene);
createCameraLabelBinding();
subScene.setOnMousePressed(event -> {
oldPosX = event.getSceneX();
oldPosY = event.getSceneY();
});
subScene.setOnMouseDragged(event -> {
double deltaX = event.getSceneX() - oldPosX;
double deltaY = event.getSceneY() - oldPosY;
if (event.isPrimaryButtonDown()) {
rotateCamera(deltaX, deltaY);
}
oldPosX = event.getSceneX();
oldPosY = event.getSceneY();
});
}
// Rotation is done by calculating the local camera position in the subscene and out of these,
// the rotation axis is calculated as well as the degree.
// Then the camera is repositioned on the pivot point, turned based on the axis and degree, and put
// back along its local backwards vector based on the starting ditance between the camera and the pivot point
private void rotateCamera(double deltaX, double deltaY) {
// Calculate rotation-axis
Point3D leftVec = getCamLeftVector().multiply(deltaX);
Point3D upVec = getCamUpVector().multiply(deltaY);
Point3D dragVec = leftVec.add(upVec).multiply(-1);
Point3D backVec = getCamBackwardVector();
Point3D axis = dragVec.crossProduct(backVec);
//Point3D axis = Rotate.Z_AXIS; //Does not work
//Point3D axis = Rotate.Y_AXIS; //Works
//Point3D axis = Rotate.X_AXIS; //Works
Rotate r = new Rotate(dragVec.magnitude(), axis);
// set camera on pivot point
Point3D pivot = Point3D.ZERO;
Point3D camCurrent = new Point3D(cam.getTranslateX(), cam.getTranslateY(), cam.getTranslateZ());
Point3D camPivVec = pivot.subtract(camCurrent);
setCameraPosition(pivot);
// rotate camera
camTransform = r.createConcatenation(camTransform);
cam.getTransforms().setAll(camTransform);
// put camera back along the local backwards vector
double length = camPivVec.magnitude();
setCameraPosition(getCamBackwardVector().multiply(length).add(pivot));
}
private void addLabels() {
if (labelGroup != null) {
labelGroup.getChildren().remove(nodeLabel1);
labelGroup.getChildren().remove(nodeLabel2);
}
labelGroup = new Group();
topPane.getChildren().add(labelGroup);
nodeLabel1 = new Label("Hello");
nodeLabel2 = new Label("Bye");
labelGroup.getChildren().addAll(nodeLabel1, nodeLabel2);
rearrangeLabels();
}
private void createCameraLabelBinding() {
cam.translateXProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
cam.translateYProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
cam.translateZProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
}
// TODO: Here is probably a bug
// I calculate the screen coordinates of the nodes, then turn them back to local positions
// in the label-group
private void rearrangeLabels() {
Point2D screenCoord1 = node1.localToScreen(Point2D.ZERO);
Point2D screenCoord2 = node2.localToScreen(Point2D.ZERO);
Point2D groupCoord1 = labelGroup.screenToLocal(screenCoord1);
Point2D groupCoord2 = labelGroup.screenToLocal(screenCoord2);
nodeLabel1.setTranslateX(groupCoord1.getX());
nodeLabel1.setTranslateY(groupCoord1.getY());
nodeLabel2.setTranslateX(groupCoord2.getX());
nodeLabel2.setTranslateY(groupCoord2.getY());
}
private void createGraph() {
graphGroup = new Group();
node1 = new Sphere(5);
node2 = new Sphere(5);
node1.setTranslateX(-50);
node1.setTranslateY(-50);
node2.setTranslateX(150);
node2.setTranslateY(50);
edge = new Cylinder(2, 10);
connectNodesWithEdge(new Point3D(node1.getTranslateX(), node1.getTranslateY(), node1.getTranslateZ()),
new Point3D(node2.getTranslateX(), node2.getTranslateY(), node2.getTranslateZ()));
graphGroup.getChildren().addAll(node1, node2, edge);
}
private void connectNodesWithEdge(Point3D origin, Point3D target) {
Point3D yAxis = new Point3D(0, 1, 0);
Point3D diff = target.subtract(origin);
double height = diff.magnitude();
Point3D mid = target.midpoint(origin);
Translate moveToMidpoint = new Translate(mid.getX(), mid.getY(), mid.getZ());
Point3D axisOfRotation = diff.crossProduct(yAxis);
double angle = Math.acos(diff.normalize().dotProduct(yAxis));
Rotate rotateAroundCenter = new Rotate(-Math.toDegrees(angle), axisOfRotation);
this.edge.setHeight(height);
edge.getTransforms().addAll(moveToMidpoint, rotateAroundCenter);
}
private Point3D getCamLeftVector() {
Point3D left = cam.localToScene(-1, 0, 0);
Point3D current = cam.localToScene(0, 0, 0);
return left.subtract(current);
}
private Point3D getCamUpVector() {
Point3D up = cam.localToScene(0, -1, 0);
Point3D current = cam.localToScene(0, 0, 0);
return up.subtract(current);
}
private Point3D getCamBackwardVector() {
Point3D backward = cam.localToScene(0, 0, -1);
Point3D current = cam.localToScene(0, 0, 0);
return backward.subtract(current);
}
private void setCameraPosition(Point3D position) {
cam.setTranslateX(position.getX());
cam.setTranslateY(position.getY());
cam.setTranslateZ(position.getZ());
}
public static void main(String[] args) {
launch(args);
}
}