Building on last week's project, this is the third part of a multi-part project where we will look at increasingly complex physical simulations. This week, we add blocks that rotate.


Setting Up

If you have not already done so, mount your personal space, create a new project10 folder and bring up TextWrangler and a Terminal. As with last week, you will need the Zelle graphics package file graphics.py. You will also want to copy over your physics_objects.py from project 9. You will need a working Ball class for this lab exercise.

The Zelle documentation is also available.

More User Input--Creating a more interactive simulation

The Zelle graphics GraphWin object has four methods that let you access user input. We've already used getMouse(), which waits for a mouse click, and checkMouse(), which checks if one has occurred recently. There are other, similar methods for keyboard input: the getKey() method waits for the user to press a key; and the checkKey() method returns either the key most recently pressed or the empty string (the string consisting of no symbols--in your programs, represent the empty string as quotes with no symbols between them, "").

Make a new file, input.py, that imports graphics. Write a main function that creates a window, then enters a while loop. Inside the while loop, have your program print out the return value of checkMouse() if it is not None as well as the return value of checkKey() if it is not the empty string. Then run the program, and click and type and see what comes up. Think about how you could use this capability in the current project.

Geometric Thinking and Rotation

No physics simulation is complete without the ability to rotate an object. In a 2D simulation, 2D objects are generally specified by three parameters (x, y, theta), where x, y are the object's coordinates (e.g., the location of an anchor point), and theta is an angle, the orientation of the object around the Z-axis (which points out of the XY plane). We're going to explore the geometric thinking required to make a simulated object rotate around a point on a flat surface. We'll start with Line objects, but the same procedures apply to Polygon objects.

Create a new file rotation.py in your project 10 directory. Put your name, date, etc. in a comment at the top (as always!), and then import the graphics, math, and time packages.

The goal of this exercise is to create a line object and then have it rotate 360 degrees around a user-selected point on the screen. We'll work with a scale factor of 10 (so, screen coordinates are 10x the size of model coordinates), but we will pretend that the Y-axis goes up, even though in screen coordinates it goes down.

Start a new class in your file called RotatingLine. For the lab exercise, we will not have it inherit from the Thing class. Start the __init__() method with the following definition.

def __init__(self, win, x0, y0, length, Ax = None, Ay = None):

The values x0 and y0 represent the coordinates at the center of the line. The length will be the total length of the line (half of its length to each side of center point x0, y0). The Ax and Ay values will be the anchor point around which the line will rotate. By default, this anchor point will be defined by the x0, y0 values at the center of the line, but eventually, we want to be able to rotate the line about any point of our choice.

Create and assign the following fields.

posTwo-element list, with elements x0 and y0.
lengthLength of the line.
anchorTwo-element list representing the anchor point for the rotation. Its elements are Ax and Ay, if both are given, otherwise its elements are x0 and y0 (this involves an if statement)
pointsList of two 2-element lists (see details in text below).
angleAngular orientation of the line. Initialize it to 0.0.
rvelRotational velocity (in degrees/s). Initialize it to 0.0.
winReference to a GraphWin object.
scaleScale factor from model coordinates to screen coordinates (our typical value is given as 10, above).
visList of graphics objects to be visualized. It will eventually hold the graphics Line object, but initialize it here to the empty list.
drawnBoolean value, indicating if the Line has been drawn. Initialize it to False.

For the points field, make a list that has two 2-element sub-lists. As a useful representation for the rotation of the line, the sub-lists will hold the coordinates of the line's endpoints as though it were centered at (0, 0) and stretched along the X-axis. That means the endpoints here are [-length/2.0, 0.0] and [length/2.0, 0.0]. Note the use of floating point values.

Next, create a method in the RotatingLine class called render(). It should have self as the only argument. This function needs to make the appropriate Zelle Line object given the current center point, angle, and anchor point for the rotation--remember, we want the line to appear to be rotating around the anchor point.

The first step is to create the two Point objects we need for the Line. For each of the 2D vertices in the self.points list (Note: A vertex--the plural is vertices--is a key point on a geometric figure), we need to execute the following actions, which are described in text here and further clarified in the code template below:

  1. Subtract the anchor point from the line's endpoints, which effectively puts the anchor point at the origin (0, 0).
  2. Rotate the endpoints around the origin. (See below about the use of the sin and cos functions.)
  3. Add the anchor point back to the rotated endpoints, effectively translating the rotated motion to be around the anchor point's location.
  4. Create a new Zelle Point object, taking into account scale and Y-axis orientation.

Once the loop is done, there will be a list with two Point objects from which you can create a Line object.

For the rotation mentioned in step 2, above, you'll be using trigonometric functions from the math package. Therefore, before starting the loop, you need to convert the value in self.angle from degrees to radians--the math package's sin() and cos() functions operate in radians. To convert a value A from degrees to radians, use the expression A*math.pi/180.0. (Don't forget to import the math package!)

def render(self):

    # assign to theta the result of converting self.angle from degrees to radians
    # assign to cth the cosine of theta
    # assign to sth the sine of theta
    # assign to pts the empty list

    # for each vertex in self.points
        # (2 lines of code): assign to x and y the result of adding the vertex to self.pos and subtracting self.anchor

        # assign to xt the calculation x * cos(Theta) - y * sin(Theta) using your precomputed cos/sin values above
        # assign to yt the calculation x * sin(Theta) + y * cos(Theta)

        # (2 lines of code): assign to x and y the result of adding xt and yt to self.anchor

        # append to pts a Zelle graphics Point object with coordinates (self.scale * x, self.win.getHeight() - self.scale*y)

    # assign to self.vis a list with a Zelle graphics Line object using the two Point objects in pts

draw( )

The next step is to write a draw() method for the RotatingLine class. The draw() method has four steps. First, execute a for loop over self.vis and have each element undraw itself by calling its undraw() method (see the Graphics documentation for details, if you'd like). Then, call the render() method. Then, execute a for loop over self.vis and have each element draw itself into the stored window (self.win). Finally, set the field drawn to True.

Getters & Setters

Create two functions, setAngle() and getAngle(). The getAngle() function should return the current value of self.angle.

The setAngle() function should update the value of self.angle, but it also needs to redraw the line if it has already been drawn. After updating the value of self.angle, then if the value of self.drawn is True, it should call self.draw().


Once you have completed these steps, it's time to test. Use the following code (which is a function you should add to the file, not a method to add to the class) and run it. You should get a short line in the middle of the window that rotates.

def test1():
    win = gr.GraphWin('line thingy', 500, 500, False)

    line = RotatingLine(win, 25, 25, 10)

    while win.checkMouse() == None:
        line.setAngle( line.getAngle() + 3)


if __name__ == "__main__":

rotate( )

Create a rotate() method in the RotatingLine class. This should take in self and one parameter, which is the amount to rotate relative to the current orientation. This function is almost identical to the setAngle() function, except that you want to increment the angle by the argument, not replace it. Then, if the line is drawn (i.e., if self.drawn is True), call the draw() method.

Update your test function to use line.rotate() instead of line.setAngle() to modify the orientation of the line, and run the program again. The argument to line.rotate() should be just the incremental amount to change the angle (e.g., 3).

If you want to have the line rotate around a different point, create a setAnchor() method that lets you specify the anchor point. Then put the call line.setAnchor([20, 25]) before the loop. Then, when you run your test function, the line should appear to rotate around its left endpoint. Explore other anchor points and make sure this makes sense.

When you are done with the lab exercises, you may start on the rest of the project.

© 2019 Eric Aaron (with contributions from Colby CS colleagues).