# cs1graphics module # # Copyright David Letscher and Michael Goldwasser, 2007 # # Ad Hoc Version (Nov 2007) # This is a preliminary version that does not yet support events. # License: # # This program 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 2 of the License, or # (at your option) any later version. # # This program 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 this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ Library for drawing and manipulating basic shapes. """ _debug = 0 # no debugging #_debug = 3 # can be 1 or 2 or 3 import copy as _copy import math as _math import random as _random _ourRandom = _random.Random() _ourRandom.seed(1234) # initialize the random seed so that treatment of equal depths is reproducible _underlyingLibrary = None try: import Tkinter as _Tkinter _tkroot = _Tkinter.Tk() _tkroot.withdraw() _underlyingLibrary = 'Tkinter' class _RenderedCanvas(object): def __init__(self, canvas, w, h, background, title, refresh): self._parent = canvas self._tkWin = _Tkinter.Toplevel() self._tkWin.protocol("WM_DELETE_WINDOW", self.close) self._tkWin.title(title) self._canvas = _Tkinter.Canvas(self._tkWin,width=w,height=h,background=getTkColor(background)) self._canvas.pack(expand=False,side=_Tkinter.TOP) self._tkWin.resizable(0,0) self.refresh() def setBackgroundColor(self, color, doRefresh=True): self._canvas.config(background=getTkColor(color)) if doRefresh: self.refresh() def setWidth(self, w, doRefresh=True): self._canvas.config(width=w) if doRefresh: self.refresh() def setHeight(self, h, doRefresh=True): self._canvas.config(height=h) if doRefresh: self.refresh() def setTitle(self, title, doRefresh=True): self._tkWin.title(title) if doRefresh: self.refresh() def refresh(self): if _debug>=3: import inspect s = inspect.stack() s.reverse() for i in s[:-1]: if i[4]: # avoid interpreter command print 'line %4d, method %-10s, command: %s'%(i[2],i[3],i[4][0].strip()) if _debug>=1: print "TkCanvas being refreshed" self._canvas.update() # unsafe if invoked by event handler? # self._canvas.update_idletasks() def open(self): """ Open the canvas window (if not already open). """ self._tkWin.deiconify() def close(self): """ Close the canvas window (if not already closed). """ self._tkWin.withdraw() def remove(self, obj, doRefresh=True): self._parent._objectDepths.remove(obj) self._canvas.delete(obj._object) if doRefresh: self.refresh() def saveToFile(self, filename): parent = self._parent background = Rectangle(parent.getWidth()+2, parent.getHeight()+2) background.move( float(parent.getWidth())/2, float(parent.getHeight())/2 ) background.setFillColor(parent.getBackgroundColor()) background.setBorderColor(parent.getBackgroundColor()) maxDepth = 0 for o in parent._contents: if o.getDepth() > maxDepth: maxDepth = o.getDepth() background.setDepth(maxDepth+1) parent.add(background) parent.refresh() self._canvas.postscript(file=filename) parent.remove(background) parent.refresh() def _moveToDepth(self, obj, depth, doRefresh=True): self._parent._objectDepths.remove(obj) self._parent._objectDepths.add(obj, depth) above = self._parent._objectDepths.getPred(obj) if above: self._canvas.lower(obj._object, above._object) else: self._canvas.lift(obj._object) if _debug>=3: print "Moving", obj, "below", above print "Depths", self._parent._objectDepths.depth(obj), if above: print "below", self._parent._objectDepths.depth(above), print if doRefresh: self.refresh() def getTkColor(color): if color._transparent: return "" return "#%04X%04X%04X" % (256*color.getColorValue()[0], 256*color.getColorValue()[1], 256*color.getColorValue()[2]) class _RenderedDrawable(object): def __init__(self, drawable, canvas, doRefresh=True): self._drawable = drawable self._canvas = canvas self._object = None def update(self, transform, doRefresh=True): pass def remove(self, doRefresh=True): self._canvas._canvas.remove(self, doRefresh) class _RenderedShape(_RenderedDrawable): def __init__(self, drawable, canvas, doRefresh=True): _RenderedDrawable.__init__(self, drawable, canvas, doRefresh) def update(self, transform, doRefresh=True): _RenderedDrawable.update(self, transform, False) # cannot update border color because it is called 'outline' for fillables, yet 'fill' for path/line self._canvas._canvas._canvas.itemconfigure(self._object, width=self._drawable._borderWidth) if doRefresh: self._canvas._canvas.refresh() class _RenderedFillableShape(_RenderedShape): def __init__(self, drawable, canvas, doRefresh=True): _RenderedShape.__init__(self, drawable, canvas, doRefresh) def update(self, transform, doRefresh=True): _RenderedShape.update(self, transform, False) self._canvas._canvas._canvas.itemconfigure(self._object, fill=getTkColor(self._drawable._fillColor), outline=getTkColor(self._drawable._borderColor)) if doRefresh: self._canvas._canvas.refresh() class _RenderedImage(_RenderedDrawable): def __init__(self, drawable, canvas, transform, doRefresh=True): _RenderedDrawable.__init__(self, drawable, canvas) self._img = _Tkinter.PhotoImage(file=self._drawable._filename, master=self._canvas._canvas._canvas) self._object = self._canvas._canvas._canvas.create_image(transform.image(Point(0,0)).get(), image=self._img, anchor="nw") self._canvas._canvas._moveToDepth(self, transform._depth, False) if doRefresh: self._canvas._canvas.refresh() def update(self, transform, doRefresh=True): # Update size and position composed = transform * self._drawable._transform self._canvas._canvas._canvas.coords(self._object, composed.image(Point(0,0)).getX(), composed.image(Point(0,0)).getY()) self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() class _RenderedText(_RenderedDrawable): def __init__(self, drawable, canvas, transform, doRefresh=True): _RenderedDrawable.__init__(self, drawable, canvas) self._object = canvas._canvas._canvas.create_text(transform.image(Point(0,0)).get(), text=self._drawable._text, fill=getTkColor(self._drawable._color), font=('Helvetica', self._drawable._size, 'normal'), anchor="nw") self._canvas._canvas._moveToDepth(self, transform._depth, False) if doRefresh: self._canvas._canvas.refresh() def update(self, transform, doRefresh=True): # Update size and position composed = transform * self._drawable._transform self._canvas._canvas._canvas.coords(self._object, composed.image(Point(0,0)).getX(), composed.image(Point(0,0)).getY()) self._canvas._canvas._canvas.itemconfigure(self._object, text=self._drawable._text, fill=getTkColor(self._drawable._color), font=('Helvetica', self._drawable._size, 'normal')) self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() class _RenderedCircle(_RenderedFillableShape): def __init__(self, drawable, canvas, transform, doRefresh=True): _RenderedFillableShape.__init__(self, drawable, canvas, False) center = transform.image(Point(0.,0.)) radius = _math.sqrt(abs(transform.det())) self._object = canvas._canvas._canvas.create_oval(center.getX() - radius, center.getY() - radius, center.getX() + radius, center.getY() + radius, fill=getTkColor(self._drawable._fillColor), outline=getTkColor(self._drawable._borderColor), width=self._drawable._borderWidth) self._canvas._canvas._moveToDepth(self, transform._depth, False) if doRefresh: self._canvas._canvas.refresh() def update(self, transform, doRefresh=True): # Update interior and border _RenderedFillableShape.update(self, transform, False) # Update size and position composed = transform * self._drawable._transform center = composed.image(Point(0.,0.)) radius = _math.sqrt(abs(composed.det())) self._canvas._canvas._canvas.coords(self._object,center.getX() - radius, center.getY() - radius, center.getX() + radius, center.getY() + radius) self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() class _RenderedRectangle(_RenderedFillableShape): def __init__(self, drawable, canvas, transform, doRefresh=True): _RenderedFillableShape.__init__(self, drawable, canvas, False) self._object = canvas._canvas._canvas.create_polygon(transform.image(Point(-.5,-.5)).get(), transform.image(Point(-.5,.5)).get(), transform.image(Point(.5,.5)).get(), transform.image(Point(.5,-.5)).get(), fill=getTkColor(self._drawable._fillColor), outline=getTkColor(self._drawable._borderColor), width=self._drawable._borderWidth) self._canvas._canvas._moveToDepth(self, transform._depth, False) if doRefresh: self._canvas._canvas.refresh() def update(self, transform, doRefresh=True): # Update interior and border _RenderedFillableShape.update(self, transform, False) # Update size and position composed = transform * self._drawable._transform self._canvas._canvas._canvas.coords(self._object, composed.image(Point(-.5,-.5)).getX(), composed.image(Point(-.5,-.5)).getY(), composed.image(Point(-.5,.5)).getX(), composed.image(Point(-.5,.5)).getY(), composed.image(Point(.5,.5)).getX(), composed.image(Point(.5,.5)).getY(), composed.image(Point(.5,-.5)).getX(), composed.image(Point(.5,-.5)).getY()) self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() class _RenderedPath(_RenderedShape): def __init__(self, drawable, canvas, transform, doRefresh=True): _RenderedShape.__init__(self, drawable, canvas, False) if len(self._drawable._points)>1: # path must have two points in TK self._object = self._createObj(transform) self._canvas._canvas._moveToDepth(self, transform._depth, False) if doRefresh: self._canvas._canvas.refresh() else: self._object = None def update(self, transform, doRefresh=True): # Update border properties _RenderedShape.update(self, transform, False) # Update size and position composed = transform * self._drawable._transform statement = "self._canvas._canvas._canvas.coords(self._object" for p in self._drawable._points: image = composed.image(p) statement += ", %d, %d" % (image.getX(), image.getY()) statement += ")" exec statement self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() def update(self, transform, doRefresh=True): # Update size and position composed = transform * self._drawable._transform if self._object: if len(self._drawable._points)>1: # existing non-trivial path # Update border properties _RenderedShape.update(self, composed, False) statement = "self._canvas._canvas._canvas.coords(self._object" for p in self._drawable._points: image = composed.image(p) statement += ", %d, %d" % (image.getX(), image.getY()) statement += ")" exec statement self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() else: # existing path must be remove self.remove(doRefresh) self._object=None else: # no previously existing path image if len(self._drawable._points)>1: self._object = self._createObj(composed) self._canvas._canvas._moveToDepth(self, composed._depth, doRefresh) def _createObj(self, transform): """ Actually creates a path (assumed to be non-trivial). """ statement = "newObj = self._canvas._canvas._canvas.create_line(" for p in self._drawable._points: image = transform.image(p) statement += "%d, %d, " % (image.getX(), image.getY()) statement += "fill=getTkColor(self._drawable._borderColor), width=self._drawable._borderWidth)" exec statement return newObj class _RenderedPolygon(_RenderedFillableShape): def __init__(self, drawable, canvas, transform, doRefresh=True): _RenderedFillableShape.__init__(self, drawable, canvas, False) if len(self._drawable._points)>0: self._object = self._createObj(transform) self._canvas._canvas._moveToDepth(self, transform._depth, False) if doRefresh: self._canvas._canvas.refresh() else: self._object = None def update(self, transform, doRefresh=True): # Update size and position composed = transform * self._drawable._transform if self._object: if len(self._drawable._points)>0: # existing non-trivial polygon # Update border properties _RenderedFillableShape.update(self, composed, False) statement = "self._canvas._canvas._canvas.coords(self._object" for p in self._drawable._points: image = composed.image(p) statement += ", %d, %d" % (image.getX(), image.getY()) statement += ")" exec statement self._canvas._canvas._moveToDepth(self, composed._depth, False) if doRefresh: self._canvas._canvas.refresh() else: # existing polygon must be remove self.remove(doRefresh) self._object=None else: # no previously existing polygon image if len(self._drawable._points)>0: self._object = self._createObj(composed) self._canvas._canvas._moveToDepth(self, composed._depth, doRefresh) def _createObj(self, transform): """ Actually creates a polygon (assumed to be non-trivial). """ statement = "newObj = self._canvas._canvas._canvas.create_polygon(" for p in self._drawable._points: image = transform.image(p) statement += "%d, %d, " % (image.getX(), image.getY()) statement += "fill=getTkColor(self._drawable._fillColor), outline=getTkColor(self._drawable._borderColor), width=self._drawable._borderWidth)" exec statement return newObj except: pass try: from java.awt import Color from javax.swing import JFrame _underlyingLibrary = 'AWT' class object: pass True = 1 False = 0 bool = int _isinstance = isinstance def isinstance(obj,classes): answer = False # convert to tuple of classes, if not already if type(classes)!=type((0,0)): classes = (classes,) for cls in classes: if cls==str: if type(obj)==type(''): answer = True break elif cls==int: if type(obj)==type(1): answer = True break elif cls==float: if type(obj)==type(1.): answer = True break elif cls==list: if type(obj)==type([]): answer = True break elif cls==dict: if type(obj)==type({}): answer = True break elif cls==tuple: if type(obj)==type(()): answer = True break elif cls==bool: if obj==True or obj==False: answer = True break else: answer = _isinstance(obj,classes) return answer class _RenderedCanvas(JFrame): def __init__(self, canvas, w, h, background, title, refresh): JFrame.__init__(self, title, size=(w,h), visible=1) self.background = background.getColorValue()[0], background.getColorValue()[1], background.getColorValue()[2] self._canvas = canvas self._graphics = self.contentPane.graphics self._autoRefresh = refresh def paint(self,graphics): pass def setBackgroundColor(self, color): self.background = color.getColorValue()[0], color.getColorValue()[1], color.getColorValue()[2] if self._autoRefresh: self.refresh() def close(self): """ Close the canvas window (if not already closed). """ pass def refresh(self): pass class _RenderedShape(object): def __init__(self, drawable, canvas): self._drawable = drawable self._canvas = canvas def draw(self, transform): pass def update(self, transform): pass class _RenderedFillableShape(_RenderedShape): def __init__(self, drawable, canvas): _RenderedShape.__init__(self, drawable, canvas) def update(self, transform): pass class _RenderedCircle(_RenderedFillableShape): def __init__(self, drawable, canvas, transform): _RenderedFillableShape.__init__(self, drawable, canvas) self._transform = transform self._object = None def draw(self, canvas, transform): canvas.refresh() def update(self, transform): canvas.refresh() except: pass if _underlyingLibrary == None: _underlyingLibrary = 'Null' class _RenderedCanvas: pass class Point(object): """ Stores a two-dimensional point using cartesian coordinates. """ def __init__(self, initialX=0, initialY=0): """ Create a new point instance. initialX x-coordinate of the point (defaults to 0) initialY y-coordinate of the point (defaults to 0) """ if not isinstance(initialX, (int,float)): raise TypeError, 'numeric value expected for x-coodinate' if not isinstance(initialY, (int,float)): raise TypeError, 'numeric value expected for y-coodinate' self._x = initialX self._y = initialY def getX(self): """ Returns the x-coordinate. """ return self._x def setX(self, val): """ Set the x-coordinate to val. """ self._x = val def getY(self): """ Returns the y-coordinate. """ return self._y def setY(self, val): """ Set the y-coordinate to val. """ self._y = val def get(self): """ Returns an (x,y) tuple. """ return self._x, self._y def scale(self, factor): self._x *= factor self._y *= factor def distance(self, other): dx = self._x - other._x dy = self._y - other._y return _math.sqrt(dx * dx + dy * dy) def normalize(self): mag = self.distance( Point() ) if mag > 0: self.scale(1/mag) def __str__(self): return '<' + str(self._x) + ',' + str(self._y) + '>' def __add__(self, other): return Point(self._x + other._x, self._y + other._y) def __mul__(self, operand): if isinstance(operand, (int,float)): # multiply by constant return Point(self._x * operand, self._y * operand) elif isinstance(operand, Point): # dot-product return self._x * operand._x + self._y * operand._y def __rmul__(self, operand): return self * operand def __xor__(self, angle): """ Returns a new point instance representing the original, rotated about the origin. angle number of degrees of rotation (clockwise) """ angle = -_math.pi*angle/180. mag = _math.sqrt(self._x * self._x + self._y * self._y) return Point(self._x * _math.cos(angle) - self._y * _math.sin(angle), self._x * _math.sin(angle) + self._y * _math.cos(angle)) class _TraceToCanvas(object): """ Represents a single nested path leading from a Drawable up to a Canvas upon which it is Rendered """ def __init__(self, drawable, parent): """ parent should either be a Canvas or else a relative _TraceToCanvas instance """ self._drawable = drawable if isinstance(parent,Canvas): self._chain = (parent,) self._chainTrans = Transformation() else: self._chain = parent._chain + (parent._drawable,) self._chainTrans = parent._chainTrans * parent._drawable._transform if _debug>=3: print "In _TraceToCanvas constructor for",hex(id(self)) print " drawable =", drawable print " parent =", parent print " self._chain =", self._chain print " self._chainTrans._depth =", self._chainTrans._depth self.render() def __getitem__(self,y): return self._chain.__getitem__(y) def getBase(self): return self._drawable def getTrans(self): return self._chainTrans def setTrans(self,newtrans): self._chainTrans = newtrans def __contains(self,value): return value in self._chain def getChain(self): return self._chain def getRendered(self): return self._rendered def startswith(self, othertrace): mylen = len(self._chain) otherlen = len(othertrace._chain) return mylen>= otherlen and self[ :otherlen] == othertrace[ : ] def render(self, doRefresh=True): # create actual rendered objects, if possible self._rendered = None # determine underlying class to use # (better way: have each class maintain Class._renderClass) renderClass = None if isinstance(self._drawable, Polygon): renderClass = _RenderedPolygon elif isinstance(self._drawable, (Path,Segment)): renderClass = _RenderedPath elif isinstance(self._drawable, Circle): renderClass = _RenderedCircle elif isinstance(self._drawable, Rectangle): renderClass = _RenderedRectangle elif isinstance(self._drawable, Text): renderClass = _RenderedText elif isinstance(self._drawable, Image): renderClass = _RenderedImage if renderClass: canv = self[0] if canv._autoRefresh: # make low-level change now self._rendered = renderClass(self._drawable, canv, self._chainTrans*self._drawable._transform, doRefresh) else: canv._addToUpdateQueue(self.render) # really should pack False as extra parameter. def update(self, doRefresh=True): """ Underlying mutation possible for the rendered object associated with this trace. Should refresh that rendering, depending upon canvas refresh mode """ if self._rendered: canv = self[0] if canv._autoRefresh: # make low-level change now self._rendered.update(self._chainTrans, doRefresh) else: canv._addToUpdateQueue(self.update) def destroy(self, doRefresh=True): """ Get rid of underlying object (if there) """ if self._rendered: canv = self[0] if canv._autoRefresh: # make low-level change now self._rendered.remove(doRefresh) else: canv._addToUpdateQueue(self.destroy) def __repr__(self): return '\n _TraceToCanvas '+str(hex(id(self)))+':\n chain='+repr(self._chain)+'\n drawable='+repr(self._drawable)+'\n rendered='+repr(self._rendered)+'\n chainTrans='+repr(self._chainTrans) class Transformation(object): def __init__(self, value=None, depth=None): if value: self._matrix = value[:4] self._translation = value[4:] else: self._matrix = (1.,0.,0.,1.) self._translation = (0.,0.) if depth: self._depth = depth else: self._depth = [] def __repr__(self): return '\n Transformation '+str(hex(id(self)))+':\n matrix = %s\n translation = %s\n depth = %s\n'%(repr(self._matrix), repr(self._translation), repr(self._depth)) def image(self, point): return Point( self._matrix[0]*point._x + self._matrix[1]*point._y + self._translation[0], self._matrix[2]*point._x + self._matrix[3]*point._y + self._translation[1]) def inv(self): detinv = 1. / self.det() m = ( self._matrix[3] * detinv, -self._matrix[1] * detinv, -self._matrix[2] * detinv, self._matrix[0] * detinv ) t = ( -m[0]*self._translation[0] - m[1]*self._translation[1], -m[2]*self._translation[0] - m[3]*self._translation[1]) return Transformation(m+t, self._depth) def __mul__(self, other): m = ( self._matrix[0] * other._matrix[0] + self._matrix[1] * other._matrix[2], self._matrix[0] * other._matrix[1] + self._matrix[1] * other._matrix[3], self._matrix[2] * other._matrix[0] + self._matrix[3] * other._matrix[2], self._matrix[2] * other._matrix[1] + self._matrix[3] * other._matrix[3]) p = self.image( Point(other._translation[0], other._translation[1]) ) return Transformation(m + (p.getX(), p.getY()), self._depth + other._depth) def det(self): return (self._matrix[0] * self._matrix[3] - self._matrix[1] * self._matrix[2]) class Color(object): """ Represents a color. Color can be specified by name or RGB value, and can be made transparent. """ _colorValues = { # more available from /usr/lib/X11/rgb.txt 'black' : ( 0, 0, 0), 'blue' : ( 0, 0,255), 'brown' : (165, 42, 42), 'burlywood' : (222, 184, 135), 'cyan' : ( 0,255,255), 'darkblue' : ( 0, 0,100), 'darkgray' : (170,170,170), 'darkgreen' : ( 0,100, 0), 'gray' : (128,128,128), 'gray10' : (230,230,230), 'gray30' : (179,179,179), 'gray50' : (128,128,128), 'green' : ( 0,255, 0), 'grey' : (128,128,128), 'lightblue' : (174,217,231), 'lightgray' : (211,211,211), 'lightgreen' : (145,239,145), 'lightgrey' : (211,211,211), 'lightpink' : (255,183,194), 'midnightblue' : ( 25, 25,112), 'orange' : (255,128, 0), 'purple' : (128, 0,128), 'red' : (255, 0, 0), 'saddle brown' : (139, 69, 19), 'skyblue' : (136,207,236), 'sky blue' : (136,207,236), 'tan' : (211,181,141), 'turquoise' : ( 64,225,209), 'white' : (255,255,255), 'yellow' : (255,255, 0) } def __init__(self, colorChoice='White'): """ Create a new instance of Color. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance (which will be cloned) It defaults to 'White' """ # we intentionally have Drawable objects using a color # register with the color instance, so that when the color is # muated, the object can be informed that it has changed self._drawables = [] if isinstance(colorChoice,str): try: self.setByName(colorChoice) except ValueError, ve: raise ValueError, str(ve) elif isinstance(colorChoice,tuple): try: self.setByValue(colorChoice) except ValueError, ve: raise ValueError, str(ve) elif isinstance(colorChoice,Color): self._colorName = colorChoice._colorName self._transparent = colorChoice._transparent self._colorValue = colorChoice._colorValue else: raise TypeError, 'Invalid color specification' def setByName(self, colorName): """ Set the color to colorName. colorName a string representing a valid name It colorName is 'Transparent' the resulting color will not show up on a canvas. """ if not isinstance(colorName,str): raise TypeError, 'string expected as color name' self._colorName = colorName if self._colorName.lower() == 'transparent': self._transparent = True self._colorValue = (0,0,0) else: try: self._transparent = False self._colorValue = Color._colorValues[self._colorName.lower()] except KeyError: raise ValueError, '%s is not a valid color name' % colorName self._informDrawables() def getColorName(self): """ Returns the name of the color. If the color was set by RGB value, it returns 'Custom'. """ return self._colorName def setByValue(self, rgbTuple): """ Sets the color to the given tuple of (red, green, blue) values. """ if not isinstance(rgbTuple,tuple): raise TypeError, '(r,g,b) tuple expected' if len(rgbTuple)!=3: raise ValueError, '(r,g,b) tuple must have three components' self._transparent = False self._colorName = 'Custom' self._colorValue = rgbTuple self._informDrawables() def getColorValue(self): """ Returns a tuple of the (red, green, blue) color components. """ return (self._colorValue[0], self._colorValue[1], self._colorValue[2]) def isTransparent(self): """ Returns a boolean variable indicating if the current color is transparent. A return value of True indicates the color is not visibile. """ return self._transparent def __repr__(self): """ Returns the name of the color, if named. Otherwise returns the (r,g,b) value. """ if self._colorName == 'Custom': return self._colorValue.__repr__() else: return self._colorName def _register(self, drawable): """ Called to register a drawable with this color instance """ if drawable not in self._drawables: self._drawables.append(drawable) def _unregister(self, drawable): """ Called to unregister a drawable with this color instance """ if drawable in self._drawables: self._drawables.remove(drawable) def _informDrawables(self): """ When the color instance has been mutated, we must inform those registered drawables. """ for drawable in self._drawables: drawable._objectChanged() class _SortedSet(list): def __init__(self, initial=None): list.__init__(self) # calls the parent class constructor if initial: self.extend(initial) def indexAfter(self, value): index = 0 while index < len(self) and value >= self[index]: index += 1 return index def insert(self, value): if value not in self: index = self.indexAfter(value) list.insert(self, index, value) # the parent's method def append(self, object): self.insert(object) def extend(self, other): for element in other: self.insert(element) def __add__(self, other): result = _SortedSet(self) # creates new copy of self result.extend(other) return result def sort(self): pass class _DepthManager(object): def __init__(self): self._set = _SortedSet() def add(self, obj, depth): self._set.insert((depth,obj)) def remove(self, obj): index = 0 while index < len(self._set): if self._set[index][1] == obj: self._set.remove(self._set[index]) return index += 1 def getPred(self, obj): index = 0 while index < len(self._set): if index + 1 < len(self._set) and self._set[index+1][1] == obj: return self._set[index][1] index += 1 return None def depth(self, obj): index = 0 while index < len(self._set): if self._set[index][1] == obj: return self._set[index][0] index += 1 class _Container(object): def __init__(self): self._contents = [] self._lastUserDrawn = [None,0] # for tracking objects from user-defined classes def __contains__(self, obj): return obj in self._contents def add(self, drawable): """ Add the drawable object to the container. """ # not doing error checking here, as we want tailored messages for Canvas and Layer self._contents.append(drawable) def remove(self, drawable): """ Removes the drawable object from the container. """ # not doing error checking here, as we want tailored messages for Canvas and Layer self._contents.remove(drawable) def clear(self): """ Removes all objects from the container. """ contents = list(self._contents) # intentional clone for drawable in contents: self.remove(drawable) class Canvas(_Container): """ Represents a window which can be drawn upon. """ def __init__(self, w=200, h=200, background=None, title="Graphics canvas", autoRefresh=True): """ Create a new drawing canvas. A new canvas will be created. w width of drawing area (defaults to 200) h height of drawing area (defaults to 200) background color of the background (defaults to White) title window title (defaults to "Graphics Canvas") autoRefresh whether auto-refresh mode is used (default to True) """ _Container.__init__(self) if not background: background = 'white' if not isinstance(w, (int,float)): raise TypeError, 'numeric value expected for width' if not isinstance(h, (int,float)): raise TypeError, 'numeric value expected for height' if not isinstance(title,str): raise TypeError, 'string expected as title' if not isinstance(autoRefresh,bool): raise TypeError, 'autoRefresh flag should be a bool' if isinstance(background,Color): self._backgroundColor = background else: try: self._backgroundColor = Color(background) except TypeError,te: raise TypeError,str(te) except ValueError,ve: raise ValueError,str(ve) self._width = w self._height = h self._title = title self._autoRefresh = autoRefresh self._canvas = _RenderedCanvas(self, w, h, self._backgroundColor, title, autoRefresh) self._updateQueue = [] # Tuples: (trace, callbackFunction) self._objectDepths = _DepthManager() def setBackgroundColor(self, backgroundColor): """ Set the background color. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ if isinstance(backgroundColor,Color): self._backgroundColor = backgroundColor else: try: self._backgroundColor = Color(backgroundColor) except TypeError,te: raise TypeError,str(te) except ValueError,ve: raise ValueError,str(ve) if self._autoRefresh: self._canvas.setBackgroundColor(self._backgroundColor) def getBackgroundColor(self): """ Returns the background color as an instance of Color. """ return self._backgroundColor def setWidth(self, w): """ Resets the canvas width to w. """ if not isinstance(w, (int,float)): raise TypeError, 'numeric value expected for width' if w<=0: raise ValueError, 'width must be positive' self._width = w if self._autoRefresh: self._canvas.setWidth(w) def getWidth(self): """ Get the width of the canvas. """ return self._width def setHeight(self, h): """ Resets the canvas height to h. """ if not isinstance(h, (int,float)): raise TypeError, 'numeric value expected for height' if h<=0: raise ValueError, 'height must be positive' self._height = h if self._autoRefresh: self._canvas.setHeight(h) def getHeight(self): """ Get the height of the canvas. """ return self._height def setTitle(self, title): """ Set the title for the canvas window. """ if not isinstance(title,str): raise TypeError, 'string expected as title' self._title = title if self._autoRefresh: self._canvas.setTitle(title) def getTitle(self): """ Get the title of the window. """ return self._title def close(self): """ Close the canvas window (if not already closed). """ self._canvas.close() def open(self): """ Opens a graphic window (if not already open). """ self._canvas.open() def add(self, drawable): """ Add the drawable object to the canvas. """ if not isinstance(drawable,Drawable): raise TypeError, 'can only add Drawable objects to a Canvas' if drawable in self._contents: raise ValueError, 'object already on the Canvas' _Container.add(self, drawable) if _debug>=2: print "Adding drawable to canvas, beginning draw cycle, drawable =", drawable drawable._draw(self) def remove(self, drawable): """ Removes the drawable object from the canvas. """ if drawable not in self._contents: raise ValueError, 'object not currently on the Canvas' _Container.remove(self, drawable) drawable._undraw(self) def setAutoRefresh(self, autoRefresh=True): """ Change the auto-refresh mode to either True or False. When True (the default), every change to the canvas or to an object drawn upon the canvas will be immediately rendered to the screen. When False, all changes are recorded internally, yet not shown on the screen until the next subsequent call to the refresh() method of this canvas. This allows multiple changes to be buffered and rendered all at once. """ if not isinstance(autoRefresh,bool): raise TypeError, 'autoRefresh flag should be a bool' if autoRefresh and not self._autoRefresh: self.refresh() # flush the current queue self._autoRefresh = autoRefresh def refresh(self): """ Forces all internal changes to be rendered to the screen. This method is only necessary when the auto-refresh property of the canvas has previously been turned off. """ if not self._autoRefresh: self._canvas.setHeight(self._height, False) # delay refresh self._canvas.setWidth(self._width, False) self._canvas.setBackgroundColor(self._backgroundColor, False) self._canvas.setTitle(self._title, False) self._autoRefresh = True # temporarily, to force rendering in following callbacks for update in self._updateQueue: update(False) # delay all refreshing until the end self._autoRefresh = False # undo temporary toggle self._updateQueue = [] self._canvas.refresh() def saveToFile(self, filename): """ Saves a picture of the current canvas to a file. """ self._canvas.saveToFile(filename) def _addToUpdateQueue(self, func): self._updateQueue.append(func) class Drawable(object): """ An object that can be drawn to a graphics canvas. """ def __init__(self, reference=None): """ Creates an instance of Drawable. referencePoint local reference point for scaling and rotating (Defaults to Point(0,0) if None specified) """ if reference: if not isinstance(reference,Point): raise TypeError, 'reference point must be a Point instance' self._reference = reference else: self._reference = Point() self._transform = Transformation() self._transform._depth = [50, _ourRandom.random()] self._traces = {} # dict of _TraceToCanvas objects for each rendered instance of this object self._drones = [] # these are objects which are detected within a UserDefined object def move(self, dx, dy): """ Move the object dx units to the right and dy units down. Negative values move the object left or up. """ if not isinstance(dx, (int,float)): raise TypeError, 'dx must be numeric' if not isinstance(dy, (int,float)): raise TypeError, 'dy must be numeric' self._transform = Transformation( (1.,0.,0.,1.,dx,dy)) * self._transform self._objectChanged() def moveTo(self, x, y): """ Move the object to align its reference point with (x,y) """ if not isinstance(x, (int,float)): raise TypeError, 'x must be numeric' if not isinstance(y, (int,float)): raise TypeError, 'y must be numeric' curRef = self.getReferencePoint() self.move(x-curRef.getX(), y-curRef.getY()) def rotate(self, angle): """ Rotates the object around its current reference point. angle number of degrees of rotation (clockwise) """ if not isinstance(angle, (int,float)): raise TypeError, 'angle must be specified numerically' angle = -_math.pi*angle/180. p = self._localToGlobal(self._reference) trans = Transformation((1.,0.,0.,1.)+p.get()) rot = Transformation((_math.cos(angle),_math.sin(angle), -_math.sin(angle),_math.cos(angle),0.,0.)) self._transform = trans*(rot*(trans.inv()*self._transform)) self._objectChanged() def scale(self, factor): """ Scales the object relative to its current reference point. factor scale is multiplied by this number (must be positive) """ if not isinstance(factor, (int,float)): raise TypeError, 'scaling factor must be a positive number' if factor<=0: raise ValueError, 'scaling factor must be a positive number' p = self._localToGlobal(self._reference) trans = Transformation((1.,0.,0.,1.)+p.get()) sca = Transformation((factor,0.,0.,factor,0.,0.)) self._transform = trans*(sca*(trans.inv()*self._transform)) self._objectChanged() def flip(self, angle=0): if not isinstance(angle, (int,float)): raise TypeError, 'scaling factor must be a positive number' angle = -_math.pi*angle/180. p = self._localToGlobal(self._reference) trans = Transformation((1.,0.,0.,1.)+p.get()) rot = Transformation((_math.cos(angle),_math.sin(angle), -_math.sin(angle),_math.cos(angle),0.,0.)) rotinv = rot.inv() invert = Transformation((-1.,0.,0.,1.,0.,0.)) self._transform = trans*(rotinv*(invert*(rot*(trans.inv()*self._transform)))) self._objectChanged() def getReferencePoint(self): """ Return a copy of the current reference point placement. Note that mutating that copy has no effect on the drawable object. """ return self._localToGlobal(self._reference) def adjustReference(self, dx, dy): """ Move the local reference point relative to its current position. Note that the object is not moved at all. """ if not isinstance(dx, (int,float)): raise TypeError, 'dx must be numeric' if not isinstance(dy, (int,float)): raise TypeError, 'dy must be numeric' p = self._localToGlobal(self._reference) p = Point(p.getX()+dx, p.getY()+dy) self._reference = self._globalToLocal(p) def setDepth(self, depth): """ Sets the depth of the object. Objects with a higher depth will be rendered behind those with lower depths. """ if not isinstance(depth, (int,float)): raise TypeError, 'depth must be numeric' self._transform._depth[-2] = depth self._objectChanged() def getDepth(self): """ Returns the depth of the object. """ return self._transform._depth[-2] def clone(self): """ Return an exact duplicate of the drawable object. """ return _copy.deepcopy(self) def _localToGlobal(self, point): if not isinstance(point,Point): raise TypeError, 'parameter must be a Point instance' return self._transform.image(point) def _globalToLocal(self, point): if not isinstance(point,Point): raise TypeError, 'parameter must be a Point instance' return self._transform.inv().image(point) def _draw(self, parent): """ Causes the object to be drawn (typically, the method is not called directly). Parent should either be a Canvas to be drawn on or else a TraceToCanvas instance which will be used as relative offset. """ if _debug>=2: print "within Drawable._draw for self=",self,"parent=",parent if isinstance(parent,Canvas): parentContainer = parent elif isinstance(parent,_TraceToCanvas): parentContainer = parent._drawable else: raise TypeError, 'Drawable._draw called with unknown parent' if self not in parentContainer: # figure out the "effective" parent, which is a userDefined object # and then adjust our trace accordingly owner = parentContainer._lastUserDrawn[0] if self not in owner._drones: owner._drones.append(self) parent = parentContainer._lastUserDrawn[1] if _debug>=3: print '*****object being drawn on misdirected container.\n drawable=',self,'container=',parentContainer print ' reassigning perceived parent to',parentContainer._lastUserDrawn[1] # adjust random component of depth to be serialized self._transform._depth[-1] = parentContainer._lastUserDrawn[2] parentContainer._lastUserDrawn[2] -= 1 if _debug>=2: print "about to create a new (composed) trace" newTrace = _TraceToCanvas(self,parent) newKey = newTrace.getChain() if newKey in self._traces: raise StandardError, 'cs1graphics -- Unexpected Error: duplicated trace' self._traces[newKey] = newTrace if self._isUserDefined(): # record information about this object being drawn on parent, in case # it's draw ends up causing additional objects to be drawn. if _debug>=3: print 'Recording _draw of UserDefined object.\n drawable=',self,'container=',parentContainer parentContainer._lastUserDrawn = [self,newTrace,0] def _undraw(self, parent, doRefresh=True): """ Get rid of all renderings of this object relative to the parent container. """ if _debug>=1: print 'within _undraw for self=',self,'parent=',parent effectedCanvases = [] for (key,trace) in self._traces.items(): can = trace._chain[0] if can not in effectedCanvases: effectedCanvases.append(can) if parent == trace.getChain()[-1]: trace.destroy(False) self._traces.pop(key) for drone in self._drones: drone._undraw(self,False) if doRefresh: for can in effectedCanvases: can._canvas.refresh() def _objectChanged(self, filter=None, doRefresh=True): """ Some trait of this object has been mutated and so all of its rendered images may need to be updated. filter a _TraceToCanvas instance. If filter given, instead of updated all rendered images, updates will only be performed for those which fall under the umbrella of that filter. """ if _debug>=2: print 'Drawable._objectChanged called with self=',self,' filter=',filter effectedCanvases = [] for trace in self._traces.values(): if _debug>=2: print " considering trace =", trace if not filter or filter in trace: if _debug>=2: print " matches filter" can = trace._chain[0] if can not in effectedCanvases: effectedCanvases.append(can) trace.update(False) # wait a bit before refreshing canvases for drawable in self._drones: try: match = drawable._traces[trace.getChain()+(self,)] except KeyError: if _debug>=3: print 'KeyError looking for trace=',trace.getChain()+(self,) print 'drawable=',drawable print 'keys() is ',drawable._traces.keys() raise StandardError, 'cs1graphics -- Unexpected Error: trace not found' if _debug>=2: print 'found match with trans = ',match.getTrans() match.setTrans(trace.getTrans()*self._transform) if _debug>=2: print 'changing to = ',match.getTrans() for drone in self._drones: drone._objectChanged(self,False) if doRefresh: for can in effectedCanvases: can._canvas.refresh() def _isUserDefined(self): """ Determines if this object has base class outside of our own primitives. Typically, this is here for examples of user defined classes which inherit directly from Drawable and then implement their own _draw method. We need to watch for hidden components that are rendered but not ever directly added to a layer/canvas. """ return not isinstance(self,(Image,Text,Circle,Rectangle,Segment,Path,Layer)) class Shape(Drawable): """ Represents objects that are drawable and have a border. """ def __init__(self, reference=None): """ Construct an instance of Shape. reference the initial placement of the shape's reference point. (Defaults to Point(0,0) if None specified) """ if reference and not isinstance(reference,Point): raise TypeError, 'reference point must be a Point instance' Drawable.__init__(self, reference) self._borderColor = Color("Black") self._borderWidth = 1 def setBorderColor(self, borderColor): """ Set the border color to a copy of borderColor. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ old = self._borderColor if isinstance(borderColor,Color): self._borderColor = borderColor else: try: self._borderColor = Color(borderColor) except TypeError,te: raise TypeError,str(te) except ValueError,ve: raise ValueError,str(ve) self._objectChanged() if self._borderColor is not old: self._borderColor._register(self) if not isinstance(self,FillableShape) or old is not self._fillColor: # this shape no longer using the old color old._unregister(self) def getBorderColor(self): """ Return the color of the object's border. """ return self._borderColor def setBorderWidth(self, borderWidth): """ Set the width of the border to borderWidth. """ if not isinstance(borderWidth, (int,float)): raise TypeError, 'Border width must be non-negative number' if borderWidth < 0: raise ValueError, "A shape's border width cannot be negative." self._borderWidth = borderWidth self._objectChanged() def getBorderWidth(self): """ Returns the width of the border. """ return self._borderWidth class FillableShape(Shape): """ A shape that can be filled in. """ def __init__(self, reference=None): """ Construct a new instance of Shape. The interior color defaults to 'transparent'. reference the initial placement of the shape's reference point. (Defaults to Point(0,0) if None specified) """ if reference and not isinstance(reference,Point): raise TypeError, 'reference point must be a Point instance' Shape.__init__(self, reference) self._fillColor = Color("Transparent") def setFillColor(self, color): """ Set the interior color of the shape to the color. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ old = self._fillColor if isinstance(color,Color): self._fillColor = color else: try: self._fillColor = Color(color) except TypeError,te: raise TypeError,str(te) except ValueError,ve: raise ValueError,str(ve) self._objectChanged() if self._fillColor is not old: self._fillColor._register(self) if self._borderColor is not old: # no longer using the old color old._unregister(self) def getFillColor(self): """ Return the color of the shape's interior. """ return self._fillColor class Image(Drawable): def __init__(self, filename): Drawable.__init__(self) if not isinstance(filename,str): raise TypeError, 'filename must be a string' self._filename = filename # of course, we won't know if this is legitimate until it is added to a canvas def rotate(self,angle): """ Not yet implemented. """ raise NotImplementedError,'rotating image is not yet implemented' def scale(self,factor): """ Not yet implemented. """ raise NotImplementedError,'scaling image is not yet implemented' class Text(Drawable): """ A piece of text that can be drawn to a canvas. """ def __init__(self, message='', size=12): """ Construct a new Text instance. The text color is black, though this can be changed by setColor. The reference point for text is the upper-left hand corner of the text. message a string which is to be displayed (empty string by default) size the font size (12 by default) """ if not isinstance(message,str): raise TypeError, 'message must be a string' if not isinstance(size,int): raise TypeError, 'size must be an integer' Drawable.__init__(self) self._text = message self._size = size self._color = Color("black") def setMessage(self, message): """ Set the string to be displayed. message a string """ if not isinstance(message,str): raise TypeError, 'message must be a string' self._text = message self._objectChanged() def getMessage(self): """ Returns the current string. """ return self._text def setFontColor(self, color): """ Set the color of the font. The parameter can be either: - a string with the name of the color - an (r,g,b) tuple - an existing Color instance """ if isinstance(color,Color): self._color = color else: try: self._color = Color(color) except TypeError,te: raise TypeError,str(te) except ValueError,ve: raise ValueError,str(ve) self._objectChanged() def getFontColor(self): """ Returns the current font color. """ return self._color def setFontSize(self, fontsize): """ Set the font size. """ if not isinstance(fontsize,int): raise TypeError, 'fontsize must be an integer' if fontsize<=0: raise ValueError, 'fontsize must be positive' self._size = fontsize self._objectChanged() def getFontSize(self): """ Returns the current font size. """ return size def rotate(self,angle): """ Not yet implemented. """ raise NotImplementedError,'rotating text is not yet implemented' def scale(self,factor): """ Not yet implemented. """ raise NotImplementedError,'scaling text is not yet implemented. Use setSize to change font' # Compatibilities to previous version setSize = setFontSize getSize = getFontSize setColor = setFontColor getColor = getFontColor setText = setMessage getText = getMessage class Circle(FillableShape): """ A circle that can be drawn to a canvas. """ def __init__(self, radius=10, center=None): """ Construct a new instance of Circle. radius the circle's radius. (Defaults to 10) center a Point representing the placement of the circle's center (Defaults to Point(0,0) ) The reference point for a circle is originally its center. """ if not isinstance(radius, (int,float)): raise TypeError, 'Radius must be a number' if radius <= 0: raise ValueError, "The circle's radius must be positive." if center and not isinstance(center,Point): raise TypeError, 'center must be specified as a Point' FillableShape.__init__(self) # intentionally not sending center if not center: center = Point() self._transform = Transformation( (radius,0.,0.,radius,center.getX(),center.getY()), self._transform._depth ) def setRadius(self, radius): """ Set the radius of the circle to radius. """ if not isinstance(radius, (int,float)): raise TypeError, 'Radius must be a number' if radius <= 0: raise ValueError, "The circle's radius must be positive." factor = float(radius)/self.getRadius() self._transform = self._transform * Transformation((factor,0.,0.,factor,0.,0.)) self._objectChanged() def getRadius(self): """ Returns the radius of the circle. """ return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[1]**2) class Rectangle(FillableShape): """ A rectangle that can be drawn to the canvas. """ def __init__(self, width=20, height=10, center=None): """ Construct a new instance of a Rectangle. The reference point for a rectangle is its center. width the width of the rectangle. (Defaults to 20) height the height of the rectangle. (Defaults to 10) center a Point representing the placement of the rectangle's center (Defaults to Point(0,0) ) """ if not isinstance(width, (int,float)): raise TypeError, 'Width must be a number' if width <= 0: raise ValueError, "The width must be positive." if not isinstance(height, (int,float)): raise TypeError, 'Height must be a number' if height <= 0: raise ValueError, "The height must be positive." if center and not isinstance(center,Point): raise TypeError, 'center must be specified as a Point' FillableShape.__init__(self) # intentionally not sending center point if not center: center = Point(0,0) self._transform = Transformation( (width, 0., 0., height, center.getX(), center.getY()), self._transform._depth ) def getWidth(self): """ Returns the width of the rectangle. """ return _math.sqrt(self._transform._matrix[0]**2 + self._transform._matrix[2]**2) def getHeight(self): """ Returns the height of the rectangle. """ return _math.sqrt(self._transform._matrix[1]**2 + self._transform._matrix[3]**2) def setWidth(self, w): """ Sets the width of the rectangle to w. """ if not isinstance(w, (int,float)): raise TypeError, 'Width must be a positive number' if w <= 0: raise ValueError, "The rectangle's width must be positive" factor = float(w) / self.getWidth() p = self._localToGlobal(self._reference) trans = Transformation((1.,0.,0.,1.)+p.get()) sca = Transformation((factor,0.,0.,1.,0.,0.)) # self._transform = self._transform * Transformation((factor,0.,0.,1.,0.,0.)) self._transform = trans*(sca*(trans.inv()*self._transform)) self._objectChanged() def setHeight(self, h): """ Sets the height of the rectangle to h. """ if not isinstance(h, (int,float)): raise TypeError, 'Height must be a positive number' if h <= 0: raise ValueError, "The rectangle's height must be positive" factor = float(h) / self.getHeight() p = self._localToGlobal(self._reference) trans = Transformation((1.,0.,0.,1.)+p.get()) sca = Transformation((1.,0.,0.,factor,0.,0.)) self._transform = trans*(sca*(trans.inv()*self._transform)) self._objectChanged() class Square(Rectangle): """ A square that can be drawn to the canvas. """ def __init__(self, size=10, center=None): """ Construct a new instance of a Square. The reference point for a square is its center. size the dimension of the square. (Defaults to 10) center a Point representing the placement of the rectangle's center (Defaults to Point(0,0) ) """ if not isinstance(size, (int,float)): raise TypeError, 'size must be a number' if size <= 0: raise ValueError, "The size must be positive." if center and not isinstance(center,Point): raise TypeError, 'center must be specified as a Point' Rectangle.__init__(self, size, size, center) def getSize(self): """ Returns the length of a side of the square. """ return self.getWidth() def setSize(self, size): """ Set the length and width of the square to size. """ if not isinstance(size, (int,float)): raise TypeError, 'size must be a number' if size <= 0: raise ValueError, "The size must be positive." Rectangle.setWidth(self, size) Rectangle.setHeight(self, size) def setWidth(self, width): """ Sets the width and height of the square to given width. """ if not isinstance(width, (int,float)): raise TypeError, 'width must be a positive number' if width <= 0: raise ValueError, "The square's width must be positive" self.setSize(width) def setHeight(self, height): """ Sets the width and height of the square to given height. """ if not isinstance(height, (int,float)): raise TypeError, 'height must be a positive number' if height <= 0: raise ValueError, "The square's height must be positive" self.setSize(height) class Segment(Shape): """ A line segment that can be drawn to a canvas. """ def __init__(self, start=None, end=None): """ Construct a new instance of a Segment. The two endpoints are specified as parameters. start the first endpoint (by default, set to Point(10,0) ) end the second endpoint (by default, set to Point(0,0) ) The reference point for a segment is originally its start point. """ Shape.__init__(self) if not start: start = Point(10,0) if not end: end = Point(0,0) if not isinstance(start,Point): raise TypeError, 'start must be specified as a Point' if not isinstance(end,Point): raise TypeError, 'end must be specified as a Point' self._points = [start,end] self.adjustReference(self._points[0].getX(), self._points[0].getY()) def getStart(self): """ Return a copy of the segment's starting point. Subsequently mutating that copy has no effect on segment. """ return Point(self._points[0].getX(), self._points[0].getY()) def getEnd(self): """ Return a copy of the segment's ending point. Subsequently mutating that copy has no effect on segment. """ return Point(self._points[1].getX(), self._points[1].getY()) def setPoints(self, startPoint, endPoint): """ Set the two endpoints of the segment. startPoint a Point instance endPoint a Point instance """ if not isinstance(startPoint,Point): raise TypeError, 'start must be specified as a Point' if not isinstance(endPoint,Point): raise TypeError, 'end must be specified as a Point' self._points = [startPoint,endPoint] self._objectChanged() class Path(Shape): """ A path that can be drawn to a canvas. """ def __init__(self, *points): """ Construct a new instance of a Path. The path is described as a series of points which are connected in order. These points can be initialized either be sending a single tuple of Point instances, or by sending each individual Point as a separate parameter. (by default, path will have zero points) By default, the reference point for a path is aligned with the first point of the path. """ Shape.__init__(self) if len(points)==1 and isinstance(points[0],tuple): points = points[0] for p in points: if not isinstance(p,Point): raise TypeError, 'all parameters must be Point instances' self._points = list(points) if len(self._points)>=1: self.adjustReference(self._points[0].getX(), self._points[0].getY()) def addPoint(self, point, index=None): """ Add a new point to the end of a Path. """ if not isinstance(point,Point): raise TypeError, 'parameter must be a Point instance' if index == None: index = len(self._points) if not isinstance(index,int): raise TypeError, 'index must be an integer' self._points.insert(index, point) if len(self._points)==1: # first point added self._reference = Point(point.getX(), point.getY()) self._objectChanged() def deletePoint(self, index): """ Remove the Point at the given index. """ if not isinstance(index,int): raise TypeError, 'index must be an integer' try: self._points.pop(index) except: raise IndexError, 'index out of range' self._objectChanged() def clearPoints(self): """ Remove all points, setting this back to an empty Path. """ self._points = list() self._objectChanged() def getNumberOfPoints(self): """ Return the current number of points. """ return len(self._points) def getPoint(self, index): """ Return a copy of the Point at the given index. Subsequently mutating that copy has no effect on path. """ if not isinstance(index,int): raise TypeError, 'index must be an integer' try: p = self._points[index] except: raise IndexError, 'index out of range' return Point(p.getX(), p.getY()) def setPoint(self, index, point): """ Change the Point at the given index to a new value. """ if not isinstance(index,int): raise TypeError, 'index must be an integer' if not isinstance(point,Point): raise TypeError, 'parameter must be a Point instance' try: self._points[index] = point except: raise IndexError, 'index out of range' self._objectChanged() class Polygon(Path,FillableShape): """ A polygon that can be drawn to a canvas. """ def __init__(self, *points): """ Construct a new instance of a Polygon. The polygon is described as a series of points which are connected in order. The last point is automatically connected back to the first to close the polygon. These points can be initialized either be sending a single tuple of Point instances, or by sending each individual Point as a separate parameter. (by default, polygon will have zero points) By default, the reference point for a polygon is aligned with the first point of the polygon. """ FillableShape.__init__(self) Path.__init__(self, points) class Layer(Drawable, _Container): """Stores a group of shapes that act as one drawable object. Objects are placed onto the layer relative to the coordinate system of the layer itself. The layer can then be placed onto a canvas (or even onto another layer). """ def __init__(self): """ Construct a new instance of Layer. The layer is initially empty. The reference point of that layer is initially the origin in its own coordinate system, (0,0). """ Drawable.__init__(self) _Container.__init__(self) def add(self, drawable): """ Add the drawable object to the layer. """ if _debug>=2: print 'Call to Layer.add with self=',self,' drawable=',drawable if not isinstance(drawable,Drawable): raise TypeError, 'can only add Drawable objects to a Layer' if drawable in self._contents: raise ValueError, 'object already on the Layer' _Container.add(self, drawable) for trace in self._traces.values(): if _debug>=2: print "Adding drawable to layer, begging draw cycle, trace = ",trace drawable._draw(trace) def remove(self, drawable): """ Removes the drawable object from the layer. """ if drawable not in self._contents: raise ValueError, 'object not currently on the Layer' _Container.remove(self,drawable) drawable._undraw(self) def _draw(self, parent): Drawable._draw(self, parent) if _debug>=2: print 'Call to Layer._draw with self=',self,' parent=',parent if self._contents: composedTrace = _TraceToCanvas(self,parent) for drawable in self._contents: drawable._draw(composedTrace) def _undraw(self, parent, doRefresh=True): # record effected canvases BEFORE calling parent method effectedCanvases = [] for trace in self._traces.values(): can = trace._chain[0] if can not in effectedCanvases: effectedCanvases.append(can) Drawable._undraw(self, parent, False) if _debug>=2: print 'Call to Layer._undraw with self=',self,' parent=',parent for drawable in self._contents: drawable._undraw(self,False) if doRefresh: for can in effectedCanvases: can._canvas.refresh() def _objectChanged(self, filter=None, doRefresh=True): if _debug>=2: print 'Layer._objectChanged called with self=',self,' filter=',filter if _debug>=2: print self._traces.values() effectedCanvases = [] for trace in self._traces.values(): if _debug>=2: print " considering trace =", trace if not filter or filter in trace: if _debug>=2: print " matches filter" can = trace._chain[0] if can not in effectedCanvases: effectedCanvases.append(can) trace.update(False) # wait a bit before refreshing canvases for drawable in self._contents: try: match = drawable._traces[trace.getChain()+(self,)] except KeyError: if _debug>=3: print 'KeyError looking for trace=',trace.getChain()+(self,) print 'drawable=',drawable print 'keys() is ',drawable._traces.keys() raise StandardError, 'cs1graphics -- Unexpected Error: trace not found' if _debug>=2: print 'found match with trans = ',match.getTrans() match.setTrans(trace.getTrans()*self._transform) if _debug>=2: print 'changing to = ',match.getTrans() for drawable in self._contents: drawable._objectChanged(self,False) if doRefresh: for can in effectedCanvases: can._canvas.refresh() def _remove(self, canvas, transform, force=False): if _debug>=2: print 'Call to Layer._remove with self=',self,' canvas=',canvas, ' transform=',transform # if transform: # transform = transform * self._transform # else: # transform = self._transform # transform._container = (canvas,) if not transform: transform = Transformation() transform._container = (canvas,) self._objLookup.removeTransform(transform) for drawable in self._contents: trans = transform * Transformation() trans._container += (self,) drawable._remove(canvas, trans, force) def _RectangleTest(): paper = Canvas(400,400) r = Rectangle(50,200) paper.add(r) r.move(200,200) r.rotate(45)