diff options
Diffstat (limited to 'Master/texmf-dist/asymptote/GUI/xasy2asy.py')
-rwxr-xr-x | Master/texmf-dist/asymptote/GUI/xasy2asy.py | 2111 |
1 files changed, 1376 insertions, 735 deletions
diff --git a/Master/texmf-dist/asymptote/GUI/xasy2asy.py b/Master/texmf-dist/asymptote/GUI/xasy2asy.py index 06c0dbd7f54..2df6c3417a0 100755 --- a/Master/texmf-dist/asymptote/GUI/xasy2asy.py +++ b/Master/texmf-dist/asymptote/GUI/xasy2asy.py @@ -1,766 +1,1407 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + ########################################################################### # # xasy2asy provides a Python interface to Asymptote # # -# Author: Orest Shardt -# Created: June 29, 2007 +# Authors: Orest Shardt, Supakorn Rassameemasmuang, and John C. Bowman # ########################################################################### -import sys,os,signal,threading -from subprocess import * -from string import * -import xasyOptions -from tempfile import mkdtemp - -if sys.version_info >= (3, 0): - from tkinter import * - import queue -else: - from Tkinter import * - import Queue as queue - -# PIL support is now mandatory due to rotations -try: - from PIL import ImageTk - from PIL import Image -except: - pass - -import CubicBezier - -quickAsyFailed = True -global AsyTempDir - -console=None - -def startQuickAsy(): - global quickAsy - global quickAsyFailed - global AsyTempDir - global fout,fin - if quickAsyRunning(): - return - try: - fout.close() - quickAsy.wait() - except: - pass - try: - quickAsyFailed = False - if os.name == "nt": - AsyTempDir=mkdtemp(prefix="asy_", dir="./") - else: - AsyTempDir=mkdtemp(prefix="asy_")+os.sep - if sys.platform[:3] == 'win': - quickAsy=Popen([xasyOptions.options['asyPath'],"-noV","-multiline","-q", - "-o"+AsyTempDir,"-inpipe=0","-outpipe=2"],stdin=PIPE, - stderr=PIPE,universal_newlines=True) - fout=quickAsy.stdin - fin=quickAsy.stderr - else: - (rx,wx) = os.pipe() - (ra,wa) = os.pipe() - if sys.version_info >= (3, 4): - os.set_inheritable(rx, True) - os.set_inheritable(wx, True) - os.set_inheritable(ra, True) - os.set_inheritable(wa, True) - quickAsy=Popen([xasyOptions.options['asyPath'],"-noV","-multiline","-q", - "-o"+AsyTempDir,"-inpipe="+str(rx),"-outpipe="+str(wa)], - close_fds=False) - fout=os.fdopen(wx,'w') - fin=os.fdopen(ra,'r') - if quickAsy.returncode != None: - quickAsyFailed = True - except: - quickAsyFailed = True - -def stopQuickAsy(): - if quickAsyRunning(): - fout.write("exit;\n"); - -def getAsyTempDir(): - return AsyTempDir - -def quickAsyRunning(): - if quickAsyFailed or quickAsy.returncode != None: - return False - else: - return True - -def asyExecute(command): - if not quickAsyRunning(): - startQuickAsy() - fout.write(command) - -def closeConsole(event): - global console - console = None - -def consoleOutput(line): - global console - global ctl - if console == None: - ctl=Toplevel() - ctl.title("Asymptote Console") - ctl.bind("<Destroy>",closeConsole) - yscrollbar=Scrollbar(ctl) - yscrollbar.pack(side=RIGHT,fill=Y) - console=Text(ctl,yscrollcommand=yscrollbar.set) - console.pack() - yscrollbar.config(command=console.yview) - console.insert(END,line) - ctl.lift() - -class asyTransform: - """A python implementation of an asy transform""" - def __init__(self,initTuple,delete=False): - """Initialize the transform with a 6 entry tuple""" - if type(initTuple) == type((0,)) 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 Exception("Illegal initializer for asyTransform") - - def getCode(self): - """Obtain the asy code that represents this transform""" - if self.deleted: - return str(self.t) + ", false" - else: - return str(self.t) - - def scale(self,s): - return asyTransform((0,0,s,0,0,s))*self - - def __str__(self): - """Equivalent functionality to getCode(). It allows the expression str(asyTransform) to be meaningful.""" - return self.getCode() - - def __mul__(self,other): - """Define multiplication of transforms as composition.""" - if type(other)==type((0,)): - 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 - else: - raise Exception("Illegal multiplier of {:s}".format(str(type(other)))) + +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)) + return asyTransform((0, 0, 1, 0, 0, 1)) + +def yflip(): + return asyTransform((0, 0, 1, 0, 0, -1)) -class asyObj: - """A base class for asy objects: an item represented by asymptote code.""" - def __init__(self): - """Initialize the object""" - self.asyCode = "" +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,mag=1.0): - """Update the object's code: should be overriden.""" - pass + 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 - def getCode(self): - """Return the code describing the object""" - self.updateCode() - return self.asyCode class asyPen(asyObj): - """A python wrapper for an asymptote pen""" - def __init__(self,color=(0,0,0),width=0.5,options=""): - """Initialize the pen""" - asyObj.__init__(self) - self.options=options - self.width=width - self.setColor(color) - self.updateCode() - if options != "": - self.computeColor() - - def updateCode(self,mag=1.0): - """Generate the pen's code""" - 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.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 type(color) == type((1,)) and len(color) == 3: - self.color = color - else: - self.color = "(0,0,0)" - self.updateCode() - - def computeColor(self): - """Find out the color of an arbitrary asymptote pen.""" - fout.write("pen p="+self.getCode()+';\n') - fout.write("file fout=output(mode='pipe');\n") - fout.write("write(fout,colorspace(p),newl);\n") - fout.write("write(fout,colors(p));\n") - fout.write("flush(fout);\n") - 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]) - self.color = (r,g,b) - - 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])) + """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): - """Initialize the path to be an empty path: a path with no nodes, control points, or links.""" - asyObj.__init__(self) - self.nodeSet = [] - self.linkSet = [] - self.controlSet = [] - self.computed = False - - 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: - return "("+str(node[0])+","+str(node[1])+")" - - def updateCode(self,mag=1.0): - """Generate the code describing the path""" - if not self.computed: - count = 0 - #this string concatenation could be optimised - self.asyCode = self.makeNodeStr(self.nodeSet[0]) - for node in self.nodeSet[1:]: - self.asyCode += self.linkSet[count]+self.makeNodeStr(node) - count += 1 - else: - count = 0 - #this string concatenation could be optimised - self.asyCode = self.makeNodeStr(self.nodeSet[0]) - for node in self.nodeSet[1:]: - self.asyCode += "..controls" - self.asyCode += self.makeNodeStr(self.controlSet[count][0]) - self.asyCode += "and" - self.asyCode += self.makeNodeStr(self.controlSet[count][1]) - self.asyCode += ".." + self.makeNodeStr(node) + "\n" - count += 1 - - 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[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 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""" - fout.write("file fout=output(mode='pipe');\n") - fout.write("path p="+self.getCode()+';\n') - fout.write("write(fout,length(p),newl);\n") - fout.write("write(fout,unstraighten(p),endl);\n") - 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 + """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=asyPen()): - """Initialize the label with the given test, location, and pen""" - asyObj.__init__(self) - self.text = text - self.location = location - self.pen = pen - - def updateCode(self,mag=1.0): - """Generate the code describing the label""" - self.asyCode = "Label(\""+self.text+"\","+str((self.location[0],self.location[1]))+","+self.pen.getCode()+",align=SE)" - - 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 + """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): - self.image = image - self.format = format - self.bbox = bbox - self.IDTag = None - -class xasyItem: - """A base class for items in the xasy GUI""" - def __init__(self,canvas=None): - """Initialize the item to an empty item""" - self.transform = [identity()] - self.asyCode = "" - self.imageList = [] - self.IDTag = None - self.asyfied = False - self.onCanvas = canvas - - def updateCode(self,mag=1.0): - """Update the item's code: to be overriden""" - pass - - def getCode(self): - """Return the code describing the item""" - self.updateCode() - return self.asyCode - - def handleImageReception(self,file,format,bbox,count): - """Receive an image from an asy deconstruction. It replaces the default in asyProcess.""" - image = Image.open(file) - self.imageList.append(asyImage(image,format,bbox)) - if self.onCanvas != None: - self.imageList[-1].itk = ImageTk.PhotoImage(image) - self.imageList[-1].originalImage = image.copy() - self.imageList[-1].originalImage.theta = 0.0 - self.imageList[-1].originalImage.bbox = bbox - if count >= len(self.transform) or self.transform[count].deleted == False: - self.imageList[-1].IDTag = self.onCanvas.create_image(bbox[0],-bbox[3],anchor=NW,tags=("image"),image=self.imageList[-1].itk) - self.onCanvas.update() - - def asyfy(self,mag=1.0): - self.removeFromCanvas() - self.imageList = [] - self.imageHandleQueue = queue.Queue() - worker = threading.Thread(target=self.asyfyThread,args=(mag,)) - worker.start() - item = self.imageHandleQueue.get() - if console != None: - console.delete(1.0,END) - while item != (None,) and item[0] != "ERROR": - if(item[0] == "OUTPUT"): - consoleOutput(item[1]) - else: - self.handleImageReception(*item) - try: - os.remove(item[0]) - except: - pass - item = self.imageHandleQueue.get() - #self.imageHandleQueue.task_done() - worker.join() - - def asyfyThread(self,mag=1.0): - """Convert the item to a list of images by deconstructing this item's code""" - fout.write("reset;\n") - fout.write("initXasyMode();\n") - fout.write("atexit(null);\n") - for line in self.getCode().splitlines(): - fout.write(line+"\n"); - fout.write("deconstruct({:f});\n".format(mag)) - fout.flush() - maxargs = int(fin.readline().split()[0]) - boxes=[] - batch=0 - n=0 - text = fin.readline() - # template=AsyTempDir+"%d_%d.%s" - fileformat = "png" - def render(): - for i in range(len(boxes)): - l,b,r,t = [float(a) for a in boxes[i].split()] - name="{:s}{:d}_{:d}.{:s}".format(AsyTempDir,batch,i+1,fileformat) - self.imageHandleQueue.put((name,fileformat,(l,b,r,t),i)) - while text != "Done\n" and text != "Error\n": - boxes.append(text) - text = fin.readline() - n += 1 - if n >= maxargs: - render() - boxes=[] - batch += 1 - n=0 - if text == "Error\n": - self.imageHandleQueue.put(("ERROR",fin.readline())) - else: - render() - self.imageHandleQueue.put((None,)) - self.asyfied = True - - def drawOnCanvas(self,canvas,mag,forceAddition=False): - pass - def removeFromCanvas(self): - pass + """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,pen = asyPen(),transform = identity()): - """Initialize the item with a path, pen, and transform""" - xasyItem.__init__(self) - self.path = path - self.pen = pen - self.transform = [transform] - - 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 - if len(self.path.nodeSet) > 1 and link != None: - self.path.linkSet.append(link) - - def clearTransform(self): - """Reset the item's transform""" - self.transform = [identity()] - - 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 - - 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 + """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,pen=asyPen(),transform=identity()): - """Initialize the shape with a path, pen, and transform""" - xasyDrawnItem.__init__(self,path,pen,transform) - - def updateCode(self,mag=1.0): - """Generate the code to describe this shape""" - self.asyCode = "xformStack.push("+self.transform[0].getCode()+");\n" - self.asyCode += "draw("+self.path.getCode()+","+self.pen.getCode()+");" - - def removeFromCanvas(self,canvas): - """Remove the shape's depiction from a tk canvas""" - if self.IDTag != None: - canvas.delete(self.IDTag) - - def drawOnCanvas(self,canvas,mag,asyFy=False,forceAddition=False): - """Add this shape to a tk canvas""" - if not asyFy: - if self.IDTag == None or forceAddition: - #add ourselves to the canvas - self.path.computeControls() - self.IDTag = canvas.create_line(0,0,0,0,tags=("drawn","xasyShape"),fill=self.pen.tkColor(),width=self.pen.width*mag) - self.drawOnCanvas(canvas,mag) - else: - self.path.computeControls() - pointSet = [] - previousNode = self.path.nodeSet[0] - nodeCount = 0 - if len(self.path.nodeSet) == 0: - pointSet = [0,0,0,0] - elif len(self.path.nodeSet) == 1: - if self.path.nodeSet[-1] != 'cycle': - p = self.transform[0]*(self.path.nodeSet[0][0],self.path.nodeSet[0][1]) - pointSet = [p[0],-p[1],p[0],-p[1],p[0],-p[1]] - else: - pointSet = [0,0,0,0] + """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: - for node in self.path.nodeSet[1:]: - if node == 'cycle': - node = self.path.nodeSet[0] - transform = self.transform[0].scale(mag) - points = CubicBezier.makeBezier(transform*previousNode,transform*self.path.controlSet[nodeCount][0],transform*self.path.controlSet[nodeCount][1],transform*node) - for point in points: - pointSet += [point[0],-point[1]] - nodeCount += 1 - previousNode = node - canvas.coords(self.IDTag,*pointSet) - canvas.itemconfigure(self.IDTag,fill=self.pen.tkColor(),width=self.pen.width*mag) - else: - #first asyfy then add an image list - pass - - def __str__(self): - """Create a string describing this shape""" - return "xasyShape code:{:s}".format("\n\t".join(self.getCode().splitlines())) + 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,pen=asyPen(),transform=identity()): - """Initialize this shape with a path, pen, and transform""" - if path.nodeSet[-1] != 'cycle': - raise Exception("Filled paths must be cyclic") - xasyShape.__init__(self,path,pen,transform) - - def updateCode(self,mag=1.0): - """Generate the code describing this shape""" - self.asyCode = "xformStack.push("+self.transform[0].getCode()+");\n" - self.asyCode += "fill("+self.path.getCode()+","+self.pen.getCode()+");" - - def removeFromCanvas(self,canvas): - """Remove this shape's depiction from a tk canvas""" - if self.IDTag != None: - canvas.delete(self.IDTag) - - def drawOnCanvas(self,canvas,mag,asyFy=False,forceAddition=False): - """Add this shape to a tk canvas""" - if not asyFy: - if self.IDTag == None or forceAddition: - #add ourselves to the canvas - self.path.computeControls() - self.IDTag = canvas.create_polygon(0,0,0,0,0,0,tags=("drawn","xasyFilledShape"),fill=self.pen.tkColor(),outline=self.pen.tkColor(),width=1*mag) - self.drawOnCanvas(canvas,mag) - else: - self.path.computeControls() - pointSet = [] - previousNode = self.path.nodeSet[0] - nodeCount = 0 - if len(self.path.nodeSet) == 0: - pointSet = [0,0,0,0,0,0] - elif len(self.path.nodeSet) == 1: - if self.path.nodeSet[-1] != 'cycle': - p = self.transform[0]*(self.path.nodeSet[0][0],self.path.nodeSet[0][1]) - pointSet = [p[0],-p[1],p[0],-p[1],p[0],-p[1]] - else: - pointSet = [0,0,0,0,0,0] - elif len(self.path.nodeSet) == 2: - if self.path.nodeSet[-1] != 'cycle': - p = self.transform[0].scale(mag)*(self.path.nodeSet[0][0],self.path.nodeSet[0][1]) - p2 = self.transform[0].scale(mag)*(self.path.nodeSet[1][0],self.path.nodeSet[1][1]) - pointSet = [p[0],-p[1],p2[0],-p2[1],p[0],-p[1]] - else: - pointSet = [0,0,0,0,0,0] - else: - for node in self.path.nodeSet[1:]: - if node == 'cycle': - node = self.path.nodeSet[0] - transform = self.transform[0].scale(mag) - points = CubicBezier.makeBezier(transform*previousNode,transform*self.path.controlSet[nodeCount][0],transform*self.path.controlSet[nodeCount][1],transform*node) - for point in points: - pointSet += [point[0],-point[1]] - nodeCount += 1 - previousNode = node - canvas.coords(self.IDTag,*pointSet) - canvas.itemconfigure(self.IDTag,fill=self.pen.tkColor(),outline=self.pen.tkColor(),width=1*mag) - else: - #first asyfy then add an image list - pass - - def __str__(self): - """Return a string describing this shape""" - return "xasyFilledShape code:{:s}".format("\n\t".join(self.getCode().splitlines())) + """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,pen=asyPen(),transform=identity()): - """Initialize this item with text, a location, pen, and transform""" - xasyItem.__init__(self) - self.label=asyLabel(text,location,pen) - self.transform = [transform] - self.onCanvas = None - - def updateCode(self,mag=1.0): - """Generate the code describing this object""" - self.asyCode = "xformStack.push("+self.transform[0].getCode()+");\n" - self.asyCode += "label("+self.label.getCode()+");" - - def removeFromCanvas(self): - """Removes the label's images from a tk canvas""" - if self.onCanvas == None: - return - for image in self.imageList: - if image.IDTag != None: - self.onCanvas.delete(image.IDTag) - - def drawOnCanvas(self,canvas,mag,asyFy=True,forceAddition=False): - """Adds the label's images to a tk canvas""" - if self.onCanvas == None: - self.onCanvas = canvas - elif self.onCanvas != canvas: - raise Exception("Error: item cannot be added to more than one canvas") - self.asyfy(mag) - - def __str__(self): - return "xasyText code:{:s}".format("\n\t".join(self.getCode().splitlines())) + """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,script="",transforms=[]): - """Initialize this script item""" - xasyItem.__init__(self,canvas) - self.transform = transforms[:] - self.script = script - - def clearTransform(self): - """Reset the transforms for each of the deconstructed images""" - self.transform = [identity() for im in self.imageList] - - def updateCode(self,mag=1.0): - """Generate the code describing this script""" - self.asyCode = ""; - if len(self.transform) > 0: - self.asyCode = "xformStack.add(" - isFirst = True - count = 0 - for xform in self.transform: - if not isFirst: - self.asyCode+=",\n" - self.asyCode += "indexedTransform({:d},{:s})".format(count,str(xform)) - isFirst = False - count += 1 - self.asyCode += ");\n" - self.asyCode += "startScript(); {\n" - self.asyCode += self.script.replace("\t"," ") - self.asyCode = self.asyCode.rstrip() - self.asyCode += "\n} endScript();\n" - - def setScript(self,script): - """Sets the content of the script item.""" - self.script = script - self.updateCode() - - def removeFromCanvas(self): - """Removes the script's images from a tk canvas""" - if self.onCanvas == None: - return - for image in self.imageList: - if image.IDTag != None: - self.onCanvas.delete(image.IDTag) - - def asyfy(self,mag): - """Generate the list of images described by this object and adjust the length of the transform list.""" - xasyItem.asyfy(self,mag) - while len(self.imageList) > len(self.transform): - self.transform.append(identity()) - while len(self.imageList) < len(self.transform): - self.transform.pop() - self.updateCode() - - def drawOnCanvas(self,canvas,mag,asyFy=True,forceAddition=False): - """Adds the script's images to a tk canvas""" - if self.onCanvas == None: - self.onCanvas = canvas - elif self.onCanvas != canvas: - raise Exception("Error: item cannot be added to more than one canvas") - self.asyfy(mag) - - 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 - -if __name__=='__main__': - root = Tk() - t=xasyText("test",(0,0)) - t.asyfy() + """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 |