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 :
- find line parts which are included in other lines and perform intersections / splittings ( overlapping colinear lines )
- 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
- . . .
- 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 )
- 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 " )
Last patch : 01.06 .2021
License : GNU GPL v3
'''
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 ) :
''' this does the same as " CTRL + SHIFT + K " '''
if breakelements == None :
breakelements = [ ]
if element . tag == inkex . addNS ( ' path ' , ' svg ' ) :
parent = element . getparent ( )
idx = parent . index ( element )
idSuffix = 0
raw = element . path . to_arrays ( )
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 : ] )
for subpath in subPaths :
replacedelement = copy . copy ( element )
oldId = replacedelement . get ( ' id ' )
2021-06-02 19:40:39 +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 . set ( ' d ' , csp )
replacedelement . set ( ' id ' , oldId + str ( idSuffix ) )
parent . insert ( idx , replacedelement )
idSuffix + = 1
breakelements . append ( replacedelement )
2021-06-01 21:37:08 +02:00
parent . remove ( element )
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
return pathElements
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 ) ) )
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
#function to refine the style of the lines
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 ,
trimLineIndex , snap_tolerance , apply_original_style ) :
''' make a group containing trimmed lines '''
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 ) ) :
trimLineId = trimGroupId + " - " + str ( trimLineIndex )
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
'''
prevLine . attrib [ ' id ' ] = trimGroupId + " - " + str ( trimLineIndex ) + self . svg . get_unique_id ( intersectedVerb )
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 )
if apply_original_style is False :
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 :
element . delete ( )
if len ( trimGroup ) == 0 :
trimGroup . delete ( )
def combine_nonintersects ( self , allTrimGroups , apply_original_style ) :
'''
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 .
'''
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 )
if apply_original_style is False :
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 ( " --path_types " , default = " closed_paths " , help = " Apply for closed paths, open paths or both " )
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 " )
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 " )
#Settings - Scanning
pars . add_argument ( " --remove_opened " , type = inkex . Boolean , default = False , help = " Remove original opened paths " )
pars . add_argument ( " --remove_closed " , type = inkex . Boolean , default = False , help = " Remove original closed paths " )
pars . add_argument ( " --remove_self_intersecting " , type = inkex . Boolean , default = False , help = " Remove original self-intersecting paths " )
pars . add_argument ( " --highlight_opened " , type = inkex . Boolean , default = False , help = " Highlight opened contours " )
pars . add_argument ( " --highlight_closed " , type = inkex . Boolean , default = False , help = " Highlight closed contours " )
pars . add_argument ( " --highlight_self_intersecting " , type = inkex . Boolean , default = False , help = " Highlight self-intersecting contours " )
pars . add_argument ( " --draw_subsplit " , type = inkex . Boolean , default = False , help = " Draw sub split lines (polylines) " )
pars . add_argument ( " --visualize_self_intersections " , type = inkex . Boolean , default = False , help = " Visualize self-intersecting path points " )
pars . add_argument ( " --visualize_global_intersections " , type = inkex . Boolean , default = False , help = " Visualize global intersection points " )
#Settings - Trimming
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 " )
pars . add_argument ( " --apply_original_style " , type = inkex . Boolean , default = True , help = " Apply original path style to trimmed lines " )
#Style - Scanning Colors
pars . add_argument ( " --color_opened " , type = Color , default = ' 4012452351 ' , help = " Color for opened contours " )
pars . add_argument ( " --color_closed " , type = Color , default = ' 2330080511 ' , help = " Color for closed contours " )
pars . add_argument ( " --color_self_intersecting_paths " , type = Color , default = ' 2593756927 ' , help = " Color for self-intersecting contours " )
pars . add_argument ( " --color_subsplit " , type = Color , default = ' 1630897151 ' , help = " Color for sub split lines " )
pars . add_argument ( " --color_self_intersections " , type = Color , default = ' 6320383 ' , help = " Color for self-intersecting line points " )
pars . add_argument ( " --color_global_intersections " , type = Color , default = ' 4239343359 ' , help = " Color for global intersection points " )
#Style - Trimming Color
pars . add_argument ( " --color_trimmed " , type = Color , default = ' 1923076095 ' , help = " Color for trimmed lines " )
pars . add_argument ( " --color_combined " , type = Color , default = ' 3227634687 ' , help = " Color for non-intersected lines " )
pars . add_argument ( " --color_nonintersected " , type = Color , default = ' 3045284607 ' , help = " Color for non-intersected paths " )
def effect ( self ) :
so = self . options
#warn if there is nothing to visualize
if \
so . keep_original_after_trim is False and \
so . remove_opened is True and \
so . remove_closed is True and \
so . visualize_self_intersections is False and \
so . visualize_global_intersections is False and \
so . draw_subsplit is False and \
so . draw_trimmed is False :
self . msg ( " Nothing to draw. Select at least one visualization option. " )
return
#some dependent configuration for drawing modes
if \
so . highlight_opened is True or \
so . highlight_closed is True or \
so . highlight_self_intersecting is True :
so . draw_subsplit = True
if so . draw_subsplit is False :
so . highlight_open = False
so . highlight_closed = False
so . highlight_self_intersecting = False
#some constant stuff / styles
keepOpenPathStyle = { ' stroke ' : str ( so . color_opened ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
keepClosedPathStyle = { ' stroke ' : str ( so . color_closed ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
keepSelfIntersectingPathStyle = { ' stroke ' : str ( so . color_self_intersecting_paths ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
subSplitLineStyle = { ' stroke ' : str ( so . color_subsplit ) , ' fill ' : ' none ' , ' stroke-width ' : so . strokewidth }
''' 1 //
get all paths which are within selection or in document and generate sub split lines
If flatten is enabled , we do the best approximation into a set of fine line segments .
To quickly find all intersections we use Bentley - Ottmann algorithm .
To use it we have to split all paths into subpaths and each sub path ' s will puzzled into single straight lines
Cool tool to visualize : https : / / bl . ocks . org / 1 wheel / 464141 fe9b940153e636
'''
pathElements = self . getPathElements ( )
allSubSplitLines = [ ]
allSubSplitIds = [ ]
allSubSplitStyles = [ ]
allSubSplitIsBezier = [ ]
allSubSplitOriginalPathIds = [ ]
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
for pathElement in pathElements :
path = pathElement . path . transform ( pathElement . composed_transform ( ) )
#path = pathElement.path
'''
Some original path checkings for analysis / highlighting purposes
Note : highlighting open / closed / self - intersecting contours does work best if you break apart
combined paths before .
'''
pathIsClosed = False
path_arrays = path . to_arrays ( )
if path_arrays [ - 1 ] [ 0 ] == ' Z ' or \
( path_arrays [ - 1 ] [ 0 ] == ' L ' and path_arrays [ 0 ] [ 1 ] == path_arrays [ - 1 ] [ 1 ] ) or \
( path_arrays [ - 1 ] [ 0 ] == ' C ' and path_arrays [ 0 ] [ 1 ] == [ path_arrays [ - 1 ] [ 1 ] [ - 2 ] , path_arrays [ - 1 ] [ 1 ] [ - 1 ] ] ) \
: #if first is last point the path is also closed. The "Z" command is not required
pathIsClosed = True
#Check if we should delete the path or not
if so . remove_opened is True and pathIsClosed is False :
pathElement . delete ( )
continue #skip this loop iteration
if so . remove_closed is True and pathIsClosed is True :
pathElement . delete ( )
continue #skip this loop iteration
#Check if we should skip or process the path anyway
if so . path_types == ' open_paths ' and pathIsClosed is True : continue #skip this loop iteration
elif so . path_types == ' closed_paths ' and pathIsClosed is False : continue #skip this loop iteration
elif so . path_types == ' both ' : pass
#adjust the style of original paths if desired. Has influence to the finally trimmed lines style results too!
if so . removefillsetstroke :
self . adjustStyle ( pathElement )
originalPathId = pathElement . attrib [ " id " ]
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
raw = path . to_arrays ( )
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
for subPath in subPaths :
#set to True if the sub path is a bezier, else we assume it only has straight lines inside
isBezier = False
if ' C ' in str ( subPath ) :
isBezier = True
if so . show_debug is True :
self . msg ( " sub path in {} is bezier: {} " . format ( originalPathId , isBezier ) )
#self.msg("sub path in {} = {}".format(element.get('id'), subPath))
#flatten the subpath if wanted
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 = [ ]
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 ( ) )
line . style = subSplitLineStyle
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
if so . draw_subsplit is True :
#check for open/closed again (at first we checked for the <maybe> combined path. Now we can do for each sub path too!
subPathIsClosed = False
if subSplitLines [ 0 ] [ 0 ] == subSplitLines [ - 1 ] [ 1 ] :
subPathIsClosed = True
for subSplitLine in subSplitTrimLineGroup :
if subPathIsClosed is True :
if so . highlight_closed is True :
subSplitLine . style = keepClosedPathStyle
else :
if so . highlight_opened is True :
subSplitLine . style = keepOpenPathStyle
#check for self intersections
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 :
subSplitLine . style = keepSelfIntersectingPathStyle #adjusts line color
#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 \
allSubSplitOriginalPathIds != None :
allSubSplitStyles . extend ( subSplitStyles )
allSubSplitLines . extend ( subSplitLines )
allSubSplitIds . extend ( subSplitIds )
allSubSplitIsBezier . extend ( subSplitIsBezier )
allSubSplitOriginalPathIds . extend ( subSplitOriginalPathIds )
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 ) ) )
''' 2 //
now we intersect the sub split lines to find the global intersection points ( contains self - intersections too ! )
'''
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 )
''' 3 //
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
'''
if so . draw_trimmed is True :
allTrimGroups = [ ] #container to collect all trim groups for later on processing
trimLineIndex = 1
for subSplitIndex in range ( len ( allSubSplitData [ 0 ] ) ) :
trimGroup = self . buildTrimLineGroups ( allSubSplitData , subSplitIndex ,
globalIntersectionPoints , trimLineIndex , so . snap_tolerance , so . apply_original_style )
if trimGroup not in allTrimGroups :
allTrimGroups . append ( trimGroup )
trimLineIndex + = 1
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).
if so . combine_nonintersects is True : self . combine_nonintersects ( allTrimGroups , so . apply_original_style )
#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 ( )
except AssertionError as e :
self . msg ( " Error calculating global intersections. \n \
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__ ' :
ContourScannerAndTrimmer ( ) . run ( )