Search code examples
javafxrotationcoordinatesmouseclick-eventrotatetransform

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

}

}


Solution

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