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