Related
How to set axis (triad) at fixed position on screen in JavaFX? I am currently developing one application in which I want to show axis (triad) at fixed position on my screen (i.e. bottom-left corner). I want rotation of axis should be in sync with the main object. Zoom and Translate operation should not be applied to axis.
But I am facing some difficulties to show axis at specific position on screen.
I have used screenToLocal method to get fixed position in scene but it only returns Point2D object which is not helpful to set 3D translate values.
Can you please give me solution for this problem?
Source code based on this example is as below:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.DepthTest;
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.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Box;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Sphere;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class TrafoTest extends Application {
final Group root = new Group();
Group axis = new Group();
final XformWorld world = new XformWorld();
final PerspectiveCamera camera = new PerspectiveCamera(true);
final XformCamera cameraXform = new XformCamera();
final XformCamera cameraXform2 = new XformCamera();
final XformCamera cameraXform3 = new XformCamera();
private static final double CAMERA_INITIAL_DISTANCE = -1000;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
private static final double MOUSE_SPEED = 1;
private static final double ROTATION_SPEED = 4.0;
private static final double TRACK_SPEED = 0.02;
double mousePosX, mousePosY, mouseOldX, mouseOldY, mouseDeltaX, mouseDeltaY;
double mouseFactorX, mouseFactorY;
Stage stage;
#Override
public void start(Stage primaryStage) {
root.getChildren().add(world);
root.setDepthTest(DepthTest.ENABLE);
buildCamera();
buildBodySystem();
Scene scene = new Scene(root, 800, 600, true);
scene.setFill(Color.GREY);
handleMouse(scene);
this.stage = primaryStage;
primaryStage.setTitle("TrafoTest");
primaryStage.setScene(scene);
primaryStage.show();
scene.setCamera(camera);
mouseFactorX = 180.0 / scene.getWidth();
mouseFactorY = 180.0 / scene.getHeight();
}
private void buildCamera() {
root.getChildren().add(cameraXform);
cameraXform.getChildren().add(cameraXform2);
cameraXform2.getChildren().add(cameraXform3);
cameraXform3.getChildren().add(camera);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
}
private void buildBodySystem() {
PhongMaterial whiteMaterial = new PhongMaterial();
whiteMaterial.setDiffuseColor(Color.WHITE);
whiteMaterial.setSpecularColor(Color.LIGHTBLUE);
Box box = new Box(400, 200, 100);
box.setMaterial(whiteMaterial);
box.setDrawMode(DrawMode.LINE);
PhongMaterial redMaterial = new PhongMaterial();
redMaterial.setDiffuseColor(Color.DARKRED);
redMaterial.setSpecularColor(Color.RED);
Sphere sphere = new Sphere(5);
sphere.setMaterial(redMaterial);
sphere.setTranslateX(200.0);
sphere.setTranslateY(-100.0);
sphere.setTranslateZ(-50.0);
axis = drawReferenceFrame();
world.getChildren().addAll(axis);
world.getChildren().add(box);
world.getChildren().addAll(sphere);
}
private void handleMouse(Scene scene) {
scene.setOnMousePressed((MouseEvent me) -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged((MouseEvent me) -> {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
if (me.isPrimaryButtonDown()) {
cameraXform.ry(mouseDeltaX * 180.0 / scene.getWidth());
cameraXform.rx(-mouseDeltaY * 180.0 / scene.getHeight());
BoundingBox point = (BoundingBox) root.screenToLocal(new BoundingBox(root.getLayoutX()+350, root.getLayoutY()+650, 0, 0,0, 20));
System.out.println(point);
axis.setTranslateX(point.getMinX());
axis.setTranslateY(point.getMinY());
axis.setTranslateZ(point.getMinZ());
} else if (me.isSecondaryButtonDown()) {
cameraXform2.setTx((cameraXform2.t.getX() + (-mouseDeltaX)*MOUSE_SPEED*TRACK_SPEED));
cameraXform2.setTy((cameraXform2.t.getY() + (-mouseDeltaY)*MOUSE_SPEED*TRACK_SPEED));
camera.setTranslateZ(camera.getTranslateZ() + mouseDeltaY);
}
});
scene.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double z = cameraXform3.getTranslateZ();
double newZ = z - event.getDeltaY() * MOUSE_SPEED * 0.05;
cameraXform3.setTranslateZ(newZ);
}
});
}
public static void main(String[] args) {
launch(args);
}
private Group drawReferenceFrame(){
Group G1= new Group();
Cylinder CX = new Cylinder(2,25);
Cylinder CY = new Cylinder(2,25);
Cylinder CZ = new Cylinder(2,25);
Sphere S = new Sphere(4);
Material mat =new PhongMaterial(Color.WHITE);
PhongMaterial Xmat = new PhongMaterial();
Xmat.setDiffuseColor(Color.GREEN);
PhongMaterial Ymat = new PhongMaterial();
Ymat.setDiffuseColor(Color.BLUE);
PhongMaterial Zmat = new PhongMaterial();
Zmat.setDiffuseColor(Color.RED);
S.setMaterial(Zmat);
CY.setMaterial(mat);
// CY.setRotationAxis(Rotate.X_AXIS);
// CY.setRotate(90);
CY.setTranslateY(-12.5);
CX.setMaterial(mat);
CX.setTranslateX(15);
CX.setRotationAxis(Rotate.Z_AXIS);
CX.setRotate(90);
CZ.setMaterial(mat);
CZ.setRotationAxis(Rotate.X_AXIS);
CZ.setRotate(90);
CZ.setTranslateZ(-12.5);
G1.getChildren().addAll(CX,CY,CZ,S);
TriangleMesh coneMeshY = createCone(3.5f, 7.5f);
TriangleMesh coneMeshX = createCone(3.5f, 7.5f);
TriangleMesh coneMeshZ = createCone(3.5f, 7.5f);
MeshView yCone = new MeshView(coneMeshY);
MeshView xCone = new MeshView(coneMeshX);
MeshView zCone = new MeshView(coneMeshZ);
yCone.setMaterial(Ymat);
yCone.setTranslateY(-32.5);
yCone.setDrawMode(DrawMode.FILL);
xCone.setMaterial(Xmat);
xCone.setTranslateY(-3.75);
xCone.setRotationAxis(Rotate.Z_AXIS);
xCone.setRotate(90);
xCone.setTranslateX(28.5);
xCone.setDrawMode(DrawMode.FILL);
zCone.setRotationAxis(Rotate.X_AXIS);
zCone.setTranslateY(-3.75);
zCone.setRotate(90);
zCone.setTranslateZ(-28.5);
zCone.setDrawMode(DrawMode.FILL);
zCone.setMaterial(Zmat);
G1.getChildren().addAll(xCone,yCone,zCone);
// G1.setScale(0.45);
return G1;
}
private TriangleMesh createCone( float radius, float height) {
int divisions=500;
TriangleMesh mesh = new TriangleMesh();
mesh.getPoints().addAll(0,0,0);
double segment_angle = 2.0 * Math.PI / divisions;
float x, z;
double angle;
double halfCount = (Math.PI / 2 - Math.PI / (divisions / 2));
for(int i=divisions+1;--i >= 0; ) {
angle = segment_angle * i;
x = (float)(radius * Math.cos(angle - halfCount));
z = (float)(radius * Math.sin(angle - halfCount));
mesh.getPoints().addAll(x,height,z);
}
mesh.getPoints().addAll(0,height,0);
mesh.getTexCoords().addAll(0,0);
for(int i=1;i<=divisions;i++) {
mesh.getFaces().addAll(
0,0,i+1,0,i,0, //COunter clock wise
divisions+2,0,i,0,i+1,0 // Clock wise
);
}
return mesh;
}
}
class XformWorld extends Group {
final Translate t = new Translate(0.0, 0.0, 0.0);
final Rotate rx = new Rotate(0, 0, 0, 0, Rotate.X_AXIS);
final Rotate ry = new Rotate(0, 0, 0, 0, Rotate.Y_AXIS);
final Rotate rz = new Rotate(0, 0, 0, 0, Rotate.Z_AXIS);
public XformWorld() {
super();
this.getTransforms().addAll(t, rx, ry, rz);
}
}
class XformCamera extends Group {
Point3D px = new Point3D(1.0, 0.0, 0.0);
Point3D py = new Point3D(0.0, 1.0, 0.0);
Rotate r;
Transform tx = new Rotate();
Translate t = new Translate();
public XformCamera() {
super();
}
public void rx(double angle) {
r = new Rotate(angle, px);
this.tx = tx.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(tx);
}
public void ry(double angle) {
r = new Rotate(angle, py);
this.tx = tx.createConcatenation(r);
this.getTransforms().clear();
this.getTransforms().addAll(tx);
}
public void setTx(double x) {
t.setX(x);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
public void setTy(double y) {
t.setY(y);
this.getTransforms().clear();
this.getTransforms().addAll(t);
}
}
In above code, first I have translated the axis at left-bottom corner of my screen, but after rotating the main object (ie. BOX and SPHERE), axis is also translated.I wanted to rotate main object and axis about their own origins.
If I tried to zoom in (changed camera Z position), then also I axis location changed with respect to screen.
In above cases, I wanted to display my axis at left-bottom corner of the screen and Rotation of the axis should be in sync with the main object.
I wanted the axis to be fixed at left, bottom corner of the screen and axis should rotate about it's own origin.
This variation leaves the axes at the origin, moves the box to
P = (size / 2, -size / 2, -size / 2)
relative to the axes, and pans the camera toward the bottom, center of the screen. Uncomment the call to camera.setTranslateX() to pan left. Moving the mouse causes the group to rotate about the axes' origin. Press shift to rotate about z, and use the mouse wheel to dolly the camera.
import javafx.application.Application;
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.transform.Rotate;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/a/37734966/230513
* #see http://stackoverflow.com/a/37714700/230513
* #see http://stackoverflow.com/a/37685167/230513
* #see http://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.group.getChildren().addAll(c.box, c.xAxis, c.yAxis, c.zAxis);
c.group.getTransforms().addAll(c.rz, c.ry, c.rx);
return c;
}
private Content(double size) {
xAxis = createBox(size * 2, WIDTH, WIDTH);
yAxis = createBox(WIDTH, size * 2, WIDTH);
zAxis = createBox(WIDTH, WIDTH, size * 2);
double edge = 3 * size / 4;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
box.setTranslateX(size / 2);
box.setTranslateY(-size / 2);
box.setTranslateZ(-size / 2);
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#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);
scene.setOnMouseMoved((final MouseEvent e) -> {
if (e.isShiftDown()) {
content.rz.setAngle(e.getSceneX() * 360 / scene.getWidth());
} else {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
}
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
//camera.setTranslateX(SIZE / 2);
camera.setTranslateY(-SIZE / 2);
camera.setTranslateZ(-4.5 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
It may be easier to have separate groups for the rotatable content versus the fixed triad. In the example below, cube contains a single child, box, and three transforms, rx, ry and rz. In contrast, axes contains three axes with no transforms. The axes are translated to the lower left corner as a group, leaving the mouse handler free to manipulate the Rotate instances belonging to cube.
import javafx.application.Application;
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.transform.Rotate;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/a/37685167/230513
* #see http://stackoverflow.com/a/37370840/230513
*/
public class TriadBox extends Application {
private static final double SIZE = 300;
private final Content content = Content.create(SIZE);
private static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Group cube = new Group();
private final Group axes = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().add(c.box);
c.cube.getTransforms().addAll(c.rz, c.ry, c.rx);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.axes.setTranslateX(-size / 2);
c.axes.setTranslateY(size / 2);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
xAxis = createBox(size*2, WIDTH, WIDTH);
yAxis = createBox(WIDTH, size*2, WIDTH);
zAxis = createBox(WIDTH, WIDTH, size*2);
double edge = 3 * SIZE / 4;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#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);
scene.setOnMouseMoved((final MouseEvent e) -> {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-3 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
How to set axis (triad) at fixed position…I also wanted axis rotation in sync with box.
In this example, the Content.create() factory method translates the triad of axes is to the point
P = (-size / 2, size / 2, 0)
Because its separate group has no transforms, the triad's origin "sticks" to that point in the space viewed by the camera.
In contrast, the example below translates the triad of axes is to the point
P = (-size / 2, size / 2, size / 2)
Because the axes are in the same group as box, they undergo the same rotations.
import javafx.application.Application;
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.transform.Rotate;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/37714700/230513
* #see https://stackoverflow.com/a/37685167/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 static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.group.getChildren().addAll(c.box, c.xAxis, c.yAxis, c.zAxis);
c.group.getTransforms().addAll(c.rz, c.ry, c.rx);
return c;
}
private Content(double size) {
xAxis = createBox(size * 2, WIDTH, WIDTH);
xAxis.setTranslateY(size / 2);
xAxis.setTranslateZ(size / 2);
yAxis = createBox(WIDTH, size * 2, WIDTH);
yAxis.setTranslateX(-size / 2);
yAxis.setTranslateZ(size / 2);
zAxis = createBox(WIDTH, WIDTH, size * 2);
zAxis.setTranslateX(-size / 2);
zAxis.setTranslateY(size / 2);
double edge = 3 * size / 4;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#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);
scene.setOnMouseMoved((final MouseEvent e) -> {
if (e.isShiftDown()) {
content.rz.setAngle(e.getSceneX() * 360 / scene.getWidth());
} else {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
}
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-3.5 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I wanted to rotate both main object and axis about their own center.
For that, I would return to the original example that has separate groups for the content and triad. The variation below adds the same threes Rotate transforms to both cube and axes. As a result, the setOnMouseMoved() implementation causes the the two to rotate in synchrony. Mouse over the stage to see the effect.
import javafx.application.Application;
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.transform.Rotate;
import javafx.stage.Stage;
/**
* #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 static final class Content {
private static final double WIDTH = 3;
private final Group group = new Group();
private final Group cube = new Group();
private final Group axes = new Group();
private final Rotate rx = new Rotate(0, Rotate.X_AXIS);
private final Rotate ry = new Rotate(0, Rotate.Y_AXIS);
private final Rotate rz = new Rotate(0, Rotate.Z_AXIS);
private final Box xAxis;
private final Box yAxis;
private final Box zAxis;
private final Box box;
private static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().add(c.box);
c.cube.getTransforms().addAll(c.rz, c.ry, c.rx);
c.cube.setTranslateX(size / 2);
c.axes.getChildren().addAll(c.xAxis, c.yAxis, c.zAxis);
c.axes.getTransforms().addAll(c.rz, c.ry, c.rx);
c.axes.setTranslateX(-size / 2);
c.group.getChildren().addAll(c.cube, c.axes);
return c;
}
private Content(double size) {
xAxis = createBox(size, WIDTH, WIDTH);
yAxis = createBox(WIDTH, size, WIDTH);
zAxis = createBox(WIDTH, WIDTH, size);
double edge = 3 * size / 5;
box = new Box(edge, edge, edge);
box.setMaterial(new PhongMaterial(Color.CORAL));
}
private Box createBox(double w, double h, double d) {
Box b = new Box(w, h, d);
b.setMaterial(new PhongMaterial(Color.AQUA));
return b;
}
}
#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);
scene.setOnMouseMoved((final MouseEvent e) -> {
if (e.isShiftDown()) {
content.rz.setAngle(e.getSceneX() * 360 / scene.getWidth());
} else {
content.rx.setAngle(e.getSceneY() * 360 / scene.getHeight());
content.ry.setAngle(e.getSceneX() * 360 / scene.getWidth());
}
});
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.setFarClip(SIZE * 6);
camera.setTranslateZ(-4 * SIZE);
scene.setCamera(camera);
scene.setOnScroll((final ScrollEvent e) -> {
camera.setTranslateZ(camera.getTranslateZ() + e.getDeltaY());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
In order to see what you were seeing, I made these changes to TrafoTest and set the scene to black.
I then modified this example to make the scene look more like yours, adding a sphere on one corner and shifting the axes to the corner opposite. I substituted a variation of your custom rotation Group and your mouse handler from here. The new group also rotates around z when isShiftDown(). Also note the slightly simpler camera dolly in the setOnScroll() handler. In particular, note that the axes can be translated independently as a group. Try this variation in Content.create().
c.axes.setTranslateX(c.axes.getTranslateX() - 20);
c.axes.setTranslateY(c.axes.getTranslateY() + 20);
c.axes.setTranslateZ(c.axes.getTranslateZ() + 20);
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.transform.Rotate;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
/**
* #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 static Content create(double size) {
Content c = new Content(size);
c.cube.getChildren().addAll(c.box, c.sphere);
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(8);
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);
}
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);
}
}
Currently experimenting with ScalaFX a bit.
Imagine the following:
I have some nodes and they are connected by some edges.
Now when I click the mousebutton I want to select the ones next to the mouse click, e.g. if I click between 1 and 2, I want those two to be selected, if I click before 0, only that one (as it's the first) etc.
Currently (and just as a proof of concept) I am doing this by adding in some helper structures. I have a HashMap of type [Index, Node] and select them like so:
wrapper.onMouseClicked = (mouseEvent: MouseEvent) =>
{
val lowerIndex: Int = (mouseEvent.sceneX).toString.charAt(0).asDigit
val left = nodes.get(lowerIndex)
val right = nodes.get(lowerIndex+1)
left.get.look.setStyle("-fx-background-color: orange;")
right.get.look.setStyle("-fx-background-color: orange;")
}
this does it's just, but I need to have an additional datastructure and it will get really tedious in 2D, like when I have a Y coordinate as well.
What I would prefer would be some method like mentioned in
How to detect Node at specific point in JavaFX?
or
JavaFX 2.2 get node at coordinates (visual tree hit testing)
These questions are based on older versions of JavaFX and use deprecated methods.
I could not find any replacement or solution in ScalaFX 8 so far. Is there a nice way to get all the nodes within a certain radius?
So "Nearest neighbor search" is the general problem you are trying to solve.
Your problem statement is a bit short on details. E.g., are nodes equidistant from each other? are nodes arranged in a grid pattern or randomly? is the node distance modeled based upon a point at the node center, a surrounding box, the actual closest point on an arbitrarily shaped node? etc.
I'll assume randomly placed shapes that may overlap, and picking is not based upon painting order, but on the closest corners of the bounding boxes of shapes. A more accurate picker might work by comparing the clicked point against against an elliptical area surrounding the actual shape rather than the shapes bounding box (as the current picker will be a bit finicky to use for things like overlapping diagonal lines).
A k-d tree algorithm or an R-tree could be used, but in general a linear brute force search will probably just work fine for most applications.
Sample brute force solution algorithm
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[] {
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare: corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
Executable Solution
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.*;
import java.util.Random;
public class FindNearest extends Application {
private static final int N_SHAPES = 10;
private static final double W = 600, H = 400;
private ShapeMachine machine;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() throws MalformedURLException, URISyntaxException {
double maxShapeSize = W / 8;
double minShapeSize = maxShapeSize / 2;
machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
}
#Override
public void start(final Stage stage) throws IOException, URISyntaxException {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < N_SHAPES; i++) {
pane.getChildren().add(machine.randomShape());
}
pane.setOnMouseClicked(event -> {
Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
highlightSelected(node, pane.getChildren());
});
Scene scene = new Scene(pane);
configureExitOnAnyKey(stage, scene);
stage.setScene(scene);
stage.setResizable(false);
stage.show();
}
private void highlightSelected(Node selected, ObservableList<Node> children) {
for (Node node: children) {
node.setEffect(null);
}
if (selected != null) {
selected.setEffect(new DropShadow(10, Color.YELLOW));
}
}
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[] {
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare: corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
private void configureExitOnAnyKey(final Stage stage, Scene scene) {
scene.setOnKeyPressed(keyEvent -> stage.hide());
}
}
Auxiliary random shape generation class
This class is not key to the solution, it just generates some shapes for testing.
class ShapeMachine {
private static final Random random = new Random();
private final double canvasWidth, canvasHeight, maxShapeSize, minShapeSize;
ShapeMachine(double canvasWidth, double canvasHeight, double maxShapeSize, double minShapeSize) {
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
this.maxShapeSize = maxShapeSize;
this.minShapeSize = minShapeSize;
}
private Color randomColor() {
return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256), 0.1 + random.nextDouble() * 0.9);
}
enum Shapes {Circle, Rectangle, Line}
public Shape randomShape() {
Shape shape = null;
switch (Shapes.values()[random.nextInt(Shapes.values().length)]) {
case Circle:
shape = randomCircle();
break;
case Rectangle:
shape = randomRectangle();
break;
case Line:
shape = randomLine();
break;
default:
System.out.println("Unknown Shape");
System.exit(1);
}
Color fill = randomColor();
shape.setFill(fill);
shape.setStroke(deriveStroke(fill));
shape.setStrokeWidth(deriveStrokeWidth(shape));
shape.setStrokeLineCap(StrokeLineCap.ROUND);
shape.relocate(randomShapeX(), randomShapeY());
return shape;
}
private double deriveStrokeWidth(Shape shape) {
return Math.max(shape.getLayoutBounds().getWidth() / 10, shape.getLayoutBounds().getHeight() / 10);
}
private Color deriveStroke(Color fill) {
return fill.desaturate();
}
private double randomShapeSize() {
double range = maxShapeSize - minShapeSize;
return random.nextDouble() * range + minShapeSize;
}
private double randomShapeX() {
return random.nextDouble() * (canvasWidth + maxShapeSize) - maxShapeSize / 2;
}
private double randomShapeY() {
return random.nextDouble() * (canvasHeight + maxShapeSize) - maxShapeSize / 2;
}
private Shape randomLine() {
int xZero = random.nextBoolean() ? 1 : 0;
int yZero = random.nextBoolean() || xZero == 0 ? 1 : 0;
int xSign = random.nextBoolean() ? 1 : -1;
int ySign = random.nextBoolean() ? 1 : -1;
return new Line(0, 0, xZero * xSign * randomShapeSize(), yZero * ySign * randomShapeSize());
}
private Shape randomRectangle() {
return new Rectangle(0, 0, randomShapeSize(), randomShapeSize());
}
private Shape randomCircle() {
double radius = randomShapeSize() / 2;
return new Circle(radius, radius, radius);
}
}
Further example placing objects in a zoomable/scrollable area
This solution uses the nearest node solution code from above and combines it with the zoomed node in a ScrollPane code from: JavaFX correct scaling. The purpose is to demonstrate that the choosing algorithm works even on nodes which have had a scaling transform applied to them (because it is based upon boundsInParent). The code is just meant as a proof of concept and not as a stylistic sample of how to structure the functionality into a class domain model :-)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
public class GraphicsScalingApp extends Application {
private static final int N_SHAPES = 10;
private static final double W = 600, H = 400;
private ShapeMachine machine;
public static void main(String[] args) {
launch(args);
}
#Override
public void init() throws MalformedURLException, URISyntaxException {
double maxShapeSize = W / 8;
double minShapeSize = maxShapeSize / 2;
machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
}
#Override
public void start(final Stage stage) {
Pane pane = new Pane();
pane.setPrefSize(W, H);
for (int i = 0; i < N_SHAPES; i++) {
pane.getChildren().add(machine.randomShape());
}
pane.setOnMouseClicked(event -> {
Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
System.out.println("Found: " + node + " at " + event.getX() + "," + event.getY());
highlightSelected(node, pane.getChildren());
});
final Group group = new Group(
pane
);
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(layout);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final ScrollPane scroller = new ScrollPane();
final Group scrollContent = new Group(zoomPane);
scroller.setContent(scrollContent);
scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
}
});
scroller.setPrefViewportWidth(256);
scroller.setPrefViewportHeight(256);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
: 1 / SCALE_DELTA;
// amount of scrolling in each direction in scrollContent coordinate
// units
Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
// move viewport so that old center remains in the center after the
// scaling
repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);
}
});
// Panning via drag....
final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()));
}
});
scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double deltaX = event.getX() - lastMouseCoordinates.get().getX();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
double desiredH = scroller.getHvalue() - deltaH;
scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));
double deltaY = event.getY() - lastMouseCoordinates.get().getY();
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
double desiredV = scroller.getVvalue() - deltaV;
scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
}
});
return scroller;
}
private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(exitMenuItem);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1 / 1.5);
group.setScaleY(group.getScaleY() * 1 / 1.5);
}
});
zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
zoomOutMenuItem);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(fileMenu, zoomMenu);
return menuBar;
}
private void highlightSelected(Node selected, ObservableList<Node> children) {
for (Node node : children) {
node.setEffect(null);
}
if (selected != null) {
selected.setEffect(new DropShadow(10, Color.YELLOW));
}
}
private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
Point2D pClick = new Point2D(x, y);
Node nearestNode = null;
double closestDistance = Double.POSITIVE_INFINITY;
for (Node node : nodes) {
Bounds bounds = node.getBoundsInParent();
Point2D[] corners = new Point2D[]{
new Point2D(bounds.getMinX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMinY()),
new Point2D(bounds.getMaxX(), bounds.getMaxY()),
new Point2D(bounds.getMinX(), bounds.getMaxY()),
};
for (Point2D pCompare : corners) {
double nextDist = pClick.distance(pCompare);
if (nextDist < closestDistance) {
closestDistance = nextDist;
nearestNode = node;
}
}
}
return nearestNode;
}
// icons source from:
// http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist
// website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
I am relatively new to property bindings and I am looking for some high-level advice on how to approach a design problem, which I will try to describe a simple example of here.
Problem description
The goal in this example is to allow the user to specify a box/rectangular region interactively in a pannable and zoomable 2D space. The 2D screen-space in which the box is depicted, maps to a 2D "real-space" (e.g. voltage vs time cartesian space, or GPS, or whatever). The user should be able to zoom/pan his viewport vertically/horizontally at any time, thereby changing the mapping between these two spaces.
screen-space <-------- user-adjustable mapping --------> real-space
The user specifies the rectangle in his viewport by dragging borders/corners, as in this demo:
class InteractiveHandle extends Rectangle {
private final Cursor hoverCursor;
private final Cursor activeCursor;
private final DoubleProperty centerXProperty = new SimpleDoubleProperty();
private final DoubleProperty centerYProperty = new SimpleDoubleProperty();
InteractiveHandle(DoubleProperty x, DoubleProperty y, double w, double h) {
super();
centerXProperty.bindBidirectional(x);
centerYProperty.bindBidirectional(y);
widthProperty().set(w);
heightProperty().set(h);
hoverCursor = Cursor.MOVE;
activeCursor = Cursor.MOVE;
bindRect();
enableDrag(true,true);
}
InteractiveHandle(DoubleProperty x, ObservableDoubleValue y, double w, ObservableDoubleValue h) {
super();
centerXProperty.bindBidirectional(x);
centerYProperty.bind(y);
widthProperty().set(w);
heightProperty().bind(h);
hoverCursor = Cursor.H_RESIZE;
activeCursor = Cursor.H_RESIZE;
bindRect();
enableDrag(true,false);
}
InteractiveHandle(ObservableDoubleValue x, DoubleProperty y, ObservableDoubleValue w, double h) {
super();
centerXProperty.bind(x);
centerYProperty.bindBidirectional(y);
widthProperty().bind(w);
heightProperty().set(h);
hoverCursor = Cursor.V_RESIZE;
activeCursor = Cursor.V_RESIZE;
bindRect();
enableDrag(false,true);
}
InteractiveHandle(ObservableDoubleValue x, ObservableDoubleValue y, ObservableDoubleValue w, ObservableDoubleValue h) {
super();
centerXProperty.bind(x);
centerYProperty.bind(y);
widthProperty().bind(w);
heightProperty().bind(h);
hoverCursor = Cursor.DEFAULT;
activeCursor = Cursor.DEFAULT;
bindRect();
enableDrag(false,false);
}
private void bindRect(){
xProperty().bind(centerXProperty.subtract(widthProperty().divide(2)));
yProperty().bind(centerYProperty.subtract(heightProperty().divide(2)));
}
//make a node movable by dragging it around with the mouse.
private void enableDrag(boolean xDraggable, boolean yDraggable) {
final Delta dragDelta = new Delta();
setOnMousePressed((MouseEvent mouseEvent) -> {
// record a delta distance for the drag and drop operation.
dragDelta.x = centerXProperty.get() - mouseEvent.getX();
dragDelta.y = centerYProperty.get() - mouseEvent.getY();
getScene().setCursor(activeCursor);
});
setOnMouseReleased((MouseEvent mouseEvent) -> {
getScene().setCursor(hoverCursor);
});
setOnMouseDragged((MouseEvent mouseEvent) -> {
if(xDraggable){
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
centerXProperty.set(newX);
}
}
if(yDraggable){
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
centerYProperty.set(newY);
}
}
});
setOnMouseEntered((MouseEvent mouseEvent) -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(hoverCursor);
}
});
setOnMouseExited((MouseEvent mouseEvent) -> {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
});
}
//records relative x and y co-ordinates.
private class Delta { double x, y; }
}
public class InteractiveBox extends Group {
private static final double sideHandleWidth = 2;
private static final double cornerHandleSize = 4;
private static final double minHandleFraction = 0.5;
private static final double maxCornerClearance = 6;
private static final double handleInset = 2;
private final Rectangle rectangle;
private final InteractiveHandle ihLeft;
private final InteractiveHandle ihTop;
private final InteractiveHandle ihRight;
private final InteractiveHandle ihBottom;
private final InteractiveHandle ihTopLeft;
private final InteractiveHandle ihTopRight;
private final InteractiveHandle ihBottomLeft;
private final InteractiveHandle ihBottomRight;
InteractiveBox(DoubleProperty xMin, DoubleProperty yMin, DoubleProperty xMax, DoubleProperty yMax){
super();
rectangle = new Rectangle();
rectangle.widthProperty().bind(xMax.subtract(xMin));
rectangle.heightProperty().bind(yMax.subtract(yMin));
rectangle.xProperty().bind(xMin);
rectangle.yProperty().bind(yMin);
DoubleBinding xMid = xMin.add(xMax).divide(2);
DoubleBinding yMid = yMin.add(yMax).divide(2);
DoubleBinding hx = (DoubleBinding) Bindings.max(
rectangle.widthProperty().multiply(minHandleFraction)
,rectangle.widthProperty().subtract(maxCornerClearance*2)
);
DoubleBinding vx = (DoubleBinding) Bindings.max(
rectangle.heightProperty().multiply(minHandleFraction)
,rectangle.heightProperty().subtract(maxCornerClearance*2)
);
ihTopLeft = new InteractiveHandle(xMin,yMax,cornerHandleSize,cornerHandleSize);
ihTopRight = new InteractiveHandle(xMax,yMax,cornerHandleSize,cornerHandleSize);
ihBottomLeft = new InteractiveHandle(xMin,yMin,cornerHandleSize,cornerHandleSize);
ihBottomRight = new InteractiveHandle(xMax,yMin,cornerHandleSize,cornerHandleSize);
ihLeft = new InteractiveHandle(xMin,yMid,sideHandleWidth,vx);
ihTop = new InteractiveHandle(xMid,yMax,hx,sideHandleWidth);
ihRight = new InteractiveHandle(xMax,yMid,sideHandleWidth,vx);
ihBottom = new InteractiveHandle(xMid,yMin,hx,sideHandleWidth);
style(ihLeft);
style(ihTop);
style(ihRight);
style(ihBottom);
style(ihTopLeft);
style(ihTopRight);
style(ihBottomLeft);
style(ihBottomRight);
getChildren().addAll(rectangle
,ihTopLeft, ihTopRight, ihBottomLeft, ihBottomRight
,ihLeft, ihTop, ihRight, ihBottom
);
rectangle.setFill(Color.ALICEBLUE);
rectangle.setStroke(Color.LIGHTGRAY);
rectangle.setStrokeWidth(2);
rectangle.setStrokeType(StrokeType.CENTERED);
}
private void style(InteractiveHandle ih){
ih.setStroke(Color.TRANSPARENT);
ih.setStrokeWidth(handleInset);
ih.setStrokeType(StrokeType.OUTSIDE);
}
}
public class Summoner extends Application {
DoubleProperty x = new SimpleDoubleProperty(50);
DoubleProperty y = new SimpleDoubleProperty(50);
DoubleProperty xMax = new SimpleDoubleProperty(100);
DoubleProperty yMax = new SimpleDoubleProperty(100);
#Override
public void start(Stage primaryStage) {
InteractiveBox box = new InteractiveBox(x,y,xMax,yMax);
Pane root = new Pane();
root.getChildren().add(box);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
After the rectangle has been specified by the user, its coordinates (in real-space) are passed on to or read by a different part of the program.
My rationale
My first instinct was to use the built-in scale/translate properties in JavaFX nodes to implement the mapping, but we want borders and handles to have a consistent size/appearance regardless of zoom-state; zooming should only embiggen the conceptual rectangle itself, not thicken the borders or corner-handles.
(In the following, arrows represent causality/influence/dependency. For example, A ---> B could mean property B is bound to property A (or it could mean that event-handler A sets property B), and <-----> could represent a bidirectional binding. A multi-tailed arrow such as --+--> could represent a binding that depends on multiple input observables.)
So my question became: which of the following should I do?
real-space-properties ---+--> screen-space-properties
real-space-properties <--+--- screen-space properties
or something different, using <---->
On the one hand, we have mouse events and the rendered rectangle itself in screen-space. This argues for a self-contained interactive rectangle (whose screen-space position/dimension properties we can observe (as well as manipulate, if we wanted to) externally) as per the demo above.
mouse events -----> screen-space properties ------> depicted rectangle
|
|
--------> real-space properties -----> API
On the other hand, when the user adjusts pan/zoom, we want the rectangle's properties in real-space (not screen-space) to be preserved. This argues for binding the screen-space properties to real-space properties using pan&zoom-state properties:
pan/zoom properties
|
|
real-space properties ---+--> screen-space properties ------> depicted rectangle
|
|
-------> API
If I try to put together both approaches above, I run into a problem:
mouse events
|
pan/zoom properties |
| |
| v
real-space properties <--+--> screen-space properties ------> depicted rectangle
| *
|
-------> API
This diagram makes a lot of sense to me, but I don't think the kind of "bidirectional" 3-way binding at * is possible, directly. But is there perhaps a simple way to emulate/work around it? Or should I take an entirely different approach?
Here's an example of a rectangle on a zoom & pannable pane with a constant stroke width. You just have to define the scale factor as a property of the pane, bind a it to a property in the calling class and divide that into a property that's bound to the strokewidth of the rectangle.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty strokeWidthProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.web("#000080", 0.2));
rect.setStrokeType(StrokeType.INSIDE);
rect.strokeWidthProperty().bind(strokeWidthProperty.divide(zoomProperty));
group.getChildren().add(rect);
// create canvas
PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(60);
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
/**
* fit the rectangle to the width of the window
*/
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = (scale / oldScale)-1;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}
I want to get the current position (x,y) of a Circle (javafx.scene.shape.Circle) i am moving via a PathTransition, while the transition is running/happening.
So i need some kind of task, that checks the position of the circle every 50 milliseconds (for example).
I also tried this solution Current circle position of javafx transition which was suggested on Stack Overflow, but i didn't seem to work for me.
Circle projectile = new Circle(Playground.PROJECTILE_SIZE, Playground.PROJECTILE_COLOR);
root.getChildren().add(projectile);
double duration = distance / Playground.PROJECTILE_SPEED;
double xOff = (0.5-Math.random())*Playground.WEAPON_OFFSET;
double yOff = (0.5-Math.random())*Playground.WEAPON_OFFSET;
Line shotLine = new Line(player.getCurrentX(), player.getCurrentY(), aimLine.getEndX() + xOff, aimLine.getEndY() + yOff);
shotLine.setEndX(shotLine.getEndX() + (Math.random()*Playground.WEAPON_OFFSET));
PathTransition pt = new PathTransition(Duration.seconds(duration), shotLine, projectile);
// Linear movement for linear speed
pt.setInterpolator(Interpolator.LINEAR);
pt.setOnFinished(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
// Remove bullet after hit/expiration
projectile.setVisible(false);
root.getChildren().remove(projectile);
}
});
projectile.translateXProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
double x = collider.getTranslateX() - projectile.getTranslateX();
double y = collider.getTranslateY() - projectile.getTranslateY();
double distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
System.out.println("Distance: "+ distance);
if (distance < 50) {
System.out.println("hit");
}
}
});
pt.play();
A PathTransition will move a node by manipulating its translateX and translateY properties. (A TranslateTransition works the same way.)
It's hard to answer your question definitively as your code is so incomplete, but if the projectile and collider have the same parent in the scene graph, converting the initial coordinates of the projectile and collider by calling localToParent will give the coordinates in the parent, including the translation. So you can observe the translateX and translateY properties and use that conversion to check for a collision. If they have different parents, you can do the same with localToScene instead and just convert both to coordinates relative to the scene.
Here's a quick SSCCE. Use the left and right arrows to aim, space to shoot:
import javafx.animation.Animation;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ShootingGame extends Application {
#Override
public void start(Stage primaryStage) {
final double width = 400 ;
final double height = 400 ;
final double targetRadius = 25 ;
final double projectileRadius = 5 ;
final double weaponLength = 25 ;
final double weaponX = width / 2 ;
final double weaponStartY = height ;
final double weaponEndY = height - weaponLength ;
final double targetStartX = targetRadius ;
final double targetY = targetRadius * 2 ;;
Pane root = new Pane();
Circle target = new Circle(targetStartX, targetY, targetRadius, Color.BLUE);
TranslateTransition targetMotion = new TranslateTransition(Duration.seconds(2), target);
targetMotion.setByX(350);
targetMotion.setAutoReverse(true);
targetMotion.setCycleCount(Animation.INDEFINITE);
targetMotion.play();
Line weapon = new Line(weaponX, weaponStartY, weaponX, weaponEndY);
weapon.setStrokeWidth(5);
Rotate weaponRotation = new Rotate(0, weaponX, weaponStartY);
weapon.getTransforms().add(weaponRotation);
Scene scene = new Scene(root, width, height);
scene.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.LEFT) {
weaponRotation.setAngle(Math.max(-45, weaponRotation.getAngle() - 2));
}
if (e.getCode() == KeyCode.RIGHT) {
weaponRotation.setAngle(Math.min(45, weaponRotation.getAngle() + 2));
}
if (e.getCode() == KeyCode.SPACE) {
Point2D weaponEnd = weapon.localToParent(weaponX, weaponEndY);
Circle projectile = new Circle(weaponEnd.getX(), weaponEnd.getY(), projectileRadius);
TranslateTransition shot = new TranslateTransition(Duration.seconds(1), projectile);
shot.setByX(Math.tan(Math.toRadians(weaponRotation.getAngle())) * height);
shot.setByY(-height);
shot.setOnFinished(event -> root.getChildren().remove(projectile));
BooleanBinding hit = Bindings.createBooleanBinding(() -> {
Point2D targetLocation = target.localToParent(targetStartX, targetY);
Point2D projectileLocation = projectile.localToParent(weaponEnd);
return (targetLocation.distance(projectileLocation) < targetRadius + projectileRadius) ;
}, projectile.translateXProperty(), projectile.translateYProperty());
hit.addListener((obs, wasHit, isNowHit) -> {
if (isNowHit) {
System.out.println("Hit");
root.getChildren().remove(projectile);
root.getChildren().remove(target);
targetMotion.stop();
shot.stop();
}
});
root.getChildren().add(projectile);
shot.play();
}
});
root.getChildren().addAll(target, weapon);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You may not be able to tell that much but for some reason a few of the characters are being offset somehow, due to a flaw in my algorithm...If someone could figure out what is causing it, I would really appreciate it and any critique is welcome as I'm still very new at java.
Edit: If you look at the image above it's the E that is offset in WE on the left and right side
Edit: I think it may be in my calculation of the size of text vs size of circle
Edit: Ok so when I enter 600 for width and height everything seems to fall in place, but as it gets smaller from say 250 for example the characters start becoming more offset and overlapping
Main class:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
/**
* Created by John on 7/11/2014.
*/
public class Prog14_05 extends Application {
#Override
public void start(Stage primaryStage) {
// Create Pane
circularText phrase = new circularText("WE ARE ANONYMOUS, " +
"WE ARE LEGION, WE DO NOT FORGIVE, WE DO NOT FORGET ",
480, 480);
// Place clock and label in border pane
GridPane pane = new GridPane();
pane.setPadding(new Insets(phrase.getTextSize() * 2));
pane.setAlignment(Pos.CENTER);
pane.setStyle("-fx-background-color: black");
pane.getChildren().add(phrase);
// Create a scene and place it in the stage
Scene scene = new Scene(pane);
primaryStage.setTitle("Exercise14_05");
primaryStage.setScene(scene);
primaryStage.show();
}
}
circularText Class:
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
/**
* Created by John on 7/11/2014.
*/
public class circularText extends Pane {
double textSize = 30;
String string = "";
String fontName = "";
Font font;
// Pane's width and height
private double w = 250, h = 250;
/** Create Constructor */
public circularText (String phrase, double w, double h) {
this.w = w;
this.h = h;
this.string = phrase;
textSize = (this.w / this.string.length()) * 2;
Font font = new Font("Times Roman", textSize);
paintText(this.string, this.font);
}
/** Set new font */
public void setFont(String name) {
Font font = new Font(name, textSize);
this.font = font;
this.fontName = name;
paintText(this.string, this.font);
}
/** Return textSize */
public double getTextSize() {
return this.textSize;
}
/** Set textSize */
public void setTextSize(double textSize) {
this.textSize = textSize;
Font font = new Font(fontName, textSize);
this.font = font;
paintText(this.string, this.font);
}
/** Return pane's width */
public double getW() {
return w;
}
/** Set pane's width */
public void setW(double w) {
this.w = w;
textSize = (this.w / this.string.length()) * 2;
paintText(this.string, this.font);
}
/** Return pane's height */
public double getH() {
return h;
}
/** Set pane's height */
public void setH(double h) {
this.h = h;
textSize = (this.w / this.string.length()) * 2;
paintText(this.string, this.font);
}
/** Paint the Letters */
protected void paintText(String phrase, Font font) {
// Initialize parameters
double radius = Math.min(w, h) * 0.8 * 0.5;
double centerX = w / 2;
double centerY = h / 2;
double size = radius / 4 - this.getTextSize();
// Draw circle
Circle circle = new Circle(centerX - size - textSize, centerY - size,
radius);
circle.setFill(null);
circle.setStroke(null);
getChildren().clear();
getChildren().add(circle);
// Place text in a circular pattern
int i = 0;
double degree = 360 / phrase.length();
for (double degrees = 0; i < phrase.length(); i++, degrees += degree) {
double pointX = circle.getCenterX() + circle.getRadius() *
Math.cos(Math.toRadians(degrees));
double pointY = circle.getCenterY() + circle.getRadius() *
Math.sin(Math.toRadians(degrees));
Text letter = new Text(pointX, pointY, phrase.charAt(i) + "");
letter.setFont(font);
letter.setFill(Color.LIME);
letter.setRotate(degrees + 90);
getChildren().add(letter);
}
}
}
My trig isn't very good so I can't help you there. I'm thinking the "W" may be offset, not the "E". I know in other versions of Swing the "W" has caused painting problems before, but I don't remember the details. So I might suggest trying different characters to see if you still have the same problem at those two locations.
Here is another example of circular painting that I found on the web a long time ago. I tried your text and the "WE" is overlapped. I changed the "W" to an "R" and it seems to work ok, so maybe this validates my above statement?
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Font;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GradientPaint;
import java.awt.RenderingHints;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.geom.AffineTransform;
//import com.sun.awt.AWTUtilities;
public class SplashPortalPanel6 extends JPanel
{
private static final long serialVersionUID = 1L;
// private static final char[] MESSAGE = " SplashPortal.net".toCharArray();
private static final char[] MESSAGE = " WE ARE ANONYMOUS, WE ARE LEGION, WE DO NOT FORGIVE, WE DO NOT FORGET ".toCharArray();
// private static final char[] MESSAGE = " RE ARE ANONYMOUS, RE ARE LEGION, RE DO NOT FORGIVE, RE DO NOT FORGET ".toCharArray();
private static final double R90 = Math.toRadians(90);
private static final double R_90 = Math.toRadians(-90);
private AffineTransform cumalativeRotation = new AffineTransform();
private double rotation = Math.toRadians(360.0 / MESSAGE.length);
private Font font = new Font("Impact",Font.ITALIC,40);
private final Timer timer = new Timer(1000/76, new ActionListener() {
public void actionPerformed(ActionEvent e) {
repaint();//just repaint
}
});
public SplashPortalPanel6() {
setPreferredSize(new java.awt.Dimension(600, 600));
setOpaque(false);
}
//This method is called when the panel is connected to a native
//screen resource. It's an indication we can now start painting.
public void addNotify() {
super.addNotify();
timer.start();
}
public void removeNotify() {
super.removeNotify();
timer.stop();
}
private static final GradientPaint gradient = new GradientPaint(0F, 0F, Color.BLUE, 5F, 10F, Color.CYAN, true);
private static final int x = 0, y = 0, w = 100, h = 100;
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setFont(font);
g2.translate( getWidth()/2, getHeight()/2 );
cumalativeRotation.rotate(rotation/50);
g2.transform( cumalativeRotation );
for(int i = 0; i < MESSAGE.length; i++) {
// fill the rectangle
g2.translate(250, 0);
g2.rotate(R90);
g2.setColor(Color.BLACK);
// g2.fillRect(x,y,w,h);
// draw the border
g2.setColor(Color.WHITE);
// g2.drawRect(x,y,w,h);
// draw the character
g2.setPaint(gradient);
g2.drawChars(MESSAGE,i, 1, x+30, y+50);
g2.rotate(R_90);
g2.translate(-250, 0);
g2.rotate(rotation);
}
}
public static void createAndShowSplashScreen() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setUndecorated(true);
frame.setContentPane(new SplashPortalPanel6());
frame.pack();
frame.setLocationRelativeTo(null);
// AWTUtilities.setWindowOpaque(frame, false);
//frame.setAlwaysOnTop(true);
frame.setVisible(true);
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
createAndShowSplashScreen();
}
});
}
}
Note, if you uncomment the "fillRect" and "drawRect" statements you will see the original implementation of the code. Of course you will need to use the shorter first message string to see the effect.
Add
letter.setTextAlignment(TextAlignment.CENTER);
letter.setWrappingWidth(100);
Not sure what is going on with JavaFX Text rendering.
The math appears correct. For clarity when coding some suggest adding explicit typing to ensure your not mixing doubles with floats with ints. So instead of
double centerY = h / 2;
do
double centerY = h / 2.0d;
(Also take out extra "this." clutter, many methods are not being used like "setH", and make class name upper case CircularText)
Alright here is what I got so far, let me know what you all think and what can be improved upon...
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
/**
* Created by John on 7/11/2014.
*/
public class CircularText extends Pane {
private double circleWidth;
private double circleHeight;
private double textSize;
private double textStartDegree;
private double textRotate;
private double gapSpacing;
private double offSetX;
private double offSetY;
private Font font;
private Paint textFill;
private String fontName;
final String text;
/** Default Constructor */
public CircularText(String text) {
this.circleWidth = 250;
this.circleHeight = 250;
this.text = text;
textSize = (this.circleWidth / this.text.length()) * 2;
this.font = new Font("Times Roman", textSize);
this.textFill = Color.BLACK;
this.textStartDegree = 240;
this.textRotate = 90;
this.gapSpacing = 0.975;
this.offSetX = 4;
this.offSetY = 3;
paintText(this.text, this.font);
}
/** Create Constructor */
public CircularText (String text, double w, double h) {
this.circleWidth = w;
this.circleHeight = h;
this.text = text;
textSize = (this.circleWidth / (this.text.length()) * 2);
this.font = new Font("Times Roman", textSize);
this.textFill = Color.BLACK;
this.textStartDegree = 240;
this.textRotate = 90;
this.gapSpacing = 0.975;
this.offSetX = 4;
this.offSetY = 3;
paintText(this.text, this.font);
}
/** Get font color */
public Paint getTextFill() {
return textFill;
}
/** Set font color */
public void setTextFill(Paint textFill) {
this.textFill = textFill;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get starting position for text */
public double getTextStartDegree() {
return textStartDegree;
}
/** Set starting position for text */
public void setTextStartDegree(double textStartDegree) {
this.textStartDegree = textStartDegree;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get letter rotation */
public double getTextRotate() {
return textRotate;
}
/** Set letter rotation */
public void setTextRotate(double textRotate) {
this.textRotate = textRotate;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get spacing between ending and beginning of phrase */
public double getGapSpacing() {
return gapSpacing;
}
/** Set spacing between ending and beginning of phrase */
public void setGapSpacing(double gapSpacing) {
this.gapSpacing = gapSpacing;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Get current font */
public Font getFont() {
return this.font;
}
/** Set new font */
public void setFont(String name) {
this.font = new Font(name, textSize);
this.fontName = name;
paintText(this.text, this.font);
}
/** Return textSize */
public double getTextSize() {
return this.textSize;
}
/** Set textSize */
public void setTextSize(double textSize, double offSetX, double offSetY) {
this.textSize = textSize;
this.offSetX = offSetX;
this.offSetY = offSetY;
this.font = new Font(fontName, textSize);
paintText(this.text, this.font);
}
/** Return circle's width */
public double getCircleWidth() {
return circleWidth;
}
/** Set circle's width */
public void setCircleWidth(double w) {
this.circleWidth = w;
textSize = (this.circleWidth / this.text.length()) * 2;
paintText(this.text, this.font);
}
/** Return circle's height */
public double getCircleHeight() {
return circleHeight;
}
/** Set circle's height */
public void setCircleHeight(double h) {
this.circleHeight = h;
textSize = (this.circleWidth / this.text.length()) * 2;
paintText(this.text, this.font);
}
/** Paint the Letters */
protected void paintText(String text, Font font) {
getChildren().clear();
// Initialize parameters
double radius = Math.min(circleWidth, circleHeight) * 0.8 * 0.5;
double centerX = circleWidth / 2;
double centerY = circleHeight / 2;
// Place text in a circular pattern
int i = 0;
double degree = 360.0 / (text.length() / this.gapSpacing);
for (double degrees = this.textStartDegree;
i < text.length(); i++, degrees += degree) {
double pointX = centerX + radius *
Math.cos(Math.toRadians(degrees)) - (this.textSize) *
this.offSetX;
double pointY = centerY + radius *
Math.sin(Math.toRadians(degrees)) - (this.textSize) *
this.offSetY;
Text letter = new Text(pointX, pointY,
String.valueOf(text.charAt(i)));
letter.setFont(font);
letter.setFill(this.textFill);
letter.setRotate(degrees + this.textRotate);
letter.setTextAlignment(TextAlignment.CENTER);
getChildren().add(letter);
}
}
}
After testing this with Courier New font, it appears to render flawlessly. I also tested this with other fonts and everything still rendered correctly. It appears the flaw in my code was correlated with the Circle object I had created for troubleshooting and for some reason decided to use in my algorithm. After removing this Circle object and fixing small flaws in my code and adding flexibility, everything works perfectly :)
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* Created by John on 7/11/2014.
*/
public class Prog14_05 extends Application {
#Override
public void start(Stage primaryStage) {
// Create Pane
CircularText phrase = new CircularText("TESTING MY CIRCULAR" +
"TEXT OBJECT CLASS",
500, 500);
phrase.setFont("Courier New");
phrase.setTextFill(Color.LIME);
phrase.setTextSize(20);
// Place clock and label in border pane
GridPane pane = new GridPane();
pane.setPadding(new Insets(phrase.getTextSize()));
pane.setAlignment(Pos.CENTER);
pane.setStyle("-fx-background-color: black");
pane.getChildren().add(phrase);
// Create a scene and place it in the stage
Scene scene = new Scene(pane);
primaryStage.setTitle("Exercise14_05");
primaryStage.setScene(scene);
primaryStage.show();
}
}
P.S Test my code and let me know if you find any bugs, Thanks for everyone's help