Hellstorm
Hellstorm

Reputation: 670

Why does some nodes have a x and y position and others not

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

Answers (1)

Slaw
Slaw

Reputation: 46170

Every Node has two different bounds properties (ignoring Node#layoutBounds) relating to two different coordinate spaces—local and parent.

  1. Node#boundsInLocal

    The rectangular bounds of this Node in the node's untransformed local coordinate space. For nodes that extend Shape, 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 with clip as well as effects set with effect.

    [...]

  2. Node#boundsInParent

    The rectangular bounds of this Node which include its transforms. boundsInParent is calculated by taking the local bounds (defined by boundsInLocal) and applying the transform created by setting the following additional variables

    1. transforms ObservableList
    2. scaleX, scaleY, scaleZ
    3. rotate
    4. layoutX, layoutY
    5. translateX, translateY, translateZ


    The resulting bounds will be conceptually in the coordinate space of the Node'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:

GIF of example application

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

Related Questions