CS 152: Lab 10

Title image Project 10
Spring 2020

Lab Exercise 10: Geometric Thinking

This project continues our work with classes within the domain of physical simulations.

This is the thid part of a multi-part project where we will look at increasingly complex physical simulations. This week we add blocks that rotate.

Video: Rotations


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 graphicsPlus.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. Review the differences between the GraphWin methods checkMouse and getMouse, and between checkKey and getKey.

Quick Block Class Check

Make sure first four parameters (after self) of the __init__ method of your Block class correspond to win (the GraphWin), width, height, and position (a 2-element list). This is necessary because the rotating block collision code needs to create a Block and relies on the Block __init__ having those parameters in that order.

In addition, add a method to your Thing class called getWin(self) that returns the GraphWin window object (e.g. self.win).

Geometric Thinking and Rotation

No physics simulation is complete without the ability to rotate an object. In the real world 2D objects have three parameters (x, y, theta), where theta is the orientation of the object around the Z-axis (which points out of the screen). We're going to explore the geometric thinking required to make an object appear to rotate. We'll start with Line objects, but the same procedures apply to Polygon objects.

  1. Setup

    Create a new file, rot.py, in your project 10 directory. Put your name, date, etc. at the top and then import your physics_objects, the graphicsPlus, 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.

  2. Create a RotatingLine class

    Start a new class called RotatingLine. Thing class (though we could). Start the __init__ method with the following definition.

    def __init__(self, win, length, pos=[0,0], anchor = None):

    The pos argument will specify the location of the center of the line. The length will be the total length of the line (1/2 to each side of the center point). The anchor values, if given, will be the rotation anchor point. By default, this will be the position at the center of the line. But we want to be able to rotate the line about any point of our choice.

    Call the pho.Thing.__init__ method with the window, type, position, and color as the arguments. Use something like "rotating line" for the type argument. Since the color is not the third parameter of the Thing init method, you will need to make it a keyword argument like color=color.

    After calling the Thing init method, create and assign the following shape specific fields in the RotatingLine __init__.

    widthThe length of the line.
    heightThe height of the line. Set this to 1.
    anchorIf the anchor argument is not None, then assign a copy of the anchor list to the field. Otherwise assign a copy of the position list.
    pointsA list that holds two 2-element lists of points. Initialize it to the empty list.
    angleThe current orientation of the line. Initialize it to 0.0.
    rvelRotational velocity (in degrees/s). Initialize it to 0.0.

    Call the render method as the last thing in the init method.

  3. Write a render method

    Create a render method in the RotatingLine class. It should have self as the only argument. This method needs to make the appropriate Zelle Line object given the current center point, angle, and anchor point for the line. Remember, we want the line to appear like it's rotating around the anchor point.

    As with all the prior render methods, store the current state of the drawn field into a local variable. If the shape has been drawn, then call the object's undraw method. At the end of the method, if the shape was drawn, call the object's draw method.

    In between create the Zelle line in the appropriate orientation.

    First, build the values for the self.points field, which is a list of two 2-element sub-lists that represent the vertices of the shape. These sub-lists will hold the coordinates of the line as though it were centered at (0, 0) and stretched along the X-axis. Given those conditions, the endpoints of the line have to be (-self.width/2.0, 0.0) and (self.width/2.0, 0.0). Note the use of floating point values.

    Second, create a variable pts to hold the transformed Line endpoints. Initialize it to the empty list. For each of the 2D vertices in the self.points list, execute the following actions.

    1. Add the vertex to the object's position and then subtract the anchor point, which puts the anchor point at the origin (0, 0).
    2. Rotate the vertices around the origin.
    3. Add the anchor point back to the vertices.
    4. Create a new Zelle Point object, taking into account the Y-axis orientation, and append it to the pts list.

    Once the loop is done, you will have a list (pts) with two Point objects from which you can create a Line object.

    Note that before starting the loop, you need to convert the value in self.angle from degrees to radians, as the math package sin and cos functions operate in radians. To convert a value A from degees to radians use the expression A*math.pi/180.0. Don't forget to import the math package.

    + (Outline)

      def render(self):
        # assign to a local variable (e.g. drawn) the value in self.drawn
        # if drawn
            # call the undraw method 
        # assign to self.points a list with two 2-element sublists that 
        #      define the end points of the line relative to the position
        #      The endpoints will be (-self.width/2, 0) and (self.width/2, 0)
        # 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 Point object with coordinates (x, self.win.getHeight() - y)
        # assign to self.shapes a list containing a Zelle graphics Line object using the Point objects in pts
        # if drawn
            # call the draw method 
  4. Write getAngle and setAngle methods

    Create two accessor methods: setAngle and getAngle. The getAngle method should return the current value of self.angle.

    The setAngle method should update the value of self.angle, but it also needs to recreate the line. After updating the value of self.angle, it should call the render method.

  5. Write getWidth, getHeight, setWidth, setHeight methods

    The get methods should return their respective values. The set methods should update their respective values and then call the render method.

  6. Test the RotatingLine

    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, 100, [250, 250])
        while win.checkMouse() == None:
            line.setAngle( line.getAngle() + 3)
    if __name__ == "__main__":
  7. Write a rotate method to orient the line

    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 method is almost identical to the setAngle method, except that you want to increment the angle by the argument, not replace it. Then call the render method.

    Update your test function to use line.rotate() instead of line.setAngle() to modify the orientation of the line and try it 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. It should call self.render after updating the anchor point field. (Or make use of the anchor parameter to the constructor.) Then put the call self.setAnchor( 200, 250 ) before the loop starts. 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 begin the project.