1# -*- coding: utf-8 -*-
2#----------------------------------------------------------------------------
3# Name:         drawn.py
4# Purpose:      DrawnShape class
5#
6# Author:       Pierre Hjälm (from C++ original by Julian Smart)
7#
8# Created:      2004-08-25
9# Copyright:    (c) 2004 Pierre Hjälm - 1998 Julian Smart
10# License:      wxWindows license
11# Tags:         phoenix-port, unittest, py3-port, documented
12#----------------------------------------------------------------------------
13"""
14The class :class:`~lib.ogl.drawn.DrawnShape`.
15
16THIS DOES NOT SEEM TO BE USED ANYWHERE
17
18"""
19import os.path
20
21from .basic import RectangleShape
22from .oglmisc import *
23
24METAFLAGS_OUTLINE         = 1
25METAFLAGS_ATTACHMENTS     = 2
26
27DRAWN_ANGLE_0        = 0
28DRAWN_ANGLE_90       = 1
29DRAWN_ANGLE_180      = 2
30DRAWN_ANGLE_270      = 3
31
32# Drawing operations
33DRAWOP_SET_PEN               = 1
34DRAWOP_SET_BRUSH             = 2
35DRAWOP_SET_FONT              = 3
36DRAWOP_SET_TEXT_COLOUR       = 4
37DRAWOP_SET_BK_COLOUR         = 5
38DRAWOP_SET_BK_MODE           = 6
39DRAWOP_SET_CLIPPING_RECT     = 7
40DRAWOP_DESTROY_CLIPPING_RECT = 8
41
42DRAWOP_DRAW_LINE             = 20
43DRAWOP_DRAW_POLYLINE         = 21
44DRAWOP_DRAW_POLYGON          = 22
45DRAWOP_DRAW_RECT             = 23
46DRAWOP_DRAW_ROUNDED_RECT     = 24
47DRAWOP_DRAW_ELLIPSE          = 25
48DRAWOP_DRAW_POINT            = 26
49DRAWOP_DRAW_ARC              = 27
50DRAWOP_DRAW_TEXT             = 28
51DRAWOP_DRAW_SPLINE           = 29
52DRAWOP_DRAW_ELLIPTIC_ARC     = 30
53
54
55class DrawOp(object):
56    def __init__(self, theOp):
57        self._op = theOp
58
59    def GetOp(self):
60        return self._op
61
62    def GetPerimeterPoint(self, x1, y1, x2, y2, xOffset, yOffset, attachmentMode):
63        return False
64
65    def Scale(self,scaleX, scaleY):
66        """not implemented???"""
67        pass
68
69    def Translate(self, x, y):
70        """not implemented???"""
71        pass
72
73    def Rotate(self, x, y, theta, sinTheta, cosTheta):
74        """not implemented???"""
75        pass
76
77class OpSetGDI(DrawOp):
78    """Set font, brush, text colour."""
79    def __init__(self, theOp, theImage, theGdiIndex, theMode = 0):
80        DrawOp.__init__(self, theOp)
81
82        self._gdiIndex = theGdiIndex
83        self._image = theImage
84        self._mode = theMode
85
86    def Do(self, dc, xoffset = 0, yoffset = 0):
87        if self._op == DRAWOP_SET_PEN:
88            # Check for overriding this operation for outline colour
89            if self._gdiIndex in self._image._outlineColours:
90                if self._image._outlinePen:
91                    dc.SetPen(self._image._outlinePen)
92            else:
93                try:
94                    dc.SetPen(self._image._gdiObjects[self._gdiIndex])
95                except IndexError:
96                    pass
97        elif self._op == DRAWOP_SET_BRUSH:
98            # Check for overriding this operation for outline or fill colour
99            if self._gdiIndex in self._image._outlineColours:
100                # Need to construct a brush to match the outline pen's colour
101                if self._image._outlinePen:
102                    br = wx.Brush(self._image._outlinePen, wx.BRUSHSTYLE_SOLID)
103                    if br:
104                        dc.SetBrush(br)
105            elif self._gdiIndex in self._image._fillColours:
106                if self._image._fillBrush:
107                    dc.SetBrush(self._image._fillBrush)
108            else:
109                brush = self._image._gdiObjects[self._gdiIndex]
110                if brush:
111                    dc.SetBrush(brush)
112        elif self._op == DRAWOP_SET_FONT:
113            try:
114                dc.SetFont(self._image._gdiObjects[self._gdiIndex])
115            except IndexError:
116                pass
117        elif self._op == DRAWOP_SET_TEXT_COLOUR:
118            dc.SetTextForeground(wx.Colour(self._r, self._g, self._b))
119        elif self._op == DRAWOP_SET_BK_COLOUR:
120            dc.SetTextBackground(wx.Colour(self._r, self._g, self._b))
121        elif self._op == DRAWOP_SET_BK_MODE:
122            dc.SetBackgroundMode(self._mode)
123
124
125class OpSetClipping(DrawOp):
126    """Set/destroy clipping."""
127    def __init__(self, theOp, theX1, theY1, theX2, theY2):
128        DrawOp.__init__(self, theOp)
129
130        self._x1 = theX1
131        self._y1 = theY1
132        self._x2 = theX2
133        self._y2 = theY2
134
135    def Do(self, dc, xoffset, yoffset):
136        if self._op == DRAWOP_SET_CLIPPING_RECT:
137            dc.SetClippingRegion(self._x1 + xoffset, self._y1 + yoffset, self._x2 + xoffset, self._y2 + yoffset)
138        elif self._op == DRAWOP_DESTROY_CLIPPING_RECT:
139            dc.DestroyClippingRegion()
140
141    def Scale(self, scaleX, scaleY):
142        self._x1 *= scaleX
143        self._y1 *= scaleY
144        self._x2 *= scaleX
145        self._y2 *= scaleY
146
147    def Translate(self, x, y):
148        self._x1 += x
149        self._y1 += y
150
151
152class OpDraw(DrawOp):
153    """Draw line, rectangle, rounded rectangle, ellipse, point, arc, text."""
154    def __init__(self, theOp, theX1, theY1, theX2, theY2, theRadius = 0.0, s = ""):
155        DrawOp.__init__(self, theOp)
156
157        self._x1 = theX1
158        self._y1 = theY1
159        self._x2 = theX2
160        self._y2 = theY2
161        self._x3 = 0.0
162        self._y3 = 0.0
163        self._radius = theRadius
164        self._textString = s
165
166    def Do(self, dc, xoffset, yoffset):
167        if self._op == DRAWOP_DRAW_LINE:
168            dc.DrawLine(self._x1 + xoffset, self._y1 + yoffset, self._x2 + xoffset, self._y2 + yoffset)
169        elif self._op == DRAWOP_DRAW_RECT:
170            dc.DrawRectangle(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2)
171        elif self._op == DRAWOP_DRAW_ROUNDED_RECT:
172            dc.DrawRoundedRectangle(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2, self._radius)
173        elif self._op == DRAWOP_DRAW_ELLIPSE:
174            dc.DrawEllipse(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2)
175        elif self._op == DRAWOP_DRAW_ARC:
176            dc.DrawArc(self._x2 + xoffset, self._y2 + yoffset, self._x3 + xoffset, self._y3 + yoffset, self._x1 + xoffset, self._y1 + yoffset)
177        elif self._op == DRAWOP_DRAW_ELLIPTIC_ARC:
178            dc.DrawEllipticArc(self._x1 + xoffset, self._y1 + yoffset, self._x2, self._y2, self._x3 * 360 / (2 * math.pi), self._y3 * 360 / (2 * math.pi))
179        elif self._op == DRAWOP_DRAW_POINT:
180            dc.DrawPoint(self._x1 + xoffset, self._y1 + yoffset)
181        elif self._op == DRAWOP_DRAW_TEXT:
182            dc.DrawText(self._textString, self._x1 + xoffset, self._y1 + yoffset)
183    def Scale(self, scaleX, scaleY):
184        self._x1 *= scaleX
185        self._y1 *= scaleY
186        self._x2 *= scaleX
187        self._y2 *= scaleY
188
189        if self._op != DRAWOP_DRAW_ELLIPTIC_ARC:
190            self._x3 *= scaleX
191            self._y3 *= scaleY
192
193        self._radius *= scaleX
194
195    def Translate(self, x, y):
196        self._x1 += x
197        self._y1 += y
198
199        if self._op == DRAWOP_DRAW_LINE:
200            self._x2 += x
201            self._y2 += y
202        elif self._op == DRAWOP_DRAW_ARC:
203            self._x2 += x
204            self._y2 += y
205            self._x3 += x
206            self._y3 += y
207
208    def Rotate(self, x, y, theta, sinTheta, cosTheta):
209        newX1 = self._x1 * cosTheta + self._y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta
210        newY1 = self._x1 * sinTheta + self._y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta
211
212        if self._op == DRAWOP_DRAW_LINE:
213            newX2 = self._x2 * cosTheta - self._y2 * sinTheta + x * (1 - cosTheta) + y * sinTheta
214            newY2 = self._x2 * sinTheta + self._y2 * cosTheta + y * (1 - cosTheta) + x * sinTheta;
215
216            self._x1 = newX1
217            self._y1 = newY1
218            self._x2 = newX2
219            self._y2 = newY2
220
221        elif self._op in [DRAWOP_DRAW_RECT, DRAWOP_DRAW_ROUNDED_RECT, DRAWOP_DRAW_ELLIPTIC_ARC]:
222            # Assume only 0, 90, 180, 270 degree rotations.
223            # oldX1, oldY1 represents the top left corner. Find the
224            # bottom right, and rotate that. Then the width/height is
225            # the difference between x/y values.
226            oldBottomRightX = self._x1 + self._x2
227            oldBottomRightY = self._y1 + self._y2
228            newBottomRightX = oldBottomRightX * cosTheta - oldBottomRightY * sinTheta + x * (1 - cosTheta) + y * sinTheta
229            newBottomRightY = oldBottomRightX * sinTheta + oldBottomRightY * cosTheta + y * (1 - cosTheta) + x * sinTheta
230
231            # Now find the new top-left, bottom-right coordinates.
232            minX = min(newX1, newBottomRightX)
233            minY = min(newY1, newBottomRightY)
234            maxX = max(newX1, newBottomRightX)
235            maxY = max(newY1, newBottomRightY)
236
237            self._x1 = minX
238            self._y1 = minY
239            self._x2 = maxX - minX # width
240            self._y2 = maxY - minY # height
241
242            if self._op == DRAWOP_DRAW_ELLIPTIC_ARC:
243                # Add rotation to angles
244                self._x3 += theta
245                self._y3 += theta
246        elif self._op == DRAWOP_DRAW_ARC:
247            newX2 = self._x2 * cosTheta - self._y2 * sinTheta + x * (1 - cosTheta) + y * sinTheta
248            newY2 = self._x2 * sinTheta + self._y2 * cosTheta + y * (1 - cosTheta) + x * sinTheta
249            newX3 = self._x3 * cosTheta - self._y3 * sinTheta + x * (1 - cosTheta) + y * sinTheta
250            newY3 = self._x3 * sinTheta + self._y3 * cosTheta + y * (1 - cosTheta) + x * sinTheta
251
252            self._x1 = newX1
253            self._y1 = newY1
254            self._x2 = newX2
255            self._y2 = newY2
256            self._x3 = newX3
257            self._y3 = newY3
258
259
260class OpPolyDraw(DrawOp):
261    """Draw polygon, polyline, spline."""
262    def __init__(self, theOp, thePoints):
263        DrawOp.__init__(self, theOp)
264
265        self._noPoints = len(thePoints)
266        self._points = thePoints
267
268    def Do(self, dc, xoffset, yoffset):
269        if self._op == DRAWOP_DRAW_POLYLINE:
270            dc.DrawLines(self._points, xoffset, yoffset)
271        elif self._op == DRAWOP_DRAW_POLYGON:
272            dc.DrawPolygon(self._points, xoffset, yoffset)
273        elif self._op == DRAWOP_DRAW_SPLINE:
274            dc.DrawSpline(self._points) # no offsets in DrawSpline
275
276    def Scale(self, scaleX, scaleY):
277        for i in range(self._noPoints):
278            self._points[i] = wx.Point(self._points[i][0] * scaleX, self._points[i][1] * scaleY)
279
280    def Translate(self, x, y):
281        for i in range(self._noPoints):
282            self._points[i][0] += x
283            self._points[i][1] += y
284
285    def Rotate(self, x, y, theta, sinTheta, cosTheta):
286        for i in range(self._noPoints):
287            x1 = self._points[i][0]
288            y1 = self._points[i][1]
289
290            self._points[i] = x1 * cosTheta - y1 * sinTheta + x * (1 - cosTheta) + y * sinTheta, x1 * sinTheta + y1 * cosTheta + y * (1 - cosTheta) + x * sinTheta
291
292    def OnDrawOutline(self, dc, x, y, w, h, oldW, oldH):
293        dc.SetBrush(wx.TRANSPARENT_BRUSH)
294
295        # Multiply all points by proportion of new size to old size
296        x_proportion = abs(w / oldW)
297        y_proportion = abs(h / oldH)
298
299        dc.DrawPolygon([(x_proportion * x, y_proportion * y) for x, y in self._points], x, y)
300
301    def GetPerimeterPoint(self, x1, y1, x2, y2, xOffset, yOffset, attachmentMode):
302        # First check for situation where the line is vertical,
303        # and we would want to connect to a point on that vertical --
304        # oglFindEndForPolyline can't cope with this (the arrow
305        # gets drawn to the wrong place).
306        if attachmentMode == ATTACHMENT_MODE_NONE and x1 == x2:
307            # Look for the point we'd be connecting to. This is
308            # a heuristic...
309            for point in self._points:
310                if point[0] == 0:
311                    if y2 > y1 and point[1] > 0:
312                        return point[0]+xOffset, point[1]+yOffset
313                    elif y2 < y1 and point[1] < 0:
314                        return point[0]+xOffset, point[1]+yOffset
315
316        return FindEndForPolyline([ p[0] + xOffset for p in self._points ],
317                                  [ p[1] + yOffset for p in self._points ],
318                                  x1, y1, x2, y2)
319
320
321class PseudoMetaFile(object):
322    """
323    A simple metafile-like class which can load data from a Windows
324    metafile on all platforms.
325    """
326    def __init__(self):
327        self._currentRotation = 0
328        self._rotateable = True
329        self._width = 0.0
330        self._height = 0.0
331        self._outlinePen = None
332        self._fillBrush = None
333        self._outlineOp = -1
334
335        self._ops = []
336        self._gdiObjects = []
337
338        self._outlineColours = []
339        self._fillColours = []
340
341    def Clear(self):
342        self._ops = []
343        self._gdiObjects = []
344        self._outlineColours = []
345        self._fillColours = []
346        self._outlineColours = -1
347
348    def IsValid(self):
349        return self._ops != []
350
351    def GetOps(self):
352        return self._ops
353
354    def SetOutlineOp(self, op):
355        self._outlineOp = op
356
357    def GetOutlineOp(self):
358        return self._outlineOp
359
360    def SetOutlinePen(self, pen):
361        self._outlinePen = pen
362
363    def GetOutlinePen(self, pen):
364        return self._outlinePen
365
366    def SetFillBrush(self, brush):
367        self._fillBrush = brush
368
369    def GetFillBrush(self):
370        return self._fillBrush
371
372    def SetSize(self, w, h):
373        self._width = w
374        self._height = h
375
376    def SetRotateable(self, rot):
377        self._rotateable = rot
378
379    def GetRotateable(self):
380        return self._rotateable
381
382    def GetFillColours(self):
383        return self._fillColours
384
385    def GetOutlineColours(self):
386        return self._outlineColours
387
388    def Draw(self, dc, xoffset, yoffset):
389        for op in self._ops:
390            op.Do(dc, xoffset, yoffset)
391
392    def Scale(self, sx, sy):
393        for op in self._ops:
394            op.Scale(sx, sy)
395
396        self._width *= sx
397        self._height *= sy
398
399    def Translate(self, x, y):
400        for op in self._ops:
401            op.Translate(x, y)
402
403    def Rotate(self, x, y, theta):
404        theta1 = theta - self._currentRotation
405        if theta1 == 0:
406            return
407
408        cosTheta = math.cos(theta1)
409        sinTheta = math.sin(theta1)
410
411        for op in self._ops:
412            op.Rotate(x, y, theta, sinTheta, cosTheta)
413
414        self._currentRotation = theta
415
416    def LoadFromMetaFile(self, filename, rwidth, rheight):
417        if not os.path.exist(filename):
418            return False
419
420        print("LoadFromMetaFile not implemented yet.")
421        return False # TODO
422
423    # Scale to fit size
424    def ScaleTo(self, w, h):
425        scaleX = w / self._width
426        scaleY = h / self._height
427
428        self.Scale(scaleX, scaleY)
429
430    def GetBounds(self):
431        maxX, maxY, minX, minY = -99999.9, -99999.9, 99999.9, 99999.9
432
433        for op in self._ops:
434            if op.GetOp() in [DRAWOP_DRAW_LINE, DRAWOP_DRAW_RECT, DRAWOP_DRAW_ROUNDED_RECT, DRAWOP_DRAW_ELLIPSE, DRAWOP_DRAW_POINT, DRAWOP_DRAW_TEXT]:
435                if op._x1 < minX:
436                    minX = op._x1
437                if op._x1 > maxX:
438                    maxX = op._x1
439                if op._y1 < minY:
440                    minY = op._y1
441                if op._y1 > maxY:
442                    maxY = op._y1
443                if op.GetOp() == DRAWOP_DRAW_LINE:
444                    if op._x2 < minX:
445                        minX = op._x2
446                    if op._x2 > maxX:
447                        maxX = op._x2
448                    if op._y2 < minY:
449                        minY = op._y2
450                    if op._y2 > maxY:
451                        maxY = op._y2
452                elif op.GetOp() in [ DRAWOP_DRAW_RECT, DRAWOP_DRAW_ROUNDED_RECT, DRAWOP_DRAW_ELLIPSE]:
453                    if op._x1 + op._x2 < minX:
454                        minX = op._x1 + op._x2
455                    if op._x1 + op._x2 > maxX:
456                        maxX = op._x1 + op._x2
457                    if op._y1 + op._y2 < minY:
458                        minY = op._y1 + op._y2
459                    if op._y1 + op._y2 > maxX:
460                        maxY = op._y1 + op._y2
461            elif op.GetOp() == DRAWOP_DRAW_ARC:
462                # TODO: don't yet know how to calculate the bounding box
463                # for an arc. So pretend it's a line; to get a correct
464                # bounding box, draw a blank rectangle first, of the
465                # correct size.
466                if op._x1 < minX:
467                    minX = op._x1
468                if op._x1 > maxX:
469                    maxX = op._x1
470                if op._y1 < minY:
471                    minY = op._y1
472                if op._y1 > maxY:
473                    maxY = op._y1
474                if op._x2 < minX:
475                    minX = op._x2
476                if op._x2 > maxX:
477                    maxX = op._x2
478                if op._y2 < minY:
479                    minY = op._y2
480                if op._y2 > maxY:
481                    maxY = op._y2
482            elif op.GetOp() in [DRAWOP_DRAW_POLYLINE, DRAWOP_DRAW_POLYGON, DRAWOP_DRAW_SPLINE]:
483                for point in op._points:
484                    if point[0] < minX:
485                        minX = point[0]
486                    if point[0] > maxX:
487                        maxX = point[0]
488                    if point[1] < minY:
489                        minY = point[1]
490                    if point[1] > maxY:
491                        maxY = point[1]
492
493        return [minX, minY, maxX, maxY]
494
495    # Calculate size from current operations
496    def CalculateSize(self, shape):
497        boundMinX, boundMinY, boundMaxX, boundMaxY = self.GetBounds()
498
499        # By Pierre Hjälm: This is NOT in the old version, which
500        # gets this totally wrong. Since the drawing is centered, we
501        # cannot get the width by measuring from left to right, we
502        # must instead make enough room to handle the largest
503        # coordinates
504        #self.SetSize(boundMaxX - boundMinX, boundMaxY - boundMinY)
505
506        w = max(abs(boundMinX), abs(boundMaxX)) * 2
507        h = max(abs(boundMinY), abs(boundMaxY)) * 2
508
509        self.SetSize(w, h)
510
511        if shape:
512            shape.SetWidth(self._width)
513            shape.SetHeight(self._height)
514
515    # Set of functions for drawing into a pseudo metafile
516    def DrawLine(self, pt1, pt2):
517        op = OpDraw(DRAWOP_DRAW_LINE, pt1[0], pt1[1], pt2[0], pt2[1])
518        self._ops.append(op)
519
520    def DrawRectangle(self, rect):
521        op = OpDraw(DRAWOP_DRAW_RECT, rect[0], rect[1], rect[2], rect[3])
522        self._ops.append(op)
523
524    def DrawRoundedRectangle(self, rect, radius):
525        op = OpDraw(DRAWOP_DRAW_ROUNDED_RECT, rect[0], rect[1], rect[2], rect[3])
526        op._radius = radius
527        self._ops.append(op)
528
529    def DrawEllipse(self, rect):
530        op = OpDraw(DRAWOP_DRAW_ELLIPSE, rect[0], rect[1], rect[2], rect[3])
531        self._ops.append(op)
532
533    def DrawArc(self, centrePt, startPt, endPt):
534        op = OpDraw(DRAWOP_DRAW_ARC, centrePt[0], centrePt[1], startPt[0], startPt[1])
535        op._x3, op._y3 = endPt
536
537        self._ops.append(op)
538
539    def DrawEllipticArc(self, rect, startAngle, endAngle):
540        startAngleRadians = startAngle * math.pi * 2 / 360
541        endAngleRadians = endAngle * math.pi * 2 / 360
542
543        op = OpDraw(DRAWOP_DRAW_ELLIPTIC_ARC, rect[0], rect[1], rect[2], rect[3])
544        op._x3 = startAngleRadians
545        op._y3 = endAngleRadians
546
547        self._ops.append(op)
548
549    def DrawPoint(self, pt):
550        op = OpDraw(DRAWOP_DRAW_POINT, pt[0], pt[1], 0, 0)
551        self._ops.append(op)
552
553    def DrawText(self, text, pt):
554        op = OpDraw(DRAWOP_DRAW_TEXT, pt[0], pt[1], 0, 0)
555        op._textString = text
556        self._ops.append(op)
557
558    def DrawLines(self, pts):
559        op = OpPolyDraw(DRAWOP_DRAW_POLYLINE, pts)
560        self._ops.append(op)
561
562    # flags:
563    # oglMETAFLAGS_OUTLINE: will be used for drawing the outline and
564    #                       also drawing lines/arrows at the circumference.
565    # oglMETAFLAGS_ATTACHMENTS: will be used for initialising attachment
566    #                       points at the vertices (perhaps a rare case...)
567    def DrawPolygon(self, pts, flags = 0):
568        op = OpPolyDraw(DRAWOP_DRAW_POLYGON, pts)
569        self._ops.append(op)
570
571        if flags & METAFLAGS_OUTLINE:
572            self._outlineOp = len(self._ops) - 1
573
574    def DrawSpline(self, pts):
575        op = OpPolyDraw(DRAWOP_DRAW_SPLINE, pts)
576        self._ops.append(op)
577
578    def SetClippingRect(self, rect):
579        OpSetClipping(DRAWOP_SET_CLIPPING_RECT, rect[0], rect[1], rect[2], rect[3])
580
581    def DestroyClippingRect(self):
582        op = OpSetClipping(DRAWOP_DESTROY_CLIPPING_RECT, 0, 0, 0, 0)
583        self._ops.append(op)
584
585    def SetPen(self, pen, isOutline = False):
586        self._gdiObjects.append(pen)
587        op = OpSetGDI(DRAWOP_SET_PEN, self, len(self._gdiObjects) - 1)
588        self._ops.append(op)
589
590        if isOutline:
591            self._outlineColours.append(len(self._gdiObjects) - 1)
592
593    def SetBrush(self, brush, isFill = False):
594        self._gdiObjects.append(brush)
595        op = OpSetGDI(DRAWOP_SET_BRUSH, self, len(self._gdiObjects) - 1)
596        self._ops.append(op)
597
598        if isFill:
599            self._fillColours.append(len(self._gdiObjects) - 1)
600
601    def SetFont(self, font):
602        self._gdiObjects.append(font)
603        op = OpSetGDI(DRAWOP_SET_FONT, self, len(self._gdiObjects) - 1)
604        self._ops.append(op)
605
606    def SetTextColour(self, colour):
607        op = OpSetGDI(DRAWOP_SET_TEXT_COLOUR, self, 0)
608        op._r, op._g, op._b = colour.Red(), colour.Green(), colour.Blue()
609
610        self._ops.append(op)
611
612    def SetBackgroundColour(self, colour):
613        op = OpSetGDI(DRAWOP_SET_BK_COLOUR, self, 0)
614        op._r, op._g, op._b = colour.Red(), colour.Green(), colour.Blue()
615
616        self._ops.append(op)
617
618    def SetBackgroundMode(self, mode):
619        op = OpSetGDI(DRAWOP_SET_BK_MODE, self, 0)
620        self._ops.append(op)
621
622class DrawnShape(RectangleShape):
623    """
624    Draws a pseudo-metafile shape, which can be loaded from a simple
625    Windows metafile.
626
627    wxDrawnShape allows you to specify a different shape for each of four
628    orientations (North, West, South and East). It also provides a set of
629    drawing functions for programmatic drawing of a shape, so that during
630    construction of the shape you can draw into it as if it were a device
631    context.
632
633    Derived from:
634      RectangleShape
635    """
636    def __init__(self):
637        RectangleShape.__init__(self, 100, 50)
638        self._saveToFile = True
639        self._currentAngle = DRAWN_ANGLE_0
640
641        self._metafiles=PseudoMetaFile(), PseudoMetaFile(), PseudoMetaFile(), PseudoMetaFile()
642
643    def OnDraw(self, dc):
644        # Pass pen and brush in case we have force outline
645        # and fill colours
646        if self._shadowMode != SHADOW_NONE:
647            if self._shadowBrush:
648                self._metafiles[self._currentAngle]._fillBrush = self._shadowBrush
649            self._metafiles[self._currentAngle]._outlinePen = wx.Pen(wx.WHITE, 1, wx.PENSTYLE_TRANSPARENT)
650            self._metafiles[self._currentAngle].Draw(dc, self._xpos + self._shadowOffsetX, self._ypos + self._shadowOffsetY)
651
652        self._metafiles[self._currentAngle]._outlinePen = self._pen
653        self._metafiles[self._currentAngle]._fillBrush = self._brush
654        self._metafiles[self._currentAngle].Draw(dc, self._xpos, self._ypos)
655
656    def SetSize(self, w, h, recursive = True):
657        self.SetAttachmentSize(w, h)
658
659        if self.GetWidth() == 0.0:
660            scaleX = 1
661        else:
662            scaleX = w / self.GetWidth()
663
664        if self.GetHeight() == 0.0:
665            scaleY = 1
666        else:
667            scaleY = h / self.GetHeight()
668
669        for i in range(4):
670            if self._metafiles[i].IsValid():
671                self._metafiles[i].Scale(scaleX, scaleY)
672
673        self._width = w
674        self._height = h
675        self.SetDefaultRegionSize()
676
677    def Scale(self, sx, sy):
678        """Scale the shape by the given amount."""
679        for i in range(4):
680            if self._metafiles[i].IsValid():
681                self._metafiles[i].Scale(sx, sy)
682                self._metafiles[i].CalculateSize(self)
683
684    def Translate(self, x, y):
685        """Translate the shape by the given amount."""
686        for i in range(4):
687            if self._metafiles[i].IsValid():
688                self._metafiles[i].Translate(x, y)
689                self._metafiles[i].CalculateSize(self)
690
691    # theta is absolute rotation from the zero position
692    def Rotate(self, x, y, theta):
693        """Rotate about the given axis by the given amount in radians."""
694        self._currentAngle = self.DetermineMetaFile(theta)
695
696        if self._currentAngle == 0:
697            # Rotate metafile
698            if not self._metafiles[0].GetRotateable():
699                return
700
701            self._metafiles[0].Rotate(x, y, theta)
702
703        actualTheta = theta - self._rotation
704
705        # Rotate attachment points
706        sinTheta = math.sin(actualTheta)
707        cosTheta = math.cos(actualTheta)
708
709        for point in self._attachmentPoints:
710            x1 = point._x
711            y1 = point._y
712
713            point._x = x1 * cosTheta - y1 * sinTheta + x * (1.0 - cosTheta) + y * sinTheta
714            point._y = x1 * sinTheta + y1 * cosTheta + y * (1.0 - cosTheta) + x * sinTheta
715
716        self._rotation = theta
717
718        self._metafiles[self._currentAngle].CalculateSize(self)
719
720    # Which metafile do we use now? Based on current rotation and validity
721    # of metafiles.
722    def DetermineMetaFile(self, rotation):
723        tolerance = 0.0001
724        angles = [0.0, math.pi / 2, math.pi, 3 * math.pi / 2]
725
726        whichMetaFile = 0
727
728        for i in range(4):
729            if RoughlyEqual(rotation, angles[i], tolerance):
730                whichMetaFile = i
731                break
732
733        if whichMetaFile > 0 and not self._metafiles[whichMetaFile].IsValid():
734            whichMetaFile = 0
735
736        return whichMetaFile
737
738    def OnDrawOutline(self, dc, x, y, w, h):
739        if self._metafiles[self._currentAngle].GetOutlineOp() != -1:
740            op = self._metafiles[self._currentAngle].GetOps()[self._metafiles[self._currentAngle].GetOutlineOp()]
741            if op.OnDrawOutline(dc, x, y, w, h, self._width, self._height):
742                return
743
744        # Default... just use a rectangle
745        RectangleShape.OnDrawOutline(self, dc, x, y, w, h)
746
747    # Get the perimeter point using the special outline op, if there is one,
748    # otherwise use default wxRectangleShape scheme
749    def GetPerimeterPoint(self, x1, y1, x2, y2):
750        if self._metafiles[self._currentAngle].GetOutlineOp() != -1:
751            op = self._metafiles[self._currentAngle].GetOps()[self._metafiles[self._currentAngle].GetOutlineOp()]
752            p = op.GetPerimeterPoint(x1, y1, x2, y2, self.GetX(), self.GetY(), self.GetAttachmentMode())
753            if p:
754                return p
755
756        return RectangleShape.GetPerimeterPoint(self, x1, y1, x2, y2)
757
758    def LoadFromMetaFile(self, filename):
759        """Load a (very simple) Windows metafile, created for example by
760        Top Draw, the Windows shareware graphics package."""
761        return self._metafiles[0].LoadFromMetaFile(filename)
762
763    # Set of functions for drawing into a pseudo metafile.
764    # They use integers, but doubles are used internally for accuracy
765    # when scaling.
766    def DrawLine(self, pt1, pt2):
767        self._metafiles[self._currentAngle].DrawLine(pt1, pt2)
768
769    def DrawRectangle(self, rect):
770        self._metafiles[self._currentAngle].DrawRectangle(rect)
771
772    def DrawRoundedRectangle(self, rect, radius):
773        """Draw a rounded rectangle.
774
775        radius is the corner radius. If radius is negative, it expresses
776        the radius as a proportion of the smallest dimension of the rectangle.
777        """
778        self._metafiles[self._currentAngle].DrawRoundedRectangle(rect, radius)
779
780    def DrawEllipse(self, rect):
781        self._metafiles[self._currentAngle].DrawEllipse(rect)
782
783    def DrawArc(self, centrePt, startPt, endPt):
784        """Draw an arc."""
785        self._metafiles[self._currentAngle].DrawArc(centrePt, startPt, endPt)
786
787    def DrawEllipticArc(self, rect, startAngle, endAngle):
788        """Draw an elliptic arc."""
789        self._metafiles[self._currentAngle].DrawEllipticArc(rect, startAngle, endAngle)
790
791    def DrawPoint(self, pt):
792        self._metafiles[self._currentAngle].DrawPoint(pt)
793
794    def DrawText(self, text, pt):
795        self._metafiles[self._currentAngle].DrawText(text, pt)
796
797    def DrawLines(self, pts):
798        self._metafiles[self._currentAngle].DrawLines(pts)
799
800    def DrawPolygon(self, pts, flags = 0):
801        """Draw a polygon.
802
803        flags can be one or more of:
804        METAFLAGS_OUTLINE (use this polygon for the drag outline) and
805        METAFLAGS_ATTACHMENTS (use the vertices of this polygon for attachments).
806        """
807        if flags and METAFLAGS_ATTACHMENTS:
808            self.ClearAttachments()
809            for i in range(len(pts)):
810                # TODO: AttachmentPoint does not excist as per PyLint, what should it be???
811                self._attachmentPoints.append(AttachmentPoint(i,pts[i][0],pts[i][1]))
812        self._metafiles[self._currentAngle].DrawPolygon(pts, flags)
813
814    def DrawSpline(self, pts):
815        self._metafiles[self._currentAngle].DrawSpline(pts)
816
817    def SetClippingRect(self, rect):
818        """Set the clipping rectangle."""
819        self._metafiles[self._currentAngle].SetClippingRect(rect)
820
821    def DestroyClippingRect(self):
822        """Destroy the clipping rectangle."""
823        self._metafiles[self._currentAngle].DestroyClippingRect()
824
825    def SetDrawnPen(self, pen, isOutline = False):
826        """Set the pen for this metafile.
827
828        If isOutline is True, this pen is taken to indicate the outline
829        (and if the outline pen is changed for the whole shape, the pen
830        will be replaced with the outline pen).
831        """
832        self._metafiles[self._currentAngle].SetPen(pen, isOutline)
833
834    def SetDrawnBrush(self, brush, isFill = False):
835        """Set the brush for this metafile.
836
837        If isFill is True, the brush is used as the fill brush.
838        """
839        self._metafiles[self._currentAngle].SetBrush(brush, isFill)
840
841    def SetDrawnFont(self, font):
842        self._metafiles[self._currentAngle].SetFont(font)
843
844    def SetDrawnTextColour(self, colour):
845        """Set the current text colour for the current metafile."""
846        self._metafiles[self._currentAngle].SetTextColour(colour)
847
848    def SetDrawnBackgroundColour(self, colour):
849        """Set the current background colour for the current metafile."""
850        self._metafiles[self._currentAngle].SetBackgroundColour(colour)
851
852    def SetDrawnBackgroundMode(self, mode):
853        """Set the current background mode for the current metafile."""
854        self._metafiles[self._currentAngle].SetBackgroundMode(mode)
855
856    def CalculateSize(self):
857        """Calculate the wxDrawnShape size from the current metafile.
858
859        Call this after you have drawn into the shape.
860        """
861        self._metafiles[self._currentAngle].CalculateSize(self)
862
863    def DrawAtAngle(self, angle):
864        """Set the metafile for the given orientation, which can be one of:
865
866        * DRAWN_ANGLE_0
867        * DRAWN_ANGLE_90
868        * DRAWN_ANGLE_180
869        * DRAWN_ANGLE_270
870        """
871        self._currentAngle = angle
872
873    def GetAngle(self):
874        """Return the current orientation, which can be one of:
875
876        * DRAWN_ANGLE_0
877        * DRAWN_ANGLE_90
878        * DRAWN_ANGLE_180
879        * DRAWN_ANGLE_270
880        """
881        return self._currentAngle
882
883    def GetRotation(self):
884        """Return the current rotation of the shape in radians."""
885        return self._rotation
886
887    def SetSaveToFile(self, save):
888        """If save is True, the image will be saved along with the shape's
889        other attributes. The reason why this might not be desirable is that
890        if there are many shapes with the same image, it would be more
891        efficient for the application to save one copy, and not duplicate
892        the information for every shape. The default is True.
893        """
894        self._saveToFile = save
895
896    def GetMetaFile(self, which = 0):
897        """Return a reference to the internal 'pseudo-metafile'."""
898        return self._metafiles[which]
899