This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.
mightyscape-1.1-deprecated/extensions/fablabchemnitz/contour_scanner_and_trimmer/contour_scanner_and_trimmer.py

1293 lines
73 KiB
Python

#!/usr/bin/env python3
'''
Extension for InkScape 1.0+
- WARNING: HORRIBLY SLOW CODE. PLEASE HELP TO MAKE IT USEFUL FOR LARGE AMOUNT OF PATHS
- ToDo:
- add more comments
- add more debug output
- add documentation about used algorithms at online page
- add statistics about type counts and path lengths (before/after sub splitting/trimming)
- add options:
- replace trimmed paths by bezier paths (calculating lengths and required t parameter)
- filter/remove overlapping/duplicates in
- in original selection (not working bezier but for straight line segments!) We can use another extension for it
- split bezier
- ...
- 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)
- note: running this extension might leave some empty parent groups in some circumstances. run the clean groups extension separately to fix that
- sort to groups by path type (open, closed, ...)
- 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.)
- Notes about shapely:
- 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)
- 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.
- Cool tool to visualize sweep line algorithm Bentley-Ottmann: https://bl.ocks.org/1wheel/464141fe9b940153e636
- 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: 08.07.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()
idPrefixSubSplit = "subsplit"
idPrefixTrimming = "trimmed"
intersectedVerb = "intersected"
collinearVerb = "collinear"
class ContourScannerAndTrimmer(inkex.EffectExtension):
def break_contours(self, element, breakelements = None):
'''
this does the same as "CTRL + SHIFT + K"
This functions honors the fact of absolute or relative paths!
'''
if breakelements == None:
breakelements = []
if element.tag == inkex.addNS('path','svg'):
parent = element.getparent()
idx = parent.index(element)
idSuffix = 0
#raw = str(element.path).split()
raw = element.path.to_arrays()
subPaths = []
prev = 0
for i in range(len(raw)): # Breaks compound paths into simple paths
#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))
prev = i
subPaths.append(Path(raw[prev:])) #finally add the last path
for subPath in subPaths:
replacedelement = copy.copy(element)
oldId = replacedelement.get('id')
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
if len(subPaths) == 1:
replacedelement.set('id', oldId)
else:
replacedelement.set('id', oldId + str(idSuffix))
idSuffix += 1
parent.insert(idx, replacedelement)
breakelements.append(replacedelement)
element.delete()
for child in element.getchildren():
self.break_contours(child, breakelements)
return breakelements
def get_child_paths(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.get_child_paths(child, elements)
return elements
def get_path_elements(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.get_child_paths(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')
exit(1)
if self.options.break_apart is True:
breakApartElements = None
for pathElement in pathElements:
breakApartElements = self.break_contours(pathElement, breakApartElements)
pathElements = breakApartElements
if self.options.show_debug is True:
self.msg("total processing paths count: {}".format(len(pathElements)))
return pathElements
def find_group(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 adjust_style(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 line_from_segments(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)
return selfIntersectionGroup
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 build_trim_line_group(self, subSplitLineArray, subSplitIndex, globalIntersectionPoints):
''' make a group containing trimmed lines'''
#Check if we should skip or process the path anyway
isClosed = subSplitLineArray[subSplitIndex].attrib['originalPathIsClosed']
if self.options.trimming_path_types == 'open_paths' and isClosed == 'True': return #skip this call
elif self.options.trimming_path_types == 'closed_paths' and isClosed == 'False': return #skip this call
elif self.options.trimming_path_types == 'both': pass
csp = Path(subSplitLineArray[subSplitIndex].path.transform(subSplitLineArray[subSplitIndex].composed_transform())).to_arrays() #will be buggy if draw subsplit lines is deactivated
ls = LineString([(csp[0][1][0], csp[0][1][1]), (csp[1][1][0], csp[1][1][1])])
trimLineStyle = {'stroke': str(self.options.color_trimmed), 'fill': 'none', 'stroke-width': self.options.strokewidth}
linesWithSnappedIntersectionPoints = snap(ls, globalIntersectionPoints, self.options.snap_tolerance)
trimGroupParentId = subSplitLineArray[subSplitIndex].attrib['originalPathId']
trimGroupId = '{}-{}-{}'.format(idPrefixTrimming, idPrefixSubSplit, trimGroupParentId)
trimGroupParent = self.svg.getElementById(trimGroupParentId)
trimGroup = self.find_group(trimGroupId)
if trimGroup is None:
trimGroup = trimGroupParent.getparent().add(inkex.Group(id=trimGroupId))
trimGroup.transform = -subSplitLineArray[subSplitIndex].composed_transform()
#apply isBezier and original path id information to group (required for bezier splitting the original path at the end)
trimGroup.attrib['originalPathIsBezier'] = subSplitLineArray[subSplitIndex].attrib['originalPathIsBezier']
trimGroup.attrib['originalPathIsPolyBezMixed'] = subSplitLineArray[subSplitIndex].attrib['originalPathIsPolyBezMixed']
trimGroup.attrib['originalPathId'] = subSplitLineArray[subSplitIndex].attrib['originalPathId']
#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 = "{}-{}".format(trimGroupId, subSplitIndex)
splitAt.append(trimGroupId)
if splitAt.count(trimGroupId) > 1: #we detected a lines with intersection on
trimLineId = "{}-{}".format(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'] = "{}-{}".format(trimGroupId, str(subSplitIndex) + "-" + self.svg.get_unique_id(intersectedVerb + "-"))
prevLine.attrib['intersected'] = 'True' #some dirty flag we need
prevLine = trimLine = inkex.PathElement(id=trimLineId)
x, y = trimLines[j].coords.xy
x0 = round(x[0], self.options.decimals)
x1 = round(x[1], self.options.decimals)
y0 = round(y[0], self.options.decimals)
y1 = round(y[1], self.options.decimals)
if x0 == x1 and y0 == y1: #check if the trimLine is a pointy one (rounded start point equals rounded end point)
if self.options.show_debug is True:
self.msg("pointy trim line (start point equals end point). Skipping ...")
continue
trimLine.attrib['d'] = 'M {},{} L {},{}'.format(x0, y0, x1, y1) #we set the path of trimLine using 'd' attribute because if we use trimLine.path the decimals get cut off unwantedly
#trimLine.path = Path([['M', [x0,y0]], ['L', [x1,y1]]])
#if trimGroupParentTransform is not None:
# trimLine.path = trimLine.path.transform(-trimGroupParentTransform)
if self.options.trimmed_style == "apply_from_trimmed":
trimLine.style = trimLineStyle
elif self.options.trimmed_style == "apply_from_original":
trimLine.style = subSplitLineArray[subSplitIndex].attrib['originalPathStyle']
trimGroup.add(trimLine)
return trimGroup
def slope(self, p0, p1):
'''
Calculate the slope (gradient) of a line's start point p0 + end point p1
'''
dx = p1[0] - p0[0]
if dx == 0:
return sys.float_info.max #vertical
return (p1[1] - p0[1]) / dx
def process_set_x(self, working_set):
if len(working_set) < 2:
return (True, working_set)
# sort working set left to right
working_set.sort(key=lambda x: x['p0'][0])
for i in range(0, len(working_set)):
for j in range(i + 1, len(working_set)):
# calculate slope from S0P0 to S1P1 and S0P1 to S1P0
# if slopes all match the working set's slope, we're collinear
# if not, these segments are parallel but not collinear and should be left alone
expected_slope = working_set[i]['slope']
if (abs(self.slope(working_set[i]['p1'], working_set[j]['p0']) - expected_slope) > self.options.collinear_filter_epsilon) \
or (abs(self.slope(working_set[i]['p0'], working_set[j]['p1']) - expected_slope) > self.options.collinear_filter_epsilon):
continue
# the only remaining permissible configuration: collinear segments with a gap between them
# e.g. --- -----
# otherwise we combine segments and flag the set as requiring more processing
s0x0 = working_set[i]['p0'][0]
s0x1 = working_set[i]['p1'][0]
s1x0 = working_set[j]['p0'][0]
s1x1 = working_set[j]['p1'][0]
if not (s0x0 < s0x1 and s0x1 < s1x0 and s1x0 < s1x1):
# make a duplicate set, omitting segments i and j
new_set = [x for (k, x) in enumerate(working_set) if k not in (i, j)]
# add a segment representing i and j's furthest points
pts = [ working_set[i]['p0'], working_set[i]['p1'], working_set[j]['p0'], working_set[j]['p1'] ]
pts.sort(key=lambda x: x[0])
new_set.append({
'p0': pts[0],
'p1': pts[-1],
'slope': self.slope(pts[0], pts[-1]),
'id': working_set[i]['id'],
'originalPathId': working_set[i]['originalPathId'],
'composed_transform': working_set[i]['composed_transform']
})
return (False, new_set)
return (True, working_set)
def process_set_y(self, working_set):
if len(working_set) < 2:
return (True, working_set)
# sort working set top to bottom
working_set.sort(key=lambda y: -y['p0'][1])
for i in range(0, len(working_set)):
for j in range(i + 1, len(working_set)):
# the only remaining permissible configuration: collinear segments with a gap between them
# e.g.
# |
# |
#
# |
# |
# |
#
# otherwise we combine segments and flag the set as requiring more processing
s0y0 = working_set[i]['p0'][1]
s0y1 = working_set[i]['p1'][1]
s1y0 = working_set[j]['p0'][1]
s1y1 = working_set[j]['p1'][1]
if not (s0y0 < s0y1 and s0y1 < s1y0 and s1y0 < s1y1):
# make a duplicate set, omitting segments i and j
new_set = [y for (k, y) in enumerate(working_set) if k not in (i, j)]
# add a segment representing i and j's furthest points
pts = [ working_set[i]['p0'], working_set[i]['p1'], working_set[j]['p0'], working_set[j]['p1'] ]
pts.sort(key=lambda y: y[1])
new_set.append({
'p0': pts[0],
'p1': pts[-1],
'slope': self.slope(pts[0], pts[-1]),
'id': working_set[i]['id'],
'originalPathId': working_set[i]['originalPathId'],
'composed_transform': working_set[i]['composed_transform']
})
return (False, new_set)
return (True, working_set)
def filter_collinear(self, lineArray):
''' Another sweep line algorithm to scan collinear lines
Loop through a set of lines and find + fiter all overlapping segments / duplicate segments
finally returns a set of merged-like lines and a set of original items which should be dropped.
Based on the style of the algorithm we have no good influence on the z-index of the items because
it is scanned by slope and point coordinates. That's why we have a more special
'remove_trim_duplicates()' function for trimmed duplicates!
'''
'''
filter for regular input lines and special vertical lines
'''
input_set = []
input_ids = []
# collect segments, calculate their slopes, order their points left-to-right
for line in lineArray:
#csp = line.path.to_arrays()
parent = line.getparent()
if parent is not None:
csp = Path(line.path.transform(parent.composed_transform())).to_arrays()
else:
csp = line.path.to_arrays()
#self.msg("csp = {}".format(csp))
x1, y1, x2, y2 = csp[0][1][0], csp[0][1][1], csp[1][1][0], csp[1][1][1]
# ensure p0 is left of p1
if x1 < x2:
s = {
'p0': [x1, y1],
'p1': [x2, y2]
}
else:
s = {
'p0': [x2, y2],
'p1': [x1, y1]
}
s['slope'] = self.slope(s['p0'], s['p1'])
s['id'] = line.attrib['id']
s['originalPathId'] = line.attrib['originalPathId']
s['composed_transform'] = line.composed_transform()
#s['d'] = line.attrib['d']
input_set.append(s)
input_set.sort(key=lambda x: x['slope'])
#input_set.append(False) # used to clear out lingering contents of working_set_x on last iteration
input_set_new = []
#loop through input_set to filter out the vertical lines because we need to handle them separately
vertical_set = []
vertical_ids = []
for i in range(0, len(input_set)):
if input_set[i]['slope'] == sys.float_info.max:
vertical_set.append(input_set[i])
else:
input_set_new.append(input_set[i])
input_set = input_set_new #overwrite the input_set with the filtered one
input_set.append(False) # used to clear out lingering contents of working_set_x on last iteration
'''
process x lines (all lines except vertical ones)
'''
working_set_x = []
working_x_ids = []
output_set_x = []
output_x_ids = []
if len(input_set) > 0:
current_slope = input_set[0]['slope']
for input in input_set:
# bin sets of input_set by slope (within a tolerance)
dm = input and abs(input['slope'] - current_slope) or 0
if input and dm < self.options.collinear_filter_epsilon:
working_set_x.append(input) #we put all lines to working set which have similar slopes
if input['id'] != '': input_ids.append(input['id'])
else: # slope discontinuity, process accumulated set
while True:
(done, working_set_x) = self.process_set_x(working_set_x)
if done:
output_set_x.extend(working_set_x)
break
if input: # begin new working set
working_set_x = [input]
current_slope = input['slope']
if input['id'] != '': input_ids.append(input['id'])
for output_x in output_set_x:
output_x_ids.append(output_x['id'])
for working_x in working_set_x:
working_x_ids.append(working_x['id'])
else:
if self.options.show_debug is True:
self.msg("Scanning: no non-vertical input lines found. That might be okay or not ...")
'''
process vertical lines
'''
working_set_y = []
working_y_ids = []
output_set_y = []
output_y_ids = []
if len(vertical_set) > 0:
vertical_set.sort(key=lambda x: x['p0'][0]) #sort verticals by their x coordinate
vertical_set.append(False) # used to clear out lingering contents of working_set_y on last iteration
current_x = vertical_set[0]['p0'][0]
for vertical in vertical_set:
if vertical and current_x == vertical['p0'][0]:
working_set_y.append(vertical) #we put all lines to working set which have same x coordinate
if vertical['id'] != '': vertical_ids.append(vertical['id'])
else: # x coord discontinuity, process accumulated set
while True:
(done, working_set_y) = self.process_set_y(working_set_y)
if done:
output_set_y.extend(working_set_y)
break
if vertical: # begin new working set
working_set_y = [vertical]
current_x = vertical['p0'][0]
if vertical['id'] != '': vertical_ids.append(vertical['id'])
else:
if self.options.show_debug is True:
self.msg("Scanning: no vertical lines found. That might be okay or not ...")
for output_y in output_set_y:
output_y_ids.append(output_y['id'])
for working_y in working_set_y:
working_y_ids.append(working_y['id'])
output_set = output_set_x
output_set.extend(output_set_y)
output_ids = []
for output in output_set:
#self.msg(output)
output_ids.append(output['id'])
#we finally build a list which contains all overlapping elements we want to drop
dropped_ids = []
for input_id in input_ids: #if the input_id id is not in the output ids we are going to drop it
if input_id not in output_ids:
dropped_ids.append(input_id)
for vertical_id in vertical_ids: #if the input_id id is not in the output ids we are going to drop it
if vertical_id not in output_ids:
dropped_ids.append(vertical_id)
if self.options.show_debug is True:
#self.msg("input_set:{}".format(input_set))
self.msg("input_ids [{}]:".format(len(input_ids)))
for input_id in input_ids:
self.msg(input_id)
self.msg("*"*24)
#self.msg("working_set_x:{}".format(working_set_x))
self.msg("working_x_ids [{}]:".format(len(working_x_ids)))
for working_x_id in working_x_ids:
self.msg(working_x_id)
self.msg("*"*24)
#self.msg("output_set_x:{}".format(output_set_x))
self.msg("output_x_ids [{}]:".format(len(output_x_ids)))
for output_x_id in output_x_ids:
self.msg(output_x_id)
self.msg("*"*24)
#self.msg("output_set_y:{}".format(output_set_y))
self.msg("output_y_ids [{}]:".format(len(output_y_ids)))
for output_y_id in output_y_ids:
self.msg(output_y_id)
self.msg("*"*24)
#self.msg("output_set:{}".format(output_set))
self.msg("output_ids [{}]:".format(len(output_ids)))
for output_id in output_ids:
self.msg(output_id)
self.msg("*"*24)
self.msg("dropped_ids [{}]:".format(len(dropped_ids)))
for dropped_id in dropped_ids:
self.msg(dropped_id)
self.msg("*"*24)
return output_set, dropped_ids
def remove_trim_duplicates(self, allTrimGroups):
'''
find duplicate lines in a given array [] of groups
note: this function is similar to filter_collinear but we keep it because we have a 'reverse_trim_removal_order' option.
We can use this option in some special situations where we work without the function 'filter_collinear()'.
'''
totalTrimPaths = []
if self.options.reverse_trim_removal_order is True:
allTrimGroups = allTrimGroups[::-1]
for trimGroup in allTrimGroups:
for element in trimGroup:
path = element.path.transform(element.composed_transform())
if path not in totalTrimPaths:
totalTrimPaths.append(path)
else:
if self.options.show_debug is True:
self.msg("Deleting path {}".format(element.get('id')))
element.delete()
if len(trimGroup) == 0:
if self.options.show_debug is True:
self.msg("Deleting group {}".format(trimGroup.get('id')))
trimGroup.delete()
def combine_nonintersects(self, allTrimGroups):
'''
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 self.options.trimmed_style == "apply_from_trimmed":
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('originalPathIsBezier') and trimGroup.attrib['originalPathIsBezier'] == "True") or\
(trimGroup.attrib.has_key('originalPathIsPolyBezMixed') and trimGroup.attrib['originalPathIsPolyBezMixed'] == "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['originalPathId'], globalTParameters))
for globalTParameter in globalTParameters:
csp = CubicSuperPath(self.svg.getElementById(trimGroup.attrib['originalPathId']))
'''
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("--nb_main")
pars.add_argument("--nb_settings_and_actions")
#Settings - General Input/Output
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")
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")
pars.add_argument("--flattenbezier", type=inkex.Boolean, default=True, help="Flatten bezier curves to (poly)lines")
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")
pars.add_argument("--collinear_filter_epsilon", type=float, default=0.01, help="Epsilon for collinear line filter")
#Settings - 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("--subsplit_style", default="default", help="Sub split line style")
pars.add_argument("--trimmed_style", default="apply_from_trimmed", help="Trimmed line style")
#Removing - Applying to original paths and sub split lines
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_rel_abs_mixed", type=inkex.Boolean, default=False, help="mixed rel/abs cmd (relative + absolute)")
pars.add_argument("--remove_polylines", type=inkex.Boolean, default=False, help="(poly)line")
pars.add_argument("--remove_beziers", type=inkex.Boolean, default=False, help="bezier")
pars.add_argument("--remove_poly_bez_mixed", type=inkex.Boolean, default=False, help="mixed (poly)line/bezier paths")
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")
#Removing - Applying to sub split lines only
pars.add_argument("--filter_subsplit_collinear", type=inkex.Boolean, default=True, help="Removes any duplicates by merging (multiple) overlapping line segments into longer lines. Not possible to apply for original paths because this routine does not support bezier type paths.")
pars.add_argument("--filter_subsplit_collinear_action", default="remove", help="What to do with collinear overlapping lines?")
#Removing - Applying to original paths only
pars.add_argument("--delete_original_after_split_trim", type=inkex.Boolean, default=False, help="Delete original paths after sub splitting / trimming")
#Highlighting - Applying to original paths and sub split lines
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_rel_abs_mixed", type=inkex.Boolean, default=False, help="mixed rel/abs cmd (relative + absolute) paths")
pars.add_argument("--highlight_polylines", type=inkex.Boolean, default=False, help="(poly)line paths")
pars.add_argument("--highlight_beziers", type=inkex.Boolean, default=False, help="bezier paths")
pars.add_argument("--highlight_poly_bez_mixed", type=inkex.Boolean, default=False, help="mixed (poly)line/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")
#Highlighting - Applying to sub split lines only
pars.add_argument("--draw_subsplit", type=inkex.Boolean, default=False, help="Draw sub split lines ((poly)lines)")
pars.add_argument("--highlight_duplicates", type=inkex.Boolean, default=False, help="duplicates (only applies to sub split lines)")
pars.add_argument("--highlight_merges", type=inkex.Boolean, default=False, help="merges (only applies to sub split lines)")
#Highlighting - Intersection points
pars.add_argument("--highlight_self_intersecting", type=inkex.Boolean, default=False, help="self-intersecting paths")
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")
#Trimming - General trimming settings
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_trim_duplicates", type=inkex.Boolean, default=True, help="Remove duplicate trim lines")
pars.add_argument("--reverse_trim_removal_order", type=inkex.Boolean, default=False, help="Reverses the order of removal. Relevant for keeping certain styles of elements")
pars.add_argument("--remove_subsplit_after_trimming", type=inkex.Boolean, default=True, help="Remove sub split lines after trimming")
#Trimming - Bentley-Ottmann sweep line settings
pars.add_argument("--bent_ott_use_ignore_segment_endings", type=inkex.Boolean, default=True, help="Whether to ignore intersections of line segments when both their end points form the intersection point")
pars.add_argument("--bent_ott_use_debug", type=inkex.Boolean, default=False)
pars.add_argument("--bent_ott_use_verbose", type=inkex.Boolean, default=False)
pars.add_argument("--bent_ott_use_paranoid", type=inkex.Boolean, default=False)
pars.add_argument("--bent_ott_use_vertical", type=inkex.Boolean, default=True)
pars.add_argument("--bent_ott_number_type", default="native")
#Colors
pars.add_argument("--color_subsplit", type=Color, default='1630897151', help="sub split lines")
#Colors - path structure
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_rel_abs_mixed", type=Color, default='3351636735', help="mixed rel/abs cmd (relative + absolute) paths")
pars.add_argument("--color_polyline", type=Color, default='4289703935', help="(poly)line paths")
pars.add_argument("--color_bezier", type=Color, default='258744063', help="bezier paths")
pars.add_argument("--color_poly_bez_mixed", type=Color, default='4118348031', help="mixed (poly)line/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")
#Colors - duplicates and merges
pars.add_argument("--color_duplicates", type=Color, default='897901823', help="duplicates")
pars.add_argument("--color_merges", type=Color, default='869366527', help="merges")
#Colors - intersections
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")
#Colors - trimming
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")
def effect(self):
so = self.options
if so.break_apart is True and so.show_debug is True:
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)!")
#some configuration dependecies
if so.highlight_self_intersecting is True or \
so.highlight_duplicates is True or \
so.highlight_merges is True:
so.draw_subsplit = True
if so.highlight_duplicates is True or \
so.highlight_merges is True:
so.filter_subsplit_collinear = True
if so.filter_subsplit_collinear is True: #this is a must.
#if so.draw_subsplit is disabled bu we filter sub split lines and follow with trim operation we lose a lot of elements which may not be deleted!
so.draw_subsplit = True
if so.draw_subsplit is False and so.draw_trimmed is True:
so.delete_original_after_split_trim = True
if so.draw_trimmed is True:
so.draw_subsplit = True
if so.bent_ott_use_debug is True:
so.show_debug = True
#some constant stuff / styles
relativePathStyle = {'stroke': str(so.color_relative), 'fill': 'none', 'stroke-width': so.strokewidth}
absolutePathStyle = {'stroke': str(so.color_absolute), 'fill': 'none', 'stroke-width': so.strokewidth}
mixedRelAbsPathStyle = {'stroke': str(so.color_rel_abs_mixed), 'fill': 'none', 'stroke-width': so.strokewidth}
polylinePathStyle = {'stroke': str(so.color_polyline), 'fill': 'none', 'stroke-width': so.strokewidth}
bezierPathStyle = {'stroke': str(so.color_bezier), 'fill': 'none', 'stroke-width': so.strokewidth}
mixedPolyBezPathStyle = {'stroke': str(so.color_poly_bez_mixed), '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}
duplicatesPathStyle = {'stroke': str(so.color_duplicates), 'fill': 'none', 'stroke-width': so.strokewidth}
mergesPathStyle = {'stroke': str(so.color_merges), '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}
#some config for Bentley Ottmann - applies to highlighting, removing, trimming
poly_point_isect.USE_IGNORE_SEGMENT_ENDINGS = so.bent_ott_use_ignore_segment_endings
poly_point_isect.USE_DEBUG = so.bent_ott_use_debug
poly_point_isect.USE_VERBOSE = so.bent_ott_use_verbose
if so.show_debug is False:
poly_point_isect.USE_VERBOSE = False
poly_point_isect.USE_PARANOID = so.bent_ott_use_paranoid
poly_point_isect.USE_VERTICAL = so.bent_ott_use_vertical
NUMBER_TYPE = so.bent_ott_number_type
if NUMBER_TYPE == 'native':
Real = float
NUM_EPS = Real("1e-10")
NUM_INF = Real(float("inf"))
elif NUMBER_TYPE == 'numpy':
import numpy
Real = numpy.float64
del numpy
NUM_EPS = Real("1e-10")
NUM_INF = Real(float("inf"))
poly_point_isect.Real = Real
poly_point_isect.NUM_EPS = NUM_EPS
poly_point_isect.NUM_INF = NUM_INF
poly_point_isect.NUM_EPS_SQ = NUM_EPS * NUM_EPS
poly_point_isect.NUM_ZERO = Real(0.0)
poly_point_isect.NUM_ONE = Real(1.0)
#get all paths which are within selection or in document and generate sub split lines
pathElements = self.get_path_elements()
subSplitLineArray = []
for pathElement in pathElements:
originalPathId = pathElement.attrib["id"]
path = pathElement.path.transform(pathElement.composed_transform())
#path = pathElement.path
'''
check for relative or absolute paths
'''
isRelative = False
isAbsolute = False
isRelAbsMixed = False
relCmds = ['m', 'l', 'h', 'v', 'c', 's', 'q', 't', 'a', 'z']
if any(relCmd in str(path) for relCmd in relCmds):
isRelative = True
if any(relCmd.upper() in str(path) for relCmd in relCmds):
isAbsolute = True
if isRelative is True and isAbsolute is True: #cannot be both at the same time, so it's mixed
isRelAbsMixed = 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_rel_abs_mixed is True and isRelAbsMixed is True:
pathElement.delete()
continue #skip this loop iteration
'''
check for bezier or (poly)line paths
'''
isPoly = False
isBezier = False
isPolyBezMixed = False
if ('c' in str(path) or 'C' in str(path)):
isBezier = True
if ('l' in str(path) or 'L' in str(path)):
isPoly = True
if isPoly is True and isBezier is True: #cannot be both at the same time, so it's mixed
isPolyBezMixed = True
isPoly = False #reset
isBezier = False #reset
#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 isPoly is True:
pathElement.delete()
continue #skip this loop iteration
if so.remove_poly_bez_mixed is True and isPolyBezMixed is False:
pathElement.delete()
continue #skip this loop iteration
'''
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
if so.draw_subsplit is True:
subSplitLineGroup = pathElement.getparent().add(inkex.Group(id="{}-{}".format(idPrefixSubSplit, originalPathId)))
#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
for subPath in subPaths:
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 or isPolyBezMixed 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 (poly)lines from segment data
subSplitLines = []
for i in range(len(segs) - 1): #we could do the same routine to build up (poly)lines using "for x, y in node.path.end_points". See "number nodes" extension
x1, y1, x2, y2 = self.line_from_segments(segs, i, so.decimals)
#self.msg("(y1 = {},y2 = {},x1 = {},x2 = {})".format(x1, y1, x2, y2))
subSplitId = "{}-{}-{}".format(idPrefixSubSplit, originalPathId, i)
line = inkex.PathElement(id=subSplitId)
#apply line path with composed negative transform from parent element
line.attrib['d'] = 'M {},{} L {},{}'.format(x1, y1, x2, y2) #we set the path of Line using 'd' attribute because if we use trimLine.path the decimals get cut off unwantedly
#line.path = [['M', [x1, y1]], ['L', [x2, y2]]]
if pathElement.getparent() != self.svg.root and pathElement.getparent() != None:
line.path = line.path.transform(-pathElement.getparent().composed_transform())
line.style = basicSubSplitLineStyle
line.attrib['originalPathId'] = originalPathId
line.attrib['originalPathIsRelative'] = str(isRelative)
line.attrib['originalPathIsAbsolute'] = str(isAbsolute)
line.attrib['originalPathIsRelAbsMixed'] = str(isRelAbsMixed)
line.attrib['originalPathIsBezier'] = str(isBezier)
line.attrib['originalPathIsPoly'] = str(isPoly)
line.attrib['originalPathIsPolyBezMixed'] = str(isPolyBezMixed)
line.attrib['originalPathIsClosed'] = str(isClosed)
line.attrib['originalPathStyle'] = str(pathElement.style)
subSplitLineArray.append(line)
if so.subsplit_style == "apply_from_highlightings":
if line.attrib['originalPathIsRelative'] == 'True':
if so.highlight_relative is True:
line.style = relativePathStyle
if line.attrib['originalPathIsAbsolute'] == 'True':
if so.highlight_absolute is True:
line.style = absolutePathStyle
if line.attrib['originalPathIsRelAbsMixed'] == 'True':
if so.highlight_rel_abs_mixed is True:
line.style = mixedRelAbsPathStyle
if line.attrib['originalPathIsPoly'] == 'True':
if so.highlight_polylines is True:
line.style = polylinePathStyle
if line.attrib['originalPathIsBezier'] == 'True':
if so.highlight_beziers is True:
line.style = bezierPathStyle
if line.attrib['originalPathIsPolyBezMixed'] == 'True':
if so.highlight_poly_bez_mixed is True:
line.style = mixedPolyBezPathStyle
if line.attrib['originalPathIsClosed'] == 'True':
if so.highlight_closed is True:
line.style = closedPathStyle
else:
if so.highlight_opened is True:
line.style = openPathStyle
elif so.subsplit_style == "apply_from_original":
line.style = line.attrib['originalPathStyle']
if so.draw_subsplit is True:
subSplitLineGroup.add(line)
subSplitLines.append([(x1, y1), (x2, y2)])
#check for self intersections using Bentley-Ottmann algorithm.
isSelfIntersecting = False
if so.highlight_self_intersecting is True or so.remove_self_intersecting or so.visualize_self_intersections:
selfIntersectionPoints = isect_segments(subSplitLines, validate=True)
if len(selfIntersectionPoints) > 0:
isSelfIntersecting = True
if so.show_debug is True:
self.msg("{} in {} intersects itself with {} intersections!".format(subSplitId, originalPathId, len(selfIntersectionPoints)))
if so.highlight_self_intersecting is True:
for subSplitLine in subSplitLineGroup:
subSplitLine.style = selfIntersectingPathStyle #adjusts line color
#delete cosmetic sub split lines if desired
if so.remove_self_intersecting:
subSplitLineGroup.delete()
if so.visualize_self_intersections is True: #draw points (circles)
selfIntersectionGroup = self.visualize_self_intersections(pathElement, selfIntersectionPoints)
#delete self-intersecting sub split lines and orginal paths
if so.remove_self_intersecting:
subSplitLineArray = subSplitLineArray[:len(subSplitLineArray) - len(segs) - 1] #remove all last added lines
pathElement.delete() #and finally delete the orginal path
continue
#adjust the style of original paths if desired. Has influence to the finally trimmed lines style results too!
if so.removefillsetstroke is True:
self.adjust_style(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 isRelAbsMixed is True:
if so.highlight_rel_abs_mixed is True:
pathElement.style = mixedRelAbsPathStyle
if isBezier is True:
if so.highlight_beziers is True:
pathElement.style = bezierPathStyle
if isPoly is True:
if so.highlight_polylines is True:
pathElement.style = polylinePathStyle
if isPolyBezMixed is True:
if so.highlight_poly_bez_mixed is True:
pathElement.style = mixedPolyBezPathStyle
if isClosed is True:
if so.highlight_closed is True:
pathElement.style = closedPathStyle
else:
if so.highlight_opened is True:
pathElement.style = openPathStyle
if isSelfIntersecting is True:
if so.highlight_self_intersecting is True:
pathElement.style = selfIntersectingPathStyle
if so.draw_subsplit is True:
if subSplitLineGroup is not None: #might get deleted before so we need to check this first
subSplitLineGroup = reversed(subSplitLineGroup) #reverse the order to match the original path segment placing
if so.show_debug is True:
self.msg("sub split line count: {}".format(len(subSplitLineArray)))
'''
check for collinear lines and apply filters to remove or regroup and to restyle them
Run this action only if one of the options requires it (to avoid useless calculation cycles)
'''
if so.filter_subsplit_collinear is True or \
so.highlight_duplicates is True or \
so.highlight_merges is True:
if so.show_debug is True: self.msg("filtering collinear overlapping lines / duplicate lines")
if len(subSplitLineArray) > 0:
output_set, dropped_ids = self.filter_collinear(subSplitLineArray)
deleteIndices = []
deleteIndice = 0
for subSplitLine in subSplitLineArray:
'''
Replace the overlapping items with the new merged output
'''
for output in output_set:
if output['id'] == subSplitLine.attrib['id']:
originalSplitLinePath = subSplitLine.path
output_line = 'M {},{} L {},{}'.format(
output['p0'][0], output['p0'][1], output['p1'][0], output['p1'][1])
output_line_reversed = 'M {},{} L {},{}'.format(
output['p1'][0], output['p1'][1], output['p0'][0], output['p0'][1])
subSplitLine.attrib['d'] = output_line #we set the path using 'd' attribute because if we use trimLine.path the decimals get cut off unwantedly
mergedSplitLinePath = subSplitLine.path
mergedSplitLinePathReversed = Path(output_line_reversed)
#subSplitLine.path = [['M', output['p0']], ['L', output['p1']]]
#self.msg("composed_transform = {}".format(output['composed_transform']))
#subSplitLine.transform = Transform(-output['composed_transform']) * subSplitLine.transform
subSplitLine.path = subSplitLine.path.transform(-output['composed_transform'])
if so.highlight_merges is True:
if originalSplitLinePath != mergedSplitLinePath and \
originalSplitLinePath != mergedSplitLinePathReversed: #if the path changed we are going to highlight it
subSplitLine.style = mergesPathStyle
'''
Delete or move sub split lines which are overlapping
'''
ssl_id = subSplitLine.get('id')
if ssl_id in dropped_ids:
if so.highlight_duplicates is True:
subSplitLine.style = duplicatesPathStyle
if so.filter_subsplit_collinear is True:
ssl_parent = subSplitLine.getparent()
if so.filter_subsplit_collinear_action == "remove":
if self.options.show_debug is True:
self.msg("Deleting sub split line {}".format(subSplitLine.get('id')))
subSplitLine.delete() #delete the line from XML tree
deleteIndices.append(deleteIndice) #store this id to remove it from stupid subSplitLineArray later
elif so.filter_subsplit_collinear_action == "separate_group":
if self.options.show_debug is True:
self.msg("Moving sub split line {}".format(subSplitLine.get('id')))
originalPathId = subSplitLine.attrib['originalPathId']
collinearGroupId = '{}-{}'.format(collinearVerb, originalPathId)
originalPathElement = self.svg.getElementById(originalPathId)
collinearGroup = self.find_group(collinearGroupId)
if collinearGroup is None:
collinearGroup = originalPathElement.getparent().add(inkex.Group(id=collinearGroupId))
collinearGroup.append(subSplitLine) #move to that group
#and delete the containg group if empty (can happen in "remove" or "separate_group" constellation
if ssl_parent is not None and len(ssl_parent) == 0:
if self.options.show_debug is True:
self.msg("Deleting group {}".format(ssl_parent.get('id')))
ssl_parent.delete()
deleteIndice += 1 #end the loop by incrementing +1
#shrink the sub split line array to kick out all unrequired indices
for deleteIndice in sorted(deleteIndices, reverse=True):
if self.options.show_debug is True:
self.msg("Deleting index {} from subSplitLineArray".format(deleteIndice))
del subSplitLineArray[deleteIndice]
'''
now we intersect the sub split lines to find the global intersection points using Bentley-Ottmann algorithm (contains self-intersections too!)
'''
if so.draw_trimmed is True:
try:
allSubSplitLineStrings = []
for subSplitLine in subSplitLineArray:
csp = Path(subSplitLine.path.transform(subSplitLine.composed_transform())).to_arrays() #will be buggy if draw subsplit lines is deactivated
lineString = [(csp[0][1][0], csp[0][1][1]), (csp[1][1][0], csp[1][1][1])]
#lineStringStyle = {'stroke': '#0000FF', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px'))}
#line = self.svg.get_current_layer().add(inkex.PathElement(id=self.svg.get_unique_id('lineString')))
#line.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(lineString[0][0],lineString[0][1],lineString[1][0],lineString[1][1]))
#line.style = lineStringStyle
#line.transform = -self.svg.get_current_layer().transform
if so.remove_trim_duplicates is True:
if lineString not in allSubSplitLineStrings:
allSubSplitLineStrings.append(lineString)
else:
if so.show_debug is True:
self.msg("line {} already in sub split line collection. Dropping ...".format(lineString))
else: #if false we append all segments without filtering duplicate ones
allSubSplitLineStrings.append(lineString)
if so.show_debug is True:
self.msg("Going to calculate intersections using Bentley Ottmann Sweep Line Algorithm")
globalIntersectionPoints = MultiPoint(isect_segments(allSubSplitLineStrings, 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
'''
allTrimGroups = [] #container to collect all trim groups for later on processing
for subSplitIndex in range(len(subSplitLineArray)):
trimGroup = self.build_trim_line_group(subSplitLineArray, subSplitIndex, globalIntersectionPoints)
if trimGroup is not None:
if trimGroup not in allTrimGroups:
allTrimGroups.append(trimGroup)
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.")
if so.bezier_trimming is True:
if so.show_debug is True: self.msg("trimming beziers - not working yet")
self.trim_bezier(allTrimGroups)
if so.remove_trim_duplicates is True:
if so.show_debug is True: self.msg("checking for duplicate trim lines and deleting them")
self.remove_trim_duplicates(allTrimGroups)
if so.combine_nonintersects is True:
if so.show_debug is True: self.msg("glueing together all non-intersected sub split lines to larger path structures again (cleaning up)")
self.combine_nonintersects(allTrimGroups)
if so.remove_subsplit_after_trimming is True:
if so.show_debug is True: self.msg("removing unwanted subsplit lines after trimming")
for subSplitLine in subSplitLineArray:
ssl_parent = subSplitLine.getparent()
subSplitLine.delete()
if ssl_parent is not None and len(ssl_parent) == 0:
if self.options.show_debug is True:
self.msg("Deleting group {}".format(ssl_parent.get('id')))
ssl_parent.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
#clean original paths if selected.
if so.delete_original_after_split_trim is True:
if so.show_debug is True: self.msg("cleaning original paths after sub splitting / trimming")
for pathElement in pathElements:
pathElement.delete()
if __name__ == '__main__':
ContourScannerAndTrimmer().run()