""" Module Primitive This module contains the Primitive class and its subclasses, used internally by the Graphite plotting package. It also contains the Style classes that control their appearance (e.g., LineStyle and TextStyle). Design: Each primitive takes its style object as a parameter for the constructor. Most primitives also accept information such as start and end points as part of its constructor. Symbols and FormattedLines don't accept their positions until drawing time so that only one instance is required for many draw calls. Primitives -points in 3D space -ultimately transformed to SPING space To Do: + incorporate symbol.py into this file - make the Text primitive into formatted-text - fancier drawing, e.g. dashed or dot-dashed lines """ try: import psyco from psyco.classes import * except ImportError: pass import Num import math from property import * import pid as PID from pid import * import colors from colors import Color import constants as C from constants import X, Y, Z import types #---------------------------------------------------------------------- class LineStyle(PropHolder): """Class: LineStyle Purpose: encapsulates settings used to draw a line """ # declare properties of this class _properties = { 'width': FloatProperty(1, "width of the line, in points", minval=0.0,maxval=10), 'period': FloatProperty(0.03, "period of the dashes, as a fraction of the canvas width", minval=0.001,maxval=1.0), 'onfrac': FloatProperty(0.5, "length of the dashes, as a fraction of the period", minval=0.0, maxval=1.0), 'color': ClassProperty(Color, colors.black, "SPING color of the line"), 'kind': EnumProperty(C.SOLID, "kind of line", (C.SOLID, C.DASHED)) } def __init__(self, width=1, color=colors.black, kind=C.SOLID): PropHolder.__init__(self) self.width = width self.color = color self.kind = kind def __repr__(self): return "LineStyle(width=%s, \n\tcolor=%s, kind=%s)" % \ (self.width, self.color, self.kind) #---------------------------------------------------------------------- class SymbolStyle(PropHolder): """Class: SymbolStyle Purpose: encapsulates settings used to draw a symbol """ # declare properties of this class _properties = { 'size': FloatProperty(1, "height and width of symbol, in points", minval=0.0, maxval=20), 'fillColor': ClassProperty(Color, colors.black, "SPING color for the symbol fill"), 'edgeWidth': FloatProperty(1, "width of outline, in points", minval=0, maxval=10), 'edgeColor': ClassProperty(Color, colors.black, "SPING color for the outline") } def __init__(self, size=5, fillColor=colors.black, edgeWidth=1, edgeColor=colors.black): PropHolder.__init__(self) self.size = size self.fillColor = fillColor self.edgeWidth = edgeWidth self.edgeColor = edgeColor def __repr__(self): return "SymbolStyle(size=%s, \n\tfillColor=%s, \n\tedgeWidth=%s, \n\tedgeColor=%s)" % \ (self.size, self.fillColor, self.edgeWidth, self.edgeColor) #---------------------------------------------------------------------- class TextStyle(PropHolder): """Class: TextStyle Purpose: encapsulates settings used to draw text """ # declare properties of this class _properties = { 'hjust': EnumProperty(C.CENTER, "horizontal justfication: LEFT, CENTER, or RIGHT", (C.LEFT, C.CENTER, C.RIGHT)), 'vjust': EnumProperty(C.CENTER, "vertical justification: TOP, CENTER, or BOTTOM", (C.TOP, C.CENTER, C.BOTTOM)), 'font': ClassProperty(PID.Font, PID.Font(), "SPING base font (and font attributes)"), 'color': ClassProperty(Color, colors.black, "SPING color of the text") \ } def __init__(self, hjust=C.CENTER, vjust=C.CENTER, font=None, color=colors.black): PropHolder.__init__(self) self.hjust, self.vjust = hjust, vjust if font: self.font = font else: self.font = PID.Font() self.color = color def __repr__(self): return "TextStyle(hjust=%s, vjust=%s, \n\tfont=%s, \n\tcolor=%s)" % \ (self.hjust, self.vjust, self.font, self.color) #---------------------------------------------------------------------- #---------------------------------------------------------------------- class Primitive(object): """Class: Primitive Purpose: abstracts any elemental drawing object in 3D space. It can be transformed, and can plot itself into a SPING canvas (ignoring Z coordinate when doing so). """ def __init__(self): self.points = [] def projectTo2D(self): "do the final transformation from extended 3D coordinates to 2D coordinates" for i in range(len(self.points)): self.points[i] = self.points[i] / self.points[i][3] def transform3x3(self, matrix): "Transform our control points by the given 3x3 transformation matrix." for i in range(len(self.points)): self.points[i] = Num.dot(matrix, self.points[i]) def transform4x4(self, matrix): "transform our control points by the given 4x4 transformation matrix" for i in range(len(self.points)): # make an extended vector so we can do matrix mulitplication p = self.points[i] if len(p) < 4: p = Num.array( [p[0], p[1], p[2], 1] ) self.points[i] = Num.dot(matrix, p) def draw(self, canvas): "draw self into the given SPING canvas (ignoring Z)" raise NotImplementedError, "draw" class Line(Primitive): """Class: Line Purpose: a 3D line primitive. """ def __init__(self, p1, p2, style=None, prevSegment=None): """Constructor: creates a line from p1 to p2 (each of which is an (x,y,z) tuple). If prevSegment is non-null, this is a continuation of another line. """ assert len(p1)>=3 assert len(p2)>=3 self.points = [p1, p2] if style is not None: self.style = style else: self.style = LineStyle() self.state = None # Whatever state info the drawing routine might need. self.prevSegment = prevSegment def draw(self, canvas): "Draw self into the given SPING canvas (ignoring Z)." if self.style.width < 0.01: return if self.style.kind == C.SOLID: self._drawSolid(canvas) else: self._drawDashed(canvas) def _drawSolid(self, canvas): canvas.drawLine(self.points[0][X], self.points[0][Y], self.points[1][X], self.points[1][Y], width=self.style.width, color=self.style.color ) def get_state(self): """Distance along the curve, from the beginning.""" if self.prevSegment is None: self.state = 0.0 else: self.state = self.prevSegment.state assert self.state >= 0.0 return self.state def incr_state(self, dist): self.state += dist return self.state def _drawDashed(self, canvas): """Draw self into the given canvas, as a dashed line.""" if self.style.onfrac < 0.001: return x1 = float(self.points[0][X]) y1 = float(self.points[0][Y]) x2 = float(self.points[1][X]) y2 = float(self.points[1][Y]) x = x1 y = y1 period = float(self.style.period) * 256.0 position = self.get_state() % period onfrac = float(self.style.onfrac) peron = period * onfrac # find our x- and y-increments per period: dist = math.hypot(x2-x1, y2-y1) if dist <= 0: return xcos = float(x2-x1)/dist ycos = float(y2-y1)/dist # dxon = dx * onfrac # dyon = dy * onfrac w = self.style.width c = self.style.color while dist > 0: # First, we advance to the end of the current period, if possible if position < peron and dist+position < peron: # The entire line segment is within the 'on' part of the dash. # Draw it, increment state, and return. canvas.drawLine(x, y, x2, y2, width=w, color=c) self.state += dist return elif position < peron: # The line segment starts within the 'on' part of a dash, # but extends beyond. # Draw to end of 'on' part. step = peron - position xe = x+xcos*step ye = y+ycos*step canvas.drawLine(x, y, xe, ye, width=w, color=c) x = xe y = ye position += step dist -= step # If we get here, we are within the 'off' part of the dash. if dist+position < period: # The segment ends in the 'off part of the dash. # Increment the state to the end and return. self.state += dist return else: # If we get here, the segment has crossed into the next # period. step = period - position dist -= step self.state += step position = 0.0 x = x+xcos*step y = y+ycos*step class Box(Primitive): """Class: Box Purpose: a 3D box primitive. """ def __init__(self, p1, p2, lineStyle=None, fillStyle=None): "Constructor: creates a box from p1 to p2 (each of which is an (x,y,z) tuple)" assert len(p1) >= 3 assert len(p2) >= 3 # Now p1, p2 are 3D points diagonally opposite on the box. # Note that while it takes only 2 points to define a box in 3D space, # we must keep all 8 corners around or we will lose important info # when the box is projected into 2D space. self.points = [p1, # bottom left back [p1[X],p2[Y],p1[Z]], # top left back [p2[X],p2[Y],p1[Z]], # top right back [p2[X],p1[Y],p1[Z]], # bottom right back [p1[X],p1[Y],p2[Z]], # bottom left front [p1[X],p2[Y],p2[Z]], # top left front p2, # top right front [p2[X],p1[Y],p2[Z]]] # bottom right front if lineStyle is not None: self.lineStyle = lineStyle else: self.lineStyle = LineStyle() if fillStyle is not None: self.fillStyle = fillStyle else: self.fillStyle = colors.black def draw(self, canvas): "draw self into the given SPING canvas (ignoring Z)" # NOTE: we shouldn't be doing this final transformation here; # rather, it should be implemented at a higher level somewhere corners = [0]*8 for i in range(8): corners[i] = (self.points[i][X], self.points[i][Y]) # draw all 3 sides which face the camera if corners[BLF][X] < corners[BRF][X]: # ...front canvas.drawPolygon( [corners[BLF], corners[TLF], corners[TRF], corners[BRF]], edgeWidth=self.lineStyle.width, edgeColor=self.lineStyle.color, fillColor=self.fillStyle, closed=1 ) else: # ... back canvas.drawPolygon( [corners[BLB], corners[TLB], corners[TRB],corners[BRB]], edgeWidth=self.lineStyle.width, edgeColor=self.lineStyle.color, fillColor=self.fillStyle, closed=1 ) if corners[BRF][X] < corners[BRB][X]: # ...right canvas.drawPolygon( [corners[BRB], corners[TRB], corners[TRF],corners[BRF]], edgeWidth=self.lineStyle.width, edgeColor=self.lineStyle.color, fillColor=self.fillStyle, closed=1 ) else: # ...left canvas.drawPolygon( [corners[BLB], corners[TLB], corners[TLF],corners[BLF]], edgeWidth=self.lineStyle.width, edgeColor=self.lineStyle.color, fillColor=self.fillStyle, closed=1 ) if corners[TLF][Y] > corners[TLB][Y]: # ...top canvas.drawPolygon( [corners[TLB], corners[TLF], corners[TRF],corners[TRB]], edgeWidth=self.lineStyle.width, edgeColor=self.lineStyle.color, fillColor=self.fillStyle, closed=1 ) else: # ...bottom canvas.drawPolygon( [corners[BLB], corners[BLF], corners[BRF],corners[BRB]], edgeWidth=self.lineStyle.width, edgeColor=self.lineStyle.color, fillColor=self.fillStyle, closed=1 ) # constants used to identify box corners above... just for convenience BRF,TRF,TLF,BLF,BRB,TRB,TLB,BLB, = range(8) #---------------------------------------------------------------------- class Symbol(Primitive): """Class: Symbol Purpose: This is the base class for a symbol. Each type of symbol will be its own subclass. """ registry = [] def __init__(self, pos, style=None): "Constructor: initializes the symbol's style." self.points = [pos] if style is not None: self.style = style else: self.style = SymbolStyle() def draw(self,canvas): "Draws the symbol on canvas." raise NotImplementedError, 'draw' def _register(cls, name, theClass): cls.registry.append( (name, theClass) ) _register = classmethod(_register) def G(cls, name): if type(name) == types.IntType: return cls.registry[name][1] for (n, cl) in cls.registry: if n == name: return cl return cls.registry[abs(hash(name))%len(cls.registry)][1] G = classmethod(G) class CircleSymbol(Symbol): """Class: CircleSymbol Purpose: implements the circle symbol """ def draw(self,canvas): x = self.points[0][X] y = self.points[0][Y] canvas.drawEllipse( x - (self.style.size/2), y - (self.style.size/2), x + (self.style.size/2), y + (self.style.size/2), fillColor=self.style.fillColor, edgeWidth=self.style.edgeWidth, edgeColor=self.style.edgeColor ) Symbol._register('circle', CircleSymbol) class _polySymbol(Symbol): def corners(self): raise RuntimeError, "Virtual Function" def draw(self,canvas): x,y = (self.points[0][0], self.points[0][1]) sss = self.style.size/2.0 corners = [ (x+sss*dx, y+sss*dy) for (dx, dy) in self.corners() ] canvas.drawPolygon( corners, fillColor=self.style.fillColor, edgeWidth=self.style.edgeWidth, edgeColor=self.style.edgeColor, closed=1) class SquareSymbol(_polySymbol): """Class: SquareSymbol Purpose: implements the square symbol """ _c = [ (-1, -1), (-1, 1), (1, 1), (1, -1) ] def corners(self): return self._c Symbol._register('square', SquareSymbol) class UTriangleSymbol(_polySymbol): """ Purpose: draws an upside-down triangle. """ r3 = math.sqrt(3.0) _c = [ (0, r3), (-1, -(2-r3)), (1, -(2-r3)) ] def corners(self): return self._c Symbol._register('utriangle', UTriangleSymbol) class TriangleSymbol(_polySymbol): """ Purpose: draws a triangle. """ r3 = math.sqrt(3.0) _c = [ (0, -r3), (-1, (2-r3)), (1, (2-r3)) ] def corners(self): return self._c Symbol._register('triangle', TriangleSymbol) class VstrokeSymbol(_polySymbol): """Purpose: implements a vertical stroke with a little bump in the middle. """ _c = [ (0.4, 0), (0.125, 0.5), (0.125, 1), (-0.125, 1), (-0.125, 0.5), (-0.4, 0), (-0.125, -0.5), (-0.125, -1), (0.125, -1), (0.125, -0.5), (0.4, 0) ] def corners(self): return self._c Symbol._register('vstroke', VstrokeSymbol) class HstrokeSymbol(_polySymbol): """Purpose: implements a horizontal stroke with a little bump in the middle. """ _c = [ (0.4, 0), (0.125, 0.5), (0.125, 1), (-0.125, 1), (-0.125, 0.5), (-0.4, 0), (-0.125, -0.5), (-0.125, -1), (0.125, -1), (0.125, -0.5), (0.4, 0) ] _c = [ (y, x) for (x, y) in _c ] def corners(self): return self._c Symbol._register('hstroke', HstrokeSymbol) #---------------------------------------------------------------------- class Text(Primitive): """Class: Text Purpose: a string to be drawn at some position in 3D space. If you do not specify the pos, it might either have a sensible default applied later (like the title), or it might just raise an exception. """ def __init__(self, text='?', pos=None, style=None, angle=0): "Constructor: creates a text object at the given location" self.text = str(text) self.points = [pos] self.angle = angle if style is not None: self.style = style else: self.style = TextStyle() def pos(self, p=None): op = self.points[0] if p is not None: self.points = [p] return op def __repr__(self): return "Text('%s', pos=%s, \n\tstyle=%s, \n\tangle=%s)" % \ (self.text, self.points[0], self.style, self.angle) def draw(self, canvas): "draw self into the given SPING canvas (ignoring Z)" assert self.points[0] is not None, "No position was set for text=(%s...) " % self.text[:6] x = self.points[0][X] y = self.points[0][Y] # use stringformat.py to give us cool formatting of style, # greek letters, super/subscript, etc. ascent = stringformat.fontAscent(canvas, self.style.font) width = stringformat.stringWidth(canvas, self.text, font=self.style.font) radians = self.angle * math.pi/180.0 if self.style.vjust == C.TOP: x += math.sin(radians) * ascent y += math.cos(radians) * ascent elif self.style.vjust == C.CENTER: x += math.sin(radians) * ascent/2 y += math.cos(radians) * ascent/2 if self.style.hjust == C.CENTER: wfrac = 0.5 elif self.style.hjust == C.RIGHT: wfrac = 1.0 elif isinstance(self.style.hjust, C.AlignChar): wfrac = self.style.hjust.align(self.text) else: wfrac = 0.0 x -= math.cos(radians) * width * wfrac y += math.sin(radians) * width * wfrac stringformat.drawString( canvas, self.text, x, y, font=self.style.font, color=self.style.color, angle=self.angle ) ############################################################## testing #### #def symtest(): # import piddleQD # canvas = piddleQD.QDCanvas( size=(400,300) ) # # # the circle symbol # sym = CircleSymbol( (100,100,0) ) # sym.draw(canvas) # # sym = CircleSymbol( (200,100,0), SymbolStyle()) # sym.draw(canvas) # # sym = CircleSymbol( (20,40,0), SymbolStyle(size=5,fillColor=red)) # sym.draw(canvas) # # # the square symbol # sym = SquareSymbol( (15,30,0), SymbolStyle(size=8,edgeColor=yellow,edgeWidth=2)) # sym.draw(canvas) # #def dashtest(): # import piddleQD # global canvas # try: canvas.close() # except: pass # canvas = piddleQD.QDCanvas() # line = Line( (10,10,0,1), (94,50,0,1), LineStyle(width=1, kind=C.DASHED) ) # line.draw(canvas) # # line = Line( (10,20,0,1), (94,60,0,1), LineStyle(width=1, kind=C.SOLID) ) # line.draw(canvas) # ##symtest() #dashtest()