changed Contour Scanner to Contour Scanner and Trimmer
This commit is contained in:
parent
7cced1b41f
commit
a2f8e71e2b
@ -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>
|
@ -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()
|
@ -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>
|
@ -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()
|
Reference in New Issue
Block a user