# Copyright 2008 by Peter Moxhay and Wade Brainerd. # This file is part of Math. # # Math is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Math is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Math. If not, see . from objectarea import Object from vector import Vector from movableobject import MovableObject import gtk, math class ShapeObject(MovableObject): """Movable convex shape object.""" def __init__(self, color, symbol, points, pos, angle): MovableObject.__init__(self) self.color = color self.symbol = symbol self.points = points self.pos = pos self.angle = angle self.area = 0 self.centroid = Vector(0, 0) self.bounds_min = Vector(0, 0) self.bounds_max = Vector(0, 0) self.selectable = True # Calculate the area and centroid of the shape self.calculate_area_and_centroid() # Transform the points of the polygon to center-of-mass coordinates self.points = [p - self.centroid for p in self.points] # Get the current bounding rectangle. self.calculate_bounds() self.symbol_visible = True def calculate_area_and_centroid(self): # Calculate the area. self.area = 0 for i in range (0, len(self.points) ): p1 = self.points[i] p2 = self.points[(i+1) % len(self.points)] self.area += (p1.x*p2.y - p2.x*p1.y)/2 # Need to take absolute value? #self.area = abs(self.area) # Calculate the centroid (center of mass). self.centroid = Vector(1, 1) for i in range (0, len(self.points) ): p1 = self.points[i] p2 = self.points[(i+1) % len(self.points)] self.centroid += (p1+p2) * (p1.x*p2.y - p2.x*p1.y) / (6 * self.area) # Calculate the "move only" radius (the radius of a circle whose area is half the area of the shape). # (Modify for a long, thin object?) self.move_only_radius = math.sqrt(self.area/(2 * math.pi)) def calculate_bounds(self): # Get the current width and height of the bounding rectangle. self.bounds_min = Vector(float('inf'), float('inf')) self.bounds_max = Vector(float('-inf'), float('-inf')) for p in self.points: p = self.transform_point(p) self.bounds_min = self.bounds_min.min(p) self.bounds_max = self.bounds_max.max(p) # Bump the bounds a little bit to account for the outline edge. self.bounds_min -= Vector(2, 2) self.bounds_max += Vector(2, 2) def get_bounds(self): #print "ShapeObject get_bounds: ", self.bounds_min, self.bounds_max return self.bounds_min, self.bounds_max def transform_point(self, p): return p.rotate(self.angle) + self.pos def inside_move_area(self, point): self.point = point boolean = False # If the point is near the center of the object, return True. if ((self.point - self.pos).length() < self.move_only_radius): boolean = True return boolean # Enables user to "see both areas" in answer box by clicking. def select_by_button_press(self): other = None self.container.select_object(self) if self.in_answer_box and (self.container.problem_type == 'area' or self.container.problem_type == 'cutting'): i = 0 for o in self.container.objects: if isinstance(o, ShapeObject): if not o == self: other = o i += 1 if not self.selected: self.container.select_object(self) else: self.container.select_object(other) # Switch self and other to make selection work correctly in answer box? #if not self.selected: # self.container.select_object(other) #else: # self.container.select_object(self) self.container.adjust_tab_order() else: self.container.select_object(self) def draw(self, cr): cr.scale(self.scale, self.scale) # Transform the points. points = [self.transform_point(p) for p in self.points] # Generate the shape. cr.move_to(points[0].x, points[0].y) for p in points: cr.line_to(p.x, p.y) cr.line_to(points[0].x, points[0].y) cr.close_path() # Draw the fill. if self.selected: cr.set_source_rgb(self.color[0]*1.6, self.color[1]*1.6, self.color[2]*1.6) else: cr.set_source_rgb(self.color[0], self.color[1], self.color[2]) cr.fill_preserve() # Draw the outline. if self.selected: cr.set_dash((10, 10), 0) cr.set_source_rgb(self.color[0]*0.75, self.color[1]*0.75, self.color[2]*0.75) cr.set_line_width(4.0) cr.stroke() # Draw the symbol (capital letter representing the shapes's area). if self.symbol_visible: cr.set_source_rgb(0, 0, 0) cr.set_font_size(50) x_bearing, y_bearing, width, height = cr.text_extents(self.symbol)[:4] cr.move_to(self.pos.x - x_bearing - width/2, self.pos.y - y_bearing - height/2) cr.show_text(self.symbol) # Algorithm to test whether point is inside the polygon def contains_point(self, pos): n = 0 p = pos for i in range (0, len(self.points) ): p1 = self.points[i] p2 = self.points[(i+1) % len(self.points)] p1 = self.transform_point(p1) p2 = self.transform_point(p2) if p.y > min(p1.y, p2.y): if p.y <= max(p1.y, p2.y): if p.x <= max(p1.x, p2.x): if p1.y != p2.y: x = (p.y-p1.y)*(p2.x-p1.x)/(p2.y-p1.y)+p1.x if p1.x == p2.x or p.x <= x: n = n + 1 if n % 2 == 0: return(False) else: return(True) def is_in_container(self): for p in self.points: p = self.transform_point(p) if p.x < -2 or p.x > self.container.DRAGGING_RECT_WIDTH + 2 or \ p.y < -1 or p.y > self.container.DRAGGING_RECT_HEIGHT: return False return True