diff --git a/extensions/fablabchemnitz/shapereco/shapereco.inx b/extensions/fablabchemnitz/shapereco/shapereco.inx
new file mode 100644
index 00000000..718572da
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shapereco.inx
@@ -0,0 +1,52 @@
+
+
+ Shape Recognition
+ fablabchemnitz.de.shape_recognition
+
+
+
+ false
+ true
+
+
+
+ true
+ 10.0
+ 0.2
+
+ true
+
+
+
+ true
+ 0.500
+ 0.48
+ 0.50
+
+
+ true
+ 0.3
+ 0.025
+
+
+ true
+ true
+ true
+ true
+
+
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shapereco/shapereco.py b/extensions/fablabchemnitz/shapereco/shapereco.py
new file mode 100644
index 00000000..1fdbbfe6
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shapereco.py
@@ -0,0 +1,836 @@
+#!/usr/bin/env python
+'''
+Copyright (C) 2017 , Pierre-Antoine Delsart
+
+This file is part of InkscapeShapeReco.
+
+InkscapeShapeReco 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 InkscapeShapeReco; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+
+Quick description:
+This extension uses all selected path, ignoring all other selected objects.
+It tries to regularize hand drawn paths BY :
+ - evaluating if the path is a full circle or ellipse
+ - else finding sequences of aligned points and replacing them by a simple segment.
+ - changing the segments angles to the closest remarkable angle (pi/2, pi/3, pi/6, etc...)
+ - eqalizing all segments lengths which are close to each other
+ - replacing 4 segments paths by a rectangle object if this makes sens (giving the correct rotation to the rectangle).
+
+Requires numpy.
+
+'''
+
+import sys
+sys.path.append('/usr/share/inkscape/extensions')
+import inkex
+import gettext
+_ = gettext.gettext
+
+# *************************************************************
+# debugging
+def void(*l):
+ pass
+def debug_on(*l):
+ sys.stderr.write(' '.join(str(i) for i in l) +'\n')
+debug = void
+#debug = debug_on
+
+from shaperrec import geometric
+from shaperrec import internal
+from shaperrec import groups
+from shaperrec import manipulation
+from shaperrec import extenders
+from shaperrec import miscellaneous
+
+import numpy
+numpy.set_printoptions(precision=3)
+
+class PreProcess():
+
+ def removeSmallEdge(paths, wTot, hTot):
+ """Remove small Path objects which stand between 2 Segments (or at the ends of the sequence).
+ Small means the bbox of the path is less then 5% of the mean of the 2 segments."""
+ if len(paths)<2:
+ return
+ def getdiag(points):
+ xmin, ymin, w, h = geometric.computeBox(points)
+ return numpy.sqrt(w**2+h**2), w, h
+ removeSeg=[]
+ def remove(p):
+ removeSeg.append(p)
+ if hasattr(p, "__next__") : p.next.prev = p.prev
+ if p.prev: p.prev.next = p.__next__ if hasattr(p, "__next__") else None
+ p.effectiveNPoints =0
+ debug(' --> remove !', p, p.length, len(p.points))
+ for p in paths:
+ if len(p.points)==0 :
+ remove(p)
+ continue
+ # select only path between 2 segments
+ next, prev = p.__next__ if hasattr(p, "__next__") else None, p.prev
+ if next is None: next = prev
+ if prev is None: prev = next
+ if not (False if next == None else next.isSegment()) or not (False if prev == None else prev.isSegment()) : continue
+ #diag = getdiag(p.points)
+ diag, w, h = getdiag(p.points)
+
+ debug(p, p.pointN, ' removing edge diag = ', diag, p.length, ' l=', next.length+prev.length, 'totDim ', (wTot, hTot))
+ debug( ' ---> ', prev, next)
+
+ #t TODO: his needs to be parameterized
+ # remove last or first very small in anycase
+ doRemove = prev==next and (diag < 0.05*(wTot+hTot)*0.5 )
+ if not doRemove:
+ # check if this small
+ isLarge = diag > (next.length+prev.length)*0.1 # check size relative to neighbour
+ isLarge = isLarge or w > 0.2*wTot or h > 0.2*hTot # check size w.r.t total size
+
+ # is it the small side of a long rectangle ?
+ dd = prev.distanceTo(next.pointN)
+ rect = abs(prev.unitv.dot(next.unitv))>0.98 and diag > dd*0.5
+ doRemove = not( isLarge or rect )
+
+ if doRemove:
+ remove(p)
+
+ if next != prev:
+ prev.setIntersectWithNext(next)
+ debug('removed Segments ', removeSeg)
+ for p in removeSeg:
+ paths.remove(p)
+
+ def prepareParrallelize( segs):
+ """Group Segment by their angles (segments are grouped together if their deltAangle is within 0.15 rad)
+ The 'newAngle' member of segments in a group are then set to the mean angle of the group (where angles are all
+ considered in [-pi, pi])
+
+ segs : list of segments
+ """
+
+ angles = numpy.array([s.angle for s in segs ])
+ angles[numpy.where(angles<0)] += geometric._pi # we care about direction, not angle orientation
+ clList = miscellaneous.clusterValues(angles, 0.30, refScaleAbs='abs')#was 15
+
+ pi = numpy.pi
+ for cl in clList:
+ anglecount = {}
+ for angle in angles[list(cl)]:
+ # #angleDeg = int(angle * 360.0 / (2.0*pi))
+ if not angle in anglecount:
+ anglecount[angle] = 1
+ else:
+ anglecount[angle] += 1
+
+ anglecount = {k: v for k, v in sorted(list(anglecount.items()), key=lambda item: item[1], reverse=True)}
+ meanA = anglecount.popitem()[0]#.items()[1]#sorted(anglecount.items(), key = lambda kv:(kv[1], kv[0]), reverse=True)[1][1]
+ #meanA = float(meanA) * (2.0*pi) / 360.0
+ #meanA = angles[list(cl)].mean()
+ for i in cl:
+ seg = segs[i]
+ seg.newAngle = meanA if seg.angle>=0. else meanA-geometric._pi
+
+
+ def prepareDistanceEqualization(segs, relDelta=0.1):
+ """ Input segments are grouped according to their length :
+ - for each length L, find all other lengths within L*relDelta. of L.
+ - Find the larger of such subgroup.
+ - repeat the procedure on remaining lengths until none is left.
+ Each length in a group is set to the mean length of the group
+
+ segs : a list of segments
+ relDelta : float, minimum relative distance.
+ """
+
+ lengths = numpy.array( [x.tempLength() for x in segs] )
+ clusters = miscellaneous.clusterValues(lengths, relDelta)
+
+ if len(clusters)==1:
+ # deal with special case with low num of segments
+ # --> don't let a single segment alone
+ if len(clusters[0])+1==len(segs):
+ clusters[0]=list(range(len(segs))) # all
+
+ allDist = []
+ for cl in clusters:
+ dmean = sum( lengths[i] for i in cl ) / len(cl)
+ allDist.append(dmean)
+ for i in cl:
+ segs[i].setNewLength(dmean)
+ debug( i, ' set newLength ', dmean, segs[i].length, segs[i].dumpShort())
+
+ return allDist
+
+
+ def prepareRadiusEqualization(circles, otherDists, relSize=0.2):
+ """group circles radius and distances into cluster.
+ Then set circles radius according to the mean of the clusters they belong to."""
+ ncircles = len(circles)
+ lengths = numpy.array( [c.radius for c in circles]+otherDists )
+ indices = numpy.array( list(range(ncircles+len(otherDists))) )
+ clusters = miscellaneous.clusterValues(numpy.stack([ lengths, indices ], 1 ), relSize, refScaleAbs='local' )
+
+ debug('prepareRadiusEqualization radius ', repr(lengths))
+ debug('prepareRadiusEqualization clusters ', clusters)
+ allDist = []
+ for cl in clusters:
+ dmean = sum( lengths[i] for i in cl ) / len(cl)
+ #print cl , dmean ,
+ allDist.append(dmean)
+ if len(cl)==1:
+ continue
+ for i in cl:
+ if i< ncircles:
+ circles[i].radius = dmean
+ debug(' post radius ', [c.radius for c in circles] )
+ return allDist
+
+
+ def centerCircOnSeg(circles, segments, relSize=0.18):
+ """ move centers of circles onto the segments if close enough"""
+ for circ in circles:
+ circ.moved = False
+ for seg in segments:
+ for circ in circles:
+ d = seg.distanceTo(circ.center)
+ #debug( ' ', seg.projectPoint(circ.center))
+ if d < circ.radius*relSize and not circ.moved :
+ circ.center = seg.projectPoint(circ.center)
+ circ.moved = True
+
+
+ def adjustToKnownAngle( paths):
+ """ Check current angle against remarkable angles. If close enough, change it
+ paths : a list of segments"""
+ for seg in paths:
+ a = seg.tempAngle()
+ i = (abs(geometric.vec_in_mPi_pPi(geometric.knownAngle - a) )).argmin()
+ seg.newAngle = geometric.knownAngle[i]
+ debug( ' Known angle ', seg, seg.tempAngle(), ' -> ', geometric.knownAngle[i])
+ ## if abs(geometric.knownAngle[i] - a) < 0.08:
+
+class PostProcess():
+ def mergeConsecutiveParralels(segments, options):
+ ignoreNext=False
+ newList=[]
+ for s in segments:
+ if ignoreNext:
+ ignoreNext=False
+ continue
+ if not s.isSegment():
+ newList.append(s)
+ continue
+ if not hasattr(s, "__next__"):
+ newList.append(s)
+ continue
+ if not s.next.isSegment():
+ newList.append(s)
+ continue
+ d = geometric.closeAngleAbs(s.angle, s.next.angle)
+ if d < options.segAngleMergePara:
+ debug("merging ", s.angle, s.next.angle )
+ snew = s.mergedWithNext(doRefit=False)
+ ignoreNext=True
+ newList.append(snew)
+ else:
+ debug("notmerging ", s.angle, s.next.angle )
+ newList.append(s)
+ if len(segments)>len(newList):
+ debug("merged parallel ", segments, '-->', newList)
+ return newList
+
+ def uniformizeShapes(pathGroupList, options):
+ allSegs = [ p for g in pathGroupList for p in g.listOfPaths if p.isSegment() ]
+
+ if options.doParrallelize:
+ PreProcess.prepareParrallelize(allSegs)
+ if options.doKnownAngle:
+ PreProcess.adjustToKnownAngle(allSegs)
+
+ adjustAng = options.doKnownAngle or options.doParrallelize
+
+ allShapeDist = []
+ for g in [ group for group in pathGroupList if not isinstance(group, groups.Circle)]:
+ # first pass : independently per path
+ if adjustAng:
+ manipulation.adjustAllAngles(g.listOfPaths)
+ g.listOfPaths[:] = PostProcess.mergeConsecutiveParralels(g.listOfPaths, options)
+ if options.doEqualizeDist:
+ allShapeDist=allShapeDist + PreProcess.prepareDistanceEqualization([p for p in g.listOfPaths if p.isSegment()], options.shapeDistLocal ) ##0.30
+ manipulation.adjustAllDistances([p for p in g.listOfPaths if p.isSegment()]) #findme was group.li..
+
+ ## # then 2nd global pass, with tighter criteria
+ if options.doEqualizeDist:
+ allShapeDist=PreProcess.prepareDistanceEqualization(allSegs, options.shapeDistGlobal) ##0.08
+ for g in [ group for group in pathGroupList if not isinstance(group, groups.Circle)]:
+ manipulation.adjustAllDistances([p for p in g.listOfPaths if p.isSegment()])
+
+ #TODO: I think this is supposed to close thje paths and it is failing
+ for g in pathGroupList:
+ if g.isClosing and not isinstance(g, groups.Circle):
+ debug('Closing intersec ', g.listOfPaths[0].point1, g.listOfPaths[0].pointN )
+ g.listOfPaths[-1].setIntersectWithNext(g.listOfPaths[0])
+
+
+ circles=[ group for group in pathGroupList if isinstance(group, groups.Circle)]
+ if options.doEqualizeRadius:
+ PreProcess.prepareRadiusEqualization(circles, allShapeDist)
+ if options.doCenterCircOnSeg:
+ PreProcess.centerCircOnSeg(circles, allSegs)
+
+ pathGroupList = [manipulation.toRemarkableShape(g) for g in pathGroupList]
+ return pathGroupList
+
+class FitShapes():
+ def checkForCircle(points, tangents):
+ """Determine if the points and their tangents represent a circle
+
+ The difficulty is to be able to recognize ellipse while avoiding paths small fluctuations a
+ nd false positive due to badly drawn rectangle or non-convex closed curves.
+
+ Method : we consider angle of tangent as function of lenght on path.
+ For circles these are : angle = c1 x lenght + c0. (c1 ~1)
+
+ We calculate dadl = d(angle)/d(length) and compare to c1.
+ We use 3 criteria :
+ * num(dadl > 6) : number of sharp angles
+ * length(dadl<0.3)/totalLength : lengths of straight lines within the path.
+ * totalLength/(2pi x radius) : fraction of lenght vs a plain circle
+
+ Still failing to recognize elongated ellipses...
+
+ """
+ if len(points)<10:
+ return False, 0
+
+ if all(points[0]==points[-1]): # last exactly equals the first.
+ # Ignore last point for this check
+ points = points[:-1]
+ tangents = tangents[:-1]
+ #print 'Removed last ', points
+ xmin, ymin, w, h = geometric.computeBox( points)
+ diag2=(w*w+h*h)
+
+ diag = numpy.sqrt(diag2)*0.5
+ norms = numpy.sqrt(numpy.sum( tangents**2, 1 ))
+
+ angles = numpy.arctan2( tangents[:, 1], tangents[:, 0] )
+ #debug( 'angle = ', repr(angles))
+ N = len(angles)
+
+ deltas = points[1:] - points[:-1]
+ deltasD = numpy.concatenate([ [geometric.D(points[0], points[-1])/diag], numpy.sqrt(numpy.sum( deltas**2, 1 )) / diag] )
+
+ # locate and avoid the point when swicthing
+ # from -pi to +pi. The point is around the minimum
+ imin = numpy.argmin(angles)
+ debug(' imin ', imin)
+ angles = numpy.roll(angles, -imin)
+ deltasD = numpy.roll(deltasD, -imin)
+ n=int(N*0.1)
+ # avoid fluctuations by removing points around the min
+ angles=angles[n:-n]
+ deltasD=deltasD[n:-n]
+ deltasD = deltasD.cumsum()
+ N = len(angles)
+
+ # smooth angles to avoid artificial bumps
+ angles = manipulation.smoothArray(angles, n=max(int(N*0.03), 2) )
+
+ deltaA = angles[1:] - angles[:-1]
+ deltasDD = (deltasD[1:] -deltasD[:-1])
+ deltasDD[numpy.where(deltasDD==0.)] = 1e-5*deltasD[0]
+ dAdD = abs(deltaA/deltasDD)
+ belowT, count = True, 0
+ for v in dAdD:
+ if v>6 and belowT:
+ count+=1
+ belowT = False
+ belowT= (v<6)
+
+ temp = (deltasD, angles, tangents, dAdD )
+ fracStraight = numpy.sum(deltasDD[numpy.where(dAdD<0.3)])/(deltasD[-1]-deltasD[0])
+ curveLength = deltasD[-1]/3.14
+ #print "SSS ",count , fracStraight
+ if curveLength> 1.4 or fracStraight>0.4 or count > 6:
+ isCircle =False
+ else:
+ isCircle= (count < 4 and fracStraight<=0.3) or \
+ (fracStraight<=0.1 and count<5)
+
+ if not isCircle:
+ return False, 0
+
+ # It's a circle !
+ radius = points - numpy.array([xmin+w*0.5, ymin+h*0.5])
+ radius_n = numpy.sqrt(numpy.sum( radius**2, 1 )) # normalize
+
+ mini = numpy.argmin(radius_n)
+ rmin = radius_n[mini]
+ maxi = numpy.argmax(radius_n)
+ rmax = radius_n[maxi]
+ # void points around maxi and mini to make sure the 2nd max is found
+ # on the "other" side
+ n = len(radius_n)
+ radius_n[maxi]=0
+ radius_n[mini]=0
+ for i in range(1, int(n/8+1)):
+ radius_n[(maxi+i)%n]=0
+ radius_n[(maxi-i)%n]=0
+ radius_n[(mini+i)%n]=0
+ radius_n[(mini-i)%n]=0
+ radius_n_2 = [ r for r in radius_n if r>0]
+ rmax_2 = max(radius_n_2)
+ rmin_2 = min(radius_n_2) # not good !!
+ anglemax = numpy.arccos( radius[maxi][0]/rmax)*numpy.sign(radius[maxi][1])
+ return True, (xmin+w*0.5, ymin+h*0.5, 0.5*(rmin+rmin_2), 0.5*(rmax+rmax_2), anglemax)
+
+
+ def checkForArcs(points, tangents):
+ """Determine if the points and their tangents represent a circle
+
+ The difficulty is to be able to recognize ellipse while avoiding paths small fluctuations a
+ nd false positive due to badly drawn rectangle or non-convex closed curves.
+
+ Method : we consider angle of tangent as function of lenght on path.
+ For circles these are : angle = c1 x lenght + c0. (c1 ~1)
+
+ We calculate dadl = d(angle)/d(length) and compare to c1.
+ We use 3 criteria :
+ * num(dadl > 6) : number of sharp angles
+ * length(dadl<0.3)/totalLength : lengths of straight lines within the path.
+ * totalLength/(2pi x radius) : fraction of lenght vs a plain circle
+
+ Still failing to recognize elongated ellipses...
+
+ """
+ if len(points)<10:
+ return False, 0
+
+ if all(points[0]==points[-1]): # last exactly equals the first.
+ # Ignore last point for this check
+ points = points[:-1]
+ tangents = tangents[:-1]
+ print(('Removed last ', points))
+ xmin, ymin, w, h = geometric.computeBox( points)
+ diag2=(w*w+h*h)
+
+ diag = numpy.sqrt(diag2)*0.5
+ norms = numpy.sqrt(numpy.sum( tangents**2, 1 ))
+
+ angles = numpy.arctan2( tangents[:, 1], tangents[:, 0] )
+ #debug( 'angle = ', repr(angles))
+ N = len(angles)
+
+ deltas = points[1:] - points[:-1]
+ deltasD = numpy.concatenate([ [geometric.D(points[0], points[-1])/diag], numpy.sqrt(numpy.sum( deltas**2, 1 )) / diag] )
+
+ # locate and avoid the point when swicthing
+ # from -pi to +pi. The point is around the minimum
+ imin = numpy.argmin(angles)
+ debug(' imin ', imin)
+ angles = numpy.roll(angles, -imin)
+ deltasD = numpy.roll(deltasD, -imin)
+ n=int(N*0.1)
+ # avoid fluctuations by removing points around the min
+ angles=angles[n:-n]
+ deltasD=deltasD[n:-n]
+ deltasD = deltasD.cumsum()
+ N = len(angles)
+
+ # smooth angles to avoid artificial bumps
+ angles = manipulation.smoothArray(angles, n=max(int(N*0.03), 2) )
+
+ deltaA = angles[1:] - angles[:-1]
+ deltasDD = (deltasD[1:] -deltasD[:-1])
+ deltasDD[numpy.where(deltasDD==0.)] = 1e-5*deltasD[0]
+ dAdD = abs(deltaA/deltasDD)
+ belowT, count = True, 0
+
+
+ self.temp = (deltasD, angles, tangents, dAdD )
+ #TODO: Loop over deltasDD searching for curved segments, no sharp bumps and a curve of at least 1/4 pi
+ curveStart = 0
+ curveToTest= numpy.array([deltasDD[curveStart]]);
+ dAdDd = numpy.array([dAdD[curveStart]])
+ v = dAdD[curveStart]
+ belowT= (v<6)
+ for i in range(1, deltasDD.size):
+ curveToTest = numpy.append(curveToTest, deltasDD[i])
+ dAdDd = numpy.append(dAdDd, dAdD[i])
+ fracStraight = numpy.sum(curveToTest[numpy.where(dAdDd<0.3)])/(deltasD[i]-deltasD[curveStart])
+ curveLength = (deltasD[i]-deltasD[curveStart])/3.14
+
+ v = dAdD[i]
+ if v>6 and belowT:
+ count+=1
+ belowT = False
+ belowT= (v<6)
+ inkex.debug("SSS "+str(count) +":"+ str(fracStraight))
+ if curveLength> 1.4 or fracStraight>0.4 or count > 8:
+ inkex.debug("curveLengtha:" + str(curveLength) +"fracStraight:"+str(fracStraight)+"count:"+str(count))
+ isArc=False
+ curveStart=int(i)
+ curveToTest= numpy.array([deltasDD[curveStart]]);
+ v = dAdD[curveStart]
+ dAdDd = numpy.array([dAdD[curveStart]])
+ belowT= (v<6)
+ count = 0
+ continue
+ else:
+ inkex.debug("curveLengthb:" + str(curveLength) +"fracStraight:"+str(fracStraight)+"count:"+str(count))
+ isArc= (count < 4 and fracStraight<=0.3) or \
+ (fracStraight<=0.1 and count<5)
+
+ if not isArc:
+ return False, 0
+
+ # It's a circle !
+ radius = points - numpy.array([xmin+w*0.5, ymin+h*0.5])
+ radius_n = numpy.sqrt(numpy.sum( radius**2, 1 )) # normalize
+
+ mini = numpy.argmin(radius_n)
+ rmin = radius_n[mini]
+ maxi = numpy.argmax(radius_n)
+ rmax = radius_n[maxi]
+ # void points around maxi and mini to make sure the 2nd max is found
+ # on the "other" side
+ n = len(radius_n)
+ radius_n[maxi]=0
+ radius_n[mini]=0
+ for i in range(1, int(n/8+1)):
+ radius_n[(maxi+i)%n]=0
+ radius_n[(maxi-i)%n]=0
+ radius_n[(mini+i)%n]=0
+ radius_n[(mini-i)%n]=0
+ radius_n_2 = [ r for r in radius_n if r>0]
+ rmax_2 = max(radius_n_2)
+ rmin_2 = min(radius_n_2) # not good !!
+ anglemax = numpy.arccos( radius[maxi][0]/rmax)*numpy.sign(radius[maxi][1])
+ return True, (xmin+w*0.5, ymin+h*0.5, 0.5*(rmin+rmin_2), 0.5*(rmax+rmax_2), anglemax)
+
+
+
+
+ def tangentEnvelop(svgCommandsList, refNode, options):
+ a, svgCommandsList = geometric.toArray(svgCommandsList)
+ tangents = manipulation.buildTangents(a)
+
+ newSegs = [ internal.Segment.fromCenterAndDir( p, t ) for (p, t) in zip(a, tangents) ]
+ debug("build envelop ", newSegs[0].point1, newSegs[0].pointN)
+ clustersInd = manipulation.clusterAngles( [s.angle for s in newSegs] )
+ debug("build envelop cluster: ", clustersInd)
+
+ return TangentEnvelop( newSegs, svgCommandsList, refNode)
+
+ def isClosing(wTot, hTot, d):
+ aR = min(wTot/hTot, hTot/wTot)
+ maxDim = max(wTot, hTot)
+ # was 0.2
+ return aR*0.5 > d/maxDim
+
+
+ def curvedFromTangents(svgCommandsList, refNode, x, y, wTot, hTot, d, isClosing, sourcepoints, tangents, options):
+
+# debug('isClosing ', isClosing, maxDim, d)
+
+ # global quantities :
+ hasArcs = False
+ res = ()
+ # Check if circle -----------------------
+ if isClosing:
+ if len(sourcepoints)<9:
+ return groups.PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=True)
+ isCircle, res = FitShapes.checkForCircle( sourcepoints, tangents)
+ debug("Is Circle = ", isCircle )
+ if isCircle:
+ x, y, rmin, rmax, angle = res
+ debug("Circle -> ", rmin, rmax, angle )
+ if rmin/rmax>0.7:
+ circ = groups.Circle((x, y), 0.5*(rmin+rmax), refNode )
+ else:
+ circ = groups.Circle((x, y), rmin, refNode, rmax=rmax, angle=angle)
+ circ.points = sourcepoints
+ return circ
+ #else:
+ # hasArcs, res = FitShapes.checkForArcs( sourcepoints, tangents)
+ #else:
+ #hasArcs, res = FitShapes.checkForArcs( sourcepoints, tangents)
+ # -----------------------
+ if hasArcs:
+ x, y, rmin, rmax, angle = res
+ debug("Circle -> ", rmin, rmax, angle )
+ if rmin/rmax>0.7:
+ circ = groups.Circle((x, y), 0.5*(rmin+rmax), refNode )
+ else:
+ circ = groups.Circle((x, y), rmin, refNode, rmax=rmax, angle=angle)
+ circ.points = sourcepoints
+ return circ
+ return None
+
+ def segsFromTangents(svgCommandsList, refNode, options):
+ """Finds segments part in a list of points represented by svgCommandsList.
+
+ The method is to build the (averaged) tangent vectors to the curve.
+ Aligned points will have tangent with similar angle, so we cluster consecutive angles together
+ to define segments.
+ Then we extend segments to connected points not already part of other segments.
+ Then we merge consecutive segments with similar angles.
+
+ """
+ sourcepoints, svgCommandsList = geometric.toArray(svgCommandsList)
+
+ d = geometric.D(sourcepoints[0], sourcepoints[-1])
+ x, y, wTot, hTot = geometric.computeBox(sourcepoints)
+ if wTot == 0: wTot = 0.001
+ if hTot == 0: hTot = 0.001
+ if d==0:
+ # then we remove the last point to avoid null distance
+ # in other calculations
+ sourcepoints = sourcepoints[:-1]
+ svgCommandsList = svgCommandsList[:-1]
+
+ isClosing = FitShapes.isClosing(wTot, hTot, d)
+
+ if len(sourcepoints) < 4:
+ return groups.PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=isClosing)
+
+ tangents = manipulation.buildTangents(sourcepoints, isClosing=isClosing)
+
+ aCurvedSegment = FitShapes.curvedFromTangents(svgCommandsList, refNode, x, y, wTot, hTot, d, isClosing, sourcepoints, tangents, options)
+
+ if not aCurvedSegment == None:
+ return aCurvedSegment
+
+ # cluster points by angle of their tangents -------------
+ tgSegs = [ internal.Segment.fromCenterAndDir( p, t ) for (p, t) in zip(sourcepoints, tangents) ]
+ clustersInd = sorted(manipulation.clusterAngles( [s.angle for s in tgSegs] ))
+ debug("build envelop cluster: ", clustersInd)
+
+ # build Segments from clusters
+ newSegs = []
+ for imin, imax in clustersInd:
+ if imin+1< imax: # consider clusters with more than 3 points
+ seg = manipulation.fitSingleSegment(sourcepoints[imin:imax+1])
+ elif imin+1==imax: # 2 point path : we build a segment
+ seg = internal.Segment.from2Points(sourcepoints[imin], sourcepoints[imax], sourcepoints[imin:imax+1])
+ else:
+ seg = internal.Path( sourcepoints[imin:imax+1] )
+ seg.sourcepoints = sourcepoints
+ newSegs.append( seg )
+ manipulation.resetPrevNextSegment( newSegs )
+ debug(newSegs)
+ # -----------------------
+
+
+ # -----------------------
+ # Merge consecutive Path objects
+ updatedSegs=[]
+ def toMerge(p):
+ l=[p]
+ setattr(p, 'merged', True)
+ if hasattr(p, "__next__") and not p.next.isSegment():
+ l += toMerge(p.next)
+ return l
+
+ for i, seg in enumerate(newSegs[:-1]):
+ if seg.isSegment():
+ updatedSegs.append( seg)
+ continue
+ if hasattr(seg, 'merged'): continue
+ mergeList = toMerge(seg)
+ debug('merging ', mergeList)
+ p = internal.Path(numpy.concatenate([ p.points for p in mergeList]) )
+ debug('merged == ', p.points)
+ updatedSegs.append(p)
+
+ if not hasattr(newSegs[-1], 'merged'): updatedSegs.append( newSegs[-1])
+ debug("merged path", updatedSegs)
+ newSegs = manipulation.resetPrevNextSegment( updatedSegs )
+
+
+ # Extend segments -----------------------------------
+ if options.segExtensionEnable:
+ newSegs = extenders.SegmentExtender.extendSegments( newSegs, options.segExtensionDtoSeg, options.segExtensionQual )
+ debug("extended segs", newSegs)
+ newSegs = manipulation.resetPrevNextSegment( newSegs )
+ debug("extended segs", newSegs)
+
+ # ----------------------------------------
+
+
+ # ---------------------------------------
+ # merge consecutive segments with close angle
+ updatedSegs=[]
+
+ if options.segAngleMergeEnable:
+ newSegs = miscellaneous.mergeConsecutiveCloseAngles( newSegs, mangle=options.segAngleMergeTol1 )
+ newSegs=manipulation.resetPrevNextSegment(newSegs)
+ debug(' __ 2nd angle merge')
+ newSegs = miscellaneous.mergeConsecutiveCloseAngles( newSegs, mangle=options.segAngleMergeTol2 ) # 2nd pass
+ newSegs=manipulation.resetPrevNextSegment(newSegs)
+ debug('after merge ', len(newSegs), newSegs)
+ # Check if first and last also have close angles.
+ if isClosing and len(newSegs)>2 :
+ first, last = newSegs[0], newSegs[-1]
+ if first.isSegment() and last.isSegment():
+ if geometric.closeAngleAbs( first.angle, last.angle) < 0.1:
+ # force merge
+ points= numpy.concatenate( [ last.points, first.points] )
+ newseg = manipulation.fitSingleSegment(points)
+ newseg.next = first.__next__ if hasattr(first, "__next__") else None
+ last.prev.next = None
+ newSegs[0]=newseg
+ newSegs.pop()
+
+ # -----------------------------------------------------
+ # remove negligible Path/Segments between 2 large Segments
+ if options.segRemoveSmallEdge:
+ PreProcess.removeSmallEdge(newSegs, wTot, hTot)
+ newSegs=manipulation.resetPrevNextSegment(newSegs)
+
+ debug('after remove small ', len(newSegs), newSegs)
+ # -----------------------------------------------------
+
+ # -----------------------------------------------------
+ # Extend segments to their intersections
+ for p in newSegs:
+ if p.isSegment() and hasattr(p, "__next__"):
+ p.setIntersectWithNext()
+ # -----------------------------------------------------
+
+ return groups.PathGroup(newSegs, svgCommandsList, refNode, isClosing)
+
+
+
+
+# *************************************************************
+# The inkscape extension
+# *************************************************************
+class ShapeReco(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--title")
+ pars.add_argument("--keepOrigin", dest="keepOrigin", default=False, type=inkex.Boolean, help="Do not replace path")
+ pars.add_argument("--MainTabs")
+ pars.add_argument("--segExtensionDtoSeg", dest="segExtensionDtoSeg", default=0.03, type=float, help="max distance from point to segment")
+ pars.add_argument("--segExtensionQual", dest="segExtensionQual", default=0.5, type=float, help="segment extension fit quality")
+ pars.add_argument("--segExtensionEnable", dest="segExtensionEnable", default=True, type=inkex.Boolean, help="Enable segment extension")
+ pars.add_argument("--segAngleMergeEnable", dest="segAngleMergeEnable", default=True, type=inkex.Boolean, help="Enable merging of almost aligned consecutive segments")
+ pars.add_argument("--segAngleMergeTol1", dest="segAngleMergeTol1", default=0.2, type=float, help="merging with tollarance 1")
+ pars.add_argument("--segAngleMergeTol2", dest="segAngleMergeTol2", default=0.35, type=float, help="merging with tollarance 2")
+ pars.add_argument("--segAngleMergePara", dest="segAngleMergePara", default=0.001, type=float, help="merge lines as parralels if they fit")
+ pars.add_argument("--segRemoveSmallEdge", dest="segRemoveSmallEdge", default=True, type=inkex.Boolean, help="Enable removing very small segments")
+ pars.add_argument("--doUniformization", dest="doUniformization", default=True, type=inkex.Boolean, help="Preform angles and distances uniformization")
+ for opt in ["doParrallelize", "doKnownAngle", "doEqualizeDist", "doEqualizeRadius", "doCenterCircOnSeg"]:
+ pars.add_argument( "--"+opt, dest=opt, default=True, type=inkex.Boolean, help=opt)
+ pars.add_argument("--shapeDistLocal", dest="shapeDistLocal", default=0.3, type=float, help="Pthe percentage of difference at which we make lengths equal, locally")
+ pars.add_argument("--shapeDistGlobal", dest="shapeDistGlobal", default=0.025, type=float, help="Pthe percentage of difference at which we make lengths equal, globally")
+
+
+
+ def effect(self):
+
+ rej='{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}type'
+ paths = []
+ for id, node in list(self.svg.selected.items()):
+ if node.tag == '{http://www.w3.org/2000/svg}path' and rej not in list(node.keys()):
+ paths.append(node)
+
+ shapes = self.extractShapes(paths)
+ # add new shapes in SVG document
+ self.addShapesToDoc( shapes )
+
+ def extractShapesFromID( self, *nids, **options ):
+ """for debugging purpose """
+ eList = []
+ for nid in nids:
+ el = self.getElementById(nid)
+ if el is None:
+ print(("Cant find ", nid))
+ return
+ eList.append(el)
+ class tmp:
+ pass
+
+ self.options = self.OptionParser.parse_args()[0]
+ self.options._update_careful(options)
+ nodes=self.extractShapes(eList)
+ self.shape = nodes[0]
+
+
+ def buildShape(self, node):
+ def rotationAngle(tr):
+ if tr and tr.startswith('rotate'):
+ # retrieve the angle :
+ return float(tr[7:-1].split(','))
+ else:
+ return 0.
+
+ if node.tag.endswith('path'):
+ g = FitShapes.segsFromTangents(node.path.to_arrays(), node, self.options)
+ elif node.tag.endswith('rect'):
+ tr = node.get('transform', None)
+ if tr and tr.startswith('matrix'):
+ return None # can't deal with scaling
+ recSize = numpy.array([node.get('width'), node.get('height')])
+ recCenter = numpy.array([node.get('x'), node.get('y')]) + recSize/2
+ angle=rotationAngle(tr)
+ g = groups.Rectangle( recSize, recCenter, 0, [], node)
+ elif node.tag.endswith('circle'):
+ g = groups.Circle(node.get('cx'), node.get('cy'), node.get('r'), [], node )
+ elif node.tag.endswith('ellipse'):
+ if tr and tr.startswith('matrix'):
+ return None # can't deal with scaling
+ angle=rotationAngle(tr)
+ rx = node.get('rx')
+ ry = node.get('ry')
+ g = groups.Circle(node.get('cx'), node.get('cy'), ry, rmax=rx, angle=angle, refNode=node)
+
+ return g
+
+ def extractShapes( self, nodes ):
+ """The main function.
+ nodes : a list of nodes"""
+ analyzedNodes = []
+
+ # convert nodes to list of segments (groups.PathGroup) or Circle
+ for n in nodes :
+ g = self.buildShape(n)
+ if g :
+ analyzedNodes.append( g )
+
+ # uniformize shapes
+ if self.options.doUniformization:
+ analyzedNodes = PostProcess.uniformizeShapes(analyzedNodes, self.options)
+
+ return analyzedNodes
+
+ def addShapesToDoc(self, pathGroupList):
+ for group in pathGroupList:
+
+ debug("final ", group.listOfPaths, group.refNode )
+ debug("final-style ", group.refNode.get('style'))
+ # change to Rectangle if possible :
+ finalshape = manipulation.toRemarkableShape( group )
+ ele = group.addToNode( group.refNode)
+ group.setNodeStyle(ele, group.refNode)
+ if not self.options.keepOrigin:
+ group.refNode.xpath('..')[0].remove(group.refNode)
+
+
+
+if __name__ == '__main__':
+ ShapeReco().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shapereco/shaperrec/extenders.py b/extensions/fablabchemnitz/shapereco/shaperrec/extenders.py
new file mode 100644
index 00000000..29c7712e
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shaperrec/extenders.py
@@ -0,0 +1,130 @@
+import numpy
+import sys
+from shaperrec import manipulation
+
+# *************************************************************
+# debugging
+def void(*l):
+ pass
+def debug_on(*l):
+ sys.stderr.write(' '.join(str(i) for i in l) +'\n')
+debug = void
+#debug = debug_on
+
+##**************************************
+##
+class SegmentExtender:
+ """Extend Segments part of a list of Path by aggregating points from neighbouring Path objects.
+
+ There are 2 concrete subclasses for extending forward and backward (due to technical reasons).
+ """
+
+ def __init__(self, relD, fitQ):
+ self.relD = relD
+ self.fitQ = fitQ
+
+ def nextPaths(self, seg):
+ pL = []
+ p = self.getNext(seg) # prev or next
+ while p :
+ if p.isSegment(): break
+ if p.mergedObj is None: break
+ pL.append(p)
+ p = self.getNext(p)
+ if pL==[]:
+ return []
+ return pL
+
+ def extend(self, seg):
+ nextPathL = self.nextPaths(seg)
+ debug('extend ', self.extDir, seg, nextPathL, seg.length, len(nextPathL))
+ if nextPathL==[]: return seg
+ pointsToTest = numpy.concatenate( [p.points for p in nextPathL] )
+ mergeD = seg.length*self.relD
+ #print seg.point1 , seg.pointN, pointsToTest
+ pointsToFit, addedPoints = self.pointsToFit(seg, pointsToTest, mergeD)
+ if len(pointsToFit)==0:
+ return seg
+ newseg = manipulation.fitSingleSegment(pointsToFit)
+ if newseg.quality()>self.fitQ: # fit failed
+ return seg
+ debug( ' EXTENDING ! ', len(seg.points), len(addedPoints) )
+ self.removePath(seg, newseg, nextPathL, addedPoints )
+ newseg.points = pointsToFit
+ seg.mergedObj= newseg
+ newseg.sourcepoints = seg.sourcepoints
+
+ return newseg
+
+ @staticmethod
+ def extendSegments(segmentList, relD=0.03, qual=0.5):
+ """Perform Segment extension from list of Path segmentList
+ returns the updated list of Path objects"""
+ fwdExt = FwdExtender(relD, qual)
+ bwdExt = BwdExtender(relD, qual)
+ # tag all objects with an attribute pointing to the extended object
+ for seg in segmentList:
+ seg.mergedObj = seg # by default the extended object is self
+ # extend each segments, starting by the longest
+ for seg in sorted(segmentList, key = lambda s : s.length, reverse=True):
+ if seg.isSegment():
+ newseg=fwdExt.extend(seg)
+ seg.mergedObj = bwdExt.extend(newseg)
+ # the extension procedure has marked as None the mergedObj
+ # which have been swallowed by an extension.
+ # filter them out :
+ updatedSegs=[seg.mergedObj for seg in segmentList if seg.mergedObj]
+ return updatedSegs
+
+
+class FwdExtender(SegmentExtender):
+ extDir='Fwd'
+ def getNext(self, seg):
+ return seg.__next__ if hasattr(seg, "__next__") else None
+ def pointsToFit(self, seg, pointsToTest, mergeD):
+ distancesToLine =abs(seg.a*pointsToTest[:, 0]+seg.b*pointsToTest[:, 1]+seg.c)
+ goodInd=len(pointsToTest)
+ for i, d in reversed(list(enumerate(distancesToLine))):
+ if dnexp:
+ np = int(deltaD[ind]/medDelta)
+ pL = [a[ind]]
+ #print i,'=',ind,'adding ', np,' _ ', deltaD[ind], a[ind], a[ind+1]
+ for j in range(np-1):
+ f = float(j+1)/np
+ #print '------> ', (1-f)*a[ind]+f*a[ind+1]
+ pL.append( (1-f)*a[ind]+f*a[ind+1] )
+ newpoints[ind] = pL
+ else:
+ newpoints[ind]=[a[ind]]
+ if(D(a[0], a[-1])/sizeC > 0.005 ) :
+ newpoints[-1]=[a[-1]]
+
+ points = numpy.concatenate([p for p in newpoints if p!=None] )
+# ## print ' medDelta ', medDelta, deltaD[sortedDind[-1]]
+# ## print len(a) ,' ------> ', len(points)
+
+ rel_norms = numpy.sqrt(numpy.sum( deltas**2, 1 )) / sizeC
+ keep = numpy.concatenate([numpy.where( rel_norms >0.005 )[0], numpy.array([len(a)-1])])
+
+ #return a[keep] , [ parsedList[i] for i in keep]
+ #print len(a),' ',len(points)
+ return points, []
+
+rotMat = numpy.array( [[1, -1], [1, 1]] )/numpy.sqrt(2)
+unrotMat = numpy.array( [[1, 1], [-1, 1]] )/numpy.sqrt(2)
+
+def setupKnownAngles():
+ pi = numpy.pi
+ #l = [ i*pi/8 for i in range(0, 9)] +[ i*pi/6 for i in [1,2,4,5,] ]
+ 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)]
+ knownAngle = numpy.array( l )
+ return numpy.concatenate( [-knownAngle[:0:-1], knownAngle ])
+knownAngle = setupKnownAngles()
+
+_twopi = 2*numpy.pi
+_pi = numpy.pi
+
+def deltaAngle(a1, a2):
+ d = a1 - a2
+ return d if d > -_pi else d+_twopi
+
+def closeAngleAbs(a1, a2):
+ d = abs(a1 - a2 )
+ return min( abs(d-_pi), abs( _twopi - d), d)
+
+def deltaAngleAbs(a1, a2):
+ return abs(in_mPi_pPi(a1 - a2 ))
+
+def in_mPi_pPi(a):
+ if(a>_pi): return a-_twopi
+ if(a<-_pi): return a+_twopi
+ return a
+vec_in_mPi_pPi = numpy.vectorize(in_mPi_pPi)
+
+def D2(p1, p2):
+ return ((p1-p2)**2).sum()
+
+def D(p1, p2):
+ return numpy.sqrt(D2(p1, p2) )
+
+def norm(p):
+ return numpy.sqrt( (p**2).sum() )
+
+def computeBox(a):
+ """returns the bounding box enclosing the array of points a
+ in the form (x,y, width, height) """
+
+ xmin, ymin = a[:, 0].min(), a[:, 1].min()
+ xmax, ymax = a[:, 0].max(), a[:, 1].max()
+ return xmin, ymin, xmax-xmin, ymax-ymin
+
+def dirAndLength(p1, p2):
+ #l = max(D(p1, p2) ,1e-4)
+ l = D(p1, p2)
+ uv = (p1-p2)/l
+ return l, uv
+
+def length(p1, p2):
+ return numpy.sqrt( D2(p1, p2) )
+
+def barycenter(points):
+ """
+ """
+ return points.sum(axis=0)/len(points)
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shapereco/shaperrec/groups.py b/extensions/fablabchemnitz/shapereco/shaperrec/groups.py
new file mode 100644
index 00000000..4016f234
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shaperrec/groups.py
@@ -0,0 +1,278 @@
+import numpy
+import sys
+import inkex
+from lxml import etree
+from shaperrec import geometric
+from shaperrec import miscellaneous
+from shaperrec import internal
+from shaperrec import manipulation
+
+# *************************************************************
+# debugging
+def void(*l):
+ pass
+def debug_on(*l):
+ sys.stderr.write(' '.join(str(i) for i in l) +'\n')
+debug = void
+#debug = debug_on
+
+# *************************************************************
+# *************************************************************
+# Groups of Path
+#
+class PathGroup(object):
+ """A group of Path representing one SVG node.
+ - a list of Path
+ - a list of SVG commands describe the full node (=SVG path element)
+ - a reference to the inkscape node object
+
+ """
+ listOfPaths = []
+ refSVGPathList = []
+ isClosing = False
+ refNode = None
+
+ def __init__(self, listOfPaths, refSVGPathList, refNode=None, isClosing=False):
+ self.refNode = refNode
+ self.listOfPaths = listOfPaths
+ self.refSVGPathList = refSVGPathList
+ self.isClosing=isClosing
+
+ def addToNode(self, node):
+ newList = miscellaneous.reformatList( self.listOfPaths)
+ ele = miscellaneous.addPath( newList, node)
+ debug("PathGroup ", newList)
+ return ele
+
+ def setNodeStyle(self, ele, node):
+ style = node.get('style')
+ cssClass = node.get('class')
+ debug("style ", style)
+ debug("class ", cssClass)
+ if style == None and cssClass == None :
+ style = 'fill:none; stroke:red; stroke-width:1'
+
+ if not cssClass == None:
+ ele.set('class', cssClass)
+ if not style == None:
+ ele.set('style', style)
+
+ @staticmethod
+ def toSegments(points, refSVGPathList, refNode, isClosing=False):
+ """
+ """
+ segs = [ internal.Segment.from2Points(p, points[i+1], points[i:i+2] ) for (i, p) in enumerate(points[:-1]) ]
+ manipulation.resetPrevNextSegment(segs)
+ return PathGroup( segs, refSVGPathList, refNode, isClosing)
+
+class TangentEnvelop(PathGroup):
+ """Specialization where the Path objects are all Segments and represent tangents to a curve """
+ def addToNode(self, node):
+ newList = [ ]
+ for s in self.listOfPaths:
+ newList += s.asSVGCommand(firstP=True)
+ debug("TangentEnvelop ", newList)
+ ele = miscellaneous.addPath( newList, node)
+ return ele
+
+ def setNodeStyle(self, ele, node):
+ style = node.get('style')+';marker-end:url(#Arrow1Lend)'
+ ele.set('style', style)
+
+
+class Circle(PathGroup):
+ """Specialization where the list of Path objects
+ is to be replaced by a Circle specified by a center and a radius.
+
+ If an other radius 'rmax' is given than the object represents an ellipse.
+ """
+ isClosing= True
+ def __init__(self, center, rad, refNode=None, rmax=None, angle=0.):
+ self.listOfPaths = []
+ self.refNode = refNode
+ self.center = numpy.array(center)
+ self.radius = rad
+ if rmax:
+ self.type ='ellipse'
+ else:
+ self.type = 'circle'
+ self.rmax = rmax
+ self.angle = angle
+
+ def addToNode(self, refnode):
+ """Add a node in the xml structure corresponding to this rect
+ refnode : xml node used as a reference, new point will be inserted a same level"""
+ ele = etree.Element('{http://www.w3.org/2000/svg}'+self.type)
+
+ ele.set('cx', str(self.center[0]))
+ ele.set('cy', str(self.center[1]))
+ if self.rmax:
+ ele.set('ry', str(self.radius))
+ ele.set('rx', str(self.rmax))
+ ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle), self.center[0], self.center[1]))
+ else:
+ ele.set('r', str(self.radius))
+ refnode.xpath('..')[0].append(ele)
+ return ele
+
+
+class Rectangle(PathGroup):
+ """Specialization where the list of Path objects
+ is to be replaced by a Rectangle specified by a center and size (w,h) and a rotation angle.
+
+ """
+ def __init__(self, center, size, angle, listOfPaths, refNode=None):
+ self.listOfPaths = listOfPaths
+ self.refNode = refNode
+ self.center = center
+ self.size = size
+ self.bbox = size
+ self.angle = angle
+ pos = self.center - numpy.array( size )/2
+ if angle != 0. :
+ cosa = numpy.cos(angle)
+ sina = numpy.sin(angle)
+ self.rotMat = numpy.matrix( [ [ cosa, sina], [-sina, cosa] ] )
+ self.rotMatstr = 'matrix(%1.7f,%1.7f,%1.7f,%1.7f,0,0)'%(cosa, sina, -sina, cosa)
+
+
+ #debug(' !!!!! Rotated rectangle !!', self.size, self.bbox, ' angles ', a, self.angle ,' center',self.center)
+ else :
+ self.rotMatstr = None
+ self.pos = pos
+ debug(' !!!!! Rectangle !!', self.size, self.bbox, ' angles ', self.angle, ' center', self.center)
+
+ def addToNode(self, refnode):
+ """Add a node in the xml structure corresponding to this rect
+ refnode : xml node used as a reference, new point will be inserted a same level"""
+ ele = etree.Element('{http://www.w3.org/2000/svg}rect')
+ self.fill(ele)
+ refnode.xpath('..')[0].append(ele)
+ return ele
+
+ def fill(self, ele):
+ w, h = self.size
+ ele.set('width', str(w))
+ ele.set('height', str(h))
+ w, h = self.bbox
+ ele.set('x', str(self.pos[0]))
+ ele.set('y', str(self.pos[1]))
+ if self.rotMatstr:
+ ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle), self.center[0], self.center[1]))
+ #ele.set('transform', self.rotMatstr)
+
+ @staticmethod
+ def isRectangle( pathGroup):
+ """Check if the segments in pathGroups can form a rectangle.
+ Returns a Rectangle or None"""
+ #print 'xxxxxxxx isRectangle',pathGroups
+ if isinstance(pathGroup, Circle ): return None
+ segmentList = [p for p in pathGroup.listOfPaths if p.isSegment() ]#or p.effectiveNPoints >0]
+ if len(segmentList) != 4:
+ debug( 'rectangle Failed at length ', len(segmentList))
+ return None
+ a, b, c, d = segmentList
+
+ if geometric.length(a.point1, d.pointN)> 0.2*(a.length+d.length)*0.5:
+ debug('rectangle test failed closing ', geometric.length(a.point1, d.pointN), a.length, d.length)
+ return None
+
+ Aac, Abd = geometric.closeAngleAbs(a.angle, c.angle), geometric.closeAngleAbs(b.angle, d.angle)
+ if min(Aac, Abd) > 0.07 or max(Aac, Abd) >0.27 :
+ debug( 'rectangle Failed at angles', Aac, Abd)
+ return None
+ notsimilarL = lambda d1, d2: abs(d1-d2)>0.20*min(d1, d2)
+
+ pi, twopi = numpy.pi, 2*numpy.pi
+ angles = numpy.array( [p.angle for p in segmentList] )
+ minAngleInd = numpy.argmin( numpy.minimum( abs(angles), abs( abs(angles)-pi), abs( abs(angles)-twopi) ) )
+ rotAngle = angles[minAngleInd]
+ width = (segmentList[minAngleInd].length + segmentList[(minAngleInd+2)%4].length)*0.5
+ height = (segmentList[(minAngleInd+1)%4].length + segmentList[(minAngleInd+3)%4].length)*0.5
+ # set rectangle center as the bbox center
+ x, y, w, h = geometric.computeBox( numpy.concatenate( [ p.points for p in segmentList]) )
+ r = Rectangle( numpy.array( [x+w/2, y+h/2]), (width, height), rotAngle, pathGroup.listOfPaths, pathGroup.refNode)
+
+ debug( ' found a rectangle !! ', a.length, b.length, c.length, d.length )
+ return r
+
+
+class CurveGroup(PathGroup):
+ """Specialization where the list of Path objects
+ is to be replaced by a Rectangle specified by a center and size (w,h) and a rotation angle.
+
+ """
+ def __init__(self, center, size, angle, listOfPaths, refNode=None):
+ self.listOfPaths = listOfPaths
+ self.refNode = refNode
+ self.center = center
+ self.size = size
+ self.bbox = size
+ self.angle = angle
+ pos = self.center - numpy.array( size )/2
+ if angle != 0. :
+ cosa = numpy.cos(angle)
+ sina = numpy.sin(angle)
+ self.rotMat = numpy.matrix( [ [ cosa, sina], [-sina, cosa] ] )
+ self.rotMatstr = 'matrix(%1.7f,%1.7f,%1.7f,%1.7f,0,0)'%(cosa, sina, -sina, cosa)
+
+
+ #debug(' !!!!! Rotated rectangle !!', self.size, self.bbox, ' angles ', a, self.angle ,' center',self.center)
+ else :
+ self.rotMatstr = None
+ self.pos = pos
+ debug(' !!!!! Rectangle !!', self.size, self.bbox, ' angles ', self.angle, ' center', self.center)
+
+ def addToNode(self, refnode):
+ """Add a node in the xml structure corresponding to this rect
+ refnode : xml node used as a reference, new point will be inserted a same level"""
+ ele = etree.Element('{http://www.w3.org/2000/svg}rect')
+ self.fill(ele)
+ refnode.xpath('..')[0].append(ele)
+ return ele
+
+# def fill(self, ele):
+# w, h = self.size
+# ele.set('width', str(w))
+# ele.set('height', str(h))
+# w, h = self.bbox
+# ele.set('x', str(self.pos[0]))
+# ele.set('y', str(self.pos[1]))
+# if self.rotMatstr:
+# ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle), self.center[0], self.center[1]))
+# #ele.set('transform', self.rotMatstr)
+
+ @staticmethod
+ def isCurvedSegment( pathGroup):
+ """Check if the segments in pathGroups can form a rectangle.
+ Returns a Rectangle or None"""
+ #print 'xxxxxxxx isRectangle',pathGroups
+ if isinstance(pathGroup, Circle ): return None
+ segmentList = [p for p in pathGroup.listOfPaths if p.isSegment() ]#or p.effectiveNPoints >0]
+ if len(segmentList) != 4:
+ debug( 'rectangle Failed at length ', len(segmentList))
+ return None
+ a, b, c, d = segmentList
+
+ if geometric.length(a.point1, d.pointN)> 0.2*(a.length+d.length)*0.5:
+ debug('rectangle test failed closing ', geometric.length(a.point1, d.pointN), a.length, d.length)
+ return None
+
+ Aac, Abd = geometric.closeAngleAbs(a.angle, c.angle), geometric.closeAngleAbs(b.angle, d.angle)
+ if min(Aac, Abd) > 0.07 or max(Aac, Abd) >0.27 :
+ debug( 'rectangle Failed at angles', Aac, Abd)
+ return None
+ notsimilarL = lambda d1, d2: abs(d1-d2)>0.20*min(d1, d2)
+
+ pi, twopi = numpy.pi, 2*numpy.pi
+ angles = numpy.array( [p.angle for p in segmentList] )
+ minAngleInd = numpy.argmin( numpy.minimum( abs(angles), abs( abs(angles)-pi), abs( abs(angles)-twopi) ) )
+ rotAngle = angles[minAngleInd]
+ width = (segmentList[minAngleInd].length + segmentList[(minAngleInd+2)%4].length)*0.5
+ height = (segmentList[(minAngleInd+1)%4].length + segmentList[(minAngleInd+3)%4].length)*0.5
+ # set rectangle center as the bbox center
+ x, y, w, h = geometric.computeBox( numpy.concatenate( [ p.points for p in segmentList]) )
+ r = Rectangle( numpy.array( [x+w/2, y+h/2]), (width, height), rotAngle, pathGroup.listOfPaths, pathGroup.refNode)
+
+ debug( ' found a rectangle !! ', a.length, b.length, c.length, d.length )
+ return r
diff --git a/extensions/fablabchemnitz/shapereco/shaperrec/internal.py b/extensions/fablabchemnitz/shapereco/shaperrec/internal.py
new file mode 100644
index 00000000..edd99407
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shaperrec/internal.py
@@ -0,0 +1,351 @@
+import numpy
+import sys
+from shaperrec import geometric
+from shaperrec import miscellaneous
+
+# *************************************************************
+# debugging
+def void(*l):
+ pass
+def debug_on(*l):
+ sys.stderr.write(' '.join(str(i) for i in l) +'\n')
+debug = void
+#debug = debug_on
+
+# *************************************************************
+# Internal Objects
+class Path(object):
+ """Private representation of a sequence of points.
+ A SVG node of type 'path' is splitted in several of these Path objects.
+ """
+ next = None # next Path in the sequence of path corresponding to a SVG node
+ prev = None # previous Path in the sequence of path corresponding to a SVG node
+ sourcepoints = None # the full list of points from which this path is a subset
+
+ normalv = None # normal vector to this Path
+
+ def __init__(self, points):
+ """points an array of points """
+ self.points = points
+ self.init()
+
+ def init(self):
+ self.effectiveNPoints = len(self.points)
+ if self.effectiveNPoints>1:
+ self.length, self.univ = geometric.dirAndLength(self.points[0], self.points[-1])
+ else:
+ self.length, self.univ = 0, numpy.array([0, 0])
+ if self.effectiveNPoints>0:
+ self.pointN=self.points[-1]
+ self.point1=self.points[0]
+
+ def isSegment(self):
+ return False
+
+ def quality(self):
+ return 1000
+
+ def dump(self):
+ n = len(self.points)
+ if n>0:
+ return 'path at '+str(self.points[0])+ ' to '+ str(self.points[-1])+' npoints=%d / %d (eff)'%(n, self.effectiveNPoints)
+ else:
+ return 'path Void !'
+
+ def setNewLength(self, l):
+ self.newLength = l
+
+ def removeLastPoints(self, n):
+ self.points = self.points[:-n]
+ self.init()
+ def removeFirstPoints(self, n):
+ self.points = self.points[n:]
+ self.init()
+
+ def costheta(self, seg):
+ return self.unitv.dot(seg.unitv)
+
+ def translate(self, tr):
+ """Translate this path by tr"""
+ self.points = self.points + tr
+
+ def asSVGCommand(self, firstP=False):
+ svgCommands = []
+ com = 'M' if firstP else 'L'
+ for p in self.points:
+ svgCommands.append( [com, [p[0], p[1]] ] )
+ com='L'
+ return svgCommands
+
+
+ def setIntersectWithNext(self, next=None):
+ pass
+
+ def mergedWithNext(self, newPath=None):
+ """ Returns the combination of self and self.next.
+ sourcepoints has to be set
+ """
+ if newPath is None: newPath = Path( numpy.concatenate([self.points, self.next.points]) )
+
+ newPath.sourcepoints = self.sourcepoints
+ newPath.prev = self.prev
+ if self.prev : newPath.prev.next = newPath
+ newPath.next = self.next.__next__
+ if newPath.__next__:
+ newPath.next.prev = newPath
+ return newPath
+
+# *************************************************************
+#
+class Segment(Path):
+ """ A segment. Defined by its line equation ax+by+c=0 and the points from orignal paths
+ it is ensured that a**2+b**2 = 1
+ """
+ QUALITYCUT = 0.9
+
+ newAngle = None # temporary angle set during the "parralelization" step
+ newLength = None # temporary lenght set during the "parralelization" step
+
+ # Segment Builders
+ @staticmethod
+ def from2Points( p1, p2, refPoints = None):
+ dirV = p2-p1
+ center = 0.5*(p2+p1)
+ return Segment.fromCenterAndDir(center, dirV, refPoints)
+
+ @staticmethod
+ def fromCenterAndDir( center, dirV, refPoints=None):
+ b = dirV[0]
+ a = -dirV[1]
+ c = - (a*center[0]+b*center[1])
+
+ if refPoints is None:
+ refPoints = numpy.array([ center-0.5*dirV, center+0.5*dirV] )
+ s = Segment( a, b, c, refPoints)
+ return s
+
+
+ def __init__(self, a,b,c, points, doinit=True):
+ """a,b,c: the line parameters.
+ points : the array of 2D points represented by this Segment
+ doinit : if true will compute additionnal parameters to this Segment (first/last points, unit vector,...)
+ """
+ self.a = a
+ self.b = b
+ self.c = c
+
+ self.points = points
+ d = numpy.sqrt(a**2+b**2)
+ if d != 1. :
+ self.a /= d
+ self.b /= d
+ self.c /= d
+
+ if doinit :
+ self.init()
+
+
+ def init(self):
+ a, b, c = self.a, self.b, self.c
+ x, y = self.points[0]
+ self.point1 = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
+ x, y = self.points[-1]
+ self.pointN = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
+ uv = self.computeDirLength()
+ self.distancesToLine = self.computeDistancesToLine(self.points)
+ self.normalv = numpy.array( [ a, b ])
+
+ self.angle = numpy.arccos( uv[0] )*numpy.sign(uv[1] )
+
+
+ def computeDirLength(self):
+ """re-compute and set unit vector and length """
+ self.length, uv = geometric.dirAndLength(self.pointN, self.point1)
+ self.unitv = uv
+ return uv
+
+ def isSegment(self):
+ return True
+
+ def recomputeEndPoints(self):
+ a, b, c = self.a, self.b, self.c
+ x, y = self.points[0]
+ self.point1 = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
+ x, y = self.points[-1]
+
+ self.pointN = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
+
+ self.length = numpy.sqrt( geometric.D2(self.pointN, self.point1) )
+
+ def projectPoint(self, p):
+ """ return the point projection of p onto this segment"""
+ a, b, c = self.a, self.b, self.c
+ x, y = p
+ return numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
+
+
+ def intersect(self, seg):
+ """Returns the intersection of this line with the line seg"""
+ nu, nv = self.normalv, seg.normalv
+ u = numpy.array([[-self.c], [-seg.c]])
+ doRotation = min(nu.min(), nv.min()) <1e-4
+ if doRotation:
+ # rotate to avoid numerical issues
+ nu = numpy.array(geometric.rotMat.dot(nu))[0]
+ nv = numpy.array(geometric.rotMat.dot(nv))[0]
+ m = numpy.matrix( (nu, nv) )
+
+ i = (m**-1).dot(u)
+ i=numpy.array( i).swapaxes(0, 1)[0]
+ debug(' intersection ', nu, nv, self.angle, seg.angle, ' --> ', i)
+ if doRotation:
+ i = geometric.unrotMat.dot(i).A1
+ debug(' ', i)
+
+
+ return i
+
+ def setIntersectWithNext(self, next=None):
+ """Modify self such as self.pointN is the intersection with next segment """
+ if next is None:
+ next = self.__next__
+ if next and next.isSegment():
+ if abs(self.normalv.dot(next.unitv)) < 1e-3:
+ return
+ debug(' Intersect', self, next, ' from ', self.point1, self.pointN, ' to ', next.point1, next.pointN,)
+ inter = self.intersect(next)
+ debug(' --> ', inter, ' d=', geometric.D(self.pointN, inter) )
+ next.point1 = inter
+ self.pointN = inter
+ self.computeDirLength()
+ next.computeDirLength()
+
+ def computeDistancesToLine(self, points):
+ """points: array of points.
+ returns the array of distances to this segment"""
+ return abs(self.a*points[:, 0]+self.b*points[:, 1]+self.c)
+
+
+ def distanceTo(self, point):
+ return abs(self.a*point[0]+self.b*point[1]+self.c)
+
+ def inverse(self):
+ """swap all x and y values. """
+ def inv(v):
+ v[0], v[1] = v[1], v[0]
+ for v in [self.point1, self.pointN, self.unitv, self.normalv]:
+ inv(v)
+
+ self.points = numpy.roll(self.points, 1, axis=1)
+ self.a, self.b = self.b, self.a
+ self.angle = numpy.arccos( self.unitv[0] )*numpy.sign(self.unitv[1] )
+ return
+
+ def dumpShort(self):
+ return 'seg '+' '+str(self.point1 )+'to '+str(self.pointN)+ ' npoints=%d | angle,offset=(%.2f,%.2f )'%(len(self.points), self.angle, self.c)+' ', self.normalv
+
+ def dump(self):
+ v = self.variance()
+ n = len(self.points)
+ return 'seg '+str(self.point1 )+' , '+str(self.pointN)+ ' v/l=%.2f / %.2f = %.2f r*numpy.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)
+
+ def variance(self):
+ d = self.distancesToLine
+ return numpy.sqrt( (d**2).sum()/len(d) )
+
+ def quality(self):
+ n = len(self.points)
+ return min(self.variance()/self.length*numpy.sqrt(n), 1000)
+
+ def formatedSegment(self, firstP=False):
+ return self.asSVGCommand(firstP)
+
+ def asSVGCommand(self, firstP=False):
+
+ if firstP:
+ segment = [ ['M', [self.point1[0], self.point1[1] ] ],
+ ['L', [self.pointN[0], self.pointN[1] ] ]
+ ]
+ else:
+ segment = [ ['L', [self.pointN[0], self.pointN[1] ] ] ]
+ #debug("Segment, format : ", segment)
+ return segment
+
+ def replaceInList(self, startPos, fullList):
+ code0 = fullList[startPos][0]
+ segment = [ [code0, [self.point1[0], self.point1[1] ] ],
+ ['L', [self.pointN[0], self.pointN[1] ] ]
+ ]
+ l = fullList[:startPos]+segment+fullList[startPos+len(self.points):]
+ return l
+
+
+
+
+ def mergedWithNext(self, doRefit=True):
+ """ Returns the combination of self and self.next.
+ sourcepoints has to be set
+ """
+ spoints = numpy.concatenate([self.points, self.next.points])
+
+ if doRefit:
+ newSeg = fitSingleSegment(spoints)
+ else:
+ newSeg = Segment.fromCenterAndDir(geometric.barycenter(spoints), self.unitv, spoints)
+
+ newSeg = Path.mergedWithNext(self, newSeg)
+ return newSeg
+
+
+
+ def center(self):
+ return 0.5*(self.point1+self.pointN)
+
+ def box(self):
+ return geometric.computeBox(self.points)
+
+
+ def translate(self, tr):
+ """Translate this segment by tr """
+ c = self.c -self.a*tr[0] -self.b*tr[1]
+ self.c =c
+ self.pointN = self.pointN+tr
+ self.point1 = self.point1+tr
+ self.points +=tr
+
+ def adjustToNewAngle(self):
+ """reset all parameters so that self.angle is change to self.newAngle """
+
+ self.a, self.b, self.c = miscellaneous.parametersFromPointAngle( 0.5*(self.point1+self.pointN), self.newAngle)
+
+ #print 'adjustToNewAngle ', self, self.angle, self.newAngle
+ self.angle = self.newAngle
+ self.normalv = numpy.array( [ self.a, self.b ])
+ self.unitv = numpy.array( [ self.b, -self.a ])
+ if abs(self.angle) > numpy.pi/2 :
+ if self.b > 0: self.unitv *= -1
+ elif self.b<0 : self.unitv *= -1
+
+ self.point1 = self.projectPoint(self.point1) # reset point1
+ if not hasattr(self, "__next__") or not self.next.isSegment():
+ # move the last point (no intersect with next)
+
+ pN = self.projectPoint(self.pointN)
+ dirN = pN - self.point1
+ lN = geometric.length(pN, self.point1)
+ self.pointN = dirN/lN*self.length + self.point1
+ #print ' ... adjusting last seg angle ',p.dump() , ' normalv=', p.normalv, 'unitv ', p.unitv
+ else:
+ self.setIntersectWithNext()
+
+ def adjustToNewDistance(self):
+ self.pointN = self.newLength* self.unitv + self.point1
+ self.length = self.newLength
+
+ def tempLength(self):
+ if self.newLength : return self.newLength
+ else : return self.length
+
+ def tempAngle(self):
+ if self.newAngle: return self.newAngle
+ return self.angle
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/shapereco/shaperrec/manipulation.py b/extensions/fablabchemnitz/shapereco/shaperrec/manipulation.py
new file mode 100644
index 00000000..e2c6b9d6
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shaperrec/manipulation.py
@@ -0,0 +1,187 @@
+import numpy
+import sys
+from shaperrec import groups
+from shaperrec import geometric
+from shaperrec import internal
+
+# *************************************************************
+# debugging
+def void(*l):
+ pass
+def debug_on(*l):
+ sys.stderr.write(' '.join(str(i) for i in l) +'\n')
+debug = void
+#debug = debug_on
+
+# *************************************************************
+# Object manipulation functions
+
+def toRemarkableShape( group ):
+ """Test if PathGroup instance 'group' looks like a remarkable shape (ex: Rectangle).
+ if so returns a new shape instance else returns group unchanged"""
+ r = groups.Rectangle.isRectangle( group )
+ if r : return r
+ return group
+
+
+def resetPrevNextSegment(segs):
+ for i, seg in enumerate(segs[:-1]):
+ s = segs[i+1]
+ seg.next = s
+ s.prev = seg
+ return segs
+
+
+def fitSingleSegment(a):
+ xmin, ymin, w, h = geometric.computeBox(a)
+ inverse = w ', tangents )
+
+ if averaged:
+ # average over neighbours
+ avTan = numpy.array(tangents)
+ avTan[:-1] += tangents[1:]
+ avTan[1:] += tangents[:-1]
+ if isClosing:
+ tangents[0]+=tangents[-1]
+ tangents[1]+=tangents[0]
+ avTan *= 1./3
+ if not isClosing:
+ avTan[0] *=1.5
+ avTan[-1] *=1.5
+
+ return avTan
+
+
+def clusterAngles(array, dAng=0.15):
+ """Cluster together consecutive angles with similar values (within 'dAng').
+ array : flat array of angles
+ returns [ ..., (indi_0, indi_1),...] where each tuple are indices of cluster i
+ """
+ N = len(array)
+
+ closebyAng = numpy.zeros( (N, 4), dtype=int)
+
+ for i, a in enumerate(array):
+ cb = closebyAng[i]
+ cb[0] =i
+ cb[2]=i
+ cb[3]=i
+ c=i-1
+ # find number of angles within dAng in nearby positions
+ while c>-1: # indices below i
+ d=geometric.closeAngleAbs(a, array[c])
+ if d>dAng:
+ break
+ cb[1]+=1
+ cb[2]=c
+ c-=1
+ c=i+1
+ while cdAng:
+ break
+ cb[1]+=1
+ cb[3]=c
+ c+=1
+ closebyAng= closebyAng[numpy.argsort(closebyAng[:, 1]) ]
+
+ clusteredPos = numpy.zeros(N, dtype=int)
+ clusters = []
+ for cb in reversed(closebyAng):
+ if clusteredPos[cb[0]]==1:
+ continue
+ # try to build a cluster
+ minI = cb[2]
+ while clusteredPos[minI]==1:
+ minI+=1
+ maxI = cb[3]
+ while clusteredPos[maxI]==1:
+ maxI-=1
+ for i in range(minI, maxI+1):
+ clusteredPos[i] = 1
+ clusters.append( (minI, maxI) )
+
+ return clusters
+
+
+
+
+def adjustAllAngles(paths):
+ for p in paths:
+ if p.isSegment() and p.newAngle is not None:
+ p.adjustToNewAngle()
+ # next translate to fit end points
+ tr = numpy.zeros(2)
+ for p in paths[1:]:
+ if p.isSegment() and p.prev.isSegment():
+ tr = p.prev.pointN - p.point1
+ debug(' translating ', p, ' prev is', p.prev, ' ', tr, )
+ p.translate(tr)
+
+def adjustAllDistances(paths):
+ for p in paths:
+ if p.isSegment() and p.newLength is not None:
+ p.adjustToNewDistance()
+ # next translate to fit end points
+ tr = numpy.zeros(2)
+ for p in paths[1:]:
+ if p.isSegment() and p.prev.isSegment():
+ tr = p.prev.pointN - p.point1
+ p.translate(tr)
diff --git a/extensions/fablabchemnitz/shapereco/shaperrec/miscellaneous.py b/extensions/fablabchemnitz/shapereco/shaperrec/miscellaneous.py
new file mode 100644
index 00000000..3949c411
--- /dev/null
+++ b/extensions/fablabchemnitz/shapereco/shaperrec/miscellaneous.py
@@ -0,0 +1,209 @@
+import sys
+import inkex
+from inkex import Path
+import numpy
+from shaperrec import manipulation
+from lxml import etree
+
+
+
+# *************************************************************
+# debugging
+def void(*l):
+ pass
+def debug_on(*l):
+ sys.stderr.write(' '.join(str(i) for i in l) +'\n')
+debug = void
+#debug = debug_on
+
+errwrite = void
+
+
+# miscellaneous helper functions to sort
+
+# merge consecutive segments with close angle
+
+def mergeConsecutiveCloseAngles( segList , mangle =0.25 , q=0.5):
+
+ def toMerge(seg):
+ l=[seg]
+ setattr(seg, 'merged', True)
+ if hasattr(seg, "__next__") and seg.next.isSegment() :
+ debug('merging segs ', seg.angle, ' with : ', seg.next.point1, seg.next.pointN, ' ang=', seg.next.angle)
+ if geometric.deltaAngleAbs( seg.angle, seg.next.angle) < mangle:
+ l += toMerge(seg.next)
+ return l
+
+ updatedSegs = []
+ for i, seg in enumerate(segList[:-1]):
+ if not seg.isSegment() :
+ updatedSegs.append(seg)
+ continue
+ if hasattr(seg, 'merged'):
+ continue
+ debug(i, ' inspect merge : ', seg.point1, '-', seg.pointN, seg.angle, ' q=', seg.quality())
+ mList = toMerge(seg)
+ debug(' --> tomerge ', len(mList))
+ if len(mList)<2:
+ delattr(seg, 'merged')
+ updatedSegs.append(seg)
+ continue
+ points= numpy.concatenate( [p.points for p in mList] )
+ newseg = fitSingleSegment(points)
+ if newseg.quality()>q:
+ delattr(seg, 'merged')
+ updatedSegs.append(seg)
+ continue
+ for p in mList:
+ setattr(seg, 'merged', True)
+ newseg.sourcepoints = seg.sourcepoints
+ debug(' --> post merge qual = ', newseg.quality(), seg.pointN, ' --> ', newseg.pointN, newseg.angle)
+ newseg.prev = mList[0].prev
+ newseg.next = mList[-1].__next__
+ updatedSegs.append(newseg)
+ if not hasattr(segList[-1], 'merged') : updatedSegs.append( segList[-1])
+ return updatedSegs
+
+
+
+
+def parametersFromPointAngle(point, angle):
+ unitv = numpy.array([ numpy.cos(angle), numpy.sin(angle) ])
+ ortangle = angle+numpy.pi/2
+ normal = numpy.array([ numpy.cos(ortangle), numpy.sin(ortangle) ])
+ genOffset = -normal.dot(point)
+ a, b = normal
+ return a, b, genOffset
+
+
+
+def addPath(newList, refnode):
+ """Add a node in the xml structure corresponding to the content of newList
+ newList : list of Segment or Path
+ refnode : xml node used as a reference, new point will be inserted a same level"""
+ ele = etree.Element('{http://www.w3.org/2000/svg}path')
+ errwrite("newList = " + str(newList) + "\n")
+ ele.set('d', str(Path(newList)))
+ refnode.xpath('..')[0].append(ele)
+ return ele
+
+def reformatList( listOfPaths):
+ """ Returns a SVG paths list (same format as simplepath.parsePath) from a list of Path objects
+ - Segments in paths are added in the new list
+ - simple Path are retrieved from the original refSVGPathList and put in the new list (thus preserving original bezier curves)
+ """
+ newList = []
+ first = True
+ for seg in listOfPaths:
+ newList += seg.asSVGCommand(first)
+ first = False
+ return newList
+
+
+def clusterValues( values, relS=0.1 , refScaleAbs='range' ):
+ """form clusters of similar quantities from input 'values'.
+ Clustered values are not necessarily contiguous in the input array.
+ Clusters size (that is max-min) is < relS*cluster_average """
+ if len(values)==0:
+ return []
+ if len(values.shape)==1:
+ sortedV = numpy.stack([ values, numpy.arange(len(values))], 1)
+ else:
+ # Assume value.shape = (N,2) and index are ok
+ sortedV = values
+ sortedV = sortedV[ numpy.argsort(sortedV[:, 0]) ]
+
+ sortedVV = sortedV[:, 0]
+ refScale = sortedVV[-1]-sortedVV[0]
+ #sortedVV += 2*min(sortedVV)) # shift to avoid numerical issues around 0
+
+ #print sortedVV
+ class Cluster:
+ def __init__(self, delta, sum, indices):
+ self.delta = delta
+ self.sum = sum
+ self.N=len(indices)
+ self.indices = indices
+ def size(self):
+ return self.delta/refScale
+
+ def combine(self, c):
+ #print ' combine ', self.indices[0], c.indices[-1], ' -> ', sortedVV[c.indices[-1]] - sortedVV[self.indices[0]]
+ newC = Cluster(sortedVV[c.indices[-1]] - sortedVV[self.indices[0]],
+ self.sum+c.sum,
+ self.indices+c.indices)
+ return newC
+
+ def originIndices(self):
+ return tuple(int(sortedV[i][1]) for i in self.indices)
+
+ def size_local(self):
+ return self.delta / sum( sortedVV[i] for i in self.indices) *len(self.indices)
+ def size_range(self):
+ return self.delta/refScale
+ def size_abs(self):
+ return self.delta
+
+ if refScaleAbs=='range':
+ Cluster.size = size_range
+ elif refScaleAbs=='local':
+ Cluster.size = size_local
+ elif refScaleAbs=='abs':
+ Cluster.size = size_abs
+
+ class ClusterPair:
+ next=None
+ prev=None
+ def __init__(self, c1, c2 ):
+ self.c1=c1
+ self.c2=c2
+ self.refresh()
+ def refresh(self):
+ self.potentialC =self.c1.combine(self.c2)
+ self.size = self.potentialC.size()
+ def setC1(self, c1):
+ self.c1=c1
+ self.refresh()
+ def setC2(self, c2):
+ self.c2=c2
+ self.refresh()
+
+ #ave = 0.5*(sortedVV[1:,0]+sortedV[:-1,0])
+ #deltaR = (sortedV[1:,0]-sortedV[:-1,0])/ave
+
+ cList = [Cluster(0, v, (i,)) for (i, v) in enumerate(sortedVV) ]
+ cpList = [ ClusterPair( c, cList[i+1] ) for (i, c) in enumerate(cList[:-1]) ]
+ manipulation.resetPrevNextSegment( cpList )
+
+ #print cpList
+ def reduceCL( cList ):
+ if len(cList)<=1:
+ return cList
+ cp = min(cList, key=lambda cp:cp.size)
+ #print '==', cp.size , relS, cp.c1.indices , cp.c2.indices, cp.potentialC.indices
+
+ while cp.size < relS:
+ if hasattr(cp, "__next__"):
+ cp.next.setC1(cp.potentialC)
+ cp.next.prev = cp.prev
+ if cp.prev:
+ cp.prev.setC2(cp.potentialC)
+ cp.prev.next = cp.__next__ if hasattr(cp, "__next__") else None
+ cList.remove(cp)
+ if len(cList)<2:
+ break
+ cp = min(cList, key=lambda cp:cp.size)
+ #print ' -----> ', [ (cp.c1.indices , cp.c2.indices) for cp in cList]
+ return cList
+
+ cpList = reduceCL(cpList)
+ if len(cpList)==1:
+ cp = cpList[0]
+ if cp.potentialC.size()