+ d = iter(data)
+ try:
+ self.position = wx.Point(d.next(), d.next())
+ self.size = wx.Size(d.next(), d.next())
+ self.penColour = wx.Colour(red=d.next(),
+ green=d.next(),
+ blue=d.next())
+ self.fillColour = wx.Colour(red=d.next(),
+ green=d.next(),
+ blue=d.next())
+ self.lineSize = d.next()
+ except StopIteration:
+ raise ValueError('Not enough data in setData call')
+
+ return d
+
+
+ def hasPropertyEditor(self):
+ return False
+
+ def doPropertyEdit(self, parent):
+ assert False, "Must be overridden if hasPropertyEditor returns True"
+
+ def setPosition(self, position):
+ """ Set the origin (top-left corner) for this DrawingObject.
+ """
+ self.position = position
+
+
+ def getPosition(self):
+ """ Return this DrawingObject's position.
+ """
+ return self.position
+
+
+ def setSize(self, size):
+ """ Set the size for this DrawingObject.
+ """
+ self.size = size
+
+
+ def getSize(self):
+ """ Return this DrawingObject's size.
+ """
+ return self.size
+
+
+ def setPenColour(self, colour):
+ """ Set the pen colour used for this DrawingObject.
+ """
+ self.penColour = colour
+
+
+ def getPenColour(self):
+ """ Return this DrawingObject's pen colour.
+ """
+ return self.penColour
+
+
+ def setFillColour(self, colour):
+ """ Set the fill colour used for this DrawingObject.
+ """
+ self.fillColour = colour
+
+
+ def getFillColour(self):
+ """ Return this DrawingObject's fill colour.
+ """
+ return self.fillColour
+
+
+ def setLineSize(self, lineSize):
+ """ Set the linesize used for this DrawingObject.
+ """
+ self.lineSize = lineSize
+
+
+ def getLineSize(self):
+ """ Return this DrawingObject's line size.
+ """
+ return self.lineSize
+
+
+ # ============================
+ # == Object Drawing Methods ==
+ # ============================
+
+ def draw(self, dc, selected):
+ """ Draw this DrawingObject into our window.
+
+ 'dc' is the device context to use for drawing.
+
+ If 'selected' is True, the object is currently selected.
+ Drawing objects can use this to change the way selected objects
+ are drawn, however the actual drawing of selection handles
+ should be done in the 'drawHandles' method
+ """
+ if self.lineSize == 0:
+ dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.TRANSPARENT))
+ else:
+ dc.SetPen(wx.Pen(self.penColour, self.lineSize, wx.SOLID))
+ dc.SetBrush(wx.Brush(self.fillColour, wx.SOLID))
+
+ self._privateDraw(dc, self.position, selected)
+
+
+ def drawHandles(self, dc):
+ """Draw selection handles for this DrawingObject"""
+
+ # Default is to draw selection handles at all four corners.
+ dc.SetPen(wx.BLACK_PEN)
+ dc.SetBrush(wx.BLACK_BRUSH)
+
+ x,y = self.position
+ self._drawSelHandle(dc, x, y)
+ self._drawSelHandle(dc, x + self.size.width, y)
+ self._drawSelHandle(dc, x, y + self.size.height)
+ self._drawSelHandle(dc, x + self.size.width, y + self.size.height)
+
+
+ # =======================
+ # == Selection Methods ==
+ # =======================
+
+ def objectContainsPoint(self, x, y):
+ """ Returns True iff this object contains the given point.
+
+ This is used to determine if the user clicked on the object.
+ """
+ # Firstly, ignore any points outside of the object's bounds.
+
+ if x < self.position.x: return False
+ if x > self.position.x + self.size.x: return False
+ if y < self.position.y: return False
+ if y > self.position.y + self.size.y: return False
+
+ # Now things get tricky. There's no straightforward way of
+ # knowing whether the point is within an arbitrary object's
+ # bounds...to get around this, we draw the object into a
+ # memory-based bitmap and see if the given point was drawn.
+ # This could no doubt be done more efficiently by some tricky
+ # maths, but this approach works and is simple enough.
+
+ # Subclasses can implement smarter faster versions of this.
+
+ bitmap = wx.EmptyBitmap(self.size.x + 10, self.size.y + 10)
+ dc = wx.MemoryDC()
+ dc.SelectObject(bitmap)
+ dc.BeginDrawing()
+ dc.SetBackground(wx.WHITE_BRUSH)
+ dc.Clear()
+ dc.SetPen(wx.Pen(wx.BLACK, self.lineSize + 5, wx.SOLID))
+ dc.SetBrush(wx.BLACK_BRUSH)
+ self._privateDraw(dc, wx.Point(5, 5), True)
+ dc.EndDrawing()
+ pixel = dc.GetPixel(x - self.position.x + 5, y - self.position.y + 5)
+ if (pixel.Red() == 0) and (pixel.Green() == 0) and (pixel.Blue() == 0):
+ return True
+ else:
+ return False
+
+ handle_TOP = 0
+ handle_BOTTOM = 1
+ handle_LEFT = 0
+ handle_RIGHT = 1
+
+ def getSelectionHandleContainingPoint(self, x, y):
+ """ Return the selection handle containing the given point, if any.
+
+ We return one of the predefined selection handle ID codes.
+ """
+ # Default implementation assumes selection handles at all four bbox corners.
+ # Return a list so we can modify the contents later in moveHandle()
+ if self._pointInSelRect(x, y, self.position.x, self.position.y):
+ return [self.handle_TOP, self.handle_LEFT]
+ elif self._pointInSelRect(x, y, self.position.x + self.size.width,
+ self.position.y):
+ return [self.handle_TOP, self.handle_RIGHT]
+ elif self._pointInSelRect(x, y, self.position.x,
+ self.position.y + self.size.height):
+ return [self.handle_BOTTOM, self.handle_LEFT]
+ elif self._pointInSelRect(x, y, self.position.x + self.size.width,
+ self.position.y + self.size.height):
+ return [self.handle_BOTTOM, self.handle_RIGHT]
+ else:
+ return None
+
+ def moveHandle(self, handle, x, y):
+ """ Move the specified selection handle to given canvas location.
+ """
+ assert handle is not None
+
+ # Default implementation assumes selection handles at all four bbox corners.
+ pt = wx.Point(x,y)
+ x,y = self.position
+ w,h = self.size
+ if handle[0] == self.handle_TOP:
+ if handle[1] == self.handle_LEFT:
+ dpos = pt - self.position
+ self.position = pt
+ self.size.width -= dpos.x
+ self.size.height -= dpos.y
+ else:
+ dx = pt.x - ( x + w )
+ dy = pt.y - ( y )
+ self.position.y = pt.y
+ self.size.width += dx
+ self.size.height -= dy
+ else: # BOTTOM
+ if handle[1] == self.handle_LEFT:
+ dx = pt.x - ( x )
+ dy = pt.y - ( y + h )
+ self.position.x = pt.x
+ self.size.width -= dx
+ self.size.height += dy
+ else:
+ dpos = pt - self.position
+ dpos.x -= w
+ dpos.y -= h
+ self.size.width += dpos.x
+ self.size.height += dpos.y
+
+
+ # Finally, normalize so no negative widths or heights.
+ # And update the handle variable accordingly.
+ if self.size.height<0:
+ self.position.y += self.size.height
+ self.size.height = -self.size.height
+ handle[0] = 1-handle[0]
+
+ if self.size.width<0:
+ self.position.x += self.size.width
+ self.size.width = -self.size.width
+ handle[1] = 1-handle[1]
+
+
+
+ def finalizeHandle(self, handle, x, y):
+ pass
+
+
+ def objectWithinRect(self, x, y, width, height):
+ """ Return True iff this object falls completely within the given rect.
+ """
+ if x > self.position.x: return False
+ if x + width < self.position.x + self.size.width: return False
+ if y > self.position.y: return False
+ if y + height < self.position.y + self.size.height: return False
+ return True
+
+ # =====================
+ # == Private Methods ==
+ # =====================
+
+ def _privateDraw(self, dc, position, selected):
+ """ Private routine to draw this DrawingObject.
+
+ 'dc' is the device context to use for drawing, while 'position' is
+ the position in which to draw the object.
+ """
+ pass
+
+ def _drawSelHandle(self, dc, x, y):
+ """ Draw a selection handle around this DrawingObject.
+
+ 'dc' is the device context to draw the selection handle within,
+ while 'x' and 'y' are the coordinates to use for the centre of the
+ selection handle.
+ """
+ dc.DrawRectangle(x - 3, y - 3, 6, 6)
+
+
+ def _pointInSelRect(self, x, y, rX, rY):
+ """ Return True iff (x, y) is within the selection handle at (rX, ry).
+ """
+ if x < rX - 3: return False
+ elif x > rX + 3: return False
+ elif y < rY - 3: return False
+ elif y > rY + 3: return False
+ else: return True
+
+
+#----------------------------------------------------------------------------
+class LineDrawingObject(DrawingObject):
+ """ DrawingObject subclass that represents one line segment.
+
+ Adds the following members to the base DrawingObject:
+ 'startPt' The point, relative to the object's position, where
+ the line starts.
+ 'endPt' The point, relative to the object's position, where
+ the line ends.
+ """
+
+ def __init__(self, startPt=wx.Point(0,0), endPt=wx.Point(0,0), *varg, **kwarg):
+ DrawingObject.__init__(self, *varg, **kwarg)
+
+ self.startPt = wx.Point(startPt.x,startPt.y)
+ self.endPt = wx.Point(endPt.x,endPt.y)
+
+ # ============================
+ # == Object Drawing Methods ==
+ # ============================
+
+ def drawHandles(self, dc):
+ """Draw selection handles for this DrawingObject"""
+
+ dc.SetPen(wx.BLACK_PEN)
+ dc.SetBrush(wx.BLACK_BRUSH)
+
+ x,y = self.position
+ # Draw selection handles at the start and end points.
+ self._drawSelHandle(dc, x + self.startPt.x, y + self.startPt.y)
+ self._drawSelHandle(dc, x + self.endPt.x, y + self.endPt.y)
+
+
+
+ # =======================
+ # == Selection Methods ==
+ # =======================
+
+
+ handle_START_POINT = 1
+ handle_END_POINT = 2
+
+ def getSelectionHandleContainingPoint(self, x, y):
+ """ Return the selection handle containing the given point, if any.
+
+ We return one of the predefined selection handle ID codes.
+ """
+ # We have selection handles at the start and end points.
+ if self._pointInSelRect(x, y, self.position.x + self.startPt.x,
+ self.position.y + self.startPt.y):
+ return self.handle_START_POINT
+ elif self._pointInSelRect(x, y, self.position.x + self.endPt.x,
+ self.position.y + self.endPt.y):
+ return self.handle_END_POINT
+ else:
+ return None
+
+ def moveHandle(self, handle, x, y):
+ """Move the handle to specified handle to the specified canvas coordinates
+ """
+ ptTrans = wx.Point(x-self.position.x, y-self.position.y)
+ if handle == self.handle_START_POINT:
+ self.startPt = ptTrans
+ elif handle == self.handle_END_POINT:
+ self.endPt = ptTrans
+ else:
+ raise ValueError("Bad handle type for a line")
+
+ self._updateBoundingBox()
+
+ # =============================
+ # == Object Property Methods ==
+ # =============================
+
+ def getData(self):
+ """ Return a copy of the object's internal data.
+
+ This is used to save this DrawingObject to disk.
+ """
+ # get the basics
+ data = DrawingObject.getData(self)
+ # add our specifics
+ data += [self.startPt.x, self.startPt.y,
+ self.endPt.x, self.endPt.y]
+ return data
+
+ def setData(self, data):
+ """ Set the object's internal data.
+
+ 'data' is a copy of the object's saved data, as returned by
+ getData() above. This is used to restore a previously saved
+ DrawingObject.
+ """
+ #data = copy.deepcopy(data) # Needed?
+
+ d = DrawingObject.setData(self, data)
+
+ try:
+ self.startPt = wx.Point(d.next(), d.next())
+ self.endPt = wx.Point(d.next(), d.next())
+ except StopIteration:
+ raise ValueError('Not enough data in setData call')
+
+ return d
+
+
+ def setStartPt(self, startPt):
+ """ Set the starting point for this line DrawingObject.
+ """
+ self.startPt = startPt - self.position
+ self._updateBoundingBox()
+
+
+ def getStartPt(self):
+ """ Return the starting point for this line DrawingObject.
+ """
+ return self.startPt + self.position
+
+
+ def setEndPt(self, endPt):
+ """ Set the ending point for this line DrawingObject.
+ """
+ self.endPt = endPt - self.position
+ self._updateBoundingBox()
+
+
+ def getEndPt(self):
+ """ Return the ending point for this line DrawingObject.
+ """
+ return self.endPt + self.position
+
+
+ # =====================
+ # == Private Methods ==
+ # =====================
+
+
+ def _privateDraw(self, dc, position, selected):
+ """ Private routine to draw this DrawingObject.
+
+ 'dc' is the device context to use for drawing, while 'position' is
+ the position in which to draw the object. If 'selected' is True,
+ the object is drawn with selection handles. This private drawing
+ routine assumes that the pen and brush have already been set by the
+ caller.
+ """
+ dc.DrawLine(position.x + self.startPt.x,
+ position.y + self.startPt.y,
+ position.x + self.endPt.x,
+ position.y + self.endPt.y)
+
+ def _updateBoundingBox(self):
+ x = [self.startPt.x, self.endPt.x]; x.sort()
+ y = [self.startPt.y, self.endPt.y]; y.sort()
+
+ dp = wx.Point(-x[0],-y[0])
+ self.position.x += x[0]
+ self.position.y += y[0]
+ self.size.width = x[1]-x[0]
+ self.size.height = y[1]-y[0]
+
+ self.startPt += dp
+ self.endPt += dp
+
+#----------------------------------------------------------------------------
+class PolygonDrawingObject(DrawingObject):
+ """ DrawingObject subclass that represents a poly-line or polygon
+ """
+ def __init__(self, points=[], *varg, **kwarg):
+ DrawingObject.__init__(self, *varg, **kwarg)
+ self.points = list(points)
+
+ # =======================
+ # == Selection Methods ==
+ # =======================
+
+ def getSelectionHandleContainingPoint(self, x, y):
+ """ Return the selection handle containing the given point, if any.
+
+ We return one of the predefined selection handle ID codes.
+ """
+ # We have selection handles at the start and end points.
+ for i,p in enumerate(self.points):
+ if self._pointInSelRect(x, y,
+ self.position.x + p[0],
+ self.position.y + p[1]):
+ return i+1
+
+ return None
+
+
+ def addPoint(self, x,y):
+ self.points.append((x-self.position.x,y-self.position.y))
+ self._updateBoundingBox()
+
+ def getPoint(self, idx):
+ x,y = self.points[idx]
+ return (x+self.position.x,y+self.position.y)
+
+ def movePoint(self, idx, x,y):
+ self.points[idx] = (x-self.position.x,y-self.position.y)
+ self._updateBoundingBox()
+
+ def popPoint(self, idx=-1):
+ self.points.pop(idx)
+ self._updateBoundingBox()
+
+ # =====================
+ # == Drawing Methods ==
+ # =====================
+
+ def drawHandles(self, dc):
+ """Draw selection handles for this DrawingObject"""
+
+ dc.SetPen(wx.BLACK_PEN)
+ dc.SetBrush(wx.BLACK_BRUSH)
+
+ x,y = self.position
+ # Draw selection handles at the start and end points.
+ for p in self.points:
+ self._drawSelHandle(dc, x + p[0], y + p[1])
+
+ def moveHandle(self, handle, x, y):
+ """Move the specified handle"""
+ self.movePoint(handle-1,x,y)
+
+
+ # =============================
+ # == Object Property Methods ==
+ # =============================
+
+ def getData(self):
+ """ Return a copy of the object's internal data.
+
+ This is used to save this DrawingObject to disk.
+ """
+ # get the basics
+ data = DrawingObject.getData(self)
+ # add our specifics
+ data += [list(self.points)]
+
+ return data
+
+
+ def setData(self, data):
+ """ Set the object's internal data.
+
+ 'data' is a copy of the object's saved data, as returned by
+ getData() above. This is used to restore a previously saved
+ DrawingObject.
+ """
+ #data = copy.deepcopy(data) # Needed?
+ d = DrawingObject.setData(self, data)
+
+ try:
+ self.points = d.next()
+ except StopIteration:
+ raise ValueError('Not enough data in setData call')
+
+ return d
+
+
+ # =====================
+ # == Private Methods ==
+ # =====================
+ def _privateDraw(self, dc, position, selected):
+ """ Private routine to draw this DrawingObject.
+
+ 'dc' is the device context to use for drawing, while 'position' is
+ the position in which to draw the object. If 'selected' is True,
+ the object is drawn with selection handles. This private drawing
+ routine assumes that the pen and brush have already been set by the
+ caller.
+ """
+ dc.DrawPolygon(self.points, position.x, position.y)
+
+ def _updateBoundingBox(self):
+ x = min([p[0] for p in self.points])
+ y = min([p[1] for p in self.points])
+ x2 = max([p[0] for p in self.points])
+ y2 = max([p[1] for p in self.points])
+ dx = -x
+ dy = -y
+ self.position.x += x
+ self.position.y += y
+ self.size.width = x2-x
+ self.size.height = y2-y
+ # update coords also because they're relative to self.position
+ for i,p in enumerate(self.points):
+ self.points[i] = (p[0]+dx,p[1]+dy)
+
+
+#----------------------------------------------------------------------------
+class ScribbleDrawingObject(DrawingObject):
+ """ DrawingObject subclass that represents a poly-line or polygon
+ """
+ def __init__(self, points=[], *varg, **kwarg):
+ DrawingObject.__init__(self, *varg, **kwarg)
+ self.points = list(points)
+
+ # =======================
+ # == Selection Methods ==
+ # =======================
+
+ def addPoint(self, x,y):
+ self.points.append((x-self.position.x,y-self.position.y))
+ self._updateBoundingBox()
+
+ def getPoint(self, idx):
+ x,y = self.points[idx]
+ return (x+self.position.x,y+self.position.y)
+
+ def movePoint(self, idx, x,y):
+ self.points[idx] = (x-self.position.x,y-self.position.y)
+ self._updateBoundingBox()
+
+ def popPoint(self, idx=-1):
+ self.points.pop(idx)
+ self._updateBoundingBox()
+
+
+ # =============================
+ # == Object Property Methods ==
+ # =============================
+
+ def getData(self):
+ """ Return a copy of the object's internal data.
+
+ This is used to save this DrawingObject to disk.
+ """
+ # get the basics
+ data = DrawingObject.getData(self)
+ # add our specifics
+ data += [list(self.points)]
+
+ return data
+
+
+ def setData(self, data):
+ """ Set the object's internal data.
+
+ 'data' is a copy of the object's saved data, as returned by
+ getData() above. This is used to restore a previously saved
+ DrawingObject.
+ """
+ #data = copy.deepcopy(data) # Needed?
+ d = DrawingObject.setData(self, data)
+
+ try:
+ self.points = d.next()
+ except StopIteration:
+ raise ValueError('Not enough data in setData call')
+
+ return d
+
+
+ # =====================
+ # == Private Methods ==
+ # =====================
+ def _privateDraw(self, dc, position, selected):
+ """ Private routine to draw this DrawingObject.
+
+ 'dc' is the device context to use for drawing, while 'position' is
+ the position in which to draw the object. If 'selected' is True,
+ the object is drawn with selection handles. This private drawing
+ routine assumes that the pen and brush have already been set by the
+ caller.
+ """
+ dc.SetBrush(wx.TRANSPARENT_BRUSH)
+ dc.DrawLines(self.points, position.x, position.y)
+
+ def _updateBoundingBox(self):
+ x = min([p[0] for p in self.points])
+ y = min([p[1] for p in self.points])
+ x2 = max([p[0] for p in self.points])
+ y2 = max([p[1] for p in self.points])
+ dx = -x
+ dy = -y
+ self.position = wx.Point(self.position.x + x,self.position.y + y)
+ self.size = wx.Size(x2-x, y2-y)
+ #self.position.x += x
+ #self.position.y += y
+ #self.size.width = x2-x
+ #self.size.height = y2-y
+ # update coords also because they're relative to self.position
+ for i,p in enumerate(self.points):
+ self.points[i] = (p[0]+dx,p[1]+dy)
+
+#----------------------------------------------------------------------------
+class RectDrawingObject(DrawingObject):
+ """ DrawingObject subclass that represents an axis-aligned rectangle.
+ """
+ def __init__(self, *varg, **kwarg):
+ DrawingObject.__init__(self, *varg, **kwarg)
+
+ def objectContainsPoint(self, x, y):
+ """ Returns True iff this object contains the given point.
+
+ This is used to determine if the user clicked on the object.
+ """
+ # Firstly, ignore any points outside of the object's bounds.
+
+ if x < self.position.x: return False
+ if x > self.position.x + self.size.x: return False
+ if y < self.position.y: return False
+ if y > self.position.y + self.size.y: return False
+
+ # Rectangles are easy -- they're always selected if the
+ # point is within their bounds.
+ return True
+
+ # =====================
+ # == Private Methods ==
+ # =====================