Creating Directed edges javafx - javafx

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

Related

Bind the Z position relative to the scene in JavaFX

I currently have a project I am working on in JavaFX that simulates our solar system. The way it is made, a planet (which extends the Sphere class) orbits on a 2D plane, meaning the Z never gets changed according to the planet object. This was done to simplify the calculations of the orbit. The path the planet goes on is then drawn on this same plane. It looks a little bit like this:
I then rotate the group that contains the planet and the orbit to get a result that looks like this:
It is done by adding a transform to the group:
Group planeteSeule = new Group(PLANETES[i]);
planeteSeule.getTransforms().addAll(
new Rotate(infoPlanetes[i].inclination, Rotate.X_AXIS),
new Rotate(infoPlanetes[i].inclination, Rotate.Y_AXIS));
The problem with the way this is done is that there is no real way to bind the Z value the planet is at according to the scene (the Z value on the planet object itself is always 0), which is something that I need to get working to be able to interact with the planet on the Z-Axis. Is there any way to bind the Z to another property according to where it is in the scene and not according to the X it has in the group?
I am trying to access this z, the one not of the planet in the group, but the one of the planet in the world.
As outlined here, "the JavaFX node picking implementation will do that for you." Starting from this example, I enlarged the Sphere, added Text, and implemented a pair of mouse listeners. Move the mouse to the red planet, and the handler shows its name; this works no matter how the group is rotated. Print the parameter t, a MouseEvent, to see the coordinates; use the PickResult explicitly, as seen here; or simply update related model elements as desired.
text = new Text(edge / 5, -edge / 3 , "");
text.setFill(Color.BLUE);
text.setFont(new Font(20));
//sphere.setOnMouseEntered(t -> System.out.println(t));
sphere.setOnMouseEntered(t -> text.setText("Mars"));
sphere.setOnMouseExited(t -> text.setText(""));
See also JavaFX: Working with JavaFX Graphics: 7 Picking.
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Sphere;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/q/72282224/230513
* #see https://stackoverflow.com/a/37755149/230513
* #see https://stackoverflow.com/a/37743539/230513
* #see https://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
private static final class Content {
private static final double WIDTH = 3;
private final Xform group = new Xform();
private final Group cube = new Group();
private final Group axes = new Group();
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private final Sphere sphere;
private final Text text;
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().addAll(c.box, c.sphere, c.text);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
double edge = 3 * size / 4;
xAxis = createBox(edge, WIDTH, WIDTH, edge);
yAxis = createBox(WIDTH, edge / 2, WIDTH, edge);
zAxis = createBox(WIDTH, WIDTH, edge / 4, edge);
box = new Box(edge, edge / 2, edge / 4);
box.setDrawMode(DrawMode.LINE);
sphere = new Sphere(24);
PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.CORAL.darker());
redMaterial.setSpecularColor(Color.CORAL);
sphere.setMaterial(redMaterial);
sphere.setTranslateX(edge / 2);
sphere.setTranslateY(-edge / 4);
sphere.setTranslateZ(-edge / 8);
text = new Text(edge / 5, -edge / 3 , "");
text.setFill(Color.BLUE);
text.setFont(new Font(20));
sphere.setOnMouseEntered(t -> text.setText("Mars"));
sphere.setOnMouseExited(t -> text.setText(""));
}
private Box createBox(double w, double h, double d, double edge) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
b.setTranslateX(-edge / 2 + w / 2);
b.setTranslateY(edge / 4 - h / 2);
b.setTranslateZ(edge / 8 - d / 2);
return b;
}
}
private static class Xform extends Group {
private final Point3D px = new Point3D(1.0, 0.0, 0.0);
private final Point3D py = new Point3D(0.0, 1.0, 0.0);
private Rotate r;
private Transform t = new Rotate();
public void rx(double angle) {
r = new Rotate(angle, px);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void ry(double angle) {
r = new Rotate(angle, py);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void rz(double angle) {
r = new Rotate(angle);
this.t = t.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("JavaFX 3D");
Scene scene = new Scene(content.group, SIZE * 2, SIZE * 2, true);
primaryStage.setScene(scene);
scene.setFill(Color.BLACK);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-2 * SIZE);
scene.setCamera(camera);
scene.setOnMousePressed((MouseEvent e) -> {
mousePosX = e.getSceneX();
mousePosY = e.getSceneY();
mouseOldX = e.getSceneX();
mouseOldY = e.getSceneY();
});
scene.setOnMouseDragged((MouseEvent e) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = e.getSceneX();
mousePosY = e.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (e.isShiftDown()) {
content.group.rz(-mouseDeltaX * 180.0 / scene.getWidth());
} else if (e.isPrimaryButtonDown()) {
content.group.rx(+mouseDeltaY * 180.0 / scene.getHeight());
content.group.ry(-mouseDeltaX * 180.0 / scene.getWidth());
} else if (e.isSecondaryButtonDown()) {
camera.setTranslateX(camera.getTranslateX() - mouseDeltaX * 0.1);
camera.setTranslateY(camera.getTranslateY() - mouseDeltaY * 0.1);
camera.setTranslateZ(camera.getTranslateZ() + mouseDeltaY);
}
});
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

lost in 3D space - tilt values (euler?) from rotation matrix (javafx affine) only works partially

it is a while ago that I asked this question:
javafx - How to apply yaw, pitch and roll deltas (not euler) to a node in respect to the nodes rotation axes instead of the scene rotation axes?
Today I want to ask, how I can get the tilt (fore-back and sideways) relative to the body (not to the room) from the rotation matrix. To make the problem understandable, I took the final code from the fantastic answer of José Pereda and basicly added a method "getEulersFromRotationMatrix". This is working a bit, but at some point freaks out.
Attached find the whole working example. The problem becomes clear with the following click path:
// right after start
tilt fore
tilt left // all right
tilt right
tilt back // all right
// right after start
turn right
turn right
turn right
tilt fore
tilt back // all right
tilt left // bang, tilt values are completely off
While the buttons move the torso as expected, the tilt values (printed out under the buttons) behave wrong at some point.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point3D;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class PuppetTestApp extends Application {
private final int width = 800;
private final int height = 500;
private XGroup torsoGroup;
private final double torsoX = 50;
private final double torsoY = 80;
private Label output = new Label();
public Parent createRobot() {
Box torso = new Box(torsoX, torsoY, 20);
torso.setMaterial(new PhongMaterial(Color.RED));
Box head = new Box(20, 20, 20);
head.setMaterial(new PhongMaterial(Color.YELLOW.darker()));
head.setTranslateY(-torsoY / 2 -10);
Box x = new Box(200, 2, 2);
x.setMaterial(new PhongMaterial(Color.BLUE));
Box y = new Box(2, 200, 2);
y.setMaterial(new PhongMaterial(Color.BLUEVIOLET));
Box z = new Box(2, 2, 200);
z.setMaterial(new PhongMaterial(Color.BURLYWOOD));
torsoGroup = new XGroup();
torsoGroup.getChildren().addAll(torso, head, x, y, z);
return torsoGroup;
}
public Parent createUI() {
HBox buttonBox = new HBox();
Button b;
buttonBox.getChildren().add(b = new Button("Exit"));
b.setOnAction( (ActionEvent arg0) -> { Platform.exit(); } );
buttonBox.getChildren().add(b = new Button("tilt fore"));
b.setOnAction(new TurnAction(torsoGroup.rx, 15) );
buttonBox.getChildren().add(b = new Button("tilt back"));
b.setOnAction(new TurnAction(torsoGroup.rx, -15) );
buttonBox.getChildren().add(b = new Button("tilt left"));
b.setOnAction(new TurnAction(torsoGroup.rz, 15) );
buttonBox.getChildren().add(b = new Button("tilt right"));
b.setOnAction(new TurnAction(torsoGroup.rz, -15) );
buttonBox.getChildren().add(b = new Button("turn left"));
b.setOnAction(new TurnAction(torsoGroup.ry, -28) ); // not 30 degree to avoid any gymbal lock problems
buttonBox.getChildren().add(b = new Button("turn right"));
b.setOnAction(new TurnAction(torsoGroup.ry, 28) ); // not 30 degree to avoid any gymbal lock problems
VBox vbox = new VBox();
vbox.getChildren().add(buttonBox);
vbox.getChildren().add(output);
return vbox;
}
class TurnAction implements EventHandler<ActionEvent> {
final Rotate rotate;
double deltaAngle;
public TurnAction(Rotate rotate, double targetAngle) {
this.rotate = rotate;
this.deltaAngle = targetAngle;
}
#Override
public void handle(ActionEvent arg0) {
addRotate(torsoGroup, rotate, deltaAngle);
}
}
private void addRotate(XGroup node, Rotate rotate, double angle) {
Affine affine = node.getTransforms().isEmpty() ? new Affine() : new Affine(node.getTransforms().get(0));
double A11 = affine.getMxx(), A12 = affine.getMxy(), A13 = affine.getMxz();
double A21 = affine.getMyx(), A22 = affine.getMyy(), A23 = affine.getMyz();
double A31 = affine.getMzx(), A32 = affine.getMzy(), A33 = affine.getMzz();
Rotate newRotateX = new Rotate(angle, new Point3D(A11, A21, A31));
Rotate newRotateY = new Rotate(angle, new Point3D(A12, A22, A32));
Rotate newRotateZ = new Rotate(angle, new Point3D(A13, A23, A33));
affine.prepend(rotate.getAxis() == Rotate.X_AXIS ? newRotateX :
rotate.getAxis() == Rotate.Y_AXIS ? newRotateY : newRotateZ);
EulerValues euler= getEulersFromRotationMatrix(affine);
output.setText(String.format("tilt fore/back=%3.0f tilt sideways=%3.0f", euler.forward, euler.leftSide));
node.getTransforms().setAll(affine);
}
public class XGroup extends Group {
public Rotate rx = new Rotate(0, Rotate.X_AXIS);
public Rotate ry = new Rotate(0, Rotate.Y_AXIS);
public Rotate rz = new Rotate(0, Rotate.Z_AXIS);
}
#Override
public void start(Stage stage) throws Exception {
Parent robot = createRobot();
SubScene subScene = new SubScene(robot, width, height, true, SceneAntialiasing.BALANCED);
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setNearClip(0.01);
camera.setFarClip(100000);
camera.setTranslateZ(-400);
subScene.setCamera(camera);
Parent ui = createUI();
StackPane combined = new StackPane(ui, subScene);
combined.setStyle("-fx-background-color: linear-gradient(to bottom, cornsilk, midnightblue);");
Scene scene = new Scene(combined, width, height);
stage.setScene(scene);
stage.show();
}
/**
* Shall return the tilt values relative to the body (not relative to the room)
* (Maybe euler angles are not the right term here, but anyway)
*/
private EulerValues getEulersFromRotationMatrix(Affine rot) {
double eulerX; // turn left/right
double eulerY; // tilt fore/back
double eulerZ; // tilt sideways
double r11 = rot.getMxx();
double r12 = rot.getMxy();
double r13 = rot.getMxz();
double r21 = rot.getMyx();
double r31 = rot.getMzx();
double r32 = rot.getMzy();
double r33 = rot.getMzz();
// used instructions from https://www.gregslabaugh.net/publications/euler.pdf
if (r31 != 1.0 && r31 != -1.0) {
eulerX = -Math.asin(r31); // already tried with the 2nd solution as well
double cosX = Math.cos(eulerX);
eulerY = Math.atan2(r32/cosX, r33/cosX);
eulerZ = Math.atan2(r21/cosX, r11/cosX);
}
else {
eulerZ = 0;
if (r31 == -1) {
eulerX = Math.PI / 2;
eulerY = Math.atan2(r12, r13);
}
else {
eulerX = -Math.PI / 2;
eulerY = Math.atan2(-r12, -r13);
}
}
return new EulerValues(
eulerY / Math.PI * 180.0,
eulerZ / Math.PI * 180.0,
-eulerX / Math.PI * 180.0);
}
public class EulerValues {
public double leftTurn;
public double forward;
public double leftSide;
public EulerValues(double forward, double leftSide, double leftTurn) {
this.forward = forward;
this.leftSide = leftSide;
this.leftTurn = leftTurn;
}
}
public static void main(String[] args) {
launch(args);
}
}
PS: This may look like I have close to no progress, but this is only because I try to reduce the question to the possible minimum. If you want to see how this stuff is embedded in my main project, you can watch this little video I just uploaded (but does not add anything to the question): https://www.youtube.com/watch?v=R3t8BIHeo7k
I think I got it by myself now: What I computed was the "default" euler angles, sometimes refered to as z x' z'', where the 1st and 3th rotation is around the same axis. But what I am looking for are the angles that can be applied to the z, y' and x'' achses (in that order) to reach the position presented by the rotation matrix. (and then ignore the z rotation).
Or even better compute the z y' x'' eulers and the z x' y'' eulers and
only use the x' and y' values.
Added:
No, that was wrong. I indeed calculated the Tait-Bryan x y z rotations. So this was not the solution.
Ok, new explanation:
The rotation axes wthat I calculate are room relative rotations (not object relative rotations), and the 2nd rotation is at the vertical axe (which I am not interested in). But because it is "in the middle", it can cancel out the 1st and 3th rotation, and this is what happens.
So the solution should be the change the rotation order, that comes out of my matrix-to-euler algorithm. But how to do this?
I just exchanged all "y" and "z":
r11 = rot.getMxx();
r12 = rot.getMxz();
r13 = rot.getMxy();
r21 = rot.getMzx();
r31 = rot.getMyx();
r32 = rot.getMyz();
r33 = rot.getMyy();
and now it really does what I want. :)

How to position node inside a rotated group at mouse event coordinates?

Given 2D scene with a node inside a group which contains a 2d rotate transformation. How do I position the node inside the group to the scene x and y coordinates of the mouse upon click?
The node that I am trying to move to the position of the click event is a circle which is located inside a group that has been rotated. The rotation happens at a pivot at the upper right corner of the group. The group has other nodes in it too.
I have been fiddling trying to achieve this for a while with no luck. It just does not position the node at the place where the click happened if the parent of the node is rotated. I have tried various techniques including the localToScene bounds with no luck.
Is there a way to do this? Thank you for your time =)
Here is some code showing a minimum verifiable example of the problem. Run it for a demo
You can drag the circle and select circles with mouse clicks. Do this to see it works fine as long as the group is not rotated.
In order to rotate the group use the left and right direction keys on your keyboard. After the group has been rotated the dragging and the mouse coordinates are no longer accurate!
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DemoBounds extends Application {
private static final int WIDTH = 600;
private static final int HEIGHT = 700;
private static final int CIRCLE_COUNT = 12;
private static final int RECTANGLE_COUNT = 3;
private static final int CIRCLE_DISTANCE = 150;
private static final int RECTANGLE_DISTANCE = 20;
private Color selectedColor = Color.RED;
private Color normalColor = Color.YELLOW;
private Rotate rotator = new Rotate();
private List<Circle> circles = new ArrayList<>();
private List<Rectangle> rectangles = new ArrayList<>();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) {
Rotate rotate = new Rotate();
Group root = new Group();
Pane pane = new Pane(root);
createRectangles();
createCircles();
root.getChildren().addAll(rectangles);
root.getChildren().addAll(circles);
root.getTransforms().add(rotate);
Scene scene = new Scene(pane, WIDTH, HEIGHT, Color.BLACK);
AddRotateControls(root);
assignActionHandling(pane);
stage.sizeToScene();
stage.setScene(scene);
stage.setTitle("Example");
stage.show();
}
private void AddRotateControls(Group root) {
root.getTransforms().add(rotator);
rotator.setPivotX(150);
rotator.setPivotY(150);
rotator.setAngle(0);
root.getScene().setOnKeyPressed(e -> {
switch(e.getCode()){
case RIGHT:
rotator.setAngle(rotator.getAngle() + 1);
break;
case LEFT:
rotator.setAngle(rotator.getAngle() - 1);
break;
default:
break;
}
});
}
private void assignActionHandling(Pane pane) {
pane.setOnMousePressed(e -> {
Circle circle = new Circle(e.getSceneX(), e.getSceneY(), 1, Color.DEEPSKYBLUE);
pane.getChildren().add(circle);
Duration duration = Duration.millis(350);
ScaleTransition scale = new ScaleTransition(duration, circle);
FadeTransition fade = new FadeTransition(duration, circle);
ParallelTransition pack = new ParallelTransition(circle, scale, fade);
scale.setFromX(1);
scale.setFromY(1);
scale.setToX(20);
scale.setToY(20);
fade.setFromValue(1);
fade.setToValue(0);
pack.setOnFinished(e2 -> {
pane.getChildren().remove(circle);
});
pack.play();
Circle selected = circles.stream().filter(c -> ((CircleData) c.getUserData()).isSelected()).findFirst().orElse(null);
if (selected != null) {
selected.setCenterX(e.getSceneX());
selected.setCenterY(e.getSceneY());
}
});
}
private void createRectangles() {
int width = 100;
int height = HEIGHT / 3;
int startX = ((WIDTH / 2) - (((width / 2) * 3) + (RECTANGLE_DISTANCE * 3))) + (RECTANGLE_DISTANCE * 2);
int startY = (HEIGHT / 2) - (height / 2);
for(int i = 0; i<RECTANGLE_COUNT; i++){
Rectangle rect = new Rectangle();
rect.setFill(Color.MEDIUMTURQUOISE);
rect.setWidth(width);
rect.setHeight(height);
rect.setX(startX);
rect.setY(startY);
rectangles.add(rect);
startX += (width + RECTANGLE_DISTANCE);
}
}
private void createCircles() {
Random randon = new Random();
int centerX = WIDTH / 2;
int centerY = HEIGHT / 2;
int minX = centerX - CIRCLE_DISTANCE;
int maxX = centerX + CIRCLE_DISTANCE;
int minY = centerY - CIRCLE_DISTANCE;
int maxY = centerY + CIRCLE_DISTANCE;
int minRadius = 10;
int maxRadius = 50;
for (int i = 0; i < CIRCLE_COUNT; i++) {
int x = minX + randon.nextInt(maxX - minX + 1);
int y = minY + randon.nextInt(maxY - minY + 1);
int radius = minRadius + randon.nextInt(maxRadius - minRadius + 1);
Circle circle = new Circle(x, y, radius, Color.ORANGE);
circle.setStroke(normalColor);
circle.setStrokeWidth(5);
circle.setUserData(new CircleData(circle, i, false));
circles.add(circle);
}
assignCircleActionHandling();
}
private double mouseX;
private double mouseY;
private void assignCircleActionHandling() {
for (Circle circle : circles) {
circle.setOnMousePressed(e -> {
mouseX = e.getSceneX() - circle.getCenterX();
mouseY = e.getSceneY() - circle.getCenterY();
((CircleData) circle.getUserData()).setSelected(true);
unselectRest(((CircleData) circle.getUserData()).getId());
});
circle.setOnMouseDragged(e -> {
double deltaX = e.getSceneX() - mouseX;
double deltaY = e.getSceneY() - mouseY;
circle.setCenterX(deltaX);
circle.setCenterY(deltaY);
});
circle.setOnMouseReleased(e -> {
e.consume();
});
}
}
private void unselectRest(int current) {
circles.stream().filter(c -> ((CircleData) c.getUserData()).getId() != current).forEach(c -> {
((CircleData) c.getUserData()).setSelected(false);
});
}
public class CircleData {
private int id;
private boolean selected;
private Circle circle;
public CircleData(Circle circle, int id, boolean selected) {
super();
this.id = id;
this.circle = circle;
this.selected = selected;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
if (selected) {
circle.setStroke(selectedColor);
} else {
circle.setStroke(normalColor);
}
}
}
}
You don't give the details of your code but there may be a problem with the pivot of your rotation. This can drive you nuts if you try to understand the rotation behaviour in some cases if you are not aware of this mechanism. Every time when you move some nodes which are attached to your group, this pivot for the rotation is recomputed which can result in unwanted effects although in some cases it is just what you want.
If you want to have full control of your rotation you should use some code similar to the one described here: http://docs.oracle.com/javafx/8/3d_graphics/overview.htm
Update:
In your method assignActionHandling modify these few lines. In order for this to work you somehow have to make root available there.
if (selected != null) {
Point2D p = root.sceneToLocal(e.getSceneX(), e.getSceneY());
selected.setCenterX(p.getX());
selected.setCenterY(p.getY());
}
The reason for you problem is that you are mixing up coordinate systems. The center points of your circles are defined relative to the root coordinate system but that is rotated with respect to pane as well as the scene. So you have to transform the scene coordinates into the local root coordinates before you set the new center of the circle.

Smooth path in javaFX

I'm drawing a Path in JavaFX and I would like to draw path (lineTo and arcTo) of this type:
Is there any simple way how to connect arcs with lines(and other arcs) to get a smooth path like this?
I don't know the perimeter of an arc, I know only the beginning point and ending point. In the picture are only half circles, I need to draw another types of arcTo as well.
The only idea, I've had so far was getting the direction of the end of the path and then counting and joining another arcTo / lineTo in this direction. However, I didn't found any method to do this as well.
A Cubic Bezier Curve is defined by four points, start, end, and two "control points" control1 and control2. It has the property that
it starts and ends at start and end
initially (i.e. at start) it is tangential to the line segment between start and control1
at end it is tangential to the line segment between control2 and end
The shape of the curve is also determined by the sizes of the line segments from start to control1 and control2 to end: roughly speaking these control the "speed" with which the line approaches the control points before turning towards the end points.
So to join two line segments with a smooth curve, you can use a cubic curve whose start is the end of the first line segment and whose end is the start of the second line segment. Compute the control points just by extending each line beyond its end (first line) or start (second line). Using the same length for the extensions will give a balanced look.
Here is an example. Run this code: drag the mouse across the pane to draw one line, then again to draw a second line, and the two lines will be connected by a cubic curve.
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class JoinLineSegmentsWithCubic extends Application {
private Line unconnectedLine = null ;
private Line currentDraggingLine = null ;
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setOnDragDetected(e -> {
currentDraggingLine = new Line(e.getX(), e.getY(), e.getX(), e.getY());
pane.getChildren().add(currentDraggingLine);
});
pane.setOnMouseDragged(e -> {
if (currentDraggingLine != null) {
currentDraggingLine.setEndX(e.getX());
currentDraggingLine.setEndY(e.getY());
}
});
pane.setOnMouseReleased(e -> {
if (currentDraggingLine != null) {
currentDraggingLine.setEndX(e.getX());
currentDraggingLine.setEndY(e.getY());
if (unconnectedLine != null) {
connect(unconnectedLine, currentDraggingLine, pane);
}
unconnectedLine = currentDraggingLine ;
currentDraggingLine = null ;
}
});
Scene scene = new Scene(pane, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void connect(Line line1, Line line2, Pane parent) {
Point2D line1Start = new Point2D(line1.getStartX(), line1.getStartY());
Point2D line1End = new Point2D(line1.getEndX(), line1.getEndY());
Point2D line2Start = new Point2D(line2.getStartX(), line2.getStartY());
Point2D line2End = new Point2D(line2.getEndX(), line2.getEndY());
double line1Length = line1End.subtract(line1Start).magnitude();
double line2Length = line2End.subtract(line2Start).magnitude();
// average length:
double aveLength = (line1Length + line2Length) / 2 ;
// extend line1 in direction of line1 for aveLength:
Point2D control1 = line1End.add(line1End.subtract(line1Start).normalize().multiply(aveLength));
// extend line2 in (reverse) direction of line2 for aveLength:
Point2D control2 = line2Start.add(line2Start.subtract(line2End).normalize().multiply(aveLength));
CubicCurve cc = new CubicCurve(
line1End.getX(), line1End.getY(),
control1.getX(), control1.getY(),
control2.getX(), control2.getY(),
line2Start.getX(), line2Start.getY());
cc.setStroke(Color.BLACK);
cc.setFill(null);
parent.getChildren().add(cc);
}
public static void main(String[] args) {
launch(args);
}
}
You can also incorporate a cubic Bezier curve into a path, using the path element CubicCurveTo. Here is an example using a Path, with vertical line segments connected by cubic bezier curves (the method generating the path will work for arbitrary line segments):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class SmoothPathWithCubicBezier extends Application {
#Override
public void start(Stage primaryStage) {
double[] points = new double[24];
for (int i = 0; i < 24 ; i+=8) {
double x = (1 + i/8) * 200 ;
points[i] = x ;
points[i+1] = 200 ;
points[i+2] = x ;
points[i+3] = 400 ;
points[i+4] = x + 100 ;
points[i+5] = 400 ;
points[i+6] = x+ 100 ;
points[i+7] = 200 ;
}
Pane pane = new Pane();
pane.getChildren().add(createPath(points));
Scene scene = new Scene(pane, 800, 800);
primaryStage.setScene(scene);
primaryStage.show();
}
// points should be an array of length a multiple of four,
// defining a set of lines {startX1, startY1, endX1, endY1, startX2, ...}
// The path will consist of the straight line segments, joined by
// cubic beziers
private Path createPath(double[] points) {
Path path = new Path();
for (int i = 0 ; i < points.length; i+=4) {
double startX = points[i];
double startY = points[i+1];
double endX = points[i+2];
double endY = points[i+3];
if (i==0) {
MoveTo moveTo = new MoveTo(startX, startY);
moveTo.setAbsolute(true);
path.getElements().add(moveTo);
} else {
double lastStartX = points[i-4];
double lastStartY = points[i-3];
double lastEndX = points[i-2];
double lastEndY = points[i-1];
double lastLength = Math.sqrt((lastEndX-lastStartX)*(lastEndX-lastStartX)
+ (lastEndY-lastStartY)*(lastEndY-lastStartY));
double length = Math.sqrt((endX-startX)*(endX-startX)
+ (endY-startY)*(endY-startY));
double aveLength = (lastLength+length)/2;
double control1X = lastEndX + (lastEndX-lastStartX)*aveLength/lastLength ;
double control1Y = lastEndY + (lastEndY-lastStartY)*aveLength/lastLength ;
double control2X = startX - (endX-startX)*aveLength/length ;
double control2Y = startY - (endY-startY)*aveLength/length ;
CubicCurveTo cct = new CubicCurveTo(control1X, control1Y, control2X, control2Y, startX, startY);
cct.setAbsolute(true);
path.getElements().add(cct);
}
LineTo lineTo = new LineTo(endX, endY);
lineTo.setAbsolute(true);
path.getElements().add(lineTo);
}
return path ;
}
public static void main(String[] args) {
launch(args);
}
}
This gives

JavaFx 8: Moving component between parents while staying in place

I need to move Nodes (mosty clipped ImageViews) between containers. During the "parent change" I have to keep the components visually in place, so from the users' perspective this reorganization should be transparent.
The schema of my scene is like this:
The Nodes I have to move are clipped, translated some effects applied to them and originally reside under the Board pane. The contents group of the desk is scaled, clipped and translated.
I would like to move them to the MoverLayer. It works fine, due to the moverLayer is bound to the Board:
moverLayer.translateXProperty().bind(board.translateXProperty());
moverLayer.translateYProperty().bind(board.translateYProperty());
moverLayer.scaleXProperty().bind(board.scaleXProperty());
moverLayer.scaleYProperty().bind(board.scaleYProperty());
moverLayer.layoutXProperty().bind(board.layoutXProperty());
moverLayer.layoutYProperty().bind(board.layoutYProperty());
so I can simply move the nodes between them:
public void start(MouseEvent me) {
board.getContainer().getChildren().remove(node);
desk.getMoverLayer().getChildren().add(node);
}
public void finish(MouseEvent me) {
desk.getMoverLayer().getChildren().remove(node);
board.getContainer().getChildren().add(node);
}
However, when moving nodes between the contents of tray and the MoverLayer it starts to get complicated. I tried to play with different coordinates (local, parent, scene, screen), but somehow it is always misplaced. It seems, that when the scale is 1.0 for desk.contents, it works to map the coordinates of translateX and translateY to screen coordinates, switch the parent and then map back the screen coordinates to local and use as translation. But with non-identical scaling, the coordinates differs (and the node moves). I also tried to map the coordinates to the common parent (desk) recursively, but works neither.
My generalized question is, what is the best practice to calculate coordinates of the same point relative to different parents?
Here is the MCVE code. Sorry, I simply couldn't make it more simple.
package hu.vissy.puzzlefx.stackoverflow;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Mcve extends Application {
// The piece to move
private Rectangle piece;
// The components representing the above structure
private Pane board;
private Group moverLayer;
private Pane trayContents;
private Pane desk;
private Group deskContents;
// The zoom scale
private double scale = 1;
// Drag info
private double startDragX;
private double startDragY;
private Point2D dragAnchor;
public Mcve() {
}
public void init(Stage primaryStage) {
// Added only for simulation
Button b = new Button("Zoom");
b.setOnAction((ah) -> setScale(0.8));
desk = new Pane();
desk.setPrefSize(800, 600);
desk.setBackground(new Background(new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
deskContents = new Group();
desk.getChildren().add(deskContents);
board = new Pane();
board.setPrefSize(700, 600);
board.setBackground(new Background(new BackgroundFill(Color.LIGHTCORAL, CornerRadii.EMPTY, Insets.EMPTY)));
// Symbolize the piece to be dragged
piece = new Rectangle();
piece.setTranslateX(500);
piece.setTranslateY(50);
piece.setWidth(50);
piece.setHeight(50);
piece.setFill(Color.BLACK);
board.getChildren().add(piece);
// Mover layer is always on top and is bound to the board (used to display the
// dragged items above all desk contents during dragging)
moverLayer = new Group();
moverLayer.translateXProperty().bind(board.translateXProperty());
moverLayer.translateYProperty().bind(board.translateYProperty());
moverLayer.scaleXProperty().bind(board.scaleXProperty());
moverLayer.scaleYProperty().bind(board.scaleYProperty());
moverLayer.layoutXProperty().bind(board.layoutXProperty());
moverLayer.layoutYProperty().bind(board.layoutYProperty());
board.setTranslateX(50);
board.setTranslateY(50);
Pane tray = new Pane();
tray.setPrefSize(400, 400);
tray.relocate(80, 80);
Pane header = new Pane();
header.setPrefHeight(30);
header.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, CornerRadii.EMPTY, Insets.EMPTY)));
trayContents = new Pane();
trayContents.setBackground(new Background(new BackgroundFill(Color.BEIGE, CornerRadii.EMPTY, Insets.EMPTY)));
VBox layout = new VBox();
layout.getChildren().addAll(header, trayContents);
VBox.setVgrow(trayContents, Priority.ALWAYS);
layout.setPrefSize(400, 400);
tray.getChildren().add(layout);
deskContents.getChildren().addAll(board, tray, moverLayer, b);
Scene scene = new Scene(desk);
// Piece is draggable
piece.setOnMousePressed((me) -> startDrag(me));
piece.setOnMouseDragged((me) -> doDrag(me));
piece.setOnMouseReleased((me) -> endDrag(me));
primaryStage.setScene(scene);
}
// Changing the scale
private void setScale(double scale) {
this.scale = scale;
// Reseting piece position and parent if needed
if (piece.getParent() != board) {
piece.setTranslateX(500);
piece.setTranslateY(50);
trayContents.getChildren().remove(piece);
board.getChildren().add(piece);
}
deskContents.setScaleX(getScale());
deskContents.setScaleY(getScale());
}
private double getScale() {
return scale;
}
private void startDrag(MouseEvent me) {
// Saving drag options
startDragX = piece.getTranslateX();
startDragY = piece.getTranslateY();
dragAnchor = new Point2D(me.getSceneX(), me.getSceneY());
// Putting the item into the mover layer -- works fine with all zoom scale level
board.getChildren().remove(piece);
moverLayer.getChildren().add(piece);
me.consume();
}
// Doing the drag
private void doDrag(MouseEvent me) {
double newTranslateX = startDragX + (me.getSceneX() - dragAnchor.getX()) / getScale();
double newTranslateY = startDragY + (me.getSceneY() - dragAnchor.getY()) / getScale();
piece.setTranslateX(newTranslateX);
piece.setTranslateY(newTranslateY);
me.consume();
}
private void endDrag(MouseEvent me) {
// For MCVE's sake I take that the drop is over the tray.
Bounds op = piece.localToScreen(piece.getBoundsInLocal());
moverLayer.getChildren().remove(piece);
// One of my several tries: mapping the coordinates till the common parent.
// I also tried to use localtoScreen -> change parent -> screenToLocal
Bounds b = localToParentRecursive(trayContents, desk, trayContents.getBoundsInLocal());
Bounds b2 = localToParentRecursive(board, desk, board.getBoundsInLocal());
trayContents.getChildren().add(piece);
piece.setTranslateX(piece.getTranslateX() + b2.getMinX() - b.getMinX() * getScale());
piece.setTranslateY(piece.getTranslateY() + b2.getMinY() - b.getMinY() * getScale());
me.consume();
}
public static Point2D localToParentRecursive(Node n, Parent parent, double x, double y) {
// For simplicity I suppose that the n node is on the path of the parent
Point2D p = new Point2D(x, y);
Node cn = n;
while (true) {
if (cn == parent) {
break;
}
p = cn.localToParent(p);
cn = cn.getParent();
}
return p;
}
public static Bounds localToParentRecursive(Node n, Parent parent, Bounds bounds) {
Point2D p = localToParentRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Well. After a lot of debugging, calculation and try I was able to find a solution.
Here is the utility function I wrote for doing the parent swap:
/**
* Change the parent of a node.
*
* <p>
* The node should have a common ancestor with the new parent.
* </p>
*
* #param item
* The node to move.
* #param newParent
* The new parent.
*/
#SuppressWarnings("unchecked")
public static void changeParent(Node item, Parent newParent) {
try {
// HAve to use reflection, because the getChildren method is protected in common ancestor of all
// parent nodes.
// Checking old parent for public getChildren() method
Parent oldParent = item.getParent();
if ((oldParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
throw new IllegalArgumentException("Old parent has no public getChildren method.");
}
// Checking new parent for public getChildren() method
if ((newParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
throw new IllegalArgumentException("New parent has no public getChildren method.");
}
// Finding common ancestor for the two parents
Parent commonAncestor = findCommonAncestor(oldParent, newParent);
if (commonAncestor == null) {
throw new IllegalArgumentException("Item has no common ancestor with the new parent.");
}
// Bounds of the item
Bounds itemBoundsInParent = item.getBoundsInParent();
// Mapping coordinates to common ancestor
Bounds boundsInParentBeforeMove = localToParentRecursive(oldParent, commonAncestor, itemBoundsInParent);
// Swapping parent
((Collection<Node>) oldParent.getClass().getMethod("getChildren").invoke(oldParent)).remove(item);
((Collection<Node>) newParent.getClass().getMethod("getChildren").invoke(newParent)).add(item);
// Mapping coordinates back from common ancestor
Bounds boundsInParentAfterMove = parentToLocalRecursive(newParent, commonAncestor, boundsInParentBeforeMove);
// Setting new translation
item.setTranslateX(
item.getTranslateX() + (boundsInParentAfterMove.getMinX() - itemBoundsInParent.getMinX()));
item.setTranslateY(
item.getTranslateY() + (boundsInParentAfterMove.getMinY() - itemBoundsInParent.getMinY()));
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new IllegalStateException("Error while switching parent.", e);
}
}
/**
* Finds the topmost common ancestor of two nodes.
*
* #param firstNode
* The first node to check.
* #param secondNode
* The second node to check.
* #return The common ancestor or null if the two node is on different
* parental tree.
*/
public static Parent findCommonAncestor(Node firstNode, Node secondNode) {
// Builds up the set of all ancestor of the first node.
Set<Node> parentalChain = new HashSet<>();
Node cn = firstNode;
while (cn != null) {
parentalChain.add(cn);
cn = cn.getParent();
}
// Iterates down through the second ancestor for common node.
cn = secondNode;
while (cn != null) {
if (parentalChain.contains(cn)) {
return (Parent) cn;
}
cn = cn.getParent();
}
return null;
}
/**
* Transitively converts the coordinates from the node to an ancestor's
* coordinate system.
*
* #param node
* The node the starting coordinates are local to.
* #param ancestor
* The ancestor to map the coordinates to.
* #param x
* The X of the point to be converted.
* #param y
* The Y of the point to be converted.
* #return The converted coordinates.
*/
public static Point2D localToParentRecursive(Node node, Parent ancestor, double x, double y) {
Point2D p = new Point2D(x, y);
Node cn = node;
while (cn != null) {
if (cn == ancestor) {
return p;
}
p = cn.localToParent(p);
cn = cn.getParent();
}
throw new IllegalStateException("The node is not a descedent of the parent.");
}
/**
* Transitively converts the coordinates of a bound from the node to an
* ancestor's coordinate system.
*
* #param node
* The node the starting coordinates are local to.
* #param ancestor
* The ancestor to map the coordinates to.
* #param bounds
* The bounds to be converted.
* #return The converted bounds.
*/
public static Bounds localToParentRecursive(Node node, Parent ancestor, Bounds bounds) {
Point2D p = localToParentRecursive(node, ancestor, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
/**
* Transitively converts the coordinates from an ancestor's coordinate
* system to the nodes local.
*
* #param node
* The node the resulting coordinates should be local to.
* #param ancestor
* The ancestor the starting coordinates are local to.
* #param x
* The X of the point to be converted.
* #param y
* The Y of the point to be converted.
* #return The converted coordinates.
*/
public static Point2D parentToLocalRecursive(Node n, Parent parent, double x, double y) {
List<Node> parentalChain = new ArrayList<>();
Node cn = n;
while (cn != null) {
if (cn == parent) {
break;
}
parentalChain.add(cn);
cn = cn.getParent();
}
if (cn == null) {
throw new IllegalStateException("The node is not a descedent of the parent.");
}
Point2D p = new Point2D(x, y);
for (int i = parentalChain.size() - 1; i >= 0; i--) {
p = parentalChain.get(i).parentToLocal(p);
}
return p;
}
/**
* Transitively converts the coordinates of the bounds from an ancestor's
* coordinate system to the nodes local.
*
* #param node
* The node the resulting coordinates should be local to.
* #param ancestor
* The ancestor the starting coordinates are local to.
* #param bounds
* The bounds to be converted.
* #return The converted coordinates.
*/
public static Bounds parentToLocalRecursive(Node n, Parent parent, Bounds bounds) {
Point2D p = parentToLocalRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
The above solution works well, but I wonder if it is the simpliest way to do the task. For the sake of generality I had to use some reflection: the getChildren() method of the Parent class is protected, only its descedents make it public if they wish, so I can't call it directly through Parent.
The use of the above utility is simple: call changeParent( node, newParent ).
This utilities gives also function to find the common ancestor of two nodes and to recursively convert coordinates through the nodes ancestal chain to and from.

Resources