###########################################################################
#
# xasy2asy provides a Python interface to Asymptote
#
#
# Authors: Orest Shardt, Supakorn Rassameemasmuang, and John C. Bowman
#
###########################################################################
import PyQt5.QtWidgets as QtWidgets
import PyQt5.QtGui as QtGui
import PyQt5.QtCore as QtCore
import PyQt5.QtSvg as QtSvg
import numpy as numpy
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 threading
from typing import Optional
import xasyUtils as xu
import xasyArgs as xa
import xasyOptions as xo
import xasySvg as xs
class AsymptoteEngine:
"""
Purpose:
--------
Class that makes it possible for xasy to communicate with asy
through a background pipe. It communicates with asy through a
subprocess of an existing xasy process.
Attributes:
-----------
istream : input stream
ostream : output stream
keepFiles : keep communicated files
tmpdir : temporary directory
args : system call arguments to start a required subprocess
asyPath : directory path to asymptote
asyProcess : the subprocess through which xasy communicates with asy
def wait(self):
""" wait for the pipe to finish any outstanding communication """
if self.asyProcess.returncode is not None:
return
else:
return self.asyProcess.wait()
@property
def active(self):
if self.asyProcess is None:
return False
return self.asyProcess.returncode is None
def stop(self):
""" kill an active asyProcess and close the pipe """
if self.active:
self.asyProcess.kill()
def cleanup(self):
""" terminate processes and cleans up communication files """
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(QtCore.QObject):
"""
Purpose:
--------
A python implementation of an asy transform. This class takes care of calibrating asymptote
coordinate system with the one used in PyQt to handle all existing inconsistencies.
To understand how this class works, having enough acquaintance with asymptote transform
feature is required. It is a child class of QtCore.QObject class.
Attributes:
-----------
t : The tuple
x, y, xx, xy, yx, yy : Coordinates corresponding to 6 entries
_deleted : Private local flag
Class Methods:
--------------
zero : Class method that returns an asyTransform object initialized with 6 zero entries
fromQTransform : Class method that converts QTransform object to asyTransform object
fromNumpyMatrix : Class method that converts transform matrix object to asyTransform object
Object Methods:
--------------
getRawCode : Returns the tuple entries
getCode : Returns the textual format of the asy code corresponding to the given transform
scale : Returns the scales version of the existing asyTransform
toQTransform : Converts asy transform object to QTransform object
identity : Return Identity asyTransform object
isIdentity : Check whether the asyTransform object is identity object
inverted : Applies the QTransform object's inverted method on the asyTransform object
yflip : Returns y-flipped asyTransform object
"""
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
class asyObj(QtCore.QObject):
"""
Purpose:
--------
A base class to create a Python object which contains all common
data and behaviors required during the translation of an xasy
object to its Asymptote code.
Attributes:
-----------
asyCode :The corresponding Asymptote code for the asyObj instance
Virtual Methods:
----------------
updateCode :Must to be re-implemented
Static Methods: NULL
--------------
Class Methods: NULL
--------------
Object Methods:
---------------
getCode :Return the Asymptote code that corresponds to the passed object
def updateCode(self, ps2asymap = identity()):
""" Update the object's code: should be overridden """
raise NotImplementedError
def getCode(self, ps2asymap = identity()):
""" Return the code describing the object """
self.updateCode(ps2asymap)
return self.asyCode
class asyPen(asyObj):
"""
Purpose:
--------
A Python object that corresponds to an Asymptote pen type. It
extends the 'asyObj' class to include a pen object. This object
will be used to make the corresponding Asymptote pen when
an xasy object gets translated to Asymptote code.
Attributes:
-----------
color : The color of Path
options : The options that can be passed to the path
width : The path width
_asyengine : The Asymptote engine that will be used
_deferAsyfy : ?
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 computeColor(self):
""" Find out the color of an arbitrary Asymptote pen """
assert isinstance(self.asyEngine, AsymptoteEngine)
assert self.asyEngine.active
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 toQPen(self):
if self._deferAsyfy:
self.computeColor()
newPen = QtGui.QPen()
color = asyPen.convertToQColor(self.color)
color.setAlpha(self.opacity)
newPen.setColor(color)
newPen.setCapStyle(self.capStyle)
newPen.setWidthF(self.width)
if self.dashPattern:
newPen.setDashPattern(self.dashPattern)
return newPen
class asyPath(asyObj):
"""
Purpose:
--------
A Python object that corresponds to an Asymptote path type. It
extends the 'asyObj' class to include a path object. This object
will be used to make the corresponding Asymptote path object when
an xasy object gets translated to its Asymptote code.
Attributes:
-----------
Virtual Methods:
----------------
Static Methods:
---------------
Class Methods:
--------------
Object Methods:
---------------
"""
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
self.fill = 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:
# 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 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()
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):
"""
Purpose:
--------
A Python object that corresponds to an asymptote label
type. It extends the 'asyObj' class to include a label
object. This object will be used to make the corresponding
Asymptote label object when an xasy object gets translated to its
asymptote code.
Attributes:
-----------
Virtual Methods:
----------------
Static Methods:
---------------
Class Methods:
--------------
Object Methods:
---------------
"""
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
class asyImage:
"""
Purpose:
--------
A Python object that is a container for an image coming from
Asymptote that is populated with the format, bounding box, and
IDTag, Asymptote key.
class xasyItem(QtCore.QObject):
"""
Purpose:
--------
A base class for any xasy object that can be drawn in PyQt. This class takes
care of all common behaviors available on any xasy item as well as all common
actions that can be done or applied to every xasy item.
Attributes:
-----------
Virtual Methods:
----------------
Static Methods:
---------------
Class Methods:
--------------
Object Methods:
---------------
"""
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.imageHandleQueue = queue.Queue()
def updateCode(self, ps2asymap = identity()):
""" Update the item's code: to be overridden """
with io.StringIO() as rawCode:
transfCode = self.getTransformCode()
objCode = self.getObjectCode()
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 == 'svg':
if containsClip:
image = xs.SvgObject(self.asyengine.tempDirName+file)
else:
image = QtSvg.QSvgRenderer(file)
assert image.isValid()
else:
raise Exception('Format {} not supported!'.format(fileformat))
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,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 #Does this ever exist?
else:
validKey = False
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.imageHandleQueue = queue.Queue()
worker = threading.Thread(target = self.asyfyThread, args = [])
worker.start()
item = self.imageHandleQueue.get()
cwd=os.getcwd();
os.chdir(self.asyengine.tempDirName)
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()
os.chdir(cwd);
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.maxKey=0
fout.write("reset\n")
fout.flush();
for line in self.getCode().splitlines():
if DebugFlags.printAsyTranscript:
print(line)
fout.write(line+"\n")
fout.write(self.asySize)
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 = '_{:d}.{:s}'.format(1+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(self.asyengine.tmpdir)
print(raw_text.strip())
fileformat = 'svg' # Output format
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
if not userkey:
self.unsetKeys.add(keydata) # the line and column to replace.
else:
if keydata.isdigit():
self.maxKey=max(self.maxKey,int(keydata))
self.userKeys.add(keydata)
# print(line, col)
if deleted:
raw_text = fin.readline()
continue
if keydata not in keyCounts.keys():
keyCounts[keydata] = 0
class xasyDrawnItem(xasyItem):
"""
Purpose:
--------
A base class dedicated to any xasy item that is drawn with the GUI.
Each object of this class corresponds to a particular drawn xasy item.
Attributes:
-----------
Virtual Methods:
----------------
Static Methods:
---------------
Class Methods:
--------------
Object Methods:
---------------
"""
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]}
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 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 """
"""
Purpose:
--------
Attributes:
-----------
Virtual Methods:
----------------
Static Methods:
---------------
Class Methods:
--------------
Object Methods:
---------------
"""
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 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 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() and not checktransf.deleted) for checktransf in writeval) and writeval:
transf = writeval.pop()
if transf.deleted:
rawAsyCode.write(xasyItem.setKeyFormatStr.format(key, transf.getCode(asy2psmap)))
else:
if transf == identity():
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(not transf.deleted and transf == identity() for transf in self.transfKeymap[key]) }
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')
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 in range(len(raw_code_lines)):
curr_str = raw_code_lines[i]
if i + 1 in keylist.keys():
# this case, we have a key.
with io.StringIO() as raw_line:
n=len(curr_str)
for j in range(n):
raw_line.write(curr_str[j])
if j + 1 in keylist[i + 1]:
# at this point, replace keys with xkey
sep=','
k=j+1
# assume begingroup is on a single line for now
while k < n:
c=curr_str[k]
if c == ')':
sep=''
break
if not c.isspace():
break
++k
raw_line.write('KEY="{0:s}"'.format(linenum2key[(i + 1, j + 1)])+sep)
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 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 --> asyfied --> 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
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()
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 Omitted"
return retVal
class DrawObject(QtCore.QObject):
"""
Purpose:
--------
The main Python class to draw an object with the help of PyQt graphical library.
Every instance of the class is
def draw(self, additionalTransformation = None, applyReverse = False, canvas: QtGui.QPainter = None, dpi = 300):
if canvas is None:
canvas = self.mainCanvas
if additionalTransformation is None:
additionalTransformation = QtGui.QTransform()
assert canvas.isActive()
canvas.save()
if self.pen:
oldPen = QtGui.QPen(canvas.pen())
localPen = self.pen.toQPen()
# localPen.setCosmetic(True)
canvas.setPen(localPen) #this fixes the object but not the box
else:
oldPen = QtGui.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)
if 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)
def __init__(self, asyengine, pen=None, transform=identity(), transfKey=None, transfKeymap = None, canvas=None, arrowActive=False, code=None):
#super().__init__(path=path, engine=asyengine, pen=pen, transform=transform)
"""Initialize the label with the given test, location, and pen"""
#asyObj.__init__(self)
super().__init__(canvas=canvas, asyengine=asyengine) #CANVAS? Seems to work.
if pen is None:
pen = asyPen()
if pen.asyEngine is None:
pen.asyEngine = asyengine
self.pen = pen
self.fillPen = asyPen()
self.fillPen.asyEngine = asyengine
self.code = code
#self.path = path
#self.path.asyengine = asyengine
self.transfKey = transfKey
if transfKeymap == None: #Better way?
self.transfKeymap = {self.transfKey: [transform]}
else:
self.transfKeymap = transfKeymap
self.location = (0,0)
self.asyfied = False
self.onCanvas = canvas
self.arrowSettings = {"active": arrowActive, "style": 0, "fill": 0} #Rename active?
self.arrowList = ["","Arrow","ArcArrow"] #The first setting corresponds to no arrow.
self.arrowStyleList = ["","SimpleHead","HookHead","TeXHead"]
self.arrowFillList = ["","FillDraw","Fill","NoFill","UnFill","Draw"]
def getArrowSettings(self):
settings = "("
if self.arrowSettings["style"] != 0:
settings += "arrowhead="
settings += self.arrowStyleList[self.arrowSettings["style"]]
if "size" in self.arrowSettings:
if settings != "(": #This is really messy.
settings += ","
settings += "size=" + str(self.arrowSettings["size"]) #Should I add options to this? Like for cm?
if "angle" in self.arrowSettings: #This is so similar, you should be able to turn this into a function or something.
if settings != "(":
settings += ","
settings += "angle=" + str(self.arrowSettings["angle"])
if self.arrowSettings["fill"] != 0:
if settings != "(":
settings += ","
settings += "filltype="
settings += self.arrowFillList[self.arrowSettings["fill"]]