views:

272

answers:

3

Hi,

I develop a Python-based drawing program, Whyteboard (https://launchpad.net/whyteboard)

I'm developing features to allow the user to rotate and scale a polygon that they draw. Here's my problem:

I have a Polygon class containing a list of all points, which is "closed off" at the end. Users can select drawn shapes in my program, which "highlights" them, drawing selection handles at each point. These points can be "grabbed" to change its position, and altering the polygon's shape.

I have a problem: I need to figure out how to calculate a the resizing "scale" to apply to the polygon. For example, (with the mouse held down), the user moving their mouse away from the shape should be a "grow" action, and bringing the mouse towards the shape should shrink it.

I have code in place to perform the scale (which I believe is correct) but I just can't create a "good" scaling factor. The code below is what I've come up with, based on the answers

/edit -- here is the solved code.

def rescale(self, x, y):
    """ 
    x and y are the current mouse positions. the center and "original" mouse 
    coords are calculated below
    """
    if not self.center:
        a = sum([x for x, y in self.points]) / len(self.points)
        b = sum([y for x, y in self.points]) / len(self.points)
        self.center = (a, b)
    if not self.orig_click:  # where the user first clicked on
        self.orig_click = (x, y)
    if not self.original_points:  # the points before applying any scaling
        self.original_points = list(self.points)


    orig_click = self.orig_click
    original_distance = math.sqrt((orig_click[0] - self.center[0]) ** 2 + (orig_click[1] - self.center[1]) ** 2)

    current_distance = (math.sqrt((x - self.center[0]) ** 2 + 
                       (y - self.center[1]) ** 2))
    self.scale_factor = current_distance / original_distance        

    for count, point in enumerate(self.original_points): 
        dist = (point[0] - self.center[0], point[1] - self.center[1]) 
        self.points[count] = (self.scale_factor * dist[0] + self.center[0], self.scale_factor * dist[1] + self.center[1]) 

Currently this code seems to scale my polygon down to nothing quickly, and no amount of mouse movement will grow it back. Sometimes it will do the opposite, and grow quickly; but not shrink back.

+1  A: 

The most intuitive scale factor would be the ratio of (distance from current mouse position to the polygon centre) to (distance from mouse position at start of drag to the polygon centre) - so that clicking on a point in the polygon and dragging it twice as far from the centre as it was doubles the size of the polygon.

moonshadow
+5  A: 

First, let's correct your scaling code:

for count, point in enumerate(self.points): 
    dist = (point[0] - self.center[0], point[1] - self.center[1]) 
    self.points[count] = (self.scale_factor * dist[0] + self.center[0], self.scale_factor * dist[1] + self.center[1]) 

I hope your points are kept in floating point, because integer truncation errors are going to accumulate very quickly. It might be better to have two copies of the points, one scaled and one unscaled.

To determine the scale factor, take the ratio of the distance from the original click to the center, and the current mouse position to the center.

original_distance = sqrt((click[0] - self.center[0])**2 + (click[1] - self.center[1])**2)
current_distance = sqrt((current_position[0] - self.center[0])**2 + (current_position[1] - self.center[1])**2)
self.scale_factor = current_distance / original_distance

Edit: Your latest problem emphasizes the importance of having two sets of points, the original and the scaled. Since the scale factor is relative to the original size of the shape, you need to start with the original points of the shape each time you scale. You can consolidate that back down to one set when the user is done playing with the mouse.

And to your comment, no you don't have to recalculate the center. The center should not be moving.

Edit 2: When you scale, you're scaling from one size to another size. If you're rescaling constantly, you have two choices: keep one copy of the shape at its original size, or make your scale factor relative to the last size of the shape, rather than the original size. I prefer the two copy approach, because otherwise it's too easy for errors to accumulate, even if you're using floating point; it's also easier to get the logic right.

Mark Ransom
Good thinking on the floats, I didn't think of that - I've converted over. Do I keep recalculating the center in my code? I've edited my original post to add in the code I've come up with, but it's not working too well - the polygon seems to resize too quickly.
Steven Sproat
Hi, sorry to keep "going on" about this. I'm not sure I follow - the current points are only used to calculate the center, once? I'm not sure where having two sets fits in, if you can help there. Thanks!
Steven Sproat
Yes! Thanks a ton, Mark! I've finally got it all working. Your help was most invaluable. Now, to show you what you've helped me with:http://imagebin.org/78777 - unscaledhttp://imagebin.org/78778 - made smallerhttp://imagebin.org/78779 - biggerCheers!
Steven Sproat
+1  A: 

I'm not versed in Python, so I'll try to answer in pseudocode.

First of all, you will want to calculate the center of the polygon. This is done very easily (and makes sense when you think about it): simply add all points together and divide them by the amount of points.

center = (point1 + point2 + point3) / 3

You want to scale it based on the mouse, correct? That's always going to be fiddly, but it should be something like this:

scale = lengthof(mouse - center) / MAGIC_NUMBER

Then you calculate the relative points to the center. You're effectively setting the origin of a graph at the centre point.

relative_point1 = point1 - center
relative_point2 = point2 - center
relative_point3 = point3 - center

Then you scale the relative points by the scale:

relative_point1 *= scale
relative_point2 *= scale
relative_point3 *= scale

And place them back at the correct position:

point1 = center + relative_point1
point2 = center + relative_point2
point3 = center + relative_point3

To avoid rounding errors, you'll probably want to keep the original points until the user is done scaling.

knight666
What is magic number here? I mean, a value .00 - 1.00, 1 - 100, or what?
Steven Sproat
It's a magic number! It depends on what looks best. Try 10 first.
knight666
Actually, no magic numbers are required. When the mouse is clicked, measure lengthof(mouse-center) and store it as *original*. Then the new scale is simply lengthof(mouse-center)/original. Take care if original is zero!
Tarydon
Thanks. I think this is what I'm doing in my code (now edited in the first post), but it's still a little "wonky".
Steven Sproat