279 lines
11 KiB
Python
279 lines
11 KiB
Python
|
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
|