Home » Blog » Simple 3D Graphics using JavaFX

Share This Post

Desktop / Featured Tutorials / Java

Simple 3D Graphics using JavaFX

Simple3D Graphics JavaFX rewrite of my first article using Swing

Simple 3D graphics using Java was my first article using Swing UI. As you know JavaFX is the replacement for Swing. In this article I will talk about JavaFX a bit more than the app itself because it was a conversion. This is not only a JavaFX app it’s a JavaFX FXML app. So like Android apps there are two ways to define the UI. You can build it with code within class  definitions and constructors. Or you can define the UI with XML. In this case its FXML for Java FX XML.

SceneBuilder and NetBeans

You can define the FXML with a text/code editor. This would be painful and you would have to know FXML fairly well and do a lot of looking things up. Or you can download SceneBuilder from Gluon. You can use the FXML generated with any IDE or without and IDE. But SceneBuilder happens to work well with NetBeans IDE which I am using. Simply because if you click the FXML file it loads it up in SceneBuilder for you to edit. Then when you save it puts it right back in your project structure.

Two main containers I used is VBOX and HBOX and sometimes GRID is useful. These are similar to the Swing Layouts. In Swing I used BorderLayout a lot. There is a BORDER type container like that in JavaFX but I’m not sure when I’ll need that now that I have VBOX, HBOX and GRID. In JavaFX you do not use layouts, simply containers.

You will notice in the first main class that it is very simple. It extends  javafx Application class and overrides the start() method which is called by the launch() method of the Application class within the main() method. It’s almost like an applet in the way it works. So first we load the FXML which is the root for the Scene. And this is also the “Scene Graph”. A sceen graph is a tree structure, not unlike the HTML DOM in a sence. It contains all your UI components. The start method is passed a stage which I believe would be like the JFrame in Swing. So you set the stage with the scene which has the root for your UI. Then set its size and show it.

I will explain the FXML later with the code. But it has a root container and the a GRID for the buttons. It has a Canvas for the drawing. Note the try catch code around the launch() method. This helps with debugging because unless you do this you get a generic exception message “java.lang.reflect.InvocationTargetException” that tells you nothing. Once you add the try catch, you will get more specific exception info, such as NullPointerException etc.

Simple3DFX main class

package simple3dfx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
 *
 * @author larry
 */
public class Simple3DFX extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("simple3d.fxml"));

        Scene scene = new Scene(root);
        stage.setTitle("Simple3D FX");
        stage.setScene(scene);
        stage.setWidth(800);
        stage.setHeight(650);
        stage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        try {
            launch(args);
        } catch (Exception e) {
            if (e.getCause().getClass().equals(AssertionError.class)) {
                e.getCause().printStackTrace();
            } else {
                e.getCause().printStackTrace();
                // handle the rest of the world exception 
            }
        }
    }

}

The Controller Class

The controller class is loaded from the FXML when it is loaded. This class contains 3 Inner classes, Point2D, Point3D and Surface3D. It has fields that contain references to the UI components that it needs to control. It has methods related to the buttons on the UI to perform user actions. And it has an initialize() method. Also paint2DShapes() method which is like the paintComponent more or less of JPanel. Only you call it whatever you like, such as update() or whatever suits you best.

Surface3D creates the 3D surface data model. I chose a very simplistic crude algorithm for this. A person might use fractal math. You might load elevation data from a real source of topography. I created another algorithm that was stack based and made something looking like a tropical island once. In this app I also threw in a method to aid the draw2DShapes() method which sets the pen color based on the average elevation of a 3D Line.

The fields that refer to the UI Components and the event handling methods called doThis or doThat were generated by SceneBuilder. @FMXL refers to things in the FXML file. You an set names for the components and the event handling methods under the Code tab in SceneBuilder. The initialize() method is also generated by SceneBuilder and its where you should put initialization code that you create instead of putting it in the constructor. I had some problems putting some code in the constructor at one point but it worked when I put it in the initialization() method.

Very Important Note about the drawing of graphics on the Canvas!

I wasted about 16 hours on this one problem. Everything was working in the conversion to JavaFX except for the clearing of the canvas to redraw after the user made some change. I made simple case test just drawing a few lines and had similar results. I tried fillRect(), strokeRect() and clearRect() and nothing worked. Every time I pressed a button all that happened was more lines drawn on top of old lines. I thought it was a bug in the API.

After looking up other examples I found one and tried it which drew some lines and circles and text. You could drag and drop and add circles. It worked! After carefully examining the code my code did exactly the same thing! I’m beating my head against the wall. I found a few articles where other people seem to have the same trouble. I found out that the Canvas object has a buffer so that when you draw into it, it adds objects to the buffer. The problem was clearRect() or fillRect() didn’t seem to do what they are supposed to do. Basically any objects underneath the rectangular area should be removed from the buffer.

Finnally I went back to the example that drew the lines, circles and text and looked again very carefully. The only difference was that I was using moveTo() and lineTo() methods and stroke() methods. It used strokeLine() strokeCircle() storkeText() etc. So as you can see from the code below I switch from moveTo LineTo to strokeLine() and it worked. Maybe I should actually read the freaking manual and maybe there is a reason for this difference. I think maybe moveTo lineTo adds to some path object that fillRect() and clearRect() didn’t remove or erase. This was almost an invisible problem because I was unaware that there were two ways to draw lines.

Simple3DController class

package simple3dfx;

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;

/**
 *
 * @author larry
 */
public class Simple3dController implements Initializable {

    public Simple3dController() throws IOException {

        //FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("simple3d.fxml"));
        // fxmlLoader.setRoot(this);
        // fxmlLoader.setController(this);
        // fxmlLoader.load();
    }

    public class Point2D {

        public Point2D(int x, int y) {
            this.x = x;
            this.y = y;
        }
        int x = 0;
        int y = 0;
    }

    public class Point3D {

        public Point3D(int x, int y, int z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        int x = 0;
        int y = 0;
        int z = 0;
    }
    int step = 50;
    int mode = 1;
    public final int LEFT_ISO = 0;
    public final int CENTER_ISO = 1;
    public final int RIGHT_ISO = 2;
    public float ySkew = 1.0f;

    public void setTransformMode(int mode) {
        this.mode = mode;
    }
    float y2dSkew=1.0f;
    public Point2D transform3D(Point3D point3D) throws Exception {
        switch (mode) {
            case LEFT_ISO:
                return new Point2D(point3D.x + point3D.z, (int) (((-point3D.y*y2dSkew) + point3D.z) * ySkew));
            case CENTER_ISO:
                return new Point2D(point3D.x + point3D.z, (int) (((-point3D.y*y2dSkew) + point3D.z - point3D.x) * ySkew));
            case RIGHT_ISO:
                return new Point2D(point3D.x - point3D.z, (int) (((-point3D.y*y2dSkew) + point3D.z) * ySkew));
            default:
                throw new Exception("Invalid Transformation Mode! [" + mode + "]?");
        }
    }

    public class Surface3D {

        int xSquareSize = 5, ySquareSize = 5;
        int xGridSize, yGridSize;
        int[][] surfaceElevations = null;
        int[][] surfaceElevationsAverage = null;

        public int getElevation(int x, int y) {
            return surfaceElevations[y][x];
        }

        public void setElevation(int x, int y, int elevation) {
            surfaceElevations[y][x] = elevation;
        }

        public int randomElevation(int minHeight, int maxHeight) {
            return (int) (Math.random() * (maxHeight - minHeight) + minHeight);
        }

        public int getElevationAvg(int x, int y) {
            return surfaceElevations[y][x];
        }

        public void setElevationAvg(int x, int y, int elevation) {
            surfaceElevationsAverage[y][x] = elevation;
        }

        public int average(int x, int y) {
            int total = getElevation(x - 1, y - 1);
            total += getElevation(x, y - 1);
            total += getElevation(x + 1, y - 1);
            total += getElevation(x - 1, y);
            total += getElevation(x, y);
            total += getElevation(x + 1, y);
            total += getElevation(x - 1, y + 1);
            total += getElevation(x, y + 1);
            total += getElevation(x + 1, y + 1);
            return (int) total / 9;
        }

        Surface3D(int xGridSize, int yGridSize, int nRandomHeights, int minHeight, int maxHeight) {
            this.xGridSize = xGridSize;
            this.yGridSize = yGridSize;
            surfaceElevations = new int[xGridSize][yGridSize];
            int randomX, randomY;
            for (int i = 0; i < nRandomHeights; i++) {
                randomX = (int) (Math.random() * (xGridSize - 2) + 1);//-2 leaves 0's in outsize edge points
                randomY = (int) (Math.random() * (yGridSize - 2) + 1);
                setElevation(randomX, randomY, randomElevation(minHeight, maxHeight));
            }
            surfaceElevationsAverage = new int[xGridSize][yGridSize];
            for (int i = 0; i < 2; i++) {
                for (int y = 1; y < yGridSize - 1; y++) {
                    for (int x = 1; x < xGridSize - 1; x++) {
                        setElevationAvg(x, y, average(x, y));
                    }
                }
                surfaceElevations = surfaceElevationsAverage;
            }
        }
    }
    Surface3D surface = new Surface3D(50, 50, 150, -300, 300);

    public int originX = 0;
    public int originY = 0;
    //public float ySkew = 1.0f;
    //public Iso3D iso3D = new Iso3D();
    GraphicsContext gc = null;
    Simple3dController simple3dController = null;
    
    private void setElevationColor(int y1,int y2){
        int yAvg=(y1+y2)/2;
        if((yAvg>-100)&&(yAvg<-10)) gc.setStroke(Color.DARKBLUE);
        if((yAvg>-10)&&(yAvg<-2))gc.setStroke(Color.BLUE);
        if((yAvg>-2)&&(yAvg<0)) gc.setStroke(Color.AQUAMARINE);
        if((yAvg>0)&&(yAvg<10)) gc.setStroke(Color.GREEN);
        if((yAvg>10)&&(yAvg<15)) gc.setStroke(Color.GRAY);
        if((yAvg>15)&&(yAvg<20)) gc.setStroke(Color.WHITE);
    }

    public void draw2DShapes() {
        gc.setLineWidth(1.0);
        gc.setFill(Color.BLACK);
        //gc.setStroke(Color.BLACK);
        //gc.clearRect(0, 0, canvas3d.getWidth(), canvas3d.getHeight());
        gc.fillRect(0.0d, 0.0d, canvas3d.getWidth(), canvas3d.getHeight());
        //gc.strokeRect(0, 0, canvas3d.getWidth(), canvas3d.getHeight());
        // = canvas3d.getParent();

        //gc.setStroke(Color.BLUE);
        Point2D point1;
        Point2D point2;
        Point3D point1_3D;
        Point3D point2_3D;
        int x;
        int y;
        int xs;
        int ys;
        xs = surface.xSquareSize;
        ys = surface.ySquareSize;
        gc.setStroke(Color.GREEN);
        for (y = 0; y < surface.yGridSize; y++) {
            for (x = 0; x < surface.xGridSize; x++) {
                try {
                    if (x < surface.xGridSize - 1) {
                        point1_3D=new Point3D(x * xs, (int) (surface.getElevationAvg(x, y) * ySkew), y * ys);
                        point2_3D=new Point3D((x + 1) * xs, (int) (surface.getElevationAvg(x + 1, y) * ySkew), y * ys);
                        
                        point1 = transform3D(point1_3D);
                        point2 = transform3D(point2_3D);
                        setElevationColor(point1_3D.y,point2_3D.y);
                        gc.strokeLine(point1.x + originX, point1.y + originY, point2.x + originX, point2.y + originY);
                        //gc.moveTo(point1.x + originX, point1.y + originY);
                        //gc.lineTo(point2.x + originX, point2.y + originY);
                        //gc.stroke();

                        //from Swing gc.drawLine(point1.x + originX, point1.y + originY, point2.x + originX, point2.y + originY);
                    }
                    if (y < surface.yGridSize - 1) {
                        point1_3D=new Point3D(x * xs, (int) (surface.getElevationAvg(x, y) * ySkew), y * ys);
                        point2_3D=new Point3D(x * xs, (int) (surface.getElevationAvg(x, y + 1) * ySkew), (y + 1) * ys);
                        point1 = transform3D(point1_3D);
                        point2 = transform3D(point2_3D);
                        setElevationColor(point1_3D.y,point2_3D.y);
                        gc.strokeLine(point1.x + originX, point1.y + originY, point2.x + originX, point2.y + originY);
                        //gc.moveTo(point1.x + originX, point1.y + originY);
                        //gc.lineTo(point2.x + originX, point2.y + originY);
                        //gc.stroke();

                        //from Swing gc.drawLine(point1.x + originX, point1.y + originY, point2.x + originX, point2.y + originY);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        gc.stroke();
    }

    @FXML // fx:id="canvas3d"
    private Canvas canvas3d;

    @FXML // fx:id="modeLeftBtn"
    private Button modeLeftBtn; // Value injected by FXMLLoader

    @FXML // fx:id="modeCenterBtn"
    private Button modeCenterBtn; // Value injected by FXMLLoader

    @FXML // fx:id="modeRightBtn"
    private Button modeRightBtn; // Value injected by FXMLLoader

    @FXML // fx:id="leftBtn"
    private Button leftBtn; // Value injected by FXMLLoader

    @FXML // fx:id="rightBtn"
    private Button rightBtn; // Value injected by FXMLLoader

    @FXML // fx:id="upBtn"
    private Button upBtn; // Value injected by FXMLLoader

    @FXML // fx:id="downBtn"
    private Button downBtn; // Value injected by FXMLLoader

    @FXML // fx:id="yPlusBtn"
    private Button yPlusBtn; // Value injected by FXMLLoader

    @FXML // fx:id="yMinusBtn"
    private Button yMinusBtn; // Value injected by FXMLLoader

    @FXML // fx:id="twoDYPlusBtn"
    private Button twoDYPlusBtn; // Value injected by FXMLLoader

    @FXML // fx:id="zoomPlusBtn"
    private Button zoomPlusBtn; // Value injected by FXMLLoader

    @FXML // fx:id="zoomMinusBtn"
    private Button zoomMinusBtn; // Value injected by FXMLLoader

    @FXML
    void doModeLeft(ActionEvent event) {
        mode = 0;
        draw2DShapes();
    }

    @FXML
    void doModeCenter(ActionEvent event) {
        mode = 1;
        draw2DShapes();
    }

    @FXML
    void doModeRight(ActionEvent event) {
        mode = 2;
        draw2DShapes();
    }

    @FXML
    void up(ActionEvent event) {
        originY -= step;
        draw2DShapes();
    }

    @FXML
    void down(ActionEvent event) {
        originY += step;
        draw2DShapes();
    }

    @FXML
    void left(ActionEvent event) {
        originX -= step;
        draw2DShapes();
    }

    @FXML
    void right(ActionEvent event) {
        originX += step;
        draw2DShapes();
    }

    @FXML
    void yMinus(ActionEvent event) {
        ySkew += 0.1f;
        if (ySkew == 0) {
            ySkew = 0.1f;
        }
        draw2DShapes();
    }

    @FXML
    void yPlus(ActionEvent event) {
        ySkew -= 0.1f;
        if (ySkew == 0) {
            ySkew = -0.1f;
        }
        draw2DShapes();
    }

    @FXML
    void twoDYPlus(ActionEvent event) {
        y2dSkew += .1f;
        draw2DShapes();
    }

    @FXML
    void zoomMinus(ActionEvent event) {
        surface.xSquareSize -= 1;
        surface.ySquareSize -= 1;
        if (surface.xSquareSize <= 0) {
            surface.xSquareSize = 1;
        }
        if (surface.ySquareSize <= 0) {
            surface.ySquareSize = 1;
        }

        draw2DShapes();
    }

    @FXML
    void zoomPlus(ActionEvent event) {
        surface.xSquareSize += 1;
        surface.ySquareSize += 1;
        draw2DShapes();
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {

        assert modeLeftBtn != null : "fx:id=\"modeLeftBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert modeCenterBtn != null : "fx:id=\"modeCenterBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert modeRightBtn != null : "fx:id=\"modeRightBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert leftBtn != null : "fx:id=\"leftBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert rightBtn != null : "fx:id=\"rightBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert upBtn != null : "fx:id=\"upBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert downBtn != null : "fx:id=\"downBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert yPlusBtn != null : "fx:id=\"yPlusBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert yMinusBtn != null : "fx:id=\"yMinusBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert twoDYPlusBtn != null : "fx:id=\"twoDYPlusBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert zoomPlusBtn != null : "fx:id=\"zoomPlusBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        assert zoomMinusBtn != null : "fx:id=\"zoomMinusBtn\" was not injected: check your FXML file 'simple3d.fxml'.";
        
            gc = canvas3d.getGraphicsContext2D();
            draw2DShapes();
       
    }

}

Simple 3D FXML

So we basically have…

  • VBox
    • Menu stuff (unused)
    • GridPane
      • Buttons
    • Canvas

And you notice <children> no idea why thats needed except to group. There is also a Group container. The buttons and other components can have a fx:id=”modeCenterBtn” attribute. This names it and is what you get as fields in the controller class. Components like buttons that have actions also have onAction=”#doModeLeft” attributes which define what method to call when the user does something. And that is about it, except to say that if you need to sublcass a baseclass such as Canvas to added sizing methods for auto resize for example you can simply change the import statement to point to in this case ‘<?import simple3dfx.ResizableCanvas ?>’. Then rename the tag from ‘<Canvas’ to ‘<ResizableCanvas’. It’s a bit more envolved to get SceneBuilder to show your new component properly.

‘fx:controller=”simple3dfx.Simple3dController”>’ is what defines the Controller class and its instantiated when the FXML file is loaded. This attribute goes in the root element which in this case is VBox.

From what I understand you must make a jar file with your new Components and add them from Library, the ger icon then JAR/FXML manager. You may be able to do it with an FXML file also.

simple3d.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SeparatorMenuItem?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<VBox prefHeight="600.0" prefWidth="640.0" xmlns:fx="http://javafx.com/fxml/1" fx:controller="simple3dfx.Simple3dController">

    <children>
        <MenuBar VBox.vgrow="NEVER">
            <menus>
                <Menu mnemonicParsing="false" text="File">
                    <items>
                        <MenuItem mnemonicParsing="false" text="New" />
                        <MenuItem mnemonicParsing="false" text="Open…" />
                        <Menu mnemonicParsing="false" text="Open Recent" />
                        <SeparatorMenuItem mnemonicParsing="false" />
                        <MenuItem mnemonicParsing="false" text="Close" />
                        <MenuItem mnemonicParsing="false" text="Save" />
                        <MenuItem mnemonicParsing="false" text="Save As…" />
                        <MenuItem mnemonicParsing="false" text="Revert" />
                        <SeparatorMenuItem mnemonicParsing="false" />
                        <MenuItem mnemonicParsing="false" text="Preferences…" />
                        <SeparatorMenuItem mnemonicParsing="false" />
                        <MenuItem mnemonicParsing="false" text="Quit" />
                    </items>
                </Menu>
                <Menu mnemonicParsing="false" text="Edit">
                    <items>
                        <MenuItem mnemonicParsing="false" text="Undo" />
                        <MenuItem mnemonicParsing="false" text="Redo" />
                        <SeparatorMenuItem mnemonicParsing="false" />
                        <MenuItem mnemonicParsing="false" text="Cut" />
                        <MenuItem mnemonicParsing="false" text="Copy" />
                        <MenuItem mnemonicParsing="false" text="Paste" />
                        <MenuItem mnemonicParsing="false" text="Delete" />
                        <SeparatorMenuItem mnemonicParsing="false" />
                        <MenuItem mnemonicParsing="false" text="Select All" />
                        <MenuItem mnemonicParsing="false" text="Unselect All" />
                    </items>
                </Menu>
                <Menu mnemonicParsing="false" text="Help">
                    <items>
                        <MenuItem mnemonicParsing="false" text="About MyHelloApp" />
                    </items>
                </Menu>
            </menus>
        </MenuBar>
        <GridPane prefHeight="76.0" prefWidth="640.0">
              <columnConstraints>
                  <ColumnConstraints hgrow="SOMETIMES" maxWidth="262.0" minWidth="10.0" prefWidth="227.0" />
                  <ColumnConstraints hgrow="SOMETIMES" maxWidth="199.0" minWidth="10.0" prefWidth="53.0" />
                  <ColumnConstraints hgrow="SOMETIMES" maxWidth="214.0" minWidth="10.0" prefWidth="52.0" />
                  <ColumnConstraints hgrow="SOMETIMES" maxWidth="325.0" minWidth="10.0" prefWidth="173.0" />
                  <ColumnConstraints hgrow="SOMETIMES" maxWidth="106.0" minWidth="10.0" prefWidth="70.0" />
                  <ColumnConstraints hgrow="SOMETIMES" maxWidth="96.0" minWidth="10.0" prefWidth="65.0" />
              </columnConstraints>
              <rowConstraints>
                  <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                  <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                  <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
              </rowConstraints>
              <children>
                  <Button fx:id="modeLeftBtn" minWidth="90.0" mnemonicParsing="false" onAction="#doModeLeft" text="Mode Left" />
                  <Button fx:id="modeCenterBtn" minWidth="90.0" mnemonicParsing="false" onAction="#doModeCenter" prefHeight="25.0" prefWidth="90.0" text="Mode Center" GridPane.rowIndex="1" />
                  <Button fx:id="modeRightBtn" minWidth="90.0" mnemonicParsing="false" onAction="#doModeRight" text="Mode Right" GridPane.rowIndex="2" />
                  <Button fx:id="leftBtn" minWidth="50.0" mnemonicParsing="false" onAction="#left" text="Left" GridPane.columnIndex="1" GridPane.rowIndex="1" />
                  <Button fx:id="rightBtn" minWidth="50.0" mnemonicParsing="false" onAction="#right" text="Right" GridPane.columnIndex="3" GridPane.rowIndex="1" />
                  <Button fx:id="upBtn" minWidth="50.0" mnemonicParsing="false" onAction="#up" text="Up" GridPane.columnIndex="2" />
                  <Button fx:id="downBtn" minWidth="50.0" mnemonicParsing="false" onAction="#down" prefHeight="25.0" prefWidth="50.0" text="Down" GridPane.columnIndex="2" GridPane.rowIndex="2" />
                  <Button fx:id="yPlusBtn" minWidth="50.0" mnemonicParsing="false" onAction="#yPlus" prefWidth="50.0" text="Y+" GridPane.columnIndex="4" />
                  <Button fx:id="yMinusBtn" minWidth="50.0" mnemonicParsing="false" onAction="#yMinus" prefWidth="50.0" text="Y-" GridPane.columnIndex="4" GridPane.rowIndex="1" />
                  <Button fx:id="twoDYPlusBtn" minWidth="50.0" mnemonicParsing="false" onAction="#twoDYPlus" prefHeight="25.0" prefWidth="50.0" text="2D Y+" GridPane.columnIndex="4" GridPane.rowIndex="2" />
                  <Button fx:id="zoomPlusBtn" minWidth="50.0" mnemonicParsing="false" onAction="#zoomPlus" prefWidth="60.0" text="Zoom+" GridPane.columnIndex="5" />
                  <Button fx:id="zoomMinusBtn" minWidth="50.0" mnemonicParsing="false" onAction="#zoomMinus" prefWidth="60.0" text="Zoom-" GridPane.columnIndex="5" GridPane.rowIndex="1" />
              </children>
          </GridPane>
          <Canvas fx:id="canvas3d" height="500.0" width="1024.0" />
    </children>
</VBox>

 

Share This Post

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>