Lab Exercise 10: User Input and 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.
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 user-selected point on the screen. We'll work with a scale factor of 10 (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 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 ofthe 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. But we want to be able to rotate the line about any point of our choice.
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 given, otherwise x0, y0 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. vis A list to hold the graphics Line object. 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.
To rotate the vertices around the origin, we will need to get the current angle of the RotatingLine as well as the cosine and sine of that angle. Make this the first three lines of the render method. Write the code that corresponds to the following three steps.
- Convert the value in self.angle to radians, e.g. Arad = PI * Adeg / 180. Note the math package has the symbol pi, which you can access as math.pi.
- Assign to a local variable cth, the cosine of the angle (in radians). The math package has the function cos.
- Assign to a local variable sth, the sine of angle (in radians).
Assign to a local variable, e.g. pts, an empty list. This will hold the Point objects we create in the for loop.
Start a for loop over the elements in self.points. A good loop variable name would be vertex. Inside the loop, execute the following steps.
- Assign to xt the value (Vx + Px) - Ax, where Vx is the vertex X-coordinate, Px is the line's position X coordinate, and Ax is the anchor Ax-coordinate. Do the same thing for the Y coordinate, assigning it to yt.
- Assign to xtt the value cos(Theta) * xt - sin(Theta) * yt. Then assign to ytt the value sin(Theta) * xt + cos(Theta) * yt. Note that you already computed cos(Theta) and sin(Theta) and stored those values in a local variable. Use the local variable in your equations.
- Assign to xf the value xtt + Ax, and assign to yf the value ytt + Ay. As above, Ax and Ay are your anchor values.
- Append to the pts list a new graphics Point object with the coordinates S * xf, and H - S * yf, where S is the scale factor, H is the window height, and xf and yf you calculated in the prior step.
Once the loop is done, assign to self.vis a list with one graphics Line object. Use the first a second elements of the pts list to create the Line object (which takes two points).
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.
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.
def test1(): win = gr.GraphWin('line thingy', 500, 500, False) line = RotatingLine(win, 25, 25, 10) line.draw() win.getMouse() win.close()
Now that you have a working draw method, create a setAngle method that
takes in self and a second parameter to hold the new orientation for
the line. The function should first assign the parameter to the angle
field of the object. Then, if the drawn field is True, call the draw
method (e.g. self.draw()). Once you have done this, add the following
two lines before line.draw() in the test function.
Run your program again. Does the line appear at a 45 degree angle? Try it with some different angles.
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 have a loop that runs 360 iterations. Inside the loop, call the rotate method with an argument of 1. Then have the computer sleep for a short period of time (e.g. 0.05s). Then call win.update() to refresh the graphics. If you want, add a test for checkMouse and break if someone clicks in the window. Once you have finished that, run the program. The line should rotate around its center point.
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 self.setAnchor( 20, 25 ) before the setAngle call. 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.