Reputation: 670
I'm trying to understand the coordinate system of JavaFX.
For some nodes (shapes?) like Line
or Rectangle
I can (or should) specify a x and y value in the coordinate system.
What exactly is this? Is this a translation and stretch which is later appended to the node or something else? Other nodes only have a setLayoutX()
method, whereas e.g. the Line
has both setLayoutX()
as well as setStartX()
.
Thanks!
Upvotes: 0
Views: 667
Reputation: 46170
Every Node
has two different bounds properties (ignoring Node#layoutBounds
) relating to two different coordinate spaces—local and parent.
The rectangular bounds of this
Node
in the node's untransformed local coordinate space. For nodes that extendShape
, the local bounds will also include space required for a non-zero stroke that may fall outside the shape's geometry that is defined by position and size attributes. The local bounds will also include any clipping set withclip
as well as effects set witheffect
.[...]
The rectangular bounds of this
Node
which include its transforms.boundsInParent
is calculated by taking the local bounds (defined byboundsInLocal
) and applying the transform created by setting the following additional variables
transforms
ObservableListscaleX
,scaleY
,scaleZ
rotate
layoutX
,layoutY
translateX
,translateY
,translateZ
The resulting bounds will be conceptually in the coordinate space of theNode
's parent, however the node need not have a parent to calculate these bounds.[...]
This means properties such as layoutX
and translateX
only affect the bounds-in-parent. Basically, the bounds-in-parent is simply the bounds-in-local with the various transformations applied. But when it comes to the "special" Shape
properties, such as the x
and y
properties of Rectangle
, they directly affect the bounds-in-local. I was unable to find documentation explaining this, though maybe I just missed it or this behavior is supposed to be obvious to those "in the know". Unfortunately I won't be able to explain to you why these Shape
properties affect the bounds-in-local directly as I lack foundational knowledge in this area.
That said, I can visually demonstrate the differences regarding bounds-in-local with the following example:
App.java
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class App extends Application {
@Override
public void start(Stage primaryStage) throws IOException {
var root = FXMLLoader.<Parent>load(getClass().getResource("App.fxml"));
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
Controller.java
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle;
public class Controller {
@FXML
private void handleMousePressed(MouseEvent event) {
event.consume();
var source = (Node) event.getSource();
source.setUserData(source.localToParent(event.getX(), event.getY()));
}
@FXML
private void handleMouseDragged(MouseEvent event) {
event.consume();
var source = (Node) event.getSource();
var lastPoint = (Point2D) source.getUserData();
var nextPoint = source.localToParent(event.getX(), event.getY());
source.setTranslateX(source.getTranslateX() + nextPoint.getX() - lastPoint.getX());
source.setTranslateY(source.getTranslateY() + nextPoint.getY() - lastPoint.getY());
source.setUserData(nextPoint);
}
@FXML
private void handleMouseReleased(MouseEvent event) {
event.consume();
var source = (Node) event.getSource();
if (source instanceof Rectangle) {
var rectangle = (Rectangle) source;
rectangle.setX(rectangle.getX() + rectangle.getTranslateX());
rectangle.setTranslateX(0);
rectangle.setY(rectangle.getY() + rectangle.getTranslateY());
rectangle.setTranslateY(0);
}
source.setUserData(null);
}
}
App.fxml
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.paint.Color?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Font?>
<HBox xmlns="http://javafx.com/javafx/12.0.2" xmlns:fx="http://javafx.com/fxml/1" fx:controller="Controller"
prefWidth="1000" prefHeight="600" spacing="15">
<fx:define>
<Font fx:id="titleFont" name="Impact" size="24"/>
<Color fx:id="inLocalColor" fx:value="LIME"/>
<Color fx:id="inParentColor" fx:value="RED"/>
</fx:define>
<padding>
<Insets topRightBottomLeft="25"/>
</padding>
<VBox HBox.hgrow="ALWAYS" spacing="10" alignment="CENTER">
<Label text="SHAPE" font="$titleFont"/>
<Separator/>
<Pane fx:id="shapePane" VBox.vgrow="ALWAYS">
<clip>
<Rectangle x="-5" y="-5" width="${shapePane.width}" height="${shapePane.height}"/>
</clip>
<Pane managed="false">
<Rectangle fx:id="shape" width="100" height="100" managed="false" onMousePressed="#handleMousePressed"
onMouseDragged="#handleMouseDragged" onMouseReleased="#handleMouseReleased"/>
<Rectangle fill="TRANSPARENT" stroke="$inLocalColor" strokeType="INSIDE" strokeWidth="2"
mouseTransparent="true" x="${shape.boundsInLocal.minX}" y="${shape.boundsInLocal.minY}"
width="${shape.boundsInLocal.width}" height="${shape.boundsInLocal.height}"/>
<Rectangle fill="TRANSPARENT" stroke="$inParentColor" strokeType="OUTSIDE" strokeWidth="2"
mouseTransparent="true" x="${shape.boundsInParent.minX}" y="${shape.boundsInParent.minY}"
width="${shape.boundsInParent.width}" height="${shape.boundsInParent.height}"/>
</Pane>
</Pane>
</VBox>
<Separator orientation="VERTICAL"/>
<VBox HBox.hgrow="ALWAYS" spacing="10" alignment="CENTER">
<Label text="NON-SHAPE" font="$titleFont"/>
<Separator/>
<Pane fx:id="nonShapePane" VBox.vgrow="ALWAYS">
<clip>
<Rectangle x="-5" y="-5" width="${nonShapePane.width}" height="${nonShapePane.height}"/>
</clip>
<Pane managed="false">
<Region fx:id="nonShape" prefWidth="100" prefHeight="100" style="-fx-background-color: black;"
onMousePressed="#handleMousePressed" onMouseDragged="#handleMouseDragged"
onMouseReleased="#handleMouseReleased"/>
<Rectangle fill="TRANSPARENT" stroke="$inLocalColor" strokeType="INSIDE" strokeWidth="2"
mouseTransparent="true" x="${nonShape.boundsInLocal.minX}" y="${nonShape.boundsInLocal.minY}"
width="${nonShape.boundsInLocal.width}" height="${nonShape.boundsInLocal.height}"/>
<Rectangle fill="TRANSPARENT" stroke="$inParentColor" strokeType="OUTSIDE" strokeWidth="2"
mouseTransparent="true" x="${nonShape.boundsInParent.minX}"
y="${nonShape.boundsInParent.minY}" width="${nonShape.boundsInParent.width}"
height="${nonShape.boundsInParent.height}"/>
</Pane>
</Pane>
</VBox>
</HBox>
Running the above results in:
The red outlines are the bounds-in-parent of the node while the green outlines are the bounds-in-local of the node. When dragging the node the position is updated via the translate[X|Y]
properties, for both the shape and non-shape, which don't affect the bounds-in-local. But for the shape, when the mouse is released the translate transforms are copied over to the "shape properties" (i.e. Rectangle.x
and Rectangle.y
) and then the translate properties are reset to 0
. This shows how, for a shape, the bounds-in-local may have a different origin point than (0,0)
.
The fact a Shape
changes its bounds-in-local has consequences for things such as transforms. For example, if you want to rotate a node about its center using a Rotate
, then the pivot point would be different for a shape than a non-shape:
Shape (e.g. Rectangle
): (x + width / 2, y + height / 2)
1
Non-shape (e.g. Region
): (width / 2, height / 2)
2
Another thing affected is the local mouse coordinates you get from a MouseEvent
.
1. Note for shapes like Circle
you can simply use centerX
and centerY
. An interesting consequence of this for Circle
is that the bounds-in-local will have a negative min values.
2. Technically the only difference here is that x
and y
are known to be 0
.
Upvotes: 6