refactored contour scanner

This commit is contained in:
Mario Voigt 2021-06-11 01:37:23 +02:00
parent 908a389eea
commit f1c189919b
2 changed files with 78 additions and 108 deletions

View File

@ -37,7 +37,7 @@
<param name="highlight_closed" type="bool" gui-text="closed paths">false</param> <param name="highlight_closed" type="bool" gui-text="closed paths">false</param>
<param name="highlight_self_intersecting" type="bool" gui-text="self-intersecting paths" gui-description="Requires to draw sub split lines. Will override highlighting colors for open and closed paths (if those options are enabled)">false</param> <param name="highlight_self_intersecting" type="bool" gui-text="self-intersecting paths" gui-description="Requires to draw sub split lines. Will override highlighting colors for open and closed paths (if those options are enabled)">false</param>
<param name="visualize_self_intersections" type="bool" gui-text="self-intersecting path points">false</param> <param name="visualize_self_intersections" type="bool" gui-text="self-intersecting path points">false</param>
<param name="visualize_global_intersections" type="bool" gui-text="global intersection points" gui-description="Will also contain self-intersecting points!">false</param> <param name="visualize_global_intersections" type="bool" gui-text="global intersection points" gui-description="Will also contain self-intersecting points! Global intersections will only show if 'Draw trimmed lines' is enabled!">false</param>
</vbox> </vbox>
<separator/> <separator/>
<vbox> <vbox>

View File

@ -18,7 +18,6 @@ Extension for InkScape 1.0+
- duplicates in original selection - duplicates in original selection
- duplicates in split bezier - duplicates in split bezier
- ... - ...
- refactor subSplitData stuff by some clean mapping/structure instead having a lot of useless arays
- maybe option: convert abs path to rel path - maybe option: convert abs path to rel path
- maybe option: convert rel path to abs path - maybe option: convert rel path to abs path
replacedelement.path = replacedelement.path.to_absolute().to_superpath().to_path() replacedelement.path = replacedelement.path.to_absolute().to_superpath().to_path()
@ -52,7 +51,7 @@ Extension for InkScape 1.0+
Author: Mario Voigt / FabLab Chemnitz Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org Mail: mario.voigt@stadtfabrikanten.org
Date: 09.08.2020 (extension originally called "Contour Scanner") Date: 09.08.2020 (extension originally called "Contour Scanner")
Last patch: 05.06.2021 Last patch: 11.06.2021
License: GNU GPL v3 License: GNU GPL v3
''' '''
@ -143,7 +142,7 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
if len(pathElements) == 0: 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') 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 exit(1)
if self.options.break_apart is True: if self.options.break_apart is True:
breakApartElements = None breakApartElements = None
@ -238,21 +237,24 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
globalIntersectionGroup.add(globalIntersectionPointCircle) globalIntersectionGroup.add(globalIntersectionPointCircle)
def buildTrimLineGroups(self, allSubSplitData, subSplitIndex, globalIntersectionPoints, def buildTrimLineGroups(self, subSplitLineArray, subSplitIndex, globalIntersectionPoints,
snap_tolerance, apply_style_to_trimmed): snap_tolerance, apply_style_to_trimmed):
''' make a group containing trimmed lines''' ''' make a group containing trimmed lines'''
#Check if we should skip or process the path anyway #Check if we should skip or process the path anyway
isClosed = allSubSplitData[5][subSplitIndex] isClosed = subSplitLineArray[subSplitIndex].attrib['originalPathIsClosed']
if self.options.trimming_path_types == 'open_paths' and isClosed is True: return #skip this call 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 is False: 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 elif self.options.trimming_path_types == 'both': pass
csp = subSplitLineArray[subSplitIndex].path.to_arrays()
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} trimLineStyle = {'stroke': str(self.options.color_trimmed), 'fill': 'none', 'stroke-width': self.options.strokewidth}
linesWithSnappedIntersectionPoints = snap(LineString(allSubSplitData[0][subSplitIndex]), globalIntersectionPoints, snap_tolerance) linesWithSnappedIntersectionPoints = snap(ls, globalIntersectionPoints, snap_tolerance)
trimGroupId = 'shapely-' + allSubSplitData[1][subSplitIndex].split("_")[0] #spit at "_" (_ from subSplitId) trimGroupId = 'shapely-' + subSplitLineArray[subSplitIndex].attrib['id'].split("_")[0] #split at "_" (_ from subSplitId)
trimGroupParentId = allSubSplitData[1][subSplitIndex].split(idPrefix+"-")[1].split("_")[0] trimGroupParentId = subSplitLineArray[subSplitIndex].attrib['id'].split(idPrefix+"-")[1].split("_")[0]
trimGroupParent = self.svg.getElementById(trimGroupParentId) trimGroupParent = self.svg.getElementById(trimGroupParentId)
trimGroupParentTransform = trimGroupParent.composed_transform() trimGroupParentTransform = trimGroupParent.composed_transform()
trimGroup = self.findGroup(trimGroupId) trimGroup = self.findGroup(trimGroupId)
@ -260,8 +262,8 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
trimGroup = trimGroupParent.getparent().add(inkex.Group(id=trimGroupId)) 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) #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['originalPathIsBezier'] = subSplitLineArray[subSplitIndex].attrib['originalPathIsBezier']
trimGroup.attrib['originalId'] = allSubSplitData[4][subSplitIndex] trimGroup.attrib['originalPathId'] = subSplitLineArray[subSplitIndex].attrib['originalPathId']
#split all lines against all other lines using the intersection points #split all lines against all other lines using the intersection points
trimLines = split(linesWithSnappedIntersectionPoints, globalIntersectionPoints) trimLines = split(linesWithSnappedIntersectionPoints, globalIntersectionPoints)
@ -281,9 +283,6 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
prevLine.attrib['id'] = trimGroupId + "-" + str(subSplitIndex) + self.svg.get_unique_id(intersectedVerb) prevLine.attrib['id'] = trimGroupId + "-" + str(subSplitIndex) + self.svg.get_unique_id(intersectedVerb)
prevLine.attrib['intersected'] = 'True' #some dirty flag we need prevLine.attrib['intersected'] = 'True' #some dirty flag we need
prevLine = trimLine = inkex.PathElement(id=trimLineId) 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 x, y = trimLines[j].coords.xy
trimLine.path = [['M', [x[0],y[0]]], ['L', [x[1],y[1]]]] trimLine.path = [['M', [x[0],y[0]]], ['L', [x[1],y[1]]]]
if trimGroupParentTransform is not None: if trimGroupParentTransform is not None:
@ -291,7 +290,7 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
if apply_style_to_trimmed is False: if apply_style_to_trimmed is False:
trimLine.style = trimLineStyle trimLine.style = trimLineStyle
else: else:
trimLine.style = allSubSplitData[2][subSplitIndex] trimLine.style = subSplitLineArray[subSplitIndex].attrib['originalPathStyle']
trimGroup.add(trimLine) trimGroup.add(trimLine)
return trimGroup return trimGroup
@ -395,9 +394,9 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
self.msg("t parameter = {}".format(globalTParameter)) self.msg("t parameter = {}".format(globalTParameter))
chainLength = 0 chainLength = 0
if self.options.show_debug is True: if self.options.show_debug is True:
self.msg("Trimming the original bezier path {} at global t parameters: {}".format(trimGroup.attrib['originalId'], globalTParameters)) self.msg("Trimming the original bezier path {} at global t parameters: {}".format(trimGroup.attrib['originalPathId'], globalTParameters))
for globalTParameter in globalTParameters: for globalTParameter in globalTParameters:
csp = CubicSuperPath(self.svg.getElementById(trimGroup.attrib['originalId'])) 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 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. Instead we only can split a bezier by getting to commands which build up a bezier path segment.
@ -500,20 +499,7 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
#get all paths which are within selection or in document and generate sub split lines #get all paths which are within selection or in document and generate sub split lines
pathElements = self.getPathElements() pathElements = self.getPathElements()
allSubSplitLines = [] subSplitLineArray = []
allSubSplitIds = []
allSubSplitStyles = []
allSubSplitIsBezier = []
allSubSplitOriginalPathIds = []
allSubSplitIsClosed = []
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
allSubSplitData.append(allSubSplitIsClosed) #column 5
for pathElement in pathElements: for pathElement in pathElements:
originalPathId = pathElement.attrib["id"] originalPathId = pathElement.attrib["id"]
@ -579,7 +565,7 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
continue #skip this loop iteration continue #skip this loop iteration
if so.draw_subsplit is True: if so.draw_subsplit is True:
subSplitTrimLineGroup = pathElement.getparent().add(inkex.Group(id="{}-{}".format(idPrefix, pathElement.attrib["id"]))) subSplitLineGroup = pathElement.getparent().add(inkex.Group(id="{}-{}".format(idPrefix, pathElement.attrib["id"])))
#get all sub paths for the path of the element #get all sub paths for the path of the element
subPaths, prev = [], 0 subPaths, prev = [], 0
@ -613,103 +599,78 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
#build polylines from segment data #build polylines from segment data
subSplitLines = [] subSplitLines = []
subSplitIds = []
subSplitStyles = []
subSplitIsBezier = []
subSplitOriginalPathIds = []
subSplitIsClosed = []
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 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) x1, y1, x2, y2 = self.lineFromSegments(segs, i, so.decimals)
#self.msg("(y1 = {},y2 = {},x1 = {},x2 = {})".format(x1, y1, x2, y2)) #self.msg("(y1 = {},y2 = {},x1 = {},x2 = {})".format(x1, y1, x2, y2))
subSplitId = "{}-{}_{}".format(idPrefix, originalPathId, i) subSplitId = "{}-{}_{}".format(idPrefix, originalPathId, i)
if so.draw_subsplit is True: line = inkex.PathElement(id=subSplitId)
line = inkex.PathElement(id=subSplitId) #apply line path with composed negative transform from parent element
#apply line path with composed negative transform from parent element line.path = [['M', [x1, y1]], ['L', [x2, y2]]]
line.path = [['M', [x1, y1]], ['L', [x2, y2]]] if pathElement.getparent() != self.svg.root and pathElement.getparent() != None:
if pathElement.getparent() != self.svg.root: line.path = line.path.transform(-pathElement.getparent().composed_transform())
line.path = line.path.transform(-pathElement.getparent().composed_transform()) line.style = basicSubSplitLineStyle
line.style = basicSubSplitLineStyle line.attrib['originalPathId'] = originalPathId
line.attrib['isRelative'] = str(isRelative) line.attrib['originalPathIsRelative'] = str(isRelative)
line.attrib['isAbsolute'] = str(isAbsolute) line.attrib['originalPathIsAbsolute'] = str(isAbsolute)
line.attrib['isMixed'] = str(isMixed) line.attrib['originalPathIsMixed'] = str(isMixed)
line.attrib['isBezier'] = str(isBezier) line.attrib['originalPathIsBezier'] = str(isBezier)
line.attrib['isClosed'] = str(isClosed) line.attrib['originalPathIsClosed'] = str(isClosed)
subSplitTrimLineGroup.add(line) line.attrib['originalPathStyle'] = str(pathElement.style)
subSplitLineArray.append(line)
subSplitLines.append([(x1, y1), (x2, y2)]) if so.apply_style_to_subsplits is True:
subSplitIds.append(subSplitId) if line.attrib['originalPathIsRelative'] == 'True':
subSplitStyles.append(pathElement.style)
subSplitIsBezier.append(isBezier) #some dirty flag we need
subSplitOriginalPathIds.append(originalPathId) #some dirty flag we need
subSplitIsClosed.append(isClosed) #some dirty flag we need
if so.draw_subsplit is True and so.apply_style_to_subsplits is True:
for subSplitLine in subSplitTrimLineGroup:
if subSplitLine.attrib['isRelative'] == 'True':
if so.highlight_relative is True: if so.highlight_relative is True:
subSplitLine.style = relativePathStyle line.style = relativePathStyle
if subSplitLine.attrib['isAbsolute'] == 'True': if line.attrib['originalPathIsAbsolute'] == 'True':
if so.highlight_absolute is True: if so.highlight_absolute is True:
subSplitLine.style = absolutePathStyle line.style = absolutePathStyle
if subSplitLine.attrib['isMixed'] == 'True': if line.attrib['originalPathIsMixed'] == 'True':
if so.highlight_mixed is True: if so.highlight_mixed is True:
subSplitLine.style = mixedPathStyle line.style = mixedPathStyle
if subSplitLine.attrib['isBezier'] == 'True': if line.attrib['originalPathIsBezier'] == 'True':
if so.highlight_beziers is True: if so.highlight_beziers is True:
subSplitLine.style = bezierPathStyle line.style = bezierPathStyle
else: else:
if so.highlight_polylines is True: if so.highlight_polylines is True:
subSplitLine.style = polylinePathStyle line.style = polylinePathStyle
if subSplitLine.attrib['isClosed'] == 'True': if line.attrib['originalPathIsClosed'] == 'True':
#if subPathIsClosed is True:
if so.highlight_closed is True: if so.highlight_closed is True:
subSplitLine.style = closedPathStyle line.style = closedPathStyle
else: else:
if so.highlight_opened is True: if so.highlight_opened is True:
subSplitLine.style = openPathStyle line.style = openPathStyle
if so.draw_subsplit is True:
subSplitLineGroup.add(line)
subSplitLines.append([(x1, y1), (x2, y2)])
#check for self intersections using Bentley-Ottmann algorithm. #check for self intersections using Bentley-Ottmann algorithm.
isSelfIntersecting = False
selfIntersectionPoints = isect_segments(subSplitLines, validate=True) selfIntersectionPoints = isect_segments(subSplitLines, validate=True)
if len(selfIntersectionPoints) > 0: if len(selfIntersectionPoints) > 0:
isSelfIntersecting = True
if so.show_debug is True: if so.show_debug is True:
self.msg("{} in {} intersects itself with {} intersections!".format(subSplitId, originalPathId, len(selfIntersectionPoints))) self.msg("{} in {} intersects itself with {} intersections!".format(subSplitId, originalPathId, len(selfIntersectionPoints)))
if so.draw_subsplit is True: if so.draw_subsplit is True:
if so.highlight_self_intersecting is True: if so.highlight_self_intersecting is True:
for subSplitLine in subSplitTrimLineGroup: for subSplitLine in subSplitLineGroup:
subSplitLine.style = selfIntersectingPathStyle #adjusts line color subSplitLine.style = selfIntersectingPathStyle #adjusts line color
#delete cosmetic sub split lines if desired #delete cosmetic sub split lines if desired
if so.remove_self_intersecting: if so.remove_self_intersecting:
subSplitTrimLineGroup.delete() subSplitLineGroup.delete()
if so.visualize_self_intersections is True: #draw points (circles) if so.visualize_self_intersections is True: #draw points (circles)
self.visualize_self_intersections(pathElement, selfIntersectionPoints) self.visualize_self_intersections(pathElement, selfIntersectionPoints)
#and also delete non-cosmetics #delete self-intersecting sub split lines and orginal paths
if so.remove_self_intersecting: if so.remove_self_intersecting:
#if we also want to avoid processing them for trimming subSplitLineArray = subSplitLineArray[:len(subSplitLineArray) - len(segs) - 1] #remove all last added lines
subSplitLines = None
subSplitIds = None
subSplitStyles = None
subSplitIsBezier = None
subSplitOriginalPathIds = None
pathElement.delete() #and finally delete the orginal path pathElement.delete() #and finally delete the orginal path
continue
#extend the complete collection
if subSplitLines != None and \
subSplitIds != None and \
subSplitStyles != None and \
subSplitIsBezier != None and \
allSubSplitOriginalPathIds != None and \
allSubSplitIsClosed != None:
allSubSplitStyles.extend(subSplitStyles)
allSubSplitLines.extend(subSplitLines)
allSubSplitIds.extend(subSplitIds)
allSubSplitIsBezier.extend(subSplitIsBezier)
allSubSplitOriginalPathIds.extend(subSplitOriginalPathIds)
allSubSplitIsClosed.extend(subSplitIsClosed)
#adjust the style of original paths if desired. Has influence to the finally trimmed lines style results too! #adjust the style of original paths if desired. Has influence to the finally trimmed lines style results too!
if so.removefillsetstroke is True: if so.removefillsetstroke is True:
@ -742,19 +703,29 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
if so.highlight_opened is True: if so.highlight_opened is True:
pathElement.style = openPathStyle pathElement.style = openPathStyle
if isSelfIntersecting is True:
if so.highlight_self_intersecting is True:
pathElement.style = selfIntersectingPathStyle
if so.draw_subsplit is True: if so.draw_subsplit is True:
if subSplitTrimLineGroup is not None: #might get deleted before so we need to check this first if subSplitLineGroup 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 subSplitLineGroup = reversed(subSplitLineGroup) #reverse the order to match the original path segment placing
if so.show_debug is True: if so.show_debug is True:
self.msg("sub split line count: {}".format(len(allSubSplitLines))) self.msg("sub split line count: {}".format(len(subSplitLineArray)))
''' '''
now we intersect the sub split lines to find the global intersection points using Bentley-Ottmann algorithm (contains self-intersections too!) 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: if so.draw_trimmed is True:
try: try:
globalIntersectionPoints = MultiPoint(isect_segments(allSubSplitData[0], validate=True)) allSubSplitLineStrings = []
for subSplitLine in subSplitLineArray:
csp = subSplitLine.path.to_arrays()
allSubSplitLineStrings.append([(csp[0][1][0], csp[0][1][1]), (csp[1][1][0], csp[1][1][1])])
globalIntersectionPoints = MultiPoint(isect_segments(allSubSplitLineStrings, validate=True))
if so.show_debug is True: if so.show_debug is True:
self.msg("global intersection points count: {}".format(len(globalIntersectionPoints))) self.msg("global intersection points count: {}".format(len(globalIntersectionPoints)))
if len(globalIntersectionPoints) > 0: if len(globalIntersectionPoints) > 0:
@ -765,10 +736,9 @@ class ContourScannerAndTrimmer(inkex.EffectExtension):
now we trim the sub split lines at all calculated intersection points. 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 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 allTrimGroups = [] #container to collect all trim groups for later on processing
for subSplitIndex in range(len(allSubSplitData[0])): for subSplitIndex in range(len(subSplitLineArray)):
trimGroup = self.buildTrimLineGroups(allSubSplitData, subSplitIndex, trimGroup = self.buildTrimLineGroups(subSplitLineArray, subSplitIndex,
globalIntersectionPoints, so.snap_tolerance, so.apply_style_to_trimmed) globalIntersectionPoints, so.snap_tolerance, so.apply_style_to_trimmed)
if trimGroup is not None: if trimGroup is not None:
if trimGroup not in allTrimGroups: if trimGroup not in allTrimGroups: