Repository: fsmMLK/inkscapeMadeEasy Branch: master Commit: 18fe5e319125 Files: 31 Total size: 626.7 KB Directory structure: gitextract_ga0zb2kn/ ├── 0.9x/ │ ├── inkscapeMadeEasy_Base.py │ ├── inkscapeMadeEasy_Draw.py │ ├── inkscapeMadeEasy_Plot.py │ └── textextLib/ │ ├── CircuitSymbolsLatexPreamble.tex │ ├── __init__.py │ ├── basicLatexPackages.tex │ ├── textext.inx │ └── textext.py ├── LICENSE ├── README.md ├── docs/ │ ├── Makefile │ └── source/ │ ├── Changelog.rst │ ├── _static/ │ │ └── style.css │ ├── conf.py │ ├── genindex.rst │ ├── index.rst │ ├── installation.rst │ ├── mainFeatures.rst │ ├── minimalExample.rst │ └── moduleDefinitions.rst ├── examples/ │ ├── iME_Draw_colorPicker.inx │ ├── iME_Draw_colorPicker.py │ ├── iME_Draw_lineStyle_and_markers.inx │ ├── iME_Draw_lineStyle_and_markers.py │ ├── minimalExample.inx │ ├── minimalExample.py │ └── testingMinimalExample.py └── latest/ ├── basicLatexPackages.tex ├── inkscapeMadeEasy_Base.py ├── inkscapeMadeEasy_Draw.py └── inkscapeMadeEasy_Plot.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: 0.9x/inkscapeMadeEasy_Base.py ================================================ #!/usr/bin/python # ----------------------------------------------------------------------------- # # inkscapeMadeEasy: - Helper module that extends Aaron Spike's inkex.py module, # focusing productivity in inkscape extension development # # Copyright (C) 2016 by Fernando Moura # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ----------------------------------------------------------------------------- import math import os import re import sys from copy import deepcopy import numpy as np import inkex """ Base helper module that extends Aaron Spike's inkex.py module, adding basic manipulation functions This module requires the following modules: inkex, re, lxml, numpy and os. """ # --------------------------------------------- class inkscapeMadeEasy(inkex.Effect): """ Base class for extensions. This class extends the inkex.Effect class by adding several basic functions to help manipulating inkscape elements. All extensions should inherit this class. """ def __init__(self): inkex.Effect.__init__(self) self.inkscapeResolution_dpi = 96.0 # number of pixels per inch resolution_in = self.inkscapeResolution_dpi resolution_mm = self.inkscapeResolution_dpi / 25.4 self.unitsDict = {'mm': resolution_mm, # 25.4mm per inch 'cm': resolution_mm * 10.0, # 1cm = 10mm 'm': resolution_mm * 1.0e3, # 1m = 1000mm 'km': resolution_mm * 1.0e6, # 1km = 1e6mm 'in': resolution_in, # 1in = 96px 'ft': resolution_in * 12.0, # foot = 12*in 'yd': resolution_in * 12.0 * 3.0, # yard = 3*ft 'pt': resolution_in / 72.0, # point 1pt = 1/72th of an inch 'px': 1.0, 'pc': resolution_in / 6.0} # picas 1pc = 1/6th of and inch # coordinates o the origin of the grid. unfortunately the grid does not fit # x0=0 # y0=-7.637817382813 def displayMsg(self, msg): """Displays a message to the user. :param msg: message :type msg: string :returns: nothing :rtype: - """ sys.stderr.write(msg + '\n') def getBasicLatexPackagesFile(self): """Returns the full path of the ``basicLatexPackages.tex`` file with commonly used Latex packages The default packages are:: \\usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \\usepackage[per=slash]{siunitx} \\usepackage{steinmetz} You can add other packages to the file ``basicLatexPackages.tex``, located in the extension directory. :returns: Full path of the file with commonly used Latex packages :rtype: string """ directory = os.getcwd() return directory + '/textextLib/basicLatexPackages.tex' def Dump(self, obj, file='./dump_file.txt', mode='w'): """Function to easily output the result of ``str(obj)`` to a file This function was created to help debugging the code while it is running under inkscape. Since inkscape does not possess a terminal as today (2016), this function overcomes partially the issue of sending things to stdout by dumping result of the function ``str()`` in a text file. :param obj: object to sent to a file. Any type that can be used in ``str()`` :param file: file path. Default: ``./dump_file.txt`` :param mode: writing mode of the file Default: ``w`` (write) :type obj: any, as long as ``str(obj)`` is implemented (see ``__str__()`` metaclass definition ) :type file: string :type mode: string :returns: nothing :rtype: - **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> x=inkscapeMadeEasy >>> vector1=[1,2,3,4,5,6] >>> x.Dump(vector1,file='~/temporary.txt',mode='w') # writes the list to a file >>> vector2=[7,8,9,10] >>> x.Dump(vector2,file='~/temporary.txt',mode='a') # append the list to a file """ file = open(file, mode) file.write(str(obj) + '\n') file.close() def removeElement(self, element): """Function to remove one element or group. If the parent of the element is a group and has no other children, then the parent is also removed. :param element: element object :type element: element object :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> line3 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[15,0]],[10,0]) # creates a line in groupA >>> self.removeElement(line1) # removes line 1 >>> self.removeElement(line2) # removes line 2 >>> self.removeElement(line3) # removes line 3. Also removes groupA since this group has no other children >>> groupB = self.createGroup(rootLayer,label='temp1') # creates a group inside rootLayer >>> line4 = inkscapeMadeEasy_Draw.line.relCoords(groupB, [[5,0]],[0,0]) # creates a line in groupB >>> self.removeElement(groupB) # removes group B and all its children """ parent = element.getparent() parent.remove(element) if parent.tag == 'g' and len(parent.getchildren()) == 0: # if object's parent is a group and has no other children, remove parent as well temp = parent.getparent() if temp is not None: temp.remove(parent) def importSVG(self, parent, fileIn, createGroup=True): """ Import SVG file into the current document :param parent: parent element where all contents will be placed :param fileIn: SVG file path :param createGroup: create a group containing all imported elements. (Default: True) :type parent: element object :type fileIn: string :type createGroup: bool :returns: imported element objects. If createGroup==True, returns the group. Otherwise returns a list with all imported elements :rtype: element object or list of objects **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> import inkscapeMadeEasy_Draw as inkDraw >>> x=inkscapeMadeEasy >>> rootLayer = x.document.getroot() # retrieves the root layer of the file >>> imported1 = self.importSVG(rootLayer,'/path/to/file1.svg',True) # import contents of the file and group them. imported1 is the group element >>> imported2 = self.importSVG(rootLayer,'/path/to/file2.svg',False) # import contents of the file. imported2 is a list of the imported elements """ documentIn = inkex.etree.parse(fileIn, parser=inkex.etree.XMLParser(huge_tree=True)).getroot() if createGroup: group = self.createGroup(parent, label='importedSVG') for elem in documentIn: if elem.tag != inkex.addNS('namedview', 'sodipodi') and elem.tag != inkex.addNS('metadata', 'svg'): group.append(elem) self.unifyDefs() return group else: listElements=[] for elem in documentIn: if elem.tag != inkex.addNS('namedview', 'sodipodi') and elem.tag != inkex.addNS('metadata', 'svg'): parent.append(elem) if elem.tag != inkex.addNS('defs', 'svg'): listElements.append(elem) self.unifyDefs() return listElements def exportSVG(self, element, fileOut): """ Export the elements in a new svgfile This function will export the element in a new SVG file. If a list of elements is passed as argument. All elements in the list will be exported to the same file. :param element: element or list of elements to be exported :param fileOut: file path, including the extension. :type element: element or list of elements :type file: string :returns: nothing :rtype: - .. note:: All the defs of the original file will be copied to the new file. Therefore you might want to run te vacuum tool to cleanup the new SVG file **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> import inkscapeMadeEasy_Draw as inkDraw >>> x=inkscapeMadeEasy >>> rootLayer = x.document.getroot() # retrieves the root layer of the file >>> groupA = x.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> groupB = x.createGroup(rootLayer,label='child') # creates a group inside groupA >>> line1 = inkDraw.line.relCoords(groupA, [[10,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(groupB, [[20,0]],[0,0]) # creates a line in groupB >>> self.exportSVG(line1,'file1.svg') # exports only line1 >>> self.exportSVG(groupA,'file2.svg') # exports groupA (and all elements contained in it) >>> self.exportSVG([groupA,groupB],'file3.svg') # exports groupA and groupB (and all elements contained in it) to the same file """ document = inkex.etree.fromstring(blankSVG) elem_tmp = deepcopy(element) # add definitions defs_tmp = deepcopy(self.getDefinitions()) document.append(defs_tmp) # add elements if isinstance(elem_tmp, list): for e in elem_tmp: document.append(e) else: document.append(elem_tmp) et = inkex.etree.ElementTree(document) et.write(fileOut, pretty_print=True) def uniqueIdNumber(self, prefix_id): """ Generates a unique ID with a given prefix ID by adding a numeric suffix This function is used to generate a valid unique ID by concatenating a given prefix with a numeric suffix. The overall format is ``prefix-%05d``. This function makes sure the ID is unique by checking in ``doc_ids`` member. This function is specially useful for creating an unique ID for markers and other elements in defs. :param prefix_id: prefix of the ID :type prefix_id: string :returns: the unique ID :rtype: string .. note:: This function has been adapted from Aaron Spike's inkex.py https://github.com/hacktoon/ink2canvas/blob/master/ink2canvas/lib/inkex.py **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> x=inkscapeMadeEasy >>> a=x.uniqueIdNumber('myName') # a=myName-00001 >>> b=x.uniqueIdNumber('myName') # b=myName-00002 >>> c=x.uniqueIdNumber('myName') # c=myName-00003 """ numberID = 1 new_id = prefix_id + '-%05d' % numberID while new_id in self.doc_ids: numberID += 1 new_id = prefix_id + '-%05d' % numberID self.doc_ids[new_id] = 1 return new_id # --------------------------------------------- def getDefinitions(self): """ retrieves the Defs element of the svg file. This function returns the element Defs of the current svg file. This elements stores the definition (e.g. marker definition) if no Defs can be found, a new empty Defs is created :returns: the defs element :rtype: etree element """ defs = self.getElemFromXpath('/svg:svg//svg:defs') if defs is None: defs = inkex.etree.SubElement(self.document.getroot(), inkex.addNS('defs', 'svg')) return defs # --------------------------------------------- def unifyDefs(self): """Unify all nodes in a single node. :returns: None :rtype: - .. note:: This function does not check whether the ids are unique """ root = self.getElemFromXpath('/svg:svg') mainDef = self.getDefinitions() for d in root.findall('.//svg:defs', namespaces=inkex.NSS): if d != mainDef: for child in d: mainDef.append(child) if child.tag == inkex.addNS('g', 'svg') or child.tag == 'g': self.ungroup(child) d.getparent().remove(d) # --------------------------------------------- def getDefsByTag(self, tag='marker'): """ retrieves the Defs elements of the svg file of a given a tag. :returns: a list with the def elements :rtype: list of etree element """ return self.getDefinitions().findall('.//svg:%s' % tag, namespaces=inkex.NSS) # --------------------------------------------- def getDefsById(self,id): """ return a list of elements in the defs node, given (part of) the id :returns: a list with the def elements :rtype: list of etree element """ return self.getDefinitions().xpath('./*[contains(@id,"%s")]' % id) # --------------------------------------------- def getElemFromXpath(self, xpath): """ returns the element from the xml, given its xpath :param xpath: tag of the element to be searched :type xpath: string :returns: element :rtype: element **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> x=inkscapeMadeEasy >>> name = x.getElemFromXpath('/svg:svg//svg:defs') # returns the list of definitions of the document """ elem = self.xpathSingle(xpath) return elem # --------------------------------------------- def getElemAttrib(self, elem, attribName): """ Returns the atribute of one element, given the atribute name :param elem: elem under consideration :param attribName: attribute to be searched. Format: namespace:attrName :type elem: element :type attribName: string :returns: attribute :rtype: string >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> x=inkscapeMadeEasy >>> elem= x.getElemFromXpath('/svg:svg') >>> docNAme = x.getElemAttrib(elem,'sodipodi:docname') """ # splits namespace and attrib name atribList = attribName.split(':') if len(atribList) == 1: # if has no namespace attrib = attribName else: # if has namespace namespace = inkex.NSS[atribList[0]] attrib = '{%s}' % namespace + atribList[1] return elem.attrib[attrib] # --------------------------------------------- def getDocumentScale(self): """returns the scale of the document **Example** >>> scale = x.getDocumentScale() """ try: elem = self.getElemFromXpath('/svg:svg') width = float(self.getElemAttrib(elem, 'width').replace(self.documentUnit, '')) viewBox = self.getElemFromXpath('/svg:svg') viewBox_width = float(self.getElemAttrib(viewBox, 'viewBox').split(' ')[2]) doc_scale = viewBox_width / width except: doc_scale = 1.0 return doc_scale # --------------------------------------------- def getDocumentName(self): """returns the name of the document :returns: fileName :rtype: string **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> x=inkscapeMadeEasy >>> name = x.getDocumentName() """ elem = self.getElemFromXpath('/svg:svg') try: fileName = self.getElemAttrib(elem, 'sodipodi:docname') except: fileName = None return fileName # --------------------------------------------- def getDocumentUnit(self): """returns the unit of the document :returns: unit string code. See table below :rtype: string **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> docunit = self.getDocumentUnit() #returns 'cm', 'mm', etc. """ elem = self.getElemFromXpath('/svg:svg/sodipodi:namedview') try: unit = self.getElemAttrib(elem, 'inkscape:document-units') except: unit = 'px' return unit # --------------------------------------------- def getcurrentLayer(self): """returns the current layer of the document :returns: name of the current layer :rtype: string **Example** >>> from inkscapeMadeEasy_Base import inkscapeMadeEasy >>> x=inkscapeMadeEasy >>> name = x.getDocumentName() """ return self.current_layer # --------------------------------------------- def removeAbsPath(self, element): abspath = self.getElemAttrib(element, 'sodipodi:absref') fileName = os.path.basename(abspath) dirName = os.path.dirname(abspath) # removes sodipodi:absref attribute namespace = inkex.NSS['sodipodi'] attrib = '{%s}' % namespace + 'absref' element.attrib.pop(attrib, None) # adds sodipodi:relref attrib = '{%s}' % namespace + 'relref' element.set(attrib, fileName) # --------------------------------------------- def unit2userUnit(self, value, unit_in): """Converts a value from given unit to inkscape's default unit (px) :param value: value to be converted :param unit_in: input unit string code. See table below :type value: float :type unit_in: string :returns: converted value :rtype: float **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> x_cm = 5.0 >>> x_px = self.unit2userUnit(x_cm,'cm') # converts 5.0cm -> 188.97px """ return value * self.unitsDict[unit_in.lower()] # --------------------------------------------- def userUnit2unit(self, value, unit_out): """Converts a value from inkscape's default unit (px) to specified unit :param value: value to be converted :param unit_out: output unit string code. See table below :type value: float :type unit_out: string :returns: converted value :rtype: float **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> x_px = 5.0 >>> x_cm = self.userUnit2unit(x_px,'cm') # converts 5.0px -> 0.1322cm """ return value / float(self.unitsDict[unit_out.lower()]) # --------------------------------------------- def unit2unit(self, value, unit_in, unit_out): """Converts a value from one provided unit to another unit :param value: value to be converted :param unit_in: input unit string code. See table below :param unit_out: output unit string code. See table below :type value: float :type unit_in: string :type unit_out: string :returns: converted value :rtype: float **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> x_in = 5.0 >>> x_cm = self.unit2unit(x_in,'in','cm') # converts 5.0in -> 12.7cm """ return value * self.unitsDict[unit_in.lower()] / float(self.unitsDict[unit_out.lower()]) # --------------------------------------------- def createGroup(self, parent, label='none'): """Creates a new empty group of elements. This function creates a new empty group of elements. In order to create new elements inside this groups you must create them informing the group as the parent element. :param parent: parent object of the group. It can be another group or the root element :param label: label of the group. Default: 'none' :type parent: element object :type label: string :returns: the group object :rtype: group element .. note:: The label does not have to be unique **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> groupB = self.createGroup(groupA,label='child') # creates a group inside groupA >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[10,0]],[0,0]) # creates a line in groupA >>> line2 = inkscapeMadeEasy_Draw.line.relCoords(groupB, [[20,0]],[0,0]) # creates a line in groupB """ if label != 'none': g_attribs = {inkex.addNS('label', 'inkscape'): label} group = inkex.etree.SubElement(parent, 'g', g_attribs) else: group = inkex.etree.SubElement(parent, 'g') return group # --------------------------------------------- def ungroup(self, group): """Ungroup elements The new parent element of the ungrouped elements will be the parent of the removed group. See example below :param group: group to be removed :type group: group element :returns: list of the elements previously contained in the group :rtype: list of elements **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> groupB = self.createGroup(groupA,label='temp') # creates a group inside groupA >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[10,0]],[0,0]) # creates a line in groupA >>> line2 = inkscapeMadeEasy_Draw.line.relCoords(groupB, [[20,0]],[0,0]) # creates a line in groupB >>> line3 = inkscapeMadeEasy_Draw.line.relCoords(groupB, [[30,0]],[0,0]) # creates a line in groupB >>> # at this point, the file struct is: groupA[ line1, groupB[ line2, line3 ] ] >>> elemList = self.ungroup(groupB) # ungroup line2 and line3. elemList is a list containing line2 and line3 elements. >>> # now the file struct is: groupA[ line1, line2, line3 ] """ if group.tag == 'g' or group.tag == inkex.addNS('g', 'svg'): # if object is a group parent = group.getparent() listElem=[] if parent is not None: for child in group: parent.append(child) listElem.append(child) self.removeElement(group) return listElem # --------------------------------------------- def getTransformMatrix(self, element): """Returns the transformation attribute of the given element and the resulting transformation matrix (numpy Array) This function is used to extract the transformation operator of a given element. :param element: element object :type element: element object :returns: [transfAttrib, transfMatrix] - transfAttrib: string containing all transformations as it is in the file - transfMatrix: numpy array with the resulting transformation matrix :rtype: tuple If the element does not have any transformation attribute, this function returns: - transfAttrib='' - transfMatrix=identity matrix """ transfAttrib = '' transfMatrix = np.eye(3) if 'transform' in element.attrib: transfAttrib = element.attrib['transform'] if not transfAttrib: return transfAttrib, transfMatrix # split operation into several strings listOperations = [e + ')' for e in transfAttrib.replace(',', ' ').split(')') if e != ""] for operation in listOperations: if 'translate' in operation: data = re.compile("translate\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values x = float(data[0]) y = float(data[1]) mat = np.array([[1, 0, x], [0, 1, y], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) if 'scale' in operation: data = re.compile("scale\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values scalex = float(data[0]) if len(data) == 2: scaley = float(data[1]) else: scaley = scalex mat = np.diag([scalex, scaley, 1]) transfMatrix = np.dot(transfMatrix, mat) if 'rotate' in operation: data = re.compile("rotate\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values angleRad = -float(data[0]) * np.pi / 180.0 # negative angle because inkscape is upside down =( matRot = np.array([[np.cos(angleRad), np.sin(angleRad), 0], [-np.sin(angleRad), np.cos(angleRad), 0], [0, 0, 1]]) if len(data) == 3: # must translate before and after rotation x = float(data[1]) y = float(data[2]) matBefore = np.array([[1, 0, x], [0, 1, y], [0, 0, 1]]) # translation before rotation matAfter = np.array([[1, 0, -x], [0, 1, -y], [0, 0, 1]]) # translation after rotation matRot = np.dot(matBefore, matRot) matRot = np.dot(matRot, matAfter) transfMatrix = np.dot(transfMatrix, matRot) if 'skewX' in operation: data = re.compile("skewX\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values angleRad = float(data[0]) * np.pi / 180.0 mat = np.array([[1, np.tan(angleRad), 0], [0, 1, 0], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) if 'skewY' in operation: data = re.compile("skewY\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values angleRad = float(data[0]) * np.pi / 180.0 mat = np.array([[1, 0, 0], [np.tan(angleRad), 1, 0], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) if 'matrix' in operation: data = re.compile("matrix\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values a = float(data[0]) b = float(data[1]) c = float(data[2]) d = float(data[3]) e = float(data[4]) f = float(data[5]) mat = np.array([[a, c, e], [b, d, f], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) return transfAttrib, transfMatrix # --------------------------------------------- def rotateElement(self, element, center, angleDeg): """Rotates the element using the transformation attribute. It is possible to rotate elements isolated or entire groups. :param element: element object to be rotated :param center: tuple with center point of rotation :param angleDeg: angle of rotation in degrees, counter-clockwise direction :type element: element object :type center: tuple :type angleDeg: float :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> self.rotateElement(line2,[0,0],120) # rotates line2 120 degrees around center x=0,y=0 >>> self.rotateElement(groupA,[1,1],-90) # rotates groupA -90 degrees around center x=1,y=1 """ transfString = '' if angleDeg == 0: return if 'transform' in element.attrib: transfString = element.attrib['transform'] # if transform attribute is present, we must add the new rotation if transfString: newTransform = 'rotate(%f %f %f) %s' % (-angleDeg, center[0], center[1], transfString) # negative angle bc inkscape is upside down else: # if no transform attribute was found newTransform = 'rotate(%f %f %f)' % (-angleDeg, center[0], center[1]) # negative angle bc inkscape is upside down element.attrib['transform'] = newTransform def copyElement(self, element, newParent, distance=None, angleDeg=None): """Copies one element to the same or other parent Element. It is possible to copy elements isolated or entire groups. :param element: element object to be copied :param newParent: New parent object. It can be another group or the group :param distance: tuple with the distance to move the object. If None, then the copy is placed at the same position :param angleDeg: angle of rotation in degrees, counter-clockwise direction :type element: element object :type newParent: element object :type distance: tuple :type angleDeg: float :returns: newElement :rtype: element object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> self.copyElement(line2,groupA) # create a copy of line2 in groupA >>> self.moveElement(groupA,[10,-10]) # moves line2 DeltaX=10, DdeltaY=-10 """ newElem = deepcopy(element) newParent.append(newElem) if distance is not None: self.moveElement(newElem, distance) if angleDeg is not None: self.rotateElement(newElem, self.getCenter(newElem), angleDeg) return newElem # --------------------------------------------- def moveElement(self, element, distance): """Moves the element using the transformation attribute. It is possible to move elements isolated or entire groups. :param element: element object to be rotated :param distance: tuple with the distance to move the object :type element: element object :type distance: tuple :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> self.moveElement(line2,[10,10]) # moves line2 DeltaX=10, DdeltaY=10 >>> self.moveElement(groupA,[10,-10]) # moves line2 DeltaX=10, DdeltaY=-10 """ if distance == 0: return transfString = '' if 'transform' in element.attrib: transfString = element.attrib['transform'] # if transform attribute is present, we must add the new translation if transfString: newTransform = 'translate(%f %f) %s ' % (distance[0], distance[1], transfString) else: # if no transform attribute was found newTransform = 'translate(%f %f)' % (distance[0], distance[1]) element.attrib['transform'] = newTransform # --------------------------------------------- def scaleElement(self, element, scaleX=1.0, scaleY=0.0, center=None): """Scales the element using the transformation attribute. It is possible to scale elements isolated or entire groups. :param element: element object to be rotated :param scaleX: scaling factor in X direction. Default=1.0 :param scaleY: scaling factor in Y direction. Default=0.0 :param center: center point considered as the origin for the scaling. Default=None. If None, the origin is adopted :type element: element object :type scaleX: float :type scaleX: float :type center: tuple :returns: nothing :rtype: - .. note:: If scaleY==0, then scaleY=scaleX is assumed (default behavior) **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> circ1 = centerRadius(groupA,centerPoint=[0,0],radius=1.0) # creates a line in groupA >>> circ2 = centerRadius(rootLayer,centerPoint=[0,0],radius=1.0) # creates a line in rootLayer >>> self.scaleElement(circ1,2.0) # scales x2 in both X and Y directions >>> self.scaleElement(circ1,2.0,3.0) # scales x2 in X and x3 in Y >>> self.scaleElement(groupA,0.5) # scales x0.5 the group in both X and Y directions """ if center is not None: self.moveElement(element, [-center[0], -center[1]]) transfString = '' if 'transform' in element.attrib: transfString = element.attrib['transform'] # if transform attribute is present, we must add the new translation if transfString: if scaleY != 0.0: newTransform = 'scale(%f %f) %s ' % (scaleX, scaleY, transfString) else: newTransform = 'scale(%f) %s' % (scaleX, transfString) else: # if no transform attribute was found if scaleY != 0.0: newTransform = 'scale(%f %f)' % (scaleX, scaleY) else: newTransform = 'scale(%f)' % scaleX element.attrib['transform'] = newTransform if center is not None: self.moveElement(element, [center[0], center[1]]) # --------------------------------------------- def findMarker(self, markerName): """Search for markerName in the document. :param markerName: marker name :type markerName: string :returns: True if markerName was found :rtype: bool """ for m in self.getDefsByTag(tag='marker'): if m.get('id') == markerName: return True return False # --------------------------------------------- def getPoints(self, element): """Retrieves the list of points of the element. This function works on paths, texts or groups. In the case of a group, the function will include recursively all its components :param element: element object :type element: element object :returns: list of points :rtype: list of list .. note:: This function will consider any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA >>> list = self.getPoints(line1) # gets list = [[0.0, 0.0], [5.0, 0.0], [5.0, 6.0]] """ # stores the list of coordinates listCoords = [] # check if element is valid. 'path', 'text' and 'g' are valid accepted_strings = set([inkex.addNS('path', 'svg'), inkex.addNS('text', 'svg'), 'g', 'path', 'use', inkex.addNS('use', 'svg')]) if element.tag not in accepted_strings: return listCoords if element.tag == inkex.addNS('path', 'svg') or element.tag == 'path': # if object is path # adds special character between letters and splits. the first regular expression excludes e and E bc they are used to represent scientific notation =S dString = re.sub('([a-df-zA-DF-Z])+?', r'#\1#', element.attrib['d']).replace('z', '').replace('Z', '').replace(',', ' ').split('#') dString = [i.lstrip() for i in dString] # removes leading spaces from strings dString = filter(None, dString) # removes empty elements Xcurrent = 0 Ycurrent = 0 while len(dString) > 0: commandType = dString[0] argument = [float(x) for x in dString[1].split()] # extracts arguments from M command and converts to float del dString[0] del dString[0] if commandType in 'mMlLtT': # extracts points from command 'move to' M/m or 'line to' l/L or 'smooth quadratic Bezier curveto't/T X = argument[0::2] # 2 parameters per segment, x is 1st Y = argument[1::2] # 2 parameters per segment, y is 2nd if commandType in 'hH': # extracts points from command 'horizontal line' h/H X = argument Y = [Ycurrent] * len(X) if commandType in 'vV': # extracts points from command 'vertical line' v/V Y = argument X = [Xcurrent] * len(Y) if commandType in 'cC': # extracts points from command 'Bezier Curve' c/C X = argument[4::6] # 6 parameters per segment, x is 5th Y = argument[5::6] # 6 parameters per segment, y is 6th if commandType in 'sSqQ': # extracts points from command 'quadratic Bezier Curve' q/Q or 'smooth curveto' s/S X = argument[2::4] # 4 parameters per segment, x is 3rd Y = argument[3::4] # 4 parameters per segment, y is 4th if commandType in 'aA': # extracts points from command 'arc' a/A X = argument[5::7] # 7 parameters per segment, x is 6th Y = argument[6::7] # 7 parameters per segment, y is 7th if commandType in 'h': # if h for i in range(0, len(X)): # convert to abs coordinates if i == 0: X[i] = X[i] + Xcurrent else: X[i] = X[i] + X[i - 1] if commandType in 'v': # if v for i in range(0, len(Y)): # convert to abs coordinates if i == 0: Y[i] = Y[i] + Ycurrent else: Y[i] = Y[i] + Y[i - 1] if commandType in 'mltcsqa': # if m or l for i in range(0, len(X)): # convert to abs coordinates if i == 0: X[i] = X[i] + Xcurrent Y[i] = Y[i] + Ycurrent else: X[i] = X[i] + X[i - 1] Y[i] = Y[i] + Y[i - 1] coords = zip(X, Y) listCoords.extend(coords) Xcurrent = X[-1] Ycurrent = Y[-1] if element.tag == inkex.addNS('text', 'svg'): # if object is a text x = float(element.attrib['x']) y = float(element.attrib['y']) coords = [[x, y]] listCoords.extend(coords) if element.tag == 'g': # if object is a group for obj in element.iterchildren("*"): if obj != element and obj.tag != 'defs': listPoints = self.getPoints(obj) listCoords.extend(listPoints) if element.tag == 'use' or element.tag == inkex.addNS('use', 'svg'): # if object is a use listCoordsTemp = [] x = float(element.attrib['x']) y = float(element.attrib['y']) link = self.getElemAttrib(element, 'xlink:href').replace('#','') elemLink = self.getElementById(link) for obj in elemLink.iter(): if obj != elemLink: listPoints = self.getPoints(obj) listCoordsTemp.extend(listPoints) #apply translation listCoords=[[coord[0]+x,coord[1]+y] for coord in listCoordsTemp] # apply transformation if len(listCoords)>0: # creates numpy array with the points to be transformed transfMat = self.getTransformMatrix(element)[1] coordsNP = np.hstack((np.array(listCoords), np.ones([len(listCoords), 1]))).transpose() coordsTransformed = np.dot(transfMat, coordsNP) coordsTransformed = np.delete(coordsTransformed, 2, 0).transpose().tolist() # remove last line, transposes and converts to list of lists else: coordsTransformed = [] return coordsTransformed # --------------------------------------------- def getBoundingBox(self, element): """Retrieves the bounding Box of the element. This function works on paths, texts or groups. In the case of a group, the function will consider recursively all its components :param element: element object :type element: element object :returns: two lists: [xMin,yMin] and [xMax,yMax] :rtype: list .. note:: This function will consider any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA >>> BboxMin,BboxMax = self.getBoundingBox(line1) # gets BboxMin = [0.0, 0.0] and BboxMax = [5.0, 6.0] """ coords = self.getPoints(element) coordsNP = np.array(coords) bboxMax = np.max(coordsNP, 0) bboxMin = np.min(coordsNP, 0) return bboxMin.tolist(), bboxMax.tolist() # --------------------------------------------- def getCenter(self, element): """Retrieves the center coordinates of the bounding Box of the element. This function works on paths, texts or groups. In the case of a group, the function will consider recursively all its components :param element: element object :type element: element object :returns: two lists: [xCenter,yCenter] :rtype: list .. note:: This function will consider any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkscapeMadeEasy_Draw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA >>> Center = self.getCenter(line1) # gets Center = [2.5, 3.0] """ bboxMin, bboxMax = self.getBoundingBox(element) bboxCenter = [(bboxMax[0] + bboxMin[0]) / 2, (bboxMax[1] + bboxMin[1]) / 2] return bboxCenter def getSegmentFromPoints(self, pointList, normalDirection='R'): """given two points of a straight line segment, returns the parameters of that segment: length, angle (in radians), tangent unitary vector and normal unitary vector :param pointList: start and end coordinates [ Pstart, Pend ] :param normalDirection: - 'R': normal vector points to the right of the tangent vector (Default) - 'L': normal vector points to the left of the tangent vector (Default) :type pointList: list of points :type normalDirection: string :returns: list: [length, theta, t_versor,n_versor] :rtype: list **Example** >>> segmentParam = getSegmentFromPoints([[1,1],[2,2]],'R') # returns [1.4142, 0.78540, [0.7071,0.7071], [0.7071,-0.7071] ] >>> segmentParam = getSegmentFromPoints([[1,1],[2,2]],'L') # returns [1.4142, 0.78540, [0.7071,0.7071], [-0.7071,0.7071] ] """ # tangent versor (pointing P2) P1 = np.array(pointList[0]) P2 = np.array(pointList[1]) t_vector = P2 - P1 length = np.linalg.norm(t_vector) t_versor = t_vector / length # normal vector: counter-clockwise with respect to tangent vector if normalDirection in 'rR': n_versor = np.array([t_versor[1], -t_versor[0]]) if normalDirection in 'lL': n_versor = np.array([-t_versor[1], t_versor[0]]) # angle theta = math.atan2(t_versor[1], t_versor[0]) return [length, theta, t_versor, n_versor] def getSegmentParameters(self, element, normalDirection='R'): """given a path segment composed by only two points, returns the parameters of that segment: length, angle (in radians), start point, end point, tangent unitary vector and normal unitary vector This function works with paths only. :param element: path element object :param normalDirection: - 'R': normal vector points to the right of the tangent vector (Default) - 'L': normal vector points to the left of the tangent vector (Default) :type element: element object :type normalDirection: string :returns: list: [Pstart,Pend,length, theta, t_versor,n_versor] :rtype: list If the element is not a path, the function returns an empty list If the path element has more than two points, the function returns an empty list .. note:: This function will consider any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkscapeMadeEasy_Draw.line.absCoords(rootLayer, [[1,1],[2,2]]) # creates a line in groupA >>> segementList = getSegmentParameters(line1,'R') # returns [[1,1], [2,2],1.4142, 0.78540, [0.7071,0.7071], [0.7071,-0.7071] ] """ # check if element is valid. 'path' accepted_strings = set([inkex.addNS('path', 'svg'), 'path']) if element.tag not in accepted_strings: return [] listPoints = self.getPoints(element) if len(listPoints) > 2: # if the path has more than two points return [] data = self.getSegmentFromPoints(listPoints, normalDirection) return listPoints + data blankSVG = """ image/svg+xml """ ================================================ FILE: 0.9x/inkscapeMadeEasy_Draw.py ================================================ #!/usr/bin/python # -------------------------------------------------------------------------------------- # # inkscapeMadeEasy: - Helper module that extends Aaron Spike's inkex.py module, # focusing productivity in inkscape extension development # # Copyright (C) 2016 by Fernando Moura # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # -------------------------------------------------------------------------------------- # Please uncomment (remove the # character) in the following line to disable LaTeX support via textext extension. # useLatex=False try: useLatex except NameError: useLatex = True else: useLatex = False import math import numpy as np import inkex import simplestyle if useLatex: import textextLib.textext as textext import sys import os import tempfile import copy """ This module contains a set of classes and some functions to help dealing with drawings. This module requires the following modules: inkex, math, simplestyle (from inkex module), numpy, lxml and sys """ def displayMsg(msg): """Displays a message to the user. :returns: nothing :rtype: - .. note:: Identical function has been also defined inside inkscapeMadeEasy class """ sys.stderr.write(msg + '\n') def Dump(obj, file='./dump_file.txt', mode='w'): """Function to easily output the result of ``str(obj)`` to a file This function was created to help debugging the code while it is running under inkscape. Since inkscape does not possess a terminal as today (2016), this function overcomes partially the issue of sending things to stdout by dumping result of the function ``str()`` in a text file. :param obj: object to sent to the file. Any type that can be used in ``str()`` :param file: file path. Default: ``./dump_file.txt`` :param mode: writing mode of the file. Default: ``w`` (write) :type obj: any, as long as ``str(obj``) is implemented (see ``__str__()`` metaclass definition ) :type file: string :type mode: string :returns: nothing :rtype: - .. note:: Identical function has been also defined inside inkscapeMadeEasy class **Example** >>> vector1=[1,2,3,4,5,6] >>> Dump(vector1,file='~/temporary.txt',mode='w') # writes the list to a file >>> vector2=[7,8,9,10] >>> Dump(vector2,file='~/temporary.txt',mode='a') # append the list to a file """ file = open(file, mode) file.write(str(obj) + '\n') file.close() class color(): """ Class to manipulate colors. This class manipulates color information, generating a string in inkscape's expected format ``#RRGGBB`` This class contains only static methods so that you don't have to inherit this in your class .. note:: alpha channel is not implemented yet. Assume alpha=1.0 """ @staticmethod def defined(colorName): """ Returns the color string representing a predefined color name :param colorName: color name :type colorName: string :returns: string representing the color in inkscape's expected format ``#RRGGBB`` :rtype: string **Available pre defined colors** .. image:: ../imagesDocs/Default_colors.png :width: 400px **Example** >>> colorString = color.defined('red') # returns #ff0000 representing red color """ if colorName not in ['Dred', 'red', 'Lred', 'Dblue', 'blue', 'Lblue', 'Dgreen', 'green', 'Lgreen', 'Dyellow', 'yellow', 'Lyellow', 'Dmagen', 'magen', 'Lmagen', 'black', 'white']: sys.exit("InkscapeDraw.color.defined() : Error. color -->" + colorName + "<-- not defined") if colorName == 'Dred': return '#800000' if colorName == 'red': return '#FF0000' if colorName == 'Lred': return '#FF8181' if colorName == 'Dblue': return '#000080' if colorName == 'blue': return '#0000FF' if colorName == 'Lblue': return '#8181FF' if colorName == 'Dgreen': return '#008000' if colorName == 'green': return '#00FF00' if colorName == 'Lgreen': return '#81FF81' if colorName == 'black': return '#000000' if colorName == 'white': return '#FFFFFF' if colorName == 'Dyellow': return '#808000' if colorName == 'yellow': return '#FFFF00' if colorName == 'Lyellow': return '#FFFF81' if colorName == 'Dmagen': return '#800080' if colorName == 'magen': return '#FF00FF' if colorName == 'Lmagen': return '#FF81FF' @staticmethod def RGB(RGBlist): """ returns a string representing a color specified by RGB level in the range 0-255 :param RGBlist: list containing RGB levels in the range 0-225 each :type RGBlist: list :returns: string representing the color in inkscape's expected format ``#RRGGBB`` :rtype: string **Example** >>> colorString = color.RGB([120,80,0]) # returns a string representing the color R=120, G=80, B=0 """ RGBhex = [''] * 3 for i in range(3): if RGBlist[i] > 255: RGBlist[i] = 255 if RGBlist[i] < 0: RGBlist[i] = 0 if RGBlist[i] < 16: RGBhex[i] = '0' + hex(int(RGBlist[i]))[2:].upper() else: RGBhex[i] = hex(int(RGBlist[i]))[2:].upper() return '#' + '%s%s%s' % (RGBhex[0], RGBhex[1], RGBhex[2]) # --------------------------------------------- @staticmethod def gray(percentage): """ returns a gray level compatible string based on white percentage between 0.0 and 1.0 if percentage is higher than 1.0, percentage is truncated to 1.0 (white) if percentage is lower than 0.0, percentage is truncated to 0.0 (black) :param percentage: value between 0.0 (black) and 1.0 (white) :type percentage: float :returns: string representing the color in inkscape's expected format ``#RRGGBB`` :rtype: string **Example** >>> colorString = color.gray(0.6) # returns a string representing the gray level with 60% of white """ RGBLevel = 255 * percentage if percentage > 1.0: RGBLevel = 255 if percentage < 0.0: RGBLevel = 0 return color.RGB([RGBLevel] * 3) # --------------------------------------------- @staticmethod def colorPickerToRGBalpha(colorPickerString): """ Function that converts the string returned by the widget 'color' in the .inx file into 2 strings, one representing the color in format ``#RRGGBB`` and the other representing the alpha channel ``AA`` :param colorPickerString: string returned by 'color' widget :type colorPickerString: string :returns: a list of strings: [color,alpha] - color: string in ``#RRGGBB`` format - alpha: string in ``AA`` format :rtype: list .. note:: For more information on this widget, see .. Warning:: you probably don't need to use this function. Consider using the method ``color.parseColorPicker()`` **usage** 1- in your inx file you must have one attribute of the type 'color':: 2- in your .py file, you must parse it as a string: >>> self.OptionParser.add_option("--myColorPicker", action="store", type="string", dest="myColorPickerVar", default='0') 3- call this function to convert so.myColorPickerVar to two strings - #RRGGBB with RGB values in hex - AA with alpha value in hex **Example** Let your .inx file contains a widget of type 'color' with the name myColorPicker:: Then in the .py file >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> inkex.Effect.__init__(self) >>> self.OptionParser.add_option("--myColorPicker", action="store", type="string", dest="myColorPickerVar", default='#000000') # parses the input parameter >>> >>> def effect(self): >>> color,alpha = inkDraw.color.colorPickerToRGBalpha(self.options.myColorPickerVar) # returns the string representing the selected color and alpha channel """ colorHex = hex(int(colorPickerString) & 0xffffffff)[2:].zfill( 8).upper() # [2:] removes the 0x , zfill adds the leading zeros, upper: uppercase RGB = '#' + colorHex[0:6] alpha = colorHex[6:] return [RGB, alpha] # --------------------------------------------- @staticmethod def parseColorPicker(stringColorOption, stringColorPicker): """ Function that converts the string returned by the widgets 'color' and 'optiongroup' in the .inx file into 2 strings, one representing the color in format ``#RRGGBB`` and the other representing the alpha channel ``AA`` You must have in your .inx both 'optiongroup' and 'color' widgets as defined below. You don't have to have all the color options presented in the example. That is the most complete example, considering the default colors in color.defined method. :param stringColorOption: string returned by 'optiongroup' widget :type stringColorOption: string :param stringColorPicker: string returned by 'color' widget :type stringColorPicker: string :returns: a list of strings: [color,alpha] - color: string in ``#RRGGBB`` format - alpha: string in ``AA`` format :rtype: list .. note:: For more information on this widget, see **Example** It works in the following manner: The user select in the optiongroup list the desired color. All pre defined colors are listed there. There is also a 'my default color' where you can set your preferred default color and a 'use color picker' to select from the color picker widget. Keep in mind that the selected color in this widget will be considered ONLY if 'use color picker' option is selected. Let your .inx file contains a widget of type 'color' with the name 'myColorPicker' and another 'optiongroup' with the name 'myColorOption':: <_option value="#FF0022">my default color <--you can set your pre define color in the form #RRGGBB <_option value="none">none <-- no color <_option value="black">black <_option value="red">red <_option value="blue">blue <_option value="yellow">yellow <_option value="green">green <-- these are all standardized colors in inkscapeMadeEasy_Draw.color class! <_option value="magen">magenta <_option value="white">white <_option value="Lred">Lred <_option value="Lblue">Lblue <_option value="Lyellow">Lyellow <_option value="Lgreen">Lgreen <_option value="Lmagen">Lmagenta <_option value="Dred">Dred <_option value="Dblue">Dblue <_option value="Dyellow">Dyellow <_option value="Dgreen">Dgreen <_option value="Dmagen">Dmagenta <_option value="picker">use color picker <-- indicate that the color must be taken from the colorPicker attribute Then in the .py file >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> inkex.Effect.__init__(self) >>> self.OptionParser.add_option("--myColorPicker", action="store", type="string", dest="myColorPickerVar", default='0') # parses the input parameters >>> self.OptionParser.add_option("--myColorOption", action="store", type="string", dest="myColorOptionVar", default='black') # parses the input parameter >>> >>> def effect(self): >>> so = self.options >>> [RGBstring,alpha] = inkDraw.color.parseColorPicker(so.myColorOptionVar,so.myColorPickerVar) """ alphaString = 'FF' if stringColorOption.startswith("#"): return [stringColorOption, alphaString] else: if stringColorOption == 'none': colorString = 'none' else: if stringColorOption == 'picker': [colorString, alphaString] = color.colorPickerToRGBalpha(stringColorPicker) else: colorString = color.defined(stringColorOption) return [colorString, alphaString] class marker(): """ Class to manipulate markers. This class is used to create new custom markers. Markers can be used with the lineStyle class to define line types that include start, mid and end markers This class contains only static methods so that you don't have to inherit this in your class """ # --------------------------------------------- @staticmethod def createMarker(ExtensionBaseObj, nameID, markerPath, RenameMode=0, strokeColor=color.defined('black'), fillColor=color.defined('black'), lineWidth=1.0, markerTransform=None): """Creates a custom line marker :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param nameID: nameID of the marker :param markerPath: path definition. Must follow 'd' attribute format. See for further information :param RenameMode: Renaming behavior mode - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified. - 1: overwrite marker definition if nameID is already taken - 2: Create a new unique nameID, adding a suffix number (Please refer to inkscapeMadeEasy.uniqueIdNumber(prefix_id) ). :param strokeColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fillColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param lineWidth: line width of the marker. Default: 1.0 :param markerTransform: custom transform applied to marker's path. Default: ``None`` the transform must follow 'transform' attribute format. See for further information :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type nameID: string :type markerPath: string :type RenameMode: int :type strokeColor: string :type fillColor: string :type lineWidth: float :type markerTransform: string :returns: NameID of the new marker :rtype: string **System of coordinates** The system of coordinates of the marker depends on the point under consideration. The following figure presents the coordinate system for all cases .. image:: ../imagesDocs/marker_Orientation.png :width: 600px **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> nameID='myMarker' >>> markerPath='M 3,0 L 0,1 L 0,-1 z' # defines a path forming an triangle with vertices (3,0) (0,1) (0,-1) >>> strokeColor=inkDraw.color.defined('red') >>> fillColor=None >>> RenameMode=1 >>> width=1 >>> markerTransform=None >>> markerID=inkDraw.marker.createMarker(self,nameID,markerPath,RenameMode,strokeColor,fillColor,width,markerTransform) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerEnd=markerID,lineColor=inkDraw.color.defined('black')) # see lineStyle class for further information on this function >>> >>> #tries to make another marker with the same nameID, changing RenameMode >>> strokeColor=inkDraw.color.defined('blue') >>> RenameMode=0 >>> markerID=inkDraw.marker.createMarker(self,nameID,RenameMode,scale,strokeColor,fillColor) # this will not modify the marker >>> RenameMode=1 >>> markerID=inkDraw.marker.createMarker(self,nameID,RenameMode,scale,strokeColor,fillColor) # modifies the marker 'myMarker' >>> RenameMode=2 >>> markerID=inkDraw.marker.createMarker(self,nameID,RenameMode,scale,strokeColor,fillColor) # creates a new marker with nameID='myMarker-0001' .. note:: In next versions, path definition and transformation will be modified to make it easier. =) """ # print tostring(ExtensionBaseObj.getDefinitions()) if RenameMode == 0 and ExtensionBaseObj.findMarker(nameID): return nameID if RenameMode == 2: numberID = 1 new_id = nameID + '_n%05d' % numberID while new_id in ExtensionBaseObj.doc_ids: numberID += 1 new_id = nameID + '_n%05d' % numberID ExtensionBaseObj.doc_ids[new_id] = 1 nameID = new_id if RenameMode == 1 and ExtensionBaseObj.findMarker(nameID): defs = ExtensionBaseObj.getDefinitions() for obj in defs.iter(): if obj.get('id') == nameID: defs.remove(obj) # creates a new marker marker_attribs = {inkex.addNS('stockid', 'inkscape'): nameID, 'orient': 'auto', 'refY': '0.0', 'refX': '0.0', 'id': nameID, 'style': 'overflow:visible'} newMarker = inkex.etree.SubElement(ExtensionBaseObj.getDefinitions(), inkex.addNS('marker', 'defs'), marker_attribs) if not fillColor: fillColor = 'none' if not strokeColor: strokeColor = 'none' marker_style = {'fill-rule': 'evenodd', 'fill': fillColor, 'stroke': strokeColor, 'stroke-width': str(lineWidth)} marker_lineline_attribs = {'d': markerPath, 'style': simplestyle.formatStyle(marker_style)} if markerTransform: marker_lineline_attribs['transform'] = markerTransform inkex.etree.SubElement(newMarker, inkex.addNS('path', 'defs'), marker_lineline_attribs) ExtensionBaseObj.doc_ids[nameID] = 1 # print tostring(ExtensionBaseObj.getDefinitions()) return nameID # --------------------------------------------- @staticmethod def createDotMarker(ExtensionBaseObj, nameID, RenameMode=0, scale=0.4, strokeColor=color.defined('black'), fillColor=color.defined('black')): """Creates a dotS/M/L marker, exactly like inkscape default markers :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param nameID: nameID of the marker :param RenameMode: Renaming behavior mode. For more information, see documentation of marker.createMarker(...) method. - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified - 1: overwrite marker if nameID is already taken - 2: Create a new unique nameID, adding a suffix number (Please refer to inkscapeMadeEasy.uniqueIdNumber(prefix_id) ). :param scale: scale of the marker. To copy exactly inkscape sizes dotS/M/L, use 0.2, 0.4 and 0.8 respectively. Default: 0.4 :param strokeColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fillColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :type fillColor: string :returns: NameID of the new marker :rtype: string **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> myMarker=inkDraw.marker.createDotMarker(self,nameID='myDotMarkerA',RenameMode=1,scale=0.5,strokeColor=inkDraw.color.defined('red'),fillColor=None) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerEnd=myMarker,lineColor=inkDraw.color.defined('black')) # see lineStyle class for further information on this function """ markerPath = 'M -2.5,-1.0 C -2.5,1.7600000 -4.7400000,4.0 -7.5,4.0 C -10.260000,4.0 -12.5,1.7600000 -12.5,-1.0 C -12.5,-3.7600000 -10.260000,-6.0 -7.5,-6.0 C -4.7400000,-6.0 -2.5,-3.7600000 -2.5,-1.0 z ' width = 1.0 markerTransform = 'scale(' + str(scale) + ') translate(7.4, 1)' return marker.createMarker(ExtensionBaseObj, nameID, markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) # --------------------------------------------- @staticmethod def createCrossMarker(ExtensionBaseObj, nameID, RenameMode=0, scale=0.4, strokeColor=color.defined('black'), fillColor=color.defined('black')): """Creates a cross marker :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param nameID: nameID of the marker :param RenameMode: Renaming behavior mode. For more information, see documentation of marker.createMarker(...) method. - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified - 1: overwrite marker if nameID is already taken - 2: Create a new unique nameID, adding a suffix number (Please refer to inkscapeMadeEasy.uniqueIdNumber(prefix_id) ). :param scale: scale of the marker. Default: 0.4 :param strokeColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fillColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :type fillColor: string :returns: NameID of the new marker :rtype: string **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> myMarker=inkDraw.marker.createCrossMarker(self,nameID='myDotMarkerA',RenameMode=1,scale=0.5,strokeColor=inkDraw.color.defined('red'),fillColor=None) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerEnd=myMarker,lineColor=inkDraw.color.defined('black')) # see lineStyle class for further information on this function """ markerPath = 'M -5,5 L 5,-5 M 5,5 L -5,-5' markerTransform = 'scale(' + str(scale) + ')' width = 1.0 return marker.createMarker(ExtensionBaseObj, nameID, markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) # --------------------------------------------- @staticmethod def createArrow1Marker(ExtensionBaseObj, nameID, RenameMode=0, scale=0.4, strokeColor=color.defined('black'), fillColor=color.defined('black')): """Creates a arrowS/M/L arrow markers (both start and end markers), exactly like inkscape :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param nameID: nameID of the marker. Start and End markers will have 'Start' and 'End' suffix respectively :param RenameMode: Renaming behavior mode. For more information, see documentation of marker.createMarker(...) method. - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified - 1: overwrite marker if nameID is already taken - 2: Create a new unique nameID, adding a suffix number (Please refer to inkscapeMadeEasy.uniqueIdNumber(prefix_id) ). :param scale: scale of the marker. Default: 0.4 :param strokeColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fillColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :type fillColor: string :returns: a list of strings: [startArrowMarker,endArrowMarker] - startArrowMarker: nameID of start marker - endArrowMarker: nameID of end marker :rtype: list **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> StartArrowMarker,EndArrowMarker=inkDraw.marker.createArrow1Marker(self,nameID='myArrow',RenameMode=1,scale=0.5,strokeColor=inkDraw.color.defined('red'),fillColor=None) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerStart=StartArrowMarker,markerEnd=EndArrowMarker,lineColor='#000000') # see lineStyle class for further information on this function """ # transform="scale(0.8) rotate(180) translate(12.5,0)" /> # transform="scale(0.4) rotate(180) translate(10,0)" /> # transform="scale(0.2) rotate(180) translate(6,0)" /> # translation=12.5-17.5/(scale*10) # linear regression from data of small medium and large translation = 10.17 * scale + 4.75 width = 1.0 markerPath = 'M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z ' markerTransform = 'scale(' + str(scale) + ') rotate(0) translate(' + str(translation) + ',0)' nameStart = marker.createMarker(ExtensionBaseObj, nameID + 'Start', markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) markerTransform = 'scale(' + str(scale) + ') rotate(180) translate(' + str(translation) + ',0)' nameEnd = marker.createMarker(ExtensionBaseObj, nameID + 'End', markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) return [nameStart, nameEnd] # --------------------------------------------- @staticmethod def createInfLineMarker(ExtensionBaseObj, nameID, RenameMode=0, scale=1.0, strokeColor=None, fillColor=color.defined('black')): """Creates ellipsis markers, both start and end markers. These markers differ from inkscape's default ellipsis since these markers are made such that the diameter of the dots are equal to the line width. :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param nameID: nameID of the marker. Start and End markers will have 'Start' and 'End' suffix respectively :param RenameMode: Renaming behavior mode. For more information, see documentation of marker.createMarker(...) method. - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified - 1: overwrite marker if nameID is already taken - 2: Create a new unique nameID, adding a suffix number (Please refer to inkscapeMadeEasy.uniqueIdNumber(prefix_id) ). :param scale: scale of the marker. Default 1.0 :param strokeColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: ``None`` :param fillColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :type fillColor: string :returns: a list of strings: [startInfMarker,endInfMarker] - startInfMarker: nameID of start marker - endInfMarker: nameID of end marker :rtype: list **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> startInfMarker,endInfMarker=inkDraw.marker.createInfLineMarker(self,nameID='myInfMarker',RenameMode=1,scale=1.0,strokeColor=None,fillColor='#00FF00') >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerStart=startInfMarker,markerEnd=endInfMarker,lineColor='#000000') # see lineStyle class for further information on this function """ # build path for 3 circles markerPath = '' radius = scale / 2.0 for i in range(3): prefix = 'M %f %f ' % (i * 2 + radius, 0) arcStringA = 'a %f %f 0 1 1 %f %f ' % (radius, radius, -2 * radius, 0) arcStringB = 'a %f %f 0 1 1 %f %f ' % (radius, radius, 2 * radius, 0) markerPath = markerPath + prefix + arcStringA + arcStringB + 'z ' if scale != 1.0: markerTransform = 'translate(' + str(-6.0 * scale) + ', 0) scale(' + str(scale) + ')' else: markerTransform = 'translate(' + str(-6.0 * scale) + ', 0)' width = 1.0 # add small line segment nameStart = marker.createMarker(ExtensionBaseObj, nameID + 'Start', markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) if scale != 1.0: markerTransform = 'translate(' + str(2.0 * scale) + ', 0) scale(' + str(scale) + ')' else: markerTransform = 'translate(' + str(2.0 * scale) + ', 0)' nameEnd = marker.createMarker(ExtensionBaseObj, nameID + 'End', markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) return [nameStart, nameEnd] class lineStyle(): """ Class to create line styles. This class is used to define line styles. It is capable of setting stroke and filling colors, line width, linejoin and linecap, markers (start, mid, and end) and stroke dash array This class contains only static methods so that you don't have to inherit this in your class """ # --------------------------------------------- @staticmethod def set(lineWidth=1.0, lineColor=color.defined('black'), fillColor=None, lineJoin='round', lineCap='round', markerStart=None, markerMid=None, markerEnd=None, strokeDashArray=None): """ Creates a new line style :param lineWidth: line width. Default: 1.0 :param lineColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fillColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: ``None`` :param lineJoin: shape of the lines at the joints. Valid values 'miter', 'round', 'bevel'. Default: round :param lineCap: shape of the lines at the ends. Valid values 'butt', 'square', 'round'. Default: round :param markerStart: marker at the start node. Default: ``None`` :param markerMid: marker at the mid nodes. Default: ``None`` :param markerEnd: marker at the end node. Default: ``None`` :param strokeDashArray: dashed line pattern definition. Default: ``None`` See for further information :type lineWidth: float :type lineColor: string :type fillColor: string :type lineJoin: string :type lineCap: string :type markerStart: string :type markerMid: string :type markerEnd: string :type strokeDashArray: string :returns: line definition following the provided specifications :rtype: string **Line node types** .. image:: ../imagesDocs/line_nodes.png :width: 600px **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> >>> # creates a line style using a dot marker at its end node >>> myMarker=inkDraw.marker.createDotMarker(self,nameID='myMarker',RenameMode=1,scale=0.5,strokeColor=color.defined('red'),fillColor=None) # see marker class for further information on this function >>> myLineStyle = inkDraw.lineStyle.set(lineWidth=1.0, markerEnd=myMarker,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color('red')) >>> >>> # creates a line style with dashed line (5 units dash , 10 units space >>> myDashedStyle = inkDraw.lineStyle.set(lineWidth=1.0,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color,strokeDashArray='5,10') >>> # creates a line style with a more complex pattern (5 units dash , 10 units space, 2 units dash, 3 units space >>> myDashedStyle = inkDraw.lineStyle.set(lineWidth=1.0,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color,strokeDashArray='5,10,2,3') """ if not fillColor: fillColor = 'none' if not lineColor: lineColor = 'none' if not strokeDashArray: strokeDashArray = 'none' # dictionary with the styles lineStyle = {'stroke': lineColor, 'stroke-width': str(lineWidth), 'stroke-dasharray': strokeDashArray, 'fill': fillColor} # Endpoint and junctions lineStyle['stroke-linecap'] = lineCap lineStyle['stroke-linejoin'] = lineJoin # add markers if needed if markerStart: lineStyle['marker-start'] = 'url(#' + markerStart + ')' if markerMid: lineStyle['marker-mid'] = 'url(#' + markerMid + ')' if markerEnd: lineStyle['marker-end'] = 'url(#' + markerEnd + ')' return lineStyle # --------------------------------------------- @staticmethod def setSimpleBlack(lineWidth=1.0): """Defines a standard black line style. The only adjustable parameter is its width. The fixed parameters are: lineColor=black, fillColor=None, lineJoin='round', lineCap='round', no markers, no dash pattern :param lineWidth: line width. Default: 1.0 :type lineWidth: float :returns: line definition following the provided specifications :rtype: string **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> >>> mySimpleStyle = inkDraw.lineStyle.setSimpleBlack(lineWidth=2.0) """ return lineStyle.set(lineWidth) class textStyle(): """ Class to create text styles. This class is used to define text styles. It is capable of setting font size, justification, text color, font family, font style, font weight, line spacing, letter spacing and word spacing This class contains only static methods so that you don't have to inherit this in your class """ # --------------------------------------------- @staticmethod def set(fontSize=10, justification='left', textColor=color.defined('black'), fontFamily='Sans', fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px'): """Defines a new text style :param fontSize: size of the font in px. Default: 10 :param justification: text justification. ``left``, ``right``, ``center``. Default: ``left`` :param textColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fontFamily: font family name. Default ``Sans`` :param fontStyle: ``normal`` or ``italic``. Default: ``normal`` :param fontWeight: ``normal`` or ``bold``. Default: ``normal`` :param lineSpacing: spacing between lines in percentage. Default: ``100%`` :param letterSpacing: extra space between letters. Format: ``_px``. Default: ``0px`` :param wordSpacing: extra space between words. Format: ``_px``. Default: ``0px`` :type fontSize: float :type justification: string :type textColor: string :type fontFamily: string :type fontStyle: string :type fontWeight: string :type lineSpacing: string :type letterSpacing: string :type wordSpacing: string :returns: text style definition following the provided specifications :rtype: string .. Warning: This method does NOT verify whether the font family is installed in your machine or not. **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> >>> myTextStyle=inkDraw.textStyle.set(fontSize=10, justification='left', textColor=color.defined('black'), fontFamily='Sans', >>> fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px') """ if not textColor: textColor = 'none' if justification == 'left': justification = 'start' anchor = 'start' if justification == 'right': justification = 'end' anchor = 'end' if justification == 'center': anchor = 'middle' textStyle = {'font-size': str(fontSize) + 'px', 'font-style': fontStyle, 'font-weight': fontWeight, 'text-align': justification, # start, center, end 'line-height': lineSpacing, 'letter-spacing': letterSpacing, 'word-spacing': wordSpacing, 'text-anchor': anchor, # start, middle, end 'fill': textColor, 'fill-opacity': '1', 'stroke': 'none', 'font-family': fontFamily} return textStyle # --------------------------------------------- @staticmethod def setSimpleBlack(fontSize=10, justification='left'): """Defines a standard black text style The only adjustable parameter are font size and justification. The fixed parameters are: textColor=color.defined('black'), fontFamily='Sans', fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px. :param fontSize: size of the font in px. Default: 10 :param justification: text justification. ``left``, ``right``, ``center``. Default: ``left`` :type fontSize: float :type justification: string :returns: line definition following the provided specifications :rtype: string **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> >>> mySimpleStyle = inkDraw.textStyle.setSimpleBlack(fontSize=20,justification='center') """ return textStyle.set(fontSize, justification) # --------------------------------------------- @staticmethod def setSimpleColor(fontSize=10, justification='left', textColor=color.defined('black')): """Defines a standard colored text style The only adjustable parameter are font size justification and textColor. The fixed parameters are: fontFamily='Sans', fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px. :param fontSize: size of the font in px. Default: 10 :param justification: text justification. ``left``, ``right``, ``center``. Default: ``left`` :param textColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :type fontSize: float :type justification: string :type textColor: string :returns: line definition following the provided specifications :rtype: string **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> >>> mySimpleStyle = inkDraw.textStyle.setSimpleColor(fontSize=20,justification='center',textColor=inkDraw.color.gray(0.5)) """ return textStyle.set(fontSize, justification, textColor) class text(): """ Class for writing texts. It is possible to add regular inkscape's text elements or LaTeX text. For the later, the excellent 'textext' extension from Pauli Virtanen's is incorporated here. Please refer to `Main Features`_ section for further instructions This class contains only static methods so that you don't have to inherit this in your class .. note:: LaTeX support is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. """ @staticmethod def write(ExtensionBaseObj, text, coords, parent, textStyle=textStyle.setSimpleBlack(fontSize=10, justification='left'), fontSize=None, justification=None, angleDeg=0.0): """Adds a text line to the document :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param text: text to be drawn. Use \\\\n in the string to start a new line :param coords: position [x,y] :param parent: parent object :param textStyle: text style to be used. See class ``textStyle``. Default: textStyle.setSimpleBlack(fontSize=10,justification='left') :param fontSize: size of the font in px. - ``None``: Uses fontSize of textStyle argument (Default) - number: takes precedence over the size on textStyle :param justification: text justification. ``left``, ``right``, ``center`` - ``None``: Uses justification of textStyle argument (Default) - ``left``, ``right``, ``center``: takes precedence over the justification set on textStyle :param angleDeg: angle of the text, counterclockwise, in degrees. Default: 0 :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type text: string :type coords: list :type parent: element object :type textStyle: textStyle object :type fontSize: float :type justification: string :type angleDeg: float :returns: the new text object :rtype: text Object **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> mySimpleStyle = inkDraw.textStyle.setSimpleBlack(fontSize=20,justification='center') # creates a simple text style. >>> >>> #adds a two-line text, at the point x=5.0,y=6.0 >>> # L1: 'foo bar who-hoo!' >>> # L2: 'second line!' >>> myText='foo bar who-hoo!\\ntwo lines!' >>> inkDraw.text.write(self, text=myText, coords=[5.0,6.0], parent=root_layer, textStyle=mySimpleStyle, fontSize=None, justification=None, angleDeg=0.0) >>> >>> # creates a group in root-layer and add text to it >>> myGroup = self.createGroup(root_layer,'textGroup') >>> #adds a text 'foo bar', rotated 45 degrees, at the point x=0,y=0, overriding justification of mySimpleStyle >>> inkDraw.text.write(self, text='foo bar', coords=[0.0,0.0], parent=myGroup, textStyle=mySimpleStyle, fontSize=None, justification='left', angleDeg=45.0) """ if justification == 'left': textStyle['text-align'] = 'start' textStyle['text-anchor'] = 'start' if justification == 'right': textStyle['text-align'] = 'end' textStyle['text-anchor'] = 'end' if justification == 'center': textStyle['text-align'] = 'center' textStyle['text-anchor'] = 'middle' if fontSize: textStyle['font-size'] = str(fontSize) + 'px' AttribsText = {inkex.addNS('space', 'xml'): "preserve", 'style': simplestyle.formatStyle(textStyle), 'x': str(coords[0]), 'y': str(coords[1]), inkex.addNS('linespacing', 'sodipodi'): textStyle['line-height']} # textObj = inkex.etree.SubElement(parent, inkex.addNS('text','svg'), AttribsText ) textObj = inkex.etree.Element(inkex.addNS('text', 'svg'), AttribsText) parent.append(textObj) AttribsLineText = {inkex.addNS('role', 'sodipodi'): "line", 'x': str(coords[0]), 'y': str(coords[1])} textLines = text.split('\\n') for n in range(len(textLines)): myTspan = inkex.etree.SubElement(textObj, inkex.addNS('tspan', 'svg'), AttribsLineText) myTspan.text = textLines[n].decode('utf-8') if angleDeg != 0: ExtensionBaseObj.rotateElement(textObj, center=coords, angleDeg=angleDeg) # negative angle bc inkscape is upside down return textObj # --------------------------------------------- @staticmethod def latex(ExtensionBaseObj, parent, LaTeXtext, position, fontSize=10, refPoint='cc', textColor=color.defined('black'), LatexCommands=' ', angleDeg=0, preambleFile=None): """Draws a text line using LaTeX. You can use any LaTeX contents here. .. note:: Employs the excellent 'textext' extension from Pauli Virtanen's is incorporated here. Please refer to `Main Features`_ section for further instructions .. note:: LaTeX support is an optional feature that requires a few extra packages to be installed outside inkscape. **It is enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. If disabled, this function will still work, internally calling the method text.write(). :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param parent: parent object :param LaTeXtext: text to be drawn. Can contain any latex command :param position: position of the reference point [x,y] :param fontSize: size of the font. Assume any text of ``\\normalsize`` will have this size. Default: 10 :param refPoint: text reference Point. See figure below for options. Default: ``cc`` :param textColor: color in the format ``#RRGGBB`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param LatexCommands: commands to be included before LaTeXtext (default: ' '). If LaTeX support is disabled, this parameter has no effect. :param angleDeg: angle of the text, counterclockwise, in degrees. Default: 0 :param preambleFile: optional preamble file to be included. Default: None. If LaTeX support is disabled, this parameter has no effect. :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type parent: element object :type LaTeXtext: string :type position: list :type fontSize: float :type refPoint: string :type textColor: string :type LatexCommands: string :type angleDeg: float :type preambleFile: string :returns: the new text object :rtype: text Object .. note:: This function does not use ``textStyle`` class. **Reference point options** .. image:: ../imagesDocs/LaTeX_reference_Point.png :width: 400px **Standard Preamble file** When a preamble file is not provided, inkscapeMadeEasy assumes a standard preamble file located at ``./textextLib/basicLatexPackages.tex``. By default, its contents is:: \\usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \\usepackage[per=slash]{siunitx} \\usepackage{steinmetz} \\usepackage[utf8]{inputenc} You will need these packages installed. This file can be modified to include extra default packages and/or commands. **LaTeX .tex document structure** LaTeX .tex document have the following structure. Note that LatexCommands lies within document environment:: \\documentclass[landscape,a0]{article} [contents of Preamble file] \\pagestyle{empty} \\begin{document} \\noindent [contents of LatexCommands] [contens of LaTeXtext] \\end{document} **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> customCommand = r'\\newcommand{\\fooBar}{\\textbf{Foo Bar Function! WhooHoo!}}' # do not forget the r to avoid backslash escape. >>> inkDraw.text.latex(self, root_layer,r'This is one equation \\begin{align} x=y^2\\end{align} And this is my \\fooBar{}', >>> position=[0.0,0.0], fontSize=10, refPoint='cc', textColor=inkDraw.color.defined('black'), LatexCommands=customCommand, angleDeg=0, preambleFile=None) """ newTmp = True # write an empty svg file. if not LaTeXtext: # check whether text is empty return 0 if useLatex: # set useLatex=False to replace latex by an standard text (much faster for debugging =) ) if newTmp: tmpf = tempfile.NamedTemporaryFile(mode='w', prefix='temp_svg_inkscapeMadeEasy_Draw_', suffix='.svg', delete=False) tempFilePath = tmpf.name tmpf.write(BlankSVG) tmpf.close() else: tempDir = tempfile.gettempdir() tempFilePath = tempDir + '/temp_svg_inkscapeMadeEasy_Draw.txt' Dump(BlankSVG,tempFilePath, 'w') # return # temp instance for determining font height. Draws a F letter just to find the height of the font if False: # turning off this part of the code. texTemp = textext.TexText() # start textText (awesome extension! =] ) texTemp.affect([r'--text=' + 'F', '--scale-factor=1', tempFilePath], output=False) groupLatex = texTemp.current_layer.find('g') BboxMin, BboxMax = ExtensionBaseObj.getBoundingBox(groupLatex) Height0 = BboxMax[1] - BboxMin[1] # running the code above, we get a 'F' with height of 6.76, with scale 1.0 from textext. This will be used to scale the text accordingly to fit user specification 'fontSize' Height0 = 6.76 scale = fontSize / Height0 tex = textext.TexText() # start textText (awesome extension! =] ) if preambleFile: tex.affect([r'--text=' + LatexCommands + LaTeXtext, '--scale-factor=1', '--preamble-file=' + preambleFile, tempFilePath], output=False) else: tex.affect( [r'--text=' + LatexCommands + LaTeXtext, '--scale-factor=1', '--preamble-file=' + ExtensionBaseObj.getBasicLatexPackagesFile(), tempFilePath], output=False) if newTmp: os.unlink(tmpf.name) groupLatex = tex.current_layer.find('g') # change color for obj in groupLatex.iter(): if obj.tag == inkex.addNS('path', 'svg') or obj.tag == 'path' or obj.tag == 'polygon': obj.set('style', 'fill:' + textColor + ';stroke-width:0') # remove transforms del groupLatex.attrib["transform"] ExtensionBaseObj.scaleElement(groupLatex, scaleX=scale, scaleY=-scale) # scale to fit font size else: if refPoint[1] == 'l': justification = 'left' if refPoint[1] == 'c': justification = 'center' if refPoint[1] == 'r': justification = 'right' mytextStyle = textStyle.setSimpleColor(fontSize=fontSize / 0.76, justification='left', textColor=textColor) groupLatex = text.write(ExtensionBaseObj, LaTeXtext, [0, 0], parent, textStyle=mytextStyle, fontSize=fontSize / 0.76, justification=justification, angleDeg=0.0) # attention! keep angleDeg=0.0 here bc it will be rotated below parent.append(groupLatex) BboxMin, BboxMax = ExtensionBaseObj.getBoundingBox(groupLatex) if useLatex: # set useLatex=False to replace latex by an standard text (much faster for debugging =) ) if refPoint[0] == 't': refPointY = BboxMin[1] # BboxMin bc inkscape is upside down if refPoint[0] == 'c': refPointY = (BboxMax[1] + BboxMin[1]) / 2.0 if refPoint[0] == 'b': refPointY = BboxMax[1] # BboxMax bc inkscape is upside down if refPoint[1] == 'l': refPointX = BboxMin[0] if refPoint[1] == 'c': refPointX = (BboxMax[0] + BboxMin[0]) / 2.0 if refPoint[1] == 'r': refPointX = BboxMax[0] else: refPointX = BboxMin[0] if refPoint[0] == 't': refPointY = BboxMin[1] - fontSize # BboxMin bc inkscape is upside down if refPoint[0] == 'c': refPointY = BboxMin[1] - fontSize / 2.0 # BboxMin bc inkscape is upside down if refPoint[0] == 'b': refPointY = BboxMax[1] # BboxMax bc inkscape is upside down ExtensionBaseObj.moveElement(groupLatex, [-refPointX, -refPointY]) # move to origin ExtensionBaseObj.moveElement(groupLatex, [position[0], position[1]]) if angleDeg != 0: ExtensionBaseObj.rotateElement(groupLatex, center=[position[0], position[1]], angleDeg=angleDeg) return groupLatex class cubicBezier(): """ This is a class with different methods for drawing cubic bezier lines. This class contains only static methods so that you don't have to inherit this in your class """ @staticmethod def addNode(NodeList, coord=[0, 0], cPbefore=[-1, 0], cPafter=[1, 0], typeNode='corner', flagAbsCoords=True): """Add a new node to the list of nodes of the cubic bezier line. .. warning:: Keep in mind that Inkscape's y axis is upside down! :param NodeList: list of nodes to be appended with the new node. :param coord: list with the coordinates of the node :param cPbefore: list with the coordinates of the control point before the node. :param cPafter: list with the coordinates of the control point after the node. Used only if 'typeNode' is 'smooth' or 'corner' :param typeNode: type of node to be added. See image below - ``corner``: Node without smoothness constraint at the node. The bezier curve can have a sharp edge at the node - ``smooth``: Node with smoothness constraint at the node. The bezier curve will be smooth at the node. If the control points do not form a straight line, then they are modified to form a straight line. See image below - ``symmetric``: same as ``smooth``, but the control points are forced to be symmetric with respect to the node. :param flagAbsCoords: indicate absolute or relative coordinates. See section below on how reference system works. .. warning:: All nodes in a given list must be defined in the same reference system (absolute or relative). :type NodeList: list :type coord: list [x,y] :type cPbefore: list [x,y] :type cPafter: list [x,y] :type typeNode: string :type flagAbsCoords: bool :returns: None :rtype: - **Node Types** The image below presents the types of nodes .. image:: ../imagesDocs/bezier_nodeTypes.png :width: 500px Image below present the process of smoothing control nodes not completely aligned when ``smooth`` is selected. .. image:: ../imagesDocs/bezier_smoothProcess.png :width: 500px **Absolute and relative coordinate systems** cubic bezier curves are composed by segments which are defined by 4 coordinates, two node coordinates and two control points. .. image:: ../imagesDocs/bezier_definitions.png :width: 500px In absolute coordinate system, all node and control point localizations are specified using the origin as reference. In relative coordinate system, control point localizations are specified using its node as reference, and each node use the previous node as reference (the first node use the origin as reference). See image below. .. warning:: Keep in mind that Inkscape's y axis is upside down! .. image:: ../imagesDocs/bezier_references.png :width: 700px **Example** .. note:: In the following example, the control point before the first node and after the last node are important when the bezier curve must be closed. See method ``draw`` .. image:: ../imagesDocs/bezier_example.png :width: 400px >>> # create a list of nodes using absolute coordinate system >>> nodeListABS=[] >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[4,4], cPbefore=[6,6], cPafter=[2,6], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[8,12], cPbefore=[4,12], cPafter=[10,12], typeNode='smooth', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,8], cPbefore=[8,8], cPafter=[12,10], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[16,8], cPbefore=[14,10], cPafter=None, typeNode='symmetric', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,4], cPbefore=[16,4], cPafter=[10,6], typeNode='corner', flagAbsCoords=True) >>> # create a list of nodes using relative coordinate system >>> nodeListREL=[] >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 4], cPbefore=[2,2], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 8], cPbefore=[-4,0], cPafter=[2,0], typeNode='smooth', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, -4], cPbefore=[-4,0], cPafter=[0,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 0], cPbefore=[-2,2], cPafter=None, typeNode='symmetric', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[-4,-4], cPbefore=[4,0], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) """ if typeNode.lower() == 'symmetric': typeNodeSodipodi = 'z' if typeNode.lower() == 'smooth': typeNodeSodipodi = 's' if typeNode.lower() == 'corner': typeNodeSodipodi = 'c' if typeNodeSodipodi.lower() == 'c': # corner NodeList.append({'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': cPafter, 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) if typeNodeSodipodi.lower() == 'z': # symmetric if flagAbsCoords: deltaX = coord[0] - cPbefore[0] deltaY = coord[1] - cPbefore[1] NodeList.append({'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': [coord[0] + deltaX, coord[1] + deltaY], 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) else: NodeList.append({'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': [-cPbefore[0],-cPbefore[1]], 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) if typeNodeSodipodi.lower() == 's': # smooth # projects the directions of the control points to a commom direction, perpendicular to both delta1 = np.array(cPbefore) delta2 = np.array(cPafter) if abs(delta1.dot(delta2)) <1.0: if flagAbsCoords: delta1 -= np.array(coord) delta2 -= np.array(coord) # https://math.stackexchange.com/questions/2285965/how-to-find-the-vector-formula-for-the-bisector-of-given-two-vectors bisectorVector = np.linalg.norm(delta2) * delta1 + np.linalg.norm(delta1) * delta2 tangentVersor = np.array([-bisectorVector[1], bisectorVector[0]]) tangentVersor /= np.linalg.norm(tangentVersor) cPbeforeNew = np.linalg.norm(delta1) * tangentVersor cPafterNew = np.linalg.norm(delta2) * tangentVersor if flagAbsCoords: cPbeforeNew += np.array(coord) cPafterNew += np.array(coord) NodeList.append({'node': coord, 'cPoint_before': cPbeforeNew.tolist(), 'cPoint_after': cPafterNew.tolist(), 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) else: NodeList.append({'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': cPafter, 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) @staticmethod def draw(parent, NodeList, offset=np.array([0, 0]), label='none', lineStyle=lineStyle.setSimpleBlack(), closePath=False): """draws the bezier line, given a list of nodes, built using ``addNode`` method :param parent: parent object :param NodeList: list of nodes. See ``addNode`` method :param offset: offset coords. Default [0,0] :param label: label of the line. Default 'none' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :param closePath: Connects the first point to the last. Default: False :type parent: inkscapeMadeEasy object (see example below) :type NodeList: list of nodes :type offset: list :type label: string :type lineStyle: lineStyle object :type closePath: bool :returns: the new line object :rtype: line Object **Example** .. note:: In the following example, the control point before the first node and after the last node are important when the bezier curve must be closed. .. image:: ../imagesDocs/bezier_example.png :width: 400px >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = set(lineWidth=1.0, lineColor=color.defined('red')) >>> # create a list of nodes using absolute coordinate system >>> nodeListABS=[] >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[4,4], cPbefore=[6,6], cPafter=[2,6], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[8,12], cPbefore=[4,12], cPafter=[10,12], typeNode='smooth', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,8], cPbefore=[8,8], cPafter=[12,10], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[16,8], cPbefore=[14,10], cPafter=None, typeNode='symmetric', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,4], cPbefore=[16,4], cPafter=[10,6], typeNode='corner', flagAbsCoords=True) >>> # create a list of nodes using relative coordinate system >>> nodeListREL=[] >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 4], cPbefore=[2,2], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 8], cPbefore=[-4,0], cPafter=[2,0], typeNode='smooth', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, -4], cPbefore=[-4,0], cPafter=[0,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 0], cPbefore=[-2,2], cPafter=None, typeNode='symmetric', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[-4,-4], cPbefore=[4,0], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) >>> C1 = inkDraw.cubicBezier.draw(root_layer,nodeListABS, offset=[0, 0],closePath=False) >>> C2 = inkDraw.cubicBezier.draw(root_layer,nodeListABS, offset=[0, 0],closePath=True) >>> C3 = inkDraw.cubicBezier.draw(root_layer,nodeListREL, offset=[0, 0],closePath=False) >>> C4 = inkDraw.cubicBezier.draw(root_layer,nodeListREL, offset=[0, 0],closePath=True) Result of the example .. image:: ../imagesDocs/bezier_example_draw.png :width: 800px """ # first node if NodeList[0]['absCoords']: string_coords = 'M %f,%f ' % (NodeList[0]['node'][0] + offset[0], NodeList[0]['node'][0] + offset[1]) else: string_coords = 'M %f,%f ' % (NodeList[0]['node'][0] + offset[0], NodeList[0]['node'][0] + offset[1]) string_nodeTypes = '' Ptotal=np.zeros(2) for i in range(len(NodeList) - 1): currNode = NodeList[i] nextNode = NodeList[i + 1] if currNode['absCoords']: bezier = 'C %f,%f ' % (currNode['cPoint_after'][0] + offset[0], currNode['cPoint_after'][1] + offset[1]) # first control point bezier += '%f,%f ' % (nextNode['cPoint_before'][0] + offset[0], nextNode['cPoint_before'][1] + offset[1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0] + offset[0], nextNode['node'][1] + offset[1]) # second node else: bezier = 'c %f,%f ' % (currNode['cPoint_after'][0], currNode['cPoint_after'][1]) # first control point bezier += '%f,%f ' % (nextNode['cPoint_before'][0] + nextNode['node'][0], nextNode['cPoint_before'][1] + nextNode['node'][1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0], nextNode['node'][1]) # second node Ptotal +=np.array(currNode['node']) string_nodeTypes += currNode['type'] string_coords = string_coords + bezier if closePath: currNode = NodeList[-1] nextNode = copy.deepcopy(NodeList[0]) if currNode['absCoords']: bezier = 'C %f,%f ' % (currNode['cPoint_after'][0] + offset[0], currNode['cPoint_after'][1] + offset[1]) # first control point bezier += '%f,%f ' % (nextNode['cPoint_before'][0] + offset[0], nextNode['cPoint_before'][1] + offset[1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0] + offset[0], nextNode['node'][1] + offset[1]) # second node else: # writes the coordinates of the first node, relative to the last node. Ptotal +=np.array(currNode['node']) nextNode['node'][0] = NodeList[0]['node'][0] - Ptotal[0] nextNode['node'][1] = NodeList[0]['node'][1] - Ptotal[1] bezier = 'c %f,%f ' % (currNode['cPoint_after'][0], currNode['cPoint_after'][1]) # first control point bezier += '%f,%f ' % (nextNode['cPoint_before'][0] + nextNode['node'][0], nextNode['cPoint_before'][1] + nextNode['node'][1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0], nextNode['node'][1]) # second node string_nodeTypes += currNode['type'] + nextNode['type'] string_coords = string_coords + bezier + ' Z' else: string_nodeTypes += currNode['type'] # M = move, L = line, H = horizontal line, V = vertical line, C = curve, S = smooth curve, # Q = quadratic Bezier curve, T = smooth quadratic Bezier curve, A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), 'd': string_coords, inkex.addNS('nodetypes', 'sodipodi'): string_nodeTypes} return inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) class line(): """ This is a class with different methods for drawing lines. This class contains only static methods so that you don't have to inherit this in your class """ @staticmethod def absCoords(parent, coordsList, offset=[0, 0], label='none', lineStyle=lineStyle.setSimpleBlack(), closePath=False): """Draws a (poly)line based on a list of absolute coordinates .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param coordsList: list with coords x and y. ex [[x1,y1], ..., [xN,yN]] :param offset: offset coords. Default [0,0] :param label: label of the line. Default 'none' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :param closePath: Connects the first point to the last. Default: False :type parent: inkscapeMadeEasy object (see example below) :type coordsList: list of list :type offset: list :type label: string :type lineStyle: lineStyle object :type closePath: bool :returns: the new line object :rtype: line Object **Example** .. image:: ../imagesDocs/lineExample.png :width: 250px >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = set(lineWidth=1.0, lineColor=color.defined('red')) >>> >>> >>> # creates a polyline passing by points (0,0) (0,1) (1,1) (1,2) (2,2) using absolute coordinates >>> coords=[ [0,0], [0,1], [1,1], [1,2], [2,2] ] >>> inkDraw.line.absCoords(root_layer, coordsList=coords, offset=[0, 0], label='fooBarLine', lineStyle=myLineStyle) >>> >>> # creates the same polyline translated to point (5,6). Note we just have to change the offset >>> inkDraw.line.absCoords(root_layer, coordsList=coords, offset=[5, 6], label='fooBarLine', lineStyle=myLineStyle) """ # string with coordinates string_coords = '' for point in coordsList: string_coords = string_coords + ' ' + str(point[0] + offset[0]) + ',' + str(point[1] + offset[1]) if closePath: string_coords += ' Z' # M = move, L = line, H = horizontal line, V = vertical line, C = curve, S = smooth curve, # Q = quadratic Bezier curve, T = smooth quadratic Bezier curve, A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), 'd': 'M ' + string_coords} return inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) # --------------------------------------------- @staticmethod def relCoords(parent, coordsList, offset=[0, 0], label='none', lineStyle=lineStyle.setSimpleBlack(), closePath=False): """Draws a (poly)line based on a list of relative coordinates .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param coordsList: list with distances dx and dy for all points. ex [[dx1,dy1], ..., [dxN,dyN]] :param offset: offset coords. Default [0,0] :param label: label of the line. Default 'none' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :param closePath: Connects the first point to the last. Default: False :type parent: inkscapeMadeEasy object (see example below) :type coordsList: list of list :type offset: list :type label: string :type lineStyle: lineStyle object :type closePath: bool :returns: the new line object :rtype: line Object **Example** .. image:: ../imagesDocs/lineExample.png :width: 250px >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = setSimpleBlack(lineWidth=1.0) >>> >>> >>> # creates a polyline passing by points (0,0) (0,1) (1,1) (1,2) (2,2) using relative coordinates >>> coords=[ [0,1], [1,0], [0,1], [1,0] ] >>> inkDraw.line.relCoords(root_layer, coordsList=coords, offset=[0, 0], label='fooBarLine', lineStyle=myLineStyle) >>> >>> # creates the same polyline translated to point (5,6) >>> inkDraw.line.relCoords(root_layer, coordsList=coords, offset=[5, 6], label='fooBarLine', lineStyle=myLineStyle) """ # string with coordinates string_coords = '' for dist in coordsList: string_coords = string_coords + ' ' + str(dist[0]) + ',' + str(dist[1]) if closePath: string_coords += ' Z' # M = move, L = line, H = horizontal line, V = vertical line, C = curve, S = smooth curve, # Q = quadratic Bezier curve, T = smooth quadratic Bezier curve, A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), 'd': 'm ' + str(offset[0]) + ' ' + str(offset[1]) + string_coords} return inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) class arc(): """ This is a class with different methods for drawing arcs. This class contains only static methods so that you don't have to inherit this in your class """ @staticmethod def startEndRadius(parent, Pstart, Pend, radius, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), flagRightOf=True, flagOpen=True, largeArc=False): """Draws a circle arc from ``Pstart`` to ``Pend`` with a given radius .. image:: ../imagesDocs/arc_startEndRadius.png :width: 80px :param parent: parent object :param Pstart: start coordinate [x,y] :param Pend: end coordinate [x,y] :param radius: arc radius :param offset: extra offset coords [x,y]. Default [0,0] :param label: label of the line. Default 'arc' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :param flagRightOf: sets the side of the vector Pend-Pstart which the arc must be drawn. See image below - True: Draws the arc to the right (Default) - False: Draws the arc to the left :param flagOpen: closes the arc. See image below. Default: True :param largeArc: Sets the largest arc to be drawn. See image below - True: Draws the largest arc - False: Draws the smallest arc (Default) :type parent: inkscapeMadeEasy object (see example below) :type Pstart: list :type Pend: list :type radius: float :type offset: list :type label: string :type lineStyle: lineStyle object :type flagRightOf: bool :type flagOpen: bool :type largeArc: bool :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/arc_startEndRadius_flags.png :width: 800px **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> P1=[10.0,0.0] >>> P2=[20.0,10.0] >>> R=15.0 >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws an opened arc >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[25,0], label='arc1', lineStyle=myLineStyle, flagOpen=True) >>> >>> #draws a closed arc >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[25,20], label='arc2', lineStyle=myLineStyle, flagOpen=False) >>> >>> #draws arcs with all combinations of flagRightOf and largeArc parameters >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[0,0], label='arc', lineStyle=myLineStyle, flagRightOf=True, largeArc=True) >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[25,0], label='arc4', lineStyle=myLineStyle, flagRightOf=False, largeArc=True) >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[0,40], label='arc5', lineStyle=myLineStyle, flagRightOf=True, largeArc=False) >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[25,40], label='arc6', lineStyle=myLineStyle, flagRightOf=False, largeArc=False) """ # finds the center point using some linear algebra StartVector = np.array(Pstart) EndVector = np.array(Pend) DistVector = EndVector - StartVector Dist = np.linalg.norm(DistVector) # distance between start and end if Dist > 2.0 * radius: return None if (flagRightOf and largeArc) or (not flagRightOf and not largeArc): RadiusDirection = np.array([-DistVector[1], DistVector[0]]) # perpendicular to DistVector else: RadiusDirection = np.array([DistVector[1], -DistVector[0]]) # perpendicular to DistVector RadiusDirection = RadiusDirection / np.linalg.norm(RadiusDirection) # normalize RadiusDirection CenterPoint = StartVector + DistVector / 2.0 + RadiusDirection * math.sqrt(radius ** 2.0 - (Dist / 2.0) ** 2.0) # computes the starting angle and ending angle temp = StartVector - CenterPoint AngStart = math.atan2(temp[1], temp[0]) temp = EndVector - CenterPoint AngEnd = math.atan2(temp[1], temp[0]) if flagRightOf: # inkscape does not follow svg path format to create arcs. It uses sodipodi which is weird =S sodipodiAngleStart = str(AngEnd) sodipodiAngleEnd = str(AngStart) else: sodipodiAngleStart = str(AngStart) sodipodiAngleEnd = str(AngEnd) # arc instructions if largeArc: largeArcFlag = 1 else: largeArcFlag = 0 if flagRightOf: sweepFlag = 0 else: sweepFlag = 1 arcString = ' a %f,%f 0 %d %d %f,%f' % (radius, radius, largeArcFlag, sweepFlag, EndVector[0] - StartVector[0], EndVector[1] - StartVector[1]) if not flagOpen: # option to close arc arcString = arcString + ' L ' + str(CenterPoint[0] + offset[0]) + ' ' + str(CenterPoint[1] + offset[1]) + ' z' # M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Bezier curve,T = smooth quadratic Bezier curveto,A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), inkex.addNS('type', 'sodipodi'): 'arc', inkex.addNS('rx', 'sodipodi'): str(radius), inkex.addNS('ry', 'sodipodi'): str(radius), inkex.addNS('cx', 'sodipodi'): str(CenterPoint[0] + offset[0]), inkex.addNS('cy', 'sodipodi'): str(CenterPoint[1] + offset[1]), inkex.addNS('start', 'sodipodi'): sodipodiAngleStart, inkex.addNS('end', 'sodipodi'): sodipodiAngleEnd, 'd': 'M ' + str(offset[0] + StartVector[0]) + ' ' + str(offset[1] + StartVector[1]) + arcString} if flagOpen: Attribs[inkex.addNS('open', 'sodipodi')] = 'true' return inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) # --------------------------------------------- @staticmethod def centerAngStartAngEnd(parent, centerPoint, radius, angStart, angEnd, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), flagOpen=True, largeArc=False): """Draws a circle arc given its center and start and end angles .. image:: ../imagesDocs/arc_centerAngStartAngEnd.png :width: 200px .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param centerPoint: center coordinate [x,y] :param radius: arc radius :param angStart: start angle in degrees :param angEnd: end angle in degrees :param offset: extra offset coords [x,y] :param label: label of the line. Default 'arc' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :param flagOpen: closes the arc. See image below. Default: True :param largeArc: Sets the largest arc to be drawn. See image below - True: Draws the largest arc - False: Draws the smallest arc (Default) :type parent: inkscapeMadeEasy object (see example below) :type centerPoint: list :type radius: float :type angStart: float :type angEnd: float :type offset: list :type label: string :type lineStyle: lineStyle object :type flagOpen: bool :type largeArc: bool :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/arc_centerAngStartAngEnd_flags.png :width: 700px **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws the shortest arc >>> inkDraw.arc.centerAngStartAngEnd(parent=root_layer, centerPoint=[0,0], radius=15.0, angStart=-10, angEnd=90, >>> offset=[0,0], label='arc1', lineStyle=myLineStyle, flagOpen=True,largeArc=False) >>> #draws the longest arc >>> inkDraw.arc.centerAngStartAngEnd(parent=root_layer, centerPoint=[0,0], radius=15.0, angStart=-10, angEnd=90, >>> offset=[30,0], label='arc1', lineStyle=myLineStyle, flagOpen=True,largeArc=True) """ Pstart = [radius * math.cos(math.radians(angStart)), radius * math.sin(math.radians(angStart))] Pend = [radius * math.cos(math.radians(angEnd)), radius * math.sin(math.radians(angEnd))] pos = [centerPoint[0] + offset[0], centerPoint[1] + offset[1]] if abs(angEnd - angStart) <= 180: flagRight = largeArc else: flagRight = not largeArc return arc.startEndRadius(parent, Pstart, Pend, radius, pos, label, lineStyle, flagRight, flagOpen, largeArc) class circle(): """ This is a class with different methods for drawing circles. This class contains only static methods so that you don't have to inherit this in your class """ @staticmethod def centerRadius(parent, centerPoint, radius, offset=[0, 0], label='circle', lineStyle=lineStyle.setSimpleBlack()): """draws a circle given its center point and radius .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param centerPoint: center coordinate [x,y] :param radius: circle's radius :param offset: extra offset coords [x,y] :param label: label of the line. Default 'circle' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscapeMadeEasy object (see example below) :type centerPoint: list :type radius: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new circle object :rtype: line Object **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws the shortest arc >>> inkDraw.circle.centerRadius(parent=root_layer, centerPoint=[0,0], radius=15.0, offset=[5,1], label='circle1', lineStyle=myLineStyle) """ # arc instructions arcStringA = ' a %f,%f 0 1 1 %f,%f' % (radius, radius, -2 * radius, 0) arcStringB = ' a %f,%f 0 1 1 %f,%f' % (radius, radius, 2 * radius, 0) # M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Bezier curve,T = smooth quadratic Bezier curveto,A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), inkex.addNS('type', 'sodipodi'): 'arc', inkex.addNS('rx', 'sodipodi'): str(radius), inkex.addNS('ry', 'sodipodi'): str(radius), inkex.addNS('cx', 'sodipodi'): str(centerPoint[0] + offset[0]), inkex.addNS('cy', 'sodipodi'): str(centerPoint[1] + offset[1]), inkex.addNS('start', 'sodipodi'): '0', inkex.addNS('end', 'sodipodi'): str(2 * math.pi), 'd': 'M ' + str(centerPoint[0] + offset[0] + radius) + ' ' + str( centerPoint[1] + offset[1]) + arcStringA + ' ' + arcStringB + ' z'} return inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) class rectangle(): """ This is a class with different methods for drawing rectangles. This class contains only static methods so that you don't have to inherit this in your class """ @staticmethod def widthHeightCenter(parent, centerPoint, width, height, radiusX=None, radiusY=None, offset=[0, 0], label='rectangle', lineStyle=lineStyle.setSimpleBlack()): """draws a rectangle given its center point and dimensions .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param centerPoint: center coordinate [x,y] :param width: dimension in X direction :param height: dimension in Y direction :param radiusX: rounding radius in X direction. If this value is ``None``, the rectangle will have sharp corners. Default: None :param radiusY: rounding radius in Y direction. If this value is ``None``, then radiusX will also be used in Y direction. If radiusX is also ``None``,then the rectangle will have sharp corners. Default: None :param offset: extra offset coords [x,y] :param label: label of the line. Default 'circle' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscapeMadeEasy object (see example below) :type centerPoint: list :type width: float :type height: float :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new rectangle object :rtype: rectangle Object **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws a 50x60 rectangle with radiusX=2.0 and radiusY=3.0 >>> inkDraw.rectangle.widthHeightCenter(parent=root_layer, centerPoint=[0,0], width=50, height=60, radiusX=2.0,radiusY=3.0, offset=[0,0], label='rect1', lineStyle=myLineStyle) """ x = centerPoint[0] - width / 2.0 + offset[0] y = centerPoint[1] - height / 2.0 + offset[1] Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), 'width': str(width), 'height': str(height), 'x': str(x), 'y': str(y), 'rx': str(radiusX), 'ry': str(radiusY)} if radiusX and radiusX > 0.0: Attribs['rx'] = str(radiusX) if radiusY is None: Attribs['ry'] = str(radiusX) else: if radiusY > 0.0: Attribs['ry'] = str(radiusY) return inkex.etree.SubElement(parent, inkex.addNS('rect', 'svg'), Attribs) @staticmethod def corners(parent, corner1, corner2, radiusX=None, radiusY=None, offset=[0, 0], label='rectangle', lineStyle=lineStyle.setSimpleBlack()): """draws a rectangle given the coordinates of two oposite corners .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param corner1: coordinates of corner 1 [x,y] :param corner2: coordinates of corner 1 [x,y] :param radiusX: rounding radius in X direction. If this value is ``None``, the rectangle will have sharp corners. Default: None :param radiusY: rounding radius in Y direction. If this value is ``None``, then radiusX will also be used in Y direction. If radiusX is also ``None``, then the rectangle will have sharp corners. Default: None :param offset: extra offset coords [x,y] :param label: label of the line. Default 'circle' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscapeMadeEasy object (see example below) :type corner1: list :type corner2: list :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new rectangle object :rtype: rectangle Object **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws a rectangle with corners C1=[1,5] and C2=[6,10], with radiusX=2.0 and radiusY=3.0 >>> inkDraw.rectangle.corners(parent=root_layer, corner1=[1,5], corner2=[6,10], radiusX=2.0,radiusY=3.0, offset=[0,0], label='rect1', lineStyle=myLineStyle) """ x = (corner1[0] + corner2[0]) / 2.0 y = (corner1[1] + corner2[1]) / 2.0 width = abs(corner1[0] - corner2[0]) height = abs(corner1[1] - corner2[1]) return rectangle.widthHeightCenter(parent, [x, y], width, height, radiusX, radiusY, offset, label, lineStyle) class ellipse(): """ This is a class with different methods for drawing ellipses. This class contains only static methods so that you don't have to inherit this in your class """ @staticmethod def centerRadius(parent, centerPoint, radiusX, radiusY, offset=[0, 0], label='circle', lineStyle=lineStyle.setSimpleBlack()): """draws an ellipse given its center point and radius .. warning:: Keep in mind that Inkscape's y axis is upside down! :param parent: parent object :param centerPoint: center coordinate [x,y] :param radiusX: circle's radius in x direction :param radiusY: circle's radius in y direction :param offset: extra offset coords [x,y] :param label: label of the line. Default 'circle' :param lineStyle: line style to be used. See class ``lineStyle``. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscapeMadeEasy object (see example below) :type centerPoint: list :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new ellipse object :rtype: line Object **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws the shortest arc >>> inkDraw.ellipse.centerRadius(parent=root_layer, centerPoint=[0,0], radiusX=15.0, radiusY=25.0, offset=[5,1], label='circle1', lineStyle=myLineStyle) """ # arc instructions arcStringA = ' a %f,%f 0 1 1 %f,%f' % (radiusX, radiusY, -2 * radiusX, 0) arcStringB = ' a %f,%f 0 1 1 %f,%f' % (radiusX, radiusY, 2 * radiusX, 0) # M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Bezier curve,T = smooth quadratic Bezier curveto,A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': simplestyle.formatStyle(lineStyle), inkex.addNS('type', 'sodipodi'): 'arc', inkex.addNS('rx', 'sodipodi'): str(radiusX), inkex.addNS('ry', 'sodipodi'): str(radiusY), inkex.addNS('cx', 'sodipodi'): str(centerPoint[0] + offset[0]), inkex.addNS('cy', 'sodipodi'): str(centerPoint[1] + offset[1]), inkex.addNS('start', 'sodipodi'): '0', inkex.addNS('end', 'sodipodi'): str(2 * math.pi), 'd': 'M ' + str(centerPoint[0] + offset[0] + radiusX) + ' ' + str( centerPoint[1] + offset[1]) + arcStringA + ' ' + arcStringB + ' z'} return inkex.etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) BlankSVG = r""" image/svg+xml """ ================================================ FILE: 0.9x/inkscapeMadeEasy_Plot.py ================================================ #!/usr/bin/python # -------------------------------------------------------------------------------------- # # inkscapeMadeEasy: - Helper module that extends Aaron Spike's inkex.py module, # focusing productivity in inkscape extension development # Copyright (C) 2016 by Fernando Moura # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # -------------------------------------------------------------------------------------- import math import sys import inkscapeMadeEasy_Draw as inkDraw """ This module contains a set of classes to help producing graphs. This module requires the following modules: math, numpy, lxml and sys """ def displayMsg(msg): """Displays a message to the user. :returns: nothing :rtype: - .. note:: Identical function has been also defined inside inkscapeMadeEasy class """ sys.stderr.write(msg + '\n') def Dump(obj, file='./dump_file.txt', mode='w'): """Function to easily output the result of ``str(obj)`` to a file This function was created to help debugging the code while it is running under inkscape. Since inkscape does not possess a terminal as today (2016), this function overcomes partially the issue of sending things to stdout by dumping result of the function ``str()`` in a text file. :param obj: object to sent to the file. Any type that can be used in ``str()`` :param file: file path. Default: ``./dump_file.txt`` :param mode: writing mode of the file. Default: ``w`` (write) :type obj: any, as long as ``str(obj``) is implemented (see ``__str__()`` metaclass definition ) :type arg2: string :type mode: string :returns: nothing :rtype: - .. note:: Identical function has been also defined inside inkscapeMadeEasy class **Example** >>> vector1=[1,2,3,4,5,6] >>> Dump(vector1,file='~/temporary.txt',mode='w') # writes the list to a file >>> vector2=[7,8,9,10] >>> Dump(vector2,file='~/temporary.txt',mode='a') # append the list to a file """ file = open(file, mode) file.write(str(obj) + '\n') file.close() def generateListOfTicksLinear(axisLimits, axisOrigin, tickStep): """Defines list of ticks to be drawn in a linear plot .. note:: Internal function. """ # make the list of ticks, symmetrically to the origin listTicksPositive = [axisOrigin] while listTicksPositive[-1] < axisLimits[1]: listTicksPositive.append(listTicksPositive[-1] + tickStep) listTicksNegative = [axisOrigin] while listTicksNegative[-1] > axisLimits[0]: listTicksNegative.append(listTicksNegative[-1] - tickStep) listTicks = listTicksPositive + listTicksNegative[1:] return listTicks def generateListOfTicksLog10(axisLimits): """Defines list of ticks to be drawn in a log10 plot .. note:: Internal function.""" # make the list of ticks, symmetrically to the origin listTicks = [axisLimits[0]] while listTicks[-1] < axisLimits[1]: listTicks.append(listTicks[-1] * 10) return listTicks def findOrigin(axisLimits, flagLog10, scale): """ retrieves the position of the origin. In case of logarithmic scale, it will be axisLimits[0] .. note:: Internal function. """ if flagLog10: axisOrigin = math.log10(axisLimits[0]) * scale else: if axisLimits[0] <= 0.0 and axisLimits[1] >= 0.0: axisOrigin = 0.0 else: if axisLimits[1] < 0: axisOrigin = axisLimits[1] * scale else: axisOrigin = axisLimits[0] * scale return axisOrigin def getPositionAndText(value, scale, flagLog10, axisUnitFactor): """given a value, its scale, and some flags, finds it position in the diagram and the text to be shown .. note:: Internal function.""" if flagLog10: pos = math.log10(value) * scale else: pos = value * scale # try to simplify number if int(value) - value == 0: valStr = str(int(round(value, 3))) else: valStr = str(round(value, 3)) # option to add extra factor to the axis ticks if flagLog10: exponent = str(int(math.log10(value))) if axisUnitFactor: if inkDraw.useLatex: Text = '10^{' + exponent + '}' + axisUnitFactor + '' else: Text = '10^' + exponent + '' + axisUnitFactor + '' else: if inkDraw.useLatex: Text = '10^{' + exponent + '}' else: Text = '10^' + exponent + '' else: if axisUnitFactor: if value == 0: Text = '0' if value == 1: Text = axisUnitFactor if value == -1: Text = '-' + axisUnitFactor if value != 0 and value != 1 and value != -1: Text = valStr + axisUnitFactor else: Text = valStr if inkDraw.useLatex: Text = '$' + Text + '$' return [pos, Text] class axis(): """ This is a class with different methods for making plot axes. This class contains only static methods so that you don't have to inherit this in your class .. note:: This class uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. """ @staticmethod def cartesian(ExtensionBaseObj, parent, xLim, yLim, position=[0, 0], xLabel='', yLabel='', xlog10scale=False, ylog10scale=False, xTicks=True, yTicks=True, xTickStep=1.0, yTickStep=1.0, xScale=20, yScale=20, xAxisUnitFactor='', yAxisUnitFactor='', xGrid=False, yGrid=False, forceTextSize=0, forceLineWidth=0, drawAxis=True, ExtraLenghtAxisX=0.0, ExtraLenghtAxisY=0.0): """Creates the axes for cartesian plot .. note:: This method uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param parent: parent object :param xLim: limits of the X axis [x_min,x_max]. If the axis is in log10 scale, then the limits will be rounded to complete one decade. :param yLim: limits of the Y axis [y_min,y_max]. If the axis is in log10 scale, then the limits will be rounded to complete one decade. :param position: position of the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. - The same rule applies to y direction. :param xLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param yLabel: Label of the Y axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param xlog10scale: sets X axis to log10 scale if True. Default: False :param ylog10scale: sets Y axis to log10 scale if True. Default: False :param xTicks: Adds axis ticks to the X axis if True. Default: True :param yTicks: Adds axis ticks to the Y axis if True. Default: True :param xTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param yTickStep: Value interval between two consecutive ticks on Y axis. (Not used if Y axis is in log10 scale). Default:1.0 :param xScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param yScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param xAxisUnitFactor: extra text to be added to Ticks in both x and y. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param yAxisUnitFactor: extra text to be added to the ticks in Y axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param xGrid: adds grid lines to X axis if true. Default: False :param yGrid: adds grid lines to Y axis if true. Default: False :param forceTextSize: Size of the text. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param forceLineWidth: Width of the lines. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param drawAxis: control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLenghtAxisX: extra length left near the arrow pointer of X axis. Default 0.0 :param ExtraLenghtAxisY: extra length left near the arrow pointer of Y axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type parent: inkscapeMadeEasy object (see example below) :type xLim: list :type yLim: list :type position: list :type xLabel: string :type yLabel: string :type xlog10scale: bool :type ylog10scale: bool :type xTicks: bool :type yTicks: bool :type xTickStep: float :type yTickStep: float :type xScale: float :type yScale: float :type xAxisUnitFactor: string :type yAxisUnitFactor: string :type xGrid: bool :type yGrid: bool :type forceTextSize: float :type forceLineWidth: float :type drawAxis: bool :type ExtraLenghtAxisX: float :type ExtraLenghtAxisY: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the axis area object (if drawAxis=False, this output is ``None``) - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list **Examples** .. image:: ../imagesDocs/plot_axisCartesianParameters_01.png :width: 800px """ if drawAxis: GroupPlot = ExtensionBaseObj.createGroup(parent, 'Plot') # sets the scale scaleX and scaleY stores the size of one unit in both axis (linear axis) or the size of a decade in log plots if xlog10scale: scaleX = xScale else: scaleX = xScale / float(xTickStep) if ylog10scale: scaleY = -yScale else: scaleY = -yScale / float(yTickStep) # negative bc inkscape is upside down # font size and other text parameters if forceTextSize == 0: textSize = 0.25 * min(xScale, yScale) else: textSize = forceTextSize textSizeSmall = 0.8 * textSize # font size for axis ticks text_offset = textSize # base space for text positioning ExtraSpaceArrowX = (2.0 + ExtraLenghtAxisX) * text_offset # extra space for drawing arrow on axis ExtraSpaceArrowY = (3.0 + ExtraLenghtAxisY) * text_offset # extra space for drawing arrow on axis lenghtTicks = textSize / 2.0 # length of the ticks # create styles if forceLineWidth == 0: lineWidth = min(xScale, yScale) / 35.0 else: lineWidth = forceLineWidth lineWidthGrid = 0.7 * lineWidth lineWidthGridFine = lineWidthGrid / 2.0 nameMarkerArrowAxis = inkDraw.marker.createArrow1Marker(ExtensionBaseObj, 'ArrowAxis', RenameMode=1, scale=0.4) lineStyleAxis = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3), markerEnd=nameMarkerArrowAxis[1]) lineStyleTicks = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3)) lineStyleGrid = inkDraw.lineStyle.set(lineWidthGrid, lineColor=inkDraw.color.gray(0.7)) lineStyleGridFine = inkDraw.lineStyle.set(lineWidthGridFine, lineColor=inkDraw.color.gray(0.7)) textStyleLarge = inkDraw.textStyle.setSimpleBlack(textSize) textStyleSmall = inkDraw.textStyle.setSimpleBlack(textSizeSmall, 'center') # check if limits are valid if xLim[0] >= xLim[1]: sys.stderr.write('Error: xLim is invalid.') return 0 if yLim[0] >= yLim[1]: sys.stderr.write('Error: yLim is invalid.') return 0 # check if the limits are valid for logarithmic scales. if xlog10scale: if xLim[0] <= 0 or xLim[1] <= 0: sys.stderr.write('Error: xLim is invalid in logarithmic scale') return 0 else: xmin = pow(10, math.floor(math.log10(xLim[0]))) xmax = pow(10, math.ceil(math.log10(xLim[1]))) xLimits = [xmin, xmax] else: xLimits = xLim if ylog10scale: if yLim[0] <= 0 or yLim[1] <= 0: sys.stderr.write('Error: yLim is invalid in logarithmic scale') return 0 else: ymin = pow(10, math.floor(math.log10(yLim[0]))) ymax = pow(10, math.ceil(math.log10(yLim[1]))) yLimits = [ymin, ymax] else: yLimits = yLim # finds the position of the Origin of axis axisOrigin = [0.0, 0.0] axisOrigin[0] = findOrigin(xLimits, xlog10scale, scaleX) axisOrigin[1] = findOrigin(yLimits, ylog10scale, scaleY) # computes the positions of the limits on svg, considering the scale if xlog10scale: # convert limits to position in diagram, including scaling factor xLimitsPos = [math.log10(x) * scaleX for x in xLimits] else: xLimitsPos = [x * scaleX for x in xLimits] if ylog10scale: # convert limits to position in diagram, including scaling factor yLimitsPos = [math.log10(y) * scaleY for y in yLimits] else: yLimitsPos = [y * scaleY for y in yLimits] # build the list of tuples with the limits of the plotting area outputLimits = zip([xLimits[0], xLimits[1], yLimits[0], yLimits[1]], [xLimitsPos[0] - axisOrigin[0] + position[0], xLimitsPos[1] - axisOrigin[0] + position[0], yLimitsPos[0] - axisOrigin[1] + position[1], yLimitsPos[1] - axisOrigin[1] + position[1]]) if not drawAxis: return [None, outputLimits, axisOrigin] # axis ticks groupTicks = ExtensionBaseObj.createGroup(GroupPlot, 'Ticks') if xTicks or xGrid: if xlog10scale: listTicks = generateListOfTicksLog10(xLimits) else: listTicks = generateListOfTicksLinear(xLimits, axisOrigin[0] / scaleX, xTickStep) for x in listTicks: if x <= xLimits[1] and x >= xLimits[0]: # get position, considering the scale and its text [posX, xText] = getPositionAndText(x, scaleX, xlog10scale, xAxisUnitFactor) if xGrid and posX != axisOrigin[0]: # grid lines. Do not draw if grid line is over the axis inkDraw.line.absCoords(groupTicks, [[posX, yLimitsPos[0]], [posX, yLimitsPos[1]]], [0, 0], lineStyle=lineStyleGrid) # intermediate grid lines in case of logarithmic scale if xGrid and xlog10scale and x < xLimits[1]: for i in range(2, 10): aditionalStep = math.log10(i) * scaleX inkDraw.line.absCoords(groupTicks, [[posX + aditionalStep, yLimitsPos[0]], [posX + aditionalStep, yLimitsPos[1]]], [0, 0], lineStyle=lineStyleGridFine) # tick if xTicks: if posX != axisOrigin[0]: # don't draw if in the origin inkDraw.line.relCoords(groupTicks, [[0, lenghtTicks]], [posX, axisOrigin[1] - lenghtTicks / 2.0], lineStyle=lineStyleTicks) # sets justification # inkDraw.text.write(ExtensionBaseObj,'orig='+str(axisOrigin),[axisOrigin[0]+10,axisOrigin[1]-30],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'xlim='+str(xLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-20],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'ylim='+str(yLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-10],groupTicks,fontSize=7) if axisOrigin[1] == yLimitsPos[0]: justif = 'tc' offsetX = 0 offsetY = text_offset / 2.0 # inkDraw.circle.centerRadius(groupTicks, axisOrigin, 10, [0,0]) if axisOrigin[1] != yLimitsPos[0] and axisOrigin[1] != yLimitsPos[1]: justif = 'tr' offsetX = -text_offset / 4.0 offsetY = text_offset / 2.0 # inkDraw.circle.centerRadius(groupTicks, axisOrigin, 10, [0,0]) # inkDraw.text.write(ExtensionBaseObj,str(axisOrigin[1]),[axisOrigin[0]+10,axisOrigin[1]+10],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,str(yLimitsPos[0]),[axisOrigin[0]+10,axisOrigin[1]+20],groupTicks,fontSize=7) if posX == axisOrigin[0]: if posX == xLimitsPos[1]: justif = 'tr' offsetX = -text_offset / 4.0 else: justif = 'tl' offsetX = +text_offset / 4.0 if axisOrigin[1] == yLimitsPos[1]: justif = 'bc' offsetX = 0 offsetY = -text_offset / 2.0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0]) if posX == axisOrigin[0]: if posX == xLimitsPos[1]: justif = 'br' offsetX = -text_offset / 4.0 else: justif = 'bl' offsetX = +text_offset / 4.0 # value if xTicks: inkDraw.text.latex(ExtensionBaseObj, groupTicks, xText, [posX + offsetX, axisOrigin[1] + offsetY], textSizeSmall, refPoint=justif) if yTicks or yGrid: # approximate limits to multiples of 10 if ylog10scale: listTicks = generateListOfTicksLog10(yLimits) else: listTicks = generateListOfTicksLinear(yLimits, axisOrigin[1] / scaleY, yTickStep) for y in listTicks: if y <= yLimits[1] and y >= yLimits[0]: # get position, considering the scale and its text [posY, yText] = getPositionAndText(y, abs(scaleY), ylog10scale, yAxisUnitFactor) posY = -posY if yGrid and posY != axisOrigin[1]: # grid lines. Do not draw if grid line is over the axis inkDraw.line.absCoords(groupTicks, [[xLimitsPos[0], posY], [xLimitsPos[1], posY]], [0, 0], lineStyle=lineStyleGrid) # intermediate grid lines in case of logarithmic scale if yGrid and ylog10scale and y < yLimits[1]: for i in range(2, 10): aditionalStep = math.log10(i) * scaleY inkDraw.line.absCoords(groupTicks, [[xLimitsPos[0], posY + aditionalStep], [xLimitsPos[1], posY + aditionalStep]], [0, 0], lineStyle=lineStyleGridFine) # tick if yTicks: if posY != axisOrigin[1]: # don't draw if in the origin inkDraw.line.relCoords(groupTicks, [[lenghtTicks, 0]], [axisOrigin[0] - lenghtTicks / 2.0, posY], lineStyle=lineStyleTicks) # sets justification # inkDraw.text.write(ExtensionBaseObj,'orig='+str(axisOrigin),[axisOrigin[0]+10,axisOrigin[1]-30],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'xlim='+str(xLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-20],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'ylim='+str(yLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-10],groupTicks,fontSize=7) if axisOrigin[0] == xLimitsPos[0]: justif = 'cr' offsetX = -text_offset / 2.0 offsetY = 0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0],'trash') if axisOrigin[0] != xLimitsPos[0] and axisOrigin[0] != xLimitsPos[1]: justif = 'tr' offsetX = -text_offset / 2.0 offsetY = text_offset / 4.0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0]) # inkDraw.text.write(ExtensionBaseObj,str(axisOrigin[0]),[axisOrigin[0]+10,axisOrigin[1]+10],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,str(yLimitsPos[0]*scaleX),[axisOrigin[0]+10,axisOrigin[1]+20],groupTicks,fontSize=7) if posY == axisOrigin[1]: if posY == yLimitsPos[1]: justif = 'tr' offsetY = text_offset / 4.0 else: justif = 'br' offsetY = -text_offset / 4.0 if axisOrigin[0] == xLimitsPos[1]: justif = 'cl' offsetX = text_offset / 2.0 offsetY = 0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0]) if posY == axisOrigin[1]: if posY == yLimitsPos[1]: justif = 'tl' offsetY = text_offset / 4.0 else: justif = 'bl' offsetY = -text_offset / 4.0 # value if yTicks: inkDraw.text.latex(ExtensionBaseObj, groupTicks, yText, [axisOrigin[0] + offsetX, (posY + offsetY)], textSizeSmall, refPoint=justif) ExtensionBaseObj.moveElement(GroupPlot, [position[0] - axisOrigin[0], position[1] - axisOrigin[1]]) # draw axis in the end so it stays on top of other objects GroupAxis = ExtensionBaseObj.createGroup(GroupPlot, 'Axis') inkDraw.line.absCoords(GroupAxis, [[xLimitsPos[0], 0], [xLimitsPos[1] + ExtraSpaceArrowX, 0]], [0, axisOrigin[1]], 'Xaxis', lineStyle=lineStyleAxis) if xLabel: # axis labels inkDraw.text.latex(ExtensionBaseObj, GroupAxis, xLabel, [xLimitsPos[1] + ExtraSpaceArrowX - text_offset / 3, axisOrigin[1] + text_offset / 2.0], textSize, refPoint='tl') inkDraw.line.absCoords(GroupAxis, [[0, yLimitsPos[0]], [0, yLimitsPos[1] - ExtraSpaceArrowY]], [axisOrigin[0], 0], 'Yaxis', lineStyle=lineStyleAxis) if yLabel: # axis labels inkDraw.text.latex(ExtensionBaseObj, GroupAxis, yLabel, [axisOrigin[0] + text_offset / 2.0, (yLimitsPos[1] - ExtraSpaceArrowY)], textSize, refPoint='tl') return [GroupPlot, outputLimits, axisOrigin] @staticmethod def polar(ExtensionBaseObj, parent, rLim, tLim=[0.0, 360.0], position=[0.0, 0.0], rLabel='', rlog10scale=False, rTicks=True, tTicks=True, rTickStep=1.0, tTickStep=45.0, rScale=20, rAxisUnitFactor='', rGrid=False, tGrid=False, forceTextSize=0, forceLineWidth=0, drawAxis=True, ExtraLenghtAxisR=0.0): """Creates the axes for polar plot .. note:: This method uses LaTeX in labels and tick marks if LaTeS support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param parent: parent object :param rLim: limits of the R axis [r_min,r_max]. If the axis is in log10 scale, then the limits will be rounded to complete one decade. :param tLim: limits of the theta axis [t_min,t_max]. Values in degrees. Default: [0,360] :param position: position of the center [x0,y0]. :param rLabel: Label of the R axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param rlog10scale: sets R axis to log10 scale if True. Default: False - If rlog10scale=True, then the lower limit of rLim must be >=1 :param rTicks: Adds axis ticks to the R axis if True. Default: True :param tTicks: Adds axis ticks to the theta axis if True. Default: True :param rTickStep: Value interval between two consecutive ticks on R axis. (Not used if R axis is in log10 scale). Default:1.0 :param tTickStep: Value interval between two consecutive ticks on theta axis. Default:45.0 :param rScale: Distance between each rTickStep in svg units. Default: 20 - If axis is linear, then rScale is the size in svg units of each tick - If axis is log10, the rScale is the size in svg units of one decade :param rAxisUnitFactor: extra text to be added to Ticks in both x and y. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param rGrid: adds grid lines to R axis if true. Default: False :param tGrid: adds grid lines to theta axis if true. Default: False :param forceTextSize: Size of the text. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param forceLineWidth: Width of the lines. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param drawAxis: control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLenghtAxisR: extra length between the R axis and its label. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type parent: inkscapeMadeEasy object (see example below) :type rLim: list :type tLim: list :type position: list :type rLabel: string :type rlog10scale: bool :type rTicks: bool :type tTicks: bool :type rTickStep: float :type tTickStep: float :type rScale: float :type rAxisUnitFactor: string :type rGrid: bool :type tGrid: bool :type forceTextSize: float :type forceLineWidth: float :type drawAxis: bool :type ExtraLenghtAxisR: float :returns: [GroupPlot, outputRLimits, axisOrigin] - GroupPlot: the axis area object (if drawAxis=False, this output is ``None``) - outputRLimits: a list with tuples:[(r_min,rPos_min),(r_max,rPos_max)] - r_min, r_max : The limits of the axis object - rPos_min, rPos_max : The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0] : A list with the coordinates of the point where the axes cross. :rtype: list **Examples** .. image:: ../imagesDocs/plot_axisPolarParameters_01.png :width: 800px """ if drawAxis: GroupPlot = ExtensionBaseObj.createGroup(parent, 'Plot') # sets the scale scaleX and scaleY stores the size of one unit in both axis (linear axis) or the size of a decade in log plots if rlog10scale: scaleR = rScale else: scaleR = rScale / float(rTickStep) # font size and other text parameters if forceTextSize == 0: textSize = 0.2 * rScale else: textSize = forceTextSize textSizeSmall = 0.8 * textSize # font size for axis ticks text_offset = textSize # base space for text positioning ExtraSpaceArrowR = (2.0 + ExtraLenghtAxisR) * text_offset # extra space for drawing arrow on axis lenghtTicks = textSize / 2.0 # length of the ticks # create styles if forceLineWidth == 0: lineWidth = rScale / 30.0 else: lineWidth = forceLineWidth lineWidthGrid = 0.7 * lineWidth lineWidthGridFine = lineWidthGrid / 2.0 # nameTickerArrowAxis = inkDraw.marker.createArrow1Marker(ExtensionBaseObj, 'ArrowAxis', RenameMode=1, scale=0.4) lineStyleAxis = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3)) lineStyleTicks = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3)) lineStyleGrid = inkDraw.lineStyle.set(lineWidthGrid, lineColor=inkDraw.color.gray(0.7)) lineStyleGridFine = inkDraw.lineStyle.set(lineWidthGridFine, lineColor=inkDraw.color.gray(0.7)) textStyleLarge = inkDraw.textStyle.setSimpleBlack(textSize) textStyleSmall = inkDraw.textStyle.setSimpleBlack(textSizeSmall, 'center') # check if limits are valid if rLim[0] < 0.0 or rLim[0] >= rLim[1]: sys.stderr.write('Error: rLim is invalid') return 0 if tLim[0] >= tLim[1]: sys.stderr.write('Error: tLim is invalid') return 0 # check if the limits are valid for logarithmic scales. if rlog10scale: if rLim[0] < 1 or rLim[1] < 1: sys.stderr.write('Error: rLim is invalid in logarithmic scale') return 0 else: rmin = pow(10, math.floor(math.log10(rLim[0]))) rmax = pow(10, math.ceil(math.log10(rLim[1]))) rLimits = [rmin, rmax] else: rLimits = rLim tLimits = tLim if abs(tLimits[1] - tLimits[0]) > 360: tLimits = [0, 360] if abs(tLimits[1] - tLimits[0]) > 180: largeArc = True else: largeArc = False # finds the position of the Origin of axis axisOrigin = [0.0, 0.0] axisOrigin[0] = findOrigin(rLimits, rlog10scale, scaleR) axisOrigin[1] = findOrigin(tLimits, False, 1.0) # computes the positions of the limits on svg, considering the scale if rlog10scale: # convert limits to position in diagram, including scaling factor rLimitsPos = [math.log10(x) * scaleR for x in rLimits] else: rLimitsPos = [x * scaleR for x in rLimits] # build the list of tuples with the limits of the plotting area outputLimits = zip([rLimits[0], rLimits[1]], [rLimitsPos[0] - axisOrigin[0] + position[0], rLimitsPos[1] - axisOrigin[0] + position[0]]) if not drawAxis: return [None, outputLimits, [0, 0]] # axis ticks groupTicks = ExtensionBaseObj.createGroup(GroupPlot, 'Ticks') if rTicks or rGrid: if rlog10scale: listTicks = generateListOfTicksLog10(rLimits) else: listTicks = generateListOfTicksLinear(rLimits, axisOrigin[0] / scaleR, rTickStep) for r in listTicks: if r <= rLimits[1] and r >= rLimits[0]: # get position, considering the scale and its text [posR, rText] = getPositionAndText(r, scaleR, rlog10scale, rAxisUnitFactor) if rGrid and posR > 0.0 and r > rLimits[0] and r < rLimits[1]: # grid lines. if tLimits[1] - tLimits[0] < 360: inkDraw.arc.centerAngStartAngEnd(groupTicks, [0, 0], posR, -tLimits[1], -tLimits[0], [0, 0], lineStyle=lineStyleGrid, largeArc=largeArc) # negative angles bc inkscape is upside down else: inkDraw.circle.centerRadius(groupTicks, [0, 0], posR, offset=[0, 0], lineStyle=lineStyleGrid) # intermediate grid lines in case of logarithmic scale if rGrid and rlog10scale and r < rLimits[1]: for i in range(2, 10): aditionalStep = math.log10(i) * scaleR if tLimits[1] - tLimits[0] < 360: inkDraw.arc.centerAngStartAngEnd(groupTicks, [0, 0], posR + aditionalStep, -tLimits[1], -tLimits[0], [0, 0], lineStyle=lineStyleGridFine, largeArc=largeArc) # negative angles bc inkscape is upside down else: inkDraw.circle.centerRadius(groupTicks, [0, 0], posR + aditionalStep, offset=[0, 0], lineStyle=lineStyleGridFine) # tick if rTicks and posR > 0.0: inkDraw.arc.centerAngStartAngEnd(groupTicks, [0, 0], posR, -tLimits[0] - math.degrees(lenghtTicks / float(posR * 2)), -tLimits[0] + math.degrees(lenghtTicks / float(posR * 2)), [0, 0], lineStyle=lineStyleTicks, largeArc=False) if rTicks and posR == 0.0: inkDraw.line.relCoords(groupTicks, [[0, lenghtTicks]], [0, - lenghtTicks / 2.0], lineStyle=lineStyleTicks) # sets justification # inkDraw.text.write(ExtensionBaseObj,'orig='+str(axisOrigin),[axisOrigin[0]+10,axisOrigin[1]-30],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'xlim='+str(xLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-20],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'ylim='+str(yLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-10],groupTicks,fontSize=7) if posR == 0: justif = 'cc' offsetX = 0 offsetY = text_offset * 1.2 posX = posR * math.cos(math.radians(-tLimits[0])) + offsetX posY = posR * math.sin(math.radians(-tLimits[0])) + offsetY else: offsetT = text_offset * 1.2 if tLimits[1] - tLimits[0] > 340: offsetR = text_offset / 2.0 else: offsetR = 0 justif = 'cc' posX = (posR + offsetR) * math.cos(math.radians(-tLimits[0])) + offsetT * math.sin(math.radians(tLimits[0])) posY = (posR + offsetR) * math.sin(math.radians(-tLimits[0])) + offsetT * math.cos(math.radians(-tLimits[0])) # value # inkDraw.circle.centerRadius(groupTicks,[posX,posY], 1) if rTicks: inkDraw.text.latex(ExtensionBaseObj, groupTicks, rText, [posX, posY], textSizeSmall, refPoint=justif) if tTicks or tGrid: listTicks = generateListOfTicksLinear(tLimits, axisOrigin[1], tTickStep) for t in listTicks: if t <= tLimits[1] and t >= tLimits[0]: c = math.cos(math.radians(-t)) # negative angles bc inkscape is upside down s = math.sin(math.radians(-t)) # negative angles bc inkscape is upside down # get position, considering the scale and its text if inkDraw.useLatex: tText = '$' + str(t) + '$' else: tText = str(t) if (tGrid and t > tLimits[0] and t < tLimits[1]) or (tGrid and t == tLimits[0] and tLimits[1] - tLimits[0] >= 360): if rLimitsPos[0] == 0: # if rmin is zero, then make the lines to reach the center if not rlog10scale: P1 = [(rLimitsPos[0] + scaleR * rTickStep / 2) * c, (rLimitsPos[0] + scaleR * rTickStep / 2) * s] else: P1 = [(rLimitsPos[0] + 0.3 * scaleR) * c, (rLimitsPos[0] + 0.3 * scaleR) * s] else: P1 = [rLimitsPos[0] * c, rLimitsPos[0] * s] P2 = [rLimitsPos[1] * c, rLimitsPos[1] * s] inkDraw.line.absCoords(groupTicks, [P1, P2], [0, 0], lineStyle=lineStyleGrid) # tick if (tTicks and t != tLimits[1]) or (tTicks and t == tLimits[1] and tLimits[1] - tLimits[0] < 360): P1 = [(rLimitsPos[1] - lenghtTicks / 2.0) * c, (rLimitsPos[1] - lenghtTicks / 2.0) * s] inkDraw.line.relCoords(groupTicks, [[lenghtTicks * c, lenghtTicks * s]], P1, lineStyle=lineStyleTicks) if c > 1.0e-4: justif = 'cl' else: if c < -1.0e-4: justif = 'cr' else: justif = 'cc' offsetR = text_offset posX = (rLimitsPos[1] + offsetR) * c posY = (rLimitsPos[1] + offsetR) * s # value if (tTicks and t != tLimits[1]) or (tTicks and t == tLimits[1] and tLimits[1] - tLimits[0] < 360): inkDraw.text.latex(ExtensionBaseObj, groupTicks, tText, [posX, posY], textSizeSmall, refPoint=justif) ExtensionBaseObj.moveElement(GroupPlot, position) # draw axis in the end so it stays on top of other objects GroupAxis = ExtensionBaseObj.createGroup(GroupPlot, 'Axis') c0 = math.cos(math.radians(-tLimits[0])) # negative angles bc inkscape is upside down s0 = math.sin(math.radians(-tLimits[0])) # negative angles bc inkscape is upside down c1 = math.cos(math.radians(-tLimits[1])) # negative angles bc inkscape is upside down s1 = math.sin(math.radians(-tLimits[1])) # negative angles bc inkscape is upside down P1 = [rLimitsPos[0] * c0, rLimitsPos[0] * s0] P2 = [rLimitsPos[1] * c0, rLimitsPos[1] * s0] P3 = [rLimitsPos[1] * c1, rLimitsPos[1] * s1] P4 = [rLimitsPos[0] * c1, rLimitsPos[0] * s1] if tLimits[1] - tLimits[0] < 360: inkDraw.line.absCoords(GroupAxis, [P1, P2], [0, 0], lineStyle=lineStyleAxis) inkDraw.line.absCoords(GroupAxis, [P3, P4], [0, 0], lineStyle=lineStyleAxis) else: if rTicks: inkDraw.line.absCoords(GroupAxis, [P1, P2], [0, 0], lineStyle=lineStyleAxis) if tLimits[1] - tLimits[0] < 360: if rLimitsPos[0] > 0: inkDraw.arc.startEndRadius(GroupAxis, P1, P4, rLimitsPos[0], offset=[0, 0], lineStyle=lineStyleAxis, flagRightOf=True, flagOpen=True, largeArc=largeArc) inkDraw.arc.startEndRadius(GroupAxis, P2, P3, rLimitsPos[1], offset=[0, 0], lineStyle=lineStyleAxis, flagRightOf=True, flagOpen=True, largeArc=largeArc) else: if rLimitsPos[0] > 0: inkDraw.circle.centerRadius(GroupAxis, [0, 0], rLimitsPos[0], offset=[0, 0], lineStyle=lineStyleAxis) inkDraw.circle.centerRadius(GroupAxis, [0, 0], rLimitsPos[1], offset=[0, 0], lineStyle=lineStyleAxis) if rLabel: # axis labels c0 = math.cos(math.radians(-tLimits[0]) + text_offset / rLimitsPos[1]) # negative angles bc inkscape is upside down s0 = math.sin(math.radians(-tLimits[0]) + text_offset / rLimitsPos[1]) # negative angles bc inkscape is upside down posText = [(rLimitsPos[1] + ExtraSpaceArrowR) * c0, (rLimitsPos[1] + ExtraSpaceArrowR) * s0] inkDraw.text.latex(ExtensionBaseObj, GroupAxis, rLabel, posText, textSize, refPoint='cl') return [GroupPlot, outputLimits, [0, 0]] class plot(): """ This is a class with different methods for making plots. This class contains only static methods so that you don't have to inherit this in your class .. note:: This class uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. """ @staticmethod def cartesian(ExtensionBaseObj, parent, xData, yData, position=[0, 0], xLabel='', yLabel='', xlog10scale=False, ylog10scale=False, xTicks=True, yTicks=True, xTickStep=1.0, yTickStep=1.0, xScale=20, yScale=20, xExtraText='', yExtraText='', xGrid=False, yGrid=False, generalAspectFactorAxis=1.0, lineStylePlot=inkDraw.lineStyle.setSimpleBlack(), forceXlim=None, forceYlim=None, drawAxis=True, ExtraLenghtAxisX=0.0, ExtraLenghtAxisY=0.0): """Cartesian Plot .. note:: This method uses LaTeX in labels and tick marks if the support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param parent: parent object :param xData: list of x data :param yData: list of y data :param position: position of the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. :param xLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param yLabel: Label of the Y axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param xlog10scale: sets X axis to log10 scale if True. Default: False :param ylog10scale: sets Y axis to log10 scale if True. Default: False :param xTicks: Adds axis ticks to the X axis if True. Default: True :param yTicks: Adds axis ticks to the Y axis if True. Default: True :param xTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param yTickStep: Value interval between two consecutive ticks on Y axis. (Not used if Y axis is in log10 scale). Default:1.0 :param xScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param yScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then yScale is the size in svg units of each tick - If axis is log10, the yScale is the size in svg units of one decade :param xExtraText: extra text to be added to Ticks in both x and y. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param yExtraText: extra text to be added to the ticks in Y axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param xGrid: adds grid lines to X axis if true. Default: False :param yGrid: adds grid lines to Y axis if true. Default: False :param generalAspectFactorAxis: regulates the general aspect ratio between grid lines, text and Ticks separations. Default: 1.0 :param lineStylePlot: line style to be used to plot the data. See class ``inkscapeMadeEasy_Draw.lineStyle``. Default: lineStylePlot=inkDraw.lineStyle.setSimpleBlack() :param forceXlim: forces limits of X axis to these limits. These limits affect the axis only, that is, all xData is plotted despite of these limits. Obs: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale - if forceXlim=None Limits will be defined by min and max of xData (Default) - if forceXlim=[xMin,xMax] then these limits will be used. :param forceYlim: forces limits of Y axis to these limits. These limits affect the axis only, that is, all yData is plotted despite of these limits. Obs: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale - if forceYlim=None Limits will be defined by min and max of yData (Default) - if forceYlim=[yMin,yMax] then these limits will be used. :param drawAxis: control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLenghtAxisX: extra length left near the arrow pointer of X axis. Default 0.0 :param ExtraLenghtAxisY: extra length left near the arrow pointer of Y axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type parent: inkscapeMadeEasy object (see example below) :type xData: list :type yData: list :type position: list :type xLabel: string :type yLabel: string :type xlog10scale: bool :type ylog10scale: bool :type xTicks: bool :type yTicks: bool :type xTickStep: float :type yTickStep: float :type xScale: float :type yScale: float :type xExtraText: string :type yExtraText: string :type xGrid: bool :type yGrid: bool :type generalAspectFactorAxis: float :type lineStylePlot: lineStyle object :type forceXlim: list :type forceYlim: list :type drawAxis: bool :type ExtraLenghtAxisX: float :type ExtraLenghtAxisY: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the plot object - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list .. note:: If any of the axis are log10, then the method ignores any pairs of (x,y) data with invalid coordinates, that is, if xData and/or yData is less than or equal to 0.0 (they would result in complex log10... =P ). The method will create a text object alongside your plot warning this. .. note:: If any of the axis are linear, the method will ignore any value greater than 10.000 (in absolute value). This avoids plotting too big numbers (basically inf =) ). The method will create a text object alongside your plot warning this. **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> import inkscapeMadeEasy_Plot as inkPlot >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> xData=[-1,-0.5,0,0.5,1.0,1.5,2] >>> yData=[x*x for x in xData] # computes y=x*x >>> >>> myMarkerDot=inkDraw.marker.createDotMarker(self,'DotM',RenameMode=2,scale=0.3,strokeColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black')) >>> lineStyleDiscrete = inkDraw.lineStyle.set(lineWidth=1.0, markerStart=myMarkerDot,markerMid=myMarkerDot,markerEnd=myMarkerDot) >>> >>> inkPlot.plot.cartesian(self,root_layer,xData,yData,position=[0,0], >>> xLabel='my $x$ data',yLabel='$y(x)$',xlog10scale=False,ylog10scale=False, >>> xTicks=True,yTicks=True,xTickStep=0.5,yTickStep=2.0, >>> xScale=20,yScale=10,xExtraText='a',yExtraText='', >>> xGrid=True,yGrid=True,generalAspectFactorAxis=1.0,lineStylePlot=lineStyleDiscrete, >>> forceXlim=None,forceYlim=None,drawAxis=True) .. image:: ../imagesDocs/plot_plotCartesianParameters_01.png :width: 800px """ textSize = generalAspectFactorAxis * 0.25 * min(xScale, yScale) lineWidthAxis = generalAspectFactorAxis * min(xScale, yScale) / 35.0 yDataTemp = [] xDataTemp = [] flagShowedError = False if xlog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(xData)): if xData[i] > 0.0: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(xData)): if abs(xData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp yDataTemp = [] xDataTemp = [] flagShowedError = False if ylog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(yData)): if yData[i] > 0.0: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(yData)): if abs(yData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp if forceXlim: Xlimits = forceXlim else: Xlimits = [min(xData), max(xData)] if forceYlim: Ylimits = forceYlim else: Ylimits = [min(yData), max(yData)] # min<->max inverted bc inkscape is upside down if Ylimits[0] == Ylimits[1]: if Ylimits[0] > 0: Ylimits[0] = 0 if Ylimits[0] == 0: Ylimits[1] = 1 if Ylimits[0] < 0: Ylimits[1] = 0 if Xlimits[0] == Xlimits[1]: if Xlimits[0] > 0: Xlimits[0] = 0 if Xlimits[0] == 0: Xlimits[1] = 1 if Xlimits[0] < 0: Xlimits[1] = 0 # draw axis axisGroup = ExtensionBaseObj.createGroup(parent, 'PlotData') [axisObj, limits, origin] = axis.cartesian(ExtensionBaseObj, axisGroup, Xlimits, Ylimits, position, xLabel=xLabel, yLabel=yLabel, xlog10scale=xlog10scale, ylog10scale=ylog10scale, xTicks=xTicks, yTicks=yTicks, xTickStep=xTickStep, yTickStep=yTickStep, xScale=xScale, yScale=yScale, xAxisUnitFactor=xExtraText, yAxisUnitFactor=yExtraText, xGrid=xGrid, yGrid=yGrid, forceTextSize=textSize, forceLineWidth=lineWidthAxis, drawAxis=drawAxis, ExtraLenghtAxisX=ExtraLenghtAxisX, ExtraLenghtAxisY=ExtraLenghtAxisY) # scales data and convert to logarithmic scale if needed. Also subtracts the origin point of the axis to move the plot to the correct position if xlog10scale: xData = [math.log10(x) * xScale - origin[0] for x in xData] else: xData = [x * (xScale / xTickStep) - origin[0] for x in xData] if ylog10scale: yData = [-math.log10(y) * yScale - origin[1] for y in yData] else: yData = [-y * (yScale / yTickStep) - origin[1] for y in yData] # negative bc inkscape is upside down coords = zip(xData, yData) inkDraw.line.absCoords(axisGroup, coords, position, lineStyle=lineStylePlot) return [axisGroup, limits, origin] @staticmethod def polar(ExtensionBaseObj, parent, rData, tData, position=[0, 0], rLabel='', rlog10scale=False, rTicks=True, tTicks=True, rTickStep=1.0, tTickStep=45.0, rScale=20, rExtraText='', rGrid=False, tGrid=False, generalAspectFactorAxis=1.0, lineStylePlot=inkDraw.lineStyle.setSimpleBlack(), forceRlim=None, forceTlim=None, drawAxis=True, ExtraLenghtAxisR=0.0): """Polar Plot .. note:: This method uses LaTeX in labels and tick marks if the support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param parent: parent object :param rData: list of R data :param tData: list of Theta data :param position: position of the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. :param rLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param rlog10scale: sets X axis to log10 scale if True. Default: False :param rTicks: Adds axis ticks to the X axis if True. Default: True :param tTicks: Adds axis ticks to the Y axis if True. Default: True :param rTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param tTickStep: Value interval between two consecutive ticks on Y axis. :param rScale: Distance between each rTickStep in svg units. Default: 20 - If axis is linear, then rScale is the size in svg units of each tick - If axis is log10, the rScale is the size in svg units of one decade :param rExtraText: extra text to be added to Ticks in both x and y. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param rGrid: adds grid lines to X axis if true. Default: False :param tGrid: adds grid lines to Y axis if true. Default: False :param generalAspectFactorAxis: regulates the general aspect ratio between grid lines, text and Ticks separations. Default: 1.0 :param lineStylePlot: line style to be used to plot the data. See class ``inkscapeMadeEasy_Draw.lineStyle``. Default: lineStylePlot=inkDraw.lineStyle.setSimpleBlack() :param forceRlim: forces limits of X axis to these limits. These limits affect the axis only, that is, all rData is plotted despite of these limits. Obs: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale - if forceRlim=None Limits will be defined by min and max of rData (Default) - if forceRlim=[xMin,xMax] then these limits will be used. :param forceTlim: forces limits of Y axis to these limits. These limits affect the axis only, that is, all tData is plotted despite of these limits. Obs: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale - if forceTlim=None Limits will be defined by min and max of tData (Default) - if forceTlim=[yMin,yMax] then these limits will be used. :param drawAxis: control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLenghtAxisR: extra length left near the arrow pointer of X axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type parent: inkscapeMadeEasy object (see example below) :type rData: list :type tData: list :type position: list :type rLabel: string :type rlog10scale: bool :type rTicks: bool :type tTicks: bool :type rTickStep: float :type tTickStep: float :type rScale: float :type rExtraText: string :type rGrid: bool :type tGrid: bool :type generalAspectFactorAxis: float :type lineStylePlot: lineStyle object :type forceRlim: list :type forceTlim: list :type drawAxis: bool :type ExtraLenghtAxisR: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the plot object - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list .. note:: If any of the axis are log10, then the method ignores any pairs of (x,y) data with invalid coordinates, that is, if rData and/or tData is less than or equal to 0.0 (they would result in complex log10... =P ). The method will create a text object alongside your plot warning this. .. note:: If any of the axis are linear, the method will ignore any value greater than 10.000 (in absolute value). This avoids plotting too big numbers (basically inf =) ). The method will create a text object alongside your plot warning this. **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> import inkscapeMadeEasy_Plot as inkPlot >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> >>> rData=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12] >>> tData=[30*x for x in range(12)] >>> >>> myMarkerDot=inkDraw.marker.createDotMarker(self,'DotM',RenameMode=2,scale=0.3,strokeColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black')) >>> lineStyleDiscrete = inkDraw.lineStyle.set(lineWidth=1.0,linecolor=inkDraw.color.defined('red'), markerStart=myMarkerDot,markerMid=myMarkerDot,markerEnd=myMarkerDot) >>> >>> inkPlot.plot.polar(self,root_layer,rData,tData,position=[0,0], >>> rLabel='my $R$ data',rlog10scale=False, >>> rTicks=True,tTicks=True,rTickStep=2,tTickStep=30, >>> rScale=20,rExtraText='a', >>> rGrid=True,tGrid=True,generalAspectFactorAxis=1.0,lineStylePlot=lineStyleDiscrete, >>> forceRlim=None,forceTlim=None,drawAxis=True) >>> >>> # another spiral, comprising two turns >>> tData=[2*x for x in range(360)] >>> rData=[x/180.0 for x in tData] >>> >>> inkPlot.plot.polar(self,root_layer,rData,tData,position=[0,0], >>> rLabel='my $R$ data',rlog10scale=False, >>> rTicks=True,tTicks=True,rTickStep=2,tTickStep=30, >>> rScale=20,rExtraText='', >>> rGrid=True,tGrid=True,generalAspectFactorAxis=1.0, >>> forceRlim=None,forceTlim=None,drawAxis=True) .. image:: ../imagesDocs/plot_plotPolarParameters_01.png :width: 900px """ textSize = generalAspectFactorAxis * 0.25 * rScale lineWidthAxis = generalAspectFactorAxis * rScale / 35.0 tDataTemp = [] rDataTemp = [] flagShowedError = False if rlog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(rData)): if rData[i] >= 1.0: tDataTemp.append(tData[i]) rDataTemp.append(rData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (rData[i], tData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(rData)): if abs(rData[i]) <= 1.0e4: tDataTemp.append(tData[i]) rDataTemp.append(rData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (rData[i], tData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True tData = tDataTemp rData = rDataTemp if forceRlim: Rlimits = forceRlim else: Rlimits = [min(rData), max(rData)] if forceTlim: Tlimits = forceTlim else: Tlimits = [min(tData), max(tData)] # min<->max inverted bc inkscape is upside down if Tlimits[0] == Tlimits[1]: if Tlimits[0] > 0: Tlimits[0] = 0 if Tlimits[0] == 0: Tlimits[1] = 360 if Tlimits[0] < 0: Tlimits[1] = 0 if Rlimits[0] == Rlimits[1]: if Rlimits[0] > 0: Rlimits[0] = 0 if Rlimits[0] == 0: Rlimits[1] = 1 if Rlimits[0] < 0: Rlimits[1] = 0 # draw axis axisGroup = ExtensionBaseObj.createGroup(parent, 'PlotData') [axisObj, limits, origin] = axis.polar(ExtensionBaseObj, axisGroup, Rlimits, Tlimits, position, rLabel=rLabel, rlog10scale=rlog10scale, rTicks=rTicks, tTicks=tTicks, rTickStep=rTickStep, tTickStep=tTickStep, rScale=rScale, rAxisUnitFactor=rExtraText, rGrid=rGrid, tGrid=tGrid, forceTextSize=textSize, forceLineWidth=lineWidthAxis, drawAxis=drawAxis, ExtraLenghtAxisR=ExtraLenghtAxisR) # scales data and convert to logarithmic scale if needed. Also subtracts the origin point of the axis to move the plot to the correct position nPoints = min(len(rData), len(tData)) xData = [] yData = [] if rlog10scale: for i in range(nPoints): xData.append(math.log10(rData[i]) * math.cos(math.radians(-tData[i])) * rScale) # negative theta bc inkscape is upside down yData.append(math.log10(rData[i]) * math.sin(math.radians(-tData[i])) * rScale) # negative theta bc inkscape is upside down else: for i in range(nPoints): xData.append(rData[i] * math.cos(math.radians(-tData[i])) * (rScale / rTickStep)) # negative theta bc inkscape is upside down yData.append(rData[i] * math.sin(math.radians(-tData[i])) * (rScale / rTickStep)) # negative theta bc inkscape is upside down coords = zip(xData, yData) inkDraw.line.absCoords(axisGroup, coords, position, lineStyle=lineStylePlot) return [axisGroup, limits, origin] @staticmethod def stem(ExtensionBaseObj, parent, xData, yData, position=[0, 0], xLabel='', yLabel='', ylog10scale=False, xTicks=True, yTicks=True, xTickStep=1.0, yTickStep=1.0, xScale=20, yScale=20, xExtraText='', yExtraText='', xGrid=False, yGrid=False, generalAspectFactorAxis=1.0, lineStylePlot=inkDraw.lineStyle.setSimpleBlack(), forceXlim=None, forceYlim=None, drawAxis=True, ExtraLenghtAxisX=0.0, ExtraLenghtAxisY=0.0): """Stem plot in Cartesian axis .. note:: This method uses LaTeX in labels and tick marks if the support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`latexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to use 'self' from inkscapeMadeEasy related objects :param parent: parent object :param xData: list of x data :param yData: list of y data :param position: position of the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. :param xLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param yLabel: Label of the Y axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param ylog10scale: sets Y axis to log10 scale if True. Default: False :param xTicks: Adds axis ticks to the X axis if True. Default: True :param yTicks: Adds axis ticks to the Y axis if True. Default: True :param xTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param yTickStep: Value interval between two consecutive ticks on Y axis. (Not used if Y axis is in log10 scale). Default:1.0 :param xScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param yScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param xExtraText: extra text to be added to Ticks in both x and y. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param yExtraText: extra text to be added to the ticks in Y axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param xGrid: adds grid lines to X axis if true. Default: False :param yGrid: adds grid lines to Y axis if true. Default: False :param generalAspectFactorAxis: regulates the general aspect ratio between grid lines, text and Ticks separations. Default: 1.0 :param lineStylePlot: line style to be used to plot the data. See class ``inkscapeMadeEasy_Draw.lineStyle``. Default: lineStylePlot=inkDraw.lineStyle.setSimpleBlack() :param forceXlim: forces limits of X axis to these limits. These limits affect the axis only, that is, all xData is plotted despite of these limits. Obs: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale - if forceXlim=None Limits will be defined by min and max of xData (Default) - if forceXlim=[xMin,xMax] then these limits will be used. :param forceYlim: forces limits of Y axis to these limits. These limits affect the axis only, that is, all yData is plotted despite of these limits. Obs: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale - if forceYlim=None Limits will be defined by min and max of yData (Default) - if forceYlim=[yMin,yMax] then these limits will be used. :param drawAxis: control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLenghtAxisX: extra length left near the arrow pointer of X axis. Default 0.0 :param ExtraLenghtAxisY: extra length left near the arrow pointer of Y axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object (see example below) :type parent: inkscapeMadeEasy object (see example below) :type xData: list :type yData: list :type position: list :type xLabel: string :type yLabel: string :type ylog10scale: bool :type xTicks: bool :type yTicks: bool :type xTickStep: float :type yTickStep: float :type xScale: float :type yScale: float :type xExtraText: string :type yExtraText: string :type xGrid: bool :type yGrid: bool :type generalAspectFactorAxis: float :type lineStylePlot: lineStyle object :type forceXlim: list :type forceYlim: list :type drawAxis: bool :type ExtraLenghtAxisX: float :type ExtraLenghtAxisY: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the plot object - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list .. note:: If any of the axis are log10, then the method ignores any pairs of (x,y) data with invalid coordinates, that is, if xData and/or yData is less than or equal to 0.0 (they would result in complex log10... =P ). The method will create a text object alongside your plot warning this. .. note:: If any of the axis are linear, the method will ignore any value greater than 10.000. this avoids plotting too big numbers (basically inf =) ). The method will create a text object alongside your plot warning this. **Example** >>> import inkex >>> import inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy_Draw as inkDraw >>> import inkscapeMadeEasy_Plot as inkPlot >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> ... >>> ... >>> >>> def effect(self): >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> xData=[-1,-0.5,0,0.5,1.0,1.5,2] >>> yData=[x*x for x in xData] # computes y=x*x >>> >>> # creates a line style with a dot marker for the stem plot >>> myMarkerDot=inkDraw.marker.createDotMarker(self,'DotMDiscreteTime',RenameMode=2,scale=0.3,strokeColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('red')) >>> lineStyleDiscrete = inkDraw.lineStyle.set(lineWidth=1.0, markerEnd=myMarkerDot) >>> >>> inkPlot.plot.stem(self,root_layer,xData,yData,position=[0,0], >>> xLabel='my $x$ data',yLabel='$y(x)$',ylog10scale=False, >>> xTicks=True,yTicks=True,xTickStep=0.5,yTickStep=2.0, >>> xScale=20,yScale=20,xExtraText='a',yExtraText='', >>> xGrid=True,yGrid=True,generalAspectFactorAxis=1.0,lineStylePlot=lineStyleDiscrete, >>> forceXlim=None,forceYlim=None,drawAxis=True) .. image:: ../imagesDocs/plot_plotStemParameters_01.png :width: 400px """ textSize = generalAspectFactorAxis * 0.25 * min(xScale, yScale) lineWidthAxis = generalAspectFactorAxis * min(xScale, yScale) / 35.0 yDataTemp = [] xDataTemp = [] flagShowedError = False # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(xData)): if abs(xData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp yDataTemp = [] xDataTemp = [] flagShowedError = False if ylog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(yData)): if yData[i] > 0.0: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(yData)): if abs(yData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp if forceXlim: Xlimits = forceXlim else: Xlimits = [min(xData), max(xData)] if forceYlim: Ylimits = forceYlim else: Ylimits = [min(yData), max(yData)] # min<->max inverted bc inkscape is upside down if Ylimits[0] == Ylimits[1]: if Ylimits[0] > 0: Ylimits[0] = 0 if Ylimits[0] == 0: Ylimits[1] = 1 if Ylimits[0] < 0: Ylimits[1] = 0 if Xlimits[0] == Xlimits[1]: if Xlimits[0] > 0: Xlimits[0] = 0 if Xlimits[0] == 0: Xlimits[1] = 1 if Xlimits[0] < 0: Xlimits[1] = 0 # draw axis axisGroup = ExtensionBaseObj.createGroup(parent, 'PlotData') [axisObj, limits, origin] = axis.cartesian(ExtensionBaseObj, axisGroup, Xlimits, Ylimits, position, xLabel=xLabel, yLabel=yLabel, xlog10scale=False, ylog10scale=ylog10scale, xTicks=xTicks, yTicks=yTicks, xTickStep=xTickStep, yTickStep=yTickStep, xScale=xScale, yScale=yScale, xAxisUnitFactor=xExtraText, yAxisUnitFactor=yExtraText, xGrid=xGrid, yGrid=yGrid, forceTextSize=textSize, forceLineWidth=lineWidthAxis, drawAxis=drawAxis, ExtraLenghtAxisX=ExtraLenghtAxisX, ExtraLenghtAxisY=ExtraLenghtAxisY) # scales data and convert to logarithmic scale if needed. Also subtracts the origin point of the axis to move the plot to the correct position xData = [x * (xScale / xTickStep) - origin[0] for x in xData] if ylog10scale: yData = [-math.log10(y) * yScale - origin[1] for y in yData] else: yData = [-y * (yScale / yTickStep) - origin[1] for y in yData] # negative bc inkscape is upside down stemGroup = ExtensionBaseObj.createGroup(axisGroup, 'StemGroup') for i in range(len(xData)): inkDraw.line.relCoords(stemGroup, [[0, yData[i]]], [xData[i] + position[0], 0 + position[1]], lineStyle=lineStylePlot) return [axisGroup, limits, origin] ================================================ FILE: 0.9x/textextLib/CircuitSymbolsLatexPreamble.tex ================================================ \usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \usepackage[per=slash]{siunitx} \usepackage{steinmetz} \usepackage[utf8]{inputenc} %\usepackage[T1]{fontenc} \usepackage{ctable} \newcommand{\micro}{\ensuremath{\mu}} \newcommand{\phasorName}[1]{ \ensuremath{ \boldsymbol{\hat #1} } } \newcommand{\impedance}[1]{ \ensuremath{ \boldsymbol{#1} } } \newcommand{\complexPol}[2]{\ensuremath{#1\phase{#2}}} \newcommand{\complexPolDeg}[2]{\ensuremath{#1\phase{#2\degree}}} % new units \newunit{\Vef}{\volt_{ef}} % volt eficaz \newunit{\Vrms}{\volt_{rms}} % volt RMS \newunit{\Vpp}{\volt_{pp}} % volt peak-to-peak \newunit{\Aef}{\ampere_{ef}} % ampere eficaz \newunit{\Arms}{\ampere_{rms}} % ampere rms \newunit{\App}{\ampere_{pp}} % ampere peak-to-peak % logic Commands \newcommand{\NOT}[1]{\ensuremath{\overline{\mbox{\ensuremath{#1}}}}} \newcommand{\AND}{\ensuremath{\cdot}} \newcommand{\OR}{\ensuremath{+}} \newcommand{\XOR}{\ensuremath{\oplus}} \newcommand{\XNOR}{\ensuremath{\odot}} ================================================ FILE: 0.9x/textextLib/__init__.py ================================================ ================================================ FILE: 0.9x/textextLib/basicLatexPackages.tex ================================================ \usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \usepackage[per=slash]{siunitx} \usepackage{steinmetz} \usepackage[utf8]{inputenc} %\usepackage[T1]{fontenc} % math \newcommand{\anotaSobre}[2]{\overbrace{#1}^{#2}} \newcommand{\anotaSob}[2]{\underbrace{#1}_{#2}} \newcommand{\igualComent}[1]{\stackrel{#1}{=}} \newcommand{\rightarrowComent}[1]{\stackrel{#1}{\rightarrow}} \newcommand{\sseComent}[1]{\stackrel{#1}{\quad\Leftrightarrow\quad}} ================================================ FILE: 0.9x/textextLib/textext.inx ================================================ <_name>Tex Text org.ekips.filter.textext textext.py inkex.py all ================================================ FILE: 0.9x/textextLib/textext.py ================================================ #!/usr/bin/env python """ ======= textext ======= :Author: Pauli Virtanen :Date: 2008-04-26 :License: BSD :modified: Fernando Moura :Date: 2016 # All GTK and TK bits were removed # Changes: line 67-69: added a few variables to control debug mode # Changes: line 708: changed order of programs # Changes: line 428: debug option to save temporary files to an easy access directory # Changes: line 459: debug option to keep the .tex file Textext is an extension for Inkscape_ that allows adding LaTeX-generated text objects to your SVG drawing. What's more, you can also *edit* these text objects after creating them. This brings some of the power of TeX typesetting to Inkscape. Textext was initially based on InkLaTeX_ written by Toru Araki, but is now rewritten. Thanks to Robert Szalai, Rafal Kolanski, Brian Clarke, and Florent Becker for contributions. .. note:: Unfortunately, the TeX input dialog is modal. That is, you cannot do anything else with Inkscape while you are composing the LaTeX text snippet. This is because I have not yet worked out whether it is possible to write asynchronous extensions for Inkscape. .. note:: Textext requires Pdflatex and one of the following - Pdf2svg_ - Pstoedit_ compiled with the ``plot-svg`` back-end - Pstoedit_ and Skconvert_ .. _Pstoedit: http://www.pstoedit.net/pstoedit .. _Skconvert: http://www.skencil.org/ .. _Pdf2svg: http://www.cityinthesky.co.uk/pdf2svg.html .. _Inkscape: http://www.inkscape.org/ .. _InkLaTeX: http://www.kono.cis.iwate-u.ac.jp/~arakit/inkscape/inklatex.html """ #------------------------------------------------------------------------------ __version__ = "0.4.4" __docformat__ = "restructuredtext en" import sys, os, glob, traceback, platform sys.path.append('/usr/share/inkscape/extensions') sys.path.append(r'c:/Program Files/Inkscape/share/extensions') sys.path.append(os.path.dirname(__file__)) import inkex import os, sys, tempfile, traceback, glob, re, md5, copy from lxml import etree # tex file will be saved in TEMPDIR_TEXFILE_DEBUG='/home/yourUserNameHere/tmp' # without the last / DEBUG_MODE=False # False: Usual mode True: debug mode, that is, temporary files are stored instead of discarted. USE_WINDOWS = (platform.system() == "Windows") TEXTEXT_NS = u"http://www.iki.fi/pav/software/textext/" SVG_NS = u"http://www.w3.org/2000/svg" XLINK_NS = u"http://www.w3.org/1999/xlink" ID_PREFIX = "textext-" NSS = { u'textext': TEXTEXT_NS, u'svg': SVG_NS, u'xlink': XLINK_NS, } #------------------------------------------------------------------------------ # Inkscape plugin functionality #------------------------------------------------------------------------------ class TexText(inkex.Effect): def __init__(self): inkex.Effect.__init__(self) self.settings = Settings() self.OptionParser.add_option( "-t", "--text", action="store", type="string", dest="text", default=None) self.OptionParser.add_option( "-p", "--preamble-file", action="store", type="string", dest="preamble_file", default=self.settings.get('preamble', str, "")) self.OptionParser.add_option( "-s", "--scale-factor", action="store", type="float", dest="scale_factor", default=self.settings.get('scale', float, 1.0)) def effect(self): """Perform the effect: create/modify TexText objects""" global CONVERTERS # Pick a converter converter_errors = [] converter_cls = None for conv_cls in CONVERTERS: try: conv_cls.available() converter_cls = conv_cls break except StandardError, e: converter_errors.append("%s: %s" % (conv_cls.__name__, str(e))) if not converter_cls: raise RuntimeError("No Latex -> SVG converter available:\n%s" % ';\n'.join(converter_errors)) # Find root element old_node, text, preamble_file = self.get_old() # Ask for TeX code if self.options.text is None: # If there is a transform, scale in GUI will be ignored if old_node is not None: scale_factor = None else: scale_factor = self.options.scale_factor if not preamble_file: preamble_file = self.options.preamble_file if not os.path.isfile(preamble_file): preamble_file = "" asker = AskText(text, preamble_file, scale_factor) asker.ask(lambda t, p, s: self.do_convert(t, p, s, converter_cls, old_node)) else: self.do_convert(self.options.text, self.options.preamble_file, self.options.scale_factor, converter_cls, old_node) def do_convert(self, text, preamble_file, scale_factor, converter_cls, old_node): if not text: return if isinstance(text, unicode): text = text.encode('utf-8') # Convert converter = converter_cls(self.document) try: new_node = converter.convert(text, preamble_file, scale_factor) finally: converter.finish() if new_node is None: return # noop # Insert into document # -- Set textext attribs new_node.attrib['{%s}text'%TEXTEXT_NS] = text.encode('string-escape') new_node.attrib['{%s}preamble'%TEXTEXT_NS] = \ preamble_file.encode('string-escape') # -- Copy transform try: # Note: the new node does *not* have the SVG namespace prefixes! # This caused some problems as Inkscape couldn't properly # handle both svg: and prefixless entries in the same file # in some cases. new_node.attrib['transform'] = old_node.attrib['transform'] except (KeyError, IndexError, TypeError, AttributeError): pass try: new_node.attrib['transform'] = old_node.attrib['{%s}transform'%SVG_NS] except (KeyError, IndexError, TypeError, AttributeError): pass # -- Copy style if old_node is not None: self.copy_style(old_node, new_node) # -- Replace self.replace_node(old_node, new_node) # -- Save settings if os.path.isfile(preamble_file): self.settings.set('preamble', preamble_file) if scale_factor is not None: self.settings.set('scale', scale_factor) self.settings.save() def get_old(self): """ Dig out LaTeX code and name of preamble file from old TexText-generated objects. :Returns: (old_node, latex_text, preamble_file_name) """ for i in self.options.ids: node = self.selected[i] if node.tag != '{%s}g' % SVG_NS: continue if '{%s}text'%TEXTEXT_NS in node.attrib: # starting from 0.2, use namespaces return (node, node.attrib.get('{%s}text'%TEXTEXT_NS, '').decode('string-escape'), node.attrib.get('{%s}preamble'%TEXTEXT_NS, '').decode('string-escape')) elif '{%s}text'%SVG_NS in node.attrib: # < 0.2 backward compatibility return (node, node.attrib.get('{%s}text'%SVG_NS, '').decode('string-escape'), node.attrib.get('{%s}preamble'%SVG_NS, '').decode('string-escape')) return None, "", "" def replace_node(self, old_node, new_node): """ Replace an XML node old_node with new_node in self.document. """ if old_node is None: self.current_layer.append(new_node) else: parent = old_node.getparent() parent.remove(old_node) parent.append(new_node) STYLE_ATTRS = ['fill','fill-opacity','fill-rule', 'font-size-adjust','font-stretch', 'font-style','font-variant', 'font-weight','letter-spacing', 'stroke','stroke-dasharray', 'stroke-linecap','stroke-linejoin', 'stroke-miterlimit','stroke-opacity', 'text-anchor','word-spacing','style'] def copy_style(self, old_node, new_node): # XXX: Needs work... # # We could try traversing the node tree downwards and # removing color-alteration from the attributes. # Not straightforward, need to read the SVG spec... # # Removing style attributes does not work in general, because # at least pdf2svg relies on preserving the stroke attrs. # try: new_node.attrib['style'] = old_node.attrib['style'] except (KeyError, IndexError, TypeError, AttributeError): pass #------------------------------------------------------------------------------ # Settings backend #------------------------------------------------------------------------------ class Settings(object): def __init__(self): self.values = {} if USE_WINDOWS: self.keyname = r"Software\TexText\TexText" else: self.filename = os.path.expanduser("~/.inkscape/textextrc") self.load() def load(self): if USE_WINDOWS: import _winreg try: key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, self.keyname) except: return try: self.values = {} for j in range(1000): try: name, data, dtype = _winreg.EnumValue(key, j) except EnvironmentError: break self.values[name] = str(data) finally: key.Close() else: try: f = open(self.filename, 'r') except (IOError, OSError): return try: self.values = {} for line in f.read().split("\n"): if not '=' in line: continue k, v = line.split("=", 1) self.values[k.strip()] = v.strip() finally: f.close() def save(self): if USE_WINDOWS: import _winreg try: key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, self.keyname, sam=_winreg.KEY_SET_VALUE | _winreg.KEY_WRITE) except: key = _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, self.keyname) try: for k, v in self.values.iteritems(): _winreg.SetValueEx(key, str(k), 0, _winreg.REG_SZ, str(v)) finally: key.Close() else: d = os.path.dirname(self.filename) if not os.path.isdir(d): os.makedirs(d) f = open(self.filename, 'w') try: data = '\n'.join(["%s=%s" % (k,v) for k,v in self.values.iteritems()]) f.write(data) finally: f.close() def get(self, key, typecast, default=None): try: return typecast(self.values[key]) except (KeyError, ValueError, TypeError): return default def set(self, key, value): self.values[key] = str(value) #------------------------------------------------------------------------------ # LaTeX converters #------------------------------------------------------------------------------ try: import subprocess def exec_command(cmd, ok_return_value=0, combine_error=False): """ Run given command, check return value, and return concatenated stdout and stderr. """ try: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) out, err = p.communicate() except OSError, e: raise RuntimeError("Command %s failed: %s" % (' '.join(cmd), e)) if ok_return_value is not None and p.returncode != ok_return_value: raise RuntimeError("Command %s failed (code %d): %s" % (' '.join(cmd), p.returncode, out + err)) return out + err except ImportError: # Python < 2.4 ... import popen2 def exec_command(cmd, ok_return_value=0, combine_error=False): """ Run given command, check return value, and return concatenated stdout and stderr. """ # XXX: unix-only! try: p = popen2.Popen4(cmd, True) p.tochild.close() returncode = p.wait() >> 8 out = p.fromchild.read() except OSError, e: raise RuntimeError("Command %s failed: %s" % (' '.join(cmd), e)) if ok_return_value is not None and returncode != ok_return_value: raise RuntimeError("Command %s failed (code %d): %s" % (' '.join(cmd), returncode, out)) return out if USE_WINDOWS: # Try to add some commonly needed paths to PATH paths = os.environ.get('PATH', '').split(os.path.pathsep) program_files = os.environ.get('PROGRAMFILES') if program_files: paths += glob.glob(os.path.join(program_files, 'gs/gs*/bin')) paths += glob.glob(os.path.join(program_files, 'pstoedit*')) paths += glob.glob(os.path.join(program_files, 'miktex*/miktex/bin')) os.environ['PATH'] = os.path.pathsep.join(paths) class LatexConverterBase(object): """ Base class for Latex -> SVG converters """ # --- Public api def __init__(self, document): """ Initialize Latex -> SVG converter. :Parameters: - `document`: Document where the result is to be embedded (read-only) """ if DEBUG_MODE: self.tmp_path = TEMPDIR_TEXFILE_DEBUG else: self.tmp_path = tempfile.mkdtemp() self.tmp_base = 'tmp' def convert(self, latex_text, preamble_file, scale_factor): """ Return an XML node containing latex text :Parameters: - `latex_text`: Latex code to use - `preamble_file`: Name of a preamble file to include - `scale_factor`: Scale factor to use if object doesn't have a ``transform`` attribute. :Returns: XML DOM node """ raise NotImplementedError def available(cls): """ :Returns: Check if converter is available, raise RuntimeError if not """ pass available = classmethod(available) def finish(self): """ Clean up any temporary files """ if not DEBUG_MODE: self.remove_temp_files() # --- Internal def tmp(self, suffix): """ Return a file name corresponding to given file suffix, and residing in the temporary directory. """ return os.path.join(self.tmp_path, self.tmp_base + '.' + suffix) def tex_to_pdf(self, latex_text, preamble_file): """ Create a PDF file from latex text """ # Read preamble preamble = "" if os.path.isfile(preamble_file): f = open(preamble_file, 'r') preamble += f.read() f.close() # Options pass to LaTeX-related commands latexOpts = ['-interaction=nonstopmode', '-halt-on-error'] texwrapper = r""" \documentclass[landscape,a0]{article} %s \pagestyle{empty} \begin{document} \noindent %s \end{document} """ % (preamble, latex_text) # Convert TeX to PDF # Write tex f_tex = open(self.tmp('tex'), 'w') try: f_tex.write(texwrapper) finally: f_tex.close() # Exec pdflatex: tex -> pdf exec_command(['pdflatex', self.tmp('tex')] + latexOpts) if not os.path.exists(self.tmp('pdf')): raise RuntimeError("pdflatex didn't produce output") def remove_temp_files(self): """Remove temporary files""" base = os.path.join(self.tmp_path, self.tmp_base) for filename in glob.glob(base + '*'): self.try_remove(filename) self.try_remove(self.tmp_path) def try_remove(self, filename): """Try to remove given file, skipping if not exists.""" if os.path.isfile(filename): os.remove(filename) elif os.path.isdir(filename): os.rmdir(filename) class PdfConverterBase(LatexConverterBase): def convert(self, latex_text, preamble_file, scale_factor): cwd = os.getcwd() try: os.chdir(self.tmp_path) self.tex_to_pdf(latex_text, preamble_file) self.pdf_to_svg() finally: os.chdir(cwd) new_node = self.svg_to_group() if new_node is None: return None if scale_factor is not None: new_node.attrib['transform'] = self.get_transform(scale_factor) return new_node def pdf_to_svg(self): """Convert the PDF file to a SVG file""" raise NotImplementedError def get_transform(self, scale_factor): """Get a suitable default value for the transform attribute""" raise NotImplementedError def svg_to_group(self): """ Convert the SVG file to an SVG group node. :Returns: node """ tree = etree.parse(self.tmp('svg')) self.fix_xml_namespace(tree.getroot()) try: return copy.copy(tree.getroot().xpath('g')[0]) except IndexError: return None def fix_xml_namespace(self, node): svg = '{%s}' % SVG_NS if node.tag.startswith(svg): node.tag = node.tag[len(svg):] for key in node.attrib.keys(): if key.startswith(svg): new_key = key[len(svg):] node.attrib[new_key] = node.attrib[key] del node.attrib[key] for c in node: self.fix_xml_namespace(c) class SkConvert(PdfConverterBase): """ Convert PDF -> SK -> SVG using pstoedit and skconvert """ def get_transform(self, scale_factor): return 'scale(%f,%f)' % (scale_factor, scale_factor) def pdf_to_svg(self): # Options for pstoedit command pstoeditOpts = '-dt -ssp -psarg -r9600x9600'.split() # Exec pstoedit: pdf -> sk exec_command(['pstoedit', '-f', 'sk', self.tmp('pdf'), self.tmp('sk')] + pstoeditOpts) if not os.path.exists(self.tmp('sk')): raise RuntimeError("pstoedit didn't produce output") # Exec skconvert: sk -> svg os.environ['LC_ALL'] = 'C' exec_command(['skconvert', self.tmp('sk'), self.tmp('svg')]) if not os.path.exists(self.tmp('svg')): raise RuntimeError("skconvert didn't produce output") def available(cls): """Check whether skconvert and pstoedit are available""" out = exec_command(['pstoedit'], ok_return_value=None) if 'version 3.44' in out and 'Ubuntu' in out: raise RuntimeError("Pstoedit version 3.44 on Ubuntu found, but it " "contains too many bugs to be usable") exec_command(['skconvert'], ok_return_value=1) available = classmethod(available) class PstoeditPlotSvg(PdfConverterBase): """ Convert PDF -> SVG using pstoedit's plot-svg backend """ def get_transform(self, scale_factor): return 'matrix(%f,0,0,%f,%f,%f)' % ( scale_factor, -scale_factor, -200*scale_factor, 750*scale_factor) def pdf_to_svg(self): # Options for pstoedit command pstoeditOpts = '-dt -ssp -psarg -r9600x9600'.split() # Exec pstoedit: pdf -> svg exec_command(['pstoedit', '-f', 'plot-svg', self.tmp('pdf'), self.tmp('svg')] + pstoeditOpts) if not os.path.exists(self.tmp('svg')): raise RuntimeError("pstoedit didn't produce output") def available(cls): """Check whether pstoedit has plot-svg available""" out = exec_command(['pstoedit', '-help'], ok_return_value=None) if 'version 3.44' in out and 'Ubuntu' in out: raise RuntimeError("Pstoedit version 3.44 on Ubuntu found, but it " "contains too many bugs to be usable") if 'plot-svg' not in out: raise RuntimeError("Pstoedit not compiled with plot-svg support") available = classmethod(available) class Pdf2Svg(PdfConverterBase): """ Convert PDF -> SVG using pdf2svg """ def __init__(self, document): PdfConverterBase.__init__(self, document) self.hash = None def convert(self, *a, **kw): # compute hash for generating unique ids for sub-elements self.hash = md5.new('%s%s' % (a, kw)).hexdigest()[:8] return PdfConverterBase.convert(self, *a, **kw) def pdf_to_svg(self): exec_command(['pdf2svg', self.tmp('pdf'), self.tmp('svg'), '1']) def get_transform(self, scale_factor): return 'scale(%f,%f)' % (scale_factor, scale_factor) def svg_to_group(self): # create xml.dom representation of the TeX file tree = etree.parse(self.tmp('svg')) root = tree.getroot() self.fix_xml_namespace(root) href_map = {} # Map items to new ids for i, el in enumerate(root.xpath('//*[attribute::id]')): cur_id = el.attrib['id'] new_id = "%s%s-%d" % (ID_PREFIX, self.hash, i) href_map['#' + cur_id] = "#" + new_id el.attrib['id'] = new_id # Replace hrefs url_re = re.compile('^url\((.*)\)$') for el in root.xpath('//*[attribute::xlink:href]', namespaces=NSS): href = el.attrib['{%s}href'%XLINK_NS] el.attrib['{%s}href'%XLINK_NS] = href_map.get(href, href) for el in root.xpath('//*[attribute::svg:clip-path]', namespaces=NSS): value = el.attrib['clip-path'] m = url_re.match(value) if m: el.attrib['clip-path'] = \ 'url(%s)' % href_map.get(m.group(1), m.group(1)) # Bundle everything in a single group master_group = etree.SubElement(root, 'g') for c in root: if c is master_group: continue master_group.append(c) return copy.copy(master_group) def available(cls): """Check whether pdf2svg is available, raise RuntimeError if not""" exec_command(['pdf2svg'], ok_return_value=None) available = classmethod(available) #original: CONVERTERS = [Pdf2Svg, PstoeditPlotSvg, SkConvert] CONVERTERS = [ PstoeditPlotSvg, SkConvert, Pdf2Svg] #------------------------------------------------------------------------------ # Entry point #------------------------------------------------------------------------------ if __name__ == "__main__": e = TexText() e.affect() ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ This set of python modules is intended to extend Aaron Spike's inkex.py module, adding functions to help the development of new extensions for Inkscape . # inkscapeMadeEasy This set of python modules is intended to extend Aaron Spike's inkex.py module, adding functions to help the development of new extensions for Inkscape . Here you will find methods and classes to deal with drawings, styles, markers, texts, plots, etc. It is a work-in-progress project and new features will be added in the future. However, there is no roadmap right now. Drawing Usage ===== **InskcapeMadeEasy is not an end-user Inkscape extension by itself**. The objective is to provide easier backstage functions and modules to facilitate the development of Inkscape extensions. For end-user extensions that employ InskcapeMadeEasy, please refer to my other repositories on GitHub (more to come soon). They provide many examples of how to use inkscapeMadeEasy: - **createMarkers** - **cartesianAxes2D** - **cartesianPlotFunction2D** - **cartesianPlotData2D** - **cartesianStemPlot** - **polarAxes2D** - **logicGates** - **circuitSymbols** - **wavedromScape** - **dimensions** - **SlopeField** - **BodePlot** - **Spirograph** ## History and Objectives Historically, this project started as a way to help myself while creating extensions, namely focusing on scientific /academic diagrams and graphs. In the academy, we prepare plots and diagrams very often to explain concepts or results for lectures, seminars, congresses and scientific articles. There are many consecrated mathematical tools that can produce them, e.g., Gnuplot, Octave, Matlab, R, etc. They all can produce nice plots. However, it might be a little cumbersome when we want to add other elements to these plots, like text, comments, arrows, etc. These packages have tools to do it but they are cumbersome to use. A better approach would be using proper graphic software. One possible approach is to export these plots as raster images and use raster graphic software to produce the annotations, like Gimp. I prefer to have the plots in a vector graphic format to be able to produce, if needed, raster images with different resolutions without the fear of having a heavily pixelated image. For this, Inkscape is very sound. Unfortunately, exporting plots as vector graphics is not always successful in the sense that the resulting document is quite "dirty" (unorganized groups, isolated elements, etc.). Therefore I decided to make my plotting/diagram tools for Inkscape. In the process of creating these tools, I noticed that many of the low-level classes and methods used to manipulate elements of the svg file could be grouped in a general-purpose set of core modules that extended inkex.py module. **inkscapeMadeEasy** was born! The core modules I created do not intend to provide an extensive array of methods and classes to cover all possibilities of manipulations/transformations, and drawing. These modules were created and expanded as I felt the necessity to have new methods to help my workflow. Nevertheless, the number of methods created allows many possibilities and is still under development so new features can (will) appear in future versions. **Obs:** Since it is not very easy to find documentation of other Inkscape modules, there might be other modules with similar/better features that I was not aware of when I was producing my extensions. Enough mumbo-jumbo. Let's start! =D # Documentation You can find the main documentation page here . I tried to make it useful to everyone by adding as many examples as possible. Check it out! =D # Report issues This is a work-in-progress project. Please report issues with the modules or documentation. ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../../inkscapeMadeEasy_sphinxBuildDirectory PDFBUILDDIR = /tmp PDF = ../manual.pdf # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" # .PHONY: clean # clean: # rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/inkscapeMadeEasy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/inkscapeMadeEasy.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/inkscapeMadeEasy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/inkscapeMadeEasy" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(PDFBUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(PDFBUILDDIR)/latex all-pdf cp $(PDFBUILDDIR)/latex/*.pdf $(PDF) @echo "pdflatex finished; the PDF files are in $(PDF)." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." commithtml: html cd $(BUILDDIR)/html; git add . ; git commit -m "rebuilt docs"; git push origin gh-pages ================================================ FILE: docs/source/Changelog.rst ================================================ Changelog ========== 2024-10oct-23 ------------- inkscapeMadeEasy_Draw.py - added methods to copy and paste line styles - fixed a bug in the create markers, introduced after changes in inkex.py 2024-04apr-18 ------------- inkscapeMadeEasy_Base.py - fixed a bug in the output of getSegmentParameters 2022-10oct-01 ------------- inkscapeMadeEasy_Base.py - fixed a bug in arc.startEndRadius - improved method ellipseArc.centerAngStartAngEnd 2022-08aug-19 ------------- inkscapeMadeEasy_Base.py - new method createEmptySVG() - new method cleanDefs() - the method getPoints() also accepts 'rectangle' elements - added option [=v2] to siunix import in the latext preamble file. 2022-02feb-19 ------------- inkscapeMadeEasy_Draw.py - new class ellipseArc with functions to draw arcs of ellipses - new method lineStyle.createDashedLinePattern inkscapeMadeEasy_Base.py - new input argument added to importSVG() method - new input argument added to unifyDefs() method - fixed a bug in getTransformMatrix() when translation operation has only one argument - fixed a bug in getPoints() when the element is 'use' 2021-09sep-12 ------------- Updated latex preamble to comply with SIunitx v2-v3 inkscapeMadeEasy_Base.py - importSVG() now accepts two new arguments - new function addAttribute() 2021-04apr-28 ------------- Fixed example of the documentation 2021-04apr-25 ------------- inkscapeMadeEasy_Draw.py - alpha channel support was added to color() class. - The following functions have now one additional input argument: color.defined(), color.RGB() color.rgb(), and color.gray() - The function color.parseColorPicker() has new output - added new function color.val2hex() 2021-04apr-04 ------------- inkscapeMadeEasy_Base.py - exportSVG() corrected text encoding when creating empty SVG file. - exportSVG() now the blank SVG file will be in px. - moveElement() fixed quick return if distance is 0.0 - getPoints() now it returns a 2D numpy array instead of a list of lists. - getBoundingBox() now it returns a 2D numpy array instead of a list. 2021-03mar-20 ------------- inkscapeMadeEasy_Draw.py - new function arc.threePoints that creates an arc based on 3 points in the arc - new function circle.threePoints that creates a circle based on 3 points in the circle 2020-10oct-06 ------------- inkscapeMadeEasy_Base.py and inkscapeMadeEasy_Draw.py - fixed a bug in text.latex that would result in wrong font size. inkscapeMadeEasy_Draw.py - fixed a bug in color.rgb() method. 2020-10oct-02 ------------- .. important:: - inkscapeMadeEasy is now compatible with 1.0 - The older version, compatible with inkscape 0.9x is now under the directory 0.9x. - latex support now uses an updated version of textext (https://github.com/textext/textext), simplifying installation. inkscapeMadeEasy_Base.py - getDocumentScale() was renamed as getDocumentScaleFactor - scaleElement() Changed scaleY=0.0 to scaleY = None to indicate scaleY=scaleX. - blankSVG: new member variable with a string representing a blank svg file inkscapeMadeEasy_Draw.py - arc.startEndRadius and arc.centerAngStartAngEnd now have a new argument, 'arcType' that replace and extends the old 'flagOpen' argument - new function color.rgb() that accepts normalized input color values in the range 0.0-1.0 - the function marker.createInfLineMarker was renamed as marker.createElipsisMarker - the argument 'fillColor' of marker.createCrossMarker was removed. - the argument 'strokeColor' of marker.createElipsisMarker was removed. inkscapeMadeEasty_Plot.py - axis.cartesian - the argument ExtraLenghtAxisX was renamed as ExtraLengthAxisX - the argument ExtraLenghtAxisY was renamed as ExtraLengthAxisY - axis.polar - the argument ExtraLenghtAxisR was renamed as ExtraLengthAxisR - plot.cartesian - the argument ExtraLenghtAxisX was renamed as ExtraLengthAxisX - the argument ExtraLenghtAxisY was renamed as ExtraLengthAxisY - plot.polar - the argument ExtraLenghtAxisR was renamed as ExtraLengthAxisR - plot.stem - the argument ExtraLenghtAxisX was renamed as ExtraLengthAxisX - the argument ExtraLenghtAxisY was renamed as ExtraLengthAxisY 2020-01jan-12 ------------- inkscapeMadeEasy_Base.py - Added a new function: importSVG 2020-01jan-11 ------------- added LaTeX installation instructions for windows users. inkscapeMadeEasy_Base.py - Now ungroup method returns a list with the elements previously contained in the removed group 2020-01jan-05 ------------- inkscapeMadeEasy_Base.py - Added a new functions: unifyDefs, getDefsByTag, getDefsById, ungroup - Changed the name of getElemAtrib -> getElemAttrib - method getPoints also can process nodes inkscapeMadeEasy_Draw.py - Modified text.latex method to try to fix some issues under Windows. 2020-01jan-02 ------------- inkscapeMadeEasy_Draw.py - Added a new class: cubicBezier - Added new option for line.absCoords and line.relCoords. Now it is possible to close the path, connecting the start and end points. - fixed documentation inkscapeMadeEasy_Base.py - Added a new function: exportSVG, getDocumentScale - fixed documentation inkscapeMadeEasy_Plot.py - fixed documentation 2019-12dec-29 ------------- inkscapeMadeEasy_Base.py - Added a new function: getDocumentScale - new optional argument for scaleElement function. 2019-12dec-22 ------------- inkscapeMadeEasy_Draw.py - fixed a bug introduced in my last commit 2019-12dec-17 ------------- inkscapeMadeEasy_Base.py - added new function: copyElement - fixed documentation - reformatted the code using pycharm inkscapeMadeEasy_Draw.py, inkscapeMadeEasy_Plot.py - fixed documentation - reformatted the code using pycharm 2019-04apr-04 ------------- - fixed documentation on installation procedure 2018-11nov-14 ------------- inkscapeMadeEasy_Base.py - added new functions: getElemFromXpath, getElemAtrib, getDocumentName, getDocumentUnit, getcurrentLayer, unit2userUnit, userUnit2unit, unit2unit 2018-07jul-31 ------------- inkscapeMadeEasy_Base.py - added two new functions: getSegmentParameters and getSegmentFromPoints - Removed the GUI of the textex module and its dependencies with GUI modules. It might be easier now to run the extensions under Windows/Mac 2017-11nov-19 ------------- inkscapeMadeEasy_Base.py - added a function to erase elements: removeElement(element) - escaped some backslashes missing in the documentation sections. This caused issues for some users. 2017-08aug-04 ------------- inkscapeMadeEasy_Draw.py - now text.write() allows multi-line text. 2017-05may-18 ------------- inkscapeMadeEasy_Draw.py - fixed documentation on predefined color 'purple' 2017-05may-06 ------------- inkscapeMadeEasy_Draw.py - added a class and two methods to draw rectangles. 2017-06jun-18 ------------- inkscapeMadeEasy_Base.py - fix a bug in getPoints method. 2016-11nov-02 ------------- inkscapeMadeEasy_Draw.py - fix text.latex() method in case LaTeX support is disabled. There was a bug when angleDeg was different than zero. 2016-11nov-02 ------------- inkscapeMadeEasy_Draw.py - small modification in text.latex() method to fix incompatibility with temporary diretory under windows. 2016-10oct-31 ------------- inkscapeMadeEasy_Draw.py, inkscapeMadeEasy_Plot.py - LaTeX support is now optional. See documentation on how to enable/disable it. 2016-10oct-28 ------------- inkscapeMadeEasy_Base.py - Changes in inkscapeMadeEasy.getPoints() to become compatible with Python 2.6 2016-10oct-12 ------------- inkscapeMadeEasy_Base.py - Fix inkscapeMadeEasy.displayMsg() definition. 2016-09sep-21 ------------- inkscapeMadeEasy_Base.py - New method inkscapeMadeEasy.displayMsg() to show messages to the user inkscapeMadeEasy_Draw.py - New method displayMsg() to show messages to the user - Minor documentation changes inkscapeMadeEasy_Plot.py - New method displayMsg() to show messages to the user - Changed argument names containing '__Mark__' to '__Tick__' to comply with other plotting packages. Attention: This might break your code. ================================================ FILE: docs/source/_static/style.css ================================================ .wy-nav-content { max-width: none; } ================================================ FILE: docs/source/conf.py ================================================ # -*- coding: utf-8 -*- # # inkscapeMadeEasy documentation build configuration file, created by # sphinx-quickstart on Wed Sep 7 03:02:48 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../../latest')) sys.path.insert(0, os.path.abspath('/usr/share/inkscape/extensions')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', ] autodoc_mock_imports = ['textext', 'inkscapeMadeEasy'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'inkscapeMadeEasy' copyright = u'2016, fsmMLK' author = u'fsmMLK' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'1.0' # The full version, including alpha/beta/rc tags. release = u'1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #html_theme = 'classic' html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. #html_title = u'inkscapeMadeEasy v0.1' # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. #html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'inkscapeMadeEasydoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', 'preamble': r'\usepackage{enumitem}\setlistdepth{99}' # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'inkscapeMadeEasy.tex', u'inkscapeMadeEasy Documentation', u'fsmMLK', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'inkscapemadeeasy', u'inkscapeMadeEasy Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'inkscapeMadeEasy', u'inkscapeMadeEasy Documentation', author, 'inkscapeMadeEasy', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False autodoc_member_order = 'bysource' def setup(app): app.add_css_file("style.css") ================================================ FILE: docs/source/genindex.rst ================================================ Classes and methods =================== ================================================ FILE: docs/source/index.rst ================================================ .. inkscapeMadeEasy documentation master file, created by sphinx-quickstart on Mon May 2 09:03:50 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ============================================ Welcome to inkscapeMadeEasy's documentation! ============================================ This set of python modules is intended to extend Aaron Spike's inkex.py module, adding functions to help the development of new extensions for `Inkscape `_. Here you will find methods and classes to deal with drawings, styles, markers, texts, plots, etc. It is a work-in-progress project and new features will be added in the future. However, there is no roadmap right now. .. image:: ../imagesDocs/samples_01.png :width: 800px Current and older versions ========================== =================== ================== ================ Version compatibility table --------------------------------------------------------- Inkscape InkscapeMadeEasy Receive updates? =================== ================== ================ 1.0 1.0 (latest) YES 0.48, 0.91, 0.92 0.9x (obsolete) NO =================== ================== ================ Current version --------------- The current version of InkscapeMadeEasy is **1.0**. This version is compatible with Inkscape 1.0 and up only. It is **incompatible** with older Inkscape versions! .. note:: This documentation page refers to version 1.0 Older versions -------------- If you have an older version of Inkscape, please use InkscapeMadeEasy 0.9x. The files are under the folder 0.9x on github. In this folder you can also find a pdf with the manual of this obsolete version. Follow the installation instructions in the pdf. .. warning:: Only the latest version will receive updates, new features, and bug fixes. Usage ===== **InskcapeMadeEasy is not an end-user Inkscape extension by itself**. The objective is to provide easier backstage functions and modules to facilitate the development of Inkscape extensions. For end-user extensions that employ InskcapeMadeEasy, please refer to my other repositories on GitHub (more to come soon). They provide many examples of how to use inkscapeMadeEasy: - **createMarkers** - **cartesianAxes2D** - **cartesianPlotFunction2D** - **cartesianPlotData2D** - **cartesianStemPlot** - **polarAxes2D** - **logicGates** - **circuitSymbols** - **wavedromScape** - **dimensions** - **SlopeField** - **BodePlot** - **Spirograph** History and Objectives ====================== Historically, this project started as a way to help myself while creating extensions, namely focusing on scientific/academic diagrams and graphs. In the academy, we prepare plots and diagrams very often to explain concepts or results for lectures, seminars, congresses and scientific articles. There are many consecrated mathematical tools that can produce them, e.g., Gnuplot, Octave, Matlab, R, etc. They all can produce nice plots. However, it might be a little cumbersome when we want to add other elements to these plots, like text, comments, arrows, etc. These packages have tools to do it but they are cumbersome to use. A better approach would be using proper graphic software. One possible approach is to export these plots as raster images and use raster graphic software to produce the annotations, like Gimp. I prefer to have the plots in a vector graphic format to be able to produce, if needed, raster images with different resolutions without the fear of having a heavily pixelated image. For this, Inkscape is very sound. Unfortunately, exporting plots as vector graphics is not always successful in the sense that the resulting document is quite "dirty" (unorganized groups, isolated elements, etc.). Therefore I decided to make my plotting/diagram tools for Inkscape. In the process of creating these tools, I noticed that many of the low-level classes and methods used to manipulate elements of the svg file could be grouped in a general-purpose set of core modules that extended inkex.py module. **inkscapeMadeEasy** was born! The core modules I created do not intend to provide an extensive array of methods and classes to cover all possibilities of manipulations/transformations, and drawing. These modules were created and expanded as I felt the necessity to have new methods to help my workflow. Nevertheless, the number of methods created allows many possibilities and is still under development so new features can (will) appear in future versions. **Obs:** Since it is not very easy to find documentation of other Inkscape modules, there might be other modules with similar/better features that I was not aware of when I was producing my extensions. Enough mumbo-jumbo. Let's start! =D .. toctree:: :caption: Main Features :maxdepth: 2 ./mainFeatures.rst .. toctree:: :caption: Installation :maxdepth: 2 ./installation.rst .. toctree:: :caption: General gist and minimal example :maxdepth: 2 ./minimalExample.rst .. toctree:: :caption: Module Definitions :maxdepth: 2 ./moduleDefinitions.rst .. toctree:: :caption: Changelog :maxdepth: 2 ./Changelog.rst .. toctree:: :caption: Index :hidden: genindex ================================================ FILE: docs/source/installation.rst ================================================ Inkscape Version ================ .. attention:: The procedure bellow refers to inkscapeMadeEasy **version 1.0 or later** only. For older versions, please see the pdf manual inside the `legacy folder 0.9x `_. InkscapeMadeEasy was developed using Inkscape 1.0 in Linux (Kubuntu 18.04). It should work in different OSs too as long as all requirements are met. Requirements ============ 0) You will need `Inkscape 1.0 or later `_ (duh! =) ) 1) You will need Python 3 in your machine. InkscapeMadeEasy will not work properly with python 2. The following python modules are required: math, os, re, sys, copy, numpy, lxml, tempfile. Make sure your python installation has these modules. 2) If you want LaTeX support, install a latex distribution for your OS. You will need in your system the following LaTeX packages: amsmath, amsthm, amsbsy, amsfonts, amssymb, siunitx, steinmetz. 3) You will need `TexText `_ version **1.0** .. note:: Starting from InkscapeMadeEasy 1.0, you don't need pstoedit, ghostscript and pdf2svg anymore. Thanks TexText team! =) Installation procedure (v1.0 only) ================================== 1) Install python 3 in your machine if you don't have it yet. You can download it from `here `_ or use your package manager. See the instructions for `Linux `_, `Windows `_ or `Mac `_ if you need help. .. note :: **Windows users:** make sure you check **Add Python 3.x to PATH** on the bottom of the installer screen. Install the following python modules: math, os, re, sys, copy, numpy, lxml, tempfile. 2) Install `Inkscape 1.0 `_. .. note :: **Windows users:** Make sure that you checked the **Python** option in Program Files as well as the **Extensions** options in Inkscape Data during the installation of Inkscape (by default this is the case). .. image:: ../imagesDocs/inkscape-install-options-windows.png :width: 400px 3) **if you want LaTeX support:** Install a LaTeX distribution . See `LaTeX installation`_ on further instructions for Linux, Mac and Windows users. 4) **if you don't want LaTeX support:** follow the instructions of section :ref:`disableLatexSupport` to disable it. 5) Install `TexText `_ version **1.0**. Follow the instructions on the link. 6) inkscapeMadeEasy installation a) Go to Inkscape's extension directory with a file browser. Your inkscape extension directory can be accessed by opening Inkscape and selecting ``Edit > Preferences > System``. Look for the item **User Extensions** field. There is a button on the right of the field that will open a file explorer window in that specific folder. b) Create a subfolder in the extension directory with the name ``inkscapeMadeEasy``. .. warning:: Be careful with upper and lower case letters. You must write as presented above. c) Download `inkscapeMadeEasy `_ files and place them inside the directory you just created. You don't have to copy all files from Github. The files you will need are inside the ``latest`` folder. In the end you must have the following files and directories in your Inkscape extension directory. .. code-block:: none inkscape ┣━━extensions ┋ ┣━━ inkscapeMadeEasy <-- inkscapeMadeEasy folder ┃ ┣━━ inkscapeMadeEasy_Base.py ┃ ┣━━ inkscapeMadeEasy_Draw.py ┃ ┣━━ inkscapeMadeEasy_Plot.py ┃ ┗━━ basicLatexPackages.tex ┃ ┣━━ textext <-- textext folder (if you installed textText) ┋ Note: You might have other sub folders inside the extensions directory. They don't interfere with inkscapeMadeEasy. .. note:: **LaTeX users**: The file `basicLatexPackages.tex` contains a basic set o packages and macros to be used with **InkscapeMadeEasy**. You can add your own macros to this file. This way they will be accessible to all plugins that employ inkscapeMadeEasy. LaTeX installation ------------------ Linux users ~~~~~~~~~~~ You might find useful installing the packages ``texlive-science``, ``texlive-pictures`` and ``texlive-latex-base`` (Debian based distros) from your package manager. They should provide most (all?) needed LaTeX packages. Other Linux distros should have similar packages. After installation, see if you can compile this `Minimal LaTex example`_ Windows users ~~~~~~~~~~~~~ 1) **Install Miktex:** Download and install `Miktex `_. 2) **testing pdflatex (for LaTeX experienced users):** You must make sure the `Minimal LaTex example`_ compiles correctly using pdflatex from the command prompt. Check whether you can call pdflatex from any folder, in other words, check if pdflatex is in the PATH environment variable. 2) **testing pdflatex (for LaTeX beginners)** a) Open notepad and create a text file with the contents of the `Minimal LaTex example`_ and save it somewhere with the name ``example.tex``. b) In File Explorer, go to the folder where you saved the file and click the address bar to select it (or press Alt+D). Type “cmd” into the address bar and hit Enter to open the Command Prompt with the path of the current folder already set.   c) type:  ``pdflatex example.tex`` in the command line and hit ENTER. Lots of text should appear on your console window. .. note:: Miktex might require your authorization to install additional packages. Depending on how you installed Miktex, it can install automatically without asking or ask you to confirm. Confirm it! d) Check whether pdflatex created a new pdf file with the same name. Open the pdf and see if you can read the short message and equation. Minimal LaTeX example ~~~~~~~~~~~~~~~~~~~~~ You should be able to compile the following example on your system. Compiling this example will also make sure you have all packages inkscapeMadeEasy requires.:: \documentclass[11pt]{article} \usepackage[utf8]{inputenc} \usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \usepackage[per-mode=symbol]{siunitx} \usepackage{steinmetz} \begin{document} Minimal example. Woo-hoo! \begin{align} E=mc^2 \end{align} \end{document} .. _disableLatexSupport: Disabling LaTeX support ======================= .. warning:: **By default, LaTeX support is ENABLED.** LaTeX support via TexText extension requires LaTeX typesetting system in your computer (it's free and awesome! =] ). This might be a problem to install for non-Linux systems. Since many people don't want to use LaTeX and/or don't have it installed, LaTeX support is optional. If you don't want LaTeX, you can still use **inkscapeMadeEasy** as long as you disable the support. You can easily do it by setting a flag in ``inkscapeMadeEasy_Draw.py``: 1- Open ``inkscapeMadeEasy_Draw.py`` in any text editor (e.g. Notepad in Windows. DO NOT use Microsoft word!) 2- Search for the line containing ``#useLatex=False``. It is near the the beginning of the file. 3- Remove the comment character ``#`` of this line, leaving just ``useLatex=False``. 4- Save the file, close the text editor, and restart inkscape if already opened. ================================================ FILE: docs/source/mainFeatures.rst ================================================ Optional LaTeX support via textext extension ============================================ Many of the functions implemented in inkscapeMadeEasy can use LaTeX to generate text if the support is enabled. To this end, I decided to employ the excellent extension `TexText `_ by `awesome people `_. .. note:: LaTeX support is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. Scientific plotting system ========================== **inkscapeMadeEasy_Plot** provides simple yet powerful tools to generate 2D Cartesian and Polar axes and plots (lines and Octave's stem plot style). This functionality was inspired by Tavmjong Bah's (and collaborators) extension **Function Plotter** already presented in Inkscape. Function Plotter extension is not required here. Control over text styles, line markers and other drawing features ================================================================== **inkscapeMadeEasy_Draw** module provides powerful tools to: - manipulate colours, including pre-defined named colours, grayscales, RGB conversion and the colour picker widget of .inx - create custom markers, including a few pre-defined often used types. They can be assigned to custom colours, stroke and filling independently - create line styles, with or without custom markers and custom line dash pattern - create text styles - create LaTeX contents (thanks to **TexText**) - direct control over text size and colour of LaTeX contents - draw straight polylines and bezier curves using absolute or relative coordinates - draw arcs, rectangles, ellipses and circles using different methods, based on their geometrical characteristics. Useful backstage functions ============================================ **inkscapeMadeEasy_Base** module inherits inkex.py module, extending it by providing useful core functions: - Dump objects to a text file. Rationale: As of today (2020) Inkscape does not return the stdout() during python code run. This method partially overcomes this issue by sending the object to a text file. - unique ID number generator (adapted from inkex.py, but using consecutive numbering) - list defined elements (e.g. markers) in the current document - create/remove groups - remove elements - extract the transformation matrix of an object, even when multiple transformations are stacked - rotate, scale and move objects - find markers - import SVG files - export selected elements to new SVG file - manipulate def nodes - get the list of coordinates of the points of an object or group. If the object is a group the method searches points recursively. Any eventual transformations are properly applied to the points before listing them - get the bounding box of an object or group - get the centre of the bounding box of an object or group ================================================ FILE: docs/source/minimalExample.rst ================================================ General Gist ============ The general gist of inkscapeMadeEasy is: 1) ``inkscapeMadeEasy_Base.inkscapeMadeEasy`` inherits ``inkex.Effect``, therefore has access to all ``inkex.Effect`` member functions plus all methods defined in ``inkscapeMadeEasy_Base.py`` 2) Your extension is a class that inherits ``inkscapeMadeEasy_Base.inkscapeMadeEasy``. Therefore you will have access to all member functions of ``inkscapeMadeEasy_Base.inkscapeMadeEasy`` plus the methods you define in your extension. 3) If you need anything from ``inkscapeMadeEasy_Draw`` or ``inkscapeMadeEasy_Plot``, you can import them into your project. The basic structure of your plugin should be the following: .. code-block:: python :linenos: import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot class MinimalExample(inkBase.inkscapeMadeEasy): def __init__(self): inkBase.inkscapeMadeEasy.__init__(self) ... initialization procedure, argument parser ... def effect(self): ... The method 'effect()' is the only method called directly by inkscape. Consider this function to be the main function of your plugin. ... def fooBar(self): ... You can create other methods if you want. These can be called by other methods inside your plugin but inkscape will NEVER call these directly. ... if __name__ == '__main__': myPlugin = MinimalExample() myPlugin.run() .. note:: The best way of learning how to use it is by looking at real life examples. Check my other `repositories `_. .. _minimalExample: Minimal example =============== The following is a minimal example using inkscapeMadeEasy. The structure of inkscape extensions directory should be as follows: .. code-block:: none inkscape ┣━━ extensions ┋ ┣━━ inkscapeMadeEasy <-- folder with inkscapeMadeEasy files ┃ ┣━━ inkscapeMadeEasy_Base.py ┃ ┣━━ inkscapeMadeEasy_Draw.py ┃ ┣━━ inkscapeMadeEasy_Plot.py ┃ ┗━━ basicLatexPackages.tex ┃ ┣━━ minimalExample <-- folder with minimal example files ┃ ┣━━ minimalExample.py ┃ ┗━━ minimalExample.inx ┋ minimalExample.inx ------------------ .. literalinclude:: ../../examples/minimalExample.inx :language: xml :linenos: minimalExample.py ----------------- .. literalinclude:: ../../examples/minimalExample.py :language: python :linenos: testingMinimalExample.py ------------------------ You can even run the plugin without inkscape! Create a .py file in the ``minimalExample`` directory with the following contents .. literalinclude:: ../../examples/testingMinimalExample.py :language: python :linenos: and run python from the console. .. code-block:: python python3 path/to/minimalExample/testingMinimalExample.py .. tip:: Why would you want to do this? Debugging your code! Inkscape neither has a stdout for you to dump stuff and inspect nor has a debug tool to run step by step or add break points. Running independently from inkscape allow you to run via PyCharm or other python IDEs. Use it now and thank me later. =) ================================================ FILE: docs/source/moduleDefinitions.rst ================================================ inkscapeMadeEasy_Base ===================== Base class for extensions. This class extends the inkex.Effect class by adding several basic functions to help manipulating inkscape elements. All extensions should inherit this class. .. Important:: Unless explicitly mentioned, all the code snippet presented in this section assume you are using inkscapeMadeEasy as presented in the :ref:`minimalExample` section and your are within the scope of your extension class and, therefore, can use ``self.[SOME_FUNCTION]``. .. note:: in all examples, assume inkscapeMadeEasy modules are imported with the following aliases .. code-block:: python :linenos: import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot .. automodule:: inkscapeMadeEasy_Base :members: :undoc-members: :show-inheritance: inkscapeMadeEasy_Draw ===================== This module contains classes and functions to help dealing with drawings. .. Important:: Unless explicitly mentioned, all the code snippet presented in this section assume you are using inkscapeMadeEasy as presented in the :ref:`minimalExample` section and your are within the scope of your extension class and, therefore, can use ``self.[SOME_FUNCTION]``. .. note:: in all examples, assume inkscapeMadeEasy modules are imported with the following aliases .. code-block:: python :linenos: import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot .. automodule:: inkscapeMadeEasy_Draw :members: :undoc-members: :show-inheritance: inkscapeMadeEasy_Plot ===================== This module contains classes and functions to create plots .. Important:: Unless explicitly mentioned, all the code snippet presented in this section assume you are using inkscapeMadeEasy as presented in the :ref:`minimalExample` section and your are within the scope of your extension class and, therefore, can use ``self.[SOME_FUNCTION]``. .. note:: in all examples, assume inkscapeMadeEasy modules are imported with the following aliases .. code-block:: python :linenos: import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot .. automodule:: inkscapeMadeEasy_Plot :members: :undoc-members: :show-inheritance: :exclude-members: generateListOfTicksLinear,generateListOfTicksLog10,findOrigin,getPositionAndText ================================================ FILE: examples/iME_Draw_colorPicker.inx ================================================ <_name>Draw-colorPicker iME.iME_Draw_colorPicker iME_Draw_colorPicker.py inkscapeMadeEasy_Draw.py inkscapeMadeEasy_Base.py inkex.py <_option value="#FF0022FF">my default color <_option value="none">none <_option value="black">black <_option value="red">red <_option value="blue">blue <_option value="yellow">yellow <_option value="green">green <_option value="magen">magenta <_option value="white">white <_option value="Lred">Lred <_option value="Lblue">Lblue <_option value="Lyellow">Lyellow <_option value="Lgreen">Lgreen <_option value="Lmagen">Lmagenta <_option value="Dred">Dred <_option value="Dblue">Dblue <_option value="Dyellow">Dyellow <_option value="Dgreen">Dgreen <_option value="Dmagen">Dmagenta <_option value="picker">use color picker all ================================================ FILE: examples/iME_Draw_colorPicker.py ================================================ #!/usr/bin/python import inkex import inkscapeMadeEasy_Base as inkBase import inkscapeMadeEasy_Draw as inkDraw class myExtension(inkBase.inkscapeMadeEasy): def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option("--tab", action="store", type="string", dest="tab", default="object") self.OptionParser.add_option("--myColorPicker", action="store", type="string", dest="myColorPickerVar", default='0') self.OptionParser.add_option("--myColorOption", action="store", type="string", dest="myColorOptionVar", default='0') def effect(self): color = inkDraw.color.colorPickerToRGBalpha(self.options.myColorPickerVar) # do the same thing, but first verify whether myColorOptionVar is providing a pre defined color or asking to get it from the picker. color = inkDraw.color.parseColorPicker(self.options.myColorOptionVar, self.options.myColorPickerVar) if __name__ == '__main__': x = myExtension() x.affect() ================================================ FILE: examples/iME_Draw_lineStyle_and_markers.inx ================================================ <_name>Draw-lineStyle iME.iME_Draw_lineStyle iME_Draw_lineStyle_and_markers.py inkscapeMadeEasy_Draw.py inkscapeMadeEasy_Base.py inkex.py <_option value="#FF0022FF">my default color <_option value="none">none <_option value="black">black <_option value="red">red <_option value="blue">blue <_option value="yellow">yellow <_option value="green">green <_option value="magen">magenta <_option value="white">white <_option value="Lred">Lred <_option value="Lblue">Lblue <_option value="Lyellow">Lyellow <_option value="Lgreen">Lgreen <_option value="Lmagen">Lmagenta <_option value="Dred">Dred <_option value="Dblue">Dblue <_option value="Dyellow">Dyellow <_option value="Dgreen">Dgreen <_option value="Dmagen">Dmagenta <_option value="picker">use color picker all ================================================ FILE: examples/iME_Draw_lineStyle_and_markers.py ================================================ #!/usr/bin/python import inkex import inkscapeMadeEasy_Base as inkBase import inkscapeMadeEasy_Draw as inkDraw import math class myExtension(inkBase.inkscapeMadeEasy): def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option("--myColorPicker", action="store", type="string", dest="lineColorPickerVar", default='0') self.OptionParser.add_option("--myColorOption", action="store", type="string", dest="lineColorOptionVar", default='0') def effect(self): # sets the position to the viewport center, round to next 10. position=[self.view_center[0],self.view_center[1]] position[0]=int(math.ceil(position[0] / 10.0)) * 10 position[1]=int(math.ceil(position[1] / 10.0)) * 10 # creates a dot marker, with red stroke color and gray (40%) filling color myDotMarker = inkDraw.marker.createDotMarker(self, nameID='myDot' , RenameMode=1, # overwrite an eventual markers with the same name scale=0.2, strokeColor=inkDraw.color.defined('red'), fillColor=inkDraw.color.gray(0.4)) # parses the input options to get the color of the line lineColor = inkDraw.color.parseColorPicker(self.options.lineColorOptionVar, self.options.lineColorPickerVar) # create a new line style with a 2.0 pt line and the marker just defined at both ends myLineStyleDot = inkDraw.lineStyle.set(lineWidth=2.0, lineColor=lineColor, fillColor=inkDraw.color.defined('blue'), lineJoin='round', lineCap='round', markerStart=myDotMarker, markerMid=myDotMarker, markerEnd=myDotMarker, strokeDashArray=None) #root_layer = self.current_layer root_layer = self.document.getroot() # draws a line using the new line style. (see inkscapeMadeEasy_Draw.line class for further info on this function inkDraw.line.relCoords(root_layer,coordsList= [[0,100],[100,0]],offset=position,lineStyle=myLineStyleDot) # -- Creates a second line style with ellipsis and # creates a ellipsis marker with default values infMarkerStart,infMarkerEnd = inkDraw.marker.createElipsisMarker(self, nameID='myEllipsis' , RenameMode=1) # overwrite an eventual markers with the same name # create a new line style myStyleInf = inkDraw.lineStyle.set(lineWidth=1.0, lineColor=lineColor, fillColor=None, lineJoin='round', lineCap='round', markerStart=infMarkerStart, markerMid=None, markerEnd=infMarkerEnd, strokeDashArray=None) # draws a line using the new line style. (see inkscapeMadeEasy_Draw.line class for further info on this function inkDraw.line.relCoords(root_layer,coordsList= [[0,100],[100,0]],offset=[position[0]+300,position[1]],lineStyle=myStyleInf) if __name__ == '__main__': x = myExtension() x.affect() ================================================ FILE: examples/minimalExample.inx ================================================ Minimal Example minimalExample minimalExample.py inkscapeMadeEasy/inkscapeMadeEasy_Base.py inkscapeMadeEasy/inkscapeMadeEasy_Draw.py inkscapeMadeEasy/inkscapeMadeEasy_Plot.py false false all ================================================ FILE: examples/minimalExample.py ================================================ #!/usr/bin/python import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase # in this example I am going to use both inkscapeMadeEasy_Draw.py and inkscapeMadeEasy_Plot functions, so I have to load them. import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot # your plugin must inherit inkBase.inkscapeMadeEasy class MinimalExample(inkBase.inkscapeMadeEasy): # the __init__ must initiate inkBase.inkscapeMadeEasy and build your argument parser def __init__(self): # initiate inkBase.inkscapeMadeEasy inkBase.inkscapeMadeEasy.__init__(self) # argument parser self.arg_parser.add_argument("--drawCircle", type=self.bool, dest="drawCircle", default=False) self.arg_parser.add_argument("--drawPlot", type=self.bool, dest="drawPlot", default=False) # the effect function will be the main function of your plugin. This function will 'do the stuff' def effect(self): # parse the arguments. so = self.options # get the coords of the center of the view. In this case I am calling a function inside inkex. position = [self.svg.namedview.center[0], self.svg.namedview.center[1]] # parent of all elements root_layer = self.document.getroot() # creates a group. Note 'createGroup' is an inkscapeMadeEasy_Base.inkscapeMadeEasy method. # However, MinimalExample inherits all the methods and properties from inkBase.inkscapeMadeEasy so you can call it with ``self.[SOME_METHOD]`` group = self.createGroup(root_layer, 'myGroup') # the next elements will be created inside the group if so.drawCircle: # draw a circle, showing how to use inkscapeMadeEasy_Draw.py center = [0, 0] radius = 2 # creates a line style for the circle self.lineStyle = inkDraw.lineStyle.setSimpleBlack(1.5) inkDraw.circle.centerRadius(group, center, radius, offset=position, lineStyle=self.lineStyle) if so.drawPlot: # draw a cartesian plane, showing how to use inkscapeMadeEasy_Plot.py inkPlot.axis.cartesian(self, group, xLim=[0.0, 2.0], yLim=[0.0, 2.0], position=position, xLabel='x axis', yLabel='y axis') if __name__ == '__main__': myPlugin = MinimalExample() myPlugin.run() ================================================ FILE: examples/testingMinimalExample.py ================================================ #!/usr/bin/python import os import minimalExample as myExtension #loads the extension myExt = myExtension.MinimalExample() #runs the plugin. Remember to change the paths of the svg files. # ``existing_file.svg`` is one existing file that will be modified by the example. # ``new_file.svg`` will be created with the result of the example. myExt.run([r'--drawCircle=True', r'--drawPlot=True', r'/path/to/existing_file.svg'], output=os.devnull) #save the result myExt.document.write('/path/to/new_file.svg') ================================================ FILE: latest/basicLatexPackages.tex ================================================ \usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \usepackage[per-mode=symbol,retain-explicit-plus]{siunitx} \usepackage{steinmetz} \usepackage[utf8]{inputenc} %\usepackage[T1]{fontenc} % math \newcommand{\mat}[1]{\boldsymbol{#1}} %\renewcommand{\vec}[1]{\boldsymbol{#1}} \newcommand{\anotaSobre}[2]{\overbrace{#1}^{#2}} \newcommand{\anotaSob}[2]{\underbrace{#1}_{#2}} \newcommand{\igualComent}[1]{\stackrel{#1}{=}} \newcommand{\rightarrowComent}[1]{\stackrel{#1}{\rightarrow}} \newcommand{\sseComent}[1]{\stackrel{#1}{\quad\Leftrightarrow\quad}} \newcommand{\Na}{\mathbb{N}} \renewcommand{\Re}{\mathbb{R}} \newcommand{\Zi}{\mathbb{Z}} \newcommand{\Co}{\mathbb{C}} % transpose symbol (a bit lower) %\newcommand{\tr}{^\intercal} % transpose symbol (a bit higher) \newcommand{\tr}{^\mathsf{T}} ================================================ FILE: latest/inkscapeMadeEasy_Base.py ================================================ #!/usr/bin/python # ----------------------------------------------------------------------------- # # inkscapeMadeEasy: - Helper module that extends Aaron Spike's inkex.py module, # focusing productivity in inkscape extension development # # Copyright (C) 2016 by Fernando Moura # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ----------------------------------------------------------------------------- import math import os import re import sys from copy import deepcopy import numpy as np from lxml import etree import inkex class inkscapeMadeEasy(inkex.Effect): def __init__(self): inkex.Effect.__init__(self) self.inkscapeResolution_dpi = 96.0 # number of pixels per inch resolution_in = self.inkscapeResolution_dpi resolution_mm = self.inkscapeResolution_dpi / 25.4 self.unitsDict = {'mm': resolution_mm, # 25.4mm per inch 'cm': resolution_mm * 10.0, # 1cm = 10mm 'm': resolution_mm * 1.0e3, # 1m = 1000mm 'km': resolution_mm * 1.0e6, # 1km = 1e6mm 'in': resolution_in, # 1in = 96px 'ft': resolution_in * 12.0, # foot = 12*in 'yd': resolution_in * 12.0 * 3.0, # yard = 3*ft 'pt': resolution_in / 72.0, # point 1pt = 1/72th of an inch 'px': 1.0, 'pc': resolution_in / 6.0} # picas 1pc = 1/6th of and inch self.blankSVG = """ image/svg+xml """ # --------------------------------------------- def bool(self,valueStr): """ ArgParser function to turn a boolean string into a python boolean :param valueStr: string representing a boolean. Valid values: 'true','false' with any letter capitalization :type valueStr: string :returns: boolean value :rtype: bool .. note:: This function was copied from inkex.utils.py to avoid having to import inkex.py in your project just to use inkex.Boolean in ``arg_parser.add_argument``. You can pass this function in ``arg_parser.add_argument`` method when dealing with boolean values. See :ref:`minimalExample` section for a good example. **Example** In your ``__init__`` function, ``arg_parser.add_argument`` requires a callable to convert string to bool when dealing with bool variables. See :ref:`minimalExample` section for a good example. >>> self.arg_parser.add_argument("--boolVariable", type=self.bool, dest="boolVariable", default=False) """ if valueStr.upper() == 'TRUE': return True elif valueStr.upper() == 'FALSE': return False return None # --------------------------------------------- def displayMsg(self, msg): """Display a message to the user. :param msg: message :type msg: string :returns: nothing :rtype: - """ sys.stderr.write(msg + '\n') def createEmptySVG(self,fileName): """Creates an empty svg file. .. note:: The empty file does not replace the current opened document :param fileName: valid filename and path :type .. note:: This function was c: string :returns: nothing :rtype: - """ with open(fileName,'wb') as f: f.write(self.blankSVG.encode('ascii')) # --------------------------------------------- def getBasicLatexPackagesFile(self): """Return the full path of the ``basicLatexPackages.tex`` file with commonly used Latex packages The default contents of the ``basicLatexPackages.tex`` is:: \\usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \\usepackage[per=slash]{siunitx} \\usepackage{steinmetz} :returns: Full path of the file with commonly used Latex packages :rtype: string .. note:: You can add other packages to the file ``basicLatexPackages.tex``. """ directory = os.getcwd() return os.path.abspath(directory + '/../inkscapeMadeEasy/basicLatexPackages.tex') # --------------------------------------------- def Dump(self, obj, file='./dump_file.txt', mode='w'): """Function to easily output the result of ``str(obj)`` to a file :param obj: python object to sent to a file. Any object can be used, as long as ``str(obj)`` is implemented (see ``__str__()`` metaclass definition of your object) :param file: file path. Default: ``./dump_file.txt`` :param mode: writing mode of the file Default: ``w`` (write) :type obj: any :type file: string :type mode: string :returns: nothing :rtype: - .. note:: This function was created to help debugging the code while it is running under inkscape. Since inkscape does not possess a terminal as today (2016), this function overcomes partially the issue of sending things to stdout by dumping result of the function ``str(obj)`` in a text file. **Example** >>> vector1=[1,2,3,4,5,6] >>> self.Dump(vector1,file='~/temporary.txt',mode='w') # writes the list to a file >>> vector2=[7,8,9,10] >>> self.Dump(vector2,file='~/temporary.txt',mode='a') # append the list to a file """ with open(file, mode) as file: file.write(str(obj) + '\n') # --------------------------------------------- def removeElement(self, element): """Remove one element (can be a group) of the document. If the parent of the removed element is a group and has no other children, then the parent group is also removed. :param element: inkscape element object to be removed. If the element is a group, then all its chidren are also removed. :type element: inkscape element object :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkDraw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> line3 = inkDraw.line.relCoords(groupA, [[15,0]],[10,0]) # creates a line in groupA >>> self.removeElement(line1) # removes line 1 >>> self.removeElement(line2) # removes line 2 >>> self.removeElement(line3) # removes line 3. Also removes groupA since this group has no other children >>> groupB = self.createGroup(rootLayer,label='temp1') # creates a group inside rootLayer >>> line4 = inkDraw.line.relCoords(groupB, [[5,0]],[0,0]) # creates a line in groupB >>> self.removeElement(groupB) # removes group B and all its children """ parent = element.getparent() parent.remove(element) if parent.tag == 'g' and len(parent.getchildren()) == 0: # if object's parent is a group and has no other children, remove parent too temp = parent.getparent() if temp is not None: temp.remove(parent) # --------------------------------------------- def importSVG(self, parent, fileIn, createGroup=True,position=None,scaleFactor=1.0,unifyDefs=True): """ Import SVG file into the current document :param parent: parent element where all contents will be placed :param fileIn: SVG file path :param createGroup: create a group containing all imported elements. (Default: True) :param position: set center position of the group. Used only if createGroup=True (Default: None) :param scaleFactor: set scaling factor of the group. Used only if createGroup=True (Default: 1.0) :param unifyDefs: unify the defs node via :meth:`unifyDefs`. (Default: False) :type parent: inkscape element object :type fileIn: string :type createGroup: bool :type unifyDefs: bool :returns: imported element objects. If createGroup==True, returns the group. Otherwise returns a list with all imported elements :rtype: inkscape element object or list of objects **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> imported1 = self.importSVG(rootLayer,'/path/to/file1.svg',True) # import contents of the file and group them. imported1 is the group element >>> imported2 = self.importSVG(rootLayer,'/path/to/file2.svg',False) # import contents of the file. imported2 is a list of the imported elements """ documentIn = etree.parse(fileIn, parser=etree.XMLParser(huge_tree=True)).getroot() if createGroup: group = self.createGroup(parent, label='importedSVG') for elem in documentIn: if elem.tag != inkex.addNS('namedview', 'sodipodi') and elem.tag != inkex.addNS('metadata', 'svg'): group.append(elem) if unifyDefs: self.unifyDefs() if position is not None: center = self.getCenter(group) self.moveElement(group, position-center) if scaleFactor != 1.0: self.scaleElement(group, scaleX=scaleFactor, scaleY=None, center=self.getCenter(group)) return group else: listElements=[] for elem in documentIn: if elem.tag != inkex.addNS('namedview', 'sodipodi') and elem.tag != inkex.addNS('metadata', 'svg'): parent.append(elem) if elem.tag != inkex.addNS('defs', 'svg'): listElements.append(elem) if unifyDefs: self.unifyDefs() return listElements # --------------------------------------------- def exportSVG(self, element, fileOut): """ Export the element (or list of elements) in a new svgfile. This function will export the element (or list of elements) to a new SVG file. If a list of elements is passed as argument, all elements in the list will be exported to the same file. :param element: element or list of elements to be exported :param fileOut: file path, including the extension. :type element: inkscape element object or list of inkscape element objects :type file: string :returns: nothing :rtype: - .. note:: Currently (2020), all the defs of the original file will be copied to the new file. Therefore you might want to run the vacuum tool to cleanup the new SVG file ``File > Clean um document`` **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> groupB = self.createGroup(rootLayer,label='child') # creates a group inside groupA >>> line1 = inkDraw.line.relCoords(groupA, [[10,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(groupB, [[20,0]],[0,0]) # creates a line in groupB >>> self.exportSVG(line1,'path/to/file1.svg') # exports only line1 >>> self.exportSVG(groupA,'path/to/file2.svg') # exports groupA (and all elements it contais) >>> self.exportSVG([groupA,groupB],'path/to/file3.svg') # exports groupA and groupB (and all elements they contain) to the same file """ document = etree.fromstring(self.blankSVG.encode('ascii')) elem_tmp = deepcopy(element) # add definitions defs_tmp = deepcopy(self.getDefinitions()) document.append(defs_tmp) # add elements if isinstance(elem_tmp, list): for e in elem_tmp: document.append(e) else: document.append(elem_tmp) et = etree.ElementTree(document) et.write(fileOut, pretty_print=True) # --------------------------------------------- def uniqueIdNumber(self, prefix_id): """ Generate an unique element ID number with a given prefix ID by adding a numeric suffix This function is used to generate a valid unique ID by concatenating a given prefix with a numeric suffix. The overall format is ``prefix-%05d``. This function makes sure the ID is unique by checking in ``doc_ids`` member. This function is specially useful for creating an unique ID for markers and other elements in defs. :param prefix_id: prefix of the ID :type prefix_id: string :returns: the unique ID :rtype: string .. note:: This function has been adapted from inkex.py. However it uses an incremental number method **Example** >>> a=self.uniqueIdNumber('myName') # a=myName-00001 >>> b=self.uniqueIdNumber('myName') # b=myName-00002, because myName-00001 is already in use >>> c=self.uniqueIdNumber('myName') # c=myName-00003, because myName-00001 and myName-00002 are already in use >>> d=self.uniqueIdNumber('myNameX') # d=myNameX-00001 """ numberID = 1 new_id = prefix_id + '-%05d' % numberID while new_id in self.get_ids(): numberID += 1 new_id = prefix_id + '-%05d' % numberID self.svg.get_ids().add(new_id) return new_id # add an unique id to the elements #for tag in svg.iter(): # if 'id' not in tag.attrib: # newID=self.svg.get_unique_id( prefix='imported_', size=None) # tag.attrib['id']=newID # --------------------------------------------- def getDefinitions(self): """ Return the element of the svg file. This function returns the principal element of the current svg file. if no can be found, a new empty is created :returns: the defs element :rtype: inkscape element object """ defs = self.getElemFromXpath('/svg:svg//svg:defs') if defs is None: defs = etree.SubElement(self.document.getroot(), inkex.addNS('defs', 'svg')) return defs def cleanDefs(self, removeUnused=False, unifyDuplicates=False): def checkEquality(elemA,elemB): # check equality between two elements. do it recursively # https://stackoverflow.com/questions/7905380/testing-equivalence-of-xml-etree-elementtree if len(elemA) != len(elemB): return False if elemA.text != elemB.text: if elemA.text is not None: valA=elemA.text.rstrip() if elemB.text is not None: valB=elemB.text.rstrip() if valA != valB: return False elemA_Id = self.getElemAttrib(elemA, 'id') elemB_Id = self.getElemAttrib(elemB, 'id') elemA_attrib = deepcopy(elemA.attrib) elemB_attrib = deepcopy(elemB.attrib) # remove ID from attrib del elemA_attrib['id'] del elemB_attrib['id'] # remove trailinq zeros of any numbers in the attributes for key, value in elemA_attrib.items(): elemA_attrib[key] = re.sub(r"\.0+\b", '', value) for key, value in elemB_attrib.items(): elemB_attrib[key] = re.sub(r"\.0+\b", '', value) #print(elemA_attrib) #print(elemB_attrib) if elemA_attrib != elemB_attrib: return False return all(checkEquality(c1, c2) for c1, c2 in zip(elemA, elemB)) for eRef in self.getDefinitions().iterchildren(inkex.addNS('marker', 'svg')): idRef = self.getElemAttrib(eRef, 'id') for e in self.getDefinitions().iterchildren(inkex.addNS('marker', 'svg')): id = self.getElemAttrib(e, 'id') if idRef != id: isEqual = checkEquality(eRef,e) if isEqual: print ('[%s] and [%s] are equal!' % (idRef , id) ) root = self.getElemFromXpath('/svg:svg') for elem in root.iter(): if elem.tag not in set(['defs', inkex.addNS('defs', 'svg')]): try: if elem.attrib['id'] != id: for key, value in elem.items(): elem.attrib[key] = elem.attrib[key].replace('url(#%s)' % id, 'url(#%s)' % idRef ) except: pass # --------------------------------------------- def unifyDefs(self,ungroupChild=False): """Unify all nodes in a single node. :param ungroupChild: if any entry is a group, removes the group. (Default: False) :type ungroupChild: bool :returns: None :rtype: - .. warning:: This function does not check whether the ids are unique! """ root = self.getElemFromXpath('/svg:svg') mainDef = self.getDefinitions() for d in root.findall('.//svg:defs', namespaces=inkex.NSS): if d != mainDef: for child in d: mainDef.append(child) if ungroupChild: if child.tag == inkex.addNS('g', 'svg') or child.tag == 'g': self.ungroup(child) d.getparent().remove(d) # --------------------------------------------- def getDefsByTag(self, tag='marker'): """ Return a list of elements in of a given a tag. :param tag: tag of the element :type tag: string :returns: a list with the def elements :rtype: list of inkscape elements """ return self.getDefinitions().findall('.//svg:%s' % tag, namespaces=inkex.NSS) # --------------------------------------------- def getDefsById(self,id): """ Return a list of elements in of a given (part of) id :param id: (part of the id of the element) :type tag: string :returns: a list with the def elements :rtype: list of inkscape elements """ return self.getDefinitions().xpath('./*[contains(@id,"%s")]' % id) # --------------------------------------------- def getElemFromXpath(self, xpath): """ Return the element from the xml, given its xpath :param xpath: xpath of the element to be searched :type xpath: string :returns: inkscape element object :rtype: inkscape element object **Example** >>> name = self.getElemFromXpath('/svg:svg//svg:defs') # returns the list of definitions of the document """ return self.svg.getElement(xpath) # --------------------------------------------- def getElemAttrib(self, elem, attribName): """ Return the atribute of one element, given the atribute name :param elem: element under consideration :param attribName: attribute to be searched. Format: namespace:attrName :type elem: inkscape element object :type attribName: string :returns: attribute :rtype: string **Example** >>> elem= self.getElemFromXpath('/svg:svg') # first get the element. In this example, the entire document >>> docNAme = self.getElemAttrib(elem,'sodipodi:docname') # now get the name of the document, an attribute of svg:svg """ # splits namespace and attrib name atribList = attribName.split(':') if len(atribList) == 1: # if has no namespace attrib = attribName else: # if has namespace namespace = inkex.NSS[atribList[0]] attrib = '{%s}' % namespace + atribList[1] return elem.attrib[attrib] # --------------------------------------------- def getDocumentScaleFactor(self): """Return the scale factor of the document. The scale factor is defined as .. math:: S=\\frac{\\text{document width}}{\\text{viewbox width}} **Example** >>> scale = self.getDocumentScaleFactor() """ try: elem = self.getElemFromXpath('/svg:svg') width = float(self.getElemAttrib(elem, 'width').replace(self.documentUnit, '')) viewBox = self.getElemFromXpath('/svg:svg') viewBox_width = float(self.getElemAttrib(viewBox, 'viewBox').split(' ')[2]) doc_scale = viewBox_width / width except: doc_scale = 1.0 return doc_scale # --------------------------------------------- def getDocumentName(self): """Return the name of the document :returns: fileName :rtype: string **Example** >>> name = self.getDocumentName() """ elem = self.getElemFromXpath('/svg:svg') try: fileName = self.getElemAttrib(elem, 'sodipodi:docname') except: fileName = None return fileName # --------------------------------------------- def getDocumentUnit(self): """Return the unit of the document :returns: unit string code. See table below :rtype: string **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> docunit = self.getDocumentUnit() #returns 'cm', 'mm', etc. """ elem = self.getElemFromXpath('/svg:svg/sodipodi:namedview') try: unit = self.getElemAttrib(elem, 'inkscape:document-units') except: unit = 'px' return unit # --------------------------------------------- def getcurrentLayer(self): """Return the current layer of the document :returns: Name of the current layer :rtype: string **Example** >>> name = self.getcurrentLayer() """ return self.svg.get_current_layer() # --------------------------------------------- def abs2relPath(self, element): abspath = self.getElemAttrib(element, 'sodipodi:absref') fileName = os.path.basename(abspath) # removes sodipodi:absref attribute namespace = inkex.NSS['sodipodi'] attrib = '{%s}' % namespace + 'absref' element.attrib.pop(attrib, None) # adds sodipodi:relref attrib = '{%s}' % namespace + 'relref' element.set(attrib, fileName) # --------------------------------------------- def unit2userUnit(self, value, unit_in): """Convert a value from given unit to inkscape's default unit (px) :param value: value to be converted :param unit_in: input unit string code. See table below :type value: float :type unit_in: string :returns: converted value :rtype: float **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> x_cm = 5.0 >>> x_px = self.unit2userUnit(x_cm,'cm') # converts 5.0cm -> 188.97px """ return value * self.unitsDict[unit_in.lower()] # --------------------------------------------- def userUnit2unit(self, value, unit_out): """Convert a value from inkscape's default unit (px) to specified unit :param value: value to be converted :param unit_out: output unit string code. See table below :type value: float :type unit_out: string :returns: converted value :rtype: float **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> x_px = 5.0 >>> x_cm = self.userUnit2unit(x_px,'cm') # converts 5.0px -> 0.1322cm """ return value / float(self.unitsDict[unit_out.lower()]) # --------------------------------------------- def unit2unit(self, value, unit_in, unit_out): """Convert a value from one unit to another unit :param value: value to be converted :param unit_in: input unit string code. See table below :param unit_out: output unit string code. See table below :type value: float :type unit_in: string :type unit_out: string :returns: converted value :rtype: float **Units** The list of available units are: ================== ============ ============= Name string code relation ================== ============ ============= millimetre mm 1in = 25.4mm centimetre cm 1cm = 10mm metre m 1m = 100cm kilometre km 1km = 1000m inch in 1in = 96px foot ft 1ft = 12in yard yd 1yd = 3ft point pt 1in = 72pt pixel px pica pc 1in = 6pc ================== ============ ============= **Example** >>> x_in = 5.0 >>> x_cm = self.unit2unit(x_in,'in','cm') # converts 5.0in -> 12.7cm """ return value * self.unitsDict[unit_in.lower()] / float(self.unitsDict[unit_out.lower()]) # --------------------------------------------- def createGroup(self, parent, label=None): """Create a new empty group of elements. This function creates a new empty group of elements. To create new elements inside this groups you must create them informing the group as the parent element. :param parent: parent object of the group. It can be another group or the root element :param label: label of the group. Default: ``None``. The label does not have to be unique :type parent: inkscape element object :type label: string :returns: the group object :rtype: group element **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> groupB = self.createGroup(groupA,label='child') # creates a group inside groupA >>> line1 = inkDraw.line.relCoords(groupA, [[10,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(groupB, [[20,0]],[0,0]) # creates a line in groupB """ if label is not None: g_attribs = {inkex.addNS('label', 'inkscape'): label} group = etree.SubElement(parent, 'g', g_attribs) else: group = etree.SubElement(parent, 'g') return group # --------------------------------------------- def ungroup(self, group): """Ungroup elements The new parent element of the ungrouped elements will be the parent of the removed group. See example below :param group: group to be removed :type group: group element :returns: list of the elements previously contained in the group :rtype: list of inkscape object elements **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> groupB = self.createGroup(groupA,label='temp') # creates a group inside groupA >>> line1 = inkDraw.line.relCoords(groupA, [[10,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(groupB, [[20,0]],[0,0]) # creates a line in groupB >>> line3 = inkDraw.line.relCoords(groupB, [[30,0]],[0,0]) # creates a line in groupB >>> # at this point, the file struct is: rootLayer[groupA[ line1, groupB[ line2, line3 ] ]] >>> elemList = self.ungroup(groupB) # ungroup line2 and line3. elemList is a list containing line2 and line3 elements. >>> # now the file struct is: rootLayer[groupA[ line1, line2, line3 ]] """ if group.tag == 'g' or group.tag == inkex.addNS('g', 'svg'): # if object is a group parent = group.getparent() listElem=[] if parent is not None: for child in group: parent.append(child) listElem.append(child) self.removeElement(group) return listElem # --------------------------------------------- def getTransformMatrix(self, element): """Return the transformation attribute of the given element and the resulting 3x3 transformation matrix (numpy Array) This function is used to extract the transformation operator of a given element. :param element: element object with the transformation matrix :type element: inkscape element object :returns: list [transfAttrib, transfMatrix] - transfAttrib: string containing all transformations as it is in the file - transfMatrix: numpy array with the resulting 3x3 transformation matrix :rtype: tuple .. note :: If the element does not have any transformation attribute, this function returns: - transfAttrib='' (empty string) - transfMatrix= 3x3 identity matrix """ transfAttrib = '' transfMatrix = np.eye(3) if 'transform' in element.attrib: transfAttrib = element.attrib['transform'] if not transfAttrib: return transfAttrib, transfMatrix # split operation into several strings listOperations = [e + ')' for e in transfAttrib.replace(',', ' ').split(')') if e != ""] for operation in listOperations: if 'translate' in operation: data = re.compile(r"translate\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values x = float(data[0]) if len(data) == 2: y = float(data[1]) else: y = 0.0 mat = np.array([[1, 0, x], [0, 1, y], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) if 'scale' in operation: data = re.compile(r"scale\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values scalex = float(data[0]) if len(data) == 2: scaley = float(data[1]) else: scaley = scalex mat = np.diag([scalex, scaley, 1]) transfMatrix = np.dot(transfMatrix, mat) if 'rotate' in operation: data = re.compile(r"rotate\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values angleRad = -float(data[0]) * np.pi / 180.0 # negative angle because inkscape is upside down =( matRot = np.array([[np.cos(angleRad), np.sin(angleRad), 0], [-np.sin(angleRad), np.cos(angleRad), 0], [0, 0, 1]]) if len(data) == 3: # must translate before and after rotation x = float(data[1]) y = float(data[2]) matBefore = np.array([[1, 0, x], [0, 1, y], [0, 0, 1]]) # translation before rotation matAfter = np.array([[1, 0, -x], [0, 1, -y], [0, 0, 1]]) # translation after rotation matRot = np.dot(matBefore, matRot) matRot = np.dot(matRot, matAfter) transfMatrix = np.dot(transfMatrix, matRot) if 'skewX' in operation: data = re.compile(r"skewX\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values angleRad = float(data[0]) * np.pi / 180.0 mat = np.array([[1, np.tan(angleRad), 0], [0, 1, 0], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) if 'skewY' in operation: data = re.compile(r"skewY\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values angleRad = float(data[0]) * np.pi / 180.0 mat = np.array([[1, 0, 0], [np.tan(angleRad), 1, 0], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) if 'matrix' in operation: data = re.compile(r"matrix\((.*?\S)\)").match(operation.lstrip()).group(1).split() # retrieves x and y values a = float(data[0]) b = float(data[1]) c = float(data[2]) d = float(data[3]) e = float(data[4]) f = float(data[5]) mat = np.array([[a, c, e], [b, d, f], [0, 0, 1]]) transfMatrix = np.dot(transfMatrix, mat) return transfAttrib, transfMatrix # --------------------------------------------- def rotateElement(self, element, center, angleDeg): """apply a rotation to the element using the transformation matrix attribute. It is possible to rotate isolated elements or groups. :param element: element object to be rotated :param center: center point of rotation :param angleDeg: angle of rotation in degrees, counter-clockwise direction :type element: inkscape element object :type center: list :type angleDeg: float :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkDraw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> self.rotateElement(line2,[0,0],120) # rotates line2 120 degrees around center x=0,y=0 >>> self.rotateElement(groupA,[1,1],-90) # rotates groupA -90 degrees around center x=1,y=1 """ transfString = '' if angleDeg == 0: return if 'transform' in element.attrib: transfString = element.attrib['transform'] # if transform attribute is present, we must add the new rotation if transfString: newTransform = 'rotate(%f %f %f) %s' % (-angleDeg, center[0], center[1], transfString) # negative angle bc inkscape is upside down else: # if no transform attribute was found newTransform = 'rotate(%f %f %f)' % (-angleDeg, center[0], center[1]) # negative angle bc inkscape is upside down element.attrib['transform'] = newTransform def copyElement(self, element, newParent, distance=None, angleDeg=None): """Copy one element to the same parent or other parent group. It is possible to copy elements isolated or entire groups. :param element: element object to be copied :param newParent: New parent object. Can be another group or the same group :param distance: moving distance of the new copy. The coordinates are relative to the original position. If ``None``, then the copy is placed at the same position :param angleDeg: angle of rotation in degrees, counter-clockwise direction :type element: inkscape element object :type newParent: inkscape element object :type distance: list :type angleDeg: float :returns: new element :rtype: inkscape element object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkDraw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> self.copyElement(line2,groupA) # create a copy of line2 in groupA >>> self.moveElement(groupA,[10,-10]) # moves line2 DeltaX=10, DdeltaY=-10 """ newElem = deepcopy(element) newParent.append(newElem) if distance is not None: self.moveElement(newElem, distance) if angleDeg is not None: self.rotateElement(newElem, self.getCenter(newElem), angleDeg) return newElem # --------------------------------------------- def moveElement(self, element, distance): """Move the element using the transformation attribute. It is possible to move elements isolated or entire groups. :param element: element object to be moved :param distance: moving distance. The coordinates are relative to the original position. :type element: inkscape element object :type distance: tuple :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> line1 = inkDraw.line.relCoords(groupA, [[5,0]],[0,0]) # creates a line in groupA >>> line2 = inkDraw.line.relCoords(rootLayer, [[5,0]],[0,0]) # creates a line in rootLayer >>> self.moveElement(line2,[10,10]) # moves line2 DeltaX=10, DdeltaY=10 >>> self.moveElement(groupA,[10,-10]) # moves line2 DeltaX=10, DdeltaY=-10 """ if distance[0] == 0 and distance[1] == 0: return transfString = '' if 'transform' in element.attrib: transfString = element.attrib['transform'] # if transform attribute is present, we must add the new translation if transfString: newTransform = 'translate(%f %f) %s ' % (distance[0], distance[1], transfString) else: # if no transform attribute was found newTransform = 'translate(%f %f)' % (distance[0], distance[1]) element.attrib['transform'] = newTransform # --------------------------------------------- def scaleElement(self, element, scaleX=1.0, scaleY=None, center=None): """Scale the element using the transformation attribute. It is possible to scale elements isolated or entire groups. :param element: element object to be scaled :param scaleX: scaling factor in X direction. Default=1.0 :param scaleY: scaling factor in Y direction. Default=``None``. If scaleY=``None``, then scaleY=scaleX is assumed (default behavior) :param center: center point considered as the origin for the scaling. Default=``None``. If ``None``, the origin is adopted :type element: inkscape element object :type scaleX: float :type scaleX: float :type center: list :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> groupA = self.createGroup(rootLayer,label='temp') # creates a group inside rootLayer >>> circ1 = centerRadius(groupA,centerPoint=[0,0],radius=1.0) # creates a line in groupA >>> circ2 = centerRadius(rootLayer,centerPoint=[0,0],radius=1.0) # creates a line in rootLayer >>> self.scaleElement(circ1,2.0) # scales x2 in both X and Y directions >>> self.scaleElement(circ1,2.0,3.0) # scales x2 in X and x3 in Y >>> self.scaleElement(groupA,0.5) # scales x0.5 the group in both X and Y directions """ if center is not None: self.moveElement(element, [-center[0], -center[1]]) transfString = '' if 'transform' in element.attrib: transfString = element.attrib['transform'] # if transform attribute is present, we must add the new translation if transfString: if scaleY is not None: newTransform = 'scale(%f %f) %s ' % (scaleX, scaleY, transfString) else: newTransform = 'scale(%f) %s' % (scaleX, transfString) else: # if no transform attribute was found if scaleY is not None: newTransform = 'scale(%f %f)' % (scaleX, scaleY) else: newTransform = 'scale(%f)' % scaleX element.attrib['transform'] = newTransform if center is not None: self.moveElement(element, [center[0], center[1]]) # --------------------------------------------- def addAttribute(self, element, attributeName, attributeValue,forceWrite=False): """Add a new attribute to the element. If the attribute already exists, forceWrite=True overwrites it. Otherwise the attribute is left unchanged. :param element: element object :param attributeName: attribute name :param attributeValue: attribute value :param forceWrite: force write if the attibute already exists :type element: inkscape element object :type attributeName: string :type attributeValue: string :type forceWrite: bool :returns: nothing :rtype: - **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkDraw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA, using relative coordinates >>> list = self.getPoints(line1) # list = [[0.0, 0.0], [5.0, 0.0], [5.0, 6.0]] >>> self.addAttribute(line1, attributeName='myInfo', attributeValue='my Data = %f' %3.5, forceWrite=True) # add new attribute >>> self.addAttribute(line1, attributeName='myInfo', attributeValue='my Data = %f' %4.5, forceWrite=False) # does nothing. attribute already exists >>> self.addAttribute(line1, attributeName='myInfo', attributeValue='my Data = %f' %5.5, forceWrite=True) # overwrite attribute """ if forceWrite: element.attrib[attributeName] = attributeValue else: if attributeName in element.attrib: return None return element # --------------------------------------------- def findMarker(self, markerName): """Search for markerName definition in the document. :param markerName: name of the marker :type markerName: string :returns: True if markerName is in the document. False otherwise :rtype: bool """ list = self.getDefsByTag(tag='marker') for m in list: if m.get('id') == markerName: return True return False # --------------------------------------------- def getPoints(self, element): """Returns a list of points of the element. This function works on paths, texts, groups, uses, rects. In the case of a group, the function will include recursively all its components. :param element: element object :type element: inkscape element object :returns: array of points :rtype: numpy array .. note:: This function will apply any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkDraw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA, using relative coordinates >>> list = self.getPoints(line1) # list = [[0.0, 0.0], [5.0, 0.0], [5.0, 6.0]] """ # stores the list of coordinates listCoords = [] # check if element is valid. 'path', 'text' and 'g' are valid accepted_strings = set(['path', inkex.addNS('path', 'svg'), inkex.addNS('text', 'svg'), 'g', inkex.addNS('g', 'svg'), 'use', inkex.addNS('use', 'svg'),'rect', inkex.addNS('rect', 'svg')]) if element.tag not in accepted_strings: print('getPoints: Element type [ %s ] ignored...' % element.tag) return listCoords #print(element.tag, element.attrib) if element.tag in [inkex.addNS('path', 'svg'), 'path']: # if object is path # adds special character between letters and splits. the first regular expression excludes e and E bc they are used to represent scientific notation =S dString = re.sub('([a-df-zA-DF-Z])+?', r'#\1#', element.attrib['d']).replace('z', '').replace('Z', '').replace(',', ' ').split('#') dString = [i.lstrip() for i in dString] # removes leading spaces from strings dString = list(filter(None, dString)) # removes empty elements Xcurrent = 0 Ycurrent = 0 while len(dString) > 0: commandType = dString[0] argument = [float(x) for x in dString[1].split()] # extracts arguments from M command and converts to float del dString[0] del dString[0] if commandType in 'mMlLtT': # extracts points from command 'move to' M/m or 'line to' l/L or 'smooth quadratic Bezier curveto't/T X = argument[0::2] # 2 parameters per segment, x is 1st Y = argument[1::2] # 2 parameters per segment, y is 2nd if commandType in 'hH': # extracts points from command 'horizontal line' h/H X = argument Y = [Ycurrent] * len(X) if commandType in 'vV': # extracts points from command 'vertical line' v/V Y = argument X = [Xcurrent] * len(Y) if commandType in 'cC': # extracts points from command 'Bezier Curve' c/C X = argument[4::6] # 6 parameters per segment, x is 5th Y = argument[5::6] # 6 parameters per segment, y is 6th if commandType in 'sSqQ': # extracts points from command 'quadratic Bezier Curve' q/Q or 'smooth curveto' s/S X = argument[2::4] # 4 parameters per segment, x is 3rd Y = argument[3::4] # 4 parameters per segment, y is 4th if commandType in 'aA': # extracts points from command 'arc' a/A X = argument[5::7] # 7 parameters per segment, x is 6th Y = argument[6::7] # 7 parameters per segment, y is 7th if commandType in 'h': # if h for i in range(0, len(X)): # convert to abs coordinates if i == 0: X[i] = X[i] + Xcurrent else: X[i] = X[i] + X[i - 1] if commandType in 'v': # if v for i in range(0, len(Y)): # convert to abs coordinates if i == 0: Y[i] = Y[i] + Ycurrent else: Y[i] = Y[i] + Y[i - 1] if commandType in 'mltcsqa': # if m or l for i in range(0, len(X)): # convert to abs coordinates if i == 0: X[i] = X[i] + Xcurrent Y[i] = Y[i] + Ycurrent else: X[i] = X[i] + X[i - 1] Y[i] = Y[i] + Y[i - 1] coords = zip(X, Y) listCoords.extend(coords) Xcurrent = X[-1] Ycurrent = Y[-1] if element.tag in ['rect', inkex.addNS('rect', 'svg')]: # if object is a rect if 'x' in element.attrib and 'y' in element.attrib: x0 = float(element.attrib['x']) y0 = float(element.attrib['y']) h = float(element.attrib['height']) w = float(element.attrib['width']) coords = [[x0, y0],[x0+w, y0+h],[x0+w, y0+h],[x0, y0+h]] listCoords.extend(coords) if element.tag in ['text', inkex.addNS('text', 'svg')]: # if object is a text if 'x' in element.attrib and 'y' in element.attrib: x = float(element.attrib['x']) y = float(element.attrib['y']) coords = [[x, y]] listCoords.extend(coords) if element.tag in ['g', inkex.addNS('g', 'svg')]: # if object is a group for obj in element.iterchildren("*"): if obj != element and obj.tag not in [ 'defs', inkex.addNS('defs', 'svg')]: listPoints = self.getPoints(obj) listCoords.extend(listPoints) if element.tag in ['use', inkex.addNS('use', 'svg')]: # if object is a use listCoordsTemp = [] if 'x' in element.attrib: x = float(element.attrib['x']) else: x=0 if 'y' in element.attrib: y = float(element.attrib['y']) else: y=0 link = self.getElemAttrib(element, 'xlink:href').replace('#','') elemLink = self.svg.getElementById(link) if elemLink is not None: for obj in elemLink.iter(): if obj != elemLink: listPoints = self.getPoints(obj) listCoordsTemp.extend(listPoints) #apply translation listCoords=[[coord[0]+x,coord[1]+y] for coord in listCoordsTemp] # apply transformation if len(listCoords)>0: # creates numpy array with the points to be transformed transfMat = self.getTransformMatrix(element)[1] coordsNP = np.hstack((np.array(listCoords), np.ones([len(listCoords), 1]))).transpose() coordsTransformed = np.dot(transfMat, coordsNP) coordsTransformed = np.delete(coordsTransformed, 2, 0).transpose() # remove last line, transposes and converts to list of lists else: coordsTransformed = np.array([]) return coordsTransformed # --------------------------------------------- def getBoundingBox(self, element): """Return the bounding Box of the element. This function works on paths, texts or groups. In the case of a group, the function will consider recursively all its components :param element: element object :type element: inkscape element object :returns: two lists: [xMin,yMin] and [xMax,yMax] :rtype: list .. note:: This function will appply any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkDraw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA >>> BboxMin,BboxMax = self.getBoundingBox(line1) # gets BboxMin = [0.0, 0.0] and BboxMax = [5.0, 6.0] """ coords = self.getPoints(element) coordsNP = np.array(coords) bboxMax = np.max(coordsNP, 0) bboxMin = np.min(coordsNP, 0) return bboxMin.tolist(), bboxMax.tolist() # --------------------------------------------- def getCenter(self, element): """Return the center coordinates of the bounding Box of the element. This function works on paths, texts or groups. In the case of a group, the function will consider recursively all its components :param element: element object :type element: inkscape element object :returns: list: [xCenter, yCenter] :rtype: list .. note:: This function will apply any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkDraw.line.relCoords(rootLayer, [[5,0],[0,6]],[0,0]) # creates a line in groupA >>> Center = self.getCenter(line1) # gets Center = [2.5, 3.0] """ bboxMin, bboxMax = self.getBoundingBox(element) bboxCenter = np.array([(bboxMax[0] + bboxMin[0]) / 2, (bboxMax[1] + bboxMin[1]) / 2]) return bboxCenter def getSegmentFromPoints(self, pointList, normalDirection='R'): """Given two points of a straight line segment, returns the parameters of that segment: length, angle (in radians), tangent unitary vector and normal unitary vector :param pointList: start and end coordinates [ Pstart, Pend ] :param normalDirection: - 'R': normal vector points to the right of the tangent vector (Default) - 'L': normal vector points to the left of the tangent vector :type pointList: list of points :type normalDirection: string :returns: list: [length, theta, t_versor,n_versor] :rtype: list **Example** >>> segmentParam = getSegmentFromPoints([[1,1],[2,2]],'R') # returns [1.4142, 0.78540, [0.7071,0.7071], [0.7071,-0.7071] ] >>> segmentParam = getSegmentFromPoints([[1,1],[2,2]],'L') # returns [1.4142, 0.78540, [0.7071,0.7071], [-0.7071,0.7071] ] """ # tangent versor (pointing P2) P1 = np.array(pointList[0]) P2 = np.array(pointList[1]) t_vector = P2 - P1 length = np.linalg.norm(t_vector) t_versor = t_vector / length # normal vector: counter-clockwise with respect to tangent vector if normalDirection in 'rR': n_versor = np.array([t_versor[1], -t_versor[0]]) if normalDirection in 'lL': n_versor = np.array([-t_versor[1], t_versor[0]]) # angle theta = math.atan2(t_versor[1], t_versor[0]) return [length, theta, t_versor, n_versor] def getSegmentParameters(self, element, normalDirection='R'): """Given a path segment composed by only two points, returns the parameters of that segment: length, angle (in radians), start point, end point, tangent unitary vector and normal unitary vector This function works with paths only. - If the element is not a path, the function returns an empty list - If the path element has more than two points, the function returns an empty list :param element: path element object :param normalDirection: - 'R': normal vector points to the right of the tangent vector (Default) - 'L': normal vector points to the left of the tangent vector :type element: inkscape element object :type normalDirection: string :returns: list: [Pstart,Pend,length, theta, t_versor,n_versor] :rtype: list .. note:: This function will apply any transformation stored in transform attribute, that is, it will compute the resulting coordinates of each object **Example** >>> rootLayer = self.document.getroot() # retrieves the root layer of the file >>> line1 = inkDraw.line.absCoords(rootLayer, [[1,1],[2,2]]) # creates a line in groupA >>> segementList = getSegmentParameters(line1,'R') # returns [[1,1], [2,2],1.4142, 0.78540, [0.7071,0.7071], [0.7071,-0.7071] ] """ # check if element is valid. 'path' accepted_strings = set([inkex.addNS('path', 'svg'), 'path']) if element.tag not in accepted_strings: return [] listPoints = self.getPoints(element) if len(listPoints) > 2: # if the path has more than two points return [] data = self.getSegmentFromPoints(listPoints, normalDirection) return [listPoints[0], listPoints[1]] + data ================================================ FILE: latest/inkscapeMadeEasy_Draw.py ================================================ #!/usr/bin/python # -------------------------------------------------------------------------------------- # # inkscapeMadeEasy: - Helper module that extends Aaron Spike's inkex.py module, # focusing productivity in inkscape extension development # # Copyright (C) 2016 by Fernando Moura # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # -------------------------------------------------------------------------------------- # Please uncomment (remove the # character) in the following line to disable LaTeX support via textext extension. # useLatex=False import os import sys try: useLatex except NameError: useLatex = True else: useLatex = False import math import numpy as np import inkex from lxml import etree import re if useLatex: sys.path.append('../textext') import textext.base as textext import tempfile import copy def displayMsg(msg): """Display a message to the user. :param msg: message :type msg: string :returns: nothing :rtype: - .. note:: Identical function has been also defined inside :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy` class """ sys.stderr.write(msg + '\n') def Dump(obj, file='./dump_file.txt', mode='w'): """Function to easily output the result of ``str(obj)`` to a file :param obj: python object to sent to a file. Any object can be used, as long as ``str(obj)`` is implemented (see ``__str__()`` metaclass definition of your object) :param file: file path. Default: ``./dump_file.txt`` :param mode: writing mode of the file Default: ``w`` (write) :type obj: any :type file: string :type mode: string :returns: nothing :rtype: - .. note:: Identical function has been also defined inside :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy` class This function was created to help debugging the code while it is running under inkscape. Since inkscape does not possess a terminal as today (2016), this function overcomes partially the issue of sending things to stdout by dumping result of the function ``str()`` in a text file. **Example** >>> vector1=[1,2,3,4,5,6] >>> inkDraw.Dump(vector1,file='~/temporary.txt',mode='w') # writes the list to a file >>> vector2=[7,8,9,10] >>> inkDraw.Dump(vector2,file='~/temporary.txt',mode='a') # append the list to a file """ with open(file, mode) as file: file.write(str(obj) + '\n') def circle3Points(P1, P2, P3): """Find the center and radius of a circle based on 3 points on the circle. Returns [None,None] either if the points are aligned and no (finite radius) circle can be defined or if two of the points are coincident. :param P1,P2,P3: points [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :type P1,P2,P3: list :returns: [center, radius] :rtype: [numpy array, float] """ # check if points are aligned v1 = np.array(P1) - np.array(P2) v2 = np.array(P1) - np.array(P3) if np.sqrt((v1[0] ** 2 + v1[1] ** 2))==0 or np.sqrt((v2[0] ** 2 + v2[1] ** 2))==0: displayMsg('Error: Two of the points are coincident. Aborting it...') return [None, None] v1 = v1 / np.sqrt((v1[0] ** 2 + v1[1] ** 2)) v2 = v2 / np.sqrt((v2[0] ** 2 + v2[1] ** 2)) cosTheta = v1.dot(v2) if abs(cosTheta) > 0.99999: displayMsg('Error: The 3 points are collinear (or very close). Aborting it...') return [None, None] # find the center A = np.array([[-2 * P1[0], -2 * P1[1], 1], [-2 * P2[0], -2 * P2[1], 1], [-2 * P3[0], - 2 * P3[1], 1]]) b = np.array([-P1[0] ** 2 - P1[1] ** 2, -P2[0] ** 2 - P2[1] ** 2, -P3[0] ** 2 - P3[1] ** 2]) x = np.linalg.solve(A, b) center = np.array([x[0], x[1]]) radius = np.sqrt(x[0] ** 2 + x[1] ** 2 - x[2]) return [center, radius] class color(): """ This class manipulates color information, generating a string in inkscape's expected format ``#RRGGBBAA`` .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def defined(colorName,alpha=1.0): """ Return the color string representing a predefined color name :param colorName: prededined color name. See figure below :type colorName: string :param alpha: alpha channel. Values between 0.0 and 1.0 :type alpha: float :returns: string representing the color in inkscape's expected format ``#RRGGBBAA`` :rtype: string **Available pre defined colors** .. image:: ../imagesDocs/Default_colors.png :width: 400px **Example** >>> colorString = inkDraw.color.defined('red',1.0) # returns #ff0000ff representing red color """ if colorName not in ['Dred', 'red', 'Lred', 'Dblue', 'blue', 'Lblue', 'Dgreen', 'green', 'Lgreen', 'Dyellow', 'yellow', 'Lyellow', 'Dmagen', 'magen', 'Lmagen', 'black', 'white']: sys.exit("InkscapeDraw.color.defined() : Error. color -->" + colorName + "<-- not defined") if colorName == 'Dred': colorStr = '#800000' if colorName == 'red': colorStr = '#FF0000' if colorName == 'Lred': colorStr = '#FF8181' if colorName == 'Dblue': colorStr = '#000080' if colorName == 'blue': colorStr = '#0000FF' if colorName == 'Lblue': colorStr = '#8181FF' if colorName == 'Dgreen': colorStr = '#008000' if colorName == 'green': colorStr = '#00FF00' if colorName == 'Lgreen': colorStr = '#81FF81' if colorName == 'black': colorStr = '#000000' if colorName == 'white': colorStr = '#FFFFFF' if colorName == 'Dyellow': colorStr = '#808000' if colorName == 'yellow': colorStr = '#FFFF00' if colorName == 'Lyellow': colorStr = '#FFFF81' if colorName == 'Dmagen': colorStr = '#800080' if colorName == 'magen': colorStr = '#FF00FF' if colorName == 'Lmagen': colorStr = '#FF81FF' colorStr += color.val2hex(alpha*255) return colorStr @staticmethod def RGB(RGBlist,alpha=255): """ return a string representing a color specified by RGB level in the range 0-255 :param RGBlist: list containing RGB levels in the range 0-225 each :type RGBlist: list of ints :param alpha: alpha channel. Values in the range 0-225. Default: 255 :type alpha: int :returns: string representing the color in inkscape's expected format ``#RRGGBBAA`` :rtype: string **Example** >>> colorString = inkDraw.color.RGB([120,80,0],255) # returns a string representing the color R=120, G=80, B=0, A=255 """ hexVal='#' for c in RGBlist: hexVal += color.val2hex(c) hexVal += color.val2hex(alpha) return hexVal @staticmethod def rgb(RGBlist,alpha=1.0): """ Return a string representing a color specified by RGB level in the range 0.0-1.0 :param RGBlist: list containing RGB levels in the range 0.0-1.0 each :type RGBlist: list of floats :param alpha: alpha channel. Values in the range 0.0-1.0. Default: 1.0 :type alpha: int :returns: string representing the color in inkscape's expected format ``#RRGGBBAA`` :rtype: string **Example** >>> colorString = color.rgb([0.5,0.8,0.0],255) # returns a string representing the color R=127, G=204, B=0, A=255 """ hexVal='#' for c in RGBlist: hexVal += color.val2hex(c*255) hexVal += color.val2hex(alpha*255) return hexVal # --------------------------------------------- @staticmethod def gray(percentage,alpha=1.0): """ Return a string representing a gray color based on white percentage between 0.0 (black) and 1.0 (white) - if percentage is higher than 1.0, percentage is truncated to 1.0 (white) - if percentage is lower than 0.0, percentage is truncated to 0.0 (black) :param percentage: value between 0.0 (black) and 1.0 (white) :type percentage: float :param alpha: alpha channel. Values in the range 0.0-1.0. Default: 1.0 :type alpha: int :returns: string representing the color in inkscape's expected format ``#RRGGBBAA`` :rtype: string **Example** >>> colorString = color.gray(0.6,1.0) # returns a string representing the gray level with 60% of white, alpha=100% """ return color.rgb([percentage] * 3,alpha) @staticmethod def val2hex(value): """ return a string representing a color specified by level in the range 0-255 If value is not in the range 0-255, the result will be truncated in this range :param value: color value in the range 0-255 :type value: int :returns: string representing the color in hexadecimal :rtype: string **Example** >>> colorString = color.val2hex(255) # returns FF >>> colorString = color.val2hex(127) # returns 7F >>> colorString = color.val2hex(-1) # returns 00 >>> colorString = color.val2hex(300) # returns FF """ if value > 255: value = 255 if value < 0.0: value = 0 if value < 16: hexVal = '0' + hex(int(value))[2:].upper() else: hexVal = hex(int(value))[2:].upper() return hexVal @staticmethod def splitColorAlpha(colorString): """ Split color and alpha channel from colorSting in #RRGGBBAA format :param: string representing the color in hexadecimal in #RRGGBBAA format :type: string :returns: a list of strings: [color,alpha] - color: string in ``#RRGGBB`` format - alpha: string in ``AA`` format :rtype: list **Example** >>> colorComponents = color.splitColorAlpha('#FFF700FF') # returns ['#FFF700', 'FF'] """ return [colorString[0:7],colorString[7:]] # --------------------------------------------- @staticmethod def colorPickerToRGBalpha(colorPickerString): """ Function that converts the string returned by the widget 'color' in the .inx file into 2 strings, one representing the color in format ``#RRGGBB`` and the other representing the alpha channel ``AA`` .. hint:: you probably don't need to use this function. Consider using the method :meth:`color.parseColorPicker` :param colorPickerString: string returned by 'color' widget :type colorPickerString: string :returns: a list of strings: [color,alpha] - color: string in ``#RRGGBB`` format - alpha: string in ``AA`` format :rtype: list .. note:: For more information on this widget, see `this `_. **Usage** 1- You must have one parameter of the type 'color' in your inx file:: 2- Parse it as a string in your .py file:: self.OptionParser.add_argument("--myColorPicker", type=str, dest="myColorPickerVar", default='#00000000') 3- call this function to convert self.options.myColorPickerVar into two strings - #RRGGBB with RGB values in hex - AA with alpha value in hex **Example** Let your .inx file contain a widget of type 'color' with the name myColorPicker:: Then in the .py file >>> import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw >>> import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> inkBase.inkscapeMadeEasy.__init__(self) >>> self.OptionParser.add_argument("--myColorPicker", type=str, dest="myColorPickerVar", default='#000000FF') # parses the input parameter >>> >>> def effect(self): >>> color,alpha = inkDraw.color.colorPickerToRGBalpha(self.options.myColorPickerVar) # returns the string representing the selected color and alpha channel """ # [2:] removes the 0x , zfill adds the leading zeros, upper: uppercase colorHex = hex(int(colorPickerString) & 0xffffffff)[2:].zfill(8).upper() RGB = '#' + colorHex[0:6] alpha = colorHex[6:] return [RGB, alpha] # --------------------------------------------- @staticmethod def parseColorPicker(stringColorOption, stringColorPicker): """ Function that converts the string returned by the widgets 'color' and 'optiongroup' in the .inx file into a string, in format ``#RRGGBBAA`` You must have in your .inx both 'optiongroup' and 'color' widgets as defined below. You don't have to have all the color options presented in the example. The example presents the most complete list with all the default colors in color.defined method. :param stringColorOption: string returned by 'optiongroup' widget :type stringColorOption: string :param stringColorPicker: string returned by 'color' widget :type stringColorPicker: string :returns: color string in ``#RRGGBBAA`` format :rtype: string .. note:: For more information on this widget, see `this link `_ **Example** It works in the following manner: The user select in the optiongroup list the desired color. All pre defined colors in inkscapeMadeEasy are listed there. There is also a 'my default color' option where you can set your preferred default color and an 'use color picker' option to activate the color picker widget. Keep in mind that the selected color in the color picker widget will be considered **ONLY** if 'use color picker' option is selected. a) Example with full form of ``color`` widget. In this example a ``use color picker`` is selected from the optiongroup widget. Therefore the color picker widget will have effect .. image:: ../imagesDocs/colorPicker02.png :width: 400px b) Example with compact form of ``color`` widget. In this example a color is selected from the optiongroup widget. Therefore the color picker widget will have no effect .. image:: ../imagesDocs/colorPicker01.png :width: 400px Bellow is the template 'color' widget with name 'myColorPicker' and an 'optiongroup' with the name 'myColorOption' for the .inx file:: <_option value="#FF0022FF">my default color <--you can set your pre define color in the form #RRGGBBAA <_option value="none">none <-- no color <_option value="black">black <_option value="red">red <_option value="blue">blue <_option value="yellow">yellow <_option value="green">green <-- these are all standardized colors in inkscapeMadeEasy_Draw.color class! <_option value="magen">magenta <_option value="white">white <_option value="Lred">Lred <_option value="Lblue">Lblue <_option value="Lyellow">Lyellow <_option value="Lgreen">Lgreen <_option value="Lmagen">Lmagenta <_option value="Dred">Dred <_option value="Dblue">Dblue <_option value="Dyellow">Dyellow <_option value="Dgreen">Dgreen <_option value="Dmagen">Dmagenta <_option value="picker">use color picker <-- indicate that the color must be taken from the colorPicker attribute Then in the .py file >>> import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase >>> import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw >>> import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot >>> >>> class myExtension(inkBase.inkscapeMadeEasy): >>> def __init__(self): >>> inkBase.inkscapeMadeEasy.__init__(self) >>> self.OptionParser.add_argument("--myColorPicker", type=str, dest="myColorPickerVar", default='0') # parses the input parameters >>> self.OptionParser.add_argument("--myColorOption", type=str, dest="myColorOptionVar", default='black') # parses the input parameter >>> >>> def effect(self): >>> so = self.options >>> colorString = inkDraw.color.parseColorPicker(so.myColorOptionVar,so.myColorPickerVar) """ if stringColorOption.startswith("#"): return stringColorOption else: if stringColorOption == 'none': colorString = 'none' else: if stringColorOption == 'picker': [colorString, alphaString] = color.colorPickerToRGBalpha(stringColorPicker) colorString += alphaString else: colorString = color.defined(stringColorOption) return colorString class marker(): """ Class to manipulate markers. This class is used to create new custom markers. Markers can be used with the :meth:`inkscapeMadeEasy_Draw.lineStyle` class to define line types that include start, mid and end markers The base method of this class is :meth:`marker.createMarker` that can create custom markers. There are also other methods that simplify the creation of commonly used markers. The implemented predefined markers are presented in the figure below. .. image:: ../imagesDocs/marker_predefined.png :width: 400px .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ # --------------------------------------------- @staticmethod def createMarker(ExtensionBaseObj, nameID, markerPath, RenameMode=0, strokeColor=color.defined('black'), fillColor=color.defined('black'), lineWidth=1.0, markerTransform=None): """Create a custom line marker :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param nameID: nameID of the marker :param markerPath: Path definition. Must follow 'd' attribute format. See `this link `_ for further information :param RenameMode: Renaming behavior mode - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified. - 1: overwrite marker definition if nameID is already taken .. Warning:: when a marker is created using RenameMode=1, any marker with the same name will disapear from inkscape's canvas. This is an inkscape issue. Save the document and reload it, everything should be fine. - 2: Create a new unique nameID, adding a suffix number (Please refer to :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy.uniqueIdNumber()` :param strokeColor: Stroke color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :param fillColor: Filling color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :param lineWidth: Line width of the marker. Default: 1.0 :param markerTransform: custom transform applied to marker's path. Default: ``None`` .. note:: The transform must follow 'transform' attribute format. See `this link `_ for further information :type ExtensionBaseObj: inkscapeMadeEasy object :type nameID: string :type markerPath: string :type RenameMode: int :type strokeColor: string :type fillColor: string :type lineWidth: float :type markerTransform: string :returns: NameID of the new marker :rtype: string **System of coordinates** The system of coordinates of the marker depends on the point under consideration. The following figure presents the coordinate system for all cases .. image:: ../imagesDocs/marker_Orientation.png :width: 600px **Example** >>> nameID='myMarker' >>> markerPath='M 3,0 L 0,1 L 0,-1 z' # defines a path forming an triangle with vertices (3,0) (0,1) (0,-1) >>> strokeColor=inkDraw.color.defined('red') >>> fillColor=None >>> RenameMode=1 >>> width=1 >>> markerTransform=None >>> markerID=inkDraw.marker.createMarker(self,nameID,markerPath,RenameMode,strokeColor,fillColor,width,markerTransform) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerEnd=markerID,lineColor=inkDraw.color.defined('black')) # see lineStyle class for further information on this function >>> >>> #tries to make another marker with the same nameID, changing RenameMode >>> strokeColor=inkDraw.color.defined('blue') >>> RenameMode=0 >>> markerID=inkDraw.marker.createMarker(self,nameID,RenameMode,scale,strokeColor,fillColor) # this will not modify the marker >>> RenameMode=1 >>> markerID=inkDraw.marker.createMarker(self,nameID,RenameMode,scale,strokeColor,fillColor) # modifies the marker 'myMarker' >>> RenameMode=2 >>> markerID=inkDraw.marker.createMarker(self,nameID,RenameMode,scale,strokeColor,fillColor) # creates a new marker with nameID='myMarker-0001' .. note:: In the future, path definition and transformation will be modified to make it easier to design custom markers. =) """ if RenameMode == 0 and ExtensionBaseObj.findMarker(nameID): return nameID if RenameMode == 2: numberID = 1 new_id = nameID + '_n%05d' % numberID while new_id in ExtensionBaseObj.svg.get_ids(): numberID += 1 new_id = nameID + '_n%05d' % numberID #ExtensionBaseObj.svg.get_ids().add(nameID) nameID = new_id if RenameMode == 1 and ExtensionBaseObj.findMarker(nameID): defs = ExtensionBaseObj.getDefinitions() for obj in defs.iter(): if obj.get('id') == nameID: defs.remove(obj) # creates a new marker marker_attribs = {inkex.addNS('stockid', 'inkscape'): nameID, 'orient': 'auto', 'refY': '0.0', 'refX': '0.0', 'id': nameID, 'style': 'overflow:visible'} newMarker = etree.SubElement(ExtensionBaseObj.getDefinitions(), 'marker', marker_attribs) if fillColor is None: fillColor = 'none' opacityFill = '1.0' if strokeColor is None: strokeColor = 'none' opacityStroke = '1.0' # set color and opacity if fillColor.startswith('#'): [fillColor,alphaFill] = color.splitColorAlpha(fillColor) opacityFill = str(int(alphaFill, 16)/255.0) if strokeColor.startswith('#'): [strokeColor, alphaStroke] = color.splitColorAlpha(strokeColor) opacityStroke = str(int(alphaStroke, 16)/255.0) marker_style = {'fill-rule': 'evenodd', 'fill': fillColor, 'stroke': strokeColor, 'stroke-width': str(lineWidth),'fill-opacity':opacityFill,'stroke-opacity':opacityStroke} marker_lineline_attribs = {'d': markerPath, 'style': str(inkex.Style(marker_style))} if markerTransform: marker_lineline_attribs['transform'] = markerTransform etree.SubElement(newMarker, 'path', marker_lineline_attribs) #ExtensionBaseObj.svg.ids.add(nameID) ExtensionBaseObj.svg.ids[nameID] = newMarker return nameID # --------------------------------------------- @staticmethod def createDotMarker(ExtensionBaseObj, nameID, RenameMode=0, scale=0.4, strokeColor=color.defined('black'), fillColor=color.defined('black')): """Create circular dot markers, exactly like inkscape default dotS dotM or dotL markers. .. image:: ../imagesDocs/marker_predefined_dot.png :width: 300px :align: center :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param nameID: nameID of the marker :param RenameMode: Renaming behavior mode - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified. - 1: overwrite marker definition if nameID is already taken .. Warning:: when a marker is created using RenameMode=1, any marker with the same name will disapear from inkscape's canvas. This is an inkscape issue. Save the document and reload it, everything should be fine. - 2: Create a new unique nameID, adding a suffix number (Please refer to :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy.uniqueIdNumber()` :param scale: Scale factor of the marker. To exactly copy inkscape sizes dotS/M/L, use 0.2, 0.4 and 0.8 respectively. Default: 0.4 :param strokeColor: Stroke color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :param fillColor: Filling color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :type ExtensionBaseObj: inkscapeMadeEasy object :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :type fillColor: string :returns: NameID of the new marker :rtype: string **Example** >>> myMarker=inkDraw.marker.createDotMarker(self,nameID='myDotMarkerA',RenameMode=1,scale=0.5,strokeColor=inkDraw.color.defined('red'),fillColor=None) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerEnd=myMarker,lineColor=inkDraw.color.defined('black')) # see lineStyle class for further information on this function """ markerPath = 'M -2.5,-1.0 C -2.5,1.7600000 -4.7400000,4.0 -7.5,4.0 C -10.260000,4.0 -12.5,1.7600000 -12.5,-1.0 C -12.5,-3.7600000 -10.260000,-6.0 -7.5,-6.0 C -4.7400000,-6.0 -2.5,-3.7600000 -2.5,-1.0 z ' width = 1.0 markerTransform = 'scale(' + str(scale) + ') translate(7.4, 1)' return marker.createMarker(ExtensionBaseObj, nameID, markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) # --------------------------------------------- @staticmethod def createCrossMarker(ExtensionBaseObj, nameID, RenameMode=0, scale=0.4, strokeColor=color.defined('black')): """Create a cross marker .. image:: ../imagesDocs/marker_predefined_cross.png :width: 300px :align: center :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param nameID: nameID of the marker :param RenameMode: Renaming behavior mode - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified. - 1: overwrite marker definition if nameID is already taken .. Warning:: when a marker is created using RenameMode=1, any marker with the same name will disapear from inkscape's canvas. This is an inkscape issue. Save the document and reload it, everything should be fine. - 2: Create a new unique nameID, adding a suffix number (Please refer to :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy.uniqueIdNumber()` :param scale: Scale of the marker. Default: 0.4 :param strokeColor: Stroke color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :type ExtensionBaseObj: inkscapeMadeEasy object :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :returns: NameID of the new marker :rtype: string **Example** >>> myMarker=inkDraw.marker.createCrossMarker(self,nameID='myDotMarkerA',RenameMode=1,scale=0.5,strokeColor=inkDraw.color.defined('red')) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerEnd=myMarker,lineColor=inkDraw.color.defined('black')) # see lineStyle class for further information on this function """ markerPath = 'M -4,4 L 4,-4 M 4,4 L -4,-4' markerTransform = 'scale(' + str(scale) + ')' width = 1.0 return marker.createMarker(ExtensionBaseObj, nameID, markerPath, RenameMode, strokeColor, None, width, markerTransform) # --------------------------------------------- @staticmethod def createArrow1Marker(ExtensionBaseObj, nameID, RenameMode=0, scale=0.4, strokeColor=color.defined('black'), fillColor=color.defined('black')): """Create arrow markers (both start and end markers) .. image:: ../imagesDocs/marker_predefined_arrow.png :width: 300px :align: center :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param nameID: nameID of the marker. .. note:: Start and End markers will have 'Start' and 'End' suffixes respectively :param RenameMode: Renaming behavior mode - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified. - 1: overwrite marker definition if nameID is already taken .. Warning:: when a marker is created using RenameMode=1, any marker with the same name will disapear from inkscape's canvas. This is an inkscape issue. Save the document and reload it, everything should be fine. - 2: Create a new unique nameID, adding a suffix number (Please refer to :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy.uniqueIdNumber()` :param scale: scale of the marker. Default: 0.4 :param strokeColor: Stroke color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :param fillColor: Filling color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :type ExtensionBaseObj: inkscapeMadeEasy object :type nameID: string :type RenameMode: int :type scale: float :type strokeColor: string :type fillColor: string :returns: a list of strings: [startArrowMarker,endArrowMarker] - startArrowMarker: nameID of start marker - endArrowMarker: nameID of end marker :rtype: list **Example** >>> StartArrowMarker,EndArrowMarker=inkDraw.marker.createArrow1Marker(self,nameID='myArrow',RenameMode=1,scale=0.5,strokeColor=inkDraw.color.defined('red'),fillColor=None) >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerStart=StartArrowMarker,markerEnd=EndArrowMarker,lineColor='#000000') # see lineStyle class for further information on this function """ # transform="scale(0.8) rotate(180) translate(12.5,0)" /> # transform="scale(0.4) rotate(180) translate(10,0)" /> # transform="scale(0.2) rotate(180) translate(6,0)" /> # translation=12.5-17.5/(scale*10) # linear regression from data of small medium and large translation = 10.17 * scale + 4.75 width = 1.0 markerPath = 'M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z ' markerTransform = 'scale(' + str(scale) + ') rotate(0) translate(' + str(translation) + ',0)' nameStart = marker.createMarker(ExtensionBaseObj, nameID + 'Start', markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) markerTransform = 'scale(' + str(scale) + ') rotate(180) translate(' + str(translation) + ',0)' nameEnd = marker.createMarker(ExtensionBaseObj, nameID + 'End', markerPath, RenameMode, strokeColor, fillColor, width, markerTransform) return [nameStart, nameEnd] # --------------------------------------------- @staticmethod def createElipsisMarker(ExtensionBaseObj, nameID, RenameMode=0, scale=1.0, fillColor=color.defined('black')): """Create ellipsis markers, both start and end markers. .. image:: ../imagesDocs/marker_predefined_elipsis.png :width: 300px :align: center .. note:: These markers differ from inkscape's default ellipsis since these markers are made such that the diameter of the dots are equal to the line width. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param nameID: nameID of the marker. Start and End markers will have 'Start' and 'End' suffix respectively :param RenameMode: Renaming behavior mode - 0: (default) do not rename the marker. If nameID is already taken, the marker will not be modified. - 1: overwrite marker definition if nameID is already taken .. Warning:: when a marker is created using RenameMode=1, any marker with the same name will disapear from inkscape's canvas. This is an inkscape issue. Save the document and reload it, everything should be fine. - 2: Create a new unique nameID, adding a suffix number (Please refer to :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy.uniqueIdNumber()` :param scale: Scale of the marker. Default 1.0 :param fillColor: Filling color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black'). See :meth:`inkscapeMadeEasy_Draw.color` for functions to create color strings :type ExtensionBaseObj: inkscapeMadeEasy object :type nameID: string :type RenameMode: int :type scale: float :type fillColor: string :returns: a list of strings: [startInfMarker,endInfMarker] - startInfMarker: nameID of start marker - endInfMarker: nameID of end marker :rtype: list **Example** >>> startInfMarker,endInfMarker=inkDraw.marker.createElipsisMarker(self,nameID='myInfMarker',RenameMode=1,scale=1.0,fillColor='#00FF00FF') >>> myLineStyle = inkDraw.lineStyle.set(1.0, markerStart=startInfMarker,markerEnd=endInfMarker,lineColor='#000000FF') # see lineStyle class for further information on this function """ # build path for 3 circles markerPath = '' radius = scale / 2.0 for i in range(3): prefix = 'M %f %f ' % (i * 2 + radius, 0) arcStringA = 'a %f %f 0 1 1 %f %f ' % (radius, radius, -2 * radius, 0) arcStringB = 'a %f %f 0 1 1 %f %f ' % (radius, radius, 2 * radius, 0) markerPath = markerPath + prefix + arcStringA + arcStringB + 'z ' if scale != 1.0: markerTransform = 'translate(' + str(-6.0 * scale) + ', 0) scale(' + str(scale) + ')' else: markerTransform = 'translate(' + str(-6.0 * scale) + ', 0)' width = 1.0 # add small line segment nameStart = marker.createMarker(ExtensionBaseObj, nameID + 'Start', markerPath, RenameMode, None, fillColor, width, markerTransform) if scale != 1.0: markerTransform = 'translate(' + str(2.0 * scale) + ', 0) scale(' + str(scale) + ')' else: markerTransform = 'translate(' + str(2.0 * scale) + ', 0)' nameEnd = marker.createMarker(ExtensionBaseObj, nameID + 'End', markerPath, RenameMode, None, fillColor, width, markerTransform) return [nameStart, nameEnd] class lineStyle(): """ Class to manipulate line styles. This class is used to define line styles. It is capable of setting stroke and filling colors, line width, linejoin and linecap, markers (start, mid, and end) and stroke dash array The base method of this class is :meth:`lineStyle.set` that can create custom line types. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ # --------------------------------------------- @staticmethod def set(lineWidth=1.0, lineColor=color.defined('black'), fillColor=None, lineJoin='round', lineCap='round', markerStart=None, markerMid=None, markerEnd=None, strokeDashArray=None): """ Create a new line style :param lineWidth: Line width. Default: 1.0 :param lineColor: Color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fillColor: Color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: ``None`` :param lineJoin: Shape of the lines at the joints. Valid values 'miter', 'round', 'bevel'. See image below. Default: round. :param lineCap: Shape of the lines at the ends. Valid values 'butt', 'square', 'round'. See image below. Default: round :param markerStart: Marker at the start node. Default: ``None`` :param markerMid: Marker at the mid nodes. Default: ``None`` :param markerEnd: Marker at the end node. Default: ``None`` :param strokeDashArray: Dashed line pattern definition. Default: ``None``. See `this link `_ for further information. Also, check method createDashedLinePattern :type lineWidth: float :type lineColor: string :type fillColor: string :type lineJoin: string :type lineCap: string :type markerStart: string :type markerMid: string :type markerEnd: string :type strokeDashArray: string :returns: line definition following the provided specifications :rtype: dict **Line node types** .. image:: ../imagesDocs/line_nodes.png :width: 600px **Example** >>> # creates a line style using a dot marker at its end node >>> myMarker=inkDraw.marker.createDotMarker(self,nameID='myMarker',RenameMode=1,scale=0.5,strokeColor=color.defined('red'),fillColor=None) # see marker class for further information on this function >>> myLineStyle = inkDraw.lineStyle.set(lineWidth=1.0, markerEnd=myMarker,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('red')) >>> >>> # creates a line style with dashed line (5 units dash , 10 units gap) >>> dashedPattern = inkDraw.lineStyle.createDashedLinePattern(5,10) >>> myDashedStyle = inkDraw.lineStyle.set(lineWidth=1.0,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black'),strokeDashArray=dashedPattern) >>> # creates a line style with a more complex pattern (5 units dash , 10 units space, 2 units dash, 3 units space >>> myDashedStyle = inkDraw.lineStyle.set(lineWidth=1.0,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black'),strokeDashArray='5,10,2,3') """ if fillColor is None: fillColor = 'none' opacityFill = '1.0' if lineColor is None: lineColor = 'none' opacityStroke = '1.0' # set color and opacity if fillColor.startswith('#'): [fillColor,alphaFill] = color.splitColorAlpha(fillColor) opacityFill = str(int(alphaFill, 16)/255.0) if lineColor.startswith('#'): [lineColor, alphaLine] = color.splitColorAlpha(lineColor) opacityStroke = str(int(alphaLine, 16)/255.0) if not strokeDashArray: strokeDashArray = 'none' # dictionary with the styles lineStyle = {'stroke': lineColor, 'stroke-width': str(lineWidth), 'stroke-dasharray': strokeDashArray, 'fill': fillColor,'fill-opacity':opacityFill,'stroke-opacity':opacityStroke} # Endpoint and junctions lineStyle['stroke-linecap'] = lineCap lineStyle['stroke-linejoin'] = lineJoin # add markers if needed if markerStart: lineStyle['marker-start'] = 'url(#' + markerStart + ')' if markerMid: lineStyle['marker-mid'] = 'url(#' + markerMid + ')' if markerEnd: lineStyle['marker-end'] = 'url(#' + markerEnd + ')' return lineStyle # --------------------------------------------- @staticmethod def createDashedLinePattern(dashLength=5.0,gapLength=10.0): """Creates strokeDashArray of a dashed line necessary for method set :param dashLength: dash length. Default: 5.0 :param dashLength: gap length. Default: 10.0 :type dashLength: float :type dashLength: float :returns: strokeDashArray :rtype: string **Example** >>> myPattern = inkDraw.lineStyle.createDashedLinePattern(5,12) >>> myDashedStyle = inkDraw.lineStyle.set(lineWidth=1.0,lineColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black'),strokeDashArray=myPattern) """ return '%f,%f' % (dashLength,gapLength) # --------------------------------------------- @staticmethod def setSimpleBlack(lineWidth=1.0): """Define a standard black line style. The only adjustable parameter is its width. The fixed parameters are: lineColor=black, fillColor=None, lineJoin='round', lineCap='round', no markers, no dash pattern :param lineWidth: line width. Default: 1.0 :type lineWidth: float :returns: line definition following the provided specifications :rtype: dict **Example** >>> mySimpleStyle = inkDraw.lineStyle.setSimpleBlack(lineWidth=2.0) """ return lineStyle.set(lineWidth) @staticmethod def getStyle(ExtensionBaseObj,element): """ This function retrieves the style of a given element. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param element: The element whose style is to be retrieved. :type ExtensionBaseObj: inkscapeMadeEasy object :type element: inkscape element object :returns: line definition following the provided specifications :rtype: dict The returned dictionary contains the following keys: - 'stroke': The color of the stroke. - 'stroke-width': The width of the stroke. - 'stroke-dasharray': The dash array of the stroke. - 'fill': The fill color of the element. - 'fill-opacity': The opacity of the fill color. - 'stroke-opacity': The opacity of the stroke color. - 'stroke-linecap': The linecap of the stroke. - 'stroke-linejoin': The linejoin of the stroke. - 'marker-start': The marker at the start of the stroke. - 'marker-mid': The marker at the mid of the stroke. - 'marker-end': The marker at the end of the stroke. **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.set(lineWidth=1.0, lineColor=inkDraw.color.defined('red')) >>> >>> # creates a polyline passing through points (0,0) (0,1) (1,1) (1,2) (2,2), and using absolute coordinates >>> coords=[ [0,0], [0,1], [1,1], [1,2], [2,2] ] >>> myLine = inkDraw.line.absCoords(root_layer, coordsList=coords, offset=[0, 0], label='fooBarLine', lineStyle=myLineStyle) >>> lineStyle = inkDraw.lineStyle.getStyle(self, myLine) """ elemStyle = ExtensionBaseObj.getElemAttrib(element,'style') styleElements = elemStyle.split(';') lineStyle = {} for a in styleElements: if a.split(':')[0] == 'stroke': lineStyle['stroke'] = a.split(':')[1] if a.split(':')[0] == 'stroke-width': lineStyle['stroke-width'] = a.split(':')[1] if a.split(':')[0] == 'stroke-dasharray': lineStyle['stroke-dasharray'] = a.split(':')[1] if a.split(':')[0] == 'fill': lineStyle['fill'] = a.split(':')[1] if a.split(':')[0] == 'fill-opacity': lineStyle['fill-opacity'] = a.split(':')[1] if a.split(':')[0] == 'stroke-opacity': lineStyle['stroke-opacity'] = a.split(':')[1] if a.split(':')[0] == 'stroke-linecap': lineStyle['stroke-linecap'] = a.split(':')[1] if a.split(':')[0] == 'stroke-linejoin': lineStyle['stroke-linejoin'] = a.split(':')[1] if a.split(':')[0] == 'marker-start': lineStyle['marker-start'] = a.split(':')[1] if a.split(':')[0] == 'marker-mid': lineStyle['marker-mid'] = a.split(':')[1] if a.split(':')[0] == 'marker-end': lineStyle['marker-end'] = a.split(':')[1] return lineStyle @staticmethod def setStyle(ExtensionBaseObj,element,lineStyle): """ This function sets the style of a given element. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param element: The element whose style is to be set. :type ExtensionBaseObj: inkscapeMadeEasy object :type element: inkscape element object :param lineStyle: The style to be applied to the element. :type lineStyle: line style object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> R1=inkDraw.rectangle.corners(root_layer, [110,10],[120,0], lineStyle=lineStyle.setSimpleBlack()) >>> # craete a new linestyle >>> newLineStyle = inkDraw.lineStyle.set(lineWidth=1.0, lineColor=inkDraw.color.defined('red')) >>> lineStyle = inkDraw.lineStyle.setStyle(self, R1, newLineStyle) """ lineStyleStr='' for key, value in lineStyle.items(): lineStyleStr += key + ':' + value + ';' ExtensionBaseObj.addAttribute(element, 'style', lineStyleStr, forceWrite=True) class textStyle(): """ Class to create text styles. This class is used to define text styles. It is capable of setting font size, justification, text color, font family, font style, font weight, line spacing, letter spacing and word spacing .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ # --------------------------------------------- @staticmethod def set(fontSize=10, justification='left', textColor=color.defined('black'), fontFamily='Sans', fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px'): """Define a new text style :param fontSize: Size of the font in px. Default: 10 :param justification: Text justification. ``left``, ``right``, ``center``. Default: ``left`` :param textColor: Color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param fontFamily: Font family name. Default ``Sans`` .. warning:: This method does NOT verify whether the font family is installed in your machine or not. :param fontStyle: ``normal`` or ``italic``. Default: ``normal`` :param fontWeight: ``normal`` or ``bold``. Default: ``normal`` :param lineSpacing: Spacing between lines in percentage. Default: ``100%`` :param letterSpacing: Extra space between letters. Format: ``_px``. Default: ``0px`` :param wordSpacing: Extra space between words. Format: ``_px``. Default: ``0px`` :type fontSize: float :type justification: string :type textColor: string :type fontFamily: string :type fontStyle: string :type fontWeight: string :type lineSpacing: string :type letterSpacing: string :type wordSpacing: string :returns: text style definition following the provided specifications :rtype: dict **Example** >>> myTextStyle=inkDraw.textStyle.set(fontSize=10, justification='left', textColor=color.defined('black',0.5), fontFamily='Sans', >>> fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px') """ if not textColor: textColor = 'none' opacityFill = '1.0' # set color and opacity if textColor.startswith('#'): [textColor, alphaFill] = color.splitColorAlpha(textColor) opacityFill = str(int(alphaFill, 16) / 255.0) if justification == 'left': justification = 'start' anchor = 'start' if justification == 'right': justification = 'end' anchor = 'end' if justification == 'center': anchor = 'middle' textStyle = {'font-size': str(fontSize) + 'px', 'font-style': fontStyle, 'font-weight': fontWeight, 'text-align': justification, # start, center, end 'line-height': lineSpacing, 'letter-spacing': letterSpacing, 'word-spacing': wordSpacing, 'text-anchor': anchor, # start, middle, end 'fill': textColor, 'fill-opacity': opacityFill, 'stroke': 'none', 'font-family': fontFamily} return textStyle # --------------------------------------------- @staticmethod def setSimpleBlack(fontSize=10, justification='left'): """Define a standard black text style The only adjustable parameter are font size and justification. The fixed parameters are: textColor=color.defined('black'), fontFamily='Sans', fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px. :param fontSize: Size of the font in px. Default: 10 :param justification: Text justification. ``left``, ``right``, ``center``. Default: ``left`` :type fontSize: float :type justification: string :returns: text style definition following the provided specifications :rtype: dict **Example** >>> mySimpleStyle = inkDraw.textStyle.setSimpleBlack(fontSize=20,justification='center') """ return textStyle.set(fontSize, justification) # --------------------------------------------- @staticmethod def setSimpleColor(fontSize=10, justification='left', textColor=color.defined('black')): """Define a standard colored text style The only adjustable parameter are font size, justification and textColor. The fixed parameters are: fontFamily='Sans', fontStyle='normal', fontWeight='normal', lineSpacing='100%', letterSpacing='0px', wordSpacing='0px. :param fontSize: Size of the font in px. Default: 10 :param justification: Text justification. ``left``, ``right``, ``center``. Default: ``left`` :param textColor: Color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :type fontSize: float :type justification: string :type textColor: string :returns: text style definition following the provided specifications :rtype: dict **Example** >>> mySimpleStyle = inkDraw.textStyle.setSimpleColor(fontSize=20,justification='center',textColor=inkDraw.color.gray(0.5)) """ return textStyle.set(fontSize, justification, textColor) class text(): """ Class for writing texts. This class allows the cration of regular inkscape's text elements or LaTeX text. For the later, TexText is incorporated here. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. .. warning:: LaTeX support is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. """ @staticmethod def write(ExtensionBaseObj, text, coords, parent, textStyle=textStyle.setSimpleBlack(fontSize=10, justification='left'), fontSize=None, justification=None, angleDeg=0.0): """Add a text line to the document :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param text: Text contents. Use \\\\n in the string to start a new line :param coords: Position [x,y] :param parent: Parent object :param textStyle: Text style to be used. See class ``textStyle``. Default: textStyle.setSimpleBlack(fontSize=10,justification='left') :param fontSize: Size of the font in px. - ``None``: Uses fontSize of textStyle argument (Default) - number: takes precedence over the size on textStyle :param justification: Text justification. ``left``, ``right``, ``center`` - ``None``: Uses justification of textStyle argument (Default) - ``left``, ``right``, ``center``: takes precedence over the justification set on textStyle :param angleDeg: Angle of the text, counterclockwise, in degrees. Default: 0 :type ExtensionBaseObj: inkscapeMadeEasy object :type text: string :type coords: list :type parent: inkscape element object :type textStyle: textStyle object :type fontSize: float :type justification: string :type angleDeg: float :returns: the new text object :rtype: text Object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> mySimpleStyle = inkDraw.textStyle.setSimpleBlack(fontSize=20,justification='center') # creates a simple text style. >>> >>> #adds a two-line text, at the point x=5.0,y=6.0 >>> # L1: 'foo bar who-hoo!' >>> # L2: 'second line!' >>> myText='foo bar who-hoo!\\nsecond line!' >>> inkDraw.text.write(self, text=myText, coords=[5.0,6.0], parent=root_layer, textStyle=mySimpleStyle, fontSize=None, justification=None, angleDeg=0.0) >>> >>> # creates a group in root-layer and add text to it >>> myGroup = self.createGroup(root_layer,'textGroup') >>> #adds a text 'foo bar', rotated 45 degrees, at the point x=0,y=0, overriding justification of mySimpleStyle >>> inkDraw.text.write(self, text='foo bar', coords=[0.0,0.0], parent=myGroup, textStyle=mySimpleStyle, fontSize=None, justification='left', angleDeg=45.0) """ if justification == 'left': textStyle['text-align'] = 'start' textStyle['text-anchor'] = 'start' if justification == 'right': textStyle['text-align'] = 'end' textStyle['text-anchor'] = 'end' if justification == 'center': textStyle['text-align'] = 'center' textStyle['text-anchor'] = 'middle' if fontSize: textStyle['font-size'] = str(fontSize) + 'px' AttribsText = {inkex.addNS('space', 'xml'): "preserve", 'style': str(inkex.Style(textStyle)), 'x': str(coords[0]), 'y': str(coords[1]), inkex.addNS('linespacing', 'sodipodi'): textStyle['line-height']} # textObj = etree.SubElement(parent, inkex.addNS('text','svg'), AttribsText ) textObj = etree.Element(inkex.addNS('text', 'svg'), AttribsText) parent.append(textObj) AttribsLineText = {inkex.addNS('role', 'sodipodi'): "line", 'x': str(coords[0]), 'y': str(coords[1])} textLines = text.split('\\n') for n in range(len(textLines)): myTspan = etree.SubElement(textObj, inkex.addNS('tspan', 'svg'), AttribsLineText) myTspan.text = textLines[n] if angleDeg != 0: ExtensionBaseObj.rotateElement(textObj, center=coords, angleDeg=angleDeg) # negative angle bc inkscape is upside down return textObj # --------------------------------------------- @staticmethod def latex(ExtensionBaseObj, parent, LaTeXtext, position, fontSize=10, refPoint='cc', textColor=color.defined('black'), LatexCommands=' ', angleDeg=0, preambleFile=None): """Creates text element using LaTeX. You can use any LaTeX contents here. .. note:: LaTeX support is an optional feature that requires a few extra packages to be installed outside inkscape. **It is enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. If disabled, this function will still work, internally calling the :meth:`text.write`. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param parent: parent object :param LaTeXtext: Contents of the text. Can contain any latex command :param position: Position of the reference point [x,y] :param fontSize: Size of the font. Assume any capitql letter of ``\\normalsize`` will have this size. Default: 10 :param refPoint: Text reference Point. See figure below for options. Default: ``cc`` :param textColor: Color in the format ``#RRGGBBAA`` (hexadecimal), or ``None`` for no color. Default: color.defined('black') :param LatexCommands: Commands to be included before LaTeXtext (default: ' '). If LaTeX support is disabled, this parameter has no effect. :param angleDeg: Angle of the text, counterclockwise, in degrees. Default: 0 :param preambleFile: Optional preamble file to be included. Default: None. If LaTeX support is disabled, this parameter has no effect. :type ExtensionBaseObj: inkscapeMadeEasy object :type parent: inkscape element object :type LaTeXtext: string :type position: list :type fontSize: float :type refPoint: string :type textColor: string :type LatexCommands: string :type angleDeg: float :type preambleFile: string :returns: the new text object :rtype: text Object .. note:: This function does not use ``textStyle`` class elements. **Reference point options** .. image:: ../imagesDocs/LaTeX_reference_Point.png :width: 300px **Standard Preamble file** When a preamble file is not provided, inkscapeMadeEasy assumes a standard preamble file located at ``./textextLib/basicLatexPackages.tex``. By default, its contents is:: \\usepackage{amsmath,amsthm,amsbsy,amsfonts,amssymb} \\usepackage[per=slash]{siunitx} \\usepackage{steinmetz} \\usepackage[utf8]{inputenc} You will need these packages installed. This file can be modified to include extra default packages and/or commands. **LaTeX .tex document structure** LaTeX .tex document have the following structure. Note that LatexCommands lies within document environment:: \\documentclass[landscape,a0]{article} [contents of Preamble file] \\pagestyle{empty} \\begin{document} \\noindent [contents of LatexCommands] [contens of LaTeXtext] \\end{document} **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> customCommand = r'\\newcommand{\\fooBar}{\\textbf{Foo Bar Function! WhooHoo!}}' # do not forget the r to avoid backslash escape. >>> inkDraw.text.latex(self, root_layer,r'This is one equation \\begin{align} x=y^2\\end{align} And this is my \\fooBar{}', >>> position=[0.0,0.0], fontSize=10, refPoint='cc', textColor=inkDraw.color.defined('black'), LatexCommands=customCommand, angleDeg=0, preambleFile=None) """ newTmp = True # write an empty svg file. if not LaTeXtext: # check whether text is empty return 0 if useLatex: # set useLatex=False to replace latex by an standard text (much faster for debugging =) ) if newTmp: tmpf = tempfile.NamedTemporaryFile(mode='w', prefix='temp_svg_inkscapeMadeEasy_Draw_', suffix='.svg', delete=False) tempFilePath = tmpf.name tmpf.write(ExtensionBaseObj.blankSVG) tmpf.close() else: tempDir = tempfile.gettempdir() tempFilePath = tempDir + '/temp_svg_inkscapeMadeEasy_Draw.txt' Dump(ExtensionBaseObj.blankSVG, tempFilePath, 'w') # return # temp instance for determining font height. Draws a F letter just to find the height of the font if False: # turning off this part of the code. texTemp = textext.TexText() # start textText (awesome extension! =] ) texTemp.run([r'--text=' + 'F', '--scale-factor=1', tempFilePath], output=os.devnull) for child in texTemp.document.getroot(): if child.typename == 'TexTextElement': groupLatex = child BboxMin, BboxMax = ExtensionBaseObj.getBoundingBox(groupLatex) Height0 = BboxMax[1] - BboxMin[1] scale = ExtensionBaseObj.getDocumentScaleFactor() ExtensionBaseObj.displayMsg('H0=%f\nscaleFactor=%f' % (Height0,scale )) else: # running the code above, we get a 'F' with height of 6.76, with scale 1.0 from textext. This will be used to scale the text accordingly to fit user specification 'fontSize' # Height0 = 6.76 Height0 = 9.041644 scale = fontSize / Height0 tex = textext.TexText() # start textText (awesome extension! =] ) if preambleFile: tex.run([r'--text=' + LatexCommands + LaTeXtext, '--scale-factor=1', '--preamble-file=' + preambleFile, tempFilePath], output=os.devnull) # in case we want to save the svg file with the text element, uncomment the fllowing line. # tex.document.write(tempFilePath) else: tex.run( [r'--text=' + LatexCommands + LaTeXtext, '--scale-factor=1', '--preamble-file=' + ExtensionBaseObj.getBasicLatexPackagesFile(), tempFilePath], output=os.devnull) if newTmp: os.unlink(tmpf.name) for child in tex.document.getroot(): if child.typename == 'TexTextElement': groupLatex = child if textColor is None: textColor = 'none' opacityFill = '1.0' # set color and opacity if textColor.startswith('#'): [textColor, alphaFill] = color.splitColorAlpha(textColor) opacityFill = str(int(alphaFill, 16) / 255.0) # change color for obj in groupLatex.iter(): oldStyle = obj.get('style') if oldStyle is not None: newStyle = re.sub('fill:#[0-9a-fA-F]+', 'fill:' + textColor, oldStyle) newStyle = re.sub('fill-opacity:[0-9]+', 'fill-opacity:' + opacityFill, newStyle) newStyle = re.sub('stroke:#[0-9a-fA-F]+', 'stroke:' + textColor, newStyle) newStyle = re.sub('stroke-opacity:[0-9]+', 'stroke-opacity:' + opacityFill, newStyle) obj.set('style', newStyle) ExtensionBaseObj.scaleElement(groupLatex, scaleX=scale, scaleY=scale) # scale to fit font size else: if refPoint[1] == 'l': justification = 'left' if refPoint[1] == 'c': justification = 'center' if refPoint[1] == 'r': justification = 'right' mytextStyle = textStyle.setSimpleColor(fontSize=fontSize / 0.76, justification='left', textColor=textColor) groupLatex = text.write(ExtensionBaseObj, LaTeXtext, [0, 0], parent, textStyle=mytextStyle, fontSize=fontSize / 0.76, justification=justification, angleDeg=0.0) # attention! keep angleDeg=0.0 here bc it will be rotated below parent.append(groupLatex) BboxMin, BboxMax = ExtensionBaseObj.getBoundingBox(groupLatex) if useLatex: # set useLatex=False to replace latex by an standard text (much faster for debugging =) ) if refPoint[0] == 't': refPointY = BboxMin[1] # BboxMin bc inkscape is upside down if refPoint[0] == 'c': refPointY = (BboxMax[1] + BboxMin[1]) / 2.0 if refPoint[0] == 'b': refPointY = BboxMax[1] # BboxMax bc inkscape is upside down if refPoint[1] == 'l': refPointX = BboxMin[0] if refPoint[1] == 'c': refPointX = (BboxMax[0] + BboxMin[0]) / 2.0 if refPoint[1] == 'r': refPointX = BboxMax[0] else: refPointX = BboxMin[0] if refPoint[0] == 't': refPointY = BboxMin[1] - fontSize # BboxMin bc inkscape is upside down if refPoint[0] == 'c': refPointY = BboxMin[1] - fontSize / 2.0 # BboxMin bc inkscape is upside down if refPoint[0] == 'b': refPointY = BboxMax[1] # BboxMax bc inkscape is upside down ExtensionBaseObj.moveElement(groupLatex, [-refPointX, -refPointY]) # move to origin ExtensionBaseObj.moveElement(groupLatex, [position[0], position[1]]) if angleDeg != 0: ExtensionBaseObj.rotateElement(groupLatex, center=[position[0], position[1]], angleDeg=angleDeg) return groupLatex class cubicBezier(): """ This is a class with different methods for drawing cubic bezier lines. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def addNode(NodeList, coord=[0, 0], cPbefore=[-1, 0], cPafter=[1, 0], typeNode='corner', flagAbsCoords=True): """Add a new node to the list of nodes of the cubic bezier line. .. important:: This function does not draw the curve. To draw the curve see :meth:`cubicBezier.draw` method. :param NodeList: Lst of nodes that will receive (append) the new node. :param coord: List with the coordinates of the node :param cPbefore: List with the coordinates of the control point before the node. :param cPafter: List with the coordinates of the control point after the node. Used only if 'typeNode' is 'smooth' or 'corner' :param typeNode: type of node to be added. See image below - ``corner``: Node without smoothness constraint. The bezier curve can have a sharp edge at this node - ``smooth``: Node with smoothness constraint. The bezier curve will be smooth at this node. If the control points do not form a straight line, then they are modified to form a straight line. See image below - ``symmetric``: same as ``smooth``, but the control points are forced to be symmetric with respect to the node. :param flagAbsCoords: Indicate absolute or relative coordinates. See section below on how reference system works. .. warning:: All nodes in a given list must be defined in the same reference system (absolute or relative). :type NodeList: list :type coord: list [x,y] :type cPbefore: list [x,y] :type cPafter: list [x,y] :type typeNode: string :type flagAbsCoords: bool :returns: None :rtype: - **Node Types** The image below presents the types of nodes .. image:: ../imagesDocs/bezier_nodeTypes.png :width: 500px **Smoothing control nodes** Image below present the process of smoothing control nodes not completely aligned when ``smooth`` is selected. .. image:: ../imagesDocs/bezier_smoothProcess.png :width: 500px **Absolute and relative coordinate systems** Cubic bezier curves are composed by segments which are defined by 4 coordinates, two node coordinates and two control points. .. image:: ../imagesDocs/bezier_definitions.png :width: 500px In absolute coordinate system, all node and control point locations are specified using the origin as reference. In relative coordinate system, control point localizations are specified using its node as reference, and each node use the previous node as reference (the first node use the origin as reference). See image below. .. warning:: Keep in mind that Inkscape's y axis is upside down! .. image:: ../imagesDocs/bezier_references.png :width: 700px **Example** .. note:: In the following example, the control point before the first node and after the last node are important when the bezier curve must be closed. See method ``draw`` .. image:: ../imagesDocs/bezier_example.png :width: 400px >>> # create a list of nodes using absolute coordinate system >>> nodeListABS=[] >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[4,4], cPbefore=[6,6], cPafter=[2,6], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[8,12], cPbefore=[4,12], cPafter=[10,12], typeNode='smooth', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,8], cPbefore=[8,8], cPafter=[12,10], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[16,8], cPbefore=[14,10], cPafter=None, typeNode='symmetric', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,4], cPbefore=[16,4], cPafter=[10,6], typeNode='corner', flagAbsCoords=True) >>> # create a list of nodes using relative coordinate system >>> nodeListREL=[] >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 4], cPbefore=[2,2], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 8], cPbefore=[-4,0], cPafter=[2,0], typeNode='smooth', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, -4], cPbefore=[-4,0], cPafter=[0,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 0], cPbefore=[-2,2], cPafter=None, typeNode='symmetric', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[-4,-4], cPbefore=[4,0], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) """ if typeNode.lower() == 'symmetric': typeNodeSodipodi = 'z' if typeNode.lower() == 'smooth': typeNodeSodipodi = 's' if typeNode.lower() == 'corner': typeNodeSodipodi = 'c' if typeNodeSodipodi.lower() == 'c': # corner NodeList.append({'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': cPafter, 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) if typeNodeSodipodi.lower() == 'z': # symmetric if flagAbsCoords: deltaX = coord[0] - cPbefore[0] deltaY = coord[1] - cPbefore[1] NodeList.append( {'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': [coord[0] + deltaX, coord[1] + deltaY], 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) else: NodeList.append({'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': [-cPbefore[0], -cPbefore[1]], 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) if typeNodeSodipodi.lower() == 's': # smooth # projects the directions of the control points to a commom direction, perpendicular to both delta1 = np.array(cPbefore) delta2 = np.array(cPafter) if abs(delta1.dot(delta2)) < 1.0: if flagAbsCoords: delta1 -= np.array(coord) delta2 -= np.array(coord) # https://math.stackexchange.com/questions/2285965/how-to-find-the-vector-formula-for-the-bisector-of-given-two-vectors bisectorVector = np.linalg.norm(delta2) * delta1 + np.linalg.norm(delta1) * delta2 tangentVersor = np.array([-bisectorVector[1], bisectorVector[0]]) tangentVersor /= np.linalg.norm(tangentVersor) cPbeforeNew = np.linalg.norm(delta1) * tangentVersor cPafterNew = np.linalg.norm(delta2) * tangentVersor if flagAbsCoords: cPbeforeNew += np.array(coord) cPafterNew += np.array(coord) NodeList.append({'node': coord, 'cPoint_before': cPbeforeNew.tolist(), 'cPoint_after': cPafterNew.tolist(), 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) else: NodeList.append( {'node': coord, 'cPoint_before': cPbefore, 'cPoint_after': cPafter, 'type': typeNodeSodipodi, 'absCoords': flagAbsCoords}) @staticmethod def draw(parent, NodeList, offset=np.array([0, 0]), label='none', lineStyle=lineStyle.setSimpleBlack(), closePath=False): """draws the bezier line, given a list of nodes, built using :meth:`cubicBezier.addNode` method :param parent: parent object :param NodeList: list of nodes. See :`cubicBezier.addNode` method :param offset: offset coords. Default [0,0] :param label: label of the line. Default 'none' :param lineStyle: line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param closePath: Connects the first point to the last. Default: False :type parent: inkscape element object :type NodeList: list of nodes :type offset: list :type label: string :type lineStyle: lineStyle object :type closePath: bool :returns: the new line object :rtype: line Object **Example** .. note:: In the following example, the control point before the first node and after the last node are important when the bezier curve must be closed. .. image:: ../imagesDocs/bezier_example.png :width: 400px >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = set(lineWidth=1.0, lineColor=color.defined('red')) >>> # create a list of nodes using absolute coordinate system >>> nodeListABS=[] >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[4,4], cPbefore=[6,6], cPafter=[2,6], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[8,12], cPbefore=[4,12], cPafter=[10,12], typeNode='smooth', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,8], cPbefore=[8,8], cPafter=[12,10], typeNode='corner', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[16,8], cPbefore=[14,10], cPafter=None, typeNode='symmetric', flagAbsCoords=True) >>> inkDraw.cubicBezier.addNode(nodeListABS, coord=[12,4], cPbefore=[16,4], cPafter=[10,6], typeNode='corner', flagAbsCoords=True) >>> # create a list of nodes using relative coordinate system >>> nodeListREL=[] >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 4], cPbefore=[2,2], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 8], cPbefore=[-4,0], cPafter=[2,0], typeNode='smooth', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, -4], cPbefore=[-4,0], cPafter=[0,2], typeNode='corner', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[4, 0], cPbefore=[-2,2], cPafter=None, typeNode='symmetric', flagAbsCoords=False) >>> inkDraw.cubicBezier.addNode(nodeListREL, coord=[-4,-4], cPbefore=[4,0], cPafter=[-2,2], typeNode='corner', flagAbsCoords=False) >>> C1 = inkDraw.cubicBezier.draw(root_layer,nodeListABS, offset=[0, 0],closePath=False) >>> C2 = inkDraw.cubicBezier.draw(root_layer,nodeListABS, offset=[0, 0],closePath=True) >>> C3 = inkDraw.cubicBezier.draw(root_layer,nodeListREL, offset=[0, 0],closePath=False) >>> C4 = inkDraw.cubicBezier.draw(root_layer,nodeListREL, offset=[0, 0],closePath=True) Result of the example .. image:: ../imagesDocs/bezier_example_draw.png :width: 800px """ # first node if NodeList[0]['absCoords']: string_coords = 'M %f,%f ' % (NodeList[0]['node'][0] + offset[0], NodeList[0]['node'][0] + offset[1]) else: string_coords = 'M %f,%f ' % (NodeList[0]['node'][0] + offset[0], NodeList[0]['node'][0] + offset[1]) string_nodeTypes = '' Ptotal = np.zeros(2) for i in range(len(NodeList) - 1): currNode = NodeList[i] nextNode = NodeList[i + 1] if currNode['absCoords']: bezier = 'C %f,%f ' % (currNode['cPoint_after'][0] + offset[0], currNode['cPoint_after'][1] + offset[1]) # first control point bezier += '%f,%f ' % (nextNode['cPoint_before'][0] + offset[0], nextNode['cPoint_before'][1] + offset[1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0] + offset[0], nextNode['node'][1] + offset[1]) # second node else: bezier = 'c %f,%f ' % (currNode['cPoint_after'][0], currNode['cPoint_after'][1]) # first control point bezier += '%f,%f ' % ( nextNode['cPoint_before'][0] + nextNode['node'][0], nextNode['cPoint_before'][1] + nextNode['node'][1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0], nextNode['node'][1]) # second node Ptotal += np.array(currNode['node']) string_nodeTypes += currNode['type'] string_coords = string_coords + bezier if closePath: currNode = NodeList[-1] nextNode = copy.deepcopy(NodeList[0]) if currNode['absCoords']: bezier = 'C %f,%f ' % (currNode['cPoint_after'][0] + offset[0], currNode['cPoint_after'][1] + offset[1]) # first control point bezier += '%f,%f ' % (nextNode['cPoint_before'][0] + offset[0], nextNode['cPoint_before'][1] + offset[1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0] + offset[0], nextNode['node'][1] + offset[1]) # second node else: # writes the coordinates of the first node, relative to the last node. Ptotal += np.array(currNode['node']) nextNode['node'][0] = NodeList[0]['node'][0] - Ptotal[0] nextNode['node'][1] = NodeList[0]['node'][1] - Ptotal[1] bezier = 'c %f,%f ' % (currNode['cPoint_after'][0], currNode['cPoint_after'][1]) # first control point bezier += '%f,%f ' % ( nextNode['cPoint_before'][0] + nextNode['node'][0], nextNode['cPoint_before'][1] + nextNode['node'][1]) # second control point bezier += '%f,%f ' % (nextNode['node'][0], nextNode['node'][1]) # second node string_nodeTypes += currNode['type'] + nextNode['type'] string_coords = string_coords + bezier + ' Z' else: string_nodeTypes += currNode['type'] # M = move, L = line, H = horizontal line, V = vertical line, C = curve, S = smooth curve, # Q = quadratic Bezier curve, T = smooth quadratic Bezier curve, A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), 'd': string_coords, inkex.addNS('nodetypes', 'sodipodi'): string_nodeTypes} return etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) class line(): """ class with methods for drawing lines (paths). .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def absCoords(parent, coordsList, offset=[0, 0], label='none', lineStyle=lineStyle.setSimpleBlack(), closePath=False): """Draw a (poly)line based on a list of absolute coordinates :param parent: Parent object :param coordsList: List with coords x and y. ex: [[x1,y1], ..., [xN,yN]] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param offset: Offset coords. Default [0,0] :param label: Label of the line. Default 'none' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param closePath: Connects the first point to the last. Default: False :type parent: inkscape element object :type coordsList: list of list :type offset: list :type label: string :type lineStyle: lineStyle object :type closePath: bool :returns: the new line object :rtype: line Object **Example** .. image:: ../imagesDocs/lineExample.png :width: 250px >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.set(lineWidth=1.0, lineColor=inkDraw.color.defined('red')) >>> >>> # creates a polyline passing through points (0,0) (0,1) (1,1) (1,2) (2,2), and using absolute coordinates >>> coords=[ [0,0], [0,1], [1,1], [1,2], [2,2] ] >>> inkDraw.line.absCoords(root_layer, coordsList=coords, offset=[0, 0], label='fooBarLine', lineStyle=myLineStyle) >>> >>> # creates the same polyline translated to point (5,6). Note we just have to change the offset >>> inkDraw.line.absCoords(root_layer, coordsList=coords, offset=[5, 6], label='fooBarLine', lineStyle=myLineStyle) """ # string with coordinates string_coords = '' for point in coordsList: string_coords = string_coords + ' ' + str(point[0] + offset[0]) + ',' + str(point[1] + offset[1]) if closePath: string_coords += ' Z' # M = move, L = line, H = horizontal line, V = vertical line, C = curve, S = smooth curve, # Q = quadratic Bezier curve, T = smooth quadratic Bezier curve, A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), 'd': 'M ' + string_coords} return etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) # --------------------------------------------- @staticmethod def relCoords(parent, coordsList, offset=[0, 0], label='none', lineStyle=lineStyle.setSimpleBlack(), closePath=False): """Draw a (poly)line based on a list of relative coordinates :param parent: Parent object :param coordsList: List with distances dx and dy for all points. ex [[dx1,dy1], ..., [dxN,dyN]] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param offset: Offset coords. Default [0,0] :param label: Label of the line. Default 'none' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param closePath: Connects the first point to the last. Default: False :type parent: inkscape element object :type coordsList: list of list :type offset: list :type label: string :type lineStyle: lineStyle object :type closePath: bool :returns: the new line object :rtype: line Object **Example** .. image:: ../imagesDocs/lineExample.png :width: 250px >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.setSimpleBlack(lineWidth=1.0) >>> >>> # creates a polyline passing through points (0,0) (0,1) (1,1) (1,2) (2,2) using relative coordinates >>> coords=[ [0,1], [1,0], [0,1], [1,0] ] >>> inkDraw.line.relCoords(root_layer, coordsList=coords, offset=[0, 0], label='fooBarLine', lineStyle=myLineStyle) >>> >>> # creates the same polyline translated to point (5,6) >>> inkDraw.line.relCoords(root_layer, coordsList=coords, offset=[5, 6], label='fooBarLine', lineStyle=myLineStyle) """ # string with coordinates string_coords = '' for dist in coordsList: string_coords = string_coords + ' ' + str(dist[0]) + ',' + str(dist[1]) if closePath: string_coords += ' Z' # M = move, L = line, H = horizontal line, V = vertical line, C = curve, S = smooth curve, # Q = quadratic Bezier curve, T = smooth quadratic Bezier curve, A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), 'd': 'm ' + str(offset[0]) + ' ' + str(offset[1]) + string_coords} return etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) class arc(): """ Class with methods for drawing arcs. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def startEndRadius(parent, Pstart, Pend, radius, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), flagRightOf=True, arcType='open', largeArc=False): """Draw a circle arc from ``Pstart`` to ``Pend`` with a given radius .. image:: ../imagesDocs/arc_startEndRadius.png :width: 80px :param parent: Parent object :param Pstart: Start coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param Pend: End coordinate [x,y] :param radius: Arc radius :param offset: Extra offset coords [x,y]. Default [0,0] :param label: Label of the line. Default 'arc' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param flagRightOf: Sets the side of the vector Pend-Pstart which the arc must be drawn. See image below - True: Draws the arc to the right (Default) - False: Draws the arc to the left :param arcType: type of arc. Valid values: 'open', 'slice', 'chord'. See image below. Default: 'open' :param largeArc: Sets the largest arc to be drawn. See image below - True: Draws the largest arc - False: Draws the smallest arc (Default) :type parent: inkscape element object :type Pstart: list :type Pend: list :type radius: float :type offset: list :type label: string :type lineStyle: lineStyle object :type flagRightOf: bool :type arcType: string :type largeArc: bool :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/arc_startEndRadius_flags.png :width: 800px **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> P1=[10.0,0.0] >>> P2=[20.0,10.0] >>> R=15.0 >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws an open arc >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[25,0], label='arc1', lineStyle=myLineStyle, arcType='open') >>> >>> #draws a closed (slice) arc >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[25,20], label='arc2', lineStyle=myLineStyle, arcType='slice') >>> >>> #draws an open arc to the right >>> inkDraw.arc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radius=R, offset=[0,0], label='arc', lineStyle=myLineStyle, flagRightOf=True, largeArc=True) """ return ellipseArc.startEndRadius(parent, Pstart, Pend, radius, radius, offset, label, lineStyle,flagRightOf, arcType, largeArc) # --------------------------------------------- @staticmethod def centerAngStartAngEnd(parent, centerPoint, radius, angStart, angEnd, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), arcType='open', largeArc=False): """Draw a circle arc given its center and start and end angles .. image:: ../imagesDocs/arc_centerAngStartAngEnd.png :width: 200px :param parent: parent object :param centerPoint: center coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param radius: Arc radius :param angStart: Start angle in degrees :param angEnd: End angle in degrees :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'arc' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param arcType: Type of arc. Valid values: 'open', 'slice', 'chord'. See image below. Default: 'open' :param largeArc: Sets the largest arc to be drawn. See image below - True: Draws the largest arc - False: Draws the smallest arc (Default) :type parent: inkscape element object :type centerPoint: list :type radius: float :type angStart: float :type angEnd: float :type offset: list :type label: string :type lineStyle: lineStyle object :type arcType: string :type largeArc: bool :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/arc_centerAngStartAngEnd_flags.png :width: 700px **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws the shortest arc >>> inkDraw.arc.centerAngStartAngEnd(parent=root_layer, centerPoint=[0,0], radius=15.0, angStart=-10, angEnd=90, >>> offset=[0,0], label='arc1', lineStyle=myLineStyle, arcType='open',largeArc=False) >>> #draws the longest arc >>> inkDraw.arc.centerAngStartAngEnd(parent=root_layer, centerPoint=[0,0], radius=15.0, angStart=-10, angEnd=90, >>> offset=[30,0], label='arc1', lineStyle=myLineStyle, arcType='open',largeArc=True) """ return ellipseArc.centerAngStartAngEnd(parent, centerPoint, radius, radius, angStart, angEnd, offset, label, lineStyle, arcType, largeArc) # --------------------------------------------- @staticmethod def threePoints(parent, Pstart, Pmid, Pend, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), arcType='open'): """Draw a circle arc given 3 points .. image:: ../imagesDocs/arc_3points.png :width: 120px :param parent: parent object :param Pstart: Start coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param Pmid: Mid coordinate [x,y] :param Pend: End coordinate [x,y] :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'arc' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param arcType: Type of arc. Valid values: 'open', 'slice', 'chord'. See image below. Default: 'open' :type parent: inkscape element object :type Pstart: list :type Pmid: list :type Pend: list :type offset: list :type label: string :type lineStyle: lineStyle object :type arcType: string :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/arc_type_flags.png :width: 400px **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.setSimpleBlack() >>> >>> P1=[10.0,0.0] >>> P2=[20.0,10.0] >>> P3=[50.0,30.0] >>> >>> #draws an open arc >>> inkDraw.arc.threePoints(parent=root_layer, Pstart=P1, Pmid=P3, Pend=P3, offset=[25,0], label='arc1', lineStyle=myLineStyle, arcType='open') """ [center,radius] = circle3Points(Pstart, Pmid, Pend) if center is None: return vStart=np.array(Pstart) vEnd=np.array(Pend) vMid=np.array(Pmid) # find side DistVector = vEnd-vStart NormalLeftVector = np.array([DistVector[1],-DistVector[0]]) MidVector = vMid - vStart CenterVector = center - vStart # check if MidVector and CenterVector are pointing to the same side of DistVector if np.dot(CenterVector,NormalLeftVector)*np.dot(MidVector,NormalLeftVector)>0: largeArc = True else: largeArc = False angStart = math.atan2(Pstart[1]-center[1], Pstart[0]-center[0]) angEnd = math.atan2(Pend[1]-center[1], Pend[0]-center[0]) angles = np.unwrap([angStart, angEnd])*180/np.pi angStart=angles[0] angEnd=angles[1] if angEnd - angStart>0: return arc.centerAngStartAngEnd(parent, center, radius, angStart, angEnd, offset, label,lineStyle,arcType,largeArc) else: return arc.centerAngStartAngEnd(parent, center, radius, angEnd, angStart, offset, label,lineStyle,arcType,largeArc) class circle(): """ Class with methods for drawing circles. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def centerRadius(parent, centerPoint, radius, offset=[0, 0], label='circle', lineStyle=lineStyle.setSimpleBlack()): """Draw a circle given its center point and radius :param parent: Parent object :param centerPoint: Center coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param radius: Circle's radius :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'circle' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscape element object :type centerPoint: list :type radius: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new circle object :rtype: line Object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.setSimpleBlack() >>> >>> inkDraw.circle.centerRadius(parent=root_layer, centerPoint=[0,0], radius=15.0, offset=[5,1], label='circle1', lineStyle=myLineStyle) """ # arc instructions arcStringA = ' a %f,%f 0 1 1 %f,%f' % (radius, radius, -2 * radius, 0) arcStringB = ' a %f,%f 0 1 1 %f,%f' % (radius, radius, 2 * radius, 0) # M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Bezier curve,T = smooth quadratic Bezier curveto,A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), inkex.addNS('type', 'sodipodi'): 'arc', inkex.addNS('rx', 'sodipodi'): str(radius), inkex.addNS('ry', 'sodipodi'): str(radius), inkex.addNS('cx', 'sodipodi'): str(centerPoint[0] + offset[0]), inkex.addNS('cy', 'sodipodi'): str(centerPoint[1] + offset[1]), inkex.addNS('start', 'sodipodi'): '0', inkex.addNS('end', 'sodipodi'): str(2 * math.pi), 'd': 'M ' + str(centerPoint[0] + offset[0] + radius) + ' ' + str( centerPoint[1] + offset[1]) + arcStringA + ' ' + arcStringB + ' z'} return etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) # --------------------------------------------- @staticmethod def threePoints(parent, P1, P2, P3, offset=[0, 0], label='circle', lineStyle=lineStyle.setSimpleBlack()): """Draw a circle given 3 poins on the circle. The function checks if the 3 points are aligned. In this case, no circle is drawn. :param parent: parent object :param P1: point coordinates [x,y] :param P2: point coordinates [x,y] :param P3: point coordinates [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'arc' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscape element object :type P1: list :type P2: list :type P3: list :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new circle object :rtype: line Object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.setSimpleBlack() >>> >>> inkDraw.circle.threePoints(parent=root_layer, P1=[0,0], P2=[30,40], P3=[-20,20], offset=[0,0], label='circle1', lineStyle=myLineStyle) .. image:: ../imagesDocs/circle_3P.png :width: 200px """ [center,radius] = circle3Points(P1, P2, P3) return circle.centerRadius(parent, center, radius, offset, label, lineStyle) class rectangle(): """ Class with methods for drawing rectangles. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def widthHeightCenter(parent, centerPoint, width, height, radiusX=None, radiusY=None, offset=[0, 0], label='rectangle', lineStyle=lineStyle.setSimpleBlack()): """Draw a rectangle given its center point and dimensions :param parent: Parent object :param centerPoint: Center coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param width: Dimension in X direction :param height: Dimension in Y direction :param radiusX: Rounding radius in X direction. If this value is ``None``, the rectangle will have sharp corners. Default: None :param radiusY: Rounding radius in Y direction. - If ``None``, then radiusX will also be used in Y direction. - If ``None`` and radiusX is also ``None``, then the rectangle will have sharp corners. Default: None :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'circle' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscape element object :type centerPoint: list :type width: float :type height: float :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new rectangle object :rtype: rectangle Object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws a 50x60 rectangle with radiusX=2.0 and radiusY=3.0 >>> inkDraw.rectangle.widthHeightCenter(parent=root_layer, centerPoint=[0,0], width=50, height=60, radiusX=2.0,radiusY=3.0, offset=[0,0], label='rect1', lineStyle=myLineStyle) """ x = centerPoint[0] - width / 2.0 + offset[0] y = centerPoint[1] - height / 2.0 + offset[1] Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), 'width': str(width), 'height': str(height), 'x': str(x), 'y': str(y), 'rx': str(radiusX), 'ry': str(radiusY)} if radiusX and radiusX > 0.0: Attribs['rx'] = str(radiusX) if radiusY is None: Attribs['ry'] = str(radiusX) else: if radiusY > 0.0: Attribs['ry'] = str(radiusY) return etree.SubElement(parent, inkex.addNS('rect', 'svg'), Attribs) @staticmethod def corners(parent, corner1, corner2, radiusX=None, radiusY=None, offset=[0, 0], label='rectangle', lineStyle=lineStyle.setSimpleBlack()): """Draw a rectangle given the coordinates of two oposite corners :param parent: Parent object :param corner1: Coordinates of corner 1 [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param corner2: Coordinates of corner 2 [x,y] :param radiusX: Rounding radius in X direction. If this value is ``None``, the rectangle will have sharp corners. Default: None :param radiusY: Rounding radius in Y direction. If this value is ``None``, then radiusX will also be used in Y direction. If radiusX is also ``None``, then the rectangle will have sharp corners. Default: None :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'circle' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscape element object :type corner1: list :type corner2: list :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new rectangle object :rtype: rectangle Object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws a rectangle with corners C1=[1,5] and C2=[6,10], with radiusX=2.0 and radiusY=3.0 >>> inkDraw.rectangle.corners(parent=root_layer, corner1=[1,5], corner2=[6,10], radiusX=2.0,radiusY=3.0, offset=[0,0], label='rect1', lineStyle=myLineStyle) """ x = (corner1[0] + corner2[0]) / 2.0 y = (corner1[1] + corner2[1]) / 2.0 width = abs(corner1[0] - corner2[0]) height = abs(corner1[1] - corner2[1]) return rectangle.widthHeightCenter(parent, [x, y], width, height, radiusX, radiusY, offset, label, lineStyle) class ellipseArc(): """ Class with methods for drawing arcs of ellipses. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def startEndRadius(parent, Pstart, Pend, radiusX=1.0, radiusY=2.0, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), flagRightOf=True, arcType='open', largeArc=False): """Draw an arc of ellipse, from ``Pstart`` to ``Pend`` with a given radiusX and radiusY .. image:: ../imagesDocs/ellipse_arc_startEndRadius.png :width: 80px :param parent: Parent object :param Pstart: Start coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param Pend: End coordinate [x,y] :param radiusX: Arc radius is X coordinate :param radiusY: Arc radius is Y coordinate :param offset: Extra offset coords [x,y]. Default [0,0] :param label: Label of the line. Default 'arc' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param flagRightOf: Sets the side of the vector Pend-Pstart which the arc must be drawn. See image below - True: Draws the arc to the right (Default) - False: Draws the arc to the left :param arcType: type of arc. Valid values: 'open', 'slice', 'chord'. See image below. Default: 'open' :param largeArc: Sets the largest arc to be drawn. See image below - True: Draws the largest arc - False: Draws the smallest arc (Default) :type parent: inkscape element object :type Pstart: list :type Pend: list :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :type flagRightOf: bool :type arcType: string :type largeArc: bool :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/ellipse_arc_startEndRadius_flags.png :width: 800px **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> P1=[10.0,0.0] >>> P2=[20.0,10.0] >>> Rx=15.0 >> Ry=20.0 >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws an open arc >>> inkDraw.ellipseArc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radiusX=Rx, radiusX=Ry, offset=[25,0], label='arc1', lineStyle=myLineStyle, arcType='open') >>> >>> #draws a closed (slice) arc >>> inkDraw.ellipseArc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radiusX=Rx, radiusX=Ry, offset=[25,20], label='arc2', lineStyle=myLineStyle, arcType='slice') >>> >>> #draws an open arc to the right >>> inkDraw.ellipseArc.startEndRadius(parent=root_layer, Pstart=P1, Pend=P2, radiusX=Rx, radiusX=Ry, offset=[0,0], label='arc', lineStyle=myLineStyle, flagRightOf=True, largeArc=True) """ # normalize with radiusX and radiusY to a circle with R=1 scaleX=radiusX scaleY=radiusY StartVector =np.divide(np.array(Pstart),np.array([scaleX,scaleY])) EndVector = np.divide(np.array(Pend),np.array([scaleX,scaleY])) radius = 1.0 # finds the center point using some linear algebra. (normalized circunference) DistVector = EndVector - StartVector Dist = np.linalg.norm(DistVector) # distance between start and end if Dist > 2.0 * radius: displayMsg('Error, distance between start and end greater than 2*radius') return None if (flagRightOf and largeArc) or (not flagRightOf and not largeArc): RadiusDirection = np.array([-DistVector[1], DistVector[0]]) # perpendicular to DistVector else: RadiusDirection = np.array([DistVector[1], -DistVector[0]]) # perpendicular to DistVector RadiusDirection = RadiusDirection / np.linalg.norm(RadiusDirection) # normalize RadiusDirection CenterPoint = StartVector + DistVector / 2.0 + RadiusDirection * math.sqrt(radius ** 2.0 - (Dist / 2.0) ** 2.0) # draw circle if False: circle.centerRadius(parent,centerPoint=CenterPoint,radius=1.0,offset=[0,0],lineStyle=lineStyle) circle.centerRadius(parent, centerPoint=StartVector, radius=0.1, offset=[0, 0], lineStyle=lineStyle) circle.centerRadius(parent, centerPoint=EndVector, radius=0.1, offset=[0, 0], lineStyle=lineStyle) circle.centerRadius(parent, centerPoint=Pstart, radius=0.1, offset=[0, 0], lineStyle=lineStyle) circle.centerRadius(parent, centerPoint=Pend, radius=0.1, offset=[0, 0], lineStyle=lineStyle) line.absCoords(parent,[StartVector,CenterPoint,EndVector], lineStyle=lineStyle) # computes the starting angle and ending angle (normalized circle) temp = StartVector - CenterPoint AngStart = math.atan2(temp[1], temp[0]) temp = EndVector - CenterPoint AngEnd = math.atan2(temp[1], temp[0]) if flagRightOf: # inkscape does not follow svg path format to create arcs. It uses sodipodi which is weird =S sodipodiAngleStart = str(AngEnd) sodipodiAngleEnd = str(AngStart) else: sodipodiAngleStart = str(AngStart) sodipodiAngleEnd = str(AngEnd) # arc instructions if largeArc: largeArcFlag = 1 else: largeArcFlag = 0 if flagRightOf: sweepFlag = 0 else: sweepFlag = 1 arcString = ' a %f,%f 0 %d %d %f,%f' % (radiusX, radiusY, largeArcFlag, sweepFlag, EndVector[0] - StartVector[0], EndVector[1] - StartVector[1]) if arcType.lower() == 'slice': arcString = arcString + ' L ' + str(CenterPoint[0]*scaleX + offset[0]) + ' ' + str(CenterPoint[1]*scaleY + offset[1]) + ' z' if arcType.lower() == 'chord': arcString = arcString + ' z' # M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Bezier curve,T = smooth quadratic Bezier curveto,A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), inkex.addNS('type', 'sodipodi'): 'arc', inkex.addNS('rx', 'sodipodi'): str(radiusX), inkex.addNS('ry', 'sodipodi'): str(radiusY), inkex.addNS('cx', 'sodipodi'): str(CenterPoint[0]*scaleX + offset[0]), inkex.addNS('cy', 'sodipodi'): str(CenterPoint[1]*scaleY + offset[1]), inkex.addNS('start', 'sodipodi'): sodipodiAngleStart, inkex.addNS('end', 'sodipodi'): sodipodiAngleEnd, 'd': 'M ' + str(offset[0] + StartVector[0]) + ' ' + str(offset[1] + StartVector[1]) + arcString} if arcType.lower() == 'open': Attribs[inkex.addNS('arc-type', 'sodipodi')] = 'arc' else: Attribs[inkex.addNS('arc-type', 'sodipodi')] = arcType.lower() return etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) # --------------------------------------------- @staticmethod def centerAngStartAngEnd(parent, centerPoint, radiusX, radiusY, angStart, angEnd, offset=[0, 0], label='arc', lineStyle=lineStyle.setSimpleBlack(), arcType='open', largeArc=False): """Draw an arc of ellipse given its center and start and end angles .. image:: ../imagesDocs/ellipse_arc_centerAngStartAngEnd.png :width: 200px :param parent: parent object :param centerPoint: center coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param radiusX: Arc radius is X coordinate :param radiusY: Arc radius is Y coordinate :param angStart: Start angle in degrees :param angEnd: End angle in degrees :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'arc' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :param arcType: Type of arc. Valid values: 'open', 'slice', 'chord'. See image below. Default: 'open' :param largeArc: Sets the largest arc to be drawn. See image below - True: Draws the largest arc - False: Draws the smallest arc (Default) :type parent: inkscape element object :type centerPoint: list :type radiusX: float :type radiusY: float :type angStart: float :type angEnd: float :type offset: list :type label: string :type lineStyle: lineStyle object :type arcType: string :type largeArc: bool :returns: the new arc object :rtype: line Object **Arc options** .. image:: ../imagesDocs/ellipse_arc_centerAngStartAngEnd_flags.png :width: 700px **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle = inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws the shortest arc >>> inkDraw.ellipseArc.centerAngStartAngEnd(parent=root_layer, centerPoint=[0,0], radiusX=15.0, radiusY=25.0, angStart=-10, angEnd=90, >>> offset=[0,0], label='arc1', lineStyle=myLineStyle, arcType='open',largeArc=False) >>> #draws the longest arc >>> inkDraw.ellipseArc.centerAngStartAngEnd(parent=root_layer, centerPoint=[0,0], radiusX=15.0, radiusY=25.0,, angStart=-10, angEnd=90, >>> offset=[30,0], label='arc1', lineStyle=myLineStyle, arcType='open',largeArc=True) """ def getEllipsePoint(a,b,theta_deg): # returns the point on the ellipse at a specified central angle # for this, this function solves the system # # x^2/a^2 + y^2/b^2 = 1 # y= x*tan(theta) # # a: semi-axis along X axis # b: semi-axis along Y axis tanTh = math.tan(math.radians(theta_deg)) xsq = (a ** 2 * b ** 2) / (b ** 2 + a ** 2 * tanTh ** 2) if math.cos(math.radians(theta_deg)) >= 0: # angle in 1st or 4th quadrants x = math.sqrt(xsq) else: # angle in 2nd or 3rd quadrants x = - math.sqrt(xsq) return [x, x * tanTh] Pstart = getEllipsePoint(radiusX,radiusY,angStart) Pend = getEllipsePoint(radiusX,radiusY,angEnd) pos = [centerPoint[0] + offset[0], centerPoint[1] + offset[1]] if abs(angEnd - angStart) <= 180: flagRight = largeArc else: flagRight = not largeArc return ellipseArc.startEndRadius(parent, Pstart, Pend, radiusX, radiusY, pos, label, lineStyle, flagRight, arcType, largeArc) class ellipse(): """ Class with methods for drawing ellipses. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. """ @staticmethod def centerRadius(parent, centerPoint, radiusX, radiusY, offset=[0, 0], label='ellipse', lineStyle=lineStyle.setSimpleBlack()): """Draw an ellipse given its center point and radii :param parent: Parent object :param centerPoint: Center coordinate [x,y] .. warning:: Keep in mind that Inkscape's y axis is upside down! :param radiusX: Ellipse's radius in x direction :param radiusY: Ellipse's radius in y direction :param offset: Extra offset coords [x,y] :param label: Label of the line. Default 'circle' :param lineStyle: Line style to be used. See :class:`lineStyle` class. Default: lineStyle=lineStyle.setSimpleBlack() :type parent: inkscape element object :type centerPoint: list :type radiusX: float :type radiusY: float :type offset: list :type label: string :type lineStyle: lineStyle object :returns: the new ellipse object :rtype: line Object **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> myLineStyle=inkDraw.lineStyle.setSimpleBlack() >>> >>> #draws the shortest arc >>> inkDraw.ellipse.centerRadius(parent=root_layer, centerPoint=[0,0], radiusX=15.0, radiusY=25.0, offset=[5,1], label='circle1', lineStyle=myLineStyle) """ # arc instructions arcStringA = ' a %f,%f 0 1 1 %f,%f' % (radiusX, radiusY, -2 * radiusX, 0) arcStringB = ' a %f,%f 0 1 1 %f,%f' % (radiusX, radiusY, 2 * radiusX, 0) # M = moveto,L = lineto,H = horizontal lineto,V = vertical lineto,C = curveto,S = smooth curveto,Q = quadratic Bezier curve,T = smooth quadratic Bezier curveto,A = elliptical Arc,Z = closepath Attribs = {inkex.addNS('label', 'inkscape'): label, 'style': str(inkex.Style(lineStyle)), inkex.addNS('type', 'sodipodi'): 'arc', inkex.addNS('rx', 'sodipodi'): str(radiusX), inkex.addNS('ry', 'sodipodi'): str(radiusY), inkex.addNS('cx', 'sodipodi'): str(centerPoint[0] + offset[0]), inkex.addNS('cy', 'sodipodi'): str(centerPoint[1] + offset[1]), inkex.addNS('start', 'sodipodi'): '0', inkex.addNS('end', 'sodipodi'): str(2 * math.pi), 'd': 'M ' + str(centerPoint[0] + offset[0] + radiusX) + ' ' + str( centerPoint[1] + offset[1]) + arcStringA + ' ' + arcStringB + ' z'} return etree.SubElement(parent, inkex.addNS('path', 'svg'), Attribs) ================================================ FILE: latest/inkscapeMadeEasy_Plot.py ================================================ #!/usr/bin/python # -------------------------------------------------------------------------------------- # # inkscapeMadeEasy: - Helper module that extends Aaron Spike's inkex.py module, # focusing productivity in inkscape extension development # # Copyright (C) 2016 by Fernando Moura # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # -------------------------------------------------------------------------------------- import math import sys import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw def displayMsg(msg): """Display a message to the user. :param msg: message :type msg: string :returns: nothing :rtype: - .. note:: Identical function has been also defined inside :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy` class """ sys.stderr.write(msg + '\n') def Dump(obj, file='./dump_file.txt', mode='w'): """Function to easily output the result of ``str(obj)`` to a file :param obj: python object to sent to a file. Any object can be used, as long as ``str(obj)`` is implemented (see ``__str__()`` metaclass definition of your object) :param file: file path. Default: ``./dump_file.txt`` :param mode: writing mode of the file Default: ``w`` (write) :type obj: any :type file: string :type mode: string :returns: nothing :rtype: - .. note:: Identical function has been also defined inside :meth:`inkscapeMadeEasy_Base.inkscapeMadeEasy` class This function was created to help debugging the code while it is running under inkscape. Since inkscape does not possess a terminal as today (2016), this function overcomes partially the issue of sending things to stdout by dumping result of the function ``str()`` in a text file. **Example** >>> vector1=[1,2,3,4,5,6] >>> inkPlot.Dump(vector1,file='~/temporary.txt',mode='w') # writes the list to a file >>> vector2=[7,8,9,10] >>> inkPlot.Dump(vector2,file='~/temporary.txt',mode='a') # append the list to a file """ with open(file, mode) as file: file.write(str(obj) + '\n') def generateListOfTicksLinear(axisLimits, axisOrigin, tickStep): """Defines list of ticks in a linear plot .. note:: Internal function. """ # make the list of ticks, symmetrically to the origin listTicksPositive = [axisOrigin] while listTicksPositive[-1] < axisLimits[1]: listTicksPositive.append(listTicksPositive[-1] + tickStep) listTicksNegative = [axisOrigin] while listTicksNegative[-1] > axisLimits[0]: listTicksNegative.append(listTicksNegative[-1] - tickStep) listTicks = listTicksPositive + listTicksNegative[1:] return listTicks def generateListOfTicksLog10(axisLimits): """Defines list of ticks in a log10 plot .. note:: Internal function. """ # make the list of ticks, symmetrically to the origin listTicks = [axisLimits[0]] while listTicks[-1] < axisLimits[1]: listTicks.append(listTicks[-1] * 10) return listTicks def findOrigin(axisLimits, flagLog10, scale): """ retrieves the position of the origin. In case of logarithmic scale, it will be axisLimits[0] .. note:: Internal function. """ if flagLog10: axisOrigin = math.log10(axisLimits[0]) * scale else: if axisLimits[0] <= 0.0 and axisLimits[1] >= 0.0: axisOrigin = 0.0 else: if axisLimits[1] < 0: axisOrigin = axisLimits[1] * scale else: axisOrigin = axisLimits[0] * scale return axisOrigin def getPositionAndText(value, scale, flagLog10, axisUnitFactor): """given a value, its scale, and some flags, finds it position in the diagram and the text to be shown .. note:: Internal function.""" if flagLog10: pos = math.log10(value) * scale else: pos = value * scale # try to simplify number if int(value) - value == 0: valStr = str(int(round(value, 3))) else: valStr = str(round(value, 3)) # option to add extra factor to the axis ticks if flagLog10: exponent = str(int(math.log10(value))) if axisUnitFactor: if inkDraw.useLatex: Text = '10^{' + exponent + '}' + axisUnitFactor + '' else: Text = '10^' + exponent + '' + axisUnitFactor + '' else: if inkDraw.useLatex: Text = '10^{' + exponent + '}' else: Text = '10^' + exponent + '' else: if axisUnitFactor: if value == 0: Text = '0' if value == 1: Text = axisUnitFactor if value == -1: Text = '-' + axisUnitFactor if value != 0 and value != 1 and value != -1: Text = valStr + axisUnitFactor else: Text = valStr if inkDraw.useLatex: Text = '$' + Text + '$' return [pos, Text] class axis(): """ This class has member functions to create customizable plot axes. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. .. note:: This class can use LaTeX to render text if LaTeX support is enabled. LaTeX support is an optional feature that requires a few extra packages to be installed outside inkscape. **It is enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. If disabled, this function will still work, internally calling the :meth:`inkscapeMadeEasy_Draw.text.write` to generate text. """ @staticmethod def cartesian(ExtensionBaseObj, parent, xLim, yLim, position=[0, 0], xLabel='', yLabel='', xlog10scale=False, ylog10scale=False, xTicks=True, yTicks=True, xTickStep=1.0, yTickStep=1.0, xScale=20, yScale=20, xAxisUnitFactor='', yAxisUnitFactor='', xGrid=False, yGrid=False, forceTextSize=0, forceLineWidth=0, drawAxis=True, ExtraLengthAxisX=0.0, ExtraLengthAxisY=0.0): """Creates the axes of a cartesian plot .. note:: This method uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param parent: Parent object :param xLim: Limits of the X axis [x_min,x_max]. If the axis is in log10 scale, then the limits will be rounded to complete one decade. :param yLim: Limits of the Y axis [y_min,y_max]. If the axis is in log10 scale, then the limits will be rounded to complete one decade. :param position: Position of the plot. It is defined at the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. - The same rule applies to y direction. :param xLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param yLabel: Label of the Y axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param xlog10scale: Sets X axis to log10 scale if True. Default: False :param ylog10scale: Sets Y axis to log10 scale if True. Default: False :param xTicks: Adds axis ticks to the X axis if True. Default: True :param yTicks: Adds axis ticks to the Y axis if True. Default: True :param xTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param yTickStep: Value interval between two consecutive ticks on Y axis. (Not used if Y axis is in log10 scale). Default:1.0 :param xScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param yScale: Distance between each yTickStep in svg units. Default: 20 - If axis is linear, then yScale is the size in svg units of each tick - If axis is log10, the yScale is the size in svg units of one decade :param xAxisUnitFactor: Extra text to be added to the ticks in X axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param yAxisUnitFactor: Extra text to be added to the ticks in Y axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param xGrid: Adds grid lines to X axis if True. Default: False :param yGrid: Adds grid lines to Y axis if True. Default: False :param forceTextSize: Size of the text. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param forceLineWidth: Width of the lines. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param drawAxis: Control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLengthAxisX: Extra length near the arrow pointer of X axis. Default 0.0 :param ExtraLengthAxisY: Extra length near the arrow pointer of Y axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object :type parent: inkscape element object :type xLim: list :type yLim: list :type position: list :type xLabel: string :type yLabel: string :type xlog10scale: bool :type ylog10scale: bool :type xTicks: bool :type yTicks: bool :type xTickStep: float :type yTickStep: float :type xScale: float :type yScale: float :type xAxisUnitFactor: string :type yAxisUnitFactor: string :type xGrid: bool :type yGrid: bool :type forceTextSize: float :type forceLineWidth: float :type drawAxis: bool :type ExtraLengthAxisX: float :type ExtraLengthAxisY: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the axis area object (if drawAxis=False, this output is ``None``) - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list **Examples** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> inkPlot.axis.cartesian(ExtensionBaseObj, parent=root_layer, xLim=[0,3], yLim=[0,2], position=[0, 0], >>> xLabel='my label $x$', yLabel='my label $y$', xlog10scale=False, ylog10scale=False, >>> xTicks=True, yTicks=True, xTickStep=0.5, yTickStep=1.0, xScale=50, yScale=60, >>> xAxisUnitFactor='', yAxisUnitFactor='', xGrid=True, yGrid=True, forceTextSize=0, forceLineWidth=0, >>> drawAxis=True, ExtraLengthAxisX=20.0, ExtraLengthAxisY=10.0) The images below show the cartesian plane of the example above, together with other variations. .. image:: ../imagesDocs/plot_axisCartesianParameters_01.png :width: 800px """ if drawAxis: GroupPlot = ExtensionBaseObj.createGroup(parent, 'Plot') # sets the scale scaleX and scaleY stores the size of one unit in both axis (linear axis) or the size of a decade in log plots if xlog10scale: scaleX = xScale else: scaleX = xScale / float(xTickStep) if ylog10scale: scaleY = -yScale else: scaleY = -yScale / float(yTickStep) # negative bc inkscape is upside down # font size and other text parameters if forceTextSize == 0: textSize = 0.25 * min(xScale, yScale) else: textSize = forceTextSize textSizeSmall = 0.8 * textSize # font size for axis ticks text_offset = textSize # base space for text positioning ExtraSpaceArrowX = (2.0 + ExtraLengthAxisX) * text_offset # extra space for drawing arrow on axis ExtraSpaceArrowY = (3.0 + ExtraLengthAxisY) * text_offset # extra space for drawing arrow on axis lenghtTicks = textSize / 2.0 # length of the ticks # create styles if forceLineWidth == 0: lineWidth = min(xScale, yScale) / 35.0 else: lineWidth = forceLineWidth lineWidthGrid = 0.7 * lineWidth lineWidthGridFine = lineWidthGrid / 2.0 nameMarkerArrowAxis = inkDraw.marker.createArrow1Marker(ExtensionBaseObj, 'ArrowAxis', RenameMode=1, scale=0.4) lineStyleAxis = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3), markerEnd=nameMarkerArrowAxis[1]) lineStyleTicks = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3)) lineStyleGrid = inkDraw.lineStyle.set(lineWidthGrid, lineColor=inkDraw.color.gray(0.7)) lineStyleGridFine = inkDraw.lineStyle.set(lineWidthGridFine, lineColor=inkDraw.color.gray(0.7)) textStyleLarge = inkDraw.textStyle.setSimpleBlack(textSize) textStyleSmall = inkDraw.textStyle.setSimpleBlack(textSizeSmall, 'center') # check if limits are valid if xLim[0] >= xLim[1]: sys.stderr.write('Error: xLim is invalid.') return 0 if yLim[0] >= yLim[1]: sys.stderr.write('Error: yLim is invalid.') return 0 # check if the limits are valid for logarithmic scales. if xlog10scale: if xLim[0] <= 0 or xLim[1] <= 0: sys.stderr.write('Error: xLim is invalid in logarithmic scale') return 0 else: xmin = pow(10, math.floor(math.log10(xLim[0]))) xmax = pow(10, math.ceil(math.log10(xLim[1]))) xLimits = [xmin, xmax] else: xLimits = xLim if ylog10scale: if yLim[0] <= 0 or yLim[1] <= 0: sys.stderr.write('Error: yLim is invalid in logarithmic scale') return 0 else: ymin = pow(10, math.floor(math.log10(yLim[0]))) ymax = pow(10, math.ceil(math.log10(yLim[1]))) yLimits = [ymin, ymax] else: yLimits = yLim # finds the position of the Origin of axis axisOrigin = [0.0, 0.0] axisOrigin[0] = findOrigin(xLimits, xlog10scale, scaleX) axisOrigin[1] = findOrigin(yLimits, ylog10scale, scaleY) # computes the positions of the limits on svg, considering the scale if xlog10scale: # convert limits to position in diagram, including scaling factor xLimitsPos = [math.log10(x) * scaleX for x in xLimits] else: xLimitsPos = [x * scaleX for x in xLimits] if ylog10scale: # convert limits to position in diagram, including scaling factor yLimitsPos = [math.log10(y) * scaleY for y in yLimits] else: yLimitsPos = [y * scaleY for y in yLimits] # build the list of tuples with the limits of the plotting area outputLimits = list(zip([xLimits[0], xLimits[1], yLimits[0], yLimits[1]], [xLimitsPos[0] - axisOrigin[0] + position[0], xLimitsPos[1] - axisOrigin[0] + position[0], yLimitsPos[0] - axisOrigin[1] + position[1], yLimitsPos[1] - axisOrigin[1] + position[1]])) if not drawAxis: return [None, outputLimits, axisOrigin] # axis ticks groupTicks = ExtensionBaseObj.createGroup(GroupPlot, 'Ticks') if xTicks or xGrid: if xlog10scale: listTicks = generateListOfTicksLog10(xLimits) else: listTicks = generateListOfTicksLinear(xLimits, axisOrigin[0] / scaleX, xTickStep) for x in listTicks: if x <= xLimits[1] and x >= xLimits[0]: # get position, considering the scale and its text [posX, xText] = getPositionAndText(x, scaleX, xlog10scale, xAxisUnitFactor) if xGrid and posX != axisOrigin[0]: # grid lines. Do not draw if grid line is over the axis inkDraw.line.absCoords(groupTicks, [[posX, yLimitsPos[0]], [posX, yLimitsPos[1]]], [0, 0], lineStyle=lineStyleGrid) # intermediate grid lines in case of logarithmic scale if xGrid and xlog10scale and x < xLimits[1]: for i in range(2, 10): aditionalStep = math.log10(i) * scaleX inkDraw.line.absCoords(groupTicks, [[posX + aditionalStep, yLimitsPos[0]], [posX + aditionalStep, yLimitsPos[1]]], [0, 0], lineStyle=lineStyleGridFine) # tick if xTicks: if posX != axisOrigin[0]: # don't draw if in the origin inkDraw.line.relCoords(groupTicks, [[0, lenghtTicks]], [posX, axisOrigin[1] - lenghtTicks / 2.0], lineStyle=lineStyleTicks) # sets justification # inkDraw.text.write(ExtensionBaseObj,'orig='+str(axisOrigin),[axisOrigin[0]+10,axisOrigin[1]-30],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'xlim='+str(xLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-20],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'ylim='+str(yLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-10],groupTicks,fontSize=7) if axisOrigin[1] == yLimitsPos[0]: justif = 'tc' offsetX = 0 offsetY = text_offset / 2.0 # inkDraw.circle.centerRadius(groupTicks, axisOrigin, 10, [0,0]) if axisOrigin[1] != yLimitsPos[0] and axisOrigin[1] != yLimitsPos[1]: justif = 'tr' offsetX = -text_offset / 4.0 offsetY = text_offset / 2.0 # inkDraw.circle.centerRadius(groupTicks, axisOrigin, 10, [0,0]) # inkDraw.text.write(ExtensionBaseObj,str(axisOrigin[1]),[axisOrigin[0]+10,axisOrigin[1]+10],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,str(yLimitsPos[0]),[axisOrigin[0]+10,axisOrigin[1]+20],groupTicks,fontSize=7) if posX == axisOrigin[0]: if posX == xLimitsPos[1]: justif = 'tr' offsetX = -text_offset / 4.0 else: justif = 'tl' offsetX = +text_offset / 4.0 if axisOrigin[1] == yLimitsPos[1]: justif = 'bc' offsetX = 0 offsetY = -text_offset / 2.0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0]) if posX == axisOrigin[0]: if posX == xLimitsPos[1]: justif = 'br' offsetX = -text_offset / 4.0 else: justif = 'bl' offsetX = +text_offset / 4.0 # value if xTicks: inkDraw.text.latex(ExtensionBaseObj, groupTicks, xText, [posX + offsetX, axisOrigin[1] + offsetY], textSizeSmall, refPoint=justif) if yTicks or yGrid: # approximate limits to multiples of 10 if ylog10scale: listTicks = generateListOfTicksLog10(yLimits) else: listTicks = generateListOfTicksLinear(yLimits, axisOrigin[1] / scaleY, yTickStep) for y in listTicks: if y <= yLimits[1] and y >= yLimits[0]: # get position, considering the scale and its text [posY, yText] = getPositionAndText(y, abs(scaleY), ylog10scale, yAxisUnitFactor) posY = -posY if yGrid and posY != axisOrigin[1]: # grid lines. Do not draw if grid line is over the axis inkDraw.line.absCoords(groupTicks, [[xLimitsPos[0], posY], [xLimitsPos[1], posY]], [0, 0], lineStyle=lineStyleGrid) # intermediate grid lines in case of logarithmic scale if yGrid and ylog10scale and y < yLimits[1]: for i in range(2, 10): aditionalStep = math.log10(i) * scaleY inkDraw.line.absCoords(groupTicks, [[xLimitsPos[0], posY + aditionalStep], [xLimitsPos[1], posY + aditionalStep]], [0, 0], lineStyle=lineStyleGridFine) # tick if yTicks: if posY != axisOrigin[1]: # don't draw if in the origin inkDraw.line.relCoords(groupTicks, [[lenghtTicks, 0]], [axisOrigin[0] - lenghtTicks / 2.0, posY], lineStyle=lineStyleTicks) # sets justification # inkDraw.text.write(ExtensionBaseObj,'orig='+str(axisOrigin),[axisOrigin[0]+10,axisOrigin[1]-30],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'xlim='+str(xLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-20],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'ylim='+str(yLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-10],groupTicks,fontSize=7) if axisOrigin[0] == xLimitsPos[0]: justif = 'cr' offsetX = -text_offset / 2.0 offsetY = 0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0],'trash') if axisOrigin[0] != xLimitsPos[0] and axisOrigin[0] != xLimitsPos[1]: justif = 'tr' offsetX = -text_offset / 2.0 offsetY = text_offset / 4.0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0]) # inkDraw.text.write(ExtensionBaseObj,str(axisOrigin[0]),[axisOrigin[0]+10,axisOrigin[1]+10],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,str(yLimitsPos[0]*scaleX),[axisOrigin[0]+10,axisOrigin[1]+20],groupTicks,fontSize=7) if posY == axisOrigin[1]: if posY == yLimitsPos[1]: justif = 'tr' offsetY = text_offset / 4.0 else: justif = 'br' offsetY = -text_offset / 4.0 if axisOrigin[0] == xLimitsPos[1]: justif = 'cl' offsetX = text_offset / 2.0 offsetY = 0 # inkDraw.circle.centerRadius(groupTicks,axisOrigin, 10, [0,0]) if posY == axisOrigin[1]: if posY == yLimitsPos[1]: justif = 'tl' offsetY = text_offset / 4.0 else: justif = 'bl' offsetY = -text_offset / 4.0 # value if yTicks: inkDraw.text.latex(ExtensionBaseObj, groupTicks, yText, [axisOrigin[0] + offsetX, (posY + offsetY)], textSizeSmall, refPoint=justif) ExtensionBaseObj.moveElement(GroupPlot, [position[0] - axisOrigin[0], position[1] - axisOrigin[1]]) # draw axis in the end so it stays on top of other objects GroupAxis = ExtensionBaseObj.createGroup(GroupPlot, 'Axis') inkDraw.line.absCoords(GroupAxis, [[xLimitsPos[0], 0], [xLimitsPos[1] + ExtraSpaceArrowX, 0]], [0, axisOrigin[1]], 'Xaxis', lineStyle=lineStyleAxis) if xLabel: # axis labels inkDraw.text.latex(ExtensionBaseObj, GroupAxis, xLabel, [xLimitsPos[1] + ExtraSpaceArrowX - text_offset / 3, axisOrigin[1] + text_offset / 2.0], textSize, refPoint='tl') inkDraw.line.absCoords(GroupAxis, [[0, yLimitsPos[0]], [0, yLimitsPos[1] - ExtraSpaceArrowY]], [axisOrigin[0], 0], 'Yaxis', lineStyle=lineStyleAxis) if yLabel: # axis labels inkDraw.text.latex(ExtensionBaseObj, GroupAxis, yLabel, [axisOrigin[0] + text_offset / 2.0, (yLimitsPos[1] - ExtraSpaceArrowY)], textSize, refPoint='tl') return [GroupPlot, outputLimits, axisOrigin] @staticmethod def polar(ExtensionBaseObj, parent, rLim, tLim=[0.0, 360.0], position=[0.0, 0.0], rLabel='', rlog10scale=False, rTicks=True, tTicks=True, rTickStep=1.0, tTickStep=45.0, rScale=20, rAxisUnitFactor='', rGrid=False, tGrid=False, forceTextSize=0, forceLineWidth=0, drawAxis=True, ExtraLengthAxisR=0.0): """Creates the axes of a polar plot .. note:: This method uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param parent: Parent object :param rLim: Limits of the R axis [r_min,r_max]. If the axis is in log10 scale, then the limits will be rounded to complete one decade. :param tLim: Limits of the theta axis [t_min,t_max]. Values in degrees. Default: [0,360] :param position: Position of the center [x0,y0]. :param rLabel: Label of the R axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param rlog10scale: Sets R axis to log10 scale if True. Default: False - If rlog10scale=True, then the lower limit of rLim must be >=1 :param rTicks: Adds axis ticks to the R axis if True. Default: True :param tTicks: Adds axis ticks to the theta axis if True. Default: True :param rTickStep: Value interval between two consecutive ticks on R axis. (Not used if R axis is in log10 scale). Default:1.0 :param tTickStep: Value interval between two consecutive ticks on theta axis. Default:45.0 :param rScale: Distance between each rTickStep in svg units. Default: 20 - If axis is linear, then rScale is the size in svg units of each tick - If axis is log10, the rScale is the size in svg units of one decade :param rAxisUnitFactor: Extra text to be added to the ticks in R axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param rGrid: Adds grid lines to R axis if true. Default: False :param tGrid: Adds grid lines to theta axis if true. Default: False :param forceTextSize: Size of the text. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param forceLineWidth: Width of the lines. If this parameter is 0.0 then the method will compute an appropriate size. Default: 0.0 :param drawAxis: Control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLengthAxisR: Extra length between the R axis and its label. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object :type parent: inkscape element object :type rLim: list :type tLim: list :type position: list :type rLabel: string :type rlog10scale: bool :type rTicks: bool :type tTicks: bool :type rTickStep: float :type tTickStep: float :type rScale: float :type rAxisUnitFactor: string :type rGrid: bool :type tGrid: bool :type forceTextSize: float :type forceLineWidth: float :type drawAxis: bool :type ExtraLengthAxisR: float :returns: [GroupPlot, outputRLimits, axisOrigin] - GroupPlot: the axis area object (if drawAxis=False, this output is ``None``) - outputRLimits: a list with tuples:[(r_min,rPos_min),(r_max,rPos_max)] - r_min, r_max : The limits of the axis object - rPos_min, rPos_max : The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0] : A list with the coordinates of the point where the axes cross. :rtype: list **Examples** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> >>> inkPlot.axis.cartesian(self, parent=root_layer, rLim=[0, 3], tLim=[0, 150], position=[0.0, 0.0], rLabel='my radius', >>> rlog10scale=False, rTicks=True, tTicks=True, rTickStep=1.0, tTickStep=30.0, rScale=50, >>> rAxisUnitFactor='', rGrid=True, tGrid=True, forceTextSize=0, forceLineWidth=0, >>> drawAxis=True, ExtraLengthAxisR=10.0) The images below show the cartesian plane of the example above, together with other variations. .. image:: ../imagesDocs/plot_axisPolarParameters_01.png :width: 800px """ if drawAxis: GroupPlot = ExtensionBaseObj.createGroup(parent, 'Plot') # sets the scale scaleX and scaleY stores the size of one unit in both axis (linear axis) or the size of a decade in log plots if rlog10scale: scaleR = rScale else: scaleR = rScale / float(rTickStep) # font size and other text parameters if forceTextSize == 0: textSize = 0.2 * rScale else: textSize = forceTextSize textSizeSmall = 0.8 * textSize # font size for axis ticks text_offset = textSize # base space for text positioning ExtraSpaceArrowR = (2.0 + ExtraLengthAxisR) * text_offset # extra space for drawing arrow on axis lenghtTicks = textSize / 2.0 # length of the ticks # create styles if forceLineWidth == 0: lineWidth = rScale / 30.0 else: lineWidth = forceLineWidth lineWidthGrid = 0.7 * lineWidth lineWidthGridFine = lineWidthGrid / 2.0 # nameTickerArrowAxis = inkDraw.marker.createArrow1Marker(ExtensionBaseObj, 'ArrowAxis', RenameMode=1, scale=0.4) lineStyleAxis = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3)) lineStyleTicks = inkDraw.lineStyle.set(lineWidth, lineColor=inkDraw.color.gray(0.3)) lineStyleGrid = inkDraw.lineStyle.set(lineWidthGrid, lineColor=inkDraw.color.gray(0.7)) lineStyleGridFine = inkDraw.lineStyle.set(lineWidthGridFine, lineColor=inkDraw.color.gray(0.7)) textStyleLarge = inkDraw.textStyle.setSimpleBlack(textSize) textStyleSmall = inkDraw.textStyle.setSimpleBlack(textSizeSmall, 'center') # check if limits are valid if rLim[0] < 0.0 or rLim[0] >= rLim[1]: sys.stderr.write('Error: rLim is invalid') return 0 if tLim[0] >= tLim[1]: sys.stderr.write('Error: tLim is invalid') return 0 # check if the limits are valid for logarithmic scales. if rlog10scale: if rLim[0] < 1 or rLim[1] < 1: sys.stderr.write('Error: rLim is invalid in logarithmic scale') return 0 else: rmin = pow(10, math.floor(math.log10(rLim[0]))) rmax = pow(10, math.ceil(math.log10(rLim[1]))) rLimits = [rmin, rmax] else: rLimits = rLim tLimits = tLim if abs(tLimits[1] - tLimits[0]) > 360: tLimits = [0, 360] if abs(tLimits[1] - tLimits[0]) > 180: largeArc = True else: largeArc = False # finds the position of the Origin of axis axisOrigin = [0.0, 0.0] axisOrigin[0] = findOrigin(rLimits, rlog10scale, scaleR) axisOrigin[1] = findOrigin(tLimits, False, 1.0) # computes the positions of the limits on svg, considering the scale if rlog10scale: # convert limits to position in diagram, including scaling factor rLimitsPos = [math.log10(x) * scaleR for x in rLimits] else: rLimitsPos = [x * scaleR for x in rLimits] # build the list of tuples with the limits of the plotting area outputLimits = list(zip([rLimits[0], rLimits[1]], [rLimitsPos[0] - axisOrigin[0] + position[0], rLimitsPos[1] - axisOrigin[0] + position[0]])) if not drawAxis: return [None, outputLimits, [0, 0]] # axis ticks groupTicks = ExtensionBaseObj.createGroup(GroupPlot, 'Ticks') if rTicks or rGrid: if rlog10scale: listTicks = generateListOfTicksLog10(rLimits) else: listTicks = generateListOfTicksLinear(rLimits, axisOrigin[0] / scaleR, rTickStep) for r in listTicks: if r <= rLimits[1] and r >= rLimits[0]: # get position, considering the scale and its text [posR, rText] = getPositionAndText(r, scaleR, rlog10scale, rAxisUnitFactor) if rGrid and posR > 0.0 and r > rLimits[0] and r < rLimits[1]: # grid lines. if tLimits[1] - tLimits[0] < 360: inkDraw.arc.centerAngStartAngEnd(groupTicks, [0, 0], posR, -tLimits[1], -tLimits[0], [0, 0], lineStyle=lineStyleGrid, largeArc=largeArc) # negative angles bc inkscape is upside down else: inkDraw.circle.centerRadius(groupTicks, [0, 0], posR, offset=[0, 0], lineStyle=lineStyleGrid) # intermediate grid lines in case of logarithmic scale if rGrid and rlog10scale and r < rLimits[1]: for i in range(2, 10): aditionalStep = math.log10(i) * scaleR if tLimits[1] - tLimits[0] < 360: inkDraw.arc.centerAngStartAngEnd(groupTicks, [0, 0], posR + aditionalStep, -tLimits[1], -tLimits[0], [0, 0], lineStyle=lineStyleGridFine, largeArc=largeArc) # negative angles bc inkscape is upside down else: inkDraw.circle.centerRadius(groupTicks, [0, 0], posR + aditionalStep, offset=[0, 0], lineStyle=lineStyleGridFine) # tick if rTicks and posR > 0.0: inkDraw.arc.centerAngStartAngEnd(groupTicks, [0, 0], posR, -tLimits[0] - math.degrees(lenghtTicks / float(posR * 2)), -tLimits[0] + math.degrees(lenghtTicks / float(posR * 2)), [0, 0], lineStyle=lineStyleTicks, largeArc=False) if rTicks and posR == 0.0: inkDraw.line.relCoords(groupTicks, [[0, lenghtTicks]], [0, - lenghtTicks / 2.0], lineStyle=lineStyleTicks) # sets justification # inkDraw.text.write(ExtensionBaseObj,'orig='+str(axisOrigin),[axisOrigin[0]+10,axisOrigin[1]-30],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'xlim='+str(xLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-20],groupTicks,fontSize=7) # inkDraw.text.write(ExtensionBaseObj,'ylim='+str(yLimitsPos),[axisOrigin[0]+10,axisOrigin[1]-10],groupTicks,fontSize=7) if posR == 0: justif = 'cc' offsetX = 0 offsetY = text_offset * 1.2 posX = posR * math.cos(math.radians(-tLimits[0])) + offsetX posY = posR * math.sin(math.radians(-tLimits[0])) + offsetY else: offsetT = text_offset * 1.2 if tLimits[1] - tLimits[0] > 340: offsetR = text_offset / 2.0 else: offsetR = 0 justif = 'cc' posX = (posR + offsetR) * math.cos(math.radians(-tLimits[0])) + offsetT * math.sin(math.radians(tLimits[0])) posY = (posR + offsetR) * math.sin(math.radians(-tLimits[0])) + offsetT * math.cos(math.radians(-tLimits[0])) # value # inkDraw.circle.centerRadius(groupTicks,[posX,posY], 1) if rTicks: inkDraw.text.latex(ExtensionBaseObj, groupTicks, rText, [posX, posY], textSizeSmall, refPoint=justif) if tTicks or tGrid: listTicks = generateListOfTicksLinear(tLimits, axisOrigin[1], tTickStep) for t in listTicks: if t <= tLimits[1] and t >= tLimits[0]: c = math.cos(math.radians(-t)) # negative angles bc inkscape is upside down s = math.sin(math.radians(-t)) # negative angles bc inkscape is upside down # get position, considering the scale and its text if inkDraw.useLatex: tText = '$' + str(t) + '$' else: tText = str(t) if (tGrid and t > tLimits[0] and t < tLimits[1]) or (tGrid and t == tLimits[0] and tLimits[1] - tLimits[0] >= 360): if rLimitsPos[0] == 0: # if rmin is zero, then make the lines to reach the center if not rlog10scale: P1 = [(rLimitsPos[0] + scaleR * rTickStep / 2) * c, (rLimitsPos[0] + scaleR * rTickStep / 2) * s] else: P1 = [(rLimitsPos[0] + 0.3 * scaleR) * c, (rLimitsPos[0] + 0.3 * scaleR) * s] else: P1 = [rLimitsPos[0] * c, rLimitsPos[0] * s] P2 = [rLimitsPos[1] * c, rLimitsPos[1] * s] inkDraw.line.absCoords(groupTicks, [P1, P2], [0, 0], lineStyle=lineStyleGrid) # tick if (tTicks and t != tLimits[1]) or (tTicks and t == tLimits[1] and tLimits[1] - tLimits[0] < 360): P1 = [(rLimitsPos[1] - lenghtTicks / 2.0) * c, (rLimitsPos[1] - lenghtTicks / 2.0) * s] inkDraw.line.relCoords(groupTicks, [[lenghtTicks * c, lenghtTicks * s]], P1, lineStyle=lineStyleTicks) if c > 1.0e-4: justif = 'cl' else: if c < -1.0e-4: justif = 'cr' else: justif = 'cc' offsetR = text_offset posX = (rLimitsPos[1] + offsetR) * c posY = (rLimitsPos[1] + offsetR) * s # value if (tTicks and t != tLimits[1]) or (tTicks and t == tLimits[1] and tLimits[1] - tLimits[0] < 360): inkDraw.text.latex(ExtensionBaseObj, groupTicks, tText, [posX, posY], textSizeSmall, refPoint=justif) ExtensionBaseObj.moveElement(GroupPlot, position) # draw axis in the end so it stays on top of other objects GroupAxis = ExtensionBaseObj.createGroup(GroupPlot, 'Axis') c0 = math.cos(math.radians(-tLimits[0])) # negative angles bc inkscape is upside down s0 = math.sin(math.radians(-tLimits[0])) # negative angles bc inkscape is upside down c1 = math.cos(math.radians(-tLimits[1])) # negative angles bc inkscape is upside down s1 = math.sin(math.radians(-tLimits[1])) # negative angles bc inkscape is upside down P1 = [rLimitsPos[0] * c0, rLimitsPos[0] * s0] P2 = [rLimitsPos[1] * c0, rLimitsPos[1] * s0] P3 = [rLimitsPos[1] * c1, rLimitsPos[1] * s1] P4 = [rLimitsPos[0] * c1, rLimitsPos[0] * s1] if tLimits[1] - tLimits[0] < 360: inkDraw.line.absCoords(GroupAxis, [P1, P2], [0, 0], lineStyle=lineStyleAxis) inkDraw.line.absCoords(GroupAxis, [P3, P4], [0, 0], lineStyle=lineStyleAxis) else: if rTicks: inkDraw.line.absCoords(GroupAxis, [P1, P2], [0, 0], lineStyle=lineStyleAxis) if tLimits[1] - tLimits[0] < 360: if rLimitsPos[0] > 0: inkDraw.arc.startEndRadius(GroupAxis, P1, P4, rLimitsPos[0], offset=[0, 0], lineStyle=lineStyleAxis, flagRightOf=True, arcType='open', largeArc=largeArc) inkDraw.arc.startEndRadius(GroupAxis, P2, P3, rLimitsPos[1], offset=[0, 0], lineStyle=lineStyleAxis, flagRightOf=True, arcType='open', largeArc=largeArc) else: if rLimitsPos[0] > 0: inkDraw.circle.centerRadius(GroupAxis, [0, 0], rLimitsPos[0], offset=[0, 0], lineStyle=lineStyleAxis) inkDraw.circle.centerRadius(GroupAxis, [0, 0], rLimitsPos[1], offset=[0, 0], lineStyle=lineStyleAxis) if rLabel: # axis labels c0 = math.cos(math.radians(-tLimits[0]) + text_offset / rLimitsPos[1]) # negative angles bc inkscape is upside down s0 = math.sin(math.radians(-tLimits[0]) + text_offset / rLimitsPos[1]) # negative angles bc inkscape is upside down posText = [(rLimitsPos[1] + ExtraSpaceArrowR) * c0, (rLimitsPos[1] + ExtraSpaceArrowR) * s0] inkDraw.text.latex(ExtensionBaseObj, GroupAxis, rLabel, posText, textSize, refPoint='cl') return [GroupPlot, outputLimits, [0, 0]] class plot(): """ This class has member functions to create plots. .. note:: This class contains only static methods so that your plugin class don't have to inherit it. .. note:: This class uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. If disabled, this function will still work, internally calling the :meth:`inkscapeMadeEasy_Draw.text.write` to generate text. """ @staticmethod def cartesian(ExtensionBaseObj, parent, xData, yData, position=[0, 0], xLabel='', yLabel='', xlog10scale=False, ylog10scale=False, xTicks=True, yTicks=True, xTickStep=1.0, yTickStep=1.0, xScale=20, yScale=20, xExtraText='', yExtraText='', xGrid=False, yGrid=False, generalAspectFactorAxis=1.0, lineStylePlot=inkDraw.lineStyle.setSimpleBlack(), forceXlim=None, forceYlim=None, drawAxis=True, ExtraLengthAxisX=0.0, ExtraLengthAxisY=0.0): """Create a cartesian Plot .. note:: This method uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param parent: Parent object :param xData: List of x data :param yData: List of y data :param position: Position of the plot. It is defined at the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. - The same rule applies to y direction. :param xLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param yLabel: Label of the Y axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param xlog10scale: Sets X axis to log10 scale if True. Default: False :param ylog10scale: Sets Y axis to log10 scale if True. Default: False :param xTicks: Adds axis ticks to the X axis if True. Default: True :param yTicks: Adds axis ticks to the Y axis if True. Default: True :param xTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param yTickStep: Value interval between two consecutive ticks on Y axis. (Not used if Y axis is in log10 scale). Default:1.0 :param xScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param yScale: Distance between each yTickStep in svg units. Default: 20 - If axis is linear, then yScale is the size in svg units of each tick - If axis is log10, the yScale is the size in svg units of one decade :param xExtraText: Extra text to be added to the ticks in X axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param yExtraText: Extra text to be added to the ticks in Y axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param xGrid: Adds grid lines to X axis if True. Default: False :param yGrid: Adds grid lines to Y axis if True. Default: False :param generalAspectFactorAxis: Regulates the general aspect ratio between grid lines, text and Ticks separations. Default: 1.0 :param lineStylePlot: Line style to be used to plot the data. See class ``inkscapeMadeEasy_Draw.lineStyle``. Default: lineStylePlot=inkDraw.lineStyle.setSimpleBlack() :param forceXlim: Forces limits of X axis to these limits. These limits affect the axis only, that is, all xData is plotted despite of these limits. - if forceXlim=None Limits will be defined by the limits of xData (Default) - if forceXlim=[xMin,xMax] then these limits will be used. .. note:: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale :param forceYlim: Forces limits of Y axis to these limits. These limits affect the axis only, that is, all yData is plotted despite of these limits. - if forceYlim=None Limits will be defined by the limits of yData (Default) - if forceYlim=[yMin,yMax] then these limits will be used. .. note:: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale :param drawAxis: Control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLengthAxisX: Extra length near the arrow pointer of X axis. Default 0.0 :param ExtraLengthAxisY: Extra length near the arrow pointer of Y axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object :type parent: inkscape element object :type xData: list :type yData: list :type position: list :type xLabel: string :type yLabel: string :type xlog10scale: bool :type ylog10scale: bool :type xTicks: bool :type yTicks: bool :type xTickStep: float :type yTickStep: float :type xScale: float :type yScale: float :type xExtraText: string :type yExtraText: string :type xGrid: bool :type yGrid: bool :type generalAspectFactorAxis: float :type lineStylePlot: lineStyle object :type forceXlim: list :type forceYlim: list :type drawAxis: bool :type ExtraLengthAxisX: float :type ExtraLengthAxisY: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the plot object - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list .. important:: If any of the axis are log10, then the method ignores any pairs of (x,y) data with invalid coordinates, that is, if xData and/or yData is less than or equal to 0.0 (they would result in complex log10... =P ). The method will create a text object alongside your plot warning this. .. note:: If any of the axis are linear, the method will ignore any value greater than 10.000 (in absolute value). This avoids plotting very large numbers. The method will create a text object alongside your plot warning this. **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> xData=[-1,-0.5,0,0.5,1.0,1.5,2] >>> yData=[x*x for x in xData] # computes y=x*x >>> #create a lineStyle for the plot >>> myMarkerDot=inkDraw.marker.createDotMarker(self,'DotM',RenameMode=2,scale=0.3, >>> strokeColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black')) >>> lineStyleDiscrete = inkDraw.lineStyle.set(lineWidth=1.0, markerStart=myMarkerDot,markerMid=myMarkerDot,markerEnd=myMarkerDot) >>> >>> inkPlot.plot.cartesian(self,root_layer,xData,yData,position=[0,0], >>> xLabel='my $x$ data',yLabel='$y(x)$',xlog10scale=False,ylog10scale=False, >>> xTicks=True,yTicks=True,xTickStep=0.5,yTickStep=2.0, >>> xScale=20,yScale=10,xExtraText='a',yExtraText='', >>> xGrid=True,yGrid=True,generalAspectFactorAxis=1.0,lineStylePlot=lineStyleDiscrete, >>> forceXlim=None,forceYlim=None,drawAxis=True) The image below present the plot above with a few argument variations. .. image:: ../imagesDocs/plot_plotCartesianParameters_01.png :width: 800px """ textSize = generalAspectFactorAxis * 0.25 * min(xScale, yScale) lineWidthAxis = generalAspectFactorAxis * min(xScale, yScale) / 35.0 yDataTemp = [] xDataTemp = [] flagShowedError = False if xlog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(xData)): if xData[i] > 0.0: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(xData)): if abs(xData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp yDataTemp = [] xDataTemp = [] flagShowedError = False if ylog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(yData)): if yData[i] > 0.0: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(yData)): if abs(yData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp if forceXlim is not None: Xlimits = forceXlim else: Xlimits = [min(xData), max(xData)] if forceYlim is not None: Ylimits = forceYlim else: Ylimits = [min(yData), max(yData)] # min<->max inverted bc inkscape is upside down if Ylimits[0] == Ylimits[1]: if Ylimits[0] > 0: Ylimits[0] = 0 if Ylimits[0] == 0: Ylimits[1] = 1 if Ylimits[0] < 0: Ylimits[1] = 0 if Xlimits[0] == Xlimits[1]: if Xlimits[0] > 0: Xlimits[0] = 0 if Xlimits[0] == 0: Xlimits[1] = 1 if Xlimits[0] < 0: Xlimits[1] = 0 # draw axis axisGroup = ExtensionBaseObj.createGroup(parent, 'PlotData') [axisObj, limits, origin] = axis.cartesian(ExtensionBaseObj, axisGroup, Xlimits, Ylimits, position, xLabel=xLabel, yLabel=yLabel, xlog10scale=xlog10scale, ylog10scale=ylog10scale, xTicks=xTicks, yTicks=yTicks, xTickStep=xTickStep, yTickStep=yTickStep, xScale=xScale, yScale=yScale, xAxisUnitFactor=xExtraText, yAxisUnitFactor=yExtraText, xGrid=xGrid, yGrid=yGrid, forceTextSize=textSize, forceLineWidth=lineWidthAxis, drawAxis=drawAxis, ExtraLengthAxisX=ExtraLengthAxisX, ExtraLengthAxisY=ExtraLengthAxisY) # scales data and convert to logarithmic scale if needed. Also subtracts the origin point of the axis to move the plot to the correct position if xlog10scale: xData = [math.log10(x) * xScale - origin[0] for x in xData] else: xData = [x * (xScale / xTickStep) - origin[0] for x in xData] if ylog10scale: yData = [-math.log10(y) * yScale - origin[1] for y in yData] else: yData = [-y * (yScale / yTickStep) - origin[1] for y in yData] # negative bc inkscape is upside down coords = zip(xData, yData) inkDraw.line.absCoords(axisGroup, coords, position, lineStyle=lineStylePlot) return [axisGroup, limits, origin] @staticmethod def polar(ExtensionBaseObj, parent, rData, tData, position=[0, 0], rLabel='', rlog10scale=False, rTicks=True, tTicks=True, rTickStep=1.0, tTickStep=45.0, rScale=20, rExtraText='', rGrid=False, tGrid=False, generalAspectFactorAxis=1.0, lineStylePlot=inkDraw.lineStyle.setSimpleBlack(), forceRlim=None, forceTlim=None, drawAxis=True, ExtraLengthAxisR=0.0): """Create a polar Plot .. note:: This method uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param parent: Parent object :param rData: List of R data :param tData: List of Theta data :param position: Position of the plot [x0,y0]. It is defined at the center point :param rLabel: Label of the R axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param rlog10scale: Sets R axis to log10 scale if True. Default: False :param rTicks: Adds axis ticks to the R axis if True. Default: True :param tTicks: Adds axis ticks to the Theta axis if True. Default: True :param rTickStep: Value interval between two consecutive ticks on R axis. (Not used if R axis is in log10 scale). Default:1.0 :param tTickStep: Value interval between two consecutive ticks on Theta axis. :param rScale: Distance between each rTickStep in svg units. Default: 20 - If axis is linear, then rScale is the size in svg units of each tick - If axis is log10, the rScale is the size in svg units of one decade :param rExtraText: Extra text to be added to the ticks in R axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param rGrid: Adds grid lines to R axis if True. Default: False :param tGrid: Adds grid lines to Theta axis if True. Default: False :param generalAspectFactorAxis: Regulates the general aspect ratio between grid lines, text and Ticks separations. Default: 1.0 :param lineStylePlot: Line style to be used to plot the data. See class ``inkscapeMadeEasy_Draw.lineStyle``. Default: lineStylePlot=inkDraw.lineStyle.setSimpleBlack() :param forceRlim: Forces limits of R axis to these limits. These limits affect the axis only, that is, all rData is plotted despite of these limits. - if forceRlim=None Limits will be defined by the limits of rData (Default) - if forceRlim=[rMin,rMax] then these limits will be used. .. note:: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale :param forceTlim: Forces limits of Theta axis to these limits. These limits affect the axis only, that is, all tData is plotted despite of these limits. - if forceTlim=None Limits will be defined by min and max of tData (Default) - if forceTlim=[tMin,tMax] then these limits will be used. :param drawAxis: Control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLengthAxisR: Extra length near the arrow pointer of R axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object :type parent: inkscape element object :type rData: list :type tData: list :type position: list :type rLabel: string :type rlog10scale: bool :type rTicks: bool :type tTicks: bool :type rTickStep: float :type tTickStep: float :type rScale: float :type rExtraText: string :type rGrid: bool :type tGrid: bool :type generalAspectFactorAxis: float :type lineStylePlot: lineStyle object :type forceRlim: list :type forceTlim: list :type drawAxis: bool :type ExtraLengthAxisR: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the plot object - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list .. important:: If any of the axis are log10, then the method ignores any pairs of (x,y) data with invalid coordinates, that is, if rData and/or tData is less than or equal to 0.0 (they would result in complex log10... =P ). The method will create a text object alongside your plot warning this. .. note:: If any of the axis are linear, the method will ignore any value greater than 10.000 (in absolute value). This avoids plotting very large numbers. The method will create a text object alongside your plot warning this. **Example** >>> root_layer = self.document.getroot() # retrieves the root layer of the document >>> rData=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12] >>> tData=[30*x for x in range(12)] >>> >>> myMarkerDot=inkDraw.marker.createDotMarker(self,'DotM',RenameMode=2,scale=0.3, >>> strokeColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('black')) >>> lineStyleDiscrete = inkDraw.lineStyle.set(lineWidth=1.0,linecolor=inkDraw.color.defined('red'), >>> markerStart=myMarkerDot,markerMid=myMarkerDot,markerEnd=myMarkerDot) >>> >>> inkPlot.plot.polar(self,root_layer,rData,tData,position=[0,0], >>> rLabel='my $R$ data',rlog10scale=False, >>> rTicks=True,tTicks=True,rTickStep=2,tTickStep=30, >>> rScale=20,rExtraText='a', >>> rGrid=True,tGrid=True,generalAspectFactorAxis=1.0,lineStylePlot=lineStyleDiscrete, >>> forceRlim=None,forceTlim=None,drawAxis=True) >>> >>> # another spiral, comprising two turns >>> tData=[2*x for x in range(360)] >>> rData=[x/180.0 for x in tData] >>> >>> inkPlot.plot.polar(self,root_layer,rData,tData,position=[0,0], >>> rLabel='my $R$ data',rlog10scale=False, >>> rTicks=True,tTicks=True,rTickStep=2,tTickStep=30, >>> rScale=20,rExtraText='', >>> rGrid=True,tGrid=True,generalAspectFactorAxis=1.0, >>> forceRlim=None,forceTlim=None,drawAxis=True) The image below present the plot above with a few argument variations. .. image:: ../imagesDocs/plot_plotPolarParameters_01.png :width: 900px """ textSize = generalAspectFactorAxis * 0.25 * rScale lineWidthAxis = generalAspectFactorAxis * rScale / 35.0 tDataTemp = [] rDataTemp = [] flagShowedError = False if rlog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(rData)): if rData[i] >= 1.0: tDataTemp.append(tData[i]) rDataTemp.append(rData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (rData[i], tData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(rData)): if abs(rData[i]) <= 1.0e4: tDataTemp.append(tData[i]) rDataTemp.append(rData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (rData[i], tData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True tData = tDataTemp rData = rDataTemp if forceRlim is not None: Rlimits = forceRlim else: Rlimits = [min(rData), max(rData)] if forceTlim is not None: Tlimits = forceTlim else: Tlimits = [min(tData), max(tData)] # min<->max inverted bc inkscape is upside down if Tlimits[0] == Tlimits[1]: if Tlimits[0] > 0: Tlimits[0] = 0 if Tlimits[0] == 0: Tlimits[1] = 360 if Tlimits[0] < 0: Tlimits[1] = 0 if Rlimits[0] == Rlimits[1]: if Rlimits[0] > 0: Rlimits[0] = 0 if Rlimits[0] == 0: Rlimits[1] = 1 if Rlimits[0] < 0: Rlimits[1] = 0 # draw axis axisGroup = ExtensionBaseObj.createGroup(parent, 'PlotData') [axisObj, limits, origin] = axis.polar(ExtensionBaseObj, axisGroup, Rlimits, Tlimits, position, rLabel=rLabel, rlog10scale=rlog10scale, rTicks=rTicks, tTicks=tTicks, rTickStep=rTickStep, tTickStep=tTickStep, rScale=rScale, rAxisUnitFactor=rExtraText, rGrid=rGrid, tGrid=tGrid, forceTextSize=textSize, forceLineWidth=lineWidthAxis, drawAxis=drawAxis, ExtraLengthAxisR=ExtraLengthAxisR) # scales data and convert to logarithmic scale if needed. Also subtracts the origin point of the axis to move the plot to the correct position nPoints = min(len(rData), len(tData)) xData = [] yData = [] if rlog10scale: for i in range(nPoints): xData.append(math.log10(rData[i]) * math.cos(math.radians(-tData[i])) * rScale) # negative theta bc inkscape is upside down yData.append(math.log10(rData[i]) * math.sin(math.radians(-tData[i])) * rScale) # negative theta bc inkscape is upside down else: for i in range(nPoints): xData.append(rData[i] * math.cos(math.radians(-tData[i])) * (rScale / rTickStep)) # negative theta bc inkscape is upside down yData.append(rData[i] * math.sin(math.radians(-tData[i])) * (rScale / rTickStep)) # negative theta bc inkscape is upside down coords = zip(xData, yData) inkDraw.line.absCoords(axisGroup, coords, position, lineStyle=lineStylePlot) return [axisGroup, limits, origin] @staticmethod def stem(ExtensionBaseObj, parent, xData, yData, position=[0, 0], xLabel='', yLabel='', ylog10scale=False, xTicks=True, yTicks=True, xTickStep=1.0, yTickStep=1.0, xScale=20, yScale=20, xExtraText='', yExtraText='', xGrid=False, yGrid=False, generalAspectFactorAxis=1.0, lineStylePlot=inkDraw.lineStyle.setSimpleBlack(), forceXlim=None, forceYlim=None, drawAxis=True, ExtraLengthAxisX=0.0, ExtraLengthAxisY=0.0): """Create a cartesian stem plot .. note:: This method uses LaTeX in labels and tick marks if LaTeX support is enabled. This is an optional feature, **enabled by default**. Please refer to :ref:`disableLatexSupport` on how to disable it. :param ExtensionBaseObj: Most of the times you have to pass 'self' when calling from inside your plugin class. See example below :param parent: Parent object :param xData: List of x data :param yData: List of y data :param position: Position of the plot. It is defined at the point where x and y axis cross [x0,y0]. The point where the axis cross depend on the limits. - If xLimits comprises the origin x=0, then the Y axis crosses the X axis at x=0. - If xLimits contains only negative numbers, then the Y axis crosses the X axis at x_max. - If xLimits contains only positive numbers, then the Y axis crosses the X axis at x_min. - The same rule applies to y direction. :param xLabel: Label of the X axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param yLabel: Label of the Y axis. Default: '' The text can contain any LaTeX command. If you want to write mathematical text, you can enclose it between dollar signs $...$. If LaTeX support is disabled, do not use $. :param ylog10scale: Sets Y axis to log10 scale if True. Default: False :param xTicks: Adds axis ticks to the X axis if True. Default: True :param yTicks: Adds axis ticks to the Y axis if True. Default: True :param xTickStep: Value interval between two consecutive ticks on X axis. (Not used if X axis is in log10 scale). Default:1.0 :param yTickStep: Value interval between two consecutive ticks on Y axis. (Not used if Y axis is in log10 scale). Default:1.0 :param xScale: Distance between each xTickStep in svg units. Default: 20 - If axis is linear, then xScale is the size in svg units of each tick - If axis is log10, the xScale is the size in svg units of one decade :param yScale: Distance between each yTickStep in svg units. Default: 20 - If axis is linear, then yScale is the size in svg units of each tick - If axis is log10, the yScale is the size in svg units of one decade :param xExtraText: Extra text to be added to the ticks in X axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param yExtraText: Extra text to be added to the ticks in Y axis. Default: '' This is useful when we want to represent interval with different units. example pi, 2pi 3pi, etc. The text can be any LaTeX text. Keep in mind that this text will be inserted within a mathematical environment $...$, therefore no $ is needed here. :param xGrid: Adds grid lines to X axis if True. Default: False :param yGrid: Adds grid lines to Y axis if True. Default: False :param generalAspectFactorAxis: Regulates the general aspect ratio between grid lines, text and Ticks separations. Default: 1.0 :param lineStylePlot: Line style to be used to plot the data. See class ``inkscapeMadeEasy_Draw.lineStyle``. Default: lineStylePlot=inkDraw.lineStyle.setSimpleBlack() :param forceXlim: Forces limits of X axis to these limits. These limits affect the axis only, that is, all xData is plotted despite of these limits. - if forceXlim=None Limits will be defined by the limits of xData (Default) - if forceXlim=[xMin,xMax] then these limits will be used. .. note:: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale :param forceYlim: Forces limits of Y axis to these limits. These limits affect the axis only, that is, all yData is plotted despite of these limits. - if forceYlim=None Limits will be defined by the limits of yData (Default) - if forceYlim=[yMin,yMax] then these limits will be used. .. note:: for logarithmic scale, the limits are always adjusted to complete the decade. Usually you don't need this for logarithmic scale :param drawAxis: Control flag of the axis method - True: draws axis normally - False: returns the limits and origin position without drawing the axis itself :param ExtraLengthAxisX: Extra length near the arrow pointer of X axis. Default 0.0 :param ExtraLengthAxisY: Extra length near the arrow pointer of Y axis. Default 0.0 :type ExtensionBaseObj: inkscapeMadeEasy object :type parent: inkscape element object :type xData: list :type yData: list :type position: list :type xLabel: string :type yLabel: string :type ylog10scale: bool :type xTicks: bool :type yTicks: bool :type xTickStep: float :type yTickStep: float :type xScale: float :type yScale: float :type xExtraText: string :type yExtraText: string :type xGrid: bool :type yGrid: bool :type generalAspectFactorAxis: float :type lineStylePlot: lineStyle object :type forceXlim: list :type forceYlim: list :type drawAxis: bool :type ExtraLengthAxisX: float :type ExtraLengthAxisY: float :returns: [GroupPlot, outputLimits, axisOrigin] - GroupPlot: the plot object - outputLimits: a list with tuples:[(x_min,xPos_min),(x_max,xPos_max),(y_min,yPos_min),(y_max,yPos_max)] - x_min, x_max, y_min, y_max: The limits of the axis object - xPos_min, xPos_max, yPos_min, yPos_max: The positions of the limits of the axis object, considering the scaling and units - axisOrigin [X0,Y0]: A list with the coordinates of the point where the axes cross. :rtype: list .. important:: If any of the axis are log10, then the method ignores any pairs of (x,y) data with invalid coordinates, that is, if xData and/or yData is less than or equal to 0.0 (they would result in complex log10... =P ). The method will create a text object alongside your plot warning this. .. note:: If any of the axis are linear, the method will ignore any value greater than 10.000 (in absolute value). This avoids plotting very large numbers. The method will create a text object alongside your plot warning this. **Example** >>> oot_layer = self.document.getroot() # retrieves the root layer of the document >>> xData=[-1,-0.5,0,0.5,1.0,1.5,2] >>> yData=[x*x for x in xData] # computes y=x*x >>> >>> # creates a line style with a dot marker for the stem plot >>> myMarkerDot=inkDraw.marker.createDotMarker(self,'DotMDiscreteTime',RenameMode=2,scale=0.3, >>> strokeColor=inkDraw.color.defined('black'),fillColor=inkDraw.color.defined('red')) >>> lineStyleDiscrete = inkDraw.lineStyle.set(lineWidth=1.0, markerEnd=myMarkerDot) >>> >>> inkPlot.plot.stem(self,root_layer,xData,yData,position=[0,0], >>> xLabel='my $x$ data',yLabel='$y(x)$',ylog10scale=False, >>> xTicks=True,yTicks=True,xTickStep=0.5,yTickStep=2.0, >>> xScale=20,yScale=20,xExtraText='a',yExtraText='', >>> xGrid=True,yGrid=True,generalAspectFactorAxis=1.0,lineStylePlot=lineStyleDiscrete, >>> forceXlim=None,forceYlim=None,drawAxis=True) The image below present the plot above. .. image:: ../imagesDocs/plot_plotStemParameters_01.png :width: 400px """ textSize = generalAspectFactorAxis * 0.25 * min(xScale, yScale) lineWidthAxis = generalAspectFactorAxis * min(xScale, yScale) / 35.0 yDataTemp = [] xDataTemp = [] flagShowedError = False # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(xData)): if abs(xData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp yDataTemp = [] xDataTemp = [] flagShowedError = False if ylog10scale: # remove invalid pairs of coordinates for log plot (less than or equal to 0.0) for i in range(len(yData)): if yData[i] > 0.0: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is invalid in logarithmic scale. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True else: # remove invalid pairs of coordinates for linear plot (larger than +-10k ) for i in range(len(yData)): if abs(yData[i]) <= 1.0e4: yDataTemp.append(yData[i]) xDataTemp.append(xData[i]) else: if not flagShowedError: inkDraw.text.write(ExtensionBaseObj, 'Error: The point (%f,%f)\n is too large. Ignoring it...' % (xData[i], yData[i]), [position[0], position[1] + 2 * textSize], parent, fontSize=textSize / 2.0) inkDraw.text.write(ExtensionBaseObj, ' Please check your graph', [position[0], position[1] + 2.5 * textSize], parent, fontSize=textSize / 2.0) flagShowedError = True yData = yDataTemp xData = xDataTemp if forceXlim: Xlimits = forceXlim else: Xlimits = [min(xData), max(xData)] if forceYlim: Ylimits = forceYlim else: Ylimits = [min(yData), max(yData)] # min<->max inverted bc inkscape is upside down if Ylimits[0] == Ylimits[1]: if Ylimits[0] > 0: Ylimits[0] = 0 if Ylimits[0] == 0: Ylimits[1] = 1 if Ylimits[0] < 0: Ylimits[1] = 0 if Xlimits[0] == Xlimits[1]: if Xlimits[0] > 0: Xlimits[0] = 0 if Xlimits[0] == 0: Xlimits[1] = 1 if Xlimits[0] < 0: Xlimits[1] = 0 # draw axis axisGroup = ExtensionBaseObj.createGroup(parent, 'PlotData') [axisObj, limits, origin] = axis.cartesian(ExtensionBaseObj, axisGroup, Xlimits, Ylimits, position, xLabel=xLabel, yLabel=yLabel, xlog10scale=False, ylog10scale=ylog10scale, xTicks=xTicks, yTicks=yTicks, xTickStep=xTickStep, yTickStep=yTickStep, xScale=xScale, yScale=yScale, xAxisUnitFactor=xExtraText, yAxisUnitFactor=yExtraText, xGrid=xGrid, yGrid=yGrid, forceTextSize=textSize, forceLineWidth=lineWidthAxis, drawAxis=drawAxis, ExtraLengthAxisX=ExtraLengthAxisX, ExtraLengthAxisY=ExtraLengthAxisY) # scales data and convert to logarithmic scale if needed. Also subtracts the origin point of the axis to move the plot to the correct position xData = [x * (xScale / xTickStep) - origin[0] for x in xData] if ylog10scale: yData = [-math.log10(y) * yScale - origin[1] for y in yData] else: yData = [-y * (yScale / yTickStep) - origin[1] for y in yData] # negative bc inkscape is upside down stemGroup = ExtensionBaseObj.createGroup(axisGroup, 'StemGroup') for i in range(len(xData)): inkDraw.line.relCoords(stemGroup, [[0, yData[i]]], [xData[i] + position[0], 0 + position[1]], lineStyle=lineStylePlot) return [axisGroup, limits, origin]