Lab Exercise 10: User Input and Geometric Thinking
This project continues our work with classes within the domain of physical simulations.
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.
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 contains four methods that let you access user input. We've used getMouse and checkMouse, which wait for a mouse click (getMouse) or check if one has occurred recently (checkMouse).
The GraphWin object also contains methods for accessing keystrokes. The getKey method waits for the user to press a key, while the checkKey returns either the empty string '' or the character most recently used.
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 ofcheckMouse if it is not None and the return value of checkKey if it is not the empty string. They 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 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.
Create a new file, rot.py, in your project 10 directory. Put your name, date, etc. at the top 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 point specified in physics coordinates. (We will continue our practice of doing most calculations in physics coordinates, and then translating to screen coordinates just when we need to create or move graphics objects.)
Start a new class 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 will be located at 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 Ax and Ay values will be our rotation anchor point. By default, this will be the x0, y0 values at the center of the line (which means the rotating line will spin around its center). But we want to be able to rotate the line about any point of our choice by giving Ax and Ay values (in physics coordinates).
Create and assign the following fields.
pos The x0 and y0 values as a 2-element list. length The length of the line. anchor the Ax and Ay values as a 2-element list, if both are given (i.e. not None), otherwise x0, y0 (this involves an if statement) points A list that holds two 2-element lists (details below). angle The current orientation of the line. Initialize it to 0.0. rvel Rotational velocity (in degrees/s). Initialize it to 0.0. win A GraphWin object reference. scale The scale factor from model coordinates to screen coordinates (with a default value of 10). vis A list to hold the graphics Line object. Set it to the empty list, for now. drawn A Boolean variable to indicate if the Line has been drawn. Set it to False.
For the points field, make a list that has two 2-element sub-lists. 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 (-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 line. Remember, we want the line to appear like it's rotating around the anchor point.
For each of the 2D vertices in the self.points list, we need to execute the following actions.
- Subtract the anchor point from the line's endpoints (vertices), which puts the anchor point at the origin (0, 0).
- Rotate the vertices around the origin.
- Add the anchor point back to the vertices.
- Create a new Zelle Point object, taking into account scale and Y-axis orientation.
Once the loop is done, you will have a list 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.
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
- The next step is to write a draw method for the RotatingLine class. The draw method has three steps. First, execute a for loop over self.vis and have each element undraw itself. 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.
Create two accessor 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().
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 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. Then put the call Then put the call
line.setAnchor(20,25)just before the while loop in the test1 function. 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.
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) line.draw() while win.checkMouse() == None: line.setAngle( line.getAngle() + 3) time.sleep(0.08) win.update() win.getMouse() win.close() if __name__ == "__main__": test1()
When you are done with the lab exercises, you may begin the project.