2020-08-09 21:25:19 +02:00
#!/usr/bin/env python3
"""
Extension for InkScape 1.0
Features
- helps to find contours which are closed or not . Good for repairing contours , closing contours , . . .
- works for paths which are packed into groups or groups of groups . #
- can break contours apart like in " Path -> Break Apart "
2020-08-13 01:28:19 +02:00
- implements Bentley - Ottmann algorithm from https : / / github . com / ideasman42 / isect_segments - bentley_ottmann to scan for self - intersecting paths . You might get " assert(event.in_sweep == False) AssertionError " . Don ' t know how to fix rgis
2020-08-09 21:25:19 +02:00
- colorized paths respective to their type
- can add dots to intersection points you ' d like to fix
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
Date : 09.08 .2020
2021-04-15 17:04:18 +02:00
Last patch : 14.04 .2021
2020-08-09 21:25:19 +02:00
License : GNU GPL v3
"""
2021-04-14 20:54:10 +02:00
import sys
2020-08-09 21:25:19 +02:00
from math import *
from lxml import etree
2020-09-03 00:35:27 +02:00
import poly_point_isect
2020-08-09 21:25:19 +02:00
import copy
2021-04-14 20:54:10 +02:00
import inkex
from inkex . paths import Path , CubicSuperPath
from inkex import Style , Color , Circle
2020-08-09 21:25:19 +02:00
2021-04-16 14:41:12 +02:00
class ContourScanner ( inkex . EffectExtension ) :
2020-08-09 21:25:19 +02:00
2021-04-16 14:41:12 +02:00
def add_arguments ( self , pars ) :
pars . add_argument ( " --main_tabs " )
pars . add_argument ( " --breakapart " , type = inkex . Boolean , default = False , help = " Break apart selection into single contours " )
pars . add_argument ( " --apply_transformations " , type = inkex . Boolean , default = False , help = " Run ' Apply Transformations ' extension before running to avoid IndexErrors in calculation. " )
pars . add_argument ( " --removefillsetstroke " , type = inkex . Boolean , default = False , help = " Remove fill and define stroke " )
pars . add_argument ( " --strokewidth " , type = float , default = 1.0 , help = " Stroke width (px) " )
pars . add_argument ( " --highlight_opened " , type = inkex . Boolean , default = True , help = " Highlight opened contours " )
pars . add_argument ( " --color_opened " , type = Color , default = ' 4012452351 ' , help = " Color opened contours " )
pars . add_argument ( " --highlight_closed " , type = inkex . Boolean , default = True , help = " Highlight closed contours " )
pars . add_argument ( " --color_closed " , type = Color , default = ' 2330080511 ' , help = " Color closed contours " )
pars . add_argument ( " --highlight_selfintersecting " , type = inkex . Boolean , default = True , help = " Highlight self-intersecting contours " )
pars . add_argument ( " --highlight_intersectionpoints " , type = inkex . Boolean , default = True , help = " Highlight self-intersecting points " )
pars . add_argument ( " --color_selfintersecting " , type = Color , default = ' 1923076095 ' , help = " Color closed contours " )
pars . add_argument ( " --color_intersectionpoints " , type = Color , default = ' 4239343359 ' , help = " Color closed contours " )
pars . add_argument ( " --addlines " , type = inkex . Boolean , default = True , help = " Add closing lines for self-crossing contours " )
pars . add_argument ( " --polypaths " , type = inkex . Boolean , default = True , help = " Add polypath outline for self-crossing contours " )
pars . add_argument ( " --dotsize " , type = int , default = 10 , help = " Dot size (px) for self-intersecting points " )
pars . add_argument ( " --remove_opened " , type = inkex . Boolean , default = False , help = " Remove opened contours " )
pars . add_argument ( " --remove_closed " , type = inkex . Boolean , default = False , help = " Remove closed contours " )
pars . add_argument ( " --remove_selfintersecting " , type = inkex . Boolean , default = False , help = " Remove self-intersecting contours " )
pars . add_argument ( " --show_debug " , type = inkex . Boolean , default = False , help = " Show debug info " )
2021-04-14 20:54:10 +02:00
2020-09-05 16:19:32 +02:00
#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 )
2020-08-09 21:25:19 +02:00
#split combined contours into single contours if enabled - this is exactly the same as "Path -> Break Apart"
2020-09-05 16:19:32 +02:00
replacedNodes = [ ]
def breakContours ( self , node ) : #this does the same as "CTRL + SHIFT + K"
2020-08-09 21:25:19 +02:00
if node . tag == inkex . addNS ( ' path ' , ' svg ' ) :
parent = node . getparent ( )
idx = parent . index ( node )
idSuffix = 0
raw = Path ( node . get ( " d " ) ) . to_arrays ( )
2020-09-05 16:19:32 +02:00
subPaths , prev = [ ] , 0
2020-08-09 21:25:19 +02:00
for i in range ( len ( raw ) ) : # Breaks compound paths into simple paths
if raw [ i ] [ 0 ] == ' M ' and i != 0 :
2020-09-05 16:19:32 +02:00
subPaths . append ( raw [ prev : i ] )
2020-08-09 21:25:19 +02:00
prev = i
2020-09-05 16:19:32 +02:00
subPaths . append ( raw [ prev : ] )
for subpath in subPaths :
2020-08-09 21:25:19 +02:00
replacedNode = copy . copy ( node )
oldId = replacedNode . get ( ' id ' )
replacedNode . set ( ' d ' , CubicSuperPath ( subpath ) )
replacedNode . set ( ' id ' , oldId + str ( idSuffix ) . zfill ( 5 ) )
parent . insert ( idx , replacedNode )
idSuffix + = 1
2020-09-05 00:54:03 +02:00
self . replacedNodes . append ( replacedNode )
2021-04-18 18:21:23 +02:00
node . delete ( )
2020-08-09 21:25:19 +02:00
for child in node :
self . breakContours ( child )
def scanContours ( self , node ) :
if node . tag == inkex . addNS ( ' path ' , ' svg ' ) :
if self . options . removefillsetstroke :
2020-09-05 16:19:32 +02:00
self . adjustStyle ( node )
2020-08-09 21:25:19 +02:00
2020-09-05 16:19:32 +02:00
intersectionGroup = node . getparent ( ) . add ( inkex . Group ( ) )
2020-08-09 21:25:19 +02:00
raw = ( Path ( node . get ( ' d ' ) ) . to_arrays ( ) )
2020-09-05 16:19:32 +02:00
subPaths , prev = [ ] , 0
2020-08-09 21:25:19 +02:00
for i in range ( len ( raw ) ) : # Breaks compound paths into simple paths
if raw [ i ] [ 0 ] == ' M ' and i != 0 :
2020-09-05 16:19:32 +02:00
subPaths . append ( raw [ prev : i ] )
2020-08-09 21:25:19 +02:00
prev = i
2020-09-05 16:19:32 +02:00
subPaths . append ( raw [ prev : ] )
2020-08-09 21:25:19 +02:00
2020-09-05 16:19:32 +02:00
for simpath in subPaths :
2020-08-09 21:25:19 +02:00
closed = False
if simpath [ - 1 ] [ 0 ] == ' Z ' :
closed = True
if simpath [ - 2 ] [ 0 ] == ' L ' : simpath [ - 1 ] [ 1 ] = simpath [ 0 ] [ 1 ]
else : simpath . pop ( )
2020-09-05 02:33:07 +02:00
points = [ ]
2020-08-09 21:25:19 +02:00
for i in range ( len ( simpath ) ) :
if simpath [ i ] [ 0 ] == ' V ' : # vertical and horizontal lines only have one point in args, but 2 are required
simpath [ i ] [ 0 ] = ' L ' #overwrite V with regular L command
add = simpath [ i - 1 ] [ 1 ] [ 0 ] #read the X value from previous segment
simpath [ i ] [ 1 ] . append ( simpath [ i ] [ 1 ] [ 0 ] ) #add the second (missing) argument by taking argument from previous segment
simpath [ i ] [ 1 ] [ 0 ] = add #replace with recent X after Y was appended
if simpath [ i ] [ 0 ] == ' H ' : # vertical and horizontal lines only have one point in args, but 2 are required
simpath [ i ] [ 0 ] = ' L ' #overwrite H with regular L command
2020-09-05 02:33:07 +02:00
simpath [ i ] [ 1 ] . append ( simpath [ i - 1 ] [ 1 ] [ 1 ] ) #add the second (missing) argument by taking argument from previous segment
points . append ( simpath [ i ] [ 1 ] [ - 2 : ] )
if points [ 0 ] == points [ - 1 ] : #if first is last point the path is also closed. The "Z" command is not required
closed = True
if closed == False :
if self . options . highlight_opened :
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_opened , ' stroke-linecap ' : ' butt ' , ' fill ' : ' none ' }
node . attrib [ ' style ' ] = Style ( style ) . to_str ( )
if self . options . remove_opened :
try :
2021-04-18 18:21:23 +02:00
node . delete ( )
2020-09-05 02:33:07 +02:00
except AttributeError :
pass #we ignore that parent can be None
if closed == True :
if self . options . highlight_closed :
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_closed , ' stroke-linecap ' : ' butt ' , ' fill ' : ' none ' }
node . attrib [ ' style ' ] = Style ( style ) . to_str ( )
if self . options . remove_closed :
try :
2021-04-18 18:21:23 +02:00
node . delete ( )
2020-09-05 02:33:07 +02:00
except AttributeError :
pass #we ignore that parent can be None
2020-08-09 21:25:19 +02:00
#if one of the options is activated we also check for self-intersecting
2020-09-05 16:19:32 +02:00
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 ( )
2020-08-09 21:25:19 +02:00
try :
2020-09-12 22:25:05 +02:00
if len ( points ) > 2 : #try to find self-intersecting /overlapping polygons. We need at least 3 points to detect for intersections (only possible if first points matched last point)
2021-04-27 14:38:04 +02:00
isect = poly_point_isect . isect_polygon ( points , validate = True )
2020-08-09 21:25:19 +02:00
if len ( isect ) > 0 :
2020-09-05 02:33:07 +02:00
if closed == False and self . options . addlines == True : #if contour is open and we found intersection points those points might be not relevant
2020-09-05 16:19:32 +02:00
closingLine = intersectionGroup . add ( inkex . PathElement ( ) )
closingLine . set ( ' id ' , self . svg . get_unique_id ( ' closingline- ' ) )
closingLine . path = [
2020-09-05 02:33:07 +02:00
[ ' M ' , [ points [ 0 ] [ 0 ] , points [ 0 ] [ 1 ] ] ] ,
[ ' L ' , [ points [ - 1 ] [ 0 ] , points [ - 1 ] [ 1 ] ] ] ,
[ ' Z ' , [ ] ]
]
2020-09-05 16:19:32 +02:00
closingLine . attrib [ ' style ' ] = closingLineStyle
#draw polylines if option is enabled
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
2020-08-09 21:25:19 +02:00
#make dot markings at the intersection points
if self . options . highlight_intersectionpoints :
for xy in isect :
#Add a dot label for this path element
2020-09-05 16:19:32 +02:00
intersectionPoint = intersectionGroup . 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- ' ) )
intersectionPoint . style = intersectionPointStyle
2020-08-09 21:25:19 +02:00
if self . options . highlight_selfintersecting :
2020-09-05 16:19:32 +02:00
node . attrib [ ' style ' ] = intersectionStyle
2020-08-09 21:25:19 +02:00
if self . options . remove_selfintersecting :
if node . getparent ( ) is not None : #might be already been deleted by previously checked settings so check again
2021-04-18 18:21:23 +02:00
node . delete ( )
2020-09-05 16:19:32 +02:00
#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
2020-09-12 22:25:05 +02:00
except AssertionError as e : # we skip AssertionError
2021-04-14 20:54:10 +02:00
if self . options . show_debug is True :
inkex . utils . debug ( " AssertionError at " + node . get ( ' id ' ) )
2020-12-20 19:13:25 +01:00
continue
2021-04-14 20:54:10 +02:00
except IndexError as i : # we skip IndexError
if self . options . show_debug is True :
inkex . utils . debug ( " IndexError at " + node . get ( ' id ' ) )
continue
2020-09-05 16:19:32 +02:00
#if the intersectionGroup was created but nothing attached we delete it again to prevent messing the SVG XML tree
if len ( intersectionGroup . getchildren ( ) ) == 0 :
intersectionGroupParent = intersectionGroup . getparent ( )
if intersectionGroupParent is not None :
2021-04-18 18:21:23 +02:00
intersectionGroup . delete ( )
2020-09-05 16:19:32 +02:00
#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
2020-09-05 02:33:07 +02:00
elif self . options . remove_selfintersecting == False :
2020-09-05 16:19:32 +02:00
intersectionGroup . insert ( 0 , node )
2020-09-05 02:33:07 +02:00
children = node . getchildren ( )
if children is not None :
for child in children :
self . scanContours ( child )
2020-08-09 21:25:19 +02:00
def effect ( self ) :
2021-04-14 20:54:10 +02:00
applyTransformAvailable = False
# at first we apply external extension
try :
2021-05-15 15:04:22 +02:00
sys . path . append ( " ../applytransform " ) # add parent directory to path to allow importing applytransform (vpype extension is encapsulated in sub directory)
2021-04-14 20:54:10 +02:00
import applytransform
applyTransformAvailable = True
except Exception as e :
#inkex.utils.debug(e)
inkex . utils . debug ( " Calling ' Apply Transformations ' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping this step " )
'''
we need to apply transfoms to the complete document even if there are only some single paths selected .
If we apply it to selected nodes only the parent groups still might contain transforms .
This messes with the coordinates and creates hardly controllable behaviour
'''
if self . options . apply_transformations is True and applyTransformAvailable is True :
applytransform . ApplyTransform ( ) . recursiveFuseTransform ( self . document . getroot ( ) )
2020-08-09 21:25:19 +02:00
if self . options . breakapart :
if len ( self . svg . selected ) == 0 :
self . breakContours ( self . document . getroot ( ) )
self . scanContours ( self . document . getroot ( ) )
else :
newContourSet = [ ]
2021-04-19 22:07:01 +02:00
for element in self . svg . selected . items ( ) :
self . breakContours ( element )
2020-09-05 00:54:03 +02:00
for newContours in self . replacedNodes :
self . scanContours ( newContours )
2020-08-09 21:25:19 +02:00
else :
if len ( self . svg . selected ) == 0 :
self . scanContours ( self . document . getroot ( ) )
else :
2021-04-19 22:07:01 +02:00
for element in self . svg . selected . values ( ) :
self . scanContours ( element )
2020-08-09 21:25:19 +02:00
2020-08-31 21:25:41 +02:00
if __name__ == ' __main__ ' :
ContourScanner ( ) . run ( )