changed Contour Scanner to Contour Scanner and Trimmer

This commit is contained in:
Mario Voigt 2021-06-01 21:37:08 +02:00
parent 7cced1b41f
commit a2f8e71e2b
5 changed files with 826 additions and 374 deletions

View File

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Contour Scanner</name>
<id>fablabchemnitz.de.contour_scanner</id>
<param name="main_tabs" type="notebook">
<page name="tab_settings" gui-text="Contour Scanner">
<hbox>
<vbox>
<label appearance="header">Highlight Paths</label>
<param name="highlight_opened" type="bool" gui-text="Highlight opened contours">true</param>
<param name="color_opened" type="color" appearance="colorbutton" gui-text="Color opened contours">4012452351</param>
<param name="highlight_closed" type="bool" gui-text="Highlight closed contours">true</param>
<param name="color_closed" type="color" appearance="colorbutton" gui-text="Color closed contours">2330080511</param>
<param name="highlight_selfintersecting" type="bool" gui-text="Highlight self-intersecting contours" gui-description="Due to the nature of the algorithm this might detect non-closed contours too.">true</param>
<param name="color_selfintersecting" type="color" appearance="colorbutton" gui-text="Color self-intersecting contours">1923076095</param>
<param name="highlight_intersectionpoints" type="bool" gui-text="Highlight self-intersecting points">true</param>
<param name="color_intersectionpoints" type="color" appearance="colorbutton" gui-text="Color self-intersecting points">4239343359</param>
<param name="dotsize" type="int" min="0" max="10000" gui-text="Dot size (px) for self-intersecting points">10</param>
<param name="addlines" type="bool" gui-text="Add closing lines for open self-crossing contours" gui-description="They will have the same color as the intersection points and help to better visualize possible virtual crossings. The algorithm can only detect intersections for closed contours by it's nature, but we handle open contours like they were closed. This may put put too much intersection points.">true</param>
<param name="polypaths" type="bool" gui-text="Add polypath outline for self-crossing contours" gui-description="This makes only sense if your path is actually a curve. If it's already a polyline you just get a duplicate line (but with reduced nodes)">true</param>
</vbox>
<separator/>
<vbox>
<label appearance="header">Remove Paths</label>
<param name="remove_opened" type="bool" gui-text="Remove opened contours">false</param>
<param name="remove_closed" type="bool" gui-text="Remove closed contours">false</param>
<param name="remove_selfintersecting" type="bool" gui-text="Remove self-intersecting contours">false</param>
<separator/>
<label appearance="header">General</label>
<param name="breakapart" type="bool" gui-text="Break apart selection into single contours" gui-description="(with ignoring the group hirarchy by taking all children elements)">false</param>
<param name="removefillsetstroke" type="bool" gui-text="Remove fill and define stroke">false</param>
<param name="strokewidth" min="0.0" max="10000.0" gui-text="Stroke width (px)" type="float">1.0</param>
<param name="show_debug" type="bool" gui-text="Show debug info">false</param>
</vbox>
</hbox>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Contour Scanner</label>
<label>This tool helps you to find nasty contours which might bug you and prevent your work from being ready for production. Ideally you should convert your paths to polylines because the algorithm used by Contour Scanner is not able to distinguish between curves and polylines. It will handle curves like polylines but that creates visual and measurable errors. Either use "Split Bezier (Subdivide Path)" or "Add Nodes" extension to split into smaller segments which makes more precise. You can also run "Flatten Bezier" to make smooth polylines. For roughest approximation use "Convert to Polylines" extension to create real polylines.</label>
<label>2020 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/contourscanner</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/MarioVoigt/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">Third Party Modules</label>
<label appearance="url">https://github.com/ideasman42/isect_segments-bentley_ottmann</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Nesting/Cut Optimization"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">contour_scanner.py</command>
</script>
</inkscape-extension>

View File

@ -1,295 +0,0 @@
#!/usr/bin/env python3
"""
Extension for InkScape 1.0
Features
- helps to find contours which are closed or not. Good for repairing contours, closing contours,...
- works for paths which are packed into groups or groups of groups. #
- can break contours apart like in "Path -> Break Apart"
- implements Bentley-Ottmann algorithm from https://github.com/ideasman42/isect_segments-bentley_ottmann to scan for self-intersecting paths. You might get "assert(event.in_sweep == False) AssertionError". Don't know how to fix rgis
- colorized paths respective to their type
- can add dots to intersection points you'd like to fix
Author: Mario Voigt / FabLab Chemnitz
Mail: mario.voigt@stadtfabrikanten.org
Date: 09.08.2020
Last patch: 19.05.2021
License: GNU GPL v3
ToDo:
- add option to replace last segment of closed paths by 'Z' or 'z' in case the first and last segment touch each other (coincident point)
"""
import sys
from math import *
from lxml import etree
import poly_point_isect
import copy
import inkex
from inkex.paths import Path, CubicSuperPath
from inkex import Style, Color, Circle
class ContourScanner(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--main_tabs")
pars.add_argument("--breakapart", type=inkex.Boolean, default=False, help="Break apart selection into single contours")
pars.add_argument("--removefillsetstroke", type=inkex.Boolean, default=False, help="Remove fill and define stroke")
pars.add_argument("--strokewidth", type=float, default=1.0, help="Stroke width (px)")
pars.add_argument("--highlight_opened", type=inkex.Boolean, default=True, help="Highlight opened contours")
pars.add_argument("--color_opened", type=Color, default='4012452351', help="Color opened contours")
pars.add_argument("--highlight_closed", type=inkex.Boolean, default=True, help="Highlight closed contours")
pars.add_argument("--color_closed", type=Color, default='2330080511', help="Color closed contours")
pars.add_argument("--highlight_selfintersecting", type=inkex.Boolean, default=True, help="Highlight self-intersecting contours")
pars.add_argument("--highlight_intersectionpoints", type=inkex.Boolean, default=True, help="Highlight self-intersecting points")
pars.add_argument("--color_selfintersecting", type=Color, default='1923076095', help="Color closed contours")
pars.add_argument("--color_intersectionpoints", type=Color, default='4239343359', help="self-intersecting points")
pars.add_argument("--addlines", type=inkex.Boolean, default=True, help="Add closing lines for self-crossing contours")
pars.add_argument("--polypaths", type=inkex.Boolean, default=True, help="Add polypath outline for self-crossing contours")
pars.add_argument("--dotsize", type=int, default=10, help="Dot size (px) for self-intersecting points")
pars.add_argument("--remove_opened", type=inkex.Boolean, default=False, help="Remove opened contours")
pars.add_argument("--remove_closed", type=inkex.Boolean, default=False, help="Remove closed contours")
pars.add_argument("--remove_selfintersecting", type=inkex.Boolean, default=False, help="Remove self-intersecting contours")
pars.add_argument("--show_debug", type=inkex.Boolean, default=False, help="Show debug info")
#function to refine the style of the lines
def adjustStyle(self, node):
if node.attrib.has_key('style'):
style = node.get('style')
if style:
declarations = style.split(';')
for i,decl in enumerate(declarations):
parts = decl.split(':', 2)
if len(parts) == 2:
(prop, val) = parts
prop = prop.strip().lower()
if prop == 'stroke-width':
declarations[i] = prop + ':' + str(self.svg.unittouu(str(self.options.strokewidth) +"px"))
if prop == 'fill':
declarations[i] = prop + ':none'
node.set('style', ';'.join(declarations) + ';stroke:#000000;stroke-opacity:1.0')
else:
node.set('style', 'stroke:#000000;stroke-opacity:1.0')
#get polyline from path
def getPolyline(self, node):
if node.tag == inkex.addNS('path','svg'):
polypath = []
i = 0
for x, y in node.path.end_points:
if i == 0:
polypath.append(['M', [x,y]])
else:
polypath.append(['L', [x,y]])
if i == 1 and polypath[len(polypath)-2][1] == polypath[len(polypath)-1][1]:
polypath.pop(len(polypath)-1) #special handling for the second point after M command
elif polypath[len(polypath)-2] == polypath[len(polypath)-1]: #get the previous point
polypath.pop(len(polypath)-1)
i += 1
return Path(polypath)
#split combined contours into single contours if enabled - this is exactly the same as "Path -> Break Apart"
replacedNodes = []
def breakContours(self, node): #this does the same as "CTRL + SHIFT + K"
if node.tag == inkex.addNS('path','svg'):
parent = node.getparent()
idx = parent.index(node)
idSuffix = 0
#raw = Path(node.get("d")).to_arrays()
#raw = node.path.transform(node.composed_transform()).to_superpath()
raw = node.path.transform(node.composed_transform()).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:
replacedNode = copy.copy(node)
oldId = replacedNode.get('id')
replacedNode.set('d', CubicSuperPath(subpath))
replacedNode.set('id', oldId + str(idSuffix).zfill(5))
parent.insert(idx, replacedNode)
idSuffix += 1
self.replacedNodes.append(replacedNode)
node.delete()
for child in node:
self.breakContours(child)
def scanContours(self, node):
if node.tag == inkex.addNS('path','svg'):
if self.options.removefillsetstroke:
self.adjustStyle(node)
intersectionGroup = node.getparent().add(inkex.Group())
#raw = (Path(node.get('d')).to_arrays())
raw = node.path.transform(node.composed_transform()).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 simpath in subPaths:
closed = False
if simpath[-1][0] == 'Z' or \
(simpath[-1][0] == 'L' and simpath[0][1] == simpath[-1][1]) or \
(simpath[-1][0] == 'C' and simpath[0][1] == [simpath[-1][1][-2], simpath[-1][1][-1]]) : #if first is last point the path is also closed. The "Z" command is not required
closed = True
if simpath[-2][0] == 'L': simpath[-1][1] = simpath[0][1]
else: simpath.pop()
points = []
for i in range(len(simpath)):
if simpath[i][0] == 'V': # vertical and horizontal lines only have one point in args, but 2 are required
simpath[i][0]='L' #overwrite V with regular L command
add=simpath[i-1][1][0] #read the X value from previous segment
simpath[i][1].append(simpath[i][1][0]) #add the second (missing) argument by taking argument from previous segment
simpath[i][1][0]=add #replace with recent X after Y was appended
if simpath[i][0] == 'H': # vertical and horizontal lines only have one point in args, but 2 are required
simpath[i][0]='L' #overwrite H with regular L command
simpath[i][1].append(simpath[i-1][1][1]) #add the second (missing) argument by taking argument from previous segment
points.append(simpath[i][1][-2:])
if points[0] == points[-1]: #if first is last point the path is also closed. The "Z" command is not required
closed = True
if closed == False:
if self.options.highlight_opened:
style = {'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_opened, 'stroke-linecap': 'butt', 'fill': 'none'}
node.attrib['style'] = Style(style).to_str()
if self.options.remove_opened:
try:
node.delete()
except AttributeError:
pass #we ignore that parent can be None
if closed == True:
if self.options.highlight_closed:
style = {'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_closed, 'stroke-linecap': 'butt', 'fill': 'none'}
node.attrib['style'] = Style(style).to_str()
if self.options.remove_closed:
try:
node.delete()
except AttributeError:
pass #we ignore that parent can be None
#if one of the options is activated we also check for self-intersecting
if self.options.highlight_selfintersecting or self.options.highlight_intersectionpoints:
#Style definitions
closingLineStyle = Style({'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_intersectionpoints, 'stroke-linecap': 'butt', 'fill': 'none'}).to_str()
intersectionPointStyle = Style({'stroke': 'none', 'fill': self.options.color_intersectionpoints}).to_str()
intersectionStyle = Style({'stroke-linejoin': 'miter', 'stroke-width': str(self.svg.unittouu(str(self.options.strokewidth) +"px")),
'stroke-opacity': '1.0', 'fill-opacity': '1.0',
'stroke': self.options.color_selfintersecting, 'stroke-linecap': 'butt', 'fill': 'none'}).to_str()
try:
if len(points) > 2: #try to find self-intersecting /overlapping polygons. We need at least 3 points to detect for intersections (only possible if first points matched last point)
isect = poly_point_isect.isect_polygon(points, validate=True)
if len(isect) > 0:
if closed == False and self.options.addlines == True: #if contour is open and we found intersection points those points might be not relevant
closingLine = intersectionGroup.add(inkex.PathElement())
closingLine.set('id', self.svg.get_unique_id('closingline-'))
closingLine.path = [
['M', [points[0][0],points[0][1]]],
['L', [points[-1][0],points[-1][1]]],
['Z', []]
]
closingLine.attrib['style'] = closingLineStyle
#draw polylines if option is enabled
if self.options.polypaths == True:
polyNode = intersectionGroup.add(inkex.PathElement())
polyNode.set('id', self.svg.get_unique_id('polypath-'))
polyNode.set('d', str(self.getPolyline(node)))
polyNode.attrib['style'] = closingLineStyle
#make dot markings at the intersection points
if self.options.highlight_intersectionpoints:
for xy in isect:
#Add a dot label for this path element
intersectionPoint = intersectionGroup.add(Circle(cx=str(xy[0]), cy=str(xy[1]), r=str(self.svg.unittouu(str(self.options.dotsize/2) + "px"))))
intersectionPoint.set('id', self.svg.get_unique_id('intersectionpoint-'))
intersectionPoint.style = intersectionPointStyle
if self.options.highlight_selfintersecting:
node.attrib['style'] = intersectionStyle
if self.options.remove_selfintersecting:
if node.getparent() is not None: #might be already been deleted by previously checked settings so check again
node.delete()
#draw intersections segment lines - useless at the moment. We could use this information to cut the original polyline to get a new curve path which included the intersection points
#isectSegs = poly_point_isect.isect_polygon_include_segments(points)
#for seg in isectSegs:
# isectSegsPath = []
# isecX = seg[0][0] #the intersection point - X
# isecY = seg[0][1] #the intersection point - Y
# isecSeg1X = seg[1][0][0][0] #the first intersection point segment - X
# isecSeg1Y = seg[1][0][0][1] #the first intersection point segment - Y
# isecSeg2X = seg[1][1][0][0] #the second intersection point segment - X
# isecSeg2Y = seg[1][1][0][1] #the second intersection point segment - Y
# isectSegsPath.append(['L', [isecSeg2X, isecSeg2Y]])
# isectSegsPath.append(['L', [isecX, isecY]])
# isectSegsPath.append(['L', [isecSeg1X, isecSeg1Y]])
# #fix the really first point. Has to be an 'M' command instead of 'L'
# isectSegsPath[0][0] = 'M'
# polySegsNode = intersectionGroup.add(inkex.PathElement())
# polySegsNode.set('id', self.svg.get_unique_id('intersectsegments-'))
# polySegsNode.set('d', str(Path(isectSegsPath)))
# polySegsNode.attrib['style'] = closingLineStyle
except AssertionError as e: # we skip AssertionError
if self.options.show_debug is True:
inkex.utils.debug("AssertionError at " + node.get('id'))
continue
except IndexError as i: # we skip IndexError
if self.options.show_debug is True:
inkex.utils.debug("IndexError at " + node.get('id'))
continue
#if the intersectionGroup was created but nothing attached we delete it again to prevent messing the SVG XML tree
if len(intersectionGroup.getchildren()) == 0:
intersectionGroupParent = intersectionGroup.getparent()
if intersectionGroupParent is not None:
intersectionGroup.delete()
#put the node into the intersectionGroup to bundle the path with it's error markers. If removal is selected we need to avoid intersectionGroup.insert(), because it will break the removal
elif self.options.remove_selfintersecting == False:
intersectionGroup.insert(0, node)
children = node.getchildren()
if children is not None:
for child in children:
self.scanContours(child)
def effect(self):
if self.options.breakapart is True:
if len(self.svg.selected) == 0:
self.breakContours(self.document.getroot())
self.scanContours(self.document.getroot())
else:
newContourSet = []
for element in self.svg.selected.values():
self.breakContours(element)
for newContours in self.replacedNodes:
self.scanContours(newContours)
else:
if len(self.svg.selected) == 0:
self.scanContours(self.document.getroot())
else:
for element in self.svg.selected.values():
self.scanContours(element)
if __name__ == '__main__':
ContourScanner().run()

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Contour Scanner And Trimmer</name>
<id>fablabchemnitz.de.contour_scanner_and_trimmer</id>
<param name="tab" type="notebook">
<page name="tab_settings" gui-text="Settings">
<label appearance="header">General</label>
<param name="show_debug" type="bool" gui-text="Show debug infos">false</param>
<param name="break_apart" type="bool" gui-text="Break apart input" gui-description="Break apart input paths into sub paths. Modifies original paths">false</param>
<param name="handle_groups" type="bool" gui-text="Handle groups" gui-description="Also looks for paths in groups which are in the current selection. Note: The generated results have a different structure (less granularity due to grouping) than directly selected paths. The colorization for non-intersected paths will be different too.">false</param>
<param name="flattenbezier" type="bool" gui-text="Quantization (flatten bezier curves to polylines)" gui-description="Convert bezier curves to polylines.">true</param>
<param name="flatness" type="float" min="0.001" max="99999.000" precision="3" gui-text="Flatness (tolerance)" gui-description="Minimum flatness = 0.001. The smaller the value the more fine segments you will get (quantization). Large values might destroy the line continuity.">0.100</param>
<param name="decimals" type="int" min="0" max="16" gui-text="Decimals" gui-description="Accuracy for sub split lines / lines trimmed by shapely (default: 3)">3</param>
<param name="snap_tolerance" type="float" min="0.01" max="10.0" gui-text="Snap tolerance" gui-description="Snap tolerance for intersection points on paths (default: 0.1)">0.1</param>
<param name="draw_subsplit" type="bool" gui-text="Draw sub split lines (for debugging purposes)" gui-description="Draws polylines">false</param>
<hbox>
<vbox>
<label appearance="header">Scanning</label>
<param name="remove_opened" type="bool" gui-text="Remove original opened paths">false</param>
<param name="remove_closed" type="bool" gui-text="Remove original closed paths">false</param>
<param name="remove_self_intersecting" type="bool" gui-text="Remove original self-intersecting paths">false</param>
<param name="highlight_opened" type="bool" gui-text="Highlight opened paths">false</param>
<param name="highlight_closed" type="bool" gui-text="Highlight closed paths">false</param>
<param name="highlight_self_intersecting" type="bool" gui-text="Highlight 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="Visualize self-intersecting path points">false</param>
<param name="visualize_global_intersections" type="bool" gui-text="Visualize global intersection points" gui-description="Will also contain self-intersecting points!">false</param>
</vbox>
<separator/>
<vbox>
<label appearance="header">Trimming</label>
<param name="path_types" type="optiongroup" appearance="combo" gui-text="Trimming selection" gui-description="Trim open paths by other open paths, closed paths by other closed paths, or all paths by all other paths">
<option value="both">all:all paths</option>
<option value="open_paths">open:open paths</option>
<option value="closed_paths">closed:closed paths</option>
</param>
<param name="draw_trimmed" type="bool" gui-text="Draw trimmed lines">false</param>
<param name="combine_nonintersects" type="bool" gui-text="Chain + combine non-intersected lines">true</param>
<param name="remove_duplicates" type="bool" gui-text="Remove duplicate trim lines">true</param>
<param name="reverse_removal_order" type="bool" gui-text="Reverse removal order" gui-description="Reverses the order of removal. Relevant for keeping certain styles of elements">false</param>
<param name="bezier_trimming" type="bool" gui-text="Trim original beziers (not working yet)" gui-description="If enabled we try to split the original bezier paths at the intersections points by finding the correct bezier segments and calculating t parameters from trimmed sub split lines. Not working yet. Will just print debug info if debug is enabled.">true</param>
<param name="keep_original_after_trim" type="bool" gui-text="Keep original paths after trimming">false</param>
</vbox>
</hbox>
</page>
<page name="tab_style" gui-text="Style">
<hbox>
<vbox>
<label appearance="header">General Style</label>
<param name="strokewidth" min="0.0" max="10000.0" gui-text="Stroke width (px)" gui-description="Applies For sub split lines and trimmed lines" type="float">1.0</param>
<param name="dotsize_intersections" type="int" min="0" max="10000" gui-text="Dot size (px)" gui-description="For self-intersecting and global intersection points">30</param>
<param name="removefillsetstroke" type="bool" gui-text="Remove fill and define stroke" gui-description="Modifies original path style">false</param>
<param name="apply_original_style" type="bool" gui-text="Original style for trimmed lines" gui-description="Apply original path style to trimmed lines.">true</param>
<label appearance="header">Scanning Colors</label>
<param name="color_opened" type="color" appearance="colorbutton" gui-text="Color for opened contours">4012452351</param>
<param name="color_closed" type="color" appearance="colorbutton" gui-text="Color for closed contours">2330080511</param>
<param name="color_self_intersecting_paths" type="color" appearance="colorbutton" gui-text="Color for self-intersecting contours">2593756927</param>
<param name="color_subsplit" type="color" appearance="colorbutton" gui-text="Color for sub split lines">1630897151</param>
<param name="color_self_intersections" type="color" appearance="colorbutton" gui-text="Color for self-intersecting line points">6320383</param>
<param name="color_global_intersections" type="color" appearance="colorbutton" gui-text="Color for global intersection points">4239343359</param>
</vbox>
<separator/>
<vbox>
<label appearance="header">Trimming Colors</label>
<param name="color_trimmed" type="color" appearance="colorbutton" gui-text="Color for trimmed lines">3227634687</param>
<param name="color_combined" type="color" appearance="colorbutton" gui-text="Color for non-intersected lines" gui-description="Colorize non-trimmed lines differently than the trimmed ones. Does not apply if 'Original style for trimmed lines' is enabled">1923076095</param>
<param name="color_nonintersected" type="color" appearance="colorbutton" gui-text="Color for non-intersected paths" gui-description="Colorize the complete path in case it does not contain any trim. Does not apply if 'Original style for trimmed lines' is enabled">3045284607</param>
</vbox>
</hbox>
</page>
<page name="tab_tips" gui-text="Tips">
<label xml:space="preserve">- Helps to find duplicate lines and to visualize intersections
- Works for self-intersecting lines too
- Uses Bentley-Ottmann algorithm to detect intersections
- Allows to separate different contour types by colors
- Works with paths which have Live Path Effects (LPE)
- Does not find overlapping colinear lines (sweep line algorithm does not intersect them)
Tips:
- Convert your strokes and objects to paths before
- Does not work for clones. You will need to unlink them before
- Use extensions to filter short/unrequired lines
- Use extensions to purge or repair invalid paths
- Use 'Path > Simplify' or hit 'CTRL + L' to simplify the trimmed result. With a fine quantization setting the simplified paths will be nearly identical to the original path (except the position of control points)</label>
<label appearance="header">Do not select too much paths at once if you have got a fine settings for quantization. This extension is slow and might calculate hours on ultra high configurations.</label>
</page>
<page name="tab_about" gui-text="About">
<label appearance="header">Contour Scanner And Trimmer</label>
<label>A utility to scan, flatten, split and trim lines.</label>
<label>2020 - 2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/>
<label appearance="header">Online Documentation</label>
<label appearance="url">https://y.stadtfabrikanten.org/contourscannerandtrimmer</label>
<spacer/>
<label appearance="header">Contributing</label>
<label appearance="url">https://gitea.fablabchemnitz.de/MarioVoigt/mightyscape-1.X</label>
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
<spacer/>
<label appearance="header">Third Party Modules</label>
<label appearance="url">https://github.com/ideasman42/isect_segments-bentley_ottmann</label>
<spacer/>
<label appearance="header">MightyScape Extension Collection</label>
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
</page>
<page name="tab_donate" gui-text="Donate">
<label appearance="header">Coffee + Pizza</label>
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
<spacer/>
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
<spacer/>
<label>Thanks for using our extension and helping us!</label>
<image>../000_about_fablabchemnitz.svg</image>
</page>
</param>
<effect needs-live-preview="true">
<object-type>all</object-type>
<effects-menu>
<submenu name="Contour Scanner And Trimmer"/>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">contour_scanner_and_trimmer.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,701 @@
#!/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')
replacedelement.set('d', CubicSuperPath(subpath))
replacedelement.set('id', oldId + str(idSuffix).zfill(5))
parent.insert(idx, replacedelement)
idSuffix += 1
breakelements.append(replacedelement)
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/1wheel/464141fe9b940153e636
'''
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()