Advertisement
oliverthered

shape recognition enhanced mk 2

Aug 19th, 2020
207
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 69.00 KB | None | 0 0
  1. #!/usr/bin/env python
  2. '''
  3. Copyright (C) 2017 , Pierre-Antoine Delsart
  4.  
  5. This file is part of InkscapeShapeReco.
  6.  
  7. InkscapeShapeReco is free software; you can redistribute it and/or modify
  8. it under the terms of the GNU General Public License as published by
  9. the Free Software Foundation; either version 3 of the License, or
  10. (at your option) any later version.
  11.  
  12. This program is distributed in the hope that it will be useful,
  13. but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15. GNU General Public License for more details.
  16.  
  17. You should have received a copy of the GNU General Public License
  18. along with InkscapeShapeReco; if not, write to the Free Software
  19. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  20.  
  21.  
  22.  
  23. Quick description:
  24. This extension uses all selected path, ignoring all other selected objects.
  25. It tries to regularize hand drawn paths BY :
  26. - evaluating if the path is a full circle or ellipse
  27. - else finding sequences of aligned points and replacing them by a simple segment.
  28. - changing the segments angles to the closest remarkable angle (pi/2, pi/3, pi/6, etc...)
  29. - eqalizing all segments lengths which are close to each other
  30. - replacing 4 segments paths by a rectangle object if this makes sens (giving the correct rotation to the rectangle).
  31.  
  32. Requires numpy.
  33.  
  34. '''
  35.  
  36. import sys
  37. sys.path.append('/usr/share/inkscape/extensions')
  38. import inkex
  39. import simplepath
  40. import gettext
  41. _ = gettext.gettext
  42.  
  43.  
  44.  
  45.  
  46. import numpy
  47. numpy.set_printoptions(precision=3)
  48. # *************************************************************
  49. # a list of geometric helper functions
  50. def toArray(parsedList):
  51.     """Interprets a list of [(command, args),...]
  52.    where command is a letter coding for a svg path command
  53.          args are the argument of the command
  54.    """
  55.     interpretCommand = {
  56.         'C' : lambda x, prevL : x[-2:], # bezier curve. Ignore the curve.
  57.         'L' : lambda x, prevL : x[0:2],
  58.         'M' : lambda x, prevL : x[0:2],
  59.         'Z' : lambda x, prevL : prevL[0],
  60.         }
  61.  
  62.     points =[]
  63.     for i,(c, arg) in enumerate(parsedList):
  64.         #debug('toArray ', i, c , arg)
  65.         newp = interpretCommand[c](arg, points)
  66.         points.append( newp)
  67.     a=numpy.array( points )
  68.  
  69.     # Some times we have points *very* close to each other
  70.     # these do not bring any meaning full info, so we remove them
  71.     #
  72.     x,y, w,h = computeBox(a)
  73.     sizeC = 0.5*(w+h)
  74.     #deltas = numpy.zeros((len(a),2) )
  75.     deltas = a[1:] - a[:-1]
  76.     #deltas[-1] = a[0] - a[-1]
  77.     deltaD = numpy.sqrt(numpy.sum( deltas**2, 1 ))
  78.     sortedDind = numpy.argsort(deltaD)
  79.     # expand longuest segments
  80.     nexp = int(len(deltaD)*0.9)
  81.     newpoints=[ None ]*len(a)
  82.     medDelta = deltaD[sortedDind[len(deltaD)/2] ]
  83.     for i,ind in enumerate(sortedDind):
  84.         if deltaD[ind]/sizeC<0.005: continue
  85.         if i>nexp:
  86.             np = int(deltaD[ind]/medDelta)
  87.             pL = [a[ind]]
  88.             #print i,'=',ind,'adding ', np,'  _ ', deltaD[ind], a[ind], a[ind+1]
  89.             for j in range(np-1):
  90.                 f = float(j+1)/np
  91.                 #print '------> ', (1-f)*a[ind]+f*a[ind+1]
  92.                 pL.append( (1-f)*a[ind]+f*a[ind+1] )
  93.             newpoints[ind] = pL
  94.         else:
  95.             newpoints[ind]=[a[ind]]
  96.     if(D(a[0],a[-1])/sizeC > 0.005 ) :
  97.         newpoints[-1]=[a[-1]]
  98.  
  99.     points = numpy.concatenate([p for p in newpoints if p!=None] )
  100.     ## print ' medDelta ', medDelta, deltaD[sortedDind[-1]]
  101.     ## print len(a) ,' ------> ', len(points)
  102.  
  103.     rel_norms = numpy.sqrt(numpy.sum( deltas**2, 1 )) / sizeC
  104.     keep = numpy.concatenate([numpy.where( rel_norms >0.005 )[0],numpy.array([len(a)-1])])
  105.  
  106.     #return a[keep] , [ parsedList[i] for i in keep]
  107.     #print len(a),' ',len(points)
  108.     return points , []
  109.  
  110. rotMat = numpy.matrix( [[1,-1],[1,1]] )/numpy.sqrt(2)
  111. unrotMat = numpy.matrix( [[1,1],[-1,1]] )/numpy.sqrt(2)
  112.  
  113. def setupKnownAngles():
  114.     pi = numpy.pi
  115.     #l = [ i*pi/8 for i in range(0, 9)] +[ i*pi/6 for i in [1,2,4,5,] ]
  116.     l = [ i*pi/8 for i in range(0, 9)] +[ i*pi/6 for i in [1,2,4,5,] ] + [i*pi/12 for i in (1,5,7,11)]
  117.     knownAngle = numpy.array( l )
  118.     return numpy.concatenate( [-knownAngle[:0:-1], knownAngle ])
  119. knownAngle = setupKnownAngles()
  120.  
  121. _twopi =  2*numpy.pi
  122. _pi = numpy.pi
  123.  
  124. def deltaAngle(a1,a2):
  125.     d = a1 - a2
  126.     return d if d > -_pi else d+_twopi
  127.  
  128. def closeAngleAbs(a1,a2):
  129.     d = abs(a1 - a2 )
  130.     return min( abs(d-_pi), abs( _twopi - d) , d)
  131.  
  132. def deltaAngleAbs(a1,a2):
  133.     return abs(in_mPi_pPi(a1 - a2 ))
  134.  
  135. def in_mPi_pPi(a):
  136.     if(a>_pi): return a-_twopi
  137.     if(a<-_pi): return a+_twopi
  138.     return a
  139. vec_in_mPi_pPi = numpy.vectorize(in_mPi_pPi)
  140. from numpy import sqrt
  141.  
  142. def D2(p1, p2):
  143.     return ((p1-p2)**2).sum()
  144.  
  145. def D(p1, p2):
  146.     return sqrt(D2(p1,p2) )
  147.  
  148. def norm(p):
  149.     return sqrt( (p**2).sum() )
  150.  
  151. def computeBox(a):
  152.     """returns the bounding box enclosing the array of points a
  153.    in the form (x,y, width, height) """
  154.     xmin , ymin = a[:,0].min(), a[:,1].min()
  155.     xmax , ymax = a[:,0].max(), a[:,1].max()
  156.  
  157.     return xmin, ymin, xmax-xmin, ymax-ymin
  158.  
  159. def dirAndLength(p1,p2):
  160.     #l = max(D(p1, p2) ,1e-4)
  161.     l = D(p1,p2)
  162.     uv = (p1-p2)/l
  163.     return l,uv
  164.  
  165. def length(p1,p2):
  166.     return sqrt( D2(p1,p2) )
  167.  
  168. def barycenter(points):
  169.     """
  170.    """
  171.     return points.sum(axis=0)/len(points)
  172.  
  173.  
  174. # *************************************************************
  175. # debugging
  176. def void(*l):
  177.     pass
  178. def debug_on(*l):
  179.     sys.stderr.write(' '.join(str(i) for i in l) +'\n')
  180. debug = void
  181. #debug = debug_on
  182.  
  183. # *************************************************************
  184. # Internal Objects
  185. class Path(object):
  186.     """Private representation of a sequence of points.
  187.    A SVG node of type 'path' is splitted in several of these Path objects.
  188.    """
  189.     next = None # next Path in the sequence of path corresponding to a SVG node
  190.     prev = None # previous Path in the sequence of path corresponding to a SVG node
  191.     sourcepoints = None  # the full list of points from which this path is a subset
  192.  
  193.     normalv = None # normal vector to this Path
  194.    
  195.     def __init__(self, points):
  196.         """points an array of points """
  197.         self.points = points
  198.         self.init()
  199.  
  200.     def init(self):
  201.         self.effectiveNPoints = len(self.points)
  202.         if self.effectiveNPoints>1:
  203.             self.length , self.univ = dirAndLength(self.points[0], self.points[-1])
  204.         else:
  205.             self.length , self.univ = 0, numpy.array([0,0])
  206.         if self.effectiveNPoints>0:
  207.             self.pointN=self.points[-1]
  208.             self.point1=self.points[0]
  209.            
  210.     def isSegment(self):
  211.         return False
  212.  
  213.     def quality(self):
  214.         return 1000        
  215.  
  216.     def dump(self):
  217.         n = len(self.points)
  218.         if n>0:
  219.             return 'path at '+str(self.points[0])+ ' to '+ str(self.points[-1])+'    npoints=%d / %d (eff)'%(n,self.effectiveNPoints)
  220.         else:
  221.             return 'path Void !'
  222.  
  223.     def setNewLength(self, l):
  224.         self.newLength = l
  225.        
  226.     def removeLastPoints(self,n):
  227.         self.points = self.points[:-n]
  228.         self.init()
  229.     def removeFirstPoints(self,n):
  230.         self.points = self.points[n:]
  231.         self.init()
  232.  
  233.     def costheta(self,seg):
  234.         return self.unitv.dot(seg.unitv)
  235.  
  236.     def translate(self, tr):
  237.         """Translate this path by tr"""
  238.         self.points = self.points + tr
  239.  
  240.     def asSVGCommand(self, firstP=False):
  241.         svgCommands = []
  242.         com = 'M' if firstP else 'L'
  243.         for p in self.points:
  244.             svgCommands.append( [com, [p[0], p[1]] ] )
  245.             com='L'
  246.         return svgCommands
  247.  
  248.  
  249.     def setIntersectWithNext(self, next=None):
  250.         pass
  251.  
  252.     def mergedWithNext(self, newPath=None):
  253.         """ Returns the combination of self and self.next.
  254.        sourcepoints has to be set
  255.        """
  256.         if newPath is None: newPath = Path( numpy.concatenate([self.points, self.next.points]) )
  257.  
  258.         newPath.sourcepoints = self.sourcepoints
  259.         newPath.prev = self.prev
  260.         if self.prev : newPath.prev.next = newPath
  261.         newPath.next = self.next.next
  262.         if newPath.next:
  263.             newPath.next.prev = newPath
  264.         return newPath
  265.  
  266. # *************************************************************
  267. #    
  268. class Segment(Path):
  269.     """ A segment. Defined by its line equation ax+by+c=0 and the points from orignal paths
  270.    it is ensured that a**2+b**2 = 1
  271.    """
  272.     QUALITYCUT = 0.9
  273.    
  274.     newAngle    = None # temporary angle set during the "parralelization" step
  275.     newLength = None   # temporary lenght set during the "parralelization" step
  276.  
  277.     # Segment Builders
  278.     @staticmethod
  279.     def from2Points( p1, p2, refPoints = None):
  280.         dirV = p2-p1
  281.         center = 0.5*(p2+p1)
  282.         return Segment.fromCenterAndDir(center, dirV, refPoints)
  283.  
  284.     @staticmethod
  285.     def fromCenterAndDir( center, dirV, refPoints=None):
  286.         b = dirV[0]
  287.         a = -dirV[1]
  288.         c = - (a*center[0]+b*center[1])
  289.  
  290.         if refPoints is None:
  291.             refPoints = numpy.array([ center-0.5*dirV, center+0.5*dirV] )
  292.         s = Segment( a, b, c,  refPoints)
  293.         return s
  294.  
  295.    
  296.     def __init__(self, a,b,c, points, doinit=True):
  297.         """a,b,c: the line parameters.
  298.        points : the array of 2D points represented by this Segment
  299.        doinit : if true will compute additionnal parameters to this Segment (first/last points, unit vector,...)
  300.        """
  301.         self.a = a
  302.         self.b = b
  303.         self.c = c
  304.        
  305.         self.points = points
  306.         d = numpy.sqrt(a**2+b**2)
  307.         if d != 1. :
  308.             self.a /= d
  309.             self.b /= d
  310.             self.c /= d
  311.  
  312.         if doinit :
  313.             self.init()
  314.  
  315.  
  316.     def init(self):
  317.         a,b,c = self.a, self.b, self.c
  318.         x,y = self.points[0]
  319.         self.point1 = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
  320.         x,y = self.points[-1]
  321.         self.pointN = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
  322.         uv = self.computeDirLength()
  323.         self.distancesToLine =  self.computeDistancesToLine(self.points)
  324.         self.normalv = numpy.array( [ a, b ])
  325.  
  326.         self.angle = numpy.arccos( uv[0] )*numpy.sign(uv[1] )
  327.  
  328.  
  329.     def computeDirLength(self):
  330.         """re-compute and set unit vector and length """
  331.         self.length , uv = dirAndLength(self.pointN, self.point1)
  332.         self.unitv = uv
  333.         return uv
  334.  
  335.     def isSegment(self):
  336.         return True
  337.  
  338.     def recomputeEndPoints(self):
  339.         a,b,c = self.a, self.b, self.c
  340.         x,y = self.points[0]
  341.         self.point1 = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
  342.         x,y = self.points[-1]
  343.        
  344.         self.pointN = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
  345.  
  346.         self.length = numpy.sqrt( D2(self.pointN, self.point1) )
  347.  
  348.     def projectPoint(self,p):
  349.         """ return the point projection of p onto this segment"""
  350.         a,b,c = self.a, self.b, self.c
  351.         x,y = p
  352.         return numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )        
  353.        
  354.  
  355.     def intersect(self, seg):
  356.         """Returns the intersection of this line with the line seg"""
  357.         nu, nv = self.normalv, seg.normalv
  358.         u = numpy.array([[-self.c],[-seg.c]])
  359.         doRotation = min(nu.min(),nv.min()) <1e-4
  360.         if doRotation:
  361.             # rotate to avoid numerical issues
  362.             nu = numpy.array(rotMat.dot(nu))[0]
  363.             nv = numpy.array(rotMat.dot(nv))[0]
  364.         m = numpy.matrix( (nu, nv) )        
  365.  
  366.         i =  (m**-1).dot(u)
  367.         i=numpy.array( i).swapaxes(0,1)[0]
  368.         debug('  intersection ' ,nu, nv, self.angle, seg.angle, ' --> ',i)
  369.         if doRotation:
  370.             i = unrotMat.dot(i).A1
  371.         debug('   ' ,i)
  372.        
  373.        
  374.         return i
  375.  
  376.     def setIntersectWithNext(self, next=None):
  377.         """Modify self such as self.pointN is the intersection with next segment """
  378.         if next is None:
  379.             next = self.next
  380.         if next and next.isSegment():
  381.             if abs(self.normalv.dot(next.unitv)) < 1e-3:
  382.                 return
  383.             debug(' Intersect',self, next,  ' from ', self.point1, self.pointN, ' to ' ,next.point1, next.pointN,)
  384.             inter = self.intersect(next)
  385.             debug('  --> ', inter, '  d=', D(self.pointN, inter) )
  386.             next.point1 = inter
  387.             self.pointN = inter
  388.             self.computeDirLength()
  389.             next.computeDirLength()
  390.            
  391.     def computeDistancesToLine(self, points):
  392.         """points: array of points.
  393.        returns the array of distances to this segment"""
  394.         return abs(self.a*points[:,0]+self.b*points[:,1]+self.c)
  395.  
  396.  
  397.     def distanceTo(self,point):
  398.         return abs(self.a*point[0]+self.b*point[1]+self.c)        
  399.  
  400.     def inverse(self):
  401.         """swap all x and y values.  """
  402.         def inv(v):
  403.             v[0], v[1] = v[1] , v[0]
  404.         for v in [self.point1 , self.pointN , self.unitv, self.normalv]:
  405.             inv(v)
  406.  
  407.         self.points = numpy.roll(self.points,1,axis=1)
  408.         self.a, self.b = self.b, self.a
  409.         self.angle = numpy.arccos( self.unitv[0] )*numpy.sign(self.unitv[1] )
  410.         return
  411.  
  412.     def dumpShort(self):
  413.         return 'seg  '+'  '+str(self.point1 )+'to '+str(self.pointN)+ ' npoints=%d | angle,offset=(%.2f,%.2f )'%(len(self.points),self.angle, self.c)+'  ',self.normalv
  414.  
  415.     def dump(self):
  416.         v = self.variance()
  417.         n = len(self.points)
  418.         return 'seg  '+str(self.point1 )+' , '+str(self.pointN)+ '  v/l=%.2f / %.2f = %.2f  r*sqrt(n)=%.2f  npoints=%d | angle,offset=(%.2f,%.2f )'%(v, self.length, v/self.length,v/self.length*numpy.sqrt(n) ,n  , self.angle, self.c)
  419.        
  420.     def variance(self):
  421.         d = self.distancesToLine
  422.         return numpy.sqrt( (d**2).sum()/len(d) )
  423.  
  424.     def quality(self):
  425.         n = len(self.points)
  426.         return min(self.variance()/self.length*numpy.sqrt(n) , 1000)
  427.  
  428.     def formatedSegment(self, firstP=False):
  429.         return self.asSVGCommand(firstP)
  430.    
  431.     def asSVGCommand(self, firstP=False):
  432.  
  433.         if firstP:            
  434.             segment = [ ['M',[self.point1[0],self.point1[1] ] ],
  435.                         ['L',[self.pointN[0],self.pointN[1] ] ]
  436.                         ]
  437.         else:
  438.             segment = [ ['L',[self.pointN[0],self.pointN[1] ] ] ]
  439.         #debug("Segment, format : ", segment)
  440.         return segment
  441.        
  442.     def replaceInList(self, startPos, fullList):
  443.         code0 = fullList[startPos][0]
  444.         segment = [ [code0,[self.point1[0],self.point1[1] ] ],
  445.                      ['L',[self.pointN[0],self.pointN[1] ] ]
  446.                     ]
  447.         l = fullList[:startPos]+segment+fullList[startPos+len(self.points):]
  448.         return l
  449.  
  450.  
  451.  
  452.  
  453.     def mergedWithNext(self, doRefit=True):
  454.         """ Returns the combination of self and self.next.
  455.        sourcepoints has to be set
  456.        """
  457.         spoints = numpy.concatenate([self.points,self.next.points])
  458.  
  459.         if doRefit:
  460.             newSeg = fitSingleSegment(spoints)
  461.         else:
  462.             newSeg = Segment.fromCenterAndDir(barycenter(spoints), self.unitv, spoints)
  463.        
  464.         newSeg = Path.mergedWithNext(self, newSeg)
  465.         return newSeg
  466.  
  467.    
  468.  
  469.     def center(self):
  470.         return 0.5*(self.point1+self.pointN)
  471.  
  472.     def box(self):
  473.         return computeBox(self.points)
  474.  
  475.  
  476.     def translate(self, tr):
  477.         """Translate this segment by tr """
  478.         c = self.c -self.a*tr[0] -self.b*tr[1]
  479.         self.c =c
  480.         self.pointN = self.pointN+tr
  481.         self.point1 = self.point1+tr
  482.         self.points +=tr
  483.        
  484.     def adjustToNewAngle(self):        
  485.         """reset all parameters so that self.angle is change to self.newAngle """
  486.  
  487.         self.a,self.b,self.c = parametersFromPointAngle( 0.5*(self.point1+self.pointN), self.newAngle)
  488.  
  489.         #print 'adjustToNewAngle ', self, self.angle, self.newAngle
  490.         self.angle = self.newAngle
  491.         self.normalv = numpy.array( [ self.a, self.b ])
  492.         self.unitv = numpy.array( [ self.b, -self.a ])
  493.         if abs(self.angle) > numpy.pi/2 :
  494.             if self.b > 0: self.unitv *= -1
  495.         elif self.b<0 : self.unitv  *= -1
  496.  
  497.         self.point1 = self.projectPoint(self.point1) # reset point1
  498.         if self.next is None or not self.next.isSegment():
  499.                 # move the last point (no intersect with next)
  500.  
  501.                 pN = self.projectPoint(self.pointN)
  502.                 dirN = pN - self.point1                
  503.                 lN = length(pN, self.point1)
  504.                 self.pointN = dirN/lN*self.length + self.point1
  505.                 #print ' ... adjusting last seg angle ',p.dump() , ' normalv=', p.normalv, 'unitv ', p.unitv
  506.         else:
  507.             self.setIntersectWithNext()
  508.  
  509.     def adjustToNewDistance(self):
  510.         self.pointN = self.newLength* self.unitv + self.point1
  511.         self.length = self.newLength
  512.  
  513.     def tempLength(self):
  514.         if self.newLength : return self.newLength
  515.         else : return self.length
  516.  
  517.     def tempAngle(self):
  518.         if self.newAngle: return self.newAngle
  519.         return self.angle
  520.  
  521.  
  522.  
  523.  
  524. # *************************************************************
  525. # *************************************************************
  526. # Groups of Path
  527. #
  528. class PathGroup(object):
  529.     """A group of Path representing one SVG node.
  530.     - a list of Path
  531.     - a list of SVG commands describe the full node (=SVG path element)
  532.     - a reference to the inkscape node object
  533.    
  534.    """
  535.     listOfPaths = []
  536.     refSVGPathList = []
  537.     isClosing = False
  538.     refNode = None
  539.    
  540.     def __init__(self, listOfPaths, refSVGPathList, refNode=None, isClosing=False):
  541.         self.refNode = refNode
  542.         self.listOfPaths = listOfPaths
  543.         self.refSVGPathList = refSVGPathList
  544.         self.isClosing=isClosing
  545.        
  546.     def addToNode(self, node):
  547.         newList = reformatList( self.listOfPaths)        
  548.         ele = addPath( newList , node)
  549.         debug("PathGroup ", newList)
  550.         return ele
  551.  
  552.     def setNodeStyle(self,ele, node):
  553.         style = node.get('style')
  554.         ele.set('style', style)
  555.        
  556.  
  557.  
  558.     @staticmethod
  559.     def toSegments(points, refSVGPathList, refNode, isClosing=False):
  560.         """
  561.        """
  562.         segs = [ Segment.from2Points(p, points[i+1], points[i:i+2] ) for (i,p) in enumerate(points[:-1]) ]
  563.         resetPrevNextSegment(segs)
  564.         return PathGroup( segs, refSVGPathList, refNode , isClosing)
  565.  
  566. class TangentEnvelop(PathGroup):
  567.     """Specialization where the Path objects are all Segments and represent tangents to a curve """
  568.     def addToNode(self, node):
  569.         newList = [ ]
  570.         for s in self.listOfPaths:
  571.             newList += s.asSVGCommand(firstP=True)
  572.         debug("TangentEnvelop ", newList)
  573.         ele = addPath( newList , node)
  574.         return ele
  575.  
  576.     def setNodeStyle(self,ele, node):
  577.         style = node.get('style')+';marker-end:url(#Arrow1Lend)'
  578.         style
  579.         ele.set('style', style)
  580.  
  581.  
  582. class Circle(PathGroup):
  583.     """Specialization where the list of Path objects
  584.    is to be replaced by a Circle specified by a center and a radius.
  585.  
  586.    If an other radius 'rmax' is given than the object represents an ellipse.
  587.    """
  588.     isClosing= True
  589.     def __init__(self, center, rad,  refNode=None, rmax=None, angle=0.):
  590.         self.listOfPaths = []
  591.         self.refNode = refNode
  592.         self.center = numpy.array(center)
  593.         self.radius = rad
  594.         if rmax:
  595.             self.type ='ellipse'
  596.         else:
  597.             self.type = 'circle'
  598.         self.rmax = rmax
  599.         self.angle = angle
  600.        
  601.     def addToNode(self, refnode):
  602.         """Add a node in the xml structure corresponding to this rect
  603.        refnode : xml node used as a reference, new point will be inserted a same level"""
  604.         ele = inkex.etree.Element('{http://www.w3.org/2000/svg}'+self.type)
  605.  
  606.         ele.set('cx',str(self.center[0]))
  607.         ele.set('cy',str(self.center[1]))
  608.         if self.rmax:
  609.             ele.set('ry',str(self.radius))
  610.             ele.set('rx',str(self.rmax))
  611.             ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle),self.center[0],self.center[1]))
  612.         else:
  613.             ele.set('r',str(self.radius))
  614.         refnode.xpath('..')[0].append(ele)
  615.         return ele
  616.  
  617.    
  618. class Rectangle(PathGroup):
  619.     """Specialization where the list of Path objects
  620.    is to be replaced by a Rectangle specified by a center and size (w,h) and a rotation angle.
  621.  
  622.    """
  623.     def __init__(self, center, size, angle, listOfPaths, refNode=None):
  624.         self.listOfPaths = listOfPaths
  625.         self.refNode = refNode
  626.         self.center = center
  627.         self.size = size
  628.         self.bbox = size
  629.         self.angle = angle
  630.         pos = self.center - numpy.array( size )/2
  631.         if angle != 0. :
  632.             cosa = numpy.cos(angle)
  633.             sina = numpy.sin(angle)            
  634.             self.rotMat = numpy.matrix( [ [ cosa, sina], [-sina, cosa] ] )
  635.             self.rotMatstr = 'matrix(%1.7f,%1.7f,%1.7f,%1.7f,0,0)'%(cosa, sina, -sina, cosa)
  636.  
  637.  
  638.             #debug(' !!!!! Rotated rectangle !!', self.size, self.bbox,  ' angles ', a, self.angle ,' center',self.center)
  639.         else :
  640.             self.rotMatstr = None
  641.         self.pos = pos
  642.         debug(' !!!!! Rectangle !!', self.size, self.bbox,  ' angles ', self.angle ,' center',self.center)
  643.  
  644.     def addToNode(self, refnode):
  645.         """Add a node in the xml structure corresponding to this rect
  646.        refnode : xml node used as a reference, new point will be inserted a same level"""
  647.         ele = inkex.etree.Element('{http://www.w3.org/2000/svg}rect')
  648.         self.fill(ele)
  649.         refnode.xpath('..')[0].append(ele)
  650.         return ele
  651.        
  652.     def fill(self,ele):
  653.         w, h = self.size
  654.         ele.set('width',str(w))
  655.         ele.set('height',str(h))
  656.         w, h = self.bbox
  657.         ele.set('x',str(self.pos[0]))
  658.         ele.set('y',str(self.pos[1]))
  659.         if self.rotMatstr:
  660.             ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle),self.center[0],self.center[1]))
  661.             #ele.set('transform', self.rotMatstr)      
  662.  
  663.     @staticmethod
  664.     def isRectangle( pathGroup):
  665.         """Check if the segments in pathGroups can form a rectangle.
  666.        Returns a Rectangle or None"""
  667.         #print 'xxxxxxxx isRectangle',pathGroups
  668.         if isinstance(pathGroup, Circle ): return None
  669.         segmentList = [p for p in pathGroup.listOfPaths if p.isSegment() ]#or p.effectiveNPoints >0]
  670.         if len(segmentList) != 4:
  671.             debug( 'rectangle Failed at length ', len(segmentList))
  672.             return None
  673.         a,b,c,d = segmentList
  674.  
  675.         if length(a.point1, d.pointN)> 0.2*(a.length+d.length)*0.5:
  676.             debug('rectangle test failed closing ', length(a.point1, d.pointN), a.length, d.length)
  677.             return None
  678.        
  679.         Aac , Abd = closeAngleAbs(a.angle,c.angle), closeAngleAbs(b.angle , d.angle)
  680.         if  min(Aac,Abd) > 0.07 or max(Aac, Abd) >0.27 :
  681.             debug( 'rectangle Failed at angles', Aac, Abd)
  682.             return None
  683.         notsimilarL = lambda d1,d2: abs(d1-d2)>0.20*min(d1,d2)
  684.  
  685.         pi , twopi = numpy.pi,2*numpy.pi
  686.         angles = numpy.array( [p.angle   for p in segmentList] )
  687.         minAngleInd = numpy.argmin( numpy.minimum( abs(angles), abs( abs(angles)-pi), abs( abs(angles)-twopi) ) )
  688.         rotAngle = angles[minAngleInd]
  689.         width = (segmentList[minAngleInd].length + segmentList[(minAngleInd+2)%4].length)*0.5
  690.         height = (segmentList[(minAngleInd+1)%4].length + segmentList[(minAngleInd+3)%4].length)*0.5
  691.         # set rectangle center as the bbox center
  692.         x,y,w,h = computeBox( numpy.concatenate( [ p.points for p in segmentList]) )
  693.         r = Rectangle( numpy.array( [x+w/2, y+h/2]), (width, height), rotAngle, pathGroup.listOfPaths, pathGroup.refNode)
  694.        
  695.         debug( ' found a rectangle !! ', a.length, b.length, c.length, d.length )
  696.         return r
  697.  
  698.  
  699. # *************************************************************
  700. # Object manipulation functions
  701.  
  702. def toRemarkableShape( group ):
  703.     """Test if PathGroup instance 'group' looks like a remarkable shape (ex: Rectangle).
  704.    if so returns a new shape instance else returns group unchanged"""
  705.     r = Rectangle.isRectangle( group )
  706.     if r : return r
  707.     return group
  708.  
  709.  
  710. def resetPrevNextSegment(segs):
  711.     for i, seg in enumerate(segs[:-1]):
  712.         s = segs[i+1]
  713.         seg.next = s
  714.         s.prev = seg          
  715.     return segs
  716.  
  717.  
  718. def fitSingleSegment(a):
  719.     xmin,ymin,w,h = computeBox(a)
  720.     inverse = w<h
  721.     if inverse:
  722.         a = numpy.roll(a,1,axis=1)
  723.  
  724.     seg = regLin(a)
  725.     if inverse:
  726.         seg.inverse()
  727.         #a = numpy.roll(a,1,axis=0)
  728.     return seg
  729.        
  730. def regLin(a , returnOnlyPars=False):
  731.     """perform a linear regression on 2dim array a. Creates a segment object in return """
  732.     sumX = a[:,0].sum()
  733.     sumY = a[:,1].sum()
  734.     sumXY = (a[:,1]*a[:,0]).sum()
  735.     a2 = a*a
  736.     sumX2 = a2[:,0].sum()
  737.     sumY2 = a2[:,1].sum()
  738.     N = a.shape[0]
  739.  
  740.     pa = (N*sumXY - sumX*sumY)/ ( N*sumX2 - sumX*sumX)
  741.     pb = (sumY - pa*sumX) /N
  742.     if returnOnlyPars:
  743.         return pa,-1, pb
  744.     return Segment(pa, -1, pb, a)
  745.  
  746.  
  747. def smoothArray(a, n=2):
  748.     count = numpy.zeros(a.shape)
  749.     smootha = numpy.array(a)
  750.     for i in range(n):
  751.         count[i]=n+i+1
  752.         count[-i-1] = n+i+1
  753.     count[n:-n] = n+n+1
  754.     #debug('smooth ', len(smooth[:-2]) [)
  755.     for i in range(1,n+1):
  756.         smootha[:-i] += a[i:]
  757.         smootha[i:]  += a[:-i]
  758.     return smootha/count
  759.  
  760. def buildTangents( points , averaged=True, isClosing=False):
  761.     """build tangent vectors to the curve 'points'.
  762.    if averaged==True, the tangents are averaged with their direct neighbours (use case : smoother tangents)"""
  763.     tangents = numpy.zeros( (len(points),2) )
  764.     i=1
  765.     tangents[:-i] += points[i:] - points[:-i] # i <- p_i+1 - p_i
  766.     tangents[i:]  += points[i:] - points[:-i] # i <- p_i - p_i-1
  767.     if isClosing:
  768.         tangents[0] += tangents[0] - tangents[-1]
  769.         tangents[-1] += tangents[0] - tangents[-1]
  770.     tangents *= 0.5
  771.     if not isClosing:
  772.         tangents[0] *=2
  773.         tangents[-1] *=2
  774.  
  775.  
  776.     ## debug('points ', points)
  777.     ## debug('buildTangents --> ', tangents )
  778.    
  779.     if averaged:
  780.         # average over neighbours
  781.         avTan = numpy.array(tangents)
  782.         avTan[:-1] += tangents[1:]
  783.         avTan[1:]  += tangents[:-1]
  784.         if isClosing:
  785.             tangents[0]+=tangents[-1]
  786.             tangents[1]+=tangents[0]
  787.         avTan *= 1./3
  788.         if not isClosing:
  789.             avTan[0] *=1.5
  790.             avTan[-1] *=1.5
  791.  
  792.     return avTan
  793.  
  794.  
  795. def clusterAngles(array, dAng=0.15):
  796.     """Cluster together consecutive angles with similar values (within 'dAng').
  797.    array : flat array of angles
  798.    returns [ ...,  (indi_0, indi_1),...] where each tuple are indices of cluster i
  799.    """
  800.     N = len(array)
  801.  
  802.     closebyAng = numpy.zeros( (N,4) , dtype=int)
  803.  
  804.     for i,a in enumerate(array):
  805.         cb = closebyAng[i]
  806.         cb[0] =i
  807.         cb[2]=i
  808.         cb[3]=i
  809.         c=i-1
  810.         # find number of angles within dAng in nearby positions
  811.         while c>-1: # indices below i
  812.             d=closeAngleAbs(a,array[c])
  813.             if d>dAng:
  814.                 break
  815.             cb[1]+=1                
  816.             cb[2]=c
  817.             c-=1
  818.         c=i+1
  819.         while c<N-1:# indices above i
  820.             d=closeAngleAbs(a,array[c])
  821.             if d>dAng:
  822.                 break
  823.             cb[1]+=1                
  824.             cb[3]=c
  825.             c+=1
  826.     closebyAng= closebyAng[numpy.argsort(closebyAng[:,1]) ]
  827.  
  828.     clusteredPos = numpy.zeros(N, dtype=int)
  829.     clusters = []
  830.     for cb in reversed(closebyAng):
  831.         if clusteredPos[cb[0]]==1:
  832.             continue
  833.         # try to build a cluster
  834.         minI = cb[2]
  835.         while clusteredPos[minI]==1:
  836.             minI+=1
  837.         maxI = cb[3]
  838.         while clusteredPos[maxI]==1:
  839.             maxI-=1
  840.         for i in range(minI, maxI+1):
  841.             clusteredPos[i] = 1
  842.         clusters.append( (minI, maxI) )
  843.  
  844.     return clusters
  845.        
  846.    
  847.                
  848.  
  849. def adjustAllAngles(paths):
  850.     for p in paths:
  851.         if p.isSegment() and p.newAngle is not None:
  852.             p.adjustToNewAngle()
  853.     # next translate to fit end points
  854.     tr = numpy.zeros(2)
  855.     for p in paths[1:]:
  856.         if p.isSegment() and p.prev.isSegment():
  857.             tr = p.prev.pointN - p.point1
  858.         debug(' translating ',p,' prev is', p.prev, '  ',tr, )
  859.         p.translate(tr)
  860.  
  861. def adjustAllDistances(paths):
  862.     for p in paths:
  863.         if p.isSegment() and  p.newLength is not None:                
  864.             p.adjustToNewDistance()
  865.     # next translate to fit end points
  866.     tr = numpy.zeros(2)
  867.     for p in paths[1:]:
  868.         if p.isSegment() and p.prev.isSegment():
  869.             tr = p.prev.pointN - p.point1
  870.         p.translate(tr)
  871.  
  872.  
  873. ##**************************************
  874. ##
  875. class SegmentExtender:
  876.     """Extend Segments part of a list of Path by aggregating points from neighbouring Path objects.
  877.  
  878.    There are 2 concrete subclasses for extending forward and backward (due to technical reasons).
  879.    """
  880.  
  881.     def __init__(self, relD, fitQ):
  882.         self.relD = relD
  883.         self.fitQ = fitQ
  884.        
  885.     def nextPaths(self,seg):
  886.         pL = []
  887.         p = self.getNext(seg) # prev or next
  888.         while p :
  889.             if p.isSegment(): break
  890.             if p.mergedObj is None: break
  891.             pL.append(p)
  892.             p = self.getNext(p)
  893.         if pL==[]:
  894.             return []
  895.         return pL
  896.  
  897.     def extend(self,seg):
  898.         nextPathL = self.nextPaths(seg)
  899.         debug('extend ',self.extDir, seg , nextPathL, seg.length , len(nextPathL))
  900.         if nextPathL==[]: return seg
  901.         pointsToTest = numpy.concatenate( [p.points for p in nextPathL] )
  902.         mergeD = seg.length*self.relD
  903.         #print seg.point1 , seg.pointN,  pointsToTest
  904.         pointsToFit, addedPoints = self.pointsToFit(seg,pointsToTest , mergeD)
  905.         if len(pointsToFit)==0:
  906.             return seg
  907.         newseg = fitSingleSegment(pointsToFit)
  908.         if newseg.quality()>self.fitQ: # fit failed
  909.             return seg
  910.         debug( '  EXTENDING ! ', len(seg.points), len(addedPoints) )
  911.         self.removePath(seg, newseg, nextPathL, addedPoints )
  912.         newseg.points = pointsToFit
  913.         seg.mergedObj= newseg
  914.         newseg.sourcepoints = seg.sourcepoints
  915.  
  916.         return newseg
  917.  
  918.     @staticmethod
  919.     def extendSegments(segmentList, relD=0.03, qual=0.5):
  920.         """Perform Segment extension from list of Path segmentList
  921.        returns the updated list of Path objects"""
  922.         fwdExt = FwdExtender(relD, qual)
  923.         bwdExt = BwdExtender(relD, qual)
  924.         # tag all objects with an attribute pointing to the extended object
  925.         for seg in segmentList:            
  926.             seg.mergedObj = seg # by default the extended object is self
  927.         # extend each segments, starting by the longest
  928.         for seg in sorted(segmentList, key = lambda s : s.length, reverse=True):
  929.             if seg.isSegment():
  930.                 newseg=fwdExt.extend(seg)
  931.                 seg.mergedObj = bwdExt.extend(newseg)
  932.         # the extension procedure has marked as None the mergedObj
  933.         # which have been swallowed by an extension.
  934.         #  filter them out :
  935.         updatedSegs=[seg.mergedObj for seg in segmentList if seg.mergedObj]
  936.         return updatedSegs
  937.  
  938.  
  939. class FwdExtender(SegmentExtender):
  940.     extDir='Fwd'
  941.     def getNext(self, seg):
  942.         return seg.next
  943.     def pointsToFit(self, seg, pointsToTest, mergeD):
  944.         distancesToLine =abs(seg.a*pointsToTest[:,0]+seg.b*pointsToTest[:,1]+seg.c)        
  945.         goodInd=len(pointsToTest)
  946.         for i,d in reversed(list(enumerate(distancesToLine))):
  947.             if d<mergeD: goodInd=i;break
  948.         addedPoints = pointsToTest[:len(pointsToTest-goodInd)]
  949.         #debug( ' ++ pointsToFit ' , mergeD, i ,len(pointsToTest), addedPoints , seg.points )
  950.         return  numpy.concatenate([seg.points, addedPoints]), addedPoints
  951.     def removePath(self, seg, newseg, nextPathL, addedPoints):
  952.         npoints = len(addedPoints)
  953.         acc=0
  954.         newseg.prev = seg.prev
  955.         for p in nextPathL:
  956.             if (acc+len(p.points))<=npoints:
  957.                 p.mergedObj = None
  958.                 acc += len(p.points)
  959.             else:
  960.                 newseg.next = p
  961.                 p.points = p.points[:(npoints-acc-len(p.points))]
  962.                 break
  963.  
  964. class BwdExtender(SegmentExtender):
  965.     extDir='Bwd'
  966.     def getNext(self, seg):
  967.         return seg.prev
  968.     def pointsToFit(self, seg, pointsToTest,  mergeD):
  969.         #  TODO: shouldn't the distances be sorted cclosest to furthest
  970.         distancesToLine =abs(seg.a*pointsToTest[:,0]+seg.b*pointsToTest[:,1]+seg.c)
  971.         goodInd=len(pointsToTest)        
  972.         for i,d in enumerate(distancesToLine):
  973.             if d<mergeD: goodInd=i; break
  974.         addedPoints = pointsToTest[goodInd:]
  975.         #debug( ' ++ pointsToFit ' , mergeD, i ,len(pointsToTest), addedPoints , seg.points )
  976.         return  numpy.concatenate([addedPoints, seg.points]), addedPoints
  977.     def removePath(self,seg, newseg, nextPathL, addedPoints):
  978.         npoints = len(addedPoints)
  979.         acc=0
  980.         newseg.next = seg.next                
  981.         for p in reversed(nextPathL):
  982.             if (acc+len(p.points))<=npoints:
  983.                 p.mergedObj = None
  984.                 acc += len(p.points)
  985.             else:
  986.                 newseg.prev = p        
  987.                 p.points = p.points[(npoints-acc-len(p.points)):]                        
  988.                 break
  989.  
  990.  
  991.  
  992. # merge consecutive segments with close angle
  993.  
  994. def mergeConsecutiveCloseAngles( segList , mangle =0.25 , q=0.5):
  995.  
  996.     def toMerge(seg):
  997.         l=[seg]
  998.         setattr(seg, 'merged', True)
  999.         if seg.next and seg.next.isSegment() :
  1000.             debug('merging segs ', seg.angle, ' with : ' ,seg.next.point1, seg.next.pointN, ' ang=',seg.next.angle)
  1001.             if deltaAngleAbs( seg.angle, seg.next.angle) < mangle:
  1002.                 l += toMerge(seg.next)
  1003.         return l
  1004.  
  1005.     updatedSegs = []
  1006.     for i,seg in enumerate(segList[:-1]):
  1007.         if not seg.isSegment() :
  1008.             updatedSegs.append(seg)
  1009.             continue
  1010.         if  hasattr(seg,'merged'):
  1011.             continue
  1012.         debug(i,' inspect merge : ', seg.point1,'-',seg.pointN, seg.angle , ' q=',seg.quality())
  1013.         mList = toMerge(seg)
  1014.         debug('  --> tomerge ', len(mList))
  1015.         if len(mList)<2:
  1016.             delattr(seg, 'merged')
  1017.             updatedSegs.append(seg)
  1018.             continue
  1019.         points= numpy.concatenate( [p.points for p in mList] )
  1020.         newseg = fitSingleSegment(points)
  1021.         if newseg.quality()>q:
  1022.             delattr(seg, 'merged')
  1023.             updatedSegs.append(seg)
  1024.             continue
  1025.         for p in mList:
  1026.             setattr(seg, 'merged',True)
  1027.         newseg.sourcepoints = seg.sourcepoints
  1028.         debug('  --> post merge qual = ', newseg.quality() , seg.pointN, ' --> ', newseg.pointN, newseg.angle)
  1029.         newseg.prev = mList[0].prev
  1030.         newseg.next = mList[-1].next
  1031.         updatedSegs.append(newseg)
  1032.     if not hasattr(segList[-1], 'merged') : updatedSegs.append( segList[-1])
  1033.     return updatedSegs
  1034.  
  1035.  
  1036.  
  1037.  
  1038. def parametersFromPointAngle(point, angle):
  1039.     unitv = numpy.array([ numpy.cos(angle), numpy.sin(angle) ])
  1040.     ortangle = angle+numpy.pi/2
  1041.     normal = numpy.array([ numpy.cos(ortangle), numpy.sin(ortangle) ])
  1042.     genOffset = -normal.dot(point)
  1043.     a, b = normal
  1044.     return a, b , genOffset
  1045.    
  1046.  
  1047.  
  1048. def addPath(newList, refnode):
  1049.     """Add a node in the xml structure corresponding to the content of newList
  1050.    newList : list of Segment or Path
  1051.    refnode : xml node used as a reference, new point will be inserted a same level"""
  1052.     ele = inkex.etree.Element('{http://www.w3.org/2000/svg}path')
  1053.     ele.set('d', simplepath.formatPath(newList))
  1054.     refnode.xpath('..')[0].append(ele)
  1055.     return ele
  1056.  
  1057. def reformatList( listOfPaths):
  1058.     """ Returns a SVG paths list (same format as simplepath.parsePath) from a list of Path objects
  1059.     - Segments in paths are added in the new list
  1060.     - simple Path are retrieved from the original refSVGPathList and put in the new list (thus preserving original bezier curves)
  1061.    """
  1062.     newList = []
  1063.     first = True
  1064.     for  seg in listOfPaths:        
  1065.         newList += seg.asSVGCommand(first)
  1066.         first = False
  1067.     return newList
  1068.  
  1069.  
  1070. def clusterValues( values, relS=0.1 , refScaleAbs='range'  ):
  1071.     """form clusters of similar quantities from input 'values'.
  1072.    Clustered values are not necessarily contiguous in the input array.
  1073.    Clusters size (that is max-min) is < relS*cluster_average """
  1074.     if len(values)==0:
  1075.         return []
  1076.     if len(values.shape)==1:
  1077.         sortedV = numpy.stack([ values , numpy.arange(len(values))] ,1)
  1078.     else:
  1079.         # Assume value.shape = (N,2) and index are ok
  1080.         sortedV = values
  1081.     sortedV = sortedV[ numpy.argsort(sortedV[:,0]) ]
  1082.  
  1083.     sortedVV = sortedV[:,0]
  1084.     refScale = sortedVV[-1]-sortedVV[0]
  1085.     #sortedVV += 2*min(sortedVV)) # shift to avoid numerical issues around 0
  1086.  
  1087.     #print sortedVV
  1088.     class Cluster:
  1089.         def __init__(self, delta, sum, indices):
  1090.             self.delta = delta
  1091.             self.sum = sum
  1092.             self.N=len(indices)
  1093.             self.indices = indices
  1094.         def size(self):
  1095.             return self.delta/refScale
  1096.        
  1097.         def combine(self, c):
  1098.             #print ' combine ', self.indices[0], c.indices[-1], ' -> ', sortedVV[c.indices[-1]] - sortedVV[self.indices[0]]
  1099.             newC = Cluster(sortedVV[c.indices[-1]] - sortedVV[self.indices[0]],
  1100.                            self.sum+c.sum,
  1101.                            self.indices+c.indices)
  1102.             return newC
  1103.  
  1104.         def originIndices(self):
  1105.             return tuple(int(sortedV[i][1]) for i in self.indices)
  1106.  
  1107.     def size_local(self):
  1108.         return self.delta / sum( sortedVV[i] for i in self.indices) *len(self.indices)
  1109.     def size_range(self):
  1110.         return self.delta/refScale
  1111.     def size_abs(self):
  1112.         return self.delta
  1113.  
  1114.     if refScaleAbs=='range':
  1115.         Cluster.size = size_range
  1116.     elif refScaleAbs=='local':
  1117.         Cluster.size = size_local
  1118.     elif refScaleAbs=='abs':
  1119.         Cluster.size = size_abs
  1120.        
  1121.     class ClusterPair:
  1122.         next=None
  1123.         prev=None
  1124.         def __init__(self, c1, c2 ):
  1125.             self.c1=c1
  1126.             self.c2=c2
  1127.             self.refresh()
  1128.         def refresh(self):
  1129.             self.potentialC =self.c1.combine(self.c2)
  1130.             self.size = self.potentialC.size()
  1131.         def setC1(self, c1):
  1132.             self.c1=c1
  1133.             self.refresh()
  1134.         def setC2(self, c2):
  1135.             self.c2=c2
  1136.             self.refresh()
  1137.            
  1138.     #ave = 0.5*(sortedVV[1:,0]+sortedV[:-1,0])
  1139.     #deltaR = (sortedV[1:,0]-sortedV[:-1,0])/ave
  1140.  
  1141.     cList = [Cluster(0,v,(i,)) for (i,v) in enumerate(sortedVV) ]
  1142.     cpList = [ ClusterPair( c, cList[i+1] ) for (i,c) in enumerate(cList[:-1]) ]
  1143.     resetPrevNextSegment( cpList )
  1144.  
  1145.     #print cpList
  1146.     def reduceCL( cList ):
  1147.         if len(cList)<=1:
  1148.             return cList
  1149.         cp = min(cList, key=lambda cp:cp.size)    
  1150.         #print '==', cp.size , relS, cp.c1.indices , cp.c2.indices, cp.potentialC.indices
  1151.  
  1152.         while cp.size < relS:
  1153.             if cp.next:
  1154.                 cp.next.setC1(cp.potentialC)
  1155.                 cp.next.prev = cp.prev
  1156.             if cp.prev:
  1157.                 cp.prev.setC2(cp.potentialC)
  1158.                 cp.prev.next = cp.next
  1159.             cList.remove(cp)
  1160.             if len(cList)<2:
  1161.                 break
  1162.             cp = min(cList, key=lambda cp:cp.size)    
  1163.         #print ' -----> ', [ (cp.c1.indices , cp.c2.indices) for cp in cList]
  1164.         return cList
  1165.  
  1166.     cpList = reduceCL(cpList)
  1167.     if len(cpList)==1:
  1168.         cp = cpList[0]
  1169.         if cp.potentialC.size()<relS:
  1170.             return [ cp.potentialC.originIndices() ]
  1171.     #print cpList
  1172.     if cpList==[]:
  1173.         return []
  1174.     finalCL = [ cp.c1.originIndices() for cp in cpList ]+[ cpList[-1].c2.originIndices() ]
  1175.     return finalCL
  1176.  
  1177.  
  1178.  
  1179.  
  1180. # *************************************************************
  1181. # The inkscape extension
  1182. # *************************************************************
  1183. class ShapeReco(inkex.Effect):
  1184.     def __init__(self):
  1185.         inkex.Effect.__init__(self)
  1186.         self.OptionParser.add_option("--title")
  1187.         self.OptionParser.add_option("-k", "--keepOrigin", dest="keepOrigin", default=False,
  1188.                                      action="store", type="inkbool",                                      
  1189.                                      help="Do not replace path")
  1190.  
  1191.         self.OptionParser.add_option( "--MainTabs")
  1192.         #self.OptionParser.add_option( "--Basic")
  1193.  
  1194.         self.OptionParser.add_option( "--segExtensionDtoSeg", dest="segExtensionDtoSeg", default=0.03,
  1195.                                       action="store", type="float",                                      
  1196.                                       help="max distance from point to segment")
  1197.         self.OptionParser.add_option( "--segExtensionQual", dest="segExtensionQual", default=0.5,
  1198.                                       action="store", type="float",                                      
  1199.                                       help="segment extension fit quality")
  1200.         self.OptionParser.add_option( "--segExtensionEnable", dest="segExtensionEnable", default=True,
  1201.                                       action="store", type="inkbool",                                      
  1202.                                       help="Enable segment extension")
  1203.  
  1204.  
  1205.         self.OptionParser.add_option( "--segAngleMergeEnable", dest="segAngleMergeEnable", default=True,
  1206.                                       action="store", type="inkbool",                                      
  1207.                                       help="Enable merging of almost aligned consecutive segments")
  1208.         self.OptionParser.add_option( "--segAngleMergeTol1", dest="segAngleMergeTol1", default=0.2,
  1209.                       action="store", type="float",                                      
  1210.                                       help="merging with tollarance 1")
  1211.         self.OptionParser.add_option( "--segAngleMergeTol2", dest="segAngleMergeTol2", default=0.35,
  1212.                       action="store", type="float",                                      
  1213.                                       help="merging with tollarance 2")
  1214.                                      
  1215.         self.OptionParser.add_option( "--segAngleMergePara", dest="segAngleMergePara", default=0.001,
  1216.                       action="store", type="float",                                      
  1217.                                       help="merge lines as parralels if they fit")
  1218.  
  1219.         self.OptionParser.add_option( "--segRemoveSmallEdge", dest="segRemoveSmallEdge", default=True,
  1220.                                       action="store", type="inkbool",                                      
  1221.                                       help="Enable removing very small segments")
  1222.  
  1223.         self.OptionParser.add_option( "--doUniformization", dest="doUniformization", default=True,
  1224.                                      action="store", type="inkbool",                                      
  1225.                                      help="Preform angles and distances uniformization")
  1226.  
  1227.         for opt in ["doParrallelize", "doKnownAngle", "doEqualizeDist" , "doEqualizeRadius" , "doCenterCircOnSeg"]:
  1228.             self.OptionParser.add_option( "--"+opt, dest=opt, default=True,
  1229.                                           action="store", type="inkbool",                                      
  1230.                                           help=opt)
  1231.                                           #0.3
  1232.         self.OptionParser.add_option( "--shapeDistLocal", dest="shapeDistLocal", default=0.3,
  1233.                                          action="store", type="float",                                      
  1234.                                      help="Pthe percentage of difference at which we make lengths equal, locally")
  1235.                                      #0.025
  1236.         self.OptionParser.add_option( "--shapeDistGlobal", dest="shapeDistGlobal", default=0.025,
  1237.                                          action="store", type="float",                                      
  1238.                                      help="Pthe percentage of difference at which we make lengths equal, globally")
  1239.                                          
  1240.  
  1241.        
  1242.     def effect(self):
  1243.  
  1244.         rej='{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}type'
  1245.         paths = []
  1246.         for id, node in self.selected.iteritems():
  1247.             if node.tag == '{http://www.w3.org/2000/svg}path' and rej not in node.keys():                
  1248.                 paths.append(node)
  1249.  
  1250.         shapes = self.extractShapes(paths)
  1251.         # add new shapes in SVG document
  1252.         self.addShapesToDoc( shapes )
  1253.  
  1254.  
  1255.     def removeSmallEdge(self, paths, wTot,hTot):
  1256.         """Remove small Path objects which stand between 2 Segments (or at the ends of the sequence).
  1257.        Small means the bbox of the path is less then 5% of the mean of the 2 segments."""
  1258.         if len(paths)<2:
  1259.             return
  1260.         def getdiag(points):
  1261.             xmin,ymin,w,h = computeBox(points)
  1262.             return sqrt(w**2+h**2), w, h
  1263.         removeSeg=[]
  1264.         def remove(p):
  1265.             removeSeg.append(p)
  1266.             if p.next : p.next.prev = p.prev
  1267.             if p.prev: p.prev.next = p.next
  1268.             p.effectiveNPoints =0
  1269.             debug('      --> remove !', p, p.length , len(p.points))
  1270.         for p in paths:
  1271.             if len(p.points)==0 :
  1272.                 remove(p)
  1273.                 continue
  1274.             # select only path between 2 segments
  1275.             next, prev = p.next, p.prev
  1276.             if next is None: next = prev
  1277.             if prev is None: prev = next
  1278.             if not next.isSegment() or not prev.isSegment() : continue
  1279.             #diag = getdiag(p.points)
  1280.             diag ,w, h = getdiag(p.points)
  1281.  
  1282.             debug(p, p.pointN, ' removing edge  diag = ', diag, p.length,  '  l=',next.length+prev.length , 'totDim ', (wTot,hTot))
  1283.             debug( '    ---> ',prev, next)
  1284.  
  1285.             #t TODO: his needs to be parameterized
  1286.             # remove last or first very small in anycase
  1287.             doRemove = prev==next and (diag < 0.05*(wTot+hTot)*0.5 )
  1288.             if not doRemove:
  1289.                 # check if this small
  1290.                 isLarge = diag > (next.length+prev.length)*0.1  # check size relative to neighbour
  1291.                 isLarge = isLarge or w > 0.2*wTot or h > 0.2*hTot # check size w.r.t total size
  1292.                
  1293.                 # is it the small side of a long rectangle ?
  1294.                 dd = prev.distanceTo(next.pointN)
  1295.                 rect = abs(prev.unitv.dot(next.unitv))>0.98 and diag > dd*0.5
  1296.                 doRemove = not( isLarge or rect )
  1297.  
  1298.             if doRemove:
  1299.                 remove(p)
  1300.  
  1301.                 if next != prev:
  1302.                     prev.setIntersectWithNext(next)
  1303.         debug('removed Segments ', removeSeg)
  1304.         for p in removeSeg:
  1305.             paths.remove(p)
  1306.  
  1307.  
  1308.            
  1309.            
  1310.    
  1311.     def prepareParrallelize(self,segs):
  1312.         """Group Segment by their angles (segments are grouped together if their deltAangle is within 0.15 rad)
  1313.        The 'newAngle' member of segments in a group are then set to the mean angle of the group (where angles are all
  1314.        considered in [-pi, pi])
  1315.  
  1316.        segs : list of segments
  1317.        """
  1318.  
  1319.         angles = numpy.array([s.angle for s in segs ])
  1320.         angles[numpy.where(angles<0)] += _pi # we care about direction, not angle orientation
  1321.         clList = clusterValues(angles, 0.30, refScaleAbs='abs')#15
  1322.  
  1323.     pi =  numpy.pi;
  1324.         for cl in clList:
  1325.             anglecount = {}
  1326.             for angle in angles[list(cl)]:
  1327.             #   #angleDeg = int(angle * 360.0 / (2.0*pi))
  1328.                 if not angle in anglecount:
  1329.                     anglecount[angle] = 1
  1330.                 else:
  1331.                     anglecount[angle] += 1
  1332.        
  1333.             anglecount = {k: v for k, v in sorted(anglecount.items(), key=lambda item: item[1], reverse=True)}
  1334.             meanA = anglecount.popitem()[0]#.items()[1]#sorted(anglecount.items(), key = lambda kv:(kv[1], kv[0]), reverse=True)[1][1]
  1335.             #meanA = float(meanA) * (2.0*pi) / 360.0
  1336.             #meanA = angles[list(cl)].mean()
  1337.             for i in cl:
  1338.                 seg = segs[i]
  1339.                 seg.newAngle = meanA if seg.angle>=0. else meanA-_pi
  1340.  
  1341.  
  1342.     def prepareDistanceEqualization(self,segs, relDelta=0.1):
  1343.         """ Input segments are grouped according to their length  :
  1344.          - for each length L, find all other lengths within L*relDelta. of L.
  1345.          - Find the larger of such subgroup.
  1346.          - repeat the procedure on remaining lengths until none is left.
  1347.        Each length in a group is set to the mean length of the group
  1348.  
  1349.        segs : a list of segments
  1350.        relDelta : float, minimum relative distance.
  1351.        """
  1352.  
  1353.         lengths = numpy.array( [x.tempLength() for x in segs] )
  1354.         clusters = clusterValues(lengths, relDelta)
  1355.  
  1356.         if len(clusters)==1:
  1357.             # deal with special case with low num of segments
  1358.             # --> don't let a single segment alone
  1359.             if len(clusters[0])+1==len(segs):
  1360.                 clusters[0]=range(len(segs)) # all
  1361.  
  1362.         allDist = []
  1363.         for cl in clusters:
  1364.             dmean = sum( lengths[i] for i in cl ) / len(cl)
  1365.             allDist.append(dmean)
  1366.             for i in cl:
  1367.                 segs[i].setNewLength(dmean)
  1368.                 debug( i,' set newLength ',dmean, segs[i].length, segs[i].dumpShort())
  1369.                
  1370.         return allDist
  1371.  
  1372.  
  1373.     def prepareRadiusEqualization(self, circles, otherDists, relSize=0.2):
  1374.         """group circles radius and distances into cluster.
  1375.        Then set circles radius according to the mean of the clusters they belong to."""
  1376.         ncircles = len(circles)
  1377.         lengths = numpy.array( [c.radius for c in circles]+otherDists )
  1378.         indices = numpy.array( range(ncircles+len(otherDists) ) )
  1379.         clusters = clusterValues(numpy.stack([ lengths, indices ],1 ), relSize, refScaleAbs='local' )
  1380.  
  1381.         debug('prepareRadiusEqualization radius ', repr(lengths))
  1382.         debug('prepareRadiusEqualization clusters ',  clusters)
  1383.         allDist = []
  1384.         for cl in clusters:
  1385.             dmean = sum( lengths[i] for i in cl ) / len(cl)
  1386.             #print cl , dmean ,
  1387.             allDist.append(dmean)
  1388.             if len(cl)==1:
  1389.                 continue
  1390.             for i in cl:
  1391.                 if i< ncircles:
  1392.                     circles[i].radius = dmean
  1393.         debug(' post radius ',[c.radius for c in circles] )
  1394.         return allDist
  1395.  
  1396.  
  1397.     def centerCircOnSeg(self, circles, segments, relSize=0.18):
  1398.         """ move centers of circles onto the segments if close enough"""
  1399.         for circ in circles:
  1400.             circ.moved = False
  1401.         for seg in segments:
  1402.             for circ in circles:                
  1403.                 d = seg.distanceTo(circ.center)
  1404.                 #debug( '      ', seg.projectPoint(circ.center))
  1405.                 if d < circ.radius*relSize and not circ.moved :
  1406.                     circ.center = seg.projectPoint(circ.center)
  1407.                     circ.moved = True
  1408.                
  1409.  
  1410.     def adjustToKnownAngle(self, paths):
  1411.         """ Check current angle against remarkable angles. If close enough, change it
  1412.        paths : a list of segments"""
  1413.         for seg in paths:
  1414.             a = seg.tempAngle()
  1415.             i = (abs(vec_in_mPi_pPi(knownAngle - a) )).argmin()
  1416.             seg.newAngle = knownAngle[i]
  1417.             debug( '  Known angle ', seg, seg.tempAngle(),'  -> ', knownAngle[i])
  1418.             ## if abs(knownAngle[i] - a) < 0.08:
  1419.  
  1420.        
  1421.  
  1422.     def checkForCircle(self, points, tangents):
  1423.         """Determine if the points and their tangents represent a circle
  1424.  
  1425.        The difficulty is to be able to recognize ellipse while avoiding paths small fluctuations a
  1426.        nd false positive due to badly drawn rectangle or non-convex closed curves.
  1427.        
  1428.        Method : we consider angle of tangent as function of lenght on path.
  1429.        For circles these are : angle = c1 x lenght + c0. (c1 ~1)
  1430.  
  1431.        We calculate dadl = d(angle)/d(length) and compare to c1.
  1432.        We use 3 criteria :
  1433.         * num(dadl > 6) : number of sharp angles
  1434.         * length(dadl<0.3)/totalLength : lengths of straight lines within the path.
  1435.         * totalLength/(2pi x radius) : fraction of lenght vs a plain circle
  1436.  
  1437.        Still failing to recognize elongated ellipses...
  1438.        
  1439.        """
  1440.         if len(points)<10:
  1441.             return False, 0
  1442.  
  1443.         if all(points[0]==points[-1]): # last exactly equals the first.
  1444.             # Ignore last point for this check
  1445.             points = points[:-1]
  1446.             tangents = tangents[:-1]
  1447.             #print 'Removed last ', points
  1448.         xmin,ymin, w, h = computeBox( points)
  1449.         diag2=(w*w+h*h)
  1450.        
  1451.         diag = sqrt(diag2)*0.5
  1452.         norms = numpy.sqrt(numpy.sum( tangents**2, 1 ))
  1453.  
  1454.         angles = numpy.arctan2(  tangents[:,1], tangents[:,0] )  
  1455.         #debug( 'angle = ', repr(angles))
  1456.         N = len(angles)
  1457.        
  1458.         deltas =  points[1:] - points[:-1]
  1459.         deltasD = numpy.concatenate([ [D(points[0],points[-1])/diag], numpy.sqrt(numpy.sum( deltas**2, 1 )) / diag] )
  1460.  
  1461.         # locate and avoid the point when swicthing
  1462.         # from -pi to +pi. The point is around the minimum
  1463.         imin = numpy.argmin(angles)
  1464.         debug(' imin ',imin)
  1465.         angles = numpy.roll(angles, -imin)
  1466.         deltasD = numpy.roll(deltasD, -imin)
  1467.         n=int(N*0.1)
  1468.         # avoid fluctuations by removing points around the min
  1469.         angles=angles[n:-n]
  1470.         deltasD=deltasD[n:-n]
  1471.         deltasD = deltasD.cumsum()
  1472.         N = len(angles)
  1473.  
  1474.         # smooth angles to avoid artificial bumps
  1475.         angles = smoothArray(angles, n=max(int(N*0.03),2) )
  1476.  
  1477.         deltaA = angles[1:] - angles[:-1]
  1478.         deltasDD =  (deltasD[1:] -deltasD[:-1])
  1479.         deltasDD[numpy.where(deltasDD==0.)] = 1e-5*deltasD[0]
  1480.         dAdD = abs(deltaA/deltasDD)
  1481.         belowT, count = True,0
  1482.         for v in dAdD:
  1483.             if v>6 and belowT:
  1484.                 count+=1
  1485.                 belowT = False
  1486.             belowT= (v<6)
  1487.  
  1488.         self.temp = (deltasD,angles, tangents, dAdD )
  1489.         fracStraight = numpy.sum(deltasDD[numpy.where(dAdD<0.3)])/(deltasD[-1]-deltasD[0])
  1490.         curveLength = deltasD[-1]/3.14
  1491.         #print "SSS ",count , fracStraight
  1492.         if curveLength> 1.4 or fracStraight>0.4 or count > 6:
  1493.             isCircle =False
  1494.         else:
  1495.             isCircle= (count < 4 and fracStraight<=0.3) or \
  1496.                       (fracStraight<=0.1 and count<5)
  1497.  
  1498.         if not isCircle:
  1499.             return False, 0
  1500.            
  1501.         # It's a circle !
  1502.         radius = points - numpy.array([xmin+w*0.5,ymin+h*0.5])
  1503.         radius_n = numpy.sqrt(numpy.sum( radius**2, 1 )) # normalize
  1504.  
  1505.         mini = numpy.argmin(radius_n)        
  1506.         rmin = radius_n[mini]
  1507.         maxi = numpy.argmax(radius_n)        
  1508.         rmax = radius_n[maxi]
  1509.         # void points around maxi and mini to make sure the 2nd max is found
  1510.         # on the "other" side
  1511.         n = len(radius_n)
  1512.         radius_n[maxi]=0        
  1513.         radius_n[mini]=0        
  1514.         for i in range(1,n/8+1):
  1515.             radius_n[(maxi+i)%n]=0
  1516.             radius_n[(maxi-i)%n]=0
  1517.             radius_n[(mini+i)%n]=0
  1518.             radius_n[(mini-i)%n]=0
  1519.         radius_n_2 = [ r for r in radius_n if r>0]
  1520.         rmax_2 = max(radius_n_2)
  1521.         rmin_2 = min(radius_n_2) # not good !!
  1522.         anglemax = numpy.arccos( radius[maxi][0]/rmax)*numpy.sign(radius[maxi][1])
  1523.         return True, (xmin+w*0.5,ymin+h*0.5, 0.5*(rmin+rmin_2), 0.5*(rmax+rmax_2), anglemax)
  1524.  
  1525.  
  1526.  
  1527.  
  1528.     def tangentEnvelop(self, svgCommandsList, refNode):
  1529.         a, svgCommandsList = toArray(svgCommandsList)
  1530.         tangents = buildTangents(a)
  1531.  
  1532.         newSegs = [ Segment.fromCenterAndDir( p, t ) for (p,t) in zip(a,tangents) ]
  1533.         debug("build envelop ", newSegs[0].point1 , newSegs[0].pointN)
  1534.         clustersInd = clusterAngles( [s.angle for s in newSegs] )
  1535.         debug("build envelop cluster:  ", clustersInd)
  1536.  
  1537.         return TangentEnvelop( newSegs, svgCommandsList, refNode)
  1538.  
  1539.  
  1540.     def segsFromTangents(self,svgCommandsList, refNode):
  1541.         """Finds segments part in a list of points represented by svgCommandsList.
  1542.  
  1543.        The method is to build the (averaged) tangent vectors to the curve.
  1544.        Aligned points will have tangent with similar angle, so we cluster consecutive angles together
  1545.        to define segments.
  1546.        Then we extend segments to connected points not already part of other segments.
  1547.        Then we merge consecutive segments with similar angles.
  1548.        
  1549.        """
  1550.         sourcepoints, svgCommandsList = toArray(svgCommandsList)
  1551.  
  1552.         d = D(sourcepoints[0],sourcepoints[-1])
  1553.         x,y,wTot,hTot = computeBox(sourcepoints)
  1554.         aR = min(wTot/hTot, hTot/wTot)
  1555.         maxDim = max(wTot, hTot)
  1556.         # was 0.2
  1557.         isClosing = aR*0.5 > d/maxDim
  1558.         debug('isClosing ', isClosing, maxDim, d)
  1559.         if d==0:
  1560.             # then we remove the last point to avoid null distance
  1561.             # in other calculations
  1562.             sourcepoints = sourcepoints[:-1]
  1563.             svgCommandsList = svgCommandsList[:-1]
  1564.  
  1565.         if len(sourcepoints) < 4:
  1566.             return PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=isClosing)
  1567.        
  1568.         tangents = buildTangents(sourcepoints, isClosing=isClosing)
  1569.  
  1570.         # global quantities :
  1571.  
  1572.         # Check if circle -----------------------
  1573.         if isClosing:
  1574.             if len(sourcepoints)<9:
  1575.                 return PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=True)
  1576.             isCircle, res = self.checkForCircle( sourcepoints, tangents)        
  1577.             debug("Is Circle = ", isCircle )
  1578.             if isCircle:
  1579.                 x,y,rmin, rmax,angle = res
  1580.                 debug("Circle -> ", rmin, rmax,angle )
  1581.                 if rmin/rmax>0.7:
  1582.                     circ = Circle((x,y),0.5*(rmin+rmax),  refNode )
  1583.                 else:
  1584.                     circ = Circle((x,y),rmin,  refNode, rmax=rmax, angle=angle)
  1585.                 circ.points = sourcepoints
  1586.                 return circ
  1587.         # -----------------------
  1588.            
  1589.  
  1590.  
  1591.         # cluster points by angle of their tangents -------------
  1592.         tgSegs = [ Segment.fromCenterAndDir( p, t ) for (p,t) in zip(sourcepoints,tangents) ]
  1593.         clustersInd = clusterAngles( [s.angle for s in tgSegs] )
  1594.         clustersInd.sort( )
  1595.         debug("build envelop cluster:  ", clustersInd)
  1596.  
  1597.         # build Segments from clusters
  1598.         newSegs = []
  1599.         for imin, imax in clustersInd:
  1600.             if imin+1< imax: # consider clusters with more than 3 points
  1601.                 seg = fitSingleSegment(sourcepoints[imin:imax+1])
  1602.             elif imin+1==imax: # 2 point path : we build a segment
  1603.                 seg = Segment.from2Points(sourcepoints[imin], sourcepoints[imax] , sourcepoints[imin:imax+1])
  1604.             else:
  1605.                 seg = Path( sourcepoints[imin:imax+1] )
  1606.             seg.sourcepoints = sourcepoints
  1607.             newSegs.append( seg )
  1608.         resetPrevNextSegment( newSegs )
  1609.         debug(newSegs)
  1610.         # -----------------------
  1611.  
  1612.  
  1613.         # -----------------------
  1614.         # Merge consecutive Path objects
  1615.         updatedSegs=[]
  1616.         def toMerge(p):
  1617.             l=[p]
  1618.             setattr(p, 'merged', True)
  1619.             if p.next and not p.next.isSegment():
  1620.                 l += toMerge(p.next)
  1621.             return l
  1622.        
  1623.         for i,seg in enumerate(newSegs[:-1]):
  1624.             if seg.isSegment():
  1625.                 updatedSegs.append( seg)                
  1626.                 continue
  1627.             if hasattr(seg,'merged'): continue
  1628.             mergeList = toMerge(seg)
  1629.             debug('merging ', mergeList)
  1630.             p = Path(numpy.concatenate([ p.points for p in mergeList]) )
  1631.             debug('merged == ', p.points)
  1632.             updatedSegs.append(p)
  1633.  
  1634.         if not hasattr(newSegs[-1],'merged'): updatedSegs.append( newSegs[-1])
  1635.         debug("merged path", updatedSegs)
  1636.         newSegs = resetPrevNextSegment( updatedSegs )
  1637.  
  1638.  
  1639.         # Extend segments -----------------------------------
  1640.         if self.options.segExtensionEnable:
  1641.             newSegs = SegmentExtender.extendSegments( newSegs, self.options.segExtensionDtoSeg, self.options.segExtensionQual )
  1642.             debug("extended segs", newSegs)
  1643.             newSegs = resetPrevNextSegment( newSegs )
  1644.             debug("extended segs", newSegs)
  1645.  
  1646.         # ----------------------------------------
  1647.            
  1648.  
  1649.         # ---------------------------------------
  1650.         # merge consecutive segments with close angle
  1651.         updatedSegs=[]
  1652.  
  1653.         if self.options.segAngleMergeEnable:
  1654.             newSegs = mergeConsecutiveCloseAngles( newSegs , mangle=self.options.segAngleMergeTol1 )
  1655.             newSegs=resetPrevNextSegment(newSegs)
  1656.             debug(' __ 2nd angle merge')
  1657.             newSegs = mergeConsecutiveCloseAngles( newSegs, mangle=self.options.segAngleMergeTol2 ) # 2nd pass
  1658.             newSegs=resetPrevNextSegment(newSegs)
  1659.             debug('after merge ', len(newSegs), newSegs)
  1660.             # Check if first and last also have close angles.
  1661.             if isClosing and len(newSegs)>2 :
  1662.                 first ,last = newSegs[0], newSegs[-1]
  1663.                 if first.isSegment() and last.isSegment():
  1664.                     if closeAngleAbs( first.angle, last.angle) < 0.1:
  1665.                         # force merge
  1666.                         points= numpy.concatenate( [  last.points, first.points] )
  1667.                         newseg = fitSingleSegment(points)
  1668.                         newseg.next = first.next
  1669.                         last.prev.next = None
  1670.                         newSegs[0]=newseg
  1671.                         newSegs.pop()
  1672.  
  1673.         # -----------------------------------------------------
  1674.         # remove negligible Path/Segments between 2 large Segments
  1675.         if self.options.segRemoveSmallEdge:
  1676.             self.removeSmallEdge(newSegs , wTot, hTot)
  1677.             newSegs=resetPrevNextSegment(newSegs)
  1678.  
  1679.             debug('after remove small ', len(newSegs),newSegs)
  1680.         # -----------------------------------------------------
  1681.  
  1682.         # -----------------------------------------------------
  1683.         # Extend segments to their intersections
  1684.         for p in newSegs:
  1685.             if p.isSegment() and p.next:
  1686.                 p.setIntersectWithNext()
  1687.         # -----------------------------------------------------
  1688.        
  1689.         return PathGroup(newSegs, svgCommandsList, refNode, isClosing)
  1690.  
  1691.  
  1692.  
  1693.     def extractShapesFromID( self, *nids, **options ):
  1694.         """for debugging purpose """
  1695.         eList = []
  1696.         for nid in nids:
  1697.             el = self.getElementById(nid)
  1698.             if el is None:
  1699.                 print "Cant find ", nid
  1700.                 return
  1701.             eList.append(el)
  1702.         class tmp:
  1703.             pass
  1704.  
  1705.         self.options = self.OptionParser.parse_args()[0]
  1706.         self.options._update_careful(options)
  1707.         nodes=self.extractShapes(eList)
  1708.         self.shape = nodes[0]
  1709.  
  1710.  
  1711.     def buildShape(self, node):
  1712.         def rotationAngle(tr):
  1713.             if tr and tr.startswith('rotate'):
  1714.                 # retrieve the angle :
  1715.                 return float(tr[7:-1].split(','))
  1716.             else:
  1717.                 return 0.
  1718.            
  1719.         if node.tag.endswith('path'):
  1720.             parsedSVGCommands = node.get('d')
  1721.             g = self.segsFromTangents(simplepath.parsePath(parsedSVGCommands), node)
  1722.             #g = self.tangentEnvelop(simplepath.parsePath(parsedSVGCommands), node)
  1723.         elif node.tag.endswith('rect'):
  1724.             tr = node.get('transform',None)
  1725.             if tr and tr.startswith('matrix'):
  1726.                 return None # can't deal with scaling
  1727.             recSize = numpy.array([node.get('width'),node.get('height')])
  1728.             recCenter = numpy.array([node.get('x'),node.get('y')]) + recSize/2
  1729.             angle=rotationAngle(tr)
  1730.             g = Rectangle( recSize, recCenter, 0 , [], node)
  1731.         elif node.tag.endswith('circle'):
  1732.             g = Circle(node.get('cx'),node.get('cy'), node.get('r'), [], node )
  1733.         elif node.tag.endswith('ellipse'):
  1734.             if tr and tr.startswith('matrix'):
  1735.                 return None # can't deal with scaling
  1736.             angle=rotationAngle(tr)
  1737.             rx = node.get('rx')
  1738.             ry = node.get('ry')
  1739.             g = Circle(node.get('cx'),node.get('cy'), ry, rmax=rx , angle=angle, refNode=node )
  1740.  
  1741.         return g
  1742.    
  1743.     def extractShapes( self, nodes ):
  1744.         """The main function.
  1745.        nodes : a list of nodes"""
  1746.         analyzedNodes = []
  1747.  
  1748.         # convert nodes to list of segments (PathGroup) or Circle
  1749.         for n in nodes :
  1750.             g = self.buildShape(n)
  1751.             if g :
  1752.                 analyzedNodes.append( g )
  1753.  
  1754.         # uniformize shapes
  1755.         if self.options.doUniformization:
  1756.             analyzedNodes = self.uniformizeShapes(analyzedNodes)
  1757.  
  1758.         return analyzedNodes
  1759.  
  1760.     def mergeConsecutiveParralels(self, segments):
  1761.         ignoreNext=False
  1762.         newList=[]
  1763.         for s in segments:
  1764.             if ignoreNext:
  1765.                 ignoreNext=False
  1766.                 continue
  1767.             if not s.isSegment():
  1768.                 newList.append(s)
  1769.                 continue
  1770.             if s.next is None:
  1771.                 newList.append(s)
  1772.                 continue
  1773.             if not s.next.isSegment():
  1774.                 newList.append(s)
  1775.                 continue
  1776.             d = closeAngleAbs(s.angle ,s.next.angle)
  1777.             if d<self.options.segAngleMergePara:
  1778.                 debug("merging ", s.angle ,s.next.angle )
  1779.                 snew = s.mergedWithNext(doRefit=False)
  1780.                 ignoreNext=True
  1781.                 newList.append(snew)
  1782.             else:
  1783.                 debug("notmerging ", s.angle ,s.next.angle )
  1784.                 newList.append(s)
  1785.         if len(segments)>len(newList):
  1786.             debug("merged parallel ", segments , '-->', newList)
  1787.         return newList
  1788.    
  1789.     def uniformizeShapes(self, pathGroupList):
  1790.         allSegs = [ p  for g in pathGroupList for p in g.listOfPaths if p.isSegment() ]
  1791.  
  1792.         if self.options.doParrallelize:
  1793.             self.prepareParrallelize(allSegs)
  1794.         if self.options.doKnownAngle:
  1795.             self.adjustToKnownAngle(allSegs)
  1796.  
  1797.         adjustAng = self.options.doKnownAngle or self.options.doParrallelize
  1798.        
  1799.         allShapeDist = []
  1800.         for g in [ group for group in pathGroupList if not isinstance(group, Circle)]:
  1801.                 # first pass : independently per path
  1802.                 if adjustAng:
  1803.                     adjustAllAngles(g.listOfPaths)
  1804.                     g.listOfPaths[:] = self.mergeConsecutiveParralels(g.listOfPaths)
  1805.                 if self.options.doEqualizeDist:
  1806.                     allShapeDist=allShapeDist + self.prepareDistanceEqualization([p for p in g.listOfPaths if p.isSegment()],self.options.shapeDistLocal ) ##0.30
  1807.         ##
  1808.                     adjustAllDistances([p for p in group.listOfPaths if p.isSegment()])            
  1809.         ## # then 2nd global pass, with tighter criteria
  1810.         if self.options.doEqualizeDist:
  1811.             allShapeDist=self.prepareDistanceEqualization(allSegs, self.options.shapeDistGlobal) ##0.08
  1812.             for g in [ group for group in pathGroupList if not isinstance(group, Circle)]:
  1813.                 adjustAllDistances([p for p in g.listOfPaths if p.isSegment()])
  1814.            
  1815.         #TODO: I think this is supposed to close thje paths and it is failing
  1816.         for g in pathGroupList:
  1817.             if g.isClosing and not isinstance(g,Circle):
  1818.                 debug('Closing intersec ', g.listOfPaths[0].point1, g.listOfPaths[0].pointN )
  1819.                 g.listOfPaths[-1].setIntersectWithNext(g.listOfPaths[0])  
  1820.  
  1821.  
  1822.         circles=[ group for group in pathGroupList if isinstance(group, Circle)]
  1823.         if self.options.doEqualizeRadius:
  1824.             self.prepareRadiusEqualization(circles, allShapeDist)
  1825.         if self.options.doCenterCircOnSeg:
  1826.             self.centerCircOnSeg(circles, allSegs)
  1827.  
  1828.         pathGroupList = [toRemarkableShape(g) for g in pathGroupList]
  1829.         return pathGroupList
  1830.        
  1831.        
  1832.     def addShapesToDoc(self, pathGroupList):
  1833.         for group in pathGroupList:            
  1834.             debug("final ", group.listOfPaths, group.refNode )
  1835.             # change to Rectangle if possible :
  1836.             #finalshape = toRemarkableShape( group )
  1837.             ele = group.addToNode( group.refNode)
  1838.             group.setNodeStyle(ele, group.refNode)
  1839.             if not self.options.keepOrigin:
  1840.                 group.refNode.xpath('..')[0].remove(group.refNode)
  1841.  
  1842.  
  1843.        
  1844. if __name__ == '__main__':
  1845.     e = ShapeReco()
  1846.     e.affect()
  1847.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement