More fixes for contour scanner and polyline converter

This commit is contained in:
Mario Voigt 2020-09-05 16:19:32 +02:00
parent 8003f7f11b
commit d1fe45451f
3 changed files with 117 additions and 56 deletions

View File

@ -19,6 +19,7 @@
<param name="color_intersectionpoints" type="color" appearance="colorbutton" gui-text="Color self-intersecting points">4239343359</param> <param name="color_intersectionpoints" type="color" appearance="colorbutton" gui-text="Color self-intersecting points">4239343359</param>
<param name="dotsize" type="int" min="0" max="10000" gui-text="Dot size (px) for self-intersecting points">10</param> <param name="dotsize" type="int" min="0" max="10000" gui-text="Dot size (px) for self-intersecting points">10</param>
<param name="addlines" type="bool" gui-text="Add closing lines for open self-crossing contours" gui-description="They will have the same color as the intersection points and help to better visualize possible virtual crossings. The algorithm can only detect intersections for closed contours by it's nature, but we handle open contours like they were closed. This may put put too much intersection points.">true</param> <param name="addlines" type="bool" gui-text="Add closing lines for open self-crossing contours" gui-description="They will have the same color as the intersection points and help to better visualize possible virtual crossings. The algorithm can only detect intersections for closed contours by it's nature, but we handle open contours like they were closed. This may put put too much intersection points.">true</param>
<param name="polypaths" type="bool" gui-text="Add polypath outline for self-crossing contours" gui-description="This makes only sense if your path is actually a curve. If it's already a polyline you just get a duplicate line (but with reduced nodes)">true</param>
<label appearance="header">Remove paths</label> <label appearance="header">Remove paths</label>
<param name="remove_opened" type="bool" gui-text="Remove opened contours">false</param> <param name="remove_opened" type="bool" gui-text="Remove opened contours">false</param>
<param name="remove_closed" type="bool" gui-text="Remove closed contours">false</param> <param name="remove_closed" type="bool" gui-text="Remove closed contours">false</param>

View File

@ -25,24 +25,6 @@ from lxml import etree
import poly_point_isect import poly_point_isect
import copy import copy
def adjustStyle(self, node):
if node.attrib.has_key('style'):
style = node.get('style')
if style:
declarations = style.split(';')
for i,decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke-width':
declarations[i] = prop + ':' + str(self.svg.unittouu(str(self.options.strokewidth) +"px"))
if prop == 'fill':
declarations[i] = prop + ':none'
node.set('style', ';'.join(declarations) + ';stroke:#000000;stroke-opacity:1.0')
else:
node.set('style', 'stroke:#000000;stroke-opacity:1.0')
class ContourScanner(inkex.Effect): class ContourScanner(inkex.Effect):
def __init__(self): def __init__(self):
@ -59,28 +41,66 @@ class ContourScanner(inkex.Effect):
self.arg_parser.add_argument("--color_selfintersecting", type=Color, default='1923076095', help="Color closed contours") self.arg_parser.add_argument("--color_selfintersecting", type=Color, default='1923076095', help="Color closed contours")
self.arg_parser.add_argument("--color_intersectionpoints", type=Color, default='4239343359', help="Color closed contours") self.arg_parser.add_argument("--color_intersectionpoints", type=Color, default='4239343359', help="Color closed contours")
self.arg_parser.add_argument("--addlines", type=inkex.Boolean, default=True, help="Add closing lines for self-crossing contours") self.arg_parser.add_argument("--addlines", type=inkex.Boolean, default=True, help="Add closing lines for self-crossing contours")
self.arg_parser.add_argument("--polypaths", type=inkex.Boolean, default=True, help="Add polypath outline for self-crossing contours")
self.arg_parser.add_argument("--dotsize", type=int, default=10, help="Dot size (px) for self-intersecting points") self.arg_parser.add_argument("--dotsize", type=int, default=10, help="Dot size (px) for self-intersecting points")
self.arg_parser.add_argument("--remove_opened", type=inkex.Boolean, default=False, help="Remove opened contours") self.arg_parser.add_argument("--remove_opened", type=inkex.Boolean, default=False, help="Remove opened contours")
self.arg_parser.add_argument("--remove_closed", type=inkex.Boolean, default=False, help="Remove closed contours") self.arg_parser.add_argument("--remove_closed", type=inkex.Boolean, default=False, help="Remove closed contours")
self.arg_parser.add_argument("--remove_selfintersecting", type=inkex.Boolean, default=False, help="Remove self-intersecting contours") self.arg_parser.add_argument("--remove_selfintersecting", type=inkex.Boolean, default=False, help="Remove self-intersecting contours")
self.arg_parser.add_argument("--main_tabs") self.arg_parser.add_argument("--main_tabs")
#function to refine the style of the lines
def adjustStyle(self, node):
if node.attrib.has_key('style'):
style = node.get('style')
if style:
declarations = style.split(';')
for i,decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke-width':
declarations[i] = prop + ':' + str(self.svg.unittouu(str(self.options.strokewidth) +"px"))
if prop == 'fill':
declarations[i] = prop + ':none'
node.set('style', ';'.join(declarations) + ';stroke:#000000;stroke-opacity:1.0')
else:
node.set('style', 'stroke:#000000;stroke-opacity:1.0')
#get polyline from path
def getPolyline(self, node):
if node.tag == inkex.addNS('path','svg'):
polypath = []
i = 0
for x, y in node.path.end_points:
if i == 0:
polypath.append(['M', [x,y]])
else:
polypath.append(['L', [x,y]])
if i == 1 and polypath[len(polypath)-2][1] == polypath[len(polypath)-1][1]:
polypath.pop(len(polypath)-1) #special handling for the seconds point after M command
elif polypath[len(polypath)-2] == polypath[len(polypath)-1]: #get the previous point
polypath.pop(len(polypath)-1)
i += 1
return Path(polypath)
#split combined contours into single contours if enabled - this is exactly the same as "Path -> Break Apart" #split combined contours into single contours if enabled - this is exactly the same as "Path -> Break Apart"
replacedNodes = [] replacedNodes = []
def breakContours(self, node): #this does the same as "CTRL + SHIFT + K"
def breakContours(self, node):
if node.tag == inkex.addNS('path','svg'): if node.tag == inkex.addNS('path','svg'):
parent = node.getparent() parent = node.getparent()
idx = parent.index(node) idx = parent.index(node)
idSuffix = 0 idSuffix = 0
raw = Path(node.get("d")).to_arrays() raw = Path(node.get("d")).to_arrays()
subpaths, prev = [], 0 subPaths, prev = [], 0
for i in range(len(raw)): # Breaks compound paths into simple paths for i in range(len(raw)): # Breaks compound paths into simple paths
if raw[i][0] == 'M' and i != 0: if raw[i][0] == 'M' and i != 0:
subpaths.append(raw[prev:i]) subPaths.append(raw[prev:i])
prev = i prev = i
subpaths.append(raw[prev:]) subPaths.append(raw[prev:])
for subpath in subpaths: for subpath in subPaths:
replacedNode = copy.copy(node) replacedNode = copy.copy(node)
oldId = replacedNode.get('id') oldId = replacedNode.get('id')
@ -96,19 +116,19 @@ class ContourScanner(inkex.Effect):
def scanContours(self, node): def scanContours(self, node):
if node.tag == inkex.addNS('path','svg'): if node.tag == inkex.addNS('path','svg'):
if self.options.removefillsetstroke: if self.options.removefillsetstroke:
adjustStyle(self, node) self.adjustStyle(node)
dot_group = node.getparent().add(inkex.Group()) intersectionGroup = node.getparent().add(inkex.Group())
raw = (Path(node.get('d')).to_arrays()) raw = (Path(node.get('d')).to_arrays())
subpaths, prev = [], 0 subPaths, prev = [], 0
for i in range(len(raw)): # Breaks compound paths into simple paths for i in range(len(raw)): # Breaks compound paths into simple paths
if raw[i][0] == 'M' and i != 0: if raw[i][0] == 'M' and i != 0:
subpaths.append(raw[prev:i]) subPaths.append(raw[prev:i])
prev = i prev = i
subpaths.append(raw[prev:]) subPaths.append(raw[prev:])
for simpath in subpaths: for simpath in subPaths:
closed = False closed = False
if simpath[-1][0] == 'Z': if simpath[-1][0] == 'Z':
closed = True closed = True
@ -152,49 +172,84 @@ class ContourScanner(inkex.Effect):
pass #we ignore that parent can be None pass #we ignore that parent can be None
#if one of the options is activated we also check for self-intersecting #if one of the options is activated we also check for self-intersecting
if self.options.highlight_selfintersecting or self.options.highlight_intersectionpoints: if self.options.highlight_selfintersecting or self.options.highlight_intersectionpoints:
#Style definitions
closingLineStyle = Style({'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_intersectionpoints, 'stroke-linecap': 'butt', 'fill': 'none'}).to_str()
intersectionPointStyle = Style({'stroke': 'none', 'fill': self.options.color_intersectionpoints}).to_str()
intersectionStyle = Style({'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_selfintersecting, 'stroke-linecap': 'butt', 'fill': 'none'}).to_str()
try: try:
if len(points) > 0: #try to find self-intersecting /overlapping polygons if len(points) > 0: #try to find self-intersecting /overlapping polygons
isect = poly_point_isect.isect_polygon(points) isect = poly_point_isect.isect_polygon(points)
if len(isect) > 0: if len(isect) > 0:
if closed == False and self.options.addlines == True: #if contour is open and we found intersection points those points might be not relevant if closed == False and self.options.addlines == True: #if contour is open and we found intersection points those points might be not relevant
line = dot_group.add(inkex.PathElement()) closingLine = intersectionGroup.add(inkex.PathElement())
line.path = [ closingLine.set('id', self.svg.get_unique_id('closingline-'))
closingLine.path = [
['M', [points[0][0],points[0][1]]], ['M', [points[0][0],points[0][1]]],
['L', [points[-1][0],points[-1][1]]], ['L', [points[-1][0],points[-1][1]]],
['Z', []] ['Z', []]
] ]
style = {'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")), closingLine.attrib['style'] = closingLineStyle
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_intersectionpoints, 'stroke-linecap': 'butt', 'fill': 'none'} #draw polylines if option is enabled
line.attrib['style'] = Style(style).to_str() if self.options.polypaths == True:
polyNode = intersectionGroup.add(inkex.PathElement())
polyNode.set('id', self.svg.get_unique_id('polypath-'))
polyNode.set('d', str(self.getPolyline(node)))
polyNode.attrib['style'] = closingLineStyle
#make dot markings at the intersection points #make dot markings at the intersection points
if self.options.highlight_intersectionpoints: if self.options.highlight_intersectionpoints:
for xy in isect: for xy in isect:
#Add a dot label for this path element #Add a dot label for this path element
style = inkex.Style({'stroke': 'none', 'fill': self.options.color_intersectionpoints}) intersectionPoint = intersectionGroup.add(Circle(cx=str(xy[0]), cy=str(xy[1]), r=str(self.svg.unittouu(str(self.options.dotsize/2) + "px"))))
circle = dot_group.add(Circle(cx=str(xy[0]), cy=str(xy[1]), r=str(self.svg.unittouu(str(self.options.dotsize/2) + "px")))) intersectionPoint.set('id', self.svg.get_unique_id('intersectionpoint-'))
circle.style = style intersectionPoint.style = intersectionPointStyle
if self.options.highlight_selfintersecting: if self.options.highlight_selfintersecting:
style = {'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")), node.attrib['style'] = intersectionStyle
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_selfintersecting, 'stroke-linecap': 'butt', 'fill': 'none'}
node.attrib['style'] = Style(style).to_str()
if self.options.remove_selfintersecting: if self.options.remove_selfintersecting:
if node.getparent() is not None: #might be already been deleted by previously checked settings so check again if node.getparent() is not None: #might be already been deleted by previously checked settings so check again
node.getparent().remove(node) node.getparent().remove(node)
#draw intersections segment lines - useless at the moment. We could use this information to cut the original polyline to get a new curve path which included the intersection points
#isectSegs = poly_point_isect.isect_polygon_include_segments(points)
#for seg in isectSegs:
# isectSegsPath = []
# isecX = seg[0][0] #the intersection point - X
# isecY = seg[0][1] #the intersection point - Y
# isecSeg1X = seg[1][0][0][0] #the first intersection point segment - X
# isecSeg1Y = seg[1][0][0][1] #the first intersection point segment - Y
# isecSeg2X = seg[1][1][0][0] #the second intersection point segment - X
# isecSeg2Y = seg[1][1][0][1] #the second intersection point segment - Y
# isectSegsPath.append(['L', [isecSeg2X, isecSeg2Y]])
# isectSegsPath.append(['L', [isecX, isecY]])
# isectSegsPath.append(['L', [isecSeg1X, isecSeg1Y]])
# #fix the really first point. Has to be an 'M' command instead of 'L'
# isectSegsPath[0][0] = 'M'
# polySegsNode = intersectionGroup.add(inkex.PathElement())
# polySegsNode.set('id', self.svg.get_unique_id('intersectsegments-'))
# polySegsNode.set('d', str(Path(isectSegsPath)))
# polySegsNode.attrib['style'] = closingLineStyle
except Exception as e: # we skip AssertionError except Exception as e: # we skip AssertionError
#inkex.utils.debug("Accuracy Error. Try to reduce the precision of the paths using the extension called Rounder to cutoff unrequired decimals.") inkex.utils.debug(str(e))
print(str(e)) #if the intersectionGroup was created but nothing attached we delete it again to prevent messing the SVG XML tree
#if the dot_group was created but nothing attached we delete it again to prevent messing the SVG XML tree if len(intersectionGroup.getchildren()) == 0:
if len(dot_group.getchildren()) == 0: intersectionGroupParent = intersectionGroup.getparent()
dot_parent = dot_group.getparent() if intersectionGroupParent is not None:
if dot_parent is not None: intersectionGroup.getparent().remove(intersectionGroup)
dot_group.getparent().remove(dot_group) #put the node into the intersectionGroup to bundle the path with it's error markers. If removal is selected we need to avoid intersectionGroup.insert(), because it will break the removal
#put the node into the dot_group to bundle the path with it's error markers. If removal is selected we need to avoid dot_group.insert(), because it will break the removal
elif self.options.remove_selfintersecting == False: elif self.options.remove_selfintersecting == False:
dot_group.insert(0, node) intersectionGroup.insert(0, node)
children = node.getchildren() children = node.getchildren()
if children is not None: if children is not None:
for child in children: for child in children:

View File

@ -20,6 +20,7 @@ class ConvertToPolylines(inkex.Effect):
def __init__(self): def __init__(self):
inkex.Effect.__init__(self) inkex.Effect.__init__(self)
#convert a path (curve) to a polyline and remove dangling/duplicate/useless overlapping handles (points)
def convertPath(self, node): def convertPath(self, node):
if node.tag == inkex.addNS('path','svg'): if node.tag == inkex.addNS('path','svg'):
polypath = [] polypath = []
@ -29,6 +30,10 @@ class ConvertToPolylines(inkex.Effect):
polypath.append(['M', [x,y]]) polypath.append(['M', [x,y]])
else: else:
polypath.append(['L', [x,y]]) polypath.append(['L', [x,y]])
if i == 1 and polypath[len(polypath)-2][1] == polypath[len(polypath)-1][1]:
polypath.pop(len(polypath)-1) #special handling for the seconds point after M command
elif polypath[len(polypath)-2] == polypath[len(polypath)-1]: #get the previous point
polypath.pop(len(polypath)-1)
i += 1 i += 1
node.set('d', str(Path(polypath))) node.set('d', str(Path(polypath)))
children = node.getchildren() children = node.getchildren()