2021-06-01 21:37:08 +02:00
#!/usr/bin/env python3
'''
Extension for InkScape 1.0 +
- WARNING : HORRIBLY SLOW CODE . PLEASE HELP TO MAKE IT USEFUL FOR LARGE AMOUNT OF PATHS
- add options :
2021-06-04 07:06:13 +02:00
- efficiently find overlapping colinear lines by checking their slope / gradient
- get all lines and sort by slope ; kick out all slopes which are unique . We only want re - occuring slopes
- intersects ( ) is equivalent to the OR - ing of contains ( ) , crosses ( ) , equals ( ) , touches ( ) , and within ( ) .
So there might be some cases where two lines intersect eachother without crossing ,
in particular when one line contains another or when two lines are equals .
- crosses ( ) returns True if the dimension of the intersection is less than the dimension of the one or the other .
So if two lines overlap , they won ' t be considered as " crossing " . intersection() will return a geometric object.
2021-06-01 21:37:08 +02:00
- replace trimmed paths by bezier paths ( calculating lengths and required t parameter )
- find more duplicates
- overlapping lines in sub splits
- overlapping in original selection
- duplicates in original selection
- duplicates in split bezier
- . . .
2021-06-04 11:25:43 +02:00
- refactor subSplitData stuff by some clean mapping / structure instead having a lot of useless arays
2021-06-05 19:42:33 +02:00
- maybe option : convert abs path to rel path
- maybe option : convert rel path to abs path
replacedelement . path = replacedelement . path . to_absolute ( ) . to_superpath ( ) . to_path ( )
- maybe option : break apart while keeping relative / absolute commands ( more complex and not sure if we have a great advantage having this )
2021-06-01 21:37:08 +02:00
- important to notice
- this algorithm might be really slow . Reduce flattening quality to speed up
- the code quality is horrible . We need a lot of asserts and functions to structure that stuff
- try to adjust snap tolerance and flatness in case of errors , like
poly_point_isect . py : " KeyError: ' Event(0x21412ce81c0, s0=(47.16, 179.1),
s1 = ( 47.17 , 178.21 ) , p = ( 47.16 , 179.1 ) , type = 2 , slope = - 88.9999999999531 ) ' "
- this extension does not check for strange paths . Please ensure that your path ' d '
data is valid ( no pointy paths , no duplicates , etc . )
- we do not use shapely to look for intersections by cutting each line against
each other line ( line1 . intersection ( line2 ) using two for - loops ) because this
kind of logic is really really slow for huge amount . You could use that only
for ~ 50 - 100 elements . So we use special algorihm ( Bentley - Ottmann )
2021-06-04 07:06:13 +02:00
- Cool tool to visualize sweep line algorithm Bentley - Ottmann : https : / / bl . ocks . org / 1 wheel / 464141 fe9b940153e636
2021-06-01 21:37:08 +02:00
- things to look at more closely :
- https : / / gis . stackexchange . com / questions / 203048 / split - lines - at - points - using - shapely
- https : / / stackoverflow . com / questions / 34754777 / shapely - split - linestrings - at - intersections - with - other - linestrings
- There are floating point precision errors when finding a point on a line . Use the distance with an appropriate threshold instead .
- line . within ( point ) # False
- line . distance ( point ) # 7.765244949417793e-11
- line . distance ( point ) < 1e-8 # True
- https : / / bezier . readthedocs . io / en / stable / python / reference / bezier . hazmat . clipping . html / https : / / github . com / dhermes / bezier
- De Casteljau Algorithm
Author : Mario Voigt / FabLab Chemnitz
Mail : mario . voigt @stadtfabrikanten.org
Date : 09.08 .2020 ( extension originally called " Contour Scanner " )
2021-06-05 19:42:33 +02:00
Last patch : 05.06 .2021
2021-06-01 21:37:08 +02:00
License : GNU GPL v3
2021-06-04 06:13:42 +02:00
2021-06-01 21:37:08 +02:00
'''
import sys
import os
import copy
from lxml import etree
import poly_point_isect
from poly_point_isect import isect_segments
import inkex
from inkex import transforms , bezier , PathElement , Color , Circle
from inkex . bezier import csplength
from inkex . paths import Path , CubicSuperPath
from shapely . geometry import LineString , Point , MultiPoint
from shapely . ops import snap , split
from shapely import speedups
if speedups . available :
speedups . enable ( )
idPrefix = " subsplit "
intersectedVerb = " -intersected- "
class ContourScannerAndTrimmer ( inkex . EffectExtension ) :
def breakContours ( self , element , breakelements = None ) :
2021-06-05 16:39:54 +02:00
'''
this does the same as " CTRL + SHIFT + K "
This functions honors the fact of absolute or relative paths !
'''
2021-06-01 21:37:08 +02:00
if breakelements == None :
breakelements = [ ]
if element . tag == inkex . addNS ( ' path ' , ' svg ' ) :
parent = element . getparent ( )
idx = parent . index ( element )
2021-06-05 16:39:54 +02:00
idSuffix = 0
2021-06-05 19:42:33 +02:00
#raw = str(element.path).split()
raw = element . path . to_arrays ( )
subPaths = [ ]
prev = 0
2021-06-01 21:37:08 +02:00
for i in range ( len ( raw ) ) : # Breaks compound paths into simple paths
2021-06-05 19:42:33 +02:00
#if raw[i][0].upper() == 'M' and i != 0:
if raw [ i ] [ 0 ] == ' M ' and i != 0 :
subPath = raw [ prev : i ]
subPaths . append ( Path ( subPath ) )
2021-06-01 21:37:08 +02:00
prev = i
2021-06-05 19:42:33 +02:00
subPaths . append ( Path ( raw [ prev : ] ) ) #finally add the last path
for subPath in subPaths :
2021-06-01 21:37:08 +02:00
replacedelement = copy . copy ( element )
oldId = replacedelement . get ( ' id ' )
2021-06-05 19:42:33 +02:00
csp = CubicSuperPath ( subPath )
if len ( subPath ) > 1 and csp [ 0 ] [ 0 ] != csp [ 0 ] [ 1 ] : #avoids pointy paths like M "31.4794 57.6024 Z"
replacedelement . path = subPath
2021-06-02 19:40:39 +02:00
replacedelement . set ( ' id ' , oldId + str ( idSuffix ) )
parent . insert ( idx , replacedelement )
idSuffix + = 1
breakelements . append ( replacedelement )
2021-06-05 16:39:54 +02:00
element . delete ( )
2021-06-01 21:37:08 +02:00
for child in element . getchildren ( ) :
self . breakContours ( child , breakelements )
return breakelements
def getChildPaths ( self , element , elements = None ) :
''' a function to get child paths from elements (used by " handling groups " option) '''
if elements == None :
elements = [ ]
if element . tag == inkex . addNS ( ' path ' , ' svg ' ) :
elements . append ( element )
for child in element . getchildren ( ) :
self . getChildPaths ( child , elements )
return elements
def getPathElements ( self ) :
''' get all path elements, either from selection or from whole document. Uses options '''
pathElements = [ ]
if len ( self . svg . selected ) == 0 : #if nothing selected we search for the complete document
pathElements = self . document . xpath ( ' //svg:path ' , namespaces = inkex . NSS )
else : # or get selected paths (and children) and convert them to shapely LineString objects
if self . options . handle_groups is False :
pathElements = list ( self . svg . selection . filter ( PathElement ) . values ( ) )
else :
for element in self . svg . selection . values ( ) :
pathElements = self . getChildPaths ( element , pathElements )
if len ( pathElements ) == 0 :
self . msg ( ' Selection appears to be empty or does not contain any valid svg:path nodes. Try to cast your objects to paths using CTRL + SHIFT + C or strokes to paths using CTRL + ALT + C ' )
return
if self . options . break_apart is True :
breakApartElements = None
for pathElement in pathElements :
breakApartElements = self . breakContours ( pathElement , breakApartElements )
pathElements = breakApartElements
if self . options . show_debug is True :
self . msg ( " total processing paths count: {} " . format ( len ( pathElements ) ) )
2021-06-04 06:13:42 +02:00
return pathElements
2021-06-01 21:37:08 +02:00
2021-06-05 19:42:33 +02:00
2021-06-01 21:37:08 +02:00
def findGroup ( self , groupId ) :
''' check if a group with a given id exists or not. Returns None if not found, else returns the group element '''
groups = self . document . xpath ( ' //svg:g ' , namespaces = inkex . NSS )
for group in groups :
#self.msg(str(layer.get('inkscape:label')) + " == " + layerName)
if group . get ( ' id ' ) == groupId :
return group
return None
def adjustStyle ( self , element ) :
''' Replace some style attributes of the given element '''
if element . attrib . has_key ( ' style ' ) :
style = element . 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 '
element . set ( ' style ' , ' ; ' . join ( declarations ) + ' ;stroke:#000000;stroke-opacity:1.0 ' )
else :
element . set ( ' style ' , ' stroke:#000000;stroke-opacity:1.0 ' )
def lineFromSegments ( self , segs , i , decimals ) :
''' builds a straight line for the segment i and the next segment i+2. Returns both point XY coordinates '''
pseudoPath = Path ( segs [ i : i + 2 ] ) . to_arrays ( )
x1 = round ( pseudoPath [ 0 ] [ 1 ] [ - 2 ] , decimals )
y1 = round ( pseudoPath [ 0 ] [ 1 ] [ - 1 ] , decimals )
if pseudoPath [ 1 ] [ 0 ] == ' Z ' : #some crappy code when the path is closed
pseudoPathEnd = Path ( segs [ 0 : 2 ] ) . to_arrays ( )
x2 = round ( pseudoPathEnd [ 0 ] [ 1 ] [ - 2 ] , decimals )
y2 = round ( pseudoPathEnd [ 0 ] [ 1 ] [ - 1 ] , decimals )
else :
x2 = round ( pseudoPath [ 1 ] [ 1 ] [ - 2 ] , decimals )
y2 = round ( pseudoPath [ 1 ] [ 1 ] [ - 1 ] , decimals )
return x1 , y1 , x2 , y2
def visualize_self_intersections ( self , pathElement , selfIntersectionPoints ) :
''' Draw some circles at given point coordinates (data from array) '''
selfIntersectionGroup = pathElement . getparent ( ) . add ( inkex . Group ( id = " selfIntersectionPoints- {} " . format ( pathElement . attrib [ " id " ] ) ) )
selfIntersectionPointStyle = { ' stroke ' : ' none ' , ' fill ' : self . options . color_self_intersections }
for selfIntersectionPoint in selfIntersectionPoints :
cx = selfIntersectionPoint [ 0 ]
cy = selfIntersectionPoint [ 1 ]
selfIntersectionPointCircle = Circle ( cx = str ( cx ) ,
cy = str ( cy ) ,
r = str ( self . svg . unittouu ( str ( self . options . dotsize_intersections / 2 ) + " px " ) )
)
if pathElement . getparent ( ) != self . svg . root :
selfIntersectionPointCircle . transform = - pathElement . getparent ( ) . composed_transform ( )
selfIntersectionPointCircle . set ( ' id ' , self . svg . get_unique_id ( ' selfIntersectionPoint- ' ) )
selfIntersectionPointCircle . style = selfIntersectionPointStyle
selfIntersectionGroup . add ( selfIntersectionPointCircle )
def visualize_global_intersections ( self , globalIntersectionPoints ) :
''' Draw some circles at given point coordinates (data from array) '''
if len ( globalIntersectionPoints ) > 0 : #only create a group and add stuff if there are some elements to work on
globalIntersectionGroup = self . svg . root . add ( inkex . Group ( id = " globalIntersectionPoints " ) )
globalIntersectionPointStyle = { ' stroke ' : ' none ' , ' fill ' : self . options . color_global_intersections }
for globalIntersectionPoint in globalIntersectionPoints :
cx = globalIntersectionPoint . coords [ 0 ] [ 0 ]
cy = globalIntersectionPoint . coords [ 0 ] [ 1 ]
globalIntersectionPointCircle = Circle ( cx = str ( cx ) ,
cy = str ( cy ) ,
r = str ( self . svg . unittouu ( str ( self . options . dotsize_intersections / 2 ) + " px " ) )
)
globalIntersectionPointCircle . set ( ' id ' , self . svg . get_unique_id ( ' globalIntersectionPoint- ' ) )
globalIntersectionPointCircle . style = globalIntersectionPointStyle
globalIntersectionGroup . add ( globalIntersectionPointCircle )
def buildTrimLineGroups ( self , allSubSplitData , subSplitIndex , globalIntersectionPoints ,
2021-06-05 19:42:33 +02:00
snap_tolerance , apply_style_to_trimmed ) :
2021-06-01 21:37:08 +02:00
''' make a group containing trimmed lines '''
2021-06-04 11:25:43 +02:00
#Check if we should skip or process the path anyway
isClosed = allSubSplitData [ 5 ] [ subSplitIndex ]
if self . options . trimming_path_types == ' open_paths ' and isClosed is True : return #skip this call
elif self . options . trimming_path_types == ' closed_paths ' and isClosed is False : return #skip this call
elif self . options . trimming_path_types == ' both ' : pass
2021-06-01 21:37:08 +02:00
trimLineStyle = { ' stroke ' : str ( self . options . color_trimmed ) , ' fill ' : ' none ' , ' stroke-width ' : self . options . strokewidth }
linesWithSnappedIntersectionPoints = snap ( LineString ( allSubSplitData [ 0 ] [ subSplitIndex ] ) , globalIntersectionPoints , snap_tolerance )
trimGroupId = ' shapely- ' + allSubSplitData [ 1 ] [ subSplitIndex ] . split ( " _ " ) [ 0 ] #spit at "_" (_ from subSplitId)
trimGroupParentId = allSubSplitData [ 1 ] [ subSplitIndex ] . split ( idPrefix + " - " ) [ 1 ] . split ( " _ " ) [ 0 ]
trimGroupParent = self . svg . getElementById ( trimGroupParentId )
trimGroupParentTransform = trimGroupParent . composed_transform ( )
trimGroup = self . findGroup ( trimGroupId )
if trimGroup is None :
trimGroup = trimGroupParent . getparent ( ) . add ( inkex . Group ( id = trimGroupId ) )
#apply isBezier and original path id information to group (required for bezier splitting the original path at the end)
trimGroup . attrib [ ' isBezier ' ] = str ( allSubSplitData [ 3 ] [ subSplitIndex ] )
trimGroup . attrib [ ' originalId ' ] = allSubSplitData [ 4 ] [ subSplitIndex ]
#split all lines against all other lines using the intersection points
trimLines = split ( linesWithSnappedIntersectionPoints , globalIntersectionPoints )
splitAt = [ ] #if the sub split line was split by an intersecting line we receive two trim lines with same assigned original path id!
prevLine = None
for j in range ( len ( trimLines ) ) :
2021-06-04 11:36:07 +02:00
trimLineId = trimGroupId + " - " + str ( subSplitIndex )
2021-06-01 21:37:08 +02:00
splitAt . append ( trimGroupId )
if splitAt . count ( trimGroupId ) > 1 : #we detected a lines with intersection on
trimLineId = trimLineId + self . svg . get_unique_id ( intersectedVerb )
'''
so the previous lines was an intersection lines too . so we change the id to include the intersected verb
( left side and right side of cut ) - note : updating element
id sometimes seems not to work if the id was used before in Inkscape
'''
2021-06-04 11:36:07 +02:00
prevLine . attrib [ ' id ' ] = trimGroupId + " - " + str ( subSplitIndex ) + self . svg . get_unique_id ( intersectedVerb )
2021-06-01 21:37:08 +02:00
prevLine . attrib [ ' intersected ' ] = ' True ' #some dirty flag we need
prevLine = trimLine = inkex . PathElement ( id = trimLineId )
#if so.show_debug is True:
# self.msg(prevLine.attrib['id'])
# self.msg(trimLineId)
x , y = trimLines [ j ] . coords . xy
trimLine . path = [ [ ' M ' , [ x [ 0 ] , y [ 0 ] ] ] , [ ' L ' , [ x [ 1 ] , y [ 1 ] ] ] ]
if trimGroupParentTransform is not None :
trimLine . path = trimLine . path . transform ( - trimGroupParentTransform )
2021-06-05 19:42:33 +02:00
if apply_style_to_trimmed is False :
2021-06-01 21:37:08 +02:00
trimLine . style = trimLineStyle
else :
trimLine . style = allSubSplitData [ 2 ] [ subSplitIndex ]
trimGroup . add ( trimLine )
return trimGroup
def remove_duplicates ( self , allTrimGroups , reverse_removal_order ) :
''' find duplicate lines in a given array [] of groups '''
totalTrimPaths = [ ]
if reverse_removal_order is True :
allTrimGroups = allTrimGroups [ : : - 1 ]
for trimGroup in allTrimGroups :
for element in trimGroup :
if element . path not in totalTrimPaths :
totalTrimPaths . append ( element . path )
else :
2021-06-04 06:13:42 +02:00
if self . options . show_debug is True :
self . msg ( " Deleting {} " . format ( element . get ( ' id ' ) ) )
2021-06-01 21:37:08 +02:00
element . delete ( )
if len ( trimGroup ) == 0 :
2021-06-04 06:13:42 +02:00
if self . options . show_debug is True :
self . msg ( " Deleting {} " . format ( trimGroup . get ( ' id ' ) ) )
2021-06-01 21:37:08 +02:00
trimGroup . delete ( )
2021-06-05 19:42:33 +02:00
def combine_nonintersects ( self , allTrimGroups , apply_style_to_trimmed ) :
2021-06-01 21:37:08 +02:00
'''
2021-06-04 07:06:13 +02:00
combine and chain all non intersected sub split lines which were trimmed at intersection points before .
- At first we sort out all lines by their id :
- if the lines id contains intersectedVerb , we ignore it
- we combine all lines which do not contain intersectedVerb
- Then we loop through that combined structure and chain their segments which touch each other
Changes the style according to user setting .
2021-06-01 21:37:08 +02:00
'''
nonTrimLineStyle = { ' stroke ' : str ( self . options . color_nonintersected ) , ' fill ' : ' none ' , ' stroke-width ' : self . options . strokewidth }
trimNonIntersectedStyle = { ' stroke ' : str ( self . options . color_combined ) , ' fill ' : ' none ' , ' stroke-width ' : self . options . strokewidth }
for trimGroup in allTrimGroups :
totalIntersectionsAtPath = 0
combinedPath = None
combinedPathData = Path ( )
if self . options . show_debug is True :
self . msg ( " trim group {} has {} paths " . format ( trimGroup . get ( ' id ' ) , len ( trimGroup ) ) )
for pElement in trimGroup :
pId = pElement . get ( ' id ' )
#if self.options.show_debug is True:
# self.msg("trim paths id {}".format(pId))
if intersectedVerb not in pId :
if combinedPath is None :
combinedPath = pElement
combinedPathData = pElement . path
else :
combinedPathData + = pElement . path
pElement . delete ( )
else :
totalIntersectionsAtPath + = 1
if len ( combinedPathData ) > 0 :
segData = combinedPathData . to_arrays ( )
newPathData = [ ]
newPathData . append ( segData [ 0 ] )
for z in range ( 1 , len ( segData ) ) : #skip first because we add it statically
if segData [ z ] [ 1 ] != segData [ z - 1 ] [ 1 ] :
newPathData . append ( segData [ z ] )
if self . options . show_debug is True :
self . msg ( " trim group {} has {} combinable segments: " . format ( trimGroup . get ( ' id ' ) , len ( newPathData ) ) )
self . msg ( " {} " . format ( newPathData ) )
combinedPath . path = Path ( newPathData )
2021-06-05 19:42:33 +02:00
if apply_style_to_trimmed is False :
2021-06-01 21:37:08 +02:00
combinedPath . style = trimNonIntersectedStyle
if totalIntersectionsAtPath == 0 :
combinedPath . style = nonTrimLineStyle
else : #the group might consist of intersections only. than we have length of 0
if self . options . show_debug is True :
self . msg ( " trim group {} has no combinable segments (contains only intersected trim lines) " . format ( trimGroup . get ( ' id ' ) ) )
def trim_bezier ( self , allTrimGroups ) :
'''
trim bezier path by checking the lengths and calculating global t parameter from the trimmed sub split lines groups
This function does not work yet .
'''
for trimGroup in allTrimGroups :
if trimGroup . attrib . has_key ( ' isBezier ' ) and trimGroup . attrib [ ' isBezier ' ] == " True " :
globalTParameters = [ ]
if self . options . show_debug is True :
self . msg ( " {} : count of trim lines = {} " . format ( trimGroup . get ( ' id ' ) , len ( trimGroup ) ) )
totalLength = 0
for trimLine in trimGroup :
ignore , lineLength = csplength ( CubicSuperPath ( trimLine . get ( ' d ' ) ) )
totalLength + = lineLength
if self . options . show_debug is True :
self . msg ( " total length = {} " . format ( totalLength ) )
chainLength = 0
for trimLine in trimGroup :
ignore , lineLength = csplength ( CubicSuperPath ( trimLine . get ( ' d ' ) ) )
chainLength + = lineLength
if trimLine . attrib . has_key ( ' intersected ' ) or trimLine == trimGroup [ - 1 ] : #we may not used intersectedVerb because this was used for the affected left as well as the right side of the splitting. This would result in one "intersection" too much.
globalTParameter = chainLength / totalLength
globalTParameters . append ( globalTParameter )
if self . options . show_debug is True :
self . msg ( " chain piece length = {} " . format ( chainLength ) )
self . msg ( " t parameter = {} " . format ( globalTParameter ) )
chainLength = 0
if self . options . show_debug is True :
self . msg ( " Trimming the original bezier path {} at global t parameters: {} " . format ( trimGroup . attrib [ ' originalId ' ] , globalTParameters ) )
for globalTParameter in globalTParameters :
csp = CubicSuperPath ( self . svg . getElementById ( trimGroup . attrib [ ' originalId ' ] ) )
'''
Sadly , those calculated global t parameters are useless for splitting because we cannot split the complete curve at a t parameter
Instead we only can split a bezier by getting to commands which build up a bezier path segment .
- we need to find those parts ( segment pairs ) of the original path first where the sub split line intersection occurs
- then we need to calculate the t parameter
- then we split the bezier part ( consisting of two commands ) and check the new intersection point .
It should match the sub split lines intersection point .
If they do not match we need to adjust the t parameter or loop to previous or next bezier command to find intersection
'''
def add_arguments ( self , pars ) :
pars . add_argument ( " --tab " )
#Settings - General
pars . add_argument ( " --show_debug " , type = inkex . Boolean , default = False , help = " Show debug infos " )
pars . add_argument ( " --break_apart " , type = inkex . Boolean , default = False , help = " Break apart input paths into sub paths " )
pars . add_argument ( " --handle_groups " , type = inkex . Boolean , default = False , help = " Also looks for paths in groups which are in the current selection " )
2021-06-04 11:25:43 +02:00
pars . add_argument ( " --trimming_path_types " , default = " closed_paths " , help = " Process open paths by other open paths, closed paths by other closed paths, or all paths by all other paths " )
2021-06-01 21:37:08 +02:00
pars . add_argument ( " --flattenbezier " , type = inkex . Boolean , default = True , help = " Flatten bezier curves to polylines " )
pars . add_argument ( " --flatness " , type = float , default = 0.1 , help = " Minimum flatness = 0.001. The smaller the value the more fine segments you will get (quantization). Large values might destroy the line continuity. " )
pars . add_argument ( " --decimals " , type = int , default = 3 , help = " Accuracy for sub split lines / lines trimmed by shapely " )
pars . add_argument ( " --snap_tolerance " , type = float , default = 0.1 , help = " Snap tolerance for intersection points " )
2021-06-04 11:25:43 +02:00
#Scanning - Removing
pars . add_argument ( " --remove_relative " , type = inkex . Boolean , default = False , help = " relative cmd " )
pars . add_argument ( " --remove_absolute " , type = inkex . Boolean , default = False , help = " absolute cmd " )
pars . add_argument ( " --remove_mixed " , type = inkex . Boolean , default = False , help = " mixed cmd (relative + absolute) " )
pars . add_argument ( " --remove_polylines " , type = inkex . Boolean , default = False , help = " polyline " )
pars . add_argument ( " --remove_beziers " , type = inkex . Boolean , default = False , help = " bezier " )
pars . add_argument ( " --remove_opened " , type = inkex . Boolean , default = False , help = " opened " )
pars . add_argument ( " --remove_closed " , type = inkex . Boolean , default = False , help = " closed " )
pars . add_argument ( " --remove_self_intersecting " , type = inkex . Boolean , default = False , help = " self-intersecting " )
#Scanning - Highlighting
pars . add_argument ( " --highlight_relative " , type = inkex . Boolean , default = False , help = " relative cmd paths " )
pars . add_argument ( " --highlight_absolute " , type = inkex . Boolean , default = False , help = " absolute cmd paths " )
pars . add_argument ( " --highlight_mixed " , type = inkex . Boolean , default = False , help = " mixed cmd (relative + absolute) paths " )
pars . add_argument ( " --highlight_polylines " , type = inkex . Boolean , default = False , help = " polyline paths " )
pars . add_argument ( " --highlight_beziers " , type = inkex . Boolean , default = False , help = " bezier paths " )
pars . add_argument ( " --highlight_opened " , type = inkex . Boolean , default = False , help = " opened paths " )
pars . add_argument ( " --highlight_closed " , type = inkex . Boolean , default = False , help = " closed paths " )
pars . add_argument ( " --highlight_self_intersecting " , type = inkex . Boolean , default = False , help = " self-intersecting paths " )
2021-06-01 21:37:08 +02:00
pars . add_argument ( " --draw_subsplit " , type = inkex . Boolean , default = False , help = " Draw sub split lines (polylines) " )
2021-06-04 11:25:43 +02:00
pars . add_argument ( " --visualize_self_intersections " , type = inkex . Boolean , default = False , help = " self-intersecting path points " )
pars . add_argument ( " --visualize_global_intersections " , type = inkex . Boolean , default = False , help = " global intersection points " )
2021-06-01 21:37:08 +02:00
2021-06-04 11:25:43 +02:00
#Settings - Trimming
2021-06-01 21:37:08 +02:00
pars . add_argument ( " --draw_trimmed " , type = inkex . Boolean , default = False , help = " Draw trimmed lines " )
pars . add_argument ( " --combine_nonintersects " , type = inkex . Boolean , default = True , help = " Combine non-intersected lines " )
pars . add_argument ( " --remove_duplicates " , type = inkex . Boolean , default = True , help = " Remove duplicate trim lines " )
pars . add_argument ( " --reverse_removal_order " , type = inkex . Boolean , default = False , help = " Reverses the order of removal. Relevant for keeping certain styles of elements " )
pars . add_argument ( " --keep_original_after_trim " , type = inkex . Boolean , default = False , help = " Keep original paths after trimming " )
#Style - General Style
pars . add_argument ( " --strokewidth " , type = float , default = 1.0 , help = " Stroke width (px) " )
pars . add_argument ( " --dotsize_intersections " , type = int , default = 30 , help = " Dot size (px) for self-intersecting and global intersection points " )
pars . add_argument ( " --removefillsetstroke " , type = inkex . Boolean , default = False , help = " Remove fill and define stroke for original paths " )
pars . add_argument ( " --bezier_trimming " , type = inkex . Boolean , default = False , help = " If true we try to use the calculated t parameters from intersection points to receive splitted bezier curves " )
2021-06-05 19:42:33 +02:00
pars . add_argument ( " --apply_style_to_subsplits " , type = inkex . Boolean , default = True , help = " Apply highlighting styles to sub split lines. " )
pars . add_argument ( " --apply_style_to_trimmed " , type = inkex . Boolean , default = True , help = " Apply original path style to trimmed lines " )
2021-06-01 21:37:08 +02:00
#Style - Scanning Colors
2021-06-04 11:25:43 +02:00
pars . add_argument ( " --color_subsplit " , type = Color , default = ' 1630897151 ' , help = " sub split lines " )
pars . add_argument ( " --color_relative " , type = Color , default = ' 3419879935 ' , help = " relative cmd paths " )
pars . add_argument ( " --color_absolute " , type = Color , default = ' 1592519679 ' , help = " absolute cmd paths " )
pars . add_argument ( " --color_mixed " , type = Color , default = ' 3351636735 ' , help = " mixed cmd (relative + absolute) paths " )
pars . add_argument ( " --color_polyline " , type = Color , default = ' 4289703935 ' , help = " polyline paths " )
pars . add_argument ( " --color_bezier " , type = Color , default = ' 258744063 ' , help = " bezier paths " )
pars . add_argument ( " --color_opened " , type = Color , default = ' 4012452351 ' , help = " opened paths " )
pars . add_argument ( " --color_closed " , type = Color , default = ' 2330080511 ' , help = " closed paths " )
pars . add_argument ( " --color_self_intersecting_paths " , type = Color , default = ' 2593756927 ' , help = " self-intersecting paths " )
pars . add_argument ( " --color_self_intersections " , type = Color , default = ' 6320383 ' , help = " self-intersecting path points " )
pars . add_argument ( " --color_global_intersections " , type = Color , default = ' 4239343359 ' , help = " global intersection points " )
2021-06-01 21:37:08 +02:00
#Style - Trimming Color
2021-06-04 11:25:43 +02:00
pars . add_argument ( " --color_trimmed " , type = Color , default = ' 1923076095 ' , help = " trimmed lines " )
pars . add_argument ( " --color_combined " , type = Color , default = ' 3227634687 ' , help = " non-intersected lines " )
pars . add_argument ( " --color_nonintersected " , type = Color , default = ' 3045284607 ' , help = " non-intersected paths " )
2021-06-01 21:37:08 +02:00
def effect ( self ) :
2021-06-05 19:42:33 +02:00
so = self . options
2021-06-01 21:37:08 +02:00
2021-06-04 07:06:13 +02:00
if so . break_apart is True and so . show_debug is True :
2021-06-05 19:42:33 +02:00
self . msg ( " Warning: ' Break apart input ' setting is enabled. Cannot check accordingly for relative, absolute or mixed paths for breaked elements (they are always absolute)! " )
2021-06-01 21:37:08 +02:00
#some constant stuff / styles
2021-06-04 07:06:13 +02:00
relativePathStyle = { ' stroke ' : str ( so . color_relative ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
absolutePathStyle = { ' stroke ' : str ( so . color_absolute ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
mixedPathStyle = { ' stroke ' : str ( so . color_mixed ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
2021-06-04 06:13:42 +02:00
polylinePathStyle = { ' stroke ' : str ( so . color_polyline ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
bezierPathStyle = { ' stroke ' : str ( so . color_bezier ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
openPathStyle = { ' stroke ' : str ( so . color_opened ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
closedPathStyle = { ' stroke ' : str ( so . color_closed ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
selfIntersectingPathStyle = { ' stroke ' : str ( so . color_self_intersecting_paths ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
basicSubSplitLineStyle = { ' stroke ' : str ( so . color_subsplit ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
2021-06-01 21:37:08 +02:00
2021-06-04 07:06:13 +02:00
#get all paths which are within selection or in document and generate sub split lines
2021-06-01 21:37:08 +02:00
pathElements = self . getPathElements ( )
2021-06-05 19:42:33 +02:00
2021-06-01 21:37:08 +02:00
allSubSplitLines = [ ]
allSubSplitIds = [ ]
allSubSplitStyles = [ ]
allSubSplitIsBezier = [ ]
allSubSplitOriginalPathIds = [ ]
2021-06-04 11:25:43 +02:00
allSubSplitIsClosed = [ ]
2021-06-01 21:37:08 +02:00
allSubSplitData = [ ] #an array of sub split lines and it's belonging sub path id
allSubSplitData . append ( allSubSplitLines ) #column 0
allSubSplitData . append ( allSubSplitIds ) #column 1
allSubSplitData . append ( allSubSplitStyles ) #column 2
allSubSplitData . append ( allSubSplitIsBezier ) #column 3
allSubSplitData . append ( allSubSplitOriginalPathIds ) #column 4
2021-06-04 11:25:43 +02:00
allSubSplitData . append ( allSubSplitIsClosed ) #column 5
2021-06-01 21:37:08 +02:00
for pathElement in pathElements :
2021-06-05 19:42:33 +02:00
originalPathId = pathElement . attrib [ " id " ]
2021-06-01 21:37:08 +02:00
path = pathElement . path . transform ( pathElement . composed_transform ( ) )
#path = pathElement.path
2021-06-04 07:06:13 +02:00
2021-06-01 21:37:08 +02:00
'''
2021-06-05 19:42:33 +02:00
check for relative or absolute paths
2021-06-01 21:37:08 +02:00
'''
2021-06-04 07:06:13 +02:00
isRelative = False
isAbsolute = False
isMixed = False
relCmds = [ ' m ' , ' l ' , ' h ' , ' v ' , ' c ' , ' s ' , ' q ' , ' t ' , ' a ' , ' z ' ]
2021-06-05 19:42:33 +02:00
if any ( relCmd in str ( path ) for relCmd in relCmds ) :
2021-06-04 07:06:13 +02:00
isRelative = True
2021-06-05 19:42:33 +02:00
if any ( relCmd . upper ( ) in str ( path ) for relCmd in relCmds ) :
2021-06-04 07:06:13 +02:00
isAbsolute = True
if isRelative is True and isAbsolute is True :
isMixed = True
isRelative = False
isAbsolute = False
if so . remove_absolute is True and isAbsolute is True :
pathElement . delete ( )
continue #skip this loop iteration
if so . remove_relative is True and isRelative is True :
pathElement . delete ( )
continue #skip this loop iteration
if so . remove_mixed is True and isMixed is True :
pathElement . delete ( )
continue #skip this loop iteration
2021-06-05 19:42:33 +02:00
'''
check for bezier or polyline paths
'''
isBezier = False
if ' c ' in str ( path ) or ' C ' in str ( path ) :
isBezier = True
if so . show_debug is True :
self . msg ( " sub path in {} is bezier: {} " . format ( originalPathId , isBezier ) )
if so . remove_beziers is True and isBezier is True :
pathElement . delete ( )
continue #skip this loop iteration
if so . remove_polylines is True and isBezier is False :
pathElement . delete ( )
continue #skip this loop iteration
2021-06-01 21:37:08 +02:00
2021-06-05 19:42:33 +02:00
'''
check for closed or open paths
'''
isClosed = False
raw = path . to_arrays ( )
if raw [ - 1 ] [ 0 ] == ' Z ' or \
( raw [ - 1 ] [ 0 ] == ' L ' and raw [ 0 ] [ 1 ] == raw [ - 1 ] [ 1 ] ) or \
( raw [ - 1 ] [ 0 ] == ' C ' and raw [ 0 ] [ 1 ] == [ raw [ - 1 ] [ 1 ] [ - 2 ] , raw [ - 1 ] [ 1 ] [ - 1 ] ] ) \
: #if first is last point the path is also closed. The "Z" command is not required
isClosed = True
if so . remove_opened is True and isClosed is False :
pathElement . delete ( )
continue #skip this loop iteration
if so . remove_closed is True and isClosed is True :
pathElement . delete ( )
continue #skip this loop iteration
2021-06-01 21:37:08 +02:00
if so . draw_subsplit is True :
subSplitTrimLineGroup = pathElement . getparent ( ) . add ( inkex . Group ( id = " {} - {} " . format ( idPrefix , pathElement . attrib [ " id " ] ) ) )
#get all sub paths for the path of the element
subPaths , prev = [ ] , 0
for i in range ( len ( raw ) ) : # Breaks compound paths into simple paths
if raw [ i ] [ 0 ] == ' M ' and i != 0 :
subPaths . append ( raw [ prev : i ] )
prev = i
subPaths . append ( raw [ prev : ] )
#now loop through all sub paths (and flatten if desired) to build up single lines
2021-06-05 19:42:33 +02:00
for subPath in subPaths :
2021-06-01 21:37:08 +02:00
subPathData = CubicSuperPath ( subPath )
#flatten bezier curves. If it was already a straight line do nothing! Otherwise we would split straight lines into a lot more straight lines
if so . flattenbezier is True and isBezier is True :
bezier . cspsubdiv ( subPathData , so . flatness ) #modifies the path
flattenedpath = [ ]
for seg in subPathData :
first = True
for csp in seg :
cmd = ' L '
if first :
cmd = ' M '
first = False
flattenedpath . append ( [ cmd , [ csp [ 1 ] [ 0 ] , csp [ 1 ] [ 1 ] ] ] )
#self.msg("flattened path = " + str(flattenedpath))
segs = list ( CubicSuperPath ( flattenedpath ) . to_segments ( ) )
else :
segs = list ( subPathData . to_segments ( ) )
#segs = segs[::-1] #reverse the segments
#build polylines from segment data
subSplitLines = [ ]
subSplitIds = [ ]
subSplitStyles = [ ]
subSplitIsBezier = [ ]
subSplitOriginalPathIds = [ ]
2021-06-04 11:25:43 +02:00
subSplitIsClosed = [ ]
2021-06-01 21:37:08 +02:00
for i in range ( len ( segs ) - 1 ) : #we could do the same routine to build up polylines using "for x, y in node.path.end_points". See "number nodes" extension
x1 , y1 , x2 , y2 = self . lineFromSegments ( segs , i , so . decimals )
#self.msg("(y1 = {},y2 = {},x1 = {},x2 = {})".format(x1, y1, x2, y2))
subSplitId = " {} - {} _ {} " . format ( idPrefix , originalPathId , i )
if so . draw_subsplit is True :
line = inkex . PathElement ( id = subSplitId )
#apply line path with composed negative transform from parent element
line . path = [ [ ' M ' , [ x1 , y1 ] ] , [ ' L ' , [ x2 , y2 ] ] ]
if pathElement . getparent ( ) != self . svg . root :
line . path = line . path . transform ( - pathElement . getparent ( ) . composed_transform ( ) )
2021-06-04 06:13:42 +02:00
line . style = basicSubSplitLineStyle
2021-06-04 07:06:13 +02:00
line . attrib [ ' isRelative ' ] = str ( isRelative )
line . attrib [ ' isAbsolute ' ] = str ( isAbsolute )
line . attrib [ ' isMixed ' ] = str ( isMixed )
line . attrib [ ' isBezier ' ] = str ( isBezier )
line . attrib [ ' isClosed ' ] = str ( isClosed )
2021-06-01 21:37:08 +02:00
subSplitTrimLineGroup . add ( line )
subSplitLines . append ( [ ( x1 , y1 ) , ( x2 , y2 ) ] )
subSplitIds . append ( subSplitId )
subSplitStyles . append ( pathElement . style )
subSplitIsBezier . append ( isBezier ) #some dirty flag we need
subSplitOriginalPathIds . append ( originalPathId ) #some dirty flag we need
2021-06-04 11:25:43 +02:00
subSplitIsClosed . append ( isClosed ) #some dirty flag we need
2021-06-01 21:37:08 +02:00
2021-06-05 19:42:33 +02:00
if so . draw_subsplit is True and so . apply_style_to_subsplits is True :
2021-06-01 21:37:08 +02:00
for subSplitLine in subSplitTrimLineGroup :
2021-06-04 07:06:13 +02:00
if subSplitLine . attrib [ ' isRelative ' ] == ' True ' :
if so . highlight_relative is True :
subSplitLine . style = relativePathStyle
if subSplitLine . attrib [ ' isAbsolute ' ] == ' True ' :
if so . highlight_absolute is True :
subSplitLine . style = absolutePathStyle
if subSplitLine . attrib [ ' isMixed ' ] == ' True ' :
if so . highlight_mixed is True :
subSplitLine . style = mixedPathStyle
2021-06-04 06:13:42 +02:00
if subSplitLine . attrib [ ' isBezier ' ] == ' True ' :
if so . highlight_beziers is True :
subSplitLine . style = bezierPathStyle
else :
if so . highlight_polylines is True :
subSplitLine . style = polylinePathStyle
if subSplitLine . attrib [ ' isClosed ' ] == ' True ' :
#if subPathIsClosed is True:
2021-06-01 21:37:08 +02:00
if so . highlight_closed is True :
2021-06-04 06:13:42 +02:00
subSplitLine . style = closedPathStyle
2021-06-01 21:37:08 +02:00
else :
if so . highlight_opened is True :
2021-06-04 07:06:13 +02:00
subSplitLine . style = openPathStyle
2021-06-04 06:13:42 +02:00
2021-06-04 07:06:13 +02:00
#check for self intersections using Bentley-Ottmann algorithm.
2021-06-01 21:37:08 +02:00
selfIntersectionPoints = isect_segments ( subSplitLines , validate = True )
if len ( selfIntersectionPoints ) > 0 :
if so . show_debug is True :
self . msg ( " {} in {} intersects itself with {} intersections! " . format ( subSplitId , originalPathId , len ( selfIntersectionPoints ) ) )
if so . draw_subsplit is True :
if so . highlight_self_intersecting is True :
for subSplitLine in subSplitTrimLineGroup :
2021-06-04 06:13:42 +02:00
subSplitLine . style = selfIntersectingPathStyle #adjusts line color
2021-06-01 21:37:08 +02:00
#delete cosmetic sub split lines if desired
if so . remove_self_intersecting :
subSplitTrimLineGroup . delete ( )
if so . visualize_self_intersections is True : #draw points (circles)
self . visualize_self_intersections ( pathElement , selfIntersectionPoints )
#and also delete non-cosmetics
if so . remove_self_intersecting :
#if we also want to avoid processing them for trimming
subSplitLines = None
subSplitIds = None
subSplitStyles = None
subSplitIsBezier = None
subSplitOriginalPathIds = None
pathElement . delete ( ) #and finally delete the orginal path
#extend the complete collection
if subSplitLines != None and \
subSplitIds != None and \
subSplitStyles != None and \
subSplitIsBezier != None and \
2021-06-04 11:25:43 +02:00
allSubSplitOriginalPathIds != None and \
allSubSplitIsClosed != None :
2021-06-01 21:37:08 +02:00
allSubSplitStyles . extend ( subSplitStyles )
allSubSplitLines . extend ( subSplitLines )
allSubSplitIds . extend ( subSplitIds )
allSubSplitIsBezier . extend ( subSplitIsBezier )
allSubSplitOriginalPathIds . extend ( subSplitOriginalPathIds )
2021-06-04 11:25:43 +02:00
allSubSplitIsClosed . extend ( subSplitIsClosed )
2021-06-01 21:37:08 +02:00
2021-06-05 19:42:33 +02:00
#adjust the style of original paths if desired. Has influence to the finally trimmed lines style results too!
if so . removefillsetstroke is True :
self . adjustStyle ( pathElement )
#apply styles to original paths
if isRelative is True :
if so . highlight_relative is True :
pathElement . style = relativePathStyle
if isAbsolute is True :
if so . highlight_absolute is True :
pathElement . style = absolutePathStyle
if isMixed is True :
if so . highlight_mixed is True :
pathElement . style = mixedPathStyle
if isBezier is True :
if so . highlight_beziers is True :
pathElement . style = bezierPathStyle
else :
if so . highlight_polylines is True :
pathElement . style = polylinePathStyle
if isClosed is True :
if so . highlight_closed is True :
pathElement . style = closedPathStyle
else :
if so . highlight_opened is True :
pathElement . style = openPathStyle
2021-06-01 21:37:08 +02:00
if so . draw_subsplit is True :
if subSplitTrimLineGroup is not None : #might get deleted before so we need to check this first
subSplitTrimLineGroup = reversed ( subSplitTrimLineGroup ) #reverse the order to match the original path segment placing
if so . show_debug is True :
self . msg ( " sub split line count: {} " . format ( len ( allSubSplitLines ) ) )
2021-06-04 07:06:13 +02:00
'''
now we intersect the sub split lines to find the global intersection points using Bentley - Ottmann algorithm ( contains self - intersections too ! )
2021-06-01 21:37:08 +02:00
'''
2021-06-05 19:42:33 +02:00
if so . draw_trimmed is True :
try :
globalIntersectionPoints = MultiPoint ( isect_segments ( allSubSplitData [ 0 ] , validate = True ) )
if so . show_debug is True :
self . msg ( " global intersection points count: {} " . format ( len ( globalIntersectionPoints ) ) )
if len ( globalIntersectionPoints ) > 0 :
if so . visualize_global_intersections is True :
self . visualize_global_intersections ( globalIntersectionPoints )
'''
now we trim the sub split lines at all calculated intersection points .
We do this path by path to keep the logic between original paths , sub split lines and the final output
'''
2021-06-04 11:36:07 +02:00
allTrimGroups = [ ] #container to collect all trim groups for later on processing
2021-06-01 21:37:08 +02:00
for subSplitIndex in range ( len ( allSubSplitData [ 0 ] ) ) :
trimGroup = self . buildTrimLineGroups ( allSubSplitData , subSplitIndex ,
2021-06-05 19:42:33 +02:00
globalIntersectionPoints , so . snap_tolerance , so . apply_style_to_trimmed )
2021-06-04 11:25:43 +02:00
if trimGroup is not None :
if trimGroup not in allTrimGroups :
allTrimGroups . append ( trimGroup )
2021-06-01 21:37:08 +02:00
if so . show_debug is True : self . msg ( " trim groups count: {} " . format ( len ( allTrimGroups ) ) )
if len ( allTrimGroups ) == 0 :
self . msg ( " You selected to draw trimmed lines but no intersections could be calculated. " )
#trim beziers - not working yet
if so . bezier_trimming is True : self . trim_bezier ( allTrimGroups )
#check for duplicate trim lines and delete them if desired
if so . remove_duplicates is True : self . remove_duplicates ( allTrimGroups , so . reverse_removal_order )
#glue together all non-intersected sub split lines to larger path structures again (cleaning up).
2021-06-05 19:42:33 +02:00
if so . combine_nonintersects is True : self . combine_nonintersects ( allTrimGroups , so . apply_style_to_trimmed )
2021-06-01 21:37:08 +02:00
#clean original paths if selected. This option is explicitely independent from remove_open, remove_closed
if so . keep_original_after_trim is False :
for pathElement in pathElements :
pathElement . delete ( )
2021-06-05 19:42:33 +02:00
except AssertionError as e :
self . msg ( " Error calculating global intersections. \n \
2021-06-01 21:37:08 +02:00
See https : / / github . com / ideasman42 / isect_segments - bentley_ottmann . \n \n \
You can try to fix this by : \n \
- reduce or raise the ' decimals ' setting ( default is 3 but try to set to 6 for example ) \n \
- reduce or raise the ' flatness ' setting ( if quantization option is used at all ; default is 0.100 ) . " )
return
if __name__ == ' __main__ ' :
2021-06-05 19:42:33 +02:00
ContourScannerAndTrimmer ( ) . run ( )