diff --git a/extensions/fablabchemnitz/animate_order/animate_order.html b/extensions/fablabchemnitz/animate_order/animate_order.html deleted file mode 100644 index 9f71ad11..00000000 --- a/extensions/fablabchemnitz/animate_order/animate_order.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Animate Order - Kopfteil-Laser.svg - - - - -
- - - - - diff --git a/extensions/fablabchemnitz/animate_order/drawing.svg b/extensions/fablabchemnitz/animate_order/drawing.svg deleted file mode 100644 index 82a5f1d1..00000000 --- a/extensions/fablabchemnitz/animate_order/drawing.svg +++ /dev/null @@ -1,13947 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Base.py b/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Base.py new file mode 100644 index 00000000..efeeb980 --- /dev/null +++ b/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Base.py @@ -0,0 +1,1307 @@ +#!/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 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 gropu) 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): + """ Import SVG file into the current document + + This function will all unify the defs node via :meth:`unifyDefs` + + :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: inkscape 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: 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) + 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 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 + + # --------------------------------------------- + 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 unifyDefs(self): + """Unify all nodes in a single node. + + :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 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) the 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]) + 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(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 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 or groups. 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([inkex.addNS('path', 'svg'), inkex.addNS('text', 'svg'), 'g', inkex.addNS('g', 'svg'), 'path', 'use', inkex.addNS('use', 'svg')]) + if element.tag not in accepted_strings: + return listCoords + + 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 ['text', 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 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 = [] + 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() # 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 + data + + + diff --git a/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Draw.py b/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Draw.py new file mode 100644 index 00000000..d9839709 --- /dev/null +++ b/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Draw.py @@ -0,0 +1,2231 @@ +#!/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 ``#RRGGBB`` + + .. note:: This class contains only static methods so that your plugin class don't have to inherit it. + + .. warning:: alpha channel is not implemented yet. Assume alpha=1.0 + + """ + + @staticmethod + def defined(colorName): + """ Return the color string representing a predefined color name + + :param colorName: prededined color name. See figure below + :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 = inkDraw.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): + """ 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 + :returns: string representing the color in inkscape's expected format ``#RRGGBB`` + :rtype: string + + **Example** + + >>> colorString = inkDraw.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 rgb(RGBlist): + """ 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 + :returns: string representing the color in inkscape's expected format ``#RRGGBB`` + :rtype: string + + **Example** + + >>> colorString = color.rgb([0.5,0.8,0.0]) # returns a string representing the color R=127, G=204, B=0 + + """ + RGBhex = [''] * 3 + for i in range(3): + if RGBlist[i] >= 1.0: + RGBlist[i] = 1.0 + + if RGBlist[i] <= 0.0: + RGBlist[i] = 0 + + if RGBlist[i]*255 < 16: + RGBhex[i] = '0' + hex(int(RGBlist[i]*255))[2:].upper() + + else: + RGBhex[i] = hex(int(RGBlist[i]*255))[2:].upper() + + return '#' + '%s%s%s' % (RGBhex[0], RGBhex[1], RGBhex[2]) + + # --------------------------------------------- + @staticmethod + def gray(percentage): + """ 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 + :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 RGBLevel > 255: + RGBLevel = 255 + if RGBLevel < 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`` + + .. 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='#000000') + + 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='#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 + + """ + # [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 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 `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="#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 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 + >>> [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 :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 ``#RRGGBB`` (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 ``#RRGGBB`` (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' + if strokeColor is None: + strokeColor = 'none' + + marker_style = {'fill-rule': 'evenodd', 'fill': fillColor, 'stroke': strokeColor, 'stroke-width': str(lineWidth)} + + 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.get_ids().add(nameID) + + 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 ``#RRGGBB`` (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 ``#RRGGBB`` (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 ``#RRGGBB`` (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 ``#RRGGBB`` (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 ``#RRGGBB`` (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 ``#RRGGBB`` (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='#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, 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 ``#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'. 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 + + :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** + + >>> # 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): + """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: string + + **Example** + + >>> 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 + + .. 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 ``#RRGGBB`` (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: string + + **Example** + + >>> 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'): + """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: string + + **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 ``#RRGGBB`` (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: string + + **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 ``#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 + :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 + + # 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('stroke:#[0-9a-fA-F]+', 'stroke:' + textColor, 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=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) + """ + + # 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 arcType.lower() == 'slice': + arcString = arcString + ' L ' + str(CenterPoint[0] + offset[0]) + ' ' + str(CenterPoint[1] + 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(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 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, 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) + """ + + 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, 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 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='circle', 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) diff --git a/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Plot.py b/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Plot.py new file mode 100644 index 00000000..5818dad9 --- /dev/null +++ b/extensions/fablabchemnitz/inkscapeMadeEasy/inkscapeMadeEasy_Plot.py @@ -0,0 +1,1632 @@ +#!/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] diff --git a/extensions/fablabchemnitz/polyhedra/polyhedra.py b/extensions/fablabchemnitz/polyhedra/polyhedra.py index daed4573..21eb39a7 100644 --- a/extensions/fablabchemnitz/polyhedra/polyhedra.py +++ b/extensions/fablabchemnitz/polyhedra/polyhedra.py @@ -150,9 +150,9 @@ class Polyhedra(inkex.EffectExtension): gsub_attribs = {inkex.addNS('label','inkscape'):'Polygon ' + str( poly ) + 'border' } gsub = etree.SubElement(g, 'g', gsub_attribs) - # Create SVG Path for gear - cutStyle = { 'stroke': '#0000FF', 'fill': 'none' } - perfStyle = { 'stroke': '#FF0000', 'fill': 'none' } + # Create SVG Path + cutStyle = { 'stroke': '#0000FF', 'stroke-width': '1px', 'fill': 'none' } + perfStyle = { 'stroke': '#FF0000', 'stroke-width': '1px', 'fill': 'none' } textStyle = { 'font-size': str( size/4 ), 'font-family': 'arial', diff --git a/extensions/fablabchemnitz/spirograph.inx b/extensions/fablabchemnitz/spirograph.inx new file mode 100644 index 00000000..b4d15ad5 --- /dev/null +++ b/extensions/fablabchemnitz/spirograph.inx @@ -0,0 +1,34 @@ + + + SpiroGraph + fablabchemnitz.de.spirograph + + + + + + + + 10.0 + 5.0 + 2 + + + 1.0 + false + false + + + + + all + + + + + + + + diff --git a/extensions/fablabchemnitz/spirograph.py b/extensions/fablabchemnitz/spirograph.py new file mode 100644 index 00000000..fcb12b37 --- /dev/null +++ b/extensions/fablabchemnitz/spirograph.py @@ -0,0 +1,251 @@ +#!/usr/bin/python + +import math +import os +import numpy as np +import scipy.signal as scipySignal + +import inkscapeMadeEasy.inkscapeMadeEasy_Base as inkBase +import inkscapeMadeEasy.inkscapeMadeEasy_Draw as inkDraw +import inkscapeMadeEasy.inkscapeMadeEasy_Plot as inkPlot + + +# least common multiplier +def myLcm(x, y): + return x * y / math.gcd(int(x), int(y)) + + +# --------------------------------------------- +# noinspection PyAttributeOutsideInit +class Spirograph(inkBase.inkscapeMadeEasy): + def __init__(self): + inkBase.inkscapeMadeEasy.__init__(self) + + self.arg_parser.add_argument("--tab", type=str, dest="tab", default="object") + self.arg_parser.add_argument("--curveType", type=str, dest="curveType", default='resistor') + self.arg_parser.add_argument("--radius_R", type=float, dest="radius_R", default=10.0) + self.arg_parser.add_argument("--radius_r", type=float, dest="radius_r", default=5.0) + self.arg_parser.add_argument("--detailLevel", type=float, dest="detailLevel", default=1.0) + self.arg_parser.add_argument("--adaptiveTheta", type=self.bool, dest="adaptiveTheta", default=False) + self.arg_parser.add_argument("--pencil_distance", type=float, dest="pencil_distance", default=1.0) + self.arg_parser.add_argument("--drawBaseCircles", type=self.bool, dest="drawBaseCircles", default=False) + self.arg_parser.add_argument("--animate", type=self.bool, dest="animate", default=False) + self.arg_parser.add_argument("--directory", type=str, dest="directory", default='./') + + def effect(self): + so = self.options + + # sets the position to the viewport center, round to next 10. + position = [self.svg.namedview.center[0], self.svg.namedview.center[1]] + position[0] = int(math.ceil(position[0] / 10.0)) * 10 + position[1] = int(math.ceil(position[1] / 10.0)) * 10 + + root_layer = self.document.getroot() + group = self.createGroup(root_layer, 'Spiro') + + so.tab = so.tab.replace('"', '') # removes de exceeding double quotes from the string + + # curve parameters + R = so.radius_R + r = so.radius_r + d = so.pencil_distance + finalTheta = 2 * np.pi * myLcm(abs(r), R) / R + + if 'hypo' in so.curveType.lower(): + typeCurve = 'hypo' + if 'epi' in so.curveType.lower(): + typeCurve = 'epi' + + # markers and linestyles + Lgray = inkDraw.color.gray(0.8) + Dgray = inkDraw.color.gray(0.3) + # wheel + markerCenterDisk = inkDraw.marker.createDotMarker(self, nameID='diskCenter', scale=0.3, RenameMode=1, strokeColor=Dgray, + fillColor=inkDraw.color.defined('white')) + markerPen = inkDraw.marker.createDotMarker(self, nameID='diskPen', scale=0.3, RenameMode=1, strokeColor=Dgray, + fillColor=inkDraw.color.defined('white')) + [startArrowMarker, endArrowMarker] = inkDraw.marker.createArrow1Marker(self, nameID='arrowRot', RenameMode=1, scale=0.3, strokeColor=Dgray, + fillColor=Dgray) + + if typeCurve == 'hypo': + self.lineStyleArrow = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=startArrowMarker, markerEnd=None) + else: + self.lineStyleArrow = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=None, markerEnd=endArrowMarker) + + self.lineStyleARM = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=Dgray, markerStart=markerCenterDisk, markerEnd=markerPen) + self.lineStyleDisk = inkDraw.lineStyle.set(lineWidth=r / 40, lineColor=None, fillColor=Lgray) + + # curve + self.lineStyleCurve = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('red'), markerStart=None, markerEnd=None, + markerMid=None) + self.lineStyleCurve2 = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('Dgreen'), markerStart=None, markerEnd=None, + markerMid=None) + self.lineStyleCurve3 = inkDraw.lineStyle.set(lineWidth=0.8, lineColor=inkDraw.color.defined('blue'), markerStart=None, markerEnd=None, + markerMid=None) + + self.lineStylePre = inkDraw.lineStyle.set(lineWidth=1, lineColor=inkDraw.color.defined('red')) + self.constructionLine = inkDraw.lineStyle.set(lineWidth=0.5, lineColor=Dgray) + + # draft Points + if so.adaptiveTheta: + nPrePoints = 10 * so.detailLevel # number of pre points per turn + thetasDraft = np.linspace(0, finalTheta, int(nPrePoints * finalTheta / (2 * np.pi))) + + [pointsDraft, _, curvatureDraft] = self.calcCurve__trochoid(typeCurve, R, r, d, thetasDraft) + + # find sampling points based on local curvature + nSamples = np.ones(curvatureDraft.shape)*min(2,so.detailLevel) + detailFactor=5 + # treshold normalized curvatures + nSamples[curvatureDraft>0.8] *=detailFactor + detailFactor = 2.5 + # check if vector changed direction abuptly + for i,p in enumerate(pointsDraft): + if i==0: + v1=pointsDraft[i+1]-pointsDraft[i] + v2=pointsDraft[i]-pointsDraft[-1] + elif i < len(pointsDraft)-1: + v1=pointsDraft[i+1]-pointsDraft[i] + v2=pointsDraft[i]-pointsDraft[i-1] + else: + v1=pointsDraft[0]-pointsDraft[i] + v2=pointsDraft[i]-pointsDraft[i-1] + + v1=v1/np.linalg.norm(v1) + v2=v2/np.linalg.norm(v2) + if np.dot(v1,v2)<0.5: + nSamples[i] *=detailFactor + + thetasFinal = np.array([]) + for i in range(len(nSamples) - 1): + thetasFinal = np.append(thetasFinal, np.linspace(thetasDraft[i], thetasDraft[i + 1], int(nSamples[i]), endpoint=False)) + + thetasFinal = np.append(thetasFinal, finalTheta) + # filter the sampled angles to have a smooth transition. + Ntaps = 5 + gaussWindow = scipySignal.gaussian(Ntaps, std=5) + gaussWindow = gaussWindow / np.sum(gaussWindow) + + # inkPlot.plot.cartesian(self, root_layer, np.arange(thetasFinal.shape[0]), thetasFinal * 180 / np.pi, position, xTicks=False, yTicks=True, xTickStep=thetasFinal.shape[0]/10, yTickStep=120.0, xScale=10, yScale=10,xGrid=True, yGrid=True, forceXlim=None, forceYlim=None) + thetasFinal = scipySignal.filtfilt(gaussWindow, np.array([1]), thetasFinal) + + # inkPlot.plot.cartesian(self, root_layer, np.arange(thetasFinal.shape[0]), thetasFinal * 180 / np.pi, position, xTicks=False, yTicks=True,xTickStep=thetasFinal.shape[0]/10, yTickStep=120.0, xScale=10, yScale=10, xGrid=True, yGrid=True, forceXlim=None, forceYlim=None,drawAxis=False) + else: + nPrePoints = 20 * so.detailLevel # number of pre points per turn + thetasFinal = np.linspace(0, finalTheta, int(nPrePoints * finalTheta / (2 * np.pi))) + + # final shape + [PointsFinal, CentersFinal, curvatureFinal] = self.calcCurve__trochoid(typeCurve, R, r, d, thetasFinal) + [PointsFinal2, CentersFinal2, curvatureFinal2] = self.calcCurve__trochoid(typeCurve, R, r, -d, thetasFinal) + [PointsFinal3, CentersFinal3, curvatureFinal3] = self.calcCurve__trochoid(typeCurve, R, r, r, thetasFinal) + + if so.animate: + animGroup = self.createGroup(group, 'Anim') + + circle_R = inkDraw.circle.centerRadius(parent=animGroup, centerPoint=[0, 0], radius=R, offset=position, lineStyle=self.constructionLine) + + # draw planetary wheel + wheelGroup = self.createGroup(animGroup, 'Anim') + circle_r = inkDraw.circle.centerRadius(wheelGroup, centerPoint=CentersFinal[0], radius=r, offset=position, lineStyle=self.lineStyleDisk) + arms1 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal[0], PointsFinal[0]], offset=position, lineStyle=self.lineStyleARM) + arms2 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal2[0], PointsFinal2[0]], offset=position, lineStyle=self.lineStyleARM) + arms3 = inkDraw.line.absCoords(wheelGroup, coordsList=[CentersFinal3[0], PointsFinal3[0]], offset=position, lineStyle=self.lineStyleARM) + + arc1 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=40, angEnd=80, offset=position, + lineStyle=self.lineStyleArrow) + arc2 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=160, angEnd=200, + offset=position, lineStyle=self.lineStyleArrow) + arc3 = inkDraw.arc.centerAngStartAngEnd(wheelGroup, centerPoint=CentersFinal[0], radius=r * 0.6, angStart=280, angEnd=320, + offset=position, lineStyle=self.lineStyleArrow) + + self.exportSVG(animGroup, os.path.join(so.directory,'outSVG_%1.5d.svg' % 0)) + + for i in range(1, len(thetasFinal)): + + self.moveElement(wheelGroup, [CentersFinal[i][0] - CentersFinal[i - 1][0], CentersFinal[i][1] - CentersFinal[i - 1][1]]) + if typeCurve == 'hypo': + self.rotateElement(wheelGroup, [position[0] + CentersFinal[i][0], position[1] + CentersFinal[i][1]], + (thetasFinal[i] - thetasFinal[i - 1]) * (R - r) / r * 180 / np.pi) + else: + self.rotateElement(wheelGroup, [position[0] + CentersFinal[i][0], position[1] + CentersFinal[i][1]], + - (thetasFinal[i] - thetasFinal[i - 1]) * (R + r) / r * 180 / np.pi) + + curve1 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal[:i + 1], offset=position, lineStyle=self.lineStyleCurve, + closePath=False) + curve2 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal2[:i + 1], offset=position, lineStyle=self.lineStyleCurve2, + closePath=False) + curve3 = inkDraw.line.absCoords(parent=animGroup, coordsList=PointsFinal3[:i + 1], offset=position, lineStyle=self.lineStyleCurve3, + closePath=False) + + self.exportSVG(animGroup, os.path.join(so.directory , 'outSVG_%1.5d.svg' % i)) + + self.removeElement(curve1) + self.removeElement(curve2) + self.removeElement(curve3) + self.removeElement(animGroup) + else: + if so.drawBaseCircles: + inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=R, offset=[0, 0], lineStyle=self.constructionLine) + + if typeCurve == 'hypo': + inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=r, offset=[R - r, 0], lineStyle=self.constructionLine) + if typeCurve == 'epi': + inkDraw.circle.centerRadius(parent=group, centerPoint=position, radius=r, offset=[R + r, 0], lineStyle=self.constructionLine) + + inkDraw.line.absCoords(group, PointsFinal, position, 'spiro', self.lineStyleCurve, closePath=True) + + # plot curvatures + if False: + inkPlot.plot.polar(self, group, curvatureFinal, thetasFinal * 180 / np.pi, [position[0] + 3 * R, position[1]], rTicks=False, + tTicks=False, rTickStep=0.2, tTickStep=45.0, rScale=20, rGrid=True, tGrid=True, lineStylePlot=self.lineStyleCurve, + forceRlim=[0.0, 1.0]) + + return + + # typeCurve: 'hypo', 'epi' + def calcCurve__trochoid(self, typeCurve, R, r, d, thetas): + j = complex(0, 1) + if typeCurve.lower() == 'hypo': + # https://www.mathcurve.com/courbes2d.gb/hypotrochoid/hypotrochoid.shtml + P_complex = (R - r) * np.exp(j * thetas) + d * np.exp(-j * thetas * (R - r) / r) + dP_complex = (R - r) * j * np.exp(j * thetas) + d * (-j) * (R - r) / r * np.exp(-j * thetas * (R - r) / r) + ddP_complex = (R - r) * (-1) * np.exp(j * thetas) + d * (-1) * ((R - r) / r) ** 2 * np.exp(-j * thetas * (R - r) / r) + centerGear = (R - r) * np.exp(j * thetas) + if typeCurve.lower() == 'epi': + # https://www.mathcurve.com/courbes2d.gb/epitrochoid/epitrochoid.shtml + P_complex = (R + r) * np.exp(j * thetas) - d * np.exp(j * thetas * (R + r) / r) + dP_complex = (R + r) * j * np.exp(j * thetas) - d * j * (R + r) / r * np.exp(j * thetas * (R + r) / r) + ddP_complex = (R + r) * (-1) * np.exp(j * thetas) - d * (-1) * ((R + r) / r) ** 2 * np.exp(j * thetas * (R + r) / r) + centerGear = (R + r) * np.exp(j * thetas) + + with np.errstate(divide='ignore', invalid='ignore'): + curvature = np.divide(abs(dP_complex.real * ddP_complex.imag - dP_complex.imag * ddP_complex.real), + (dP_complex.real ** 2 + dP_complex.imag ** 2) ** (2 / 3)) + + # remove Nan=0/0 + np.nan_to_num(curvature, copy=False) + + # remove values too large + curvature[curvature > 10 * np.mean(curvature)] = 0.0 + + # self.Dump(curvature, '/home/fernando/lixo.txt', 'w') + # normalize curvature + curvature = self._normalizeCurvatures(curvature, 0.0, 1.0) + + Points = np.column_stack((P_complex.real, P_complex.imag)) + Centers = np.column_stack((centerGear.real, centerGear.imag)) + + return [Points, Centers, curvature] + + def _normalizeCurvatures(self, curvatures, normMin=0.0, normMax=1.0): + y1 = normMin + y2 = normMax + x1 = np.min(curvatures) + x2 = np.max(curvatures) + alpha = (y2 - y1) / (x2 - x1) + return alpha * (curvatures - x1) + y1 + + +if __name__ == '__main__': + sp = Spirograph() + sp.run() diff --git a/extensions/fablabchemnitz/styles_to_layers.py b/extensions/fablabchemnitz/styles_to_layers.py index 0f1d7126..67827e26 100644 --- a/extensions/fablabchemnitz/styles_to_layers.py +++ b/extensions/fablabchemnitz/styles_to_layers.py @@ -23,7 +23,7 @@ class StylesToLayers(inkex.EffectExtension): def findLayer(self, layerName): svg_layers = self.document.xpath('//svg:g[@inkscape:groupmode="layer"]', namespaces=inkex.NSS) for layer in svg_layers: - #inkex.utils.debug(str(layer.get('inkscape:label')) + " == " + layerName) + #self.msg(str(layer.get('inkscape:label')) + " == " + layerName) if layer.get('inkscape:label') == layerName: return layer return None @@ -31,7 +31,7 @@ class StylesToLayers(inkex.EffectExtension): def createLayer(self, layerNodeList, layerName): svg = self.document.xpath('//svg:svg',namespaces=inkex.NSS)[0] for layer in layerNodeList: - #inkex.utils.debug(str(layer[0].get('inkscape:label')) + " == " + layerName) + #self.msg(str(layer[0].get('inkscape:label')) + " == " + layerName) if layer[0].get('inkscape:label') == layerName: return layer[0] #already exists. Do not create duplicate layer = etree.SubElement(svg, 'g') @@ -68,8 +68,8 @@ class StylesToLayers(inkex.EffectExtension): import applytransform applyTransformAvailable = True except Exception as e: - # inkex.utils.debug(e) - inkex.utils.debug("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...") + # self.msg(e) + self.msg("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...") layer_name = None layerNodeList = [] #list with layer, neutral_value, element and self.options.separateby type @@ -175,12 +175,11 @@ class StylesToLayers(inkex.EffectExtension): layer_name = "fill-opacity-none" else: - inkex.utils.debug("No proper option selected.") + self.msg("No proper option selected.") exit(1) - + if neutral_value is not None: #apply decimals filter neutral_value = float(round(neutral_value, self.options.decimals)) - if layer_name is not None: layer_name = layer_name.split(";")[0] #cut off existing semicolons to avoid duplicated layers with/without semicolon currentLayer = self.findLayer(layer_name) @@ -188,13 +187,16 @@ class StylesToLayers(inkex.EffectExtension): layerNodeList.append([self.createLayer(layerNodeList, layer_name), neutral_value, element, self.options.separateby]) else: layerNodeList.append([currentLayer, neutral_value, element, self.options.separateby]) #layer is existent. append items to this later + elif layer_name is None and self.options.put_unfiltered: + layer_name = 'without-' + self.options.separateby + '-in-style-attribute' else: #if no style attribute in element and not a group if isinstance(element, inkex.Group) is False: if self.options.show_info: - inkex.utils.debug(element.get('id') + ' has no style attribute') + self.msg(element.get('id') + ' has no style attribute') if self.options.put_unfiltered: layer_name = 'without-style-attribute' currentLayer = self.findLayer(layer_name) + if currentLayer is None: #layer does not exist, so create a new one layerNodeList.append([self.createLayer(layerNodeList, layer_name), None, element, None]) else: @@ -235,11 +237,11 @@ class StylesToLayers(inkex.EffectExtension): maxval = max(minmax_range) sliceinterval = (maxval - minval) / self.options.subdividethreshold - #inkex.utils.debug("neutral values (sorted) = " + str(minmax_range)) - #inkex.utils.debug("min neutral_value = " + str(minval)) - #inkex.utils.debug("max neutral_value = " + str(maxval)) - #inkex.utils.debug("slice value (divide step size) = " + str(sliceinterval)) - #inkex.utils.debug("subdivides (parent layers) = " + str(self.options.subdividethreshold)) + #self.msg("neutral values (sorted) = " + str(minmax_range)) + #self.msg("min neutral_value = " + str(minval)) + #self.msg("max neutral_value = " + str(maxval)) + #self.msg("slice value (divide step size) = " + str(sliceinterval)) + #self.msg("subdivides (parent layers) = " + str(self.options.subdividethreshold)) for layerNode in layerNodeList: for x in range(0, self.options.subdividethreshold): #loop through the sorted neutral_values and determine to which layer they should belong @@ -276,8 +278,8 @@ class StylesToLayers(inkex.EffectExtension): #finally append the sublayers to the slices #for layer in topLevelLayerNodeList: - #inkex.utils.debug(layer[0].get('inkscape:label')) - #inkex.utils.debug(layer[1]) + #self.msg(layer[0].get('inkscape:label')) + #self.msg(layer[1]) for newLayerNode in topLevelLayerNodeList: newLayerNode[0].append(newLayerNode[1]) #append newlayer to layer @@ -286,7 +288,7 @@ class StylesToLayers(inkex.EffectExtension): import cleangroups cleangroups.CleanGroups.effect(self) except: - inkex.utils.debug("Calling 'Remove Empty Groups' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...") + self.msg("Calling 'Remove Empty Groups' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...") if __name__ == '__main__': StylesToLayers().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/vpypetools/vpypetools.py b/extensions/fablabchemnitz/vpypetools/vpypetools.py index b037cf4f..82e33365 100644 --- a/extensions/fablabchemnitz/vpypetools/vpypetools.py +++ b/extensions/fablabchemnitz/vpypetools/vpypetools.py @@ -30,7 +30,7 @@ Extension for InkScape 1.X Author: Mario Voigt / FabLab Chemnitz Mail: mario.voigt@stadtfabrikanten.org Date: 02.04.2021 -Last patch: 08.04.2021 +Last patch: 24.04.2021 License: GNU GPL v3 This piece of spaghetti-code, called "vpypetools", is a wrapper to pass (pipe) line elements from InkScape selection (or complete canvas) to vpype. @@ -180,7 +180,7 @@ class vpypetools (inkex.EffectExtension): for csp in subPath: if len(csp[1]) > 0: #we need exactly two points per straight line segment points.append(Point(round(csp[1][0], self.options.decimals), round(csp[1][1], self.options.decimals))) - if subPath[-1][0] == 'Z' or subPath[0][1] == subPath[-1][1]: #check if path is closed by Z or first pont == last point + if len(subPath) > 2 and (subPath[-1][0] == 'Z' or subPath[0][1] == subPath[-1][1]): #check if path has more than 2 points and is closed by Z or first pont == last point points.append(Point(round(subPath[0][1][0], self.options.decimals), round(subPath[0][1][1], self.options.decimals))) #if closed, we add the first point again lc.append(LineString(points)) @@ -365,7 +365,7 @@ class vpypetools (inkex.EffectExtension): # convert vpype polylines/lines/polygons to regular paths again. We need to use "--with-gui" to respond to "WARNING: ignoring verb FileSave - GUI required for this verb." if self.options.strokes_to_paths is True: - cli_output = inkscape(output_file, "--with-gui", actions="EditSelectAllInAllLayers;EditUnlinkClone;ObjectToPath;FileSave;FileQuit") + cli_output = inkscape(output_file, "--with-gui", actions="EditSelectAllInAllLayers;EditUnlinkClone;ObjectToPath;FileSave;FileQuit") #we do not use StrokeToPath because it will convert svg:line to a svg:path, but as closed path with four points if len(cli_output) > 0: self.debug(_("Inkscape returned the following output when trying to run the vpype object to path back-conversion:")) self.debug(cli_output) @@ -436,7 +436,7 @@ class vpypetools (inkex.EffectExtension): if self.options.input_handling == "layers": if self_viewBox is not None: element.set('transform', 'scale(' + str(scaleX) + ',' + str(scaleY) + ')') #imported groups need to be transformed. Or they have wrong size. Reason: different viewBox sizes/units in namedview definitions - if self.options.apply_transformations is True and applyTransformAvailable is True: #we apply the transforms directly after adding them + if self.options.apply_transformations is True and applyTransformAvailable is True and self.options.strokes_to_paths is True: #we apply the transforms directly after adding them, but we need to have strokes_to_paths enabled! applytransform.ApplyTransform().recursiveFuseTransform(element) # Delete the temporary file again because we do not need it anymore