diff options
Diffstat (limited to 'graphics/asymptote/GUI/xasy2asy.py')
-rwxr-xr-x | graphics/asymptote/GUI/xasy2asy.py | 1407 |
1 files changed, 1407 insertions, 0 deletions
diff --git a/graphics/asymptote/GUI/xasy2asy.py b/graphics/asymptote/GUI/xasy2asy.py new file mode 100755 index 0000000000..2df6c3417a --- /dev/null +++ b/graphics/asymptote/GUI/xasy2asy.py @@ -0,0 +1,1407 @@ +#!/usr/bin/env python3 + +########################################################################### +# +# xasy2asy provides a Python interface to Asymptote +# +# +# Authors: Orest Shardt, Supakorn Rassameemasmuang, and John C. Bowman +# +########################################################################### + +import PyQt5.QtWidgets as Qw +import PyQt5.QtGui as Qg +import PyQt5.QtCore as Qc +import PyQt5.QtSvg as Qs + +import numpy as np + +import sys +import os +import signal +import threading +import string +import subprocess +import tempfile +import re +import shutil +import copy +import queue +import io +import atexit +import DebugFlags + +import xasyUtils as xu +import xasyArgs as xa +import xasyOptions as xo +import xasySvg as xs + +class AsymptoteEngine: + xasy=chr(4)+"\n" + + def __init__(self, path=None, keepFiles=DebugFlags.keepFiles, keepDefaultArgs=True): + if path is None: + path = xa.getArgs().asypath + if path is None: + opt = xo.BasicConfigs.defaultOpt + opt.load() + path = opt['asyPath'] + + if sys.platform[:3] == 'win': + rx = 0 # stdin + wa = 2 # stderr + else: + rx, wx = os.pipe() + ra, wa = os.pipe() + os.set_inheritable(rx, True) + os.set_inheritable(wx, True) + os.set_inheritable(ra, True) + os.set_inheritable(wa, True) + self.ostream = os.fdopen(wx, 'w') + self.istream = os.fdopen(ra, 'r') + + self.keepFiles = keepFiles + if sys.platform[:3] == 'win': + self.tmpdir = tempfile.mkdtemp(prefix='xasyData_',dir='./')+'/' + else: + self.tmpdir = tempfile.mkdtemp(prefix='xasyData_')+os.sep + + self.args=['-xasy', '-noV', '-q', '-inpipe=' + str(rx), '-outpipe=' + str(wa), '-o', self.tmpdir] + + self.asyPath = path + self.asyProcess = None + + def start(self): + try: + if sys.platform[:3] == 'win': + self.asyProcess = subprocess.Popen([self.asyPath] + self.args, + stdin=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + self.ostream = self.asyProcess.stdin + self.istream = self.asyProcess.stderr + else: + self.asyProcess = subprocess.Popen([self.asyPath] + self.args,close_fds=False) + finally: + atexit.register(self.cleanup) + + def wait(self): + if self.asyProcess.returncode is not None: + return + else: + return self.asyProcess.wait() + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + self.wait() + + @property + def tempDirName(self): + return self.tmpdir + + def startThenStop(self): + self.start() + self.stop() + self.wait() + + @property + def active(self): + if self.asyProcess is None: + return False + return self.asyProcess.returncode is None + + def stop(self): + if self.active: + self.asyProcess.kill() + + def cleanup(self): + self.stop() + if self.asyProcess is not None: + self.asyProcess.wait() + if not self.keepFiles: + if os.path.isdir(self.tempDirName + os.sep): + shutil.rmtree(self.tempDirName, ignore_errors=True) + +class asyTransform(Qc.QObject): + """A python implementation of an asy transform""" + + def __init__(self, initTuple, delete=False): + """Initialize the transform with a 6 entry tuple""" + super().__init__() + if isinstance(initTuple, (tuple, list)) and len(initTuple) == 6: + self.t = initTuple + self.x, self.y, self.xx, self.xy, self.yx, self.yy = initTuple + self._deleted = delete + else: + raise TypeError("Illegal initializer for asyTransform") + + @property + def deleted(self): + return self._deleted + + @deleted.setter + def deleted(self, value): + self._deleted = value + + @classmethod + def zero(cls): + return asyTransform((0, 0, 0, 0, 0, 0)) + + @classmethod + def fromQTransform(cls, transform: Qg.QTransform): + tx, ty = transform.dx(), transform.dy() + xx, xy, yx, yy = transform.m11(), transform.m21(), transform.m12(), transform.m22() + + return asyTransform((tx, ty, xx, xy, yx, yy)) + + @classmethod + def fromNumpyMatrix(cls, transform: np.ndarray): + assert transform.shape == (3, 3) + + tx = transform[0, 2] + ty = transform[1, 2] + + xx, xy, yx, yy = transform[0:2, 0:2].ravel().tolist()[0] + + return asyTransform((tx, ty, xx, xy, yx, yy)) + + def getRawCode(self): + return xu.tuple2StrWOspaces(self.t) + + def getCode(self, asy2psmap=None): + """Obtain the asy code that represents this transform""" + if asy2psmap is None: + asy2psmap = asyTransform((0, 0, 1, 0, 0, 1)) + if self.deleted: + return 'zeroTransform' + else: + return (asy2psmap.inverted() * self * asy2psmap).getRawCode() + + def scale(self, s): + return asyTransform((0, 0, s, 0, 0, s)) * self + + def toQTransform(self): + return Qg.QTransform(self.xx, self.yx, self.xy, self.yy, self.x, self.y) + + def __str__(self): + """Equivalent functionality to getCode(). It allows the expression str(asyTransform) to be meaningful.""" + return self.getCode() + + def isIdentity(self): + return self == identity() + + def inverted(self): + return asyTransform.fromQTransform(self.toQTransform().inverted()[0]) + + def __eq__(self, other): + return list(self.t) == list(other.t) + + def __mul__(self, other): + """Define multiplication of transforms as composition.""" + if isinstance(other, tuple): + if len(other) == 6: + return self * asyTransform(other) + elif len(other) == 2: + return ((self.t[0] + self.t[2] * other[0] + self.t[3] * other[1]), + (self.t[1] + self.t[4] * other[0] + self.t[5] * other[1])) + else: + raise Exception("Illegal multiplier of {:s}".format(str(type(other)))) + elif isinstance(other, asyTransform): + result = asyTransform((0, 0, 0, 0, 0, 0)) + result.x = self.x + self.xx * other.x + self.xy * other.y + result.y = self.y + self.yx * other.x + self.yy * other.y + result.xx = self.xx * other.xx + self.xy * other.yx + result.xy = self.xx * other.xy + self.xy * other.yy + result.yx = self.yx * other.xx + self.yy * other.yx + result.yy = self.yx * other.xy + self.yy * other.yy + result.t = (result.x, result.y, result.xx, result.xy, result.yx, result.yy) + return result + elif isinstance(other, str): + if other != 'cycle': + raise TypeError + else: + return 'cycle' + else: + raise TypeError("Illegal multiplier of {:s}".format(str(type(other)))) + + +def identity(): + return asyTransform((0, 0, 1, 0, 0, 1)) + +def yflip(): + return asyTransform((0, 0, 1, 0, 0, -1)) + +class asyObj(Qc.QObject): + """A base class for asy objects: an item represented by asymptote code.""" + def __init__(self): + """Initialize the object""" + super().__init__() + self.asyCode = '' + + def updateCode(self, ps2asymap=identity()): + """Update the object's code: should be overriden.""" + raise NotImplementedError + + def getCode(self, ps2asymap=identity()): + """Return the code describing the object""" + self.updateCode(ps2asymap) + return self.asyCode + + +class asyPen(asyObj): + """A python wrapper for an asymptote pen""" + @staticmethod + def getColorFromQColor(color): + return color.redF(), color.greenF(), color.blueF() + + @staticmethod + def convertToQColor(color): + r, g, b = color + return Qg.QColor.fromRgbF(r, g, b) + + @classmethod + def fromAsyPen(cls, pen): + assert isinstance(pen, cls) + return cls(asyengine=pen._asyengine, color=pen.color, width=pen.width, pen_options=pen.options) + + def __init__(self, asyengine=None, color=(0, 0, 0), width=0.5, pen_options=""): + """Initialize the pen""" + asyObj.__init__(self) + self.color = (0, 0, 0) + self.options = pen_options + self.width = width + self._asyengine = asyengine + self._deferAsyfy = False + if pen_options: + self._deferAsyfy = True + self.updateCode() + self.setColor(color) + + @property + def asyEngine(self): + return self._asyengine + + @asyEngine.setter + def asyEngine(self, value): + self._asyengine = value + + def updateCode(self, asy2psmap=identity()): + """Generate the pen's code""" + if self._deferAsyfy: + self.computeColor() + self.asyCode = 'rgb({:g},{:g},{:g})+{:s}'.format(self.color[0], self.color[1], self.color[2], str(self.width)) + if len(self.options) > 0: + self.asyCode = self.asyCode + '+' + self.options + + def setWidth(self, newWidth): + """Set the pen's width""" + self.width = newWidth + self.updateCode() + + def setColor(self, color): + """Set the pen's color""" + if isinstance(color, tuple) and len(color) == 3: + self.color = color + else: + self.color = (0, 0, 0) + self.updateCode() + + def setColorFromQColor(self, color): + self.setColor(asyPen.getColorFromQColor(color)) + + def computeColor(self): + """Find out the color of an arbitrary asymptote pen.""" + assert isinstance(self.asyEngine, AsymptoteEngine) + assert self.asyEngine.active + + fout = self.asyEngine.ostream + fin = self.asyEngine.istream + + fout.write("pen p=" + self.getCode() + ';\n') + fout.write("write(_outpipe,colorspace(p),newl);\n") + fout.write("write(_outpipe,colors(p));\n") + fout.write("flush(_outpipe);\n") + fout.write(self.asyEngine.xasy) + fout.flush() + + colorspace = fin.readline() + if colorspace.find("cmyk") != -1: + lines = fin.readline() + fin.readline() + fin.readline() + fin.readline() + parts = lines.split() + c, m, y, k = eval(parts[0]), eval(parts[1]), eval(parts[2]), eval(parts[3]) + k = 1 - k + r, g, b = ((1 - c) * k, (1 - m) * k, (1 - y) * k) + elif colorspace.find("rgb") != -1: + lines = fin.readline() + fin.readline() + fin.readline() + parts = lines.split() + r, g, b = eval(parts[0]), eval(parts[1]), eval(parts[2]) + elif colorspace.find("gray") != -1: + lines = fin.readline() + parts = lines.split() + r = g = b = eval(parts[0]) + else: + raise ChildProcessError('Asymptote error.') + self.color = (r, g, b) + self._deferAsyfy = False + + def tkColor(self): + """Return the tk version of the pen's color""" + self.computeColor() + return '#{}'.format("".join(["{:02x}".format(min(int(256 * a), 255)) for a in self.color])) + + def toQPen(self): + if self._deferAsyfy: + self.computeColor() + newPen = Qg.QPen() + newPen.setColor(asyPen.convertToQColor(self.color)) + newPen.setWidthF(self.width) + + return newPen + + +class asyPath(asyObj): + """A python wrapper for an asymptote path""" + + def __init__(self, asyengine: AsymptoteEngine=None, forceCurve=False): + """Initialize the path to be an empty path: a path with no nodes, control points, or links.""" + super().__init__() + self.nodeSet = [] + self.linkSet = [] + self.forceCurve = forceCurve + self.controlSet = [] + self.computed = False + self.asyengine = asyengine + + @classmethod + def fromPath(cls, oldPath): + newObj = asyPath(None) + newObj.nodeSet = copy.copy(oldPath.nodeSet) + newObj.linkSet = copy.copy(oldPath.linkSet) + newObj.controlSet = copy.deepcopy(oldPath.controlSet) + newObj.computed = oldPath.computed + newObj.asyengine = oldPath.asyengine + + return newObj + + @classmethod + def fromBezierPoints(cls, pointList: list, engine=None): + if not pointList: + return None + assert isinstance(pointList[0], BezierCurveEditor.BezierPoint) + nodeList = [] + controlList = [] + for point in pointList: + nodeList.append(BezierCurveEditor.QPoint2Tuple(point.point)) + if point.rCtrlPoint is not None: # first + controlList.append([BezierCurveEditor.QPoint2Tuple(point.rCtrlPoint)]) + if point.lCtrlPoint is not None: # last + controlList[-1].append(BezierCurveEditor.QPoint2Tuple(point.lCtrlPoint)) + newPath = asyPath(asyengine=engine) + newPath.initFromControls(nodeList, controlList) + return newPath + + def setInfo(self, path): + self.nodeSet = copy.copy(path.nodeSet) + self.linkSet = copy.copy(path.linkSet) + self.controlSet = copy.deepcopy(path.controlSet) + self.computed = path.computed + + @property + def isEmpty(self): + return len(self.nodeSet) == 0 + + @property + def isDrawable(self): + return len(self.nodeSet) >= 2 + + def toQPainterPath(self) -> Qg.QPainterPath: + return self.toQPainterPathCurve() if self.containsCurve else self.toQPainterPathLine() + + def toQPainterPathLine(self): + baseX, baseY = self.nodeSet[0] + painterPath = Qg.QPainterPath(Qc.QPointF(baseX, baseY)) + + for pointIndex in range(1, len(self.nodeSet)): + node = self.nodeSet[pointIndex] + if self.nodeSet[pointIndex] == 'cycle': + node = self.nodeSet[0] + + painterPath.lineTo(*node) + + return painterPath + + + def toQPainterPathCurve(self): + if not self.computed: + self.computeControls() + + baseX, baseY = self.nodeSet[0] + painterPath = Qg.QPainterPath(Qc.QPointF(baseX, baseY)) + + for pointIndex in range(1, len(self.nodeSet)): + node = self.nodeSet[pointIndex] + if self.nodeSet[pointIndex] == 'cycle': + node = self.nodeSet[0] + endPoint = Qc.QPointF(node[0], node[1]) + ctrlPoint1 = Qc.QPointF(self.controlSet[pointIndex-1][0][0], self.controlSet[pointIndex-1][0][1]) + ctrlPoint2 = Qc.QPointF(self.controlSet[pointIndex-1][1][0], self.controlSet[pointIndex-1][1][1]) + + painterPath.cubicTo(ctrlPoint1, ctrlPoint2, endPoint) + return painterPath + + def initFromNodeList(self, nodeSet, linkSet): + """Initialize the path from a set of nodes and link types, "--", "..", or "::" """ + if len(nodeSet) > 0: + self.nodeSet = nodeSet[:] + self.linkSet = linkSet[:] + self.computed = False + + def initFromControls(self, nodeSet, controlSet): + """Initialize the path from nodes and control points""" + self.controlSet = controlSet[:] + self.nodeSet = nodeSet[:] + self.computed = True + + def makeNodeStr(self, node): + """Represent a node as a string""" + if node == 'cycle': + return node + else: + # if really want to, disable this rounding + # shouldn't be to much of a problem since 10e-6 is quite small... + return '({:.6g},{:.6g})'.format(node[0], node[1]) + + def updateCode(self, ps2asymap=identity()): + """Generate the code describing the path""" + # currently at postscript. Convert to asy + asy2psmap = ps2asymap.inverted() + with io.StringIO() as rawAsyCode: + count = 0 + rawAsyCode.write(self.makeNodeStr(asy2psmap * self.nodeSet[0])) + for node in self.nodeSet[1:]: + if not self.computed or count >= len(self.controlSet): + rawAsyCode.write(self.linkSet[count]) + rawAsyCode.write(self.makeNodeStr(asy2psmap * node)) + else: + rawAsyCode.write('..controls ') + rawAsyCode.write(self.makeNodeStr(asy2psmap * self.controlSet[count][0])) + rawAsyCode.write(' and ') + rawAsyCode.write(self.makeNodeStr(asy2psmap * self.controlSet[count][1])) + rawAsyCode.write(".." + self.makeNodeStr(asy2psmap * node)) + count = count + 1 + self.asyCode = rawAsyCode.getvalue() + + @property + def containsCurve(self): + return '..' in self.linkSet or self.forceCurve + + def getNode(self, index): + """Return the requested node""" + return self.nodeSet[index] + + def getLink(self, index): + """Return the requested link""" + return self.linkSet[index] + + def setNode(self, index, newNode): + """Set a node to a new position""" + self.nodeSet[index] = newNode + + def moveNode(self, index, offset): + """Translate a node""" + if self.nodeSet[index] != "cycle": + self.nodeSet[index] = (self.nodeSet[index][0] + offset[0], self.nodeSet[index][1] + offset[1]) + + def setLink(self, index, ltype): + """Change the specified link""" + self.linkSet[index] = ltype + + def addNode(self, point, ltype): + """Add a node to the end of a path""" + self.nodeSet.append(point) + if len(self.nodeSet) != 1: + self.linkSet.append(ltype) + if self.computed: + self.computeControls() + + def insertNode(self, index, point, ltype=".."): + """Insert a node, and its corresponding link, at the given index""" + self.nodeSet.insert(index, point) + self.linkSet.insert(index, ltype) + if self.computed: + self.computeControls() + + def setControl(self, index, position): + """Set a control point to a new position""" + self.controlSet[index] = position + + def popNode(self): + if len(self.controlSet) == len(self.nodeSet): + self.controlSet.pop() + self.nodeSet.pop() + self.linkSet.pop() + + def moveControl(self, index, offset): + """Translate a control point""" + self.controlSet[index] = (self.controlSet[index][0] + offset[0], self.controlSet[index][1] + offset[1]) + + def computeControls(self): + """Evaluate the code of the path to obtain its control points""" + # For now, if no asymptote process is given spawns a new one. + # Only happens if asyengine is None. + if self.asyengine is not None: + assert isinstance(self.asyengine, AsymptoteEngine) + assert self.asyengine.active + asy = self.asyengine + startUp = False + else: + startUp = True + asy = AsymptoteEngine() + asy.start() + + fout = asy.ostream + fin = asy.istream + + fout.write("path p=" + self.getCode() + ';\n') + fout.write("write(_outpipe,length(p),newl);\n") + fout.write("write(_outpipe,unstraighten(p),endl);\n") + fout.write(asy.xasy) + fout.flush() + + lengthStr = fin.readline() + pathSegments = eval(lengthStr.split()[-1]) + pathStrLines = [] + for i in range(pathSegments + 1): + line = fin.readline() + line = line.replace("\n", "") + pathStrLines.append(line) + oneLiner = "".join(pathStrLines).replace(" ", "") + splitList = oneLiner.split("..") + nodes = [a for a in splitList if a.find("controls") == -1] + self.nodeSet = [] + for a in nodes: + if a == 'cycle': + self.nodeSet.append(a) + else: + self.nodeSet.append(eval(a)) + controls = [a.replace("controls", "").split("and") for a in splitList if a.find("controls") != -1] + self.controlSet = [[eval(a[0]), eval(a[1])] for a in controls] + self.computed = True + + if startUp: + asy.stop() + +class asyLabel(asyObj): + """A python wrapper for an asy label""" + + def __init__(self, text="", location=(0, 0), pen=None, align=None, fontSize:int=None): + """Initialize the label with the given test, location, and pen""" + asyObj.__init__(self) + self.align = align + self.pen = pen + self.fontSize = fontSize + if align is None: + self.align = 'SE' + if pen is None: + self.pen = asyPen() + self.text = text + self.location = location + + def updateCode(self, asy2psmap=identity()): + """Generate the code describing the label""" + newLoc = asy2psmap.inverted() * self.location + locStr = xu.tuple2StrWOspaces(newLoc) + self.asyCode = 'Label("{0}",{1},p={2}{4},align={3})'.format(self.text, locStr, self.pen.getCode(), self.align, + self.getFontSizeText()) + + def getFontSizeText(self): + if self.fontSize is not None: + return '+fontsize({:.6g})'.format(self.fontSize) + else: + return '' + + def setText(self, text): + """Set the label's text""" + self.text = text + self.updateCode() + + def setPen(self, pen): + """Set the label's pen""" + self.pen = pen + self.updateCode() + + def moveTo(self, newl): + """Translate the label's location""" + self.location = newl + + +class asyImage: + """A structure containing an image and its format, bbox, and IDTag""" + def __init__(self, image, format, bbox, transfKey=None, keyIndex=0): + self.image = image + self.format = format + self.bbox = bbox + self.IDTag = None + self.key = transfKey + self.keyIndex = keyIndex + +class xasyItem(Qc.QObject): + """A base class for items in the xasy GUI""" + mapString = 'xmap' + setKeyFormatStr = string.Template('$map("{:s}",{:s});').substitute(map=mapString) + setKeyAloneFormatStr = string.Template('$map("{:s}");').substitute(map=mapString) + resizeComment="// Resize to initial xasy transform" + asySize="" + def __init__(self, canvas=None, asyengine=None): + """Initialize the item to an empty item""" + super().__init__() + self.transfKeymap = {} # the new keymap. + # should be a dictionary to a list... + self.asyCode = '' + self.imageList = [] + self.IDTag = None + self.asyfied = False + self.onCanvas = canvas + self.keyBuffer = None + self._asyengine = asyengine + self.drawObjects = [] + self.drawObjectsMap = {} + self.setKeyed = True + self.unsetKeys = set() + self.userKeys = set() + self.lineOffset = 0 + self.imageHandleQueue = queue.Queue() + + def updateCode(self, ps2asymap=identity()): + """Update the item's code: to be overriden""" + with io.StringIO() as rawCode: + transfCode = self.getTransformCode() + objCode = self.getObjectCode() + + rawCode.write(transfCode) + rawCode.write(objCode) + self.asyCode = rawCode.getvalue() + + return len(transfCode.splitlines()), len(objCode.splitlines()) + + @property + def asyengine(self): + return self._asyengine + + @asyengine.setter + def asyengine(self, value): + self._asyengine = value + + def getCode(self, ps2asymap=identity()): + """Return the code describing the item""" + self.updateCode(ps2asymap) + return self.asyCode + + def getTransformCode(self, asy2psmap=identity()): + raise NotImplementedError + + def getObjectCode(self, asy2psmap=identity()): + raise NotImplementedError + + def generateDrawObjects(self): + raise NotImplementedError + + def handleImageReception(self, file, fileformat, bbox, count, key=None, localCount=0, containsClip=False): + """Receive an image from an asy deconstruction. It replaces the default n asyProcess.""" + # image = Image.open(file).transpose(Image.FLIP_TOP_BOTTOM) + if fileformat == 'png': + image = Qg.QImage(file) + elif fileformat == 'svg': + if containsClip: + image = xs.SvgObject(file) + else: + image = Qs.QSvgRenderer(file) + assert image.isValid() + else: + raise Exception('Format not supported!') + self.imageList.append(asyImage(image, fileformat, bbox, transfKey=key, keyIndex=localCount)) + if self.onCanvas is not None: + # self.imageList[-1].iqt = ImageTk.PhotoImage(image) + currImage = self.imageList[-1] + currImage.iqt = image + currImage.originalImage = image + currImage.originalImage.theta = 0.0 + currImage.originalImage.bbox = list(bbox) + currImage.performCanvasTransform = False + + # handle this case if transform is not in the map yet. + # if deleted - set transform to 0, 0, 0, 0, 0 + transfExists = key in self.transfKeymap.keys() + if transfExists: + transfExists = localCount <= len(self.transfKeymap[key]) - 1 + if transfExists: + validKey = not self.transfKeymap[key][localCount].deleted + else: + validKey = False + + if (not transfExists) or validKey: + currImage.IDTag = str(file) + newDrawObj = DrawObject(currImage.iqt, self.onCanvas['canvas'], transform=identity(), + btmRightanchor=Qc.QPointF(bbox[0], bbox[2]), drawOrder=-1, key=key, + parentObj=self, keyIndex=localCount) + newDrawObj.setBoundingBoxPs(bbox) + newDrawObj.setParent(self) + + self.drawObjects.append(newDrawObj) + + if key not in self.drawObjectsMap.keys(): + self.drawObjectsMap[key] = [newDrawObj] + else: + self.drawObjectsMap[key].append(newDrawObj) + return containsClip + def asyfy(self, force=False): + if self.asyengine is None: + return 1 + if self.asyfied and not force: + return + + self.drawObjects = [] + self.drawObjectsMap.clear() + assert isinstance(self.asyengine, AsymptoteEngine) + self.imageList = [] + + self.unsetKeys.clear() + self.userKeys.clear() + + self.imageHandleQueue = queue.Queue() + worker = threading.Thread(target=self.asyfyThread, args=[]) + worker.start() + item = self.imageHandleQueue.get() + while item != (None,) and item[0] != "ERROR": + if item[0] == "OUTPUT": + print(item[1]) + else: + keepFile = self.handleImageReception(*item) + if not DebugFlags.keepFiles and not keepFile: + try: + os.remove(item[0]) + pass + except OSError: + pass + finally: + pass + item = self.imageHandleQueue.get() + # self.imageHandleQueue.task_done() + worker.join() + + def asyfyThread(self): + """Convert the item to a list of images by deconstructing this item's code""" + assert self.asyengine.active + + fout = self.asyengine.ostream + fin = self.asyengine.istream + + self.lineOffset = len(self.getTransformCode().splitlines()) + + fout.write("reset\n") + fout.flush(); + for line in self.getCode().splitlines(): + if DebugFlags.printDeconstTranscript: + print('fout:', line) + fout.write(line+"\n") + fout.write(self.asySize) + fout.write("deconstruct();\n") + fout.write('write(_outpipe,yscale(-1)*currentpicture.calculateTransform(),endl);\n') + fout.write(self.asyengine.xasy) + fout.flush() + + imageInfos = [] # of (box, key) + n = 0 + + keyCounts = {} + + def render(): + for i in range(len(imageInfos)): + box, key, localCount, useClip = imageInfos[i] + l, b, r, t = [float(a) for a in box.split()] + name = "{:s}_{:d}.{:s}".format(self.asyengine.tempDirName, i, fileformat) + + self.imageHandleQueue.put((name, fileformat, (l, -t, r, -b), i, key, localCount, useClip)) + + # key first, box second. + # if key is "Done" + raw_text = fin.readline() + text = "" + if DebugFlags.printDeconstTranscript: + print(raw_text.strip()) + + # template=AsyTempDir+"%d_%d.%s" + fileformat = 'svg' + + while raw_text != "Done\n" and raw_text != "Error\n": +# print(raw_text) + text = fin.readline() # the actual bounding box. + # print('TESTING:', text) + keydata = raw_text.strip().replace('KEY=', '', 1) # key + + clipflag = keydata[-1] == '1' + userkey = keydata[-2] == '1' + keydata = keydata[:-3] + + if not userkey: + self.unsetKeys.add(keydata) # the line and column to replace. + else: + self.userKeys.add(keydata) + +# print(line, col) + + if keydata not in keyCounts.keys(): + keyCounts[keydata] = 0 + + imageInfos.append((text, keydata, keyCounts[keydata], clipflag)) # key-data pair + + # for the next item + keyCounts[keydata] += 1 + + raw_text = fin.readline() + + if DebugFlags.printDeconstTranscript: + print(text.rstrip()) + print(raw_text.rstrip()) + + n += 1 + + if text == "Error\n": + self.imageHandleQueue.put(("ERROR", fin.readline())) + else: + render() + + self.asy2psmap = asyTransform(xu.listize(fin.readline().rstrip(),float)) + self.imageHandleQueue.put((None,)) + self.asyfied = True + +class xasyDrawnItem(xasyItem): + """A base class for GUI items was drawn by the user. It combines a path, a pen, and a transform.""" + + def __init__(self, path, engine, pen=None, transform=identity(), key=None): + """Initialize the item with a path, pen, and transform""" + super().__init__(canvas=None, asyengine=engine) + if pen is None: + pen = asyPen() + self.path = path + self.path.asyengine = engine + self.asyfied = True + self.pen = pen + self._asyengine = engine + self.rawIdentifier = '' + self.transfKey = key + self.transfKeymap = {self.transfKey: [transform]} + + @property + def asyengine(self): + return self._asyengine + + @asyengine.setter + def asyengine(self, value: AsymptoteEngine): + self._asyengine = value + self.path.asyengine = value + + def setKey(self, newKey=None): + transform = self.transfKeymap[self.transfKey][0] + + self.transfKey = newKey + self.transfKeymap = {self.transfKey: [transform]} + + def generateDrawObjects(self, forceUpdate=False): + raise NotImplementedError + + def appendPoint(self, point, link=None): + """Append a point to the path. If the path is cyclic, add this point before the 'cycle' node.""" + if self.path.nodeSet[-1] == 'cycle': + self.path.nodeSet[-1] = point + self.path.nodeSet.append('cycle') + else: + self.path.nodeSet.append(point) + self.path.computed = False + self.asyfied = False + if len(self.path.nodeSet) > 1 and link is not None: + self.path.linkSet.append(link) + + def clearTransform(self): + """Reset the item's transform""" + self.transform = [identity()] + self.asyfied = False + + def removeLastPoint(self): + """Remove the last point in the path. If the path is cyclic, remove the node before the 'cycle' node.""" + if self.path.nodeSet[-1] == 'cycle': + del self.path.nodeSet[-2] + else: + del self.path.nodeSet[-1] + del self.path.linkSet[-1] + self.path.computed = False + self.asyfied = False + + def setLastPoint(self, point): + """Modify the last point in the path. If the path is cyclic, modify the node before the 'cycle' node.""" + if self.path.nodeSet[-1] == 'cycle': + self.path.nodeSet[-2] = point + else: + self.path.nodeSet[-1] = point + self.path.computed = False + self.asyfied = False + + +class xasyShape(xasyDrawnItem): + """An outlined shape drawn on the GUI""" + def __init__(self, path, asyengine, pen=None, transform=identity()): + """Initialize the shape with a path, pen, and transform""" + super().__init__(path=path, engine=asyengine, pen=pen, transform=transform) + + def getObjectCode(self, asy2psmap=identity()): + return 'draw(KEY="{0}",{1},{2});'.format(self.transfKey, self.path.getCode(asy2psmap), self.pen.getCode())+'\n\n' + + def getTransformCode(self, asy2psmap=identity()): + transf = self.transfKeymap[self.transfKey][0] + if transf == identity(): + return '' + else: + return xasyItem.setKeyFormatStr.format(self.transfKey, transf.getCode(asy2psmap))+'\n' + + def generateDrawObjects(self, forceUpdate=False): + if self.path.containsCurve: + self.path.computeControls() + transf = self.transfKeymap[self.transfKey][0] + + newObj = DrawObject(self.path.toQPainterPath(), None, drawOrder=0, transform=transf, pen=self.pen, + key=self.transfKey) + newObj.originalObj = self + newObj.setParent(self) + return [newObj] + + def __str__(self): + """Create a string describing this shape""" + return "xasyShape code:{:s}".format("\n\t".join(self.getCode().splitlines())) + + +class xasyFilledShape(xasyShape): + """A filled shape drawn on the GUI""" + + def __init__(self, path, asyengine, pen=None, transform=identity()): + """Initialize this shape with a path, pen, and transform""" + if path.nodeSet[-1] != 'cycle': + raise Exception("Filled paths must be cyclic") + super().__init__(path, asyengine, pen, transform) + + def getObjectCode(self, asy2psmap=identity()): + return 'fill(KEY="{0}",{1},{2});'.format(self.transfKey, self.path.getCode(asy2psmap), self.pen.getCode())+'\n\n' + + def generateDrawObjects(self, forceUpdate=False): + if self.path.containsCurve: + self.path.computeControls() + newObj = DrawObject(self.path.toQPainterPath(), None, drawOrder=0, transform=self.transfKeymap[self.transfKey][0], + pen=self.pen, key=self.transfKey, fill=True) + newObj.originalObj = self + newObj.setParent(self) + return [newObj] + + def __str__(self): + """Return a string describing this shape""" + return "xasyFilledShape code:{:s}".format("\n\t".join(self.getCode().splitlines())) + + +class xasyText(xasyItem): + """Text created by the GUI""" + + def __init__(self, text, location, asyengine, pen=None, transform=yflip(), key=None, align=None, fontsize:int=None): + """Initialize this item with text, a location, pen, and transform""" + super().__init__(asyengine=asyengine) + if pen is None: + pen = asyPen(asyengine=asyengine) + if pen.asyEngine is None: + pen.asyEngine = asyengine + self.label = asyLabel(text, location, pen, align, fontSize=fontsize) + # self.transform = [transform] + self.transfKey = key + self.transfKeymap = {self.transfKey: [transform]} + self.asyfied = False + self.onCanvas = None + + def setKey(self, newKey=None): + transform = self.transfKeymap[self.transfKey][0] + + self.transfKey = newKey + self.transfKeymap = {self.transfKey: [transform]} + + def getTransformCode(self, asy2psmap=yflip()): + transf = self.transfKeymap[self.transfKey][0] + if transf == yflip(): + # return xasyItem.setKeyAloneFormatStr.format(self.transfKey) + return '' + else: + return xasyItem.setKeyFormatStr.format(self.transfKey, transf.getCode(asy2psmap))+"\n" + + def getObjectCode(self, asy2psmap=yflip()): + return 'label(KEY="{0}",{1});'.format(self.transfKey, self.label.getCode(asy2psmap))+'\n' + + def generateDrawObjects(self, forceUpdate=False): + self.asyfy(forceUpdate) + return self.drawObjects + + def getBoundingBox(self): + self.asyfy() + return self.imageList[0].bbox + + def __str__(self): + return "xasyText code:{:s}".format("\n\t".join(self.getCode().splitlines())) + + +class xasyScript(xasyItem): + """A set of images create from asymptote code. It is always deconstructed.""" + + def __init__(self, canvas, engine, script="", transforms=None, transfKeyMap=None): + """Initialize this script item""" + super().__init__(canvas, asyengine=engine) + if transfKeyMap is not None: + self.transfKeymap = transfKeyMap + else: + self.transfKeymap = {} + + self.script = script + self.key2imagemap = {} + self.namedUnsetKeys = {} + self.keyPrefix = '' + self.scriptAsyfied = False + self.updatedPrefix = True + + def clearTransform(self): + """Reset the transforms for each of the deconstructed images""" + # self.transform = [identity()] * len(self.imageList) + keyCount = {} + + for im in self.imageList: + if im.key not in keyCount.keys(): + keyCount[im.key] = 1 + else: + keyCount[im.key] += 1 + + for key in keyCount: + self.transfKeymap[key] = [identity()] * keyCount[key] + + def getMaxKeyCounter(self): + maxCounter = -1 + for key in self.transfKeymap: + testNum = re.match(r'^x(\d+)$', key) + if testNum is not None: + maxCounter = max(maxCounter, int(testNum.group(1))) + return maxCounter + 1 + + def getTransformCode(self, asy2psmap=identity()): + with io.StringIO() as rawAsyCode: + if self.transfKeymap: + for key in self.transfKeymap.keys(): + val = self.transfKeymap[key] + + writeval = list(reversed(val)) + # need to map all transforms in a list if there is any non-identity + # unfortunately, have to check all transformations in the list. + while not all(checktransf == identity() for checktransf in writeval) and writeval: + transf = writeval.pop() + if transf.deleted: + rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap)) + '\n//') + if transf == identity() and not transf.deleted: + rawAsyCode.write(xasyItem.setKeyAloneFormatStr.format(key)) + else: + rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap))) + rawAsyCode.write('\n') + result = rawAsyCode.getvalue() + return result + + def findNonIdKeys(self): + return {key for key in self.transfKeymap if not all(transf == identity() for transf in self.transfKeymap[key]) } + + def getObjectCode(self, asy2psmap=identity()): + numeric=r'([-+]?(?:(?:\d*\.\d+)|(?:\d+\.?)))' + rSize=re.compile("size\(\("+numeric+","+numeric+","+numeric+"," + +numeric+","+numeric+","+numeric+"\)\); "+ + self.resizeComment) + + newScript = self.getReplacedKeysCode(self.findNonIdKeys()) + with io.StringIO() as rawAsyCode: + for line in newScript.splitlines(): + if(rSize.match(line)): + self.asySize=line.rstrip()+'\n' + else: + raw_line = line.rstrip().replace('\t', ' ' * 4) + rawAsyCode.write(raw_line + '\n') + + self.updatedCode = rawAsyCode.getvalue() + return self.updatedCode + + def setScript(self, script): + """Sets the content of the script item.""" + self.script = script + self.updateCode() + + def setKeyPrefix(self, newPrefix=''): + self.keyPrefix = newPrefix + self.updatedPrefix = False + + def getReplacedKeysCode(self, key2replace: set=None) -> str: + keylist = {} + prefix = '' + + key2replaceSet = self.unsetKeys if key2replace is None else \ + self.unsetKeys & key2replace + + linenum2key = {} + + if not self.updatedPrefix: + prefix = self.keyPrefix + + for key in key2replaceSet: + actualkey = key + + key = key.split(':')[0] + raw_parsed = xu.tryParseKey(key) + assert raw_parsed is not None + line, col = [int(val) for val in raw_parsed.groups()] + if line not in keylist: + keylist[line] = set() + keylist[line].add(col) + linenum2key[(line, col)] = actualkey + self.unsetKeys.discard(key) + + + raw_code_lines = self.script.splitlines() + with io.StringIO() as raw_str: + for i_0 in range(len(raw_code_lines)): + i = i_0 + self.lineOffset + curr_str = raw_code_lines[i_0] + if i + 1 in keylist.keys(): + # this case, we have a key. + with io.StringIO() as raw_line: + for j in range(len(curr_str)): + raw_line.write(curr_str[j]) + if j + 1 in keylist[i + 1]: + # at this point, replace keys with xkey + raw_line.write('KEY="{0:s}",'.format(linenum2key[(i + 1, j + 1)])) + self.userKeys.add(linenum2key[(i + 1, j + 1)]) + curr_str = raw_line.getvalue() + # else, skip and just write the line. + raw_str.write(curr_str + '\n') + return raw_str.getvalue() + + def getUnusedKey(self, oldkey) -> str: + baseCounter = 0 + newKey = oldkey + while newKey in self.userKeys: + newKey = oldkey + ':' + str(baseCounter) + baseCounter += 1 + return newKey + + def asyfy(self, keyOnly=False): + """Generate the list of images described by this object and adjust the length of the transform list.""" + super().asyfy() + + # Id --> Transf --> asy-fied --> Transf + # Transf should keep the original, raw transformation + # but for all new drawn objects - assign Id as transform. + + if self.scriptAsyfied: + return + + keyCount = {} + settedKey = {} + + for im in self.imageList: + if im.key in self.unsetKeys and im.key not in settedKey.keys(): + oldkey = im.key + self.unsetKeys.remove(im.key) + im.key = self.getUnusedKey(im.key) + self.unsetKeys.add(im.key) + + for drawobj in self.drawObjectsMap[oldkey]: + drawobj.key = im.key + + self.drawObjectsMap[im.key] = self.drawObjectsMap[oldkey] + self.drawObjectsMap.pop(oldkey) + + settedKey[oldkey] = im.key + elif im.key in settedKey.keys(): + im.key = settedKey[im.key] + + if im.key not in keyCount.keys(): + keyCount[im.key] = 1 + else: + keyCount[im.key] += 1 + + if im.key not in self.key2imagemap.keys(): + self.key2imagemap[im.key] = [im] + else: + self.key2imagemap[im.key].append(im) + + + + for key in keyCount: + if key not in self.transfKeymap.keys(): + self.transfKeymap[key] = [identity()] * keyCount[key] + else: + while len(self.transfKeymap[key]) < keyCount[key]: + self.transfKeymap[key].append(identity()) + + # while len(self.transfKeymap[key]) > keyCount[key]: + # self.transfKeymap[key].pop() + + # change of basis + for keylist in self.transfKeymap.values(): + for i in range(len(keylist)): + if keylist[i] != identity(): + keylist[i] = self.asy2psmap * keylist[i] * self.asy2psmap.inverted() + + self.updateCode() + self.scriptAsyfied = True + + def generateDrawObjects(self, forceUpdate=False): + self.asyfy(forceUpdate) + return self.drawObjects + + def __str__(self): + """Return a string describing this script""" + retVal = "xasyScript\n\tTransforms:\n" + for xform in self.transform: + retVal += "\t" + str(xform) + "\n" + retVal += "\tCode Ommitted" + return retVal + + +class DrawObject(Qc.QObject): + def __init__(self, drawObject, mainCanvas=None, transform=identity(), btmRightanchor=Qc.QPointF(0, 0), + drawOrder=(-1, -1), pen=None, key=None, parentObj=None, fill=False, keyIndex=0): + super().__init__() + self.drawObject = drawObject + self.mainCanvas = mainCanvas + self.pTransform = transform + self.baseTransform = transform + self.drawOrder = drawOrder + self.btmRightAnchor = btmRightanchor + self.originalObj = parentObj + self.explicitBoundingBox = None + self.useCanvasTransformation = False + self.key = key + self.cachedSvgImg = None + self.cachedDPI = None + self.maxDPI=0 + self.keyIndex = keyIndex + self.pen = pen + self.fill = fill + + def getInteriorScrTransform(self, transform): + """Generates the transform with Interior transform applied beforehand.""" + if isinstance(transform, Qg.QTransform): + transform = asyTransform.fromQTransform(transform) + return self.transform * transform * self.baseTransform.inverted() + + @property + def transform(self): + return self.pTransform + + @transform.setter + def transform(self, value): + self.pTransform = value + + def setBoundingBoxPs(self, bbox): + l, b, r, t = bbox + self.explicitBoundingBox = Qc.QRectF(Qc.QPointF(l, b), Qc.QPointF(r, t)) + # self.explicitBoundingBox = Qc.QRectF(0, 0, 100, 100) + + @property + def boundingBox(self): + if self.explicitBoundingBox is not None: + testBbox = self.explicitBoundingBox + else: + if isinstance(self.drawObject, Qg.QImage): + testBbox = self.drawObject.rect() + testBbox.moveTo(self.btmRightAnchor.toPoint()) + elif isinstance(self.drawObject, Qg.QPainterPath): + testBbox = self.baseTransform.toQTransform().mapRect(self.drawObject.boundingRect()) + else: + raise TypeError('drawObject is not a valid type!') + pointList = [self.getScreenTransform().toQTransform().map(point) for point in [ + testBbox.topLeft(), testBbox.topRight(), testBbox.bottomLeft(), testBbox.bottomRight() + ]] + return Qg.QPolygonF(pointList).boundingRect() + + @property + def localBoundingBox(self): + testBbox = self.drawObject.rect() + testBbox.moveTo(self.btmRightAnchor.toPoint()) + return testBbox + + def getScreenTransform(self): + scrTransf = self.baseTransform.toQTransform().inverted()[0] * self.pTransform.toQTransform() + return asyTransform.fromQTransform(scrTransf) + + def draw(self, additionalTransformation=None, applyReverse=False, canvas: Qg.QPainter=None, dpi=300): + if canvas is None: + canvas = self.mainCanvas + if additionalTransformation is None: + additionalTransformation = Qg.QTransform() + + assert canvas.isActive() + + canvas.save() + if self.pen: + oldPen = Qg.QPen(canvas.pen()) + canvas.setPen(self.pen.toQPen()) + else: + oldPen = Qg.QPen() + + if not applyReverse: + canvas.setTransform(additionalTransformation, True) + canvas.setTransform(self.transform.toQTransform(), True) + else: + canvas.setTransform(self.transform.toQTransform(), True) + canvas.setTransform(additionalTransformation, True) + + canvas.setTransform(self.baseTransform.toQTransform().inverted()[0], True) + + if isinstance(self.drawObject, Qg.QImage): + canvas.drawImage(self.explicitBoundingBox, self.drawObject) + elif isinstance(self.drawObject, xs.SvgObject): + threshold = 1.44 + + if self.cachedDPI is None or self.cachedSvgImg is None \ + or dpi > self.maxDPI*threshold: + self.cachedDPI = dpi + self.maxDPI=max(self.maxDPI,dpi) + self.cachedSvgImg = self.drawObject.render(dpi) + + canvas.drawImage(self.explicitBoundingBox, self.cachedSvgImg) + elif isinstance(self.drawObject, Qs.QSvgRenderer): + self.drawObject.render(canvas, self.explicitBoundingBox) + elif isinstance(self.drawObject, Qg.QPainterPath): + path = self.baseTransform.toQTransform().map(self.drawObject) + if self.fill: + if self.pen: + brush = self.pen.toQPen().brush() + else: + brush = Qg.QBrush() + canvas.fillPath(path, brush) + else: + canvas.drawPath(path) + + if self.pen: + canvas.setPen(oldPen) + canvas.restore() + + def collide(self, coords, canvasCoordinates=True): + # modify these values to grow/shrink the fuzz. + fuzzTolerance = 1 + marginGrowth = 1 + leftMargin = marginGrowth if self.boundingBox.width() < fuzzTolerance else 0 + topMargin = marginGrowth if self.boundingBox.height() < fuzzTolerance else 0 + + newMargin = Qc.QMarginsF(leftMargin, topMargin, leftMargin, topMargin) + return self.boundingBox.marginsAdded(newMargin).contains(coords) + + def getID(self): + return self.originalObj |