keep rectangle rotation pivot on center when resized javafx - javafx

I recently start learn javafx. With help of several posts from here i created a draggable, resizable and rotable rectangle. My plan is use it as resizing control of custom nodes. I want rotation pĂ­vot keep on rectangle center but if is resized, center changes and when i set the new center the rectangle makes a bump. Why is happenning this and how i fix it? I am sorry if made any mistake english is not my native language. Thanks in advance
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.stage.Stage;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Rotate;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
final Delta dragDelta = new Delta();
Wrapper<Point2D> mouseLocation = new Wrapper<>();
Pane root = new Pane();
Rectangle rect = new Rectangle(100,100);
final Rotate rotate = new Rotate();
{
//set pivot on rectangle center
rotate.setPivotX((rect.getX() + rect.getWidth())/2);
rotate.setPivotY((rect.getY() + rect.getHeight())/2);
}
rect.setStyle(
"-fx-stroke: blue; " +
"-fx-stroke-width: 2px; " +
"-fx-stroke-dash-array: 12 2 4 2; " +
"-fx-stroke-dash-offset: 6; " +
"-fx-stroke-line-cap: butt; " +
"-fx-fill: rgba(255, 255, 255, .0);"
);
Group group = new Group();
Circle rotateCircle = new Circle(7);
Circle topLeft = new Circle(7);
topLeft.setOnMousePressed(e->{
mouseLocation.value = new Point2D(e.getSceneX(), e.getSceneY());
});
topLeft.setOnMouseDragged(e->{
// Get the mouse deltas
double dx = e.getSceneX() - mouseLocation.value.getX();
double dy = e.getSceneY() - mouseLocation.value.getY();
dragDelta.x = e.getSceneX();
dragDelta.x = e.getSceneY();
// Get the angle in radians
double tau = - Math.toRadians(rotate.getAngle());
double sinTau = Math.sin(tau);
double cosTau = Math.cos(tau);
// Perform a rotation on dx and dy to the object coordinate
double dx_ = dx * cosTau - dy * sinTau;
double dy_ = dy * cosTau + dx * sinTau;
rect.setWidth(w(rect) - dx_);
rect.setHeight(h(rect) - dy_);
// Set save the current mouse value
mouseLocation.value = new Point2D(e.getSceneX(), e.getSceneY());
// Move the control
group.setTranslateX(group.getTranslateX() + dx);
group.setTranslateY(group.getTranslateY() + dy);
});
topLeft.setOnMouseReleased(e->{
//This is the problem
rotate.setPivotX((rect.getX() + rect.getWidth())/2);
rotate.setPivotY((rect.getY() + rect.getHeight())/2);
});
group.getTransforms().add(rotate);
rotateCircle.centerXProperty().bind(rect.xProperty().add(rect.widthProperty()).divide(2));
rotateCircle.centerYProperty().bind(rect.yProperty().subtract(25d));
topLeft.setCenterX(rect.getX());
topLeft.setCenterY(rect.getY());
group.getChildren().addAll(rect, rotateCircle, topLeft);
group.setLayoutX(100);
group.setLayoutY(100);
root.getChildren().add(group);
Util.enableDrag(rect, rotate);
Util.makeRotable(rotateCircle, rotate);
Scene scene = new Scene(root,400,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public double w(Rectangle rect) {
return Math.abs(rect.getWidth() - rect.getX());
}
public double h(Rectangle rect) {
return Math.abs(rect.getHeight() - rect.getY());
}
// Return the angle from 0 to 360
public static double deltaAngle (double x, double y, double px, double py) {
double dx = x - px;
double dy = y - py;
double angle = Math.abs(Math.toDegrees(Math.atan2(dy, dx)));
if(dy < 0) {
angle = 360 - angle;
}
return angle;
}
static class Util {
static Wrapper<Point2D> mouseLocation = new Wrapper<>();
// make a targetNode movable by dragging it around with the mouse.
static void enableDrag(Rectangle rectangle, Rotate rotate) {
final var dragDelta = new Delta();
Parent parent = rectangle.getParent();
rectangle.setOnMousePressed(mouseEvent -> {
mouseLocation.value = new Point2D(mouseEvent.getSceneX(), mouseEvent.getSceneY());
// record a delta distance for the drag and drop operation.
dragDelta.x = mouseEvent.getX() - mouseEvent.getX();
dragDelta.y = mouseEvent.getY() - mouseEvent.getY();
parent.getScene().setCursor(Cursor.MOVE);
});
rectangle.setOnMouseReleased(mouseEvent -> {
mouseLocation.value = null ;
parent.getScene().setCursor(Cursor.HAND);
parent.relocate(parent.getLayoutX() + parent.getTranslateX(),
parent.getLayoutY() + parent.getTranslateY());
parent.setTranslateX(0);
parent.setTranslateY(0);
rotate.setPivotX((rectangle.getX() + rectangle.getWidth())/2);
rotate.setPivotY((rectangle.getY() + rectangle.getHeight())/2);
});
rectangle.setOnMouseDragged(mouseEvent -> {
// Get the mouse deltas
double deltaX = mouseEvent.getSceneX() - mouseLocation.value.getX();
double deltaY = mouseEvent.getSceneY() - mouseLocation.value.getY();
parent.setTranslateX(parent.getTranslateX() + deltaX);
parent.setTranslateY(parent.getTranslateY() + deltaY);
mouseLocation.value = new Point2D(mouseEvent.getSceneX(), mouseEvent.getSceneY());
});
}
// make a targetNode movable by dragging it around with the mouse.
static void makeRotable(Circle circle, Rotate rotate) {
final var dragDelta = new Delta();
// Make it draggable
circle.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
dragDelta.x = event.getSceneX();
dragDelta.x = event.getSceneY();
});
// When it's dragged rotate the box
circle.addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
var localToScene = circle.getParent().getLocalToSceneTransform();
double x1 = dragDelta.x;
double y1 = dragDelta.y;
var x2 = event.getSceneX();
var y2 = event.getSceneY();
var px = rotate.getPivotX() + localToScene.getTx();
var py = rotate.getPivotY() + localToScene.getTy();
// Work out the angle rotated
double th1 = deltaAngle(x1, y1, px, py);
double th2 = deltaAngle(x2, y2, px, py);
var angle = rotate.getAngle();
angle += th2 - th1;
// Rotate the rectangle
rotate.setAngle(angle);
dragDelta.x = event.getSceneX();
dragDelta.y = event.getSceneY();
});
}
}
// records relative x and y co-ordinates.
private static class Delta {
double x, y;
}
static class Wrapper<T> {
T value ;
}
public static void main(String[] args) {
launch(args);
}
}

You are scaling the rectangle by setting a new width and height:
rect.setWidth(w(rect) - dx_);
rect.setHeight(h(rect) - dy_);
This maintains the origin of the rectangle but changes its center.
Instead, consider scaling the rectangle around its center:
An easier approach anyway is to use a
Scale
transformation. With this you can specify the "pivot" point about
which the scale occurs.
as explained and demonstrated in this so answer.
By using Scale.setX and Scale.setY you can apply different width and height scales.
Edit:
When changing the pivot of the Rotate it also affects rotations that were performed before the change. That is the reason for the "jump"
you see.
To demonstrate it, run the following mre, where one buttons performs a rotate and the other only changes the pivot :
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100,100);
rect.setStyle(
"-fx-stroke: blue; " +
"-fx-fill: rgba(255, 255, 255, .0);"
);
rect.setLayoutX(100);
rect.setLayoutY(100);
Rotate rotate = new Rotate();
rotate.setPivotX((rect.getX() + rect.getWidth())/2);
rotate.setPivotY((rect.getY() + rect.getHeight())/2);
rect.getTransforms().add(rotate);
Button rotateBtn = new Button("Rotate");
Button movePivotBtn = new Button("Move Rotate Pivot");
movePivotBtn.setDisable(true);
movePivotBtn.setLayoutX(60);
rotateBtn.setOnAction(e->{
rotate.setAngle(45);
rotateBtn.setDisable(true);
movePivotBtn.setDisable(false);
});
movePivotBtn.setOnAction(e->{
rotate.setPivotX(rotate.getPivotX()+15);
movePivotBtn.setDisable(true);
});
Pane root = new Pane(rotateBtn, movePivotBtn, rect);
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
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);
}
}

Rotate a polygon around its center using Rotate

I want to track my points in a Polygon class after transformations. My problem with using the Rotate class from JavaFX, is to get the points position after the rotation, by doing something like this in the Piece class:
double x = this.getPoints().get(0) + this.getLocalToParentTransform().getTx()
double y = this.getPoints().get(1) + this.getLocalToParentTransform().getTy()
It works using the Translate class, but when I rotate, the coordinates will rotate itself with the rotation with a weird pivot point, and not follow the polygon at all, unless it is rotated 360 degrees. Piece is a subclass of Polygon.
private double originalX;
private double originalY;
private double centerX;
private double centerY;
private void initializePiece(Piece piece, Pane pane, int i) {
piece.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
originalX = event.getSceneX();
originalY = event.getSceneY();
centerX = piece.getCenterX();
centerY = piece.getCenterY();
}
});
piece.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton() == MouseButton.PRIMARY) {
// Position of piece wrt. picked up position
double deltaX = event.getSceneX() - originalX;
double deltaY = event.getSceneY() - originalY;
Translate translate = new Translate();
translate.setX(deltaX);
translate.setY(deltaY);
piece.getTransforms().addAll(translate);
originalX = event.getSceneX();
originalY = event.getSceneY();
}
if (event.getButton() == MouseButton.SECONDARY) {
double deltaY = event.getSceneY() - originalY;
Rotate rotation = new Rotate(deltaY, centerX, centerY);
piece.getTransforms().add(rotation);
originalY = event.getSceneY();
}
}
});
}
And this is a snippet from the Piececlass.
public double getCenterX() {
double avg = 0;
for (int i = 0; i < this.getPoints().size(); i += 2) {
avg += this.getPoints().get(i) + this.getLocalToParentTransform().getTx();
}
avg = avg / (this.getPoints().size() / 2);
return avg;
}
public double getCenterY() {
double avg = 0;
for (int i = 1; i < this.getPoints().size(); i += 2) {
avg += this.getPoints().get(i) + this.getLocalToParentTransform().getTy();
}
avg = avg / (this.getPoints().size() / 2);
return avg;
}
All transformations are applied in local coordinates, so the pivot point should be given in the local coordinate system of the polygon, which is "unaware" of the transformations applied to it. I.e. you should just compute the center of the polygon in its own coordinate system, not its parent's coordinate system.
The mouse event's getX() and getY() methods will give the coordinates in the coordinate system of the event's source node, so using these makes the computations relatively easy. Here's a simple example (I calculated the current angle between the mouse press and the center of the polygon, and then the change in angle on dragging, instead of just using the y-coordinate; this feels more natural.)
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class DraggingTransforms extends Application {
public static void main(String[] args) {
Application.launch(args);
}
private double pressX ;
private double pressY ;
private double angle ;
#Override
public void start(Stage stage) throws Exception {
Polygon poly = new Polygon(10, 10, 50, 10, 50, 0, 70, 20, 50, 40, 50, 30, 10, 30, 10, 10);
poly.setFill(Color.web("#00b140"));
Pane root = new Pane(poly);
poly.setOnMousePressed(e -> {
pressX = e.getX();
pressY = e.getY();
angle = computeAngle(poly, e);
});
poly.setOnMouseDragged(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
poly.getTransforms().add(new Translate(e.getX() - pressX, e.getY()-pressY));
} else {
double delta = computeAngle(poly, e) - angle;
Rotate rotation = new Rotate(delta, computeCenterX(poly), computeCenterY(poly));
poly.getTransforms().add(rotation);
}
});
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private double computeAngle(Polygon poly, MouseEvent e) {
return new Point2D(computeCenterX(poly), computeCenterY(poly))
.angle(new Point2D(e.getX(), e.getY()));
}
private double computeCenter(int offset, Polygon poly) {
double total = 0 ;
for (int i = offset ; i < poly.getPoints().size(); i+=2) {
total += poly.getPoints().get(i);
}
return total / (poly.getPoints().size() / 2);
}
private double computeCenterX(Polygon poly) {
return computeCenter(0, poly);
}
private double computeCenterY(Polygon poly) {
return computeCenter(1, poly);
}
}
Note that if you do need the coordinates of a point in the shape transformed to the parent's coordinate system, all you need to do is, for example,
poly.getLocalToParentTransform()
.transform(
poly.getPoints().get(0),
poly.getPoints().get(1)
)

JavaFX 3D Rotation around Scene Fixed Axes

Creating a Virtual Trackball
Using JavaFX I want to create a virtual trackball device where X and Y mouse drag events rotate my virtual trackball in an intuitive way.
Intuitive (at least to me) means, with my scene axes being:
X increasing left to right
Y increasing top to bottom
Z increasing perpendicular to the screen towards the viewer
I want vertical mouse drag events to cause the trackball to roll around the
scene X axis, and mouse horizontal drag events to cause the trackball to
rotate around the scene Y axis.
Starting with the Oracle JavaFX SmampleApp 3D, I have modified things so my scene
comprises a fixed axis x:red, y:green, z:blue, a camera a PerspectiveCamera
trained on the axis origin, and my trackball (which, for now is a cube so we
can watch how it behaves when rotated).
Mouse dragged movement in the X direction, rotates the
trackball around the trackball's y-axis
Mouse dragged movement
in the Y direction, rotates the trackball around the
trackball's x-axis
First I Rotate the trackball 45 degress around the Y axis (by dragging the
mouse horizontally). Then if I drag the mouse vertically, the trackball
rotates about it's X axis. However, the trackball's X axis has now been
rotated through 45 degrees by the previous rotation, and I do not get the behaviour that I want, which is to rotate the trackball around the fixed X axis (i.e. the fixed red axis as it appears in my scene)
This code is based on original code from:
https://docs.oracle.com/javase/8/javafx/graphics-tutorial/sampleapp3d.htm
The code for XForm is at https://docs.oracle.com/javase/8/javafx/graphics-tutorial/sampleapp3d-code.htm#CJAGGIFG
How do I need to change my code to achieve my aims?
package moleculesampleapp;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Shape3D;
public class MoleculeSampleApp1 extends Application {
Group root = new Group();
Xform axisXForm = new Xform();
Xform boxXForm = new Xform();
Xform worldXForm = new Xform();
Xform cameraXform = new Xform();
PhongMaterial redMaterial,greenMaterial,blueMaterial;
PerspectiveCamera camera = new PerspectiveCamera(true);
private static double CAMERA_INITIAL_DISTANCE = -450;
private static double CAMERA_INITIAL_X_ANGLE = -10.0;
private static double CAMERA_INITIAL_Y_ANGLE = 0.0;
private static double CAMERA_NEAR_CLIP = 0.1;
private static double CAMERA_FAR_CLIP = 10000.0;
private static double AXIS_LENGTH = 250.0;
private static double MOUSE_SPEED = 0.1;
private static double ROTATION_SPEED = 2.0;
double mousePosX, mousePosY;
double mouseOldX, mouseOldY;
double mouseDeltaX, mouseDeltaY;
private void handleMouse(Scene scene) {
scene.setOnMousePressed(me -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged(me -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
boxXForm.ry.setAngle(boxXForm.ry.getAngle() - mouseDeltaX * MOUSE_SPEED * ROTATION_SPEED); // left right
boxXForm.rx.setAngle(boxXForm.rx.getAngle() + mouseDeltaY * MOUSE_SPEED * ROTATION_SPEED); // up down
}
});
}
private void handleKeyboard(Scene scene) {
scene.setOnKeyPressed(event -> {
switch (event.getCode()) {
case Z:
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
cameraXform.ry.setAngle(CAMERA_INITIAL_Y_ANGLE);
cameraXform.rx.setAngle(CAMERA_INITIAL_X_ANGLE);
boxXForm.reset();
break;
}
});
}
PhongMaterial createMaterial(Color diffuseColor, Color specularColor) {
PhongMaterial material = new PhongMaterial(diffuseColor);
material.setSpecularColor(specularColor);
return material;
}
#Override
public void start(Stage primaryStage) {
root.getChildren().add(worldXForm);
root.setDepthTest(DepthTest.ENABLE);
// Create materials
redMaterial = createMaterial(Color.DARKRED,Color.RED);
greenMaterial = createMaterial(Color.DARKGREEN,Color.GREEN);
blueMaterial = createMaterial(Color.DARKBLUE,Color.BLUE);
// Build Camera
root.getChildren().add(camera);
cameraXform.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
cameraXform.ry.setAngle(CAMERA_INITIAL_Y_ANGLE);
cameraXform.rx.setAngle(CAMERA_INITIAL_X_ANGLE);
// Build Axes
Box xAxis = new Box(AXIS_LENGTH, 1, 1);
Box yAxis = new Box(1, AXIS_LENGTH, 1);
Box zAxis = new Box(1, 1, AXIS_LENGTH);
xAxis.setMaterial(redMaterial);
yAxis.setMaterial(greenMaterial);
zAxis.setMaterial(blueMaterial);
axisXForm.getChildren().addAll(xAxis, yAxis, zAxis);
worldXForm.getChildren().addAll(axisXForm);
// Build shiney red box
Shape3D box = new Box(80, 80, 80);
box.setMaterial(redMaterial);
boxXForm.getChildren().add(box);
worldXForm.getChildren().addAll(boxXForm);
Scene scene = new Scene(root, 1024, 768, true);
scene.setFill(Color.GREY);
handleKeyboard(scene);
handleMouse(scene);
primaryStage.setTitle("Molecule Sample Application");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
}
public static void main(String[] args) {
launch(args);
}
}
Thanks to bronkowitz in this post here: JavaFX 3D rotations for leading me towards this solution!
package moleculesampleapp;
import javafx.application.Application;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Shape3D;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Rotate;
public class MoleculeSampleApp1 extends Application {
Group root = new Group();
XformBox cameraXform = new XformBox();
XformBox ballXForm = new XformBox();
Shape3D ball;
PhongMaterial redMaterial, greenMaterial, blueMaterial;
PerspectiveCamera camera = new PerspectiveCamera(true);
private static double CAMERA_INITIAL_DISTANCE = -450;
private static double CAMERA_INITIAL_X_ANGLE = -10.0;
private static double CAMERA_INITIAL_Y_ANGLE = 0.0;
private static double CAMERA_NEAR_CLIP = 0.1;
private static double CAMERA_FAR_CLIP = 10000.0;
private static double AXIS_LENGTH = 250.0;
private static double MOUSE_SPEED = 0.1;
private static double ROTATION_SPEED = 2.0;
double mouseStartPosX, mouseStartPosY;
double mousePosX, mousePosY;
double mouseOldX, mouseOldY;
double mouseDeltaX, mouseDeltaY;
private void handleMouse(Scene scene) {
System.out.printf("handleMouse%n");
scene.setOnMousePressed(me -> {
mouseStartPosX = me.getSceneX();
mouseStartPosY = me.getSceneY();
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged(me -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
ballXForm.addRotation(-mouseDeltaX * MOUSE_SPEED * ROTATION_SPEED, Rotate.Y_AXIS);
ballXForm.addRotation(mouseDeltaY * MOUSE_SPEED * ROTATION_SPEED, Rotate.X_AXIS);
}
});
}
private void handleKeyboard(Scene scene) {
scene.setOnKeyPressed(event -> ballXForm.reset());
}
PhongMaterial createMaterial(Color diffuseColor, Color specularColor) {
PhongMaterial material = new PhongMaterial(diffuseColor);
material.setSpecularColor(specularColor);
return material;
}
#Override
public void start(Stage primaryStage) {
System.out.printf("start%n");
root.setDepthTest(DepthTest.ENABLE);
// Create materials
redMaterial = createMaterial(Color.DARKRED, Color.RED);
greenMaterial = createMaterial(Color.DARKGREEN, Color.GREEN);
blueMaterial = createMaterial(Color.DARKBLUE, Color.BLUE);
// Build Camera
root.getChildren().add(camera);
cameraXform.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
cameraXform.addRotation(CAMERA_INITIAL_X_ANGLE, Rotate.X_AXIS);
cameraXform.addRotation(CAMERA_INITIAL_Y_ANGLE, Rotate.Y_AXIS);
// Build Axes
Box xAxis = new Box(AXIS_LENGTH, 1, 1);
Box yAxis = new Box(1, AXIS_LENGTH, 1);
Box zAxis = new Box(1, 1, AXIS_LENGTH);
xAxis.setMaterial(redMaterial);
yAxis.setMaterial(greenMaterial);
zAxis.setMaterial(blueMaterial);
root.getChildren().addAll(xAxis, yAxis, zAxis);
// Build shiney red ball
ball = new Sphere(50);
ball.setDrawMode(DrawMode.LINE); // draw mesh so we can watch how it rotates
ballXForm.getChildren().add(ball);
root.getChildren().addAll(ballXForm);
Scene scene = new Scene(root, 1024, 768, true);
scene.setFill(Color.GREY);
handleKeyboard(scene);
handleMouse(scene);
primaryStage.setTitle("TrackBall");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
}
public static void main(String[] args) {
launch(args);
}
}
class XformBox extends Group {
XformBox() {
super();
getTransforms().add(new Affine());
}
/**
* Accumulate rotation about specified axis
*
* #param angle
* #param axis
*/
public void addRotation(double angle, Point3D axis) {
Rotate r = new Rotate(angle, axis);
/**
* This is the important bit and thanks to bronkowitz in this post
* https://stackoverflow.com/questions/31382634/javafx-3d-rotations for
* getting me to the solution that the rotations need accumulated in
* this way
*/
getTransforms().set(0, r.createConcatenation(getTransforms().get(0)));
}
/**
* Reset transform to identity transform
*/
public void reset() {
getTransforms().set(0, new Affine());
}
}
If I understand your question correctly the only thing you have to do is to replace this line.
Xform cameraXform = new Xform(RotateOrder.ZYX);
This changes the rotation order of the single rotations and should give you what you need.

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.

How is set the Pivot for Shapes in JavaFX after Rotate Transformation?

I'm trying to move the upper left corner on a rotated rectangle and keep the lower right corner fixed. But after the update of the pivots of the rectangle there is a jump of the rectangle. How do I have the corners set correctly so that the jump does not occur after update the pivots? The example illustrates the problem. The green and blue rectangle are different after clicking the button! Has everone an idea? May help the 'deltaTransform' procedure?
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.NonInvertibleTransformException;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
public class RotateTest extends Application {
Rectangle rect0,rect1, rect2;
#Override
public void start(Stage primaryStage) {
rect0 = new Rectangle(100, 100, 200, 100); //start rect to compare
rect0.setFill(Color.TRANSPARENT);
rect0.setStroke(Color.GREEN);
rect0.setStrokeWidth(1.5);
rect0.setRotate(20);
rect1 = new Rectangle(100, 100, 200, 100); // moved rect
rect1.setFill(Color.TRANSPARENT);
rect1.setStroke(Color.RED);
rect1.setStrokeWidth(1.5);
rect2 = new Rectangle(100, 100, 200, 100);// unmoved rect
rect2.setFill(Color.TRANSPARENT);
rect2.setStrokeWidth(1.5);
rect2.setStroke(Color.BLUE);
Rotate rotate = new Rotate(20, rect1.getX() + rect1.getWidth() / 2., rect1.getY() + rect1.getHeight() / 2.);
rect1.getTransforms().add(rotate);
rect2.getTransforms().add(rotate);
Button btn = new Button();
btn.setText("Move to target");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
Point2D p1ns = new Point2D(150, 50); //target for upper left corner in transformed system
Point2D p1n = rotate.inverseTransform(p1ns);//target for upper left corner in nontransformed system
Point2D p2 = new Point2D(rect1.getX()+rect1.getWidth(), rect1.getY()+rect1.getHeight());
//bottom right corner in nontransformed system
Point2D p2s = rotate.transform(p2);//bottom right corner in transformed system
rect1.setX(p1n.getX());
rect1.setY(p1n.getY());
rect1.setWidth(p2.getX()-p1n.getX());
rect1.setHeight(p2.getY()-p1n.getY());
//this make the problem:
rotate.setPivotX(rect1.getX() + rect1.getWidth() / 2.);
rotate.setPivotY(rect1.getY() + rect1.getHeight() / 2.);
} catch (NonInvertibleTransformException ex) {
Logger.getLogger(RotateTest.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
Group root = new Group();
for (int i = 0; i < 10; i++) {
Line l1 = new Line(i * 100, 0, i * 100, 400);
Line l2 = new Line(0, i * 100, 1000, i * 100);
root.getChildren().addAll(l1, l2);
}
root.getChildren().addAll(btn, rect0, rect1, rect2);
Scene scene = new Scene(root, 400, 300);
primaryStage.setTitle("Rotation");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Now I have found a solution. A secondary temporary Rotate-object helps! See the code now! The green and red rectangle have the same right bottom corner!
#Override
public void handle(ActionEvent event) {
try {
Point2D p1s = new Point2D(150, 50); //target for upper left corner in transformed system
Point2D p2s = rotate.transform(rect1.getX()+rect1.getWidth(), rect1.getY()+rect1.getHeight());//bottom right corner in transformed system
Rotate rotTemp=new Rotate(rotate.getAngle(), (p1s.getX() + p2s.getX() )/ 2., (p1s.getY() + p2s.getY() )/ 2.);
Point2D q1 = rotTemp.inverseTransform(p1s);
Point2D q2 = rotTemp.inverseTransform(p2s);
rect1.setX(q1.getX());
rect1.setY(q1.getY());
rect1.setWidth(q2.getX()-q1.getX());
rect1.setHeight(q2.getY()-q1.getY());
rotate.setPivotX(rotTemp.getPivotX());
rotate.setPivotY(rotTemp.getPivotY());
} catch (NonInvertibleTransformException ex) {
Logger.getLogger(RotateTest.class.getName()).log(Level.SEVERE, null, ex);
}
}
You can always fix the bottom right of a rectangle with a change of properties of the form:
x -> x + deltaX ;
y -> y + deltaY ;
width -> width - deltaX ;
height -> height - deltaY ;
So you can do
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
Point2D targetAbsolute = new Point2D(150, 50);
Point2D targetLocal = rotate.inverseTransform(targetAbsolute);
double newX = targetLocal.getX() ;
double newY = targetLocal.getY() ;
double deltaX = newX - rect1.getX();
double deltaY = newY - rect1.getY();
rect1.setX(newX);
rect1.setY(newY);
rect1.setWidth(rect1.getWidth() - deltaX);
rect1.setHeight(rect1.getHeight() - deltaY);
} catch (NonInvertibleTransformException ex) {
Logger.getLogger(RotateTest.class.getName()).log(Level.SEVERE, null, ex);
}
}
});

Resources