I am having a problem with JavaFX 3D, the problem is as follows:
When I turn my perspective camera around, the last added box (blue box) overlaps the first added box (red box), here is a screenshot:
can anyone tell me why is this happening? And is there a way to fix it? (the boxes are literally 2 box classes with a width, height, depth, position and color)
Minimal reproducible example since somebody asked for it:
Box box1 = new Box();
Box box2 = new Box();
box1.setWidth(300);
box2.setWidth(300);
box1.setHeight(300);
box2.setHeight(300);
box1.setDepth(300);
box2.setDepth(300);
box1.setTranslateX(300);
box2.setTranslateX(300);
box1.setTranslateY(300);
box2.setTranslateX(300);
Group root = new Group();
root.getChildren().addAll(box, box2);
PerspectiveCamera cam = new PerspectiveCamera();
Scene scene = new Scene(root);
scene.setCamera(camera);
stage.setScene(scene);
stage.show();
where stage is the stage inside public void start(Stage stage), JavaFX's default run method (any class that extends Application should implement it)
You probably have to add a subscene with the depth buffer enabled. See: https://openjfx.io/javadoc/15/javafx.graphics/javafx/scene/SubScene.html#%3Cinit%3E(javafx.scene.Parent,double,double,boolean,javafx.scene.SceneAntialiasing)
Solution:
I used the following constructor:
new Scene(root, WIDTH, HEIGHT, true, SceneAntialiasing.BALANCED);
instead of:
new Scene(root);
Related
I am making a simple graphical interface for saving previously generated images. All images come to me square but I want to allow for some cropping functionality (more precisely cutting off equal parts from the bottom and top of the image). I want to do this by allowing the user to drag a shaded region over the image which will tell the user that this region will be cropped out. See the below image for details. To enable this drag functionality I have added small triangles that I want the user to drag which in turn will move the shaded regions about. However the coordinates for the triangles are all weird and seem nonsensical. Therefor I was wondering what the best way is to get the coordinates of the triangles in relation to the ImageView (or their first common parent node) in terms of ImageView-side-lengths. So if the triangle is in the center its coordinates are [0.5, 0.5] for instance.
The Image view will be moving around inside the scene and will also be changing size so it is vital that I can get the coordinates relative to not only the ImageView but also to the size of the ImageView.
Here is also the surrounding hierarchy of nodes if that helps. The Polygons are the triangles and the regions are the rectangles.
Thanks for all forms of help!
Node.getBoundsInParent returns the bounds of a node in it's parent coordinates. E.g. polygon.getBoundsInParent() would return the bounds in the VBox.
If you need to "go up" one additional step, you can use parent.localToParent to do this. vBox.localToParent(boundsInVbox) returns the bounds in the coordinate system of the AnchorPane.
To get values relative to the size of the image, you simply need to divide by it's size.
The following example only allows you to move the cover regions to in one direction and does not check, if the regions intersect, but it should be sufficient to demonstrate the approach.
The interesting part is the event handler of the button. It restricts the viewport of the second image to the part of the first image that isn't covered.
private static void setSideAnchors(Node node) {
AnchorPane.setLeftAnchor(node, 0d);
AnchorPane.setRightAnchor(node, 0d);
}
#Override
public void start(Stage primaryStage) {
// create covering area
Region topRegion = new Region();
topRegion.setStyle("-fx-background-color: white;");
Polygon topArrow = new Polygon(0, 0, 20, 0, 10, 20);
topArrow.setFill(Color.WHITE);
VBox top = new VBox(topRegion, topArrow);
top.setAlignment(Pos.TOP_CENTER);
topArrow.setOnMouseClicked(evt -> {
topRegion.setPrefHeight(topRegion.getPrefHeight() + 10);
});
// create bottom covering area
Region bottomRegion = new Region();
bottomRegion.setStyle("-fx-background-color: white;");
Polygon bottomArrow = new Polygon(0, 20, 20, 20, 10, 0);
bottomArrow.setFill(Color.WHITE);
VBox bottom = new VBox(bottomArrow, bottomRegion);
bottom.setAlignment(Pos.BOTTOM_CENTER);
bottomArrow.setOnMouseClicked(evt -> {
bottomRegion.setPrefHeight(bottomRegion.getPrefHeight() + 10);
});
Image image = new Image("https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/402px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg");
ImageView imageView = new ImageView(image);
setSideAnchors(top);
setSideAnchors(bottom);
setSideAnchors(imageView);
AnchorPane.setTopAnchor(top, 0d);
AnchorPane.setBottomAnchor(bottom, 0d);
AnchorPane.setTopAnchor(imageView, 0d);
AnchorPane.setBottomAnchor(imageView, 0d);
AnchorPane container = new AnchorPane(imageView, top, bottom);
ImageView imageViewRestricted = new ImageView(image);
Button button = new Button("restrict");
button.setOnAction(evt -> {
// determine bouns of Regions in AnchorPane
Bounds topBounds = top.localToParent(topRegion.getBoundsInParent());
Bounds bottomBounds = bottom.localToParent(bottomRegion.getBoundsInParent());
// set viewport accordingly
imageViewRestricted.setViewport(new Rectangle2D(
0,
topBounds.getMaxY(),
image.getWidth(),
bottomBounds.getMinY() - topBounds.getMaxY()));
});
HBox root = new HBox(container, button, imageViewRestricted);
root.setFillHeight(false);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
Here is my code. You can copy-paste and follow what I write bellow to see the problem yourself.
public class MyApp extends Application {
#Override
public void start(Stage stage) throws Exception {
Scene scene = new Scene(new MyView(), 100, 150);
stage.setScene(scene);
stage.show();
}
private class MyView extends BorderPane {
MyView() {
GridPane board = new GridPane();
int size = 3;
for (int i = 0; i < size*size; i++) {
BorderPane pane = new BorderPane();
pane.setMinSize(30, 30);
pane.setBackground(new Background(new BackgroundFill(Color.RED, null, null)));
pane.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID,
null, null, null)));
pane.setOnMousePressed(e -> {
PickResult pick = e.getPickResult();
Pane selectedNode = (Pane) pick.getIntersectedNode();
selectedNode.setBackground(new Background(new BackgroundFill(Color.GREEN, null, null)));
});
board.add(pane, i / size, i % size);
}
Box box = new Box(20d, 20d, 20d);
BorderPane boardPane = new BorderPane(box, null, null, board, null);
Group root = new Group(boardPane);
SubScene scene = new SubScene(root, USE_PREF_SIZE, USE_PREF_SIZE, true, SceneAntialiasing.BALANCED);
scene.widthProperty().bind(widthProperty());
scene.heightProperty().bind(heightProperty());
setCenter(scene);
}
}
public static void main(String[] args) throws Exception {
launch(args);
}
}
I create a subscene with a grid of squares. When i press on a square I want its background to change color. This works in 2 situations:
if I don't add the Box to the boardPane
if I don't set the scene with a depth buffer
or both. But if i both add the box and set a depth buffer, the squares don't receive the event. instead the boardPane receives it. i guess it's something to do with 2D nodes in a 3D scene.
I tried setting combinations of these methods :setPickOnBounds, setDepthTest, setMouseTransparent but nothing worked.
What's the solution?
Once you have a 3D scene, there aren't really such things as 2D nodes, everything in the scene graph has an x, y and z co-ordinate; even things that were previously treated as 2D nodes when the depth buffer was set to false.
When you place a box in your root border pane, that root border pane assumes the 3D co-ordinates of the box. For picking purposes, the box you have defined is represented in 3D space from z co-ordinates -10 to 10, so the root border pane is defined as -10 for picking purposes. The border panes that you place inside the root border pane have no z co-ordinate defined for them, so they end up at a z co-ordinate of 0, which, from the viewers perspective is behind the root border pane.
So, the root border pane is receiving clicks, but because it is now on a different z plane than the rest of its contents, the other 2D square contents you have defined do not receive clicks. One could argue that the root border pane is not rendered at all as it has no color, so it should be treated as transparent and the clicks just go through to the child nodes, but it seems that is just not how the 3D picking algorithm for JavaFX works.
For your example, to get everything in the same Z plane and picking working correctly, add the following line inside your for loop: pane.setTranslateZ(-10);.
Note: I debugged this by adding the following line to your source code (which reported to me the target of the mouse clicks and the x,y,z pick result co-ordinate for each click):
root.setOnMouseClicked(System.out::println);
My advice is to avoid using layout panes that are designed for 2D purposes (e.g. border panes) to attempt laying out elements in 3D space. The JavaFX layout panes are really only meant for 2D layout. To handle positioning in 3D space you should manage co-ordinates yourself, by just using Groups rather than anything that derives from Pane. At the very least don't try adding 3D elements into 2D layout panes as the results can be confusing (as you have discovered).
You can further separate 2D and 3D items by placing them in different sub scenes (that is really what the intention of the sub scene notion in JavaFX is I think). An example of an application with multiple sub scenes is shown in the following answer: How to create custom 3d model in JavaFX 8?
viewScroll.setContent(new ImageView(bigimg));
double w = viewScroll.getContent().getBoundsInLocal().getWidth();
double vw = viewScroll.getViewportBounds().getWidth();
viewScroll.getContent().setTranslateX((vw/2)-(w/2));
viewScroll.toFront();
I set an ImageView with some Image inside the ScrollPane but the ImageView always goes to the far left corner. Here I'm trying to manually offset the difference, but it doesn't work well. The ImageView goes too far to the right plus it only updates once because it's inside the eventhandler for a button.
Here is an example using a label without the need for listeners:
public void start(Stage primaryStage) throws Exception {
ScrollPane scrollPane = new ScrollPane();
Label label = new Label("Hello!");
label.translateXProperty().bind(scrollPane.widthProperty().subtract(label.widthProperty()).divide(2));
scrollPane.setContent(label);
Scene scene = new Scene(scrollPane);
primaryStage.setScene(scene);
primaryStage.setWidth(200);
primaryStage.setHeight(200);
primaryStage.show();
}
I am not sure if you are familiar with properties or not, but in the code above, when I bind the translateXProperty to the formula, every time one of the dependencies changes, such as the ScrollPane's widthProperty or the Label's widthProperty, the formula is recalculated and translateXProperty is set to the result of the formula.
I am not sure, but in your previous code, it appears that the calculation code would be in a resize listener. This is not required when dealing with properties as they update whenever dependencies changed (note the bind() and not set()).
Can someone please explain me why this strange behaviour exists? When I have a group in a vbox every item in a child appears to modify the siblings to.
Following strange behaviour happens:
everything normal here
here too everything normal
whoops, why did the searchbar move???
First of the structure of the application I have:
root (VBox) //vbox so the menubar has its own space
├───menubar (MenuBar)
└───contentroot (Group)
├───searchbar (TextField) //searchbar should always be on the top left
└───nodeRoot (Group)
├───circle1 (Nodes)
└───circle2 (Nodes)
The root is a vbox so the menubar has its own unchallenged space. The searchbar should always be top left, directly under the menubar.
The nodeRoot should contain every other node. Kinda like a drawing board which I should be able to drag around.
Code:
public void start(Stage primaryStage) throws Exception{
VBox root = new VBox();
MenuBar menuBar = new MenuBar(new Menu("File"));
Group contentRoot = new Group();
contentRoot.getChildren().add(new TextField("SearchBar"));
Group nodeRoot = new Group();
contentRoot.getChildren().add(nodeRoot);
root.getChildren().addAll(menuBar, contentRoot);
Circle circle = new Circle(30, Color.RED);
nodeRoot.getChildren().add(circle);
Scene scene = new Scene(root, 300, 275);
primaryStage.setScene(scene);
primaryStage.show();
scene.setOnMousePressed(event -> {
circle.setTranslateX(event.getSceneX() - 15);
circle.setTranslateY(event.getSceneY() - 15);
});
}
My guess why this happens:
The problem started to appear after I added the menubar and put everything into a VBox. This is when the siblings of the nodeRoot get changed too. My guess is that because VBox is a Region the behaviour is different than a normal group which expands. But then I dont understand why it only happens if the item moves to the left or top.
Can somebody please explain why this happens and how I can fix it?
From the javadocs for Group:
A Group will take on the collective bounds of its children and is not
directly resizable.
When you click near the top or left of the scene, the circle's bounds include negative values. Since the group takes on those bounds, it also takes on negative values. The TextField never has any layout bounds set, so the Group positions it at (0,0). Hence the text field can end up below or to the right of the circle. The vbox positions the group in order to try and contain it entirely, so it shifts it right if it contains negative x-value bounds and down if it contains negative y-value bounds.
If you use a Pane to contain the circle, instead of a Group:
Pane contentRoot = new Pane();
it behaves more intuitively: the Pane does not take on the union of the bounds of its child nodes, so if the circle has negative bounds, it just moves left and/or above the pane's visible area.
I'm quite new to JavaFX and I have two methods - one returning a grid layout and the other one returning a HBox with a MenuBar, but fot the life of me I can't make it, so it's not overlapping (I want the grid to be a few pixels lower). I have this code in my start method:
final Group rootGroup = new Group();
final Scene scene = new Scene(rootGroup);
rootGroup.getChildren().add(addBar(stage.widthProperty()));
rootGroup.getChildren().add(addGridPane());
stage.setScene(scene);
stage.show();
How do I fix this?
You may use another Layout Manager as root. For example:
final VBox rootGroup = new Vbox();
Then all its children will be aligned vertically.