1#!/usr/local/bin/python3.8 2 3########################################################################### 4# 5# xasy2asy provides a Python interface to Asymptote 6# 7# 8# Authors: Orest Shardt, Supakorn Rassameemasmuang, and John C. Bowman 9# 10########################################################################### 11 12import PyQt5.QtWidgets as Qw 13import PyQt5.QtGui as Qg 14import PyQt5.QtCore as Qc 15import PyQt5.QtSvg as Qs 16 17import numpy as np 18 19import sys 20import os 21import signal 22import threading 23import string 24import subprocess 25import tempfile 26import re 27import shutil 28import copy 29import queue 30import io 31import atexit 32import DebugFlags 33 34import xasyUtils as xu 35import xasyArgs as xa 36import xasyOptions as xo 37import xasySvg as xs 38 39class AsymptoteEngine: 40 xasy=chr(4)+"\n" 41 42 def __init__(self, path=None, keepFiles=DebugFlags.keepFiles, keepDefaultArgs=True): 43 if path is None: 44 path = xa.getArgs().asypath 45 if path is None: 46 opt = xo.BasicConfigs.defaultOpt 47 opt.load() 48 path = opt['asyPath'] 49 50 if sys.platform[:3] == 'win': 51 rx = 0 # stdin 52 wa = 2 # stderr 53 else: 54 rx, wx = os.pipe() 55 ra, wa = os.pipe() 56 os.set_inheritable(rx, True) 57 os.set_inheritable(wx, True) 58 os.set_inheritable(ra, True) 59 os.set_inheritable(wa, True) 60 self.ostream = os.fdopen(wx, 'w') 61 self.istream = os.fdopen(ra, 'r') 62 63 self.keepFiles = keepFiles 64 if sys.platform[:3] == 'win': 65 self.tmpdir = tempfile.mkdtemp(prefix='xasyData_',dir='./')+'/' 66 else: 67 self.tmpdir = tempfile.mkdtemp(prefix='xasyData_')+os.sep 68 69 self.args=['-xasy', '-noV', '-q', '-inpipe=' + str(rx), '-outpipe=' + str(wa), '-o', self.tmpdir] 70 71 self.asyPath = path 72 self.asyProcess = None 73 74 def start(self): 75 try: 76 if sys.platform[:3] == 'win': 77 self.asyProcess = subprocess.Popen([self.asyPath] + self.args, 78 stdin=subprocess.PIPE, stderr=subprocess.PIPE, 79 universal_newlines=True) 80 self.ostream = self.asyProcess.stdin 81 self.istream = self.asyProcess.stderr 82 else: 83 self.asyProcess = subprocess.Popen([self.asyPath] + self.args,close_fds=False) 84 finally: 85 atexit.register(self.cleanup) 86 87 def wait(self): 88 if self.asyProcess.returncode is not None: 89 return 90 else: 91 return self.asyProcess.wait() 92 93 def __enter__(self): 94 self.start() 95 return self 96 97 def __exit__(self, exc_type, exc_val, exc_tb): 98 self.stop() 99 self.wait() 100 101 @property 102 def tempDirName(self): 103 return self.tmpdir 104 105 def startThenStop(self): 106 self.start() 107 self.stop() 108 self.wait() 109 110 @property 111 def active(self): 112 if self.asyProcess is None: 113 return False 114 return self.asyProcess.returncode is None 115 116 def stop(self): 117 if self.active: 118 self.asyProcess.kill() 119 120 def cleanup(self): 121 self.stop() 122 if self.asyProcess is not None: 123 self.asyProcess.wait() 124 if not self.keepFiles: 125 if os.path.isdir(self.tempDirName + os.sep): 126 shutil.rmtree(self.tempDirName, ignore_errors=True) 127 128class asyTransform(Qc.QObject): 129 """A python implementation of an asy transform""" 130 131 def __init__(self, initTuple, delete=False): 132 """Initialize the transform with a 6 entry tuple""" 133 super().__init__() 134 if isinstance(initTuple, (tuple, list)) and len(initTuple) == 6: 135 self.t = initTuple 136 self.x, self.y, self.xx, self.xy, self.yx, self.yy = initTuple 137 self._deleted = delete 138 else: 139 raise TypeError("Illegal initializer for asyTransform") 140 141 @property 142 def deleted(self): 143 return self._deleted 144 145 @deleted.setter 146 def deleted(self, value): 147 self._deleted = value 148 149 @classmethod 150 def zero(cls): 151 return asyTransform((0, 0, 0, 0, 0, 0)) 152 153 @classmethod 154 def fromQTransform(cls, transform: Qg.QTransform): 155 tx, ty = transform.dx(), transform.dy() 156 xx, xy, yx, yy = transform.m11(), transform.m21(), transform.m12(), transform.m22() 157 158 return asyTransform((tx, ty, xx, xy, yx, yy)) 159 160 @classmethod 161 def fromNumpyMatrix(cls, transform: np.ndarray): 162 assert transform.shape == (3, 3) 163 164 tx = transform[0, 2] 165 ty = transform[1, 2] 166 167 xx, xy, yx, yy = transform[0:2, 0:2].ravel().tolist()[0] 168 169 return asyTransform((tx, ty, xx, xy, yx, yy)) 170 171 def getRawCode(self): 172 return xu.tuple2StrWOspaces(self.t) 173 174 def getCode(self, asy2psmap=None): 175 """Obtain the asy code that represents this transform""" 176 if asy2psmap is None: 177 asy2psmap = asyTransform((0, 0, 1, 0, 0, 1)) 178 if self.deleted: 179 return 'zeroTransform' 180 else: 181 return (asy2psmap.inverted() * self * asy2psmap).getRawCode() 182 183 def scale(self, s): 184 return asyTransform((0, 0, s, 0, 0, s)) * self 185 186 def toQTransform(self): 187 return Qg.QTransform(self.xx, self.yx, self.xy, self.yy, self.x, self.y) 188 189 def __str__(self): 190 """Equivalent functionality to getCode(). It allows the expression str(asyTransform) to be meaningful.""" 191 return self.getCode() 192 193 def isIdentity(self): 194 return self == identity() 195 196 def inverted(self): 197 return asyTransform.fromQTransform(self.toQTransform().inverted()[0]) 198 199 def __eq__(self, other): 200 return list(self.t) == list(other.t) 201 202 def __mul__(self, other): 203 """Define multiplication of transforms as composition.""" 204 if isinstance(other, tuple): 205 if len(other) == 6: 206 return self * asyTransform(other) 207 elif len(other) == 2: 208 return ((self.t[0] + self.t[2] * other[0] + self.t[3] * other[1]), 209 (self.t[1] + self.t[4] * other[0] + self.t[5] * other[1])) 210 else: 211 raise Exception("Illegal multiplier of {:s}".format(str(type(other)))) 212 elif isinstance(other, asyTransform): 213 result = asyTransform((0, 0, 0, 0, 0, 0)) 214 result.x = self.x + self.xx * other.x + self.xy * other.y 215 result.y = self.y + self.yx * other.x + self.yy * other.y 216 result.xx = self.xx * other.xx + self.xy * other.yx 217 result.xy = self.xx * other.xy + self.xy * other.yy 218 result.yx = self.yx * other.xx + self.yy * other.yx 219 result.yy = self.yx * other.xy + self.yy * other.yy 220 result.t = (result.x, result.y, result.xx, result.xy, result.yx, result.yy) 221 return result 222 elif isinstance(other, str): 223 if other != 'cycle': 224 raise TypeError 225 else: 226 return 'cycle' 227 else: 228 raise TypeError("Illegal multiplier of {:s}".format(str(type(other)))) 229 230 231def identity(): 232 return asyTransform((0, 0, 1, 0, 0, 1)) 233 234def yflip(): 235 return asyTransform((0, 0, 1, 0, 0, -1)) 236 237class asyObj(Qc.QObject): 238 """A base class for asy objects: an item represented by asymptote code.""" 239 def __init__(self): 240 """Initialize the object""" 241 super().__init__() 242 self.asyCode = '' 243 244 def updateCode(self, ps2asymap=identity()): 245 """Update the object's code: should be overriden.""" 246 raise NotImplementedError 247 248 def getCode(self, ps2asymap=identity()): 249 """Return the code describing the object""" 250 self.updateCode(ps2asymap) 251 return self.asyCode 252 253 254class asyPen(asyObj): 255 """A python wrapper for an asymptote pen""" 256 @staticmethod 257 def getColorFromQColor(color): 258 return color.redF(), color.greenF(), color.blueF() 259 260 @staticmethod 261 def convertToQColor(color): 262 r, g, b = color 263 return Qg.QColor.fromRgbF(r, g, b) 264 265 @classmethod 266 def fromAsyPen(cls, pen): 267 assert isinstance(pen, cls) 268 return cls(asyengine=pen._asyengine, color=pen.color, width=pen.width, pen_options=pen.options) 269 270 def __init__(self, asyengine=None, color=(0, 0, 0), width=0.5, pen_options=""): 271 """Initialize the pen""" 272 asyObj.__init__(self) 273 self.color = (0, 0, 0) 274 self.options = pen_options 275 self.width = width 276 self._asyengine = asyengine 277 self._deferAsyfy = False 278 if pen_options: 279 self._deferAsyfy = True 280 self.updateCode() 281 self.setColor(color) 282 283 @property 284 def asyEngine(self): 285 return self._asyengine 286 287 @asyEngine.setter 288 def asyEngine(self, value): 289 self._asyengine = value 290 291 def updateCode(self, asy2psmap=identity()): 292 """Generate the pen's code""" 293 if self._deferAsyfy: 294 self.computeColor() 295 self.asyCode = 'rgb({:g},{:g},{:g})+{:s}'.format(self.color[0], self.color[1], self.color[2], str(self.width)) 296 if len(self.options) > 0: 297 self.asyCode = self.asyCode + '+' + self.options 298 299 def setWidth(self, newWidth): 300 """Set the pen's width""" 301 self.width = newWidth 302 self.updateCode() 303 304 def setColor(self, color): 305 """Set the pen's color""" 306 if isinstance(color, tuple) and len(color) == 3: 307 self.color = color 308 else: 309 self.color = (0, 0, 0) 310 self.updateCode() 311 312 def setColorFromQColor(self, color): 313 self.setColor(asyPen.getColorFromQColor(color)) 314 315 def computeColor(self): 316 """Find out the color of an arbitrary asymptote pen.""" 317 assert isinstance(self.asyEngine, AsymptoteEngine) 318 assert self.asyEngine.active 319 320 fout = self.asyEngine.ostream 321 fin = self.asyEngine.istream 322 323 fout.write("pen p=" + self.getCode() + ';\n') 324 fout.write("write(_outpipe,colorspace(p),newl);\n") 325 fout.write("write(_outpipe,colors(p));\n") 326 fout.write("flush(_outpipe);\n") 327 fout.write(self.asyEngine.xasy) 328 fout.flush() 329 330 colorspace = fin.readline() 331 if colorspace.find("cmyk") != -1: 332 lines = fin.readline() + fin.readline() + fin.readline() + fin.readline() 333 parts = lines.split() 334 c, m, y, k = eval(parts[0]), eval(parts[1]), eval(parts[2]), eval(parts[3]) 335 k = 1 - k 336 r, g, b = ((1 - c) * k, (1 - m) * k, (1 - y) * k) 337 elif colorspace.find("rgb") != -1: 338 lines = fin.readline() + fin.readline() + fin.readline() 339 parts = lines.split() 340 r, g, b = eval(parts[0]), eval(parts[1]), eval(parts[2]) 341 elif colorspace.find("gray") != -1: 342 lines = fin.readline() 343 parts = lines.split() 344 r = g = b = eval(parts[0]) 345 else: 346 raise ChildProcessError('Asymptote error.') 347 self.color = (r, g, b) 348 self._deferAsyfy = False 349 350 def tkColor(self): 351 """Return the tk version of the pen's color""" 352 self.computeColor() 353 return '#{}'.format("".join(["{:02x}".format(min(int(256 * a), 255)) for a in self.color])) 354 355 def toQPen(self): 356 if self._deferAsyfy: 357 self.computeColor() 358 newPen = Qg.QPen() 359 newPen.setColor(asyPen.convertToQColor(self.color)) 360 newPen.setWidthF(self.width) 361 362 return newPen 363 364 365class asyPath(asyObj): 366 """A python wrapper for an asymptote path""" 367 368 def __init__(self, asyengine: AsymptoteEngine=None, forceCurve=False): 369 """Initialize the path to be an empty path: a path with no nodes, control points, or links.""" 370 super().__init__() 371 self.nodeSet = [] 372 self.linkSet = [] 373 self.forceCurve = forceCurve 374 self.controlSet = [] 375 self.computed = False 376 self.asyengine = asyengine 377 378 @classmethod 379 def fromPath(cls, oldPath): 380 newObj = asyPath(None) 381 newObj.nodeSet = copy.copy(oldPath.nodeSet) 382 newObj.linkSet = copy.copy(oldPath.linkSet) 383 newObj.controlSet = copy.deepcopy(oldPath.controlSet) 384 newObj.computed = oldPath.computed 385 newObj.asyengine = oldPath.asyengine 386 387 return newObj 388 389 @classmethod 390 def fromBezierPoints(cls, pointList: list, engine=None): 391 if not pointList: 392 return None 393 assert isinstance(pointList[0], BezierCurveEditor.BezierPoint) 394 nodeList = [] 395 controlList = [] 396 for point in pointList: 397 nodeList.append(BezierCurveEditor.QPoint2Tuple(point.point)) 398 if point.rCtrlPoint is not None: # first 399 controlList.append([BezierCurveEditor.QPoint2Tuple(point.rCtrlPoint)]) 400 if point.lCtrlPoint is not None: # last 401 controlList[-1].append(BezierCurveEditor.QPoint2Tuple(point.lCtrlPoint)) 402 newPath = asyPath(asyengine=engine) 403 newPath.initFromControls(nodeList, controlList) 404 return newPath 405 406 def setInfo(self, path): 407 self.nodeSet = copy.copy(path.nodeSet) 408 self.linkSet = copy.copy(path.linkSet) 409 self.controlSet = copy.deepcopy(path.controlSet) 410 self.computed = path.computed 411 412 @property 413 def isEmpty(self): 414 return len(self.nodeSet) == 0 415 416 @property 417 def isDrawable(self): 418 return len(self.nodeSet) >= 2 419 420 def toQPainterPath(self) -> Qg.QPainterPath: 421 return self.toQPainterPathCurve() if self.containsCurve else self.toQPainterPathLine() 422 423 def toQPainterPathLine(self): 424 baseX, baseY = self.nodeSet[0] 425 painterPath = Qg.QPainterPath(Qc.QPointF(baseX, baseY)) 426 427 for pointIndex in range(1, len(self.nodeSet)): 428 node = self.nodeSet[pointIndex] 429 if self.nodeSet[pointIndex] == 'cycle': 430 node = self.nodeSet[0] 431 432 painterPath.lineTo(*node) 433 434 return painterPath 435 436 437 def toQPainterPathCurve(self): 438 if not self.computed: 439 self.computeControls() 440 441 baseX, baseY = self.nodeSet[0] 442 painterPath = Qg.QPainterPath(Qc.QPointF(baseX, baseY)) 443 444 for pointIndex in range(1, len(self.nodeSet)): 445 node = self.nodeSet[pointIndex] 446 if self.nodeSet[pointIndex] == 'cycle': 447 node = self.nodeSet[0] 448 endPoint = Qc.QPointF(node[0], node[1]) 449 ctrlPoint1 = Qc.QPointF(self.controlSet[pointIndex-1][0][0], self.controlSet[pointIndex-1][0][1]) 450 ctrlPoint2 = Qc.QPointF(self.controlSet[pointIndex-1][1][0], self.controlSet[pointIndex-1][1][1]) 451 452 painterPath.cubicTo(ctrlPoint1, ctrlPoint2, endPoint) 453 return painterPath 454 455 def initFromNodeList(self, nodeSet, linkSet): 456 """Initialize the path from a set of nodes and link types, "--", "..", or "::" """ 457 if len(nodeSet) > 0: 458 self.nodeSet = nodeSet[:] 459 self.linkSet = linkSet[:] 460 self.computed = False 461 462 def initFromControls(self, nodeSet, controlSet): 463 """Initialize the path from nodes and control points""" 464 self.controlSet = controlSet[:] 465 self.nodeSet = nodeSet[:] 466 self.computed = True 467 468 def makeNodeStr(self, node): 469 """Represent a node as a string""" 470 if node == 'cycle': 471 return node 472 else: 473 # if really want to, disable this rounding 474 # shouldn't be to much of a problem since 10e-6 is quite small... 475 return '({:.6g},{:.6g})'.format(node[0], node[1]) 476 477 def updateCode(self, ps2asymap=identity()): 478 """Generate the code describing the path""" 479 # currently at postscript. Convert to asy 480 asy2psmap = ps2asymap.inverted() 481 with io.StringIO() as rawAsyCode: 482 count = 0 483 rawAsyCode.write(self.makeNodeStr(asy2psmap * self.nodeSet[0])) 484 for node in self.nodeSet[1:]: 485 if not self.computed or count >= len(self.controlSet): 486 rawAsyCode.write(self.linkSet[count]) 487 rawAsyCode.write(self.makeNodeStr(asy2psmap * node)) 488 else: 489 rawAsyCode.write('..controls ') 490 rawAsyCode.write(self.makeNodeStr(asy2psmap * self.controlSet[count][0])) 491 rawAsyCode.write(' and ') 492 rawAsyCode.write(self.makeNodeStr(asy2psmap * self.controlSet[count][1])) 493 rawAsyCode.write(".." + self.makeNodeStr(asy2psmap * node)) 494 count = count + 1 495 self.asyCode = rawAsyCode.getvalue() 496 497 @property 498 def containsCurve(self): 499 return '..' in self.linkSet or self.forceCurve 500 501 def getNode(self, index): 502 """Return the requested node""" 503 return self.nodeSet[index] 504 505 def getLink(self, index): 506 """Return the requested link""" 507 return self.linkSet[index] 508 509 def setNode(self, index, newNode): 510 """Set a node to a new position""" 511 self.nodeSet[index] = newNode 512 513 def moveNode(self, index, offset): 514 """Translate a node""" 515 if self.nodeSet[index] != "cycle": 516 self.nodeSet[index] = (self.nodeSet[index][0] + offset[0], self.nodeSet[index][1] + offset[1]) 517 518 def setLink(self, index, ltype): 519 """Change the specified link""" 520 self.linkSet[index] = ltype 521 522 def addNode(self, point, ltype): 523 """Add a node to the end of a path""" 524 self.nodeSet.append(point) 525 if len(self.nodeSet) != 1: 526 self.linkSet.append(ltype) 527 if self.computed: 528 self.computeControls() 529 530 def insertNode(self, index, point, ltype=".."): 531 """Insert a node, and its corresponding link, at the given index""" 532 self.nodeSet.insert(index, point) 533 self.linkSet.insert(index, ltype) 534 if self.computed: 535 self.computeControls() 536 537 def setControl(self, index, position): 538 """Set a control point to a new position""" 539 self.controlSet[index] = position 540 541 def popNode(self): 542 if len(self.controlSet) == len(self.nodeSet): 543 self.controlSet.pop() 544 self.nodeSet.pop() 545 self.linkSet.pop() 546 547 def moveControl(self, index, offset): 548 """Translate a control point""" 549 self.controlSet[index] = (self.controlSet[index][0] + offset[0], self.controlSet[index][1] + offset[1]) 550 551 def computeControls(self): 552 """Evaluate the code of the path to obtain its control points""" 553 # For now, if no asymptote process is given spawns a new one. 554 # Only happens if asyengine is None. 555 if self.asyengine is not None: 556 assert isinstance(self.asyengine, AsymptoteEngine) 557 assert self.asyengine.active 558 asy = self.asyengine 559 startUp = False 560 else: 561 startUp = True 562 asy = AsymptoteEngine() 563 asy.start() 564 565 fout = asy.ostream 566 fin = asy.istream 567 568 fout.write("path p=" + self.getCode() + ';\n') 569 fout.write("write(_outpipe,length(p),newl);\n") 570 fout.write("write(_outpipe,unstraighten(p),endl);\n") 571 fout.write(asy.xasy) 572 fout.flush() 573 574 lengthStr = fin.readline() 575 pathSegments = eval(lengthStr.split()[-1]) 576 pathStrLines = [] 577 for i in range(pathSegments + 1): 578 line = fin.readline() 579 line = line.replace("\n", "") 580 pathStrLines.append(line) 581 oneLiner = "".join(pathStrLines).replace(" ", "") 582 splitList = oneLiner.split("..") 583 nodes = [a for a in splitList if a.find("controls") == -1] 584 self.nodeSet = [] 585 for a in nodes: 586 if a == 'cycle': 587 self.nodeSet.append(a) 588 else: 589 self.nodeSet.append(eval(a)) 590 controls = [a.replace("controls", "").split("and") for a in splitList if a.find("controls") != -1] 591 self.controlSet = [[eval(a[0]), eval(a[1])] for a in controls] 592 self.computed = True 593 594 if startUp: 595 asy.stop() 596 597class asyLabel(asyObj): 598 """A python wrapper for an asy label""" 599 600 def __init__(self, text="", location=(0, 0), pen=None, align=None, fontSize:int=None): 601 """Initialize the label with the given test, location, and pen""" 602 asyObj.__init__(self) 603 self.align = align 604 self.pen = pen 605 self.fontSize = fontSize 606 if align is None: 607 self.align = 'SE' 608 if pen is None: 609 self.pen = asyPen() 610 self.text = text 611 self.location = location 612 613 def updateCode(self, asy2psmap=identity()): 614 """Generate the code describing the label""" 615 newLoc = asy2psmap.inverted() * self.location 616 locStr = xu.tuple2StrWOspaces(newLoc) 617 self.asyCode = 'Label("{0}",{1},p={2}{4},align={3})'.format(self.text, locStr, self.pen.getCode(), self.align, 618 self.getFontSizeText()) 619 620 def getFontSizeText(self): 621 if self.fontSize is not None: 622 return '+fontsize({:.6g})'.format(self.fontSize) 623 else: 624 return '' 625 626 def setText(self, text): 627 """Set the label's text""" 628 self.text = text 629 self.updateCode() 630 631 def setPen(self, pen): 632 """Set the label's pen""" 633 self.pen = pen 634 self.updateCode() 635 636 def moveTo(self, newl): 637 """Translate the label's location""" 638 self.location = newl 639 640 641class asyImage: 642 """A structure containing an image and its format, bbox, and IDTag""" 643 def __init__(self, image, format, bbox, transfKey=None, keyIndex=0): 644 self.image = image 645 self.format = format 646 self.bbox = bbox 647 self.IDTag = None 648 self.key = transfKey 649 self.keyIndex = keyIndex 650 651class xasyItem(Qc.QObject): 652 """A base class for items in the xasy GUI""" 653 mapString = 'xmap' 654 setKeyFormatStr = string.Template('$map("{:s}",{:s});').substitute(map=mapString) 655 setKeyAloneFormatStr = string.Template('$map("{:s}");').substitute(map=mapString) 656 resizeComment="// Resize to initial xasy transform" 657 asySize="" 658 def __init__(self, canvas=None, asyengine=None): 659 """Initialize the item to an empty item""" 660 super().__init__() 661 self.transfKeymap = {} # the new keymap. 662 # should be a dictionary to a list... 663 self.asyCode = '' 664 self.imageList = [] 665 self.IDTag = None 666 self.asyfied = False 667 self.onCanvas = canvas 668 self.keyBuffer = None 669 self._asyengine = asyengine 670 self.drawObjects = [] 671 self.drawObjectsMap = {} 672 self.setKeyed = True 673 self.unsetKeys = set() 674 self.userKeys = set() 675 self.lineOffset = 0 676 self.imageHandleQueue = queue.Queue() 677 678 def updateCode(self, ps2asymap=identity()): 679 """Update the item's code: to be overriden""" 680 with io.StringIO() as rawCode: 681 transfCode = self.getTransformCode() 682 objCode = self.getObjectCode() 683 684 rawCode.write(transfCode) 685 rawCode.write(objCode) 686 self.asyCode = rawCode.getvalue() 687 688 return len(transfCode.splitlines()), len(objCode.splitlines()) 689 690 @property 691 def asyengine(self): 692 return self._asyengine 693 694 @asyengine.setter 695 def asyengine(self, value): 696 self._asyengine = value 697 698 def getCode(self, ps2asymap=identity()): 699 """Return the code describing the item""" 700 self.updateCode(ps2asymap) 701 return self.asyCode 702 703 def getTransformCode(self, asy2psmap=identity()): 704 raise NotImplementedError 705 706 def getObjectCode(self, asy2psmap=identity()): 707 raise NotImplementedError 708 709 def generateDrawObjects(self): 710 raise NotImplementedError 711 712 def handleImageReception(self, file, fileformat, bbox, count, key=None, localCount=0, containsClip=False): 713 """Receive an image from an asy deconstruction. It replaces the default n asyProcess.""" 714 # image = Image.open(file).transpose(Image.FLIP_TOP_BOTTOM) 715 if fileformat == 'png': 716 image = Qg.QImage(file) 717 elif fileformat == 'svg': 718 if containsClip: 719 image = xs.SvgObject(self.asyengine.tempDirName+file) 720 else: 721 image = Qs.QSvgRenderer(file) 722 assert image.isValid() 723 else: 724 raise Exception('Format not supported!') 725 self.imageList.append(asyImage(image, fileformat, bbox, transfKey=key, keyIndex=localCount)) 726 if self.onCanvas is not None: 727 # self.imageList[-1].iqt = ImageTk.PhotoImage(image) 728 currImage = self.imageList[-1] 729 currImage.iqt = image 730 currImage.originalImage = image 731 currImage.originalImage.theta = 0.0 732 currImage.originalImage.bbox = list(bbox) 733 currImage.performCanvasTransform = False 734 735 # handle this case if transform is not in the map yet. 736 # if deleted - set transform to 0, 0, 0, 0, 0 737 transfExists = key in self.transfKeymap.keys() 738 if transfExists: 739 transfExists = localCount <= len(self.transfKeymap[key]) - 1 740 if transfExists: 741 validKey = not self.transfKeymap[key][localCount].deleted 742 else: 743 validKey = False 744 745 if (not transfExists) or validKey: 746 currImage.IDTag = str(file) 747 newDrawObj = DrawObject(currImage.iqt, self.onCanvas['canvas'], transform=identity(), 748 btmRightanchor=Qc.QPointF(bbox[0], bbox[2]), drawOrder=-1, key=key, 749 parentObj=self, keyIndex=localCount) 750 newDrawObj.setBoundingBoxPs(bbox) 751 newDrawObj.setParent(self) 752 753 self.drawObjects.append(newDrawObj) 754 755 if key not in self.drawObjectsMap.keys(): 756 self.drawObjectsMap[key] = [newDrawObj] 757 else: 758 self.drawObjectsMap[key].append(newDrawObj) 759 return containsClip 760 def asyfy(self, force=False): 761 if self.asyengine is None: 762 return 1 763 if self.asyfied and not force: 764 return 765 766 self.drawObjects = [] 767 self.drawObjectsMap.clear() 768 assert isinstance(self.asyengine, AsymptoteEngine) 769 self.imageList = [] 770 771 self.unsetKeys.clear() 772 self.userKeys.clear() 773 774 self.imageHandleQueue = queue.Queue() 775 worker = threading.Thread(target=self.asyfyThread, args=[]) 776 worker.start() 777 item = self.imageHandleQueue.get() 778 cwd=os.getcwd(); 779 os.chdir(self.asyengine.tempDirName) 780 while item != (None,) and item[0] != "ERROR": 781 if item[0] == "OUTPUT": 782 print(item[1]) 783 else: 784 keepFile = self.handleImageReception(*item) 785 if not DebugFlags.keepFiles and not keepFile: 786 try: 787 os.remove(item[0]) 788 pass 789 except OSError: 790 pass 791 finally: 792 pass 793 item = self.imageHandleQueue.get() 794 # self.imageHandleQueue.task_done() 795 os.chdir(cwd); 796 797 worker.join() 798 799 def asyfyThread(self): 800 """Convert the item to a list of images by deconstructing this item's code""" 801 assert self.asyengine.active 802 803 fout = self.asyengine.ostream 804 fin = self.asyengine.istream 805 806 self.lineOffset = len(self.getTransformCode().splitlines()) 807 808 fout.write("reset\n") 809 fout.flush(); 810 for line in self.getCode().splitlines(): 811 if DebugFlags.printDeconstTranscript: 812 print('fout:', line) 813 fout.write(line+"\n") 814 fout.write(self.asySize) 815 fout.write("deconstruct();\n") 816 fout.write('write(_outpipe,yscale(-1)*currentpicture.calculateTransform(),endl);\n') 817 fout.write(self.asyengine.xasy) 818 fout.flush() 819 820 imageInfos = [] # of (box, key) 821 n = 0 822 823 keyCounts = {} 824 825 def render(): 826 for i in range(len(imageInfos)): 827 box, key, localCount, useClip = imageInfos[i] 828 l, b, r, t = [float(a) for a in box.split()] 829 name = "_{:d}.{:s}".format(i, fileformat) 830 831 self.imageHandleQueue.put((name, fileformat, (l, -t, r, -b), i, key, localCount, useClip)) 832 833 # key first, box second. 834 # if key is "Done" 835 raw_text = fin.readline() 836 text = "" 837 if DebugFlags.printDeconstTranscript: 838 print(raw_text.strip()) 839 840 # template=AsyTempDir+"%d_%d.%s" 841 fileformat = 'svg' 842 843 while raw_text != "Done\n" and raw_text != "Error\n": 844# print(raw_text) 845 text = fin.readline() # the actual bounding box. 846 # print('TESTING:', text) 847 keydata = raw_text.strip().replace('KEY=', '', 1) # key 848 849 clipflag = keydata[-1] == '1' 850 userkey = keydata[-2] == '1' 851 keydata = keydata[:-3] 852 853 if not userkey: 854 self.unsetKeys.add(keydata) # the line and column to replace. 855 else: 856 self.userKeys.add(keydata) 857 858# print(line, col) 859 860 if keydata not in keyCounts.keys(): 861 keyCounts[keydata] = 0 862 863 imageInfos.append((text, keydata, keyCounts[keydata], clipflag)) # key-data pair 864 865 # for the next item 866 keyCounts[keydata] += 1 867 868 raw_text = fin.readline() 869 870 if DebugFlags.printDeconstTranscript: 871 print(text.rstrip()) 872 print(raw_text.rstrip()) 873 874 n += 1 875 876 if raw_text != "Error\n": 877 if text == "Error\n": 878 self.imageHandleQueue.put(("ERROR", fin.readline())) 879 else: 880 render() 881 882 self.asy2psmap = asyTransform(xu.listize(fin.readline().rstrip(),float)) 883 else: 884 self.asy2psmap = identity() 885 self.imageHandleQueue.put((None,)) 886 self.asyfied = True 887 888class xasyDrawnItem(xasyItem): 889 """A base class for GUI items was drawn by the user. It combines a path, a pen, and a transform.""" 890 891 def __init__(self, path, engine, pen=None, transform=identity(), key=None): 892 """Initialize the item with a path, pen, and transform""" 893 super().__init__(canvas=None, asyengine=engine) 894 if pen is None: 895 pen = asyPen() 896 self.path = path 897 self.path.asyengine = engine 898 self.asyfied = True 899 self.pen = pen 900 self._asyengine = engine 901 self.rawIdentifier = '' 902 self.transfKey = key 903 self.transfKeymap = {self.transfKey: [transform]} 904 905 @property 906 def asyengine(self): 907 return self._asyengine 908 909 @asyengine.setter 910 def asyengine(self, value: AsymptoteEngine): 911 self._asyengine = value 912 self.path.asyengine = value 913 914 def setKey(self, newKey=None): 915 transform = self.transfKeymap[self.transfKey][0] 916 917 self.transfKey = newKey 918 self.transfKeymap = {self.transfKey: [transform]} 919 920 def generateDrawObjects(self, forceUpdate=False): 921 raise NotImplementedError 922 923 def appendPoint(self, point, link=None): 924 """Append a point to the path. If the path is cyclic, add this point before the 'cycle' node.""" 925 if self.path.nodeSet[-1] == 'cycle': 926 self.path.nodeSet[-1] = point 927 self.path.nodeSet.append('cycle') 928 else: 929 self.path.nodeSet.append(point) 930 self.path.computed = False 931 self.asyfied = False 932 if len(self.path.nodeSet) > 1 and link is not None: 933 self.path.linkSet.append(link) 934 935 def clearTransform(self): 936 """Reset the item's transform""" 937 self.transform = [identity()] 938 self.asyfied = False 939 940 def removeLastPoint(self): 941 """Remove the last point in the path. If the path is cyclic, remove the node before the 'cycle' node.""" 942 if self.path.nodeSet[-1] == 'cycle': 943 del self.path.nodeSet[-2] 944 else: 945 del self.path.nodeSet[-1] 946 del self.path.linkSet[-1] 947 self.path.computed = False 948 self.asyfied = False 949 950 def setLastPoint(self, point): 951 """Modify the last point in the path. If the path is cyclic, modify the node before the 'cycle' node.""" 952 if self.path.nodeSet[-1] == 'cycle': 953 self.path.nodeSet[-2] = point 954 else: 955 self.path.nodeSet[-1] = point 956 self.path.computed = False 957 self.asyfied = False 958 959 960class xasyShape(xasyDrawnItem): 961 """An outlined shape drawn on the GUI""" 962 def __init__(self, path, asyengine, pen=None, transform=identity()): 963 """Initialize the shape with a path, pen, and transform""" 964 super().__init__(path=path, engine=asyengine, pen=pen, transform=transform) 965 966 def getObjectCode(self, asy2psmap=identity()): 967 return 'draw(KEY="{0}",{1},{2});'.format(self.transfKey, self.path.getCode(asy2psmap), self.pen.getCode())+'\n\n' 968 969 def getTransformCode(self, asy2psmap=identity()): 970 transf = self.transfKeymap[self.transfKey][0] 971 if transf == identity(): 972 return '' 973 else: 974 return xasyItem.setKeyFormatStr.format(self.transfKey, transf.getCode(asy2psmap))+'\n' 975 976 def generateDrawObjects(self, forceUpdate=False): 977 if self.path.containsCurve: 978 self.path.computeControls() 979 transf = self.transfKeymap[self.transfKey][0] 980 981 newObj = DrawObject(self.path.toQPainterPath(), None, drawOrder=0, transform=transf, pen=self.pen, 982 key=self.transfKey) 983 newObj.originalObj = self 984 newObj.setParent(self) 985 return [newObj] 986 987 def __str__(self): 988 """Create a string describing this shape""" 989 return "xasyShape code:{:s}".format("\n\t".join(self.getCode().splitlines())) 990 991 992class xasyFilledShape(xasyShape): 993 """A filled shape drawn on the GUI""" 994 995 def __init__(self, path, asyengine, pen=None, transform=identity()): 996 """Initialize this shape with a path, pen, and transform""" 997 if path.nodeSet[-1] != 'cycle': 998 raise Exception("Filled paths must be cyclic") 999 super().__init__(path, asyengine, pen, transform) 1000 1001 def getObjectCode(self, asy2psmap=identity()): 1002 return 'fill(KEY="{0}",{1},{2});'.format(self.transfKey, self.path.getCode(asy2psmap), self.pen.getCode())+'\n\n' 1003 1004 def generateDrawObjects(self, forceUpdate=False): 1005 if self.path.containsCurve: 1006 self.path.computeControls() 1007 newObj = DrawObject(self.path.toQPainterPath(), None, drawOrder=0, transform=self.transfKeymap[self.transfKey][0], 1008 pen=self.pen, key=self.transfKey, fill=True) 1009 newObj.originalObj = self 1010 newObj.setParent(self) 1011 return [newObj] 1012 1013 def __str__(self): 1014 """Return a string describing this shape""" 1015 return "xasyFilledShape code:{:s}".format("\n\t".join(self.getCode().splitlines())) 1016 1017 1018class xasyText(xasyItem): 1019 """Text created by the GUI""" 1020 1021 def __init__(self, text, location, asyengine, pen=None, transform=yflip(), key=None, align=None, fontsize:int=None): 1022 """Initialize this item with text, a location, pen, and transform""" 1023 super().__init__(asyengine=asyengine) 1024 if pen is None: 1025 pen = asyPen(asyengine=asyengine) 1026 if pen.asyEngine is None: 1027 pen.asyEngine = asyengine 1028 self.label = asyLabel(text, location, pen, align, fontSize=fontsize) 1029 # self.transform = [transform] 1030 self.transfKey = key 1031 self.transfKeymap = {self.transfKey: [transform]} 1032 self.asyfied = False 1033 self.onCanvas = None 1034 1035 def setKey(self, newKey=None): 1036 transform = self.transfKeymap[self.transfKey][0] 1037 1038 self.transfKey = newKey 1039 self.transfKeymap = {self.transfKey: [transform]} 1040 1041 def getTransformCode(self, asy2psmap=yflip()): 1042 transf = self.transfKeymap[self.transfKey][0] 1043 if transf == yflip(): 1044 # return xasyItem.setKeyAloneFormatStr.format(self.transfKey) 1045 return '' 1046 else: 1047 return xasyItem.setKeyFormatStr.format(self.transfKey, transf.getCode(asy2psmap))+"\n" 1048 1049 def getObjectCode(self, asy2psmap=yflip()): 1050 return 'label(KEY="{0}",{1});'.format(self.transfKey, self.label.getCode(asy2psmap))+'\n' 1051 1052 def generateDrawObjects(self, forceUpdate=False): 1053 self.asyfy(forceUpdate) 1054 return self.drawObjects 1055 1056 def getBoundingBox(self): 1057 self.asyfy() 1058 return self.imageList[0].bbox 1059 1060 def __str__(self): 1061 return "xasyText code:{:s}".format("\n\t".join(self.getCode().splitlines())) 1062 1063 1064class xasyScript(xasyItem): 1065 """A set of images create from asymptote code. It is always deconstructed.""" 1066 1067 def __init__(self, canvas, engine, script="", transforms=None, transfKeyMap=None): 1068 """Initialize this script item""" 1069 super().__init__(canvas, asyengine=engine) 1070 if transfKeyMap is not None: 1071 self.transfKeymap = transfKeyMap 1072 else: 1073 self.transfKeymap = {} 1074 1075 self.script = script 1076 self.key2imagemap = {} 1077 self.namedUnsetKeys = {} 1078 self.keyPrefix = '' 1079 self.scriptAsyfied = False 1080 self.updatedPrefix = True 1081 1082 def clearTransform(self): 1083 """Reset the transforms for each of the deconstructed images""" 1084 # self.transform = [identity()] * len(self.imageList) 1085 keyCount = {} 1086 1087 for im in self.imageList: 1088 if im.key not in keyCount.keys(): 1089 keyCount[im.key] = 1 1090 else: 1091 keyCount[im.key] += 1 1092 1093 for key in keyCount: 1094 self.transfKeymap[key] = [identity()] * keyCount[key] 1095 1096 def getMaxKeyCounter(self): 1097 maxCounter = -1 1098 for key in self.transfKeymap: 1099 testNum = re.match(r'^x(\d+)$', key) 1100 if testNum is not None: 1101 maxCounter = max(maxCounter, int(testNum.group(1))) 1102 return maxCounter + 1 1103 1104 def getTransformCode(self, asy2psmap=identity()): 1105 with io.StringIO() as rawAsyCode: 1106 if self.transfKeymap: 1107 for key in self.transfKeymap.keys(): 1108 val = self.transfKeymap[key] 1109 1110 writeval = list(reversed(val)) 1111 # need to map all transforms in a list if there is any non-identity 1112 # unfortunately, have to check all transformations in the list. 1113 while not all(checktransf == identity() for checktransf in writeval) and writeval: 1114 transf = writeval.pop() 1115 if transf.deleted: 1116 rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap)) + '\n//') 1117 if transf == identity() and not transf.deleted: 1118 rawAsyCode.write(xasyItem.setKeyAloneFormatStr.format(key)) 1119 else: 1120 rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap))) 1121 rawAsyCode.write('\n') 1122 result = rawAsyCode.getvalue() 1123 return result 1124 1125 def findNonIdKeys(self): 1126 return {key for key in self.transfKeymap if not all(transf == identity() for transf in self.transfKeymap[key]) } 1127 1128 def getObjectCode(self, asy2psmap=identity()): 1129 numeric=r'([-+]?(?:(?:\d*\.\d+)|(?:\d+\.?)))' 1130 rSize=re.compile("size\(\("+numeric+","+numeric+","+numeric+"," 1131 +numeric+","+numeric+","+numeric+"\)\); "+ 1132 self.resizeComment) 1133 1134 newScript = self.getReplacedKeysCode(self.findNonIdKeys()) 1135 with io.StringIO() as rawAsyCode: 1136 for line in newScript.splitlines(): 1137 if(rSize.match(line)): 1138 self.asySize=line.rstrip()+'\n' 1139 else: 1140 raw_line = line.rstrip().replace('\t', ' ' * 4) 1141 rawAsyCode.write(raw_line + '\n') 1142 1143 self.updatedCode = rawAsyCode.getvalue() 1144 return self.updatedCode 1145 1146 def setScript(self, script): 1147 """Sets the content of the script item.""" 1148 self.script = script 1149 self.updateCode() 1150 1151 def setKeyPrefix(self, newPrefix=''): 1152 self.keyPrefix = newPrefix 1153 self.updatedPrefix = False 1154 1155 def getReplacedKeysCode(self, key2replace: set=None) -> str: 1156 keylist = {} 1157 prefix = '' 1158 1159 key2replaceSet = self.unsetKeys if key2replace is None else \ 1160 self.unsetKeys & key2replace 1161 1162 linenum2key = {} 1163 1164 if not self.updatedPrefix: 1165 prefix = self.keyPrefix 1166 1167 for key in key2replaceSet: 1168 actualkey = key 1169 1170 key = key.split(':')[0] 1171 raw_parsed = xu.tryParseKey(key) 1172 assert raw_parsed is not None 1173 line, col = [int(val) for val in raw_parsed.groups()] 1174 if line not in keylist: 1175 keylist[line] = set() 1176 keylist[line].add(col) 1177 linenum2key[(line, col)] = actualkey 1178 self.unsetKeys.discard(key) 1179 1180 1181 raw_code_lines = self.script.splitlines() 1182 with io.StringIO() as raw_str: 1183 for i_0 in range(len(raw_code_lines)): 1184 i = i_0 + self.lineOffset 1185 curr_str = raw_code_lines[i_0] 1186 if i + 1 in keylist.keys(): 1187 # this case, we have a key. 1188 with io.StringIO() as raw_line: 1189 for j in range(len(curr_str)): 1190 raw_line.write(curr_str[j]) 1191 if j + 1 in keylist[i + 1]: 1192 # at this point, replace keys with xkey 1193 raw_line.write('KEY="{0:s}",'.format(linenum2key[(i + 1, j + 1)])) 1194 self.userKeys.add(linenum2key[(i + 1, j + 1)]) 1195 curr_str = raw_line.getvalue() 1196 # else, skip and just write the line. 1197 raw_str.write(curr_str + '\n') 1198 return raw_str.getvalue() 1199 1200 def getUnusedKey(self, oldkey) -> str: 1201 baseCounter = 0 1202 newKey = oldkey 1203 while newKey in self.userKeys: 1204 newKey = oldkey + ':' + str(baseCounter) 1205 baseCounter += 1 1206 return newKey 1207 1208 def asyfy(self, keyOnly=False): 1209 """Generate the list of images described by this object and adjust the length of the transform list.""" 1210 super().asyfy() 1211 1212 # Id --> Transf --> asy-fied --> Transf 1213 # Transf should keep the original, raw transformation 1214 # but for all new drawn objects - assign Id as transform. 1215 1216 if self.scriptAsyfied: 1217 return 1218 1219 keyCount = {} 1220 settedKey = {} 1221 1222 for im in self.imageList: 1223 if im.key in self.unsetKeys and im.key not in settedKey.keys(): 1224 oldkey = im.key 1225 self.unsetKeys.remove(im.key) 1226 im.key = self.getUnusedKey(im.key) 1227 self.unsetKeys.add(im.key) 1228 1229 for drawobj in self.drawObjectsMap[oldkey]: 1230 drawobj.key = im.key 1231 1232 self.drawObjectsMap[im.key] = self.drawObjectsMap[oldkey] 1233 self.drawObjectsMap.pop(oldkey) 1234 1235 settedKey[oldkey] = im.key 1236 elif im.key in settedKey.keys(): 1237 im.key = settedKey[im.key] 1238 1239 if im.key not in keyCount.keys(): 1240 keyCount[im.key] = 1 1241 else: 1242 keyCount[im.key] += 1 1243 1244 if im.key not in self.key2imagemap.keys(): 1245 self.key2imagemap[im.key] = [im] 1246 else: 1247 self.key2imagemap[im.key].append(im) 1248 1249 1250 1251 for key in keyCount: 1252 if key not in self.transfKeymap.keys(): 1253 self.transfKeymap[key] = [identity()] * keyCount[key] 1254 else: 1255 while len(self.transfKeymap[key]) < keyCount[key]: 1256 self.transfKeymap[key].append(identity()) 1257 1258 # while len(self.transfKeymap[key]) > keyCount[key]: 1259 # self.transfKeymap[key].pop() 1260 1261 # change of basis 1262 for keylist in self.transfKeymap.values(): 1263 for i in range(len(keylist)): 1264 if keylist[i] != identity(): 1265 keylist[i] = self.asy2psmap * keylist[i] * self.asy2psmap.inverted() 1266 1267 self.updateCode() 1268 self.scriptAsyfied = True 1269 1270 def generateDrawObjects(self, forceUpdate=False): 1271 self.asyfy(forceUpdate) 1272 return self.drawObjects 1273 1274 def __str__(self): 1275 """Return a string describing this script""" 1276 retVal = "xasyScript\n\tTransforms:\n" 1277 for xform in self.transform: 1278 retVal += "\t" + str(xform) + "\n" 1279 retVal += "\tCode Ommitted" 1280 return retVal 1281 1282 1283class DrawObject(Qc.QObject): 1284 def __init__(self, drawObject, mainCanvas=None, transform=identity(), btmRightanchor=Qc.QPointF(0, 0), 1285 drawOrder=(-1, -1), pen=None, key=None, parentObj=None, fill=False, keyIndex=0): 1286 super().__init__() 1287 self.drawObject = drawObject 1288 self.mainCanvas = mainCanvas 1289 self.pTransform = transform 1290 self.baseTransform = transform 1291 self.drawOrder = drawOrder 1292 self.btmRightAnchor = btmRightanchor 1293 self.originalObj = parentObj 1294 self.explicitBoundingBox = None 1295 self.useCanvasTransformation = False 1296 self.key = key 1297 self.cachedSvgImg = None 1298 self.cachedDPI = None 1299 self.maxDPI=0 1300 self.keyIndex = keyIndex 1301 self.pen = pen 1302 self.fill = fill 1303 1304 def getInteriorScrTransform(self, transform): 1305 """Generates the transform with Interior transform applied beforehand.""" 1306 if isinstance(transform, Qg.QTransform): 1307 transform = asyTransform.fromQTransform(transform) 1308 return self.transform * transform * self.baseTransform.inverted() 1309 1310 @property 1311 def transform(self): 1312 return self.pTransform 1313 1314 @transform.setter 1315 def transform(self, value): 1316 self.pTransform = value 1317 1318 def setBoundingBoxPs(self, bbox): 1319 l, b, r, t = bbox 1320 self.explicitBoundingBox = Qc.QRectF(Qc.QPointF(l, b), Qc.QPointF(r, t)) 1321 # self.explicitBoundingBox = Qc.QRectF(0, 0, 100, 100) 1322 1323 @property 1324 def boundingBox(self): 1325 if self.explicitBoundingBox is not None: 1326 testBbox = self.explicitBoundingBox 1327 else: 1328 if isinstance(self.drawObject, Qg.QImage): 1329 testBbox = self.drawObject.rect() 1330 testBbox.moveTo(self.btmRightAnchor.toPoint()) 1331 elif isinstance(self.drawObject, Qg.QPainterPath): 1332 testBbox = self.baseTransform.toQTransform().mapRect(self.drawObject.boundingRect()) 1333 else: 1334 raise TypeError('drawObject is not a valid type!') 1335 pointList = [self.getScreenTransform().toQTransform().map(point) for point in [ 1336 testBbox.topLeft(), testBbox.topRight(), testBbox.bottomLeft(), testBbox.bottomRight() 1337 ]] 1338 return Qg.QPolygonF(pointList).boundingRect() 1339 1340 @property 1341 def localBoundingBox(self): 1342 testBbox = self.drawObject.rect() 1343 testBbox.moveTo(self.btmRightAnchor.toPoint()) 1344 return testBbox 1345 1346 def getScreenTransform(self): 1347 scrTransf = self.baseTransform.toQTransform().inverted()[0] * self.pTransform.toQTransform() 1348 return asyTransform.fromQTransform(scrTransf) 1349 1350 def draw(self, additionalTransformation=None, applyReverse=False, canvas: Qg.QPainter=None, dpi=300): 1351 if canvas is None: 1352 canvas = self.mainCanvas 1353 if additionalTransformation is None: 1354 additionalTransformation = Qg.QTransform() 1355 1356 assert canvas.isActive() 1357 1358 canvas.save() 1359 if self.pen: 1360 oldPen = Qg.QPen(canvas.pen()) 1361 canvas.setPen(self.pen.toQPen()) 1362 else: 1363 oldPen = Qg.QPen() 1364 1365 if not applyReverse: 1366 canvas.setTransform(additionalTransformation, True) 1367 canvas.setTransform(self.transform.toQTransform(), True) 1368 else: 1369 canvas.setTransform(self.transform.toQTransform(), True) 1370 canvas.setTransform(additionalTransformation, True) 1371 1372 canvas.setTransform(self.baseTransform.toQTransform().inverted()[0], True) 1373 1374 if isinstance(self.drawObject, Qg.QImage): 1375 canvas.drawImage(self.explicitBoundingBox, self.drawObject) 1376 elif isinstance(self.drawObject, xs.SvgObject): 1377 threshold = 1.44 1378 1379 if self.cachedDPI is None or self.cachedSvgImg is None \ 1380 or dpi > self.maxDPI*threshold: 1381 self.cachedDPI = dpi 1382 self.maxDPI=max(self.maxDPI,dpi) 1383 self.cachedSvgImg = self.drawObject.render(dpi) 1384 1385 canvas.drawImage(self.explicitBoundingBox, self.cachedSvgImg) 1386 elif isinstance(self.drawObject, Qs.QSvgRenderer): 1387 self.drawObject.render(canvas, self.explicitBoundingBox) 1388 elif isinstance(self.drawObject, Qg.QPainterPath): 1389 path = self.baseTransform.toQTransform().map(self.drawObject) 1390 if self.fill: 1391 if self.pen: 1392 brush = self.pen.toQPen().brush() 1393 else: 1394 brush = Qg.QBrush() 1395 canvas.fillPath(path, brush) 1396 else: 1397 canvas.drawPath(path) 1398 1399 if self.pen: 1400 canvas.setPen(oldPen) 1401 canvas.restore() 1402 1403 def collide(self, coords, canvasCoordinates=True): 1404 # modify these values to grow/shrink the fuzz. 1405 fuzzTolerance = 1 1406 marginGrowth = 1 1407 leftMargin = marginGrowth if self.boundingBox.width() < fuzzTolerance else 0 1408 topMargin = marginGrowth if self.boundingBox.height() < fuzzTolerance else 0 1409 1410 newMargin = Qc.QMarginsF(leftMargin, topMargin, leftMargin, topMargin) 1411 return self.boundingBox.marginsAdded(newMargin).contains(coords) 1412 1413 def getID(self): 1414 return self.originalObj 1415