JavaFX 3D Rotation around Scene Fixed Axes - javafx

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.

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

keep rectangle rotation pivot on center when resized 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);
}
}

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.

JavaFX 3D: label positioning is wrong while rotating the camera around the Z-axis

I am currently writing a program in JavaFX, which visualises a graph in 3D. The user should be able to translate and rotate it.
As the common rule for movement in 3D is, to move the camera instead of the whole graph, I followed this rule.
I created a StackPane, which contains two Panes (bottomPane and topPane). The bottomPane contains the SubScene, in which the graph is displayed and which uses a PerspectiveCamera. In the topPane I put a Group containing the Labels for specific nodes.
I did this setup to prevent labels being hidden by the graph, when their corresponding nodes are behind other nodes or edges.
To get the positioning of the labels right, I bind a function to the translateX/Y/Z properties of the camera, which updates the positions of the labels accordingly (rearrangeLabels()).
This function calculates the screen coordinates of the nodes and then calculates these back to the coordinates of the group containing the labels.
This works perfectly for translation (e.g. moving the graph up/down/left/right). It also works, if I rotate the camera only around the x-axis OR the y-axis.
But when I want to rotate in both axis the same time (e.g. diagonal) the labels are not moving accordingly.
I tried a lot of different approaches and came to the conclusion, that this might be a bug in JavaFX, or more precise in either the localToScreen() or screenToLocal() function.
I attached a demo program, which depicts the mentioned problem.
Just press Start and use your primary mouse button to rotate the camera.
So my question is, does anyone of you know, if this is a known bug? And if yes, is there a workaround? Or am I doing something wrong?
EDIT: I narrowed the error down to being the rotation around the Z-axis. When rotating ONLY around the z-axis, the weird behaviour happens, whereas rotating around either the X-axis or the Y-axis alone, everything moves as expected.
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class RotateCamWithLabel extends Application{
Sphere node1;
Sphere node2;
Cylinder edge;
Label nodeLabel1;
Label nodeLabel2;
Group labelGroup;
Group graphGroup;
Pane topPane;
Pane bottomPane;
PerspectiveCamera cam;
double oldPosX;
double oldPosY;
Transform camTransform = new Rotate();
#Override
public void start(Stage primaryStage) throws Exception {
// Setting up the root of the whole Scene
topPane = new Pane();
topPane.setPickOnBounds(false);
bottomPane = new Pane();
StackPane root = new StackPane();
root.getChildren().addAll(bottomPane, topPane);
Button startButton = new Button("Start");
startButton.setOnAction(event -> {
setUpSubSceneAndCam();
addLabels();
});
topPane.getChildren().add(startButton);
// Starting the Scene
Scene scene = new Scene(root, 700, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
private void setUpSubSceneAndCam() {
createGraph();
SubScene subScene = new SubScene(graphGroup, 700, 500, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.LIGHTGRAY);
cam = new PerspectiveCamera(true);
cam.setTranslateZ(-1000);
cam.setNearClip(0.1);
cam.setFarClip(Double.MAX_VALUE);
subScene.setCamera(cam);
bottomPane.getChildren().add(subScene);
createCameraLabelBinding();
subScene.setOnMousePressed(event -> {
oldPosX = event.getSceneX();
oldPosY = event.getSceneY();
});
subScene.setOnMouseDragged(event -> {
double deltaX = event.getSceneX() - oldPosX;
double deltaY = event.getSceneY() - oldPosY;
if (event.isPrimaryButtonDown()) {
rotateCamera(deltaX, deltaY);
}
oldPosX = event.getSceneX();
oldPosY = event.getSceneY();
});
}
// Rotation is done by calculating the local camera position in the subscene and out of these,
// the rotation axis is calculated as well as the degree.
// Then the camera is repositioned on the pivot point, turned based on the axis and degree, and put
// back along its local backwards vector based on the starting ditance between the camera and the pivot point
private void rotateCamera(double deltaX, double deltaY) {
// Calculate rotation-axis
Point3D leftVec = getCamLeftVector().multiply(deltaX);
Point3D upVec = getCamUpVector().multiply(deltaY);
Point3D dragVec = leftVec.add(upVec).multiply(-1);
Point3D backVec = getCamBackwardVector();
Point3D axis = dragVec.crossProduct(backVec);
//Point3D axis = Rotate.Z_AXIS; //Does not work
//Point3D axis = Rotate.Y_AXIS; //Works
//Point3D axis = Rotate.X_AXIS; //Works
Rotate r = new Rotate(dragVec.magnitude(), axis);
// set camera on pivot point
Point3D pivot = Point3D.ZERO;
Point3D camCurrent = new Point3D(cam.getTranslateX(), cam.getTranslateY(), cam.getTranslateZ());
Point3D camPivVec = pivot.subtract(camCurrent);
setCameraPosition(pivot);
// rotate camera
camTransform = r.createConcatenation(camTransform);
cam.getTransforms().setAll(camTransform);
// put camera back along the local backwards vector
double length = camPivVec.magnitude();
setCameraPosition(getCamBackwardVector().multiply(length).add(pivot));
}
private void addLabels() {
if (labelGroup != null) {
labelGroup.getChildren().remove(nodeLabel1);
labelGroup.getChildren().remove(nodeLabel2);
}
labelGroup = new Group();
topPane.getChildren().add(labelGroup);
nodeLabel1 = new Label("Hello");
nodeLabel2 = new Label("Bye");
labelGroup.getChildren().addAll(nodeLabel1, nodeLabel2);
rearrangeLabels();
}
private void createCameraLabelBinding() {
cam.translateXProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
cam.translateYProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
cam.translateZProperty().addListener((observable, oldValue, newValue) -> rearrangeLabels());
}
// TODO: Here is probably a bug
// I calculate the screen coordinates of the nodes, then turn them back to local positions
// in the label-group
private void rearrangeLabels() {
Point2D screenCoord1 = node1.localToScreen(Point2D.ZERO);
Point2D screenCoord2 = node2.localToScreen(Point2D.ZERO);
Point2D groupCoord1 = labelGroup.screenToLocal(screenCoord1);
Point2D groupCoord2 = labelGroup.screenToLocal(screenCoord2);
nodeLabel1.setTranslateX(groupCoord1.getX());
nodeLabel1.setTranslateY(groupCoord1.getY());
nodeLabel2.setTranslateX(groupCoord2.getX());
nodeLabel2.setTranslateY(groupCoord2.getY());
}
private void createGraph() {
graphGroup = new Group();
node1 = new Sphere(5);
node2 = new Sphere(5);
node1.setTranslateX(-50);
node1.setTranslateY(-50);
node2.setTranslateX(150);
node2.setTranslateY(50);
edge = new Cylinder(2, 10);
connectNodesWithEdge(new Point3D(node1.getTranslateX(), node1.getTranslateY(), node1.getTranslateZ()),
new Point3D(node2.getTranslateX(), node2.getTranslateY(), node2.getTranslateZ()));
graphGroup.getChildren().addAll(node1, node2, edge);
}
private void connectNodesWithEdge(Point3D origin, Point3D target) {
Point3D yAxis = new Point3D(0, 1, 0);
Point3D diff = target.subtract(origin);
double height = diff.magnitude();
Point3D mid = target.midpoint(origin);
Translate moveToMidpoint = new Translate(mid.getX(), mid.getY(), mid.getZ());
Point3D axisOfRotation = diff.crossProduct(yAxis);
double angle = Math.acos(diff.normalize().dotProduct(yAxis));
Rotate rotateAroundCenter = new Rotate(-Math.toDegrees(angle), axisOfRotation);
this.edge.setHeight(height);
edge.getTransforms().addAll(moveToMidpoint, rotateAroundCenter);
}
private Point3D getCamLeftVector() {
Point3D left = cam.localToScene(-1, 0, 0);
Point3D current = cam.localToScene(0, 0, 0);
return left.subtract(current);
}
private Point3D getCamUpVector() {
Point3D up = cam.localToScene(0, -1, 0);
Point3D current = cam.localToScene(0, 0, 0);
return up.subtract(current);
}
private Point3D getCamBackwardVector() {
Point3D backward = cam.localToScene(0, 0, -1);
Point3D current = cam.localToScene(0, 0, 0);
return backward.subtract(current);
}
private void setCameraPosition(Point3D position) {
cam.setTranslateX(position.getX());
cam.setTranslateY(position.getY());
cam.setTranslateZ(position.getZ());
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX line/curve with arrow head

I'm creating a graph in JavaFX which is supposed to be connected by directed edges. Best would be a bicubic curve. Does anyone know how to do add the arrow heads?
The arrow heads should of course be rotated depending on the end of the curve.
Here's a simple example without the arrows:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class BasicConnection extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Group root = new Group();
// bending curve
Rectangle srcRect1 = new Rectangle(100,100,50,50);
Rectangle dstRect1 = new Rectangle(300,300,50,50);
CubicCurve curve1 = new CubicCurve( 125, 150, 125, 200, 325, 200, 325, 300);
curve1.setStroke(Color.BLACK);
curve1.setStrokeWidth(1);
curve1.setFill( null);
root.getChildren().addAll( srcRect1, dstRect1, curve1);
// steep curve
Rectangle srcRect2 = new Rectangle(100,400,50,50);
Rectangle dstRect2 = new Rectangle(200,500,50,50);
CubicCurve curve2 = new CubicCurve( 125, 450, 125, 450, 225, 500, 225, 500);
curve2.setStroke(Color.BLACK);
curve2.setStrokeWidth(1);
curve2.setFill( null);
root.getChildren().addAll( srcRect2, dstRect2, curve2);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
}
What's the best practice? Should I create a custom control or add 2 arrow controls per curve and rotate them (seems overkill to me)? Or is there a better solution?
Or does anyone know how to calculate the angle at which the cubic curve ends? I tried creating a simple small arrow and put it at the end of the curve, but it doesn't look nice if you don't rotate it slightly.
Thank you very much!
edit: Here's a solution in which I applied José's mechanism to jewelsea's cubic curve manipulator (CubicCurve JavaFX) in case someone nees it:
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeType;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
/**
* Example of how a cubic curve works, drag the anchors around to change the curve.
* Extended with arrows with the help of José Pereda: https://stackoverflow.com/questions/26702519/javafx-line-curve-with-arrow-head
* Original code by jewelsea: https://stackoverflow.com/questions/13056795/cubiccurve-javafx
*/
public class CubicCurveManipulatorWithArrows extends Application {
List<Arrow> arrows = new ArrayList<Arrow>();
public static class Arrow extends Polygon {
public double rotate;
public float t;
CubicCurve curve;
Rotate rz;
public Arrow( CubicCurve curve, float t) {
super();
this.curve = curve;
this.t = t;
init();
}
public Arrow( CubicCurve curve, float t, double... arg0) {
super(arg0);
this.curve = curve;
this.t = t;
init();
}
private void init() {
setFill(Color.web("#ff0900"));
rz = new Rotate();
{
rz.setAxis(Rotate.Z_AXIS);
}
getTransforms().addAll(rz);
update();
}
public void update() {
double size = Math.max(curve.getBoundsInLocal().getWidth(), curve.getBoundsInLocal().getHeight());
double scale = size / 4d;
Point2D ori = eval(curve, t);
Point2D tan = evalDt(curve, t).normalize().multiply(scale);
setTranslateX(ori.getX());
setTranslateY(ori.getY());
double angle = Math.atan2( tan.getY(), tan.getX());
angle = Math.toDegrees(angle);
// arrow origin is top => apply offset
double offset = -90;
if( t > 0.5)
offset = +90;
rz.setAngle(angle + offset);
}
/**
* Evaluate the cubic curve at a parameter 0<=t<=1, returns a Point2D
* #param c the CubicCurve
* #param t param between 0 and 1
* #return a Point2D
*/
private Point2D eval(CubicCurve c, float t){
Point2D p=new Point2D(Math.pow(1-t,3)*c.getStartX()+
3*t*Math.pow(1-t,2)*c.getControlX1()+
3*(1-t)*t*t*c.getControlX2()+
Math.pow(t, 3)*c.getEndX(),
Math.pow(1-t,3)*c.getStartY()+
3*t*Math.pow(1-t, 2)*c.getControlY1()+
3*(1-t)*t*t*c.getControlY2()+
Math.pow(t, 3)*c.getEndY());
return p;
}
/**
* Evaluate the tangent of the cubic curve at a parameter 0<=t<=1, returns a Point2D
* #param c the CubicCurve
* #param t param between 0 and 1
* #return a Point2D
*/
private Point2D evalDt(CubicCurve c, float t){
Point2D p=new Point2D(-3*Math.pow(1-t,2)*c.getStartX()+
3*(Math.pow(1-t, 2)-2*t*(1-t))*c.getControlX1()+
3*((1-t)*2*t-t*t)*c.getControlX2()+
3*Math.pow(t, 2)*c.getEndX(),
-3*Math.pow(1-t,2)*c.getStartY()+
3*(Math.pow(1-t, 2)-2*t*(1-t))*c.getControlY1()+
3*((1-t)*2*t-t*t)*c.getControlY2()+
3*Math.pow(t, 2)*c.getEndY());
return p;
}
}
public static void main(String[] args) throws Exception { launch(args); }
#Override public void start(final Stage stage) throws Exception {
CubicCurve curve = createStartingCurve();
Line controlLine1 = new BoundLine(curve.controlX1Property(), curve.controlY1Property(), curve.startXProperty(), curve.startYProperty());
Line controlLine2 = new BoundLine(curve.controlX2Property(), curve.controlY2Property(), curve.endXProperty(), curve.endYProperty());
Anchor start = new Anchor(Color.PALEGREEN, curve.startXProperty(), curve.startYProperty());
Anchor control1 = new Anchor(Color.GOLD, curve.controlX1Property(), curve.controlY1Property());
Anchor control2 = new Anchor(Color.GOLDENROD, curve.controlX2Property(), curve.controlY2Property());
Anchor end = new Anchor(Color.TOMATO, curve.endXProperty(), curve.endYProperty());
Group root = new Group();
root.getChildren().addAll( controlLine1, controlLine2, curve, start, control1, control2, end);
double[] arrowShape = new double[] { 0,0,10,20,-10,20 };
arrows.add( new Arrow( curve, 0f, arrowShape));
arrows.add( new Arrow( curve, 0.2f, arrowShape));
arrows.add( new Arrow( curve, 0.4f, arrowShape));
arrows.add( new Arrow( curve, 0.6f, arrowShape));
arrows.add( new Arrow( curve, 0.8f, arrowShape));
arrows.add( new Arrow( curve, 1f, arrowShape));
root.getChildren().addAll( arrows);
stage.setTitle("Cubic Curve Manipulation Sample");
stage.setScene(new Scene( root, 400, 400, Color.ALICEBLUE));
stage.show();
}
private CubicCurve createStartingCurve() {
CubicCurve curve = new CubicCurve();
curve.setStartX(100);
curve.setStartY(100);
curve.setControlX1(150);
curve.setControlY1(50);
curve.setControlX2(250);
curve.setControlY2(150);
curve.setEndX(300);
curve.setEndY(100);
curve.setStroke(Color.FORESTGREEN);
curve.setStrokeWidth(4);
curve.setStrokeLineCap(StrokeLineCap.ROUND);
curve.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
return curve;
}
class BoundLine extends Line {
BoundLine(DoubleProperty startX, DoubleProperty startY, DoubleProperty endX, DoubleProperty endY) {
startXProperty().bind(startX);
startYProperty().bind(startY);
endXProperty().bind(endX);
endYProperty().bind(endY);
setStrokeWidth(2);
setStroke(Color.GRAY.deriveColor(0, 1, 1, 0.5));
setStrokeLineCap(StrokeLineCap.BUTT);
getStrokeDashArray().setAll(10.0, 5.0);
}
}
// a draggable anchor displayed around a point.
class Anchor extends Circle {
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
}
});
setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
}
// update arrow positions
for( Arrow arrow: arrows) {
arrow.update();
}
}
});
setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
}
});
}
// records relative x and y co-ordinates.
private class Delta { double x, y; }
}
}
Since you're already dealing with shapes (curves), the best approach for the arrows is just keep adding more shapes to the group, using Path.
Based on this answer, I've added two methods: one for getting any point of the curve at a given parameter between 0 (start) and 1 (end), one for getting the tangent to the curve at that point.
With these methods now you can draw an arrow tangent to the curve at any point. And we use them to create two at the start (0) and at the end (1):
#Override
public void start(Stage primaryStage) {
Group root = new Group();
// bending curve
Rectangle srcRect1 = new Rectangle(100,100,50,50);
Rectangle dstRect1 = new Rectangle(300,300,50,50);
CubicCurve curve1 = new CubicCurve( 125, 150, 125, 225, 325, 225, 325, 300);
curve1.setStroke(Color.BLACK);
curve1.setStrokeWidth(1);
curve1.setFill( null);
double size=Math.max(curve1.getBoundsInLocal().getWidth(),
curve1.getBoundsInLocal().getHeight());
double scale=size/4d;
Point2D ori=eval(curve1,0);
Point2D tan=evalDt(curve1,0).normalize().multiply(scale);
Path arrowIni=new Path();
arrowIni.getElements().add(new MoveTo(ori.getX()+0.2*tan.getX()-0.2*tan.getY(),
ori.getY()+0.2*tan.getY()+0.2*tan.getX()));
arrowIni.getElements().add(new LineTo(ori.getX(), ori.getY()));
arrowIni.getElements().add(new LineTo(ori.getX()+0.2*tan.getX()+0.2*tan.getY(),
ori.getY()+0.2*tan.getY()-0.2*tan.getX()));
ori=eval(curve1,1);
tan=evalDt(curve1,1).normalize().multiply(scale);
Path arrowEnd=new Path();
arrowEnd.getElements().add(new MoveTo(ori.getX()-0.2*tan.getX()-0.2*tan.getY(),
ori.getY()-0.2*tan.getY()+0.2*tan.getX()));
arrowEnd.getElements().add(new LineTo(ori.getX(), ori.getY()));
arrowEnd.getElements().add(new LineTo(ori.getX()-0.2*tan.getX()+0.2*tan.getY(),
ori.getY()-0.2*tan.getY()-0.2*tan.getX()));
root.getChildren().addAll(srcRect1, dstRect1, curve1, arrowIni, arrowEnd);
primaryStage.setScene(new Scene(root, 800, 600));
primaryStage.show();
}
/**
* Evaluate the cubic curve at a parameter 0<=t<=1, returns a Point2D
* #param c the CubicCurve
* #param t param between 0 and 1
* #return a Point2D
*/
private Point2D eval(CubicCurve c, float t){
Point2D p=new Point2D(Math.pow(1-t,3)*c.getStartX()+
3*t*Math.pow(1-t,2)*c.getControlX1()+
3*(1-t)*t*t*c.getControlX2()+
Math.pow(t, 3)*c.getEndX(),
Math.pow(1-t,3)*c.getStartY()+
3*t*Math.pow(1-t, 2)*c.getControlY1()+
3*(1-t)*t*t*c.getControlY2()+
Math.pow(t, 3)*c.getEndY());
return p;
}
/**
* Evaluate the tangent of the cubic curve at a parameter 0<=t<=1, returns a Point2D
* #param c the CubicCurve
* #param t param between 0 and 1
* #return a Point2D
*/
private Point2D evalDt(CubicCurve c, float t){
Point2D p=new Point2D(-3*Math.pow(1-t,2)*c.getStartX()+
3*(Math.pow(1-t, 2)-2*t*(1-t))*c.getControlX1()+
3*((1-t)*2*t-t*t)*c.getControlX2()+
3*Math.pow(t, 2)*c.getEndX(),
-3*Math.pow(1-t,2)*c.getStartY()+
3*(Math.pow(1-t, 2)-2*t*(1-t))*c.getControlY1()+
3*((1-t)*2*t-t*t)*c.getControlY2()+
3*Math.pow(t, 2)*c.getEndY());
return p;
}
And this is what it looks like:
If you move the control points, you'll see that the arrows are already well oriented:
CubicCurve curve1 = new CubicCurve( 125, 150, 55, 285, 375, 155, 325, 300);

Resources