4
\$\begingroup\$

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.");
        childVectors.add(vector);
        Point2D.Float headLocation = getHeadLocation();
        vector.setLocation(headLocation.x, headLocation.y);
    }

    /**
     * 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;
        Point2D.Float headLocation = getHeadLocation();

        for (Vector2D childVector : childVectors) {
            childVector.setLocation(headLocation.x, headLocation.y);
            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.");
        }

        this.vectorList.addAll(vectorList);
        addKeyListener(new MyCanvasKeyListener());
    }

    /**
     * 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();
        Point2D.Float head = vector.getHeadLocation();

        g.drawLine((int) tail.x, 
                   (int) tail.y, 
                   (int) head.x, 
                   (int) head.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());
        this.getContentPane().add(panel, BorderLayout.CENTER);

        panel.setLayout(new BorderLayout());
        panel.add(myCanvas, BorderLayout.CENTER);
        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),
        };

        vectors[0].addChildVector(vectors[1]);
        vectors[0].addChildVector(vectors[2]);

        vectors[1].addChildVector(vectors[3]);
        vectors[1].addChildVector(vectors[4]);
        vectors[2].addChildVector(vectors[5]);
        vectors[2].addChildVector(vectors[6]);

        vectors[6].addChildVector(vectors[7]);

        vectorList.addAll(Arrays.asList(vectors));
        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:

Robot arm is moving

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.

\$\endgroup\$
1
\$\begingroup\$
    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;
    Point2D.Float headLocation = getHeadLocation();

    for (Vector2D childVector : childVectors) {
        childVector.setLocation(headLocation.x, headLocation.y);
        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;
    Point2D.Float headLocation = getHeadLocation();

    for (Vector2D childVector : childVectors) {
        childVector.setLocation(headLocation.x, headLocation.y);
        childVector.addAngle(angleDelta);
    }
}

You save having to calculate the delta for every child.


public void update(Graphics g) {
    vectorList.get(0).setLocation(getWidth() / 2, getHeight() / 2);

What's that for? On a repaint, you update your model. Why does looking at data change the data? Could use a comment. Or a wrapping function. (I get the feeling it's for centering the hand.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.