Controlling a robot arm in a Java GUI program

I have this funny program for playing around with 2D vectors:

Vector2D.java:

package net.coderodde.robohand;

import java.awt.geom.Point2D;
import java.util.List;
import java.util.ArrayList;
import java.util.Objects;

/**
* This class implements a vector. The tail of another vector can be attached to
* the head of this vector.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Apr 17, 2016)
*/
public class Vector2D {

/**
* The <tt>x</tt>-coordinate of the tail of this vector.
*/
private float locationX;

/**
* The <tt>y</tt>-coordinate of the tail of this vector.
*/
private float locationY;

/**
* The length of this vector.
*/
private float length;

/**
* The angle of this vector in radians.
*/
private float angle;

/**
* The list of all the child vectors of this vector. Child vectors' tails
* are attached to the head of this vector.
*/
private final List<Vector2D> childVectors = new ArrayList<>();

/**
* Constructs a vector with given length and angle.
*
* @param length the length of a new vector.
* @param angle  the angle in radians of a new vector.
*/
public Vector2D(float length, float angle) {
this.length = length;
this.angle = angle;
}

/**
* Connects {@code vector} as a child to this vector.
*
* @param vector the child vector to add.
*/
public void addChildVector(Vector2D vector) {
Objects.requireNonNull(vector, "The input vector is null.");
}

/**
* Removes a child vector if it is connected to the tail of this vector.
*
* @param vector the vector to remove from the child list.
*/
public void removeChildVector(Vector2D vector) {
Objects.requireNonNull(vector, "The input vector is null.");
childVectors.remove(vector);
}

/**
* Gets the location of the tail of this vector.
*
* @return the tail location.
*/
public Point2D.Float getLocation() {
return new Point2D.Float(locationX, locationY);
}

/**
* Sets the location of this vector. If this vector has child vectors, they
* are moved as well.
*
* @param locationX the <tt>x</tt>-coordinate of the new location.
* @param locationY the <tt>y</tt>-coordinate of the new location.
*/
public void setLocation(float locationX, float locationY) {
this.locationX = locationX;
this.locationY = locationY;
Point2D.Float point = getHeadLocation();

for (Vector2D childVector : childVectors) {
childVector.setLocation(point.x, point.y);
}
}

/**
* Returns the length of this vector.
*
* @return the length of this vector.
*/
public float getLength() {
return length;
}

/**
* Sets the new length of this vector. If this vector has child vectors,
* they are moved as well.
*
* @param length the new length.
*/
public void setLength(float length) {
this.length = length;
Point2D.Float point = getHeadLocation();

for (Vector2D childVector : childVectors) {
childVector.setLocation(point.x, point.y);
}
}

/**
* Gets the angle of this vector.
*
* @return the angle of this vector.
*/
public float getAngle() {
return angle;
}

/**
* Sets the angle of this vector. If this vector has child vectors, it
* updates their location and angles as well.
*
* @param angle the angle in radians.
*/
public void setAngle(float angle) {
float angleDelta = angle - this.angle;
this.angle = angle;

for (Vector2D childVector : childVectors) {
childVector.setAngle(childVector.getAngle() + angleDelta);
}
}

/**
* Returns the location of the head of this vector.
*
* @return the location of the head of this vector.
*/
public Point2D.Float getHeadLocation() {
Point2D.Float point = new Point2D.Float();
float x = locationX + length * (float) Math.cos(angle);
float y = locationY + length * (float) Math.sin(angle);
point.x = x;
point.y = y;
return point;
}
}

VectorCanvas.java:

package net.coderodde.robohand;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

/**
* This class implements the canvas that draws vectors.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Apr 17, 2016)
*/
public class VectorCanvas extends Canvas {

/**
* Minimum number of vectors allowed.
*/
private static final int MINIMUM_VECTORS = 1;

/**
* When resizing a vector, its length may be increased or decreased by
* {@code LENGTH_DELTA} pixels per request.
*/
private static final float LENGTH_DELTA = 5.0f;

/**
* When changing the angle of a vector, the angle increases or decreases by
* {@code ANGLE_DELTA} radians per request. This is roughly 5 degrees.
*/
private static final float ANGLE_DELTA = ((float) Math.PI) / 36;

/**
* The color used to draw non-selected vectors.
*/
private static final Color VECTOR_COLOR = Color.RED;

/**
* The color of the current vector.
*/
private static final Color CURRENT_VECTOR_COLOR = Color.WHITE;

/**
* The list of vectors in this canvas.
*/
private final List<Vector2D> vectorList = new ArrayList<>();

/**
* The index of the selected vector.
*/
private int selectedVectorIndex;

/**
* Constructs this canvas with given vectors.
*
* @param vectorList the vectors to operate on. May form a tree. A cycle
* will eventually lead to stack overflow.
*/
public VectorCanvas(List<Vector2D> vectorList) {
if (vectorList.size() < 1) {
throw new IllegalArgumentException(
"This canvas requires at least " + MINIMUM_VECTORS + " vector.");
}

}

/**
* Chooses a preceding vector in the list as selected. If the current
* selected vector is first in the list, does nothing.
*/
public void decSelectedVectorIndex() {
if (selectedVectorIndex > 0) {
selectedVectorIndex--;
}

repaint();
}

/**
* Chooses a succeeding vector in the list as selected. If the current
* selected vector is last in the list, does nothing.
*/
public void incSelectedVectorIndex() {
if (selectedVectorIndex < vectorList.size() - 1) {
selectedVectorIndex++;
}

repaint();
}

/**
* Makes selected vector a little bit longer.
*/
public void enlargeCurrentSelectedVector() {
setSelectedVectorLength(LENGTH_DELTA);
}

/**
* Makes selected vector a little bit shorter. The length of the vector will
* never become negative.
*/
public void shrinkCurrentSelectedVector() {
setSelectedVectorLength(-LENGTH_DELTA);
}

/**
* Rotates the selected vector at its tail. If the selected vector has
* child vectors, their angle and location is modified as well such that
* they preserve their geometry with respect to the selected vector.
*
* @param angleDelta the amount of rotation in radians.
*/
public void rotateSelectedVector(float angleDelta) {
float newAngle = vectorList.get(selectedVectorIndex)
.getAngle() + angleDelta;
vectorList.get(selectedVectorIndex).setAngle(newAngle);
repaint();
}

@Override
public void update(Graphics g) {
vectorList.get(0).setLocation(getWidth() / 2, getHeight() / 2);
// Fill everything with the background color.
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());

// Draw the vectors.
g.setColor(VECTOR_COLOR);
for (Vector2D vector : vectorList) {
draw(g, vector);
}

g.setColor(CURRENT_VECTOR_COLOR);
draw(g, vectorList.get(selectedVectorIndex));
}

@Override
public void paint(Graphics g) {
update(g);
}

private void draw(Graphics g, Vector2D vector) {
Point2D.Float tail = vector.getLocation();

g.drawLine((int) tail.x,
(int) tail.y,
}

private void setSelectedVectorLength(float delta) {
Vector2D vector = vectorList.get(selectedVectorIndex);
vector.setLength(Math.max(0.0f, vector.getLength() + delta));
repaint();
}

private final class MyCanvasKeyListener implements KeyListener {

@Override
public void keyTyped(KeyEvent e) {
switch (e.getKeyChar()) {
case 'a':
case 'A':
decSelectedVectorIndex();
break;

case 'd':
case 'D':
incSelectedVectorIndex();
break;

case 'w':
case 'W':
enlargeCurrentSelectedVector();
break;

case 's':
case 'S':
shrinkCurrentSelectedVector();
break;

case 'q':
case 'Q':
rotateSelectedVector(-ANGLE_DELTA);
break;

case 'e':
case 'E':
rotateSelectedVector(ANGLE_DELTA);
break;
}
}

@Override
public void keyPressed(KeyEvent e) {}

@Override
public void keyReleased(KeyEvent e) {}
}
}

RobohandApp.java:

package net.coderodde.robohand;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
* This class implements the GUI for dealing with a robohand.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Apr 17, 2016)
*/
public class RobohandApp extends JFrame {

/**
* The width of this frame in pixels.
*/
private static final int FRAME_WIDTH  = 800;

/**
* The height of this frame in pixels.
*/
private static final int FRAME_HEIGHT = 600;

private final JPanel panel = new JPanel();
private Canvas myCanvas;

public RobohandApp() {
super("Robohand 1.6");

constructCanvas();

this.getContentPane().setLayout(new BorderLayout());

panel.setLayout(new BorderLayout());
myCanvas.setBackground(Color.BLACK);

setPreferredSize(new Dimension(FRAME_WIDTH, FRAME_HEIGHT));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setResizable(false);
centerOut();
setVisible(true);
}

private void constructCanvas() {
List<Vector2D> vectorList = new ArrayList<>();
float length = 30.0f;
float angle  = 0.0f;

Vector2D[] vectors = new Vector2D[] {
new Vector2D(length, angle),

new Vector2D(length, angle),
new Vector2D(length, angle),

new Vector2D(length, angle),
new Vector2D(length, angle),
new Vector2D(length, angle),
new Vector2D(length, angle),

new Vector2D(length, angle),
};

myCanvas = new VectorCanvas(vectorList);
}

private void centerOut() {
final int currentWidth  = getWidth();
final int currentHeight = getHeight();
final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
setLocation((screen.width - currentWidth) / 2,
(screen.height - currentHeight) / 2);
}

public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> {
RobohandApp app = new RobohandApp();
});
}
}

It looks like this: In order to manipulate it, click for focus the frame, and use keys wasdqe. This time, it would be nice to hear about my API design.

float x = locationX + length * (float) Math.cos(angle);
float y = locationY + length * (float) Math.sin(angle);
point.x = x;
point.y = y;

These local stores don't add any relevant information and should just be written directly to point's variables.

/**
* Chooses a preceding vector in the list as selected. If the current
* selected vector is first in the list, does nothing.
*/
public void decSelectedVectorIndex() {
if (selectedVectorIndex > 0) {
selectedVectorIndex--;
}

repaint();
}

If the current selected vector is first in the list, does nothing.

So what does repaint() do? Probably not nothing.

/**
* Makes selected vector a little bit longer.
*/
public void enlargeCurrentSelectedVector() {
setSelectedVectorLength(LENGTH_DELTA);
}

longer

"set" implies "becomes equal to". But here it adds, instead.

public void rotateSelectedVector(float angleDelta) {
float newAngle = vectorList.get(selectedVectorIndex)
.getAngle() + angleDelta;
vectorList.get(selectedVectorIndex).setAngle(newAngle);
repaint();
}

You have to break it up into multiple lines because you don't want to pull the vector out of the list. This makes for a lengthy line...

public void rotateSelectedVector(float angleDelta) {
Vector2D selectedVector = vectorList.get(selectedVectorIndex);
float newAngle = selectedVector.getAngle() + angleDelta;
selectedVector.setAngle(newAngle);

repaint();
}

But even when edited it looks weird. Your getters/setters are hindering you here, ideally you'd just do something like this:

selectedVector.angle += angleDelta;

Because we can't do this without making access public, the second alternative is making a method that will allow you to add a value.

/**
* Gets the angle of this vector.
*
* @return the angle of this vector.
*/
public float getAngle() {
return angle;
}

/**
* Sets the angle of this vector. If this vector has child vectors, it
* updates their location and angles as well.
*
* @param angle the angle in radians.
*/
public void setAngle(float angle) {
float angleDelta = angle - this.angle;
this.angle = angle;

for (Vector2D childVector : childVectors) {
childVector.setAngle(childVector.getAngle() + angleDelta);
}
}

But it looks like your setter is actually quite complex, so we definitely cannot get rid of the setter here. I think a helper which takes angleDelta would be useful.

...

Heck, looking at it again, I think making the set a helper and the delta the real one is the better choice.

public void addAngle(float angleDelta) {
this.angle += angleDelta;

for (Vector2D childVector : childVectors) {