// BlocksWorld.java, version 1.11, December 9, 1998.
// Applet for interactive blocks world.
// Copyright 1998 by Rick Wagner, all rights reserved.

import java.applet.*;
import java.awt.*;

// This is an educational example of object oriented design for a 3D graphics applet.
// Use of this source code is authorized for educational purposes only. No use without
// proper attribution to Rick Wagner (wagner@pollux.usc.edu) at the University of Southern
// California.

// Compiled with the Sun JDK 1.1 and written for the JDK 1.0 so it will run in all browsers.

// The prefix naming convention used here is a modified Hungarian notation.
// "s" is string, "sf" is single precision floating point, "i" is integer, "b" is boolean,
// "d" is dimension, and "hm" is homogeneous matrix. Other objects defined here have no prefix.
// One-based indexing of arrays is used (the zeroeth element is generally reserved for swap space).

public class BlocksWorld extends Applet
{
  // Applet instance variables:
  private String sVerNum = "1.11";                       // Only constructors can run here ("" is a constructor)
  private Dimension dApplet;                             // The applet panel size (read from the html file)
  private int iNumBlocks;                                // Number of blocks in the world
  private Cube3D Block[];                                // Array of blocks
  private HMatrix3D hmPerspXform;                        // Perspective transform
  private float sfFocalLength;                           // For perspective projection
  private Point3D ViewPoint;                             // The viewer's location in world space
  private boolean bSelectedDest = false;                 // So mousedown() knows what selection mode
  private int iSourceBlockIndex = 0;                     // For remembering the source block in stacking
  private int iDestBlockIndex = 0;                       // For remembering the destination block in stacking
  private float sfPositionFactor;                        // Viewpoint positioning
  private float sfXPosition;                             // Viewpoint positioning
  private float sfYPosition;                             // Viewpoint positioning
  private float sfPanX;                                  // Viewpoint orienting
  private float sfPanY;                                  // Viewpoint orienting
  private Image imOffScreen = null;                      // Offscreen image for double buffering
  private Graphics grOffScreen = null;                   // Offscreen graphics for double buffering

  // To allow browsers to get information about the applet:
  public String getAppletInfo()
  {
    return "BlocksWorld applet, version " + sVerNum +
           ", by Rick Wagner, copyright 1998,\nall rights reserved.\n\n" +
           "This is an educational example of object oriented design.\n" +
           "Compiled December 9, 1998. Source code use authorized for\n" +
           "educational purposes only. No use without attribution.";
  }

  // Initialize the applet
  public void init()
  {
    this.setBackground(Color.lightGray);
    dApplet = this.size();
    sfFocalLength = dApplet.width;

    iNumBlocks = 7;                                         // Initial number of blocks

    Block = new Cube3D[iNumBlocks + 1];                     // Create the block array (one based indexing)
    ScatterBlocks();                                        // Scatter the blocks on the X-Z plane

    sfPositionFactor = 1;
    sfXPosition = 0;
    sfYPosition = 0;
    sfPanX = 0;
    sfPanY = 0;
    SetupPerspXform();                                      // Uses the starting view point (0, 1000, -1732)
  }

  // Execute this code after initialization
  public void start()
  {
    this.requestFocus();                                    // So we can get keyboard input
    System.out.println("\n" + this.getAppletInfo());        // Identify self to the Java console-aware user
    this.showStatus("Welcome to the blocks world. Click blocks to stack. Spacebar to reset.");
  }

  // The applet frame painting function
  public void paint(Graphics g)
  {
    // Code for displaying images or drawing in the applet frame
    int i;
    int j;
    float sfDSQi;
    float sfDSQj;
    Point3D c;
    Point3D vp;
    Cube3D BlockCopy[];
    BlockCopy = new Cube3D[iNumBlocks + 1];

    g.clearRect(0, 0, dApplet.width, dApplet.height);

    // Make a copy of the blocks array for painting:
    for (i = 1; i <= iNumBlocks; i++)
    {
      BlockCopy[i] = new Cube3D(Block[i]);                   // We don't want to change the indexes of the
    }                                                        // original blocks.

    vp = new Point3D(-ViewPoint.getX(), -ViewPoint.getY(), -ViewPoint.getZ()); // Opposite view point

    // Painter's algorithm. Works with BlocksWorld where all the faces are the same size. Not
    // generally correct. Z-buffer algorithm is used with most low level 3D graphics libraries.
    // Sort the copies of the blocks on distance from the view point:
    for (i = 1; i <= iNumBlocks - 1; i++)                     // This fixed loop bubble sort is good enough
    {                                                         // for blocks world. More general applications
      for (j = i + 1; j <= iNumBlocks; j++)                   // will use a faster sort algorithm.
      {
        // Put most distant first:
        sfDSQi = BlockCopy[i].getDSquared(vp);                // Distance from the block to the viewpoint
        sfDSQj = BlockCopy[j].getDSquared(vp);
        if (sfDSQj > sfDSQi)
        {
          BlockCopy[0] = BlockCopy[i];                        // Use the zeroeth block for swap space
          BlockCopy[i] = BlockCopy[j];
          BlockCopy[j] = BlockCopy[0];
        }
      }
    }

    for (i = 1; i <= iNumBlocks; i++)
    {
      BlockCopy[i].paint(g, hmPerspXform, vp);                // Have the copies of the blocks paint
    }                                                         // themselves.

    // Draw a recessed frame around the applet border. Designed for gray-on-gray browser background.
    g.setColor(Color.black);
    g.drawLine(0, 0, dApplet.width - 1, 0);
    g.drawLine(0, 0, 0, dApplet.height - 1);
    g.setColor(Color.white);
    g.drawLine(0, dApplet.height - 1, dApplet.width - 1, dApplet.height - 1);
    g.drawLine(dApplet.width - 1, 1, dApplet.width - 1, dApplet.height - 1);

  } // End of paint()

  // Implements double buffering
  public void update(Graphics g)
  {
    if (imOffScreen == null)
    {
      // Make sure the offscreen and graphics exist
      imOffScreen = this.createImage(dApplet.width, dApplet.height);
      grOffScreen = imOffScreen.getGraphics();
      grOffScreen.clearRect(0, 0, dApplet.width, dApplet.height);
    }
    this.paint(grOffScreen);
    g.drawImage(imOffScreen, 0, 0, null);
  }

  public boolean mouseDown(Event e, int x, int y)
  {
    // Select a block for source or destination:
    // bSelectedDest is initially false and toggles with each mouse click on the applet frame.
    // The cube nearest (in 2D screen space) to the mouse click point gets selected.

    Point ptBCP = new Point(0, 0);                      // Block center point
    Point3D TempPoint;
    int iClosestBlockIndex = 0;
    int i;
    float sfDSquared = 0;
    float sfAdjust = 0;
    float sfMin = 100000000;                      // Some big number
    THMatrix3D TM = null;                         // Translation matrix for making the block jump

    // Find the closest block (a very simple solution to the selection interaction task (seems to work fine)):
    for (i = 1; i <= iNumBlocks; i++)
    {
      // Compute the distance from the cube centerpoint to the mouse point:
      TempPoint = Block[i].getCenterPoint();
      TempPoint.transform(hmPerspXform);
      sfAdjust =  sfFocalLength / TempPoint.getZ();       // Adjust x and y for perspective view
      ptBCP.x = dApplet.width / 2 + ((int) (TempPoint.getX() * sfAdjust));
      ptBCP.y = dApplet.height / 2 - ((int) (TempPoint.getY() * sfAdjust));
      sfDSquared = (ptBCP.x - x) * (ptBCP.x - x) + (ptBCP.y - y) * (ptBCP.y - y);
      if (sfDSquared < sfMin)
      {
        sfMin = sfDSquared;
        iClosestBlockIndex = i;
      }
    }
    if (Block[iClosestBlockIndex].topBlock())
    {
      if (bSelectedDest && iClosestBlockIndex == iSourceBlockIndex)
      {
        this.showStatus("Block " + Integer.toString(iClosestBlockIndex) + " can't be stacked on top of itself.");
      }
      else
      {
        if (bSelectedDest)
        {
          // We are selecting the destination block for stacking the previously selected block
          iDestBlockIndex = iClosestBlockIndex;
          bSelectedDest = false;                                     // Toggle the selection state

          // Compute the translation transform for stacking the block:
          TempPoint = new Point3D(Block[iDestBlockIndex].getCenterPoint().getX() -
                                  Block[iSourceBlockIndex].getCenterPoint().getX(),
                                  Block[iDestBlockIndex].getCenterPoint().getY() -
                                  Block[iSourceBlockIndex].getCenterPoint().getY() - 100, // -Y is up
                                  Block[iDestBlockIndex].getCenterPoint().getZ() -
                                  Block[iSourceBlockIndex].getCenterPoint().getZ());
          TM = new THMatrix3D(TempPoint);
          Block[iSourceBlockIndex].transform(TM);
          Block[iDestBlockIndex].setTopBlock(false);                  // The block it moves to now can't move

          if (Block[iSourceBlockIndex].getOnBlock() != 0)
          {
            // The block it was sitting on is now free to move:
            Block[Block[iSourceBlockIndex].getOnBlock()].setTopBlock(true);
          }

          Block[iSourceBlockIndex].setOnBlock(iDestBlockIndex);       // Block knows what block it sits on
          Block[iSourceBlockIndex].setBlockColor(Color.white);        // so it can free it later if it moves
          repaint();
          this.showStatus("Block " + Integer.toString(iSourceBlockIndex) + " stacked on block " +
                           iDestBlockIndex + ".");
        }
        else
        {
          // We are selecting the source block for stacking on the next block selected
          this.showStatus("Block " + Integer.toString(iClosestBlockIndex) + " selected.");

          // Change the selected block color to red and redisplay:
          Block[iClosestBlockIndex].setBlockColor(Color.red);
          repaint();

          iSourceBlockIndex = iClosestBlockIndex;
          bSelectedDest = true;                                     // Toggle the selection state
        }
      }
    }
    else
    {
      this.showStatus("Block " + Integer.toString(iClosestBlockIndex) + " has a block above it.");
    }
    return true;
  }

  public boolean keyDown(Event e, int k)
  {
    switch (k)
    {
      case 32:                                              // Space bar
      {
        ScatterBlocks();
        repaint();
        this.showStatus("Blocks world reset.");
        break;
      }
      case 88:                                              // X (translate right)
      {
        sfXPosition += (float) 10;
        SetupPerspXform();                                  // Translates right 10
        repaint();
        this.showStatus("Moved right by 10 to " + Integer.toString((int) sfXPosition) + ".");
        break;
      }
      case 89:                                              // Y (translate up)
      {
        sfYPosition += (float) 10;
        SetupPerspXform();                                  // Translates up 10
        repaint();
        this.showStatus("Moved up by 10 to " + Integer.toString((int) sfYPosition) + ".");
        break;
      }
      case 100:                                             // d (decrement number of blocks)
      {
        if (iNumBlocks > 1)
        {
          iNumBlocks--;
          ScatterBlocks();
          repaint();
          this.showStatus("Blocks world reset with " + Integer.toString(iNumBlocks) + " blocks.");
        }
        else
        {
          this.showStatus("It wouldn't make any sense to remove the last block.");
        }
        break;
      }
      case 102:                                             // f (farther)
      {
        sfPositionFactor *= (float) 1.1;
        SetupPerspXform();                                  // Move outward 10%
        repaint();
        this.showStatus("Moved farther away by 10 percent.");
        break;
      }
      case 105:                                             // i (increment number of blocks)
      {
        if (iNumBlocks < 30)
        {
          iNumBlocks++;
          Block = new Cube3D[iNumBlocks + 1];               // Might need a bigger block array
          ScatterBlocks();
          repaint();
          this.showStatus("Blocks world reset with " + Integer.toString(iNumBlocks) + " blocks.");
        }
        else
        {
          this.showStatus("You already have enough blocks to play with. Don't be greedy.");
        }
        break;
      }
      case 108:                                             // l (longer focal length)
      {
        sfFocalLength *= (float) 1.1;
        SetupPerspXform();                                  // Zooms in 10%
        repaint();
        this.showStatus("Zoomed in by 10 percent.");
        break;
      }
      case 110:                                             // n (nearer)
      {
        if (sfPositionFactor > .5)
        {
          sfPositionFactor *= (float) 0.9;
          SetupPerspXform();                                // Move inward 10%
          repaint();
          this.showStatus("Moved nearer by 10 percent.");
        }
        else
        {
          this.showStatus("Can't move any nearer.");
        }
        break;
      }
      case 115:                                             // s (shorter focal length)
      {
        sfFocalLength *= (float) 0.9;
        SetupPerspXform();                                  // Zooms out 10%
        repaint();
        this.showStatus("Zoomed out by 10 percent.");
        break;
      }
      case 120:                                             // x (translate left)
      {
        sfXPosition -= (float) 10;
        SetupPerspXform();                                  // Translates left 10
        repaint();
        this.showStatus("Moved left by 10 to " + Integer.toString((int) sfXPosition) + ".");
        break;
      }
      case 121:                                             // y (translate down)
      {
        sfYPosition -= (float) 10;
        SetupPerspXform();                                  // Translates down 10
        repaint();
        this.showStatus("Moved down by 10 to " + Integer.toString((int) sfYPosition) + ".");
        break;
      }
      case 1004:                                            // Up arrow (pan up)
      {
        sfPanX += (float) 1;
        SetupPerspXform();
        repaint();
        this.showStatus("Panned up one degree.");
        break;
      }
      case 1005:                                            // Down arrow (pan down)
      {
        sfPanX -= (float) 1;
        SetupPerspXform();
        repaint();
        this.showStatus("Panned down one degree.");
        break;
      }
      case 1006:                                            // Left arrow (pan left)
      {
        sfPanY += (float) 1;
        SetupPerspXform();
        repaint();
        this.showStatus("Panned left one degree.");
        break;
      }
      case 1007:                                            // Right arrow (pan right)
      {
        sfPanY -= (float) 1;
        SetupPerspXform();
        repaint();
        this.showStatus("Panned left one degree.");
        break;
      }
    }
    return true;
  }

  public void ScatterBlocks()
  {
    boolean bTooClose;
    boolean bFarEnough;
    Point3D ProposedCenter;
    int i;
    int j;
    int iLoopCounter = 0;
    float sfTheta;
    float sfDSquared;
    HMatrix3D hmXform = null;
    RHMatrix3DY hmRMY = null;
    for (i = 1; i <= iNumBlocks; i++)                      // Create the random blocks
    {
      Block[i] = new Cube3D();
      sfTheta = (float) Math.random() * 45;
      hmRMY = new RHMatrix3DY(sfTheta);
      Block[i].transform(hmRMY);                           // Rotate the block about its center vertical axis
      if (i > 1)
      {
        // Avoid collisions with other blocks
        bTooClose = true;
        while (bTooClose)
        {
          iLoopCounter++;
          ProposedCenter = new Point3D();
          hmXform = new HMatrix3D();
          hmXform.setElement(1, 4, (float) Math.random() * 1000 - 500);
          hmXform.setElement(3, 4, (float) Math.random() * 1000 - 500);
          ProposedCenter.transform(hmXform);
          // Test the proposed block center point against the existing blocks:
          bFarEnough = true;
          for (j = 1; j < i; j++)
          {
            sfDSquared = (Block[j].getCenterPoint().getX() - ProposedCenter.getX()) *
                         (Block[j].getCenterPoint().getX() - ProposedCenter.getX()) +
                         (Block[j].getCenterPoint().getY() - ProposedCenter.getY()) *
                         (Block[j].getCenterPoint().getY() - ProposedCenter.getY()) +
                         (Block[j].getCenterPoint().getZ() - ProposedCenter.getZ()) *
                         (Block[j].getCenterPoint().getZ() - ProposedCenter.getZ());
            if (sfDSquared < 150 * 150)
            {
              bFarEnough = false;
              break;
            }
          }
          if (bFarEnough) bTooClose = false;
          if (iLoopCounter > 1000) bTooClose = false;       // Loop safety (if there are lots of blocks,
        }                                                   // it could loop a long time).
      }
      else
      {
        hmXform = new HMatrix3D();
        hmXform.setElement(1, 4, (float) Math.random() * 1000 - 500);
        hmXform.setElement(3, 4, (float) Math.random() * 1000 - 500);
      }
      Block[i].transform(hmXform);                          // Transform the block to its new position
    }                                                       // and orientation.
    bSelectedDest = false;

  } // End of ScatterBlocks()

  public void SetupPerspXform()
  {   
    HMatrix3D hmXform;                                      // Transform matrix
    RHMatrix3DX hmRMX;                                      // Rotation transform matrices
    RHMatrix3DY hmRMY;
    RHMatrix3DZ hmRMZ;
    HMatrix3D hmXformR;                                     // Compound rotation matrix

    float vx = sfXPosition;
    float vy = 1000 * sfPositionFactor + sfYPosition;
    float vz = -1732 * sfPositionFactor;
    ViewPoint = new Point3D(vx, vy, vz);                    // The point in world space the viewer is seeing from

    // Set up the perspective transform:
    hmPerspXform = new HMatrix3D();
    hmPerspXform.setElement(4, 3, 1 / sfFocalLength);
    hmPerspXform.setElement(4, 4, 0);

    // Rotate the view direction:
    hmRMX = new RHMatrix3DX(-30 + sfPanX);                  // Rotation of viewpoint about X in degrees
    hmRMY = new RHMatrix3DY(sfPanY);                        // Rotation of viewpoint about Y in degrees
    hmRMZ = new RHMatrix3DZ(0);                             // Rotation of viewpoint about Z in degrees
    hmXformR = hmRMX.multiply(hmRMZ, hmRMX);                // Compound rotation
    hmXformR = hmRMY.multiply(hmXformR, hmRMY);             // Compound rotation

    // Transform the view:
    hmXform = new THMatrix3D(ViewPoint);                    // Translation matrix constructed from the view point
    hmXform = hmXform.multiply(hmXformR, hmXform);          // Multiply the rot. matrix by the trans. matrix

    hmPerspXform = hmXform.multiply(hmPerspXform, hmXform); // Postmultiply the perspective matrix by the view 
                                                            // transformation
  } // End of SetupPerspXform()

  // This is the atomic geometric object, a 3D point in single precision floating point coordinates:
  class Point3D
  {
    private float x;
    private float y;
    private float z;

    public Point3D()                           // Default constructor
    {
      x = 0;
      y = 0;
      z = 0;
    }

    public Point3D(Point3D p)                  // Copy constructor
    {
      x = p.x;
      y = p.y;
      z = p.z;
    }

    public Point3D(float a, float b, float c)  // General constructor
    {
      x = a;
      y = b;
      z = c;
    }

    public void setX(float a)                  // Mutator
    {
      x = a;
    }

    public void setY(float a)                  // Mutator
    {
      y = a;
    }

    public void setZ(float a)                  // Mutator
    {
      z = a;
    }

    public float getX()                        // Accessor
    {
      return x;
    }

    public float getY()                        // Accessor
    {
      return y;
    }

    public float getZ()                        // Accessor
    {
      return z;
    }

    public void transform(HMatrix3D m)         // Transform the point
    {
      float a = x;                             // Make copies for correct computation
      float b = y;
      float c = z;
      float d = 1;

      // Multiply the point vector by the transformation matrix:
      x = m.getElement(1, 1) * a + m.getElement(1, 2) * b + m.getElement(1, 3) * c + m.getElement(1, 4) * d;
      y = m.getElement(2, 1) * a + m.getElement(2, 2) * b + m.getElement(2, 3) * c + m.getElement(2, 4) * d;
      z = m.getElement(3, 1) * a + m.getElement(3, 2) * b + m.getElement(3, 3) * c + m.getElement(3, 4);
    }

  } // End of class Point3D

  // A square in 3D is built up out of four corner points going around counterclockwise as you look
  // at the square from the outside of a solid it might be a face of. A square can have any position
  // and orientation in 3-space. The center point of the square is used to compute average distance
  // for the perspective rendering painter's algorithm and is itself computed as the average of its
  // corner points.
  class Square3D
  {
    private Point3D V[];                        // Vertex array, zeroeth element is the average

    public Square3D()                           // Default constructor
    {
      V = new Point3D[5];
      V[0] = new Point3D(0, 0, 0);              // Centered at the origin
      V[1] = new Point3D(50, 50, 0);
      V[2] = new Point3D(-50, 50, 0);
      V[3] = new Point3D(-50, -50, 0);
      V[4] = new Point3D(50, -50, 0);
    }

    public Square3D(Point3D a, Point3D b, Point3D c, Point3D d)  // 4 point constructor
    {
      float x = (a.getX() + b.getX() + c.getX() + d.getX()) / 4;  // Average x
      float y = (a.getY() + b.getY() + c.getY() + d.getY()) / 4;  // Average y
      float z = (a.getZ() + b.getZ() + c.getZ() + d.getZ()) / 4;  // Average z
      V = new Point3D[5];
      V[0] = new Point3D(x, y, z);
      V[1] = new Point3D(a);
      V[2] = new Point3D(b);
      V[3] = new Point3D(c);
      V[4] = new Point3D(d);
    }

    public Square3D(Point3D a[])  // Point array constructor
    {
      float x = (a[1].getX() + a[2].getX() + a[3].getX() + a[4].getX()) / 4;  // Average x
      float y = (a[1].getY() + a[2].getY() + a[3].getY() + a[4].getY()) / 4;  // Average y
      float z = (a[1].getZ() + a[2].getZ() + a[3].getZ() + a[4].getZ()) / 4;  // Average z
      V = new Point3D[5];
      V[0] = new Point3D(x, y, z);
      V[1] = new Point3D(a[1]);
      V[2] = new Point3D(a[2]);
      V[3] = new Point3D(a[3]);
      V[4] = new Point3D(a[4]);
    }

    public Square3D(Square3D s)  // Copy constructor
    {
      V = new Point3D[5];
      V[0] = new Point3D(s.V[0]);
      V[1] = new Point3D(s.V[1]);
      V[2] = new Point3D(s.V[2]);
      V[3] = new Point3D(s.V[3]);
      V[4] = new Point3D(s.V[4]);
    }

    public Point3D getPoint(int i)
    {
      return new Point3D(V[i]);             // Makes a copy of the point
    }

    public void transform(HMatrix3D m)      // Transform the square
    {
      int i = 0;
      for (i = 0; i <=4; i++)
      {
        V[i].transform(m);                  // Transform all the points in the square
      }
    }

    // Get the distance squared to some point:
    public float getDSquared(Point3D p)
    {
      float sfDSquared;
      sfDSquared = (V[0].getX() - p.getX()) * (V[0].getX() - p.getX()) +
                   (V[0].getY() - p.getY()) * (V[0].getY() - p.getY()) +
                   (V[0].getZ() - p.getZ()) * (V[0].getZ() - p.getZ());
      return sfDSquared;                              // Distance squared from the square to the point
    }

  } // End of class Square3D

  // The cube class is built of 6 squares (a surface model), by default with its bottom
  // face on the X-Z plane. The cube has a paint() method for painting itself.
  class Cube3D
  {
    private Square3D F[];                              // Face array
    private Point3D centerPoint;                       // For distance calculations
    private Color BlockColor;                          // The block knows its own color
    private boolean bTopBlock;                         // Not any blocks on top of it, true by default
    private int iOnBlockIndex;                         // Index of the block this one rests on, 0 by default

    public Cube3D()                                    // Default constructor
    {
      bTopBlock = true;
      iOnBlockIndex = 0;
      BlockColor = Color.white;
      Point3D V[];
      centerPoint = new Point3D(0, -50, 0);            // 50 units above origin, -Y is up
      V = new Point3D[5];
      F = new Square3D[7];

      V[1] = new Point3D(50, -100, 50);                 // Front face
      V[2] = new Point3D(-50, -100, 50);
      V[3] = new Point3D(-50, 0, 50);
      V[4] = new Point3D(50, 0, 50);
      F[1] = new Square3D(V);

      V[1] = new Point3D(50, -100, -50);                // Right face
      V[2] = new Point3D(50, -100, 50);
      V[3] = new Point3D(50, 0, 50);
      V[4] = new Point3D(50, 0, -50);
      F[2] = new Square3D(V);

      V[1] = new Point3D(-50, -100, -50);               // Back face
      V[2] = new Point3D(50, -100, -50);
      V[3] = new Point3D(50, 0, -50);
      V[4] = new Point3D(-50, 0, -50);
      F[3] = new Square3D(V);

      V[1] = new Point3D(-50, -100, 50);                // Left face
      V[2] = new Point3D(-50, -100, -50);
      V[3] = new Point3D(-50, 0, -50);
      V[4] = new Point3D(-50, 0, 50);
      F[4] = new Square3D(V);

      V[1] = new Point3D(50, -100, -50);                // Top face
      V[2] = new Point3D(-50, -100, -50);
      V[3] = new Point3D(-50, -100, 50);
      V[4] = new Point3D(50, -100, 50);
      F[5] = new Square3D(V);

      V[1] = new Point3D(50, 0, -50);                  // Top face
      V[2] = new Point3D(50, 0, 50);
      V[3] = new Point3D(-50, 0, 50);
      V[4] = new Point3D(-50, 0, -50);
      F[6] = new Square3D(V);
    }

    public Cube3D(Cube3D c)                            // Copyconstructor
    {
      int i;
      bTopBlock = c.bTopBlock;
      iOnBlockIndex = c.iOnBlockIndex;
      BlockColor = c.BlockColor;
      centerPoint = new Point3D(c.centerPoint);           
      F = new Square3D[7];
      for (i = 1; i <= 6; i++)
      {
        F[i] = new Square3D(c.F[i]);
      }
    }

    public void transform(HMatrix3D m)                 // transform the cube
    {
      centerPoint.transform(m);
      int i = 0;
      for (i = 1; i <=6; i++)
      {
        F[i].transform(m);
      }
    }

    // Get the square of the distance from the center of the cube to some point:
    public float getDSquared(Point3D p)
    {
      float sfDSquared;
      sfDSquared = (centerPoint.getX() - p.getX()) * (centerPoint.getX() - p.getX()) +
                   (centerPoint.getY() - p.getY()) * (centerPoint.getY() - p.getY()) +
                   (centerPoint.getZ() - p.getZ()) * (centerPoint.getZ() - p.getZ());
      return sfDSquared;                             // Distance squared from the cube to the point
    }

    public Point3D getCenterPoint()
    {
      return new Point3D(centerPoint);      // Return a copy of the center point of the cube
    }

    // The cube paints itself:
    public void paint(Graphics g, HMatrix3D pxf, Point3D vp)
    {
      int i;
      int j;
      Polygon Pgon;
      int x[];             // For passing x coordinates to the polygon
      int y[];             // For passing y coordinates to the polygon
      x = new int[5];
      y = new int[5];
      Point3D TempPoint;   // Temporary point
      float sfAdjust;
      float sfDSQi;
      float sfDSQj;

      // Sort the faces on distance from the viewpoint:
      for (i = 1; i <= 5; i++)                              // This fixed loop bubble sort is good enough
      {                                                     // for a simple cube. More general applications
        for (j = i + 1; j <= 6; j++)                        // will use a faster sort algorithm.
        {
          // Put most distant first:
          sfDSQi = F[i].getDSquared(vp);                    // Distance from the face to the viewpoint
          sfDSQj = F[j].getDSquared(vp);
          if (sfDSQj > sfDSQi)
          {
            F[0] = F[j];                                    // Use the zeroeth face for swap space
            F[j] = F[i];
            F[i] = F[0];
          }
        }
      }
      // Render the faces to the applet panel:
      for (i = 1; i <= 6; i++)                              // For each face
      {
        // Establish a front clipping plane so we don't render faces behind us:
        TempPoint = F[i].getPoint(0);                       // Center point of the face
        TempPoint.transform(pxf);
        if (TempPoint.getZ() < -100)
        {
          for (j = 1; j <= 4; j++)                          // For each point of the face
          {
            TempPoint = F[i].getPoint(j);
            TempPoint.transform(pxf);
            sfAdjust =  sfFocalLength / TempPoint.getZ();   // Adjust x and y for perspective view
            x[j - 1] = dApplet.width / 2 + ((int) (TempPoint.getX() * sfAdjust));
            y[j - 1] = dApplet.height / 2 - ((int) (TempPoint.getY() * sfAdjust));
          }
          Pgon = new Polygon(x, y, 4);
          g.setColor(BlockColor);
          g.fillPolygon(Pgon);
          g.setColor(Color.black);
          g.drawPolygon(Pgon);
        }
      }
    } // End of Cube3D paint()

    public Color getBlockColor()                   // Accessor
    {
      return BlockColor;
    }

    public void setBlockColor(Color c)             // Mutator
    {
      BlockColor = c;
    }

    public boolean topBlock()                      // Accessor
    {
      return bTopBlock;
    }

    public void setTopBlock(boolean v)             // Mutator
    {
      bTopBlock = v;
    }

    public int getOnBlock()                        // Accessor
    {
      return iOnBlockIndex;
    }

    public void setOnBlock(int i)                  // Mutator
    {
      iOnBlockIndex = i;
    }

  } // End of class Cube3D

  // Having a 3D homogeneous matrix object is very convenient. The matrix class can
  // also multiply two matrices.
  class HMatrix3D                      // Homogeneous matrix class
  {
    private float m[][];               // Matrix elements

    public HMatrix3D()                 // Default constructor
    {
      int i = 0;
      int j = 0;
      m = new float[5][5];             // 1-based indexing
      for (i = 1; i <= 4; i++)         // rows
      {
        for (j = 1; j <= 4; j++)       // columns
        {
          if (i == j)
          {
            m[i][j] = 1;
          }
          else
          {
            m[i][j] = 0;               // Identity matrix by default
          }
        }
      }
    } // End of constructor

    public void setElement(int i, int j, float a)  // Mutator
    {
      m[i][j] = a;
    }

    public float getElement(int i, int j)          // Accessor
    {
      return m[i][j];
    }

    public HMatrix3D multiply(HMatrix3D A, HMatrix3D B)         // Post-multiply by another matrix
    {
      HMatrix3D C = new HMatrix3D();                            // New matrix to return
      int i = 0;
      int j = 0;
      int k = 0;
      for (i = 1; i <= 4; i++)         // rows
      {
        for (j = 1; j <= 4; j++)       // columns
        {
          C.m[i][j] = 0;
          for (k = 1; k <= 4; k++)
          {
            C.m[i][j] += A.m[i][k] * B.m[k][j];
          }
        }
      }
      return C;
    }

  } // End of class HMatrix3D

  // Use inheritance to create some special matrix classes:

  class THMatrix3D extends HMatrix3D                       // Translation matrix class
  {
    public THMatrix3D(float x, float y, float z)           // Three-parameter constructor
    {
      super();
      super.m[1][4] = (x);
      super.m[2][4] = (y);
      super.m[3][4] = (z);
    }

    public THMatrix3D(Point3D p)                           // Point constructor
    {
      super();
      super.m[1][4] = (p.getX());
      super.m[2][4] = (p.getY());
      super.m[3][4] = (p.getZ());
    }
  }

  class RHMatrix3DX extends HMatrix3D                      // Rotation matrix class
  {
    public RHMatrix3DX(float Theta)                        // Constructor, Theta in degrees
    {
      super();
      super.m[2][2] = (float) Math.cos(Theta * Math.PI / 180);
      super.m[2][3] = (float) -Math.sin(Theta * Math.PI / 180);
      super.m[3][2] = (float) Math.sin(Theta * Math.PI / 180);
      super.m[3][3] = (float) Math.cos(Theta * Math.PI / 180);
    }
  }

  class RHMatrix3DY extends HMatrix3D                       // Rotation matrix class
  {
    public RHMatrix3DY(float Theta)                         // Constructor, Theta in degrees
    {
      super();
      super.m[1][1] = (float) Math.cos(Theta * Math.PI / 180);
      super.m[1][3] = (float) Math.sin(Theta * Math.PI / 180);
      super.m[3][1] = (float) -Math.sin(Theta * Math.PI / 180);
      super.m[3][3] = (float) Math.cos(Theta * Math.PI / 180);
    }
  }

  class RHMatrix3DZ extends HMatrix3D                       // Rotation matrix class
  {
    public RHMatrix3DZ(float Theta)                         // Constructor, Theta in degrees
    {
      super();
      super.m[1][1] = (float) Math.cos(Theta * Math.PI / 180);
      super.m[1][2] = (float) -Math.sin(Theta * Math.PI / 180);
      super.m[2][1] = (float) Math.sin(Theta * Math.PI / 180);
      super.m[2][2] = (float) Math.cos(Theta * Math.PI / 180);
    }
  }

} // End of Applet class
